From 8709b698722a54357fbe4d83b741599a4bbf93c2 Mon Sep 17 00:00:00 2001 From: Yoshiharu KAMATA Date: Mon, 21 Jul 2025 11:17:08 +0900 Subject: [PATCH] Support for Zod 4 --- README.md | 72 +- codegen.yml | 24 + example/zod/README.md | 2 +- example/zod/schemas.ts | 2 +- example/zodv4/README.md | 158 +++ example/zodv4/schemas.ts | 140 +++ package.json | 4 +- src/config.ts | 21 +- src/index.ts | 3 + src/yup/index.ts | 6 +- src/zod/index.ts | 3 +- src/zodv4/index.ts | 401 +++++++ tests/zod.spec.ts | 64 ++ tests/zodv4.spec.ts | 2230 ++++++++++++++++++++++++++++++++++++++ 14 files changed, 3123 insertions(+), 7 deletions(-) create mode 100644 example/zodv4/README.md create mode 100644 example/zodv4/schemas.ts create mode 100644 src/zodv4/index.ts create mode 100644 tests/zodv4.spec.ts diff --git a/README.md b/README.md index 6225d43a..f07ab5f3 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ - [x] support [yup](https://github.com/jquense/yup) - [x] support [zod](https://github.com/colinhacks/zod) +- [x] support [zod v4](https://github.com/colinhacks/zod) (zodv4) - [x] support [myzod](https://github.com/davidmdm/myzod) - [x] support [valibot](https://valibot.dev/) @@ -49,7 +50,7 @@ type: `ValidationSchema` default: `'yup'` Specify generete validation schema you want. -You can specify `yup` or `zod` or `myzod`. +You can specify `yup` or `zod` or `zodv4` or `myzod` or `valibot`. ```yml generates: @@ -61,6 +62,26 @@ generates: schema: yup ``` +### `zodImportPath` + +type: `string` default: `'zod'` + +Specifies a custom import path for the zod package. This is useful when you want to use a specific +version or subpath of zod, such as the v3 compatibility layer in zod v4 ('zod/v3'). +Only applies when schema is set to 'zod'. + +```yml +generates: + path/to/schemas.ts: + plugins: + - typescript + - typescript-validation-schema + config: + schema: zod + # Use zod v3 compatibility layer when using zod v4 + zodImportPath: zod/v3 +``` + ### `importFrom` type: `string` @@ -215,6 +236,16 @@ config: Email: z.string().email() ``` +#### zodv4 schema + +```yml +config: + schema: zodv4 + scalarSchemas: + Date: z.date() + Email: z.string().email() +``` + ### `defaultScalarTypeSchema` type: `string` @@ -235,6 +266,13 @@ config: defaultScalarSchema: z.unknown() ``` +#### zodv4 schema +```yml +config: + schema: zodv4 + defaultScalarSchema: z.unknown() +``` + ### `withObjectType` type: `boolean` default: `false` @@ -360,6 +398,38 @@ export function ExampleInputSchema(): z.ZodSchema { } ``` +#### zodv4 schema + +```yml +generates: + path/to/graphql.ts: + plugins: + - typescript + - typescript-validation-schema + config: + schema: zodv4 + directives: + constraint: + minLength: min + # Replace $1 with specified `startsWith` argument value of the constraint directive + startsWith: [regex, /^$1/, message] + format: + # This example means `validation-schema: directive-arg` + # directive-arg is supported String and Enum. + email: email +``` + +Then generates zodv4 validation schema like below. + +```ts +export function ExampleInputSchema(): z.ZodObject> { + return z.object({ + email: z.string().min(50).email(), + message: z.string().regex(/^Hello/, 'message') + }) +} +``` + #### other schema Please see [example](https://github.com/Code-Hex/graphql-codegen-typescript-validation-schema/tree/main/example) directory. diff --git a/codegen.yml b/codegen.yml index a0d3949f..7d98d7a8 100644 --- a/codegen.yml +++ b/codegen.yml @@ -47,6 +47,30 @@ generates: plugins: - ./dist/cjs/index.js: schema: zod + zodImportPath: zod/v3 + importFrom: ../types + withObjectType: true + directives: + # Write directives like + # + # directive: + # arg1: schemaApi + # arg2: ["schemaApi2", "Hello $1"] + # + # See more examples in `./tests/directive.spec.ts` + # https://github.com/Code-Hex/graphql-codegen-typescript-validation-schema/blob/main/tests/directive.spec.ts + constraint: + minLength: min + # Replace $1 with specified `startsWith` argument value of the constraint directive + startsWith: [regex, /^$1/, message] + format: + email: email + scalars: + ID: string + example/zodv4/schemas.ts: + plugins: + - ./dist/cjs/index.js: + schema: zodv4 importFrom: ../types withObjectType: true directives: diff --git a/example/zod/README.md b/example/zod/README.md index 53916fa5..b5f4e369 100644 --- a/example/zod/README.md +++ b/example/zod/README.md @@ -2,7 +2,7 @@ ## How to overwrite generated schema? -You can use zod [extend API](https://github.com/colinhacks/zod#extend). +You can use zod [extend API](https://v3.zod.dev/?id=extend). ```ts const AttributeInputSchemaWithCUID = AttributeInputSchema().extend({ diff --git a/example/zod/schemas.ts b/example/zod/schemas.ts index 407e3895..c1bea2b2 100644 --- a/example/zod/schemas.ts +++ b/example/zod/schemas.ts @@ -1,4 +1,4 @@ -import { z } from 'zod' +import { z } from 'zod/v3' import { Admin, AttributeInput, ButtonComponentType, ComponentInput, DropDownComponentInput, EventArgumentInput, EventInput, EventOptionType, Guest, HttpInput, HttpMethod, LayoutInput, MyType, MyTypeFooArgs, Namer, PageInput, PageType, User } from '../types' type Properties = Required<{ diff --git a/example/zodv4/README.md b/example/zodv4/README.md new file mode 100644 index 00000000..fed7d7d0 --- /dev/null +++ b/example/zodv4/README.md @@ -0,0 +1,158 @@ +# Tips for zod v4 schema + +## Overview + +The `zodv4` schema type is designed to work with Zod v4, which introduces significant changes to the type system and API. This implementation uses the updated type definitions and APIs that are compatible with Zod v4. + +## Key Differences from Zod v3 + +### Type System Changes + +Zod v4 introduces changes to the `ZodType` generic parameters: + +```ts +// Zod v3 +z.ZodType + +// Zod v4 +z.ZodType +``` + +The `Properties` type definition has been updated accordingly: + +```ts +// Updated for Zod v4 +type Properties = Required<{ + [K in keyof T]: z.ZodType; +}>; +``` + +### Enum Handling + +Zod v4 changes how enums are handled: + +```ts +// Zod v3 +z.nativeEnum(ButtonComponentType) + +// Zod v4 +z.enum(ButtonComponentType) +``` + +## How to overwrite generated schema? + +You can use zod [extend API](https://zod.dev/api#extend), same as with Zod v3: + +```ts +const AttributeInputSchemaWithCUID = AttributeInputSchema().extend({ + key: z.string().cuid(), +}); +``` + +## Apply input validator via ts decorator + +Validate the input object via typescript decorators when implementing resolvers. The implementation is compatible with Zod v4's type system: + +### Usage + +```ts +class Mutation { + @validateInput(SignupInputSchema) + async signup( + _root: Record, + { input: { email, password } }: MutationSignupArgs, + context: Context + ): Promise { + // The input here is automatically valid to adhere to SignupInputSchema + } +} +``` + +### Implementation: + +```ts +type ZodResolver> = ResolverFn< + any, + any, + any, + { input: TypeOf } +> + +/** + * Method decorator that validates the argument of the target function against the given schema. + * Updated for Zod v4 type system. + * + * @export + * @template T The type of the zod schema. + * @param {T} arg The zod schema used for the validation. + * @return {MethodDecorator} A {@link MethodDecorator}. + */ +export function validateInput( + arg: T | (() => T) +): MethodDecorator> { + return function (_target, _propertyKey, descriptor) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const originalMethod = descriptor.value! + // @ts-expect-error: should be fine + descriptor.value = function (root, { input }, context, info) { + const schema = typeof arg === 'function' ? arg() : arg + const result = schema.safeParse(input) + + if (result.success) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return originalMethod.call( + this, + root, + { input: result.data }, + context, + info + ) + } else { + return { problems: result.error.issues } + } + } + return descriptor + } +} +``` + +## Migration from Zod v3 + +If you're migrating from Zod v3 to v4, consider the following: + +### 1. Using Zod v4 with v3 compatibility layer + +You can use the `zodImportPath` option to import from Zod v4's v3 compatibility layer: + +```yml +generates: + path/to/schemas.ts: + plugins: + - graphql-codegen-validation-schema + config: + schema: zod + zodImportPath: zod/v3 # Use v3 compatibility layer +``` + +### 2. Using zodv4 schema type + +Alternatively, use the `zodv4` schema type for full Zod v4 compatibility: + +```yml +generates: + path/to/schemas.ts: + plugins: + - graphql-codegen-validation-schema + config: + schema: zodv4 # Use zodv4 schema type +``` + +## Performance and Type Safety + +Zod v4 provides improvements in: + +1. **Stricter Type Checking**: Enhanced type safety with simplified generic parameters +2. **Better API Design**: More intuitive and consistent API +3. **Internal Optimizations**: Performance improvements in validation logic + +These changes result in more reliable and maintainable validation code. diff --git a/example/zodv4/schemas.ts b/example/zodv4/schemas.ts new file mode 100644 index 00000000..237ae4d0 --- /dev/null +++ b/example/zodv4/schemas.ts @@ -0,0 +1,140 @@ +import * as z from 'zod' +import { Admin, AttributeInput, ButtonComponentType, ComponentInput, DropDownComponentInput, EventArgumentInput, EventInput, EventOptionType, Guest, HttpInput, HttpMethod, LayoutInput, MyType, MyTypeFooArgs, Namer, PageInput, PageType, User } from '../types' + +type Properties = Required<{ + [K in keyof T]: z.ZodType; +}>; + +type definedNonNullAny = {}; + +export const isDefinedNonNullAny = (v: any): v is definedNonNullAny => v !== undefined && v !== null; + +export const definedNonNullAnySchema = z.any().refine((v) => isDefinedNonNullAny(v)); + +export const ButtonComponentTypeSchema = z.enum(ButtonComponentType); + +export const EventOptionTypeSchema = z.enum(EventOptionType); + +export const HttpMethodSchema = z.enum(HttpMethod); + +export const PageTypeSchema = z.enum(PageType); + +export function AdminSchema(): z.ZodObject> { + return z.object({ + __typename: z.literal('Admin').optional(), + lastModifiedAt: definedNonNullAnySchema.nullish() + }) +} + +export function AttributeInputSchema(): z.ZodObject> { + return z.object({ + key: z.string().nullish(), + val: z.string().nullish() + }) +} + +export function ComponentInputSchema(): z.ZodObject> { + return z.object({ + child: z.lazy(() => ComponentInputSchema().nullish()), + childrens: z.array(z.lazy(() => ComponentInputSchema().nullable())).nullish(), + event: z.lazy(() => EventInputSchema().nullish()), + name: z.string(), + type: ButtonComponentTypeSchema + }) +} + +export function DropDownComponentInputSchema(): z.ZodObject> { + return z.object({ + dropdownComponent: z.lazy(() => ComponentInputSchema().nullish()), + getEvent: z.lazy(() => EventInputSchema()) + }) +} + +export function EventArgumentInputSchema(): z.ZodObject> { + return z.object({ + name: z.string().min(5), + value: z.string().regex(/^foo/, "message") + }) +} + +export function EventInputSchema(): z.ZodObject> { + return z.object({ + arguments: z.array(z.lazy(() => EventArgumentInputSchema())), + options: z.array(EventOptionTypeSchema).nullish() + }) +} + +export function GuestSchema(): z.ZodObject> { + return z.object({ + __typename: z.literal('Guest').optional(), + lastLoggedIn: definedNonNullAnySchema.nullish() + }) +} + +export function HttpInputSchema(): z.ZodObject> { + return z.object({ + method: HttpMethodSchema.nullish(), + url: definedNonNullAnySchema + }) +} + +export function LayoutInputSchema(): z.ZodObject> { + return z.object({ + dropdown: z.lazy(() => DropDownComponentInputSchema().nullish()) + }) +} + +export function MyTypeSchema(): z.ZodObject> { + return z.object({ + __typename: z.literal('MyType').optional(), + foo: z.string().nullish() + }) +} + +export function MyTypeFooArgsSchema(): z.ZodObject> { + return z.object({ + a: z.string().nullish(), + b: z.number(), + c: z.boolean().nullish(), + d: z.number() + }) +} + +export function NamerSchema(): z.ZodObject> { + return z.object({ + name: z.string().nullish() + }) +} + +export function PageInputSchema(): z.ZodObject> { + return z.object({ + attributes: z.array(z.lazy(() => AttributeInputSchema())).nullish(), + date: definedNonNullAnySchema.nullish(), + height: z.number(), + id: z.string(), + layout: z.lazy(() => LayoutInputSchema()), + pageType: PageTypeSchema, + postIDs: z.array(z.string()).nullish(), + show: z.boolean(), + tags: z.array(z.string().nullable()).nullish(), + title: z.string(), + width: z.number() + }) +} + +export function UserSchema(): z.ZodObject> { + return z.object({ + __typename: z.literal('User').optional(), + createdAt: definedNonNullAnySchema.nullish(), + email: z.string().nullish(), + id: z.string().nullish(), + kind: UserKindSchema().nullish(), + name: z.string().nullish(), + password: z.string().nullish(), + updatedAt: definedNonNullAnySchema.nullish() + }) +} + +export function UserKindSchema() { + return z.union([AdminSchema(), GuestSchema()]) +} diff --git a/package.json b/package.json index 27efcd56..1012f942 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "type-check": "tsc --noEmit", "type-check:yup": "tsc --strict --skipLibCheck --noEmit example/yup/schemas.ts", "type-check:zod": "tsc --strict --skipLibCheck --noEmit example/zod/schemas.ts", + "type-check:zodv4": "tsc --strict --skipLibCheck --noEmit example/zodv4/schemas.ts", "type-check:myzod": "tsc --strict --skipLibCheck --noEmit example/myzod/schemas.ts", "type-check:valibot": "tsc --strict --skipLibCheck --noEmit example/valibot/schemas.ts", "test": "vitest run", @@ -74,7 +75,8 @@ "prepublish": "run-p build:*" }, "peerDependencies": { - "graphql": "^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" + "graphql": "^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0", + "zod": "^3.25.0 || ^4.0.0" }, "dependencies": { "@graphql-codegen/plugin-helpers": "^5.0.0", diff --git a/src/config.ts b/src/config.ts index d6ec9f2b..f75ab40f 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' | 'zodv4' | 'myzod' | 'valibot'; export type ValidationSchemaExportType = 'function' | 'const'; export interface DirectiveConfig { @@ -19,6 +19,25 @@ interface ScalarSchemas { } export interface ValidationSchemaPluginConfig extends TypeScriptPluginConfig { + /** + * @description Specifies a custom import path for the zod package. This is useful when you want to use a specific + * version or subpath of zod, such as the v3 compatibility layer in zod v4 ('zod/v3'). + * Only applies when schema is set to 'zod'. + * @default 'zod' + * + * @exampleMarkdown + * ```yml + * generates: + * path/to/schemas.ts: + * plugins: + * - graphql-codegen-validation-schema + * config: + * schema: zod + * # Use zod v3 compatibility layer when using zod v4 + * zodImportPath: zod/v3 + * ``` + */ + zodImportPath?: string /** * @description specify generate schema * @default yup diff --git a/src/index.ts b/src/index.ts index 39dd1e8a..7a37065e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,6 +10,7 @@ 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 +33,8 @@ export const plugin: PluginFunction') + .withContent(['Required<{', ' [K in keyof T]: z.ZodType;', '}>'].join('\n')) + .string, + // Unfortunately, zod doesn’t provide non-null defined any schema. + // This is a temporary hack until it is fixed. + // see: https://github.com/colinhacks/zod/issues/884 + new DeclarationBlock({}).asKind('type').withName('definedNonNullAny').withContent('{}').string, + new DeclarationBlock({}) + .export() + .asKind('const') + .withName(`isDefinedNonNullAny`) + .withContent(`(v: any): v is definedNonNullAny => v !== undefined && v !== null`) + .string, + new DeclarationBlock({}) + .export() + .asKind('const') + .withName(`${anySchema}`) + .withContent(`z.any().refine((v) => isDefinedNonNullAny(v))`) + .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, (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( + new DeclarationBlock({}) + .export() + .asKind('const') + .withName(`${enumname}Schema`) + .withContent( + this.config.enumsAsTypes + ? `z.enum([${node.values?.map(enumOption => `'${enumOption.name.value}'`).join(', ')}])` + : `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 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({', shape, '})'].join('\n')) + .string; + + case 'function': + default: + return new DeclarationBlock({}) + .export() + .asKind('function') + .withName(`${name}Schema(): z.ZodObject>`) + .withBlock([indent(`return z.object({`), 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 anySchema; +} diff --git a/tests/zod.spec.ts b/tests/zod.spec.ts index b025ae04..9d76346e 100644 --- a/tests/zod.spec.ts +++ b/tests/zod.spec.ts @@ -1797,4 +1797,68 @@ describe('zod', () => { " `) }); + + it('with zodImportPath', async () => { + const schema = buildSchema(/* GraphQL */ ` + input Say { + phrase: String! + } + `); + const result = await plugin( + schema, + [], + { + schema: 'zod', + zodImportPath: 'zod/v3', + }, + {}, + ); + expect(result.prepend).toMatchInlineSnapshot(` + [ + "import { z } from 'zod/v3'", + ] + `); + expect(removedInitialEmitValue(result.content)).toMatchInlineSnapshot(` + " + export function SaySchema(): z.ZodObject> { + return z.object({ + phrase: z.string() + }) + } + " + `); + }); + + it('with zodImportPath and importFrom', async () => { + const schema = buildSchema(/* GraphQL */ ` + input Say { + phrase: String! + } + `); + const result = await plugin( + schema, + [], + { + schema: 'zod', + zodImportPath: 'zod/v3', + importFrom: './types', + }, + {}, + ); + expect(result.prepend).toMatchInlineSnapshot(` + [ + "import { z } from 'zod/v3'", + "import { Say } from './types'", + ] + `); + expect(removedInitialEmitValue(result.content)).toMatchInlineSnapshot(` + " + export function SaySchema(): z.ZodObject> { + return z.object({ + phrase: z.string() + }) + } + " + `); + }); }); diff --git a/tests/zodv4.spec.ts b/tests/zodv4.spec.ts new file mode 100644 index 00000000..2cc2a0c4 --- /dev/null +++ b/tests/zodv4.spec.ts @@ -0,0 +1,2230 @@ +import { buildClientSchema, buildSchema, introspectionFromSchema } from 'graphql'; +import { dedent } from 'ts-dedent'; + +import { plugin } from '../src/index'; + +const initialEmitValue = dedent(` + type Properties = Required<{ + [K in keyof T]: z.ZodType; + }>; + + type definedNonNullAny = {}; + + export const isDefinedNonNullAny = (v: any): v is definedNonNullAny => v !== undefined && v !== null; + + export const definedNonNullAnySchema = z.any().refine((v) => isDefinedNonNullAny(v)); + + + `) + +function removedInitialEmitValue(content: string) { + return content.replace(initialEmitValue, ''); +} + +describe('zodv4', () => { + it('non-null and defined', async () => { + const schema = buildSchema(/* GraphQL */ ` + input PrimitiveInput { + a: ID! + b: String! + c: Boolean! + d: Int! + e: Float! + } + `); + const scalars = { + ID: 'string', + } + const result = await plugin(schema, [], { schema: 'zodv4', scalars }, {}); + expect(result.prepend).toMatchInlineSnapshot(` + [ + "import * as z from 'zod'", + ] + `); + + expect(removedInitialEmitValue(result.content)).toMatchInlineSnapshot(` + " + type Properties = Required<{ + [K in keyof T]: z.ZodType; + }>; + + type definedNonNullAny = {}; + + export const isDefinedNonNullAny = (v: any): v is definedNonNullAny => v !== undefined && v !== null; + + export const definedNonNullAnySchema = z.any().refine((v) => isDefinedNonNullAny(v)); + + export function PrimitiveInputSchema(): z.ZodObject> { + return z.object({ + a: z.string(), + b: z.string(), + c: z.boolean(), + d: z.number(), + e: z.number() + }) + } + " + `); + }) + + it('nullish', async () => { + const schema = buildSchema(/* GraphQL */ ` + input PrimitiveInput { + a: ID + b: String + c: Boolean + d: Int + e: Float + z: String! # no defined check + } + `); + const scalars = { + ID: 'string', + } + const result = await plugin(schema, [], { schema: 'zodv4', scalars }, {}); + + expect(removedInitialEmitValue(result.content)).toMatchInlineSnapshot(` + " + type Properties = Required<{ + [K in keyof T]: z.ZodType; + }>; + + type definedNonNullAny = {}; + + export const isDefinedNonNullAny = (v: any): v is definedNonNullAny => v !== undefined && v !== null; + + export const definedNonNullAnySchema = z.any().refine((v) => isDefinedNonNullAny(v)); + + export function PrimitiveInputSchema(): z.ZodObject> { + return z.object({ + a: z.string().nullish(), + b: z.string().nullish(), + c: z.boolean().nullish(), + d: z.number().nullish(), + e: z.number().nullish(), + z: z.string() + }) + } + " + `) + }) + + it('array', async () => { + const schema = buildSchema(/* GraphQL */ ` + input ArrayInput { + a: [String] + b: [String!] + c: [String!]! + d: [[String]] + e: [[String]!] + f: [[String]!]! + } + `); + const scalars = undefined + const result = await plugin(schema, [], { schema: 'zodv4', scalars }, {}); + expect(removedInitialEmitValue(result.content)).toMatchInlineSnapshot(` + " + type Properties = Required<{ + [K in keyof T]: z.ZodType; + }>; + + type definedNonNullAny = {}; + + export const isDefinedNonNullAny = (v: any): v is definedNonNullAny => v !== undefined && v !== null; + + export const definedNonNullAnySchema = z.any().refine((v) => isDefinedNonNullAny(v)); + + export function ArrayInputSchema(): z.ZodObject> { + return z.object({ + a: z.array(z.string().nullable()).nullish(), + b: z.array(z.string()).nullish(), + c: z.array(z.string()), + d: z.array(z.array(z.string().nullable()).nullish()).nullish(), + e: z.array(z.array(z.string().nullable())).nullish(), + f: z.array(z.array(z.string().nullable())) + }) + } + " + `) + }) + + it('ref input object', async () => { + const schema = buildSchema(/* GraphQL */ ` + input AInput { + b: BInput! + } + input BInput { + c: CInput! + } + input CInput { + a: AInput! + } + `); + const scalars = undefined + const result = await plugin(schema, [], { schema: 'zodv4', scalars }, {}); + expect(removedInitialEmitValue(result.content)).toMatchInlineSnapshot(` + " + type Properties = Required<{ + [K in keyof T]: z.ZodType; + }>; + + type definedNonNullAny = {}; + + export const isDefinedNonNullAny = (v: any): v is definedNonNullAny => v !== undefined && v !== null; + + export const definedNonNullAnySchema = z.any().refine((v) => isDefinedNonNullAny(v)); + + export function AInputSchema(): z.ZodObject> { + return z.object({ + b: z.lazy(() => BInputSchema()) + }) + } + + export function BInputSchema(): z.ZodObject> { + return z.object({ + c: z.lazy(() => CInputSchema()) + }) + } + + export function CInputSchema(): z.ZodObject> { + return z.object({ + a: z.lazy(() => AInputSchema()) + }) + } + " + `) + }) + + it('ref input object w/ schemaNamespacedImportName', async () => { + const schema = buildSchema(/* GraphQL */ ` + input AInput { + b: BInput! + } + input BInput { + c: CInput! + } + input CInput { + a: AInput! + } + `); + const scalars = undefined + const result = await plugin(schema, [], { schema: 'zodv4', scalars, importFrom: './types', schemaNamespacedImportName: 't' }, {}); + expect(removedInitialEmitValue(result.content)).toMatchInlineSnapshot(` + " + type Properties = Required<{ + [K in keyof T]: z.ZodType; + }>; + + type definedNonNullAny = {}; + + export const isDefinedNonNullAny = (v: any): v is definedNonNullAny => v !== undefined && v !== null; + + export const definedNonNullAnySchema = z.any().refine((v) => isDefinedNonNullAny(v)); + + export function AInputSchema(): z.ZodObject> { + return z.object({ + b: z.lazy(() => BInputSchema()) + }) + } + + export function BInputSchema(): z.ZodObject> { + return z.object({ + c: z.lazy(() => CInputSchema()) + }) + } + + export function CInputSchema(): z.ZodObject> { + return z.object({ + a: z.lazy(() => AInputSchema()) + }) + } + " + `) + }) + + it('nested input object', async () => { + const schema = buildSchema(/* GraphQL */ ` + input NestedInput { + child: NestedInput + childrens: [NestedInput] + } + `); + const scalars = undefined + const result = await plugin(schema, [], { schema: 'zodv4', scalars }, {}); + expect(removedInitialEmitValue(result.content)).toMatchInlineSnapshot(` + " + type Properties = Required<{ + [K in keyof T]: z.ZodType; + }>; + + type definedNonNullAny = {}; + + export const isDefinedNonNullAny = (v: any): v is definedNonNullAny => v !== undefined && v !== null; + + export const definedNonNullAnySchema = z.any().refine((v) => isDefinedNonNullAny(v)); + + export function NestedInputSchema(): z.ZodObject> { + return z.object({ + child: z.lazy(() => NestedInputSchema().nullish()), + childrens: z.array(z.lazy(() => NestedInputSchema().nullable())).nullish() + }) + } + " + `) + }) + + it('enum', async () => { + const schema = buildSchema(/* GraphQL */ ` + enum PageType { + PUBLIC + BASIC_AUTH + } + input PageInput { + pageType: PageType! + } + `); + const scalars = undefined + const result = await plugin(schema, [], { schema: 'zodv4', scalars }, {}); + expect(removedInitialEmitValue(result.content)).toMatchInlineSnapshot(` + " + type Properties = Required<{ + [K in keyof T]: z.ZodType; + }>; + + type definedNonNullAny = {}; + + export const isDefinedNonNullAny = (v: any): v is definedNonNullAny => v !== undefined && v !== null; + + export const definedNonNullAnySchema = z.any().refine((v) => isDefinedNonNullAny(v)); + + export const PageTypeSchema = z.enum(PageType); + + export function PageInputSchema(): z.ZodObject> { + return z.object({ + pageType: PageTypeSchema + }) + } + " + `) + }) + + it('enum w/ schemaNamespacedImportName', async () => { + const schema = buildSchema(/* GraphQL */ ` + enum PageType { + PUBLIC + BASIC_AUTH + } + input PageInput { + pageType: PageType! + } + `); + const scalars = undefined + const result = await plugin(schema, [], { schema: 'zodv4', scalars, importFrom: './', schemaNamespacedImportName: 't' }, {}); + expect(removedInitialEmitValue(result.content)).toMatchInlineSnapshot(` + " + type Properties = Required<{ + [K in keyof T]: z.ZodType; + }>; + + type definedNonNullAny = {}; + + export const isDefinedNonNullAny = (v: any): v is definedNonNullAny => v !== undefined && v !== null; + + export const definedNonNullAnySchema = z.any().refine((v) => isDefinedNonNullAny(v)); + + export const PageTypeSchema = z.enum(t.PageType); + + export function PageInputSchema(): z.ZodObject> { + return z.object({ + pageType: PageTypeSchema + }) + } + " + `) + }) + + it('camelcase', async () => { + const schema = buildSchema(/* GraphQL */ ` + input HTTPInput { + method: HTTPMethod + url: URL! + } + + enum HTTPMethod { + GET + POST + } + + scalar URL # unknown scalar, should be any (definedNonNullAnySchema) + `); + const scalars = undefined + const result = await plugin(schema, [], { schema: 'zodv4', scalars }, {}); + expect(removedInitialEmitValue(result.content)).toMatchInlineSnapshot(` + " + type Properties = Required<{ + [K in keyof T]: z.ZodType; + }>; + + type definedNonNullAny = {}; + + export const isDefinedNonNullAny = (v: any): v is definedNonNullAny => v !== undefined && v !== null; + + export const definedNonNullAnySchema = z.any().refine((v) => isDefinedNonNullAny(v)); + + export const HttpMethodSchema = z.enum(HttpMethod); + + export function HttpInputSchema(): z.ZodObject> { + return z.object({ + method: HttpMethodSchema.nullish(), + url: definedNonNullAnySchema + }) + } + " + `) + }) + + it('with scalars', async () => { + const schema = buildSchema(/* GraphQL */ ` + input Say { + phrase: Text! + times: Count! + } + + scalar Count + scalar Text + `); + const result = await plugin( + schema, + [], + { + schema: 'zodv4', + scalars: { + Text: 'string', + Count: 'number', + }, + }, + {}, + ); + expect(removedInitialEmitValue(result.content)).toMatchInlineSnapshot(` + " + type Properties = Required<{ + [K in keyof T]: z.ZodType; + }>; + + type definedNonNullAny = {}; + + export const isDefinedNonNullAny = (v: any): v is definedNonNullAny => v !== undefined && v !== null; + + export const definedNonNullAnySchema = z.any().refine((v) => isDefinedNonNullAny(v)); + + export function SaySchema(): z.ZodObject> { + return z.object({ + phrase: z.string(), + times: z.number() + }) + } + " + `) + }); + + it('with importFrom', async () => { + const schema = buildSchema(/* GraphQL */ ` + input Say { + phrase: String! + } + `); + const result = await plugin( + schema, + [], + { + schema: 'zodv4', + importFrom: './types', + }, + {}, + ); + expect(result.prepend).toMatchInlineSnapshot(` + [ + "import * as z from 'zod'", + "import { Say } from './types'", + ] + `); + expect(removedInitialEmitValue(result.content)).toMatchInlineSnapshot(` + " + type Properties = Required<{ + [K in keyof T]: z.ZodType; + }>; + + type definedNonNullAny = {}; + + export const isDefinedNonNullAny = (v: any): v is definedNonNullAny => v !== undefined && v !== null; + + export const definedNonNullAnySchema = z.any().refine((v) => isDefinedNonNullAny(v)); + + export function SaySchema(): z.ZodObject> { + return z.object({ + phrase: z.string() + }) + } + " + `) + }); + + it('with importFrom & useTypeImports', async () => { + const schema = buildSchema(/* GraphQL */ ` + input Say { + phrase: String! + } + `); + const result = await plugin( + schema, + [], + { + schema: 'zodv4', + importFrom: './types', + useTypeImports: true, + }, + {}, + ); + expect(result.prepend).toMatchInlineSnapshot(` + [ + "import * as z from 'zod'", + "import type { Say } from './types'", + ] + `); + expect(removedInitialEmitValue(result.content)).toMatchInlineSnapshot(` + " + type Properties = Required<{ + [K in keyof T]: z.ZodType; + }>; + + type definedNonNullAny = {}; + + export const isDefinedNonNullAny = (v: any): v is definedNonNullAny => v !== undefined && v !== null; + + export const definedNonNullAnySchema = z.any().refine((v) => isDefinedNonNullAny(v)); + + export function SaySchema(): z.ZodObject> { + return z.object({ + phrase: z.string() + }) + } + " + `) + }); + + it('with importFrom & schemaNamespacedImportName', async () => { + const schema = buildSchema(/* GraphQL */ ` + input Say { + phrase: String! + } + `); + const result = await plugin( + schema, + [], + { + schema: 'zodv4', + importFrom: './types', + schemaNamespacedImportName: 't', + }, + {}, + ); + expect(result.prepend).toMatchInlineSnapshot(` + [ + "import * as z from 'zod'", + "import * as t from './types'", + ] + `); + expect(removedInitialEmitValue(result.content)).toMatchInlineSnapshot(` + " + type Properties = Required<{ + [K in keyof T]: z.ZodType; + }>; + + type definedNonNullAny = {}; + + export const isDefinedNonNullAny = (v: any): v is definedNonNullAny => v !== undefined && v !== null; + + export const definedNonNullAnySchema = z.any().refine((v) => isDefinedNonNullAny(v)); + + export function SaySchema(): z.ZodObject> { + return z.object({ + phrase: z.string() + }) + } + " + `) + }); + + it('with enumsAsTypes', async () => { + const schema = buildSchema(/* GraphQL */ ` + enum PageType { + PUBLIC + BASIC_AUTH + } + `); + const result = await plugin( + schema, + [], + { + schema: 'zodv4', + enumsAsTypes: true, + }, + {}, + ); + expect(removedInitialEmitValue(result.content)).toMatchInlineSnapshot(` + " + type Properties = Required<{ + [K in keyof T]: z.ZodType; + }>; + + type definedNonNullAny = {}; + + export const isDefinedNonNullAny = (v: any): v is definedNonNullAny => v !== undefined && v !== null; + + export const definedNonNullAnySchema = z.any().refine((v) => isDefinedNonNullAny(v)); + + export const PageTypeSchema = z.enum(['PUBLIC', 'BASIC_AUTH']); + " + `) + }); + + it('with enumsAsTypes + schemaNamespacedImportName', async () => { + const schema = buildSchema(/* GraphQL */ ` + enum PageType { + PUBLIC + BASIC_AUTH + } + `); + const result = await plugin( + schema, + [], + { + schema: 'zodv4', + enumsAsTypes: true, + importFrom: './types', + schemaNamespacedImportName: 't', + }, + {}, + ); + expect(removedInitialEmitValue(result.content)).toMatchInlineSnapshot(` + " + type Properties = Required<{ + [K in keyof T]: z.ZodType; + }>; + + type definedNonNullAny = {}; + + export const isDefinedNonNullAny = (v: any): v is definedNonNullAny => v !== undefined && v !== null; + + export const definedNonNullAnySchema = z.any().refine((v) => isDefinedNonNullAny(v)); + + export const PageTypeSchema = z.enum(['PUBLIC', 'BASIC_AUTH']); + " + `) + }); + + it('with notAllowEmptyString', async () => { + const schema = buildSchema(/* GraphQL */ ` + input PrimitiveInput { + a: ID! + b: String! + c: Boolean! + d: Int! + e: Float! + } + `); + const result = await plugin( + schema, + [], + { + schema: 'zodv4', + notAllowEmptyString: true, + scalars: { + ID: 'string', + }, + }, + {}, + ); + expect(removedInitialEmitValue(result.content)).toMatchInlineSnapshot(` + " + type Properties = Required<{ + [K in keyof T]: z.ZodType; + }>; + + type definedNonNullAny = {}; + + export const isDefinedNonNullAny = (v: any): v is definedNonNullAny => v !== undefined && v !== null; + + export const definedNonNullAnySchema = z.any().refine((v) => isDefinedNonNullAny(v)); + + export function PrimitiveInputSchema(): z.ZodObject> { + return z.object({ + a: z.string().min(1), + b: z.string().min(1), + c: z.boolean(), + d: z.number(), + e: z.number() + }) + } + " + `) + }); + + it('with notAllowEmptyString issue #386', async () => { + const schema = buildSchema(/* GraphQL */ ` + input InputOne { + field: InputNested! + } + + input InputNested { + field: String! + } + `); + const result = await plugin( + schema, + [], + { + schema: 'zodv4', + notAllowEmptyString: true, + scalars: { + ID: 'string', + }, + }, + {}, + ); + expect(removedInitialEmitValue(result.content)).toMatchInlineSnapshot(` + " + type Properties = Required<{ + [K in keyof T]: z.ZodType; + }>; + + type definedNonNullAny = {}; + + export const isDefinedNonNullAny = (v: any): v is definedNonNullAny => v !== undefined && v !== null; + + export const definedNonNullAnySchema = z.any().refine((v) => isDefinedNonNullAny(v)); + + export function InputOneSchema(): z.ZodObject> { + return z.object({ + field: z.lazy(() => InputNestedSchema()) + }) + } + + export function InputNestedSchema(): z.ZodObject> { + return z.object({ + field: z.string().min(1) + }) + } + " + `) + }); + + it('with scalarSchemas', async () => { + const schema = buildSchema(/* GraphQL */ ` + input ScalarsInput { + date: Date! + email: Email + str: String! + } + scalar Date + scalar Email + `); + const result = await plugin( + schema, + [], + { + schema: 'zodv4', + scalarSchemas: { + Date: 'z.date()', + Email: 'z.string().email()', + }, + }, + {}, + ); + expect(removedInitialEmitValue(result.content)).toMatchInlineSnapshot(` + " + type Properties = Required<{ + [K in keyof T]: z.ZodType; + }>; + + type definedNonNullAny = {}; + + export const isDefinedNonNullAny = (v: any): v is definedNonNullAny => v !== undefined && v !== null; + + export const definedNonNullAnySchema = z.any().refine((v) => isDefinedNonNullAny(v)); + + export function ScalarsInputSchema(): z.ZodObject> { + return z.object({ + date: z.date(), + email: z.string().email().nullish(), + str: z.string() + }) + } + " + `) + }); + + it('with defaultScalarTypeSchema', async () => { + const schema = buildSchema(/* GraphQL */ ` + input ScalarsInput { + date: Date! + email: Email + str: String! + } + scalar Date + scalar Email + `); + const result = await plugin( + schema, + [], + { + schema: 'zodv4', + scalarSchemas: { + Email: 'z.string().email()', + }, + defaultScalarTypeSchema: 'z.string()', + }, + {}, + ); + expect(removedInitialEmitValue(result.content)).toMatchInlineSnapshot(` + " + type Properties = Required<{ + [K in keyof T]: z.ZodType; + }>; + + type definedNonNullAny = {}; + + export const isDefinedNonNullAny = (v: any): v is definedNonNullAny => v !== undefined && v !== null; + + export const definedNonNullAnySchema = z.any().refine((v) => isDefinedNonNullAny(v)); + + export function ScalarsInputSchema(): z.ZodObject> { + return z.object({ + date: z.string(), + email: z.string().email().nullish(), + str: z.string() + }) + } + " + `) + }); + + it('with typesPrefix', async () => { + const schema = buildSchema(/* GraphQL */ ` + input Say { + phrase: String! + } + `); + const result = await plugin( + schema, + [], + { + schema: 'zodv4', + typesPrefix: 'I', + importFrom: './types', + }, + {}, + ); + expect(result.prepend).toMatchInlineSnapshot(` + [ + "import * as z from 'zod'", + "import { ISay } from './types'", + ] + `); + expect(removedInitialEmitValue(result.content)).toMatchInlineSnapshot(` + " + type Properties = Required<{ + [K in keyof T]: z.ZodType; + }>; + + type definedNonNullAny = {}; + + export const isDefinedNonNullAny = (v: any): v is definedNonNullAny => v !== undefined && v !== null; + + export const definedNonNullAnySchema = z.any().refine((v) => isDefinedNonNullAny(v)); + + export function ISaySchema(): z.ZodObject> { + return z.object({ + phrase: z.string() + }) + } + " + `) + }); + + it('with typesSuffix', async () => { + const schema = buildSchema(/* GraphQL */ ` + input Say { + phrase: String! + } + `); + const result = await plugin( + schema, + [], + { + schema: 'zodv4', + typesSuffix: 'I', + importFrom: './types', + }, + {}, + ); + expect(result.prepend).toMatchInlineSnapshot(` + [ + "import * as z from 'zod'", + "import { SayI } from './types'", + ] + `); + expect(removedInitialEmitValue(result.content)).toMatchInlineSnapshot(` + " + type Properties = Required<{ + [K in keyof T]: z.ZodType; + }>; + + type definedNonNullAny = {}; + + export const isDefinedNonNullAny = (v: any): v is definedNonNullAny => v !== undefined && v !== null; + + export const definedNonNullAnySchema = z.any().refine((v) => isDefinedNonNullAny(v)); + + export function SayISchema(): z.ZodObject> { + return z.object({ + phrase: z.string() + }) + } + " + `) + }); + + it('with default input values as enum types', async () => { + const schema = buildSchema(/* GraphQL */ ` + enum PageType { + PUBLIC + BASIC_AUTH + } + input PageInput { + pageType: PageType! = PUBLIC + greeting: String = "Hello" + score: Int = 100 + ratio: Float = 0.5 + isMember: Boolean = true + } + `); + const result = await plugin( + schema, + [], + { + schema: 'zodv4', + importFrom: './types', + useEnumTypeAsDefaultValue: true, + }, + { + }, + ); + + expect(result.content).toContain('export const PageTypeSchema = z.enum(PageType)'); + expect(result.content).toContain('export function PageInputSchema(): z.ZodObject>'); + + expect(result.content).toContain('pageType: PageTypeSchema.default(PageType.Public)'); + expect(result.content).toContain('greeting: z.string().default("Hello").nullish()'); + expect(result.content).toContain('score: z.number().default(100).nullish()'); + expect(result.content).toContain('ratio: z.number().default(0.5).nullish()'); + expect(result.content).toContain('isMember: z.boolean().default(true).nullish()'); + }); + + it('with default input values as enum types with underscores', async () => { + const schema = buildSchema(/* GraphQL */ ` + enum PageType { + BASIC_AUTH + PUBLIC + } + input PageInput { + pageType: PageType! = BASIC_AUTH + greeting: String = "Hello" + score: Int = 100 + ratio: Float = 0.5 + isMember: Boolean = true + } + `); + const result = await plugin( + schema, + [], + { + schema: 'zodv4', + importFrom: './types', + useEnumTypeAsDefaultValue: true, + }, + { + }, + ); + + expect(result.content).toContain('export const PageTypeSchema = z.enum(PageType)'); + expect(result.content).toContain('export function PageInputSchema(): z.ZodObject>'); + + expect(result.content).toContain('pageType: PageTypeSchema.default(PageType.Basic_Auth)'); + expect(result.content).toContain('greeting: z.string().default("Hello").nullish()'); + expect(result.content).toContain('score: z.number().default(100).nullish()'); + expect(result.content).toContain('ratio: z.number().default(0.5).nullish()'); + expect(result.content).toContain('isMember: z.boolean().default(true).nullish()'); + }); + + it('with default input values as enum types with no underscores', async () => { + const schema = buildSchema(/* GraphQL */ ` + enum PageType { + BASIC_AUTH + PUBLIC + } + input PageInput { + pageType: PageType! = BASIC_AUTH + greeting: String = "Hello" + score: Int = 100 + ratio: Float = 0.5 + isMember: Boolean = true + } + `); + const result = await plugin( + schema, + [], + { + schema: 'zodv4', + importFrom: './types', + useEnumTypeAsDefaultValue: true, + namingConvention: { + transformUnderscore: true, + }, + }, + { + }, + ); + + expect(result.content).toContain('export const PageTypeSchema = z.enum(PageType)'); + expect(result.content).toContain('export function PageInputSchema(): z.ZodObject>'); + + expect(result.content).toContain('pageType: PageTypeSchema.default(PageType.BasicAuth)'); + expect(result.content).toContain('greeting: z.string().default("Hello").nullish()'); + expect(result.content).toContain('score: z.number().default(100).nullish()'); + expect(result.content).toContain('ratio: z.number().default(0.5).nullish()'); + expect(result.content).toContain('isMember: z.boolean().default(true).nullish()'); + }); + + it('with default input values', async () => { + const schema = buildSchema(/* GraphQL */ ` + enum PageType { + PUBLIC + BASIC_AUTH + } + input PageInput { + pageType: PageType! = PUBLIC + greeting: String = "Hello" + newline: String = "Hello\\nWorld" + score: Int = 100 + ratio: Float = 0.5 + isMember: Boolean = true + } + `); + const result = await plugin( + schema, + [], + { + schema: 'zodv4', + importFrom: './types', + }, + {}, + ); + + expect(removedInitialEmitValue(result.content)).toMatchInlineSnapshot(` + " + type Properties = Required<{ + [K in keyof T]: z.ZodType; + }>; + + type definedNonNullAny = {}; + + export const isDefinedNonNullAny = (v: any): v is definedNonNullAny => v !== undefined && v !== null; + + export const definedNonNullAnySchema = z.any().refine((v) => isDefinedNonNullAny(v)); + + export const PageTypeSchema = z.enum(PageType); + + export function PageInputSchema(): z.ZodObject> { + return z.object({ + pageType: PageTypeSchema.default("PUBLIC"), + greeting: z.string().default("Hello").nullish(), + newline: z.string().default("Hello\\nWorld").nullish(), + score: z.number().default(100).nullish(), + ratio: z.number().default(0.5).nullish(), + isMember: z.boolean().default(true).nullish() + }) + } + " + `) + }); + + describe('issues #19', () => { + it('string field', async () => { + const schema = buildSchema(/* GraphQL */ ` + input UserCreateInput { + profile: String @constraint(minLength: 1, maxLength: 5000) + } + + directive @constraint(minLength: Int!, maxLength: Int!) on INPUT_FIELD_DEFINITION + `); + const result = await plugin( + schema, + [], + { + schema: 'zodv4', + directives: { + constraint: { + minLength: ['min', '$1', 'Please input more than $1'], + maxLength: ['max', '$1', 'Please input less than $1'], + }, + }, + }, + {}, + ); + expect(removedInitialEmitValue(result.content)).toMatchInlineSnapshot(` + " + type Properties = Required<{ + [K in keyof T]: z.ZodType; + }>; + + type definedNonNullAny = {}; + + export const isDefinedNonNullAny = (v: any): v is definedNonNullAny => v !== undefined && v !== null; + + export const definedNonNullAnySchema = z.any().refine((v) => isDefinedNonNullAny(v)); + + export function UserCreateInputSchema(): z.ZodObject> { + return z.object({ + profile: z.string().min(1, "Please input more than 1").max(5000, "Please input less than 5000").nullish() + }) + } + " + `) + }); + + it('not null field', async () => { + const schema = buildSchema(/* GraphQL */ ` + input UserCreateInput { + profile: String! @constraint(minLength: 1, maxLength: 5000) + } + + directive @constraint(minLength: Int!, maxLength: Int!) on INPUT_FIELD_DEFINITION + `); + const result = await plugin( + schema, + [], + { + schema: 'zodv4', + directives: { + constraint: { + minLength: ['min', '$1', 'Please input more than $1'], + maxLength: ['max', '$1', 'Please input less than $1'], + }, + }, + }, + {}, + ); + expect(removedInitialEmitValue(result.content)).toMatchInlineSnapshot(` + " + type Properties = Required<{ + [K in keyof T]: z.ZodType; + }>; + + type definedNonNullAny = {}; + + export const isDefinedNonNullAny = (v: any): v is definedNonNullAny => v !== undefined && v !== null; + + export const definedNonNullAnySchema = z.any().refine((v) => isDefinedNonNullAny(v)); + + export function UserCreateInputSchema(): z.ZodObject> { + return z.object({ + profile: z.string().min(1, "Please input more than 1").max(5000, "Please input less than 5000") + }) + } + " + `) + }); + + it('list field', async () => { + const schema = buildSchema(/* GraphQL */ ` + input UserCreateInput { + profile: [String] @constraint(minLength: 1, maxLength: 5000) + } + + directive @constraint(minLength: Int!, maxLength: Int!) on INPUT_FIELD_DEFINITION + `); + const result = await plugin( + schema, + [], + { + schema: 'zodv4', + directives: { + constraint: { + minLength: ['min', '$1', 'Please input more than $1'], + maxLength: ['max', '$1', 'Please input less than $1'], + }, + }, + }, + {}, + ); + expect(removedInitialEmitValue(result.content)).toMatchInlineSnapshot(` + " + type Properties = Required<{ + [K in keyof T]: z.ZodType; + }>; + + type definedNonNullAny = {}; + + export const isDefinedNonNullAny = (v: any): v is definedNonNullAny => v !== undefined && v !== null; + + export const definedNonNullAnySchema = z.any().refine((v) => isDefinedNonNullAny(v)); + + export function UserCreateInputSchema(): z.ZodObject> { + return z.object({ + profile: z.array(z.string().nullable()).min(1, "Please input more than 1").max(5000, "Please input less than 5000").nullish() + }) + } + " + `) + }); + }); + + describe('pR #112', () => { + it('with notAllowEmptyString', async () => { + const schema = buildSchema(/* GraphQL */ ` + input UserCreateInput { + profile: String! @constraint(maxLength: 5000) + age: Int! + } + + directive @constraint(maxLength: Int!) on INPUT_FIELD_DEFINITION + `); + const result = await plugin( + schema, + [], + { + schema: 'zodv4', + notAllowEmptyString: true, + directives: { + constraint: { + maxLength: ['max', '$1', 'Please input less than $1'], + }, + }, + }, + {}, + ); + expect(removedInitialEmitValue(result.content)).toMatchInlineSnapshot(` + " + type Properties = Required<{ + [K in keyof T]: z.ZodType; + }>; + + type definedNonNullAny = {}; + + export const isDefinedNonNullAny = (v: any): v is definedNonNullAny => v !== undefined && v !== null; + + export const definedNonNullAnySchema = z.any().refine((v) => isDefinedNonNullAny(v)); + + export function UserCreateInputSchema(): z.ZodObject> { + return z.object({ + profile: z.string().max(5000, "Please input less than 5000").min(1), + age: z.number() + }) + } + " + `) + }); + + it('without notAllowEmptyString', async () => { + const schema = buildSchema(/* GraphQL */ ` + input UserCreateInput { + profile: String! @constraint(maxLength: 5000) + age: Int! + } + + directive @constraint(maxLength: Int!) on INPUT_FIELD_DEFINITION + `); + const result = await plugin( + schema, + [], + { + schema: 'zodv4', + directives: { + constraint: { + maxLength: ['max', '$1', 'Please input less than $1'], + }, + }, + }, + {}, + ); + expect(removedInitialEmitValue(result.content)).toMatchInlineSnapshot(` + " + type Properties = Required<{ + [K in keyof T]: z.ZodType; + }>; + + type definedNonNullAny = {}; + + export const isDefinedNonNullAny = (v: any): v is definedNonNullAny => v !== undefined && v !== null; + + export const definedNonNullAnySchema = z.any().refine((v) => isDefinedNonNullAny(v)); + + export function UserCreateInputSchema(): z.ZodObject> { + return z.object({ + profile: z.string().max(5000, "Please input less than 5000"), + age: z.number() + }) + } + " + `) + }); + }); + + describe('with withObjectType', () => { + it('not generate if withObjectType false', async () => { + const schema = buildSchema(/* GraphQL */ ` + type User { + id: ID! + name: String + } + `); + const result = await plugin( + schema, + [], + { + schema: 'zodv4', + }, + {}, + ); + expect(result.content).not.toContain('export function UserSchema(): z.ZodObject>'); + }); + + it('generate object type contains object type', async () => { + const schema = buildSchema(/* GraphQL */ ` + type Book { + author: Author + title: String + } + + type Author { + books: [Book] + name: String + } + `); + const result = await plugin( + schema, + [], + { + schema: 'zodv4', + withObjectType: true, + }, + {}, + ); + expect(removedInitialEmitValue(result.content)).toMatchInlineSnapshot(` + " + type Properties = Required<{ + [K in keyof T]: z.ZodType; + }>; + + type definedNonNullAny = {}; + + export const isDefinedNonNullAny = (v: any): v is definedNonNullAny => v !== undefined && v !== null; + + export const definedNonNullAnySchema = z.any().refine((v) => isDefinedNonNullAny(v)); + + export function BookSchema(): z.ZodObject> { + return z.object({ + __typename: z.literal('Book').optional(), + author: AuthorSchema().nullish(), + title: z.string().nullish() + }) + } + + export function AuthorSchema(): z.ZodObject> { + return z.object({ + __typename: z.literal('Author').optional(), + books: z.array(BookSchema().nullable()).nullish(), + name: z.string().nullish() + }) + } + " + `) + + for (const wantNotContain of ['Query', 'Mutation', 'Subscription']) + expect(result.content).not.toContain(wantNotContain); + }); + + it('generate both input & type', async () => { + const schema = buildSchema(/* GraphQL */ ` + scalar Date + scalar Email + input UserCreateInput { + name: String! + date: Date! + email: Email! + } + input UsernameUpdateInput { + updateInputId: ID! + updateName: String! + } + type User { + id: ID! + name: String + age: Int + email: Email + isMember: Boolean + createdAt: Date! + } + + type Mutation { + _empty: String + } + + type Query { + _empty: String + } + + type Subscription { + _empty: String + } + `); + const result = await plugin( + schema, + [], + { + schema: 'zodv4', + withObjectType: true, + scalarSchemas: { + Date: 'z.date()', + Email: 'z.string().email()', + }, + scalars: { + ID: { + input: 'number', + output: 'string', + }, + }, + }, + {}, + ); + expect(removedInitialEmitValue(result.content)).toMatchInlineSnapshot(` + " + type Properties = Required<{ + [K in keyof T]: z.ZodType; + }>; + + type definedNonNullAny = {}; + + export const isDefinedNonNullAny = (v: any): v is definedNonNullAny => v !== undefined && v !== null; + + export const definedNonNullAnySchema = z.any().refine((v) => isDefinedNonNullAny(v)); + + export function UserCreateInputSchema(): z.ZodObject> { + return z.object({ + name: z.string(), + date: z.date(), + email: z.string().email() + }) + } + + export function UsernameUpdateInputSchema(): z.ZodObject> { + return z.object({ + updateInputId: z.number(), + updateName: z.string() + }) + } + + export function UserSchema(): z.ZodObject> { + return z.object({ + __typename: z.literal('User').optional(), + id: z.string(), + name: z.string().nullish(), + age: z.number().nullish(), + email: z.string().email().nullish(), + isMember: z.boolean().nullish(), + createdAt: z.date() + }) + } + " + `) + + for (const wantNotContain of ['Query', 'Mutation', 'Subscription']) + expect(result.content).not.toContain(wantNotContain); + }); + + it('generate union types', async () => { + const schema = buildSchema(/* GraphQL */ ` + type Square { + size: Int + } + type Circle { + radius: Int + } + union Shape = Circle | Square + `); + + const result = await plugin( + schema, + [], + { + schema: 'zodv4', + withObjectType: true, + }, + {}, + ); + + expect(removedInitialEmitValue(result.content)).toMatchInlineSnapshot(` + " + type Properties = Required<{ + [K in keyof T]: z.ZodType; + }>; + + type definedNonNullAny = {}; + + export const isDefinedNonNullAny = (v: any): v is definedNonNullAny => v !== undefined && v !== null; + + export const definedNonNullAnySchema = z.any().refine((v) => isDefinedNonNullAny(v)); + + export function SquareSchema(): z.ZodObject> { + return z.object({ + __typename: z.literal('Square').optional(), + size: z.number().nullish() + }) + } + + export function CircleSchema(): z.ZodObject> { + return z.object({ + __typename: z.literal('Circle').optional(), + radius: z.number().nullish() + }) + } + + export function ShapeSchema() { + return z.union([CircleSchema(), SquareSchema()]) + } + " + `) + }); + + it('generate union types + schemaNamespacedImportName', async () => { + const schema = buildSchema(/* GraphQL */ ` + type Square { + size: Int + } + type Circle { + radius: Int + } + union Shape = Circle | Square + `); + + const result = await plugin( + schema, + [], + { + schema: 'zodv4', + withObjectType: true, + importFrom: './types', + schemaNamespacedImportName: 't', + }, + {}, + ); + + expect(removedInitialEmitValue(result.content)).toMatchInlineSnapshot(` + " + type Properties = Required<{ + [K in keyof T]: z.ZodType; + }>; + + type definedNonNullAny = {}; + + export const isDefinedNonNullAny = (v: any): v is definedNonNullAny => v !== undefined && v !== null; + + export const definedNonNullAnySchema = z.any().refine((v) => isDefinedNonNullAny(v)); + + export function SquareSchema(): z.ZodObject> { + return z.object({ + __typename: z.literal('Square').optional(), + size: z.number().nullish() + }) + } + + export function CircleSchema(): z.ZodObject> { + return z.object({ + __typename: z.literal('Circle').optional(), + radius: z.number().nullish() + }) + } + + export function ShapeSchema() { + return z.union([CircleSchema(), SquareSchema()]) + } + " + `) + }); + + it('generate union types with single element', async () => { + const schema = buildSchema(/* GraphQL */ ` + type Square { + size: Int + } + type Circle { + radius: Int + } + union Shape = Circle | Square + + type Geometry { + shape: Shape + } + `); + + const result = await plugin( + schema, + [], + { + schema: 'zodv4', + withObjectType: true, + }, + {}, + ); + + expect(removedInitialEmitValue(result.content)).toMatchInlineSnapshot(` + " + type Properties = Required<{ + [K in keyof T]: z.ZodType; + }>; + + type definedNonNullAny = {}; + + export const isDefinedNonNullAny = (v: any): v is definedNonNullAny => v !== undefined && v !== null; + + export const definedNonNullAnySchema = z.any().refine((v) => isDefinedNonNullAny(v)); + + export function SquareSchema(): z.ZodObject> { + return z.object({ + __typename: z.literal('Square').optional(), + size: z.number().nullish() + }) + } + + export function CircleSchema(): z.ZodObject> { + return z.object({ + __typename: z.literal('Circle').optional(), + radius: z.number().nullish() + }) + } + + export function ShapeSchema() { + return z.union([CircleSchema(), SquareSchema()]) + } + + export function GeometrySchema(): z.ZodObject> { + return z.object({ + __typename: z.literal('Geometry').optional(), + shape: ShapeSchema().nullish() + }) + } + " + `) + }); + + it('correctly reference generated union types', async () => { + const schema = buildSchema(/* GraphQL */ ` + type Circle { + radius: Int + } + union Shape = Circle + `); + + const result = await plugin( + schema, + [], + { + schema: 'zodv4', + withObjectType: true, + }, + {}, + ); + + expect(removedInitialEmitValue(result.content)).toMatchInlineSnapshot(` + " + type Properties = Required<{ + [K in keyof T]: z.ZodType; + }>; + + type definedNonNullAny = {}; + + export const isDefinedNonNullAny = (v: any): v is definedNonNullAny => v !== undefined && v !== null; + + export const definedNonNullAnySchema = z.any().refine((v) => isDefinedNonNullAny(v)); + + export function CircleSchema(): z.ZodObject> { + return z.object({ + __typename: z.literal('Circle').optional(), + radius: z.number().nullish() + }) + } + + export function ShapeSchema() { + return CircleSchema() + } + " + `) + }); + + it('generate enum union types', async () => { + const schema = buildSchema(/* GraphQL */ ` + enum PageType { + PUBLIC + BASIC_AUTH + } + + enum MethodType { + GET + POST + } + + union AnyType = PageType | MethodType + `); + + const result = await plugin( + schema, + [], + { + schema: 'zodv4', + withObjectType: true, + }, + {}, + ); + + expect(removedInitialEmitValue(result.content)).toMatchInlineSnapshot(` + " + type Properties = Required<{ + [K in keyof T]: z.ZodType; + }>; + + type definedNonNullAny = {}; + + export const isDefinedNonNullAny = (v: any): v is definedNonNullAny => v !== undefined && v !== null; + + export const definedNonNullAnySchema = z.any().refine((v) => isDefinedNonNullAny(v)); + + export const PageTypeSchema = z.enum(PageType); + + export const MethodTypeSchema = z.enum(MethodType); + + export function AnyTypeSchema() { + return z.union([PageTypeSchema, MethodTypeSchema]) + } + " + `) + }); + + it('generate union types with single element, export as const', async () => { + const schema = buildSchema(/* GraphQL */ ` + type Square { + size: Int + } + type Circle { + radius: Int + } + union Shape = Circle | Square + + type Geometry { + shape: Shape + } + `); + + const result = await plugin( + schema, + [], + { + schema: 'zodv4', + withObjectType: true, + validationSchemaExportType: 'const', + }, + {}, + ); + + expect(removedInitialEmitValue(result.content)).toMatchInlineSnapshot(` + " + type Properties = Required<{ + [K in keyof T]: z.ZodType; + }>; + + type definedNonNullAny = {}; + + export const isDefinedNonNullAny = (v: any): v is definedNonNullAny => v !== undefined && v !== null; + + export const definedNonNullAnySchema = z.any().refine((v) => isDefinedNonNullAny(v)); + + export const CircleSchema: z.ZodObject> = z.object({ + __typename: z.literal('Circle').optional(), + radius: z.number().nullish() + }); + + export const SquareSchema: z.ZodObject> = z.object({ + __typename: z.literal('Square').optional(), + size: z.number().nullish() + }); + + export const ShapeSchema = z.union([CircleSchema, SquareSchema]); + + export const GeometrySchema: z.ZodObject> = z.object({ + __typename: z.literal('Geometry').optional(), + shape: ShapeSchema.nullish() + }); + " + `) + }); + + it('with object arguments', async () => { + const schema = buildSchema(/* GraphQL */ ` + type MyType { + foo(a: String, b: Int!, c: Boolean, d: Float!, e: Text): String + } + scalar Text + `); + const result = await plugin( + schema, + [], + { + schema: 'zodv4', + withObjectType: true, + scalars: { + Text: 'string', + }, + }, + {}, + ); + expect(removedInitialEmitValue(result.content)).toMatchInlineSnapshot(` + " + type Properties = Required<{ + [K in keyof T]: z.ZodType; + }>; + + type definedNonNullAny = {}; + + export const isDefinedNonNullAny = (v: any): v is definedNonNullAny => v !== undefined && v !== null; + + export const definedNonNullAnySchema = z.any().refine((v) => isDefinedNonNullAny(v)); + + export function MyTypeSchema(): z.ZodObject> { + return z.object({ + __typename: z.literal('MyType').optional(), + foo: z.string().nullish() + }) + } + + export function MyTypeFooArgsSchema(): z.ZodObject> { + return z.object({ + a: z.string().nullish(), + b: z.number(), + c: z.boolean().nullish(), + d: z.number(), + e: z.string().nullish() + }) + } + " + `) + }); + + describe('with InterfaceType', () => { + it('not generate if withObjectType false', async () => { + const schema = buildSchema(/* GraphQL */ ` + interface User { + id: ID! + name: String + } + `); + const result = await plugin( + schema, + [], + { + schema: 'zodv4', + withObjectType: false, + }, + {}, + ); + expect(result.content).not.toContain('export function UserSchema(): z.ZodObject>'); + }); + + it('generate if withObjectType true', async () => { + const schema = buildSchema(/* GraphQL */ ` + interface Book { + title: String + } + `); + const result = await plugin( + schema, + [], + { + schema: 'zodv4', + withObjectType: true, + }, + {}, + ); + expect(removedInitialEmitValue(result.content)).toMatchInlineSnapshot(` + " + type Properties = Required<{ + [K in keyof T]: z.ZodType; + }>; + + type definedNonNullAny = {}; + + export const isDefinedNonNullAny = (v: any): v is definedNonNullAny => v !== undefined && v !== null; + + export const definedNonNullAnySchema = z.any().refine((v) => isDefinedNonNullAny(v)); + + export function BookSchema(): z.ZodObject> { + return z.object({ + title: z.string().nullish() + }) + } + " + `) + const wantNotContains = ['__typename: z.literal(\'Book\')']; + + for (const wantNotContain of wantNotContains) + expect(result.content).not.toContain(wantNotContain); + }); + + it('generate interface type contains interface type', async () => { + const schema = buildSchema(/* GraphQL */ ` + interface Book { + author: Author + title: String + } + + interface Author { + books: [Book] + name: String + } + `); + const result = await plugin( + schema, + [], + { + schema: 'zodv4', + withObjectType: true, + }, + {}, + ); + expect(removedInitialEmitValue(result.content)).toMatchInlineSnapshot(` + " + type Properties = Required<{ + [K in keyof T]: z.ZodType; + }>; + + type definedNonNullAny = {}; + + export const isDefinedNonNullAny = (v: any): v is definedNonNullAny => v !== undefined && v !== null; + + export const definedNonNullAnySchema = z.any().refine((v) => isDefinedNonNullAny(v)); + + export function BookSchema(): z.ZodObject> { + return z.object({ + author: AuthorSchema().nullish(), + title: z.string().nullish() + }) + } + + export function AuthorSchema(): z.ZodObject> { + return z.object({ + books: z.array(BookSchema().nullable()).nullish(), + name: z.string().nullish() + }) + } + " + `) + }); + + it('generate object type contains interface type', async () => { + const schema = buildSchema(/* GraphQL */ ` + interface Book { + title: String! + author: Author! + } + + type Textbook implements Book { + title: String! + author: Author! + courses: [String!]! + } + + type ColoringBook implements Book { + title: String! + author: Author! + colors: [String!]! + } + + type Author { + books: [Book!] + name: String + } + `); + const result = await plugin( + schema, + [], + { + schema: 'zodv4', + withObjectType: true, + }, + {}, + ); + expect(removedInitialEmitValue(result.content)).toMatchInlineSnapshot(` + " + type Properties = Required<{ + [K in keyof T]: z.ZodType; + }>; + + type definedNonNullAny = {}; + + export const isDefinedNonNullAny = (v: any): v is definedNonNullAny => v !== undefined && v !== null; + + export const definedNonNullAnySchema = z.any().refine((v) => isDefinedNonNullAny(v)); + + export function BookSchema(): z.ZodObject> { + return z.object({ + title: z.string(), + author: AuthorSchema() + }) + } + + export function TextbookSchema(): z.ZodObject> { + return z.object({ + __typename: z.literal('Textbook').optional(), + title: z.string(), + author: AuthorSchema(), + courses: z.array(z.string()) + }) + } + + export function ColoringBookSchema(): z.ZodObject> { + return z.object({ + __typename: z.literal('ColoringBook').optional(), + title: z.string(), + author: AuthorSchema(), + colors: z.array(z.string()) + }) + } + + export function AuthorSchema(): z.ZodObject> { + return z.object({ + __typename: z.literal('Author').optional(), + books: z.array(BookSchema()).nullish(), + name: z.string().nullish() + }) + } + " + `) + }); + }); + }); + + it('properly generates custom directive values', async () => { + const schema = buildSchema(/* GraphQL */ ` + input UserCreateInput { + name: String! @constraint(startsWith: "Sir") + age: Int! @constraint(min: 0, max: 100) + } + directive @constraint(startsWith: String, min: Int, max: Int) on INPUT_FIELD_DEFINITION + `); + const result = await plugin( + schema, + [], + { + schema: 'zodv4', + directives: { + constraint: { + min: 'min', + max: 'max', + startsWith: ['regex', '/^$1/'], + }, + }, + }, + {}, + ); + expect(removedInitialEmitValue(result.content)).toMatchInlineSnapshot(` + " + type Properties = Required<{ + [K in keyof T]: z.ZodType; + }>; + + type definedNonNullAny = {}; + + export const isDefinedNonNullAny = (v: any): v is definedNonNullAny => v !== undefined && v !== null; + + export const definedNonNullAnySchema = z.any().refine((v) => isDefinedNonNullAny(v)); + + export function UserCreateInputSchema(): z.ZodObject> { + return z.object({ + name: z.string().regex(/^Sir/), + age: z.number().min(0).max(100) + }) + } + " + `) + }); + + it('exports as const instead of func', async () => { + const schema = buildSchema(/* GraphQL */ ` + input Say { + phrase: String! + } + `); + const result = await plugin( + schema, + [], + { + schema: 'zodv4', + validationSchemaExportType: 'const', + }, + {}, + ); + expect(removedInitialEmitValue(result.content)).toMatchInlineSnapshot(` + " + type Properties = Required<{ + [K in keyof T]: z.ZodType; + }>; + + type definedNonNullAny = {}; + + export const isDefinedNonNullAny = (v: any): v is definedNonNullAny => v !== undefined && v !== null; + + export const definedNonNullAnySchema = z.any().refine((v) => isDefinedNonNullAny(v)); + + export const SaySchema: z.ZodObject> = z.object({ + phrase: z.string() + }); + " + `) + }); + + it('generate both input & type, export as const', async () => { + const schema = buildSchema(/* GraphQL */ ` + scalar Date + scalar Email + input UserCreateInput { + name: String! + date: Date! + email: Email! + } + type User { + id: ID! + name: String + age: Int + email: Email + isMember: Boolean + createdAt: Date! + } + type Mutation { + _empty: String + } + type Query { + _empty: String + } + type Subscription { + _empty: String + } + `); + const result = await plugin( + schema, + [], + { + schema: 'zodv4', + withObjectType: true, + scalarSchemas: { + Date: 'z.date()', + Email: 'z.string().email()', + }, + validationSchemaExportType: 'const', + }, + {}, + ); + + expect(removedInitialEmitValue(result.content)).toMatchInlineSnapshot(` + " + type Properties = Required<{ + [K in keyof T]: z.ZodType; + }>; + + type definedNonNullAny = {}; + + export const isDefinedNonNullAny = (v: any): v is definedNonNullAny => v !== undefined && v !== null; + + export const definedNonNullAnySchema = z.any().refine((v) => isDefinedNonNullAny(v)); + + export const UserSchema: z.ZodObject> = z.object({ + __typename: z.literal('User').optional(), + id: z.string(), + name: z.string().nullish(), + age: z.number().nullish(), + email: z.string().email().nullish(), + isMember: z.boolean().nullish(), + createdAt: z.date() + }); + + export const UserCreateInputSchema: z.ZodObject> = z.object({ + name: z.string(), + date: z.date(), + email: z.string().email() + }); + " + `) + + for (const wantNotContain of ['Query', 'Mutation', 'Subscription']) + expect(result.content).not.toContain(wantNotContain); + }); + + it('issue #394', async () => { + const schema = buildSchema(/* GraphQL */ ` + enum Test { + A + B + } + + type Query { + _dummy: Test + } + + input QueryInput { + _dummy: Test + } + `); + const query = introspectionFromSchema(schema); + const clientSchema = buildClientSchema(query); + const result = await plugin( + clientSchema, + [], + { + schema: 'zodv4', + scalars: { + ID: 'string', + }, + }, + {}, + ); + expect(removedInitialEmitValue(result.content)).toMatchInlineSnapshot(` + " + type Properties = Required<{ + [K in keyof T]: z.ZodType; + }>; + + type definedNonNullAny = {}; + + export const isDefinedNonNullAny = (v: any): v is definedNonNullAny => v !== undefined && v !== null; + + export const definedNonNullAnySchema = z.any().refine((v) => isDefinedNonNullAny(v)); + + export const TestSchema = z.enum(Test); + + export function QueryInputSchema(): z.ZodObject> { + return z.object({ + _dummy: TestSchema.nullish() + }) + } + " + `) + }); +});