@ix-xs/node-comfort 1.2.0 → 2.0.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.
Files changed (75) hide show
  1. package/CHANGELOG.md +190 -0
  2. package/README.md +136 -549
  3. package/index.js +80 -35
  4. package/package.json +160 -33
  5. package/src/Arr.js +887 -419
  6. package/src/Async.js +585 -0
  7. package/src/Cache.js +329 -0
  8. package/src/Checker.js +971 -539
  9. package/src/Cli.js +1034 -0
  10. package/src/Color.js +415 -0
  11. package/src/Crypto.js +707 -0
  12. package/src/Emitter.js +289 -0
  13. package/src/Env.js +555 -0
  14. package/src/FS.js +1483 -894
  15. package/src/Func.js +670 -353
  16. package/src/Http.js +538 -0
  17. package/src/Id.js +384 -201
  18. package/src/Logger.js +739 -378
  19. package/src/Num.js +844 -361
  20. package/src/Obj.js +855 -372
  21. package/src/SQLite.js +1308 -1005
  22. package/src/Schema.js +1586 -0
  23. package/src/Str.js +983 -436
  24. package/src/Sys.js +511 -0
  25. package/src/Time.js +1260 -422
  26. package/src/Utils.js +321 -325
  27. package/src/errors.js +292 -0
  28. package/types/index.d.ts +693 -526
  29. package/types/src/Arr.d.ts +455 -145
  30. package/types/src/Arr.d.ts.map +1 -1
  31. package/types/src/Async.d.ts +321 -0
  32. package/types/src/Async.d.ts.map +1 -0
  33. package/types/src/Cache.d.ts +195 -0
  34. package/types/src/Cache.d.ts.map +1 -0
  35. package/types/src/Checker.d.ts +501 -235
  36. package/types/src/Checker.d.ts.map +1 -1
  37. package/types/src/Cli.d.ts +582 -0
  38. package/types/src/Cli.d.ts.map +1 -0
  39. package/types/src/Color.d.ts +294 -0
  40. package/types/src/Color.d.ts.map +1 -0
  41. package/types/src/Crypto.d.ts +534 -0
  42. package/types/src/Crypto.d.ts.map +1 -0
  43. package/types/src/Emitter.d.ts +170 -0
  44. package/types/src/Emitter.d.ts.map +1 -0
  45. package/types/src/Env.d.ts +382 -0
  46. package/types/src/Env.d.ts.map +1 -0
  47. package/types/src/FS.d.ts +686 -238
  48. package/types/src/FS.d.ts.map +1 -1
  49. package/types/src/Func.d.ts +380 -135
  50. package/types/src/Func.d.ts.map +1 -1
  51. package/types/src/Http.d.ts +374 -0
  52. package/types/src/Http.d.ts.map +1 -0
  53. package/types/src/Id.d.ts +149 -68
  54. package/types/src/Id.d.ts.map +1 -1
  55. package/types/src/Logger.d.ts +363 -46
  56. package/types/src/Logger.d.ts.map +1 -1
  57. package/types/src/Num.d.ts +473 -137
  58. package/types/src/Num.d.ts.map +1 -1
  59. package/types/src/Obj.d.ts +390 -104
  60. package/types/src/Obj.d.ts.map +1 -1
  61. package/types/src/SQLite.d.ts +646 -309
  62. package/types/src/SQLite.d.ts.map +1 -1
  63. package/types/src/Schema.d.ts +940 -0
  64. package/types/src/Schema.d.ts.map +1 -0
  65. package/types/src/Str.d.ts +569 -166
  66. package/types/src/Str.d.ts.map +1 -1
  67. package/types/src/Sys.d.ts +269 -0
  68. package/types/src/Sys.d.ts.map +1 -0
  69. package/types/src/Time.d.ts +663 -204
  70. package/types/src/Time.d.ts.map +1 -1
  71. package/types/src/Utils.d.ts +154 -102
  72. package/types/src/Utils.d.ts.map +1 -1
  73. package/types/src/errors.d.ts +292 -0
  74. package/types/src/errors.d.ts.map +1 -0
  75. package/types/index.d.ts.map +0 -1
