From f4ed9dec05c3df420062c2f4dcab0fb36caea13d Mon Sep 17 00:00:00 2001 From: simonljus Date: Tue, 12 Dec 2023 18:32:14 +0100 Subject: [PATCH 01/14] add withInterfaceType in config --- src/config.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) 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 From 44a62c8919e2fa3403e67a514294002b1a4db2c5 Mon Sep 17 00:00:00 2001 From: simonljus Date: Wed, 13 Dec 2023 09:21:53 +0100 Subject: [PATCH 02/14] add builder for InterfaceTypeDefinition --- src/graphql.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) 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 = [ From 909ab6ed548db39e29f1ae4f55eff4e4fa75da2a Mon Sep 17 00:00:00 2001 From: simonljus Date: Wed, 13 Dec 2023 09:25:19 +0100 Subject: [PATCH 03/14] support InterfaceTypeDefinitionNode --- src/visitor.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/visitor.ts b/src/visitor.ts index cc06e829..c684486a 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'; @@ -52,7 +59,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) ?? []; From 6162cd06109b187f3605fba0b25270b78e72c187 Mon Sep 17 00:00:00 2001 From: simonljus Date: Wed, 13 Dec 2023 09:27:25 +0100 Subject: [PATCH 04/14] rename to support interfaces --- src/myzod/index.ts | 2 +- src/schema_visitor.ts | 13 +++++++++++-- src/yup/index.ts | 2 +- src/zod/index.ts | 3 ++- 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/myzod/index.ts b/src/myzod/index.ts index a8e77496..caa9eee2 100644 --- a/src/myzod/index.ts +++ b/src/myzod/index.ts @@ -58,7 +58,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. 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/yup/index.ts b/src/yup/index.ts index 171d47f4..6c01764a 100644 --- a/src/yup/index.ts +++ b/src/yup/index.ts @@ -64,7 +64,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. diff --git a/src/zod/index.ts b/src/zod/index.ts index ef83ad1d..aaca9250 100644 --- a/src/zod/index.ts +++ b/src/zod/index.ts @@ -74,7 +74,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 +279,7 @@ const generateNameNodeZodSchema = (config: ValidationSchemaPluginConfig, visitor const converter = visitor.getNameNodeConverter(node); switch (converter?.targetKind) { + case 'InterfaceTypeDefinition': case 'InputObjectTypeDefinition': case 'ObjectTypeDefinition': case 'UnionTypeDefinition': From e68dcaca5c21b5a9d26f8187f2b83f0d3b51e42c Mon Sep 17 00:00:00 2001 From: simonljus Date: Wed, 13 Dec 2023 09:28:07 +0100 Subject: [PATCH 05/14] build zod schema for interfaces --- src/zod/index.ts | 48 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/src/zod/index.ts b/src/zod/index.ts index aaca9250..83c5db16 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) => { From bbfaa676a7244e924d1b37bfe800a0b79fc73e89 Mon Sep 17 00:00:00 2001 From: simonljus Date: Wed, 13 Dec 2023 09:28:48 +0100 Subject: [PATCH 06/14] add interface type test cases for zod --- tests/zod.spec.ts | 140 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 140 insertions(+) diff --git a/tests/zod.spec.ts b/tests/zod.spec.ts index 76f3ecde..4c335fb3 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,145 @@ 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 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 */ ` From 4c85b9006a4d26704ff3712ce9f42045de847847 Mon Sep 17 00:00:00 2001 From: simonljus Date: Wed, 13 Dec 2023 21:18:02 +0100 Subject: [PATCH 07/14] case for ScalarTypeDefinition --- src/zod/index.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/zod/index.ts b/src/zod/index.ts index 83c5db16..906f656b 100644 --- a/src/zod/index.ts +++ b/src/zod/index.ts @@ -339,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); } }; From 570e58e34d17d4adb125faa634210e8d7507a7ef Mon Sep 17 00:00:00 2001 From: simonljus Date: Wed, 13 Dec 2023 21:18:40 +0100 Subject: [PATCH 08/14] throw detailed error message instead of npe --- src/visitor.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/visitor.ts b/src/visitor.ts index c684486a..f476d655 100644 --- a/src/visitor.ts +++ b/src/visitor.ts @@ -43,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 { From 73f9f0eaff4bbf625ed627d3cb124eaffa5e72ef Mon Sep 17 00:00:00 2001 From: simonljus Date: Wed, 13 Dec 2023 21:23:25 +0100 Subject: [PATCH 09/14] InterfaceTypeDefinition for myzod --- src/myzod/index.ts | 61 ++++++++++++++++++++++++++++++++++++++++++++- tests/myzod.spec.ts | 55 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+), 1 deletion(-) diff --git a/src/myzod/index.ts b/src/myzod/index.ts index caa9eee2..e4f0995a 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,51 @@ 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({`), + indent(`__typename: myzod.literal('${node.name.value}').optional(),`, 2), + shape, + indent('})'), + ].join('\n') + ).string + appendArguments + ); + } + }), + }; + } + get ObjectTypeDefinition() { return { leave: ObjectTypeDefinitionBuilder(this.config.withObjectType, (node: ObjectTypeDefinitionNode) => { @@ -270,6 +323,7 @@ const generateNameNodeMyZodSchema = ( const converter = visitor.getNameNodeConverter(node); switch (converter?.targetKind) { + case 'InterfaceTypeDefinition': case 'InputObjectTypeDefinition': case 'ObjectTypeDefinition': case 'UnionTypeDefinition': @@ -283,7 +337,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/tests/myzod.spec.ts b/tests/myzod.spec.ts index c0b91ecb..22149524 100644 --- a/tests/myzod.spec.ts +++ b/tests/myzod.spec.ts @@ -492,6 +492,61 @@ 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 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); + } + }); + }); + describe('with withObjectType', () => { it('not generate if withObjectType false', async () => { const schema = buildSchema(/* GraphQL */ ` From 804af5534294210c9777f797386be9f8dc49ddfa Mon Sep 17 00:00:00 2001 From: simonljus Date: Thu, 14 Dec 2023 17:14:29 +0100 Subject: [PATCH 10/14] add InterfaceTypeDefinition support for yup --- src/yup/index.ts | 54 ++++++++++++++++++++++++++++++++++++- tests/yup.spec.ts | 69 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+), 1 deletion(-) diff --git a/src/yup/index.ts b/src/yup/index.ts index 6c01764a..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) => { @@ -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/tests/yup.spec.ts b/tests/yup.spec.ts index 50316ad7..bd4f8535 100644 --- a/tests/yup.spec.ts +++ b/tests/yup.spec.ts @@ -395,6 +395,75 @@ 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 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().nonNullable()', + + 'export function Book2Schema(): yup.ObjectSchema {', + 'author: AuthorSchema().nonNullable(),', + 'title: yup.string().defined().nullable().optional()', + ]; + for (const wantContain of wantContains) { + expect(result.content).toContain(wantContain); + } + + for (const wantNotContain of ['Query', 'Mutation', 'Subscription']) { + expect(result.content).not.toContain(wantNotContain); + } + }); + }); + describe('with withObjectType', () => { it('not generate if withObjectType false', async () => { const schema = buildSchema(/* GraphQL */ ` From 6f25cd1889ace1557e971d8bda34d3aee7755b46 Mon Sep 17 00:00:00 2001 From: simonljus Date: Thu, 14 Dec 2023 18:46:11 +0100 Subject: [PATCH 11/14] should not contain typename --- src/myzod/index.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/myzod/index.ts b/src/myzod/index.ts index e4f0995a..abec91af 100644 --- a/src/myzod/index.ts +++ b/src/myzod/index.ts @@ -89,14 +89,7 @@ export class MyZodSchemaVisitor extends BaseSchemaVisitor { .export() .asKind('function') .withName(`${name}Schema(): myzod.Type<${name}>`) - .withBlock( - [ - indent(`return myzod.object({`), - indent(`__typename: myzod.literal('${node.name.value}').optional(),`, 2), - shape, - indent('})'), - ].join('\n') - ).string + appendArguments + .withBlock([indent(`return myzod.object({`), shape, indent('})')].join('\n')).string + appendArguments ); } }), From a40fc4fb0b33ca06f96d6d30fd18eb3c15cd9157 Mon Sep 17 00:00:00 2001 From: simonljus Date: Thu, 14 Dec 2023 18:56:03 +0100 Subject: [PATCH 12/14] add test case for yup --- tests/yup.spec.ts | 83 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/tests/yup.spec.ts b/tests/yup.spec.ts index bd4f8535..b88bcbe7 100644 --- a/tests/yup.spec.ts +++ b/tests/yup.spec.ts @@ -462,6 +462,89 @@ describe('yup', () => { 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', () => { From 924fe45df51e1b2172c752578f4cd35e70f9ab2e Mon Sep 17 00:00:00 2001 From: simonljus Date: Thu, 14 Dec 2023 18:56:15 +0100 Subject: [PATCH 13/14] add test case for myzod --- tests/myzod.spec.ts | 82 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/tests/myzod.spec.ts b/tests/myzod.spec.ts index 22149524..c8b3afbd 100644 --- a/tests/myzod.spec.ts +++ b/tests/myzod.spec.ts @@ -545,6 +545,88 @@ describe('myzod', () => { 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', () => { From c26e8e3c50ba5d8c418925bcde3784b065ce6e3e Mon Sep 17 00:00:00 2001 From: simonljus Date: Thu, 14 Dec 2023 19:18:13 +0100 Subject: [PATCH 14/14] add test to verify interface with no typename --- tests/myzod.spec.ts | 28 ++++++++++++++++++++++++++++ tests/yup.spec.ts | 32 ++++++++++++++++++++++++++++++-- tests/zod.spec.ts | 28 ++++++++++++++++++++++++++++ 3 files changed, 86 insertions(+), 2 deletions(-) diff --git a/tests/myzod.spec.ts b/tests/myzod.spec.ts index c8b3afbd..1a732852 100644 --- a/tests/myzod.spec.ts +++ b/tests/myzod.spec.ts @@ -511,6 +511,34 @@ describe('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 { diff --git a/tests/yup.spec.ts b/tests/yup.spec.ts index b88bcbe7..847daf6b 100644 --- a/tests/yup.spec.ts +++ b/tests/yup.spec.ts @@ -415,6 +415,34 @@ describe('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 { @@ -448,11 +476,11 @@ describe('yup', () => { 'export function BookSchema(): yup.ObjectSchema {', 'author: AuthorSchema().nullable().optional(),', - 'title: yup.string().defined().nonNullable()', + 'title: yup.string().defined().nullable().optional()', 'export function Book2Schema(): yup.ObjectSchema {', 'author: AuthorSchema().nonNullable(),', - 'title: yup.string().defined().nullable().optional()', + 'title: yup.string().defined().nonNullable()', ]; for (const wantContain of wantContains) { expect(result.content).toContain(wantContain); diff --git a/tests/zod.spec.ts b/tests/zod.spec.ts index 4c335fb3..1d44f4f2 100644 --- a/tests/zod.spec.ts +++ b/tests/zod.spec.ts @@ -583,6 +583,34 @@ describe('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 {