@compas/stdlib
Version:
All kinds of utility functions
281 lines (252 loc) • 6.89 kB
JavaScript
// @ts-nocheck
import { inspect } from "node:util";
import { isNil, isPlainObject } from "./lodash.js";
/**
* Standard error to use. This contains a key, status code and info object.
* Mostly provided to make it easier to return errors from your API's.
*
* @since 0.1.0
* @class
*/
export class AppError extends Error {
/**
* @param {string} key
* @param {number} status
* @param {Record<string, any>} [info={}]
* @param {unknown} [cause]
*/
constructor(key, status, info, cause) {
let errMessage = info?.message ?? info?.type ?? "";
if (typeof errMessage !== "string") {
errMessage = "";
}
if (errMessage) {
errMessage = `: ${errMessage}`;
}
super(`AppError: ${key}${errMessage}`);
this.key = key;
this.status = status;
this.info = info || {};
/** @type {Error} */
// @ts-expect-error force cast
this.cause = cause;
Object.setPrototypeOf(this, AppError.prototype);
if (typeof status !== "number" || typeof key !== "string") {
return AppError.serverError(
{
appErrorConstructParams: {
key,
status,
},
},
this,
);
}
}
/**
* @this {void}
* @param {*} value
* @returns {value is AppError}
*/
static instanceOf(value) {
return (
value &&
typeof value.key === "string" &&
typeof value.status === "number" &&
!!value.info
);
}
/**
* @this {void}
* @param {Record<string, any>} [info={}]
* @param {unknown} [error]
* @returns {AppError}
*/
static notFound(info = {}, error = undefined) {
return new AppError("error.server.notFound", 404, info, error);
}
/**
* @this {void}
* @param {Record<string, any>} [info={}]
* @param {unknown} [error]
* @returns {AppError}
*/
static notImplemented(info = {}, error = undefined) {
return new AppError("error.server.notImplemented", 405, info, error);
}
/**
* @this {void}
* @param {Record<string, any>} [info={}]
* @param {unknown} [error]
* @returns {AppError}
*/
static serverError(info = {}, error = undefined) {
return new AppError("error.server.internal", 500, info, error);
}
/**
* @this {void}
* @param {string} key
* @param {Record<string, any>} [info={}]
* @param {unknown} [error]
* @returns {AppError}
*/
static validationError(key, info = {}, error = undefined) {
return new AppError(key, 400, info, error);
}
/**
* Format any error skipping the stack automatically for nested errors
*
* @this {void}
* @param {AppError | Error | undefined | null | {} | string | number | boolean |
* Function | unknown} [e]
* @returns {Record<string, any>}
*/
static format(e) {
if (isNil(e)) {
return {
warning: "Missing error",
};
}
const typeOf = typeof e;
if (typeOf === "symbol") {
return {
warning: "Can't serialize Symbol",
};
}
if (typeOf === "bigint") {
return {
warning: "Can't serialize BigInt",
};
}
if (typeOf === "string" || typeOf === "boolean" || typeOf === "number") {
return {
value: e,
};
}
if (typeOf === "function") {
return {
type: "function",
name: e.name,
parameterLength: e.length,
};
}
let stack;
if (typeof (e?.stack ?? "") === "string") {
stack = (e?.stack ?? "").split("\n").map((it) => it.trim());
// Remove first item as this is the Error name
stack.shift();
} else if (Array.isArray(e?.stack)) {
stack = e?.stack;
}
if (isNil(e)) {
return e;
} else if (AppError.instanceOf(e)) {
if (Array.isArray(e.stack)) {
// Already formatted error
return e;
}
return {
key: e.key,
status: e.status,
info: e.info,
stack,
cause: e.cause ? AppError.format(e.cause) : undefined,
};
} else if (e.name === "AggregateError") {
return {
name: e.name,
message: e.message,
stack,
cause: e.errors?.map((it) => AppError.format(it)),
};
} else if (e.name === "PostgresError") {
return {
name: e.name,
message: e.message,
postgres: {
severity: e?.severity,
code: e?.code,
position: e?.position,
routine: e?.routine,
severity_local: e?.severity_local,
file: e?.file,
line: e?.line,
detail: e?.detail,
hint: e?.hint,
internal_position: e?.internal_position,
internal_query: e?.internal_query,
where: e?.where,
schema_name: e?.schema_name,
table_name: e?.table_name,
column_name: e?.column_name,
data: e?.data,
type_name: e?.type_name,
constraint_name: e?.constraint_name,
query: e?.query,
parameters: e?.parameters,
},
stack,
};
} else if (e.isAxiosError) {
// Dumping is most of the time not what the user expects, so prevent these from
// being added to the result.
const body =
(
typeof e.response?.data?.pipe === "function" && // @ts-ignore
typeof e.response?.data?._read === "function"
) ?
{
message:
"Response was a stream, which can not be serialized by AppError#format. Use a try-catch and 'streamToBuffer(e.response?.data)' to get the provided response.",
}
: e.response?.data;
return {
name: e.name,
message: e.message,
axios: {
request: {
path: e.request?.path,
method: e.request?.method,
baseUrl: e.request?.baseURL,
},
response: {
status: e.response?.status,
body,
},
},
stack,
};
} else if (typeof e.toJSON === "function") {
const result = e.toJSON();
result.stack = stack;
return result;
} else if (isPlainObject(e)) {
if (isNil(e.stack)) {
e.stack = stack;
}
return e;
}
// Any unhandled case
return {
name: e.name,
message: e.message,
stack,
cause: e.cause ? AppError.format(e.cause) : undefined,
};
}
/**
* Use AppError#format when AppError is passed to console.log / console.error.
* This works because it uses `util.inspect` under the hood.
* Util#inspect checks if the Symbol `util.inspect.custom` is available.
*/
[inspect.custom]() {
return AppError.format(this);
}
/**
* Use AppError#format when AppError is passed to JSON.stringify().
* This is used in the compas insight logger in production mode.
*/
toJSON() {
return AppError.format(this);
}
}