|
|
|
|
@@ -14,9 +14,9 @@ export namespace Endpoints {
|
|
|
|
|
// <Endpoints>
|
|
|
|
|
|
|
|
|
|
export type get_Get_health_info = {
|
|
|
|
|
method: 'GET';
|
|
|
|
|
path: '/api/health/info';
|
|
|
|
|
requestFormat: 'json';
|
|
|
|
|
method: "GET";
|
|
|
|
|
path: "/api/health/info";
|
|
|
|
|
requestFormat: "json";
|
|
|
|
|
parameters: never;
|
|
|
|
|
responses: { 200: Schemas.HealthInfo; 404: unknown };
|
|
|
|
|
};
|
|
|
|
|
@@ -27,14 +27,14 @@ export namespace Endpoints {
|
|
|
|
|
// <EndpointByMethod>
|
|
|
|
|
export type EndpointByMethod = {
|
|
|
|
|
get: {
|
|
|
|
|
'/api/health/info': Endpoints.get_Get_health_info;
|
|
|
|
|
"/api/health/info": Endpoints.get_Get_health_info;
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// </EndpointByMethod>
|
|
|
|
|
|
|
|
|
|
// <EndpointByMethod.Shorthands>
|
|
|
|
|
export type GetEndpoints = EndpointByMethod['get'];
|
|
|
|
|
export type GetEndpoints = EndpointByMethod["get"];
|
|
|
|
|
// </EndpointByMethod.Shorthands>
|
|
|
|
|
|
|
|
|
|
// <ApiClientTypes>
|
|
|
|
|
@@ -45,10 +45,10 @@ export type EndpointParameters = {
|
|
|
|
|
path?: Record<string, unknown>;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export type MutationMethod = 'post' | 'put' | 'patch' | 'delete';
|
|
|
|
|
export type Method = 'get' | 'head' | 'options' | MutationMethod;
|
|
|
|
|
export type MutationMethod = "post" | "put" | "patch" | "delete";
|
|
|
|
|
export type Method = "get" | "head" | "options" | MutationMethod;
|
|
|
|
|
|
|
|
|
|
type RequestFormat = 'json' | 'form-data' | 'form-url' | 'binary' | 'text';
|
|
|
|
|
type RequestFormat = "json" | "form-data" | "form-url" | "binary" | "text";
|
|
|
|
|
|
|
|
|
|
export type DefaultEndpoint = {
|
|
|
|
|
parameters?: EndpointParameters | undefined;
|
|
|
|
|
@@ -61,14 +61,14 @@ export type Endpoint<TConfig extends DefaultEndpoint = DefaultEndpoint> = {
|
|
|
|
|
method: Method;
|
|
|
|
|
path: string;
|
|
|
|
|
requestFormat: RequestFormat;
|
|
|
|
|
parameters?: TConfig['parameters'];
|
|
|
|
|
parameters?: TConfig["parameters"];
|
|
|
|
|
meta: {
|
|
|
|
|
alias: string;
|
|
|
|
|
hasParameters: boolean;
|
|
|
|
|
areParametersRequired: boolean;
|
|
|
|
|
};
|
|
|
|
|
responses?: TConfig['responses'];
|
|
|
|
|
responseHeaders?: TConfig['responseHeaders'];
|
|
|
|
|
responses?: TConfig["responses"];
|
|
|
|
|
responseHeaders?: TConfig["responseHeaders"];
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export interface Fetcher {
|
|
|
|
|
@@ -87,28 +87,30 @@ export interface Fetcher {
|
|
|
|
|
parseResponseData?: (response: Response) => Promise<unknown>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const successStatusCodes = [200, 201, 202, 203, 204, 205, 206, 207, 208, 226, 300, 301, 302, 303, 304, 305, 306, 307, 308] as const;
|
|
|
|
|
export const successStatusCodes = [
|
|
|
|
|
200, 201, 202, 203, 204, 205, 206, 207, 208, 226, 300, 301, 302, 303, 304, 305, 306, 307, 308,
|
|
|
|
|
] as const;
|
|
|
|
|
export type SuccessStatusCode = (typeof successStatusCodes)[number];
|
|
|
|
|
|
|
|
|
|
export const errorStatusCodes = [
|
|
|
|
|
400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418, 421, 422, 423, 424, 425, 426, 428, 429, 431, 451, 500, 501, 502, 503,
|
|
|
|
|
504, 505, 506, 507, 508, 510, 511,
|
|
|
|
|
400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418, 421, 422, 423, 424,
|
|
|
|
|
425, 426, 428, 429, 431, 451, 500, 501, 502, 503, 504, 505, 506, 507, 508, 510, 511,
|
|
|
|
|
] as const;
|
|
|
|
|
export type ErrorStatusCode = (typeof errorStatusCodes)[number];
|
|
|
|
|
|
|
|
|
|
// Taken from https://github.com/unjs/fetchdts/blob/ec4eaeab5d287116171fc1efd61f4a1ad34e4609/src/fetch.ts#L3
|
|
|
|
|
export interface TypedHeaders<TypedHeaderValues extends Record<string, string> | unknown>
|
|
|
|
|
extends Omit<Headers, 'append' | 'delete' | 'get' | 'getSetCookie' | 'has' | 'set' | 'forEach'> {
|
|
|
|
|
extends Omit<Headers, "append" | "delete" | "get" | "getSetCookie" | "has" | "set" | "forEach"> {
|
|
|
|
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/append) */
|
|
|
|
|
append: <Name extends Extract<keyof TypedHeaderValues, string> | (string & {})>(
|
|
|
|
|
name: Name,
|
|
|
|
|
value: Lowercase<Name> extends keyof TypedHeaderValues ? TypedHeaderValues[Lowercase<Name>] : string
|
|
|
|
|
value: Lowercase<Name> extends keyof TypedHeaderValues ? TypedHeaderValues[Lowercase<Name>] : string,
|
|
|
|
|
) => void;
|
|
|
|
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/delete) */
|
|
|
|
|
delete: <Name extends Extract<keyof TypedHeaderValues, string> | (string & {})>(name: Name) => void;
|
|
|
|
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/get) */
|
|
|
|
|
get: <Name extends Extract<keyof TypedHeaderValues, string> | (string & {})>(
|
|
|
|
|
name: Name
|
|
|
|
|
name: Name,
|
|
|
|
|
) => (Lowercase<Name> extends keyof TypedHeaderValues ? TypedHeaderValues[Lowercase<Name>] : string) | null;
|
|
|
|
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/getSetCookie) */
|
|
|
|
|
getSetCookie: () => string[];
|
|
|
|
|
@@ -117,20 +119,21 @@ export interface TypedHeaders<TypedHeaderValues extends Record<string, string> |
|
|
|
|
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/set) */
|
|
|
|
|
set: <Name extends Extract<keyof TypedHeaderValues, string> | (string & {})>(
|
|
|
|
|
name: Name,
|
|
|
|
|
value: Lowercase<Name> extends keyof TypedHeaderValues ? TypedHeaderValues[Lowercase<Name>] : string
|
|
|
|
|
value: Lowercase<Name> extends keyof TypedHeaderValues ? TypedHeaderValues[Lowercase<Name>] : string,
|
|
|
|
|
) => void;
|
|
|
|
|
forEach: (
|
|
|
|
|
callbackfn: (
|
|
|
|
|
value: TypedHeaderValues[keyof TypedHeaderValues] | (string & {}),
|
|
|
|
|
key: Extract<keyof TypedHeaderValues, string> | (string & {}),
|
|
|
|
|
parent: TypedHeaders<TypedHeaderValues>
|
|
|
|
|
parent: TypedHeaders<TypedHeaderValues>,
|
|
|
|
|
) => void,
|
|
|
|
|
thisArg?: any
|
|
|
|
|
thisArg?: any,
|
|
|
|
|
) => void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** @see https://developer.mozilla.org/en-US/docs/Web/API/Response */
|
|
|
|
|
export interface TypedSuccessResponse<TSuccess, TStatusCode, THeaders> extends Omit<Response, 'ok' | 'status' | 'json' | 'headers'> {
|
|
|
|
|
export interface TypedSuccessResponse<TSuccess, TStatusCode, THeaders>
|
|
|
|
|
extends Omit<Response, "ok" | "status" | "json" | "headers"> {
|
|
|
|
|
ok: true;
|
|
|
|
|
status: TStatusCode;
|
|
|
|
|
headers: never extends THeaders ? Headers : TypedHeaders<THeaders>;
|
|
|
|
|
@@ -140,7 +143,8 @@ export interface TypedSuccessResponse<TSuccess, TStatusCode, THeaders> extends O
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** @see https://developer.mozilla.org/en-US/docs/Web/API/Response */
|
|
|
|
|
export interface TypedErrorResponse<TData, TStatusCode, THeaders> extends Omit<Response, 'ok' | 'status' | 'json' | 'headers'> {
|
|
|
|
|
export interface TypedErrorResponse<TData, TStatusCode, THeaders>
|
|
|
|
|
extends Omit<Response, "ok" | "status" | "json" | "headers"> {
|
|
|
|
|
ok: false;
|
|
|
|
|
status: TStatusCode;
|
|
|
|
|
headers: never extends THeaders ? Headers : TypedHeaders<THeaders>;
|
|
|
|
|
@@ -157,10 +161,10 @@ export type TypedApiResponse<TAllResponses extends Record<string | number, unkno
|
|
|
|
|
: TypedErrorResponse<TAllResponses[K], TStatusCode, K extends keyof THeaders ? THeaders[K] : never>
|
|
|
|
|
: never
|
|
|
|
|
: K extends number
|
|
|
|
|
? K extends SuccessStatusCode
|
|
|
|
|
? TypedSuccessResponse<TAllResponses[K], K, K extends keyof THeaders ? THeaders[K] : never>
|
|
|
|
|
: TypedErrorResponse<TAllResponses[K], K, K extends keyof THeaders ? THeaders[K] : never>
|
|
|
|
|
: never;
|
|
|
|
|
? K extends SuccessStatusCode
|
|
|
|
|
? TypedSuccessResponse<TAllResponses[K], K, K extends keyof THeaders ? THeaders[K] : never>
|
|
|
|
|
: TypedErrorResponse<TAllResponses[K], K, K extends keyof THeaders ? THeaders[K] : never>
|
|
|
|
|
: never;
|
|
|
|
|
}[keyof TAllResponses];
|
|
|
|
|
|
|
|
|
|
export type SafeApiResponse<TEndpoint> = TEndpoint extends { responses: infer TResponses }
|
|
|
|
|
@@ -169,7 +173,10 @@ export type SafeApiResponse<TEndpoint> = TEndpoint extends { responses: infer TR
|
|
|
|
|
: never
|
|
|
|
|
: never;
|
|
|
|
|
|
|
|
|
|
export type InferResponseByStatus<TEndpoint, TStatusCode> = Extract<SafeApiResponse<TEndpoint>, { status: TStatusCode }>;
|
|
|
|
|
export type InferResponseByStatus<TEndpoint, TStatusCode> = Extract<
|
|
|
|
|
SafeApiResponse<TEndpoint>,
|
|
|
|
|
{ status: TStatusCode }
|
|
|
|
|
>;
|
|
|
|
|
|
|
|
|
|
type RequiredKeys<T> = {
|
|
|
|
|
[P in keyof T]-?: undefined extends T[P] ? never : P;
|
|
|
|
|
@@ -186,7 +193,7 @@ export class TypedStatusError<TData = unknown> extends Error {
|
|
|
|
|
status: number;
|
|
|
|
|
constructor(response: TypedErrorResponse<TData, ErrorStatusCode, unknown>) {
|
|
|
|
|
super(`HTTP ${response.status}: ${response.statusText}`);
|
|
|
|
|
this.name = 'TypedStatusError';
|
|
|
|
|
this.name = "TypedStatusError";
|
|
|
|
|
this.response = response;
|
|
|
|
|
this.status = response.status;
|
|
|
|
|
}
|
|
|
|
|
@@ -195,7 +202,7 @@ export class TypedStatusError<TData = unknown> extends Error {
|
|
|
|
|
|
|
|
|
|
// <ApiClient>
|
|
|
|
|
export class ApiClient {
|
|
|
|
|
baseUrl: string = '';
|
|
|
|
|
baseUrl: string = "";
|
|
|
|
|
successStatusCodes = successStatusCodes;
|
|
|
|
|
errorStatusCodes = errorStatusCodes;
|
|
|
|
|
|
|
|
|
|
@@ -211,7 +218,9 @@ export class ApiClient {
|
|
|
|
|
* Supports both OpenAPI format {param} and Express format :param
|
|
|
|
|
*/
|
|
|
|
|
defaultDecodePathParams = (url: string, params: Record<string, string>): string => {
|
|
|
|
|
return url.replace(/{(\w+)}/g, (_, key: string) => params[key] || `{${key}}`).replace(/:([a-zA-Z0-9_]+)/g, (_, key: string) => params[key] || `:${key}`);
|
|
|
|
|
return url
|
|
|
|
|
.replace(/{(\w+)}/g, (_, key: string) => params[key] || `{${key}}`)
|
|
|
|
|
.replace(/:([a-zA-Z0-9_]+)/g, (_, key: string) => params[key] || `:${key}`);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/** Uses URLSearchParams, skips null/undefined values */
|
|
|
|
|
@@ -234,16 +243,20 @@ export class ApiClient {
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
defaultParseResponseData = async (response: Response): Promise<unknown> => {
|
|
|
|
|
const contentType = response.headers.get('content-type') ?? '';
|
|
|
|
|
if (contentType.startsWith('text/')) {
|
|
|
|
|
const contentType = response.headers.get("content-type") ?? "";
|
|
|
|
|
if (contentType.startsWith("text/")) {
|
|
|
|
|
return await response.text();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (contentType === 'application/octet-stream') {
|
|
|
|
|
if (contentType === "application/octet-stream") {
|
|
|
|
|
return await response.arrayBuffer();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (contentType.includes('application/json') || (contentType.includes('application/') && contentType.includes('json')) || contentType === '*/*') {
|
|
|
|
|
if (
|
|
|
|
|
contentType.includes("application/json") ||
|
|
|
|
|
(contentType.includes("application/") && contentType.includes("json")) ||
|
|
|
|
|
contentType === "*/*"
|
|
|
|
|
) {
|
|
|
|
|
try {
|
|
|
|
|
return await response.json();
|
|
|
|
|
} catch {
|
|
|
|
|
@@ -264,7 +277,7 @@ export class ApiClient {
|
|
|
|
|
: { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean }
|
|
|
|
|
: { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean }
|
|
|
|
|
>
|
|
|
|
|
): Promise<Extract<InferResponseByStatus<TEndpoint, SuccessStatusCode>, { data: {} }>['data']>;
|
|
|
|
|
): Promise<Extract<InferResponseByStatus<TEndpoint, SuccessStatusCode>, { data: {} }>["data"]>;
|
|
|
|
|
|
|
|
|
|
get<Path extends keyof GetEndpoints, TEndpoint extends GetEndpoints[Path]>(
|
|
|
|
|
path: Path,
|
|
|
|
|
@@ -277,8 +290,11 @@ export class ApiClient {
|
|
|
|
|
>
|
|
|
|
|
): Promise<SafeApiResponse<TEndpoint>>;
|
|
|
|
|
|
|
|
|
|
get<Path extends keyof GetEndpoints, _TEndpoint extends GetEndpoints[Path]>(path: Path, ...params: MaybeOptionalArg<any>): Promise<any> {
|
|
|
|
|
return this.request('get', path, ...params);
|
|
|
|
|
get<Path extends keyof GetEndpoints, _TEndpoint extends GetEndpoints[Path]>(
|
|
|
|
|
path: Path,
|
|
|
|
|
...params: MaybeOptionalArg<any>
|
|
|
|
|
): Promise<any> {
|
|
|
|
|
return this.request("get", path, ...params);
|
|
|
|
|
}
|
|
|
|
|
// </ApiClient.get>
|
|
|
|
|
|
|
|
|
|
@@ -286,7 +302,11 @@ export class ApiClient {
|
|
|
|
|
/**
|
|
|
|
|
* Generic request method with full type-safety for any endpoint
|
|
|
|
|
*/
|
|
|
|
|
request<TMethod extends keyof EndpointByMethod, TPath extends keyof EndpointByMethod[TMethod], TEndpoint extends EndpointByMethod[TMethod][TPath]>(
|
|
|
|
|
request<
|
|
|
|
|
TMethod extends keyof EndpointByMethod,
|
|
|
|
|
TPath extends keyof EndpointByMethod[TMethod],
|
|
|
|
|
TEndpoint extends EndpointByMethod[TMethod][TPath],
|
|
|
|
|
>(
|
|
|
|
|
method: TMethod,
|
|
|
|
|
path: TPath,
|
|
|
|
|
...params: MaybeOptionalArg<
|
|
|
|
|
@@ -296,9 +316,13 @@ export class ApiClient {
|
|
|
|
|
: { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean }
|
|
|
|
|
: { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean }
|
|
|
|
|
>
|
|
|
|
|
): Promise<Extract<InferResponseByStatus<TEndpoint, SuccessStatusCode>, { data: {} }>['data']>;
|
|
|
|
|
): Promise<Extract<InferResponseByStatus<TEndpoint, SuccessStatusCode>, { data: {} }>["data"]>;
|
|
|
|
|
|
|
|
|
|
request<TMethod extends keyof EndpointByMethod, TPath extends keyof EndpointByMethod[TMethod], TEndpoint extends EndpointByMethod[TMethod][TPath]>(
|
|
|
|
|
request<
|
|
|
|
|
TMethod extends keyof EndpointByMethod,
|
|
|
|
|
TPath extends keyof EndpointByMethod[TMethod],
|
|
|
|
|
TEndpoint extends EndpointByMethod[TMethod][TPath],
|
|
|
|
|
>(
|
|
|
|
|
method: TMethod,
|
|
|
|
|
path: TPath,
|
|
|
|
|
...params: MaybeOptionalArg<
|
|
|
|
|
@@ -310,14 +334,19 @@ export class ApiClient {
|
|
|
|
|
>
|
|
|
|
|
): Promise<SafeApiResponse<TEndpoint>>;
|
|
|
|
|
|
|
|
|
|
request<TMethod extends keyof EndpointByMethod, TPath extends keyof EndpointByMethod[TMethod], TEndpoint extends EndpointByMethod[TMethod][TPath]>(
|
|
|
|
|
method: TMethod,
|
|
|
|
|
path: TPath,
|
|
|
|
|
...params: MaybeOptionalArg<any>
|
|
|
|
|
): Promise<any> {
|
|
|
|
|
request<
|
|
|
|
|
TMethod extends keyof EndpointByMethod,
|
|
|
|
|
TPath extends keyof EndpointByMethod[TMethod],
|
|
|
|
|
TEndpoint extends EndpointByMethod[TMethod][TPath],
|
|
|
|
|
>(method: TMethod, path: TPath, ...params: MaybeOptionalArg<any>): Promise<any> {
|
|
|
|
|
const requestParams = params[0];
|
|
|
|
|
const withResponse = requestParams?.withResponse;
|
|
|
|
|
const { withResponse: _, throwOnStatusError = withResponse ? false : true, overrides, ...fetchParams } = requestParams || {};
|
|
|
|
|
const {
|
|
|
|
|
withResponse: _,
|
|
|
|
|
throwOnStatusError = withResponse ? false : true,
|
|
|
|
|
overrides,
|
|
|
|
|
...fetchParams
|
|
|
|
|
} = requestParams || {};
|
|
|
|
|
|
|
|
|
|
const parametersToSend: EndpointParameters = {};
|
|
|
|
|
if (requestParams?.body !== undefined) (parametersToSend as any).body = requestParams.body;
|
|
|
|
|
@@ -327,9 +356,8 @@ export class ApiClient {
|
|
|
|
|
|
|
|
|
|
const resolvedPath = (this.fetcher.decodePathParams ?? this.defaultDecodePathParams)(
|
|
|
|
|
this.baseUrl + (path as string),
|
|
|
|
|
(parametersToSend.path ?? {}) as Record<string, string>
|
|
|
|
|
(parametersToSend.path ?? {}) as Record<string, string>,
|
|
|
|
|
);
|
|
|
|
|
console.log('Resolved Path:', resolvedPath);
|
|
|
|
|
const url = new URL(resolvedPath);
|
|
|
|
|
const urlSearchParams = (this.fetcher.encodeSearchParams ?? this.defaultEncodeSearchParams)(parametersToSend.query);
|
|
|
|
|
|
|
|
|
|
@@ -357,13 +385,13 @@ export class ApiClient {
|
|
|
|
|
return withResponse ? typedResponse : data;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return promise as Extract<InferResponseByStatus<TEndpoint, SuccessStatusCode>, { data: {} }>['data'];
|
|
|
|
|
return promise as Extract<InferResponseByStatus<TEndpoint, SuccessStatusCode>, { data: {} }>["data"];
|
|
|
|
|
}
|
|
|
|
|
// </ApiClient.request>
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function createApiClient(fetcher: Fetcher, baseUrl?: string) {
|
|
|
|
|
return new ApiClient(fetcher).setBaseUrl(baseUrl ?? '');
|
|
|
|
|
return new ApiClient(fetcher).setBaseUrl(baseUrl ?? "");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|