@tryghost/errors 0.2.14 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/lib/errors.js +207 -17
  2. package/lib/utils.js +200 -0
  3. package/package.json +7 -8
package/lib/errors.js CHANGED
@@ -1,17 +1,203 @@
1
+ const uuid = require('uuid');
1
2
  const merge = require('lodash/merge');
2
- const each = require('lodash/each');
3
- const util = require('util');
4
- const errors = require('@tryghost/ignition-errors');
3
+ const isString = require('lodash/isString');
4
+ const utils = require('./utils');
5
5
 
6
- class GhostError extends errors.IgnitionError {
6
+ class GhostError extends Error {
7
7
  constructor(options) {
8
8
  options = options || {};
9
- super(options);
10
- this.value = options.value;
9
+
10
+ super();
11
+
12
+ /**
13
+ * defaults
14
+ */
15
+ this.statusCode = 500;
16
+ this.errorType = 'InternalServerError';
17
+ this.level = 'normal';
18
+ this.message = 'The server has encountered an error.';
19
+ this.id = uuid.v1();
20
+
21
+ /**
22
+ * custom overrides
23
+ */
24
+ this.id = options.id || this.id;
25
+ this.statusCode = options.statusCode || this.statusCode;
26
+ this.level = options.level || this.level;
27
+ this.context = options.context || this.context;
28
+ this.help = options.help || this.help;
29
+ this.errorType = this.name = options.errorType || this.errorType;
30
+ this.errorDetails = options.errorDetails;
31
+ // @ts-ignore
32
+ this.code = options.code || null;
33
+ this.property = options.property || null;
34
+ this.redirect = options.redirect || null;
35
+
36
+ this.message = options.message || this.message;
37
+ this.hideStack = options.hideStack;
38
+
39
+ // NOTE: Error to inherit from, override!
40
+ // Nested objects are getting copied over in one piece (can be changed, but not needed right now)
41
+ if (options.err) {
42
+ // CASE: Support err as string (it happens that third party libs return a string instead of an error instance)
43
+ if (isString(options.err)) {
44
+ /* eslint-disable no-restricted-syntax */
45
+ options.err = new Error(options.err);
46
+ /* eslint-enable no-restricted-syntax */
47
+ }
48
+
49
+ Object.getOwnPropertyNames(options.err).forEach((property) => {
50
+ if (['errorType', 'name', 'statusCode', 'message', 'level'].indexOf(property) !== -1) {
51
+ return;
52
+ }
53
+
54
+ // CASE: `code` should put options as priority over err
55
+ if (property === 'code') {
56
+ // @ts-ignore
57
+ this[property] = this[property] || options.err[property];
58
+ return;
59
+ }
60
+
61
+ if (property === 'stack') {
62
+ this[property] += '\n\n' + options.err[property];
63
+ return;
64
+ }
65
+
66
+ this[property] = options.err[property] || this[property];
67
+ });
68
+ }
11
69
  }
12
70
  }
13
71
 
14
72
  const ghostErrors = {
73
+ InternalServerError: class InternalServerError extends GhostError {
74
+ constructor(options) {
75
+ super(merge({
76
+ statusCode: 500,
77
+ level: 'critical',
78
+ errorType: 'InternalServerError',
79
+ message: 'The server has encountered an error.'
80
+ }, options));
81
+ }
82
+ },
83
+ IncorrectUsageError: class IncorrectUsageError extends GhostError {
84
+ constructor(options) {
85
+ super(merge({
86
+ statusCode: 400,
87
+ level: 'critical',
88
+ errorType: 'IncorrectUsageError',
89
+ message: 'We detected a misuse. Please read the stack trace.'
90
+ }, options));
91
+ }
92
+ },
93
+ NotFoundError: class NotFoundError extends GhostError {
94
+ constructor(options) {
95
+ super(merge({
96
+ statusCode: 404,
97
+ errorType: 'NotFoundError',
98
+ message: 'Resource could not be found.'
99
+ }, options));
100
+ }
101
+ },
102
+ BadRequestError: class BadRequestError extends GhostError {
103
+ constructor(options) {
104
+ super(merge({
105
+ statusCode: 400,
106
+ errorType: 'BadRequestError',
107
+ message: 'The request could not be understood.'
108
+ }, options));
109
+ }
110
+ },
111
+ UnauthorizedError: class UnauthorizedError extends GhostError {
112
+ constructor(options) {
113
+ super(merge({
114
+ statusCode: 401,
115
+ errorType: 'UnauthorizedError',
116
+ message: 'You are not authorised to make this request.'
117
+ }, options));
118
+ }
119
+ },
120
+ NoPermissionError: class NoPermissionError extends GhostError {
121
+ constructor(options) {
122
+ super(merge({
123
+ statusCode: 403,
124
+ errorType: 'NoPermissionError',
125
+ message: 'You do not have permission to perform this request.'
126
+ }, options));
127
+ }
128
+ },
129
+ ValidationError: class ValidationError extends GhostError {
130
+ constructor(options) {
131
+ super(merge({
132
+ statusCode: 422,
133
+ errorType: 'ValidationError',
134
+ message: 'The request failed validation.'
135
+ }, options));
136
+ }
137
+ },
138
+ UnsupportedMediaTypeError: class UnsupportedMediaTypeError extends GhostError {
139
+ constructor(options) {
140
+ super(merge({
141
+ statusCode: 415,
142
+ errorType: 'UnsupportedMediaTypeError',
143
+ message: 'The media in the request is not supported by the server.'
144
+ }, options));
145
+ }
146
+ },
147
+ TooManyRequestsError: class TooManyRequestsError extends GhostError {
148
+ constructor(options) {
149
+ super(merge({
150
+ statusCode: 429,
151
+ errorType: 'TooManyRequestsError',
152
+ message: 'Server has received too many similar requests in a short space of time.'
153
+ }, options));
154
+ }
155
+ },
156
+ MaintenanceError: class MaintenanceError extends GhostError {
157
+ constructor(options) {
158
+ super(merge({
159
+ statusCode: 503,
160
+ errorType: 'MaintenanceError',
161
+ message: 'The server is temporarily down for maintenance.'
162
+ }, options));
163
+ }
164
+ },
165
+ MethodNotAllowedError: class MethodNotAllowedError extends GhostError {
166
+ constructor(options) {
167
+ super(merge({
168
+ statusCode: 405,
169
+ errorType: 'MethodNotAllowedError',
170
+ message: 'Method not allowed for resource.'
171
+ }, options));
172
+ }
173
+ },
174
+ RequestEntityTooLargeError: class RequestEntityTooLargeError extends GhostError {
175
+ constructor(options) {
176
+ super(merge({
177
+ statusCode: 413,
178
+ errorType: 'RequestEntityTooLargeError',
179
+ message: 'Request was too big for the server to handle.'
180
+ }, options));
181
+ }
182
+ },
183
+ TokenRevocationError: class TokenRevocationError extends GhostError {
184
+ constructor(options) {
185
+ super(merge({
186
+ statusCode: 503,
187
+ errorType: 'TokenRevocationError',
188
+ message: 'Token is no longer available.'
189
+ }, options));
190
+ }
191
+ },
192
+ VersionMismatchError: class VersionMismatchError extends GhostError {
193
+ constructor(options) {
194
+ super(merge({
195
+ statusCode: 400,
196
+ errorType: 'VersionMismatchError',
197
+ message: 'Requested version does not match server version.'
198
+ }, options));
199
+ }
200
+ },
15
201
  DataExportError: class DataExportError extends GhostError {
16
202
  constructor(options) {
17
203
  super(merge({
@@ -95,17 +281,21 @@ const ghostErrors = {
95
281
  message: 'For security, you need to create a new password. An email has been sent to you with instructions!'
96
282
  }, options));
97
283
  }
284
+ },
285
+ UnhandledJobError: class UnhandledJobError extends GhostError {
286
+ constructor(options) {
287
+ super(merge({
288
+ errorType: 'UnhandledJobError',
289
+ message: 'Processed job threw an unhandled error',
290
+ level: 'critical'
291
+ }, options));
292
+ }
98
293
  }
99
294
  };
100
295
 
101
- // we need to inherit all general errors from GhostError, otherwise we have to check instanceof IgnitionError
102
- each(errors, function (error) {
103
- if (error.name === 'IgnitionError' || typeof error === 'object') {
104
- return;
105
- }
106
-
107
- util.inherits(error, GhostError);
108
- });
109
-
110
- module.exports = merge(ghostErrors, errors);
111
- module.exports.GhostError = GhostError;
296
+ module.exports = ghostErrors;
297
+ module.exports.utils = {
298
+ serialize: utils.serialize.bind(ghostErrors),
299
+ deserialize: utils.deserialize.bind(ghostErrors),
300
+ isGhostError: utils.isGhostError.bind(ghostErrors)
301
+ };
package/lib/utils.js ADDED
@@ -0,0 +1,200 @@
1
+ const omit = require('lodash/omit');
2
+ const merge = require('lodash/merge');
3
+ const extend = require('lodash/extend');
4
+ const _private = {};
5
+
6
+ _private.serialize = function serialize(err) {
7
+ try {
8
+ return {
9
+ id: err.id,
10
+ status: err.statusCode,
11
+ code: err.code || err.errorType,
12
+ title: err.name,
13
+ detail: err.message,
14
+ meta: {
15
+ context: err.context,
16
+ help: err.help,
17
+ errorDetails: err.errorDetails,
18
+ level: err.level,
19
+ errorType: err.errorType
20
+ }
21
+ };
22
+ } catch (error) {
23
+ return {
24
+ detail: 'Something went wrong.'
25
+ };
26
+ }
27
+ };
28
+
29
+ _private.deserialize = function deserialize(obj) {
30
+ try {
31
+ return {
32
+ id: obj.id,
33
+ message: obj.detail || obj.error_description || obj.message,
34
+ statusCode: obj.status,
35
+ code: obj.code || obj.error,
36
+ level: obj.meta && obj.meta.level,
37
+ help: obj.meta && obj.meta.help,
38
+ context: obj.meta && obj.meta.context
39
+ };
40
+ } catch (err) {
41
+ return {
42
+ message: 'Something went wrong.'
43
+ };
44
+ }
45
+ };
46
+
47
+ /**
48
+ * @description Serialize error instance into oauth format.
49
+ *
50
+ * @see https://tools.ietf.org/html/rfc6749#page-45
51
+ *
52
+ * To not loose any error data when sending errors between internal services, we use the suggested OAuth properties and add ours as well.
53
+ */
54
+ _private.OAuthSerialize = function OAuthSerialize(err) {
55
+ const matchTable = {};
56
+
57
+ matchTable[this.NoPermissionError.name] = 'access_denied';
58
+ matchTable[this.MaintenanceError.name] = 'temporarily_unavailable';
59
+ matchTable[this.BadRequestError.name] = matchTable[this.ValidationError.name] = 'invalid_request';
60
+ matchTable.default = 'server_error';
61
+
62
+ return merge({
63
+ error: err.code || matchTable[err.name] || 'server_error',
64
+ error_description: err.message
65
+ }, omit(_private.serialize(err), ['detail', 'code']));
66
+ };
67
+
68
+ /**
69
+ * @description Deserialize oauth error format into GhostError instance.
70
+ * @param {Object} errorFormat
71
+ * @return {Error}
72
+ * @constructor
73
+ */
74
+ _private.OAuthDeserialize = function OAuthDeserialize(errorFormat) {
75
+ try {
76
+ return new this[errorFormat.title || errorFormat.name || this.InternalServerError.name](_private.deserialize(errorFormat));
77
+ } catch (err) {
78
+ // CASE: you receive an OAuth formatted error, but the error prototype is unknown
79
+ return new this.InternalServerError(extend({
80
+ errorType: errorFormat.title || errorFormat.name
81
+ }, _private.deserialize(errorFormat)));
82
+ }
83
+ };
84
+
85
+ /**
86
+ * @description Serialize GhostError instance into jsonapi.org format.
87
+ * @param {Error} err
88
+ * @return {Object}
89
+ */
90
+ _private.JSONAPISerialize = function JSONAPISerialize(err) {
91
+ const errorFormat = {
92
+ errors: [_private.serialize(err)]
93
+ };
94
+
95
+ errorFormat.errors[0].source = {};
96
+
97
+ if (err.property) {
98
+ errorFormat.errors[0].source.pointer = '/data/attributes/' + err.property;
99
+ }
100
+
101
+ return errorFormat;
102
+ };
103
+
104
+ /**
105
+ * @description Deserialize JSON api format into GhostError instance.
106
+ * @param {Object} errorFormat
107
+ * @return {Error}
108
+ */
109
+ _private.JSONAPIDeserialize = function JSONAPIDeserialize(errorFormat) {
110
+ errorFormat = errorFormat.errors && errorFormat.errors[0] || {};
111
+
112
+ let internalError;
113
+
114
+ try {
115
+ internalError = new this[errorFormat.title || errorFormat.name || this.InternalServerError.name](_private.deserialize(errorFormat));
116
+ } catch (err) {
117
+ // CASE: you receive a JSON format error, but the error prototype is unknown
118
+ internalError = new this.InternalServerError(extend({
119
+ errorType: errorFormat.title || errorFormat.name
120
+ }, _private.deserialize(errorFormat)));
121
+ }
122
+
123
+ if (errorFormat.source && errorFormat.source.pointer) {
124
+ internalError.property = errorFormat.source.pointer.split('/')[3];
125
+ }
126
+
127
+ return internalError;
128
+ };
129
+
130
+ /**
131
+ * @description Serialize GhostError instance to error JSON format
132
+ *
133
+ * jsonapi.org error format:
134
+ *
135
+ * source: {
136
+ * parameter: URL query parameter (no support yet)
137
+ * pointer: HTTP body attribute
138
+ * }
139
+ *
140
+ * @see http://jsonapi.org/format/#errors
141
+ *
142
+ * @param {Error} err
143
+ * @param {Object} options { format: [String] (jsonapi || oauth) }
144
+ */
145
+ exports.serialize = function serialize(err, options) {
146
+ options = options || {format: 'jsonapi'};
147
+
148
+ let errorFormat = {};
149
+
150
+ try {
151
+ if (options.format === 'jsonapi') {
152
+ errorFormat = _private.JSONAPISerialize.bind(this)(err);
153
+ } else {
154
+ errorFormat = _private.OAuthSerialize.bind(this)(err);
155
+ }
156
+ } catch (error) {
157
+ errorFormat.message = 'Something went wrong.';
158
+ }
159
+
160
+ // no need to sanitize the undefined values, on response send JSON.stringify get's called
161
+ return errorFormat;
162
+ };
163
+
164
+ /**
165
+ * @description Deserialize from error JSON format to GhostError instance
166
+ * @param {Object} errorFormat
167
+ */
168
+ exports.deserialize = function deserialize(errorFormat) {
169
+ let internalError = {};
170
+
171
+ if (errorFormat.errors) {
172
+ internalError = _private.JSONAPIDeserialize.bind(this)(errorFormat);
173
+ } else {
174
+ internalError = _private.OAuthDeserialize.bind(this)(errorFormat);
175
+ }
176
+
177
+ return internalError;
178
+ };
179
+
180
+ /**
181
+ * @description Check whether an error instance is a GhostError.
182
+ */
183
+ exports.isGhostError = function isGhostError(err) {
184
+ const errorName = this.GhostError.name;
185
+
186
+ const recursiveIsGhostError = function recursiveIsGhostError(obj) {
187
+ // no super constructor available anymore
188
+ if (!obj || !obj.name) {
189
+ return false;
190
+ }
191
+
192
+ if (obj.name === errorName) {
193
+ return true;
194
+ }
195
+
196
+ return recursiveIsGhostError(Object.getPrototypeOf(obj));
197
+ };
198
+
199
+ return recursiveIsGhostError(err.constructor);
200
+ };
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@tryghost/errors",
3
- "version": "0.2.14",
4
- "repository": "https://github.com/TryGhost/Utils/tree/master/packages/errors",
3
+ "version": "1.0.0",
4
+ "repository": "https://github.com/TryGhost/Utils/tree/main/packages/errors",
5
5
  "author": "Ghost Foundation",
6
6
  "license": "MIT",
7
7
  "main": "index.js",
8
8
  "scripts": {
9
9
  "dev": "echo \"Implement me!\"",
10
- "test": "NODE_ENV=testing c8 mocha './test/**/*.test.js'",
10
+ "test": "NODE_ENV=testing c8 --reporter text --reporter cobertura mocha './test/**/*.test.js'",
11
11
  "lint": "eslint . --ext .js --cache",
12
12
  "posttest": "yarn lint"
13
13
  },
@@ -19,14 +19,13 @@
19
19
  "access": "public"
20
20
  },
21
21
  "devDependencies": {
22
- "c8": "7.9.0",
23
- "mocha": "9.0.0",
22
+ "c8": "7.10.0",
23
+ "mocha": "9.1.3",
24
24
  "should": "13.2.3",
25
- "sinon": "11.0.0"
25
+ "sinon": "11.1.2"
26
26
  },
27
27
  "dependencies": {
28
- "@tryghost/ignition-errors": "^0.1.0",
29
28
  "lodash": "^4.17.21"
30
29
  },
31
- "gitHead": "9545d13a14c1b0ac70cd80ee111f66f642eb0a4d"
30
+ "gitHead": "9cce0db03f9229f7829ad2d0119b4916ca44fc78"
32
31
  }