/** * API execution context factory. * * Creates the context object that is passed to user API functions, * providing access to integrations, logging, and environment. */ import type { IntegrationDeclaration } from "../integrations/declarations.js"; import { createClient, type QueryExecutor, type TraceMetadata, } from "../integrations/registry.js"; import type { IntegrationConfig, IntegrationClientImpl, } from "../integrations/types.js"; import type { ApiContext, ApiUser, Logger, AnyIntegrationRef, } from "../types.js"; import { createDefaultLogger } from "./logger.js"; /** * Options for creating an API execution context. */ export interface CreateContextOptions { /** Available integration configurations keyed by id */ integrations: Map; /** * Declared integrations from the API config. * * These are the integrations specified in the `integrations` field of the API config. * The context will only expose these declared integrations via `ctx.integrations`. */ integrationDeclarations: IntegrationDeclaration[]; /** * Function to execute integration operations. * * For language plugins (JavaScript), pass the bindings parameter * to the API execution request as `input` field for binding resolution. */ executeQuery: ( integrationId: string, request: Record, bindings?: Record, metadata?: TraceMetadata, ) => Promise; /** Execution ID for logging correlation */ executionId: string; /** Environment variables */ env: Record; /** * Server-resolved data tag key. * * Optional for compatibility with execution wrappers from older agents. */ dataTag?: string; /** User information from JWT */ user: ApiUser; /** Optional custom logger */ logger?: Logger; } /** * Creates an API execution context. * * The context provides user code with access to: * - Integration clients (built from declarations) * - Logging utilities * - Environment variables * * @param options - Context creation options * @returns The API context for user code execution * * @example * ```typescript * const ctx = createApiContext({ * integrations: new Map([ * ['int_1', { id: 'int_1', name: 'Production Postgres', pluginId: 'postgres', configuration: {} }], * ]), * integrationDeclarations: [ * { key: 'db', pluginId: 'postgres', id: 'int_1' }, * ], * executeQuery: orchestratorExecutor, * executionId: 'exec_abc', * env: { NODE_ENV: 'production' }, * }); * * // User code can access: ctx.integrations.db (typed as PostgresClient) * ``` */ export function createApiContext( options: CreateContextOptions, ): ApiContext> { const { integrations, integrationDeclarations, executeQuery, executionId, env, dataTag, user, logger, } = options; // Cache for created integration clients const clientCache = new Map(); // Create logger const log = logger ?? createDefaultLogger(executionId); /** * Gets or creates a typed integration client by id and plugin ID. * * Supports two modes: * 1. Pre-loaded configs: Integration configs are provided upfront (orchestrator use) * 2. Lazy resolution: Configs are empty, executeQuery handles ID resolution (library use) */ function getTypedClient(id: string, pluginId: string): IntegrationClientImpl { // Check cache first const cached = clientCache.get(id); if (cached) { return cached; } // Look up integration config (may be undefined for lazy resolution) const config = integrations.get(id); // Create executor bound to this integration const boundExecutor: QueryExecutor = ( request: Record, bindings?: Record, metadata?: TraceMetadata, ) => executeQuery(id, request, bindings, metadata); // Create config for client - use pre-loaded config or create synthetic one const clientConfig: IntegrationConfig = config ?? { id, name: id, // Use ID as name placeholder for lazy resolution pluginId, configuration: {}, }; // Create and cache the client const client = createClient({ config: clientConfig, executeQuery: boundExecutor, }); clientCache.set(id, client); return client; } // Build the integrations object from declarations // Each key maps to a lazily-created typed client const integrationsObject: Record = {}; for (const declaration of integrationDeclarations) { // Use Object.defineProperty to create a getter that lazily creates the client Object.defineProperty(integrationsObject, declaration.key, { get: () => getTypedClient(declaration.id, declaration.pluginId), enumerable: true, configurable: false, }); } return { integrations: integrationsObject as ApiContext< Record >["integrations"], log, env: Object.freeze({ ...env }), dataTag, user, }; }