package/src/Obj.js CHANGED
@@ -1,421 +1,904 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Object helpers: deep clone, merge, equality, diff, typed dot paths,
5
+ * pick and omit. Inputs are never mutated, and `__proto__`, `constructor`
6
+ * and `prototype` keys are always ignored, so user input can't pollute
7
+ * prototypes.
8
+ *
9
+ * @example
10
+ * obj.get(config, "db.pool.max", 10); // typed, paths autocompleted
11
+ * obj.set(state, "user.profile.name", "Ada"); // returns an updated copy
12
+ * obj.merge(defaults, userConfig);
13
+ */
14
+
15
+ const { types } = require("node:util");
16
+
17
+ /**
18
+ * Values that end a path: dates, regexes, maps, sets, functions and arrays.
19
+ * @typedef {Date | RegExp | Map<any, any> | Set<any> | ((...args: any[]) => any) | readonly any[]} PathLeaf
20
+ */
21
+
22
+ /**
23
+ * Every dotted path of an object type, up to 6 levels. This is what powers
24
+ * path completion in `obj.get()` and friends.
25
+ *
26
+ * @example
27
+ * // Paths<{ a: { b: number }, c: string }> is "a" | "a.b" | "c"
28
+ *
29
+ * @template T
30
+ * @template {unknown[]} [Depth=[]]
31
+ * @typedef {Depth["length"] extends 6 ? never : T extends PathLeaf ? never : T extends object ? { [K in keyof T & (string | number)]: NonNullable<T[K]> extends PathLeaf ? `${K}` : NonNullable<T[K]> extends object ? `${K}` | `${K}.${Paths<NonNullable<T[K]>, [...Depth, 0]>}` : `${K}` }[keyof T & (string | number)] : never} Paths
32
+ */
33
+
34
+ /**
35
+ * @template T
36
+ * @typedef {[Extract<T, null | undefined>] extends [never] ? never : undefined} NullishToUndefined
37
+ */
38
+
1
39
  /**
2
- * Object utilities.
40
+ * The type found at a dotted path of `T`, or `any` for an unknown path.
3
41
  *
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.
42
+ * @template T
43
+ * @template {string} P
44
+ * @typedef {P extends `${infer Head}.${infer Rest}` ? PathStep<T, Head> extends infer Next ? ([Next] extends [typeof _NOT_FOUND] ? any : PathValue<Next, Rest> | NullishToUndefined<T>) : any : PathStep<T, P> extends infer Next ? ([Next] extends [typeof _NOT_FOUND] ? any : Next | NullishToUndefined<T>) : any} PathValue
45
+ */
46
+
47
+ /**
48
+ * @template T
49
+ * @template {string} K
50
+ * @typedef {K extends keyof NonNullable<T> ? NonNullable<T>[K] : NonNullable<T> extends readonly (infer E)[] ? (K extends `${number}` ? E | undefined : typeof _NOT_FOUND) : typeof _NOT_FOUND} PathStep
51
+ */
52
+
53
+ /**
54
+ * A path: a known dotted path (completed by your editor), any other string
55
+ * like `"items[0].name"`, or an array of keys.
56
+ *
57
+ * @template T
58
+ * @typedef {Paths<T> | (string & {}) | ReadonlyArray<string | number>} PathInput
59
+ */
60
+
61
+ /**
62
+ * `T`, read-only all the way down.
63
+ * @template T
64
+ * @typedef {T extends (...args: any[]) => any ? T : T extends object ? { readonly [K in keyof T]: DeepReadonly<T[K]> } : T} DeepReadonly
65
+ */
66
+
67
+ /**
68
+ * One difference reported by `diff()`.
69
+ * @typedef {object} Change
70
+ * @property {Array<string | number>} path Where it changed, like `["address", "city"]`.
71
+ * @property {"added" | "removed" | "changed"} type
72
+ * @property {unknown} [from] The old value.
73
+ * @property {unknown} [to] The new value.
74
+ */
75
+
76
+ /**
77
+ * Options for `mergeWith()`.
78
+ * @typedef {object} MergeOptions
79
+ * @property {"replace" | "concat" | "unique"} [arrays] When both sides have an array: keep the last, join them, or join without duplicates. Defaults to `"replace"`.
80
+ * @property {boolean} [skipUndefined] Don't let `undefined` overwrite an existing value.
7
81
  */
8
82
 
9
- const checker = require("./Checker");
83
+ /**
84
+ * Options for `compact()`.
85
+ * @typedef {object} CompactOptions
86
+ * @property {boolean} [deep] Clean nested objects and arrays too.
87
+ * @property {boolean} [removeEmpty] Also remove `""`, `[]` and `{}`.
88
+ */
89
+
90
+ const _UNSAFE = new Set(["__proto__", "constructor", "prototype"]);
91
+
92
+ /** Marks an unknown path segment in `PathStep`. */
93
+ const _NOT_FOUND = Symbol("not-found");
94
+
95
+ /** @param {unknown} value */
96
+ const _isPlain = (value) => {
97
+ if (typeof value !== "object" || value === null) return false;
98
+ const proto = Object.getPrototypeOf(value);
99
+ // The last check accepts objects from another realm (a worker, a `vm`).
100
+ return proto === null || proto === Object.prototype || Object.getPrototypeOf(proto) === null;
101
+ };
10
102
 
11
103
  /**
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>}
104
+ * Parses `"a.b[0].c"` or `["a", "b", 0, "c"]` into keys.
105
+ * @param {unknown} path
106
+ * @returns {Array<string | number>}
16
107
  */
17
108
  const _toPath = (path) => {
18
109
  if (Array.isArray(path)) return path;
19
110
  return String(path)
20
111
  .replace(/\[(\d+)\]/g, ".$1")
112
+ .replace(/\[["']?([^\]"']+)["']?\]/g, ".$1")
21
113
  .split(".")
22
- .filter((p) => p !== "");
114
+ .filter((part) => part !== "");
23
115
  };
24
116
 
117
+ /** @param {Array<string | number>} keys */
118
+ const _isSafePath = (keys) => keys.every((key) => !_UNSAFE.has(String(key)));
119
+
25
120
  /**
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.
121
+ * Deep copy for what `structuredClone` can't handle (functions, class instances).
122
+ * @param {any} value
123
+ * @param {WeakMap<object, any>} seen
124
+ * @returns {any}
33
125
  */
