@spfn/core
Version:
SPFN Framework Core - File-based routing, transactions, repository pattern
185 lines (184 loc) • 5.71 kB
JavaScript
// 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