@ix-xs/node-comfort 1.1.31 → 2.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.
- package/CHANGELOG.md +190 -0
- package/LICENSE +21 -0
- package/README.md +210 -748
- package/index.js +84 -13
- package/package.json +201 -21
- package/src/Arr.js +901 -0
- package/src/Async.js +585 -0
- package/src/Cache.js +329 -0
- package/src/Checker.js +979 -188
- package/src/Cli.js +1034 -0
- package/src/Color.js +415 -0
- package/src/Crypto.js +707 -0
- package/src/Emitter.js +289 -0
- package/src/Env.js +555 -0
- package/src/FS.js +1483 -768
- package/src/Func.js +692 -0
- package/src/Http.js +538 -0
- package/src/Id.js +418 -0
- package/src/Logger.js +739 -307
- package/src/Num.js +849 -0
- package/src/Obj.js +904 -0
- package/src/SQLite.js +1308 -994
- package/src/Schema.js +1586 -0
- package/src/Str.js +999 -0
- package/src/Sys.js +511 -0
- package/src/Time.js +1323 -0
- package/src/Utils.js +321 -325
- package/src/errors.js +292 -0
- package/types/index.d.ts +693 -120
- package/types/src/Arr.d.ts +562 -0
- package/types/src/Arr.d.ts.map +1 -0
- package/types/src/Async.d.ts +321 -0
- package/types/src/Async.d.ts.map +1 -0
- package/types/src/Cache.d.ts +195 -0
- package/types/src/Cache.d.ts.map +1 -0
- package/types/src/Checker.d.ts +609 -81
- package/types/src/Checker.d.ts.map +1 -1
- package/types/src/Cli.d.ts +582 -0
- package/types/src/Cli.d.ts.map +1 -0
- package/types/src/Color.d.ts +294 -0
- package/types/src/Color.d.ts.map +1 -0
- package/types/src/Crypto.d.ts +534 -0
- package/types/src/Crypto.d.ts.map +1 -0
- package/types/src/Emitter.d.ts +170 -0
- package/types/src/Emitter.d.ts.map +1 -0
- package/types/src/Env.d.ts +382 -0
- package/types/src/Env.d.ts.map +1 -0
- package/types/src/FS.d.ts +709 -201
- package/types/src/FS.d.ts.map +1 -1
- package/types/src/Func.d.ts +455 -0
- package/types/src/Func.d.ts.map +1 -0
- package/types/src/Http.d.ts +374 -0
- package/types/src/Http.d.ts.map +1 -0
- package/types/src/Id.d.ts +200 -0
- package/types/src/Id.d.ts.map +1 -0
- package/types/src/Logger.d.ts +374 -24
- package/types/src/Logger.d.ts.map +1 -1
- package/types/src/Num.d.ts +578 -0
- package/types/src/Num.d.ts.map +1 -0
- package/types/src/Obj.d.ts +475 -0
- package/types/src/Obj.d.ts.map +1 -0
- package/types/src/SQLite.d.ts +647 -305
- package/types/src/SQLite.d.ts.map +1 -1
- package/types/src/Schema.d.ts +940 -0
- package/types/src/Schema.d.ts.map +1 -0
- package/types/src/Str.d.ts +696 -0
- package/types/src/Str.d.ts.map +1 -0
- package/types/src/Sys.d.ts +269 -0
- package/types/src/Sys.d.ts.map +1 -0
- package/types/src/Time.d.ts +664 -0
- package/types/src/Time.d.ts.map +1 -0
- package/types/src/Utils.d.ts +154 -102
- package/types/src/Utils.d.ts.map +1 -1
- package/types/src/errors.d.ts +292 -0
- package/types/src/errors.d.ts.map +1 -0
- package/tsconfig.json +0 -18
- package/types/index.d.ts.map +0 -1
package/README.md
CHANGED
|
@@ -1,748 +1,210 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
![
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
[![
|
|
14
|
-
[![
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
### List and delete
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
```js
|
|
215
|
-
// Folders inside a path
|
|
216
|
-
const dirs = nodeComfort.getFoldersIn("./src", true);
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
// Delete folders matching a filter
|
|
220
|
-
nodeComfort.deleteFoldersIn("./tmp", (folder) =>
|
|
221
|
-
folder.endsWith(".cache"),
|
|
222
|
-
);
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
// Files
|
|
226
|
-
const files = nodeComfort.getFilesIn("./src", true);
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
// Delete files matching a filter
|
|
230
|
-
nodeComfort.deleteFilesIn("./logs", false, (file) =>
|
|
231
|
-
file.endsWith(".log"),
|
|
232
|
-
);
|
|
233
|
-
```
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
### Copy
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
```js
|
|
240
|
-
// Copy one folder
|
|
241
|
-
nodeComfort.copyFolder({
|
|
242
|
-
path: "./templates",
|
|
243
|
-
dest: "./dist/templates",
|
|
244
|
-
recursive: true,
|
|
245
|
-
withFiles: true,
|
|
246
|
-
force: true,
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
// Copy only some folders in a tree
|
|
251
|
-
nodeComfort.copyFoldersIn({
|
|
252
|
-
path: "./src",
|
|
253
|
-
dest: "./dist",
|
|
254
|
-
recursive: true,
|
|
255
|
-
withFiles: false,
|
|
256
|
-
filter: (folder) => folder.endsWith("components"),
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
// Copy files
|
|
261
|
-
nodeComfort.copyFilesIn({
|
|
262
|
-
path: "./src",
|
|
263
|
-
dest: "./dist",
|
|
264
|
-
recursive: true,
|
|
265
|
-
force: true,
|
|
266
|
-
filter: (file) => file.endsWith(".js"),
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
// Copy a single file
|
|
271
|
-
nodeComfort.copyFile({
|
|
272
|
-
path: "./src/index.js",
|
|
273
|
-
dest: "./dist", // or "./dist/index.js"
|
|
274
|
-
force: true,
|
|
275
|
-
});
|
|
276
|
-
```
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
### Move
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
Move helpers are basically copy + delete.
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
```js
|
|
286
|
-
// Move a folder tree
|
|
287
|
-
nodeComfort.moveFolder({
|
|
288
|
-
path: "./build",
|
|
289
|
-
dest: "./dist",
|
|
290
|
-
recursive: true,
|
|
291
|
-
withFiles: true,
|
|
292
|
-
force: true,
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
// Move some folders under a path
|
|
297
|
-
nodeComfort.moveFoldersIn({
|
|
298
|
-
path: "./tmp",
|
|
299
|
-
dest: "./archive",
|
|
300
|
-
recursive: true,
|
|
301
|
-
withFiles: false,
|
|
302
|
-
filter: (folder) => folder.includes("session"),
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
// Move files
|
|
307
|
-
nodeComfort.moveFilesIn({
|
|
308
|
-
path: "./logs",
|
|
309
|
-
dest: "./logs/archive",
|
|
310
|
-
recursive: false,
|
|
311
|
-
force: true,
|
|
312
|
-
filter: (file) => file.endsWith(".log"),
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
// Move a single file
|
|
317
|
-
nodeComfort.moveFile({
|
|
318
|
-
path: "./logs/app.log",
|
|
319
|
-
dest: "./logs/archive/app.log",
|
|
320
|
-
force: true,
|
|
321
|
-
});
|
|
322
|
-
```
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
### Read and watch
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
```js
|
|
329
|
-
// Read a UTF-8 file
|
|
330
|
-
const content = nodeComfort.readFile("./README.md");
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
// Watch a directory
|
|
334
|
-
const watcher = nodeComfort.watch({
|
|
335
|
-
path: "./src",
|
|
336
|
-
recursive: true,
|
|
337
|
-
filter: (event, file) => file.endsWith(".js"),
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
watcher
|
|
342
|
-
.on("change", (file) => {
|
|
343
|
-
nodeComfort.log(`<% cyan File changed: %> ${file}`);
|
|
344
|
-
})
|
|
345
|
-
.on("rename", (file) => {
|
|
346
|
-
nodeComfort.log(`<% yellow File renamed: %> ${file}`);
|
|
347
|
-
});
|
|
348
|
-
```
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
You can pause, resume and stop watching:
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
```js
|
|
355
|
-
watcher.pause();
|
|
356
|
-
// ...
|
|
357
|
-
watcher.resume();
|
|
358
|
-
// ...
|
|
359
|
-
watcher.stop();
|
|
360
|
-
```
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
---
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
## Checker
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
Runtime type-checking helpers. Everything is just a boolean guard that works on any input.
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
```js
|
|
373
|
-
nodeComfort.isArray(); // true[1][2]
|
|
374
|
-
nodeComfort.isArray("nope"); // false
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
nodeComfort.isNumber(42); // true
|
|
378
|
-
nodeComfort.isNumber(NaN); // false
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
nodeComfort.isFunction(() => {}); // true
|
|
382
|
-
nodeComfort.isFunction(class {}); // true
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
nodeComfort.isObject({}); // true
|
|
386
|
-
nodeComfort.isObject([]); // true
|
|
387
|
-
nodeComfort.isObject(null); // false
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
nodeComfort.isBoolean(false); // true
|
|
391
|
-
nodeComfort.isString("hello"); // true
|
|
392
|
-
nodeComfort.isUndefined(undefined); // true
|
|
393
|
-
nodeComfort.isNull(null); // true
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
nodeComfort.isDate(new Date()); // true
|
|
397
|
-
nodeComfort.isMap(new Map()); // true
|
|
398
|
-
nodeComfort.isSet(new Set()); // true
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
nodeComfort.isError(new Error("x")); // true
|
|
402
|
-
nodeComfort.isError({ message: "x" }); // false
|
|
403
|
-
```
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
---
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
## Utils
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
Small async and process helpers.
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
### wait(ms)
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
```js
|
|
419
|
-
await nodeComfort.wait(500);
|
|
420
|
-
// code here runs after ~500ms
|
|
421
|
-
```
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
### when(predicate, payload?, options?)
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
Polling helper that emits events until a condition is met.
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
```js
|
|
431
|
-
const task = nodeComfort.when(
|
|
432
|
-
() => Math.random() > 0.8,
|
|
433
|
-
{ label: "Lucky" },
|
|
434
|
-
{ interval: 200, max: 3, timeout: 5000 },
|
|
435
|
-
);
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
task
|
|
439
|
-
.on("trigger", (payload) => {
|
|
440
|
-
nodeComfort.log(`<% green Triggered: %> ${payload.label}`);
|
|
441
|
-
})
|
|
442
|
-
.on("timeout", () => {
|
|
443
|
-
nodeComfort.log("<% red Timeout %>");
|
|
444
|
-
})
|
|
445
|
-
.on("error", (err) => {
|
|
446
|
-
nodeComfort.log(`<% red Error in predicate %>\n${err}`);
|
|
447
|
-
})
|
|
448
|
-
.start();
|
|
449
|
-
```
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
- `predicate`: boolean, promise, or function (sync/async) returning a boolean.
|
|
453
|
-
- `payload`: value passed to listeners on `trigger` / `timeout`.
|
|
454
|
-
- `options`:
|
|
455
|
-
- `interval` (ms, default `50`)
|
|
456
|
-
- `timeout` (ms or `null`)
|
|
457
|
-
- `max` (number of triggers or `null` for infinite)
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
### dontCrash()
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
Installs process-level safety nets for errors, exits and signals.
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
```js
|
|
467
|
-
nodeComfort.dontCrash();
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
// Or customize:
|
|
471
|
-
nodeComfort.dontCrash()
|
|
472
|
-
.on("error", (err) => {
|
|
473
|
-
console.error("Global error:", err);
|
|
474
|
-
process.exit(1);
|
|
475
|
-
})
|
|
476
|
-
.on("exit", (code) => {
|
|
477
|
-
nodeComfort.log(`<% gray Process exited with code ${code} %>`);
|
|
478
|
-
})
|
|
479
|
-
.on("sig", (signal) => {
|
|
480
|
-
nodeComfort.log(`<% yellow Caught signal ${signal}, exiting… %>`);
|
|
481
|
-
process.exit(0);
|
|
482
|
-
});
|
|
483
|
-
```
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
Events:
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
- `"error"` – uncaught exceptions + unhandled rejections
|
|
490
|
-
- `"exit"` – `process.on("exit")`
|
|
491
|
-
- `"sig"` – `SIGINT`, `SIGTERM`, `SIGQUIT`
|
|
492
|
-
- `"beforeExit"` – `process.on("beforeExit")`
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
### JSON helpers
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
```js
|
|
499
|
-
const json = nodeComfort.JSONString({ hello: "world" });
|
|
500
|
-
// pretty-printed with 4 spaces
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
const data = nodeComfort.JSONParse(json);
|
|
504
|
-
```
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
---
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
## SQLite
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
Thin wrapper over the built-in `node:sqlite` `DatabaseSync` with a small "model" system and CRUD-style helpers.
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
### Creating an instance
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
```js
|
|
520
|
-
// Default: ./db.sqlite (relative to cwd)
|
|
521
|
-
const db = new nodeComfort.SQLite();
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
// Custom path (folder created if needed)
|
|
525
|
-
const db2 = new nodeComfort.SQLite("data/app.sqlite");
|
|
526
|
-
```
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
The constructor ensures the parent folder exists, and enables WAL journal mode and foreign keys by default.
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
### Defining a table
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
```js
|
|
536
|
-
await db.createTable({
|
|
537
|
-
name: "users",
|
|
538
|
-
columns: {
|
|
539
|
-
id: {
|
|
540
|
-
type: "INTEGER",
|
|
541
|
-
primaryKey: true,
|
|
542
|
-
autoincrement: true,
|
|
543
|
-
},
|
|
544
|
-
name: {
|
|
545
|
-
type: "TEXT",
|
|
546
|
-
notNull: true,
|
|
547
|
-
},
|
|
548
|
-
role: {
|
|
549
|
-
type: "TEXT",
|
|
550
|
-
values: ["admin", "user"],
|
|
551
|
-
defaultValue: "user",
|
|
552
|
-
},
|
|
553
|
-
createdAt: {
|
|
554
|
-
type: "TEXT",
|
|
555
|
-
defaultValue: new Date().toISOString(),
|
|
556
|
-
},
|
|
557
|
-
},
|
|
558
|
-
});
|
|
559
|
-
```
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
Models are kept in memory and used for defaults like ordering.
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
The `columns` property is the new, recommended way. The legacy `options` property is still supported for backward compatibility.
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
### Insert / update / upsert
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
```js
|
|
572
|
-
// Insert a new row
|
|
573
|
-
await db.insert("users", { name: "John" });
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
// Update rows matching a where clause
|
|
577
|
-
await db.update("users", { role: "admin" }, { name: "John" });
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
// Upsert (INSERT with ON CONFLICT DO UPDATE)
|
|
581
|
-
await db.upsert("users", { id: 1, role: "user" }, ["id"]);
|
|
582
|
-
```
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
When `where` or `data.id` is provided, `set` (legacy helper):
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
1. loads the existing row if it exists,
|
|
589
|
-
2. merges it with `data`,
|
|
590
|
-
3. inserts or updates depending on whether the row exists.
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
```js
|
|
594
|
-
// Insert
|
|
595
|
-
await db.set("users", { name: "John" });
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
// Upsert by id
|
|
599
|
-
await db.set("users", { id: 1, role: "admin" });
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
// Upsert with custom where
|
|
603
|
-
await db.set("users", { role: "user" }, { name: "John" });
|
|
604
|
-
```
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
### Get / list
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
```js
|
|
611
|
-
// One row or undefined
|
|
612
|
-
const user = await db.get("users", { name: "John" });
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
// Multiple rows with pagination and ordering
|
|
616
|
-
const admins = await db.getAll(
|
|
617
|
-
"users",
|
|
618
|
-
{ role: "admin" }, // where
|
|
619
|
-
{
|
|
620
|
-
limit: 100,
|
|
621
|
-
offset: 0,
|
|
622
|
-
orderBy: "createdAt",
|
|
623
|
-
direction: "DESC",
|
|
624
|
-
},
|
|
625
|
-
);
|
|
626
|
-
```
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
If the table is not registered, `getAll` returns an empty array.
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
### Delete / clear / count
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
```js
|
|
636
|
-
// Delete rows
|
|
637
|
-
const res = await db.delete("users", { role: "user" });
|
|
638
|
-
// res.ok, res.changes
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
// Delete all rows
|
|
642
|
-
await db.clearTable("users");
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
// Count rows
|
|
646
|
-
const totalUsers = await db.count("users");
|
|
647
|
-
const adminCount = await db.count("users", { role: "admin" });
|
|
648
|
-
```
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
### Indexes and constraints
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
```js
|
|
655
|
-
// Create a composite unique index
|
|
656
|
-
await db.createIndex("UserAuthMethod", {
|
|
657
|
-
columns: ["provider", "providerUserId"],
|
|
658
|
-
unique: true,
|
|
659
|
-
});
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
// Create a non-unique index
|
|
663
|
-
await db.createIndex("Post", {
|
|
664
|
-
columns: ["userId"],
|
|
665
|
-
});
|
|
666
|
-
```
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
```js
|
|
670
|
-
// Table-level constraints
|
|
671
|
-
await db.createTable({
|
|
672
|
-
name: "UserAuthMethod",
|
|
673
|
-
columns: {
|
|
674
|
-
id: { type: "INTEGER", primaryKey: true, autoincrement: true },
|
|
675
|
-
userId: { type: "INTEGER", notNull: true },
|
|
676
|
-
provider: { type: "TEXT", notNull: true },
|
|
677
|
-
providerUserId: { type: "TEXT", notNull: true },
|
|
678
|
-
},
|
|
679
|
-
constraints: [
|
|
680
|
-
{
|
|
681
|
-
type: "unique",
|
|
682
|
-
columns: ["provider", "providerUserId"],
|
|
683
|
-
},
|
|
684
|
-
{
|
|
685
|
-
type: "foreignKey",
|
|
686
|
-
columns: ["userId"],
|
|
687
|
-
references: { table: "User", columns: ["id"] },
|
|
688
|
-
onDelete: "CASCADE",
|
|
689
|
-
},
|
|
690
|
-
],
|
|
691
|
-
});
|
|
692
|
-
```
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
### Transactions
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
```js
|
|
699
|
-
const result = await db.transaction(async () => {
|
|
700
|
-
await db.insert("User", {
|
|
701
|
-
email: "a@b.com",
|
|
702
|
-
createdAt: Date.now(),
|
|
703
|
-
});
|
|
704
|
-
await db.insert("Profile", {
|
|
705
|
-
userId: 1,
|
|
706
|
-
nickname: "John",
|
|
707
|
-
});
|
|
708
|
-
return { ok: true };
|
|
709
|
-
});
|
|
710
|
-
```
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
If the callback throws, the transaction is rolled back.
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
### Raw SQL helpers
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
```js
|
|
720
|
-
// Execute a write statement (INSERT/UPDATE/DELETE/CREATE/PRAGMA)
|
|
721
|
-
await db.exec("CREATE INDEX IF NOT EXISTS idx_email ON users (email)");
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
// Execute a query and get one row
|
|
725
|
-
const user = await db.queryOne("SELECT * FROM users WHERE id = ?", );[1]
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
// Execute a query and get all rows
|
|
729
|
-
const users = await db.queryAll("SELECT * FROM users WHERE role = ?", ["admin"]);
|
|
730
|
-
```
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
### Drop and close
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
```js
|
|
737
|
-
await db.deleteTable("users");
|
|
738
|
-
db.close();
|
|
739
|
-
```
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
---
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
## License
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
MIT.
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# node-comfort
|
|
4
|
+
|
|
5
|
+
**The standard library Node.js deserves.**
|
|
6
|
+
|
|
7
|
+
Logging, config, validation, HTTP, SQLite, dates, crypto, CLI tools and 500+ helpers.<br>
|
|
8
|
+
One package, zero dependencies, documented right in your editor.
|
|
9
|
+
|
|
10
|
+
[](https://www.npmjs.com/package/@ix-xs/node-comfort)
|
|
11
|
+
[](https://www.npmjs.com/package/@ix-xs/node-comfort)
|
|
12
|
+
[](https://github.com/ix-xs/node-comfort/actions/workflows/ci.yml)
|
|
13
|
+
[](https://www.npmjs.com/package/@ix-xs/node-comfort?activeTab=dependencies)
|
|
14
|
+
[](https://nodejs.org)
|
|
15
|
+
[](./LICENSE)
|
|
16
|
+
|
|
17
|
+
[Documentation](https://ix-xs.github.io/node-comfort/) •
|
|
18
|
+
[Getting started](https://ix-xs.github.io/node-comfort/guide/getting-started.html) •
|
|
19
|
+
[API reference](https://ix-xs.github.io/node-comfort/api/index.html) •
|
|
20
|
+
[Recipes](https://ix-xs.github.io/node-comfort/guide/recipes.html) •
|
|
21
|
+
[Changelog](./CHANGELOG.md)
|
|
22
|
+
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
Most Node.js projects install the same twenty packages before writing a single line of their own: a logger, dotenv, a validation library, an HTTP client, a retry helper, a date library, uuid, bcrypt, a CLI parser, a spinner. Hundreds of transitive dependencies come with them.
|
|
28
|
+
|
|
29
|
+
Node.js can do most of this on its own now. It ships `fetch`, `Intl`, `node:crypto` and even SQLite. node-comfort is the missing layer on top: one consistent, fully typed API, and nothing else to install.
|
|
30
|
+
|
|
31
|
+
```js
|
|
32
|
+
const nc = require("@ix-xs/node-comfort");
|
|
33
|
+
|
|
34
|
+
const config = nc.env.validate({
|
|
35
|
+
PORT: { type: "port", default: 3000 },
|
|
36
|
+
DATABASE_URL: { type: "url" },
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const db = new nc.SQLite("./data/app.sqlite");
|
|
40
|
+
const github = nc.http.create({ baseURL: "https://api.github.com/", retry: { attempts: 3 } });
|
|
41
|
+
|
|
42
|
+
const { data: repo } = await github.get("repos/nodejs/node");
|
|
43
|
+
nc.info(`Node.js has <% yellow ${nc.num.abbreviate(repo.stargazers_count)} %> stars`);
|
|
44
|
+
|
|
45
|
+
nc.sys.onShutdown(() => db.close());
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Install
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
npm install @ix-xs/node-comfort
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Node.js 22.13 or newer. That's the first version where `node:sqlite` works without a flag.
|
|
55
|
+
|
|
56
|
+
## Why node-comfort
|
|
57
|
+
|
|
58
|
+
- **Zero dependencies.** Built only on Node's own modules. No install scripts, nothing else to audit or keep up to date.
|
|
59
|
+
- **Your editor already knows it.** Every function, parameter and option is typed and documented, with examples. Type `nc.` and look around. It works in plain JavaScript too.
|
|
60
|
+
- **Fast to load.** Namespaces load the first time you use them. Requiring the package takes about 3 ms, and a script that only logs never loads the database or the HTTP client.
|
|
61
|
+
- **Safe by default.** Prototype-pollution-proof objects, scrypt passwords, AES-256-GCM, constant-time comparisons, parameterized SQL, atomic file writes.
|
|
62
|
+
- **One way of doing things.** Durations are always `"5m"` or milliseconds, options always come last, and every error has a stable `code`.
|
|
63
|
+
- **Tested everywhere.** Linux, macOS and Windows, Node.js 22 and 24, TypeScript 5.9, 6 and 7.
|
|
64
|
+
|
|
65
|
+
## What's inside
|
|
66
|
+
|
|
67
|
+
| Namespace | What it's for | Often replaces |
|
|
68
|
+
| --- | --- | --- |
|
|
69
|
+
| [Logger](https://ix-xs.github.io/node-comfort/guide/logger.html) | Colorful logs, levels, JSON output, log files with rotation | `chalk`, `pino`, `winston` |
|
|
70
|
+
| [`fs`](https://ix-xs.github.io/node-comfort/guide/fs.html) | Files and folders without try/catch, atomic writes, glob, watch | `fs-extra`, `glob`, `rimraf` |
|
|
71
|
+
| [`checker`](https://ix-xs.github.io/node-comfort/guide/checker.html) | 60+ type guards and validators, `assert` | `is`, `validator` |
|
|
72
|
+
| [`env`](https://ix-xs.github.io/node-comfort/guide/env.html) | `.env` loading, typed and validated configuration | `dotenv`, `envalid` |
|
|
73
|
+
| [`schema`](https://ix-xs.github.io/node-comfort/guide/schema.html) | Data validation with TypeScript inference | `zod`, `yup`, `joi` |
|
|
74
|
+
| [`http`](https://ix-xs.github.io/node-comfort/guide/http.html) | `fetch` with JSON, retries, timeouts, hooks, downloads | `axios`, `got`, `ky` |
|
|
75
|
+
| [`SQLite`](https://ix-xs.github.io/node-comfort/guide/sqlite.html) | CRUD, filters, JSON columns, transactions, migrations | `better-sqlite3` and helpers |
|
|
76
|
+
| [`time`](https://ix-xs.github.io/node-comfort/guide/time.html) | Formatting in any language and time zone, durations, date math, cron | `dayjs`, `ms`, `node-cron` |
|
|
77
|
+
| [`str`](https://ix-xs.github.io/node-comfort/guide/str.html) | Case conversion, slugs, emoji-safe truncation, fuzzy matching, plurals | `lodash`, `slugify`, `leven` |
|
|
78
|
+
| [`num`](https://ix-xs.github.io/node-comfort/guide/num.html) | Exact rounding, statistics, currencies, bytes | `numeral`, `pretty-bytes` |
|
|
79
|
+
| [`arr`](https://ix-xs.github.io/node-comfort/guide/arr.html) | Chunk, group, multi-key sort, paginate, set operations | `lodash` |
|
|
80
|
+
| [`obj`](https://ix-xs.github.io/node-comfort/guide/obj.html) | Deep clone, merge, equal and diff, typed dot paths | `lodash`, `deepmerge`, `dequal` |
|
|
81
|
+
| [`func`](https://ix-xs.github.io/node-comfort/guide/func.html) | Debounce, throttle, memoize, retry with backoff, timeout | `p-retry`, `p-timeout` |
|
|
82
|
+
| [`async`](https://ix-xs.github.io/node-comfort/guide/async.html) | Concurrency limits, queues, mutex, polling | `p-limit`, `p-map`, `p-queue` |
|
|
83
|
+
| [`Cache`](https://ix-xs.github.io/node-comfort/guide/cache.html) | LRU cache with TTL, loaders and statistics | `lru-cache` |
|
|
84
|
+
| [`Emitter`](https://ix-xs.github.io/node-comfort/guide/emitter.html) | Typed events, `waitFor`, async iteration | `mitt`, `emittery` |
|
|
85
|
+
| [`id`](https://ix-xs.github.io/node-comfort/guide/id.html) | UUID v4 and v7, ULID, nanoid, tokens, Snowflakes | `uuid`, `ulid`, `nanoid` |
|
|
86
|
+
| [`crypto`](https://ix-xs.github.io/node-comfort/guide/crypto.html) | Passwords, encryption, JWT, TOTP two-factor codes | `bcrypt`, `jsonwebtoken`, `otplib` |
|
|
87
|
+
| [`cli`](https://ix-xs.github.io/node-comfort/guide/cli.html) | Typed arguments, prompts, menus, spinners, progress bars, tables | `commander`, `inquirer`, `ora` |
|
|
88
|
+
| [`color`](https://ix-xs.github.io/node-comfort/guide/color.html) | Chainable terminal colors, truecolor, gradients, links | `chalk`, `picocolors` |
|
|
89
|
+
| [`sys`](https://ix-xs.github.io/node-comfort/guide/sys.html) | Run commands, `which`, graceful shutdown, platform detection | `execa`, `which` |
|
|
90
|
+
| [`utils`](https://ix-xs.github.io/node-comfort/guide/utils.html) | `wait`, safe JSON, crash guard | |
|
|
91
|
+
| [`errors`](https://ix-xs.github.io/node-comfort/guide/errors.html) | Error classes with stable codes | |
|
|
92
|
+
|
|
93
|
+
The logger, `fs`, `checker` and `utils` helpers are also available at the top level: `nc.info()`, `nc.readJSON()`, `nc.isEmail()`, `nc.wait()`.
|
|
94
|
+
|
|
95
|
+
## Importing
|
|
96
|
+
|
|
97
|
+
```js
|
|
98
|
+
// CommonJS
|
|
99
|
+
const nc = require("@ix-xs/node-comfort");
|
|
100
|
+
const { str, isEmail, info } = require("@ix-xs/node-comfort");
|
|
101
|
+
|
|
102
|
+
// ES modules
|
|
103
|
+
import nc from "@ix-xs/node-comfort";
|
|
104
|
+
import { slugify } from "@ix-xs/node-comfort/str"; // loads only what it needs
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Every namespace has its own entry point (`@ix-xs/node-comfort/http`, `/sqlite`, `/schema`...). In an ES module, importing from these keeps startup lean. See [Performance](https://ix-xs.github.io/node-comfort/guide/performance.html).
|
|
108
|
+
|
|
109
|
+
TypeScript needs no setup: the types ship with the package.
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
import { schema as s, type Infer } from "@ix-xs/node-comfort";
|
|
113
|
+
|
|
114
|
+
const User = s.object({ email: s.string().email(), age: s.number().int().optional() });
|
|
115
|
+
type User = Infer<typeof User>; // { email: string; age?: number | undefined }
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## A quick tour
|
|
119
|
+
|
|
120
|
+
**Logs** that look good in a terminal and turn into JSON in production:
|
|
121
|
+
|
|
122
|
+
```js
|
|
123
|
+
nc.info("Server listening on port 3000").success("Connected to the database");
|
|
124
|
+
nc.log("<% cyan bold Tip:%> press <% bgWhite black q %> to quit");
|
|
125
|
+
|
|
126
|
+
const log = nc.createLogger({ scope: "api", file: { path: "./logs/api.log", maxSize: "10MB", maxFiles: 5 } });
|
|
127
|
+
const reqLog = log.child("request", { fields: { requestId: "a1b2" } });
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**Validation** with the type inferred for you:
|
|
131
|
+
|
|
132
|
+
```js
|
|
133
|
+
const User = nc.schema.object({
|
|
134
|
+
email: nc.schema.string().trim().email(),
|
|
135
|
+
age: nc.schema.number().int().min(18).optional(),
|
|
136
|
+
role: nc.schema.enum(["admin", "user"]).default("user"),
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const result = User.safeParse(req.body);
|
|
140
|
+
if (!result.success) return res.status(400).json(result.error.flatten());
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
**HTTP** on top of `fetch`, with the parts you always end up writing:
|
|
144
|
+
|
|
145
|
+
```js
|
|
146
|
+
const api = nc.http.create({
|
|
147
|
+
baseURL: "https://api.example.com/v1/",
|
|
148
|
+
auth: { bearer: process.env.API_TOKEN },
|
|
149
|
+
timeout: "10s",
|
|
150
|
+
retry: { attempts: 3 },
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const { data: users } = await api.get("users", { query: { page: 2 } });
|
|
154
|
+
await api.post("users", { name: "Ada" });
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
**SQLite** without compiling anything, with objects in and objects out:
|
|
158
|
+
|
|
159
|
+
```js
|
|
160
|
+
const db = new nc.SQLite("./data/app.sqlite");
|
|
161
|
+
|
|
162
|
+
db.insert("users", { email: "ada@example.com", settings: { theme: "dark" } });
|
|
163
|
+
const admins = db.getAll("users", { role: ["admin", "owner"], age: { gte: 18 } }, { orderBy: "email", limit: 20 });
|
|
164
|
+
db.transaction(() => {
|
|
165
|
+
db.update("accounts", { balance: 90 }, { id: 1 });
|
|
166
|
+
db.update("accounts", { balance: 110 }, { id: 2 });
|
|
167
|
+
});
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
**Dates** in any language and time zone:
|
|
171
|
+
|
|
172
|
+
```js
|
|
173
|
+
nc.time.setLocale("fr").setTimezone("Europe/Paris");
|
|
174
|
+
nc.time.format(new Date(), "dddd D MMMM YYYY HH:mm"); // "vendredi 11 septembre 2026 14:30"
|
|
175
|
+
nc.time.relative(Date.now() - 3 * 3600e3); // "il y a 3 heures"
|
|
176
|
+
nc.time.cron("0 9 * * 1-5", sendReport); // weekdays at 9:00
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
**Command-line tools** with typed flags, prompts and spinners:
|
|
180
|
+
|
|
181
|
+
```js
|
|
182
|
+
const { flags } = nc.cli.args({
|
|
183
|
+
port: { type: "number", short: "p", default: 3000, description: "Port to listen on" },
|
|
184
|
+
}, { version: "1.0.0" }); // --help and --version for free
|
|
185
|
+
|
|
186
|
+
const env = await nc.cli.select("Environment?", ["staging", "production"]);
|
|
187
|
+
const spinner = nc.cli.spinner(`Deploying to ${env}`).start();
|
|
188
|
+
spinner.succeed("Deployed");
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
That's a small part of it. The [guide](https://ix-xs.github.io/node-comfort/guide/introduction.html) walks through every namespace, and the [recipes](https://ix-xs.github.io/node-comfort/guide/recipes.html) show them working together: a small API server, a CLI, a scheduled job.
|
|
192
|
+
|
|
193
|
+
## Coming from 1.x
|
|
194
|
+
|
|
195
|
+
2.0 keeps the 1.x API but changes a few behaviors. The main ones:
|
|
196
|
+
|
|
197
|
+
- Node.js 22.13 or newer is required.
|
|
198
|
+
- SQLite methods throw a `SQLiteError` instead of returning `{ error }`, and `getAll()` no longer adds a default limit and order.
|
|
199
|
+
- `warn` and `error` write to stderr.
|
|
200
|
+
- Deep imports (`@ix-xs/node-comfort/src/...`) are replaced by official entry points (`@ix-xs/node-comfort/str`).
|
|
201
|
+
|
|
202
|
+
The [migration guide](https://ix-xs.github.io/node-comfort/guide/migration.html) lists every change with before and after examples.
|
|
203
|
+
|
|
204
|
+
## Contributing
|
|
205
|
+
|
|
206
|
+
Bug reports, ideas and pull requests are welcome. [CONTRIBUTING.md](./CONTRIBUTING.md) explains how the project is organized. In short: `npm install`, then `npm run check` builds everything and runs the tests.
|
|
207
|
+
|
|
208
|
+
## License
|
|
209
|
+
|
|
210
|
+
[MIT](./LICENSE) © ix-xs
|