diff --git a/meteor/__mocks__/helpers/methods.ts b/meteor/__mocks__/helpers/methods.ts new file mode 100644 index 00000000000..da44b5856ac --- /dev/null +++ b/meteor/__mocks__/helpers/methods.ts @@ -0,0 +1,17 @@ +import { MethodRegistry } from '../../server/methodRegistry' +import { registerAllApiMethods } from '../../server/methodRegistrations' + +/** + * Test helper: register all API methods on a fresh MethodRegistry and apply them to the (mock) + * Meteor server, mirroring what `main.ts` does at startup. + * + * Call this in suites that exercise Meteor methods (via `MeteorCall`, `Meteor.callAsync`, or by + * spying on `MeteorMock.mockMethods`). It is needed because the production registration now only + * runs explicitly from `main.ts`, rather than as an import-time side effect of each API file. + */ +export function registerAllMethodsForTest(): MethodRegistry { + const registry = new MethodRegistry() + registerAllApiMethods(registry) + registry.applyToMeteor() + return registry +} diff --git a/meteor/__mocks__/helpers/publications.ts b/meteor/__mocks__/helpers/publications.ts new file mode 100644 index 00000000000..d26d1344f00 --- /dev/null +++ b/meteor/__mocks__/helpers/publications.ts @@ -0,0 +1,17 @@ +import { PublicationRegistry } from '../../server/publicationRegistry' +import { registerAllPublications } from '../../server/publicationRegistrations' + +/** + * Test helper: register all DDP publications on a fresh PublicationRegistry and apply them to the + * (mock) Meteor server, mirroring what `main.ts` does at startup. + * + * Call this in suites that exercise Meteor subscriptions. It is needed because the production + * registration now only runs explicitly from `main.ts`, rather than as an import-time side effect of + * each publication file. + */ +export function registerAllPublicationsForTest(): PublicationRegistry { + const registry = new PublicationRegistry() + registerAllPublications(registry) + registry.applyToMeteor() + return registry +} diff --git a/meteor/server/__tests__/__snapshots__/methodRegistry.test.ts.snap b/meteor/server/__tests__/__snapshots__/methodRegistry.test.ts.snap new file mode 100644 index 00000000000..9fa39b88c52 --- /dev/null +++ b/meteor/server/__tests__/__snapshots__/methodRegistry.test.ts.snap @@ -0,0 +1,199 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`MethodRegistry the full set of registered method names is stable (drift guard) 1`] = ` +[ + "blueprint.assignSystem", + "client.callBackgroundPeripheralDeviceFunction", + "client.callPeripheralDeviceAction", + "client.callPeripheralDeviceFunction", + "client.clientErrorReport", + "client.clientLogNotification", + "client.clientLogger", + "debug_clearAllResetInstances", + "debug_forceClearAllCaches", + "debug_playlistRunBlueprints", + "debug_previewTrigger", + "debug_regenerateNextPartInstance", + "debug_removeAllPlaylists", + "debug_removePlaylist", + "debug_segmentRunBlueprints", + "debug_syncPlayheadInfinitesForNextPartInstance", + "debug_updateTimeline", + "externalMessages.remove", + "externalMessages.retry", + "externalMessages.toggleHold", + "migration.fixupConfigForShowStyleBase", + "migration.fixupConfigForStudio", + "migration.forceMigration", + "migration.getMigrationStatus", + "migration.ignoreFixupConfigForShowStyleBase", + "migration.ignoreFixupConfigForStudio", + "migration.resetDatabaseVersions", + "migration.runMigration", + "migration.runUpgradeForCoreSystem", + "migration.runUpgradeForShowStyleBase", + "migration.runUpgradeForStudio", + "migration.validateConfigForShowStyleBase", + "migration.validateConfigForStudio", + "mongo.insert", + "mongo.remove", + "mongo.update", + "peripheralDevice.functionReply", + "peripheralDevice.getPeripheralDevice", + "peripheralDevice.initialize", + "peripheralDevice.input.inputDeviceTrigger", + "peripheralDevice.killProcess", + "peripheralDevice.mediaScanner.clearMediaObjectCollection", + "peripheralDevice.mediaScanner.getMediaObjectRevisions", + "peripheralDevice.mediaScanner.updateMediaObject", + "peripheralDevice.mos.roCreate", + "peripheralDevice.mos.roDelete", + "peripheralDevice.mos.roFullStory", + "peripheralDevice.mos.roItemDelete", + "peripheralDevice.mos.roItemInsert", + "peripheralDevice.mos.roItemMove", + "peripheralDevice.mos.roItemReplace", + "peripheralDevice.mos.roItemStatus", + "peripheralDevice.mos.roItemSwap", + "peripheralDevice.mos.roMetadata", + "peripheralDevice.mos.roReadyToAir", + "peripheralDevice.mos.roReplace", + "peripheralDevice.mos.roStatus", + "peripheralDevice.mos.roStoryDelete", + "peripheralDevice.mos.roStoryInsert", + "peripheralDevice.mos.roStoryMove", + "peripheralDevice.mos.roStoryReplace", + "peripheralDevice.mos.roStoryStatus", + "peripheralDevice.mos.roStorySwap", + "peripheralDevice.packageManager.fetchPackageInfoMetadata", + "peripheralDevice.packageManager.removeAllExpectedPackageWorkStatusOfDevice", + "peripheralDevice.packageManager.removeAllPackageContainerPackageStatusesOfDevice", + "peripheralDevice.packageManager.removeAllPackageContainerStatusesOfDevice", + "peripheralDevice.packageManager.removePackageInfo", + "peripheralDevice.packageManager.updateExpectedPackageWorkStatuses", + "peripheralDevice.packageManager.updatePackageContainerPackageStatuses", + "peripheralDevice.packageManager.updatePackageContainerStatuses", + "peripheralDevice.packageManager.updatePackageInfo", + "peripheralDevice.ping", + "peripheralDevice.pingWithCommand", + "peripheralDevice.playlist.playlistGet", + "peripheralDevice.playout.playbackChanged", + "peripheralDevice.playout.reportExternalEvents", + "peripheralDevice.removePeripheralDevice", + "peripheralDevice.reportResolveDone", + "peripheralDevice.rundown.partCreate", + "peripheralDevice.rundown.partDelete", + "peripheralDevice.rundown.partUpdate", + "peripheralDevice.rundown.rundownCreate", + "peripheralDevice.rundown.rundownDelete", + "peripheralDevice.rundown.rundownGet", + "peripheralDevice.rundown.rundownList", + "peripheralDevice.rundown.rundownMetaDataUpdate", + "peripheralDevice.rundown.rundownUpdate", + "peripheralDevice.rundown.segmentCreate", + "peripheralDevice.rundown.segmentDelete", + "peripheralDevice.rundown.segmentGet", + "peripheralDevice.rundown.segmentRanksUpdate", + "peripheralDevice.rundown.segmentUpdate", + "peripheralDevice.spreadsheet.requestUserAuthToken", + "peripheralDevice.spreadsheet.storeAccessToken", + "peripheralDevice.status", + "peripheralDevice.testMethod", + "peripheralDevice.timeline.setTimelineTriggerTime", + "peripheralDevice.unInitialize", + "playout.shouldUpdateStudioBaseline", + "playout.updateStudioBaseline", + "rundown.rundownPlaylistNeedsResync", + "rundownLayout.createRundownLayout", + "rundownLayout.removeRundownLayout", + "showstyles.getCreateAdlibTestingRundownOptions", + "showstyles.importShowStyleVariant", + "showstyles.importShowStyleVariantAsNew", + "showstyles.insertBlueprint", + "showstyles.insertShowStyleBase", + "showstyles.insertShowStyleVariant", + "showstyles.removeBlueprint", + "showstyles.removeShowStyleBase", + "showstyles.removeShowStyleVariant", + "showstyles.reorderShowStyleVariant", + "snapshot.debugSnaphot", + "snapshot.removeSnaphot", + "snapshot.restoreSnaphot", + "snapshot.rundownPlaylistSnapshot", + "snapshot.systemSnapshot", + "studio.assignConfigToPeripheralDevice", + "studio.insertStudio", + "studio.removeStudio", + "system.cleanupIndexes", + "system.cleanupOldData", + "system.doSystemBenchmark", + "system.generateSingleUseToken", + "system.getTranslationBundle", + "system.runCronjob", + "systemStatus.getDebugStates", + "systemStatus.getSystemStatus", + "systemTime.determineDiffTime", + "systemTime.getTime", + "systemTime.getTimeDiff", + "triggeredActions.createTriggeredActions", + "triggeredActions.removeTriggeredActions", + "user.getUserPermissions", + "userAction.DEBUG_crashStudioWorker", + "userAction.activate", + "userAction.activateAdlibTestingMode", + "userAction.activateHold", + "userAction.baselineAdLibPieceStart", + "userAction.blurred", + "userAction.bucketAdlibImport", + "userAction.bucketAdlibStart", + "userAction.bucketsModifyBucketAdLib", + "userAction.bucketsModifyBucketAdLibAction", + "userAction.bucketsSaveActionIntoBucket", + "userAction.clearQuickLoop", + "userAction.createAdlibTestingRundownForShowStyleVariant", + "userAction.createBucket", + "userAction.deactivate", + "userAction.disableNextPiece", + "userAction.emptyBucket", + "userAction.executeAction", + "userAction.executeUserChangeOperation", + "userAction.focused", + "userAction.forceResetAndActivate", + "userAction.ingest.regenerateRundownPlaylist", + "userAction.modifyBucket", + "userAction.moveNext", + "userAction.moveRundown", + "userAction.packagemanager.abortExpectation", + "userAction.packagemanager.restartAllExpectations", + "userAction.packagemanager.restartExpectation", + "userAction.packagemanager.restartPackageContainer", + "userAction.pieceSetInOutPoints", + "userAction.pieceTakeNow", + "userAction.prepareForBroadcast", + "userAction.queueNextSegment", + "userAction.removeBucket", + "userAction.removeBucketAdLib", + "userAction.removeBucketAdLibAction", + "userAction.removeRundown", + "userAction.removeRundownPlaylist", + "userAction.resetAndActivate", + "userAction.resetRundownPlaylist", + "userAction.restoreRundownOrder", + "userAction.resyncRundown", + "userAction.resyncRundownPlaylist", + "userAction.saveEvaluation", + "userAction.segmentAdLibPieceStart", + "userAction.setNext", + "userAction.setNextSegment", + "userAction.setQuickLoopEnd", + "userAction.setQuickLoopStart", + "userAction.sourceLayerOnPartStop", + "userAction.sourceLayerStickyPieceStart", + "userAction.storeRundownSnapshot", + "userAction.switchRouteSet", + "userAction.system.disablePeripheralSubDevice", + "userAction.system.restartCore", + "userAction.take", + "userAction.unsyncRundown", +] +`; diff --git a/meteor/server/__tests__/__snapshots__/publicationRegistry.test.ts.snap b/meteor/server/__tests__/__snapshots__/publicationRegistry.test.ts.snap new file mode 100644 index 00000000000..206479d5049 --- /dev/null +++ b/meteor/server/__tests__/__snapshots__/publicationRegistry.test.ts.snap @@ -0,0 +1,76 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`PublicationRegistry the full set of registered publication names is stable (drift guard) 1`] = ` +[ + "adLibActions", + "adLibActionsForPart", + "adLibPieces", + "adLibPiecesForPart", + "blueprints", + "bucketAdLibActions", + "bucketAdLibPieces", + "buckets", + "coreSystem", + "deviceTriggersPreview", + "evaluations", + "expectedPackageWorkStatuses", + "expectedPackages", + "expectedPlayoutItemsForDevice", + "externalEventSubscriptionsForDevice", + "externalMessageQueue", + "ingestDataCache", + "ingestDeviceRundownStatus", + "ingestDeviceRundownStatusTestTool", + "mappingsForDevice", + "mappingsForStudio", + "mountedTriggersForDevice", + "mountedTriggersForDevicePreview", + "notificationsForRundown", + "notificationsForRundownPlaylist", + "packageContainerStatuses", + "packageInfos", + "packageManagerExpectedPackages", + "packageManagerPackageContainers", + "packageManagerPlayoutContext", + "partInstances", + "partInstancesSimple", + "parts", + "peripheralDeviceCommands", + "peripheralDeviceForDevice", + "peripheralDevices", + "peripheralDevicesAndSubDevices", + "pieceInstances", + "pieceInstancesSimple", + "pieces", + "piecesInfiniteStartingBefore", + "rundownBaselineAdLibActions", + "rundownBaselineAdLibPieces", + "rundownLayouts", + "rundownPlaylistForStudio", + "rundownPlaylists", + "rundownsForDevice", + "rundownsInPlaylists", + "rundownsWithShowStyleBases", + "segments", + "showStyleBases", + "showStyleVariants", + "snapshots", + "studios", + "timelineDatastore", + "timelineDatastoreForDevice", + "timelineForDevice", + "timelineForStudio", + "translationsBundles", + "triggeredActions", + "uiBlueprintUpgradeStatuses", + "uiBucketContentStatuses", + "uiPartInstances", + "uiParts", + "uiPieceContentStatuses", + "uiSegmentPartNotes", + "uiShowStyleBase", + "uiStudio", + "uiTriggeredActions", + "userActionsLog", +] +`; diff --git a/meteor/server/__tests__/cronjobs.test.ts b/meteor/server/__tests__/cronjobs.test.ts index 93de8155db4..02e6bbb5d2d 100644 --- a/meteor/server/__tests__/cronjobs.test.ts +++ b/meteor/server/__tests__/cronjobs.test.ts @@ -48,8 +48,9 @@ jest.mock('../api/deviceTriggers/observer') const MAX_WAIT_TIME = 4 * 1000 import '../cronjobs' +import { registerAllMethodsForTest } from '../../__mocks__/helpers/methods' -import '../api/peripheralDevice' +registerAllMethodsForTest() import { CoreSystem, NrcsIngestDataCache, diff --git a/meteor/server/__tests__/methodRegistry.test.ts b/meteor/server/__tests__/methodRegistry.test.ts new file mode 100644 index 00000000000..eecc901c135 --- /dev/null +++ b/meteor/server/__tests__/methodRegistry.test.ts @@ -0,0 +1,50 @@ +import { MethodApiRegistration, MethodRegistry } from '../methodRegistry' +import { METHOD_REGISTRATIONS, registerAllApiMethods } from '../methodRegistrations' + +describe('MethodRegistry', () => { + test('registers all API methods without error', () => { + const registry = new MethodRegistry() + // registerApi throws if a class method has no matching wire name, so this is itself an assertion + // that every implemented method maps to an enum entry (the historical behaviour). + expect(() => registerAllApiMethods(registry)).not.toThrow() + }) + + test('registered names are unique and only use known wire names', () => { + const registry = new MethodRegistry() + registerAllApiMethods(registry) + const names = registry.getAllMethodNames() + + // No duplicate registrations across all APIs + expect(new Set(names).size).toBe(names.length) + + // Every non-debug registered name must be a value from one of the method-name enums. + // (Debug methods are not enum-backed and are checked separately.) + const enumWireNames = new Set() + for (const reg of Object.values>(METHOD_REGISTRATIONS)) { + for (const wireName of Object.values(reg.methods)) enumWireNames.add(wireName) + } + for (const name of names) { + if (registry.isDebugMethod(name)) continue + expect(enumWireNames.has(name)).toBe(true) + } + + // A handful of representative names must be present, guarding against an API silently + // dropping out of the registry. + expect(names).toEqual( + expect.arrayContaining([ + 'userAction.take', + 'playout.updateStudioBaseline', + 'peripheralDevice.initialize', + 'system.cleanupIndexes', + ]) + ) + }) + + test('the full set of registered method names is stable (drift guard)', () => { + const registry = new MethodRegistry() + registerAllApiMethods(registry) + // If this snapshot changes, a method's wire name was added, removed or renamed. That must be a + // deliberate change reviewed against the clients that depend on these exact names. + expect([...registry.getAllMethodNames()].sort()).toMatchSnapshot() + }) +}) diff --git a/meteor/server/__tests__/publicationRegistry.test.ts b/meteor/server/__tests__/publicationRegistry.test.ts new file mode 100644 index 00000000000..4b576893ce5 --- /dev/null +++ b/meteor/server/__tests__/publicationRegistry.test.ts @@ -0,0 +1,40 @@ +import { PublicationRegistry } from '../publicationRegistry' +import { registerAllPublications } from '../publicationRegistrations' +import { AllPubSubNames } from '@sofie-automation/meteor-lib/dist/api/pubsub' + +describe('PublicationRegistry', () => { + test('registers all publications without error', () => { + const registry = new PublicationRegistry() + // publishUnsafe throws on a duplicate name, so this is itself an assertion that no two + // publications register under the same wire name. + expect(() => registerAllPublications(registry)).not.toThrow() + }) + + test('registered names are unique and only use known publication names', () => { + const registry = new PublicationRegistry() + registerAllPublications(registry) + const names = registry.getAllPublicationNames() + + // No duplicate registrations across all publication modules + expect(new Set(names).size).toBe(names.length) + + // Every registered name must be a known PubSub name. + const knownNames = new Set(AllPubSubNames) + for (const name of names) { + expect(knownNames.has(name)).toBe(true) + } + + // Conversely, every known PubSub name must be registered (the historical dev-mode check). + for (const pubName of AllPubSubNames) { + expect(names).toContain(pubName) + } + }) + + test('the full set of registered publication names is stable (drift guard)', () => { + const registry = new PublicationRegistry() + registerAllPublications(registry) + // If this snapshot changes, a publication's wire name was added, removed or renamed. That must + // be a deliberate change reviewed against the clients that depend on these exact names. + expect([...registry.getAllPublicationNames()].sort()).toMatchSnapshot() + }) +}) diff --git a/meteor/server/api/ExternalMessageQueue.ts b/meteor/server/api/ExternalMessageQueue.ts index 0a5fdf7414c..d18199017fa 100644 --- a/meteor/server/api/ExternalMessageQueue.ts +++ b/meteor/server/api/ExternalMessageQueue.ts @@ -2,11 +2,7 @@ import { Meteor } from 'meteor/meteor' import { check } from '../lib/check' import { StatusCode } from '@sofie-automation/blueprints-integration' import { deferAsync, getCurrentTime } from '../lib/lib' -import { registerClassToMeteorMethods } from '../methods' -import { - NewExternalMessageQueueAPI, - ExternalMessageQueueAPIMethods, -} from '@sofie-automation/meteor-lib/dist/api/ExternalMessageQueue' +import { NewExternalMessageQueueAPI } from '@sofie-automation/meteor-lib/dist/api/ExternalMessageQueue' import { StatusObject, setSystemStatus } from '../systemStatus/systemStatus' import { MethodContextAPI, MethodContext } from './methodContext' import { ExternalMessageQueueObjId } from '@sofie-automation/corelib/dist/dataModel/Ids' @@ -114,15 +110,14 @@ async function retry(context: MethodContext, messageId: ExternalMessageQueueObjI }) // triggerdoMessageQueue(1000) } -class ServerExternalMessageQueueAPI extends MethodContextAPI implements NewExternalMessageQueueAPI { - async remove(messageId: ExternalMessageQueueObjId) { +export class ServerExternalMessageQueueAPI extends MethodContextAPI implements NewExternalMessageQueueAPI { + async remove(messageId: ExternalMessageQueueObjId): Promise { return removeExternalMessage(this, messageId) } - async toggleHold(messageId: ExternalMessageQueueObjId) { + async toggleHold(messageId: ExternalMessageQueueObjId): Promise { return toggleHold(this, messageId) } - async retry(messageId: ExternalMessageQueueObjId) { + async retry(messageId: ExternalMessageQueueObjId): Promise { return retry(this, messageId) } } -registerClassToMeteorMethods(ExternalMessageQueueAPIMethods, ServerExternalMessageQueueAPI, false) diff --git a/meteor/server/api/__tests__/client.test.ts b/meteor/server/api/__tests__/client.test.ts index dad7d64df0a..ca6bb981ac0 100644 --- a/meteor/server/api/__tests__/client.test.ts +++ b/meteor/server/api/__tests__/client.test.ts @@ -16,8 +16,9 @@ import { MeteorCall } from '../methods' import { PeripheralDeviceId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { PeripheralDeviceCommands, UserActionsLog } from '../../collections' import { SupressLogMessages } from '../../../__mocks__/suppressLogging' +import { registerAllMethodsForTest } from '../../../__mocks__/helpers/methods' -require('../client') // include in order to create the Meteor methods needed +registerAllMethodsForTest() setLogLevel(LogLevel.INFO) diff --git a/meteor/server/api/__tests__/externalMessageQueue.test.ts b/meteor/server/api/__tests__/externalMessageQueue.test.ts index 1b5fb53f938..d225fc8b56d 100644 --- a/meteor/server/api/__tests__/externalMessageQueue.test.ts +++ b/meteor/server/api/__tests__/externalMessageQueue.test.ts @@ -8,9 +8,11 @@ import { protectString } from '@sofie-automation/corelib/dist/protectedString' import { getCurrentTime } from '../../lib/lib' import { MeteorCall } from '../methods' -import '../ExternalMessageQueue' +import { registerAllMethodsForTest } from '../../../__mocks__/helpers/methods' import { SupressLogMessages } from '../../../__mocks__/suppressLogging' +registerAllMethodsForTest() + describe('Test external message queue static methods', () => { let studioEnv: DefaultEnvironment beforeAll(async () => { diff --git a/meteor/server/api/__tests__/peripheralDevice.test.ts b/meteor/server/api/__tests__/peripheralDevice.test.ts index 5a37e3edb70..c76d7b31f79 100644 --- a/meteor/server/api/__tests__/peripheralDevice.test.ts +++ b/meteor/server/api/__tests__/peripheralDevice.test.ts @@ -22,10 +22,10 @@ import { StatusCode, } from '@sofie-automation/blueprints-integration' import { CreateFakeResult, QueueStudioJobSpy } from '../../../__mocks__/worker' +import { registerAllMethodsForTest } from '../../../__mocks__/helpers/methods' jest.mock('../../api/deviceTriggers/observer') -import '../peripheralDevice' import { OnTimelineTriggerTimeProps, StudioJobFunc, StudioJobs } from '@sofie-automation/corelib/dist/worker/studio' import { MeteorCall } from '../methods' import { PeripheralDeviceForDevice } from '@sofie-automation/shared-lib/dist/core/model/peripheralDevice' @@ -50,6 +50,8 @@ import { SupressLogMessages } from '../../../__mocks__/suppressLogging' import { JSONBlobStringify } from '@sofie-automation/shared-lib/dist/lib/JSONBlob' import { PeripheralDeviceCommand } from '@sofie-automation/corelib/dist/dataModel/PeripheralDeviceCommand' +registerAllMethodsForTest() + const DEBUG = false describe('test peripheralDevice general API methods', () => { diff --git a/meteor/server/api/__tests__/rundownLayouts.test.ts b/meteor/server/api/__tests__/rundownLayouts.test.ts index 280db82dda9..11c0f48ca40 100644 --- a/meteor/server/api/__tests__/rundownLayouts.test.ts +++ b/meteor/server/api/__tests__/rundownLayouts.test.ts @@ -13,6 +13,9 @@ import { RundownLayouts } from '../../collections' import { SupressLogMessages } from '../../../__mocks__/suppressLogging' import { shelfLayoutsRouter } from '../rundownLayouts' import { callKoaRoute } from '../../../__mocks__/koa-util' +import { registerAllMethodsForTest } from '../../../__mocks__/helpers/methods' + +registerAllMethodsForTest() describe('Rundown Layouts', () => { let env: DefaultEnvironment diff --git a/meteor/server/api/__tests__/userActions/general.test.ts b/meteor/server/api/__tests__/userActions/general.test.ts index 22eaadac687..39224b976b7 100644 --- a/meteor/server/api/__tests__/userActions/general.test.ts +++ b/meteor/server/api/__tests__/userActions/general.test.ts @@ -5,10 +5,9 @@ import { getCurrentTime, sleep } from '../../../lib/lib' import { MeteorCall } from '../../methods' import { ClientAPI } from '@sofie-automation/meteor-lib/dist/api/client' import { UserActionsLog } from '../../../collections' +import { registerAllMethodsForTest } from '../../../../__mocks__/helpers/methods' -require('../../system') // include so that we can call generateSingleUseToken() -require('../../client') // include in order to create the Meteor methods needed -require('../../userActions') // include in order to create the Meteor methods needed +registerAllMethodsForTest() describe('User Actions - General', () => { beforeEach(async () => { diff --git a/meteor/server/api/__tests__/userActions/system.test.ts b/meteor/server/api/__tests__/userActions/system.test.ts index 876ae367563..02c70e3cd89 100644 --- a/meteor/server/api/__tests__/userActions/system.test.ts +++ b/meteor/server/api/__tests__/userActions/system.test.ts @@ -22,8 +22,9 @@ import { import { literal } from '@sofie-automation/shared-lib/dist/lib/lib' import { StudioPlayoutDevice } from '@sofie-automation/corelib/dist/dataModel/Studio' import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' +import { registerAllMethodsForTest } from '../../../../__mocks__/helpers/methods' -require('../../userActions') // include in order to create the Meteor methods needed +registerAllMethodsForTest() describe('User Actions - Disable Peripheral SubDevice', () => { let env: DefaultEnvironment diff --git a/meteor/server/api/blueprints/__tests__/api.test.ts b/meteor/server/api/blueprints/__tests__/api.test.ts index 2c92ed6f4f8..e41424d461a 100644 --- a/meteor/server/api/blueprints/__tests__/api.test.ts +++ b/meteor/server/api/blueprints/__tests__/api.test.ts @@ -16,11 +16,12 @@ import { SupressLogMessages } from '../../../../__mocks__/suppressLogging' import { JSONBlobStringify } from '@sofie-automation/shared-lib/dist/lib/JSONBlob' import { Meteor } from 'meteor/meteor' import * as CoreSystemAPI from '../../../coreSystem' +import { registerAllMethodsForTest } from '../../../../__mocks__/helpers/methods' // we don't want the deviceTriggers observer to start up at this time jest.mock('../../deviceTriggers/observer') -require('../../peripheralDevice.ts') // include in order to create the Meteor methods needed +registerAllMethodsForTest() const DEFAULT_CONNECTION: Meteor.Connection = { id: 'mockConnectionId', diff --git a/meteor/server/api/blueprints/api.ts b/meteor/server/api/blueprints/api.ts index a02c027868b..b325b1a21c1 100644 --- a/meteor/server/api/blueprints/api.ts +++ b/meteor/server/api/blueprints/api.ts @@ -14,8 +14,8 @@ import { TranslationsBundle, } from '@sofie-automation/blueprints-integration' import { check, Match } from '../../lib/check' -import { NewBlueprintAPI, BlueprintAPIMethods } from '@sofie-automation/meteor-lib/dist/api/blueprint' -import { registerClassToMeteorMethods, ReplaceOptionalWithNullInMethodArguments } from '../../methods' +import { NewBlueprintAPI } from '@sofie-automation/meteor-lib/dist/api/blueprint' +import { ReplaceOptionalWithNullInMethodArguments } from '../../methods' import { SYSTEM_ID } from '@sofie-automation/meteor-lib/dist/collections/CoreSystem' import { parseVersion } from '../../systemStatus/semverUtils' import { evalBlueprint } from './cache' @@ -406,15 +406,17 @@ async function assignSystemBlueprint(methodContext: MethodContext, blueprintId: } } -class ServerBlueprintAPI extends MethodContextAPI implements ReplaceOptionalWithNullInMethodArguments { - async insertBlueprint() { +export class ServerBlueprintAPI + extends MethodContextAPI + implements ReplaceOptionalWithNullInMethodArguments +{ + async insertBlueprint(): Promise { return insertBlueprint(this.connection) } - async removeBlueprint(blueprintId: BlueprintId) { + async removeBlueprint(blueprintId: BlueprintId): Promise { return removeBlueprint(this, blueprintId) } - async assignSystemBlueprint(blueprintId: BlueprintId | null) { + async assignSystemBlueprint(blueprintId: BlueprintId | null): Promise { return assignSystemBlueprint(this, blueprintId) } } -registerClassToMeteorMethods(BlueprintAPIMethods, ServerBlueprintAPI, false) diff --git a/meteor/server/api/client.ts b/meteor/server/api/client.ts index 10dd6dc7cac..49f541dcf5b 100644 --- a/meteor/server/api/client.ts +++ b/meteor/server/api/client.ts @@ -4,15 +4,15 @@ import type { Time } from '@sofie-automation/shared-lib/dist/lib/lib' import { getCurrentTime } from '../lib/lib' import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' import { logger } from '../logging' -import { ClientAPI, NewClientAPI, ClientAPIMethods } from '@sofie-automation/meteor-lib/dist/api/client' +import { ClientAPI, NewClientAPI } from '@sofie-automation/meteor-lib/dist/api/client' import { UserActionsLogItem } from '@sofie-automation/meteor-lib/dist/collections/UserActionsLog' -import { registerClassToMeteorMethods } from '../methods' import { MethodContext, MethodContextAPI } from './methodContext' import { isInTestWrite, triggerWriteAccessBecauseNoCheckNecessary } from '../security/securityVerify' import { endTrace, sendTrace, startTrace } from './integration/influx' import { interpollateTranslation, translateMessage } from '@sofie-automation/corelib/dist/TranslatableMessage' import { UserError } from '@sofie-automation/corelib/dist/error' import { StudioJobFunc } from '@sofie-automation/corelib/dist/worker/studio' +import { TSR } from '@sofie-automation/blueprints-integration' import { QueueStudioJob } from '../worker/worker' import { profiler } from './profiler' import { @@ -402,7 +402,7 @@ export namespace ServerClientAPI { } } -class ServerClientAPIClass extends MethodContextAPI implements NewClientAPI { +export class ServerClientAPIClass extends MethodContextAPI implements NewClientAPI { async clientLogger(type: string, ...args: string[]): Promise { triggerWriteAccessBecauseNoCheckNecessary() @@ -410,7 +410,7 @@ class ServerClientAPIClass extends MethodContextAPI implements NewClientAPI { loggerFunction(args.join(', ')) } - async clientErrorReport(timestamp: Time, errorString: string, location: string) { + async clientErrorReport(timestamp: Time, errorString: string, location: string): Promise { check(timestamp, Number) triggerWriteAccessBecauseNoCheckNecessary() // TODO: discuss if is this ok? logger.error( @@ -419,7 +419,14 @@ class ServerClientAPIClass extends MethodContextAPI implements NewClientAPI { }"\n at ${new Date(timestamp).toISOString()}:\n"${errorString}` ) } - async clientLogNotification(timestamp: Time, from: string, severity: number, message: string, source?: any) { + async clientLogNotification( + timestamp: Time, + from: string, + severity: number, + message: string, + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types + source?: any + ): Promise { check(timestamp, Number) triggerWriteAccessBecauseNoCheckNecessary() // TODO: discuss if is this ok? const address = this.connection ? this.connection.clientAddress : 'N/A' @@ -432,6 +439,7 @@ class ServerClientAPIClass extends MethodContextAPI implements NewClientAPI { address, }) } + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types async callPeripheralDeviceFunction( context: string, deviceId: PeripheralDeviceId, @@ -458,7 +466,7 @@ class ServerClientAPIClass extends MethodContextAPI implements NewClientAPI { timeoutTime: number | undefined, actionId: string, payload?: Record - ) { + ): Promise { const result = await ServerClientAPI.callPeripheralDeviceFunctionOrAction( this, context, @@ -488,4 +496,3 @@ class ServerClientAPIClass extends MethodContextAPI implements NewClientAPI { ) } } -registerClassToMeteorMethods(ClientAPIMethods, ServerClientAPIClass, false) diff --git a/meteor/server/api/ingest/debug.ts b/meteor/server/api/ingest/debug.ts index 160d62070b6..94a723f8251 100644 --- a/meteor/server/api/ingest/debug.ts +++ b/meteor/server/api/ingest/debug.ts @@ -7,9 +7,9 @@ import { IngestJobs } from '@sofie-automation/corelib/dist/worker/ingest' import { QueueStudioJob } from '../../worker/worker' import { StudioJobs } from '@sofie-automation/corelib/dist/worker/studio' import { RundownPlaylistId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { MeteorDebugMethods } from '../../methods' +import { MeteorDebugMethod } from '../../methods' -MeteorDebugMethods({ +export const ingestDebugMethods: { [key: string]: MeteorDebugMethod } = { /** * Simulate a 'Reload from NRCS' for the specified playlist */ @@ -46,4 +46,4 @@ MeteorDebugMethods({ segmentExternalId: segment.externalId, }) }, -}) +} diff --git a/meteor/server/api/mongo.ts b/meteor/server/api/mongo.ts index 656271381be..f053f8777a1 100644 --- a/meteor/server/api/mongo.ts +++ b/meteor/server/api/mongo.ts @@ -1,6 +1,5 @@ -import { registerClassToMeteorMethods } from '../methods' import { MethodContextAPI } from './methodContext' -import { MongoAPI, MongoAPIMethods } from '@sofie-automation/meteor-lib/dist/api/mongo' +import { MongoAPI } from '@sofie-automation/meteor-lib/dist/api/mongo' import { CollectionName } from '@sofie-automation/corelib/dist/dataModel/Collections' import { ProtectedString } from '@sofie-automation/corelib/dist/protectedString' import { logger } from '../logging' @@ -23,7 +22,8 @@ const ALLOWED_UPDATE_OPERATIONS = { $bit: 1, } -class MongoAPIClass extends MethodContextAPI implements MongoAPI { +export class MongoAPIClass extends MethodContextAPI implements MongoAPI { + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types async insertDocument(collectionName: CollectionName, _newDocument: any): Promise> { triggerWriteAccess() @@ -31,6 +31,7 @@ class MongoAPIClass extends MethodContextAPI implements MongoAPI { throw new Error('Not supported') } + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types async updateDocument(collectionName: CollectionName, selector: any, modifier: any, _options: any): Promise { triggerWriteAccess() @@ -92,6 +93,7 @@ class MongoAPIClass extends MethodContextAPI implements MongoAPI { return collection.updateAsync(currentDocument._id, modifier) } + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types async removeDocument(collectionName: CollectionName, _selector: any): Promise { triggerWriteAccess() @@ -99,4 +101,3 @@ class MongoAPIClass extends MethodContextAPI implements MongoAPI { throw new Meteor.Error(500, 'Not supported') } } -registerClassToMeteorMethods(MongoAPIMethods, MongoAPIClass, true) diff --git a/meteor/server/api/peripheralDevice.ts b/meteor/server/api/peripheralDevice.ts index ca864f3438d..5ac5edd050a 100644 --- a/meteor/server/api/peripheralDevice.ts +++ b/meteor/server/api/peripheralDevice.ts @@ -15,12 +15,12 @@ import { protectString, unprotectString } from '@sofie-automation/corelib/dist/p import { getCurrentTime } from '../lib/lib' import { logger } from '../logging' import { TimelineHash } from '@sofie-automation/corelib/dist/dataModel/Timeline' -import { registerClassToMeteorMethods } from '../methods' import { RundownInput } from './ingest/rundownInput' import { IngestRundown, IngestSegment, IngestPart, + IngestPlaylist, ExpectedPackageStatusAPI, PackageInfo, StatusCode, @@ -45,6 +45,8 @@ import { PeripheralDeviceStatusObject, TimelineTriggerTimeResult, DeviceStatusDetail, + DiffTimeResult, + TimeDiff, } from '@sofie-automation/shared-lib/dist/peripheralDevice/peripheralDeviceAPI' import type { PeripheralDeviceExternalEvent } from '@sofie-automation/shared-lib/dist/peripheralDevice/externalEvents' import { checkStudioExists } from '../optimizations' @@ -54,10 +56,9 @@ import { PeripheralDeviceCommandId, PeripheralDeviceId, } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { - NewPeripheralDeviceAPI, - PeripheralDeviceAPIMethods, -} from '@sofie-automation/shared-lib/dist/peripheralDevice/methodsAPI' +import { NewPeripheralDeviceAPI } from '@sofie-automation/shared-lib/dist/peripheralDevice/methodsAPI' +import { PeripheralDeviceForDevice } from '@sofie-automation/shared-lib/dist/core/model/peripheralDevice' +import { MediaObjectRevision } from '@sofie-automation/shared-lib/dist/peripheralDevice/mediaManager' import { insertInputDeviceTriggerIntoPreview } from '../publications/deviceTriggersPreview' import { receiveInputDeviceTrigger } from './deviceTriggers/observer' import { upsertBundles, generateTranslationBundleOriginId } from './translationsBundles' @@ -1082,17 +1083,17 @@ async function functionReply( } // Set up ALL PeripheralDevice methods: -class ServerPeripheralDeviceAPIClass extends MethodContextAPI implements NewPeripheralDeviceAPI { +export class ServerPeripheralDeviceAPIClass extends MethodContextAPI implements NewPeripheralDeviceAPI { // -------- System time -------- - async determineDiffTime() { + async determineDiffTime(): Promise { triggerWriteAccessBecauseNoCheckNecessary() return determineDiffTime() } - async getTimeDiff() { + async getTimeDiff(): Promise { triggerWriteAccessBecauseNoCheckNecessary() return getTimeDiff() } - async getTime() { + async getTime(): Promise { triggerWriteAccessBecauseNoCheckNecessary() return getCurrentTime() } @@ -1102,24 +1103,34 @@ class ServerPeripheralDeviceAPIClass extends MethodContextAPI implements NewPeri deviceId: PeripheralDeviceId, deviceToken: string, commandId: PeripheralDeviceCommandId, + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types err: any, + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types result: any - ) { + ): Promise { return functionReply(this, deviceId, deviceToken, commandId, err, result) } - async initialize(deviceId: PeripheralDeviceId, deviceToken: string, options: PeripheralDeviceInitOptions) { + async initialize( + deviceId: PeripheralDeviceId, + deviceToken: string, + options: PeripheralDeviceInitOptions + ): Promise { return ServerPeripheralDeviceAPI.initialize(this, deviceId, deviceToken, options) } - async unInitialize(deviceId: PeripheralDeviceId, deviceToken: string) { + async unInitialize(deviceId: PeripheralDeviceId, deviceToken: string): Promise { return ServerPeripheralDeviceAPI.unInitialize(this, deviceId, deviceToken) } - async setStatus(deviceId: PeripheralDeviceId, deviceToken: string, status: PeripheralDeviceStatusObject) { + async setStatus( + deviceId: PeripheralDeviceId, + deviceToken: string, + status: PeripheralDeviceStatusObject + ): Promise { return ServerPeripheralDeviceAPI.setStatus(this, deviceId, deviceToken, status) } - async ping(deviceId: PeripheralDeviceId, deviceToken: string) { + async ping(deviceId: PeripheralDeviceId, deviceToken: string): Promise { return ServerPeripheralDeviceAPI.ping(this, deviceId, deviceToken) } - async getPeripheralDevice(deviceId: PeripheralDeviceId, deviceToken: string) { + async getPeripheralDevice(deviceId: PeripheralDeviceId, deviceToken: string): Promise { const peripheralDevice = await checkAccessAndGetPeripheralDevice(deviceId, deviceToken, this) const studio = @@ -1133,35 +1144,44 @@ class ServerPeripheralDeviceAPIClass extends MethodContextAPI implements NewPeri deviceToken: string, message: string, cb?: (err: any | null, msg: any) => void - ) { + ): Promise { return ServerPeripheralDeviceAPI.pingWithCommand(this, deviceId, deviceToken, message, cb) } - async killProcess(deviceId: PeripheralDeviceId, deviceToken: string, really: boolean) { + async killProcess(deviceId: PeripheralDeviceId, deviceToken: string, really: boolean): Promise { return ServerPeripheralDeviceAPI.killProcess(this, deviceId, deviceToken, really) } - async testMethod(deviceId: PeripheralDeviceId, deviceToken: string, returnValue: string, throwError?: boolean) { + async testMethod( + deviceId: PeripheralDeviceId, + deviceToken: string, + returnValue: string, + throwError?: boolean + ): Promise { return ServerPeripheralDeviceAPI.testMethod(this, deviceId, deviceToken, returnValue, throwError) } - async removePeripheralDevice(deviceId: PeripheralDeviceId) { + async removePeripheralDevice(deviceId: PeripheralDeviceId): Promise { return ServerPeripheralDeviceAPI.removePeripheralDevice(this, deviceId) } // ------ Playout Gateway -------- - async timelineTriggerTime(deviceId: PeripheralDeviceId, deviceToken: string, r: TimelineTriggerTimeResult) { + async timelineTriggerTime( + deviceId: PeripheralDeviceId, + deviceToken: string, + r: TimelineTriggerTimeResult + ): Promise { return ServerPeripheralDeviceAPI.timelineTriggerTime(this, deviceId, deviceToken, r) } async playoutPlaybackChanged( deviceId: PeripheralDeviceId, deviceToken: string, changedResults: PlayoutChangedResults - ) { + ): Promise { return ServerPeripheralDeviceAPI.playoutPlaybackChanged(this, deviceId, deviceToken, changedResults) } async reportExternalEvents( deviceId: PeripheralDeviceId, deviceToken: string, events: PeripheralDeviceExternalEvent[] - ) { + ): Promise { return ServerPeripheralDeviceAPI.reportExternalEvents(this, deviceId, deviceToken, events) } async reportResolveDone( @@ -1169,42 +1189,62 @@ class ServerPeripheralDeviceAPIClass extends MethodContextAPI implements NewPeri deviceToken: string, timelineHash: TimelineHash, resolveDuration: number - ) { + ): Promise { return ServerPeripheralDeviceAPI.reportResolveDone(this, deviceId, deviceToken, timelineHash, resolveDuration) } // ------ Spreadsheet Gateway -------- - async requestUserAuthToken(deviceId: PeripheralDeviceId, deviceToken: string, authUrl: string) { + async requestUserAuthToken(deviceId: PeripheralDeviceId, deviceToken: string, authUrl: string): Promise { return ServerPeripheralDeviceAPI.requestUserAuthToken(this, deviceId, deviceToken, authUrl) } - async storeAccessToken(deviceId: PeripheralDeviceId, deviceToken: string, authToken: unknown) { + async storeAccessToken(deviceId: PeripheralDeviceId, deviceToken: string, authToken: string): Promise { return ServerPeripheralDeviceAPI.storeAccessToken(this, deviceId, deviceToken, authToken) } // ------ Ingest methods: ------------ - async dataPlaylistGet(deviceId: PeripheralDeviceId, deviceToken: string, playlistExternalId: string) { + async dataPlaylistGet( + deviceId: PeripheralDeviceId, + deviceToken: string, + playlistExternalId: string + ): Promise { return RundownInput.dataPlaylistGet(this, deviceId, deviceToken, playlistExternalId) } - async dataRundownList(deviceId: PeripheralDeviceId, deviceToken: string) { + async dataRundownList(deviceId: PeripheralDeviceId, deviceToken: string): Promise { return RundownInput.dataRundownList(this, deviceId, deviceToken) } - async dataRundownGet(deviceId: PeripheralDeviceId, deviceToken: string, rundownExternalId: string) { + async dataRundownGet( + deviceId: PeripheralDeviceId, + deviceToken: string, + rundownExternalId: string + ): Promise { return RundownInput.dataRundownGet(this, deviceId, deviceToken, rundownExternalId) } - async dataRundownDelete(deviceId: PeripheralDeviceId, deviceToken: string, rundownExternalId: string) { + async dataRundownDelete( + deviceId: PeripheralDeviceId, + deviceToken: string, + rundownExternalId: string + ): Promise { return RundownInput.dataRundownDelete(this, deviceId, deviceToken, rundownExternalId) } - async dataRundownCreate(deviceId: PeripheralDeviceId, deviceToken: string, ingestRundown: IngestRundown) { + async dataRundownCreate( + deviceId: PeripheralDeviceId, + deviceToken: string, + ingestRundown: IngestRundown + ): Promise { return RundownInput.dataRundownCreate(this, deviceId, deviceToken, ingestRundown) } - async dataRundownUpdate(deviceId: PeripheralDeviceId, deviceToken: string, ingestRundown: IngestRundown) { + async dataRundownUpdate( + deviceId: PeripheralDeviceId, + deviceToken: string, + ingestRundown: IngestRundown + ): Promise { return RundownInput.dataRundownUpdate(this, deviceId, deviceToken, ingestRundown) } async dataRundownMetaDataUpdate( deviceId: PeripheralDeviceId, deviceToken: string, ingestRundown: Omit - ) { + ): Promise { return RundownInput.dataRundownMetaDataUpdate(this, deviceId, deviceToken, ingestRundown) } async dataSegmentGet( @@ -1212,7 +1252,7 @@ class ServerPeripheralDeviceAPIClass extends MethodContextAPI implements NewPeri deviceToken: string, rundownExternalId: string, segmentExternalId: string - ) { + ): Promise { return RundownInput.dataSegmentGet(this, deviceId, deviceToken, rundownExternalId, segmentExternalId) } async dataSegmentDelete( @@ -1220,7 +1260,7 @@ class ServerPeripheralDeviceAPIClass extends MethodContextAPI implements NewPeri deviceToken: string, rundownExternalId: string, segmentExternalId: string - ) { + ): Promise { return RundownInput.dataSegmentDelete(this, deviceId, deviceToken, rundownExternalId, segmentExternalId) } async dataSegmentCreate( @@ -1228,7 +1268,7 @@ class ServerPeripheralDeviceAPIClass extends MethodContextAPI implements NewPeri deviceToken: string, rundownExternalId: string, ingestSegment: IngestSegment - ) { + ): Promise { return RundownInput.dataSegmentCreate(this, deviceId, deviceToken, rundownExternalId, ingestSegment) } async dataSegmentUpdate( @@ -1236,7 +1276,7 @@ class ServerPeripheralDeviceAPIClass extends MethodContextAPI implements NewPeri deviceToken: string, rundownExternalId: string, ingestSegment: IngestSegment - ) { + ): Promise { return RundownInput.dataSegmentUpdate(this, deviceId, deviceToken, rundownExternalId, ingestSegment) } async dataSegmentRanksUpdate( @@ -1244,7 +1284,7 @@ class ServerPeripheralDeviceAPIClass extends MethodContextAPI implements NewPeri deviceToken: string, rundownExternalId: string, newRanks: { [segmentExternalId: string]: number } - ) { + ): Promise { return RundownInput.dataSegmentRanksUpdate(this, deviceId, deviceToken, rundownExternalId, newRanks) } async dataPartDelete( @@ -1253,7 +1293,7 @@ class ServerPeripheralDeviceAPIClass extends MethodContextAPI implements NewPeri rundownExternalId: string, segmentExternalId: string, partExternalId: string - ) { + ): Promise { return RundownInput.dataPartDelete( this, deviceId, @@ -1269,7 +1309,7 @@ class ServerPeripheralDeviceAPIClass extends MethodContextAPI implements NewPeri rundownExternalId: string, segmentExternalId: string, ingestPart: IngestPart - ) { + ): Promise { return RundownInput.dataPartCreate( this, deviceId, @@ -1285,7 +1325,7 @@ class ServerPeripheralDeviceAPIClass extends MethodContextAPI implements NewPeri rundownExternalId: string, segmentExternalId: string, ingestPart: IngestPart - ) { + ): Promise { return RundownInput.dataPartUpdate( this, deviceId, @@ -1297,25 +1337,53 @@ class ServerPeripheralDeviceAPIClass extends MethodContextAPI implements NewPeri } // ------ MOS methods: -------- - async mosRoCreate(deviceId: PeripheralDeviceId, deviceToken: string, mosRunningOrder: MOS.IMOSRunningOrder) { + async mosRoCreate( + deviceId: PeripheralDeviceId, + deviceToken: string, + mosRunningOrder: MOS.IMOSRunningOrder + ): Promise { return MosIntegration.mosRoCreate(this, deviceId, deviceToken, mosRunningOrder) } - async mosRoReplace(deviceId: PeripheralDeviceId, deviceToken: string, mosRunningOrder: MOS.IMOSRunningOrder) { + async mosRoReplace( + deviceId: PeripheralDeviceId, + deviceToken: string, + mosRunningOrder: MOS.IMOSRunningOrder + ): Promise { return MosIntegration.mosRoReplace(this, deviceId, deviceToken, mosRunningOrder) } - async mosRoDelete(deviceId: PeripheralDeviceId, deviceToken: string, mosRunningOrderId: MOS.IMOSString128) { + async mosRoDelete( + deviceId: PeripheralDeviceId, + deviceToken: string, + mosRunningOrderId: MOS.IMOSString128 + ): Promise { return MosIntegration.mosRoDelete(this, deviceId, deviceToken, mosRunningOrderId) } - async mosRoMetadata(deviceId: PeripheralDeviceId, deviceToken: string, metadata: MOS.IMOSRunningOrderBase) { + async mosRoMetadata( + deviceId: PeripheralDeviceId, + deviceToken: string, + metadata: MOS.IMOSRunningOrderBase + ): Promise { return MosIntegration.mosRoMetadata(this, deviceId, deviceToken, metadata) } - async mosRoStatus(deviceId: PeripheralDeviceId, deviceToken: string, status: MOS.IMOSRunningOrderStatus) { + async mosRoStatus( + deviceId: PeripheralDeviceId, + deviceToken: string, + status: MOS.IMOSRunningOrderStatus + ): Promise { return MosIntegration.mosRoStatus(this, deviceId, deviceToken, status) } - async mosRoStoryStatus(deviceId: PeripheralDeviceId, deviceToken: string, status: MOS.IMOSStoryStatus) { + async mosRoStoryStatus( + deviceId: PeripheralDeviceId, + deviceToken: string, + status: MOS.IMOSStoryStatus + ): Promise { return MosIntegration.mosRoStoryStatus(this, deviceId, deviceToken, status) } - async mosRoItemStatus(deviceId: PeripheralDeviceId, deviceToken: string, status: MOS.IMOSItemStatus) { + async mosRoItemStatus( + deviceId: PeripheralDeviceId, + deviceToken: string, + status: MOS.IMOSItemStatus + ): Promise { return MosIntegration.mosRoItemStatus(this, deviceId, deviceToken, status) } async mosRoStoryInsert( @@ -1323,7 +1391,7 @@ class ServerPeripheralDeviceAPIClass extends MethodContextAPI implements NewPeri deviceToken: string, Action: MOS.IMOSStoryAction, Stories: Array - ) { + ): Promise { return MosIntegration.mosRoStoryInsert(this, deviceId, deviceToken, Action, Stories) } async mosRoItemInsert( @@ -1331,7 +1399,7 @@ class ServerPeripheralDeviceAPIClass extends MethodContextAPI implements NewPeri deviceToken: string, Action: MOS.IMOSItemAction, Items: Array - ) { + ): Promise { return MosIntegration.mosRoItemInsert(this, deviceId, deviceToken, Action, Items) } async mosRoStoryReplace( @@ -1339,7 +1407,7 @@ class ServerPeripheralDeviceAPIClass extends MethodContextAPI implements NewPeri deviceToken: string, Action: MOS.IMOSStoryAction, Stories: Array - ) { + ): Promise { return MosIntegration.mosRoStoryReplace(this, deviceId, deviceToken, Action, Stories) } async mosRoItemReplace( @@ -1347,7 +1415,7 @@ class ServerPeripheralDeviceAPIClass extends MethodContextAPI implements NewPeri deviceToken: string, Action: MOS.IMOSItemAction, Items: Array - ) { + ): Promise { return MosIntegration.mosRoItemReplace(this, deviceId, deviceToken, Action, Items) } async mosRoStoryMove( @@ -1355,7 +1423,7 @@ class ServerPeripheralDeviceAPIClass extends MethodContextAPI implements NewPeri deviceToken: string, Action: MOS.IMOSStoryAction, Stories: Array - ) { + ): Promise { return MosIntegration.mosRoStoryMove(this, deviceId, deviceToken, Action, Stories) } async mosRoItemMove( @@ -1363,7 +1431,7 @@ class ServerPeripheralDeviceAPIClass extends MethodContextAPI implements NewPeri deviceToken: string, Action: MOS.IMOSItemAction, Items: Array - ) { + ): Promise { return MosIntegration.mosRoItemMove(this, deviceId, deviceToken, Action, Items) } async mosRoStoryDelete( @@ -1371,7 +1439,7 @@ class ServerPeripheralDeviceAPIClass extends MethodContextAPI implements NewPeri deviceToken: string, Action: MOS.IMOSROAction, Stories: Array - ) { + ): Promise { return MosIntegration.mosRoStoryDelete(this, deviceId, deviceToken, Action, Stories) } async mosRoItemDelete( @@ -1379,7 +1447,7 @@ class ServerPeripheralDeviceAPIClass extends MethodContextAPI implements NewPeri deviceToken: string, Action: MOS.IMOSStoryAction, Items: Array - ) { + ): Promise { return MosIntegration.mosRoItemDelete(this, deviceId, deviceToken, Action, Items) } async mosRoStorySwap( @@ -1388,7 +1456,7 @@ class ServerPeripheralDeviceAPIClass extends MethodContextAPI implements NewPeri Action: MOS.IMOSROAction, StoryID0: MOS.IMOSString128, StoryID1: MOS.IMOSString128 - ) { + ): Promise { return MosIntegration.mosRoStorySwap(this, deviceId, deviceToken, Action, StoryID0, StoryID1) } async mosRoItemSwap( @@ -1397,17 +1465,25 @@ class ServerPeripheralDeviceAPIClass extends MethodContextAPI implements NewPeri Action: MOS.IMOSStoryAction, ItemID0: MOS.IMOSString128, ItemID1: MOS.IMOSString128 - ) { + ): Promise { return MosIntegration.mosRoItemSwap(this, deviceId, deviceToken, Action, ItemID0, ItemID1) } - async mosRoReadyToAir(deviceId: PeripheralDeviceId, deviceToken: string, Action: MOS.IMOSROReadyToAir) { + async mosRoReadyToAir( + deviceId: PeripheralDeviceId, + deviceToken: string, + Action: MOS.IMOSROReadyToAir + ): Promise { return MosIntegration.mosRoReadyToAir(this, deviceId, deviceToken, Action) } - async mosRoFullStory(deviceId: PeripheralDeviceId, deviceToken: string, story: MOS.IMOSROFullStory) { + async mosRoFullStory(deviceId: PeripheralDeviceId, deviceToken: string, story: MOS.IMOSROFullStory): Promise { return MosIntegration.mosRoFullStory(this, deviceId, deviceToken, story) } // ------- Expected Playout Items (Previously: Media Manager (Media Scanner)) - async getMediaObjectRevisions(deviceId: PeripheralDeviceId, deviceToken: string, collectionId: string) { + async getMediaObjectRevisions( + deviceId: PeripheralDeviceId, + deviceToken: string, + collectionId: string + ): Promise { return MediaScannerIntegration.getMediaObjectRevisions(this, deviceId, deviceToken, collectionId) } async updateMediaObject( @@ -1416,10 +1492,14 @@ class ServerPeripheralDeviceAPIClass extends MethodContextAPI implements NewPeri collectionId: string, id: string, doc: MediaObject | null - ) { + ): Promise { return MediaScannerIntegration.updateMediaObject(this, deviceId, deviceToken, collectionId, id, doc) } - async clearMediaObjectCollection(deviceId: PeripheralDeviceId, deviceToken: string, collectionId: string) { + async clearMediaObjectCollection( + deviceId: PeripheralDeviceId, + deviceToken: string, + collectionId: string + ): Promise { return MediaScannerIntegration.clearMediaObjectCollection(this, deviceId, deviceToken, collectionId) } // ------- Package Manager -------------- @@ -1445,7 +1525,7 @@ class ServerPeripheralDeviceAPIClass extends MethodContextAPI implements NewPeri ): Promise { await PackageManagerIntegration.updateExpectedPackageWorkStatuses(this, deviceId, deviceToken, changes) } - async removeAllExpectedPackageWorkStatusOfDevice(deviceId: PeripheralDeviceId, deviceToken: string) { + async removeAllExpectedPackageWorkStatusOfDevice(deviceId: PeripheralDeviceId, deviceToken: string): Promise { await PackageManagerIntegration.removeAllExpectedPackageWorkStatusOfDevice(this, deviceId, deviceToken) } async updatePackageContainerPackageStatuses( @@ -1467,7 +1547,10 @@ class ServerPeripheralDeviceAPIClass extends MethodContextAPI implements NewPeri ): Promise { await PackageManagerIntegration.updatePackageContainerPackageStatuses(this, deviceId, deviceToken, changes) } - async removeAllPackageContainerPackageStatusesOfDevice(deviceId: PeripheralDeviceId, deviceToken: string) { + async removeAllPackageContainerPackageStatusesOfDevice( + deviceId: PeripheralDeviceId, + deviceToken: string + ): Promise { await PackageManagerIntegration.removeAllPackageContainerPackageStatusesOfDevice(this, deviceId, deviceToken) } async updatePackageContainerStatuses( @@ -1487,7 +1570,7 @@ class ServerPeripheralDeviceAPIClass extends MethodContextAPI implements NewPeri ): Promise { await PackageManagerIntegration.updatePackageContainerStatuses(this, deviceId, deviceToken, changes) } - async removeAllPackageContainerStatusesOfDevice(deviceId: PeripheralDeviceId, deviceToken: string) { + async removeAllPackageContainerStatusesOfDevice(deviceId: PeripheralDeviceId, deviceToken: string): Promise { await PackageManagerIntegration.removeAllPackageContainerStatusesOfDevice(this, deviceId, deviceToken) } @@ -1496,7 +1579,9 @@ class ServerPeripheralDeviceAPIClass extends MethodContextAPI implements NewPeri deviceToken: string, type: string, packageIds: ExpectedPackageId[] - ) { + ): Promise< + { packageId: ExpectedPackageId; expectedContentVersionHash: string; actualContentVersionHash: string }[] + > { return PackageManagerIntegration.fetchPackageInfoMetadata(this, deviceId, deviceToken, type, packageIds) } async updatePackageInfo( @@ -1506,8 +1591,9 @@ class ServerPeripheralDeviceAPIClass extends MethodContextAPI implements NewPeri packageId: ExpectedPackageId, expectedContentVersionHash: string, actualContentVersionHash: string, + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types payload: any - ) { + ): Promise { await PackageManagerIntegration.updatePackageInfo( this, deviceId, @@ -1525,7 +1611,7 @@ class ServerPeripheralDeviceAPIClass extends MethodContextAPI implements NewPeri type: string, packageId: ExpectedPackageId, removeDelay?: number - ) { + ): Promise { await PackageManagerIntegration.removePackageInfo(this, deviceId, deviceToken, type, packageId, removeDelay) } // --- Triggers --- @@ -1539,9 +1625,8 @@ class ServerPeripheralDeviceAPIClass extends MethodContextAPI implements NewPeri triggerDeviceId: string, triggerId: string, values?: Record | null - ) { + ): Promise { await receiveInputDeviceTrigger(this, deviceId, deviceToken, triggerDeviceId, triggerId, values ?? undefined) await insertInputDeviceTriggerIntoPreview(deviceId, triggerDeviceId, triggerId, values ?? undefined) } } -registerClassToMeteorMethods(PeripheralDeviceAPIMethods, ServerPeripheralDeviceAPIClass, false) diff --git a/meteor/server/api/playout/api.ts b/meteor/server/api/playout/api.ts index 8b18fed8aa9..37be03b89de 100644 --- a/meteor/server/api/playout/api.ts +++ b/meteor/server/api/playout/api.ts @@ -1,8 +1,5 @@ -import { registerClassToMeteorMethods, MeteorDebugMethods } from '../../methods' -import { NewPlayoutAPI, PlayoutAPIMethods } from '@sofie-automation/meteor-lib/dist/api/playout' +import { NewPlayoutAPI } from '@sofie-automation/meteor-lib/dist/api/playout' import { ServerPlayoutAPI } from './playout' -import { getCurrentTime } from '../../lib/lib' -import { logger } from '../../logging' import { MethodContextAPI } from '../methodContext' import { QueueStudioJob } from '../../worker/worker' import { StudioJobs } from '@sofie-automation/corelib/dist/worker/studio' @@ -15,14 +12,14 @@ import { Meteor } from 'meteor/meteor' const PERMISSIONS_FOR_STUDIO_BASELINE: Array = ['configure', 'studio'] -class ServerPlayoutAPIClass extends MethodContextAPI implements NewPlayoutAPI { +export class ServerPlayoutAPIClass extends MethodContextAPI implements NewPlayoutAPI { async updateStudioBaseline(studioId: StudioId): Promise { assertConnectionHasOneOfPermissions(this.connection, ...PERMISSIONS_FOR_STUDIO_BASELINE) const res = await QueueStudioJob(StudioJobs.UpdateStudioBaseline, studioId, undefined) return res.complete } - async shouldUpdateStudioBaseline(studioId: StudioId) { + async shouldUpdateStudioBaseline(studioId: StudioId): Promise { assertConnectionHasOneOfPermissions(this.connection, ...PERMISSIONS_FOR_STUDIO_BASELINE) const studio = await Studios.findOneAsync(studioId) @@ -31,13 +28,3 @@ class ServerPlayoutAPIClass extends MethodContextAPI implements NewPlayoutAPI { return ServerPlayoutAPI.shouldUpdateStudioBaseline(studio) } } -registerClassToMeteorMethods(PlayoutAPIMethods, ServerPlayoutAPIClass, false) - -// Temporary methods -MeteorDebugMethods({ - debug__printTime: () => { - const now = getCurrentTime() - logger.debug(new Date(now)) - return now - }, -}) diff --git a/meteor/server/api/playout/debug.ts b/meteor/server/api/playout/debug.ts index fda81c3bbd6..33ae7a6d6e8 100644 --- a/meteor/server/api/playout/debug.ts +++ b/meteor/server/api/playout/debug.ts @@ -8,12 +8,12 @@ import { StudioJobs } from '@sofie-automation/corelib/dist/worker/studio' import { fetchStudioIds } from '../../optimizations' import { PeripheralDeviceId, RundownPlaylistId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { insertInputDeviceTriggerIntoPreview } from '../../publications/deviceTriggersPreview' -import { MeteorDebugMethods } from '../../methods' +import { MeteorDebugMethod } from '../../methods' // These are temporary method to fill the rundown database with some sample data // for development -MeteorDebugMethods({ +export const playoutDebugMethods: { [key: string]: MeteorDebugMethod } = { /** * Remove a playlist from the system. * This can be done in the ui too, but this will bypass any checks that are usually performed @@ -143,4 +143,4 @@ MeteorDebugMethods({ await insertInputDeviceTriggerIntoPreview(peripheralDeviceId, triggerDeviceId, triggerId, values) }, -}) +} diff --git a/meteor/server/api/rest/api.ts b/meteor/server/api/rest/api.ts index 4496cd493b5..8f8ab4c1018 100644 --- a/meteor/server/api/rest/api.ts +++ b/meteor/server/api/rest/api.ts @@ -1,6 +1,5 @@ import KoaRouter from '@koa/router' import { bindKoaRouter } from './koa' -import { Meteor } from 'meteor/meteor' import koa from 'koa' import { koaRouter as apiV1Router } from './v1/index' import { snapshotPrivateApiRouter } from '../snapshot' @@ -12,37 +11,43 @@ import { blueprintsRouter } from '../blueprints/http' import { createLegacyApiRouter } from './v0/index' import { heapSnapshotPrivateApiRouter } from '../heapSnapshot' import { getRootSubpath } from '../../lib' +import type { MethodRegistry } from '../../methodRegistry' +import type { PublicationRegistry } from '../../publicationRegistry' const LATEST_REST_API = 'v1.0' -const apiRouter = new KoaRouter() - -apiRouter.get('/', redirectToLatest) -apiRouter.get('/latest', redirectToLatest) - -apiRouter.use('/v1.0', apiV1Router.routes(), apiV1Router.allowedMethods()) - -apiRouter.use('/private/ingest', ingestRouter.routes(), ingestRouter.allowedMethods()) -apiRouter.use('/private/snapshot', snapshotPrivateApiRouter.routes(), snapshotPrivateApiRouter.allowedMethods()) -apiRouter.use('/private/shelfLayouts', shelfLayoutsRouter.routes(), shelfLayoutsRouter.allowedMethods()) -apiRouter.use('/private/actionTriggers', actionTriggersRouter.routes(), actionTriggersRouter.allowedMethods()) -apiRouter.use('/private/peripheralDevices', peripheralDeviceRouter.routes(), peripheralDeviceRouter.allowedMethods()) -apiRouter.use('/private/blueprints', blueprintsRouter.routes(), blueprintsRouter.allowedMethods()) -apiRouter.use( - '/private/heapSnapshot', - heapSnapshotPrivateApiRouter.routes(), - heapSnapshotPrivateApiRouter.allowedMethods() -) - async function redirectToLatest(ctx: koa.ParameterizedContext, _next: koa.Next): Promise { ctx.redirect(`${getRootSubpath()}/api/${LATEST_REST_API}`) ctx.status = 307 } -Meteor.startup(() => { +export function bindRestApiRouter(methodRegistry: MethodRegistry, publicationRegistry: PublicationRegistry): void { + const apiRouter = new KoaRouter() + + apiRouter.get('/', redirectToLatest) + apiRouter.get('/latest', redirectToLatest) + + apiRouter.use('/v1.0', apiV1Router.routes(), apiV1Router.allowedMethods()) + + apiRouter.use('/private/ingest', ingestRouter.routes(), ingestRouter.allowedMethods()) + apiRouter.use('/private/snapshot', snapshotPrivateApiRouter.routes(), snapshotPrivateApiRouter.allowedMethods()) + apiRouter.use('/private/shelfLayouts', shelfLayoutsRouter.routes(), shelfLayoutsRouter.allowedMethods()) + apiRouter.use('/private/actionTriggers', actionTriggersRouter.routes(), actionTriggersRouter.allowedMethods()) + apiRouter.use( + '/private/peripheralDevices', + peripheralDeviceRouter.routes(), + peripheralDeviceRouter.allowedMethods() + ) + apiRouter.use('/private/blueprints', blueprintsRouter.routes(), blueprintsRouter.allowedMethods()) + apiRouter.use( + '/private/heapSnapshot', + heapSnapshotPrivateApiRouter.routes(), + heapSnapshotPrivateApiRouter.allowedMethods() + ) + // Needs to be lazily generated - const legacyApiRouter = createLegacyApiRouter() + const legacyApiRouter = createLegacyApiRouter(methodRegistry, publicationRegistry) apiRouter.use('/0', legacyApiRouter.routes(), legacyApiRouter.allowedMethods()) bindKoaRouter(apiRouter, '/api') -}) +} diff --git a/meteor/server/api/rest/v0/__tests__/rest.test.ts b/meteor/server/api/rest/v0/__tests__/rest.test.ts index e39fa5ba46e..8ad0c68772b 100644 --- a/meteor/server/api/rest/v0/__tests__/rest.test.ts +++ b/meteor/server/api/rest/v0/__tests__/rest.test.ts @@ -1,11 +1,12 @@ import { MeteorMock } from '../../../../../__mocks__/meteor' import { Meteor } from 'meteor/meteor' import { UserActionAPIMethods } from '@sofie-automation/meteor-lib/dist/api/userActions' -import { MeteorMethodSignatures } from '../../../../methods' +import { MethodRegistry, AnyMethodApiRegistration } from '../../../../methodRegistry' +import { PublicationRegistry } from '../../../../publicationRegistry' import { ClientAPI } from '@sofie-automation/meteor-lib/dist/api/client' import { callKoaRoute } from '../../../../../__mocks__/koa-util' import { createLegacyApiRouter } from '..' -import '../../../userActions' // required to get the UserActionsAPI methods populated +import { ServerUserActionAPI } from '../../../userActions' // we don't want the deviceTriggers observer to start up at this time jest.mock('../../../deviceTriggers/observer') @@ -18,11 +19,19 @@ describe('REST API', () => { await MeteorMock.mockRunMeteorStartup() }) - const legacyApiRouter = createLegacyApiRouter() + const methodRegistry = new MethodRegistry() + methodRegistry.registerApi({ + methods: UserActionAPIMethods, + class: ServerUserActionAPI, + } as unknown as AnyMethodApiRegistration) + methodRegistry.applyToMeteor() // register the methods on the (mock) Meteor server + const methodSignatures = methodRegistry.getSignatures() + const publicationRegistry = new PublicationRegistry() + const legacyApiRouter = createLegacyApiRouter(methodRegistry, publicationRegistry) test('calls the UserActionAPI methods, when doing a POST to the endpoint', async () => { for (const [methodName, methodValue] of Object.entries(UserActionAPIMethods)) { - const signature = MeteorMethodSignatures[methodValue] + const signature = methodSignatures[methodValue] let docString = `/action/${methodName}` for (const paramName of signature || []) { @@ -51,7 +60,7 @@ describe('REST API', () => { const methodName = Object.keys(UserActionAPIMethods)[0] const methodValue: string = (UserActionAPIMethods as any)[methodName] - const signature = MeteorMethodSignatures[methodValue] + const signature = methodSignatures[methodValue] let docString = `/action/${methodName}` for (const paramName of signature || []) { @@ -77,7 +86,7 @@ describe('REST API', () => { const methodName = Object.keys(UserActionAPIMethods)[0] const methodValue: string = (UserActionAPIMethods as any)[methodName] - const signature = MeteorMethodSignatures[methodValue] + const signature = methodSignatures[methodValue] let docString = `/action/${methodName}` for (const paramName of signature || []) { @@ -103,7 +112,7 @@ describe('REST API', () => { const methodName = Object.keys(UserActionAPIMethods)[0] const methodValue: string = (UserActionAPIMethods as any)[methodName] - const signature = MeteorMethodSignatures[methodValue] || [] + const signature = methodSignatures[methodValue] || [] const params: any[] = ['one', true, false, { one: 'two' }, null, 1.323, 30] @@ -152,7 +161,7 @@ describe('REST API', () => { const index = JSON.parse(ctx.response.body as string) for (const [methodName, methodValue] of Object.entries(UserActionAPIMethods)) { - const signature = MeteorMethodSignatures[methodValue] + const signature = methodSignatures[methodValue] let docString = `/api/0/action/${methodName}` for (const paramName of signature || []) { diff --git a/meteor/server/api/rest/v0/index.ts b/meteor/server/api/rest/v0/index.ts index 1203e2a63a8..7baeb1878b8 100644 --- a/meteor/server/api/rest/v0/index.ts +++ b/meteor/server/api/rest/v0/index.ts @@ -6,9 +6,10 @@ import _ from 'underscore' import { Meteor } from 'meteor/meteor' -import { MeteorMethodSignatures } from '../../../methods' +import type { MethodRegistry } from '../../../methodRegistry' +import type { PublicationRegistry } from '../../../publicationRegistry' +import type { PublicationContext } from '../../../publications/lib/lib' import { MeteorPubSub } from '@sofie-automation/meteor-lib/dist/api/pubsub' -import { MeteorPublications, MeteorPublicationSignatures } from '../../../publications/lib/lib' import { UserActionAPIMethods } from '@sofie-automation/meteor-lib/dist/api/userActions' import { logger } from '../../../logging' import { ClientAPI } from '@sofie-automation/meteor-lib/dist/api/client' @@ -19,6 +20,20 @@ import { PeripheralDevicePubSub } from '@sofie-automation/shared-lib/dist/pubsub const LEGACY_API_VERSION = 0 +/** + * A no-op publication context for the legacy REST path: there is no live subscription, the callback is + * only invoked to obtain its cursor, which is then fetched once. There is no connection, so any + * publication that requires one will reject (matching the historical behaviour). + */ +const legacyRestPublicationContext: PublicationContext = { + connection: null, + onStop: () => undefined, + ready: () => undefined, + added: () => undefined, + changed: () => undefined, + removed: () => undefined, +} + /** * Takes an array of strings and converts them to Null, Boolean, Number, String primitives or Objects, if the string * seems like a valid JSON. @@ -54,9 +69,15 @@ function typeConvertUrlParameters(args: any[]) { return convertedArgs } -export function createLegacyApiRouter(): KoaRouter { +export function createLegacyApiRouter( + methodRegistry: MethodRegistry, + publicationRegistry: PublicationRegistry +): KoaRouter { const router = new KoaRouter() + const methodSignatures = methodRegistry.getSignatures() + const publicationSignatures = publicationRegistry.getSignatures() + const index = { version: `${LEGACY_API_VERSION}`, GET: [] as string[], @@ -66,7 +87,7 @@ export function createLegacyApiRouter(): KoaRouter { // Expose all user actions: for (const [methodName, methodValue] of Object.entries(UserActionAPIMethods)) { - const signature = MeteorMethodSignatures[methodValue] || [] + const signature = methodSignatures[methodValue] || [] let resource = `/action/${methodName}` let docString = `/api/${LEGACY_API_VERSION}${resource}` @@ -85,9 +106,9 @@ export function createLegacyApiRouter(): KoaRouter { } function exposePublication(pubName: string, pubValue: string) { - const signature = MeteorPublicationSignatures[pubValue] || [] + const signature = publicationSignatures[pubValue] || [] - const f = MeteorPublications[pubValue] + const f = publicationRegistry.getCursorPublication(pubValue) if (f) { let resource = `/publication/${pubName}` @@ -101,12 +122,10 @@ export function createLegacyApiRouter(): KoaRouter { assignRoute(router, 'GET', resource, signature.length, async (args) => { const convArgs = typeConvertUrlParameters(args) - const cursor = await f.apply( - { - ready: () => null, - }, - convArgs - ) + const cursor = (await f(legacyRestPublicationContext, ...convArgs)) as + | { fetch: () => unknown } + | null + | undefined if (cursor) return cursor.fetch() return [] diff --git a/meteor/server/api/rundown.ts b/meteor/server/api/rundown.ts index 30c038953e6..00002f9fb1d 100644 --- a/meteor/server/api/rundown.ts +++ b/meteor/server/api/rundown.ts @@ -1,8 +1,7 @@ import _ from 'underscore' import { check } from '../lib/check' import { logger } from '../logging' -import { registerClassToMeteorMethods } from '../methods' -import { NewRundownAPI, RundownAPIMethods } from '@sofie-automation/meteor-lib/dist/api/rundown' +import { NewRundownAPI } from '@sofie-automation/meteor-lib/dist/api/rundown' import { DBShowStyleVariant } from '@sofie-automation/corelib/dist/dataModel/ShowStyleVariant' import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' import { PackageInfo } from '../coreSystem' @@ -129,9 +128,8 @@ export namespace ClientRundownAPI { } } -class ServerRundownAPIClass extends MethodContextAPI implements NewRundownAPI { - async rundownPlaylistNeedsResync(playlistId: RundownPlaylistId) { +export class ServerRundownAPIClass extends MethodContextAPI implements NewRundownAPI { + async rundownPlaylistNeedsResync(playlistId: RundownPlaylistId): Promise { return ClientRundownAPI.rundownPlaylistNeedsResync(this, playlistId) } } -registerClassToMeteorMethods(RundownAPIMethods, ServerRundownAPIClass, false) diff --git a/meteor/server/api/rundownLayouts.ts b/meteor/server/api/rundownLayouts.ts index c75570804da..d167e3fc66f 100644 --- a/meteor/server/api/rundownLayouts.ts +++ b/meteor/server/api/rundownLayouts.ts @@ -1,7 +1,6 @@ import { Meteor } from 'meteor/meteor' import { check, Match } from '../lib/check' -import { registerClassToMeteorMethods } from '../methods' -import { NewRundownLayoutsAPI, RundownLayoutsAPIMethods } from '@sofie-automation/meteor-lib/dist/api/rundownLayouts' +import { NewRundownLayoutsAPI } from '@sofie-automation/meteor-lib/dist/api/rundownLayouts' import { RundownLayoutType, RundownLayoutBase, @@ -150,17 +149,16 @@ async function apiRemoveRundownLayout(context: MethodContext, id: RundownLayoutI await removeRundownLayout(id) } -class ServerRundownLayoutsAPI extends MethodContextAPI implements NewRundownLayoutsAPI { +export class ServerRundownLayoutsAPI extends MethodContextAPI implements NewRundownLayoutsAPI { async createRundownLayout( name: string, type: RundownLayoutType, showStyleBaseId: ShowStyleBaseId, regionId: CustomizableRegions - ) { + ): Promise { return apiCreateRundownLayout(this, name, type, showStyleBaseId, regionId) } - async removeRundownLayout(rundownLayoutId: RundownLayoutId) { + async removeRundownLayout(rundownLayoutId: RundownLayoutId): Promise { return apiRemoveRundownLayout(this, rundownLayoutId) } } -registerClassToMeteorMethods(RundownLayoutsAPIMethods, ServerRundownLayoutsAPI, false) diff --git a/meteor/server/api/showStyles.ts b/meteor/server/api/showStyles.ts index 9a097621fb7..c6bdd64bcbd 100644 --- a/meteor/server/api/showStyles.ts +++ b/meteor/server/api/showStyles.ts @@ -1,10 +1,5 @@ import { check } from '../lib/check' -import { registerClassToMeteorMethods } from '../methods' -import { - CreateAdlibTestingRundownOption, - NewShowStylesAPI, - ShowStylesAPIMethods, -} from '@sofie-automation/meteor-lib/dist/api/showStyles' +import { CreateAdlibTestingRundownOption, NewShowStylesAPI } from '@sofie-automation/meteor-lib/dist/api/showStyles' import { Meteor } from 'meteor/meteor' import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' import { DBShowStyleVariant } from '@sofie-automation/corelib/dist/dataModel/ShowStyleVariant' @@ -282,31 +277,30 @@ async function getCreateAdlibTestingRundownOptions(context: MethodContext): Prom return options } -class ServerShowStylesAPI extends MethodContextAPI implements NewShowStylesAPI { - async insertShowStyleBase() { +export class ServerShowStylesAPI extends MethodContextAPI implements NewShowStylesAPI { + async insertShowStyleBase(): Promise { return insertShowStyleBase(this) } - async insertShowStyleVariant(showStyleBaseId: ShowStyleBaseId) { + async insertShowStyleVariant(showStyleBaseId: ShowStyleBaseId): Promise { return insertShowStyleVariant(this, showStyleBaseId) } - async importShowStyleVariant(showStyleVariant: DBShowStyleVariant) { + async importShowStyleVariant(showStyleVariant: DBShowStyleVariant): Promise { return importShowStyleVariant(this, showStyleVariant) } - async importShowStyleVariantAsNew(showStyleVariant: Omit) { + async importShowStyleVariantAsNew(showStyleVariant: Omit): Promise { return importShowStyleVariantAsNew(this, showStyleVariant) } - async removeShowStyleBase(showStyleBaseId: ShowStyleBaseId) { + async removeShowStyleBase(showStyleBaseId: ShowStyleBaseId): Promise { return removeShowStyleBase(this, showStyleBaseId) } - async removeShowStyleVariant(showStyleVariantId: ShowStyleVariantId) { + async removeShowStyleVariant(showStyleVariantId: ShowStyleVariantId): Promise { return removeShowStyleVariant(this, showStyleVariantId) } - async reorderShowStyleVariant(showStyleVariantId: ShowStyleVariantId, newRank: number) { + async reorderShowStyleVariant(showStyleVariantId: ShowStyleVariantId, newRank: number): Promise { return reorderShowStyleVariant(this, showStyleVariantId, newRank) } - async getCreateAdlibTestingRundownOptions() { + async getCreateAdlibTestingRundownOptions(): Promise { return getCreateAdlibTestingRundownOptions(this) } } -registerClassToMeteorMethods(ShowStylesAPIMethods, ServerShowStylesAPI, false) diff --git a/meteor/server/api/snapshot.ts b/meteor/server/api/snapshot.ts index aee61efff6a..13deafed6a5 100644 --- a/meteor/server/api/snapshot.ts +++ b/meteor/server/api/snapshot.ts @@ -27,10 +27,8 @@ import { PeripheralDevice, PERIPHERAL_SUBTYPE_PROCESS } from '@sofie-automation/ import { logger } from '../logging' import { TimelineComplete } from '@sofie-automation/corelib/dist/dataModel/Timeline' import { PeripheralDeviceCommand } from '@sofie-automation/corelib/dist/dataModel/PeripheralDeviceCommand' -import { registerClassToMeteorMethods } from '../methods' import { NewSnapshotAPI, - SnapshotAPIMethods, PlaylistSnapshotOptions, SystemSnapshotOptions, } from '@sofie-automation/meteor-lib/dist/api/shapshot' @@ -904,14 +902,18 @@ snapshotPrivateApiRouter.get('/retrieve/:snapshotId', async (ctx) => { }) }) -class ServerSnapshotAPI extends MethodContextAPI implements NewSnapshotAPI { - async storeSystemSnapshot(hashedToken: string, studioId: StudioId | null, reason: string) { +export class ServerSnapshotAPI extends MethodContextAPI implements NewSnapshotAPI { + async storeSystemSnapshot(hashedToken: string, studioId: StudioId | null, reason: string): Promise { if (!verifyHashedToken(hashedToken)) { throw new Meteor.Error(401, `Idempotency token is invalid or has expired`) } return storeSystemSnapshot(this, { studioId: studioId ?? undefined }, reason) } - async storeRundownPlaylist(hashedToken: string, playlistId: RundownPlaylistId, reason: string) { + async storeRundownPlaylist( + hashedToken: string, + playlistId: RundownPlaylistId, + reason: string + ): Promise { if (!verifyHashedToken(hashedToken)) { throw new Meteor.Error(401, `Idempotency token is invalid or has expired`) } @@ -919,14 +921,13 @@ class ServerSnapshotAPI extends MethodContextAPI implements NewSnapshotAPI { const playlist = await checkAccessToPlaylist(this.connection, playlistId) return storeRundownPlaylistSnapshot(playlist, {}, reason) } - async storeDebugSnapshot(hashedToken: string, studioId: StudioId, reason: string) { + async storeDebugSnapshot(hashedToken: string, studioId: StudioId, reason: string): Promise { return storeDebugSnapshot(this, hashedToken, studioId, reason) } - async restoreSnapshot(snapshotId: SnapshotId, restoreDebugData: boolean) { + async restoreSnapshot(snapshotId: SnapshotId, restoreDebugData: boolean): Promise { return restoreSnapshot(this, snapshotId, restoreDebugData) } - async removeSnapshot(snapshotId: SnapshotId) { + async removeSnapshot(snapshotId: SnapshotId): Promise { return removeSnapshot(this, snapshotId) } } -registerClassToMeteorMethods(SnapshotAPIMethods, ServerSnapshotAPI, false) diff --git a/meteor/server/api/studio/api.ts b/meteor/server/api/studio/api.ts index 0e245cab794..8f17d1946f6 100644 --- a/meteor/server/api/studio/api.ts +++ b/meteor/server/api/studio/api.ts @@ -1,7 +1,6 @@ import { Meteor } from 'meteor/meteor' import { check } from '../../lib/check' -import { registerClassToMeteorMethods } from '../../methods' -import { NewStudiosAPI, StudiosAPIMethods } from '@sofie-automation/meteor-lib/dist/api/studios' +import { NewStudiosAPI } from '@sofie-automation/meteor-lib/dist/api/studios' import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { literal, getRandomId } from '@sofie-automation/corelib/dist/lib' import { protectString } from '@sofie-automation/corelib/dist/protectedString' @@ -139,15 +138,19 @@ async function removeStudio(context: MethodContext, studioId: StudioId): Promise ]) } -class ServerStudiosAPI extends MethodContextAPI implements NewStudiosAPI { - async insertStudio() { +export class ServerStudiosAPI extends MethodContextAPI implements NewStudiosAPI { + async insertStudio(): Promise { return insertStudio(this) } - async removeStudio(studioId: StudioId) { + async removeStudio(studioId: StudioId): Promise { return removeStudio(this, studioId) } - async assignConfigToPeripheralDevice(studioId: StudioId, configId: string, deviceId: PeripheralDeviceId | null) { + async assignConfigToPeripheralDevice( + studioId: StudioId, + configId: string, + deviceId: PeripheralDeviceId | null + ): Promise { assertConnectionHasOneOfPermissions(this.connection, ...PERMISSIONS_FOR_MANAGE_STUDIOS) // Unassign other uses @@ -182,7 +185,6 @@ class ServerStudiosAPI extends MethodContextAPI implements NewStudiosAPI { } } } -registerClassToMeteorMethods(StudiosAPIMethods, ServerStudiosAPI, false) // Set up a watcher for updating the mappingsHash whenever a mapping or route is changed: function triggerUpdateStudioMappingsHash(studioId: StudioId) { diff --git a/meteor/server/api/system.ts b/meteor/server/api/system.ts index b77b19485ee..738fd732292 100644 --- a/meteor/server/api/system.ts +++ b/meteor/server/api/system.ts @@ -1,10 +1,8 @@ import _ from 'underscore' import type { Time } from '@sofie-automation/shared-lib/dist/lib/lib' import { sleep, getCurrentTime } from '../lib/lib' -import { registerClassToMeteorMethods } from '../methods' import { MethodContextAPI, MethodContext } from './methodContext' import { - SystemAPIMethods, CollectionCleanupResult, SystemAPI, BenchmarkResult, @@ -389,17 +387,17 @@ async function generateSingleUseToken() { return ClientAPI.responseSuccess(newToken) } -class SystemAPIClass extends MethodContextAPI implements SystemAPI { - async cleanupIndexes(actuallyRemoveOldIndexes: boolean) { +export class SystemAPIClass extends MethodContextAPI implements SystemAPI { + async cleanupIndexes(actuallyRemoveOldIndexes: boolean): Promise { return cleanupIndexes(this, actuallyRemoveOldIndexes) } - async cleanupOldData(actuallyRemoveOldData: boolean) { + async cleanupOldData(actuallyRemoveOldData: boolean): Promise { return cleanupOldData(this, actuallyRemoveOldData) } - async runCronjob() { + async runCronjob(): Promise { return runCronjob(this) } - async doSystemBenchmark(runCount = 1) { + async doSystemBenchmark(runCount = 1): Promise { return doSystemBenchmark(this, runCount) } async getTranslationBundle(bundleId: TranslationsBundleId): Promise> { @@ -409,4 +407,3 @@ class SystemAPIClass extends MethodContextAPI implements SystemAPI { return generateSingleUseToken() } } -registerClassToMeteorMethods(SystemAPIMethods, SystemAPIClass, false) diff --git a/meteor/server/api/triggeredActions.ts b/meteor/server/api/triggeredActions.ts index ed64ca075a9..a160d7b6b56 100644 --- a/meteor/server/api/triggeredActions.ts +++ b/meteor/server/api/triggeredActions.ts @@ -1,6 +1,6 @@ import { Meteor } from 'meteor/meteor' import { check, Match } from '../lib/check' -import { registerClassToMeteorMethods, ReplaceOptionalWithNullInMethodArguments } from '../methods' +import { ReplaceOptionalWithNullInMethodArguments } from '../methods' import { literal, getRandomId, Complete } from '@sofie-automation/corelib/dist/lib' import { protectString } from '@sofie-automation/corelib/dist/protectedString' import { logger } from '../logging' @@ -9,7 +9,6 @@ import { DBTriggeredActions, TriggeredActionsObj } from '@sofie-automation/meteo import { CreateTriggeredActionsContent, NewTriggeredActionsAPI, - TriggeredActionsAPIMethods, } from '@sofie-automation/meteor-lib/dist/api/triggeredActions' import { fetchShowStyleBaseLight } from '../optimizations' import { @@ -178,15 +177,17 @@ async function apiRemoveTriggeredActions(context: MethodContext, id: TriggeredAc await removeTriggeredActions(id) } -class ServerTriggeredActionsAPI +export class ServerTriggeredActionsAPI extends MethodContextAPI implements ReplaceOptionalWithNullInMethodArguments { - async createTriggeredActions(showStyleBaseId: ShowStyleBaseId | null, base: CreateTriggeredActionsContent | null) { + async createTriggeredActions( + showStyleBaseId: ShowStyleBaseId | null, + base: CreateTriggeredActionsContent | null + ): Promise { return apiCreateTriggeredActions(this, showStyleBaseId, base) } - async removeTriggeredActions(triggeredActionId: TriggeredActionId) { + async removeTriggeredActions(triggeredActionId: TriggeredActionId): Promise { return apiRemoveTriggeredActions(this, triggeredActionId) } } -registerClassToMeteorMethods(TriggeredActionsAPIMethods, ServerTriggeredActionsAPI, false) diff --git a/meteor/server/api/user.ts b/meteor/server/api/user.ts index e626071dd3f..318afe4859b 100644 --- a/meteor/server/api/user.ts +++ b/meteor/server/api/user.ts @@ -1,14 +1,15 @@ import { MethodContextAPI } from './methodContext' -import { NewUserAPI, UserAPIMethods } from '@sofie-automation/meteor-lib/dist/api/user' -import { registerClassToMeteorMethods } from '../methods' +import { NewUserAPI } from '@sofie-automation/meteor-lib/dist/api/user' import { triggerWriteAccessBecauseNoCheckNecessary } from '../security/securityVerify' -import { parseUserPermissions, USER_PERMISSIONS_HEADER } from '@sofie-automation/meteor-lib/dist/userPermissions' +import { + parseUserPermissions, + USER_PERMISSIONS_HEADER, + UserPermissions, +} from '@sofie-automation/meteor-lib/dist/userPermissions' -class ServerUserAPI extends MethodContextAPI implements NewUserAPI { - async getUserPermissions() { +export class ServerUserAPI extends MethodContextAPI implements NewUserAPI { + async getUserPermissions(): Promise { triggerWriteAccessBecauseNoCheckNecessary() return parseUserPermissions(this.connection?.httpHeaders?.[USER_PERMISSIONS_HEADER]) } } - -registerClassToMeteorMethods(UserAPIMethods, ServerUserAPI, false) diff --git a/meteor/server/api/userActions.ts b/meteor/server/api/userActions.ts index 545ddfb74e2..2b85c5de0bc 100644 --- a/meteor/server/api/userActions.ts +++ b/meteor/server/api/userActions.ts @@ -3,11 +3,15 @@ import { Meteor } from 'meteor/meteor' import { ClientAPI } from '@sofie-automation/meteor-lib/dist/api/client' import type { Time } from '@sofie-automation/shared-lib/dist/lib/lib' import { ServerPlayoutAPI } from './playout/playout' -import { NewUserActionAPI, UserActionAPIMethods } from '@sofie-automation/meteor-lib/dist/api/userActions' +import { + NewUserActionAPI, + ReloadRundownPlaylistResponse, + TriggerReloadDataResponse, +} from '@sofie-automation/meteor-lib/dist/api/userActions' import { EvaluationBase } from '@sofie-automation/meteor-lib/dist/collections/Evaluations' import { IngestPart, IngestAdlib, ActionUserData, UserOperationTarget } from '@sofie-automation/blueprints-integration' import { storeRundownPlaylistSnapshot } from './snapshot' -import { registerClassToMeteorMethods, ReplaceOptionalWithNullInMethodArguments } from '../methods' +import { ReplaceOptionalWithNullInMethodArguments } from '../methods' import { ServerRundownAPI } from './rundown' import { saveEvaluation } from './evaluations' import { MOSDeviceActions } from './ingest/mosDevice/actions' @@ -21,7 +25,12 @@ import { AdLibActionCommon } from '@sofie-automation/corelib/dist/dataModel/Adli import { BucketAdLibAction } from '@sofie-automation/corelib/dist/dataModel/BucketAdLibAction' import * as PackageManagerAPI from './packageManager' import { ServerPeripheralDeviceAPI } from './peripheralDevice' -import { StudioJobs } from '@sofie-automation/corelib/dist/worker/studio' +import { + ExecuteActionResult, + QueueNextSegmentResult, + StudioJobs, + TakeNextPartResult, +} from '@sofie-automation/corelib/dist/worker/studio' import { AdLibActionId, BucketAdLibActionId, @@ -38,6 +47,7 @@ import { SegmentId, ShowStyleBaseId, ShowStyleVariantId, + SnapshotId, StudioId, } from '@sofie-automation/corelib/dist/dataModel/Ids' import { NrcsIngestDataCache, Parts, Pieces, Rundowns } from '../collections' @@ -92,7 +102,7 @@ async function pieceSetInOutPoints( ) // MOS data is in seconds } -class ServerUserActionAPI +export class ServerUserActionAPI extends MethodContextAPI implements ReplaceOptionalWithNullInMethodArguments { @@ -101,7 +111,7 @@ class ServerUserActionAPI eventTime: Time, rundownPlaylistId: RundownPlaylistId, fromPartInstanceId: PartInstanceId | null - ) { + ): Promise> { return ServerClientAPI.runUserActionInLogForPlaylistOnWorker( this, userEvent, @@ -125,7 +135,7 @@ class ServerUserActionAPI nextPartOrInstanceId: PartId | PartInstanceId, timeOffset: number | null, isInstance: boolean | null - ) { + ): Promise> { return ServerClientAPI.runUserActionInLogForPlaylistOnWorker( this, userEvent, @@ -152,7 +162,7 @@ class ServerUserActionAPI eventTime: Time, rundownPlaylistId: RundownPlaylistId, nextSegmentId: SegmentId - ) { + ): Promise> { return ServerClientAPI.runUserActionInLogForPlaylistOnWorker( this, userEvent, @@ -174,7 +184,7 @@ class ServerUserActionAPI eventTime: Time, rundownPlaylistId: RundownPlaylistId, queuedSegmentId: SegmentId | null - ) { + ): Promise> { return ServerClientAPI.runUserActionInLogForPlaylistOnWorker( this, userEvent, @@ -198,7 +208,7 @@ class ServerUserActionAPI partDelta: number, segmentDelta: number, ignoreQuickLoop: boolean | null - ) { + ): Promise> { return ServerClientAPI.runUserActionInLogForPlaylistOnWorker( this, userEvent, @@ -218,7 +228,11 @@ class ServerUserActionAPI } ) } - async prepareForBroadcast(userEvent: string, eventTime: Time, rundownPlaylistId: RundownPlaylistId) { + async prepareForBroadcast( + userEvent: string, + eventTime: Time, + rundownPlaylistId: RundownPlaylistId + ): Promise> { return ServerClientAPI.runUserActionInLogForPlaylistOnWorker( this, userEvent, @@ -233,7 +247,11 @@ class ServerUserActionAPI } ) } - async resetRundownPlaylist(userEvent: string, eventTime: Time, rundownPlaylistId: RundownPlaylistId) { + async resetRundownPlaylist( + userEvent: string, + eventTime: Time, + rundownPlaylistId: RundownPlaylistId + ): Promise> { return ServerClientAPI.runUserActionInLogForPlaylistOnWorker( this, userEvent, @@ -253,7 +271,7 @@ class ServerUserActionAPI eventTime: Time, rundownPlaylistId: RundownPlaylistId, rehearsal: boolean | null - ) { + ): Promise> { return ServerClientAPI.runUserActionInLogForPlaylistOnWorker( this, userEvent, @@ -270,7 +288,12 @@ class ServerUserActionAPI } ) } - async activate(userEvent: string, eventTime: Time, rundownPlaylistId: RundownPlaylistId, rehearsal: boolean) { + async activate( + userEvent: string, + eventTime: Time, + rundownPlaylistId: RundownPlaylistId, + rehearsal: boolean + ): Promise> { return ServerClientAPI.runUserActionInLogForPlaylistOnWorker( this, userEvent, @@ -287,7 +310,11 @@ class ServerUserActionAPI } ) } - async deactivate(userEvent: string, eventTime: Time, rundownPlaylistId: RundownPlaylistId) { + async deactivate( + userEvent: string, + eventTime: Time, + rundownPlaylistId: RundownPlaylistId + ): Promise> { return ServerClientAPI.runUserActionInLogForPlaylistOnWorker( this, userEvent, @@ -307,7 +334,7 @@ class ServerUserActionAPI eventTime: Time, rundownPlaylistId: RundownPlaylistId, rehearsal: boolean - ) { + ): Promise> { return ServerClientAPI.runUserActionInLogForPlaylistOnWorker( this, userEvent, @@ -330,7 +357,7 @@ class ServerUserActionAPI eventTime: Time, rundownPlaylistId: RundownPlaylistId, undo: boolean | null - ) { + ): Promise> { return ServerClientAPI.runUserActionInLogForPlaylistOnWorker( this, userEvent, @@ -353,7 +380,7 @@ class ServerUserActionAPI rundownPlaylistId: RundownPlaylistId, partInstanceId: PartInstanceId, pieceInstanceIdOrPieceIdToCopy: PieceInstanceId | PieceId - ) { + ): Promise> { return ServerClientAPI.runUserActionInLogForPlaylistOnWorker( this, userEvent, @@ -380,7 +407,7 @@ class ServerUserActionAPI pieceId: PieceId, inPoint: number, duration: number - ) { + ): Promise> { return ServerClientAPI.runUserActionInLogForPlaylist( this, userEvent, @@ -408,7 +435,7 @@ class ServerUserActionAPI actionId: string, userData: ActionUserData | null, triggerMode: string | null - ) { + ): Promise> { return ServerClientAPI.runUserActionInLogForPlaylistOnWorker( this, userEvent, @@ -438,7 +465,7 @@ class ServerUserActionAPI partInstanceId: PartInstanceId, adlibPieceId: PieceId, queue: boolean - ) { + ): Promise> { return ServerClientAPI.runUserActionInLogForPlaylistOnWorker( this, userEvent, @@ -466,7 +493,7 @@ class ServerUserActionAPI rundownPlaylistId: RundownPlaylistId, partInstanceId: PartInstanceId, sourceLayerIds: string[] - ) { + ): Promise> { return ServerClientAPI.runUserActionInLogForPlaylistOnWorker( this, userEvent, @@ -492,7 +519,7 @@ class ServerUserActionAPI partInstanceId: PartInstanceId, adlibPieceId: PieceId, queue: boolean - ) { + ): Promise> { return ServerClientAPI.runUserActionInLogForPlaylistOnWorker( this, userEvent, @@ -519,7 +546,7 @@ class ServerUserActionAPI eventTime: Time, rundownPlaylistId: RundownPlaylistId, sourceLayerId: string - ) { + ): Promise> { return ServerClientAPI.runUserActionInLogForPlaylistOnWorker( this, userEvent, @@ -542,7 +569,7 @@ class ServerUserActionAPI bucketId: BucketId, showStyleBaseId: ShowStyleBaseId, ingestItem: IngestAdlib - ) { + ): Promise> { return ServerClientAPI.runUserActionInLog( this, userEvent, @@ -566,7 +593,7 @@ class ServerUserActionAPI partInstanceId: PartInstanceId, bucketAdlibId: BucketAdLibId, queue: boolean | null - ) { + ): Promise> { return ServerClientAPI.runUserActionInLogForPlaylistOnWorker( this, userEvent, @@ -588,7 +615,12 @@ class ServerUserActionAPI } ) } - async activateHold(userEvent: string, eventTime: Time, rundownPlaylistId: RundownPlaylistId, undo: boolean | null) { + async activateHold( + userEvent: string, + eventTime: Time, + rundownPlaylistId: RundownPlaylistId, + undo: boolean | null + ): Promise> { if (undo) { return ServerClientAPI.runUserActionInLogForPlaylistOnWorker( this, @@ -619,7 +651,11 @@ class ServerUserActionAPI ) } } - async saveEvaluation(userEvent: string, eventTime: Time, evaluation: EvaluationBase) { + async saveEvaluation( + userEvent: string, + eventTime: Time, + evaluation: EvaluationBase + ): Promise> { return ServerClientAPI.runUserActionInLogForPlaylist( this, userEvent, @@ -642,7 +678,7 @@ class ServerUserActionAPI playlistId: RundownPlaylistId, reason: string, full: boolean - ) { + ): Promise> { if (!verifyHashedToken(hashedToken)) { throw new Meteor.Error(401, `Idempotency token is invalid or has expired`) } @@ -662,7 +698,11 @@ class ServerUserActionAPI } ) } - async removeRundownPlaylist(userEvent: string, eventTime: Time, rundownPlaylistId: RundownPlaylistId) { + async removeRundownPlaylist( + userEvent: string, + eventTime: Time, + rundownPlaylistId: RundownPlaylistId + ): Promise> { return ServerClientAPI.runUserActionInLogForPlaylistOnWorker( this, userEvent, @@ -677,7 +717,11 @@ class ServerUserActionAPI } ) } - async DEBUG_crashStudioWorker(userEvent: string, eventTime: Time, rundownPlaylistId: RundownPlaylistId) { + async DEBUG_crashStudioWorker( + userEvent: string, + eventTime: Time, + rundownPlaylistId: RundownPlaylistId + ): Promise> { // Make sure we never crash in production if (Meteor.isProduction) return ClientAPI.responseSuccess(undefined) @@ -695,7 +739,11 @@ class ServerUserActionAPI } ) } - async resyncRundownPlaylist(userEvent: string, eventTime: Time, playlistId: RundownPlaylistId) { + async resyncRundownPlaylist( + userEvent: string, + eventTime: Time, + playlistId: RundownPlaylistId + ): Promise> { return ServerClientAPI.runUserActionInLogForPlaylist( this, userEvent, @@ -711,7 +759,11 @@ class ServerUserActionAPI } ) } - async unsyncRundown(userEvent: string, eventTime: Time, rundownId: RundownId) { + async unsyncRundown( + userEvent: string, + eventTime: Time, + rundownId: RundownId + ): Promise> { return ServerClientAPI.runUserActionInLogForRundown( this, userEvent, @@ -727,7 +779,11 @@ class ServerUserActionAPI } ) } - async removeRundown(userEvent: string, eventTime: Time, rundownId: RundownId) { + async removeRundown( + userEvent: string, + eventTime: Time, + rundownId: RundownId + ): Promise> { return ServerClientAPI.runUserActionInLogForRundown( this, userEvent, @@ -743,7 +799,11 @@ class ServerUserActionAPI } ) } - async resyncRundown(userEvent: string, eventTime: Time, rundownId: RundownId) { + async resyncRundown( + userEvent: string, + eventTime: Time, + rundownId: RundownId + ): Promise> { return ServerClientAPI.runUserActionInLogForRundown( this, userEvent, @@ -764,7 +824,7 @@ class ServerUserActionAPI eventTime: Time, deviceId: PeripheralDeviceId, workId: string - ) { + ): Promise> { return ServerClientAPI.runUserActionInLog( this, userEvent, @@ -781,7 +841,11 @@ class ServerUserActionAPI } ) } - async packageManagerRestartAllExpectations(userEvent: string, eventTime: Time, studioId: StudioId) { + async packageManagerRestartAllExpectations( + userEvent: string, + eventTime: Time, + studioId: StudioId + ): Promise> { return ServerClientAPI.runUserActionInLog( this, userEvent, @@ -802,7 +866,7 @@ class ServerUserActionAPI eventTime: Time, deviceId: PeripheralDeviceId, workId: string - ) { + ): Promise> { return ServerClientAPI.runUserActionInLog( this, userEvent, @@ -824,7 +888,7 @@ class ServerUserActionAPI eventTime: Time, deviceId: PeripheralDeviceId, containerId: string - ) { + ): Promise> { return ServerClientAPI.runUserActionInLog( this, userEvent, @@ -841,7 +905,11 @@ class ServerUserActionAPI } ) } - async regenerateRundownPlaylist(userEvent: string, eventTime: Time, rundownPlaylistId: RundownPlaylistId) { + async regenerateRundownPlaylist( + userEvent: string, + eventTime: Time, + rundownPlaylistId: RundownPlaylistId + ): Promise> { return ServerClientAPI.runUserActionInLogForPlaylistOnWorker( this, userEvent, @@ -856,7 +924,11 @@ class ServerUserActionAPI } ) } - async restartCore(userEvent: string, eventTime: Time, hashedToken: string) { + async restartCore( + userEvent: string, + eventTime: Time, + hashedToken: string + ): Promise> { return ServerClientAPI.runUserActionInLog( this, userEvent, @@ -881,18 +953,30 @@ class ServerUserActionAPI ) } - async guiFocused(userEvent: string, eventTime: Time, viewInfo: unknown | null) { + async guiFocused( + userEvent: string, + eventTime: Time, + viewInfo: unknown | null + ): Promise> { return ServerClientAPI.runUserActionInLog(this, userEvent, eventTime, 'guiFocused', { viewInfo }, async () => { triggerWriteAccessBecauseNoCheckNecessary() }) } - async guiBlurred(userEvent: string, eventTime: Time, viewInfo: unknown | null) { + async guiBlurred( + userEvent: string, + eventTime: Time, + viewInfo: unknown | null + ): Promise> { return ServerClientAPI.runUserActionInLog(this, userEvent, eventTime, 'guiBlurred', { viewInfo }, async () => { triggerWriteAccessBecauseNoCheckNecessary() }) } - async bucketsRemoveBucket(userEvent: string, eventTime: Time, bucketId: BucketId) { + async bucketsRemoveBucket( + userEvent: string, + eventTime: Time, + bucketId: BucketId + ): Promise> { return ServerClientAPI.runUserActionInLog( this, userEvent, @@ -912,7 +996,7 @@ class ServerUserActionAPI eventTime: Time, bucketId: BucketId, bucketProps: Partial> - ) { + ): Promise> { return ServerClientAPI.runUserActionInLog( this, userEvent, @@ -928,7 +1012,11 @@ class ServerUserActionAPI } ) } - async bucketsEmptyBucket(userEvent: string, eventTime: Time, bucketId: BucketId) { + async bucketsEmptyBucket( + userEvent: string, + eventTime: Time, + bucketId: BucketId + ): Promise> { return ServerClientAPI.runUserActionInLog( this, userEvent, @@ -943,7 +1031,12 @@ class ServerUserActionAPI } ) } - async bucketsCreateNewBucket(userEvent: string, eventTime: Time, studioId: StudioId, name: string) { + async bucketsCreateNewBucket( + userEvent: string, + eventTime: Time, + studioId: StudioId, + name: string + ): Promise> { return ServerClientAPI.runUserActionInLog( this, userEvent, @@ -959,7 +1052,11 @@ class ServerUserActionAPI } ) } - async bucketsRemoveBucketAdLib(userEvent: string, eventTime: Time, adlibId: BucketAdLibId) { + async bucketsRemoveBucketAdLib( + userEvent: string, + eventTime: Time, + adlibId: BucketAdLibId + ): Promise> { check(adlibId, String) return ServerClientAPI.runUserActionInLog( @@ -974,7 +1071,11 @@ class ServerUserActionAPI } ) } - async bucketsRemoveBucketAdLibAction(userEvent: string, eventTime: Time, actionId: BucketAdLibActionId) { + async bucketsRemoveBucketAdLibAction( + userEvent: string, + eventTime: Time, + actionId: BucketAdLibActionId + ): Promise> { return ServerClientAPI.runUserActionInLog( this, userEvent, @@ -994,7 +1095,7 @@ class ServerUserActionAPI eventTime: Time, adlibId: BucketAdLibId, adlibProps: Partial> - ) { + ): Promise> { return ServerClientAPI.runUserActionInLog( this, userEvent, @@ -1015,7 +1116,7 @@ class ServerUserActionAPI eventTime: Time, actionId: BucketAdLibActionId, actionProps: Partial> - ) { + ): Promise> { return ServerClientAPI.runUserActionInLog( this, userEvent, @@ -1269,7 +1370,7 @@ class ServerUserActionAPI eventTime: number, studioId: StudioId, showStyleVariantId: ShowStyleVariantId - ) { + ): Promise> { const jobName = IngestJobs.CreateAdlibTestingRundownForShowStyleVariant return ServerClientAPI.runUserActionInLog( this, @@ -1290,4 +1391,3 @@ class ServerUserActionAPI ) } } -registerClassToMeteorMethods(UserActionAPIMethods, ServerUserActionAPI, false) diff --git a/meteor/server/lib/customPublication/index.ts b/meteor/server/lib/customPublication/index.ts index 836e0154544..774642bb682 100644 --- a/meteor/server/lib/customPublication/index.ts +++ b/meteor/server/lib/customPublication/index.ts @@ -2,4 +2,4 @@ export { CustomPublishCollection } from './customPublishCollection' export { setUpOptimizedObserverArray } from './optimizedObserverArray' export { TriggerUpdate, SetupObserversResult } from './optimizedObserverBase' export { setUpCollectionOptimizedObserver } from './optimizedObserverCollection' -export { meteorCustomPublish, CustomPublish, CustomPublishChanges } from './publish' +export { CustomPublish, CustomPublishChanges } from './publish' diff --git a/meteor/server/lib/customPublication/publish.ts b/meteor/server/lib/customPublication/publish.ts index 57599be93fc..53fbce4561d 100644 --- a/meteor/server/lib/customPublication/publish.ts +++ b/meteor/server/lib/customPublication/publish.ts @@ -1,7 +1,6 @@ import { Meteor } from 'meteor/meteor' -import { AllPubSubTypes } from '@sofie-automation/meteor-lib/dist/api/pubsub' import { ProtectedString, unprotectString } from '@sofie-automation/corelib/dist/protectedString' -import { PublishDocType, SubscriptionContext, meteorPublishUnsafe } from '../../publications/lib/lib' +import { PublicationContext } from '../../publications/lib/lib' export interface CustomPublishChanges }> { added: Array @@ -33,7 +32,7 @@ export class CustomPublishMeteor }> { #isReady = false constructor( - private _meteorSubscription: SubscriptionContext, + private _meteorSubscription: PublicationContext, private _collectionName: string ) { this._meteorSubscription.onStop(() => { @@ -86,19 +85,4 @@ export class CustomPublishMeteor }> { } } -type PublishIfDocument = Doc extends { _id: ProtectedString } ? CustomPublish : never - -/** Wrapping of Meteor.publish to provide types for for custom publications */ -export function meteorCustomPublish>( - publicationName: K, - customCollectionName: N, - cb: ( - this: SubscriptionContext, - publication: PublishIfDocument>, - ...args: Parameters - ) => Promise -): void { - meteorPublishUnsafe(publicationName, async function (this: SubscriptionContext, ...args: any[]) { - return cb.call(this, new CustomPublishMeteor(this, String(customCollectionName)) as any, ...(args as any)) - }) -} +export type PublishIfDocument = Doc extends { _id: ProtectedString } ? CustomPublish : never diff --git a/meteor/server/main.ts b/meteor/server/main.ts index 6da6c4fe266..11e859a5b6e 100644 --- a/meteor/server/main.ts +++ b/meteor/server/main.ts @@ -42,7 +42,6 @@ import './api/serviceMessages/api' import './webmanifest' // import all files that calls Meteor.startup: -import './api/rest/api' import './Connections' import './coreSystem' import './cronjobs' @@ -52,6 +51,28 @@ import './logo' import './systemTime' // import './performanceMonitor' // called above -// Setup publications and security: -import './publications/_publications' -import './security/securityVerify' +import { MethodRegistry } from './methodRegistry' +import { registerAllApiMethods } from './methodRegistrations' +import { PublicationRegistry } from './publicationRegistry' +import { registerAllPublications } from './publicationRegistrations' +import { bindRestApiRouter } from './api/rest/api' +import { startupVerifyAllMethods } from './security/securityVerify' + +// Build and populate the method registry +const methodRegistry = new MethodRegistry() +registerAllApiMethods(methodRegistry) + +// Build and populate the publication registry +const publicationRegistry = new PublicationRegistry() +registerAllPublications(publicationRegistry) + +// Apply methods and publications +methodRegistry.applyToMeteor() +publicationRegistry.applyToMeteor() +Meteor.startup(() => { + bindRestApiRouter(methodRegistry, publicationRegistry) + startupVerifyAllMethods(methodRegistry) + + // Ensure all the publications were registered at startup + if (Meteor.isDevelopment) publicationRegistry.verifyAllPublicationsRegistered() +}) diff --git a/meteor/server/methodRegistrations.ts b/meteor/server/methodRegistrations.ts new file mode 100644 index 00000000000..69f2575a3d6 --- /dev/null +++ b/meteor/server/methodRegistrations.ts @@ -0,0 +1,79 @@ +import { IMeteorCall } from '@sofie-automation/meteor-lib/dist/api/methods' +import { BlueprintAPIMethods } from '@sofie-automation/meteor-lib/dist/api/blueprint' +import { ClientAPIMethods } from '@sofie-automation/meteor-lib/dist/api/client' +import { ExternalMessageQueueAPIMethods } from '@sofie-automation/meteor-lib/dist/api/ExternalMessageQueue' +import { MigrationAPIMethods } from '@sofie-automation/meteor-lib/dist/api/migration' +import { PlayoutAPIMethods } from '@sofie-automation/meteor-lib/dist/api/playout' +import { RundownAPIMethods } from '@sofie-automation/meteor-lib/dist/api/rundown' +import { RundownLayoutsAPIMethods } from '@sofie-automation/meteor-lib/dist/api/rundownLayouts' +import { SnapshotAPIMethods } from '@sofie-automation/meteor-lib/dist/api/shapshot' +import { ShowStylesAPIMethods } from '@sofie-automation/meteor-lib/dist/api/showStyles' +import { TriggeredActionsAPIMethods } from '@sofie-automation/meteor-lib/dist/api/triggeredActions' +import { StudiosAPIMethods } from '@sofie-automation/meteor-lib/dist/api/studios' +import { SystemStatusAPIMethods } from '@sofie-automation/meteor-lib/dist/api/systemStatus' +import { UserAPIMethods } from '@sofie-automation/meteor-lib/dist/api/user' +import { UserActionAPIMethods } from '@sofie-automation/meteor-lib/dist/api/userActions' +import { SystemAPIMethods } from '@sofie-automation/meteor-lib/dist/api/system' +import { MongoAPIMethods } from '@sofie-automation/meteor-lib/dist/api/mongo' +import { PeripheralDeviceAPIMethods } from '@sofie-automation/shared-lib/dist/peripheralDevice/methodsAPI' + +import { ServerBlueprintAPI } from './api/blueprints/api' +import { ServerClientAPIClass } from './api/client' +import { ServerExternalMessageQueueAPI } from './api/ExternalMessageQueue' +import { ServerMigrationAPI } from './migration/api' +import { ServerPeripheralDeviceAPIClass } from './api/peripheralDevice' +import { ServerPlayoutAPIClass } from './api/playout/api' +import { ServerRundownAPIClass } from './api/rundown' +import { ServerRundownLayoutsAPI } from './api/rundownLayouts' +import { ServerSnapshotAPI } from './api/snapshot' +import { ServerShowStylesAPI } from './api/showStyles' +import { ServerTriggeredActionsAPI } from './api/triggeredActions' +import { ServerStudiosAPI } from './api/studio/api' +import { ServerSystemStatusAPI } from './systemStatus/api' +import { ServerUserAPI } from './api/user' +import { ServerUserActionAPI } from './api/userActions' +import { SystemAPIClass } from './api/system' +import { MongoAPIClass } from './api/mongo' + +import { playoutDebugMethods } from './api/playout/debug' +import { ingestDebugMethods } from './api/ingest/debug' + +import { AnyMethodApiRegistration, MethodApiRegistration, MethodRegistry } from './methodRegistry' + +/** + * The complete set of API method registrations. + * + * The `satisfies` clause is the safety net: it is keyed off `IMeteorCall` — the exact interface the + * client method wrappers are built from, ensuring everything is defined correctly. + */ +export const METHOD_REGISTRATIONS = { + blueprint: { methods: BlueprintAPIMethods, class: ServerBlueprintAPI }, + client: { methods: ClientAPIMethods, class: ServerClientAPIClass }, + externalMessages: { methods: ExternalMessageQueueAPIMethods, class: ServerExternalMessageQueueAPI }, + migration: { methods: MigrationAPIMethods, class: ServerMigrationAPI }, + peripheralDevice: { methods: PeripheralDeviceAPIMethods, class: ServerPeripheralDeviceAPIClass }, + playout: { methods: PlayoutAPIMethods, class: ServerPlayoutAPIClass }, + rundown: { methods: RundownAPIMethods, class: ServerRundownAPIClass }, + rundownLayout: { methods: RundownLayoutsAPIMethods, class: ServerRundownLayoutsAPI }, + snapshot: { methods: SnapshotAPIMethods, class: ServerSnapshotAPI }, + showstyles: { methods: ShowStylesAPIMethods, class: ServerShowStylesAPI }, + triggeredActions: { methods: TriggeredActionsAPIMethods, class: ServerTriggeredActionsAPI }, + studio: { methods: StudiosAPIMethods, class: ServerStudiosAPI }, + systemStatus: { methods: SystemStatusAPIMethods, class: ServerSystemStatusAPI }, + user: { methods: UserAPIMethods, class: ServerUserAPI }, + userAction: { methods: UserActionAPIMethods, class: ServerUserActionAPI }, + system: { methods: SystemAPIMethods, class: SystemAPIClass }, + mongo: { methods: MongoAPIMethods, class: MongoAPIClass, secret: true }, +} satisfies { [K in keyof IMeteorCall]: MethodApiRegistration } + +/** Register every API method on the given registry. */ +export function registerAllApiMethods(registry: MethodRegistry): void { + for (const registration of Object.values(METHOD_REGISTRATIONS)) { + registry.registerApi(registration) + } + + // Developer-only debug methods. These are not part of `IMeteorCall`, but they must still live on + // the registry to be usable + registry.registerDebugMethods(playoutDebugMethods) + registry.registerDebugMethods(ingestDebugMethods) +} diff --git a/meteor/server/methodRegistry.ts b/meteor/server/methodRegistry.ts new file mode 100644 index 00000000000..5b19784612c --- /dev/null +++ b/meteor/server/methodRegistry.ts @@ -0,0 +1,195 @@ +import { Meteor } from 'meteor/meteor' +import { MethodContext, MethodContextAPI } from './api/methodContext' +import { extractFunctionSignature } from './lib' +import { logger } from './logging' +import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' +import { + MeteorMethod, + MeteorDebugMethod, + ReplaceOptionalWithNullInMethodArguments, + wrapMethodForExecution, +} from './methods' +import { assertConnectionHasOneOfPermissions } from './security/auth' + +export type MethodWrapper = ( + methodContext: MethodContext, + methodName: string, + args: any[], + fcn: (...args: any[]) => any +) => any + +/** + * A registration of one API class against its method-name map. + * + * The generic `TApi` is the interface the client wrappers are built from (a member of `IMeteorCall`), + * which ties three things together at compile-time when used via the `satisfies` check on the + * registration map: + * - `methods` must have an entry for every method name of `TApi` + * - `class` instances must implement `TApi` — either directly, or via the DDP null-arg replacement + * (`ReplaceOptionalWithNullInMethodArguments`) that some of the classes apply. The union accepts + * both conventions without forcing every class to adopt the same one. + */ +export interface MethodApiRegistration { + methods: Record + class: new () => MethodContextAPI & (TApi | ReplaceOptionalWithNullInMethodArguments) + secret?: boolean + wrapper?: MethodWrapper +} + +/** The type-erased shape consumed at runtime (the per-`TApi` guarantee lives in the map's `satisfies`). */ +export interface AnyMethodApiRegistration { + methods: Record + class: typeof MethodContextAPI + secret?: boolean + wrapper?: MethodWrapper +} + +interface RegisteredMethod { + /** The method wrapped for execution (running-method tracking, unblock, error logging). */ + wrapped: MeteorMethod + /** The original, unwrapped function — used for signature extraction. */ + original: (...args: any[]) => any + secret: boolean + /** Developer-only debug method, gated behind the 'developer' permission. */ + debug: boolean +} + +/** + * Holds all registered methods on an instance instead of mutating global state at import time. + * The same instance is handed to both the Meteor path (`applyToMeteor()`) and, later, the + * standalone DDP server, so methods live on both transports off a single source of truth. + */ +export class MethodRegistry { + private readonly methods = new Map() + private applied = false + + /** Register all methods of an API class, mapping each class method to its wire name via `methods`. */ + registerApi(registration: AnyMethodApiRegistration): void { + const { methods: methodEnum, class: orgClass, secret = false, wrapper } = registration + + for (const classMethodName of getAllClassMethods(orgClass)) { + try { + const wireName = methodEnum[classMethodName] + if (!wireName) { + throw new Meteor.Error( + 500, + `MethodRegistry.registerApi: The method "${classMethodName}" is not set in the methods map.` + ) + } + + const original = (orgClass.prototype as any)[classMethodName] as MeteorMethod + const inner: MeteorMethod = wrapper + ? function (this: MethodContext, ...args: any[]) { + return wrapper(this, wireName, args, original) + } + : original + + this.add(wireName, inner, original, secret) + } catch (e) { + // A missing wire name or duplicate registration is a programming error. Log it and rethrow + // to abort registration of the whole API class, so applyToMeteor() can never run against a + // partially-registered registry. + logger.error(`MethodRegistry: failed to register method "${classMethodName}": ${stringifyError(e)}`) + throw e + } + } + } + + /** Register a single ad-hoc method (e.g. methods not backed by an API class). */ + registerMethod(name: string, fn: MeteorMethod, opts?: { secret?: boolean }): void { + this.add(name, fn, fn, opts?.secret ?? false) + } + + /** + * Register a map of developer-only debug methods. Each is gated behind the 'developer' permission + * (matching the historical `MeteorDebugMethods` behaviour), but registered on this registry so it + * is served by every transport the registry is applied to — not only Meteor's DDP server. + */ + registerDebugMethods(methods: { [key: string]: MeteorDebugMethod }): void { + for (const [name, fn] of Object.entries(methods)) { + if (!name || !fn) continue + const guarded: MeteorMethod = function (this: MethodContext, ...args: any[]) { + assertConnectionHasOneOfPermissions(this.connection, 'developer') + return (fn as (...a: any[]) => any).apply(this, args) + } + this.add(name, guarded, fn, false, true) + } + } + + private add( + name: string, + wrappedInner: MeteorMethod, + original: (...args: any[]) => any, + secret: boolean, + debug = false + ): void { + // Once applyToMeteor() has snapshotted the registry into Meteor.methods(), later additions would be + // invisible to Meteor. Freeze all registration entry points (they all funnel through here) so this + // programming error fails loudly instead of silently dropping the method. + if (this.applied) { + throw new Meteor.Error( + 500, + `MethodRegistry: Cannot register method "${name}" after applyToMeteor() has been called.` + ) + } + if (this.methods.has(name)) { + throw new Meteor.Error(500, `MethodRegistry: A method called "${name}" is already registered.`) + } + this.methods.set(name, { + wrapped: wrapMethodForExecution(name, wrappedInner), + original, + secret, + debug, + }) + } + + /** Look up a wrapped method by wire name (for the standalone DDP server's method dispatch). */ + get(name: string): MeteorMethod | undefined { + return this.methods.get(name)?.wrapped + } + + getAllMethodNames(): string[] { + return Array.from(this.methods.keys()) + } + + /** Whether the given wire name is a developer-only debug method (gated behind the 'developer' permission). */ + isDebugMethod(name: string): boolean { + return this.methods.get(name)?.debug ?? false + } + + /** + * Param-name signatures of all non-secret methods, keyed by wire name. + * Used by the legacy REST API to build its routes. + */ + getSignatures(): { [methodName: string]: string[] } { + const signatures: { [methodName: string]: string[] } = {} + for (const [name, method] of this.methods) { + if (method.secret) continue + const signature = extractFunctionSignature(method.original) + if (signature) signatures[name] = signature + } + return signatures + } + + /** Apply all registered methods to Meteor's DDP server. The only place that touches `Meteor.methods()`. */ + applyToMeteor(): void { + if (this.applied) throw new Meteor.Error(500, 'MethodRegistry.applyToMeteor() has already been called') + this.applied = true + + const meteorMethods: { [name: string]: MeteorMethod } = {} + for (const [name, method] of this.methods) { + meteorMethods[name] = method.wrapped + } + Meteor.methods(meteorMethods) + } +} + +/** Reflect the (own, non-Object) method names of a class. */ +function getAllClassMethods(myClass: any): string[] { + const objectProtProps = Object.getOwnPropertyNames(Object.prototype) + const classProps = Object.getOwnPropertyNames(myClass.prototype) + + return classProps + .filter((name) => objectProtProps.indexOf(name) < 0) + .filter((name) => typeof myClass.prototype[name] === 'function') +} diff --git a/meteor/server/methods.ts b/meteor/server/methods.ts index 921c6bb544d..4999d5fa584 100644 --- a/meteor/server/methods.ts +++ b/meteor/server/methods.ts @@ -1,24 +1,10 @@ import { Meteor } from 'meteor/meteor' -import _ from 'underscore' import { logger } from './logging' -import { extractFunctionSignature } from './lib' -import { MethodContext, MethodContextAPI } from './api/methodContext' +import { MethodContext } from './api/methodContext' import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' import { isPromise } from '@sofie-automation/shared-lib/dist/lib/lib' -import { assertConnectionHasOneOfPermissions } from './security/auth' -type MeteorMethod = (this: MethodContext, ...args: any[]) => any - -interface Methods { - [method: string]: MeteorMethod -} -export interface MethodsInner { - [method: string]: { wrapped: MeteorMethod; original: MeteorMethod } -} -/** All (non-secret) methods */ -export const MeteorMethodSignatures: { [key: string]: string[] } = {} -/** All methods */ -export const AllMeteorMethods: string[] = [] +export type MeteorMethod = (this: MethodContext, ...args: any[]) => any export interface RunningMethods { [methodId: string]: { @@ -30,15 +16,6 @@ export interface RunningMethods { let runningMethods: RunningMethods = {} let runningMethodsId = 0 -function getAllClassMethods(myClass: any): string[] { - const objectProtProps = Object.getOwnPropertyNames(Object.prototype) - const classProps = Object.getOwnPropertyNames(myClass.prototype) - - return classProps - .filter((name) => objectProtProps.indexOf(name) < 0) - .filter((name) => typeof myClass.prototype[name] === 'function') -} - /** This expects an array of values (likely the output of Parameters), and makes anything optional be nullable instead */ export type ReplaceOptionalWithNullInArray = { [K in keyof T]-?: undefined extends T[K] ? NonNullable | null : T[K] @@ -54,115 +31,56 @@ export type ReplaceOptionalWithNullInMethodArguments = { : T[K] } -export function registerClassToMeteorMethods( - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types - methodEnum: any, - orgClass: typeof MethodContextAPI, - secret?: boolean, - // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type - wrapper?: (methodContext: MethodContext, methodName: string, args: any[], fcn: Function) => any -): void { - const methods: MethodsInner = {} - _.each(getAllClassMethods(orgClass), (classMethodName) => { - const enumValue = methodEnum[classMethodName] - if (!enumValue) - throw new Meteor.Error( - 500, - `registerClassToMeteorMethods: The method "${classMethodName}" is not set in the enum containing methods.` - ) - if (wrapper) { - methods[enumValue] = { - wrapped: function (...args: any[]) { - return wrapper(this, enumValue, args, (orgClass.prototype as any)[classMethodName]) - }, - original: (orgClass.prototype as any)[classMethodName], - } - } else { - methods[enumValue] = { - wrapped: (orgClass.prototype as any)[classMethodName], - original: (orgClass.prototype as any)[classMethodName], - } - } - }) - setMeteorMethods(methods, secret) -} /** - * Wrapper for Meteor.methods(), keeps track of which methods are currently running - * @param orgMethods The methods to add - * @param secret Set to true to not expose methods to API + * Wrap a method so that it is tracked while running, unblocks the session when it returns a promise + * (preserving Meteor 2.7 behaviour, to avoid breaking Sofie), and logs errors. + * This is transport-agnostic: it applies equally to methods served over Meteor's DDP server and + * over the standalone DDP server. */ -function setMeteorMethods(orgMethods: MethodsInner, secret?: boolean): void { - // Wrap methods - const methods: Methods = {} - _.each(orgMethods, (m, methodName: string) => { - const method = m.wrapped - if (method) { - methods[methodName] = function (...args: any[]) { - const i = runningMethodsId++ - const methodId = 'm' + i - - runningMethods[methodId] = { - method: methodName, - startTime: Date.now(), - i: i, - } - try { - const result = method.apply(this, args) +export function wrapMethodForExecution(methodName: string, method: MeteorMethod): MeteorMethod { + return function (this: MethodContext, ...args: any[]) { + const i = runningMethodsId++ + const methodId = 'm' + i + + runningMethods[methodId] = { + method: methodName, + startTime: Date.now(), + i: i, + } + try { + const result = method.apply(this, args) - if (isPromise(result)) { - // Don't block execution of other methods while waiting for this to resolve. (This is how meteor 2.7 behaved, added to avoid breaking Sofie) - this.unblock() + if (isPromise(result)) { + // Don't block execution of other methods while waiting for this to resolve. (This is how meteor 2.7 behaved, added to avoid breaking Sofie) + this.unblock() - // The method result is a promise - return Promise.resolve(result) - .finally(() => { - delete runningMethods[methodId] - }) - .catch(async (err) => { - if (!_suppressExtraErrorLogging) { - logger.error(stringifyError(err)) - } - // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors - return Promise.reject(err) - }) - } else { + // The method result is a promise + return Promise.resolve(result) + .finally(() => { delete runningMethods[methodId] - return result - } - } catch (err) { - if (!_suppressExtraErrorLogging) { - logger.error(stringifyError(err)) - } - delete runningMethods[methodId] - throw err - } + }) + .catch(async (err) => { + if (!_suppressExtraErrorLogging) { + logger.error(stringifyError(err)) + } + // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors + return Promise.reject(err) + }) + } else { + delete runningMethods[methodId] + return result } - if (!secret) { - const signature = extractFunctionSignature(m.original) - if (signature) MeteorMethodSignatures[methodName] = signature + } catch (err) { + if (!_suppressExtraErrorLogging) { + logger.error(stringifyError(err)) } - AllMeteorMethods.push(methodName) + delete runningMethods[methodId] + throw err } - }) - Meteor.methods(methods) + } } export type MeteorDebugMethod = (this: Meteor.MethodThisType, ...args: any[]) => Promise | any -export function MeteorDebugMethods(methods: { [key: string]: MeteorDebugMethod }): void { - const fiberMethods: { [key: string]: (this: Meteor.MethodThisType, ...args: any[]) => any } = {} - - for (const [key, fn] of Object.entries(methods)) { - if (key && !!fn) { - fiberMethods[key] = function (this: Meteor.MethodThisType, ...args: any[]) { - assertConnectionHasOneOfPermissions(this.connection, 'developer') - - return fn.call(this, ...args) - } - } - } - - Meteor.methods(fiberMethods) -} export function getRunningMethods(): RunningMethods { return runningMethods diff --git a/meteor/server/migration/__tests__/migrations.test.ts b/meteor/server/migration/__tests__/migrations.test.ts index 2d4319f44b6..f06b4af4569 100644 --- a/meteor/server/migration/__tests__/migrations.test.ts +++ b/meteor/server/migration/__tests__/migrations.test.ts @@ -12,10 +12,9 @@ import { wrapDefaultObject } from '@sofie-automation/corelib/dist/settings/objec import { ShowStyleBases, ShowStyleVariants, Studios } from '../../collections' import { getCoreSystemAsync } from '../../coreSystem/collection' import fs from 'fs' +import { registerAllMethodsForTest } from '../../../__mocks__/helpers/methods' -require('../../api/peripheralDevice.ts') // include in order to create the Meteor methods needed -require('../api') // include in order to create the Meteor methods needed -require('../../api/blueprints/api.ts') // include in order to create the Meteor methods needed +registerAllMethodsForTest() require('../migrations') // include in order to create the migration steps diff --git a/meteor/server/migration/api.ts b/meteor/server/migration/api.ts index c39fc44e85d..7fd25ede49b 100644 --- a/meteor/server/migration/api.ts +++ b/meteor/server/migration/api.ts @@ -1,10 +1,10 @@ import { check, Match } from '../lib/check' -import { registerClassToMeteorMethods } from '../methods' import { MigrationChunk, NewMigrationAPI, - MigrationAPIMethods, BlueprintFixUpConfigMessage, + GetMigrationStatusResult, + RunMigrationResult, } from '@sofie-automation/meteor-lib/dist/api/migration' import * as Migrations from './databaseMigration' import { MethodContextAPI } from '../api/methodContext' @@ -26,14 +26,18 @@ import { UserPermissions } from '@sofie-automation/meteor-lib/dist/userPermissio const PERMISSIONS_FOR_MIGRATIONS: Array = ['configure'] -class ServerMigrationAPI extends MethodContextAPI implements NewMigrationAPI { - async getMigrationStatus() { +export class ServerMigrationAPI extends MethodContextAPI implements NewMigrationAPI { + async getMigrationStatus(): Promise { assertConnectionHasOneOfPermissions(this.connection, ...PERMISSIONS_FOR_MIGRATIONS) return Migrations.getMigrationStatus() } - async runMigration(chunks: Array, hash: string, isFirstOfPartialMigrations?: boolean | null) { + async runMigration( + chunks: Array, + hash: string, + isFirstOfPartialMigrations?: boolean | null + ): Promise { check(chunks, Array) check(hash, String) check(isFirstOfPartialMigrations, Match.Maybe(Boolean)) @@ -43,7 +47,7 @@ class ServerMigrationAPI extends MethodContextAPI implements NewMigrationAPI { return Migrations.runMigration(chunks, hash, isFirstOfPartialMigrations || false) } - async forceMigration(chunks: Array) { + async forceMigration(chunks: Array): Promise { check(chunks, Array) assertConnectionHasOneOfPermissions(this.connection, ...PERMISSIONS_FOR_MIGRATIONS) @@ -51,7 +55,7 @@ class ServerMigrationAPI extends MethodContextAPI implements NewMigrationAPI { return Migrations.forceMigration(chunks) } - async resetDatabaseVersions() { + async resetDatabaseVersions(): Promise { assertConnectionHasOneOfPermissions(this.connection, ...PERMISSIONS_FOR_MIGRATIONS) return Migrations.resetDatabaseVersions() @@ -131,4 +135,3 @@ class ServerMigrationAPI extends MethodContextAPI implements NewMigrationAPI { return runUpgradeForCoreSystem(coreSystemId) } } -registerClassToMeteorMethods(MigrationAPIMethods, ServerMigrationAPI, false) diff --git a/meteor/server/publicationRegistrations.ts b/meteor/server/publicationRegistrations.ts new file mode 100644 index 00000000000..5bbe4939549 --- /dev/null +++ b/meteor/server/publicationRegistrations.ts @@ -0,0 +1,60 @@ +import { PublicationRegistry } from './publicationRegistry' + +import { registerBucketsPublications } from './publications/buckets' +import { registerBlueprintUpgradeStatusPublications } from './publications/blueprintUpgradeStatus/publication' +import { registerIngestStatusPublications } from './publications/ingestStatus/publication' +import { registerExternalEventSubscriptionsPublications } from './publications/externalEventSubscriptions' +import { registerExpectedPackagesPublications } from './publications/packageManager/expectedPackages/publication' +import { registerPackageContainersPublications } from './publications/packageManager/packageContainers' +import { registerPlayoutContextPublications } from './publications/packageManager/playoutContext' +import { registerBucketContentStatusUIPublications } from './publications/pieceContentStatusUI/bucket/publication' +import { registerRundownContentStatusUIPublications } from './publications/pieceContentStatusUI/rundown/publication' +import { registerOrganizationPublications } from './publications/organization' +import { registerPartsUIPublications } from './publications/partsUI/publication' +import { registerPartInstancesUIPublications } from './publications/partInstancesUI/publication' +import { registerPeripheralDevicePublications } from './publications/peripheralDevice' +import { registerPeripheralDeviceForDevicePublications } from './publications/peripheralDeviceForDevice' +import { registerRundownPublications } from './publications/rundown' +import { registerRundownPlaylistPublications } from './publications/rundownPlaylist' +import { registerSegmentPartNotesUIPublications } from './publications/segmentPartNotesUI/publication' +import { registerShowStylePublications } from './publications/showStyle' +import { registerShowStyleUIPublications } from './publications/showStyleUI' +import { registerStudioPublications } from './publications/studio' +import { registerStudioUIPublications } from './publications/studioUI' +import { registerSystemPublications } from './publications/system' +import { registerTimelinePublications } from './publications/timeline' +import { registerTranslationsBundlesPublications } from './publications/translationsBundles' +import { registerTriggeredActionsUIPublications } from './publications/triggeredActionsUI' +import { registerMountedTriggersPublications } from './publications/mountedTriggers' +import { registerDeviceTriggersPreviewPublications } from './publications/deviceTriggersPreview' + +/** Register every DDP publication on the given registry. */ +export function registerAllPublications(registry: PublicationRegistry): void { + registerBucketsPublications(registry) + registerBlueprintUpgradeStatusPublications(registry) + registerIngestStatusPublications(registry) + registerExternalEventSubscriptionsPublications(registry) + registerExpectedPackagesPublications(registry) + registerPackageContainersPublications(registry) + registerPlayoutContextPublications(registry) + registerBucketContentStatusUIPublications(registry) + registerRundownContentStatusUIPublications(registry) + registerOrganizationPublications(registry) + registerPartsUIPublications(registry) + registerPartInstancesUIPublications(registry) + registerPeripheralDevicePublications(registry) + registerPeripheralDeviceForDevicePublications(registry) + registerRundownPublications(registry) + registerRundownPlaylistPublications(registry) + registerSegmentPartNotesUIPublications(registry) + registerShowStylePublications(registry) + registerShowStyleUIPublications(registry) + registerStudioPublications(registry) + registerStudioUIPublications(registry) + registerSystemPublications(registry) + registerTimelinePublications(registry) + registerTranslationsBundlesPublications(registry) + registerTriggeredActionsUIPublications(registry) + registerMountedTriggersPublications(registry) + registerDeviceTriggersPreviewPublications(registry) +} diff --git a/meteor/server/publicationRegistry.ts b/meteor/server/publicationRegistry.ts new file mode 100644 index 00000000000..29b148aa332 --- /dev/null +++ b/meteor/server/publicationRegistry.ts @@ -0,0 +1,215 @@ +import { Meteor, Subscription } from 'meteor/meteor' +import { AllPubSubNames, AllPubSubTypes } from '@sofie-automation/meteor-lib/dist/api/pubsub' +import { MetricsGauge } from '@sofie-automation/corelib/dist/prometheus' +import { extractFunctionSignature } from './lib' +import { logger } from './logging' +import { MinimalMongoCursor } from './collections/implementations/asyncCollection' +import { PublicationContext, PublishDocType } from './publications/lib/lib' +import { CustomPublishMeteor, PublishIfDocument } from './lib/customPublication/publish' + +// The Prometheus gauge is registered globally by name, so it must live at module scope rather than on +// the registry instance, otherwise constructing a second registry (e.g. in tests) would throw. +const MeteorPublicationsGauge = new MetricsGauge({ + name: `sofie_meteor_publication_subscribers_total`, + help: 'Number of subscribers on a publication (ignoring arguments)', + labelNames: ['publication', 'server'], +}) + +/** + * A publication callback, as stored on the registry. The context is passed as the first argument (not + * via `this`), keeping the callbacks free of Meteor semantics so the same registry can be served by a + * non-Meteor transport. The return value is transport-agnostic (a cursor, `null`, or `void`). + */ +type PublicationCallback = (context: PublicationContext, ...args: any[]) => Promise + +interface RegisteredPublication { + /** The publication callback, invoked with a transport-supplied `PublicationContext`. */ + callback: PublicationCallback + /** Param-name signature of the callback, if it could be extracted. Used by the legacy REST API. */ + signature: string[] | undefined + /** + * Whether this is a custom publication (registered via `customPublish`). Custom publications push + * documents through the `PublicationContext` and rely on `onStop` to tear down their observers, so + * they cannot be served by the fire-once legacy REST path (which has no working `onStop`). + */ + isCustom: boolean +} + +/** + * Drop the first `count` parameter names from an extracted signature. Registered callbacks carry + * registry-internal leading parameters (`context`, and for custom publications also `publication`) that + * are not part of the public API and must not leak into REST routes or introspection. + */ +function dropLeadingParams(signature: string[] | undefined, count: number): string[] | undefined { + if (!signature) return undefined + return signature.slice(count) +} + +/** The Meteor implementation of `PublicationContext`, adapting a Meteor `Subscription`. */ +class MeteorPublicationContext implements PublicationContext { + constructor(private readonly subscription: Subscription) {} + + get connection(): Meteor.Connection | null { + return this.subscription.connection + } + onStop(callback: () => void): void { + this.subscription.onStop(callback) + } + ready(): void { + this.subscription.ready() + } + added(collection: string, id: string, fields: Record): void { + this.subscription.added(collection, id, fields) + } + changed(collection: string, id: string, fields: Record): void { + this.subscription.changed(collection, id, fields) + } + removed(collection: string, id: string): void { + this.subscription.removed(collection, id) + } +} + +/** + * Holds all registered publications on an instance instead of mutating global state at import time. + * The same instance is handed to the Meteor path (`applyToMeteor()`) and, later, any standalone DDP + * server, so publications live on every transport off a single source of truth. + * + * Note: unlike the method registry there is no compile-time `satisfies` completeness check, because + * publication registrations are imperative calls scattered across many files rather than a single + * keyed literal. Completeness is instead asserted at runtime via `verifyAllPublicationsRegistered()` + * (and the publicationRegistry drift-guard test). + */ +export class PublicationRegistry { + private readonly publications = new Map() + private applied = false + + /** + * Unsafe registration of a publication. + * Prefer the typed `publish`/`customPublish` wrappers below. + */ + publishUnsafe(name: string, callback: PublicationCallback, signature?: string[], isCustom = false): void { + if (this.applied) { + throw new Meteor.Error( + 500, + `PublicationRegistry: Cannot register publication "${name}" after the registry has been applied.` + ) + } + if (this.publications.has(name)) { + throw new Meteor.Error(500, `PublicationRegistry: A publication called "${name}" is already registered.`) + } + + // The first parameter of every registered callback is the synthetic `context`, which is not part + // of the public API. Strip it (unless the caller supplied an already-normalized signature) so that + // introspection/REST sees only the real user arguments in the correct order. + const resolvedSignature = signature ?? dropLeadingParams(extractFunctionSignature(callback), 1) + this.publications.set(name, { callback, signature: resolvedSignature, isCustom }) + } + + /** + * Register a publication with stricter typings. The subscription context is the first argument. + */ + publish( + name: K, + callback: ( + context: PublicationContext, + ...args: Parameters + ) => Promise> | null> + ): void { + this.publishUnsafe(name, callback as PublicationCallback) + } + + /** + * Register a custom publication, providing types for the custom collection it pushes documents into. + * The subscription context is the first argument, followed by the custom-publication handle. + */ + customPublish>( + publicationName: K, + customCollectionName: N, + cb: ( + context: PublicationContext, + publication: PublishIfDocument>, + ...args: Parameters + ) => Promise + ): void { + // The wrapper below has signature `(context, ...args)`, which would hide the real user arguments + // from `extractFunctionSignature`. Extract the public signature from the typed `cb` instead, + // dropping its `context` and `publication` parameters so REST/introspection sees only user args. + const signature = dropLeadingParams(extractFunctionSignature(cb), 2) + this.publishUnsafe( + publicationName, + async (context, ...args) => { + return cb( + context, + new CustomPublishMeteor(context, String(customCollectionName)) as any, + ...(args as any) + ) + }, + signature, + true + ) + } + + /** Look up a publication callback by name (for any standalone DDP server). */ + get(name: string): PublicationCallback | undefined { + return this.publications.get(name)?.callback + } + + /** + * Look up a publication callback for the legacy REST path. Returns `undefined` for custom publications, + * which rely on a working `onStop` to release their observers and so cannot be served by the fire-once + * REST path (doing so would leak resources while only ever returning an empty result). + */ + getCursorPublication(name: string): PublicationCallback | undefined { + const publication = this.publications.get(name) + if (!publication || publication.isCustom) return undefined + return publication.callback + } + + getAllPublicationNames(): string[] { + return Array.from(this.publications.keys()) + } + + /** + * Param-name signatures of all publications that one could be extracted from, keyed by name. + * Used by the legacy REST API to build its routes. + */ + getSignatures(): { [publicationName: string]: string[] } { + const signatures: { [publicationName: string]: string[] } = {} + for (const [name, publication] of this.publications) { + if (publication.signature) signatures[name] = publication.signature + } + return signatures + } + + /** Apply all registered publications to Meteor's DDP server. The only place that touches `Meteor.publish()`. */ + applyToMeteor(): void { + if (this.applied) throw new Meteor.Error(500, 'PublicationRegistry.applyToMeteor() has already been called') + this.applied = true + + for (const [name, publication] of this.publications) { + const { callback } = publication + const publicationGauge = MeteorPublicationsGauge.labels({ publication: name, server: 'meteor' }) // Future: custom ddp server should use a dynamic name instead of hardcoded + + Meteor.publish(name, async function (...args: any[]): Promise { + publicationGauge.inc() + this.onStop(() => publicationGauge.dec()) + + const callbackRes = await callback(new MeteorPublicationContext(this), ...args) + // If no value is returned, return an empty array so that meteor marks the subscription as ready + return callbackRes || [] + }) + } + } + + /** + * Verify that every known publication name has a registration. + * Replaces the historical dev-mode check that lived in `_publications.ts`. + */ + verifyAllPublicationsRegistered(): void { + for (const pubName of AllPubSubNames) { + if (!this.publications.has(pubName)) { + logger.error(`Publication "${pubName}" is not setup!`) + } + } + } +} diff --git a/meteor/server/publications/_publications.ts b/meteor/server/publications/_publications.ts deleted file mode 100644 index 501d8fa4d15..00000000000 --- a/meteor/server/publications/_publications.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { Meteor } from 'meteor/meteor' -import './lib/lib' - -import './buckets' -import './blueprintUpgradeStatus/publication' -import './ingestStatus/publication' -import './externalEventSubscriptions' -import './packageManager/expectedPackages/publication' -import './packageManager/packageContainers' -import './packageManager/playoutContext' -import './pieceContentStatusUI/bucket/publication' -import './pieceContentStatusUI/rundown/publication' -import './organization' -import './partsUI/publication' -import './partInstancesUI/publication' -import './peripheralDevice' -import './peripheralDeviceForDevice' -import './rundown' -import './rundownPlaylist' -import './segmentPartNotesUI/publication' -import './showStyle' -import './showStyleUI' -import './studio' -import './studioUI' -import './system' -import './timeline' -import './translationsBundles' -import './triggeredActionsUI' -import './mountedTriggers' -import './deviceTriggersPreview' - -import { AllPubSubNames } from '@sofie-automation/meteor-lib/dist/api/pubsub' -import { MeteorPublications } from './lib/lib' -import { logger } from '../logging' - -// Ensure all the publications were registered at startup -if (Meteor.isDevelopment) { - Meteor.startup(() => { - for (const pubName of AllPubSubNames) { - if (!MeteorPublications[pubName]) { - logger.error(`Publication "${pubName}" is not setup!`) - } - } - }) -} diff --git a/meteor/server/publications/blueprintUpgradeStatus/publication.ts b/meteor/server/publications/blueprintUpgradeStatus/publication.ts index e837ce2893e..f93c6718504 100644 --- a/meteor/server/publications/blueprintUpgradeStatus/publication.ts +++ b/meteor/server/publications/blueprintUpgradeStatus/publication.ts @@ -5,11 +5,11 @@ import { ProtectedString, protectString } from '@sofie-automation/corelib/dist/p import { CustomPublish, CustomPublishCollection, - meteorCustomPublish, setUpCollectionOptimizedObserver, SetupObserversResult, TriggerUpdate, } from '../../lib/customPublication' +import type { PublicationRegistry } from '../../publicationRegistry' import { ContentCache, CoreSystemFields, @@ -284,12 +284,14 @@ export async function createBlueprintUpgradeStatusSubscriptionHandle( ) } -meteorCustomPublish( - MeteorPubSub.uiBlueprintUpgradeStatuses, - CustomCollectionName.UIBlueprintUpgradeStatuses, - async function (pub) { - assertConnectionHasOneOfPermissions(this.connection, 'configure', 'service') +export function registerBlueprintUpgradeStatusPublications(registry: PublicationRegistry): void { + registry.customPublish( + MeteorPubSub.uiBlueprintUpgradeStatuses, + CustomCollectionName.UIBlueprintUpgradeStatuses, + async (context, pub) => { + assertConnectionHasOneOfPermissions(context.connection, 'configure', 'service') - await createBlueprintUpgradeStatusSubscriptionHandle(pub) - } -) + await createBlueprintUpgradeStatusSubscriptionHandle(pub) + } + ) +} diff --git a/meteor/server/publications/buckets.ts b/meteor/server/publications/buckets.ts index 06e5dfe4aaa..1c4ed26ad04 100644 --- a/meteor/server/publications/buckets.ts +++ b/meteor/server/publications/buckets.ts @@ -1,5 +1,4 @@ import { FindOptions } from '@sofie-automation/meteor-lib/dist/collections/lib' -import { meteorPublish } from './lib/lib' import { Bucket } from '@sofie-automation/corelib/dist/dataModel/Bucket' import { BucketAdLibActions, BucketAdLibs, Buckets } from '../collections' import { check, Match } from 'meteor/check' @@ -9,76 +8,79 @@ import { triggerWriteAccessBecauseNoCheckNecessary } from '../security/securityV import { MongoQuery } from '@sofie-automation/corelib/dist/mongo' import { BucketAdLib } from '@sofie-automation/corelib/dist/dataModel/BucketAdLibPiece' import { BucketAdLibAction } from '@sofie-automation/corelib/dist/dataModel/BucketAdLibAction' +import type { PublicationRegistry } from '../publicationRegistry' -meteorPublish( - CorelibPubSub.buckets, - async function (studioId: StudioId, bucketId: BucketId | null, _token: string | undefined) { - check(studioId, String) - check(bucketId, Match.Maybe(String)) +export function registerBucketsPublications(registry: PublicationRegistry): void { + registry.publish( + CorelibPubSub.buckets, + async (_context, studioId: StudioId, bucketId: BucketId | null, _token: string | undefined) => { + check(studioId, String) + check(bucketId, Match.Maybe(String)) - triggerWriteAccessBecauseNoCheckNecessary() + triggerWriteAccessBecauseNoCheckNecessary() - const modifier: FindOptions = { - projection: {}, - } + const modifier: FindOptions = { + projection: {}, + } + + const selector: MongoQuery = { + studioId, + } + if (bucketId) selector._id = bucketId - const selector: MongoQuery = { - studioId, + return Buckets.findWithCursor(selector, modifier) } - if (bucketId) selector._id = bucketId + ) - return Buckets.findWithCursor(selector, modifier) - } -) + registry.publish( + CorelibPubSub.bucketAdLibPieces, + async (_context, studioId: StudioId, bucketId: BucketId | null, showStyleVariantIds: ShowStyleVariantId[]) => { + check(studioId, String) + check(bucketId, Match.Maybe(String)) + check(showStyleVariantIds, Array) -meteorPublish( - CorelibPubSub.bucketAdLibPieces, - async function (studioId: StudioId, bucketId: BucketId | null, showStyleVariantIds: ShowStyleVariantId[]) { - check(studioId, String) - check(bucketId, Match.Maybe(String)) - check(showStyleVariantIds, Array) + triggerWriteAccessBecauseNoCheckNecessary() - triggerWriteAccessBecauseNoCheckNecessary() + const selector: MongoQuery = { + studioId: studioId, + showStyleVariantId: { + $in: [null, ...showStyleVariantIds], // null = valid for all variants + }, + } + if (bucketId) selector.bucketId = bucketId - const selector: MongoQuery = { - studioId: studioId, - showStyleVariantId: { - $in: [null, ...showStyleVariantIds], // null = valid for all variants - }, + return BucketAdLibs.findWithCursor(selector, { + projection: { + ingestInfo: 0, // This is a large blob, and is not of interest to the UI + privateData: 0, + }, + }) } - if (bucketId) selector.bucketId = bucketId + ) - return BucketAdLibs.findWithCursor(selector, { - projection: { - ingestInfo: 0, // This is a large blob, and is not of interest to the UI - privateData: 0, - }, - }) - } -) + registry.publish( + CorelibPubSub.bucketAdLibActions, + async (_context, studioId: StudioId, bucketId: BucketId | null, showStyleVariantIds: ShowStyleVariantId[]) => { + check(studioId, String) + check(bucketId, Match.Maybe(String)) + check(showStyleVariantIds, Array) -meteorPublish( - CorelibPubSub.bucketAdLibActions, - async function (studioId: StudioId, bucketId: BucketId | null, showStyleVariantIds: ShowStyleVariantId[]) { - check(studioId, String) - check(bucketId, Match.Maybe(String)) - check(showStyleVariantIds, Array) + triggerWriteAccessBecauseNoCheckNecessary() - triggerWriteAccessBecauseNoCheckNecessary() + const selector: MongoQuery = { + studioId: studioId, + showStyleVariantId: { + $in: [null, ...showStyleVariantIds], // null = valid for all variants + }, + } + if (bucketId) selector.bucketId = bucketId - const selector: MongoQuery = { - studioId: studioId, - showStyleVariantId: { - $in: [null, ...showStyleVariantIds], // null = valid for all variants - }, + return BucketAdLibActions.findWithCursor(selector, { + projection: { + ingestInfo: 0, // This is a large blob, and is not of interest to the UI + privateData: 0, + }, + }) } - if (bucketId) selector.bucketId = bucketId - - return BucketAdLibActions.findWithCursor(selector, { - projection: { - ingestInfo: 0, // This is a large blob, and is not of interest to the UI - privateData: 0, - }, - }) - } -) + ) +} diff --git a/meteor/server/publications/deviceTriggersPreview.ts b/meteor/server/publications/deviceTriggersPreview.ts index 7f55a3b0af0..ce1e4981b22 100644 --- a/meteor/server/publications/deviceTriggersPreview.ts +++ b/meteor/server/publications/deviceTriggersPreview.ts @@ -8,25 +8,28 @@ import { CustomCollectionName, MeteorPubSub } from '@sofie-automation/meteor-lib import { DeviceTriggerArguments, UIDeviceTriggerPreview } from '@sofie-automation/meteor-lib/dist/api/MountedTriggers' import { getCurrentTime } from '../lib/lib' import { SetupObserversResult, setUpOptimizedObserverArray, TriggerUpdate } from '../lib/customPublication' -import { CustomPublish, meteorCustomPublish } from '../lib/customPublication/publish' +import { CustomPublish } from '../lib/customPublication/publish' import { PeripheralDevices } from '../collections' import { assertConnectionHasOneOfPermissions } from '../security/auth' +import type { PublicationRegistry } from '../publicationRegistry' /** IDEA: This could potentially be a Capped Collection, thus enabling scaling Core horizontally: * https://www.mongodb.com/docs/manual/core/capped-collections/ */ const lastTriggers: Record void) | undefined }> = {} -meteorCustomPublish( - MeteorPubSub.deviceTriggersPreview, - CustomCollectionName.UIDeviceTriggerPreviews, - async function (pub, studioId: StudioId, _token: string | undefined) { - check(studioId, String) +export function registerDeviceTriggersPreviewPublications(registry: PublicationRegistry): void { + registry.customPublish( + MeteorPubSub.deviceTriggersPreview, + CustomCollectionName.UIDeviceTriggerPreviews, + async (context, pub, studioId: StudioId, _token: string | undefined) => { + check(studioId, String) - assertConnectionHasOneOfPermissions(this.connection, 'configure') + assertConnectionHasOneOfPermissions(context.connection, 'configure') - await createObserverForDeviceTriggersPreviewsPublication(pub, MeteorPubSub.deviceTriggersPreview, studioId) - } -) + await createObserverForDeviceTriggersPreviewsPublication(pub, MeteorPubSub.deviceTriggersPreview, studioId) + } + ) +} export async function insertInputDeviceTriggerIntoPreview( deviceId: PeripheralDeviceId, diff --git a/meteor/server/publications/externalEventSubscriptions.ts b/meteor/server/publications/externalEventSubscriptions.ts index acf8b14d559..3e88156c895 100644 --- a/meteor/server/publications/externalEventSubscriptions.ts +++ b/meteor/server/publications/externalEventSubscriptions.ts @@ -8,11 +8,11 @@ import { ReadonlyDeep } from 'type-fest' import { CustomPublish, CustomPublishCollection, - meteorCustomPublish, setUpCollectionOptimizedObserver, SetupObserversResult, TriggerUpdate, } from '../lib/customPublication' +import type { PublicationRegistry } from '../publicationRegistry' import { logger } from '../logging' import { RundownPlaylists, Rundowns } from '../collections' import { @@ -152,28 +152,31 @@ async function startOrJoinExternalEventSubscriptionsPublication( ) } -meteorCustomPublish( - PeripheralDevicePubSub.externalEventSubscriptionsForDevice, - PeripheralDevicePubSubCollectionsNames.externalEventSubscriptions, - async function ( - pub: CustomPublish, - type: PeripheralDeviceExternalEvent['type'], - deviceId: PeripheralDeviceId, - token: string | undefined - ) { - check(deviceId, String) - check(type, String) - - const peripheralDevice = await checkAccessAndGetPeripheralDevice(deviceId, token, this) - - const studioId = peripheralDevice.studioAndConfigId?.studioId - if (!studioId) { - logger.warn( - `Publication ${PeripheralDevicePubSub.externalEventSubscriptionsForDevice}: device ${deviceId} has no studio` - ) - return - } +export function registerExternalEventSubscriptionsPublications(registry: PublicationRegistry): void { + registry.customPublish( + PeripheralDevicePubSub.externalEventSubscriptionsForDevice, + PeripheralDevicePubSubCollectionsNames.externalEventSubscriptions, + async ( + context, + pub: CustomPublish, + type: PeripheralDeviceExternalEvent['type'], + deviceId: PeripheralDeviceId, + token: string | undefined + ) => { + check(deviceId, String) + check(type, String) + + const peripheralDevice = await checkAccessAndGetPeripheralDevice(deviceId, token, context) + + const studioId = peripheralDevice.studioAndConfigId?.studioId + if (!studioId) { + logger.warn( + `Publication ${PeripheralDevicePubSub.externalEventSubscriptionsForDevice}: device ${deviceId} has no studio` + ) + return + } - await startOrJoinExternalEventSubscriptionsPublication(pub, studioId, type) - } -) + await startOrJoinExternalEventSubscriptionsPublication(pub, studioId, type) + } + ) +} diff --git a/meteor/server/publications/ingestStatus/publication.ts b/meteor/server/publications/ingestStatus/publication.ts index a28dd3844f1..89a0f3d3ba8 100644 --- a/meteor/server/publications/ingestStatus/publication.ts +++ b/meteor/server/publications/ingestStatus/publication.ts @@ -3,7 +3,6 @@ import { ReadonlyDeep } from 'type-fest' import { CustomPublish, CustomPublishCollection, - meteorCustomPublish, setUpCollectionOptimizedObserver, SetupObserversResult, TriggerUpdate, @@ -24,6 +23,7 @@ import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { createIngestRundownStatus } from './createIngestRundownStatus' import { assertConnectionHasOneOfPermissions } from '../../security/auth' import { MeteorPubSub } from '@sofie-automation/meteor-lib/dist/api/pubsub' +import type { PublicationRegistry } from '../../publicationRegistry' interface IngestRundownStatusArgs { readonly deviceId: PeripheralDeviceId @@ -192,26 +192,28 @@ async function startOrJoinIngestStatusPublication( ) } -meteorCustomPublish( - PeripheralDevicePubSub.ingestDeviceRundownStatus, - PeripheralDevicePubSubCollectionsNames.ingestRundownStatus, - async function (pub, deviceId: PeripheralDeviceId, token: string | undefined) { - check(deviceId, String) +export function registerIngestStatusPublications(registry: PublicationRegistry): void { + registry.customPublish( + PeripheralDevicePubSub.ingestDeviceRundownStatus, + PeripheralDevicePubSubCollectionsNames.ingestRundownStatus, + async (context, pub, deviceId: PeripheralDeviceId, token: string | undefined) => { + check(deviceId, String) - await checkAccessAndGetPeripheralDevice(deviceId, token, this) + await checkAccessAndGetPeripheralDevice(deviceId, token, context) - await startOrJoinIngestStatusPublication(pub, deviceId) - } -) + await startOrJoinIngestStatusPublication(pub, deviceId) + } + ) -meteorCustomPublish( - MeteorPubSub.ingestDeviceRundownStatusTestTool, - PeripheralDevicePubSubCollectionsNames.ingestRundownStatus, - async function (pub, deviceId: PeripheralDeviceId) { - check(deviceId, String) + registry.customPublish( + MeteorPubSub.ingestDeviceRundownStatusTestTool, + PeripheralDevicePubSubCollectionsNames.ingestRundownStatus, + async (context, pub, deviceId: PeripheralDeviceId) => { + check(deviceId, String) - assertConnectionHasOneOfPermissions(this.connection, 'testing') + assertConnectionHasOneOfPermissions(context.connection, 'testing') - await startOrJoinIngestStatusPublication(pub, deviceId) - } -) + await startOrJoinIngestStatusPublication(pub, deviceId) + } + ) +} diff --git a/meteor/server/publications/lib/lib.ts b/meteor/server/publications/lib/lib.ts index 62bb2aa2e42..b53efec0978 100644 --- a/meteor/server/publications/lib/lib.ts +++ b/meteor/server/publications/lib/lib.ts @@ -1,46 +1,25 @@ -import { Meteor, Subscription } from 'meteor/meteor' +import { Meteor } from 'meteor/meteor' import { AllPubSubCollections, AllPubSubTypes } from '@sofie-automation/meteor-lib/dist/api/pubsub' -import { extractFunctionSignature } from '../../lib' -import { protectStringObject } from '@sofie-automation/corelib/dist/protectedString' -import { MetricsGauge } from '@sofie-automation/corelib/dist/prometheus' -import { MinimalMongoCursor } from '../../collections/implementations/asyncCollection' - -export const MeteorPublicationSignatures: { [key: string]: string[] } = {} -// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type -export const MeteorPublications: { [key: string]: Function } = {} - -const MeteorPublicationsGauge = new MetricsGauge({ - name: `sofie_meteor_publication_subscribers_total`, - help: 'Number of subscribers on a Meteor publication (ignoring arguments)', - labelNames: ['publication'], -}) - -export type SubscriptionContext = Omit /** - * Unsafe wrapper around Meteor.publish - * @param name - * @param callback + * The context handed to a publication callback. */ -export function meteorPublishUnsafe( - name: string, - callback: (this: SubscriptionContext, ...args: any) => Promise -): void { - const signature = extractFunctionSignature(callback) - if (signature) MeteorPublicationSignatures[name] = signature - - MeteorPublications[name] = callback - - const publicationGauge = MeteorPublicationsGauge.labels({ publication: name }) - - Meteor.publish(name, async function (...args: any[]): Promise { - publicationGauge.inc() - this.onStop(() => publicationGauge.dec()) - - const callbackRes = await callback.apply(protectStringObject(this), args) - // If no value is returned, return an empty array so that meteor marks the subscription as ready - return callbackRes || [] - }) +export interface PublicationContext { + /** The client connection that opened this subscription. Used by the auth layer for permission checks. */ + readonly connection: Meteor.Connection | null + + /** Register a function to be called when the subscriber unsubscribes. */ + onStop(callback: () => void): void + + /** Mark the subscription as ready (i.e. the initial set of documents has been sent). */ + ready(): void + + /** Send an added document to the subscriber. */ + added(collection: string, id: string, fields: Record): void + /** Send a changed document (changed fields only) to the subscriber. */ + changed(collection: string, id: string, fields: Record): void + /** Send a removed document to the subscriber. */ + removed(collection: string, id: string): void } export type PublishDocType = @@ -48,21 +27,6 @@ export type PublishDocType = ? AllPubSubCollections[ReturnType] : never -/** - * Wrapper around Meteor.publish with stricter typings - * @param name - * @param callback - */ -export function meteorPublish( - name: K, - callback: ( - this: SubscriptionContext, - ...args: Parameters - ) => Promise> | null> -): void { - meteorPublishUnsafe(name, callback) -} - /** * Await each observer, and return the handles * If an observer throws, this will make sure to stop all the ones that were successfully started, to avoid leaking memory diff --git a/meteor/server/publications/mountedTriggers.ts b/meteor/server/publications/mountedTriggers.ts index aa9469b283c..e333ac6b50b 100644 --- a/meteor/server/publications/mountedTriggers.ts +++ b/meteor/server/publications/mountedTriggers.ts @@ -1,5 +1,5 @@ import { Meteor } from 'meteor/meteor' -import { CustomPublish, meteorCustomPublish } from '../lib/customPublication' +import { CustomPublish } from '../lib/customPublication' import { PeripheralDeviceId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { logger } from '../logging' import { DeviceTriggerMountedActionAdlibsPreview, DeviceTriggerMountedActions } from '../api/deviceTriggers/observer' @@ -13,54 +13,57 @@ import { } from '@sofie-automation/shared-lib/dist/pubsub/peripheralDevice' import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' import { checkAccessAndGetPeripheralDevice } from '../security/check' +import type { PublicationRegistry } from '../publicationRegistry' const PUBLICATION_DEBOUNCE = 20 -meteorCustomPublish( - PeripheralDevicePubSub.mountedTriggersForDevice, - PeripheralDevicePubSubCollectionsNames.mountedTriggers, - async function (pub, deviceId: PeripheralDeviceId, deviceIds: string[], token: string | undefined) { - check(deviceId, String) - check(deviceIds, [String]) - - const peripheralDevice = await checkAccessAndGetPeripheralDevice(deviceId, token, this) - - const studioId = peripheralDevice.studioAndConfigId?.studioId - if (!studioId) throw new Meteor.Error(400, `Peripheral Device "${deviceId}" not attached to a studio`) - - cursorCustomPublish( - pub, - DeviceTriggerMountedActions.find({ - studioId, - deviceId: { - $in: deviceIds, - }, - }), - PeripheralDevicePubSub.mountedTriggersForDevice - ) - } -) - -meteorCustomPublish( - PeripheralDevicePubSub.mountedTriggersForDevicePreview, - PeripheralDevicePubSubCollectionsNames.mountedTriggersPreviews, - async function (pub, deviceId: PeripheralDeviceId, token: string | undefined) { - check(deviceId, String) - - const peripheralDevice = await checkAccessAndGetPeripheralDevice(deviceId, token, this) - - const studioId = peripheralDevice.studioAndConfigId?.studioId - if (!studioId) throw new Meteor.Error(400, `Peripheral Device "${deviceId}" not attached to a studio`) - - cursorCustomPublish( - pub, - DeviceTriggerMountedActionAdlibsPreview.find({ - studioId, - }), - PeripheralDevicePubSub.mountedTriggersForDevicePreview - ) - } -) +export function registerMountedTriggersPublications(registry: PublicationRegistry): void { + registry.customPublish( + PeripheralDevicePubSub.mountedTriggersForDevice, + PeripheralDevicePubSubCollectionsNames.mountedTriggers, + async (context, pub, deviceId: PeripheralDeviceId, deviceIds: string[], token: string | undefined) => { + check(deviceId, String) + check(deviceIds, [String]) + + const peripheralDevice = await checkAccessAndGetPeripheralDevice(deviceId, token, context) + + const studioId = peripheralDevice.studioAndConfigId?.studioId + if (!studioId) throw new Meteor.Error(400, `Peripheral Device "${deviceId}" not attached to a studio`) + + cursorCustomPublish( + pub, + DeviceTriggerMountedActions.find({ + studioId, + deviceId: { + $in: deviceIds, + }, + }), + PeripheralDevicePubSub.mountedTriggersForDevice + ) + } + ) + + registry.customPublish( + PeripheralDevicePubSub.mountedTriggersForDevicePreview, + PeripheralDevicePubSubCollectionsNames.mountedTriggersPreviews, + async (context, pub, deviceId: PeripheralDeviceId, token: string | undefined) => { + check(deviceId, String) + + const peripheralDevice = await checkAccessAndGetPeripheralDevice(deviceId, token, context) + + const studioId = peripheralDevice.studioAndConfigId?.studioId + if (!studioId) throw new Meteor.Error(400, `Peripheral Device "${deviceId}" not attached to a studio`) + + cursorCustomPublish( + pub, + DeviceTriggerMountedActionAdlibsPreview.find({ + studioId, + }), + PeripheralDevicePubSub.mountedTriggersForDevicePreview + ) + } + ) +} interface CustomOptimizedPublishChanges }> { added: Map diff --git a/meteor/server/publications/organization.ts b/meteor/server/publications/organization.ts index e4ac642e56f..4d3d0eaa1ea 100644 --- a/meteor/server/publications/organization.ts +++ b/meteor/server/publications/organization.ts @@ -1,4 +1,3 @@ -import { meteorPublish } from './lib/lib' import { MeteorPubSub } from '@sofie-automation/meteor-lib/dist/api/pubsub' import { Blueprint } from '@sofie-automation/corelib/dist/dataModel/Blueprint' import { Evaluation } from '@sofie-automation/meteor-lib/dist/collections/Evaluations' @@ -12,65 +11,71 @@ import { check, Match } from '../lib/check' import { getCurrentTime } from '../lib/lib' import { triggerWriteAccessBecauseNoCheckNecessary } from '../security/securityVerify' import { assertConnectionHasOneOfPermissions } from '../security/auth' +import type { PublicationRegistry } from '../publicationRegistry' -meteorPublish( - CorelibPubSub.blueprints, - async function (blueprintIds: BlueprintId[] | null, _token: string | undefined) { - assertConnectionHasOneOfPermissions(this.connection, 'configure') +export function registerOrganizationPublications(registry: PublicationRegistry): void { + registry.publish( + CorelibPubSub.blueprints, + async (context, blueprintIds: BlueprintId[] | null, _token: string | undefined) => { + assertConnectionHasOneOfPermissions(context.connection, 'configure') - check(blueprintIds, Match.Maybe(Array)) + check(blueprintIds, Match.Maybe(Array)) - // If values were provided, they must have values - if (blueprintIds && blueprintIds.length === 0) return null + // If values were provided, they must have values + if (blueprintIds && blueprintIds.length === 0) return null - // Add the requested filter - const selector: MongoQuery = {} - if (blueprintIds) selector._id = { $in: blueprintIds } + // Add the requested filter + const selector: MongoQuery = {} + if (blueprintIds) selector._id = { $in: blueprintIds } - return Blueprints.findWithCursor(selector, { - projection: { - code: 0, - }, - }) - } -) -meteorPublish(MeteorPubSub.evaluations, async function (dateFrom: number, dateTo: number, _token: string | undefined) { - triggerWriteAccessBecauseNoCheckNecessary() - - const selector: MongoQuery = { - timestamp: { - $gte: dateFrom, - $lt: dateTo, - }, - } - - return Evaluations.findWithCursor(selector) -}) -meteorPublish(MeteorPubSub.snapshots, async function (_token: string | undefined) { - assertConnectionHasOneOfPermissions(this.connection, 'configure') + return Blueprints.findWithCursor(selector, { + projection: { + code: 0, + }, + }) + } + ) + registry.publish( + MeteorPubSub.evaluations, + async (_context, dateFrom: number, dateTo: number, _token: string | undefined) => { + triggerWriteAccessBecauseNoCheckNecessary() - const selector: MongoQuery = { - created: { - $gt: getCurrentTime() - 30 * 24 * 3600 * 1000, // last 30 days - }, - } + const selector: MongoQuery = { + timestamp: { + $gte: dateFrom, + $lt: dateTo, + }, + } - return Snapshots.findWithCursor(selector) -}) -meteorPublish( - MeteorPubSub.userActionsLog, - async function (dateFrom: number, dateTo: number, _token: string | undefined) { - triggerWriteAccessBecauseNoCheckNecessary() + return Evaluations.findWithCursor(selector) + } + ) + registry.publish(MeteorPubSub.snapshots, async (context, _token: string | undefined) => { + assertConnectionHasOneOfPermissions(context.connection, 'configure') - const selector: MongoQuery = { - timestamp: { - $gte: dateFrom, - $lt: dateTo, + const selector: MongoQuery = { + created: { + $gt: getCurrentTime() - 30 * 24 * 3600 * 1000, // last 30 days }, } - return UserActionsLog.findWithCursor(selector, { - limit: 10_000, // this is to prevent having a publication that produces a very large array - }) - } -) + return Snapshots.findWithCursor(selector) + }) + registry.publish( + MeteorPubSub.userActionsLog, + async (_context, dateFrom: number, dateTo: number, _token: string | undefined) => { + triggerWriteAccessBecauseNoCheckNecessary() + + const selector: MongoQuery = { + timestamp: { + $gte: dateFrom, + $lt: dateTo, + }, + } + + return UserActionsLog.findWithCursor(selector, { + limit: 10_000, // this is to prevent having a publication that produces a very large array + }) + } + ) +} diff --git a/meteor/server/publications/packageManager/expectedPackages/publication.ts b/meteor/server/publications/packageManager/expectedPackages/publication.ts index 2fbd90b9e1e..3fa63285d5f 100644 --- a/meteor/server/publications/packageManager/expectedPackages/publication.ts +++ b/meteor/server/publications/packageManager/expectedPackages/publication.ts @@ -1,11 +1,11 @@ import { DBStudio, StudioPackageContainer } from '@sofie-automation/corelib/dist/dataModel/Studio' import { TriggerUpdate, - meteorCustomPublish, setUpCollectionOptimizedObserver, CustomPublishCollection, SetupObserversResult, } from '../../../lib/customPublication' +import type { PublicationRegistry } from '../../../publicationRegistry' import { literal, omit } from '@sofie-automation/corelib/dist/lib' import { protectString } from '@sofie-automation/corelib/dist/protectedString' import { logger } from '../../../logging' @@ -235,40 +235,43 @@ function updatePackagePriorities( }) } -meteorCustomPublish( - PeripheralDevicePubSub.packageManagerExpectedPackages, - PeripheralDevicePubSubCollectionsNames.packageManagerExpectedPackages, - async function ( - pub, - deviceId: PeripheralDeviceId, - filterPlayoutDeviceIds: PeripheralDeviceId[] | undefined, - token: string | undefined - ) { - check(deviceId, String) - check(filterPlayoutDeviceIds, Match.Maybe([String])) - - const peripheralDevice = await checkAccessAndGetPeripheralDevice(deviceId, token, this) - - const studioId = peripheralDevice.studioAndConfigId?.studioId - if (!studioId) { - logger.warn(`Pub.packageManagerExpectedPackages: device "${peripheralDevice._id}" has no studioId`) - return this.ready() - } - - await setUpCollectionOptimizedObserver< - PackageManagerExpectedPackage, - ExpectedPackagesPublicationArgs, - ExpectedPackagesPublicationState, - ExpectedPackagesPublicationUpdateProps - >( - `${PeripheralDevicePubSub.packageManagerExpectedPackages}_${studioId}_${deviceId}_${JSON.stringify( - (filterPlayoutDeviceIds || []).sort() - )}`, - { studioId, deviceId, filterPlayoutDeviceIds }, - setupExpectedPackagesPublicationObservers, - manipulateExpectedPackagesPublicationData, +export function registerExpectedPackagesPublications(registry: PublicationRegistry): void { + registry.customPublish( + PeripheralDevicePubSub.packageManagerExpectedPackages, + PeripheralDevicePubSubCollectionsNames.packageManagerExpectedPackages, + async ( + context, pub, - 500 // ms, wait this time before sending an update - ) - } -) + deviceId: PeripheralDeviceId, + filterPlayoutDeviceIds: PeripheralDeviceId[] | undefined, + token: string | undefined + ) => { + check(deviceId, String) + check(filterPlayoutDeviceIds, Match.Maybe([String])) + + const peripheralDevice = await checkAccessAndGetPeripheralDevice(deviceId, token, context) + + const studioId = peripheralDevice.studioAndConfigId?.studioId + if (!studioId) { + logger.warn(`Pub.packageManagerExpectedPackages: device "${peripheralDevice._id}" has no studioId`) + return context.ready() + } + + await setUpCollectionOptimizedObserver< + PackageManagerExpectedPackage, + ExpectedPackagesPublicationArgs, + ExpectedPackagesPublicationState, + ExpectedPackagesPublicationUpdateProps + >( + `${PeripheralDevicePubSub.packageManagerExpectedPackages}_${studioId}_${deviceId}_${JSON.stringify( + (filterPlayoutDeviceIds || []).sort() + )}`, + { studioId, deviceId, filterPlayoutDeviceIds }, + setupExpectedPackagesPublicationObservers, + manipulateExpectedPackagesPublicationData, + pub, + 500 // ms, wait this time before sending an update + ) + } + ) +} diff --git a/meteor/server/publications/packageManager/packageContainers.ts b/meteor/server/publications/packageManager/packageContainers.ts index 6c838a29596..10491a146b3 100644 --- a/meteor/server/publications/packageManager/packageContainers.ts +++ b/meteor/server/publications/packageManager/packageContainers.ts @@ -7,12 +7,8 @@ import { PackageManagerPackageContainers } from '@sofie-automation/shared-lib/di import { check } from 'meteor/check' import { ReadonlyDeep } from 'type-fest' import { Studios } from '../../collections' -import { - meteorCustomPublish, - SetupObserversResult, - setUpOptimizedObserverArray, - TriggerUpdate, -} from '../../lib/customPublication' +import { SetupObserversResult, setUpOptimizedObserverArray, TriggerUpdate } from '../../lib/customPublication' +import type { PublicationRegistry } from '../../publicationRegistry' import { logger } from '../../logging' import { PeripheralDevicePubSub, @@ -89,32 +85,34 @@ async function manipulateExpectedPackagesPublicationData( ]) } -meteorCustomPublish( - PeripheralDevicePubSub.packageManagerPackageContainers, - PeripheralDevicePubSubCollectionsNames.packageManagerPackageContainers, - async function (pub, deviceId: PeripheralDeviceId, token: string | undefined) { - check(deviceId, String) +export function registerPackageContainersPublications(registry: PublicationRegistry): void { + registry.customPublish( + PeripheralDevicePubSub.packageManagerPackageContainers, + PeripheralDevicePubSubCollectionsNames.packageManagerPackageContainers, + async (context, pub, deviceId: PeripheralDeviceId, token: string | undefined) => { + check(deviceId, String) - const peripheralDevice = await checkAccessAndGetPeripheralDevice(deviceId, token, this) + const peripheralDevice = await checkAccessAndGetPeripheralDevice(deviceId, token, context) - const studioId = peripheralDevice.studioAndConfigId?.studioId - if (!studioId) { - logger.warn(`Pub.packageManagerPackageContainers: device "${peripheralDevice._id}" has no studioId`) - return this.ready() - } + const studioId = peripheralDevice.studioAndConfigId?.studioId + if (!studioId) { + logger.warn(`Pub.packageManagerPackageContainers: device "${peripheralDevice._id}" has no studioId`) + return context.ready() + } - await setUpOptimizedObserverArray< - PackageManagerPackageContainers, - PackageManagerPackageContainersArgs, - PackageManagerPackageContainersState, - PackageManagerPackageContainersUpdateProps - >( - `${PeripheralDevicePubSub.packageManagerPackageContainers}_${studioId}_${deviceId}`, - { studioId, deviceId }, - setupExpectedPackagesPublicationObservers, - manipulateExpectedPackagesPublicationData, - pub, - 500 // ms, wait this time before sending an update - ) - } -) + await setUpOptimizedObserverArray< + PackageManagerPackageContainers, + PackageManagerPackageContainersArgs, + PackageManagerPackageContainersState, + PackageManagerPackageContainersUpdateProps + >( + `${PeripheralDevicePubSub.packageManagerPackageContainers}_${studioId}_${deviceId}`, + { studioId, deviceId }, + setupExpectedPackagesPublicationObservers, + manipulateExpectedPackagesPublicationData, + pub, + 500 // ms, wait this time before sending an update + ) + } + ) +} diff --git a/meteor/server/publications/packageManager/playoutContext.ts b/meteor/server/publications/packageManager/playoutContext.ts index bb3f5390a11..910b10dc5c1 100644 --- a/meteor/server/publications/packageManager/playoutContext.ts +++ b/meteor/server/publications/packageManager/playoutContext.ts @@ -7,12 +7,8 @@ import { PackageManagerPlayoutContext } from '@sofie-automation/shared-lib/dist/ import { check } from 'meteor/check' import { ReadonlyDeep } from 'type-fest' import { RundownPlaylists, Rundowns } from '../../collections' -import { - meteorCustomPublish, - SetupObserversResult, - setUpOptimizedObserverArray, - TriggerUpdate, -} from '../../lib/customPublication' +import { SetupObserversResult, setUpOptimizedObserverArray, TriggerUpdate } from '../../lib/customPublication' +import type { PublicationRegistry } from '../../publicationRegistry' import { logger } from '../../logging' import { PeripheralDevicePubSub, @@ -107,32 +103,34 @@ async function manipulateExpectedPackagesPublicationData( ]) } -meteorCustomPublish( - PeripheralDevicePubSub.packageManagerPlayoutContext, - PeripheralDevicePubSubCollectionsNames.packageManagerPlayoutContext, - async function (pub, deviceId: PeripheralDeviceId, token: string | undefined) { - check(deviceId, String) +export function registerPlayoutContextPublications(registry: PublicationRegistry): void { + registry.customPublish( + PeripheralDevicePubSub.packageManagerPlayoutContext, + PeripheralDevicePubSubCollectionsNames.packageManagerPlayoutContext, + async (context, pub, deviceId: PeripheralDeviceId, token: string | undefined) => { + check(deviceId, String) - const peripheralDevice = await checkAccessAndGetPeripheralDevice(deviceId, token, this) + const peripheralDevice = await checkAccessAndGetPeripheralDevice(deviceId, token, context) - const studioId = peripheralDevice.studioAndConfigId?.studioId - if (!studioId) { - logger.warn(`Pub.packageManagerPlayoutContext: device "${peripheralDevice._id}" has no studioId`) - return this.ready() - } + const studioId = peripheralDevice.studioAndConfigId?.studioId + if (!studioId) { + logger.warn(`Pub.packageManagerPlayoutContext: device "${peripheralDevice._id}" has no studioId`) + return context.ready() + } - await setUpOptimizedObserverArray< - PackageManagerPlayoutContext, - PackageManagerPlayoutContextArgs, - PackageManagerPlayoutContextState, - PackageManagerPlayoutContextUpdateProps - >( - `${PeripheralDevicePubSub.packageManagerPlayoutContext}_${studioId}_${deviceId}`, - { studioId, deviceId }, - setupExpectedPackagesPublicationObservers, - manipulateExpectedPackagesPublicationData, - pub, - 500 // ms, wait this time before sending an update - ) - } -) + await setUpOptimizedObserverArray< + PackageManagerPlayoutContext, + PackageManagerPlayoutContextArgs, + PackageManagerPlayoutContextState, + PackageManagerPlayoutContextUpdateProps + >( + `${PeripheralDevicePubSub.packageManagerPlayoutContext}_${studioId}_${deviceId}`, + { studioId, deviceId }, + setupExpectedPackagesPublicationObservers, + manipulateExpectedPackagesPublicationData, + pub, + 500 // ms, wait this time before sending an update + ) + } + ) +} diff --git a/meteor/server/publications/partInstancesUI/publication.ts b/meteor/server/publications/partInstancesUI/publication.ts index 464cc6cb51d..305191819d4 100644 --- a/meteor/server/publications/partInstancesUI/publication.ts +++ b/meteor/server/publications/partInstancesUI/publication.ts @@ -4,7 +4,6 @@ import { CustomPublishCollection, SetupObserversResult, TriggerUpdate, - meteorCustomPublish, setUpCollectionOptimizedObserver, } from '../../lib/customPublication' import { logger } from '../../logging' @@ -27,6 +26,7 @@ import { stringsToIndexLookup, } from '../lib/quickLoop' import { triggerWriteAccessBecauseNoCheckNecessary } from '../../security/securityVerify' +import type { PublicationRegistry } from '../../publicationRegistry' interface UIPartInstancesArgs { readonly playlistActivationId: RundownPlaylistActivationId @@ -209,30 +209,32 @@ export async function manipulateUIPartInstancesPublicationData( }) } -meteorCustomPublish( - MeteorPubSub.uiPartInstances, - CustomCollectionName.UIPartInstances, - async function (pub, playlistActivationId: RundownPlaylistActivationId | null) { - check(playlistActivationId, Match.Maybe(String)) +export function registerPartInstancesUIPublications(registry: PublicationRegistry): void { + registry.customPublish( + MeteorPubSub.uiPartInstances, + CustomCollectionName.UIPartInstances, + async (_context, pub, playlistActivationId: RundownPlaylistActivationId | null) => { + check(playlistActivationId, Match.Maybe(String)) - triggerWriteAccessBecauseNoCheckNecessary() + triggerWriteAccessBecauseNoCheckNecessary() - if (!playlistActivationId) { - logger.info(`Pub.${CustomCollectionName.UISegmentPartNotes}: Not playlistActivationId`) - return - } + if (!playlistActivationId) { + logger.info(`Pub.${CustomCollectionName.UISegmentPartNotes}: Not playlistActivationId`) + return + } - await setUpCollectionOptimizedObserver< - Omit, - UIPartInstancesArgs, - UIPartInstancesState, - UIPartInstancesUpdateProps - >( - `pub_${MeteorPubSub.uiPartInstances}_${playlistActivationId}`, - { playlistActivationId }, - setupUIPartInstancesPublicationObservers, - manipulateUIPartInstancesPublicationData, - pub - ) - } -) + await setUpCollectionOptimizedObserver< + Omit, + UIPartInstancesArgs, + UIPartInstancesState, + UIPartInstancesUpdateProps + >( + `pub_${MeteorPubSub.uiPartInstances}_${playlistActivationId}`, + { playlistActivationId }, + setupUIPartInstancesPublicationObservers, + manipulateUIPartInstancesPublicationData, + pub + ) + } + ) +} diff --git a/meteor/server/publications/partsUI/publication.ts b/meteor/server/publications/partsUI/publication.ts index 8474450ecc5..0a0247591c5 100644 --- a/meteor/server/publications/partsUI/publication.ts +++ b/meteor/server/publications/partsUI/publication.ts @@ -4,7 +4,6 @@ import { CustomPublishCollection, SetupObserversResult, TriggerUpdate, - meteorCustomPublish, setUpCollectionOptimizedObserver, } from '../../lib/customPublication' import { logger } from '../../logging' @@ -21,6 +20,7 @@ import { RundownContentObserver } from './rundownContentObserver' import { protectString } from '@sofie-automation/corelib/dist/protectedString' import { extractRanks, findMarkerPosition, modifyPartForQuickLoop, stringsToIndexLookup } from '../lib/quickLoop' import { triggerWriteAccessBecauseNoCheckNecessary } from '../../security/securityVerify' +import type { PublicationRegistry } from '../../publicationRegistry' interface UIPartsArgs { readonly playlistId: RundownPlaylistId @@ -187,30 +187,32 @@ export async function manipulateUIPartsPublicationData( }) } -meteorCustomPublish( - MeteorPubSub.uiParts, - CustomCollectionName.UIParts, - async function (pub, playlistId: RundownPlaylistId | null) { - check(playlistId, Match.Maybe(String)) +export function registerPartsUIPublications(registry: PublicationRegistry): void { + registry.customPublish( + MeteorPubSub.uiParts, + CustomCollectionName.UIParts, + async (_context, pub, playlistId: RundownPlaylistId | null) => { + check(playlistId, Match.Maybe(String)) - triggerWriteAccessBecauseNoCheckNecessary() + triggerWriteAccessBecauseNoCheckNecessary() - if (!playlistId) { - logger.warn(`Pub.uiParts: Not allowed: "${playlistId}"`) - return - } + if (!playlistId) { + logger.warn(`Pub.uiParts: Not allowed: "${playlistId}"`) + return + } - await setUpCollectionOptimizedObserver< - Omit, - UIPartsArgs, - UIPartsState, - UIPartsUpdateProps - >( - `pub_${MeteorPubSub.uiParts}_${playlistId}`, - { playlistId }, - setupUIPartsPublicationObservers, - manipulateUIPartsPublicationData, - pub - ) - } -) + await setUpCollectionOptimizedObserver< + Omit, + UIPartsArgs, + UIPartsState, + UIPartsUpdateProps + >( + `pub_${MeteorPubSub.uiParts}_${playlistId}`, + { playlistId }, + setupUIPartsPublicationObservers, + manipulateUIPartsPublicationData, + pub + ) + } + ) +} diff --git a/meteor/server/publications/peripheralDevice.ts b/meteor/server/publications/peripheralDevice.ts index 776c4591715..a8cf1828734 100644 --- a/meteor/server/publications/peripheralDevice.ts +++ b/meteor/server/publications/peripheralDevice.ts @@ -1,5 +1,4 @@ import { check, Match } from '../lib/check' -import { meteorPublish } from './lib/lib' import { PeripheralDevice } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' import { MongoFieldSpecifierZeroes, MongoQuery } from '@sofie-automation/corelib/dist/mongo' import { PeripheralDeviceId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' @@ -9,6 +8,7 @@ import { PeripheralDevicePubSub } from '@sofie-automation/shared-lib/dist/pubsub import { clone } from '@sofie-automation/corelib/dist/lib' import { triggerWriteAccessBecauseNoCheckNecessary } from '../security/securityVerify' import { checkAccessAndGetPeripheralDevice } from '../security/check' +import type { PublicationRegistry } from '../publicationRegistry' /* * This file contains publications for the peripheralDevices, such as playout-gateway, mos-gateway and package-manager @@ -19,62 +19,64 @@ const peripheralDeviceProjection: MongoFieldSpecifierZeroes = secretSettings: 0, } -meteorPublish( - CorelibPubSub.peripheralDevices, - async function (peripheralDeviceIds: PeripheralDeviceId[] | null, token: string | undefined) { - check(peripheralDeviceIds, Match.Maybe(Array)) +export function registerPeripheralDevicePublications(registry: PublicationRegistry): void { + registry.publish( + CorelibPubSub.peripheralDevices, + async (_context, peripheralDeviceIds: PeripheralDeviceId[] | null, token: string | undefined) => { + check(peripheralDeviceIds, Match.Maybe(Array)) - triggerWriteAccessBecauseNoCheckNecessary() + triggerWriteAccessBecauseNoCheckNecessary() - // If values were provided, they must have values - if (peripheralDeviceIds && peripheralDeviceIds.length === 0) return null + // If values were provided, they must have values + if (peripheralDeviceIds && peripheralDeviceIds.length === 0) return null - // Add the requested filter - const selector: MongoQuery = {} - if (peripheralDeviceIds) selector._id = { $in: peripheralDeviceIds } + // Add the requested filter + const selector: MongoQuery = {} + if (peripheralDeviceIds) selector._id = { $in: peripheralDeviceIds } - const projection = clone(peripheralDeviceProjection) - if (selector._id && token) { - // in this case, send the secretSettings: - delete projection.secretSettings + const projection = clone(peripheralDeviceProjection) + if (selector._id && token) { + // in this case, send the secretSettings: + delete projection.secretSettings + } + return PeripheralDevices.findWithCursor(selector, { + projection, + }) } - return PeripheralDevices.findWithCursor(selector, { - projection, - }) - } -) + ) -meteorPublish(CorelibPubSub.peripheralDevicesAndSubDevices, async function (studioId: StudioId) { - triggerWriteAccessBecauseNoCheckNecessary() + registry.publish(CorelibPubSub.peripheralDevicesAndSubDevices, async (_context, studioId: StudioId) => { + triggerWriteAccessBecauseNoCheckNecessary() - const selector: MongoQuery = { - 'studioAndConfigId.studioId': studioId, - } + const selector: MongoQuery = { + 'studioAndConfigId.studioId': studioId, + } - // TODO - this is not correctly reactive when changing the `studioId` property of a parent device - const parents = (await PeripheralDevices.findFetchAsync(selector, { projection: { _id: 1 } })) as Array< - Pick - > + // TODO - this is not correctly reactive when changing the `studioId` property of a parent device + const parents = (await PeripheralDevices.findFetchAsync(selector, { projection: { _id: 1 } })) as Array< + Pick + > - return PeripheralDevices.findWithCursor( - { - $or: [ - { - parentDeviceId: { $in: parents.map((i) => i._id) }, - }, - selector, - ], - }, - { - projection: peripheralDeviceProjection, + return PeripheralDevices.findWithCursor( + { + $or: [ + { + parentDeviceId: { $in: parents.map((i) => i._id) }, + }, + selector, + ], + }, + { + projection: peripheralDeviceProjection, + } + ) + }) + registry.publish( + PeripheralDevicePubSub.peripheralDeviceCommands, + async (context, deviceId: PeripheralDeviceId, token: string | undefined) => { + await checkAccessAndGetPeripheralDevice(deviceId, token, context) + + return PeripheralDeviceCommands.findWithCursor({ deviceId: deviceId }) } ) -}) -meteorPublish( - PeripheralDevicePubSub.peripheralDeviceCommands, - async function (deviceId: PeripheralDeviceId, token: string | undefined) { - await checkAccessAndGetPeripheralDevice(deviceId, token, this) - - return PeripheralDeviceCommands.findWithCursor({ deviceId: deviceId }) - } -) +} diff --git a/meteor/server/publications/peripheralDeviceForDevice.ts b/meteor/server/publications/peripheralDeviceForDevice.ts index 525dfc2d01b..1ce183d72df 100644 --- a/meteor/server/publications/peripheralDeviceForDevice.ts +++ b/meteor/server/publications/peripheralDeviceForDevice.ts @@ -1,12 +1,8 @@ import { PeripheralDevice, PeripheralDeviceCategory } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' import { PeripheralDeviceId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { PeripheralDevices, Studios } from '../collections' -import { - SetupObserversResult, - TriggerUpdate, - meteorCustomPublish, - setUpOptimizedObserverArray, -} from '../lib/customPublication' +import { SetupObserversResult, TriggerUpdate, setUpOptimizedObserverArray } from '../lib/customPublication' +import type { PublicationRegistry } from '../publicationRegistry' import { PeripheralDeviceForDevice } from '@sofie-automation/shared-lib/dist/core/model/peripheralDevice' import { ReadonlyDeep } from 'type-fest' import { ReactiveMongoObserverGroup } from './lib/observerGroup' @@ -209,28 +205,30 @@ async function manipulatePeripheralDevicePublicationData( return [convertPeripheralDeviceForGateway(peripheralDevice, studio)] } -meteorCustomPublish( - PeripheralDevicePubSub.peripheralDeviceForDevice, - PeripheralDevicePubSubCollectionsNames.peripheralDeviceForDevice, - async function (pub, deviceId: PeripheralDeviceId, token: string | undefined) { - check(deviceId, String) - - const peripheralDevice = await checkAccessAndGetPeripheralDevice(deviceId, token, this) - - const studioId = peripheralDevice.studioAndConfigId?.studioId - if (!studioId) return - - await setUpOptimizedObserverArray< - PeripheralDeviceForDevice, - PeripheralDeviceForDeviceArgs, - PeripheralDeviceForDeviceState, - PeripheralDeviceForDeviceUpdateProps - >( - `${PeripheralDevicePubSubCollectionsNames.peripheralDeviceForDevice}_${deviceId}`, - { deviceId }, - setupPeripheralDevicePublicationObservers, - manipulatePeripheralDevicePublicationData, - pub - ) - } -) +export function registerPeripheralDeviceForDevicePublications(registry: PublicationRegistry): void { + registry.customPublish( + PeripheralDevicePubSub.peripheralDeviceForDevice, + PeripheralDevicePubSubCollectionsNames.peripheralDeviceForDevice, + async (context, pub, deviceId: PeripheralDeviceId, token: string | undefined) => { + check(deviceId, String) + + const peripheralDevice = await checkAccessAndGetPeripheralDevice(deviceId, token, context) + + const studioId = peripheralDevice.studioAndConfigId?.studioId + if (!studioId) return + + await setUpOptimizedObserverArray< + PeripheralDeviceForDevice, + PeripheralDeviceForDeviceArgs, + PeripheralDeviceForDeviceState, + PeripheralDeviceForDeviceUpdateProps + >( + `${PeripheralDevicePubSubCollectionsNames.peripheralDeviceForDevice}_${deviceId}`, + { deviceId }, + setupPeripheralDevicePublicationObservers, + manipulatePeripheralDevicePublicationData, + pub + ) + } + ) +} diff --git a/meteor/server/publications/pieceContentStatusUI/bucket/publication.ts b/meteor/server/publications/pieceContentStatusUI/bucket/publication.ts index 2ca9bcc7cb2..88322d26539 100644 --- a/meteor/server/publications/pieceContentStatusUI/bucket/publication.ts +++ b/meteor/server/publications/pieceContentStatusUI/bucket/publication.ts @@ -17,11 +17,11 @@ import { literal } from '@sofie-automation/corelib/dist/lib' import { protectString } from '@sofie-automation/corelib/dist/protectedString' import { CustomPublishCollection, - meteorCustomPublish, setUpCollectionOptimizedObserver, TriggerUpdate, SetupObserversResult, } from '../../../lib/customPublication' +import type { PublicationRegistry } from '../../../publicationRegistry' import { BucketContentCache, createReactiveContentCache } from './bucketContentCache' import { Bucket } from '@sofie-automation/corelib/dist/dataModel/Bucket' import { @@ -268,27 +268,29 @@ async function manipulateUIBucketContentStatusesPublicationData( ) } -meteorCustomPublish( - MeteorPubSub.uiBucketContentStatuses, - CustomCollectionName.UIBucketContentStatuses, - async function (pub, studioId: StudioId, bucketId: BucketId) { - check(studioId, String) - check(bucketId, String) - - triggerWriteAccessBecauseNoCheckNecessary() - - await setUpCollectionOptimizedObserver< - UIBucketContentStatus, - UIBucketContentStatusesArgs, - UIBucketContentStatusesState, - UIBucketContentStatusesUpdateProps - >( - `pub_${MeteorPubSub.uiBucketContentStatuses}_${studioId}_${bucketId}`, - { studioId, bucketId }, - setupUIBucketContentStatusesPublicationObservers, - manipulateUIBucketContentStatusesPublicationData, - pub, - 100 - ) - } -) +export function registerBucketContentStatusUIPublications(registry: PublicationRegistry): void { + registry.customPublish( + MeteorPubSub.uiBucketContentStatuses, + CustomCollectionName.UIBucketContentStatuses, + async (_context, pub, studioId: StudioId, bucketId: BucketId) => { + check(studioId, String) + check(bucketId, String) + + triggerWriteAccessBecauseNoCheckNecessary() + + await setUpCollectionOptimizedObserver< + UIBucketContentStatus, + UIBucketContentStatusesArgs, + UIBucketContentStatusesState, + UIBucketContentStatusesUpdateProps + >( + `pub_${MeteorPubSub.uiBucketContentStatuses}_${studioId}_${bucketId}`, + { studioId, bucketId }, + setupUIBucketContentStatusesPublicationObservers, + manipulateUIBucketContentStatusesPublicationData, + pub, + 100 + ) + } + ) +} diff --git a/meteor/server/publications/pieceContentStatusUI/rundown/publication.ts b/meteor/server/publications/pieceContentStatusUI/rundown/publication.ts index f2710d50b3a..e8896095900 100644 --- a/meteor/server/publications/pieceContentStatusUI/rundown/publication.ts +++ b/meteor/server/publications/pieceContentStatusUI/rundown/publication.ts @@ -27,11 +27,11 @@ import { literal } from '@sofie-automation/corelib/dist/lib' import { protectString } from '@sofie-automation/corelib/dist/protectedString' import { CustomPublishCollection, - meteorCustomPublish, setUpCollectionOptimizedObserver, SetupObserversResult, TriggerUpdate, } from '../../../lib/customPublication' +import type { PublicationRegistry } from '../../../publicationRegistry' import { logger } from '../../../logging' import { ContentCache, PartInstanceFields, createReactiveContentCache } from './reactiveContentCache' import { RundownContentObserver } from './rundownContentObserver' @@ -500,31 +500,33 @@ function updatePartAndSegmentInfoForExistingDocs( }) } -meteorCustomPublish( - CorelibPubSub.uiPieceContentStatuses, - CustomCollectionName.UIPieceContentStatuses, - async function (pub, rundownPlaylistId: RundownPlaylistId | null) { - check(rundownPlaylistId, Match.Maybe(String)) +export function registerRundownContentStatusUIPublications(registry: PublicationRegistry): void { + registry.customPublish( + CorelibPubSub.uiPieceContentStatuses, + CustomCollectionName.UIPieceContentStatuses, + async (_context, pub, rundownPlaylistId: RundownPlaylistId | null) => { + check(rundownPlaylistId, Match.Maybe(String)) - triggerWriteAccessBecauseNoCheckNecessary() + triggerWriteAccessBecauseNoCheckNecessary() - if (!rundownPlaylistId) { - logger.info(`Pub.${CustomCollectionName.UIPieceContentStatuses}: Not playlistId`) - return - } + if (!rundownPlaylistId) { + logger.info(`Pub.${CustomCollectionName.UIPieceContentStatuses}: Not playlistId`) + return + } - await setUpCollectionOptimizedObserver< - UIPieceContentStatus, - UIPieceContentStatusesArgs, - UIPieceContentStatusesState, - UIPieceContentStatusesUpdateProps - >( - `pub_${CorelibPubSub.uiPieceContentStatuses}_${rundownPlaylistId}`, - { rundownPlaylistId }, - setupUIPieceContentStatusesPublicationObservers, - manipulateUIPieceContentStatusesPublicationData, - pub, - 100 - ) - } -) + await setUpCollectionOptimizedObserver< + UIPieceContentStatus, + UIPieceContentStatusesArgs, + UIPieceContentStatusesState, + UIPieceContentStatusesUpdateProps + >( + `pub_${CorelibPubSub.uiPieceContentStatuses}_${rundownPlaylistId}`, + { rundownPlaylistId }, + setupUIPieceContentStatusesPublicationObservers, + manipulateUIPieceContentStatusesPublicationData, + pub, + 100 + ) + } + ) +} diff --git a/meteor/server/publications/rundown.ts b/meteor/server/publications/rundown.ts index b83748913c9..d8e3468574c 100644 --- a/meteor/server/publications/rundown.ts +++ b/meteor/server/publications/rundown.ts @@ -1,5 +1,4 @@ import { Meteor } from 'meteor/meteor' -import { meteorPublish } from './lib/lib' import { MeteorPubSub } from '@sofie-automation/meteor-lib/dist/api/pubsub' import { MongoFieldSpecifierZeroes, MongoQuery } from '@sofie-automation/corelib/dist/mongo' import { AdLibPiece } from '@sofie-automation/corelib/dist/dataModel/AdLibPiece' @@ -45,506 +44,528 @@ import { AdLibAction } from '@sofie-automation/corelib/dist/dataModel/AdlibActio import { PieceLifespan } from '@sofie-automation/blueprints-integration' import { triggerWriteAccessBecauseNoCheckNecessary } from '../security/securityVerify' import { checkAccessAndGetPeripheralDevice } from '../security/check' +import type { PublicationRegistry } from '../publicationRegistry' -meteorPublish( - PeripheralDevicePubSub.rundownsForDevice, - async function (deviceId: PeripheralDeviceId, token: string | undefined) { - check(deviceId, String) - check(token, String) +const piecesSubFields: MongoFieldSpecifierZeroes = { + privateData: 0, + timelineObjectsString: 0, +} - // Future: this should be reactive to studioId changes, but this matches how the other *ForDevice publications behave +const adlibPiecesSubFields: MongoFieldSpecifierZeroes = { + privateData: 0, + timelineObjectsString: 0, +} - const peripheralDevice = await checkAccessAndGetPeripheralDevice(deviceId, token, this) +const pieceInstanceFields: MongoFieldSpecifierZeroes = { + // @ts-expect-error Mongo typings aren't clever enough yet + 'piece.privateData': 0, + 'piece.timelineObjectsString': 0, +} - // No studio, then no rundowns - const studioId = peripheralDevice.studioAndConfigId?.studioId - if (!studioId) return null +const adlibActionSubFields: MongoFieldSpecifierZeroes = { + privateData: 0, +} - return Rundowns.findWithCursor( - { - studioId: studioId, - }, - { - projection: { - privateData: 0, - externalEventSubscriptions: 0, - }, - } - ) - } -) +export function registerRundownPublications(registry: PublicationRegistry): void { + registry.publish( + PeripheralDevicePubSub.rundownsForDevice, + async (context, deviceId: PeripheralDeviceId, token: string | undefined) => { + check(deviceId, String) + check(token, String) -meteorPublish( - CorelibPubSub.rundownsInPlaylists, - async function (playlistIds: RundownPlaylistId[], _token: string | undefined) { - check(playlistIds, Array) + // Future: this should be reactive to studioId changes, but this matches how the other *ForDevice publications behave - triggerWriteAccessBecauseNoCheckNecessary() + const peripheralDevice = await checkAccessAndGetPeripheralDevice(deviceId, token, context) - // If values were provided, they must have values - if (playlistIds.length === 0) return null + // No studio, then no rundowns + const studioId = peripheralDevice.studioAndConfigId?.studioId + if (!studioId) return null - const selector: MongoQuery = { - playlistId: { $in: playlistIds }, + return Rundowns.findWithCursor( + { + studioId: studioId, + }, + { + projection: { + privateData: 0, + externalEventSubscriptions: 0, + }, + } + ) } + ) - const modifier: FindOptions = { - projection: { - privateData: 0, - externalEventSubscriptions: 0, - }, - } + registry.publish( + CorelibPubSub.rundownsInPlaylists, + async (_context, playlistIds: RundownPlaylistId[], _token: string | undefined) => { + check(playlistIds, Array) - return Rundowns.findWithCursor(selector, modifier) - } -) -meteorPublish( - CorelibPubSub.rundownsWithShowStyleBases, - async function (showStyleBaseIds: ShowStyleBaseId[], _token: string | undefined) { - check(showStyleBaseIds, Array) + triggerWriteAccessBecauseNoCheckNecessary() - triggerWriteAccessBecauseNoCheckNecessary() + // If values were provided, they must have values + if (playlistIds.length === 0) return null - if (showStyleBaseIds.length === 0) return null + const selector: MongoQuery = { + playlistId: { $in: playlistIds }, + } - const selector: MongoQuery = { - showStyleBaseId: { $in: showStyleBaseIds }, - } + const modifier: FindOptions = { + projection: { + privateData: 0, + externalEventSubscriptions: 0, + }, + } - const modifier: FindOptions = { - projection: { - privateData: 0, - externalEventSubscriptions: 0, - }, + return Rundowns.findWithCursor(selector, modifier) } + ) + registry.publish( + CorelibPubSub.rundownsWithShowStyleBases, + async (_context, showStyleBaseIds: ShowStyleBaseId[], _token: string | undefined) => { + check(showStyleBaseIds, Array) - return Rundowns.findWithCursor(selector, modifier) - } -) + triggerWriteAccessBecauseNoCheckNecessary() -meteorPublish( - CorelibPubSub.segments, - async function (rundownIds: RundownId[], filter: { omitHidden?: boolean } | undefined, _token: string | undefined) { - check(rundownIds, Array) + if (showStyleBaseIds.length === 0) return null - triggerWriteAccessBecauseNoCheckNecessary() + const selector: MongoQuery = { + showStyleBaseId: { $in: showStyleBaseIds }, + } - if (rundownIds.length === 0) return null + const modifier: FindOptions = { + projection: { + privateData: 0, + externalEventSubscriptions: 0, + }, + } - const selector: MongoQuery = { - rundownId: { $in: rundownIds }, + return Rundowns.findWithCursor(selector, modifier) } - if (filter?.omitHidden) selector.isHidden = { $ne: true } + ) - return Segments.findWithCursor(selector, { - projection: { - privateData: 0, - }, - }) - } -) + registry.publish( + CorelibPubSub.segments, + async ( + _context, + rundownIds: RundownId[], + filter: { omitHidden?: boolean } | undefined, + _token: string | undefined + ) => { + check(rundownIds, Array) -meteorPublish( - CorelibPubSub.parts, - async function (rundownIds: RundownId[], segmentIds: SegmentId[] | null, _token: string | undefined) { - check(rundownIds, Array) - check(segmentIds, Match.Maybe(Array)) + triggerWriteAccessBecauseNoCheckNecessary() - triggerWriteAccessBecauseNoCheckNecessary() + if (rundownIds.length === 0) return null - if (rundownIds.length === 0) return null - if (segmentIds && segmentIds.length === 0) return null + const selector: MongoQuery = { + rundownId: { $in: rundownIds }, + } + if (filter?.omitHidden) selector.isHidden = { $ne: true } - const modifier: FindOptions = { - projection: { - privateData: 0, - }, + return Segments.findWithCursor(selector, { + projection: { + privateData: 0, + }, + }) } + ) - const selector: MongoQuery = { - rundownId: { $in: rundownIds }, - reset: { $ne: true }, - } - if (segmentIds) selector.segmentId = { $in: segmentIds } - - return Parts.findWithCursor(selector, modifier) - } -) -meteorPublish( - CorelibPubSub.partInstances, - async function ( - rundownIds: RundownId[], - playlistActivationId: RundownPlaylistActivationId | null, - _token: string | undefined - ) { - check(rundownIds, Array) - check(playlistActivationId, Match.Maybe(String)) + registry.publish( + CorelibPubSub.parts, + async (_context, rundownIds: RundownId[], segmentIds: SegmentId[] | null, _token: string | undefined) => { + check(rundownIds, Array) + check(segmentIds, Match.Maybe(Array)) - triggerWriteAccessBecauseNoCheckNecessary() + triggerWriteAccessBecauseNoCheckNecessary() - if (rundownIds.length === 0 || !playlistActivationId) return null + if (rundownIds.length === 0) return null + if (segmentIds && segmentIds.length === 0) return null - const modifier: FindOptions = { - projection: { - // @ts-expect-error Mongo typings aren't clever enough yet - 'part.privateData': 0, - }, - } + const modifier: FindOptions = { + projection: { + privateData: 0, + }, + } - const selector: MongoQuery = { - rundownId: { $in: rundownIds }, - reset: { $ne: true }, - } - if (playlistActivationId) selector.playlistActivationId = playlistActivationId - - return PartInstances.findWithCursor(selector, modifier) - } -) -meteorPublish( - CorelibPubSub.partInstancesSimple, - async function ( - rundownIds: RundownId[], - playlistActivationId: RundownPlaylistActivationId | null, - _token: string | undefined - ) { - check(rundownIds, Array) + const selector: MongoQuery = { + rundownId: { $in: rundownIds }, + reset: { $ne: true }, + } + if (segmentIds) selector.segmentId = { $in: segmentIds } - triggerWriteAccessBecauseNoCheckNecessary() + return Parts.findWithCursor(selector, modifier) + } + ) + registry.publish( + CorelibPubSub.partInstances, + async ( + _context, + rundownIds: RundownId[], + playlistActivationId: RundownPlaylistActivationId | null, + _token: string | undefined + ) => { + check(rundownIds, Array) + check(playlistActivationId, Match.Maybe(String)) + + triggerWriteAccessBecauseNoCheckNecessary() + + if (rundownIds.length === 0 || !playlistActivationId) return null + + const modifier: FindOptions = { + projection: { + // @ts-expect-error Mongo typings aren't clever enough yet + 'part.privateData': 0, + }, + } - if (rundownIds.length === 0) return null + const selector: MongoQuery = { + rundownId: { $in: rundownIds }, + reset: { $ne: true }, + } + if (playlistActivationId) selector.playlistActivationId = playlistActivationId - const selector: MongoQuery = { - rundownId: { $in: rundownIds }, - // Enforce only not-reset - reset: { $ne: true }, + return PartInstances.findWithCursor(selector, modifier) + } + ) + registry.publish( + CorelibPubSub.partInstancesSimple, + async ( + _context, + rundownIds: RundownId[], + playlistActivationId: RundownPlaylistActivationId | null, + _token: string | undefined + ) => { + check(rundownIds, Array) + check(playlistActivationId, Match.Maybe(String)) + + triggerWriteAccessBecauseNoCheckNecessary() + + if (rundownIds.length === 0) return null + + const selector: MongoQuery = { + rundownId: { $in: rundownIds }, + // Enforce only not-reset + reset: { $ne: true }, + } + if (playlistActivationId) selector.playlistActivationId = playlistActivationId + + return PartInstances.findWithCursor(selector, { + projection: literal>({ + // @ts-expect-error Mongo typings aren't clever enough yet + 'part.privateData': 0, + isTaken: 0, + timings: 0, + }), + }) } - if (playlistActivationId) selector.playlistActivationId = playlistActivationId - - return PartInstances.findWithCursor(selector, { - projection: literal>({ - // @ts-expect-error Mongo typings aren't clever enough yet - 'part.privateData': 0, - isTaken: 0, - timings: 0, - }), - }) - } -) + ) -const piecesSubFields: MongoFieldSpecifierZeroes = { - privateData: 0, - timelineObjectsString: 0, -} + registry.publish( + CorelibPubSub.pieces, + async (_context, rundownIds: RundownId[], partIds: PartId[] | null, _token: string | undefined) => { + check(rundownIds, Array) + check(partIds, Match.Maybe(Array)) -meteorPublish( - CorelibPubSub.pieces, - async function (rundownIds: RundownId[], partIds: PartId[] | null, _token: string | undefined) { - check(rundownIds, Array) - check(partIds, Match.Maybe(Array)) + triggerWriteAccessBecauseNoCheckNecessary() - triggerWriteAccessBecauseNoCheckNecessary() + // If values were provided, they must have values + if (partIds && partIds.length === 0) return null - // If values were provided, they must have values - if (partIds && partIds.length === 0) return null + const selector: MongoQuery = { + startRundownId: { $in: rundownIds }, + } + if (partIds) selector.startPartId = { $in: partIds } - const selector: MongoQuery = { - startRundownId: { $in: rundownIds }, + return Pieces.findWithCursor(selector, { + projection: piecesSubFields, + }) } - if (partIds) selector.startPartId = { $in: partIds } - - return Pieces.findWithCursor(selector, { - projection: piecesSubFields, - }) - } -) - -meteorPublish( - CorelibPubSub.piecesInfiniteStartingBefore, - async function ( - thisRundownId: RundownId, - segmentsIdsBefore: SegmentId[], - rundownIdsBefore: RundownId[], - _token: string | undefined - ) { - triggerWriteAccessBecauseNoCheckNecessary() + ) - const selector: MongoQuery = { - invalid: { - $ne: true, - }, - $or: [ - // same rundown, and previous segment - { - startRundownId: thisRundownId, - startSegmentId: { $in: segmentsIdsBefore }, - lifespan: { - $in: [ - PieceLifespan.OutOnRundownEnd, - PieceLifespan.OutOnRundownChange, - PieceLifespan.OutOnShowStyleEnd, - ], - }, + registry.publish( + CorelibPubSub.piecesInfiniteStartingBefore, + async ( + _context, + thisRundownId: RundownId, + segmentsIdsBefore: SegmentId[], + rundownIdsBefore: RundownId[], + _token: string | undefined + ) => { + check(thisRundownId, String) + + triggerWriteAccessBecauseNoCheckNecessary() + + const selector: MongoQuery = { + invalid: { + $ne: true, }, - // Previous rundown - { - startRundownId: { $in: rundownIdsBefore }, - lifespan: { - $in: [PieceLifespan.OutOnShowStyleEnd], + $or: [ + // same rundown, and previous segment + { + startRundownId: thisRundownId, + startSegmentId: { $in: segmentsIdsBefore }, + lifespan: { + $in: [ + PieceLifespan.OutOnRundownEnd, + PieceLifespan.OutOnRundownChange, + PieceLifespan.OutOnShowStyleEnd, + ], + }, }, - }, - ], - } - - return Pieces.findWithCursor(selector, { - projection: piecesSubFields, - }) - } -) + // Previous rundown + { + startRundownId: { $in: rundownIdsBefore }, + lifespan: { + $in: [PieceLifespan.OutOnShowStyleEnd], + }, + }, + ], + } -const adlibPiecesSubFields: MongoFieldSpecifierZeroes = { - privateData: 0, - timelineObjectsString: 0, -} + return Pieces.findWithCursor(selector, { + projection: piecesSubFields, + }) + } + ) -meteorPublish(CorelibPubSub.adLibPieces, async function (rundownIds: RundownId[], _token: string | undefined) { - check(rundownIds, Array) + registry.publish( + CorelibPubSub.adLibPieces, + async (_context, rundownIds: RundownId[], _token: string | undefined) => { + check(rundownIds, Array) - triggerWriteAccessBecauseNoCheckNecessary() + triggerWriteAccessBecauseNoCheckNecessary() - if (rundownIds.length === 0) return null + if (rundownIds.length === 0) return null - const selector: MongoQuery = { - rundownId: { $in: rundownIds }, - } + const selector: MongoQuery = { + rundownId: { $in: rundownIds }, + } - return AdLibPieces.findWithCursor(selector, { - projection: adlibPiecesSubFields, - }) -}) -meteorPublish(MeteorPubSub.adLibPiecesForPart, async function (partId: PartId, sourceLayerIds: string[]) { - check(partId, String) - check(sourceLayerIds, Array) - - triggerWriteAccessBecauseNoCheckNecessary() - - return AdLibPieces.findWithCursor( - { - partId, - sourceLayerId: { $in: sourceLayerIds }, - }, - { - projection: adlibPiecesSubFields, + return AdLibPieces.findWithCursor(selector, { + projection: adlibPiecesSubFields, + }) } ) -}) - -const pieceInstanceFields: MongoFieldSpecifierZeroes = { - // @ts-expect-error Mongo typings aren't clever enough yet - 'piece.privateData': 0, - 'piece.timelineObjectsString': 0, -} - -meteorPublish( - CorelibPubSub.pieceInstances, - async function ( - rundownIds: RundownId[], - partInstanceIds: PartInstanceId[] | null, - filter: - | { - onlyPlayingAdlibsOrWithTags?: boolean - } - | undefined, - _token: string | undefined - ) { - check(rundownIds, Array) - check(partInstanceIds, Match.Maybe(Array)) + registry.publish(MeteorPubSub.adLibPiecesForPart, async (_context, partId: PartId, sourceLayerIds: string[]) => { + check(partId, String) + check(sourceLayerIds, Array) triggerWriteAccessBecauseNoCheckNecessary() - // If values were provided, they must have values - if (rundownIds.length === 0) return null - if (partInstanceIds && partInstanceIds.length === 0) return null - - const selector: MongoQuery = { - rundownId: { $in: rundownIds }, - - // Enforce only not-reset - reset: { $ne: true }, - } - if (partInstanceIds) selector.partInstanceId = { $in: partInstanceIds } + return AdLibPieces.findWithCursor( + { + partId, + sourceLayerId: { $in: sourceLayerIds }, + }, + { + projection: adlibPiecesSubFields, + } + ) + }) - if (filter?.onlyPlayingAdlibsOrWithTags) { - selector.plannedStartedPlayback = { - $exists: true, + registry.publish( + CorelibPubSub.pieceInstances, + async ( + _context, + rundownIds: RundownId[], + partInstanceIds: PartInstanceId[] | null, + filter: + | { + onlyPlayingAdlibsOrWithTags?: boolean + } + | undefined, + _token: string | undefined + ) => { + check(rundownIds, Array) + check(partInstanceIds, Match.Maybe(Array)) + + triggerWriteAccessBecauseNoCheckNecessary() + + // If values were provided, they must have values + if (rundownIds.length === 0) return null + if (partInstanceIds && partInstanceIds.length === 0) return null + + const selector: MongoQuery = { + rundownId: { $in: rundownIds }, + + // Enforce only not-reset + reset: { $ne: true }, } - selector.$and = [ - { - $or: [ - { - adLibSourceId: { - $exists: true, + if (partInstanceIds) selector.partInstanceId = { $in: partInstanceIds } + + if (filter?.onlyPlayingAdlibsOrWithTags) { + selector.plannedStartedPlayback = { + $exists: true, + } + selector.$and = [ + { + $or: [ + { + adLibSourceId: { + $exists: true, + }, }, - }, - { - 'piece.tags': { - $exists: true, + { + 'piece.tags': { + $exists: true, + }, }, - }, - ], - }, - { - $or: [ - { - plannedStoppedPlayback: { - $eq: 0, + ], + }, + { + $or: [ + { + plannedStoppedPlayback: { + $eq: 0, + }, }, - }, - { - plannedStoppedPlayback: { - $exists: false, + { + plannedStoppedPlayback: { + $exists: false, + }, }, - }, - ], - }, - ] + ], + }, + ] + } + + return PieceInstances.findWithCursor(selector, { + projection: pieceInstanceFields, + }) } + ) - return PieceInstances.findWithCursor(selector, { - projection: pieceInstanceFields, - }) - } -) - -meteorPublish( - CorelibPubSub.pieceInstancesSimple, - async function ( - rundownIds: RundownId[], - playlistActivationId: RundownPlaylistActivationId | null, - _token: string | undefined - ) { - check(rundownIds, Array) + registry.publish( + CorelibPubSub.pieceInstancesSimple, + async ( + _context, + rundownIds: RundownId[], + playlistActivationId: RundownPlaylistActivationId | null, + _token: string | undefined + ) => { + check(rundownIds, Array) - triggerWriteAccessBecauseNoCheckNecessary() + triggerWriteAccessBecauseNoCheckNecessary() - if (rundownIds.length === 0) return null + if (rundownIds.length === 0) return null - const selector: MongoQuery = { - rundownId: { $in: rundownIds }, - // Enforce only not-reset - reset: { $ne: true }, + const selector: MongoQuery = { + rundownId: { $in: rundownIds }, + // Enforce only not-reset + reset: { $ne: true }, + } + if (playlistActivationId) selector.playlistActivationId = playlistActivationId + + return PieceInstances.findWithCursor(selector, { + projection: literal>({ + ...pieceInstanceFields, + plannedStartedPlayback: 0, + plannedStoppedPlayback: 0, + }), + }) } - if (playlistActivationId) selector.playlistActivationId = playlistActivationId - - return PieceInstances.findWithCursor(selector, { - projection: literal>({ - ...pieceInstanceFields, - plannedStartedPlayback: 0, - plannedStoppedPlayback: 0, - }), - }) - } -) - -meteorPublish( - PeripheralDevicePubSub.expectedPlayoutItemsForDevice, - async function (deviceId: PeripheralDeviceId, token: string | undefined) { - check(deviceId, String) - - const peripheralDevice = await checkAccessAndGetPeripheralDevice(deviceId, token, this) - - const studioId = peripheralDevice.studioAndConfigId?.studioId - if (!studioId) return null - - return ExpectedPlayoutItems.findWithCursor({ studioId }) - } -) -// Note: this publication is for dev purposes only: -meteorPublish( - CorelibPubSub.ingestDataCache, - async function (selector: MongoQuery, _token: string | undefined) { - triggerWriteAccessBecauseNoCheckNecessary() + ) - if (!selector) throw new Meteor.Error(400, 'selector argument missing') - const modifier: FindOptions = { - projection: {}, - } + registry.publish( + PeripheralDevicePubSub.expectedPlayoutItemsForDevice, + async (context, deviceId: PeripheralDeviceId, token: string | undefined) => { + check(deviceId, String) - return NrcsIngestDataCache.findWithCursor(selector, modifier) - } -) -meteorPublish( - CorelibPubSub.rundownBaselineAdLibPieces, - async function (rundownIds: RundownId[], _token: string | undefined) { - check(rundownIds, Array) + const peripheralDevice = await checkAccessAndGetPeripheralDevice(deviceId, token, context) - triggerWriteAccessBecauseNoCheckNecessary() + const studioId = peripheralDevice.studioAndConfigId?.studioId + if (!studioId) return null - if (rundownIds.length === 0) return null + return ExpectedPlayoutItems.findWithCursor({ studioId }) + } + ) + // Note: this publication is for dev purposes only: + registry.publish( + CorelibPubSub.ingestDataCache, + async (_context, selector: MongoQuery, _token: string | undefined) => { + triggerWriteAccessBecauseNoCheckNecessary() + + if (!selector) throw new Meteor.Error(400, 'selector argument missing') + const modifier: FindOptions = { + projection: {}, + } - const selector: MongoQuery = { - rundownId: { $in: rundownIds }, + return NrcsIngestDataCache.findWithCursor(selector, modifier) } + ) + registry.publish( + CorelibPubSub.rundownBaselineAdLibPieces, + async (_context, rundownIds: RundownId[], _token: string | undefined) => { + check(rundownIds, Array) - return RundownBaselineAdLibPieces.findWithCursor(selector, { - projection: { - timelineObjectsString: 0, - privateData: 0, - }, - }) - } -) + triggerWriteAccessBecauseNoCheckNecessary() -const adlibActionSubFields: MongoFieldSpecifierZeroes = { - privateData: 0, -} + if (rundownIds.length === 0) return null + + const selector: MongoQuery = { + rundownId: { $in: rundownIds }, + } + + return RundownBaselineAdLibPieces.findWithCursor(selector, { + projection: { + timelineObjectsString: 0, + privateData: 0, + }, + }) + } + ) -meteorPublish(CorelibPubSub.adLibActions, async function (rundownIds: RundownId[], _token: string | undefined) { - check(rundownIds, Array) + registry.publish( + CorelibPubSub.adLibActions, + async (_context, rundownIds: RundownId[], _token: string | undefined) => { + check(rundownIds, Array) - triggerWriteAccessBecauseNoCheckNecessary() + triggerWriteAccessBecauseNoCheckNecessary() - if (rundownIds.length === 0) return null + if (rundownIds.length === 0) return null - const selector: MongoQuery = { - rundownId: { $in: rundownIds }, - } + const selector: MongoQuery = { + rundownId: { $in: rundownIds }, + } - return AdLibActions.findWithCursor(selector, { - projection: adlibActionSubFields, - }) -}) -meteorPublish(MeteorPubSub.adLibActionsForPart, async function (partId: PartId, sourceLayerIds: string[]) { - check(partId, String) - check(sourceLayerIds, Array) - - triggerWriteAccessBecauseNoCheckNecessary() - - return AdLibActions.findWithCursor( - { - partId, - 'display.sourceLayerId': { $in: sourceLayerIds }, - }, - { - projection: adlibActionSubFields, + return AdLibActions.findWithCursor(selector, { + projection: adlibActionSubFields, + }) } ) -}) - -meteorPublish( - CorelibPubSub.rundownBaselineAdLibActions, - async function (rundownIds: RundownId[], _token: string | undefined) { - check(rundownIds, Array) + registry.publish(MeteorPubSub.adLibActionsForPart, async (_context, partId: PartId, sourceLayerIds: string[]) => { + check(partId, String) + check(sourceLayerIds, Array) triggerWriteAccessBecauseNoCheckNecessary() - if (rundownIds.length === 0) return null + return AdLibActions.findWithCursor( + { + partId, + 'display.sourceLayerId': { $in: sourceLayerIds }, + }, + { + projection: adlibActionSubFields, + } + ) + }) + + registry.publish( + CorelibPubSub.rundownBaselineAdLibActions, + async (_context, rundownIds: RundownId[], _token: string | undefined) => { + check(rundownIds, Array) - const selector: MongoQuery = { - rundownId: { $in: rundownIds }, - } + triggerWriteAccessBecauseNoCheckNecessary() + + if (rundownIds.length === 0) return null - return RundownBaselineAdLibActions.findWithCursor(selector, { - projection: adlibActionSubFields, - }) - } -) + const selector: MongoQuery = { + rundownId: { $in: rundownIds }, + } + + return RundownBaselineAdLibActions.findWithCursor(selector, { + projection: adlibActionSubFields, + }) + } + ) +} diff --git a/meteor/server/publications/rundownPlaylist.ts b/meteor/server/publications/rundownPlaylist.ts index 582c104934e..1a58a39a16e 100644 --- a/meteor/server/publications/rundownPlaylist.ts +++ b/meteor/server/publications/rundownPlaylist.ts @@ -1,4 +1,3 @@ -import { meteorPublish } from './lib/lib' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist' import { RundownPlaylists } from '../collections' import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' @@ -7,44 +6,48 @@ import { RundownPlaylistId, StudioId } from '@sofie-automation/corelib/dist/data import { check, Match } from '../lib/check' import { MongoQuery } from '@sofie-automation/corelib/dist/mongo' import { triggerWriteAccessBecauseNoCheckNecessary } from '../security/securityVerify' - -meteorPublish( - CorelibPubSub.rundownPlaylists, - async function ( - rundownPlaylistIds: RundownPlaylistId[] | null, - studioIds: StudioId[] | null, - _token: string | undefined - ) { - check(rundownPlaylistIds, Match.Maybe(Array)) - check(studioIds, Match.Maybe(Array)) - +import type { PublicationRegistry } from '../publicationRegistry' + +export function registerRundownPlaylistPublications(registry: PublicationRegistry): void { + registry.publish( + CorelibPubSub.rundownPlaylists, + async ( + _context, + rundownPlaylistIds: RundownPlaylistId[] | null, + studioIds: StudioId[] | null, + _token: string | undefined + ) => { + check(rundownPlaylistIds, Match.Maybe(Array)) + check(studioIds, Match.Maybe(Array)) + + triggerWriteAccessBecauseNoCheckNecessary() + + // If values were provided, they must have values + if (rundownPlaylistIds && rundownPlaylistIds.length === 0) return null + if (studioIds && studioIds.length === 0) return null + + // Add the requested filter + const selector: MongoQuery = {} + if (rundownPlaylistIds) selector._id = { $in: rundownPlaylistIds } + if (studioIds) selector.studioId = { $in: studioIds } + + return RundownPlaylists.findWithCursor(selector) + } + ) + + registry.publish(MeteorPubSub.rundownPlaylistForStudio, async (_context, studioId: StudioId, isActive: boolean) => { triggerWriteAccessBecauseNoCheckNecessary() - // If values were provided, they must have values - if (rundownPlaylistIds && rundownPlaylistIds.length === 0) return null - if (studioIds && studioIds.length === 0) return null + const selector: MongoQuery = { + studioId, + } - // Add the requested filter - const selector: MongoQuery = {} - if (rundownPlaylistIds) selector._id = { $in: rundownPlaylistIds } - if (studioIds) selector.studioId = { $in: studioIds } + if (isActive) { + selector.activationId = { $exists: true } + } else { + selector.activationId = { $exists: false } + } return RundownPlaylists.findWithCursor(selector) - } -) - -meteorPublish(MeteorPubSub.rundownPlaylistForStudio, async function (studioId: StudioId, isActive: boolean) { - triggerWriteAccessBecauseNoCheckNecessary() - - const selector: MongoQuery = { - studioId, - } - - if (isActive) { - selector.activationId = { $exists: true } - } else { - selector.activationId = { $exists: false } - } - - return RundownPlaylists.findWithCursor(selector) -}) + }) +} diff --git a/meteor/server/publications/segmentPartNotesUI/publication.ts b/meteor/server/publications/segmentPartNotesUI/publication.ts index 131440a6f1a..8f9c1a92a79 100644 --- a/meteor/server/publications/segmentPartNotesUI/publication.ts +++ b/meteor/server/publications/segmentPartNotesUI/publication.ts @@ -11,7 +11,6 @@ import { groupByToMap, literal, normalizeArrayToMap } from '@sofie-automation/co import { protectString } from '@sofie-automation/corelib/dist/protectedString' import { CustomPublishCollection, - meteorCustomPublish, setUpCollectionOptimizedObserver, SetupObserversResult, TriggerUpdate, @@ -32,6 +31,7 @@ import { generateNotesForSegment } from './generateNotesForSegment' import { RundownPlaylists } from '../../collections' import { check, Match } from 'meteor/check' import { triggerWriteAccessBecauseNoCheckNecessary } from '../../security/securityVerify' +import type { PublicationRegistry } from '../../publicationRegistry' interface UISegmentPartNotesArgs { readonly playlistId: RundownPlaylistId @@ -214,31 +214,33 @@ function updateNotesForSegment( } } -meteorCustomPublish( - MeteorPubSub.uiSegmentPartNotes, - CustomCollectionName.UISegmentPartNotes, - async function (pub, playlistId: RundownPlaylistId | null) { - check(playlistId, Match.Maybe(String)) +export function registerSegmentPartNotesUIPublications(registry: PublicationRegistry): void { + registry.customPublish( + MeteorPubSub.uiSegmentPartNotes, + CustomCollectionName.UISegmentPartNotes, + async (_context, pub, playlistId: RundownPlaylistId | null) => { + check(playlistId, Match.Maybe(String)) - triggerWriteAccessBecauseNoCheckNecessary() + triggerWriteAccessBecauseNoCheckNecessary() - if (!playlistId) { - logger.info(`Pub.${CustomCollectionName.UISegmentPartNotes}: Not playlistId`) - return - } + if (!playlistId) { + logger.info(`Pub.${CustomCollectionName.UISegmentPartNotes}: Not playlistId`) + return + } - await setUpCollectionOptimizedObserver< - UISegmentPartNote, - UISegmentPartNotesArgs, - UISegmentPartNotesState, - UISegmentPartNotesUpdateProps - >( - `pub_${MeteorPubSub.uiSegmentPartNotes}_${playlistId}`, - { playlistId }, - setupUISegmentPartNotesPublicationObservers, - manipulateUISegmentPartNotesPublicationData, - pub, - 100 - ) - } -) + await setUpCollectionOptimizedObserver< + UISegmentPartNote, + UISegmentPartNotesArgs, + UISegmentPartNotesState, + UISegmentPartNotesUpdateProps + >( + `pub_${MeteorPubSub.uiSegmentPartNotes}_${playlistId}`, + { playlistId }, + setupUISegmentPartNotesPublicationObservers, + manipulateUISegmentPartNotesPublicationData, + pub, + 100 + ) + } + ) +} diff --git a/meteor/server/publications/showStyle.ts b/meteor/server/publications/showStyle.ts index 94f58aaabce..d4168ac2965 100644 --- a/meteor/server/publications/showStyle.ts +++ b/meteor/server/publications/showStyle.ts @@ -1,4 +1,3 @@ -import { meteorPublish } from './lib/lib' import { MeteorPubSub } from '@sofie-automation/meteor-lib/dist/api/pubsub' import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' import { DBShowStyleVariant } from '@sofie-automation/corelib/dist/dataModel/ShowStyleVariant' @@ -10,88 +9,92 @@ import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' import { ShowStyleBaseId, ShowStyleVariantId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { check, Match } from '../lib/check' import { triggerWriteAccessBecauseNoCheckNecessary } from '../security/securityVerify' - -meteorPublish( - CorelibPubSub.showStyleBases, - async function (showStyleBaseIds: ShowStyleBaseId[] | null, _token: string | undefined) { - check(showStyleBaseIds, Match.Maybe(Array)) - - triggerWriteAccessBecauseNoCheckNecessary() - - // If values were provided, they must have values - if (showStyleBaseIds && showStyleBaseIds.length === 0) return null - - // Add the requested filter - const selector: MongoQuery = {} - if (showStyleBaseIds) selector._id = { $in: showStyleBaseIds } - - return ShowStyleBases.findWithCursor(selector) - } -) - -meteorPublish( - CorelibPubSub.showStyleVariants, - async function ( - showStyleBaseIds: ShowStyleBaseId[] | null, - showStyleVariantIds: ShowStyleVariantId[] | null, - _token: string | undefined - ) { - check(showStyleBaseIds, Match.Maybe(Array)) - check(showStyleVariantIds, Match.Maybe(Array)) - - triggerWriteAccessBecauseNoCheckNecessary() - - // If values were provided, they must have values - if (showStyleBaseIds && showStyleBaseIds.length === 0) return null - if (showStyleVariantIds && showStyleVariantIds.length === 0) return null - - // Add the requested filter - const selector: MongoQuery = {} - if (showStyleBaseIds) selector.showStyleBaseId = { $in: showStyleBaseIds } - if (showStyleVariantIds) selector._id = { $in: showStyleVariantIds } - - return ShowStyleVariants.findWithCursor(selector) - } -) - -meteorPublish( - MeteorPubSub.rundownLayouts, - async function (showStyleBaseIds: ShowStyleBaseId[] | null, _token: string | undefined) { - check(showStyleBaseIds, Match.Maybe(Array)) - - triggerWriteAccessBecauseNoCheckNecessary() - - // If values were provided, they must have values - if (showStyleBaseIds && showStyleBaseIds.length === 0) return null - - const selector: MongoQuery = {} - if (showStyleBaseIds) selector.showStyleBaseId = { $in: showStyleBaseIds } - - return RundownLayouts.findWithCursor(selector) - } -) - -meteorPublish( - MeteorPubSub.triggeredActions, - async function (showStyleBaseIds: ShowStyleBaseId[] | null, _token: string | undefined) { - check(showStyleBaseIds, Match.Maybe(Array)) - - triggerWriteAccessBecauseNoCheckNecessary() - - const selector: MongoQuery = - showStyleBaseIds && showStyleBaseIds.length > 0 - ? { - $or: [ - { - showStyleBaseId: null, - }, - { - showStyleBaseId: { $in: showStyleBaseIds }, - }, - ], - } - : { showStyleBaseId: null } - - return TriggeredActions.findWithCursor(selector) - } -) +import type { PublicationRegistry } from '../publicationRegistry' + +export function registerShowStylePublications(registry: PublicationRegistry): void { + registry.publish( + CorelibPubSub.showStyleBases, + async (_context, showStyleBaseIds: ShowStyleBaseId[] | null, _token: string | undefined) => { + check(showStyleBaseIds, Match.Maybe(Array)) + + triggerWriteAccessBecauseNoCheckNecessary() + + // If values were provided, they must have values + if (showStyleBaseIds && showStyleBaseIds.length === 0) return null + + // Add the requested filter + const selector: MongoQuery = {} + if (showStyleBaseIds) selector._id = { $in: showStyleBaseIds } + + return ShowStyleBases.findWithCursor(selector) + } + ) + + registry.publish( + CorelibPubSub.showStyleVariants, + async ( + _context, + showStyleBaseIds: ShowStyleBaseId[] | null, + showStyleVariantIds: ShowStyleVariantId[] | null, + _token: string | undefined + ) => { + check(showStyleBaseIds, Match.Maybe(Array)) + check(showStyleVariantIds, Match.Maybe(Array)) + + triggerWriteAccessBecauseNoCheckNecessary() + + // If values were provided, they must have values + if (showStyleBaseIds && showStyleBaseIds.length === 0) return null + if (showStyleVariantIds && showStyleVariantIds.length === 0) return null + + // Add the requested filter + const selector: MongoQuery = {} + if (showStyleBaseIds) selector.showStyleBaseId = { $in: showStyleBaseIds } + if (showStyleVariantIds) selector._id = { $in: showStyleVariantIds } + + return ShowStyleVariants.findWithCursor(selector) + } + ) + + registry.publish( + MeteorPubSub.rundownLayouts, + async (_context, showStyleBaseIds: ShowStyleBaseId[] | null, _token: string | undefined) => { + check(showStyleBaseIds, Match.Maybe(Array)) + + triggerWriteAccessBecauseNoCheckNecessary() + + // If values were provided, they must have values + if (showStyleBaseIds && showStyleBaseIds.length === 0) return null + + const selector: MongoQuery = {} + if (showStyleBaseIds) selector.showStyleBaseId = { $in: showStyleBaseIds } + + return RundownLayouts.findWithCursor(selector) + } + ) + + registry.publish( + MeteorPubSub.triggeredActions, + async (_context, showStyleBaseIds: ShowStyleBaseId[] | null, _token: string | undefined) => { + check(showStyleBaseIds, Match.Maybe(Array)) + + triggerWriteAccessBecauseNoCheckNecessary() + + const selector: MongoQuery = + showStyleBaseIds && showStyleBaseIds.length > 0 + ? { + $or: [ + { + showStyleBaseId: null, + }, + { + showStyleBaseId: { $in: showStyleBaseIds }, + }, + ], + } + : { showStyleBaseId: null } + + return TriggeredActions.findWithCursor(selector) + } + ) +} diff --git a/meteor/server/publications/showStyleUI.ts b/meteor/server/publications/showStyleUI.ts index 170eaea3bdf..a877132f543 100644 --- a/meteor/server/publications/showStyleUI.ts +++ b/meteor/server/publications/showStyleUI.ts @@ -5,15 +5,11 @@ import { ReadonlyDeep } from 'type-fest' import { CustomCollectionName, MeteorPubSub } from '@sofie-automation/meteor-lib/dist/api/pubsub' import { DBShowStyleBase, UIShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' import { Complete, literal } from '@sofie-automation/corelib/dist/lib' -import { - meteorCustomPublish, - SetupObserversResult, - setUpOptimizedObserverArray, - TriggerUpdate, -} from '../lib/customPublication' +import { SetupObserversResult, setUpOptimizedObserverArray, TriggerUpdate } from '../lib/customPublication' import { ShowStyleBases } from '../collections' import { check } from 'meteor/check' import { triggerWriteAccessBecauseNoCheckNecessary } from '../security/securityVerify' +import type { PublicationRegistry } from '../publicationRegistry' interface UIShowStyleBaseArgs { readonly showStyleBaseId: ShowStyleBaseId @@ -89,25 +85,27 @@ async function manipulateUIShowStyleBasePublicationData( ] } -meteorCustomPublish( - MeteorPubSub.uiShowStyleBase, - CustomCollectionName.UIShowStyleBase, - async function (pub, showStyleBaseId: ShowStyleBaseId) { - check(showStyleBaseId, String) +export function registerShowStyleUIPublications(registry: PublicationRegistry): void { + registry.customPublish( + MeteorPubSub.uiShowStyleBase, + CustomCollectionName.UIShowStyleBase, + async (_context, pub, showStyleBaseId: ShowStyleBaseId) => { + check(showStyleBaseId, String) - triggerWriteAccessBecauseNoCheckNecessary() + triggerWriteAccessBecauseNoCheckNecessary() - await setUpOptimizedObserverArray< - UIShowStyleBase, - UIShowStyleBaseArgs, - UIShowStyleBaseState, - UIShowStyleBaseUpdateProps - >( - `pub_${MeteorPubSub.uiShowStyleBase}_${showStyleBaseId}`, - { showStyleBaseId }, - setupUIShowStyleBasePublicationObservers, - manipulateUIShowStyleBasePublicationData, - pub - ) - } -) + await setUpOptimizedObserverArray< + UIShowStyleBase, + UIShowStyleBaseArgs, + UIShowStyleBaseState, + UIShowStyleBaseUpdateProps + >( + `pub_${MeteorPubSub.uiShowStyleBase}_${showStyleBaseId}`, + { showStyleBaseId }, + setupUIShowStyleBasePublicationObservers, + manipulateUIShowStyleBasePublicationData, + pub + ) + } + ) +} diff --git a/meteor/server/publications/studio.ts b/meteor/server/publications/studio.ts index 48d2065caf1..c5532a83f17 100644 --- a/meteor/server/publications/studio.ts +++ b/meteor/server/publications/studio.ts @@ -1,12 +1,10 @@ import { Meteor } from 'meteor/meteor' import { check, Match } from '../lib/check' -import { meteorPublish } from './lib/lib' import { MeteorPubSub } from '@sofie-automation/meteor-lib/dist/api/pubsub' import { getActiveRoutes, getRoutedMappings } from '@sofie-automation/meteor-lib/dist/collections/Studios' import { ExternalMessageQueueObj } from '@sofie-automation/corelib/dist/dataModel/ExternalMessageQueue' import { CustomPublish, - meteorCustomPublish, SetupObserversResult, setUpOptimizedObserverArray, TriggerUpdate, @@ -36,112 +34,124 @@ import { triggerWriteAccessBecauseNoCheckNecessary } from '../security/securityV import { checkAccessAndGetPeripheralDevice } from '../security/check' import { assertConnectionHasOneOfPermissions } from '../security/auth' import { fetchStudioIds } from '../optimizations' +import type { PublicationRegistry } from '../publicationRegistry' -meteorPublish(CorelibPubSub.studios, async function (studioIds: StudioId[] | null, _token: string | undefined) { - check(studioIds, Match.Maybe(Array)) +export function registerStudioPublications(registry: PublicationRegistry): void { + registry.publish( + CorelibPubSub.studios, + async (_context, studioIds: StudioId[] | null, _token: string | undefined) => { + check(studioIds, Match.Maybe(Array)) - triggerWriteAccessBecauseNoCheckNecessary() + triggerWriteAccessBecauseNoCheckNecessary() - // If values were provided, they must have values - if (studioIds && studioIds.length === 0) return null + // If values were provided, they must have values + if (studioIds && studioIds.length === 0) return null - // Add the requested filter - const selector: MongoQuery = {} - if (studioIds) selector._id = { $in: studioIds } + // Add the requested filter + const selector: MongoQuery = {} + if (studioIds) selector._id = { $in: studioIds } - return Studios.findWithCursor(selector) -}) + return Studios.findWithCursor(selector) + } + ) -meteorPublish( - CorelibPubSub.externalMessageQueue, - async function (selector: MongoQuery, _token: string | undefined) { - triggerWriteAccessBecauseNoCheckNecessary() + registry.publish( + CorelibPubSub.externalMessageQueue, + async (_context, selector: MongoQuery, _token: string | undefined) => { + triggerWriteAccessBecauseNoCheckNecessary() - if (!selector) throw new Meteor.Error(400, 'selector argument missing') - const modifier: FindOptions = { - fields: {}, - } + if (!selector) throw new Meteor.Error(400, 'selector argument missing') + const modifier: FindOptions = { + fields: {}, + } - return ExternalMessageQueue.findWithCursor(selector, modifier) - } -) + return ExternalMessageQueue.findWithCursor(selector, modifier) + } + ) -meteorPublish(CorelibPubSub.expectedPackages, async function (studioIds: StudioId[], _token: string | undefined) { - // Note: This differs from the expected packages sent to the Package Manager, instead @see PubSub.expectedPackagesForDevice - check(studioIds, Array) + registry.publish( + CorelibPubSub.expectedPackages, + async (_context, studioIds: StudioId[], _token: string | undefined) => { + // Note: This differs from the expected packages sent to the Package Manager, instead @see PubSub.expectedPackagesForDevice + check(studioIds, Array) - triggerWriteAccessBecauseNoCheckNecessary() + triggerWriteAccessBecauseNoCheckNecessary() - if (studioIds.length === 0) return null + if (studioIds.length === 0) return null - return ExpectedPackages.findWithCursor({ - studioId: { $in: studioIds }, - }) -}) -meteorPublish( - CorelibPubSub.expectedPackageWorkStatuses, - async function (studioIds: StudioId[], _token: string | undefined) { - check(studioIds, Array) - triggerWriteAccessBecauseNoCheckNecessary() + return ExpectedPackages.findWithCursor({ + studioId: { $in: studioIds }, + }) + } + ) + registry.publish( + CorelibPubSub.expectedPackageWorkStatuses, + async (_context, studioIds: StudioId[], _token: string | undefined) => { + check(studioIds, Array) + triggerWriteAccessBecauseNoCheckNecessary() - if (studioIds.length === 0) return null + if (studioIds.length === 0) return null - return ExpectedPackageWorkStatuses.findWithCursor({ - studioId: { $in: studioIds }, - }) - } -) -meteorPublish( - CorelibPubSub.packageContainerStatuses, - async function (studioIds: StudioId[], _token: string | undefined) { - check(studioIds, Array) + return ExpectedPackageWorkStatuses.findWithCursor({ + studioId: { $in: studioIds }, + }) + } + ) + registry.publish( + CorelibPubSub.packageContainerStatuses, + async (_context, studioIds: StudioId[], _token: string | undefined) => { + check(studioIds, Array) - triggerWriteAccessBecauseNoCheckNecessary() + triggerWriteAccessBecauseNoCheckNecessary() - if (studioIds.length === 0) return null + if (studioIds.length === 0) return null - return PackageContainerStatuses.findWithCursor({ - studioId: { $in: studioIds }, - }) - } -) + return PackageContainerStatuses.findWithCursor({ + studioId: { $in: studioIds }, + }) + } + ) -meteorPublish(CorelibPubSub.packageInfos, async function (deviceId: PeripheralDeviceId, _token: string | undefined) { - check(deviceId, String) + registry.publish( + CorelibPubSub.packageInfos, + async (_context, deviceId: PeripheralDeviceId, _token: string | undefined) => { + check(deviceId, String) - triggerWriteAccessBecauseNoCheckNecessary() + triggerWriteAccessBecauseNoCheckNecessary() - return PackageInfos.findWithCursor({ deviceId }) -}) + return PackageInfos.findWithCursor({ deviceId }) + } + ) -meteorCustomPublish( - PeripheralDevicePubSub.mappingsForDevice, - PeripheralDevicePubSubCollectionsNames.studioMappings, - async function (pub, deviceId: PeripheralDeviceId, token: string | undefined) { - check(deviceId, String) + registry.customPublish( + PeripheralDevicePubSub.mappingsForDevice, + PeripheralDevicePubSubCollectionsNames.studioMappings, + async (context, pub, deviceId: PeripheralDeviceId, token: string | undefined) => { + check(deviceId, String) - const peripheralDevice = await checkAccessAndGetPeripheralDevice(deviceId, token, this) + const peripheralDevice = await checkAccessAndGetPeripheralDevice(deviceId, token, context) - const studioId = peripheralDevice.studioAndConfigId?.studioId - if (!studioId) return + const studioId = peripheralDevice.studioAndConfigId?.studioId + if (!studioId) return - await createObserverForMappingsPublication(pub, studioId) - } -) + await createObserverForMappingsPublication(pub, studioId) + } + ) -meteorCustomPublish( - MeteorPubSub.mappingsForStudio, - PeripheralDevicePubSubCollectionsNames.studioMappings, - async function (pub) { - assertConnectionHasOneOfPermissions(this.connection, 'testing') + registry.customPublish( + MeteorPubSub.mappingsForStudio, + PeripheralDevicePubSubCollectionsNames.studioMappings, + async (context, pub) => { + assertConnectionHasOneOfPermissions(context.connection, 'testing') - // Find the first studioId. There should only be one, but we don't know what it will be - const studioIds = await fetchStudioIds({}) - if (studioIds.length < 1) throw new Error('No studios found') + // Find the first studioId. There should only be one, but we don't know what it will be + const studioIds = await fetchStudioIds({}) + if (studioIds.length < 1) throw new Error('No studios found') - await createObserverForMappingsPublication(pub, studioIds[0]) - } -) + await createObserverForMappingsPublication(pub, studioIds[0]) + } + ) +} interface RoutedMappingsArgs { readonly studioId: StudioId diff --git a/meteor/server/publications/studioUI.ts b/meteor/server/publications/studioUI.ts index 99442f12073..2cb0399f0a7 100644 --- a/meteor/server/publications/studioUI.ts +++ b/meteor/server/publications/studioUI.ts @@ -7,7 +7,6 @@ import { DBStudio, UIStudio } from '@sofie-automation/corelib/dist/dataModel/Stu import { Complete, literal } from '@sofie-automation/corelib/dist/lib' import { CustomPublishCollection, - meteorCustomPublish, setUpCollectionOptimizedObserver, SetupObserversResult, TriggerUpdate, @@ -15,6 +14,7 @@ import { import { Studios } from '../collections' import { check, Match } from 'meteor/check' import { triggerWriteAccessBecauseNoCheckNecessary } from '../security/securityVerify' +import type { PublicationRegistry } from '../publicationRegistry' interface UIStudioArgs { readonly studioId: StudioId | null @@ -121,20 +121,22 @@ async function manipulateUIStudioPublicationData( } } -meteorCustomPublish( - MeteorPubSub.uiStudio, - CustomCollectionName.UIStudio, - async function (pub, studioId: StudioId | null) { - check(studioId, Match.Maybe(String)) - - triggerWriteAccessBecauseNoCheckNecessary() - - await setUpCollectionOptimizedObserver( - `pub_${MeteorPubSub.uiStudio}_${studioId}`, - { studioId }, - setupUIStudioPublicationObservers, - manipulateUIStudioPublicationData, - pub - ) - } -) +export function registerStudioUIPublications(registry: PublicationRegistry): void { + registry.customPublish( + MeteorPubSub.uiStudio, + CustomCollectionName.UIStudio, + async (_context, pub, studioId: StudioId | null) => { + check(studioId, Match.Maybe(String)) + + triggerWriteAccessBecauseNoCheckNecessary() + + await setUpCollectionOptimizedObserver( + `pub_${MeteorPubSub.uiStudio}_${studioId}`, + { studioId }, + setupUIStudioPublicationObservers, + manipulateUIStudioPublicationData, + pub + ) + } + ) +} diff --git a/meteor/server/publications/system.ts b/meteor/server/publications/system.ts index d95e8eaa988..30eb0cba65c 100644 --- a/meteor/server/publications/system.ts +++ b/meteor/server/publications/system.ts @@ -1,4 +1,3 @@ -import { meteorPublish } from './lib/lib' import { MeteorPubSub } from '@sofie-automation/meteor-lib/dist/api/pubsub' import { CoreSystem, Notifications } from '../collections' import { RundownId, RundownPlaylistId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' @@ -6,54 +5,60 @@ import { check } from 'meteor/check' import { SYSTEM_ID } from '@sofie-automation/meteor-lib/dist/collections/CoreSystem' import { triggerWriteAccessBecauseNoCheckNecessary } from '../security/securityVerify' import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' +import type { PublicationRegistry } from '../publicationRegistry' -meteorPublish(MeteorPubSub.coreSystem, async function (_token: string | undefined) { - triggerWriteAccessBecauseNoCheckNecessary() - - return CoreSystem.findWithCursor(SYSTEM_ID, { - projection: { - // Include only specific fields in the result documents: - _id: 1, - systemInfo: 1, - apm: 1, - name: 1, - logLevel: 1, - serviceMessages: 1, - blueprintId: 1, - logo: 1, - settingsWithOverrides: 1, - enableMonitorBlockedThread: 1, - }, +export function registerSystemPublications(registry: PublicationRegistry): void { + registry.publish(MeteorPubSub.coreSystem, async (_context, _token: string | undefined) => { + triggerWriteAccessBecauseNoCheckNecessary() + + return CoreSystem.findWithCursor(SYSTEM_ID, { + projection: { + // Include only specific fields in the result documents: + _id: 1, + systemInfo: 1, + apm: 1, + name: 1, + logLevel: 1, + serviceMessages: 1, + blueprintId: 1, + logo: 1, + settingsWithOverrides: 1, + enableMonitorBlockedThread: 1, + }, + }) }) -}) -meteorPublish(CorelibPubSub.notificationsForRundown, async function (studioId: StudioId, rundownId: RundownId) { - // HACK: This should do real auth - triggerWriteAccessBecauseNoCheckNecessary() + registry.publish( + CorelibPubSub.notificationsForRundown, + async (_context, studioId: StudioId, rundownId: RundownId) => { + // HACK: This should do real auth + triggerWriteAccessBecauseNoCheckNecessary() - check(studioId, String) - check(rundownId, String) + check(studioId, String) + check(rundownId, String) - return Notifications.findWithCursor({ - // Loosely match any notifications related to this rundown - 'relatedTo.studioId': studioId, - 'relatedTo.rundownId': rundownId, - }) -}) + return Notifications.findWithCursor({ + // Loosely match any notifications related to this rundown + 'relatedTo.studioId': studioId, + 'relatedTo.rundownId': rundownId, + }) + } + ) -meteorPublish( - CorelibPubSub.notificationsForRundownPlaylist, - async function (studioId: StudioId, playlistId: RundownPlaylistId) { - // HACK: This should do real auth - triggerWriteAccessBecauseNoCheckNecessary() + registry.publish( + CorelibPubSub.notificationsForRundownPlaylist, + async (_context, studioId: StudioId, playlistId: RundownPlaylistId) => { + // HACK: This should do real auth + triggerWriteAccessBecauseNoCheckNecessary() - check(studioId, String) - check(playlistId, String) + check(studioId, String) + check(playlistId, String) - return Notifications.findWithCursor({ - // Loosely match any notifications related to this playlist - 'relatedTo.studioId': studioId, - 'relatedTo.playlistId': playlistId, - }) - } -) + return Notifications.findWithCursor({ + // Loosely match any notifications related to this playlist + 'relatedTo.studioId': studioId, + 'relatedTo.playlistId': playlistId, + }) + } + ) +} diff --git a/meteor/server/publications/timeline.ts b/meteor/server/publications/timeline.ts index d42a7a38ca6..51107526f49 100644 --- a/meteor/server/publications/timeline.ts +++ b/meteor/server/publications/timeline.ts @@ -7,12 +7,10 @@ import { serializeTimelineBlob, TimelineBlob, } from '@sofie-automation/corelib/dist/dataModel/Timeline' -import { meteorPublish } from './lib/lib' import { MeteorPubSub } from '@sofie-automation/meteor-lib/dist/api/pubsub' import { FindOptions } from '@sofie-automation/meteor-lib/dist/collections/lib' import { CustomPublish, - meteorCustomPublish, SetupObserversResult, setUpOptimizedObserverArray, TriggerUpdate, @@ -37,58 +35,61 @@ import { import { applyAndValidateOverrides } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' import { checkAccessAndGetPeripheralDevice } from '../security/check' import { assertConnectionHasOneOfPermissions } from '../security/auth' +import type { PublicationRegistry } from '../publicationRegistry' -meteorPublish(CorelibPubSub.timelineDatastore, async function () { - assertConnectionHasOneOfPermissions(this.connection, 'testing') +export function registerTimelinePublications(registry: PublicationRegistry): void { + registry.publish(CorelibPubSub.timelineDatastore, async (context) => { + assertConnectionHasOneOfPermissions(context.connection, 'testing') - return TimelineDatastore.findWithCursor({}) -}) + return TimelineDatastore.findWithCursor({}) + }) -meteorCustomPublish( - PeripheralDevicePubSub.timelineForDevice, - PeripheralDevicePubSubCollectionsNames.studioTimeline, - async function (pub, deviceId: PeripheralDeviceId, token: string | undefined) { - check(deviceId, String) + registry.customPublish( + PeripheralDevicePubSub.timelineForDevice, + PeripheralDevicePubSubCollectionsNames.studioTimeline, + async (context, pub, deviceId: PeripheralDeviceId, token: string | undefined) => { + check(deviceId, String) - const peripheralDevice = await checkAccessAndGetPeripheralDevice(deviceId, token, this) + const peripheralDevice = await checkAccessAndGetPeripheralDevice(deviceId, token, context) - const studioId = peripheralDevice.studioAndConfigId?.studioId - if (!studioId) return + const studioId = peripheralDevice.studioAndConfigId?.studioId + if (!studioId) return - await createObserverForTimelinePublication(pub, studioId) - } -) -meteorPublish( - PeripheralDevicePubSub.timelineDatastoreForDevice, - async function (deviceId: PeripheralDeviceId, token: string | undefined) { - check(deviceId, String) + await createObserverForTimelinePublication(pub, studioId) + } + ) + registry.publish( + PeripheralDevicePubSub.timelineDatastoreForDevice, + async (context, deviceId: PeripheralDeviceId, token: string | undefined) => { + check(deviceId, String) - const peripheralDevice = await checkAccessAndGetPeripheralDevice(deviceId, token, this) + const peripheralDevice = await checkAccessAndGetPeripheralDevice(deviceId, token, context) - const studioId = peripheralDevice.studioAndConfigId?.studioId - if (!studioId) return null + const studioId = peripheralDevice.studioAndConfigId?.studioId + if (!studioId) return null - const modifier: FindOptions = { - fields: {}, - } + const modifier: FindOptions = { + fields: {}, + } - return TimelineDatastore.findWithCursor({ studioId }, modifier) - } -) + return TimelineDatastore.findWithCursor({ studioId }, modifier) + } + ) -meteorCustomPublish( - MeteorPubSub.timelineForStudio, - PeripheralDevicePubSubCollectionsNames.studioTimeline, - async function (pub) { - assertConnectionHasOneOfPermissions(this.connection, 'testing') + registry.customPublish( + MeteorPubSub.timelineForStudio, + PeripheralDevicePubSubCollectionsNames.studioTimeline, + async (context, pub) => { + assertConnectionHasOneOfPermissions(context.connection, 'testing') - // Find the first studioId. There should only be one, but we don't know what it will be - const studioIds = await fetchStudioIds({}) - if (studioIds.length < 1) throw new Error('No studios found') + // Find the first studioId. There should only be one, but we don't know what it will be + const studioIds = await fetchStudioIds({}) + if (studioIds.length < 1) throw new Error('No studios found') - await createObserverForTimelinePublication(pub, studioIds[0]) - } -) + await createObserverForTimelinePublication(pub, studioIds[0]) + } + ) +} interface RoutedTimelineArgs { readonly studioId: StudioId diff --git a/meteor/server/publications/translationsBundles.ts b/meteor/server/publications/translationsBundles.ts index 8f04a3c9bdc..104d1833f6e 100644 --- a/meteor/server/publications/translationsBundles.ts +++ b/meteor/server/publications/translationsBundles.ts @@ -1,18 +1,20 @@ -import { meteorPublish } from './lib/lib' import { MeteorPubSub } from '@sofie-automation/meteor-lib/dist/api/pubsub' import { TranslationsBundles } from '../collections' import { MongoQuery } from '@sofie-automation/corelib/dist/mongo' import { TranslationsBundle } from '@sofie-automation/meteor-lib/dist/collections/TranslationsBundles' import { triggerWriteAccessBecauseNoCheckNecessary } from '../security/securityVerify' +import type { PublicationRegistry } from '../publicationRegistry' -meteorPublish(MeteorPubSub.translationsBundles, async (_token: string | undefined) => { - const selector: MongoQuery = {} +export function registerTranslationsBundlesPublications(registry: PublicationRegistry): void { + registry.publish(MeteorPubSub.translationsBundles, async (_context, _token: string | undefined) => { + const selector: MongoQuery = {} - triggerWriteAccessBecauseNoCheckNecessary() + triggerWriteAccessBecauseNoCheckNecessary() - return TranslationsBundles.findWithCursor(selector, { - projection: { - data: 0, - }, + return TranslationsBundles.findWithCursor(selector, { + projection: { + data: 0, + }, + }) }) -}) +} diff --git a/meteor/server/publications/triggeredActionsUI.ts b/meteor/server/publications/triggeredActionsUI.ts index 00406388515..d75b7928ec1 100644 --- a/meteor/server/publications/triggeredActionsUI.ts +++ b/meteor/server/publications/triggeredActionsUI.ts @@ -9,7 +9,6 @@ import { import { Complete, literal } from '@sofie-automation/corelib/dist/lib' import { CustomPublishCollection, - meteorCustomPublish, setUpCollectionOptimizedObserver, SetupObserversResult, TriggerUpdate, @@ -18,6 +17,7 @@ import { TriggeredActions } from '../collections' import { check, Match } from 'meteor/check' import { MongoQuery } from '@sofie-automation/corelib/dist/mongo' import { triggerWriteAccessBecauseNoCheckNecessary } from '../security/securityVerify' +import type { PublicationRegistry } from '../publicationRegistry' interface UITriggeredActionsArgs { readonly showStyleBaseId: ShowStyleBaseId | null @@ -105,25 +105,27 @@ async function manipulateUITriggeredActionsPublicationData( } } -meteorCustomPublish( - MeteorPubSub.uiTriggeredActions, - CustomCollectionName.UITriggeredActions, - async function (pub, showStyleBaseId: ShowStyleBaseId | null) { - check(showStyleBaseId, Match.Maybe(String)) - - triggerWriteAccessBecauseNoCheckNecessary() - - await setUpCollectionOptimizedObserver< - UITriggeredActionsObj, - UITriggeredActionsArgs, - UITriggeredActionsState, - UITriggeredActionsUpdateProps - >( - `pub_${MeteorPubSub.uiTriggeredActions}_${showStyleBaseId}`, - { showStyleBaseId }, - setupUITriggeredActionsPublicationObservers, - manipulateUITriggeredActionsPublicationData, - pub - ) - } -) +export function registerTriggeredActionsUIPublications(registry: PublicationRegistry): void { + registry.customPublish( + MeteorPubSub.uiTriggeredActions, + CustomCollectionName.UITriggeredActions, + async (_context, pub, showStyleBaseId: ShowStyleBaseId | null) => { + check(showStyleBaseId, Match.Maybe(String)) + + triggerWriteAccessBecauseNoCheckNecessary() + + await setUpCollectionOptimizedObserver< + UITriggeredActionsObj, + UITriggeredActionsArgs, + UITriggeredActionsState, + UITriggeredActionsUpdateProps + >( + `pub_${MeteorPubSub.uiTriggeredActions}_${showStyleBaseId}`, + { showStyleBaseId }, + setupUITriggeredActionsPublicationObservers, + manipulateUITriggeredActionsPublicationData, + pub + ) + } + ) +} diff --git a/meteor/server/security/check.ts b/meteor/server/security/check.ts index df19d425037..2a654ab8751 100644 --- a/meteor/server/security/check.ts +++ b/meteor/server/security/check.ts @@ -7,7 +7,7 @@ import { PeripheralDevices, RundownPlaylists, Rundowns } from '../collections' import { PeripheralDevice } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' import { MethodContext } from '../api/methodContext' import { profiler } from '../api/profiler' -import { SubscriptionContext } from '../publications/lib/lib' +import { PublicationContext } from '../publications/lib/lib' /** * Check that the current user has write access to the specified playlist, and ensure that the playlist exists @@ -66,7 +66,7 @@ export type VerifiedRundownForUserAction = Pick< export async function checkAccessAndGetPeripheralDevice( deviceId: PeripheralDeviceId, token: string | undefined, - context: MethodContext | SubscriptionContext + context: MethodContext | PublicationContext ): Promise { const span = profiler.startSpan('lib.checkAccessAndGetPeripheralDevice') diff --git a/meteor/server/security/securityVerify.ts b/meteor/server/security/securityVerify.ts index e7edc63cfcc..df0d16ebf4d 100644 --- a/meteor/server/security/securityVerify.ts +++ b/meteor/server/security/securityVerify.ts @@ -1,7 +1,8 @@ import { Meteor } from 'meteor/meteor' -import { AllMeteorMethods, suppressExtraErrorLogging } from '../methods' +import { suppressExtraErrorLogging } from '../methods' import { disableChecks, enableChecks as restoreChecks } from '../lib/check' import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' +import type { MethodRegistry } from '../methodRegistry' /** These function are used to verify that all methods defined are using security functions */ @@ -36,32 +37,35 @@ export function triggerWriteAccessBecauseNoCheckNecessary(): void { triggerWriteAccess() } -Meteor.startup(() => { - if (!Meteor.isProduction && !Meteor.isTest) { - Meteor.setTimeout(() => { - console.log('Security check: Verifying methods...') - verifyAllMethods() - // .then(() => { - // }) - .then((ok) => { - if (ok) { - console.log('Security check: ok!') - } else { - console.log('There are security issues that needs fixing, see above!') - } - }) - .catch((e) => { - console.log('Error') - console.log(e) - }) - }, 1000) - } -}) +export function startupVerifyAllMethods(methodRegistry: MethodRegistry): void { + if (Meteor.isProduction || Meteor.isTest) return + + Meteor.setTimeout(() => { + console.log('Security check: Verifying methods...') + verifyAllMethods(methodRegistry) + .then((ok) => { + if (ok) { + console.log('Security check: ok!') + } else { + console.log('There are security issues that needs fixing, see above!') + } + }) + .catch((e) => { + console.log('Error') + console.log(e) + }) + }, 1000) +} -export async function verifyAllMethods(): Promise { +async function verifyAllMethods(methodRegistry: MethodRegistry): Promise { // Verify all Meteor methods let ok = true - for (const methodName of AllMeteorMethods) { + for (const methodName of methodRegistry.getAllMethodNames()) { + // Developer-only debug methods are gated behind a 'developer' permission check that throws when + // verifyMethod calls them without a real connection, producing a false security failure. They were + // never part of the verified set before the registry refactor, so skip them here. + if (methodRegistry.isDebugMethod(methodName)) continue + ok = ok && (await verifyMethod(methodName)) if (!ok) return false // Bail on first error diff --git a/meteor/server/systemStatus/__tests__/api.test.ts b/meteor/server/systemStatus/__tests__/api.test.ts index fa04c875866..73970ca87b0 100644 --- a/meteor/server/systemStatus/__tests__/api.test.ts +++ b/meteor/server/systemStatus/__tests__/api.test.ts @@ -10,11 +10,12 @@ import { MeteorCall } from '../../api/methods' import { callKoaRoute } from '../../../__mocks__/koa-util' import { healthRouter } from '../api' import { UIBlueprintUpgradeStatus } from '@sofie-automation/meteor-lib/dist/api/upgradeStatus' +import { registerAllMethodsForTest } from '../../../__mocks__/helpers/methods' // we don't want the deviceTriggers observer to start up at this time jest.mock('../../api/deviceTriggers/observer') -require('../api') +registerAllMethodsForTest() require('../../coreSystem/index') const PackageInfo = require('../../../package.json') diff --git a/meteor/server/systemStatus/__tests__/systemStatus.test.ts b/meteor/server/systemStatus/__tests__/systemStatus.test.ts index 09571d5b698..3df890e6356 100644 --- a/meteor/server/systemStatus/__tests__/systemStatus.test.ts +++ b/meteor/server/systemStatus/__tests__/systemStatus.test.ts @@ -12,11 +12,11 @@ import { MeteorCall } from '../../api/methods' import { PeripheralDeviceStatusObject } from '@sofie-automation/shared-lib/dist/peripheralDevice/peripheralDeviceAPI' import { PeripheralDevices } from '../../collections' import { UIBlueprintUpgradeStatus } from '@sofie-automation/meteor-lib/dist/api/upgradeStatus' +import { registerAllMethodsForTest } from '../../../__mocks__/helpers/methods' // we don't want the deviceTriggers observer to start up at this time jest.mock('../../api/deviceTriggers/observer') -require('../api') const PackageInfo = require('../../../package.json') import * as getServerBlueprintUpgradeStatuses from '../../publications/blueprintUpgradeStatus/systemStatus' @@ -25,6 +25,8 @@ const getServerBlueprintUpgradeStatusesMock = jest.spyOn( 'getServerBlueprintUpgradeStatuses' ) +registerAllMethodsForTest() + describe('systemStatus', () => { beforeEach(() => { getServerBlueprintUpgradeStatusesMock.mockReturnValue(Promise.resolve(literal([]))) diff --git a/meteor/server/systemStatus/api.ts b/meteor/server/systemStatus/api.ts index fd2f1161f5a..e0921e2cda4 100644 --- a/meteor/server/systemStatus/api.ts +++ b/meteor/server/systemStatus/api.ts @@ -1,9 +1,4 @@ -import { registerClassToMeteorMethods } from '../methods' -import { - StatusResponse, - NewSystemStatusAPI, - SystemStatusAPIMethods, -} from '@sofie-automation/meteor-lib/dist/api/systemStatus' +import { StatusResponse, NewSystemStatusAPI } from '@sofie-automation/meteor-lib/dist/api/systemStatus' import { getDebugStates, getSystemStatus } from './systemStatus' import { protectString } from '@sofie-automation/corelib/dist/protectedString' import { MethodContextAPI } from '../api/methodContext' @@ -61,16 +56,15 @@ function health(status: StatusResponse, ctx: Koa.ParameterizedContext) { ctx.body = JSON.stringify(status) } -class ServerSystemStatusAPI extends MethodContextAPI implements NewSystemStatusAPI { - async getSystemStatus() { +export class ServerSystemStatusAPI extends MethodContextAPI implements NewSystemStatusAPI { + async getSystemStatus(): Promise { return getSystemStatus(this.connection) } - async getDebugStates(peripheralDeviceId: PeripheralDeviceId) { + async getDebugStates(peripheralDeviceId: PeripheralDeviceId): Promise { return getDebugStates(this, peripheralDeviceId) } } -registerClassToMeteorMethods(SystemStatusAPIMethods, ServerSystemStatusAPI, false) Meteor.startup(() => { bindKoaRouter(metricsRouter, '/metrics')