Skip to content

Commit 8c10a53

Browse files
authored
feat: allow blueprints to specify preview and thumbnail containter ids in applyConfig (Sofie-Automation#1613)
* feat(EAV-663): allow blueprints to specify preview and thumbnail containter ids in `applyConfig` * refactor(EAV-663): rename `packageContainerIds` to `packageContainerSettings` * refactor(EAV-663): move migration step to version folder * chore(EAV-667): remove `console.log` and lint * fix(EAV-663): add `packageContainerSettings` defaults in case blueprints don't provide them anymore If blueprints don't provide packageContainerSettings anymore, reset the defaults to a default, so that stale defaults previously returned by the blueprints are not kept forever
1 parent 22916fd commit 8c10a53

23 files changed

Lines changed: 366 additions & 175 deletions

File tree

meteor/__mocks__/defaultCollectionObjects.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,8 +116,10 @@ export function defaultStudio(_id: StudioId): DBStudio {
116116
routeSetsWithOverrides: wrapDefaultObject({}),
117117
routeSetExclusivityGroupsWithOverrides: wrapDefaultObject({}),
118118
packageContainersWithOverrides: wrapDefaultObject({}),
119-
previewContainerIds: [],
120-
thumbnailContainerIds: [],
119+
packageContainerSettingsWithOverrides: wrapDefaultObject({
120+
previewContainerIds: [],
121+
thumbnailContainerIds: [],
122+
}),
121123
peripheralDeviceSettings: {
122124
deviceSettings: wrapDefaultObject({}),
123125
playoutDevices: wrapDefaultObject({}),

meteor/server/__tests__/cronjobs.test.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -592,8 +592,10 @@ describe('cronjobs', () => {
592592
routeSetsWithOverrides: newObjectWithOverrides({}),
593593
routeSetExclusivityGroupsWithOverrides: newObjectWithOverrides({}),
594594
packageContainersWithOverrides: newObjectWithOverrides({}),
595-
previewContainerIds: [],
596-
thumbnailContainerIds: [],
595+
packageContainerSettingsWithOverrides: newObjectWithOverrides({
596+
previewContainerIds: [],
597+
thumbnailContainerIds: [],
598+
}),
597599
peripheralDeviceSettings: {
598600
deviceSettings: newObjectWithOverrides({}),
599601
ingestDevices: newObjectWithOverrides({}),

meteor/server/api/rest/v1/typeConversion.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -359,8 +359,10 @@ export async function buildStudioFromResolved({
359359
_rundownVersionHash: '',
360360
routeSetExclusivityGroupsWithOverrides: wrapDefaultObject({}),
361361
packageContainersWithOverrides: wrapDefaultObject({}),
362-
previewContainerIds: [],
363-
thumbnailContainerIds: [],
362+
packageContainerSettingsWithOverrides: wrapDefaultObject({
363+
previewContainerIds: [],
364+
thumbnailContainerIds: [],
365+
}),
364366
peripheralDeviceSettings: {
365367
deviceSettings: wrapDefaultObject({}),
366368
playoutDevices: wrapDefaultObject({}),

meteor/server/api/studio/api.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,10 @@ export async function insertStudioInner(newId?: StudioId): Promise<StudioId> {
6969
routeSetsWithOverrides: wrapDefaultObject({}),
7070
routeSetExclusivityGroupsWithOverrides: wrapDefaultObject({}),
7171
packageContainersWithOverrides: wrapDefaultObject({}),
72-
thumbnailContainerIds: [],
73-
previewContainerIds: [],
72+
packageContainerSettingsWithOverrides: wrapDefaultObject({
73+
thumbnailContainerIds: [],
74+
previewContainerIds: [],
75+
}),
7476
peripheralDeviceSettings: {
7577
deviceSettings: wrapDefaultObject({}),
7678
playoutDevices: wrapDefaultObject({}),

meteor/server/migration/0_1_0.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,10 @@ export const addSteps = addMigrationSteps('0.1.0', [
4444
routeSetsWithOverrides: wrapDefaultObject({}),
4545
routeSetExclusivityGroupsWithOverrides: wrapDefaultObject({}),
4646
packageContainersWithOverrides: wrapDefaultObject({}),
47-
thumbnailContainerIds: [],
48-
previewContainerIds: [],
47+
packageContainerSettingsWithOverrides: wrapDefaultObject({
48+
thumbnailContainerIds: [],
49+
previewContainerIds: [],
50+
}),
4951
peripheralDeviceSettings: {
5052
deviceSettings: wrapDefaultObject({}),
5153
playoutDevices: wrapDefaultObject({}),

meteor/server/migration/X_X_X.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
} from '@sofie-automation/corelib/dist/dataModel/ExpectedPackages'
1111
import { BucketId, RundownId } from '@sofie-automation/corelib/dist/dataModel/Ids'
1212
import { assertNever, Complete } from '@sofie-automation/corelib/dist/lib'
13+
import { ContainerIdsToObjectWithOverridesMigrationStep } from './steps/X_X_X/ContainerIdsToObjectWithOverridesMigrationStep'
1314

1415
/*
1516
* **************************************************************************************
@@ -36,7 +37,9 @@ export const addSteps = addMigrationSteps(CURRENT_SYSTEM_VERSION, [
3637
['expectedMediaItems', 'mediaWorkFlows', 'mediaWorkFlowSteps'].includes(c.name)
3738
)
3839
if (collectionsToDrop.length > 0) {
39-
return `There are ${collectionsToDrop.length} obsolete collections to be removed: ${collectionsToDrop.map((c) => c.name).join(', ')}`
40+
return `There are ${collectionsToDrop.length} obsolete collections to be removed: ${collectionsToDrop
41+
.map((c) => c.name)
42+
.join(', ')}`
4043
}
4144

4245
return false
@@ -195,4 +198,7 @@ export const addSteps = addMigrationSteps(CURRENT_SYSTEM_VERSION, [
195198
}
196199
},
197200
},
201+
// Add your migration here
202+
203+
new ContainerIdsToObjectWithOverridesMigrationStep(),
198204
])

meteor/server/migration/__tests__/migrations.test.ts

Lines changed: 49 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
11
import _ from 'underscore'
2-
import { setupEmptyEnvironment } from '../../../__mocks__/helpers/database'
2+
import { setupEmptyEnvironment, setupMockStudio } from '../../../__mocks__/helpers/database'
33
import { ICoreSystem, GENESIS_SYSTEM_VERSION } from '@sofie-automation/meteor-lib/dist/collections/CoreSystem'
44
import { clearMigrationSteps, addMigrationSteps, prepareMigration, PreparedMigration } from '../databaseMigration'
55
import { CURRENT_SYSTEM_VERSION } from '../currentSystemVersion'
66
import { RunMigrationResult, GetMigrationStatusResult } from '@sofie-automation/meteor-lib/dist/api/migration'
77
import { literal } from '@sofie-automation/corelib/dist/lib'
88
import { protectString } from '@sofie-automation/corelib/dist/protectedString'
9-
import { MigrationStepInputResult } from '@sofie-automation/blueprints-integration'
9+
import { MigrationStepCore, MigrationStepInputResult } from '@sofie-automation/blueprints-integration'
1010
import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio'
1111
import { MeteorCall } from '../../api/methods'
1212
import { wrapDefaultObject } from '@sofie-automation/corelib/dist/settings/objectWithOverrides'
1313
import { ShowStyleBases, ShowStyleVariants, Studios } from '../../collections'
1414
import { getCoreSystemAsync } from '../../coreSystem/collection'
15-
import { DEFAULT_MINIMUM_TAKE_SPAN } from '@sofie-automation/shared-lib/dist/core/constants'
1615
import fs from 'fs'
1716

1817
require('../../api/peripheralDevice.ts') // include in order to create the Meteor methods needed
@@ -107,35 +106,8 @@ describe('Migrations', () => {
107106
return false
108107
},
109108
migrate: async () => {
110-
await Studios.insertAsync({
109+
await setupMockStudio({
111110
_id: protectString('studioMock2'),
112-
name: 'Default studio',
113-
supportedShowStyleBase: [],
114-
settingsWithOverrides: wrapDefaultObject({
115-
mediaPreviewsUrl: '',
116-
frameRate: 25,
117-
minimumTakeSpan: DEFAULT_MINIMUM_TAKE_SPAN,
118-
allowHold: true,
119-
allowPieceDirectPlay: true,
120-
enableBuckets: true,
121-
enableEvaluationForm: true,
122-
}),
123-
mappingsWithOverrides: wrapDefaultObject({}),
124-
blueprintConfigWithOverrides: wrapDefaultObject({}),
125-
_rundownVersionHash: '',
126-
routeSetsWithOverrides: wrapDefaultObject({}),
127-
routeSetExclusivityGroupsWithOverrides: wrapDefaultObject({}),
128-
packageContainersWithOverrides: wrapDefaultObject({}),
129-
previewContainerIds: [],
130-
thumbnailContainerIds: [],
131-
peripheralDeviceSettings: {
132-
deviceSettings: wrapDefaultObject({}),
133-
playoutDevices: wrapDefaultObject({}),
134-
ingestDevices: wrapDefaultObject({}),
135-
inputDevices: wrapDefaultObject({}),
136-
},
137-
lastBlueprintConfig: undefined,
138-
lastBlueprintFixUpHash: undefined,
139111
})
140112
},
141113
},
@@ -149,35 +121,8 @@ describe('Migrations', () => {
149121
return false
150122
},
151123
migrate: async () => {
152-
await Studios.insertAsync({
124+
await setupMockStudio({
153125
_id: protectString('studioMock3'),
154-
name: 'Default studio',
155-
supportedShowStyleBase: [],
156-
settingsWithOverrides: wrapDefaultObject({
157-
mediaPreviewsUrl: '',
158-
frameRate: 25,
159-
minimumTakeSpan: DEFAULT_MINIMUM_TAKE_SPAN,
160-
allowHold: true,
161-
allowPieceDirectPlay: true,
162-
enableBuckets: true,
163-
enableEvaluationForm: true,
164-
}),
165-
mappingsWithOverrides: wrapDefaultObject({}),
166-
blueprintConfigWithOverrides: wrapDefaultObject({}),
167-
_rundownVersionHash: '',
168-
routeSetsWithOverrides: wrapDefaultObject({}),
169-
routeSetExclusivityGroupsWithOverrides: wrapDefaultObject({}),
170-
packageContainersWithOverrides: wrapDefaultObject({}),
171-
previewContainerIds: [],
172-
thumbnailContainerIds: [],
173-
peripheralDeviceSettings: {
174-
deviceSettings: wrapDefaultObject({}),
175-
playoutDevices: wrapDefaultObject({}),
176-
ingestDevices: wrapDefaultObject({}),
177-
inputDevices: wrapDefaultObject({}),
178-
},
179-
lastBlueprintConfig: undefined,
180-
lastBlueprintFixUpHash: undefined,
181126
})
182127
},
183128
},
@@ -191,35 +136,8 @@ describe('Migrations', () => {
191136
return false
192137
},
193138
migrate: async () => {
194-
await Studios.insertAsync({
139+
await setupMockStudio({
195140
_id: protectString('studioMock1'),
196-
name: 'Default studio',
197-
supportedShowStyleBase: [],
198-
settingsWithOverrides: wrapDefaultObject({
199-
mediaPreviewsUrl: '',
200-
frameRate: 25,
201-
minimumTakeSpan: DEFAULT_MINIMUM_TAKE_SPAN,
202-
allowHold: true,
203-
allowPieceDirectPlay: true,
204-
enableBuckets: true,
205-
enableEvaluationForm: true,
206-
}),
207-
mappingsWithOverrides: wrapDefaultObject({}),
208-
blueprintConfigWithOverrides: wrapDefaultObject({}),
209-
_rundownVersionHash: '',
210-
routeSetsWithOverrides: wrapDefaultObject({}),
211-
routeSetExclusivityGroupsWithOverrides: wrapDefaultObject({}),
212-
packageContainersWithOverrides: wrapDefaultObject({}),
213-
previewContainerIds: [],
214-
thumbnailContainerIds: [],
215-
peripheralDeviceSettings: {
216-
deviceSettings: wrapDefaultObject({}),
217-
playoutDevices: wrapDefaultObject({}),
218-
ingestDevices: wrapDefaultObject({}),
219-
inputDevices: wrapDefaultObject({}),
220-
},
221-
lastBlueprintConfig: undefined,
222-
lastBlueprintFixUpHash: undefined,
223141
})
224142
},
225143
},
@@ -322,4 +240,48 @@ describe('Migrations', () => {
322240
expect(steps.indexOf(myShowStyleMockStep3)).toEqual(8)
323241
*/
324242
})
243+
244+
test('Class-based migration steps work with proper binding', async () => {
245+
await MeteorCall.migration.resetDatabaseVersions()
246+
clearMigrationSteps()
247+
248+
// Create a migration step class that uses instance properties
249+
class TestClassMigrationStep implements Omit<MigrationStepCore, 'version'> {
250+
public readonly id = 'classBasedMigrationTest'
251+
public readonly canBeRunAutomatically = true
252+
public testValue = 'initialized'
253+
254+
public async validate(): Promise<boolean | string> {
255+
// If 'this' is not bound, testValue will be undefined
256+
return this.testValue === 'initialized' ? 'Migration needed' : false
257+
}
258+
259+
public async migrate(): Promise<void> {
260+
// If 'this' is not bound, this will throw or fail to update the correct instance
261+
this.testValue = 'migrated'
262+
}
263+
}
264+
265+
// Instantiate the step so we can check it later
266+
const step = new TestClassMigrationStep()
267+
addMigrationSteps('1.0.0', [step])()
268+
269+
// Prepare migration to ensure it's detected
270+
const migration = await prepareMigration(true)
271+
expect(migration.migrationNeeded).toEqual(true)
272+
expect(_.find(migration.steps, (s) => s.id === 'classBasedMigrationTest')).toBeTruthy()
273+
274+
// Run the migration to verify that methods are properly bound
275+
const migrationStatus: GetMigrationStatusResult = await MeteorCall.migration.getMigrationStatus()
276+
const migrationResult: RunMigrationResult = await MeteorCall.migration.runMigration(
277+
migrationStatus.migration.chunks,
278+
migrationStatus.migration.hash,
279+
userInput(migrationStatus)
280+
)
281+
282+
expect(migrationResult.migrationCompleted).toEqual(true)
283+
284+
// Verify that migrate() was called and 'this' was correctly bound
285+
expect(step.testValue).toEqual('migrated')
286+
})
325287
})

meteor/server/migration/databaseMigration.ts

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -82,10 +82,8 @@ const coreMigrationSteps: Array<MigrationStepCore> = []
8282
export function addMigrationSteps(version: string, steps: Array<Omit<MigrationStepCore, 'version'>>) {
8383
return (): void => {
8484
for (const step of steps) {
85-
coreMigrationSteps.push({
86-
...step,
87-
version: version,
88-
})
85+
;(step as MigrationStepCore).version = version
86+
coreMigrationSteps.push(step as MigrationStepCore)
8987
}
9088
}
9189
}
@@ -135,9 +133,9 @@ export async function prepareMigration(returnAllChunks?: boolean): Promise<Prepa
135133
allMigrationSteps.push({
136134
id: step.id,
137135
overrideSteps: step.overrideSteps,
138-
validate: step.validate,
136+
validate: step.validate.bind(step),
139137
canBeRunAutomatically: step.canBeRunAutomatically,
140-
migrate: step.migrate,
138+
migrate: step.migrate?.bind(step),
141139
input: step.input,
142140
dependOnResultFrom: step.dependOnResultFrom,
143141
version: step.version,
@@ -188,9 +186,9 @@ export async function prepareMigration(returnAllChunks?: boolean): Promise<Prepa
188186
prefixIdsOnStep('blueprint_' + blueprint._id + '_system_', {
189187
id: step.id,
190188
overrideSteps: step.overrideSteps,
191-
validate: step.validate,
189+
validate: step.validate.bind(step),
192190
canBeRunAutomatically: step.canBeRunAutomatically,
193-
migrate: step.migrate,
191+
migrate: step.migrate?.bind(step),
194192
input: step.input,
195193
dependOnResultFrom: step.dependOnResultFrom,
196194
version: step.version,
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { MigrationStepCore } from '@sofie-automation/blueprints-integration'
2+
import { Studios } from '../../../collections'
3+
import {
4+
convertObjectIntoOverrides,
5+
ObjectWithOverrides,
6+
} from '@sofie-automation/corelib/dist/settings/objectWithOverrides'
7+
import { StudioPackageContainerSettings } from '@sofie-automation/shared-lib/dist/core/model/PackageContainer'
8+
9+
export class ContainerIdsToObjectWithOverridesMigrationStep implements Omit<MigrationStepCore, 'version'> {
10+
public readonly id = `convert previewContainerIds to ObjectWithOverrides`
11+
public readonly canBeRunAutomatically = true
12+
13+
public async validate(): Promise<boolean | string> {
14+
const studios = await this.findStudiosToMigrate()
15+
16+
if (studios.length) {
17+
return 'previewContainerIds and thumbnailContainerIds must be converted to an ObjectWithOverrides'
18+
}
19+
20+
return false
21+
}
22+
23+
public async migrate(): Promise<void> {
24+
const studios = await this.findStudiosToMigrate()
25+
26+
for (const studio of studios) {
27+
// @ts-expect-error previewContainerIds is typed as string[]
28+
const oldPreviewContainerIds = studio.previewContainerIds
29+
// @ts-expect-error thumbnailContainerIds is typed as string[]
30+
const oldThumbnailContainerIds = studio.thumbnailContainerIds
31+
32+
const newPackageContainers = convertObjectIntoOverrides({
33+
previewContainerIds: oldPreviewContainerIds ?? [],
34+
thumbnailContainerIds: oldThumbnailContainerIds ?? [],
35+
} satisfies StudioPackageContainerSettings) as ObjectWithOverrides<StudioPackageContainerSettings>
36+
37+
await Studios.updateAsync(studio._id, {
38+
$set: {
39+
packageContainerSettingsWithOverrides: newPackageContainers,
40+
},
41+
$unset: {
42+
previewContainerIds: 1,
43+
thumbnailContainerIds: 1,
44+
},
45+
})
46+
}
47+
}
48+
49+
private async findStudiosToMigrate() {
50+
return Studios.findFetchAsync({
51+
packageContainerSettingsWithOverrides: { $exists: false },
52+
})
53+
}
54+
}

0 commit comments

Comments
 (0)