From 56995bf7d37726c346b0f0716f8915454048c641 Mon Sep 17 00:00:00 2001 From: Alexander Galabov Date: Wed, 3 Apr 2024 16:06:52 +0300 Subject: [PATCH 1/3] added support for effects in request params --- spec/routes/parameters.spec.ts | 104 ++++++++++++++++++++++++++++----- src/openapi-generator.ts | 54 +++++++++++------ src/openapi-registry.ts | 10 ++-- 3 files changed, 131 insertions(+), 37 deletions(-) diff --git a/spec/routes/parameters.spec.ts b/spec/routes/parameters.spec.ts index 6f18340..9ea55f6 100644 --- a/spec/routes/parameters.spec.ts +++ b/spec/routes/parameters.spec.ts @@ -21,16 +21,14 @@ describe('parameters', () => { ]); }); - it('generates a deepPartial object query parameter for route', () => { + it('generates query parameter for route from object with refine', () => { const { parameters } = generateDataForRoute({ request: { query: z .object({ - filter: z - .object({ test: z.string() }) - .openapi({ param: { style: 'deepObject' } }), + filter: z.string(), }) - .deepPartial(), + .refine(({ filter }) => filter.length > 3), }, }); @@ -38,15 +36,26 @@ describe('parameters', () => { { in: 'query', name: 'filter', - required: false, - style: 'deepObject', + required: true, schema: { - type: 'object', - properties: { - test: { - type: 'string', - }, - }, + type: 'string', + }, + }, + ]); + }); + + it('generates a query parameter for route', () => { + const { parameters } = generateDataForRoute({ + request: { query: z.object({ test: z.string() }) }, + }); + + expect(parameters).toEqual([ + { + in: 'query', + name: 'test', + required: true, + schema: { + type: 'string', }, }, ]); @@ -133,6 +142,29 @@ describe('parameters', () => { ]); }); + it('generates path parameter for route from object with refine', () => { + const { parameters } = generateDataForRoute({ + request: { + params: z + .object({ + filter: z.string(), + }) + .refine(({ filter }) => filter.length > 3), + }, + }); + + expect(parameters).toEqual([ + { + in: 'path', + name: 'filter', + required: true, + schema: { + type: 'string', + }, + }, + ]); + }); + it('generates a reference path parameter for route', () => { const TestParam = registerParameter( 'TestParam', @@ -212,6 +244,29 @@ describe('parameters', () => { ]); }); + it('generates cookie parameter for route from object with refine', () => { + const { parameters } = generateDataForRoute({ + request: { + cookies: z + .object({ + filter: z.string(), + }) + .refine(({ filter }) => filter.length > 3), + }, + }); + + expect(parameters).toEqual([ + { + in: 'cookie', + name: 'filter', + required: true, + schema: { + type: 'string', + }, + }, + ]); + }); + it('generates a reference cookie parameter for route', () => { const TestParam = registerParameter( 'TestParam', @@ -312,6 +367,29 @@ describe('parameters', () => { ]); }); + it('generates header parameter for route from object with refine', () => { + const { parameters } = generateDataForRoute({ + request: { + headers: z + .object({ + filter: z.string(), + }) + .refine(({ filter }) => filter.length > 3), + }, + }); + + expect(parameters).toEqual([ + { + in: 'header', + name: 'filter', + required: true, + schema: { + type: 'string', + }, + }, + ]); + }); + it('generates a reference header parameter for route', () => { const TestHeader = registerParameter( 'TestHeader', diff --git a/src/openapi-generator.ts b/src/openapi-generator.ts index f9a54ad..e897935 100644 --- a/src/openapi-generator.ts +++ b/src/openapi-generator.ts @@ -1,17 +1,16 @@ -import type { AnyZodObject, ZodRawShape, ZodType, ZodTypeAny } from 'zod'; +import type { + AnyZodObject, + ZodEffects, + ZodRawShape, + ZodType, + ZodTypeAny, +} from 'zod'; import { ConflictError, MissingParameterDataError, enhanceMissingParametersError, } from './errors'; -import { - compact, - isNil, - mapValues, - objectEquals, - omit, - omitBy, -} from './lib/lodash'; +import { compact, isNil, mapValues, objectEquals, omitBy } from './lib/lodash'; import { isAnyZodType, isZodType } from './lib/zod-is-type'; import { OpenAPIComponentObject, @@ -21,7 +20,7 @@ import { ZodContentObject, ZodRequestBody, } from './openapi-registry'; -import { ZodOpenApiFullMetadata, ZodOpenAPIMetadata } from './zod-extensions'; +import { ZodOpenApiFullMetadata } from './zod-extensions'; import { BaseParameterObject, ComponentsObject, @@ -500,7 +499,11 @@ export class OpenAPIGenerator { return []; } - const { query, params, headers, cookies } = request; + const { headers } = request; + + const query = this.cleanParameter(request.query); + const params = this.cleanParameter(request.params); + const cookies = this.cleanParameter(request.cookies); const queryParameters = enhanceMissingParametersError( () => (query ? this.generateInlineParameters(query, 'query') : []), @@ -518,14 +521,17 @@ export class OpenAPIGenerator { ); const headerParameters = enhanceMissingParametersError( - () => - headers - ? isZodType(headers, 'ZodObject') - ? this.generateInlineParameters(headers, 'header') - : headers.flatMap(header => - this.generateInlineParameters(header, 'header') - ) - : [], + () => { + if (Array.isArray(headers)) { + return headers.flatMap(header => + this.generateInlineParameters(header, 'header') + ); + } + const cleanHeaders = this.cleanParameter(headers); + return cleanHeaders + ? this.generateInlineParameters(cleanHeaders, 'header') + : []; + }, { location: 'header' } ); @@ -537,6 +543,16 @@ export class OpenAPIGenerator { ]; } + private cleanParameter( + schema: AnyZodObject | ZodEffects | undefined + ) { + if (!schema) { + return undefined; + } + + return isZodType(schema, 'ZodEffects') ? schema._def.schema : schema; + } + generatePath(route: RouteConfig): PathItemObject { const { method, path, request, responses, ...pathItemConfig } = route; diff --git a/src/openapi-registry.ts b/src/openapi-registry.ts index 96440ec..9e40819 100644 --- a/src/openapi-registry.ts +++ b/src/openapi-registry.ts @@ -58,7 +58,7 @@ type ResponseObject = ResponseObject30 | ResponseObject31; type SchemaObject = SchemaObject30 | SchemaObject31; type SecuritySchemeObject = SecuritySchemeObject30 | SecuritySchemeObject31; -import type { AnyZodObject, ZodType, ZodTypeAny } from 'zod'; +import type { AnyZodObject, ZodEffects, ZodType, ZodTypeAny } from 'zod'; type Method = | 'get' @@ -107,10 +107,10 @@ export type RouteConfig = Omit & { path: string; request?: { body?: ZodRequestBody; - params?: AnyZodObject; - query?: AnyZodObject; - cookies?: AnyZodObject; - headers?: AnyZodObject | ZodType[]; + params?: AnyZodObject | ZodEffects; + query?: AnyZodObject | ZodEffects; + cookies?: AnyZodObject | ZodEffects; + headers?: AnyZodObject | ZodEffects | ZodType[]; }; responses: { [statusCode: string]: ResponseConfig; From da040769037255693b40b41bc3009bcd7a5507ab Mon Sep 17 00:00:00 2001 From: Alexander Galabov Date: Wed, 3 Apr 2024 16:17:33 +0300 Subject: [PATCH 2/3] change type definitions to account for transform and preprocess --- src/openapi-generator.ts | 5 ++--- src/openapi-registry.ts | 13 +++++++++---- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/openapi-generator.ts b/src/openapi-generator.ts index e897935..ccc481a 100644 --- a/src/openapi-generator.ts +++ b/src/openapi-generator.ts @@ -17,6 +17,7 @@ import { OpenAPIDefinitions, ResponseConfig, RouteConfig, + RouteParameter, ZodContentObject, ZodRequestBody, } from './openapi-registry'; @@ -543,9 +544,7 @@ export class OpenAPIGenerator { ]; } - private cleanParameter( - schema: AnyZodObject | ZodEffects | undefined - ) { + private cleanParameter(schema: RouteParameter) { if (!schema) { return undefined; } diff --git a/src/openapi-registry.ts b/src/openapi-registry.ts index 9e40819..4e6e6e8 100644 --- a/src/openapi-registry.ts +++ b/src/openapi-registry.ts @@ -102,15 +102,20 @@ export interface ResponseConfig { content?: ZodContentObject; } +export type RouteParameter = + | AnyZodObject + | ZodEffects + | undefined; + export type RouteConfig = Omit & { method: Method; path: string; request?: { body?: ZodRequestBody; - params?: AnyZodObject | ZodEffects; - query?: AnyZodObject | ZodEffects; - cookies?: AnyZodObject | ZodEffects; - headers?: AnyZodObject | ZodEffects | ZodType[]; + params?: RouteParameter; + query?: RouteParameter; + cookies?: RouteParameter; + headers?: RouteParameter | ZodType[]; }; responses: { [statusCode: string]: ResponseConfig; From d4dad12b0ff6fdf18da3ae0a84233ac07dbe553c Mon Sep 17 00:00:00 2001 From: Alexander Galabov Date: Fri, 5 Apr 2024 12:44:51 +0300 Subject: [PATCH 3/3] ran npm audit fix --- package-lock.json | 48 +++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1ea993d..ed35cf9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2960,9 +2960,9 @@ } }, "node_modules/jest-snapshot/node_modules/semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -3686,9 +3686,9 @@ } }, "node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -3951,9 +3951,9 @@ } }, "node_modules/ts-jest/node_modules/semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -4183,9 +4183,9 @@ } }, "node_modules/zod": { - "version": "3.22.2", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.2.tgz", - "integrity": "sha512-wvWkphh5WQsJbVk1tbx1l1Ly4yg+XecD+Mq280uBGt9wa5BKSWf4Mhp6GmrkPixhMxmabYY7RbzlwVP32pbGCg==", + "version": "3.22.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", + "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", "dev": true, "funding": { "url": "https://github.com/sponsors/colinhacks" @@ -6392,9 +6392,9 @@ }, "dependencies": { "semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -6937,9 +6937,9 @@ } }, "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true }, "shebang-command": { @@ -7118,9 +7118,9 @@ }, "dependencies": { "semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -7277,9 +7277,9 @@ "dev": true }, "zod": { - "version": "3.22.2", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.2.tgz", - "integrity": "sha512-wvWkphh5WQsJbVk1tbx1l1Ly4yg+XecD+Mq280uBGt9wa5BKSWf4Mhp6GmrkPixhMxmabYY7RbzlwVP32pbGCg==", + "version": "3.22.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", + "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", "dev": true } }