34
126
  const _deepClone = (value, seen) => {
35
127
  if (value === null || typeof value !== "object") return value;
36
128
  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
-
129
+ if (types.isDate(value)) return new Date(value.getTime());
130
+ if (types.isRegExp(value)) return new RegExp(value.source, value.flags);
40
131
  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;
132
+ const out = /** @type {any[]} */ ([]);
133
+ seen.set(value, out);
134
+ for (const item of value) out.push(_deepClone(item, seen));
135
+ return out;
45
136
  }
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;
137
+ if (types.isMap(value)) {
138
+ const out = new Map();
139
+ seen.set(value, out);
140
+ for (const [k, v] of value) out.set(_deepClone(k, seen), _deepClone(v, seen));
141
+ return out;
51
142
  }
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;
143
+ if (types.isSet(value)) {
144
+ const out = new Set();
145
+ seen.set(value, out);
146
+ for (const v of value) out.add(_deepClone(v, seen));
147
+ return out;
57
148
  }
58
-
59
- const out = {};
149
+ if (ArrayBuffer.isView(value) && !(value instanceof DataView)) {
150
+ return /** @type {any} */ (value).slice();
151
+ }
152
+ const out = Object.create(Object.getPrototypeOf(value));
60
153
  seen.set(value, out);
61
- for (const key of Object.keys(value)) {
62
- out[key] = _deepClone(value[key], seen);
154
+ for (const key of Reflect.ownKeys(value)) {
155
+ if (typeof key === "string" && _UNSAFE.has(key)) continue;
156
+ const descriptor = /** @type {PropertyDescriptor} */ (Object.getOwnPropertyDescriptor(value, key));
157
+ if ("value" in descriptor) descriptor.value = _deepClone(descriptor.value, seen);
158
+ Object.defineProperty(out, key, descriptor);
63
159
  }
64
160
  return out;
65
161
  };
