@apollo/composition
Version:
Apollo Federation composition utilities
990 lines • 126 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
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.schemaToImportNameToFeatureUrl = new Map();
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.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(({ schema }) => {
if (!this.schemaToImportNameToFeatureUrl.has(schema)) {
this.schemaToImportNameToFeatureUrl.set(schema, this.computeMapFromImportNameToIdentityUrl(schema));
}
return 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,
});
}
}
for (const { specInSupergraph, directives } of supergraphInfoByIdentity.values()) {
const imports = [];
for (const { nameInFeature, nameInSupergraph } of directives) {
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);
}
}
}
}
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 (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 nonUnionEnumTypes = [];
this.merged.types().forEach(type => {
if (this.linkSpec.isSpecType(type) ||
this.joinSpec.isSpecType(type)) {
return;
}
switch (type.kind) {
case 'UnionType':
unionTypes.push(type);
return;
case 'EnumType':
enumTypes.push(type);
return;
case 'ObjectType':
objectTypes.push(type);
break;
case 'InterfaceType':
interfaceTypes.push(type);
break;
}
nonUnionEnumTypes.push(type);
});
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 nonUnionEnumTypes) {
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.recordAppliedDirectivesToMerge(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) {
source.appliedDirectives.forEach((d) => {
if (!this.joinSpec.isSpecDirective(d.definition)) {
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.indexOf(sourceSubgraphName),
fromField: sources.get(fromIdx),
});
if (hasIncompatible) {
(0, federation_internals_1.assert)(conflictingDirective !== undefined, 'conflictingDirective should not be undefined');
this.errors.push(federation_internals_1.ERRORS.OVERRIDE_COLLISION_WITH_ANOTHER_DIRECTIVE.err(`@override cannot be used on field "${fromField === null || fromField === void 0 ? void 0 : fromField.coordinate}" on subgraph "${subgraphName}" since "${fromField === null || fromField === void 0 ? void 0 : fromField.coordinate}" on "${subgraph}" is marked with directive "@${conflictingDirective.name}"`, { nodes: (0, federation_internals_1.sourceASTs)(overrideDirective, conflictingDirective) }));
}
else {
(0, federation_internals_1.assert)(fromField, 'fromField should not be undefined');
const overriddenSubgraphASTNode = fromField.sourceAST ? (0, federation_internals_1.addSubgraphToASTNode)(fromField.sourceAST, sourceSubgraphName) : undefined;
const overrideLabel = overrideDirective.arguments().label;
const overriddenFieldIsReferenced = !!this.metadata(fromIdx).isFieldUsed(fromField);
if (this.isExternal(fromIdx, fromField)) {
this.hints.push(new hints_1.CompositionHint(hints_1.HINTS.OVERRIDE_DIRECTIVE_CAN_BE_REMOVED, `Field "${dest.coordinate}" on subgraph "${subgraphName}" is not resolved anymore by the from subgraph (it is marked "@external" in "${sourceSubgraphName}"). The @override directive can be removed.`, dest, overridingSubgraphASTNode));
}
else if (overriddenFieldIsReferenced) {
result.setUsedOverridden(fromIdx);
if (!overrideLabel) {
this.hints.push(new hints_1.CompositionHint(hints_1.HINTS.OVERRIDDEN_FIELD_CAN_BE_REMOVED, `Field "${dest.coordinate}" on subgraph "${sourceSubgraphName}" is overridden. It is still used in some federation directive(s) (@key, @requires, and/or @provides) and/or to satisfy interface constraint(s), but consider marking it @external explicitly or removing it along with its references.`, dest, overriddenSubgraphASTNode));
}
}
else {
result.setUnusedOverridden(fromIdx);
if (!overrideLabel) {
this.hints.push(new hints_1.CompositionHint(hints_1.HINTS.OVERRIDDEN_FIELD_CAN_BE_REMOVED, `Field "${dest.coordinate}" on subgraph "${sourceSubgraphName}" is overridden. Consider removing it.`, dest, overriddenSubgraphASTNode));
}
}
if (overrideLabel) {
const labelRegex = /^[a-zA-Z][a-zA-Z0-9_\-:./]*$/;
const percentRegex = /^percent\((\d{1,2}(\.\d{1,8})?|100)\)$/;
if (labelRegex.test(overrideLabel)) {
result.setOverrideLabel(idx, overrideLabel)