@helpers4/object 3.0.0-alpha.2 → 3.0.1

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/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.
@@ -449,43 +487,92 @@ function equalsShallow(objA, objB) {
449
487
  return true;
450
488
  }
451
489
  //#endregion
452
- //#region helpers/object/_parsePath.ts
490
+ //#region helpers/object/flatten.ts
453
491
  /**
454
492
  * This file is part of helpers4.
455
493
  * Copyright (C) 2025 baxyz
456
494
  * SPDX-License-Identifier: LGPL-3.0-or-later
457
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
+ }
458
503
  /**
459
- * Parses a string path into an array of string/number segments.
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}.
460
545
  *
461
546
  * - Dot separators (`.`) split segments; each segment becomes a string key.
462
547
  * - Bracket indices (`[n]`) become number keys.
463
548
  * - A leading `.` is treated as "current level" and stripped before parsing,
464
549
  * so `.[0]` ≡ `[0]` and `.a.b` ≡ `a.b`.
465
- * - 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).
466
551
  * - Consecutive dots (`a..b`) produce an empty-string segment: `['a', '', 'b']`.
467
- * - Non-bracket text trailing a `]` within the same dot-segment throws a RangeError;
468
- * use a dot separator instead (`'a[0].b'` not `'a[0]b'`).
469
552
  *
470
- * @ignore
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)
471
560
  * @example
472
- * parsePath('a.b.c') // => ['a', 'b', 'c'] (all strings)
473
- * parsePath('layers.1.x') // => ['layers', '1', 'x'] (all strings)
474
- * parsePath('layers[1].x') // => ['layers', 1, 'x'] (bracket → number)
475
- * parsePath('.[0]') // => [0] (leading dot stripped)
476
- * parsePath('a..b') // => ['a', '', 'b'] (empty segment preserved)
477
- * 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
478
566
  */
479
- var BRACKET = /\[(\d+)\]/g;
480
- var parsePathCache = /* @__PURE__ */ new Map();
481
- function parsePath(path) {
482
- const cached = parsePathCache.get(path);
567
+ function parsePropertyPath(path) {
568
+ const cached = pathCache.get(path);
483
569
  if (cached !== void 0) return cached;
484
- const result = Object.freeze(parsePathUncached(path));
485
- parsePathCache.set(path, result);
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);
486
573
  return result;
487
574
  }
488
- function parsePathUncached(path) {
575
+ function parseUncached(path) {
489
576
  if (path === "") return [""];
490
577
  if (path[0] === ".") path = path.slice(1);
491
578
  if (path === "") return [""];
@@ -500,7 +587,7 @@ function parsePathUncached(path) {
500
587
  last = BRACKET.lastIndex;
501
588
  }
502
589
  if (last === 0) result.push(dotSegment);
503
- else if (last < dotSegment.length) throw new RangeError(`parsePath: non-bracket text '${dotSegment.slice(last)}' after ']' in segment '${dotSegment}' — use 'a[0].b' instead of 'a[0]b'`);
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'`);
504
591
  }
505
592
  return result;
506
593
  }
@@ -513,7 +600,7 @@ function parsePathUncached(path) {
513
600
  */
514
601
  function get(obj, path, defaultValue) {
515
602
  if (obj == null) return defaultValue;
516
- const keys = typeof path === "string" ? parsePath(path) : path;
603
+ const keys = typeof path === "string" ? parsePropertyPath(path) : path;
517
604
  let result = obj;
518
605
  for (const key of keys) {
519
606
  if (result == null || typeof result !== "object") return defaultValue;
@@ -732,6 +819,37 @@ function omit(obj, keys) {
732
819
  return result;
733
820
  }
734
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
735
853
  //#region helpers/object/pick.ts
736
854
  /**
737
855
  * This file is part of helpers4.
@@ -748,6 +866,38 @@ function pick(obj, keys) {
748
866
  return result;
749
867
  }
750
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
751
901
  //#region helpers/guard/isNullish.ts
752
902
  /**
753
903
  * This file is part of helpers4.
@@ -808,7 +958,7 @@ function safeJsonParse(input, fallback) {
808
958
  * SPDX-License-Identifier: LGPL-3.0-or-later
809
959
  */
810
960
  function set(obj, path, value) {
811
- const keys = typeof path === "string" ? parsePath(path) : path;
961
+ const keys = typeof path === "string" ? parsePropertyPath(path) : path;
812
962
  if (keys.length === 0) return obj;
813
963
  if (keys.some((k) => typeof k === "string" && UNSAFE_KEYS.has(k))) return obj;
814
964
  let current = obj;
@@ -821,6 +971,85 @@ function set(obj, path, value) {
821
971
  return obj;
822
972
  }
823
973
  //#endregion
824
- export { cloneDeep, compact, diff, equalsDeep, equalsShallow, get, groupBy, invert, isEmpty, isNonEmpty, map, mergeDeep, omit, pick, removeUndefinedNull, safeJsonParse, set };
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 };
825
1054
 
826
1055
  //# sourceMappingURL=index.js.map