graphql-codegen-typescript-validation-schema
Version:
GraphQL Code Generator plugin to generate form validation schema from your GraphQL schema
211 lines (210 loc) • 10 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildZodOperationSchemas = buildZodOperationSchemas;
const visitor_plugin_common_1 = require("@graphql-codegen/visitor-plugin-common");
const graphql_1 = require("graphql");
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 === graphql_1.Kind.FRAGMENT_DEFINITION)
fragments.set(definition.name.value, definition);
else if (definition.kind === graphql_1.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 visitor_plugin_common_1.DeclarationBlock({})
.export()
.asKind('const')
.withName(`${typeName}Schema: z.ZodType<${typeReference}>`)
.withContent(schemaExpression)
.string;
case 'function':
default:
return new visitor_plugin_common_1.DeclarationBlock({})
.export()
.asKind('function')
.withName(`${typeName}Schema(): z.ZodType<${typeReference}>`)
.withBlock((0, visitor_plugin_common_1.indent)(`return ${schemaExpression}`))
.string;
}
}
function buildSelectionSetSchema(ctx, parentType, selectionSet) {
if ((0, graphql_1.isUnionType)(parentType) || (0, graphql_1.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 => (0, visitor_plugin_common_1.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]) => (0, visitor_plugin_common_1.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 graphql_1.Kind.FIELD:
collectField(ctx, parentType, runtimeType, selection, fields, optional);
return;
case graphql_1.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 graphql_1.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 (!(0, graphql_1.isObjectType)(parentType) && !(0, graphql_1.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 ((0, graphql_1.isNonNullType)(type))
return buildNonNullOutputTypeSchema(ctx, type.ofType, selectionSet);
return `${buildNonNullOutputTypeSchema(ctx, type, selectionSet)}.nullable()`;
}
function buildNonNullOutputTypeSchema(ctx, type, selectionSet) {
if ((0, graphql_1.isListType)(type))
return `z.array(${buildOutputTypeSchema(ctx, type.ofType, selectionSet)})`;
const namedType = (0, graphql_1.getNamedType)(type);
if ((0, graphql_1.isScalarType)(namedType))
return scalarSchema(ctx, namedType.name);
if ((0, graphql_1.isEnumType)(namedType)) {
const converter = ctx.visitor.getNameNodeConverter({ kind: graphql_1.Kind.NAME, value: namedType.name });
return `${converter?.convertName() ?? ctx.visitor.convertSchemaName(namedType.name, namedType.astNode?.kind)}Schema`;
}
if (selectionSet && ((0, graphql_1.isObjectType)(namedType) || (0, graphql_1.isInterfaceType)(namedType) || (0, graphql_1.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 graphql_1.GraphQLString.name:
case graphql_1.GraphQLID.name:
return 'z.string()';
case graphql_1.GraphQLInt.name:
case graphql_1.GraphQLFloat.name:
return 'z.number()';
case graphql_1.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 && ((0, graphql_1.isObjectType)(type) || (0, graphql_1.isInterfaceType)(type) || (0, graphql_1.isUnionType)(type)) ? type : undefined;
}
function typesOverlap(ctx, conditionalType, runtimeType) {
if ((0, graphql_1.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 !== graphql_1.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);
}