Skip to content

Commit 6ab7a59

Browse files
committed
before implementing myzod
1 parent 5d8614b commit 6ab7a59

File tree

3 files changed

+202
-0
lines changed

3 files changed

+202
-0
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
"@typescript-eslint/parser": "^5.10.0",
5353
"eslint": "^8.7.0",
5454
"jest": "^27.4.7",
55+
"myzod": "^1.8.7",
5556
"npm-run-all": "^4.1.5",
5657
"prettier": "2.5.1",
5758
"ts-jest": "^27.1.3",

src/myzod/index.ts

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
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+
'\n' +
32+
[
33+
new DeclarationBlock({})
34+
.asKind('type')
35+
.withName('Properties<T>')
36+
.withContent(['Required<{', ' [K in keyof T]: z.ZodType<T[K], any, T[K]>;', '}>'].join('\n')).string,
37+
// Unfortunately, zod doesn’t provide non-null defined any schema.
38+
// This is a temporary hack until it is fixed.
39+
// see: https://github.com/colinhacks/zod/issues/884
40+
new DeclarationBlock({}).asKind('type').withName('definedNonNullAny').withContent('{}').string,
41+
new DeclarationBlock({})
42+
.export()
43+
.asKind('const')
44+
.withName(`isDefinedNonNullAny`)
45+
.withContent(`(v: any): v is definedNonNullAny => v !== undefined && v !== null`).string,
46+
new DeclarationBlock({})
47+
.export()
48+
.asKind('const')
49+
.withName(`${anySchema}`)
50+
.withContent(`z.any().refine((v) => isDefinedNonNullAny(v))`).string,
51+
].join('\n'),
52+
InputObjectTypeDefinition: (node: InputObjectTypeDefinitionNode) => {
53+
const name = tsVisitor.convertName(node.name.value);
54+
importTypes.push(name);
55+
56+
const shape = node.fields
57+
?.map(field => generateInputObjectFieldZodSchema(config, tsVisitor, schema, field, 2))
58+
.join(',\n');
59+
60+
return new DeclarationBlock({})
61+
.export()
62+
.asKind('function')
63+
.withName(`${name}Schema(): z.ZodObject<Properties<${name}>>`)
64+
.withBlock([indent(`return z.object({`), shape, indent('})')].join('\n')).string;
65+
},
66+
EnumTypeDefinition: (node: EnumTypeDefinitionNode) => {
67+
const enumname = tsVisitor.convertName(node.name.value);
68+
importTypes.push(enumname);
69+
70+
if (config.enumsAsTypes) {
71+
return new DeclarationBlock({})
72+
.export()
73+
.asKind('const')
74+
.withName(`${enumname}Schema`)
75+
.withContent(`z.enum([${node.values?.map(enumOption => `'${enumOption.name.value}'`).join(', ')}])`).string;
76+
}
77+
78+
return new DeclarationBlock({})
79+
.export()
80+
.asKind('const')
81+
.withName(`${enumname}Schema`)
82+
.withContent(`z.nativeEnum(${enumname})`).string;
83+
},
84+
};
85+
};
86+
87+
const generateInputObjectFieldZodSchema = (
88+
config: ValidationSchemaPluginConfig,
89+
tsVisitor: TsVisitor,
90+
schema: GraphQLSchema,
91+
field: InputValueDefinitionNode,
92+
indentCount: number
93+
): string => {
94+
const gen = generateInputObjectFieldTypeZodSchema(config, tsVisitor, schema, field, field.type);
95+
return indent(`${field.name.value}: ${maybeLazy(field.type, gen)}`, indentCount);
96+
};
97+
98+
const generateInputObjectFieldTypeZodSchema = (
99+
config: ValidationSchemaPluginConfig,
100+
tsVisitor: TsVisitor,
101+
schema: GraphQLSchema,
102+
field: InputValueDefinitionNode,
103+
type: TypeNode,
104+
parentType?: TypeNode
105+
): string => {
106+
if (isListType(type)) {
107+
const gen = generateInputObjectFieldTypeZodSchema(config, tsVisitor, schema, field, type.type, type);
108+
if (!isNonNullType(parentType)) {
109+
const arrayGen = `z.array(${maybeLazy(type.type, gen)})`;
110+
const maybeLazyGen = applyDirectives(config, field, arrayGen);
111+
return `${maybeLazyGen}.nullish()`;
112+
}
113+
return `z.array(${maybeLazy(type.type, gen)})`;
114+
}
115+
if (isNonNullType(type)) {
116+
const gen = generateInputObjectFieldTypeZodSchema(config, tsVisitor, schema, field, type.type, type);
117+
return maybeLazy(type.type, gen);
118+
}
119+
if (isNamedType(type)) {
120+
const gen = generateNameNodeZodSchema(config, tsVisitor, schema, type.name);
121+
if (isListType(parentType)) {
122+
return `${gen}.nullable()`;
123+
}
124+
const appliedDirectivesGen = applyDirectives(config, field, gen);
125+
if (isNonNullType(parentType)) {
126+
if (config.notAllowEmptyString === true) {
127+
const tsType = tsVisitor.scalars[type.name.value];
128+
if (tsType === 'string') return `${gen}.min(1)`;
129+
}
130+
return appliedDirectivesGen;
131+
}
132+
if (isListType(parentType)) {
133+
return `${appliedDirectivesGen}.nullable()`;
134+
}
135+
return `${appliedDirectivesGen}.nullish()`;
136+
}
137+
console.warn('unhandled type:', type);
138+
return '';
139+
};
140+
141+
const applyDirectives = (
142+
config: ValidationSchemaPluginConfig,
143+
field: InputValueDefinitionNode,
144+
gen: string
145+
): string => {
146+
if (config.directives && field.directives) {
147+
const formatted = formatDirectiveConfig(config.directives);
148+
return gen + buildApi(formatted, field.directives);
149+
}
150+
return gen;
151+
};
152+
153+
const generateNameNodeZodSchema = (
154+
config: ValidationSchemaPluginConfig,
155+
tsVisitor: TsVisitor,
156+
schema: GraphQLSchema,
157+
node: NameNode
158+
): string => {
159+
const typ = schema.getType(node.value);
160+
161+
if (typ && typ.astNode?.kind === 'InputObjectTypeDefinition') {
162+
const enumName = tsVisitor.convertName(typ.astNode.name.value);
163+
return `${enumName}Schema()`;
164+
}
165+
166+
if (typ && typ.astNode?.kind === 'EnumTypeDefinition') {
167+
const enumName = tsVisitor.convertName(typ.astNode.name.value);
168+
return `${enumName}Schema`;
169+
}
170+
171+
return zod4Scalar(config, tsVisitor, node.value);
172+
};
173+
174+
const maybeLazy = (type: TypeNode, schema: string): string => {
175+
if (isNamedType(type) && isInput(type.name.value)) {
176+
return `z.lazy(() => ${schema})`;
177+
}
178+
return schema;
179+
};
180+
181+
const zod4Scalar = (config: ValidationSchemaPluginConfig, tsVisitor: TsVisitor, scalarName: string): string => {
182+
if (config.scalarSchemas?.[scalarName]) {
183+
return config.scalarSchemas[scalarName];
184+
}
185+
const tsType = tsVisitor.scalars[scalarName];
186+
switch (tsType) {
187+
case 'string':
188+
return `z.string()`;
189+
case 'number':
190+
return `z.number()`;
191+
case 'boolean':
192+
return `z.boolean()`;
193+
}
194+
console.warn('unhandled name:', scalarName);
195+
return anySchema;
196+
};

yarn.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4321,6 +4321,11 @@ [email protected]:
43214321
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d"
43224322
integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==
43234323

4324+
myzod@^1.8.7:
4325+
version "1.8.7"
4326+
resolved "https://registry.yarnpkg.com/myzod/-/myzod-1.8.7.tgz#f7e1d5379b517de039729735a8578a550c562292"
4327+
integrity sha512-H/Nmst+ZIGQppKVeOq6ufieRnnK0u+UfDLgCrG1Rtn6W/GzMoz6Ur9/iLBwB0N8s6ZE/4hhbRTUt3Pg7nH4X2Q==
4328+
43244329
nanoclone@^0.2.1:
43254330
version "0.2.1"
43264331
resolved "https://registry.yarnpkg.com/nanoclone/-/nanoclone-0.2.1.tgz#dd4090f8f1a110d26bb32c49ed2f5b9235209ed4"

0 commit comments

Comments
 (0)