From 7f77b3f348deb94a69136dd315eff7fd4e7a6f56 Mon Sep 17 00:00:00 2001 From: Simon Rogers Date: Mon, 8 Jul 2024 16:16:04 +0100 Subject: [PATCH] Add studio and showStyle blueprintConfig validation support via blueprints --- meteor/lib/api/rest/v1/showstyles.ts | 29 +++ meteor/lib/api/rest/v1/studios.ts | 28 +++ meteor/server/api/rest/v1/showstyles.ts | 187 +++++++++++++++- meteor/server/api/rest/v1/studios.ts | 145 +++++++++++- meteor/server/api/rest/v1/typeConversion.ts | 207 ++++++++++++++++-- .../src/api/showStyle.ts | 11 + .../blueprints-integration/src/api/studio.ts | 9 + 7 files changed, 587 insertions(+), 29 deletions(-) diff --git a/meteor/lib/api/rest/v1/showstyles.ts b/meteor/lib/api/rest/v1/showstyles.ts index 68dfa88d48..d50d9f15e8 100644 --- a/meteor/lib/api/rest/v1/showstyles.ts +++ b/meteor/lib/api/rest/v1/showstyles.ts @@ -59,6 +59,34 @@ export interface ShowStylesRestAPI { showStyleBaseId: ShowStyleBaseId, showStyleBase: APIShowStyleBase ): Promise> + /** + * Gets a ShowStyle config, if the ShowStyle id exists. + * + * Throws if the specified ShowStyle does not exist. + * @param connection Connection data including client and header details + * @param event User event string + * @param showStyleBaseId ShowStyleBaseId to fetch + */ + getShowStyleConfig( + connection: Meteor.Connection, + event: string, + showStyleBaseId: ShowStyleBaseId + ): Promise> + /** + * Updates a ShowStyle configuration. + * + * Throws if the ShowStyle is in use in an active Rundown. + * @param connection Connection data including client and header details + * @param event User event string + * @param showStyleBaseId Id of the ShowStyleBase to update + * @param object Blueprint configuration object + */ + updateShowStyleConfig( + connection: Meteor.Connection, + event: string, + showStyleBaseId: ShowStyleBaseId, + config: object + ): Promise> /** * Removed a ShowStyleBase. * @@ -192,6 +220,7 @@ export interface APIShowStyleBase { export interface APIShowStyleVariant { name: string showStyleBaseId: string + blueprintConfigPresetId?: string config: object rank: number } diff --git a/meteor/lib/api/rest/v1/studios.ts b/meteor/lib/api/rest/v1/studios.ts index ce3e967ed8..fd3a133283 100644 --- a/meteor/lib/api/rest/v1/studios.ts +++ b/meteor/lib/api/rest/v1/studios.ts @@ -56,6 +56,34 @@ export interface StudiosRestAPI { studioId: StudioId, studio: APIStudio ): Promise> + /** + * Gets a Studio config, if it exists. + * + * Throws if the specified Studio does not exist. + * @param connection Connection data including client and header details + * @param event User event string + * @param studioId Id of the Studio to fetch + */ + getStudioConfig( + connection: Meteor.Connection, + event: string, + studioId: StudioId + ): Promise> + /** + * Updates a Studio configuration. + * + * Throws if the Studio already exists and is in use in an active Rundown. + * @param connection Connection data including client and header details + * @param event User event string + * @param studioId Id of the Studio to update + * @param object Blueprint configuration object + */ + updateStudioConfig( + connection: Meteor.Connection, + event: string, + studioId: StudioId, + config: object + ): Promise> /** * Deletes a Studio. * diff --git a/meteor/server/api/rest/v1/showstyles.ts b/meteor/server/api/rest/v1/showstyles.ts index b97532d58a..52cc5ec477 100644 --- a/meteor/server/api/rest/v1/showstyles.ts +++ b/meteor/server/api/rest/v1/showstyles.ts @@ -20,6 +20,7 @@ import { APIShowStyleVariantFrom, showStyleBaseFrom, showStyleVariantFrom, + validateAPIBlueprintConfigForShowStyle, } from './typeConversion' import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' @@ -60,16 +61,38 @@ class ShowStylesServerAPI implements ShowStylesRestAPI { const showStyleBase = await ShowStyleBases.findOneAsync(showStyleBaseId) if (!showStyleBase) throw new Meteor.Error(404, `ShowStyleBase ${showStyleBaseId} does not exist`) - return ClientAPI.responseSuccess(APIShowStyleBaseFrom(showStyleBase)) + return ClientAPI.responseSuccess(await APIShowStyleBaseFrom(showStyleBase)) } async addOrUpdateShowStyleBase( _connection: Meteor.Connection, _event: string, showStyleBaseId: ShowStyleBaseId, - showStyleBase: APIShowStyleBase + apiShowStyleBase: APIShowStyleBase ): Promise> { - const showStyle = await showStyleBaseFrom(showStyleBase, showStyleBaseId) + const blueprintConfigValidation = await validateAPIBlueprintConfigForShowStyle( + apiShowStyleBase, + protectString(apiShowStyleBase.blueprintId) + ) + const blueprintConfigValidationOK = blueprintConfigValidation.reduce( + (acc, msg) => acc && msg.level === NoteSeverity.INFO, + true + ) + if (!blueprintConfigValidationOK) { + const details = JSON.stringify( + blueprintConfigValidation.filter((msg) => msg.level < NoteSeverity.INFO).map((msg) => msg.message.key), + null, + 2 + ) + logger.error(`addOrUpdateShowStyleBase failed blueprint config validation with errors: ${details}`) + throw new Meteor.Error( + 409, + `ShowStyleBase ${showStyleBaseId} has failed blueprint config validation`, + details + ) + } + + const showStyle = await showStyleBaseFrom(apiShowStyleBase, showStyleBaseId) if (!showStyle) throw new Meteor.Error(400, `Invalid ShowStyleBase`) const existingShowStyle = await ShowStyleBases.findOneAsync(showStyleBaseId) @@ -108,7 +131,104 @@ class ShowStylesServerAPI implements ShowStylesRestAPI { throw new Meteor.Error(409, `ShowStyleBase ${showStyleBaseId} has failed validation`, details) } - return ClientAPI.responseSuccess(await runUpgradeForShowStyleBase(showStyleBaseId)) + return ClientAPI.responseSuccess( + await new Promise((resolve) => + // wait for the upsert to complete before upgrade + setTimeout(async () => resolve(await runUpgradeForShowStyleBase(showStyleBaseId)), 200) + ) + ) + } + + async getShowStyleConfig( + _connection: Meteor.Connection, + _event: string, + showStyleBaseId: ShowStyleBaseId + ): Promise> { + const showStyleBase = await ShowStyleBases.findOneAsync(showStyleBaseId) + if (!showStyleBase) throw new Meteor.Error(404, `ShowStyleBase ${showStyleBaseId} does not exist`) + + return ClientAPI.responseSuccess((await APIShowStyleBaseFrom(showStyleBase)).config) + } + + async updateShowStyleConfig( + _connection: Meteor.Connection, + _event: string, + showStyleBaseId: ShowStyleBaseId, + config: object + ): Promise> { + const existingShowStyleBase = await ShowStyleBases.findOneAsync(showStyleBaseId) + if (existingShowStyleBase) { + const existingShowStyle = await ShowStyleBases.findOneAsync(showStyleBaseId) + if (existingShowStyle) { + const rundowns = (await Rundowns.findFetchAsync( + { showStyleBaseId }, + { projection: { playlistId: 1 } } + )) as Array> + const playlists = (await RundownPlaylists.findFetchAsync( + { _id: { $in: rundowns.map((r) => r.playlistId) } }, + { + projection: { + activationId: 1, + }, + } + )) as Array> + if (playlists.some((playlist) => playlist.activationId !== undefined)) { + throw new Meteor.Error( + 412, + `Cannot update ShowStyleBase ${showStyleBaseId} as it is in use by an active Playlist` + ) + } + } + } else throw new Meteor.Error(404, `ShowStyleBase ${showStyleBaseId} not found`) + + const apiShowStyleBase = await APIShowStyleBaseFrom(existingShowStyleBase) + apiShowStyleBase.config = config + + const blueprintConfigValidation = await validateAPIBlueprintConfigForShowStyle( + apiShowStyleBase, + protectString(apiShowStyleBase.blueprintId) + ) + const blueprintConfigValidationOK = blueprintConfigValidation.reduce( + (acc, msg) => acc && msg.level === NoteSeverity.INFO, + true + ) + if (!blueprintConfigValidationOK) { + const details = JSON.stringify( + blueprintConfigValidation.filter((msg) => msg.level < NoteSeverity.INFO).map((msg) => msg.message.key), + null, + 2 + ) + logger.error(`updateShowStyleBase failed blueprint config validation with errors: ${details}`) + throw new Meteor.Error( + 409, + `ShowStyleBase ${showStyleBaseId} has failed blueprint config validation`, + details + ) + } + + const showStyle = await showStyleBaseFrom(apiShowStyleBase, showStyleBaseId) + if (!showStyle) throw new Meteor.Error(400, `Invalid ShowStyleBase`) + + await ShowStyleBases.upsertAsync(showStyleBaseId, showStyle) + + const validation = await validateConfigForShowStyleBase(showStyleBaseId) + const validateOK = validation.messages.reduce((acc, msg) => acc && msg.level === NoteSeverity.INFO, true) + if (!validateOK) { + const details = JSON.stringify( + validation.messages.filter((msg) => msg.level < NoteSeverity.INFO).map((msg) => msg.message.key), + null, + 2 + ) + logger.error(`addOrUpdateShowStyleBase failed validation with errors: ${details}`) + throw new Meteor.Error(409, `ShowStyleBase ${showStyleBaseId} has failed validation`, details) + } + + return ClientAPI.responseSuccess( + await new Promise((resolve) => + // wait for the upsert to complete before upgrade + setTimeout(async () => resolve(await runUpgradeForShowStyleBase(showStyleBaseId)), 200) + ) + ) } async deleteShowStyleBase( @@ -185,7 +305,7 @@ class ShowStylesServerAPI implements ShowStylesRestAPI { const variant = await ShowStyleVariants.findOneAsync(showStyleVariantId) if (!variant) throw new Meteor.Error(404, `ShowStyleVariant ${showStyleVariantId} not found`) - return ClientAPI.responseSuccess(APIShowStyleVariantFrom(variant)) + return ClientAPI.responseSuccess(await APIShowStyleVariantFrom(showStyleBase, variant)) } async addOrUpdateShowStyleVariant( @@ -193,12 +313,34 @@ class ShowStylesServerAPI implements ShowStylesRestAPI { _event: string, showStyleBaseId: ShowStyleBaseId, showStyleVariantId: ShowStyleVariantId, - showStyleVariant: APIShowStyleVariant + apiShowStyleVariant: APIShowStyleVariant ): Promise> { const showStyleBase = await ShowStyleBases.findOneAsync(showStyleBaseId) if (!showStyleBase) throw new Meteor.Error(404, `ShowStyleBase ${showStyleBaseId} does not exist`) - const showStyle = showStyleVariantFrom(showStyleVariant, showStyleVariantId) + const blueprintConfigValidation = await validateAPIBlueprintConfigForShowStyle( + apiShowStyleVariant, + showStyleBase.blueprintId + ) + const blueprintConfigValidationOK = blueprintConfigValidation.reduce( + (acc, msg) => acc && msg.level === NoteSeverity.INFO, + true + ) + if (!blueprintConfigValidationOK) { + const details = JSON.stringify( + blueprintConfigValidation.filter((msg) => msg.level < NoteSeverity.INFO).map((msg) => msg.message.key), + null, + 2 + ) + logger.error(`addOrUpdateShowStyleVariant failed blueprint config validation with errors: ${details}`) + throw new Meteor.Error( + 409, + `ShowStyleBase ${showStyleBaseId} variant has failed blueprint config validation`, + details + ) + } + + const showStyle = showStyleVariantFrom(apiShowStyleVariant, showStyleVariantId) if (!showStyle) throw new Meteor.Error(400, `Invalid ShowStyleVariant`) const existingShowStyle = await ShowStyleVariants.findOneAsync(showStyleVariantId) @@ -335,6 +477,37 @@ export function registerRoutes(registerRoute: APIRegisterHook } ) + registerRoute<{ showStyleBaseId: string }, never, object>( + 'get', + '/showstyles/:showStyleBaseId/config', + new Map([[404, [UserErrorMessage.ShowStyleBaseNotFound]]]), + showStylesAPIFactory, + async (serverAPI, connection, event, params, _) => { + const showStyleBaseId = protectString(params.showStyleBaseId) + logger.info(`API GET: ShowStyleBase config ${showStyleBaseId}`) + + check(showStyleBaseId, String) + return await serverAPI.getShowStyleConfig(connection, event, showStyleBaseId) + } + ) + + registerRoute<{ showStyleBaseId: string }, object, void>( + 'put', + '/showstyles/:showStyleBaseId/config', + new Map([ + [404, [UserErrorMessage.ShowStyleBaseNotFound]], + [409, [UserErrorMessage.ValidationFailed]], + ]), + showStylesAPIFactory, + async (serverAPI, connection, event, params, body) => { + const showStyleBaseId = protectString(params.showStyleBaseId) + logger.info(`API PUT: Update ShowStyleBase config ${showStyleBaseId}`) + + check(showStyleBaseId, String) + return await serverAPI.updateShowStyleConfig(connection, event, showStyleBaseId, body) + } + ) + registerRoute<{ showStyleBaseId: string }, never, void>( 'delete', '/showstyles/:showStyleBaseId', diff --git a/meteor/server/api/rest/v1/studios.ts b/meteor/server/api/rest/v1/studios.ts index 9c5cfb3096..b33379a769 100644 --- a/meteor/server/api/rest/v1/studios.ts +++ b/meteor/server/api/rest/v1/studios.ts @@ -8,7 +8,7 @@ import { APIStudio, StudioAction, StudioActionType, StudiosRestAPI } from '../.. import { Meteor } from 'meteor/meteor' import { ClientAPI } from '../../../../lib/api/client' import { PeripheralDevices, RundownPlaylists, Studios } from '../../../collections' -import { APIStudioFrom, studioFrom } from './typeConversion' +import { APIStudioFrom, studioFrom, validateAPIBlueprintConfigForStudio } from './typeConversion' import { runUpgradeForStudio, validateConfigForStudio } from '../../../migration/upgrades' import { NoteSeverity } from '@sofie-automation/blueprints-integration' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' @@ -35,9 +35,24 @@ class StudiosServerAPI implements StudiosRestAPI { async addStudio( _connection: Meteor.Connection, _event: string, - studio: APIStudio + apiStudio: APIStudio ): Promise> { - const newStudio = await studioFrom(studio) + const blueprintConfigValidation = await validateAPIBlueprintConfigForStudio(apiStudio) + const blueprintConfigValidationOK = blueprintConfigValidation.reduce( + (acc, msg) => acc && msg.level === NoteSeverity.INFO, + true + ) + if (!blueprintConfigValidationOK) { + const details = JSON.stringify( + blueprintConfigValidation.filter((msg) => msg.level < NoteSeverity.INFO).map((msg) => msg.message.key), + null, + 2 + ) + logger.error(`addStudio failed blueprint config validation with errors: ${details}`) + throw new Meteor.Error(409, `Studio has failed blueprint config validation`, details) + } + + const newStudio = await studioFrom(apiStudio) if (!newStudio) throw new Meteor.Error(400, `Invalid Studio`) const newStudioId = await Studios.insertAsync(newStudio) @@ -66,16 +81,31 @@ class StudiosServerAPI implements StudiosRestAPI { const studio = await Studios.findOneAsync(studioId) if (!studio) throw new Meteor.Error(404, `Studio ${studioId} not found`) - return ClientAPI.responseSuccess(APIStudioFrom(studio)) + return ClientAPI.responseSuccess(await APIStudioFrom(studio)) } async addOrUpdateStudio( _connection: Meteor.Connection, _event: string, studioId: StudioId, - studio: APIStudio + apiStudio: APIStudio ): Promise> { - const newStudio = await studioFrom(studio, studioId) + const blueprintConfigValidation = await validateAPIBlueprintConfigForStudio(apiStudio) + const blueprintConfigValidationOK = blueprintConfigValidation.reduce( + (acc, msg) => acc && msg.level === NoteSeverity.INFO, + true + ) + if (!blueprintConfigValidationOK) { + const details = JSON.stringify( + blueprintConfigValidation.filter((msg) => msg.level < NoteSeverity.INFO).map((msg) => msg.message.key), + null, + 2 + ) + logger.error(`addOrUpdateStudio failed blueprint config validation with errors: ${details}`) + throw new Meteor.Error(409, `Studio ${studioId} has failed blueprint config validation`, details) + } + + const newStudio = await studioFrom(apiStudio, studioId) if (!newStudio) throw new Meteor.Error(400, `Invalid Studio`) const existingStudio = await Studios.findOneAsync(studioId) @@ -107,7 +137,77 @@ class StudiosServerAPI implements StudiosRestAPI { throw new Meteor.Error(409, `Studio ${studioId} has failed validation`, details) } - return ClientAPI.responseSuccess(await runUpgradeForStudio(studioId)) + return ClientAPI.responseSuccess( + await new Promise((resolve) => + // wait for the upsert to complete before upgrade + setTimeout(async () => resolve(await runUpgradeForStudio(studioId)), 200) + ) + ) + } + + async getStudioConfig( + _connection: Meteor.Connection, + _event: string, + studioId: StudioId + ): Promise> { + const studio = await Studios.findOneAsync(studioId) + if (!studio) throw new Meteor.Error(404, `Studio ${studioId} not found`) + + return ClientAPI.responseSuccess((await APIStudioFrom(studio)).config) + } + + async updateStudioConfig( + _connection: Meteor.Connection, + _event: string, + studioId: StudioId, + config: object + ): Promise> { + const existingStudio = await Studios.findOneAsync(studioId) + if (!existingStudio) { + throw new Meteor.Error(404, `Studio ${studioId} not found`) + } + + const apiStudio = await APIStudioFrom(existingStudio) + apiStudio.config = config + + const blueprintConfigValidation = await validateAPIBlueprintConfigForStudio(apiStudio) + const blueprintConfigValidationOK = blueprintConfigValidation.reduce( + (acc, msg) => acc && msg.level === NoteSeverity.INFO, + true + ) + if (!blueprintConfigValidationOK) { + const details = JSON.stringify( + blueprintConfigValidation.filter((msg) => msg.level < NoteSeverity.INFO).map((msg) => msg.message.key), + null, + 2 + ) + logger.error(`updateStudioConfig failed blueprint config validation with errors: ${details}`) + throw new Meteor.Error(409, `Studio ${studioId} has failed blueprint config validation`, details) + } + + const newStudio = await studioFrom(apiStudio, studioId) + if (!newStudio) throw new Meteor.Error(400, `Invalid Studio`) + + await Studios.upsertAsync(studioId, newStudio) + + const validation = await validateConfigForStudio(studioId) + const validateOK = validation.messages.reduce((acc, msg) => acc && msg.level === NoteSeverity.INFO, true) + if (!validateOK) { + const details = JSON.stringify( + validation.messages.filter((msg) => msg.level < NoteSeverity.INFO).map((msg) => msg.message.key), + null, + 2 + ) + logger.error(`updateStudioConfig failed validation with errors: ${details}`) + throw new Meteor.Error(409, `Studio ${studioId} has failed validation`, details) + } + + return ClientAPI.responseSuccess( + await new Promise((resolve) => + // wait for the upsert to complete before upgrade + setTimeout(async () => resolve(await runUpgradeForStudio(studioId)), 200) + ) + ) } async deleteStudio( @@ -337,6 +437,37 @@ export function registerRoutes(registerRoute: APIRegisterHook): } ) + registerRoute<{ studioId: string }, never, object>( + 'get', + '/studios/:studioId/config', + new Map([[404, [UserErrorMessage.StudioNotFound]]]), + studiosAPIFactory, + async (serverAPI, connection, event, params, _) => { + const studioId = protectString(params.studioId) + logger.info(`API GET: studio config ${studioId}`) + + check(studioId, String) + return await serverAPI.getStudioConfig(connection, event, studioId) + } + ) + + registerRoute<{ studioId: string }, object, void>( + 'put', + '/studios/:studioId/config', + new Map([ + [404, [UserErrorMessage.StudioNotFound]], + [409, [UserErrorMessage.ValidationFailed]], + ]), + studiosAPIFactory, + async (serverAPI, connection, event, params, body) => { + const studioId = protectString(params.studioId) + logger.info(`API PUT: Update studio config ${studioId}`) + + check(studioId, String) + return await serverAPI.updateStudioConfig(connection, event, studioId, body) + } + ) + registerRoute<{ studioId: string }, never, void>( 'delete', '/studios/:studioId', diff --git a/meteor/server/api/rest/v1/typeConversion.ts b/meteor/server/api/rest/v1/typeConversion.ts index fbc1d7a870..c7eda619f4 100644 --- a/meteor/server/api/rest/v1/typeConversion.ts +++ b/meteor/server/api/rest/v1/typeConversion.ts @@ -1,14 +1,23 @@ import { BlueprintManifestType, IBlueprintConfig, + IConfigMessage, IOutputLayer, ISourceLayer, + ShowStyleBlueprintManifest, SourceLayerType, StatusCode, + StudioBlueprintManifest, } from '@sofie-automation/blueprints-integration' import { PeripheralDevice, PeripheralDeviceType } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' import { Blueprint } from '@sofie-automation/corelib/dist/dataModel/Blueprint' -import { BucketId, ShowStyleBaseId, ShowStyleVariantId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { + BlueprintId, + BucketId, + ShowStyleBaseId, + ShowStyleVariantId, + StudioId, +} from '@sofie-automation/corelib/dist/dataModel/Ids' import { DBStudio, IStudioSettings } from '@sofie-automation/corelib/dist/dataModel/Studio' import { assertNever, getRandomId, literal } from '@sofie-automation/corelib/dist/lib' import { protectString, unprotectString } from '@sofie-automation/corelib/dist/protectedString' @@ -17,6 +26,8 @@ import { ObjectOverrideSetOp, wrapDefaultObject, updateOverrides, + convertObjectIntoOverrides, + ObjectWithOverrides, } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' import { APIBlueprint, @@ -33,6 +44,10 @@ import { import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' import { DBShowStyleVariant } from '@sofie-automation/corelib/dist/dataModel/ShowStyleVariant' import { Blueprints, ShowStyleBases, Studios } from '../../../collections' +import { Meteor } from 'meteor/meteor' +import { evalBlueprint } from '../../blueprints/cache' +import { CommonContext } from '../../../migration/upgrades/context' +import { logger } from '../../../logging' import { DEFAULT_MINIMUM_TAKE_SPAN } from '@sofie-automation/shared-lib/dist/core/constants' import { Bucket } from '../../../../lib/collections/Buckets' @@ -68,9 +83,20 @@ export async function showStyleBaseFrom( ? updateOverrides(showStyleBase.sourceLayersWithOverrides, newSourceLayers) : wrapDefaultObject({}) - const blueprintConfig = showStyleBase - ? updateOverrides(showStyleBase.blueprintConfigWithOverrides, apiShowStyleBase.config as IBlueprintConfig) - : wrapDefaultObject({}) + const blueprintManifest = evalBlueprint(blueprint) as ShowStyleBlueprintManifest + let blueprintConfig: ObjectWithOverrides + if (typeof blueprintManifest.blueprintConfigFromAPI !== 'function') { + blueprintConfig = showStyleBase + ? updateOverrides(showStyleBase.blueprintConfigWithOverrides, apiShowStyleBase.config as IBlueprintConfig) + : wrapDefaultObject({}) + } else { + blueprintConfig = showStyleBase + ? updateOverrides( + showStyleBase.blueprintConfigWithOverrides, + await ShowStyleBaseBlueprintConfigFromAPI(apiShowStyleBase, blueprintManifest) + ) + : convertObjectIntoOverrides(await ShowStyleBaseBlueprintConfigFromAPI(apiShowStyleBase, blueprintManifest)) + } return { _id: existingId ?? getRandomId(), @@ -87,7 +113,7 @@ export async function showStyleBaseFrom( } } -export function APIShowStyleBaseFrom(showStyleBase: DBShowStyleBase): APIShowStyleBase { +export async function APIShowStyleBaseFrom(showStyleBase: DBShowStyleBase): Promise { return { name: showStyleBase.name, blueprintId: unprotectString(showStyleBase.blueprintId), @@ -98,7 +124,7 @@ export function APIShowStyleBaseFrom(showStyleBase: DBShowStyleBase): APIShowSty sourceLayers: Object.values( applyAndValidateOverrides(showStyleBase.sourceLayersWithOverrides).obj ).map((layer) => APISourceLayerFrom(layer!)), - config: applyAndValidateOverrides(showStyleBase.blueprintConfigWithOverrides).obj, + config: await APIShowStyleBlueprintConfigFrom(showStyleBase, showStyleBase.blueprintId), } } @@ -124,12 +150,16 @@ export function showStyleVariantFrom( } } -export function APIShowStyleVariantFrom(showStyleVariant: DBShowStyleVariant): APIShowStyleVariant { +export async function APIShowStyleVariantFrom( + showStyleBase: DBShowStyleBase, + showStyleVariant: DBShowStyleVariant +): Promise { return { name: showStyleVariant.name, rank: showStyleVariant._rank, showStyleBaseId: unprotectString(showStyleVariant.showStyleBaseId), - config: applyAndValidateOverrides(showStyleVariant.blueprintConfigWithOverrides).obj, + blueprintConfigPresetId: showStyleVariant.blueprintConfigPresetId, + config: await APIShowStyleBlueprintConfigFrom(showStyleVariant, showStyleBase.blueprintId), } } @@ -251,16 +281,27 @@ export async function studioFrom(apiStudio: APIStudio, existingId?: StudioId): P let blueprint: Blueprint | undefined if (apiStudio.blueprintId) { blueprint = await Blueprints.findOneAsync(protectString(apiStudio.blueprintId)) - if (!blueprint) return undefined - if (blueprint.blueprintType !== BlueprintManifestType.STUDIO) return undefined + if (blueprint?.blueprintType !== BlueprintManifestType.STUDIO) return undefined } + if (!blueprint) return undefined let studio: DBStudio | undefined if (existingId) studio = await Studios.findOneAsync(existingId) - const blueprintConfig = studio - ? updateOverrides(studio.blueprintConfigWithOverrides, apiStudio.config as IBlueprintConfig) - : wrapDefaultObject({}) + const blueprintManifest = evalBlueprint(blueprint) as StudioBlueprintManifest + let blueprintConfig: ObjectWithOverrides + if (typeof blueprintManifest.blueprintConfigFromAPI !== 'function') { + blueprintConfig = studio + ? updateOverrides(studio.blueprintConfigWithOverrides, apiStudio.config as IBlueprintConfig) + : wrapDefaultObject({}) + } else { + blueprintConfig = studio + ? updateOverrides( + studio.blueprintConfigWithOverrides, + await StudioBlueprintConfigFromAPI(apiStudio, blueprintManifest) + ) + : convertObjectIntoOverrides(await StudioBlueprintConfigFromAPI(apiStudio, blueprintManifest)) + } return { _id: existingId ?? getRandomId(), @@ -288,14 +329,14 @@ export async function studioFrom(apiStudio: APIStudio, existingId?: StudioId): P } } -export function APIStudioFrom(studio: DBStudio): APIStudio { +export async function APIStudioFrom(studio: DBStudio): Promise { const studioSettings = APIStudioSettingsFrom(studio.settings) return { name: studio.name, blueprintId: unprotectString(studio.blueprintId), blueprintConfigPresetId: studio.blueprintConfigPresetId, - config: applyAndValidateOverrides(studio.blueprintConfigWithOverrides).obj, + config: await APIStudioBlueprintConfigFrom(studio), settings: studioSettings, supportedShowStyleBase: studio.supportedShowStyleBase.map((id) => unprotectString(id)), } @@ -418,6 +459,142 @@ export function APIOutputLayerFrom(outputLayer: IOutputLayer): APIOutputLayer { } } +async function getBlueprint( + blueprintId: BlueprintId | undefined, + blueprintType: BlueprintManifestType +): Promise { + const blueprint = blueprintId + ? await Blueprints.findOneAsync({ + _id: blueprintId, + blueprintType, + }) + : undefined + if (!blueprint) throw new Meteor.Error(404, `Blueprint "${blueprintId}" not found!`) + + if (!blueprint.blueprintHash) throw new Meteor.Error(500, 'Blueprint is not valid') + + return blueprint +} + +export async function validateAPIBlueprintConfigForShowStyle( + apiShowStyle: APIShowStyleBase | APIShowStyleVariant, + blueprintId: BlueprintId +): Promise> { + if (!apiShowStyle.blueprintConfigPresetId) + throw new Meteor.Error(500, `ShowStyle ${apiShowStyle.name} is missing config preset`) + const blueprint = await getBlueprint(blueprintId, BlueprintManifestType.SHOWSTYLE) + const blueprintManifest = evalBlueprint(blueprint) as ShowStyleBlueprintManifest + + if (typeof blueprintManifest.validateConfigFromAPI !== 'function') { + logger.info(`Blueprint ${blueprintManifest.blueprintId} does not support Config validation`) + return [] + } + + const blueprintContext = new CommonContext( + 'validateAPIBlueprintConfig', + `showStyle:${apiShowStyle.name},blueprint:${blueprint._id}` + ) + + return blueprintManifest.validateConfigFromAPI(blueprintContext, apiShowStyle.config) +} + +export async function ShowStyleBaseBlueprintConfigFromAPI( + apiShowStyleBase: APIShowStyleBase, + blueprintManifest: ShowStyleBlueprintManifest +): Promise { + if (!apiShowStyleBase.blueprintConfigPresetId) + throw new Meteor.Error(500, `ShowStyleBase ${apiShowStyleBase.name} is missing config preset`) + + if (typeof blueprintManifest.blueprintConfigFromAPI !== 'function') + throw new Meteor.Error(500, `Blueprint ${blueprintManifest.blueprintId} does not support this config flow`) + + const blueprintContext = new CommonContext( + 'BlueprintConfigFromAPI', + `showStyleBase:${apiShowStyleBase.name},blueprint:${blueprintManifest.blueprintId}` + ) + + return blueprintManifest.blueprintConfigFromAPI(blueprintContext, apiShowStyleBase.config) +} + +export async function APIShowStyleBlueprintConfigFrom( + showStyle: DBShowStyleBase | DBShowStyleVariant, + blueprintId: BlueprintId +): Promise { + if (!showStyle.blueprintConfigPresetId) + throw new Meteor.Error(500, `ShowStyle ${showStyle._id} is missing config preset`) + const blueprint = await getBlueprint(blueprintId, BlueprintManifestType.SHOWSTYLE) + const blueprintManifest = evalBlueprint(blueprint) as ShowStyleBlueprintManifest + + if (typeof blueprintManifest.blueprintConfigToAPI !== 'function') + return applyAndValidateOverrides(showStyle.blueprintConfigWithOverrides).obj + + const blueprintContext = new CommonContext( + 'APIShowStyleBlueprintConfigFrom', + `showStyleBase:${showStyle._id},blueprint:${blueprint._id}` + ) + + return blueprintManifest.blueprintConfigToAPI( + blueprintContext, + applyAndValidateOverrides(showStyle.blueprintConfigWithOverrides).obj + ) +} + +export async function validateAPIBlueprintConfigForStudio(apiStudio: APIStudio): Promise> { + if (!apiStudio.blueprintConfigPresetId) + throw new Meteor.Error(500, `Studio ${apiStudio.name} is missing config preset`) + const blueprint = await getBlueprint(protectString(apiStudio.blueprintId), BlueprintManifestType.STUDIO) + const blueprintManifest = evalBlueprint(blueprint) as StudioBlueprintManifest + + if (typeof blueprintManifest.validateConfigFromAPI !== 'function') { + logger.info(`Blueprint ${blueprintManifest.blueprintId} does not support Config validation`) + return [] + } + + const blueprintContext = new CommonContext( + 'validateAPIBlueprintConfig', + `studio:${apiStudio.name},blueprint:${blueprint._id}` + ) + + return blueprintManifest.validateConfigFromAPI(blueprintContext, apiStudio.config) +} + +export async function StudioBlueprintConfigFromAPI( + apiStudio: APIStudio, + blueprintManifest: StudioBlueprintManifest +): Promise { + if (!apiStudio.blueprintConfigPresetId) + throw new Meteor.Error(500, `Studio ${apiStudio.name} is missing config preset`) + + if (typeof blueprintManifest.blueprintConfigFromAPI !== 'function') + throw new Meteor.Error(500, `Blueprint ${blueprintManifest.blueprintId} does not support this config flow`) + + const blueprintContext = new CommonContext( + 'BlueprintConfigFromAPI', + `studio:${apiStudio.name},blueprint:${blueprintManifest.blueprintId}` + ) + + return blueprintManifest.blueprintConfigFromAPI(blueprintContext, apiStudio.config) +} + +export async function APIStudioBlueprintConfigFrom(studio: DBStudio): Promise { + if (!studio.blueprintConfigPresetId) throw new Meteor.Error(500, `Studio ${studio._id} is missing config preset`) + const blueprint = await getBlueprint(studio.blueprintId, BlueprintManifestType.STUDIO) + const blueprintManifest = evalBlueprint(blueprint) as StudioBlueprintManifest + + if (typeof blueprintManifest.blueprintConfigToAPI !== 'function') + return applyAndValidateOverrides(studio.blueprintConfigWithOverrides).obj + + const blueprintContext = new CommonContext( + 'APIStudioBlueprintConfigFrom', + `studio:${studio.name},blueprint:${blueprint._id}` + ) + + return blueprintManifest.blueprintConfigToAPI( + blueprintContext, + applyAndValidateOverrides(studio.blueprintConfigWithOverrides).obj + ) +} + export function bucketFrom(apiBucket: APIBucket, existingId?: BucketId): Bucket { return { _id: existingId ?? getRandomId(), diff --git a/packages/blueprints-integration/src/api/showStyle.ts b/packages/blueprints-integration/src/api/showStyle.ts index 52ec4559fd..30d2a154bd 100644 --- a/packages/blueprints-integration/src/api/showStyle.ts +++ b/packages/blueprints-integration/src/api/showStyle.ts @@ -169,6 +169,17 @@ export interface ShowStyleBlueprintManifest TProcessedConfig + /** + * Validate the blueprint config passed to this blueprint according to the API schema, returning a list of messages to display to the user. + */ + validateConfigFromAPI?: (context: ICommonContext, apiConfig: object) => Array + + /** transform API blueprint config to the database format */ + blueprintConfigFromAPI?: (context: ICommonContext, config: object) => TRawConfig + + /** transform blueprint config to the API format */ + blueprintConfigToAPI?: (context: ICommonContext, config: TRawConfig) => object + // Events onRundownActivate?: (context: IRundownActivationContext, wasActive: boolean) => Promise diff --git a/packages/blueprints-integration/src/api/studio.ts b/packages/blueprints-integration/src/api/studio.ts index a4be296f26..d295e088e2 100644 --- a/packages/blueprints-integration/src/api/studio.ts +++ b/packages/blueprints-integration/src/api/studio.ts @@ -75,6 +75,15 @@ export interface StudioBlueprintManifest TProcessedConfig + + /** Validate the blueprint config passed to this blueprint according to the API schema, returning a list of messages to display to the user. */ + validateConfigFromAPI?: (context: ICommonContext, apiConfig: object) => Array + + /** transform API blueprint config to the database format */ + blueprintConfigFromAPI?: (context: ICommonContext, config: object) => IBlueprintConfig + + /** transform blueprint config to the API format */ + blueprintConfigToAPI?: (context: ICommonContext, config: TRawConfig) => object } export interface BlueprintResultStudioBaseline {