@ix-xs/node-comfort 1.2.0 → 2.1.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.
- package/CHANGELOG.md +340 -0
- package/README.md +137 -548
- package/index.js +80 -35
- package/package.json +161 -33
- package/src/Arr.js +1050 -418
- package/src/Async.js +817 -0
- package/src/Cache.js +433 -0
- package/src/Checker.js +1202 -539
- package/src/Cli.js +1293 -0
- package/src/Color.js +556 -0
- package/src/Crypto.js +1082 -0
- package/src/Emitter.js +356 -0
- package/src/Env.js +689 -0
- package/src/FS.js +1626 -894
- package/src/Func.js +823 -356
- package/src/Http.js +700 -0
- package/src/Id.js +568 -200
- package/src/Logger.js +963 -378
- package/src/Num.js +965 -360
- package/src/Obj.js +1077 -379
- package/src/SQLite.js +1629 -1005
- package/src/Schema.js +2101 -0
- package/src/Str.js +1209 -432
- package/src/Sys.js +654 -0
- package/src/Time.js +1533 -440
- package/src/Utils.js +342 -325
- package/src/errors.js +335 -0
- package/src/internal/shared.js +415 -0
- package/types/index.d.ts +735 -526
- package/types/src/Arr.d.ts +533 -145
- package/types/src/Arr.d.ts.map +1 -1
- package/types/src/Async.d.ts +372 -0
- package/types/src/Async.d.ts.map +1 -0
- package/types/src/Cache.d.ts +221 -0
- package/types/src/Cache.d.ts.map +1 -0
- package/types/src/Checker.d.ts +651 -232
- package/types/src/Checker.d.ts.map +1 -1
- package/types/src/Cli.d.ts +664 -0
- package/types/src/Cli.d.ts.map +1 -0
- package/types/src/Color.d.ts +300 -0
- package/types/src/Color.d.ts.map +1 -0
- package/types/src/Crypto.d.ts +620 -0
- package/types/src/Crypto.d.ts.map +1 -0
- package/types/src/Emitter.d.ts +189 -0
- package/types/src/Emitter.d.ts.map +1 -0
- package/types/src/Env.d.ts +412 -0
- package/types/src/Env.d.ts.map +1 -0
- package/types/src/FS.d.ts +717 -238
- package/types/src/FS.d.ts.map +1 -1
- package/types/src/Func.d.ts +508 -135
- package/types/src/Func.d.ts.map +1 -1
- package/types/src/Http.d.ts +444 -0
- package/types/src/Http.d.ts.map +1 -0
- package/types/src/Id.d.ts +199 -66
- package/types/src/Id.d.ts.map +1 -1
- package/types/src/Logger.d.ts +414 -46
- package/types/src/Logger.d.ts.map +1 -1
- package/types/src/Num.d.ts +514 -137
- package/types/src/Num.d.ts.map +1 -1
- package/types/src/Obj.d.ts +453 -104
- package/types/src/Obj.d.ts.map +1 -1
- package/types/src/SQLite.d.ts +754 -308
- package/types/src/SQLite.d.ts.map +1 -1
- package/types/src/Schema.d.ts +1189 -0
- package/types/src/Schema.d.ts.map +1 -0
- package/types/src/Str.d.ts +617 -166
- package/types/src/Str.d.ts.map +1 -1
- package/types/src/Sys.d.ts +314 -0
- package/types/src/Sys.d.ts.map +1 -0
- package/types/src/Time.d.ts +759 -204
- package/types/src/Time.d.ts.map +1 -1
- package/types/src/Utils.d.ts +158 -102
- package/types/src/Utils.d.ts.map +1 -1
- package/types/src/errors.d.ts +316 -0
- package/types/src/errors.d.ts.map +1 -0
- package/types/src/internal/shared.d.ts +123 -0
- package/types/src/internal/shared.d.ts.map +1 -0
- package/types/index.d.ts.map +0 -1
package/src/errors.js
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The errors node-comfort throws when something goes wrong at run time: a
|
|
5
|
+
* request fails, data is invalid, a command exits badly. They all extend
|
|
6
|
+
* `NodeComfortError` (itself a regular `Error`) and carry a stable `code`,
|
|
7
|
+
* so you can branch on the kind of failure without parsing messages.
|
|
8
|
+
*
|
|
9
|
+
* A mistake in your own code, like a wrong argument, still throws a plain
|
|
10
|
+
* `TypeError` or `RangeError`, as it does everywhere in JavaScript.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* try {
|
|
14
|
+
* await nc.http.get("https://api.example.com/users/42");
|
|
15
|
+
* } catch (error) {
|
|
16
|
+
* if (error instanceof nc.errors.HttpError && error.status === 404) return null;
|
|
17
|
+
* throw error;
|
|
18
|
+
* }
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Options shared by every error constructor.
|
|
23
|
+
* @typedef {object} NodeComfortErrorOptions
|
|
24
|
+
* @property {string} [code] Machine-readable code, like `"ETIMEDOUT"`.
|
|
25
|
+
* @property {unknown} [cause] The error that caused this one, kept as `error.cause`.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Base class of every node-comfort error. `name` is the class name and
|
|
30
|
+
* `code` a stable identifier.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* if (error instanceof nc.errors.NodeComfortError) logger.warn(error.code);
|
|
34
|
+
*/
|
|
35
|
+
class NodeComfortError extends Error {
|
|
36
|
+
/**
|
|
37
|
+
* @param {string} message
|
|
38
|
+
* @param {NodeComfortErrorOptions} [options]
|
|
39
|
+
*/
|
|
40
|
+
constructor(message, options = {}) {
|
|
41
|
+
super(message, options.cause === undefined ? undefined : { cause: options.cause });
|
|
42
|
+
/** The class name, like `"TimeoutError"`. */
|
|
43
|
+
this.name = new.target.name;
|
|
44
|
+
/** Stable, machine-readable code. */
|
|
45
|
+
this.code = options.code ?? "ERR_NODE_COMFORT";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* What `JSON.stringify()` writes for the error: its name, code, message
|
|
50
|
+
* and own fields, without the stack. Handy for API responses and logs.
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* res.status(400).json({ error });
|
|
54
|
+
* // {"error":{"name":"ValidationError","code":"ERR_VALIDATION","message":"...","issues":[...]}}
|
|
55
|
+
*
|
|
56
|
+
* @returns {Record<string, unknown>}
|
|
57
|
+
*/
|
|
58
|
+
toJSON() {
|
|
59
|
+
/** @type {Record<string, unknown>} */
|
|
60
|
+
const out = { name: this.name, code: this.code, message: this.message };
|
|
61
|
+
for (const key of Object.keys(this)) if (!(key in out)) out[key] = /** @type {any} */ (this)[key];
|
|
62
|
+
const cause = /** @type {any} */ (this.cause);
|
|
63
|
+
if (cause !== undefined) out.cause = cause instanceof Error ? { name: cause.name, message: cause.message, code: /** @type {any} */ (cause).code } : cause;
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Makes `instanceof` work when two copies of the package are installed,
|
|
69
|
+
* which would otherwise give two unrelated `HttpError` classes.
|
|
70
|
+
*
|
|
71
|
+
* @override
|
|
72
|
+
* @param {unknown} value
|
|
73
|
+
* @returns {boolean}
|
|
74
|
+
*/
|
|
75
|
+
static [Symbol.hasInstance](value) {
|
|
76
|
+
if (Function.prototype[Symbol.hasInstance].call(this, value)) return true;
|
|
77
|
+
const brand = Symbol.for(`@ix-xs/node-comfort/${this.name}`);
|
|
78
|
+
if (!Object.prototype.hasOwnProperty.call(this.prototype, brand)) return false;
|
|
79
|
+
return value !== null && typeof value === "object" && /** @type {any} */ (value)[brand] === true;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Thrown by `nc.assert()` and `nc.assertType()`.
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* nc.assert(user, "User is required");
|
|
88
|
+
*/
|
|
89
|
+
class AssertionError extends NodeComfortError {
|
|
90
|
+
/**
|
|
91
|
+
* @param {string} [message="Assertion failed"]
|
|
92
|
+
* @param {NodeComfortErrorOptions} [options]
|
|
93
|
+
*/
|
|
94
|
+
constructor(message = "Assertion failed", options = {}) {
|
|
95
|
+
super(message, { ...options, code: options.code ?? "ERR_ASSERTION" });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Thrown when something takes too long: `func.timeout`, `async.poll`, HTTP
|
|
101
|
+
* requests, `emitter.waitFor`...
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* try {
|
|
105
|
+
* await nc.func.timeout(slowTask(), 1000);
|
|
106
|
+
* } catch (error) {
|
|
107
|
+
* if (error instanceof nc.errors.TimeoutError) console.log(`Gave up after ${error.timeout}ms`);
|
|
108
|
+
* }
|
|
109
|
+
*/
|
|
110
|
+
class TimeoutError extends NodeComfortError {
|
|
111
|
+
/**
|
|
112
|
+
* @param {string} [message="Operation timed out"]
|
|
113
|
+
* @param {NodeComfortErrorOptions & { timeout?: number }} [options] Can include the time limit in milliseconds.
|
|
114
|
+
*/
|
|
115
|
+
constructor(message = "Operation timed out", options = {}) {
|
|
116
|
+
super(message, { ...options, code: options.code ?? "ETIMEDOUT" });
|
|
117
|
+
/** The time limit in milliseconds, when known. */
|
|
118
|
+
this.timeout = options.timeout;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Thrown when an operation is cancelled with an `AbortSignal`. Its name is
|
|
124
|
+
* `"AbortError"`, like native abort errors, so existing checks keep working.
|
|
125
|
+
*/
|
|
126
|
+
class AbortError extends NodeComfortError {
|
|
127
|
+
/**
|
|
128
|
+
* @param {string} [message="The operation was aborted"]
|
|
129
|
+
* @param {NodeComfortErrorOptions} [options] Usually `{ cause: signal.reason }`.
|
|
130
|
+
*/
|
|
131
|
+
constructor(message = "The operation was aborted", options = {}) {
|
|
132
|
+
super(message, { ...options, code: options.code ?? "ABORT_ERR" });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* One problem found while validating data.
|
|
138
|
+
* @typedef {object} ValidationIssue
|
|
139
|
+
* @property {Array<string|number>} path Where the problem is, like `["user", "emails", 0]`. Empty for the root value.
|
|
140
|
+
* @property {string} code What went wrong: `"invalid_type"`, `"too_small"`, `"invalid_string"`, `"custom"`...
|
|
141
|
+
* @property {string} message A message you can show to users.
|
|
142
|
+
* @property {string} [expected] The expected type, for type errors.
|
|
143
|
+
* @property {string} [received] The received type, for type errors.
|
|
144
|
+
*/
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Thrown when data doesn't match a schema or when environment variables are
|
|
148
|
+
* invalid. `issues` lists every problem, not just the first one.
|
|
149
|
+
*
|
|
150
|
+
* @example
|
|
151
|
+
* try {
|
|
152
|
+
* User.parse(body);
|
|
153
|
+
* } catch (error) {
|
|
154
|
+
* if (error instanceof nc.errors.ValidationError) return res.status(400).json(error.flatten());
|
|
155
|
+
* throw error;
|
|
156
|
+
* }
|
|
157
|
+
*/
|
|
158
|
+
class ValidationError extends NodeComfortError {
|
|
159
|
+
/**
|
|
160
|
+
* @param {ValidationIssue[]} issues
|
|
161
|
+
* @param {NodeComfortErrorOptions & { message?: string }} [options]
|
|
162
|
+
*/
|
|
163
|
+
constructor(issues, options = {}) {
|
|
164
|
+
super(options.message ?? ValidationError.format(issues), { ...options, code: options.code ?? "ERR_VALIDATION" });
|
|
165
|
+
/** Every problem found, in order. */
|
|
166
|
+
this.issues = issues;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Turns issues into a readable message, one line per issue.
|
|
171
|
+
*
|
|
172
|
+
* @param {ValidationIssue[]} issues
|
|
173
|
+
* @returns {string}
|
|
174
|
+
*/
|
|
175
|
+
static format(issues) {
|
|
176
|
+
if (!issues.length) return "Validation failed";
|
|
177
|
+
const lines = issues.map((issue) => {
|
|
178
|
+
const where = issue.path.length ? issue.path.join(".") : "(root)";
|
|
179
|
+
return ` • ${where}: ${issue.message}`;
|
|
180
|
+
});
|
|
181
|
+
return `Validation failed with ${issues.length} issue${issues.length > 1 ? "s" : ""}:\n${lines.join("\n")}`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Groups messages by path, which is handy for forms.
|
|
186
|
+
*
|
|
187
|
+
* @example
|
|
188
|
+
* error.flatten(); // { email: ["Invalid email address"], age: ["Must be at least 18"] }
|
|
189
|
+
*
|
|
190
|
+
* @returns {Record<string, string[]>} Messages by dotted path (`""` for the root).
|
|
191
|
+
*/
|
|
192
|
+
flatten() {
|
|
193
|
+
/** @type {Record<string, string[]>} */
|
|
194
|
+
const out = {};
|
|
195
|
+
for (const issue of this.issues) {
|
|
196
|
+
const key = issue.path.join(".");
|
|
197
|
+
(out[key] ??= []).push(issue.message);
|
|
198
|
+
}
|
|
199
|
+
return out;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Thrown by `nc.http` when the response status isn't 2xx (unless you pass
|
|
205
|
+
* `throwHttpErrors: false`), and for network failures (`status` 0).
|
|
206
|
+
*
|
|
207
|
+
* @example
|
|
208
|
+
* try {
|
|
209
|
+
* await nc.http.get(url);
|
|
210
|
+
* } catch (error) {
|
|
211
|
+
* if (error instanceof nc.errors.HttpError) console.log(error.status, error.data);
|
|
212
|
+
* }
|
|
213
|
+
*/
|
|
214
|
+
class HttpError extends NodeComfortError {
|
|
215
|
+
/**
|
|
216
|
+
* @param {string} message
|
|
217
|
+
* @param {NodeComfortErrorOptions & { status?: number, statusText?: string, url?: string, method?: string, headers?: Record<string, string>, data?: unknown }} [options]
|
|
218
|
+
*/
|
|
219
|
+
constructor(message, options = {}) {
|
|
220
|
+
super(message, { ...options, code: options.code ?? "ERR_HTTP" });
|
|
221
|
+
/** HTTP status, like `404`. `0` when no response came back. */
|
|
222
|
+
this.status = options.status ?? 0;
|
|
223
|
+
/** HTTP status text, like `"Not Found"`. */
|
|
224
|
+
this.statusText = options.statusText ?? "";
|
|
225
|
+
/** The requested URL. */
|
|
226
|
+
this.url = options.url ?? "";
|
|
227
|
+
/** The HTTP method, like `"GET"`. */
|
|
228
|
+
this.method = options.method ?? "GET";
|
|
229
|
+
/** Response headers, with lower-cased names. */
|
|
230
|
+
this.headers = options.headers ?? {};
|
|
231
|
+
/** The response body: parsed JSON when possible, text otherwise. */
|
|
232
|
+
this.data = options.data;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Thrown by `nc.crypto.verifyJWT()`. The `code` tells you why:
|
|
238
|
+
* `ERR_JWT_MALFORMED`, `ERR_JWT_ALGORITHM`, `ERR_JWT_SIGNATURE`,
|
|
239
|
+
* `ERR_JWT_EXPIRED`, `ERR_JWT_NOT_BEFORE` or `ERR_JWT_CLAIM`.
|
|
240
|
+
*
|
|
241
|
+
* @example
|
|
242
|
+
* try {
|
|
243
|
+
* return nc.crypto.verifyJWT(token, secret);
|
|
244
|
+
* } catch (error) {
|
|
245
|
+
* if (error.code === "ERR_JWT_EXPIRED") return refreshSession();
|
|
246
|
+
* throw error;
|
|
247
|
+
* }
|
|
248
|
+
*/
|
|
249
|
+
class JWTError extends NodeComfortError {
|
|
250
|
+
/**
|
|
251
|
+
* @param {string} message
|
|
252
|
+
* @param {NodeComfortErrorOptions & { expiredAt?: Date }} [options]
|
|
253
|
+
*/
|
|
254
|
+
constructor(message, options = {}) {
|
|
255
|
+
super(message, { ...options, code: options.code ?? "ERR_JWT" });
|
|
256
|
+
/** When the token expired. Only set for `ERR_JWT_EXPIRED`. */
|
|
257
|
+
this.expiredAt = options.expiredAt;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Thrown by `nc.sys.run()` and `nc.sys.exec()` when a command fails, is
|
|
263
|
+
* killed or times out. Its output is kept on the error.
|
|
264
|
+
*
|
|
265
|
+
* @example
|
|
266
|
+
* try {
|
|
267
|
+
* await nc.sys.run("git", ["push"]);
|
|
268
|
+
* } catch (error) {
|
|
269
|
+
* console.error(error.exitCode, error.stderr);
|
|
270
|
+
* }
|
|
271
|
+
*/
|
|
272
|
+
class ProcessError extends NodeComfortError {
|
|
273
|
+
/**
|
|
274
|
+
* @param {string} message
|
|
275
|
+
* @param {NodeComfortErrorOptions & { command?: string, exitCode?: number | null, signal?: string | null, stdout?: string, stderr?: string, timedOut?: boolean }} [options]
|
|
276
|
+
*/
|
|
277
|
+
constructor(message, options = {}) {
|
|
278
|
+
super(message, { ...options, code: options.code ?? (options.timedOut ? "ETIMEDOUT" : "ERR_PROCESS") });
|
|
279
|
+
/** The command that ran. */
|
|
280
|
+
this.command = options.command ?? "";
|
|
281
|
+
/** Exit code, or `null` if a signal killed the process. */
|
|
282
|
+
this.exitCode = options.exitCode ?? null;
|
|
283
|
+
/** The signal that killed the process, like `"SIGTERM"`. */
|
|
284
|
+
this.signal = options.signal ?? null;
|
|
285
|
+
/** Everything written to stdout. */
|
|
286
|
+
this.stdout = options.stdout ?? "";
|
|
287
|
+
/** Everything written to stderr. */
|
|
288
|
+
this.stderr = options.stderr ?? "";
|
|
289
|
+
/** `true` if the process was killed for exceeding its `timeout`. */
|
|
290
|
+
this.timedOut = options.timedOut ?? false;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Thrown by `nc.SQLite` when a statement fails. Check `sqliteCode` to handle
|
|
296
|
+
* specific cases like a duplicate value.
|
|
297
|
+
*
|
|
298
|
+
* @example
|
|
299
|
+
* try {
|
|
300
|
+
* db.insert("users", { email });
|
|
301
|
+
* } catch (error) {
|
|
302
|
+
* if (error.sqliteCode === "SQLITE_CONSTRAINT_UNIQUE") return "Email already taken";
|
|
303
|
+
* throw error;
|
|
304
|
+
* }
|
|
305
|
+
*/
|
|
306
|
+
class SQLiteError extends NodeComfortError {
|
|
307
|
+
/**
|
|
308
|
+
* @param {string} message
|
|
309
|
+
* @param {NodeComfortErrorOptions & { sql?: string, sqliteCode?: string }} [options]
|
|
310
|
+
*/
|
|
311
|
+
constructor(message, options = {}) {
|
|
312
|
+
super(message, { ...options, code: options.code ?? "ERR_SQLITE" });
|
|
313
|
+
/** The SQL that failed, when there is one. */
|
|
314
|
+
this.sql = options.sql;
|
|
315
|
+
/** SQLite's error code, like `"SQLITE_CONSTRAINT_UNIQUE"`, when known. */
|
|
316
|
+
this.sqliteCode = options.sqliteCode;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const _classes = [NodeComfortError, AssertionError, TimeoutError, AbortError, ValidationError, HttpError, JWTError, ProcessError, SQLiteError];
|
|
321
|
+
for (const Class of _classes) {
|
|
322
|
+
Object.defineProperty(Class.prototype, Symbol.for(`@ix-xs/node-comfort/${Class.name}`), { value: true });
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
module.exports = {
|
|
326
|
+
NodeComfortError,
|
|
327
|
+
AssertionError,
|
|
328
|
+
TimeoutError,
|
|
329
|
+
AbortError,
|
|
330
|
+
ValidationError,
|
|
331
|
+
HttpError,
|
|
332
|
+
JWTError,
|
|
333
|
+
ProcessError,
|
|
334
|
+
SQLiteError,
|
|
335
|
+
};
|