import type { Transport } from "./transport"; /** * Client interface for invoking custom backend functions. * * Custom functions are Hono route files auto-mounted by the Rebase backend * at `/api/functions/{name}`. The `FunctionsClient` wraps the shared * transport so callers never need to manually construct URLs or inject * auth tokens. * * @example * ```ts * const result = await client.functions.invoke<{ job: Job }>('extract-job', { * url: 'https://example.com/posting', * html: htmlContent, * }); * ``` */ export interface FunctionsClient { /** * Invoke a custom backend function by name. * * @typeParam T - Expected shape of the response payload. * @param name - Function name (the filename without extension, e.g. `"extract-job"`). * @param payload - Optional JSON-serialisable body sent as `POST`. * @param options - Optional overrides (HTTP method, sub-path, extra headers). * @returns The parsed JSON response from the function. */ invoke( name: string, payload?: unknown, options?: FunctionInvokeOptions, ): Promise; } export type { FunctionInvokeOptions } from "@rebasepro/types"; import type { FunctionInvokeOptions } from "@rebasepro/types"; /** * Create a `FunctionsClient` backed by the given transport. * * The transport already handles: * - Base URL resolution * - JWT injection via `Authorization: Bearer` * - 401 retry / `onUnauthorized` flow * - Consistent error throwing via `RebaseApiError` * * @internal */ export function createFunctionsClient(transport: Transport): FunctionsClient { return { async invoke( name: string, payload?: unknown, options?: FunctionInvokeOptions ): Promise { const method = options?.method ?? "POST"; // A `path` that starts the query or fragment is appended as-is. Only a // real sub-path gets a separator: inserting one before `?days=30` asks // for `/functions/dashboard-stats/?days=30`, and the trailing slash // misses the route, so a function that exists answers 404 — and the // caller sees it as the backend being down rather than as a bad URL. const rawPath = options?.path; const subPath = rawPath ? (/^[?#]/.test(rawPath) ? rawPath : `/${rawPath.replace(/^\//, "")}`) : ""; const routePath = `/functions/${encodeURIComponent(name)}${subPath}`; const init: RequestInit = { method }; if (payload !== undefined && method !== "GET") { init.body = JSON.stringify(payload); } if (options?.headers) { init.headers = options.headers; } return transport.request(routePath, init); } }; }