@apollo/composition
Version:
Apollo Federation composition utilities
348 lines • 19 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.ComposeDirectiveManager = void 0;
const federation_internals_1 = require("@apollo/federation-internals");
const hints_1 = require("./hints");
const reporter_1 = require("./merging/reporter");
const merging_1 = require("./merging");
const directiveHasDifferentNameInSubgraph = ({ subgraph, origName, expectedName, identity, }) => {
var _a, _b, _c, _d;
const imp = (_c = (_b = (_a = subgraph.schema.coreFeatures) === null || _a === void 0 ? void 0 : _a.getByIdentity(identity)) === null || _b === void 0 ? void 0 : _b.imports) === null || _c === void 0 ? void 0 : _c.find(imp => imp.name === `@${origName}`);
if (!imp) {
return false;
}
const importedName = (_d = imp.as) !== null && _d !== void 0 ? _d : imp.name;
return importedName !== `@${expectedName}`;
};
const allEqual = (arr) => arr.every((val) => val === arr[0]);
const DISALLOWED_IDENTITIES = [
'https://specs.apollo.dev/core',
'https://specs.apollo.dev/join',
'https://specs.apollo.dev/link',
'https://specs.apollo.dev/tag',
'https://specs.apollo.dev/inaccessible',
'https://specs.apollo.dev/federation',
'https://specs.apollo.dev/authenticated',
'https://specs.apollo.dev/requiresScopes',
'https://specs.apollo.dev/source',
'https://specs.apollo.dev/context',
'https://specs.apollo.dev/cost',
];
class ComposeDirectiveManager {
constructor(subgraphs, pushError, pushHint) {
this.subgraphs = subgraphs;
this.pushError = pushError;
this.pushHint = pushHint;
this.mergeDirectiveMap = new Map();
this.latestFeatureMap = new Map();
this.directiveIdentityMap = new Map();
this.mismatchReporter = new reporter_1.MismatchReporter(subgraphs.names(), pushError, pushHint);
}
coreFeatureASTs(coreIdentity) {
return this.subgraphs.values()
.flatMap(sg => {
var _a, _b;
const ast = (_b = (_a = sg.schema.coreFeatures) === null || _a === void 0 ? void 0 : _a.getByIdentity(coreIdentity)) === null || _b === void 0 ? void 0 : _b.directive.sourceAST;
return ast === undefined ? [] : [{ ...ast, subgraph: sg.name }];
});
}
getLatestIfCompatible(coreIdentity, subgraphsUsed) {
let raisedHint = false;
const pairs = this.subgraphs.values()
.map(sg => {
var _a;
const feature = (_a = sg.schema.coreFeatures) === null || _a === void 0 ? void 0 : _a.getByIdentity(coreIdentity);
if (!feature) {
return undefined;
}
return {
feature,
subgraphName: sg.name,
isComposed: subgraphsUsed.includes(sg.name),
};
})
.filter(federation_internals_1.isDefined);
const latest = pairs.reduce((acc, pair) => {
if (acc === null) {
return pair;
}
if (acc === undefined) {
return acc;
}
if (acc.feature.url.version.major !== pair.feature.url.version.major) {
if (acc.isComposed && pair.isComposed) {
this.pushError(federation_internals_1.ERRORS.DIRECTIVE_COMPOSITION_ERROR.err(`Core feature "${coreIdentity}" requested to be merged has major version mismatch across subgraphs`, {
nodes: this.coreFeatureASTs(coreIdentity),
}));
return undefined;
}
if (!raisedHint) {
this.pushHint(new hints_1.CompositionHint(hints_1.HINTS.DIRECTIVE_COMPOSITION_INFO, `Non-composed core feature "${coreIdentity}" has major version mismatch across subgraphs`, undefined, this.coreFeatureASTs(coreIdentity)));
raisedHint = true;
}
return acc.isComposed ? acc : pair;
}
if (acc.isComposed && !pair.isComposed) {
return acc;
}
else if (!acc.isComposed && pair.isComposed) {
return pair;
}
return (acc.feature.url.version.minor > pair.feature.url.version.minor) ? acc : pair;
}, null);
if (!(latest === null || latest === void 0 ? void 0 : latest.isComposed)) {
return undefined;
}
return latest;
}
forFederationDirective(sg, composeInstance, directive) {
const directivesComposedByDefault = [
sg.metadata().tagDirective(),
sg.metadata().inaccessibleDirective(),
sg.metadata().authenticatedDirective(),
sg.metadata().requiresScopesDirective(),
sg.metadata().policyDirective(),
sg.metadata().contextDirective(),
].map(d => d.name);
if (directivesComposedByDefault.includes(directive.name)) {
this.pushHint(new hints_1.CompositionHint(hints_1.HINTS.DIRECTIVE_COMPOSITION_INFO, `Directive "@${directive.name}" should not be explicitly manually composed since it is a federation directive composed by default`, directive, composeInstance.sourceAST ? {
...composeInstance.sourceAST,
subgraph: sg.name,
} : undefined));
}
else {
this.pushError(federation_internals_1.ERRORS.DIRECTIVE_COMPOSITION_ERROR.err(`Composing federation directive "${composeInstance.arguments().name}" in subgraph "${sg.name}" is not supported`, { nodes: composeInstance.sourceAST }));
}
}
allCoreFeaturesUsedBySubgraphs() {
const identities = new Set();
this.subgraphs.values().forEach(sg => {
if (sg.schema.coreFeatures) {
for (const feature of sg.schema.coreFeatures.allFeatures()) {
identities.add(feature.url.identity);
}
}
});
return identities;
}
validate() {
var _a;
const errors = [];
const hints = [];
const wontMergeFeatures = new Set();
const wontMergeDirectiveNames = new Set();
const itemsBySubgraph = new federation_internals_1.MultiMap();
const itemsByDirectiveName = new federation_internals_1.MultiMap();
const itemsByOrigDirectiveName = new federation_internals_1.MultiMap();
const tagNamesInSubgraphs = this.subgraphs.values().map(sg => sg.metadata().federationDirectiveNameInSchema('tag'));
const inaccessibleNamesInSubgraphs = this.subgraphs.values().map(sg => sg.metadata().federationDirectiveNameInSchema('inaccessible'));
for (const sg of this.subgraphs) {
const composeDirectives = sg.metadata()
.composeDirective()
.applications();
for (const composeInstance of composeDirectives) {
if (composeInstance.arguments().name == null || composeInstance.arguments().name === '') {
this.pushError(federation_internals_1.ERRORS.DIRECTIVE_COMPOSITION_ERROR.err(`Argument to @composeDirective in subgraph "${sg.name}" cannot be NULL or an empty String`, { nodes: composeInstance.sourceAST }));
continue;
}
if (composeInstance.arguments().name[0] !== '@') {
this.pushError(federation_internals_1.ERRORS.DIRECTIVE_COMPOSITION_ERROR.err(`Argument to @composeDirective "${composeInstance.arguments().name}" in subgraph "${sg.name}" must have a leading "@"`, { nodes: composeInstance.sourceAST }));
continue;
}
const name = composeInstance.arguments().name.slice(1);
const directive = sg.schema.directive(name);
if (directive) {
const featureDetails = (_a = sg.schema.coreFeatures) === null || _a === void 0 ? void 0 : _a.sourceFeature(directive);
if (featureDetails) {
const identity = featureDetails.feature.url.identity;
if (DISALLOWED_IDENTITIES.includes(identity)) {
this.forFederationDirective(sg, composeInstance, directive);
}
else if (tagNamesInSubgraphs.includes(name)) {
const subgraphs = [];
this.subgraphs.names().forEach((sg, idx) => {
if (tagNamesInSubgraphs[idx] === name) {
subgraphs.push(sg);
}
});
this.pushError(federation_internals_1.ERRORS.DIRECTIVE_COMPOSITION_ERROR.err(`Directive "@${name}" in subgraph "${sg.name}" cannot be composed because it conflicts with automatically composed federation directive "@tag". Conflict exists in subgraph(s): (${subgraphs.join(',')})`, { nodes: composeInstance.sourceAST }));
}
else if (inaccessibleNamesInSubgraphs.includes(name)) {
const subgraphs = [];
this.subgraphs.names().forEach((sg, idx) => {
if (inaccessibleNamesInSubgraphs[idx] === name) {
subgraphs.push(sg);
}
});
this.pushError(federation_internals_1.ERRORS.DIRECTIVE_COMPOSITION_ERROR.err(`Directive "@${name}" in subgraph "${sg.name}" cannot be composed because it conflicts with automatically composed federation directive "@inaccessible". Conflict exists in subgraph(s): (${subgraphs.join(',')})`, { nodes: composeInstance.sourceAST }));
}
else {
const item = {
composeDirective: composeInstance,
sgName: sg.name,
feature: featureDetails.feature,
directiveName: featureDetails.nameInFeature,
directiveNameAs: name,
};
itemsBySubgraph.add(sg.name, item);
itemsByDirectiveName.add(name, item);
itemsByOrigDirectiveName.add(item.directiveName, item);
}
}
else {
this.pushError(federation_internals_1.ERRORS.DIRECTIVE_COMPOSITION_ERROR.err(`Directive "@${name}" in subgraph "${sg.name}" cannot be composed because it is not a member of a core feature`, { nodes: composeInstance.sourceAST }));
}
}
else {
const words = (0, federation_internals_1.suggestionList)(`@${name}`, sg.schema.directives().map(d => `@${d.name}`));
this.pushError(federation_internals_1.ERRORS.DIRECTIVE_COMPOSITION_ERROR.err(`Could not find matching directive definition for argument to @composeDirective "@${name}" in subgraph "${sg.name}".${(0, federation_internals_1.didYouMean)(words)}`, { nodes: composeInstance.sourceAST }));
}
}
}
for (const identity of this.allCoreFeaturesUsedBySubgraphs()) {
const subgraphsUsed = this.subgraphs.values()
.map(sg => {
const items = itemsBySubgraph.get(sg.name);
if (items && items.find(item => item.feature.url.identity === identity)) {
return sg.name;
}
return undefined;
})
.filter(federation_internals_1.isDefined);
const latest = this.getLatestIfCompatible(identity, subgraphsUsed);
if (latest) {
this.latestFeatureMap.set(identity, [latest.feature, latest.subgraphName]);
}
else {
wontMergeFeatures.add(identity);
}
}
for (const [name, items] of itemsByDirectiveName.entries()) {
if (!allEqual(items.map(item => item.directiveName))) {
wontMergeDirectiveNames.add(name);
this.pushError(federation_internals_1.ERRORS.DIRECTIVE_COMPOSITION_ERROR.err(`Composed directive "@${name}" does not refer to the same directive in every subgraph`, {
nodes: items.map(item => item.composeDirective.sourceAST).filter(federation_internals_1.isDefined),
}));
}
if (!allEqual(items.map(item => item.feature.url.identity))) {
wontMergeDirectiveNames.add(name);
this.pushError(federation_internals_1.ERRORS.DIRECTIVE_COMPOSITION_ERROR.err(`Composed directive "@${name}" is not linked by the same core feature in every subgraph`, {
nodes: items.map(item => item.composeDirective.sourceAST).filter(federation_internals_1.isDefined),
}));
}
}
for (const [name, items] of itemsByOrigDirectiveName.entries()) {
if (!allEqual(items.map(item => item.directiveNameAs))) {
for (const item of items) {
wontMergeDirectiveNames.add(item.directiveNameAs);
}
this.mismatchReporter.reportMismatchErrorWithoutSupergraph(federation_internals_1.ERRORS.DIRECTIVE_COMPOSITION_ERROR, 'Composed directive is not named consistently in all subgraphs', (0, merging_1.sourcesFromArray)(this.subgraphs.values()
.map(sg => {
const item = items.find(item => sg.name === item.sgName);
return item ? {
item,
sg,
} : undefined;
})
.map((val) => {
var _a, _b;
if (!val) {
return undefined;
}
const sourceAST = (_b = (_a = val.sg.schema.coreFeatures) === null || _a === void 0 ? void 0 : _a.getByIdentity('https://specs.apollo.dev/foo')) === null || _b === void 0 ? void 0 : _b.directive.sourceAST;
return sourceAST ? {
sourceAST,
item: val.item,
} : undefined;
})), (elt) => elt ? `"@${elt.item.directiveNameAs}"` : undefined);
}
const nonExportedSubgraphs = this.subgraphs.values()
.filter(sg => !items.map(item => item.sgName).includes(sg.name));
const subgraphsWithDifferentNaming = nonExportedSubgraphs.filter(subgraph => directiveHasDifferentNameInSubgraph({
subgraph,
origName: items[0].directiveName,
expectedName: items[0].directiveNameAs,
identity: items[0].feature.url.identity,
}));
if (subgraphsWithDifferentNaming.length > 0) {
this.pushHint(new hints_1.CompositionHint(hints_1.HINTS.DIRECTIVE_COMPOSITION_WARN, `Composed directive "@${name}" is named differently in a subgraph that doesn't export it. Consistent naming will be required to export it.`, undefined, subgraphsWithDifferentNaming
.map((subgraph) => {
var _a, _b;
const ast = (_b = (_a = subgraph.schema.coreFeatures) === null || _a === void 0 ? void 0 : _a.getByIdentity(items[0].feature.url.identity)) === null || _b === void 0 ? void 0 : _b.directive.sourceAST;
return ast ? {
...ast,
subgraph: subgraph.name,
} : undefined;
})
.filter(federation_internals_1.isDefined)));
}
}
for (const [subgraph, items] of itemsBySubgraph.entries()) {
const directivesForSubgraph = new Set();
for (const item of items) {
if (!wontMergeFeatures.has(item.feature.url.identity) && !wontMergeDirectiveNames.has(item.directiveNameAs)) {
directivesForSubgraph.add(item.directiveNameAs);
}
this.directiveIdentityMap.set(item.directiveNameAs, [item.feature.url.identity, item.directiveName]);
}
this.mergeDirectiveMap.set(subgraph, directivesForSubgraph);
}
return {
errors,
hints,
};
}
shouldComposeDirective({ subgraphName, directiveName }) {
const sg = this.mergeDirectiveMap.get(subgraphName);
return !!sg && sg.has(directiveName);
}
directiveExistsInSupergraph(directiveName) {
return !!this.directiveIdentityMap.get(directiveName);
}
getLatestDirectiveDefinition(directiveName) {
var _a, _b;
const val = this.directiveIdentityMap.get(directiveName);
if (val) {
const [identity, origName] = val;
const entry = this.latestFeatureMap.get(identity);
(0, federation_internals_1.assert)(entry, 'core feature identity must exist in map');
const [feature, subgraphName] = entry;
const subgraph = this.subgraphs.get(subgraphName);
(0, federation_internals_1.assert)(subgraph, `subgraph "${subgraphName}" does not exist`);
const nameInSchema = (_b = (_a = subgraph.schema.coreFeatures) === null || _a === void 0 ? void 0 : _a.getByIdentity(identity)) === null || _b === void 0 ? void 0 : _b.directiveNameInSchema(origName);
if (nameInSchema) {
const directive = subgraph.schema.directive(nameInSchema);
if (!directive) {
this.pushError(federation_internals_1.ERRORS.DIRECTIVE_COMPOSITION_ERROR.err(`Core feature "${identity}" in subgraph "${subgraphName}" does not have a directive definition for "@${directiveName}"`, {
nodes: feature.directive.sourceAST,
}));
}
return directive;
}
}
return undefined;
}
directivesForFeature(identity) {
const directives = {};
for (const [name, val] of this.directiveIdentityMap) {
const [id, origName] = val;
if (id === identity) {
if (!(name in directives)) {
directives[name] = origName;
}
}
}
return Object.entries(directives);
}
allComposedCoreFeatures() {
return Array.from(this.latestFeatureMap.values())
.map(value => value[0])
.filter(feature => !DISALLOWED_IDENTITIES.includes(feature.url.identity))
.map(feature => ([
feature,
this.directivesForFeature(feature.url.identity),
]));
}
}
exports.ComposeDirectiveManager = ComposeDirectiveManager;
//# sourceMappingURL=composeDirectiveManager.js.map