From eb31a0cebb01c7ebab530d221baa13ff32ec61db Mon Sep 17 00:00:00 2001 From: Alexey Lyakhov Date: Fri, 28 Jul 2023 21:39:21 +0700 Subject: [PATCH 1/7] improved ts config, fixed types --- .vscode/settings.json | 3 +- e2e/shared-scripts/src/types.ts | 4 +- e2e/shared-server/src/index.ts | 2 +- .../nextjs-example/src/pages/api/[...slug].ts | 1 + examples/todolist/src/router.ts | 3 +- examples/zod-example/package.json | 2 +- examples/zod-example/src/index.ts | 6 + examples/zod-example/tsconfig.json | 1 + package.json | 4 +- packages/fets/src/createRouter.ts | 11 +- packages/fets/src/plugins/ajv.ts | 32 +++- packages/fets/src/plugins/openapi.ts | 179 +++++++++++------- packages/fets/src/types.ts | 139 +++++++------- packages/fets/src/utils.ts | 12 +- packages/fets/src/zod/types.ts | 21 +- packages/fets/tests/json-schema-test.ts | 1 + packages/fets/tests/plugins/openapi.spec.ts | 1 + packages/fets/tests/router.spec.ts | 23 ++- tsconfig.json | 1 + yarn.lock | 9 +- 20 files changed, 273 insertions(+), 182 deletions(-) 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/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/nextjs-example/src/pages/api/[...slug].ts b/examples/nextjs-example/src/pages/api/[...slug].ts index 16245c835..beab0318d 100644 --- a/examples/nextjs-example/src/pages/api/[...slug].ts +++ b/examples/nextjs-example/src/pages/api/[...slug].ts @@ -38,6 +38,7 @@ export default createRouter({ openAPI: { endpoint: '/api/openapi.json', }, + plugins: [], }) .route({ method: 'GET', diff --git a/examples/todolist/src/router.ts b/examples/todolist/src/router.ts index 3cce6547b..24f0f6105 100644 --- a/examples/todolist/src/router.ts +++ b/examples/todolist/src/router.ts @@ -25,8 +25,9 @@ export const router = createRouter({ schemas: { Todo: TodoSchema, }, - } as const, + }, }, + plugins: [], }) .route({ description: 'Get all todos', diff --git a/examples/zod-example/package.json b/examples/zod-example/package.json index 2a51f478e..8a98d64b8 100644 --- a/examples/zod-example/package.json +++ b/examples/zod-example/package.json @@ -11,7 +11,7 @@ "fets": "0.4.8", "ts-node": "10.9.1", "ts-node-dev": "2.0.0", - "typescript": "^5.0.0", + "typescript": "5.1.6", "zod": "3.21.4" } } 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/package.json b/package.json index e9467ab17..04aeee16a 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "eslint": "8.45.0", "eslint-config-prettier": "8.9.0", "eslint-config-standard": "17.1.0", - "eslint-plugin-import": "2.27.5", + "eslint-plugin-import": "2.28.0", "eslint-plugin-n": "16.0.1", "eslint-plugin-node": "11.1.0", "eslint-plugin-promise": "6.1.1", @@ -57,7 +57,7 @@ "lint-staged": "13.2.3", "patch-package": "8.0.0", "prettier": "3.0.0", - "prettier-plugin-tailwindcss": "^0.4.0", + "prettier-plugin-tailwindcss": "0.4.1", "ts-jest": "29.1.1", "typescript": "5.1.6" }, diff --git a/packages/fets/src/createRouter.ts b/packages/fets/src/createRouter.ts index ce339cd04..e354f4b7d 100644 --- a/packages/fets/src/createRouter.ts +++ b/packages/fets/src/createRouter.ts @@ -37,13 +37,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 = { @@ -242,19 +242,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'; diff --git a/packages/fets/src/plugins/ajv.ts b/packages/fets/src/plugins/ajv.ts index c02e7333a..4288dcc7f 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 ( 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/types.ts b/packages/fets/src/types.ts index c88b4888f..9e8a1f9b1 100644 --- a/packages/fets/src/types.ts +++ b/packages/fets/src/types.ts @@ -33,42 +33,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 { @@ -76,36 +79,38 @@ export interface OpenAPIMediaTypeObject { } export type OpenAPIDocument = { - openapi?: string; - info?: OpenAPIInfo; - servers?: { - url: string; - }[]; - paths?: Record; + openapi?: string | undefined; + info?: OpenAPIInfo | undefined; + servers?: + | { + url: 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; } export interface RouterOptions extends ServerAdapterOptions { - base?: string; - plugins?: RouterPlugin[]; + base?: string | undefined; + plugins: RouterPlugin[]; - openAPI?: RouterOpenAPIOptions; - swaggerUI?: RouterSwaggerUIOptions; + openAPI?: RouterOpenAPIOptions | undefined; + swaggerUI?: RouterSwaggerUIOptions | undefined; } export type RouterComponentsBase = { - schemas?: Record; + schemas: Record; }; /* Maybe later; @@ -171,7 +176,7 @@ export type FromRouterComponentSchema< export type PromiseOrValue = T | Promise; export type StatusCodeMap = { - [TKey in StatusCode]?: T; + [TKey in StatusCode]?: T | undefined; }; export type TypedRouterHandlerTypeConfig< @@ -183,13 +188,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< @@ -315,12 +320,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[]; }; @@ -339,20 +344,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< @@ -367,11 +374,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; @@ -382,7 +389,7 @@ export type RouterSDK< > = { [TPathKey in TPath]: { [TMethod in Lowercase]: ( - opts?: RouterSDKOpts, + opts?: RouterSDKOpts | undefined, ) => Promise>; }; }; @@ -475,11 +482,11 @@ export type AddRouteWithTypesOpts< TTypedRequest extends TypedRequest, 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 @@ -521,7 +528,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..e379172f6 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,18 +7,20 @@ 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< diff --git a/packages/fets/tests/json-schema-test.ts b/packages/fets/tests/json-schema-test.ts index e9dc0269f..3201e1bbb 100644 --- a/packages/fets/tests/json-schema-test.ts +++ b/packages/fets/tests/json-schema-test.ts @@ -267,6 +267,7 @@ async function main() { }, } as const, }, + plugins: [], }).route({ operationId: 'getTodos', method: 'GET', diff --git a/packages/fets/tests/plugins/openapi.spec.ts b/packages/fets/tests/plugins/openapi.spec.ts index 011a2bb37..5e07fad53 100644 --- a/packages/fets/tests/plugins/openapi.spec.ts +++ b/packages/fets/tests/plugins/openapi.spec.ts @@ -4,6 +4,7 @@ describe('OpenAPI spec', () => { it('respects base path', async () => { const router = createRouter({ base: '/api', + plugins: [], }).route({ path: '/greetings', method: 'GET', diff --git a/packages/fets/tests/router.spec.ts b/packages/fets/tests/router.spec.ts index 3122500f5..6b712ee0e 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', @@ -106,7 +111,8 @@ describe('Router', () => { expect(json.message).toBe('Hello to you John!'); }); it('can pull route params from the basepath as well', async () => { - const router = createRouter({ base: '/api' }); + const router = createRouter({ base: '/api', plugins: [] }); + router.route({ path: '/greetings/:name', method: 'GET', @@ -121,10 +127,12 @@ describe('Router', () => { }); it('can handle nested routers', async () => { - const router = createRouter(); - const nested = createRouter({ + const router = createRouter(); + const nested = createRouter({ base: '/api', + plugins: [], }); + nested.route({ path: '/greetings/:name', method: 'GET', @@ -163,6 +171,7 @@ describe('Router', () => { it('supports "/" with base', async () => { const router = createRouter({ base: '/api', + plugins: [], }); router.route({ path: '/', @@ -193,6 +202,7 @@ describe('Router', () => { it('supports "/" in the base', async () => { const router = createRouter({ base: '/', + plugins: [], }); router.route({ path: '/greetings', @@ -209,6 +219,7 @@ describe('Router', () => { it('supports "/" both in the base and in the route', async () => { const router = createRouter({ base: '/', + plugins: [], }); router.route({ path: '/', 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 0d1275e61..870b9cb95 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10699,10 +10699,10 @@ prettier-plugin-sh@^0.13.0: mvdan-sh "^0.10.1" sh-syntax "^0.4.1" -prettier-plugin-tailwindcss@^0.4.0: - version "0.4.1" - resolved "https://registry.yarnpkg.com/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.4.1.tgz#f7ed664199540978b2cbd037bac3a337d6689e86" - integrity sha512-hwn2EiJmv8M+AW4YDkbjJ6HlZCTzLyz1QlySn9sMuKV/Px0fjwldlB7tol8GzdgqtkdPtzT3iJ4UzdnYXP25Ag== +prettier-plugin-tailwindcss@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.4.0.tgz#b3bd20e28514607c584d4ce515ed09b76cc16dd2" + integrity sha512-Rna0sDPETA0KNhMHlN8wxKNgfSa8mTl2hPPAGxnbv6tUcHT6J4RQmQ8TLXyhB7Dm5Von4iHloBxTyClYM6wT0A== prettier@3.0.0: version "3.0.0" @@ -12783,7 +12783,6 @@ typescript@~3.8.3: uWebSockets.js@uNetworking/uWebSockets.js#v20.30.0: version "20.30.0" - uid d39d4181daf5b670d44cbc1b18f8c28c85fd4142 resolved "https://codeload.github.com/uNetworking/uWebSockets.js/tar.gz/d39d4181daf5b670d44cbc1b18f8c28c85fd4142" ufo@^1.1.2: From e180697c589f03e24e152de462e29adbaca7d56f Mon Sep 17 00:00:00 2001 From: Alexey Lyakhov Date: Sun, 30 Jul 2023 12:07:06 +0700 Subject: [PATCH 2/7] regenerate yarn-lock --- yarn.lock | 70 ++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 62 insertions(+), 8 deletions(-) diff --git a/yarn.lock b/yarn.lock index 870b9cb95..6f44ace40 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4104,6 +4104,17 @@ array-union@^2.1.0: resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== +array.prototype.findlastindex@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.2.tgz#bc229aef98f6bd0533a2bc61ff95209875526c9b" + integrity sha512-tb5thFFlUcp7NdNF6/MpDk/1r/4awWG1FIz3YqDf+/zJSTezBb+/5WViH41obXULHVpDzoiCLpJ/ZO9YbJMsdw== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + es-shim-unscopables "^1.0.0" + get-intrinsic "^1.1.3" + array.prototype.flat@^1.2.3, array.prototype.flat@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz#ffc6576a7ca3efc2f46a143b9d1dda9b4b3cf5e2" @@ -6126,7 +6137,7 @@ eslint-import-resolver-typescript@^3.5.2: is-glob "^4.0.3" synckit "^0.8.5" -eslint-module-utils@^2.7.4: +eslint-module-utils@^2.7.4, eslint-module-utils@^2.8.0: version "2.8.0" resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz#e439fee65fc33f6bba630ff621efc38ec0375c49" integrity sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw== @@ -6149,7 +6160,31 @@ eslint-plugin-es@^3.0.0: eslint-utils "^2.0.0" regexpp "^3.0.0" -eslint-plugin-import@2.27.5, eslint-plugin-import@^2.26.0: +eslint-plugin-import@2.28.0: + version "2.28.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.28.0.tgz#8d66d6925117b06c4018d491ae84469eb3cb1005" + integrity sha512-B8s/n+ZluN7sxj9eUf7/pRFERX0r5bnFA2dCaLHy2ZeaQEAz0k+ZZkFWRFHJAqxfxQDx6KLv9LeIki7cFdwW+Q== + dependencies: + array-includes "^3.1.6" + array.prototype.findlastindex "^1.2.2" + array.prototype.flat "^1.3.1" + array.prototype.flatmap "^1.3.1" + debug "^3.2.7" + doctrine "^2.1.0" + eslint-import-resolver-node "^0.3.7" + eslint-module-utils "^2.8.0" + has "^1.0.3" + is-core-module "^2.12.1" + is-glob "^4.0.3" + minimatch "^3.1.2" + object.fromentries "^2.0.6" + object.groupby "^1.0.0" + object.values "^1.1.6" + resolve "^1.22.3" + semver "^6.3.1" + tsconfig-paths "^3.14.2" + +eslint-plugin-import@^2.26.0: version "2.27.5" resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.27.5.tgz#876a6d03f52608a3e5bb439c2550588e51dd6c65" integrity sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow== @@ -7626,7 +7661,7 @@ is-ci@^3.0.1: dependencies: ci-info "^3.2.0" -is-core-module@^2.11.0, is-core-module@^2.12.1, is-core-module@^2.5.0, is-core-module@^2.9.0: +is-core-module@^2.11.0, is-core-module@^2.12.0, is-core-module@^2.12.1, is-core-module@^2.5.0, is-core-module@^2.9.0: version "2.12.1" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.12.1.tgz#0c0b6885b6f80011c71541ce15c8d66cf5a4f9fd" integrity sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg== @@ -9983,6 +10018,16 @@ object.getownpropertydescriptors@^2.0.3: es-abstract "^1.21.2" safe-array-concat "^1.0.0" +object.groupby@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/object.groupby/-/object.groupby-1.0.0.tgz#cb29259cf90f37e7bac6437686c1ea8c916d12a9" + integrity sha512-70MWG6NfRH9GnbZOikuhPPYzpUpof9iW2J9E4dW7FXTqPNb6rllE6u39SKwwiNh8lCwX3DDb5OgcKGiEBrTTyw== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.21.2" + get-intrinsic "^1.2.1" + object.hasown@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/object.hasown/-/object.hasown-1.1.2.tgz#f919e21fad4eb38a57bc6345b3afd496515c3f92" @@ -10699,10 +10744,10 @@ prettier-plugin-sh@^0.13.0: mvdan-sh "^0.10.1" sh-syntax "^0.4.1" -prettier-plugin-tailwindcss@0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.4.0.tgz#b3bd20e28514607c584d4ce515ed09b76cc16dd2" - integrity sha512-Rna0sDPETA0KNhMHlN8wxKNgfSa8mTl2hPPAGxnbv6tUcHT6J4RQmQ8TLXyhB7Dm5Von4iHloBxTyClYM6wT0A== +prettier-plugin-tailwindcss@0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.4.1.tgz#f7ed664199540978b2cbd037bac3a337d6689e86" + integrity sha512-hwn2EiJmv8M+AW4YDkbjJ6HlZCTzLyz1QlySn9sMuKV/Px0fjwldlB7tol8GzdgqtkdPtzT3iJ4UzdnYXP25Ag== prettier@3.0.0: version "3.0.0" @@ -11413,6 +11458,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" @@ -12618,7 +12672,7 @@ tsconfig-paths@4.2.0: minimist "^1.2.6" strip-bom "^3.0.0" -tsconfig-paths@^3.14.1: +tsconfig-paths@^3.14.1, tsconfig-paths@^3.14.2: version "3.14.2" resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz#6e32f1f79412decd261f92d633a9dc1cfa99f088" integrity sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g== From afaa1e66dfc981f983680fce89e58ed5d0594ffd Mon Sep 17 00:00:00 2001 From: Alexey Lyakhov Date: Mon, 31 Jul 2023 11:42:49 +0700 Subject: [PATCH 3/7] fix benchmark error --- benchmark/start-server.ts | 5 +++++ 1 file changed, 5 insertions(+) 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'); From 741dc167b4b5a6a833b00ff2888cd434cc43dcf1 Mon Sep 17 00:00:00 2001 From: Alexey Lyakhov Date: Tue, 1 Aug 2023 12:57:14 +0700 Subject: [PATCH 4/7] revert plugins to optional --- examples/nextjs-example/src/pages/api/[...slug].ts | 1 - examples/todolist/src/router.ts | 1 - packages/fets/src/types.ts | 9 ++++++++- packages/fets/tests/json-schema-test.ts | 1 - packages/fets/tests/plugins/openapi.spec.ts | 1 - packages/fets/tests/router.spec.ts | 6 +----- 6 files changed, 9 insertions(+), 10 deletions(-) diff --git a/examples/nextjs-example/src/pages/api/[...slug].ts b/examples/nextjs-example/src/pages/api/[...slug].ts index beab0318d..16245c835 100644 --- a/examples/nextjs-example/src/pages/api/[...slug].ts +++ b/examples/nextjs-example/src/pages/api/[...slug].ts @@ -38,7 +38,6 @@ export default createRouter({ openAPI: { endpoint: '/api/openapi.json', }, - plugins: [], }) .route({ method: 'GET', diff --git a/examples/todolist/src/router.ts b/examples/todolist/src/router.ts index 24f0f6105..43119e343 100644 --- a/examples/todolist/src/router.ts +++ b/examples/todolist/src/router.ts @@ -27,7 +27,6 @@ export const router = createRouter({ }, }, }, - plugins: [], }) .route({ description: 'Get all todos', diff --git a/packages/fets/src/types.ts b/packages/fets/src/types.ts index 68543672e..dc8434bff 100644 --- a/packages/fets/src/types.ts +++ b/packages/fets/src/types.ts @@ -101,10 +101,17 @@ export interface RouterSwaggerUIOptions extends SwaggerUIOpts { 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 | undefined; - plugins: RouterPlugin[]; + plugins?: RouterPlugin[] | undefined; openAPI?: RouterOpenAPIOptions | undefined; swaggerUI?: RouterSwaggerUIOptions | undefined; diff --git a/packages/fets/tests/json-schema-test.ts b/packages/fets/tests/json-schema-test.ts index 3201e1bbb..e9dc0269f 100644 --- a/packages/fets/tests/json-schema-test.ts +++ b/packages/fets/tests/json-schema-test.ts @@ -267,7 +267,6 @@ async function main() { }, } as const, }, - plugins: [], }).route({ operationId: 'getTodos', method: 'GET', diff --git a/packages/fets/tests/plugins/openapi.spec.ts b/packages/fets/tests/plugins/openapi.spec.ts index 5e07fad53..011a2bb37 100644 --- a/packages/fets/tests/plugins/openapi.spec.ts +++ b/packages/fets/tests/plugins/openapi.spec.ts @@ -4,7 +4,6 @@ describe('OpenAPI spec', () => { it('respects base path', async () => { const router = createRouter({ base: '/api', - plugins: [], }).route({ path: '/greetings', method: 'GET', diff --git a/packages/fets/tests/router.spec.ts b/packages/fets/tests/router.spec.ts index 6b712ee0e..6a0244a52 100644 --- a/packages/fets/tests/router.spec.ts +++ b/packages/fets/tests/router.spec.ts @@ -111,7 +111,7 @@ describe('Router', () => { expect(json.message).toBe('Hello to you John!'); }); it('can pull route params from the basepath as well', async () => { - const router = createRouter({ base: '/api', plugins: [] }); + const router = createRouter({ base: '/api' }); router.route({ path: '/greetings/:name', @@ -130,7 +130,6 @@ describe('Router', () => { const router = createRouter(); const nested = createRouter({ base: '/api', - plugins: [], }); nested.route({ @@ -171,7 +170,6 @@ describe('Router', () => { it('supports "/" with base', async () => { const router = createRouter({ base: '/api', - plugins: [], }); router.route({ path: '/', @@ -202,7 +200,6 @@ describe('Router', () => { it('supports "/" in the base', async () => { const router = createRouter({ base: '/', - plugins: [], }); router.route({ path: '/greetings', @@ -219,7 +216,6 @@ describe('Router', () => { it('supports "/" both in the base and in the route', async () => { const router = createRouter({ base: '/', - plugins: [], }); router.route({ path: '/', From 6e23086200905f7d2464440ccb408db01782d726 Mon Sep 17 00:00:00 2001 From: Alexey Lyakhov Date: Tue, 1 Aug 2023 21:07:47 +0700 Subject: [PATCH 5/7] improve types --- examples/todolist/src/router.ts | 78 +++++++++++++++++++++++++++---- packages/fets/src/createRouter.ts | 17 +++++++ packages/fets/src/plugins/ajv.ts | 8 +++- packages/fets/src/typed-fetch.ts | 16 +++++-- packages/fets/src/types.ts | 17 ++++--- packages/fets/src/zod/types.ts | 11 +++-- 6 files changed, 121 insertions(+), 26 deletions(-) diff --git a/examples/todolist/src/router.ts b/examples/todolist/src/router.ts index 43119e343..ce42fbf01 100644 --- a/examples/todolist/src/router.ts +++ b/examples/todolist/src/router.ts @@ -1,4 +1,5 @@ import { createRouter, FromSchema, Response } from 'fets'; +import { TypedRequest, TypedResponse } from 'fets/src/typed-fetch'; const TodoSchema = { type: 'object', @@ -42,7 +43,9 @@ export const router = createRouter({ }, }, } as const, - handler: () => Response.json(todos), + handler: () => { + return Response.json(todos); + }, }) .route({ description: 'Get a todo', @@ -72,7 +75,18 @@ export const router = createRouter({ }, }, } as const, - handler: async request => { + handler: async ( + request: TypedRequest< + any, + Record, + Record, + 'GET', + Record, + { + id: string; + } + >, + ) => { const { id } = request.params; const todo = todos.find(todo => todo.id === id); if (!todo) { @@ -109,7 +123,18 @@ export const router = createRouter({ }, }, } as const, - handler: async request => { + handler: async ( + request: TypedRequest< + any, + Record, + Record, + 'PUT', + Record, + { + id: string; + } + >, + ) => { const input = await request.json(); const todo: Todo = { id: crypto.randomUUID(), @@ -153,7 +178,18 @@ export const router = createRouter({ }, }, } as const, - handler: async request => { + handler: async ( + request: TypedRequest< + any, + Record, + Record, + 'DELETE', + Record, + { + id: string; + } + >, + ) => { const { id } = request.params; const index = todos.findIndex(todo => todo.id === id); if (index === -1) { @@ -193,7 +229,7 @@ export const router = createRouter({ required: ['file'], additionalProperties: false, }, - }, + } as const, responses: { 200: { type: 'object', @@ -209,16 +245,40 @@ export const router = createRouter({ }, }, } as const, - handler: async request => { + handler: async ( + request: TypedRequest< + any, + any, + Record, + 'POST', + Record, + Record + >, + ): Promise< + TypedResponse< + { + readonly name: string; + readonly description: string; + readonly type: string; + readonly size: number; + readonly lastModified: number; + }, + Record, + 200 + > + > => { 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/packages/fets/src/createRouter.ts b/packages/fets/src/createRouter.ts index e354f4b7d..5c987a86b 100644 --- a/packages/fets/src/createRouter.ts +++ b/packages/fets/src/createRouter.ts @@ -291,9 +291,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 4288dcc7f..5e92b74c1 100644 --- a/packages/fets/src/plugins/ajv.ts +++ b/packages/fets/src/plugins/ajv.ts @@ -167,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/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 dc8434bff..de94df778 100644 --- a/packages/fets/src/types.ts +++ b/packages/fets/src/types.ts @@ -190,7 +190,10 @@ export type StatusCodeMap = { 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< @@ -233,7 +236,7 @@ export type TypedRequestFromTypeConfig< : never : TypedRequest< any, - Record, + Record, Record, TMethod, Record, @@ -443,10 +446,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, @@ -475,7 +478,7 @@ export type TypedRequestFromRouteSchemas< > : TypedRequest< any, - Record, + Record, Record, TMethod, Record, @@ -511,7 +514,7 @@ export type AddRouteWithTypesOpts< TPath extends string, TTypedRequest extends TypedRequest< any, - Record, + Record, Record, TMethod, Record, diff --git a/packages/fets/src/zod/types.ts b/packages/fets/src/zod/types.ts index e379172f6..4d4c0464d 100644 --- a/packages/fets/src/zod/types.ts +++ b/packages/fets/src/zod/types.ts @@ -26,14 +26,14 @@ export type RouteZodSchemas = { 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, @@ -45,7 +45,12 @@ export type TypedRequestFromRouteZodSchemas< ? InferZodType : Record > - : TypedRequest, Record, TMethod>; + : TypedRequest< + any, + Record, + Record, + TMethod + >; export type TypedResponseFromRouteZodSchemas = TRouteZodSchemas extends { responses: StatusCodeMap } From 2fbb740647c1f6445bdbba02492ce92952841fcd Mon Sep 17 00:00:00 2001 From: Alexey Lyakhov Date: Tue, 1 Aug 2023 21:12:25 +0700 Subject: [PATCH 6/7] fix type import --- examples/todolist/src/router.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/todolist/src/router.ts b/examples/todolist/src/router.ts index ce42fbf01..77238fe71 100644 --- a/examples/todolist/src/router.ts +++ b/examples/todolist/src/router.ts @@ -1,5 +1,5 @@ import { createRouter, FromSchema, Response } from 'fets'; -import { TypedRequest, TypedResponse } from 'fets/src/typed-fetch'; +import type { TypedRequest, TypedResponse } from 'fets/src/typed-fetch'; const TodoSchema = { type: 'object', From 36b597a85d526ebc3f4ae07db4b2db9d46a928f3 Mon Sep 17 00:00:00 2001 From: Alexey Lyakhov Date: Tue, 1 Aug 2023 21:27:57 +0700 Subject: [PATCH 7/7] revert types --- examples/todolist/src/router.ts | 63 +++------------------------------ 1 file changed, 4 insertions(+), 59 deletions(-) diff --git a/examples/todolist/src/router.ts b/examples/todolist/src/router.ts index 77238fe71..06dbae395 100644 --- a/examples/todolist/src/router.ts +++ b/examples/todolist/src/router.ts @@ -1,5 +1,4 @@ import { createRouter, FromSchema, Response } from 'fets'; -import type { TypedRequest, TypedResponse } from 'fets/src/typed-fetch'; const TodoSchema = { type: 'object', @@ -75,18 +74,7 @@ export const router = createRouter({ }, }, } as const, - handler: async ( - request: TypedRequest< - any, - Record, - Record, - 'GET', - Record, - { - id: string; - } - >, - ) => { + handler: async request => { const { id } = request.params; const todo = todos.find(todo => todo.id === id); if (!todo) { @@ -123,18 +111,7 @@ export const router = createRouter({ }, }, } as const, - handler: async ( - request: TypedRequest< - any, - Record, - Record, - 'PUT', - Record, - { - id: string; - } - >, - ) => { + handler: async request => { const input = await request.json(); const todo: Todo = { id: crypto.randomUUID(), @@ -178,18 +155,7 @@ export const router = createRouter({ }, }, } as const, - handler: async ( - request: TypedRequest< - any, - Record, - Record, - 'DELETE', - Record, - { - id: string; - } - >, - ) => { + handler: async request => { const { id } = request.params; const index = todos.findIndex(todo => todo.id === id); if (index === -1) { @@ -245,28 +211,7 @@ export const router = createRouter({ }, }, } as const, - handler: async ( - request: TypedRequest< - any, - any, - Record, - 'POST', - Record, - Record - >, - ): Promise< - TypedResponse< - { - readonly name: string; - readonly description: string; - readonly type: string; - readonly size: number; - readonly lastModified: number; - }, - Record, - 200 - > - > => { + handler: async request => { const body = await request.formData(); const file = body.get('file'); const description = body.get('description');