@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/src/Obj.js ADDED
@@ -0,0 +1,421 @@
1
+ /**
2
+ * Object utilities.
3
+ *
4
+ * Deep clone / merge / equality, safe nested get/set via dot-paths,
5
+ * pick/omit, mapping over keys and values, and flatten/unflatten • the
6
+ * everyday object plumbing, done without mutating your inputs.
7
+ */
8
+
9
+ const checker = require("./Checker");
10
+
11
+ /**
12
+ * Parses a dot/bracket path into an array of keys.
13
+ * @private
14
+ * @param {string|Array<string|number>} path
15
+ * @returns {Array<string|number>}
16
+ */
17
+ const _toPath = (path) => {
18
+ if (Array.isArray(path)) return path;
19
+ return String(path)
20
+ .replace(/\[(\d+)\]/g, ".$1")
21
+ .split(".")
22
+ .filter((p) => p !== "");
23
+ };
24
+
25
+ /**
26
+ * Manual deep-clone used when `structuredClone` is unavailable or throws
27
+ * (e.g. objects containing functions). Handles arrays, Maps, Sets, Dates and
28
+ * RegExps, and guards against circular references.
29
+ * @private
30
+ * @param {*} value - Value to clone.
31
+ * @param {WeakMap} seen - Cache of already-cloned references.
32
+ * @returns {*} The cloned value.
33
+ */
34
+ const _deepClone = (value, seen) => {
35
+ if (value === null || typeof value !== "object") return value;
36
+ if (seen.has(value)) return seen.get(value);
37
+ if (checker.isDate(value)) return new Date(value.getTime());
38
+ if (checker.isRegExp(value)) return new RegExp(value.source, value.flags);
39
+
40
+ if (Array.isArray(value)) {
41
+ const arr = [];
42
+ seen.set(value, arr);
43
+ for (const item of value) arr.push(_deepClone(item, seen));
44
+ return arr;
45
+ }
46
+ if (checker.isMap(value)) {
47
+ const map = new Map();
48
+ seen.set(value, map);
49
+ for (const [k, v] of value) map.set(k, _deepClone(v, seen));
50
+ return map;
51
+ }
52
+ if (checker.isSet(value)) {
53
+ const set = new Set();
54
+ seen.set(value, set);
55
+ for (const v of value) set.add(_deepClone(v, seen));
56
+ return set;
57
+ }
58
+
59
+ const out = {};
60
+ seen.set(value, out);
61
+ for (const key of Object.keys(value)) {
62
+ out[key] = _deepClone(value[key], seen);
63
+ }
64
+ return out;
65
+ };
66
+
67
+ module.exports = {
68
+ /**
69
+ * Deeply clones a value.
70
+ *
71
+ * Uses the native `structuredClone` when the value supports it (plain data,
72
+ * Dates, Maps, Sets, typed arrays, ...), and falls back to a manual deep copy
73
+ * otherwise. Functions are copied by reference.
74
+ *
75
+ * @example
76
+ * const copy = nodeComfort.obj.clone({ a: { b: 1 } });
77
+ *
78
+ * @template T
79
+ * @param {T} value - Value to clone.
80
+ * @returns {T} A deep copy.
81
+ */
82
+ clone(value) {
83
+ if (value === null || typeof value !== "object") return value;
84
+ try {
85
+ return structuredClone(value);
86
+ } catch {
87
+ return _deepClone(value, new WeakMap());
88
+ }
89
+ },
90
+
91
+ /**
92
+ * Deeply merges two or more plain objects into a new object.
93
+ *
94
+ * Later sources win. Plain-object values are merged recursively; arrays and
95
+ * other values are replaced (not concatenated). Inputs are never mutated.
96
+ *
97
+ * @example
98
+ * nodeComfort.obj.merge({ a: { x: 1 } }, { a: { y: 2 } });
99
+ * // { a: { x: 1, y: 2 } }
100
+ *
101
+ * @param {...Record<string, any>} sources - Objects to merge.
102
+ * @returns {Record<string, any>} The merged object.
103
+ */
104
+ merge(...sources) {
105
+ const result = {};
106
+ for (const source of sources) {
107
+ if (!checker.isPlainObject(source)) continue;
108
+ for (const key of Object.keys(source)) {
109
+ const value = source[key];
110
+ if (checker.isPlainObject(value) && checker.isPlainObject(result[key])) {
111
+ result[key] = this.merge(result[key], value);
112
+ } else if (checker.isPlainObject(value)) {
113
+ result[key] = this.merge(value);
114
+ } else {
115
+ result[key] = value;
116
+ }
117
+ }
118
+ }
119
+ return result;
120
+ },
121
+
122
+ /**
123
+ * Deeply compares two values for structural equality.
124
+ *
125
+ * Handles plain objects, arrays, Dates, RegExps, Maps and Sets.
126
+ *
127
+ * @example
128
+ * nodeComfort.obj.equal({ a: [1, 2] }, { a: [1, 2] }); // true
129
+ *
130
+ * @param {*} a - First value.
131
+ * @param {*} b - Second value.
132
+ * @returns {boolean} True if the values are deeply equal.
133
+ */
134
+ equal(a, b) {
135
+ if (a === b) return true;
136
+ if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) {
137
+ return a !== a && b !== b; // both NaN
138
+ }
139
+ if (checker.isDate(a) && checker.isDate(b)) return a.getTime() === b.getTime();
140
+ if (checker.isRegExp(a) && checker.isRegExp(b)) return a.toString() === b.toString();
141
+
142
+ if (Array.isArray(a) || Array.isArray(b)) {
143
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
144
+ return a.every((item, i) => this.equal(item, b[i]));
145
+ }
146
+ if (checker.isMap(a) && checker.isMap(b)) {
147
+ if (a.size !== b.size) return false;
148
+ for (const [k, v] of a) {
149
+ if (!b.has(k) || !this.equal(v, b.get(k))) return false;
150
+ }
151
+ return true;
152
+ }
153
+ if (checker.isSet(a) && checker.isSet(b)) {
154
+ if (a.size !== b.size) return false;
155
+ for (const v of a) if (!b.has(v)) return false;
156
+ return true;
157
+ }
158
+
159
+ const keysA = Object.keys(a);
160
+ const keysB = Object.keys(b);
161
+ if (keysA.length !== keysB.length) return false;
162
+ return keysA.every((key) => Object.hasOwn(b, key) && this.equal(a[key], b[key]));
163
+ },
164
+
165
+ /**
166
+ * Gets a nested value from an object using a dot/bracket path.
167
+ *
168
+ * @example
169
+ * nodeComfort.obj.get({ a: { b: [{ c: 42 }] } }, "a.b[0].c"); // 42
170
+ * nodeComfort.obj.get({}, "a.b.c", "default"); // "default"
171
+ *
172
+ * @param {Record<string, any>} object - Source object.
173
+ * @param {string|Array<string|number>} path - Dot path or key array.
174
+ * @param {*} [fallback] - Returned when the path does not resolve.
175
+ * @returns {*} The value at the path, or the fallback.
176
+ */
177
+ get(object, path, fallback) {
178
+ const keys = _toPath(path);
179
+ let current = object;
180
+ for (const key of keys) {
181
+ if (current == null || !(key in Object(current))) return fallback;
182
+ current = current[key];
183
+ }
184
+ return current === undefined ? fallback : current;
185
+ },
186
+
187
+ /**
188
+ * Sets a nested value in an object using a dot/bracket path (returns a clone).
189
+ *
190
+ * Intermediate objects/arrays are created as needed. The original object is
191
+ * not mutated.
192
+ *
193
+ * @example
194
+ * nodeComfort.obj.set({}, "a.b.c", 1); // { a: { b: { c: 1 } } }
195
+ *
196
+ * @param {Record<string, any>} object - Source object.
197
+ * @param {string|Array<string|number>} path - Dot path or key array.
198
+ * @param {*} value - Value to set.
199
+ * @returns {Record<string, any>} A new object with the value set.
200
+ */
201
+ set(object, path, value) {
202
+ const keys = _toPath(path);
203
+ if (!keys.length) return object;
204
+ const root = this.clone(object) ?? {};
205
+ let current = root;
206
+ for (let i = 0; i < keys.length - 1; i++) {
207
+ const key = keys[i];
208
+ if (current[key] == null || typeof current[key] !== "object") {
209
+ current[key] = typeof keys[i + 1] === "number" || /^\d+$/.test(String(keys[i + 1])) ? [] : {};
210
+ }
211
+ current = current[key];
212
+ }
213
+ current[keys[keys.length - 1]] = value;
214
+ return root;
215
+ },
216
+
217
+ /**
218
+ * Checks whether a nested path exists in an object.
219
+ *
220
+ * @param {Record<string, any>} object - Source object.
221
+ * @param {string|Array<string|number>} path - Dot path or key array.
222
+ * @returns {boolean} True if the path resolves.
223
+ */
224
+ has(object, path) {
225
+ const keys = _toPath(path);
226
+ let current = object;
227
+ for (const key of keys) {
228
+ if (current == null || !(key in Object(current))) return false;
229
+ current = current[key];
230
+ }
231
+ return true;
232
+ },
233
+
234
+ /**
235
+ * Returns a new object containing only the given keys.
236
+ *
237
+ * @example
238
+ * nodeComfort.obj.pick({ a: 1, b: 2, c: 3 }, ["a", "c"]); // { a: 1, c: 3 }
239
+ *
240
+ * @param {Record<string, any>} object - Source object.
241
+ * @param {string[]} keys - Keys to keep.
242
+ * @returns {Record<string, any>} The picked object.
243
+ */
244
+ pick(object, keys) {
245
+ const result = {};
246
+ for (const key of keys) {
247
+ if (object != null && Object.hasOwn(object, key)) result[key] = object[key];
248
+ }
249
+ return result;
250
+ },
251
+
252
+ /**
253
+ * Returns a new object without the given keys.
254
+ *
255
+ * @example
256
+ * nodeComfort.obj.omit({ a: 1, b: 2, c: 3 }, ["b"]); // { a: 1, c: 3 }
257
+ *
258
+ * @param {Record<string, any>} object - Source object.
259
+ * @param {string[]} keys - Keys to remove.
260
+ * @returns {Record<string, any>} The remaining object.
261
+ */
262
+ omit(object, keys) {
263
+ const exclude = new Set(keys);
264
+ const result = {};
265
+ for (const key of Object.keys(object ?? {})) {
266
+ if (!exclude.has(key)) result[key] = object[key];
267
+ }
268
+ return result;
269
+ },
270
+
271
+ /**
272
+ * Returns a new object keeping only the entries that pass a predicate.
273
+ *
274
+ * @example
275
+ * nodeComfort.obj.filter({ a: 1, b: 2, c: 3 }, (v) => v > 1); // { b: 2, c: 3 }
276
+ *
277
+ * @param {Record<string, any>} object - Source object.
278
+ * @param {(value: any, key: string) => boolean} predicate - Test function.
279
+ * @returns {Record<string, any>} The filtered object.
280
+ */
281
+ filter(object, predicate) {
282
+ const result = {};
283
+ for (const [key, value] of Object.entries(object ?? {})) {
284
+ if (predicate(value, key)) result[key] = value;
285
+ }
286
+ return result;
287
+ },
288
+
289
+ /**
290
+ * Maps over an object's values, returning a new object with the same keys.
291
+ *
292
+ * @example
293
+ * nodeComfort.obj.mapValues({ a: 1, b: 2 }, (v) => v * 10); // { a: 10, b: 20 }
294
+ *
295
+ * @param {Record<string, any>} object - Source object.
296
+ * @param {(value: any, key: string) => any} fn - Mapping function.
297
+ * @returns {Record<string, any>} The mapped object.
298
+ */
299
+ mapValues(object, fn) {
300
+ const result = {};
301
+ for (const [key, value] of Object.entries(object ?? {})) {
302
+ result[key] = fn(value, key);
303
+ }
304
+ return result;
305
+ },
306
+
307
+ /**
308
+ * Maps over an object's keys, returning a new object with remapped keys.
309
+ *
310
+ * @example
311
+ * nodeComfort.obj.mapKeys({ a: 1 }, (k) => k.toUpperCase()); // { A: 1 }
312
+ *
313
+ * @param {Record<string, any>} object - Source object.
314
+ * @param {(key: string, value: any) => string} fn - Key-mapping function.
315
+ * @returns {Record<string, any>} The remapped object.
316
+ */
317
+ mapKeys(object, fn) {
318
+ const result = {};
319
+ for (const [key, value] of Object.entries(object ?? {})) {
320
+ result[fn(key, value)] = value;
321
+ }
322
+ return result;
323
+ },
324
+
325
+ /**
326
+ * Swaps keys and values of an object.
327
+ *
328
+ * @example
329
+ * nodeComfort.obj.invert({ a: "x", b: "y" }); // { x: "a", y: "b" }
330
+ *
331
+ * @param {Record<string, any>} object - Source object.
332
+ * @returns {Record<string, string>} The inverted object.
333
+ */
334
+ invert(object) {
335
+ const result = {};
336
+ for (const [key, value] of Object.entries(object ?? {})) {
337
+ result[value] = key;
338
+ }
339
+ return result;
340
+ },
341
+
342
+ /**
343
+ * Removes entries whose value is `null` or `undefined` (shallow).
344
+ *
345
+ * @example
346
+ * nodeComfort.obj.compact({ a: 1, b: null, c: undefined }); // { a: 1 }
347
+ *
348
+ * @param {Record<string, any>} object - Source object.
349
+ * @returns {Record<string, any>} The compacted object.
350
+ */
351
+ compact(object) {
352
+ return this.filter(object, (v) => v !== null && v !== undefined);
353
+ },
354
+
355
+ /**
356
+ * Flattens a nested object into a single-level object with dot-path keys.
357
+ *
358
+ * @example
359
+ * nodeComfort.obj.flatten({ a: { b: { c: 1 } } }); // { "a.b.c": 1 }
360
+ *
361
+ * @param {Record<string, any>} object - Source object.
362
+ * @param {string} [prefix=""] - Prefix for keys (used internally).
363
+ * @returns {Record<string, any>} The flattened object.
364
+ */
365
+ flatten(object, prefix = "") {
366
+ const result = {};
367
+ for (const [key, value] of Object.entries(object ?? {})) {
368
+ const path = prefix ? `${prefix}.${key}` : key;
369
+ if (checker.isPlainObject(value) && Object.keys(value).length) {
370
+ Object.assign(result, this.flatten(value, path));
371
+ } else {
372
+ result[path] = value;
373
+ }
374
+ }
375
+ return result;
376
+ },
377
+
378
+ /**
379
+ * Rebuilds a nested object from a flat, dot-path keyed object.
380
+ *
381
+ * @example
382
+ * nodeComfort.obj.unflatten({ "a.b.c": 1 }); // { a: { b: { c: 1 } } }
383
+ *
384
+ * @param {Record<string, any>} object - Flat object.
385
+ * @returns {Record<string, any>} The nested object.
386
+ */
387
+ unflatten(object) {
388
+ let result = {};
389
+ for (const [path, value] of Object.entries(object ?? {})) {
390
+ result = this.set(result, path, value);
391
+ }
392
+ return result;
393
+ },
394
+
395
+ /**
396
+ * Checks whether an object has no own enumerable keys.
397
+ *
398
+ * @param {Record<string, any>} object - Source object.
399
+ * @returns {boolean} True if the object is empty.
400
+ */
401
+ isEmpty(object) {
402
+ return object == null || Object.keys(object).length === 0;
403
+ },
404
+
405
+ /**
406
+ * Freezes an object deeply, making it (and all nested objects) immutable.
407
+ *
408
+ * @param {Record<string, any>} object - Object to freeze.
409
+ * @returns {Record<string, any>} The same, now deeply-frozen, object.
410
+ */
411
+ deepFreeze(object) {
412
+ if (object == null || typeof object !== "object" || Object.isFrozen(object)) {
413
+ return object;
414
+ }
415
+ Object.freeze(object);
416
+ for (const key of Object.keys(object)) {
417
+ this.deepFreeze(object[key]);
418
+ }
419
+ return object;
420
+ },
421
+ };
package/src/SQLite.js CHANGED
@@ -1,5 +1,6 @@
1
- const fs = require("./FS");
2
1
  const checker = require("./Checker");
