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