diff --git a/lib/adapters/REST/endpoints/app-action-call.ts b/lib/adapters/REST/endpoints/app-action-call.ts index bc111f14f..96b622524 100644 --- a/lib/adapters/REST/endpoints/app-action-call.ts +++ b/lib/adapters/REST/endpoints/app-action-call.ts @@ -3,6 +3,7 @@ import type { AppActionCallProps, AppActionCallResponse, CreateAppActionCallProps, + AppActionCallStructuredResult, } from '../../../entities/app-action-call' import * as raw from './raw' import type { RestEndpoint } from '../types' @@ -109,3 +110,75 @@ export const createWithResponse: RestEndpoint<'AppActionCall', 'createWithRespon return callAppActionResult(http, params, { callId }) } + +export const createWithResult: RestEndpoint<'AppActionCall', 'createWithResult'> = async ( + http: AxiosInstance, + params: CreateWithResponseParams, + data: CreateAppActionCallProps +) => { + const createResponse = await raw.post( + http, + `/spaces/${params.spaceId}/environments/${params.environmentId}/app_installations/${params.appDefinitionId}/actions/${params.appActionId}/calls`, + data + ) + + const callId = createResponse.sys.id + + return callAppActionStructuredResult(http, params, { callId }) +} + +async function callAppActionStructuredResult( + http: AxiosInstance, + params: CreateWithResponseParams, + { + callId, + }: { + callId: string + } +): Promise { + let checkCount = 1 + const retryInterval = params.retryInterval || APP_ACTION_CALL_RETRY_INTERVAL + const retries = params.retries || APP_ACTION_CALL_RETRIES + + return new Promise((resolve, reject) => { + const poll = async () => { + try { + // Use format=structured to get AppActionCall format instead of raw webhook logs + const result = await raw.get( + http, + `/spaces/${params.spaceId}/environments/${params.environmentId}/actions/${params.appActionId}/calls/${callId}?format=structured` + ) + + // Check if the app action call is completed + if (result.sys.status === 'succeeded' || result.sys.status === 'failed') { + resolve(result) + } + // The call is still processing, continue polling + else if (result.sys.status === 'processing' && checkCount < retries) { + checkCount++ + await waitFor(retryInterval) + poll() + } + // Timeout - the processing is taking too long + else { + const error = new Error( + 'The app action response is taking longer than expected to process.' + ) + reject(error) + } + } catch (error) { + checkCount++ + + if (checkCount > retries) { + reject(new Error('The app action response is taking longer than expected to process.')) + return + } + // If the call throws, we re-poll as it might mean that the result is not available yet + await waitFor(retryInterval) + poll() + } + } + + poll() + }) +} diff --git a/lib/common-types.ts b/lib/common-types.ts index a0a268696..27376dd06 100644 --- a/lib/common-types.ts +++ b/lib/common-types.ts @@ -11,6 +11,7 @@ import type { AppActionCallProps, AppActionCallResponse, CreateAppActionCallProps, + AppActionCallStructuredResult, } from './entities/app-action-call' import type { AppBundleProps, CreateAppBundleProps } from './entities/app-bundle' import type { @@ -361,7 +362,7 @@ interface CursorPaginationBase { limit?: number } -// Interfaces for each “exclusive” shape +// Interfaces for each "exclusive" shape interface CursorPaginationPageNext extends CursorPaginationBase { pageNext: string pagePrev?: never @@ -444,6 +445,10 @@ type MRInternal = { 'AppActionCall', 'createWithResponse' > + (opts: MROpts<'AppActionCall', 'createWithResult', UA>): MRReturn< + 'AppActionCall', + 'createWithResult' + > (opts: MROpts<'AppActionCall', 'getCallDetails', UA>): MRReturn<'AppActionCall', 'getCallDetails'> (opts: MROpts<'AppBundle', 'get', UA>): MRReturn<'AppBundle', 'get'> @@ -1013,15 +1018,24 @@ export type MRActions = { create: { params: GetAppActionCallParams payload: CreateAppActionCallProps + headers?: RawAxiosRequestHeaders return: AppActionCallProps } - getCallDetails: { - params: GetAppActionCallDetailsParams + createWithResponse: { + params: CreateWithResponseParams + payload: CreateAppActionCallProps + headers?: RawAxiosRequestHeaders return: AppActionCallResponse } - createWithResponse: { - params: GetAppActionCallParams + createWithResult: { + params: CreateWithResponseParams payload: CreateAppActionCallProps + headers?: RawAxiosRequestHeaders + return: AppActionCallStructuredResult + } + getCallDetails: { + params: GetAppActionCallDetailsParams + headers?: RawAxiosRequestHeaders return: AppActionCallResponse } } diff --git a/lib/entities/app-action-call.ts b/lib/entities/app-action-call.ts index 87a620a22..e4d054131 100644 --- a/lib/entities/app-action-call.ts +++ b/lib/entities/app-action-call.ts @@ -35,15 +35,63 @@ export type CreateAppActionCallProps = { type AppActionCallApi = { createWithResponse(): Promise getCallDetails(): Promise + createWithResult(): Promise } export type AppActionCallResponse = WebhookCallDetailsProps +// Structured AppActionCall result format that matches the solution document +export type AppActionCallStructuredResult = { + sys: { + type: 'AppActionCall' + id: string + status: 'processing' | 'succeeded' | 'failed' + action: { + sys: { + type: 'Link' + linkType: 'AppAction' + id: string + } + } + space: { + sys: { + type: 'Link' + linkType: 'Space' + id: string + } + } + environment?: { + sys: { + type: 'Link' + linkType: 'Environment' + id: string + } + } + createdAt: string + updatedAt: string + } + result?: any + error?: { + name?: string + message: string + details?: any + sys?: { + type: 'Error' + id: string + } + } +} + export interface AppActionCallResponseData extends AppActionCallResponse, DefaultElements, AppActionCallApi {} +export interface AppActionCallStructuredData + extends AppActionCallStructuredResult, + DefaultElements, + AppActionCallApi {} + export interface AppActionCall extends AppActionCallProps, DefaultElements {} /** @@ -76,6 +124,28 @@ export default function createAppActionCallApi( }).then((data) => wrapAppActionCallResponse(makeRequest, data)) }, + createWithResult: function () { + const payload: CreateAppActionCallProps = { + parameters: { + recipient: 'Alice ', + message_body: 'Hello from Bob!', + }, + } + + return makeRequest({ + entityType: 'AppActionCall', + action: 'createWithResult', + params: { + spaceId: 'space-id', + environmentId: 'environment-id', + appDefinitionId: 'app-definiton-id', + appActionId: 'app-action-id', + ...retryOptions, + }, + payload: payload, + }).then((data) => wrapAppActionCallStructuredResult(makeRequest, data)) + }, + getCallDetails: function getCallDetails() { return makeRequest({ entityType: 'AppActionCall', @@ -127,3 +197,21 @@ export function wrapAppActionCallResponse( ) return appActionCallResponseWithMethods } + +/** + * @private + * @param http - HTTP client instance + * @param data - Raw AppActionCall data + * @return Wrapped AppActionCall data + */ +export function wrapAppActionCallStructuredResult( + makeRequest: MakeRequest, + data: AppActionCallStructuredResult +): AppActionCallStructuredData { + const appActionCallStructuredResult = toPlainObject(copy(data)) + const appActionCallStructuredResultWithMethods = enhanceWithMethods( + appActionCallStructuredResult, + createAppActionCallApi(makeRequest) + ) + return appActionCallStructuredResultWithMethods +} diff --git a/lib/entities/app-action.ts b/lib/entities/app-action.ts index 361e1b813..b638ead55 100644 --- a/lib/entities/app-action.ts +++ b/lib/entities/app-action.ts @@ -66,6 +66,11 @@ type BaseAppActionProps = AppActionCategory & { * Human readable description of the action */ description?: string + /** + * JSON Schema defining the expected response format from the action + * Used for validating and structuring app action call results + */ + resultSchema?: Record } type CreateEndpointAppActionProps = { @@ -130,6 +135,11 @@ type LegacyFunctionAppActionProps = Record & { export type CreateAppActionProps = AppActionCategory & { name: string description?: string + /** + * JSON Schema defining the expected response format from the action + * Used for validating and structuring app action call results + */ + resultSchema?: Record } & (CreateEndpointAppActionProps | CreateFunctionAppActionProps | LegacyFunctionAppActionProps) export type AppActionProps = BaseAppActionProps & diff --git a/lib/export-types.ts b/lib/export-types.ts index f88cca269..328199a14 100644 --- a/lib/export-types.ts +++ b/lib/export-types.ts @@ -24,6 +24,7 @@ export type { export type { AppActionCall, AppActionCallProps, + AppActionCallStructuredResult, CreateAppActionCallProps, } from './entities/app-action-call' export type { diff --git a/lib/plain/entities/app-action-call.ts b/lib/plain/entities/app-action-call.ts index ea7a9e455..47af5a9c1 100644 --- a/lib/plain/entities/app-action-call.ts +++ b/lib/plain/entities/app-action-call.ts @@ -1,10 +1,12 @@ -import type { GetAppActionCallDetailsParams, GetAppActionCallParams } from '../../common-types' +import type { GetAppActionCallDetailsParams, GetAppActionCallParams, CreateWithResponseParams } from '../../common-types' import type { AppActionCallProps, AppActionCallResponse, + AppActionCallStructuredResult, CreateAppActionCallProps, } from '../../entities/app-action-call' import type { OptionalDefaults } from '../wrappers/wrap' +import type { RawAxiosRequestHeaders } from 'axios' export type AppActionCallPlainClientAPI = { /** @@ -30,7 +32,8 @@ export type AppActionCallPlainClientAPI = { */ create( params: OptionalDefaults, - payload: CreateAppActionCallProps + payload: CreateAppActionCallProps, + headers?: RawAxiosRequestHeaders ): Promise /** * Fetches the details of an App Action Call @@ -51,10 +54,10 @@ export type AppActionCallPlainClientAPI = { params: OptionalDefaults ): Promise /** - * Calls (triggers) an App Action + * Calls (triggers) an App Action and returns raw webhook log format * @param params entity IDs to identify the App Action to call * @param payload the payload to be sent to the App Action - * @returns detailed metadata about the App Action Call + * @returns detailed metadata about the App Action Call (raw webhook log format) * @throws if the request fails, or the App Action is not found * @example * ```javascript @@ -72,7 +75,40 @@ export type AppActionCallPlainClientAPI = { * ``` */ createWithResponse( - params: OptionalDefaults, - payload: CreateAppActionCallProps + params: OptionalDefaults, + payload: CreateAppActionCallProps, + headers?: RawAxiosRequestHeaders ): Promise + /** + * Calls (triggers) an App Action and returns structured AppActionCall format + * @param params entity IDs to identify the App Action to call + * @param payload the payload to be sent to the App Action + * @returns structured AppActionCall with result/error fields + * @throws if the request fails, or the App Action is not found + * @example + * ```javascript + * const appActionCall = await client.appActionCall.createWithResult( + * { + * spaceId: "", + * environmentId: "", + * appDefinitionId: "", + * appActionId: "", + * }, + * { + * parameters: { // ... }, + * } + * ); + * + * if (appActionCall.sys.status === 'succeeded') { + * console.log('Result:', appActionCall.result); + * } else if (appActionCall.sys.status === 'failed') { + * console.log('Error:', appActionCall.error); + * } + * ``` + */ + createWithResult( + params: OptionalDefaults, + payload: CreateAppActionCallProps, + headers?: RawAxiosRequestHeaders + ): Promise } diff --git a/lib/plain/entities/app-action.ts b/lib/plain/entities/app-action.ts index dbf9faa2b..b2306bed5 100644 --- a/lib/plain/entities/app-action.ts +++ b/lib/plain/entities/app-action.ts @@ -84,7 +84,6 @@ export type AppActionPlainClientAPI = { * { * organizationId: "", * appDefinitionId: "", - * appActionId: "", * }, * { * category: "Notification.v1.0", @@ -94,12 +93,44 @@ export type AppActionPlainClientAPI = { * } * ); * + * // app action with resultSchema for structured responses + * const appActionWithSchema = await client.appAction.create( + * { + * organizationId: "", + * appDefinitionId: "", + * }, + * { + * category: "Custom", + * parameters: [ + * { id: "userId", name: "User ID", type: "Symbol" } + * ], + * url: "https://api.example.com/user-profile", + * description: "Fetches user profile data", + * name: "Get User Profile", + * resultSchema: { + * type: "object", + * properties: { + * profile: { + * type: "object", + * properties: { + * name: { type: "string" }, + * email: { type: "string" }, + * preferences: { type: "object" } + * }, + * required: ["name", "email"] + * }, + * success: { type: "boolean" } + * }, + * required: ["profile", "success"] + * } + * } + * ); + * * // app action that invokes a Contentful Function * const functionAppAction = await client.appAction.create( * { * organizationId: '', * appDefinitionId: '', - * appActionId: '', * }, * { * type: "function-invocation", @@ -144,6 +175,40 @@ export type AppActionPlainClientAPI = { * } * ); * + * // app action with resultSchema for structured responses + * const appActionWithSchema = await client.appAction.update( + * { + * organizationId: "", + * appDefinitionId: "", + * appActionId: "", + * }, + * { + * category: "Custom", + * parameters: [ + * { id: "userId", name: "User ID", type: "Symbol" } + * ], + * url: "https://api.example.com/user-profile", + * description: "Fetches user profile data", + * name: "Get User Profile", + * resultSchema: { + * type: "object", + * properties: { + * profile: { + * type: "object", + * properties: { + * name: { type: "string" }, + * email: { type: "string" }, + * preferences: { type: "object" } + * }, + * required: ["name", "email"] + * }, + * success: { type: "boolean" } + * }, + * required: ["profile", "success"] + * } + * } + * ); + * * // app action that invokes a Contentful Function * const functionAppAction = await client.appAction.update( * { diff --git a/lib/plain/plain-client.ts b/lib/plain/plain-client.ts index 7f510acf4..5b8c742d3 100644 --- a/lib/plain/plain-client.ts +++ b/lib/plain/plain-client.ts @@ -81,8 +81,9 @@ export const createPlainClient = ( }, appActionCall: { create: wrap(wrapParams, 'AppActionCall', 'create'), - getCallDetails: wrap(wrapParams, 'AppActionCall', 'getCallDetails'), createWithResponse: wrap(wrapParams, 'AppActionCall', 'createWithResponse'), + createWithResult: wrap(wrapParams, 'AppActionCall', 'createWithResult'), + getCallDetails: wrap(wrapParams, 'AppActionCall', 'getCallDetails'), }, appBundle: { get: wrap(wrapParams, 'AppBundle', 'get'),