diff --git a/src/config.ts b/src/config.ts index 04c1af45..c1cd6d9e 100644 --- a/src/config.ts +++ b/src/config.ts @@ -194,6 +194,24 @@ export interface ValidationSchemaPluginConfig extends TypeScriptPluginConfig { * ``` */ withObjectType?: boolean; + /** + * @description Generates validation schema with GraphQL type interfaces. + * + * @exampleMarkdown + * ```yml + * generates: + * path/to/types.ts: + * plugins: + * - typescript + * path/to/schemas.ts: + * plugins: + * - graphql-codegen-validation-schema + * config: + * schema: yup + * withInterfaceType: true + * ``` + */ + withInterfaceType?: boolean; /** * @description Specify validation schema export type. * @default function diff --git a/src/graphql.ts b/src/graphql.ts index 58b3b922..ab0e0d83 100644 --- a/src/graphql.ts +++ b/src/graphql.ts @@ -4,6 +4,7 @@ import { DefinitionNode, DocumentNode, GraphQLSchema, + InterfaceTypeDefinitionNode, isSpecifiedScalarType, ListTypeNode, NamedTypeNode, @@ -21,6 +22,7 @@ export const isNamedType = (typ?: TypeNode): typ is NamedTypeNode => typ?.kind = export const isInput = (kind: string) => kind.includes('Input'); type ObjectTypeDefinitionFn = (node: ObjectTypeDefinitionNode) => any; +type InterfaceTypeDefinitionFn = (node: InterfaceTypeDefinitionNode) => any; export const ObjectTypeDefinitionBuilder = ( useObjectTypes: boolean | undefined, @@ -35,6 +37,16 @@ export const ObjectTypeDefinitionBuilder = ( }; }; +export const InterfaceTypeDefinitionBuilder = ( + useInterfaceTypes: boolean | undefined, + callback: InterfaceTypeDefinitionFn +): InterfaceTypeDefinitionFn | undefined => { + if (!useInterfaceTypes) return undefined; + return node => { + return callback(node); + }; +}; + export const topologicalSortAST = (schema: GraphQLSchema, ast: DocumentNode): DocumentNode => { const dependencyGraph = new Graph(); const targetKinds = [ diff --git a/src/myzod/index.ts b/src/myzod/index.ts index a8e77496..abec91af 100644 --- a/src/myzod/index.ts +++ b/src/myzod/index.ts @@ -5,6 +5,7 @@ import { GraphQLSchema, InputObjectTypeDefinitionNode, InputValueDefinitionNode, + InterfaceTypeDefinitionNode, NameNode, ObjectTypeDefinitionNode, TypeNode, @@ -15,7 +16,14 @@ import { ValidationSchemaPluginConfig } from '../config'; import { buildApi, formatDirectiveConfig } from '../directive'; import { BaseSchemaVisitor } from '../schema_visitor'; import { Visitor } from '../visitor'; -import { isInput, isListType, isNamedType, isNonNullType, ObjectTypeDefinitionBuilder } from './../graphql'; +import { + InterfaceTypeDefinitionBuilder, + isInput, + isListType, + isNamedType, + isNonNullType, + ObjectTypeDefinitionBuilder, +} from './../graphql'; const anySchema = `definedNonNullAnySchema`; @@ -50,6 +58,44 @@ export class MyZodSchemaVisitor extends BaseSchemaVisitor { }; } + get InterfaceTypeDefinition() { + return { + leave: InterfaceTypeDefinitionBuilder(this.config.withInterfaceType, (node: InterfaceTypeDefinitionNode) => { + const visitor = this.createVisitor('output'); + const name = visitor.convertName(node.name.value); + 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 => generateFieldMyZodSchema(this.config, visitor, field, 2)).join(',\n'); + + switch (this.config.validationSchemaExportType) { + case 'const': + return ( + new DeclarationBlock({}) + .export() + .asKind('const') + .withName(`${name}Schema: myzod.Type<${name}>`) + .withContent([`myzod.object({`, shape, '})'].join('\n')).string + appendArguments + ); + + case 'function': + default: + return ( + new DeclarationBlock({}) + .export() + .asKind('function') + .withName(`${name}Schema(): myzod.Type<${name}>`) + .withBlock([indent(`return myzod.object({`), shape, indent('})')].join('\n')).string + appendArguments + ); + } + }), + }; + } + get ObjectTypeDefinition() { return { leave: ObjectTypeDefinitionBuilder(this.config.withObjectType, (node: ObjectTypeDefinitionNode) => { @@ -58,7 +104,7 @@ export class MyZodSchemaVisitor extends BaseSchemaVisitor { this.importTypes.push(name); // Building schema for field arguments. - const argumentBlocks = this.buildObjectTypeDefinitionArguments(node, visitor); + const argumentBlocks = this.buildTypeDefinitionArguments(node, visitor); const appendArguments = argumentBlocks ? '\n' + argumentBlocks : ''; // Building schema for fields. @@ -270,6 +316,7 @@ const generateNameNodeMyZodSchema = ( const converter = visitor.getNameNodeConverter(node); switch (converter?.targetKind) { + case 'InterfaceTypeDefinition': case 'InputObjectTypeDefinition': case 'ObjectTypeDefinition': case 'UnionTypeDefinition': @@ -283,7 +330,12 @@ const generateNameNodeMyZodSchema = ( } case 'EnumTypeDefinition': return `${converter.convertName()}Schema`; + case 'ScalarTypeDefinition': + return myzod4Scalar(config, visitor, node.value); default: + if (converter?.targetKind) { + console.warn('Unknown target kind', converter.targetKind); + } return myzod4Scalar(config, visitor, node.value); } }; diff --git a/src/schema_visitor.ts b/src/schema_visitor.ts index 686ca7d9..d363d95e 100644 --- a/src/schema_visitor.ts +++ b/src/schema_visitor.ts @@ -1,4 +1,10 @@ -import { FieldDefinitionNode, GraphQLSchema, InputValueDefinitionNode, ObjectTypeDefinitionNode } from 'graphql'; +import { + FieldDefinitionNode, + GraphQLSchema, + InputValueDefinitionNode, + InterfaceTypeDefinitionNode, + ObjectTypeDefinitionNode, +} from 'graphql'; import { ValidationSchemaPluginConfig } from './config'; import { SchemaVisitor } from './types'; @@ -39,7 +45,10 @@ export abstract class BaseSchemaVisitor implements SchemaVisitor { name: string ): string; - protected buildObjectTypeDefinitionArguments(node: ObjectTypeDefinitionNode, visitor: Visitor) { + protected buildTypeDefinitionArguments( + node: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode, + visitor: Visitor + ) { return visitor.buildArgumentsSchemaBlock(node, (typeName, field) => { this.importTypes.push(typeName); return this.buildInputFields(field.arguments ?? [], visitor, typeName); diff --git a/src/visitor.ts b/src/visitor.ts index cc06e829..f476d655 100644 --- a/src/visitor.ts +++ b/src/visitor.ts @@ -1,5 +1,12 @@ import { TsVisitor } from '@graphql-codegen/typescript'; -import { FieldDefinitionNode, GraphQLSchema, NameNode, ObjectTypeDefinitionNode, specifiedScalarTypes } from 'graphql'; +import { + FieldDefinitionNode, + GraphQLSchema, + InterfaceTypeDefinitionNode, + NameNode, + ObjectTypeDefinitionNode, + specifiedScalarTypes, +} from 'graphql'; import { ValidationSchemaPluginConfig } from './config'; @@ -36,7 +43,11 @@ export class Visitor extends TsVisitor { if (this.scalarDirection === 'both') { return null; } - return this.scalars[scalarName][this.scalarDirection]; + const scalar = this.scalars[scalarName]; + if (!scalar) { + throw new Error(`Unknown scalar ${scalarName}`); + } + return scalar[this.scalarDirection]; } public shouldEmitAsNotAllowEmptyString(name: string): boolean { @@ -52,7 +63,7 @@ export class Visitor extends TsVisitor { } public buildArgumentsSchemaBlock( - node: ObjectTypeDefinitionNode, + node: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode, callback: (typeName: string, field: FieldDefinitionNode) => string ) { const fieldsWithArguments = node.fields?.filter(field => field.arguments && field.arguments.length > 0) ?? []; diff --git a/src/yup/index.ts b/src/yup/index.ts index 171d47f4..0909bee7 100644 --- a/src/yup/index.ts +++ b/src/yup/index.ts @@ -5,6 +5,7 @@ import { GraphQLSchema, InputObjectTypeDefinitionNode, InputValueDefinitionNode, + InterfaceTypeDefinitionNode, NameNode, ObjectTypeDefinitionNode, TypeNode, @@ -15,7 +16,14 @@ import { ValidationSchemaPluginConfig } from '../config'; import { buildApi, formatDirectiveConfig } from '../directive'; import { BaseSchemaVisitor } from '../schema_visitor'; import { Visitor } from '../visitor'; -import { isInput, isListType, isNamedType, isNonNullType, ObjectTypeDefinitionBuilder } from './../graphql'; +import { + InterfaceTypeDefinitionBuilder, + isInput, + isListType, + isNamedType, + isNonNullType, + ObjectTypeDefinitionBuilder, +} from './../graphql'; export class YupSchemaVisitor extends BaseSchemaVisitor { constructor(schema: GraphQLSchema, config: ValidationSchemaPluginConfig) { @@ -56,6 +64,49 @@ export class YupSchemaVisitor extends BaseSchemaVisitor { }; } + get InterfaceTypeDefinition() { + return { + leave: InterfaceTypeDefinitionBuilder(this.config.withInterfaceType, (node: InterfaceTypeDefinitionNode) => { + const visitor = this.createVisitor('output'); + const name = visitor.convertName(node.name.value); + 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 => { + const fieldSchema = generateFieldYupSchema(this.config, visitor, field, 2); + return isNonNullType(field.type) ? fieldSchema : `${fieldSchema}.optional()`; + }) + .join(',\n'); + + switch (this.config.validationSchemaExportType) { + case 'const': + return ( + new DeclarationBlock({}) + .export() + .asKind('const') + .withName(`${name}Schema: yup.ObjectSchema<${name}>`) + .withContent([`yup.object({`, shape, '})'].join('\n')).string + appendArguments + ); + + case 'function': + default: + return ( + new DeclarationBlock({}) + .export() + .asKind('function') + .withName(`${name}Schema(): yup.ObjectSchema<${name}>`) + .withBlock([indent(`return yup.object({`), shape, indent('})')].join('\n')).string + appendArguments + ); + } + }), + }; + } + get ObjectTypeDefinition() { return { leave: ObjectTypeDefinitionBuilder(this.config.withObjectType, (node: ObjectTypeDefinitionNode) => { @@ -64,7 +115,7 @@ export class YupSchemaVisitor extends BaseSchemaVisitor { this.importTypes.push(name); // Building schema for field arguments. - const argumentBlocks = this.buildObjectTypeDefinitionArguments(node, visitor); + const argumentBlocks = this.buildTypeDefinitionArguments(node, visitor); const appendArguments = argumentBlocks ? '\n' + argumentBlocks : ''; // Building schema for fields. @@ -282,6 +333,7 @@ const generateNameNodeYupSchema = (config: ValidationSchemaPluginConfig, visitor const converter = visitor.getNameNodeConverter(node); switch (converter?.targetKind) { + case 'InterfaceTypeDefinition': case 'InputObjectTypeDefinition': case 'ObjectTypeDefinition': case 'UnionTypeDefinition': diff --git a/src/zod/index.ts b/src/zod/index.ts index ef83ad1d..906f656b 100644 --- a/src/zod/index.ts +++ b/src/zod/index.ts @@ -5,6 +5,7 @@ import { GraphQLSchema, InputObjectTypeDefinitionNode, InputValueDefinitionNode, + InterfaceTypeDefinitionNode, NameNode, ObjectTypeDefinitionNode, TypeNode, @@ -15,7 +16,14 @@ import { ValidationSchemaPluginConfig } from '../config'; import { buildApi, formatDirectiveConfig } from '../directive'; import { BaseSchemaVisitor } from '../schema_visitor'; import { Visitor } from '../visitor'; -import { isInput, isListType, isNamedType, isNonNullType, ObjectTypeDefinitionBuilder } from './../graphql'; +import { + InterfaceTypeDefinitionBuilder, + isInput, + isListType, + isNamedType, + isNonNullType, + ObjectTypeDefinitionBuilder, +} from './../graphql'; const anySchema = `definedNonNullAnySchema`; @@ -66,6 +74,44 @@ export class ZodSchemaVisitor extends BaseSchemaVisitor { }; } + get InterfaceTypeDefinition() { + return { + leave: InterfaceTypeDefinitionBuilder(this.config.withInterfaceType, (node: InterfaceTypeDefinitionNode) => { + const visitor = this.createVisitor('output'); + const name = visitor.convertName(node.name.value); + 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) => { @@ -74,7 +120,7 @@ export class ZodSchemaVisitor extends BaseSchemaVisitor { this.importTypes.push(name); // Building schema for field arguments. - const argumentBlocks = this.buildObjectTypeDefinitionArguments(node, visitor); + const argumentBlocks = this.buildTypeDefinitionArguments(node, visitor); const appendArguments = argumentBlocks ? '\n' + argumentBlocks : ''; // Building schema for fields. @@ -279,6 +325,7 @@ const generateNameNodeZodSchema = (config: ValidationSchemaPluginConfig, visitor const converter = visitor.getNameNodeConverter(node); switch (converter?.targetKind) { + case 'InterfaceTypeDefinition': case 'InputObjectTypeDefinition': case 'ObjectTypeDefinition': case 'UnionTypeDefinition': @@ -292,7 +339,12 @@ const generateNameNodeZodSchema = (config: ValidationSchemaPluginConfig, visitor } 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); } }; diff --git a/tests/myzod.spec.ts b/tests/myzod.spec.ts index c0b91ecb..1a732852 100644 --- a/tests/myzod.spec.ts +++ b/tests/myzod.spec.ts @@ -492,6 +492,171 @@ describe('myzod', () => { }); }); + describe('with withInterfaceType', () => { + it('not generate if withObjectType false', async () => { + const schema = buildSchema(/* GraphQL */ ` + interface User { + id: ID! + name: String + } + `); + const result = await plugin( + schema, + [], + { + schema: 'myzod', + }, + {} + ); + expect(result.content).not.toContain('export function UserSchema(): myzod.Type {'); + }); + + it('generate if withInterfaceType true', async () => { + const schema = buildSchema(/* GraphQL */ ` + interface Book { + title: String + } + `); + const result = await plugin( + schema, + [], + { + schema: 'myzod', + withInterfaceType: true, + }, + {} + ); + const wantContains = [ + 'export function BookSchema(): myzod.Type {', + 'title: myzod.string().optional().nullable()', + ]; + const wantNotContains = ["__typename: myzod.literal('Book')"]; + for (const wantContain of wantContains) { + expect(result.content).toContain(wantContain); + } + 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: 'myzod', + withInterfaceType: true, + }, + {} + ); + const wantContains = [ + 'export function AuthorSchema(): myzod.Type {', + 'books: myzod.array(BookSchema().nullable()).optional().nullable(),', + 'name: myzod.string().optional().nullable()', + + 'export function BookSchema(): myzod.Type {', + 'author: AuthorSchema().optional().nullable(),', + 'title: myzod.string().optional().nullable()', + ]; + for (const wantContain of wantContains) { + expect(result.content).toContain(wantContain); + } + }); + 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: 'myzod', + withInterfaceType: true, + withObjectType: true, + }, + {} + ); + const wantContains = [ + [ + 'export function BookSchema(): myzod.Type {', + 'return myzod.object({', + 'title: myzod.string(),', + 'author: AuthorSchema()', + '})', + '}', + ], + + [ + 'export function TextbookSchema(): myzod.Type {', + 'return myzod.object({', + "__typename: myzod.literal('Textbook').optional(),", + 'title: myzod.string(),', + 'author: AuthorSchema(),', + 'courses: myzod.array(myzod.string())', + '})', + '}', + ], + + [ + 'export function ColoringBookSchema(): myzod.Type {', + 'return myzod.object({', + "__typename: myzod.literal('ColoringBook').optional(),", + 'title: myzod.string(),', + 'author: AuthorSchema(),', + 'colors: myzod.array(myzod.string())', + '})', + '}', + ], + + [ + 'export function AuthorSchema(): myzod.Type {', + 'return myzod.object({', + "__typename: myzod.literal('Author').optional()", + 'books: myzod.array(BookSchema()).optional().nullable()', + 'name: myzod.string().optional().nullable()', + '})', + '}', + ], + ]; + for (const wantContain of wantContains) { + for (const wantContainLine of wantContain) { + expect(result.content).toContain(wantContainLine); + } + } + }); + }); + describe('with withObjectType', () => { it('not generate if withObjectType false', async () => { const schema = buildSchema(/* GraphQL */ ` diff --git a/tests/yup.spec.ts b/tests/yup.spec.ts index 50316ad7..847daf6b 100644 --- a/tests/yup.spec.ts +++ b/tests/yup.spec.ts @@ -395,6 +395,186 @@ describe('yup', () => { expect(result.prepend).toContain("import { SayI } from './types'"); expect(result.content).toContain('export function SayISchema(): yup.ObjectSchema {'); }); + + describe('with interfaceType', () => { + it('not generate if withInterfaceType false', async () => { + const schema = buildSchema(/* GraphQL */ ` + interface User { + id: ID! + name: String + } + `); + const result = await plugin( + schema, + [], + { + schema: 'yup', + }, + {} + ); + expect(result.content).not.toContain('export function UserSchema(): yup.ObjectSchema {'); + }); + + it('generate if withInterfaceType true', async () => { + const schema = buildSchema(/* GraphQL */ ` + interface Book { + title: String + } + `); + const result = await plugin( + schema, + [], + { + schema: 'yup', + withInterfaceType: true, + }, + {} + ); + const wantContains = [ + 'export function BookSchema(): yup.ObjectSchema {', + 'title: yup.string().defined().nullable().optional()', + ]; + const wantNotContains = ["__typename: yup.string<'Book'>().optional()"]; + for (const wantContain of wantContains) { + expect(result.content).toContain(wantContain); + } + 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 Book2 { + author: Author! + title: String! + } + + interface Author { + books: [Book] + name: String + } + `); + const result = await plugin( + schema, + [], + { + schema: 'yup', + withInterfaceType: true, + }, + {} + ); + const wantContains = [ + 'export function AuthorSchema(): yup.ObjectSchema {', + 'books: yup.array(BookSchema().nullable()).defined().nullable().optional(),', + 'name: yup.string().defined().nullable().optional()', + + 'export function BookSchema(): yup.ObjectSchema {', + 'author: AuthorSchema().nullable().optional(),', + 'title: yup.string().defined().nullable().optional()', + + 'export function Book2Schema(): yup.ObjectSchema {', + 'author: AuthorSchema().nonNullable(),', + 'title: yup.string().defined().nonNullable()', + ]; + for (const wantContain of wantContains) { + expect(result.content).toContain(wantContain); + } + + for (const wantNotContain of ['Query', 'Mutation', 'Subscription']) { + expect(result.content).not.toContain(wantNotContain); + } + }); + 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: 'yup', + withInterfaceType: true, + withObjectType: true, + }, + {} + ); + const wantContains = [ + [ + 'export function BookSchema(): yup.ObjectSchema {', + 'return yup.object({', + 'title: yup.string().defined().nonNullable(),', + 'author: AuthorSchema().nonNullable()', + '})', + '}', + ], + + [ + 'export function TextbookSchema(): yup.ObjectSchema {', + 'return yup.object({', + "__typename: yup.string<'Textbook'>().optional(),", + 'title: yup.string().defined().nonNullable(),', + 'author: AuthorSchema().nonNullable(),', + 'courses: yup.array(yup.string().defined().nonNullable()).defined()', + '})', + '}', + ], + + [ + 'export function ColoringBookSchema(): yup.ObjectSchema {', + 'return yup.object({', + "__typename: yup.string<'ColoringBook'>().optional(),", + 'title: yup.string().defined().nonNullable(),', + 'author: AuthorSchema().nonNullable(),', + 'colors: yup.array(yup.string().defined().nonNullable()).defined()', + '})', + '}', + ], + + [ + 'export function AuthorSchema(): yup.ObjectSchema {', + 'return yup.object({', + "__typename: yup.string<'Author'>().optional(),", + 'books: yup.array(BookSchema().nonNullable()).defined().nullable().optional(),', + 'name: yup.string().defined().nullable().optional()', + '})', + '}', + ], + ]; + + for (const wantContain of wantContains) { + for (const wantContainLine of wantContain) { + expect(result.content).toContain(wantContainLine); + } + } + }); + }); + describe('with withObjectType', () => { it('not generate if withObjectType false', async () => { const schema = buildSchema(/* GraphQL */ ` diff --git a/tests/zod.spec.ts b/tests/zod.spec.ts index 76f3ecde..1d44f4f2 100644 --- a/tests/zod.spec.ts +++ b/tests/zod.spec.ts @@ -1,5 +1,6 @@ import { getCachedDocumentNodeFromSchema } from '@graphql-codegen/plugin-helpers'; import { buildClientSchema, buildSchema, introspectionFromSchema, isSpecifiedScalarType } from 'graphql'; +import { join } from 'path'; import { dedent } from 'ts-dedent'; import { plugin } from '../src/index'; @@ -563,6 +564,173 @@ describe('zod', () => { }); }); + describe('with withInterfaceType', () => { + it('not generate if withObjectType false', async () => { + const schema = buildSchema(/* GraphQL */ ` + interface User { + id: ID! + name: String + } + `); + const result = await plugin( + schema, + [], + { + schema: 'zod', + }, + {} + ); + expect(result.content).not.toContain('export function UserSchema(): z.ZodObject>'); + }); + + it('generate if withInterfaceType true', async () => { + const schema = buildSchema(/* GraphQL */ ` + interface Book { + title: String + } + `); + const result = await plugin( + schema, + [], + { + schema: 'zod', + withInterfaceType: true, + }, + {} + ); + const wantContains = [ + 'export function BookSchema(): z.ZodObject> {', + 'title: z.string().nullish()', + ]; + const wantNotContains = ["__typename: z.literal('Book')"]; + for (const wantContain of wantContains) { + expect(result.content).toContain(wantContain); + } + 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: 'zod', + withInterfaceType: true, + }, + {} + ); + const wantContains = [ + 'export function AuthorSchema(): z.ZodObject> {', + 'books: z.array(BookSchema().nullable()).nullish(),', + 'name: z.string().nullish()', + + 'export function BookSchema(): z.ZodObject> {', + 'author: AuthorSchema().nullish(),', + 'title: z.string().nullish()', + ]; + for (const wantContain of wantContains) { + expect(result.content).toContain(wantContain); + } + }); + + 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: 'zod', + withInterfaceType: true, + withObjectType: true, + }, + {} + ); + const wantContains = [ + [ + '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()', + '})', + '}', + ], + ]; + + for (const wantContain of wantContains) { + for (const wantContainLine of wantContain) { + expect(result.content).toContain(wantContainLine); + } + } + }); + }); + describe('with withObjectType', () => { it('not generate if withObjectType false', async () => { const schema = buildSchema(/* GraphQL */ `