@graphql-codegen/plugin-helpers
Version:
GraphQL Code Generator common utils and types
388 lines (386 loc) • 15.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ApolloFederation = exports.federationSpec = void 0;
exports.addFederationReferencesToSchema = addFederationReferencesToSchema;
exports.removeFederation = removeFederation;
const utils_1 = require("@graphql-tools/utils");
const graphql_1 = require("graphql");
const index_js_1 = require("./index.js");
const utils_js_1 = require("./utils.js");
/**
* Federation Spec
*/
exports.federationSpec = (0, graphql_1.parse)(/* GraphQL */ `
scalar _FieldSet
directive on FIELD_DEFINITION
directive on FIELD_DEFINITION
directive on FIELD_DEFINITION
directive on OBJECT | INTERFACE
`);
/**
* Adds `__resolveReference` in each ObjectType and InterfaceType involved in Federation.
* We do this to utilise the existing FieldDefinition logic of the plugin, which includes many logic:
* - mapper
* - return type
* @param schema
*/
function addFederationReferencesToSchema(schema) {
const setFederationMeta = ({ meta, typeName, update, }) => {
meta[typeName] = {
...(meta[typeName] ||
{
hasResolveReference: false,
resolvableKeyDirectives: [],
referenceSelectionSets: [],
}),
...update,
};
};
const getReferenceSelectionSets = ({ resolvableKeyDirectives, fields, }) => {
const referenceSelectionSets = [];
// @key() @key() - "primary keys" in Federation
// A reference may receive one primary key combination at a time, so they will be combined with `|`
const primaryKeys = resolvableKeyDirectives.map(extractReferenceSelectionSet);
referenceSelectionSets.push([...primaryKeys]);
for (const fieldNode of Object.values(fields)) {
// Look for @requires and see what the service needs and gets
const directives = getDirectivesByName('requires', fieldNode.astNode);
for (const directive of directives) {
const requires = extractReferenceSelectionSet(directive);
referenceSelectionSets.push([requires]);
}
}
return referenceSelectionSets;
};
const federationMeta = {};
const transformedSchema = (0, utils_1.mapSchema)(schema, {
[utils_1.MapperKind.INTERFACE_TYPE]: type => {
const federationDetails = checkTypeFederationDetails(type, schema);
if (federationDetails && federationDetails.resolvableKeyDirectives.length > 0) {
const typeConfig = type.toConfig();
typeConfig.fields = {
[resolveReferenceFieldName]: {
type,
},
...typeConfig.fields,
};
const referenceSelectionSets = getReferenceSelectionSets({
resolvableKeyDirectives: federationDetails.resolvableKeyDirectives,
fields: typeConfig.fields,
});
setFederationMeta({
meta: federationMeta,
typeName: type.name,
update: {
hasResolveReference: true,
resolvableKeyDirectives: federationDetails.resolvableKeyDirectives,
referenceSelectionSets,
},
});
return new graphql_1.GraphQLInterfaceType(typeConfig);
}
return type;
},
[utils_1.MapperKind.OBJECT_TYPE]: type => {
const federationDetails = checkTypeFederationDetails(type, schema);
if (federationDetails && federationDetails.resolvableKeyDirectives.length > 0) {
const typeConfig = type.toConfig();
const referenceSelectionSets = getReferenceSelectionSets({
resolvableKeyDirectives: federationDetails.resolvableKeyDirectives,
fields: typeConfig.fields,
});
typeConfig.fields = {
[resolveReferenceFieldName]: {
type,
},
...typeConfig.fields,
};
setFederationMeta({
meta: federationMeta,
typeName: type.name,
update: {
hasResolveReference: true,
resolvableKeyDirectives: federationDetails.resolvableKeyDirectives,
referenceSelectionSets,
},
});
return new graphql_1.GraphQLObjectType(typeConfig);
}
return type;
},
});
return {
transformedSchema,
federationMeta,
};
}
/**
* Removes Federation Spec from GraphQL Schema
* @param schema
* @param config
*/
function removeFederation(schema) {
return (0, utils_1.mapSchema)(schema, {
[utils_1.MapperKind.QUERY]: queryType => {
const queryTypeConfig = queryType.toConfig();
delete queryTypeConfig.fields._entities;
delete queryTypeConfig.fields._service;
return new graphql_1.GraphQLObjectType(queryTypeConfig);
},
[utils_1.MapperKind.UNION_TYPE]: unionType => {
const unionTypeName = unionType.name;
if (unionTypeName === '_Entity' || unionTypeName === '_Any') {
return null;
}
return unionType;
},
[utils_1.MapperKind.OBJECT_TYPE]: objectType => {
if (objectType.name === '_Service') {
return null;
}
return objectType;
},
});
}
const resolveReferenceFieldName = '__resolveReference';
class ApolloFederation {
constructor({ enabled, schema, meta }) {
this.enabled = false;
this.meta = {};
this.enabled = enabled;
this.schema = schema;
this.providesMap = this.createMapOfProvides();
this.fieldsToGenerate = {};
this.meta = meta;
}
/**
* Excludes types definde by Federation
* @param typeNames List of type names
*/
filterTypeNames(typeNames) {
return this.enabled ? typeNames.filter(t => t !== '_FieldSet') : typeNames;
}
/**
* Excludes `__resolveReference` fields
* @param fieldNames List of field names
*/
filterFieldNames(fieldNames) {
return this.enabled ? fieldNames.filter(t => t !== resolveReferenceFieldName) : fieldNames;
}
/**
* Decides if directive should not be generated
* @param name directive's name
*/
skipDirective(name) {
return this.enabled && ['external', 'requires', 'provides', 'key'].includes(name);
}
/**
* Decides if scalar should not be generated
* @param name directive's name
*/
skipScalar(name) {
return this.enabled && name === '_FieldSet';
}
/**
* findFieldNodesToGenerate
* @description Function to find field nodes to generate.
* In a normal setup, all fields must be generated.
* However, in a Federatin setup, a field should not be generated if:
* - The field is marked as `@external` and there is no `@provides` path to the field
* - The parent object is marked as `@external` and there is no `@provides` path to the field
*/
findFieldNodesToGenerate({ node, }) {
const nodeName = node.name;
if (this.fieldsToGenerate[nodeName]) {
return this.fieldsToGenerate[nodeName];
}
const fieldNodes = (node.fields || []).map(field => field.node);
if (!this.enabled) {
return fieldNodes;
}
// If the object is marked with `@external`, fields to generate are those with `@provides`
if (this.isExternal(node)) {
const fieldNodesWithProvides = fieldNodes.reduce((acc, fieldNode) => {
if (this.hasProvides(node, fieldNode.name.value)) {
acc.push(fieldNode);
return acc;
}
return acc;
}, []);
this.fieldsToGenerate[nodeName] = fieldNodesWithProvides;
return fieldNodesWithProvides;
}
// If the object is not marked with `@external`, fields to generate are:
// - the fields without `@external`
// - the `@external` fields with `@provides`
const fieldNodesWithoutExternalOrHasProvides = fieldNodes.reduce((acc, fieldNode) => {
if (!this.isExternal(fieldNode)) {
acc.push(fieldNode);
return acc;
}
if (this.isExternal(fieldNode) && this.hasProvides(node, fieldNode.name.value)) {
acc.push(fieldNode);
return acc;
}
return acc;
}, []);
this.fieldsToGenerate[nodeName] = fieldNodesWithoutExternalOrHasProvides;
return fieldNodesWithoutExternalOrHasProvides;
}
isResolveReferenceField(fieldNode) {
const name = typeof fieldNode.name === 'string' ? fieldNode.name : fieldNode.name.value;
return this.enabled && name === resolveReferenceFieldName;
}
/**
* Transforms a field's ParentType signature in ObjectTypes or InterfaceTypes involved in Federation
*/
transformFieldParentType({ fieldNode, parentType, parentTypeSignature, federationTypeSignature, }) {
if (!this.enabled) {
return parentTypeSignature;
}
const result = this.printReferenceSelectionSets({
typeName: parentType.name,
baseFederationType: federationTypeSignature,
});
// When `!result`, it means this is not a Federation entity, so we just return the parentTypeSignature
if (!result) {
return parentTypeSignature;
}
const isEntityResolveReferenceField = ((0, graphql_1.isObjectType)(parentType) || (0, graphql_1.isInterfaceType)(parentType)) && fieldNode.name.value === resolveReferenceFieldName;
if (!isEntityResolveReferenceField) {
return parentTypeSignature;
}
return result;
}
addFederationTypeGenericIfApplicable({ genericTypes, typeName, federationTypesType, }) {
if (!this.getMeta()[typeName]) {
return;
}
const typeRef = `${federationTypesType}['${typeName}']`;
genericTypes.push(`FederationType extends ${typeRef} = ${typeRef}`);
}
getMeta() {
return this.meta;
}
isExternal(node) {
return getDirectivesByName('external', node).length > 0;
}
hasProvides(node, fieldName) {
const fields = this.providesMap[node.name];
if (fields?.length) {
return fields.includes(fieldName);
}
return false;
}
printReferenceSelectionSet({ typeName, referenceSelectionSet, }) {
return `GraphQLRecursivePick<${typeName}, ${JSON.stringify(referenceSelectionSet)}>`;
}
printReferenceSelectionSets({ typeName, baseFederationType, }) {
const federationMeta = this.getMeta()[typeName];
if (!federationMeta?.hasResolveReference) {
return false;
}
return `\n ( { __typename: '${typeName}' }\n & ${federationMeta.referenceSelectionSets
.map(referenceSelectionSetArray => {
const result = referenceSelectionSetArray.map(referenceSelectionSet => {
return this.printReferenceSelectionSet({
referenceSelectionSet,
typeName: baseFederationType,
});
});
return result.length > 1 ? `( ${result.join(' | ')} )` : result.join(' | ');
})
.join('\n & ')} )`;
}
createMapOfProvides() {
const providesMap = {};
for (const typename of Object.keys(this.schema.getTypeMap())) {
const objectType = this.schema.getType(typename);
if ((0, graphql_1.isObjectType)(objectType)) {
for (const field of Object.values(objectType.getFields())) {
const provides = getDirectivesByName('provides', field.astNode)
.map(extractReferenceSelectionSet)
.reduce((prev, curr) => [...prev, ...Object.keys(curr)], []); // FIXME: this is not taking into account nested selection sets e.g. `company { taxCode }`
const ofType = (0, utils_js_1.getBaseType)(field.type);
providesMap[ofType.name] ||= [];
providesMap[ofType.name].push(...provides);
}
}
}
return providesMap;
}
}
exports.ApolloFederation = ApolloFederation;
/**
* Checks if Object Type is involved in Federation. Based on `@key` directive
* @param node Type
*/
function checkTypeFederationDetails(typeOrNode, schema) {
const node = (0, graphql_1.isObjectType)(typeOrNode)
? (0, utils_1.astFromObjectType)(typeOrNode, schema)
: (0, graphql_1.isInterfaceType)(typeOrNode)
? (0, utils_1.astFromInterfaceType)(typeOrNode, schema)
: typeOrNode;
const name = node.name.value || typeOrNode.name;
const directives = node.directives;
const rootTypeNames = (0, utils_1.getRootTypeNames)(schema);
const isNotRoot = !rootTypeNames.has(name);
const isNotIntrospection = !name.startsWith('__');
const keyDirectives = directives.filter(d => d.name.value === 'key' || d.name === 'key');
const check = isNotRoot && isNotIntrospection && keyDirectives.length > 0;
if (!check) {
return false;
}
const resolvableKeyDirectives = keyDirectives.filter(d => {
for (const arg of d.arguments) {
if (arg.name.value === 'resolvable' && arg.value.kind === 'BooleanValue' && arg.value.value === false) {
return false;
}
}
return true;
});
return { resolvableKeyDirectives };
}
/**
* Extracts directives from a node based on directive's name
* @param name directive name
* @param node ObjectType or Field
*/
function getDirectivesByName(name, node) {
let astNode;
if ((0, graphql_1.isObjectType)(node) || (0, graphql_1.isInterfaceType)(node)) {
astNode = node.astNode;
}
else {
astNode = node;
}
return (astNode?.directives?.filter(d => {
// A ObjectTypeDefinitionNode's directive looks like `{ kind: 'Directive', name: 'external', arguments: [] }`
// However, other directives looks like `{ kind: 'Directive', name: { kind: 'Name', value: 'external' }, arguments: [] }`
// Therefore, we need to check for both `d.name.value` and d.name
return d.name.value === name || d.name === name;
}) || []);
}
function extractReferenceSelectionSet(directive) {
const arg = directive.arguments.find(arg => arg.name.value === 'fields');
const { value } = arg.value;
return (0, index_js_1.oldVisit)((0, graphql_1.parse)(`{${value}}`), {
leave: {
SelectionSet(node) {
return node.selections.reduce((accum, field) => {
accum[field.name] = field.selection;
return accum;
}, {});
},
Field(node) {
return {
name: node.name.value,
selection: node.selectionSet || true,
};
},
Document(node) {
return node.definitions.find((def) => def.kind === 'OperationDefinition' && def.operation === 'query').selectionSet;
},
},
});
}