@apollo/federation-internals
Version:
Apollo Federation internal utilities
937 lines • 97.3 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.removeInactiveProvidesAndRequires = exports.addSubgraphToError = exports.addSubgraphToASTNode = exports.Subgraph = exports.FEDERATION_OPERATION_FIELDS = exports.entitiesFieldName = exports.serviceFieldName = exports.FEDERATION_OPERATION_TYPES = exports.entityTypeSpec = exports.serviceTypeSpec = exports.anyTypeSpec = exports.Subgraphs = exports.subgraphsFromServiceList = exports.collectTargetFields = exports.parseFieldSetArgument = exports.newEmptyFederation2Schema = exports.buildSubgraph = exports.isInterfaceObjectType = exports.isEntityType = exports.isFederationField = exports.isFederationSubgraphSchema = exports.federationMetadata = exports.printSubgraphNames = exports.asFed2SubgraphDocument = exports.FEDERATION2_LINK_WITH_AUTO_EXPANDED_IMPORTS_UPGRADED = exports.FEDERATION2_LINK_WITH_AUTO_EXPANDED_IMPORTS = exports.FEDERATION2_LINK_WITH_FULL_IMPORTS = exports.setSchemaAsFed2Subgraph = exports.FederationBlueprint = exports.hasAppliedDirective = exports.isFederationDirectiveDefinedInSchema = exports.FederationMetadata = exports.collectUsedFields = exports.parseContext = exports.FEDERATION_UNNAMED_SUBGRAPH_NAME = exports.FEDERATION_RESERVED_SUBGRAPH_NAME = void 0;
const definitions_1 = require("./definitions");
const utils_1 = require("./utils");
const specifiedRules_1 = require("graphql/validation/specifiedRules");
const graphql_1 = require("graphql");
const KnownTypeNamesInFederationRule_1 = require("./validation/KnownTypeNamesInFederationRule");
const buildSchema_1 = require("./buildSchema");
const operations_1 = require("./operations");
const tagSpec_1 = require("./specs/tagSpec");
const error_1 = require("./error");
const precompute_1 = require("./precompute");
const coreSpec_1 = require("./specs/coreSpec");
const federationSpec_1 = require("./specs/federationSpec");
const print_1 = require("./print");
const directiveAndTypeSpecification_1 = require("./directiveAndTypeSpecification");
const suggestions_1 = require("./suggestions");
const knownCoreFeatures_1 = require("./knownCoreFeatures");
const joinSpec_1 = require("./specs/joinSpec");
const costSpec_1 = require("./specs/costSpec");
const linkSpec = coreSpec_1.LINK_VERSIONS.latest();
const tagSpec = tagSpec_1.TAG_VERSIONS.latest();
const federationSpec = (version) => {
if (!version)
return federationSpec_1.FEDERATION_VERSIONS.latest();
const spec = federationSpec_1.FEDERATION_VERSIONS.find(version);
(0, utils_1.assert)(spec, `Federation spec version ${version} is not known`);
return spec;
};
const autoExpandedFederationSpec = federationSpec(new coreSpec_1.FeatureVersion(2, 4));
const latestFederationSpec = federationSpec();
exports.FEDERATION_RESERVED_SUBGRAPH_NAME = '_';
exports.FEDERATION_UNNAMED_SUBGRAPH_NAME = '<unnamed>';
const FEDERATION_OMITTED_VALIDATION_RULES = [
graphql_1.PossibleTypeExtensionsRule,
graphql_1.KnownTypeNamesRule
];
const FEDERATION_SPECIFIC_VALIDATION_RULES = [
KnownTypeNamesInFederationRule_1.KnownTypeNamesInFederationRule
];
const FEDERATION_VALIDATION_RULES = specifiedRules_1.specifiedSDLRules.filter(rule => !FEDERATION_OMITTED_VALIDATION_RULES.includes(rule)).concat(FEDERATION_SPECIFIC_VALIDATION_RULES);
const ALL_DEFAULT_FEDERATION_DIRECTIVE_NAMES = Object.values(federationSpec_1.FederationDirectiveName);
const FAKE_FED1_CORE_FEATURE_TO_RENAME_TYPES = new definitions_1.CoreFeature(new coreSpec_1.FeatureUrl('<fed1>', 'fed1', new coreSpec_1.FeatureVersion(0, 1)), 'fed1', new definitions_1.Directive('fed1'), federationSpec_1.FEDERATION1_TYPES.map((spec) => ({ name: spec.name, as: '_' + spec.name })));
function validateFieldSetSelections({ directiveName, selectionSet, hasExternalInParents, metadata, onError, allowOnNonExternalLeafFields, allowFieldsWithArguments, }) {
for (const selection of selectionSet.selections()) {
const appliedDirectives = selection.element.appliedDirectives;
if (appliedDirectives.length > 0) {
onError(error_1.ERROR_CATEGORIES.DIRECTIVE_IN_FIELDS_ARG.get(directiveName).err(`cannot have directive applications in the @${directiveName}(fields:) argument but found ${appliedDirectives.join(', ')}.`));
}
if (selection.kind === 'FieldSelection') {
const field = selection.element.definition;
const isExternal = metadata.isFieldExternal(field);
if (!allowFieldsWithArguments && field.hasArguments()) {
onError(error_1.ERROR_CATEGORIES.FIELDS_HAS_ARGS.get(directiveName).err(`field ${field.coordinate} cannot be included because it has arguments (fields with argument are not allowed in @${directiveName})`, { nodes: field.sourceAST }));
}
const mustBeExternal = !selection.selectionSet && !allowOnNonExternalLeafFields && !hasExternalInParents;
if (!isExternal && mustBeExternal) {
const errorCode = error_1.ERROR_CATEGORIES.DIRECTIVE_FIELDS_MISSING_EXTERNAL.get(directiveName);
if (metadata.isFieldFakeExternal(field)) {
onError(errorCode.err(`field "${field.coordinate}" should not be part of a @${directiveName} since it is already "effectively" provided by this subgraph `
+ `(while it is marked @${federationSpec_1.FederationDirectiveName.EXTERNAL}, it is a @${federationSpec_1.FederationDirectiveName.KEY} field of an extension type, which are not internally considered external for historical/backward compatibility reasons)`, { nodes: field.sourceAST }));
}
else {
onError(errorCode.err(`field "${field.coordinate}" should not be part of a @${directiveName} since it is already provided by this subgraph (it is not marked @${federationSpec_1.FederationDirectiveName.EXTERNAL})`, { nodes: field.sourceAST }));
}
}
if (selection.selectionSet) {
let newHasExternalInParents = hasExternalInParents || isExternal;
const parentType = field.parent;
if (!newHasExternalInParents && (0, definitions_1.isInterfaceType)(parentType)) {
for (const implem of parentType.possibleRuntimeTypes()) {
const fieldInImplem = implem.field(field.name);
if (fieldInImplem && metadata.isFieldExternal(fieldInImplem)) {
newHasExternalInParents = true;
break;
}
}
}
validateFieldSetSelections({
directiveName,
selectionSet: selection.selectionSet,
hasExternalInParents: newHasExternalInParents,
metadata,
onError,
allowOnNonExternalLeafFields,
allowFieldsWithArguments,
});
}
}
else {
validateFieldSetSelections({
directiveName,
selectionSet: selection.selectionSet,
hasExternalInParents,
metadata,
onError,
allowOnNonExternalLeafFields,
allowFieldsWithArguments,
});
}
}
}
function validateFieldSet({ type, directive, metadata, errorCollector, allowOnNonExternalLeafFields, allowFieldsWithArguments, onFields, }) {
try {
const fieldAccessor = onFields
? (type, fieldName) => {
const field = type.field(fieldName);
if (field) {
onFields(field);
}
return field;
}
: undefined;
const selectionSet = parseFieldSetArgument({ parentType: type, directive, fieldAccessor });
validateFieldSetSelections({
directiveName: directive.name,
selectionSet,
hasExternalInParents: false,
metadata,
onError: (error) => errorCollector.push(handleFieldSetValidationError(directive, error)),
allowOnNonExternalLeafFields,
allowFieldsWithArguments,
});
}
catch (e) {
if (e instanceof graphql_1.GraphQLError) {
errorCollector.push(e);
}
else {
throw e;
}
}
}
function handleFieldSetValidationError(directive, originalError, messageUpdater) {
const nodes = (0, definitions_1.sourceASTs)(directive);
if (originalError.nodes) {
nodes.push(...originalError.nodes);
}
let codeDef = (0, error_1.errorCodeDef)(originalError);
if (!codeDef || codeDef === error_1.ERRORS.INVALID_GRAPHQL) {
codeDef = error_1.ERROR_CATEGORIES.DIRECTIVE_INVALID_FIELDS.get(directive.name);
}
let msg = originalError.message.trim();
if (messageUpdater) {
msg = messageUpdater(msg);
}
return codeDef.err(`${fieldSetErrorDescriptor(directive)}: ${msg}`, {
nodes,
originalError,
});
}
function fieldSetErrorDescriptor(directive) {
return `On ${fieldSetTargetDescription(directive)}, for ${directiveStrUsingASTIfPossible(directive)}`;
}
function directiveStrUsingASTIfPossible(directive) {
return directive.sourceAST ? (0, graphql_1.print)(directive.sourceAST) : directive.toString();
}
function fieldSetTargetDescription(directive) {
var _a;
const targetKind = directive.parent instanceof definitions_1.FieldDefinition ? "field" : "type";
return `${targetKind} "${(_a = directive.parent) === null || _a === void 0 ? void 0 : _a.coordinate}"`;
}
function parseContext(input) {
const regex = /^(?:[\n\r\t ,]|#[^\n\r]*(?![^\n\r]))*\$(?:[\n\r\t ,]|#[^\n\r]*(?![^\n\r]))*([A-Za-z_]\w*(?!\w))([\s\S]*)$/;
const match = input.match(regex);
if (!match) {
return { context: undefined, selection: undefined };
}
const [, context, selection] = match;
return {
context,
selection,
};
}
exports.parseContext = parseContext;
const wrapResolvedType = ({ originalType, resolvedType, }) => {
const stack = [];
let unwrappedType = originalType;
while (unwrappedType.kind === 'NonNullType' || unwrappedType.kind === 'ListType') {
stack.push(unwrappedType.kind);
unwrappedType = unwrappedType.ofType;
}
let type = resolvedType;
while (stack.length > 0) {
const kind = stack.pop();
if (kind === 'ListType') {
type = new definitions_1.ListType(type);
}
}
return type;
};
const validateFieldValueType = ({ currentType, selectionSet, errorCollector, metadata, fromContextParent, }) => {
const selections = selectionSet.selections();
const interfaceObjectDirective = metadata.interfaceObjectDirective();
if (currentType.kind === 'ObjectType' && isFederationDirectiveDefinedInSchema(interfaceObjectDirective) && (currentType.appliedDirectivesOf(interfaceObjectDirective).length > 0)) {
errorCollector.push(error_1.ERRORS.CONTEXT_INVALID_SELECTION.err(`Context "is used in "${fromContextParent.coordinate}" but the selection is invalid: One of the types in the selection is an interfaceObject: "${currentType.name}"`, { nodes: (0, definitions_1.sourceASTs)(fromContextParent) }));
}
const typesArray = selections.map((selection) => {
if (selection.kind !== 'FieldSelection') {
return { resolvedType: undefined };
}
const { element, selectionSet: childSelectionSet } = selection;
(0, utils_1.assert)(element.definition.type, 'Element type definition should exist');
let type = element.definition.type;
if (childSelectionSet) {
(0, utils_1.assert)((0, definitions_1.isCompositeType)((0, definitions_1.baseType)(type)), 'Child selection sets should only exist on composite types');
const { resolvedType } = validateFieldValueType({
currentType: (0, definitions_1.baseType)(type),
selectionSet: childSelectionSet,
errorCollector,
metadata,
fromContextParent,
});
if (!resolvedType) {
return { resolvedType: undefined };
}
return { resolvedType: wrapResolvedType({ originalType: type, resolvedType }) };
}
(0, utils_1.assert)((0, definitions_1.isLeafType)((0, definitions_1.baseType)(type)), 'Expected a leaf type');
return {
resolvedType: wrapResolvedType({
originalType: type,
resolvedType: (0, definitions_1.baseType)(type)
})
};
});
return typesArray.reduce((acc, { resolvedType }) => {
var _a;
if (((_a = acc.resolvedType) === null || _a === void 0 ? void 0 : _a.toString()) === (resolvedType === null || resolvedType === void 0 ? void 0 : resolvedType.toString())) {
return { resolvedType };
}
return { resolvedType: undefined };
});
};
const validateSelectionFormat = ({ context, selection, fromContextParent, errorCollector, }) => {
try {
const node = (0, operations_1.parseOperationAST)(selection.trim().startsWith('{') ? selection : `{${selection}}`);
const selections = node.selectionSet.selections;
if (selections.length === 0) {
errorCollector.push(error_1.ERRORS.CONTEXT_INVALID_SELECTION.err(`Context "${context}" is used in "${fromContextParent.coordinate}" but the selection is invalid: no selection is made`, { nodes: (0, definitions_1.sourceASTs)(fromContextParent) }));
return { selectionType: 'error' };
}
const firstSelectionKind = selections[0].kind;
if (firstSelectionKind === 'Field') {
if (selections.length !== 1) {
errorCollector.push(error_1.ERRORS.CONTEXT_INVALID_SELECTION.err(`Context "${context}" is used in "${fromContextParent.coordinate}" but the selection is invalid: multiple selections are made`, { nodes: (0, definitions_1.sourceASTs)(fromContextParent) }));
return { selectionType: 'error' };
}
return { selectionType: 'field' };
}
else if (firstSelectionKind === 'InlineFragment') {
const inlineFragmentTypeConditions = new Set();
if (!selections.every((s) => s.kind === 'InlineFragment')) {
errorCollector.push(error_1.ERRORS.CONTEXT_INVALID_SELECTION.err(`Context "${context}" is used in "${fromContextParent.coordinate}" but the selection is invalid: multiple fields could be selected`, { nodes: (0, definitions_1.sourceASTs)(fromContextParent) }));
return { selectionType: 'error' };
}
selections.forEach((s) => {
(0, utils_1.assert)(s.kind === 'InlineFragment', 'Expected an inline fragment');
const { typeCondition } = s;
if (typeCondition) {
inlineFragmentTypeConditions.add(typeCondition.name.value);
}
});
if (inlineFragmentTypeConditions.size !== selections.length) {
errorCollector.push(error_1.ERRORS.CONTEXT_INVALID_SELECTION.err(`Context "${context}" is used in "${fromContextParent.coordinate}" but the selection is invalid: type conditions have same name`, { nodes: (0, definitions_1.sourceASTs)(fromContextParent) }));
return { selectionType: 'error' };
}
return {
selectionType: 'inlineFragment',
typeConditions: inlineFragmentTypeConditions,
};
}
else if (firstSelectionKind === 'FragmentSpread') {
errorCollector.push(error_1.ERRORS.CONTEXT_INVALID_SELECTION.err(`Context "${context}" is used in "${fromContextParent.coordinate}" but the selection is invalid: fragment spread is not allowed`, { nodes: (0, definitions_1.sourceASTs)(fromContextParent) }));
return { selectionType: 'error' };
}
else {
(0, utils_1.assertUnreachable)(firstSelectionKind);
}
}
catch (err) {
errorCollector.push(error_1.ERRORS.CONTEXT_INVALID_SELECTION.err(`Context "${context}" is used in "${fromContextParent.coordinate}" but the selection is invalid: ${err.message}`, { nodes: (0, definitions_1.sourceASTs)(fromContextParent) }));
return { selectionType: 'error' };
}
};
function isValidImplementationFieldType(fieldType, implementedFieldType) {
if ((0, definitions_1.isNonNullType)(fieldType)) {
if ((0, definitions_1.isNonNullType)(implementedFieldType)) {
return isValidImplementationFieldType(fieldType.ofType, implementedFieldType.ofType);
}
else {
return isValidImplementationFieldType(fieldType.ofType, implementedFieldType);
}
}
if ((0, definitions_1.isListType)(fieldType) && (0, definitions_1.isListType)(implementedFieldType)) {
return isValidImplementationFieldType(fieldType.ofType, implementedFieldType.ofType);
}
return !(0, definitions_1.isWrapperType)(fieldType) &&
!(0, definitions_1.isWrapperType)(implementedFieldType) &&
fieldType.name === implementedFieldType.name;
}
function selectionSetHasDirectives(selectionSet) {
return (0, operations_1.hasSelectionWithPredicate)(selectionSet, (s) => {
if (s.kind === 'FieldSelection') {
return s.element.appliedDirectives.length > 0;
}
else if (s.kind === 'FragmentSelection') {
return s.element.appliedDirectives.length > 0;
}
else {
(0, utils_1.assertUnreachable)(s);
}
});
}
function selectionSetHasAlias(selectionSet) {
return (0, operations_1.hasSelectionWithPredicate)(selectionSet, (s) => {
if (s.kind === 'FieldSelection') {
return s.element.alias !== undefined;
}
return false;
});
}
function validateFieldValue({ context, selection, fromContextParent, setContextLocations, errorCollector, metadata, }) {
const expectedType = fromContextParent.type;
(0, utils_1.assert)(expectedType, 'Expected a type');
const validateSelectionFormatResults = validateSelectionFormat({ context, selection, fromContextParent, errorCollector });
const selectionType = validateSelectionFormatResults.selectionType;
if (selectionType === 'error') {
return;
}
const usedTypeConditions = new Set;
for (const location of setContextLocations) {
let selectionSet;
try {
selectionSet = (0, operations_1.parseSelectionSet)({ parentType: location, source: selection });
}
catch (e) {
errorCollector.push(error_1.ERRORS.CONTEXT_INVALID_SELECTION.err(`Context "${context}" is used in "${fromContextParent.coordinate}" but the selection is invalid for type ${location.name}. Error: ${e.message}`, { nodes: (0, definitions_1.sourceASTs)(fromContextParent) }));
return;
}
if (selectionSetHasDirectives(selectionSet)) {
errorCollector.push(error_1.ERRORS.CONTEXT_INVALID_SELECTION.err(`Context "${context}" is used in "${fromContextParent.coordinate}" but the selection is invalid: directives are not allowed in the selection`, { nodes: (0, definitions_1.sourceASTs)(fromContextParent) }));
}
if (selectionSetHasAlias(selectionSet)) {
errorCollector.push(error_1.ERRORS.CONTEXT_INVALID_SELECTION.err(`Context "${context}" is used in "${fromContextParent.coordinate}" but the selection is invalid: aliases are not allowed in the selection`, { nodes: (0, definitions_1.sourceASTs)(fromContextParent) }));
}
if (selectionType === 'field') {
const { resolvedType } = validateFieldValueType({
currentType: location,
selectionSet,
errorCollector,
metadata,
fromContextParent,
});
if (resolvedType === undefined || !isValidImplementationFieldType(resolvedType, expectedType)) {
errorCollector.push(error_1.ERRORS.CONTEXT_INVALID_SELECTION.err(`Context "${context}" is used in "${fromContextParent.coordinate}" but the selection is invalid: the type of the selection "${resolvedType}" does not match the expected type "${expectedType === null || expectedType === void 0 ? void 0 : expectedType.toString()}"`, { nodes: (0, definitions_1.sourceASTs)(fromContextParent) }));
return;
}
}
else if (selectionType === 'inlineFragment') {
const selections = [];
for (const selection of selectionSet.selections()) {
if (selection.kind !== 'FragmentSelection') {
errorCollector.push(error_1.ERRORS.CONTEXT_INVALID_SELECTION.err(`Context "${context}" is used in "${fromContextParent.coordinate}" but the selection is invalid: selection should only contain a single field or at least one inline fragment}"`, { nodes: (0, definitions_1.sourceASTs)(fromContextParent) }));
continue;
}
const { typeCondition } = selection.element;
if (!typeCondition) {
errorCollector.push(error_1.ERRORS.CONTEXT_INVALID_SELECTION.err(`Context "${context}" is used in "${fromContextParent.coordinate}" but the selection is invalid: inline fragments must have type conditions"`, { nodes: (0, definitions_1.sourceASTs)(fromContextParent) }));
continue;
}
if (typeCondition.kind === 'ObjectType') {
if ((0, definitions_1.possibleRuntimeTypes)(location).includes(typeCondition)) {
selections.push(selection);
usedTypeConditions.add(typeCondition.name);
}
}
else {
errorCollector.push(error_1.ERRORS.CONTEXT_INVALID_SELECTION.err(`Context "${context}" is used in "${fromContextParent.coordinate}" but the selection is invalid: type conditions must be an object type"`, { nodes: (0, definitions_1.sourceASTs)(fromContextParent) }));
}
}
if (selections.length === 0) {
errorCollector.push(error_1.ERRORS.CONTEXT_INVALID_SELECTION.err(`Context "${context}" is used in "${fromContextParent.coordinate}" but the selection is invalid: no type condition matches the location "${location.coordinate}"`, { nodes: (0, definitions_1.sourceASTs)(fromContextParent) }));
return;
}
else {
for (const selection of selections) {
let { resolvedType } = validateFieldValueType({
currentType: selection.element.typeCondition,
selectionSet: selection.selectionSet,
errorCollector,
metadata,
fromContextParent,
});
if (resolvedType === undefined) {
errorCollector.push(error_1.ERRORS.CONTEXT_INVALID_SELECTION.err(`Context "${context}" is used in "${fromContextParent.coordinate}" but the selection is invalid: the type of the selection does not match the expected type "${expectedType === null || expectedType === void 0 ? void 0 : expectedType.toString()}"`, { nodes: (0, definitions_1.sourceASTs)(fromContextParent) }));
return;
}
if ((0, definitions_1.isNonNullType)(resolvedType)) {
resolvedType = resolvedType.ofType;
}
if (!isValidImplementationFieldType(resolvedType, expectedType)) {
errorCollector.push(error_1.ERRORS.CONTEXT_INVALID_SELECTION.err(`Context "${context}" is used in "${fromContextParent.coordinate}" but the selection is invalid: the type of the selection "${resolvedType === null || resolvedType === void 0 ? void 0 : resolvedType.toString()}" does not match the expected type "${expectedType === null || expectedType === void 0 ? void 0 : expectedType.toString()}"`, { nodes: (0, definitions_1.sourceASTs)(fromContextParent) }));
return;
}
}
}
}
}
if (validateSelectionFormatResults.selectionType === 'inlineFragment') {
for (const typeCondition of validateSelectionFormatResults.typeConditions) {
if (!usedTypeConditions.has(typeCondition)) {
errorCollector.push(error_1.ERRORS.CONTEXT_INVALID_SELECTION.err(`Context "${context}" is used in "${fromContextParent.coordinate}" but the selection is invalid: type condition "${typeCondition}" is never used.`, { nodes: (0, definitions_1.sourceASTs)(fromContextParent) }));
}
}
}
}
function validateAllFieldSet({ definition, targetTypeExtractor, errorCollector, metadata, isOnParentType = false, allowOnNonExternalLeafFields = false, allowFieldsWithArguments = false, allowOnInterface = false, onFields, }) {
for (const application of definition.applications()) {
const elt = application.parent;
const type = targetTypeExtractor(elt);
const parentType = isOnParentType ? type : elt.parent;
if ((0, definitions_1.isInterfaceType)(parentType) && !allowOnInterface) {
const code = error_1.ERROR_CATEGORIES.DIRECTIVE_UNSUPPORTED_ON_INTERFACE.get(definition.name);
errorCollector.push(code.err(isOnParentType
? `Cannot use ${definition.coordinate} on interface "${parentType.coordinate}": ${definition.coordinate} is not yet supported on interfaces`
: `Cannot use ${definition.coordinate} on ${fieldSetTargetDescription(application)} of parent type "${parentType}": ${definition.coordinate} is not yet supported within interfaces`, { nodes: (0, definitions_1.sourceASTs)(application).concat(isOnParentType ? [] : (0, definitions_1.sourceASTs)(type)) }));
}
validateFieldSet({
type,
directive: application,
metadata,
errorCollector,
allowOnNonExternalLeafFields,
allowFieldsWithArguments,
onFields,
});
}
}
function collectUsedFields(metadata) {
const usedFields = new Set();
collectUsedFieldsForDirective(metadata.keyDirective(), type => type, usedFields);
collectUsedFieldsForDirective(metadata.requiresDirective(), field => field.parent, usedFields);
collectUsedFieldsForDirective(metadata.providesDirective(), field => {
const type = (0, definitions_1.baseType)(field.type);
return (0, definitions_1.isCompositeType)(type) ? type : undefined;
}, usedFields);
collectUsedFieldsForFromContext(metadata, usedFields);
for (const itfType of metadata.schema.interfaceTypes()) {
const runtimeTypes = itfType.possibleRuntimeTypes();
for (const field of itfType.fields()) {
for (const runtimeType of runtimeTypes) {
const implemField = runtimeType.field(field.name);
if (implemField) {
usedFields.add(implemField);
}
}
}
}
return usedFields;
}
exports.collectUsedFields = collectUsedFields;
function collectUsedFieldsForFromContext(metadata, usedFieldDefs) {
const fromContextDirective = metadata.fromContextDirective();
const contextDirective = metadata.contextDirective();
if (!isFederationDirectiveDefinedInSchema(fromContextDirective) || !isFederationDirectiveDefinedInSchema(contextDirective)) {
return;
}
const entryPoints = new Map();
for (const application of contextDirective.applications()) {
const type = application.parent;
if (!type) {
continue;
}
const context = application.arguments().name;
if (!entryPoints.has(context)) {
entryPoints.set(context, new Set());
}
entryPoints.get(context).add(type);
}
for (const application of fromContextDirective.applications()) {
const type = application.parent;
if (!type) {
continue;
}
const fieldValue = application.arguments().field;
const { context, selection } = parseContext(fieldValue);
if (!context) {
continue;
}
const contextTypes = entryPoints.get(context);
if (!contextTypes) {
continue;
}
for (const contextType of contextTypes) {
try {
const fieldAccessor = (t, f) => {
const field = t.field(f);
if (field) {
usedFieldDefs.add(field);
if ((0, definitions_1.isInterfaceType)(t)) {
for (const implType of t.possibleRuntimeTypes()) {
const implField = implType.field(f);
if (implField) {
usedFieldDefs.add(implField);
}
}
}
}
return field;
};
(0, operations_1.parseSelectionSet)({ parentType: contextType, source: selection, fieldAccessor });
}
catch (e) {
}
}
}
}
function collectUsedFieldsForDirective(definition, targetTypeExtractor, usedFieldDefs) {
for (const application of definition.applications()) {
const type = targetTypeExtractor(application.parent);
if (!type) {
continue;
}
collectTargetFields({
parentType: type,
directive: application,
includeInterfaceFieldsImplementations: true,
validate: false,
}).forEach((field) => usedFieldDefs.add(field));
}
}
function validateAllExternalFieldsUsed(metadata, errorCollector) {
for (const type of metadata.schema.types()) {
if (!(0, definitions_1.isObjectType)(type) && !(0, definitions_1.isInterfaceType)(type)) {
continue;
}
for (const field of type.fields()) {
if (!metadata.isFieldExternal(field) || metadata.isFieldUsed(field)) {
continue;
}
errorCollector.push(error_1.ERRORS.EXTERNAL_UNUSED.err(`Field "${field.coordinate}" is marked @external but is not used in any federation directive (@key, @provides, @requires) or to satisfy an interface;`
+ ' the field declaration has no use and should be removed (or the field should not be @external).', { nodes: field.sourceAST }));
}
}
}
function validateNoExternalOnInterfaceFields(metadata, errorCollector) {
for (const itf of metadata.schema.interfaceTypes()) {
for (const field of itf.fields()) {
if (metadata.isFieldExternal(field)) {
errorCollector.push(error_1.ERRORS.EXTERNAL_ON_INTERFACE.err(`Interface type field "${field.coordinate}" is marked @external but @external is not allowed on interface fields (it is nonsensical).`, { nodes: field.sourceAST }));
}
}
}
}
function validateKeyOnInterfacesAreAlsoOnAllImplementations(metadata, errorCollector) {
for (const itfType of metadata.schema.interfaceTypes()) {
const implementations = itfType.possibleRuntimeTypes();
for (const keyApplication of itfType.appliedDirectivesOf(metadata.keyDirective())) {
const fields = parseFieldSetArgument({ parentType: itfType, directive: keyApplication, validate: false });
const isResolvable = !(keyApplication.arguments().resolvable === false);
const implementationsWithKeyButNotResolvable = new Array();
const implementationsMissingKey = new Array();
for (const type of implementations) {
const matchingApp = type.appliedDirectivesOf(metadata.keyDirective()).find((app) => {
const appFields = parseFieldSetArgument({ parentType: type, directive: app, validate: false });
return fields.equals(appFields);
});
if (matchingApp) {
if (isResolvable && matchingApp.arguments().resolvable === false) {
implementationsWithKeyButNotResolvable.push(type);
}
}
else {
implementationsMissingKey.push(type);
}
}
if (implementationsMissingKey.length > 0) {
const typesString = (0, utils_1.printHumanReadableList)(implementationsMissingKey.map((i) => `"${i.coordinate}"`), {
prefix: 'type',
prefixPlural: 'types',
});
errorCollector.push(error_1.ERRORS.INTERFACE_KEY_NOT_ON_IMPLEMENTATION.err(`Key ${keyApplication} on interface type "${itfType.coordinate}" is missing on implementation ${typesString}.`, { nodes: (0, definitions_1.sourceASTs)(...implementationsMissingKey) }));
}
else if (implementationsWithKeyButNotResolvable.length > 0) {
const typesString = (0, utils_1.printHumanReadableList)(implementationsWithKeyButNotResolvable.map((i) => `"${i.coordinate}"`), {
prefix: 'type',
prefixPlural: 'types',
});
errorCollector.push(error_1.ERRORS.INTERFACE_KEY_NOT_ON_IMPLEMENTATION.err(`Key ${keyApplication} on interface type "${itfType.coordinate}" should be resolvable on all implementation types, but is declared with argument "@key(resolvable:)" set to false in ${typesString}.`, { nodes: (0, definitions_1.sourceASTs)(...implementationsWithKeyButNotResolvable) }));
}
}
}
}
function validateInterfaceObjectsAreOnEntities(metadata, errorCollector) {
for (const application of metadata.interfaceObjectDirective().applications()) {
if (!isEntityType(application.parent)) {
errorCollector.push(error_1.ERRORS.INTERFACE_OBJECT_USAGE_ERROR.err(`The @interfaceObject directive can only be applied to entity types but type "${application.parent.coordinate}" has no @key in this subgraph.`, { nodes: application.parent.sourceAST }));
}
}
}
function validateShareableNotRepeatedOnSameDeclaration(element, metadata, errorCollector) {
const shareableApplications = element.appliedDirectivesOf(metadata.shareableDirective());
if (shareableApplications.length <= 1) {
return;
}
const byExtensions = shareableApplications.reduce((acc, v) => {
const ext = v.ofExtension();
if (ext) {
acc.with.add(ext, v);
}
else {
acc.without.push(v);
}
return acc;
}, { without: [], with: new utils_1.MultiMap() });
const groups = [byExtensions.without].concat((0, utils_1.mapValues)(byExtensions.with));
for (const group of groups) {
if (group.length > 1) {
const eltStr = element.kind === 'ObjectType'
? `the same type declaration of "${element.coordinate}"`
: `field "${element.coordinate}"`;
errorCollector.push(error_1.ERRORS.INVALID_SHAREABLE_USAGE.err(`Invalid duplicate application of @shareable on ${eltStr}: `
+ '@shareable is only repeatable on types so it can be used simultaneously on a type definition and its extensions, but it should not be duplicated on the same definition/extension declaration', { nodes: (0, definitions_1.sourceASTs)(...group) }));
}
}
}
function validateCostNotAppliedToInterface(application, errorCollector) {
const parent = application.parent;
if (parent instanceof definitions_1.FieldDefinition && parent.parent instanceof definitions_1.InterfaceType) {
errorCollector.push(error_1.ERRORS.COST_APPLIED_TO_INTERFACE_FIELD.err(`@cost cannot be applied to interface "${parent.coordinate}"`, { nodes: (0, definitions_1.sourceASTs)(application, parent) }));
}
}
function validateListSizeAppliedToList(application, parent, errorCollector) {
const { sizedFields = [] } = application.arguments();
if (!sizedFields.length && parent.type && !(0, definitions_1.isListType)(parent.type)) {
errorCollector.push(error_1.ERRORS.LIST_SIZE_APPLIED_TO_NON_LIST.err(`"${parent.coordinate}" is not a list`, { nodes: (0, definitions_1.sourceASTs)(application, parent) }));
}
}
function validateAssumedSizeNotNegative(application, parent, errorCollector) {
const { assumedSize } = application.arguments();
if (assumedSize !== undefined && assumedSize !== null && assumedSize < 0) {
errorCollector.push(error_1.ERRORS.LIST_SIZE_INVALID_ASSUMED_SIZE.err(`Assumed size of "${parent.coordinate}" cannot be negative`, { nodes: (0, definitions_1.sourceASTs)(application, parent) }));
}
}
function isNonNullIntType(ty) {
return (0, definitions_1.isNonNullType)(ty) && (0, definitions_1.isIntType)(ty.ofType);
}
function validateSlicingArgumentsAreValidIntegers(application, parent, errorCollector) {
const { slicingArguments = [] } = application.arguments();
for (const slicingArgumentName of slicingArguments) {
const slicingArgument = parent.argument(slicingArgumentName);
if (!(slicingArgument === null || slicingArgument === void 0 ? void 0 : slicingArgument.type)) {
errorCollector.push(error_1.ERRORS.LIST_SIZE_INVALID_SLICING_ARGUMENT.err(`Slicing argument "${slicingArgumentName}" is not an argument of "${parent.coordinate}"`, { nodes: (0, definitions_1.sourceASTs)(application, parent) }));
}
else if (!(0, definitions_1.isIntType)(slicingArgument.type) && !isNonNullIntType(slicingArgument.type)) {
errorCollector.push(error_1.ERRORS.LIST_SIZE_INVALID_SLICING_ARGUMENT.err(`Slicing argument "${slicingArgument.coordinate}" must be Int or Int!`, { nodes: (0, definitions_1.sourceASTs)(application, parent) }));
}
}
}
function isNonNullListType(ty) {
return (0, definitions_1.isNonNullType)(ty) && (0, definitions_1.isListType)(ty.ofType);
}
function validateSizedFieldsAreValidLists(application, parent, errorCollector) {
const { sizedFields = [] } = application.arguments();
if (sizedFields.length) {
if (!parent.type || !(0, definitions_1.isCompositeType)(parent.type)) {
errorCollector.push(error_1.ERRORS.LIST_SIZE_INVALID_SIZED_FIELD.err(`Sized fields cannot be used because "${parent.type}" is not a composite type`, { nodes: (0, definitions_1.sourceASTs)(application, parent) }));
}
else {
for (const sizedFieldName of sizedFields) {
const sizedField = parent.type.field(sizedFieldName);
if (!sizedField) {
errorCollector.push(error_1.ERRORS.LIST_SIZE_INVALID_SIZED_FIELD.err(`Sized field "${sizedFieldName}" is not a field on type "${parent.type.coordinate}"`, { nodes: (0, definitions_1.sourceASTs)(application, parent) }));
}
else if (!sizedField.type || !((0, definitions_1.isListType)(sizedField.type) || isNonNullListType(sizedField.type))) {
errorCollector.push(error_1.ERRORS.LIST_SIZE_APPLIED_TO_NON_LIST.err(`Sized field "${sizedField.coordinate}" is not a list`, { nodes: (0, definitions_1.sourceASTs)(application, parent) }));
}
}
}
}
}
class FederationMetadata {
constructor(schema) {
this.schema = schema;
}
onInvalidate() {
this._externalTester = undefined;
this._sharingPredicate = undefined;
this._isFed2Schema = undefined;
this._fieldUsedPredicate = undefined;
}
isFed2Schema() {
if (!this._isFed2Schema) {
const feature = this.federationFeature();
this._isFed2Schema = !!feature && feature.url.version.satisfies(new coreSpec_1.FeatureVersion(2, 0));
}
return this._isFed2Schema;
}
federationFeature() {
var _a;
return (_a = this.schema.coreFeatures) === null || _a === void 0 ? void 0 : _a.getByIdentity(latestFederationSpec.identity);
}
externalTester() {
if (!this._externalTester) {
this._externalTester = new ExternalTester(this.schema, this.isFed2Schema());
}
return this._externalTester;
}
sharingPredicate() {
if (!this._sharingPredicate) {
this._sharingPredicate = (0, precompute_1.computeShareables)(this.schema);
}
return this._sharingPredicate;
}
fieldUsedPredicate() {
if (!this._fieldUsedPredicate) {
const usedFields = collectUsedFields(this);
this._fieldUsedPredicate = (field) => !!usedFields.has(field);
}
return this._fieldUsedPredicate;
}
isFieldUsed(field) {
return this.fieldUsedPredicate()(field);
}
isFieldExternal(field) {
return this.externalTester().isExternal(field);
}
isFieldPartiallyExternal(field) {
return this.externalTester().isPartiallyExternal(field);
}
isFieldFullyExternal(field) {
return this.externalTester().isFullyExternal(field);
}
isFieldFakeExternal(field) {
return this.externalTester().isFakeExternal(field);
}
selectionSelectsAnyExternalField(selectionSet) {
return this.externalTester().selectsAnyExternalField(selectionSet);
}
isFieldShareable(field) {
return this.sharingPredicate()(field);
}
isInterfaceObjectType(type) {
return (0, definitions_1.isObjectType)(type)
&& hasAppliedDirective(type, this.interfaceObjectDirective());
}
federationDirectiveNameInSchema(name) {
if (this.isFed2Schema()) {
const coreFeatures = this.schema.coreFeatures;
(0, utils_1.assert)(coreFeatures, 'Schema should be a core schema');
const federationFeature = coreFeatures.getByIdentity(latestFederationSpec.identity);
(0, utils_1.assert)(federationFeature, 'Schema should have the federation feature');
return federationFeature.directiveNameInSchema(name);
}
else {
return name;
}
}
federationTypeNameInSchema(name) {
if (name.charAt(0) === '_') {
return name;
}
if (this.isFed2Schema()) {
const coreFeatures = this.schema.coreFeatures;
(0, utils_1.assert)(coreFeatures, 'Schema should be a core schema');
const federationFeature = coreFeatures.getByIdentity(latestFederationSpec.identity);
(0, utils_1.assert)(federationFeature, 'Schema should have the federation feature');
return federationFeature.typeNameInSchema(name);
}
else {
return '_' + name;
}
}
getLegacyFederationDirective(name) {
const directive = this.getFederationDirective(name);
(0, utils_1.assert)(directive, `The provided schema does not have federation directive @${name}`);
return directive;
}
getFederationDirective(name) {
return this.schema.directive(this.federationDirectiveNameInSchema(name));
}
getPost20FederationDirective(name) {
var _a;
return (_a = this.getFederationDirective(name)) !== null && _a !== void 0 ? _a : {
name,
applications: () => new Set(),
};
}
keyDirective() {
return this.getLegacyFederationDirective(federationSpec_1.FederationDirectiveName.KEY);
}
overrideDirective() {
return this.getLegacyFederationDirective(federationSpec_1.FederationDirectiveName.OVERRIDE);
}
extendsDirective() {
return this.getLegacyFederationDirective(federationSpec_1.FederationDirectiveName.EXTENDS);
}
externalDirective() {
return this.getLegacyFederationDirective(federationSpec_1.FederationDirectiveName.EXTERNAL);
}
requiresDirective() {
return this.getLegacyFederationDirective(federationSpec_1.FederationDirectiveName.REQUIRES);
}
providesDirective() {
return this.getLegacyFederationDirective(federationSpec_1.FederationDirectiveName.PROVIDES);
}
shareableDirective() {
return this.getLegacyFederationDirective(federationSpec_1.FederationDirectiveName.SHAREABLE);
}
tagDirective() {
return this.getLegacyFederationDirective(federationSpec_1.FederationDirectiveName.TAG);
}
composeDirective() {
return this.getPost20FederationDirective(federationSpec_1.FederationDirectiveName.COMPOSE_DIRECTIVE);
}
inaccessibleDirective() {
return this.getLegacyFederationDirective(federationSpec_1.FederationDirectiveName.INACCESSIBLE);
}
interfaceObjectDirective() {
return this.getPost20FederationDirective(federationSpec_1.FederationDirectiveName.INTERFACE_OBJECT);
}
authenticatedDirective() {
return this.getPost20FederationDirective(federationSpec_1.FederationDirectiveName.AUTHENTICATED);
}
requiresScopesDirective() {
return this.getPost20FederationDirective(federationSpec_1.FederationDirectiveName.REQUIRES_SCOPES);
}
policyDirective() {
return this.getPost20FederationDirective(federationSpec_1.FederationDirectiveName.POLICY);
}
fromContextDirective() {
return this.getPost20FederationDirective(federationSpec_1.FederationDirectiveName.FROM_CONTEXT);
}
contextDirective() {
return this.getPost20FederationDirective(federationSpec_1.FederationDirectiveName.CONTEXT);
}
costDirective() {
return this.getPost20FederationDirective(federationSpec_1.FederationDirectiveName.COST);
}
listSizeDirective() {
return this.getPost20FederationDirective(federationSpec_1.FederationDirectiveName.LIST_SIZE);
}
allFederationDirectives() {
const baseDirectives = [
this.keyDirective(),
this.externalDirective(),
this.requiresDirective(),
this.providesDirective(),
this.tagDirective(),
this.extendsDirective(),
];
if (!this.isFed2Schema()) {
return baseDirectives;
}
baseDirectives.push(this.shareableDirective());
baseDirectives.push(this.inaccessibleDirective());
baseDirectives.push(this.overrideDirective());
const composeDirective = this.composeDirective();
if (isFederationDirectiveDefinedInSchema(composeDirective)) {
baseDirectives.push(composeDirective);
}
const interfaceObjectDirective = this.interfaceObjectDirective();
if (isFederationDirectiveDefinedInSchema(interfaceObjectDirective)) {
baseDirectives.push(interfaceObjectDirective);
}
const authenticatedDirective = this.authenticatedDirective();
if (isFederationDirectiveDefinedInSchema(authenticatedDirective)) {
baseDirectives.push(authenticatedDirective);
}
const requiresScopesDirective = this.requiresScopesDirective();
if (isFederationDirectiveDefinedInSchema(requiresScopesDirective)) {
baseDirectives.push(requiresScopesDirective);
}
const policyDirective = this.policyDirective();
if (isFederationDirectiveDefinedInSchema(policyDirective)) {
baseDirectives.push(policyDirective);
}
const contextDirective = this.contextDirective();
if (isFederationDirectiveDefinedInSchema(contextDirective)) {
baseDirectives.push(contextDirective);
}
const fromContextDirective = this.fromContextDirective();
if (isFederationDirectiveDefinedInSchema(fromContextDirective)) {
baseDirectives.push(fromContextDirective);
}
const costDirective = this.costDirective();
if (isFederationDirectiveDefinedInSchema(costDirective)) {
baseDirectives.push(costDirective);
}
const listSizeDirective = this.listSizeDirective();
if (isFederationDirectiveDefinedInSchema(listSizeDirective)) {
baseDirectives.push(listSizeDirective);
}
return baseDirectives;
}
entityType() {
return this.schema.type(this.federationTypeNameInSchema(exports.entityTypeSpec.name));
}
anyType() {
return this.schema.type(this.federationTypeNameInSchema(exports.anyTypeSpec.name));
}
serviceType() {
return this.schema.type(this.federationTypeNameInSchema(exports.serviceTypeSpec.name));
}
fieldSetType() {
return this.schema.type(this.federationTypeNameInSchema(federationSpec_1.FederationTypeName.FIELD_SET));
}
allFederationTypes() {
const fedTypes = [
this.anyType(),
this.serviceType(),
];
const fedFeature = this.federationFeature();
if (fedFeature) {
const featureDef = federationSpec_1.FEDERATION_VERSIONS.find(fedFeature.url.version);
(0, utils_1.assert)(featureDef, () => `Federation spec should be known, but got ${fedFeature.url}`);
for (const typeSpec of featureDef.typeSpecs()) {
const type = this.schema.type(fedFeature.typeNameInSchema(typeSpec.name));
if (type) {
fedTypes.push(type);
}
}
}
else {
fedTypes.push(this.fieldSetType());
}
const entityType = this.entityType();
if (entityType) {
fedTypes.push(entityType);
}
return fedTypes;
}
}
exports.FederationMetadata = FederationMetadata;
function isFederationDirectiveDefinedInS