66
162
 
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;
163
+ /**
164
+ * @param {any} target
165
+ * @param {any} source
166
+ * @param {MergeOptions} options
167
+ * @returns {any}
168
+ */
169
+ const _mergeTwo = (target, source, options) => {
170
+ if (Array.isArray(target) && Array.isArray(source)) {
171
+ if (options.arrays === "concat") return [...target, ...source];
172
+ if (options.arrays === "unique") return [...new Set([...target, ...source])];
173
+ return [...source];
174
+ }
175
+ if (!_isPlain(source)) return source;
176
+ const out = /** @type {Record<string, any>} */ (_isPlain(target) ? { ...target } : {});
177
+ for (const key of Object.keys(source)) {
178
+ if (_UNSAFE.has(key)) continue;
179
+ const value = source[key];
180
+ if (value === undefined && options.skipUndefined) continue;
181
+ const current = out[key];
182
+ out[key] = (_isPlain(value) && _isPlain(current)) || (Array.isArray(value) && Array.isArray(current))
183
+ ? _mergeTwo(current, value, options)
184
+ : _isPlain(value)
185
+ ? _mergeTwo({}, value, options)
186
+ : value;
187
+ }
188
+ return out;
189
+ };
190
+
191
+ /**
192
+ * Deep copy. Uses `structuredClone` when it can, and falls back to a copy
193
+ * that keeps functions and class prototypes.
194
+ *
195
+ * @example
196
+ * const copy = obj.clone({ a: { b: [1, 2] }, when: new Date() });
197
+ * copy.a.b.push(3); // the original is untouched
198
+ *
199
+ * @template T
200
+ * @param {T} value
201
+ * @returns {T}
202
+ */
203
+ function clone(value) {
204
+ if (value === null || typeof value !== "object") return value;
205
+ if (_isPlain(value) || Array.isArray(value)) {
84
206
  try {
85
207
  return structuredClone(value);
86
208
  } catch {
87
- return _deepClone(value, new WeakMap());
209
+ // Functions or exotic values inside: fall through to the manual copy.
210
+ }
211
+ }
212
+ return _deepClone(value, new WeakMap());
213
+ }
214
+
215
+ /**
216
+ * Deep-merges objects into a new one. Later objects win, nested objects are
217
+ * merged, arrays are replaced. See `mergeWith()` to join arrays instead.
218
+ *
219
+ * @example
220
+ * obj.merge({ db: { host: "localhost", port: 5432 } }, { db: { port: 6543 } });
221
+ * // { db: { host: "localhost", port: 6543 } }
222
+ *
223
+ * @template {object} A
224
+ * @overload
225
+ * @param {A} a
226
+ * @returns {A}
227
+ */
228
+ /**
229
+ * Deep-merges `b` into a copy of `a`.
230
+ *
231
+ * @template {object} A
232
+ * @template {object} B
233
+ * @overload
234
+ * @param {A} a
235
+ * @param {B} b
236
+ * @returns {A & B}
237
+ */
238
+ /**
239
+ * Deep-merges three objects; the last one wins.
240
+ *
241
+ * @template {object} A
242
+ * @template {object} B
243
+ * @template {object} C
244
+ * @overload
245
+ * @param {A} a
246
+ * @param {B} b
247
+ * @param {C} c
248
+ * @returns {A & B & C}
249
+ */
250
+ /**
251
+ * Deep-merges any number of objects; the last one wins.
252
+ *
253
+ * @overload
254
+ * @param {...object} sources
255
+ * @returns {Record<string, any>}
256
+ */
257
+ /**
258
+ * @param {...object} sources
259
+ * @returns {Record<string, any>}
260
+ */
261
+ function merge(...sources) {
262
+ return sources.reduce((acc, source) => (_isPlain(source) ? _mergeTwo(acc, source, {}) : acc), /** @type {Record<string, any>} */ ({}));
263
+ }
264
+
265
+ /**
266
+ * Like `merge()`, with a choice of what happens to arrays and `undefined`.
267
+ *
268
+ * @example
269
+ * obj.mergeWith({ arrays: "unique" }, { tags: ["a", "b"] }, { tags: ["b", "c"] });
270
+ * // { tags: ["a", "b", "c"] }
271
+ * obj.mergeWith({ skipUndefined: true }, { port: 80 }, { port: undefined });
272
+ * // { port: 80 }
273
+ *
274
+ * @param {MergeOptions} options
275
+ * @param {...object} sources
276
+ * @returns {Record<string, any>}
277
+ */
278
+ function mergeWith(options, ...sources) {
279
+ return sources.reduce((acc, source) => (_isPlain(source) ? _mergeTwo(acc, source, options) : acc), /** @type {Record<string, any>} */ ({}));
280
+ }
281
+
282
+ /**
283
+ * Fills in missing properties from defaults, deeply. Values that are set,
284
+ * even `null` or `0`, are kept.
285
+ *
286
+ * @example
287
+ * obj.defaults({ port: 8080, db: { user: "me" } }, { port: 80, db: { user: "root", pass: "" } });
288
+ * // { port: 8080, db: { user: "me", pass: "" } }
289
+ *
290
+ * @template {object} T
291
+ * @template {object} D
292
+ * @param {T} target
293
+ * @param {...D} sources The first one has the highest priority.
294
+ * @returns {T & D}
295
+ */
296
+ function defaults(target, ...sources) {
297
+ /** @type {(t: any, s: any) => any} */
298
+ const fill = (t, s) => {
299
+ const out = _isPlain(t) ? { ...t } : {};
300
+ for (const key of Object.keys(s)) {
301
+ if (_UNSAFE.has(key)) continue;
302
+ if (out[key] === undefined) out[key] = clone(s[key]);
303
+ else if (_isPlain(out[key]) && _isPlain(s[key])) out[key] = fill(out[key], s[key]);
88
304
  }
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
- }
305
+ return out;
306
+ };
307
+ return sources.reduce((acc, source) => (_isPlain(source) ? fill(acc, source) : acc), /** @type {any} */ (clone(target)));
308
+ }
309
+
310
+ /**
311
+ * Deep equality. Knows about arrays, dates, regexes, maps, sets, typed
312
+ * arrays, `NaN` and circular references.
313
+ *
314
+ * @example
315
+ * obj.equal({ a: [1, { b: 2 }] }, { a: [1, { b: 2 }] }); // true
316
+ * obj.equal(new Set([1, 2]), new Set([2, 1])); // true
317
+ * obj.equal({ a: 1 }, { a: 1, b: undefined }); // false
318
+ *
319
+ * @param {unknown} a
320
+ * @param {unknown} b
321
+ * @returns {boolean}
322
+ */
323
+ function equal(a, b) {
324
+ return _equal(a, b, new WeakMap());
325
+ }
326
+
327
+ /**
328
+ * Same kind of built-in value, even when they come from different realms
329
+ * (a worker, a `vm` context) where prototypes are not shared.
330
+ * @param {object} a
331
+ * @param {object} b
332
+ * @returns {boolean}
333
+ */
334
+ const _sameBuiltIn = (a, b) => {
335
+ const tag = Object.prototype.toString.call(a);
336
+ if (tag !== Object.prototype.toString.call(b)) return false;
337
+ return tag !== "[object Object]" || (Object.getPrototypeOf(Object.getPrototypeOf(a) ?? {}) === null && Object.getPrototypeOf(Object.getPrototypeOf(b) ?? {}) === null);
338
+ };
339
+
340
+ /**
341
+ * @param {any} a
342
+ * @param {any} b
343
+ * @param {WeakMap<object, object>} seen
344
+ * @returns {boolean}
345
+ */
346
+ function _equal(a, b, seen) {
347
+ if (Object.is(a, b)) return true;
348
+ if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
349
+ if (Object.getPrototypeOf(a) !== Object.getPrototypeOf(b) && !_sameBuiltIn(a, b)) return false;
350
+ if (seen.get(a) === b) return true;
351
+ seen.set(a, b);
352
+
353
+ if (types.isDate(a)) return a.getTime() === b.getTime();
354
+ if (types.isRegExp(a)) return a.source === b.source && a.flags === b.flags;
355
+ if (Array.isArray(a)) return a.length === b.length && a.every((item, i) => _equal(item, b[i], seen));
356
+ if (ArrayBuffer.isView(a) && !(a instanceof DataView)) {
357
+ const x = /** @type {any} */ (a);
358
+ return x.length === b.length && x.every((/** @type {unknown} */ v, /** @type {number} */ i) => Object.is(v, b[i]));
359
+ }
360
+ if (types.isMap(a)) {
361
+ if (a.size !== b.size) return false;
362
+ outer: for (const [key, value] of a) {
363
+ if (b.has(key)) {
364
+ if (!_equal(value, b.get(key), seen)) return false;
365
+ continue;
117
366
  }
367
+ for (const [otherKey, otherValue] of b) if (_equal(key, otherKey, seen) && _equal(value, otherValue, seen)) continue outer;
368
+ return false;
118
369
  }
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
370
+ return true;
371
+ }
372
+ if (types.isSet(a)) {
373
+ if (a.size !== b.size) return false;
374
+ outer: for (const value of a) {
375
+ if (b.has(value)) continue;
376
+ for (const other of b) if (_equal(value, other, seen)) continue outer;
377
+ return false;
138
378
  }
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();
379
+ return true;
380
+ }
381
+ const keysA = Object.keys(a);
382
+ const keysB = Object.keys(b);
383
+ if (keysA.length !== keysB.length) return false;
384
+ return keysA.every((key) => Object.prototype.hasOwnProperty.call(b, key) && _equal(a[key], b[key], seen));
385
+ }
141
386
 
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;
387
+ /**
388
+ * Lists what changed between two values, deeply. Handy for audit logs and
389
+ * partial updates.
390
+ *
391
+ * @example
392
+ * obj.diff({ name: "Ada", tags: ["a"] }, { name: "Ada L.", tags: ["a", "b"] });
393
+ * // [
394
+ * // { path: ["name"], type: "changed", from: "Ada", to: "Ada L." },
395
+ * // { path: ["tags", 1], type: "added", to: "b" },
396
+ * // ]
397
+ *
398
+ * @param {unknown} before
399
+ * @param {unknown} after
400
+ * @returns {Change[]} Empty when nothing changed.
401
+ */
402
+ function diff(before, after) {
403
+ /** @type {Change[]} */
404
+ const changes = [];
405
+ /** @type {(a: any, b: any, path: Array<string | number>) => void} */
406
+ const walk = (a, b, path) => {
407
+ if (equal(a, b)) return;
408
+ const bothArrays = Array.isArray(a) && Array.isArray(b);
409
+ if (bothArrays || (_isPlain(a) && _isPlain(b))) {
410
+ const keys = bothArrays
411
+ ? Array.from({ length: Math.max(a.length, b.length) }, (_, i) => i)
412
+ : [...new Set([...Object.keys(a), ...Object.keys(b)])];
413
+ for (const key of keys) {
414
+ const inA = bothArrays ? /** @type {number} */ (key) < a.length : Object.prototype.hasOwnProperty.call(a, key);
415
+ const inB = bothArrays ? /** @type {number} */ (key) < b.length : Object.prototype.hasOwnProperty.call(b, key);
416
+ if (inA && !inB) changes.push({ path: [...path, key], type: "removed", from: a[key] });
417
+ else if (!inA && inB) changes.push({ path: [...path, key], type: "added", to: b[key] });
418
+ else walk(a[key], b[key], [...path, key]);
150
419
  }
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;
420
+ return;
157
421
  }
422
+ changes.push({ path, type: "changed", from: a, to: b });
423
+ };
424
+ walk(before, after, []);
425
+ return changes;
426
+ }
158
427
 
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];
428
+ /**
429
+ * Reads a nested value from a dotted path. Your editor completes the paths
430
+ * and knows the type of the result.
431
+ *
432
+ * @example
433
+ * obj.get(config, "db.pool.max"); // number | undefined
434
+ * obj.get(config, "db.pool.max", 10); // number
435
+ * obj.get(data, "users[0].name");
436
+ * obj.get(data, ["users", 0, "name"]);
437
+ *
438
+ * @template T
439
+ * @template {string} P
440
+ * @overload
441
+ * @param {T} object
442
+ * @param {P | Paths<T> | ReadonlyArray<string | number>} path Like `"a.b.c"`, `"a[0].b"` or `["a", 0, "b"]`.
443
+ * @returns {PathValue<T, P>}
444
+ */
445
+ /**
446
+ * Reads a nested value from a dotted path, or returns `fallback` if there's
447
+ * nothing there.
448
+ *
449
+ * @template T
450
+ * @template {string} P
451
+ * @template F
452
+ * @overload
453
+ * @param {T} object
454
+ * @param {P | Paths<T> | ReadonlyArray<string | number>} path Like `"a.b.c"`, `"a[0].b"` or `["a", 0, "b"]`.
455
+ * @param {F} fallback
456
+ * @returns {Exclude<PathValue<T, P>, undefined> | F}
457
+ */
458
+ /**
459
+ * @param {unknown} object
460
+ * @param {string | ReadonlyArray<string | number>} path
461
+ * @param {unknown} [fallback]
462
+ * @returns {any}
463
+ */
464
+ function get(object, path, fallback) {
465
+ const keys = _toPath(path);
466
+ if (!_isSafePath(keys)) return fallback;
467
+ /** @type {any} */
468
+ let current = object;
469
+ for (const key of keys) {
470
+ if (current === null || current === undefined) return fallback;
471
+ current = current[key];
472
+ }
473
+ return current === undefined ? fallback : current;
474
+ }
475
+
476
+ /**
477
+ * Returns a copy with a nested value set. Missing levels are created
478
+ * (arrays when the next key is a number).
479
+ *
480
+ * @example
481
+ * obj.set({}, "a.b.c", 1); // { a: { b: { c: 1 } } }
482
+ * obj.set(state, "user.name", "Ada"); // state is untouched
483
+ *
484
+ * @template T
485
+ * @param {T} object
486
+ * @param {PathInput<T>} path
487
+ * @param {unknown} value
488
+ * @returns {T}
489
+ */
490
+ function set(object, path, value) {
491
+ const keys = _toPath(path);
492
+ /** @type {any} */
493
+ const root = object === null || typeof object !== "object" ? {} : clone(object);
494
+ if (!keys.length || !_isSafePath(keys)) return root;
495
+ let current = root;
496
+ for (let i = 0; i < keys.length - 1; i++) {
497
+ const key = keys[i];
498
+ const next = keys[i + 1];
499
+ if (current[key] === null || typeof current[key] !== "object") {
500
+ current[key] = typeof next === "number" || /^\d+$/.test(String(next)) ? [] : {};
183
501
  }
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) {
502
+ current = current[key];
503
+ }
504
+ current[keys[keys.length - 1]] = value;
505
+ return root;
506
+ }
507
+
508
+ /**
509
+ * Returns a copy without the property at `path`.
510
+ *
511
+ * @example
512
+ * obj.unset({ a: { b: 1, c: 2 } }, "a.b"); // { a: { c: 2 } }
513
+ *
514
+ * @template T
515
+ * @param {T} object
516
+ * @param {PathInput<T>} path
517
+ * @returns {T}
518
+ */
519
+ function unset(object, path) {
520
+ const keys = _toPath(path);
521
+ /** @type {any} */
522
+ const root = clone(object);
523
+ if (!keys.length || !_isSafePath(keys) || root === null || typeof root !== "object") return root;
524
+ let current = root;
525
+ for (let i = 0; i < keys.length - 1; i++) {
526
+ current = current?.[keys[i]];
527
+ if (current === null || typeof current !== "object") return root;
528
+ }
529
+ const lastKey = keys[keys.length - 1];
530
+ if (Array.isArray(current) && typeof lastKey === "number") current.splice(lastKey, 1);
531
+ else delete current[lastKey];
532
+ return root;
533
+ }
534
+
535
+ /**
536
+ * Does the path exist? A property set to `undefined` still counts.
537
+ *
538
+ * @example
539
+ * obj.has({ a: { b: undefined } }, "a.b"); // true
540
+ * obj.has({ a: {} }, "a.b"); // false
541
+ *
542
+ * @template T
543
+ * @param {T} object
544
+ * @param {PathInput<T>} path
545
+ * @returns {boolean}
546
+ */
547
+ function has(object, path) {
548
+ const keys = _toPath(path);
549
+ if (!keys.length || !_isSafePath(keys)) return false;
550
+ /** @type {any} */
551
+ let current = object;
552
+ for (const key of keys) {
553
+ if (current === null || current === undefined || !Object.prototype.hasOwnProperty.call(Object(current), key)) return false;
554
+ current = current[key];
555
+ }
556
+ return true;
557
+ }
558
+
559
+ /**
560
+ * A copy with only the listed keys.
561
+ *
562
+ * @example
563
+ * obj.pick(user, ["id", "email"]);
564
+ *
565
+ * @template {object} T
566
+ * @template {keyof T} K
567
+ * @param {T} object
568
+ * @param {readonly K[]} keys
569
+ * @returns {Pick<T, K>}
570
+ */
571
+ function pick(object, keys) {
572
+ /** @type {any} */
573
+ const out = {};
574
+ for (const key of keys) {
575
+ if (object != null && Object.prototype.hasOwnProperty.call(object, key)) out[key] = object[key];
576
+ }
577
+ return out;
578
+ }
579
+
580
+ /**
581
+ * A copy without the listed keys.
582
+ *
583
+ * @example
584
+ * obj.omit(user, ["password", "token"]);
585
+ *
586
+ * @template {object} T
587
+ * @template {keyof T} K
588
+ * @param {T} object
589
+ * @param {readonly K[]} keys
590
+ * @returns {Omit<T, K>}
591
+ */
592
+ function omit(object, keys) {
593
+ const exclude = new Set(/** @type {readonly PropertyKey[]} */ (keys));
594
+ /** @type {any} */
595
+ const out = {};
596
+ for (const key of Object.keys(object ?? {})) {
597
+ if (!exclude.has(key)) out[key] = /** @type {any} */ (object)[key];
598
+ }
599
+ return out;
600
+ }
601
+
602
+ /**
603
+ * Keeps the entries that pass a test.
604
+ *
605
+ * @example
606
+ * obj.filter({ a: 1, b: 2, c: 3 }, (value) => value > 1); // { b: 2, c: 3 }
607
+ *
608
+ * @template {object} T
609
+ * @param {T} object
610
+ * @param {(value: T[keyof T], key: keyof T & string) => unknown} predicate
611
+ * @returns {Partial<T>}
612
+ */
613
+ function filter(object, predicate) {
614
+ /** @type {any} */
615
+ const out = {};
616
+ for (const [key, value] of Object.entries(object ?? {})) {
617
+ if (predicate(value, /** @type {keyof T & string} */ (key))) out[key] = value;
618
+ }
619
+ return out;
620
+ }
621
+
622
+ /**
623
+ * Transforms every value, keeping the keys.
624
+ *
625
+ * @example
626
+ * obj.mapValues({ a: 1, b: 2 }, (v) => v * 10); // { a: 10, b: 20 }
627
+ *
628
+ * @template {object} T
629
+ * @template R
630
+ * @param {T} object
631
+ * @param {(value: T[keyof T], key: keyof T & string) => R} fn
632
+ * @returns {{ [K in keyof T]: R }}
633
+ */
634
+ function mapValues(object, fn) {
635
+ /** @type {any} */
636
+ const out = {};
637
+ for (const [key, value] of Object.entries(object ?? {})) out[key] = fn(value, /** @type {keyof T & string} */ (key));
638
+ return out;
639
+ }
640
+
641
+ /**
642
+ * Transforms every key, keeping the values.
643
+ *
644
+ * @example
645
+ * obj.mapKeys({ first_name: "Ada" }, (key) => nc.str.camelCase(key)); // { firstName: "Ada" }
646
+ *
647
+ * @template {object} T
648
+ * @param {T} object
649
+ * @param {(key: keyof T & string, value: T[keyof T]) => PropertyKey} fn Returns the new key.
650
+ * @returns {Record<string, T[keyof T]>}
651
+ */
652
+ function mapKeys(object, fn) {
653
+ /** @type {any} */
654
+ const out = {};
655
+ for (const [key, value] of Object.entries(object ?? {})) {
656
+ const next = fn(/** @type {keyof T & string} */ (key), value);
657
+ if (typeof next === "string" && _UNSAFE.has(next)) continue;
658
+ out[next] = value;
659
+ }
660
+ return out;
661
+ }
662
+
663
+ /**
664
+ * Renames some keys and leaves the others alone.
665
+ *
666
+ * @example
667
+ * obj.renameKeys({ _id: 1, name: "Ada" }, { _id: "id" }); // { id: 1, name: "Ada" }
668
+ *
669
+ * @template {object} T
670
+ * @param {T} object
671
+ * @param {Partial<Record<keyof T, string>>} mapping Old name to new name.
672
+ * @returns {Record<string, T[keyof T]>}
673
+ */
674
+ function renameKeys(object, mapping) {
675
+ const map = /** @type {Record<string, string | undefined>} */ (mapping);
676
+ return mapKeys(object, (key) => map[key] ?? key);
677
+ }
678
+
679
+ /**
680
+ * Swaps keys and values.
681
+ *
682
+ * @example
683
+ * obj.invert({ a: "x", b: "y" }); // { x: "a", y: "b" }
684
+ *
685
+ * @param {Record<string, PropertyKey>} object
686
+ * @returns {Record<string, string>}
687
+ */
688
+ function invert(object) {
689
+ /** @type {Record<string, string>} */
690
+ const out = {};
691
+ for (const [key, value] of Object.entries(object ?? {})) {
692
+ const next = String(value);
693
+ if (!_UNSAFE.has(next)) out[next] = key;
694
+ }
695
+ return out;
696
+ }
697
+
698
+ /**
699
+ * Removes `null` and `undefined` values, and optionally empty ones.
700
+ *
701
+ * @example
702
+ * obj.compact({ a: 1, b: null, c: undefined }); // { a: 1 }
703
+ * obj.compact({ a: { b: null, c: "" } }, { deep: true, removeEmpty: true }); // {}
704
+ *
705
+ * @template {object} T
706
+ * @param {T} object
707
+ * @param {CompactOptions} [options]
708
+ * @returns {Partial<T>}
709
+ */
710
+ function compact(object, options = {}) {
711
+ /** @type {(value: any) => boolean} */
712
+ const isEmptyValue = (value) =>
713
+ value === null ||
714
+ value === undefined ||
715
+ (options.removeEmpty === true &&
716
+ (value === "" || (Array.isArray(value) && value.length === 0) || (_isPlain(value) && Object.keys(value).length === 0)));
717
+ /** @type {(value: any) => any} */
718
+ const clean = (value) => {
719
+ if (!options.deep) return value;
720
+ if (Array.isArray(value)) return value.map(clean).filter((v) => !isEmptyValue(v));
721
+ if (_isPlain(value)) return compact(value, options);
722
+ return value;
723
+ };
724
+ /** @type {any} */
725
+ const out = {};
726
+ for (const [key, value] of Object.entries(object ?? {})) {
727
+ const cleaned = clean(value);
728
+ if (!isEmptyValue(cleaned)) out[key] = cleaned;
729
+ }
730
+ return out;
731
+ }
732
+
733
+ /**
734
+ * Flattens nested objects into dotted keys. Arrays stay as values.
735
+ *
736
+ * @example
737
+ * obj.flatten({ db: { host: "x", port: 5432 } }); // { "db.host": "x", "db.port": 5432 }
738
+ *
739
+ * @param {object} object
740
+ * @param {string} [prefix=""] Added in front of every key.
741
+ * @returns {Record<string, unknown>}
742
+ */
743
+ function flatten(object, prefix = "") {
744
+ /** @type {Record<string, unknown>} */
745
+ const out = {};
746
+ for (const [key, value] of Object.entries(object ?? {})) {
747
+ const path = prefix ? `${prefix}.${key}` : key;
748
+ if (_isPlain(value) && Object.keys(/** @type {object} */ (value)).length) Object.assign(out, flatten(/** @type {object} */ (value), path));
749
+ else out[path] = value;
750
+ }
751
+ return out;
752
+ }
753
+
754
+ /**
755
+ * Rebuilds nested objects from dotted keys. The opposite of `flatten()`.
756
+ *
757
+ * @example
758
+ * obj.unflatten({ "db.host": "x", "db.port": 5432 }); // { db: { host: "x", port: 5432 } }
759
+ *
760
+ * @param {Record<string, unknown>} object
761
+ * @returns {Record<string, any>}
762
+ */
763
+ function unflatten(object) {
764
+ /** @type {Record<string, any>} */
765
+ const out = {};
766
+ for (const [path, value] of Object.entries(object ?? {})) {
202
767
  const keys = _toPath(path);
203
- if (!keys.length) return object;
204
- const root = this.clone(object) ?? {};
205
- let current = root;
768
+ if (!keys.length || !_isSafePath(keys)) continue;
769
+ let current = out;
206
770
  for (let i = 0; i < keys.length - 1; i++) {
207
771
  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])) ? [] : {};
