UNPKG

fets

Version:

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

319 lines (318 loc) • 19 kB
import type { Pipe, Strings, Tuples } from 'hotscript'; import type { FromSchema as FromSchemaOriginal, JSONSchema as JSONSchemaOrBoolean } from 'json-schema-to-ts'; import type { ErrorHandler, FetchAPI, ServerAdapter, ServerAdapterOptions, ServerAdapterPlugin, ServerAdapterRequestHandler } from '@whatwg-node/server'; import type { ClientTypedResponsePromise } from './client/clientResponse.js'; import type { ClientRequestInit } from './client/types.js'; import type { SwaggerUIOpts } from './plugins/openapi.js'; import type { HTTPMethod, StatusCode, TypedRequest, TypedResponse, TypedResponseWithJSONStatusMap } from './typed-fetch.js'; export { TypedRequest as RouterRequest }; export type Simplify<T> = { [KeyType in keyof T]: Simplify<T[KeyType]>; } & {}; export type JSONSerializer = (obj: any) => string; export type JSONSchema = Exclude<JSONSchemaOrBoolean, boolean>; export interface OpenAPIInfo { title?: string; description?: string; version?: string; license?: { name?: string; url?: string; }; termsOfService?: string; contact?: { name?: string; url?: string; email?: string; }; } export type OpenAPIPathObject = Record<string, OpenAPIOperationObject> & { parameters?: OpenAPIParameterObject[]; }; export interface OpenAPIParameterObject { name: string; in: 'path' | 'query' | 'header' | 'cookie'; required?: boolean; schema?: any; } export interface OpenAPIRequestBodyObject { content?: Record<string, OpenAPIMediaTypeObject>; } export interface OpenAPIOperationObject { operationId?: string; description?: string; tags?: string[]; parameters?: OpenAPIParameterObject[]; requestBody?: OpenAPIRequestBodyObject; responses?: Record<string | number, OpenAPIResponseObject>; security?: any[]; } export interface OpenAPIResponseObject { description?: string; content?: Record<string, OpenAPIMediaTypeObject>; } export interface OpenAPIMediaTypeObject { schema?: any; } export type OpenAPIDocument = { openapi?: string; info?: OpenAPIInfo; servers?: { url: string; }[] | string[]; paths?: Record<string, OpenAPIPathObject>; components?: unknown; }; export interface RouterOpenAPIOptions<TComponents extends RouterComponentsBase> extends OpenAPIDocument { endpoint?: string | false; components?: TComponents; includeValidationErrors?: boolean; } export interface RouterSwaggerUIOptions extends SwaggerUIOpts { endpoint?: string | false; } export interface RouterOptions<TServerContext, TComponents extends RouterComponentsBase> extends ServerAdapterOptions<TServerContext> { base?: string; plugins?: RouterPlugin<TServerContext, TComponents>[]; openAPI?: RouterOpenAPIOptions<TComponents>; swaggerUI?: RouterSwaggerUIOptions; landingPage?: boolean; onError?: ErrorHandler<TServerContext>; } export type RouterComponentsBase = { schemas?: Record<string, JSONSchema>; securitySchemes?: Record<string, SecurityScheme>; }; export type BasicAuthSecurityScheme = { type: 'http'; scheme: 'basic'; }; export type BearerAuthSecurityScheme = { type: 'http'; scheme: 'bearer'; }; export type ApiKeySecurityScheme = { type: 'apiKey'; name: string; in: 'header' | 'query' | 'cookie'; }; export type SecurityScheme = BasicAuthSecurityScheme | BearerAuthSecurityScheme | ApiKeySecurityScheme; export type Circular<TJSONSchema extends JSONSchema> = TJSONSchema extends { properties: { [key: string]: JSONSchema; }; } ? TJSONSchema extends PropertyValue<TJSONSchema, Property<TJSONSchema>> ? true : [Extract<PropertyValue<TJSONSchema, Property<TJSONSchema>>, { $id: string; }>] extends [never] ? [ArrayItemValue<PropertyValue<TJSONSchema, Property<TJSONSchema>>>] extends [never] ? Circular<PropertyValue<TJSONSchema, Property<TJSONSchema>>> : ArrayItemValue<PropertyValue<TJSONSchema, Property<TJSONSchema>>> extends TJSONSchema ? true : Circular<PropertyValue<TJSONSchema, Property<TJSONSchema>>> : true : false; export type Property<TJSONSchema extends JSONSchema> = keyof TJSONSchema['properties']; export type PropertyValue<TJSONSchema extends JSONSchema, TProperty extends keyof TJSONSchema['properties']> = TJSONSchema['properties'][TProperty]; export type ArrayItemValue<TJSONSchema extends JSONSchema> = TJSONSchema extends { items: infer TItems extends JSONSchema; } ? TItems : never; export type FromSchema<T> = T extends { static: infer U; } ? U : T extends JSONSchema ? FromSchemaOriginal<T, { deserialize: Circular<T> extends false ? [ { pattern: { type: 'string'; format: 'binary'; }; output: File; }, { pattern: { type: 'number'; format: 'int64'; }; output: bigint | number; }, { pattern: { type: 'integer'; format: 'int64'; }; output: bigint | number; } ] : false; }> : never; export type FromRouterComponentSchema<TRouter extends Router<any, any, any>, TName extends string> = TRouter extends Router<any, infer TComponents, any> ? TComponents extends Required<RouterComponentsBase> ? FromSchema<TComponents['schemas'][TName]> : never : never; export type PromiseOrValue<T> = T | Promise<T>; export type StatusCodeMap<T> = { [TKey in StatusCode]?: T; }; export interface RouterBaseObject<TServerContext, TComponents extends RouterComponentsBase, TRouterSDK extends RouterSDK<string, TypedRequest, TypedResponse>> { openAPIDocument: OpenAPIDocument; handle: ServerAdapterRequestHandler<TServerContext>; route<const TRouteSchemas extends RouteSchemas, TMethod extends HTTPMethod, TPath extends string, TTypedRequest extends TypedRequestFromRouteSchemas<TComponents, TRouteSchemas, TMethod, TPath>, TTypedResponse extends TypedResponseFromRouteSchemas<TComponents, TRouteSchemas>>(opts: RouteWithSchemasOpts<TServerContext, TComponents, TRouteSchemas, TMethod, TPath, TTypedRequest, TTypedResponse>): Router<TServerContext, TComponents, TRouterSDK & RouterSDK<TPath, TTypedRequest, TTypedResponse>>; route<TMethod extends HTTPMethod, TPath extends string, TTypedRequest extends TypedRequest<any, Record<string, FormDataEntryValue | undefined>, Record<string, string | undefined>, TMethod, any, Record<ExtractPathParamsWithPattern<TPath>, string>>, TTypedResponse extends TypedResponse>(opts: RouteWithTypesOpts<TServerContext, TMethod, TPath, TTypedRequest, TTypedResponse>): Router<TServerContext, TComponents, TRouterSDK & RouterSDK<TPath, TTypedRequest, TTypedResponse>>; use<TSubServerContext, TSubComponents extends RouterComponentsBase, TSubRouterSDK extends RouterSDK<string, TypedRequest, TypedResponse>>(subRouter: Router<TSubServerContext, TSubComponents, TSubRouterSDK>): Router<TServerContext, TComponents, TRouterSDK & TSubRouterSDK>; use<const TPrefix extends string, TSubServerContext, TSubComponents extends RouterComponentsBase, TSubRouterSDK extends RouterSDK<string, TypedRequest, TypedResponse>>(prefix: TPrefix, subRouter: Router<TSubServerContext, TSubComponents, TSubRouterSDK>): Router<TServerContext, TComponents, TRouterSDK & { [TKey in keyof TSubRouterSDK as TKey extends string ? `${TPrefix}${TKey}` : TKey]: TSubRouterSDK[TKey]; }>; __client: TRouterSDK; __onRouterInitHooks: OnRouterInitHook<TServerContext>[]; __routes: RouteWithSchemasOpts<any, RouterComponentsBase, RouteSchemas, HTTPMethod, string, TypedRequest, TypedResponse>[]; __base: string; } export type Router<TServerContext, TComponents extends RouterComponentsBase, TRouterSDK extends RouterSDK<string, TypedRequest, TypedResponse>> = ServerAdapter<TServerContext, RouterBaseObject<TServerContext, TComponents, TRouterSDK>>; export type OnRouteHook<TServerContext> = (payload: OnRouteHookPayload<TServerContext>) => void; export type OnRouteHandleHook<TServerContext, TComponents extends RouterComponentsBase> = (payload: OnRouteHandleHookPayload<TServerContext, TComponents>) => void; export interface OnRouteHandleHookPayload<TServerContext, TComponents extends RouterComponentsBase> { request: TypedRequest; route: RouteWithSchemasOpts<TServerContext, TComponents, RouteSchemas, HTTPMethod, string, TypedRequest, TypedResponse>; } export type RouteHandler<TServerContext = {}, TTypedRequest extends TypedRequest = TypedRequest, TTypedResponse extends TypedResponse = TypedResponse> = ( /** * The request object represents the incoming HTTP request. * This object implements [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) interface. */ request: TTypedRequest, context: TServerContext) => PromiseOrValue<TTypedResponse>; export type OnRouteHookPayload<TServerContext> = { basePath: string; openAPIDocument: OpenAPIDocument; routeByPatternByMethod: Map<HTTPMethod, Map<URLPattern, RouteWithSchemasOpts<any, RouterComponentsBase, RouteSchemas, HTTPMethod, string, TypedRequest, TypedResponse>>>; routeByPathByMethod: Map<HTTPMethod, Map<string, RouteWithSchemasOpts<any, RouterComponentsBase, RouteSchemas, HTTPMethod, string, TypedRequest, TypedResponse>>>; route: RouteWithSchemasOpts<TServerContext, RouterComponentsBase, RouteSchemas, HTTPMethod, string, TypedRequest, TypedResponse>; fetchAPI: FetchAPI; }; export type OnRouterInitHook<TServerContext> = (router: Router<TServerContext, any, any>) => void; export type RouterPlugin<TServerContext, TComponents extends RouterComponentsBase> = ServerAdapterPlugin<TServerContext> & { onRouterInit?: OnRouterInitHook<TServerContext>; onRoute?: OnRouteHook<TServerContext>; onRouteHandle?: OnRouteHandleHook<TServerContext, TComponents>; }; type ObjectSchemaWithPrimitiveProperties = JSONSchema & { type: 'object'; properties: Record<string, { type: 'string' | 'number' | 'integer' | 'boolean' | 'null'; }>; }; type ObjectSchema = JSONSchema & { type: 'object'; }; export type RouteSchemas = { request?: { headers?: ObjectSchemaWithPrimitiveProperties; params?: ObjectSchemaWithPrimitiveProperties; query?: ObjectSchema; json?: JSONSchema; formData?: ObjectSchema; }; responses?: StatusCodeMap<JSONSchema>; }; export type RouterSDKOpts<TTypedRequest extends TypedRequest = TypedRequest, TMethod extends HTTPMethod = HTTPMethod> = TTypedRequest extends TypedRequest<infer TJSONBody, infer TFormData, infer THeaders, TMethod, infer TQueryParams, infer TPathParam> ? Simplify<(Partial<TJSONBody> extends TJSONBody ? { json?: TJSONBody; } : { json: TJSONBody; }) & (Partial<THeaders> extends THeaders ? { headers?: THeaders; } : { headers: THeaders; }) & (Partial<TQueryParams> extends TQueryParams ? { query?: TQueryParams; } : { query: TQueryParams; }) & (Partial<TPathParam> extends TPathParam ? { params?: TPathParam; } : { params: TPathParam; })> & (Partial<TFormData> extends TFormData ? { formData?: TFormData; } : { formData: TFormData; }) : never; export type RouterSDK<TPath extends string = string, TTypedRequest extends TypedRequest = TypedRequest, TTypedResponse extends TypedResponse = TypedResponse> = { [TPathKey in TPath]: { [TMethod in Lowercase<TTypedRequest['method']>]: Partial<RouterSDKOpts<TTypedRequest, TTypedRequest['method']>> extends RouterSDKOpts<TTypedRequest, TTypedRequest['method']> ? (opts?: RouterSDKOpts<TTypedRequest, TTypedRequest['method']> & ClientRequestInit) => ClientTypedResponsePromise<Exclude<TTypedResponse, undefined>> : (opts: RouterSDKOpts<TTypedRequest, TTypedRequest['method']> & ClientRequestInit) => ClientTypedResponsePromise<Exclude<TTypedResponse, undefined>>; }; }; export type FromSchemaWithComponents<TComponents, TSchema extends JSONSchema> = TComponents extends { schemas: Record<string, JSONSchema>; } ? FromSchema<{ components: TComponents; } & TSchema> : FromSchema<TSchema>; export type TypedRequestFromRouteSchemas<TComponents extends RouterComponentsBase, TRouteSchemas extends RouteSchemas, TMethod extends HTTPMethod, TPath extends string> = TRouteSchemas extends { request: Required<RouteSchemas>['request']; } ? TypedRequest<TRouteSchemas['request'] extends { json: JSONSchema; } ? FromSchemaWithComponents<TComponents, TRouteSchemas['request']['json']> : {}, TRouteSchemas['request'] extends { formData: JSONSchema; } ? FromSchemaWithComponents<TComponents, TRouteSchemas['request']['formData']> extends Record<string, FormDataEntryValue | undefined> ? FromSchemaWithComponents<TComponents, TRouteSchemas['request']['formData']> : never : {}, TRouteSchemas['request'] extends { headers: JSONSchema; } ? FromSchemaWithComponents<TComponents, TRouteSchemas['request']['headers']> extends Record<string, string> ? FromSchemaWithComponents<TComponents, TRouteSchemas['request']['headers']> : never : {}, TMethod, TRouteSchemas['request'] extends { query: JSONSchema; } ? FromSchemaWithComponents<TComponents, TRouteSchemas['request']['query']> : {}, TRouteSchemas['request'] extends { params: JSONSchema; } ? FromSchemaWithComponents<TComponents, TRouteSchemas['request']['params']> extends Record<string, any> ? FromSchemaWithComponents<TComponents, TRouteSchemas['request']['params']> : never : Record<ExtractPathParamsWithPattern<TPath>, string>> : TypedRequest<any, Partial<Record<string, FormDataEntryValue>>, Partial<Record<string, string>>, TMethod, any, Record<ExtractPathParamsWithPattern<TPath>, string>>; export type TypedResponseFromRouteSchemas<TComponents extends RouterComponentsBase, TRouteSchemas extends RouteSchemas> = TRouteSchemas extends { responses: StatusCodeMap<JSONSchema>; } ? TypedResponseWithJSONStatusMap<{ [TStatusCode in keyof TRouteSchemas['responses']]: TRouteSchemas['responses'][TStatusCode] extends JSONSchema ? FromSchemaWithComponents<TComponents, TRouteSchemas['responses'][TStatusCode]> : never; }> : TypedResponse; export type RouteWithSchemasOpts<TServerContext, TComponents extends RouterComponentsBase, TRouteSchemas extends RouteSchemas, TMethod extends HTTPMethod, TPath extends string, TTypedRequest extends TypedRequestFromRouteSchemas<TComponents, TRouteSchemas, TMethod, TPath>, TTypedResponse extends TypedResponseFromRouteSchemas<TComponents, TRouteSchemas>> = { schemas: TRouteSchemas; security?: SecuritySchemeRefsFromComponents<TComponents>[]; } & RouteWithTypesOpts<TServerContext, TMethod, TPath, TTypedRequest, TTypedResponse>; export type SecuritySchemeRefsFromComponents<TComponents extends RouterComponentsBase> = TComponents extends { securitySchemes: Record<string, SecurityScheme>; } ? Record<keyof TComponents['securitySchemes'], any> : never; export type RouteWithTypesOpts<TServerContext, TMethod extends HTTPMethod, TPath extends string, TTypedRequest extends TypedRequest<any, Record<string, FormDataEntryValue | undefined>, Record<string, string | undefined>, TMethod, any, Record<ExtractPathParamsWithPattern<TPath>, string>>, TTypedResponse extends TypedResponse> = { operationId?: string; description?: string; method?: TMethod; tags?: string[]; internal?: boolean; path: TPath; handler: RouteHandler<TServerContext, TTypedRequest, TTypedResponse>; }; export type RouteInput<TRouter extends Router<any, any, {}>, TPath extends string, TMethod extends Lowercase<HTTPMethod> = 'post', TParamType extends keyof RouterSDKOpts = 'json'> = TRouter extends Router<any, any, infer TRouterSDK> ? TRouterSDK[TPath][TMethod] extends (requestParams?: infer TRequestParams) => any ? TRequestParams extends { [TParamTypeKey in TParamType]?: infer TParamTypeValue; } ? TParamTypeValue : never : never : never; export type RouteOutput<TRouter extends Router<any, any, {}>, TPath extends string, TMethod extends Lowercase<HTTPMethod> = 'post', TStatusCode extends StatusCode = 200> = TRouter extends Router<any, any, infer TRouterSDK> ? TRouterSDK extends RouterSDK ? TRouterSDK[TPath][TMethod] extends (...args: any[]) => Promise<infer TTypedResponse> ? TTypedResponse extends TypedResponse<infer TJSONBody, any, TStatusCode> ? TJSONBody : never : never : never : never; export type RouterClient<TRouter extends Router<any, any, any>> = TRouter['__client']; export type RouterInput<TRouter extends Router<any, any, any>> = { [TPath in keyof RouterClient<TRouter>]: { [TMethod in keyof RouterClient<TRouter>[TPath]]: RouterClient<TRouter>[TPath][TMethod] extends (requestParams?: infer TRequestParams) => any ? Required<TRequestParams> : never; }; }; export type RouterJsonPostInput<TRouter extends Router<any, any, any>> = { [TPath in keyof RouterClient<TRouter>]: { [TMethod in keyof RouterClient<TRouter>[TPath]]: RouterInput<TRouter>[TPath][TMethod] extends { json: infer TJSON; } ? TJSON : never; }['post']; }; export type RouterJsonPostSuccessOutput<TRouter extends Router<any, any, any>> = { [TPath in keyof RouterClient<TRouter>]: { [TMethod in keyof RouterClient<TRouter>[TPath]]: RouterOutput<TRouter>[TPath][TMethod][200]; }['post']; }; export type RouterOutput<TRouter extends Router<any, any, any>> = { [TPath in keyof RouterClient<TRouter>]: { [TMethod in keyof RouterClient<TRouter>[TPath]]: RouterClient<TRouter>[TPath][TMethod] extends (requestParams?: any) => Promise<infer TTypedResponse> ? { [TStatusCode in StatusCode]: TTypedResponse extends TypedResponse<infer TJSONBody, any, TStatusCode> ? TJSONBody : never; } : never; }; }; export type RouterComponentSchema<TRouter extends Router<any, any, any>, TName extends string> = TRouter extends Router<any, infer TComponents, any> ? TComponents extends { schemas: Record<string, JSONSchema>; } ? FromSchema<TComponents['schemas'][TName]> : never : never; type SplitByDelimiter<T extends string, D extends string> = T extends `${infer P}${D}${infer Q}` ? [P, ...SplitByDelimiter<Q, D>] : [T]; type IsPathParameter<T extends string> = T extends `{${infer U}}` ? U : never; type ExtractPathParametersFromSegment<T extends string> = IsPathParameter<T>; type ExtractPathParameters<T extends any[]> = { [K in keyof T]: ExtractPathParametersFromSegment<T[K]>; }; type TupleToUnion<T> = T extends any[] ? T[number] : never; type ExtractSegments<TPath extends string> = SplitByDelimiter<TPath, '/'>; type ExtractSubSegments<T extends any[]> = { [K in keyof T]: SplitByDelimiter<T[K], ';'>; }; export type ExtractPathParamsWithBrackets<TPath extends string> = TupleToUnion<ExtractPathParameters<ExtractSubSegments<ExtractSegments<TPath>>[number]>>; export type ExtractPathParamsWithPattern<TPath extends string> = Pipe<TPath, [ Strings.Split<'/'>, Tuples.Filter<Strings.StartsWith<':'>>, Tuples.Map<Strings.Trim<':'>>, Tuples.ToUnion ]>;