From 14f86e2695e7b7bf44ead69425c48d8ba91f0c2a Mon Sep 17 00:00:00 2001 From: MH4GF Date: Sat, 25 May 2024 22:45:05 +0900 Subject: [PATCH 01/18] chore: add valibot to devDependencies --- package.json | 1 + pnpm-lock.yaml | 8 ++++++++ 2 files changed, 9 insertions(+) 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 From 812a7ab4726841f653e072e62de4bb56095082f8 Mon Sep 17 00:00:00 2001 From: MH4GF Date: Sat, 25 May 2024 23:34:42 +0900 Subject: [PATCH 02/18] feat: implement non-null and defined --- src/config.ts | 2 +- src/index.ts | 3 ++ src/valibot/index.ts | 121 ++++++++++++++++++++++++++++++++++++++++++ tests/valibot.spec.ts | 40 ++++++++++++++ 4 files changed, 165 insertions(+), 1 deletion(-) create mode 100644 src/valibot/index.ts create mode 100644 tests/valibot.spec.ts 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/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..6d7286e6 --- /dev/null +++ b/src/valibot/index.ts @@ -0,0 +1,121 @@ +import { DeclarationBlock, indent } from '@graphql-codegen/visitor-plugin-common'; +import type { + FieldDefinitionNode, + GraphQLSchema, + InputObjectTypeDefinitionNode, + InputValueDefinitionNode, + NameNode, + TypeNode, +} from 'graphql'; + +import type { ValidationSchemaPluginConfig } from '../config'; +import { BaseSchemaVisitor } from '../schema_visitor'; +import type { Visitor } from '../visitor'; +import { + isNamedType, + isNonNullType, +} from './../graphql'; + +const anySchema = `definedNonNullAnySchema`; + +export class ValibotSchemaVisitor extends BaseSchemaVisitor { + constructor(schema: GraphQLSchema, config: ValidationSchemaPluginConfig) { + super(schema, config); + } + + importValidationSchema(): string { + return `import * as v from 'valibot'`; + } + + initialEmit(): string { + return ''; + } + + 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); + }, + }; + } + + 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 (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 (isNonNullType(parentType)) + return gen; + } + console.warn('unhandled type:', type); + return ''; +} + +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': + case 'EnumTypeDefinition': + case 'ScalarTypeDefinition': + throw new Error('not implemented'); + default: + if (converter?.targetKind) + console.warn('Unknown targetKind', converter?.targetKind); + + return valibot4Scalar(config, visitor, node.value); + } +} + +function maybeLazy(type: TypeNode, schema: string): string { + return schema; +} + +function valibot4Scalar(config: ValidationSchemaPluginConfig, visitor: Visitor, scalarName: string): string { + 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 anySchema; +} diff --git a/tests/valibot.spec.ts b/tests/valibot.spec.ts new file mode 100644 index 00000000..8f8b5889 --- /dev/null +++ b/tests/valibot.spec.ts @@ -0,0 +1,40 @@ +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', + }, + }, + ], + ])('%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); + }); +}) From 7e106612a20fc9e9fe86658825fff7ec3f9d0235 Mon Sep 17 00:00:00 2001 From: MH4GF Date: Sat, 25 May 2024 23:41:38 +0900 Subject: [PATCH 03/18] feat: implement nullish --- src/valibot/index.ts | 2 ++ tests/valibot.spec.ts | 27 +++++++++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/src/valibot/index.ts b/src/valibot/index.ts index 6d7286e6..d8093460 100644 --- a/src/valibot/index.ts +++ b/src/valibot/index.ts @@ -78,6 +78,8 @@ function generateFieldTypeValibotSchema(config: ValidationSchemaPluginConfig, vi if (isNonNullType(parentType)) return gen; + + return `v.nullish(${gen})`; } console.warn('unhandled type:', type); return ''; diff --git a/tests/valibot.spec.ts b/tests/valibot.spec.ts index 8f8b5889..8fcd4685 100644 --- a/tests/valibot.spec.ts +++ b/tests/valibot.spec.ts @@ -29,6 +29,33 @@ describe('valibot', () => { }, }, ], + [ + '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', + }, + }, + ], ])('%s', async (_, { textSchema, wantContains, scalars }) => { const schema = buildSchema(textSchema); const result = await plugin(schema, [], { schema: 'valibot', scalars }, {}); From 2e49647f0469be7355b7a6fcad2b753968efd840 Mon Sep 17 00:00:00 2001 From: MH4GF Date: Sat, 25 May 2024 23:54:47 +0900 Subject: [PATCH 04/18] feat: implement array --- src/valibot/index.ts | 12 ++++++++++++ tests/valibot.spec.ts | 25 +++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/src/valibot/index.ts b/src/valibot/index.ts index d8093460..e0050b56 100644 --- a/src/valibot/index.ts +++ b/src/valibot/index.ts @@ -12,6 +12,7 @@ import type { ValidationSchemaPluginConfig } from '../config'; import { BaseSchemaVisitor } from '../schema_visitor'; import type { Visitor } from '../visitor'; import { + isListType, isNamedType, isNonNullType, } from './../graphql'; @@ -69,12 +70,23 @@ function generateFieldValibotSchema(config: ValidationSchemaPluginConfig, visito } 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})`; if (isNonNullType(parentType)) return gen; diff --git a/tests/valibot.spec.ts b/tests/valibot.spec.ts index 8fcd4685..38cec87d 100644 --- a/tests/valibot.spec.ts +++ b/tests/valibot.spec.ts @@ -56,6 +56,31 @@ describe('valibot', () => { }, }, ], + [ + '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 }, {}); From 6985192be082610331fe25f115322dd34b763934 Mon Sep 17 00:00:00 2001 From: MH4GF Date: Sun, 26 May 2024 09:26:06 +0900 Subject: [PATCH 05/18] feat: support enum --- src/valibot/index.ts | 29 +++++++++++++++++++++++++++-- tests/valibot.spec.ts | 28 ++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/src/valibot/index.ts b/src/valibot/index.ts index e0050b56..2cdbed03 100644 --- a/src/valibot/index.ts +++ b/src/valibot/index.ts @@ -1,5 +1,6 @@ import { DeclarationBlock, indent } from '@graphql-codegen/visitor-plugin-common'; import type { + EnumTypeDefinitionNode, FieldDefinitionNode, GraphQLSchema, InputObjectTypeDefinitionNode, @@ -29,7 +30,11 @@ export class ValibotSchemaVisitor extends BaseSchemaVisitor { } initialEmit(): string { - return ''; + return ( + `\n${[ + ...this.enumDeclarations, + ].join('\n')}` + ); } get InputObjectTypeDefinition() { @@ -43,6 +48,25 @@ export class ValibotSchemaVisitor extends BaseSchemaVisitor { }; } + 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( + new DeclarationBlock({}) + .export() + .asKind('const') + .withName(`${enumname}Schema`) + .withContent(`v.enum_(${enumname})`).string, + ); + }, + }; + } + protected buildInputFields( fields: readonly (FieldDefinitionNode | InputValueDefinitionNode)[], visitor: Visitor, @@ -101,11 +125,12 @@ function generateNameNodeValibotSchema(config: ValidationSchemaPluginConfig, vis const converter = visitor.getNameNodeConverter(node); switch (converter?.targetKind) { + case 'EnumTypeDefinition': + return `${converter.convertName()}Schema`; case 'InterfaceTypeDefinition': case 'InputObjectTypeDefinition': case 'ObjectTypeDefinition': case 'UnionTypeDefinition': - case 'EnumTypeDefinition': case 'ScalarTypeDefinition': throw new Error('not implemented'); default: diff --git a/tests/valibot.spec.ts b/tests/valibot.spec.ts index 38cec87d..379471d1 100644 --- a/tests/valibot.spec.ts +++ b/tests/valibot.spec.ts @@ -89,4 +89,32 @@ describe('valibot', () => { for (const wantContain of wantContains) expect(result.content).toContain(wantContain); }); + + it.todo('ref input object') + 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 + }) + } + " + `); + }) }) From 482f89d2dac9c634b8f5a0abbca969c1a69a4a78 Mon Sep 17 00:00:00 2001 From: MH4GF Date: Sun, 26 May 2024 10:41:01 +0900 Subject: [PATCH 06/18] feat: support camelcase --- src/valibot/index.ts | 7 +++---- tests/valibot.spec.ts | 34 ++++++++++++++++++++++++++++++++-- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/src/valibot/index.ts b/src/valibot/index.ts index 2cdbed03..9029a2af 100644 --- a/src/valibot/index.ts +++ b/src/valibot/index.ts @@ -18,8 +18,6 @@ import { isNonNullType, } from './../graphql'; -const anySchema = `definedNonNullAnySchema`; - export class ValibotSchemaVisitor extends BaseSchemaVisitor { constructor(schema: GraphQLSchema, config: ValidationSchemaPluginConfig) { super(schema, config); @@ -127,11 +125,12 @@ function generateNameNodeValibotSchema(config: ValidationSchemaPluginConfig, vis 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': - case 'ScalarTypeDefinition': throw new Error('not implemented'); default: if (converter?.targetKind) @@ -156,5 +155,5 @@ function valibot4Scalar(config: ValidationSchemaPluginConfig, visitor: Visitor, return `v.boolean()`; } console.warn('unhandled scalar name:', scalarName); - return anySchema; + return 'v.any()'; } diff --git a/tests/valibot.spec.ts b/tests/valibot.spec.ts index 379471d1..b9ce35ba 100644 --- a/tests/valibot.spec.ts +++ b/tests/valibot.spec.ts @@ -105,7 +105,7 @@ describe('valibot', () => { `); const scalars = undefined const result = await plugin(schema, [], { schema: 'valibot', scalars }, {}); - expect(result.content).toMatchInlineSnapshot(` + expect(result.content).toMatchInlineSnapshot(` " export const PageTypeSchema = v.enum_(PageType); @@ -117,4 +117,34 @@ describe('valibot', () => { " `); }) -}) + + 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() + }) + } + " + `); + }) +}) \ No newline at end of file From 2eefd7aef3bd0224ca26896d0fb9ad300d48db9e Mon Sep 17 00:00:00 2001 From: MH4GF Date: Sun, 26 May 2024 10:45:16 +0900 Subject: [PATCH 07/18] feat: add test for scalars --- tests/valibot.spec.ts | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/tests/valibot.spec.ts b/tests/valibot.spec.ts index b9ce35ba..4ffd4fa8 100644 --- a/tests/valibot.spec.ts +++ b/tests/valibot.spec.ts @@ -147,4 +147,39 @@ describe('valibot', () => { " `); }) + + 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() + }) + } + " + `); + }); }) \ No newline at end of file From a3bbea584221c9e4b7dd55f02006af25dbc3c759 Mon Sep 17 00:00:00 2001 From: MH4GF Date: Sun, 26 May 2024 10:49:45 +0900 Subject: [PATCH 08/18] feat: supprt enumAsTypes --- src/valibot/index.ts | 17 ++++++++++++----- tests/valibot.spec.ts | 27 +++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/src/valibot/index.ts b/src/valibot/index.ts index 9029a2af..f6da0f59 100644 --- a/src/valibot/index.ts +++ b/src/valibot/index.ts @@ -55,11 +55,18 @@ export class ValibotSchemaVisitor extends BaseSchemaVisitor { // hoist enum declarations this.enumDeclarations.push( - new DeclarationBlock({}) - .export() - .asKind('const') - .withName(`${enumname}Schema`) - .withContent(`v.enum_(${enumname})`).string, + 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, ); }, }; diff --git a/tests/valibot.spec.ts b/tests/valibot.spec.ts index 4ffd4fa8..120f0502 100644 --- a/tests/valibot.spec.ts +++ b/tests/valibot.spec.ts @@ -182,4 +182,31 @@ describe('valibot', () => { " `); }); + + 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\']); + " + `); + }); + }) \ No newline at end of file From 87470d09794c5928550adaf8dd8e2341108ef7f2 Mon Sep 17 00:00:00 2001 From: MH4GF Date: Sun, 26 May 2024 10:59:40 +0900 Subject: [PATCH 09/18] feat: support notAllowEmptyString option --- src/valibot/index.ts | 6 +++++- tests/valibot.spec.ts | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/valibot/index.ts b/src/valibot/index.ts index f6da0f59..6951d9c1 100644 --- a/src/valibot/index.ts +++ b/src/valibot/index.ts @@ -117,8 +117,12 @@ function generateFieldTypeValibotSchema(config: ValidationSchemaPluginConfig, vi if (isListType(parentType)) return `v.nullable(${gen})`; - if (isNonNullType(parentType)) + if (isNonNullType(parentType)) { + if (visitor.shouldEmitAsNotAllowEmptyString(type.name.value)) + return "v.string([v.minLength(1)])"; // TODO + return gen; + } return `v.nullish(${gen})`; } diff --git a/tests/valibot.spec.ts b/tests/valibot.spec.ts index 120f0502..a46bac77 100644 --- a/tests/valibot.spec.ts +++ b/tests/valibot.spec.ts @@ -209,4 +209,41 @@ describe('valibot', () => { `); }); + 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.string([v.minLength(1)]), + b: v.string([v.minLength(1)]), + c: v.boolean(), + d: v.number(), + e: v.number() + }) + } + " + `) + }); }) \ No newline at end of file From 86dd7e0cca184d1fc7df3993f8f770fb8b40fc18 Mon Sep 17 00:00:00 2001 From: MH4GF Date: Sun, 26 May 2024 11:02:16 +0900 Subject: [PATCH 10/18] feat: support notAllowEmptyString issue #386 --- src/valibot/index.ts | 8 +++++++- tests/valibot.spec.ts | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/src/valibot/index.ts b/src/valibot/index.ts index 6951d9c1..18637374 100644 --- a/src/valibot/index.ts +++ b/src/valibot/index.ts @@ -142,7 +142,13 @@ function generateNameNodeValibotSchema(config: ValidationSchemaPluginConfig, vis case 'InputObjectTypeDefinition': case 'ObjectTypeDefinition': case 'UnionTypeDefinition': - throw new Error('not implemented'); + // 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); diff --git a/tests/valibot.spec.ts b/tests/valibot.spec.ts index a46bac77..8e470a99 100644 --- a/tests/valibot.spec.ts +++ b/tests/valibot.spec.ts @@ -246,4 +246,44 @@ describe('valibot', () => { " `) }); + + 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: InputNestedSchema() + }) + } + + export function InputNestedSchema() { + return v.object({ + field: v.string([v.minLength(1)]) + }) + } + " + `); + }); }) \ No newline at end of file From 5e63ae4951140c1ed38592b41bfb3d8803be6985 Mon Sep 17 00:00:00 2001 From: MH4GF Date: Sun, 26 May 2024 11:05:58 +0900 Subject: [PATCH 11/18] support scalarSchema --- src/valibot/index.ts | 3 +++ tests/valibot.spec.ts | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/src/valibot/index.ts b/src/valibot/index.ts index 18637374..674c57d2 100644 --- a/src/valibot/index.ts +++ b/src/valibot/index.ts @@ -162,6 +162,9 @@ function maybeLazy(type: TypeNode, schema: string): string { } 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': diff --git a/tests/valibot.spec.ts b/tests/valibot.spec.ts index 8e470a99..7bced805 100644 --- a/tests/valibot.spec.ts +++ b/tests/valibot.spec.ts @@ -286,4 +286,40 @@ describe('valibot', () => { " `); }); + + 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() + }) + } + " + `) + }); }) \ No newline at end of file From f0712a17f255c29a1251497d6ae2fc203bfc3273 Mon Sep 17 00:00:00 2001 From: MH4GF Date: Sun, 26 May 2024 11:17:01 +0900 Subject: [PATCH 12/18] feat: support default input values --- src/valibot/index.ts | 28 +++++++++++++++++--------- tests/valibot.spec.ts | 46 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 64 insertions(+), 10 deletions(-) diff --git a/src/valibot/index.ts b/src/valibot/index.ts index 674c57d2..7dfe69e6 100644 --- a/src/valibot/index.ts +++ b/src/valibot/index.ts @@ -1,12 +1,13 @@ import { DeclarationBlock, indent } from '@graphql-codegen/visitor-plugin-common'; -import type { - EnumTypeDefinitionNode, - FieldDefinitionNode, - GraphQLSchema, - InputObjectTypeDefinitionNode, - InputValueDefinitionNode, - NameNode, - TypeNode, +import { + Kind, + type EnumTypeDefinitionNode, + type FieldDefinitionNode, + type GraphQLSchema, + type InputObjectTypeDefinitionNode, + type InputValueDefinitionNode, + type NameNode, + type TypeNode, } from 'graphql'; import type { ValidationSchemaPluginConfig } from '../config'; @@ -113,10 +114,19 @@ function generateFieldTypeValibotSchema(config: ValidationSchemaPluginConfig, vi return maybeLazy(type.type, gen); } if (isNamedType(type)) { - const gen = generateNameNodeValibotSchema(config, visitor, type.name); + 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}")`; + + } if (isNonNullType(parentType)) { if (visitor.shouldEmitAsNotAllowEmptyString(type.name.value)) return "v.string([v.minLength(1)])"; // TODO diff --git a/tests/valibot.spec.ts b/tests/valibot.spec.ts index 7bced805..36b24995 100644 --- a/tests/valibot.spec.ts +++ b/tests/valibot.spec.ts @@ -322,4 +322,48 @@ describe('valibot', () => { " `) }); -}) \ No newline at end of file + + 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)) + }) + } + " + `) + }); +}) From f85c96e341b6ed0a27fdc83b985a6920a98181c0 Mon Sep 17 00:00:00 2001 From: MH4GF Date: Sun, 26 May 2024 12:17:39 +0900 Subject: [PATCH 13/18] refactor: change buildApiFromDirectiveArguments return type to string[] --- src/directive.ts | 5 ++--- tests/directive.spec.ts | 28 ++++++++++++++-------------- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/src/directive.ts b/src/directive.ts index 71683287..25f9895e 100644 --- a/src/directive.ts +++ b/src/directive.ts @@ -115,7 +115,7 @@ 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('') } @@ -132,7 +132,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 +142,6 @@ function buildApiFromDirectiveArguments(config: FormattedDirectiveArguments, arg return buildApiSchema(validationSchema, arg.value); }) - .join(''); } function buildApiFromDirectiveObjectArguments(config: FormattedDirectiveObjectArguments, argValue: ConstValueNode): string { diff --git a/tests/directive.spec.ts b/tests/directive.spec.ts index 25bf1658..a17ce01a 100644 --- a/tests/directive.spec.ts +++ b/tests/directive.spec.ts @@ -352,7 +352,7 @@ describe('format directive config', () => { config: FormattedDirectiveArguments args: ReadonlyArray } - want: string + want: string[] }[] = [ { name: 'string', @@ -364,7 +364,7 @@ describe('format directive config', () => { msg: `"hello"`, }), }, - want: `.required("hello")`, + want: [`.required("hello")`], }, { name: 'string with additional stuff', @@ -376,7 +376,7 @@ describe('format directive config', () => { startWith: `"hello"`, }), }, - want: `.matched("^hello")`, + want: [`.matched("^hello")`], }, { name: 'number', @@ -388,7 +388,7 @@ describe('format directive config', () => { minLength: `1`, }), }, - want: `.min(1)`, + want: [`.min(1)`], }, { name: 'boolean', @@ -401,7 +401,7 @@ describe('format directive config', () => { enabled: `true`, }), }, - want: `.strict(true)`, + want: [`.strict(true)`], }, { name: 'list', @@ -413,7 +413,7 @@ describe('format directive config', () => { minLength: `[1, "message"]`, }), }, - want: `.min(1, "message")`, + want: [`.min(1, "message")`], }, { name: 'object in list', @@ -425,7 +425,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 +438,7 @@ describe('format directive config', () => { msg2: `"world"`, }), }, - want: `.required("hello")`, + want: [`.required("hello")`, ``], }, { name: 'two arguments but matched to second argument', @@ -451,7 +451,7 @@ describe('format directive config', () => { msg2: `"world"`, }), }, - want: `.required("world")`, + want: [``, `.required("world")`], }, { name: 'two arguments matched all', @@ -465,7 +465,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 +479,7 @@ describe('format directive config', () => { format: `"uri"`, }), }, - want: `.url()`, + want: [`.url()`], }, { name: 'argument matched argument but doesn\'t match api', @@ -493,7 +493,7 @@ describe('format directive config', () => { format: `"uuid"`, }), }, - want: ``, + want: [``], }, { name: 'complex', @@ -509,7 +509,7 @@ describe('format directive config', () => { format: `"uri"`, }), }, - want: `.required("message").url()`, + want: [`.required("message")`, `.url()`], }, { name: 'complex 2', @@ -525,7 +525,7 @@ describe('format directive config', () => { format: `"uuid"`, }), }, - want: `.required("message")`, + want: [`.required("message")`, ``], }, ]; for (const tc of cases) { From 0efd767f27ad5ba7c8d6fe8a63a4ef04e95e8f70 Mon Sep 17 00:00:00 2001 From: MH4GF Date: Sun, 26 May 2024 12:18:41 +0900 Subject: [PATCH 14/18] feat: adds buildApiForValibot() --- src/directive.ts | 33 ++++++++++++++++++++++ tests/directive.spec.ts | 61 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+) diff --git a/src/directive.ts b/src/directive.ts index 25f9895e..23196442 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 ''; diff --git a/tests/directive.spec.ts b/tests/directive.spec.ts index a17ce01a..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, @@ -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); + }); + } + }); }); From 68204e8dff66c11e7fda6da9aa7c23bf9a9fdf62 Mon Sep 17 00:00:00 2001 From: MH4GF Date: Sun, 26 May 2024 12:21:08 +0900 Subject: [PATCH 15/18] feat: support directives --- src/valibot/index.ts | 35 ++++++++++++++------- tests/valibot.spec.ts | 73 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+), 11 deletions(-) diff --git a/src/valibot/index.ts b/src/valibot/index.ts index 7dfe69e6..c7086370 100644 --- a/src/valibot/index.ts +++ b/src/valibot/index.ts @@ -1,16 +1,19 @@ import { DeclarationBlock, indent } from '@graphql-codegen/visitor-plugin-common'; +import type { + EnumTypeDefinitionNode, + FieldDefinitionNode, + GraphQLSchema, + InputObjectTypeDefinitionNode, + InputValueDefinitionNode, + NameNode, + TypeNode, +} from 'graphql'; import { Kind, - type EnumTypeDefinitionNode, - type FieldDefinitionNode, - type GraphQLSchema, - type InputObjectTypeDefinitionNode, - type InputValueDefinitionNode, - type NameNode, - type TypeNode, } from 'graphql'; import type { ValidationSchemaPluginConfig } from '../config'; +import { buildApiForValibot, formatDirectiveConfig } from '../directive'; import { BaseSchemaVisitor } from '../schema_visitor'; import type { Visitor } from '../visitor'; import { @@ -118,28 +121,38 @@ function generateFieldTypeValibotSchema(config: ValidationSchemaPluginConfig, vi if (isListType(parentType)) return `v.nullable(${gen})`; + let appliedDirectivesGen = applyDirectives(config, field, 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})`; + appliedDirectivesGen = `v.optional(${appliedDirectivesGen}, ${defaultValue.value})`; if (defaultValue?.kind === Kind.STRING || defaultValue?.kind === Kind.ENUM) - gen = `v.optional(${gen}, "${defaultValue.value}")`; + appliedDirectivesGen = `v.optional(${appliedDirectivesGen}, "${defaultValue.value}")`; } if (isNonNullType(parentType)) { if (visitor.shouldEmitAsNotAllowEmptyString(type.name.value)) return "v.string([v.minLength(1)])"; // TODO - return gen; + return appliedDirectivesGen; } - return `v.nullish(${gen})`; + return `v.nullish(${appliedDirectivesGen})`; } console.warn('unhandled type:', type); return ''; } +function applyDirectives(config: ValidationSchemaPluginConfig, field: InputValueDefinitionNode | FieldDefinitionNode, gen: string): string { + if (config.directives && field.directives) { + const formatted = formatDirectiveConfig(config.directives); + return `v.pipe(${gen}, ${buildApiForValibot(formatted, field.directives).join(', ')})`; + } + return gen; +} + function generateNameNodeValibotSchema(config: ValidationSchemaPluginConfig, visitor: Visitor, node: NameNode): string { const converter = visitor.getNameNodeConverter(node); diff --git a/tests/valibot.spec.ts b/tests/valibot.spec.ts index 36b24995..31beef05 100644 --- a/tests/valibot.spec.ts +++ b/tests/valibot.spec.ts @@ -366,4 +366,77 @@ describe('valibot', () => { " `) }); + + 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'); + }) }) From 57d022c77b001366c032adcec69d890e1ac3e898 Mon Sep 17 00:00:00 2001 From: MH4GF Date: Sun, 26 May 2024 19:45:49 +0900 Subject: [PATCH 16/18] fix: Change the actions setting method to v.pipe() --- src/valibot/index.ts | 33 ++++++++++++++++++++------------ tests/valibot.spec.ts | 44 ++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 62 insertions(+), 15 deletions(-) diff --git a/src/valibot/index.ts b/src/valibot/index.ts index c7086370..4d142ad2 100644 --- a/src/valibot/index.ts +++ b/src/valibot/index.ts @@ -121,36 +121,45 @@ function generateFieldTypeValibotSchema(config: ValidationSchemaPluginConfig, vi if (isListType(parentType)) return `v.nullable(${gen})`; - let appliedDirectivesGen = applyDirectives(config, field, gen); - if (field.kind === Kind.INPUT_VALUE_DEFINITION) { const { defaultValue } = field; if (defaultValue?.kind === Kind.INT || defaultValue?.kind === Kind.FLOAT || defaultValue?.kind === Kind.BOOLEAN) - appliedDirectivesGen = `v.optional(${appliedDirectivesGen}, ${defaultValue.value})`; + gen = `v.optional(${gen}, ${defaultValue.value})`; if (defaultValue?.kind === Kind.STRING || defaultValue?.kind === Kind.ENUM) - appliedDirectivesGen = `v.optional(${appliedDirectivesGen}, "${defaultValue.value}")`; - + gen = `v.optional(${gen}, "${defaultValue.value}")`; } + + const actions = actionsFromDirectives(config, field); + if (isNonNullType(parentType)) { - if (visitor.shouldEmitAsNotAllowEmptyString(type.name.value)) - return "v.string([v.minLength(1)])"; // TODO + if (visitor.shouldEmitAsNotAllowEmptyString(type.name.value)) { + actions.push('v.minLength(1)'); + } - return appliedDirectivesGen; + return pipeSchemaAndActions(gen, actions); } - return `v.nullish(${appliedDirectivesGen})`; + return `v.nullish(${pipeSchemaAndActions(gen, actions)})`; } console.warn('unhandled type:', type); return ''; } -function applyDirectives(config: ValidationSchemaPluginConfig, field: InputValueDefinitionNode | FieldDefinitionNode, gen: string): string { +function actionsFromDirectives(config: ValidationSchemaPluginConfig, field: InputValueDefinitionNode | FieldDefinitionNode): string[] { if (config.directives && field.directives) { const formatted = formatDirectiveConfig(config.directives); - return `v.pipe(${gen}, ${buildApiForValibot(formatted, field.directives).join(', ')})`; + return buildApiForValibot(formatted, field.directives); } - return gen; + + 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 { diff --git a/tests/valibot.spec.ts b/tests/valibot.spec.ts index 31beef05..84789dba 100644 --- a/tests/valibot.spec.ts +++ b/tests/valibot.spec.ts @@ -236,8 +236,8 @@ describe('valibot', () => { export function PrimitiveInputSchema() { return v.object({ - a: v.string([v.minLength(1)]), - b: v.string([v.minLength(1)]), + 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() @@ -280,7 +280,7 @@ describe('valibot', () => { export function InputNestedSchema() { return v.object({ - field: v.string([v.minLength(1)]) + field: v.pipe(v.string(), v.minLength(1)) }) } " @@ -439,4 +439,42 @@ describe('valibot', () => { 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() + }) + } + " + `) + }); + }); }) From 15da551203a294a5ccdf4f767b9870cb5b3c740a Mon Sep 17 00:00:00 2001 From: MH4GF Date: Sun, 26 May 2024 23:17:52 +0900 Subject: [PATCH 17/18] feat: implement ref input object --- src/valibot/index.ts | 4 ++++ tests/valibot.spec.ts | 40 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/src/valibot/index.ts b/src/valibot/index.ts index 4d142ad2..e0cf197f 100644 --- a/src/valibot/index.ts +++ b/src/valibot/index.ts @@ -17,6 +17,7 @@ import { buildApiForValibot, formatDirectiveConfig } from '../directive'; import { BaseSchemaVisitor } from '../schema_visitor'; import type { Visitor } from '../visitor'; import { + isInput, isListType, isNamedType, isNonNullType, @@ -190,6 +191,9 @@ function generateNameNodeValibotSchema(config: ValidationSchemaPluginConfig, vis } function maybeLazy(type: TypeNode, schema: string): string { + if (isNamedType(type) && isInput(type.name.value)) + return `v.lazy(() => ${schema})`; + return schema; } diff --git a/tests/valibot.spec.ts b/tests/valibot.spec.ts index 84789dba..d36d582c 100644 --- a/tests/valibot.spec.ts +++ b/tests/valibot.spec.ts @@ -90,7 +90,43 @@ describe('valibot', () => { expect(result.content).toContain(wantContain); }); - it.todo('ref input object') + 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 () => { @@ -274,7 +310,7 @@ describe('valibot', () => { export function InputOneSchema() { return v.object({ - field: InputNestedSchema() + field: v.lazy(() => InputNestedSchema()) }) } From c6039fdd18d9b2467e8e084f00b7866fc93494b1 Mon Sep 17 00:00:00 2001 From: MH4GF Date: Wed, 29 May 2024 23:13:21 +0900 Subject: [PATCH 18/18] wip --- codegen.yml | 23 ++++++++ example/valibot/README.md | 4 ++ example/valibot/schemas.ts | 104 +++++++++++++++++++++++++++++++++++++ 3 files changed, 131 insertions(+) create mode 100644 example/valibot/README.md create mode 100644 example/valibot/schemas.ts 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() + }) +}