UNPKG

@graphql-codegen/graphql-modules-preset

Version:

GraphQL Code Generator preset for modularized schema

342 lines (341 loc) • 13.1 kB
import { pascalCase } from 'change-case-all'; import { isScalarType, Kind, visit, } from 'graphql'; import { buildBlock, collectUsedTypes, concatByKey, createObject, indent, pushUnique, unique, uniqueByKey, withQuotes, } from './utils.js'; const registryKeys = ['objects', 'inputs', 'interfaces', 'scalars', 'unions', 'enums']; const resolverKeys = ['scalars', 'objects', 'enums']; export function buildModule(name, doc, { importNamespace, importPath, encapsulate, requireRootResolvers, shouldDeclare, rootTypes, schema, baseVisitor, useGraphQLModules, useTypeImports = false, }) { const picks = createObject(registryKeys, () => ({})); const defined = createObject(registryKeys, () => []); const extended = createObject(registryKeys, () => []); // List of types used in objects, fields, arguments etc const usedTypes = collectUsedTypes(doc); visit(doc, { ObjectTypeDefinition(node) { collectTypeDefinition(node); }, ObjectTypeExtension(node) { collectTypeExtension(node); }, InputObjectTypeDefinition(node) { collectTypeDefinition(node); }, InputObjectTypeExtension(node) { collectTypeExtension(node); }, InterfaceTypeDefinition(node) { collectTypeDefinition(node); }, InterfaceTypeExtension(node) { collectTypeExtension(node); }, ScalarTypeDefinition(node) { collectTypeDefinition(node); }, UnionTypeDefinition(node) { collectTypeDefinition(node); }, UnionTypeExtension(node) { collectTypeExtension(node); }, EnumTypeDefinition(node) { collectTypeDefinition(node); }, EnumTypeExtension(node) { collectTypeExtension(node); }, }); // Defined and Extended types const visited = createObject(registryKeys, key => concatByKey(defined, extended, key)); // Types that are not defined or extended in a module, they come from other modules const external = createObject(registryKeys, key => uniqueByKey(extended, defined, key)); // // // // Prints // // // // An actual output const imports = [`import${useTypeImports ? ' type' : ''} * as ${importNamespace} from "${importPath}";`]; if (useGraphQLModules) { imports.push(`import${useTypeImports ? ' type' : ''} * as gm from "graphql-modules";`); } let content = [ printDefinedFields(), printDefinedEnumValues(), printDefinedInputFields(), printSchemaTypes(usedTypes), printScalars(visited), printResolveSignaturesPerType(visited), printResolversType(visited), useGraphQLModules ? printResolveMiddlewareMap() : undefined, ] .filter(Boolean) .join('\n\n'); if (encapsulate === 'namespace') { content = `${shouldDeclare ? 'declare' : 'export'} namespace ${baseVisitor.convertName(name, { suffix: 'Module', useTypesPrefix: false, useTypesSuffix: false, })} {\n` + (shouldDeclare ? `${indent(2)(imports.join('\n'))}\n` : '') + indent(2)(content) + '\n}'; } return [...(shouldDeclare ? [] : imports), content].filter(Boolean).join('\n'); /** * A dictionary of fields to pick from an object */ function printDefinedFields() { return buildBlock({ name: `interface DefinedFields`, lines: [...visited.objects, ...visited.interfaces].map(typeName => `${typeName}: ${printPicks(typeName, { ...picks.objects, ...picks.interfaces, })};`), }); } /** * A dictionary of values to pick from an enum */ function printDefinedEnumValues() { return buildBlock({ name: `interface DefinedEnumValues`, lines: visited.enums.map(typeName => `${typeName}: ${printPicks(typeName, picks.enums)};`), }); } function encapsulateTypeName(typeName) { if (encapsulate === 'prefix') { return `${pascalCase(name)}_${typeName}`; } return typeName; } /** * A dictionary of fields to pick from an input */ function printDefinedInputFields() { return buildBlock({ name: `interface DefinedInputFields`, lines: visited.inputs.map(typeName => `${typeName}: ${printPicks(typeName, picks.inputs)};`), }); } /** * Prints signatures of schema types with picks */ function printSchemaTypes(types) { return types .filter(type => !visited.scalars.includes(type)) .map(printExportType) .join('\n'); } function printResolveSignaturesPerType(registry) { return [ [...registry.objects, ...registry.interfaces] .map(name => printResolverType(name, 'DefinedFields', // In case of enabled `requireRootResolvers` flag, the preset has to produce a non-optional properties. requireRootResolvers && rootTypes.includes(name), !rootTypes.includes(name) && defined.objects.includes(name) ? ` | '__isTypeOf'` : '')) .join('\n'), ].join('\n'); } function printScalars(registry) { if (!registry.scalars.length) { return ''; } return [ `export type ${encapsulateTypeName('Scalars')} = Pick<${importNamespace}.Scalars, ${registry.scalars .map(withQuotes) .join(' | ')}>;`, ...registry.scalars.map(scalar => { const convertedName = baseVisitor.convertName(scalar, { suffix: 'ScalarConfig', }); return `export type ${encapsulateTypeName(convertedName)} = ${importNamespace}.${convertedName};`; }), ].join('\n'); } /** * Aggregation of type resolver signatures */ function printResolversType(registry) { const lines = []; for (const kind in registry) { const k = kind; if (Object.prototype.hasOwnProperty.call(registry, k) && resolverKeys.includes(k)) { const types = registry[k]; for (const typeName of types) { if (k === 'enums') { continue; } if (k === 'scalars') { lines.push(`${typeName}?: ${encapsulateTypeName(importNamespace)}.Resolvers['${typeName}'];`); } else { // In case of enabled `requireRootResolvers` flag, the preset has to produce a non-optional property. const fieldModifier = requireRootResolvers && rootTypes.includes(typeName) ? '' : '?'; lines.push(`${typeName}${fieldModifier}: ${encapsulateTypeName(typeName)}Resolvers;`); } } } } return buildBlock({ name: `export interface ${encapsulateTypeName('Resolvers')}`, lines, }); } /** * Signature for a map of resolve middlewares */ function printResolveMiddlewareMap() { const wildcardField = printResolveMiddlewareRecord(withQuotes('*')); const blocks = [buildBlock({ name: `${withQuotes('*')}?:`, lines: [wildcardField] })]; // Type.Field for (const typeName in picks.objects) { if (Object.prototype.hasOwnProperty.call(picks.objects, typeName)) { const fields = picks.objects[typeName]; const lines = [wildcardField].concat(fields.map(field => printResolveMiddlewareRecord(field))); blocks.push(buildBlock({ name: `${typeName}?:`, lines, })); } } return buildBlock({ name: `export interface ${encapsulateTypeName('MiddlewareMap')}`, lines: blocks, }); } function printResolveMiddlewareRecord(path) { return `${path}?: gm.Middleware[];`; } function printResolverType(typeName, picksTypeName, requireFieldsResolvers = false, extraKeys = '') { const typeSignature = `Pick<${importNamespace}.${baseVisitor.convertName(typeName, { suffix: 'Resolvers', })}, ${picksTypeName}['${typeName}']${extraKeys}>`; return `export type ${encapsulateTypeName(`${typeName}Resolvers`)} = ${requireFieldsResolvers ? `Required<${typeSignature}>` : typeSignature};`; } function printPicks(typeName, records) { return records[typeName].filter(unique).map(withQuotes).join(' | '); } function printTypeBody(typeName) { const coreType = `${importNamespace}.${baseVisitor.convertName(typeName, { useTypesSuffix: true, useTypesPrefix: true, })}`; if (external.enums.includes(typeName) || external.objects.includes(typeName)) { if (schema && isScalarType(schema.getType(typeName))) { return `${importNamespace}.Scalars['${typeName}']`; } return coreType; } if (defined.enums.includes(typeName) && picks.enums[typeName]) { return `DefinedEnumValues['${typeName}']`; } if (defined.objects.includes(typeName) && picks.objects[typeName]) { return `Pick<${coreType}, DefinedFields['${typeName}']>`; } if (defined.interfaces.includes(typeName) && picks.interfaces[typeName]) { return `Pick<${coreType}, DefinedFields['${typeName}']>`; } if (defined.inputs.includes(typeName) && picks.inputs[typeName]) { return `Pick<${coreType}, DefinedInputFields['${typeName}']>`; } return coreType; } function printExportType(typeName) { return `export type ${encapsulateTypeName(typeName)} = ${printTypeBody(typeName)};`; } // // // // Utils // // // function collectFields(node, picksObj) { const name = node.name.value; if (node.fields) { picksObj[name] ||= []; for (const field of node.fields) { picksObj[name].push(field.name.value); } } } function collectValuesFromEnum(node) { const name = node.name.value; if (node.values) { picks.enums[name] ||= []; for (const field of node.values) { picks.enums[name].push(field.name.value); } } } function collectTypeDefinition(node) { const name = node.name.value; switch (node.kind) { case Kind.OBJECT_TYPE_DEFINITION: { defined.objects.push(name); collectFields(node, picks.objects); break; } case Kind.ENUM_TYPE_DEFINITION: { defined.enums.push(name); collectValuesFromEnum(node); break; } case Kind.INPUT_OBJECT_TYPE_DEFINITION: { defined.inputs.push(name); collectFields(node, picks.inputs); break; } case Kind.SCALAR_TYPE_DEFINITION: { defined.scalars.push(name); break; } case Kind.INTERFACE_TYPE_DEFINITION: { defined.interfaces.push(name); collectFields(node, picks.interfaces); break; } case Kind.UNION_TYPE_DEFINITION: { defined.unions.push(name); break; } } } function collectTypeExtension(node) { const name = node.name.value; switch (node.kind) { case Kind.OBJECT_TYPE_EXTENSION: { collectFields(node, picks.objects); // Do not include root types as extensions // so we can use them in DefinedFields if (rootTypes.includes(name)) { pushUnique(defined.objects, name); return; } pushUnique(extended.objects, name); break; } case Kind.ENUM_TYPE_EXTENSION: { collectValuesFromEnum(node); pushUnique(extended.enums, name); break; } case Kind.INPUT_OBJECT_TYPE_EXTENSION: { collectFields(node, picks.inputs); pushUnique(extended.inputs, name); break; } case Kind.INTERFACE_TYPE_EXTENSION: { collectFields(node, picks.interfaces); pushUnique(extended.interfaces, name); break; } case Kind.UNION_TYPE_EXTENSION: { pushUnique(extended.unions, name); break; } } } }