Skip to content

Commit 3d336dc

Browse files
committed
added zod
1 parent 4651460 commit 3d336dc

File tree

4 files changed

+183
-3
lines changed

4 files changed

+183
-3
lines changed

src/config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { TypeScriptPluginConfig } from '@graphql-codegen/typescript';
22

3-
export type ValidationSchema = 'yup';
3+
export type ValidationSchema = 'yup' | 'zod';
44

55
export interface DirectiveConfig {
66
[directive: string]: {

src/index.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { ZodSchemaVisitor } from './zod/index';
12
import { transformSchemaAST } from '@graphql-codegen/schema-ast';
23
import { YupSchemaVisitor } from './yup/index';
34
import { ValidationSchemaPluginConfig } from './config';
@@ -10,7 +11,7 @@ export const plugin: PluginFunction<ValidationSchemaPluginConfig, Types.ComplexP
1011
config: ValidationSchemaPluginConfig
1112
): Types.ComplexPluginOutput => {
1213
const { schema: _schema, ast } = transformSchemaAST(schema, config);
13-
const { buildImports, ...visitor } = YupSchemaVisitor(_schema, config);
14+
const { buildImports, initialEmit, ...visitor } = schemaVisitor(_schema, config);
1415

1516
const result = oldVisit(ast, {
1617
leave: visitor,
@@ -22,6 +23,16 @@ export const plugin: PluginFunction<ValidationSchemaPluginConfig, Types.ComplexP
2223

2324
return {
2425
prepend: buildImports(),
25-
content: '\n' + [...generated].join('\n'),
26+
content: '\n' + [
27+
initialEmit(),
28+
...generated,
29+
].join('\n'),
2630
};
2731
};
32+
33+
const schemaVisitor = (schema: GraphQLSchema, config: ValidationSchemaPluginConfig) => {
34+
if (config?.schema === 'zod') {
35+
return ZodSchemaVisitor(schema, config)
36+
}
37+
return YupSchemaVisitor(schema, config)
38+
}

src/yup/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export const YupSchemaVisitor = (schema: GraphQLSchema, config: ValidationSchema
2626
}
2727
return [importYup];
2828
},
29+
initialEmit: (): string => "",
2930
InputObjectTypeDefinition: (node: InputObjectTypeDefinitionNode) => {
3031
const name = tsVisitor.convertName(node.name.value);
3132
importTypes.push(name);

src/zod/index.ts

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
import { isInput, isNonNullType, isListType, isNamedType } from './../graphql';
2+
import { ValidationSchemaPluginConfig } from '../config';
3+
import {
4+
InputValueDefinitionNode,
5+
NameNode,
6+
TypeNode,
7+
GraphQLSchema,
8+
InputObjectTypeDefinitionNode,
9+
EnumTypeDefinitionNode,
10+
} from 'graphql';
11+
import { DeclarationBlock, indent } from '@graphql-codegen/visitor-plugin-common';
12+
import { TsVisitor } from '@graphql-codegen/typescript';
13+
import { buildApi, formatDirectiveConfig } from '../directive';
14+
15+
const importZod = `import { z } from 'zod'`;
16+
const anySchema = `definedNonNullAnySchema`
17+
18+
export const ZodSchemaVisitor = (schema: GraphQLSchema, config: ValidationSchemaPluginConfig) => {
19+
const tsVisitor = new TsVisitor(schema, config);
20+
21+
const importTypes: string[] = [];
22+
23+
return {
24+
buildImports: (): string[] => {
25+
if (config.importFrom && importTypes.length > 0) {
26+
return [importZod, `import { ${importTypes.join(', ')} } from '${config.importFrom}'`];
27+
}
28+
return [importZod];
29+
},
30+
initialEmit: (): string =>
31+
[
32+
// Unfortunately, zod doesn’t provide non-null defined any schema.
33+
// This is a temporary hack until it is fixed.
34+
// see: https://github.com/colinhacks/zod/issues/884
35+
'type definedNonNullAny = {}',
36+
new DeclarationBlock({})
37+
.export()
38+
.asKind('const')
39+
.withName(`isDefinedNonNullAny`)
40+
.withContent(`(v: any): v is definedNonNullAny => v !== undefined && v !== null`).string,
41+
new DeclarationBlock({})
42+
.export()
43+
.asKind('const')
44+
.withName(`${anySchema}: z.ZodSchema<definedNonNullAny>`)
45+
.withContent(`z.any().refine((v) => isDefinedNonNullAny(v))`).string,
46+
].join('\n\n'),
47+
InputObjectTypeDefinition: (node: InputObjectTypeDefinitionNode) => {
48+
const name = tsVisitor.convertName(node.name.value);
49+
importTypes.push(name);
50+
51+
const shape = node.fields
52+
?.map(field => generateInputObjectFieldYupSchema(config, tsVisitor, schema, field, 2))
53+
.join(',\n');
54+
55+
return new DeclarationBlock({})
56+
.export()
57+
.asKind('function')
58+
.withName(`${name}Schema(): z.ZodSchema<${name}>`)
59+
.withBlock([indent(`return z.object({`), shape, indent('})')].join('\n')).string;
60+
},
61+
EnumTypeDefinition: (node: EnumTypeDefinitionNode) => {
62+
const enumname = tsVisitor.convertName(node.name.value);
63+
importTypes.push(enumname);
64+
65+
if (config.enumsAsTypes) {
66+
return new DeclarationBlock({})
67+
.export()
68+
.asKind('const')
69+
.withName(`${enumname}Schema`)
70+
.withContent(`z.enum([${node.values?.map(enumOption => `'${enumOption.name.value}'`).join(', ')}])`).string;
71+
}
72+
73+
return new DeclarationBlock({})
74+
.export()
75+
.asKind('const')
76+
.withName(`${enumname}Schema`)
77+
.withContent(`z.nativeEnum(${enumname})`).string;
78+
},
79+
};
80+
};
81+
82+
const generateInputObjectFieldYupSchema = (
83+
config: ValidationSchemaPluginConfig,
84+
tsVisitor: TsVisitor,
85+
schema: GraphQLSchema,
86+
field: InputValueDefinitionNode,
87+
indentCount: number
88+
): string => {
89+
let gen = generateInputObjectFieldTypeZodSchema(config, tsVisitor, schema, field.type);
90+
if (config.directives && field.directives) {
91+
const formatted = formatDirectiveConfig(config.directives);
92+
gen += buildApi(formatted, field.directives);
93+
}
94+
return indent(`${field.name.value}: ${maybeLazy(field.type, gen)}`, indentCount);
95+
};
96+
97+
const generateInputObjectFieldTypeZodSchema = (
98+
config: ValidationSchemaPluginConfig,
99+
tsVisitor: TsVisitor,
100+
schema: GraphQLSchema,
101+
type: TypeNode,
102+
parentType?: TypeNode
103+
): string => {
104+
if (isListType(type)) {
105+
const gen = generateInputObjectFieldTypeZodSchema(config, tsVisitor, schema, type.type, type);
106+
if (!isNonNullType(parentType)) {
107+
return `z.array(${maybeLazy(type.type, gen)}).nullish()`;
108+
}
109+
return `z.array(${maybeLazy(type.type, gen)})`;
110+
}
111+
if (isNonNullType(type)) {
112+
const gen = generateInputObjectFieldTypeZodSchema(config, tsVisitor, schema, type.type, type);
113+
return maybeLazy(type.type, gen);
114+
}
115+
if (isNamedType(type)) {
116+
const gen = generateNameNodeZodSchema(tsVisitor, schema, type.name);
117+
if (isNonNullType(parentType)) {
118+
return gen
119+
}
120+
if (isListType(parentType)) {
121+
return `${gen}.nullable()`
122+
}
123+
return `${gen}.nullish()`
124+
}
125+
console.warn('unhandled type:', type);
126+
return '';
127+
};
128+
129+
const generateNameNodeZodSchema = (
130+
tsVisitor: TsVisitor,
131+
schema: GraphQLSchema,
132+
node: NameNode
133+
): string => {
134+
const typ = schema.getType(node.value);
135+
136+
if (typ && typ.astNode?.kind === 'InputObjectTypeDefinition') {
137+
const enumName = tsVisitor.convertName(typ.astNode.name.value);
138+
return `${enumName}Schema()`;
139+
}
140+
141+
if (typ && typ.astNode?.kind === 'EnumTypeDefinition') {
142+
const enumName = tsVisitor.convertName(typ.astNode.name.value);
143+
return `${enumName}Schema`;
144+
}
145+
146+
return zod4Scalar(tsVisitor, node.value);
147+
};
148+
149+
const maybeLazy = (type: TypeNode, schema: string): string => {
150+
if (isNamedType(type) && isInput(type.name.value)) {
151+
return `z.lazy(() => ${schema})`;
152+
}
153+
return schema;
154+
};
155+
156+
const zod4Scalar = (tsVisitor: TsVisitor, scalarName: string): string => {
157+
const tsType = tsVisitor.scalars[scalarName];
158+
switch (tsType) {
159+
case 'string':
160+
return `z.string()`;
161+
case 'number':
162+
return `z.number()`;
163+
case 'boolean':
164+
return `z.boolean()`;
165+
}
166+
console.warn('unhandled name:', scalarName);
167+
return anySchema;
168+
};

0 commit comments

Comments
 (0)