diff --git a/docs/src/pages/reference/configuration/output.md b/docs/src/pages/reference/configuration/output.md index 176a367c9..e39b1de2f 100644 --- a/docs/src/pages/reference/configuration/output.md +++ b/docs/src/pages/reference/configuration/output.md @@ -1041,6 +1041,40 @@ Default Value: `false`. Use to set the strict mode for the zod schema. If you set it to true, the schema will be generated with the strict mode. +##### generate + +Type: `Object`. + +Default Value: `true`. + +Use to set the which type of schemas you want to generate for the zod schema. + +example: + +```js +module.exports = { + petstore: { + output: { + ... + override: { + zod: { + generate: { + param: true, + body: true, + response: false, + query: true, + header: true, + } + }, + }, + }, + ... + }, +}; +``` + +In the above example exclude response body validations not generated + ##### coerce Type: `Object`. diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index eb631e2b4..baa77a1c2 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -384,6 +384,13 @@ export type ZodOptions = { body?: boolean; response?: boolean; }; + generate: { + param: boolean; + query: boolean; + header: boolean; + body: boolean; + response: boolean; + }; coerce?: { param?: boolean | ZodCoerceType[]; query?: boolean | ZodCoerceType[]; @@ -411,6 +418,13 @@ export type NormalizedZodOptions = { body: boolean; response: boolean; }; + generate: { + param: boolean; + query: boolean; + header: boolean; + body: boolean; + response: boolean; + }; coerce: { param: boolean | ZodCoerceType[]; query: boolean | ZodCoerceType[]; diff --git a/packages/orval/src/utils/options.ts b/packages/orval/src/utils/options.ts index 9af9c7ced..7686b90a4 100644 --- a/packages/orval/src/utils/options.ts +++ b/packages/orval/src/utils/options.ts @@ -21,7 +21,6 @@ import { NormalizedOperationOptions, NormalizedOptions, NormalizedQueryOptions, - NormalizedZodOptions, OperationOptions, OptionsExport, OutputClient, @@ -31,11 +30,9 @@ import { RefComponentSuffix, SwaggerParserOptions, upath, - ZodOptions, } from '@orval/core'; import { DEFAULT_MOCK_OPTIONS } from '@orval/mock'; import chalk from 'chalk'; -import { InfoObject } from 'openapi3-ts/oas30'; import pkg from '../../package.json'; import { githubResolver } from './github'; import { loadPackageJson } from './package-json'; @@ -241,6 +238,13 @@ export const normalizeOptions = async ( body: outputOptions.override?.zod?.strict?.body ?? false, response: outputOptions.override?.zod?.strict?.response ?? false, }, + generate: { + param: outputOptions.override?.zod?.generate?.param ?? true, + query: outputOptions.override?.zod?.generate?.query ?? true, + header: outputOptions.override?.zod?.generate?.header ?? true, + body: outputOptions.override?.zod?.generate?.body ?? true, + response: outputOptions.override?.zod?.generate?.response ?? true, + }, coerce: { param: outputOptions.override?.zod?.coerce?.param ?? false, query: outputOptions.override?.zod?.coerce?.query ?? false, @@ -420,6 +424,13 @@ const normalizeOperationsAndTags = ( body: zod.strict?.body ?? false, response: zod.strict?.response ?? false, }, + generate: { + param: zod.generate?.param ?? true, + query: zod.generate?.query ?? true, + header: zod.generate?.header ?? true, + body: zod.generate?.body ?? true, + response: zod.generate?.response ?? true, + }, coerce: { param: zod.coerce?.param ?? false, query: zod.coerce?.query ?? false, diff --git a/packages/zod/src/index.ts b/packages/zod/src/index.ts index 39b4b8b40..0273b629e 100644 --- a/packages/zod/src/index.ts +++ b/packages/zod/src/index.ts @@ -1,15 +1,14 @@ import { + camel, ClientBuilder, ClientGeneratorsBuilder, ContextSpecs, + escape, + generateMutator, GeneratorDependency, GeneratorMutator, GeneratorOptions, GeneratorVerbOptions, - ZodCoerceType, - camel, - escape, - generateMutator, getNumberWord, isBoolean, isObject, @@ -17,6 +16,7 @@ import { jsStringEscape, pascal, resolveRef, + ZodCoerceType, } from '@orval/core'; import uniq from 'lodash.uniq'; import { @@ -460,11 +460,13 @@ const parseBodyAndResponse = ({ context, name, strict, + generate, }: { data: ResponseObject | RequestBodyObject | ReferenceObject | undefined; context: ContextSpecs; name: string; strict: boolean; + generate: boolean; }): { input: ZodValidationSchemaDefinition; isArray: boolean; @@ -473,7 +475,7 @@ const parseBodyAndResponse = ({ max?: number; }; } => { - if (!data) { + if (!data || !generate) { return { input: { functions: [], consts: [] }, isArray: false, @@ -545,6 +547,7 @@ const parseParameters = ({ context, operationName, strict, + generate, }: { data: (ParameterObject | ReferenceObject)[] | undefined; context: ContextSpecs; @@ -556,6 +559,13 @@ const parseParameters = ({ body: boolean; response: boolean; }; + generate: { + param: boolean; + query: boolean; + header: boolean; + body: boolean; + response: boolean; + }; }): { headers: ZodValidationSchemaDefinition; queryParams: ZodValidationSchemaDefinition; @@ -594,6 +604,12 @@ const parseParameters = ({ header: strict.header, }; + const mapGenerate = { + path: generate.param, + query: generate.query, + header: generate.header, + }; + const definition = generateZodValidationSchemaDefinition( schema, context, @@ -604,21 +620,21 @@ const parseParameters = ({ }, ); - if (parameter.in === 'header') { + if (parameter.in === 'header' && mapGenerate.header) { return { ...acc, headers: { ...acc.headers, [parameter.name]: definition }, }; } - if (parameter.in === 'query') { + if (parameter.in === 'query' && mapGenerate.query) { return { ...acc, queryParams: { ...acc.queryParams, [parameter.name]: definition }, }; } - if (parameter.in === 'path') { + if (parameter.in === 'path' && mapGenerate.path) { return { ...acc, params: { ...acc.params, [parameter.name]: definition }, @@ -691,12 +707,16 @@ const generateZodRoute = async ( | PathItemObject | undefined; - const parameters = spec?.[verb]?.parameters; + const parameters = spec?.[verb]?.parameters as ( + | ParameterObject + | ReferenceObject + )[]; const parsedParameters = parseParameters({ data: parameters, context, operationName, strict: override.zod.strict, + generate: override.zod.generate, }); const requestBody = spec?.[verb]?.requestBody; @@ -705,6 +725,7 @@ const generateZodRoute = async ( context, name: camel(`${operationName}-body`), strict: override.zod.strict.body, + generate: override.zod.generate.body, }); const responses = ( @@ -718,6 +739,7 @@ const generateZodRoute = async ( context, name: camel(`${operationName}-${code}-response`), strict: override.zod.strict.response, + generate: override.zod.generate.response, }), ); diff --git a/packages/zod/src/zod.test.ts b/packages/zod/src/zod.test.ts index a219db6f5..c04dcc6d8 100644 --- a/packages/zod/src/zod.test.ts +++ b/packages/zod/src/zod.test.ts @@ -1,11 +1,13 @@ import { describe, expect, it } from 'vitest'; import { - type ZodValidationSchemaDefinition, - parseZodValidationSchemaDefinition, + generateZod, generateZodValidationSchemaDefinition, + parseZodValidationSchemaDefinition, + type ZodValidationSchemaDefinition, } from '.'; import { SchemaObject } from 'openapi3-ts/oas30'; import { ContextSpecs } from '@orval/core'; +import * as fs from 'node:fs'; const queryParams: ZodValidationSchemaDefinition = { functions: [ @@ -340,3 +342,327 @@ describe('generateZodValidationSchemaDefinition`', () => { }); }); }); + +const basicApiSchema = { + pathRoute: '/cats', + context: { + specKey: 'cat', + specs: { + cat: { + paths: { + '/cats': { + post: { + operationId: 'xyz', + parameters: [ + { + name: 'id', + required: true, + in: 'path', + schema: { + type: 'string', + }, + }, + { + name: 'page', + required: false, + in: 'query', + schema: { + type: 'number', + }, + }, + { + name: 'x-header', + in: 'header', + required: true, + schema: { + type: 'string', + }, + }, + ], + requestBody: { + required: true, + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + name: { + type: 'string', + }, + }, + }, + }, + }, + }, + responses: { + '200': { + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + name: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + output: { + override: { + zod: { + generateEachHttpStatus: false, + }, + }, + }, + }, +}; +describe('generatePartOfSchemaGenerateZod', () => { + it('Default Config', async () => { + const result = await generateZod( + { + pathRoute: '/cats', + verb: 'post', + operationName: 'test', + override: { + zod: { + strict: { + param: false, + body: false, + response: false, + query: false, + header: false, + }, + generate: { + param: true, + body: true, + response: true, + query: true, + header: true, + }, + coerce: { + param: false, + body: false, + response: false, + query: false, + header: false, + }, + }, + }, + }, + basicApiSchema, + {}, + ); + + expect(result.implementation).toBe( + 'export const testParams = zod.object({\n "id": zod.string()\n})\n\nexport const testQueryParams = zod.object({\n "page": zod.number().optional()\n})\n\nexport const testHeader = zod.object({\n "x-header": zod.string()\n})\n\nexport const testBody = zod.object({\n "name": zod.string().optional()\n})\n\nexport const testResponse = zod.object({\n "name": zod.string().optional()\n})\n\n', + ); + }); + + it('Only generate response body', async () => { + const result = await generateZod( + { + pathRoute: '/cats', + verb: 'post', + operationName: 'test', + override: { + zod: { + strict: { + param: false, + body: false, + response: false, + query: false, + header: false, + }, + generate: { + param: false, + body: false, + response: true, + query: false, + header: false, + }, + coerce: { + param: false, + body: false, + response: false, + query: false, + header: false, + }, + }, + }, + }, + basicApiSchema, + {}, + ); + expect(result.implementation).toBe( + 'export const testResponse = zod.object({\n "name": zod.string().optional()\n})\n\n', + ); + }); + + it('Only generate request body', async () => { + const result = await generateZod( + { + pathRoute: '/cats', + verb: 'post', + operationName: 'test', + override: { + zod: { + strict: { + param: false, + body: false, + response: false, + query: false, + header: false, + }, + generate: { + param: false, + body: true, + response: false, + query: false, + header: false, + }, + coerce: { + param: false, + body: false, + response: false, + query: false, + header: false, + }, + }, + }, + }, + basicApiSchema, + {}, + ); + expect(result.implementation).toBe( + 'export const testBody = zod.object({\n "name": zod.string().optional()\n})\n\n', + ); + }); + + it('Only generate query params', async () => { + const result = await generateZod( + { + pathRoute: '/cats', + verb: 'post', + operationName: 'test', + override: { + zod: { + strict: { + param: false, + body: false, + response: false, + query: false, + header: false, + }, + generate: { + param: false, + body: false, + response: false, + query: true, + header: false, + }, + coerce: { + param: false, + body: false, + response: false, + query: false, + header: false, + }, + }, + }, + }, + basicApiSchema, + {}, + ); + expect(result.implementation).toBe( + 'export const testQueryParams = zod.object({\n "page": zod.number().optional()\n})\n\n', + ); + }); + + it('Only generate url params', async () => { + const result = await generateZod( + { + pathRoute: '/cats', + verb: 'post', + operationName: 'test', + override: { + zod: { + strict: { + param: false, + body: false, + response: false, + query: false, + header: false, + }, + generate: { + param: true, + body: false, + response: false, + query: false, + header: false, + }, + coerce: { + param: false, + body: false, + response: false, + query: false, + header: false, + }, + }, + }, + }, + basicApiSchema, + {}, + ); + expect(result.implementation).toBe( + 'export const testParams = zod.object({\n "id": zod.string()\n})\n\n', + ); + }); + + it('Only generate header', async () => { + const result = await generateZod( + { + pathRoute: '/cats', + verb: 'post', + operationName: 'test', + override: { + zod: { + strict: { + param: false, + body: false, + response: false, + query: false, + header: false, + }, + generate: { + param: false, + body: false, + response: false, + query: false, + header: true, + }, + coerce: { + param: false, + body: false, + response: false, + query: false, + header: false, + }, + }, + }, + }, + basicApiSchema, + {}, + ); + expect(result.implementation).toBe( + 'export const testHeader = zod.object({\n "x-header": zod.string()\n})\n\n', + ); + }); +});