@ix-xs/node-comfort 1.1.31 → 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/README.md CHANGED
@@ -1,748 +1,623 @@
1
- # @ix-xs/node-comfort
2
-
3
-
4
- <div align="center">
5
-
6
-
7
- ![Node.js](https://img.shields.io/badge/node.js-6DA55F?style=for-the-badge&logo=node.js&logoColor=white)
8
- ![JavaScript](https://img.shields.io/badge/javascript-%23323330.svg?style=for-the-badge&logo=javascript&logoColor=%23F7DF1E)
9
- ![NPM](https://img.shields.io/badge/NPM-%23CB3837.svg?style=for-the-badge&logo=npm&logoColor=white)
10
- ![Github](https://img.shields.io/badge/github-%23121011.svg?style=for-the-badge&logo=github&logoColor=white)
11
-
12
-
13
- [![npm version](https://badge.fury.io/js/%40ix-xs%2Fnode-comfort.svg)](https://www.npmjs.com/package/@ix-xs/node-comfort)
14
- [![Downloads](https://img.shields.io/npm/dm/@ix-xs/node-comfort.svg)](https://www.npmjs.com/package/@ix-xs/node-comfort)
15
-
16
-
17
- </div>
18
-
19
-
20
- Small collection of Node.js utilities I use across projects: a logger, a file-system helper, a SQLite wrapper, a runtime type checker, and a few async/process helpers.
21
-
22
-
23
- Everything is plain CommonJS with JSDoc. The package ships with type information for TypeScript and modern editors.
24
-
25
-
26
- ---
27
-
28
-
29
- ## Installation
30
-
31
-
32
- ```bash
33
- npm install @ix-xs/node-comfort
34
- # or
35
- yarn add @ix-xs/node-comfort
36
- # or
37
- pnpm add @ix-xs/node-comfort
38
- ```
39
-
40
-
41
- Requires Node 20+ (uses `node:sqlite` and `process.loadEnvFile`).
42
-
43
-
44
- ---
45
-
46
-
47
- ## Import and global usage
48
-
49
-
50
- The package exposes a **flat API**: all utilities are directly on the `nodeComfort` object.
51
-
52
-
53
- ```js
54
- const nodeComfort = require("@ix-xs/node-comfort");
55
-
56
-
57
- // Logger
58
- nodeComfort.log("Hello");
59
- nodeComfort.log("<% greenBright Success %>");
60
-
61
-
62
- // FS
63
- const files = nodeComfort.getFilesIn(".", true);
64
- nodeComfort.createFolder("./dist");
65
-
66
-
67
- // Checker
68
- if (nodeComfort.isArray(files)) {
69
- nodeComfort.log(`<% cyan Found ${files.length} files %>`);
70
- }
71
-
72
-
73
- // Utils
74
- await nodeComfort.wait(500);
75
- nodeComfort.dontCrash();
76
-
77
-
78
- // SQLite
79
- const db = new nodeComfort.SQLite();
80
- await db.createTable({
81
- name: "users",
82
- columns: {
83
- id: { type: "INTEGER", primaryKey: true, autoincrement: true },
84
- name: { type: "TEXT", notNull: true },
85
- },
86
- });
87
- ```
88
-
89
-
90
- ---
91
-
92
-
93
- ## Logger
94
-
95
-
96
- Colorized console logger with a minimal markup syntax and optional timestamps.
97
-
98
-
99
- You wrap styles in delimiters (by default `<%` and `%>`):
100
-
101
-
102
- - styles: `bold`, `italic`, `underline`, `overline`
103
- - colors: `red`, `green`, `yellow`, `blue`, `magenta`, `cyan`, `white`, `gray`, `black`
104
- - bright: `redBright`, `greenBright`, `yellowBright`, `blueBright`, `magentaBright`, `cyanBright`, `whiteBright`
105
- - background: `bgRed`, `bgBlue`, etc.
106
- - RGB: `rgb(255, 0, 0)`
107
-
108
-
109
- ### Basic usage
110
-
111
-
112
- ```js
113
- nodeComfort.log("Plain log");
114
- nodeComfort.log("<% green Success %>");
115
- nodeComfort.log("<% red bold Error:%> something went wrong");
116
- nodeComfort.log("<% bgBlue white INFO %> message");
117
- ```
118
-
119
-
120
- Multi-line strings keep indentation and timestamps readable:
121
-
122
-
123
- ```js
124
- nodeComfort.log(`
125
- <% yellow Multi-line %>
126
- This is line 2
127
- This is line 3
128
- `);
129
- ```
130
-
131
-
132
- ### Timestamp and delimiters
133
-
134
-
135
- ```js
136
- // Disable timestamps
137
- nodeComfort.setTimestamp(false);
138
-
139
-
140
- // Change delimiters if "<%" conflicts with other template syntax
141
- nodeComfort.setDelimiter({ open: "{{", close: "}}" });
142
- nodeComfort.log("{{red Hello}} world");
143
- ```
144
-
145
-
146
- ### Groups
147
-
148
-
149
- Groups add indentation to related logs.
150
-
151
-
152
- ```js
153
- nodeComfort.group("Startup");
154
-
155
-
156
- nodeComfort.log("Loading config…");
157
- nodeComfort.log("<% green OK %>");
158
-
159
-
160
- nodeComfort.group("DB");
161
- nodeComfort.log("Connecting…");
162
- // ...
163
- nodeComfort.groupEnd(); // end "DB"
164
-
165
-
166
- nodeComfort.groupEnd(); // end "Startup"
167
- ```
168
-
169
-
170
- ---
171
-
172
-
173
- ## FS
174
-
175
-
176
- File-system helper with safe path resolution, recursive walking, copy/move helpers, and a small watcher abstraction.
177
-
178
-
179
- It resolves relative paths from the **caller file**, not just from `process.cwd()`, which makes it nicer inside libraries.
180
-
181
-
182
- ### Path helpers
183
-
184
-
185
- ```js
186
- // Absolute path, even if it doesn't exist yet
187
- const configPath = nodeComfort.createPath("./config/app.json");
188
-
189
-
190
- // Existing folder/file or undefined
191
- const folder = nodeComfort.getFolder("./src");
192
- const file = nodeComfort.getFile("package.json");
193
- ```
194
-
195
-
196
- ### Create folders and files
197
-
198
-
199
- ```js
200
- // Folders
201
- nodeComfort.createFolder("./dist"); // true if created
202
- nodeComfort.createFolder("./dist", true); // force recreate
203
-
204
-
205
- // Files
206
- nodeComfort.createFile("./dist/info.txt", false, "hello");
207
- nodeComfort.createFile("./dist/data.json", true, { ok: true });
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
+ # @ix-xs/node-comfort
2
+
3
+ <div align="center">
4
+
5
+ ![Node.js](https://img.shields.io/badge/node.js-6DA55F?style=for-the-badge&logo=node.js&logoColor=white)
6
+ ![JavaScript](https://img.shields.io/badge/javascript-%23323330.svg?style=for-the-badge&logo=javascript&logoColor=%23F7DF1E)
7
+ ![NPM](https://img.shields.io/badge/NPM-%23CB3837.svg?style=for-the-badge&logo=npm&logoColor=white)
8
+ ![Github](https://img.shields.io/badge/github-%23121011.svg?style=for-the-badge&logo=github&logoColor=white)
9
+
10
+ [![npm version](https://badge.fury.io/js/%40ix-xs%2Fnode-comfort.svg)](https://www.npmjs.com/package/@ix-xs/node-comfort)
11
+ [![Downloads](https://img.shields.io/npm/dm/@ix-xs/node-comfort.svg)](https://www.npmjs.com/package/@ix-xs/node-comfort)
12
+ [![Zero dependencies](https://img.shields.io/badge/dependencies-0-brightgreen)](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>&"'); // "&lt;a&gt;&amp;&quot;"
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