@ix-xs/node-comfort 1.1.1 → 1.1.3

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.
Files changed (3) hide show
  1. package/README.md +265 -50
  2. package/package.json +2 -2
  3. package/src/SQLite.js +719 -174
package/README.md CHANGED
@@ -1,13 +1,34 @@
1
1
  # @ix-xs/node-comfort
2
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
+
3
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.
4
21
 
5
- Everything is plain CommonJS with JSDoc, and ships with `.d.ts` so you get autocomplete in TypeScript and modern editors.
22
+
23
+ Everything is plain CommonJS with JSDoc. The package ships with type information for TypeScript and modern editors.
24
+
6
25
 
7
26
  ---
8
27
 
28
+
9
29
  ## Installation
10
30
 
31
+
11
32
  ```bash
12
33
  npm install @ix-xs/node-comfort
13
34
  # or
@@ -16,61 +37,78 @@ yarn add @ix-xs/node-comfort
16
37
  pnpm add @ix-xs/node-comfort
17
38
  ```
18
39
 
40
+
19
41
  Requires Node 20+ (uses `node:sqlite` and `process.loadEnvFile`).
20
42
 
43
+
21
44
  ---
22
45
 
46
+
23
47
  ## Import and global usage
24
48
 
25
- The package exposes a **flat API**: all methods are directly on the `nodeComfort` object.
49
+
50
+ The package exposes a **flat API**: all utilities are directly on the `nodeComfort` object.
51
+
26
52
 
27
53
  ```js
28
54
  const nodeComfort = require("@ix-xs/node-comfort");
29
55
 
56
+
30
57
  // Logger
31
58
  nodeComfort.log("Hello");
32
59
  nodeComfort.log("<% greenBright Success %>");
33
60
 
61
+
34
62
  // FS
35
63
  const files = nodeComfort.getFilesIn(".", true);
36
64
  nodeComfort.createFolder("./dist");
37
65
 
66
+
38
67
  // Checker
39
68
  if (nodeComfort.isArray(files)) {
40
69
  nodeComfort.log(`<% cyan Found ${files.length} files %>`);
41
70
  }
42
71
 
72
+
43
73
  // Utils
44
74
  await nodeComfort.wait(500);
45
75
  nodeComfort.dontCrash();
46
76
 
77
+
47
78
  // SQLite
48
79
  const db = new nodeComfort.SQLite();
49
80
  await db.createTable({
50
81
  name: "users",
51
- options: {
82
+ columns: {
52
83
  id: { type: "INTEGER", primaryKey: true, autoincrement: true },
53
84
  name: { type: "TEXT", notNull: true },
54
85
  },
55
86
  });
56
87
  ```
57
88
 
89
+
58
90
  ---
59
91
 
92
+
60
93
  ## Logger
61
94
 
95
+
62
96
  Colorized console logger with a minimal markup syntax and optional timestamps.
63
97
 
98
+
64
99
  You wrap styles in delimiters (by default `<%` and `%>`):
65
100
 
101
+
66
102
  - styles: `bold`, `italic`, `underline`, `overline`
67
103
  - colors: `red`, `green`, `yellow`, `blue`, `magenta`, `cyan`, `white`, `gray`, `black`
68
104
  - bright: `redBright`, `greenBright`, `yellowBright`, `blueBright`, `magentaBright`, `cyanBright`, `whiteBright`
69
105
  - background: `bgRed`, `bgBlue`, etc.
70
106
  - RGB: `rgb(255, 0, 0)`
71
107
 
108
+
72
109
  ### Basic usage
73
110
 
111
+
74
112
  ```js
75
113
  nodeComfort.log("Plain log");
76
114
  nodeComfort.log("<% green Success %>");
@@ -78,8 +116,10 @@ nodeComfort.log("<% red bold Error:%> something went wrong");
78
116
  nodeComfort.log("<% bgBlue white INFO %> message");
79
117
  ```
80
118
 
119
+
81
120
  Multi-line strings keep indentation and timestamps readable:
82
121
 
122
+
83
123
  ```js
84
124
  nodeComfort.log(`
