graphql-codegen-typescript-validation-schema
Version:
GraphQL Code Generator plugin to generate form validation schema from your GraphQL schema
208 lines (207 loc) • 9.58 kB
JavaScript
import { DeclarationBlock, indent } from '@graphql-codegen/visitor-plugin-common';
import { getNamedType, GraphQLBoolean, GraphQLFloat, GraphQLID, GraphQLInt, GraphQLString, isEnumType, isInterfaceType, isListType, isNonNullType, isObjectType, isScalarType, isUnionType, Kind, } from 'graphql';
export function buildZodOperationSchemas(schema, config, documents, visitor) {
const fragments = new Map();
const operations = [];
for (const documentFile of documents) {
for (const definition of documentFile.document?.definitions ?? []) {
if (definition.kind === Kind.FRAGMENT_DEFINITION)
fragments.set(definition.name.value, definition);
else if (definition.kind === Kind.OPERATION_DEFINITION && definition.name)
operations.push(definition);
}
}
const ctx = { schema, config, visitor, fragments };
const blocks = [];
const typeNames = [];
for (const operation of operations) {
const rootType = operationRootType(schema, operation);
if (!rootType)
continue;
const typeName = operationResultTypeName(visitor, operation);
typeNames.push(typeName);
const schemaExpression = buildSelectionSetSchema(ctx, rootType, operation.selectionSet);
blocks.push(operationDeclaration(config, typeName, visitor.prefixTypeNamespace(typeName), schemaExpression));
}
return { blocks, typeNames };
}
function operationRootType(schema, operation) {
switch (operation.operation) {
case 'query':
return schema.getQueryType() ?? undefined;
case 'mutation':
return schema.getMutationType() ?? undefined;
case 'subscription':
return schema.getSubscriptionType() ?? undefined;
}
}
function operationResultTypeName(visitor, operation) {
const operationName = operation.name?.value ?? '';
return visitor.convertName(`${operationName}${capitalize(operation.operation)}`);
}
function operationDeclaration(config, typeName, typeReference, schemaExpression) {
switch (config.validationSchemaExportType) {
case 'const':
return new DeclarationBlock({})
.export()
.asKind('const')
.withName(`${typeName}Schema: z.ZodType<${typeReference}>`)
.withContent(schemaExpression)
.string;
case 'function':
default:
return new DeclarationBlock({})
.export()
.asKind('function')
.withName(`${typeName}Schema(): z.ZodType<${typeReference}>`)
.withBlock(indent(`return ${schemaExpression}`))
.string;
}
}
function buildSelectionSetSchema(ctx, parentType, selectionSet) {
if (isUnionType(parentType) || isInterfaceType(parentType)) {
const variants = ctx.schema
.getPossibleTypes(parentType)
.map(type => buildObjectSelectionSetSchema(ctx, parentType, type, selectionSet, false));
return variants.length > 1
? `z.union([\n${variants.map(variant => indent(variant, 2)).join(',\n')}\n])`
: variants[0] ?? 'definedNonNullAnySchema';
}
return buildObjectSelectionSetSchema(ctx, parentType, parentType, selectionSet, false);
}
function buildObjectSelectionSetSchema(ctx, parentType, runtimeType, selectionSet, optional) {
const fields = new Map();
if (ctx.config.nonOptionalTypename === true)
setField(fields, '__typename', typenameSchema(runtimeType), false);
for (const selection of selectionSet.selections)
collectSelection(ctx, parentType, runtimeType, selection, fields, optional);
return buildObjectExpression(ctx.config, [...fields.entries()]
.map(([responseName, field]) => indent(`${responseName}: ${field.optional ? `${field.schema}.optional()` : field.schema}`, 2))
.join(',\n'));
}
function collectSelection(ctx, parentType, runtimeType, selection, fields, inheritedOptional) {
const directiveState = executableDirectiveState(selection);
if (directiveState === 'omit')
return;
const optional = inheritedOptional || directiveState === 'optional';
switch (selection.kind) {
case Kind.FIELD:
collectField(ctx, parentType, runtimeType, selection, fields, optional);
return;
case Kind.FRAGMENT_SPREAD: {
const fragment = ctx.fragments.get(selection.name.value);
const fragmentType = fragment ? fragmentTypeCondition(ctx, fragment) : undefined;
if (fragment && fragmentType && typesOverlap(ctx, fragmentType, runtimeType))
collectSelectionSetForType(ctx, fragmentType, runtimeType, fragment.selectionSet, fields, optional);
return;
}
case Kind.INLINE_FRAGMENT: {
const fragmentType = selection.typeCondition ? typeCondition(ctx, selection.typeCondition.name.value) : parentType;
if (fragmentType && typesOverlap(ctx, fragmentType, runtimeType))
collectSelectionSetForType(ctx, fragmentType, runtimeType, selection.selectionSet, fields, optional);
}
}
}
function collectSelectionSetForType(ctx, parentType, runtimeType, selectionSet, fields, optional) {
for (const selection of selectionSet.selections)
collectSelection(ctx, parentType, runtimeType, selection, fields, optional);
}
function collectField(ctx, parentType, runtimeType, field, fields, optional) {
const responseName = field.alias?.value ?? field.name.value;
if (field.name.value === '__typename') {
setField(fields, responseName, typenameSchema(runtimeType), optional);
return;
}
if (!isObjectType(parentType) && !isInterfaceType(parentType))
return;
const fieldDefinition = parentType.getFields()[field.name.value];
if (!fieldDefinition)
return;
setField(fields, responseName, buildOutputTypeSchema(ctx, fieldDefinition.type, field.selectionSet), optional);
}
function buildOutputTypeSchema(ctx, type, selectionSet) {
if (isNonNullType(type))
return buildNonNullOutputTypeSchema(ctx, type.ofType, selectionSet);
return `${buildNonNullOutputTypeSchema(ctx, type, selectionSet)}.nullable()`;
}
function buildNonNullOutputTypeSchema(ctx, type, selectionSet) {
if (isListType(type))
return `z.array(${buildOutputTypeSchema(ctx, type.ofType, selectionSet)})`;
const namedType = getNamedType(type);
if (isScalarType(namedType))
return scalarSchema(ctx, namedType.name);
if (isEnumType(namedType)) {
const converter = ctx.visitor.getNameNodeConverter({ kind: Kind.NAME, value: namedType.name });
return `${converter?.convertName() ?? ctx.visitor.convertSchemaName(namedType.name, namedType.astNode?.kind)}Schema`;
}
if (selectionSet && (isObjectType(namedType) || isInterfaceType(namedType) || isUnionType(namedType)))
return buildSelectionSetSchema(ctx, namedType, selectionSet);
return 'definedNonNullAnySchema';
}
function scalarSchema(ctx, scalarName) {
if (ctx.config.scalarSchemas?.[scalarName])
return ctx.config.scalarSchemas[scalarName];
switch (scalarName) {
case GraphQLString.name:
case GraphQLID.name:
return 'z.string()';
case GraphQLInt.name:
case GraphQLFloat.name:
return 'z.number()';
case GraphQLBoolean.name:
return 'z.boolean()';
}
if (ctx.config.defaultScalarTypeSchema)
return ctx.config.defaultScalarTypeSchema;
console.warn('unhandled scalar name:', scalarName);
return 'definedNonNullAnySchema';
}
function fragmentTypeCondition(ctx, fragment) {
return typeCondition(ctx, fragment.typeCondition.name.value);
}
function typeCondition(ctx, typeName) {
const type = ctx.schema.getType(typeName);
return type && (isObjectType(type) || isInterfaceType(type) || isUnionType(type)) ? type : undefined;
}
function typesOverlap(ctx, conditionalType, runtimeType) {
if (isObjectType(conditionalType))
return conditionalType.name === runtimeType.name;
return ctx.schema.isSubType(conditionalType, runtimeType);
}
function setField(fields, responseName, schema, optional) {
const existing = fields.get(responseName);
if (existing && !existing.optional)
return;
fields.set(responseName, { schema, optional });
}
function typenameSchema(parentType) {
return `z.literal('${parentType.name}')`;
}
function executableDirectiveState(selection) {
let optional = false;
for (const directive of selection.directives ?? []) {
if (directive.name.value !== 'skip' && directive.name.value !== 'include')
continue;
const condition = directive.arguments?.find(argument => argument.name.value === 'if')?.value;
if (!condition)
continue;
if (condition.kind !== Kind.BOOLEAN) {
optional = true;
continue;
}
if (directive.name.value === 'skip' && condition.value)
return 'omit';
if (directive.name.value === 'include' && !condition.value)
return 'omit';
}
return optional ? 'optional' : 'required';
}
function buildObjectExpression(config, shape) {
return ['z.object({', shape, `})${strictObjectSuffix(config)}`].join('\n');
}
function strictObjectSuffix(config) {
return config.strictObjectSchemas === true ? '.strict()' : '';
}
function capitalize(value) {
return value.charAt(0).toUpperCase() + value.slice(1);
}