Comparing version 1.0.13 to 1.1.1
import ExtendableError from 'es6-error'; | ||
import { IBaseError, SerializedError, SerializedErrorSafe } from '../interfaces'; | ||
import { DeserializeOpts, IBaseError, SerializedError, SerializedErrorSafe } from '../interfaces'; | ||
/** | ||
@@ -125,2 +125,17 @@ * Improved error class. | ||
toJSONSafe(fieldsToOmit?: string[]): Partial<SerializedErrorSafe>; | ||
/** | ||
* Helper method for use with fromJson() | ||
* @param errInstance An error instance that extends BaseError | ||
* @param {string} data JSON.parse()'d error object from | ||
* BaseError#toJSON() or BaseError#toJSONSafe() | ||
* @param {DeserializeOpts} [opts] Deserialization options | ||
*/ | ||
static copyDeserializationData<T extends IBaseError = IBaseError, U extends DeserializeOpts = DeserializeOpts>(errInstance: T, data: Partial<SerializedError>, opts: U): void; | ||
/** | ||
* Deserializes an error into an instance | ||
* @param {string} data JSON.parse()'d error object from | ||
* BaseError#toJSON() or BaseError#toJSONSafe() | ||
* @param {DeserializeOpts} [opts] Deserialization options | ||
*/ | ||
static fromJSON<T extends DeserializeOpts = DeserializeOpts>(data: Partial<SerializedError>, opts?: T): IBaseError; | ||
} |
@@ -184,2 +184,3 @@ "use strict"; | ||
statusCode: this._statusCode, | ||
logLevel: this._logLevel, | ||
meta: { | ||
@@ -229,4 +230,71 @@ ...this._metadata, | ||
} | ||
/** | ||
* Helper method for use with fromJson() | ||
* @param errInstance An error instance that extends BaseError | ||
* @param {string} data JSON.parse()'d error object from | ||
* BaseError#toJSON() or BaseError#toJSONSafe() | ||
* @param {DeserializeOpts} [opts] Deserialization options | ||
*/ | ||
static copyDeserializationData(errInstance, data, opts) { | ||
if (data.code) { | ||
errInstance.withErrorCode(data.code); | ||
} | ||
if (data.subCode) { | ||
errInstance.withErrorSubCode(data.subCode); | ||
} | ||
if (data.errId) { | ||
errInstance.withErrorId(data.errId); | ||
} | ||
if (data.statusCode) { | ||
errInstance.withStatusCode(data.statusCode); | ||
} | ||
if (data.stack) { | ||
errInstance.stack = data.stack; | ||
} | ||
if (data.logLevel) { | ||
errInstance.withLogLevel(data.logLevel); | ||
} | ||
// not possible to know what the underlying causedBy type is | ||
// so we can't deserialize to its original representation | ||
if (data.causedBy) { | ||
errInstance.causedBy(data.causedBy); | ||
} | ||
// if defined, pluck the metadata fields to their respective safe and unsafe counterparts | ||
if (data.meta && opts && opts.safeMetadataFields) { | ||
Object.keys(data.meta).forEach(key => { | ||
if (opts.safeMetadataFields[key]) { | ||
errInstance.withSafeMetadata({ | ||
[key]: data.meta[key] | ||
}); | ||
} | ||
else { | ||
errInstance.withMetadata({ | ||
[key]: data.meta[key] | ||
}); | ||
} | ||
}); | ||
} | ||
else { | ||
errInstance.withMetadata(data.meta); | ||
} | ||
} | ||
/** | ||
* Deserializes an error into an instance | ||
* @param {string} data JSON.parse()'d error object from | ||
* BaseError#toJSON() or BaseError#toJSONSafe() | ||
* @param {DeserializeOpts} [opts] Deserialization options | ||
*/ | ||
static fromJSON(data, opts) { | ||
if (!opts) { | ||
opts = {}; | ||
} | ||
if (typeof data !== 'object') { | ||
throw new Error(`fromJSON(): Data is not an object.`); | ||
} | ||
let err = new this(data.message); | ||
this.copyDeserializationData(err, data, opts); | ||
return err; | ||
} | ||
} | ||
exports.BaseError = BaseError; | ||
//# sourceMappingURL=BaseError.js.map |
@@ -1,2 +0,2 @@ | ||
import { BaseError } from '../index'; | ||
import { BaseError, DeserializeOpts, IBaseError, SerializedError } from '../index'; | ||
import { HighLevelError, LowLevelErrorInternal } from '../interfaces'; | ||
@@ -8,2 +8,9 @@ /** | ||
constructor(highLevelErrorDef: HighLevelError, lowLevelErrorDef: LowLevelErrorInternal); | ||
/** | ||
* Deserializes an error into an instance | ||
* @param {string} data JSON.parse()'d error object from | ||
* BaseError#toJSON() or BaseError#toJSONSafe() | ||
* @param {DeserializeOpts} [opts] Deserialization options | ||
*/ | ||
static fromJSON<T extends DeserializeOpts = DeserializeOpts>(data: Partial<SerializedError>, opts?: T): IBaseError; | ||
} |
@@ -31,4 +31,25 @@ "use strict"; | ||
} | ||
/** | ||
* Deserializes an error into an instance | ||
* @param {string} data JSON.parse()'d error object from | ||
* BaseError#toJSON() or BaseError#toJSONSafe() | ||
* @param {DeserializeOpts} [opts] Deserialization options | ||
*/ | ||
static fromJSON(data, opts) { | ||
if (!opts) { | ||
opts = {}; | ||
} | ||
if (typeof data !== 'object') { | ||
throw new Error(`fromJSON(): Data is not an object.`); | ||
} | ||
let err = new this({ | ||
code: data.code | ||
}, { | ||
message: data.message | ||
}); | ||
this.copyDeserializationData(err, data, opts); | ||
return err; | ||
} | ||
} | ||
exports.BaseRegistryError = BaseRegistryError; | ||
//# sourceMappingURL=BaseRegistryError.js.map |
@@ -1,2 +0,2 @@ | ||
import { HighLevelErrorInternal, LowLevelErrorInternal } from './interfaces'; | ||
import { DeserializeOpts, HighLevelErrorInternal, IBaseError, LowLevelErrorInternal, SerializedError } from './interfaces'; | ||
import { BaseRegistryError } from './error-types/BaseRegistryError'; | ||
@@ -13,2 +13,7 @@ /** | ||
/** | ||
* A map of high level names to class name | ||
* @protected | ||
*/ | ||
protected classNameHighLevelNameMap: Record<keyof HLError, string>; | ||
/** | ||
* Cached high level error classes | ||
@@ -55,2 +60,9 @@ */ | ||
newError(highLvErrName: keyof HLError, lowLvErrName: keyof LLError): BaseRegistryError; | ||
/** | ||
* Deserializes data into an error | ||
* @param {string} data JSON.parse()'d error object from | ||
* BaseError#toJSON() or BaseError#toJSONSafe() | ||
* @param {DeserializeOpts} [opts] Deserialization options | ||
*/ | ||
fromJSON<T extends IBaseError = IBaseError, U extends DeserializeOpts = DeserializeOpts>(data: Partial<SerializedError>, opts?: U): T; | ||
} |
@@ -5,2 +5,3 @@ "use strict"; | ||
const BaseRegistryError_1 = require("./error-types/BaseRegistryError"); | ||
const BaseError_1 = require("./error-types/BaseError"); | ||
/** | ||
@@ -14,3 +15,7 @@ * Contains the definitions for High and Low Level Errors and | ||
this.lowLevelErrors = {}; | ||
this.classNameHighLevelNameMap = {}; | ||
this.highLevelErrorClasses = {}; | ||
Object.keys(highLvErrors).forEach(name => { | ||
this.classNameHighLevelNameMap[highLvErrors[name].className] = name; | ||
}); | ||
// populate the lowLevelErrors dictionary | ||
@@ -87,4 +92,30 @@ Object.keys(lowLvErrors).forEach(type => { | ||
} | ||
/** | ||
* Deserializes data into an error | ||
* @param {string} data JSON.parse()'d error object from | ||
* BaseError#toJSON() or BaseError#toJSONSafe() | ||
* @param {DeserializeOpts} [opts] Deserialization options | ||
*/ | ||
fromJSON(data, opts) { | ||
if (typeof data !== 'object') { | ||
throw new Error(`fromJSON(): Data is not an object.`); | ||
} | ||
// data.name is the class name - we need to resolve it to the name of the high level class definition | ||
const errorName = this.classNameHighLevelNameMap[data.name]; | ||
// use the lookup results to see if we can get the class definition of the high level error | ||
const highLevelDef = this.getHighLevelError(errorName); | ||
let err = null; | ||
// Can deserialize into an custom error instance class | ||
if (highLevelDef) { | ||
// get the class for the error type | ||
const C = this.getClass(errorName); | ||
err = C.fromJSON(data, opts); | ||
} | ||
else { | ||
err = BaseError_1.BaseError.fromJSON(data, opts); | ||
} | ||
return err; | ||
} | ||
} | ||
exports.ErrorRegistry = ErrorRegistry; | ||
//# sourceMappingURL=ErrorRegistry.js.map |
import { BaseError } from './error-types/BaseError'; | ||
import { BaseRegistryError } from './error-types/BaseRegistryError'; | ||
import { ErrorRegistry } from './ErrorRegistry'; | ||
import { IBaseError, SerializedError, SerializedErrorSafe, HighLevelError, LowLevelError } from './interfaces'; | ||
export { BaseError, BaseRegistryError, ErrorRegistry, IBaseError, HighLevelError, LowLevelError, SerializedError, SerializedErrorSafe }; | ||
import { IBaseError, SerializedError, SerializedErrorSafe, HighLevelError, LowLevelError, DeserializeOpts } from './interfaces'; | ||
export { BaseError, BaseRegistryError, ErrorRegistry, IBaseError, HighLevelError, LowLevelError, SerializedError, SerializedErrorSafe, DeserializeOpts }; |
@@ -1,4 +0,1 @@ | ||
/** | ||
* A High Level Error definition defined by the user | ||
*/ | ||
export interface HighLevelError { | ||
@@ -156,2 +153,7 @@ /** | ||
/** | ||
* Set a protocol-specific status code | ||
* @param statusCode | ||
*/ | ||
withStatusCode(statusCode: any): this; | ||
/** | ||
* Replaces printf flags in an error message, if present. | ||
@@ -175,3 +177,3 @@ * @see https://www.npmjs.com/package/sprintf-js | ||
*/ | ||
toJSON(fieldsToOmit: string[]): Partial<SerializedError>; | ||
toJSON(fieldsToOmit?: string[]): Partial<SerializedError>; | ||
/** | ||
@@ -182,3 +184,7 @@ * Returns a safe json representation of the error (error stack / causedBy is removed). | ||
*/ | ||
toJSONSafe(fieldsToOmit: string[]): Partial<SerializedErrorSafe>; | ||
toJSONSafe(fieldsToOmit?: string[]): Partial<SerializedErrorSafe>; | ||
/** | ||
* Stack trace | ||
*/ | ||
stack?: any; | ||
} | ||
@@ -191,2 +197,6 @@ /** | ||
/** | ||
* The error id | ||
*/ | ||
errId?: string; | ||
/** | ||
* The high level code to show to a client. | ||
@@ -204,2 +214,6 @@ */ | ||
/** | ||
* Assigned log level | ||
*/ | ||
logLevel?: string | number; | ||
/** | ||
* User-defined metadata | ||
@@ -234,1 +248,7 @@ */ | ||
} | ||
export interface DeserializeOpts { | ||
/** | ||
* Fields from meta to pluck as a safe metadata field | ||
*/ | ||
safeMetadataFields?: Record<string, true>; | ||
} |
@@ -0,1 +1,9 @@ | ||
## 1.1.1 - Mon Sep 21 2020 03:57:31 | ||
**Contributor:** Theo Gravity | ||
- Add deserialization support (#7) | ||
Please read the README section on the limitations and security issues relating to deserialization. | ||
## 1.0.13 - Sat Jun 20 2020 03:22:14 | ||
@@ -5,5 +13,9 @@ | ||
- Update README.md [skip ci] | ||
- Add support for defining log levels | ||
This adds the `logLevel` property to the error definitions along with | ||
corresponding `getLogLevel()` and `withLogLevel()` methods. | ||
Add another example for log level. | ||
There are cases where certain errors do not warrant being logged | ||
under an `error` log level when combined with a logging system. | ||
@@ -10,0 +22,0 @@ ## 1.0.12 - Wed Jun 03 2020 03:54:55 |
{ | ||
"name": "new-error", | ||
"version": "1.0.13", | ||
"version": "1.1.1", | ||
"description": "A production-grade error creation and serialization library designed for Typescript", | ||
@@ -68,21 +68,21 @@ "main": "build/index.js", | ||
"@theo.gravity/changelog-version": "2.1.10", | ||
"@theo.gravity/version-bump": "2.0.9", | ||
"@types/jest": "25.2.2", | ||
"@types/node": "^14.0.1", | ||
"@typescript-eslint/eslint-plugin": "^2.33.0", | ||
"@typescript-eslint/parser": "^2.33.0", | ||
"eslint": "7.0.0", | ||
"@theo.gravity/version-bump": "2.0.10", | ||
"@types/jest": "26.0.14", | ||
"@types/node": "^14.11.1", | ||
"@typescript-eslint/eslint-plugin": "^4.1.1", | ||
"@typescript-eslint/parser": "^4.1.1", | ||
"eslint": "7.9.0", | ||
"git-commit-stamper": "^1.0.9", | ||
"jest": "^25.5.4", | ||
"jest-cli": "26.0.1", | ||
"jest": "^26.4.2", | ||
"jest-cli": "26.4.2", | ||
"jest-junit-reporter": "1.1.0", | ||
"lint-staged": "10.2.2", | ||
"lint-staged": "10.4.0", | ||
"pre-commit": "1.2.2", | ||
"prettier-standard": "16.3.0", | ||
"prettier-standard": "16.4.1", | ||
"standardx": "^5.0.0", | ||
"toc-md-alt": "^0.3.2", | ||
"ts-jest": "25.5.1", | ||
"ts-node": "8.10.1", | ||
"ts-node-dev": "1.0.0-pre.44", | ||
"typescript": "3.9.2" | ||
"toc-md-alt": "^0.4.1", | ||
"ts-jest": "26.4.0", | ||
"ts-node": "9.0.0", | ||
"ts-node-dev": "1.0.0-pre.62", | ||
"typescript": "4.0.3" | ||
}, | ||
@@ -89,0 +89,0 @@ "eslintConfig": { |
169
README.md
@@ -25,2 +25,3 @@ # new-error | ||
<!-- TOC --> | ||
- [Motivation / Error handling use-cases](#motivation--error-handling-use-cases) | ||
@@ -44,2 +45,3 @@ - [Installation](#installation) | ||
- [Basic setters](#basic-setters) | ||
- [Static methods](#static-methods) | ||
- [Set an error id](#set-an-error-id) | ||
@@ -54,2 +56,9 @@ - [Attaching errors](#attaching-errors) | ||
- [Internal serialization](#internal-serialization) | ||
- [Deserialization](#deserialization) | ||
- [Issues with deserialization](#issues-with-deserialization) | ||
- [Deserialization is not perfect](#deserialization-is-not-perfect) | ||
- [potential security issues with deserialization:](#potential-security-issues-with-deserialization) | ||
- [`ErrorRegistry#fromJSON()` method](#errorregistryfromjson-method) | ||
- [`static BaseError#fromJSON()` method](#static-baseerrorfromjson-method) | ||
- [Stand-alone instance-based deserialization](#stand-alone-instance-based-deserialization) | ||
@@ -468,2 +477,6 @@ <!-- TOC END --> | ||
## Static methods | ||
- `static BaseError#fromJSON(data: object, options?: object): BaseError` | ||
## Set an error id | ||
@@ -579,2 +592,3 @@ | ||
- Omits `type` | ||
- Omits `logLevel` | ||
- Omits the stack trace | ||
@@ -585,3 +599,2 @@ - Omits any data defined via `BaseError#withMetadata()` | ||
err.withSafeMetadata({ | ||
errorId: 'err-12345', | ||
requestId: 'req-12345' | ||
@@ -601,3 +614,3 @@ }) | ||
statusCode: 500, | ||
meta: { errorId: 'err-12345', requestId: 'req-12345' } | ||
meta: { requestId: 'req-12345' } | ||
} | ||
@@ -621,3 +634,3 @@ ``` | ||
err.withSafeMetadata({ | ||
errorId: 'err-12345', | ||
reqId: 'req-12345', | ||
}).withMetadata({ | ||
@@ -655,1 +668,151 @@ email: 'test@test.com' | ||
``` | ||
# Deserialization | ||
## Issues with deserialization | ||
### Deserialization is not perfect | ||
- The serialized output may or may not include the `name` property (if using `toJSONSafe()`) that would be able to | ||
hydrate it back into a specific error instance. | ||
- The metadata is squashed in the serialized output that information is required to separate them. | ||
- It is difficult to determine the original type / structure of the `causedBy` data. As a result, it will be copied as-is. | ||
### potential security issues with deserialization: | ||
- You need to be able to trust the data you're deserializing as the serialized data can be modified in various ways by | ||
an untrusted party. | ||
- The deserialization implementation does not perform `JSON.parse()` as `JSON.parse()` in its raw form is susceptible to | ||
[prototype pollution](https://medium.com/intrinsic/javascript-prototype-poisoning-vulnerabilities-in-the-wild-7bc15347c96) | ||
if the parse function does not have a proper sanitization function. It is up to the developer to properly | ||
trust / sanitize / parse the data. | ||
## `ErrorRegistry#fromJSON()` method | ||
This method will attempt to deserialize into a registered error type. If it is unable to, a `BaseError` instance is | ||
returned instead. | ||
`ErrorRegistry#fromJSON(data: object, [options]: DeserializeOpts): IBaseError` | ||
- `data`: Data that is the output of `BaseError#toJSON()`. The data must be an object, not a string. | ||
- `options`: Optional deserialization options. | ||
```typescript | ||
interface DeserializeOpts { | ||
/** | ||
* Fields from meta to pluck as a safe metadata field | ||
*/ | ||
safeMetadataFields?: { | ||
// the value must be set to true. | ||
[key: string]: true | ||
} | ||
} | ||
``` | ||
Returns a `BaseError` instance or an instance of a registered error type. | ||
```typescript | ||
import { ErrorRegistry } from 'new-error' | ||
const errors = { | ||
INTERNAL_SERVER_ERROR: { | ||
className: 'InternalServerError', | ||
code: 'ERR_INT_500', | ||
statusCode: 500, | ||
logLevel: 'error' | ||
} | ||
} | ||
const errorCodes = { | ||
DATABASE_FAILURE: { | ||
message: 'There was a database failure, SQL err code %s', | ||
subCode: 'DB_0001', | ||
statusCode: 500, | ||
logLevel: 'error' | ||
} | ||
} | ||
const errRegistry = new ErrorRegistry(errors, errorCodes) | ||
const data = { | ||
'errId': 'err-123', | ||
'code': 'ERR_INT_500', | ||
'subCode': 'DB_0001', | ||
'message': 'test message', | ||
'meta': { 'safeData': 'test454', 'test': 'test123' }, | ||
'name': 'InternalServerError', | ||
'statusCode': 500, | ||
'causedBy': 'test', | ||
'stack': 'abcd' | ||
} | ||
// err should be an instance of InternalServerError | ||
const err = errRegistry.toJSON(data, { | ||
safeMetadataFields: { | ||
safeData: true | ||
} | ||
}) | ||
``` | ||
## `static BaseError#fromJSON()` method | ||
If you are not using the registry, you can deserialize using this method. This also applies to any class that extends | ||
`BaseError`. | ||
`static BaseError#fromJSON(data: object, [options]: DeserializeOpts): IBaseError` | ||
- `data`: Data that is the output of `BaseError#toJSON()`. The data must be an object, not a string. | ||
- `options`: Optional deserialization options. | ||
Returns a `BaseError` instance or an instance of the class that extends it. | ||
```typescript | ||
import { BaseError } from 'new-error' | ||
// assume we have serialized error data | ||
const data = { | ||
code: 'ERR_INT_500', | ||
subCode: 'DB_0001', | ||
statusCode: 500, | ||
errId: 'err-1234', | ||
meta: { requestId: 'req-12345', safeData: '123' } | ||
} | ||
// deserialize | ||
// specify meta field assignment - fields that are not assigned will be assumed as withMetadata() type data | ||
const err = BaseError.fromJSON(data, { | ||
// (optional) Fields to pluck from 'meta' to be sent to BaseError#safeMetadataFields() | ||
// value must be set to 'true' | ||
safeMetadataFields: { | ||
safeData: true | ||
} | ||
}) | ||
``` | ||
## Stand-alone instance-based deserialization | ||
If the `name` property is present in the serialized data if it was serialized with `toJson()`, you can use a switch | ||
to map to an instance: | ||
```typescript | ||
const data = { | ||
// be sure that you trust the source of the deserialized data! | ||
// anyone can modify the 'name' property to whatever | ||
name: 'InternalServerError', | ||
code: 'ERR_INT_500', | ||
subCode: 'DB_0001', | ||
statusCode: 500, | ||
errId: 'err-1234', | ||
meta: { requestId: 'req-12345', safeData: '123' } | ||
} | ||
let err = null | ||
switch (data.name) { | ||
case 'InternalServerError': | ||
// assume InternalServerError extends BaseError | ||
return InternalServerError.fromJSON(data) | ||
default: | ||
return BaseError.fromJSON(data) | ||
} | ||
``` |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
72188
22
952
810