85
125
  <% yellow Multi-line %>
@@ -88,88 +128,114 @@ This is line 3
88
128
  `);
89
129
  ```
90
130
 
131
+
91
132
  ### Timestamp and delimiters
92
133
 
134
+
93
135
  ```js
94
136
  // Disable timestamps
95
137
  nodeComfort.setTimestamp(false);
96
138
 
139
+
97
140
  // Change delimiters if "<%" conflicts with other template syntax
98
141
  nodeComfort.setDelimiter({ open: "{{", close: "}}" });
99
142
  nodeComfort.log("{{red Hello}} world");
100
143
  ```
101
144
 
145
+
102
146
  ### Groups
103
147
 
148
+
104
149
  Groups add indentation to related logs.
105
150
 
151
+
106
152
  ```js
107
153
  nodeComfort.group("Startup");
108
154
 
155
+
109
156
  nodeComfort.log("Loading config…");
110
157
  nodeComfort.log("<% green OK %>");
111
158
 
159
+
112
160
  nodeComfort.group("DB");
113
161
  nodeComfort.log("Connecting…");
114
162
  // ...
115
163
  nodeComfort.groupEnd(); // end "DB"
116
164
 
165
+
117
166
  nodeComfort.groupEnd(); // end "Startup"
118
167
  ```
119
168
 
169
+
120
170
  ---
121
171
 
172
+
122
173
  ## FS
123
174
 
175
+
124
176
  File-system helper with safe path resolution, recursive walking, copy/move helpers, and a small watcher abstraction.
125
177
 
178
+
126
179
  It resolves relative paths from the **caller file**, not just from `process.cwd()`, which makes it nicer inside libraries.
127
180
 
181
+
128
182
  ### Path helpers
129
183
 
184
+
130
185
  ```js
131
186
  // Absolute path, even if it doesn't exist yet
132
187
  const configPath = nodeComfort.createPath("./config/app.json");
133
188
 
189
+
134
190
  // Existing folder/file or undefined
135
191
  const folder = nodeComfort.getFolder("./src");
136
192
  const file = nodeComfort.getFile("package.json");
137
193
  ```
138
194
 
195
+
139
196
  ### Create folders and files
140
197
 
198
+
141
199
  ```js
142
200
  // Folders
143
201
  nodeComfort.createFolder("./dist"); // true if created
144
202
  nodeComfort.createFolder("./dist", true); // force recreate
145
203
 
204
+
146
205
  // Files
147
206
  nodeComfort.createFile("./dist/info.txt", false, "hello");
148
207
  nodeComfort.createFile("./dist/data.json", true, { ok: true });
149
208
  ```
150
209
 
210
+
151
211
  ### List and delete
152
212
 
213
+
153
214
  ```js
154
215
  // Folders inside a path
155
216
  const dirs = nodeComfort.getFoldersIn("./src", true);
156
217
 
218
+
157
219
  // Delete folders matching a filter
158
220
  nodeComfort.deleteFoldersIn("./tmp", (folder) =>
159
221
  folder.endsWith(".cache"),
160
222
  );
161
223
 
224
+
162
225
  // Files
163
226
  const files = nodeComfort.getFilesIn("./src", true);
164
227
 
228
+
165
229
  // Delete files matching a filter
166
230
  nodeComfort.deleteFilesIn("./logs", false, (file) =>
167
231
  file.endsWith(".log"),
168
232
  );
169
233
  ```
170
234
 
235
+
171
236
  ### Copy
172
237
 
238
+
173
239
  ```js
174
240
  // Copy one folder
175
241
  nodeComfort.copyFolder({
@@ -180,6 +246,7 @@ nodeComfort.copyFolder({
180
246
  force: true,
181
247
  });
182
248
 
249
+
183
250
  // Copy only some folders in a tree
184
251
  nodeComfort.copyFoldersIn({
185
252
  path: "./src",
@@ -189,6 +256,7 @@ nodeComfort.copyFoldersIn({
189
256
  filter: (folder) => folder.endsWith("components"),
190
257
  });
191
258
 
259
+
192
260
  // Copy files
193
261
  nodeComfort.copyFilesIn({
194
262
  path: "./src",
@@ -198,6 +266,7 @@ nodeComfort.copyFilesIn({
198
266
  filter: (file) => file.endsWith(".js"),
199
267
  });
200
268
 
269
+
201
270
  // Copy a single file
202
271
  nodeComfort.copyFile({
203
272
  path: "./src/index.js",
@@ -206,10 +275,13 @@ nodeComfort.copyFile({
206
275
  });
207
276
  ```
