@helpers4/object 3.0.0-alpha.1 → 3.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/README.md +10 -0
- package/lib/index.d.ts +314 -40
- package/lib/index.js +260 -27
- package/lib/index.js.map +1 -1
- package/llms.txt +381 -2
- package/meta/api.json +351 -1
- package/meta/examples.json +120 -0
- package/package.json +11 -3
package/lib/index.js
CHANGED
|
@@ -4,6 +4,44 @@ var UNSAFE_KEYS = /* @__PURE__ */ new Set([
|
|
|
4
4
|
"prototype"
|
|
5
5
|
]);
|
|
6
6
|
//#endregion
|
|
7
|
+
//#region helpers/object/clone.ts
|
|
8
|
+
/**
|
|
9
|
+
* This file is part of helpers4.
|
|
10
|
+
* Copyright (C) 2025 baxyz
|
|
11
|
+
* SPDX-License-Identifier: LGPL-3.0-or-later
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Creates a shallow copy of a value — one level deep, unlike {@link cloneDeep}.
|
|
15
|
+
*
|
|
16
|
+
* Unlike a plain `{ ...value }` spread, this correctly reconstructs `Date`,
|
|
17
|
+
* `Map`, `Set`, and arrays instead of producing an empty (or wrong-shaped)
|
|
18
|
+
* plain object for them. Primitives are returned as-is. Any other object
|
|
19
|
+
* (including class instances not listed above) has its own enumerable string
|
|
20
|
+
* keys shallow-copied into a plain object — the same fallback `cloneDeep`
|
|
21
|
+
* uses, so the two stay consistent for types neither one special-cases.
|
|
22
|
+
*
|
|
23
|
+
* @param value - The value to shallow-clone
|
|
24
|
+
* @returns A shallow copy of `value`
|
|
25
|
+
* @example
|
|
26
|
+
* clone({ a: { b: 1 } })
|
|
27
|
+
* // => { a: { b: 1 } } — new object, but `a` is the *same* nested reference
|
|
28
|
+
* @example
|
|
29
|
+
* clone(new Date('2024-01-01')) // => new Date with the same timestamp
|
|
30
|
+
* clone(new Map([['a', 1]])) // => new Map with the same entries
|
|
31
|
+
* clone([1, 2, 3]) // => new array, same elements
|
|
32
|
+
* @since 3.0.0
|
|
33
|
+
*/
|
|
34
|
+
function clone(value) {
|
|
35
|
+
if (value === null || typeof value !== "object") return value;
|
|
36
|
+
if (value instanceof Date) return new Date(value.getTime());
|
|
37
|
+
if (value instanceof Map) return new Map(value);
|
|
38
|
+
if (value instanceof Set) return new Set(value);
|
|
39
|
+
if (Array.isArray(value)) return [...value];
|
|
40
|
+
const result = {};
|
|
41
|
+
for (const key of Object.keys(value)) if (!UNSAFE_KEYS.has(key)) result[key] = value[key];
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
//#endregion
|
|
7
45
|
//#region helpers/object/cloneDeep.ts
|
|
8
46
|
/**
|
|
9
47
|
* This file is part of helpers4.
|
|
@@ -166,10 +204,14 @@ function normalizeTimestamp(timestamp) {
|
|
|
166
204
|
* SPDX-License-Identifier: LGPL-3.0-or-later
|
|
167
205
|
*/
|
|
168
206
|
/**
|
|
169
|
-
* Type guard that checks whether a value
|
|
170
|
-
*
|
|
207
|
+
* Type guard that checks whether a value is a `Temporal.Instant` or
|
|
208
|
+
* `Temporal.ZonedDateTime`.
|
|
209
|
+
*
|
|
210
|
+
* Duck-types the `epochMilliseconds` property rather than using `instanceof`,
|
|
211
|
+
* so values crossing a realm boundary (vm context, worker, iframe) are still
|
|
212
|
+
* recognized correctly.
|
|
171
213
|
*/
|
|
172
|
-
function
|
|
214
|
+
function isEpochMillisecondsLike(value) {
|
|
173
215
|
return typeof value === "object" && value !== null && "epochMilliseconds" in value && typeof value.epochMilliseconds === "number";
|
|
174
216
|
}
|
|
175
217
|
/**
|
|
@@ -200,7 +242,7 @@ function hasEpochMilliseconds(value) {
|
|
|
200
242
|
function ensureDate(input) {
|
|
201
243
|
if (input === null || input === void 0 || input === "" || input === 0) return null;
|
|
202
244
|
if (input instanceof Date) return isNaN(input.getTime()) ? null : input;
|
|
203
|
-
if (
|
|
245
|
+
if (isEpochMillisecondsLike(input)) {
|
|
204
246
|
const date = new Date(input.epochMilliseconds);
|
|
205
247
|
return isNaN(date.getTime()) ? null : date;
|
|
206
248
|
}
|
|
@@ -445,43 +487,92 @@ function equalsShallow(objA, objB) {
|
|
|
445
487
|
return true;
|
|
446
488
|
}
|
|
447
489
|
//#endregion
|
|
448
|
-
//#region helpers/object/
|
|
490
|
+
//#region helpers/object/flatten.ts
|
|
449
491
|
/**
|
|
450
492
|
* This file is part of helpers4.
|
|
451
493
|
* Copyright (C) 2025 baxyz
|
|
452
494
|
* SPDX-License-Identifier: LGPL-3.0-or-later
|
|
453
495
|
*/
|
|
496
|
+
function flattenInto(obj, prefix, result) {
|
|
497
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
498
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
499
|
+
if (isPlainObject(value) && Object.keys(value).length > 0) flattenInto(value, path, result);
|
|
500
|
+
else result[path] = value;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
454
503
|
/**
|
|
455
|
-
*
|
|
504
|
+
* Flattens a nested object into a single-level object whose keys are the
|
|
505
|
+
* dot-notation path to each leaf value. The inverse of {@link unflatten}.
|
|
506
|
+
*
|
|
507
|
+
* Only **plain objects** are recursed into — arrays, `Date`, `Map`, `RegExp`,
|
|
508
|
+
* class instances, and empty plain objects `{}` are kept as opaque leaf
|
|
509
|
+
* values. This keeps `flatten`/`unflatten` a clean, invertible pair: arrays
|
|
510
|
+
* can't be losslessly told apart from plain objects once reduced to dotted
|
|
511
|
+
* keys, so this implementation doesn't attempt it.
|
|
512
|
+
*
|
|
513
|
+
* Caveat shared by every dotted-path flattening scheme: a key that itself
|
|
514
|
+
* contains a literal `.` is indistinguishable from real nesting once
|
|
515
|
+
* flattened (`{ 'a.b': 1 }` and `{ a: { b: 1 } }` both produce `{ 'a.b': 1 }`).
|
|
516
|
+
*
|
|
517
|
+
* @param obj - The object to flatten
|
|
518
|
+
* @returns A single-level object with dot-notation keys
|
|
519
|
+
* @example
|
|
520
|
+
* flatten({ a: { b: { c: 1 }, d: 2 } })
|
|
521
|
+
* // => { 'a.b.c': 1, 'a.d': 2 }
|
|
522
|
+
* @example
|
|
523
|
+
* flatten({ a: [1, 2], b: new Date(0) })
|
|
524
|
+
* // => { a: [1, 2], b: Date } (arrays and special objects stay as leaves)
|
|
525
|
+
* @since 3.0.0
|
|
526
|
+
*/
|
|
527
|
+
function flatten(obj) {
|
|
528
|
+
const result = {};
|
|
529
|
+
flattenInto(obj, "", result);
|
|
530
|
+
return result;
|
|
531
|
+
}
|
|
532
|
+
//#endregion
|
|
533
|
+
//#region helpers/object/parsePropertyPath.ts
|
|
534
|
+
/**
|
|
535
|
+
* This file is part of helpers4.
|
|
536
|
+
* Copyright (C) 2025 baxyz
|
|
537
|
+
* SPDX-License-Identifier: LGPL-3.0-or-later
|
|
538
|
+
*/
|
|
539
|
+
var BRACKET = /\[(\d+)\]/g;
|
|
540
|
+
var MAX_CACHE_SIZE = 500;
|
|
541
|
+
var pathCache = /* @__PURE__ */ new Map();
|
|
542
|
+
/**
|
|
543
|
+
* Parses a dot/bracket-notation property path into an array of string/number
|
|
544
|
+
* key segments — the same notation accepted by {@link get} and {@link set}.
|
|
456
545
|
*
|
|
457
546
|
* - Dot separators (`.`) split segments; each segment becomes a string key.
|
|
458
547
|
* - Bracket indices (`[n]`) become number keys.
|
|
459
548
|
* - A leading `.` is treated as "current level" and stripped before parsing,
|
|
460
549
|
* so `.[0]` ≡ `[0]` and `.a.b` ≡ `a.b`.
|
|
461
|
-
* - Empty string (or bare `.`) returns `['']` (addresses the `''` key on the root object).
|
|
550
|
+
* - Empty string (or a bare `.`) returns `['']` (addresses the `''` key on the root object).
|
|
462
551
|
* - Consecutive dots (`a..b`) produce an empty-string segment: `['a', '', 'b']`.
|
|
463
|
-
* - Non-bracket text trailing a `]` within the same dot-segment throws a RangeError;
|
|
464
|
-
* use a dot separator instead (`'a[0].b'` not `'a[0]b'`).
|
|
465
552
|
*
|
|
466
|
-
*
|
|
553
|
+
* Results are cached (up to 500 distinct path strings, oldest evicted first)
|
|
554
|
+
* since real-world callers tend to reuse a small, fixed set of literal paths.
|
|
555
|
+
*
|
|
556
|
+
* @param path - The dot/bracket-notation path string to parse
|
|
557
|
+
* @returns The parsed key segments
|
|
558
|
+
* @throws {RangeError} When non-bracket text trails the last `]` within a
|
|
559
|
+
* dot-segment (e.g. `'a[0]b'` is ambiguous — use `'a[0].b'` instead)
|
|
467
560
|
* @example
|
|
468
|
-
*
|
|
469
|
-
*
|
|
470
|
-
*
|
|
471
|
-
*
|
|
472
|
-
*
|
|
473
|
-
* parsePath('.') // => [''] (leading dot → empty-string key)
|
|
561
|
+
* parsePropertyPath('a.b.c') // => ['a', 'b', 'c']
|
|
562
|
+
* parsePropertyPath('layers[1].x') // => ['layers', 1, 'x']
|
|
563
|
+
* parsePropertyPath('.[0]') // => [0] (leading dot stripped)
|
|
564
|
+
* parsePropertyPath('a..b') // => ['a', '', 'b'] (empty segment preserved)
|
|
565
|
+
* @since 3.0.0
|
|
474
566
|
*/
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
function parsePath(path) {
|
|
478
|
-
const cached = parsePathCache.get(path);
|
|
567
|
+
function parsePropertyPath(path) {
|
|
568
|
+
const cached = pathCache.get(path);
|
|
479
569
|
if (cached !== void 0) return cached;
|
|
480
|
-
const result = Object.freeze(
|
|
481
|
-
|
|
570
|
+
const result = Object.freeze(parseUncached(path));
|
|
571
|
+
if (pathCache.size >= MAX_CACHE_SIZE) pathCache.delete(pathCache.keys().next().value);
|
|
572
|
+
pathCache.set(path, result);
|
|
482
573
|
return result;
|
|
483
574
|
}
|
|
484
|
-
function
|
|
575
|
+
function parseUncached(path) {
|
|
485
576
|
if (path === "") return [""];
|
|
486
577
|
if (path[0] === ".") path = path.slice(1);
|
|
487
578
|
if (path === "") return [""];
|
|
@@ -496,7 +587,7 @@ function parsePathUncached(path) {
|
|
|
496
587
|
last = BRACKET.lastIndex;
|
|
497
588
|
}
|
|
498
589
|
if (last === 0) result.push(dotSegment);
|
|
499
|
-
else if (last < dotSegment.length) throw new RangeError(`
|
|
590
|
+
else if (last < dotSegment.length) throw new RangeError(`parsePropertyPath: non-bracket text '${dotSegment.slice(last)}' after ']' in segment '${dotSegment}' — use 'a[0].b' instead of 'a[0]b'`);
|
|
500
591
|
}
|
|
501
592
|
return result;
|
|
502
593
|
}
|
|
@@ -509,7 +600,7 @@ function parsePathUncached(path) {
|
|
|
509
600
|
*/
|
|
510
601
|
function get(obj, path, defaultValue) {
|
|
511
602
|
if (obj == null) return defaultValue;
|
|
512
|
-
const keys = typeof path === "string" ?
|
|
603
|
+
const keys = typeof path === "string" ? parsePropertyPath(path) : path;
|
|
513
604
|
let result = obj;
|
|
514
605
|
for (const key of keys) {
|
|
515
606
|
if (result == null || typeof result !== "object") return defaultValue;
|
|
@@ -728,6 +819,37 @@ function omit(obj, keys) {
|
|
|
728
819
|
return result;
|
|
729
820
|
}
|
|
730
821
|
//#endregion
|
|
822
|
+
//#region helpers/object/omitBy.ts
|
|
823
|
+
/**
|
|
824
|
+
* This file is part of helpers4.
|
|
825
|
+
* Copyright (C) 2025 baxyz
|
|
826
|
+
* SPDX-License-Identifier: LGPL-3.0-or-later
|
|
827
|
+
*/
|
|
828
|
+
/**
|
|
829
|
+
* Creates a new object without the own enumerable entries for which
|
|
830
|
+
* `predicate` returns `true`.
|
|
831
|
+
*
|
|
832
|
+
* Complements {@link omit} for when the keys to remove aren't known ahead of
|
|
833
|
+
* time — `omit` takes an explicit key list, `omitBy` takes a predicate.
|
|
834
|
+
*
|
|
835
|
+
* @param obj - The source object. `null`/`undefined` pass through unchanged.
|
|
836
|
+
* @param predicate - Called with `(value, key)` for each own enumerable entry
|
|
837
|
+
* @returns A new object without the matching entries
|
|
838
|
+
* @example
|
|
839
|
+
* omitBy({ a: 1, b: undefined, c: 2 }, (value) => value === undefined)
|
|
840
|
+
* // => { a: 1, c: 2 }
|
|
841
|
+
* @since 3.0.0
|
|
842
|
+
*/
|
|
843
|
+
function omitBy(obj, predicate) {
|
|
844
|
+
if (obj == null) return obj;
|
|
845
|
+
const result = {};
|
|
846
|
+
for (const key of Object.keys(obj)) {
|
|
847
|
+
const typedKey = key;
|
|
848
|
+
if (!predicate(obj[typedKey], typedKey)) result[key] = obj[typedKey];
|
|
849
|
+
}
|
|
850
|
+
return result;
|
|
851
|
+
}
|
|
852
|
+
//#endregion
|
|
731
853
|
//#region helpers/object/pick.ts
|
|
732
854
|
/**
|
|
733
855
|
* This file is part of helpers4.
|
|
@@ -744,6 +866,38 @@ function pick(obj, keys) {
|
|
|
744
866
|
return result;
|
|
745
867
|
}
|
|
746
868
|
//#endregion
|
|
869
|
+
//#region helpers/object/pickBy.ts
|
|
870
|
+
/**
|
|
871
|
+
* This file is part of helpers4.
|
|
872
|
+
* Copyright (C) 2025 baxyz
|
|
873
|
+
* SPDX-License-Identifier: LGPL-3.0-or-later
|
|
874
|
+
*/
|
|
875
|
+
/**
|
|
876
|
+
* Creates a new object with only the own enumerable entries for which
|
|
877
|
+
* `predicate` returns `true`.
|
|
878
|
+
*
|
|
879
|
+
* Complements {@link pick} for when the keys to keep aren't known ahead of
|
|
880
|
+
* time — `pick` takes an explicit key list, `pickBy` takes a predicate.
|
|
881
|
+
*
|
|
882
|
+
* @param obj - The source object. `null`/`undefined` pass through unchanged.
|
|
883
|
+
* @param predicate - Called with `(value, key)` for each own enumerable entry
|
|
884
|
+
* @returns A new object with only the matching entries
|
|
885
|
+
* @example
|
|
886
|
+
* pickBy({ a: 1, b: 0, c: 2 }, (value) => value > 0)
|
|
887
|
+
* // => { a: 1, c: 2 }
|
|
888
|
+
* @since 3.0.0
|
|
889
|
+
*/
|
|
890
|
+
function pickBy(obj, predicate) {
|
|
891
|
+
if (obj == null) return obj;
|
|
892
|
+
const result = {};
|
|
893
|
+
for (const key of Object.keys(obj)) {
|
|
894
|
+
if (UNSAFE_KEYS.has(key)) continue;
|
|
895
|
+
const typedKey = key;
|
|
896
|
+
if (predicate(obj[typedKey], typedKey)) result[key] = obj[typedKey];
|
|
897
|
+
}
|
|
898
|
+
return result;
|
|
899
|
+
}
|
|
900
|
+
//#endregion
|
|
747
901
|
//#region helpers/guard/isNullish.ts
|
|
748
902
|
/**
|
|
749
903
|
* This file is part of helpers4.
|
|
@@ -804,7 +958,7 @@ function safeJsonParse(input, fallback) {
|
|
|
804
958
|
* SPDX-License-Identifier: LGPL-3.0-or-later
|
|
805
959
|
*/
|
|
806
960
|
function set(obj, path, value) {
|
|
807
|
-
const keys = typeof path === "string" ?
|
|
961
|
+
const keys = typeof path === "string" ? parsePropertyPath(path) : path;
|
|
808
962
|
if (keys.length === 0) return obj;
|
|
809
963
|
if (keys.some((k) => typeof k === "string" && UNSAFE_KEYS.has(k))) return obj;
|
|
810
964
|
let current = obj;
|
|
@@ -817,6 +971,85 @@ function set(obj, path, value) {
|
|
|
817
971
|
return obj;
|
|
818
972
|
}
|
|
819
973
|
//#endregion
|
|
820
|
-
|
|
974
|
+
//#region helpers/object/unflatten.ts
|
|
975
|
+
/**
|
|
976
|
+
* This file is part of helpers4.
|
|
977
|
+
* Copyright (C) 2025 baxyz
|
|
978
|
+
* SPDX-License-Identifier: LGPL-3.0-or-later
|
|
979
|
+
*/
|
|
980
|
+
/**
|
|
981
|
+
* Rebuilds a nested object from a single-level object whose keys are
|
|
982
|
+
* dot-notation paths. The inverse of {@link flatten}.
|
|
983
|
+
*
|
|
984
|
+
* Uses {@link set} internally, so intermediate nodes are always created as
|
|
985
|
+
* plain objects (never arrays — see {@link flatten}'s doc for why), and any
|
|
986
|
+
* key segment equal to `__proto__`, `constructor`, or `prototype` is silently
|
|
987
|
+
* rejected (same prototype-pollution guard as `set`).
|
|
988
|
+
*
|
|
989
|
+
* @param obj - A single-level object with dot-notation keys
|
|
990
|
+
* @returns The rebuilt nested object
|
|
991
|
+
* @example
|
|
992
|
+
* unflatten({ 'a.b.c': 1, 'a.d': 2 })
|
|
993
|
+
* // => { a: { b: { c: 1 }, d: 2 } }
|
|
994
|
+
* @since 3.0.0
|
|
995
|
+
*/
|
|
996
|
+
function unflatten(obj) {
|
|
997
|
+
const result = {};
|
|
998
|
+
for (const [key, value] of Object.entries(obj)) set(result, key, value);
|
|
999
|
+
return result;
|
|
1000
|
+
}
|
|
1001
|
+
//#endregion
|
|
1002
|
+
//#region helpers/object/unset.ts
|
|
1003
|
+
/**
|
|
1004
|
+
* This file is part of helpers4.
|
|
1005
|
+
* Copyright (C) 2025 baxyz
|
|
1006
|
+
* SPDX-License-Identifier: LGPL-3.0-or-later
|
|
1007
|
+
*/
|
|
1008
|
+
/**
|
|
1009
|
+
* Removes the value at a dot/bracket-notation path or explicit key array,
|
|
1010
|
+
* mutating the object in place. Uses the same path syntax as {@link get}/{@link set}.
|
|
1011
|
+
*
|
|
1012
|
+
* A missing intermediate segment is a no-op (nothing to remove), not an error.
|
|
1013
|
+
* As with `set`, any path containing a string segment equal to `__proto__`,
|
|
1014
|
+
* `constructor`, or `prototype` is rejected and the object is returned unchanged.
|
|
1015
|
+
*
|
|
1016
|
+
* The removed key stops appearing in `Object.keys`/`for...in` — unlike setting
|
|
1017
|
+
* it to `undefined`, which would keep the key present.
|
|
1018
|
+
*
|
|
1019
|
+
* @param obj - The object to mutate
|
|
1020
|
+
* @param path - Dot/bracket-notation string, or explicit `PropertyKey[]`
|
|
1021
|
+
* @returns The same object reference, with the key removed if it existed
|
|
1022
|
+
* @example
|
|
1023
|
+
* unset({ a: { b: 1, c: 2 } }, 'a.b')
|
|
1024
|
+
* // => { a: { c: 2 } }
|
|
1025
|
+
* @example
|
|
1026
|
+
* unset({ a: 1 }, 'x.y') // path doesn't exist — no-op
|
|
1027
|
+
* // => { a: 1 }
|
|
1028
|
+
* @since 3.0.0
|
|
1029
|
+
*/
|
|
1030
|
+
function unset(obj, path) {
|
|
1031
|
+
const keys = typeof path === "string" ? parsePropertyPath(path) : path;
|
|
1032
|
+
if (keys.length === 0) return obj;
|
|
1033
|
+
if (keys.some((key) => typeof key === "string" && UNSAFE_KEYS.has(key))) return obj;
|
|
1034
|
+
let current = obj;
|
|
1035
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
1036
|
+
if (current == null || typeof current !== "object") return obj;
|
|
1037
|
+
current = current[keys[i]];
|
|
1038
|
+
}
|
|
1039
|
+
if (current != null && typeof current === "object") delete current[keys[keys.length - 1]];
|
|
1040
|
+
return obj;
|
|
1041
|
+
}
|
|
1042
|
+
//#endregion
|
|
1043
|
+
//#region helpers/object/update.ts
|
|
1044
|
+
/**
|
|
1045
|
+
* This file is part of helpers4.
|
|
1046
|
+
* Copyright (C) 2025 baxyz
|
|
1047
|
+
* SPDX-License-Identifier: LGPL-3.0-or-later
|
|
1048
|
+
*/
|
|
1049
|
+
function update(obj, path, updater) {
|
|
1050
|
+
return set(obj, path, updater(get(obj, path)));
|
|
1051
|
+
}
|
|
1052
|
+
//#endregion
|
|
1053
|
+
export { clone, cloneDeep, compact, diff, equalsDeep, equalsShallow, flatten, get, groupBy, invert, isEmpty, isNonEmpty, map, mergeDeep, omit, omitBy, parsePropertyPath, pick, pickBy, removeUndefinedNull, safeJsonParse, set, unflatten, unset, update };
|
|
821
1054
|
|
|
822
1055
|
//# sourceMappingURL=index.js.map
|