@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.
- package/CHANGELOG.md +190 -0
- package/README.md +136 -549
- package/index.js +80 -35
- package/package.json +160 -33
- package/src/Arr.js +887 -419
- package/src/Async.js +585 -0
- package/src/Cache.js +329 -0
- package/src/Checker.js +971 -539
- package/src/Cli.js +1034 -0
- package/src/Color.js +415 -0
- package/src/Crypto.js +707 -0
- package/src/Emitter.js +289 -0
- package/src/Env.js +555 -0
- package/src/FS.js +1483 -894
- package/src/Func.js +670 -353
- package/src/Http.js +538 -0
- package/src/Id.js +384 -201
- package/src/Logger.js +739 -378
- package/src/Num.js +844 -361
- package/src/Obj.js +855 -372
- package/src/SQLite.js +1308 -1005
- package/src/Schema.js +1586 -0
- package/src/Str.js +983 -436
- package/src/Sys.js +511 -0
- package/src/Time.js +1260 -422
- package/src/Utils.js +321 -325
- package/src/errors.js +292 -0
- package/types/index.d.ts +693 -526
- package/types/src/Arr.d.ts +455 -145
- package/types/src/Arr.d.ts.map +1 -1
- package/types/src/Async.d.ts +321 -0
- package/types/src/Async.d.ts.map +1 -0
- package/types/src/Cache.d.ts +195 -0
- package/types/src/Cache.d.ts.map +1 -0
- package/types/src/Checker.d.ts +501 -235
- package/types/src/Checker.d.ts.map +1 -1
- package/types/src/Cli.d.ts +582 -0
- package/types/src/Cli.d.ts.map +1 -0
- package/types/src/Color.d.ts +294 -0
- package/types/src/Color.d.ts.map +1 -0
- package/types/src/Crypto.d.ts +534 -0
- package/types/src/Crypto.d.ts.map +1 -0
- package/types/src/Emitter.d.ts +170 -0
- package/types/src/Emitter.d.ts.map +1 -0
- package/types/src/Env.d.ts +382 -0
- package/types/src/Env.d.ts.map +1 -0
- package/types/src/FS.d.ts +686 -238
- package/types/src/FS.d.ts.map +1 -1
- package/types/src/Func.d.ts +380 -135
- package/types/src/Func.d.ts.map +1 -1
- package/types/src/Http.d.ts +374 -0
- package/types/src/Http.d.ts.map +1 -0
- package/types/src/Id.d.ts +149 -68
- package/types/src/Id.d.ts.map +1 -1
- package/types/src/Logger.d.ts +363 -46
- package/types/src/Logger.d.ts.map +1 -1
- package/types/src/Num.d.ts +473 -137
- package/types/src/Num.d.ts.map +1 -1
- package/types/src/Obj.d.ts +390 -104
- package/types/src/Obj.d.ts.map +1 -1
- package/types/src/SQLite.d.ts +646 -309
- package/types/src/SQLite.d.ts.map +1 -1
- package/types/src/Schema.d.ts +940 -0
- package/types/src/Schema.d.ts.map +1 -0
- package/types/src/Str.d.ts +569 -166
- package/types/src/Str.d.ts.map +1 -1
- package/types/src/Sys.d.ts +269 -0
- package/types/src/Sys.d.ts.map +1 -0
- package/types/src/Time.d.ts +663 -204
- package/types/src/Time.d.ts.map +1 -1
- package/types/src/Utils.d.ts +154 -102
- package/types/src/Utils.d.ts.map +1 -1
- package/types/src/errors.d.ts +292 -0
- package/types/src/errors.d.ts.map +1 -0
- 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
|
-
*
|
|
40
|
+
* The type found at a dotted path of `T`, or `any` for an unknown path.
|
|
3
41
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
-
|
|
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
|
|
13
|
-
* @
|
|
14
|
-
* @
|
|
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((
|
|
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
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* @
|
|
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 (
|
|
38
|
-
if (
|
|
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
|
|
42
|
-
seen.set(value,
|
|
43
|
-
for (const item of value)
|
|
44
|
-
return
|
|
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 (
|
|
47
|
-
const
|
|
48
|
-
seen.set(value,
|
|
49
|
-
for (const [k, v] of value)
|
|
50
|
-
return
|
|
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 (
|
|
53
|
-
const
|
|
54
|
-
seen.set(value,
|
|
55
|
-
for (const v of value)
|
|
56
|
-
return
|
|
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
|
-
|
|
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
|
|
62
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
140
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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)
|
|
204
|
-
|
|
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]
|
|
209
|
-
current[key] =
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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
|
};
|