UNPKG

@apollo/federation-internals

Version:
642 lines 27.1 kB
"use strict"; 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