@web-ts-toolkit/access-router 0.44.0 → 0.45.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 isPlainObject20, 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 isPlainObject16, 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,
@@ -943,7 +943,9 @@ var defaultRequestComplexity = {
943
943
  maxIncludeCount: 10,
944
944
  maxSubQueryCount: 10,
945
945
  maxBulkConcurrency: 10,
946
- maxHookConcurrency: 10
946
+ maxHookConcurrency: 10,
947
+ maxCorrelatedQueries: 100,
948
+ maxCorrelatedDepth: 5
947
949
  };
948
950
  var normalizePositiveInteger = (value, fallback) => {
949
951
  return Number.isSafeInteger(value) && Number(value) > 0 ? Number(value) : fallback;
@@ -963,6 +965,14 @@ var resolveRequestComplexity = (options) => ({
963
965
  maxHookConcurrency: normalizePositiveInteger(
964
966
  options?.maxHookConcurrency,
965
967
  defaultRequestComplexity.maxHookConcurrency
968
+ ),
969
+ maxCorrelatedQueries: normalizePositiveInteger(
970
+ options?.maxCorrelatedQueries,
971
+ defaultRequestComplexity.maxCorrelatedQueries
972
+ ),
973
+ maxCorrelatedDepth: normalizePositiveInteger(
974
+ options?.maxCorrelatedDepth,
975
+ defaultRequestComplexity.maxCorrelatedDepth
966
976
  )
967
977
  });
968
978
  function validateRequestComplexity(value, options, scope = "request") {
@@ -1550,14 +1560,471 @@ var getModelRef = (modelName, refPath) => {
1550
1560
  // src/services/base.ts
1551
1561
  import {
1552
1562
  castArray,
1563
+ cloneDeep as cloneDeep2,
1553
1564
  compact,
1554
1565
  flatten,
1555
1566
  forEach as forEach3,
1556
1567
  get as get4,
1557
- isArray as isArray6,
1558
- isPlainObject as isPlainObject11,
1568
+ isArray as isArray7,
1569
+ isPlainObject as isPlainObject12,
1559
1570
  uniq
1560
1571
  } from "@web-ts-toolkit/utils";
1572
+
1573
+ // src/correlated-includes.ts
1574
+ import { cloneDeep, isArray as isArray6, isPlainObject as isPlainObject11 } from "@web-ts-toolkit/utils";
1575
+ var PARENT_MARKER_KEY = "$parent";
1576
+ var ESCAPE_WRAPPER_KEY = "$escape";
1577
+ var SUBQUERY_PAYLOAD_KEYS = /* @__PURE__ */ new Set(["$$sq", "$$date"]);
1578
+ var MARKER_FORBIDDEN_OPERATOR_KEYS = /* @__PURE__ */ new Set(["$text", "$where", "$comment"]);
1579
+ var LOGICAL_CLAUSE_KEYS = /* @__PURE__ */ new Set(["$and", "$or", "$nor"]);
1580
+ var DANGEROUS_PATH_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
1581
+ var FIELD_SEGMENT_PATTERN = /^[A-Za-z_][A-Za-z0-9_$]*$/;
1582
+ var ARRAY_INDEX_SEGMENT_PATTERN = /^(0|[1-9]\d*)$/;
1583
+ var ownKeys = (value) => Object.keys(value);
1584
+ function isParentRefMarker(value) {
1585
+ return isPlainObject11(value) && ownKeys(value).length === 1 && ownKeys(value)[0] === PARENT_MARKER_KEY;
1586
+ }
1587
+ function isEscapedParentRef(value) {
1588
+ return isPlainObject11(value) && ownKeys(value).length === 1 && ownKeys(value)[0] === ESCAPE_WRAPPER_KEY;
1589
+ }
1590
+ function hasMarkerKey(value) {
1591
+ return ownKeys(value).includes(PARENT_MARKER_KEY) || ownKeys(value).includes(ESCAPE_WRAPPER_KEY);
1592
+ }
1593
+ function isValidParentRefPath(path) {
1594
+ if (typeof path !== "string" || path.length === 0) return false;
1595
+ const segments = path.split(".");
1596
+ return segments.every(
1597
+ (segment) => (FIELD_SEGMENT_PATTERN.test(segment) || ARRAY_INDEX_SEGMENT_PATTERN.test(segment)) && !DANGEROUS_PATH_SEGMENTS.has(segment)
1598
+ );
1599
+ }
1600
+ function isValidCorrelatedOutputPath(path) {
1601
+ return typeof path === "string" && path.length > 0 && isValidFieldPath(path) && path !== "_id" && !path.startsWith("$");
1602
+ }
1603
+ function containsParentMarkerShape(value) {
1604
+ const seen = /* @__PURE__ */ new Set();
1605
+ const visit = (current) => {
1606
+ if (isParentRefMarker(current) || isEscapedParentRef(current)) return true;
1607
+ if (isArray6(current)) {
1608
+ if (seen.has(current)) return false;
1609
+ seen.add(current);
1610
+ return current.some(visit);
1611
+ }
1612
+ if (isPlainObject11(current)) {
1613
+ if (seen.has(current)) return false;
1614
+ seen.add(current);
1615
+ if (hasMarkerKey(current)) return true;
1616
+ return Object.values(current).some(visit);
1617
+ }
1618
+ return false;
1619
+ };
1620
+ return visit(value);
1621
+ }
1622
+ function scanTemplateValueNode(value, path, issues) {
1623
+ if (isEscapedParentRef(value)) {
1624
+ const inner = value.$escape;
1625
+ if (!isParentRefMarker(inner) || typeof inner.$parent !== "string" || (inner.$parent?.length ?? 0) === 0) {
1626
+ issues.push({
1627
+ message: 'Invalid $escape payload: expected { "$escape": { "$parent": "<field>" } }',
1628
+ path
1629
+ });
1630
+ }
1631
+ return;
1632
+ }
1633
+ if (isParentRefMarker(value)) {
1634
+ if (!isValidParentRefPath(value.$parent)) {
1635
+ issues.push({
1636
+ message: `Invalid $parent reference path at ${path.length === 0 ? "#" : path.join(".")}`,
1637
+ path
1638
+ });
1639
+ }
1640
+ return;
1641
+ }
1642
+ if (isPlainObject11(value)) {
1643
+ if (hasMarkerKey(value)) {
1644
+ issues.push({
1645
+ message: 'Malformed $parent reference: objects carrying $parent must be exactly { "$parent": "<field>" }',
1646
+ path
1647
+ });
1648
+ return;
1649
+ }
1650
+ scanTemplateObjectEntries(value, path, issues);
1651
+ return;
1652
+ }
1653
+ if (isArray6(value)) {
1654
+ value.forEach((item, index) => scanTemplateValueNode(item, [...path, index], issues));
1655
+ }
1656
+ }
1657
+ function scanTemplateObjectEntries(obj, basePath, issues) {
1658
+ for (const [key, child] of Object.entries(obj)) {
1659
+ const childPath = [...basePath, key];
1660
+ if (key === PARENT_MARKER_KEY) {
1661
+ issues.push({ message: "$parent is never a field name in correlated templates", path: childPath });
1662
+ continue;
1663
+ }
1664
+ if (SUBQUERY_PAYLOAD_KEYS.has(key)) {
1665
+ if (containsParentMarkerShape(child)) {
1666
+ issues.push({
1667
+ message: `Unsupported $parent reference inside ${key}: markers are not resolved in subquery payloads`,
1668
+ path: childPath
1669
+ });
1670
+ }
1671
+ continue;
1672
+ }
1673
+ if (MARKER_FORBIDDEN_OPERATOR_KEYS.has(key)) {
1674
+ if (containsParentMarkerShape(child)) {
1675
+ issues.push({
1676
+ message: `Unsupported $parent reference inside ${key}: markers are not resolved in ${key} values`,
1677
+ path: childPath
1678
+ });
1679
+ }
1680
+ continue;
1681
+ }
1682
+ if (key === "$elemMatch") {
1683
+ if (isParentRefMarker(child) || isEscapedParentRef(child)) {
1684
+ issues.push({
1685
+ message: "Unsupported $parent reference as a direct $elemMatch value",
1686
+ path: childPath
1687
+ });
1688
+ continue;
1689
+ }
1690
+ scanTemplateValueNode(child, childPath, issues);
1691
+ continue;
1692
+ }
1693
+ if (LOGICAL_CLAUSE_KEYS.has(key)) {
1694
+ if (!isArray6(child)) {
1695
+ scanTemplateValueNode(child, childPath, issues);
1696
+ continue;
1697
+ }
1698
+ child.forEach((clause, index) => {
1699
+ const clausePath = [...childPath, index];
1700
+ if (isParentRefMarker(clause) || isEscapedParentRef(clause)) {
1701
+ issues.push({
1702
+ message: `Unsupported $parent reference as a direct ${key} clause: clauses must be filter objects`,
1703
+ path: clausePath
1704
+ });
1705
+ return;
1706
+ }
1707
+ if (isPlainObject11(clause)) {
1708
+ scanTemplateObjectEntries(clause, clausePath, issues);
1709
+ }
1710
+ });
1711
+ continue;
1712
+ }
1713
+ scanTemplateValueNode(child, childPath, issues);
1714
+ }
1715
+ }
1716
+ function validateCorrelatedFilterTemplate(filter2) {
1717
+ const issues = [];
1718
+ if (!isPlainObject11(filter2)) {
1719
+ issues.push({ message: "Correlated include filter must be an object", path: [] });
1720
+ return issues;
1721
+ }
1722
+ if (isParentRefMarker(filter2) || isEscapedParentRef(filter2)) {
1723
+ issues.push({ message: "Correlated include filter must not itself be a $parent reference", path: [] });
1724
+ return issues;
1725
+ }
1726
+ scanTemplateObjectEntries(filter2, [], issues);
1727
+ return issues;
1728
+ }
1729
+ function validateCorrelatedIdTemplate(id) {
1730
+ if (typeof id === "string") {
1731
+ return id.length > 0 ? [] : [{ message: "Correlated include id must be a non-empty string or $parent reference", path: [] }];
1732
+ }
1733
+ if (isEscapedParentRef(id)) {
1734
+ return [{ message: "Unsupported $escape as a correlated include id", path: [] }];
1735
+ }
1736
+ if (isParentRefMarker(id)) {
1737
+ return isValidParentRefPath(id.$parent) ? [] : [{ message: "Invalid $parent reference path in correlated include id", path: [] }];
1738
+ }
1739
+ return [{ message: "Correlated include id must be a string or $parent reference", path: [] }];
1740
+ }
1741
+ function findArgsMarkerIssue(value, where) {
1742
+ if (value !== void 0 && containsParentMarkerShape(value)) {
1743
+ return {
1744
+ message: `Unsupported $parent reference in ${where}: references are only supported in filter and id positions`,
1745
+ path: []
1746
+ };
1747
+ }
1748
+ return null;
1749
+ }
1750
+ function collectFilterReferencePaths(filter2) {
1751
+ const paths = [];
1752
+ const seen = /* @__PURE__ */ new Set();
1753
+ const visitValue = (value) => {
1754
+ if (isParentRefMarker(value)) {
1755
+ if (typeof value.$parent === "string") {
1756
+ paths.push(value.$parent);
1757
+ }
1758
+ return;
1759
+ }
1760
+ if (isEscapedParentRef(value)) return;
1761
+ if (isArray6(value)) {
1762
+ if (seen.has(value)) return;
1763
+ seen.add(value);
1764
+ value.forEach(visitValue);
1765
+ return;
1766
+ }
1767
+ if (isPlainObject11(value)) {
1768
+ if (seen.has(value)) return;
1769
+ seen.add(value);
1770
+ for (const [key, child] of Object.entries(value)) {
1771
+ if (SUBQUERY_PAYLOAD_KEYS.has(key) || MARKER_FORBIDDEN_OPERATOR_KEYS.has(key)) continue;
1772
+ visitValue(child);
1773
+ }
1774
+ }
1775
+ };
1776
+ visitValue(filter2);
1777
+ return paths;
1778
+ }
1779
+ function collectCorrelatedReferencePaths(include) {
1780
+ const paths = [];
1781
+ if (isParentRefMarker(include.id) && typeof include.id !== "string") {
1782
+ const ref = include.id;
1783
+ if (typeof ref.$parent === "string") paths.push(ref.$parent);
1784
+ } else if (typeof include.id === "string") {
1785
+ }
1786
+ const filter2 = include.filter;
1787
+ if (filter2 !== void 0) {
1788
+ paths.push(...collectFilterReferencePaths(filter2));
1789
+ }
1790
+ return [...new Set(paths)];
1791
+ }
1792
+ var CorrelatedReferenceError = class extends Error {
1793
+ constructor(message) {
1794
+ super(message);
1795
+ this.name = "CorrelatedReferenceError";
1796
+ }
1797
+ };
1798
+ var UNRESOLVABLE = /* @__PURE__ */ Symbol("correlated-include-unresolvable");
1799
+ function getOwnPathValue(parent, path) {
1800
+ const segments = path.split(".");
1801
+ let current = parent;
1802
+ for (const segment of segments) {
1803
+ if (DANGEROUS_PATH_SEGMENTS.has(segment)) {
1804
+ throw new CorrelatedReferenceError(`Invalid $parent reference path: ${path}`);
1805
+ }
1806
+ if (typeof current !== "object" || current === null) return void 0;
1807
+ if (!Object.prototype.hasOwnProperty.call(current, segment)) return void 0;
1808
+ current = current[segment];
1809
+ }
1810
+ return current;
1811
+ }
1812
+ function cloneSubstitutedValue(value) {
1813
+ if (isPlainObject11(value) || isArray6(value)) return cloneDeep(value);
1814
+ return value;
1815
+ }
1816
+ function literalParentRefObject(path) {
1817
+ const literal = {};
1818
+ Object.defineProperty(literal, PARENT_MARKER_KEY, {
1819
+ value: path,
1820
+ writable: true,
1821
+ enumerable: true,
1822
+ configurable: true
1823
+ });
1824
+ return literal;
1825
+ }
1826
+ function resolveObjectEntries(obj, parent) {
1827
+ const out = {};
1828
+ for (const [key, child] of Object.entries(obj)) {
1829
+ if (key === PARENT_MARKER_KEY) {
1830
+ throw new CorrelatedReferenceError("$parent is never a field name in correlated templates");
1831
+ }
1832
+ if (SUBQUERY_PAYLOAD_KEYS.has(key)) {
1833
+ if (containsParentMarkerShape(child)) {
1834
+ throw new CorrelatedReferenceError(`Unsupported $parent reference inside ${key}`);
1835
+ }
1836
+ out[key] = cloneSubstitutedValue(child);
1837
+ continue;
1838
+ }
1839
+ if (MARKER_FORBIDDEN_OPERATOR_KEYS.has(key)) {
1840
+ if (containsParentMarkerShape(child)) {
1841
+ throw new CorrelatedReferenceError(`Unsupported $parent reference inside ${key}`);
1842
+ }
1843
+ out[key] = cloneSubstitutedValue(child);
1844
+ continue;
1845
+ }
1846
+ if (key === "$elemMatch") {
1847
+ if (isParentRefMarker(child) || isEscapedParentRef(child)) {
1848
+ throw new CorrelatedReferenceError("Unsupported $parent reference as a direct $elemMatch value");
1849
+ }
1850
+ out[key] = resolveValueNode(child, parent, false);
1851
+ continue;
1852
+ }
1853
+ if (LOGICAL_CLAUSE_KEYS.has(key)) {
1854
+ if (!isArray6(child)) {
1855
+ out[key] = resolveValueNode(child, parent, false);
1856
+ continue;
1857
+ }
1858
+ out[key] = child.map((clause) => {
1859
+ if (isParentRefMarker(clause) || isEscapedParentRef(clause)) {
1860
+ throw new CorrelatedReferenceError(`Unsupported $parent reference as a direct ${key} clause`);
1861
+ }
1862
+ if (!isPlainObject11(clause)) return cloneSubstitutedValue(clause);
1863
+ return resolveObjectEntries(clause, parent);
1864
+ });
1865
+ continue;
1866
+ }
1867
+ if (key.startsWith("$")) {
1868
+ out[key] = resolveValueNode(child, parent, false);
1869
+ continue;
1870
+ }
1871
+ out[key] = resolveValueNode(child, parent, true);
1872
+ }
1873
+ return out;
1874
+ }
1875
+ function resolveValueNode(template, parent, bare) {
1876
+ if (isEscapedParentRef(template)) {
1877
+ const inner = template.$escape;
1878
+ if (!isParentRefMarker(inner) || typeof inner.$parent !== "string" || (inner.$parent?.length ?? 0) === 0) {
1879
+ throw new CorrelatedReferenceError('Invalid $escape payload: expected { "$escape": { "$parent": "<field>" } }');
1880
+ }
1881
+ return literalParentRefObject(inner.$parent);
1882
+ }
1883
+ if (isParentRefMarker(template)) {
1884
+ const path = template.$parent;
1885
+ if (!isValidParentRefPath(path)) {
1886
+ throw new CorrelatedReferenceError(`Invalid $parent reference path: ${String(path)}`);
1887
+ }
1888
+ const value = getOwnPathValue(parent, path);
1889
+ if (value === void 0 || value === null) throw UNRESOLVABLE;
1890
+ if (bare && isPlainObject11(value)) return { $eq: cloneDeep(value) };
1891
+ return cloneSubstitutedValue(value);
1892
+ }
1893
+ if (isPlainObject11(template)) {
1894
+ if (hasMarkerKey(template)) {
1895
+ throw new CorrelatedReferenceError(
1896
+ 'Malformed $parent reference: objects carrying $parent must be exactly { "$parent": "<field>" }'
1897
+ );
1898
+ }
1899
+ return resolveObjectEntries(template, parent);
1900
+ }
1901
+ if (isArray6(template)) {
1902
+ return template.map((item) => resolveValueNode(item, parent, false));
1903
+ }
1904
+ return template;
1905
+ }
1906
+ function isUnresolvableSignal(error2) {
1907
+ return error2 === UNRESOLVABLE;
1908
+ }
1909
+ function resolveCorrelatedIdTemplate(id, parent) {
1910
+ try {
1911
+ if (typeof id === "string") {
1912
+ if (id.length === 0) throw new CorrelatedReferenceError("Correlated include id must be a non-empty string");
1913
+ return { status: "resolved", id };
1914
+ }
1915
+ if (isParentRefMarker(id)) {
1916
+ const path = id.$parent;
1917
+ if (!isValidParentRefPath(path)) {
1918
+ throw new CorrelatedReferenceError(`Invalid $parent reference path: ${String(path)}`);
1919
+ }
1920
+ const value = getOwnPathValue(parent, path);
1921
+ if (typeof value === "string") return { status: "resolved", id: value };
1922
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
1923
+ return { status: "resolved", id: String(value) };
1924
+ }
1925
+ throw UNRESOLVABLE;
1926
+ }
1927
+ throw new CorrelatedReferenceError("Correlated include id must be a string or $parent reference");
1928
+ } catch (error2) {
1929
+ if (isUnresolvableSignal(error2)) return { status: "unresolvable" };
1930
+ throw error2;
1931
+ }
1932
+ }
1933
+ function resolveCorrelatedFilterTemplate(filter2, parent) {
1934
+ try {
1935
+ if (!isPlainObject11(filter2) || isParentRefMarker(filter2) || isEscapedParentRef(filter2)) {
1936
+ throw new CorrelatedReferenceError("Correlated include filter must be an object");
1937
+ }
1938
+ return { status: "resolved", filter: resolveObjectEntries(filter2, parent) };
1939
+ } catch (error2) {
1940
+ if (isUnresolvableSignal(error2)) return { status: "unresolvable" };
1941
+ throw error2;
1942
+ }
1943
+ }
1944
+ function validateExpandedCorrelatedOperands(expanded, limits) {
1945
+ return validateRequestComplexity(expanded, limits ?? null, "filter");
1946
+ }
1947
+ var MAX_NESTED_INCLUDE_DEPTH = 10;
1948
+ function validateCorrelatedArgsShape(args, op, depth) {
1949
+ if (args === void 0) return [];
1950
+ if (!isPlainObject11(args)) return ["Correlated include args must be an object"];
1951
+ const record = args;
1952
+ const allowed = op === "list" ? /* @__PURE__ */ new Set(["select", "sort", "skip", "limit", "page", "pageSize", "include"]) : /* @__PURE__ */ new Set(["select", "sort", "include"]);
1953
+ for (const key of Object.keys(record)) {
1954
+ if (!allowed.has(key)) return [`Unsupported correlated include arg: ${key}`];
1955
+ }
1956
+ if (op === "count" && Object.keys(record).length > 0) return ["Count includes accept no args"];
1957
+ const selectIssue = findArgsMarkerIssue(record.select, "args.select");
1958
+ if (selectIssue) return [selectIssue.message];
1959
+ const sortIssue = findArgsMarkerIssue(record.sort, "args.sort");
1960
+ if (sortIssue) return [sortIssue.message];
1961
+ if (record.include !== void 0) {
1962
+ if (depth >= MAX_NESTED_INCLUDE_DEPTH) return ["Correlated include nesting exceeds the supported depth"];
1963
+ const nested = Array.isArray(record.include) ? record.include : [record.include];
1964
+ for (const entry of nested) {
1965
+ const errors = validateCorrelatedIncludeShape(entry, depth + 1);
1966
+ if (errors.length > 0) return errors;
1967
+ }
1968
+ }
1969
+ return [];
1970
+ }
1971
+ function validateCorrelatedIncludeShape(entry, depth = 0) {
1972
+ if (!isPlainObject11(entry)) return ["Correlated include must be an object"];
1973
+ const record = entry;
1974
+ if (record.mode !== "correlated") return ['Correlated include mode must be "correlated"'];
1975
+ if (typeof record.model !== "string" || record.model.length === 0)
1976
+ return ["Correlated include model must be a non-empty string"];
1977
+ if (record.op !== "list" && record.op !== "read" && record.op !== "count") {
1978
+ return ['Correlated include op must be one of "list", "read", or "count"'];
1979
+ }
1980
+ if (!isValidCorrelatedOutputPath(record.path))
1981
+ return [`Invalid correlated include output path: ${String(record.path)}`];
1982
+ if ("localField" in record || "foreignField" in record) {
1983
+ return ["Correlated includes must not carry localField or foreignField"];
1984
+ }
1985
+ if (record.options !== void 0 && !(isPlainObject11(record.options) && Object.keys(record.options).length === 0)) {
1986
+ return ["Correlated include options must be absent or empty"];
1987
+ }
1988
+ const hasId = record.id !== void 0;
1989
+ const hasFilter = record.filter !== void 0;
1990
+ if (record.op === "read") {
1991
+ if (hasId === hasFilter) return ['Correlated read includes require exactly one of "id" or "filter"'];
1992
+ if (hasId) {
1993
+ const issues = validateCorrelatedIdTemplate(record.id);
1994
+ if (issues.length > 0) return [issues[0].message];
1995
+ } else {
1996
+ const issues = validateCorrelatedFilterTemplate(record.filter);
1997
+ if (issues.length > 0) return [issues[0].message];
1998
+ }
1999
+ } else {
2000
+ if (!hasFilter || hasId) return [`Correlated ${record.op} includes require a filter and no id`];
2001
+ const issues = validateCorrelatedFilterTemplate(record.filter);
2002
+ if (issues.length > 0) return [issues[0].message];
2003
+ }
2004
+ return validateCorrelatedArgsShape(record.args, record.op, depth);
2005
+ }
2006
+ function isCorrelatedInclude(value) {
2007
+ return isPlainObject11(value) && value.mode === "correlated";
2008
+ }
2009
+
2010
+ // src/services/base.ts
2011
+ var correlatedExecStates = /* @__PURE__ */ new WeakMap();
2012
+ function getCorrelatedTemplateDepth(includes) {
2013
+ let max = 0;
2014
+ const visit = (entries, depth) => {
2015
+ const list = castArray(entries).filter(Boolean);
2016
+ for (const entry of list) {
2017
+ if (!isPlainObject12(entry)) continue;
2018
+ max = Math.max(max, depth);
2019
+ const nested = entry.args?.include;
2020
+ if (nested !== void 0) {
2021
+ visit(nested, depth + 1);
2022
+ }
2023
+ }
2024
+ };
2025
+ if (includes.length > 0) visit(includes, 1);
2026
+ return max;
2027
+ }
1561
2028
  var ClientRequestError = class extends Error {
1562
2029
  constructor(result) {
1563
2030
  super(String(result.code));
@@ -1572,11 +2039,11 @@ function validateClientFilter(filter2) {
1572
2039
  }
1573
2040
  const blockedOperators = /* @__PURE__ */ new Set(["$where", "$expr", "$function", "$accumulator"]);
1574
2041
  const visit = (value, path) => {
1575
- if (isArray6(value)) {
2042
+ if (isArray7(value)) {
1576
2043
  value.forEach((item, index) => visit(item, `${path}[${index}]`));
1577
2044
  return;
1578
2045
  }
1579
- if (!isPlainObject11(value)) return;
2046
+ if (!isPlainObject12(value)) return;
1580
2047
  Object.entries(value).forEach(([key, child]) => {
1581
2048
  const nextPath = path ? `${path}.${key}` : key;
1582
2049
  if (blockedOperators.has(key)) {
@@ -1696,21 +2163,52 @@ var Base = class {
1696
2163
  return this.req.macl.getPublicService(modelName);
1697
2164
  }
1698
2165
  processInclude(include) {
1699
- const includes = compact(castArray(include)).filter(({ model, op, path, localField, foreignField }) => {
1700
- return model && op && path && localField && foreignField;
1701
- });
2166
+ const items = compact(castArray(include));
2167
+ const legacy = [];
2168
+ const correlated = [];
2169
+ const correlatedReferenceFields = [];
2170
+ for (const entry of items) {
2171
+ if (isCorrelatedInclude(entry)) {
2172
+ const shapeErrors = validateCorrelatedIncludeShape(entry);
2173
+ if (shapeErrors.length > 0) {
2174
+ this.throwClientRequestError("bad_request" /* BadRequest */, shapeErrors[0]);
2175
+ }
2176
+ correlated.push(entry);
2177
+ correlatedReferenceFields.push(...collectCorrelatedReferencePaths(entry));
2178
+ continue;
2179
+ }
2180
+ if (containsParentMarkerShape(entry)) {
2181
+ this.throwClientRequestError(
2182
+ "bad_request" /* BadRequest */,
2183
+ 'Legacy includes must not contain $parent markers; use a correlated include (mode: "correlated")'
2184
+ );
2185
+ }
2186
+ const candidate = entry;
2187
+ if (candidate.model && candidate.op && candidate.path && candidate.localField && candidate.foreignField) {
2188
+ legacy.push(entry);
2189
+ }
2190
+ }
2191
+ const seenCorrelatedPaths = /* @__PURE__ */ new Set();
2192
+ for (const entry of correlated) {
2193
+ if (seenCorrelatedPaths.has(entry.path)) {
2194
+ this.throwClientRequestError("bad_request" /* BadRequest */, `Duplicate correlated include output path: ${entry.path}`);
2195
+ }
2196
+ seenCorrelatedPaths.add(entry.path);
2197
+ }
1702
2198
  let includeLocalFields = [];
1703
2199
  let includePaths = [];
1704
- forEach3(includes, (inc) => {
2200
+ forEach3(legacy, (inc) => {
1705
2201
  includeLocalFields.push(inc.localField);
1706
2202
  includePaths.push(inc.path);
1707
2203
  });
1708
2204
  includeLocalFields = uniq(compact(includeLocalFields));
1709
2205
  includePaths = uniq(compact(includePaths));
1710
2206
  return {
1711
- includes,
2207
+ includes: legacy,
2208
+ correlatedIncludes: correlated,
1712
2209
  includeLocalFields,
1713
- includePaths
2210
+ includePaths,
2211
+ correlatedReferenceFields: uniq(compact(correlatedReferenceFields))
1714
2212
  };
1715
2213
  }
1716
2214
  getForeignKeyValues(value) {
@@ -1753,7 +2251,7 @@ var Base = class {
1753
2251
  return ids.size;
1754
2252
  }
1755
2253
  sanitizeIncludeArgs(args) {
1756
- if (!isPlainObject11(args)) return {};
2254
+ if (!isPlainObject12(args)) return {};
1757
2255
  const { overrides: _clientOverrides, ...trustedArgs } = args;
1758
2256
  return trustedArgs;
1759
2257
  }
@@ -1766,10 +2264,16 @@ var Base = class {
1766
2264
  if (!include) return docs;
1767
2265
  const includes = compact(castArray(include));
1768
2266
  if (includes.length === 0) return docs;
1769
- const isSingle = !isArray6(docs);
2267
+ const isSingle = !isArray7(docs);
1770
2268
  let docList = isSingle ? [docs] : docs;
1771
2269
  for (let x = 0; x < includes.length; x++) {
1772
2270
  const include2 = includes[x];
2271
+ if (isCorrelatedInclude(include2)) {
2272
+ this.throwClientRequestError(
2273
+ "bad_request" /* BadRequest */,
2274
+ "Correlated includes are not supported by the legacy include executor"
2275
+ );
2276
+ }
1773
2277
  this.assertIncludeForeignField(include2);
1774
2278
  switch (include2.op) {
1775
2279
  case "count":
@@ -1848,6 +2352,356 @@ var Base = class {
1848
2352
  }
1849
2353
  return docs;
1850
2354
  }
2355
+ getCorrelatedExecState() {
2356
+ const key = this.req;
2357
+ let state = correlatedExecStates.get(key);
2358
+ if (!state) {
2359
+ state = {
2360
+ totalQueries: 0,
2361
+ scheduler: new RequestConcurrencyScheduler(this.getRequestComplexity().maxBulkConcurrency)
2362
+ };
2363
+ correlatedExecStates.set(key, state);
2364
+ }
2365
+ return state;
2366
+ }
2367
+ claimCorrelatedQuerySlot() {
2368
+ const limits = this.getRequestComplexity();
2369
+ const state = this.getCorrelatedExecState();
2370
+ if (state.totalQueries >= limits.maxCorrelatedQueries) {
2371
+ this.throwClientRequestError(
2372
+ "bad_request" /* BadRequest */,
2373
+ `Correlated include query budget exceeded (max ${limits.maxCorrelatedQueries})`
2374
+ );
2375
+ }
2376
+ state.totalQueries += 1;
2377
+ }
2378
+ sanitizeCorrelatedArgs(include) {
2379
+ const args = include.args ?? {};
2380
+ if (include.op === "list") {
2381
+ const { select: select2, sort: sort2, skip, limit, page, pageSize, include: nestedInclude2 } = args;
2382
+ return { select: select2, sort: sort2, skip, limit, page, pageSize, nestedInclude: nestedInclude2 };
2383
+ }
2384
+ const { select, sort, include: nestedInclude } = args;
2385
+ return { select, sort, nestedInclude };
2386
+ }
2387
+ async includeCorrelatedDocs(docs, correlatedIncludes, snapshots, depth = 0) {
2388
+ if (correlatedIncludes.length === 0) return docs;
2389
+ const limits = this.getRequestComplexity();
2390
+ const templateDepth = getCorrelatedTemplateDepth(correlatedIncludes);
2391
+ if (depth + templateDepth > limits.maxCorrelatedDepth) {
2392
+ this.throwClientRequestError(
2393
+ "bad_request" /* BadRequest */,
2394
+ `Correlated include depth exceeded (max ${limits.maxCorrelatedDepth})`
2395
+ );
2396
+ }
2397
+ const state = this.getCorrelatedExecState();
2398
+ const resolvedSnapshots = snapshots ?? docs.map((doc) => cloneDeep2(toObject(doc)));
2399
+ for (const include of correlatedIncludes) {
2400
+ switch (include.op) {
2401
+ case "read":
2402
+ await this.includeCorrelatedRead(docs, include, resolvedSnapshots, state, depth);
2403
+ break;
2404
+ case "list":
2405
+ await this.includeCorrelatedList(docs, include, resolvedSnapshots, state, depth);
2406
+ break;
2407
+ case "count":
2408
+ await this.includeCorrelatedCount(docs, include, resolvedSnapshots, state, depth);
2409
+ break;
2410
+ default:
2411
+ this.throwClientRequestError(
2412
+ "bad_request" /* BadRequest */,
2413
+ `Unsupported correlated include op: ${include.op}`
2414
+ );
2415
+ }
2416
+ }
2417
+ return docs;
2418
+ }
2419
+ async includeCorrelatedRead(docs, include, snapshots, state, depth) {
2420
+ const svc = await this.getAuthorizedTargetService(include.model, "read");
2421
+ const { select, sort, nestedInclude } = this.sanitizeCorrelatedArgs(include);
2422
+ const hasId = include.id !== void 0;
2423
+ if (hasId) {
2424
+ const idTemplate = include.id;
2425
+ const tasks2 = docs.map((_, index) => ({ index, snapshot: snapshots[index] }));
2426
+ await state.scheduler.map(tasks2, async ({ index, snapshot }) => {
2427
+ let resolution;
2428
+ try {
2429
+ resolution = resolveCorrelatedIdTemplate(idTemplate, snapshot);
2430
+ } catch (error2) {
2431
+ if (error2 instanceof CorrelatedReferenceError) {
2432
+ throw new ClientRequestError({
2433
+ success: false,
2434
+ kind: "error",
2435
+ code: "bad_request" /* BadRequest */,
2436
+ errors: [{ detail: error2.message }]
2437
+ });
2438
+ }
2439
+ throw error2;
2440
+ }
2441
+ if (resolution.status === "unresolvable") {
2442
+ setDocValue(docs[index], include.path, null);
2443
+ return;
2444
+ }
2445
+ const resolvedId = resolution.id;
2446
+ let idFilter;
2447
+ try {
2448
+ idFilter = await svc.genIDFilter(resolvedId);
2449
+ } catch (error2) {
2450
+ const clientResult = this.getClientRequestErrorResult(error2);
2451
+ if (clientResult) throw new ClientRequestError(clientResult);
2452
+ throw error2;
2453
+ }
2454
+ const authorized = await svc.genFilter("read", idFilter);
2455
+ if (authorized === false) {
2456
+ throw new ClientRequestError({ success: false, kind: "error", code: "forbidden" /* Forbidden */ });
2457
+ }
2458
+ this.claimCorrelatedQuerySlot();
2459
+ let result;
2460
+ try {
2461
+ result = await svc.findOne(
2462
+ {},
2463
+ {
2464
+ select,
2465
+ sort,
2466
+ include: nestedInclude,
2467
+ overrides: { filter: authorized }
2468
+ },
2469
+ { access: "read", lean: true, includePermissions: false }
2470
+ );
2471
+ } catch (error2) {
2472
+ const clientResult = this.getClientRequestErrorResult(error2);
2473
+ if (clientResult) throw new ClientRequestError(clientResult);
2474
+ throw error2;
2475
+ }
2476
+ if (result.success) {
2477
+ setDocValue(docs[index], include.path, result.data);
2478
+ } else if (result.code === "not_found" /* NotFound */) {
2479
+ setDocValue(docs[index], include.path, null);
2480
+ } else {
2481
+ throw new ClientRequestError(result);
2482
+ }
2483
+ });
2484
+ return;
2485
+ }
2486
+ const filterTemplate = include.filter;
2487
+ const templateErrors = this.validateClientFilter(filterTemplate);
2488
+ if (templateErrors.length > 0) {
2489
+ this.throwClientRequestError("bad_request" /* BadRequest */, templateErrors[0]);
2490
+ }
2491
+ let parsedTemplate;
2492
+ try {
2493
+ parsedTemplate = await this.parseClientData(
2494
+ filterTemplate,
2495
+ state.scheduler
2496
+ );
2497
+ } catch (error2) {
2498
+ const clientResult = this.getClientRequestErrorResult(error2);
2499
+ if (clientResult) throw new ClientRequestError(clientResult);
2500
+ throw error2;
2501
+ }
2502
+ const tasks = docs.map((_, index) => ({ index, snapshot: snapshots[index] }));
2503
+ await state.scheduler.map(tasks, async ({ index, snapshot }) => {
2504
+ let resolution;
2505
+ try {
2506
+ resolution = resolveCorrelatedFilterTemplate(parsedTemplate, snapshot);
2507
+ } catch (error2) {
2508
+ if (error2 instanceof CorrelatedReferenceError) {
2509
+ throw new ClientRequestError({
2510
+ success: false,
2511
+ kind: "error",
2512
+ code: "bad_request" /* BadRequest */,
2513
+ errors: [{ detail: error2.message }]
2514
+ });
2515
+ }
2516
+ throw error2;
2517
+ }
2518
+ if (resolution.status === "unresolvable") {
2519
+ setDocValue(docs[index], include.path, null);
2520
+ return;
2521
+ }
2522
+ const expanded = resolution.filter;
2523
+ const operandErrors = validateExpandedCorrelatedOperands(expanded, this.getRequestComplexity());
2524
+ if (operandErrors.length > 0) {
2525
+ throw new ClientRequestError({
2526
+ success: false,
2527
+ kind: "error",
2528
+ code: "bad_request" /* BadRequest */,
2529
+ errors: [{ detail: operandErrors[0].detail }]
2530
+ });
2531
+ }
2532
+ const authorized = await svc.genFilter("read", expanded);
2533
+ if (authorized === false) {
2534
+ throw new ClientRequestError({ success: false, kind: "error", code: "forbidden" /* Forbidden */ });
2535
+ }
2536
+ this.claimCorrelatedQuerySlot();
2537
+ let result;
2538
+ try {
2539
+ result = await svc.findOne(
2540
+ {},
2541
+ {
2542
+ select,
2543
+ sort,
2544
+ include: nestedInclude,
2545
+ overrides: { filter: authorized }
2546
+ },
2547
+ { access: "read", lean: true, includePermissions: false }
2548
+ );
2549
+ } catch (error2) {
2550
+ const clientResult = this.getClientRequestErrorResult(error2);
2551
+ if (clientResult) throw new ClientRequestError(clientResult);
2552
+ throw error2;
2553
+ }
2554
+ if (result.success) {
2555
+ setDocValue(docs[index], include.path, result.data);
2556
+ } else if (result.code === "not_found" /* NotFound */) {
2557
+ setDocValue(docs[index], include.path, null);
2558
+ } else {
2559
+ throw new ClientRequestError(result);
2560
+ }
2561
+ });
2562
+ void depth;
2563
+ }
2564
+ async includeCorrelatedList(docs, include, snapshots, state, depth) {
2565
+ const svc = await this.getAuthorizedTargetService(include.model, "list");
2566
+ const { select, sort, skip, limit, page, pageSize, nestedInclude } = this.sanitizeCorrelatedArgs(include);
2567
+ const filterTemplate = include.filter;
2568
+ const templateErrors = this.validateClientFilter(filterTemplate);
2569
+ if (templateErrors.length > 0) {
2570
+ this.throwClientRequestError("bad_request" /* BadRequest */, templateErrors[0]);
2571
+ }
2572
+ let parsedTemplate;
2573
+ try {
2574
+ parsedTemplate = await this.parseClientData(
2575
+ filterTemplate,
2576
+ state.scheduler
2577
+ );
2578
+ } catch (error2) {
2579
+ const clientResult = this.getClientRequestErrorResult(error2);
2580
+ if (clientResult) throw new ClientRequestError(clientResult);
2581
+ throw error2;
2582
+ }
2583
+ const tasks = docs.map((_, index) => ({ index, snapshot: snapshots[index] }));
2584
+ await state.scheduler.map(tasks, async ({ index, snapshot }) => {
2585
+ let resolution;
2586
+ try {
2587
+ resolution = resolveCorrelatedFilterTemplate(parsedTemplate, snapshot);
2588
+ } catch (error2) {
2589
+ if (error2 instanceof CorrelatedReferenceError) {
2590
+ throw new ClientRequestError({
2591
+ success: false,
2592
+ kind: "error",
2593
+ code: "bad_request" /* BadRequest */,
2594
+ errors: [{ detail: error2.message }]
2595
+ });
2596
+ }
2597
+ throw error2;
2598
+ }
2599
+ if (resolution.status === "unresolvable") {
2600
+ setDocValue(docs[index], include.path, []);
2601
+ return;
2602
+ }
2603
+ const expanded = resolution.filter;
2604
+ const operandErrors = validateExpandedCorrelatedOperands(expanded, this.getRequestComplexity());
2605
+ if (operandErrors.length > 0) {
2606
+ throw new ClientRequestError({
2607
+ success: false,
2608
+ kind: "error",
2609
+ code: "bad_request" /* BadRequest */,
2610
+ errors: [{ detail: operandErrors[0].detail }]
2611
+ });
2612
+ }
2613
+ const authorized = await svc.genFilter("list", expanded);
2614
+ if (authorized === false) {
2615
+ throw new ClientRequestError({ success: false, kind: "error", code: "forbidden" /* Forbidden */ });
2616
+ }
2617
+ this.claimCorrelatedQuerySlot();
2618
+ let result;
2619
+ try {
2620
+ result = await svc.find(
2621
+ {},
2622
+ {
2623
+ select,
2624
+ sort,
2625
+ skip,
2626
+ limit,
2627
+ page,
2628
+ pageSize,
2629
+ include: nestedInclude,
2630
+ overrides: { filter: authorized }
2631
+ },
2632
+ { includeCount: false, lean: true, includePermissions: false }
2633
+ );
2634
+ } catch (error2) {
2635
+ const clientResult = this.getClientRequestErrorResult(error2);
2636
+ if (clientResult) throw new ClientRequestError(clientResult);
2637
+ throw error2;
2638
+ }
2639
+ if (result.success) {
2640
+ setDocValue(docs[index], include.path, result.data);
2641
+ } else {
2642
+ throw new ClientRequestError(result);
2643
+ }
2644
+ });
2645
+ void depth;
2646
+ }
2647
+ async includeCorrelatedCount(docs, include, snapshots, state, depth) {
2648
+ const svc = await this.getAuthorizedTargetService(include.model, "count");
2649
+ const filterTemplate = include.filter;
2650
+ const templateErrors = this.validateClientFilter(filterTemplate);
2651
+ if (templateErrors.length > 0) {
2652
+ this.throwClientRequestError("bad_request" /* BadRequest */, templateErrors[0]);
2653
+ }
2654
+ const tasks = docs.map((_, index) => ({ index, snapshot: snapshots[index] }));
2655
+ await state.scheduler.map(tasks, async ({ index, snapshot }) => {
2656
+ let resolution;
2657
+ try {
2658
+ resolution = resolveCorrelatedFilterTemplate(filterTemplate, snapshot);
2659
+ } catch (error2) {
2660
+ if (error2 instanceof CorrelatedReferenceError) {
2661
+ throw new ClientRequestError({
2662
+ success: false,
2663
+ kind: "error",
2664
+ code: "bad_request" /* BadRequest */,
2665
+ errors: [{ detail: error2.message }]
2666
+ });
2667
+ }
2668
+ throw error2;
2669
+ }
2670
+ if (resolution.status === "unresolvable") {
2671
+ setDocValue(docs[index], include.path, 0);
2672
+ return;
2673
+ }
2674
+ const expanded = resolution.filter;
2675
+ const operandErrors = validateExpandedCorrelatedOperands(expanded, this.getRequestComplexity());
2676
+ if (operandErrors.length > 0) {
2677
+ throw new ClientRequestError({
2678
+ success: false,
2679
+ kind: "error",
2680
+ code: "bad_request" /* BadRequest */,
2681
+ errors: [{ detail: operandErrors[0].detail }]
2682
+ });
2683
+ }
2684
+ const authorized = await svc.genFilter("count", expanded);
2685
+ if (authorized === false) {
2686
+ throw new ClientRequestError({ success: false, kind: "error", code: "forbidden" /* Forbidden */ });
2687
+ }
2688
+ this.claimCorrelatedQuerySlot();
2689
+ let result;
2690
+ try {
2691
+ result = await svc.countTrusted(authorized);
2692
+ } catch (error2) {
2693
+ const clientResult = this.getClientRequestErrorResult(error2);
2694
+ if (clientResult) throw new ClientRequestError(clientResult);
2695
+ throw error2;
2696
+ }
2697
+ if (result.success) {
2698
+ setDocValue(docs[index], include.path, result.data);
2699
+ } else {
2700
+ throw new ClientRequestError(result);
2701
+ }
2702
+ });
2703
+ void depth;
2704
+ }
1851
2705
  async parseClientData(filter2, scheduler = new RequestConcurrencyScheduler(this.getRequestComplexity().maxBulkConcurrency), scheduled = false) {
1852
2706
  const result = await iterateQuery(
1853
2707
  filter2,
@@ -1890,7 +2744,7 @@ var Base = class {
1890
2744
  }
1891
2745
  let ret = result.data;
1892
2746
  if (sqOptions.path) {
1893
- ret = isArray6(ret) ? flatten(ret.map((v) => get4(v, sqOptions.path))) : get4(ret, sqOptions.path);
2747
+ ret = isArray7(ret) ? flatten(ret.map((v) => get4(v, sqOptions.path))) : get4(ret, sqOptions.path);
1894
2748
  }
1895
2749
  if (sqOptions.compact) {
1896
2750
  ret = compact(castArray(ret));
@@ -1907,8 +2761,9 @@ var Base = class {
1907
2761
  // src/services/service.ts
1908
2762
  import {
1909
2763
  castArray as castArray2,
2764
+ cloneDeep as cloneDeep3,
1910
2765
  compact as compact2,
1911
- isArray as isArray7,
2766
+ isArray as isArray8,
1912
2767
  isBoolean as isBoolean3,
1913
2768
  isFunction as isFunction3,
1914
2769
  omit,
@@ -2411,6 +3266,29 @@ var assertModelDocument = (value, modelName, hookName) => {
2411
3266
  }
2412
3267
  throw new Error(`${hookName} hook for model=${modelName} must return a Mongoose document instance`);
2413
3268
  };
3269
+ var unsetDocPath = (doc, path) => {
3270
+ const segments = path.split(".");
3271
+ let current = doc;
3272
+ const unwrap = (value) => value != null && typeof value === "object" && "_doc" in value ? value._doc : value;
3273
+ current = unwrap(current);
3274
+ for (let i = 0; i < segments.length - 1; i++) {
3275
+ if (current == null || typeof current !== "object") return;
3276
+ if (!Object.prototype.hasOwnProperty.call(current, segments[i])) return;
3277
+ current = unwrap(current[segments[i]]);
3278
+ }
3279
+ if (current != null && typeof current === "object") {
3280
+ delete current[segments[segments.length - 1]];
3281
+ }
3282
+ };
3283
+ var shouldKeepCorrelatedRef = (ref, keep) => {
3284
+ if (keep.has(ref)) return true;
3285
+ const top = ref.split(".")[0];
3286
+ if (keep.has(top)) return true;
3287
+ for (const kept of keep) {
3288
+ if (kept === ref || kept.startsWith(`${ref}.`) || ref.startsWith(`${kept}.`)) return true;
3289
+ }
3290
+ return false;
3291
+ };
2414
3292
  var Service = class extends Base {
2415
3293
  findRawParentDoc(args) {
2416
3294
  return this.model.findOne({
@@ -2477,8 +3355,9 @@ var Service = class extends Base {
2477
3355
  overridePopulate || this.genPopulate(populateAccess || access, populate),
2478
3356
  this.genAllowedFields({}, access, this.baseFieldsExt)
2479
3357
  ]);
2480
- const { includes, includeLocalFields, includePaths } = this.processInclude(include);
2481
- const finalSelect = normalizeSelect(_select).concat(includeLocalFields);
3358
+ const { includes, correlatedIncludes, includeLocalFields, includePaths, correlatedReferenceFields } = this.processInclude(include);
3359
+ const correlatedOutputPaths = correlatedIncludes.map((entry) => entry.path);
3360
+ const finalSelect = normalizeSelect(_select).concat(includeLocalFields, correlatedReferenceFields);
2482
3361
  const query = {
2483
3362
  filter: _filter,
2484
3363
  select: finalSelect,
@@ -2511,8 +3390,14 @@ var Service = class extends Base {
2511
3390
  originalDocumentSnapshot: toObject(doc),
2512
3391
  resolvedQuery: query
2513
3392
  };
3393
+ const correlatedSnapshots = [cloneDeep3(toObject(doc))];
2514
3394
  try {
2515
3395
  doc = await this.includeDocs(doc, includes);
3396
+ if (correlatedIncludes.length > 0) {
3397
+ const docList = [doc];
3398
+ await this.includeCorrelatedDocs(docList, correlatedIncludes, correlatedSnapshots);
3399
+ doc = docList[0];
3400
+ }
2516
3401
  } catch (error2) {
2517
3402
  const result = this.getClientRequestErrorResult(error2);
2518
3403
  if (result) {
@@ -2530,8 +3415,20 @@ var Service = class extends Base {
2530
3415
  doc = await this.trimOutputFields(
2531
3416
  doc,
2532
3417
  access,
2533
- this.baseFieldsExt.concat(includePaths, normalizeSelect(overrideSelect))
3418
+ this.baseFieldsExt.concat(includePaths, correlatedOutputPaths, normalizeSelect(overrideSelect))
2534
3419
  );
3420
+ if (correlatedReferenceFields.length > 0) {
3421
+ const keep = /* @__PURE__ */ new Set([
3422
+ ...normalizeSelect(_select),
3423
+ ...normalizeSelect(overrideSelect),
3424
+ ...this.baseFieldsExt,
3425
+ ...includePaths,
3426
+ ...correlatedOutputPaths
3427
+ ]);
3428
+ for (const ref of correlatedReferenceFields) {
3429
+ if (!shouldKeepCorrelatedRef(ref, keep)) unsetDocPath(doc, ref);
3430
+ }
3431
+ }
2535
3432
  if (!includePermissions) doc = this.addEmptyPermissions(doc);
2536
3433
  this.completeOp("findOne", startedAt, "success" /* Success */, _filter);
2537
3434
  return { success: true, kind: "single", code: "success" /* Success */, data: doc, query, context };
@@ -2577,11 +3474,12 @@ var Service = class extends Base {
2577
3474
  this.genAllowedFields({}, "list", this.baseFieldsExt)
2578
3475
  ]);
2579
3476
  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);
3477
+ const filteredPopulate = isArray8(finalSelect) && isArray8(_populate) ? _populate.filter((p) => finalSelect.includes(p.path.split(".")[0])) : _populate;
3478
+ const { includes, correlatedIncludes, includeLocalFields, includePaths, correlatedReferenceFields } = this.processInclude(include);
3479
+ const correlatedOutputPaths = correlatedIncludes.map((entry) => entry.path);
2582
3480
  const query = {
2583
3481
  filter: _filter,
2584
- select: finalSelect.concat(includeLocalFields),
3482
+ select: finalSelect.concat(includeLocalFields, correlatedReferenceFields),
2585
3483
  populate: filteredPopulate,
2586
3484
  sort,
2587
3485
  ...pagination
@@ -2590,7 +3488,7 @@ var Service = class extends Base {
2590
3488
  sort,
2591
3489
  skip: pagination.skip,
2592
3490
  limit: pagination.limit,
2593
- selectCount: finalSelect.concat(includeLocalFields).length,
3491
+ selectCount: finalSelect.concat(includeLocalFields, correlatedReferenceFields).length,
2594
3492
  populateCount: Array.isArray(filteredPopulate) ? filteredPopulate.length : filteredPopulate ? 1 : 0
2595
3493
  });
2596
3494
  if (_filter === false) {
@@ -2615,8 +3513,12 @@ var Service = class extends Base {
2615
3513
  resolvedQuery: query
2616
3514
  }));
2617
3515
  const _decorate = isFunction3(decorate) ? decorate : (v) => v;
3516
+ const correlatedSnapshots = docs.map((doc) => cloneDeep3(toObject(doc)));
2618
3517
  try {
2619
3518
  docs = await this.includeDocs(docs, includes);
3519
+ if (correlatedIncludes.length > 0) {
3520
+ docs = await this.includeCorrelatedDocs(docs, correlatedIncludes, correlatedSnapshots);
3521
+ }
2620
3522
  } catch (error2) {
2621
3523
  const result = this.getClientRequestErrorResult(error2);
2622
3524
  if (result) {
@@ -2638,8 +3540,20 @@ var Service = class extends Base {
2638
3540
  doc = await this.trimOutputFields(
2639
3541
  doc,
2640
3542
  "list",
2641
- this.baseFieldsExt.concat(includePaths, normalizeSelect(overrideSelect))
3543
+ this.baseFieldsExt.concat(includePaths, correlatedOutputPaths, normalizeSelect(overrideSelect))
2642
3544
  );
3545
+ if (correlatedReferenceFields.length > 0) {
3546
+ const keep = /* @__PURE__ */ new Set([
3547
+ ...normalizeSelect(_select),
3548
+ ...normalizeSelect(overrideSelect),
3549
+ ...this.baseFieldsExt,
3550
+ ...includePaths,
3551
+ ...correlatedOutputPaths
3552
+ ]);
3553
+ for (const ref of correlatedReferenceFields) {
3554
+ if (!shouldKeepCorrelatedRef(ref, keep)) unsetDocPath(doc, ref);
3555
+ }
3556
+ }
2643
3557
  doc = await _decorate(doc, contexts[i]);
2644
3558
  if (!includePermissions) doc = this.addEmptyPermissions(doc);
2645
3559
  return doc;
@@ -2724,7 +3638,7 @@ var Service = class extends Base {
2724
3638
  validationErrors.push({ index, errors: [] });
2725
3639
  return void 0;
2726
3640
  }
2727
- } else if (isArray7(validated)) {
3641
+ } else if (isArray8(validated)) {
2728
3642
  if (validated.length > 0) {
2729
3643
  validationErrors.push({
2730
3644
  index,
@@ -2881,7 +3795,7 @@ var Service = class extends Base {
2881
3795
  this.completeOp("updateOne", startedAt, "bad_request" /* BadRequest */, _filter);
2882
3796
  return { success: false, kind: "error", code: "bad_request" /* BadRequest */ };
2883
3797
  }
2884
- } else if (isArray7(validated)) {
3798
+ } else if (isArray8(validated)) {
2885
3799
  if (validated.length > 0) {
2886
3800
  this.completeOp("updateOne", startedAt, "bad_request" /* BadRequest */, _filter);
2887
3801
  return { success: false, kind: "error", code: "bad_request" /* BadRequest */, errors: validated };
@@ -3068,6 +3982,23 @@ var Service = class extends Base {
3068
3982
  if (filter2 === false) return { success: false, kind: "error", code: "forbidden" /* Forbidden */, query };
3069
3983
  return { success: true, kind: "single", code: "success" /* Success */, data: await this.model.countDocuments(filter2), query };
3070
3984
  }
3985
+ /**
3986
+ * Trusted count for correlated includes (ACI-03): the caller already
3987
+ * resolved parent references, revalidated expanded operands, and applied
3988
+ * explicit `count` access via `genFilter`. This skips client-filter
3989
+ * validation and subquery parsing so substituted parent data stays inert.
3990
+ */
3991
+ async countTrusted(authorizedFilter) {
3992
+ const query = { filter: authorizedFilter };
3993
+ if (authorizedFilter === false) return { success: false, kind: "error", code: "forbidden" /* Forbidden */, query };
3994
+ return {
3995
+ success: true,
3996
+ kind: "single",
3997
+ code: "success" /* Success */,
3998
+ data: await this.model.countDocuments(authorizedFilter),
3999
+ query
4000
+ };
4001
+ }
3071
4002
  async countByFieldValues(foreignField, values, filter2 = {}, access = "list") {
3072
4003
  if (!isValidFieldPath(foreignField)) {
3073
4004
  return {
@@ -3583,29 +4514,64 @@ var DataService = class {
3583
4514
  };
3584
4515
 
3585
4516
  // 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";
4517
+ import { cloneDeep as cloneDeep4, forEach as forEach6, get as get7, hasOwn, isArray as isArray9, isPlainObject as isPlainObject14, map, set as set5 } from "@web-ts-toolkit/utils";
3587
4518
  var UNSAFE_PATH_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
3588
- function failUnsafeSegment(path) {
4519
+ var DEFAULT_MAX_SLICE = 1e3;
4520
+ var DEFAULT_MASK_REPLACEMENT = "***";
4521
+ function failUnsafeSegment(path, fn = "copyAndDepopulate") {
3589
4522
  throw new Error(
3590
- `copyAndDepopulate: refusing path '${path}' because it contains an unsafe segment (__proto__, prototype, or constructor).`
4523
+ `${fn}: refusing path '${path}' because it contains an unsafe segment (__proto__, prototype, or constructor).`
3591
4524
  );
3592
4525
  }
3593
4526
  function failMissingId(path) {
3594
4527
  throw new Error(`copyAndDepopulate: path '${path}' resolved to a record missing the configured id field.`);
3595
4528
  }
3596
- function assertSafePath(path) {
4529
+ function failInvalidSlice(value, field, fn) {
4530
+ throw new Error(`${fn}: '${field}' must be a non-negative safe integer, received ${String(value)}.`);
4531
+ }
4532
+ function assertSafePath(path, fn = "copyAndDepopulate") {
3597
4533
  const segs = path.split(".");
3598
4534
  for (const seg of segs) {
3599
4535
  if (seg.length === 0) continue;
3600
4536
  if (UNSAFE_PATH_KEYS.has(seg)) {
3601
- failUnsafeSegment(path);
4537
+ failUnsafeSegment(path, fn);
3602
4538
  }
3603
4539
  }
3604
4540
  }
4541
+ function assertNonNegativeSafeInteger(value, field, fn) {
4542
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
4543
+ failInvalidSlice(value, field, fn);
4544
+ }
4545
+ }
4546
+ function resolveLeafParents(obj, src) {
4547
+ const segs = src.split(".");
4548
+ let targets = [obj];
4549
+ for (let ind = 0; ind < segs.length - 1; ind += 1) {
4550
+ const seg = segs[ind];
4551
+ targets = targets.reduce((ret, target) => {
4552
+ if (!isPlainObject14(target)) return ret;
4553
+ const targetObject = target;
4554
+ const next = targetObject[seg];
4555
+ if (isArray9(next)) {
4556
+ for (const item of next) {
4557
+ ret.push(item);
4558
+ }
4559
+ } else if (next !== null && next !== void 0) ret.push(next);
4560
+ return ret;
4561
+ }, []);
4562
+ }
4563
+ const leaf = segs[segs.length - 1];
4564
+ const pairs = [];
4565
+ forEach6(targets, (target) => {
4566
+ if (!isPlainObject14(target)) return;
4567
+ pairs.push({ parent: target, leaf });
4568
+ });
4569
+ return pairs;
4570
+ }
3605
4571
  var copyAndDepopulate = (docObject, operations, options = { mutable: true, idField: "_id" }) => {
3606
4572
  const mutable = options.mutable !== false;
3607
4573
  const idField = options.idField ?? "_id";
3608
- const obj = mutable ? docObject : cloneDeep(docObject);
4574
+ const obj = mutable ? docObject : cloneDeep4(docObject);
3609
4575
  forEach6(Array.isArray(operations) ? operations : [], (op) => {
3610
4576
  if (!op || typeof op.src !== "string" || typeof op.dest !== "string") return;
3611
4577
  const src = op.src;
@@ -3618,15 +4584,15 @@ var copyAndDepopulate = (docObject, operations, options = { mutable: true, idFie
3618
4584
  forEach6(segs, (seg, ind) => {
3619
4585
  if (segs.length === ind + 1) {
3620
4586
  forEach6(targets, (target) => {
3621
- if (!isPlainObject13(target)) return;
4587
+ if (!isPlainObject14(target)) return;
3622
4588
  const targetObject = target;
3623
4589
  const leaf = targetObject[seg];
3624
4590
  const sourceForDest = leaf;
3625
4591
  let depopulated;
3626
- if (isArray8(leaf)) {
4592
+ if (isArray9(leaf)) {
3627
4593
  if (leaf.length === 0) {
3628
4594
  depopulated = [];
3629
- } else if (leaf.every((item) => isPlainObject13(item))) {
4595
+ } else if (leaf.every((item) => isPlainObject14(item))) {
3630
4596
  depopulated = map(leaf, idField).map((id, i) => {
3631
4597
  if (id === void 0) {
3632
4598
  failMissingId(`${src}[${i}]`);
@@ -3636,7 +4602,7 @@ var copyAndDepopulate = (docObject, operations, options = { mutable: true, idFie
3636
4602
  } else {
3637
4603
  return;
3638
4604
  }
3639
- } else if (isPlainObject13(leaf)) {
4605
+ } else if (isPlainObject14(leaf)) {
3640
4606
  const id = get7(leaf, idField);
3641
4607
  if (id === void 0) {
3642
4608
  failMissingId(src);
@@ -3650,10 +4616,10 @@ var copyAndDepopulate = (docObject, operations, options = { mutable: true, idFie
3650
4616
  });
3651
4617
  } else {
3652
4618
  targets = targets.reduce((ret, target) => {
3653
- if (!isPlainObject13(target)) return ret;
4619
+ if (!isPlainObject14(target)) return ret;
3654
4620
  const targetObject = target;
3655
4621
  const next = targetObject[seg];
3656
- if (isArray8(next)) {
4622
+ if (isArray9(next)) {
3657
4623
  for (const item of next) {
3658
4624
  ret.push(item);
3659
4625
  }
@@ -3665,6 +4631,125 @@ var copyAndDepopulate = (docObject, operations, options = { mutable: true, idFie
3665
4631
  });
3666
4632
  return obj;
3667
4633
  };
4634
+ var copyPaths = (docObject, operations, options = { mutable: true }) => {
4635
+ const mutable = options.mutable !== false;
4636
+ const obj = mutable ? docObject : cloneDeep4(docObject);
4637
+ forEach6(Array.isArray(operations) ? operations : [], (op) => {
4638
+ if (!op || typeof op.src !== "string" || typeof op.dest !== "string") return;
4639
+ const src = op.src;
4640
+ const dest = op.dest;
4641
+ if (src.length === 0 || dest.length === 0) return;
4642
+ assertSafePath(src, "copyPaths");
4643
+ assertSafePath(dest, "copyPaths");
4644
+ if (src === dest) return;
4645
+ forEach6(resolveLeafParents(obj, src), ({ parent, leaf }) => {
4646
+ if (!hasOwn(parent, leaf)) return;
4647
+ set5(parent, dest, cloneDeep4(parent[leaf]));
4648
+ });
4649
+ });
4650
+ return obj;
4651
+ };
4652
+ var movePaths = (docObject, operations, options = { mutable: true }) => {
4653
+ const mutable = options.mutable !== false;
4654
+ const obj = mutable ? docObject : cloneDeep4(docObject);
4655
+ forEach6(Array.isArray(operations) ? operations : [], (op) => {
4656
+ if (!op || typeof op.src !== "string" || typeof op.dest !== "string") return;
4657
+ const src = op.src;
4658
+ const dest = op.dest;
4659
+ if (src.length === 0 || dest.length === 0) return;
4660
+ assertSafePath(src, "movePaths");
4661
+ assertSafePath(dest, "movePaths");
4662
+ if (src === dest) return;
4663
+ forEach6(resolveLeafParents(obj, src), ({ parent, leaf }) => {
4664
+ if (!hasOwn(parent, leaf)) return;
4665
+ const value = cloneDeep4(parent[leaf]);
4666
+ set5(parent, dest, value);
4667
+ if (dest === leaf || dest.startsWith(`${leaf}.`)) return;
4668
+ delete parent[leaf];
4669
+ });
4670
+ });
4671
+ return obj;
4672
+ };
4673
+ var sliceArrays = (docObject, operations, options = { mutable: true, maxSlice: DEFAULT_MAX_SLICE }) => {
4674
+ const mutable = options.mutable !== false;
4675
+ const maxSlice = options.maxSlice ?? DEFAULT_MAX_SLICE;
4676
+ assertNonNegativeSafeInteger(maxSlice, "maxSlice", "sliceArrays");
4677
+ const obj = mutable ? docObject : cloneDeep4(docObject);
4678
+ forEach6(Array.isArray(operations) ? operations : [], (op) => {
4679
+ if (!op || typeof op.src !== "string") return;
4680
+ const src = op.src;
4681
+ if (src.length === 0) return;
4682
+ assertSafePath(src, "sliceArrays");
4683
+ const rawDest = typeof op.dest === "undefined" ? void 0 : op.dest;
4684
+ if (typeof rawDest !== "undefined") {
4685
+ if (typeof rawDest !== "string" || rawDest.length === 0) return;
4686
+ assertSafePath(rawDest, "sliceArrays");
4687
+ }
4688
+ const skip = op.skip ?? 0;
4689
+ assertNonNegativeSafeInteger(skip, "skip", "sliceArrays");
4690
+ let limit;
4691
+ if (typeof op.limit !== "undefined") {
4692
+ assertNonNegativeSafeInteger(op.limit, "limit", "sliceArrays");
4693
+ limit = Math.min(op.limit, maxSlice);
4694
+ }
4695
+ forEach6(resolveLeafParents(obj, src), ({ parent, leaf }) => {
4696
+ const leafValue = parent[leaf];
4697
+ if (!isArray9(leafValue)) return;
4698
+ const end = typeof limit === "undefined" ? void 0 : skip + limit;
4699
+ const window = leafValue.slice(skip, end);
4700
+ if (typeof rawDest === "undefined") {
4701
+ set5(parent, leaf, window);
4702
+ } else {
4703
+ set5(parent, rawDest, window);
4704
+ }
4705
+ });
4706
+ });
4707
+ return obj;
4708
+ };
4709
+ var countPaths = (docObject, operations, options = { mutable: true }) => {
4710
+ const mutable = options.mutable !== false;
4711
+ const obj = mutable ? docObject : cloneDeep4(docObject);
4712
+ forEach6(Array.isArray(operations) ? operations : [], (op) => {
4713
+ if (!op || typeof op.src !== "string" || typeof op.dest !== "string") return;
4714
+ const src = op.src;
4715
+ const dest = op.dest;
4716
+ if (src.length === 0 || dest.length === 0) return;
4717
+ assertSafePath(src, "countPaths");
4718
+ assertSafePath(dest, "countPaths");
4719
+ forEach6(resolveLeafParents(obj, src), ({ parent, leaf }) => {
4720
+ if (!hasOwn(parent, leaf)) return;
4721
+ const leafValue = parent[leaf];
4722
+ let count;
4723
+ if (isArray9(leafValue)) {
4724
+ count = leafValue.length;
4725
+ } else if (isPlainObject14(leafValue)) {
4726
+ count = Object.keys(leafValue).length;
4727
+ } else if (leafValue === null || typeof leafValue === "undefined") {
4728
+ count = 0;
4729
+ } else {
4730
+ count = 1;
4731
+ }
4732
+ set5(parent, dest, count);
4733
+ });
4734
+ });
4735
+ return obj;
4736
+ };
4737
+ var maskPaths = (docObject, operations, options = { mutable: true }) => {
4738
+ const mutable = options.mutable !== false;
4739
+ const obj = mutable ? docObject : cloneDeep4(docObject);
4740
+ forEach6(Array.isArray(operations) ? operations : [], (op) => {
4741
+ if (!op || typeof op.src !== "string") return;
4742
+ const src = op.src;
4743
+ if (src.length === 0) return;
4744
+ assertSafePath(src, "maskPaths");
4745
+ const replacement = Object.prototype.hasOwnProperty.call(op, "replacement") ? op.replacement : DEFAULT_MASK_REPLACEMENT;
4746
+ forEach6(resolveLeafParents(obj, src), ({ parent, leaf }) => {
4747
+ if (!hasOwn(parent, leaf)) return;
4748
+ set5(parent, leaf, replacement);
4749
+ });
4750
+ });
4751
+ return obj;
4752
+ };
3668
4753
 
3669
4754
  // src/symbols.ts
3670
4755
  var MIDDLEWARE = /* @__PURE__ */ Symbol.for("@web-ts-toolkit/access-router/middleware");
@@ -3707,18 +4792,18 @@ var Cache = class {
3707
4792
  import {
3708
4793
  castArray as castArray3,
3709
4794
  arrayToRecord,
3710
- hasOwn as hasOwn2,
3711
- isArray as isArray9,
4795
+ hasOwn as hasOwn3,
4796
+ isArray as isArray10,
3712
4797
  isBoolean as isBoolean4,
3713
4798
  isEmpty as isEmpty2,
3714
4799
  isEqual,
3715
4800
  isFunction as isFunction4,
3716
- isPlainObject as isPlainObject14,
4801
+ isPlainObject as isPlainObject15,
3717
4802
  isString as isString5
3718
4803
  } from "@web-ts-toolkit/utils";
3719
4804
 
3720
4805
  // src/permission.ts
3721
- import { hasOwn } from "@web-ts-toolkit/utils";
4806
+ import { hasOwn as hasOwn2 } from "@web-ts-toolkit/utils";
3722
4807
  var Permission = class {
3723
4808
  constructor(permissions) {
3724
4809
  this.permissions = { ...permissions };
@@ -3734,7 +4819,7 @@ var Permission = class {
3734
4819
  return [permissionOrPermissions].concat(permissions);
3735
4820
  }
3736
4821
  hasKey(permission) {
3737
- return hasOwn(this.permissions, permission);
4822
+ return hasOwn2(this.permissions, permission);
3738
4823
  }
3739
4824
  has(permission) {
3740
4825
  return this.permissions[permission] === true;
@@ -3754,10 +4839,10 @@ var permission_default = Permission;
3754
4839
 
3755
4840
  // src/core-shared.ts
3756
4841
  function isAndFilter(filter2) {
3757
- return !!filter2 && isPlainObject14(filter2) && Object.keys(filter2).length === 1 && isArray9(filter2.$and);
4842
+ return !!filter2 && isPlainObject15(filter2) && Object.keys(filter2).length === 1 && isArray10(filter2.$and);
3758
4843
  }
3759
4844
  function isMergeableClause(filter2) {
3760
- return !!filter2 && isPlainObject14(filter2) && Object.keys(filter2).every((key) => !key.startsWith("$"));
4845
+ return !!filter2 && isPlainObject15(filter2) && Object.keys(filter2).every((key) => !key.startsWith("$"));
3761
4846
  }
3762
4847
  function optimizeAndFilter(clausesInput) {
3763
4848
  const clauses = [];
@@ -3772,10 +4857,10 @@ function optimizeAndFilter(clausesInput) {
3772
4857
  }
3773
4858
  }
3774
4859
  const dedupeKey = (value) => {
3775
- if (isArray9(value)) {
4860
+ if (isArray10(value)) {
3776
4861
  return `[${value.map(dedupeKey).join(",")}]`;
3777
4862
  }
3778
- if (isPlainObject14(value)) {
4863
+ if (isPlainObject15(value)) {
3779
4864
  return `{${Object.keys(value).sort().map((key) => `${key}:${dedupeKey(value[key])}`).join(",")}}`;
3780
4865
  }
3781
4866
  return JSON.stringify(value);
@@ -3799,7 +4884,7 @@ function optimizeAndFilter(clausesInput) {
3799
4884
  }
3800
4885
  let canMerge = true;
3801
4886
  for (const [key, value] of Object.entries(clause)) {
3802
- if (hasOwn2(mergedClause, key) && !isEqual(mergedClause[key], value)) {
4887
+ if (hasOwn3(mergedClause, key) && !isEqual(mergedClause[key], value)) {
3803
4888
  canMerge = false;
3804
4889
  break;
3805
4890
  }
@@ -3817,7 +4902,7 @@ function optimizeAndFilter(clausesInput) {
3817
4902
  }
3818
4903
  function normalizeFilter(filter2) {
3819
4904
  if (filter2 === false) return false;
3820
- if (!filter2 || !isPlainObject14(filter2) || isEmpty2(filter2)) return null;
4905
+ if (!filter2 || !isPlainObject15(filter2) || isEmpty2(filter2)) return null;
3821
4906
  if (!isAndFilter(filter2)) {
3822
4907
  return filter2;
3823
4908
  }
@@ -3884,8 +4969,8 @@ async function setRequestPermissions(req) {
3884
4969
  const globalPermissions = getGlobalOption("globalPermissions");
3885
4970
  if (!isFunction4(globalPermissions)) return;
3886
4971
  const permissions = await globalPermissions.call(req, req);
3887
- if (isPlainObject14(permissions)) req[requestPermissionField] = permissions;
3888
- else if (isArray9(permissions)) req[requestPermissionField] = arrayToRecord(permissions);
4972
+ if (isPlainObject15(permissions)) req[requestPermissionField] = permissions;
4973
+ else if (isArray10(permissions)) req[requestPermissionField] = arrayToRecord(permissions);
3889
4974
  else if (isString5(permissions)) req[requestPermissionField] = { [permissions]: true };
3890
4975
  owners.set(requestPermissionField, activeRuntime);
3891
4976
  }
@@ -3898,7 +4983,7 @@ async function evaluateRouteGuard(req, permissions, routeGuard) {
3898
4983
  if (isString5(routeGuard)) {
3899
4984
  return stringHandler(routeGuard);
3900
4985
  }
3901
- if (isArray9(routeGuard)) {
4986
+ if (isArray10(routeGuard)) {
3902
4987
  return arrayHandler(routeGuard);
3903
4988
  }
3904
4989
  if (isFunction4(routeGuard)) {
@@ -3936,7 +5021,7 @@ async function collectSchemaFields({
3936
5021
  if (value) fields.push(key);
3937
5022
  } else if (isString5(value)) {
3938
5023
  if (stringHandler(value)) fields.push(key);
3939
- } else if (isArray9(value)) {
5024
+ } else if (isArray10(value)) {
3940
5025
  if (arrayHandler(value)) fields.push(key);
3941
5026
  } else if (isFunction4(value)) {
3942
5027
  if (await value.call(req, ...functionArgs)) fields.push(key);
@@ -4241,7 +5326,7 @@ var Core = class {
4241
5326
  if (isFunction5(validate)) {
4242
5327
  const permissions = this.getGlobalPermissions();
4243
5328
  return validate.call(this.req, allowedData, permissions, context);
4244
- } else if (isBoolean5(validate) || isArray10(validate)) {
5329
+ } else if (isBoolean5(validate) || isArray11(validate)) {
4245
5330
  return validate;
4246
5331
  } else {
4247
5332
  return true;
@@ -4385,6 +5470,41 @@ var Core = class {
4385
5470
  options
4386
5471
  );
4387
5472
  break;
5473
+ case "COPY":
5474
+ docObject = copyPaths(
5475
+ docObject,
5476
+ args,
5477
+ options
5478
+ );
5479
+ break;
5480
+ case "MOVE":
5481
+ docObject = movePaths(
5482
+ docObject,
5483
+ args,
5484
+ options
5485
+ );
5486
+ break;
5487
+ case "SLICE":
5488
+ docObject = sliceArrays(
5489
+ docObject,
5490
+ args,
5491
+ options
5492
+ );
5493
+ break;
5494
+ case "COUNT":
5495
+ docObject = countPaths(
5496
+ docObject,
5497
+ args,
5498
+ options
5499
+ );
5500
+ break;
5501
+ case "MASK":
5502
+ docObject = maskPaths(
5503
+ docObject,
5504
+ args,
5505
+ options
5506
+ );
5507
+ break;
4388
5508
  }
4389
5509
  });
4390
5510
  return docObject;
@@ -4462,19 +5582,19 @@ function guard(condition) {
4462
5582
  const permissions = req[PERMISSIONS];
4463
5583
  let cond = condition;
4464
5584
  let phas = (key) => permissions.has(key);
4465
- if (isPlainObject15(condition)) {
5585
+ if (isPlainObject16(condition)) {
4466
5586
  const { modelName, id, condition: _cond } = condition;
4467
5587
  const svc = req.macl.getPublicService(modelName);
4468
5588
  const select = getModelOption(modelName, `alwaysSelectFields.read`, void 0);
4469
5589
  let _id = isString7(id) ? id : null;
4470
- if (isPlainObject15(id)) {
5590
+ if (isPlainObject16(id)) {
4471
5591
  const { type = "param", key } = id;
4472
5592
  if (type === "param") {
4473
5593
  const paramValue = req.params[key];
4474
- _id = isArray11(paramValue) ? paramValue[0] ?? null : paramValue ?? null;
5594
+ _id = isArray12(paramValue) ? paramValue[0] ?? null : paramValue ?? null;
4475
5595
  } else if (type === "query") {
4476
5596
  const _qval = req.query[key];
4477
- if (isArray11(_qval)) _id = _qval.length > 0 ? _qval[0] : null;
5597
+ if (isArray12(_qval)) _id = _qval.length > 0 ? _qval[0] : null;
4478
5598
  else _id = _qval;
4479
5599
  } else {
4480
5600
  _id = null;
@@ -4494,7 +5614,7 @@ function guard(condition) {
4494
5614
  const { stringHandler, arrayHandler } = createValidator(phas);
4495
5615
  if (isString7(cond)) {
4496
5616
  if (stringHandler(cond)) return next();
4497
- } else if (isArray11(cond)) {
5617
+ } else if (isArray12(cond)) {
4498
5618
  if (arrayHandler(cond)) return next();
4499
5619
  } else if (isFunction6(cond)) {
4500
5620
  const result = await cond.call(req, permissions);
@@ -4509,7 +5629,7 @@ import express2 from "express";
4509
5629
 
4510
5630
  // src/routers/model-router.ts
4511
5631
  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";
5632
+ import { forEach as forEach8, isPlainObject as isPlainObject18, isString as isString8, isUndefined as isUndefined2, normalizeUrlPath as normalizeUrlPath2, padEnd } from "@web-ts-toolkit/utils";
4513
5633
 
4514
5634
  // src/routers/router-mutation.ts
4515
5635
  var MODEL_BUILD_TIME_OPTION_KEYS = /* @__PURE__ */ new Set([
@@ -4638,6 +5758,7 @@ import JsonRouter4 from "@web-ts-toolkit/express-json-router";
4638
5758
 
4639
5759
  // src/validation/common.ts
4640
5760
  import { z as z2 } from "zod";
5761
+ import { isPlainObject as isPlainObject17 } from "@web-ts-toolkit/utils";
4641
5762
  var stringOrStringArray = z2.union([z2.string(), z2.array(z2.string())]);
4642
5763
  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
5764
  var queryBooleanString = z2.enum(["true", "false"]);
@@ -4698,7 +5819,8 @@ var subPopulateSchema = z2.union([
4698
5819
  select: projectionSchema.optional()
4699
5820
  }).passthrough()
4700
5821
  ]);
4701
- var includeItemSchema = z2.object({
5822
+ var legacyIncludeItemSchema = z2.object({
5823
+ mode: z2.enum(["legacy"]).optional(),
4702
5824
  model: z2.string().min(1),
4703
5825
  op: z2.enum(["list", "read", "count"]),
4704
5826
  path: z2.string().min(1),
@@ -4707,8 +5829,139 @@ var includeItemSchema = z2.object({
4707
5829
  foreignField: z2.string().min(1),
4708
5830
  args: z2.unknown().optional().describe("Forwarded to list/read includes. Count includes ignore pagination fields so counts remain exact."),
4709
5831
  options: z2.unknown().optional()
4710
- }).passthrough();
4711
- var includeSchema = z2.union([includeItemSchema, z2.array(includeItemSchema)]);
5832
+ }).passthrough().superRefine((entry, ctx) => {
5833
+ if (entry.filter !== void 0 && containsParentMarkerShape(entry.filter)) {
5834
+ ctx.addIssue({
5835
+ code: z2.ZodIssueCode.custom,
5836
+ message: 'Legacy includes must not contain $parent markers; use a correlated include (mode: "correlated")',
5837
+ path: ["filter"]
5838
+ });
5839
+ }
5840
+ });
5841
+ var parentRefSchema = z2.object({ $parent: z2.string().min(1) }).strict().superRefine((value, ctx) => {
5842
+ if (!isValidParentRefPath(value.$parent)) {
5843
+ ctx.addIssue({
5844
+ code: z2.ZodIssueCode.custom,
5845
+ message: `Invalid $parent reference path: ${value.$parent}`,
5846
+ path: ["$parent"]
5847
+ });
5848
+ }
5849
+ });
5850
+ var correlatedIdSchema = z2.union([z2.string().min(1), parentRefSchema]);
5851
+ var correlatedFilterSchema = z2.record(z2.string(), z2.unknown()).superRefine((filter2, ctx) => {
5852
+ for (const issue of validateCorrelatedFilterTemplate(filter2)) {
5853
+ ctx.addIssue({
5854
+ code: z2.ZodIssueCode.custom,
5855
+ message: issue.message,
5856
+ path: issue.path.map(String)
5857
+ });
5858
+ }
5859
+ });
5860
+ var correlatedEmptyOptionsSchema = z2.unknown().refine(
5861
+ (value) => value === void 0 || isPlainObject17(value) && Object.keys(value).length === 0,
5862
+ "Correlated include options must be absent or empty"
5863
+ ).optional();
5864
+ var correlatedPathSchema = z2.string().min(1).superRefine((path, ctx) => {
5865
+ if (!isValidCorrelatedOutputPath(path)) {
5866
+ ctx.addIssue({
5867
+ code: z2.ZodIssueCode.custom,
5868
+ message: `Invalid correlated include output path: ${path}`,
5869
+ path: []
5870
+ });
5871
+ }
5872
+ });
5873
+ var checkArgsMarkers = (select, sort, ctx) => {
5874
+ const selectIssue = findArgsMarkerIssue(select, "args.select");
5875
+ if (selectIssue) {
5876
+ ctx.addIssue({ code: z2.ZodIssueCode.custom, message: selectIssue.message, path: ["select"] });
5877
+ }
5878
+ const sortIssue = findArgsMarkerIssue(sort, "args.sort");
5879
+ if (sortIssue) {
5880
+ ctx.addIssue({ code: z2.ZodIssueCode.custom, message: sortIssue.message, path: ["sort"] });
5881
+ }
5882
+ };
5883
+ var correlatedReadArgsSchema = z2.object({
5884
+ select: projectionSchema.optional(),
5885
+ sort: sortSchema.optional(),
5886
+ include: z2.lazy(() => correlatedNestedIncludeSchema).optional()
5887
+ }).strict().superRefine((args, ctx) => checkArgsMarkers(args.select, args.sort, ctx));
5888
+ var correlatedListArgsSchema = z2.object({
5889
+ select: projectionSchema.optional(),
5890
+ sort: sortSchema.optional(),
5891
+ skip: z2.union([nonNegativeIntegerSchema, nonNegativeIntegerString]).optional(),
5892
+ limit: z2.union([positiveIntegerSchema, positiveIntegerString]).optional(),
5893
+ page: z2.union([nonNegativeIntegerSchema, nonNegativeIntegerString]).optional(),
5894
+ pageSize: z2.union([positiveIntegerSchema, positiveIntegerString]).optional(),
5895
+ include: z2.lazy(() => correlatedNestedIncludeSchema).optional()
5896
+ }).strict().superRefine((args, ctx) => checkArgsMarkers(args.select, args.sort, ctx));
5897
+ var correlatedCountArgsSchema = z2.unknown().refine(
5898
+ (value) => value === void 0 || isPlainObject17(value) && Object.keys(value).length === 0,
5899
+ "Count includes accept no args"
5900
+ ).optional();
5901
+ var correlatedReadByIdSchema = z2.object({
5902
+ mode: z2.literal("correlated"),
5903
+ model: z2.string().min(1),
5904
+ op: z2.literal("read"),
5905
+ path: correlatedPathSchema,
5906
+ id: correlatedIdSchema,
5907
+ args: correlatedReadArgsSchema.optional(),
5908
+ options: correlatedEmptyOptionsSchema
5909
+ }).strict();
5910
+ var correlatedReadByFilterSchema = z2.object({
5911
+ mode: z2.literal("correlated"),
5912
+ model: z2.string().min(1),
5913
+ op: z2.literal("read"),
5914
+ path: correlatedPathSchema,
5915
+ filter: correlatedFilterSchema,
5916
+ args: correlatedReadArgsSchema.optional(),
5917
+ options: correlatedEmptyOptionsSchema
5918
+ }).strict();
5919
+ var correlatedListSchema = z2.object({
5920
+ mode: z2.literal("correlated"),
5921
+ model: z2.string().min(1),
5922
+ op: z2.literal("list"),
5923
+ path: correlatedPathSchema,
5924
+ filter: correlatedFilterSchema,
5925
+ args: correlatedListArgsSchema.optional(),
5926
+ options: correlatedEmptyOptionsSchema
5927
+ }).strict();
5928
+ var correlatedCountSchema = z2.object({
5929
+ mode: z2.literal("correlated"),
5930
+ model: z2.string().min(1),
5931
+ op: z2.literal("count"),
5932
+ path: correlatedPathSchema,
5933
+ filter: correlatedFilterSchema,
5934
+ args: correlatedCountArgsSchema,
5935
+ options: correlatedEmptyOptionsSchema
5936
+ }).strict();
5937
+ var includeItemSchema = z2.union([
5938
+ legacyIncludeItemSchema,
5939
+ correlatedReadByIdSchema,
5940
+ correlatedReadByFilterSchema,
5941
+ correlatedListSchema,
5942
+ correlatedCountSchema
5943
+ ]);
5944
+ var correlatedNestedIncludeSchema = z2.union([includeItemSchema, z2.array(includeItemSchema)]);
5945
+ var rejectDuplicateCorrelatedPaths = (items, ctx) => {
5946
+ const seen = /* @__PURE__ */ new Set();
5947
+ items.forEach((item, index) => {
5948
+ if (isPlainObject17(item) && item.mode === "correlated" && typeof item.path === "string") {
5949
+ if (seen.has(item.path)) {
5950
+ ctx.addIssue({
5951
+ code: z2.ZodIssueCode.custom,
5952
+ message: `Duplicate correlated include output path: ${item.path}`,
5953
+ path: [index, "path"]
5954
+ });
5955
+ } else {
5956
+ seen.add(item.path);
5957
+ }
5958
+ }
5959
+ });
5960
+ };
5961
+ var includeSchema = z2.union([
5962
+ includeItemSchema,
5963
+ z2.array(includeItemSchema).superRefine(rejectDuplicateCorrelatedPaths)
5964
+ ]);
4712
5965
  var fieldsSchema = z2.array(z2.string().min(1));
4713
5966
  var taskSchema = z2.object({
4714
5967
  type: z2.string().min(1),
@@ -5245,7 +6498,7 @@ var listBodySchema = z3.object({
5245
6498
  }).passthrough().superRefine((body, ctx) => rejectKeys(body, ctx, ["query"]));
5246
6499
  var countBodySchema = z3.object({
5247
6500
  filter: objectOrArraySchema.optional()
5248
- }).passthrough().superRefine((body, ctx) => rejectKeys(body, ctx, ["query", "access", "options"]));
6501
+ }).passthrough().superRefine((body, ctx) => rejectKeys(body, ctx, ["query", "access", "options", "include"]));
5249
6502
  var readFilterBodySchema = z3.object({
5250
6503
  filter: objectOrArraySchema.optional(),
5251
6504
  select: projectionSchema.optional(),
@@ -6523,7 +7776,7 @@ var ModelRouter = class _ModelRouter {
6523
7776
  value
6524
7777
  );
6525
7778
  }
6526
- if (arguments.length === 1 && isPlainObject16(keyOrOptions)) {
7779
+ if (arguments.length === 1 && isPlainObject18(keyOrOptions)) {
6527
7780
  assertMutableRouterOptions("model", keyOrOptions);
6528
7781
  this.runtime.setModelOptions(this.modelName, keyOrOptions);
6529
7782
  }
@@ -6962,7 +8215,7 @@ var RootRouter = class {
6962
8215
 
6963
8216
  // src/routers/data-router.ts
6964
8217
  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";
8218
+ import { isPlainObject as isPlainObject19, isString as isString9, isUndefined as isUndefined3, normalizeUrlPath as normalizeUrlPath4 } from "@web-ts-toolkit/utils";
6966
8219
  var clientErrors4 = JsonRouter7.clientErrors;
6967
8220
  function setOption2(parentKey, optionKey, option) {
6968
8221
  const key = isUndefined3(option) ? parentKey : `${parentKey}.${optionKey}`;
@@ -7163,7 +8416,7 @@ var DataRouter = class {
7163
8416
  assertMutableRouterOption("data", keyOrOptions);
7164
8417
  this.runtime.setDataOption(this.dataName, keyOrOptions, value);
7165
8418
  }
7166
- if (arguments.length === 1 && isPlainObject17(keyOrOptions)) {
8419
+ if (arguments.length === 1 && isPlainObject19(keyOrOptions)) {
7167
8420
  assertMutableRouterOptions("data", keyOrOptions);
7168
8421
  this.runtime.setDataOptions(this.dataName, keyOrOptions);
7169
8422
  }
@@ -7310,7 +8563,7 @@ function createRuntimeApi(runtime) {
7310
8563
  if (arguments.length === 2 && isString10(keyOrOptions)) {
7311
8564
  return runtime.setGlobalOption(keyOrOptions, value);
7312
8565
  }
7313
- if (arguments.length === 1 && isPlainObject18(keyOrOptions)) {
8566
+ if (arguments.length === 1 && isPlainObject20(keyOrOptions)) {
7314
8567
  return runtime.setGlobalOptions(keyOrOptions);
7315
8568
  }
7316
8569
  };