@apollo/gateway
Version:
546 lines • 25.5 kB
JavaScript
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
;