@web-ts-toolkit/access-router 0.32.0 → 0.34.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/processors.d.mts CHANGED
@@ -1,11 +1,82 @@
1
+ /**
2
+ * A depopulate copy operation consumed by {@link copyAndDepopulate}.
3
+ */
1
4
  interface ProcessCopy {
5
+ /**
6
+ * Dotted source path on `docObject` whose populated value should be
7
+ * relocated to {@link ProcessCopy.dest}. {@link copyAndDepopulate} walks
8
+ * intermediate segments and, at the leaf, copies the original objects to
9
+ * {@link ProcessCopy.dest} and replaces the leaf with the `idField`
10
+ * value(s) of those objects.
11
+ *
12
+ * Examples:
13
+ * `src: 'items'` -> depopulate `docObject.items`
14
+ * `src: 'pear.items'` -> depopulate `docObject.pear.items`
15
+ *
16
+ * Empty strings are treated as safe no-ops. Unsafe segments
17
+ * (`__proto__`, `prototype`, `constructor`) throw a descriptive
18
+ * `ProcessorPathError`.
19
+ */
2
20
  src: string;
21
+ /**
22
+ * Dotted destination path on `docObject` where the moved objects will be
23
+ * written. Unsafe segments (`__proto__`, `prototype`, `constructor`)
24
+ * throw a descriptive `ProcessorPathError`. Empty and missing
25
+ * destinations are safe no-ops.
26
+ */
3
27
  dest: string;
4
28
  }
29
+ /**
30
+ * Options for {@link copyAndDepopulate}.
31
+ */
5
32
  interface CopyAndDepopulateOptions {
33
+ /**
34
+ * When `true` (default), `docObject` is mutated in place and returned.
35
+ * When `false`, a deep clone is produced via `cloneDeep` and the input is
36
+ * left untouched. Both modes produce value-identical output; only object
37
+ * identity differs.
38
+ */
6
39
  mutable?: boolean;
40
+ /**
41
+ * Identifier field pulled from each relocated record to replace the
42
+ * original populated value. Defaults to `'_id'`.
43
+ *
44
+ * For nested populated arrays, every leaf record must carry this field.
45
+ * Records missing the id field throw a descriptive `ProcessorPathError`.
46
+ * Primitive array members (non-record scalars) cannot be depopulated and
47
+ * are left in place as a safe no-op.
48
+ */
7
49
  idField?: string;
8
50
  }
51
+ /**
52
+ * Hardened processor that relocates populated documents to a destination
53
+ * path and replaces the original location with their proposed
54
+ * "depopulated" form: a single id for plain-object values, or an array of
55
+ * ids for populated arrays.
56
+ *
57
+ * Key semantics:
58
+ * - Operations run sequentially in the order supplied. Each operation
59
+ * sees the state produced by the previous one. Two operations that
60
+ * reference the same `src` therefore intentionally chain - the second
61
+ * operation will find the id value/type left by the first and treat it
62
+ * as a safe no-op if it can no longer be depopulated (e.g. it is now an
63
+ * array of primitive ids rather than records).
64
+ * - Missing, null, or scalar intermediate segments in `src` are safe
65
+ * no-ops: nothing is copied, nothing is replaced, and no error is
66
+ * thrown. The object identity of intermediates is unchanged.
67
+ * - An array leaf whose members are all plain records is depopulated to
68
+ * an array of ids. Records missing the id field throw a descriptive
69
+ * error; primitive/scalar array members prevent depopulation and the
70
+ * leaf is left in place as a safe no-op.
71
+ * - A plain-object leaf is depopulated to a single id value; a missing id
72
+ * field throws a descriptive error.
73
+ * - `__proto__`, `prototype`, and `constructor` segments in either `src`
74
+ * or `dest` throw a descriptive error. No operation can mutate
75
+ * `Object.prototype` or any object's prototype.
76
+ * - Empty `src` or `dest` strings are safe no-ops.
77
+ *
78
+ * @typeParam T - Object shape produced by the depopulation.
79
+ */
9
80
  declare const copyAndDepopulate: <T extends object>(docObject: T, operations: ProcessCopy[], options?: CopyAndDepopulateOptions) => T;
10
81
 
