Skip to content

Commit 52cb609

Browse files
committed
chore: move external id lookup out of playout code
1 parent 760f174 commit 52cb609

5 files changed

Lines changed: 143 additions & 7 deletions

File tree

‎packages/blueprints-integration/src/context/onTakeContext.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,13 @@ export interface IOnTakeContext
3838
/**
3939
* Insertion point for {@link IPlayoutActionContext.queuePart} and {@link IOnTakeContext.queuePartAfterTake}.
4040
* Specify exactly one of `targetPartId` or `targetPartInstanceId`.
41+
* `targetPartId` may be a Part `_id` or `externalId`; Core converts it to `_id` before playout.
4142
* Inserts before the target unless `after` is true.
4243
*/
4344
export type QueuePartTarget = { after?: boolean } & (QueuePartTargetProps | QueuePartInstanceTargetProps)
4445

4546
interface QueuePartTargetProps {
47+
/** Part `_id` or `externalId` */
4648
targetPartId: string
4749
targetPartInstanceId?: never
4850
}

‎packages/job-worker/src/blueprints/context/services/PartAndPieceInstanceActionService.ts‎

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,13 @@ import {
4949
PieceTimelineObjectsBlob,
5050
serializePieceTimelineObjectsBlob,
5151
} from '@sofie-automation/corelib/dist/dataModel/Piece'
52-
import { PartInstanceId, PieceInstanceId, RundownId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids'
52+
import {
53+
PartId,
54+
PartInstanceId,
55+
PieceInstanceId,
56+
RundownId,
57+
SegmentId,
58+
} from '@sofie-automation/corelib/dist/dataModel/Ids'
5359
import {
5460
protectString,
5561
unprotectString,
@@ -102,6 +108,24 @@ export class PartAndPieceInstanceActionService {
102108
this.showStyleCompound = showStyle
103109
}
104110

111+
/**
112+
* Convert blueprint `targetPartId` to an internal Part `_id`.
113+
* Accepts either `_id` or `externalId`; playout only ever sees `_id`.
114+
*/
115+
#toInternalQueuePartTarget(target?: QueuePartTarget): QueuePartTarget | undefined {
116+
if (!target?.targetPartId) return target
117+
118+
const part =
119+
this._playoutModel.findPart(protectString<PartId>(target.targetPartId)) ??
120+
this._playoutModel.getAllOrderedParts().find((p) => p.externalId === target.targetPartId)
121+
if (!part) return target
122+
123+
return {
124+
...target,
125+
targetPartId: unprotectString(part._id),
126+
}
127+
}
128+
105129
#assertInsertTargetShowStyleCompatible(insertTarget: QueuedAdlibInsertTarget): void {
106130
if (insertTarget.targetRundown.rundown.showStyleVariantId !== this.showStyleCompound.showStyleVariantId) {
107131
throw new Error(
@@ -426,7 +450,8 @@ export class PartAndPieceInstanceActionService {
426450
throw new Error('Too close to an autonext to queue a part')
427451
}
428452

429-
const insertTarget = resolveQueuedAdlibInsertTarget(this._playoutModel, currentPartInstance, target)
453+
const internalTarget = this.#toInternalQueuePartTarget(target)
454+
const insertTarget = resolveQueuedAdlibInsertTarget(this._playoutModel, currentPartInstance, internalTarget)
430455
this.#assertInsertTargetShowStyleCompatible(insertTarget)
431456

432457
const processedPartsAndPieces = this.processPartAndPiecesToQueueOrFail(
@@ -441,7 +466,7 @@ export class PartAndPieceInstanceActionService {
441466
this._context,
442467
this._playoutModel,
443468
currentPartInstance,
444-
{ ...processedPartsAndPieces, target },
469+
{ ...processedPartsAndPieces, target: internalTarget },
445470
undefined,
446471
insertTarget
447472
)
@@ -458,7 +483,8 @@ export class PartAndPieceInstanceActionService {
458483
currentPartInstance: PlayoutPartInstanceModel,
459484
target?: QueuePartTarget
460485
): QueueablePartAndPieces {
461-
const insertTarget = resolveQueuedAdlibInsertTarget(this._playoutModel, currentPartInstance, target)
486+
const internalTarget = this.#toInternalQueuePartTarget(target)
487+
const insertTarget = resolveQueuedAdlibInsertTarget(this._playoutModel, currentPartInstance, internalTarget)
462488
this.#assertInsertTargetShowStyleCompatible(insertTarget)
463489

464490
const { part, pieces } = this.processPartAndPiecesToQueueOrFail(
@@ -468,7 +494,7 @@ export class PartAndPieceInstanceActionService {
468494
insertTarget.targetSegment.segment._id
469495
)
470496

471-
return { part, pieces, target }
497+
return { part, pieces, target: internalTarget }
472498
}
473499

474500
public processPartAndPiecesToQueueOrFail(

‎packages/job-worker/src/blueprints/context/services/__tests__/PartAndPieceInstanceActionService.test.ts‎

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1285,6 +1285,53 @@ describe('Test blueprint api context', () => {
12851285
})
12861286
})
12871287

1288+
test('good with explicit insert before part externalId', async () => {
1289+
const { jobContext, playlistId, rundownId } = await setupMyDefaultRundown()
1290+
1291+
const partInstance = (await jobContext.mockCollections.PartInstances.findOne({
1292+
rundownId,
1293+
'part.externalId': 'MOCK_PART_0_0',
1294+
})) as DBPartInstance
1295+
expect(partInstance).toBeTruthy()
1296+
await setPartInstances(jobContext, playlistId, partInstance, undefined)
1297+
1298+
const targetPart = await jobContext.mockCollections.Parts.findOne({
1299+
externalId: 'MOCK_PART_1_1',
1300+
rundownId,
1301+
})
1302+
expect(targetPart).toBeTruthy()
1303+
1304+
await wrapWithPlayoutModel(jobContext, playlistId, async (playoutModel) => {
1305+
const { service } = await getTestee(jobContext, playoutModel)
1306+
1307+
const newPiece: IBlueprintPiece = {
1308+
name: 'test piece',
1309+
sourceLayerId: 'sl1',
1310+
outputLayerId: 'o1',
1311+
externalId: '-',
1312+
enable: { start: 0 },
1313+
lifespan: PieceLifespan.OutOnRundownEnd,
1314+
content: {
1315+
timelineObjects: [],
1316+
},
1317+
}
1318+
const newPart: IBlueprintPart = {
1319+
externalId: 'nope',
1320+
title: 'something',
1321+
}
1322+
1323+
postProcessPiecesMock.mockImplementationOnce(postProcessPiecesOrig)
1324+
insertQueuedPartWithPiecesMock.mockImplementationOnce(insertQueuedPartWithPiecesOrig)
1325+
await service.queuePart(newPart, [newPiece], { targetPartId: 'MOCK_PART_1_1' })
1326+
1327+
const newPartInstance = playoutModel.getPartInstance(
1328+
playoutModel.playlist.nextPartInfo!.partInstanceId
1329+
)!
1330+
expect(newPartInstance.partInstance.segmentId).toEqual(targetPart!.segmentId)
1331+
expect(newPartInstance.partInstance.part._rank).toBeLessThan(targetPart!._rank)
1332+
})
1333+
})
1334+
12881335
test('unknown insert before part id', async () => {
12891336
const { jobContext, playlistId, rundownId } = await setupMyDefaultRundown()
12901337

@@ -1578,6 +1625,52 @@ describe('Test blueprint api context', () => {
15781625
})
15791626
})
15801627

1628+
test('prepareQueueablePartAndPieces converts targetPartId from externalId to _id', async () => {
1629+
const { jobContext, playlistId, rundownId } = await setupMyDefaultRundown()
1630+
1631+
const partInstance = (await jobContext.mockCollections.PartInstances.findOne({
1632+
rundownId,
1633+
'part.externalId': 'MOCK_PART_0_0',
1634+
})) as DBPartInstance
1635+
expect(partInstance).toBeTruthy()
1636+
await setPartInstances(jobContext, playlistId, partInstance, undefined)
1637+
1638+
const targetPart = await jobContext.mockCollections.Parts.findOne({
1639+
externalId: 'MOCK_PART_1_1',
1640+
rundownId,
1641+
})
1642+
expect(targetPart).toBeTruthy()
1643+
1644+
await wrapWithPlayoutModel(jobContext, playlistId, async (playoutModel) => {
1645+
const { service } = await getTestee(jobContext, playoutModel)
1646+
1647+
const currentPartInstance = playoutModel.currentPartInstance!
1648+
postProcessPiecesMock.mockImplementationOnce(postProcessPiecesOrig)
1649+
1650+
const queueable = service.prepareQueueablePartAndPieces(
1651+
{ externalId: 'nope', title: 'something' },
1652+
[
1653+
{
1654+
name: 'test piece',
1655+
sourceLayerId: 'sl1',
1656+
outputLayerId: 'o1',
1657+
externalId: '-',
1658+
enable: { start: 0 },
1659+
lifespan: PieceLifespan.OutOnRundownEnd,
1660+
content: {
1661+
timelineObjects: [],
1662+
},
1663+
},
1664+
],
1665+
currentPartInstance,
1666+
{ targetPartId: 'MOCK_PART_1_1' }
1667+
)
1668+
1669+
expect(queueable.target?.targetPartId).toEqual(unprotectString(targetPart!._id))
1670+
expect(queueable.target?.targetPartId).not.toEqual('MOCK_PART_1_1')
1671+
})
1672+
})
1673+
15811674
test('good with explicit insert after part id', async () => {
15821675
const { jobContext, playlistId, rundownId } = await setupMyDefaultRundown()
15831676

‎packages/job-worker/src/playout/model/implementation/PlayoutSegmentModelImpl.ts‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { ReadonlyDeep } from 'type-fest'
33
import { DBSegment, SegmentOrphanedReason } from '@sofie-automation/corelib/dist/dataModel/Segment'
44
import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part'
55
import { PlayoutSegmentModel } from '../PlayoutSegmentModel.js'
6-
import { unprotectString } from '@sofie-automation/corelib/dist/protectedString'
76

87
export class PlayoutSegmentModelImpl implements PlayoutSegmentModel {
98
readonly #segment: DBSegment
@@ -21,7 +20,7 @@ export class PlayoutSegmentModelImpl implements PlayoutSegmentModel {
2120
}
2221

2322
getPart(id: PartId): ReadonlyDeep<DBPart> | undefined {
24-
return this.parts.find((part) => part._id === id || part.externalId === unprotectString(id))
23+
return this.parts.find((part) => part._id === id)
2524
}
2625

2726
getPartIds(): PartId[] {

‎packages/job-worker/src/playout/model/implementation/__tests__/PlayoutSegmentModelImpl.spec.ts‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,22 @@ describe('PlayoutSegmentModelImpl', () => {
6969

7070
expect(model.getPart(fakePart._id)).toBe(fakePart)
7171
})
72+
it('matches by _id even when another part has that id as externalId and a lower rank', async () => {
73+
const byId: DBPart = { _id: protectString('part0'), _rank: 5, externalId: 'other' } as any
74+
const byExternalId: DBPart = { _id: protectString('part1'), _rank: 1, externalId: 'part0' } as any
75+
const segment = createBasicDBSegment()
76+
const model = new PlayoutSegmentModelImpl(segment, [byId, byExternalId])
77+
78+
expect(model.getPart(byId._id)).toBe(byId)
79+
expect(model.getPart(protectString('part0'))).toBe(byId)
80+
})
81+
it('does not match by externalId', async () => {
82+
const fakePart: DBPart = { _id: protectString('part0'), _rank: 1, externalId: 'ext-part' } as any
83+
const segment = createBasicDBSegment()
84+
const model = new PlayoutSegmentModelImpl(segment, [fakePart])
85+
86+
expect(model.getPart(protectString('ext-part'))).toBeUndefined()
87+
})
7288
})
7389

7490
describe('setAdlibTestingRank', () => {

0 commit comments

Comments
 (0)