208
277
 
278
+
209
279
  ### Move
210
280
 
281
+
211
282
  Move helpers are basically copy + delete.
212
283
 
284
+
213
285
  ```js
214
286
  // Move a folder tree
215
287
  nodeComfort.moveFolder({
@@ -220,6 +292,7 @@ nodeComfort.moveFolder({
220
292
  force: true,
221
293
  });
222
294
 
295
+
223
296
  // Move some folders under a path
224
297
  nodeComfort.moveFoldersIn({
225
298
  path: "./tmp",
@@ -229,6 +302,7 @@ nodeComfort.moveFoldersIn({
229
302
  filter: (folder) => folder.includes("session"),
230
303
  });
231
304
 
305
+
232
306
  // Move files
233
307
  nodeComfort.moveFilesIn({
234
308
  path: "./logs",
@@ -238,6 +312,7 @@ nodeComfort.moveFilesIn({
238
312
  filter: (file) => file.endsWith(".log"),
239
313
  });
240
314
 
315
+
241
316
  // Move a single file
242
317
  nodeComfort.moveFile({
243
318
  path: "./logs/app.log",
@@ -246,12 +321,15 @@ nodeComfort.moveFile({
246
321
  });
247
322
  ```
248
323
 
324
+
249
325
  ### Read and watch
250
326
 
327
+
251
328
  ```js
252
329
  // Read a UTF-8 file
253
330
  const content = nodeComfort.readFile("./README.md");
254
331
 
332
+
255
333
  // Watch a directory
256
334
  const watcher = nodeComfort.watch({
257
335
  path: "./src",
@@ -259,6 +337,7 @@ const watcher = nodeComfort.watch({
259
337
  filter: (event, file) => file.endsWith(".js"),
260
338
  });
261
339
 
340
+
262
341
  watcher
263
342
  .on("change", (file) => {
264
343
  nodeComfort.log(`<% cyan File changed: %> ${file}`);
@@ -268,8 +347,10 @@ watcher
268
347
  });
269
348
  ```
270
349
 
350
+
271
351
  You can pause, resume and stop watching:
272
352
 
353
+
273
354
  ```js
274
355
  watcher.pause();
275
356
  // ...
@@ -278,56 +359,74 @@ watcher.resume();
278
359
  watcher.stop();
279
360
  ```
280
361
 
362
+
281
363
  ---
282
364
 
365
+
283
366
  ## Checker
284
367
 
368
+
285
369
  Runtime type-checking helpers. Everything is just a boolean guard that works on any input.
286
370
 
371
+
287
372
  ```js
288
373
  nodeComfort.isArray(); // true[1][2]
289
- nodeComfort.isArray("nope"); // false
374
+ nodeComfort.isArray("nope"); // false
375
+
290
376
 
291
- nodeComfort.isNumber(42); // true
292
- nodeComfort.isNumber(NaN); // false
377
+ nodeComfort.isNumber(42); // true
378
+ nodeComfort.isNumber(NaN); // false
293
379
 
294
- nodeComfort.isFunction(() => {}); // true
295
- nodeComfort.isFunction(class {}); // true
296
380
 
297
- nodeComfort.isObject({}); // true
298
- nodeComfort.isObject([]); // true
299
- nodeComfort.isObject(null); // false
381
+ nodeComfort.isFunction(() => {}); // true
382
+ nodeComfort.isFunction(class {}); // true
300
383
 
301
- nodeComfort.isBoolean(false); // true
302
- nodeComfort.isString("hello"); // true
303
- nodeComfort.isUndefined(undefined); // true
304
- nodeComfort.isNull(null); // true
305
384
 
306
- nodeComfort.isDate(new Date()); // true
307
- nodeComfort.isMap(new Map()); // true
308
- nodeComfort.isSet(new Set()); // true
385
+ nodeComfort.isObject({}); // true
386
+ nodeComfort.isObject([]); // true
387
+ nodeComfort.isObject(null); // false
309
388
 
310
- nodeComfort.isError(new Error("x")); // true
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
311
402
  nodeComfort.isError({ message: "x" }); // false
312
403
  ```
