/** * API execution engine. * * Provides the main entry point for executing compiled APIs, * handling input/output validation and error management. */ import type { CompiledApi } from "../api/definition.js"; import { getIntegrationDeclarations } from "../integrations/declarations.js"; import type { TraceMetadata } from "../integrations/registry.js"; import type { IntegrationConfig } from "../integrations/types.js"; import type { ApiUser } from "../types.js"; import { createApiContext } from "./context.js"; import { SdkError, InputValidationError, OutputValidationError, ExecutionError, ErrorCode, type ErrorCodeType, } from "./errors.js"; /** * Request to execute an API. */ export interface ExecuteApiRequest { /** Raw input data to be validated */ input: unknown; /** Available integration configurations */ integrations: IntegrationConfig[]; /** Unique execution ID for tracing */ executionId: string; /** Environment variables available to the API */ env: Record; /** * Server-resolved data tag key for this execution. * * Optional on the low-level request for compatibility with older runtimes. */ dataTag?: string; /** User information extracted from the Superblocks JWT */ user: ApiUser; /** * Function to execute integration operations. * Called by integration clients to perform actual operations. * * The request object should match the protobuf Plugin schema for the * specific integration type (e.g., postgresql.v1.Plugin for Postgres). * * For language plugins (JavaScript), the bindings parameter should be * passed to the API execution request as `input` or `inputs` field for * binding resolution. * * @param integrationId - The integration ID * @param request - Plugin-specific request object matching the proto schema * @param bindings - Optional bindings data for binding resolution (e.g., JavaScript bindings) * @returns Promise resolving to the operation result */ executeQuery: ( integrationId: string, request: Record, bindings?: Record, metadata?: TraceMetadata, ) => Promise; } /** * Successful API execution response. * * @template TOutput - The output type from the API */ export interface ExecuteApiSuccessResponse { success: true; output: TOutput; } /** * Failed API execution response. */ export interface ExecuteApiErrorResponse { success: false; error: { code: ErrorCodeType; message: string; details?: unknown; }; } /** * API execution response. * * @template TOutput - The output type from the API */ export type ExecuteApiResponse = | ExecuteApiSuccessResponse | ExecuteApiErrorResponse; /** * Executes a compiled API with the given request. * * This is the main entry point called by the orchestrator to execute * TypeScript-based APIs. It handles: * * 1. Input validation against the API's Zod schema * 2. Context creation with integration clients * 3. API function execution * 4. Output validation against the API's Zod schema * 5. Error handling and categorization * * @param api - The compiled API definition * @param request - The execution request with input and integrations * @returns Promise resolving to success or error response * * @example * ```typescript * import { api, z } from '@superblocksteam/sdk-api'; * import { executeApi } from '@superblocksteam/sdk-api/runtime'; * * const myApi = api({ * input: z.object({ userId: z.string() }), * output: z.object({ name: z.string() }), * async run(ctx, { userId }) { * const [user] = await ctx.integrations.db.query( * 'SELECT name FROM users WHERE id = $1', * [userId] * ); * return { name: user.name }; * }, * }); * * const result = await executeApi(myApi, { * input: { userId: '123' }, * integrations: [...], * executionId: 'exec_abc', * env: {}, * executeQuery: orchestratorExecutor, * }); * ``` */ export async function executeApi( api: CompiledApi, request: ExecuteApiRequest, ): Promise> { try { // 1. Validate input const inputResult = api.inputSchema.safeParse(request.input); if (!inputResult.success) { throw new InputValidationError("Input validation failed", { issues: inputResult.error.issues, }); } // 2. Build integration map (keyed by id) const integrations = new Map(); for (const config of request.integrations) { integrations.set(config.id, config); } // 3. Create execution context const ctx = createApiContext({ integrations, integrationDeclarations: getIntegrationDeclarations(api), executeQuery: request.executeQuery, executionId: request.executionId, env: request.env, dataTag: request.dataTag, user: request.user, }); // 4. Execute the API let rawOutput: unknown; try { rawOutput = await api.run(ctx, inputResult.data); } catch (err) { // Re-throw SDK errors as-is if (err instanceof SdkError) { throw err; } // Wrap user errors const message = err instanceof Error ? err.message : String(err); throw new ExecutionError(message, err); } // 5. Validate output const outputResult = api.outputSchema.safeParse(rawOutput); if (!outputResult.success) { throw new OutputValidationError("Output validation failed", { issues: outputResult.error.issues, }); } return { success: true, output: outputResult.data as TOutput, }; } catch (err) { // Handle SDK errors if (err instanceof SdkError) { return { success: false, error: err.toJSON(), }; } // Handle unexpected errors const message = err instanceof Error ? err.message : String(err); return { success: false, error: { code: ErrorCode.INTERNAL_ERROR, message: `Unexpected error: ${message}`, details: err instanceof Error ? { stack: err.stack } : undefined, }, }; } }