11
- export { copyAndDepopulate };
82
+ export { type CopyAndDepopulateOptions, type ProcessCopy, copyAndDepopulate };
package/processors.d.ts CHANGED
@@ -1,11 +1,82 @@
1
+ /**
2
+ * A depopulate copy operation consumed by {@link copyAndDepopulate}.
3
+ */
1
4
  interface ProcessCopy {
5
+ /**
6
+ * Dotted source path on `docObject` whose populated value should be
7
+ * relocated to {@link ProcessCopy.dest}. {@link copyAndDepopulate} walks
8
+ * intermediate segments and, at the leaf, copies the original objects to
9
+ * {@link ProcessCopy.dest} and replaces the leaf with the `idField`
10
+ * value(s) of those objects.
11
+ *
12
+ * Examples:
13
+ * `src: 'items'` -> depopulate `docObject.items`
14
+ * `src: 'pear.items'` -> depopulate `docObject.pear.items`
15
+ *
16
+ * Empty strings are treated as safe no-ops. Unsafe segments
17
+ * (`__proto__`, `prototype`, `constructor`) throw a descriptive
18
+ * `ProcessorPathError`.
19
+ */
2
20
  src: string;
21
+ /**
22
+ * Dotted destination path on `docObject` where the moved objects will be
23
+ * written. Unsafe segments (`__proto__`, `prototype`, `constructor`)
24
+ * throw a descriptive `ProcessorPathError`. Empty and missing
25
+ * destinations are safe no-ops.
26
+ */
3
27
  dest: string;
4
28
  }
29
+ /**
30
+ * Options for {@link copyAndDepopulate}.
31
+ */
5
32
  interface CopyAndDepopulateOptions {
33
+ /**
34
+ * When `true` (default), `docObject` is mutated in place and returned.
35
+ * When `false`, a deep clone is produced via `cloneDeep` and the input is
36
+ * left untouched. Both modes produce value-identical output; only object
37
+ * identity differs.
38
+ */
6
39
  mutable?: boolean;
40
+ /**
41
+ * Identifier field pulled from each relocated record to replace the
42
+ * original populated value. Defaults to `'_id'`.
43
+ *
44
+ * For nested populated arrays, every leaf record must carry this field.
45
+ * Records missing the id field throw a descriptive `ProcessorPathError`.
46
+ * Primitive array members (non-record scalars) cannot be depopulated and
47
+ * are left in place as a safe no-op.
48
+ */
7
49
  idField?: string;
8
50
  }
51
+ /**
52
+ * Hardened processor that relocates populated documents to a destination
53
+ * path and replaces the original location with their proposed
54
+ * "depopulated" form: a single id for plain-object values, or an array of
55
+ * ids for populated arrays.
56
+ *
57
+ * Key semantics:
58
+ * - Operations run sequentially in the order supplied. Each operation
59
+ * sees the state produced by the previous one. Two operations that
60
+ * reference the same `src` therefore intentionally chain - the second
61
+ * operation will find the id value/type left by the first and treat it
62
+ * as a safe no-op if it can no longer be depopulated (e.g. it is now an
63
+ * array of primitive ids rather than records).
64
+ * - Missing, null, or scalar intermediate segments in `src` are safe
65
+ * no-ops: nothing is copied, nothing is replaced, and no error is
66
+ * thrown. The object identity of intermediates is unchanged.
67
+ * - An array leaf whose members are all plain records is depopulated to
68
+ * an array of ids. Records missing the id field throw a descriptive
69
+ * error; primitive/scalar array members prevent depopulation and the
70
+ * leaf is left in place as a safe no-op.
71
+ * - A plain-object leaf is depopulated to a single id value; a missing id
72
+ * field throws a descriptive error.
73
+ * - `__proto__`, `prototype`, and `constructor` segments in either `src`
74
+ * or `dest` throw a descriptive error. No operation can mutate
75
+ * `Object.prototype` or any object's prototype.
76
+ * - Empty `src` or `dest` strings are safe no-ops.
77
+ *
78
+ * @typeParam T - Object shape produced by the depopulation.
79
+ */
9
80
  declare const copyAndDepopulate: <T extends object>(docObject: T, operations: ProcessCopy[], options?: CopyAndDepopulateOptions) => T;
10
81
 
11
- export { copyAndDepopulate };
82
+ export { type CopyAndDepopulateOptions, type ProcessCopy, copyAndDepopulate };
package/processors.js CHANGED
@@ -23,29 +23,77 @@ __export(processors_exports, {
23
23
  });
