UNPKG

@apollo/composition

Version:
495 lines 27.1 kB
"use strict"; 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.subgraphPathInfos.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 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), initialSubgraphPaths(kind, federatedQueryGraph).map((p) => query_graphs_1.TransitionPathWithLazyIndirectPaths.initial(p, conditionResolver, overrideConditions)).map((p) => ({ path: p, contexts: new Map(), }))); } validateTransition(context, supergraphEdge, matchingContexts) { (0, federation_internals_1.assert)(!supergraphEdge.conditions, () => `Supergraph edges should not have conditions (${supergraphEdge})`); const transition = supergraphEdge.transition; const targetType = supergraphEdge.tail.type; const newSubgraphPathInfos = []; const deadEnds = []; const newOverrideConditions = new Map([...this.selectedOverrideConditions]); if (supergraphEdge.overrideCondition) { newOverrideConditions.set(supergraphEdge.overrideCondition.label, supergraphEdge.overrideCondition.condition); } for (const { path, contexts } of this.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 { state: undefined }; } 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 }))); } const newPath = this.supergraphPath.add(transition, supergraphEdge, query_graphs_1.noConditionsResolution); if (newSubgraphPathInfos.length === 0) { return { error: satisfiabilityError(newPath, this.subgraphPathInfos.map((p) => p.path.path), deadEnds.map((d) => d.toUnadvanceables())) }; } const updatedState = new ValidationState(newPath, newSubgraphPathInfos, newOverrideConditions); let hint = undefined; if (newSubgraphPathInfos.length > 1 && transition.kind === 'FieldCollection' && (0, federation_internals_1.isAbstractType)(newPath.tail.type) && context.isShareable(transition.definition)) { const filteredPaths = newSubgraphPathInfos.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 newSubgraphPathInfos) { 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) { return { error: shareableFieldNonIntersectingRuntimeTypesError(updatedState, transition.definition, runtimeTypesToSubgraphs) }; } if (runtimeTypesToSubgraphs.size > 1) { hint = shareableFieldMismatchedRuntimeTypesHint(updatedState, transition.definition, intersection, runtimeTypesPerSubgraphs); } } } } return { state: updatedState, hint }; } currentSubgraphNames() { const subgraphs = []; for (const pathInfo of this.subgraphPathInfos) { const source = pathInfo.path.path.tail.source; if (!subgraphs.includes(source)) { subgraphs.push(source); } } return subgraphs; } currentSubgraphContextKeys(subgraphNameToGraphEnumValue) { const subgraphContextKeys = new Set(); for (const pathInfo of this.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() { if (this.subgraphPathInfos.length === 0) { return []; } const sources = this.subgraphPathInfos[0].path.path.graph.sources; return this.currentSubgraphNames().map((name) => ({ name, schema: sources.get(name) })); } toString() { return `${this.supergraphPath} <=> [${this.subgraphPathInfos.map(s => s.path.toString()).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.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.subgraphPathInfos.length; 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.subgraphPathInfos.length; } return state; } validate() { while (this.stack.length > 0) { const { error } = this.handleState(this.popStack()); if (error) { return { errors: [error], hints: this.validationHints }; } } return { errors: this.validationErrors, hints: this.validationHints }; } handleState(state) { var _a, _b, _c; debug.group(() => `Validation: ${this.stack.length + 1} open states. Validating ${state}`); const vertex = state.supergraphPath.tail; const currentVertexVisit = { subgraphContextKeys: state.currentSubgraphContextKeys(this.context.subgraphNameToGraphEnumValue), overrideConditions: state.selectedOverrideConditions }; const previousVisitsForVertex = this.previousVisits.getVertexState(vertex); if (previousVisitsForVertex) { for (const previousVisit of previousVisitsForVertex) { if (isSupersetOrEqual(currentVertexVisit, previousVisit)) { debug.groupEnd(`Has already validated this vertex.`); return {}; } } previousVisitsForVertex.push(currentVertexVisit); } else { this.previousVisits.setVertexState(vertex, [currentVertexVisit]); } 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, error, hint } = state.validateTransition(this.context, edge, matchingContexts); if (error) { debug.groupEnd(`Validation error!`); this.validationErrors.push(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