Skip to content

Commit 706996a

Browse files
committed
feat: implement non-null and defined
1 parent 2aceab3 commit 706996a

File tree

4 files changed

+165
-1
lines changed

4 files changed

+165
-1
lines changed

src/config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { TypeScriptPluginConfig } from '@graphql-codegen/typescript';
22

3-
export type ValidationSchema = 'yup' | 'zod' | 'myzod';
3+
export type ValidationSchema = 'yup' | 'zod' | 'myzod' | 'valibot';
44
export type ValidationSchemaExportType = 'function' | 'const';
55

66
export interface DirectiveConfig {

src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { MyZodSchemaVisitor } from './myzod/index';
99
import type { SchemaVisitor } from './types';
1010
import { YupSchemaVisitor } from './yup/index';
1111
import { ZodSchemaVisitor } from './zod/index';
12+
import { ValibotSchemaVisitor } from './valibot/index';
1213

1314
export const plugin: PluginFunction<ValidationSchemaPluginConfig, Types.ComplexPluginOutput> = (
1415
schema: GraphQLSchema,
@@ -33,6 +34,8 @@ function schemaVisitor(schema: GraphQLSchema, config: ValidationSchemaPluginConf
3334
return new ZodSchemaVisitor(schema, config);
3435
else if (config?.schema === 'myzod')
3536
return new MyZodSchemaVisitor(schema, config);
37+
else if (config?.schema === 'valibot')
38+
return new ValibotSchemaVisitor(schema, config);
3639

3740
return new YupSchemaVisitor(schema, config);
3841
}

src/valibot/index.ts

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import { DeclarationBlock, indent } from '@graphql-codegen/visitor-plugin-common';
2+
import type {
3+
FieldDefinitionNode,
4+
GraphQLSchema,
5+
InputObjectTypeDefinitionNode,
6+
InputValueDefinitionNode,
7+
NameNode,
8+
TypeNode,
9+
} from 'graphql';
10+
11+
import type { ValidationSchemaPluginConfig } from '../config';
12+
import { BaseSchemaVisitor } from '../schema_visitor';
13+
import type { Visitor } from '../visitor';
14+
import {
15+
isNamedType,
16+
isNonNullType,
17+
} from './../graphql';
18+
19+
const anySchema = `definedNonNullAnySchema`;
20+
21+
export class ValibotSchemaVisitor extends BaseSchemaVisitor {
22+
constructor(schema: GraphQLSchema, config: ValidationSchemaPluginConfig) {
23+
super(schema, config);
24+
}
25+
26+
importValidationSchema(): string {
27+
return `import * as v from 'valibot'`;
28+
}
29+
30+
initialEmit(): string {
31+
return '';
32+
}
33+
34+
get InputObjectTypeDefinition() {
35+
return {
36+
leave: (node: InputObjectTypeDefinitionNode) => {
37+
const visitor = this.createVisitor('input');
38+
const name = visitor.convertName(node.name.value);
39+
this.importTypes.push(name);
40+
return this.buildInputFields(node.fields ?? [], visitor, name);
41+
},
42+
};
43+
}
44+
45+
protected buildInputFields(
46+
fields: readonly (FieldDefinitionNode | InputValueDefinitionNode)[],
47+
visitor: Visitor,
48+
name: string,
49+
) {
50+
const shape = fields.map(field => generateFieldValibotSchema(this.config, visitor, field, 2)).join(',\n');
51+
52+
switch (this.config.validationSchemaExportType) {
53+
case 'const':
54+
throw new Error('not implemented');
55+
case 'function':
56+
default:
57+
return new DeclarationBlock({})
58+
.export()
59+
.asKind('function')
60+
.withName(`${name}Schema()`)
61+
.withBlock([indent(`return v.object({`), shape, indent('})')].join('\n')).string;
62+
}
63+
}
64+
}
65+
66+
function generateFieldValibotSchema(config: ValidationSchemaPluginConfig, visitor: Visitor, field: InputValueDefinitionNode | FieldDefinitionNode, indentCount: number): string {
67+
const gen = generateFieldTypeValibotSchema(config, visitor, field, field.type);
68+
return indent(`${field.name.value}: ${maybeLazy(field.type, gen)}`, indentCount);
69+
}
70+
71+
function generateFieldTypeValibotSchema(config: ValidationSchemaPluginConfig, visitor: Visitor, field: InputValueDefinitionNode | FieldDefinitionNode, type: TypeNode, parentType?: TypeNode): string {
72+
if (isNonNullType(type)) {
73+
const gen = generateFieldTypeValibotSchema(config, visitor, field, type.type, type);
74+
return maybeLazy(type.type, gen);
75+
}
76+
if (isNamedType(type)) {
77+
const gen = generateNameNodeValibotSchema(config, visitor, type.name);
78+
79+
if (isNonNullType(parentType))
80+
return gen;
81+
}
82+
console.warn('unhandled type:', type);
83+
return '';
84+
}
85+
86+
function generateNameNodeValibotSchema(config: ValidationSchemaPluginConfig, visitor: Visitor, node: NameNode): string {
87+
const converter = visitor.getNameNodeConverter(node);
88+
89+
switch (converter?.targetKind) {
90+
case 'InterfaceTypeDefinition':
91+
case 'InputObjectTypeDefinition':
92+
case 'ObjectTypeDefinition':
93+
case 'UnionTypeDefinition':
94+
case 'EnumTypeDefinition':
95+
case 'ScalarTypeDefinition':
96+
throw new Error('not implemented');
97+
default:
98+
if (converter?.targetKind)
99+
console.warn('Unknown targetKind', converter?.targetKind);
100+
101+
return valibot4Scalar(config, visitor, node.value);
102+
}
103+
}
104+
105+
function maybeLazy(type: TypeNode, schema: string): string {
106+
return schema;
107+
}
108+
109+
function valibot4Scalar(config: ValidationSchemaPluginConfig, visitor: Visitor, scalarName: string): string {
110+
const tsType = visitor.getScalarType(scalarName);
111+
switch (tsType) {
112+
case 'string':
113+
return `v.string()`;
114+
case 'number':
115+
return `v.number()`;
116+
case 'boolean':
117+
return `v.boolean()`;
118+
}
119+
console.warn('unhandled scalar name:', scalarName);
120+
return anySchema;
121+
}

tests/valibot.spec.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { buildSchema } from 'graphql';
2+
3+
import { plugin } from '../src/index';
4+
5+
describe('valibot', () => {
6+
it.each([
7+
[
8+
'non-null and defined',
9+
{
10+
textSchema: /* GraphQL */ `
11+
input PrimitiveInput {
12+
a: ID!
13+
b: String!
14+
c: Boolean!
15+
d: Int!
16+
e: Float!
17+
}
18+
`,
19+
wantContains: [
20+
'export function PrimitiveInputSchema()',
21+
'a: v.string(),',
22+
'b: v.string(),',
23+
'c: v.boolean(),',
24+
'd: v.number(),',
25+
'e: v.number()',
26+
],
27+
scalars: {
28+
ID: 'string',
29+
},
30+
},
31+
],
32+
])('%s', async (_, { textSchema, wantContains, scalars }) => {
33+
const schema = buildSchema(textSchema);
34+
const result = await plugin(schema, [], { schema: 'valibot', scalars }, {});
35+
expect(result.prepend).toContain('import * as v from \'valibot\'');
36+
37+
for (const wantContain of wantContains)
38+
expect(result.content).toContain(wantContain);
39+
});
40+
})

0 commit comments

Comments
 (0)