1 | import {
|
2 | ASTNode,
|
3 | GraphQLError,
|
4 | GraphQLFormattedError,
|
5 | Source,
|
6 | SourceLocation,
|
7 | printError,
|
8 | formatError,
|
9 | } from 'graphql';
|
10 |
|
11 | declare module 'graphql' {
|
12 | export interface GraphQLErrorExtensions {
|
13 | exception?: {
|
14 | code?: string;
|
15 | stacktrace?: ReadonlyArray<string>;
|
16 | };
|
17 | }
|
18 | }
|
19 |
|
20 |
|
21 |
|
22 | export class ApolloError extends Error implements GraphQLError {
|
23 | public extensions: Record<string, any>;
|
24 | override readonly name!: string;
|
25 | readonly locations: ReadonlyArray<SourceLocation> | undefined;
|
26 | readonly path: ReadonlyArray<string | number> | undefined;
|
27 | readonly source: Source | undefined;
|
28 | readonly positions: ReadonlyArray<number> | undefined;
|
29 | readonly nodes: ReadonlyArray<ASTNode> | undefined;
|
30 | public originalError: Error | undefined;
|
31 |
|
32 | [key: string]: any;
|
33 |
|
34 | constructor(
|
35 | message: string,
|
36 | code?: string,
|
37 | extensions?: Record<string, any>,
|
38 | ) {
|
39 | super(message);
|
40 |
|
41 |
|
42 | if (!this.name) {
|
43 | Object.defineProperty(this, 'name', { value: 'ApolloError' });
|
44 | }
|
45 |
|
46 | if (extensions?.extensions) {
|
47 | throw Error(
|
48 | 'Pass extensions directly as the third argument of the ApolloError constructor: `new ' +
|
49 | 'ApolloError(message, code, {myExt: value})`, not `new ApolloError(message, code, ' +
|
50 | '{extensions: {myExt: value}})`',
|
51 | );
|
52 | }
|
53 |
|
54 | this.extensions = { ...extensions, code };
|
55 | }
|
56 |
|
57 | toJSON(): GraphQLFormattedError {
|
58 | return formatError(toGraphQLError(this));
|
59 | }
|
60 |
|
61 | override toString(): string {
|
62 | return printError(toGraphQLError(this));
|
63 | }
|
64 |
|
65 | get [Symbol.toStringTag](): string {
|
66 | return this.name;
|
67 | }
|
68 | }
|
69 |
|
70 | function toGraphQLError(error: ApolloError): GraphQLError {
|
71 | return new GraphQLError(
|
72 | error.message,
|
73 | error.nodes,
|
74 | error.source,
|
75 | error.positions,
|
76 | error.path,
|
77 | error.originalError,
|
78 | error.extensions,
|
79 | );
|
80 | }
|
81 |
|
82 | function enrichError(error: Partial<GraphQLError>, debug: boolean = false) {
|
83 |
|
84 |
|
85 | const expanded = Object.create(Object.getPrototypeOf(error), {
|
86 | name: {
|
87 | value: error.name,
|
88 | },
|
89 | message: {
|
90 | value: error.message,
|
91 | enumerable: true,
|
92 | writable: true,
|
93 | },
|
94 | locations: {
|
95 | value: error.locations || undefined,
|
96 | enumerable: true,
|
97 | },
|
98 | path: {
|
99 | value: error.path || undefined,
|
100 | enumerable: true,
|
101 | },
|
102 | nodes: {
|
103 | value: error.nodes || undefined,
|
104 | },
|
105 | source: {
|
106 | value: error.source || undefined,
|
107 | },
|
108 | positions: {
|
109 | value: error.positions || undefined,
|
110 | },
|
111 | originalError: {
|
112 | value: error.originalError,
|
113 | },
|
114 | });
|
115 |
|
116 | expanded.extensions = {
|
117 | ...error.extensions,
|
118 | code: error.extensions?.code || 'INTERNAL_SERVER_ERROR',
|
119 | exception: {
|
120 | ...error.extensions?.exception,
|
121 | ...(error.originalError as any),
|
122 | },
|
123 | };
|
124 |
|
125 |
|
126 |
|
127 |
|
128 | delete expanded.extensions.exception.extensions;
|
129 | if (debug && !expanded.extensions.exception.stacktrace) {
|
130 | const stack = error.originalError?.stack || error.stack;
|
131 | expanded.extensions.exception.stacktrace = stack?.split('\n');
|
132 | }
|
133 |
|
134 | if (Object.keys(expanded.extensions.exception).length === 0) {
|
135 |
|
136 | delete expanded.extensions.exception;
|
137 | }
|
138 |
|
139 | return expanded as ApolloError;
|
140 | }
|
141 |
|
142 | export function toApolloError(
|
143 | error: Error & { extensions?: Record<string, any> },
|
144 | code: string = 'INTERNAL_SERVER_ERROR',
|
145 | ): Error & { extensions: Record<string, any> } {
|
146 | let err = error;
|
147 | if (err.extensions) {
|
148 | err.extensions.code = code;
|
149 | } else {
|
150 | err.extensions = { code };
|
151 | }
|
152 | return err as Error & { extensions: Record<string, any> };
|
153 | }
|
154 |
|
155 | export interface ErrorOptions {
|
156 | code?: string;
|
157 |
|
158 |
|
159 | errorClass?: new (message: string) => ApolloError;
|
160 | }
|
161 |
|
162 | export function fromGraphQLError(error: GraphQLError, options?: ErrorOptions) {
|
163 | const copy: ApolloError = options?.errorClass
|
164 | ? new options.errorClass(error.message)
|
165 | : new ApolloError(error.message);
|
166 |
|
167 |
|
168 | Object.entries(error).forEach(([key, value]) => {
|
169 | if (key === 'extensions') {
|
170 | return;
|
171 | }
|
172 | copy[key] = value;
|
173 | });
|
174 |
|
175 |
|
176 | copy.extensions = {
|
177 | ...copy.extensions,
|
178 | ...error.extensions,
|
179 | };
|
180 |
|
181 |
|
182 | if (!copy.extensions.code) {
|
183 | copy.extensions.code = options?.code || 'INTERNAL_SERVER_ERROR';
|
184 | }
|
185 |
|
186 |
|
187 |
|
188 | Object.defineProperty(copy, 'originalError', { value: {} });
|
189 | Object.getOwnPropertyNames(error).forEach((key) => {
|
190 | Object.defineProperty(copy.originalError, key, {
|
191 | value: (error as any)[key],
|
192 | });
|
193 | });
|
194 |
|
195 | return copy;
|
196 | }
|
197 |
|
198 | export class SyntaxError extends ApolloError {
|
199 | constructor(message: string) {
|
200 | super(message, 'GRAPHQL_PARSE_FAILED');
|
201 |
|
202 | Object.defineProperty(this, 'name', { value: 'SyntaxError' });
|
203 | }
|
204 | }
|
205 |
|
206 | export class ValidationError extends ApolloError {
|
207 | constructor(message: string) {
|
208 | super(message, 'GRAPHQL_VALIDATION_FAILED');
|
209 |
|
210 | Object.defineProperty(this, 'name', { value: 'ValidationError' });
|
211 | }
|
212 | }
|
213 |
|
214 | export class AuthenticationError extends ApolloError {
|
215 | constructor(message: string, extensions?: Record<string, any>) {
|
216 | super(message, 'UNAUTHENTICATED', extensions);
|
217 |
|
218 | Object.defineProperty(this, 'name', { value: 'AuthenticationError' });
|
219 | }
|
220 | }
|
221 |
|
222 | export class ForbiddenError extends ApolloError {
|
223 | constructor(message: string, extensions?: Record<string, any>) {
|
224 | super(message, 'FORBIDDEN', extensions);
|
225 |
|
226 | Object.defineProperty(this, 'name', { value: 'ForbiddenError' });
|
227 | }
|
228 | }
|
229 |
|
230 | export class PersistedQueryNotFoundError extends ApolloError {
|
231 | constructor() {
|
232 | super('PersistedQueryNotFound', 'PERSISTED_QUERY_NOT_FOUND');
|
233 |
|
234 | Object.defineProperty(this, 'name', {
|
235 | value: 'PersistedQueryNotFoundError',
|
236 | });
|
237 | }
|
238 | }
|
239 |
|
240 | export class PersistedQueryNotSupportedError extends ApolloError {
|
241 | constructor() {
|
242 | super('PersistedQueryNotSupported', 'PERSISTED_QUERY_NOT_SUPPORTED');
|
243 |
|
244 | Object.defineProperty(this, 'name', {
|
245 | value: 'PersistedQueryNotSupportedError',
|
246 | });
|
247 | }
|
248 | }
|
249 |
|
250 | export class UserInputError extends ApolloError {
|
251 | constructor(message: string, extensions?: Record<string, any>) {
|
252 | super(message, 'BAD_USER_INPUT', extensions);
|
253 |
|
254 | Object.defineProperty(this, 'name', { value: 'UserInputError' });
|
255 | }
|
256 | }
|
257 |
|
258 | export function formatApolloErrors(
|
259 | errors: ReadonlyArray<Error>,
|
260 | options?: {
|
261 | formatter?: (error: GraphQLError) => GraphQLFormattedError;
|
262 | debug?: boolean;
|
263 | },
|
264 | ): Array<ApolloError> {
|
265 | if (!options) {
|
266 | return errors.map((error) => enrichError(error));
|
267 | }
|
268 | const { formatter, debug } = options;
|
269 |
|
270 |
|
271 |
|
272 |
|
273 |
|
274 |
|
275 |
|
276 |
|
277 |
|
278 |
|
279 |
|
280 |
|
281 |
|
282 |
|
283 |
|
284 |
|
285 |
|
286 |
|
287 |
|
288 |
|
289 |
|
290 | const enrichedErrors = errors.map((error) => enrichError(error, debug));
|
291 | const makePrintable = (error: GraphQLFormattedError) => {
|
292 | if (error instanceof Error) {
|
293 |
|
294 | const graphQLError = error as GraphQLFormattedError;
|
295 | return {
|
296 | message: graphQLError.message,
|
297 | ...(graphQLError.locations && { locations: graphQLError.locations }),
|
298 | ...(graphQLError.path && { path: graphQLError.path }),
|
299 | ...(graphQLError.extensions && { extensions: graphQLError.extensions }),
|
300 | };
|
301 | }
|
302 | return error;
|
303 | };
|
304 |
|
305 | if (!formatter) {
|
306 | return enrichedErrors;
|
307 | }
|
308 |
|
309 | return enrichedErrors.map((error) => {
|
310 | try {
|
311 | return makePrintable(formatter(error));
|
312 | } catch (err) {
|
313 | if (debug) {
|
314 |
|
315 | return enrichError(err as Partial<GraphQLError>, debug);
|
316 | } else {
|
317 |
|
318 | const newError = fromGraphQLError(
|
319 | new GraphQLError('Internal server error'),
|
320 | );
|
321 | return enrichError(newError, debug);
|
322 | }
|
323 | }
|
324 | }) as Array<ApolloError>;
|
325 | }
|