From 545404caa9cde61d240ae767e124ba610c3b7e58 Mon Sep 17 00:00:00 2001 From: Maciek Antek Papiewski <41971218+anteeek@users.noreply.github.com> Date: Thu, 5 Feb 2026 03:21:32 +0100 Subject: [PATCH 1/2] SOFIE-318 | allow returning error from adlib actions in blueprints (WIP) --- meteor/server/api/rest/v1/playlists.ts | 27 +++++++++++++++++-- .../src/context/adlibActionContext.ts | 7 +++++ packages/corelib/src/worker/studio.ts | 2 ++ .../src/blueprints/context/adlibActions.ts | 9 +++++++ .../__tests__/playout-executeAction.test.ts | 23 ++++++++++++++++ .../job-worker/src/playout/adlibAction.ts | 1 + 6 files changed, 67 insertions(+), 2 deletions(-) diff --git a/meteor/server/api/rest/v1/playlists.ts b/meteor/server/api/rest/v1/playlists.ts index 2616f2e6f93..319fe631a75 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,19 @@ class PlaylistsServerAPI implements PlaylistsRestAPI { actionOptions: adLibOptions, } ) + if (ClientAPI.isClientResponseError(result)) return result + // Check if the action rejected the request + if (result.result?.errorMessage) { + return ClientAPI.responseError( + UserError.from( + new Error(result.result.errorMessage), + UserErrorMessage.InternalError, + undefined, + 400 + ) + ) + } + return ClientAPI.responseSuccess(result.result ?? {}) } else { return ClientAPI.responseError( UserError.from(new Error(`No adLib with Id ${adLibId}`), UserErrorMessage.AdlibNotFound, undefined, 412) @@ -268,7 +281,7 @@ class PlaylistsServerAPI implements PlaylistsRestAPI { ) } - return ServerClientAPI.runUserActionInLogForPlaylistOnWorker( + const result = await ServerClientAPI.runUserActionInLogForPlaylistOnWorker( this.context.getMethodContext(connection), event, getCurrentTime(), @@ -286,6 +299,14 @@ class PlaylistsServerAPI implements PlaylistsRestAPI { triggerMode: triggerMode ?? undefined, } ) + if (ClientAPI.isClientResponseError(result)) return result + // Check if the action rejected the request + if (result.result?.errorMessage) { + return ClientAPI.responseError( + UserError.from(new Error(result.result.errorMessage), UserErrorMessage.InternalError, undefined, 400) + ) + } + return ClientAPI.responseSuccess(result.result ?? {}) } async moveNextPart( connection: Meteor.Connection, @@ -602,6 +623,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 +660,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/context/adlibActionContext.ts b/packages/blueprints-integration/src/context/adlibActionContext.ts index 6f9931eeea7..68f87847857 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/worker/studio.ts b/packages/corelib/src/worker/studio.ts index 6eb045fc5e0..bf995f10148 100644 --- a/packages/corelib/src/worker/studio.ts +++ b/packages/corelib/src/worker/studio.ts @@ -285,6 +285,8 @@ export interface ExecuteBucketAdLibOrActionProps extends RundownPlayoutPropsBase export interface ExecuteActionResult { queuedPartInstanceId?: PartInstanceId taken?: boolean + /** If set, the action was rejected - contains the error message */ + errorMessage?: string } 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 0359871eb2a..4b0089680e4 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 ae64dc1480d..19e2fd46e23 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 fa85af74050..efc5333b6ea 100644 --- a/packages/job-worker/src/playout/adlibAction.ts +++ b/packages/job-worker/src/playout/adlibAction.ts @@ -276,6 +276,7 @@ export async function executeActionInner( return { queuedPartInstanceId: actionContext.queuedPartInstanceId, taken: actionContext.takeAfterExecute, + errorMessage: actionContext.requestError, } } From f806f8d99f6eca0d72d06be366b9a84349a487b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Mareti=C4=87?= Date: Wed, 21 Jun 2023 17:18:45 +0200 Subject: [PATCH 2/2] feat: return adlib action validation errors to client --- meteor/server/api/rest/v1/playlists.ts | 17 ----------------- .../blueprints-integration/src/api/showStyle.ts | 2 +- packages/corelib/src/error.ts | 2 +- packages/corelib/src/worker/studio.ts | 8 +------- packages/job-worker/src/playout/adlibAction.ts | 16 ++++++++++++++-- 5 files changed, 17 insertions(+), 28 deletions(-) diff --git a/meteor/server/api/rest/v1/playlists.ts b/meteor/server/api/rest/v1/playlists.ts index 319fe631a75..b0827408412 100644 --- a/meteor/server/api/rest/v1/playlists.ts +++ b/meteor/server/api/rest/v1/playlists.ts @@ -221,17 +221,6 @@ class PlaylistsServerAPI implements PlaylistsRestAPI { } ) if (ClientAPI.isClientResponseError(result)) return result - // Check if the action rejected the request - if (result.result?.errorMessage) { - return ClientAPI.responseError( - UserError.from( - new Error(result.result.errorMessage), - UserErrorMessage.InternalError, - undefined, - 400 - ) - ) - } return ClientAPI.responseSuccess(result.result ?? {}) } else { return ClientAPI.responseError( @@ -300,12 +289,6 @@ class PlaylistsServerAPI implements PlaylistsRestAPI { } ) if (ClientAPI.isClientResponseError(result)) return result - // Check if the action rejected the request - if (result.result?.errorMessage) { - return ClientAPI.responseError( - UserError.from(new Error(result.result.errorMessage), UserErrorMessage.InternalError, undefined, 400) - ) - } return ClientAPI.responseSuccess(result.result ?? {}) } async moveNextPart( diff --git a/packages/blueprints-integration/src/api/showStyle.ts b/packages/blueprints-integration/src/api/showStyle.ts index 43182638f38..307fdbe80ed 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/corelib/src/error.ts b/packages/corelib/src/error.ts index 3cbf71c558b..ebdf84cf29e 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 bf995f10148..4a09c922c4d 100644 --- a/packages/corelib/src/worker/studio.ts +++ b/packages/corelib/src/worker/studio.ts @@ -277,16 +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 - /** If set, the action was rejected - contains the error message */ - errorMessage?: string + validationErrors?: any } export interface TakeNextPartProps extends RundownPlayoutPropsBase { fromPartInstanceId: PartInstanceId | null diff --git a/packages/job-worker/src/playout/adlibAction.ts b/packages/job-worker/src/playout/adlibAction.ts index efc5333b6ea..68d491f765f 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, @@ -276,7 +289,6 @@ export async function executeActionInner( return { queuedPartInstanceId: actionContext.queuedPartInstanceId, taken: actionContext.takeAfterExecute, - errorMessage: actionContext.requestError, } }