@davars/graphql-codegen-zod 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +83 -0
- package/dist/config.d.ts +363 -0
- package/dist/config.js +1 -0
- package/dist/directive.d.ts +24 -0
- package/dist/directive.js +211 -0
- package/dist/graphql.d.ts +21 -0
- package/dist/graphql.js +167 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +37 -0
- package/dist/regexp.d.ts +1 -0
- package/dist/regexp.js +2 -0
- package/dist/schema_visitor.d.ts +17 -0
- package/dist/schema_visitor.js +33 -0
- package/dist/transform_config.d.ts +20 -0
- package/dist/transform_config.js +1 -0
- package/dist/transforms.d.ts +35 -0
- package/dist/transforms.js +137 -0
- package/dist/types.d.ts +10 -0
- package/dist/types.js +1 -0
- package/dist/visitor.d.ts +19 -0
- package/dist/visitor.js +68 -0
- package/dist/zod.d.ts +32 -0
- package/dist/zod.js +368 -0
- package/package.json +66 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import ts from 'typescript';
|
|
4
|
+
/**
|
|
5
|
+
* Loads and resolves transform configuration using pure static analysis.
|
|
6
|
+
*
|
|
7
|
+
* Parses the TypeScript config file's AST to extract:
|
|
8
|
+
* 1. Import declarations → symbol name to module path mapping
|
|
9
|
+
* 2. Default export object → GraphQL type name to symbol name mapping
|
|
10
|
+
* 3. Rebased import paths → adjusted for the output file location
|
|
11
|
+
*/
|
|
12
|
+
export function loadTransforms(configPath, outputPath) {
|
|
13
|
+
const absoluteConfigPath = path.resolve(configPath);
|
|
14
|
+
const configSource = fs.readFileSync(absoluteConfigPath, 'utf-8');
|
|
15
|
+
const sourceFile = ts.createSourceFile(absoluteConfigPath, configSource, ts.ScriptTarget.Latest, true);
|
|
16
|
+
// Step 1: symbol name → module path (from import declarations)
|
|
17
|
+
const importMap = extractImportMap(sourceFile);
|
|
18
|
+
// Step 2: type name → symbol name (from default export object)
|
|
19
|
+
const typeMap = extractDefaultExportMap(sourceFile);
|
|
20
|
+
// Step 3: combine and rebase paths
|
|
21
|
+
const result = {};
|
|
22
|
+
for (const [typeName, symbolName] of typeMap) {
|
|
23
|
+
const importPath = importMap.get(symbolName);
|
|
24
|
+
if (!importPath) {
|
|
25
|
+
throw new Error(`Transform for '${typeName}': could not resolve import path for '${symbolName}'. `
|
|
26
|
+
+ `Ensure it is a named function imported in the config file.`);
|
|
27
|
+
}
|
|
28
|
+
result[typeName] = {
|
|
29
|
+
symbolName,
|
|
30
|
+
importPath: rebasePath(importPath, absoluteConfigPath, outputPath),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Extracts a map of imported symbol names to their module paths.
|
|
37
|
+
* Skips `import type` declarations since we need value imports.
|
|
38
|
+
*/
|
|
39
|
+
export function extractImportMap(sourceFile) {
|
|
40
|
+
const map = new Map();
|
|
41
|
+
for (const stmt of sourceFile.statements) {
|
|
42
|
+
if (!ts.isImportDeclaration(stmt) || !stmt.importClause?.namedBindings)
|
|
43
|
+
continue;
|
|
44
|
+
// Skip `import type` — we only care about value imports
|
|
45
|
+
if (stmt.importClause.isTypeOnly)
|
|
46
|
+
continue;
|
|
47
|
+
const modulePath = stmt.moduleSpecifier.text;
|
|
48
|
+
if (ts.isNamedImports(stmt.importClause.namedBindings)) {
|
|
49
|
+
for (const element of stmt.importClause.namedBindings.elements) {
|
|
50
|
+
if (!element.isTypeOnly) {
|
|
51
|
+
map.set(element.name.text, modulePath);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return map;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Extracts the default export object literal, mapping property names to
|
|
60
|
+
* their identifier values (the imported symbol names).
|
|
61
|
+
*
|
|
62
|
+
* Supports these patterns:
|
|
63
|
+
* export default { TypeName: transformFn }
|
|
64
|
+
* const config = { TypeName: transformFn }; export default config;
|
|
65
|
+
*/
|
|
66
|
+
export function extractDefaultExportMap(sourceFile) {
|
|
67
|
+
const map = new Map();
|
|
68
|
+
// Collect top-level variable declarations for resolving `export default config`
|
|
69
|
+
const varDecls = new Map();
|
|
70
|
+
for (const stmt of sourceFile.statements) {
|
|
71
|
+
if (ts.isVariableStatement(stmt)) {
|
|
72
|
+
for (const decl of stmt.declarationList.declarations) {
|
|
73
|
+
if (ts.isIdentifier(decl.name) && decl.initializer) {
|
|
74
|
+
varDecls.set(decl.name.text, decl.initializer);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
for (const stmt of sourceFile.statements) {
|
|
80
|
+
if (!ts.isExportAssignment(stmt) || stmt.isExportEquals)
|
|
81
|
+
continue;
|
|
82
|
+
let expr = stmt.expression;
|
|
83
|
+
// Handle `export default config` where config is a variable
|
|
84
|
+
if (ts.isIdentifier(expr)) {
|
|
85
|
+
const resolved = varDecls.get(expr.text);
|
|
86
|
+
if (resolved) {
|
|
87
|
+
expr = resolved;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// Handle satisfies: `export default { ... } satisfies TransformConfig`
|
|
91
|
+
if (ts.isSatisfiesExpression(expr)) {
|
|
92
|
+
expr = expr.expression;
|
|
93
|
+
}
|
|
94
|
+
// Handle `as` cast: `export default { ... } as TransformConfig`
|
|
95
|
+
if (ts.isAsExpression(expr)) {
|
|
96
|
+
expr = expr.expression;
|
|
97
|
+
}
|
|
98
|
+
if (ts.isObjectLiteralExpression(expr)) {
|
|
99
|
+
extractObjectProperties(expr, map);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return map;
|
|
103
|
+
}
|
|
104
|
+
function extractObjectProperties(obj, map) {
|
|
105
|
+
for (const prop of obj.properties) {
|
|
106
|
+
if (!ts.isPropertyAssignment(prop))
|
|
107
|
+
continue;
|
|
108
|
+
const key = ts.isIdentifier(prop.name)
|
|
109
|
+
? prop.name.text
|
|
110
|
+
: ts.isStringLiteral(prop.name)
|
|
111
|
+
? prop.name.text
|
|
112
|
+
: undefined;
|
|
113
|
+
if (!key)
|
|
114
|
+
continue;
|
|
115
|
+
if (ts.isIdentifier(prop.initializer)) {
|
|
116
|
+
map.set(key, prop.initializer.text);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
throw new Error(`Transform for '${key}' must be an imported named function, not an inline definition. `
|
|
120
|
+
+ `Import the function and reference it by name.`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Adjusts a relative import path so it's correct relative to the output file
|
|
126
|
+
* rather than the config file. Non-relative paths (aliases, packages) pass through unchanged.
|
|
127
|
+
*/
|
|
128
|
+
export function rebasePath(importPath, configPath, outputPath) {
|
|
129
|
+
// Only rebase relative paths — leave aliases and package imports as-is
|
|
130
|
+
if (!importPath.startsWith('.'))
|
|
131
|
+
return importPath;
|
|
132
|
+
const configDir = path.dirname(path.resolve(configPath));
|
|
133
|
+
const outputDir = path.dirname(path.resolve(outputPath));
|
|
134
|
+
const absoluteTarget = path.resolve(configDir, importPath);
|
|
135
|
+
const rebased = path.relative(outputDir, absoluteTarget);
|
|
136
|
+
return rebased.startsWith('.') ? rebased : `./${rebased}`;
|
|
137
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ASTNode, ASTVisitFn } from 'graphql';
|
|
2
|
+
export type NewVisitor = Partial<{
|
|
3
|
+
readonly [NodeT in ASTNode as NodeT['kind']]?: {
|
|
4
|
+
leave?: ASTVisitFn<NodeT>;
|
|
5
|
+
};
|
|
6
|
+
}>;
|
|
7
|
+
export interface SchemaVisitor extends NewVisitor {
|
|
8
|
+
buildImports: () => string[];
|
|
9
|
+
initialEmit: () => string;
|
|
10
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { FieldDefinitionNode, GraphQLSchema, InterfaceTypeDefinitionNode, NameNode, ObjectTypeDefinitionNode } from 'graphql';
|
|
2
|
+
import type { ValidationSchemaPluginConfig } from './config.js';
|
|
3
|
+
import { TsVisitor } from '@graphql-codegen/typescript';
|
|
4
|
+
export declare class Visitor extends TsVisitor {
|
|
5
|
+
private scalarDirection;
|
|
6
|
+
private schema;
|
|
7
|
+
private pluginConfig;
|
|
8
|
+
constructor(scalarDirection: 'input' | 'output' | 'both', schema: GraphQLSchema, pluginConfig: ValidationSchemaPluginConfig);
|
|
9
|
+
prefixTypeNamespace(type: string): string;
|
|
10
|
+
private isSpecifiedScalarName;
|
|
11
|
+
getType(name: string): import("graphql").GraphQLNamedType | undefined;
|
|
12
|
+
getNameNodeConverter(node: NameNode): {
|
|
13
|
+
targetKind: import("graphql").Kind.SCALAR_TYPE_DEFINITION | import("graphql").Kind.OBJECT_TYPE_DEFINITION | import("graphql").Kind.INTERFACE_TYPE_DEFINITION | import("graphql").Kind.UNION_TYPE_DEFINITION | import("graphql").Kind.ENUM_TYPE_DEFINITION | import("graphql").Kind.INPUT_OBJECT_TYPE_DEFINITION;
|
|
14
|
+
convertName: () => string;
|
|
15
|
+
} | undefined;
|
|
16
|
+
getScalarType(scalarName: string): string | null;
|
|
17
|
+
shouldEmitAsNotAllowEmptyString(name: string): boolean;
|
|
18
|
+
buildArgumentsSchemaBlock(node: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode, callback: (typeName: string, field: FieldDefinitionNode) => string): string | undefined;
|
|
19
|
+
}
|
package/dist/visitor.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { TsVisitor } from '@graphql-codegen/typescript';
|
|
2
|
+
import { specifiedScalarTypes, } from 'graphql';
|
|
3
|
+
export class Visitor extends TsVisitor {
|
|
4
|
+
scalarDirection;
|
|
5
|
+
schema;
|
|
6
|
+
pluginConfig;
|
|
7
|
+
constructor(scalarDirection, schema, pluginConfig) {
|
|
8
|
+
super(schema, pluginConfig);
|
|
9
|
+
this.scalarDirection = scalarDirection;
|
|
10
|
+
this.schema = schema;
|
|
11
|
+
this.pluginConfig = pluginConfig;
|
|
12
|
+
}
|
|
13
|
+
prefixTypeNamespace(type) {
|
|
14
|
+
if (this.pluginConfig.importFrom && this.pluginConfig.schemaNamespacedImportName) {
|
|
15
|
+
return `${this.pluginConfig.schemaNamespacedImportName}.${type}`;
|
|
16
|
+
}
|
|
17
|
+
return type;
|
|
18
|
+
}
|
|
19
|
+
isSpecifiedScalarName(scalarName) {
|
|
20
|
+
return specifiedScalarTypes.some(({ name }) => name === scalarName);
|
|
21
|
+
}
|
|
22
|
+
getType(name) {
|
|
23
|
+
return this.schema.getType(name);
|
|
24
|
+
}
|
|
25
|
+
getNameNodeConverter(node) {
|
|
26
|
+
const typ = this.schema.getType(node.value);
|
|
27
|
+
const astNode = typ?.astNode;
|
|
28
|
+
if (astNode === undefined || astNode === null)
|
|
29
|
+
return undefined;
|
|
30
|
+
return {
|
|
31
|
+
targetKind: astNode.kind,
|
|
32
|
+
convertName: () => this.convertName(astNode.name.value),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
getScalarType(scalarName) {
|
|
36
|
+
if (this.scalarDirection === 'both')
|
|
37
|
+
return null;
|
|
38
|
+
const scalar = this.scalars[scalarName];
|
|
39
|
+
if (!scalar)
|
|
40
|
+
throw new Error(`Unknown scalar ${scalarName}`);
|
|
41
|
+
return scalar[this.scalarDirection];
|
|
42
|
+
}
|
|
43
|
+
shouldEmitAsNotAllowEmptyString(name) {
|
|
44
|
+
if (this.pluginConfig.notAllowEmptyString !== true)
|
|
45
|
+
return false;
|
|
46
|
+
const typ = this.getType(name);
|
|
47
|
+
if (typ?.astNode?.kind !== 'ScalarTypeDefinition' && !this.isSpecifiedScalarName(name))
|
|
48
|
+
return false;
|
|
49
|
+
const tsType = this.getScalarType(name);
|
|
50
|
+
return tsType === 'string';
|
|
51
|
+
}
|
|
52
|
+
buildArgumentsSchemaBlock(node, callback) {
|
|
53
|
+
const fieldsWithArguments = node.fields?.filter(field => field.arguments && field.arguments.length > 0) ?? [];
|
|
54
|
+
if (fieldsWithArguments.length === 0)
|
|
55
|
+
return undefined;
|
|
56
|
+
return fieldsWithArguments
|
|
57
|
+
.map((field) => {
|
|
58
|
+
const name = `${this.convertName(node.name.value)
|
|
59
|
+
+ (this.config.addUnderscoreToArgsType ? '_' : '')
|
|
60
|
+
+ this.convertName(field, {
|
|
61
|
+
useTypesPrefix: false,
|
|
62
|
+
useTypesSuffix: false,
|
|
63
|
+
})}Args`;
|
|
64
|
+
return callback(name, field);
|
|
65
|
+
})
|
|
66
|
+
.join('\n');
|
|
67
|
+
}
|
|
68
|
+
}
|
package/dist/zod.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { EnumTypeDefinitionNode, FieldDefinitionNode, GraphQLSchema, InputObjectTypeDefinitionNode, InputValueDefinitionNode, InterfaceTypeDefinitionNode, ObjectTypeDefinitionNode, UnionTypeDefinitionNode } from 'graphql';
|
|
2
|
+
import type { ValidationSchemaPluginConfig } from './config.js';
|
|
3
|
+
import type { ResolvedTransform } from './transforms.js';
|
|
4
|
+
import type { Visitor } from './visitor.js';
|
|
5
|
+
import { BaseSchemaVisitor } from './schema_visitor.js';
|
|
6
|
+
export declare class ZodSchemaVisitor extends BaseSchemaVisitor {
|
|
7
|
+
private resolvedTransforms;
|
|
8
|
+
private usedTransformImports;
|
|
9
|
+
constructor(schema: GraphQLSchema, config: ValidationSchemaPluginConfig, resolvedTransforms?: Record<string, ResolvedTransform>);
|
|
10
|
+
private addTransformImport;
|
|
11
|
+
buildImports(): string[];
|
|
12
|
+
private getTransformForGraphQLName;
|
|
13
|
+
private hasTransformedFieldRef;
|
|
14
|
+
importValidationSchema(): string;
|
|
15
|
+
initialEmit(): string;
|
|
16
|
+
get InputObjectTypeDefinition(): {
|
|
17
|
+
leave: (node: InputObjectTypeDefinitionNode) => string;
|
|
18
|
+
};
|
|
19
|
+
get InterfaceTypeDefinition(): {
|
|
20
|
+
leave: ((node: InterfaceTypeDefinitionNode) => any) | undefined;
|
|
21
|
+
};
|
|
22
|
+
get ObjectTypeDefinition(): {
|
|
23
|
+
leave: ((node: ObjectTypeDefinitionNode) => any) | undefined;
|
|
24
|
+
};
|
|
25
|
+
get EnumTypeDefinition(): {
|
|
26
|
+
leave: (node: EnumTypeDefinitionNode) => void;
|
|
27
|
+
};
|
|
28
|
+
get UnionTypeDefinition(): {
|
|
29
|
+
leave: (node: UnionTypeDefinitionNode) => string | undefined;
|
|
30
|
+
};
|
|
31
|
+
protected buildInputFields(fields: readonly (FieldDefinitionNode | InputValueDefinitionNode)[], visitor: Visitor, name: string, graphqlName?: string): string;
|
|
32
|
+
}
|
package/dist/zod.js
ADDED
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import { resolveExternalModuleAndFn } from '@graphql-codegen/plugin-helpers';
|
|
2
|
+
import { convertNameParts, DeclarationBlock, indent } from '@graphql-codegen/visitor-plugin-common';
|
|
3
|
+
import { isEnumType, isScalarType, Kind, } from 'graphql';
|
|
4
|
+
import { buildApi, formatDirectiveConfig } from './directive.js';
|
|
5
|
+
import { escapeGraphQLCharacters, InterfaceTypeDefinitionBuilder, isListType, isNamedType, isNonNullType, ObjectTypeDefinitionBuilder, } from './graphql.js';
|
|
6
|
+
import { BaseSchemaVisitor } from './schema_visitor.js';
|
|
7
|
+
const anySchema = `definedNonNullAnySchema`;
|
|
8
|
+
export class ZodSchemaVisitor extends BaseSchemaVisitor {
|
|
9
|
+
resolvedTransforms;
|
|
10
|
+
usedTransformImports = new Map();
|
|
11
|
+
constructor(schema, config, resolvedTransforms = {}) {
|
|
12
|
+
super(schema, config);
|
|
13
|
+
this.resolvedTransforms = resolvedTransforms;
|
|
14
|
+
}
|
|
15
|
+
addTransformImport(transform) {
|
|
16
|
+
const existing = this.usedTransformImports.get(transform.importPath) ?? new Set();
|
|
17
|
+
existing.add(transform.symbolName);
|
|
18
|
+
this.usedTransformImports.set(transform.importPath, existing);
|
|
19
|
+
}
|
|
20
|
+
buildImports() {
|
|
21
|
+
const baseImports = super.buildImports();
|
|
22
|
+
const transformImports = Array.from(this.usedTransformImports.entries())
|
|
23
|
+
.map(([modulePath, symbols]) => `import { ${Array.from(symbols).sort().join(', ')} } from '${modulePath}'`);
|
|
24
|
+
return [...baseImports, ...transformImports];
|
|
25
|
+
}
|
|
26
|
+
getTransformForGraphQLName(graphqlName) {
|
|
27
|
+
return this.resolvedTransforms[graphqlName];
|
|
28
|
+
}
|
|
29
|
+
hasTransformedFieldRef(fields) {
|
|
30
|
+
if (!fields)
|
|
31
|
+
return false;
|
|
32
|
+
return fields.some((field) => {
|
|
33
|
+
let type = field.type;
|
|
34
|
+
while (type.kind === Kind.NON_NULL_TYPE || type.kind === Kind.LIST_TYPE)
|
|
35
|
+
type = type.type;
|
|
36
|
+
return type.kind === Kind.NAMED_TYPE && this.resolvedTransforms[type.name.value] !== undefined;
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
importValidationSchema() {
|
|
40
|
+
return `import * as z from 'zod'`;
|
|
41
|
+
}
|
|
42
|
+
initialEmit() {
|
|
43
|
+
return (`\n${[
|
|
44
|
+
new DeclarationBlock({})
|
|
45
|
+
.asKind('type')
|
|
46
|
+
.withName('Properties<T>')
|
|
47
|
+
.withContent(['Required<{', ' [K in keyof T]: z.ZodType<T[K], T[K]>;', '}>'].join('\n'))
|
|
48
|
+
.string,
|
|
49
|
+
// Unfortunately, zod doesn't provide non-null defined any schema.
|
|
50
|
+
// This is a temporary hack until it is fixed.
|
|
51
|
+
// see: https://github.com/colinhacks/zod/issues/884
|
|
52
|
+
new DeclarationBlock({}).asKind('type').withName('definedNonNullAny').withContent('{}').string,
|
|
53
|
+
new DeclarationBlock({})
|
|
54
|
+
.export()
|
|
55
|
+
.asKind('const')
|
|
56
|
+
.withName(`isDefinedNonNullAny`)
|
|
57
|
+
.withContent(`(v: any): v is definedNonNullAny => v !== undefined && v !== null`)
|
|
58
|
+
.string,
|
|
59
|
+
new DeclarationBlock({})
|
|
60
|
+
.export()
|
|
61
|
+
.asKind('const')
|
|
62
|
+
.withName(`${anySchema}`)
|
|
63
|
+
.withContent(`z.any().refine((v) => isDefinedNonNullAny(v))`)
|
|
64
|
+
.string,
|
|
65
|
+
...this.enumDeclarations,
|
|
66
|
+
].join('\n')}`);
|
|
67
|
+
}
|
|
68
|
+
get InputObjectTypeDefinition() {
|
|
69
|
+
return {
|
|
70
|
+
leave: (node) => {
|
|
71
|
+
const visitor = this.createVisitor('input');
|
|
72
|
+
const name = visitor.convertName(node.name.value);
|
|
73
|
+
this.importTypes.push(name);
|
|
74
|
+
return this.buildInputFields(node.fields ?? [], visitor, name, node.name.value);
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
get InterfaceTypeDefinition() {
|
|
79
|
+
return {
|
|
80
|
+
leave: InterfaceTypeDefinitionBuilder(this.config.withObjectType, (node) => {
|
|
81
|
+
const visitor = this.createVisitor('output');
|
|
82
|
+
const name = visitor.convertName(node.name.value);
|
|
83
|
+
const typeName = visitor.prefixTypeNamespace(name);
|
|
84
|
+
this.importTypes.push(name);
|
|
85
|
+
const transform = this.getTransformForGraphQLName(node.name.value);
|
|
86
|
+
if (transform) {
|
|
87
|
+
this.addTransformImport(transform);
|
|
88
|
+
}
|
|
89
|
+
// Building schema for field arguments.
|
|
90
|
+
const argumentBlocks = this.buildTypeDefinitionArguments(node, visitor);
|
|
91
|
+
const appendArguments = argumentBlocks ? `\n${argumentBlocks}` : '';
|
|
92
|
+
// Building schema for fields.
|
|
93
|
+
const shape = node.fields?.map(field => generateFieldZodSchema(this.config, visitor, field, 2)).join(',\n');
|
|
94
|
+
const transformSuffix = transform ? `.transform(${transform.symbolName})` : '';
|
|
95
|
+
const dropAnnotation = !!transform || this.hasTransformedFieldRef(node.fields);
|
|
96
|
+
const schemaName = dropAnnotation
|
|
97
|
+
? `${name}Schema`
|
|
98
|
+
: `${name}Schema: z.ZodObject<Properties<${typeName}>>`;
|
|
99
|
+
switch (this.config.validationSchemaExportType) {
|
|
100
|
+
case 'const':
|
|
101
|
+
return (new DeclarationBlock({})
|
|
102
|
+
.export()
|
|
103
|
+
.asKind('const')
|
|
104
|
+
.withName(schemaName)
|
|
105
|
+
.withContent([`z.object({`, shape, `})${transformSuffix}`].join('\n'))
|
|
106
|
+
.string + appendArguments);
|
|
107
|
+
case 'function':
|
|
108
|
+
default:
|
|
109
|
+
return (new DeclarationBlock({})
|
|
110
|
+
.export()
|
|
111
|
+
.asKind('function')
|
|
112
|
+
.withName(dropAnnotation ? `${name}Schema()` : `${name}Schema(): z.ZodObject<Properties<${typeName}>>`)
|
|
113
|
+
.withBlock([indent(`return z.object({`), shape, indent(`})${transformSuffix}`)].join('\n'))
|
|
114
|
+
.string + appendArguments);
|
|
115
|
+
}
|
|
116
|
+
}),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
get ObjectTypeDefinition() {
|
|
120
|
+
return {
|
|
121
|
+
leave: ObjectTypeDefinitionBuilder(this.config.withObjectType, (node) => {
|
|
122
|
+
const visitor = this.createVisitor('output');
|
|
123
|
+
const name = visitor.convertName(node.name.value);
|
|
124
|
+
const typeName = visitor.prefixTypeNamespace(name);
|
|
125
|
+
this.importTypes.push(name);
|
|
126
|
+
const transform = this.getTransformForGraphQLName(node.name.value);
|
|
127
|
+
if (transform) {
|
|
128
|
+
this.addTransformImport(transform);
|
|
129
|
+
}
|
|
130
|
+
// Building schema for field arguments.
|
|
131
|
+
const argumentBlocks = this.buildTypeDefinitionArguments(node, visitor);
|
|
132
|
+
const appendArguments = argumentBlocks ? `\n${argumentBlocks}` : '';
|
|
133
|
+
// Building schema for fields.
|
|
134
|
+
const shape = node.fields?.map(field => generateFieldZodSchema(this.config, visitor, field, 2)).join(',\n');
|
|
135
|
+
const transformSuffix = transform ? `.transform(${transform.symbolName})` : '';
|
|
136
|
+
// Drop the explicit type annotation when a transform is applied (changes
|
|
137
|
+
// return type from ZodObject to ZodPipe) or when any field references a
|
|
138
|
+
// transformed type (input/output types diverge, breaking Properties<T>).
|
|
139
|
+
const dropAnnotation = !!transform || this.hasTransformedFieldRef(node.fields);
|
|
140
|
+
const schemaName = dropAnnotation
|
|
141
|
+
? `${name}Schema`
|
|
142
|
+
: `${name}Schema: z.ZodObject<Properties<${typeName}>>`;
|
|
143
|
+
switch (this.config.validationSchemaExportType) {
|
|
144
|
+
case 'const':
|
|
145
|
+
return (new DeclarationBlock({})
|
|
146
|
+
.export()
|
|
147
|
+
.asKind('const')
|
|
148
|
+
.withName(schemaName)
|
|
149
|
+
.withContent([
|
|
150
|
+
`z.object({`,
|
|
151
|
+
indent(`__typename: z.literal('${node.name.value}').optional(),`, 2),
|
|
152
|
+
shape,
|
|
153
|
+
`})${transformSuffix}`,
|
|
154
|
+
].join('\n'))
|
|
155
|
+
.string + appendArguments);
|
|
156
|
+
case 'function':
|
|
157
|
+
default:
|
|
158
|
+
return (new DeclarationBlock({})
|
|
159
|
+
.export()
|
|
160
|
+
.asKind('function')
|
|
161
|
+
.withName(dropAnnotation ? `${name}Schema()` : `${name}Schema(): z.ZodObject<Properties<${typeName}>>`)
|
|
162
|
+
.withBlock([
|
|
163
|
+
indent(`return z.object({`),
|
|
164
|
+
indent(`__typename: z.literal('${node.name.value}').optional(),`, 2),
|
|
165
|
+
shape,
|
|
166
|
+
indent(`})${transformSuffix}`),
|
|
167
|
+
].join('\n'))
|
|
168
|
+
.string + appendArguments);
|
|
169
|
+
}
|
|
170
|
+
}),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
get EnumTypeDefinition() {
|
|
174
|
+
return {
|
|
175
|
+
leave: (node) => {
|
|
176
|
+
const visitor = this.createVisitor('both');
|
|
177
|
+
const enumname = visitor.convertName(node.name.value);
|
|
178
|
+
const enumTypeName = visitor.prefixTypeNamespace(enumname);
|
|
179
|
+
this.importTypes.push(enumname);
|
|
180
|
+
// hoist enum declarations
|
|
181
|
+
this.enumDeclarations.push(new DeclarationBlock({})
|
|
182
|
+
.export()
|
|
183
|
+
.asKind('const')
|
|
184
|
+
.withName(`${enumname}Schema`)
|
|
185
|
+
.withContent(this.config.enumsAsTypes
|
|
186
|
+
? `z.enum([${node.values?.map(enumOption => `'${enumOption.name.value}'`).join(', ')}])`
|
|
187
|
+
: `z.enum(${enumTypeName})`)
|
|
188
|
+
.string);
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
get UnionTypeDefinition() {
|
|
193
|
+
return {
|
|
194
|
+
leave: (node) => {
|
|
195
|
+
if (!node.types || !this.config.withObjectType)
|
|
196
|
+
return;
|
|
197
|
+
const visitor = this.createVisitor('output');
|
|
198
|
+
const unionName = visitor.convertName(node.name.value);
|
|
199
|
+
const unionElements = node.types.map((t) => {
|
|
200
|
+
const element = visitor.convertName(t.name.value);
|
|
201
|
+
const typ = visitor.getType(t.name.value);
|
|
202
|
+
if (typ?.astNode?.kind === 'EnumTypeDefinition')
|
|
203
|
+
return `${element}Schema`;
|
|
204
|
+
switch (this.config.validationSchemaExportType) {
|
|
205
|
+
case 'const':
|
|
206
|
+
return `${element}Schema`;
|
|
207
|
+
case 'function':
|
|
208
|
+
default:
|
|
209
|
+
return `${element}Schema()`;
|
|
210
|
+
}
|
|
211
|
+
}).join(', ');
|
|
212
|
+
const unionElementsCount = node.types.length ?? 0;
|
|
213
|
+
const union = unionElementsCount > 1 ? `z.union([${unionElements}])` : unionElements;
|
|
214
|
+
switch (this.config.validationSchemaExportType) {
|
|
215
|
+
case 'const':
|
|
216
|
+
return new DeclarationBlock({}).export().asKind('const').withName(`${unionName}Schema`).withContent(union).string;
|
|
217
|
+
case 'function':
|
|
218
|
+
default:
|
|
219
|
+
return new DeclarationBlock({})
|
|
220
|
+
.export()
|
|
221
|
+
.asKind('function')
|
|
222
|
+
.withName(`${unionName}Schema()`)
|
|
223
|
+
.withBlock(indent(`return ${union}`))
|
|
224
|
+
.string;
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
buildInputFields(fields, visitor, name, graphqlName) {
|
|
230
|
+
const typeName = visitor.prefixTypeNamespace(name);
|
|
231
|
+
const shape = fields.map(field => generateFieldZodSchema(this.config, visitor, field, 2)).join(',\n');
|
|
232
|
+
const transform = graphqlName ? this.getTransformForGraphQLName(graphqlName) : undefined;
|
|
233
|
+
if (transform) {
|
|
234
|
+
this.addTransformImport(transform);
|
|
235
|
+
}
|
|
236
|
+
const transformSuffix = transform ? `.transform(${transform.symbolName})` : '';
|
|
237
|
+
const dropAnnotation = !!transform || this.hasTransformedFieldRef(fields);
|
|
238
|
+
const schemaName = dropAnnotation
|
|
239
|
+
? `${name}Schema`
|
|
240
|
+
: `${name}Schema: z.ZodObject<Properties<${typeName}>>`;
|
|
241
|
+
switch (this.config.validationSchemaExportType) {
|
|
242
|
+
case 'const':
|
|
243
|
+
return new DeclarationBlock({})
|
|
244
|
+
.export()
|
|
245
|
+
.asKind('const')
|
|
246
|
+
.withName(schemaName)
|
|
247
|
+
.withContent([`z.object({`, shape, `})${transformSuffix}`].join('\n'))
|
|
248
|
+
.string;
|
|
249
|
+
case 'function':
|
|
250
|
+
default:
|
|
251
|
+
return new DeclarationBlock({})
|
|
252
|
+
.export()
|
|
253
|
+
.asKind('function')
|
|
254
|
+
.withName(dropAnnotation ? `${name}Schema()` : `${name}Schema(): z.ZodObject<Properties<${typeName}>>`)
|
|
255
|
+
.withBlock([indent(`return z.object({`), shape, indent(`})${transformSuffix}`)].join('\n'))
|
|
256
|
+
.string;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
function generateFieldZodSchema(config, visitor, field, indentCount) {
|
|
261
|
+
const gen = generateFieldTypeZodSchema(config, visitor, field, field.type);
|
|
262
|
+
return indent(`${field.name.value}: ${maybeLazy(visitor, field.type, gen)}`, indentCount);
|
|
263
|
+
}
|
|
264
|
+
function generateFieldTypeZodSchema(config, visitor, field, type, parentType) {
|
|
265
|
+
if (isListType(type)) {
|
|
266
|
+
const gen = generateFieldTypeZodSchema(config, visitor, field, type.type, type);
|
|
267
|
+
if (!isNonNullType(parentType)) {
|
|
268
|
+
const arrayGen = `z.array(${maybeLazy(visitor, type.type, gen)})`;
|
|
269
|
+
const maybeLazyGen = applyDirectives(config, field, arrayGen);
|
|
270
|
+
return `${maybeLazyGen}.nullish()`;
|
|
271
|
+
}
|
|
272
|
+
return `z.array(${maybeLazy(visitor, type.type, gen)})`;
|
|
273
|
+
}
|
|
274
|
+
if (isNonNullType(type)) {
|
|
275
|
+
const gen = generateFieldTypeZodSchema(config, visitor, field, type.type, type);
|
|
276
|
+
return maybeLazy(visitor, type.type, gen);
|
|
277
|
+
}
|
|
278
|
+
if (isNamedType(type)) {
|
|
279
|
+
const gen = generateNameNodeZodSchema(config, visitor, type.name);
|
|
280
|
+
if (isListType(parentType))
|
|
281
|
+
return `${gen}.nullable()`;
|
|
282
|
+
let appliedDirectivesGen = applyDirectives(config, field, gen);
|
|
283
|
+
if (field.kind === Kind.INPUT_VALUE_DEFINITION) {
|
|
284
|
+
const { defaultValue } = field;
|
|
285
|
+
if (defaultValue?.kind === Kind.INT || defaultValue?.kind === Kind.FLOAT || defaultValue?.kind === Kind.BOOLEAN)
|
|
286
|
+
appliedDirectivesGen = `${appliedDirectivesGen}.default(${defaultValue.value})`;
|
|
287
|
+
if (defaultValue?.kind === Kind.STRING || defaultValue?.kind === Kind.ENUM) {
|
|
288
|
+
if (config.useEnumTypeAsDefaultValue && defaultValue?.kind !== Kind.STRING) {
|
|
289
|
+
let value = convertNameParts(defaultValue.value, resolveExternalModuleAndFn('change-case-all#pascalCase'), config.namingConvention?.transformUnderscore);
|
|
290
|
+
if (config.namingConvention?.enumValues)
|
|
291
|
+
value = convertNameParts(defaultValue.value, resolveExternalModuleAndFn(config.namingConvention?.enumValues), config.namingConvention?.transformUnderscore);
|
|
292
|
+
appliedDirectivesGen = `${appliedDirectivesGen}.default(${type.name.value}.${value})`;
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
appliedDirectivesGen = `${appliedDirectivesGen}.default("${escapeGraphQLCharacters(defaultValue.value)}")`;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (isNonNullType(parentType)) {
|
|
300
|
+
if (visitor.shouldEmitAsNotAllowEmptyString(type.name.value))
|
|
301
|
+
return `${appliedDirectivesGen}.min(1)`;
|
|
302
|
+
return appliedDirectivesGen;
|
|
303
|
+
}
|
|
304
|
+
if (isListType(parentType))
|
|
305
|
+
return `${appliedDirectivesGen}.nullable()`;
|
|
306
|
+
return `${appliedDirectivesGen}.nullish()`;
|
|
307
|
+
}
|
|
308
|
+
console.warn('unhandled type:', type);
|
|
309
|
+
return '';
|
|
310
|
+
}
|
|
311
|
+
function applyDirectives(config, field, gen) {
|
|
312
|
+
if (config.directives && field.directives) {
|
|
313
|
+
const formatted = formatDirectiveConfig(config.directives);
|
|
314
|
+
return gen + buildApi(formatted, field.directives);
|
|
315
|
+
}
|
|
316
|
+
return gen;
|
|
317
|
+
}
|
|
318
|
+
function generateNameNodeZodSchema(config, visitor, node) {
|
|
319
|
+
const converter = visitor.getNameNodeConverter(node);
|
|
320
|
+
switch (converter?.targetKind) {
|
|
321
|
+
case 'InterfaceTypeDefinition':
|
|
322
|
+
case 'InputObjectTypeDefinition':
|
|
323
|
+
case 'ObjectTypeDefinition':
|
|
324
|
+
case 'UnionTypeDefinition':
|
|
325
|
+
// using switch-case rather than if-else to allow for future expansion
|
|
326
|
+
switch (config.validationSchemaExportType) {
|
|
327
|
+
case 'const':
|
|
328
|
+
return `${converter.convertName()}Schema`;
|
|
329
|
+
case 'function':
|
|
330
|
+
default:
|
|
331
|
+
return `${converter.convertName()}Schema()`;
|
|
332
|
+
}
|
|
333
|
+
case 'EnumTypeDefinition':
|
|
334
|
+
return `${converter.convertName()}Schema`;
|
|
335
|
+
case 'ScalarTypeDefinition':
|
|
336
|
+
return zod4Scalar(config, visitor, node.value);
|
|
337
|
+
default:
|
|
338
|
+
if (converter?.targetKind)
|
|
339
|
+
console.warn('Unknown targetKind', converter?.targetKind);
|
|
340
|
+
return zod4Scalar(config, visitor, node.value);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
function maybeLazy(visitor, type, schema) {
|
|
344
|
+
if (!isNamedType(type)) {
|
|
345
|
+
return schema;
|
|
346
|
+
}
|
|
347
|
+
const schemaType = visitor.getType(type.name.value);
|
|
348
|
+
const isComplexType = !isScalarType(schemaType) && !isEnumType(schemaType);
|
|
349
|
+
return isComplexType ? `z.lazy(() => ${schema})` : schema;
|
|
350
|
+
}
|
|
351
|
+
function zod4Scalar(config, visitor, scalarName) {
|
|
352
|
+
if (config.scalarSchemas?.[scalarName])
|
|
353
|
+
return config.scalarSchemas[scalarName];
|
|
354
|
+
const tsType = visitor.getScalarType(scalarName);
|
|
355
|
+
switch (tsType) {
|
|
356
|
+
case 'string':
|
|
357
|
+
return `z.string()`;
|
|
358
|
+
case 'number':
|
|
359
|
+
return `z.number()`;
|
|
360
|
+
case 'boolean':
|
|
361
|
+
return `z.boolean()`;
|
|
362
|
+
}
|
|
363
|
+
if (config.defaultScalarTypeSchema) {
|
|
364
|
+
return config.defaultScalarTypeSchema;
|
|
365
|
+
}
|
|
366
|
+
console.warn('unhandled scalar name:', scalarName);
|
|
367
|
+
return anySchema;
|
|
368
|
+
}
|