UNPKG

@apollo/composition

Version:
348 lines 19 kB
"use strict"; 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