From 2fd8ac79636b50f5584a8ab6486ed5780b2a7c71 Mon Sep 17 00:00:00 2001 From: MPiland Date: Thu, 5 Jun 2025 10:11:14 -0500 Subject: [PATCH 01/42] Update config.ts Added importOnly and importDiscriminator options --- src/config.ts | 46 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/src/config.ts b/src/config.ts index d6ec9f2b..59bc6f68 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,7 +1,7 @@ import type { TypeScriptPluginConfig } from '@graphql-codegen/typescript'; import type { NamingConventionMap } from '@graphql-codegen/visitor-plugin-common'; -export type ValidationSchema = 'yup' | 'zod' | 'myzod' | 'valibot'; +export type ValidationSchema = 'yup' | 'zod' | 'zod/v4' | 'myzod' | 'valibot'; export type ValidationSchemaExportType = 'function' | 'const'; export interface DirectiveConfig { @@ -96,6 +96,50 @@ export interface ValidationSchemaPluginConfig extends TypeScriptPluginConfig { * ``` */ useTypeImports?: boolean + /** + * @description Creates schemas for input types only. + * This gives compatibility with TypeScript's "importsNotUsedAsValues": "error" option + * Should used in conjunction with `importFrom` option. + * @default false + * + * @exampleMarkdown + * ```yml + * generates: + * path/to/types.ts: + * plugins: + * - typescript + * path/to/schemas.ts: + * plugins: + * - graphql-codegen-validation-schema + * config: + * schema: yup + * importFrom: ./path/to/types + * inputsOnly: true + * ``` + */ + inputOnly?: boolean; + /** + * @description Creates schemas for input types only. + * This gives compatibility with TypeScript's "importsNotUsedAsValues": "error" option + * Should used in conjunction with `importFrom` option. + * @default false + * + * @exampleMarkdown + * ```yml + * generates: + * path/to/types.ts: + * plugins: + * - typescript + * path/to/schemas.ts: + * plugins: + * - graphql-codegen-validation-schema + * config: + * schema: yup + * importFrom: ./path/to/types + * inputsOnly: true + * ``` + */ + inputDiscriminator?: string; /** * @description Prefixes all import types from generated typescript type. * @default "" From 0cbb5188daa2ceb5112759403096871201f3b18b Mon Sep 17 00:00:00 2001 From: MPiland Date: Thu, 5 Jun 2025 10:13:54 -0500 Subject: [PATCH 02/42] Update index.ts Added discriminator in buildInputFields to handle inputDiscriminator config option Added condition for object types that inputOnly config option is false --- src/zod/index.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/zod/index.ts b/src/zod/index.ts index fc77e16d..9cd32042 100644 --- a/src/zod/index.ts +++ b/src/zod/index.ts @@ -125,7 +125,7 @@ export class ZodSchemaVisitor extends BaseSchemaVisitor { get ObjectTypeDefinition() { return { - leave: ObjectTypeDefinitionBuilder(this.config.withObjectType, (node: ObjectTypeDefinitionNode) => { + leave: ObjectTypeDefinitionBuilder(this.config.withObjectType && !this.config.inputOnly, (node: ObjectTypeDefinitionNode) => { const visitor = this.createVisitor('output'); const name = visitor.convertName(node.name.value); const typeName = visitor.prefixTypeNamespace(name); @@ -253,6 +253,8 @@ export class ZodSchemaVisitor extends BaseSchemaVisitor { name: string, ) { const typeName = visitor.prefixTypeNamespace(name); + const discriminator = + this.config.inputDiscriminator ? `${this.config.inputDiscriminator}: z.literal('${name}',\n)` : '' const shape = fields.map(field => generateFieldZodSchema(this.config, visitor, field, 2)).join(',\n'); switch (this.config.validationSchemaExportType) { @@ -261,7 +263,7 @@ export class ZodSchemaVisitor extends BaseSchemaVisitor { .export() .asKind('const') .withName(`${name}Schema: z.ZodObject>`) - .withContent(['z.object({', shape, '})'].join('\n')) + .withContent(['z.object({', discriminator, shape, '})'].join('\n')) .string; case 'function': @@ -270,7 +272,7 @@ export class ZodSchemaVisitor extends BaseSchemaVisitor { .export() .asKind('function') .withName(`${name}Schema(): z.ZodObject>`) - .withBlock([indent(`return z.object({`), shape, indent('})')].join('\n')) + .withBlock([indent(`return z.object({`), discriminator, shape, indent('})')].join('\n')) .string; } } From f50745be888656b2827e329ad4ac6c7650ea152c Mon Sep 17 00:00:00 2001 From: MPiland Date: Thu, 5 Jun 2025 10:15:04 -0500 Subject: [PATCH 03/42] Update index.ts --- src/yup/index.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/yup/index.ts b/src/yup/index.ts index 7370e9ea..db62fe08 100644 --- a/src/yup/index.ts +++ b/src/yup/index.ts @@ -117,7 +117,7 @@ export class YupSchemaVisitor extends BaseSchemaVisitor { get ObjectTypeDefinition() { return { - leave: ObjectTypeDefinitionBuilder(this.config.withObjectType, (node: ObjectTypeDefinitionNode) => { + leave: ObjectTypeDefinitionBuilder(this.config.withObjectType && !this.config.inputOnly, (node: ObjectTypeDefinitionNode) => { const visitor = this.createVisitor('output'); const name = visitor.convertName(node.name.value); const typeName = visitor.prefixTypeNamespace(name); @@ -256,6 +256,8 @@ export class YupSchemaVisitor extends BaseSchemaVisitor { name: string, ) { const typeName = visitor.prefixTypeNamespace(name); + const discriminator = + this.config.inputDiscriminator ? `${this.config.inputDiscriminator}: z.literal('${name}',\n)` : '' const shape = shapeFields(fields, this.config, visitor); switch (this.config.validationSchemaExportType) { @@ -264,7 +266,7 @@ export class YupSchemaVisitor extends BaseSchemaVisitor { .export() .asKind('const') .withName(`${name}Schema: yup.ObjectSchema<${typeName}>`) - .withContent(['yup.object({', shape, '})'].join('\n')) + .withContent(['yup.object({', discriminator, shape, '})'].join('\n')) .string; case 'function': @@ -273,7 +275,7 @@ export class YupSchemaVisitor extends BaseSchemaVisitor { .export() .asKind('function') .withName(`${name}Schema(): yup.ObjectSchema<${typeName}>`) - .withBlock([indent(`return yup.object({`), shape, indent('})')].join('\n')) + .withBlock([indent(`return yup.object({`), discriminator, shape, indent('})')].join('\n')) .string; } } From f50f8698328996949e07d69f31249e514e45ea4e Mon Sep 17 00:00:00 2001 From: MPiland Date: Thu, 5 Jun 2025 10:16:15 -0500 Subject: [PATCH 04/42] Update index.ts --- src/valibot/index.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/valibot/index.ts b/src/valibot/index.ts index 7b9671ba..4b418639 100644 --- a/src/valibot/index.ts +++ b/src/valibot/index.ts @@ -85,7 +85,7 @@ export class ValibotSchemaVisitor extends BaseSchemaVisitor { get ObjectTypeDefinition() { return { - leave: ObjectTypeDefinitionBuilder(this.config.withObjectType, (node: ObjectTypeDefinitionNode) => { + leave: ObjectTypeDefinitionBuilder(this.config.withObjectType && !this.config.inputOnly, (node: ObjectTypeDefinitionNode) => { const visitor = this.createVisitor('output'); const name = visitor.convertName(node.name.value); const typeName = visitor.prefixTypeNamespace(name); @@ -189,6 +189,8 @@ export class ValibotSchemaVisitor extends BaseSchemaVisitor { name: string, ) { const typeName = visitor.prefixTypeNamespace(name); + const discriminator = + this.config.inputDiscriminator ? `${this.config.inputDiscriminator}: z.literal('${name}',\n)` : '' const shape = fields.map(field => generateFieldValibotSchema(this.config, visitor, field, 2)).join(',\n'); switch (this.config.validationSchemaExportType) { @@ -197,7 +199,7 @@ export class ValibotSchemaVisitor extends BaseSchemaVisitor { .export() .asKind('function') .withName(`${name}Schema(): v.GenericSchema<${typeName}>`) - .withBlock([indent(`return v.object({`), shape, indent('})')].join('\n')) + .withBlock([indent(`return v.object({`), discriminator, shape, indent('})')].join('\n')) .string; } } From 09c38fabf505de0a759d1394769beb90ef594c42 Mon Sep 17 00:00:00 2001 From: MPiland Date: Thu, 5 Jun 2025 10:17:08 -0500 Subject: [PATCH 05/42] Update index.ts --- src/myzod/index.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/myzod/index.ts b/src/myzod/index.ts index 4a9e5778..eb9e964a 100644 --- a/src/myzod/index.ts +++ b/src/myzod/index.ts @@ -105,7 +105,7 @@ export class MyZodSchemaVisitor extends BaseSchemaVisitor { get ObjectTypeDefinition() { return { - leave: ObjectTypeDefinitionBuilder(this.config.withObjectType, (node: ObjectTypeDefinitionNode) => { + leave: ObjectTypeDefinitionBuilder(this.config.withObjectType && !this.config.inputOnly, (node: ObjectTypeDefinitionNode) => { const visitor = this.createVisitor('output'); const name = visitor.convertName(node.name.value); const typeName = visitor.prefixTypeNamespace(name); @@ -237,6 +237,8 @@ export class MyZodSchemaVisitor extends BaseSchemaVisitor { name: string, ) { const typeName = visitor.prefixTypeNamespace(name); + const discriminator = + this.config.inputDiscriminator ? `${this.config.inputDiscriminator}: z.literal('${name}',\n)` : '' const shape = fields.map(field => generateFieldMyZodSchema(this.config, visitor, field, 2)).join(',\n'); switch (this.config.validationSchemaExportType) { @@ -245,7 +247,7 @@ export class MyZodSchemaVisitor extends BaseSchemaVisitor { .export() .asKind('const') .withName(`${name}Schema: myzod.Type<${typeName}>`) - .withContent(['myzod.object({', shape, '})'].join('\n')) + .withContent(['myzod.object({', discriminator, shape, '})'].join('\n')) .string; case 'function': @@ -254,7 +256,7 @@ export class MyZodSchemaVisitor extends BaseSchemaVisitor { .export() .asKind('function') .withName(`${name}Schema(): myzod.Type<${typeName}>`) - .withBlock([indent(`return myzod.object({`), shape, indent('})')].join('\n')) + .withBlock([indent(`return myzod.object({`), discriminator, shape, indent('})')].join('\n')) .string; } } From 6f84a6b96e3a81c60e25995ae7d4c5f4fedd6934 Mon Sep 17 00:00:00 2001 From: MPiland Date: Thu, 5 Jun 2025 10:18:19 -0500 Subject: [PATCH 06/42] Create index.ts --- src/zodv4/index.ts | 1 + 1 file changed, 1 insertion(+) create mode 100644 src/zodv4/index.ts diff --git a/src/zodv4/index.ts b/src/zodv4/index.ts new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/src/zodv4/index.ts @@ -0,0 +1 @@ + From 5bb4905e27b2299025697793d8eb94b6826cbe96 Mon Sep 17 00:00:00 2001 From: MPiland Date: Thu, 5 Jun 2025 10:19:26 -0500 Subject: [PATCH 07/42] Update index.ts --- src/zodv4/index.ts | 388 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 388 insertions(+) diff --git a/src/zodv4/index.ts b/src/zodv4/index.ts index 8b137891..d8d51c12 100644 --- a/src/zodv4/index.ts +++ b/src/zodv4/index.ts @@ -1 +1,389 @@ +import type { + EnumTypeDefinitionNode, + FieldDefinitionNode, + GraphQLSchema, + InputObjectTypeDefinitionNode, + InputValueDefinitionNode, + InterfaceTypeDefinitionNode, + NameNode, + ObjectTypeDefinitionNode, + TypeNode, + UnionTypeDefinitionNode, +} from 'graphql'; + +import type { ValidationSchemaPluginConfig } from '../config.js'; +import type { Visitor } from '../visitor.js'; +import { resolveExternalModuleAndFn } from '@graphql-codegen/plugin-helpers'; +import { convertNameParts, DeclarationBlock, indent } from '@graphql-codegen/visitor-plugin-common'; +import { + Kind, +} from 'graphql'; +import { buildApi, formatDirectiveConfig } from '../directive.js'; +import { + escapeGraphQLCharacters, + InterfaceTypeDefinitionBuilder, + isInput, + isListType, + isNamedType, + isNonNullType, + ObjectTypeDefinitionBuilder, +} from '../graphql.js'; +import { BaseSchemaVisitor } from '../schema_visitor.js'; + +export class ZodSchemaVisitor extends BaseSchemaVisitor { + constructor(schema: GraphQLSchema, config: ValidationSchemaPluginConfig) { + super(schema, config); + } + + importValidationSchema(): string { + return `import { z } from 'zod/v4'`; + } + + initialEmit(): string { + return ( + `\n${ + [ + new DeclarationBlock({}) + .asKind('type') + .withName('Properties') + .withContent(['Required<{', ' [K in keyof T]: z.ZodType;', '}>'].join('\n')) + .string, + ...this.enumDeclarations, + ].join('\n')}` + ); + } + + get InputObjectTypeDefinition() { + return { + leave: (node: InputObjectTypeDefinitionNode) => { + const visitor = this.createVisitor('input'); + const name = visitor.convertName(node.name.value); + this.importTypes.push(name); + return this.buildInputFields(node.fields ?? [], visitor, name); + }, + }; + } + + get InterfaceTypeDefinition() { + return { + leave: InterfaceTypeDefinitionBuilder(this.config.withObjectType, (node: InterfaceTypeDefinitionNode) => { + const visitor = this.createVisitor('output'); + const name = visitor.convertName(node.name.value); + const typeName = visitor.prefixTypeNamespace(name); + this.importTypes.push(name); + + // Building schema for field arguments. + const argumentBlocks = this.buildTypeDefinitionArguments(node, visitor); + const appendArguments = argumentBlocks ? `\n${argumentBlocks}` : ''; + + // Building schema for fields. + const shape = node.fields?.map(field => generateFieldZodSchema(this.config, visitor, field, 2)).join(',\n'); + + switch (this.config.validationSchemaExportType) { + case 'const': + return ( + new DeclarationBlock({}) + .export() + .asKind('const') + .withName(`${name}Schema: z.ZodObject>`) + .withContent([`z.object({`, shape, '})'].join('\n')) + .string + appendArguments + ); + + case 'function': + default: + return ( + new DeclarationBlock({}) + .export() + .asKind('function') + .withName(`${name}Schema(): z.ZodObject>`) + .withBlock([indent(`return z.object({`), shape, indent('})')].join('\n')) + .string + appendArguments + ); + } + }), + }; + } + + get ObjectTypeDefinition() { + return { + leave: ObjectTypeDefinitionBuilder(this.config.withObjectType && !this.config.inputOnly, (node: ObjectTypeDefinitionNode) => { + const visitor = this.createVisitor('output'); + const name = visitor.convertName(node.name.value); + const typeName = visitor.prefixTypeNamespace(name); + this.importTypes.push(name); + + // Building schema for field arguments. + const argumentBlocks = this.buildTypeDefinitionArguments(node, visitor); + const appendArguments = argumentBlocks ? `\n${argumentBlocks}` : ''; + + // Building schema for fields. + const shape = node.fields?.map(field => generateFieldZodSchema(this.config, visitor, field, 2)).join(',\n'); + + switch (this.config.validationSchemaExportType) { + case 'const': + return ( + new DeclarationBlock({}) + .export() + .asKind('const') + .withName(`${name}Schema: z.ZodObject>`) + .withContent( + [ + `z.object({`, + indent(`__typename: z.literal('${node.name.value}').optional(),`, 2), + shape, + '})', + ].join('\n'), + ) + .string + appendArguments + ); + + case 'function': + default: + return ( + new DeclarationBlock({}) + .export() + .asKind('function') + .withName(`${name}Schema(): z.ZodObject>`) + .withBlock( + [ + indent(`return z.object({`), + indent(`__typename: z.literal('${node.name.value}').optional(),`, 2), + shape, + indent('})'), + ].join('\n'), + ) + .string + appendArguments + ); + } + }), + }; + } + + get EnumTypeDefinition() { + return { + leave: (node: EnumTypeDefinitionNode) => { + const visitor = this.createVisitor('both'); + const enumname = visitor.convertName(node.name.value); + const enumTypeName = visitor.prefixTypeNamespace(enumname); + this.importTypes.push(enumname); + + // hoist enum declarations + this.enumDeclarations.push( + this.config.enumsAsTypes + ? new DeclarationBlock({}) + .export() + .asKind('const') + .withName(`${enumname}Schema`) + .withContent(`z.enum([${node.values?.map(enumOption => `'${enumOption.name.value}'`).join(', ')}])`) + .string + : new DeclarationBlock({}) + .export() + .asKind('const') + .withName(`${enumname}Schema`) + .withContent(`z.enum(${enumTypeName})`) + .string, + ); + }, + }; + } + + get UnionTypeDefinition() { + return { + leave: (node: UnionTypeDefinitionNode) => { + if (!node.types || !this.config.withObjectType) + return; + const visitor = this.createVisitor('output'); + const unionName = visitor.convertName(node.name.value); + const unionElements = node.types.map((t) => { + const element = visitor.convertName(t.name.value); + const typ = visitor.getType(t.name.value); + if (typ?.astNode?.kind === 'EnumTypeDefinition') + return `${element}Schema`; + + switch (this.config.validationSchemaExportType) { + case 'const': + return `${element}Schema`; + case 'function': + default: + return `${element}Schema()`; + } + }).join(', '); + const unionElementsCount = node.types.length ?? 0; + + const union = unionElementsCount > 1 ? `z.union([${unionElements}])` : unionElements; + + switch (this.config.validationSchemaExportType) { + case 'const': + return new DeclarationBlock({}).export().asKind('const').withName(`${unionName}Schema`).withContent(union).string; + case 'function': + default: + return new DeclarationBlock({}) + .export() + .asKind('function') + .withName(`${unionName}Schema()`) + .withBlock(indent(`return ${union}`)) + .string; + } + }, + }; + } + + protected buildInputFields( + fields: readonly (FieldDefinitionNode | InputValueDefinitionNode)[], + visitor: Visitor, + name: string, + ) { + const typeName = visitor.prefixTypeNamespace(name); + const discriminator = + this.config.inputDiscriminator ? `${this.config.inputDiscriminator}: z.literal('${name}',\n)` : '' + const shape = fields.map(field => generateFieldZodSchema(this.config, visitor, field, 2)).join(',\n'); + + switch (this.config.validationSchemaExportType) { + case 'const': + return new DeclarationBlock({}) + .export() + .asKind('const') + .withName(`${name}Schema: z.ZodObject>`) + .withContent(['z.object({', discriminator, shape, '})'].join('\n')) + .string; + + case 'function': + default: + return new DeclarationBlock({}) + .export() + .asKind('function') + .withName(`${name}Schema(): z.ZodObject>`) + .withBlock([indent(`return z.object({`), discriminator, shape, indent('})')].join('\n')) + .string; + } + } +} + +function generateFieldZodSchema(config: ValidationSchemaPluginConfig, visitor: Visitor, field: InputValueDefinitionNode | FieldDefinitionNode, indentCount: number): string { + const gen = generateFieldTypeZodSchema(config, visitor, field, field.type); + return indent(`${field.name.value}: ${maybeLazy(field.type, gen)}`, indentCount); +} + +function generateFieldTypeZodSchema(config: ValidationSchemaPluginConfig, visitor: Visitor, field: InputValueDefinitionNode | FieldDefinitionNode, type: TypeNode, parentType?: TypeNode): string { + if (isListType(type)) { + const gen = generateFieldTypeZodSchema(config, visitor, field, type.type, type); + if (!isNonNullType(parentType)) { + const arrayGen = `z.array(${maybeLazy(type.type, gen)})`; + const maybeLazyGen = applyDirectives(config, field, arrayGen); + return `${maybeLazyGen}.nullish()`; + } + return `z.array(${maybeLazy(type.type, gen)})`; + } + if (isNonNullType(type)) { + const gen = generateFieldTypeZodSchema(config, visitor, field, type.type, type); + return maybeLazy(type.type, gen); + } + if (isNamedType(type)) { + const gen = generateNameNodeZodSchema(config, visitor, type.name); + if (isListType(parentType)) + return `${gen}.nullable()`; + + let appliedDirectivesGen = applyDirectives(config, field, gen); + + if (field.kind === Kind.INPUT_VALUE_DEFINITION) { + const { defaultValue } = field; + + if (defaultValue?.kind === Kind.INT || defaultValue?.kind === Kind.FLOAT || defaultValue?.kind === Kind.BOOLEAN) + appliedDirectivesGen = `${appliedDirectivesGen}.default(${defaultValue.value})`; + + if (defaultValue?.kind === Kind.STRING || defaultValue?.kind === Kind.ENUM) { + if (config.useEnumTypeAsDefaultValue && defaultValue?.kind !== Kind.STRING) { + let value = convertNameParts(defaultValue.value, resolveExternalModuleAndFn('change-case-all#pascalCase'), config.namingConvention?.transformUnderscore); + + if (config.namingConvention?.enumValues) + value = convertNameParts(defaultValue.value, resolveExternalModuleAndFn(config.namingConvention?.enumValues), config.namingConvention?.transformUnderscore); + + appliedDirectivesGen = `${appliedDirectivesGen}.default(${type.name.value}.${value})`; + } + else { + appliedDirectivesGen = `${appliedDirectivesGen}.default("${escapeGraphQLCharacters(defaultValue.value)}")`; + } + } + } + + if (isNonNullType(parentType)) { + if (visitor.shouldEmitAsNotAllowEmptyString(type.name.value)) + return `${appliedDirectivesGen}.min(1)`; + + return appliedDirectivesGen; + } + if (isListType(parentType)) + return `${appliedDirectivesGen}.nullable()`; + + return `${appliedDirectivesGen}.nullish()`; + } + console.warn('unhandled type:', type); + return ''; +} + +function applyDirectives(config: ValidationSchemaPluginConfig, field: InputValueDefinitionNode | FieldDefinitionNode, gen: string): string { + if (config.directives && field.directives) { + const formatted = formatDirectiveConfig(config.directives); + return gen + buildApi(formatted, field.directives); + } + return gen; +} + +function generateNameNodeZodSchema(config: ValidationSchemaPluginConfig, visitor: Visitor, node: NameNode): string { + const converter = visitor.getNameNodeConverter(node); + + switch (converter?.targetKind) { + case 'InterfaceTypeDefinition': + case 'InputObjectTypeDefinition': + case 'ObjectTypeDefinition': + case 'UnionTypeDefinition': + // using switch-case rather than if-else to allow for future expansion + switch (config.validationSchemaExportType) { + case 'const': + return `${converter.convertName()}Schema`; + case 'function': + default: + return `${converter.convertName()}Schema()`; + } + case 'EnumTypeDefinition': + return `${converter.convertName()}Schema`; + case 'ScalarTypeDefinition': + return zod4Scalar(config, visitor, node.value); + default: + if (converter?.targetKind) + console.warn('Unknown targetKind', converter?.targetKind); + + return zod4Scalar(config, visitor, node.value); + } +} + +function maybeLazy(type: TypeNode, schema: string): string { + if (isNamedType(type) && isInput(type.name.value)) + return `z.lazy(() => ${schema})`; + + return schema; +} + +function zod4Scalar(config: ValidationSchemaPluginConfig, visitor: Visitor, scalarName: string): string { + if (config.scalarSchemas?.[scalarName]) + return config.scalarSchemas[scalarName]; + + const tsType = visitor.getScalarType(scalarName); + switch (tsType) { + case 'string': + return `z.string()`; + case 'number': + return `z.number()`; + case 'boolean': + return `z.boolean()`; + } + + if (config.defaultScalarTypeSchema) { + return config.defaultScalarTypeSchema; + } + + console.warn('unhandled scalar name:', scalarName); + return 'z.any()'; +} From b587b83d932dd4aaad3086f15098ab072f7958ef Mon Sep 17 00:00:00 2001 From: MPiland Date: Thu, 5 Jun 2025 10:25:54 -0500 Subject: [PATCH 08/42] Update index.ts --- src/zodv4/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/zodv4/index.ts b/src/zodv4/index.ts index d8d51c12..39c8d3c1 100644 --- a/src/zodv4/index.ts +++ b/src/zodv4/index.ts @@ -237,7 +237,7 @@ export class ZodSchemaVisitor extends BaseSchemaVisitor { ) { const typeName = visitor.prefixTypeNamespace(name); const discriminator = - this.config.inputDiscriminator ? `${this.config.inputDiscriminator}: z.literal('${name}',\n)` : '' + this.config.inputDiscriminator ? `${this.config.inputDiscriminator}: z.literal('${name}'),\n` : '' const shape = fields.map(field => generateFieldZodSchema(this.config, visitor, field, 2)).join(',\n'); switch (this.config.validationSchemaExportType) { From 8fe54e9a4c057b26d88e76e78d106df5b53b632b Mon Sep 17 00:00:00 2001 From: MPiland Date: Thu, 5 Jun 2025 10:26:55 -0500 Subject: [PATCH 09/42] Update index.ts --- src/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 39dd1e8a..565aaac2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,12 +4,12 @@ import type { ValidationSchemaPluginConfig } from './config.js'; import type { SchemaVisitor } from './types.js'; import { transformSchemaAST } from '@graphql-codegen/schema-ast'; import { buildSchema, printSchema, visit } from 'graphql'; - import { isGeneratedByIntrospection, topologicalSortAST } from './graphql.js'; import { MyZodSchemaVisitor } from './myzod/index.js'; import { ValibotSchemaVisitor } from './valibot/index.js'; import { YupSchemaVisitor } from './yup/index.js'; import { ZodSchemaVisitor } from './zod/index.js'; +import {Zodv4SchemaVisitor} from './zodv4/index.js'; export const plugin: PluginFunction = ( schema: GraphQLSchema, @@ -32,6 +32,8 @@ export const plugin: PluginFunction Date: Thu, 5 Jun 2025 10:27:42 -0500 Subject: [PATCH 10/42] Update index.ts --- src/zodv4/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/zodv4/index.ts b/src/zodv4/index.ts index 39c8d3c1..68ff0846 100644 --- a/src/zodv4/index.ts +++ b/src/zodv4/index.ts @@ -31,7 +31,7 @@ import { } from '../graphql.js'; import { BaseSchemaVisitor } from '../schema_visitor.js'; -export class ZodSchemaVisitor extends BaseSchemaVisitor { +export class Zodv4SchemaVisitor extends BaseSchemaVisitor { constructor(schema: GraphQLSchema, config: ValidationSchemaPluginConfig) { super(schema, config); } From a5c92bbe40e5635704a0773de92f4831d55e450b Mon Sep 17 00:00:00 2001 From: MPiland Date: Thu, 5 Jun 2025 10:28:13 -0500 Subject: [PATCH 11/42] Update index.ts --- src/zod/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/zod/index.ts b/src/zod/index.ts index 9cd32042..f4070c23 100644 --- a/src/zod/index.ts +++ b/src/zod/index.ts @@ -253,8 +253,8 @@ export class ZodSchemaVisitor extends BaseSchemaVisitor { name: string, ) { const typeName = visitor.prefixTypeNamespace(name); - const discriminator = - this.config.inputDiscriminator ? `${this.config.inputDiscriminator}: z.literal('${name}',\n)` : '' + const discriminator = + this.config.inputDiscriminator ? `${this.config.inputDiscriminator}: z.literal('${name}'),\n` : '' const shape = fields.map(field => generateFieldZodSchema(this.config, visitor, field, 2)).join(',\n'); switch (this.config.validationSchemaExportType) { From e113b8da65b04862456cd75abfa1a0727ad597c6 Mon Sep 17 00:00:00 2001 From: MPiland Date: Thu, 5 Jun 2025 10:35:48 -0500 Subject: [PATCH 12/42] Update index.ts --- src/yup/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yup/index.ts b/src/yup/index.ts index db62fe08..94580a5f 100644 --- a/src/yup/index.ts +++ b/src/yup/index.ts @@ -257,7 +257,7 @@ export class YupSchemaVisitor extends BaseSchemaVisitor { ) { const typeName = visitor.prefixTypeNamespace(name); const discriminator = - this.config.inputDiscriminator ? `${this.config.inputDiscriminator}: z.literal('${name}',\n)` : '' + this.config.inputDiscriminator ? `${this.config.inputDiscriminator}: z.literal('${name}'),\n` : '' const shape = shapeFields(fields, this.config, visitor); switch (this.config.validationSchemaExportType) { From 877ecda50038ae420196b934f1408b8447738916 Mon Sep 17 00:00:00 2001 From: MPiland Date: Thu, 5 Jun 2025 10:36:11 -0500 Subject: [PATCH 13/42] Update index.ts --- src/valibot/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/valibot/index.ts b/src/valibot/index.ts index 4b418639..53b2f3f2 100644 --- a/src/valibot/index.ts +++ b/src/valibot/index.ts @@ -190,7 +190,7 @@ export class ValibotSchemaVisitor extends BaseSchemaVisitor { ) { const typeName = visitor.prefixTypeNamespace(name); const discriminator = - this.config.inputDiscriminator ? `${this.config.inputDiscriminator}: z.literal('${name}',\n)` : '' + this.config.inputDiscriminator ? `${this.config.inputDiscriminator}: z.literal('${name}'),\n` : '' const shape = fields.map(field => generateFieldValibotSchema(this.config, visitor, field, 2)).join(',\n'); switch (this.config.validationSchemaExportType) { From a79e9432fdcda7411a0e3342b2c5c61dffef6c2d Mon Sep 17 00:00:00 2001 From: MPiland Date: Thu, 5 Jun 2025 10:36:33 -0500 Subject: [PATCH 14/42] Update index.ts --- src/myzod/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/myzod/index.ts b/src/myzod/index.ts index eb9e964a..1bc15e06 100644 --- a/src/myzod/index.ts +++ b/src/myzod/index.ts @@ -238,7 +238,7 @@ export class MyZodSchemaVisitor extends BaseSchemaVisitor { ) { const typeName = visitor.prefixTypeNamespace(name); const discriminator = - this.config.inputDiscriminator ? `${this.config.inputDiscriminator}: z.literal('${name}',\n)` : '' + this.config.inputDiscriminator ? `${this.config.inputDiscriminator}: z.literal('${name}'),\n` : '' const shape = fields.map(field => generateFieldMyZodSchema(this.config, visitor, field, 2)).join(',\n'); switch (this.config.validationSchemaExportType) { From 66db8ef1f72896a0f1f2b920f716fdb64fe185c6 Mon Sep 17 00:00:00 2001 From: MPiland Date: Thu, 5 Jun 2025 11:12:39 -0500 Subject: [PATCH 15/42] Update config.ts --- src/config.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/config.ts b/src/config.ts index 59bc6f68..08012864 100644 --- a/src/config.ts +++ b/src/config.ts @@ -2,6 +2,7 @@ import type { TypeScriptPluginConfig } from '@graphql-codegen/typescript'; import type { NamingConventionMap } from '@graphql-codegen/visitor-plugin-common'; export type ValidationSchema = 'yup' | 'zod' | 'zod/v4' | 'myzod' | 'valibot'; +export type LazyType = 'always' | 'circular' export type ValidationSchemaExportType = 'function' | 'const'; export interface DirectiveConfig { @@ -35,6 +36,23 @@ export interface ValidationSchemaPluginConfig extends TypeScriptPluginConfig { * ``` */ schema?: ValidationSchema + /** + * @description specify generate schema + * @default yup + * + * @exampleMarkdown + * ```yml + * generates: + * path/to/file.ts: + * plugins: + * - typescript + * - graphql-codegen-validation-schema + * config: + * schema: yup + * lazy: always + * ``` + */ + lazy?: LazyType; /** * @description import types from generated typescript type path * if not given, omit import statement. From fe44e91e608e6abd5806b76637d8461ac2327600 Mon Sep 17 00:00:00 2001 From: MPiland Date: Thu, 5 Jun 2025 11:17:23 -0500 Subject: [PATCH 16/42] Update config.ts --- src/config.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/config.ts b/src/config.ts index 08012864..402c119e 100644 --- a/src/config.ts +++ b/src/config.ts @@ -2,7 +2,7 @@ import type { TypeScriptPluginConfig } from '@graphql-codegen/typescript'; import type { NamingConventionMap } from '@graphql-codegen/visitor-plugin-common'; export type ValidationSchema = 'yup' | 'zod' | 'zod/v4' | 'myzod' | 'valibot'; -export type LazyType = 'always' | 'circular' +export type LazyType = 'all' | 'circular' export type ValidationSchemaExportType = 'function' | 'const'; export interface DirectiveConfig { @@ -37,8 +37,8 @@ export interface ValidationSchemaPluginConfig extends TypeScriptPluginConfig { */ schema?: ValidationSchema /** - * @description specify generate schema - * @default yup + * @description Setting to determine when to set a property to lazy. 'Circular' will only use lazy for circular references. 'All' will set lazy for all properties referencing another schema. + * @default all * * @exampleMarkdown * ```yml @@ -49,7 +49,7 @@ export interface ValidationSchemaPluginConfig extends TypeScriptPluginConfig { * - graphql-codegen-validation-schema * config: * schema: yup - * lazy: always + * lazy: circular * ``` */ lazy?: LazyType; From c0923731150a3e9752c541a1522b6bd9608162d3 Mon Sep 17 00:00:00 2001 From: MPiland Date: Thu, 5 Jun 2025 11:32:12 -0500 Subject: [PATCH 17/42] Update index.ts --- src/zodv4/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/zodv4/index.ts b/src/zodv4/index.ts index 68ff0846..215512e0 100644 --- a/src/zodv4/index.ts +++ b/src/zodv4/index.ts @@ -34,6 +34,7 @@ import { BaseSchemaVisitor } from '../schema_visitor.js'; export class Zodv4SchemaVisitor extends BaseSchemaVisitor { constructor(schema: GraphQLSchema, config: ValidationSchemaPluginConfig) { super(schema, config); + this.generationStack = new Set() } importValidationSchema(): string { From d032a5156df0c5fd65b10b265551c4c75a0f7ec8 Mon Sep 17 00:00:00 2001 From: MPiland Date: Thu, 5 Jun 2025 11:36:52 -0500 Subject: [PATCH 18/42] Update config.ts --- src/config.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/config.ts b/src/config.ts index 402c119e..0e1d669b 100644 --- a/src/config.ts +++ b/src/config.ts @@ -2,7 +2,7 @@ import type { TypeScriptPluginConfig } from '@graphql-codegen/typescript'; import type { NamingConventionMap } from '@graphql-codegen/visitor-plugin-common'; export type ValidationSchema = 'yup' | 'zod' | 'zod/v4' | 'myzod' | 'valibot'; -export type LazyType = 'all' | 'circular' +export type LazyStrategy = 'all' | 'circular' export type ValidationSchemaExportType = 'function' | 'const'; export interface DirectiveConfig { @@ -52,7 +52,7 @@ export interface ValidationSchemaPluginConfig extends TypeScriptPluginConfig { * lazy: circular * ``` */ - lazy?: LazyType; + lazyStrategy?: LazyStrategy; /** * @description import types from generated typescript type path * if not given, omit import statement. From ece2753c492022c2855a67ff1225fa3b92bd3be2 Mon Sep 17 00:00:00 2001 From: MPiland Date: Thu, 5 Jun 2025 11:37:35 -0500 Subject: [PATCH 19/42] Create utils.ts --- src/utils.ts | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 src/utils.ts diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 00000000..44a928a8 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,39 @@ +import { getNamedType, GraphQLNamedType, isObjectType, isInputObjectType } from 'graphql'; + +export function findCircularTypes(schema: GraphQLSchema): Set { + const circular = new Set(); + const visited = new Set(); + const stack = new Set(); + + function visit(typeName: string) { + if (stack.has(typeName)) { + circular.add(typeName); + return; + } + if (visited.has(typeName)) return; + visited.add(typeName); + stack.add(typeName); + + const type = schema.getType(typeName); + if (!type || !(isObjectType(type) || isInputObjectType(type))) { + stack.delete(typeName); + return; + } + + const fields = type.getFields(); + for (const field of Object.values(fields)) { + const fieldType = getNamedType(field.type); + visit(fieldType.name); + } + + stack.delete(typeName); + } + + for (const type of Object.values(schema.getTypeMap())) { + if (!type.name.startsWith('__')) { + visit(type.name); + } + } + + return circular; +} From dc7b5b8dcbbc18bb81161e05ced083a64498a2a0 Mon Sep 17 00:00:00 2001 From: MPiland Date: Thu, 5 Jun 2025 11:38:37 -0500 Subject: [PATCH 20/42] Update index.ts --- src/zodv4/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/zodv4/index.ts b/src/zodv4/index.ts index 215512e0..331d5d62 100644 --- a/src/zodv4/index.ts +++ b/src/zodv4/index.ts @@ -30,8 +30,10 @@ import { ObjectTypeDefinitionBuilder, } from '../graphql.js'; import { BaseSchemaVisitor } from '../schema_visitor.js'; +import { findCircularTypes } from '../utils.js' export class Zodv4SchemaVisitor extends BaseSchemaVisitor { + private circularTypes: Set constructor(schema: GraphQLSchema, config: ValidationSchemaPluginConfig) { super(schema, config); this.generationStack = new Set() From 889fe16103564524518ddd790bff039f9f443332 Mon Sep 17 00:00:00 2001 From: MPiland Date: Thu, 5 Jun 2025 11:40:04 -0500 Subject: [PATCH 21/42] Update index.ts --- src/zodv4/index.ts | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/zodv4/index.ts b/src/zodv4/index.ts index 331d5d62..10dd87c5 100644 --- a/src/zodv4/index.ts +++ b/src/zodv4/index.ts @@ -36,7 +36,8 @@ export class Zodv4SchemaVisitor extends BaseSchemaVisitor { private circularTypes: Set constructor(schema: GraphQLSchema, config: ValidationSchemaPluginConfig) { super(schema, config); - this.generationStack = new Set() + this.circularTypes = findCircularTypes(schema); + this.config.lazyStrategy ??= 'all'; } importValidationSchema(): string { @@ -362,9 +363,23 @@ function generateNameNodeZodSchema(config: ValidationSchemaPluginConfig, visitor } } -function maybeLazy(type: TypeNode, schema: string): string { - if (isNamedType(type) && isInput(type.name.value)) - return `z.lazy(() => ${schema})`; +function maybeLazy( + type: TypeNode, + schema: string, + config: ValidationSchemaPluginConfig, + circularTypes: Set +): string { + if (isNamedType(type)) { + const typeName = type.name.value; + + if (config.lazyStrategy === 'all' && isInput(typeName)) { + return `z.lazy(() => ${schema})`; + } + + if (config.lazyStrategy === 'circular' && circularTypes.has(typeName)) { + return `z.lazy(() => ${schema})`; + } + } return schema; } From 92884a066446637b8a4b197ea45a69e0cdc93596 Mon Sep 17 00:00:00 2001 From: MPiland Date: Thu, 5 Jun 2025 11:42:45 -0500 Subject: [PATCH 22/42] Update index.ts --- src/zodv4/index.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/zodv4/index.ts b/src/zodv4/index.ts index 10dd87c5..908f6cc2 100644 --- a/src/zodv4/index.ts +++ b/src/zodv4/index.ts @@ -123,7 +123,7 @@ export class Zodv4SchemaVisitor extends BaseSchemaVisitor { const appendArguments = argumentBlocks ? `\n${argumentBlocks}` : ''; // Building schema for fields. - const shape = node.fields?.map(field => generateFieldZodSchema(this.config, visitor, field, 2)).join(',\n'); + const shape = node.fields?.map(field => generateFieldZodSchema(this.config, visitor, field, 2, this.circularTypes)).join(',\n'); switch (this.config.validationSchemaExportType) { case 'const': @@ -242,7 +242,7 @@ export class Zodv4SchemaVisitor extends BaseSchemaVisitor { const typeName = visitor.prefixTypeNamespace(name); const discriminator = this.config.inputDiscriminator ? `${this.config.inputDiscriminator}: z.literal('${name}'),\n` : '' - const shape = fields.map(field => generateFieldZodSchema(this.config, visitor, field, 2)).join(',\n'); + const shape = fields.map(field => generateFieldZodSchema(this.config, visitor, field, 2, this.circularTypes)).join(',\n'); switch (this.config.validationSchemaExportType) { case 'const': @@ -265,12 +265,12 @@ export class Zodv4SchemaVisitor extends BaseSchemaVisitor { } } -function generateFieldZodSchema(config: ValidationSchemaPluginConfig, visitor: Visitor, field: InputValueDefinitionNode | FieldDefinitionNode, indentCount: number): string { +function generateFieldZodSchema(config: ValidationSchemaPluginConfig, visitor: Visitor, field: InputValueDefinitionNode | FieldDefinitionNode, indentCount: number, circularTypes: Set): string { const gen = generateFieldTypeZodSchema(config, visitor, field, field.type); - return indent(`${field.name.value}: ${maybeLazy(field.type, gen)}`, indentCount); + return indent(`${field.name.value}: ${maybeLazy(field.type, gen, config, circularTypes)}`, indentCount); } -function generateFieldTypeZodSchema(config: ValidationSchemaPluginConfig, visitor: Visitor, field: InputValueDefinitionNode | FieldDefinitionNode, type: TypeNode, parentType?: TypeNode): string { +function generateFieldTypeZodSchema(config: ValidationSchemaPluginConfig, visitor: Visitor, field: InputValueDefinitionNode | FieldDefinitionNode, type: TypeNode, parentType?: TypeNoder, circularTypes: Set): string { if (isListType(type)) { const gen = generateFieldTypeZodSchema(config, visitor, field, type.type, type); if (!isNonNullType(parentType)) { From 7064cf8ab01c317df2cb18479b9dca97bcbc8337 Mon Sep 17 00:00:00 2001 From: MPiland Date: Thu, 5 Jun 2025 11:45:24 -0500 Subject: [PATCH 23/42] Update index.ts --- src/zodv4/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/zodv4/index.ts b/src/zodv4/index.ts index 908f6cc2..7f038546 100644 --- a/src/zodv4/index.ts +++ b/src/zodv4/index.ts @@ -82,7 +82,7 @@ export class Zodv4SchemaVisitor extends BaseSchemaVisitor { const appendArguments = argumentBlocks ? `\n${argumentBlocks}` : ''; // Building schema for fields. - const shape = node.fields?.map(field => generateFieldZodSchema(this.config, visitor, field, 2)).join(',\n'); + const shape = node.fields?.map(field => generateFieldZodSchema(this.config, visitor, field, 2, this.circularTypes)).join(',\n'); switch (this.config.validationSchemaExportType) { case 'const': From d5bbdc091caf320ec3a565ff6cd2a3d1b1a77c5f Mon Sep 17 00:00:00 2001 From: MPiland Date: Thu, 5 Jun 2025 11:59:18 -0500 Subject: [PATCH 24/42] Update index.ts --- src/zodv4/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/zodv4/index.ts b/src/zodv4/index.ts index 7f038546..53aea3d5 100644 --- a/src/zodv4/index.ts +++ b/src/zodv4/index.ts @@ -266,13 +266,13 @@ export class Zodv4SchemaVisitor extends BaseSchemaVisitor { } function generateFieldZodSchema(config: ValidationSchemaPluginConfig, visitor: Visitor, field: InputValueDefinitionNode | FieldDefinitionNode, indentCount: number, circularTypes: Set): string { - const gen = generateFieldTypeZodSchema(config, visitor, field, field.type); + const gen = generateFieldTypeZodSchema(config, visitor, field, field.type, undefined, circularTypes); return indent(`${field.name.value}: ${maybeLazy(field.type, gen, config, circularTypes)}`, indentCount); } function generateFieldTypeZodSchema(config: ValidationSchemaPluginConfig, visitor: Visitor, field: InputValueDefinitionNode | FieldDefinitionNode, type: TypeNode, parentType?: TypeNoder, circularTypes: Set): string { if (isListType(type)) { - const gen = generateFieldTypeZodSchema(config, visitor, field, type.type, type); + const gen = generateFieldTypeZodSchema(config, visitor, field, type.type, type, circularTypes); if (!isNonNullType(parentType)) { const arrayGen = `z.array(${maybeLazy(type.type, gen)})`; const maybeLazyGen = applyDirectives(config, field, arrayGen); @@ -281,7 +281,7 @@ function generateFieldTypeZodSchema(config: ValidationSchemaPluginConfig, visito return `z.array(${maybeLazy(type.type, gen)})`; } if (isNonNullType(type)) { - const gen = generateFieldTypeZodSchema(config, visitor, field, type.type, type); + const gen = generateFieldTypeZodSchema(config, visitor, field, type.type, type, circularTypes); return maybeLazy(type.type, gen); } if (isNamedType(type)) { From e482d029ec153231ce314818256a1c1f07c28bee Mon Sep 17 00:00:00 2001 From: MPiland Date: Thu, 5 Jun 2025 12:56:48 -0500 Subject: [PATCH 25/42] Update index.ts --- src/zodv4/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/zodv4/index.ts b/src/zodv4/index.ts index 53aea3d5..52e4b8b6 100644 --- a/src/zodv4/index.ts +++ b/src/zodv4/index.ts @@ -274,15 +274,15 @@ function generateFieldTypeZodSchema(config: ValidationSchemaPluginConfig, visito if (isListType(type)) { const gen = generateFieldTypeZodSchema(config, visitor, field, type.type, type, circularTypes); if (!isNonNullType(parentType)) { - const arrayGen = `z.array(${maybeLazy(type.type, gen)})`; + const arrayGen = `z.array(${maybeLazy(type.type, gen, config, circularTypes)})`; const maybeLazyGen = applyDirectives(config, field, arrayGen); return `${maybeLazyGen}.nullish()`; } - return `z.array(${maybeLazy(type.type, gen)})`; + return `z.array(${maybeLazy(type.type, gen, config, circularTypes)})`; } if (isNonNullType(type)) { const gen = generateFieldTypeZodSchema(config, visitor, field, type.type, type, circularTypes); - return maybeLazy(type.type, gen); + return maybeLazy(type.type, gen, config, circularTypes); } if (isNamedType(type)) { const gen = generateNameNodeZodSchema(config, visitor, type.name); From c34efd909537a53dffe82ad4a2f90821f860f887 Mon Sep 17 00:00:00 2001 From: MPiland Date: Thu, 5 Jun 2025 12:58:39 -0500 Subject: [PATCH 26/42] Update index.ts --- src/zod/index.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/zod/index.ts b/src/zod/index.ts index f4070c23..f4e7e340 100644 --- a/src/zod/index.ts +++ b/src/zod/index.ts @@ -29,12 +29,15 @@ import { ObjectTypeDefinitionBuilder, } from '../graphql.js'; import { BaseSchemaVisitor } from '../schema_visitor.js'; +import { findCircularTypes } from '../utils.js'; const anySchema = `definedNonNullAnySchema`; export class ZodSchemaVisitor extends BaseSchemaVisitor { constructor(schema: GraphQLSchema, config: ValidationSchemaPluginConfig) { super(schema, config); + this.circularTypes = findCircularTypes(schema); + this.config.lazyStrategy ??= 'all'; } importValidationSchema(): string { @@ -95,7 +98,7 @@ export class ZodSchemaVisitor extends BaseSchemaVisitor { const appendArguments = argumentBlocks ? `\n${argumentBlocks}` : ''; // Building schema for fields. - const shape = node.fields?.map(field => generateFieldZodSchema(this.config, visitor, field, 2)).join(',\n'); + const shape = node.fields?.map(field => generateFieldZodSchema(this.config, visitor, field, 2, this.circularTypes)).join(',\n'); switch (this.config.validationSchemaExportType) { case 'const': @@ -136,7 +139,7 @@ export class ZodSchemaVisitor extends BaseSchemaVisitor { const appendArguments = argumentBlocks ? `\n${argumentBlocks}` : ''; // Building schema for fields. - const shape = node.fields?.map(field => generateFieldZodSchema(this.config, visitor, field, 2)).join(',\n'); + const shape = node.fields?.map(field => generateFieldZodSchema(this.config, visitor, field, 2, this.circularTypes)).join(',\n'); switch (this.config.validationSchemaExportType) { case 'const': @@ -255,7 +258,7 @@ export class ZodSchemaVisitor extends BaseSchemaVisitor { const typeName = visitor.prefixTypeNamespace(name); const discriminator = this.config.inputDiscriminator ? `${this.config.inputDiscriminator}: z.literal('${name}'),\n` : '' - const shape = fields.map(field => generateFieldZodSchema(this.config, visitor, field, 2)).join(',\n'); + const shape = fields.map(field => generateFieldZodSchema(this.config, visitor, field, 2, this.circularTypes)).join(',\n'); switch (this.config.validationSchemaExportType) { case 'const': From 5356fddcfa4ee833e26476f5cb83dfc20cc38244 Mon Sep 17 00:00:00 2001 From: MPiland Date: Thu, 5 Jun 2025 13:00:02 -0500 Subject: [PATCH 27/42] Update index.ts --- src/zod/index.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/zod/index.ts b/src/zod/index.ts index f4e7e340..2c69e9ef 100644 --- a/src/zod/index.ts +++ b/src/zod/index.ts @@ -281,24 +281,24 @@ export class ZodSchemaVisitor extends BaseSchemaVisitor { } } -function generateFieldZodSchema(config: ValidationSchemaPluginConfig, visitor: Visitor, field: InputValueDefinitionNode | FieldDefinitionNode, indentCount: number): string { - const gen = generateFieldTypeZodSchema(config, visitor, field, field.type); - return indent(`${field.name.value}: ${maybeLazy(field.type, gen)}`, indentCount); +function generateFieldZodSchema(config: ValidationSchemaPluginConfig, visitor: Visitor, field: InputValueDefinitionNode | FieldDefinitionNode, indentCount: number, circularTypes: Set): string { + const gen = generateFieldTypeZodSchema(config, visitor, field, field.type, undefined, circularTypes); + return indent(`${field.name.value}: ${maybeLazy(field.type, gen, config, circularTypes)}`, indentCount); } -function generateFieldTypeZodSchema(config: ValidationSchemaPluginConfig, visitor: Visitor, field: InputValueDefinitionNode | FieldDefinitionNode, type: TypeNode, parentType?: TypeNode): string { +function generateFieldTypeZodSchema(config: ValidationSchemaPluginConfig, visitor: Visitor, field: InputValueDefinitionNode | FieldDefinitionNode, type: TypeNode, parentType?: TypeNode, circularTypes: Set): string { if (isListType(type)) { - const gen = generateFieldTypeZodSchema(config, visitor, field, type.type, type); + const gen = generateFieldTypeZodSchema(config, visitor, field, type.type, type, circularTypes); if (!isNonNullType(parentType)) { - const arrayGen = `z.array(${maybeLazy(type.type, gen)})`; + const arrayGen = `z.array(${maybeLazy(type.type, gen, config, circularTypes)})`; const maybeLazyGen = applyDirectives(config, field, arrayGen); return `${maybeLazyGen}.nullish()`; } - return `z.array(${maybeLazy(type.type, gen)})`; + return `z.array(${maybeLazy(type.type, gen, config, circularTypes)})`; } if (isNonNullType(type)) { - const gen = generateFieldTypeZodSchema(config, visitor, field, type.type, type); - return maybeLazy(type.type, gen); + const gen = generateFieldTypeZodSchema(config, visitor, field, type.type, type, circularTypes); + return maybeLazy(type.type, gen, config, circularTypes); } if (isNamedType(type)) { const gen = generateNameNodeZodSchema(config, visitor, type.name); From d9712f9f1101e72b3612e5df57b60b1f1888bd48 Mon Sep 17 00:00:00 2001 From: MPiland Date: Thu, 5 Jun 2025 13:00:29 -0500 Subject: [PATCH 28/42] Update index.ts --- src/zod/index.ts | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/zod/index.ts b/src/zod/index.ts index 2c69e9ef..03c81369 100644 --- a/src/zod/index.ts +++ b/src/zod/index.ts @@ -379,9 +379,23 @@ function generateNameNodeZodSchema(config: ValidationSchemaPluginConfig, visitor } } -function maybeLazy(type: TypeNode, schema: string): string { - if (isNamedType(type) && isInput(type.name.value)) - return `z.lazy(() => ${schema})`; +function maybeLazy( + type: TypeNode, + schema: string, + config: ValidationSchemaPluginConfig, + circularTypes: Set +): string { + if (isNamedType(type)) { + const typeName = type.name.value; + + if (config.lazyStrategy === 'all' && isInput(typeName)) { + return `z.lazy(() => ${schema})`; + } + + if (config.lazyStrategy === 'circular' && circularTypes.has(typeName)) { + return `z.lazy(() => ${schema})`; + } + } return schema; } From 5b75ed73b27579b9c2e264fb45eda979d06579ad Mon Sep 17 00:00:00 2001 From: MPiland Date: Thu, 5 Jun 2025 13:44:36 -0500 Subject: [PATCH 29/42] Update index.ts --- src/zod/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/zod/index.ts b/src/zod/index.ts index 03c81369..406d13b9 100644 --- a/src/zod/index.ts +++ b/src/zod/index.ts @@ -386,6 +386,7 @@ function maybeLazy( circularTypes: Set ): string { if (isNamedType(type)) { + // https://github.com/jquense/yup/issues/1283#issuecomment-786559444 const typeName = type.name.value; if (config.lazyStrategy === 'all' && isInput(typeName)) { From f0c638f5e69620d568b4940afad8f255e596838f Mon Sep 17 00:00:00 2001 From: MPiland Date: Thu, 5 Jun 2025 13:44:41 -0500 Subject: [PATCH 30/42] Update index.ts --- src/yup/index.ts | 51 ++++++++++++++++++++++++++++++++---------------- 1 file changed, 34 insertions(+), 17 deletions(-) diff --git a/src/yup/index.ts b/src/yup/index.ts index 94580a5f..a2c91ff5 100644 --- a/src/yup/index.ts +++ b/src/yup/index.ts @@ -29,10 +29,13 @@ import { ObjectTypeDefinitionBuilder, } from '../graphql.js'; import { BaseSchemaVisitor } from '../schema_visitor.js'; +import { findCircularTypes } from '../utils.js'; export class YupSchemaVisitor extends BaseSchemaVisitor { constructor(schema: GraphQLSchema, config: ValidationSchemaPluginConfig) { super(schema, config); + this.circularTypes = findCircularTypes(schema); + this.config.lazyStrategy ??= 'all'; } importValidationSchema(): string { @@ -85,7 +88,7 @@ export class YupSchemaVisitor extends BaseSchemaVisitor { // Building schema for fields. const shape = node.fields?.map((field) => { - const fieldSchema = generateFieldYupSchema(this.config, visitor, field, 2); + const fieldSchema = generateFieldYupSchema(this.config, visitor, field, 2, this.circularTypes); return isNonNullType(field.type) ? fieldSchema : `${fieldSchema}.optional()`; }).join(',\n'); @@ -128,7 +131,7 @@ export class YupSchemaVisitor extends BaseSchemaVisitor { const appendArguments = argumentBlocks ? `\n${argumentBlocks}` : ''; // Building schema for fields. - const shape = shapeFields(node.fields, this.config, visitor); + const shape = shapeFields(node.fields, this.config, visitor, this.circularTypes); switch (this.config.validationSchemaExportType) { case 'const': @@ -258,7 +261,7 @@ export class YupSchemaVisitor extends BaseSchemaVisitor { const typeName = visitor.prefixTypeNamespace(name); const discriminator = this.config.inputDiscriminator ? `${this.config.inputDiscriminator}: z.literal('${name}'),\n` : '' - const shape = shapeFields(fields, this.config, visitor); + const shape = shapeFields(fields, this.config, visitor, this.circularTypes); switch (this.config.validationSchemaExportType) { case 'const': @@ -281,10 +284,10 @@ export class YupSchemaVisitor extends BaseSchemaVisitor { } } -function shapeFields(fields: readonly (FieldDefinitionNode | InputValueDefinitionNode)[] | undefined, config: ValidationSchemaPluginConfig, visitor: Visitor) { +function shapeFields(fields: readonly (FieldDefinitionNode | InputValueDefinitionNode)[] | undefined, config: ValidationSchemaPluginConfig, visitor: Visitor, circularTypes: Set) { return fields ?.map((field) => { - let fieldSchema = generateFieldYupSchema(config, visitor, field, 2); + let fieldSchema = generateFieldYupSchema(config, visitor, field, 2, circularTypes); if (field.kind === Kind.INPUT_VALUE_DEFINITION) { const { defaultValue } = field; @@ -320,26 +323,26 @@ function shapeFields(fields: readonly (FieldDefinitionNode | InputValueDefinitio .join(',\n'); } -function generateFieldYupSchema(config: ValidationSchemaPluginConfig, visitor: Visitor, field: InputValueDefinitionNode | FieldDefinitionNode, indentCount: number): string { - let gen = generateFieldTypeYupSchema(config, visitor, field.type); +function generateFieldYupSchema(config: ValidationSchemaPluginConfig, visitor: Visitor, field: InputValueDefinitionNode | FieldDefinitionNode, indentCount: number, circularTypes: Set): string { + let gen = generateFieldTypeYupSchema(config, visitor, field.type, undefined, circularTypes); if (config.directives && field.directives) { const formatted = formatDirectiveConfig(config.directives); gen += buildApi(formatted, field.directives); } - return indent(`${field.name.value}: ${maybeLazy(field.type, gen)}`, indentCount); + return indent(`${field.name.value}: ${maybeLazy(field.type, gen, config, circularTypes)}`, indentCount); } -function generateFieldTypeYupSchema(config: ValidationSchemaPluginConfig, visitor: Visitor, type: TypeNode, parentType?: TypeNode): string { +function generateFieldTypeYupSchema(config: ValidationSchemaPluginConfig, visitor: Visitor, type: TypeNode, parentType?: TypeNode, circularTypes: Set): string { if (isListType(type)) { - const gen = generateFieldTypeYupSchema(config, visitor, type.type, type); + const gen = generateFieldTypeYupSchema(config, visitor, type.type, type, circularTypes); if (!isNonNullType(parentType)) - return `yup.array(${maybeLazy(type.type, gen)}).defined().nullable()`; + return `yup.array(${maybeLazy(type.type, gen, config, circularTypes)}).defined().nullable()`; - return `yup.array(${maybeLazy(type.type, gen)}).defined()`; + return `yup.array(${maybeLazy(type.type, gen, config, circularTypes)}).defined()`; } if (isNonNullType(type)) { - const gen = generateFieldTypeYupSchema(config, visitor, type.type, type); - return maybeLazy(type.type, gen); + const gen = generateFieldTypeYupSchema(config, visitor, type.type, type, circularTypes); + return maybeLazy(type.type, gen, config, circularTypes); } if (isNamedType(type)) { const gen = generateNameNodeYupSchema(config, visitor, type.name); @@ -382,11 +385,25 @@ function generateNameNodeYupSchema(config: ValidationSchemaPluginConfig, visitor } } -function maybeLazy(type: TypeNode, schema: string): string { - if (isNamedType(type) && isInput(type.name.value)) { +function maybeLazy( + type: TypeNode, + schema: string, + config: ValidationSchemaPluginConfig, + circularTypes: Set +): string { + if (isNamedType(type)) { // https://github.com/jquense/yup/issues/1283#issuecomment-786559444 - return `yup.lazy(() => ${schema})`; + const typeName = type.name.value; + + if (config.lazyStrategy === 'all' && isInput(typeName)) { + return `yup.lazy(() => ${schema})`; + } + + if (config.lazyStrategy === 'circular' && circularTypes.has(typeName)) { + return `yup.lazy(() => ${schema})`; + } } + return schema; } From 1407df9d32649445d53eabbfda7af8f02c0db797 Mon Sep 17 00:00:00 2001 From: MPiland Date: Thu, 5 Jun 2025 13:54:15 -0500 Subject: [PATCH 31/42] Update index.ts --- src/valibot/index.ts | 46 ++++++++++++++++++++++++++++++-------------- 1 file changed, 32 insertions(+), 14 deletions(-) diff --git a/src/valibot/index.ts b/src/valibot/index.ts index 53b2f3f2..95a1a5e0 100644 --- a/src/valibot/index.ts +++ b/src/valibot/index.ts @@ -24,10 +24,13 @@ import { ObjectTypeDefinitionBuilder, } from '../graphql.js'; import { BaseSchemaVisitor } from '../schema_visitor.js'; +import { findCircularTypes } from '../utils.js'; export class ValibotSchemaVisitor extends BaseSchemaVisitor { constructor(schema: GraphQLSchema, config: ValidationSchemaPluginConfig) { super(schema, config); + this.circularTypes = findCircularTypes(schema); + this.config.lazyStrategy ??= 'all'; } importValidationSchema(): string { @@ -66,7 +69,7 @@ export class ValibotSchemaVisitor extends BaseSchemaVisitor { const appendArguments = argumentBlocks ? `\n${argumentBlocks}` : ''; // Building schema for fields. - const shape = node.fields?.map(field => generateFieldValibotSchema(this.config, visitor, field, 2)).join(',\n'); + const shape = node.fields?.map(field => generateFieldValibotSchema(this.config, visitor, field, 2, this.circularTypes)).join(',\n'); switch (this.config.validationSchemaExportType) { default: @@ -96,7 +99,7 @@ export class ValibotSchemaVisitor extends BaseSchemaVisitor { const appendArguments = argumentBlocks ? `\n${argumentBlocks}` : ''; // Building schema for fields. - const shape = node.fields?.map(field => generateFieldValibotSchema(this.config, visitor, field, 2)).join(',\n'); + const shape = node.fields?.map(field => generateFieldValibotSchema(this.config, visitor, field, 2, this.circularTypes)).join(',\n'); switch (this.config.validationSchemaExportType) { default: @@ -191,7 +194,7 @@ export class ValibotSchemaVisitor extends BaseSchemaVisitor { const typeName = visitor.prefixTypeNamespace(name); const discriminator = this.config.inputDiscriminator ? `${this.config.inputDiscriminator}: z.literal('${name}'),\n` : '' - const shape = fields.map(field => generateFieldValibotSchema(this.config, visitor, field, 2)).join(',\n'); + const shape = fields.map(field => generateFieldValibotSchema(this.config, visitor, field, 2, this.circularTypes)).join(',\n'); switch (this.config.validationSchemaExportType) { default: @@ -205,23 +208,23 @@ export class ValibotSchemaVisitor extends BaseSchemaVisitor { } } -function generateFieldValibotSchema(config: ValidationSchemaPluginConfig, visitor: Visitor, field: InputValueDefinitionNode | FieldDefinitionNode, indentCount: number): string { - const gen = generateFieldTypeValibotSchema(config, visitor, field, field.type); - return indent(`${field.name.value}: ${maybeLazy(field.type, gen)}`, indentCount); +function generateFieldValibotSchema(config: ValidationSchemaPluginConfig, visitor: Visitor, field: InputValueDefinitionNode | FieldDefinitionNode, indentCount: number, circularTypes: Set): string { + const gen = generateFieldTypeValibotSchema(config, visitor, field, field.type, undefined, circularTypes); + return indent(`${field.name.value}: ${maybeLazy(field.type, gen, config, circularTypes)}`, indentCount); } -function generateFieldTypeValibotSchema(config: ValidationSchemaPluginConfig, visitor: Visitor, field: InputValueDefinitionNode | FieldDefinitionNode, type: TypeNode, parentType?: TypeNode): string { +function generateFieldTypeValibotSchema(config: ValidationSchemaPluginConfig, visitor: Visitor, field: InputValueDefinitionNode | FieldDefinitionNode, type: TypeNode, parentType?: TypeNode, circularTypes: Set): string { if (isListType(type)) { - const gen = generateFieldTypeValibotSchema(config, visitor, field, type.type, type); - const arrayGen = `v.array(${maybeLazy(type.type, gen)})`; + const gen = generateFieldTypeValibotSchema(config, visitor, field, type.type, type, circularTypes); + const arrayGen = `v.array(${maybeLazy(type.type, gen, config, circularTypes)})`; if (!isNonNullType(parentType)) return `v.nullish(${arrayGen})`; return arrayGen; } if (isNonNullType(type)) { - const gen = generateFieldTypeValibotSchema(config, visitor, field, type.type, type); - return maybeLazy(type.type, gen); + const gen = generateFieldTypeValibotSchema(config, visitor, field, type.type, type, circularTypes); + return maybeLazy(type.type, gen, config, circularTypes); } if (isNamedType(type)) { const gen = generateNameNodeValibotSchema(config, visitor, type.name); @@ -285,9 +288,24 @@ function generateNameNodeValibotSchema(config: ValidationSchemaPluginConfig, vis } } -function maybeLazy(type: TypeNode, schema: string): string { - if (isNamedType(type) && isInput(type.name.value)) - return `v.lazy(() => ${schema})`; +function maybeLazy( + type: TypeNode, + schema: string, + config: ValidationSchemaPluginConfig, + circularTypes: Set +): string { + if (isNamedType(type)) { + // https://github.com/jquense/yup/issues/1283#issuecomment-786559444 + const typeName = type.name.value; + + if (config.lazyStrategy === 'all' && isInput(typeName)) { + return `v.lazy(() => ${schema})`; + } + + if (config.lazyStrategy === 'circular' && circularTypes.has(typeName)) { + return `v.lazy(() => ${schema})`; + } + } return schema; } From b95543eea7766e4860336e14e62becedd9c29700 Mon Sep 17 00:00:00 2001 From: MPiland Date: Thu, 5 Jun 2025 13:59:03 -0500 Subject: [PATCH 32/42] Update index.ts --- src/myzod/index.ts | 49 +++++++++++++++++++++++++++++++--------------- 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/src/myzod/index.ts b/src/myzod/index.ts index 1bc15e06..287d2913 100644 --- a/src/myzod/index.ts +++ b/src/myzod/index.ts @@ -29,12 +29,15 @@ import { ObjectTypeDefinitionBuilder, } from '../graphql.js'; import { BaseSchemaVisitor } from '../schema_visitor.js'; +import { findCircularTypes } from '../utils.js'; const anySchema = `definedNonNullAnySchema`; export class MyZodSchemaVisitor extends BaseSchemaVisitor { constructor(schema: GraphQLSchema, config: ValidationSchemaPluginConfig) { super(schema, config); + this.circularTypes = findCircularTypes(schema); + this.config.lazyStrategy ??= 'all'; } importValidationSchema(): string { @@ -75,7 +78,7 @@ export class MyZodSchemaVisitor extends BaseSchemaVisitor { const appendArguments = argumentBlocks ? `\n${argumentBlocks}` : ''; // Building schema for fields. - const shape = node.fields?.map(field => generateFieldMyZodSchema(this.config, visitor, field, 2)).join(',\n'); + const shape = node.fields?.map(field => generateFieldMyZodSchema(this.config, visitor, field, 2, this.circularTypes)).join(',\n'); switch (this.config.validationSchemaExportType) { case 'const': @@ -116,7 +119,7 @@ export class MyZodSchemaVisitor extends BaseSchemaVisitor { const appendArguments = argumentBlocks ? `\n${argumentBlocks}` : ''; // Building schema for fields. - const shape = node.fields?.map(field => generateFieldMyZodSchema(this.config, visitor, field, 2)).join(',\n'); + const shape = node.fields?.map(field => generateFieldMyZodSchema(this.config, visitor, field, 2, this.circularTypes)).join(',\n'); switch (this.config.validationSchemaExportType) { case 'const': @@ -238,8 +241,8 @@ export class MyZodSchemaVisitor extends BaseSchemaVisitor { ) { const typeName = visitor.prefixTypeNamespace(name); const discriminator = - this.config.inputDiscriminator ? `${this.config.inputDiscriminator}: z.literal('${name}'),\n` : '' - const shape = fields.map(field => generateFieldMyZodSchema(this.config, visitor, field, 2)).join(',\n'); + this.config.inputDiscriminator ? `\t${this.config.inputDiscriminator}: z.literal('${name}'),` : '' + const shape = fields.map(field => generateFieldMyZodSchema(this.config, visitor, field, 2, this.circularTypes)).join(',\n'); switch (this.config.validationSchemaExportType) { case 'const': @@ -262,24 +265,24 @@ export class MyZodSchemaVisitor extends BaseSchemaVisitor { } } -function generateFieldMyZodSchema(config: ValidationSchemaPluginConfig, visitor: Visitor, field: InputValueDefinitionNode | FieldDefinitionNode, indentCount: number): string { - const gen = generateFieldTypeMyZodSchema(config, visitor, field, field.type); - return indent(`${field.name.value}: ${maybeLazy(field.type, gen)}`, indentCount); +function generateFieldMyZodSchema(config: ValidationSchemaPluginConfig, visitor: Visitor, field: InputValueDefinitionNode | FieldDefinitionNode, indentCount: number, circularTypes: Set): string { + const gen = generateFieldTypeMyZodSchema(config, visitor, field, field.type, undefined, circularTypes); + return indent(`${field.name.value}: ${maybeLazy(field.type, gen, config, circularTypes)}`, indentCount); } -function generateFieldTypeMyZodSchema(config: ValidationSchemaPluginConfig, visitor: Visitor, field: InputValueDefinitionNode | FieldDefinitionNode, type: TypeNode, parentType?: TypeNode): string { +function generateFieldTypeMyZodSchema(config: ValidationSchemaPluginConfig, visitor: Visitor, field: InputValueDefinitionNode | FieldDefinitionNode, type: TypeNode, parentType?: TypeNode, circularTypes: Set): string { if (isListType(type)) { - const gen = generateFieldTypeMyZodSchema(config, visitor, field, type.type, type); + const gen = generateFieldTypeMyZodSchema(config, visitor, field, type.type, type, circularTypes); if (!isNonNullType(parentType)) { - const arrayGen = `myzod.array(${maybeLazy(type.type, gen)})`; + const arrayGen = `myzod.array(${maybeLazy(type.type, gen, config, circularTypes)})`; const maybeLazyGen = applyDirectives(config, field, arrayGen); return `${maybeLazyGen}.optional().nullable()`; } - return `myzod.array(${maybeLazy(type.type, gen)})`; + return `myzod.array(${maybeLazy(type.type, gen, config, circularTypes)})`; } if (isNonNullType(type)) { - const gen = generateFieldTypeMyZodSchema(config, visitor, field, type.type, type); - return maybeLazy(type.type, gen); + const gen = generateFieldTypeMyZodSchema(config, visitor, field, type.type, type, circularTypes); + return maybeLazy(type.type, gen, config, circularTypes); } if (isNamedType(type)) { const gen = generateNameNodeMyZodSchema(config, visitor, type.name); @@ -360,9 +363,23 @@ function generateNameNodeMyZodSchema(config: ValidationSchemaPluginConfig, visit } } -function maybeLazy(type: TypeNode, schema: string): string { - if (isNamedType(type) && isInput(type.name.value)) - return `myzod.lazy(() => ${schema})`; +function maybeLazy( + type: TypeNode, + schema: string, + config: ValidationSchemaPluginConfig, + circularTypes: Set +): string { + if (isNamedType(type)) { + const typeName = type.name.value; + + if (config.lazyStrategy === 'all' && isInput(typeName)) { + return `myzod.lazy(() => ${schema})`; + } + + if (config.lazyStrategy === 'circular' && circularTypes.has(typeName)) { + return `myzod.lazy(() => ${schema})`; + } + } return schema; } From 404f91efc153ff51137398be75a8dce525d310dd Mon Sep 17 00:00:00 2001 From: MPiland Date: Thu, 5 Jun 2025 14:01:29 -0500 Subject: [PATCH 33/42] Update index.ts --- src/zodv4/index.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/zodv4/index.ts b/src/zodv4/index.ts index 52e4b8b6..963ae0c8 100644 --- a/src/zodv4/index.ts +++ b/src/zodv4/index.ts @@ -240,8 +240,7 @@ export class Zodv4SchemaVisitor extends BaseSchemaVisitor { name: string, ) { const typeName = visitor.prefixTypeNamespace(name); - const discriminator = - this.config.inputDiscriminator ? `${this.config.inputDiscriminator}: z.literal('${name}'),\n` : '' + const discriminator = this.config.inputDiscriminator ? `\t${this.config.inputDiscriminator}: z.literal('${name}'),` : '' const shape = fields.map(field => generateFieldZodSchema(this.config, visitor, field, 2, this.circularTypes)).join(',\n'); switch (this.config.validationSchemaExportType) { From 55c8199d2d9059cb6530f11bdd7f8b1c52bf58ba Mon Sep 17 00:00:00 2001 From: MPiland Date: Thu, 5 Jun 2025 14:01:50 -0500 Subject: [PATCH 34/42] Update index.ts --- src/zod/index.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/zod/index.ts b/src/zod/index.ts index 406d13b9..7d541342 100644 --- a/src/zod/index.ts +++ b/src/zod/index.ts @@ -256,8 +256,7 @@ export class ZodSchemaVisitor extends BaseSchemaVisitor { name: string, ) { const typeName = visitor.prefixTypeNamespace(name); - const discriminator = - this.config.inputDiscriminator ? `${this.config.inputDiscriminator}: z.literal('${name}'),\n` : '' + const discriminator = this.config.inputDiscriminator ? `\t${this.config.inputDiscriminator}: z.literal('${name}'),` : '' const shape = fields.map(field => generateFieldZodSchema(this.config, visitor, field, 2, this.circularTypes)).join(',\n'); switch (this.config.validationSchemaExportType) { From 59492f7787a506ba57fcd821b1d7b76b66b4432b Mon Sep 17 00:00:00 2001 From: MPiland Date: Thu, 5 Jun 2025 14:02:07 -0500 Subject: [PATCH 35/42] Update index.ts --- src/yup/index.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/yup/index.ts b/src/yup/index.ts index a2c91ff5..b09326d3 100644 --- a/src/yup/index.ts +++ b/src/yup/index.ts @@ -259,8 +259,7 @@ export class YupSchemaVisitor extends BaseSchemaVisitor { name: string, ) { const typeName = visitor.prefixTypeNamespace(name); - const discriminator = - this.config.inputDiscriminator ? `${this.config.inputDiscriminator}: z.literal('${name}'),\n` : '' + const discriminator = this.config.inputDiscriminator ? `\t${this.config.inputDiscriminator}: z.literal('${name}'),` : '' const shape = shapeFields(fields, this.config, visitor, this.circularTypes); switch (this.config.validationSchemaExportType) { From 729cfcb7aff88b6b75706384c26ef8beb96da6c9 Mon Sep 17 00:00:00 2001 From: MPiland Date: Thu, 5 Jun 2025 14:02:25 -0500 Subject: [PATCH 36/42] Update index.ts --- src/valibot/index.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/valibot/index.ts b/src/valibot/index.ts index 95a1a5e0..e6b35f99 100644 --- a/src/valibot/index.ts +++ b/src/valibot/index.ts @@ -192,8 +192,7 @@ export class ValibotSchemaVisitor extends BaseSchemaVisitor { name: string, ) { const typeName = visitor.prefixTypeNamespace(name); - const discriminator = - this.config.inputDiscriminator ? `${this.config.inputDiscriminator}: z.literal('${name}'),\n` : '' + const discriminator = this.config.inputDiscriminator ? `\t${this.config.inputDiscriminator}: z.literal('${name}'),` : '' const shape = fields.map(field => generateFieldValibotSchema(this.config, visitor, field, 2, this.circularTypes)).join(',\n'); switch (this.config.validationSchemaExportType) { From 9ddf8bafd512bad6b64de48d57ce24686e8161a1 Mon Sep 17 00:00:00 2001 From: MPiland Date: Thu, 5 Jun 2025 14:02:40 -0500 Subject: [PATCH 37/42] Update index.ts --- src/myzod/index.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/myzod/index.ts b/src/myzod/index.ts index 287d2913..05e95d5c 100644 --- a/src/myzod/index.ts +++ b/src/myzod/index.ts @@ -240,8 +240,7 @@ export class MyZodSchemaVisitor extends BaseSchemaVisitor { name: string, ) { const typeName = visitor.prefixTypeNamespace(name); - const discriminator = - this.config.inputDiscriminator ? `\t${this.config.inputDiscriminator}: z.literal('${name}'),` : '' + const discriminator = this.config.inputDiscriminator ? `\t${this.config.inputDiscriminator}: z.literal('${name}'),` : '' const shape = fields.map(field => generateFieldMyZodSchema(this.config, visitor, field, 2, this.circularTypes)).join(',\n'); switch (this.config.validationSchemaExportType) { From c35f272eeebf998313b8935dec3ca22c04b0a4df Mon Sep 17 00:00:00 2001 From: MPiland Date: Thu, 5 Jun 2025 14:07:59 -0500 Subject: [PATCH 38/42] Update index.ts --- src/yup/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yup/index.ts b/src/yup/index.ts index b09326d3..3084dfc9 100644 --- a/src/yup/index.ts +++ b/src/yup/index.ts @@ -259,7 +259,7 @@ export class YupSchemaVisitor extends BaseSchemaVisitor { name: string, ) { const typeName = visitor.prefixTypeNamespace(name); - const discriminator = this.config.inputDiscriminator ? `\t${this.config.inputDiscriminator}: z.literal('${name}'),` : '' + const discriminator = this.config.inputDiscriminator ? `\t${this.config.inputDiscriminator}: yup.string().oneOf(['${name}']).defined(),` : ''; const shape = shapeFields(fields, this.config, visitor, this.circularTypes); switch (this.config.validationSchemaExportType) { From 2626e6953ca15855cee4671493d22e46ef9fb50d Mon Sep 17 00:00:00 2001 From: MPiland Date: Thu, 5 Jun 2025 14:08:20 -0500 Subject: [PATCH 39/42] Update index.ts --- src/myzod/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/myzod/index.ts b/src/myzod/index.ts index 05e95d5c..3b2840a7 100644 --- a/src/myzod/index.ts +++ b/src/myzod/index.ts @@ -240,7 +240,7 @@ export class MyZodSchemaVisitor extends BaseSchemaVisitor { name: string, ) { const typeName = visitor.prefixTypeNamespace(name); - const discriminator = this.config.inputDiscriminator ? `\t${this.config.inputDiscriminator}: z.literal('${name}'),` : '' + const discriminator = this.config.inputDiscriminator ? `\t${this.config.inputDiscriminator}: myzod.literal('${name}'),` : '' const shape = fields.map(field => generateFieldMyZodSchema(this.config, visitor, field, 2, this.circularTypes)).join(',\n'); switch (this.config.validationSchemaExportType) { From bcf9971c5e86925cec4ca3dcf01c3491505cea8d Mon Sep 17 00:00:00 2001 From: MPiland Date: Thu, 5 Jun 2025 14:09:18 -0500 Subject: [PATCH 40/42] Update index.ts --- src/valibot/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/valibot/index.ts b/src/valibot/index.ts index e6b35f99..d1fe39ce 100644 --- a/src/valibot/index.ts +++ b/src/valibot/index.ts @@ -192,7 +192,7 @@ export class ValibotSchemaVisitor extends BaseSchemaVisitor { name: string, ) { const typeName = visitor.prefixTypeNamespace(name); - const discriminator = this.config.inputDiscriminator ? `\t${this.config.inputDiscriminator}: z.literal('${name}'),` : '' + const discriminator = this.config.inputDiscriminator ? `\t${this.config.inputDiscriminator}: v.literal('${name}'),` : '' const shape = fields.map(field => generateFieldValibotSchema(this.config, visitor, field, 2, this.circularTypes)).join(',\n'); switch (this.config.validationSchemaExportType) { From 02e46deff2bd0dfe527832b91740fd970261b087 Mon Sep 17 00:00:00 2001 From: MPiland Date: Thu, 5 Jun 2025 14:35:23 -0500 Subject: [PATCH 41/42] Update README.md --- README.md | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/README.md b/README.md index 6225d43a..bd07d937 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,56 @@ generates: schema: yup ``` +### `inputOnly` + +type: `boolean` default: `false` + +When true, will create only schema for input types. + +```yml +generates: + path/to/graphql.ts: + plugins: + - typescript + - typescript-validation-schema + config: + inputOnly: true +``` + +### `inputDiscriminator` + +type: `string` default: `''` + +When a discriminator is provided, it will add the type name as the defined string for the discriminator key provided. + +```yml +generates: + path/to/graphql.ts: + plugins: + - typescript + - typescript-validation-schema + config: + inputDiscriminator: __kind +``` + +### `lazyStrategy` + +type: `lazyStrategy` default: `'all'` + +Sets how the lazy function is added to references. All will apply it to all references, circular will only apply it to truly circular references. + +You can specify `all` or `circular`. + +```yml +generates: + path/to/graphql.ts: + plugins: + - typescript + - typescript-validation-schema + config: + lazyStrategy: circular +``` + ### `importFrom` type: `string` From dba6a3d2213269b8d9668ca68abf13c549150dd2 Mon Sep 17 00:00:00 2001 From: MPiland Date: Thu, 5 Jun 2025 19:19:03 -0500 Subject: [PATCH 42/42] added zod strict object --- README.md | 16 ++++++++-------- src/config.ts | 42 ++++++++++++++++++++---------------------- src/myzod/index.ts | 2 +- src/valibot/index.ts | 2 +- src/yup/index.ts | 2 +- src/zod/index.ts | 30 +++++++++++++++++++----------- src/zodv4/index.ts | 14 +++++++------- 7 files changed, 57 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index bd07d937..96d5bb0c 100644 --- a/README.md +++ b/README.md @@ -61,11 +61,11 @@ generates: schema: yup ``` -### `inputOnly` +### `inputDiscriminator` -type: `boolean` default: `false` +type: `string` default: `''` -When true, will create only schema for input types. +When a discriminator is provided, it will add the type name as the defined string for the discriminator key provided. ```yml generates: @@ -74,14 +74,14 @@ generates: - typescript - typescript-validation-schema config: - inputOnly: true + inputDiscriminator: __kind ``` -### `inputDiscriminator` +### `zodStrictObject` -type: `string` default: `''` +type: `boolean` default: `false` -When a discriminator is provided, it will add the type name as the defined string for the discriminator key provided. +Will create a strict zod object. Should be used with Zod schema. ```yml generates: @@ -90,7 +90,7 @@ generates: - typescript - typescript-validation-schema config: - inputDiscriminator: __kind + zodStrictObject: true ``` ### `lazyStrategy` diff --git a/src/config.ts b/src/config.ts index 0e1d669b..3a2750a3 100644 --- a/src/config.ts +++ b/src/config.ts @@ -114,28 +114,26 @@ export interface ValidationSchemaPluginConfig extends TypeScriptPluginConfig { * ``` */ useTypeImports?: boolean - /** - * @description Creates schemas for input types only. - * This gives compatibility with TypeScript's "importsNotUsedAsValues": "error" option - * Should used in conjunction with `importFrom` option. - * @default false - * - * @exampleMarkdown - * ```yml - * generates: - * path/to/types.ts: - * plugins: - * - typescript - * path/to/schemas.ts: - * plugins: - * - graphql-codegen-validation-schema - * config: - * schema: yup - * importFrom: ./path/to/types - * inputsOnly: true - * ``` - */ - inputOnly?: boolean; + /** + * @description Will create strict type object. + * To be used with Zod schema. + * @default false + * + * @exampleMarkdown + * ```yml + * generates: + * path/to/types.ts: + * plugins: + * - typescript + * path/to/schemas.ts: + * plugins: + * - graphql-codegen-validation-schema + * config: + * schema: zod + * strictShape: true + * ``` + */ + zodStrictObject?: boolean /** * @description Creates schemas for input types only. * This gives compatibility with TypeScript's "importsNotUsedAsValues": "error" option diff --git a/src/myzod/index.ts b/src/myzod/index.ts index 3b2840a7..21669885 100644 --- a/src/myzod/index.ts +++ b/src/myzod/index.ts @@ -108,7 +108,7 @@ export class MyZodSchemaVisitor extends BaseSchemaVisitor { get ObjectTypeDefinition() { return { - leave: ObjectTypeDefinitionBuilder(this.config.withObjectType && !this.config.inputOnly, (node: ObjectTypeDefinitionNode) => { + leave: ObjectTypeDefinitionBuilder(this.config.withObjectType, (node: ObjectTypeDefinitionNode) => { const visitor = this.createVisitor('output'); const name = visitor.convertName(node.name.value); const typeName = visitor.prefixTypeNamespace(name); diff --git a/src/valibot/index.ts b/src/valibot/index.ts index d1fe39ce..93792091 100644 --- a/src/valibot/index.ts +++ b/src/valibot/index.ts @@ -88,7 +88,7 @@ export class ValibotSchemaVisitor extends BaseSchemaVisitor { get ObjectTypeDefinition() { return { - leave: ObjectTypeDefinitionBuilder(this.config.withObjectType && !this.config.inputOnly, (node: ObjectTypeDefinitionNode) => { + leave: ObjectTypeDefinitionBuilder(this.config.withObjectType, (node: ObjectTypeDefinitionNode) => { const visitor = this.createVisitor('output'); const name = visitor.convertName(node.name.value); const typeName = visitor.prefixTypeNamespace(name); diff --git a/src/yup/index.ts b/src/yup/index.ts index 3084dfc9..4a0545b0 100644 --- a/src/yup/index.ts +++ b/src/yup/index.ts @@ -120,7 +120,7 @@ export class YupSchemaVisitor extends BaseSchemaVisitor { get ObjectTypeDefinition() { return { - leave: ObjectTypeDefinitionBuilder(this.config.withObjectType && !this.config.inputOnly, (node: ObjectTypeDefinitionNode) => { + leave: ObjectTypeDefinitionBuilder(this.config.withObjectType, (node: ObjectTypeDefinitionNode) => { const visitor = this.createVisitor('output'); const name = visitor.convertName(node.name.value); const typeName = visitor.prefixTypeNamespace(name); diff --git a/src/zod/index.ts b/src/zod/index.ts index 7d541342..f854c686 100644 --- a/src/zod/index.ts +++ b/src/zod/index.ts @@ -107,7 +107,7 @@ export class ZodSchemaVisitor extends BaseSchemaVisitor { .export() .asKind('const') .withName(`${name}Schema: z.ZodObject>`) - .withContent([`z.object({`, shape, '})'].join('\n')) + .withContent([`z.object({`, shape, `})${this.config.zodStrictObject ? '.strict()' : ''}`].join('\n')) .string + appendArguments ); @@ -118,7 +118,7 @@ export class ZodSchemaVisitor extends BaseSchemaVisitor { .export() .asKind('function') .withName(`${name}Schema(): z.ZodObject>`) - .withBlock([indent(`return z.object({`), shape, indent('})')].join('\n')) + .withBlock([indent(`return z.object({`), shape, indent(v)].join('\n')) .string + appendArguments ); } @@ -128,7 +128,7 @@ export class ZodSchemaVisitor extends BaseSchemaVisitor { get ObjectTypeDefinition() { return { - leave: ObjectTypeDefinitionBuilder(this.config.withObjectType && !this.config.inputOnly, (node: ObjectTypeDefinitionNode) => { + leave: ObjectTypeDefinitionBuilder(this.config.withObjectType, (node: ObjectTypeDefinitionNode) => { const visitor = this.createVisitor('output'); const name = visitor.convertName(node.name.value); const typeName = visitor.prefixTypeNamespace(name); @@ -153,7 +153,7 @@ export class ZodSchemaVisitor extends BaseSchemaVisitor { `z.object({`, indent(`__typename: z.literal('${node.name.value}').optional(),`, 2), shape, - '})', + `})${this.config.zodStrictObject ? '.strict()' : ''}`, ].join('\n'), ) .string + appendArguments @@ -171,7 +171,7 @@ export class ZodSchemaVisitor extends BaseSchemaVisitor { indent(`return z.object({`), indent(`__typename: z.literal('${node.name.value}').optional(),`, 2), shape, - indent('})'), + indent(`})${this.config.zodStrictObject ? '.strict()' : ''}`), ].join('\n'), ) .string + appendArguments @@ -265,7 +265,7 @@ export class ZodSchemaVisitor extends BaseSchemaVisitor { .export() .asKind('const') .withName(`${name}Schema: z.ZodObject>`) - .withContent(['z.object({', discriminator, shape, '})'].join('\n')) + .withContent(['z.object({', discriminator, shape, `})${this.config.zodStrictObject ? '.strict()' : ''}`].join('\n')) .string; case 'function': @@ -274,20 +274,20 @@ export class ZodSchemaVisitor extends BaseSchemaVisitor { .export() .asKind('function') .withName(`${name}Schema(): z.ZodObject>`) - .withBlock([indent(`return z.object({`), discriminator, shape, indent('})')].join('\n')) + .withBlock([indent(`return z.object({`), discriminator, shape, indent(`})${this.config.zodStrictObject ? '.strict()' : ''}`)].join('\n')) .string; } } } function generateFieldZodSchema(config: ValidationSchemaPluginConfig, visitor: Visitor, field: InputValueDefinitionNode | FieldDefinitionNode, indentCount: number, circularTypes: Set): string { - const gen = generateFieldTypeZodSchema(config, visitor, field, field.type, undefined, circularTypes); + const gen = generateFieldTypeZodSchema(config, visitor, field, field.type, circularTypes); return indent(`${field.name.value}: ${maybeLazy(field.type, gen, config, circularTypes)}`, indentCount); } -function generateFieldTypeZodSchema(config: ValidationSchemaPluginConfig, visitor: Visitor, field: InputValueDefinitionNode | FieldDefinitionNode, type: TypeNode, parentType?: TypeNode, circularTypes: Set): string { +function generateFieldTypeZodSchema(config: ValidationSchemaPluginConfig, visitor: Visitor, field: InputValueDefinitionNode | FieldDefinitionNode, type: TypeNode, circularTypes: Set, parentType?: TypeNode): string { if (isListType(type)) { - const gen = generateFieldTypeZodSchema(config, visitor, field, type.type, type, circularTypes); + const gen = generateFieldTypeZodSchema(config, visitor, field, type.type, circularTypes, type); if (!isNonNullType(parentType)) { const arrayGen = `z.array(${maybeLazy(type.type, gen, config, circularTypes)})`; const maybeLazyGen = applyDirectives(config, field, arrayGen); @@ -296,7 +296,7 @@ function generateFieldTypeZodSchema(config: ValidationSchemaPluginConfig, visito return `z.array(${maybeLazy(type.type, gen, config, circularTypes)})`; } if (isNonNullType(type)) { - const gen = generateFieldTypeZodSchema(config, visitor, field, type.type, type, circularTypes); + const gen = generateFieldTypeZodSchema(config, visitor, field, type.type, circularTypes, type); return maybeLazy(type.type, gen, config, circularTypes); } if (isNamedType(type)) { @@ -421,3 +421,11 @@ function zod4Scalar(config: ValidationSchemaPluginConfig, visitor: Visitor, scal console.warn('unhandled scalar name:', scalarName); return anySchema; } + +function generateSchemaObject(name: string, discriminatorField: string, typeName: string, shape: string | undefined) { + const objectName = name.charAt(0).toLowerCase() + name.slice(1) + 'SchemaObject' + return { + string: `export const ${objectName}: Properties<${typeName}> = {\n${discriminatorField}\n${shape}\n}\n\n`, + name: objectName, + } +} diff --git a/src/zodv4/index.ts b/src/zodv4/index.ts index 963ae0c8..8422f9cc 100644 --- a/src/zodv4/index.ts +++ b/src/zodv4/index.ts @@ -91,7 +91,7 @@ export class Zodv4SchemaVisitor extends BaseSchemaVisitor { .export() .asKind('const') .withName(`${name}Schema: z.ZodObject>`) - .withContent([`z.object({`, shape, '})'].join('\n')) + .withContent([`${this.config.zodStrictObject ? 'z.strictObject' : 'z.object'}({`, shape, '})'].join('\n')) .string + appendArguments ); @@ -102,7 +102,7 @@ export class Zodv4SchemaVisitor extends BaseSchemaVisitor { .export() .asKind('function') .withName(`${name}Schema(): z.ZodObject>`) - .withBlock([indent(`return z.object({`), shape, indent('})')].join('\n')) + .withBlock([indent(`return ${this.config.zodStrictObject ? 'z.strictObject' : 'z.object'}({`), shape, indent('})')].join('\n')) .string + appendArguments ); } @@ -112,7 +112,7 @@ export class Zodv4SchemaVisitor extends BaseSchemaVisitor { get ObjectTypeDefinition() { return { - leave: ObjectTypeDefinitionBuilder(this.config.withObjectType && !this.config.inputOnly, (node: ObjectTypeDefinitionNode) => { + leave: ObjectTypeDefinitionBuilder(this.config.withObjectType, (node: ObjectTypeDefinitionNode) => { const visitor = this.createVisitor('output'); const name = visitor.convertName(node.name.value); const typeName = visitor.prefixTypeNamespace(name); @@ -134,7 +134,7 @@ export class Zodv4SchemaVisitor extends BaseSchemaVisitor { .withName(`${name}Schema: z.ZodObject>`) .withContent( [ - `z.object({`, + `${this.config.zodStrictObject ? 'z.strictObject' : 'z.object'}({`, indent(`__typename: z.literal('${node.name.value}').optional(),`, 2), shape, '})', @@ -152,7 +152,7 @@ export class Zodv4SchemaVisitor extends BaseSchemaVisitor { .withName(`${name}Schema(): z.ZodObject>`) .withBlock( [ - indent(`return z.object({`), + indent(`return ${this.config.zodStrictObject ? 'z.strictObject' : 'z.object'}({`), indent(`__typename: z.literal('${node.name.value}').optional(),`, 2), shape, indent('})'), @@ -249,7 +249,7 @@ export class Zodv4SchemaVisitor extends BaseSchemaVisitor { .export() .asKind('const') .withName(`${name}Schema: z.ZodObject>`) - .withContent(['z.object({', discriminator, shape, '})'].join('\n')) + .withContent([`${this.config.zodStrictObject ? 'z.strictObject' : 'z.object'}({`, discriminator, shape, '})'].join('\n')) .string; case 'function': @@ -258,7 +258,7 @@ export class Zodv4SchemaVisitor extends BaseSchemaVisitor { .export() .asKind('function') .withName(`${name}Schema(): z.ZodObject>`) - .withBlock([indent(`return z.object({`), discriminator, shape, indent('})')].join('\n')) + .withBlock([indent(`return ${this.config.zodStrictObject ? 'z.strictObject' : 'z.object'}({`), discriminator, shape, indent('})')].join('\n')) .string; } }