import { isObjectLike } from '../jsutils/isObjectLike';
import type { Maybe } from '../jsutils/Maybe';
import type { ASTNode, Location } from '../language/ast';
import type { SourceLocation } from '../language/location';
import { getLocation } from '../language/location';
import { printLocation, printSourceLocation } from '../language/printLocation';
import type { Source } from '../language/source';
export interface GraphQLErrorExtensions {
[attributeName: string]: unknown;
}
export interface GraphQLFormattedErrorExtensions {
[attributeName: string]: unknown;
}
export interface GraphQLErrorOptions {
nodes?: ReadonlyArray<ASTNode> | ASTNode | null;
source?: Maybe<Source>;
positions?: Maybe<ReadonlyArray<number>>;
path?: Maybe<ReadonlyArray<string | number>>;
originalError?: Maybe<Error & { readonly extensions?: unknown }>;
extensions?: Maybe<GraphQLErrorExtensions>;
}
type BackwardsCompatibleArgs =
| [options?: GraphQLErrorOptions]
| [
nodes?: GraphQLErrorOptions['nodes'],
source?: GraphQLErrorOptions['source'],
positions?: GraphQLErrorOptions['positions'],
path?: GraphQLErrorOptions['path'],
originalError?: GraphQLErrorOptions['originalError'],
extensions?: GraphQLErrorOptions['extensions'],
];
function toNormalizedOptions(
args: BackwardsCompatibleArgs,
): GraphQLErrorOptions {
const firstArg = args[0];
if (firstArg == null || 'kind' in firstArg || 'length' in firstArg) {
return {
nodes: firstArg,
source: args[1],
positions: args[2],
path: args[3],
originalError: args[4],
extensions: args[5],
};
}
return firstArg;
}
export class GraphQLError extends Error {
readonly locations: ReadonlyArray<SourceLocation> | undefined;
readonly path: ReadonlyArray<string | number> | undefined;
readonly nodes: ReadonlyArray<ASTNode> | undefined;
readonly source: Source | undefined;
readonly positions: ReadonlyArray<number> | undefined;
readonly originalError: Error | undefined;
readonly extensions: GraphQLErrorExtensions;
constructor(message: string, options?: GraphQLErrorOptions);
constructor(
message: string,
nodes?: ReadonlyArray<ASTNode> | ASTNode | null,
source?: Maybe<Source>,
positions?: Maybe<ReadonlyArray<number>>,
path?: Maybe<ReadonlyArray<string | number>>,
originalError?: Maybe<Error & { readonly extensions?: unknown }>,
extensions?: Maybe<GraphQLErrorExtensions>,
);
constructor(message: string, ...rawArgs: BackwardsCompatibleArgs) {
const { nodes, source, positions, path, originalError, extensions } =
toNormalizedOptions(rawArgs);
super(message);
this.name = 'GraphQLError';
this.path = path ?? undefined;
this.originalError = originalError ?? undefined;
this.nodes = undefinedIfEmpty(
Array.isArray(nodes) ? nodes : nodes ? [nodes] : undefined,
);
const nodeLocations = undefinedIfEmpty(
this.nodes
?.map((node) => node.loc)
.filter((loc): loc is Location => loc != null),
);
this.source = source ?? nodeLocations?.[0]?.source;
this.positions = positions ?? nodeLocations?.map((loc) => loc.start);
this.locations =
positions && source
? positions.map((pos) => getLocation(source, pos))
: nodeLocations?.map((loc) => getLocation(loc.source, loc.start));
const originalExtensions = isObjectLike(originalError?.extensions)
? originalError?.extensions
: undefined;
this.extensions = extensions ?? originalExtensions ?? Object.create(null);
Object.defineProperties(this, {
message: {
writable: true,
enumerable: true,
},
name: { enumerable: false },
nodes: { enumerable: false },
source: { enumerable: false },
positions: { enumerable: false },
originalError: { enumerable: false },
});
if (originalError?.stack) {
Object.defineProperty(this, 'stack', {
value: originalError.stack,
writable: true,
configurable: true,
});
} else if (Error.captureStackTrace) {
Error.captureStackTrace(this, GraphQLError);
} else {
Object.defineProperty(this, 'stack', {
value: Error().stack,
writable: true,
configurable: true,
});
}
}
get [Symbol.toStringTag](): string {
return 'GraphQLError';
}
toString(): string {
let output = this.message;
if (this.nodes) {
for (const node of this.nodes) {
if (node.loc) {
output += '\n\n' + printLocation(node.loc);
}
}
} else if (this.source && this.locations) {
for (const location of this.locations) {
output += '\n\n' + printSourceLocation(this.source, location);
}
}
return output;
}
toJSON(): GraphQLFormattedError {
type WritableFormattedError = {
-readonly [P in keyof GraphQLFormattedError]: GraphQLFormattedError[P];
};
const formattedError: WritableFormattedError = {
message: this.message,
};
if (this.locations != null) {
formattedError.locations = this.locations;
}
if (this.path != null) {
formattedError.path = this.path;
}
if (this.extensions != null && Object.keys(this.extensions).length > 0) {
formattedError.extensions = this.extensions;
}
return formattedError;
}
}
function undefinedIfEmpty<T>(
array: Array<T> | undefined,
): Array<T> | undefined {
return array === undefined || array.length === 0 ? undefined : array;
}
export interface GraphQLFormattedError {
readonly message: string;
readonly locations?: ReadonlyArray<SourceLocation>;
readonly path?: ReadonlyArray<string | number>;
readonly extensions?: GraphQLFormattedErrorExtensions;
}
export function printError(error: GraphQLError): string {
return error.toString();
}
export function formatError(error: GraphQLError): GraphQLFormattedError {
return error.toJSON();
}