Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions meteor/__mocks__/defaultCollectionObjects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,10 @@ export function defaultStudio(_id: StudioId): DBStudio {
routeSetsWithOverrides: wrapDefaultObject({}),
routeSetExclusivityGroupsWithOverrides: wrapDefaultObject({}),
packageContainersWithOverrides: wrapDefaultObject({}),
previewContainerIds: [],
thumbnailContainerIds: [],
packageContainerIdsWithOverrides: wrapDefaultObject({
previewContainerIds: [],
thumbnailContainerIds: [],
}),
peripheralDeviceSettings: {
deviceSettings: wrapDefaultObject({}),
playoutDevices: wrapDefaultObject({}),
Expand Down
6 changes: 4 additions & 2 deletions meteor/server/__tests__/cronjobs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -592,8 +592,10 @@ describe('cronjobs', () => {
routeSetsWithOverrides: newObjectWithOverrides({}),
routeSetExclusivityGroupsWithOverrides: newObjectWithOverrides({}),
packageContainersWithOverrides: newObjectWithOverrides({}),
previewContainerIds: [],
thumbnailContainerIds: [],
packageContainerIdsWithOverrides: newObjectWithOverrides({
previewContainerIds: [],
thumbnailContainerIds: [],
}),
peripheralDeviceSettings: {
deviceSettings: newObjectWithOverrides({}),
ingestDevices: newObjectWithOverrides({}),
Expand Down
6 changes: 4 additions & 2 deletions meteor/server/api/rest/v1/typeConversion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -359,8 +359,10 @@ export async function buildStudioFromResolved({
_rundownVersionHash: '',
routeSetExclusivityGroupsWithOverrides: wrapDefaultObject({}),
packageContainersWithOverrides: wrapDefaultObject({}),
previewContainerIds: [],
thumbnailContainerIds: [],
packageContainerIdsWithOverrides: wrapDefaultObject({
previewContainerIds: [],
thumbnailContainerIds: [],
}),
peripheralDeviceSettings: {
deviceSettings: wrapDefaultObject({}),
playoutDevices: wrapDefaultObject({}),
Expand Down
6 changes: 4 additions & 2 deletions meteor/server/api/studio/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,10 @@ export async function insertStudioInner(newId?: StudioId): Promise<StudioId> {
routeSetsWithOverrides: wrapDefaultObject({}),
routeSetExclusivityGroupsWithOverrides: wrapDefaultObject({}),
packageContainersWithOverrides: wrapDefaultObject({}),
thumbnailContainerIds: [],
previewContainerIds: [],
packageContainerIdsWithOverrides: wrapDefaultObject({
thumbnailContainerIds: [],
previewContainerIds: [],
}),
peripheralDeviceSettings: {
deviceSettings: wrapDefaultObject({}),
playoutDevices: wrapDefaultObject({}),
Expand Down
6 changes: 4 additions & 2 deletions meteor/server/migration/0_1_0.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,10 @@ export const addSteps = addMigrationSteps('0.1.0', [
routeSetsWithOverrides: wrapDefaultObject({}),
routeSetExclusivityGroupsWithOverrides: wrapDefaultObject({}),
packageContainersWithOverrides: wrapDefaultObject({}),
thumbnailContainerIds: [],
previewContainerIds: [],
packageContainerIdsWithOverrides: wrapDefaultObject({
thumbnailContainerIds: [],
previewContainerIds: [],
}),
peripheralDeviceSettings: {
deviceSettings: wrapDefaultObject({}),
playoutDevices: wrapDefaultObject({}),
Expand Down
8 changes: 7 additions & 1 deletion meteor/server/migration/X_X_X.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
} from '@sofie-automation/corelib/dist/dataModel/ExpectedPackages'
import { BucketId, RundownId } from '@sofie-automation/corelib/dist/dataModel/Ids'
import { assertNever, Complete } from '@sofie-automation/corelib/dist/lib'
import { ContainerIdsToObjectWithOverridesMigrationStep } from './steps/ContainerIdsToObjectWithOverridesMigrationStep'

/*
* **************************************************************************************
Expand All @@ -36,7 +37,9 @@ export const addSteps = addMigrationSteps(CURRENT_SYSTEM_VERSION, [
['expectedMediaItems', 'mediaWorkFlows', 'mediaWorkFlowSteps'].includes(c.name)
)
if (collectionsToDrop.length > 0) {
return `There are ${collectionsToDrop.length} obsolete collections to be removed: ${collectionsToDrop.map((c) => c.name).join(', ')}`
return `There are ${collectionsToDrop.length} obsolete collections to be removed: ${collectionsToDrop
.map((c) => c.name)
.join(', ')}`
}

return false
Expand Down Expand Up @@ -195,4 +198,7 @@ export const addSteps = addMigrationSteps(CURRENT_SYSTEM_VERSION, [
}
},
},
// Add your migration here

new ContainerIdsToObjectWithOverridesMigrationStep(),
])
136 changes: 49 additions & 87 deletions meteor/server/migration/__tests__/migrations.test.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
import _ from 'underscore'
import { setupEmptyEnvironment } from '../../../__mocks__/helpers/database'
import { setupEmptyEnvironment, setupMockStudio } from '../../../__mocks__/helpers/database'
import { ICoreSystem, GENESIS_SYSTEM_VERSION } from '@sofie-automation/meteor-lib/dist/collections/CoreSystem'
import { clearMigrationSteps, addMigrationSteps, prepareMigration, PreparedMigration } from '../databaseMigration'
import { CURRENT_SYSTEM_VERSION } from '../currentSystemVersion'
import { RunMigrationResult, GetMigrationStatusResult } from '@sofie-automation/meteor-lib/dist/api/migration'
import { literal } from '@sofie-automation/corelib/dist/lib'
import { protectString } from '@sofie-automation/corelib/dist/protectedString'
import { MigrationStepInputResult } from '@sofie-automation/blueprints-integration'
import { MigrationStepCore, MigrationStepInputResult } from '@sofie-automation/blueprints-integration'
import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio'
import { MeteorCall } from '../../api/methods'
import { wrapDefaultObject } from '@sofie-automation/corelib/dist/settings/objectWithOverrides'
import { ShowStyleBases, ShowStyleVariants, Studios } from '../../collections'
import { getCoreSystemAsync } from '../../coreSystem/collection'
import { DEFAULT_MINIMUM_TAKE_SPAN } from '@sofie-automation/shared-lib/dist/core/constants'
import fs from 'fs'

require('../../api/peripheralDevice.ts') // include in order to create the Meteor methods needed
Expand Down Expand Up @@ -107,35 +106,8 @@ describe('Migrations', () => {
return false
},
migrate: async () => {
await Studios.insertAsync({
await setupMockStudio({
_id: protectString('studioMock2'),
name: 'Default studio',
supportedShowStyleBase: [],
settingsWithOverrides: wrapDefaultObject({
mediaPreviewsUrl: '',
frameRate: 25,
minimumTakeSpan: DEFAULT_MINIMUM_TAKE_SPAN,
allowHold: true,
allowPieceDirectPlay: true,
enableBuckets: true,
enableEvaluationForm: true,
}),
mappingsWithOverrides: wrapDefaultObject({}),
blueprintConfigWithOverrides: wrapDefaultObject({}),
_rundownVersionHash: '',
routeSetsWithOverrides: wrapDefaultObject({}),
routeSetExclusivityGroupsWithOverrides: wrapDefaultObject({}),
packageContainersWithOverrides: wrapDefaultObject({}),
previewContainerIds: [],
thumbnailContainerIds: [],
peripheralDeviceSettings: {
deviceSettings: wrapDefaultObject({}),
playoutDevices: wrapDefaultObject({}),
ingestDevices: wrapDefaultObject({}),
inputDevices: wrapDefaultObject({}),
},
lastBlueprintConfig: undefined,
lastBlueprintFixUpHash: undefined,
})
},
},
Expand All @@ -149,35 +121,8 @@ describe('Migrations', () => {
return false
},
migrate: async () => {
await Studios.insertAsync({
await setupMockStudio({
_id: protectString('studioMock3'),
name: 'Default studio',
supportedShowStyleBase: [],
settingsWithOverrides: wrapDefaultObject({
mediaPreviewsUrl: '',
frameRate: 25,
minimumTakeSpan: DEFAULT_MINIMUM_TAKE_SPAN,
allowHold: true,
allowPieceDirectPlay: true,
enableBuckets: true,
enableEvaluationForm: true,
}),
mappingsWithOverrides: wrapDefaultObject({}),
blueprintConfigWithOverrides: wrapDefaultObject({}),
_rundownVersionHash: '',
routeSetsWithOverrides: wrapDefaultObject({}),
routeSetExclusivityGroupsWithOverrides: wrapDefaultObject({}),
packageContainersWithOverrides: wrapDefaultObject({}),
previewContainerIds: [],
thumbnailContainerIds: [],
peripheralDeviceSettings: {
deviceSettings: wrapDefaultObject({}),
playoutDevices: wrapDefaultObject({}),
ingestDevices: wrapDefaultObject({}),
inputDevices: wrapDefaultObject({}),
},
lastBlueprintConfig: undefined,
lastBlueprintFixUpHash: undefined,
})
},
},
Expand All @@ -191,35 +136,8 @@ describe('Migrations', () => {
return false
},
migrate: async () => {
await Studios.insertAsync({
await setupMockStudio({
_id: protectString('studioMock1'),
name: 'Default studio',
supportedShowStyleBase: [],
settingsWithOverrides: wrapDefaultObject({
mediaPreviewsUrl: '',
frameRate: 25,
minimumTakeSpan: DEFAULT_MINIMUM_TAKE_SPAN,
allowHold: true,
allowPieceDirectPlay: true,
enableBuckets: true,
enableEvaluationForm: true,
}),
mappingsWithOverrides: wrapDefaultObject({}),
blueprintConfigWithOverrides: wrapDefaultObject({}),
_rundownVersionHash: '',
routeSetsWithOverrides: wrapDefaultObject({}),
routeSetExclusivityGroupsWithOverrides: wrapDefaultObject({}),
packageContainersWithOverrides: wrapDefaultObject({}),
previewContainerIds: [],
thumbnailContainerIds: [],
peripheralDeviceSettings: {
deviceSettings: wrapDefaultObject({}),
playoutDevices: wrapDefaultObject({}),
ingestDevices: wrapDefaultObject({}),
inputDevices: wrapDefaultObject({}),
},
lastBlueprintConfig: undefined,
lastBlueprintFixUpHash: undefined,
})
},
},
Expand Down Expand Up @@ -322,4 +240,48 @@ describe('Migrations', () => {
expect(steps.indexOf(myShowStyleMockStep3)).toEqual(8)
*/
})

test('Class-based migration steps work with proper binding', async () => {
await MeteorCall.migration.resetDatabaseVersions()
clearMigrationSteps()

// Create a migration step class that uses instance properties
class TestClassMigrationStep implements Omit<MigrationStepCore, 'version'> {
public readonly id = 'classBasedMigrationTest'
public readonly canBeRunAutomatically = true
public testValue = 'initialized'

public async validate(): Promise<boolean | string> {
// If 'this' is not bound, testValue will be undefined
return this.testValue === 'initialized' ? 'Migration needed' : false
}

public async migrate(): Promise<void> {
// If 'this' is not bound, this will throw or fail to update the correct instance
this.testValue = 'migrated'
}
}

// Instantiate the step so we can check it later
const step = new TestClassMigrationStep()
addMigrationSteps('1.0.0', [step])()

// Prepare migration to ensure it's detected
const migration = await prepareMigration(true)
expect(migration.migrationNeeded).toEqual(true)
expect(_.find(migration.steps, (s) => s.id === 'classBasedMigrationTest')).toBeTruthy()

// Run the migration to verify that methods are properly bound
const migrationStatus: GetMigrationStatusResult = await MeteorCall.migration.getMigrationStatus()
const migrationResult: RunMigrationResult = await MeteorCall.migration.runMigration(
migrationStatus.migration.chunks,
migrationStatus.migration.hash,
userInput(migrationStatus)
)

expect(migrationResult.migrationCompleted).toEqual(true)

// Verify that migrate() was called and 'this' was correctly bound
expect(step.testValue).toEqual('migrated')
})
})
14 changes: 6 additions & 8 deletions meteor/server/migration/databaseMigration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,8 @@ const coreMigrationSteps: Array<MigrationStepCore> = []
export function addMigrationSteps(version: string, steps: Array<Omit<MigrationStepCore, 'version'>>) {
return (): void => {
for (const step of steps) {
coreMigrationSteps.push({
...step,
version: version,
})
;(step as MigrationStepCore).version = version
coreMigrationSteps.push(step as MigrationStepCore)
}
}
}
Expand Down Expand Up @@ -135,9 +133,9 @@ export async function prepareMigration(returnAllChunks?: boolean): Promise<Prepa
allMigrationSteps.push({
id: step.id,
overrideSteps: step.overrideSteps,
validate: step.validate,
validate: step.validate.bind(step),
canBeRunAutomatically: step.canBeRunAutomatically,
migrate: step.migrate,
migrate: step.migrate?.bind(step),
input: step.input,
dependOnResultFrom: step.dependOnResultFrom,
version: step.version,
Expand Down Expand Up @@ -188,9 +186,9 @@ export async function prepareMigration(returnAllChunks?: boolean): Promise<Prepa
prefixIdsOnStep('blueprint_' + blueprint._id + '_system_', {
id: step.id,
overrideSteps: step.overrideSteps,
validate: step.validate,
validate: step.validate.bind(step),
canBeRunAutomatically: step.canBeRunAutomatically,
migrate: step.migrate,
migrate: step.migrate?.bind(step),
input: step.input,
dependOnResultFrom: step.dependOnResultFrom,
version: step.version,
Expand Down
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be good to have this in a folder matching the X_X_X naming scheme, so that we can more easily keep track of which steps are being for each version. (Which will help us when we want to clean migrations from some really old versions again)

I like the class approach though, seems like it will help with some of the duplication 'problem' that bugs me with the other approach (not enough to consider changing it though)

Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { MigrationStepCore } from '@sofie-automation/blueprints-integration'
import { Studios } from '../../collections'
import {
convertObjectIntoOverrides,
ObjectWithOverrides,
} from '@sofie-automation/corelib/dist/settings/objectWithOverrides'
import { StudioPackageContainerIds } from '@sofie-automation/shared-lib/dist/core/model/PackageContainer'

export class ContainerIdsToObjectWithOverridesMigrationStep implements Omit<MigrationStepCore, 'version'> {
public readonly id = `convert previewContainerIds to ObjectWithOverrides`
public readonly canBeRunAutomatically = true

public async validate(): Promise<boolean | string> {
const studios = await this.findStudiosToMigrate()

if (studios.length) {
return 'previewContainerIds and thumbnailContainerIds must be converted to an ObjectWithOverrides'
}

return false
}

public async migrate(): Promise<void> {
const studios = await this.findStudiosToMigrate()

for (const studio of studios) {
// @ts-expect-error previewContainerIds is typed as string[]
const oldPreviewContainerIds = studio.previewContainerIds
// @ts-expect-error thumbnailContainerIds is typed as string[]
const oldThumbnailContainerIds = studio.thumbnailContainerIds

const newPackageContainers = convertObjectIntoOverrides({
previewContainerIds: oldPreviewContainerIds ?? [],
thumbnailContainerIds: oldThumbnailContainerIds ?? [],
} satisfies StudioPackageContainerIds) as ObjectWithOverrides<StudioPackageContainerIds>

await Studios.updateAsync(studio._id, {
$set: {
packageContainerIdsWithOverrides: newPackageContainers,
},
$unset: {
previewContainerIds: 1,
thumbnailContainerIds: 1,
},
})
}
}

private async findStudiosToMigrate() {
return Studios.findFetchAsync({
packageContainerIdsWithOverrides: { $exists: false },
})
}
}
Loading
Loading