Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
2fd8ac7
Update config.ts
MPiland Jun 5, 2025
0cbb518
Update index.ts
MPiland Jun 5, 2025
f50745b
Update index.ts
MPiland Jun 5, 2025
f50f869
Update index.ts
MPiland Jun 5, 2025
09c38fa
Update index.ts
MPiland Jun 5, 2025
6f84a6b
Create index.ts
MPiland Jun 5, 2025
5bb4905
Update index.ts
MPiland Jun 5, 2025
b587b83
Update index.ts
MPiland Jun 5, 2025
8fe54e9
Update index.ts
MPiland Jun 5, 2025
90bebd9
Update index.ts
MPiland Jun 5, 2025
a5c92bb
Update index.ts
MPiland Jun 5, 2025
e113b8d
Update index.ts
MPiland Jun 5, 2025
877ecda
Update index.ts
MPiland Jun 5, 2025
a79e943
Update index.ts
MPiland Jun 5, 2025
66db8ef
Update config.ts
MPiland Jun 5, 2025
fe44e91
Update config.ts
MPiland Jun 5, 2025
c092373
Update index.ts
MPiland Jun 5, 2025
d032a51
Update config.ts
MPiland Jun 5, 2025
ece2753
Create utils.ts
MPiland Jun 5, 2025
dc7b5b8
Update index.ts
MPiland Jun 5, 2025
889fe16
Update index.ts
MPiland Jun 5, 2025
92884a0
Update index.ts
MPiland Jun 5, 2025
7064cf8
Update index.ts
MPiland Jun 5, 2025
d5bbdc0
Update index.ts
MPiland Jun 5, 2025
e482d02
Update index.ts
MPiland Jun 5, 2025
c34efd9
Update index.ts
MPiland Jun 5, 2025
5356fdd
Update index.ts
MPiland Jun 5, 2025
d9712f9
Update index.ts
MPiland Jun 5, 2025
5b75ed7
Update index.ts
MPiland Jun 5, 2025
f0c638f
Update index.ts
MPiland Jun 5, 2025
1407df9
Update index.ts
MPiland Jun 5, 2025
b95543e
Update index.ts
MPiland Jun 5, 2025
404f91e
Update index.ts
MPiland Jun 5, 2025
55c8199
Update index.ts
MPiland Jun 5, 2025
59492f7
Update index.ts
MPiland Jun 5, 2025
729cfcb
Update index.ts
MPiland Jun 5, 2025
9ddf8ba
Update index.ts
MPiland Jun 5, 2025
c35f272
Update index.ts
MPiland Jun 5, 2025
2626e69
Update index.ts
MPiland Jun 5, 2025
bcf9971
Update index.ts
MPiland Jun 5, 2025
02e46de
Update README.md
MPiland Jun 5, 2025
eef742a
Merge branch 'Code-Hex:main' into main
MPiland Jun 5, 2025
dba6a3d
added zod strict object
MPiland Jun 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,56 @@ generates:
schema: yup
```

### `inputDiscriminator`

type: `string` default: `''`

When a discriminator is provided, it will add the type name as the defined string for the discriminator key provided.

```yml
generates:
path/to/graphql.ts:
plugins:
- typescript
- typescript-validation-schema
config:
inputDiscriminator: __kind
```

### `zodStrictObject`

type: `boolean` default: `false`

Will create a strict zod object. Should be used with Zod schema.

```yml
generates:
path/to/graphql.ts:
plugins:
- typescript
- typescript-validation-schema
config:
zodStrictObject: true
```

### `lazyStrategy`

type: `lazyStrategy` default: `'all'`

Sets how the lazy function is added to references. All will apply it to all references, circular will only apply it to truly circular references.

You can specify `all` or `circular`.

```yml
generates:
path/to/graphql.ts:
plugins:
- typescript
- typescript-validation-schema
config:
lazyStrategy: circular
```

### `importFrom`

type: `string`
Expand Down
62 changes: 61 additions & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type { TypeScriptPluginConfig } from '@graphql-codegen/typescript';
import type { NamingConventionMap } from '@graphql-codegen/visitor-plugin-common';

export type ValidationSchema = 'yup' | 'zod' | 'myzod' | 'valibot';
export type ValidationSchema = 'yup' | 'zod' | 'zod/v4' | 'myzod' | 'valibot';
export type LazyStrategy = 'all' | 'circular'
export type ValidationSchemaExportType = 'function' | 'const';

export interface DirectiveConfig {
Expand Down Expand Up @@ -35,6 +36,23 @@ export interface ValidationSchemaPluginConfig extends TypeScriptPluginConfig {
* ```
*/
schema?: ValidationSchema
/**
* @description Setting to determine when to set a property to lazy. 'Circular' will only use lazy for circular references. 'All' will set lazy for all properties referencing another schema.
* @default all
*
* @exampleMarkdown
* ```yml
* generates:
* path/to/file.ts:
* plugins:
* - typescript
* - graphql-codegen-validation-schema
* config:
* schema: yup
* lazy: circular
* ```
*/
lazyStrategy?: LazyStrategy;
/**
* @description import types from generated typescript type path
* if not given, omit import statement.
Expand Down Expand Up @@ -96,6 +114,48 @@ export interface ValidationSchemaPluginConfig extends TypeScriptPluginConfig {
* ```
*/
useTypeImports?: boolean
/**
* @description Will create strict type object.
* To be used with Zod schema.
* @default false
*
* @exampleMarkdown
* ```yml
* generates:
* path/to/types.ts:
* plugins:
* - typescript
* path/to/schemas.ts:
* plugins:
* - graphql-codegen-validation-schema
* config:
* schema: zod
* strictShape: true
* ```
*/
zodStrictObject?: boolean
/**
* @description Creates schemas for input types only.
* This gives compatibility with TypeScript's "importsNotUsedAsValues": "error" option
* Should used in conjunction with `importFrom` option.
* @default false
*
* @exampleMarkdown
* ```yml
* generates:
* path/to/types.ts:
* plugins:
* - typescript
* path/to/schemas.ts:
* plugins:
* - graphql-codegen-validation-schema
* config:
* schema: yup
* importFrom: ./path/to/types
* inputsOnly: true
* ```
*/
inputDiscriminator?: string;
/**
* @description Prefixes all import types from generated typescript type.
* @default ""
Expand Down
4 changes: 3 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import type { ValidationSchemaPluginConfig } from './config.js';
import type { SchemaVisitor } from './types.js';
import { transformSchemaAST } from '@graphql-codegen/schema-ast';
import { buildSchema, printSchema, visit } from 'graphql';

import { isGeneratedByIntrospection, topologicalSortAST } from './graphql.js';
import { MyZodSchemaVisitor } from './myzod/index.js';
import { ValibotSchemaVisitor } from './valibot/index.js';
import { YupSchemaVisitor } from './yup/index.js';
import { ZodSchemaVisitor } from './zod/index.js';
import {Zodv4SchemaVisitor} from './zodv4/index.js';

export const plugin: PluginFunction<ValidationSchemaPluginConfig, Types.ComplexPluginOutput> = (
schema: GraphQLSchema,
Expand All @@ -32,6 +32,8 @@ export const plugin: PluginFunction<ValidationSchemaPluginConfig, Types.ComplexP
function schemaVisitor(schema: GraphQLSchema, config: ValidationSchemaPluginConfig): SchemaVisitor {
if (config?.schema === 'zod')
return new ZodSchemaVisitor(schema, config);
else if(config?.schema === 'zod/v4')
return new Zodv4SchemaVisitor(schema, config);
else if (config?.schema === 'myzod')
return new MyZodSchemaVisitor(schema, config);
else if (config?.schema === 'valibot')
Expand Down
52 changes: 35 additions & 17 deletions src/myzod/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,15 @@ import {
ObjectTypeDefinitionBuilder,
} from '../graphql.js';
import { BaseSchemaVisitor } from '../schema_visitor.js';
import { findCircularTypes } from '../utils.js';

const anySchema = `definedNonNullAnySchema`;

export class MyZodSchemaVisitor extends BaseSchemaVisitor {
constructor(schema: GraphQLSchema, config: ValidationSchemaPluginConfig) {
super(schema, config);
this.circularTypes = findCircularTypes(schema);
this.config.lazyStrategy ??= 'all';
}

importValidationSchema(): string {
Expand Down Expand Up @@ -75,7 +78,7 @@ export class MyZodSchemaVisitor extends BaseSchemaVisitor {
const appendArguments = argumentBlocks ? `\n${argumentBlocks}` : '';

// Building schema for fields.
const shape = node.fields?.map(field => generateFieldMyZodSchema(this.config, visitor, field, 2)).join(',\n');
const shape = node.fields?.map(field => generateFieldMyZodSchema(this.config, visitor, field, 2, this.circularTypes)).join(',\n');

switch (this.config.validationSchemaExportType) {
case 'const':
Expand Down Expand Up @@ -116,7 +119,7 @@ export class MyZodSchemaVisitor extends BaseSchemaVisitor {
const appendArguments = argumentBlocks ? `\n${argumentBlocks}` : '';

// Building schema for fields.
const shape = node.fields?.map(field => generateFieldMyZodSchema(this.config, visitor, field, 2)).join(',\n');
const shape = node.fields?.map(field => generateFieldMyZodSchema(this.config, visitor, field, 2, this.circularTypes)).join(',\n');

switch (this.config.validationSchemaExportType) {
case 'const':
Expand Down Expand Up @@ -237,15 +240,16 @@ export class MyZodSchemaVisitor extends BaseSchemaVisitor {
name: string,
) {
const typeName = visitor.prefixTypeNamespace(name);
const shape = fields.map(field => generateFieldMyZodSchema(this.config, visitor, field, 2)).join(',\n');
const discriminator = this.config.inputDiscriminator ? `\t${this.config.inputDiscriminator}: myzod.literal('${name}'),` : ''
const shape = fields.map(field => generateFieldMyZodSchema(this.config, visitor, field, 2, this.circularTypes)).join(',\n');

switch (this.config.validationSchemaExportType) {
case 'const':
return new DeclarationBlock({})
.export()
.asKind('const')
.withName(`${name}Schema: myzod.Type<${typeName}>`)
.withContent(['myzod.object({', shape, '})'].join('\n'))
.withContent(['myzod.object({', discriminator, shape, '})'].join('\n'))
.string;

case 'function':
Expand All @@ -254,30 +258,30 @@ export class MyZodSchemaVisitor extends BaseSchemaVisitor {
.export()
.asKind('function')
.withName(`${name}Schema(): myzod.Type<${typeName}>`)
.withBlock([indent(`return myzod.object({`), shape, indent('})')].join('\n'))
.withBlock([indent(`return myzod.object({`), discriminator, shape, indent('})')].join('\n'))
.string;
}
}
}

function generateFieldMyZodSchema(config: ValidationSchemaPluginConfig, visitor: Visitor, field: InputValueDefinitionNode | FieldDefinitionNode, indentCount: number): string {
const gen = generateFieldTypeMyZodSchema(config, visitor, field, field.type);
return indent(`${field.name.value}: ${maybeLazy(field.type, gen)}`, indentCount);
function generateFieldMyZodSchema(config: ValidationSchemaPluginConfig, visitor: Visitor, field: InputValueDefinitionNode | FieldDefinitionNode, indentCount: number, circularTypes: Set<string>): string {
const gen = generateFieldTypeMyZodSchema(config, visitor, field, field.type, undefined, circularTypes);
return indent(`${field.name.value}: ${maybeLazy(field.type, gen, config, circularTypes)}`, indentCount);
}

function generateFieldTypeMyZodSchema(config: ValidationSchemaPluginConfig, visitor: Visitor, field: InputValueDefinitionNode | FieldDefinitionNode, type: TypeNode, parentType?: TypeNode): string {
function generateFieldTypeMyZodSchema(config: ValidationSchemaPluginConfig, visitor: Visitor, field: InputValueDefinitionNode | FieldDefinitionNode, type: TypeNode, parentType?: TypeNode, circularTypes: Set<string>): string {
if (isListType(type)) {
const gen = generateFieldTypeMyZodSchema(config, visitor, field, type.type, type);
const gen = generateFieldTypeMyZodSchema(config, visitor, field, type.type, type, circularTypes);
if (!isNonNullType(parentType)) {
const arrayGen = `myzod.array(${maybeLazy(type.type, gen)})`;
const arrayGen = `myzod.array(${maybeLazy(type.type, gen, config, circularTypes)})`;
const maybeLazyGen = applyDirectives(config, field, arrayGen);
return `${maybeLazyGen}.optional().nullable()`;
}
return `myzod.array(${maybeLazy(type.type, gen)})`;
return `myzod.array(${maybeLazy(type.type, gen, config, circularTypes)})`;
}
if (isNonNullType(type)) {
const gen = generateFieldTypeMyZodSchema(config, visitor, field, type.type, type);
return maybeLazy(type.type, gen);
const gen = generateFieldTypeMyZodSchema(config, visitor, field, type.type, type, circularTypes);
return maybeLazy(type.type, gen, config, circularTypes);
}
if (isNamedType(type)) {
const gen = generateNameNodeMyZodSchema(config, visitor, type.name);
Expand Down Expand Up @@ -358,9 +362,23 @@ function generateNameNodeMyZodSchema(config: ValidationSchemaPluginConfig, visit
}
}

function maybeLazy(type: TypeNode, schema: string): string {
if (isNamedType(type) && isInput(type.name.value))
return `myzod.lazy(() => ${schema})`;
function maybeLazy(
type: TypeNode,
schema: string,
config: ValidationSchemaPluginConfig,
circularTypes: Set<string>
): string {
if (isNamedType(type)) {
const typeName = type.name.value;

if (config.lazyStrategy === 'all' && isInput(typeName)) {
return `myzod.lazy(() => ${schema})`;
}

if (config.lazyStrategy === 'circular' && circularTypes.has(typeName)) {
return `myzod.lazy(() => ${schema})`;
}
}

return schema;
}
Expand Down
39 changes: 39 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { getNamedType, GraphQLNamedType, isObjectType, isInputObjectType } from 'graphql';

export function findCircularTypes(schema: GraphQLSchema): Set<string> {
const circular = new Set<string>();
const visited = new Set<string>();
const stack = new Set<string>();

function visit(typeName: string) {
if (stack.has(typeName)) {
circular.add(typeName);
return;
}
if (visited.has(typeName)) return;
visited.add(typeName);
stack.add(typeName);

const type = schema.getType(typeName);
if (!type || !(isObjectType(type) || isInputObjectType(type))) {
stack.delete(typeName);
return;
}

const fields = type.getFields();
for (const field of Object.values(fields)) {
const fieldType = getNamedType(field.type);
visit(fieldType.name);
}

stack.delete(typeName);
}

for (const type of Object.values(schema.getTypeMap())) {
if (!type.name.startsWith('__')) {
visit(type.name);
}
}

return circular;
}
Loading