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(), diff --git a/src/config.ts b/src/config.ts index 6475f9fc..a91c1652 100644 --- a/src/config.ts +++ b/src/config.ts @@ -194,6 +194,24 @@ export interface ValidationSchemaPluginConfig extends TypeScriptPluginConfig { * ``` */ withObjectType?: boolean + /** + * @description Generates validation schema with GraphQL type interfaces. + * + * @exampleMarkdown + * ```yml + * generates: + * path/to/types.ts: + * plugins: + * - typescript + * path/to/schemas.ts: + * plugins: + * - graphql-codegen-validation-schema + * config: + * schema: yup + * withInterfaceType: true + * ``` + */ + withInterfaceType?: boolean /** * @description Specify validation schema export type. * @default function diff --git a/src/graphql.ts b/src/graphql.ts index 694b9b64..60c847fb 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,6 +37,14 @@ export function ObjectTypeDefinitionBuilder(useObjectTypes: boolean | undefined, }; } +export function InterfaceTypeDefinitionBuilder(useInterfaceTypes: boolean | undefined, callback: InterfaceTypeDefinitionFn): InterfaceTypeDefinitionFn | undefined { + if (!useInterfaceTypes) + return undefined; + return (node) => { + return callback(node); + }; +} + export function topologicalSortAST(schema: GraphQLSchema, ast: DocumentNode): DocumentNode { const dependencyGraph = new Graph(); const targetKinds = [ diff --git a/src/myzod/index.ts b/src/myzod/index.ts index 4c2fc3b8..319f41ad 100644 --- a/src/myzod/index.ts +++ b/src/myzod/index.ts @@ -5,6 +5,7 @@ import type { GraphQLSchema, InputObjectTypeDefinitionNode, InputValueDefinitionNode, + InterfaceTypeDefinitionNode, NameNode, ObjectTypeDefinitionNode, TypeNode, @@ -18,7 +19,14 @@ 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 { + InterfaceTypeDefinitionBuilder, + ObjectTypeDefinitionBuilder, + isInput, + isListType, + isNamedType, + isNonNullType, +} from './../graphql'; const anySchema = `definedNonNullAnySchema`; @@ -53,6 +61,44 @@ export class MyZodSchemaVisitor extends BaseSchemaVisitor { }; } + get InterfaceTypeDefinition() { + return { + leave: InterfaceTypeDefinitionBuilder(this.config.withInterfaceType, (node: InterfaceTypeDefinitionNode) => { + const visitor = this.createVisitor('output'); + const name = visitor.convertName(node.name.value); + this.importTypes.push(name); + + // Building schema for field arguments. + const argumentBlocks = this.buildTypeDefinitionArguments(node, visitor); + const appendArguments = argumentBlocks ? `\n${argumentBlocks}` : ''; + + // Building schema for fields. + const shape = node.fields?.map(field => generateFieldMyZodSchema(this.config, visitor, field, 2)).join(',\n'); + + switch (this.config.validationSchemaExportType) { + case 'const': + return ( + new DeclarationBlock({}) + .export() + .asKind('const') + .withName(`${name}Schema: myzod.Type<${name}>`) + .withContent([`myzod.object({`, shape, '})'].join('\n')).string + appendArguments + ); + + case 'function': + default: + return ( + new DeclarationBlock({}) + .export() + .asKind('function') + .withName(`${name}Schema(): myzod.Type<${name}>`) + .withBlock([indent(`return myzod.object({`), shape, indent('})')].join('\n')).string + appendArguments + ); + } + }), + }; + } + get ObjectTypeDefinition() { return { leave: ObjectTypeDefinitionBuilder(this.config.withObjectType, (node: ObjectTypeDefinitionNode) => { @@ -61,7 +107,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. @@ -266,6 +312,7 @@ function generateNameNodeMyZodSchema(config: ValidationSchemaPluginConfig, visit const converter = visitor.getNameNodeConverter(node); switch (converter?.targetKind) { + case 'InterfaceTypeDefinition': case 'InputObjectTypeDefinition': case 'ObjectTypeDefinition': case 'UnionTypeDefinition': @@ -279,7 +326,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/src/schema_visitor.ts b/src/schema_visitor.ts index 988a3f0d..c339cf2e 100644 --- a/src/schema_visitor.ts +++ b/src/schema_visitor.ts @@ -1,4 +1,10 @@ -import type { FieldDefinitionNode, GraphQLSchema, InputValueDefinitionNode, ObjectTypeDefinitionNode } from 'graphql'; +import type { + 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/visitor.ts b/src/visitor.ts index dc6960ca..5d733351 100644 --- a/src/visitor.ts +++ b/src/visitor.ts @@ -1,6 +1,14 @@ import { TsVisitor } from '@graphql-codegen/typescript'; -import type { FieldDefinitionNode, GraphQLSchema, NameNode, ObjectTypeDefinitionNode } from 'graphql'; -import { specifiedScalarTypes } from 'graphql'; +import type { + FieldDefinitionNode, + GraphQLSchema, + InterfaceTypeDefinitionNode, + NameNode, + ObjectTypeDefinitionNode, +} from 'graphql'; +import { + specifiedScalarTypes, +} from 'graphql'; import type { ValidationSchemaPluginConfig } from './config'; @@ -37,7 +45,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 { @@ -53,7 +65,7 @@ export class Visitor extends TsVisitor { } public buildArgumentsSchemaBlock( - node: ObjectTypeDefinitionNode, + node: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode, callback: (typeName: string, field: FieldDefinitionNode) => string, ) { const fieldsWithArguments = node.fields?.filter(field => field.arguments && field.arguments.length > 0) ?? []; diff --git a/src/yup/index.ts b/src/yup/index.ts index f6e0afaa..c10710b7 100644 --- a/src/yup/index.ts +++ b/src/yup/index.ts @@ -5,6 +5,7 @@ import type { GraphQLSchema, InputObjectTypeDefinitionNode, InputValueDefinitionNode, + InterfaceTypeDefinitionNode, NameNode, ObjectTypeDefinitionNode, TypeNode, @@ -18,7 +19,14 @@ 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 { + InterfaceTypeDefinitionBuilder, + ObjectTypeDefinitionBuilder, + isInput, + isListType, + isNamedType, + isNonNullType, +} 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) => { @@ -68,7 +119,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. @@ -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/src/zod/index.ts b/src/zod/index.ts index eacbfe77..85ecb09d 100644 --- a/src/zod/index.ts +++ b/src/zod/index.ts @@ -5,6 +5,7 @@ import type { GraphQLSchema, InputObjectTypeDefinitionNode, InputValueDefinitionNode, + InterfaceTypeDefinitionNode, NameNode, ObjectTypeDefinitionNode, TypeNode, @@ -18,7 +19,14 @@ 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 { + InterfaceTypeDefinitionBuilder, + ObjectTypeDefinitionBuilder, + isInput, + isListType, + isNamedType, + isNonNullType, +} 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) => { @@ -77,7 +123,7 @@ export class ZodSchemaVisitor extends BaseSchemaVisitor { this.importTypes.push(name); // Building schema for field arguments. - const argumentBlocks = this.buildObjectTypeDefinitionArguments(node, visitor); + const argumentBlocks = this.buildTypeDefinitionArguments(node, visitor); const appendArguments = argumentBlocks ? `\n${argumentBlocks}` : ''; // Building schema for fields. @@ -279,6 +325,7 @@ function generateNameNodeZodSchema(config: ValidationSchemaPluginConfig, visitor const converter = visitor.getNameNodeConverter(node); switch (converter?.targetKind) { + case 'InterfaceTypeDefinition': case 'InputObjectTypeDefinition': case 'ObjectTypeDefinition': case 'UnionTypeDefinition': @@ -292,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); } } diff --git a/tests/myzod.spec.ts b/tests/myzod.spec.ts index ed631755..ac822e0e 100644 --- a/tests/myzod.spec.ts +++ b/tests/myzod.spec.ts @@ -486,6 +486,168 @@ describe('myzod', () => { }); }); + describe('with withInterfaceType', () => { + it('not generate if withObjectType false', async () => { + const schema = buildSchema(/* GraphQL */ ` + interface User { + id: ID! + name: String + } + `); + const result = await plugin( + schema, + [], + { + schema: 'myzod', + }, + {}, + ); + expect(result.content).not.toContain('export function UserSchema(): myzod.Type {'); + }); + + it('generate if withInterfaceType true', async () => { + const schema = buildSchema(/* GraphQL */ ` + interface Book { + title: String + } + `); + const result = await plugin( + schema, + [], + { + schema: 'myzod', + withInterfaceType: true, + }, + {}, + ); + const wantContains = [ + 'export function BookSchema(): myzod.Type {', + 'title: myzod.string().optional().nullable()', + ]; + const wantNotContains = ['__typename: myzod.literal(\'Book\')']; + for (const wantContain of wantContains) + expect(result.content).toContain(wantContain); + + for (const wantNotContain of wantNotContains) + expect(result.content).not.toContain(wantNotContain); + }); + + it('generate interface type contains interface type', async () => { + const schema = buildSchema(/* GraphQL */ ` + interface Book { + author: Author + title: String + } + + interface Author { + books: [Book] + name: String + } + `); + const result = await plugin( + schema, + [], + { + schema: 'myzod', + withInterfaceType: true, + }, + {}, + ); + const wantContains = [ + 'export function AuthorSchema(): myzod.Type {', + 'books: myzod.array(BookSchema().nullable()).optional().nullable(),', + 'name: myzod.string().optional().nullable()', + + 'export function BookSchema(): myzod.Type {', + 'author: AuthorSchema().optional().nullable(),', + 'title: myzod.string().optional().nullable()', + ]; + for (const wantContain of wantContains) + expect(result.content).toContain(wantContain); + }); + it('generate object type contains interface type', async () => { + const schema = buildSchema(/* GraphQL */ ` + interface Book { + title: String! + author: Author! + } + + type Textbook implements Book { + title: String! + author: Author! + courses: [String!]! + } + + type ColoringBook implements Book { + title: String! + author: Author! + colors: [String!]! + } + + type Author { + books: [Book!] + name: String + } + `); + const result = await plugin( + schema, + [], + { + schema: 'myzod', + withInterfaceType: true, + withObjectType: true, + }, + {}, + ); + const wantContains = [ + [ + 'export function BookSchema(): myzod.Type {', + 'return myzod.object({', + 'title: myzod.string(),', + 'author: AuthorSchema()', + '})', + '}', + ], + + [ + 'export function TextbookSchema(): myzod.Type {', + 'return myzod.object({', + '__typename: myzod.literal(\'Textbook\').optional(),', + 'title: myzod.string(),', + 'author: AuthorSchema(),', + 'courses: myzod.array(myzod.string())', + '})', + '}', + ], + + [ + 'export function ColoringBookSchema(): myzod.Type {', + 'return myzod.object({', + '__typename: myzod.literal(\'ColoringBook\').optional(),', + 'title: myzod.string(),', + 'author: AuthorSchema(),', + 'colors: myzod.array(myzod.string())', + '})', + '}', + ], + + [ + 'export function AuthorSchema(): myzod.Type {', + 'return myzod.object({', + '__typename: myzod.literal(\'Author\').optional()', + 'books: myzod.array(BookSchema()).optional().nullable()', + 'name: myzod.string().optional().nullable()', + '})', + '}', + ], + ]; + for (const wantContain of wantContains) { + for (const wantContainLine of wantContain) + expect(result.content).toContain(wantContainLine); + } + }); + }); + describe('with withObjectType', () => { it('not generate if withObjectType false', async () => { const schema = buildSchema(/* GraphQL */ ` diff --git a/tests/yup.spec.ts b/tests/yup.spec.ts index 5f2caf04..466492b3 100644 --- a/tests/yup.spec.ts +++ b/tests/yup.spec.ts @@ -392,6 +392,182 @@ describe('yup', () => { expect(result.prepend).toContain('import { SayI } from \'./types\''); expect(result.content).toContain('export function SayISchema(): yup.ObjectSchema {'); }); + + describe('with interfaceType', () => { + it('not generate if withInterfaceType false', async () => { + const schema = buildSchema(/* GraphQL */ ` + interface User { + id: ID! + name: String + } + `); + const result = await plugin( + schema, + [], + { + schema: 'yup', + }, + {}, + ); + expect(result.content).not.toContain('export function UserSchema(): yup.ObjectSchema {'); + }); + + it('generate if withInterfaceType true', async () => { + const schema = buildSchema(/* GraphQL */ ` + interface Book { + title: String + } + `); + const result = await plugin( + schema, + [], + { + schema: 'yup', + withInterfaceType: true, + }, + {}, + ); + const wantContains = [ + 'export function BookSchema(): yup.ObjectSchema {', + 'title: yup.string().defined().nullable().optional()', + ]; + const wantNotContains = ['__typename: yup.string<\'Book\'>().optional()']; + for (const wantContain of wantContains) + expect(result.content).toContain(wantContain); + + for (const wantNotContain of wantNotContains) + expect(result.content).not.toContain(wantNotContain); + }); + + it('generate interface type contains interface type', async () => { + const schema = buildSchema(/* GraphQL */ ` + interface Book { + author: Author + title: String + } + + interface Book2 { + author: Author! + title: String! + } + + interface Author { + books: [Book] + name: String + } + `); + const result = await plugin( + schema, + [], + { + schema: 'yup', + withInterfaceType: true, + }, + {}, + ); + const wantContains = [ + 'export function AuthorSchema(): yup.ObjectSchema {', + 'books: yup.array(BookSchema().nullable()).defined().nullable().optional(),', + 'name: yup.string().defined().nullable().optional()', + + 'export function BookSchema(): yup.ObjectSchema {', + 'author: AuthorSchema().nullable().optional(),', + 'title: yup.string().defined().nullable().optional()', + + 'export function Book2Schema(): yup.ObjectSchema {', + 'author: AuthorSchema().nonNullable(),', + 'title: yup.string().defined().nonNullable()', + ]; + for (const wantContain of wantContains) + expect(result.content).toContain(wantContain); + + for (const wantNotContain of ['Query', 'Mutation', 'Subscription']) + expect(result.content).not.toContain(wantNotContain); + }); + it('generate object type contains interface type', async () => { + const schema = buildSchema(/* GraphQL */ ` + interface Book { + title: String! + author: Author! + } + + type Textbook implements Book { + title: String! + author: Author! + courses: [String!]! + } + + type ColoringBook implements Book { + title: String! + author: Author! + colors: [String!]! + } + + type Author { + books: [Book!] + name: String + } + `); + const result = await plugin( + schema, + [], + { + schema: 'yup', + withInterfaceType: true, + withObjectType: true, + }, + {}, + ); + const wantContains = [ + [ + 'export function BookSchema(): yup.ObjectSchema {', + 'return yup.object({', + 'title: yup.string().defined().nonNullable(),', + 'author: AuthorSchema().nonNullable()', + '})', + '}', + ], + + [ + 'export function TextbookSchema(): yup.ObjectSchema {', + 'return yup.object({', + '__typename: yup.string<\'Textbook\'>().optional(),', + 'title: yup.string().defined().nonNullable(),', + 'author: AuthorSchema().nonNullable(),', + 'courses: yup.array(yup.string().defined().nonNullable()).defined()', + '})', + '}', + ], + + [ + 'export function ColoringBookSchema(): yup.ObjectSchema {', + 'return yup.object({', + '__typename: yup.string<\'ColoringBook\'>().optional(),', + 'title: yup.string().defined().nonNullable(),', + 'author: AuthorSchema().nonNullable(),', + 'colors: yup.array(yup.string().defined().nonNullable()).defined()', + '})', + '}', + ], + + [ + 'export function AuthorSchema(): yup.ObjectSchema {', + 'return yup.object({', + '__typename: yup.string<\'Author\'>().optional(),', + 'books: yup.array(BookSchema().nonNullable()).defined().nullable().optional(),', + 'name: yup.string().defined().nullable().optional()', + '})', + '}', + ], + ]; + + for (const wantContain of wantContains) { + for (const wantContainLine of wantContain) + expect(result.content).toContain(wantContainLine); + } + }); + }); + describe('with withObjectType', () => { it('not generate if withObjectType false', async () => { const schema = buildSchema(/* GraphQL */ ` diff --git a/tests/zod.spec.ts b/tests/zod.spec.ts index 2dfe6473..8527a5a5 100644 --- a/tests/zod.spec.ts +++ b/tests/zod.spec.ts @@ -588,6 +588,170 @@ describe('zod', () => { }); }); + describe('with withInterfaceType', () => { + it('not generate if withObjectType false', async () => { + const schema = buildSchema(/* GraphQL */ ` + interface User { + id: ID! + name: String + } + `); + const result = await plugin( + schema, + [], + { + schema: 'zod', + }, + {}, + ); + expect(result.content).not.toContain('export function UserSchema(): z.ZodObject>'); + }); + + it('generate if withInterfaceType true', async () => { + const schema = buildSchema(/* GraphQL */ ` + interface Book { + title: String + } + `); + const result = await plugin( + schema, + [], + { + schema: 'zod', + withInterfaceType: true, + }, + {}, + ); + const wantContains = [ + 'export function BookSchema(): z.ZodObject> {', + 'title: z.string().nullish()', + ]; + const wantNotContains = ['__typename: z.literal(\'Book\')']; + for (const wantContain of wantContains) + expect(result.content).toContain(wantContain); + + for (const wantNotContain of wantNotContains) + expect(result.content).not.toContain(wantNotContain); + }); + + it('generate interface type contains interface type', async () => { + const schema = buildSchema(/* GraphQL */ ` + interface Book { + author: Author + title: String + } + + interface Author { + books: [Book] + name: String + } + `); + const result = await plugin( + schema, + [], + { + schema: 'zod', + withInterfaceType: true, + }, + {}, + ); + const wantContains = [ + 'export function AuthorSchema(): z.ZodObject> {', + 'books: z.array(BookSchema().nullable()).nullish(),', + 'name: z.string().nullish()', + + 'export function BookSchema(): z.ZodObject> {', + 'author: AuthorSchema().nullish(),', + 'title: z.string().nullish()', + ]; + for (const wantContain of wantContains) + expect(result.content).toContain(wantContain); + }); + + it('generate object type contains interface type', async () => { + const schema = buildSchema(/* GraphQL */ ` + interface Book { + title: String! + author: Author! + } + + type Textbook implements Book { + title: String! + author: Author! + courses: [String!]! + } + + type ColoringBook implements Book { + title: String! + author: Author! + colors: [String!]! + } + + type Author { + books: [Book!] + name: String + } + `); + const result = await plugin( + schema, + [], + { + schema: 'zod', + withInterfaceType: true, + withObjectType: true, + }, + {}, + ); + const wantContains = [ + [ + 'export function BookSchema(): z.ZodObject> {', + 'return z.object({', + 'title: z.string(),', + 'author: AuthorSchema()', + '})', + '}', + ], + + [ + 'export function TextbookSchema(): z.ZodObject> {', + 'return z.object({', + '__typename: z.literal(\'Textbook\').optional(),', + 'title: z.string(),', + 'author: AuthorSchema(),', + 'courses: z.array(z.string())', + '})', + '}', + ], + + [ + 'export function ColoringBookSchema(): z.ZodObject> {', + 'return z.object({', + '__typename: z.literal(\'ColoringBook\').optional(),', + 'title: z.string(),', + 'author: AuthorSchema(),', + 'colors: z.array(z.string())', + '})', + '}', + ], + + [ + 'export function AuthorSchema(): z.ZodObject> {', + 'return z.object({', + '__typename: z.literal(\'Author\').optional()', + 'books: z.array(BookSchema()).nullish()', + 'name: z.string().nullish()', + '})', + '}', + ], + ]; + + for (const wantContain of wantContains) { + for (const wantContainLine of wantContain) + expect(result.content).toContain(wantContainLine); + } + }); + }); + describe('with withObjectType', () => { it('not generate if withObjectType false', async () => { const schema = buildSchema(/* GraphQL */ `