@apollo/federation-internals
Version:
Apollo Federation internal utilities
642 lines • 27.1 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.removeAllCoreFeatures = exports.LINK_VERSIONS = exports.CORE_VERSIONS = exports.findCoreSpecVersion = exports.FeatureUrl = exports.FeatureVersion = exports.FeatureDefinitions = exports.CoreSpecDefinition = exports.isCoreSpecDirectiveApplication = exports.extractCoreFeatureImports = exports.FeatureDefinition = exports.corePurposes = exports.ErrCoreCheckFailed = exports.linkDirectiveDefaultName = exports.linkIdentity = exports.coreIdentity = void 0;
const graphql_1 = require("graphql");
const url_1 = require("url");
const definitions_1 = require("../definitions");
const types_1 = require("../types");
const utils_1 = require("../utils");
const error_1 = require("../error");
const values_1 = require("../values");
const knownCoreFeatures_1 = require("../knownCoreFeatures");
const suggestions_1 = require("../suggestions");
const directiveAndTypeSpecification_1 = require("../directiveAndTypeSpecification");
exports.coreIdentity = 'https://specs.apollo.dev/core';
exports.linkIdentity = 'https://specs.apollo.dev/link';
exports.linkDirectiveDefaultName = 'link';
const ErrCoreCheckFailed = (causes) => (0, error_1.aggregateError)('CheckFailed', 'one or more checks failed', causes);
exports.ErrCoreCheckFailed = ErrCoreCheckFailed;
function buildError(message) {
return new Error(message);
}
exports.corePurposes = [
'SECURITY',
'EXECUTION',
];
function purposesDescription(purpose) {
switch (purpose) {
case 'SECURITY': return "`SECURITY` features provide metadata necessary to securely resolve fields.";
case 'EXECUTION': return "`EXECUTION` features provide metadata necessary for operation execution.";
}
}
class FeatureDefinition {
constructor(url, minimumFederationVersion) {
this.minimumFederationVersion = minimumFederationVersion;
this._directiveSpecs = new utils_1.MapWithCachedArrays();
this._typeSpecs = new utils_1.MapWithCachedArrays();
this.url = typeof url === 'string' ? FeatureUrl.parse(url) : url;
}
registerDirective(spec) {
this._directiveSpecs.set(spec.name, spec);
}
registerType(spec) {
this._typeSpecs.set(spec.name, spec);
}
registerSubFeature(subFeature) {
for (const typeSpec of subFeature.typeSpecs()) {
this.registerType(typeSpec);
}
for (const directiveSpec of subFeature.directiveSpecs()) {
this.registerDirective(directiveSpec);
}
}
directiveSpecs() {
return this._directiveSpecs.values();
}
directiveSpec(name) {
return this._directiveSpecs.get(name);
}
typeSpecs() {
return this._typeSpecs.values();
}
typeSpec(name) {
return this._typeSpecs.get(name);
}
get identity() {
return this.url.identity;
}
get version() {
return this.url.version;
}
isSpecType(type) {
const nameInSchema = this.nameInSchema(type.schema());
return nameInSchema !== undefined && type.name.startsWith(`${nameInSchema}__`);
}
isSpecDirective(directive) {
const nameInSchema = this.nameInSchema(directive.schema());
return nameInSchema != undefined && (directive.name === nameInSchema || directive.name.startsWith(`${nameInSchema}__`));
}
addElementsToSchema(schema) {
const feature = this.featureInSchema(schema);
(0, utils_1.assert)(feature, () => `The ${this.url} specification should have been added to the schema before this is called`);
let errors = [];
for (const type of this.typeSpecs()) {
errors = errors.concat(type.checkOrAdd(schema, feature));
}
for (const directive of this.directiveSpecs()) {
errors = errors.concat(directive.checkOrAdd(schema, feature));
}
return errors;
}
allElementNames() {
return this.directiveSpecs().map((spec) => `@${spec.name}`)
.concat(this.typeSpecs().map((spec) => spec.name));
}
nameInSchema(schema) {
const feature = this.featureInSchema(schema);
return feature === null || feature === void 0 ? void 0 : feature.nameInSchema;
}
directiveNameInSchema(schema, directiveName) {
const feature = this.featureInSchema(schema);
return feature ? feature.directiveNameInSchema(directiveName) : undefined;
}
typeNameInSchema(schema, typeName) {
const feature = this.featureInSchema(schema);
return feature ? feature.typeNameInSchema(typeName) : undefined;
}
rootDirective(schema) {
const name = this.nameInSchema(schema);
return name ? schema.directive(name) : undefined;
}
directive(schema, elementName) {
const name = this.directiveNameInSchema(schema, elementName);
return name ? schema.directive(name) : undefined;
}
type(schema, elementName) {
const name = this.typeNameInSchema(schema, elementName);
return name ? schema.type(name) : undefined;
}
addRootDirective(schema) {
return schema.addDirectiveDefinition(this.nameInSchema(schema));
}
addDirective(schema, name) {
return schema.addDirectiveDefinition(this.directiveNameInSchema(schema, name));
}
addScalarType(schema, name) {
return schema.addType(new definitions_1.ScalarType(this.typeNameInSchema(schema, name)));
}
addEnumType(schema, name) {
return schema.addType(new definitions_1.EnumType(this.typeNameInSchema(schema, name)));
}
featureInSchema(schema) {
const features = schema.coreFeatures;
if (!features) {
throw buildError(`Schema is not a core schema (add @link first)`);
}
return features.getByIdentity(this.identity);
}
get defaultCorePurpose() {
return undefined;
}
compositionSpecification(directiveNameInFeature) {
const spec = this._directiveSpecs.get(directiveNameInFeature);
return spec === null || spec === void 0 ? void 0 : spec.composition;
}
toString() {
return `${this.identity}/${this.version}`;
}
}
exports.FeatureDefinition = FeatureDefinition;
function extractCoreFeatureImports(url, directive) {
const args = directive.arguments();
if (!('import' in args) || !args.import) {
return [];
}
const importArgValue = args.import;
const definition = (0, knownCoreFeatures_1.coreFeatureDefinitionIfKnown)(url);
const knownElements = definition === null || definition === void 0 ? void 0 : definition.allElementNames();
const errors = [];
const imports = [];
importArgLoop: for (const elt of importArgValue) {
if (typeof elt === 'string') {
imports.push({ name: elt });
validateImportedName(elt, knownElements, errors, directive);
continue;
}
if (typeof elt !== 'object') {
errors.push(error_1.ERRORS.INVALID_LINK_DIRECTIVE_USAGE.err(`Invalid sub-value ${(0, values_1.valueToString)(elt)} for @link(import:) argument: values should be either strings or input object values of the form { name: "<importedElement>", as: "<alias>" }.`, { nodes: directive.sourceAST }));
continue;
}
let name;
for (const [key, value] of Object.entries(elt)) {
switch (key) {
case 'name':
if (typeof value !== 'string') {
errors.push(error_1.ERRORS.INVALID_LINK_DIRECTIVE_USAGE.err(`Invalid value for the "name" field for sub-value ${(0, values_1.valueToString)(elt)} of @link(import:) argument: must be a string.`, { nodes: directive.sourceAST }));
continue importArgLoop;
}
name = value;
break;
case 'as':
if (typeof value !== 'string') {
errors.push(error_1.ERRORS.INVALID_LINK_DIRECTIVE_USAGE.err(`Invalid value for the "as" field for sub-value ${(0, values_1.valueToString)(elt)} of @link(import:) argument: must be a string.`, { nodes: directive.sourceAST }));
continue importArgLoop;
}
break;
default:
errors.push(error_1.ERRORS.INVALID_LINK_DIRECTIVE_USAGE.err(`Unknown field "${key}" for sub-value ${(0, values_1.valueToString)(elt)} of @link(import:) argument.`, { nodes: directive.sourceAST }));
continue importArgLoop;
}
}
if (name) {
const i = elt;
imports.push(i);
if (i.as) {
if (i.name.charAt(0) === '@' && i.as.charAt(0) !== '@') {
errors.push(error_1.ERRORS.INVALID_LINK_DIRECTIVE_USAGE.err(`Invalid @link import renaming: directive "${i.name}" imported name should start with a '@' character, but got "${i.as}".`, { nodes: directive.sourceAST }));
}
else if (i.name.charAt(0) !== '@' && i.as.charAt(0) === '@') {
errors.push(error_1.ERRORS.INVALID_LINK_DIRECTIVE_USAGE.err(`Invalid @link import renaming: type "${i.name}" imported name should not start with a '@' character, but got "${i.as}" (or, if @${i.name} is a directive, then it should be referred to with a '@').`, { nodes: directive.sourceAST }));
}
}
validateImportedName(name, knownElements, errors, directive);
}
else {
errors.push(error_1.ERRORS.INVALID_LINK_DIRECTIVE_USAGE.err(`Invalid sub-value ${(0, values_1.valueToString)(elt)} for @link(import:) argument: missing mandatory "name" field.`, { nodes: directive.sourceAST }));
}
}
if (errors.length > 0) {
throw (0, definitions_1.ErrGraphQLValidationFailed)(errors);
}
return imports;
}
exports.extractCoreFeatureImports = extractCoreFeatureImports;
function validateImportedName(name, knownElements, errors, directive) {
if (knownElements && !knownElements.includes(name)) {
let details = '';
if (!name.startsWith('@') && knownElements.includes('@' + name)) {
details = ` Did you mean directive "@${name}"?`;
}
else {
const suggestions = (0, suggestions_1.suggestionList)(name, knownElements);
if (suggestions) {
details = (0, suggestions_1.didYouMean)(suggestions);
}
}
errors.push(error_1.ERRORS.INVALID_LINK_DIRECTIVE_USAGE.err(`Cannot import unknown element "${name}".${details}`, { nodes: directive.sourceAST }));
}
}
function isCoreSpecDirectiveApplication(directive) {
var _a, _b, _c;
const definition = directive.definition;
if (!definition) {
return false;
}
const asArg = definition.argument('as');
if (asArg && !(0, types_1.sameType)(asArg.type, directive.schema().stringType())) {
return false;
}
if (!definition.repeatable || definition.locations.length !== 1 || definition.locations[0] !== graphql_1.DirectiveLocation.SCHEMA) {
return false;
}
const urlArg = (_a = definition.argument('url')) !== null && _a !== void 0 ? _a : definition.argument('feature');
if (!urlArg || !isValidUrlArgumentType(urlArg.type, directive.schema())) {
return false;
}
const args = directive.arguments();
try {
const url = FeatureUrl.parse(args[urlArg.name]);
if (url.identity === exports.coreIdentity) {
return directive.name === ((_b = args.as) !== null && _b !== void 0 ? _b : 'core');
}
else {
return url.identity === exports.linkIdentity && directive.name === ((_c = args.as) !== null && _c !== void 0 ? _c : exports.linkDirectiveDefaultName);
}
}
catch (err) {
return false;
}
}
exports.isCoreSpecDirectiveApplication = isCoreSpecDirectiveApplication;
function isValidUrlArgumentType(type, schema) {
return (0, types_1.sameType)(type, schema.stringType())
|| (0, types_1.sameType)(type, new definitions_1.NonNullType(schema.stringType()));
}
const linkPurposeTypeSpec = (0, directiveAndTypeSpecification_1.createEnumTypeSpecification)({
name: 'Purpose',
values: exports.corePurposes.map((name) => ({ name, description: purposesDescription(name) }))
});
const linkImportTypeSpec = (0, directiveAndTypeSpecification_1.createScalarTypeSpecification)({ name: 'Import' });
class CoreSpecDefinition extends FeatureDefinition {
constructor(version, minimumFederationVersion, identity = exports.linkIdentity, name = exports.linkDirectiveDefaultName) {
super(new FeatureUrl(identity, name, version), minimumFederationVersion);
this.directiveDefinitionSpec = (0, directiveAndTypeSpecification_1.createDirectiveSpecification)({
name,
locations: [graphql_1.DirectiveLocation.SCHEMA],
repeatable: true,
args: this.createDefinitionArgumentSpecifications(),
});
this.registerDirective(this.directiveDefinitionSpec);
}
createDefinitionArgumentSpecifications() {
const args = [
{ name: this.urlArgName(), type: (schema) => schema.stringType() },
{ name: 'as', type: (schema) => schema.stringType() },
];
if (this.supportPurposes()) {
args.push({
name: 'for',
type: (schema, feature) => {
(0, utils_1.assert)(feature, "Shouldn't be added without being attached to a @link spec");
return schema.type(feature.typeNameInSchema(linkPurposeTypeSpec.name));
},
});
}
if (this.supportImport()) {
args.push({
name: 'import',
type: (schema, feature) => {
(0, utils_1.assert)(feature, "Shouldn't be added without being attached to a @link spec");
return new definitions_1.ListType(schema.type(feature.typeNameInSchema(linkImportTypeSpec.name)));
}
});
}
return args;
}
addElementsToSchema(_) {
return [];
}
addToSchema(schema, alias) {
const errors = this.addDefinitionsToSchema(schema, alias);
if (errors.length > 0) {
return errors;
}
const args = { [this.urlArgName()]: this.toString() };
if (alias) {
args.as = alias;
}
const schemaDef = schema.schemaDefinition;
const hasDefinition = schemaDef.hasNonExtensionElements();
const directive = schemaDef.applyDirective(alias !== null && alias !== void 0 ? alias : this.url.name, args, true);
if (!hasDefinition && schemaDef.hasExtensionElements()) {
const extension = (0, utils_1.firstOf)(schemaDef.extensions());
(0, utils_1.assert)(extension, '`hasExtensionElements` should not have been `true`');
directive.setOfExtension(extension);
}
return [];
}
addDefinitionsToSchema(schema, as, imports = []) {
const existingCore = schema.coreFeatures;
if (existingCore) {
if (existingCore.coreItself.url.identity === this.identity) {
return [];
}
else {
return [error_1.ERRORS.INVALID_LINK_DIRECTIVE_USAGE.err(`Cannot add feature ${this} to the schema, it already uses ${existingCore.coreItself.url}`)];
}
}
const nameInSchema = as !== null && as !== void 0 ? as : this.url.name;
const feature = new definitions_1.CoreFeature(this.url, nameInSchema, new definitions_1.Directive(nameInSchema), imports);
let errors = [];
errors = errors.concat(linkPurposeTypeSpec.checkOrAdd(schema, feature));
errors = errors.concat(linkImportTypeSpec.checkOrAdd(schema, feature));
errors = errors.concat(this.directiveDefinitionSpec.checkOrAdd(schema, feature));
return errors;
}
allElementNames() {
const names = [`@${this.url.name}`];
if (this.supportPurposes()) {
names.push('Purpose');
}
if (this.supportImport()) {
names.push('Import');
}
return names;
}
supportPurposes() {
return this.version.strictlyGreaterThan(new FeatureVersion(0, 1));
}
supportImport() {
return this.url.name === exports.linkDirectiveDefaultName;
}
extractFeature(schema) {
const features = schema.coreFeatures;
if (!features) {
throw buildError(`Schema is not a core schema (add @core first)`);
}
if (!features.coreItself.url.version.equals(this.version)) {
throw buildError(`Cannot use this version of @core (${this.version}), the schema uses version ${features.coreItself.url.version}`);
}
return features.coreItself;
}
coreDirective(schema) {
const feature = this.extractFeature(schema);
const directive = schema.directive(feature.nameInSchema);
return directive;
}
coreVersion(schema) {
const feature = this.extractFeature(schema);
return feature.url.version;
}
applyFeatureToSchema(schema, feature, as, purpose, imports) {
const coreDirective = this.coreDirective(schema);
const args = {
[this.urlArgName()]: feature.toString(),
as,
};
if (purpose) {
if (this.supportPurposes()) {
args.for = purpose;
}
else {
return [new graphql_1.GraphQLError(`Cannot apply feature ${feature} with purpose since the schema's @core/@link version does not support it.`)];
}
}
if (imports && imports.length > 0) {
if (this.supportImport()) {
args.import = imports.map(i => i.as ? i : i.name);
}
else {
return [new graphql_1.GraphQLError(`Cannot apply feature ${feature} with imports since the schema's @core/@link version does not support it.`)];
}
}
schema.schemaDefinition.applyDirective(coreDirective, args);
return feature.addElementsToSchema(schema);
}
extractFeatureUrl(args) {
return FeatureUrl.parse(args[this.urlArgName()]);
}
urlArgName() {
return this.url.name === 'core' ? 'feature' : 'url';
}
}
exports.CoreSpecDefinition = CoreSpecDefinition;
class FeatureDefinitions {
constructor(identity) {
this.identity = identity;
this._definitions = [];
}
add(definition) {
if (definition.identity !== this.identity) {
throw buildError(`Cannot add definition for ${definition} to the versions of definitions for ${this.identity}`);
}
if (this._definitions.find(def => definition.version.equals(def.version))) {
return this;
}
this._definitions.push(definition);
this._definitions.sort((def1, def2) => -def1.version.compareTo(def2.version));
return this;
}
find(requested) {
return this._definitions.find((def) => def.version.equals(requested));
}
versions() {
return this._definitions.map(def => def.version);
}
latest() {
(0, utils_1.assert)(this._definitions.length > 0, 'Trying to get latest when no definitions exist');
return this._definitions[0];
}
getMinimumRequiredVersion(fedVersion) {
var _a;
const def = this._definitions.find(def => def.minimumFederationVersion ? fedVersion.gte(def.minimumFederationVersion) : true);
(0, utils_1.assert)(def, `No compatible definition exists for federation version ${fedVersion}`);
const latestMajor = this.latest().version.major;
if (def.version.major !== latestMajor) {
return (_a = (0, utils_1.findLast)(this._definitions, def => def.version.major === latestMajor)) !== null && _a !== void 0 ? _a : this.latest();
}
return def;
}
}
exports.FeatureDefinitions = FeatureDefinitions;
class FeatureVersion {
constructor(major, minor) {
this.major = major;
this.minor = minor;
}
static parse(input) {
const match = input.match(this.VERSION_RE);
if (!match) {
throw error_1.ERRORS.INVALID_LINK_IDENTIFIER.err(`Expected a version string (of the form v1.2), got ${input}`);
}
return new this(+match[1], +match[2]);
}
static max(versions) {
let max;
for (const version of versions) {
if (!max || version.gt(max)) {
max = version;
}
}
return max;
}
satisfies(required) {
const { major, minor } = this;
const { major: rMajor, minor: rMinor } = required;
return rMajor == major && (major == 0
? rMinor == minor
: rMinor <= minor);
}
get series() {
const { major } = this;
return major > 0 ? `${major}.x` : String(this);
}
compareTo(other) {
if (this.major > other.major) {
return 1;
}
if (this.major < other.major) {
return -1;
}
if (this.minor > other.minor) {
return 1;
}
if (this.minor < other.minor) {
return -1;
}
return 0;
}
lt(other) {
return this.compareTo(other) < 0;
}
lte(other) {
return this.compareTo(other) <= 0;
}
gt(other) {
return this.compareTo(other) > 0;
}
gte(other) {
return this.compareTo(other) >= 0;
}
strictlyGreaterThan(version) {
return this.compareTo(version) > 0;
}
toString() {
return `v${this.major}.${this.minor}`;
}
equals(other) {
return this.major === other.major && this.minor === other.minor;
}
}
exports.FeatureVersion = FeatureVersion;
FeatureVersion.VERSION_RE = /^v(\d+)\.(\d+)$/;
class FeatureUrl {
constructor(identity, name, version, element) {
this.identity = identity;
this.name = name;
this.version = version;
this.element = element;
}
static maybeParse(input, node) {
try {
return FeatureUrl.parse(input, node);
}
catch (err) {
return undefined;
}
}
static parse(input, node) {
const url = new url_1.URL(input);
if (!url.pathname || url.pathname === '/') {
throw error_1.ERRORS.INVALID_LINK_IDENTIFIER.err(`Missing path in feature url '${url}'`, { nodes: node });
}
const path = url.pathname.split('/');
const verStr = path.pop();
if (!verStr) {
throw error_1.ERRORS.INVALID_LINK_IDENTIFIER.err(`Missing version component in feature url '${url}'`, { nodes: node });
}
const version = FeatureVersion.parse(verStr);
const name = path[path.length - 1];
if (!name) {
throw error_1.ERRORS.INVALID_LINK_IDENTIFIER.err(`Missing feature name component in feature url '${url}'`, { nodes: node });
}
const element = url.hash ? url.hash.slice(1) : undefined;
url.hash = '';
url.search = '';
url.password = '';
url.username = '';
url.pathname = path.join('/');
return new FeatureUrl(url.toString(), name, version, element);
}
static decode(node) {
return this.parse(node.value, node);
}
satisfies(requested) {
return requested.identity === this.identity &&
this.version.satisfies(requested.version);
}
equals(other) {
return this.identity === other.identity &&
this.version.equals(other.version);
}
get url() {
return this.element ?
`${this.identity}/${this.version}#${this.element}`
: `${this.identity}/${this.version}`;
}
get isDirective() {
var _a;
return (_a = this.element) === null || _a === void 0 ? void 0 : _a.startsWith('@');
}
get elementName() {
var _a;
return this.isDirective ? (_a = this.element) === null || _a === void 0 ? void 0 : _a.slice(1) : this.element;
}
get base() {
if (!this.element)
return this;
return new FeatureUrl(this.identity, this.name, this.version);
}
toString() {
return this.url;
}
}
exports.FeatureUrl = FeatureUrl;
function findCoreSpecVersion(featureUrl) {
return featureUrl.name === 'core'
? exports.CORE_VERSIONS.find(featureUrl.version)
: (featureUrl.name === exports.linkDirectiveDefaultName ? exports.LINK_VERSIONS.find(featureUrl.version) : undefined);
}
exports.findCoreSpecVersion = findCoreSpecVersion;
exports.CORE_VERSIONS = new FeatureDefinitions(exports.coreIdentity)
.add(new CoreSpecDefinition(new FeatureVersion(0, 1), undefined, exports.coreIdentity, 'core'))
.add(new CoreSpecDefinition(new FeatureVersion(0, 2), new FeatureVersion(2, 0), exports.coreIdentity, 'core'));
exports.LINK_VERSIONS = new FeatureDefinitions(exports.linkIdentity)
.add(new CoreSpecDefinition(new FeatureVersion(1, 0), new FeatureVersion(2, 0)));
(0, knownCoreFeatures_1.registerKnownFeature)(exports.CORE_VERSIONS);
(0, knownCoreFeatures_1.registerKnownFeature)(exports.LINK_VERSIONS);
function removeAllCoreFeatures(schema) {
var _a, _b;
const coreFeatures = [...((_b = (_a = schema.coreFeatures) === null || _a === void 0 ? void 0 : _a.allFeatures()) !== null && _b !== void 0 ? _b : [])];
const typeReferences = [];
for (const feature of coreFeatures) {
const featureDirectiveDefs = schema.directives()
.filter(d => feature.isFeatureDefinition(d));
featureDirectiveDefs.forEach(def => def.remove().forEach(application => application.remove()));
const featureTypes = schema.types()
.filter(t => feature.isFeatureDefinition(t));
featureTypes.forEach(type => {
const references = type.remove();
if (references.length > 0) {
typeReferences.push({
feature,
type,
references,
});
}
});
}
const errors = [];
for (const { feature, type, references } of typeReferences) {
const referencesInSchema = references.filter(r => r.isAttached());
if (referencesInSchema.length > 0) {
errors.push(error_1.ERRORS.REFERENCED_INACCESSIBLE.err(`Cannot remove elements of feature ${feature} as feature type ${type}` +
` is referenced by elements: ${referencesInSchema.join(', ')}`, { nodes: (0, definitions_1.sourceASTs)(...references) }));
}
}
if (errors.length > 0) {
throw (0, definitions_1.ErrGraphQLAPISchemaValidationFailed)(errors);
}
}
exports.removeAllCoreFeatures = removeAllCoreFeatures;
//# sourceMappingURL=coreSpec.js.map
;