313
404
 
405
+
314
406
  ---
315
407
 
408
+
316
409
  ## Utils
317
410
 
411
+
318
412
  Small async and process helpers.
319
413
 
414
+
320
415
  ### wait(ms)
321
416
 
417
+
322
418
  ```js
323
419
  await nodeComfort.wait(500);
324
420
  // code here runs after ~500ms
325
421
  ```
326
422
 
423
+
327
424
  ### when(predicate, payload?, options?)
328
425
 
426
+
329
427
  Polling helper that emits events until a condition is met.
330
428
 
429
+
331
430
  ```js
332
431
  const task = nodeComfort.when(
333
432
  () => Math.random() > 0.8,
@@ -335,6 +434,7 @@ const task = nodeComfort.when(
335
434
  { interval: 200, max: 3, timeout: 5000 },
336
435
  );
337
436
 
437
+
338
438
  task
339
439
  .on("trigger", (payload) => {
340
440
  nodeComfort.log(`<% green Triggered: %> ${payload.label}`);
@@ -348,6 +448,7 @@ task
348
448
  .start();
349
449
  ```
350
450
 
451
+
351
452
  - `predicate`: boolean, promise, or function (sync/async) returning a boolean.
352
453
  - `payload`: value passed to listeners on `trigger` / `timeout`.
353
454
  - `options`:
@@ -355,13 +456,17 @@ task
355
456
  - `timeout` (ms or `null`)
356
457
  - `max` (number of triggers or `null` for infinite)
357
458
 
459
+
358
460
  ### dontCrash()
359
461
 
462
+
360
463
  Installs process-level safety nets for errors, exits and signals.
361
464
 
465
+
362
466
  ```js
363
467
  nodeComfort.dontCrash();
364
468
 
469
+
365
470
  // Or customize:
366
471
  nodeComfort.dontCrash()
367
472
  .on("error", (err) => {
@@ -377,46 +482,60 @@ nodeComfort.dontCrash()
377
482
  });
378
483
  ```
379
484
 
485
+
380
486
  Events:
381
487
 
488
+
382
489
  - `"error"` – uncaught exceptions + unhandled rejections
383
490
  - `"exit"` – `process.on("exit")`
384
491
  - `"sig"` – `SIGINT`, `SIGTERM`, `SIGQUIT`
385
492
  - `"beforeExit"` – `process.on("beforeExit")`
386
493
 
494
+
387
495
  ### JSON helpers
388
496
 
497
+
389
498
  ```js
390
499
  const json = nodeComfort.JSONString({ hello: "world" });
391
500
  // pretty-printed with 4 spaces
392
501
 
502
+
393
503
  const data = nodeComfort.JSONParse(json);
394
504
  ```
395
505
 
506
+
396
507
  ---
397
508
 
509
+
398
510
  ## SQLite
399
511
 
400
- Thin wrapper over the built-in `node:sqlite` `DatabaseSync` with a small “model” system and a couple of CRUD-style helpers.
512
+
513
+ Thin wrapper over the built-in `node:sqlite` `DatabaseSync` with a small "model" system and CRUD-style helpers.
514
+
401
515
 
402
516
  ### Creating an instance
403
517
 
518
+
404
519
  ```js
405
520
  // Default: ./db.sqlite (relative to cwd)
406
521
  const db = new nodeComfort.SQLite();
407
522
 
523
+
408
524
  // Custom path (folder created if needed)
409
525
  const db2 = new nodeComfort.SQLite("data/app.sqlite");
410
526
  ```
411
527
 
528
+
412
529
  The constructor ensures the parent folder exists, and enables WAL journal mode and foreign keys by default.
