From 8e68319c73aced07fa8eb3d079a5a3e8ae3d1f90 Mon Sep 17 00:00:00 2001 From: simonljus Date: Tue, 12 Dec 2023 18:32:14 +0100 Subject: [PATCH 01/17] add withInterfaceType in config --- src/config.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/config.ts b/src/config.ts index 6475f9fc..32de59b3 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 21dce0e077300a690df5935ff21bd7742f477a9a Mon Sep 17 00:00:00 2001 From: simonljus Date: Wed, 13 Dec 2023 09:21:53 +0100 Subject: [PATCH 02/17] add builder for InterfaceTypeDefinition --- src/graphql.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/graphql.ts b/src/graphql.ts index 694b9b64..60421db3 100644 --- a/src/graphql.ts +++ b/src/graphql.ts @@ -4,6 +4,7 @@ import type { DefinitionNode, DocumentNode, GraphQLSchema, + InterfaceTypeDefinitionNode, ListTypeNode, NameNode, NamedTypeNode, @@ -23,6 +24,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 function ObjectTypeDefinitionBuilder(useObjectTypes: boolean | undefined, callback: ObjectTypeDefinitionFn): ObjectTypeDefinitionFn | undefined { if (!useObjectTypes) @@ -35,7 +37,17 @@ export function ObjectTypeDefinitionBuilder(useObjectTypes: boolean | undefined, }; } -export function topologicalSortAST(schema: GraphQLSchema, ast: DocumentNode): DocumentNode { +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 = [ 'ObjectTypeDefinition', From 3b84d3f287383411da8edfaeaca5bbe193fdf796 Mon Sep 17 00:00:00 2001 From: simonljus Date: Wed, 13 Dec 2023 09:25:19 +0100 Subject: [PATCH 03/17] support InterfaceTypeDefinitionNode --- src/visitor.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/visitor.ts b/src/visitor.ts index dc6960ca..b495b116 100644 --- a/src/visitor.ts +++ b/src/visitor.ts @@ -1,6 +1,12 @@ import { TsVisitor } from '@graphql-codegen/typescript'; -import type { FieldDefinitionNode, GraphQLSchema, NameNode, ObjectTypeDefinitionNode } from 'graphql'; -import { specifiedScalarTypes } from 'graphql'; +import { + FieldDefinitionNode, + GraphQLSchema, + InterfaceTypeDefinitionNode, + NameNode, + ObjectTypeDefinitionNode, + specifiedScalarTypes, +} from 'graphql'; import type { ValidationSchemaPluginConfig } from './config'; @@ -53,8 +59,8 @@ export class Visitor extends TsVisitor { } public buildArgumentsSchemaBlock( - node: ObjectTypeDefinitionNode, - callback: (typeName: string, field: FieldDefinitionNode) => string, + node: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode, + callback: (typeName: string, field: FieldDefinitionNode) => string ) { const fieldsWithArguments = node.fields?.filter(field => field.arguments && field.arguments.length > 0) ?? []; if (fieldsWithArguments.length === 0) From 124d39c5a400925b19ee161f2d07c52435495887 Mon Sep 17 00:00:00 2001 From: simonljus Date: Wed, 13 Dec 2023 09:27:25 +0100 Subject: [PATCH 04/17] rename to support interfaces --- src/myzod/index.ts | 4 ++-- src/schema_visitor.ts | 13 +++++++++++-- src/yup/index.ts | 4 ++-- src/zod/index.ts | 5 +++-- 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/myzod/index.ts b/src/myzod/index.ts index 4c2fc3b8..a89fd588 100644 --- a/src/myzod/index.ts +++ b/src/myzod/index.ts @@ -61,8 +61,8 @@ export class MyZodSchemaVisitor extends BaseSchemaVisitor { this.importTypes.push(name); // Building schema for field arguments. - const argumentBlocks = this.buildObjectTypeDefinitionArguments(node, visitor); - const appendArguments = argumentBlocks ? `\n${argumentBlocks}` : ''; + 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'); diff --git a/src/schema_visitor.ts b/src/schema_visitor.ts index 988a3f0d..577569f3 100644 --- a/src/schema_visitor.ts +++ b/src/schema_visitor.ts @@ -1,4 +1,10 @@ -import type { FieldDefinitionNode, GraphQLSchema, InputValueDefinitionNode, ObjectTypeDefinitionNode } from 'graphql'; +import { + FieldDefinitionNode, + GraphQLSchema, + InputValueDefinitionNode, + InterfaceTypeDefinitionNode, + ObjectTypeDefinitionNode, +} from 'graphql'; import type { ValidationSchemaPluginConfig } from './config'; import type { 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 f6e0afaa..68e48ebe 100644 --- a/src/yup/index.ts +++ b/src/yup/index.ts @@ -68,8 +68,8 @@ export class YupSchemaVisitor extends BaseSchemaVisitor { this.importTypes.push(name); // Building schema for field arguments. - const argumentBlocks = this.buildObjectTypeDefinitionArguments(node, visitor); - const appendArguments = argumentBlocks ? `\n${argumentBlocks}` : ''; + const argumentBlocks = this.buildTypeDefinitionArguments(node, visitor); + const appendArguments = argumentBlocks ? '\n' + argumentBlocks : ''; // Building schema for fields. const shape = shapeFields(node.fields, this.config, visitor); diff --git a/src/zod/index.ts b/src/zod/index.ts index eacbfe77..aaac826b 100644 --- a/src/zod/index.ts +++ b/src/zod/index.ts @@ -77,8 +77,8 @@ export class ZodSchemaVisitor extends BaseSchemaVisitor { this.importTypes.push(name); // Building schema for field arguments. - const argumentBlocks = this.buildObjectTypeDefinitionArguments(node, visitor); - const appendArguments = argumentBlocks ? `\n${argumentBlocks}` : ''; + 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'); @@ -279,6 +279,7 @@ function generateNameNodeZodSchema(config: ValidationSchemaPluginConfig, visitor const converter = visitor.getNameNodeConverter(node); switch (converter?.targetKind) { + case 'InterfaceTypeDefinition': case 'InputObjectTypeDefinition': case 'ObjectTypeDefinition': case 'UnionTypeDefinition': From 0659a025cb44f73c564fa75d1e84b549b92337da Mon Sep 17 00:00:00 2001 From: simonljus Date: Wed, 13 Dec 2023 09:28:07 +0100 Subject: [PATCH 05/17] build zod schema for interfaces --- src/zod/index.ts | 50 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/src/zod/index.ts b/src/zod/index.ts index aaac826b..0248ed59 100644 --- a/src/zod/index.ts +++ b/src/zod/index.ts @@ -5,6 +5,7 @@ import type { GraphQLSchema, InputObjectTypeDefinitionNode, InputValueDefinitionNode, + InterfaceTypeDefinitionNode, NameNode, ObjectTypeDefinitionNode, TypeNode, @@ -17,8 +18,15 @@ import { import type { ValidationSchemaPluginConfig } from '../config'; import { buildApi, formatDirectiveConfig } from '../directive'; import { BaseSchemaVisitor } from '../schema_visitor'; -import type { Visitor } from '../visitor'; -import { ObjectTypeDefinitionBuilder, isInput, isListType, isNamedType, isNonNullType } from './../graphql'; +import { Visitor } from '../visitor'; +import { + InterfaceTypeDefinitionBuilder, + isInput, + isListType, + isNamedType, + isNonNullType, + ObjectTypeDefinitionBuilder, +} from './../graphql'; const anySchema = `definedNonNullAnySchema`; @@ -69,6 +77,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 6ee7b36440c1c058628ad935f9bad446bdfecd7c Mon Sep 17 00:00:00 2001 From: simonljus Date: Wed, 13 Dec 2023 09:28:48 +0100 Subject: [PATCH 06/17] add interface type test cases for zod --- tests/zod.spec.ts | 142 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 141 insertions(+), 1 deletion(-) diff --git a/tests/zod.spec.ts b/tests/zod.spec.ts index 2dfe6473..095dfac3 100644 --- a/tests/zod.spec.ts +++ b/tests/zod.spec.ts @@ -1,4 +1,5 @@ -import { buildClientSchema, buildSchema, introspectionFromSchema } from 'graphql'; +import { getCachedDocumentNodeFromSchema } from '@graphql-codegen/plugin-helpers'; +import { buildClientSchema, buildSchema, introspectionFromSchema, isSpecifiedScalarType } from 'graphql'; import { dedent } from 'ts-dedent'; import { plugin } from '../src/index'; @@ -588,6 +589,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 28b340006dc06cf3df7a4ad80c75410256b21209 Mon Sep 17 00:00:00 2001 From: simonljus Date: Wed, 13 Dec 2023 21:18:02 +0100 Subject: [PATCH 07/17] 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 0248ed59..a6089ff8 100644 --- a/src/zod/index.ts +++ b/src/zod/index.ts @@ -339,7 +339,12 @@ function 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 9a40f72fb2905acf79dd2b098637a6d293de5677 Mon Sep 17 00:00:00 2001 From: simonljus Date: Wed, 13 Dec 2023 21:18:40 +0100 Subject: [PATCH 08/17] throw detailed error message instead of npe --- src/visitor.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/visitor.ts b/src/visitor.ts index b495b116..15f1da3e 100644 --- a/src/visitor.ts +++ b/src/visitor.ts @@ -42,8 +42,12 @@ export class Visitor extends TsVisitor { public getScalarType(scalarName: string): string | null { 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 1e49cc539db4e2d2b78e8dbe3edbd11e8fbcf1de Mon Sep 17 00:00:00 2001 From: simonljus Date: Wed, 13 Dec 2023 21:23:25 +0100 Subject: [PATCH 09/17] InterfaceTypeDefinition for myzod --- src/myzod/index.ts | 63 +++++++++++++++++++++++++++++++++++++++++++-- tests/myzod.spec.ts | 55 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+), 2 deletions(-) diff --git a/src/myzod/index.ts b/src/myzod/index.ts index a89fd588..260ce196 100644 --- a/src/myzod/index.ts +++ b/src/myzod/index.ts @@ -5,6 +5,7 @@ import type { GraphQLSchema, InputObjectTypeDefinitionNode, InputValueDefinitionNode, + InterfaceTypeDefinitionNode, NameNode, ObjectTypeDefinitionNode, TypeNode, @@ -17,8 +18,15 @@ import { import type { ValidationSchemaPluginConfig } from '../config'; import { buildApi, formatDirectiveConfig } from '../directive'; import { BaseSchemaVisitor } from '../schema_visitor'; -import type { Visitor } from '../visitor'; -import { ObjectTypeDefinitionBuilder, isInput, isListType, isNamedType, isNonNullType } from './../graphql'; +import { Visitor } from '../visitor'; +import { + InterfaceTypeDefinitionBuilder, + isInput, + isListType, + isNamedType, + isNonNullType, + ObjectTypeDefinitionBuilder, +} from './../graphql'; const anySchema = `definedNonNullAnySchema`; @@ -53,6 +61,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) => { @@ -266,6 +319,7 @@ function generateNameNodeMyZodSchema(config: ValidationSchemaPluginConfig, visit const converter = visitor.getNameNodeConverter(node); switch (converter?.targetKind) { + case 'InterfaceTypeDefinition': case 'InputObjectTypeDefinition': case 'ObjectTypeDefinition': case 'UnionTypeDefinition': @@ -279,7 +333,12 @@ function generateNameNodeMyZodSchema(config: ValidationSchemaPluginConfig, visit } 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 ed631755..66d1c347 100644 --- a/tests/myzod.spec.ts +++ b/tests/myzod.spec.ts @@ -486,6 +486,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 602400595668bb1908a95b175d234460167dbf96 Mon Sep 17 00:00:00 2001 From: simonljus Date: Thu, 14 Dec 2023 17:14:29 +0100 Subject: [PATCH 10/17] add InterfaceTypeDefinition support for yup --- src/yup/index.ts | 56 ++++++++++++++++++++++++++++++++++++-- tests/yup.spec.ts | 69 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 123 insertions(+), 2 deletions(-) diff --git a/src/yup/index.ts b/src/yup/index.ts index 68e48ebe..83065c07 100644 --- a/src/yup/index.ts +++ b/src/yup/index.ts @@ -5,6 +5,7 @@ import type { GraphQLSchema, InputObjectTypeDefinitionNode, InputValueDefinitionNode, + InterfaceTypeDefinitionNode, NameNode, ObjectTypeDefinitionNode, TypeNode, @@ -17,8 +18,15 @@ import { import type { ValidationSchemaPluginConfig } from '../config'; import { buildApi, formatDirectiveConfig } from '../directive'; import { BaseSchemaVisitor } from '../schema_visitor'; -import type { Visitor } from '../visitor'; -import { ObjectTypeDefinitionBuilder, isInput, isListType, isNamedType, isNonNullType } from './../graphql'; +import { Visitor } from '../visitor'; +import { + InterfaceTypeDefinitionBuilder, + isInput, + isListType, + isNamedType, + isNonNullType, + ObjectTypeDefinitionBuilder, +} from './../graphql'; export class YupSchemaVisitor extends BaseSchemaVisitor { constructor(schema: GraphQLSchema, config: ValidationSchemaPluginConfig) { @@ -60,6 +68,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) => { @@ -286,6 +337,7 @@ function 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 5f2caf04..f4101c91 100644 --- a/tests/yup.spec.ts +++ b/tests/yup.spec.ts @@ -392,6 +392,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 80a0f53ec71a003cbb14bf9d494eff0c76953fb4 Mon Sep 17 00:00:00 2001 From: simonljus Date: Thu, 14 Dec 2023 18:46:11 +0100 Subject: [PATCH 11/17] 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 260ce196..05a67ccb 100644 --- a/src/myzod/index.ts +++ b/src/myzod/index.ts @@ -92,14 +92,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 92defaa367fb8a45b3ac48b6f63c1c4cf2c0bc1b Mon Sep 17 00:00:00 2001 From: simonljus Date: Thu, 14 Dec 2023 18:56:03 +0100 Subject: [PATCH 12/17] 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 f4101c91..8860664a 100644 --- a/tests/yup.spec.ts +++ b/tests/yup.spec.ts @@ -459,6 +459,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 24cc8b1a506087f9ad21d322bc3ac54a5a81d9e7 Mon Sep 17 00:00:00 2001 From: simonljus Date: Thu, 14 Dec 2023 18:56:15 +0100 Subject: [PATCH 13/17] 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 66d1c347..50dfb43a 100644 --- a/tests/myzod.spec.ts +++ b/tests/myzod.spec.ts @@ -539,6 +539,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 dc754140946c812b3a011228f77fbd71044bb2ad Mon Sep 17 00:00:00 2001 From: simonljus Date: Thu, 14 Dec 2023 19:18:13 +0100 Subject: [PATCH 14/17] 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 50dfb43a..e53c319a 100644 --- a/tests/myzod.spec.ts +++ b/tests/myzod.spec.ts @@ -505,6 +505,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 8860664a..429f7329 100644 --- a/tests/yup.spec.ts +++ b/tests/yup.spec.ts @@ -412,6 +412,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 { @@ -445,11 +473,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 095dfac3..084ca715 100644 --- a/tests/zod.spec.ts +++ b/tests/zod.spec.ts @@ -608,6 +608,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 { From 4684976c7a3d3bef64051d85ec0671845440b387 Mon Sep 17 00:00:00 2001 From: Kei Kamikawa Date: Sun, 7 Apr 2024 01:31:10 +0900 Subject: [PATCH 15/17] fixed failing conflict code --- src/visitor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/visitor.ts b/src/visitor.ts index 15f1da3e..d69592c4 100644 --- a/src/visitor.ts +++ b/src/visitor.ts @@ -40,7 +40,7 @@ export class Visitor extends TsVisitor { } public getScalarType(scalarName: string): string | null { - if (this.scalarDirection === 'both') + if (this.scalarDirection === 'both') { return null; } const scalar = this.scalars[scalarName]; From d7751ef6ce844b5254b030aff3aa04d747794c84 Mon Sep 17 00:00:00 2001 From: Kei Kamikawa Date: Sun, 7 Apr 2024 01:37:42 +0900 Subject: [PATCH 16/17] updated example for interface --- codegen.yml | 3 +++ example/myzod/schemas.ts | 8 +++++++- example/test.graphql | 6 +++++- example/types.ts | 6 +++++- example/yup/schemas.ts | 8 +++++++- example/zod/schemas.ts | 8 +++++++- 6 files changed, 34 insertions(+), 5 deletions(-) diff --git a/codegen.yml b/codegen.yml index f3170225..02311409 100644 --- a/codegen.yml +++ b/codegen.yml @@ -13,6 +13,7 @@ generates: schema: yup importFrom: ../types withObjectType: true + withInterfaceType: true directives: required: msg: required @@ -49,6 +50,7 @@ generates: schema: zod importFrom: ../types withObjectType: true + withInterfaceType: true directives: # Write directives like # @@ -72,6 +74,7 @@ generates: schema: myzod importFrom: ../types withObjectType: true + withInterfaceType: true directives: constraint: minLength: min diff --git a/example/myzod/schemas.ts b/example/myzod/schemas.ts index 1b4cb08e..68c8623d 100644 --- a/example/myzod/schemas.ts +++ b/example/myzod/schemas.ts @@ -1,5 +1,5 @@ import * as myzod from 'myzod' -import { Admin, AttributeInput, ButtonComponentType, ComponentInput, DropDownComponentInput, EventArgumentInput, EventInput, EventOptionType, Guest, HttpInput, HttpMethod, LayoutInput, MyType, MyTypeFooArgs, PageInput, PageType, User } from '../types' +import { Admin, AttributeInput, ButtonComponentType, ComponentInput, DropDownComponentInput, EventArgumentInput, EventInput, EventOptionType, Guest, HttpInput, HttpMethod, LayoutInput, MyType, MyTypeFooArgs, Namer, PageInput, PageType, User } from '../types' export const definedNonNullAnySchema = myzod.object({}); @@ -92,6 +92,12 @@ export function MyTypeFooArgsSchema(): myzod.Type { }) } +export function NamerSchema(): myzod.Type { + return myzod.object({ + name: myzod.string().optional().nullable() + }) +} + export function PageInputSchema(): myzod.Type { return myzod.object({ attributes: myzod.array(myzod.lazy(() => AttributeInputSchema())).optional().nullable(), diff --git a/example/test.graphql b/example/test.graphql index e314b59a..eb3a699e 100644 --- a/example/test.graphql +++ b/example/test.graphql @@ -15,7 +15,7 @@ type Guest { union UserKind = Admin | Guest -type User { +type User implements Namer { id: ID name: String email: String @@ -25,6 +25,10 @@ type User { updatedAt: Date } +interface Namer { + name: String +} + input PageInput { id: ID! title: String! diff --git a/example/types.ts b/example/types.ts index 17afc628..d1c67290 100644 --- a/example/types.ts +++ b/example/types.ts @@ -91,6 +91,10 @@ export type MyTypeFooArgs = { d: Scalars['Float']['input']; }; +export type Namer = { + name?: Maybe; +}; + export type PageInput = { attributes?: InputMaybe>; date?: InputMaybe; @@ -112,7 +116,7 @@ export enum PageType { Service = 'SERVICE' } -export type User = { +export type User = Namer & { __typename?: 'User'; createdAt?: Maybe; email?: Maybe; diff --git a/example/yup/schemas.ts b/example/yup/schemas.ts index 8ae15650..abc049aa 100644 --- a/example/yup/schemas.ts +++ b/example/yup/schemas.ts @@ -1,5 +1,5 @@ import * as yup from 'yup' -import { Admin, AttributeInput, ButtonComponentType, ComponentInput, DropDownComponentInput, EventArgumentInput, EventInput, EventOptionType, Guest, HttpInput, HttpMethod, LayoutInput, MyType, MyTypeFooArgs, PageInput, PageType, User, UserKind } from '../types' +import { Admin, AttributeInput, ButtonComponentType, ComponentInput, DropDownComponentInput, EventArgumentInput, EventInput, EventOptionType, Guest, HttpInput, HttpMethod, LayoutInput, MyType, MyTypeFooArgs, Namer, PageInput, PageType, User, UserKind } from '../types' export const ButtonComponentTypeSchema = yup.string().oneOf(Object.values(ButtonComponentType)).defined(); @@ -96,6 +96,12 @@ export function MyTypeFooArgsSchema(): yup.ObjectSchema { }) } +export function NamerSchema(): yup.ObjectSchema { + return yup.object({ + name: yup.string().defined().nullable().optional() + }) +} + export function PageInputSchema(): yup.ObjectSchema { return yup.object({ attributes: yup.array(yup.lazy(() => AttributeInputSchema().nonNullable())).defined().nullable().optional(), diff --git a/example/zod/schemas.ts b/example/zod/schemas.ts index b6fb5679..407e3895 100644 --- a/example/zod/schemas.ts +++ b/example/zod/schemas.ts @@ -1,5 +1,5 @@ import { z } from 'zod' -import { Admin, AttributeInput, ButtonComponentType, ComponentInput, DropDownComponentInput, EventArgumentInput, EventInput, EventOptionType, Guest, HttpInput, HttpMethod, LayoutInput, MyType, MyTypeFooArgs, PageInput, PageType, User } from '../types' +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; @@ -100,6 +100,12 @@ export function MyTypeFooArgsSchema(): z.ZodObject> { }) } +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(), From 08dbfa042bf1e1af47783a2c80cd3479862853ca Mon Sep 17 00:00:00 2001 From: Kei Kamikawa Date: Sun, 7 Apr 2024 01:38:14 +0900 Subject: [PATCH 17/17] lint-fix --- src/config.ts | 2 +- src/graphql.ts | 14 ++++++-------- src/myzod/index.ts | 12 ++++++------ src/schema_visitor.ts | 4 ++-- src/visitor.ts | 14 ++++++++------ src/yup/index.ts | 10 +++++----- src/zod/index.ts | 12 ++++++------ tests/myzod.spec.ts | 29 +++++++++++++---------------- tests/yup.spec.ts | 32 ++++++++++++++------------------ tests/zod.spec.ts | 32 ++++++++++++++------------------ 10 files changed, 75 insertions(+), 86 deletions(-) diff --git a/src/config.ts b/src/config.ts index 32de59b3..a91c1652 100644 --- a/src/config.ts +++ b/src/config.ts @@ -211,7 +211,7 @@ export interface ValidationSchemaPluginConfig extends TypeScriptPluginConfig { * withInterfaceType: true * ``` */ - withInterfaceType?: boolean; + withInterfaceType?: boolean /** * @description Specify validation schema export type. * @default function diff --git a/src/graphql.ts b/src/graphql.ts index 60421db3..60c847fb 100644 --- a/src/graphql.ts +++ b/src/graphql.ts @@ -37,17 +37,15 @@ export function ObjectTypeDefinitionBuilder(useObjectTypes: boolean | undefined, }; } -export const InterfaceTypeDefinitionBuilder = ( - useInterfaceTypes: boolean | undefined, - callback: InterfaceTypeDefinitionFn -): InterfaceTypeDefinitionFn | undefined => { - if (!useInterfaceTypes) return undefined; - return node => { +export function 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 => { +export function topologicalSortAST(schema: GraphQLSchema, ast: DocumentNode): DocumentNode { const dependencyGraph = new Graph(); const targetKinds = [ 'ObjectTypeDefinition', diff --git a/src/myzod/index.ts b/src/myzod/index.ts index 05a67ccb..319f41ad 100644 --- a/src/myzod/index.ts +++ b/src/myzod/index.ts @@ -18,14 +18,14 @@ import { import type { ValidationSchemaPluginConfig } from '../config'; import { buildApi, formatDirectiveConfig } from '../directive'; import { BaseSchemaVisitor } from '../schema_visitor'; -import { Visitor } from '../visitor'; +import type { Visitor } from '../visitor'; import { InterfaceTypeDefinitionBuilder, + ObjectTypeDefinitionBuilder, isInput, isListType, isNamedType, isNonNullType, - ObjectTypeDefinitionBuilder, } from './../graphql'; const anySchema = `definedNonNullAnySchema`; @@ -70,7 +70,7 @@ export class MyZodSchemaVisitor extends BaseSchemaVisitor { // Building schema for field arguments. const argumentBlocks = this.buildTypeDefinitionArguments(node, visitor); - const appendArguments = argumentBlocks ? '\n' + argumentBlocks : ''; + const appendArguments = argumentBlocks ? `\n${argumentBlocks}` : ''; // Building schema for fields. const shape = node.fields?.map(field => generateFieldMyZodSchema(this.config, visitor, field, 2)).join(',\n'); @@ -108,7 +108,7 @@ export class MyZodSchemaVisitor extends BaseSchemaVisitor { // Building schema for field arguments. const argumentBlocks = this.buildTypeDefinitionArguments(node, visitor); - const appendArguments = argumentBlocks ? '\n' + argumentBlocks : ''; + const appendArguments = argumentBlocks ? `\n${argumentBlocks}` : ''; // Building schema for fields. const shape = node.fields?.map(field => generateFieldMyZodSchema(this.config, visitor, field, 2)).join(',\n'); @@ -329,9 +329,9 @@ function generateNameNodeMyZodSchema(config: ValidationSchemaPluginConfig, visit case 'ScalarTypeDefinition': return myzod4Scalar(config, visitor, node.value); default: - if (converter?.targetKind) { + 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 577569f3..c339cf2e 100644 --- a/src/schema_visitor.ts +++ b/src/schema_visitor.ts @@ -1,4 +1,4 @@ -import { +import type { FieldDefinitionNode, GraphQLSchema, InputValueDefinitionNode, @@ -47,7 +47,7 @@ export abstract class BaseSchemaVisitor implements SchemaVisitor { protected buildTypeDefinitionArguments( node: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode, - visitor: Visitor + visitor: Visitor, ) { return visitor.buildArgumentsSchemaBlock(node, (typeName, field) => { this.importTypes.push(typeName); diff --git a/src/visitor.ts b/src/visitor.ts index d69592c4..5d733351 100644 --- a/src/visitor.ts +++ b/src/visitor.ts @@ -1,10 +1,12 @@ import { TsVisitor } from '@graphql-codegen/typescript'; -import { +import type { FieldDefinitionNode, GraphQLSchema, InterfaceTypeDefinitionNode, NameNode, ObjectTypeDefinitionNode, +} from 'graphql'; +import { specifiedScalarTypes, } from 'graphql'; @@ -40,13 +42,13 @@ export class Visitor extends TsVisitor { } public getScalarType(scalarName: string): string | null { - if (this.scalarDirection === 'both') { + if (this.scalarDirection === 'both') return null; - } + const scalar = this.scalars[scalarName]; - if (!scalar) { + if (!scalar) throw new Error(`Unknown scalar ${scalarName}`); - } + return scalar[this.scalarDirection]; } @@ -64,7 +66,7 @@ export class Visitor extends TsVisitor { public buildArgumentsSchemaBlock( node: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode, - callback: (typeName: string, field: FieldDefinitionNode) => string + callback: (typeName: string, field: FieldDefinitionNode) => string, ) { const fieldsWithArguments = node.fields?.filter(field => field.arguments && field.arguments.length > 0) ?? []; if (fieldsWithArguments.length === 0) diff --git a/src/yup/index.ts b/src/yup/index.ts index 83065c07..c10710b7 100644 --- a/src/yup/index.ts +++ b/src/yup/index.ts @@ -18,14 +18,14 @@ import { import type { ValidationSchemaPluginConfig } from '../config'; import { buildApi, formatDirectiveConfig } from '../directive'; import { BaseSchemaVisitor } from '../schema_visitor'; -import { Visitor } from '../visitor'; +import type { Visitor } from '../visitor'; import { InterfaceTypeDefinitionBuilder, + ObjectTypeDefinitionBuilder, isInput, isListType, isNamedType, isNonNullType, - ObjectTypeDefinitionBuilder, } from './../graphql'; export class YupSchemaVisitor extends BaseSchemaVisitor { @@ -77,11 +77,11 @@ export class YupSchemaVisitor extends BaseSchemaVisitor { // Building schema for field arguments. const argumentBlocks = this.buildTypeDefinitionArguments(node, visitor); - const appendArguments = argumentBlocks ? '\n' + argumentBlocks : ''; + const appendArguments = argumentBlocks ? `\n${argumentBlocks}` : ''; // Building schema for fields. const shape = node.fields - ?.map(field => { + ?.map((field) => { const fieldSchema = generateFieldYupSchema(this.config, visitor, field, 2); return isNonNullType(field.type) ? fieldSchema : `${fieldSchema}.optional()`; }) @@ -120,7 +120,7 @@ export class YupSchemaVisitor extends BaseSchemaVisitor { // Building schema for field arguments. const argumentBlocks = this.buildTypeDefinitionArguments(node, visitor); - const appendArguments = argumentBlocks ? '\n' + argumentBlocks : ''; + const appendArguments = argumentBlocks ? `\n${argumentBlocks}` : ''; // Building schema for fields. const shape = shapeFields(node.fields, this.config, visitor); diff --git a/src/zod/index.ts b/src/zod/index.ts index a6089ff8..85ecb09d 100644 --- a/src/zod/index.ts +++ b/src/zod/index.ts @@ -18,14 +18,14 @@ import { import type { ValidationSchemaPluginConfig } from '../config'; import { buildApi, formatDirectiveConfig } from '../directive'; import { BaseSchemaVisitor } from '../schema_visitor'; -import { Visitor } from '../visitor'; +import type { Visitor } from '../visitor'; import { InterfaceTypeDefinitionBuilder, + ObjectTypeDefinitionBuilder, isInput, isListType, isNamedType, isNonNullType, - ObjectTypeDefinitionBuilder, } from './../graphql'; const anySchema = `definedNonNullAnySchema`; @@ -86,7 +86,7 @@ export class ZodSchemaVisitor extends BaseSchemaVisitor { // Building schema for field arguments. const argumentBlocks = this.buildTypeDefinitionArguments(node, visitor); - const appendArguments = argumentBlocks ? '\n' + argumentBlocks : ''; + const appendArguments = argumentBlocks ? `\n${argumentBlocks}` : ''; // Building schema for fields. const shape = node.fields?.map(field => generateFieldZodSchema(this.config, visitor, field, 2)).join(',\n'); @@ -124,7 +124,7 @@ export class ZodSchemaVisitor extends BaseSchemaVisitor { // Building schema for field arguments. const argumentBlocks = this.buildTypeDefinitionArguments(node, visitor); - const appendArguments = argumentBlocks ? '\n' + argumentBlocks : ''; + const appendArguments = argumentBlocks ? `\n${argumentBlocks}` : ''; // Building schema for fields. const shape = node.fields?.map(field => generateFieldZodSchema(this.config, visitor, field, 2)).join(',\n'); @@ -342,9 +342,9 @@ function generateNameNodeZodSchema(config: ValidationSchemaPluginConfig, visitor case 'ScalarTypeDefinition': return zod4Scalar(config, visitor, node.value); default: - if (converter?.targetKind) { + 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 e53c319a..ac822e0e 100644 --- a/tests/myzod.spec.ts +++ b/tests/myzod.spec.ts @@ -500,7 +500,7 @@ describe('myzod', () => { { schema: 'myzod', }, - {} + {}, ); expect(result.content).not.toContain('export function UserSchema(): myzod.Type {'); }); @@ -518,19 +518,18 @@ describe('myzod', () => { 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) { + const wantNotContains = ['__typename: myzod.literal(\'Book\')']; + for (const wantContain of wantContains) expect(result.content).toContain(wantContain); - } - for (const wantNotContain of wantNotContains) { + + for (const wantNotContain of wantNotContains) expect(result.content).not.toContain(wantNotContain); - } }); it('generate interface type contains interface type', async () => { @@ -552,7 +551,7 @@ describe('myzod', () => { schema: 'myzod', withInterfaceType: true, }, - {} + {}, ); const wantContains = [ 'export function AuthorSchema(): myzod.Type {', @@ -563,9 +562,8 @@ describe('myzod', () => { 'author: AuthorSchema().optional().nullable(),', 'title: myzod.string().optional().nullable()', ]; - for (const wantContain of wantContains) { + for (const wantContain of wantContains) expect(result.content).toContain(wantContain); - } }); it('generate object type contains interface type', async () => { const schema = buildSchema(/* GraphQL */ ` @@ -599,7 +597,7 @@ describe('myzod', () => { withInterfaceType: true, withObjectType: true, }, - {} + {}, ); const wantContains = [ [ @@ -614,7 +612,7 @@ describe('myzod', () => { [ 'export function TextbookSchema(): myzod.Type {', 'return myzod.object({', - "__typename: myzod.literal('Textbook').optional(),", + '__typename: myzod.literal(\'Textbook\').optional(),', 'title: myzod.string(),', 'author: AuthorSchema(),', 'courses: myzod.array(myzod.string())', @@ -625,7 +623,7 @@ describe('myzod', () => { [ 'export function ColoringBookSchema(): myzod.Type {', 'return myzod.object({', - "__typename: myzod.literal('ColoringBook').optional(),", + '__typename: myzod.literal(\'ColoringBook\').optional(),', 'title: myzod.string(),', 'author: AuthorSchema(),', 'colors: myzod.array(myzod.string())', @@ -636,7 +634,7 @@ describe('myzod', () => { [ 'export function AuthorSchema(): myzod.Type {', 'return myzod.object({', - "__typename: myzod.literal('Author').optional()", + '__typename: myzod.literal(\'Author\').optional()', 'books: myzod.array(BookSchema()).optional().nullable()', 'name: myzod.string().optional().nullable()', '})', @@ -644,9 +642,8 @@ describe('myzod', () => { ], ]; for (const wantContain of wantContains) { - for (const wantContainLine of wantContain) { + for (const wantContainLine of wantContain) expect(result.content).toContain(wantContainLine); - } } }); }); diff --git a/tests/yup.spec.ts b/tests/yup.spec.ts index 429f7329..466492b3 100644 --- a/tests/yup.spec.ts +++ b/tests/yup.spec.ts @@ -407,7 +407,7 @@ describe('yup', () => { { schema: 'yup', }, - {} + {}, ); expect(result.content).not.toContain('export function UserSchema(): yup.ObjectSchema {'); }); @@ -425,19 +425,18 @@ describe('yup', () => { 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) { + const wantNotContains = ['__typename: yup.string<\'Book\'>().optional()']; + for (const wantContain of wantContains) expect(result.content).toContain(wantContain); - } - for (const wantNotContain of wantNotContains) { + + for (const wantNotContain of wantNotContains) expect(result.content).not.toContain(wantNotContain); - } }); it('generate interface type contains interface type', async () => { @@ -464,7 +463,7 @@ describe('yup', () => { schema: 'yup', withInterfaceType: true, }, - {} + {}, ); const wantContains = [ 'export function AuthorSchema(): yup.ObjectSchema {', @@ -479,13 +478,11 @@ describe('yup', () => { 'author: AuthorSchema().nonNullable(),', 'title: yup.string().defined().nonNullable()', ]; - for (const wantContain of wantContains) { + for (const wantContain of wantContains) expect(result.content).toContain(wantContain); - } - for (const wantNotContain of ['Query', 'Mutation', 'Subscription']) { + 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 */ ` @@ -519,7 +516,7 @@ describe('yup', () => { withInterfaceType: true, withObjectType: true, }, - {} + {}, ); const wantContains = [ [ @@ -534,7 +531,7 @@ describe('yup', () => { [ 'export function TextbookSchema(): yup.ObjectSchema {', 'return yup.object({', - "__typename: yup.string<'Textbook'>().optional(),", + '__typename: yup.string<\'Textbook\'>().optional(),', 'title: yup.string().defined().nonNullable(),', 'author: AuthorSchema().nonNullable(),', 'courses: yup.array(yup.string().defined().nonNullable()).defined()', @@ -545,7 +542,7 @@ describe('yup', () => { [ 'export function ColoringBookSchema(): yup.ObjectSchema {', 'return yup.object({', - "__typename: yup.string<'ColoringBook'>().optional(),", + '__typename: yup.string<\'ColoringBook\'>().optional(),', 'title: yup.string().defined().nonNullable(),', 'author: AuthorSchema().nonNullable(),', 'colors: yup.array(yup.string().defined().nonNullable()).defined()', @@ -556,7 +553,7 @@ describe('yup', () => { [ 'export function AuthorSchema(): yup.ObjectSchema {', 'return yup.object({', - "__typename: yup.string<'Author'>().optional(),", + '__typename: yup.string<\'Author\'>().optional(),', 'books: yup.array(BookSchema().nonNullable()).defined().nullable().optional(),', 'name: yup.string().defined().nullable().optional()', '})', @@ -565,9 +562,8 @@ describe('yup', () => { ]; for (const wantContain of wantContains) { - for (const wantContainLine of wantContain) { + for (const wantContainLine of wantContain) expect(result.content).toContain(wantContainLine); - } } }); }); diff --git a/tests/zod.spec.ts b/tests/zod.spec.ts index 084ca715..8527a5a5 100644 --- a/tests/zod.spec.ts +++ b/tests/zod.spec.ts @@ -1,5 +1,4 @@ -import { getCachedDocumentNodeFromSchema } from '@graphql-codegen/plugin-helpers'; -import { buildClientSchema, buildSchema, introspectionFromSchema, isSpecifiedScalarType } from 'graphql'; +import { buildClientSchema, buildSchema, introspectionFromSchema } from 'graphql'; import { dedent } from 'ts-dedent'; import { plugin } from '../src/index'; @@ -603,7 +602,7 @@ describe('zod', () => { { schema: 'zod', }, - {} + {}, ); expect(result.content).not.toContain('export function UserSchema(): z.ZodObject>'); }); @@ -621,19 +620,18 @@ describe('zod', () => { 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) { + const wantNotContains = ['__typename: z.literal(\'Book\')']; + for (const wantContain of wantContains) expect(result.content).toContain(wantContain); - } - for (const wantNotContain of wantNotContains) { + + for (const wantNotContain of wantNotContains) expect(result.content).not.toContain(wantNotContain); - } }); it('generate interface type contains interface type', async () => { @@ -655,7 +653,7 @@ describe('zod', () => { schema: 'zod', withInterfaceType: true, }, - {} + {}, ); const wantContains = [ 'export function AuthorSchema(): z.ZodObject> {', @@ -666,9 +664,8 @@ describe('zod', () => { 'author: AuthorSchema().nullish(),', 'title: z.string().nullish()', ]; - for (const wantContain of wantContains) { + for (const wantContain of wantContains) expect(result.content).toContain(wantContain); - } }); it('generate object type contains interface type', async () => { @@ -703,7 +700,7 @@ describe('zod', () => { withInterfaceType: true, withObjectType: true, }, - {} + {}, ); const wantContains = [ [ @@ -718,7 +715,7 @@ describe('zod', () => { [ 'export function TextbookSchema(): z.ZodObject> {', 'return z.object({', - "__typename: z.literal('Textbook').optional(),", + '__typename: z.literal(\'Textbook\').optional(),', 'title: z.string(),', 'author: AuthorSchema(),', 'courses: z.array(z.string())', @@ -729,7 +726,7 @@ describe('zod', () => { [ 'export function ColoringBookSchema(): z.ZodObject> {', 'return z.object({', - "__typename: z.literal('ColoringBook').optional(),", + '__typename: z.literal(\'ColoringBook\').optional(),', 'title: z.string(),', 'author: AuthorSchema(),', 'colors: z.array(z.string())', @@ -740,7 +737,7 @@ describe('zod', () => { [ 'export function AuthorSchema(): z.ZodObject> {', 'return z.object({', - "__typename: z.literal('Author').optional()", + '__typename: z.literal(\'Author\').optional()', 'books: z.array(BookSchema()).nullish()', 'name: z.string().nullish()', '})', @@ -749,9 +746,8 @@ describe('zod', () => { ]; for (const wantContain of wantContains) { - for (const wantContainLine of wantContain) { + for (const wantContainLine of wantContain) expect(result.content).toContain(wantContainLine); - } } }); });