@web-ts-toolkit/access-router 0.44.0 → 0.46.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/index.mjs CHANGED
@@ -1,16 +1,16 @@
1
1
  // src/index.ts
2
- import { isPlainObject as isPlainObject18, isString as isString10, isUndefined as isUndefined4 } from "@web-ts-toolkit/utils";
2
+ import { isPlainObject as isPlainObject21, isString as isString10, isUndefined as isUndefined4 } from "@web-ts-toolkit/utils";
3
3
 
4
4
  // src/middleware.ts
5
5
  import JsonRouter2 from "@web-ts-toolkit/express-json-router";
6
- import { isArray as isArray11, isFunction as isFunction6, isPlainObject as isPlainObject15, isString as isString7 } from "@web-ts-toolkit/utils";
6
+ import { isArray as isArray12, isFunction as isFunction6, isPlainObject as isPlainObject17, isString as isString7 } from "@web-ts-toolkit/utils";
7
7
 
8
8
  // src/core.ts
9
9
  import {
10
10
  castArray as castArray4,
11
11
  compact as compact3,
12
12
  forEach as forEach7,
13
- isArray as isArray10,
13
+ isArray as isArray11,
14
14
  isBoolean as isBoolean5,
15
15
  isFunction as isFunction5,
16
16
  isString as isString6,
@@ -31,7 +31,8 @@ import {
31
31
  isNil as isNil2,
32
32
  isPlainObject as isPlainObject10,
33
33
  isString as isString4,
34
- keys
34
+ keys,
35
+ normalizeUrlPath
35
36
  } from "@web-ts-toolkit/utils";
36
37
 
37
38
  // src/helpers/collection.ts
@@ -65,8 +66,12 @@ var mapWithConcurrencyLimit = async (items, limit, iteratee) => {
65
66
  };
66
67
  var RequestConcurrencyScheduler = class {
67
68
  constructor(limit) {
69
+ this.activeWork = 0;
70
+ this.waiters = [];
68
71
  this.limit = Math.max(1, limit);
69
72
  }
73
+ // Each map bounds its own orchestration workers. Recursive maps never hold
74
+ // shared work permits while awaiting descendants (including at limit 1).
70
75
  async map(items, iteratee) {
71
76
  return mapWithConcurrencyLimit(items, this.limit, (item, index) => iteratee(item, index, true));
72
77
  }
@@ -74,6 +79,23 @@ var RequestConcurrencyScheduler = class {
74
79
  const [result] = await this.map([void 0], () => iteratee(true));
75
80
  return result;
76
81
  }
82
+ // Only leaf persistence operations belong here, not service/hook callbacks.
83
+ // All work calls on this scheduler share a FIFO admission ceiling.
84
+ async work(operation) {
85
+ if (this.activeWork >= this.limit) {
86
+ await new Promise((resolve) => this.waiters.push(resolve));
87
+ } else {
88
+ this.activeWork += 1;
89
+ }
90
+ try {
91
+ return await operation();
92
+ } finally {
93
+ const next = this.waiters.shift();
94
+ if (next)
95
+ next();
96
+ else this.activeWork -= 1;
97
+ }
98
+ }
77
99
  };
78
100
 
79
101
  // src/helpers/document.ts
@@ -943,7 +965,9 @@ var defaultRequestComplexity = {
943
965
  maxIncludeCount: 10,
944
966
  maxSubQueryCount: 10,
945
967
  maxBulkConcurrency: 10,
946
- maxHookConcurrency: 10
968
+ maxHookConcurrency: 10,
969
+ maxCorrelatedQueries: 100,
970
+ maxCorrelatedDepth: 5
947
971
  };
948
972
  var normalizePositiveInteger = (value, fallback) => {
949
973
  return Number.isSafeInteger(value) && Number(value) > 0 ? Number(value) : fallback;
@@ -963,6 +987,14 @@ var resolveRequestComplexity = (options) => ({
963
987
  maxHookConcurrency: normalizePositiveInteger(
964
988
  options?.maxHookConcurrency,
965
989
  defaultRequestComplexity.maxHookConcurrency
990
+ ),
991
+ maxCorrelatedQueries: normalizePositiveInteger(
992
+ options?.maxCorrelatedQueries,
993
+ defaultRequestComplexity.maxCorrelatedQueries
994
+ ),
995
+ maxCorrelatedDepth: normalizePositiveInteger(
996
+ options?.maxCorrelatedDepth,
997
+ defaultRequestComplexity.maxCorrelatedDepth
966
998
  )
967
999
  });
968
1000
  function validateRequestComplexity(value, options, scope = "request") {
@@ -1155,6 +1187,7 @@ var AccessRuntime = class {
1155
1187
  this.modelAtts = {};
1156
1188
  this.modelInstances = {};
1157
1189
  this.openApiRegistry = new OpenApiRegistry();
1190
+ this.openApiPathPrefix = "";
1158
1191
  this.allowGlobalModelLookup = options.allowGlobalModelLookup ?? false;
1159
1192
  ensureMongooseJsonSchemaInitialized();
1160
1193
  }
@@ -1183,7 +1216,24 @@ var AccessRuntime = class {
1183
1216
  return global;
1184
1217
  }
1185
1218
  registerOpenApiRoute(route) {
1186
- this.openApiRegistry.register(route);
1219
+ this.openApiRegistry.register(
1220
+ this.openApiPathPrefix ? { ...route, path: normalizeUrlPath(`${this.openApiPathPrefix}/${route.path}`) } : route
1221
+ );
1222
+ }
1223
+ /**
1224
+ * Compose OpenAPI paths while synchronously constructing mounted routers.
1225
+ * Prefixes apply before collision checks; prior descriptors and Express paths
1226
+ * are unchanged. Nested calls compose and the prior scope is restored even on
1227
+ * failure. The callback must finish registration synchronously (no async work).
1228
+ */
1229
+ withOpenApiPathPrefix(prefix, build) {
1230
+ const previous = this.openApiPathPrefix;
1231
+ this.openApiPathPrefix = normalizeUrlPath(`${previous}/${prefix}`);
1232
+ try {
1233
+ return build();
1234
+ } finally {
1235
+ this.openApiPathPrefix = previous;
1236
+ }
1187
1237
  }
1188
1238
  /**
1189
1239
  * Enable strict OpenAPI collision detection for the remainder of this
@@ -1550,14 +1600,493 @@ var getModelRef = (modelName, refPath) => {
1550
1600
  // src/services/base.ts
1551
1601
  import {
1552
1602
  castArray,
1603
+ cloneDeep as cloneDeep2,
1553
1604
  compact,
1554
1605
  flatten,
1555
1606
  forEach as forEach3,
1556
1607
  get as get4,
1557
- isArray as isArray6,
1558
- isPlainObject as isPlainObject11,
1608
+ isArray as isArray7,
1609
+ isPlainObject as isPlainObject12,
1610
+ set as set3,
1559
1611
  uniq
1560
1612
  } from "@web-ts-toolkit/utils";
1613
+
1614
+ // src/correlated-includes.ts
1615
+ import { cloneDeep, isArray as isArray6, isPlainObject as isPlainObject11 } from "@web-ts-toolkit/utils";
1616
+ var PARENT_MARKER_KEY = "$parent";
1617
+ var ESCAPE_WRAPPER_KEY = "$escape";
1618
+ var SUBQUERY_PAYLOAD_KEYS = /* @__PURE__ */ new Set(["$$sq", "$$date"]);
1619
+ var MARKER_FORBIDDEN_OPERATOR_KEYS = /* @__PURE__ */ new Set(["$text", "$where", "$comment"]);
1620
+ var LOGICAL_CLAUSE_KEYS = /* @__PURE__ */ new Set(["$and", "$or", "$nor"]);
1621
+ var DANGEROUS_PATH_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
1622
+ var FIELD_SEGMENT_PATTERN = /^[A-Za-z_][A-Za-z0-9_$]*$/;
1623
+ var ARRAY_INDEX_SEGMENT_PATTERN = /^(0|[1-9]\d*)$/;
1624
+ var ownKeys = (value) => Object.keys(value);
1625
+ function isParentRefMarker(value) {
1626
+ return isPlainObject11(value) && ownKeys(value).length === 1 && ownKeys(value)[0] === PARENT_MARKER_KEY;
1627
+ }
1628
+ function isEscapedParentRef(value) {
1629
+ return isPlainObject11(value) && ownKeys(value).length === 1 && ownKeys(value)[0] === ESCAPE_WRAPPER_KEY;
1630
+ }
1631
+ function hasMarkerKey(value) {
1632
+ return ownKeys(value).includes(PARENT_MARKER_KEY) || ownKeys(value).includes(ESCAPE_WRAPPER_KEY);
1633
+ }
1634
+ function isValidParentRefPath(path) {
1635
+ if (typeof path !== "string" || path.length === 0) return false;
1636
+ const segments = path.split(".");
1637
+ return segments.every(
1638
+ (segment) => (FIELD_SEGMENT_PATTERN.test(segment) || ARRAY_INDEX_SEGMENT_PATTERN.test(segment)) && !DANGEROUS_PATH_SEGMENTS.has(segment)
1639
+ );
1640
+ }
1641
+ function isValidCorrelatedOutputPath(path) {
1642
+ return typeof path === "string" && path.length > 0 && isValidFieldPath(path) && path !== "_id" && !path.startsWith("$");
1643
+ }
1644
+ function containsParentMarkerShape(value) {
1645
+ const seen = /* @__PURE__ */ new Set();
1646
+ const visit = (current) => {
1647
+ if (isParentRefMarker(current) || isEscapedParentRef(current)) return true;
1648
+ if (isArray6(current)) {
1649
+ if (seen.has(current)) return false;
1650
+ seen.add(current);
1651
+ return current.some(visit);
1652
+ }
1653
+ if (isPlainObject11(current)) {
1654
+ if (seen.has(current)) return false;
1655
+ seen.add(current);
1656
+ if (hasMarkerKey(current)) return true;
1657
+ return Object.values(current).some(visit);
1658
+ }
1659
+ return false;
1660
+ };
1661
+ return visit(value);
1662
+ }
1663
+ function scanTemplateValueNode(value, path, issues) {
1664
+ if (isEscapedParentRef(value)) {
1665
+ const inner = value.$escape;
1666
+ if (!isParentRefMarker(inner) || typeof inner.$parent !== "string" || (inner.$parent?.length ?? 0) === 0) {
1667
+ issues.push({
1668
+ message: 'Invalid $escape payload: expected { "$escape": { "$parent": "<field>" } }',
1669
+ path
1670
+ });
1671
+ }
1672
+ return;
1673
+ }
1674
+ if (isParentRefMarker(value)) {
1675
+ if (!isValidParentRefPath(value.$parent)) {
1676
+ issues.push({
1677
+ message: `Invalid $parent reference path at ${path.length === 0 ? "#" : path.join(".")}`,
1678
+ path
1679
+ });
1680
+ }
1681
+ return;
1682
+ }
1683
+ if (isPlainObject11(value)) {
1684
+ if (hasMarkerKey(value)) {
1685
+ issues.push({
1686
+ message: 'Malformed $parent reference: objects carrying $parent must be exactly { "$parent": "<field>" }',
1687
+ path
1688
+ });
1689
+ return;
1690
+ }
1691
+ scanTemplateObjectEntries(value, path, issues);
1692
+ return;
1693
+ }
1694
+ if (isArray6(value)) {
1695
+ value.forEach((item, index) => scanTemplateValueNode(item, [...path, index], issues));
1696
+ }
1697
+ }
1698
+ function scanTemplateObjectEntries(obj, basePath, issues) {
1699
+ for (const [key, child] of Object.entries(obj)) {
1700
+ const childPath = [...basePath, key];
1701
+ if (key === PARENT_MARKER_KEY) {
1702
+ issues.push({ message: "$parent is never a field name in correlated templates", path: childPath });
1703
+ continue;
1704
+ }
1705
+ if (SUBQUERY_PAYLOAD_KEYS.has(key)) {
1706
+ if (containsParentMarkerShape(child)) {
1707
+ issues.push({
1708
+ message: `Unsupported $parent reference inside ${key}: markers are not resolved in subquery payloads`,
1709
+ path: childPath
1710
+ });
1711
+ }
1712
+ continue;
1713
+ }
1714
+ if (MARKER_FORBIDDEN_OPERATOR_KEYS.has(key)) {
1715
+ if (containsParentMarkerShape(child)) {
1716
+ issues.push({
1717
+ message: `Unsupported $parent reference inside ${key}: markers are not resolved in ${key} values`,
1718
+ path: childPath
1719
+ });
1720
+ }
1721
+ continue;
1722
+ }
1723
+ if (key === "$elemMatch") {
1724
+ if (isParentRefMarker(child) || isEscapedParentRef(child)) {
1725
+ issues.push({
1726
+ message: "Unsupported $parent reference as a direct $elemMatch value",
1727
+ path: childPath
1728
+ });
1729
+ continue;
1730
+ }
1731
+ scanTemplateValueNode(child, childPath, issues);
1732
+ continue;
1733
+ }
1734
+ if (LOGICAL_CLAUSE_KEYS.has(key)) {
1735
+ if (!isArray6(child)) {
1736
+ scanTemplateValueNode(child, childPath, issues);
1737
+ continue;
1738
+ }
1739
+ child.forEach((clause, index) => {
1740
+ const clausePath = [...childPath, index];
1741
+ if (isParentRefMarker(clause) || isEscapedParentRef(clause)) {
1742
+ issues.push({
1743
+ message: `Unsupported $parent reference as a direct ${key} clause: clauses must be filter objects`,
1744
+ path: clausePath
1745
+ });
1746
+ return;
1747
+ }
1748
+ if (isPlainObject11(clause)) {
1749
+ scanTemplateObjectEntries(clause, clausePath, issues);
1750
+ }
1751
+ });
1752
+ continue;
1753
+ }
1754
+ scanTemplateValueNode(child, childPath, issues);
1755
+ }
1756
+ }
1757
+ function validateCorrelatedFilterTemplate(filter2) {
1758
+ const issues = [];
1759
+ if (!isPlainObject11(filter2)) {
1760
+ issues.push({ message: "Correlated include filter must be an object", path: [] });
1761
+ return issues;
1762
+ }
1763
+ if (isParentRefMarker(filter2) || isEscapedParentRef(filter2)) {
1764
+ issues.push({ message: "Correlated include filter must not itself be a $parent reference", path: [] });
1765
+ return issues;
1766
+ }
1767
+ scanTemplateObjectEntries(filter2, [], issues);
1768
+ return issues;
1769
+ }
1770
+ function validateCorrelatedIdTemplate(id) {
1771
+ if (typeof id === "string") {
1772
+ return id.length > 0 ? [] : [{ message: "Correlated include id must be a non-empty string or $parent reference", path: [] }];
1773
+ }
1774
+ if (isEscapedParentRef(id)) {
1775
+ return [{ message: "Unsupported $escape as a correlated include id", path: [] }];
1776
+ }
1777
+ if (isParentRefMarker(id)) {
1778
+ return isValidParentRefPath(id.$parent) ? [] : [{ message: "Invalid $parent reference path in correlated include id", path: [] }];
1779
+ }
1780
+ return [{ message: "Correlated include id must be a string or $parent reference", path: [] }];
1781
+ }
1782
+ function findArgsMarkerIssue(value, where) {
1783
+ if (value !== void 0 && containsParentMarkerShape(value)) {
1784
+ return {
1785
+ message: `Unsupported $parent reference in ${where}: references are only supported in filter and id positions`,
1786
+ path: []
1787
+ };
1788
+ }
1789
+ return null;
1790
+ }
1791
+ function collectFilterReferencePaths(filter2) {
1792
+ const paths = [];
1793
+ const seen = /* @__PURE__ */ new Set();
1794
+ const visitValue = (value) => {
1795
+ if (isParentRefMarker(value)) {
1796
+ if (typeof value.$parent === "string") {
1797
+ paths.push(value.$parent);
1798
+ }
1799
+ return;
1800
+ }
1801
+ if (isEscapedParentRef(value)) return;
1802
+ if (isArray6(value)) {
1803
+ if (seen.has(value)) return;
1804
+ seen.add(value);
1805
+ value.forEach(visitValue);
1806
+ return;
1807
+ }
1808
+ if (isPlainObject11(value)) {
1809
+ if (seen.has(value)) return;
1810
+ seen.add(value);
1811
+ for (const [key, child] of Object.entries(value)) {
1812
+ if (SUBQUERY_PAYLOAD_KEYS.has(key) || MARKER_FORBIDDEN_OPERATOR_KEYS.has(key)) continue;
1813
+ visitValue(child);
1814
+ }
1815
+ }
1816
+ };
1817
+ visitValue(filter2);
1818
+ return paths;
1819
+ }
1820
+ function collectCorrelatedReferencePaths(include) {
1821
+ const paths = [];
1822
+ if (isParentRefMarker(include.id) && typeof include.id !== "string") {
1823
+ const ref = include.id;
1824
+ if (typeof ref.$parent === "string") paths.push(ref.$parent);
1825
+ } else if (typeof include.id === "string") {
1826
+ }
1827
+ const filter2 = include.filter;
1828
+ if (filter2 !== void 0) {
1829
+ paths.push(...collectFilterReferencePaths(filter2));
1830
+ }
1831
+ return [...new Set(paths)];
1832
+ }
1833
+ var CorrelatedReferenceError = class extends Error {
1834
+ constructor(message) {
1835
+ super(message);
1836
+ this.name = "CorrelatedReferenceError";
1837
+ }
1838
+ };
1839
+ var UNRESOLVABLE = /* @__PURE__ */ Symbol("correlated-include-unresolvable");
1840
+ function getOwnPathValue(parent, path) {
1841
+ const segments = path.split(".");
1842
+ let current = parent;
1843
+ for (const segment of segments) {
1844
+ if (DANGEROUS_PATH_SEGMENTS.has(segment)) {
1845
+ throw new CorrelatedReferenceError(`Invalid $parent reference path: ${path}`);
1846
+ }
1847
+ if (typeof current !== "object" || current === null) return void 0;
1848
+ if (!Object.prototype.hasOwnProperty.call(current, segment)) return void 0;
1849
+ current = current[segment];
1850
+ }
1851
+ return current;
1852
+ }
1853
+ function cloneSubstitutedValue(value) {
1854
+ if (isPlainObject11(value) || isArray6(value)) return cloneDeep(value);
1855
+ return value;
1856
+ }
1857
+ function literalParentRefObject(path) {
1858
+ const literal = {};
1859
+ Object.defineProperty(literal, PARENT_MARKER_KEY, {
1860
+ value: path,
1861
+ writable: true,
1862
+ enumerable: true,
1863
+ configurable: true
1864
+ });
1865
+ return literal;
1866
+ }
1867
+ function resolveObjectEntries(obj, parent) {
1868
+ const out = {};
1869
+ for (const [key, child] of Object.entries(obj)) {
1870
+ if (key === PARENT_MARKER_KEY) {
1871
+ throw new CorrelatedReferenceError("$parent is never a field name in correlated templates");
1872
+ }
1873
+ if (SUBQUERY_PAYLOAD_KEYS.has(key)) {
1874
+ if (containsParentMarkerShape(child)) {
1875
+ throw new CorrelatedReferenceError(`Unsupported $parent reference inside ${key}`);
1876
+ }
1877
+ out[key] = cloneSubstitutedValue(child);
1878
+ continue;
1879
+ }
1880
+ if (MARKER_FORBIDDEN_OPERATOR_KEYS.has(key)) {
1881
+ if (containsParentMarkerShape(child)) {
1882
+ throw new CorrelatedReferenceError(`Unsupported $parent reference inside ${key}`);
1883
+ }
1884
+ out[key] = cloneSubstitutedValue(child);
1885
+ continue;
1886
+ }
1887
+ if (key === "$elemMatch") {
1888
+ if (isParentRefMarker(child) || isEscapedParentRef(child)) {
1889
+ throw new CorrelatedReferenceError("Unsupported $parent reference as a direct $elemMatch value");
1890
+ }
1891
+ out[key] = resolveValueNode(child, parent, false);
1892
+ continue;
1893
+ }
1894
+ if (LOGICAL_CLAUSE_KEYS.has(key)) {
1895
+ if (!isArray6(child)) {
1896
+ out[key] = resolveValueNode(child, parent, false);
1897
+ continue;
1898
+ }
1899
+ out[key] = child.map((clause) => {
1900
+ if (isParentRefMarker(clause) || isEscapedParentRef(clause)) {
1901
+ throw new CorrelatedReferenceError(`Unsupported $parent reference as a direct ${key} clause`);
1902
+ }
1903
+ if (!isPlainObject11(clause)) return cloneSubstitutedValue(clause);
1904
+ return resolveObjectEntries(clause, parent);
1905
+ });
1906
+ continue;
1907
+ }
1908
+ if (key.startsWith("$")) {
1909
+ out[key] = resolveValueNode(child, parent, false);
1910
+ continue;
1911
+ }
1912
+ out[key] = resolveValueNode(child, parent, true);
1913
+ }
1914
+ return out;
1915
+ }
1916
+ function resolveValueNode(template, parent, bare) {
1917
+ if (isEscapedParentRef(template)) {
1918
+ const inner = template.$escape;
1919
+ if (!isParentRefMarker(inner) || typeof inner.$parent !== "string" || (inner.$parent?.length ?? 0) === 0) {
1920
+ throw new CorrelatedReferenceError('Invalid $escape payload: expected { "$escape": { "$parent": "<field>" } }');
1921
+ }
1922
+ const literal = literalParentRefObject(inner.$parent);
1923
+ return bare ? { $eq: literal } : literal;
1924
+ }
1925
+ if (isParentRefMarker(template)) {
1926
+ const path = template.$parent;
1927
+ if (!isValidParentRefPath(path)) {
1928
+ throw new CorrelatedReferenceError(`Invalid $parent reference path: ${String(path)}`);
1929
+ }
1930
+ const value = getOwnPathValue(parent, path);
1931
+ if (value === void 0 || value === null) throw UNRESOLVABLE;
1932
+ if (bare && isPlainObject11(value)) return { $eq: cloneDeep(value) };
1933
+ return cloneSubstitutedValue(value);
1934
+ }
1935
+ if (isPlainObject11(template)) {
1936
+ if (hasMarkerKey(template)) {
1937
+ throw new CorrelatedReferenceError(
1938
+ 'Malformed $parent reference: objects carrying $parent must be exactly { "$parent": "<field>" }'
1939
+ );
1940
+ }
1941
+ return resolveObjectEntries(template, parent);
1942
+ }
1943
+ if (isArray6(template)) {
1944
+ return template.map((item) => resolveValueNode(item, parent, false));
1945
+ }
1946
+ return template;
1947
+ }
1948
+ function isUnresolvableSignal(error2) {
1949
+ return error2 === UNRESOLVABLE;
1950
+ }
1951
+ function resolveCorrelatedIdTemplate(id, parent) {
1952
+ try {
1953
+ if (typeof id === "string") {
1954
+ if (id.length === 0) throw new CorrelatedReferenceError("Correlated include id must be a non-empty string");
1955
+ return { status: "resolved", id };
1956
+ }
1957
+ if (isParentRefMarker(id)) {
1958
+ const path = id.$parent;
1959
+ if (!isValidParentRefPath(path)) {
1960
+ throw new CorrelatedReferenceError(`Invalid $parent reference path: ${String(path)}`);
1961
+ }
1962
+ const value = getOwnPathValue(parent, path);
1963
+ if (typeof value === "string") return { status: "resolved", id: value };
1964
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
1965
+ return { status: "resolved", id: String(value) };
1966
+ }
1967
+ throw UNRESOLVABLE;
1968
+ }
1969
+ throw new CorrelatedReferenceError("Correlated include id must be a string or $parent reference");
1970
+ } catch (error2) {
1971
+ if (isUnresolvableSignal(error2)) return { status: "unresolvable" };
1972
+ throw error2;
1973
+ }
1974
+ }
1975
+ function resolveCorrelatedFilterTemplate(filter2, parent) {
1976
+ try {
1977
+ if (!isPlainObject11(filter2) || isParentRefMarker(filter2) || isEscapedParentRef(filter2)) {
1978
+ throw new CorrelatedReferenceError("Correlated include filter must be an object");
1979
+ }
1980
+ return { status: "resolved", filter: resolveObjectEntries(filter2, parent) };
1981
+ } catch (error2) {
1982
+ if (isUnresolvableSignal(error2)) return { status: "unresolvable" };
1983
+ throw error2;
1984
+ }
1985
+ }
1986
+ function validateExpandedCorrelatedOperands(expanded, limits) {
1987
+ return validateRequestComplexity(expanded, limits ?? null, "filter");
1988
+ }
1989
+ var MAX_NESTED_INCLUDE_DEPTH = 10;
1990
+ function validateCorrelatedArgsShape(args, op, depth) {
1991
+ if (args === void 0) return [];
1992
+ if (!isPlainObject11(args)) return ["Correlated include args must be an object"];
1993
+ const record = args;
1994
+ const allowed = op === "list" ? /* @__PURE__ */ new Set(["select", "sort", "skip", "limit", "page", "pageSize", "include"]) : /* @__PURE__ */ new Set(["select", "sort", "include"]);
1995
+ for (const key of Object.keys(record)) {
1996
+ if (!allowed.has(key)) return [`Unsupported correlated include arg: ${key}`];
1997
+ }
1998
+ if (op === "count" && Object.keys(record).length > 0) return ["Count includes accept no args"];
1999
+ const selectIssue = findArgsMarkerIssue(record.select, "args.select");
2000
+ if (selectIssue) return [selectIssue.message];
2001
+ const sortIssue = findArgsMarkerIssue(record.sort, "args.sort");
2002
+ if (sortIssue) return [sortIssue.message];
2003
+ if (record.include !== void 0) {
2004
+ if (depth >= MAX_NESTED_INCLUDE_DEPTH) return ["Correlated include nesting exceeds the supported depth"];
2005
+ const nested = Array.isArray(record.include) ? record.include : [record.include];
2006
+ for (const entry of nested) {
2007
+ const errors = validateCorrelatedIncludeShape(entry, depth + 1);
2008
+ if (errors.length > 0) return errors;
2009
+ }
2010
+ }
2011
+ return [];
2012
+ }
2013
+ function validateCorrelatedIncludeShape(entry, depth = 0) {
2014
+ if (!isPlainObject11(entry)) return ["Correlated include must be an object"];
2015
+ const record = entry;
2016
+ if (record.mode !== "correlated") return ['Correlated include mode must be "correlated"'];
2017
+ if (typeof record.model !== "string" || record.model.length === 0)
2018
+ return ["Correlated include model must be a non-empty string"];
2019
+ if (record.op !== "list" && record.op !== "read" && record.op !== "count") {
2020
+ return ['Correlated include op must be one of "list", "read", or "count"'];
2021
+ }
2022
+ if (!isValidCorrelatedOutputPath(record.path))
2023
+ return [`Invalid correlated include output path: ${String(record.path)}`];
2024
+ if ("localField" in record || "foreignField" in record) {
2025
+ return ["Correlated includes must not carry localField or foreignField"];
2026
+ }
2027
+ if (record.options !== void 0 && !(isPlainObject11(record.options) && Object.keys(record.options).length === 0)) {
2028
+ return ["Correlated include options must be absent or empty"];
2029
+ }
2030
+ const hasId = record.id !== void 0;
2031
+ const hasFilter = record.filter !== void 0;
2032
+ if (record.op === "read") {
2033
+ if (hasId === hasFilter) return ['Correlated read includes require exactly one of "id" or "filter"'];
2034
+ if (hasId) {
2035
+ const issues = validateCorrelatedIdTemplate(record.id);
2036
+ if (issues.length > 0) return [issues[0].message];
2037
+ } else {
2038
+ const issues = validateCorrelatedFilterTemplate(record.filter);
2039
+ if (issues.length > 0) return [issues[0].message];
2040
+ }
2041
+ } else {
2042
+ if (!hasFilter || hasId) return [`Correlated ${record.op} includes require a filter and no id`];
2043
+ const issues = validateCorrelatedFilterTemplate(record.filter);
2044
+ if (issues.length > 0) return [issues[0].message];
2045
+ }
2046
+ return validateCorrelatedArgsShape(record.args, record.op, depth);
2047
+ }
2048
+ function isCorrelatedInclude(value) {
2049
+ return isPlainObject11(value) && value.mode === "correlated";
2050
+ }
2051
+
2052
+ // src/helpers/request-work.ts
2053
+ var states = /* @__PURE__ */ new WeakMap();
2054
+ function getRequestWorkState(req, runtime) {
2055
+ let owners = states.get(req);
2056
+ if (!owners) {
2057
+ owners = /* @__PURE__ */ new WeakMap();
2058
+ states.set(req, owners);
2059
+ }
2060
+ let state = owners.get(runtime);
2061
+ if (!state) {
2062
+ state = {
2063
+ totalQueries: 0,
2064
+ scheduler: new RequestConcurrencyScheduler(
2065
+ resolveRequestComplexity(runtime.getGlobalOption("requestComplexity")).maxBulkConcurrency
2066
+ )
2067
+ };
2068
+ owners.set(runtime, state);
2069
+ }
2070
+ return state;
2071
+ }
2072
+
2073
+ // src/services/base.ts
2074
+ function getCorrelatedTemplateDepth(includes) {
2075
+ let max = 0;
2076
+ const visit = (entries, depth) => {
2077
+ const list = castArray(entries).filter(Boolean);
2078
+ for (const entry of list) {
2079
+ if (!isPlainObject12(entry)) continue;
2080
+ max = Math.max(max, depth);
2081
+ const nested = entry.args?.include;
2082
+ if (nested !== void 0) {
2083
+ visit(nested, depth + 1);
2084
+ }
2085
+ }
2086
+ };
2087
+ if (includes.length > 0) visit(includes, 1);
2088
+ return max;
2089
+ }
1561
2090
  var ClientRequestError = class extends Error {
1562
2091
  constructor(result) {
1563
2092
  super(String(result.code));
@@ -1572,11 +2101,11 @@ function validateClientFilter(filter2) {
1572
2101
  }
1573
2102
  const blockedOperators = /* @__PURE__ */ new Set(["$where", "$expr", "$function", "$accumulator"]);
1574
2103
  const visit = (value, path) => {
1575
- if (isArray6(value)) {
2104
+ if (isArray7(value)) {
1576
2105
  value.forEach((item, index) => visit(item, `${path}[${index}]`));
1577
2106
  return;
1578
2107
  }
1579
- if (!isPlainObject11(value)) return;
2108
+ if (!isPlainObject12(value)) return;
1580
2109
  Object.entries(value).forEach(([key, child]) => {
1581
2110
  const nextPath = path ? `${path}.${key}` : key;
1582
2111
  if (blockedOperators.has(key)) {
@@ -1591,6 +2120,7 @@ function validateClientFilter(filter2) {
1591
2120
  }
1592
2121
  var Base = class {
1593
2122
  constructor(req, modelName) {
2123
+ this.requestRuntime = getActiveRuntime() ?? defaultRuntime;
1594
2124
  this.req = req;
1595
2125
  this.modelName = modelName;
1596
2126
  }
@@ -1696,23 +2226,88 @@ var Base = class {
1696
2226
  return this.req.macl.getPublicService(modelName);
1697
2227
  }
1698
2228
  processInclude(include) {
1699
- const includes = compact(castArray(include)).filter(({ model, op, path, localField, foreignField }) => {
1700
- return model && op && path && localField && foreignField;
1701
- });
2229
+ const items = compact(castArray(include));
2230
+ const legacy = [];
2231
+ const correlated = [];
2232
+ const correlatedReferenceFields = [];
2233
+ for (const entry of items) {
2234
+ if (isCorrelatedInclude(entry)) {
2235
+ const shapeErrors = validateCorrelatedIncludeShape(entry);
2236
+ if (shapeErrors.length > 0) {
2237
+ this.throwClientRequestError("bad_request" /* BadRequest */, shapeErrors[0]);
2238
+ }
2239
+ correlated.push(entry);
2240
+ correlatedReferenceFields.push(...collectCorrelatedReferencePaths(entry));
2241
+ continue;
2242
+ }
2243
+ if (containsParentMarkerShape(entry)) {
2244
+ this.throwClientRequestError(
2245
+ "bad_request" /* BadRequest */,
2246
+ 'Legacy includes must not contain $parent markers; use a correlated include (mode: "correlated")'
2247
+ );
2248
+ }
2249
+ const candidate = entry;
2250
+ if (candidate.model && candidate.op && candidate.path && candidate.localField && candidate.foreignField) {
2251
+ legacy.push(entry);
2252
+ }
2253
+ }
2254
+ const seenCorrelatedPaths = /* @__PURE__ */ new Set();
2255
+ for (const entry of correlated) {
2256
+ if (seenCorrelatedPaths.has(entry.path)) {
2257
+ this.throwClientRequestError("bad_request" /* BadRequest */, `Duplicate correlated include output path: ${entry.path}`);
2258
+ }
2259
+ seenCorrelatedPaths.add(entry.path);
2260
+ }
2261
+ this.assertIncludeOutputPaths([...legacy, ...correlated]);
1702
2262
  let includeLocalFields = [];
1703
2263
  let includePaths = [];
1704
- forEach3(includes, (inc) => {
2264
+ forEach3(legacy, (inc) => {
1705
2265
  includeLocalFields.push(inc.localField);
1706
2266
  includePaths.push(inc.path);
1707
2267
  });
1708
2268
  includeLocalFields = uniq(compact(includeLocalFields));
1709
2269
  includePaths = uniq(compact(includePaths));
1710
2270
  return {
1711
- includes,
2271
+ includes: legacy,
2272
+ correlatedIncludes: correlated,
1712
2273
  includeLocalFields,
1713
- includePaths
2274
+ includePaths,
2275
+ correlatedReferenceFields: uniq(compact(correlatedReferenceFields))
1714
2276
  };
1715
2277
  }
2278
+ validateIncludeOutputPath(path, modelName) {
2279
+ const permissionField = getModelOption(modelName, "documentPermissionField");
2280
+ if (get4(set3({}, path, true), permissionField) !== void 0 || get4(set3({}, permissionField, true), path) !== void 0) {
2281
+ this.throwClientRequestError(
2282
+ "bad_request" /* BadRequest */,
2283
+ `Include output path ${path} overlaps document permission field ${permissionField} on model ${modelName}`
2284
+ );
2285
+ }
2286
+ }
2287
+ assertIncludeOutputPaths(include) {
2288
+ const pending = [{ entries: include, modelName: this.modelName }];
2289
+ const visited = /* @__PURE__ */ new WeakMap();
2290
+ while (pending.length > 0) {
2291
+ const { entries, modelName } = pending.pop();
2292
+ if (modelName !== this.modelName && !getModelNames().includes(modelName)) {
2293
+ this.throwClientRequestError("bad_request" /* BadRequest */, `Model ${modelName} not found`);
2294
+ }
2295
+ for (const entry of entries) {
2296
+ if (!isPlainObject12(entry)) continue;
2297
+ const models = visited.get(entry) ?? /* @__PURE__ */ new Set();
2298
+ if (models.has(modelName)) continue;
2299
+ models.add(modelName);
2300
+ visited.set(entry, models);
2301
+ if (typeof entry.path === "string" && entry.path) {
2302
+ this.validateIncludeOutputPath(entry.path, modelName);
2303
+ }
2304
+ if ((entry.op === "read" || entry.op === "list") && typeof entry.model === "string") {
2305
+ const nested = entry.args?.include;
2306
+ if (nested) pending.push({ entries: compact(castArray(nested)), modelName: entry.model });
2307
+ }
2308
+ }
2309
+ }
2310
+ }
1716
2311
  getForeignKeyValues(value) {
1717
2312
  return flatten(castArray(value));
1718
2313
  }
@@ -1753,7 +2348,7 @@ var Base = class {
1753
2348
  return ids.size;
1754
2349
  }
1755
2350
  sanitizeIncludeArgs(args) {
1756
- if (!isPlainObject11(args)) return {};
2351
+ if (!isPlainObject12(args)) return {};
1757
2352
  const { overrides: _clientOverrides, ...trustedArgs } = args;
1758
2353
  return trustedArgs;
1759
2354
  }
@@ -1766,10 +2361,16 @@ var Base = class {
1766
2361
  if (!include) return docs;
1767
2362
  const includes = compact(castArray(include));
1768
2363
  if (includes.length === 0) return docs;
1769
- const isSingle = !isArray6(docs);
2364
+ const isSingle = !isArray7(docs);
1770
2365
  let docList = isSingle ? [docs] : docs;
1771
2366
  for (let x = 0; x < includes.length; x++) {
1772
2367
  const include2 = includes[x];
2368
+ if (isCorrelatedInclude(include2)) {
2369
+ this.throwClientRequestError(
2370
+ "bad_request" /* BadRequest */,
2371
+ "Correlated includes are not supported by the legacy include executor"
2372
+ );
2373
+ }
1773
2374
  this.assertIncludeForeignField(include2);
1774
2375
  switch (include2.op) {
1775
2376
  case "count":
@@ -1819,34 +2420,376 @@ var Base = class {
1819
2420
  overrides: {
1820
2421
  filter: authorizedFilter
1821
2422
  }
1822
- };
1823
- const trustedOptions = {
1824
- ...options,
1825
- lean: true,
1826
- includePermissions: false,
1827
- includeCount: false
1828
- };
1829
- const trustedResult = await svc.find(filter2, trustedArgs, trustedOptions);
1830
- if (!trustedResult.success) return docs;
1831
- const index = this.buildForeignKeyIndex(trustedResult.data, foreignField);
1832
- for (let y = 0; y < docs.length; y++) {
1833
- const doc = docs[y];
1834
- const matches = this.getIndexedMatches(index, get4(doc, localField));
1835
- setDocValue(doc, path, op === "list" ? matches : matches[0]);
1836
- }
1837
- return docs;
1838
- }
1839
- async includeDocsCount(docs, include) {
1840
- const { model, path, localField, foreignField, filter: _filters } = include;
1841
- const svc = await this.getAuthorizedTargetService(model, "count");
1842
- const localValues = docs.flatMap((doc) => this.getForeignKeyValues(get4(doc, localField)));
1843
- const result = await svc.countByFieldValues(foreignField, localValues, _filters ?? {}, "count");
1844
- if (!result.success) return docs;
1845
- for (let y = 0; y < docs.length; y++) {
1846
- const doc = docs[y];
1847
- setDocValue(doc, path, this.getIndexedCount(result.data, get4(doc, localField)));
1848
- }
1849
- return docs;
2423
+ };
2424
+ const trustedOptions = {
2425
+ ...options,
2426
+ lean: true,
2427
+ includePermissions: false,
2428
+ includeCount: false
2429
+ };
2430
+ const trustedResult = await svc.find(filter2, trustedArgs, trustedOptions);
2431
+ if (!trustedResult.success) return docs;
2432
+ const index = this.buildForeignKeyIndex(trustedResult.data, foreignField);
2433
+ for (let y = 0; y < docs.length; y++) {
2434
+ const doc = docs[y];
2435
+ const matches = this.getIndexedMatches(index, get4(doc, localField));
2436
+ setDocValue(doc, path, op === "list" ? matches : matches[0]);
2437
+ }
2438
+ return docs;
2439
+ }
2440
+ async includeDocsCount(docs, include) {
2441
+ const { model, path, localField, foreignField, filter: _filters } = include;
2442
+ const svc = await this.getAuthorizedTargetService(model, "count");
2443
+ const localValues = docs.flatMap((doc) => this.getForeignKeyValues(get4(doc, localField)));
2444
+ const result = await svc.countByFieldValues(foreignField, localValues, _filters ?? {}, "count");
2445
+ if (!result.success && result.code === "bad_request" /* BadRequest */) throw new ClientRequestError(result);
2446
+ if (!result.success) return docs;
2447
+ for (let y = 0; y < docs.length; y++) {
2448
+ const doc = docs[y];
2449
+ setDocValue(doc, path, this.getIndexedCount(result.data, get4(doc, localField)));
2450
+ }
2451
+ return docs;
2452
+ }
2453
+ getCorrelatedExecState() {
2454
+ return getRequestWorkState(this.req, this.requestRuntime);
2455
+ }
2456
+ claimCorrelatedQuerySlot() {
2457
+ const limits = this.getRequestComplexity();
2458
+ const state = this.getCorrelatedExecState();
2459
+ if (state.totalQueries >= limits.maxCorrelatedQueries) {
2460
+ this.throwClientRequestError(
2461
+ "bad_request" /* BadRequest */,
2462
+ `Correlated include query budget exceeded (max ${limits.maxCorrelatedQueries})`
2463
+ );
2464
+ }
2465
+ state.totalQueries += 1;
2466
+ }
2467
+ sanitizeCorrelatedArgs(include) {
2468
+ const args = include.args ?? {};
2469
+ if (include.op === "list") {
2470
+ const { select: select2, sort: sort2, skip, limit, page, pageSize, include: nestedInclude2 } = args;
2471
+ return { select: select2, sort: sort2, skip, limit, page, pageSize, nestedInclude: nestedInclude2 };
2472
+ }
2473
+ const { select, sort, include: nestedInclude } = args;
2474
+ return { select, sort, nestedInclude };
2475
+ }
2476
+ async includeCorrelatedDocs(docs, correlatedIncludes, snapshots, depth = 0) {
2477
+ if (correlatedIncludes.length === 0) return docs;
2478
+ const limits = this.getRequestComplexity();
2479
+ const templateDepth = getCorrelatedTemplateDepth(correlatedIncludes);
2480
+ if (depth + templateDepth > limits.maxCorrelatedDepth) {
2481
+ this.throwClientRequestError(
2482
+ "bad_request" /* BadRequest */,
2483
+ `Correlated include depth exceeded (max ${limits.maxCorrelatedDepth})`
2484
+ );
2485
+ }
2486
+ const state = this.getCorrelatedExecState();
2487
+ const resolvedSnapshots = snapshots ?? docs.map((doc) => cloneDeep2(toObject(doc)));
2488
+ for (const include of correlatedIncludes) {
2489
+ switch (include.op) {
2490
+ case "read":
2491
+ await this.includeCorrelatedRead(docs, include, resolvedSnapshots, state, depth);
2492
+ break;
2493
+ case "list":
2494
+ await this.includeCorrelatedList(docs, include, resolvedSnapshots, state, depth);
2495
+ break;
2496
+ case "count":
2497
+ await this.includeCorrelatedCount(docs, include, resolvedSnapshots, state, depth);
2498
+ break;
2499
+ default:
2500
+ this.throwClientRequestError(
2501
+ "bad_request" /* BadRequest */,
2502
+ `Unsupported correlated include op: ${include.op}`
2503
+ );
2504
+ }
2505
+ }
2506
+ return docs;
2507
+ }
2508
+ async includeCorrelatedRead(docs, include, snapshots, state, depth) {
2509
+ const svc = await this.getAuthorizedTargetService(include.model, "read");
2510
+ const { select, sort, nestedInclude } = this.sanitizeCorrelatedArgs(include);
2511
+ const hasId = include.id !== void 0;
2512
+ if (hasId) {
2513
+ const idTemplate = include.id;
2514
+ const tasks2 = docs.map((_, index) => ({ index, snapshot: snapshots[index] }));
2515
+ await state.scheduler.map(tasks2, async ({ index, snapshot }) => {
2516
+ let resolution;
2517
+ try {
2518
+ resolution = resolveCorrelatedIdTemplate(idTemplate, snapshot);
2519
+ } catch (error2) {
2520
+ if (error2 instanceof CorrelatedReferenceError) {
2521
+ throw new ClientRequestError({
2522
+ success: false,
2523
+ kind: "error",
2524
+ code: "bad_request" /* BadRequest */,
2525
+ errors: [{ detail: error2.message }]
2526
+ });
2527
+ }
2528
+ throw error2;
2529
+ }
2530
+ if (resolution.status === "unresolvable") {
2531
+ setDocValue(docs[index], include.path, null);
2532
+ return;
2533
+ }
2534
+ const resolvedId = resolution.id;
2535
+ let idFilter;
2536
+ try {
2537
+ idFilter = await svc.genIDFilter(resolvedId);
2538
+ } catch (error2) {
2539
+ const clientResult = this.getClientRequestErrorResult(error2);
2540
+ if (clientResult) throw new ClientRequestError(clientResult);
2541
+ throw error2;
2542
+ }
2543
+ const authorized = await svc.genFilter("read", idFilter);
2544
+ if (authorized === false) {
2545
+ throw new ClientRequestError({ success: false, kind: "error", code: "forbidden" /* Forbidden */ });
2546
+ }
2547
+ this.claimCorrelatedQuerySlot();
2548
+ let result;
2549
+ try {
2550
+ result = await svc.findOne(
2551
+ {},
2552
+ {
2553
+ select,
2554
+ sort,
2555
+ include: nestedInclude,
2556
+ overrides: { filter: authorized }
2557
+ },
2558
+ { access: "read", lean: true, includePermissions: false }
2559
+ );
2560
+ } catch (error2) {
2561
+ const clientResult = this.getClientRequestErrorResult(error2);
2562
+ if (clientResult) throw new ClientRequestError(clientResult);
2563
+ throw error2;
2564
+ }
2565
+ if (result.success) {
2566
+ setDocValue(docs[index], include.path, result.data);
2567
+ } else if (result.code === "not_found" /* NotFound */) {
2568
+ setDocValue(docs[index], include.path, null);
2569
+ } else {
2570
+ throw new ClientRequestError(result);
2571
+ }
2572
+ });
2573
+ return;
2574
+ }
2575
+ const filterTemplate = include.filter;
2576
+ const templateErrors = this.validateClientFilter(filterTemplate);
2577
+ if (templateErrors.length > 0) {
2578
+ this.throwClientRequestError("bad_request" /* BadRequest */, templateErrors[0]);
2579
+ }
2580
+ let parsedTemplate;
2581
+ try {
2582
+ parsedTemplate = await this.parseClientData(
2583
+ filterTemplate,
2584
+ state.scheduler
2585
+ );
2586
+ } catch (error2) {
2587
+ const clientResult = this.getClientRequestErrorResult(error2);
2588
+ if (clientResult) throw new ClientRequestError(clientResult);
2589
+ throw error2;
2590
+ }
2591
+ const tasks = docs.map((_, index) => ({ index, snapshot: snapshots[index] }));
2592
+ await state.scheduler.map(tasks, async ({ index, snapshot }) => {
2593
+ let resolution;
2594
+ try {
2595
+ resolution = resolveCorrelatedFilterTemplate(parsedTemplate, snapshot);
2596
+ } catch (error2) {
2597
+ if (error2 instanceof CorrelatedReferenceError) {
2598
+ throw new ClientRequestError({
2599
+ success: false,
2600
+ kind: "error",
2601
+ code: "bad_request" /* BadRequest */,
2602
+ errors: [{ detail: error2.message }]
2603
+ });
2604
+ }
2605
+ throw error2;
2606
+ }
2607
+ if (resolution.status === "unresolvable") {
2608
+ setDocValue(docs[index], include.path, null);
2609
+ return;
2610
+ }
2611
+ const expanded = resolution.filter;
2612
+ const operandErrors = validateExpandedCorrelatedOperands(expanded, this.getRequestComplexity());
2613
+ if (operandErrors.length > 0) {
2614
+ throw new ClientRequestError({
2615
+ success: false,
2616
+ kind: "error",
2617
+ code: "bad_request" /* BadRequest */,
2618
+ errors: [{ detail: operandErrors[0].detail }]
2619
+ });
2620
+ }
2621
+ const authorized = await svc.genFilter("read", expanded);
2622
+ if (authorized === false) {
2623
+ throw new ClientRequestError({ success: false, kind: "error", code: "forbidden" /* Forbidden */ });
2624
+ }
2625
+ this.claimCorrelatedQuerySlot();
2626
+ let result;
2627
+ try {
2628
+ result = await svc.findOne(
2629
+ {},
2630
+ {
2631
+ select,
2632
+ sort,
2633
+ include: nestedInclude,
2634
+ overrides: { filter: authorized }
2635
+ },
2636
+ { access: "read", lean: true, includePermissions: false }
2637
+ );
2638
+ } catch (error2) {
2639
+ const clientResult = this.getClientRequestErrorResult(error2);
2640
+ if (clientResult) throw new ClientRequestError(clientResult);
2641
+ throw error2;
2642
+ }
2643
+ if (result.success) {
2644
+ setDocValue(docs[index], include.path, result.data);
2645
+ } else if (result.code === "not_found" /* NotFound */) {
2646
+ setDocValue(docs[index], include.path, null);
2647
+ } else {
2648
+ throw new ClientRequestError(result);
2649
+ }
2650
+ });
2651
+ void depth;
2652
+ }
2653
+ async includeCorrelatedList(docs, include, snapshots, state, depth) {
2654
+ const svc = await this.getAuthorizedTargetService(include.model, "list");
2655
+ const { select, sort, skip, limit, page, pageSize, nestedInclude } = this.sanitizeCorrelatedArgs(include);
2656
+ const filterTemplate = include.filter;
2657
+ const templateErrors = this.validateClientFilter(filterTemplate);
2658
+ if (templateErrors.length > 0) {
2659
+ this.throwClientRequestError("bad_request" /* BadRequest */, templateErrors[0]);
2660
+ }
2661
+ let parsedTemplate;
2662
+ try {
2663
+ parsedTemplate = await this.parseClientData(
2664
+ filterTemplate,
2665
+ state.scheduler
2666
+ );
2667
+ } catch (error2) {
2668
+ const clientResult = this.getClientRequestErrorResult(error2);
2669
+ if (clientResult) throw new ClientRequestError(clientResult);
2670
+ throw error2;
2671
+ }
2672
+ const tasks = docs.map((_, index) => ({ index, snapshot: snapshots[index] }));
2673
+ await state.scheduler.map(tasks, async ({ index, snapshot }) => {
2674
+ let resolution;
2675
+ try {
2676
+ resolution = resolveCorrelatedFilterTemplate(parsedTemplate, snapshot);
2677
+ } catch (error2) {
2678
+ if (error2 instanceof CorrelatedReferenceError) {
2679
+ throw new ClientRequestError({
2680
+ success: false,
2681
+ kind: "error",
2682
+ code: "bad_request" /* BadRequest */,
2683
+ errors: [{ detail: error2.message }]
2684
+ });
2685
+ }
2686
+ throw error2;
2687
+ }
2688
+ if (resolution.status === "unresolvable") {
2689
+ setDocValue(docs[index], include.path, []);
2690
+ return;
2691
+ }
2692
+ const expanded = resolution.filter;
2693
+ const operandErrors = validateExpandedCorrelatedOperands(expanded, this.getRequestComplexity());
2694
+ if (operandErrors.length > 0) {
2695
+ throw new ClientRequestError({
2696
+ success: false,
2697
+ kind: "error",
2698
+ code: "bad_request" /* BadRequest */,
2699
+ errors: [{ detail: operandErrors[0].detail }]
2700
+ });
2701
+ }
2702
+ const authorized = await svc.genFilter("list", expanded);
2703
+ if (authorized === false) {
2704
+ throw new ClientRequestError({ success: false, kind: "error", code: "forbidden" /* Forbidden */ });
2705
+ }
2706
+ this.claimCorrelatedQuerySlot();
2707
+ let result;
2708
+ try {
2709
+ result = await svc.find(
2710
+ {},
2711
+ {
2712
+ select,
2713
+ sort,
2714
+ skip,
2715
+ limit,
2716
+ page,
2717
+ pageSize,
2718
+ include: nestedInclude,
2719
+ overrides: { filter: authorized }
2720
+ },
2721
+ { includeCount: false, lean: true, includePermissions: false }
2722
+ );
2723
+ } catch (error2) {
2724
+ const clientResult = this.getClientRequestErrorResult(error2);
2725
+ if (clientResult) throw new ClientRequestError(clientResult);
2726
+ throw error2;
2727
+ }
2728
+ if (result.success) {
2729
+ setDocValue(docs[index], include.path, result.data);
2730
+ } else {
2731
+ throw new ClientRequestError(result);
2732
+ }
2733
+ });
2734
+ void depth;
2735
+ }
2736
+ async includeCorrelatedCount(docs, include, snapshots, state, depth) {
2737
+ const svc = await this.getAuthorizedTargetService(include.model, "count");
2738
+ const filterTemplate = include.filter;
2739
+ const templateErrors = this.validateClientFilter(filterTemplate);
2740
+ if (templateErrors.length > 0) {
2741
+ this.throwClientRequestError("bad_request" /* BadRequest */, templateErrors[0]);
2742
+ }
2743
+ const tasks = docs.map((_, index) => ({ index, snapshot: snapshots[index] }));
2744
+ await state.scheduler.map(tasks, async ({ index, snapshot }) => {
2745
+ let resolution;
2746
+ try {
2747
+ resolution = resolveCorrelatedFilterTemplate(filterTemplate, snapshot);
2748
+ } catch (error2) {
2749
+ if (error2 instanceof CorrelatedReferenceError) {
2750
+ throw new ClientRequestError({
2751
+ success: false,
2752
+ kind: "error",
2753
+ code: "bad_request" /* BadRequest */,
2754
+ errors: [{ detail: error2.message }]
2755
+ });
2756
+ }
2757
+ throw error2;
2758
+ }
2759
+ if (resolution.status === "unresolvable") {
2760
+ setDocValue(docs[index], include.path, 0);
2761
+ return;
2762
+ }
2763
+ const expanded = resolution.filter;
2764
+ const operandErrors = validateExpandedCorrelatedOperands(expanded, this.getRequestComplexity());
2765
+ if (operandErrors.length > 0) {
2766
+ throw new ClientRequestError({
2767
+ success: false,
2768
+ kind: "error",
2769
+ code: "bad_request" /* BadRequest */,
2770
+ errors: [{ detail: operandErrors[0].detail }]
2771
+ });
2772
+ }
2773
+ const authorized = await svc.genFilter("count", expanded);
2774
+ if (authorized === false) {
2775
+ throw new ClientRequestError({ success: false, kind: "error", code: "forbidden" /* Forbidden */ });
2776
+ }
2777
+ this.claimCorrelatedQuerySlot();
2778
+ let result;
2779
+ try {
2780
+ result = await svc.countTrusted(authorized);
2781
+ } catch (error2) {
2782
+ const clientResult = this.getClientRequestErrorResult(error2);
2783
+ if (clientResult) throw new ClientRequestError(clientResult);
2784
+ throw error2;
2785
+ }
2786
+ if (result.success) {
2787
+ setDocValue(docs[index], include.path, result.data);
2788
+ } else {
2789
+ throw new ClientRequestError(result);
2790
+ }
2791
+ });
2792
+ void depth;
1850
2793
  }
1851
2794
  async parseClientData(filter2, scheduler = new RequestConcurrencyScheduler(this.getRequestComplexity().maxBulkConcurrency), scheduled = false) {
1852
2795
  const result = await iterateQuery(
@@ -1890,7 +2833,7 @@ var Base = class {
1890
2833
  }
1891
2834
  let ret = result.data;
1892
2835
  if (sqOptions.path) {
1893
- ret = isArray6(ret) ? flatten(ret.map((v) => get4(v, sqOptions.path))) : get4(ret, sqOptions.path);
2836
+ ret = isArray7(ret) ? flatten(ret.map((v) => get4(v, sqOptions.path))) : get4(ret, sqOptions.path);
1894
2837
  }
1895
2838
  if (sqOptions.compact) {
1896
2839
  ret = compact(castArray(ret));
@@ -1907,8 +2850,9 @@ var Base = class {
1907
2850
  // src/services/service.ts
1908
2851
  import {
1909
2852
  castArray as castArray2,
2853
+ cloneDeep as cloneDeep4,
1910
2854
  compact as compact2,
1911
- isArray as isArray7,
2855
+ isArray as isArray8,
1912
2856
  isBoolean as isBoolean3,
1913
2857
  isFunction as isFunction3,
1914
2858
  omit,
@@ -1919,6 +2863,26 @@ import {
1919
2863
  import { diff } from "just-diff";
1920
2864
 
1921
2865
  // src/model.ts
2866
+ import { cloneDeep as cloneDeep3 } from "@web-ts-toolkit/utils";
2867
+ function admitModelPersistence(adapter, scheduler) {
2868
+ return {
2869
+ modelName: adapter.modelName,
2870
+ mongooseModel: adapter.mongooseModel,
2871
+ new: () => adapter.new(),
2872
+ // Mongoose create(array) otherwise starts all document saves at once. Keep
2873
+ // the adapter's array input/output contract, with one permit per item; all
2874
+ // items are submitted even if one rejects, as with unordered create(array).
2875
+ create: (data) => Array.isArray(data) ? Promise.all(data.map((item) => scheduler.work(async () => (await adapter.create([item]))[0]))) : scheduler.work(() => adapter.create(data)),
2876
+ find: (props) => scheduler.work(() => adapter.find(props)),
2877
+ findOne: (props) => scheduler.work(() => adapter.findOne(props)),
2878
+ exists: (filter2) => scheduler.work(() => adapter.exists(filter2)),
2879
+ countDocuments: (filter2) => scheduler.work(() => adapter.countDocuments(filter2)),
2880
+ distinct: (field, conditions) => scheduler.work(() => adapter.distinct(field, conditions)),
2881
+ aggregate: (pipeline) => scheduler.work(() => adapter.aggregate(pipeline)),
2882
+ // Casting builds no persistence work and must not acquire a recursive permit.
2883
+ ...adapter.castFilter ? { castFilter: (filter2) => adapter.castFilter(filter2) } : {}
2884
+ };
2885
+ }
1922
2886
  var Model = class {
1923
2887
  constructor(modelName, runtime) {
1924
2888
  this.modelName = modelName;
@@ -1971,6 +2935,11 @@ var Model = class {
1971
2935
  countDocuments(filter2 = {}) {
1972
2936
  return this.mongooseModel.countDocuments(filter2);
1973
2937
  }
2938
+ /** Cast a copy using this model's query schema/setters, without executing query middleware or persistence. */
2939
+ castFilter(filter2) {
2940
+ const query = this.mongooseModel.countDocuments(cloneDeep3(filter2));
2941
+ return query.cast(this.mongooseModel);
2942
+ }
1974
2943
  // see https://mongoosejs.com/docs/api.html#model_Model.distinct
1975
2944
  distinct(field, conditions = {}) {
1976
2945
  return this.mongooseModel.distinct(field, conditions);
@@ -1981,6 +2950,39 @@ var Model = class {
1981
2950
  };
1982
2951
  var model_default = Model;
1983
2952
 
2953
+ // src/helpers/apply-update.ts
2954
+ import { isPlainObject as isPlainObject13, set as set4 } from "@web-ts-toolkit/utils";
2955
+ function applyUpdate(target, data, allowedFields) {
2956
+ const wholeFields = /* @__PURE__ */ new Set();
2957
+ const partialContainers = /* @__PURE__ */ new Set();
2958
+ const collectBoundary = (shape, prefix = "") => {
2959
+ for (const [key, value] of Object.entries(shape)) {
2960
+ const path = prefix ? `${prefix}.${key}` : key;
2961
+ if (value === true) wholeFields.add(path);
2962
+ else {
2963
+ partialContainers.add(path);
2964
+ collectBoundary(value, path);
2965
+ }
2966
+ }
2967
+ };
2968
+ for (const field of allowedFields) {
2969
+ collectBoundary(set4({}, field, true));
2970
+ }
2971
+ const apply = (values, prefix = "") => {
2972
+ for (const [key, value] of Object.entries(values)) {
2973
+ const path = prefix ? `${prefix}.${key}` : key;
2974
+ if (partialContainers.has(path) && !wholeFields.has(path) && (isPlainObject13(value) || Array.isArray(value))) {
2975
+ apply(value, path);
2976
+ } else if (isDocument2(target)) {
2977
+ target.set(path, value);
2978
+ } else {
2979
+ set4(target, path, value);
2980
+ }
2981
+ }
2982
+ };
2983
+ apply(data ?? {});
2984
+ }
2985
+
1984
2986
  // src/logger-helpers.ts
1985
2987
  var SENSITIVE_FILTER_KEYS = /* @__PURE__ */ new Set([
1986
2988
  "password",
@@ -2160,6 +3162,30 @@ var error = (error2) => {
2160
3162
 
2161
3163
  // src/services/model-subdocument-service.ts
2162
3164
  import { forEach as forEach4, get as get5, pick as pick2 } from "@web-ts-toolkit/utils";
3165
+ async function visibleMutationRows(service, isAllowed, parentId, sub, rows, response) {
3166
+ const accesses = ["read", `subs.${sub}.read`];
3167
+ if (response === "create") accesses.push(`subs.${sub}.list`);
3168
+ const allowed = await Promise.all(accesses.map(isAllowed));
3169
+ if (allowed.some((value) => !value)) return [];
3170
+ const parentFilter = await service.genFilter("read", { _id: parentId });
3171
+ if (parentFilter === false) return [];
3172
+ const parent = await service.findRawParentDoc({
3173
+ filter: { $and: [{ _id: parentId }, parentFilter] },
3174
+ select: "_id",
3175
+ populate: [],
3176
+ lean: true
3177
+ });
3178
+ if (!parent) return [];
3179
+ const targetFilter = response === "create" ? {} : response === "single" ? { _id: rows[0]?._id } : { _id: { $in: rows.map((row) => row._id) } };
3180
+ const [readFilter, listFilter, select] = await Promise.all([
3181
+ service.genFilter(`subs.${sub}.read`, targetFilter),
3182
+ response === "create" ? service.genFilter(`subs.${sub}.list`) : Promise.resolve({}),
3183
+ service.genQuerySelect("read", null, false, [sub, "sub"])
3184
+ ]);
3185
+ if (readFilter === false || listFilter === false) return [];
3186
+ const visible = filterCollection(filterCollection(rows, listFilter), readFilter);
3187
+ return select ? visible.map((row) => pick2(toObject(row), select.concat("_id"))) : visible;
3188
+ }
2163
3189
  async function listSub(service, id, sub, options) {
2164
3190
  const { filter: ft, select } = options ?? {};
2165
3191
  const filterErrors = validateClientFilter(ft);
@@ -2191,25 +3217,24 @@ async function readSub(service, id, sub, subId, options) {
2191
3217
  if (subSelect) subdoc = pick2(toObject(subdoc), subSelect.concat(["_id"]));
2192
3218
  return { success: true, kind: "single", code: "success" /* Success */, data: subdoc };
2193
3219
  }
2194
- async function updateSub(service, id, sub, subId, data) {
3220
+ async function updateSub(service, persist, isAllowed, id, sub, subId, data) {
2195
3221
  const parentDoc = await getParentDoc(service, id, sub, null, { access: "update" });
2196
3222
  if (!parentDoc) return { success: false, kind: "error", code: "not_found" /* NotFound */ };
2197
3223
  const result = get5(parentDoc, sub);
2198
- const [subFilter, subReadSelect, subUpdateSelect] = await Promise.all([
3224
+ const [subFilter, subUpdateSelect] = await Promise.all([
2199
3225
  service.genFilter(`subs.${sub}.update`, { _id: subId }),
2200
- service.genQuerySelect("read", null, false, [sub, "sub"]),
2201
3226
  service.genQuerySelect("update", null, false, [sub, "sub"])
2202
3227
  ]);
2203
3228
  if (subFilter === false) return { success: false, kind: "error", code: "forbidden" /* Forbidden */ };
2204
- let subdoc = findElement(result, subFilter);
3229
+ const subdoc = findElement(result, subFilter);
2205
3230
  if (!subdoc) return { success: false, kind: "error", code: "not_found" /* NotFound */ };
2206
3231
  const allowedData = pick2(data, subUpdateSelect);
2207
- Object.assign(subdoc, allowedData);
2208
- await parentDoc.save();
2209
- if (subReadSelect) subdoc = pick2(toObject(subdoc), subReadSelect.concat(["_id"]));
2210
- return { success: true, kind: "single", code: "success" /* Success */, data: subdoc };
3232
+ applyUpdate(subdoc, allowedData, subUpdateSelect);
3233
+ await persist(() => parentDoc.save());
3234
+ const visible = await visibleMutationRows(service, isAllowed, parentDoc._id, sub, [subdoc], "single");
3235
+ return { success: true, kind: "single", code: "success" /* Success */, data: visible[0] ?? null };
2211
3236
  }
2212
- async function bulkUpdateSub(service, id, sub, data) {
3237
+ async function bulkUpdateSub(service, persist, isAllowed, id, sub, data) {
2213
3238
  const { maxBulkItems } = service.getRequestComplexity();
2214
3239
  if (data.length > maxBulkItems) {
2215
3240
  return {
@@ -2222,24 +3247,23 @@ async function bulkUpdateSub(service, id, sub, data) {
2222
3247
  const parentDoc = await getParentDoc(service, id, sub, null, { access: "update" });
2223
3248
  if (!parentDoc) return { success: false, kind: "error", code: "not_found" /* NotFound */ };
2224
3249
  let result = get5(parentDoc, sub);
2225
- const [subFilter, subReadSelect, subUpdateSelect] = await Promise.all([
3250
+ const [subFilter, subUpdateSelect] = await Promise.all([
2226
3251
  service.genFilter(`subs.${sub}.update`, { _id: { $in: data.map((v) => v._id) } }),
2227
- service.genQuerySelect("read", null, false, [sub, "sub"]),
2228
3252
  service.genQuerySelect("update", null, false, [sub, "sub"])
2229
3253
  ]);
2230
3254
  if (subFilter === false) return { success: false, kind: "error", code: "forbidden" /* Forbidden */ };
2231
- result = filterCollection(result, subFilter);
3255
+ result = filterCollection(result, subFilter).filter((subdoc) => findElementById(data, subdoc._id));
2232
3256
  forEach4(result, (subdoc) => {
2233
3257
  const tdata = findElementById(data, subdoc._id);
2234
3258
  if (!tdata) return;
2235
3259
  const allowedData = pick2(tdata, subUpdateSelect);
2236
- Object.assign(subdoc, allowedData);
3260
+ applyUpdate(subdoc, allowedData, subUpdateSelect);
2237
3261
  });
2238
- await parentDoc.save();
2239
- if (subReadSelect) result = result.map((v) => pick2(toObject(v), subReadSelect.concat(["_id"])));
3262
+ await persist(() => parentDoc.save());
3263
+ result = await visibleMutationRows(service, isAllowed, parentDoc._id, sub, result, "bulk");
2240
3264
  return { success: true, kind: "list", code: "success" /* Success */, data: result, count: result.length };
2241
3265
  }
2242
- async function createSub(service, id, sub, data, options) {
3266
+ async function createSub(service, persist, isAllowed, id, sub, data, options) {
2243
3267
  const { addFirst } = options ?? {};
2244
3268
  const { maxBulkItems } = service.getRequestComplexity();
2245
3269
  if (Array.isArray(data) && data.length > maxBulkItems) {
@@ -2253,21 +3277,18 @@ async function createSub(service, id, sub, data, options) {
2253
3277
  const parentDoc = await getParentDoc(service, id, sub, null, { access: "update" });
2254
3278
  if (!parentDoc) return { success: false, kind: "error", code: "not_found" /* NotFound */ };
2255
3279
  let result = get5(parentDoc, sub);
2256
- const [subCreateSelect, subReadSelect] = await Promise.all([
2257
- service.genQuerySelect("create", null, false, [sub, "sub"]),
2258
- service.genQuerySelect("read", null, false, [sub, "sub"])
2259
- ]);
3280
+ const subCreateSelect = await service.genQuerySelect("create", null, false, [sub, "sub"]);
2260
3281
  const allowedData = Array.isArray(data) ? data.map((row) => pick2(row, subCreateSelect)) : pick2(data, subCreateSelect);
2261
3282
  if (Array.isArray(allowedData)) {
2262
3283
  addFirst === true ? result.unshift(...allowedData) : result.push(...allowedData);
2263
3284
  } else {
2264
3285
  addFirst === true ? result.unshift(allowedData) : result.push(allowedData);
2265
3286
  }
2266
- await parentDoc.save();
2267
- if (subReadSelect) result = result.map((v) => pick2(toObject(v), subReadSelect.concat(["_id"])));
3287
+ await persist(() => parentDoc.save());
3288
+ result = await visibleMutationRows(service, isAllowed, parentDoc._id, sub, result, "create");
2268
3289
  return { success: true, kind: "list", code: "created" /* Created */, data: result, count: result.length };
2269
3290
  }
2270
- async function deleteSub(service, id, sub, subId) {
3291
+ async function deleteSub(service, persist, id, sub, subId) {
2271
3292
  const parentDoc = await getParentDoc(service, id, sub, null, { access: "update" });
2272
3293
  if (!parentDoc) return { success: false, kind: "error", code: "not_found" /* NotFound */ };
2273
3294
  const result = get5(parentDoc, sub);
@@ -2276,7 +3297,7 @@ async function deleteSub(service, id, sub, subId) {
2276
3297
  const subdoc = findElement(result, subFilter);
2277
3298
  if (!subdoc) return { success: false, kind: "error", code: "not_found" /* NotFound */ };
2278
3299
  await ("deleteOne" in subdoc ? subdoc.deleteOne?.() : subdoc.remove?.());
2279
- await parentDoc.save();
3300
+ await persist(() => parentDoc.save());
2280
3301
  return { success: true, kind: "single", code: "success" /* Success */, data: subdoc._id };
2281
3302
  }
2282
3303
  async function getParentDoc(service, id, sub, args, options) {
@@ -2411,7 +3432,39 @@ var assertModelDocument = (value, modelName, hookName) => {
2411
3432
  }
2412
3433
  throw new Error(`${hookName} hook for model=${modelName} must return a Mongoose document instance`);
2413
3434
  };
3435
+ var unsetDocPath = (doc, path) => {
3436
+ const segments = path.split(".");
3437
+ let current = doc;
3438
+ const unwrap = (value) => value != null && typeof value === "object" && "_doc" in value ? value._doc : value;
3439
+ current = unwrap(current);
3440
+ for (let i = 0; i < segments.length - 1; i++) {
3441
+ if (current == null || typeof current !== "object") return;
3442
+ if (!Object.prototype.hasOwnProperty.call(current, segments[i])) return;
3443
+ current = unwrap(current[segments[i]]);
3444
+ }
3445
+ if (current != null && typeof current === "object") {
3446
+ delete current[segments[segments.length - 1]];
3447
+ }
3448
+ };
3449
+ var shouldKeepCorrelatedRef = (ref, keep) => {
3450
+ if (keep.has(ref)) return true;
3451
+ const top = ref.split(".")[0];
3452
+ if (keep.has(top)) return true;
3453
+ for (const kept of keep) {
3454
+ if (kept === ref || kept.startsWith(`${ref}.`) || ref.startsWith(`${kept}.`)) return true;
3455
+ }
3456
+ return false;
3457
+ };
2414
3458
  var Service = class extends Base {
3459
+ constructor(req, modelName) {
3460
+ super(req, modelName);
3461
+ this.persist = (operation) => this.getCorrelatedExecState().scheduler.work(operation);
3462
+ this.model = admitModelPersistence(this.createModelAdapter(modelName), this.getCorrelatedExecState().scheduler);
3463
+ this.options = this.getModelRouterOptions(modelName);
3464
+ this.defaults = this.options.defaults || {};
3465
+ this.baseFields = ["_id"];
3466
+ this.baseFieldsExt = this.baseFields.concat(this.options.documentPermissionField);
3467
+ }
2415
3468
  findRawParentDoc(args) {
2416
3469
  return this.model.findOne({
2417
3470
  ...args,
@@ -2449,14 +3502,6 @@ var Service = class extends Base {
2449
3502
  ...extra ?? {}
2450
3503
  });
2451
3504
  }
2452
- constructor(req, modelName) {
2453
- super(req, modelName);
2454
- this.model = this.createModelAdapter(modelName);
2455
- this.options = this.getModelRouterOptions(modelName);
2456
- this.defaults = this.options.defaults || {};
2457
- this.baseFields = ["_id"];
2458
- this.baseFieldsExt = this.baseFields.concat(this.options.documentPermissionField);
2459
- }
2460
3505
  async findOne(filter2, args, options) {
2461
3506
  const filterErrors = this.validateClientFilter(filter2);
2462
3507
  if (filterErrors.length > 0) return { success: false, kind: "error", code: "bad_request" /* BadRequest */, errors: filterErrors };
@@ -2464,21 +3509,24 @@ var Service = class extends Base {
2464
3509
  const { skim, includePermissions, access, populateAccess, lean } = this.resolveFindOneOptions(options);
2465
3510
  const { filter: overrideFilter, select: overrideSelect, populate: overridePopulate } = overrides ?? {};
2466
3511
  let parsedFilter;
3512
+ let processedInclude;
2467
3513
  try {
2468
- parsedFilter = await this.parseClientData(filter2);
3514
+ processedInclude = this.processInclude(include);
3515
+ parsedFilter = overrideFilter === false ? false : await this.parseClientData(filter2);
2469
3516
  } catch (error2) {
2470
3517
  const result = this.getClientRequestErrorResult(error2);
2471
3518
  if (result) return result;
2472
3519
  throw error2;
2473
3520
  }
2474
3521
  let [_filter, _select, _populate, allowedSortFields] = await Promise.all([
2475
- overrideFilter || this.genFilter(access, parsedFilter),
3522
+ overrideFilter ?? this.genFilter(access, parsedFilter),
2476
3523
  overrideSelect || this.genQuerySelect(access, select),
2477
3524
  overridePopulate || this.genPopulate(populateAccess || access, populate),
2478
3525
  this.genAllowedFields({}, access, this.baseFieldsExt)
2479
3526
  ]);
2480
- const { includes, includeLocalFields, includePaths } = this.processInclude(include);
2481
- const finalSelect = normalizeSelect(_select).concat(includeLocalFields);
3527
+ const { includes, correlatedIncludes, includeLocalFields, includePaths, correlatedReferenceFields } = processedInclude;
3528
+ const correlatedOutputPaths = correlatedIncludes.map((entry) => entry.path);
3529
+ const finalSelect = normalizeSelect(_select).concat(includeLocalFields, correlatedReferenceFields);
2482
3530
  const query = {
2483
3531
  filter: _filter,
2484
3532
  select: finalSelect,
@@ -2511,8 +3559,14 @@ var Service = class extends Base {
2511
3559
  originalDocumentSnapshot: toObject(doc),
2512
3560
  resolvedQuery: query
2513
3561
  };
3562
+ const correlatedSnapshots = [cloneDeep4(toObject(doc))];
2514
3563
  try {
2515
3564
  doc = await this.includeDocs(doc, includes);
3565
+ if (correlatedIncludes.length > 0) {
3566
+ const docList = [doc];
3567
+ await this.includeCorrelatedDocs(docList, correlatedIncludes, correlatedSnapshots);
3568
+ doc = docList[0];
3569
+ }
2516
3570
  } catch (error2) {
2517
3571
  const result = this.getClientRequestErrorResult(error2);
2518
3572
  if (result) {
@@ -2530,8 +3584,20 @@ var Service = class extends Base {
2530
3584
  doc = await this.trimOutputFields(
2531
3585
  doc,
2532
3586
  access,
2533
- this.baseFieldsExt.concat(includePaths, normalizeSelect(overrideSelect))
3587
+ this.baseFieldsExt.concat(includePaths, correlatedOutputPaths, normalizeSelect(overrideSelect))
2534
3588
  );
3589
+ if (correlatedReferenceFields.length > 0) {
3590
+ const keep = /* @__PURE__ */ new Set([
3591
+ ...normalizeSelect(_select),
3592
+ ...normalizeSelect(overrideSelect),
3593
+ ...this.baseFieldsExt,
3594
+ ...includePaths,
3595
+ ...correlatedOutputPaths
3596
+ ]);
3597
+ for (const ref of correlatedReferenceFields) {
3598
+ if (!shouldKeepCorrelatedRef(ref, keep)) unsetDocPath(doc, ref);
3599
+ }
3600
+ }
2535
3601
  if (!includePermissions) doc = this.addEmptyPermissions(doc);
2536
3602
  this.completeOp("findOne", startedAt, "success" /* Success */, _filter);
2537
3603
  return { success: true, kind: "single", code: "success" /* Success */, data: doc, query, context };
@@ -2540,7 +3606,8 @@ var Service = class extends Base {
2540
3606
  const { select, populate, include, overrides } = this.resolveFindByIdArgs(args);
2541
3607
  const { skim, includePermissions, access, populateAccess, lean } = this.resolveFindByIdOptions(options);
2542
3608
  const { select: overrideSelect, populate: overridePopulate, idFilter: overrideIdFilter } = overrides ?? {};
2543
- const filter2 = overrideIdFilter || await this.genIDFilter(id);
3609
+ const filter2 = overrideIdFilter ?? await this.genIDFilter(id);
3610
+ if (filter2 === false) return { success: false, kind: "error", code: "forbidden" /* Forbidden */, query: { filter: filter2 } };
2544
3611
  return this.findOne(
2545
3612
  filter2,
2546
3613
  {
@@ -2562,26 +3629,29 @@ var Service = class extends Base {
2562
3629
  const { skim, includePermissions, includeCount, populateAccess, lean } = this.resolveFindOptions(options);
2563
3630
  const { filter: overrideFilter, select: overrideSelect, populate: overridePopulate } = overrides ?? {};
2564
3631
  let parsedFilter;
3632
+ let processedInclude;
2565
3633
  try {
2566
- parsedFilter = await this.parseClientData(filter2);
3634
+ processedInclude = this.processInclude(include);
3635
+ parsedFilter = overrideFilter === false ? false : await this.parseClientData(filter2);
2567
3636
  } catch (error2) {
2568
3637
  const result = this.getClientRequestErrorResult(error2);
2569
3638
  if (result) return result;
2570
3639
  throw error2;
2571
3640
  }
2572
3641
  const [_filter, _select, _populate, pagination, allowedSortFields] = await Promise.all([
2573
- overrideFilter || this.genFilter("list", parsedFilter),
3642
+ overrideFilter ?? this.genFilter("list", parsedFilter),
2574
3643
  overrideSelect || this.genQuerySelect("list", select),
2575
3644
  overridePopulate || this.genPopulate(populateAccess, populate),
2576
3645
  genPagination({ skip, limit, page, pageSize }, this.options.listHardLimit),
2577
3646
  this.genAllowedFields({}, "list", this.baseFieldsExt)
2578
3647
  ]);
2579
3648
  const finalSelect = normalizeSelect(_select);
2580
- const filteredPopulate = isArray7(finalSelect) && isArray7(_populate) ? _populate.filter((p) => finalSelect.includes(p.path.split(".")[0])) : _populate;
2581
- const { includes, includeLocalFields, includePaths } = this.processInclude(include);
3649
+ const filteredPopulate = isArray8(finalSelect) && isArray8(_populate) ? _populate.filter((p) => finalSelect.includes(p.path.split(".")[0])) : _populate;
3650
+ const { includes, correlatedIncludes, includeLocalFields, includePaths, correlatedReferenceFields } = processedInclude;
3651
+ const correlatedOutputPaths = correlatedIncludes.map((entry) => entry.path);
2582
3652
  const query = {
2583
3653
  filter: _filter,
2584
- select: finalSelect.concat(includeLocalFields),
3654
+ select: finalSelect.concat(includeLocalFields, correlatedReferenceFields),
2585
3655
  populate: filteredPopulate,
2586
3656
  sort,
2587
3657
  ...pagination
@@ -2590,7 +3660,7 @@ var Service = class extends Base {
2590
3660
  sort,
2591
3661
  skip: pagination.skip,
2592
3662
  limit: pagination.limit,
2593
- selectCount: finalSelect.concat(includeLocalFields).length,
3663
+ selectCount: finalSelect.concat(includeLocalFields, correlatedReferenceFields).length,
2594
3664
  populateCount: Array.isArray(filteredPopulate) ? filteredPopulate.length : filteredPopulate ? 1 : 0
2595
3665
  });
2596
3666
  if (_filter === false) {
@@ -2615,8 +3685,12 @@ var Service = class extends Base {
2615
3685
  resolvedQuery: query
2616
3686
  }));
2617
3687
  const _decorate = isFunction3(decorate) ? decorate : (v) => v;
3688
+ const correlatedSnapshots = docs.map((doc) => cloneDeep4(toObject(doc)));
2618
3689
  try {
2619
3690
  docs = await this.includeDocs(docs, includes);
3691
+ if (correlatedIncludes.length > 0) {
3692
+ docs = await this.includeCorrelatedDocs(docs, correlatedIncludes, correlatedSnapshots);
3693
+ }
2620
3694
  } catch (error2) {
2621
3695
  const result = this.getClientRequestErrorResult(error2);
2622
3696
  if (result) {
@@ -2638,8 +3712,20 @@ var Service = class extends Base {
2638
3712
  doc = await this.trimOutputFields(
2639
3713
  doc,
2640
3714
  "list",
2641
- this.baseFieldsExt.concat(includePaths, normalizeSelect(overrideSelect))
3715
+ this.baseFieldsExt.concat(includePaths, correlatedOutputPaths, normalizeSelect(overrideSelect))
2642
3716
  );
3717
+ if (correlatedReferenceFields.length > 0) {
3718
+ const keep = /* @__PURE__ */ new Set([
3719
+ ...normalizeSelect(_select),
3720
+ ...normalizeSelect(overrideSelect),
3721
+ ...this.baseFieldsExt,
3722
+ ...includePaths,
3723
+ ...correlatedOutputPaths
3724
+ ]);
3725
+ for (const ref of correlatedReferenceFields) {
3726
+ if (!shouldKeepCorrelatedRef(ref, keep)) unsetDocPath(doc, ref);
3727
+ }
3728
+ }
2643
3729
  doc = await _decorate(doc, contexts[i]);
2644
3730
  if (!includePermissions) doc = this.addEmptyPermissions(doc);
2645
3731
  return doc;
@@ -2724,7 +3810,7 @@ var Service = class extends Base {
2724
3810
  validationErrors.push({ index, errors: [] });
2725
3811
  return void 0;
2726
3812
  }
2727
- } else if (isArray7(validated)) {
3813
+ } else if (isArray8(validated)) {
2728
3814
  if (validated.length > 0) {
2729
3815
  validationErrors.push({
2730
3816
  index,
@@ -2771,7 +3857,7 @@ var Service = class extends Base {
2771
3857
  }
2772
3858
  if (includeDocPermissions) doc = await this.addDocPermissions(doc, "create", contexts[index]);
2773
3859
  if (includePermissions) doc = await this.addFieldPermissions(doc, "read", contexts[index]);
2774
- if (resolvedPopulate.length > 0) await populateDoc(doc, resolvedPopulate);
3860
+ if (resolvedPopulate.length > 0) await this.persist(() => populateDoc(doc, resolvedPopulate));
2775
3861
  doc = await this.trimOutputFields(doc, "read", this.baseFieldsExt);
2776
3862
  let outputDoc = await _decorate(doc, contexts[index]);
2777
3863
  if (!includePermissions) outputDoc = this.addEmptyPermissions(outputDoc);
@@ -2827,6 +3913,13 @@ var Service = class extends Base {
2827
3913
  data: doc
2828
3914
  };
2829
3915
  }
3916
+ /**
3917
+ * Update at authorized policy paths: admitted leaves preserve omitted siblings;
3918
+ * whole-authorized objects/arrays replace. Shared by updateById and existing-row
3919
+ * upsert. Validation precedes trusted prepare output (not re-filtered); partial
3920
+ * prepare containers preserve omissions. Transform retains explicit document-setter
3921
+ * replacement authority. Send nested client JSON, not literal dotted update keys.
3922
+ */
2830
3923
  async updateOne(filter2, data, args, options, decorate) {
2831
3924
  const filterErrors = this.validateClientFilter(filter2);
2832
3925
  if (filterErrors.length > 0) return { success: false, kind: "error", code: "bad_request" /* BadRequest */, errors: filterErrors };
@@ -2834,7 +3927,7 @@ var Service = class extends Base {
2834
3927
  const { skim, includePermissions, populateAccess } = this.resolveUpdateOneOptions(options);
2835
3928
  const { filter: overrideFilter, populate: overridePopulate } = overrides ?? {};
2836
3929
  const [_filter, _populate] = await Promise.all([
2837
- overrideFilter || this.genFilter("update", filter2),
3930
+ overrideFilter ?? this.genFilter("update", filter2),
2838
3931
  overridePopulate || this.genPopulate(populateAccess, populate)
2839
3932
  ]);
2840
3933
  const query = { filter: _filter, populate: _populate };
@@ -2881,7 +3974,7 @@ var Service = class extends Base {
2881
3974
  this.completeOp("updateOne", startedAt, "bad_request" /* BadRequest */, _filter);
2882
3975
  return { success: false, kind: "error", code: "bad_request" /* BadRequest */ };
2883
3976
  }
2884
- } else if (isArray7(validated)) {
3977
+ } else if (isArray8(validated)) {
2885
3978
  if (validated.length > 0) {
2886
3979
  this.completeOp("updateOne", startedAt, "bad_request" /* BadRequest */, _filter);
2887
3980
  return { success: false, kind: "error", code: "bad_request" /* BadRequest */, errors: validated };
@@ -2889,11 +3982,11 @@ var Service = class extends Base {
2889
3982
  }
2890
3983
  const prepared = await this.prepare(allowedData, "update", context);
2891
3984
  context.preparedData = prepared;
2892
- Object.assign(doc, prepared);
3985
+ applyUpdate(doc, prepared, allowedFields);
2893
3986
  context.modifiedPaths = doc.modifiedPaths();
2894
3987
  doc = assertModelDocument(await this.transform(doc, "update", context), this.modelName, "transform");
2895
3988
  context.currentDocument = doc;
2896
- doc = await doc.save();
3989
+ doc = await this.persist(() => doc.save());
2897
3990
  const diffExcludeFields = [this.options.documentPermissionField, "__v"];
2898
3991
  this.asServiceHookContext(context).diff = (d) => {
2899
3992
  context.changes = diff(
@@ -2913,7 +4006,7 @@ var Service = class extends Base {
2913
4006
  }
2914
4007
  if (includeDocPermissions) doc = await this.addDocPermissions(doc, "update", context);
2915
4008
  if (includePermissions) doc = await this.addFieldPermissions(doc, "update", context);
2916
- if (_populate) await populateDoc(doc, _populate);
4009
+ if (_populate) await this.persist(() => populateDoc(doc, _populate));
2917
4010
  doc = await this.trimOutputFields(doc, "read", this.baseFieldsExt);
2918
4011
  let outputDoc = doc;
2919
4012
  if (isFunction3(decorate)) outputDoc = await decorate(outputDoc, context);
@@ -2925,7 +4018,8 @@ var Service = class extends Base {
2925
4018
  const { populate, overrides } = this.resolveUpdateByIdArgs(args);
2926
4019
  const { skim, includePermissions, populateAccess } = this.resolveUpdateByIdOptions(options);
2927
4020
  const { populate: overridePopulate, idFilter: overrideIdFilter } = overrides;
2928
- const filter2 = overrideIdFilter || await this.genIDFilter(id);
4021
+ const filter2 = overrideIdFilter ?? await this.genIDFilter(id);
4022
+ if (filter2 === false) return { success: false, kind: "error", code: "forbidden" /* Forbidden */, query: { filter: filter2 } };
2929
4023
  return this.updateOne(
2930
4024
  filter2,
2931
4025
  data,
@@ -2945,7 +4039,7 @@ var Service = class extends Base {
2945
4039
  const { populate, overrides } = this.resolveUpsertArgs(args);
2946
4040
  const { skim, includePermissions, populateAccess } = this.resolveUpsertOptions(options);
2947
4041
  const { filter: overrideFilter, populate: overridePopulate } = overrides ?? {};
2948
- const _filter = await (overrideFilter || this.genFilter("update", filter2));
4042
+ const _filter = await (overrideFilter ?? this.genFilter("update", filter2));
2949
4043
  const query = { filter: _filter };
2950
4044
  const startedAt = this.beginOp("upsert", _filter);
2951
4045
  if (_filter === false) {
@@ -3005,7 +4099,9 @@ var Service = class extends Base {
3005
4099
  resolvedQuery: query
3006
4100
  };
3007
4101
  await this.beforeDelete(doc, context);
3008
- await ("deleteOne" in doc ? doc.deleteOne() : doc.remove());
4102
+ await this.persist(
4103
+ () => "deleteOne" in doc ? doc.deleteOne() : doc.remove()
4104
+ );
3009
4105
  context.finalDocumentSnapshot = toObject(doc);
3010
4106
  await this.afterDelete(doc, context);
3011
4107
  this.completeOp("delete", startedAt, "success" /* Success */, filter2);
@@ -3016,6 +4112,7 @@ var Service = class extends Base {
3016
4112
  if (filterErrors.length > 0) return { success: false, kind: "error", code: "bad_request" /* BadRequest */, errors: filterErrors };
3017
4113
  const { access, includeId } = this.resolveExistsOptions(options);
3018
4114
  filter2 = await this.genFilter(access, filter2);
4115
+ if (filter2 === false) return { success: false, kind: "error", code: "forbidden" /* Forbidden */, query: { filter: filter2 } };
3019
4116
  const result = await this.model.exists(filter2);
3020
4117
  return {
3021
4118
  success: true,
@@ -3068,6 +4165,30 @@ var Service = class extends Base {
3068
4165
  if (filter2 === false) return { success: false, kind: "error", code: "forbidden" /* Forbidden */, query };
3069
4166
  return { success: true, kind: "single", code: "success" /* Success */, data: await this.model.countDocuments(filter2), query };
3070
4167
  }
4168
+ /**
4169
+ * Trusted count for correlated includes (ACI-03): the caller already
4170
+ * resolved parent references, revalidated expanded operands, and applied
4171
+ * explicit `count` access via `genFilter`. This skips client-filter
4172
+ * validation and subquery parsing so substituted parent data stays inert.
4173
+ */
4174
+ async countTrusted(authorizedFilter) {
4175
+ const query = { filter: authorizedFilter };
4176
+ if (authorizedFilter === false) return { success: false, kind: "error", code: "forbidden" /* Forbidden */, query };
4177
+ return {
4178
+ success: true,
4179
+ kind: "single",
4180
+ code: "success" /* Success */,
4181
+ data: await this.model.countDocuments(authorizedFilter),
4182
+ query
4183
+ };
4184
+ }
4185
+ /**
4186
+ * Group target document IDs by original parent-key aliases using one aggregate.
4187
+ * Resolves the requested row policy (legacy count includes pass `count`), casts
4188
+ * authorized filters/foreign operands with the target schema, and deduplicates IDs
4189
+ * independently of pagination. Cast failures return BadRequest; false policy returns
4190
+ * Forbidden. Casting does not execute countDocuments middleware/plugins.
4191
+ */
3071
4192
  async countByFieldValues(foreignField, values, filter2 = {}, access = "list") {
3072
4193
  if (!isValidFieldPath(foreignField)) {
3073
4194
  return {
@@ -3086,7 +4207,25 @@ var Service = class extends Base {
3086
4207
  });
3087
4208
  const query = { filter: filter2 };
3088
4209
  if (filter2 === false) return { success: false, kind: "error", code: "forbidden" /* Forbidden */, query };
3089
- const matchFilter = filter2;
4210
+ let matchFilter = filter2;
4211
+ let castValues = uniqueValues;
4212
+ try {
4213
+ if (this.model.castFilter) {
4214
+ matchFilter = this.model.castFilter(matchFilter);
4215
+ const foreignFilter = this.model.castFilter({ [foreignField]: { $in: uniqueValues } });
4216
+ castValues = foreignFilter[foreignField]?.$in;
4217
+ if (!Array.isArray(castValues) || castValues.length !== uniqueValues.length) {
4218
+ throw new Error("Foreign field query casting must retain the join operands");
4219
+ }
4220
+ }
4221
+ } catch {
4222
+ return {
4223
+ success: false,
4224
+ kind: "error",
4225
+ code: "bad_request" /* BadRequest */,
4226
+ errors: [{ detail: "Invalid grouped include count filter or foreign-key value" }]
4227
+ };
4228
+ }
3090
4229
  const rows = await this.model.aggregate([
3091
4230
  { $match: matchFilter },
3092
4231
  {
@@ -3097,12 +4236,20 @@ var Service = class extends Base {
3097
4236
  }
3098
4237
  },
3099
4238
  { $unwind: "$foreignValues" },
3100
- { $match: { foreignValues: { $in: uniqueValues } } },
4239
+ { $match: { foreignValues: { $in: castValues } } },
3101
4240
  { $group: { _id: "$foreignValues", documentIds: { $addToSet: "$_id" } } }
3102
4241
  ]);
3103
- const counts = /* @__PURE__ */ new Map();
4242
+ const grouped = /* @__PURE__ */ new Map();
3104
4243
  for (const row of rows) {
3105
- counts.set(String(row._id), new Set(row.documentIds.map((id) => String(id))));
4244
+ const key = String(row._id);
4245
+ const ids = grouped.get(key) ?? /* @__PURE__ */ new Set();
4246
+ for (const id of row.documentIds) ids.add(String(id));
4247
+ grouped.set(key, ids);
4248
+ }
4249
+ const counts = /* @__PURE__ */ new Map();
4250
+ for (let i = 0; i < uniqueValues.length; i++) {
4251
+ const ids = grouped.get(String(castValues[i]));
4252
+ if (ids) counts.set(String(uniqueValues[i]), ids);
3106
4253
  }
3107
4254
  return { success: true, kind: "single", code: "success" /* Success */, data: counts, query };
3108
4255
  }
@@ -3181,17 +4328,60 @@ var Service = class extends Base {
3181
4328
  async readSub(id, sub, subId, options) {
3182
4329
  return readSub(this, id, sub, subId, options);
3183
4330
  }
4331
+ /**
4332
+ * Apply authorized policy-path updates, preserving protected siblings, then save.
4333
+ * Output requires post-save parent read and subdocument read operation/row access;
4334
+ * hidden output is successful `data: null` (direct/root-entry 200), not a failed write.
4335
+ * Visible output uses read fields plus `_id`. Do not retry solely for null output.
4336
+ */
3184
4337
  async updateSub(id, sub, subId, data) {
3185
- return updateSub(this, id, sub, subId, data);
4338
+ return updateSub(
4339
+ this,
4340
+ this.persist,
4341
+ (access) => this.req.macl.isAllowed(this.modelName, access),
4342
+ id,
4343
+ sub,
4344
+ subId,
4345
+ data
4346
+ );
3186
4347
  }
4348
+ /**
4349
+ * Save authorized targeted updates, then return readable targets in stored order.
4350
+ * Requires parent/subdocument read access for output, not subdocument list access;
4351
+ * uses read fields plus `_id`. Hidden output succeeds with `data: [], count: 0`
4352
+ * (direct/root-entry 200); count measures visible rows, not all updated rows.
4353
+ */
3187
4354
  async bulkUpdateSub(id, sub, data) {
3188
- return bulkUpdateSub(this, id, sub, castArray2(data));
4355
+ return bulkUpdateSub(
4356
+ this,
4357
+ this.persist,
4358
+ (access) => this.req.macl.isAllowed(this.modelName, access),
4359
+ id,
4360
+ sub,
4361
+ castArray2(data)
4362
+ );
3189
4363
  }
4364
+ /**
4365
+ * Append/prepend admitted rows, then return the visible full array (including when
4366
+ * input is empty). Output requires post-save parent read and subdocument list AND
4367
+ * read guards/row filters; fields use read projection plus `_id`.
4368
+ * Hidden output succeeds with code `created`, `data: [], count: 0`
4369
+ * (direct/root-entry 201). Visible count/order follow filtered stored rows.
4370
+ * Empty output does not mean the write failed; addFirst is a service option.
4371
+ */
3190
4372
  async createSub(id, sub, data, options) {
3191
- return createSub(this, id, sub, data, options);
4373
+ return createSub(
4374
+ this,
4375
+ this.persist,
4376
+ (access) => this.req.macl.isAllowed(this.modelName, access),
4377
+ id,
4378
+ sub,
4379
+ data,
4380
+ options
4381
+ );
3192
4382
  }
3193
4383
  async deleteSub(id, sub, subId) {
3194
- return deleteSub(this, id, sub, subId);
4384
+ return deleteSub(this, this.persist, id, sub, subId);
3195
4385
  }
3196
4386
  async getParentDoc(id, sub, args, options) {
3197
4387
  return getParentDoc(this, id, sub, args, options);
@@ -3265,6 +4455,7 @@ var PublicService = class extends Service {
3265
4455
  },
3266
4456
  { skim, includePermissions, access, populateAccess, lean }
3267
4457
  );
4458
+ if (!result.success && (result.code === "bad_request" /* BadRequest */ || result.code === "forbidden" /* Forbidden */)) return result;
3268
4459
  if (tryList && (!result.success || !result.data)) {
3269
4460
  const listAllowed = await this.req.macl.isAllowed(this.modelName, "list");
3270
4461
  if (!listAllowed) {
@@ -3305,6 +4496,7 @@ var PublicService = class extends Service {
3305
4496
  },
3306
4497
  { skim, includePermissions, access, populateAccess, lean }
3307
4498
  );
4499
+ if (!result.success && (result.code === "bad_request" /* BadRequest */ || result.code === "forbidden" /* Forbidden */)) return result;
3308
4500
  if (tryList && (!result.success || !result.data)) {
3309
4501
  const listAllowed = await this.req.macl.isAllowed(this.modelName, "list");
3310
4502
  if (!listAllowed) {
@@ -3353,6 +4545,11 @@ var PublicService = class extends Service {
3353
4545
  }
3354
4546
  return result;
3355
4547
  }
4548
+ /**
4549
+ * With an _id, check update-policy existence then update: terminal denial returns
4550
+ * Forbidden, an allowed miss remains Unauthorized, and neither creates a row.
4551
+ * Without an _id, use the create path.
4552
+ */
3356
4553
  async _upsert(data, args, options) {
3357
4554
  const idKey = this.getIdentifier();
3358
4555
  if (idKey !== "_id") {
@@ -3583,29 +4780,64 @@ var DataService = class {
3583
4780
  };
3584
4781
 
3585
4782
  // src/processors.ts
3586
- import { cloneDeep, forEach as forEach6, get as get7, isArray as isArray8, isPlainObject as isPlainObject13, map, set as set5 } from "@web-ts-toolkit/utils";
4783
+ import { cloneDeep as cloneDeep5, forEach as forEach6, get as get7, hasOwn, isArray as isArray9, isPlainObject as isPlainObject15, map, set as set6 } from "@web-ts-toolkit/utils";
3587
4784
  var UNSAFE_PATH_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
3588
- function failUnsafeSegment(path) {
4785
+ var DEFAULT_MAX_SLICE = 1e3;
4786
+ var DEFAULT_MASK_REPLACEMENT = "***";
4787
+ function failUnsafeSegment(path, fn = "copyAndDepopulate") {
3589
4788
  throw new Error(
3590
- `copyAndDepopulate: refusing path '${path}' because it contains an unsafe segment (__proto__, prototype, or constructor).`
4789
+ `${fn}: refusing path '${path}' because it contains an unsafe segment (__proto__, prototype, or constructor).`
3591
4790
  );
3592
4791
  }
3593
4792
  function failMissingId(path) {
3594
4793
  throw new Error(`copyAndDepopulate: path '${path}' resolved to a record missing the configured id field.`);
3595
4794
  }
3596
- function assertSafePath(path) {
4795
+ function failInvalidSlice(value, field, fn) {
4796
+ throw new Error(`${fn}: '${field}' must be a non-negative safe integer, received ${String(value)}.`);
4797
+ }
4798
+ function assertSafePath(path, fn = "copyAndDepopulate") {
3597
4799
  const segs = path.split(".");
3598
4800
  for (const seg of segs) {
3599
4801
  if (seg.length === 0) continue;
3600
4802
  if (UNSAFE_PATH_KEYS.has(seg)) {
3601
- failUnsafeSegment(path);
4803
+ failUnsafeSegment(path, fn);
3602
4804
  }
3603
4805
  }
3604
4806
  }
4807
+ function assertNonNegativeSafeInteger(value, field, fn) {
4808
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
4809
+ failInvalidSlice(value, field, fn);
4810
+ }
4811
+ }
4812
+ function resolveLeafParents(obj, src) {
4813
+ const segs = src.split(".");
4814
+ let targets = [obj];
4815
+ for (let ind = 0; ind < segs.length - 1; ind += 1) {
4816
+ const seg = segs[ind];
4817
+ targets = targets.reduce((ret, target) => {
4818
+ if (!isPlainObject15(target)) return ret;
4819
+ const targetObject = target;
4820
+ const next = targetObject[seg];
4821
+ if (isArray9(next)) {
4822
+ for (const item of next) {
4823
+ ret.push(item);
4824
+ }
4825
+ } else if (next !== null && next !== void 0) ret.push(next);
4826
+ return ret;
4827
+ }, []);
4828
+ }
4829
+ const leaf = segs[segs.length - 1];
4830
+ const pairs = [];
4831
+ forEach6(targets, (target) => {
4832
+ if (!isPlainObject15(target)) return;
4833
+ pairs.push({ parent: target, leaf });
4834
+ });
4835
+ return pairs;
4836
+ }
3605
4837
  var copyAndDepopulate = (docObject, operations, options = { mutable: true, idField: "_id" }) => {
3606
4838
  const mutable = options.mutable !== false;
3607
4839
  const idField = options.idField ?? "_id";
3608
- const obj = mutable ? docObject : cloneDeep(docObject);
4840
+ const obj = mutable ? docObject : cloneDeep5(docObject);
3609
4841
  forEach6(Array.isArray(operations) ? operations : [], (op) => {
3610
4842
  if (!op || typeof op.src !== "string" || typeof op.dest !== "string") return;
3611
4843
  const src = op.src;
@@ -3618,15 +4850,15 @@ var copyAndDepopulate = (docObject, operations, options = { mutable: true, idFie
3618
4850
  forEach6(segs, (seg, ind) => {
3619
4851
  if (segs.length === ind + 1) {
3620
4852
  forEach6(targets, (target) => {
3621
- if (!isPlainObject13(target)) return;
4853
+ if (!isPlainObject15(target)) return;
3622
4854
  const targetObject = target;
3623
4855
  const leaf = targetObject[seg];
3624
4856
  const sourceForDest = leaf;
3625
4857
  let depopulated;
3626
- if (isArray8(leaf)) {
4858
+ if (isArray9(leaf)) {
3627
4859
  if (leaf.length === 0) {
3628
4860
  depopulated = [];
3629
- } else if (leaf.every((item) => isPlainObject13(item))) {
4861
+ } else if (leaf.every((item) => isPlainObject15(item))) {
3630
4862
  depopulated = map(leaf, idField).map((id, i) => {
3631
4863
  if (id === void 0) {
3632
4864
  failMissingId(`${src}[${i}]`);
@@ -3636,7 +4868,7 @@ var copyAndDepopulate = (docObject, operations, options = { mutable: true, idFie
3636
4868
  } else {
3637
4869
  return;
3638
4870
  }
3639
- } else if (isPlainObject13(leaf)) {
4871
+ } else if (isPlainObject15(leaf)) {
3640
4872
  const id = get7(leaf, idField);
3641
4873
  if (id === void 0) {
3642
4874
  failMissingId(src);
@@ -3645,15 +4877,15 @@ var copyAndDepopulate = (docObject, operations, options = { mutable: true, idFie
3645
4877
  } else {
3646
4878
  return;
3647
4879
  }
3648
- set5(targetObject, dest, sourceForDest);
3649
- set5(targetObject, seg, depopulated);
4880
+ set6(targetObject, dest, sourceForDest);
4881
+ set6(targetObject, seg, depopulated);
3650
4882
  });
3651
4883
  } else {
3652
4884
  targets = targets.reduce((ret, target) => {
3653
- if (!isPlainObject13(target)) return ret;
4885
+ if (!isPlainObject15(target)) return ret;
3654
4886
  const targetObject = target;
3655
4887
  const next = targetObject[seg];
3656
- if (isArray8(next)) {
4888
+ if (isArray9(next)) {
3657
4889
  for (const item of next) {
3658
4890
  ret.push(item);
3659
4891
  }
@@ -3665,6 +4897,125 @@ var copyAndDepopulate = (docObject, operations, options = { mutable: true, idFie
3665
4897
  });
3666
4898
  return obj;
3667
4899
  };
4900
+ var copyPaths = (docObject, operations, options = { mutable: true }) => {
4901
+ const mutable = options.mutable !== false;
4902
+ const obj = mutable ? docObject : cloneDeep5(docObject);
4903
+ forEach6(Array.isArray(operations) ? operations : [], (op) => {
4904
+ if (!op || typeof op.src !== "string" || typeof op.dest !== "string") return;
4905
+ const src = op.src;
4906
+ const dest = op.dest;
4907
+ if (src.length === 0 || dest.length === 0) return;
4908
+ assertSafePath(src, "copyPaths");
4909
+ assertSafePath(dest, "copyPaths");
4910
+ if (src === dest) return;
4911
+ forEach6(resolveLeafParents(obj, src), ({ parent, leaf }) => {
4912
+ if (!hasOwn(parent, leaf)) return;
4913
+ set6(parent, dest, cloneDeep5(parent[leaf]));
4914
+ });
4915
+ });
4916
+ return obj;
4917
+ };
4918
+ var movePaths = (docObject, operations, options = { mutable: true }) => {
4919
+ const mutable = options.mutable !== false;
4920
+ const obj = mutable ? docObject : cloneDeep5(docObject);
4921
+ forEach6(Array.isArray(operations) ? operations : [], (op) => {
4922
+ if (!op || typeof op.src !== "string" || typeof op.dest !== "string") return;
4923
+ const src = op.src;
4924
+ const dest = op.dest;
4925
+ if (src.length === 0 || dest.length === 0) return;
4926
+ assertSafePath(src, "movePaths");
4927
+ assertSafePath(dest, "movePaths");
4928
+ if (src === dest) return;
4929
+ forEach6(resolveLeafParents(obj, src), ({ parent, leaf }) => {
4930
+ if (!hasOwn(parent, leaf)) return;
4931
+ const value = cloneDeep5(parent[leaf]);
4932
+ set6(parent, dest, value);
4933
+ if (dest === leaf || dest.startsWith(`${leaf}.`)) return;
4934
+ delete parent[leaf];
4935
+ });
4936
+ });
4937
+ return obj;
4938
+ };
4939
+ var sliceArrays = (docObject, operations, options = { mutable: true, maxSlice: DEFAULT_MAX_SLICE }) => {
4940
+ const mutable = options.mutable !== false;
4941
+ const maxSlice = options.maxSlice ?? DEFAULT_MAX_SLICE;
4942
+ assertNonNegativeSafeInteger(maxSlice, "maxSlice", "sliceArrays");
4943
+ const obj = mutable ? docObject : cloneDeep5(docObject);
4944
+ forEach6(Array.isArray(operations) ? operations : [], (op) => {
4945
+ if (!op || typeof op.src !== "string") return;
4946
+ const src = op.src;
4947
+ if (src.length === 0) return;
4948
+ assertSafePath(src, "sliceArrays");
4949
+ const rawDest = typeof op.dest === "undefined" ? void 0 : op.dest;
4950
+ if (typeof rawDest !== "undefined") {
4951
+ if (typeof rawDest !== "string" || rawDest.length === 0) return;
4952
+ assertSafePath(rawDest, "sliceArrays");
4953
+ }
4954
+ const skip = op.skip ?? 0;
4955
+ assertNonNegativeSafeInteger(skip, "skip", "sliceArrays");
4956
+ let limit;
4957
+ if (typeof op.limit !== "undefined") {
4958
+ assertNonNegativeSafeInteger(op.limit, "limit", "sliceArrays");
4959
+ limit = Math.min(op.limit, maxSlice);
4960
+ }
4961
+ forEach6(resolveLeafParents(obj, src), ({ parent, leaf }) => {
4962
+ const leafValue = parent[leaf];
4963
+ if (!isArray9(leafValue)) return;
4964
+ const end = typeof limit === "undefined" ? void 0 : skip + limit;
4965
+ const window = leafValue.slice(skip, end);
4966
+ if (typeof rawDest === "undefined") {
4967
+ set6(parent, leaf, window);
4968
+ } else {
4969
+ set6(parent, rawDest, window);
4970
+ }
4971
+ });
4972
+ });
4973
+ return obj;
4974
+ };
4975
+ var countPaths = (docObject, operations, options = { mutable: true }) => {
4976
+ const mutable = options.mutable !== false;
4977
+ const obj = mutable ? docObject : cloneDeep5(docObject);
4978
+ forEach6(Array.isArray(operations) ? operations : [], (op) => {
4979
+ if (!op || typeof op.src !== "string" || typeof op.dest !== "string") return;
4980
+ const src = op.src;
4981
+ const dest = op.dest;
4982
+ if (src.length === 0 || dest.length === 0) return;
4983
+ assertSafePath(src, "countPaths");
4984
+ assertSafePath(dest, "countPaths");
4985
+ forEach6(resolveLeafParents(obj, src), ({ parent, leaf }) => {
4986
+ if (!hasOwn(parent, leaf)) return;
4987
+ const leafValue = parent[leaf];
4988
+ let count;
4989
+ if (isArray9(leafValue)) {
4990
+ count = leafValue.length;
4991
+ } else if (isPlainObject15(leafValue)) {
4992
+ count = Object.keys(leafValue).length;
4993
+ } else if (leafValue === null || typeof leafValue === "undefined") {
4994
+ count = 0;
4995
+ } else {
4996
+ count = 1;
4997
+ }
4998
+ set6(parent, dest, count);
4999
+ });
5000
+ });
5001
+ return obj;
5002
+ };
5003
+ var maskPaths = (docObject, operations, options = { mutable: true }) => {
5004
+ const mutable = options.mutable !== false;
5005
+ const obj = mutable ? docObject : cloneDeep5(docObject);
5006
+ forEach6(Array.isArray(operations) ? operations : [], (op) => {
5007
+ if (!op || typeof op.src !== "string") return;
5008
+ const src = op.src;
5009
+ if (src.length === 0) return;
5010
+ assertSafePath(src, "maskPaths");
5011
+ const replacement = Object.prototype.hasOwnProperty.call(op, "replacement") ? op.replacement : DEFAULT_MASK_REPLACEMENT;
5012
+ forEach6(resolveLeafParents(obj, src), ({ parent, leaf }) => {
5013
+ if (!hasOwn(parent, leaf)) return;
5014
+ set6(parent, leaf, replacement);
5015
+ });
5016
+ });
5017
+ return obj;
5018
+ };
3668
5019
 
3669
5020
  // src/symbols.ts
3670
5021
  var MIDDLEWARE = /* @__PURE__ */ Symbol.for("@web-ts-toolkit/access-router/middleware");
@@ -3707,18 +5058,18 @@ var Cache = class {
3707
5058
  import {
3708
5059
  castArray as castArray3,
3709
5060
  arrayToRecord,
3710
- hasOwn as hasOwn2,
3711
- isArray as isArray9,
5061
+ hasOwn as hasOwn3,
5062
+ isArray as isArray10,
3712
5063
  isBoolean as isBoolean4,
3713
5064
  isEmpty as isEmpty2,
3714
5065
  isEqual,
3715
5066
  isFunction as isFunction4,
3716
- isPlainObject as isPlainObject14,
5067
+ isPlainObject as isPlainObject16,
3717
5068
  isString as isString5
3718
5069
  } from "@web-ts-toolkit/utils";
3719
5070
 
3720
5071
  // src/permission.ts
3721
- import { hasOwn } from "@web-ts-toolkit/utils";
5072
+ import { hasOwn as hasOwn2 } from "@web-ts-toolkit/utils";
3722
5073
  var Permission = class {
3723
5074
  constructor(permissions) {
3724
5075
  this.permissions = { ...permissions };
@@ -3734,7 +5085,7 @@ var Permission = class {
3734
5085
  return [permissionOrPermissions].concat(permissions);
3735
5086
  }
3736
5087
  hasKey(permission) {
3737
- return hasOwn(this.permissions, permission);
5088
+ return hasOwn2(this.permissions, permission);
3738
5089
  }
3739
5090
  has(permission) {
3740
5091
  return this.permissions[permission] === true;
@@ -3754,10 +5105,10 @@ var permission_default = Permission;
3754
5105
 
3755
5106
  // src/core-shared.ts
3756
5107
  function isAndFilter(filter2) {
3757
- return !!filter2 && isPlainObject14(filter2) && Object.keys(filter2).length === 1 && isArray9(filter2.$and);
5108
+ return !!filter2 && isPlainObject16(filter2) && Object.keys(filter2).length === 1 && isArray10(filter2.$and);
3758
5109
  }
3759
5110
  function isMergeableClause(filter2) {
3760
- return !!filter2 && isPlainObject14(filter2) && Object.keys(filter2).every((key) => !key.startsWith("$"));
5111
+ return !!filter2 && isPlainObject16(filter2) && Object.keys(filter2).every((key) => !key.startsWith("$"));
3761
5112
  }
3762
5113
  function optimizeAndFilter(clausesInput) {
3763
5114
  const clauses = [];
@@ -3772,10 +5123,10 @@ function optimizeAndFilter(clausesInput) {
3772
5123
  }
3773
5124
  }
3774
5125
  const dedupeKey = (value) => {
3775
- if (isArray9(value)) {
5126
+ if (isArray10(value)) {
3776
5127
  return `[${value.map(dedupeKey).join(",")}]`;
3777
5128
  }
3778
- if (isPlainObject14(value)) {
5129
+ if (isPlainObject16(value)) {
3779
5130
  return `{${Object.keys(value).sort().map((key) => `${key}:${dedupeKey(value[key])}`).join(",")}}`;
3780
5131
  }
3781
5132
  return JSON.stringify(value);
@@ -3799,7 +5150,7 @@ function optimizeAndFilter(clausesInput) {
3799
5150
  }
3800
5151
  let canMerge = true;
3801
5152
  for (const [key, value] of Object.entries(clause)) {
3802
- if (hasOwn2(mergedClause, key) && !isEqual(mergedClause[key], value)) {
5153
+ if (hasOwn3(mergedClause, key) && !isEqual(mergedClause[key], value)) {
3803
5154
  canMerge = false;
3804
5155
  break;
3805
5156
  }
@@ -3817,7 +5168,7 @@ function optimizeAndFilter(clausesInput) {
3817
5168
  }
3818
5169
  function normalizeFilter(filter2) {
3819
5170
  if (filter2 === false) return false;
3820
- if (!filter2 || !isPlainObject14(filter2) || isEmpty2(filter2)) return null;
5171
+ if (!filter2 || !isPlainObject16(filter2) || isEmpty2(filter2)) return null;
3821
5172
  if (!isAndFilter(filter2)) {
3822
5173
  return filter2;
3823
5174
  }
@@ -3884,8 +5235,8 @@ async function setRequestPermissions(req) {
3884
5235
  const globalPermissions = getGlobalOption("globalPermissions");
3885
5236
  if (!isFunction4(globalPermissions)) return;
3886
5237
  const permissions = await globalPermissions.call(req, req);
3887
- if (isPlainObject14(permissions)) req[requestPermissionField] = permissions;
3888
- else if (isArray9(permissions)) req[requestPermissionField] = arrayToRecord(permissions);
5238
+ if (isPlainObject16(permissions)) req[requestPermissionField] = permissions;
5239
+ else if (isArray10(permissions)) req[requestPermissionField] = arrayToRecord(permissions);
3889
5240
  else if (isString5(permissions)) req[requestPermissionField] = { [permissions]: true };
3890
5241
  owners.set(requestPermissionField, activeRuntime);
3891
5242
  }
@@ -3898,7 +5249,7 @@ async function evaluateRouteGuard(req, permissions, routeGuard) {
3898
5249
  if (isString5(routeGuard)) {
3899
5250
  return stringHandler(routeGuard);
3900
5251
  }
3901
- if (isArray9(routeGuard)) {
5252
+ if (isArray10(routeGuard)) {
3902
5253
  return arrayHandler(routeGuard);
3903
5254
  }
3904
5255
  if (isFunction4(routeGuard)) {
@@ -3936,7 +5287,7 @@ async function collectSchemaFields({
3936
5287
  if (value) fields.push(key);
3937
5288
  } else if (isString5(value)) {
3938
5289
  if (stringHandler(value)) fields.push(key);
3939
- } else if (isArray9(value)) {
5290
+ } else if (isArray10(value)) {
3940
5291
  if (arrayHandler(value)) fields.push(key);
3941
5292
  } else if (isFunction4(value)) {
3942
5293
  if (await value.call(req, ...functionArgs)) fields.push(key);
@@ -4241,7 +5592,7 @@ var Core = class {
4241
5592
  if (isFunction5(validate)) {
4242
5593
  const permissions = this.getGlobalPermissions();
4243
5594
  return validate.call(this.req, allowedData, permissions, context);
4244
- } else if (isBoolean5(validate) || isArray10(validate)) {
5595
+ } else if (isBoolean5(validate) || isArray11(validate)) {
4245
5596
  return validate;
4246
5597
  } else {
4247
5598
  return true;
@@ -4385,6 +5736,41 @@ var Core = class {
4385
5736
  options
4386
5737
  );
4387
5738
  break;
5739
+ case "COPY":
5740
+ docObject = copyPaths(
5741
+ docObject,
5742
+ args,
5743
+ options
5744
+ );
5745
+ break;
5746
+ case "MOVE":
5747
+ docObject = movePaths(
5748
+ docObject,
5749
+ args,
5750
+ options
5751
+ );
5752
+ break;
5753
+ case "SLICE":
5754
+ docObject = sliceArrays(
5755
+ docObject,
5756
+ args,
5757
+ options
5758
+ );
5759
+ break;
5760
+ case "COUNT":
5761
+ docObject = countPaths(
5762
+ docObject,
5763
+ args,
5764
+ options
5765
+ );
5766
+ break;
5767
+ case "MASK":
5768
+ docObject = maskPaths(
5769
+ docObject,
5770
+ args,
5771
+ options
5772
+ );
5773
+ break;
4388
5774
  }
4389
5775
  });
4390
5776
  return docObject;
@@ -4462,19 +5848,19 @@ function guard(condition) {
4462
5848
  const permissions = req[PERMISSIONS];
4463
5849
  let cond = condition;
4464
5850
  let phas = (key) => permissions.has(key);
4465
- if (isPlainObject15(condition)) {
5851
+ if (isPlainObject17(condition)) {
4466
5852
  const { modelName, id, condition: _cond } = condition;
4467
5853
  const svc = req.macl.getPublicService(modelName);
4468
5854
  const select = getModelOption(modelName, `alwaysSelectFields.read`, void 0);
4469
5855
  let _id = isString7(id) ? id : null;
4470
- if (isPlainObject15(id)) {
5856
+ if (isPlainObject17(id)) {
4471
5857
  const { type = "param", key } = id;
4472
5858
  if (type === "param") {
4473
5859
  const paramValue = req.params[key];
4474
- _id = isArray11(paramValue) ? paramValue[0] ?? null : paramValue ?? null;
5860
+ _id = isArray12(paramValue) ? paramValue[0] ?? null : paramValue ?? null;
4475
5861
  } else if (type === "query") {
4476
5862
  const _qval = req.query[key];
4477
- if (isArray11(_qval)) _id = _qval.length > 0 ? _qval[0] : null;
5863
+ if (isArray12(_qval)) _id = _qval.length > 0 ? _qval[0] : null;
4478
5864
  else _id = _qval;
4479
5865
  } else {
4480
5866
  _id = null;
@@ -4494,7 +5880,7 @@ function guard(condition) {
4494
5880
  const { stringHandler, arrayHandler } = createValidator(phas);
4495
5881
  if (isString7(cond)) {
4496
5882
  if (stringHandler(cond)) return next();
4497
- } else if (isArray11(cond)) {
5883
+ } else if (isArray12(cond)) {
4498
5884
  if (arrayHandler(cond)) return next();
4499
5885
  } else if (isFunction6(cond)) {
4500
5886
  const result = await cond.call(req, permissions);
@@ -4509,7 +5895,7 @@ import express2 from "express";
4509
5895
 
4510
5896
  // src/routers/model-router.ts
4511
5897
  import JsonRouter5 from "@web-ts-toolkit/express-json-router";
4512
- import { forEach as forEach8, isPlainObject as isPlainObject16, isString as isString8, isUndefined as isUndefined2, normalizeUrlPath as normalizeUrlPath2, padEnd } from "@web-ts-toolkit/utils";
5898
+ import { forEach as forEach8, isPlainObject as isPlainObject19, isString as isString8, isUndefined as isUndefined2, normalizeUrlPath as normalizeUrlPath3, padEnd } from "@web-ts-toolkit/utils";
4513
5899
 
4514
5900
  // src/routers/router-mutation.ts
4515
5901
  var MODEL_BUILD_TIME_OPTION_KEYS = /* @__PURE__ */ new Set([
@@ -4638,6 +6024,7 @@ import JsonRouter4 from "@web-ts-toolkit/express-json-router";
4638
6024
 
4639
6025
  // src/validation/common.ts
4640
6026
  import { z as z2 } from "zod";
6027
+ import { isPlainObject as isPlainObject18 } from "@web-ts-toolkit/utils";
4641
6028
  var stringOrStringArray = z2.union([z2.string(), z2.array(z2.string())]);
4642
6029
  var safeIntegerString = (min, message) => z2.string().regex(/^\d+$/, message).refine((value) => Number.isSafeInteger(Number(value)), "Expected a safe integer").refine((value) => Number(value) >= min, message);
4643
6030
  var queryBooleanString = z2.enum(["true", "false"]);
@@ -4698,7 +6085,8 @@ var subPopulateSchema = z2.union([
4698
6085
  select: projectionSchema.optional()
4699
6086
  }).passthrough()
4700
6087
  ]);
4701
- var includeItemSchema = z2.object({
6088
+ var legacyIncludeItemSchema = z2.object({
6089
+ mode: z2.enum(["legacy"]).optional(),
4702
6090
  model: z2.string().min(1),
4703
6091
  op: z2.enum(["list", "read", "count"]),
4704
6092
  path: z2.string().min(1),
@@ -4707,8 +6095,139 @@ var includeItemSchema = z2.object({
4707
6095
  foreignField: z2.string().min(1),
4708
6096
  args: z2.unknown().optional().describe("Forwarded to list/read includes. Count includes ignore pagination fields so counts remain exact."),
4709
6097
  options: z2.unknown().optional()
4710
- }).passthrough();
4711
- var includeSchema = z2.union([includeItemSchema, z2.array(includeItemSchema)]);
6098
+ }).passthrough().superRefine((entry, ctx) => {
6099
+ if (entry.filter !== void 0 && containsParentMarkerShape(entry.filter)) {
6100
+ ctx.addIssue({
6101
+ code: z2.ZodIssueCode.custom,
6102
+ message: 'Legacy includes must not contain $parent markers; use a correlated include (mode: "correlated")',
6103
+ path: ["filter"]
6104
+ });
6105
+ }
6106
+ });
6107
+ var parentRefSchema = z2.object({ $parent: z2.string().min(1) }).strict().superRefine((value, ctx) => {
6108
+ if (!isValidParentRefPath(value.$parent)) {
6109
+ ctx.addIssue({
6110
+ code: z2.ZodIssueCode.custom,
6111
+ message: `Invalid $parent reference path: ${value.$parent}`,
6112
+ path: ["$parent"]
6113
+ });
6114
+ }
6115
+ });
6116
+ var correlatedIdSchema = z2.union([z2.string().min(1), parentRefSchema]);
6117
+ var correlatedFilterSchema = z2.record(z2.string(), z2.unknown()).superRefine((filter2, ctx) => {
6118
+ for (const issue of validateCorrelatedFilterTemplate(filter2)) {
6119
+ ctx.addIssue({
6120
+ code: z2.ZodIssueCode.custom,
6121
+ message: issue.message,
6122
+ path: issue.path.map(String)
6123
+ });
6124
+ }
6125
+ });
6126
+ var correlatedEmptyOptionsSchema = z2.unknown().refine(
6127
+ (value) => value === void 0 || isPlainObject18(value) && Object.keys(value).length === 0,
6128
+ "Correlated include options must be absent or empty"
6129
+ ).optional();
6130
+ var correlatedPathSchema = z2.string().min(1).superRefine((path, ctx) => {
6131
+ if (!isValidCorrelatedOutputPath(path)) {
6132
+ ctx.addIssue({
6133
+ code: z2.ZodIssueCode.custom,
6134
+ message: `Invalid correlated include output path: ${path}`,
6135
+ path: []
6136
+ });
6137
+ }
6138
+ });
6139
+ var checkArgsMarkers = (select, sort, ctx) => {
6140
+ const selectIssue = findArgsMarkerIssue(select, "args.select");
6141
+ if (selectIssue) {
6142
+ ctx.addIssue({ code: z2.ZodIssueCode.custom, message: selectIssue.message, path: ["select"] });
6143
+ }
6144
+ const sortIssue = findArgsMarkerIssue(sort, "args.sort");
6145
+ if (sortIssue) {
6146
+ ctx.addIssue({ code: z2.ZodIssueCode.custom, message: sortIssue.message, path: ["sort"] });
6147
+ }
6148
+ };
6149
+ var correlatedReadArgsSchema = z2.object({
6150
+ select: projectionSchema.optional(),
6151
+ sort: sortSchema.optional(),
6152
+ include: z2.lazy(() => correlatedNestedIncludeSchema).optional()
6153
+ }).strict().superRefine((args, ctx) => checkArgsMarkers(args.select, args.sort, ctx));
6154
+ var correlatedListArgsSchema = z2.object({
6155
+ select: projectionSchema.optional(),
6156
+ sort: sortSchema.optional(),
6157
+ skip: z2.union([nonNegativeIntegerSchema, nonNegativeIntegerString]).optional(),
6158
+ limit: z2.union([positiveIntegerSchema, positiveIntegerString]).optional(),
6159
+ page: z2.union([nonNegativeIntegerSchema, nonNegativeIntegerString]).optional(),
6160
+ pageSize: z2.union([positiveIntegerSchema, positiveIntegerString]).optional(),
6161
+ include: z2.lazy(() => correlatedNestedIncludeSchema).optional()
6162
+ }).strict().superRefine((args, ctx) => checkArgsMarkers(args.select, args.sort, ctx));
6163
+ var correlatedCountArgsSchema = z2.unknown().refine(
6164
+ (value) => value === void 0 || isPlainObject18(value) && Object.keys(value).length === 0,
6165
+ "Count includes accept no args"
6166
+ ).optional();
6167
+ var correlatedReadByIdSchema = z2.object({
6168
+ mode: z2.literal("correlated"),
6169
+ model: z2.string().min(1),
6170
+ op: z2.literal("read"),
6171
+ path: correlatedPathSchema,
6172
+ id: correlatedIdSchema,
6173
+ args: correlatedReadArgsSchema.optional(),
6174
+ options: correlatedEmptyOptionsSchema
6175
+ }).strict();
6176
+ var correlatedReadByFilterSchema = z2.object({
6177
+ mode: z2.literal("correlated"),
6178
+ model: z2.string().min(1),
6179
+ op: z2.literal("read"),
6180
+ path: correlatedPathSchema,
6181
+ filter: correlatedFilterSchema,
6182
+ args: correlatedReadArgsSchema.optional(),
6183
+ options: correlatedEmptyOptionsSchema
6184
+ }).strict();
6185
+ var correlatedListSchema = z2.object({
6186
+ mode: z2.literal("correlated"),
6187
+ model: z2.string().min(1),
6188
+ op: z2.literal("list"),
6189
+ path: correlatedPathSchema,
6190
+ filter: correlatedFilterSchema,
6191
+ args: correlatedListArgsSchema.optional(),
6192
+ options: correlatedEmptyOptionsSchema
6193
+ }).strict();
6194
+ var correlatedCountSchema = z2.object({
6195
+ mode: z2.literal("correlated"),
6196
+ model: z2.string().min(1),
6197
+ op: z2.literal("count"),
6198
+ path: correlatedPathSchema,
6199
+ filter: correlatedFilterSchema,
6200
+ args: correlatedCountArgsSchema,
6201
+ options: correlatedEmptyOptionsSchema
6202
+ }).strict();
6203
+ var includeItemSchema = z2.union([
6204
+ legacyIncludeItemSchema,
6205
+ correlatedReadByIdSchema,
6206
+ correlatedReadByFilterSchema,
6207
+ correlatedListSchema,
6208
+ correlatedCountSchema
6209
+ ]);
6210
+ var correlatedNestedIncludeSchema = z2.union([includeItemSchema, z2.array(includeItemSchema)]);
6211
+ var rejectDuplicateCorrelatedPaths = (items, ctx) => {
6212
+ const seen = /* @__PURE__ */ new Set();
6213
+ items.forEach((item, index) => {
6214
+ if (isPlainObject18(item) && item.mode === "correlated" && typeof item.path === "string") {
6215
+ if (seen.has(item.path)) {
6216
+ ctx.addIssue({
6217
+ code: z2.ZodIssueCode.custom,
6218
+ message: `Duplicate correlated include output path: ${item.path}`,
6219
+ path: [index, "path"]
6220
+ });
6221
+ } else {
6222
+ seen.add(item.path);
6223
+ }
6224
+ }
6225
+ });
6226
+ };
6227
+ var includeSchema = z2.union([
6228
+ includeItemSchema,
6229
+ z2.array(includeItemSchema).superRefine(rejectDuplicateCorrelatedPaths)
6230
+ ]);
4712
6231
  var fieldsSchema = z2.array(z2.string().min(1));
4713
6232
  var taskSchema = z2.object({
4714
6233
  type: z2.string().min(1),
@@ -4891,19 +6410,12 @@ function fromJoi(schema) {
4891
6410
  }
4892
6411
  function fromAjv(validator) {
4893
6412
  return async (value) => {
4894
- const outcome = validator(value);
4895
- if (isThenable(outcome)) {
6413
+ if (validator.$async === true) {
4896
6414
  try {
4897
- const data = await outcome;
4898
- if (data === false) {
4899
- return {
4900
- success: false,
4901
- issues: normalizeAjvIssues(validator.errors ?? [])
4902
- };
4903
- }
6415
+ const data = await validator(value);
4904
6416
  return {
4905
6417
  success: true,
4906
- data: data === true ? value : data
6418
+ data
4907
6419
  };
4908
6420
  } catch (error2) {
4909
6421
  if (!isAjvValidationError(error2)) {
@@ -4911,11 +6423,21 @@ function fromAjv(validator) {
4911
6423
  }
4912
6424
  return {
4913
6425
  success: false,
4914
- issues: normalizeAjvIssues(error2.errors ?? [])
6426
+ issues: normalizeAjvIssues(error2.errors)
4915
6427
  };
4916
6428
  }
4917
6429
  }
4918
- const errorsSnapshot = validator.errors ? [...validator.errors] : [];
6430
+ const outcome = validator(value);
6431
+ if (isThenable(outcome)) {
6432
+ void Promise.resolve(outcome).catch(() => {
6433
+ });
6434
+ throw new TypeError(
6435
+ "fromAjv: promise/thenable validators must declare $async: true and resolve with validated data"
6436
+ );
6437
+ }
6438
+ if (typeof outcome !== "boolean") {
6439
+ throw new TypeError("fromAjv: synchronous validators must return a boolean verdict");
6440
+ }
4919
6441
  if (outcome) {
4920
6442
  return {
4921
6443
  success: true,
@@ -4924,7 +6446,9 @@ function fromAjv(validator) {
4924
6446
  }
4925
6447
  return {
4926
6448
  success: false,
4927
- issues: normalizeAjvIssues(errorsSnapshot)
6449
+ // Normalize immediately, copying message/path before another call can
6450
+ // replace the errors array or mutate its entries.
6451
+ issues: normalizeAjvIssues(validator.errors ?? [])
4928
6452
  };
4929
6453
  };
4930
6454
  }
@@ -5154,14 +6678,14 @@ function isArkTypeErrors(value) {
5154
6678
  return record.arkKind === "errors" || record[" arkKind"] === "errors";
5155
6679
  }
5156
6680
  function isThenable(value) {
5157
- return typeof value === "object" && value !== null && "then" in value && typeof value.then === "function";
6681
+ return (typeof value === "object" || typeof value === "function") && value !== null && "then" in value && typeof value.then === "function";
5158
6682
  }
5159
6683
  function isAjvValidationError(error2) {
5160
6684
  if (typeof error2 !== "object" || error2 === null || !("errors" in error2)) {
5161
6685
  return false;
5162
6686
  }
5163
- const errors = error2.errors;
5164
- return errors == null || Array.isArray(errors);
6687
+ const candidate = error2;
6688
+ return candidate.ajv === true && candidate.validation === true && Array.isArray(candidate.errors);
5165
6689
  }
5166
6690
  function isVineValidationError(error2) {
5167
6691
  if (typeof error2 !== "object" || error2 === null || !("messages" in error2)) {
@@ -5245,7 +6769,7 @@ var listBodySchema = z3.object({
5245
6769
  }).passthrough().superRefine((body, ctx) => rejectKeys(body, ctx, ["query"]));
5246
6770
  var countBodySchema = z3.object({
5247
6771
  filter: objectOrArraySchema.optional()
5248
- }).passthrough().superRefine((body, ctx) => rejectKeys(body, ctx, ["query", "access", "options"]));
6772
+ }).passthrough().superRefine((body, ctx) => rejectKeys(body, ctx, ["query", "access", "options", "include"]));
5249
6773
  var readFilterBodySchema = z3.object({
5250
6774
  filter: objectOrArraySchema.optional(),
5251
6775
  select: projectionSchema.optional(),
@@ -6254,11 +7778,11 @@ function setModelSubDocumentRoutes(context) {
6254
7778
  }
6255
7779
 
6256
7780
  // src/openapi/route-registration.ts
6257
- import { normalizeUrlPath } from "@web-ts-toolkit/utils";
7781
+ import { normalizeUrlPath as normalizeUrlPath2 } from "@web-ts-toolkit/utils";
6258
7782
  function registerOpenApiRoute(runtime, basePath, defaultTag, route) {
6259
7783
  runtime.registerOpenApiRoute({
6260
7784
  ...route,
6261
- path: normalizeUrlPath(basePath + route.path),
7785
+ path: normalizeUrlPath2(basePath + route.path),
6262
7786
  tags: route.tags ?? [defaultTag]
6263
7787
  });
6264
7788
  }
@@ -6429,7 +7953,7 @@ var ModelRouter = class _ModelRouter {
6429
7953
  this.runtime = runtime;
6430
7954
  this.runtime.setModelOptions(modelName, initialOptions);
6431
7955
  this.options = this.runtime.getModelOptions(modelName);
6432
- this.fullBasePath = normalizeUrlPath2(this.options.parentPath + this.options.basePath);
7956
+ this.fullBasePath = normalizeUrlPath3(this.options.parentPath + this.options.basePath);
6433
7957
  this.modelName = modelName;
6434
7958
  this.router = new JsonRouter5(this.options.basePath, createSetCore(this.runtime), accessRouterResponseHandler);
6435
7959
  this.model = new model_default(modelName, this.runtime);
@@ -6510,7 +8034,7 @@ var ModelRouter = class _ModelRouter {
6510
8034
  logEndpoints() {
6511
8035
  runWithRuntime(this.runtime, () => {
6512
8036
  forEach8(this.router.getEndpoints(), ({ method, path }) => {
6513
- logInfoMessage(`${padEnd(method, 6)} ${normalizeUrlPath2(this.options.parentPath + path)}`);
8037
+ logInfoMessage(`${padEnd(method, 6)} ${normalizeUrlPath3(this.options.parentPath + path)}`);
6514
8038
  });
6515
8039
  });
6516
8040
  }
@@ -6523,7 +8047,7 @@ var ModelRouter = class _ModelRouter {
6523
8047
  value
6524
8048
  );
6525
8049
  }
6526
- if (arguments.length === 1 && isPlainObject16(keyOrOptions)) {
8050
+ if (arguments.length === 1 && isPlainObject19(keyOrOptions)) {
6527
8051
  assertMutableRouterOptions("model", keyOrOptions);
6528
8052
  this.runtime.setModelOptions(this.modelName, keyOrOptions);
6529
8053
  }
@@ -6546,7 +8070,7 @@ var ModelRouter = class _ModelRouter {
6546
8070
 
6547
8071
  // src/routers/root-router.ts
6548
8072
  import JsonRouter6 from "@web-ts-toolkit/express-json-router";
6549
- import { isNumber as _isNumber, normalizeUrlPath as normalizeUrlPath3, orderBy as _orderBy } from "@web-ts-toolkit/utils";
8073
+ import { isNumber as _isNumber, normalizeUrlPath as normalizeUrlPath4, orderBy as _orderBy } from "@web-ts-toolkit/utils";
6550
8074
 
6551
8075
  // src/core-data.ts
6552
8076
  var DataCore = class {
@@ -6812,7 +8336,7 @@ var RootRouter = class {
6812
8336
  this.setRoutes();
6813
8337
  }
6814
8338
  registerOpenApiRoute(route) {
6815
- registerOpenApiRoute(this.runtime, normalizeUrlPath3(this.basename), "root", route);
8339
+ registerOpenApiRoute(this.runtime, normalizeUrlPath4(this.basename), "root", route);
6816
8340
  }
6817
8341
  wrapResult(index, item, result) {
6818
8342
  return {
@@ -6962,7 +8486,7 @@ var RootRouter = class {
6962
8486
 
6963
8487
  // src/routers/data-router.ts
6964
8488
  import JsonRouter7 from "@web-ts-toolkit/express-json-router";
6965
- import { isPlainObject as isPlainObject17, isString as isString9, isUndefined as isUndefined3, normalizeUrlPath as normalizeUrlPath4 } from "@web-ts-toolkit/utils";
8489
+ import { isPlainObject as isPlainObject20, isString as isString9, isUndefined as isUndefined3, normalizeUrlPath as normalizeUrlPath5 } from "@web-ts-toolkit/utils";
6966
8490
  var clientErrors4 = JsonRouter7.clientErrors;
6967
8491
  function setOption2(parentKey, optionKey, option) {
6968
8492
  const key = isUndefined3(option) ? parentKey : `${parentKey}.${optionKey}`;
@@ -7007,7 +8531,7 @@ var DataRouter = class {
7007
8531
  this.runtime = runtime;
7008
8532
  this.runtime.setDataOptions(dataName, initialOptions);
7009
8533
  this.options = this.runtime.getDataOptions(dataName);
7010
- this.fullBasePath = normalizeUrlPath4(this.options.parentPath + this.options.basePath);
8534
+ this.fullBasePath = normalizeUrlPath5(this.options.parentPath + this.options.basePath);
7011
8535
  this.dataName = dataName;
7012
8536
  this.router = new JsonRouter7(this.options.basePath, createSetDataCore(this.runtime), accessRouterResponseHandler);
7013
8537
  this.setCollectionRoutes();
@@ -7163,7 +8687,7 @@ var DataRouter = class {
7163
8687
  assertMutableRouterOption("data", keyOrOptions);
7164
8688
  this.runtime.setDataOption(this.dataName, keyOrOptions, value);
7165
8689
  }
7166
- if (arguments.length === 1 && isPlainObject17(keyOrOptions)) {
8690
+ if (arguments.length === 1 && isPlainObject20(keyOrOptions)) {
7167
8691
  assertMutableRouterOptions("data", keyOrOptions);
7168
8692
  this.runtime.setDataOptions(this.dataName, keyOrOptions);
7169
8693
  }
@@ -7310,7 +8834,7 @@ function createRuntimeApi(runtime) {
7310
8834
  if (arguments.length === 2 && isString10(keyOrOptions)) {
7311
8835
  return runtime.setGlobalOption(keyOrOptions, value);
7312
8836
  }
7313
- if (arguments.length === 1 && isPlainObject18(keyOrOptions)) {
8837
+ if (arguments.length === 1 && isPlainObject21(keyOrOptions)) {
7314
8838
  return runtime.setGlobalOptions(keyOrOptions);
7315
8839
  }
7316
8840
  };