UNPKG

@spfn/core

Version:

SPFN Framework Core - File-based routing, transactions, repository pattern

359 lines (353 loc) 10.5 kB
import '../auto-loader-JFaZ9gON.js'; import { a as RouteContract, I as InferContract } from '../types-BXibIEyj.js'; import 'hono'; import 'hono/utils/http-status'; import '@sinclair/typebox'; import '../types/index.js'; /** * Contract-Based API Client * * Type-safe HTTP client that works with RouteContract for full end-to-end type safety */ type RequestInterceptor = (url: string, init: RequestInit) => Promise<RequestInit> | RequestInit; interface ClientConfig { /** * API base URL (e.g., http://localhost:4000) */ baseUrl?: string; /** * Default headers to include in all requests */ headers?: Record<string, string>; /** * Request timeout in milliseconds */ timeout?: number; /** * Custom fetch implementation */ fetch?: typeof fetch; } interface CallOptions<TContract extends RouteContract> { params?: InferContract<TContract>['params']; query?: InferContract<TContract>['query']; body?: InferContract<TContract>['body']; headers?: Record<string, string>; baseUrl?: string; /** * Additional fetch options (extends RequestInit) * * Can be used for environment-specific options like Next.js cache/next * * @example * ```ts * // Next.js time-based revalidation * { fetchOptions: { next: { revalidate: 60 } } } * * // Next.js disable cache * { fetchOptions: { cache: 'no-store' } } * * // Next.js on-demand revalidation * { fetchOptions: { next: { tags: ['products'] } } } * ``` */ fetchOptions?: Record<string, any>; } /** * API Client Error */ declare class ApiClientError extends Error { readonly status: number; readonly url: string; readonly response?: unknown | undefined; readonly errorType?: "timeout" | "network" | "http" | undefined; constructor(message: string, status: number, url: string, response?: unknown | undefined, errorType?: "timeout" | "network" | "http" | undefined); } /** * Contract-based API Client */ declare class ContractClient { private readonly config; private readonly interceptors; constructor(config?: ClientConfig); /** * Add request interceptor */ use(interceptor: RequestInterceptor): void; /** * Make a type-safe API call using a contract * * @param contract - Route contract with absolute path * @param options - Call options (params, query, body, headers) */ call<TContract extends RouteContract>(contract: TContract, options?: CallOptions<TContract>): Promise<InferContract<TContract>['response']>; /** * Create a new client with merged configuration */ withConfig(config: Partial<ClientConfig>): ContractClient; private static buildUrl; private static buildQuery; private static getHttpMethod; private static isFormData; } /** * Type guard for timeout errors * * @example * ```ts * try { * await api.users.getById({ params: { id: '123' } }); * } catch (error) { * if (isTimeoutError(error)) { * console.error('Request timed out, retrying...'); * // Implement retry logic * } * } * ``` */ declare function isTimeoutError(error: unknown): error is ApiClientError; /** * Type guard for network errors * * @example * ```ts * try { * await api.users.list(); * } catch (error) { * if (isNetworkError(error)) { * showOfflineMessage(); * } * } * ``` */ declare function isNetworkError(error: unknown): error is ApiClientError; /** * Type guard for HTTP errors (4xx, 5xx) * * @example * ```ts * try { * await api.users.create({ body: userData }); * } catch (error) { * if (isHttpError(error)) { * if (error.status === 401) { * redirectToLogin(); * } else if (error.status === 404) { * showNotFoundMessage(); * } * } * } * ``` */ declare function isHttpError(error: unknown): error is ApiClientError; /** * Check if error is a specific server error type * * @example * ```ts * try { * await api.workflows.getById({ params: { uuid: 'xxx' } }); * } catch (error) { * if (isServerError(error, 'NotFoundError')) { * showNotFoundMessage(); * } else if (isServerError(error, 'ValidationError')) { * showValidationErrors(getServerErrorDetails(error)); * } * } * ``` */ declare function isServerError(error: unknown, errorType: string): error is ApiClientError; /** * Get server error type from ApiClientError * * @example * ```ts * const errorType = getServerErrorType(error); * // 'NotFoundError', 'ValidationError', 'PaymentFailedError', etc. * ``` */ declare function getServerErrorType(error: ApiClientError): string | undefined; /** * Get server error details from ApiClientError * * @example * ```ts * if (isServerError(error, 'PaymentFailedError')) { * const details = getServerErrorDetails(error); * console.log('Payment ID:', details.paymentId); * } * ``` */ declare function getServerErrorDetails<T = any>(error: ApiClientError): T | undefined; /** * Universal API Client * * Automatically routes requests based on execution environment: * - Server Environment: Direct call to SPFN API (internal network) * - Browser Environment: Proxies through Next.js API Route (cookie forwarding) */ /** * Universal Client Configuration */ interface UniversalClientConfig { /** * SPFN API server URL (for server-side direct calls) * * @default process.env.SERVER_API_URL || process.env.SPFN_API_URL || 'http://localhost:8790' */ apiUrl?: string; /** * Next.js API route base path (for client-side proxy calls) * * @default '/api/actions' * @example '/api/proxy' */ proxyBasePath?: string; /** * Additional headers to include in all requests */ headers?: Record<string, string>; /** * Request timeout in milliseconds * * @default 30000 */ timeout?: number; /** * Custom fetch implementation */ fetch?: typeof fetch; } /** * Universal API Client * * Automatically detects execution environment and routes requests accordingly: * * **Server Environment** (Next.js Server Components, API Routes): * - Direct HTTP call to SPFN API server * - Uses internal network (e.g., http://localhost:8790) * - No proxy overhead * * **Browser Environment** (Next.js Client Components): * - Routes through Next.js API Route proxy (e.g., /api/proxy/*) * - Enables HttpOnly cookie forwarding * - Maintains CORS security * * @example * ```typescript * // Server Component - direct call * import { createUniversalClient } from '@spfn/core/client'; * const client = createUniversalClient(); * const result = await client.call(loginContract, { body: {...} }); * * // Client Component - proxied call (automatic) * 'use client'; * const client = createUniversalClient(); * const result = await client.call(loginContract, { body: {...} }); // Goes through /api/proxy * ``` */ declare class UniversalClient { private readonly directClient; private readonly proxyBasePath; private readonly isServer; private readonly fetchImpl; constructor(config?: UniversalClientConfig); /** * Make a type-safe API call using a contract * * Automatically routes based on environment: * - Server: Direct SPFN API call * - Browser: Next.js API Route proxy * * @param contract - Route contract with absolute path * @param options - Call options (params, query, body, headers) */ call<TContract extends RouteContract>(contract: TContract, options?: CallOptions<TContract>): Promise<InferContract<TContract>['response']>; /** * Call via Next.js API Route proxy (client-side) * * Routes request through /api/proxy/[...path] to enable: * - HttpOnly cookie forwarding * - CORS security * - Server-side session management * * @private */ private callViaProxy; /** * Build URL path with parameter substitution */ private buildUrlPath; /** * Build query string from query parameters */ private buildQueryString; /** * Get HTTP method from contract or infer from options */ private getHttpMethod; /** * Check if body is FormData */ private isFormData; /** * Check if currently running in server environment */ isServerEnv(): boolean; /** * Create a new client with merged configuration */ withConfig(config: Partial<UniversalClientConfig>): UniversalClient; } /** * Create a new universal API client * * @example * ```typescript * // Default configuration * const client = createUniversalClient(); * * // Custom configuration * const client = createUniversalClient({ * apiUrl: 'http://localhost:4000', * proxyBasePath: '/api/spfn', * headers: { 'X-App-Version': '1.0.0' }, * }); * ``` */ declare function createUniversalClient(config?: UniversalClientConfig): UniversalClient; /** * Configure the global universal client instance * * Call this in your app initialization to set default configuration * for all auto-generated API calls. * * @example * ```typescript * // In app initialization (layout.tsx, _app.tsx, etc) * import { configureUniversalClient } from '@spfn/core/client'; * * configureUniversalClient({ * apiUrl: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8790', * proxyBasePath: '/api/proxy', * headers: { * 'X-App-Version': '1.0.0' * } * }); * ``` */ declare function configureUniversalClient(config: UniversalClientConfig): void; /** * Get the global universal client instance * * Creates a default instance if not configured */ declare function getUniversalClient(): UniversalClient; /** * Global universal client singleton with Proxy * * This client can be configured using configureUniversalClient() before use. * Used by auto-generated API client code. */ declare const universalClient: UniversalClient; export { ApiClientError, type CallOptions, type UniversalClientConfig as ClientConfig, ContractClient, type RequestInterceptor, UniversalClient, type UniversalClientConfig, universalClient as client, configureUniversalClient as configureClient, configureUniversalClient, createUniversalClient as createClient, createUniversalClient, getServerErrorDetails, getServerErrorType, getUniversalClient, isHttpError, isNetworkError, isServerError, isTimeoutError, universalClient };