@apollo/gateway
Version:
940 lines (859 loc) • 33.4 kB
text/typescript
import { Headers } from 'node-fetch';
import {
GraphQLError,
Kind,
TypeNameMetaFieldDef,
GraphQLFieldResolver,
GraphQLFormattedError,
GraphQLSchema,
GraphQLErrorOptions,
DocumentNode,
executeSync,
OperationTypeNode,
FieldNode,
visit,
ASTNode,
VariableDefinitionNode,
} from 'graphql';
import { Trace, google } from '@apollo/usage-reporting-protobuf';
import { GraphQLDataSource, GraphQLDataSourceRequestKind } from './datasources/types';
import { OperationContext } from './operationContext';
import {
FetchNode,
PlanNode,
QueryPlan,
ResponsePath,
QueryPlanSelectionNode,
QueryPlanFieldNode,
getResponseName,
evaluateCondition,
} from '@apollo/query-planner';
import { deepMerge } from './utilities/deepMerge';
import { isNotNullOrUndefined } from './utilities/array';
import { SpanStatusCode } from "@opentelemetry/api";
import { OpenTelemetryConfig, OpenTelemetrySpanNames, recordExceptions, tracer } from "./utilities/opentelemetry";
import { assert, defaultRootName, errorCodeDef, ERRORS, Operation, operationFromDocument, Schema } from '@apollo/federation-internals';
import { GatewayGraphQLRequestContext, GatewayExecutionResult } from '@apollo/server-gateway-interface';
import { computeResponse } from './resultShaping';
import { applyRewrites, isObjectOfType } from './dataRewrites';
export type ServiceMap = {
[serviceName: string]: GraphQLDataSource;
};
type ResultMap = Record<string, any>;
/**
* Represents some "cursor" within the full result, or put another way, a path into the full result and where it points to.
*
* Note that results can include lists and the the `path` considered can traverse those lists (the path will have a '@' character) so
* the data pointed by a cursor is not necessarily a single "branch" of the full results, but is in general a flattened list of all
* the sub-branches pointed by the path.
*/
type ResultCursor = {
// Path into `fullResult` this cursor is pointing at.
path: ResponsePath,
// The data pointed by this cursor.
data: ResultMap | ResultMap[],
// The full result .
fullResult: ResultMap,
}
interface ExecutionContext {
queryPlan: QueryPlan;
operationContext: OperationContext;
operation: Operation,
serviceMap: ServiceMap;
requestContext: GatewayGraphQLRequestContext;
supergraphSchema: GraphQLSchema;
errors: GraphQLError[];
}
function collectUsedVariables(node: ASTNode): Set<string> {
const usedVariables = new Set<string>();
visit(node, {
Variable: ({ name }) => {
usedVariables.add(name.value);
}
});
return usedVariables;
}
function makeIntrospectionQueryDocument(
introspectionSelection: FieldNode,
variableDefinitions?: readonly VariableDefinitionNode[],
): DocumentNode {
const usedVariables = collectUsedVariables(introspectionSelection);
const usedVariableDefinitions = variableDefinitions?.filter((def) => usedVariables.has(def.variable.name.value));
assert(
usedVariables.size === (usedVariableDefinitions?.length ?? 0),
() => `Should have found all used variables ${[...usedVariables]} in definitions ${JSON.stringify(variableDefinitions)}`,
);
return {
kind: Kind.DOCUMENT,
definitions: [
{
kind: Kind.OPERATION_DEFINITION,
operation: OperationTypeNode.QUERY,
variableDefinitions: usedVariableDefinitions,
selectionSet: {
kind: Kind.SELECTION_SET,
selections: [ introspectionSelection ],
}
}
],
};
}
function executeIntrospection(
schema: GraphQLSchema,
introspectionSelection: FieldNode,
variableDefinitions: ReadonlyArray<VariableDefinitionNode> | undefined,
variableValues: Record<string, any> | undefined,
): any {
const { data, errors } = executeSync({
schema,
document: makeIntrospectionQueryDocument(introspectionSelection, variableDefinitions),
rootValue: {},
variableValues,
});
assert(
!errors || errors.length === 0,
() => `Introspection query for ${JSON.stringify(introspectionSelection)} should not have failed but got ${JSON.stringify(errors)}`
);
assert(data, () => `Introspection query for ${JSON.stringify(introspectionSelection)} should not have failed`);
return data[introspectionSelection.alias?.value ?? introspectionSelection.name.value];
}
export async function executeQueryPlan(
queryPlan: QueryPlan,
serviceMap: ServiceMap,
requestContext: GatewayGraphQLRequestContext,
operationContext: OperationContext,
supergraphSchema: GraphQLSchema,
apiSchema: Schema,
telemetryConfig?: OpenTelemetryConfig
): Promise<GatewayExecutionResult> {
const logger = requestContext.logger || console;
return tracer.startActiveSpan(OpenTelemetrySpanNames.EXECUTE, async span => {
try {
const errors: GraphQLError[] = [];
let operation: Operation;
try {
operation = operationFromDocument(
apiSchema,
{
kind: Kind.DOCUMENT,
definitions: [
operationContext.operation,
...Object.values(operationContext.fragments),
],
},
{
validate: false,
}
);
} catch (err) {
// We shouldn't really have errors as the operation should already have been validated, but if something still
// happens, we should report it properly (plus, some of our tests call this method directly and blow up if we don't
// handle this correctly).
// TODO: we are doing some duplicate work by building both `OperationContext` and this `Operation`. Ideally we
// would remove `OperationContext`, pass the `Operation` directly to this method, and only use that. This would change
// the signature of this method though and it is exported so ... maybe later ?
//
if (err instanceof GraphQLError) {
return { errors: [err] };
}
throw err;
}
const context: ExecutionContext = {
queryPlan,
operationContext,
operation,
serviceMap,
requestContext,
supergraphSchema,
errors,
};
const unfilteredData: ResultMap = Object.create(null);
const captureTraces = !!(
requestContext.metrics && requestContext.metrics.captureTraces
);
if (queryPlan.node?.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 tracer.startActiveSpan(OpenTelemetrySpanNames.POST_PROCESSING, async (span) => {
let data;
try {
let postProcessingErrors: GraphQLError[];
const variables = requestContext.request.variables;
({ data, errors: postProcessingErrors } = computeResponse({
operation,
variables,
input: unfilteredData,
introspectionHandling: (f) => executeIntrospection(
operationContext.schema,
f.expandFragments().toSelectionNode(),
operationContext.operation.variableDefinitions,
variables,
),
}));
// If we have errors during the post-processing, we ignore them if any other errors have been thrown during
// query plan execution. That is because in many cases, errors during query plan execution will leave the
// internal data in a state that triggers additional post-processing errors, but that leads to 2 errors recorded
// for the same problem and that is unexpected by clients. See https://github.com/apollographql/federation/issues/981
// for additional context.
// If we had no errors during query plan execution, then we do ship any post-processing ones, but not as "normal"
// errors, as "extensions". The reason is that we used to completely ignore those post-processing errors, and as a
// result some users have been relying on not getting errors in some nullability related cases that post-processing
// cover, and switching to returning errors in those case is problematic. Putting these error messages in `extensions`
// is a compromise in that the errors are still part of the reponse, which may help users debug an issue, but are
// not "normal graphQL errors", so clients and tooling will mostly ignore them.
//
// Note that this behavior is also what the router does (and in fact, the exact name of the `extensions` we use,
// "valueCompletion", comes from the router and we use it for alignment.
if (errors.length === 0 && postProcessingErrors.length > 0) {
recordExceptions(span, postProcessingErrors, telemetryConfig);
span.setStatus({ code:SpanStatusCode.ERROR });
return { extensions: { "valueCompletion": postProcessingErrors }, data };
}
} catch (error) {
recordExceptions(span, [error], telemetryConfig);
span.setStatus({ code:SpanStatusCode.ERROR });
if (error instanceof GraphQLError) {
return { errors: [error] };
} else if (error instanceof Error) {
return {
errors: [
new GraphQLError(
error.message,
{ originalError: error },
)
]
};
} else {
// The above cases should cover the known cases, but if we received
// something else in the `catch` — like an object or something, we
// may not want to merely return this to the client.
logger.error(
"Unexpected error during query plan execution: " + error);
return {
errors: [
new GraphQLError(
"Unexpected error during query plan execution",
)
]
};
}
}
finally {
span.end()
}
return errors.length === 0 ? { data } : { errors, data };
});
if(result.errors) {
recordExceptions(span, result.errors, telemetryConfig);
span.setStatus({ code:SpanStatusCode.ERROR });
}
return result;
}
catch (err) {
recordExceptions(span, [err], telemetryConfig)
span.setStatus({ code:SpanStatusCode.ERROR });
throw err;
}
finally {
span.end();
}
});
}
// Note: this function always returns a protobuf QueryPlanNode tree, even if
// we're going to ignore it, because it makes the code much simpler and more
// typesafe. However, it doesn't actually ask for traces from the backend
// service unless we are capturing traces for Studio.
async function executeNode(
context: ExecutionContext,
node: PlanNode,
currentCursor: ResultCursor | undefined,
captureTraces: boolean,
telemetryConfig?: OpenTelemetryConfig
): Promise<Trace.QueryPlanNode> {
if (!currentCursor) {
// XXX I don't understand `results` threading well enough to understand when this happens
// and if this corresponds to a real query plan node that should be reported or not.
//
// This may be if running something like `query { fooOrNullFromServiceA {
// somethingFromServiceB } }` and the first field is null, then we don't bother to run the
// inner field at all.
return new Trace.QueryPlanNode();
}
switch (node.kind) {
case 'Sequence': {
const traceNode = new Trace.QueryPlanNode.SequenceNode();
for (const childNode of node.nodes) {
const childTraceNode = await executeNode(
context,
childNode,
currentCursor,
captureTraces,
telemetryConfig
);
traceNode.nodes.push(childTraceNode!);
}
return new Trace.QueryPlanNode({ sequence: traceNode });
}
case 'Parallel': {
const childTraceNodes = await Promise.all(
node.nodes.map(async (childNode) =>
executeNode(
context,
childNode,
currentCursor,
captureTraces,
telemetryConfig
),
),
);
return new Trace.QueryPlanNode({
parallel: new Trace.QueryPlanNode.ParallelNode({
nodes: childTraceNodes,
}),
});
}
case 'Flatten': {
return new Trace.QueryPlanNode({
flatten: new Trace.QueryPlanNode.FlattenNode({
responsePath: node.path.map(
id =>
new 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 Trace.QueryPlanNode.FetchNode({
serviceName: node.serviceName,
// executeFetch will fill in the other fields if desired.
});
try {
await executeFetch(
context,
node,
currentCursor,
captureTraces ? traceNode : null,
telemetryConfig
);
} catch (error) {
context.errors.push(error);
}
return new Trace.QueryPlanNode({ fetch: traceNode });
}
case 'Condition': {
const condition = evaluateCondition(node, context.operation.variableDefinitions, context.requestContext.request.variables);
const pickedBranch = condition ? node.ifClause : node.elseClause;
let branchTraceNode: Trace.QueryPlanNode | undefined = undefined;
if (pickedBranch) {
branchTraceNode = await executeNode(
context,
pickedBranch,
currentCursor,
captureTraces,
telemetryConfig
);
}
return new Trace.QueryPlanNode({
condition: new Trace.QueryPlanNode.ConditionNode({
condition: node.condition,
ifClause: condition ? branchTraceNode : undefined,
elseClause: condition ? undefined : branchTraceNode,
}),
});
}
case 'Defer': {
assert(false, `@defer support is not available in the gateway`);
}
}
}
async function executeFetch(
context: ExecutionContext,
fetch: FetchNode,
currentCursor: ResultCursor,
traceNode: Trace.QueryPlanNode.FetchNode | null,
telemetryConfig?: OpenTelemetryConfig
): Promise<void> {
const logger = context.requestContext.logger || console;
const service = context.serviceMap[fetch.serviceName];
return tracer.startActiveSpan(OpenTelemetrySpanNames.FETCH, {attributes:{service:fetch.serviceName}}, async span => {
try {
if (!service) {
throw new Error(`Couldn't find service with name "${fetch.serviceName}"`);
}
let entities: ResultMap[];
if (Array.isArray(currentCursor.data)) {
// Remove null or undefined entities from the list
entities = currentCursor.data.filter(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) {
applyRewrites(context.supergraphSchema, fetch.outputRewrites, dataReceivedFromService);
}
for (const entity of entities) {
deepMerge(entity, dataReceivedFromService);
}
} else {
const requires = fetch.requires;
const representations: ResultMap[] = [];
const representationToEntity: number[] = [];
entities.forEach((entity, index) => {
const representation = executeSelectionSet(
// Note that `requires` may include references to inacessible elements, so we should "execute" it using the supergrah
// schema, _not_ the API schema (the one in `context.operationContext.schema`). And this is not a security risk since
// what we're extracting here is what is sent to subgraphs, and subgraphs knows `@inacessible` elements.
context.supergraphSchema,
entity,
requires,
);
if (representation && representation[TypeNameMetaFieldDef.name]) {
applyRewrites(context.supergraphSchema, fetch.inputRewrites, representation);
representations.push(representation);
representationToEntity.push(index);
}
});
// If there are no representations, that means the type conditions in
// the requires don't match any entities.
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;
}
applyRewrites(context.supergraphSchema, fetch.outputRewrites, receivedEntity);
deepMerge(entities[representationToEntity[i]], receivedEntity);
}
}
}
catch (err) {
recordExceptions(span, [err], telemetryConfig)
span.setStatus({ code:SpanStatusCode.ERROR });
throw err;
}
finally
{
span.end();
}
});
async function sendOperation(
variables: Record<string, any>,
): Promise<ResultMap | void | null> {
// We declare this as 'any' because it is missing url and method, which
// GraphQLRequest.http is supposed to have if it exists.
// (This is admittedly kinda weird, since we currently do pass url and
// method to `process` from the SDL fetching call site, but presumably
// existing implementation of the interface don't try to look for these
// fields. RemoteGraphQLDataSource just overwrites them.)
let http: any;
// If we're capturing a trace for Studio, then save the operation text to
// the node we're building and tell the federated service to include a trace
// in its response.
if (traceNode) {
http = {
headers: new 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: 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 (!response.extensions?.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 we're capturing a trace for Studio, save the received trace into the
// query plan.
if (traceNode) {
traceNode.receivedTime = dateToProtoTimestamp(new Date());
if (response.extensions && response.extensions.ftv1) {
const traceBase64 = response.extensions.ftv1;
let traceBuffer: Buffer | undefined;
let traceParsingFailed = false;
try {
// XXX support non-Node implementations by using Uint8Array? protobufjs
// supports that, but there's not a no-deps base64 implementation.
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 = 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) {
// Federation requires the root operations in the composed schema
// to have the default names (Query, Mutation, Subscription) even
// if the implementing services choose different names, so we override
// whatever the implementing service reported here.
const rootTypeName = defaultRootName(
context.operationContext.operation.operation,
);
traceNode.trace.root?.child?.forEach((child) => {
child.parentType = rootTypeName;
});
}
traceNode.traceParsingFailed = traceParsingFailed;
}
}
return response.data;
}
}
type ErrorPathGenerator = (
path: GraphQLErrorOptions['path'],
) => GraphQLErrorOptions['path'];
/**
* Given response data collected so far and a path such as:
*
* ["foo", "@", "bar", "@"]
*
* the returned function generates a list of "hydrated" paths, replacing the
* `"@"` with array indices from the actual data. When we encounter an error in
* a subgraph fetch, we can use the index in the error's path (e.g.
* `["_entities", 2, "boom"]`) to look up the appropriate "hydrated" path
* prefix. The result is something like:
*
* ["foo", 1, "bar", 2, "boom"]
*
* The returned function is lazy — if we don't encounter errors and it's never
* called, then we never process the response data to hydrate the paths.
*
* This approach is inspired by Apollo Router: https://github.com/apollographql/router/blob/0fd59d2e11cc09e82c876a5fee263b5658cb9539/apollo-router/src/query_planner/fetch.rs#L295-L403
*/
function makeLazyErrorPathGenerator(
fetch: FetchNode,
cursor: ResultCursor,
): ErrorPathGenerator {
let hydratedPaths: ResponsePath[] | undefined;
return (errorPath: GraphQLErrorOptions['path']) => {
if (fetch.requires && typeof errorPath?.[1] === 'number') {
// only generate paths if we need to look them up via entity index
if (!hydratedPaths) {
hydratedPaths = [];
generateHydratedPaths(
[],
cursor.path,
cursor.fullResult,
hydratedPaths,
);
}
const hydratedPath = hydratedPaths[errorPath[1]] ?? [];
return [...hydratedPath, ...errorPath.slice(2)];
} else {
return errorPath ? [...cursor.path, ...errorPath.slice()] : undefined;
}
};
}
/**
* Given a deeply nested object and a path such as `["foo", "@", "bar", "@"]`,
* walk the path to build up a list of of "hydrated" paths that match the data,
* such as:
*
* [
* ["foo", 0, "bar", 0, "boom"],
* ["foo", 0, "bar", 1, "boom"]
* ["foo", 1, "bar", 0, "boom"],
* ["foo", 1, "bar", 1, "boom"]
* ]
*/
export function generateHydratedPaths(
parent: ResponsePath,
path: ResponsePath,
data: ResultMap | null,
result: ResponsePath[],
) {
const head = path[0];
if (data == null) {
return;
}
if (head == null) { // terminate recursion
result.push(parent.slice());
} else if (head === '@') {
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 {
assert(false, `unknown path part "${head}"`);
}
}
/**
*
* @param source Result of GraphQL execution.
* @param selectionSet
*/
function executeSelectionSet(
schema: GraphQLSchema,
source: Record<string, any> | null,
selections: QueryPlanSelectionNode[],
): Record<string, any> | null {
// If the underlying service has returned null for the parent (source)
// then there is no need to iterate through the parent's selection set
if (source === null) {
return null;
}
const result: Record<string, any> = Object.create(null);
for (const selection of selections) {
switch (selection.kind) {
case Kind.FIELD:
const responseName = getResponseName(selection as QueryPlanFieldNode);
const selections = (selection as QueryPlanFieldNode).selections;
if (typeof source[responseName] === 'undefined') {
// This method is called to collect the inputs/requires of a fetch. So, assuming query plans are correct
// (and we have not reason to assume otherwise here), all inputs should be fetched beforehand and the
// only reason for not finding one of the inputs is that we had an error fetching it _and_ that field
// is non-nullable (it it was nullable, error fetching the input would have make that input `null`; not
// having the input means the field is non-nullable so the whole entity had to be nullified/ignored,
// leading use to not have the field at all).
// In any case, we don't have all the necessary inputs for this particular entity and should ignore it.
// Note that an error has already been logged for whichever issue happen while fetching the inputs we're
// missing here, and that error had much more context, so no reason to log a duplicate (less useful) error
// here.
return null;
}
if (Array.isArray(source[responseName])) {
result[responseName] = source[responseName].map((value: any) =>
selections
? executeSelectionSet(schema, value, selections)
: value,
);
} else if (selections) {
result[responseName] = executeSelectionSet(
schema,
source[responseName],
selections,
);
} else {
result[responseName] = source[responseName];
}
break;
case Kind.INLINE_FRAGMENT:
if (!selection.typeCondition || !source) continue;
if (isObjectOfType(schema, source, selection.typeCondition)) {
deepMerge(
result,
executeSelectionSet(schema, source, selection.selections),
);
}
break;
}
}
return result;
}
function moveIntoCursor(cursor: ResultCursor, pathInCursor: ResponsePath): ResultCursor | undefined {
const data = flattenResultsAtPath(cursor.data, pathInCursor);
return data ? {
path: cursor.path.concat(pathInCursor),
data,
fullResult: cursor.fullResult,
} : undefined;
}
function flattenResultsAtPath(value: ResultCursor['data'] | undefined | null, path: ResponsePath): ResultCursor['data'] | undefined | null {
if (path.length === 0) return value;
if (value === undefined || value === null) return value;
const [current, ...rest] = path;
if (current === '@') {
return value.flatMap((element: any) => flattenResultsAtPath(element, rest));
} else {
assert(typeof current === 'string', () => `Unexpected ${typeof current} found in path`);
assert(!Array.isArray(value), () => `Unexpected array in result for path element ${current}`);
// Note that this typecheck because `value[current]` is of type `any` and so the typechecker "trusts us", but in
// practice this only work because we use this on path that do not point to leaf types, and the `value[current]`
// is never a base type (non-object nor null/undefined).
return flattenResultsAtPath(value[current], rest);
}
}
function downstreamServiceError(
originalError: GraphQLFormattedError,
serviceName: string,
generateErrorPath: ErrorPathGenerator,
) {
let { message } = originalError;
const { extensions } = originalError;
if (!message) {
message = `Error while fetching subquery from service "${serviceName}"`;
}
const errorOptions: GraphQLErrorOptions = {
originalError: originalError as Error,
path: generateErrorPath(originalError.path),
extensions: {
...extensions,
// XXX The presence of a serviceName in extensions is used to
// determine if this error should be captured for metrics reporting.
serviceName,
},
};
const codeDef = errorCodeDef(originalError);
// It's possible the orignal has a code, but not one we know about (one generated by the underlying `GraphQLDataSource`,
// which we don't control). In that case, we want to use that code (and have thus no `ErrorCodeDefinition` usable).
if (!codeDef && extensions?.code) {
return new GraphQLError(message, errorOptions);
}
// Otherwise, we either use the code we found and know, or default to a general downstream error code.
return (codeDef ?? ERRORS.DOWNSTREAM_SERVICE_ERROR).err(
message,
errorOptions,
);
}
export const defaultFieldResolverWithAliasSupport: GraphQLFieldResolver<
any,
any
> = function(source, args, contextValue, info) {
// ensure source is a value for which property access is acceptable.
if (typeof source === 'object' || typeof source === 'function') {
// if this is an alias, check it first because a downstream service
// would have returned the data *already cast* to an alias responseName
const property = source[info.path.key];
if (typeof property === 'function') {
return source[info.fieldName](args, contextValue, info);
}
return property;
}
};
// Converts an hrtime array (as returned from process.hrtime) to nanoseconds.
//
// ONLY CALL THIS ON VALUES REPRESENTING DELTAS, NOT ON THE RAW RETURN VALUE
// FROM process.hrtime() WITH NO ARGUMENTS.
//
// The entire point of the hrtime data structure is that the JavaScript Number
// type can't represent all int64 values without loss of precision:
// Number.MAX_SAFE_INTEGER nanoseconds is about 104 days. Calling this function
// on a duration that represents a value less than 104 days is fine. Calling
// this function on an absolute time (which is generally roughly time since
// system boot) is not a good idea.
//
// XXX We should probably use google.protobuf.Duration on the wire instead of
// ever trying to store durations in a single number.
function durationHrTimeToNanos(hrtime: [number, number]) {
return hrtime[0] * 1e9 + hrtime[1];
}
// Converts a JS Date into a Timestamp.
function dateToProtoTimestamp(date: Date): google.protobuf.Timestamp {
const totalMillis = +date;
const millis = totalMillis % 1000;
return new google.protobuf.Timestamp({
seconds: (totalMillis - millis) / 1000,
nanos: millis * 1e6,
});
}