Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/__tests__/starWarsQuery-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,7 @@ describe('Star Wars Query Tests', () => {
{
message: 'secretBackstory is secret.',
locations: [{ line: 5, column: 13 }],
pathNonNull: [false, false],
path: ['hero', 'secretBackstory'],
},
],
Expand Down Expand Up @@ -451,16 +452,19 @@ describe('Star Wars Query Tests', () => {
{
message: 'secretBackstory is secret.',
locations: [{ line: 7, column: 15 }],
pathNonNull: [false, false, false, false],
path: ['hero', 'friends', 0, 'secretBackstory'],
},
{
message: 'secretBackstory is secret.',
locations: [{ line: 7, column: 15 }],
pathNonNull: [false, false, false, false],
path: ['hero', 'friends', 1, 'secretBackstory'],
},
{
message: 'secretBackstory is secret.',
locations: [{ line: 7, column: 15 }],
pathNonNull: [false, false, false, false],
path: ['hero', 'friends', 2, 'secretBackstory'],
},
],
Expand Down Expand Up @@ -489,6 +493,7 @@ describe('Star Wars Query Tests', () => {
{
message: 'secretBackstory is secret.',
locations: [{ line: 5, column: 13 }],
pathNonNull: [false, false],
path: ['mainHero', 'story'],
},
],
Expand Down
24 changes: 22 additions & 2 deletions src/error/GraphQLError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ export interface GraphQLErrorOptions {
positions?: Maybe<ReadonlyArray<number>>;
/** Response path where this error occurred during execution. */
path?: Maybe<ReadonlyArray<string | number>>;
/** Nullability for each position in the response path. */
pathNonNull?: Maybe<ReadonlyArray<boolean>>;
/**
* Original error that caused this GraphQLError, if one exists.
* Deprecated in favor of `cause` to better align with JavaScript standards.
Expand Down Expand Up @@ -85,6 +87,9 @@ export class GraphQLError extends Error {
*/
readonly path: ReadonlyArray<string | number> | undefined;

/** Nullability for each position in the response path. @experimental */
readonly pathNonNull: ReadonlyArray<boolean> | undefined;

/** An array of GraphQL AST Nodes corresponding to this error. */
readonly nodes: ReadonlyArray<ASTNode> | undefined;

Expand Down Expand Up @@ -156,8 +161,16 @@ export class GraphQLError extends Error {
* ```
*/
constructor(message: string, options: GraphQLErrorOptions = {}) {
const { nodes, source, positions, path, originalError, cause, extensions } =
options;
const {
nodes,
source,
positions,
path,
pathNonNull,
originalError,
cause,
extensions,
} = options;

const hasCause = 'cause' in options;
const errorCause = hasCause ? cause : originalError;
Expand All @@ -167,6 +180,7 @@ export class GraphQLError extends Error {

this.name = 'GraphQLError';
this.path = path ?? undefined;
this.pathNonNull = pathNonNull ?? undefined;
const underlyingError: typeof originalError =
originalError ?? (cause instanceof Error ? cause : undefined);
this.originalError = underlyingError;
Expand Down Expand Up @@ -309,6 +323,10 @@ export class GraphQLError extends Error {
formattedError.path = this.path;
}

if (this.pathNonNull != null) {
formattedError.pathNonNull = this.pathNonNull;
}

if (this.extensions != null && Object.keys(this.extensions).length > 0) {
formattedError.extensions = this.extensions;
}
Expand Down Expand Up @@ -343,6 +361,8 @@ export interface GraphQLFormattedError {
* identify whether a null result is intentional or caused by a runtime error.
*/
readonly path?: ReadonlyArray<string | number>;
/** Nullability for each position in `path`. @experimental */
readonly pathNonNull?: ReadonlyArray<boolean>;
/**
* Reserved for implementors to extend the protocol however they see fit,
* and hence there are no additional restrictions on its contents.
Expand Down
29 changes: 29 additions & 0 deletions src/error/GraphQLErrorBehavior.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* Specifies how execution errors should be handled:
*
* - `PROPAGATE`: errors in non-null positions propagate to the closest nullable position.
* - `NULL`: errors resolve to null without propagation.
* - `ABORT`: errors propagate as far as possible, typically to the operation
* root. With incremental delivery, propagation can stop at an incremental
* delivery boundary.
* @experimental
* @category Errors
*/
export type GraphQLErrorBehavior = 'NULL' | 'PROPAGATE' | 'ABORT';

/**
* True if the given value is a GraphQL error behavior.
* @param onError - The value to check.
* @returns True when the value is a supported error behavior.
* @experimental
* @example
* ```ts
* isErrorBehavior('PROPAGATE'); // true
* isErrorBehavior('THROW'); // false
* ```
*/
export function isErrorBehavior(
onError: unknown,
): onError is GraphQLErrorBehavior {
return onError === 'NULL' || onError === 'PROPAGATE' || onError === 'ABORT';
}
15 changes: 14 additions & 1 deletion src/error/__tests__/GraphQLError-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ describe('GraphQLError', () => {
source,
positions: [1, 2, 3],
path: ['a', 'b', 'c'],
pathNonNull: [false, true, false],
cause: new Error('test'),
originalError: new Error('test'),
extensions: { foo: 'bar' },
Expand All @@ -60,6 +61,7 @@ describe('GraphQLError', () => {
'message',
'locations',
'path',
'pathNonNull',
'extensions',
]);
});
Expand Down Expand Up @@ -328,10 +330,12 @@ describe('GraphQLError', () => {
`);

const path = ['path', 2, 'field'];
const pathNonNull = [false, true, false];
const extensions = { foo: 'bar' };
const eFull = new GraphQLError('msg', {
nodes: fieldNode,
path,
pathNonNull,
extensions,
});

Expand All @@ -351,6 +355,11 @@ describe('GraphQLError', () => {
2,
"field"
],
"pathNonNull": [
false,
true,
false
],
"extensions": {
"foo": "bar"
}
Expand Down Expand Up @@ -427,11 +436,15 @@ describe('toString', () => {

describe('toJSON', () => {
it('includes path', () => {
const error = new GraphQLError('msg', { path: ['path', 3, 'to', 'field'] });
const error = new GraphQLError('msg', {
path: ['path', 3, 'to', 'field'],
pathNonNull: [false, true, false, true],
});

expect(error.toJSON()).to.deep.equal({
message: 'msg',
path: ['path', 3, 'to', 'field'],
pathNonNull: [false, true, false, true],
});
});

Expand Down
17 changes: 16 additions & 1 deletion src/error/__tests__/locatedError-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ import { locatedError } from '../locatedError.ts';

describe('locatedError', () => {
it('passes GraphQLError through', () => {
const e = new GraphQLError('msg', { path: ['path', 3, 'to', 'field'] });
const e = new GraphQLError('msg', {
path: ['path', 3, 'to', 'field'],
pathNonNull: [false, true, false, true],
});

expect(locatedError(e, [], [])).to.deep.equal(e);
});
Expand All @@ -30,21 +33,33 @@ describe('locatedError', () => {
// @ts-expect-error
e.path = [];
// @ts-expect-error
e.pathNonNull = [];
// @ts-expect-error
e.nodes = [];
// @ts-expect-error
e.source = null;
// @ts-expect-error
e.positions = [];
e.name = 'GraphQLError';

expect(locatedError(e, [], { path: [], pathNonNull: [] })).to.deep.equal(e);
// Test legacy:
expect(locatedError(e, [], [])).to.deep.equal(e);
// Test legacy optional:
expect(locatedError(e, [])).to.deep.equal(e);
});

it('does not pass through elasticsearch-like errors', () => {
const e = new Error('I am from elasticsearch');
// @ts-expect-error
e.path = '/something/feed/_search';

expect(
locatedError(e, [], { path: [], pathNonNull: [] }),
).to.not.deep.equal(e);
// Test legacy:
expect(locatedError(e, [], [])).to.not.deep.equal(e);
// Test legacy optional:
expect(locatedError(e, [])).to.not.deep.equal(e);
});
});
2 changes: 2 additions & 0 deletions src/error/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,5 @@ export type {
export { syntaxError } from './syntaxError.ts';

export { locatedError } from './locatedError.ts';
export type { GraphQLErrorBehavior } from './GraphQLErrorBehavior.ts';
export { isErrorBehavior } from './GraphQLErrorBehavior.ts';
44 changes: 41 additions & 3 deletions src/error/locatedError.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
/** @category Errors */

import type { Maybe } from '../jsutils/Maybe.ts';
import type { PathDigest } from '../jsutils/Path.ts';
import { toError } from '../jsutils/toError.ts';

import type { ASTNode } from '../language/ast.ts';

import { GraphQLError } from './GraphQLError.ts';

type Optional<T> = { [K in keyof T]: T[K] | undefined };

/**
* Given an arbitrary value, presumably thrown while attempting to execute a
* GraphQL operation, produce a new GraphQLError aware of the location in the
* document responsible for the original Error.
* @param rawOriginalError - The original error value to wrap.
* @param nodes - The AST nodes associated with the error.
* @param path - The response path associated with the error.
* @param digest - The response path digest associated with the error.
* @returns The GraphQL error.
* @example
* ```ts
Expand All @@ -22,17 +25,45 @@ import { GraphQLError } from './GraphQLError.ts';
*
* const document = parse('{ viewer { name } }');
* const fieldNode = document.definitions[0].selectionSet.selections[0];
* const error = locatedError(new Error('Resolver failed'), fieldNode, ['viewer']);
* const error = locatedError(new Error('Resolver failed'), fieldNode, {
* path: ['viewer'],
* pathNonNull: [false],
* });
*
* error.message; // => 'Resolver failed'
* error.locations; // => [{ line: 1, column: 3 }]
* error.path; // => ['viewer']
* ```
*/
export function locatedError(
rawOriginalError: unknown,
nodes: ASTNode | ReadonlyArray<ASTNode> | undefined | null,
digest: PathDigest,
): GraphQLError;
/**
* Given an arbitrary value, presumably thrown while attempting to execute a
* GraphQL operation, produce a new GraphQLError aware of the location in the
* document responsible for the original Error.
* @param rawOriginalError - The original error value to wrap.
* @param nodes - The AST nodes associated with the error.
* @param path - The response path associated with the error.
* @returns The GraphQL error.
* @example
* ```ts
* locatedError(new Error('Resolver failed'), undefined, ['viewer']);
* ```
* @deprecated Pass a digest rather than a path.
*/
export function locatedError(
rawOriginalError: unknown,
nodes: ASTNode | ReadonlyArray<ASTNode> | undefined | null,
path?: Maybe<ReadonlyArray<string | number>>,
): GraphQLError;
/** @internal */
export function locatedError(
rawOriginalError: unknown,
nodes: ASTNode | ReadonlyArray<ASTNode> | undefined | null,
digestOrPath?: Maybe<PathDigest | ReadonlyArray<string | number>>,
): GraphQLError {
const originalError = toError(rawOriginalError);

Expand All @@ -41,15 +72,22 @@ export function locatedError(
return originalError;
}

const { path, pathNonNull }: Optional<PathDigest> =
digestOrPath == null
? { path: undefined, pathNonNull: undefined }
: 'length' in digestOrPath
? { path: digestOrPath, pathNonNull: undefined }
: digestOrPath;
return new GraphQLError(originalError.message, {
nodes: (originalError as GraphQLError).nodes ?? nodes,
source: (originalError as GraphQLError).source,
positions: (originalError as GraphQLError).positions,
path,
pathNonNull,
originalError,
});
}

function isLocatedGraphQLError(error: any): error is GraphQLError {
return Array.isArray(error.path);
return Array.isArray(error.path) && Array.isArray(error.pathNonNull);
}
15 changes: 13 additions & 2 deletions src/execution/ExecutionArgs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import type { Maybe } from '../jsutils/Maybe.ts';
import type { ObjMap } from '../jsutils/ObjMap.ts';

import type { GraphQLErrorBehavior } from '../error/GraphQLErrorBehavior.ts';

import type {
DocumentNode,
FragmentDefinitionNode,
Expand Down Expand Up @@ -39,6 +41,15 @@ export interface ExecutionArgs {
typeResolver?: Maybe<GraphQLTypeResolver<any, any>>;
/** Resolver used for the root subscription field. */
subscribeFieldResolver?: Maybe<GraphQLFieldResolver<any, any>>;
/**
* Set to `"NULL"` to disable error propagation. Set to `"ABORT"` to have
* errors propagate as far as possible, typically to the operation root.
* With incremental delivery, propagation can stop at an incremental delivery
* boundary.
* @defaultValue `"PROPAGATE"`
* @experimental
*/
onError?: GraphQLErrorBehavior;
/** Whether suggestion text should be omitted from request errors. */
hideSuggestions?: Maybe<boolean>;
/** AbortSignal used to cancel execution. */
Expand Down Expand Up @@ -94,8 +105,8 @@ export interface ValidatedExecutionArgs {
subscribeFieldResolver: GraphQLFieldResolver<any, any>;
/** Whether suggestion text should be omitted from execution errors. */
hideSuggestions: boolean;
/** Whether execution should use error propagation. */
errorPropagation: boolean;
/** How execution errors should be handled. */
onError: GraphQLErrorBehavior;
/** External signal that may abort execution. */
externalAbortSignal: AbortSignal | undefined;
/** Whether incremental execution may begin eligible work early. */
Expand Down
Loading
Loading