fets
Version:
TypeScript HTTP Framework focusing on e2e type-safety, easy setup, performance & great developer experience
475 lines (474 loc) • 20.2 kB
TypeScript
import type { B, Call, Fn, Objects, Pipe, Strings, Tuples } from 'hotscript';
import { type O } from 'ts-toolbelt';
import { HTTPMethod, NotOkStatusCode, OkStatusCode, StatusCode, TypedResponse } from '../typed-fetch.js';
import type { ExtractPathParamsWithBrackets, ExtractPathParamsWithPattern, FromSchema, JSONSchema, OpenAPIDocument, Simplify } from '../types.js';
import type { OASOAuthPath, OAuth2AuthParams } from './auth/oauth.js';
import { ClientTypedResponsePromise } from './clientResponse.js';
type JSONSchema7TypeName = 'string' | 'number' | 'integer' | 'boolean' | 'object' | 'array' | 'null';
type Mutable<Type> = FixJSONSchema<{
-readonly [Key in keyof Type]: Mutable<Type[Key]>;
}>;
type RefToPath<T extends string> = T extends `#/${infer Ref}` ? Call<Strings.Split<'/'>, Ref> : never;
type ResolveRef<TObj, TRef extends string> = {
$id: TRef;
} & O.Path<TObj, RefToPath<TRef>>;
type ResolveRefInObj<T, TBase> = T extends {
$ref: infer Ref;
} ? Ref extends string ? ResolveRef<TBase, Ref> : T : T;
type ResolveRefsInObj<T, TBase = T> = {
[K in keyof T]: ResolveRefsInObj<ResolveRefInObj<T[K], TBase>, TBase>;
};
/**
* Resolve all $refs in the OpenAPI document and normalizes the types for the client generic
*/
export type NormalizeOAS<TOAS> = Mutable<ResolveRefsInObj<TOAS>>;
export type OASPathMap<TOAS extends OpenAPIDocument> = TOAS['paths'];
export type OASMethodMap<TOAS extends OpenAPIDocument, TPath extends keyof OASPathMap<TOAS>> = OASPathMap<TOAS>[TPath];
export type OASStatusMap<TOAS extends OpenAPIDocument, TPath extends keyof OASPathMap<TOAS>, TMethod extends keyof OASMethodMap<TOAS, TPath>> = OASMethodMap<TOAS, TPath>[TMethod] extends {
responses: any;
} ? OASMethodMap<TOAS, TPath>[TMethod]['responses'] : never;
export type OASResponseSchemas<TOAS extends OpenAPIDocument, TPath extends keyof OASPathMap<TOAS>, TMethod extends keyof OASMethodMap<TOAS, TPath>, TStatus extends keyof OASStatusMap<TOAS, TPath, TMethod>> = OASStatusMap<TOAS, TPath, TMethod>[TStatus]['content'];
export type OASJSONResponseSchema<TOAS extends OpenAPIDocument, TPath extends keyof OASPathMap<TOAS>, TMethod extends keyof OASMethodMap<TOAS, TPath>, TStatus extends keyof OASStatusMap<TOAS, TPath, TMethod>> = OASStatusMap<TOAS, TPath, TMethod>[TStatus] extends {
content: any;
} ? OASResponseSchemas<TOAS, TPath, TMethod, TStatus>[keyof OASResponseSchemas<TOAS, TPath, TMethod, TStatus>]['schema'] : OASStatusMap<TOAS, TPath, TMethod>[TStatus]['schema'];
type ToNumber<T> = T extends `${infer N extends number}` ? N : never;
export type OASResponse<TOAS extends OpenAPIDocument, TPath extends keyof TOAS['paths'], TMethod extends keyof TOAS['paths'][TPath]> = {
[TStatus in keyof OASStatusMap<TOAS, TPath, TMethod>]: TypedResponse<FromSchema<OASJSONResponseSchema<TOAS, TPath, TMethod, TStatus>>, Record<string, string>, TStatus extends StatusCode ? TStatus : TStatus extends 'default' ? OASStatusMap<TOAS, TPath, TMethod> extends Record<'200' | 200, any> ? Exclude<NotOkStatusCode, ToNumber<Exclude<keyof OASStatusMap<TOAS, TPath, TMethod>, symbol>>> : Exclude<OkStatusCode, ToNumber<Exclude<keyof OASStatusMap<TOAS, TPath, TMethod>, symbol>>> : TStatus extends `${StatusCode}` ? ToNumber<TStatus> : 200>;
}[keyof OASStatusMap<TOAS, TPath, TMethod>];
interface OASParamPropMap {
query: 'query';
path: 'params';
header: 'headers';
}
export type OASParamObj<TParameter extends {
name: string;
}> = TParameter extends {
required: true;
} ? {
[TName in TParameter['name']]: TParameter extends {
schema: JSONSchema;
} ? FromSchema<TParameter['schema']> : TParameter extends {
type: JSONSchema7TypeName;
enum?: any[];
} ? FromSchema<{
type: TParameter['type'];
enum: TParameter['enum'];
}> : unknown;
} : {
[TName in TParameter['name']]?: TParameter extends {
schema: JSONSchema;
} ? FromSchema<TParameter['schema']> : TParameter extends {
type: JSONSchema7TypeName;
enum?: any[];
} ? FromSchema<{
type: TParameter['type'];
enum: TParameter['enum'];
}> : unknown;
};
interface OASParamToRequestParam<TParameters extends {
in: string;
required?: boolean;
}[]> extends Fn {
return: this['arg0'] extends {
name: string;
in: infer TParamType;
} ? Extract<TParameters[number], {
in: TParamType;
required: true;
}> extends never ? {
[TKey in TParamType extends keyof OASParamPropMap ? OASParamPropMap[TParamType] : never]?: OASParamObj<this['arg0']>;
} : {
[TKey in TParamType extends keyof OASParamPropMap ? OASParamPropMap[TParamType] : never]: OASParamObj<this['arg0']>;
} : {};
}
export type OASParamMap<TParameters extends {
name: string;
in: string;
}[]> = Pipe<TParameters, [
Tuples.Map<OASParamToRequestParam<TParameters>>,
Tuples.ToIntersection
]>;
export type OASClient<TOAS extends OpenAPIDocument, TAuthParamsRequired extends boolean = true> = {
[TPath in keyof OASPathMap<TOAS>]: {
[TMethod in keyof OASMethodMap<TOAS, TPath>]: OASRequestParams<TOAS, TPath, TMethod, TAuthParamsRequired> extends {
json: {};
} | {
params: {};
} | {
headers: {};
} | {
query: {};
} ? (requestParams: Simplify<OASRequestParams<TOAS, TPath, TMethod, TAuthParamsRequired>> & ClientRequestInit) => ClientTypedResponsePromise<OASResponse<TOAS, TPath, TMethod>> : (requestParams?: Simplify<OASRequestParams<TOAS, TPath, TMethod, TAuthParamsRequired>> & ClientRequestInit) => ClientTypedResponsePromise<OASResponse<TOAS, TPath, TMethod>>;
};
} & OASOAuthPath<TOAS>;
export type OASModel<TOAS extends OpenAPIDocument, TName extends TOAS extends {
components: {
schemas: Record<string, JSONSchema>;
};
} ? keyof TOAS['components']['schemas'] : TOAS extends {
definitions: Record<string, JSONSchema>;
} ? keyof TOAS['definitions'] : never> = TOAS extends {
components: {
schemas: {
[TModelName in TName]: JSONSchema;
};
};
} ? FromSchema<TOAS['components']['schemas'][TName]> : TOAS extends {
definitions: {
[TModelName in TName]: JSONSchema;
};
} ? FromSchema<TOAS['definitions'][TName]> : never;
export type FixJSONSchema<T> = RemoveExclusiveMinimumAndMaximum<FixAdditionalPropertiesForAllOf<FixMissingAdditionalProperties<FixMissingTypeObject<FixExtraRequiredFields<T>>>>>;
type FixAdditionalPropertiesForAllOf<T> = T extends {
allOf: any[];
} ? Omit<T, 'allOf'> & {
allOf: Call<Tuples.Map<Objects.Omit<'additionalProperties'>>, T['allOf']>;
} : T;
type LooksLikeSchemaObject<T> = T extends {
type: JSONSchema7TypeName;
} | {
$ref: string;
} | {
enum: any;
} | {
const: any;
} | {
oneOf: any;
} | {
anyOf: any;
} | {
allOf: any;
} | {
not: any;
} | {
additionalProperties: any;
} | {
items: any;
} ? true : false;
type FixMissingTypeObject<T> = T extends {
type: any;
properties: any;
} ? T : T extends {
properties: infer TProps;
} ? LooksLikeSchemaObject<TProps> extends true ? T : T & {
type: 'object';
} : T;
type FixMissingAdditionalProperties<T> = T extends {
type: 'object';
properties: any;
} ? Omit<T, 'additionalProperties'> & {
additionalProperties: false;
} : T extends {
type: readonly (infer TType)[];
properties: any;
} ? 'object' extends TType ? Omit<T, 'additionalProperties'> & {
additionalProperties: false;
} : T : T;
type FixExtraRequiredFields<T> = T extends {
properties: Record<string, any>;
required: string[];
} ? Omit<T, 'required'> & {
required: Call<Tuples.Filter<B.Extends<keyof T['properties']>>, T['required']>;
} : T;
type RemoveExclusiveMinimumAndMaximum<T> = T extends {
exclusiveMinimum?: boolean;
exclusiveMaximum?: boolean;
} ? Omit<T, 'exclusiveMinimum' | 'exclusiveMaximum'> : T;
export type OASRequestParams<TOAS extends OpenAPIDocument, TPath extends keyof OASPathMap<TOAS>, TMethod extends keyof OASMethodMap<TOAS, TPath>, TAuthParamsRequired extends boolean = true> = (OASMethodMap<TOAS, TPath>[TMethod] extends {
requestBody: {
content: {
'application/json': {
schema: JSONSchema;
};
};
};
} ? OASMethodMap<TOAS, TPath>[TMethod]['requestBody'] extends {
required: true;
} ? {
/**
* The request body in JSON is required for this request.
*
* The value of `json` will be stringified and sent as the request body with `Content-Type: application/json`.
*/
json: FromSchema<OASMethodMap<TOAS, TPath>[TMethod]['requestBody']['content']['application/json']['schema']>;
} : {
/**
* The request body in JSON is optional for this request.
*
* The value of `json` will be stringified and sent as the request body with `Content-Type: application/json`.
*/
json?: FromSchema<OASMethodMap<TOAS, TPath>[TMethod]['requestBody']['content']['application/json']['schema']>;
} : OASMethodMap<TOAS, TPath>[TMethod] extends {
requestBody: {
content: {
'multipart/form-data': {
schema: JSONSchema;
};
};
};
} ? OASMethodMap<TOAS, TPath>[TMethod]['requestBody'] extends {
required: true;
} ? {
/**
* The request body in multipart/form-data is required for this request.
*
* The value of `formData` will be sent as the request body with `Content-Type: multipart/form-data`.
*/
formData: FromSchema<OASMethodMap<TOAS, TPath>[TMethod]['requestBody']['content']['multipart/form-data']['schema']>;
} : {
/**
* The request body in multipart/form-data is optional for this request.
*
* The value of `formData` will be sent as the request body with `Content-Type: multipart/form-data`.
*/
formData?: FromSchema<OASMethodMap<TOAS, TPath>[TMethod]['requestBody']['content']['multipart/form-data']['schema']>;
} : OASMethodMap<TOAS, TPath>[TMethod] extends {
requestBody: {
content: {
'application/x-www-form-urlencoded': {
schema: JSONSchema;
};
};
};
} ? OASMethodMap<TOAS, TPath>[TMethod]['requestBody'] extends {
required: true;
} ? {
/**
* The request body in application/x-www-form-urlencoded is required for this request.
*
* The value of `formUrlEncoded` will be sent as the request body with `Content-Type: application/x-www-form-urlencoded`.
*/
formUrlEncoded: FromSchema<OASMethodMap<TOAS, TPath>[TMethod]['requestBody']['content']['application/x-www-form-urlencoded']['schema']>;
} : {
/**
* The request body in application/x-www-form-urlencoded is optional for this request.
*
* The value of `formUrlEncoded` will be sent as the request body with `Content-Type: application/x-www-form-urlencoded`.
*/
formUrlEncoded?: FromSchema<OASMethodMap<TOAS, TPath>[TMethod]['requestBody']['content']['application/x-www-form-urlencoded']['schema']>;
} : {}) & (OASMethodMap<TOAS, TPath>[TMethod] extends {
parameters: {
name: string;
in: string;
}[];
} ? OASParamMap<OASMethodMap<TOAS, TPath>[TMethod]['parameters']> : {}) & (TPath extends `${string}{${string}}${string}` ? {
/**
* Parameters defined in the path are required for this request.
*
* The value of `params` will be used to replace the path parameters.
*
* For example if path is `/todos/{id}` and `params` is `{ id: '1' }`, the path will be `/todos/1`
*/
params: Record<ExtractPathParamsWithBrackets<TPath>, string | number | bigint | boolean>;
} : {}) & (TPath extends `${string}:${string}${string}` ? {
/**
* Parameters defined in the path are required for this request.
*
* The value of `params` will be used to replace the path parameters.
*
* For example if path is `/todos/:id` and `params` is `{ id: '1' }`, the path will be `/todos/1`.
*/
params: Record<ExtractPathParamsWithPattern<TPath>, string | number | bigint | boolean>;
} : {}) & (TAuthParamsRequired extends true ? OASSecurityParamsBySecurityRef<TOAS, OASMethodMap<TOAS, TPath>[TMethod]> : DeepPartial<OASSecurityParamsBySecurityRef<TOAS, OASMethodMap<TOAS, TPath>[TMethod]>>) & (TAuthParamsRequired extends true ? OASSecurityParamsBySecurityRef<TOAS, TOAS> : DeepPartial<OASSecurityParamsBySecurityRef<TOAS, TOAS>>);
type DeepPartial<T> = T extends Record<string, any> ? {
[K in keyof T]?: DeepPartial<T[K]>;
} : T;
export type OASInput<TOAS extends OpenAPIDocument, TPath extends keyof OASPathMap<TOAS>, TMethod extends keyof OASMethodMap<TOAS, TPath>, TRequestType extends keyof OASRequestParams<TOAS, TPath, TMethod>> = OASRequestParams<TOAS, TPath, TMethod>[TRequestType];
export type OASOutput<TOAS extends OpenAPIDocument, TPath extends keyof OASPathMap<TOAS>, TMethod extends keyof OASMethodMap<TOAS, TPath>, TStatusCode extends keyof OASStatusMap<TOAS, TPath, TMethod> = 200> = FromSchema<OASJSONResponseSchema<TOAS, TPath, TMethod, TStatusCode>>;
export type OASComponentSchema<TOAS extends OpenAPIDocument, TName extends string> = TOAS extends {
components: {
schemas: {
[TModelName in TName]: JSONSchema;
};
};
} ? FromSchema<TOAS['components']['schemas'][TName]> : never;
export interface ClientOptions {
/**
* The base URL of the API
*/
endpoint?: string;
/**
* WHATWG compatible fetch implementation
*
* @see https://the-guild.dev/openapi/fets/client/client-configuration#customizing-the-fetch-function
*/
fetchFn?: ClientFetchFn;
/**
* Plugins to extend the client functionality
*
* @see https://the-guild.dev/openapi/fets/client/plugins
*/
plugins?: ClientPlugin[];
/**
* Global parameters
*/
globalParams?: ClientRequestParams;
}
type ServerVariableType<TVarName extends string, TVariables> = TVariables extends Record<string, unknown> ? TVarName extends keyof TVariables ? TVariables[TVarName] extends {
enum: readonly (infer TEnum extends string)[];
} ? TEnum : string : string : string;
type ResolveServerUrl<TUrl extends string, TVariables> = TUrl extends `${infer Before}{${infer VarName}}${infer After}` ? `${Before}${ServerVariableType<VarName, TVariables>}${ResolveServerUrl<After, TVariables>}` : TUrl;
export type ClientOptionsWithStrictEndpoint<TOAS extends OpenAPIDocument> = Omit<ClientOptions, 'endpoint'> & (TOAS extends {
servers: (infer TEndpoint extends string)[];
} ? {
/**
* The base URL of the API defined in the OAS document.
*
* @see https://swagger.io/docs/specification/api-host-and-base-path/
*/
endpoint: TEndpoint;
} : TOAS extends {
servers: {
url: infer TEndpoint extends string;
variables?: infer TVariables;
}[];
} ? {
/**
* The base URL of the API defined in the OAS document.
*
* @see https://swagger.io/docs/specification/api-host-and-base-path/
*/
endpoint: ResolveServerUrl<TEndpoint, TVariables>;
} : TOAS extends {
host: infer THost extends string;
basePath: infer TBasePath extends string;
schemes: (infer TProtocol extends string)[];
} ? {
/**
* REST APIs have a base URL to which the endpoint paths are appended. The base URL is defined by `schemes`, `host` and `basePath` on the root level of the API specification.
*
* @see https://swagger.io/docs/specification/2-0/api-host-and-base-path/
*/
endpoint: `${TProtocol}://${THost}${TBasePath}`;
} : {
endpoint?: string;
});
export interface ClientRequestParams extends ClientRequestInit {
json?: any;
formData?: Record<string, string | Blob>;
formUrlEncoded?: Record<string, string | string[]>;
params?: Record<string, string>;
query?: any;
headers?: Record<string, string>;
}
export type ClientRequestInit = Omit<RequestInit, 'body' | 'method' | 'headers'>;
export type ClientMethod = (requestParams?: ClientRequestParams) => ClientTypedResponsePromise<any>;
export interface ClientPlugin {
onRequestInit?: OnRequestInitHook;
onFetch?: OnFetchHook;
onResponse?: OnResponseHook;
}
export type OnRequestInitHook = (payload: ClientOnRequestInitPayload) => Promise<void> | void;
export type OnFetchHook = (payload: ClientOnFetchHookPayload) => Promise<void> | void;
export type OnResponseHook = (payload: ClientOnResponseHookPayload) => Promise<void> | void;
export interface ClientOnRequestInitPayload {
path: string;
method: HTTPMethod;
requestParams: ClientRequestParams;
requestInit: RequestInit;
endResponse(response: Response): void;
}
export type ClientFetchFn = (input: string, init?: RequestInit) => Promise<Response> | Response;
export interface ClientOnFetchHookPayload {
url: string;
init: RequestInit;
fetchFn: ClientFetchFn;
setFetchFn(fetchFn: ClientFetchFn): void;
}
export interface ClientOnResponseHookPayload {
path: string;
method: HTTPMethod;
requestParams: ClientRequestParams;
requestInit: RequestInit;
response: Response;
}
export type BasicAuthParams<TSecurityScheme> = TSecurityScheme extends {
type: 'http';
scheme: 'basic';
} | {
type: 'basic';
} ? {
headers: {
/**
* `Authorization` header is required for basic authentication
* @see https://en.wikipedia.org/wiki/Basic_access_authentication
*
* It contains the word `Basic` followed by a space and a base64-encoded string `username:password`
*
* @example
* ```
* Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
* ```
*/
Authorization: `Basic ${string}`;
};
} : {};
export type BearerAuthParams<TSecurityScheme> = TSecurityScheme extends {
type: 'http';
scheme: 'bearer';
} | {
type: 'bearer';
} ? {
/**
* `Authorization` header is required for bearer authentication
* @see https://swagger.io/docs/specification/authentication/bearer-authentication/
*/
headers: {
/**
* It contains the word `Bearer` followed by a space and the token
*
* @example
* ```
* Authorization: Bearer {token}
* ```
*/
Authorization: `Bearer ${string}`;
};
} : {};
export type ApiKeyAuthParams<TSecurityScheme> = TSecurityScheme extends {
type: 'apiKey';
in: 'header';
name: infer TApiKeyHeaderName;
} ? {
headers: {
[THeaderName in TApiKeyHeaderName extends string ? TApiKeyHeaderName : never]: string;
};
} : TSecurityScheme extends {
type: 'apiKey';
in: 'query';
name: infer TApiKeyQueryName;
} ? {
query: {
[TQueryName in TApiKeyQueryName extends string ? TApiKeyQueryName : never]: string;
};
} : {};
export type SecuritySchemeName<T extends {
security: {
[key: string]: any;
}[];
}> = Call<Tuples.Map<Objects.Keys>, T['security']>[number];
export type OASSecurityParams<TSecurityScheme> = BasicAuthParams<TSecurityScheme> & BearerAuthParams<TSecurityScheme> & ApiKeyAuthParams<TSecurityScheme> & OAuth2AuthParams<TSecurityScheme>;
export type OASSecurityParamsBySecurityRef<TOAS, TSecurityObj> = TSecurityObj extends {
security: {
[key: string]: any;
}[];
} ? TOAS extends {
components: {
securitySchemes: {
[TSecuritySchemeNameKey in SecuritySchemeName<TSecurityObj> extends string ? SecuritySchemeName<TSecurityObj> : never]: infer TSecurityScheme;
};
};
} | {
securityDefinitions: {
[TSecuritySchemeNameKey in SecuritySchemeName<TSecurityObj> extends string ? SecuritySchemeName<TSecurityObj> : never]: infer TSecurityScheme;
};
} ? OASSecurityParams<TSecurityScheme> : SecuritySchemeName<TSecurityObj> extends `basic${string}` ? BasicAuthParams<{
type: 'http';
scheme: 'basic';
}> : SecuritySchemeName<TSecurityObj> extends `bearer${string}` ? BearerAuthParams<{
type: 'http';
scheme: 'bearer';
}> : SecuritySchemeName<TSecurityObj> extends `oauth${string}` ? OAuth2AuthParams<{
type: 'oauth2';
}> : {} : {};
export {};