@graphql-codegen/graphql-modules-preset
Version:
GraphQL Code Generator preset for modularized schema
342 lines (341 loc) • 13.1 kB
JavaScript
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;
}
}
}
}