diff --git a/codegen.yml b/codegen.yml index f3170225..17e5e3e5 100644 --- a/codegen.yml +++ b/codegen.yml @@ -81,3 +81,26 @@ generates: email: email scalars: ID: string + example/valibot/schemas.ts: + plugins: + - ./dist/main/index.js: + schema: valibot + importFrom: ../types + withObjectType: true + directives: + # Write directives like + # + # directive: + # arg1: schemaApi + # arg2: ["schemaApi2", "Hello $1"] + # + # See more examples in `./tests/directive.spec.ts` + # https://github.com/Code-Hex/graphql-codegen-typescript-validation-schema/blob/main/tests/directive.spec.ts + constraint: + minLength: minLength + # Replace $1 with specified `startsWith` argument value of the constraint directive + startsWith: [regex, /^$1/, message] + format: + email: email + scalars: + ID: string diff --git a/example/valibot/schemas.ts b/example/valibot/schemas.ts new file mode 100644 index 00000000..14c8bac1 --- /dev/null +++ b/example/valibot/schemas.ts @@ -0,0 +1,130 @@ +import * as v from 'valibot' +import { Admin, AttributeInput, ButtonComponentType, ComponentInput, DropDownComponentInput, EventArgumentInput, EventInput, EventOptionType, Guest, HttpInput, HttpMethod, LayoutInput, MyType, MyTypeFooArgs, Namer, PageInput, PageType, User } from '../types' + +export const ButtonComponentTypeSchema = v.enum_(ButtonComponentType); + +export const EventOptionTypeSchema = v.enum_(EventOptionType); + +export const HttpMethodSchema = v.enum_(HttpMethod); + +export const PageTypeSchema = v.enum_(PageType); + +export function AdminSchema(): v.GenericSchema { + return v.object({ + __typename: v.optional(v.literal('Admin')), + lastModifiedAt: v.nullish(v.any()) + }) +} + +export function AttributeInputSchema(): v.GenericSchema { + return v.object({ + key: v.nullish(v.string()), + val: v.nullish(v.string()) + }) +} + +export function ComponentInputSchema(): v.GenericSchema { + return v.object({ + child: v.lazy(() => v.nullish(ComponentInputSchema())), + childrens: v.nullish(v.array(v.lazy(() => v.nullable(ComponentInputSchema())))), + event: v.lazy(() => v.nullish(EventInputSchema())), + name: v.string(), + type: ButtonComponentTypeSchema + }) +} + +export function DropDownComponentInputSchema(): v.GenericSchema { + return v.object({ + dropdownComponent: v.lazy(() => v.nullish(ComponentInputSchema())), + getEvent: v.lazy(() => EventInputSchema()) + }) +} + +export function EventArgumentInputSchema(): v.GenericSchema { + return v.object({ + name: v.pipe(v.string(), v.minLength(5)), + value: v.pipe(v.string(), v.regex(/^foo/, "message")) + }) +} + +export function EventInputSchema(): v.GenericSchema { + return v.object({ + arguments: v.array(v.lazy(() => EventArgumentInputSchema())), + options: v.nullish(v.array(EventOptionTypeSchema)) + }) +} + +export function GuestSchema(): v.GenericSchema { + return v.object({ + __typename: v.optional(v.literal('Guest')), + lastLoggedIn: v.nullish(v.any()) + }) +} + +export function HttpInputSchema(): v.GenericSchema { + return v.object({ + method: v.nullish(HttpMethodSchema), + url: v.any() + }) +} + +export function LayoutInputSchema(): v.GenericSchema { + return v.object({ + dropdown: v.lazy(() => v.nullish(DropDownComponentInputSchema())) + }) +} + +export function MyTypeSchema(): v.GenericSchema { + return v.object({ + __typename: v.optional(v.literal('MyType')), + foo: v.nullish(v.string()) + }) +} + +export function MyTypeFooArgsSchema(): v.GenericSchema { + return v.object({ + a: v.nullish(v.string()), + b: v.number(), + c: v.nullish(v.boolean()), + d: v.number() + }) +} + +export function NamerSchema(): v.GenericSchema { + return v.object({ + name: v.nullish(v.string()) + }) +} + +export function PageInputSchema(): v.GenericSchema { + return v.object({ + attributes: v.nullish(v.array(v.lazy(() => AttributeInputSchema()))), + date: v.nullish(v.any()), + height: v.number(), + id: v.string(), + layout: v.lazy(() => LayoutInputSchema()), + pageType: PageTypeSchema, + postIDs: v.nullish(v.array(v.string())), + show: v.boolean(), + tags: v.nullish(v.array(v.nullable(v.string()))), + title: v.string(), + width: v.number() + }) +} + +export function UserSchema(): v.GenericSchema { + return v.object({ + __typename: v.optional(v.literal('User')), + createdAt: v.nullish(v.any()), + email: v.nullish(v.string()), + id: v.nullish(v.string()), + kind: v.nullish(UserKindSchema()), + name: v.nullish(v.string()), + password: v.nullish(v.string()), + updatedAt: v.nullish(v.any()) + }) +} + +export function UserKindSchema() { + return v.union([AdminSchema(), GuestSchema()]) +} diff --git a/package.json b/package.json index 6ec79bca..e9488d50 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "type-check:yup": "tsc --strict --skipLibCheck --noEmit example/yup/schemas.ts", "type-check:zod": "tsc --strict --skipLibCheck --noEmit example/zod/schemas.ts", "type-check:myzod": "tsc --strict --skipLibCheck --noEmit example/myzod/schemas.ts", + "type-check:valibot": "tsc --strict --skipLibCheck --noEmit example/valibot/schemas.ts", "test": "vitest run", "build": "run-p build:*", "build:main": "tsc -p tsconfig.main.json", @@ -82,6 +83,7 @@ "ts-dedent": "^2.2.0", "ts-jest": "29.1.4", "typescript": "5.4.5", + "valibot": "0.31.0-rc.6", "vitest": "^1.0.0", "yup": "1.4.0", "zod": "3.23.8" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a3099dea..4204ef4d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -66,6 +66,9 @@ importers: typescript: specifier: 5.4.5 version: 5.4.5 + valibot: + specifier: 0.31.0-rc.6 + version: 0.31.0-rc.6 vitest: specifier: ^1.0.0 version: 1.6.0(@types/node@20.14.2) @@ -3700,6 +3703,9 @@ packages: resolution: {integrity: sha512-74Y4LqY74kLE6IFyIjPtkSTWzUZmj8tdHT9Ii/26dvQ6K9Dl2NbEfj0XgU2sHCtKgt5VupqhlO/5aWuqS+IY1w==} engines: {node: '>=10.12.0'} + valibot@0.31.0-rc.6: + resolution: {integrity: sha512-NW4mnZsSyLCj2TweTPBuo7jzhZywh3C2M0T5UU53po2jhg/0k7B63pQTt3hkcdZl2JkSZEYHliKzO/2OsLFqlQ==} + validate-npm-package-license@3.0.4: resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} @@ -8355,6 +8361,8 @@ snapshots: '@types/istanbul-lib-coverage': 2.0.4 convert-source-map: 1.9.0 + valibot@0.31.0-rc.6: {} + validate-npm-package-license@3.0.4: dependencies: spdx-correct: 3.2.0 diff --git a/src/config.ts b/src/config.ts index 6475f9fc..ca6d7502 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,6 +1,6 @@ import type { TypeScriptPluginConfig } from '@graphql-codegen/typescript'; -export type ValidationSchema = 'yup' | 'zod' | 'myzod'; +export type ValidationSchema = 'yup' | 'zod' | 'myzod' | 'valibot'; export type ValidationSchemaExportType = 'function' | 'const'; export interface DirectiveConfig { diff --git a/src/directive.ts b/src/directive.ts index d4c44f7f..f2918423 100644 --- a/src/directive.ts +++ b/src/directive.ts @@ -120,6 +120,39 @@ export function buildApi(config: FormattedDirectiveConfig, directives: ReadonlyA .join('') } +// This function generates `[v.minLength(100), v.email()]` +// NOTE: valibot's API is not a method chain, so it is prepared separately from buildApi. +// +// config +// { +// 'constraint': { +// 'minLength': ['minLength', '$1'], +// 'format': { +// 'uri': ['url', '$2'], +// 'email': ['email', '$2'], +// } +// } +// } +// +// GraphQL schema +// ```graphql +// input ExampleInput { +// email: String! @required(msg: "message") @constraint(minLength: 100, format: "email") +// } +// ``` +// +// FIXME: v.required() is not supported yet. v.required() is classified as `Methods` and must wrap the schema. ex) `v.required(v.object({...}))` +export function buildApiForValibot(config: FormattedDirectiveConfig, directives: ReadonlyArray): string[] { + return directives + .filter(directive => config[directive.name.value] !== undefined) + .map((directive) => { + const directiveName = directive.name.value; + const argsConfig = config[directiveName]; + const apis = _buildApiFromDirectiveArguments(argsConfig, directive.arguments ?? []); + return apis.map(api => `v${api}`); + }).flat() +} + function buildApiSchema(validationSchema: string[] | undefined, argValue: ConstValueNode): string { if (!validationSchema) return ''; @@ -133,6 +166,10 @@ function buildApiSchema(validationSchema: string[] | undefined, argValue: ConstV } function buildApiFromDirectiveArguments(config: FormattedDirectiveArguments, args: ReadonlyArray): string { + return _buildApiFromDirectiveArguments(config, args).join(''); +} + +function _buildApiFromDirectiveArguments(config: FormattedDirectiveArguments, args: ReadonlyArray): string[] { return args .map((arg) => { const argName = arg.name.value; @@ -142,7 +179,6 @@ function buildApiFromDirectiveArguments(config: FormattedDirectiveArguments, arg return buildApiSchema(validationSchema, arg.value); }) - .join(''); } function buildApiFromDirectiveObjectArguments(config: FormattedDirectiveObjectArguments, argValue: ConstValueNode): string { diff --git a/src/index.ts b/src/index.ts index 4bd6008a..22497a6f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,6 +9,7 @@ import { MyZodSchemaVisitor } from './myzod/index'; import type { SchemaVisitor } from './types'; import { YupSchemaVisitor } from './yup/index'; import { ZodSchemaVisitor } from './zod/index'; +import { ValibotSchemaVisitor } from './valibot'; export const plugin: PluginFunction = ( schema: GraphQLSchema, @@ -33,6 +34,8 @@ function schemaVisitor(schema: GraphQLSchema, config: ValidationSchemaPluginConf return new ZodSchemaVisitor(schema, config); else if (config?.schema === 'myzod') return new MyZodSchemaVisitor(schema, config); + else if (config?.schema === 'valibot') + return new ValibotSchemaVisitor(schema, config); return new YupSchemaVisitor(schema, config); } diff --git a/src/valibot/index.ts b/src/valibot/index.ts new file mode 100644 index 00000000..04841cd6 --- /dev/null +++ b/src/valibot/index.ts @@ -0,0 +1,296 @@ +import { DeclarationBlock, indent } from '@graphql-codegen/visitor-plugin-common'; +import type { + EnumTypeDefinitionNode, + FieldDefinitionNode, + GraphQLSchema, + InputObjectTypeDefinitionNode, + InputValueDefinitionNode, + InterfaceTypeDefinitionNode, + NameNode, + ObjectTypeDefinitionNode, + TypeNode, + UnionTypeDefinitionNode, +} from 'graphql'; + +import type { ValidationSchemaPluginConfig } from '../config'; +import { BaseSchemaVisitor } from '../schema_visitor'; +import type { Visitor } from '../visitor'; +import { buildApiForValibot, formatDirectiveConfig } from '../directive'; +import { + InterfaceTypeDefinitionBuilder, + ObjectTypeDefinitionBuilder, + isInput, + isListType, + isNamedType, + isNonNullType, +} from './../graphql'; + +export class ValibotSchemaVisitor extends BaseSchemaVisitor { + constructor(schema: GraphQLSchema, config: ValidationSchemaPluginConfig) { + super(schema, config); + } + + importValidationSchema(): string { + return `import * as v from 'valibot'`; + } + + initialEmit(): string { + return ( + `\n${[ + ...this.enumDeclarations, + ].join('\n')}` + ); + } + + get InputObjectTypeDefinition() { + return { + leave: (node: InputObjectTypeDefinitionNode) => { + const visitor = this.createVisitor('input'); + const name = visitor.convertName(node.name.value); + this.importTypes.push(name); + return this.buildInputFields(node.fields ?? [], visitor, name); + }, + }; + } + + get InterfaceTypeDefinition() { + return { + leave: InterfaceTypeDefinitionBuilder(this.config.withObjectType, (node: InterfaceTypeDefinitionNode) => { + const visitor = this.createVisitor('output'); + const name = visitor.convertName(node.name.value); + this.importTypes.push(name); + + // Building schema for field arguments. + const argumentBlocks = this.buildTypeDefinitionArguments(node, visitor); + const appendArguments = argumentBlocks ? `\n${argumentBlocks}` : ''; + + // Building schema for fields. + const shape = node.fields?.map(field => generateFieldValibotSchema(this.config, visitor, field, 2)).join(',\n'); + + switch (this.config.validationSchemaExportType) { + default: + return ( + new DeclarationBlock({}) + .export() + .asKind('function') + .withName(`${name}Schema(): v.GenericSchema<${name}>`) + .withBlock([indent(`return v.object({`), shape, indent('})')].join('\n')).string + appendArguments + ); + } + }), + }; + } + + get ObjectTypeDefinition() { + return { + leave: ObjectTypeDefinitionBuilder(this.config.withObjectType, (node: ObjectTypeDefinitionNode) => { + const visitor = this.createVisitor('output'); + const name = visitor.convertName(node.name.value); + this.importTypes.push(name); + + // Building schema for field arguments. + const argumentBlocks = this.buildTypeDefinitionArguments(node, visitor); + const appendArguments = argumentBlocks ? `\n${argumentBlocks}` : ''; + + // Building schema for fields. + const shape = node.fields?.map(field => generateFieldValibotSchema(this.config, visitor, field, 2)).join(',\n'); + + switch (this.config.validationSchemaExportType) { + default: + return ( + new DeclarationBlock({}) + .export() + .asKind('function') + .withName(`${name}Schema(): v.GenericSchema<${name}>`) + .withBlock( + [ + indent(`return v.object({`), + indent(`__typename: v.optional(v.literal('${node.name.value}')),`, 2), + shape, + indent('})'), + ].join('\n'), + ).string + appendArguments + ); + } + }), + }; + } + + get EnumTypeDefinition() { + return { + leave: (node: EnumTypeDefinitionNode) => { + const visitor = this.createVisitor('both'); + const enumname = visitor.convertName(node.name.value); + this.importTypes.push(enumname); + + // hoist enum declarations + this.enumDeclarations.push( + this.config.enumsAsTypes + ? new DeclarationBlock({}) + .export() + .asKind('const') + .withName(`${enumname}Schema`) + .withContent(`v.picklist([${node.values?.map(enumOption => `'${enumOption.name.value}'`).join(', ')}])`) + .string + : new DeclarationBlock({}) + .export() + .asKind('const') + .withName(`${enumname}Schema`) + .withContent(`v.enum_(${enumname})`).string, + ); + }, + }; + } + + get UnionTypeDefinition() { + return { + leave: (node: UnionTypeDefinitionNode) => { + if (!node.types || !this.config.withObjectType) + return; + const visitor = this.createVisitor('output'); + const unionName = visitor.convertName(node.name.value); + const unionElements = node.types + .map((t) => { + const element = visitor.convertName(t.name.value); + const typ = visitor.getType(t.name.value); + if (typ?.astNode?.kind === 'EnumTypeDefinition') + return `${element}Schema`; + + switch (this.config.validationSchemaExportType) { + default: + return `${element}Schema()`; + } + }) + .join(', '); + const unionElementsCount = node.types.length ?? 0; + + const union = unionElementsCount > 1 ? `v.union([${unionElements}])` : unionElements; + + switch (this.config.validationSchemaExportType) { + default: + return new DeclarationBlock({}) + .export() + .asKind('function') + .withName(`${unionName}Schema()`) + .withBlock(indent(`return ${union}`)).string; + } + }, + }; + } + + protected buildInputFields( + fields: readonly (FieldDefinitionNode | InputValueDefinitionNode)[], + visitor: Visitor, + name: string, + ) { + const shape = fields.map(field => generateFieldValibotSchema(this.config, visitor, field, 2)).join(',\n'); + + switch (this.config.validationSchemaExportType) { + default: + return new DeclarationBlock({}) + .export() + .asKind('function') + .withName(`${name}Schema(): v.GenericSchema<${name}>`) + .withBlock([indent(`return v.object({`), shape, indent('})')].join('\n')).string; + } + } +} + +function generateFieldValibotSchema(config: ValidationSchemaPluginConfig, visitor: Visitor, field: InputValueDefinitionNode | FieldDefinitionNode, indentCount: number): string { + const gen = generateFieldTypeValibotSchema(config, visitor, field, field.type); + return indent(`${field.name.value}: ${maybeLazy(field.type, gen)}`, indentCount); +} + +function generateFieldTypeValibotSchema(config: ValidationSchemaPluginConfig, visitor: Visitor, field: InputValueDefinitionNode | FieldDefinitionNode, type: TypeNode, parentType?: TypeNode): string { + if (isListType(type)) { + const gen = generateFieldTypeValibotSchema(config, visitor, field, type.type, type); + const arrayGen = `v.array(${maybeLazy(type.type, gen)})`; + if (!isNonNullType(parentType)) { + return `v.nullish(${arrayGen})`; + } + return arrayGen; + } + if (isNonNullType(type)) { + const gen = generateFieldTypeValibotSchema(config, visitor, field, type.type, type); + return maybeLazy(type.type, gen); + } + if (isNamedType(type)) { + const gen = generateNameNodeValibotSchema(config, visitor, type.name); + if (isListType(parentType)) + return `v.nullable(${gen})`; + + const actions = actionsFromDirectives(config, field); + + if (isNonNullType(parentType)) + return pipeSchemaAndActions(gen, actions); ; + + return `v.nullish(${pipeSchemaAndActions(gen, actions)})`; + } + console.warn('unhandled type:', type); + return ''; +} + +function actionsFromDirectives(config: ValidationSchemaPluginConfig, field: InputValueDefinitionNode | FieldDefinitionNode): string[] { + if (config.directives && field.directives) { + const formatted = formatDirectiveConfig(config.directives); + return buildApiForValibot(formatted, field.directives); + } + + return []; +} + +function pipeSchemaAndActions(schema: string, actions: string[]): string { + if (actions.length === 0) + return schema; + + return `v.pipe(${schema}, ${actions.join(', ')})`; +} + +function generateNameNodeValibotSchema(config: ValidationSchemaPluginConfig, visitor: Visitor, node: NameNode): string { + const converter = visitor.getNameNodeConverter(node); + + switch (converter?.targetKind) { + case 'InterfaceTypeDefinition': + case 'InputObjectTypeDefinition': + case 'ObjectTypeDefinition': + case 'UnionTypeDefinition': + // using switch-case rather than if-else to allow for future expansion + switch (config.validationSchemaExportType) { + default: + return `${converter.convertName()}Schema()`; + } + case 'EnumTypeDefinition': + return `${converter.convertName()}Schema`; + case 'ScalarTypeDefinition': + return valibot4Scalar(config, visitor, node.value); + default: + if (converter?.targetKind) + console.warn('Unknown targetKind', converter?.targetKind); + + return valibot4Scalar(config, visitor, node.value); + } +} + +function maybeLazy(type: TypeNode, schema: string): string { + if (isNamedType(type) && isInput(type.name.value)) + return `v.lazy(() => ${schema})`; + + return schema; +} + +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()`; + } + console.warn('unhandled scalar name:', scalarName); + return 'v.any()'; +} diff --git a/tests/directive.spec.ts b/tests/directive.spec.ts index 25bf1658..d5b3e4e6 100644 --- a/tests/directive.spec.ts +++ b/tests/directive.spec.ts @@ -9,6 +9,7 @@ import type { } from '../src/directive'; import { buildApi, + buildApiForValibot, exportedForTesting, formatDirectiveConfig, formatDirectiveObjectArguments, @@ -603,4 +604,64 @@ describe('format directive config', () => { }); } }); + + describe('buildApiForValibot', () => { + const cases: { + name: string + args: { + config: FormattedDirectiveConfig + args: ReadonlyArray + } + want: string[] + }[] = [ + { + name: 'valid', + args: { + config: { + constraint: { + minLength: ['minLength', '$1'], + format: { + uri: ['url'], + email: ['email'], + }, + }, + }, + args: [ + // @constraint(minLength: 100, format: "email") + buildConstDirectiveNodes('constraint', { + minLength: `100`, + format: `"email"`, + }), + ], + }, + want: [`v.minLength(100)`, `v.email()`], + }, + { + name: 'enum', + args: { + config: { + constraint: { + format: { + URI: ['uri'], + }, + }, + }, + args: [ + // @constraint(format: EMAIL) + buildConstDirectiveNodes('constraint', { + format: 'URI', + }), + ], + }, + want: [`v.uri()`], + }, + ]; + for (const tc of cases) { + it(tc.name, () => { + const { config, args } = tc.args; + const got = buildApiForValibot(config, args); + expect(got).toStrictEqual(tc.want); + }); + } + }); }); diff --git a/tests/valibot.spec.ts b/tests/valibot.spec.ts new file mode 100644 index 00000000..61f5a535 --- /dev/null +++ b/tests/valibot.spec.ts @@ -0,0 +1,942 @@ +import { buildSchema } from 'graphql'; + +import { plugin } from '../src/index'; + +describe('valibot', () => { + it('non-null and defined', async () => { + const schema = buildSchema(/* GraphQL */ ` + input PrimitiveInput { + a: ID! + b: String! + c: Boolean! + d: Int! + e: Float! + } + `); + const scalars = { + ID: 'string', + } + const result = await plugin(schema, [], { schema: 'valibot', scalars }, {}); + expect(result.content).toMatchInlineSnapshot(` + " + + export function PrimitiveInputSchema(): v.GenericSchema { + return v.object({ + a: v.string(), + b: v.string(), + c: v.boolean(), + d: v.number(), + e: v.number() + }) + } + " + `); + }) + it('nullish', async () => { + const schema = buildSchema(/* GraphQL */ ` + input PrimitiveInput { + a: ID + b: String + c: Boolean + d: Int + e: Float + z: String! # no defined check + } + `); + const scalars = { + ID: 'string', + } + const result = await plugin(schema, [], { schema: 'valibot', scalars }, {}); + expect(result.content).toMatchInlineSnapshot(` + " + + export function PrimitiveInputSchema(): v.GenericSchema { + return v.object({ + a: v.nullish(v.string()), + b: v.nullish(v.string()), + c: v.nullish(v.boolean()), + d: v.nullish(v.number()), + e: v.nullish(v.number()), + z: v.string() + }) + } + " + `); + }) + it('array', async () => { + const schema = buildSchema(/* GraphQL */ ` + input PrimitiveInput { + a: [String] + b: [String!] + c: [String!]! + d: [[String]] + e: [[String]!] + f: [[String]!]! + } + `); + const scalars = undefined + const result = await plugin(schema, [], { schema: 'valibot', scalars }, {}); + expect(result.content).toMatchInlineSnapshot(` + " + + export function PrimitiveInputSchema(): v.GenericSchema { + return v.object({ + a: v.nullish(v.array(v.nullable(v.string()))), + b: v.nullish(v.array(v.string())), + c: v.array(v.string()), + d: v.nullish(v.array(v.nullish(v.array(v.nullable(v.string()))))), + e: v.nullish(v.array(v.array(v.nullable(v.string())))), + f: v.array(v.array(v.nullable(v.string()))) + }) + } + " + `); + }) + it('ref input object', async () => { + const schema = buildSchema(/* GraphQL */ ` + input AInput { + b: BInput! + } + input BInput { + c: CInput! + } + input CInput { + a: AInput! + } + `); + const scalars = undefined + const result = await plugin(schema, [], { schema: 'valibot', scalars }, {}); + expect(result.content).toMatchInlineSnapshot(` + " + + export function AInputSchema(): v.GenericSchema { + return v.object({ + b: v.lazy(() => BInputSchema()) + }) + } + + export function BInputSchema(): v.GenericSchema { + return v.object({ + c: v.lazy(() => CInputSchema()) + }) + } + + export function CInputSchema(): v.GenericSchema { + return v.object({ + a: v.lazy(() => AInputSchema()) + }) + } + " + `); + }) + it.todo('nested input object') + it('enum', async () => { + const schema = buildSchema(/* GraphQL */ ` + enum PageType { + PUBLIC + BASIC_AUTH + } + input PageInput { + pageType: PageType! + } + `); + const scalars = undefined + const result = await plugin(schema, [], { schema: 'valibot', scalars }, {}); + expect(result.content).toMatchInlineSnapshot(` + " + export const PageTypeSchema = v.enum_(PageType); + + export function PageInputSchema(): v.GenericSchema { + return v.object({ + pageType: PageTypeSchema + }) + } + " + `); + }) + it('camelcase', async () => { + const schema = buildSchema(/* GraphQL */ ` + input HTTPInput { + method: HTTPMethod + url: URL! + } + enum HTTPMethod { + GET + POST + } + scalar URL # unknown scalar, should be any + `); + const scalars = undefined + const result = await plugin(schema, [], { schema: 'valibot', scalars }, {}); + expect(result.content).toMatchInlineSnapshot(` + " + export const HttpMethodSchema = v.enum_(HttpMethod); + + export function HttpInputSchema(): v.GenericSchema { + return v.object({ + method: v.nullish(HttpMethodSchema), + url: v.any() + }) + } + " + `); + }) + it('with scalars', async () => { + const schema = buildSchema(/* GraphQL */ ` + input Say { + phrase: Text! + times: Count! + } + scalar Count + scalar Text + `); + const result = await plugin( + schema, + [], + { + schema: 'valibot', + scalars: { + Text: 'string', + Count: 'number', + }, + }, + {}, + ); + expect(result.content).toMatchInlineSnapshot(` + " + + export function SaySchema(): v.GenericSchema { + return v.object({ + phrase: v.string(), + times: v.number() + }) + } + " + `); + }); + it('with importFrom', async () => { + const schema = buildSchema(/* GraphQL */ ` + input Say { + phrase: String! + } + `); + const result = await plugin( + schema, + [], + { + schema: 'valibot', + importFrom: './types', + }, + {}, + ); + expect(result.prepend).toMatchInlineSnapshot(` + [ + "import * as v from 'valibot'", + "import { Say } from './types'", + ] + `); + expect(result.content).toMatchInlineSnapshot(` + " + + export function SaySchema(): v.GenericSchema { + return v.object({ + phrase: v.string() + }) + } + " + `); + }); + it('with importFrom & useTypeImports', async () => { + const schema = buildSchema(/* GraphQL */ ` + input Say { + phrase: String! + } + `); + const result = await plugin( + schema, + [], + { + schema: 'valibot', + importFrom: './types', + useTypeImports: true, + }, + {}, + ); + expect(result.prepend).toMatchInlineSnapshot(` + [ + "import * as v from 'valibot'", + "import type { Say } from './types'", + ] + `); + expect(result.content).toMatchInlineSnapshot(` + " + + export function SaySchema(): v.GenericSchema { + return v.object({ + phrase: v.string() + }) + } + " + `); + }); + it('with enumsAsTypes', async () => { + const schema = buildSchema(/* GraphQL */ ` + enum PageType { + PUBLIC + BASIC_AUTH + } + `); + const result = await plugin( + schema, + [], + { + schema: 'valibot', + enumsAsTypes: true, + }, + {}, + ); + expect(result.content).toMatchInlineSnapshot(` + " + export const PageTypeSchema = v.picklist([\'PUBLIC\', \'BASIC_AUTH\']); + " + `); + }); + it.todo('with notAllowEmptyString') + it.todo('with notAllowEmptyString issue #386') + it('with scalarSchemas', async () => { + const schema = buildSchema(/* GraphQL */ ` + input ScalarsInput { + date: Date! + email: Email + str: String! + } + scalar Date + scalar Email + `); + const result = await plugin( + schema, + [], + { + schema: 'valibot', + scalarSchemas: { + Date: 'v.date()', + Email: 'v.string([v.email()])', + }, + }, + {}, + ); + expect(result.content).toMatchInlineSnapshot(` + " + + export function ScalarsInputSchema(): v.GenericSchema { + return v.object({ + date: v.date(), + email: v.nullish(v.string([v.email()])), + str: v.string() + }) + } + " + `) + }); + it.todo('with typesPrefix') + it.todo('with typesSuffix') + it.todo('with default input values') + describe('issues #19', () => { + it('string field', async () => { + const schema = buildSchema(/* GraphQL */ ` + input UserCreateInput { + profile: String @constraint(minLength: 1, maxLength: 5000) + } + directive @constraint(minLength: Int!, maxLength: Int!) on INPUT_FIELD_DEFINITION + `); + const result = await plugin( + schema, + [], + { + schema: 'valibot', + directives: { + constraint: { + minLength: ['minLength', '$1', 'Please input more than $1'], + maxLength: ['maxLength', '$1', 'Please input less than $1'], + }, + }, + }, + {}, + ); + expect(result.content).toMatchInlineSnapshot(` + " + + export function UserCreateInputSchema(): v.GenericSchema { + return v.object({ + profile: v.nullish(v.pipe(v.string(), v.minLength(1, "Please input more than 1"), v.maxLength(5000, "Please input less than 5000"))) + }) + } + " + `) + }); + + it('not null field', async () => { + const schema = buildSchema(/* GraphQL */ ` + input UserCreateInput { + profile: String! @constraint(minLength: 1, maxLength: 5000) + } + directive @constraint(minLength: Int!, maxLength: Int!) on INPUT_FIELD_DEFINITION + `); + const result = await plugin( + schema, + [], + { + schema: 'valibot', + directives: { + constraint: { + minLength: ['minLength', '$1', 'Please input more than $1'], + maxLength: ['maxLength', '$1', 'Please input less than $1'], + }, + }, + }, + {}, + ); + + expect(result.content).toMatchInlineSnapshot(` + " + + export function UserCreateInputSchema(): v.GenericSchema { + return v.object({ + profile: v.pipe(v.string(), v.minLength(1, "Please input more than 1"), v.maxLength(5000, "Please input less than 5000")) + }) + } + " + `) + }); + it.todo('list field') + describe('pR #112', () => { + it.todo('with notAllowEmptyString') + it.todo('without notAllowEmptyString') + }) + describe('with withObjectType', () => { + it('not generate if withObjectType false', async () => { + const schema = buildSchema(/* GraphQL */ ` + type User { + id: ID! + name: String + } + `); + const result = await plugin( + schema, + [], + { + schema: 'valibot', + }, + {}, + ); + expect(result.content).not.toContain('export function UserSchema(): v.GenericSchema'); + }); + it('generate object type contains object type', async () => { + const schema = buildSchema(/* GraphQL */ ` + type Book { + author: Author + title: String + } + + type Author { + books: [Book] + name: String + } + `); + const result = await plugin( + schema, + [], + { + schema: 'valibot', + withObjectType: true, + }, + {}, + ); + expect(result.content).toMatchInlineSnapshot(` + " + + export function BookSchema(): v.GenericSchema { + return v.object({ + __typename: v.optional(v.literal('Book')), + author: v.nullish(AuthorSchema()), + title: v.nullish(v.string()) + }) + } + + export function AuthorSchema(): v.GenericSchema { + return v.object({ + __typename: v.optional(v.literal('Author')), + books: v.nullish(v.array(v.nullable(BookSchema()))), + name: v.nullish(v.string()) + }) + } + " + `) + + for (const wantNotContain of ['Query', 'Mutation', 'Subscription']) + expect(result.content).not.toContain(wantNotContain); + }); + it('generate both input & type', async () => { + const schema = buildSchema(/* GraphQL */ ` + scalar Date + scalar Email + input UserCreateInput { + name: String! + date: Date! + email: Email! + } + input UsernameUpdateInput { + updateInputId: ID! + updateName: String! + } + type User { + id: ID! + name: String + age: Int + email: Email + isMember: Boolean + createdAt: Date! + } + + type Mutation { + _empty: String + } + + type Query { + _empty: String + } + + type Subscription { + _empty: String + } + `); + const result = await plugin( + schema, + [], + { + schema: 'valibot', + withObjectType: true, + scalarSchemas: { + Date: 'v.date()', + Email: 'v.pipe(v.string(), v.email())', + }, + scalars: { + ID: { + input: 'number', + output: 'string', + }, + }, + }, + {}, + ); + expect(result.content).toMatchInlineSnapshot(` + " + + export function UserCreateInputSchema(): v.GenericSchema { + return v.object({ + name: v.string(), + date: v.date(), + email: v.pipe(v.string(), v.email()) + }) + } + + export function UsernameUpdateInputSchema(): v.GenericSchema { + return v.object({ + updateInputId: v.number(), + updateName: v.string() + }) + } + + export function UserSchema(): v.GenericSchema { + return v.object({ + __typename: v.optional(v.literal('User')), + id: v.string(), + name: v.nullish(v.string()), + age: v.nullish(v.number()), + email: v.nullish(v.pipe(v.string(), v.email())), + isMember: v.nullish(v.boolean()), + createdAt: v.date() + }) + } + " + `) + + for (const wantNotContain of ['Query', 'Mutation', 'Subscription']) + expect(result.content).not.toContain(wantNotContain); + }); + }) + it('generate union types', async () => { + const schema = buildSchema(/* GraphQL */ ` + type Square { + size: Int + } + type Circle { + radius: Int + } + union Shape = Circle | Square + `); + + const result = await plugin( + schema, + [], + { + schema: 'valibot', + withObjectType: true, + }, + {}, + ); + + expect(result.content).toMatchInlineSnapshot(` + " + + export function SquareSchema(): v.GenericSchema { + return v.object({ + __typename: v.optional(v.literal('Square')), + size: v.nullish(v.number()) + }) + } + + export function CircleSchema(): v.GenericSchema { + return v.object({ + __typename: v.optional(v.literal('Circle')), + radius: v.nullish(v.number()) + }) + } + + export function ShapeSchema() { + return v.union([CircleSchema(), SquareSchema()]) + } + " + `) + }); + }) + it('generate union types with single element', async () => { + const schema = buildSchema(/* GraphQL */ ` + type Square { + size: Int + } + type Circle { + radius: Int + } + union Shape = Circle | Square + + type Geometry { + shape: Shape + } + `); + + const result = await plugin( + schema, + [], + { + schema: 'valibot', + withObjectType: true, + }, + {}, + ); + + expect(result.content).toMatchInlineSnapshot(` + " + + export function SquareSchema(): v.GenericSchema { + return v.object({ + __typename: v.optional(v.literal('Square')), + size: v.nullish(v.number()) + }) + } + + export function CircleSchema(): v.GenericSchema { + return v.object({ + __typename: v.optional(v.literal('Circle')), + radius: v.nullish(v.number()) + }) + } + + export function ShapeSchema() { + return v.union([CircleSchema(), SquareSchema()]) + } + + export function GeometrySchema(): v.GenericSchema { + return v.object({ + __typename: v.optional(v.literal('Geometry')), + shape: v.nullish(ShapeSchema()) + }) + } + " + `) + }); + it('correctly reference generated union types', async () => { + const schema = buildSchema(/* GraphQL */ ` + type Circle { + radius: Int + } + union Shape = Circle + `); + + const result = await plugin( + schema, + [], + { + schema: 'valibot', + withObjectType: true, + }, + {}, + ); + + expect(result.content).toMatchInlineSnapshot(` + " + + export function CircleSchema(): v.GenericSchema { + return v.object({ + __typename: v.optional(v.literal('Circle')), + radius: v.nullish(v.number()) + }) + } + + export function ShapeSchema() { + return CircleSchema() + } + " + `) + }); + it('generate enum union types', async () => { + const schema = buildSchema(/* GraphQL */ ` + enum PageType { + PUBLIC + BASIC_AUTH + } + + enum MethodType { + GET + POST + } + + union AnyType = PageType | MethodType + `); + + const result = await plugin( + schema, + [], + { + schema: 'valibot', + withObjectType: true, + }, + {}, + ); + + expect(result.content).toMatchInlineSnapshot(` + " + export const PageTypeSchema = v.enum_(PageType); + + export const MethodTypeSchema = v.enum_(MethodType); + + export function AnyTypeSchema() { + return v.union([PageTypeSchema, MethodTypeSchema]) + } + " + `) + }); + it.todo('generate union types with single element, export as const') + it('with object arguments', async () => { + const schema = buildSchema(/* GraphQL */ ` + type MyType { + foo(a: String, b: Int!, c: Boolean, d: Float!, e: Text): String + } + scalar Text + `); + const result = await plugin( + schema, + [], + { + schema: 'valibot', + withObjectType: true, + scalars: { + Text: 'string', + }, + }, + {}, + ); + expect(result.content).toMatchInlineSnapshot(` + " + + export function MyTypeSchema(): v.GenericSchema { + return v.object({ + __typename: v.optional(v.literal('MyType')), + foo: v.nullish(v.string()) + }) + } + + export function MyTypeFooArgsSchema(): v.GenericSchema { + return v.object({ + a: v.nullish(v.string()), + b: v.number(), + c: v.nullish(v.boolean()), + d: v.number(), + e: v.nullish(v.string()) + }) + } + " + `) + }); + describe('with InterfaceType', () => { + it('not generate if withObjectType false', async () => { + const schema = buildSchema(/* GraphQL */ ` + interface User { + id: ID! + name: String + } + `); + const result = await plugin( + schema, + [], + { + schema: 'valibot', + withObjectType: false, + }, + {}, + ); + expect(result.content).not.toContain('export function UserSchema(): v.GenericSchema'); + }); + it('generate if withObjectType true', async () => { + const schema = buildSchema(/* GraphQL */ ` + interface Book { + title: String + } + `); + const result = await plugin( + schema, + [], + { + schema: 'valibot', + withObjectType: true, + }, + {}, + ); + expect(result.content).toMatchInlineSnapshot(` + " + + export function BookSchema(): v.GenericSchema { + return v.object({ + title: v.nullish(v.string()) + }) + } + " + `) + }); + it('generate interface type contains interface type', async () => { + const schema = buildSchema(/* GraphQL */ ` + interface Book { + author: Author + title: String + } + + interface Author { + books: [Book] + name: String + } + `); + const result = await plugin( + schema, + [], + { + schema: 'valibot', + withObjectType: true, + }, + {}, + ); + expect(result.content).toMatchInlineSnapshot(` + " + + export function BookSchema(): v.GenericSchema { + return v.object({ + author: v.nullish(AuthorSchema()), + title: v.nullish(v.string()) + }) + } + + export function AuthorSchema(): v.GenericSchema { + return v.object({ + books: v.nullish(v.array(v.nullable(BookSchema()))), + name: v.nullish(v.string()) + }) + } + " + `) + }); + it('generate object type contains interface type', async () => { + const schema = buildSchema(/* GraphQL */ ` + interface Book { + title: String! + author: Author! + } + + type Textbook implements Book { + title: String! + author: Author! + courses: [String!]! + } + + type ColoringBook implements Book { + title: String! + author: Author! + colors: [String!]! + } + + type Author { + books: [Book!] + name: String + } + `); + const result = await plugin( + schema, + [], + { + schema: 'valibot', + withObjectType: true, + }, + {}, + ); + expect(result.content).toMatchInlineSnapshot(` + " + + export function BookSchema(): v.GenericSchema { + return v.object({ + title: v.string(), + author: AuthorSchema() + }) + } + + export function TextbookSchema(): v.GenericSchema { + return v.object({ + __typename: v.optional(v.literal('Textbook')), + title: v.string(), + author: AuthorSchema(), + courses: v.array(v.string()) + }) + } + + export function ColoringBookSchema(): v.GenericSchema { + return v.object({ + __typename: v.optional(v.literal('ColoringBook')), + title: v.string(), + author: AuthorSchema(), + colors: v.array(v.string()) + }) + } + + export function AuthorSchema(): v.GenericSchema { + return v.object({ + __typename: v.optional(v.literal('Author')), + books: v.nullish(v.array(BookSchema())), + name: v.nullish(v.string()) + }) + } + " + `) + }); + }) + it.todo('properly generates custom directive values') + it.todo('exports as const instead of func') + it.todo('generate both input & type, export as const') + it.todo('issue #394') +})