2
+ const { resolve, dirname, isAbsolute } = require("node:path");
3
+ const { existsSync, mkdirSync } = require("node:fs");
3
4
  const originalEmitWarning = process.emitWarning;
4
5
 
5
6
  process.emitWarning = (warning, ...args) => {
@@ -77,23 +78,33 @@ module.exports = class SQLite {
77
78
  * The database file and its parent folders are created if necessary.
78
79
  * WAL journal mode and foreign key support are enabled automatically.
79
80
  *
80
- * @param {string} [path] - Path to the SQLite database file.
81
+ * Pass `":memory:"` to create a transient in-memory database.
82
+ *
83
+ * @param {string} [path="db.sqlite"] - Path to the SQLite database file.
81
84
  *
82
85
  * @example
83
86
  * const db = new SQLite();
84
87
  *
85
88
  * @example
86
89
  * const db = new SQLite("data/my-app.sqlite");
90
+ *
91
+ * @example
92
+ * const db = new SQLite(":memory:");
87
93
  */
88
- constructor(path = fs.createPath("db.sqlite").replaceAll("\\", "/")) {
89
- if (path.includes("/")) {
90
- const parts = path.split("/");
91
- const file = parts[parts.length - 1];
92
- const folder = fs.createPath(path.slice(0, -file.length));
93
- fs.createFolder(folder);
94
+ constructor(path = "db.sqlite") {
95
+ let target = path;
96
+
97
+ if (path !== ":memory:" && path !== "") {
98
+ // Resolve to an absolute path so the folder we create and the file we
99
+ // open are guaranteed to be the same location (relative to the cwd).
100
+ target = isAbsolute(path) ? path : resolve(process.cwd(), path);
101
+ const folder = dirname(target);
102
+ if (folder && !existsSync(folder)) {
103
+ mkdirSync(folder, { recursive: true });
104
+ }
94
105
  }
95
106
 
96
- this.#$ = new DatabaseSync(path);
107
+ this.#$ = new DatabaseSync(target);
97
108
  this.#run("PRAGMA journal_mode = WAL");
98
109
  this.#run("PRAGMA foreign_keys = ON");
99
110
  }