@ix-xs/node-comfort 1.1.3 → 1.2.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/LICENSE +21 -0
- package/README.md +623 -748
- package/index.js +39 -13
- package/package.json +74 -24
- package/src/Arr.js +433 -0
- package/src/Checker.js +547 -188
- package/src/FS.js +127 -1
- package/src/Func.js +375 -0
- package/src/Id.js +235 -0
- package/src/Logger.js +71 -0
- package/src/Num.js +366 -0
- package/src/Obj.js +421 -0
- package/src/SQLite.js +20 -9
- package/src/Str.js +452 -0
- package/src/Time.js +485 -0
- package/types/index.d.ts +526 -0
- package/types/index.d.ts.map +1 -0
- package/types/src/Arr.d.ts +252 -0
- package/types/src/Arr.d.ts.map +1 -0
- package/types/src/Checker.d.ts +407 -0
- package/types/src/Checker.d.ts.map +1 -0
- package/types/src/FS.d.ts +303 -0
- package/types/src/FS.d.ts.map +1 -0
- package/types/src/Func.d.ts +210 -0
- package/types/src/Func.d.ts.map +1 -0
- package/types/src/Id.d.ts +119 -0
- package/types/src/Id.d.ts.map +1 -0
- package/types/src/Logger.d.ts +68 -0
- package/types/src/Logger.d.ts.map +1 -0
- package/types/src/Num.d.ts +242 -0
- package/types/src/Num.d.ts.map +1 -0
- package/types/src/Obj.d.ts +189 -0
- package/types/src/Obj.d.ts.map +1 -0
- package/types/src/SQLite.d.ts +396 -0
- package/types/src/SQLite.d.ts.map +1 -0
- package/types/src/Str.d.ts +293 -0
- package/types/src/Str.d.ts.map +1 -0
- package/types/src/Time.d.ts +205 -0
- package/types/src/Time.d.ts.map +1 -0
- package/types/src/Utils.d.ts +124 -0
- package/types/src/Utils.d.ts.map +1 -0
package/README.md
CHANGED
|
@@ -1,748 +1,623 @@
|
|
|
1
|
-
# @ix-xs/node-comfort
|
|
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
|
-
```js
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
//
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
###
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
);
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
//
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
//
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
//
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
//
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
//
|
|
357
|
-
|
|
358
|
-
//
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
```
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
###
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
//
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
//
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
```
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
await db.
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
//
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
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
|
+
# @ix-xs/node-comfort
|
|
2
|
+
|
|
3
|
+
<div align="center">
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+

|
|
7
|
+

|
|
8
|
+

|
|
9
|
+
|
|
10
|
+
[](https://www.npmjs.com/package/@ix-xs/node-comfort)
|
|
11
|
+
[](https://www.npmjs.com/package/@ix-xs/node-comfort)
|
|
12
|
+
[](https://www.npmjs.com/package/@ix-xs/node-comfort)
|
|
13
|
+
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
A **zero-dependency** comfort belt for Node.js • the helpers you rewrite in every
|
|
17
|
+
project, in one small, well-typed package:
|
|
18
|
+
|
|
19
|
+
- 🎨 **Logger** • colorful console logging with a tiny markup syntax and levels
|
|
20
|
+
- 📁 **FS** • safe filesystem operations, JSON I/O, watching, hashing
|
|
21
|
+
- 🧪 **Checker** • 35 runtime type guards (`isEmail`, `isEmpty`, `isPlainObject`, …)
|
|
22
|
+
- ⏳ **Utils** • `wait`, `when`, `dontCrash`, JSON helpers
|
|
23
|
+
- 🔤 **str** • case conversion, slugs, truncation, templating
|
|
24
|
+
- 🔢 **num** • clamp, round, ranges, byte/duration/number formatting
|
|
25
|
+
- 📚 **arr** • chunk, groupBy, unique, set operations, shuffle
|
|
26
|
+
- 🧩 **obj** • deep clone/merge/equal, dot-path get/set, pick/omit
|
|
27
|
+
- 🛠️ **func** • debounce, throttle, memoize, retry, timeout, pipe/compose
|
|
28
|
+
- 🕒 **time** • durations, relative time, date math • locale & time-zone aware
|
|
29
|
+
- 🆔 **id** • secure UUID / ULID / nanoid / tokens, hashing, constant-time compare
|
|
30
|
+
- 🗄️ **SQLite** • a small model + CRUD wrapper over Node's built-in `node:sqlite`
|
|
31
|
+
|
|
32
|
+
Everything is plain CommonJS with JSDoc, and the package ships full TypeScript
|
|
33
|
+
type definitions.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Installation
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm install @ix-xs/node-comfort
|
|
41
|
+
# or
|
|
42
|
+
yarn add @ix-xs/node-comfort
|
|
43
|
+
# or
|
|
44
|
+
pnpm add @ix-xs/node-comfort
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Requires **Node 20.12+** (uses `node:sqlite`, `process.loadEnvFile`, `structuredClone`).
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Quick start
|
|
52
|
+
|
|
53
|
+
The package exposes a **flat API** for the historical modules (Logger, FS,
|
|
54
|
+
Checker, Utils) and **namespaces** for the newer utility collections.
|
|
55
|
+
|
|
56
|
+
```js
|
|
57
|
+
const nc = require("@ix-xs/node-comfort");
|
|
58
|
+
|
|
59
|
+
// Logger (flat)
|
|
60
|
+
nc.log("<% greenBright Success %>");
|
|
61
|
+
nc.info("server started");
|
|
62
|
+
nc.error(new Error("boom"));
|
|
63
|
+
|
|
64
|
+
// FS (flat)
|
|
65
|
+
nc.writeJSON("./data/config.json", { ready: true });
|
|
66
|
+
const cfg = nc.readJSON("./data/config.json", {});
|
|
67
|
+
|
|
68
|
+
// Checker (flat)
|
|
69
|
+
nc.isEmail("john@example.com"); // true
|
|
70
|
+
nc.isEmpty([]); // true
|
|
71
|
+
|
|
72
|
+
// Utils (flat)
|
|
73
|
+
await nc.wait(500);
|
|
74
|
+
|
|
75
|
+
// Namespaced collections
|
|
76
|
+
nc.str.slugify("Héllo World"); // "hello-world"
|
|
77
|
+
nc.num.formatBytes(1536); // "1.5 KB"
|
|
78
|
+
nc.arr.chunk([1, 2, 3, 4], 2); // [[1, 2], [3, 4]]
|
|
79
|
+
nc.obj.get(data, "a.b[0].c", "def");
|
|
80
|
+
nc.func.debounce(fn, 200);
|
|
81
|
+
nc.time.relative(Date.now() - 3600e3); // "1 hour ago"
|
|
82
|
+
nc.id.uuid();
|
|
83
|
+
|
|
84
|
+
// SQLite (class)
|
|
85
|
+
const db = new nc.SQLite("data/app.sqlite");
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
> The flat modules are also available as namespaces if you prefer explicit
|
|
89
|
+
> grouping: `nc.logger`, `nc.fs`, `nc.checker`, `nc.utils`.
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## API at a glance
|
|
94
|
+
|
|
95
|
+
| Namespace | Highlights |
|
|
96
|
+
| --- | --- |
|
|
97
|
+
| flat / `logger` | `log` `info` `success` `warn` `error` `debug` `group` `groupEnd` `setTimestamp` `setDelimiter` |
|
|
98
|
+
| flat / `fs` | `readFile` `readJSON` `writeJSON` `appendFile` `createFile` `createFolder` `copy*` `move*` `delete*` `getFilesIn` `getFoldersIn` `exists` `stat` `fileSize` `hashFile` `emptyFolder` `watch` `getEnv` |
|
|
99
|
+
| flat / `checker` | `isArray` `isNumber` `isInteger` `isString` `isPlainObject` `isPromise` `isEmpty` `isEmail` `isURL` `isUUID` `isJSON` `isNumeric` … (35 total) |
|
|
100
|
+
| flat / `utils` | `wait` `when` `dontCrash` `JSONString` `JSONParse` |
|
|
101
|
+
| `str` | `capitalize` `camelCase` `snakeCase` `kebabCase` `slugify` `truncate` `template` `escapeHTML` `words` … |
|
|
102
|
+
| `num` | `clamp` `round` `range` `sum` `average` `median` `formatBytes` `abbreviate` `thousands` `ordinal` `percent` … |
|
|
103
|
+
| `arr` | `chunk` `unique` `groupBy` `keyBy` `partition` `sortBy` `difference` `intersection` `union` `zip` `shuffle` `sample` … |
|
|
104
|
+
| `obj` | `clone` `merge` `equal` `get` `set` `has` `pick` `omit` `mapValues` `flatten` `unflatten` `deepFreeze` … |
|
|
105
|
+
| `func` | `debounce` `throttle` `once` `memoize` `retry` `timeout` `pipe` `compose` `curry` `promisify` `attempt` … |
|
|
106
|
+
| `time` | `parseDuration` `formatDuration` `relative` `format` `add` `subtract` `diff` `startOf` `isSameDay` `stopwatch` `setLocale` `setTimezone` … (i18n + time-zone aware) |
|
|
107
|
+
| `id` | `uuid` `ulid` `nano` `token` `code` `hash` `hmac` `safeEqual` `seq` |
|
|
108
|
+
| `SQLite` | `createTable` `insert` `update` `upsert` `get` `getAll` `delete` `count` `transaction` `createIndex` … |
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## Logger
|
|
113
|
+
|
|
114
|
+
Colorized console logger with a minimal markup syntax, optional timestamps,
|
|
115
|
+
groups, and level helpers.
|
|
116
|
+
|
|
117
|
+
Wrap styles in delimiters (default `<%` … `%>`):
|
|
118
|
+
|
|
119
|
+
- **styles**: `bold`, `italic`, `underline`, `overline`
|
|
120
|
+
- **colors**: `red`, `green`, `yellow`, `blue`, `magenta`, `cyan`, `white`, `gray`, `black`
|
|
121
|
+
- **bright**: `redBright`, `greenBright`, … `whiteBright`
|
|
122
|
+
- **background**: `bgRed`, `bgBlue`, …
|
|
123
|
+
- **RGB**: `rgb(255, 0, 0)`
|
|
124
|
+
|
|
125
|
+
```js
|
|
126
|
+
nc.log("Plain log");
|
|
127
|
+
nc.log("<% green Success %>");
|
|
128
|
+
nc.log("<% red bold Error:%> something went wrong");
|
|
129
|
+
nc.log("<% bgBlue white INFO %> message");
|
|
130
|
+
nc.log("<% rgb(255,128,0) Orange text %>");
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Level helpers
|
|
134
|
+
|
|
135
|
+
```js
|
|
136
|
+
nc.info("Loading configuration…"); // ℹ INFO
|
|
137
|
+
nc.success("Database connected"); // ✔ OK
|
|
138
|
+
nc.warn("Cache miss"); // ⚠ WARN
|
|
139
|
+
nc.error(new Error("Boom")); // ✖ ERROR (prints the stack)
|
|
140
|
+
nc.debug({ requestId: 42 }); // ● DEBUG (only when DEBUG/NODE_DEBUG is set)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Timestamp, delimiters & groups
|
|
144
|
+
|
|
145
|
+
```js
|
|
146
|
+
nc.setTimestamp(false);
|
|
147
|
+
nc.setDelimiter({ open: "{{", close: "}}" });
|
|
148
|
+
nc.log("{{red Hello}} world");
|
|
149
|
+
|
|
150
|
+
nc.group("Startup");
|
|
151
|
+
nc.log("Loading config…");
|
|
152
|
+
nc.group("DB");
|
|
153
|
+
nc.log("Connecting…");
|
|
154
|
+
nc.groupEnd(); // end "DB"
|
|
155
|
+
nc.groupEnd(); // end "Startup"
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## FS
|
|
161
|
+
|
|
162
|
+
File-system helper with safe path resolution, recursive walking, copy/move
|
|
163
|
+
helpers, JSON I/O, hashing and a small watcher. Relative paths (`./`, `../`) are
|
|
164
|
+
resolved from the **caller file**, which is handy inside libraries.
|
|
165
|
+
|
|
166
|
+
```js
|
|
167
|
+
// Paths
|
|
168
|
+
const p = nc.createPath("./config/app.json"); // absolute, even if missing
|
|
169
|
+
const folder = nc.getFolder("./src");
|
|
170
|
+
const file = nc.getFile("package.json");
|
|
171
|
+
|
|
172
|
+
// Create
|
|
173
|
+
nc.createFolder("./dist");
|
|
174
|
+
nc.createFile("./dist/info.txt", false, "hello");
|
|
175
|
+
|
|
176
|
+
// JSON I/O
|
|
177
|
+
nc.writeJSON("./dist/data.json", { ok: true }); // pretty, 2 spaces
|
|
178
|
+
const data = nc.readJSON("./dist/data.json", {}); // fallback on error
|
|
179
|
+
|
|
180
|
+
// Append / read
|
|
181
|
+
nc.appendFile("./logs/app.log", "started\n");
|
|
182
|
+
const content = nc.readFile("./README.md");
|
|
183
|
+
|
|
184
|
+
// Inspect
|
|
185
|
+
nc.exists("./dist"); // true
|
|
186
|
+
nc.fileSize("./dist/data.json"); // bytes
|
|
187
|
+
nc.stat("./dist/data.json"); // fs.Stats
|
|
188
|
+
nc.hashFile("./dist/data.json"); // sha256 hex
|
|
189
|
+
|
|
190
|
+
// List & delete
|
|
191
|
+
const files = nc.getFilesIn("./src", true);
|
|
192
|
+
nc.deleteFilesIn("./logs", false, (f) => f.endsWith(".log"));
|
|
193
|
+
nc.emptyFolder("./tmp"); // keep the folder, clear its contents
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Copy & move
|
|
197
|
+
|
|
198
|
+
```js
|
|
199
|
+
nc.copyFolder({ path: "./templates", dest: "./dist/templates", recursive: true, withFiles: true, force: true });
|
|
200
|
+
nc.copyFilesIn({ path: "./src", dest: "./dist", recursive: true, filter: (f) => f.endsWith(".js") });
|
|
201
|
+
nc.copyFile({ path: "./src/index.js", dest: "./dist", force: true });
|
|
202
|
+
|
|
203
|
+
nc.moveFolder({ path: "./build", dest: "./dist", recursive: true, withFiles: true, force: true });
|
|
204
|
+
nc.moveFile({ path: "./logs/app.log", dest: "./logs/archive/app.log", force: true });
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Watch
|
|
208
|
+
|
|
209
|
+
```js
|
|
210
|
+
const watcher = nc.watch({ path: "./src", recursive: true, filter: (event, file) => file.endsWith(".js") });
|
|
211
|
+
|
|
212
|
+
watcher
|
|
213
|
+
.on("change", (file) => nc.log(`<% cyan changed %> ${file}`))
|
|
214
|
+
.on("rename", (file) => nc.log(`<% yellow renamed %> ${file}`));
|
|
215
|
+
|
|
216
|
+
watcher.pause();
|
|
217
|
+
watcher.resume();
|
|
218
|
+
watcher.stop();
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## Checker
|
|
224
|
+
|
|
225
|
+
35 runtime type guards. Each is a boolean function that accepts any input.
|
|
226
|
+
|
|
227
|
+
```js
|
|
228
|
+
// Primitives & core
|
|
229
|
+
nc.isArray([1, 2]); nc.isNumber(42); nc.isInteger(4);
|
|
230
|
+
nc.isFloat(4.2); nc.isString("x"); nc.isBoolean(false);
|
|
231
|
+
nc.isBigInt(10n); nc.isSymbol(Symbol()); nc.isFunction(() => {});
|
|
232
|
+
nc.isAsyncFunction(async () => {});
|
|
233
|
+
|
|
234
|
+
// Nullish & primitiveness
|
|
235
|
+
nc.isNull(null); nc.isUndefined(undefined); nc.isNil(null); nc.isPrimitive(1);
|
|
236
|
+
|
|
237
|
+
// Objects & structures
|
|
238
|
+
nc.isObject({}); nc.isPlainObject({}); nc.isPromise(Promise.resolve());
|
|
239
|
+
nc.isRegExp(/a/); nc.isDate(new Date()); nc.isValidDate(new Date());
|
|
240
|
+
nc.isMap(new Map()); nc.isSet(new Set()); nc.isIterable([1]);
|
|
241
|
+
nc.isBuffer(Buffer.from("")); nc.isTypedArray(new Uint8Array()); nc.isError(new Error());
|
|
242
|
+
|
|
243
|
+
// Emptiness & formats
|
|
244
|
+
nc.isEmpty(""); nc.isEmpty([]); nc.isEmpty({}); nc.isEmpty(new Map());
|
|
245
|
+
nc.isEmail("john@example.com"); nc.isURL("https://x.com");
|
|
246
|
+
nc.isUUID("123e4567-e89b-12d3-a456-426614174000");
|
|
247
|
+
nc.isJSON('{"a":1}'); nc.isNumeric("42");
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
## Utils
|
|
253
|
+
|
|
254
|
+
Async & process helpers.
|
|
255
|
+
|
|
256
|
+
```js
|
|
257
|
+
// wait
|
|
258
|
+
await nc.wait(500);
|
|
259
|
+
|
|
260
|
+
// when • poll a predicate and emit events
|
|
261
|
+
nc.when(() => Math.random() > 0.8, { label: "Lucky" }, { interval: 200, max: 3, timeout: 5000 })
|
|
262
|
+
.on("trigger", (p) => nc.log(`<% green ${p.label} %>`))
|
|
263
|
+
.on("timeout", () => nc.log("<% red Timeout %>"))
|
|
264
|
+
.start();
|
|
265
|
+
|
|
266
|
+
// dontCrash • process-level safety nets
|
|
267
|
+
nc.dontCrash()
|
|
268
|
+
.on("error", (err) => console.error("Global error:", err))
|
|
269
|
+
.on("sig", (signal) => { nc.warn(`Caught ${signal}`); process.exit(0); });
|
|
270
|
+
|
|
271
|
+
// JSON
|
|
272
|
+
const json = nc.JSONString({ hello: "world" }); // 4-space pretty print
|
|
273
|
+
const value = nc.JSONParse(json);
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## str • string utilities
|
|
279
|
+
|
|
280
|
+
```js
|
|
281
|
+
nc.str.capitalize("hELLO"); // "Hello"
|
|
282
|
+
nc.str.titleCase("the quick fox"); // "The Quick Fox"
|
|
283
|
+
nc.str.camelCase("foo-bar_baz"); // "fooBarBaz"
|
|
284
|
+
nc.str.pascalCase("foo bar"); // "FooBar"
|
|
285
|
+
nc.str.snakeCase("helloWorld"); // "hello_world"
|
|
286
|
+
nc.str.kebabCase("helloWorld"); // "hello-world"
|
|
287
|
+
nc.str.constantCase("helloWorld"); // "HELLO_WORLD"
|
|
288
|
+
|
|
289
|
+
nc.str.slugify("Héllo, World!"); // "hello-world"
|
|
290
|
+
nc.str.truncate("Hello world", 8); // "Hello w…"
|
|
291
|
+
nc.str.center("hi", 6, "*"); // "**hi**"
|
|
292
|
+
nc.str.reverse("abc"); // "cba" (emoji-safe)
|
|
293
|
+
nc.str.squish(" a b "); // "a b"
|
|
294
|
+
nc.str.stripTags("<b>Hi</b>"); // "Hi"
|
|
295
|
+
nc.str.escapeHTML('<a>&"'); // "<a>&""
|
|
296
|
+
nc.str.escapeRegExp("a.b"); // "a\\.b"
|
|
297
|
+
nc.str.count("banana", "a"); // 3
|
|
298
|
+
nc.str.ensurePrefix("a.com", "https://"); // "https://a.com"
|
|
299
|
+
nc.str.template("Hi {name} ({user.role})", { name: "Jo", user: { role: "admin" } });
|
|
300
|
+
// "Hi Jo (admin)"
|
|
301
|
+
nc.str.words("helloWorld-foo"); // ["hello", "World", "foo"]
|
|
302
|
+
nc.str.length("😀"); // 1
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
## num • number & math utilities
|
|
308
|
+
|
|
309
|
+
```js
|
|
310
|
+
nc.num.clamp(15, 0, 10); // 10
|
|
311
|
+
nc.num.round(1.005, 2); // 1.01
|
|
312
|
+
nc.num.inRange(5, 0, 10); // true
|
|
313
|
+
nc.num.lerp(0, 100, 0.5); // 50
|
|
314
|
+
nc.num.mapRange(5, 0, 10, 0, 100); // 50
|
|
315
|
+
nc.num.randomInt(1, 6); // dice roll
|
|
316
|
+
|
|
317
|
+
nc.num.range(4); // [0, 1, 2, 3]
|
|
318
|
+
nc.num.range(0, 10, 2); // [0, 2, 4, 6, 8]
|
|
319
|
+
nc.num.range(5, 0); // [5, 4, 3, 2, 1]
|
|
320
|
+
|
|
321
|
+
nc.num.sum([1, 2, 3]); // 6
|
|
322
|
+
nc.num.average([2, 4]); // 3
|
|
323
|
+
nc.num.median([3, 1, 2]); // 2
|
|
324
|
+
nc.num.percent(25, 200); // 12.5
|
|
325
|
+
|
|
326
|
+
nc.num.formatBytes(1536); // "1.5 KB"
|
|
327
|
+
nc.num.formatBytes(1024, { iec: true }); // "1 KiB"
|
|
328
|
+
nc.num.abbreviate(2_400_000); // "2.4M"
|
|
329
|
+
nc.num.thousands(1234567); // "1,234,567"
|
|
330
|
+
nc.num.ordinal(22); // "22nd"
|
|
331
|
+
nc.num.parse("42px"); // 42
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
---
|
|
335
|
+
|
|
336
|
+
## arr • array utilities
|
|
337
|
+
|
|
338
|
+
All methods are pure (they never mutate their input). Many accept an
|
|
339
|
+
`iteratee`: a property key (string) or a function.
|
|
340
|
+
|
|
341
|
+
```js
|
|
342
|
+
nc.arr.chunk([1, 2, 3, 4, 5], 2); // [[1, 2], [3, 4], [5]]
|
|
343
|
+
nc.arr.unique([1, 1, 2]); // [1, 2]
|
|
344
|
+
nc.arr.unique(users, "id"); // de-dupe by id
|
|
345
|
+
nc.arr.groupBy([1, 2, 3, 4], (n) => (n % 2 ? "odd" : "even"));
|
|
346
|
+
nc.arr.keyBy(users, "id");
|
|
347
|
+
nc.arr.partition([1, 2, 3, 4], (n) => n % 2 === 0); // [[2, 4], [1, 3]]
|
|
348
|
+
nc.arr.countBy(["a", "b", "a"]); // { a: 2, b: 1 }
|
|
349
|
+
|
|
350
|
+
nc.arr.sortBy(users, "age");
|
|
351
|
+
nc.arr.sortBy(users, ["age", "name"], "desc");
|
|
352
|
+
nc.arr.sumBy(items, "price");
|
|
353
|
+
nc.arr.maxBy(items, "score");
|
|
354
|
+
|
|
355
|
+
nc.arr.difference([1, 2, 3, 4], [2, 4]); // [1, 3]
|
|
356
|
+
nc.arr.intersection([1, 2, 3], [2, 3, 4]); // [2, 3]
|
|
357
|
+
nc.arr.union([1, 2], [2, 3]); // [1, 2, 3]
|
|
358
|
+
nc.arr.zip(["a", "b"], [1, 2]); // [["a", 1], ["b", 2]]
|
|
359
|
+
|
|
360
|
+
nc.arr.compact([0, 1, false, 2, ""]); // [1, 2]
|
|
361
|
+
nc.arr.flatten([1, [2, [3]]], Infinity); // [1, 2, 3]
|
|
362
|
+
nc.arr.shuffle([1, 2, 3]); // random order
|
|
363
|
+
nc.arr.sample([1, 2, 3]); // random element
|
|
364
|
+
nc.arr.sampleSize([1, 2, 3, 4], 2); // 2 random elements
|
|
365
|
+
nc.arr.first([1, 2, 3], 2); // [1, 2]
|
|
366
|
+
nc.arr.last([1, 2, 3]); // 3
|
|
367
|
+
nc.arr.move([1, 2, 3], 0, 2); // [2, 3, 1]
|
|
368
|
+
nc.arr.times(3, (i) => i * 2); // [0, 2, 4]
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
---
|
|
372
|
+
|
|
373
|
+
## obj • object utilities
|
|
374
|
+
|
|
375
|
+
Pure, dot-path-aware helpers. `get`/`set`/`has` accept `"a.b[0].c"` paths.
|
|
376
|
+
|
|
377
|
+
```js
|
|
378
|
+
nc.obj.clone({ a: { b: 1 } }); // deep clone (structuredClone + fallback)
|
|
379
|
+
nc.obj.merge({ a: { x: 1 } }, { a: { y: 2 } }); // { a: { x: 1, y: 2 } }
|
|
380
|
+
nc.obj.equal({ a: [1, 2] }, { a: [1, 2] }); // true (deep)
|
|
381
|
+
|
|
382
|
+
nc.obj.get(data, "a.b[0].c", "default");
|
|
383
|
+
nc.obj.set({}, "a.b.c", 1); // { a: { b: { c: 1 } } } (returns a clone)
|
|
384
|
+
nc.obj.has({ a: { b: 1 } }, "a.b"); // true
|
|
385
|
+
|
|
386
|
+
nc.obj.pick({ a: 1, b: 2, c: 3 }, ["a", "c"]); // { a: 1, c: 3 }
|
|
387
|
+
nc.obj.omit({ a: 1, b: 2 }, ["b"]); // { a: 1 }
|
|
388
|
+
nc.obj.filter({ a: 1, b: 2 }, (v) => v > 1); // { b: 2 }
|
|
389
|
+
nc.obj.mapValues({ a: 1 }, (v) => v * 10); // { a: 10 }
|
|
390
|
+
nc.obj.mapKeys({ a: 1 }, (k) => k.toUpperCase()); // { A: 1 }
|
|
391
|
+
nc.obj.invert({ a: "x" }); // { x: "a" }
|
|
392
|
+
nc.obj.compact({ a: 1, b: null }); // { a: 1 }
|
|
393
|
+
|
|
394
|
+
nc.obj.flatten({ a: { b: { c: 1 } } }); // { "a.b.c": 1 }
|
|
395
|
+
nc.obj.unflatten({ "a.b.c": 1 }); // { a: { b: { c: 1 } } }
|
|
396
|
+
nc.obj.deepFreeze(config); // recursively immutable
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
---
|
|
400
|
+
|
|
401
|
+
## func • function & control-flow utilities
|
|
402
|
+
|
|
403
|
+
```js
|
|
404
|
+
// Rate limiting
|
|
405
|
+
const onResize = nc.func.debounce(() => render(), 200);
|
|
406
|
+
const onScroll = nc.func.throttle(() => update(), 100);
|
|
407
|
+
onResize.cancel(); onResize.flush();
|
|
408
|
+
|
|
409
|
+
// Caching & single-call
|
|
410
|
+
const init = nc.func.once(() => setup());
|
|
411
|
+
const fib = nc.func.memoize((n) => (n < 2 ? n : fib(n - 1) + fib(n - 2)));
|
|
412
|
+
|
|
413
|
+
// Resilience
|
|
414
|
+
const data = await nc.func.retry(() => fetchThing(), {
|
|
415
|
+
attempts: 5, delay: 200, backoff: 2, // 200, 400, 800…
|
|
416
|
+
});
|
|
417
|
+
const res = await nc.func.timeout(fetchThing(), 5000, "too slow");
|
|
418
|
+
|
|
419
|
+
// Go-style error handling • no try/catch
|
|
420
|
+
const [err, user] = await nc.func.attempt(() => getUser(id))();
|
|
421
|
+
|
|
422
|
+
// Composition
|
|
423
|
+
const clean = nc.func.pipe((s) => s.trim(), (s) => s.toUpperCase());
|
|
424
|
+
const add = nc.func.curry((a, b, c) => a + b + c);
|
|
425
|
+
add(1)(2)(3); // 6
|
|
426
|
+
|
|
427
|
+
// Interop
|
|
428
|
+
const readFile = nc.func.promisify(require("fs").readFile);
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
---
|
|
432
|
+
|
|
433
|
+
## time • date & time utilities
|
|
434
|
+
|
|
435
|
+
No heavyweight date library required.
|
|
436
|
+
|
|
437
|
+
```js
|
|
438
|
+
nc.time.parseDuration("1h30m"); // 5400000
|
|
439
|
+
nc.time.parseDuration("2 days"); // 172800000
|
|
440
|
+
nc.time.formatDuration(5_400_000); // "1h 30m"
|
|
441
|
+
nc.time.formatDuration(90_000, { long: true }); // "1 minute 30 seconds"
|
|
442
|
+
|
|
443
|
+
nc.time.relative(Date.now() - 3600e3); // "1 hour ago"
|
|
444
|
+
nc.time.relative(Date.now() + 86400e3); // "in 1 day"
|
|
445
|
+
|
|
446
|
+
nc.time.format(new Date(), "YYYY-MM-DD HH:mm:ss");
|
|
447
|
+
nc.time.add(new Date(), "2h");
|
|
448
|
+
nc.time.subtract(new Date(), 1, "d");
|
|
449
|
+
nc.time.diff("2024-01-03", "2024-01-01", "d"); // 2
|
|
450
|
+
nc.time.startOf(new Date(), "day");
|
|
451
|
+
nc.time.isSameDay(a, b);
|
|
452
|
+
nc.time.unix(); // seconds since epoch
|
|
453
|
+
|
|
454
|
+
const sw = nc.time.stopwatch();
|
|
455
|
+
doWork();
|
|
456
|
+
sw.stop(); // 12.34 (ms, sub-ms precise)
|
|
457
|
+
sw.stop(true); // "12.34ms"
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
### Language & time zone
|
|
461
|
+
|
|
462
|
+
`relative`, `formatDuration` and `format` are **locale-aware** and built on the
|
|
463
|
+
platform's native `Intl` (Node ships with full ICU), so **every language works
|
|
464
|
+
out of the box** • no locale files, no dependencies. The default locale is
|
|
465
|
+
`"en"` for deterministic output.
|
|
466
|
+
|
|
467
|
+
Set a global default (chainable), or override per call:
|
|
468
|
+
|
|
469
|
+
```js
|
|
470
|
+
// Global defaults
|
|
471
|
+
nc.time.setLocale("fr").setTimezone("Europe/Paris");
|
|
472
|
+
nc.time.getConfig(); // { locale: "fr", timeZone: "Europe/Paris" }
|
|
473
|
+
|
|
474
|
+
// …or per call via options
|
|
475
|
+
nc.time.relative(Date.now() - 3600e3, undefined, { locale: "fr" }); // "il y a 1 heure"
|
|
476
|
+
nc.time.relative(Date.now() - 86400e3, undefined, { numeric: "auto" }); // "yesterday"
|
|
477
|
+
nc.time.formatDuration(90_000, { long: true, locale: "de" }); // "1 Minute 30 Sekunden"
|
|
478
|
+
|
|
479
|
+
// Localized date formatting
|
|
480
|
+
nc.time.format(Date.now(), "dddd D MMMM YYYY", { locale: "fr" }); // "lundi 15 janvier 2024"
|
|
481
|
+
nc.time.format(Date.now(), "h:mm A"); // "2:30 PM"
|
|
482
|
+
|
|
483
|
+
// Time-zone aware (IANA names)
|
|
484
|
+
nc.time.format(utcDate, "YYYY-MM-DD HH:mm", { timeZone: "Asia/Tokyo" });
|
|
485
|
+
nc.time.isSameDay(a, b, { timeZone: "America/New_York" });
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
**`format` tokens** • numeric: `YYYY` `YY` `MM` `M` `DD` `D` `HH` `H` (24h)
|
|
489
|
+
`hh` `h` (12h) `mm` `m` `ss` `s` `SSS`; localized: `MMMM` `MMM` (month),
|
|
490
|
+
`dddd` `ddd` (weekday), `A` `a` (AM/PM). Wrap literal text in square brackets:
|
|
491
|
+
`format(d, "[Updated on] dddd")`.
|
|
492
|
+
|
|
493
|
+
---
|
|
494
|
+
|
|
495
|
+
## id • id, token & hashing utilities
|
|
496
|
+
|
|
497
|
+
Cryptographically secure, built on Node's native `node:crypto`.
|
|
498
|
+
|
|
499
|
+
```js
|
|
500
|
+
nc.id.uuid(); // UUID v4
|
|
501
|
+
nc.id.ulid(); // sortable, timestamp-prefixed id
|
|
502
|
+
nc.id.nano(); // URL-safe nanoid (21 chars)
|
|
503
|
+
nc.id.nano(10); // custom length
|
|
504
|
+
|
|
505
|
+
nc.id.token(); // 32-byte hex token
|
|
506
|
+
nc.id.token(16, "base64url"); // URL-safe base64
|
|
507
|
+
nc.id.token(16, "base58"); // Base58 (Bitcoin alphabet)
|
|
508
|
+
nc.id.code(); // "K7QF9X" (unambiguous, human-friendly)
|
|
509
|
+
|
|
510
|
+
nc.id.hash("hello"); // sha256 hex
|
|
511
|
+
nc.id.hash("hello", { algorithm: "md5" });
|
|
512
|
+
nc.id.hmac("payload", "secret"); // HMAC-SHA256
|
|
513
|
+
|
|
514
|
+
nc.id.safeEqual(provided, expected); // constant-time comparison
|
|
515
|
+
nc.id.seq("user"); // "user-1", "user-2", …
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
> **Tip:** ULIDs sort by creation time, making them a great primary-key
|
|
519
|
+
> alternative to UUIDs. Use `nc.id.safeEqual` whenever you compare secrets,
|
|
520
|
+
> tokens or signatures to avoid timing attacks.
|
|
521
|
+
|
|
522
|
+
---
|
|
523
|
+
|
|
524
|
+
## SQLite
|
|
525
|
+
|
|
526
|
+
Thin wrapper over the built-in `node:sqlite` `DatabaseSync`, with a small model
|
|
527
|
+
system and CRUD helpers. Identifiers are validated and values are bound with
|
|
528
|
+
`?` placeholders. WAL mode and foreign keys are enabled automatically.
|
|
529
|
+
|
|
530
|
+
```js
|
|
531
|
+
const db = new nc.SQLite("data/app.sqlite"); // folder created if needed
|
|
532
|
+
// const db = new nc.SQLite(":memory:"); // transient in-memory DB
|
|
533
|
+
|
|
534
|
+
await db.createTable({
|
|
535
|
+
name: "users",
|
|
536
|
+
columns: {
|
|
537
|
+
id: { type: "INTEGER", primaryKey: true, autoincrement: true },
|
|
538
|
+
name: { type: "TEXT", notNull: true },
|
|
539
|
+
role: { type: "TEXT", values: ["admin", "user"], defaultValue: "user" },
|
|
540
|
+
createdAt: { type: "INTEGER", notNull: true },
|
|
541
|
+
},
|
|
542
|
+
indexes: [{ columns: ["role"] }],
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
await db.insert("users", { name: "John", role: "admin", createdAt: Date.now() });
|
|
546
|
+
await db.update("users", { role: "user" }, { name: "John" });
|
|
547
|
+
await db.upsert("users", { id: 1, role: "admin" }, ["id"]);
|
|
548
|
+
|
|
549
|
+
const user = await db.get("users", { name: "John" });
|
|
550
|
+
const admins = await db.getAll("users", { role: "admin" }, { limit: 100, orderBy: "createdAt", direction: "DESC" });
|
|
551
|
+
|
|
552
|
+
await db.delete("users", { role: "user" });
|
|
553
|
+
await db.count("users", { role: "admin" });
|
|
554
|
+
await db.clearTable("users");
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
### Constraints & indexes
|
|
558
|
+
|
|
559
|
+
```js
|
|
560
|
+
await db.createTable({
|
|
561
|
+
name: "UserAuthMethod",
|
|
562
|
+
columns: {
|
|
563
|
+
id: { type: "INTEGER", primaryKey: true, autoincrement: true },
|
|
564
|
+
userId: { type: "INTEGER", notNull: true },
|
|
565
|
+
provider: { type: "TEXT", notNull: true },
|
|
566
|
+
providerUserId: { type: "TEXT", notNull: true },
|
|
567
|
+
},
|
|
568
|
+
constraints: [
|
|
569
|
+
{ type: "unique", columns: ["provider", "providerUserId"] },
|
|
570
|
+
{ type: "foreignKey", columns: ["userId"], references: { table: "users", columns: ["id"] }, onDelete: "CASCADE" },
|
|
571
|
+
],
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
await db.createIndex("UserAuthMethod", { columns: ["provider", "providerUserId"], unique: true });
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
### Transactions & raw SQL
|
|
578
|
+
|
|
579
|
+
```js
|
|
580
|
+
await db.transaction(async () => {
|
|
581
|
+
await db.insert("users", { name: "A", createdAt: Date.now() });
|
|
582
|
+
await db.insert("profiles", { userId: 1, nickname: "A" });
|
|
583
|
+
return { ok: true }; // throw to roll back
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
await db.exec("CREATE INDEX IF NOT EXISTS idx_name ON users (name)");
|
|
587
|
+
const one = await db.queryOne("SELECT * FROM users WHERE id = ?", [1]);
|
|
588
|
+
const many = await db.queryAll("SELECT * FROM users WHERE role = ?", ["admin"]);
|
|
589
|
+
|
|
590
|
+
db.close();
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
---
|
|
594
|
+
|
|
595
|
+
## TypeScript
|
|
596
|
+
|
|
597
|
+
The package ships generated `.d.ts` files, so everything is fully typed out of
|
|
598
|
+
the box • no `@types` package needed.
|
|
599
|
+
|
|
600
|
+
```ts
|
|
601
|
+
import nc from "@ix-xs/node-comfort";
|
|
602
|
+
// or: import * as nc from "@ix-xs/node-comfort";
|
|
603
|
+
|
|
604
|
+
const id: string = nc.id.uuid();
|
|
605
|
+
const size: string = nc.num.formatBytes(2048);
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
---
|
|
609
|
+
|
|
610
|
+
## Contributing
|
|
611
|
+
|
|
612
|
+
Issues and PRs are welcome at
|
|
613
|
+
[github.com/ix-xs/node-comfort](https://github.com/ix-xs/node-comfort).
|
|
614
|
+
|
|
615
|
+
```bash
|
|
616
|
+
npm run build # regenerate the type definitions (tsc)
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
---
|
|
620
|
+
|
|
621
|
+
## License
|
|
622
|
+
|
|
623
|
+
[MIT](./LICENSE) © ix-xs
|