Skip to content

Commit 163658b

Browse files
authored
Merge pull request #27 from Parables/v0.4.1-feature-myzod
V0.4.1 feature myzod
2 parents 7380f4b + 1ca4334 commit 163658b

File tree

7 files changed

+592
-3
lines changed

7 files changed

+592
-3
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/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' | 'zod';
3+
export type ValidationSchema = 'yup' | 'zod' | 'myzod';
44

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

src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { ZodSchemaVisitor } from './zod/index';
2+
import { MyZodSchemaVisitor } from './myzod/index';
23
import { transformSchemaAST } from '@graphql-codegen/schema-ast';
34
import { YupSchemaVisitor } from './yup/index';
45
import { ValidationSchemaPluginConfig } from './config';
@@ -30,6 +31,8 @@ export const plugin: PluginFunction<ValidationSchemaPluginConfig, Types.ComplexP
3031
const schemaVisitor = (schema: GraphQLSchema, config: ValidationSchemaPluginConfig) => {
3132
if (config?.schema === 'zod') {
3233
return ZodSchemaVisitor(schema, config);
34+
} else if (config?.schema === 'myzod') {
35+
return MyZodSchemaVisitor(schema, config);
3336
}
3437
return YupSchemaVisitor(schema, config);
3538
};

src/myzod/index.ts

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

src/zod/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export const ZodSchemaVisitor = (schema: GraphQLSchema, config: ValidationSchema
5454
importTypes.push(name);
5555

5656
const shape = node.fields
57-
?.map(field => generateInputObjectFieldYupSchema(config, tsVisitor, schema, field, 2))
57+
?.map(field => generateInputObjectFieldZodSchema(config, tsVisitor, schema, field, 2))
5858
.join(',\n');
5959

6060
return new DeclarationBlock({})
@@ -84,7 +84,7 @@ export const ZodSchemaVisitor = (schema: GraphQLSchema, config: ValidationSchema
8484
};
8585
};
8686

87-
const generateInputObjectFieldYupSchema = (
87+
const generateInputObjectFieldZodSchema = (
8888
config: ValidationSchemaPluginConfig,
8989
tsVisitor: TsVisitor,
9090
schema: GraphQLSchema,

0 commit comments

Comments
 (0)