UNPKG

@apollo/federation-internals

Version:
589 lines 27 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.upgradeSubgraphsIfNecessary = exports.RemovedTagOnExternal = exports.FieldsArgumentCoercionToString = exports.ProvidesOnNonCompositeRemoval = exports.ProvidesOrRequiresOnInterfaceFieldRemoval = exports.KeyOnInterfaceRemoval = exports.ShareableTypeAddition = exports.ShareableFieldAddition = exports.InactiveProvidesOrRequiresFieldsRemoval = exports.InactiveProvidesOrRequiresRemoval = exports.TypeWithOnlyUnusedExternalRemoval = exports.UnusedExternalRemoval = exports.ExternalOnObjectTypeRemoval = exports.ExternalOnInterfaceRemoval = exports.TypeExtensionRemoval = exports.ExternalOnTypeExtensionRemoval = void 0; const graphql_1 = require("graphql"); const error_1 = require("./error"); const definitions_1 = require("./definitions"); const federation_1 = require("./federation"); const utils_1 = require("./utils"); const values_1 = require("./values"); const federationSpec_1 = require("./specs/federationSpec"); class ExternalOnTypeExtensionRemoval { constructor(field) { this.field = field; this.id = 'EXTERNAL_ON_TYPE_EXTENSION_REMOVAL'; } toString() { return `Removed @external from field "${this.field}" as it is a key of an extension type`; } } exports.ExternalOnTypeExtensionRemoval = ExternalOnTypeExtensionRemoval; class TypeExtensionRemoval { constructor(type) { this.type = type; this.id = 'TYPE_EXTENSION_REMOVAL'; } toString() { return `Switched type "${this.type}" from an extension to a definition`; } } exports.TypeExtensionRemoval = TypeExtensionRemoval; class ExternalOnInterfaceRemoval { constructor(field) { this.field = field; this.id = 'EXTERNAL_ON_INTERFACE_REMOVAL'; } toString() { return `Removed @external directive on interface type field "${this.field}": @external is nonsensical on interface fields`; } } exports.ExternalOnInterfaceRemoval = ExternalOnInterfaceRemoval; class ExternalOnObjectTypeRemoval { constructor(type) { this.type = type; this.id = 'EXTERNAL_ON_OBJECT_TYPE_REMOVAL'; } toString() { return `Removed @external directive on object type "${this.type}": @external on types was not rejected but was inactive in fed1`; } } exports.ExternalOnObjectTypeRemoval = ExternalOnObjectTypeRemoval; class UnusedExternalRemoval { constructor(field) { this.field = field; this.id = 'UNUSED_EXTERNAL_REMOVAL'; } toString() { return `Removed @external field "${this.field}" as it was not used in any @key, @provides or @requires`; } } exports.UnusedExternalRemoval = UnusedExternalRemoval; class TypeWithOnlyUnusedExternalRemoval { constructor(type) { this.type = type; this.id = 'TYPE_WITH_ONLY_UNUSED_EXTERNAL_REMOVAL'; } toString() { return `Removed type ${this.type} that is not referenced in the schema and only declares unused @external fields`; } } exports.TypeWithOnlyUnusedExternalRemoval = TypeWithOnlyUnusedExternalRemoval; class InactiveProvidesOrRequiresRemoval { constructor(parent, removed) { this.parent = parent; this.removed = removed; this.id = 'INACTIVE_PROVIDES_OR_REQUIRES_REMOVAL'; } toString() { return `Removed directive ${this.removed} on "${this.parent}": none of the fields were truly @external`; } } exports.InactiveProvidesOrRequiresRemoval = InactiveProvidesOrRequiresRemoval; class InactiveProvidesOrRequiresFieldsRemoval { constructor(parent, original, updated) { this.parent = parent; this.original = original; this.updated = updated; this.id = 'INACTIVE_PROVIDES_OR_REQUIRES_FIELDS_REMOVAL'; } toString() { return `Updated directive ${this.original} on "${this.parent}" to ${this.updated}: removed fields that were not truly @external`; } } exports.InactiveProvidesOrRequiresFieldsRemoval = InactiveProvidesOrRequiresFieldsRemoval; class ShareableFieldAddition { constructor(field, declaringSubgraphs) { this.field = field; this.declaringSubgraphs = declaringSubgraphs; this.id = 'SHAREABLE_FIELD_ADDITION'; } toString() { return `Added @shareable to field "${this.field}": it is also resolved by ${(0, federation_1.printSubgraphNames)(this.declaringSubgraphs)}`; } } exports.ShareableFieldAddition = ShareableFieldAddition; class ShareableTypeAddition { constructor(type, declaringSubgraphs) { this.type = type; this.declaringSubgraphs = declaringSubgraphs; this.id = 'SHAREABLE_TYPE_ADDITION'; } toString() { return `Added @shareable to type "${this.type}": it is a "value type" and is also declared in ${(0, federation_1.printSubgraphNames)(this.declaringSubgraphs)}`; } } exports.ShareableTypeAddition = ShareableTypeAddition; class KeyOnInterfaceRemoval { constructor(type) { this.type = type; this.id = 'KEY_ON_INTERFACE_REMOVAL'; } toString() { return `Removed @key on interface "${this.type}": while allowed by federation 0.x, @key on interfaces were completely ignored/had no effect`; } } exports.KeyOnInterfaceRemoval = KeyOnInterfaceRemoval; class ProvidesOrRequiresOnInterfaceFieldRemoval { constructor(field, directive) { this.field = field; this.directive = directive; this.id = 'PROVIDES_OR_REQUIRES_ON_INTERFACE_FIELD_REMOVAL'; } toString() { return `Removed @${this.directive} on interface field "${this.field}": while allowed by federation 0.x, @${this.directive} on interface fields were completely ignored/had no effect`; } } exports.ProvidesOrRequiresOnInterfaceFieldRemoval = ProvidesOrRequiresOnInterfaceFieldRemoval; class ProvidesOnNonCompositeRemoval { constructor(field, type) { this.field = field; this.type = type; this.id = 'PROVIDES_ON_NON_COMPOSITE_REMOVAL'; } toString() { return `Removed @provides directive on field "${this.field}" as it is of non-composite type "${this.type}": while not rejected by federation 0.x, such @provide is nonsensical and was ignored`; } } exports.ProvidesOnNonCompositeRemoval = ProvidesOnNonCompositeRemoval; class FieldsArgumentCoercionToString { constructor(element, directive, before, after) { this.element = element; this.directive = directive; this.before = before; this.after = after; this.id = 'FIELDS_ARGUMENT_COERCION_TO_STRING'; } toString() { return `Coerced "fields" argument for directive @${this.directive} for "${this.element}" into a string: coerced from ${this.before} to ${this.after}`; } } exports.FieldsArgumentCoercionToString = FieldsArgumentCoercionToString; class RemovedTagOnExternal { constructor(application, element) { this.application = application; this.element = element; this.id = 'REMOVED_TAG_ON_EXTERNAL'; } toString() { return `Removed ${this.application} application on @external "${this.element}" as the @tag application is on another definition`; } } exports.RemovedTagOnExternal = RemovedTagOnExternal; function upgradeSubgraphsIfNecessary(inputs) { const changes = new Map(); if (inputs.values().every((s) => s.isFed2Subgraph())) { return { subgraphs: inputs, changes }; } const subgraphs = new federation_1.Subgraphs(); let errors = []; const subgraphsUsingInterfaceObject = []; const objectTypeMap = new Map(); for (const subgraph of inputs.values()) { for (const t of subgraph.schema.objectTypes()) { let entry = objectTypeMap.get(t.name); if (!entry) { entry = new Map(); objectTypeMap.set(t.name, entry); } entry.set(subgraph.name, [t, subgraph.metadata()]); } for (const t of subgraph.schema.interfaceTypes()) { let entry = objectTypeMap.get(t.name); if (!entry) { entry = new Map(); objectTypeMap.set(t.name, entry); } entry.set(subgraph.name, [t, subgraph.metadata()]); } } for (const subgraph of inputs.values()) { if (subgraph.isFed2Subgraph()) { subgraphs.add(subgraph); if (subgraph.metadata().interfaceObjectDirective().applications().size > 0) { subgraphsUsingInterfaceObject.push(subgraph.name); } } else { const res = new SchemaUpgrader(subgraph, inputs.values(), objectTypeMap).upgrade(); if (res.errors) { errors = errors.concat(res.errors); } else { subgraphs.add(res.upgraded); changes.set(subgraph.name, res.changes); } } } if (errors.length === 0 && subgraphsUsingInterfaceObject.length > 0) { const fed1Subgraphs = inputs.values().filter((s) => !s.isFed2Subgraph()).map((s) => s.name); errors = [error_1.ERRORS.INTERFACE_OBJECT_USAGE_ERROR.err('The @interfaceObject directive can only be used if all subgraphs have federation 2 subgraph schema (schema with a `@link` to "https://specs.apollo.dev/federation" version 2.0 or newer): ' + `@interfaceObject is used in ${(0, federation_1.printSubgraphNames)(subgraphsUsingInterfaceObject)} but ${(0, federation_1.printSubgraphNames)(fed1Subgraphs)} ${fed1Subgraphs.length > 1 ? 'are not' : 'is not a'} federation 2 subgraph schema.`)]; } return errors.length === 0 ? { subgraphs, changes } : { errors }; } exports.upgradeSubgraphsIfNecessary = upgradeSubgraphsIfNecessary; function isFederationTypeExtension(type) { const metadata = (0, federation_1.federationMetadata)(type.schema()); (0, utils_1.assert)(metadata, 'Should be a subgraph schema'); const hasExtend = type.hasAppliedDirective(metadata.extendsDirective()); return (type.hasExtensionElements() || hasExtend) && ((0, definitions_1.isObjectType)(type) || (0, definitions_1.isInterfaceType)(type)) && (hasExtend || !type.hasNonExtensionElements()); } function isRootTypeExtension(type) { const metadata = (0, federation_1.federationMetadata)(type.schema()); (0, utils_1.assert)(metadata, 'Should be a subgraph schema'); return (0, definitions_1.isObjectType)(type) && type.isRootType() && (type.hasAppliedDirective(metadata.extendsDirective()) || (type.hasExtensionElements() && !type.hasNonExtensionElements())); } function getField(schema, typeName, fieldName) { const type = schema.type(typeName); return type && (0, definitions_1.isCompositeType)(type) ? type.field(fieldName) : undefined; } class SchemaUpgrader { constructor(originalSubgraph, allSubgraphs, objectTypeMap) { this.originalSubgraph = originalSubgraph; this.allSubgraphs = allSubgraphs; this.objectTypeMap = objectTypeMap; this.changes = new utils_1.MultiMap(); this.errors = []; this.schema = originalSubgraph.schema.clone(); this.renameFederationTypes(); this.subgraph = new federation_1.Subgraph(originalSubgraph.name, originalSubgraph.url, this.schema); try { (0, federation_1.setSchemaAsFed2Subgraph)(this.schema); } catch (e) { const causes = (0, error_1.errorCauses)(e); if (causes) { causes.forEach((c) => this.addError(c)); } else { throw e; } } this.metadata = this.subgraph.metadata(); } addError(e) { this.errors.push((0, federation_1.addSubgraphToError)(e, this.subgraph.name, error_1.ERRORS.INVALID_GRAPHQL)); } renameFederationTypes() { for (const typeSpec of federationSpec_1.FEDERATION1_TYPES) { const typeNameInOriginal = this.originalSubgraph.metadata().federationTypeNameInSchema(typeSpec.name); const type = this.schema.type(typeNameInOriginal); if (type) { type.rename(`federation__${typeSpec.name}`); } } } external(elt) { const applications = elt.appliedDirectivesOf(this.metadata.externalDirective()); return applications.length === 0 ? undefined : applications[0]; } addChange(change) { this.changes.add(change.id, change); } checkForExtensionWithNoBase(type) { var _a; if (isRootTypeExtension(type) || !isFederationTypeExtension(type)) { return; } const extensionAST = (_a = (0, utils_1.firstOf)(type.extensions().values())) === null || _a === void 0 ? void 0 : _a.sourceAST; const typeInOtherSubgraphs = Array.from(this.objectTypeMap.get(type.name).entries()).filter(([subgraphName, _]) => subgraphName !== this.subgraph.name); for (let i = 0; i < typeInOtherSubgraphs.length; i += 1) { const otherType = typeInOtherSubgraphs[i][1][0]; if (otherType && otherType.hasNonExtensionElements()) { return; } } this.addError(error_1.ERRORS.EXTENSION_WITH_NO_BASE.err(`Type "${type}" is an extension type, but there is no type definition for "${type}" in any subgraph.`, { nodes: extensionAST })); } preUpgradeValidations() { for (const type of this.schema.types()) { this.checkForExtensionWithNoBase(type); } } upgrade() { this.preUpgradeValidations(); this.fixFederationDirectivesArguments(); this.removeExternalOnInterface(); this.removeExternalOnObjectTypes(); this.removeExternalOnTypeExtensions(); this.fixInactiveProvidesAndRequires(); this.removeTypeExtensions(); this.removeDirectivesOnInterface(); this.removeProvidesOnNonComposite(); this.removeUnusedExternals(); this.addShareable(); this.removeTagOnExternal(); if (this.errors.length > 0) { return { errors: this.errors }; } try { this.subgraph.validate(); return { upgraded: this.subgraph, changes: this.changes, }; } catch (e) { const errors = (0, error_1.errorCauses)(e); if (!errors) { throw e; } return { errors }; } } fixFederationDirectivesArguments() { var _a; for (const directive of [this.metadata.keyDirective(), this.metadata.requiresDirective(), this.metadata.providesDirective()]) { for (const application of Array.from(directive.applications())) { const fields = application.arguments().fields; if (typeof fields !== 'string') { if (Array.isArray(fields) && fields.every((f) => typeof f === 'string')) { this.replaceFederationDirectiveApplication(application, application.toString(), fields.join(' '), directive.sourceAST); } continue; } const nodes = application.sourceAST; if (nodes && nodes.kind === 'Directive') { for (const argNode of (_a = nodes.arguments) !== null && _a !== void 0 ? _a : []) { if (argNode.name.value === 'fields') { if (argNode.value.kind === graphql_1.Kind.ENUM) { this.replaceFederationDirectiveApplication(application, (0, graphql_1.print)(nodes), fields, { ...nodes, arguments: [{ ...argNode, value: { kind: graphql_1.Kind.STRING, value: fields } }] }); break; } } } } } } } removeExternalOnInterface() { for (const itf of this.schema.interfaceTypes()) { for (const field of itf.fields()) { const external = this.external(field); if (external) { this.addChange(new ExternalOnInterfaceRemoval(field.coordinate)); external.remove(); } } } } removeExternalOnObjectTypes() { for (const type of this.schema.objectTypes()) { const external = type.appliedDirectivesOf(this.metadata.externalDirective())[0]; if (external) { this.addChange(new ExternalOnObjectTypeRemoval(type.coordinate)); external.remove(); } } } replaceFederationDirectiveApplication(application, before, fields, updatedSourceAST) { const directive = application.definition; const parent = application.parent; application.remove(); const newDirective = parent.applyDirective(directive, { fields }); newDirective.sourceAST = updatedSourceAST; this.addChange(new FieldsArgumentCoercionToString(parent.coordinate, directive.name, before, newDirective.toString())); } fixInactiveProvidesAndRequires() { (0, federation_1.removeInactiveProvidesAndRequires)(this.schema, (field, original, updated) => { if (updated) { this.addChange(new InactiveProvidesOrRequiresFieldsRemoval(field.coordinate, original.toString(), updated.toString())); } else { this.addChange(new InactiveProvidesOrRequiresRemoval(field.coordinate, original.toString())); } }); } removeExternalOnTypeExtensions() { for (const type of this.schema.types()) { if (!(0, definitions_1.isCompositeType)(type)) { continue; } if (!isFederationTypeExtension(type) && !isRootTypeExtension(type)) { continue; } const keyApplications = type.appliedDirectivesOf(this.metadata.keyDirective()); if (keyApplications.length > 0) { for (const keyApplication of type.appliedDirectivesOf(this.metadata.keyDirective())) { (0, federation_1.collectTargetFields)({ parentType: type, directive: keyApplication, includeInterfaceFieldsImplementations: false, validate: false, }).forEach((field) => { if (field.parent !== type) { return; } const external = this.external(field); if (external) { this.addChange(new ExternalOnTypeExtensionRemoval(field.coordinate)); external.remove(); } }); } } else { const typeInOtherSubgraphs = Array.from(this.objectTypeMap.get(type.name).entries()).filter(([subgraphName, _]) => subgraphName !== this.subgraph.name); for (const [otherSubgraphName, v] of typeInOtherSubgraphs) { const [typeInOther, metadata] = v; (0, utils_1.assert)((0, definitions_1.isCompositeType)(typeInOther), () => `Type ${type} is of kind ${type.kind} in ${this.subgraph.name} but ${typeInOther.kind} in ${otherSubgraphName}`); const keysInOther = typeInOther.appliedDirectivesOf(metadata.keyDirective()); if (keysInOther.length === 0) { continue; } (0, federation_1.collectTargetFields)({ parentType: typeInOther, directive: keysInOther[0], includeInterfaceFieldsImplementations: false, validate: false, }).forEach((field) => { if (field.parent !== typeInOther) { return; } const ownField = type.field(field.name); if (!ownField) { return; } const external = this.external(ownField); if (external) { this.addChange(new ExternalOnTypeExtensionRemoval(ownField.coordinate)); external.remove(); } }); } } } } removeTypeExtensions() { for (const type of this.schema.types()) { if (!isFederationTypeExtension(type) && !isRootTypeExtension(type)) { continue; } this.addChange(new TypeExtensionRemoval(type.coordinate)); type.removeExtensions(); } } removeUnusedExternals() { for (const type of this.schema.types()) { if (!(0, definitions_1.isObjectType)(type) && !(0, definitions_1.isInterfaceType)(type)) { continue; } for (const field of type.fields()) { if (this.metadata.isFieldExternal(field) && !this.metadata.isFieldUsed(field)) { this.addChange(new UnusedExternalRemoval(field.coordinate)); field.remove(); } } if (!type.hasFields()) { if (type.isReferenced()) { this.addError(error_1.ERRORS.TYPE_WITH_ONLY_UNUSED_EXTERNAL.err(`Type ${type} contains only external fields and all those fields are all unused (they do not appear in any @key, @provides or @requires).`, { nodes: type.sourceAST })); } else { this.addChange(new TypeWithOnlyUnusedExternalRemoval(type.name)); type.remove(); } } } } removeDirectivesOnInterface() { for (const type of this.schema.interfaceTypes()) { for (const application of type.appliedDirectivesOf(this.metadata.keyDirective())) { this.addChange(new KeyOnInterfaceRemoval(type.name)); application.remove(); } for (const field of type.fields()) { for (const directive of [this.metadata.providesDirective(), this.metadata.requiresDirective()]) { for (const application of field.appliedDirectivesOf(directive)) { this.addChange(new ProvidesOrRequiresOnInterfaceFieldRemoval(field.coordinate, directive.name)); application.remove(); } } } } } removeProvidesOnNonComposite() { for (const type of this.schema.objectTypes()) { for (const field of type.fields()) { if ((0, definitions_1.isCompositeType)((0, definitions_1.baseType)(field.type))) { continue; } for (const application of field.appliedDirectivesOf(this.metadata.providesDirective())) { this.addChange(new ProvidesOnNonCompositeRemoval(field.coordinate, field.type.toString())); application.remove(); } } } } addShareable() { const originalMetadata = this.originalSubgraph.metadata(); const keyDirective = this.metadata.keyDirective(); const shareableDirective = this.metadata.shareableDirective(); for (const type of this.schema.objectTypes()) { if (type.isSubscriptionRootType()) { continue; } if (type.hasAppliedDirective(keyDirective) || (type.isRootType())) { for (const field of type.fields()) { if (originalMetadata.isFieldShareable(field)) { continue; } const entries = Array.from(this.objectTypeMap.get(type.name).entries()); const typeInOtherSubgraphs = entries.filter(([subgraphName, v]) => { if (subgraphName === this.subgraph.name) { return false; } const f = v[0].field(field.name); return !!f && (!v[1].isFieldExternal(f) || v[1].isFieldPartiallyExternal(f)); }); if (typeInOtherSubgraphs.length > 0 && !field.hasAppliedDirective(shareableDirective)) { field.applyDirective(shareableDirective); this.addChange(new ShareableFieldAddition(field.coordinate, typeInOtherSubgraphs.map(([s]) => s))); } } } else { const typeInOtherSubgraphs = Array.from(this.objectTypeMap.get(type.name).entries()).filter(([subgraphName, _]) => subgraphName !== this.subgraph.name); if (typeInOtherSubgraphs.length > 0 && !type.hasAppliedDirective(shareableDirective)) { type.applyDirective(shareableDirective); this.addChange(new ShareableTypeAddition(type.coordinate, typeInOtherSubgraphs.map(([s]) => s))); } } } } removeTagOnExternal() { const tagDirective = this.schema.directive('tag'); if (!tagDirective) { return; } for (const application of Array.from(tagDirective.applications())) { const element = application.parent; if (!(element instanceof definitions_1.FieldDefinition)) { continue; } if (this.external(element)) { const tagIsUsedInOtherDefinition = this.allSubgraphs .map((s) => s.name === this.originalSubgraph.name ? undefined : getField(s.schema, element.parent.name, element.name)) .filter((f) => !(f && f.hasAppliedDirective('external'))) .some((f) => f && f.appliedDirectivesOf('tag').some((d) => (0, values_1.valueEquals)(application.arguments(), d.arguments()))); if (tagIsUsedInOtherDefinition) { this.addChange(new RemovedTagOnExternal(application.toString(), element.coordinate)); application.remove(); } } } } } //# sourceMappingURL=schemaUpgrader.js.map