UNPKG

@spfn/core

Version:

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

185 lines (184 loc) 5.71 kB
// src/client/contract-client.ts var ApiClientError = class extends Error { constructor(message, status, url, response, errorType) { super(message); this.status = status; this.url = url; this.response = response; this.errorType = errorType; this.name = "ApiClientError"; } }; var ContractClient = class _ContractClient { config; interceptors = []; constructor(config = {}) { this.config = { baseUrl: config.baseUrl || process.env.SERVER_API_URL || process.env.NEXT_PUBLIC_API_URL || "http://localhost:4000", headers: config.headers || {}, timeout: config.timeout || 3e4, fetch: config.fetch || globalThis.fetch.bind(globalThis) }; } /** * Add request interceptor */ use(interceptor) { this.interceptors.push(interceptor); } /** * Make a type-safe API call using a contract * * @param contract - Route contract with absolute path * @param options - Call options (params, query, body, headers) */ async call(contract, options) { const baseUrl = options?.baseUrl || this.config.baseUrl; const urlPath = _ContractClient.buildUrl( contract.path, options?.params ); const queryString = _ContractClient.buildQuery( options?.query ); const url = `${baseUrl}${urlPath}${queryString}`; const method = _ContractClient.getHttpMethod(contract, options); const headers = { ...this.config.headers, ...options?.headers }; const isFormData = _ContractClient.isFormData(options?.body); if (options?.body !== void 0 && !isFormData && !headers["Content-Type"]) { headers["Content-Type"] = "application/json"; } let init = { method, headers }; if (options?.body !== void 0) { init.body = isFormData ? options.body : JSON.stringify(options.body); } const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), this.config.timeout); init.signal = controller.signal; for (const interceptor of this.interceptors) { init = await interceptor(url, init); } const response = await this.config.fetch(url, init).catch((error) => { clearTimeout(timeoutId); if (error instanceof Error && error.name === "AbortError") { throw new ApiClientError( `Request timed out after ${this.config.timeout}ms`, 0, url, void 0, "timeout" ); } if (error instanceof Error) { throw new ApiClientError( `Network error: ${error.message}`, 0, url, void 0, "network" ); } throw error; }); clearTimeout(timeoutId); if (!response.ok) { const errorBody = await response.json().catch(() => null); throw new ApiClientError( `${method} ${urlPath} failed: ${response.status} ${response.statusText}`, response.status, url, errorBody, "http" ); } const data = await response.json(); return data; } /** * Create a new client with merged configuration */ withConfig(config) { return new _ContractClient({ baseUrl: config.baseUrl || this.config.baseUrl, headers: { ...this.config.headers, ...config.headers }, timeout: config.timeout || this.config.timeout, fetch: config.fetch || this.config.fetch }); } static buildUrl(path, params) { if (!params) return path; let url = path; for (const [key, value] of Object.entries(params)) { url = url.replace(`:${key}`, String(value)); } return url; } static buildQuery(query) { if (!query || Object.keys(query).length === 0) return ""; const params = new URLSearchParams(); for (const [key, value] of Object.entries(query)) { if (Array.isArray(value)) { value.forEach((v) => params.append(key, String(v))); } else if (value !== void 0 && value !== null) { params.append(key, String(value)); } } const queryString = params.toString(); return queryString ? `?${queryString}` : ""; } static getHttpMethod(contract, options) { if ("method" in contract && typeof contract.method === "string") { return contract.method.toUpperCase(); } if (options?.body !== void 0) { return "POST"; } return "GET"; } static isFormData(body) { return body instanceof FormData; } }; function createClient(config) { return new ContractClient(config); } var _clientInstance = new ContractClient(); function configureClient(config) { _clientInstance = new ContractClient(config); } var client = new Proxy({}, { get(_target, prop) { return _clientInstance[prop]; } }); function isTimeoutError(error) { return error instanceof ApiClientError && error.errorType === "timeout"; } function isNetworkError(error) { return error instanceof ApiClientError && error.errorType === "network"; } function isHttpError(error) { return error instanceof ApiClientError && error.errorType === "http"; } function isServerError(error, errorType) { if (!isHttpError(error)) return false; const response = error.response; return response?.error?.type === errorType; } function getServerErrorType(error) { const response = error.response; return response?.error?.type; } function getServerErrorDetails(error) { const response = error.response; return response?.error?.details; } export { ApiClientError, ContractClient, client, configureClient, createClient, getServerErrorDetails, getServerErrorType, isHttpError, isNetworkError, isServerError, isTimeoutError }; //# sourceMappingURL=index.js.map //# sourceMappingURL=index.js.map