Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,11 @@
],
"scripts": {
"type-check": "tsc --noEmit",
"type-check:yup": "tsc --strict --skipLibCheck --noEmit example/yup/schemas.ts",
"type-check:zod": "tsc --strict --skipLibCheck --noEmit example/zod/schemas.ts",
"type-check:zodv4": "tsc --strict --skipLibCheck --noEmit example/zodv4/schemas.ts",
"type-check:myzod": "tsc --strict --skipLibCheck --noEmit example/myzod/schemas.ts",
"type-check:valibot": "tsc --strict --skipLibCheck --noEmit example/valibot/schemas.ts",
"type-check:yup": "tsc --strict --skipLibCheck --noEmit --ignoreConfig example/yup/schemas.ts",
"type-check:zod": "tsc --strict --skipLibCheck --noEmit --ignoreConfig example/zod/schemas.ts",
"type-check:zodv4": "tsc --strict --skipLibCheck --noEmit --ignoreConfig example/zodv4/schemas.ts",
"type-check:myzod": "tsc --strict --skipLibCheck --noEmit --ignoreConfig example/myzod/schemas.ts",
"type-check:valibot": "tsc --strict --skipLibCheck --noEmit --ignoreConfig example/valibot/schemas.ts",
"test": "vitest run",
"build": "run-p build:*",
"build:cjs": "tsc -p tsconfig.cjs.json && echo '{\"type\":\"commonjs\"}' > dist/cjs/package.json",
Expand Down
29 changes: 29 additions & 0 deletions src/lazy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { TypeNode } from 'graphql';
import type { Visitor } from './visitor.js';

import { isEnumType, isScalarType } from 'graphql';
import { isNamedType } from './graphql.js';

/**
* Wraps a schema expression in a library-specific lazy reference when the type
* is a complex (non-scalar, non-enum) named type — avoiding issues with
* mutually-recursive input types.
*
* Each validation library has its own lazy syntax (z.lazy, v.lazy, etc.), so
* callers supply the wrapper function.
*
* @param lazyWrapper - e.g. `(s) => \`z.lazy(() => ${s})\``
*/
export function buildMaybeLazy(
visitor: Visitor,
type: TypeNode,
schema: string,
lazyWrapper: (schema: string) => string,
): string {
if (!isNamedType(type))
return schema;

const schemaType = visitor.getType(type.name.value);
const isComplexType = !isScalarType(schemaType) && !isEnumType(schemaType);
return isComplexType ? lazyWrapper(schema) : schema;
}
35 changes: 7 additions & 28 deletions src/myzod/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@ import type { Visitor } from '../visitor.js';
import { resolveExternalModuleAndFn } from '@graphql-codegen/plugin-helpers';
import { convertNameParts, DeclarationBlock, indent } from '@graphql-codegen/visitor-plugin-common';
import {
isEnumType,
isScalarType,
Kind,
} from 'graphql';
import { buildApi, formatDirectiveConfig } from '../directive.js';
Expand All @@ -29,6 +27,8 @@ import {
isNonNullType,
ObjectTypeDefinitionBuilder,
} from '../graphql.js';
import { buildMaybeLazy } from '../lazy.js';
import { buildScalarSchema } from '../scalar.js';
import { BaseSchemaVisitor } from '../schema_visitor.js';

const anySchema = `definedNonNullAnySchema`;
Expand Down Expand Up @@ -365,33 +365,12 @@ function generateNameNodeMyZodSchema(config: ValidationSchemaPluginConfig, visit
}

function maybeLazy(visitor: Visitor, type: TypeNode, schema: string): string {
if (!isNamedType(type)) {
return schema;
}

const schemaType = visitor.getType(type.name.value);
const isComplexType = !isScalarType(schemaType) && !isEnumType(schemaType);
return isComplexType ? `myzod.lazy(() => ${schema})` : schema;
return buildMaybeLazy(visitor, type, schema, s => `myzod.lazy(() => ${s})`);
}

function myzod4Scalar(config: ValidationSchemaPluginConfig, visitor: Visitor, scalarName: string): string {
if (config.scalarSchemas?.[scalarName])
return config.scalarSchemas[scalarName];

const tsType = visitor.getScalarType(scalarName);
switch (tsType) {
case 'string':
return `myzod.string()`;
case 'number':
return `myzod.number()`;
case 'boolean':
return `myzod.boolean()`;
}

if (config.defaultScalarTypeSchema) {
return config.defaultScalarTypeSchema;
}

console.warn('unhandled name:', scalarName);
return anySchema;
return buildScalarSchema(config, visitor, scalarName, {
typeMap: { string: 'myzod.string()', number: 'myzod.number()', boolean: 'myzod.boolean()' },
fallback: anySchema,
});
}
39 changes: 39 additions & 0 deletions src/scalar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type { ValidationSchemaPluginConfig } from './config.js';
import type { Visitor } from './visitor.js';

/**
* Builds a library-specific scalar schema expression.
*
* All five validation libraries follow the same pattern: check for a custom
* `scalarSchemas` override, fall back to a built-in type map for the resolved
* TypeScript type (string/number/boolean), then apply `defaultScalarTypeSchema`,
* and finally warn and return the library fallback.
*
* The only per-library differences are the strings in `typeMap`, the `fallback`
* value, and whether custom schemas need wrapping (yup appends `.defined()`).
*/
export function buildScalarSchema(
config: ValidationSchemaPluginConfig,
visitor: Visitor,
scalarName: string,
options: {
typeMap: Record<'string' | 'number' | 'boolean', string>
fallback: string
wrapCustom?: (schema: string) => string
},
): string {
if (config.scalarSchemas?.[scalarName]) {
const custom = config.scalarSchemas[scalarName];
return options.wrapCustom ? options.wrapCustom(custom) : custom;
}

const tsType = visitor.getScalarType(scalarName);
if (tsType != null && tsType in options.typeMap)
return options.typeMap[tsType as keyof typeof options.typeMap];

if (config.defaultScalarTypeSchema)
return config.defaultScalarTypeSchema;

console.warn('unhandled scalar name:', scalarName);
return options.fallback;
}
34 changes: 7 additions & 27 deletions src/valibot/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import type { ValidationSchemaPluginConfig } from '../config.js';
import type { Visitor } from '../visitor.js';
import { DeclarationBlock, indent } from '@graphql-codegen/visitor-plugin-common';

import { isEnumType, isScalarType } from 'graphql';
import { buildApiForValibot, formatDirectiveConfig } from '../directive.js';
import {
InterfaceTypeDefinitionBuilder,
Expand All @@ -23,6 +22,8 @@ import {
isNonNullType,
ObjectTypeDefinitionBuilder,
} from '../graphql.js';
import { buildMaybeLazy } from '../lazy.js';
import { buildScalarSchema } from '../scalar.js';
import { BaseSchemaVisitor } from '../schema_visitor.js';

export class ValibotSchemaVisitor extends BaseSchemaVisitor {
Expand Down Expand Up @@ -287,33 +288,12 @@ function generateNameNodeValibotSchema(config: ValidationSchemaPluginConfig, vis
}

function maybeLazy(visitor: Visitor, type: TypeNode, schema: string): string {
if (!isNamedType(type)) {
return schema;
}

const schemaType = visitor.getType(type.name.value);
const isComplexType = !isScalarType(schemaType) && !isEnumType(schemaType);
return isComplexType ? `v.lazy(() => ${schema})` : schema;
return buildMaybeLazy(visitor, type, schema, s => `v.lazy(() => ${s})`);
}

function valibot4Scalar(config: ValidationSchemaPluginConfig, visitor: Visitor, scalarName: string): string {
if (config.scalarSchemas?.[scalarName])
return config.scalarSchemas[scalarName];

const tsType = visitor.getScalarType(scalarName);
switch (tsType) {
case 'string':
return `v.string()`;
case 'number':
return `v.number()`;
case 'boolean':
return `v.boolean()`;
}

if (config.defaultScalarTypeSchema) {
return config.defaultScalarTypeSchema;
}

console.warn('unhandled scalar name:', scalarName);
return 'v.any()';
return buildScalarSchema(config, visitor, scalarName, {
typeMap: { string: 'v.string()', number: 'v.number()', boolean: 'v.boolean()' },
fallback: 'v.any()',
});
}
36 changes: 8 additions & 28 deletions src/yup/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@ import type { Visitor } from '../visitor.js';
import { resolveExternalModuleAndFn } from '@graphql-codegen/plugin-helpers';
import { convertNameParts, DeclarationBlock, indent } from '@graphql-codegen/visitor-plugin-common';
import {
isEnumType,
isScalarType,
Kind,
} from 'graphql';
import { buildApi, formatDirectiveConfig } from '../directive.js';
Expand All @@ -29,6 +27,8 @@ import {
isNonNullType,
ObjectTypeDefinitionBuilder,
} from '../graphql.js';
import { buildMaybeLazy } from '../lazy.js';
import { buildScalarSchema } from '../scalar.js';
import { BaseSchemaVisitor } from '../schema_visitor.js';

export class YupSchemaVisitor extends BaseSchemaVisitor {
Expand Down Expand Up @@ -391,33 +391,13 @@ function generateNameNodeYupSchema(config: ValidationSchemaPluginConfig, visitor
}

function maybeLazy(visitor: Visitor, type: TypeNode, schema: string): string {
if (!isNamedType(type)) {
return schema;
}

const schemaType = visitor.getType(type.name.value);
const isComplexType = !isScalarType(schemaType) && !isEnumType(schemaType);
return isComplexType ? `yup.lazy(() => ${schema})` : schema;
return buildMaybeLazy(visitor, type, schema, s => `yup.lazy(() => ${s})`);
}

function yup4Scalar(config: ValidationSchemaPluginConfig, visitor: Visitor, scalarName: string): string {
if (config.scalarSchemas?.[scalarName])
return `${config.scalarSchemas[scalarName]}.defined()`;

const tsType = visitor.getScalarType(scalarName);
switch (tsType) {
case 'string':
return `yup.string().defined()`;
case 'number':
return `yup.number().defined()`;
case 'boolean':
return `yup.boolean().defined()`;
}

if (config.defaultScalarTypeSchema) {
return config.defaultScalarTypeSchema
}

console.warn('unhandled name:', scalarName);
return `yup.mixed()`;
return buildScalarSchema(config, visitor, scalarName, {
typeMap: { string: 'yup.string().defined()', number: 'yup.number().defined()', boolean: 'yup.boolean().defined()' },
fallback: 'yup.mixed()',
wrapCustom: s => `${s}.defined()`,
});
}
Loading