@apollo/composition
Version:
Apollo Federation composition utilities
679 lines • 35.8 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.ValidationState = exports.ValidationContext = exports.extractValidationError = exports.validateGraphComposition = exports.ValidationError = void 0;
const federation_internals_1 = require("@apollo/federation-internals");
const query_graphs_1 = require("@apollo/query-graphs");
const hints_1 = require("./hints");
const graphql_1 = require("graphql");
const debug = (0, federation_internals_1.newDebugLogger)('validation');
class ValidationError extends Error {
constructor(message, supergraphUnsatisfiablePath, subgraphsPaths, witness) {
super(message);
this.supergraphUnsatisfiablePath = supergraphUnsatisfiablePath;
this.subgraphsPaths = subgraphsPaths;
this.witness = witness;
this.name = 'ValidationError';
}
}
exports.ValidationError = ValidationError;
function satisfiabilityError(unsatisfiablePath, subgraphsPaths, subgraphsPathsUnadvanceables) {
const witness = buildWitnessOperation(unsatisfiablePath);
const operation = (0, graphql_1.print)((0, federation_internals_1.operationToDocument)(witness));
const message = `The following supergraph API query:\n${operation}\n`
+ 'cannot be satisfied by the subgraphs because:\n'
+ displayReasons(subgraphsPathsUnadvanceables);
const error = new ValidationError(message, unsatisfiablePath, subgraphsPaths, witness);
return federation_internals_1.ERRORS.SATISFIABILITY_ERROR.err(error.message, {
originalError: error,
});
}
function subgraphNodes(state, extractNode) {
return state.currentSubgraphs().map(({ name, schema }) => {
const node = extractNode(schema);
return node ? (0, federation_internals_1.addSubgraphToASTNode)(node, name) : undefined;
}).filter(federation_internals_1.isDefined);
}
function shareableFieldNonIntersectingRuntimeTypesError(invalidState, field, runtimeTypesToSubgraphs) {
const witness = buildWitnessOperation(invalidState.supergraphPath);
const operation = (0, graphql_1.print)((0, federation_internals_1.operationToDocument)(witness));
const typeStrings = [...runtimeTypesToSubgraphs].map(([ts, subgraphs]) => ` - in ${(0, federation_internals_1.printSubgraphNames)(subgraphs)}, ${ts}`);
const message = `For the following supergraph API query:\n${operation}`
+ `\nShared field "${field.coordinate}" return type "${field.type}" has a non-intersecting set of possible runtime types across subgraphs. Runtime types in subgraphs are:`
+ `\n${typeStrings.join(';\n')}.`
+ `\nThis is not allowed as shared fields must resolve the same way in all subgraphs, and that imply at least some common runtime types between the subgraphs.`;
const error = new ValidationError(message, invalidState.supergraphPath, invalidState.allSubgraphPathInfos().map((p) => p.path.path), witness);
return federation_internals_1.ERRORS.SHAREABLE_HAS_MISMATCHED_RUNTIME_TYPES.err(error.message, {
nodes: subgraphNodes(invalidState, (s) => { var _a, _b; return (_b = (_a = s.type(field.parent.name)) === null || _a === void 0 ? void 0 : _a.field(field.name)) === null || _b === void 0 ? void 0 : _b.sourceAST; }),
});
}
function shareableFieldMismatchedRuntimeTypesHint(state, field, commonRuntimeTypes, runtimeTypesPerSubgraphs) {
const witness = buildWitnessOperation(state.supergraphPath);
const operation = (0, graphql_1.print)((0, federation_internals_1.operationToDocument)(witness));
const allSubgraphs = state.currentSubgraphNames();
const printTypes = (ts) => (0, federation_internals_1.printHumanReadableList)(ts.map((t) => '"' + t + '"'), {
prefix: 'type',
prefixPlural: 'types'
});
const subgraphsWithTypeNotInIntersectionString = allSubgraphs.map((s) => {
const typesToNotImplement = runtimeTypesPerSubgraphs.get(s).filter((t) => !commonRuntimeTypes.includes(t));
if (typesToNotImplement.length === 0) {
return undefined;
}
return ` - subgraph "${s}" should never resolve "${field.coordinate}" to an object of ${printTypes(typesToNotImplement)}`;
}).filter(federation_internals_1.isDefined);
const message = `For the following supergraph API query:\n${operation}`
+ `\nShared field "${field.coordinate}" return type "${field.type}" has different sets of possible runtime types across subgraphs.`
+ `\nSince a shared field must be resolved the same way in all subgraphs, make sure that ${(0, federation_internals_1.printSubgraphNames)(allSubgraphs)} only resolve "${field.coordinate}" to objects of ${printTypes(commonRuntimeTypes)}. In particular:`
+ `\n${subgraphsWithTypeNotInIntersectionString.join(';\n')}.`
+ `\nOtherwise the @shareable contract will be broken.`;
return new hints_1.CompositionHint(hints_1.HINTS.INCONSISTENT_RUNTIME_TYPES_FOR_SHAREABLE_RETURN, message, field, subgraphNodes(state, (s) => { var _a, _b; return (_b = (_a = s.type(field.parent.name)) === null || _a === void 0 ? void 0 : _a.field(field.name)) === null || _b === void 0 ? void 0 : _b.sourceAST; }));
}
function displayReasons(reasons) {
const bySubgraph = new federation_internals_1.MultiMap();
for (const reason of reasons) {
for (const unadvanceable of reason.reasons) {
bySubgraph.add(unadvanceable.sourceSubgraph, unadvanceable);
}
}
return [...bySubgraph.entries()].map(([subgraph, reasons]) => {
let msg = `- from subgraph "${subgraph}":`;
if (reasons.length === 1) {
msg += ' ' + reasons[0].details + '.';
}
else {
const allDetails = new Set(reasons.map((r) => r.details));
for (const details of allDetails) {
msg += '\n - ' + details + '.';
}
}
return msg;
}).join('\n');
}
function buildWitnessOperation(witness) {
(0, federation_internals_1.assert)(witness.size > 0, "unsatisfiablePath should contain at least one edge/transition");
const root = witness.root;
const schema = witness.graph.sources.get(root.source);
return new federation_internals_1.Operation(schema, root.rootKind, buildWitnessNextStep([...witness].map(e => e[0]), 0), new federation_internals_1.VariableDefinitions());
}
function buildWitnessNextStep(edges, index) {
if (index >= edges.length) {
const lastType = edges[edges.length - 1].tail.type;
(0, federation_internals_1.assert)((0, federation_internals_1.isOutputType)(lastType), 'Should not have input types as vertex types');
return (0, federation_internals_1.isLeafType)(lastType) ? undefined : new federation_internals_1.SelectionSet(lastType);
}
const edge = edges[index];
let selection;
const subSelection = buildWitnessNextStep(edges, index + 1);
switch (edge.transition.kind) {
case 'DownCast':
const type = edge.transition.castedType;
selection = (0, federation_internals_1.selectionOfElement)(new federation_internals_1.FragmentElement(edge.transition.sourceType, type.name), subSelection);
break;
case 'FieldCollection':
const field = edge.transition.definition;
selection = new federation_internals_1.FieldSelection(buildWitnessField(field), subSelection);
break;
case 'SubgraphEnteringTransition':
case 'KeyResolution':
case 'RootTypeResolution':
case 'InterfaceObjectFakeDownCast':
(0, federation_internals_1.assert)(false, `Invalid edge ${edge} found in supergraph path`);
}
return (0, federation_internals_1.selectionSetOf)(edge.head.type, selection);
}
function buildWitnessField(definition) {
if (definition.arguments().length === 0) {
return new federation_internals_1.Field(definition);
}
const args = Object.create(null);
for (const argDef of definition.arguments()) {
args[argDef.name] = generateWitnessValue(argDef.type);
}
return new federation_internals_1.Field(definition, args);
}
function generateWitnessValue(type) {
switch (type.kind) {
case 'ScalarType':
switch (type.name) {
case 'Int':
return 0;
case 'Float':
return 3.14;
case 'Boolean':
return true;
case 'String':
return 'A string value';
case 'ID':
return '<any id>';
default:
return '<some value>';
}
case 'EnumType':
return type.values[0].name;
case 'InputObjectType':
const obj = Object.create(null);
for (const field of type.fields()) {
if (field.defaultValue || (0, federation_internals_1.isNullableType)(field.type)) {
continue;
}
obj[field.name] = generateWitnessValue(field.type);
}
return obj;
case 'ListType':
return [];
case 'NonNullType':
return generateWitnessValue(type.ofType);
default:
(0, federation_internals_1.assert)(false, `Unhandled input type ${type}`);
}
}
function validateGraphComposition(supergraphSchema, subgraphNameToGraphEnumValue, supergraphAPI, federatedQueryGraph, compositionOptions = {}) {
const { errors, hints } = new ValidationTraversal(supergraphSchema, subgraphNameToGraphEnumValue, supergraphAPI, federatedQueryGraph, compositionOptions).validate();
return errors.length > 0 ? { errors, hints } : { hints };
}
exports.validateGraphComposition = validateGraphComposition;
function initialSubgraphPaths(kind, subgraphs) {
const root = subgraphs.root(kind);
(0, federation_internals_1.assert)(root, () => `The supergraph shouldn't have a ${kind} root if no subgraphs have one`);
(0, federation_internals_1.assert)(root.type.name == (0, query_graphs_1.federatedGraphRootTypeName)(kind), () => `Unexpected type ${root.type} for subgraphs root type (expected ${(0, query_graphs_1.federatedGraphRootTypeName)(kind)}`);
const initialState = query_graphs_1.GraphPath.fromGraphRoot(subgraphs, kind);
return subgraphs.outEdges(root).map(e => initialState.add(query_graphs_1.subgraphEnteringTransition, e, query_graphs_1.noConditionsResolution));
}
function possibleRuntimeTypeNamesSorted(path) {
const types = path.tailPossibleRuntimeTypes().map((o) => o.name);
types.sort((a, b) => a.localeCompare(b));
return types;
}
function extractValidationError(error) {
if (!(error instanceof graphql_1.GraphQLError) || !(error.originalError instanceof ValidationError)) {
return undefined;
}
return error.originalError;
}
exports.extractValidationError = extractValidationError;
class ValidationContext {
constructor(supergraphSchema, subgraphNameToGraphEnumValue) {
var _a, _b;
this.supergraphSchema = supergraphSchema;
this.subgraphNameToGraphEnumValue = subgraphNameToGraphEnumValue;
const [_, joinSpec] = (0, federation_internals_1.validateSupergraph)(supergraphSchema);
this.joinTypeDirective = joinSpec.typeDirective(supergraphSchema);
this.joinFieldDirective = joinSpec.fieldDirective(supergraphSchema);
this.typesToContexts = new Map();
let contextDirective;
const contextFeature = (_a = supergraphSchema.coreFeatures) === null || _a === void 0 ? void 0 : _a.getByIdentity(federation_internals_1.ContextSpecDefinition.identity);
if (contextFeature) {
const contextSpec = federation_internals_1.CONTEXT_VERSIONS.find(contextFeature.url.version);
(0, federation_internals_1.assert)(contextSpec, `Unexpected context spec version ${contextFeature.url.version}`);
contextDirective = contextSpec.contextDirective(supergraphSchema);
}
for (const application of (_b = contextDirective === null || contextDirective === void 0 ? void 0 : contextDirective.applications()) !== null && _b !== void 0 ? _b : []) {
const { name: context } = application.arguments();
(0, federation_internals_1.assert)(application.parent instanceof federation_internals_1.NamedSchemaElement, "Unexpectedly found unnamed element with @context");
const type = supergraphSchema.type(application.parent.name);
(0, federation_internals_1.assert)(type, `Type ${application.parent.name} unexpectedly doesn't exist`);
const typeNames = [type.name];
if ((0, federation_internals_1.isInterfaceType)(type)) {
typeNames.push(...type.allImplementations().map((t) => t.name));
}
else if ((0, federation_internals_1.isUnionType)(type)) {
typeNames.push(...type.types().map((t) => t.name));
}
for (const typeName of typeNames) {
if (this.typesToContexts.has(typeName)) {
this.typesToContexts.get(typeName).add(context);
}
else {
this.typesToContexts.set(typeName, new Set([context]));
}
}
}
}
isShareable(field) {
const typeInSupergraph = this.supergraphSchema.type(field.parent.name);
(0, federation_internals_1.assert)(typeInSupergraph && (0, federation_internals_1.isCompositeType)(typeInSupergraph), () => `${field.parent.name} should exists in the supergraph and be a composite`);
if (!(0, federation_internals_1.isObjectType)(typeInSupergraph)) {
return false;
}
const fieldInSupergraph = typeInSupergraph.field(field.name);
(0, federation_internals_1.assert)(fieldInSupergraph, () => `${field.coordinate} should exists in the supergraph`);
const joinFieldApplications = fieldInSupergraph.appliedDirectivesOf(this.joinFieldDirective);
return joinFieldApplications.length === 0
? typeInSupergraph.appliedDirectivesOf(this.joinTypeDirective).length > 1
: (joinFieldApplications.filter((application) => {
const args = application.arguments();
return !args.external && !args.usedOverridden;
}).length > 1);
}
matchingContexts(typeName) {
var _a;
return [...((_a = this.typesToContexts.get(typeName)) !== null && _a !== void 0 ? _a : [])];
}
}
exports.ValidationContext = ValidationContext;
class SubgraphPathInfos {
constructor(paths) {
this.paths = paths;
}
}
class TopLevelMutationFieldSubgraphPathInfos {
constructor(mutationField, paths) {
this.mutationField = mutationField;
this.paths = paths;
}
}
class ValidationState {
constructor(supergraphPath, subgraphPathInfos, selectedOverrideConditions = new Map()) {
this.supergraphPath = supergraphPath;
this.subgraphPathInfos = subgraphPathInfos;
this.selectedOverrideConditions = selectedOverrideConditions;
}
static initial({ supergraphAPI, kind, federatedQueryGraph, conditionResolver, overrideConditions, }) {
return new ValidationState(query_graphs_1.GraphPath.fromGraphRoot(supergraphAPI, kind), new SubgraphPathInfos(initialSubgraphPaths(kind, federatedQueryGraph).map((p) => query_graphs_1.TransitionPathWithLazyIndirectPaths.initial(p, conditionResolver, overrideConditions)).map((p) => ({
path: p,
contexts: new Map(),
}))));
}
canSkipVisit(subgraphNameToGraphEnumValue, previousVisits) {
const vertex = this.supergraphPath.tail;
if (this.subgraphPathInfos instanceof SubgraphPathInfos) {
return this.canSkipVisitForSubgraphPaths(vertex, this.subgraphPathInfos.paths, subgraphNameToGraphEnumValue, previousVisits);
}
else {
let canSkip = true;
for (const subgraphPathInfos of this.subgraphPathInfos.paths.values()) {
if (!this.canSkipVisitForSubgraphPaths(vertex, subgraphPathInfos, subgraphNameToGraphEnumValue, previousVisits)) {
canSkip = false;
}
}
return canSkip;
}
}
canSkipVisitForSubgraphPaths(supergraphPathTail, subgraphPathInfos, subgraphNameToGraphEnumValue, previousVisits) {
const currentVertexVisit = {
subgraphContextKeys: this.currentSubgraphContextKeys(subgraphNameToGraphEnumValue, subgraphPathInfos),
overrideConditions: this.selectedOverrideConditions
};
const previousVisitsForVertex = previousVisits.getVertexState(supergraphPathTail);
if (previousVisitsForVertex) {
for (const previousVisit of previousVisitsForVertex) {
if (isSupersetOrEqual(currentVertexVisit, previousVisit)) {
debug.groupEnd(`Has already validated this vertex.`);
return true;
}
}
previousVisitsForVertex.push(currentVertexVisit);
}
else {
previousVisits.setVertexState(supergraphPathTail, [currentVertexVisit]);
}
return false;
}
validateTransition(context, supergraphEdge, matchingContexts, validationErrors, satisfiabilityErrorsByMutationFieldAndSubgraph) {
(0, federation_internals_1.assert)(!supergraphEdge.conditions, () => `Supergraph edges should not have conditions (${supergraphEdge})`);
const transition = supergraphEdge.transition;
const targetType = supergraphEdge.tail.type;
const newOverrideConditions = new Map([...this.selectedOverrideConditions]);
if (supergraphEdge.overrideCondition) {
newOverrideConditions.set(supergraphEdge.overrideCondition.label, supergraphEdge.overrideCondition.condition);
}
const newPath = this.supergraphPath.add(transition, supergraphEdge, query_graphs_1.noConditionsResolution);
let updatedState;
if (this.subgraphPathInfos instanceof SubgraphPathInfos) {
const { newSubgraphPathInfos, error, } = this.validateTransitionForSubgraphPaths(this.subgraphPathInfos.paths, newOverrideConditions, transition, targetType, matchingContexts, newPath);
if (error) {
validationErrors.push(error);
return {};
}
if (newSubgraphPathInfos.length === 0) {
return {};
}
const mutationField = ValidationState.fieldIfTopLevelMutation(this.supergraphPath, supergraphEdge);
if (mutationField) {
const partitionedSubgraphPathInfos = new Map();
for (const subgraphPathInfo of newSubgraphPathInfos) {
let subgraph = ValidationState.subgraphOfTopLevelMutation(subgraphPathInfo);
let subgraphPathInfos = partitionedSubgraphPathInfos.get(subgraph);
if (!subgraphPathInfos) {
subgraphPathInfos = [];
partitionedSubgraphPathInfos.set(subgraph, subgraphPathInfos);
}
subgraphPathInfos.push(subgraphPathInfo);
}
if (partitionedSubgraphPathInfos.size <= 1) {
updatedState = new ValidationState(newPath, new SubgraphPathInfos([...partitionedSubgraphPathInfos.values()].flat()), newOverrideConditions);
}
else {
let errorsBySubgraph = satisfiabilityErrorsByMutationFieldAndSubgraph
.get(mutationField.coordinate);
if (!errorsBySubgraph) {
errorsBySubgraph = new Map();
satisfiabilityErrorsByMutationFieldAndSubgraph.set(mutationField.coordinate, errorsBySubgraph);
}
for (const subgraph of partitionedSubgraphPathInfos.keys()) {
if (!errorsBySubgraph.has(subgraph)) {
errorsBySubgraph.set(subgraph, []);
}
}
updatedState = new ValidationState(newPath, {
mutationField,
paths: partitionedSubgraphPathInfos,
}, newOverrideConditions);
}
}
else {
updatedState = new ValidationState(newPath, new SubgraphPathInfos(newSubgraphPathInfos), newOverrideConditions);
}
}
else {
const partitionedSubgraphPathInfos = new Map();
for (const [subgraph, subgraphPathInfos] of this.subgraphPathInfos.paths) {
const errors = satisfiabilityErrorsByMutationFieldAndSubgraph
.get(this.subgraphPathInfos.mutationField.coordinate)
.get(subgraph);
const { newSubgraphPathInfos, error, } = this.validateTransitionForSubgraphPaths(subgraphPathInfos, newOverrideConditions, transition, targetType, matchingContexts, newPath);
if (error) {
errors.push(error);
continue;
}
if (newSubgraphPathInfos.length === 0) {
return {};
}
partitionedSubgraphPathInfos.set(subgraph, newSubgraphPathInfos);
}
if (partitionedSubgraphPathInfos.size === 0) {
return {};
}
updatedState = new ValidationState(newPath, {
mutationField: this.subgraphPathInfos.mutationField,
paths: partitionedSubgraphPathInfos,
}, newOverrideConditions);
}
let allSubgraphPathInfos = updatedState.allSubgraphPathInfos();
let hint = undefined;
if (allSubgraphPathInfos.length > 1
&& transition.kind === 'FieldCollection'
&& (0, federation_internals_1.isAbstractType)(newPath.tail.type)
&& context.isShareable(transition.definition)) {
const filteredPaths = allSubgraphPathInfos.map((p) => p.path.path).filter((p) => (0, federation_internals_1.isAbstractType)(p.tail.type));
if (filteredPaths.length > 1) {
const allRuntimeTypes = possibleRuntimeTypeNamesSorted(newPath);
let intersection = allRuntimeTypes;
const runtimeTypesToSubgraphs = new federation_internals_1.MultiMap();
const runtimeTypesPerSubgraphs = new federation_internals_1.MultiMap();
let hasAllEmpty = true;
for (const { path } of allSubgraphPathInfos) {
const subgraph = path.path.tail.source;
const typeNames = possibleRuntimeTypeNamesSorted(path.path);
if (typeNames.length === 1 && !allRuntimeTypes.includes(typeNames[0])) {
continue;
}
runtimeTypesPerSubgraphs.set(subgraph, typeNames);
let typeNamesStr = 'no runtime type is defined';
if (typeNames.length > 0) {
typeNamesStr = (typeNames.length > 1 ? 'types ' : 'type ') + (0, federation_internals_1.joinStrings)(typeNames.map((n) => `"${n}"`));
hasAllEmpty = false;
}
runtimeTypesToSubgraphs.add(typeNamesStr, subgraph);
intersection = intersection.filter((t) => typeNames.includes(t));
}
if (!hasAllEmpty) {
if (intersection.length === 0) {
validationErrors.push(shareableFieldNonIntersectingRuntimeTypesError(updatedState, transition.definition, runtimeTypesToSubgraphs));
return {};
}
if (runtimeTypesToSubgraphs.size > 1) {
hint = shareableFieldMismatchedRuntimeTypesHint(updatedState, transition.definition, intersection, runtimeTypesPerSubgraphs);
}
}
}
}
return { state: updatedState, hint };
}
validateTransitionForSubgraphPaths(subgraphPathInfos, newOverrideConditions, transition, targetType, matchingContexts, newPath) {
const newSubgraphPathInfos = [];
const deadEnds = [];
for (const { path, contexts } of subgraphPathInfos) {
const options = (0, query_graphs_1.advancePathWithTransition)(path, transition, targetType, newOverrideConditions);
if ((0, query_graphs_1.isUnadvanceableClosures)(options)) {
deadEnds.push(options);
continue;
}
if (options.length === 0) {
return { newSubgraphPathInfos: [] };
}
let newContexts = contexts;
if (matchingContexts.length) {
const subgraphName = path.path.tail.source;
const typeName = path.path.tail.type.name;
newContexts = new Map([...contexts]);
for (const matchingContext in matchingContexts) {
newContexts.set(matchingContext, {
subgraphName,
typeName,
});
}
}
newSubgraphPathInfos.push(...options.map((p) => ({ path: p, contexts: newContexts })));
}
return newSubgraphPathInfos.length === 0
? {
error: satisfiabilityError(newPath, subgraphPathInfos.map((p) => p.path.path), deadEnds.map((d) => d.toUnadvanceables()))
}
: { newSubgraphPathInfos };
}
static fieldIfTopLevelMutation(supergraphPath, edge) {
if (supergraphPath.size !== 0) {
return null;
}
if (edge.transition.kind !== 'FieldCollection') {
return null;
}
if (supergraphPath.root !== supergraphPath.graph.root('mutation')) {
return null;
}
return edge.transition.definition;
}
static subgraphOfTopLevelMutation(subgraphPathInfo) {
const lastEdge = subgraphPathInfo.path.path.lastEdge();
(0, federation_internals_1.assert)(lastEdge, "Path unexpectedly missing edge");
return lastEdge.head.source;
}
allSubgraphPathInfos() {
return this.subgraphPathInfos instanceof SubgraphPathInfos
? this.subgraphPathInfos.paths
: Array.from(this.subgraphPathInfos.paths.values()).flat();
}
allSubgraphPathsCount() {
if (this.subgraphPathInfos instanceof SubgraphPathInfos) {
return this.subgraphPathInfos.paths.length;
}
else {
let count = 0;
for (const subgraphPathInfos of this.subgraphPathInfos.paths.values()) {
count += subgraphPathInfos.length;
}
return count;
}
}
currentSubgraphNames() {
const subgraphs = [];
for (const pathInfo of this.allSubgraphPathInfos()) {
const source = pathInfo.path.path.tail.source;
if (!subgraphs.includes(source)) {
subgraphs.push(source);
}
}
return subgraphs;
}
currentSubgraphContextKeys(subgraphNameToGraphEnumValue, subgraphPathInfos) {
const subgraphContextKeys = new Set();
for (const pathInfo of subgraphPathInfos) {
const tailSubgraphName = pathInfo.path.path.tail.source;
const tailSubgraphEnumValue = subgraphNameToGraphEnumValue.get(tailSubgraphName);
const tailTypeName = pathInfo.path.path.tail.type.name;
const entryKeys = [];
const contexts = Array.from(pathInfo.contexts.entries());
contexts.sort((a, b) => a[0].localeCompare(b[0]));
for (const [context, { subgraphName, typeName }] of contexts) {
const subgraphEnumValue = subgraphNameToGraphEnumValue.get(subgraphName);
entryKeys.push(`${context}=${subgraphEnumValue}.${typeName}`);
}
subgraphContextKeys.add(`${tailSubgraphEnumValue}.${tailTypeName}[${entryKeys.join(',')}]`);
}
return subgraphContextKeys;
}
currentSubgraphs() {
const allSubgraphPathInfos = this.allSubgraphPathInfos();
if (allSubgraphPathInfos.length === 0) {
return [];
}
const sources = allSubgraphPathInfos[0].path.path.graph.sources;
return this.currentSubgraphNames().map((name) => ({ name, schema: sources.get(name) }));
}
toString() {
if (this.subgraphPathInfos instanceof SubgraphPathInfos) {
return `${this.supergraphPath} <=> [${this.subgraphPathInfos.paths.map(p => p.path.toString()).join(', ')}]`;
}
else {
return `${this.supergraphPath} <=> {${Array.from(this.subgraphPathInfos.paths.entries()).map(([s, p]) => `${s}: [${p.map(p => p.path.toString()).join(', ')}]`).join(', ')}}`;
}
}
}
exports.ValidationState = ValidationState;
function isSupersetOrEqual(maybeSuperset, other) {
const includesAllSubgraphs = [...other.subgraphContextKeys]
.every((s) => maybeSuperset.subgraphContextKeys.has(s));
const includesAllOverrideConditions = [...other.overrideConditions.entries()].every(([label, value]) => maybeSuperset.overrideConditions.get(label) === value);
return includesAllSubgraphs && includesAllOverrideConditions;
}
class ValidationTraversal {
constructor(supergraphSchema, subgraphNameToGraphEnumValue, supergraphAPI, federatedQueryGraph, compositionOptions) {
var _a;
this.stack = [];
this.validationErrors = [];
this.validationHints = [];
this.satisfiabilityErrorsByMutationFieldAndSubgraph = new Map();
this.totalValidationSubgraphPaths = 0;
this.maxValidationSubgraphPaths = (_a = compositionOptions.maxValidationSubgraphPaths) !== null && _a !== void 0 ? _a : ValidationTraversal.DEFAULT_MAX_VALIDATION_SUBGRAPH_PATHS;
this.conditionResolver = (0, query_graphs_1.simpleValidationConditionResolver)({
supergraph: supergraphSchema,
queryGraph: federatedQueryGraph,
withCaching: true,
});
supergraphAPI.rootKinds().forEach((kind) => this.pushStack(ValidationState.initial({
supergraphAPI,
kind,
federatedQueryGraph,
conditionResolver: this.conditionResolver,
overrideConditions: new Map(),
})));
this.previousVisits = new query_graphs_1.QueryGraphState();
this.context = new ValidationContext(supergraphSchema, subgraphNameToGraphEnumValue);
}
pushStack(state) {
this.totalValidationSubgraphPaths += state.allSubgraphPathsCount();
this.stack.push(state);
if (this.totalValidationSubgraphPaths > this.maxValidationSubgraphPaths) {
return {
error: federation_internals_1.ERRORS.MAX_VALIDATION_SUBGRAPH_PATHS_EXCEEDED.err(`Maximum number of validation subgraph paths exceeded: ${this.totalValidationSubgraphPaths}`)
};
}
return {};
}
popStack() {
const state = this.stack.pop();
if (state) {
this.totalValidationSubgraphPaths -= state.allSubgraphPathsCount();
}
return state;
}
validate() {
while (this.stack.length > 0) {
const { error } = this.handleState(this.popStack());
if (error) {
return { errors: [error], hints: this.validationHints };
}
}
for (const [fieldCoordinate, errorsBySubgraph,] of this.satisfiabilityErrorsByMutationFieldAndSubgraph) {
let someSubgraphHasNoErrors = false;
for (const errors of errorsBySubgraph.values()) {
if (errors.length === 0) {
someSubgraphHasNoErrors = true;
break;
}
}
if (someSubgraphHasNoErrors) {
continue;
}
let messageParts = [
`Supergraph API queries using the mutation field "${fieldCoordinate}"`
+ ` at top-level must be satisfiable without needing to call that`
+ ` field from multiple subgraphs, but every subgraph with that field`
+ ` encounters satisfiability errors. Please fix these satisfiability`
+ ` errors for (at least) one of the following subgraphs with the`
+ ` mutation field:`
];
for (const [subgraph, errors] of errorsBySubgraph) {
messageParts.push(`- When calling "${fieldCoordinate}" at top-level from subgraph`
+ ` "${subgraph}":`);
for (const error of errors) {
for (const line of error.message.split("\n")) {
if (line.length === 0) {
messageParts.push(line);
}
else {
messageParts.push(" " + line);
}
}
}
}
this.validationErrors.push(federation_internals_1.ERRORS.SATISFIABILITY_ERROR.err(messageParts.join("\n")));
}
return { errors: this.validationErrors, hints: this.validationHints };
}
handleState(state) {
var _a, _b, _c;
debug.group(() => `Validation: ${this.stack.length + 1} open states. Validating ${state}`);
if (state.canSkipVisit(this.context.subgraphNameToGraphEnumValue, this.previousVisits)) {
return {};
}
for (const edge of state.supergraphPath.nextEdges()) {
if (edge.isEdgeForField(federation_internals_1.typenameFieldName)) {
continue;
}
if (edge.overrideCondition
&& state.selectedOverrideConditions.has(edge.overrideCondition.label)
&& !edge.satisfiesOverrideConditions(state.selectedOverrideConditions)) {
debug.groupEnd(`Edge ${edge} doesn't satisfy label condition: ${(_a = edge.overrideCondition) === null || _a === void 0 ? void 0 : _a.label}(${state.selectedOverrideConditions.get((_c = (_b = edge.overrideCondition) === null || _b === void 0 ? void 0 : _b.label) !== null && _c !== void 0 ? _c : "")}), no need to validate further`);
continue;
}
const matchingContexts = edge.transition.kind === 'FieldCollection'
? this.context.matchingContexts(edge.head.type.name)
: [];
debug.group(() => `Validating supergraph edge ${edge}`);
const { state: newState, hint } = state.validateTransition(this.context, edge, matchingContexts, this.validationErrors, this.satisfiabilityErrorsByMutationFieldAndSubgraph);
if (!newState) {
debug.groupEnd(`Validation error!`);
continue;
}
if (hint) {
this.validationHints.push(hint);
}
if (newState && !newState.supergraphPath.isTerminal()) {
const { error } = this.pushStack(newState);
if (error) {
return { error };
}
debug.groupEnd(() => `Reached new state ${newState}`);
}
else {
debug.groupEnd(`Reached terminal vertex/cycle`);
}
}
debug.groupEnd();
return {};
}
}
ValidationTraversal.DEFAULT_MAX_VALIDATION_SUBGRAPH_PATHS = 1000000;
//# sourceMappingURL=validate.js.map