413
530
 
531
+
414
532
  ### Defining a table
415
533
 
534
+
416
535
  ```js
417
536
  await db.createTable({
418
537
  name: "users",
419
- options: {
538
+ columns: {
420
539
  id: {
421
540
  type: "INTEGER",
422
541
  primaryKey: true,
@@ -439,95 +558,191 @@ await db.createTable({
439
558
  });
440
559
  ```
441
560
 
561
+
442
562
  Models are kept in memory and used for defaults like ordering.
443
563
 
444
- ### Insert / update
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
+
445
592
 
446
593
  ```js
447
594
  // Insert
448
595
  await db.set("users", { name: "John" });
449
596
 
597
+
450
598
  // Upsert by id
451
599
  await db.set("users", { id: 1, role: "admin" });
452
600
 
601
+
453
602
  // Upsert with custom where
454
603
  await db.set("users", { role: "user" }, { name: "John" });
455
604
  ```
456
605
 
457
- When `where` or `data.id` is provided, `set`:
458
-
459
- 1. loads the existing row if it exists,
460
- 2. merges it with `data`,
461
- 3. runs `INSERT OR REPLACE`.
462
606
 
463
607
  ### Get / list
464
608
 
609
+
465
610
  ```js
466
611
  // One row or undefined
467
612
  const user = await db.get("users", { name: "John" });
468
613
 
469
- // Multiple rows
614
+
615
+ // Multiple rows with pagination and ordering
470
616
  const admins = await db.getAll(
471
617
  "users",
472
618
  { role: "admin" }, // where
473
- 100, // limit
474
- 0, // offset
619
+ {
620
+ limit: 100,
621
+ offset: 0,
622
+ orderBy: "createdAt",
623
+ direction: "DESC",
624
+ },
475
625
  );
476
626
  ```
477
627
 
628
+
478
629
  If the table is not registered, `getAll` returns an empty array.
479
630
 
631
+
480
632
  ### Delete / clear / count
481
633
 
634
+
482
635
  ```js
483
636
  // Delete rows
484
637
  const res = await db.delete("users", { role: "user" });
485
638
  // res.ok, res.changes
486
639
 
640
+
487
641
  // Delete all rows
488
642
  await db.clearTable("users");
489
643
 
644
+
490
645
  // Count rows
491
646
  const totalUsers = await db.count("users");
492
647
  const adminCount = await db.count("users", { role: "admin" });
493
648
  ```
494
649
 
495
- ### Drop and close
650
+
651
+ ### Indexes and constraints
652
+
496
653
 
497
654
  ```js
498
- await db.deleteTable("users");
499
- db.close();
500
- ```
655
+ // Create a composite unique index
656
+ await db.createIndex("UserAuthMethod", {
657
+ columns: ["provider", "providerUserId"],
658
+ unique: true,
659
+ });
501
660
 
502
- ---
503
661
 
504
- ## TypeScript / editor support
662
+ // Create a non-unique index
663
+ await db.createIndex("Post", {
664
+ columns: ["userId"],
665
+ });
666
+ ```
505
667
 
506
- The library is written in JavaScript with JSDoc and compiled to `.d.ts` using `tsconfig.json` (JS input, declaration output only).
507
668
 
508
- You can still use it comfortably from TypeScript:
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
+ ```
509
693
 
510
- ```ts
511
- import nodeComfort = require("@ix-xs/node-comfort");
512
694
 
513
- nodeComfort.log("<% green Type-safe logging %>");
695
+ ### Transactions
514
696
 
515
- const files = nodeComfort.getFilesIn("./src", true);
516
697
 
517
- const db = new nodeComfort.SQLite();
518
- await db.createTable({
519
- name: "sessions",
520
- options: {
521
- id: { type: "TEXT", primaryKey: true },
522
- data: { type: "TEXT" },
523
- },
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 };
524
709
  });
525
710
  ```
526
711
 
527
- Methods are available directly on the default export (no nested namespaces).
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
+
528
741
 
529
742
  ---
530
743
 
744
+
531
745
  ## License
532
746
 
747
+
533
748
  MIT.