2018-04-17 21:19:44 -07:00
|
|
|
import { GraphQLError } from 'graphql';
|
2018-05-02 16:27:33 -07:00
|
|
|
import { LogStep, LogAction, LogMessage, LogFunction } from './logging';
|
2018-04-17 21:19:44 -07:00
|
|
|
|
2018-05-21 15:41:36 -07:00
|
|
|
export class ApolloError extends Error implements GraphQLError {
|
|
|
|
public extensions: Record<string, any>;
|
2018-05-30 16:41:30 -07:00
|
|
|
readonly name;
|
2018-05-21 15:41:36 -07:00
|
|
|
readonly locations;
|
|
|
|
readonly path;
|
|
|
|
readonly source;
|
|
|
|
readonly positions;
|
|
|
|
readonly nodes;
|
|
|
|
public originalError;
|
|
|
|
|
2018-05-16 14:46:33 -07:00
|
|
|
[key: string]: any;
|
|
|
|
|
2018-04-23 15:09:36 -07:00
|
|
|
constructor(
|
|
|
|
message: string,
|
|
|
|
code?: string,
|
|
|
|
properties?: Record<string, any>,
|
|
|
|
) {
|
2018-04-17 21:19:44 -07:00
|
|
|
super(message);
|
2018-05-20 02:06:16 -07:00
|
|
|
// Set the prototype explicitly.
|
|
|
|
// https://stackoverflow.com/a/41102306
|
|
|
|
Object.setPrototypeOf(this, ApolloError.prototype);
|
|
|
|
|
2018-05-03 09:59:12 -07:00
|
|
|
if (properties) {
|
|
|
|
Object.keys(properties).forEach(key => {
|
|
|
|
this[key] = properties[key];
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2018-05-30 16:41:30 -07:00
|
|
|
//if no name provided, use the default. defineProperty ensures that it stays non-enumerable
|
|
|
|
if (!this.name) {
|
|
|
|
Object.defineProperty(this, 'name', { value: 'ApolloError' });
|
|
|
|
}
|
|
|
|
|
2018-05-02 16:43:44 -07:00
|
|
|
//extensions are flattened to be included in the root of GraphQLError's, so
|
|
|
|
//don't add properties to extensions
|
|
|
|
this.extensions = { code };
|
2018-04-17 21:19:44 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-05-21 15:41:36 -07:00
|
|
|
function enrichError(error: Partial<GraphQLError>, debug: boolean = false) {
|
|
|
|
const expanded = {} as any;
|
2018-05-21 16:08:32 -07:00
|
|
|
// follows similar structure to https://github.com/graphql/graphql-js/blob/master/src/error/GraphQLError.js#L145-L193
|
|
|
|
// with the addition of name
|
2018-05-21 15:41:36 -07:00
|
|
|
Object.defineProperties(expanded, {
|
|
|
|
name: {
|
|
|
|
value: error.name,
|
|
|
|
},
|
|
|
|
message: {
|
|
|
|
value: error.message,
|
|
|
|
enumerable: true,
|
|
|
|
writable: true,
|
|
|
|
},
|
|
|
|
locations: {
|
|
|
|
value: error.locations || undefined,
|
|
|
|
enumerable: true,
|
|
|
|
},
|
|
|
|
path: {
|
|
|
|
value: error.path || undefined,
|
|
|
|
enumerable: true,
|
|
|
|
},
|
|
|
|
nodes: {
|
|
|
|
value: error.nodes || undefined,
|
|
|
|
},
|
|
|
|
source: {
|
|
|
|
value: error.source || undefined,
|
|
|
|
},
|
|
|
|
positions: {
|
|
|
|
value: error.positions || undefined,
|
|
|
|
},
|
|
|
|
originalError: {
|
|
|
|
value: error.originalError,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
expanded.extensions = {
|
|
|
|
...error.extensions,
|
|
|
|
code:
|
|
|
|
(error.extensions && error.extensions.code) || 'INTERNAL_SERVER_ERROR',
|
|
|
|
exception: {
|
|
|
|
...(error.extensions && error.extensions.exception),
|
|
|
|
...(error.originalError as any),
|
2018-04-17 21:19:44 -07:00
|
|
|
},
|
|
|
|
};
|
|
|
|
|
|
|
|
//ensure that extensions is not taken from the originalError
|
|
|
|
//graphql-js ensures that the originalError's extensions are hoisted
|
|
|
|
//https://github.com/graphql/graphql-js/blob/0bb47b2/src/error/GraphQLError.js#L138
|
|
|
|
delete expanded.extensions.exception.extensions;
|
2018-04-18 09:56:02 -04:00
|
|
|
if (debug && !expanded.extensions.exception.stacktrace) {
|
2018-04-17 21:19:44 -07:00
|
|
|
expanded.extensions.exception.stacktrace =
|
|
|
|
(error.originalError &&
|
|
|
|
error.originalError.stack &&
|
|
|
|
error.originalError.stack.split('\n')) ||
|
|
|
|
(error.stack && error.stack.split('\n'));
|
|
|
|
}
|
|
|
|
|
|
|
|
if (Object.keys(expanded.extensions.exception).length === 0) {
|
|
|
|
//remove from printing an empty object
|
2018-04-18 09:56:02 -04:00
|
|
|
delete expanded.extensions.exception;
|
2018-04-17 21:19:44 -07:00
|
|
|
}
|
|
|
|
|
2018-05-21 15:41:36 -07:00
|
|
|
return expanded as ApolloError;
|
2018-04-17 21:19:44 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
export function toApolloError(
|
2018-05-21 15:41:36 -07:00
|
|
|
error: Error & { extensions?: Record<string, any> },
|
2018-04-18 09:52:36 -07:00
|
|
|
code: string = 'INTERNAL_SERVER_ERROR',
|
2018-04-17 21:19:44 -07:00
|
|
|
): Error & { extensions: Record<string, any> } {
|
2018-05-21 15:41:36 -07:00
|
|
|
let err = error;
|
2018-04-17 21:19:44 -07:00
|
|
|
if (err.extensions) {
|
|
|
|
err.extensions.code = code;
|
|
|
|
} else {
|
|
|
|
err.extensions = { code };
|
|
|
|
}
|
|
|
|
return err as Error & { extensions: Record<string, any> };
|
|
|
|
}
|
|
|
|
|
2018-04-23 15:09:36 -07:00
|
|
|
export interface ErrorOptions {
|
|
|
|
code?: string;
|
|
|
|
errorClass?: typeof ApolloError;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function fromGraphQLError(error: GraphQLError, options?: ErrorOptions) {
|
2018-05-21 15:41:36 -07:00
|
|
|
const copy: ApolloError =
|
2018-04-23 16:24:08 -07:00
|
|
|
options && options.errorClass
|
|
|
|
? new options.errorClass(error.message)
|
|
|
|
: new ApolloError(error.message);
|
2018-04-23 15:09:36 -07:00
|
|
|
|
|
|
|
//copy enumerable keys
|
|
|
|
Object.keys(error).forEach(key => {
|
|
|
|
copy[key] = error[key];
|
|
|
|
});
|
|
|
|
|
|
|
|
//extensions are non enumerable, so copy them directly
|
2018-04-17 21:19:44 -07:00
|
|
|
copy.extensions = {
|
|
|
|
...copy.extensions,
|
2018-04-23 15:09:36 -07:00
|
|
|
...error.extensions,
|
2018-04-17 21:19:44 -07:00
|
|
|
};
|
|
|
|
|
2018-04-23 15:09:36 -07:00
|
|
|
//Fallback on default for code
|
|
|
|
if (!copy.extensions.code) {
|
2018-04-23 16:24:08 -07:00
|
|
|
copy.extensions.code = (options && options.code) || 'INTERNAL_SERVER_ERROR';
|
2018-04-23 15:09:36 -07:00
|
|
|
}
|
|
|
|
|
2018-04-17 21:19:44 -07:00
|
|
|
//copy the original error, while keeping all values non-enumerable, so they
|
|
|
|
//are not printed unless directly referenced
|
|
|
|
Object.defineProperty(copy, 'originalError', { value: {} });
|
2018-05-03 17:44:53 -07:00
|
|
|
Object.getOwnPropertyNames(error).forEach(key => {
|
2018-04-17 21:19:44 -07:00
|
|
|
Object.defineProperty(copy.originalError, key, { value: error[key] });
|
|
|
|
});
|
|
|
|
|
|
|
|
return copy;
|
|
|
|
}
|
|
|
|
|
2018-04-23 15:09:36 -07:00
|
|
|
export class SyntaxError extends ApolloError {
|
2018-04-17 21:19:44 -07:00
|
|
|
constructor(message: string) {
|
2018-04-23 15:09:36 -07:00
|
|
|
super(message, 'GRAPHQL_PARSE_FAILED');
|
2018-05-20 02:06:16 -07:00
|
|
|
|
|
|
|
// Set the prototype explicitly.
|
|
|
|
// https://stackoverflow.com/a/41102306
|
|
|
|
Object.setPrototypeOf(this, SyntaxError.prototype);
|
|
|
|
Object.defineProperty(this, 'name', { value: 'SyntaxError' });
|
2018-04-17 21:19:44 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export class ValidationError extends ApolloError {
|
|
|
|
constructor(message: string) {
|
2018-04-23 15:09:36 -07:00
|
|
|
super(message, 'GRAPHQL_VALIDATION_FAILED');
|
2018-05-20 02:06:16 -07:00
|
|
|
|
|
|
|
// Set the prototype explicitly.
|
|
|
|
// https://stackoverflow.com/a/41102306
|
|
|
|
Object.setPrototypeOf(this, ValidationError.prototype);
|
|
|
|
Object.defineProperty(this, 'name', { value: 'ValidationError' });
|
2018-04-17 21:19:44 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export class AuthenticationError extends ApolloError {
|
2018-04-23 15:09:36 -07:00
|
|
|
constructor(message: string) {
|
|
|
|
super(message, 'UNAUTHENTICATED');
|
2018-05-20 02:06:16 -07:00
|
|
|
|
|
|
|
// Set the prototype explicitly.
|
|
|
|
// https://stackoverflow.com/a/41102306
|
|
|
|
Object.setPrototypeOf(this, AuthenticationError.prototype);
|
|
|
|
Object.defineProperty(this, 'name', { value: 'AuthenticationError' });
|
2018-04-23 15:09:36 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export class ForbiddenError extends ApolloError {
|
2018-04-17 21:19:44 -07:00
|
|
|
constructor(message: string) {
|
2018-04-23 15:09:36 -07:00
|
|
|
super(message, 'FORBIDDEN');
|
2018-05-20 02:06:16 -07:00
|
|
|
|
|
|
|
// Set the prototype explicitly.
|
|
|
|
// https://stackoverflow.com/a/41102306
|
|
|
|
Object.setPrototypeOf(this, ForbiddenError.prototype);
|
|
|
|
Object.defineProperty(this, 'name', { value: 'ForbiddenError' });
|
2018-04-17 21:19:44 -07:00
|
|
|
}
|
|
|
|
}
|
2018-05-02 16:40:03 -07:00
|
|
|
|
|
|
|
export function formatApolloErrors(
|
|
|
|
errors: Array<Error>,
|
|
|
|
options?: {
|
|
|
|
formatter?: Function;
|
|
|
|
logFunction?: LogFunction;
|
|
|
|
debug?: boolean;
|
|
|
|
},
|
2018-05-16 14:46:33 -07:00
|
|
|
): Array<ApolloError> {
|
2018-05-16 15:57:51 -07:00
|
|
|
if (!options) {
|
|
|
|
return errors.map(error => enrichError(error));
|
|
|
|
}
|
2018-05-02 16:40:03 -07:00
|
|
|
const { formatter, debug, logFunction } = options;
|
2018-05-08 11:02:21 -07:00
|
|
|
|
|
|
|
const enrichedErrors = errors.map(error => enrichError(error, debug));
|
|
|
|
|
|
|
|
if (!formatter) {
|
|
|
|
return enrichedErrors;
|
|
|
|
}
|
|
|
|
|
|
|
|
return enrichedErrors.map(error => {
|
|
|
|
try {
|
|
|
|
return formatter(error);
|
|
|
|
} catch (err) {
|
|
|
|
logFunction({
|
|
|
|
action: LogAction.cleanup,
|
|
|
|
step: LogStep.status,
|
|
|
|
data: err,
|
|
|
|
key: 'error',
|
|
|
|
});
|
|
|
|
|
|
|
|
if (debug) {
|
|
|
|
return enrichError(err, debug);
|
|
|
|
} else {
|
|
|
|
//obscure error
|
2018-05-21 15:41:36 -07:00
|
|
|
const newError = fromGraphQLError(
|
2018-05-08 11:02:21 -07:00
|
|
|
new GraphQLError('Internal server error'),
|
|
|
|
);
|
|
|
|
return enrichError(newError, debug);
|
2018-05-02 16:40:03 -07:00
|
|
|
}
|
|
|
|
}
|
2018-05-16 14:46:33 -07:00
|
|
|
}) as Array<ApolloError>;
|
2018-05-02 16:40:03 -07:00
|
|
|
}
|