import { isObjectLike } from '../jsutils/isObjectLike.js';
import type { Maybe } from '../jsutils/Maybe.js';
import type { ASTNode, Location } from '../language/ast.js';
import type { SourceLocation } from '../language/location.js';
import { getLocation } from '../language/location.js';
import {
printLocation,
printSourceLocation,
} from '../language/printLocation.js';
import type { Source } from '../language/source.js';
export interface GraphQLErrorExtensions {
[attributeName: string]: unknown;
}
export interface GraphQLErrorOptions {
nodes?: ReadonlyArray<ASTNode> | ASTNode | null | undefined;
source?: Maybe<Source>;
positions?: Maybe<ReadonlyArray<number>>;
path?: Maybe<ReadonlyArray<string | number>>;
originalError?: Maybe<Error & { readonly extensions?: unknown }>;
extensions?: Maybe<GraphQLErrorExtensions>;
}
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 = {}) {
const { nodes, source, positions, path, originalError, extensions } =
options;
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';
}
override 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?: { [key: string]: unknown };
}