graphql-codegen-typescript-validation-schema
Version:
GraphQL Code Generator plugin to generate form validation schema from your GraphQL schema
229 lines (228 loc) • 10.5 kB
JavaScript
import { DeclarationBlock, indent } from '@graphql-codegen/visitor-plugin-common';
import { buildApiForValibot, formatDirectiveConfig } from '../directive.js';
import { InterfaceTypeDefinitionBuilder, isListType, isNamedType, isNonNullType, ObjectTypeDefinitionBuilder, } from '../graphql.js';
import { buildMaybeLazy } from '../lazy.js';
import { buildScalarSchema } from '../scalar.js';
import { BaseSchemaVisitor } from '../schema_visitor.js';
export class ValibotSchemaVisitor extends BaseSchemaVisitor {
constructor(schema, config) {
super(schema, config);
}
importValidationSchema() {
return `import * as v from 'valibot'`;
}
initialEmit() {
return (`\n${[
...this.enumDeclarations,
].join('\n')}`);
}
get InputObjectTypeDefinition() {
return {
leave: (node) => {
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) => {
const visitor = this.createVisitor('output');
const name = visitor.convertName(node.name.value);
const typeName = visitor.prefixTypeNamespace(name);
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<${typeName}>`)
.withBlock([indent(`return v.object({`), shape, indent('})')].join('\n'))
.string + appendArguments);
}
}),
};
}
get ObjectTypeDefinition() {
return {
leave: ObjectTypeDefinitionBuilder(this.config.withObjectType, (node) => {
const visitor = this.createVisitor('output');
const name = visitor.convertName(node.name.value);
const typeName = visitor.prefixTypeNamespace(name);
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<${typeName}>`)
.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) => {
const visitor = this.createVisitor('both');
const enumname = visitor.convertSchemaName(node.name.value, node.kind);
const enumTypeName = visitor.prefixTypeNamespace(enumname);
this.importTypes.push(enumname);
if (!this.config.enumsAsTypes)
this.importValueTypes.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_(${enumTypeName})`)
.string);
},
};
}
get UnionTypeDefinition() {
return {
leave: (node) => {
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;
}
},
};
}
buildInputFields(fields, visitor, name) {
const typeName = visitor.prefixTypeNamespace(name);
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<${typeName}>`)
.withBlock([indent(`return v.object({`), shape, indent('})')].join('\n'))
.string;
}
}
}
function generateFieldValibotSchema(config, visitor, field, indentCount) {
const gen = generateFieldTypeValibotSchema(config, visitor, field, field.type);
return indent(`${field.name.value}: ${gen}`, indentCount);
}
function generateFieldTypeValibotSchema(config, visitor, field, type, parentType) {
if (isListType(type)) {
const gen = generateFieldTypeValibotSchema(config, visitor, field, type.type, type);
const arrayGen = `v.array(${gen})`;
if (!isNonNullType(parentType))
return `v.nullish(${arrayGen})`;
return arrayGen;
}
if (isNonNullType(type)) {
const gen = generateFieldTypeValibotSchema(config, visitor, field, type.type, type);
return gen;
}
if (isNamedType(type)) {
const gen = generateNameNodeValibotSchema(config, visitor, type.name);
if (isListType(parentType))
return `v.nullable(${maybeLazy(visitor, type, gen)})`;
const actions = actionsFromDirectives(config, field);
const schema = maybeLazy(visitor, type, pipeSchemaAndActions(gen, actions));
if (isNonNullType(parentType)) {
if (visitor.shouldEmitAsNotAllowEmptyString(type.name.value)) {
actions.push('v.minLength(1)');
}
return maybeLazy(visitor, type, pipeSchemaAndActions(gen, actions));
}
return `v.nullish(${schema})`;
}
console.warn('unhandled type:', type);
return '';
}
function actionsFromDirectives(config, field) {
if (config.directives && field.directives) {
const formatted = formatDirectiveConfig(config.directives);
return buildApiForValibot(formatted, field.directives);
}
return [];
}
function pipeSchemaAndActions(schema, actions) {
if (actions.length === 0)
return schema;
return `v.pipe(${schema}, ${actions.join(', ')})`;
}
function generateNameNodeValibotSchema(config, visitor, node) {
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(visitor, type, schema) {
return buildMaybeLazy(visitor, type, schema, s => `v.lazy(() => ${s})`);
}
function valibot4Scalar(config, visitor, scalarName) {
return buildScalarSchema(config, visitor, scalarName, {
typeMap: { string: 'v.string()', number: 'v.number()', boolean: 'v.boolean()' },
fallback: 'v.any()',
});
}