From 3d336dcab0de44b6f7dc3843264f3b5cfaf04f21 Mon Sep 17 00:00:00 2001 From: Kei Kamikawa Date: Wed, 26 Jan 2022 23:26:19 +0900 Subject: [PATCH 01/10] added zod --- src/config.ts | 2 +- src/index.ts | 15 ++++- src/yup/index.ts | 1 + src/zod/index.ts | 168 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 183 insertions(+), 3 deletions(-) create mode 100644 src/zod/index.ts diff --git a/src/config.ts b/src/config.ts index 0125018d..40507132 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,6 +1,6 @@ import { TypeScriptPluginConfig } from '@graphql-codegen/typescript'; -export type ValidationSchema = 'yup'; +export type ValidationSchema = 'yup' | 'zod'; export interface DirectiveConfig { [directive: string]: { diff --git a/src/index.ts b/src/index.ts index b865b318..c8c23950 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,4 @@ +import { ZodSchemaVisitor } from './zod/index'; import { transformSchemaAST } from '@graphql-codegen/schema-ast'; import { YupSchemaVisitor } from './yup/index'; import { ValidationSchemaPluginConfig } from './config'; @@ -10,7 +11,7 @@ export const plugin: PluginFunction { const { schema: _schema, ast } = transformSchemaAST(schema, config); - const { buildImports, ...visitor } = YupSchemaVisitor(_schema, config); + const { buildImports, initialEmit, ...visitor } = schemaVisitor(_schema, config); const result = oldVisit(ast, { leave: visitor, @@ -22,6 +23,16 @@ export const plugin: PluginFunction { + if (config?.schema === 'zod') { + return ZodSchemaVisitor(schema, config) + } + return YupSchemaVisitor(schema, config) +} diff --git a/src/yup/index.ts b/src/yup/index.ts index 5f275187..c7280a67 100644 --- a/src/yup/index.ts +++ b/src/yup/index.ts @@ -26,6 +26,7 @@ export const YupSchemaVisitor = (schema: GraphQLSchema, config: ValidationSchema } return [importYup]; }, + initialEmit: (): string => "", InputObjectTypeDefinition: (node: InputObjectTypeDefinitionNode) => { const name = tsVisitor.convertName(node.name.value); importTypes.push(name); diff --git a/src/zod/index.ts b/src/zod/index.ts new file mode 100644 index 00000000..4b5870ed --- /dev/null +++ b/src/zod/index.ts @@ -0,0 +1,168 @@ +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 => + [ + // 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 + 'type definedNonNullAny = {}', + 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}: z.ZodSchema`) + .withContent(`z.any().refine((v) => isDefinedNonNullAny(v))`).string, + ].join('\n\n'), + InputObjectTypeDefinition: (node: InputObjectTypeDefinitionNode) => { + const name = tsVisitor.convertName(node.name.value); + importTypes.push(name); + + const shape = node.fields + ?.map(field => generateInputObjectFieldYupSchema(config, tsVisitor, schema, field, 2)) + .join(',\n'); + + return new DeclarationBlock({}) + .export() + .asKind('function') + .withName(`${name}Schema(): z.ZodSchema<${name}>`) + .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 generateInputObjectFieldYupSchema = ( + config: ValidationSchemaPluginConfig, + tsVisitor: TsVisitor, + schema: GraphQLSchema, + field: InputValueDefinitionNode, + indentCount: number +): string => { + let gen = generateInputObjectFieldTypeZodSchema(config, tsVisitor, schema, field.type); + if (config.directives && field.directives) { + const formatted = formatDirectiveConfig(config.directives); + gen += buildApi(formatted, field.directives); + } + return indent(`${field.name.value}: ${maybeLazy(field.type, gen)}`, indentCount); +}; + +const generateInputObjectFieldTypeZodSchema = ( + config: ValidationSchemaPluginConfig, + tsVisitor: TsVisitor, + schema: GraphQLSchema, + type: TypeNode, + parentType?: TypeNode +): string => { + if (isListType(type)) { + const gen = generateInputObjectFieldTypeZodSchema(config, tsVisitor, schema, type.type, type); + if (!isNonNullType(parentType)) { + return `z.array(${maybeLazy(type.type, gen)}).nullish()`; + } + return `z.array(${maybeLazy(type.type, gen)})`; + } + if (isNonNullType(type)) { + const gen = generateInputObjectFieldTypeZodSchema(config, tsVisitor, schema, type.type, type); + return maybeLazy(type.type, gen); + } + if (isNamedType(type)) { + const gen = generateNameNodeZodSchema(tsVisitor, schema, type.name); + if (isNonNullType(parentType)) { + return gen + } + if (isListType(parentType)) { + return `${gen}.nullable()` + } + return `${gen}.nullish()` + } + console.warn('unhandled type:', type); + return ''; +}; + +const generateNameNodeZodSchema = ( + 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(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 = (tsVisitor: TsVisitor, scalarName: string): string => { + 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; +}; From 959b95cec8e250668d08a0a064b3079f4eb46fa1 Mon Sep 17 00:00:00 2001 From: Kei Kamikawa Date: Wed, 26 Jan 2022 23:26:32 +0900 Subject: [PATCH 02/10] added test --- tests/zod.spec.ts | 218 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 218 insertions(+) create mode 100644 tests/zod.spec.ts diff --git a/tests/zod.spec.ts b/tests/zod.spec.ts new file mode 100644 index 00000000..642a8492 --- /dev/null +++ b/tests/zod.spec.ts @@ -0,0 +1,218 @@ +import { buildSchema } from 'graphql'; +import { plugin } from '../src/index'; + +describe('zod', () => { + test.each([ + [ + 'non-null and defined', + /* GraphQL */ ` + input PrimitiveInput { + a: ID! + b: String! + c: Boolean! + d: Int! + e: Float! + } + `, + [ + 'export function PrimitiveInputSchema(): z.ZodSchema', + 'a: z.string()', + 'b: z.string()', + 'c: z.boolean()', + 'd: z.number()', + 'e: z.number()', + ], + ], + [ + 'nullish', + /* GraphQL */ ` + input PrimitiveInput { + a: ID + b: String + c: Boolean + d: Int + e: Float + z: String! # no defined check + } + `, + [ + 'export function PrimitiveInputSchema(): z.ZodSchema', + // alphabet order + 'a: z.string().nullish(),', + 'b: z.string().nullish(),', + 'c: z.boolean().nullish(),', + 'd: z.number().nullish(),', + 'e: z.number().nullish(),', + ], + ], + [ + 'array', + /* GraphQL */ ` + input ArrayInput { + a: [String] + b: [String!] + c: [String!]! + d: [[String]] + e: [[String]!] + f: [[String]!]! + } + `, + [ + 'export function ArrayInputSchema(): z.ZodSchema', + 'a: z.array(z.string().nullable()).nullish(),', + 'b: z.array(z.string()).nullish(),', + 'c: z.array(z.string()),', + 'd: z.array(z.array(z.string().nullable()).nullish()).nullish(),', + 'e: z.array(z.array(z.string().nullable())).nullish(),', + 'f: z.array(z.array(z.string().nullable()))', + ], + ], + [ + 'ref input object', + /* GraphQL */ ` + input AInput { + b: BInput! + } + input BInput { + c: CInput! + } + input CInput { + a: AInput! + } + `, + [ + 'export function AInputSchema(): z.ZodSchema', + 'b: z.lazy(() => BInputSchema())', + 'export function BInputSchema(): z.ZodSchema', + 'c: z.lazy(() => CInputSchema())', + 'export function CInputSchema(): z.ZodSchema', + 'a: z.lazy(() => AInputSchema())', + ], + ], + [ + 'nested input object', + /* GraphQL */ ` + input NestedInput { + child: NestedInput + childrens: [NestedInput] + } + `, + [ + 'export function NestedInputSchema(): z.ZodSchema', + 'child: z.lazy(() => NestedInputSchema().nullish()),', + 'childrens: z.array(z.lazy(() => NestedInputSchema().nullable())).nullish()', + ], + ], + [ + 'enum', + /* GraphQL */ ` + enum PageType { + PUBLIC + BASIC_AUTH + } + input PageInput { + pageType: PageType! + } + `, + [ + 'export const PageTypeSchema = z.nativeEnum(PageType)', + 'export function PageInputSchema(): z.ZodSchema', + 'pageType: PageTypeSchema', + ], + ], + [ + 'camelcase', + /* GraphQL */ ` + input HTTPInput { + method: HTTPMethod + url: URL! + } + + enum HTTPMethod { + GET + POST + } + + scalar URL # unknown scalar, should be any (definedNonNullAnySchema) + `, + [ + 'export function HttpInputSchema(): z.ZodSchema', + 'export const HttpMethodSchema = z.nativeEnum(HttpMethod)', + 'method: HttpMethodSchema', + 'url: definedNonNullAnySchema', + ], + ], + ])('%s', async (_, textSchema, wantContains) => { + const schema = buildSchema(textSchema); + const result = await plugin(schema, [], { schema: 'zod' }, {}); + expect(result.prepend).toContain("import { z } from 'zod'"); + + 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: 'zod', + scalars: { + Text: 'string', + Count: 'number', + }, + }, + {} + ); + expect(result.content).toContain('phrase: z.string()'); + expect(result.content).toContain('times: z.number()'); + }); + + it('with importFrom', async () => { + const schema = buildSchema(/* GraphQL */ ` + input Say { + phrase: String! + } + `); + const result = await plugin( + schema, + [], + { + schema: 'zod', + importFrom: './types', + }, + {} + ); + expect(result.prepend).toContain("import { Say } from './types'"); + expect(result.content).toContain('phrase: z.string()'); + }); + + it('with enumsAsTypes', async () => { + const schema = buildSchema(/* GraphQL */ ` + enum PageType { + PUBLIC + BASIC_AUTH + } + `); + const result = await plugin( + schema, + [], + { + schema: 'zod', + enumsAsTypes: true, + }, + {} + ); + expect(result.content).toContain("export const PageTypeSchema = z.enum(['PUBLIC', 'BASIC_AUTH'])"); + }); +}); From a662acbc4978613cb08fbe98ae1cd1a9454735e0 Mon Sep 17 00:00:00 2001 From: Kei Kamikawa Date: Wed, 26 Jan 2022 23:33:30 +0900 Subject: [PATCH 03/10] fixed initialEmit in zod --- src/index.ts | 11 ++++------- src/zod/index.ts | 19 ++++++++----------- 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/src/index.ts b/src/index.ts index c8c23950..291c10bd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,16 +23,13 @@ export const plugin: PluginFunction { if (config?.schema === 'zod') { - return ZodSchemaVisitor(schema, config) + return ZodSchemaVisitor(schema, config); } - return YupSchemaVisitor(schema, config) -} + return YupSchemaVisitor(schema, config); +}; diff --git a/src/zod/index.ts b/src/zod/index.ts index 4b5870ed..04328bcb 100644 --- a/src/zod/index.ts +++ b/src/zod/index.ts @@ -13,7 +13,7 @@ import { TsVisitor } from '@graphql-codegen/typescript'; import { buildApi, formatDirectiveConfig } from '../directive'; const importZod = `import { z } from 'zod'`; -const anySchema = `definedNonNullAnySchema` +const anySchema = `definedNonNullAnySchema`; export const ZodSchemaVisitor = (schema: GraphQLSchema, config: ValidationSchemaPluginConfig) => { const tsVisitor = new TsVisitor(schema, config); @@ -28,11 +28,12 @@ export const ZodSchemaVisitor = (schema: GraphQLSchema, config: ValidationSchema return [importZod]; }, initialEmit: (): string => + '\n' + [ // 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 - 'type definedNonNullAny = {}', + new DeclarationBlock({}).asKind('type').withName('definedNonNullAny').withContent('{}').string, new DeclarationBlock({}) .export() .asKind('const') @@ -43,7 +44,7 @@ export const ZodSchemaVisitor = (schema: GraphQLSchema, config: ValidationSchema .asKind('const') .withName(`${anySchema}: z.ZodSchema`) .withContent(`z.any().refine((v) => isDefinedNonNullAny(v))`).string, - ].join('\n\n'), + ].join('\n'), InputObjectTypeDefinition: (node: InputObjectTypeDefinitionNode) => { const name = tsVisitor.convertName(node.name.value); importTypes.push(name); @@ -115,22 +116,18 @@ const generateInputObjectFieldTypeZodSchema = ( if (isNamedType(type)) { const gen = generateNameNodeZodSchema(tsVisitor, schema, type.name); if (isNonNullType(parentType)) { - return gen + return gen; } if (isListType(parentType)) { - return `${gen}.nullable()` + return `${gen}.nullable()`; } - return `${gen}.nullish()` + return `${gen}.nullish()`; } console.warn('unhandled type:', type); return ''; }; -const generateNameNodeZodSchema = ( - tsVisitor: TsVisitor, - schema: GraphQLSchema, - node: NameNode -): string => { +const generateNameNodeZodSchema = (tsVisitor: TsVisitor, schema: GraphQLSchema, node: NameNode): string => { const typ = schema.getType(node.value); if (typ && typ.astNode?.kind === 'InputObjectTypeDefinition') { From c4615d758114934f3c361702832f03a27a6f6e7e Mon Sep 17 00:00:00 2001 From: Kei Kamikawa Date: Wed, 26 Jan 2022 23:34:01 +0900 Subject: [PATCH 04/10] fixed tsconfig to build zod example --- tsconfig.json | 7 +++---- tsconfig.main.json | 6 +++++- tsconfig.module.json | 7 +++---- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/tsconfig.json b/tsconfig.json index dd41427a..85db1042 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,16 +4,15 @@ "incremental": true, "declaration": true, "noEmit": false, - "rootDir": "src", "outDir": "dist", "baseUrl": ".", "paths": {} }, "include": [ - "src" + "src", + "example" ], "exclude": [ - "./example", - "./dist" + "dist" ] } \ No newline at end of file diff --git a/tsconfig.main.json b/tsconfig.main.json index 54a340ab..c33d0b44 100644 --- a/tsconfig.main.json +++ b/tsconfig.main.json @@ -1,9 +1,13 @@ { "extends": "./tsconfig", "compilerOptions": { + "rootDir": "src", "outDir": "dist/main", "types": [ "node" ] - } + }, + "exclude": [ + "example" + ] } \ No newline at end of file diff --git a/tsconfig.module.json b/tsconfig.module.json index 1c9cedf4..5fad3c03 100644 --- a/tsconfig.module.json +++ b/tsconfig.module.json @@ -6,15 +6,14 @@ "declaration": false, "moduleResolution": "node", "resolveJsonModule": true, + "rootDir": "src", "outDir": "dist/module", "types": [ "node" ] }, - "include": [ - "src/**/*.ts" - ], "exclude": [ - "node_modules/**" + "node_modules/**", + "example" ] } \ No newline at end of file From ac852b0d44e5ae9f047e1af297933b0933bb7773 Mon Sep 17 00:00:00 2001 From: Kei Kamikawa Date: Wed, 26 Jan 2022 23:34:27 +0900 Subject: [PATCH 05/10] install zod --- codegen.yml | 33 +++++++++++++++++ example/zod/schemas.ts | 83 ++++++++++++++++++++++++++++++++++++++++++ package.json | 3 +- yarn.lock | 5 +++ 4 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 example/zod/schemas.ts diff --git a/codegen.yml b/codegen.yml index 1c24c845..51ff267b 100644 --- a/codegen.yml +++ b/codegen.yml @@ -37,3 +37,36 @@ generates: max: ['max', '$1 + 1'] exclusiveMin: min exclusiveMax: max + example/zod/schemas.ts: + plugins: + - ./dist/main/index.js: + schema: zod + importFrom: ../types + directives: + required: + msg: required + # This is example using constraint directive. + # see: https://github.com/confuser/graphql-constraint-directive + constraint: + minLength: min # same as ['min', '$1'] + maxLength: max + startsWith: ['regex', '/^$1/'] + endsWith: ['regex', '/$1$/'] + contains: ['regex', '/$1/'] + notContains: ['regex', '/^((?!$1).)*$/'] + pattern: ['regex', '/$1/'] + format: + # For example, `@constraint(format: "uri")`. this case $1 will be "uri". + # Therefore the generator generates yup schema `.url()` followed by `uri: 'url'` + # If $1 does not match anywhere, the generator will ignore. + uri: url + email: email + uuid: uuid + # yup does not have `ipv4` API. If you want to add this, + # you need to add the logic using `yup.addMethod`. + # see: https://github.com/jquense/yup#addmethodschematype-schema-name-string-method--schema-void + ipv4: ipv4 + min: ['min', '$1 - 1'] + max: ['max', '$1 + 1'] + exclusiveMin: min + exclusiveMax: max diff --git a/example/zod/schemas.ts b/example/zod/schemas.ts new file mode 100644 index 00000000..56f20233 --- /dev/null +++ b/example/zod/schemas.ts @@ -0,0 +1,83 @@ +import { z } from 'zod' +import { AttributeInput, ButtonComponentType, ComponentInput, DropDownComponentInput, EventArgumentInput, EventInput, EventOptionType, HttpInput, HttpMethod, LayoutInput, PageInput, PageType } from '../types' + +type definedNonNullAny = {}; + +export const isDefinedNonNullAny = (v: any): v is definedNonNullAny => v !== undefined && v !== null; + +export const definedNonNullAnySchema: z.ZodSchema = z.any().refine((v) => isDefinedNonNullAny(v)); + +export function AttributeInputSchema(): z.ZodSchema { + return z.object({ + key: z.string().nullish(), + val: z.string().nullish() + }) +} + +export const ButtonComponentTypeSchema = z.nativeEnum(ButtonComponentType); + +export function ComponentInputSchema(): z.ZodSchema { + return z.object({ + child: z.lazy(() => ComponentInputSchema().nullish()), + childrens: z.array(z.lazy(() => ComponentInputSchema().nullable())).nullish(), + event: z.lazy(() => EventInputSchema().nullish()), + name: z.string(), + type: ButtonComponentTypeSchema + }) +} + +export function DropDownComponentInputSchema(): z.ZodSchema { + return z.object({ + dropdownComponent: z.lazy(() => ComponentInputSchema().nullish()), + getEvent: z.lazy(() => EventInputSchema()) + }) +} + +export function EventArgumentInputSchema(): z.ZodSchema { + return z.object({ + name: z.string().min(5), + value: z.string().regex(/^foo/) + }) +} + +export function EventInputSchema(): z.ZodSchema { + return z.object({ + arguments: z.array(z.lazy(() => EventArgumentInputSchema())), + options: z.array(EventOptionTypeSchema).nullish() + }) +} + +export const EventOptionTypeSchema = z.nativeEnum(EventOptionType); + +export function HttpInputSchema(): z.ZodSchema { + return z.object({ + method: HttpMethodSchema.nullish(), + url: definedNonNullAnySchema + }) +} + +export const HttpMethodSchema = z.nativeEnum(HttpMethod); + +export function LayoutInputSchema(): z.ZodSchema { + return z.object({ + dropdown: z.lazy(() => DropDownComponentInputSchema().nullish()) + }) +} + +export function PageInputSchema(): z.ZodSchema { + return z.object({ + attributes: z.array(z.lazy(() => AttributeInputSchema())).nullish(), + date: definedNonNullAnySchema.nullish(), + height: z.number(), + id: z.string(), + layout: z.lazy(() => LayoutInputSchema()), + pageType: PageTypeSchema, + postIDs: z.array(z.string()).nullish(), + show: z.boolean(), + tags: z.array(z.string().nullable()).nullish(), + title: z.string(), + width: z.number() + }) +} + +export const PageTypeSchema = z.nativeEnum(PageType); diff --git a/package.json b/package.json index 977145bd..871346b4 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,8 @@ "prettier": "2.5.1", "ts-jest": "^27.1.3", "typescript": "^4.5.4", - "yup": "^0.32.11" + "yup": "^0.32.11", + "zod": "^3.11.6" }, "dependencies": { "@graphql-codegen/plugin-helpers": "^2.3.2", diff --git a/yarn.lock b/yarn.lock index 03a0d6d7..d658fd2f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5975,3 +5975,8 @@ yup@^0.32.11: nanoclone "^0.2.1" property-expr "^2.0.4" toposort "^2.0.2" + +zod@^3.11.6: + version "3.11.6" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.11.6.tgz#e43a5e0c213ae2e02aefe7cb2b1a6fa3d7f1f483" + integrity sha512-daZ80A81I3/9lIydI44motWe6n59kRBfNzTuS2bfzVh1nAXi667TOTWWtatxyG+fwgNUiagSj/CWZwRRbevJIg== From 0e1edda4f477d676536d1592ce1b8b751d3410b0 Mon Sep 17 00:00:00 2001 From: Code-Hex Date: Wed, 26 Jan 2022 14:35:57 +0000 Subject: [PATCH 06/10] Apply auto lint-fix changes --- src/yup/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yup/index.ts b/src/yup/index.ts index c7280a67..b6647863 100644 --- a/src/yup/index.ts +++ b/src/yup/index.ts @@ -26,7 +26,7 @@ export const YupSchemaVisitor = (schema: GraphQLSchema, config: ValidationSchema } return [importYup]; }, - initialEmit: (): string => "", + initialEmit: (): string => '', InputObjectTypeDefinition: (node: InputObjectTypeDefinitionNode) => { const name = tsVisitor.convertName(node.name.value); importTypes.push(name); From 978f2d1c4923c8a087ca73a7373fed8d0560b62b Mon Sep 17 00:00:00 2001 From: Kei Kamikawa Date: Thu, 27 Jan 2022 00:57:40 +0900 Subject: [PATCH 07/10] filter defined configs --- src/directive.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/directive.ts b/src/directive.ts index de72be2b..59caa56e 100644 --- a/src/directive.ts +++ b/src/directive.ts @@ -109,6 +109,7 @@ export const formatDirectiveObjectArguments = (args: DirectiveObjectArguments): // ``` export const buildApi = (config: FormattedDirectiveConfig, directives: ReadonlyArray): string => directives + .filter(directive => config[directive.name.value] !== undefined) .map(directive => { const directiveName = directive.name.value; const argsConfig = config[directiveName]; From 2f3d4825800f45518a59122cb6a687c767a6905e Mon Sep 17 00:00:00 2001 From: Kei Kamikawa Date: Sun, 30 Jan 2022 23:21:25 +0900 Subject: [PATCH 08/10] fixed README --- README.md | 61 +++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 52 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 2ced1593..307a6c82 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [GraphQL code generator](https://github.com/dotansimha/graphql-code-generator) plugin to generate form validation schema from your GraphQL schema. - [x] support [yup](https://github.com/jquense/yup) -- [ ] support [zod](https://github.com/colinhacks/zod) +- [x] support [zod](https://github.com/colinhacks/zod) ## Quick Start @@ -26,7 +26,7 @@ generates: # see: https://www.graphql-code-generator.com/plugins/typescript strictScalars: true # You can also write the config for this plugin together - schema: yup + schema: yup # or zod ``` You can check [example directory](https://github.com/Code-Hex/graphql-codegen-typescript-validation-schema/tree/main/example) if you want to see more complex config example or how is generated some files. @@ -39,6 +39,8 @@ type: `ValidationSchema` default: `'yup'` Specify generete validation schema you want. +You can specify `yup` or `zod`. + ```yml generates: path/to/graphql.ts: @@ -87,6 +89,15 @@ type: `DirectiveConfig` Generates validation schema with more API based on directive schema. For example, yaml config and GraphQL schema is here. +```graphql +input ExampleInput { + email: String! @required(msg: "Hello, World!") @constraint(minLength: 50, format: "email") + message: String! @constraint(startsWith: "Hello") +} +``` + +#### yup + ```yml generates: path/to/graphql.ts: @@ -114,13 +125,6 @@ generates: email: email ``` -```graphql -input ExampleInput { - email: String! @required(msg: "Hello, World!") @constraint(minLength: 50, format: "email") - message: String! @constraint(startsWith: "Hello") -} -``` - Then generates yup validation schema like below. ```ts @@ -131,3 +135,42 @@ export function ExampleInputSchema(): yup.SchemaOf { }) } ``` + +#### zod + + +```yml +generates: + path/to/graphql.ts: + plugins: + - typescript + - graphql-codegen-validation-schema + config: + schema: zod + directives: + # Write directives like + # + # directive: + # arg1: schemaApi + # arg2: ["schemaApi2", "Hello $1"] + # + # See more examples in `./tests/directive.spec.ts` + # https://github.com/Code-Hex/graphql-codegen-typescript-validation-schema/blob/main/tests/directive.spec.ts + constraint: + minLength: min + # Replace $1 with specified `startsWith` argument value of the constraint directive + startsWith: ["regex", "/^$1/", "message"] + format: + email: email +``` + +Then generates yup validation schema like below. + +```ts +export function ExampleInputSchema(): z.ZodSchema { + return z.object({ + email: z.string().min(50).email(), + message: z.string().regex(/^Hello/, "message") + }) +} +``` \ No newline at end of file From 8d7094d6494ba52952a4b8b3026b0e5f1a1920aa Mon Sep 17 00:00:00 2001 From: Kei Kamikawa Date: Sun, 30 Jan 2022 23:21:37 +0900 Subject: [PATCH 09/10] fixed example for zod --- codegen.yml | 35 +++++++++++------------------------ example/zod/schemas.ts | 2 +- 2 files changed, 12 insertions(+), 25 deletions(-) diff --git a/codegen.yml b/codegen.yml index 51ff267b..32129b68 100644 --- a/codegen.yml +++ b/codegen.yml @@ -43,30 +43,17 @@ generates: schema: zod importFrom: ../types directives: - required: - msg: required - # This is example using constraint directive. - # see: https://github.com/confuser/graphql-constraint-directive + # Write directives like + # + # directive: + # arg1: schemaApi + # arg2: ["schemaApi2", "Hello $1"] + # + # See more examples in `./tests/directive.spec.ts` + # https://github.com/Code-Hex/graphql-codegen-typescript-validation-schema/blob/main/tests/directive.spec.ts constraint: - minLength: min # same as ['min', '$1'] - maxLength: max - startsWith: ['regex', '/^$1/'] - endsWith: ['regex', '/$1$/'] - contains: ['regex', '/$1/'] - notContains: ['regex', '/^((?!$1).)*$/'] - pattern: ['regex', '/$1/'] + minLength: min + # Replace $1 with specified `startsWith` argument value of the constraint directive + startsWith: ["regex", "/^$1/", "message"] format: - # For example, `@constraint(format: "uri")`. this case $1 will be "uri". - # Therefore the generator generates yup schema `.url()` followed by `uri: 'url'` - # If $1 does not match anywhere, the generator will ignore. - uri: url email: email - uuid: uuid - # yup does not have `ipv4` API. If you want to add this, - # you need to add the logic using `yup.addMethod`. - # see: https://github.com/jquense/yup#addmethodschematype-schema-name-string-method--schema-void - ipv4: ipv4 - min: ['min', '$1 - 1'] - max: ['max', '$1 + 1'] - exclusiveMin: min - exclusiveMax: max diff --git a/example/zod/schemas.ts b/example/zod/schemas.ts index 56f20233..842c45e8 100644 --- a/example/zod/schemas.ts +++ b/example/zod/schemas.ts @@ -36,7 +36,7 @@ export function DropDownComponentInputSchema(): z.ZodSchema { return z.object({ name: z.string().min(5), - value: z.string().regex(/^foo/) + value: z.string().regex(/^foo/, "message") }) } From 6e8cddd79b49ea1fa22f28af51b313f5ff564ee6 Mon Sep 17 00:00:00 2001 From: Code-Hex Date: Sun, 30 Jan 2022 14:23:16 +0000 Subject: [PATCH 10/10] Apply auto lint-fix changes --- codegen.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codegen.yml b/codegen.yml index 32129b68..6ccd8a11 100644 --- a/codegen.yml +++ b/codegen.yml @@ -54,6 +54,6 @@ generates: constraint: minLength: min # Replace $1 with specified `startsWith` argument value of the constraint directive - startsWith: ["regex", "/^$1/", "message"] + startsWith: ['regex', '/^$1/', 'message'] format: email: email