24
24
  module.exports = __toCommonJS(processors_exports);
25
25
  var import_utils = require("@web-ts-toolkit/utils");
26
+ var UNSAFE_PATH_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
27
+ function failUnsafeSegment(path) {
28
+ throw new Error(
29
+ `copyAndDepopulate: refusing path '${path}' because it contains an unsafe segment (__proto__, prototype, or constructor).`
30
+ );
31
+ }
32
+ function failMissingId(path) {
33
+ throw new Error(`copyAndDepopulate: path '${path}' resolved to a record missing the configured id field.`);
34
+ }
35
+ function assertSafePath(path) {
36
+ const segs = path.split(".");
37
+ for (const seg of segs) {
38
+ if (seg.length === 0) continue;
39
+ if (UNSAFE_PATH_KEYS.has(seg)) {
40
+ failUnsafeSegment(path);
41
+ }
42
+ }
43
+ }
26
44
  var copyAndDepopulate = (docObject, operations, options = { mutable: true, idField: "_id" }) => {
27
- const obj = (0, import_utils.get)(options, "mutable", true) ? docObject : (0, import_utils.cloneDeep)(docObject);
28
- const idField = (0, import_utils.get)(options, "idField", "_id");
29
- (0, import_utils.forEach)((0, import_utils.castArray)(operations), (op) => {
30
- if (!op.src || !op.dest) return;
45
+ const mutable = options.mutable !== false;
46
+ const idField = options.idField ?? "_id";
47
+ const obj = mutable ? docObject : (0, import_utils.cloneDeep)(docObject);
48
+ (0, import_utils.forEach)(Array.isArray(operations) ? operations : [], (op) => {
49
+ if (!op || typeof op.src !== "string" || typeof op.dest !== "string") return;
50
+ const src = op.src;
51
+ const dest = op.dest;
52
+ if (src.length === 0 || dest.length === 0) return;
53
+ assertSafePath(src);
54
+ assertSafePath(dest);
31
55
  let targets = [obj];
32
- const segs = op.src.split(".");
56
+ const segs = src.split(".");
33
57
  (0, import_utils.forEach)(segs, (seg, ind) => {
34
58
  if (segs.length === ind + 1) {
35
59
  (0, import_utils.forEach)(targets, (target) => {
60
+ if (!(0, import_utils.isPlainObject)(target)) return;
36
61
  const targetObject = target;
37
- (0, import_utils.set)(targetObject, op.dest, (0, import_utils.get)(targetObject, seg));
38
- (0, import_utils.set)(
39
- targetObject,
40
- seg,
41
- (0, import_utils.isArray)(targetObject[seg]) ? (0, import_utils.map)(targetObject[seg], idField) : (0, import_utils.get)(targetObject, `${seg}.${idField}`)
42
- );
62
+ const leaf = targetObject[seg];
63
+ const sourceForDest = leaf;
64
+ let depopulated;
65
+ if ((0, import_utils.isArray)(leaf)) {
66
+ if (leaf.length === 0) {
67
+ depopulated = [];
68
+ } else if (leaf.every((item) => (0, import_utils.isPlainObject)(item))) {
69
+ depopulated = (0, import_utils.map)(leaf, idField).map((id, i) => {
70
+ if (id === void 0) {
71
+ failMissingId(`${src}[${i}]`);
72
+ }
73
+ return id;
74
+ });
75
+ } else {
76
+ return;
77
+ }
78
+ } else if ((0, import_utils.isPlainObject)(leaf)) {
79
+ const id = (0, import_utils.get)(leaf, idField);
80
+ if (id === void 0) {
81
+ failMissingId(src);
82
+ }
83
+ depopulated = id;
84
+ } else {
85
+ return;
86
+ }
87
+ (0, import_utils.set)(targetObject, dest, sourceForDest);
88
+ (0, import_utils.set)(targetObject, seg, depopulated);
43
89
  });
44
90
  } else {
45
91
  targets = targets.reduce((ret, target) => {
92
+ if (!(0, import_utils.isPlainObject)(target)) return ret;
46
93
  const targetObject = target;
47
- if ((0, import_utils.isArray)(targetObject[seg])) ret.push(...targetObject[seg]);
48
- else ret.push(targetObject[seg]);
94
+ const next = targetObject[seg];
95
+ if ((0, import_utils.isArray)(next)) ret.push(...next);
96
+ else if (next !== null && next !== void 0) ret.push(next);
49
97
  return ret;
50
98
  }, []);
51
99
  }
package/processors.mjs CHANGED
@@ -1,28 +1,76 @@
1
1
  // src/processors.ts
2
- import { castArray, cloneDeep, forEach, get, isArray, map, set } from "@web-ts-toolkit/utils";
2
+ import { cloneDeep, forEach, get, isArray, isPlainObject, map, set } from "@web-ts-toolkit/utils";
3
+ var UNSAFE_PATH_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
4
+ function failUnsafeSegment(path) {
5
+ throw new Error(
6
+ `copyAndDepopulate: refusing path '${path}' because it contains an unsafe segment (__proto__, prototype, or constructor).`
7
+ );
8
+ }
9
+ function failMissingId(path) {
10
+ throw new Error(`copyAndDepopulate: path '${path}' resolved to a record missing the configured id field.`);
11
+ }
12
+ function assertSafePath(path) {
13
+ const segs = path.split(".");
14
+ for (const seg of segs) {
15
+ if (seg.length === 0) continue;
16
+ if (UNSAFE_PATH_KEYS.has(seg)) {
17
+ failUnsafeSegment(path);
18
+ }
19
+ }
20
+ }
3
21
  var copyAndDepopulate = (docObject, operations, options = { mutable: true, idField: "_id" }) => {
4
- const obj = get(options, "mutable", true) ? docObject : cloneDeep(docObject);
5
- const idField = get(options, "idField", "_id");
6
- forEach(castArray(operations), (op) => {
7
- if (!op.src || !op.dest) return;
22
+ const mutable = options.mutable !== false;
23
+ const idField = options.idField ?? "_id";
24
+ const obj = mutable ? docObject : cloneDeep(docObject);
25
+ forEach(Array.isArray(operations) ? operations : [], (op) => {
26
+ if (!op || typeof op.src !== "string" || typeof op.dest !== "string") return;
27
+ const src = op.src;
28
+ const dest = op.dest;
29
+ if (src.length === 0 || dest.length === 0) return;
30
+ assertSafePath(src);
31
+ assertSafePath(dest);
8
32
  let targets = [obj];
9
- const segs = op.src.split(".");
33
+ const segs = src.split(".");
10
34
  forEach(segs, (seg, ind) => {
11
35
  if (segs.length === ind + 1) {
12
36
  forEach(targets, (target) => {
37
+ if (!isPlainObject(target)) return;
13
38
  const targetObject = target;
14
- set(targetObject, op.dest, get(targetObject, seg));
15
- set(
16
- targetObject,
17
- seg,
18
- isArray(targetObject[seg]) ? map(targetObject[seg], idField) : get(targetObject, `${seg}.${idField}`)
19
- );
39
+ const leaf = targetObject[seg];
40
+ const sourceForDest = leaf;
41
+ let depopulated;
42
+ if (isArray(leaf)) {
43
+ if (leaf.length === 0) {
44
+ depopulated = [];
45
+ } else if (leaf.every((item) => isPlainObject(item))) {
46
+ depopulated = map(leaf, idField).map((id, i) => {
47
+ if (id === void 0) {
48
+ failMissingId(`${src}[${i}]`);
49
+ }
50
+ return id;
51
+ });
52
+ } else {
53
+ return;
54
+ }
55
+ } else if (isPlainObject(leaf)) {
56
+ const id = get(leaf, idField);
57
+ if (id === void 0) {
58
+ failMissingId(src);
59
+ }
60
+ depopulated = id;
61
+ } else {
62
+ return;
63
+ }
64
+ set(targetObject, dest, sourceForDest);
65
+ set(targetObject, seg, depopulated);
20
66
  });
21
67
  } else {
22
68
  targets = targets.reduce((ret, target) => {
69
+ if (!isPlainObject(target)) return ret;
23
70
  const targetObject = target;
24
- if (isArray(targetObject[seg])) ret.push(...targetObject[seg]);
25
- else ret.push(targetObject[seg]);
71
+ const next = targetObject[seg];
72
+ if (isArray(next)) ret.push(...next);
73
+ else if (next !== null && next !== void 0) ret.push(next);
26
74
  return ret;
27
75
  }, []);
28
76
  }