Skip to content

Commit ecc1768

Browse files
authored
Merge pull request Sofie-Automation#1223 from evs-broadcast/feat/blueprint-config-validation
Support for schema validation of blueprintConfiguration objects
2 parents db9bd7d + e245b15 commit ecc1768

File tree

13 files changed

+773
-62
lines changed

13 files changed

+773
-62
lines changed

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import KoaRouter from '@koa/router'
22
import { interpollateTranslation, translateMessage } from '@sofie-automation/corelib/dist/TranslatableMessage'
33
import { UserError, UserErrorMessage } from '@sofie-automation/corelib/dist/error'
4+
import { IConfigMessage, NoteSeverity } from '@sofie-automation/blueprints-integration'
45
import Koa from 'koa'
56
import bodyParser from 'koa-bodyparser'
67
import { Meteor } from 'meteor/meteor'
@@ -92,6 +93,30 @@ function extractErrorDetails(e: unknown): string[] | undefined {
9293
}
9394
}
9495

96+
export const checkValidation = (method: string, configValidationMsgs: IConfigMessage[]): void => {
97+
/**
98+
* Throws if any of the configValidationMsgs indicates that the config has errors.
99+
* Will log any messages with severity WARNING or INFO
100+
*/
101+
const configValidationOK = configValidationMsgs.reduce((acc, msg) => acc && msg.level !== NoteSeverity.ERROR, true)
102+
if (!configValidationOK) {
103+
const details = JSON.stringify(
104+
configValidationMsgs.filter((msg) => msg.level === NoteSeverity.ERROR).map((msg) => msg.message.key),
105+
null,
106+
2
107+
)
108+
logger.error(`${method} failed blueprint config validation with errors: ${details}`)
109+
throw new Meteor.Error(409, `${method} has failed blueprint config validation`, details)
110+
} else {
111+
const details = JSON.stringify(
112+
configValidationMsgs.map((msg) => msg.message.key),
113+
null,
114+
2
115+
)
116+
logger.info(`${method} received messages from bluepring config validation: ${details}`)
117+
}
118+
}
119+
95120
interface APIRequestError {
96121
status: number
97122
message: string

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

Lines changed: 125 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,14 @@ import {
2020
APIShowStyleVariantFrom,
2121
showStyleBaseFrom,
2222
showStyleVariantFrom,
23+
validateAPIBlueprintConfigForShowStyle,
2324
} from './typeConversion'
2425
import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown'
2526
import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist'
2627
import { runUpgradeForShowStyleBase, validateConfigForShowStyleBase } from '../../../migration/upgrades'
27-
import { NoteSeverity } from '@sofie-automation/blueprints-integration'
2828
import { DBShowStyleVariant } from '@sofie-automation/corelib/dist/dataModel/ShowStyleVariant'
2929
import { assertNever } from '@sofie-automation/corelib/dist/lib'
30+
import { checkValidation } from '.'
3031

3132
class ShowStylesServerAPI implements ShowStylesRestAPI {
3233
async getShowStyleBases(
@@ -42,9 +43,15 @@ class ShowStylesServerAPI implements ShowStylesRestAPI {
4243
async addShowStyleBase(
4344
_connection: Meteor.Connection,
4445
_event: string,
45-
showStyleBase: APIShowStyleBase
46+
apiShowStyleBase: APIShowStyleBase
4647
): Promise<ClientAPI.ClientResponse<string>> {
47-
const showStyle = await showStyleBaseFrom(showStyleBase)
48+
const blueprintConfigValidation = await validateAPIBlueprintConfigForShowStyle(
49+
apiShowStyleBase,
50+
protectString(apiShowStyleBase.blueprintId)
51+
)
52+
checkValidation(`addShowStyleBase`, blueprintConfigValidation)
53+
54+
const showStyle = await showStyleBaseFrom(apiShowStyleBase)
4855
if (!showStyle) throw new Meteor.Error(400, `Invalid ShowStyleBase`)
4956
const showStyleId = showStyle._id
5057
await ShowStyleBases.insertAsync(showStyle)
@@ -60,16 +67,22 @@ class ShowStylesServerAPI implements ShowStylesRestAPI {
6067
const showStyleBase = await ShowStyleBases.findOneAsync(showStyleBaseId)
6168
if (!showStyleBase) throw new Meteor.Error(404, `ShowStyleBase ${showStyleBaseId} does not exist`)
6269

63-
return ClientAPI.responseSuccess(APIShowStyleBaseFrom(showStyleBase))
70+
return ClientAPI.responseSuccess(await APIShowStyleBaseFrom(showStyleBase))
6471
}
6572

6673
async addOrUpdateShowStyleBase(
6774
_connection: Meteor.Connection,
6875
_event: string,
6976
showStyleBaseId: ShowStyleBaseId,
70-
showStyleBase: APIShowStyleBase
77+
apiShowStyleBase: APIShowStyleBase
7178
): Promise<ClientAPI.ClientResponse<void>> {
72-
const showStyle = await showStyleBaseFrom(showStyleBase, showStyleBaseId)
79+
const blueprintConfigValidation = await validateAPIBlueprintConfigForShowStyle(
80+
apiShowStyleBase,
81+
protectString(apiShowStyleBase.blueprintId)
82+
)
83+
checkValidation(`addOrUpdateShowStyleBase ${showStyleBaseId}`, blueprintConfigValidation)
84+
85+
const showStyle = await showStyleBaseFrom(apiShowStyleBase, showStyleBaseId)
7386
if (!showStyle) throw new Meteor.Error(400, `Invalid ShowStyleBase`)
7487

7588
const existingShowStyle = await ShowStyleBases.findOneAsync(showStyleBaseId)
@@ -96,17 +109,73 @@ class ShowStylesServerAPI implements ShowStylesRestAPI {
96109

97110
await ShowStyleBases.upsertAsync(showStyleBaseId, showStyle)
98111

112+
// wait for the upsert to complete before validation and upgrade read from the showStyleBases collection
113+
await new Promise<void>((resolve) => setTimeout(() => resolve(), 200))
114+
99115
const validation = await validateConfigForShowStyleBase(showStyleBaseId)
100-
const validateOK = validation.messages.reduce((acc, msg) => acc && msg.level === NoteSeverity.INFO, true)
101-
if (!validateOK) {
102-
const details = JSON.stringify(
103-
validation.messages.filter((msg) => msg.level < NoteSeverity.INFO).map((msg) => msg.message.key),
104-
null,
105-
2
106-
)
107-
logger.error(`addOrUpdateShowStyleBase failed validation with errors: ${details}`)
108-
throw new Meteor.Error(409, `ShowStyleBase ${showStyleBaseId} has failed validation`, details)
109-
}
116+
checkValidation(`addOrUpdateShowStyleBase ${showStyleBaseId}`, validation.messages)
117+
118+
return ClientAPI.responseSuccess(await runUpgradeForShowStyleBase(showStyleBaseId))
119+
}
120+
121+
async getShowStyleConfig(
122+
_connection: Meteor.Connection,
123+
_event: string,
124+
showStyleBaseId: ShowStyleBaseId
125+
): Promise<ClientAPI.ClientResponse<object>> {
126+
const showStyleBase = await ShowStyleBases.findOneAsync(showStyleBaseId)
127+
if (!showStyleBase) throw new Meteor.Error(404, `ShowStyleBase ${showStyleBaseId} does not exist`)
128+
129+
return ClientAPI.responseSuccess((await APIShowStyleBaseFrom(showStyleBase)).config)
130+
}
131+
132+
async updateShowStyleConfig(
133+
_connection: Meteor.Connection,
134+
_event: string,
135+
showStyleBaseId: ShowStyleBaseId,
136+
config: object
137+
): Promise<ClientAPI.ClientResponse<void>> {
138+
const existingShowStyleBase = await ShowStyleBases.findOneAsync(showStyleBaseId)
139+
if (existingShowStyleBase) {
140+
const rundowns = (await Rundowns.findFetchAsync(
141+
{ showStyleBaseId },
142+
{ projection: { playlistId: 1 } }
143+
)) as Array<Pick<Rundown, 'playlistId'>>
144+
const playlists = (await RundownPlaylists.findFetchAsync(
145+
{ _id: { $in: rundowns.map((r) => r.playlistId) } },
146+
{
147+
projection: {
148+
activationId: 1,
149+
},
150+
}
151+
)) as Array<Pick<DBRundownPlaylist, 'activationId'>>
152+
if (playlists.some((playlist) => playlist.activationId !== undefined)) {
153+
throw new Meteor.Error(
154+
412,
155+
`Cannot update ShowStyleBase ${showStyleBaseId} as it is in use by an active Playlist`
156+
)
157+
}
158+
} else throw new Meteor.Error(404, `ShowStyleBase ${showStyleBaseId} not found`)
159+
160+
const apiShowStyleBase = await APIShowStyleBaseFrom(existingShowStyleBase)
161+
apiShowStyleBase.config = config
162+
163+
const blueprintConfigValidation = await validateAPIBlueprintConfigForShowStyle(
164+
apiShowStyleBase,
165+
protectString(apiShowStyleBase.blueprintId)
166+
)
167+
checkValidation(`updateShowStyleConfig ${showStyleBaseId}`, blueprintConfigValidation)
168+
169+
const showStyle = await showStyleBaseFrom(apiShowStyleBase, showStyleBaseId)
170+
if (!showStyle) throw new Meteor.Error(400, `Invalid ShowStyleBase`)
171+
172+
await ShowStyleBases.upsertAsync(showStyleBaseId, showStyle)
173+
174+
// wait for the upsert to complete before validation and upgrade read from the showStyleBases collection
175+
await new Promise<void>((resolve) => setTimeout(() => resolve(), 200))
176+
177+
const validation = await validateConfigForShowStyleBase(showStyleBaseId)
178+
checkValidation(`updateShowStyleConfig ${showStyleBaseId}`, validation.messages)
110179

111180
return ClientAPI.responseSuccess(await runUpgradeForShowStyleBase(showStyleBaseId))
112181
}
@@ -185,20 +254,26 @@ class ShowStylesServerAPI implements ShowStylesRestAPI {
185254
const variant = await ShowStyleVariants.findOneAsync(showStyleVariantId)
186255
if (!variant) throw new Meteor.Error(404, `ShowStyleVariant ${showStyleVariantId} not found`)
187256

188-
return ClientAPI.responseSuccess(APIShowStyleVariantFrom(variant))
257+
return ClientAPI.responseSuccess(await APIShowStyleVariantFrom(showStyleBase, variant))
189258
}
190259

191260
async addOrUpdateShowStyleVariant(
192261
_connection: Meteor.Connection,
193262
_event: string,
194263
showStyleBaseId: ShowStyleBaseId,
195264
showStyleVariantId: ShowStyleVariantId,
196-
showStyleVariant: APIShowStyleVariant
265+
apiShowStyleVariant: APIShowStyleVariant
197266
): Promise<ClientAPI.ClientResponse<void>> {
198267
const showStyleBase = await ShowStyleBases.findOneAsync(showStyleBaseId)
199268
if (!showStyleBase) throw new Meteor.Error(404, `ShowStyleBase ${showStyleBaseId} does not exist`)
200269

201-
const showStyle = showStyleVariantFrom(showStyleVariant, showStyleVariantId)
270+
const blueprintConfigValidation = await validateAPIBlueprintConfigForShowStyle(
271+
apiShowStyleVariant,
272+
showStyleBase.blueprintId
273+
)
274+
checkValidation(`addOrUpdateShowStyleVariant ${showStyleVariantId}`, blueprintConfigValidation)
275+
276+
const showStyle = showStyleVariantFrom(apiShowStyleVariant, showStyleVariantId)
202277
if (!showStyle) throw new Meteor.Error(400, `Invalid ShowStyleVariant`)
203278

204279
const existingShowStyle = await ShowStyleVariants.findOneAsync(showStyleVariantId)
@@ -335,6 +410,37 @@ export function registerRoutes(registerRoute: APIRegisterHook<ShowStylesRestAPI>
335410
}
336411
)
337412

413+
registerRoute<{ showStyleBaseId: string }, never, object>(
414+
'get',
415+
'/showstyles/:showStyleBaseId/config',
416+
new Map([[404, [UserErrorMessage.ShowStyleBaseNotFound]]]),
417+
showStylesAPIFactory,
418+
async (serverAPI, connection, event, params, _) => {
419+
const showStyleBaseId = protectString<ShowStyleBaseId>(params.showStyleBaseId)
420+
logger.info(`API GET: ShowStyleBase config ${showStyleBaseId}`)
421+
422+
check(showStyleBaseId, String)
423+
return await serverAPI.getShowStyleConfig(connection, event, showStyleBaseId)
424+
}
425+
)
426+
427+
registerRoute<{ showStyleBaseId: string }, object, void>(
428+
'put',
429+
'/showstyles/:showStyleBaseId/config',
430+
new Map([
431+
[404, [UserErrorMessage.ShowStyleBaseNotFound]],
432+
[409, [UserErrorMessage.ValidationFailed]],
433+
]),
434+
showStylesAPIFactory,
435+
async (serverAPI, connection, event, params, body) => {
436+
const showStyleBaseId = protectString<ShowStyleBaseId>(params.showStyleBaseId)
437+
logger.info(`API PUT: Update ShowStyleBase config ${showStyleBaseId}`)
438+
439+
check(showStyleBaseId, String)
440+
return await serverAPI.updateShowStyleConfig(connection, event, showStyleBaseId, body)
441+
}
442+
)
443+
338444
registerRoute<{ showStyleBaseId: string }, never, void>(
339445
'delete',
340446
'/showstyles/:showStyleBaseId',

0 commit comments

Comments
 (0)