diff --git a/meteor/server/api/rest/v1/playlists.ts b/meteor/server/api/rest/v1/playlists.ts index 2616f2e6f9..b082740841 100644 --- a/meteor/server/api/rest/v1/playlists.ts +++ b/meteor/server/api/rest/v1/playlists.ts @@ -201,7 +201,7 @@ class PlaylistsServerAPI implements PlaylistsRestAPI { ) ) - return ServerClientAPI.runUserActionInLogForPlaylistOnWorker( + const result = await ServerClientAPI.runUserActionInLogForPlaylistOnWorker( this.context.getMethodContext(connection), event, getCurrentTime(), @@ -220,6 +220,8 @@ class PlaylistsServerAPI implements PlaylistsRestAPI { actionOptions: adLibOptions, } ) + if (ClientAPI.isClientResponseError(result)) return result + return ClientAPI.responseSuccess(result.result ?? {}) } else { return ClientAPI.responseError( UserError.from(new Error(`No adLib with Id ${adLibId}`), UserErrorMessage.AdlibNotFound, undefined, 412) @@ -268,7 +270,7 @@ class PlaylistsServerAPI implements PlaylistsRestAPI { ) } - return ServerClientAPI.runUserActionInLogForPlaylistOnWorker( + const result = await ServerClientAPI.runUserActionInLogForPlaylistOnWorker( this.context.getMethodContext(connection), event, getCurrentTime(), @@ -286,6 +288,8 @@ class PlaylistsServerAPI implements PlaylistsRestAPI { triggerMode: triggerMode ?? undefined, } ) + if (ClientAPI.isClientResponseError(result)) return result + return ClientAPI.responseSuccess(result.result ?? {}) } async moveNextPart( connection: Meteor.Connection, @@ -602,6 +606,7 @@ export function registerRoutes(registerRoute: APIRegisterHook) 'post', '/playlists/:playlistId/execute-adlib', new Map([ + [400, []], [404, [UserErrorMessage.RundownPlaylistNotFound]], [412, [UserErrorMessage.InactiveRundown, UserErrorMessage.NoCurrentPart, UserErrorMessage.AdlibNotFound]], ]), @@ -638,6 +643,7 @@ export function registerRoutes(registerRoute: APIRegisterHook) 'post', '/playlists/:playlistId/execute-bucket-adlib', new Map([ + [400, []], [404, [UserErrorMessage.RundownPlaylistNotFound]], [ 412, diff --git a/packages/blueprints-integration/src/api/showStyle.ts b/packages/blueprints-integration/src/api/showStyle.ts index 43182638f3..307fdbe80e 100644 --- a/packages/blueprints-integration/src/api/showStyle.ts +++ b/packages/blueprints-integration/src/api/showStyle.ts @@ -140,7 +140,7 @@ export interface ShowStyleBlueprintManifest Promise + ) => Promise<{ validationErrors: any } | void> /** Generate adlib piece from ingest data */ getAdlibItem?: ( diff --git a/packages/blueprints-integration/src/context/adlibActionContext.ts b/packages/blueprints-integration/src/context/adlibActionContext.ts index 6f9931eeea..68f8784785 100644 --- a/packages/blueprints-integration/src/context/adlibActionContext.ts +++ b/packages/blueprints-integration/src/context/adlibActionContext.ts @@ -43,6 +43,13 @@ export interface IActionExecutionContext /** Insert a queued part to follow the taken part */ queuePartAfterTake(part: IBlueprintPart, pieces: IBlueprintPiece[]): void + /** + * Reject the action request with an error message. + * This will cause the API to return a 400 error response. + * @param message Error message to return to the client + */ + rejectRequest(message: string): void + /** Misc actions */ // updateAction(newManifest: Pick): void // only updates itself. to allow for the next one to do something different // executePeripheralDeviceAction(deviceId: string, functionName: string, args: any[]): Promise diff --git a/packages/corelib/src/error.ts b/packages/corelib/src/error.ts index 3cbf71c558..ebdf84cf29 100644 --- a/packages/corelib/src/error.ts +++ b/packages/corelib/src/error.ts @@ -116,7 +116,7 @@ const UserErrorMessagesTranslations: { [key in UserErrorMessage]: string } = { [UserErrorMessage.DeviceAlreadyAttachedToStudio]: t(`Device is already attached to another studio.`), [UserErrorMessage.ShowStyleBaseNotFound]: t(`ShowStyleBase not found!`), [UserErrorMessage.NoMigrationsToApply]: t(`No migrations to apply`), - [UserErrorMessage.ValidationFailed]: t('Validation failed!'), + [UserErrorMessage.ValidationFailed]: t('Validation failed! {{message}}'), [UserErrorMessage.AdlibTestingNotAllowed]: t(`Rehearsal mode is not allowed`), [UserErrorMessage.AdlibTestingAlreadyActive]: t(`Rehearsal mode is already active`), [UserErrorMessage.BucketNotFound]: t(`Bucket not found!`), diff --git a/packages/corelib/src/worker/studio.ts b/packages/corelib/src/worker/studio.ts index 6eb045fc5e..4a09c922c4 100644 --- a/packages/corelib/src/worker/studio.ts +++ b/packages/corelib/src/worker/studio.ts @@ -277,14 +277,10 @@ export interface ExecuteBucketAdLibOrActionProps extends RundownPlayoutPropsBase externalId: string triggerMode?: string } -export interface ExecuteBucketAdLibOrActionProps extends RundownPlayoutPropsBase { - bucketId: BucketId - externalId: string - triggerMode?: string -} export interface ExecuteActionResult { queuedPartInstanceId?: PartInstanceId taken?: boolean + validationErrors?: any } export interface TakeNextPartProps extends RundownPlayoutPropsBase { fromPartInstanceId: PartInstanceId | null diff --git a/packages/job-worker/src/blueprints/context/adlibActions.ts b/packages/job-worker/src/blueprints/context/adlibActions.ts index 0359871eb2..4b0089680e 100644 --- a/packages/job-worker/src/blueprints/context/adlibActions.ts +++ b/packages/job-worker/src/blueprints/context/adlibActions.ts @@ -82,6 +82,11 @@ export class ActionExecutionContext extends ShowStyleUserContext implements IAct public partToQueueAfterTake: QueueablePartAndPieces | undefined + /** + * If set, the blueprint has rejected the request with an error message + */ + public requestError: string | undefined + public get quickLoopInfo(): BlueprintQuickLookInfo | null { return this.partAndPieceInstanceService.quickLoopInfo } @@ -280,4 +285,8 @@ export class ActionExecutionContext extends ShowStyleUserContext implements IAct getCurrentTime(): number { return getCurrentTime() } + + rejectRequest(message: string): void { + this.requestError = message + } } diff --git a/packages/job-worker/src/playout/__tests__/playout-executeAction.test.ts b/packages/job-worker/src/playout/__tests__/playout-executeAction.test.ts index ae64dc1480..19e2fd46e2 100644 --- a/packages/job-worker/src/playout/__tests__/playout-executeAction.test.ts +++ b/packages/job-worker/src/playout/__tests__/playout-executeAction.test.ts @@ -200,5 +200,28 @@ describe('Playout API', () => { expect(takeNextPartMock).toHaveBeenCalledTimes(0) }) + + test('rejectRequest returns error message', async () => { + const errorMessage = 'This action is not allowed right now' + + context.updateShowStyleBlueprint({ + executeAction: async (context) => { + context.rejectRequest(errorMessage) + }, + }) + + const actionDocId: AdLibActionId = protectString('action-id') + const actionId = 'some-action' + const userData = { blobby: true } + const result = await handleExecuteAdlibAction(context, { + playlistId, + actionDocId, + actionId, + userData, + }) + + expect(result.errorMessage).toBe(errorMessage) + expect(result.taken).toBeFalsy() + }) }) }) diff --git a/packages/job-worker/src/playout/adlibAction.ts b/packages/job-worker/src/playout/adlibAction.ts index fa85af7405..68d491f765 100644 --- a/packages/job-worker/src/playout/adlibAction.ts +++ b/packages/job-worker/src/playout/adlibAction.ts @@ -240,10 +240,12 @@ export async function executeActionInner( )} (${actionParameters.triggerMode})` ) + let result: ExecuteActionResult | void + try { const blueprintPersistentState = new PersistentPlayoutStateStore(playoutModel.playlist.previousPersistentState) - await blueprint.blueprint.executeAction( + result = await blueprint.blueprint.executeAction( actionContext, blueprintPersistentState, actionParameters.actionId, @@ -262,6 +264,17 @@ export async function executeActionInner( throw UserError.fromUnknown(err) } + const validationErrors = result?.validationErrors ?? actionContext.requestError + if (validationErrors) { + const message = typeof validationErrors === 'string' ? validationErrors : JSON.stringify(validationErrors) + throw UserError.from( + new Error(`AdLib Action "${actionParameters.actionId}" validation failed: ${message}`), + UserErrorMessage.ValidationFailed, + { message }, + 409 + ) + } + // Store any notes generated by the action storeNotificationsForCategory( playoutModel,