Skip to content

Commit 7f078b0

Browse files
committed
feat(zod): support validationSchemaExportType
1 parent 2dde904 commit 7f078b0

File tree

2 files changed

+160
-50
lines changed

2 files changed

+160
-50
lines changed

src/zod/index.ts

Lines changed: 76 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const anySchema = `definedNonNullAnySchema`;
2121

2222
export const ZodSchemaVisitor = (schema: GraphQLSchema, config: ValidationSchemaPluginConfig): SchemaVisitor => {
2323
const importTypes: string[] = [];
24+
const enumDeclarations: string[] = [];
2425

2526
return {
2627
buildImports: (): string[] => {
@@ -53,6 +54,7 @@ export const ZodSchemaVisitor = (schema: GraphQLSchema, config: ValidationSchema
5354
.asKind('const')
5455
.withName(`${anySchema}`)
5556
.withContent(`z.any().refine((v) => isDefinedNonNullAny(v))`).string,
57+
...enumDeclarations,
5658
].join('\n'),
5759
InputObjectTypeDefinition: {
5860
leave: (node: InputObjectTypeDefinitionNode) => {
@@ -62,11 +64,22 @@ export const ZodSchemaVisitor = (schema: GraphQLSchema, config: ValidationSchema
6264

6365
const shape = node.fields?.map(field => generateFieldZodSchema(config, visitor, field, 2)).join(',\n');
6466

65-
return new DeclarationBlock({})
66-
.export()
67-
.asKind('function')
68-
.withName(`${name}Schema(): z.ZodObject<Properties<${name}>>`)
69-
.withBlock([indent(`return z.object<Properties<${name}>>({`), shape, indent('})')].join('\n')).string;
67+
switch (config.validationSchemaExportType) {
68+
case 'const':
69+
return new DeclarationBlock({})
70+
.export()
71+
.asKind('const')
72+
.withName(`${name}Schema: z.ZodObject<Properties<${name}>>`)
73+
.withContent(['z.object({', shape, '})'].join('\n')).string;
74+
75+
case 'function':
76+
default:
77+
return new DeclarationBlock({})
78+
.export()
79+
.asKind('function')
80+
.withName(`${name}Schema(): z.ZodObject<Properties<${name}>>`)
81+
.withBlock([indent(`return z.object({`), shape, indent('})')].join('\n')).string;
82+
}
7083
},
7184
},
7285
ObjectTypeDefinition: {
@@ -77,18 +90,33 @@ export const ZodSchemaVisitor = (schema: GraphQLSchema, config: ValidationSchema
7790

7891
const shape = node.fields?.map(field => generateFieldZodSchema(config, visitor, field, 2)).join(',\n');
7992

80-
return new DeclarationBlock({})
81-
.export()
82-
.asKind('function')
83-
.withName(`${name}Schema(): z.ZodObject<Properties<${name}>>`)
84-
.withBlock(
85-
[
86-
indent(`return z.object<Properties<${name}>>({`),
87-
indent(`__typename: z.literal('${node.name.value}').optional(),`, 2),
88-
shape,
89-
indent('})'),
90-
].join('\n')
91-
).string;
93+
switch (config.validationSchemaExportType) {
94+
case 'const':
95+
return new DeclarationBlock({})
96+
.export()
97+
.asKind('const')
98+
.withName(`${name}Schema: z.ZodObject<Properties<${name}>>`)
99+
.withContent(
100+
[`z.object({`, indent(`__typename: z.literal('${node.name.value}').optional(),`, 2), shape, '})'].join(
101+
'\n'
102+
)
103+
).string;
104+
105+
case 'function':
106+
default:
107+
return new DeclarationBlock({})
108+
.export()
109+
.asKind('function')
110+
.withName(`${name}Schema(): z.ZodObject<Properties<${name}>>`)
111+
.withBlock(
112+
[
113+
indent(`return z.object({`),
114+
indent(`__typename: z.literal('${node.name.value}').optional(),`, 2),
115+
shape,
116+
indent('})'),
117+
].join('\n')
118+
).string;
119+
}
92120
}),
93121
},
94122
EnumTypeDefinition: {
@@ -97,19 +125,21 @@ export const ZodSchemaVisitor = (schema: GraphQLSchema, config: ValidationSchema
97125
const enumname = visitor.convertName(node.name.value);
98126
importTypes.push(enumname);
99127

100-
if (config.enumsAsTypes) {
101-
return new DeclarationBlock({})
102-
.export()
103-
.asKind('const')
104-
.withName(`${enumname}Schema`)
105-
.withContent(`z.enum([${node.values?.map(enumOption => `'${enumOption.name.value}'`).join(', ')}])`).string;
106-
}
107-
108-
return new DeclarationBlock({})
109-
.export()
110-
.asKind('const')
111-
.withName(`${enumname}Schema`)
112-
.withContent(`z.nativeEnum(${enumname})`).string;
128+
// hoist enum declarations
129+
enumDeclarations.push(
130+
config.enumsAsTypes
131+
? new DeclarationBlock({})
132+
.export()
133+
.asKind('const')
134+
.withName(`${enumname}Schema`)
135+
.withContent(`z.enum([${node.values?.map(enumOption => `'${enumOption.name.value}'`).join(', ')}])`)
136+
.string
137+
: new DeclarationBlock({})
138+
.export()
139+
.asKind('const')
140+
.withName(`${enumname}Schema`)
141+
.withContent(`z.nativeEnum(${enumname})`).string
142+
);
113143
},
114144
},
115145
UnionTypeDefinition: {
@@ -206,27 +236,23 @@ const applyDirectives = (
206236
const generateNameNodeZodSchema = (config: ValidationSchemaPluginConfig, visitor: Visitor, node: NameNode): string => {
207237
const converter = visitor.getNameNodeConverter(node);
208238

209-
if (converter?.targetKind === 'InputObjectTypeDefinition') {
210-
const name = converter.convertName();
211-
return `${name}Schema()`;
212-
}
213-
214-
if (converter?.targetKind === 'ObjectTypeDefinition') {
215-
const name = converter.convertName();
216-
return `${name}Schema()`;
217-
}
218-
219-
if (converter?.targetKind === 'EnumTypeDefinition') {
220-
const name = converter.convertName();
221-
return `${name}Schema`;
222-
}
223-
224-
if (converter?.targetKind === 'UnionTypeDefinition') {
225-
const name = converter.convertName();
226-
return `${name}Schema()`;
239+
switch (converter?.targetKind) {
240+
case 'InputObjectTypeDefinition':
241+
case 'ObjectTypeDefinition':
242+
case 'UnionTypeDefinition':
243+
// using switch-case rather than if-else to allow for future expansion
244+
switch (config.validationSchemaExportType) {
245+
case 'const':
246+
return `${converter.convertName()}Schema`;
247+
case 'function':
248+
default:
249+
return `${converter.convertName()}Schema()`;
250+
}
251+
case 'EnumTypeDefinition':
252+
return `${converter.convertName()}Schema`;
253+
default:
254+
return zod4Scalar(config, visitor, node.value);
227255
}
228-
229-
return zod4Scalar(config, visitor, node.value);
230256
};
231257

232258
const maybeLazy = (type: TypeNode, schema: string): string => {

tests/zod.spec.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -837,4 +837,88 @@ describe('zod', () => {
837837
expect(result.content).toContain(wantContain);
838838
}
839839
});
840+
841+
it('exports as const instead of func', async () => {
842+
const schema = buildSchema(/* GraphQL */ `
843+
input Say {
844+
phrase: String!
845+
}
846+
`);
847+
const result = await plugin(
848+
schema,
849+
[],
850+
{
851+
schema: 'zod',
852+
validationSchemaExportType: 'const',
853+
},
854+
{}
855+
);
856+
expect(result.content).toContain('export const SaySchema: z.ZodObject<Properties<Say>> = z.object({');
857+
});
858+
859+
it('generate both input & type, export as const', async () => {
860+
const schema = buildSchema(/* GraphQL */ `
861+
scalar Date
862+
scalar Email
863+
input UserCreateInput {
864+
name: String!
865+
date: Date!
866+
email: Email!
867+
}
868+
type User {
869+
id: ID!
870+
name: String
871+
age: Int
872+
email: Email
873+
isMember: Boolean
874+
createdAt: Date!
875+
}
876+
type Mutation {
877+
_empty: String
878+
}
879+
type Query {
880+
_empty: String
881+
}
882+
type Subscription {
883+
_empty: String
884+
}
885+
`);
886+
const result = await plugin(
887+
schema,
888+
[],
889+
{
890+
schema: 'zod',
891+
withObjectType: true,
892+
scalarSchemas: {
893+
Date: 'z.date()',
894+
Email: 'z.string().email()',
895+
},
896+
validationSchemaExportType: 'const',
897+
},
898+
{}
899+
);
900+
const wantContains = [
901+
// User Create Input
902+
'export const UserCreateInputSchema: z.ZodObject<Properties<UserCreateInput>> = z.object({',
903+
'name: z.string(),',
904+
'date: z.date(),',
905+
'email: z.string().email()',
906+
// User
907+
'export const UserSchema: z.ZodObject<Properties<User>> = z.object({',
908+
"__typename: z.literal('User').optional()",
909+
'id: z.string(),',
910+
'name: z.string().nullish(),',
911+
'age: z.number().nullish(),',
912+
'isMember: z.boolean().nullish(),',
913+
'email: z.string().email().nullish(),',
914+
'createdAt: z.date()',
915+
];
916+
for (const wantContain of wantContains) {
917+
expect(result.content).toContain(wantContain);
918+
}
919+
920+
for (const wantNotContain of ['Query', 'Mutation', 'Subscription']) {
921+
expect(result.content).not.toContain(wantNotContain);
922+
}
923+
});
840924
});

0 commit comments

Comments
 (0)