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/README.md b/example/valibot/README.md new file mode 100644 index 00000000..cc9cb3ad --- /dev/null +++ b/example/valibot/README.md @@ -0,0 +1,4 @@ +# Tips for valibot schema + +## How to overwrite generated schema? + diff --git a/example/valibot/schemas.ts b/example/valibot/schemas.ts new file mode 100644 index 00000000..03022972 --- /dev/null +++ b/example/valibot/schemas.ts @@ -0,0 +1,104 @@ +import * as v from 'valibot' +import { AttributeInput, ButtonComponentType, ComponentInput, DropDownComponentInput, EventArgumentInput, EventInput, EventOptionType, HttpInput, HttpMethod, LayoutInput, PageInput, PageType } 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 AttributeInputSchema() { + 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() { + 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")) + }) +} + +type Infer1 = v.InferOutput>; + +const ESchema = v.object({ + name: v.pipe(v.string(), v.minLength(5)), + value: v.pipe(v.string(), v.regex(/^foo/, "message")) +}); + +type Infer2 = v.InferOutput; + +// const obj = v.object({ value: v.pipe(v.string(), v.minLength(5)) }) +// // const obj = v.pipe(v.string(), v.minLength(5)); +// type Infer1 = v.InferOutput; + +// // const str2: v.ObjectSchema, v.MinLengthAction]> }>, undefined> = v.object({ value: v.pipe(v.string(), v.minLength(5)) }) +// // const str2: v.ObjectSchema, '_types' | '_run'> }>, undefined> = v.object({ value: v.pipe(v.string(), v.minLength(5)) }) +// // const str2: v.GenericSchema = v.pipe(v.string(), v.minLength(5)); +// const obj2: v.GenericSchema<{ value: string }> = v.object({ value: v.pipe(v.string(), v.minLength(5)) }) +// type Infer2 = v.InferOutput; + +// 型が一致しているか確認するためのチェック +export const typeCheck: Infer1 extends Infer2 + ? Infer2 extends Infer1 + ? true + : false + : false = true; + + +export function EventInputSchema() { + return v.object({ + arguments: v.array(v.lazy(() => EventArgumentInputSchema())), + options: v.nullish(v.array(EventOptionTypeSchema)) + }) +} + +export function HttpInputSchema() { + return v.object({ + method: v.nullish(HttpMethodSchema), + url: v.any() + }) +} + +export function LayoutInputSchema() { + return v.object({ + dropdown: v.lazy(() => v.nullish(DropDownComponentInputSchema())) + }) +} + +export function PageInputSchema() { + 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() + }) +} diff --git a/package.json b/package.json index 5dbc7616..c2318c87 100644 --- a/package.json +++ b/package.json @@ -81,6 +81,7 @@ "ts-dedent": "^2.2.0", "ts-jest": "29.1.3", "typescript": "5.4.5", + "valibot": "0.31.0-rc.5", "vitest": "^1.0.0", "yup": "1.4.0", "zod": "3.23.8" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d334b2f9..44007210 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.5 + version: 0.31.0-rc.5 vitest: specifier: ^1.0.0 version: 1.6.0(@types/node@20.12.12) @@ -3715,6 +3718,9 @@ packages: resolution: {integrity: sha512-74Y4LqY74kLE6IFyIjPtkSTWzUZmj8tdHT9Ii/26dvQ6K9Dl2NbEfj0XgU2sHCtKgt5VupqhlO/5aWuqS+IY1w==} engines: {node: '>=10.12.0'} + valibot@0.31.0-rc.5: + resolution: {integrity: sha512-yv1koEWTUIajUkMW9QVUoZFF2b8cAS6EOZpZTIme6QXXH8K+Tl+b/1yen/VIyqVOf8WAZaYdqAzzU2ccF5CJfQ==} + validate-npm-package-license@3.0.4: resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} @@ -8426,6 +8432,8 @@ snapshots: '@types/istanbul-lib-coverage': 2.0.4 convert-source-map: 1.9.0 + valibot@0.31.0-rc.5: {} + 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 71683287..23196442 100644 --- a/src/directive.ts +++ b/src/directive.ts @@ -115,11 +115,44 @@ export function buildApi(config: FormattedDirectiveConfig, directives: ReadonlyA .map((directive) => { const directiveName = directive.name.value; const argsConfig = config[directiveName]; - return buildApiFromDirectiveArguments(argsConfig, directive.arguments ?? []); + return buildApiFromDirectiveArguments(argsConfig, directive.arguments ?? []).join(''); }) .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 ''; @@ -132,7 +165,7 @@ function buildApiSchema(validationSchema: string[] | undefined, argValue: ConstV return `.${schemaApi}(${schemaApiArgs.join(', ')})`; } -function buildApiFromDirectiveArguments(config: FormattedDirectiveArguments, args: ReadonlyArray): string { +function buildApiFromDirectiveArguments(config: FormattedDirectiveArguments, args: ReadonlyArray): string[] { return args .map((arg) => { const argName = arg.name.value; @@ -142,7 +175,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..c631a3f9 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/index'; 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..e0cf197f --- /dev/null +++ b/src/valibot/index.ts @@ -0,0 +1,215 @@ +import { DeclarationBlock, indent } from '@graphql-codegen/visitor-plugin-common'; +import type { + EnumTypeDefinitionNode, + FieldDefinitionNode, + GraphQLSchema, + InputObjectTypeDefinitionNode, + InputValueDefinitionNode, + NameNode, + TypeNode, +} from 'graphql'; +import { + Kind, +} from 'graphql'; + +import type { ValidationSchemaPluginConfig } from '../config'; +import { buildApiForValibot, formatDirectiveConfig } from '../directive'; +import { BaseSchemaVisitor } from '../schema_visitor'; +import type { Visitor } from '../visitor'; +import { + 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 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, + ); + }, + }; + } + + 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) { + case 'const': + throw new Error('not implemented'); + case 'function': + default: + return new DeclarationBlock({}) + .export() + .asKind('function') + .withName(`${name}Schema()`) + .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)) { + let gen = generateNameNodeValibotSchema(config, visitor, type.name); + if (isListType(parentType)) + return `v.nullable(${gen})`; + + if (field.kind === Kind.INPUT_VALUE_DEFINITION) { + const { defaultValue } = field; + if (defaultValue?.kind === Kind.INT || defaultValue?.kind === Kind.FLOAT || defaultValue?.kind === Kind.BOOLEAN) + gen = `v.optional(${gen}, ${defaultValue.value})`; + + if (defaultValue?.kind === Kind.STRING || defaultValue?.kind === Kind.ENUM) + gen = `v.optional(${gen}, "${defaultValue.value}")`; + } + + const actions = actionsFromDirectives(config, field); + + if (isNonNullType(parentType)) { + if (visitor.shouldEmitAsNotAllowEmptyString(type.name.value)) { + actions.push('v.minLength(1)'); + } + + 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 'EnumTypeDefinition': + return `${converter.convertName()}Schema`; + case 'ScalarTypeDefinition': + return valibot4Scalar(config, visitor, node.value); + case 'InterfaceTypeDefinition': + case 'InputObjectTypeDefinition': + case 'ObjectTypeDefinition': + case 'UnionTypeDefinition': + // using switch-case rather than if-else to allow for future expansion + switch (config.validationSchemaExportType) { + case 'const': + case 'function': + default: + return `${converter.convertName()}Schema()`; + } + 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..c40e55e3 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, @@ -352,7 +353,7 @@ describe('format directive config', () => { config: FormattedDirectiveArguments args: ReadonlyArray } - want: string + want: string[] }[] = [ { name: 'string', @@ -364,7 +365,7 @@ describe('format directive config', () => { msg: `"hello"`, }), }, - want: `.required("hello")`, + want: [`.required("hello")`], }, { name: 'string with additional stuff', @@ -376,7 +377,7 @@ describe('format directive config', () => { startWith: `"hello"`, }), }, - want: `.matched("^hello")`, + want: [`.matched("^hello")`], }, { name: 'number', @@ -388,7 +389,7 @@ describe('format directive config', () => { minLength: `1`, }), }, - want: `.min(1)`, + want: [`.min(1)`], }, { name: 'boolean', @@ -401,7 +402,7 @@ describe('format directive config', () => { enabled: `true`, }), }, - want: `.strict(true)`, + want: [`.strict(true)`], }, { name: 'list', @@ -413,7 +414,7 @@ describe('format directive config', () => { minLength: `[1, "message"]`, }), }, - want: `.min(1, "message")`, + want: [`.min(1, "message")`], }, { name: 'object in list', @@ -425,7 +426,7 @@ describe('format directive config', () => { matches: `["hello", {message:"message", excludeEmptyString:true}]`, }), }, - want: `.matches("hello", {"message":"message","excludeEmptyString":true})`, + want: [`.matches("hello", {"message":"message","excludeEmptyString":true})`], }, { name: 'two arguments but matched to first argument', @@ -438,7 +439,7 @@ describe('format directive config', () => { msg2: `"world"`, }), }, - want: `.required("hello")`, + want: [`.required("hello")`, ``], }, { name: 'two arguments but matched to second argument', @@ -451,7 +452,7 @@ describe('format directive config', () => { msg2: `"world"`, }), }, - want: `.required("world")`, + want: [``, `.required("world")`], }, { name: 'two arguments matched all', @@ -465,7 +466,7 @@ describe('format directive config', () => { minLength: `1`, }), }, - want: `.required("message").min(1)`, + want: [`.required("message")`, `.min(1)`], }, { name: 'argument matches validation schema api', @@ -479,7 +480,7 @@ describe('format directive config', () => { format: `"uri"`, }), }, - want: `.url()`, + want: [`.url()`], }, { name: 'argument matched argument but doesn\'t match api', @@ -493,7 +494,7 @@ describe('format directive config', () => { format: `"uuid"`, }), }, - want: ``, + want: [``], }, { name: 'complex', @@ -509,7 +510,7 @@ describe('format directive config', () => { format: `"uri"`, }), }, - want: `.required("message").url()`, + want: [`.required("message")`, `.url()`], }, { name: 'complex 2', @@ -525,7 +526,7 @@ describe('format directive config', () => { format: `"uuid"`, }), }, - want: `.required("message")`, + want: [`.required("message")`, ``], }, ]; for (const tc of cases) { @@ -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..d36d582c --- /dev/null +++ b/tests/valibot.spec.ts @@ -0,0 +1,516 @@ +import { buildSchema } from 'graphql'; + +import { plugin } from '../src/index'; + +describe('valibot', () => { + it.each([ + [ + 'non-null and defined', + { + textSchema: /* GraphQL */ ` + input PrimitiveInput { + a: ID! + b: String! + c: Boolean! + d: Int! + e: Float! + } + `, + wantContains: [ + 'export function PrimitiveInputSchema()', + 'a: v.string(),', + 'b: v.string(),', + 'c: v.boolean(),', + 'd: v.number(),', + 'e: v.number()', + ], + scalars: { + ID: 'string', + }, + }, + ], + [ + 'nullish', + { + textSchema: /* GraphQL */ ` + input PrimitiveInput { + a: ID + b: String + c: Boolean + d: Int + e: Float + z: String! # no defined check + } + `, + wantContains: [ + 'export function PrimitiveInputSchema()', + // alphabet order + '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()),', + ], + scalars: { + ID: 'string', + }, + }, + ], + [ + 'array', + { + textSchema: /* GraphQL */ ` + input ArrayInput { + a: [String] + b: [String!] + c: [String!]! + d: [[String]] + e: [[String]!] + f: [[String]!]! + } + `, + wantContains: [ + 'export function ArrayInputSchema()', + '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())))', + ], + scalars: undefined, + }, + ], + ])('%s', async (_, { textSchema, wantContains, scalars }) => { + const schema = buildSchema(textSchema); + const result = await plugin(schema, [], { schema: 'valibot', scalars }, {}); + expect(result.prepend).toContain('import * as v from \'valibot\''); + + for (const wantContain of wantContains) + expect(result.content).toContain(wantContain); + }); + + 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() { + return v.object({ + b: v.lazy(() => BInputSchema()) + }) + } + + export function BInputSchema() { + return v.object({ + c: v.lazy(() => CInputSchema()) + }) + } + + export function CInputSchema() { + 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() { + 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() { + 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() { + return v.object({ + phrase: v.string(), + times: v.number() + }) + } + " + `); + }); + + it.todo('with importFrom'); + it.todo('with importFrom & useTypeImports') + + 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('with notAllowEmptyString', async () => { + const schema = buildSchema(/* GraphQL */ ` + input PrimitiveInput { + a: ID! + b: String! + c: Boolean! + d: Int! + e: Float! + } + `); + const result = await plugin( + schema, + [], + { + schema: 'valibot', + notAllowEmptyString: true, + scalars: { + ID: 'string', + }, + }, + {}, + ); + expect(result.content).toMatchInlineSnapshot(` + " + + export function PrimitiveInputSchema() { + return v.object({ + a: v.pipe(v.string(), v.minLength(1)), + b: v.pipe(v.string(), v.minLength(1)), + c: v.boolean(), + d: v.number(), + e: v.number() + }) + } + " + `) + }); + + it('with notAllowEmptyString issue #386', async () => { + const schema = buildSchema(/* GraphQL */ ` + input InputOne { + field: InputNested! + } + + input InputNested { + field: String! + } + `); + const result = await plugin( + schema, + [], + { + schema: 'valibot', + notAllowEmptyString: true, + scalars: { + ID: 'string', + }, + }, + {}, + ); + expect(result.content).toMatchInlineSnapshot(` + " + + export function InputOneSchema() { + return v.object({ + field: v.lazy(() => InputNestedSchema()) + }) + } + + export function InputNestedSchema() { + return v.object({ + field: v.pipe(v.string(), v.minLength(1)) + }) + } + " + `); + }); + + 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() { + 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('with default input values', async () => { + const schema = buildSchema(/* GraphQL */ ` + enum PageType { + PUBLIC + BASIC_AUTH + } + input PageInput { + pageType: PageType! = PUBLIC + greeting: String = "Hello" + score: Int = 100 + ratio: Float = 0.5 + isMember: Boolean = true + } + `); + const result = await plugin( + schema, + [], + { + schema: 'valibot', + importFrom: './types', + }, + {}, + ); + + expect(result.content).toMatchInlineSnapshot(` + " + export const PageTypeSchema = v.enum_(PageType); + + export function PageInputSchema() { + return v.object({ + pageType: v.optional(PageTypeSchema, "PUBLIC"), + greeting: v.nullish(v.optional(v.string(), "Hello")), + score: v.nullish(v.optional(v.number(), 100)), + ratio: v.nullish(v.optional(v.number(), 0.5)), + isMember: v.nullish(v.optional(v.boolean(), true)) + }) + } + " + `) + }); + + 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() { + 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() { + 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('with notAllowEmptyString', async () => { + const schema = buildSchema(/* GraphQL */ ` + input UserCreateInput { + profile: String! @constraint(maxLength: 5000) + age: Int! + } + + directive @constraint(maxLength: Int!) on INPUT_FIELD_DEFINITION + `); + const result = await plugin( + schema, + [], + { + schema: 'valibot', + notAllowEmptyString: true, + directives: { + constraint: { + maxLength: ['maxLength', '$1', 'Please input less than $1'], + }, + }, + }, + {}, + ); + expect(result.content).toMatchInlineSnapshot(` + " + + export function UserCreateInputSchema() { + return v.object({ + profile: v.pipe(v.string(), v.maxLength(5000, "Please input less than 5000"), v.minLength(1)), + age: v.number() + }) + } + " + `) + }); + }); +})