UNPKG

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
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); }