From 4cfa2d46d958628cdb082ac9489b80c6c389b64d Mon Sep 17 00:00:00 2001 From: Kei Kamikawa Date: Sun, 13 Aug 2023 23:31:58 +0900 Subject: [PATCH 1/4] use class --- src/index.ts | 12 ++-- src/myzod/index.ts | 104 +++++++++++++++------------- src/schema_visitor.ts | 35 ++++++++++ src/types.ts | 5 +- src/yup/index.ts | 154 +++++++++++++++++++----------------------- src/zod/index.ts | 104 +++++++++++++++------------- 6 files changed, 225 insertions(+), 189 deletions(-) create mode 100644 src/schema_visitor.ts diff --git a/src/index.ts b/src/index.ts index 0f05b117..c4f6ff24 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,7 +15,7 @@ export const plugin: PluginFunction { const { schema: _schema, ast } = _transformSchemaAST(schema, config); - const { buildImports, initialEmit, ...visitor } = schemaVisitor(_schema, config); + const visitor = schemaVisitor(_schema, config); const result = visit(ast, visitor); @@ -24,18 +24,18 @@ export const plugin: PluginFunction typeof def === 'string'); return { - prepend: buildImports(), - content: [initialEmit(), ...generated].join('\n'), + prepend: visitor.buildImports(), + content: [visitor.initialEmit(), ...generated].join('\n'), }; }; const schemaVisitor = (schema: GraphQLSchema, config: ValidationSchemaPluginConfig): SchemaVisitor => { if (config?.schema === 'zod') { - return ZodSchemaVisitor(schema, config); + return new ZodSchemaVisitor(schema, config); } else if (config?.schema === 'myzod') { - return MyZodSchemaVisitor(schema, config); + return new MyZodSchemaVisitor(schema, config); } - return YupSchemaVisitor(schema, config); + return new YupSchemaVisitor(schema, config); }; const _transformSchemaAST = (schema: GraphQLSchema, config: ValidationSchemaPluginConfig) => { diff --git a/src/myzod/index.ts b/src/myzod/index.ts index c4f49607..ca41175f 100644 --- a/src/myzod/index.ts +++ b/src/myzod/index.ts @@ -10,46 +10,45 @@ import { TypeNode, UnionTypeDefinitionNode, } from 'graphql'; +import { BaseSchemaVisitor } from 'src/schema_visitor'; import { ValidationSchemaPluginConfig } from '../config'; import { buildApi, formatDirectiveConfig } from '../directive'; -import { SchemaVisitor } from '../types'; import { Visitor } from '../visitor'; import { isInput, isListType, isNamedType, isNonNullType, ObjectTypeDefinitionBuilder } from './../graphql'; -const importZod = `import * as myzod from 'myzod'`; const anySchema = `definedNonNullAnySchema`; -export const MyZodSchemaVisitor = (schema: GraphQLSchema, config: ValidationSchemaPluginConfig): SchemaVisitor => { - const importTypes: string[] = []; - const enumDeclarations: string[] = []; +export class MyZodSchemaVisitor extends BaseSchemaVisitor { + constructor(schema: GraphQLSchema, config: ValidationSchemaPluginConfig) { + super(schema, config); + } - return { - buildImports: (): string[] => { - if (config.importFrom && importTypes.length > 0) { - return [ - importZod, - `import ${config.useTypeImports ? 'type ' : ''}{ ${importTypes.join(', ')} } from '${config.importFrom}'`, - ]; - } - return [importZod]; - }, - initialEmit: (): string => + importValidationSchema(): string { + return `import * as myzod from 'myzod'`; + } + + initialEmit(): string { + return ( '\n' + [ new DeclarationBlock({}).export().asKind('const').withName(`${anySchema}`).withContent(`myzod.object({})`) .string, - ...enumDeclarations, - ].join('\n'), - InputObjectTypeDefinition: { + ...this.enumDeclarations, + ].join('\n') + ); + } + + get InputObjectTypeDefinition() { + return { leave: (node: InputObjectTypeDefinitionNode) => { - const visitor = new Visitor('input', schema, config); + const visitor = this.createVisitor('input'); const name = visitor.convertName(node.name.value); - importTypes.push(name); + this.importTypes.push(name); - const shape = node.fields?.map(field => generateFieldMyZodSchema(config, visitor, field, 2)).join(',\n'); + const shape = node.fields?.map(field => generateFieldMyZodSchema(this.config, visitor, field, 2)).join(',\n'); - switch (config.validationSchemaExportType) { + switch (this.config.validationSchemaExportType) { case 'const': return new DeclarationBlock({}) .export() @@ -66,19 +65,22 @@ export const MyZodSchemaVisitor = (schema: GraphQLSchema, config: ValidationSche .withBlock([indent(`return myzod.object({`), shape, indent('})')].join('\n')).string; } }, - }, - ObjectTypeDefinition: { - leave: ObjectTypeDefinitionBuilder(config.withObjectType, (node: ObjectTypeDefinitionNode) => { - const visitor = new Visitor('output', schema, config); + }; + } + + get ObjectTypeDefinition() { + return { + leave: ObjectTypeDefinitionBuilder(this.config.withObjectType, (node: ObjectTypeDefinitionNode) => { + const visitor = this.createVisitor('output'); const name = visitor.convertName(node.name.value); - importTypes.push(name); + this.importTypes.push(name); // Building schema for field arguments. const argumentBlocks = visitor.buildArgumentsSchemaBlock(node, (typeName, field) => { - importTypes.push(typeName); + this.importTypes.push(typeName); const args = field.arguments ?? []; - const shape = args.map(field => generateFieldMyZodSchema(config, visitor, field, 2)).join(',\n'); - switch (config.validationSchemaExportType) { + const shape = args.map(field => generateFieldMyZodSchema(this.config, visitor, field, 2)).join(',\n'); + switch (this.config.validationSchemaExportType) { case 'const': return new DeclarationBlock({}) .export() @@ -98,9 +100,9 @@ export const MyZodSchemaVisitor = (schema: GraphQLSchema, config: ValidationSche const appendArguments = argumentBlocks ? '\n' + argumentBlocks : ''; // Building schema for fields. - const shape = node.fields?.map(field => generateFieldMyZodSchema(config, visitor, field, 2)).join(',\n'); + const shape = node.fields?.map(field => generateFieldMyZodSchema(this.config, visitor, field, 2)).join(',\n'); - switch (config.validationSchemaExportType) { + switch (this.config.validationSchemaExportType) { case 'const': return ( new DeclarationBlock({}) @@ -135,16 +137,19 @@ export const MyZodSchemaVisitor = (schema: GraphQLSchema, config: ValidationSche ); } }), - }, - EnumTypeDefinition: { + }; + } + + get EnumTypeDefinition() { + return { leave: (node: EnumTypeDefinitionNode) => { - const visitor = new Visitor('both', schema, config); + const visitor = this.createVisitor('both'); const enumname = visitor.convertName(node.name.value); - importTypes.push(enumname); + this.importTypes.push(enumname); // z.enum are basically myzod.literals // hoist enum declarations - enumDeclarations.push( - config.enumsAsTypes + this.enumDeclarations.push( + this.config.enumsAsTypes ? new DeclarationBlock({}) .export() .asKind('type') @@ -159,12 +164,15 @@ export const MyZodSchemaVisitor = (schema: GraphQLSchema, config: ValidationSche .withContent(`myzod.enum(${enumname})`).string ); }, - }, - UnionTypeDefinition: { + }; + } + + get UnionTypeDefinition() { + return { leave: (node: UnionTypeDefinitionNode) => { - if (!node.types || !config.withObjectType) return; + if (!node.types || !this.config.withObjectType) return; - const visitor = new Visitor('output', schema, config); + const visitor = this.createVisitor('output'); const unionName = visitor.convertName(node.name.value); const unionElements = node.types @@ -174,7 +182,7 @@ export const MyZodSchemaVisitor = (schema: GraphQLSchema, config: ValidationSche if (typ?.astNode?.kind === 'EnumTypeDefinition') { return `${element}Schema`; } - switch (config.validationSchemaExportType) { + switch (this.config.validationSchemaExportType) { case 'const': return `${element}Schema`; case 'function': @@ -187,7 +195,7 @@ export const MyZodSchemaVisitor = (schema: GraphQLSchema, config: ValidationSche const union = unionElementsCount > 1 ? `myzod.union([${unionElements}])` : unionElements; - switch (config.validationSchemaExportType) { + switch (this.config.validationSchemaExportType) { case 'const': return new DeclarationBlock({}).export().asKind('const').withName(`${unionName}Schema`).withContent(union) .string; @@ -200,9 +208,9 @@ export const MyZodSchemaVisitor = (schema: GraphQLSchema, config: ValidationSche .withBlock(indent(`return ${union}`)).string; } }, - }, - }; -}; + }; + } +} const generateFieldMyZodSchema = ( config: ValidationSchemaPluginConfig, diff --git a/src/schema_visitor.ts b/src/schema_visitor.ts new file mode 100644 index 00000000..cd828971 --- /dev/null +++ b/src/schema_visitor.ts @@ -0,0 +1,35 @@ +import { GraphQLSchema } from 'graphql'; + +import { ValidationSchemaPluginConfig } from './config'; +import { SchemaVisitor } from './types'; +import { Visitor } from './visitor'; + +export abstract class BaseSchemaVisitor implements SchemaVisitor { + protected importTypes: string[] = []; + protected enumDeclarations: string[] = []; + + constructor( + protected schema: GraphQLSchema, + protected config: ValidationSchemaPluginConfig + ) {} + + abstract importValidationSchema(): string; + + buildImports(): string[] { + if (this.config.importFrom && this.importTypes.length > 0) { + return [ + this.importValidationSchema(), + `import ${this.config.useTypeImports ? 'type ' : ''}{ ${this.importTypes.join(', ')} } from '${ + this.config.importFrom + }'`, + ]; + } + return [this.importValidationSchema()]; + } + + abstract initialEmit(): string; + + createVisitor(scalarDirection: 'input' | 'output' | 'both'): Visitor { + return new Visitor(scalarDirection, this.schema, this.config); + } +} diff --git a/src/types.ts b/src/types.ts index b252e426..4fe8df9f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -5,7 +5,8 @@ export type NewVisitor = Partial<{ leave?: ASTVisitFn; }; }>; -export type SchemaVisitor = { + +export interface SchemaVisitor extends NewVisitor { buildImports: () => string[]; initialEmit: () => string; -} & NewVisitor; +} diff --git a/src/yup/index.ts b/src/yup/index.ts index 96ac7dc4..79b6edaf 100644 --- a/src/yup/index.ts +++ b/src/yup/index.ts @@ -10,61 +10,56 @@ import { TypeNode, UnionTypeDefinitionNode, } from 'graphql'; +import { BaseSchemaVisitor } from 'src/schema_visitor'; import { ValidationSchemaPluginConfig } from '../config'; import { buildApi, formatDirectiveConfig } from '../directive'; -import { SchemaVisitor } from '../types'; import { Visitor } from '../visitor'; import { isInput, isListType, isNamedType, isNonNullType, ObjectTypeDefinitionBuilder } from './../graphql'; -const importYup = `import * as yup from 'yup'`; +export class YupSchemaVisitor extends BaseSchemaVisitor { + constructor(schema: GraphQLSchema, config: ValidationSchemaPluginConfig) { + super(schema, config); + } -export const YupSchemaVisitor = (schema: GraphQLSchema, config: ValidationSchemaPluginConfig): SchemaVisitor => { - const importTypes: string[] = []; - const enumDeclarations: string[] = []; + importValidationSchema(): string { + return `import * as yup from 'yup'`; + } - return { - buildImports: (): string[] => { - if (config.importFrom && importTypes.length > 0) { - return [ - importYup, - `import ${config.useTypeImports ? 'type ' : ''}{ ${importTypes.join(', ')} } from '${config.importFrom}'`, - ]; - } - return [importYup]; - }, - initialEmit: (): string => { - if (!config.withObjectType) return '\n' + enumDeclarations.join('\n'); - return ( - '\n' + - enumDeclarations.join('\n') + - '\n' + - new DeclarationBlock({}) - .asKind('function') - .withName('union(...schemas: ReadonlyArray>): yup.MixedSchema') - .withBlock( - [ - indent('return yup.mixed().test({'), - indent('test: (value) => schemas.some((schema) => schema.isValidSync(value))', 2), - indent('}).defined()'), - ].join('\n') - ).string - ); - }, - InputObjectTypeDefinition: { + initialEmit(): string { + if (!this.config.withObjectType) return '\n' + this.enumDeclarations.join('\n'); + return ( + '\n' + + this.enumDeclarations.join('\n') + + '\n' + + new DeclarationBlock({}) + .asKind('function') + .withName('union(...schemas: ReadonlyArray>): yup.MixedSchema') + .withBlock( + [ + indent('return yup.mixed().test({'), + indent('test: (value) => schemas.some((schema) => schema.isValidSync(value))', 2), + indent('}).defined()'), + ].join('\n') + ).string + ); + } + + get InputObjectTypeDefinition() { + return { leave: (node: InputObjectTypeDefinitionNode) => { - const visitor = new Visitor('input', schema, config); + const visitor = this.createVisitor('input'); const name = visitor.convertName(node.name.value); - importTypes.push(name); + this.importTypes.push(name); const shape = node.fields ?.map(field => { - const fieldSchema = generateFieldYupSchema(config, visitor, field, 2); + const fieldSchema = generateFieldYupSchema(this.config, visitor, field, 2); return isNonNullType(field.type) ? fieldSchema : `${fieldSchema}.optional()`; }) .join(',\n'); - switch (config.validationSchemaExportType) { + switch (this.config.validationSchemaExportType) { case 'const': return new DeclarationBlock({}) .export() @@ -81,19 +76,22 @@ export const YupSchemaVisitor = (schema: GraphQLSchema, config: ValidationSchema .withBlock([indent(`return yup.object({`), shape, indent('})')].join('\n')).string; } }, - }, - ObjectTypeDefinition: { - leave: ObjectTypeDefinitionBuilder(config.withObjectType, (node: ObjectTypeDefinitionNode) => { - const visitor = new Visitor('output', schema, config); + }; + } + + get ObjectTypeDefinition() { + return { + leave: ObjectTypeDefinitionBuilder(this.config.withObjectType, (node: ObjectTypeDefinitionNode) => { + const visitor = this.createVisitor('output'); const name = visitor.convertName(node.name.value); - importTypes.push(name); + this.importTypes.push(name); // Building schema for field arguments. const argumentBlocks = visitor.buildArgumentsSchemaBlock(node, (typeName, field) => { - importTypes.push(typeName); + this.importTypes.push(typeName); const args = field.arguments ?? []; - const shape = args.map(field => generateFieldYupSchema(config, visitor, field, 2)).join(',\n'); - switch (config.validationSchemaExportType) { + const shape = args.map(field => generateFieldYupSchema(this.config, visitor, field, 2)).join(',\n'); + switch (this.config.validationSchemaExportType) { case 'const': return new DeclarationBlock({}) .export() @@ -113,15 +111,14 @@ export const YupSchemaVisitor = (schema: GraphQLSchema, config: ValidationSchema const appendArguments = argumentBlocks ? '\n' + argumentBlocks : ''; // Building schema for fields. - const shape = node.fields ?.map(field => { - const fieldSchema = generateFieldYupSchema(config, visitor, field, 2); + const fieldSchema = generateFieldYupSchema(this.config, visitor, field, 2); return isNonNullType(field.type) ? fieldSchema : `${fieldSchema}.optional()`; }) .join(',\n'); - switch (config.validationSchemaExportType) { + switch (this.config.validationSchemaExportType) { case 'const': return ( new DeclarationBlock({}) @@ -156,18 +153,21 @@ export const YupSchemaVisitor = (schema: GraphQLSchema, config: ValidationSchema ); } }), - }, - EnumTypeDefinition: { + }; + } + + get EnumTypeDefinition() { + return { leave: (node: EnumTypeDefinitionNode) => { - const visitor = new Visitor('both', schema, config); + const visitor = this.createVisitor('both'); const enumname = visitor.convertName(node.name.value); - importTypes.push(enumname); + this.importTypes.push(enumname); // hoise enum declarations - if (config.enumsAsTypes) { + if (this.config.enumsAsTypes) { const enums = node.values?.map(enumOption => `'${enumOption.name.value}'`); - enumDeclarations.push( + this.enumDeclarations.push( new DeclarationBlock({}) .export() .asKind('const') @@ -184,7 +184,7 @@ export const YupSchemaVisitor = (schema: GraphQLSchema, config: ValidationSchema })}` ) .join(', '); - enumDeclarations.push( + this.enumDeclarations.push( new DeclarationBlock({}) .export() .asKind('const') @@ -193,14 +193,17 @@ export const YupSchemaVisitor = (schema: GraphQLSchema, config: ValidationSchema ); } }, - }, - UnionTypeDefinition: { + }; + } + + get UnionTypeDefinition() { + return { leave: (node: UnionTypeDefinitionNode) => { - if (!node.types || !config.withObjectType) return; - const visitor = new Visitor('output', schema, config); + if (!node.types || !this.config.withObjectType) return; + const visitor = this.createVisitor('output'); const unionName = visitor.convertName(node.name.value); - importTypes.push(unionName); + this.importTypes.push(unionName); const unionElements = node.types ?.map(t => { @@ -209,7 +212,7 @@ export const YupSchemaVisitor = (schema: GraphQLSchema, config: ValidationSchema if (typ?.astNode?.kind === 'EnumTypeDefinition') { return `${element}Schema`; } - switch (config.validationSchemaExportType) { + switch (this.config.validationSchemaExportType) { case 'const': return `${element}Schema`; case 'function': @@ -219,7 +222,7 @@ export const YupSchemaVisitor = (schema: GraphQLSchema, config: ValidationSchema }) .join(', '); - switch (config.validationSchemaExportType) { + switch (this.config.validationSchemaExportType) { case 'const': return new DeclarationBlock({}) .export() @@ -235,28 +238,9 @@ export const YupSchemaVisitor = (schema: GraphQLSchema, config: ValidationSchema .withBlock(indent(`return union<${unionName}>(${unionElements})`)).string; } }, - }, - // ScalarTypeDefinition: (node) => { - // const decl = new DeclarationBlock({}) - // .export() - // .asKind("const") - // .withName(`${node.name.value}Schema`); - - // if (tsVisitor.scalars[node.name.value]) { - // const tsType = tsVisitor.scalars[node.name.value]; - // switch (tsType) { - // case "string": - // return decl.withContent(`yup.string()`).string; - // case "number": - // return decl.withContent(`yup.number()`).string; - // case "boolean": - // return decl.withContent(`yup.boolean()`).string; - // } - // } - // return decl.withContent(`yup.mixed()`).string; - // }, - }; -}; + }; + } +} const generateFieldYupSchema = ( config: ValidationSchemaPluginConfig, diff --git a/src/zod/index.ts b/src/zod/index.ts index bc457544..0e7b4a6b 100644 --- a/src/zod/index.ts +++ b/src/zod/index.ts @@ -10,31 +10,26 @@ import { TypeNode, UnionTypeDefinitionNode, } from 'graphql'; +import { BaseSchemaVisitor } from 'src/schema_visitor'; import { ValidationSchemaPluginConfig } from '../config'; import { buildApi, formatDirectiveConfig } from '../directive'; -import { SchemaVisitor } from '../types'; import { Visitor } from '../visitor'; import { isInput, isListType, isNamedType, isNonNullType, ObjectTypeDefinitionBuilder } from './../graphql'; -const importZod = `import { z } from 'zod'`; const anySchema = `definedNonNullAnySchema`; -export const ZodSchemaVisitor = (schema: GraphQLSchema, config: ValidationSchemaPluginConfig): SchemaVisitor => { - const importTypes: string[] = []; - const enumDeclarations: string[] = []; +export class ZodSchemaVisitor extends BaseSchemaVisitor { + constructor(schema: GraphQLSchema, config: ValidationSchemaPluginConfig) { + super(schema, config); + } - return { - buildImports: (): string[] => { - if (config.importFrom && importTypes.length > 0) { - return [ - importZod, - `import ${config.useTypeImports ? 'type ' : ''}{ ${importTypes.join(', ')} } from '${config.importFrom}'`, - ]; - } - return [importZod]; - }, - initialEmit: (): string => + importValidationSchema(): string { + return `import { z } from 'zod'`; + } + + initialEmit(): string { + return ( '\n' + [ new DeclarationBlock({}) @@ -55,17 +50,21 @@ export const ZodSchemaVisitor = (schema: GraphQLSchema, config: ValidationSchema .asKind('const') .withName(`${anySchema}`) .withContent(`z.any().refine((v) => isDefinedNonNullAny(v))`).string, - ...enumDeclarations, - ].join('\n'), - InputObjectTypeDefinition: { + ...this.enumDeclarations, + ].join('\n') + ); + } + + get InputObjectTypeDefinition() { + return { leave: (node: InputObjectTypeDefinitionNode) => { - const visitor = new Visitor('input', schema, config); + const visitor = this.createVisitor('input'); const name = visitor.convertName(node.name.value); - importTypes.push(name); + this.importTypes.push(name); - const shape = node.fields?.map(field => generateFieldZodSchema(config, visitor, field, 2)).join(',\n'); + const shape = node.fields?.map(field => generateFieldZodSchema(this.config, visitor, field, 2)).join(',\n'); - switch (config.validationSchemaExportType) { + switch (this.config.validationSchemaExportType) { case 'const': return new DeclarationBlock({}) .export() @@ -82,19 +81,22 @@ export const ZodSchemaVisitor = (schema: GraphQLSchema, config: ValidationSchema .withBlock([indent(`return z.object({`), shape, indent('})')].join('\n')).string; } }, - }, - ObjectTypeDefinition: { - leave: ObjectTypeDefinitionBuilder(config.withObjectType, (node: ObjectTypeDefinitionNode) => { - const visitor = new Visitor('output', schema, config); + }; + } + + get ObjectTypeDefinition() { + return { + leave: ObjectTypeDefinitionBuilder(this.config.withObjectType, (node: ObjectTypeDefinitionNode) => { + const visitor = this.createVisitor('output'); const name = visitor.convertName(node.name.value); - importTypes.push(name); + this.importTypes.push(name); // Building schema for field arguments. const argumentBlocks = visitor.buildArgumentsSchemaBlock(node, (typeName, field) => { - importTypes.push(typeName); + this.importTypes.push(typeName); const args = field.arguments ?? []; - const shape = args.map(field => generateFieldZodSchema(config, visitor, field, 2)).join(',\n'); - switch (config.validationSchemaExportType) { + const shape = args.map(field => generateFieldZodSchema(this.config, visitor, field, 2)).join(',\n'); + switch (this.config.validationSchemaExportType) { case 'const': return new DeclarationBlock({}) .export() @@ -114,9 +116,9 @@ export const ZodSchemaVisitor = (schema: GraphQLSchema, config: ValidationSchema const appendArguments = argumentBlocks ? '\n' + argumentBlocks : ''; // Building schema for fields. - const shape = node.fields?.map(field => generateFieldZodSchema(config, visitor, field, 2)).join(',\n'); + const shape = node.fields?.map(field => generateFieldZodSchema(this.config, visitor, field, 2)).join(',\n'); - switch (config.validationSchemaExportType) { + switch (this.config.validationSchemaExportType) { case 'const': return ( new DeclarationBlock({}) @@ -151,16 +153,19 @@ export const ZodSchemaVisitor = (schema: GraphQLSchema, config: ValidationSchema ); } }), - }, - EnumTypeDefinition: { + }; + } + + get EnumTypeDefinition() { + return { leave: (node: EnumTypeDefinitionNode) => { - const visitor = new Visitor('both', schema, config); + const visitor = this.createVisitor('both'); const enumname = visitor.convertName(node.name.value); - importTypes.push(enumname); + this.importTypes.push(enumname); // hoist enum declarations - enumDeclarations.push( - config.enumsAsTypes + this.enumDeclarations.push( + this.config.enumsAsTypes ? new DeclarationBlock({}) .export() .asKind('const') @@ -174,11 +179,14 @@ export const ZodSchemaVisitor = (schema: GraphQLSchema, config: ValidationSchema .withContent(`z.nativeEnum(${enumname})`).string ); }, - }, - UnionTypeDefinition: { + }; + } + + get UnionTypeDefinition() { + return { leave: (node: UnionTypeDefinitionNode) => { - if (!node.types || !config.withObjectType) return; - const visitor = new Visitor('output', schema, config); + if (!node.types || !this.config.withObjectType) return; + const visitor = this.createVisitor('output'); const unionName = visitor.convertName(node.name.value); const unionElements = node.types .map(t => { @@ -187,7 +195,7 @@ export const ZodSchemaVisitor = (schema: GraphQLSchema, config: ValidationSchema if (typ?.astNode?.kind === 'EnumTypeDefinition') { return `${element}Schema`; } - switch (config.validationSchemaExportType) { + switch (this.config.validationSchemaExportType) { case 'const': return `${element}Schema`; case 'function': @@ -200,7 +208,7 @@ export const ZodSchemaVisitor = (schema: GraphQLSchema, config: ValidationSchema const union = unionElementsCount > 1 ? `z.union([${unionElements}])` : unionElements; - switch (config.validationSchemaExportType) { + switch (this.config.validationSchemaExportType) { case 'const': return new DeclarationBlock({}).export().asKind('const').withName(`${unionName}Schema`).withContent(union) .string; @@ -213,9 +221,9 @@ export const ZodSchemaVisitor = (schema: GraphQLSchema, config: ValidationSchema .withBlock(indent(`return ${union}`)).string; } }, - }, - }; -}; + }; + } +} const generateFieldZodSchema = ( config: ValidationSchemaPluginConfig, From c939e13cc692d742020c24db5df34e66974b7fce Mon Sep 17 00:00:00 2001 From: Kei Kamikawa Date: Mon, 14 Aug 2023 00:31:13 +0900 Subject: [PATCH 2/4] fixed import path --- src/myzod/index.ts | 2 +- src/yup/index.ts | 2 +- src/zod/index.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/myzod/index.ts b/src/myzod/index.ts index ca41175f..1befec82 100644 --- a/src/myzod/index.ts +++ b/src/myzod/index.ts @@ -10,10 +10,10 @@ import { TypeNode, UnionTypeDefinitionNode, } from 'graphql'; -import { BaseSchemaVisitor } from 'src/schema_visitor'; import { ValidationSchemaPluginConfig } from '../config'; import { buildApi, formatDirectiveConfig } from '../directive'; +import { BaseSchemaVisitor } from '../schema_visitor'; import { Visitor } from '../visitor'; import { isInput, isListType, isNamedType, isNonNullType, ObjectTypeDefinitionBuilder } from './../graphql'; diff --git a/src/yup/index.ts b/src/yup/index.ts index 79b6edaf..d3db9bfd 100644 --- a/src/yup/index.ts +++ b/src/yup/index.ts @@ -10,10 +10,10 @@ import { TypeNode, UnionTypeDefinitionNode, } from 'graphql'; -import { BaseSchemaVisitor } from 'src/schema_visitor'; import { ValidationSchemaPluginConfig } from '../config'; import { buildApi, formatDirectiveConfig } from '../directive'; +import { BaseSchemaVisitor } from '../schema_visitor'; import { Visitor } from '../visitor'; import { isInput, isListType, isNamedType, isNonNullType, ObjectTypeDefinitionBuilder } from './../graphql'; diff --git a/src/zod/index.ts b/src/zod/index.ts index 0e7b4a6b..dd18ce9f 100644 --- a/src/zod/index.ts +++ b/src/zod/index.ts @@ -10,10 +10,10 @@ import { TypeNode, UnionTypeDefinitionNode, } from 'graphql'; -import { BaseSchemaVisitor } from 'src/schema_visitor'; import { ValidationSchemaPluginConfig } from '../config'; import { buildApi, formatDirectiveConfig } from '../directive'; +import { BaseSchemaVisitor } from '../schema_visitor'; import { Visitor } from '../visitor'; import { isInput, isListType, isNamedType, isNonNullType, ObjectTypeDefinitionBuilder } from './../graphql'; From b57abca218eadd615e2fdb703cfc56968e7c862f Mon Sep 17 00:00:00 2001 From: Kei Kamikawa Date: Mon, 14 Aug 2023 00:55:55 +0900 Subject: [PATCH 3/4] added buildInputFields method for commonality --- example/yup/schemas.ts | 4 +-- src/myzod/index.ts | 64 +++++++++++++++--------------------- src/yup/index.ts | 74 ++++++++++++++++++------------------------ src/zod/index.ts | 64 +++++++++++++++--------------------- tests/yup.spec.ts | 6 ++-- 5 files changed, 91 insertions(+), 121 deletions(-) diff --git a/example/yup/schemas.ts b/example/yup/schemas.ts index 3e86db71..6a481b0d 100644 --- a/example/yup/schemas.ts +++ b/example/yup/schemas.ts @@ -89,9 +89,9 @@ export function MyTypeSchema(): yup.ObjectSchema { export function MyTypeFooArgsSchema(): yup.ObjectSchema { return yup.object({ - a: yup.string().defined().nullable(), + a: yup.string().defined().nullable().optional(), b: yup.number().defined().nonNullable(), - c: yup.boolean().defined().nullable(), + c: yup.boolean().defined().nullable().optional(), d: yup.number().defined().nonNullable() }) } diff --git a/src/myzod/index.ts b/src/myzod/index.ts index 1befec82..8970c105 100644 --- a/src/myzod/index.ts +++ b/src/myzod/index.ts @@ -45,25 +45,7 @@ export class MyZodSchemaVisitor extends BaseSchemaVisitor { const visitor = this.createVisitor('input'); const name = visitor.convertName(node.name.value); this.importTypes.push(name); - - 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; - - 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; - } + return this.buildInputFields(node.fields ?? [], visitor, name); }, }; } @@ -78,24 +60,7 @@ export class MyZodSchemaVisitor extends BaseSchemaVisitor { // Building schema for field arguments. const argumentBlocks = visitor.buildArgumentsSchemaBlock(node, (typeName, field) => { this.importTypes.push(typeName); - const args = field.arguments ?? []; - const shape = args.map(field => generateFieldMyZodSchema(this.config, visitor, field, 2)).join(',\n'); - switch (this.config.validationSchemaExportType) { - case 'const': - return new DeclarationBlock({}) - .export() - .asKind('const') - .withName(`${typeName}Schema: myzod.Type<${typeName}>`) - .withContent([`myzod.object({`, shape, '})'].join('\n')).string; - - case 'function': - default: - return new DeclarationBlock({}) - .export() - .asKind('function') - .withName(`${typeName}Schema(): myzod.Type<${typeName}>`) - .withBlock([indent(`return myzod.object({`), shape, indent('})')].join('\n')).string; - } + return this.buildInputFields(field.arguments ?? [], visitor, typeName); }); const appendArguments = argumentBlocks ? '\n' + argumentBlocks : ''; @@ -210,6 +175,31 @@ export class MyZodSchemaVisitor extends BaseSchemaVisitor { }, }; } + + private buildInputFields( + fields: readonly (FieldDefinitionNode | InputValueDefinitionNode)[], + visitor: Visitor, + name: string + ) { + const shape = 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; + + 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; + } + } } const generateFieldMyZodSchema = ( diff --git a/src/yup/index.ts b/src/yup/index.ts index d3db9bfd..f49c6d11 100644 --- a/src/yup/index.ts +++ b/src/yup/index.ts @@ -51,30 +51,7 @@ export class YupSchemaVisitor extends BaseSchemaVisitor { const visitor = this.createVisitor('input'); const name = visitor.convertName(node.name.value); this.importTypes.push(name); - - 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; - - 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; - } + return this.buildInputFields(node.fields ?? [], visitor, name); }, }; } @@ -89,24 +66,7 @@ export class YupSchemaVisitor extends BaseSchemaVisitor { // Building schema for field arguments. const argumentBlocks = visitor.buildArgumentsSchemaBlock(node, (typeName, field) => { this.importTypes.push(typeName); - const args = field.arguments ?? []; - const shape = args.map(field => generateFieldYupSchema(this.config, visitor, field, 2)).join(',\n'); - switch (this.config.validationSchemaExportType) { - case 'const': - return new DeclarationBlock({}) - .export() - .asKind('const') - .withName(`${typeName}Schema: yup.ObjectSchema<${typeName}>`) - .withContent([`yup.object({`, shape, '})'].join('\n')).string; - - case 'function': - default: - return new DeclarationBlock({}) - .export() - .asKind('function') - .withName(`${typeName}Schema(): yup.ObjectSchema<${typeName}>`) - .withBlock([indent(`return yup.object({`), shape, indent('})')].join('\n')).string; - } + return this.buildInputFields(field.arguments ?? [], visitor, typeName); }); const appendArguments = argumentBlocks ? '\n' + argumentBlocks : ''; @@ -240,6 +200,36 @@ export class YupSchemaVisitor extends BaseSchemaVisitor { }, }; } + + private buildInputFields( + fields: readonly (FieldDefinitionNode | InputValueDefinitionNode)[], + visitor: Visitor, + name: string + ) { + const shape = 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; + + 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; + } + } } const generateFieldYupSchema = ( diff --git a/src/zod/index.ts b/src/zod/index.ts index dd18ce9f..f1cecdc1 100644 --- a/src/zod/index.ts +++ b/src/zod/index.ts @@ -61,25 +61,7 @@ export class ZodSchemaVisitor extends BaseSchemaVisitor { const visitor = this.createVisitor('input'); const name = visitor.convertName(node.name.value); this.importTypes.push(name); - - 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; - - case 'function': - default: - return new DeclarationBlock({}) - .export() - .asKind('function') - .withName(`${name}Schema(): z.ZodObject>`) - .withBlock([indent(`return z.object({`), shape, indent('})')].join('\n')).string; - } + return this.buildInputFields(node.fields ?? [], visitor, name); }, }; } @@ -94,24 +76,7 @@ export class ZodSchemaVisitor extends BaseSchemaVisitor { // Building schema for field arguments. const argumentBlocks = visitor.buildArgumentsSchemaBlock(node, (typeName, field) => { this.importTypes.push(typeName); - const args = field.arguments ?? []; - const shape = args.map(field => generateFieldZodSchema(this.config, visitor, field, 2)).join(',\n'); - switch (this.config.validationSchemaExportType) { - case 'const': - return new DeclarationBlock({}) - .export() - .asKind('const') - .withName(`${typeName}Schema: z.ZodObject>`) - .withContent([`z.object({`, shape, '})'].join('\n')).string; - - case 'function': - default: - return new DeclarationBlock({}) - .export() - .asKind('function') - .withName(`${typeName}Schema(): z.ZodObject>`) - .withBlock([indent(`return z.object({`), shape, indent('})')].join('\n')).string; - } + return this.buildInputFields(field.arguments ?? [], visitor, typeName); }); const appendArguments = argumentBlocks ? '\n' + argumentBlocks : ''; @@ -223,6 +188,31 @@ export class ZodSchemaVisitor extends BaseSchemaVisitor { }, }; } + + private buildInputFields( + fields: readonly (FieldDefinitionNode | InputValueDefinitionNode)[], + visitor: Visitor, + name: string + ) { + const shape = fields.map(field => generateFieldZodSchema(this.config, visitor, field, 2)).join(',\n'); + + switch (this.config.validationSchemaExportType) { + case 'const': + return new DeclarationBlock({}) + .export() + .asKind('const') + .withName(`${name}Schema: z.ZodObject>`) + .withContent(['z.object({', shape, '})'].join('\n')).string; + + case 'function': + default: + return new DeclarationBlock({}) + .export() + .asKind('function') + .withName(`${name}Schema(): z.ZodObject>`) + .withBlock([indent(`return z.object({`), shape, indent('})')].join('\n')).string; + } + } } const generateFieldZodSchema = ( diff --git a/tests/yup.spec.ts b/tests/yup.spec.ts index 86ae3d6f..50316ad7 100644 --- a/tests/yup.spec.ts +++ b/tests/yup.spec.ts @@ -740,11 +740,11 @@ describe('yup', () => { const wantContain = dedent` export function MyTypeFooArgsSchema(): yup.ObjectSchema { return yup.object({ - a: yup.string().defined().nullable(), + a: yup.string().defined().nullable().optional(), b: yup.number().defined().nonNullable(), - c: yup.boolean().defined().nullable(), + c: yup.boolean().defined().nullable().optional(), d: yup.number().defined().nonNullable(), - e: yup.string().defined().nullable() + e: yup.string().defined().nullable().optional() }) }`; expect(result.content).toContain(wantContain); From eb05f425debc6e3886f7d9c0893683f2f560a13a Mon Sep 17 00:00:00 2001 From: Kei Kamikawa Date: Mon, 14 Aug 2023 01:03:38 +0900 Subject: [PATCH 4/4] Refactor schema building for field arguments - Abstract the building of arguments schema block into `buildObjectTypeDefinitionArguments` method in `BaseSchemaVisitor` - Change visibility of `buildInputFields` method from private to protected - Update necessary imports in `schema_visitor.ts` --- src/myzod/index.ts | 7 ++----- src/schema_visitor.ts | 15 ++++++++++++++- src/yup/index.ts | 7 ++----- src/zod/index.ts | 7 ++----- 4 files changed, 20 insertions(+), 16 deletions(-) diff --git a/src/myzod/index.ts b/src/myzod/index.ts index 8970c105..a8e77496 100644 --- a/src/myzod/index.ts +++ b/src/myzod/index.ts @@ -58,10 +58,7 @@ export class MyZodSchemaVisitor extends BaseSchemaVisitor { this.importTypes.push(name); // Building schema for field arguments. - const argumentBlocks = visitor.buildArgumentsSchemaBlock(node, (typeName, field) => { - this.importTypes.push(typeName); - return this.buildInputFields(field.arguments ?? [], visitor, typeName); - }); + const argumentBlocks = this.buildObjectTypeDefinitionArguments(node, visitor); const appendArguments = argumentBlocks ? '\n' + argumentBlocks : ''; // Building schema for fields. @@ -176,7 +173,7 @@ export class MyZodSchemaVisitor extends BaseSchemaVisitor { }; } - private buildInputFields( + protected buildInputFields( fields: readonly (FieldDefinitionNode | InputValueDefinitionNode)[], visitor: Visitor, name: string diff --git a/src/schema_visitor.ts b/src/schema_visitor.ts index cd828971..686ca7d9 100644 --- a/src/schema_visitor.ts +++ b/src/schema_visitor.ts @@ -1,4 +1,4 @@ -import { GraphQLSchema } from 'graphql'; +import { FieldDefinitionNode, GraphQLSchema, InputValueDefinitionNode, ObjectTypeDefinitionNode } from 'graphql'; import { ValidationSchemaPluginConfig } from './config'; import { SchemaVisitor } from './types'; @@ -32,4 +32,17 @@ export abstract class BaseSchemaVisitor implements SchemaVisitor { createVisitor(scalarDirection: 'input' | 'output' | 'both'): Visitor { return new Visitor(scalarDirection, this.schema, this.config); } + + protected abstract buildInputFields( + fields: readonly (FieldDefinitionNode | InputValueDefinitionNode)[], + visitor: Visitor, + name: string + ): string; + + protected buildObjectTypeDefinitionArguments(node: ObjectTypeDefinitionNode, 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 f49c6d11..171d47f4 100644 --- a/src/yup/index.ts +++ b/src/yup/index.ts @@ -64,10 +64,7 @@ export class YupSchemaVisitor extends BaseSchemaVisitor { this.importTypes.push(name); // Building schema for field arguments. - const argumentBlocks = visitor.buildArgumentsSchemaBlock(node, (typeName, field) => { - this.importTypes.push(typeName); - return this.buildInputFields(field.arguments ?? [], visitor, typeName); - }); + const argumentBlocks = this.buildObjectTypeDefinitionArguments(node, visitor); const appendArguments = argumentBlocks ? '\n' + argumentBlocks : ''; // Building schema for fields. @@ -201,7 +198,7 @@ export class YupSchemaVisitor extends BaseSchemaVisitor { }; } - private buildInputFields( + protected buildInputFields( fields: readonly (FieldDefinitionNode | InputValueDefinitionNode)[], visitor: Visitor, name: string diff --git a/src/zod/index.ts b/src/zod/index.ts index f1cecdc1..ef83ad1d 100644 --- a/src/zod/index.ts +++ b/src/zod/index.ts @@ -74,10 +74,7 @@ export class ZodSchemaVisitor extends BaseSchemaVisitor { this.importTypes.push(name); // Building schema for field arguments. - const argumentBlocks = visitor.buildArgumentsSchemaBlock(node, (typeName, field) => { - this.importTypes.push(typeName); - return this.buildInputFields(field.arguments ?? [], visitor, typeName); - }); + const argumentBlocks = this.buildObjectTypeDefinitionArguments(node, visitor); const appendArguments = argumentBlocks ? '\n' + argumentBlocks : ''; // Building schema for fields. @@ -189,7 +186,7 @@ export class ZodSchemaVisitor extends BaseSchemaVisitor { }; } - private buildInputFields( + protected buildInputFields( fields: readonly (FieldDefinitionNode | InputValueDefinitionNode)[], visitor: Visitor, name: string