/** * API definition function. * * This module provides the `api()` function used to define TypeScript-based APIs * with input/output validation using Zod schemas and upfront integration declarations. */ import type { z } from "zod"; /** * Extract the input type from a CompiledApi. * * Use this utility type to extract input types from API definitions for use in the frontend * without importing the runtime API code. * * @example * ```typescript * import type GetUsersApi from '../server/apis/GetUsers/api'; * import type { ExtractApiInput } from '@superblocksteam/sdk-api'; * * type GetUsersInput = ExtractApiInput; * // GetUsersInput is now { email?: string; name?: string; } * ``` */ export type ExtractApiInput = // eslint-disable-next-line @typescript-eslint/no-explicit-any T extends CompiledApi ? TInput : never; /** * Extract the output type from a CompiledApi. * * Use this utility type to extract output types from API definitions for use in the frontend * without importing the runtime API code. * * @example * ```typescript * import type GetUsersApi from '../server/apis/GetUsers/api'; * import type { ExtractApiOutput } from '@superblocksteam/sdk-api'; * * type GetUsersOutput = ExtractApiOutput; * // GetUsersOutput is now { users: User[]; } * ``` */ export type ExtractApiOutput = // eslint-disable-next-line @typescript-eslint/no-explicit-any T extends CompiledApi ? TOutput : never; import { extractIntegrationDeclarations, type IntegrationDeclaration, } from "../integrations/declarations.js"; import type { ApiConfig, ApiContext, AnyIntegrationRef } from "../types.js"; // --------------------------------------------------------------------------- // Entry point setter — used by the Vite plugin to stamp the source file path // onto every api()/streamingApi() call in a module. ESM top-level execution // is synchronous, so the prepended __setEntryPoint() always runs before any // api() call in the same file. The value is intentionally NOT cleared after // the first read so that multiple named-exported api() calls in a single file // all receive the same entry point. The next file's __setEntryPoint() call // naturally overwrites the value. // --------------------------------------------------------------------------- let __pendingEntryPoint: string | undefined; /** Called by the Vite plugin to set the entry point for api() calls in this file. */ export function __setEntryPoint(entryPoint: string): void { __pendingEntryPoint = entryPoint; } /** * Returns the current pending entry point without clearing it. * This allows multiple api() calls within the same file to share the same * entry point (e.g. named exports from a single module). */ export function getEntryPoint(): string | undefined { return __pendingEntryPoint; } /** * A compiled API definition ready for execution. * * @template TInput - The inferred input type * @template TOutput - The inferred output type */ export interface CompiledApi { /** Name of the API for identification in logs and debugging */ readonly name: string; /** Plain-language summary of what this API does */ readonly description?: string; /** * Source file path relative to the app root (e.g. "server/apis/GetUsers/api.ts"). * Injected automatically by the Vite plugin — not authored by the user. */ readonly entryPoint?: string; /** Zod schema for validating inputs */ readonly inputSchema: z.ZodType; /** Zod schema for validating outputs */ readonly outputSchema: z.ZodType; /** * The API implementation function. * * @param ctx - Execution context * @param input - Validated input parameters * @returns Promise resolving to the output */ readonly run: ( ctx: ApiContext>, input: TInput, ) => Promise; /** * Declared integrations for upfront authentication. * * This array contains all integrations declared in the API config, * allowing the runtime to authenticate them before API execution. * * @example * ```typescript * // Given an API with: * // integrations: { db: postgres('Prod DB'), notifier: slack('Ops Slack') } * // * // compiledApi.integrations would be: * // [ * // { key: 'db', pluginId: 'postgres', name: 'Prod DB' }, * // { key: 'notifier', pluginId: 'slack', name: 'Ops Slack' }, * // ] * ``` */ readonly integrations: ReadonlyArray; } /** * Define a TypeScript-based API with input/output validation. * * This function creates a compiled API definition that can be executed * by the Superblocks runtime. Inputs and outputs are validated using * the provided Zod schemas. * * @param config - The API configuration * @returns A compiled API definition * * @example * ```typescript * import { api, z, postgres, slack } from '@superblocksteam/sdk-api'; * * export default api({ * name: 'GetUser', * integrations: { * db: postgres('Production Postgres'), * notifier: slack('Ops Slack'), * }, * * input: z.object({ * userId: z.string().uuid(), * }), * * output: z.object({ * user: z.object({ * id: z.string(), * name: z.string(), * email: z.string().email(), * }), * }), * * async run(ctx, { userId }) { * const [user] = await ctx.integrations.db.query( * 'SELECT * FROM users WHERE id = $1', * z.object({ id: z.string(), name: z.string(), email: z.string() }), * [userId] * ); * * if (!user) { * throw new Error('User not found'); * } * * await ctx.integrations.notifier.apiRequest( * { method: 'POST', path: '/chat.postMessage', body: { channel: '#user-lookups', text: `Fetched user ${user.name}` } }, * { response: z.object({ ok: z.boolean() }) } * ); * * return { user }; * }, * }); * ``` */ export function api< TInput extends z.ZodType, TOutput extends z.ZodType, TIntegrations extends Record = Record< string, never >, >( config: ApiConfig, ): CompiledApi, z.infer> { // Extract integration declarations for upfront auth const integrations = extractIntegrationDeclarations(config.integrations); const compiled: CompiledApi, z.infer> = { name: config.name, description: config.description, entryPoint: getEntryPoint(), inputSchema: config.input, outputSchema: config.output, run: config.run as ( ctx: ApiContext>, input: z.infer, ) => Promise>, integrations, }; return compiled; }