UNPKG

fastify-openapi-connector

Version:

Fastify plugin that will set-up routes with security & json validation based on OpenAPI specification

227 lines (226 loc) 7.99 kB
import type { FastifyContextConfig, FastifyReply, FastifyRequest, FastifyRequestContext, RouteHandler } from 'fastify'; /** * Settings used to determine prefix from servers section of OAS */ export interface PrefixExtractingSettings { urlRegex?: RegExp; descriptionRegex?: RegExp; prefixVariable?: string; } /** * Options used to setup the plugin */ export interface Options<Ops = any> { securityHandlers?: SecurityHandlers; operationHandlers: OperationHandlers<Ops> | OperationHandlersUntyped; openApiSpecification: OpenAPISpec; settings?: { initializePaths?: boolean; initializeWebhooks?: boolean; prefix?: string | PrefixExtractingSettings; useXSecurity?: boolean; validateResponses?: boolean; contentTypes?: string[]; }; } /** * Type for security handler function that will be used to check if user has access to specific route */ export type SecurityHandler = (req: FastifyRequest, scopes?: string[]) => boolean | Promise<boolean>; /** * Dictionary of security handlers, where key is securitySchema name from OAS */ export interface SecurityHandlers { [resolverName: string]: SecurityHandler | undefined; } /** * Dictionary of operation handlers, where key is operationId from OAS */ export interface OperationHandlers<Ops = Record<string, unknown>, Content = 'application/json', T extends keyof Ops = keyof Ops> { [resolverName: string]: TypedHandlerBase<Ops, T, Content> | undefined; } export interface OperationHandlersUntyped { [resolverName: string]: ((req: FastifyRequest, reply: FastifyReply) => any) | undefined; } /** * Typed security section of OAS */ export type SecuritySpecification = { [securityHandlerName: string]: string[] | undefined; }[]; /** * Typed components section of OAS */ export interface Components { parameters?: Record<string, SchemaParameter>; schemas?: Record<string, Record<string, unknown>>; } /** * Typed paths section of OAS */ export interface PathsMap { [name: string]: unknown; } /** * Typed server variable object of OAS */ export interface ServerVariableObject { enum?: string[]; default: string; description?: string; } /** * Typed sever object of OAS */ export interface ServerObject { url: string; description?: string; variables?: Record<string, ServerVariableObject>; } /** * Typed OAS */ export interface OpenAPISpec { components?: Components; security?: SecuritySpecification; paths?: PathsMap; webhooks?: PathsMap; servers?: ServerObject[]; } /** * Typed response object of OAS */ export interface SpecResponse { [statusCode: string]: { description: string; type?: string; content?: unknown; }; } /** * Typed operation object of OAS */ export interface PathOperation { operationId?: string; parameters?: (SchemaParameter | ReferenceObject)[]; requestBody: unknown; security?: SecuritySpecification; responses?: SpecResponse; 'x-fastify-config'?: Omit<FastifyRequestContext<FastifyContextConfig>['config'], 'url' | 'method'>; } /** * Typed paths object of OAS */ export type Paths = { parameters?: SchemaParameter[]; 'x-security'?: unknown; } & { [method: string]: PathOperation; }; /** * Typed possible in paremeter values of OAS */ export type SchemaParametersIn = 'query' | 'path' | 'header' | 'cookie'; /** * Typed schema parameter object of OAS */ export interface SchemaParameter { name: string; description?: string; required?: boolean; schema: { type: string; }; in: SchemaParametersIn; } /** * Parameters handling */ export interface ParsedParameter { type: string; properties: { [name: string]: { type: string; description?: string; }; }; required?: string[]; } /** * Typed reference object of OAS */ export interface ReferenceObject { $ref: string; summary?: string; description?: string; } type OperationWithParams = { parameters: any; }; type OperationWithBody = { requestBody: any; }; type OperationWithResponse = { responses: any; }; type RecordToTuple<T> = [keyof T] extends [never] ? [] : T extends Record<string, unknown> ? { [K in keyof T]: [T[K], ...RecordToTuple<Omit<T, K>>]; }[keyof T] : []; type evaluate<T> = { [K in keyof T]: T[K]; } & unknown; /** * XOR type for two types. */ type xor<A, B> = evaluate<A & { [K in keyof B]?: undefined; }> | evaluate<B & { [K in keyof A]?: undefined; }>; /** * XOR type for multiple types. * Recursively applies `xor2` to combine all types into a mutually exclusive type. */ type ArrayToXor<T extends unknown[]> = T extends [infer First, ...infer Rest] ? Rest extends unknown[] ? xor<First, ArrayToXor<Rest>> : First : unknown; type TransformOperationsToReply<Ops, T extends keyof Ops> = Ops[T] extends { responses: infer Responses; } ? { [StatusCode in keyof Responses]: Responses[StatusCode] extends { content: infer Content; } ? ArrayToXor<RecordToTuple<Content>> : Responses[StatusCode]; } : never; type TypedFastifyReply<Ops, T extends keyof Ops> = FastifyReply<{ Reply: TransformOperationsToReply<Ops, T>; }>; /** * First argument is interface with operations, second is name of operation we want to get request type for */ export type TypedRequestBase<Ops, T extends keyof Ops, Content = 'application/json'> = FastifyRequest<{ Params: Ops[T] extends OperationWithParams ? Ops[T]['parameters']['path'] : never; Querystring: Ops[T] extends OperationWithParams ? Ops[T]['parameters']['query'] : never; Body: Ops[T] extends OperationWithBody ? Content extends keyof Ops[T]['requestBody']['content'] ? Ops[T]['requestBody']['content'][Content] : never : never; }>; export type TypedRouteHandler<Ops, T extends keyof Ops, Content = 'application/json'> = RouteHandler<{ Params: Ops[T] extends OperationWithParams ? Ops[T]['parameters']['path'] : never; Querystring: Ops[T] extends OperationWithParams ? Ops[T]['parameters']['query'] : never; Body: Ops[T] extends OperationWithBody ? Content extends keyof Ops[T]['requestBody']['content'] ? Ops[T]['requestBody']['content'][Content] : never : never; Reply: TransformOperationsToReply<Ops, T>; }>; /** First argument is interface with operations, second is name of operation we want to get response type for * As what ever you retunr in fastify will be treated as 200 response, we want to restrict this to only valid responses * If operation has 200 response with content application/json, we set it as type (or FastifyReply), otherwise we set FastifyReply * That way we can wither return strongly typed response or just FastifyReply where we can set any additional information like code, headers, etc. */ export type TypedResponseBaseSync<Ops, T extends keyof Ops, Content = 'application/json'> = Ops[T] extends OperationWithResponse ? TypedFastifyReply<Ops, T> | (Content extends keyof Ops[T]['responses']['200']['content'] ? Ops[T]['responses']['200']['content'][Content] : never) : TypedFastifyReply<Ops, T>; export type TypedResponseBaseAsync<Ops, T extends keyof Ops, Content = 'application/json'> = Promise<TypedResponseBaseSync<Ops, T, Content>>; export type TypedResponseBase<Ops, T extends keyof Ops, Content = 'application/json'> = TypedResponseBaseSync<Ops, T, Content> | Promise<TypedResponseBaseSync<Ops, T, Content>>; /** * Base type for operation handlers functions * !!! IMPORTANT !!! * As TypeScript does not enforce return type of function (but provides suggestions) you should define your handles as follows: * `const myHandler: TypedHandlerBase = (req, reply): TypedResponseBase<Ops, T> => {` */ export interface TypedHandlerBase<Ops = Record<string, unknown>, T extends keyof Ops = keyof Ops, Content = 'application/json'> { (req: TypedRequestBase<Ops, T, Content>, reply: TypedFastifyReply<Ops, T>): TypedResponseBase<Ops, T, Content>; } export {};