UNPKG

fets

Version:

TypeScript HTTP Framework focusing on e2e type-safety, easy setup, performance & great developer experience

341 lines (340 loc) • 14.6 kB
import type { B, Call, Objects, Pipe, Strings, Tuples } from 'hotscript'; import type { O } from 'ts-toolbelt'; import { HTTPMethod, NotOkStatusCode, StatusCode, TypedResponse } from '../typed-fetch.cjs'; import type { FromSchema, JSONSchema, OpenAPIDocument } from '../types.cjs'; import type { OASOAuthPath, OAuth2AuthParams } from './auth/oauth.cjs'; 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 extends string, R extends any[] = []> = T extends `${R['length']}` ? R['length'] : ToNumber<T, [1, ...R]>; 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' ? NotOkStatusCode : TStatus extends `${number}${number}${number}` ? ToNumber<TStatus> extends StatusCode ? ToNumber<TStatus> : 200 : 200>; }[keyof OASStatusMap<TOAS, TPath, TMethod>]; interface OASParamPropMap { query: 'query'; path: 'params'; header: 'headers'; } type UnionToIntersectionHelper<U> = (U extends unknown ? (k: U) => void : never) extends (k: infer I) => void ? I : never; type UnionToIntersection<U> = boolean extends U ? UnionToIntersectionHelper<Exclude<U, boolean>> & boolean : UnionToIntersectionHelper<U>; 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; }; export type OASParamMap<TParameters extends { name: string; in: string; }[]> = UnionToIntersection<{ [TIndex in keyof TParameters]: TParameters[TIndex] extends { in: infer TParamType; } ? TParameters[TIndex] extends { required: true; } ? { [TKey in TParamType extends keyof OASParamPropMap ? OASParamPropMap[TParamType] : never]: OASParamObj<TParameters[TIndex]>; } : { [TKey in TParamType extends keyof OASParamPropMap ? OASParamPropMap[TParamType] : never]?: OASParamObj<TParameters[TIndex]>; } : never; }[number]>; export type OASClient<TOAS extends OpenAPIDocument> = { [TPath in keyof OASPathMap<TOAS>]: { [TMethod in keyof OASMethodMap<TOAS, TPath>]: OASRequestParams<TOAS, TPath, TMethod> extends { json: {}; } | { params: {}; } | { headers: {}; } | { query: {}; } ? (requestParams: OASRequestParams<TOAS, TPath, TMethod>, init?: RequestInit) => Promise<OASResponse<TOAS, TPath, TMethod>> : (requestParams?: OASRequestParams<TOAS, TPath, TMethod>, init?: RequestInit) => Promise<OASResponse<TOAS, TPath, TMethod>>; }; } & OASOAuthPath<TOAS>; export type OASModel<TOAS extends OpenAPIDocument, TName extends string> = TOAS extends { components: { schemas: { [TModelName in TName]: JSONSchema; }; }; } ? FromSchema<TOAS['components']['schemas'][TName]> : never; export type FixJSONSchema<T> = 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 FixMissingTypeObject<T> = T extends { properties: any; } ? T & { type: 'object'; } : T; type FixMissingAdditionalProperties<T> = T extends { type: 'object'; properties: any; } ? T & { additionalProperties: false; } : 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; export type OASRequestParams<TOAS extends OpenAPIDocument, TPath extends keyof OASPathMap<TOAS>, TMethod extends keyof OASMethodMap<TOAS, TPath>> = (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 */ json: FromSchema<OASMethodMap<TOAS, TPath>[TMethod]['requestBody']['content']['application/json']['schema']>; } : { /** * The request body in JSON is optional for this request */ json?: FromSchema<OASMethodMap<TOAS, TPath>[TMethod]['requestBody']['content']['application/json']['schema']>; } : {}) & (OASMethodMap<TOAS, TPath>[TMethod] extends { parameters: { name: string; in: string; }[]; } ? OASParamMap<OASMethodMap<TOAS, TPath>[TMethod]['parameters']> : {}) & (TPath extends `${string}{${string}}${string}` ? { params: Record<ExtractPathParamsWithBrackets<TPath>, string | number>; } : {}) & (TPath extends `${string}:${string}${string}` ? { params: Record<ExtractPathParamsWithPattern<TPath>, string | number>; } : {}) & (OASMethodMap<TOAS, TPath>[TMethod] extends { security: { [key: string]: any; }[]; } ? TOAS extends { components: { securitySchemes: { [TSecuritySchemeNameKey in SecuritySchemeName<OASMethodMap<TOAS, TPath>[TMethod]> extends string ? SecuritySchemeName<OASMethodMap<TOAS, TPath>[TMethod]> : never]: infer TSecurityScheme; }; }; } ? OASSecurityParams<TSecurityScheme> : {} : {}) & (TOAS extends { security: { [key: string]: any; }[]; } ? TOAS extends { components: { securitySchemes: { [TSecuritySchemeNameKey in SecuritySchemeName<TOAS> extends string ? SecuritySchemeName<TOAS> : never]: infer TSecurityScheme; }; }; } ? OASSecurityParams<TSecurityScheme> : {} : {}) & (TOAS extends { security: { [key: string]: any; }[]; } ? TOAS extends { securityDefinitions: { [TSecuritySchemeNameKey in SecuritySchemeName<TOAS> extends string ? SecuritySchemeName<TOAS> : never]: infer TSecurityScheme; }; } ? OASSecurityParams<TSecurityScheme> : {} : {}) & (OASMethodMap<TOAS, TPath>[TMethod] extends { requestBody: { content: { 'multipart/form-data': { schema: JSONSchema; }; }; }; } ? OASMethodMap<TOAS, TPath>[TMethod]['requestBody'] extends { required: true; } ? { formData: FromSchema<OASMethodMap<TOAS, TPath>[TMethod]['requestBody']['content']['multipart/form-data']['schema']>; } : { 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; } ? { formUrlEncoded: FromSchema<OASMethodMap<TOAS, TPath>[TMethod]['requestBody']['content']['application/x-www-form-urlencoded']['schema']>; } : { formUrlEncoded?: FromSchema<OASMethodMap<TOAS, TPath>[TMethod]['requestBody']['content']['application/x-www-form-urlencoded']['schema']>; } : {}); 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 { endpoint?: string; fetchFn?: typeof fetch; plugins?: ClientPlugin[]; } export interface ClientRequestParams { json?: any; formData?: FormData; formUrlEncoded?: Record<string, string | string[]>; params?: Record<string, string>; query?: Record<string, string | string[]>; headers?: Record<string, string>; } export type ClientMethod = (requestParams?: ClientRequestParams) => Promise<Response>; 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 interface ClientOnFetchHookPayload { url: string; init: RequestInit; fetchFn: typeof fetch; setFetchFn(fetchFn: typeof fetch): void; } export interface ClientOnResponseHookPayload { path: string; method: HTTPMethod; requestParams: ClientRequestParams; requestInit: RequestInit; response: Response; } export type ExtractPathParamsWithBrackets<TPath extends string> = Pipe<TPath, [ Strings.Split<'/' | ';'>, Tuples.Filter<Strings.StartsWith<'{'>>, Tuples.Map<Strings.Trim<'{' | '}'>>, Tuples.ToUnion ]>; export type ExtractPathParamsWithPattern<TPath extends string> = Pipe<TPath, [ Strings.Split<'/'>, Tuples.Filter<Strings.StartsWith<':'>>, Tuples.Map<Strings.Trim<':'>>, Tuples.ToUnion ]>; 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'; } ? { headers: { /** * `Authorization` header is required for bearer authentication * @see https://swagger.io/docs/specification/authentication/bearer-authentication/ * * 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 {};