diff --git a/.vscode/settings.json b/.vscode/settings.json index 25fa6215f..8400d618a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,4 @@ { - "typescript.tsdk": "node_modules/typescript/lib" + "typescript.tsdk": "node_modules/typescript/lib", + "cSpell.words": ["fets"] } diff --git a/benchmark/start-server.ts b/benchmark/start-server.ts index e7a0ed536..7bc184a8c 100644 --- a/benchmark/start-server.ts +++ b/benchmark/start-server.ts @@ -115,6 +115,11 @@ const router = createRouter({ handler, }); +// @ts-ignore Types of parameters 'req' and 'req' are incompatible. +// Type 'IncomingMessage' is not assignable to type 'NodeRequest' with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the types of the target's properties. +// Types of property 'url' are incompatible. +// Type 'string | undefined' is not assignable to type 'string'. +// Type 'undefined' is not assignable to type 'string'. createServer(router).listen(4000, () => { readyCount++; console.log('listening on 0.0.0.0:4000'); diff --git a/e2e/shared-scripts/src/types.ts b/e2e/shared-scripts/src/types.ts index 899003742..5ebe11859 100644 --- a/e2e/shared-scripts/src/types.ts +++ b/e2e/shared-scripts/src/types.ts @@ -3,8 +3,8 @@ import { OutputValue, Stack } from '@pulumi/pulumi/automation'; export type DeploymentConfiguration = { name: string; - prerequisites?: (stack: Stack) => Promise; - config?: (stack: Stack) => Promise; + prerequisites?: ((stack: Stack) => Promise) | undefined; + config?: ((stack: Stack) => Promise) | undefined; program: () => Promise<{ [K in keyof TProgramOutput]: Output | TProgramOutput[K]; }>; diff --git a/e2e/shared-server/src/index.ts b/e2e/shared-server/src/index.ts index b25eb638f..8517e1596 100644 --- a/e2e/shared-server/src/index.ts +++ b/e2e/shared-server/src/index.ts @@ -1,7 +1,7 @@ import { createRouter, Response, useErrorHandling } from 'fets'; import { z } from 'zod'; -export function createTestServerAdapter(base?: string) { +export function createTestServerAdapter(base?: string | undefined) { return createRouter({ base, plugins: [useErrorHandling()], diff --git a/examples/todolist/src/router.ts b/examples/todolist/src/router.ts index 3cce6547b..06dbae395 100644 --- a/examples/todolist/src/router.ts +++ b/examples/todolist/src/router.ts @@ -25,7 +25,7 @@ export const router = createRouter({ schemas: { Todo: TodoSchema, }, - } as const, + }, }, }) .route({ @@ -42,7 +42,9 @@ export const router = createRouter({ }, }, } as const, - handler: () => Response.json(todos), + handler: () => { + return Response.json(todos); + }, }) .route({ description: 'Get a todo', @@ -193,7 +195,7 @@ export const router = createRouter({ required: ['file'], additionalProperties: false, }, - }, + } as const, responses: { 200: { type: 'object', @@ -213,12 +215,15 @@ export const router = createRouter({ const body = await request.formData(); const file = body.get('file'); const description = body.get('description'); - return Response.json({ + + const json = { name: file.name, - description, + description: description || '', type: file.type, size: file.size, lastModified: file.lastModified, - }); + } as const; + + return Response.json(json); }, }); diff --git a/examples/zod-example/src/index.ts b/examples/zod-example/src/index.ts index 0a10913b7..50af43f78 100644 --- a/examples/zod-example/src/index.ts +++ b/examples/zod-example/src/index.ts @@ -96,6 +96,12 @@ export const router = createRouter() }, }); +// TODO: Type 'IncomingMessage' is not assignable to type 'NodeRequest' with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the types of the target's properties. +// Types of property 'url' are incompatible. +// Type 'string | undefined' is not assignable to type 'string'. +// Type 'undefined' is not assignable to type 'string'. +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore createServer(router).listen(3000, () => { console.log('SwaggerUI is served at http://localhost:3000/docs'); }); diff --git a/examples/zod-example/tsconfig.json b/examples/zod-example/tsconfig.json index 7e87753c3..5a013a7a9 100644 --- a/examples/zod-example/tsconfig.json +++ b/examples/zod-example/tsconfig.json @@ -1,4 +1,5 @@ { + "extends": "../../tsconfig.json", "compilerOptions": { "target": "esnext", "moduleResolution": "node", diff --git a/packages/fets/src/createRouter.ts b/packages/fets/src/createRouter.ts index 28be5c03a..7163e0848 100644 --- a/packages/fets/src/createRouter.ts +++ b/packages/fets/src/createRouter.ts @@ -38,13 +38,13 @@ const HTTP_METHODS: HTTPMethod[] = [ const EMPTY_OBJECT = {}; const EMPTY_MATCH = { pathname: { groups: {} } } as URLPatternResult; -export function createRouterBase( +export function createRouterBase( { fetchAPI: givenFetchAPI, base: basePath = '/', plugins = [], swaggerUI, - }: RouterOptions = {}, + }: RouterOptions, openAPIDocument: OpenAPIDocument, ): RouterBaseObject { const fetchAPI = { @@ -325,19 +325,20 @@ export function createRouterBase( export function createRouter< TServerContext, - TComponents extends RouterComponentsBase = {}, + TComponents extends RouterComponentsBase, TRouterSDK extends RouterSDK = { [TKey: string]: never; }, >( - options: RouterOptions = {}, + options?: RouterOptions | undefined, ): Router { const { openAPI: { endpoint: oasEndpoint = '/openapi.json', ...openAPIDocument } = {}, swaggerUI: { endpoint: swaggerUIEndpoint = '/docs', ...swaggerUIOpts } = {}, plugins: userPlugins = [], base = '/', - } = options; + } = options || {}; + openAPIDocument.openapi = openAPIDocument.openapi || '3.0.1'; const oasInfo = (openAPIDocument.info ||= {} as OpenAPIInfo); oasInfo.title ||= 'feTS API'; @@ -373,9 +374,26 @@ export function createRouter< plugins, }; const routerBaseObject = createRouterBase(finalOpts, openAPIDocument as OpenAPIDocument); + + // Argument of type 'RouterOptions' is not assignable to parameter of type 'ServerAdapterOptions' with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the types of the target's properties. + // Types of property 'plugins' are incompatible. + // Type 'RouterPlugin[] | undefined' is not assignable to type 'ServerAdapterPlugin[]'. + // Type 'undefined' is not assignable to type 'ServerAdapterPlugin[]'. + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore const router = createServerAdapter(routerBaseObject, finalOpts); + for (const onRouterInitHook of routerBaseObject.__onRouterInitHooks) { + // Argument of type 'ServerAdapter>>' is not assignable to parameter of type 'Router'. + // Type 'ServerAdapter>>' is not assignable to type 'RouterBaseObject'. + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore onRouterInitHook(router); } + + // Type 'ServerAdapter>>' is not assignable to type 'Router'. + // Type 'ServerAdapter>>' is not assignable to type 'RouterBaseObject'. + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore return router; } diff --git a/packages/fets/src/plugins/ajv.ts b/packages/fets/src/plugins/ajv.ts index c02e7333a..5e92b74c1 100644 --- a/packages/fets/src/plugins/ajv.ts +++ b/packages/fets/src/plugins/ajv.ts @@ -4,8 +4,8 @@ import addFormats from 'ajv-formats'; import jsonSerializerFactory from '@ardatan/fast-json-stringify'; import { URL } from '@whatwg-node/fetch'; import { Response } from '../Response.js'; -import { StatusCode } from '../typed-fetch.js'; -import { +import type { StatusCode } from '../typed-fetch.js'; +import type { JSONSerializer, PromiseOrValue, RouterComponentsBase, @@ -18,9 +18,9 @@ import { getHeadersObj } from './utils.js'; type ValidateRequestFn = (request: RouterRequest) => PromiseOrValue; export function useAjv({ - components = {}, + components = { schemas: {} }, }: { - components?: RouterComponentsBase; + components?: RouterComponentsBase | undefined; } = {}): RouterPlugin { const ajv = new Ajv({ strict: false, @@ -73,10 +73,16 @@ export function useAjv({ onRoute({ path, schemas, handlers }) { const validationMiddlewares = new Map(); if (schemas?.request?.headers && !isZodSchema(schemas.request.headers)) { + const { headers } = schemas.request; + + // TODO: Property '$async' is missing in type '{ components: RouterComponentsBase; type?: JSONSchema7TypeName | JSONSchema7TypeName[] | undefined; pattern?: string | undefined; ... 46 more ...; [$JSONSchema7]?: unique symbol; }' but required in type 'AsyncSchema' + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore const validateFn = ajv.compile({ - ...schemas.request.headers, + ...headers, components, }); + validationMiddlewares.set('headers', request => { const headersObj = getHeadersObj(request.headers); const isValid = validateFn(headersObj); @@ -87,10 +93,14 @@ export function useAjv({ }); } if (schemas?.request?.params && !isZodSchema(schemas.request.params)) { + // TODO: Property '$async' is missing in type '{ components: RouterComponentsBase; type?: JSONSchema7TypeName | JSONSchema7TypeName[] | undefined; pattern?: string | undefined; ... 46 more ...; [$JSONSchema7]?: unique symbol; }' but required in type 'AsyncSchema' + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore const validateFn = ajv.compile({ ...schemas.request.params, components, }); + validationMiddlewares.set('params', request => { const isValid = validateFn(request.params); if (!isValid) { @@ -100,10 +110,14 @@ export function useAjv({ }); } if (schemas?.request?.query && !isZodSchema(schemas.request.query)) { + // TODO: Property '$async' is missing in type '{ components: RouterComponentsBase; type?: JSONSchema7TypeName | JSONSchema7TypeName[] | undefined; pattern?: string | undefined; ... 46 more ...; [$JSONSchema7]?: unique symbol; }' but required in type 'AsyncSchema' + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore const validateFn = ajv.compile({ ...schemas.request.query, components, }); + validationMiddlewares.set('query', request => { const isValid = validateFn(request.query); if (!isValid) { @@ -113,10 +127,14 @@ export function useAjv({ }); } if (schemas?.request?.json && !isZodSchema(schemas.request.json)) { + // TODO: Property '$async' is missing in type '{ components: RouterComponentsBase; type?: JSONSchema7TypeName | JSONSchema7TypeName[] | undefined; pattern?: string | undefined; ... 46 more ...; [$JSONSchema7]?: unique symbol; }' but required in type 'AsyncSchema' + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore const validateFn = ajv.compile({ ...schemas.request.json, components, }); + validationMiddlewares.set('json', async request => { const contentType = request.headers.get('content-type'); if (contentType?.includes('json')) { @@ -134,10 +152,14 @@ export function useAjv({ }); } if (schemas?.request?.formData && !isZodSchema(schemas.request.formData)) { + // TODO: Property '$async' is missing in type '{ components: RouterComponentsBase; type?: JSONSchema7TypeName | JSONSchema7TypeName[] | undefined; pattern?: string | undefined; ... 46 more ...; [$JSONSchema7]?: unique symbol; }' but required in type 'AsyncSchema' + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore const validateFn = ajv.compile({ ...schemas.request.formData, components, }); + validationMiddlewares.set('formData', async request => { const contentType = request.headers.get('content-type'); if ( @@ -145,14 +167,18 @@ export function useAjv({ contentType?.includes('application/x-www-form-urlencoded') ) { const formData = await request.formData(); - const formDataObj: Record = {}; + const formDataObj: Record = {}; const jobs: Promise[] = []; formData.forEach((value, key) => { + if (typeof value === 'undefined') { + return; + } + if (typeof value === 'string') { formDataObj[key] = value; } else { jobs.push( - value.arrayBuffer().then(buffer => { + value.arrayBuffer().then((buffer: ArrayBuffer): void => { const typedArray = new Uint8Array(buffer); const binaryStrParts: string[] = []; typedArray.forEach((byte, index) => { diff --git a/packages/fets/src/plugins/openapi.ts b/packages/fets/src/plugins/openapi.ts index 85450c42e..ebfad575e 100644 --- a/packages/fets/src/plugins/openapi.ts +++ b/packages/fets/src/plugins/openapi.ts @@ -3,38 +3,39 @@ import { Response } from '../Response.js'; import swaggerUiHtml from '../swagger-ui-html.js'; import { StatusCode } from '../typed-fetch.js'; import { + JSONSchema, OpenAPIDocument, OpenAPIOperationObject, OpenAPIPathObject, RouterPlugin, } from '../types.js'; -import { isZodSchema } from '../zod/types.js'; +import { isZodSchema, ZodType } from '../zod/types.js'; export interface SwaggerUIOpts { - spec?: OpenAPIDocument; - dom_id?: string; - displayOperationId?: boolean; - tryItOutEnabled?: boolean; - requestSnippetsEnabled?: boolean; - displayRequestDuration?: boolean; - defaultModelRendering?: 'model' | 'example' | 'schema'; - defaultModelExpandDepth?: number; - defaultModelsExpandDepth?: number; - docExpansion?: 'none' | 'list' | 'full'; - filter?: boolean; - maxDisplayedTags?: number; - showExtensions?: boolean; - showCommonExtensions?: boolean; - tagsSorter?: 'alpha'; - operationsSorter?: 'alpha'; - showTags?: boolean; - showMutatedRequest?: boolean; - oauth2RedirectUrl?: string; - validatorUrl?: string; - deepLinking?: boolean; - presets?: any[]; - plugins?: any[]; - layout?: string; + spec?: OpenAPIDocument | undefined; + dom_id?: string | undefined; + displayOperationId?: boolean | undefined; + tryItOutEnabled?: boolean | undefined; + requestSnippetsEnabled?: boolean | undefined; + displayRequestDuration?: boolean | undefined; + defaultModelRendering?: 'model' | 'example' | 'schema' | undefined; + defaultModelExpandDepth?: number | undefined; + defaultModelsExpandDepth?: number | undefined; + docExpansion?: 'none' | 'list' | 'full' | undefined; + filter?: boolean | undefined; + maxDisplayedTags?: number | undefined; + showExtensions?: boolean | undefined; + showCommonExtensions?: boolean | undefined; + tagsSorter?: 'alpha' | undefined; + operationsSorter?: 'alpha' | undefined; + showTags?: boolean | undefined; + showMutatedRequest?: boolean | undefined; + oauth2RedirectUrl?: string | undefined; + validatorUrl?: string | undefined; + deepLinking?: boolean | undefined; + presets?: any[] | undefined; + plugins?: any[] | undefined; + layout?: string | undefined; } export type OpenAPIPluginOptions = { @@ -43,11 +44,11 @@ export type OpenAPIPluginOptions = { swaggerUIOpts: SwaggerUIOpts; }; -export function useOpenAPI({ +export function useOpenAPI({ oasEndpoint, swaggerUIEndpoint, swaggerUIOpts, -}: OpenAPIPluginOptions): RouterPlugin { +}: OpenAPIPluginOptions): RouterPlugin { let paths: Record; return { onRouterInit(router) { @@ -97,7 +98,7 @@ export function useOpenAPI({ if (!pathForOAS.startsWith('/')) { pathForOAS = `/${pathForOAS}`; } - const pathObj: any = (paths[pathForOAS] = paths[pathForOAS] || {}); + const pathObj = (paths[pathForOAS] = paths[pathForOAS] || {}); const lowerCasedMethod = method.toLowerCase(); pathObj[lowerCasedMethod] = pathObj[lowerCasedMethod] || {}; const operation = pathObj[lowerCasedMethod] as OpenAPIOperationObject; @@ -106,9 +107,14 @@ export function useOpenAPI({ operation.tags = tags; if (schemas.responses) { for (const statusCode in schemas.responses) { - let responseSchema = schemas.responses[statusCode as any as StatusCode]; + let responseSchema: JSONSchema | ZodType | undefined = + schemas.responses[statusCode as unknown as StatusCode]; + if (isZodSchema(responseSchema)) { - responseSchema = zodToJsonSchema(responseSchema as any, { + // TODO: Possible bug. zodToJsonSchema is not returning the correct type. + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + responseSchema = zodToJsonSchema(responseSchema, { target: 'openApi3', }); } @@ -117,7 +123,7 @@ export function useOpenAPI({ description: '', content: { 'application/json': { - schema: responseSchema as any, + schema: responseSchema, }, }, }; @@ -130,80 +136,109 @@ export function useOpenAPI({ }; } if (schemas.request?.headers) { - let headersSchema: any = schemas.request.headers; + let headersSchema = schemas.request.headers; + if (isZodSchema(headersSchema)) { - headersSchema = zodToJsonSchema(headersSchema as any, { + // TODO: Possible bug. zodToJsonSchema is not returning the correct type. + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + headersSchema = zodToJsonSchema(headersSchema, { target: 'openApi3', }); } - for (const headerName in headersSchema.properties) { - const headerSchema = headersSchema.properties[headerName]; - operation.parameters = operation.parameters || []; - operation.parameters.push({ - name: headerName, - in: 'header', - required: headersSchema.required?.includes(headerName), - schema: headerSchema, - }); + if ('properties' in headersSchema) { + for (const headerName in headersSchema.properties) { + const headerSchema = headersSchema.properties[headerName]; + operation.parameters = operation.parameters || []; + operation.parameters.push({ + name: headerName, + in: 'header', + required: headersSchema.required?.includes(headerName), + schema: headerSchema, + }); + } } } if (schemas.request?.params) { - let paramsSchema: any = schemas.request.params; + let paramsSchema = schemas.request.params; + if (isZodSchema(paramsSchema)) { - paramsSchema = zodToJsonSchema(paramsSchema as any, { + // TODO: Possible bug. zodToJsonSchema is not returning the correct type. + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + paramsSchema = zodToJsonSchema(paramsSchema, { target: 'openApi3', }); } - for (const paramName in paramsSchema.properties) { - const paramSchema: any = paramsSchema.properties[paramName]; - operation.parameters = operation.parameters || []; - operation.parameters.push({ - name: paramName, - in: 'path', - required: paramsSchema.required?.includes(paramName), - schema: paramSchema, - }); + + if ('properties' in paramsSchema) { + for (const paramName in paramsSchema.properties) { + const paramSchema = paramsSchema.properties[paramName]; + + operation.parameters = operation.parameters || []; + operation.parameters.push({ + name: paramName, + in: 'path', + required: paramsSchema.required?.includes(paramName), + schema: paramSchema, + }); + } } } if (schemas.request?.query) { - let queriesSchema: any = schemas.request.query; + let queriesSchema = schemas.request.query; if (isZodSchema(queriesSchema)) { - queriesSchema = zodToJsonSchema(queriesSchema as any, { + // TODO: Possible bug. zodToJsonSchema is not returning the correct type. + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + queriesSchema = zodToJsonSchema(queriesSchema, { target: 'openApi3', }); } - for (const queryName in queriesSchema.properties) { - const querySchema = queriesSchema.properties[queryName]; - operation.parameters = operation.parameters || []; - operation.parameters.push({ - name: queryName, - in: 'query', - required: queriesSchema.required?.includes(queryName), - schema: querySchema as any, - }); + + if ('properties' in queriesSchema) { + for (const queryName in queriesSchema.properties) { + const querySchema = queriesSchema.properties[queryName]; + operation.parameters = operation.parameters || []; + operation.parameters.push({ + name: queryName, + in: 'query', + required: queriesSchema.required?.includes(queryName), + schema: querySchema, + }); + } } } if (schemas.request?.json) { - let requestJsonSchema: any = schemas.request.json; + let requestJsonSchema = schemas.request.json; + if (isZodSchema(requestJsonSchema)) { - requestJsonSchema = zodToJsonSchema(requestJsonSchema as any, { + // TODO: Possible bug. zodToJsonSchema is not returning the correct type. + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + requestJsonSchema = zodToJsonSchema(requestJsonSchema, { target: 'openApi3', }); } - const requestBody = (operation.requestBody = (operation.requestBody || {}) as any); + + const requestBody = (operation.requestBody = operation.requestBody || {}); requestBody.required = true; - const requestBodyContent = (requestBody.content = (requestBody.content || {}) as any); + const requestBodyContent = (requestBody.content = requestBody.content || {}); requestBodyContent['application/json'] = { schema: requestJsonSchema, }; } if (schemas.request?.formData) { - const requestBody = (operation.requestBody = (operation.requestBody || {}) as any); + const requestBody = (operation.requestBody = operation.requestBody || {}); requestBody.required = true; - const requestBodyContent = (requestBody.content = (requestBody.content || {}) as any); - let requestFormDataSchema: any = schemas.request.formData; + const requestBodyContent = (requestBody.content = requestBody.content || {}); + let requestFormDataSchema = schemas.request.formData; + if (isZodSchema(requestFormDataSchema)) { - requestFormDataSchema = zodToJsonSchema(requestFormDataSchema as any, { + // TODO: Possible bug. zodToJsonSchema is not returning the correct type. + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + requestFormDataSchema = zodToJsonSchema(requestFormDataSchema, { target: 'openApi3', }); } diff --git a/packages/fets/src/typed-fetch.ts b/packages/fets/src/typed-fetch.ts index c5bdacdd2..f2d9a81c2 100644 --- a/packages/fets/src/typed-fetch.ts +++ b/packages/fets/src/typed-fetch.ts @@ -47,7 +47,7 @@ export type NotOkStatusCode = Exclude; export type TypedBody< TJSON, - TFormData extends Record, + TFormData extends Record, THeaders extends Record, > = Omit & { /** @@ -314,7 +314,7 @@ export type HTTPMethod = export type TypedRequestInit< THeaders extends Record, TMethod extends HTTPMethod, - TFormData extends Record, + TFormData extends Record, > = Omit & { method: TMethod; headers: TypedHeaders; @@ -323,7 +323,10 @@ export type TypedRequestInit< export type TypedRequest< TJSON = any, - TFormData extends Record = Record, + TFormData extends Record = Record< + string, + FormDataEntryValue | undefined + >, THeaders extends Record = Record, TMethod extends HTTPMethod = HTTPMethod, TQueryParams extends Record = Record, @@ -340,7 +343,7 @@ export type TypedRequestCtor = new < THeaders extends Record, TMethod extends HTTPMethod, TQueryParams extends Record, - TFormData extends Record, + TFormData extends Record, >( input: string | TypedURL, init?: TypedRequestInit, @@ -391,7 +394,10 @@ export type TypedURLCtor = new TypedURL; export interface TypedFormData< - TMap extends Record = Record, + TMap extends Record = Record< + string, + FormDataEntryValue | undefined + >, > { append( name: TName, diff --git a/packages/fets/src/types.ts b/packages/fets/src/types.ts index e903f96fe..9508369d1 100644 --- a/packages/fets/src/types.ts +++ b/packages/fets/src/types.ts @@ -34,42 +34,45 @@ export type JSONSerializer = (obj: any) => string; export type JSONSchema = Exclude; export interface OpenAPIInfo { - title?: string; - description?: string; - version?: string; - license?: { - name?: string; - url?: string; - }; + title?: string | undefined; + description?: string | undefined; + version?: string | undefined; + license?: + | { + name?: string | undefined; + url?: string | undefined; + } + | undefined; } export type OpenAPIPathObject = Record & { - parameters?: OpenAPIParameterObject[]; + parameters?: OpenAPIParameterObject[] | undefined; }; export interface OpenAPIParameterObject { name: string; in: 'path' | 'query' | 'header' | 'cookie'; - required?: boolean; + required?: boolean | undefined; schema?: any; } export interface OpenAPIRequestBodyObject { - content?: Record; + content?: Record | undefined; + required?: boolean | undefined; } export interface OpenAPIOperationObject { - operationId?: string; - description?: string; - tags?: string[]; - parameters?: OpenAPIParameterObject[]; - requestBody?: OpenAPIRequestBodyObject; - responses?: Record; + operationId?: string | undefined; + description?: string | undefined; + tags?: string[] | undefined; + parameters?: OpenAPIParameterObject[] | undefined; + requestBody?: OpenAPIRequestBodyObject | undefined; + responses?: Record | undefined; } export interface OpenAPIResponseObject { - description?: string; - content?: Record; + description?: string | undefined; + content?: Record | undefined; } export interface OpenAPIMediaTypeObject { @@ -77,38 +80,46 @@ export interface OpenAPIMediaTypeObject { } export type OpenAPIDocument = { - openapi?: string; - info?: OpenAPIInfo; + openapi?: string | undefined; + info?: OpenAPIInfo | undefined; servers?: | { url: string; }[] - | string[]; - paths?: Record; + | string[] + | undefined; + paths?: Record | undefined; components?: unknown; }; export interface RouterOpenAPIOptions extends OpenAPIDocument { - endpoint?: string | false; - components?: TComponents; + endpoint?: string | false | undefined; + components?: TComponents | undefined; } export interface RouterSwaggerUIOptions extends SwaggerUIOpts { - endpoint?: string | false; + endpoint?: string | false | undefined; } +// I've created a PR to fix this in @whatwg-node/server https://github.com/ardatan/whatwg-node/issues/391 +// Interface 'RouterOptions' incorrectly extends interface 'ServerAdapterOptions'. +// Types of property 'plugins' are incompatible. +// Type 'RouterPlugin[] | undefined' is not assignable to type 'ServerAdapterPlugin[]'. +// Type 'undefined' is not assignable to type 'ServerAdapterPlugin[]' +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore export interface RouterOptions extends ServerAdapterOptions { - base?: string; - plugins?: RouterPlugin[]; + base?: string | undefined; + plugins?: RouterPlugin[] | undefined; - openAPI?: RouterOpenAPIOptions; - swaggerUI?: RouterSwaggerUIOptions; + openAPI?: RouterOpenAPIOptions | undefined; + swaggerUI?: RouterSwaggerUIOptions | undefined; } export type RouterComponentsBase = { - schemas?: Record; + schemas: Record; }; /* Maybe later; @@ -174,13 +185,16 @@ export type FromRouterComponentSchema< export type PromiseOrValue = T | Promise; export type StatusCodeMap = { - [TKey in StatusCode]?: T; + [TKey in StatusCode]?: T | undefined; }; export type TypedRouterHandlerTypeConfig< TPath extends string, TRequestJSON = any, - TRequestFormData extends Record = Record, + TRequestFormData extends Record = Record< + string, + FormDataEntryValue | undefined + >, TRequestHeaders extends Record = Record, TRequestQueryParams extends Record = Record, TRequestPathParams extends Record = Record< @@ -190,13 +204,13 @@ export type TypedRouterHandlerTypeConfig< TResponseJSONStatusMap extends StatusCodeMap = StatusCodeMap, > = { request: { - json?: TRequestJSON; - formData?: TRequestFormData; - headers?: TRequestHeaders; - query?: TRequestQueryParams; - params?: TRequestPathParams; + json?: TRequestJSON | undefined; + formData?: TRequestFormData | undefined; + headers?: TRequestHeaders | undefined; + query?: TRequestQueryParams | undefined; + params?: TRequestPathParams | undefined; }; - responses?: TResponseJSONStatusMap; + responses?: TResponseJSONStatusMap | undefined; }; export type TypedRequestFromTypeConfig< @@ -223,7 +237,7 @@ export type TypedRequestFromTypeConfig< : never : TypedRequest< any, - Record, + Record, Record, TMethod, Record, @@ -332,12 +346,12 @@ export type RouteHandler< // TODO: Remove Response from here export type OnRouteHookPayload = { - operationId?: string; - description?: string; - tags?: string[]; + operationId?: string | undefined; + description?: string | undefined; + tags?: string[] | undefined; method: HTTPMethod; path: string; - schemas?: RouteSchemas | RouteZodSchemas; + schemas?: RouteSchemas | RouteZodSchemas | undefined; openAPIDocument: OpenAPIDocument; handlers: RouteHandler[]; }; @@ -356,20 +370,22 @@ export type OnSerializeResponseHook = ( ) => void; export type RouterPlugin = ServerAdapterPlugin & { - onRouterInit?: OnRouterInitHook; - onRoute?: OnRouteHook; - onSerializeResponse?: OnSerializeResponseHook; + onRouterInit?: OnRouterInitHook | undefined; + onRoute?: OnRouteHook | undefined; + onSerializeResponse?: OnSerializeResponseHook | undefined; }; export type RouteSchemas = { - request?: { - headers?: JSONSchema; - params?: JSONSchema; - query?: JSONSchema; - json?: JSONSchema; - formData?: JSONSchema; - }; - responses?: StatusCodeMap; + request?: + | { + headers?: JSONSchema | undefined; + params?: JSONSchema | undefined; + query?: JSONSchema | undefined; + json?: JSONSchema | undefined; + formData?: JSONSchema | undefined; + } + | undefined; + responses?: StatusCodeMap | undefined; }; export type RouterSDKOpts< @@ -384,11 +400,11 @@ export type RouterSDKOpts< infer TPathParam > ? { - json?: TJSONBody; - formData?: TFormData; - headers?: THeaders; - query?: TQueryParams; - params?: TPathParam; + json?: TJSONBody | undefined; + formData?: TFormData | undefined; + headers?: THeaders | undefined; + query?: TQueryParams | undefined; + params?: TPathParam | undefined; } : never; @@ -399,7 +415,7 @@ export type RouterSDK< > = { [TPathKey in TPath]: { [TMethod in Lowercase]: ( - opts?: RouterSDKOpts, + opts?: RouterSDKOpts | undefined, ) => Promise>; }; }; @@ -431,10 +447,10 @@ export type TypedRequestFromRouteSchemas< ? FromSchemaWithComponents< TComponents, TRouteSchemas['request']['formData'] - > extends Record + > extends Record ? FromSchemaWithComponents - : Record - : Record, + : Record + : Record, TRouteSchemas['request'] extends { headers: JSONSchema } ? FromSchemaWithComponents extends Record< string, @@ -463,7 +479,7 @@ export type TypedRequestFromRouteSchemas< > : TypedRequest< any, - Record, + Record, Record, TMethod, Record, @@ -499,7 +515,7 @@ export type AddRouteWithTypesOpts< TPath extends string, TTypedRequest extends TypedRequest< any, - Record, + Record, Record, TMethod, Record, @@ -507,11 +523,11 @@ export type AddRouteWithTypesOpts< >, TTypedResponse extends TypedResponse, > = { - operationId?: string; - description?: string; - method?: TMethod; - tags?: string[]; - internal?: boolean; + operationId?: string | undefined; + description?: string | undefined; + method?: TMethod | undefined; + tags?: string[] | undefined; + internal?: boolean | undefined; path: TPath; handler: | RouteHandler @@ -553,7 +569,7 @@ export type RouterClient> = TRouter['__cli export type RouterInput> = { [TPath in keyof RouterClient]: { [TMethod in keyof RouterClient[TPath]]: RouterClient[TPath][TMethod] extends ( - requestParams?: infer TRequestParams, + requestParams?: infer TRequestParams | undefined, ) => any ? Required : never; diff --git a/packages/fets/src/utils.ts b/packages/fets/src/utils.ts index 7df9df7de..26477cc1d 100644 --- a/packages/fets/src/utils.ts +++ b/packages/fets/src/utils.ts @@ -10,14 +10,14 @@ export interface PatternHandlersObj { interface AddHandlerToMethodOpts { // Operation related options - operationId?: string; - description?: string; - tags?: string[]; + operationId?: string | undefined; + description?: string | undefined; + tags?: string[] | undefined; method: HTTPMethod; path: string; - schemas?: RouteSchemas; + schemas?: RouteSchemas | undefined; handlers: RouteHandler[]; - internal?: boolean; + internal?: boolean | undefined; // Router related options onRouteHooks: OnRouteHook[]; @@ -59,7 +59,7 @@ function preparePatternHandlerObjByMethod({ declare global { interface URLPattern { - isPattern?: boolean; + isPattern?: boolean | undefined; } } diff --git a/packages/fets/src/zod/types.ts b/packages/fets/src/zod/types.ts index 2bf4c2f47..4d4c0464d 100644 --- a/packages/fets/src/zod/types.ts +++ b/packages/fets/src/zod/types.ts @@ -1,3 +1,4 @@ +import type { ZodSchema } from 'zod'; import { HTTPMethod, TypedRequest, @@ -6,31 +7,33 @@ import { } from '../typed-fetch'; import { AddRouteWithTypesOpts, StatusCodeMap } from '../types'; -export type ZodType = { _output: any; safeParse(input: any): any }; +export type ZodType = ZodSchema; // { _output: any; safeParse(input: any): any }; export type InferZodType = T['_output']; export type RouteZodSchemas = { - request?: { - json?: ZodType; - formData?: ZodType; - headers?: ZodType; - params?: ZodType; - query?: ZodType; - }; - responses?: StatusCodeMap; + request?: + | { + json?: ZodType | undefined; + formData?: ZodType | undefined; + headers?: ZodType | undefined; + params?: ZodType | undefined; + query?: ZodType | undefined; + } + | undefined; + responses?: StatusCodeMap | undefined; }; export type TypedRequestFromRouteZodSchemas< TRouteZodSchemas extends RouteZodSchemas, TMethod extends HTTPMethod, -> = TRouteZodSchemas extends { request: Required['request'] } +> = TRouteZodSchemas extends { request: RouteZodSchemas['request'] } ? TypedRequest< TRouteZodSchemas['request'] extends { json: ZodType } ? InferZodType : any, TRouteZodSchemas['request'] extends { formData: ZodType } ? InferZodType - : Record, + : Record, TRouteZodSchemas['request'] extends { headers: ZodType } ? InferZodType : Record, @@ -42,7 +45,12 @@ export type TypedRequestFromRouteZodSchemas< ? InferZodType : Record > - : TypedRequest, Record, TMethod>; + : TypedRequest< + any, + Record, + Record, + TMethod + >; export type TypedResponseFromRouteZodSchemas = TRouteZodSchemas extends { responses: StatusCodeMap } diff --git a/packages/fets/tests/router.spec.ts b/packages/fets/tests/router.spec.ts index 3122500f5..6a0244a52 100644 --- a/packages/fets/tests/router.spec.ts +++ b/packages/fets/tests/router.spec.ts @@ -58,9 +58,14 @@ describe('Router', () => { expect(json.message).toBe('Hello John!'); }); it('should process multiple handlers for the same route', async () => { - const router = createRouter<{ - message: string; - }>(); + const router = createRouter< + { + message: string; + }, + { schemas: {} }, + {} + >(); + router.route({ path: '/greetings', method: 'GET', @@ -107,6 +112,7 @@ describe('Router', () => { }); it('can pull route params from the basepath as well', async () => { const router = createRouter({ base: '/api' }); + router.route({ path: '/greetings/:name', method: 'GET', @@ -121,10 +127,11 @@ describe('Router', () => { }); it('can handle nested routers', async () => { - const router = createRouter(); - const nested = createRouter({ + const router = createRouter(); + const nested = createRouter({ base: '/api', }); + nested.route({ path: '/greetings/:name', method: 'GET', diff --git a/tsconfig.json b/tsconfig.json index 38bd46c3c..c15520dac 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -26,6 +26,7 @@ "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, "noPropertyAccessFromIndexSignature": false, + "exactOptionalPropertyTypes": true, "paths": { "@e2e/*": ["e2e/*/src/index.ts"], "fets": ["packages/fets/src/index.ts"] diff --git a/yarn.lock b/yarn.lock index 187ac0fdb..711054a44 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11481,6 +11481,15 @@ resolve@^1.0.0, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.10.1, resolve@^1.14. path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" +resolve@^1.22.3: + version "1.22.3" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.3.tgz#4b4055349ffb962600972da1fdc33c46a4eb3283" + integrity sha512-P8ur/gp/AmbEzjr729bZnLjXK5Z+4P0zhIJgBgzqRih7hL7BOukHGtSTA3ACMY467GRFz3duQsi0bDZdR7DKdw== + dependencies: + is-core-module "^2.12.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + resolve@^2.0.0-next.4: version "2.0.0-next.4" resolved "https://registry.yarnpkg.com/resolve/-/resolve-2.0.0-next.4.tgz#3d37a113d6429f496ec4752d2a2e58efb1fd4660"