UNPKG

@apollo/composition

Version:
1,015 lines 153 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.AuthValidator = exports.mergeSubgraphs = exports.isMergeFailure = exports.isMergeSuccessful = exports.sourcesFromArray = void 0; const federation_internals_1 = require("@apollo/federation-internals"); const graphql_1 = require("graphql"); const hints_1 = require("../hints"); const composeDirectiveManager_1 = require("../composeDirectiveManager"); const reporter_1 = require("./reporter"); const util_1 = require("util"); const coreDirectiveCollector_1 = require("./coreDirectiveCollector"); function mapSources(sources, mapper) { const result = new Map; sources.forEach((source, idx) => { result.set(idx, mapper(source, idx)); }); return result; } function filterSources(sources) { const result = new Map; sources.forEach((source, idx) => { if (typeof source !== 'undefined') { result.set(idx, source); } }); return result; } function someSources(sources, predicate) { for (const [idx, source] of sources.entries()) { if (predicate(source, idx)) { return true; } } return false; } function sourcesFromArray(array) { const sources = new Map; array.forEach((source, idx) => { sources.set(idx, source); }); return sources; } exports.sourcesFromArray = sourcesFromArray; class FieldMergeContext { constructor(sources) { this._props = new Map; sources.forEach((_, i) => { this._props.set(i, { usedOverridden: false, unusedOverridden: false, overrideWithUnknownTarget: false, overrideLabel: undefined, }); }); } isUsedOverridden(idx) { var _a; return !!((_a = this._props.get(idx)) === null || _a === void 0 ? void 0 : _a.usedOverridden); } isUnusedOverridden(idx) { var _a; return !!((_a = this._props.get(idx)) === null || _a === void 0 ? void 0 : _a.unusedOverridden); } hasOverrideWithUnknownTarget(idx) { var _a; return !!((_a = this._props.get(idx)) === null || _a === void 0 ? void 0 : _a.overrideWithUnknownTarget); } overrideLabel(idx) { var _a; return (_a = this._props.get(idx)) === null || _a === void 0 ? void 0 : _a.overrideLabel; } setUsedOverridden(idx) { this._props.get(idx).usedOverridden = true; } setUnusedOverridden(idx) { this._props.get(idx).unusedOverridden = true; } setOverrideWithUnknownTarget(idx) { this._props.get(idx).overrideWithUnknownTarget = true; } setOverrideLabel(idx, label) { this._props.get(idx).overrideLabel = label; } some(predicate) { for (const [i, props] of this._props.entries()) { if (predicate(props, i)) { return true; } } return false; } } function isMergeSuccessful(mergeResult) { return !isMergeFailure(mergeResult); } exports.isMergeSuccessful = isMergeSuccessful; function isMergeFailure(mergeResult) { return !!mergeResult.errors; } exports.isMergeFailure = isMergeFailure; function mergeSubgraphs(subgraphs, options = {}) { (0, federation_internals_1.assert)(subgraphs.values().every((s) => s.isFed2Subgraph()), 'Merging should only be applied to federation 2 subgraphs'); return new Merger(subgraphs, options).merge(); } exports.mergeSubgraphs = mergeSubgraphs; function copyTypeReference(source, dest) { switch (source.kind) { case 'ListType': return new federation_internals_1.ListType(copyTypeReference(source.ofType, dest)); case 'NonNullType': return new federation_internals_1.NonNullType(copyTypeReference(source.ofType, dest)); default: const type = dest.type(source.name); (0, federation_internals_1.assert)(type, () => `Cannot find type ${source} in destination schema (with types: ${dest.types().join(', ')})`); return type; } } const NON_MERGED_CORE_FEATURES = [federation_internals_1.federationIdentity, federation_internals_1.linkIdentity, federation_internals_1.coreIdentity, federation_internals_1.connectIdentity]; function isMergedType(type) { var _a; if (type.isIntrospectionType() || federation_internals_1.FEDERATION_OPERATION_TYPES.map((s) => s.name).includes(type.name)) { return false; } const coreFeatures = type.schema().coreFeatures; const typeFeature = (_a = coreFeatures === null || coreFeatures === void 0 ? void 0 : coreFeatures.sourceFeature(type)) === null || _a === void 0 ? void 0 : _a.feature.url.identity; return !(typeFeature && NON_MERGED_CORE_FEATURES.includes(typeFeature)); } function isMergedField(field) { return field.kind !== 'FieldDefinition' || !(0, federation_internals_1.isFederationField)(field); } function isGraphQLBuiltInDirective(def) { return !!def.schema().builtInDirective(def.name); } function printTypes(types) { return (0, federation_internals_1.printHumanReadableList)(types.map((t) => `"${t.coordinate}"`), { prefix: 'type', prefixPlural: 'types', }); } function filteredRoot(def, rootKind) { var _a; const type = (_a = def.root(rootKind)) === null || _a === void 0 ? void 0 : _a.type; return type && hasMergedFields(type) ? type : undefined; } function hasMergedFields(type) { for (const field of type.fields()) { if (isMergedField(field)) { return true; } } return false; } function indexOfMax(arr) { if (arr.length === 0) { return -1; } let indexOfMax = 0; for (let i = 1; i < arr.length; i++) { if (arr[i] > arr[indexOfMax]) { indexOfMax = i; } } return indexOfMax; } function descriptionString(toIndent, indentation) { return indentation + '"""\n' + indentation + toIndent.replace('\n', '\n' + indentation) + '\n' + indentation + '"""'; } function locationString(locations) { if (locations.length === 0) { return ""; } return (locations.length === 1 ? 'location ' : 'locations ') + '"' + locations.join(', ') + '"'; } class Merger { constructor(subgraphs, options) { this.subgraphs = subgraphs; this.options = options; this.errors = []; this.hints = []; this.merged = new federation_internals_1.Schema(); this.mergedFederationDirectiveNames = new Set(); this.mergedFederationDirectiveInSupergraphByDirectiveName = new Map(); this.enumUsages = new Map(); this.joinDirectiveFeatureDefinitionsByIdentity = new Map(); this.directivesUsingJoinDirective = new Set(); this.accessControlDirectivesInSupergraph = []; this.latestFedVersionUsed = this.getLatestFederationVersionUsed(); this.joinSpec = federation_internals_1.JOIN_VERSIONS.getMinimumRequiredVersion(this.latestFedVersionUsed); this.linkSpec = federation_internals_1.LINK_VERSIONS.getMinimumRequiredVersion(this.latestFedVersionUsed); this.fieldsWithFromContext = this.getFieldsWithFromContextDirective(); this.fieldsWithOverride = this.getFieldsWithOverrideDirective(); this.fieldsWithRequires = this.getFieldsWithRequiresDirective(); this.names = subgraphs.names(); this.composeDirectiveManager = new composeDirectiveManager_1.ComposeDirectiveManager(this.subgraphs, (error) => { this.errors.push(error); }, (hint) => { this.hints.push(hint); }); this.mismatchReporter = new reporter_1.MismatchReporter(this.names, (error) => { this.errors.push(error); }, (hint) => { this.hints.push(hint); }); this.subgraphsSchema = subgraphs.values().map((subgraph) => { return subgraph.schema; }); this.subgraphNamesToJoinSpecName = this.prepareSupergraph(); this.appliedDirectivesToMerge = []; this.joinDirectiveFeatureDefinitionsByIdentity.set(federation_internals_1.CONNECT_VERSIONS.identity, federation_internals_1.CONNECT_VERSIONS); } getLatestFederationVersionUsed() { var _a; const versions = this.subgraphs.values() .map((s) => this.getLatestFederationVersionUsedInSubgraph(s)) .filter(federation_internals_1.isDefined); return (_a = federation_internals_1.FeatureVersion.max(versions)) !== null && _a !== void 0 ? _a : federation_internals_1.FEDERATION_VERSIONS.latest().version; } getLatestFederationVersionUsedInSubgraph(subgraph) { var _a, _b, _c, _d, _e, _f; const linkedFederationVersion = (_b = (_a = subgraph.metadata()) === null || _a === void 0 ? void 0 : _a.federationFeature()) === null || _b === void 0 ? void 0 : _b.url.version; if (!linkedFederationVersion) { return undefined; } const versionsFromFeatures = []; for (const feature of (_d = (_c = subgraph.schema.coreFeatures) === null || _c === void 0 ? void 0 : _c.allFeatures()) !== null && _d !== void 0 ? _d : []) { const version = feature.minimumFederationVersion(); if (version) { versionsFromFeatures.push(version); } } const impliedFederationVersion = federation_internals_1.FeatureVersion.max(versionsFromFeatures); if (!(impliedFederationVersion === null || impliedFederationVersion === void 0 ? void 0 : impliedFederationVersion.satisfies(linkedFederationVersion)) || linkedFederationVersion.gte(impliedFederationVersion)) { return linkedFederationVersion; } let featureCausingUpgrade; for (const feature of (_f = (_e = subgraph.schema.coreFeatures) === null || _e === void 0 ? void 0 : _e.allFeatures()) !== null && _f !== void 0 ? _f : []) { if (feature.minimumFederationVersion() == impliedFederationVersion) { featureCausingUpgrade = feature; break; } } if (featureCausingUpgrade) { this.hints.push(new hints_1.CompositionHint(hints_1.HINTS.IMPLICITLY_UPGRADED_FEDERATION_VERSION, `Subgraph ${subgraph.name} has been implicitly upgraded from federation ${linkedFederationVersion} to ${impliedFederationVersion}`, featureCausingUpgrade.directive.definition, featureCausingUpgrade.directive.sourceAST ? (0, federation_internals_1.addSubgraphToASTNode)(featureCausingUpgrade.directive.sourceAST, subgraph.name) : undefined)); } return impliedFederationVersion; } prepareSupergraph() { this.linkSpec.addToSchema(this.merged); const errors = this.linkSpec.applyFeatureToSchema(this.merged, this.joinSpec, undefined, this.joinSpec.defaultCorePurpose); (0, federation_internals_1.assert)(errors.length === 0, "We shouldn't have errors adding the join spec to the (still empty) supergraph schema"); const directivesMergeInfo = (0, coreDirectiveCollector_1.collectCoreDirectivesToCompose)(this.subgraphs); this.validateAndMaybeAddSpecs(directivesMergeInfo); return this.joinSpec.populateGraphEnum(this.merged, this.subgraphs); } validateAndMaybeAddSpecs(directivesMergeInfo) { var _a, _b; const supergraphInfoByIdentity = new Map; for (const { url, name, definitionsPerSubgraph, compositionSpec } of directivesMergeInfo) { if (!compositionSpec) { return; } let nameInSupergraph; for (const subgraph of this.subgraphs) { const directive = definitionsPerSubgraph.get(subgraph.name); if (!directive) { continue; } if (!nameInSupergraph) { nameInSupergraph = directive.name; } else if (nameInSupergraph !== directive.name) { this.mismatchReporter.reportMismatchError(federation_internals_1.ERRORS.LINK_IMPORT_NAME_MISMATCH, `The "@${name}" directive (from ${url}) is imported with mismatched name between subgraphs: it is imported as `, directive, sourcesFromArray(this.subgraphs.values().map((s) => definitionsPerSubgraph.get(s.name))), (def) => `"@${def.name}"`); return; } } if (nameInSupergraph) { const specInSupergraph = compositionSpec.supergraphSpecification(this.latestFedVersionUsed); let supergraphInfo = supergraphInfoByIdentity.get(specInSupergraph.url.identity); if (supergraphInfo) { (0, federation_internals_1.assert)(specInSupergraph.url.equals(supergraphInfo.specInSupergraph.url), `Spec ${specInSupergraph.url} directives disagree on version for supergraph`); } else { supergraphInfo = { specInSupergraph, directives: [], }; supergraphInfoByIdentity.set(specInSupergraph.url.identity, supergraphInfo); } supergraphInfo.directives.push({ nameInFeature: name, nameInSupergraph, compositionSpec, }); if (compositionSpec.useJoinDirective) { this.directivesUsingJoinDirective.add(nameInSupergraph); } } } for (const { specInSupergraph, directives } of supergraphInfoByIdentity.values()) { const imports = []; for (const { nameInFeature, nameInSupergraph, compositionSpec } of directives) { if (compositionSpec.useJoinDirective) { continue; } const defaultNameInSupergraph = federation_internals_1.CoreFeature.directiveNameInSchemaForCoreArguments(specInSupergraph.url, specInSupergraph.url.name, [], nameInFeature); if (nameInSupergraph !== defaultNameInSupergraph) { imports.push(nameInFeature === nameInSupergraph ? { name: `@${nameInFeature}` } : { name: `@${nameInFeature}`, as: `@${nameInSupergraph}` }); } } const errors = this.linkSpec.applyFeatureToSchema(this.merged, specInSupergraph, undefined, specInSupergraph.defaultCorePurpose, imports); (0, federation_internals_1.assert)(errors.length === 0, "We shouldn't have errors adding the join spec to the (still empty) supergraph schema"); const feature = (_a = this.merged.coreFeatures) === null || _a === void 0 ? void 0 : _a.getByIdentity(specInSupergraph.url.identity); (0, federation_internals_1.assert)(feature, 'Should have found the feature we just added'); for (const { nameInFeature, nameInSupergraph, compositionSpec } of directives) { const argumentsMerger = (_b = compositionSpec.argumentsMerger) === null || _b === void 0 ? void 0 : _b.call(null, this.merged, feature); if (argumentsMerger instanceof graphql_1.GraphQLError) { throw argumentsMerger; } this.mergedFederationDirectiveNames.add(nameInSupergraph); this.mergedFederationDirectiveInSupergraphByDirectiveName.set(nameInSupergraph, { definition: this.merged.directive(nameInSupergraph), argumentsMerger, staticArgumentTransform: compositionSpec.staticArgumentTransform, }); if (specInSupergraph.identity === federation_internals_1.inaccessibleIdentity && nameInFeature === specInSupergraph.url.name) { this.inaccessibleDirectiveInSupergraph = this.merged.directive(nameInSupergraph); } if (specInSupergraph.identity === federation_internals_1.AuthenticatedSpecDefinition.identity && nameInFeature === specInSupergraph.url.name) { const authenticatedDirective = this.merged.directive(nameInSupergraph); if (authenticatedDirective) { this.accessControlDirectivesInSupergraph.push({ name: federation_internals_1.FederationDirectiveName.AUTHENTICATED, nameInSupergraph: authenticatedDirective.name, }); } } if (specInSupergraph.identity === federation_internals_1.RequiresScopesSpecDefinition.identity && nameInFeature === specInSupergraph.url.name) { const requiresScopesDirective = this.merged.directive(nameInSupergraph); if (requiresScopesDirective) { this.accessControlDirectivesInSupergraph.push({ name: federation_internals_1.FederationDirectiveName.REQUIRES_SCOPES, nameInSupergraph: requiresScopesDirective.name, }); } } if (specInSupergraph.identity === federation_internals_1.PolicySpecDefinition.identity && nameInFeature === specInSupergraph.url.name) { const policyDirective = this.merged.directive(nameInSupergraph); if (policyDirective) { this.accessControlDirectivesInSupergraph.push({ name: federation_internals_1.FederationDirectiveName.POLICY, nameInSupergraph: policyDirective.name, }); } } } } } joinSpecName(subgraphIndex) { return this.subgraphNamesToJoinSpecName.get(this.names[subgraphIndex]); } metadata(idx) { return this.subgraphs.values()[idx].metadata(); } isMergedDirective(subgraphName, definition) { if (this.composeDirectiveManager.shouldComposeDirective({ subgraphName, directiveName: definition.name })) { return true; } if (this.directivesUsingJoinDirective.has(definition.name)) { return false; } if (definition instanceof federation_internals_1.Directive) { return this.mergedFederationDirectiveNames.has(definition.name) || isGraphQLBuiltInDirective(definition.definition); } else if (isGraphQLBuiltInDirective(definition)) { return false; } return definition.hasExecutableLocations(); } merge() { this.composeDirectiveManager.validate(); this.addCoreFeatures(); this.addTypesShallow(); this.addDirectivesShallow(); const objectTypes = []; const interfaceTypes = []; const unionTypes = []; const enumTypes = []; const scalarTypes = []; const inputObjectTypes = []; this.merged.types().forEach(type => { if (this.linkSpec.isSpecType(type) || this.joinSpec.isSpecType(type)) { return; } switch (type.kind) { case 'UnionType': unionTypes.push(type); break; case 'EnumType': enumTypes.push(type); break; case 'ObjectType': objectTypes.push(type); break; case 'InterfaceType': interfaceTypes.push(type); break; case 'ScalarType': scalarTypes.push(type); break; case 'InputObjectType': inputObjectTypes.push(type); break; } }); for (const objectType of objectTypes) { this.mergeImplements(this.subgraphsTypes(objectType), objectType); } for (const interfaceType of interfaceTypes) { this.mergeImplements(this.subgraphsTypes(interfaceType), interfaceType); } for (const unionType of unionTypes) { this.mergeType(this.subgraphsTypes(unionType), unionType); } this.mergeSchemaDefinition(sourcesFromArray(this.subgraphsSchema.map(s => s.schemaDefinition)), this.merged.schemaDefinition); for (const type of [...scalarTypes, ...inputObjectTypes, ...interfaceTypes, ...objectTypes]) { this.mergeType(this.subgraphsTypes(type), type); } for (const definition of this.merged.directives()) { if (this.linkSpec.isSpecDirective(definition) || this.joinSpec.isSpecDirective(definition)) { continue; } this.mergeDirectiveDefinition(sourcesFromArray(this.subgraphsSchema.map(s => s.directive(definition.name))), definition); } for (const enumType of enumTypes) { this.mergeType(this.subgraphsTypes(enumType), enumType); } if (!this.merged.schemaDefinition.rootType('query')) { this.errors.push(federation_internals_1.ERRORS.NO_QUERIES.err("No queries found in any subgraph: a supergraph must have a query root type.")); } this.mergeAllAppliedDirectives(); this.addMissingInterfaceObjectFieldsToImplementations(); if (this.errors.length === 0) { this.postMergeValidations(); if (this.errors.length === 0) { try { this.merged.validate(); this.merged.toAPISchema(); } catch (e) { const causes = (0, federation_internals_1.errorCauses)(e); if (causes) { this.errors.push(...this.updateInaccessibleErrorsWithLinkToSubgraphs(causes)); } else { throw e; } } } } if (this.errors.length > 0) { return { errors: this.errors }; } else { return { supergraph: this.merged, hints: this.hints }; } } addTypesShallow() { const mismatchedTypes = new Set(); const typesWithInterfaceObject = new Set(); for (const subgraph of this.subgraphs) { const metadata = subgraph.metadata(); for (const type of subgraph.schema.allTypes()) { if (!isMergedType(type)) { continue; } let expectedKind = type.kind; if (metadata.isInterfaceObjectType(type)) { expectedKind = 'InterfaceType'; typesWithInterfaceObject.add(type.name); } const previous = this.merged.type(type.name); if (!previous) { this.merged.addType((0, federation_internals_1.newNamedType)(expectedKind, type.name)); } else if (previous.kind !== expectedKind) { mismatchedTypes.add(type.name); } } } mismatchedTypes.forEach(t => this.reportMismatchedTypeDefinitions(t)); for (const itfObjectType of typesWithInterfaceObject) { if (mismatchedTypes.has(itfObjectType)) { continue; } if (!this.subgraphsSchema.some((s) => { var _a; return ((_a = s.type(itfObjectType)) === null || _a === void 0 ? void 0 : _a.kind) === 'InterfaceType'; })) { const subgraphsWithType = this.subgraphs.values().filter((s) => s.schema.type(itfObjectType) !== undefined); this.errors.push(federation_internals_1.ERRORS.INTERFACE_OBJECT_USAGE_ERROR.err(`Type "${itfObjectType}" is declared with @interfaceObject in all the subgraphs in which is is defined (it is defined in ${(0, federation_internals_1.printSubgraphNames)(subgraphsWithType.map((s) => s.name))} but should be defined as an interface in at least one subgraph)`, { nodes: (0, federation_internals_1.sourceASTs)(...subgraphsWithType.map((s) => s.schema.type(itfObjectType))) })); } } } addCoreFeatures() { const features = this.composeDirectiveManager.allComposedCoreFeatures(); for (const [feature, directives] of features) { const imports = directives.map(([asName, origName]) => { if (asName === origName) { return `@${asName}`; } else { return { name: `@${origName}`, as: `@${asName}`, }; } }); this.merged.schemaDefinition.applyDirective('link', { url: feature.url.toString(), import: imports, }); } } addDirectivesShallow() { this.subgraphsSchema.forEach((subgraph, idx) => { for (const directive of subgraph.allDirectives()) { if (!this.isMergedDirective(this.names[idx], directive)) { continue; } if (!this.merged.directive(directive.name)) { this.merged.addDirectiveDefinition(new federation_internals_1.DirectiveDefinition(directive.name)); } } }); } reportMismatchedTypeDefinitions(mismatchedType) { const supergraphType = this.merged.type(mismatchedType); const typeKindToString = (t) => { const metadata = (0, federation_internals_1.federationMetadata)(t.schema()); if (metadata === null || metadata === void 0 ? void 0 : metadata.isInterfaceObjectType(t)) { return 'Interface Object Type (Object Type with @interfaceObject)'; } else { return t.kind.replace("Type", " Type"); } }; this.mismatchReporter.reportMismatchError(federation_internals_1.ERRORS.TYPE_KIND_MISMATCH, `Type "${mismatchedType}" has mismatched kind: it is defined as `, supergraphType, sourcesFromArray(this.subgraphsSchema.map(s => s.type(mismatchedType))), typeKindToString); } subgraphsTypes(supergraphType) { return sourcesFromArray(this.subgraphs.values().map(subgraph => { const type = subgraph.schema.type(supergraphType.name); if (!type) { return; } const kind = subgraph.metadata().isInterfaceObjectType(type) ? 'InterfaceType' : type.kind; if (kind !== supergraphType.kind) { return; } return type; })); } mergeImplements(sources, dest) { const implemented = new Set(); const joinImplementsDirective = this.joinSpec.implementsDirective(this.merged); for (const [idx, source] of sources.entries()) { if (source) { const name = this.joinSpecName(idx); for (const itf of source.interfaces()) { implemented.add(itf.name); dest.applyDirective(joinImplementsDirective, { graph: name, interface: itf.name }); } } } implemented.forEach(itf => dest.addImplementedInterface(itf)); } mergeDescription(sources, dest) { const descriptions = []; const counts = []; for (const source of sources.values()) { if (!source || source.description === undefined) { continue; } const idx = descriptions.indexOf(source.description); if (idx < 0) { descriptions.push(source.description); counts.push(source.description === '' ? Number.MIN_SAFE_INTEGER : 1); } else { counts[idx]++; } } if (descriptions.length > 0) { const nonEmptyDescriptions = descriptions.filter(desc => desc !== ''); if (descriptions.length === 1) { dest.description = descriptions[0]; } else if (nonEmptyDescriptions.length === 1) { dest.description = nonEmptyDescriptions[0]; } else { const idx = indexOfMax(counts); dest.description = descriptions[idx]; const name = dest instanceof federation_internals_1.NamedSchemaElement ? `Element "${dest.coordinate}"` : 'The schema definition'; this.mismatchReporter.reportMismatchHint({ code: hints_1.HINTS.INCONSISTENT_DESCRIPTION, message: `${name} has inconsistent descriptions across subgraphs. `, supergraphElement: dest, subgraphElements: sources, elementToString: elt => elt.description, supergraphElementPrinter: (desc, subgraphs) => `The supergraph will use description (from ${subgraphs}):\n${descriptionString(desc, ' ')}`, otherElementsPrinter: (desc, subgraphs) => `\nIn ${subgraphs}, the description is:\n${descriptionString(desc, ' ')}`, ignorePredicate: elt => (elt === null || elt === void 0 ? void 0 : elt.description) === undefined, noEndOfMessageDot: true, }); } } } mergeType(sources, dest) { this.checkForExtensionWithNoBase(sources, dest); this.mergeDescription(sources, dest); this.addJoinType(sources, dest); this.recordTypeAppliedDirectivesToMerge(sources, dest); this.addJoinDirectiveDirectives(sources, dest); switch (dest.kind) { case 'ScalarType': break; case 'ObjectType': this.mergeObject(sources, dest); break; case 'InterfaceType': this.mergeInterface(sources, dest); break; case 'UnionType': this.mergeUnion(sources, dest); break; case 'EnumType': this.mergeEnum(sources, dest); break; case 'InputObjectType': this.mergeInput(sources, dest); break; } } checkForExtensionWithNoBase(sources, dest) { if ((0, federation_internals_1.isObjectType)(dest) && dest.isRootType()) { return; } const defSubgraphs = []; const extensionSubgraphs = []; const extensionASTs = []; for (const [i, source] of sources.entries()) { if (!source) { continue; } if (source.hasNonExtensionElements()) { defSubgraphs.push(this.names[i]); } if (source.hasExtensionElements()) { extensionSubgraphs.push(this.names[i]); extensionASTs.push((0, federation_internals_1.firstOf)(source.extensions().values()).sourceAST); } } if (extensionSubgraphs.length > 0 && defSubgraphs.length === 0) { for (const [i, subgraph] of extensionSubgraphs.entries()) { this.errors.push(federation_internals_1.ERRORS.EXTENSION_WITH_NO_BASE.err(`[${subgraph}] Type "${dest}" is an extension type, but there is no type definition for "${dest}" in any subgraph.`, { nodes: extensionASTs[i] })); } } } addJoinType(sources, dest) { const joinTypeDirective = this.joinSpec.typeDirective(this.merged); for (const [idx, source] of sources.entries()) { if (!source) { continue; } const sourceMetadata = this.subgraphs.values()[idx].metadata(); const isInterfaceObject = sourceMetadata.isInterfaceObjectType(source) ? true : undefined; const keys = source.appliedDirectivesOf(sourceMetadata.keyDirective()); const name = this.joinSpecName(idx); if (!keys.length) { dest.applyDirective(joinTypeDirective, { graph: name, isInterfaceObject }); } else { for (const key of keys) { const extension = key.ofExtension() || source.hasAppliedDirective(sourceMetadata.extendsDirective()) ? true : undefined; const { resolvable } = key.arguments(); dest.applyDirective(joinTypeDirective, { graph: name, key: key.arguments().fields, extension, resolvable, isInterfaceObject }); } } } } mergeObject(sources, dest) { const isEntity = this.hintOnInconsistentEntity(sources, dest); const isValueType = !isEntity && !dest.isRootType(); const isSubscription = dest.isSubscriptionRootType(); const added = this.addFieldsShallow(sources, dest); if (!added.size) { dest.remove(); } else { added.forEach((subgraphFields, destField) => { if (isValueType) { this.hintOnInconsistentValueTypeField(sources, dest, destField); } const mergeContext = this.validateOverride(subgraphFields, destField); if (isSubscription) { this.validateSubscriptionField(subgraphFields); } this.mergeField({ sources: subgraphFields, dest: destField, mergeContext, }); this.validateFieldSharing(subgraphFields, destField, mergeContext); }); } } hintOnInconsistentEntity(sources, dest) { const sourceAsEntity = []; const sourceAsNonEntity = []; for (const [idx, source] of sources.entries()) { if (!source) { continue; } const sourceMetadata = this.subgraphs.values()[idx].metadata(); const keyDirective = sourceMetadata.keyDirective(); if (source.hasAppliedDirective(keyDirective)) { sourceAsEntity.push(source); } else { sourceAsNonEntity.push(source); } } if (sourceAsEntity.length > 0 && sourceAsNonEntity.length > 0) { this.mismatchReporter.reportMismatchHint({ code: hints_1.HINTS.INCONSISTENT_ENTITY, message: `Type "${dest}" is declared as an entity (has a @key applied) in some but not all defining subgraphs: `, supergraphElement: dest, subgraphElements: sources, elementToString: type => sourceAsEntity.find(entity => entity === type) ? 'yes' : 'no', supergraphElementPrinter: (_, subgraphs) => `it has no @key in ${subgraphs}`, otherElementsPrinter: (_, subgraphs) => ` but has some @key in ${subgraphs}`, }); } return sourceAsEntity.length > 0; } hintOnInconsistentValueTypeField(sources, dest, field) { let hintId; let typeDescription; switch (dest.kind) { case 'ObjectType': hintId = hints_1.HINTS.INCONSISTENT_OBJECT_VALUE_TYPE_FIELD; typeDescription = 'non-entity object'; break; case 'InterfaceType': hintId = hints_1.HINTS.INCONSISTENT_INTERFACE_VALUE_TYPE_FIELD; typeDescription = 'interface'; break; } for (const [index, source] of sources.entries()) { if (source && !source.field(field.name) && !this.areAllFieldsExternal(index, source)) { this.mismatchReporter.reportMismatchHint({ code: hintId, message: `Field "${field.coordinate}" of ${typeDescription} type "${dest}" is defined in some but not all subgraphs that define "${dest}": `, supergraphElement: dest, subgraphElements: sources, elementToString: type => type.field(field.name) ? 'yes' : 'no', supergraphElementPrinter: (_, subgraphs) => `"${field.coordinate}" is defined in ${subgraphs}`, otherElementsPrinter: (_, subgraphs) => ` but not in ${subgraphs}`, }); break; } } } addMissingInterfaceObjectFieldsToImplementations() { for (const type of this.merged.objectTypes()) { for (const implementedItf of type.interfaces()) { for (const itfField of implementedItf.fields()) { if (type.field(itfField.name)) { continue; } if (this.isFieldProvidedByAnInterfaceObject(itfField.name, implementedItf.name)) { const implemField = type.addField(itfField.name, itfField.type); implemField.description = itfField.description; this.copyNonJoinAppliedDirectives(itfField, implemField); for (const itfArg of itfField.arguments()) { const implemArg = implemField.addArgument(itfArg.name, itfArg.type, itfArg.defaultValue); implemArg.description = itfArg.description; this.copyNonJoinAppliedDirectives(itfArg, implemArg); } implemField.applyDirective(this.joinSpec.fieldDirective(this.merged), { graph: undefined }); const sources = new Map; for (let i = 0; i < this.names.length; ++i) { sources.set(i, undefined); } this.validateFieldSharing(sources, implemField, new FieldMergeContext(sources)); } } } } } copyNonJoinAppliedDirectives(source, dest) { if (dest instanceof federation_internals_1.FieldDefinition) { for (const { name, nameInSupergraph } of this.accessControlDirectivesInSupergraph) { let additionalSources = this.accessControlAdditionalSources().get(`${dest.coordinate}_${name}`); if (!additionalSources) { additionalSources = []; } if (additionalSources.length > 0) { this.mergeAppliedDirective(nameInSupergraph, sourcesFromArray(additionalSources), dest); } } } source.appliedDirectives.forEach((d) => { if (this.joinSpec.isSpecDirective(d.definition)) { return; } if (this.accessControlDirectivesInSupergraph.some(({ nameInSupergraph }) => d.name === nameInSupergraph)) { return; } dest.applyDirective(d.name, { ...d.arguments() }); }); } isFieldProvidedByAnInterfaceObject(fieldName, interfaceName) { return this.subgraphs.values().some((s) => { const meta = s.metadata(); const type = s.schema.type(interfaceName); const field = type && meta.isInterfaceObjectType(type) ? type.field(fieldName) : undefined; return field && !meta.isFieldExternal(field); }); } addFieldsShallow(sources, dest) { const added = new Map(); const fieldsToAdd = new Map(); function fieldSet(sourceIndex) { let set = fieldsToAdd.get(sourceIndex); if (!set) fieldsToAdd.set(sourceIndex, set = new Set); return set; } const extraSources = new Map; sources.forEach((source, sourceIndex) => { const schema = this.subgraphsSchema[sourceIndex]; const fields = fieldSet(sourceIndex); if ((0, federation_internals_1.isObjectType)(dest) || (0, federation_internals_1.isInterfaceType)(dest)) { for (const itf of dest.interfaces()) { const itfType = schema.type(itf.name); const subgraph = this.subgraphs.get(this.names[sourceIndex]); if (itfType && (0, federation_internals_1.isObjectType)(itfType) && (subgraph === null || subgraph === void 0 ? void 0 : subgraph.metadata().isInterfaceObjectType(itfType))) { extraSources.set(sourceIndex, undefined); } } } if (source) { for (const field of source.fields()) { fields.add(field); } } if (schema.type(dest.name)) { extraSources.set(sourceIndex, undefined); } }); fieldsToAdd.forEach((fieldSet, sourceIndex) => { fieldSet.forEach(field => { if (field && isMergedField(field)) { const destField = dest.field(field.name) || dest.addField(field.name); let sources = added.get(destField); if (!sources) { sources = new Map(extraSources); added.set(destField, sources); } sources.set(sourceIndex, field); } }); }); return added; } isExternal(sourceIdx, field) { return this.metadata(sourceIdx).isFieldExternal(field); } isFullyExternal(sourceIdx, field) { return this.metadata(sourceIdx).isFieldFullyExternal(field); } areAllFieldsExternal(sourceIdx, type) { return type.fields().every(f => this.isExternal(sourceIdx, f)); } validateAndFilterExternal(sources) { const filtered = new Map; for (const [i, source] of sources.entries()) { if (!source || !this.isExternal(i, source)) { filtered.set(i, source); } else { filtered.set(i, undefined); for (const directive of source.appliedDirectives) { if (this.isMergedDirective(source.name, directive)) { this.errors.push(federation_internals_1.ERRORS.MERGED_DIRECTIVE_APPLICATION_ON_EXTERNAL.err(`[${this.names[i]}] Cannot apply merged directive ${directive} to external field "${source.coordinate}"`, { nodes: directive.sourceAST })); } } } } return filtered; } hasExternal(sources) { for (const [i, source] of sources.entries()) { if (source && this.isExternal(i, source)) { return true; } } return false; } isShareable(sourceIdx, field) { return this.metadata(sourceIdx).isFieldShareable(field); } getOverrideDirective(sourceIdx, field) { const metadata = this.metadata(sourceIdx); const overrideDirective = metadata.isFed2Schema() ? metadata.overrideDirective() : undefined; const allFieldOverrides = overrideDirective ? field.appliedDirectivesOf(overrideDirective) : []; return allFieldOverrides[0]; } overrideConflictsWithOtherDirective({ idx, field, subgraphName, fromIdx, fromField, }) { const fromMetadata = this.metadata(fromIdx); for (const directive of [fromMetadata.requiresDirective(), fromMetadata.providesDirective()]) { if (fromField === null || fromField === void 0 ? void 0 : fromField.hasAppliedDirective(directive)) { return { result: true, conflictingDirective: directive, subgraph: this.names[fromIdx], }; } } if (field && this.isExternal(idx, field)) { return { result: true, conflictingDirective: fromMetadata.externalDirective(), subgraph: subgraphName, }; } return { result: false }; } validateOverride(sources, dest) { const result = new FieldMergeContext(sources); if (!this.fieldsWithOverride.has(dest.coordinate)) { return result; } const mapped = mapSources(sources, (source, idx) => { if (!source) { const interfaceObjectAbstractingFields = this.fieldsInSourceIfAbstractedByInterfaceObject(dest, idx); if (interfaceObjectAbstractingFields.length > 0) { return { idx, name: this.names[idx], interfaceObjectAbstractingFields, }; } return undefined; } return { idx, name: this.names[idx], isInterfaceField: (0, federation_internals_1.isInterfaceType)(source.parent), isInterfaceObject: this.metadata(idx).isInterfaceObjectType(source.parent), overrideDirective: this.getOverrideDirective(idx, source), }; }); const { subgraphsWithOverride, subgraphMap } = Array.from(mapped.values()).reduce((acc, elem) => { if (elem !== undefined) { acc.subgraphMap[elem.name] = elem; if (elem.overrideDirective !== undefined) { acc.subgraphsWithOverride.push(elem.name); } } return acc; }, { subgraphsWithOverride: [], subgraphMap: {} }); subgraphsWithOverride.forEach((subgraphName) => { const { overrideDirective, idx, isInterfaceObject, isInterfaceField } = subgraphMap[subgraphName]; if (!overrideDirective) return; const overridingSubgraphASTNode = overrideDirective.sourceAST ? (0, federation_internals_1.addSubgraphToASTNode)(overrideDirective.sourceAST, subgraphName) : undefined; if (isInterfaceField) { this.errors.push(federation_internals_1.ERRORS.OVERRIDE_ON_INTERFACE.err(`@override cannot be used on field "${dest.coordinate}" on subgraph "${subgraphName}": @override is not supported on interface type fields.`, { nodes: overridingSubgraphASTNode })); return; } if (isInterfaceObject) { this.errors.push(federation_internals_1.ERRORS.OVERRIDE_COLLISION_WITH_ANOTHER_DIRECTIVE.err(`@override is not yet supported on fields of @interfaceObject types: cannot be used on field "${dest.coordinate}" on subgraph "${subgraphName}".`, { nodes: overridingSubgraphASTNode })); return; } const sourceSubgraphName = overrideDirective.arguments().from; if (!this.names.includes(sourceSubgraphName)) { result.setOverrideWithUnknownTarget(idx); const suggestions = (0, federation_internals_1.suggestionList)(sourceSubgraphName, this.names); const extraMsg = (0, federation_internals_1.didYouMean)(suggestions); this.hints.push(new hints_1.CompositionHint(hints_1.HINTS.FROM_SUBGRAPH_DOES_NOT_EXIST, `Source subgraph "${sourceSubgraphName}" for field "${dest.coordinate}" on subgraph "${subgraphName}" does not exist.${extraMsg}`, dest, overridingSubgraphASTNode)); } else if (sourceSubgraphName === subgraphName) { this.errors.push(federation_internals_1.ERRORS.OVERRIDE_FROM_SELF_ERROR.err(`Source and destination subgraphs "${sourceSubgraphName}" are the same for overridden field "${dest.coordinate}"`, { nodes: overrideDirective.sourceAST })); } else if (subgraphsWithOverride.includes(sourceSubgraphName)) { this.errors.push(federation_internals_1.ERRORS.OVERRIDE_SOURCE_HAS_OVERRIDE.err(`Field "${dest.coordinate}" on subgraph "${subgraphName}" is also marked with directive @override in subgraph "${sourceSubgraphName}". Only one @override directive is allowed per field.`, { nodes: (0, federation_internals_1.sourceASTs)(overrideDirective, subgraphMap[sourceSubgraphName].overrideDirective) })); } else if (subgraphMap[sourceSubgraphName] === undefined) { this.hints.push(new hints_1.CompositionHint(hints_1.HINTS.OVERRIDE_DIRECTIVE_CAN_BE_REMOVED, `Field "${dest.coordinate}" on subgraph "${subgraphName}" no longer exists in the from subgraph. The @override directive can be removed.`, dest, overridingSubgraphASTNode)); } else { const { interfaceObjectAbstractingFields } = subgraphMap[sourceSubgraphName]; if (interfaceObjectAbstractingFields) { const abstractingTypes = printTypes(interfaceObjectAbstractingFields.map((f) => f.parent)); this.errors.push(federation_internals_1.ERRORS.OVERRIDE_COLLISION_WITH_ANOTHER_DIRECTIVE.err(`Invalid @override on field "${dest.coordinate}" of subgraph "${subgraphName}": source subgraph "${sourceSubgraphName}" does not have field "${dest.coordinate}" but abstract it in ${abstractingTypes} and overriding abstracted fields is not supported.`, { nodes: (0, federation_internals_1.sourceASTs)(overrideDirective, subgraphMap[sourceSubgraphName].overrideDirective) })); return; } const fromIdx = this.names.indexOf(sourceSubgraphName); const fromField = sources.get(fromIdx); const { result: hasIncompatible, conflictingDirective, subgraph } = this.overrideConflictsWithOtherDirective({ idx, field: sources.get(idx), subgraphName, fromIdx: this.names.