UNPKG

@apollo/gateway

Version:
546 lines 25.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.defaultFieldResolverWithAliasSupport = exports.generateHydratedPaths = exports.executeQueryPlan = void 0; const node_fetch_1 = require("node-fetch"); const graphql_1 = require("graphql"); const usage_reporting_protobuf_1 = require("@apollo/usage-reporting-protobuf"); const types_1 = require("./datasources/types"); const query_planner_1 = require("@apollo/query-planner"); const deepMerge_1 = require("./utilities/deepMerge"); const array_1 = require("./utilities/array"); const api_1 = require("@opentelemetry/api"); const opentelemetry_1 = require("./utilities/opentelemetry"); const federation_internals_1 = require("@apollo/federation-internals"); const resultShaping_1 = require("./resultShaping"); const dataRewrites_1 = require("./dataRewrites"); function collectUsedVariables(node) { const usedVariables = new Set(); (0, graphql_1.visit)(node, { Variable: ({ name }) => { usedVariables.add(name.value); } }); return usedVariables; } function makeIntrospectionQueryDocument(introspectionSelection, variableDefinitions) { var _a; const usedVariables = collectUsedVariables(introspectionSelection); const usedVariableDefinitions = variableDefinitions === null || variableDefinitions === void 0 ? void 0 : variableDefinitions.filter((def) => usedVariables.has(def.variable.name.value)); (0, federation_internals_1.assert)(usedVariables.size === ((_a = usedVariableDefinitions === null || usedVariableDefinitions === void 0 ? void 0 : usedVariableDefinitions.length) !== null && _a !== void 0 ? _a : 0), () => `Should have found all used variables ${[...usedVariables]} in definitions ${JSON.stringify(variableDefinitions)}`); return { kind: graphql_1.Kind.DOCUMENT, definitions: [ { kind: graphql_1.Kind.OPERATION_DEFINITION, operation: graphql_1.OperationTypeNode.QUERY, variableDefinitions: usedVariableDefinitions, selectionSet: { kind: graphql_1.Kind.SELECTION_SET, selections: [introspectionSelection], } } ], }; } function executeIntrospection(schema, introspectionSelection, variableDefinitions, variableValues) { var _a, _b; const { data, errors } = (0, graphql_1.executeSync)({ schema, document: makeIntrospectionQueryDocument(introspectionSelection, variableDefinitions), rootValue: {}, variableValues, }); (0, federation_internals_1.assert)(!errors || errors.length === 0, () => `Introspection query for ${JSON.stringify(introspectionSelection)} should not have failed but got ${JSON.stringify(errors)}`); (0, federation_internals_1.assert)(data, () => `Introspection query for ${JSON.stringify(introspectionSelection)} should not have failed`); return data[(_b = (_a = introspectionSelection.alias) === null || _a === void 0 ? void 0 : _a.value) !== null && _b !== void 0 ? _b : introspectionSelection.name.value]; } async function executeQueryPlan(queryPlan, serviceMap, requestContext, operationContext, supergraphSchema, apiSchema, telemetryConfig) { const logger = requestContext.logger || console; return opentelemetry_1.tracer.startActiveSpan(opentelemetry_1.OpenTelemetrySpanNames.EXECUTE, async (span) => { var _a; try { const errors = []; let operation; try { operation = (0, federation_internals_1.operationFromDocument)(apiSchema, { kind: graphql_1.Kind.DOCUMENT, definitions: [ operationContext.operation, ...Object.values(operationContext.fragments), ], }, { validate: false, }); } catch (err) { if (err instanceof graphql_1.GraphQLError) { return { errors: [err] }; } throw err; } const context = { queryPlan, operationContext, operation, serviceMap, requestContext, supergraphSchema, errors, }; const unfilteredData = Object.create(null); const captureTraces = !!(requestContext.metrics && requestContext.metrics.captureTraces); if (((_a = queryPlan.node) === null || _a === void 0 ? void 0 : _a.kind) === 'Subscription') { throw new Error('Execution of subscriptions not supported by gateway'); } if (queryPlan.node) { const traceNode = await executeNode(context, queryPlan.node, { path: [], data: unfilteredData, fullResult: unfilteredData, }, captureTraces, telemetryConfig); if (captureTraces) { requestContext.metrics.queryPlanTrace = traceNode; } } const result = await opentelemetry_1.tracer.startActiveSpan(opentelemetry_1.OpenTelemetrySpanNames.POST_PROCESSING, async (span) => { let data; try { let postProcessingErrors; const variables = requestContext.request.variables; ({ data, errors: postProcessingErrors } = (0, resultShaping_1.computeResponse)({ operation, variables, input: unfilteredData, introspectionHandling: (f) => executeIntrospection(operationContext.schema, f.expandFragments().toSelectionNode(), operationContext.operation.variableDefinitions, variables), })); if (errors.length === 0 && postProcessingErrors.length > 0) { (0, opentelemetry_1.recordExceptions)(span, postProcessingErrors, telemetryConfig); span.setStatus({ code: api_1.SpanStatusCode.ERROR }); return { extensions: { "valueCompletion": postProcessingErrors }, data }; } } catch (error) { (0, opentelemetry_1.recordExceptions)(span, [error], telemetryConfig); span.setStatus({ code: api_1.SpanStatusCode.ERROR }); if (error instanceof graphql_1.GraphQLError) { return { errors: [error] }; } else if (error instanceof Error) { return { errors: [ new graphql_1.GraphQLError(error.message, { originalError: error }) ] }; } else { logger.error("Unexpected error during query plan execution: " + error); return { errors: [ new graphql_1.GraphQLError("Unexpected error during query plan execution") ] }; } } finally { span.end(); } return errors.length === 0 ? { data } : { errors, data }; }); if (result.errors) { (0, opentelemetry_1.recordExceptions)(span, result.errors, telemetryConfig); span.setStatus({ code: api_1.SpanStatusCode.ERROR }); } return result; } catch (err) { (0, opentelemetry_1.recordExceptions)(span, [err], telemetryConfig); span.setStatus({ code: api_1.SpanStatusCode.ERROR }); throw err; } finally { span.end(); } }); } exports.executeQueryPlan = executeQueryPlan; async function executeNode(context, node, currentCursor, captureTraces, telemetryConfig) { if (!currentCursor) { return new usage_reporting_protobuf_1.Trace.QueryPlanNode(); } switch (node.kind) { case 'Sequence': { const traceNode = new usage_reporting_protobuf_1.Trace.QueryPlanNode.SequenceNode(); for (const childNode of node.nodes) { const childTraceNode = await executeNode(context, childNode, currentCursor, captureTraces, telemetryConfig); traceNode.nodes.push(childTraceNode); } return new usage_reporting_protobuf_1.Trace.QueryPlanNode({ sequence: traceNode }); } case 'Parallel': { const childTraceNodes = await Promise.all(node.nodes.map(async (childNode) => executeNode(context, childNode, currentCursor, captureTraces, telemetryConfig))); return new usage_reporting_protobuf_1.Trace.QueryPlanNode({ parallel: new usage_reporting_protobuf_1.Trace.QueryPlanNode.ParallelNode({ nodes: childTraceNodes, }), }); } case 'Flatten': { return new usage_reporting_protobuf_1.Trace.QueryPlanNode({ flatten: new usage_reporting_protobuf_1.Trace.QueryPlanNode.FlattenNode({ responsePath: node.path.map(id => new usage_reporting_protobuf_1.Trace.QueryPlanNode.ResponsePathElement(typeof id === 'string' ? { fieldName: id } : { index: id })), node: await executeNode(context, node.node, moveIntoCursor(currentCursor, node.path), captureTraces, telemetryConfig), }), }); } case 'Fetch': { const traceNode = new usage_reporting_protobuf_1.Trace.QueryPlanNode.FetchNode({ serviceName: node.serviceName, }); try { await executeFetch(context, node, currentCursor, captureTraces ? traceNode : null, telemetryConfig); } catch (error) { context.errors.push(error); } return new usage_reporting_protobuf_1.Trace.QueryPlanNode({ fetch: traceNode }); } case 'Condition': { const condition = (0, query_planner_1.evaluateCondition)(node, context.operation.variableDefinitions, context.requestContext.request.variables); const pickedBranch = condition ? node.ifClause : node.elseClause; let branchTraceNode = undefined; if (pickedBranch) { branchTraceNode = await executeNode(context, pickedBranch, currentCursor, captureTraces, telemetryConfig); } return new usage_reporting_protobuf_1.Trace.QueryPlanNode({ condition: new usage_reporting_protobuf_1.Trace.QueryPlanNode.ConditionNode({ condition: node.condition, ifClause: condition ? branchTraceNode : undefined, elseClause: condition ? undefined : branchTraceNode, }), }); } case 'Defer': { (0, federation_internals_1.assert)(false, `@defer support is not available in the gateway`); } } } async function executeFetch(context, fetch, currentCursor, traceNode, telemetryConfig) { const logger = context.requestContext.logger || console; const service = context.serviceMap[fetch.serviceName]; return opentelemetry_1.tracer.startActiveSpan(opentelemetry_1.OpenTelemetrySpanNames.FETCH, { attributes: { service: fetch.serviceName } }, async (span) => { try { if (!service) { throw new Error(`Couldn't find service with name "${fetch.serviceName}"`); } let entities; if (Array.isArray(currentCursor.data)) { entities = currentCursor.data.filter(array_1.isNotNullOrUndefined); } else { entities = [currentCursor.data]; } if (entities.length < 1) return; const variables = Object.create(null); if (fetch.variableUsages) { for (const variableName of fetch.variableUsages) { const providedVariables = context.requestContext.request.variables; if (providedVariables && typeof providedVariables[variableName] !== 'undefined') { variables[variableName] = providedVariables[variableName]; } } } if (!fetch.requires) { const dataReceivedFromService = await sendOperation(variables); if (dataReceivedFromService) { (0, dataRewrites_1.applyRewrites)(context.supergraphSchema, fetch.outputRewrites, dataReceivedFromService); } for (const entity of entities) { (0, deepMerge_1.deepMerge)(entity, dataReceivedFromService); } } else { const requires = fetch.requires; const representations = []; const representationToEntity = []; entities.forEach((entity, index) => { const representation = executeSelectionSet(context.supergraphSchema, entity, requires); if (representation && representation[graphql_1.TypeNameMetaFieldDef.name]) { (0, dataRewrites_1.applyRewrites)(context.supergraphSchema, fetch.inputRewrites, representation); representations.push(representation); representationToEntity.push(index); } }); if (representations.length < 1) return; if ('representations' in variables) { throw new Error(`Variables cannot contain key "representations"`); } const dataReceivedFromService = await sendOperation({ ...variables, representations }); if (!dataReceivedFromService) { return; } if (!(dataReceivedFromService._entities && Array.isArray(dataReceivedFromService._entities))) { throw new Error(`Expected "data._entities" in response to be an array`); } const receivedEntities = dataReceivedFromService._entities; if (receivedEntities.length !== representations.length) { throw new Error(`Expected "data._entities" to contain ${representations.length} elements`); } for (let i = 0; i < entities.length; i++) { const receivedEntity = receivedEntities[i]; const existingEntity = entities[representationToEntity[i]]; if (receivedEntity && !receivedEntity["__typename"]) { const typename = existingEntity["__typename"]; receivedEntity["__typename"] = typename; } (0, dataRewrites_1.applyRewrites)(context.supergraphSchema, fetch.outputRewrites, receivedEntity); (0, deepMerge_1.deepMerge)(entities[representationToEntity[i]], receivedEntity); } } } catch (err) { (0, opentelemetry_1.recordExceptions)(span, [err], telemetryConfig); span.setStatus({ code: api_1.SpanStatusCode.ERROR }); throw err; } finally { span.end(); } }); async function sendOperation(variables) { var _a, _b, _c; let http; if (traceNode) { http = { headers: new node_fetch_1.Headers({ 'apollo-federation-include-trace': 'ftv1' }), }; if (context.requestContext.metrics && context.requestContext.metrics.startHrTime) { traceNode.sentTimeOffset = durationHrTimeToNanos(process.hrtime(context.requestContext.metrics.startHrTime)); } traceNode.sentTime = dateToProtoTimestamp(new Date()); } const response = await service.process({ kind: types_1.GraphQLDataSourceRequestKind.INCOMING_OPERATION, request: { query: fetch.operation, variables, operationName: fetch.operationName, http, }, incomingRequestContext: context.requestContext, context: context.requestContext.context, document: fetch.operationDocumentNode, pathInIncomingRequest: currentCursor.path }); if (response.errors) { const errorPathHelper = makeLazyErrorPathGenerator(fetch, currentCursor); const errors = response.errors.map((error) => downstreamServiceError(error, fetch.serviceName, errorPathHelper)); context.errors.push(...errors); if (!((_a = response.extensions) === null || _a === void 0 ? void 0 : _a.ftv1)) { const errorPaths = response.errors.map((error) => ({ subgraph: fetch.serviceName, path: error.path, })); if (context.requestContext.metrics.nonFtv1ErrorPaths) { context.requestContext.metrics.nonFtv1ErrorPaths.push(...errorPaths); } else { context.requestContext.metrics.nonFtv1ErrorPaths = errorPaths; } } } if (traceNode) { traceNode.receivedTime = dateToProtoTimestamp(new Date()); if (response.extensions && response.extensions.ftv1) { const traceBase64 = response.extensions.ftv1; let traceBuffer; let traceParsingFailed = false; try { traceBuffer = Buffer.from(traceBase64, 'base64'); } catch (err) { logger.error(`error decoding base64 for federated trace from ${fetch.serviceName}: ${err}`); traceParsingFailed = true; } if (traceBuffer) { try { const trace = usage_reporting_protobuf_1.Trace.decode(traceBuffer); traceNode.trace = trace; } catch (err) { logger.error(`error decoding protobuf for federated trace from ${fetch.serviceName}: ${err}`); traceParsingFailed = true; } } if (traceNode.trace) { const rootTypeName = (0, federation_internals_1.defaultRootName)(context.operationContext.operation.operation); (_c = (_b = traceNode.trace.root) === null || _b === void 0 ? void 0 : _b.child) === null || _c === void 0 ? void 0 : _c.forEach((child) => { child.parentType = rootTypeName; }); } traceNode.traceParsingFailed = traceParsingFailed; } } return response.data; } } function makeLazyErrorPathGenerator(fetch, cursor) { let hydratedPaths; return (errorPath) => { var _a; if (fetch.requires && typeof (errorPath === null || errorPath === void 0 ? void 0 : errorPath[1]) === 'number') { if (!hydratedPaths) { hydratedPaths = []; generateHydratedPaths([], cursor.path, cursor.fullResult, hydratedPaths); } const hydratedPath = (_a = hydratedPaths[errorPath[1]]) !== null && _a !== void 0 ? _a : []; return [...hydratedPath, ...errorPath.slice(2)]; } else { return errorPath ? [...cursor.path, ...errorPath.slice()] : undefined; } }; } function generateHydratedPaths(parent, path, data, result) { const head = path[0]; if (data == null) { return; } if (head == null) { result.push(parent.slice()); } else if (head === '@') { (0, federation_internals_1.assert)(Array.isArray(data), 'expected array when encountering `@`'); for (const [i, value] of data.entries()) { parent.push(i); generateHydratedPaths(parent, path.slice(1), value, result); parent.pop(); } } else if (typeof head === 'string') { if (Array.isArray(data)) { for (const [i, value] of data.entries()) { parent.push(i); generateHydratedPaths(parent, path, value, result); parent.pop(); } } else { if (head in data) { const value = data[head]; parent.push(head); generateHydratedPaths(parent, path.slice(1), value, result); parent.pop(); } } } else { (0, federation_internals_1.assert)(false, `unknown path part "${head}"`); } } exports.generateHydratedPaths = generateHydratedPaths; function executeSelectionSet(schema, source, selections) { if (source === null) { return null; } const result = Object.create(null); for (const selection of selections) { switch (selection.kind) { case graphql_1.Kind.FIELD: const responseName = (0, query_planner_1.getResponseName)(selection); const selections = selection.selections; if (typeof source[responseName] === 'undefined') { return null; } if (Array.isArray(source[responseName])) { result[responseName] = source[responseName].map((value) => selections ? executeSelectionSet(schema, value, selections) : value); } else if (selections) { result[responseName] = executeSelectionSet(schema, source[responseName], selections); } else { result[responseName] = source[responseName]; } break; case graphql_1.Kind.INLINE_FRAGMENT: if (!selection.typeCondition || !source) continue; if ((0, dataRewrites_1.isObjectOfType)(schema, source, selection.typeCondition)) { (0, deepMerge_1.deepMerge)(result, executeSelectionSet(schema, source, selection.selections)); } break; } } return result; } function moveIntoCursor(cursor, pathInCursor) { const data = flattenResultsAtPath(cursor.data, pathInCursor); return data ? { path: cursor.path.concat(pathInCursor), data, fullResult: cursor.fullResult, } : undefined; } function flattenResultsAtPath(value, path) { if (path.length === 0) return value; if (value === undefined || value === null) return value; const [current, ...rest] = path; if (current === '@') { return value.flatMap((element) => flattenResultsAtPath(element, rest)); } else { (0, federation_internals_1.assert)(typeof current === 'string', () => `Unexpected ${typeof current} found in path`); (0, federation_internals_1.assert)(!Array.isArray(value), () => `Unexpected array in result for path element ${current}`); return flattenResultsAtPath(value[current], rest); } } function downstreamServiceError(originalError, serviceName, generateErrorPath) { let { message } = originalError; const { extensions } = originalError; if (!message) { message = `Error while fetching subquery from service "${serviceName}"`; } const errorOptions = { originalError: originalError, path: generateErrorPath(originalError.path), extensions: { ...extensions, serviceName, }, }; const codeDef = (0, federation_internals_1.errorCodeDef)(originalError); if (!codeDef && (extensions === null || extensions === void 0 ? void 0 : extensions.code)) { return new graphql_1.GraphQLError(message, errorOptions); } return (codeDef !== null && codeDef !== void 0 ? codeDef : federation_internals_1.ERRORS.DOWNSTREAM_SERVICE_ERROR).err(message, errorOptions); } const defaultFieldResolverWithAliasSupport = function (source, args, contextValue, info) { if (typeof source === 'object' || typeof source === 'function') { const property = source[info.path.key]; if (typeof property === 'function') { return source[info.fieldName](args, contextValue, info); } return property; } }; exports.defaultFieldResolverWithAliasSupport = defaultFieldResolverWithAliasSupport; function durationHrTimeToNanos(hrtime) { return hrtime[0] * 1e9 + hrtime[1]; } function dateToProtoTimestamp(date) { const totalMillis = +date; const millis = totalMillis % 1000; return new usage_reporting_protobuf_1.google.protobuf.Timestamp({ seconds: (totalMillis - millis) / 1000, nanos: millis * 1e6, }); } //# sourceMappingURL=executeQueryPlan.js.map