772
+ if (current[key] === null || typeof current[key] !== "object") {
773
+ current[key] = /^\d+$/.test(String(keys[i + 1])) ? [] : {};
210
774
  }
211
775
  current = current[key];
212
776
  }
213
777
  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
- }
778
+ }
779
+ return out;
780
+ }
781
+
782
+ /**
783
+ * `Object.keys`, but typed with the object's actual keys.
784
+ *
785
+ * @example
786
+ * for (const key of obj.keys(config)) config[key]; // no type error
787
+ *
788
+ * @template {object} T
789
+ * @param {T} object
790
+ * @returns {Array<keyof T & string>}
791
+ */
792
+ function keys(object) {
793
+ return /** @type {Array<keyof T & string>} */ (Object.keys(object ?? {}));
794
+ }
795
+
796
+ /**
797
+ * `Object.entries`, with typed keys and values.
798
+ *
799
+ * @template {object} T
800
+ * @param {T} object
801
+ * @returns {Array<[keyof T & string, T[keyof T]]>}
802
+ */
803
+ function entries(object) {
804
+ return /** @type {Array<[keyof T & string, T[keyof T]]>} */ (Object.entries(object ?? {}));
805
+ }
806
+
807
+ /**
808
+ * `Object.fromEntries` that accepts any iterable (a `Map` too) and skips
809
+ * unsafe keys.
810
+ *
811
+ * @example
812
+ * obj.fromEntries(new Map([["x", true]])); // { x: true }
813
+ *
814
+ * @template {PropertyKey} K
815
+ * @template V
816
+ * @param {Iterable<readonly [K, V]>} pairs
817
+ * @returns {Record<K, V>}
818
+ */
819
+ function fromEntries(pairs) {
820
+ /** @type {any} */
821
+ const out = {};
822
+ for (const [key, value] of pairs) {
823
+ if (typeof key === "string" && _UNSAFE.has(key)) continue;
824
+ out[key] = value;
825
+ }
826
+ return out;
827
+ }
828
+
829
+ /**
830
+ * How many entries: own keys of an object, size of a map or set, length of
831
+ * an array or string.
832
+ *
833
+ * @example
834
+ * obj.size({ a: 1, b: 2 }); // 2
835
+ *
836
+ * @param {unknown} value
837
+ * @returns {number}
838
+ */
839
+ function size(value) {
840
+ if (value === null || value === undefined) return 0;
841
+ if (typeof value === "string" || Array.isArray(value)) return value.length;
842
+ if (types.isMap(value) || types.isSet(value)) return /** @type {Set<unknown>} */ (value).size;
843
+ return typeof value === "object" ? Object.keys(value).length : 0;
844
+ }
845
+
846
+ /**
847
+ * Does the object have no own keys? `null` and `undefined` count as empty.
848
+ *
849
+ * @param {unknown} object
850
+ * @returns {boolean}
851
+ */
852
+ function isEmpty(object) {
853
+ return object === null || object === undefined || Object.keys(object).length === 0;
854
+ }
855
+
856
+ /**
857
+ * Freezes an object and everything inside it. Good for configuration and
858
+ * constants.
859
+ *
860
+ * @example
861
+ * const CONFIG = obj.deepFreeze({ db: { host: "localhost" } });
862
+ * CONFIG.db.host = "x"; // TypeScript error, and ignored at runtime
863
+ *
864
+ * @template T
865
+ * @param {T} object
866
+ * @returns {DeepReadonly<T>} The same object.
867
+ */
868
+ function deepFreeze(object) {
869
+ if (object !== null && (typeof object === "object" || typeof object === "function") && !Object.isFrozen(object)) {
415
870
  Object.freeze(object);
416
- for (const key of Object.keys(object)) {
417
- this.deepFreeze(object[key]);
418
- }
419
- return object;
420
- },
871
+ for (const key of Reflect.ownKeys(/** @type {object} */ (object))) deepFreeze(/** @type {any} */ (object)[key]);
872
+ }
873
+ return /** @type {DeepReadonly<T>} */ (object);
874
+ }
875
+
876
+ module.exports = {
877
+ clone,
878
+ merge,
879
+ mergeWith,
880
+ defaults,
881
+ equal,
882
+ diff,
883
+ // not shorthand: Node's ES module export detection stops at a bare `get`
884
+ get: get,
885
+ set,
886
+ unset,
887
+ has,
888
+ pick,
889
+ omit,
890
+ filter,
891
+ mapValues,
892
+ mapKeys,
893
+ renameKeys,
894
+ invert,
895
+ compact,
896
+ flatten,
897
+ unflatten,
898
+ keys,
899
+ entries,
900
+ fromEntries,
901
+ size,
902
+ isEmpty,
903
+ deepFreeze,
421
904
  };