@apollo/federation-internals
Version:
Apollo Federation internal utilities
589 lines • 27 kB
JavaScript
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
;