From 5d8614b8b67321a26a4b461c94171cf41dd496f1 Mon Sep 17 00:00:00 2001 From: Parables Date: Fri, 1 Apr 2022 15:14:49 +0000 Subject: [PATCH 1/4] renamed possible typo --- src/zod/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/zod/index.ts b/src/zod/index.ts index 21b90747..e9e7ee68 100644 --- a/src/zod/index.ts +++ b/src/zod/index.ts @@ -54,7 +54,7 @@ export const ZodSchemaVisitor = (schema: GraphQLSchema, config: ValidationSchema importTypes.push(name); const shape = node.fields - ?.map(field => generateInputObjectFieldYupSchema(config, tsVisitor, schema, field, 2)) + ?.map(field => generateInputObjectFieldZodSchema(config, tsVisitor, schema, field, 2)) .join(',\n'); return new DeclarationBlock({}) @@ -84,7 +84,7 @@ export const ZodSchemaVisitor = (schema: GraphQLSchema, config: ValidationSchema }; }; -const generateInputObjectFieldYupSchema = ( +const generateInputObjectFieldZodSchema = ( config: ValidationSchemaPluginConfig, tsVisitor: TsVisitor, schema: GraphQLSchema, From 6ab7a59d79d8282fe35301187527435f66814a9d Mon Sep 17 00:00:00 2001 From: Parables Date: Fri, 1 Apr 2022 15:24:21 +0000 Subject: [PATCH 2/4] before implementing myzod --- package.json | 1 + src/myzod/index.ts | 196 +++++++++++++++++++++++++++++++++++++++++++++ yarn.lock | 5 ++ 3 files changed, 202 insertions(+) create mode 100644 src/myzod/index.ts diff --git a/package.json b/package.json index 7e98e646..0d8d88f4 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "@typescript-eslint/parser": "^5.10.0", "eslint": "^8.7.0", "jest": "^27.4.7", + "myzod": "^1.8.7", "npm-run-all": "^4.1.5", "prettier": "2.5.1", "ts-jest": "^27.1.3", diff --git a/src/myzod/index.ts b/src/myzod/index.ts new file mode 100644 index 00000000..e9e7ee68 --- /dev/null +++ b/src/myzod/index.ts @@ -0,0 +1,196 @@ +import { isInput, isNonNullType, isListType, isNamedType } from './../graphql'; +import { ValidationSchemaPluginConfig } from '../config'; +import { + InputValueDefinitionNode, + NameNode, + TypeNode, + GraphQLSchema, + InputObjectTypeDefinitionNode, + EnumTypeDefinitionNode, +} from 'graphql'; +import { DeclarationBlock, indent } from '@graphql-codegen/visitor-plugin-common'; +import { TsVisitor } from '@graphql-codegen/typescript'; +import { buildApi, formatDirectiveConfig } from '../directive'; + +const importZod = `import { z } from 'zod'`; +const anySchema = `definedNonNullAnySchema`; + +export const ZodSchemaVisitor = (schema: GraphQLSchema, config: ValidationSchemaPluginConfig) => { + const tsVisitor = new TsVisitor(schema, config); + + const importTypes: string[] = []; + + return { + buildImports: (): string[] => { + if (config.importFrom && importTypes.length > 0) { + return [importZod, `import { ${importTypes.join(', ')} } from '${config.importFrom}'`]; + } + return [importZod]; + }, + initialEmit: (): string => + '\n' + + [ + new DeclarationBlock({}) + .asKind('type') + .withName('Properties') + .withContent(['Required<{', ' [K in keyof T]: z.ZodType;', '}>'].join('\n')).string, + // Unfortunately, zod doesn’t provide non-null defined any schema. + // This is a temporary hack until it is fixed. + // see: https://github.com/colinhacks/zod/issues/884 + new DeclarationBlock({}).asKind('type').withName('definedNonNullAny').withContent('{}').string, + new DeclarationBlock({}) + .export() + .asKind('const') + .withName(`isDefinedNonNullAny`) + .withContent(`(v: any): v is definedNonNullAny => v !== undefined && v !== null`).string, + new DeclarationBlock({}) + .export() + .asKind('const') + .withName(`${anySchema}`) + .withContent(`z.any().refine((v) => isDefinedNonNullAny(v))`).string, + ].join('\n'), + InputObjectTypeDefinition: (node: InputObjectTypeDefinitionNode) => { + const name = tsVisitor.convertName(node.name.value); + importTypes.push(name); + + const shape = node.fields + ?.map(field => generateInputObjectFieldZodSchema(config, tsVisitor, schema, field, 2)) + .join(',\n'); + + return new DeclarationBlock({}) + .export() + .asKind('function') + .withName(`${name}Schema(): z.ZodObject>`) + .withBlock([indent(`return z.object({`), shape, indent('})')].join('\n')).string; + }, + EnumTypeDefinition: (node: EnumTypeDefinitionNode) => { + const enumname = tsVisitor.convertName(node.name.value); + importTypes.push(enumname); + + if (config.enumsAsTypes) { + return new DeclarationBlock({}) + .export() + .asKind('const') + .withName(`${enumname}Schema`) + .withContent(`z.enum([${node.values?.map(enumOption => `'${enumOption.name.value}'`).join(', ')}])`).string; + } + + return new DeclarationBlock({}) + .export() + .asKind('const') + .withName(`${enumname}Schema`) + .withContent(`z.nativeEnum(${enumname})`).string; + }, + }; +}; + +const generateInputObjectFieldZodSchema = ( + config: ValidationSchemaPluginConfig, + tsVisitor: TsVisitor, + schema: GraphQLSchema, + field: InputValueDefinitionNode, + indentCount: number +): string => { + const gen = generateInputObjectFieldTypeZodSchema(config, tsVisitor, schema, field, field.type); + return indent(`${field.name.value}: ${maybeLazy(field.type, gen)}`, indentCount); +}; + +const generateInputObjectFieldTypeZodSchema = ( + config: ValidationSchemaPluginConfig, + tsVisitor: TsVisitor, + schema: GraphQLSchema, + field: InputValueDefinitionNode, + type: TypeNode, + parentType?: TypeNode +): string => { + if (isListType(type)) { + const gen = generateInputObjectFieldTypeZodSchema(config, tsVisitor, schema, field, type.type, type); + if (!isNonNullType(parentType)) { + const arrayGen = `z.array(${maybeLazy(type.type, gen)})`; + const maybeLazyGen = applyDirectives(config, field, arrayGen); + return `${maybeLazyGen}.nullish()`; + } + return `z.array(${maybeLazy(type.type, gen)})`; + } + if (isNonNullType(type)) { + const gen = generateInputObjectFieldTypeZodSchema(config, tsVisitor, schema, field, type.type, type); + return maybeLazy(type.type, gen); + } + if (isNamedType(type)) { + const gen = generateNameNodeZodSchema(config, tsVisitor, schema, type.name); + if (isListType(parentType)) { + return `${gen}.nullable()`; + } + const appliedDirectivesGen = applyDirectives(config, field, gen); + if (isNonNullType(parentType)) { + if (config.notAllowEmptyString === true) { + const tsType = tsVisitor.scalars[type.name.value]; + if (tsType === 'string') return `${gen}.min(1)`; + } + return appliedDirectivesGen; + } + if (isListType(parentType)) { + return `${appliedDirectivesGen}.nullable()`; + } + return `${appliedDirectivesGen}.nullish()`; + } + console.warn('unhandled type:', type); + return ''; +}; + +const applyDirectives = ( + config: ValidationSchemaPluginConfig, + field: InputValueDefinitionNode, + gen: string +): string => { + if (config.directives && field.directives) { + const formatted = formatDirectiveConfig(config.directives); + return gen + buildApi(formatted, field.directives); + } + return gen; +}; + +const generateNameNodeZodSchema = ( + config: ValidationSchemaPluginConfig, + tsVisitor: TsVisitor, + schema: GraphQLSchema, + node: NameNode +): string => { + const typ = schema.getType(node.value); + + if (typ && typ.astNode?.kind === 'InputObjectTypeDefinition') { + const enumName = tsVisitor.convertName(typ.astNode.name.value); + return `${enumName}Schema()`; + } + + if (typ && typ.astNode?.kind === 'EnumTypeDefinition') { + const enumName = tsVisitor.convertName(typ.astNode.name.value); + return `${enumName}Schema`; + } + + return zod4Scalar(config, tsVisitor, node.value); +}; + +const maybeLazy = (type: TypeNode, schema: string): string => { + if (isNamedType(type) && isInput(type.name.value)) { + return `z.lazy(() => ${schema})`; + } + return schema; +}; + +const zod4Scalar = (config: ValidationSchemaPluginConfig, tsVisitor: TsVisitor, scalarName: string): string => { + if (config.scalarSchemas?.[scalarName]) { + return config.scalarSchemas[scalarName]; + } + const tsType = tsVisitor.scalars[scalarName]; + switch (tsType) { + case 'string': + return `z.string()`; + case 'number': + return `z.number()`; + case 'boolean': + return `z.boolean()`; + } + console.warn('unhandled name:', scalarName); + return anySchema; +}; diff --git a/yarn.lock b/yarn.lock index d658fd2f..8e1383ec 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4321,6 +4321,11 @@ mute-stream@0.0.8: resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== +myzod@^1.8.7: + version "1.8.7" + resolved "https://registry.yarnpkg.com/myzod/-/myzod-1.8.7.tgz#f7e1d5379b517de039729735a8578a550c562292" + integrity sha512-H/Nmst+ZIGQppKVeOq6ufieRnnK0u+UfDLgCrG1Rtn6W/GzMoz6Ur9/iLBwB0N8s6ZE/4hhbRTUt3Pg7nH4X2Q== + nanoclone@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/nanoclone/-/nanoclone-0.2.1.tgz#dd4090f8f1a110d26bb32c49ed2f5b9235209ed4" From 97da322fb3d930d553df8fad0dde856da22889bc Mon Sep 17 00:00:00 2001 From: Parables Date: Fri, 1 Apr 2022 20:14:15 +0000 Subject: [PATCH 3/4] Implemented support for myzod --- src/myzod/index.ts | 60 +++++++++++++++++++++++++++------------------- 1 file changed, 35 insertions(+), 25 deletions(-) diff --git a/src/myzod/index.ts b/src/myzod/index.ts index e9e7ee68..f38cda6d 100644 --- a/src/myzod/index.ts +++ b/src/myzod/index.ts @@ -12,7 +12,7 @@ import { DeclarationBlock, indent } from '@graphql-codegen/visitor-plugin-common import { TsVisitor } from '@graphql-codegen/typescript'; import { buildApi, formatDirectiveConfig } from '../directive'; -const importZod = `import { z } from 'zod'`; +const importZod = `import myzod from 'myzod'`; const anySchema = `definedNonNullAnySchema`; export const ZodSchemaVisitor = (schema: GraphQLSchema, config: ValidationSchemaPluginConfig) => { @@ -27,13 +27,21 @@ export const ZodSchemaVisitor = (schema: GraphQLSchema, config: ValidationSchema } return [importZod]; }, + initialEmit: (): string => '\n' + [ + /* + * MyZod allows you to create typed objects with `myzod.Type` + * See https://www.npmjs.com/package/myzod#lazy new DeclarationBlock({}) .asKind('type') .withName('Properties') .withContent(['Required<{', ' [K in keyof T]: z.ZodType;', '}>'].join('\n')).string, + */ + /* + * MyZod allows empty object hence no need for these hacks + * See https://www.npmjs.com/package/myzod#object // Unfortunately, zod doesn’t provide non-null defined any schema. // This is a temporary hack until it is fixed. // see: https://github.com/colinhacks/zod/issues/884 @@ -48,25 +56,26 @@ export const ZodSchemaVisitor = (schema: GraphQLSchema, config: ValidationSchema .asKind('const') .withName(`${anySchema}`) .withContent(`z.any().refine((v) => isDefinedNonNullAny(v))`).string, + */ ].join('\n'), InputObjectTypeDefinition: (node: InputObjectTypeDefinitionNode) => { const name = tsVisitor.convertName(node.name.value); importTypes.push(name); const shape = node.fields - ?.map(field => generateInputObjectFieldZodSchema(config, tsVisitor, schema, field, 2)) + ?.map(field => generateInputObjectFieldMyZodSchema(config, tsVisitor, schema, field, 2)) .join(',\n'); return new DeclarationBlock({}) .export() - .asKind('function') - .withName(`${name}Schema(): z.ZodObject>`) - .withBlock([indent(`return z.object({`), shape, indent('})')].join('\n')).string; + .asKind('const') + .withName(`${name}Schema: myzod.Type<${name}>`) //TODO: Test this + .withBlock([indent(`myzod.object({`), shape, indent('})')].join('\n')).string; }, EnumTypeDefinition: (node: EnumTypeDefinitionNode) => { const enumname = tsVisitor.convertName(node.name.value); importTypes.push(enumname); - + /* if (config.enumsAsTypes) { return new DeclarationBlock({}) .export() @@ -74,28 +83,28 @@ export const ZodSchemaVisitor = (schema: GraphQLSchema, config: ValidationSchema .withName(`${enumname}Schema`) .withContent(`z.enum([${node.values?.map(enumOption => `'${enumOption.name.value}'`).join(', ')}])`).string; } - + */ return new DeclarationBlock({}) .export() .asKind('const') .withName(`${enumname}Schema`) - .withContent(`z.nativeEnum(${enumname})`).string; + .withContent(`myzod.enum(${enumname})`).string; }, }; }; -const generateInputObjectFieldZodSchema = ( +const generateInputObjectFieldMyZodSchema = ( config: ValidationSchemaPluginConfig, tsVisitor: TsVisitor, schema: GraphQLSchema, field: InputValueDefinitionNode, indentCount: number ): string => { - const gen = generateInputObjectFieldTypeZodSchema(config, tsVisitor, schema, field, field.type); + const gen = generateInputObjectFieldTypeMyZodSchema(config, tsVisitor, schema, field, field.type); return indent(`${field.name.value}: ${maybeLazy(field.type, gen)}`, indentCount); }; -const generateInputObjectFieldTypeZodSchema = ( +const generateInputObjectFieldTypeMyZodSchema = ( config: ValidationSchemaPluginConfig, tsVisitor: TsVisitor, schema: GraphQLSchema, @@ -104,22 +113,22 @@ const generateInputObjectFieldTypeZodSchema = ( parentType?: TypeNode ): string => { if (isListType(type)) { - const gen = generateInputObjectFieldTypeZodSchema(config, tsVisitor, schema, field, type.type, type); + const gen = generateInputObjectFieldTypeMyZodSchema(config, tsVisitor, schema, field, type.type, type); if (!isNonNullType(parentType)) { - const arrayGen = `z.array(${maybeLazy(type.type, gen)})`; + const arrayGen = `myzod.array(${maybeLazy(type.type, gen)})`; const maybeLazyGen = applyDirectives(config, field, arrayGen); - return `${maybeLazyGen}.nullish()`; + return `${maybeLazyGen}.optional()`; // to make it equivalent to nullish: `${maybeLazyGen}.optional().optional().or(nullable())`; } - return `z.array(${maybeLazy(type.type, gen)})`; + return `myzod.array(${maybeLazy(type.type, gen)})`; } if (isNonNullType(type)) { - const gen = generateInputObjectFieldTypeZodSchema(config, tsVisitor, schema, field, type.type, type); + const gen = generateInputObjectFieldTypeMyZodSchema(config, tsVisitor, schema, field, type.type, type); return maybeLazy(type.type, gen); } if (isNamedType(type)) { - const gen = generateNameNodeZodSchema(config, tsVisitor, schema, type.name); + const gen = generateNameNodeMyZodSchema(config, tsVisitor, schema, type.name); if (isListType(parentType)) { - return `${gen}.nullable()`; + return `${gen}.nullable()`; //TODO: Test this later } const appliedDirectivesGen = applyDirectives(config, field, gen); if (isNonNullType(parentType)) { @@ -130,14 +139,15 @@ const generateInputObjectFieldTypeZodSchema = ( return appliedDirectivesGen; } if (isListType(parentType)) { - return `${appliedDirectivesGen}.nullable()`; + return `${appliedDirectivesGen}.nullable()`; //TODO: Test this later } - return `${appliedDirectivesGen}.nullish()`; + return `${appliedDirectivesGen}.optional()`; // to make it equivalent to nullish: `${appliedDirectivesGen}.optional().or(nullable())`; } console.warn('unhandled type:', type); return ''; }; +// TODO: Find out how it works and implement it const applyDirectives = ( config: ValidationSchemaPluginConfig, field: InputValueDefinitionNode, @@ -150,7 +160,7 @@ const applyDirectives = ( return gen; }; -const generateNameNodeZodSchema = ( +const generateNameNodeMyZodSchema = ( config: ValidationSchemaPluginConfig, tsVisitor: TsVisitor, schema: GraphQLSchema, @@ -173,7 +183,7 @@ const generateNameNodeZodSchema = ( const maybeLazy = (type: TypeNode, schema: string): string => { if (isNamedType(type) && isInput(type.name.value)) { - return `z.lazy(() => ${schema})`; + return `myzod.lazy(() => ${schema})`; } return schema; }; @@ -185,11 +195,11 @@ const zod4Scalar = (config: ValidationSchemaPluginConfig, tsVisitor: TsVisitor, const tsType = tsVisitor.scalars[scalarName]; switch (tsType) { case 'string': - return `z.string()`; + return `myzod.string()`; case 'number': - return `z.number()`; + return `myzod.number()`; case 'boolean': - return `z.boolean()`; + return `myzod.boolean()`; } console.warn('unhandled name:', scalarName); return anySchema; From 1ca433491ab3748bb66a8b38a6101fa2e2b932db Mon Sep 17 00:00:00 2001 From: Parables Date: Sat, 2 Apr 2022 03:20:18 +0000 Subject: [PATCH 4/4] fully implemented myzod with test closes #25 --- src/config.ts | 2 +- src/index.ts | 3 + src/myzod/index.ts | 21 ++- tests/myzod.spec.ts | 375 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 389 insertions(+), 12 deletions(-) create mode 100644 tests/myzod.spec.ts diff --git a/src/config.ts b/src/config.ts index 8f894bbb..fd6503ed 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,6 +1,6 @@ import { TypeScriptPluginConfig } from '@graphql-codegen/typescript'; -export type ValidationSchema = 'yup' | 'zod'; +export type ValidationSchema = 'yup' | 'zod' | 'myzod'; export interface DirectiveConfig { [directive: string]: { diff --git a/src/index.ts b/src/index.ts index 291c10bd..5a3c8e1e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,5 @@ import { ZodSchemaVisitor } from './zod/index'; +import { MyZodSchemaVisitor } from './myzod/index'; import { transformSchemaAST } from '@graphql-codegen/schema-ast'; import { YupSchemaVisitor } from './yup/index'; import { ValidationSchemaPluginConfig } from './config'; @@ -30,6 +31,8 @@ export const plugin: PluginFunction { if (config?.schema === 'zod') { return ZodSchemaVisitor(schema, config); + } else if (config?.schema === 'myzod') { + return MyZodSchemaVisitor(schema, config); } return YupSchemaVisitor(schema, config); }; diff --git a/src/myzod/index.ts b/src/myzod/index.ts index f38cda6d..3612128e 100644 --- a/src/myzod/index.ts +++ b/src/myzod/index.ts @@ -15,7 +15,7 @@ import { buildApi, formatDirectiveConfig } from '../directive'; const importZod = `import myzod from 'myzod'`; const anySchema = `definedNonNullAnySchema`; -export const ZodSchemaVisitor = (schema: GraphQLSchema, config: ValidationSchemaPluginConfig) => { +export const MyZodSchemaVisitor = (schema: GraphQLSchema, config: ValidationSchemaPluginConfig) => { const tsVisitor = new TsVisitor(schema, config); const importTypes: string[] = []; @@ -27,7 +27,6 @@ export const ZodSchemaVisitor = (schema: GraphQLSchema, config: ValidationSchema } return [importZod]; }, - initialEmit: (): string => '\n' + [ @@ -75,15 +74,16 @@ export const ZodSchemaVisitor = (schema: GraphQLSchema, config: ValidationSchema EnumTypeDefinition: (node: EnumTypeDefinitionNode) => { const enumname = tsVisitor.convertName(node.name.value); importTypes.push(enumname); - /* + // z.enum are basically myzod.literals if (config.enumsAsTypes) { return new DeclarationBlock({}) .export() - .asKind('const') + .asKind('type') .withName(`${enumname}Schema`) - .withContent(`z.enum([${node.values?.map(enumOption => `'${enumOption.name.value}'`).join(', ')}])`).string; + .withContent(`myzod.literals(${node.values?.map(enumOption => `'${enumOption.name.value}'`).join(', ')})`) + .string; } - */ + return new DeclarationBlock({}) .export() .asKind('const') @@ -117,7 +117,7 @@ const generateInputObjectFieldTypeMyZodSchema = ( if (!isNonNullType(parentType)) { const arrayGen = `myzod.array(${maybeLazy(type.type, gen)})`; const maybeLazyGen = applyDirectives(config, field, arrayGen); - return `${maybeLazyGen}.optional()`; // to make it equivalent to nullish: `${maybeLazyGen}.optional().optional().or(nullable())`; + return `${maybeLazyGen}.optional().nullable()`; } return `myzod.array(${maybeLazy(type.type, gen)})`; } @@ -128,7 +128,7 @@ const generateInputObjectFieldTypeMyZodSchema = ( if (isNamedType(type)) { const gen = generateNameNodeMyZodSchema(config, tsVisitor, schema, type.name); if (isListType(parentType)) { - return `${gen}.nullable()`; //TODO: Test this later + return `${gen}.nullable()`; } const appliedDirectivesGen = applyDirectives(config, field, gen); if (isNonNullType(parentType)) { @@ -139,15 +139,14 @@ const generateInputObjectFieldTypeMyZodSchema = ( return appliedDirectivesGen; } if (isListType(parentType)) { - return `${appliedDirectivesGen}.nullable()`; //TODO: Test this later + return `${appliedDirectivesGen}.nullable()`; } - return `${appliedDirectivesGen}.optional()`; // to make it equivalent to nullish: `${appliedDirectivesGen}.optional().or(nullable())`; + return `${appliedDirectivesGen}.optional().nullable()`; } console.warn('unhandled type:', type); return ''; }; -// TODO: Find out how it works and implement it const applyDirectives = ( config: ValidationSchemaPluginConfig, field: InputValueDefinitionNode, diff --git a/tests/myzod.spec.ts b/tests/myzod.spec.ts new file mode 100644 index 00000000..b9af0d9f --- /dev/null +++ b/tests/myzod.spec.ts @@ -0,0 +1,375 @@ +import { buildSchema } from 'graphql'; +import { plugin } from '../src/index'; + +describe('myzod', () => { + test.each([ + [ + 'non-null and defined', + /* GraphQL */ ` + input PrimitiveInput { + a: ID! + b: String! + c: Boolean! + d: Int! + e: Float! + } + `, + [ + 'export const PrimitiveInputSchema: myzod.Type', + 'a: myzod.string()', + 'b: myzod.string()', + 'c: myzod.boolean()', + 'd: myzod.number()', + 'e: myzod.number()', + ], + ], + [ + 'nullish', + /* GraphQL */ ` + input PrimitiveInput { + a: ID + b: String + c: Boolean + d: Int + e: Float + z: String! # no defined check + } + `, + [ + 'export const PrimitiveInputSchema: myzod.Type', + // alphabet order + 'a: myzod.string().optional().nullable(),', + 'b: myzod.string().optional().nullable(),', + 'c: myzod.boolean().optional().nullable(),', + 'd: myzod.number().optional().nullable(),', + 'e: myzod.number().optional().nullable(),', + ], + ], + [ + 'array', + /* GraphQL */ ` + input ArrayInput { + a: [String] + b: [String!] + c: [String!]! + d: [[String]] + e: [[String]!] + f: [[String]!]! + } + `, + [ + 'export const ArrayInputSchema: myzod.Type', + 'a: myzod.array(myzod.string().nullable()).optional().nullable(),', + 'b: myzod.array(myzod.string()).optional().nullable(),', + 'c: myzod.array(myzod.string()),', + 'd: myzod.array(myzod.array(myzod.string().nullable()).optional().nullable()).optional().nullable(),', + 'e: myzod.array(myzod.array(myzod.string().nullable())).optional().nullable(),', + 'f: myzod.array(myzod.array(myzod.string().nullable()))', + ], + ], + [ + 'ref input object', + /* GraphQL */ ` + input AInput { + b: BInput! + } + input BInput { + c: CInput! + } + input CInput { + a: AInput! + } + `, + [ + 'export const AInputSchema: myzod.Type', + 'b: myzod.lazy(() => BInputSchema())', + 'export const BInputSchema: myzod.Type', + 'c: myzod.lazy(() => CInputSchema())', + 'export const CInputSchema: myzod.Type', + 'a: myzod.lazy(() => AInputSchema())', + ], + ], + [ + 'nested input object', + /* GraphQL */ ` + input NestedInput { + child: NestedInput + childrens: [NestedInput] + } + `, + [ + 'export const NestedInputSchema: myzod.Type', + 'child: myzod.lazy(() => NestedInputSchema().optional().nullable()),', + 'childrens: myzod.array(myzod.lazy(() => NestedInputSchema().nullable())).optional().nullable()', + ], + ], + [ + 'enum', + /* GraphQL */ ` + enum PageType { + PUBLIC + BASIC_AUTH + } + input PageInput { + pageType: PageType! + } + `, + [ + 'export const PageTypeSchema = myzod.enum(PageType)', + 'export const PageInputSchema: myzod.Type', + 'pageType: PageTypeSchema', + ], + ], + [ + 'camelcase', + /* GraphQL */ ` + input HTTPInput { + method: HTTPMethod + url: URL! + } + + enum HTTPMethod { + GET + POST + } + + scalar URL # unknown scalar, should be any (definedNonNullAnySchema) + `, + [ + 'export const HttpInputSchema: myzod.Type', + 'export const HttpMethodSchema = myzod.enum(HttpMethod)', + 'method: HttpMethodSchema', + 'url: definedNonNullAnySchema', + ], + ], + ])('%s', async (_, textSchema, wantContains) => { + const schema = buildSchema(textSchema); + const result = await plugin(schema, [], { schema: 'myzod' }, {}); + expect(result.prepend).toContain("import myzod from 'myzod'"); + + for (const wantContain of wantContains) { + expect(result.content).toContain(wantContain); + } + }); + + it('with scalars', async () => { + const schema = buildSchema(/* GraphQL */ ` + input Say { + phrase: Text! + times: Count! + } + + scalar Count + scalar Text + `); + const result = await plugin( + schema, + [], + { + schema: 'myzod', + scalars: { + Text: 'string', + Count: 'number', + }, + }, + {} + ); + expect(result.content).toContain('phrase: myzod.string()'); + expect(result.content).toContain('times: myzod.number()'); + }); + + it('with importFrom', async () => { + const schema = buildSchema(/* GraphQL */ ` + input Say { + phrase: String! + } + `); + const result = await plugin( + schema, + [], + { + schema: 'myzod', + importFrom: './types', + }, + {} + ); + expect(result.prepend).toContain("import { Say } from './types'"); + expect(result.content).toContain('phrase: myzod.string()'); + }); + + it('with enumsAsTypes', async () => { + const schema = buildSchema(/* GraphQL */ ` + enum PageType { + PUBLIC + BASIC_AUTH + } + `); + const result = await plugin( + schema, + [], + { + schema: 'myzod', + enumsAsTypes: true, + }, + {} + ); + expect(result.content).toContain("export type PageTypeSchema = myzod.literals('PUBLIC', 'BASIC_AUTH')"); + }); + + it('with notAllowEmptyString', async () => { + const schema = buildSchema(/* GraphQL */ ` + input PrimitiveInput { + a: ID! + b: String! + c: Boolean! + d: Int! + e: Float! + } + `); + const result = await plugin( + schema, + [], + { + schema: 'myzod', + notAllowEmptyString: true, + }, + {} + ); + const wantContains = [ + 'export const PrimitiveInputSchema: myzod.Type', + 'a: myzod.string().min(1),', + 'b: myzod.string().min(1),', + 'c: myzod.boolean(),', + 'd: myzod.number(),', + 'e: myzod.number()', + ]; + for (const wantContain of wantContains) { + expect(result.content).toContain(wantContain); + } + }); + + it('with scalarSchemas', async () => { + const schema = buildSchema(/* GraphQL */ ` + input ScalarsInput { + date: Date! + email: Email + str: String! + } + scalar Date + scalar Email + `); + const result = await plugin( + schema, + [], + { + schema: 'myzod', + scalarSchemas: { + Date: 'myzod.date()', + Email: 'myzod.string()', // generate the basic type. User can later extend it using `withPredicate(fn: (val: string) => boolean), errMsg?: string }` + }, + }, + {} + ); + const wantContains = [ + 'export const ScalarsInputSchema: myzod.Type', + 'date: myzod.date(),', + 'email: myzod.string()', // TODO: Test implementation + 'str: myzod.string()', + ]; + for (const wantContain of wantContains) { + expect(result.content).toContain(wantContain); + } + }); + describe('issues #19', () => { + it('string field', async () => { + const schema = buildSchema(/* GraphQL */ ` + input UserCreateInput { + profile: String @constraint(minLength: 1, maxLength: 5000) + } + + directive @constraint(minLength: Int!, maxLength: Int!) on INPUT_FIELD_DEFINITION + `); + const result = await plugin( + schema, + [], + { + schema: 'myzod', + directives: { + constraint: { + minLength: ['min', '$1', 'Please input more than $1'], + maxLength: ['max', '$1', 'Please input less than $1'], + }, + }, + }, + {} + ); + const wantContains = [ + 'export const UserCreateInputSchema: myzod.Type', + 'profile: myzod.string().min(1, "Please input more than 1").max(5000, "Please input less than 5000").optional().nullable()', + ]; + for (const wantContain of wantContains) { + expect(result.content).toContain(wantContain); + } + }); + it('not null field', async () => { + const schema = buildSchema(/* GraphQL */ ` + input UserCreateInput { + profile: String! @constraint(minLength: 1, maxLength: 5000) + } + + directive @constraint(minLength: Int!, maxLength: Int!) on INPUT_FIELD_DEFINITION + `); + const result = await plugin( + schema, + [], + { + schema: 'myzod', + directives: { + constraint: { + minLength: ['min', '$1', 'Please input more than $1'], + maxLength: ['max', '$1', 'Please input less than $1'], + }, + }, + }, + {} + ); + const wantContains = [ + 'export const UserCreateInputSchema: myzod.Type', + 'profile: myzod.string().min(1, "Please input more than 1").max(5000, "Please input less than 5000")', + ]; + for (const wantContain of wantContains) { + expect(result.content).toContain(wantContain); + } + }); + it('list field', async () => { + const schema = buildSchema(/* GraphQL */ ` + input UserCreateInput { + profile: [String] @constraint(minLength: 1, maxLength: 5000) + } + + directive @constraint(minLength: Int!, maxLength: Int!) on INPUT_FIELD_DEFINITION + `); + const result = await plugin( + schema, + [], + { + schema: 'myzod', + directives: { + constraint: { + minLength: ['min', '$1', 'Please input more than $1'], + maxLength: ['max', '$1', 'Please input less than $1'], + }, + }, + }, + {} + ); + const wantContains = [ + 'export const UserCreateInputSchema: myzod.Type', + 'profile: myzod.array(myzod.string().nullable()).min(1, "Please input more than 1").max(5000, "Please input less than 5000").optional().nullable()', + ]; + for (const wantContain of wantContains) { + expect(result.content).toContain(wantContain); + } + }); + }); +});