@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.js CHANGED
@@ -77,14 +77,14 @@ __export(index_exports, {
77
77
  setModelOptions: () => setModelOptions
78
78
  });
79
79
  module.exports = __toCommonJS(index_exports);
80
- var import_utils28 = require("@web-ts-toolkit/utils");
80
+ var import_utils30 = require("@web-ts-toolkit/utils");
81
81
 
82
82
  // src/middleware.ts
83
83
  var import_express_json_router2 = __toESM(require("@web-ts-toolkit/express-json-router"));
84
- var import_utils22 = require("@web-ts-toolkit/utils");
84
+ var import_utils23 = require("@web-ts-toolkit/utils");
85
85
 
86
86
  // src/core.ts
87
- var import_utils21 = require("@web-ts-toolkit/utils");
87
+ var import_utils22 = require("@web-ts-toolkit/utils");
88
88
 
89
89
  // src/runtime.ts
90
90
  var import_mongoose3 = __toESM(require("mongoose"));
@@ -1000,7 +1000,9 @@ var defaultRequestComplexity = {
1000
1000
  maxIncludeCount: 10,
1001
1001
  maxSubQueryCount: 10,
1002
1002
  maxBulkConcurrency: 10,
1003
- maxHookConcurrency: 10
1003
+ maxHookConcurrency: 10,
1004
+ maxCorrelatedQueries: 100,
1005
+ maxCorrelatedDepth: 5
1004
1006
  };
1005
1007
  var normalizePositiveInteger = (value, fallback) => {
1006
1008
  return Number.isSafeInteger(value) && Number(value) > 0 ? Number(value) : fallback;
@@ -1020,6 +1022,14 @@ var resolveRequestComplexity = (options) => ({
1020
1022
  maxHookConcurrency: normalizePositiveInteger(
1021
1023
  options?.maxHookConcurrency,
1022
1024
  defaultRequestComplexity.maxHookConcurrency
1025
+ ),
1026
+ maxCorrelatedQueries: normalizePositiveInteger(
1027
+ options?.maxCorrelatedQueries,
1028
+ defaultRequestComplexity.maxCorrelatedQueries
1029
+ ),
1030
+ maxCorrelatedDepth: normalizePositiveInteger(
1031
+ options?.maxCorrelatedDepth,
1032
+ defaultRequestComplexity.maxCorrelatedDepth
1023
1033
  )
1024
1034
  });
1025
1035
  function validateRequestComplexity(value, options, scope = "request") {
@@ -1605,7 +1615,463 @@ var getModelRef = (modelName, refPath) => {
1605
1615
  };
1606
1616
 
1607
1617
  // src/services/base.ts
1618
+ var import_utils13 = require("@web-ts-toolkit/utils");
1619
+
1620
+ // src/correlated-includes.ts
1608
1621
  var import_utils12 = require("@web-ts-toolkit/utils");
1622
+ var PARENT_MARKER_KEY = "$parent";
1623
+ var ESCAPE_WRAPPER_KEY = "$escape";
1624
+ var SUBQUERY_PAYLOAD_KEYS = /* @__PURE__ */ new Set(["$$sq", "$$date"]);
1625
+ var MARKER_FORBIDDEN_OPERATOR_KEYS = /* @__PURE__ */ new Set(["$text", "$where", "$comment"]);
1626
+ var LOGICAL_CLAUSE_KEYS = /* @__PURE__ */ new Set(["$and", "$or", "$nor"]);
1627
+ var DANGEROUS_PATH_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
1628
+ var FIELD_SEGMENT_PATTERN = /^[A-Za-z_][A-Za-z0-9_$]*$/;
1629
+ var ARRAY_INDEX_SEGMENT_PATTERN = /^(0|[1-9]\d*)$/;
1630
+ var ownKeys = (value) => Object.keys(value);
1631
+ function isParentRefMarker(value) {
1632
+ return (0, import_utils12.isPlainObject)(value) && ownKeys(value).length === 1 && ownKeys(value)[0] === PARENT_MARKER_KEY;
1633
+ }
1634
+ function isEscapedParentRef(value) {
1635
+ return (0, import_utils12.isPlainObject)(value) && ownKeys(value).length === 1 && ownKeys(value)[0] === ESCAPE_WRAPPER_KEY;
1636
+ }
1637
+ function hasMarkerKey(value) {
1638
+ return ownKeys(value).includes(PARENT_MARKER_KEY) || ownKeys(value).includes(ESCAPE_WRAPPER_KEY);
1639
+ }
1640
+ function isValidParentRefPath(path) {
1641
+ if (typeof path !== "string" || path.length === 0) return false;
1642
+ const segments = path.split(".");
1643
+ return segments.every(
1644
+ (segment) => (FIELD_SEGMENT_PATTERN.test(segment) || ARRAY_INDEX_SEGMENT_PATTERN.test(segment)) && !DANGEROUS_PATH_SEGMENTS.has(segment)
1645
+ );
1646
+ }
1647
+ function isValidCorrelatedOutputPath(path) {
1648
+ return typeof path === "string" && path.length > 0 && isValidFieldPath(path) && path !== "_id" && !path.startsWith("$");
1649
+ }
1650
+ function containsParentMarkerShape(value) {
1651
+ const seen = /* @__PURE__ */ new Set();
1652
+ const visit = (current) => {
1653
+ if (isParentRefMarker(current) || isEscapedParentRef(current)) return true;
1654
+ if ((0, import_utils12.isArray)(current)) {
1655
+ if (seen.has(current)) return false;
1656
+ seen.add(current);
1657
+ return current.some(visit);
1658
+ }
1659
+ if ((0, import_utils12.isPlainObject)(current)) {
1660
+ if (seen.has(current)) return false;
1661
+ seen.add(current);
1662
+ if (hasMarkerKey(current)) return true;
1663
+ return Object.values(current).some(visit);
1664
+ }
1665
+ return false;
1666
+ };
1667
+ return visit(value);
1668
+ }
1669
+ function scanTemplateValueNode(value, path, issues) {
1670
+ if (isEscapedParentRef(value)) {
1671
+ const inner = value.$escape;
1672
+ if (!isParentRefMarker(inner) || typeof inner.$parent !== "string" || (inner.$parent?.length ?? 0) === 0) {
1673
+ issues.push({
1674
+ message: 'Invalid $escape payload: expected { "$escape": { "$parent": "<field>" } }',
1675
+ path
1676
+ });
1677
+ }
1678
+ return;
1679
+ }
1680
+ if (isParentRefMarker(value)) {
1681
+ if (!isValidParentRefPath(value.$parent)) {
1682
+ issues.push({
1683
+ message: `Invalid $parent reference path at ${path.length === 0 ? "#" : path.join(".")}`,
1684
+ path
1685
+ });
1686
+ }
1687
+ return;
1688
+ }
1689
+ if ((0, import_utils12.isPlainObject)(value)) {
1690
+ if (hasMarkerKey(value)) {
1691
+ issues.push({
1692
+ message: 'Malformed $parent reference: objects carrying $parent must be exactly { "$parent": "<field>" }',
1693
+ path
1694
+ });
1695
+ return;
1696
+ }
1697
+ scanTemplateObjectEntries(value, path, issues);
1698
+ return;
1699
+ }
1700
+ if ((0, import_utils12.isArray)(value)) {
1701
+ value.forEach((item, index) => scanTemplateValueNode(item, [...path, index], issues));
1702
+ }
1703
+ }
1704
+ function scanTemplateObjectEntries(obj, basePath, issues) {
1705
+ for (const [key, child] of Object.entries(obj)) {
1706
+ const childPath = [...basePath, key];
1707
+ if (key === PARENT_MARKER_KEY) {
1708
+ issues.push({ message: "$parent is never a field name in correlated templates", path: childPath });
1709
+ continue;
1710
+ }
1711
+ if (SUBQUERY_PAYLOAD_KEYS.has(key)) {
1712
+ if (containsParentMarkerShape(child)) {
1713
+ issues.push({
1714
+ message: `Unsupported $parent reference inside ${key}: markers are not resolved in subquery payloads`,
1715
+ path: childPath
1716
+ });
1717
+ }
1718
+ continue;
1719
+ }
1720
+ if (MARKER_FORBIDDEN_OPERATOR_KEYS.has(key)) {
1721
+ if (containsParentMarkerShape(child)) {
1722
+ issues.push({
1723
+ message: `Unsupported $parent reference inside ${key}: markers are not resolved in ${key} values`,
1724
+ path: childPath
1725
+ });
1726
+ }
1727
+ continue;
1728
+ }
1729
+ if (key === "$elemMatch") {
1730
+ if (isParentRefMarker(child) || isEscapedParentRef(child)) {
1731
+ issues.push({
1732
+ message: "Unsupported $parent reference as a direct $elemMatch value",
1733
+ path: childPath
1734
+ });
1735
+ continue;
1736
+ }
1737
+ scanTemplateValueNode(child, childPath, issues);
1738
+ continue;
1739
+ }
1740
+ if (LOGICAL_CLAUSE_KEYS.has(key)) {
1741
+ if (!(0, import_utils12.isArray)(child)) {
1742
+ scanTemplateValueNode(child, childPath, issues);
1743
+ continue;
1744
+ }
1745
+ child.forEach((clause, index) => {
1746
+ const clausePath = [...childPath, index];
1747
+ if (isParentRefMarker(clause) || isEscapedParentRef(clause)) {
1748
+ issues.push({
1749
+ message: `Unsupported $parent reference as a direct ${key} clause: clauses must be filter objects`,
1750
+ path: clausePath
1751
+ });
1752
+ return;
1753
+ }
1754
+ if ((0, import_utils12.isPlainObject)(clause)) {
1755
+ scanTemplateObjectEntries(clause, clausePath, issues);
1756
+ }
1757
+ });
1758
+ continue;
1759
+ }
1760
+ scanTemplateValueNode(child, childPath, issues);
1761
+ }
1762
+ }
1763
+ function validateCorrelatedFilterTemplate(filter2) {
1764
+ const issues = [];
1765
+ if (!(0, import_utils12.isPlainObject)(filter2)) {
1766
+ issues.push({ message: "Correlated include filter must be an object", path: [] });
1767
+ return issues;
1768
+ }
1769
+ if (isParentRefMarker(filter2) || isEscapedParentRef(filter2)) {
1770
+ issues.push({ message: "Correlated include filter must not itself be a $parent reference", path: [] });
1771
+ return issues;
1772
+ }
1773
+ scanTemplateObjectEntries(filter2, [], issues);
1774
+ return issues;
1775
+ }
1776
+ function validateCorrelatedIdTemplate(id) {
1777
+ if (typeof id === "string") {
1778
+ return id.length > 0 ? [] : [{ message: "Correlated include id must be a non-empty string or $parent reference", path: [] }];
1779
+ }
1780
+ if (isEscapedParentRef(id)) {
1781
+ return [{ message: "Unsupported $escape as a correlated include id", path: [] }];
1782
+ }
1783
+ if (isParentRefMarker(id)) {
1784
+ return isValidParentRefPath(id.$parent) ? [] : [{ message: "Invalid $parent reference path in correlated include id", path: [] }];
1785
+ }
1786
+ return [{ message: "Correlated include id must be a string or $parent reference", path: [] }];
1787
+ }
1788
+ function findArgsMarkerIssue(value, where) {
1789
+ if (value !== void 0 && containsParentMarkerShape(value)) {
1790
+ return {
1791
+ message: `Unsupported $parent reference in ${where}: references are only supported in filter and id positions`,
1792
+ path: []
1793
+ };
1794
+ }
1795
+ return null;
1796
+ }
1797
+ function collectFilterReferencePaths(filter2) {
1798
+ const paths = [];
1799
+ const seen = /* @__PURE__ */ new Set();
1800
+ const visitValue = (value) => {
1801
+ if (isParentRefMarker(value)) {
1802
+ if (typeof value.$parent === "string") {
1803
+ paths.push(value.$parent);
1804
+ }
1805
+ return;
1806
+ }
1807
+ if (isEscapedParentRef(value)) return;
1808
+ if ((0, import_utils12.isArray)(value)) {
1809
+ if (seen.has(value)) return;
1810
+ seen.add(value);
1811
+ value.forEach(visitValue);
1812
+ return;
1813
+ }
1814
+ if ((0, import_utils12.isPlainObject)(value)) {
1815
+ if (seen.has(value)) return;
1816
+ seen.add(value);
1817
+ for (const [key, child] of Object.entries(value)) {
1818
+ if (SUBQUERY_PAYLOAD_KEYS.has(key) || MARKER_FORBIDDEN_OPERATOR_KEYS.has(key)) continue;
1819
+ visitValue(child);
1820
+ }
1821
+ }
1822
+ };
1823
+ visitValue(filter2);
1824
+ return paths;
1825
+ }
1826
+ function collectCorrelatedReferencePaths(include) {
1827
+ const paths = [];
1828
+ if (isParentRefMarker(include.id) && typeof include.id !== "string") {
1829
+ const ref = include.id;
1830
+ if (typeof ref.$parent === "string") paths.push(ref.$parent);
1831
+ } else if (typeof include.id === "string") {
1832
+ }
1833
+ const filter2 = include.filter;
1834
+ if (filter2 !== void 0) {
1835
+ paths.push(...collectFilterReferencePaths(filter2));
1836
+ }
1837
+ return [...new Set(paths)];
1838
+ }
1839
+ var CorrelatedReferenceError = class extends Error {
1840
+ constructor(message) {
1841
+ super(message);
1842
+ this.name = "CorrelatedReferenceError";
1843
+ }
1844
+ };
1845
+ var UNRESOLVABLE = /* @__PURE__ */ Symbol("correlated-include-unresolvable");
1846
+ function getOwnPathValue(parent, path) {
1847
+ const segments = path.split(".");
1848
+ let current = parent;
1849
+ for (const segment of segments) {
1850
+ if (DANGEROUS_PATH_SEGMENTS.has(segment)) {
1851
+ throw new CorrelatedReferenceError(`Invalid $parent reference path: ${path}`);
1852
+ }
1853
+ if (typeof current !== "object" || current === null) return void 0;
1854
+ if (!Object.prototype.hasOwnProperty.call(current, segment)) return void 0;
1855
+ current = current[segment];
1856
+ }
1857
+ return current;
1858
+ }
1859
+ function cloneSubstitutedValue(value) {
1860
+ if ((0, import_utils12.isPlainObject)(value) || (0, import_utils12.isArray)(value)) return (0, import_utils12.cloneDeep)(value);
1861
+ return value;
1862
+ }
1863
+ function literalParentRefObject(path) {
1864
+ const literal = {};
1865
+ Object.defineProperty(literal, PARENT_MARKER_KEY, {
1866
+ value: path,
1867
+ writable: true,
1868
+ enumerable: true,
1869
+ configurable: true
1870
+ });
1871
+ return literal;
1872
+ }
1873
+ function resolveObjectEntries(obj, parent) {
1874
+ const out = {};
1875
+ for (const [key, child] of Object.entries(obj)) {
1876
+ if (key === PARENT_MARKER_KEY) {
1877
+ throw new CorrelatedReferenceError("$parent is never a field name in correlated templates");
1878
+ }
1879
+ if (SUBQUERY_PAYLOAD_KEYS.has(key)) {
1880
+ if (containsParentMarkerShape(child)) {
1881
+ throw new CorrelatedReferenceError(`Unsupported $parent reference inside ${key}`);
1882
+ }
1883
+ out[key] = cloneSubstitutedValue(child);
1884
+ continue;
1885
+ }
1886
+ if (MARKER_FORBIDDEN_OPERATOR_KEYS.has(key)) {
1887
+ if (containsParentMarkerShape(child)) {
1888
+ throw new CorrelatedReferenceError(`Unsupported $parent reference inside ${key}`);
1889
+ }
1890
+ out[key] = cloneSubstitutedValue(child);
1891
+ continue;
1892
+ }
1893
+ if (key === "$elemMatch") {
1894
+ if (isParentRefMarker(child) || isEscapedParentRef(child)) {
1895
+ throw new CorrelatedReferenceError("Unsupported $parent reference as a direct $elemMatch value");
1896
+ }
1897
+ out[key] = resolveValueNode(child, parent, false);
1898
+ continue;
1899
+ }
1900
+ if (LOGICAL_CLAUSE_KEYS.has(key)) {
1901
+ if (!(0, import_utils12.isArray)(child)) {
1902
+ out[key] = resolveValueNode(child, parent, false);
1903
+ continue;
1904
+ }
1905
+ out[key] = child.map((clause) => {
1906
+ if (isParentRefMarker(clause) || isEscapedParentRef(clause)) {
1907
+ throw new CorrelatedReferenceError(`Unsupported $parent reference as a direct ${key} clause`);
1908
+ }
1909
+ if (!(0, import_utils12.isPlainObject)(clause)) return cloneSubstitutedValue(clause);
1910
+ return resolveObjectEntries(clause, parent);
1911
+ });
1912
+ continue;
1913
+ }
1914
+ if (key.startsWith("$")) {
1915
+ out[key] = resolveValueNode(child, parent, false);
1916
+ continue;
1917
+ }
1918
+ out[key] = resolveValueNode(child, parent, true);
1919
+ }
1920
+ return out;
1921
+ }
1922
+ function resolveValueNode(template, parent, bare) {
1923
+ if (isEscapedParentRef(template)) {
1924
+ const inner = template.$escape;
1925
+ if (!isParentRefMarker(inner) || typeof inner.$parent !== "string" || (inner.$parent?.length ?? 0) === 0) {
1926
+ throw new CorrelatedReferenceError('Invalid $escape payload: expected { "$escape": { "$parent": "<field>" } }');
1927
+ }
1928
+ return literalParentRefObject(inner.$parent);
1929
+ }
1930
+ if (isParentRefMarker(template)) {
1931
+ const path = template.$parent;
1932
+ if (!isValidParentRefPath(path)) {
1933
+ throw new CorrelatedReferenceError(`Invalid $parent reference path: ${String(path)}`);
1934
+ }
1935
+ const value = getOwnPathValue(parent, path);
1936
+ if (value === void 0 || value === null) throw UNRESOLVABLE;
1937
+ if (bare && (0, import_utils12.isPlainObject)(value)) return { $eq: (0, import_utils12.cloneDeep)(value) };
1938
+ return cloneSubstitutedValue(value);
1939
+ }
1940
+ if ((0, import_utils12.isPlainObject)(template)) {
1941
+ if (hasMarkerKey(template)) {
1942
+ throw new CorrelatedReferenceError(
1943
+ 'Malformed $parent reference: objects carrying $parent must be exactly { "$parent": "<field>" }'
1944
+ );
1945
+ }
1946
+ return resolveObjectEntries(template, parent);
1947
+ }
1948
+ if ((0, import_utils12.isArray)(template)) {
1949
+ return template.map((item) => resolveValueNode(item, parent, false));
1950
+ }
1951
+ return template;
1952
+ }
1953
+ function isUnresolvableSignal(error2) {
1954
+ return error2 === UNRESOLVABLE;
1955
+ }
1956
+ function resolveCorrelatedIdTemplate(id, parent) {
1957
+ try {
1958
+ if (typeof id === "string") {
1959
+ if (id.length === 0) throw new CorrelatedReferenceError("Correlated include id must be a non-empty string");
1960
+ return { status: "resolved", id };
1961
+ }
1962
+ if (isParentRefMarker(id)) {
1963
+ const path = id.$parent;
1964
+ if (!isValidParentRefPath(path)) {
1965
+ throw new CorrelatedReferenceError(`Invalid $parent reference path: ${String(path)}`);
1966
+ }
1967
+ const value = getOwnPathValue(parent, path);
1968
+ if (typeof value === "string") return { status: "resolved", id: value };
1969
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
1970
+ return { status: "resolved", id: String(value) };
1971
+ }
1972
+ throw UNRESOLVABLE;
1973
+ }
1974
+ throw new CorrelatedReferenceError("Correlated include id must be a string or $parent reference");
1975
+ } catch (error2) {
1976
+ if (isUnresolvableSignal(error2)) return { status: "unresolvable" };
1977
+ throw error2;
1978
+ }
1979
+ }
1980
+ function resolveCorrelatedFilterTemplate(filter2, parent) {
1981
+ try {
1982
+ if (!(0, import_utils12.isPlainObject)(filter2) || isParentRefMarker(filter2) || isEscapedParentRef(filter2)) {
1983
+ throw new CorrelatedReferenceError("Correlated include filter must be an object");
1984
+ }
1985
+ return { status: "resolved", filter: resolveObjectEntries(filter2, parent) };
1986
+ } catch (error2) {
1987
+ if (isUnresolvableSignal(error2)) return { status: "unresolvable" };
1988
+ throw error2;
1989
+ }
1990
+ }
1991
+ function validateExpandedCorrelatedOperands(expanded, limits) {
1992
+ return validateRequestComplexity(expanded, limits ?? null, "filter");
1993
+ }
1994
+ var MAX_NESTED_INCLUDE_DEPTH = 10;
1995
+ function validateCorrelatedArgsShape(args, op, depth) {
1996
+ if (args === void 0) return [];
1997
+ if (!(0, import_utils12.isPlainObject)(args)) return ["Correlated include args must be an object"];
1998
+ const record = args;
1999
+ const allowed = op === "list" ? /* @__PURE__ */ new Set(["select", "sort", "skip", "limit", "page", "pageSize", "include"]) : /* @__PURE__ */ new Set(["select", "sort", "include"]);
2000
+ for (const key of Object.keys(record)) {
2001
+ if (!allowed.has(key)) return [`Unsupported correlated include arg: ${key}`];
2002
+ }
2003
+ if (op === "count" && Object.keys(record).length > 0) return ["Count includes accept no args"];
2004
+ const selectIssue = findArgsMarkerIssue(record.select, "args.select");
2005
+ if (selectIssue) return [selectIssue.message];
2006
+ const sortIssue = findArgsMarkerIssue(record.sort, "args.sort");
2007
+ if (sortIssue) return [sortIssue.message];
2008
+ if (record.include !== void 0) {
2009
+ if (depth >= MAX_NESTED_INCLUDE_DEPTH) return ["Correlated include nesting exceeds the supported depth"];
2010
+ const nested = Array.isArray(record.include) ? record.include : [record.include];
2011
+ for (const entry of nested) {
2012
+ const errors = validateCorrelatedIncludeShape(entry, depth + 1);
2013
+ if (errors.length > 0) return errors;
2014
+ }
2015
+ }
2016
+ return [];
2017
+ }
2018
+ function validateCorrelatedIncludeShape(entry, depth = 0) {
2019
+ if (!(0, import_utils12.isPlainObject)(entry)) return ["Correlated include must be an object"];
2020
+ const record = entry;
2021
+ if (record.mode !== "correlated") return ['Correlated include mode must be "correlated"'];
2022
+ if (typeof record.model !== "string" || record.model.length === 0)
2023
+ return ["Correlated include model must be a non-empty string"];
2024
+ if (record.op !== "list" && record.op !== "read" && record.op !== "count") {
2025
+ return ['Correlated include op must be one of "list", "read", or "count"'];
2026
+ }
2027
+ if (!isValidCorrelatedOutputPath(record.path))
2028
+ return [`Invalid correlated include output path: ${String(record.path)}`];
2029
+ if ("localField" in record || "foreignField" in record) {
2030
+ return ["Correlated includes must not carry localField or foreignField"];
2031
+ }
2032
+ if (record.options !== void 0 && !((0, import_utils12.isPlainObject)(record.options) && Object.keys(record.options).length === 0)) {
2033
+ return ["Correlated include options must be absent or empty"];
2034
+ }
2035
+ const hasId = record.id !== void 0;
2036
+ const hasFilter = record.filter !== void 0;
2037
+ if (record.op === "read") {
2038
+ if (hasId === hasFilter) return ['Correlated read includes require exactly one of "id" or "filter"'];
2039
+ if (hasId) {
2040
+ const issues = validateCorrelatedIdTemplate(record.id);
2041
+ if (issues.length > 0) return [issues[0].message];
2042
+ } else {
2043
+ const issues = validateCorrelatedFilterTemplate(record.filter);
2044
+ if (issues.length > 0) return [issues[0].message];
2045
+ }
2046
+ } else {
2047
+ if (!hasFilter || hasId) return [`Correlated ${record.op} includes require a filter and no id`];
2048
+ const issues = validateCorrelatedFilterTemplate(record.filter);
2049
+ if (issues.length > 0) return [issues[0].message];
2050
+ }
2051
+ return validateCorrelatedArgsShape(record.args, record.op, depth);
2052
+ }
2053
+ function isCorrelatedInclude(value) {
2054
+ return (0, import_utils12.isPlainObject)(value) && value.mode === "correlated";
2055
+ }
2056
+
2057
+ // src/services/base.ts
2058
+ var correlatedExecStates = /* @__PURE__ */ new WeakMap();
2059
+ function getCorrelatedTemplateDepth(includes) {
2060
+ let max = 0;
2061
+ const visit = (entries, depth) => {
2062
+ const list = (0, import_utils13.castArray)(entries).filter(Boolean);
2063
+ for (const entry of list) {
2064
+ if (!(0, import_utils13.isPlainObject)(entry)) continue;
2065
+ max = Math.max(max, depth);
2066
+ const nested = entry.args?.include;
2067
+ if (nested !== void 0) {
2068
+ visit(nested, depth + 1);
2069
+ }
2070
+ }
2071
+ };
2072
+ if (includes.length > 0) visit(includes, 1);
2073
+ return max;
2074
+ }
1609
2075
  var ClientRequestError = class extends Error {
1610
2076
  constructor(result) {
1611
2077
  super(String(result.code));
@@ -1620,11 +2086,11 @@ function validateClientFilter(filter2) {
1620
2086
  }
1621
2087
  const blockedOperators = /* @__PURE__ */ new Set(["$where", "$expr", "$function", "$accumulator"]);
1622
2088
  const visit = (value, path) => {
1623
- if ((0, import_utils12.isArray)(value)) {
2089
+ if ((0, import_utils13.isArray)(value)) {
1624
2090
  value.forEach((item, index) => visit(item, `${path}[${index}]`));
1625
2091
  return;
1626
2092
  }
1627
- if (!(0, import_utils12.isPlainObject)(value)) return;
2093
+ if (!(0, import_utils13.isPlainObject)(value)) return;
1628
2094
  Object.entries(value).forEach(([key, child]) => {
1629
2095
  const nextPath = path ? `${path}.${key}` : key;
1630
2096
  if (blockedOperators.has(key)) {
@@ -1744,25 +2210,56 @@ var Base = class {
1744
2210
  return this.req.macl.getPublicService(modelName);
1745
2211
  }
1746
2212
  processInclude(include) {
1747
- const includes = (0, import_utils12.compact)((0, import_utils12.castArray)(include)).filter(({ model, op, path, localField, foreignField }) => {
1748
- return model && op && path && localField && foreignField;
1749
- });
2213
+ const items = (0, import_utils13.compact)((0, import_utils13.castArray)(include));
2214
+ const legacy = [];
2215
+ const correlated = [];
2216
+ const correlatedReferenceFields = [];
2217
+ for (const entry of items) {
2218
+ if (isCorrelatedInclude(entry)) {
2219
+ const shapeErrors = validateCorrelatedIncludeShape(entry);
2220
+ if (shapeErrors.length > 0) {
2221
+ this.throwClientRequestError("bad_request" /* BadRequest */, shapeErrors[0]);
2222
+ }
2223
+ correlated.push(entry);
2224
+ correlatedReferenceFields.push(...collectCorrelatedReferencePaths(entry));
2225
+ continue;
2226
+ }
2227
+ if (containsParentMarkerShape(entry)) {
2228
+ this.throwClientRequestError(
2229
+ "bad_request" /* BadRequest */,
2230
+ 'Legacy includes must not contain $parent markers; use a correlated include (mode: "correlated")'
2231
+ );
2232
+ }
2233
+ const candidate = entry;
2234
+ if (candidate.model && candidate.op && candidate.path && candidate.localField && candidate.foreignField) {
2235
+ legacy.push(entry);
2236
+ }
2237
+ }
2238
+ const seenCorrelatedPaths = /* @__PURE__ */ new Set();
2239
+ for (const entry of correlated) {
2240
+ if (seenCorrelatedPaths.has(entry.path)) {
2241
+ this.throwClientRequestError("bad_request" /* BadRequest */, `Duplicate correlated include output path: ${entry.path}`);
2242
+ }
2243
+ seenCorrelatedPaths.add(entry.path);
2244
+ }
1750
2245
  let includeLocalFields = [];
1751
2246
  let includePaths = [];
1752
- (0, import_utils12.forEach)(includes, (inc) => {
2247
+ (0, import_utils13.forEach)(legacy, (inc) => {
1753
2248
  includeLocalFields.push(inc.localField);
1754
2249
  includePaths.push(inc.path);
1755
2250
  });
1756
- includeLocalFields = (0, import_utils12.uniq)((0, import_utils12.compact)(includeLocalFields));
1757
- includePaths = (0, import_utils12.uniq)((0, import_utils12.compact)(includePaths));
2251
+ includeLocalFields = (0, import_utils13.uniq)((0, import_utils13.compact)(includeLocalFields));
2252
+ includePaths = (0, import_utils13.uniq)((0, import_utils13.compact)(includePaths));
1758
2253
  return {
1759
- includes,
2254
+ includes: legacy,
2255
+ correlatedIncludes: correlated,
1760
2256
  includeLocalFields,
1761
- includePaths
2257
+ includePaths,
2258
+ correlatedReferenceFields: (0, import_utils13.uniq)((0, import_utils13.compact)(correlatedReferenceFields))
1762
2259
  };
1763
2260
  }
1764
2261
  getForeignKeyValues(value) {
1765
- return (0, import_utils12.flatten)((0, import_utils12.castArray)(value));
2262
+ return (0, import_utils13.flatten)((0, import_utils13.castArray)(value));
1766
2263
  }
1767
2264
  getForeignKey(value) {
1768
2265
  return String(value);
@@ -1770,7 +2267,7 @@ var Base = class {
1770
2267
  buildForeignKeyIndex(rows, foreignField) {
1771
2268
  const index = /* @__PURE__ */ new Map();
1772
2269
  for (const row of rows) {
1773
- for (const value of this.getForeignKeyValues((0, import_utils12.get)(row, foreignField))) {
2270
+ for (const value of this.getForeignKeyValues((0, import_utils13.get)(row, foreignField))) {
1774
2271
  const key = this.getForeignKey(value);
1775
2272
  const values = index.get(key);
1776
2273
  if (values) values.push(row);
@@ -1801,7 +2298,7 @@ var Base = class {
1801
2298
  return ids.size;
1802
2299
  }
1803
2300
  sanitizeIncludeArgs(args) {
1804
- if (!(0, import_utils12.isPlainObject)(args)) return {};
2301
+ if (!(0, import_utils13.isPlainObject)(args)) return {};
1805
2302
  const { overrides: _clientOverrides, ...trustedArgs } = args;
1806
2303
  return trustedArgs;
1807
2304
  }
@@ -1812,12 +2309,18 @@ var Base = class {
1812
2309
  }
1813
2310
  async includeDocs(docs, include) {
1814
2311
  if (!include) return docs;
1815
- const includes = (0, import_utils12.compact)((0, import_utils12.castArray)(include));
2312
+ const includes = (0, import_utils13.compact)((0, import_utils13.castArray)(include));
1816
2313
  if (includes.length === 0) return docs;
1817
- const isSingle = !(0, import_utils12.isArray)(docs);
2314
+ const isSingle = !(0, import_utils13.isArray)(docs);
1818
2315
  let docList = isSingle ? [docs] : docs;
1819
2316
  for (let x = 0; x < includes.length; x++) {
1820
2317
  const include2 = includes[x];
2318
+ if (isCorrelatedInclude(include2)) {
2319
+ this.throwClientRequestError(
2320
+ "bad_request" /* BadRequest */,
2321
+ "Correlated includes are not supported by the legacy include executor"
2322
+ );
2323
+ }
1821
2324
  this.assertIncludeForeignField(include2);
1822
2325
  switch (include2.op) {
1823
2326
  case "count":
@@ -1838,7 +2341,7 @@ var Base = class {
1838
2341
  const svc = await this.getAuthorizedTargetService(model, "read");
1839
2342
  for (let y = 0; y < docs.length; y++) {
1840
2343
  const doc = docs[y];
1841
- const localValue = (0, import_utils12.get)(doc, localField);
2344
+ const localValue = (0, import_utils13.get)(doc, localField);
1842
2345
  const filter2 = { ..._filters ?? {}, [foreignField]: { $in: this.getForeignKeyValues(localValue) } };
1843
2346
  const trustedOptions = {
1844
2347
  ...options,
@@ -1857,10 +2360,10 @@ var Base = class {
1857
2360
  const { model, op, path, localField, foreignField, filter: _filters, args = {}, options = {} } = include;
1858
2361
  const svc = await this.getAuthorizedTargetService(model, op);
1859
2362
  const includeLocalValues = [];
1860
- (0, import_utils12.forEach)(docs, (doc) => {
1861
- includeLocalValues.push(...this.getForeignKeyValues((0, import_utils12.get)(doc, localField)));
2363
+ (0, import_utils13.forEach)(docs, (doc) => {
2364
+ includeLocalValues.push(...this.getForeignKeyValues((0, import_utils13.get)(doc, localField)));
1862
2365
  });
1863
- const filter2 = { ..._filters ?? {}, [foreignField]: { $in: (0, import_utils12.flatten)(includeLocalValues) } };
2366
+ const filter2 = { ..._filters ?? {}, [foreignField]: { $in: (0, import_utils13.flatten)(includeLocalValues) } };
1864
2367
  const authorizedFilter = await svc.genFilter(op, filter2);
1865
2368
  const trustedArgs = {
1866
2369
  ...this.sanitizeIncludeArgs(args),
@@ -1879,7 +2382,7 @@ var Base = class {
1879
2382
  const index = this.buildForeignKeyIndex(trustedResult.data, foreignField);
1880
2383
  for (let y = 0; y < docs.length; y++) {
1881
2384
  const doc = docs[y];
1882
- const matches = this.getIndexedMatches(index, (0, import_utils12.get)(doc, localField));
2385
+ const matches = this.getIndexedMatches(index, (0, import_utils13.get)(doc, localField));
1883
2386
  setDocValue(doc, path, op === "list" ? matches : matches[0]);
1884
2387
  }
1885
2388
  return docs;
@@ -1887,15 +2390,365 @@ var Base = class {
1887
2390
  async includeDocsCount(docs, include) {
1888
2391
  const { model, path, localField, foreignField, filter: _filters } = include;
1889
2392
  const svc = await this.getAuthorizedTargetService(model, "count");
1890
- const localValues = docs.flatMap((doc) => this.getForeignKeyValues((0, import_utils12.get)(doc, localField)));
2393
+ const localValues = docs.flatMap((doc) => this.getForeignKeyValues((0, import_utils13.get)(doc, localField)));
1891
2394
  const result = await svc.countByFieldValues(foreignField, localValues, _filters ?? {}, "count");
1892
2395
  if (!result.success) return docs;
1893
2396
  for (let y = 0; y < docs.length; y++) {
1894
2397
  const doc = docs[y];
1895
- setDocValue(doc, path, this.getIndexedCount(result.data, (0, import_utils12.get)(doc, localField)));
2398
+ setDocValue(doc, path, this.getIndexedCount(result.data, (0, import_utils13.get)(doc, localField)));
2399
+ }
2400
+ return docs;
2401
+ }
2402
+ getCorrelatedExecState() {
2403
+ const key = this.req;
2404
+ let state = correlatedExecStates.get(key);
2405
+ if (!state) {
2406
+ state = {
2407
+ totalQueries: 0,
2408
+ scheduler: new RequestConcurrencyScheduler(this.getRequestComplexity().maxBulkConcurrency)
2409
+ };
2410
+ correlatedExecStates.set(key, state);
2411
+ }
2412
+ return state;
2413
+ }
2414
+ claimCorrelatedQuerySlot() {
2415
+ const limits = this.getRequestComplexity();
2416
+ const state = this.getCorrelatedExecState();
2417
+ if (state.totalQueries >= limits.maxCorrelatedQueries) {
2418
+ this.throwClientRequestError(
2419
+ "bad_request" /* BadRequest */,
2420
+ `Correlated include query budget exceeded (max ${limits.maxCorrelatedQueries})`
2421
+ );
2422
+ }
2423
+ state.totalQueries += 1;
2424
+ }
2425
+ sanitizeCorrelatedArgs(include) {
2426
+ const args = include.args ?? {};
2427
+ if (include.op === "list") {
2428
+ const { select: select2, sort: sort2, skip, limit, page, pageSize, include: nestedInclude2 } = args;
2429
+ return { select: select2, sort: sort2, skip, limit, page, pageSize, nestedInclude: nestedInclude2 };
2430
+ }
2431
+ const { select, sort, include: nestedInclude } = args;
2432
+ return { select, sort, nestedInclude };
2433
+ }
2434
+ async includeCorrelatedDocs(docs, correlatedIncludes, snapshots, depth = 0) {
2435
+ if (correlatedIncludes.length === 0) return docs;
2436
+ const limits = this.getRequestComplexity();
2437
+ const templateDepth = getCorrelatedTemplateDepth(correlatedIncludes);
2438
+ if (depth + templateDepth > limits.maxCorrelatedDepth) {
2439
+ this.throwClientRequestError(
2440
+ "bad_request" /* BadRequest */,
2441
+ `Correlated include depth exceeded (max ${limits.maxCorrelatedDepth})`
2442
+ );
2443
+ }
2444
+ const state = this.getCorrelatedExecState();
2445
+ const resolvedSnapshots = snapshots ?? docs.map((doc) => (0, import_utils13.cloneDeep)(toObject(doc)));
2446
+ for (const include of correlatedIncludes) {
2447
+ switch (include.op) {
2448
+ case "read":
2449
+ await this.includeCorrelatedRead(docs, include, resolvedSnapshots, state, depth);
2450
+ break;
2451
+ case "list":
2452
+ await this.includeCorrelatedList(docs, include, resolvedSnapshots, state, depth);
2453
+ break;
2454
+ case "count":
2455
+ await this.includeCorrelatedCount(docs, include, resolvedSnapshots, state, depth);
2456
+ break;
2457
+ default:
2458
+ this.throwClientRequestError(
2459
+ "bad_request" /* BadRequest */,
2460
+ `Unsupported correlated include op: ${include.op}`
2461
+ );
2462
+ }
1896
2463
  }
1897
2464
  return docs;
1898
2465
  }
2466
+ async includeCorrelatedRead(docs, include, snapshots, state, depth) {
2467
+ const svc = await this.getAuthorizedTargetService(include.model, "read");
2468
+ const { select, sort, nestedInclude } = this.sanitizeCorrelatedArgs(include);
2469
+ const hasId = include.id !== void 0;
2470
+ if (hasId) {
2471
+ const idTemplate = include.id;
2472
+ const tasks2 = docs.map((_, index) => ({ index, snapshot: snapshots[index] }));
2473
+ await state.scheduler.map(tasks2, async ({ index, snapshot }) => {
2474
+ let resolution;
2475
+ try {
2476
+ resolution = resolveCorrelatedIdTemplate(idTemplate, snapshot);
2477
+ } catch (error2) {
2478
+ if (error2 instanceof CorrelatedReferenceError) {
2479
+ throw new ClientRequestError({
2480
+ success: false,
2481
+ kind: "error",
2482
+ code: "bad_request" /* BadRequest */,
2483
+ errors: [{ detail: error2.message }]
2484
+ });
2485
+ }
2486
+ throw error2;
2487
+ }
2488
+ if (resolution.status === "unresolvable") {
2489
+ setDocValue(docs[index], include.path, null);
2490
+ return;
2491
+ }
2492
+ const resolvedId = resolution.id;
2493
+ let idFilter;
2494
+ try {
2495
+ idFilter = await svc.genIDFilter(resolvedId);
2496
+ } catch (error2) {
2497
+ const clientResult = this.getClientRequestErrorResult(error2);
2498
+ if (clientResult) throw new ClientRequestError(clientResult);
2499
+ throw error2;
2500
+ }
2501
+ const authorized = await svc.genFilter("read", idFilter);
2502
+ if (authorized === false) {
2503
+ throw new ClientRequestError({ success: false, kind: "error", code: "forbidden" /* Forbidden */ });
2504
+ }
2505
+ this.claimCorrelatedQuerySlot();
2506
+ let result;
2507
+ try {
2508
+ result = await svc.findOne(
2509
+ {},
2510
+ {
2511
+ select,
2512
+ sort,
2513
+ include: nestedInclude,
2514
+ overrides: { filter: authorized }
2515
+ },
2516
+ { access: "read", lean: true, includePermissions: false }
2517
+ );
2518
+ } catch (error2) {
2519
+ const clientResult = this.getClientRequestErrorResult(error2);
2520
+ if (clientResult) throw new ClientRequestError(clientResult);
2521
+ throw error2;
2522
+ }
2523
+ if (result.success) {
2524
+ setDocValue(docs[index], include.path, result.data);
2525
+ } else if (result.code === "not_found" /* NotFound */) {
2526
+ setDocValue(docs[index], include.path, null);
2527
+ } else {
2528
+ throw new ClientRequestError(result);
2529
+ }
2530
+ });
2531
+ return;
2532
+ }
2533
+ const filterTemplate = include.filter;
2534
+ const templateErrors = this.validateClientFilter(filterTemplate);
2535
+ if (templateErrors.length > 0) {
2536
+ this.throwClientRequestError("bad_request" /* BadRequest */, templateErrors[0]);
2537
+ }
2538
+ let parsedTemplate;
2539
+ try {
2540
+ parsedTemplate = await this.parseClientData(
2541
+ filterTemplate,
2542
+ state.scheduler
2543
+ );
2544
+ } catch (error2) {
2545
+ const clientResult = this.getClientRequestErrorResult(error2);
2546
+ if (clientResult) throw new ClientRequestError(clientResult);
2547
+ throw error2;
2548
+ }
2549
+ const tasks = docs.map((_, index) => ({ index, snapshot: snapshots[index] }));
2550
+ await state.scheduler.map(tasks, async ({ index, snapshot }) => {
2551
+ let resolution;
2552
+ try {
2553
+ resolution = resolveCorrelatedFilterTemplate(parsedTemplate, snapshot);
2554
+ } catch (error2) {
2555
+ if (error2 instanceof CorrelatedReferenceError) {
2556
+ throw new ClientRequestError({
2557
+ success: false,
2558
+ kind: "error",
2559
+ code: "bad_request" /* BadRequest */,
2560
+ errors: [{ detail: error2.message }]
2561
+ });
2562
+ }
2563
+ throw error2;
2564
+ }
2565
+ if (resolution.status === "unresolvable") {
2566
+ setDocValue(docs[index], include.path, null);
2567
+ return;
2568
+ }
2569
+ const expanded = resolution.filter;
2570
+ const operandErrors = validateExpandedCorrelatedOperands(expanded, this.getRequestComplexity());
2571
+ if (operandErrors.length > 0) {
2572
+ throw new ClientRequestError({
2573
+ success: false,
2574
+ kind: "error",
2575
+ code: "bad_request" /* BadRequest */,
2576
+ errors: [{ detail: operandErrors[0].detail }]
2577
+ });
2578
+ }
2579
+ const authorized = await svc.genFilter("read", expanded);
2580
+ if (authorized === false) {
2581
+ throw new ClientRequestError({ success: false, kind: "error", code: "forbidden" /* Forbidden */ });
2582
+ }
2583
+ this.claimCorrelatedQuerySlot();
2584
+ let result;
2585
+ try {
2586
+ result = await svc.findOne(
2587
+ {},
2588
+ {
2589
+ select,
2590
+ sort,
2591
+ include: nestedInclude,
2592
+ overrides: { filter: authorized }
2593
+ },
2594
+ { access: "read", lean: true, includePermissions: false }
2595
+ );
2596
+ } catch (error2) {
2597
+ const clientResult = this.getClientRequestErrorResult(error2);
2598
+ if (clientResult) throw new ClientRequestError(clientResult);
2599
+ throw error2;
2600
+ }
2601
+ if (result.success) {
2602
+ setDocValue(docs[index], include.path, result.data);
2603
+ } else if (result.code === "not_found" /* NotFound */) {
2604
+ setDocValue(docs[index], include.path, null);
2605
+ } else {
2606
+ throw new ClientRequestError(result);
2607
+ }
2608
+ });
2609
+ void depth;
2610
+ }
2611
+ async includeCorrelatedList(docs, include, snapshots, state, depth) {
2612
+ const svc = await this.getAuthorizedTargetService(include.model, "list");
2613
+ const { select, sort, skip, limit, page, pageSize, nestedInclude } = this.sanitizeCorrelatedArgs(include);
2614
+ const filterTemplate = include.filter;
2615
+ const templateErrors = this.validateClientFilter(filterTemplate);
2616
+ if (templateErrors.length > 0) {
2617
+ this.throwClientRequestError("bad_request" /* BadRequest */, templateErrors[0]);
2618
+ }
2619
+ let parsedTemplate;
2620
+ try {
2621
+ parsedTemplate = await this.parseClientData(
2622
+ filterTemplate,
2623
+ state.scheduler
2624
+ );
2625
+ } catch (error2) {
2626
+ const clientResult = this.getClientRequestErrorResult(error2);
2627
+ if (clientResult) throw new ClientRequestError(clientResult);
2628
+ throw error2;
2629
+ }
2630
+ const tasks = docs.map((_, index) => ({ index, snapshot: snapshots[index] }));
2631
+ await state.scheduler.map(tasks, async ({ index, snapshot }) => {
2632
+ let resolution;
2633
+ try {
2634
+ resolution = resolveCorrelatedFilterTemplate(parsedTemplate, snapshot);
2635
+ } catch (error2) {
2636
+ if (error2 instanceof CorrelatedReferenceError) {
2637
+ throw new ClientRequestError({
2638
+ success: false,
2639
+ kind: "error",
2640
+ code: "bad_request" /* BadRequest */,
2641
+ errors: [{ detail: error2.message }]
2642
+ });
2643
+ }
2644
+ throw error2;
2645
+ }
2646
+ if (resolution.status === "unresolvable") {
2647
+ setDocValue(docs[index], include.path, []);
2648
+ return;
2649
+ }
2650
+ const expanded = resolution.filter;
2651
+ const operandErrors = validateExpandedCorrelatedOperands(expanded, this.getRequestComplexity());
2652
+ if (operandErrors.length > 0) {
2653
+ throw new ClientRequestError({
2654
+ success: false,
2655
+ kind: "error",
2656
+ code: "bad_request" /* BadRequest */,
2657
+ errors: [{ detail: operandErrors[0].detail }]
2658
+ });
2659
+ }
2660
+ const authorized = await svc.genFilter("list", expanded);
2661
+ if (authorized === false) {
2662
+ throw new ClientRequestError({ success: false, kind: "error", code: "forbidden" /* Forbidden */ });
2663
+ }
2664
+ this.claimCorrelatedQuerySlot();
2665
+ let result;
2666
+ try {
2667
+ result = await svc.find(
2668
+ {},
2669
+ {
2670
+ select,
2671
+ sort,
2672
+ skip,
2673
+ limit,
2674
+ page,
2675
+ pageSize,
2676
+ include: nestedInclude,
2677
+ overrides: { filter: authorized }
2678
+ },
2679
+ { includeCount: false, lean: true, includePermissions: false }
2680
+ );
2681
+ } catch (error2) {
2682
+ const clientResult = this.getClientRequestErrorResult(error2);
2683
+ if (clientResult) throw new ClientRequestError(clientResult);
2684
+ throw error2;
2685
+ }
2686
+ if (result.success) {
2687
+ setDocValue(docs[index], include.path, result.data);
2688
+ } else {
2689
+ throw new ClientRequestError(result);
2690
+ }
2691
+ });
2692
+ void depth;
2693
+ }
2694
+ async includeCorrelatedCount(docs, include, snapshots, state, depth) {
2695
+ const svc = await this.getAuthorizedTargetService(include.model, "count");
2696
+ const filterTemplate = include.filter;
2697
+ const templateErrors = this.validateClientFilter(filterTemplate);
2698
+ if (templateErrors.length > 0) {
2699
+ this.throwClientRequestError("bad_request" /* BadRequest */, templateErrors[0]);
2700
+ }
2701
+ const tasks = docs.map((_, index) => ({ index, snapshot: snapshots[index] }));
2702
+ await state.scheduler.map(tasks, async ({ index, snapshot }) => {
2703
+ let resolution;
2704
+ try {
2705
+ resolution = resolveCorrelatedFilterTemplate(filterTemplate, snapshot);
2706
+ } catch (error2) {
2707
+ if (error2 instanceof CorrelatedReferenceError) {
2708
+ throw new ClientRequestError({
2709
+ success: false,
2710
+ kind: "error",
2711
+ code: "bad_request" /* BadRequest */,
2712
+ errors: [{ detail: error2.message }]
2713
+ });
2714
+ }
2715
+ throw error2;
2716
+ }
2717
+ if (resolution.status === "unresolvable") {
2718
+ setDocValue(docs[index], include.path, 0);
2719
+ return;
2720
+ }
2721
+ const expanded = resolution.filter;
2722
+ const operandErrors = validateExpandedCorrelatedOperands(expanded, this.getRequestComplexity());
2723
+ if (operandErrors.length > 0) {
2724
+ throw new ClientRequestError({
2725
+ success: false,
2726
+ kind: "error",
2727
+ code: "bad_request" /* BadRequest */,
2728
+ errors: [{ detail: operandErrors[0].detail }]
2729
+ });
2730
+ }
2731
+ const authorized = await svc.genFilter("count", expanded);
2732
+ if (authorized === false) {
2733
+ throw new ClientRequestError({ success: false, kind: "error", code: "forbidden" /* Forbidden */ });
2734
+ }
2735
+ this.claimCorrelatedQuerySlot();
2736
+ let result;
2737
+ try {
2738
+ result = await svc.countTrusted(authorized);
2739
+ } catch (error2) {
2740
+ const clientResult = this.getClientRequestErrorResult(error2);
2741
+ if (clientResult) throw new ClientRequestError(clientResult);
2742
+ throw error2;
2743
+ }
2744
+ if (result.success) {
2745
+ setDocValue(docs[index], include.path, result.data);
2746
+ } else {
2747
+ throw new ClientRequestError(result);
2748
+ }
2749
+ });
2750
+ void depth;
2751
+ }
1899
2752
  async parseClientData(filter2, scheduler = new RequestConcurrencyScheduler(this.getRequestComplexity().maxBulkConcurrency), scheduled = false) {
1900
2753
  const result = await iterateQuery(
1901
2754
  filter2,
@@ -1938,10 +2791,10 @@ var Base = class {
1938
2791
  }
1939
2792
  let ret = result.data;
1940
2793
  if (sqOptions.path) {
1941
- ret = (0, import_utils12.isArray)(ret) ? (0, import_utils12.flatten)(ret.map((v) => (0, import_utils12.get)(v, sqOptions.path))) : (0, import_utils12.get)(ret, sqOptions.path);
2794
+ ret = (0, import_utils13.isArray)(ret) ? (0, import_utils13.flatten)(ret.map((v) => (0, import_utils13.get)(v, sqOptions.path))) : (0, import_utils13.get)(ret, sqOptions.path);
1942
2795
  }
1943
2796
  if (sqOptions.compact) {
1944
- ret = (0, import_utils12.compact)((0, import_utils12.castArray)(ret));
2797
+ ret = (0, import_utils13.compact)((0, import_utils13.castArray)(ret));
1945
2798
  }
1946
2799
  return ret;
1947
2800
  }
@@ -1953,7 +2806,7 @@ var Base = class {
1953
2806
  };
1954
2807
 
1955
2808
  // src/services/service.ts
1956
- var import_utils14 = require("@web-ts-toolkit/utils");
2809
+ var import_utils15 = require("@web-ts-toolkit/utils");
1957
2810
  var import_just_diff = require("just-diff");
1958
2811
 
1959
2812
  // src/model.ts
@@ -2197,28 +3050,28 @@ var error = (error2) => {
2197
3050
  };
2198
3051
 
2199
3052
  // src/services/model-subdocument-service.ts
2200
- var import_utils13 = require("@web-ts-toolkit/utils");
3053
+ var import_utils14 = require("@web-ts-toolkit/utils");
2201
3054
  async function listSub(service, id, sub, options) {
2202
3055
  const { filter: ft, select } = options ?? {};
2203
3056
  const filterErrors = validateClientFilter(ft);
2204
3057
  if (filterErrors.length > 0) return { success: false, kind: "error", code: "bad_request" /* BadRequest */, errors: filterErrors };
2205
3058
  const parentDoc = await getParentDoc(service, id, sub, null, { access: "read" });
2206
3059
  if (!parentDoc) return { success: false, kind: "error", code: "not_found" /* NotFound */ };
2207
- let result = (0, import_utils13.get)(parentDoc, sub);
3060
+ let result = (0, import_utils14.get)(parentDoc, sub);
2208
3061
  const [subFilter, subSelect] = await Promise.all([
2209
3062
  service.genFilter(`subs.${sub}.list`, ft),
2210
3063
  service.genQuerySelect("list", select, false, [sub, "sub"])
2211
3064
  ]);
2212
3065
  if (subFilter === false) return { success: false, kind: "error", code: "forbidden" /* Forbidden */ };
2213
3066
  result = filterCollection(result, subFilter);
2214
- if (subSelect) result = result.map((v) => (0, import_utils13.pick)(toObject(v), subSelect.concat("_id")));
3067
+ if (subSelect) result = result.map((v) => (0, import_utils14.pick)(toObject(v), subSelect.concat("_id")));
2215
3068
  return { success: true, kind: "list", code: "success" /* Success */, data: result, count: result.length };
2216
3069
  }
2217
3070
  async function readSub(service, id, sub, subId, options) {
2218
3071
  const { select, populate } = options ?? {};
2219
3072
  const parentDoc = await getParentDoc(service, id, sub, { populate }, { access: "read" });
2220
3073
  if (!parentDoc) return { success: false, kind: "error", code: "not_found" /* NotFound */ };
2221
- const result = (0, import_utils13.get)(parentDoc, sub);
3074
+ const result = (0, import_utils14.get)(parentDoc, sub);
2222
3075
  const [subFilter, subSelect] = await Promise.all([
2223
3076
  service.genFilter(`subs.${sub}.read`, { _id: subId }),
2224
3077
  service.genQuerySelect("read", select, false, [sub, "sub"])
@@ -2226,13 +3079,13 @@ async function readSub(service, id, sub, subId, options) {
2226
3079
  if (subFilter === false) return { success: false, kind: "error", code: "forbidden" /* Forbidden */ };
2227
3080
  let subdoc = findElement(result, subFilter);
2228
3081
  if (!subdoc) return { success: false, kind: "error", code: "not_found" /* NotFound */ };
2229
- if (subSelect) subdoc = (0, import_utils13.pick)(toObject(subdoc), subSelect.concat(["_id"]));
3082
+ if (subSelect) subdoc = (0, import_utils14.pick)(toObject(subdoc), subSelect.concat(["_id"]));
2230
3083
  return { success: true, kind: "single", code: "success" /* Success */, data: subdoc };
2231
3084
  }
2232
3085
  async function updateSub(service, id, sub, subId, data) {
2233
3086
  const parentDoc = await getParentDoc(service, id, sub, null, { access: "update" });
2234
3087
  if (!parentDoc) return { success: false, kind: "error", code: "not_found" /* NotFound */ };
2235
- const result = (0, import_utils13.get)(parentDoc, sub);
3088
+ const result = (0, import_utils14.get)(parentDoc, sub);
2236
3089
  const [subFilter, subReadSelect, subUpdateSelect] = await Promise.all([
2237
3090
  service.genFilter(`subs.${sub}.update`, { _id: subId }),
2238
3091
  service.genQuerySelect("read", null, false, [sub, "sub"]),
@@ -2241,10 +3094,10 @@ async function updateSub(service, id, sub, subId, data) {
2241
3094
  if (subFilter === false) return { success: false, kind: "error", code: "forbidden" /* Forbidden */ };
2242
3095
  let subdoc = findElement(result, subFilter);
2243
3096
  if (!subdoc) return { success: false, kind: "error", code: "not_found" /* NotFound */ };
2244
- const allowedData = (0, import_utils13.pick)(data, subUpdateSelect);
3097
+ const allowedData = (0, import_utils14.pick)(data, subUpdateSelect);
2245
3098
  Object.assign(subdoc, allowedData);
2246
3099
  await parentDoc.save();
2247
- if (subReadSelect) subdoc = (0, import_utils13.pick)(toObject(subdoc), subReadSelect.concat(["_id"]));
3100
+ if (subReadSelect) subdoc = (0, import_utils14.pick)(toObject(subdoc), subReadSelect.concat(["_id"]));
2248
3101
  return { success: true, kind: "single", code: "success" /* Success */, data: subdoc };
2249
3102
  }
2250
3103
  async function bulkUpdateSub(service, id, sub, data) {
@@ -2259,7 +3112,7 @@ async function bulkUpdateSub(service, id, sub, data) {
2259
3112
  }
2260
3113
  const parentDoc = await getParentDoc(service, id, sub, null, { access: "update" });
2261
3114
  if (!parentDoc) return { success: false, kind: "error", code: "not_found" /* NotFound */ };
2262
- let result = (0, import_utils13.get)(parentDoc, sub);
3115
+ let result = (0, import_utils14.get)(parentDoc, sub);
2263
3116
  const [subFilter, subReadSelect, subUpdateSelect] = await Promise.all([
2264
3117
  service.genFilter(`subs.${sub}.update`, { _id: { $in: data.map((v) => v._id) } }),
2265
3118
  service.genQuerySelect("read", null, false, [sub, "sub"]),
@@ -2267,14 +3120,14 @@ async function bulkUpdateSub(service, id, sub, data) {
2267
3120
  ]);
2268
3121
  if (subFilter === false) return { success: false, kind: "error", code: "forbidden" /* Forbidden */ };
2269
3122
  result = filterCollection(result, subFilter);
2270
- (0, import_utils13.forEach)(result, (subdoc) => {
3123
+ (0, import_utils14.forEach)(result, (subdoc) => {
2271
3124
  const tdata = findElementById(data, subdoc._id);
2272
3125
  if (!tdata) return;
2273
- const allowedData = (0, import_utils13.pick)(tdata, subUpdateSelect);
3126
+ const allowedData = (0, import_utils14.pick)(tdata, subUpdateSelect);
2274
3127
  Object.assign(subdoc, allowedData);
2275
3128
  });
2276
3129
  await parentDoc.save();
2277
- if (subReadSelect) result = result.map((v) => (0, import_utils13.pick)(toObject(v), subReadSelect.concat(["_id"])));
3130
+ if (subReadSelect) result = result.map((v) => (0, import_utils14.pick)(toObject(v), subReadSelect.concat(["_id"])));
2278
3131
  return { success: true, kind: "list", code: "success" /* Success */, data: result, count: result.length };
2279
3132
  }
2280
3133
  async function createSub(service, id, sub, data, options) {
@@ -2290,25 +3143,25 @@ async function createSub(service, id, sub, data, options) {
2290
3143
  }
2291
3144
  const parentDoc = await getParentDoc(service, id, sub, null, { access: "update" });
2292
3145
  if (!parentDoc) return { success: false, kind: "error", code: "not_found" /* NotFound */ };
2293
- let result = (0, import_utils13.get)(parentDoc, sub);
3146
+ let result = (0, import_utils14.get)(parentDoc, sub);
2294
3147
  const [subCreateSelect, subReadSelect] = await Promise.all([
2295
3148
  service.genQuerySelect("create", null, false, [sub, "sub"]),
2296
3149
  service.genQuerySelect("read", null, false, [sub, "sub"])
2297
3150
  ]);
2298
- const allowedData = Array.isArray(data) ? data.map((row) => (0, import_utils13.pick)(row, subCreateSelect)) : (0, import_utils13.pick)(data, subCreateSelect);
3151
+ const allowedData = Array.isArray(data) ? data.map((row) => (0, import_utils14.pick)(row, subCreateSelect)) : (0, import_utils14.pick)(data, subCreateSelect);
2299
3152
  if (Array.isArray(allowedData)) {
2300
3153
  addFirst === true ? result.unshift(...allowedData) : result.push(...allowedData);
2301
3154
  } else {
2302
3155
  addFirst === true ? result.unshift(allowedData) : result.push(allowedData);
2303
3156
  }
2304
3157
  await parentDoc.save();
2305
- if (subReadSelect) result = result.map((v) => (0, import_utils13.pick)(toObject(v), subReadSelect.concat(["_id"])));
3158
+ if (subReadSelect) result = result.map((v) => (0, import_utils14.pick)(toObject(v), subReadSelect.concat(["_id"])));
2306
3159
  return { success: true, kind: "list", code: "created" /* Created */, data: result, count: result.length };
2307
3160
  }
2308
3161
  async function deleteSub(service, id, sub, subId) {
2309
3162
  const parentDoc = await getParentDoc(service, id, sub, null, { access: "update" });
2310
3163
  if (!parentDoc) return { success: false, kind: "error", code: "not_found" /* NotFound */ };
2311
- const result = (0, import_utils13.get)(parentDoc, sub);
3164
+ const result = (0, import_utils14.get)(parentDoc, sub);
2312
3165
  const subFilter = await service.genFilter(`subs.${sub}.delete`, { _id: subId });
2313
3166
  if (subFilter === false) return { success: false, kind: "error", code: "forbidden" /* Forbidden */ };
2314
3167
  const subdoc = findElement(result, subFilter);
@@ -2449,6 +3302,29 @@ var assertModelDocument = (value, modelName, hookName) => {
2449
3302
  }
2450
3303
  throw new Error(`${hookName} hook for model=${modelName} must return a Mongoose document instance`);
2451
3304
  };
3305
+ var unsetDocPath = (doc, path) => {
3306
+ const segments = path.split(".");
3307
+ let current = doc;
3308
+ const unwrap = (value) => value != null && typeof value === "object" && "_doc" in value ? value._doc : value;
3309
+ current = unwrap(current);
3310
+ for (let i = 0; i < segments.length - 1; i++) {
3311
+ if (current == null || typeof current !== "object") return;
3312
+ if (!Object.prototype.hasOwnProperty.call(current, segments[i])) return;
3313
+ current = unwrap(current[segments[i]]);
3314
+ }
3315
+ if (current != null && typeof current === "object") {
3316
+ delete current[segments[segments.length - 1]];
3317
+ }
3318
+ };
3319
+ var shouldKeepCorrelatedRef = (ref, keep) => {
3320
+ if (keep.has(ref)) return true;
3321
+ const top = ref.split(".")[0];
3322
+ if (keep.has(top)) return true;
3323
+ for (const kept of keep) {
3324
+ if (kept === ref || kept.startsWith(`${ref}.`) || ref.startsWith(`${kept}.`)) return true;
3325
+ }
3326
+ return false;
3327
+ };
2452
3328
  var Service = class extends Base {
2453
3329
  findRawParentDoc(args) {
2454
3330
  return this.model.findOne({
@@ -2515,8 +3391,9 @@ var Service = class extends Base {
2515
3391
  overridePopulate || this.genPopulate(populateAccess || access, populate),
2516
3392
  this.genAllowedFields({}, access, this.baseFieldsExt)
2517
3393
  ]);
2518
- const { includes, includeLocalFields, includePaths } = this.processInclude(include);
2519
- const finalSelect = normalizeSelect(_select).concat(includeLocalFields);
3394
+ const { includes, correlatedIncludes, includeLocalFields, includePaths, correlatedReferenceFields } = this.processInclude(include);
3395
+ const correlatedOutputPaths = correlatedIncludes.map((entry) => entry.path);
3396
+ const finalSelect = normalizeSelect(_select).concat(includeLocalFields, correlatedReferenceFields);
2520
3397
  const query = {
2521
3398
  filter: _filter,
2522
3399
  select: finalSelect,
@@ -2549,8 +3426,14 @@ var Service = class extends Base {
2549
3426
  originalDocumentSnapshot: toObject(doc),
2550
3427
  resolvedQuery: query
2551
3428
  };
3429
+ const correlatedSnapshots = [(0, import_utils15.cloneDeep)(toObject(doc))];
2552
3430
  try {
2553
3431
  doc = await this.includeDocs(doc, includes);
3432
+ if (correlatedIncludes.length > 0) {
3433
+ const docList = [doc];
3434
+ await this.includeCorrelatedDocs(docList, correlatedIncludes, correlatedSnapshots);
3435
+ doc = docList[0];
3436
+ }
2554
3437
  } catch (error2) {
2555
3438
  const result = this.getClientRequestErrorResult(error2);
2556
3439
  if (result) {
@@ -2568,8 +3451,20 @@ var Service = class extends Base {
2568
3451
  doc = await this.trimOutputFields(
2569
3452
  doc,
2570
3453
  access,
2571
- this.baseFieldsExt.concat(includePaths, normalizeSelect(overrideSelect))
3454
+ this.baseFieldsExt.concat(includePaths, correlatedOutputPaths, normalizeSelect(overrideSelect))
2572
3455
  );
3456
+ if (correlatedReferenceFields.length > 0) {
3457
+ const keep = /* @__PURE__ */ new Set([
3458
+ ...normalizeSelect(_select),
3459
+ ...normalizeSelect(overrideSelect),
3460
+ ...this.baseFieldsExt,
3461
+ ...includePaths,
3462
+ ...correlatedOutputPaths
3463
+ ]);
3464
+ for (const ref of correlatedReferenceFields) {
3465
+ if (!shouldKeepCorrelatedRef(ref, keep)) unsetDocPath(doc, ref);
3466
+ }
3467
+ }
2573
3468
  if (!includePermissions) doc = this.addEmptyPermissions(doc);
2574
3469
  this.completeOp("findOne", startedAt, "success" /* Success */, _filter);
2575
3470
  return { success: true, kind: "single", code: "success" /* Success */, data: doc, query, context };
@@ -2615,11 +3510,12 @@ var Service = class extends Base {
2615
3510
  this.genAllowedFields({}, "list", this.baseFieldsExt)
2616
3511
  ]);
2617
3512
  const finalSelect = normalizeSelect(_select);
2618
- const filteredPopulate = (0, import_utils14.isArray)(finalSelect) && (0, import_utils14.isArray)(_populate) ? _populate.filter((p) => finalSelect.includes(p.path.split(".")[0])) : _populate;
2619
- const { includes, includeLocalFields, includePaths } = this.processInclude(include);
3513
+ const filteredPopulate = (0, import_utils15.isArray)(finalSelect) && (0, import_utils15.isArray)(_populate) ? _populate.filter((p) => finalSelect.includes(p.path.split(".")[0])) : _populate;
3514
+ const { includes, correlatedIncludes, includeLocalFields, includePaths, correlatedReferenceFields } = this.processInclude(include);
3515
+ const correlatedOutputPaths = correlatedIncludes.map((entry) => entry.path);
2620
3516
  const query = {
2621
3517
  filter: _filter,
2622
- select: finalSelect.concat(includeLocalFields),
3518
+ select: finalSelect.concat(includeLocalFields, correlatedReferenceFields),
2623
3519
  populate: filteredPopulate,
2624
3520
  sort,
2625
3521
  ...pagination
@@ -2628,7 +3524,7 @@ var Service = class extends Base {
2628
3524
  sort,
2629
3525
  skip: pagination.skip,
2630
3526
  limit: pagination.limit,
2631
- selectCount: finalSelect.concat(includeLocalFields).length,
3527
+ selectCount: finalSelect.concat(includeLocalFields, correlatedReferenceFields).length,
2632
3528
  populateCount: Array.isArray(filteredPopulate) ? filteredPopulate.length : filteredPopulate ? 1 : 0
2633
3529
  });
2634
3530
  if (_filter === false) {
@@ -2652,9 +3548,13 @@ var Service = class extends Base {
2652
3548
  originalDocumentSnapshot: toObject(doc),
2653
3549
  resolvedQuery: query
2654
3550
  }));
2655
- const _decorate = (0, import_utils14.isFunction)(decorate) ? decorate : (v) => v;
3551
+ const _decorate = (0, import_utils15.isFunction)(decorate) ? decorate : (v) => v;
3552
+ const correlatedSnapshots = docs.map((doc) => (0, import_utils15.cloneDeep)(toObject(doc)));
2656
3553
  try {
2657
3554
  docs = await this.includeDocs(docs, includes);
3555
+ if (correlatedIncludes.length > 0) {
3556
+ docs = await this.includeCorrelatedDocs(docs, correlatedIncludes, correlatedSnapshots);
3557
+ }
2658
3558
  } catch (error2) {
2659
3559
  const result = this.getClientRequestErrorResult(error2);
2660
3560
  if (result) {
@@ -2676,8 +3576,20 @@ var Service = class extends Base {
2676
3576
  doc = await this.trimOutputFields(
2677
3577
  doc,
2678
3578
  "list",
2679
- this.baseFieldsExt.concat(includePaths, normalizeSelect(overrideSelect))
3579
+ this.baseFieldsExt.concat(includePaths, correlatedOutputPaths, normalizeSelect(overrideSelect))
2680
3580
  );
3581
+ if (correlatedReferenceFields.length > 0) {
3582
+ const keep = /* @__PURE__ */ new Set([
3583
+ ...normalizeSelect(_select),
3584
+ ...normalizeSelect(overrideSelect),
3585
+ ...this.baseFieldsExt,
3586
+ ...includePaths,
3587
+ ...correlatedOutputPaths
3588
+ ]);
3589
+ for (const ref of correlatedReferenceFields) {
3590
+ if (!shouldKeepCorrelatedRef(ref, keep)) unsetDocPath(doc, ref);
3591
+ }
3592
+ }
2681
3593
  doc = await _decorate(doc, contexts[i]);
2682
3594
  if (!includePermissions) doc = this.addEmptyPermissions(doc);
2683
3595
  return doc;
@@ -2753,16 +3665,16 @@ var Service = class extends Base {
2753
3665
  resolvedQuery: resolvedPopulate.length > 0 ? { populate: resolvedPopulate } : {}
2754
3666
  };
2755
3667
  const allowedFields = await this.genAllowedFields(item, "create");
2756
- const allowedData = (0, import_utils14.pick)(item, allowedFields);
3668
+ const allowedData = (0, import_utils15.pick)(item, allowedFields);
2757
3669
  context.allowedFields = allowedFields;
2758
3670
  context.allowedData = allowedData;
2759
3671
  const validated = await this.validate(allowedData, "create", context);
2760
- if ((0, import_utils14.isBoolean)(validated)) {
3672
+ if ((0, import_utils15.isBoolean)(validated)) {
2761
3673
  if (!validated) {
2762
3674
  validationErrors.push({ index, errors: [] });
2763
3675
  return void 0;
2764
3676
  }
2765
- } else if ((0, import_utils14.isArray)(validated)) {
3677
+ } else if ((0, import_utils15.isArray)(validated)) {
2766
3678
  if (validated.length > 0) {
2767
3679
  validationErrors.push({
2768
3680
  index,
@@ -2792,7 +3704,7 @@ var Service = class extends Base {
2792
3704
  contexts[index].preparedData = preparedData;
2793
3705
  return preparedData;
2794
3706
  });
2795
- const _decorate = (0, import_utils14.isFunction)(decorate) ? decorate : (v) => v;
3707
+ const _decorate = (0, import_utils15.isFunction)(decorate) ? decorate : (v) => v;
2796
3708
  const createdDocs = await this.model.create(items);
2797
3709
  const docs = await mapWithConcurrencyLimit(createdDocs, maxBulkConcurrency, async (doc, index) => {
2798
3710
  contexts[index].currentDocument = doc;
@@ -2910,16 +3822,16 @@ var Service = class extends Base {
2910
3822
  context.docPermissions = this.getDocPermissions(doc);
2911
3823
  context.currentDocument = doc;
2912
3824
  const allowedFields = await this.genAllowedFields(doc, "update");
2913
- const allowedData = (0, import_utils14.pick)(data, allowedFields);
3825
+ const allowedData = (0, import_utils15.pick)(data, allowedFields);
2914
3826
  context.allowedFields = allowedFields;
2915
3827
  context.allowedData = allowedData;
2916
3828
  const validated = await this.validate(allowedData, "update", context);
2917
- if ((0, import_utils14.isBoolean)(validated)) {
3829
+ if ((0, import_utils15.isBoolean)(validated)) {
2918
3830
  if (!validated) {
2919
3831
  this.completeOp("updateOne", startedAt, "bad_request" /* BadRequest */, _filter);
2920
3832
  return { success: false, kind: "error", code: "bad_request" /* BadRequest */ };
2921
3833
  }
2922
- } else if ((0, import_utils14.isArray)(validated)) {
3834
+ } else if ((0, import_utils15.isArray)(validated)) {
2923
3835
  if (validated.length > 0) {
2924
3836
  this.completeOp("updateOne", startedAt, "bad_request" /* BadRequest */, _filter);
2925
3837
  return { success: false, kind: "error", code: "bad_request" /* BadRequest */, errors: validated };
@@ -2935,10 +3847,10 @@ var Service = class extends Base {
2935
3847
  const diffExcludeFields = [this.options.documentPermissionField, "__v"];
2936
3848
  this.asServiceHookContext(context).diff = (d) => {
2937
3849
  context.changes = (0, import_just_diff.diff)(
2938
- (0, import_utils14.omit)(context.originalDocumentSnapshot, diffExcludeFields),
2939
- (0, import_utils14.omit)(d.toObject({ virtuals: false }), diffExcludeFields)
3850
+ (0, import_utils15.omit)(context.originalDocumentSnapshot, diffExcludeFields),
3851
+ (0, import_utils15.omit)(d.toObject({ virtuals: false }), diffExcludeFields)
2940
3852
  ) || [];
2941
- context.modifiedPaths = (0, import_utils14.uniq)(context.changes.map((di) => di.path.length > 0 ? String(di.path[0]) : ""));
3853
+ context.modifiedPaths = (0, import_utils15.uniq)(context.changes.map((di) => di.path.length > 0 ? String(di.path[0]) : ""));
2942
3854
  };
2943
3855
  doc = assertModelDocument(await this.afterPersist(doc, "update", context), this.modelName, "afterPersist");
2944
3856
  context.currentDocument = doc;
@@ -2954,7 +3866,7 @@ var Service = class extends Base {
2954
3866
  if (_populate) await populateDoc(doc, _populate);
2955
3867
  doc = await this.trimOutputFields(doc, "read", this.baseFieldsExt);
2956
3868
  let outputDoc = doc;
2957
- if ((0, import_utils14.isFunction)(decorate)) outputDoc = await decorate(outputDoc, context);
3869
+ if ((0, import_utils15.isFunction)(decorate)) outputDoc = await decorate(outputDoc, context);
2958
3870
  if (!includePermissions) outputDoc = this.addEmptyPermissions(outputDoc);
2959
3871
  this.completeOp("updateOne", startedAt, "success" /* Success */, _filter);
2960
3872
  return { success: true, kind: "single", code: "success" /* Success */, data: outputDoc, input: prepared };
@@ -3106,6 +4018,23 @@ var Service = class extends Base {
3106
4018
  if (filter2 === false) return { success: false, kind: "error", code: "forbidden" /* Forbidden */, query };
3107
4019
  return { success: true, kind: "single", code: "success" /* Success */, data: await this.model.countDocuments(filter2), query };
3108
4020
  }
4021
+ /**
4022
+ * Trusted count for correlated includes (ACI-03): the caller already
4023
+ * resolved parent references, revalidated expanded operands, and applied
4024
+ * explicit `count` access via `genFilter`. This skips client-filter
4025
+ * validation and subquery parsing so substituted parent data stays inert.
4026
+ */
4027
+ async countTrusted(authorizedFilter) {
4028
+ const query = { filter: authorizedFilter };
4029
+ if (authorizedFilter === false) return { success: false, kind: "error", code: "forbidden" /* Forbidden */, query };
4030
+ return {
4031
+ success: true,
4032
+ kind: "single",
4033
+ code: "success" /* Success */,
4034
+ data: await this.model.countDocuments(authorizedFilter),
4035
+ query
4036
+ };
4037
+ }
3109
4038
  async countByFieldValues(foreignField, values, filter2 = {}, access = "list") {
3110
4039
  if (!isValidFieldPath(foreignField)) {
3111
4040
  return {
@@ -3117,7 +4046,7 @@ var Service = class extends Base {
3117
4046
  }
3118
4047
  const filterErrors = this.validateClientFilter(filter2);
3119
4048
  if (filterErrors.length > 0) return { success: false, kind: "error", code: "bad_request" /* BadRequest */, errors: filterErrors };
3120
- const uniqueValues = (0, import_utils14.uniqBy)(values, (value) => String(value));
4049
+ const uniqueValues = (0, import_utils15.uniqBy)(values, (value) => String(value));
3121
4050
  filter2 = await this.genFilter(access, {
3122
4051
  ...filter2,
3123
4052
  [foreignField]: { $in: uniqueValues }
@@ -3193,7 +4122,7 @@ var Service = class extends Base {
3193
4122
  return resolveExistsOptions(this, options);
3194
4123
  }
3195
4124
  async getFieldPermissionAccess(ids) {
3196
- const uniqueIds = (0, import_utils14.compact)((0, import_utils14.uniqBy)(ids, (id) => String(id)).map((id) => String(id)));
4125
+ const uniqueIds = (0, import_utils15.compact)((0, import_utils15.uniqBy)(ids, (id) => String(id)).map((id) => String(id)));
3197
4126
  if (uniqueIds.length === 0) {
3198
4127
  return {
3199
4128
  readIds: /* @__PURE__ */ new Set(),
@@ -3223,7 +4152,7 @@ var Service = class extends Base {
3223
4152
  return updateSub(this, id, sub, subId, data);
3224
4153
  }
3225
4154
  async bulkUpdateSub(id, sub, data) {
3226
- return bulkUpdateSub(this, id, sub, (0, import_utils14.castArray)(data));
4155
+ return bulkUpdateSub(this, id, sub, (0, import_utils15.castArray)(data));
3227
4156
  }
3228
4157
  async createSub(id, sub, data, options) {
3229
4158
  return createSub(this, id, sub, data, options);
@@ -3237,7 +4166,7 @@ var Service = class extends Base {
3237
4166
  };
3238
4167
 
3239
4168
  // src/services/public-service.ts
3240
- var import_utils15 = require("@web-ts-toolkit/utils");
4169
+ var import_utils16 = require("@web-ts-toolkit/utils");
3241
4170
  var PublicService = class extends Service {
3242
4171
  async _list(filter2, args, options) {
3243
4172
  const { select, populate, include, sort, skip, limit, page, pageSize, tasks } = this.resolvePublicListArgs(args);
@@ -3276,7 +4205,7 @@ var PublicService = class extends Service {
3276
4205
  let d = toObject(doc);
3277
4206
  d = await this.decorate(d, "create", context);
3278
4207
  d = this.runTasks(d, tasks);
3279
- if (select) d = (0, import_utils15.pick)(d, [...normalizeSelect(select), ...this.baseFieldsExt]);
4208
+ if (select) d = (0, import_utils16.pick)(d, [...normalizeSelect(select), ...this.baseFieldsExt]);
3280
4209
  return d;
3281
4210
  }
3282
4211
  );
@@ -3381,8 +4310,8 @@ var PublicService = class extends Service {
3381
4310
  let d = toObject(doc);
3382
4311
  d = await this.decorate(d, "update", context);
3383
4312
  d = this.runTasks(d, tasks);
3384
- if (select) d = (0, import_utils15.pick)(d, [...normalizeSelect(select), ...this.baseFieldsExt]);
3385
- else if (!returningAll) d = (0, import_utils15.pick)(d, [...Object.keys(data), "_id"]);
4313
+ if (select) d = (0, import_utils16.pick)(d, [...normalizeSelect(select), ...this.baseFieldsExt]);
4314
+ else if (!returningAll) d = (0, import_utils16.pick)(d, [...Object.keys(data), "_id"]);
3386
4315
  return d;
3387
4316
  }
3388
4317
  );
@@ -3504,7 +4433,7 @@ var PublicService = class extends Service {
3504
4433
  };
3505
4434
 
3506
4435
  // src/services/data-service.ts
3507
- var import_utils16 = require("@web-ts-toolkit/utils");
4436
+ var import_utils17 = require("@web-ts-toolkit/utils");
3508
4437
  var DataService = class {
3509
4438
  constructor(req, dataName) {
3510
4439
  this.req = req;
@@ -3526,7 +4455,7 @@ var DataService = class {
3526
4455
  let doc = await findElement(this.data, _filter);
3527
4456
  if (!doc) return { success: false, kind: "error", code: "not_found" /* NotFound */, query };
3528
4457
  doc = await this.trimOutputFields(doc, access);
3529
- if (_select.length > 0) doc = (0, import_utils16.pick)(doc, _select);
4458
+ if (_select.length > 0) doc = (0, import_utils17.pick)(doc, _select);
3530
4459
  return { success: true, kind: "single", code: "success" /* Success */, data: doc, query };
3531
4460
  }
3532
4461
  async findById(id, args, options) {
@@ -3566,13 +4495,13 @@ var DataService = class {
3566
4495
  const totalCount = docs.length;
3567
4496
  if (sort) {
3568
4497
  const normalizedSort = normalizeSortForOrderBy(sort);
3569
- docs = (0, import_utils16.orderBy)(docs, normalizedSort.fields, normalizedSort.orders);
4498
+ docs = (0, import_utils17.orderBy)(docs, normalizedSort.fields, normalizedSort.orders);
3570
4499
  }
3571
4500
  const pagedDocs = docs.slice(query.skip, query.limit && query.skip + query.limit);
3572
4501
  const { maxHookConcurrency } = this.getRequestComplexity();
3573
4502
  const trimmed = await mapWithConcurrencyLimit(pagedDocs, maxHookConcurrency, async (doc) => {
3574
4503
  doc = await this.trimOutputFields(doc, "list");
3575
- if (_select.length > 0) doc = (0, import_utils16.pick)(doc, _select);
4504
+ if (_select.length > 0) doc = (0, import_utils17.pick)(doc, _select);
3576
4505
  return doc;
3577
4506
  });
3578
4507
  if (!hasExplicitLimit && trimmed.length === totalCount) {
@@ -3621,30 +4550,65 @@ var DataService = class {
3621
4550
  };
3622
4551
 
3623
4552
  // src/processors.ts
3624
- var import_utils17 = require("@web-ts-toolkit/utils");
4553
+ var import_utils18 = require("@web-ts-toolkit/utils");
3625
4554
  var UNSAFE_PATH_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
3626
- function failUnsafeSegment(path) {
4555
+ var DEFAULT_MAX_SLICE = 1e3;
4556
+ var DEFAULT_MASK_REPLACEMENT = "***";
4557
+ function failUnsafeSegment(path, fn = "copyAndDepopulate") {
3627
4558
  throw new Error(
3628
- `copyAndDepopulate: refusing path '${path}' because it contains an unsafe segment (__proto__, prototype, or constructor).`
4559
+ `${fn}: refusing path '${path}' because it contains an unsafe segment (__proto__, prototype, or constructor).`
3629
4560
  );
3630
4561
  }
3631
4562
  function failMissingId(path) {
3632
4563
  throw new Error(`copyAndDepopulate: path '${path}' resolved to a record missing the configured id field.`);
3633
4564
  }
3634
- function assertSafePath(path) {
4565
+ function failInvalidSlice(value, field, fn) {
4566
+ throw new Error(`${fn}: '${field}' must be a non-negative safe integer, received ${String(value)}.`);
4567
+ }
4568
+ function assertSafePath(path, fn = "copyAndDepopulate") {
3635
4569
  const segs = path.split(".");
3636
4570
  for (const seg of segs) {
3637
4571
  if (seg.length === 0) continue;
3638
4572
  if (UNSAFE_PATH_KEYS.has(seg)) {
3639
- failUnsafeSegment(path);
4573
+ failUnsafeSegment(path, fn);
3640
4574
  }
3641
4575
  }
3642
4576
  }
4577
+ function assertNonNegativeSafeInteger(value, field, fn) {
4578
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
4579
+ failInvalidSlice(value, field, fn);
4580
+ }
4581
+ }
4582
+ function resolveLeafParents(obj, src) {
4583
+ const segs = src.split(".");
4584
+ let targets = [obj];
4585
+ for (let ind = 0; ind < segs.length - 1; ind += 1) {
4586
+ const seg = segs[ind];
4587
+ targets = targets.reduce((ret, target) => {
4588
+ if (!(0, import_utils18.isPlainObject)(target)) return ret;
4589
+ const targetObject = target;
4590
+ const next = targetObject[seg];
4591
+ if ((0, import_utils18.isArray)(next)) {
4592
+ for (const item of next) {
4593
+ ret.push(item);
4594
+ }
4595
+ } else if (next !== null && next !== void 0) ret.push(next);
4596
+ return ret;
4597
+ }, []);
4598
+ }
4599
+ const leaf = segs[segs.length - 1];
4600
+ const pairs = [];
4601
+ (0, import_utils18.forEach)(targets, (target) => {
4602
+ if (!(0, import_utils18.isPlainObject)(target)) return;
4603
+ pairs.push({ parent: target, leaf });
4604
+ });
4605
+ return pairs;
4606
+ }
3643
4607
  var copyAndDepopulate = (docObject, operations, options = { mutable: true, idField: "_id" }) => {
3644
4608
  const mutable = options.mutable !== false;
3645
4609
  const idField = options.idField ?? "_id";
3646
- const obj = mutable ? docObject : (0, import_utils17.cloneDeep)(docObject);
3647
- (0, import_utils17.forEach)(Array.isArray(operations) ? operations : [], (op) => {
4610
+ const obj = mutable ? docObject : (0, import_utils18.cloneDeep)(docObject);
4611
+ (0, import_utils18.forEach)(Array.isArray(operations) ? operations : [], (op) => {
3648
4612
  if (!op || typeof op.src !== "string" || typeof op.dest !== "string") return;
3649
4613
  const src = op.src;
3650
4614
  const dest = op.dest;
@@ -3653,19 +4617,19 @@ var copyAndDepopulate = (docObject, operations, options = { mutable: true, idFie
3653
4617
  assertSafePath(dest);
3654
4618
  let targets = [obj];
3655
4619
  const segs = src.split(".");
3656
- (0, import_utils17.forEach)(segs, (seg, ind) => {
4620
+ (0, import_utils18.forEach)(segs, (seg, ind) => {
3657
4621
  if (segs.length === ind + 1) {
3658
- (0, import_utils17.forEach)(targets, (target) => {
3659
- if (!(0, import_utils17.isPlainObject)(target)) return;
4622
+ (0, import_utils18.forEach)(targets, (target) => {
4623
+ if (!(0, import_utils18.isPlainObject)(target)) return;
3660
4624
  const targetObject = target;
3661
4625
  const leaf = targetObject[seg];
3662
4626
  const sourceForDest = leaf;
3663
4627
  let depopulated;
3664
- if ((0, import_utils17.isArray)(leaf)) {
4628
+ if ((0, import_utils18.isArray)(leaf)) {
3665
4629
  if (leaf.length === 0) {
3666
4630
  depopulated = [];
3667
- } else if (leaf.every((item) => (0, import_utils17.isPlainObject)(item))) {
3668
- depopulated = (0, import_utils17.map)(leaf, idField).map((id, i) => {
4631
+ } else if (leaf.every((item) => (0, import_utils18.isPlainObject)(item))) {
4632
+ depopulated = (0, import_utils18.map)(leaf, idField).map((id, i) => {
3669
4633
  if (id === void 0) {
3670
4634
  failMissingId(`${src}[${i}]`);
3671
4635
  }
@@ -3674,8 +4638,8 @@ var copyAndDepopulate = (docObject, operations, options = { mutable: true, idFie
3674
4638
  } else {
3675
4639
  return;
3676
4640
  }
3677
- } else if ((0, import_utils17.isPlainObject)(leaf)) {
3678
- const id = (0, import_utils17.get)(leaf, idField);
4641
+ } else if ((0, import_utils18.isPlainObject)(leaf)) {
4642
+ const id = (0, import_utils18.get)(leaf, idField);
3679
4643
  if (id === void 0) {
3680
4644
  failMissingId(src);
3681
4645
  }
@@ -3683,15 +4647,15 @@ var copyAndDepopulate = (docObject, operations, options = { mutable: true, idFie
3683
4647
  } else {
3684
4648
  return;
3685
4649
  }
3686
- (0, import_utils17.set)(targetObject, dest, sourceForDest);
3687
- (0, import_utils17.set)(targetObject, seg, depopulated);
4650
+ (0, import_utils18.set)(targetObject, dest, sourceForDest);
4651
+ (0, import_utils18.set)(targetObject, seg, depopulated);
3688
4652
  });
3689
4653
  } else {
3690
4654
  targets = targets.reduce((ret, target) => {
3691
- if (!(0, import_utils17.isPlainObject)(target)) return ret;
4655
+ if (!(0, import_utils18.isPlainObject)(target)) return ret;
3692
4656
  const targetObject = target;
3693
4657
  const next = targetObject[seg];
3694
- if ((0, import_utils17.isArray)(next)) {
4658
+ if ((0, import_utils18.isArray)(next)) {
3695
4659
  for (const item of next) {
3696
4660
  ret.push(item);
3697
4661
  }
@@ -3703,6 +4667,125 @@ var copyAndDepopulate = (docObject, operations, options = { mutable: true, idFie
3703
4667
  });
3704
4668
  return obj;
3705
4669
  };
4670
+ var copyPaths = (docObject, operations, options = { mutable: true }) => {
4671
+ const mutable = options.mutable !== false;
4672
+ const obj = mutable ? docObject : (0, import_utils18.cloneDeep)(docObject);
4673
+ (0, import_utils18.forEach)(Array.isArray(operations) ? operations : [], (op) => {
4674
+ if (!op || typeof op.src !== "string" || typeof op.dest !== "string") return;
4675
+ const src = op.src;
4676
+ const dest = op.dest;
4677
+ if (src.length === 0 || dest.length === 0) return;
4678
+ assertSafePath(src, "copyPaths");
4679
+ assertSafePath(dest, "copyPaths");
4680
+ if (src === dest) return;
4681
+ (0, import_utils18.forEach)(resolveLeafParents(obj, src), ({ parent, leaf }) => {
4682
+ if (!(0, import_utils18.hasOwn)(parent, leaf)) return;
4683
+ (0, import_utils18.set)(parent, dest, (0, import_utils18.cloneDeep)(parent[leaf]));
4684
+ });
4685
+ });
4686
+ return obj;
4687
+ };
4688
+ var movePaths = (docObject, operations, options = { mutable: true }) => {
4689
+ const mutable = options.mutable !== false;
4690
+ const obj = mutable ? docObject : (0, import_utils18.cloneDeep)(docObject);
4691
+ (0, import_utils18.forEach)(Array.isArray(operations) ? operations : [], (op) => {
4692
+ if (!op || typeof op.src !== "string" || typeof op.dest !== "string") return;
4693
+ const src = op.src;
4694
+ const dest = op.dest;
4695
+ if (src.length === 0 || dest.length === 0) return;
4696
+ assertSafePath(src, "movePaths");
4697
+ assertSafePath(dest, "movePaths");
4698
+ if (src === dest) return;
4699
+ (0, import_utils18.forEach)(resolveLeafParents(obj, src), ({ parent, leaf }) => {
4700
+ if (!(0, import_utils18.hasOwn)(parent, leaf)) return;
4701
+ const value = (0, import_utils18.cloneDeep)(parent[leaf]);
4702
+ (0, import_utils18.set)(parent, dest, value);
4703
+ if (dest === leaf || dest.startsWith(`${leaf}.`)) return;
4704
+ delete parent[leaf];
4705
+ });
4706
+ });
4707
+ return obj;
4708
+ };
4709
+ var sliceArrays = (docObject, operations, options = { mutable: true, maxSlice: DEFAULT_MAX_SLICE }) => {
4710
+ const mutable = options.mutable !== false;
4711
+ const maxSlice = options.maxSlice ?? DEFAULT_MAX_SLICE;
4712
+ assertNonNegativeSafeInteger(maxSlice, "maxSlice", "sliceArrays");
4713
+ const obj = mutable ? docObject : (0, import_utils18.cloneDeep)(docObject);
4714
+ (0, import_utils18.forEach)(Array.isArray(operations) ? operations : [], (op) => {
4715
+ if (!op || typeof op.src !== "string") return;
4716
+ const src = op.src;
4717
+ if (src.length === 0) return;
4718
+ assertSafePath(src, "sliceArrays");
4719
+ const rawDest = typeof op.dest === "undefined" ? void 0 : op.dest;
4720
+ if (typeof rawDest !== "undefined") {
4721
+ if (typeof rawDest !== "string" || rawDest.length === 0) return;
4722
+ assertSafePath(rawDest, "sliceArrays");
4723
+ }
4724
+ const skip = op.skip ?? 0;
4725
+ assertNonNegativeSafeInteger(skip, "skip", "sliceArrays");
4726
+ let limit;
4727
+ if (typeof op.limit !== "undefined") {
4728
+ assertNonNegativeSafeInteger(op.limit, "limit", "sliceArrays");
4729
+ limit = Math.min(op.limit, maxSlice);
4730
+ }
4731
+ (0, import_utils18.forEach)(resolveLeafParents(obj, src), ({ parent, leaf }) => {
4732
+ const leafValue = parent[leaf];
4733
+ if (!(0, import_utils18.isArray)(leafValue)) return;
4734
+ const end = typeof limit === "undefined" ? void 0 : skip + limit;
4735
+ const window = leafValue.slice(skip, end);
4736
+ if (typeof rawDest === "undefined") {
4737
+ (0, import_utils18.set)(parent, leaf, window);
4738
+ } else {
4739
+ (0, import_utils18.set)(parent, rawDest, window);
4740
+ }
4741
+ });
4742
+ });
4743
+ return obj;
4744
+ };
4745
+ var countPaths = (docObject, operations, options = { mutable: true }) => {
4746
+ const mutable = options.mutable !== false;
4747
+ const obj = mutable ? docObject : (0, import_utils18.cloneDeep)(docObject);
4748
+ (0, import_utils18.forEach)(Array.isArray(operations) ? operations : [], (op) => {
4749
+ if (!op || typeof op.src !== "string" || typeof op.dest !== "string") return;
4750
+ const src = op.src;
4751
+ const dest = op.dest;
4752
+ if (src.length === 0 || dest.length === 0) return;
4753
+ assertSafePath(src, "countPaths");
4754
+ assertSafePath(dest, "countPaths");
4755
+ (0, import_utils18.forEach)(resolveLeafParents(obj, src), ({ parent, leaf }) => {
4756
+ if (!(0, import_utils18.hasOwn)(parent, leaf)) return;
4757
+ const leafValue = parent[leaf];
4758
+ let count;
4759
+ if ((0, import_utils18.isArray)(leafValue)) {
4760
+ count = leafValue.length;
4761
+ } else if ((0, import_utils18.isPlainObject)(leafValue)) {
4762
+ count = Object.keys(leafValue).length;
4763
+ } else if (leafValue === null || typeof leafValue === "undefined") {
4764
+ count = 0;
4765
+ } else {
4766
+ count = 1;
4767
+ }
4768
+ (0, import_utils18.set)(parent, dest, count);
4769
+ });
4770
+ });
4771
+ return obj;
4772
+ };
4773
+ var maskPaths = (docObject, operations, options = { mutable: true }) => {
4774
+ const mutable = options.mutable !== false;
4775
+ const obj = mutable ? docObject : (0, import_utils18.cloneDeep)(docObject);
4776
+ (0, import_utils18.forEach)(Array.isArray(operations) ? operations : [], (op) => {
4777
+ if (!op || typeof op.src !== "string") return;
4778
+ const src = op.src;
4779
+ if (src.length === 0) return;
4780
+ assertSafePath(src, "maskPaths");
4781
+ const replacement = Object.prototype.hasOwnProperty.call(op, "replacement") ? op.replacement : DEFAULT_MASK_REPLACEMENT;
4782
+ (0, import_utils18.forEach)(resolveLeafParents(obj, src), ({ parent, leaf }) => {
4783
+ if (!(0, import_utils18.hasOwn)(parent, leaf)) return;
4784
+ (0, import_utils18.set)(parent, leaf, replacement);
4785
+ });
4786
+ });
4787
+ return obj;
4788
+ };
3706
4789
 
3707
4790
  // src/symbols.ts
3708
4791
  var MIDDLEWARE = /* @__PURE__ */ Symbol.for("@web-ts-toolkit/access-router/middleware");
@@ -3742,10 +4825,10 @@ var Cache = class {
3742
4825
  };
3743
4826
 
3744
4827
  // src/core-shared.ts
3745
- var import_utils19 = require("@web-ts-toolkit/utils");
4828
+ var import_utils20 = require("@web-ts-toolkit/utils");
3746
4829
 
3747
4830
  // src/permission.ts
3748
- var import_utils18 = require("@web-ts-toolkit/utils");
4831
+ var import_utils19 = require("@web-ts-toolkit/utils");
3749
4832
  var Permission = class {
3750
4833
  constructor(permissions) {
3751
4834
  this.permissions = { ...permissions };
@@ -3761,7 +4844,7 @@ var Permission = class {
3761
4844
  return [permissionOrPermissions].concat(permissions);
3762
4845
  }
3763
4846
  hasKey(permission) {
3764
- return (0, import_utils18.hasOwn)(this.permissions, permission);
4847
+ return (0, import_utils19.hasOwn)(this.permissions, permission);
3765
4848
  }
3766
4849
  has(permission) {
3767
4850
  return this.permissions[permission] === true;
@@ -3781,10 +4864,10 @@ var permission_default = Permission;
3781
4864
 
3782
4865
  // src/core-shared.ts
3783
4866
  function isAndFilter(filter2) {
3784
- return !!filter2 && (0, import_utils19.isPlainObject)(filter2) && Object.keys(filter2).length === 1 && (0, import_utils19.isArray)(filter2.$and);
4867
+ return !!filter2 && (0, import_utils20.isPlainObject)(filter2) && Object.keys(filter2).length === 1 && (0, import_utils20.isArray)(filter2.$and);
3785
4868
  }
3786
4869
  function isMergeableClause(filter2) {
3787
- return !!filter2 && (0, import_utils19.isPlainObject)(filter2) && Object.keys(filter2).every((key) => !key.startsWith("$"));
4870
+ return !!filter2 && (0, import_utils20.isPlainObject)(filter2) && Object.keys(filter2).every((key) => !key.startsWith("$"));
3788
4871
  }
3789
4872
  function optimizeAndFilter(clausesInput) {
3790
4873
  const clauses = [];
@@ -3799,10 +4882,10 @@ function optimizeAndFilter(clausesInput) {
3799
4882
  }
3800
4883
  }
3801
4884
  const dedupeKey = (value) => {
3802
- if ((0, import_utils19.isArray)(value)) {
4885
+ if ((0, import_utils20.isArray)(value)) {
3803
4886
  return `[${value.map(dedupeKey).join(",")}]`;
3804
4887
  }
3805
- if ((0, import_utils19.isPlainObject)(value)) {
4888
+ if ((0, import_utils20.isPlainObject)(value)) {
3806
4889
  return `{${Object.keys(value).sort().map((key) => `${key}:${dedupeKey(value[key])}`).join(",")}}`;
3807
4890
  }
3808
4891
  return JSON.stringify(value);
@@ -3826,7 +4909,7 @@ function optimizeAndFilter(clausesInput) {
3826
4909
  }
3827
4910
  let canMerge = true;
3828
4911
  for (const [key, value] of Object.entries(clause)) {
3829
- if ((0, import_utils19.hasOwn)(mergedClause, key) && !(0, import_utils19.isEqual)(mergedClause[key], value)) {
4912
+ if ((0, import_utils20.hasOwn)(mergedClause, key) && !(0, import_utils20.isEqual)(mergedClause[key], value)) {
3830
4913
  canMerge = false;
3831
4914
  break;
3832
4915
  }
@@ -3837,24 +4920,24 @@ function optimizeAndFilter(clausesInput) {
3837
4920
  }
3838
4921
  Object.assign(mergedClause, clause);
3839
4922
  }
3840
- const finalClauses = (0, import_utils19.isEmpty)(mergedClause) ? remainingClauses : [mergedClause, ...remainingClauses];
4923
+ const finalClauses = (0, import_utils20.isEmpty)(mergedClause) ? remainingClauses : [mergedClause, ...remainingClauses];
3841
4924
  if (finalClauses.length === 0) return null;
3842
4925
  if (finalClauses.length === 1) return finalClauses[0];
3843
4926
  return { $and: finalClauses };
3844
4927
  }
3845
4928
  function normalizeFilter(filter2) {
3846
4929
  if (filter2 === false) return false;
3847
- if (!filter2 || !(0, import_utils19.isPlainObject)(filter2) || (0, import_utils19.isEmpty)(filter2)) return null;
4930
+ if (!filter2 || !(0, import_utils20.isPlainObject)(filter2) || (0, import_utils20.isEmpty)(filter2)) return null;
3848
4931
  if (!isAndFilter(filter2)) {
3849
4932
  return filter2;
3850
4933
  }
3851
4934
  return optimizeAndFilter(filter2.$and);
3852
4935
  }
3853
4936
  async function resolveIdentifierFilter(req, idField, resolveIdFilter, id) {
3854
- if ((0, import_utils19.isFunction)(resolveIdFilter)) {
4937
+ if ((0, import_utils20.isFunction)(resolveIdFilter)) {
3855
4938
  return await resolveIdFilter.call(req, id);
3856
4939
  }
3857
- if ((0, import_utils19.isString)(idField)) {
4940
+ if ((0, import_utils20.isString)(idField)) {
3858
4941
  return { [idField]: id };
3859
4942
  }
3860
4943
  return { _id: id };
@@ -3870,12 +4953,12 @@ async function resolveAccessFilter({
3870
4953
  }) {
3871
4954
  let nextFilter = normalizeFilter(filter2);
3872
4955
  const overrideFilterFn = getOption(`overrideFilter.${access}`, null);
3873
- if (nextFilter !== false && (0, import_utils19.isFunction)(overrideFilterFn)) {
4956
+ if (nextFilter !== false && (0, import_utils20.isFunction)(overrideFilterFn)) {
3874
4957
  nextFilter = normalizeFilter(await overrideFilterFn.call(req, nextFilter, permissions));
3875
4958
  }
3876
4959
  if (nextFilter === false) return false;
3877
4960
  const baseFilterFn = getOption(`baseFilter.${access}`, null);
3878
- if (!(0, import_utils19.isFunction)(baseFilterFn)) return nextFilter ?? {};
4961
+ if (!(0, import_utils20.isFunction)(baseFilterFn)) return nextFilter ?? {};
3879
4962
  const baseFilter = normalizeFilter(
3880
4963
  cache.has(cacheKey) ? cache.get(cacheKey) : await baseFilterFn.call(req, permissions)
3881
4964
  );
@@ -3909,34 +4992,34 @@ async function setRequestPermissions(req) {
3909
4992
  if (owner === activeRuntime) return;
3910
4993
  if (hasFieldValue && owner === void 0) return;
3911
4994
  const globalPermissions = getGlobalOption("globalPermissions");
3912
- if (!(0, import_utils19.isFunction)(globalPermissions)) return;
4995
+ if (!(0, import_utils20.isFunction)(globalPermissions)) return;
3913
4996
  const permissions = await globalPermissions.call(req, req);
3914
- if ((0, import_utils19.isPlainObject)(permissions)) req[requestPermissionField] = permissions;
3915
- else if ((0, import_utils19.isArray)(permissions)) req[requestPermissionField] = (0, import_utils19.arrayToRecord)(permissions);
3916
- else if ((0, import_utils19.isString)(permissions)) req[requestPermissionField] = { [permissions]: true };
4997
+ if ((0, import_utils20.isPlainObject)(permissions)) req[requestPermissionField] = permissions;
4998
+ else if ((0, import_utils20.isArray)(permissions)) req[requestPermissionField] = (0, import_utils20.arrayToRecord)(permissions);
4999
+ else if ((0, import_utils20.isString)(permissions)) req[requestPermissionField] = { [permissions]: true };
3917
5000
  owners.set(requestPermissionField, activeRuntime);
3918
5001
  }
3919
5002
  async function evaluateRouteGuard(req, permissions, routeGuard) {
3920
5003
  const phas = (key) => permissions.has(key);
3921
5004
  const { stringHandler, arrayHandler } = createValidator(phas);
3922
- if ((0, import_utils19.isBoolean)(routeGuard)) {
5005
+ if ((0, import_utils20.isBoolean)(routeGuard)) {
3923
5006
  return routeGuard === true;
3924
5007
  }
3925
- if ((0, import_utils19.isString)(routeGuard)) {
5008
+ if ((0, import_utils20.isString)(routeGuard)) {
3926
5009
  return stringHandler(routeGuard);
3927
5010
  }
3928
- if ((0, import_utils19.isArray)(routeGuard)) {
5011
+ if ((0, import_utils20.isArray)(routeGuard)) {
3929
5012
  return arrayHandler(routeGuard);
3930
5013
  }
3931
- if ((0, import_utils19.isFunction)(routeGuard)) {
5014
+ if ((0, import_utils20.isFunction)(routeGuard)) {
3932
5015
  return routeGuard.call(req, permissions);
3933
5016
  }
3934
5017
  return false;
3935
5018
  }
3936
5019
  async function callHookChain(req, hook, doc, permissions, context) {
3937
- const hooks = (0, import_utils19.castArray)(hook);
5020
+ const hooks = (0, import_utils20.castArray)(hook);
3938
5021
  for (let x = 0; x < hooks.length; x++) {
3939
- if ((0, import_utils19.isFunction)(hooks[x])) {
5022
+ if ((0, import_utils20.isFunction)(hooks[x])) {
3940
5023
  doc = await hooks[x].call(req, doc, permissions, context);
3941
5024
  }
3942
5025
  }
@@ -3959,13 +5042,13 @@ async function collectSchemaFields({
3959
5042
  if (baseFields.includes(key)) continue;
3960
5043
  const val = permissionSchema[key];
3961
5044
  const value = val && val[access] || val;
3962
- if ((0, import_utils19.isBoolean)(value)) {
5045
+ if ((0, import_utils20.isBoolean)(value)) {
3963
5046
  if (value) fields.push(key);
3964
- } else if ((0, import_utils19.isString)(value)) {
5047
+ } else if ((0, import_utils20.isString)(value)) {
3965
5048
  if (stringHandler(value)) fields.push(key);
3966
- } else if ((0, import_utils19.isArray)(value)) {
5049
+ } else if ((0, import_utils20.isArray)(value)) {
3967
5050
  if (arrayHandler(value)) fields.push(key);
3968
- } else if ((0, import_utils19.isFunction)(value)) {
5051
+ } else if ((0, import_utils20.isFunction)(value)) {
3969
5052
  if (await value.call(req, ...functionArgs)) fields.push(key);
3970
5053
  }
3971
5054
  }
@@ -4051,7 +5134,7 @@ async function runDecorateAllHook({
4051
5134
  }
4052
5135
 
4053
5136
  // src/acl/select-resolution.ts
4054
- var import_utils20 = require("@web-ts-toolkit/utils");
5137
+ var import_utils21 = require("@web-ts-toolkit/utils");
4055
5138
  async function collectAllowedFieldsForRequest({
4056
5139
  req,
4057
5140
  permissionSchema,
@@ -4119,15 +5202,15 @@ async function resolveSelectForRequest({
4119
5202
  const excludeall = normalizedSelect.every((v) => v.startsWith("-"));
4120
5203
  if (excludeall) {
4121
5204
  normalizedSelect = normalizedSelect.map((v) => v.substring(1));
4122
- fields = (0, import_utils20.difference)(fields, normalizedSelect);
5205
+ fields = (0, import_utils21.difference)(fields, normalizedSelect);
4123
5206
  if (excludeid) fields.push("-_id");
4124
5207
  } else {
4125
- fields = (0, import_utils20.intersection)(normalizedSelect, fields.concat(excludeid ? "-_id" : "_id"));
5208
+ fields = (0, import_utils21.intersection)(normalizedSelect, fields.concat(excludeid ? "-_id" : "_id"));
4126
5209
  }
4127
5210
  }
4128
5211
  return fields.concat(alwaysSelectFields);
4129
5212
  }
4130
- return (0, import_utils20.intersection)(normalizedSelect, fields);
5213
+ return (0, import_utils21.intersection)(normalizedSelect, fields);
4131
5214
  }
4132
5215
 
4133
5216
  // src/core.ts
@@ -4142,10 +5225,10 @@ var Core = class {
4142
5225
  getIdentifier(modelName) {
4143
5226
  const resolveIdFilter = getModelOption(modelName, "resolveIdFilter");
4144
5227
  const idField = getModelOption(modelName, "idField");
4145
- if ((0, import_utils21.isFunction)(resolveIdFilter)) {
5228
+ if ((0, import_utils22.isFunction)(resolveIdFilter)) {
4146
5229
  return null;
4147
5230
  }
4148
- if ((0, import_utils21.isString)(idField)) {
5231
+ if ((0, import_utils22.isString)(idField)) {
4149
5232
  return idField;
4150
5233
  }
4151
5234
  return "_id";
@@ -4223,11 +5306,11 @@ var Core = class {
4223
5306
  async genPopulate(modelName, access = "read", _populate = null, subPaths = []) {
4224
5307
  if (!_populate) return [];
4225
5308
  let populate = Array.isArray(_populate) ? _populate : [_populate];
4226
- populate = (0, import_utils21.compact)(
5309
+ populate = (0, import_utils22.compact)(
4227
5310
  await Promise.all(
4228
5311
  populate.map(async (p) => {
4229
- const populateAccess = !(0, import_utils21.isString)(p) && p.access ? p.access : access;
4230
- const ret = (0, import_utils21.isString)(p) ? { path: p } : {
5312
+ const populateAccess = !(0, import_utils22.isString)(p) && p.access ? p.access : access;
5313
+ const ret = (0, import_utils22.isString)(p) ? { path: p } : {
4231
5314
  path: p.path,
4232
5315
  select: normalizeSelect(p.select)
4233
5316
  };
@@ -4265,10 +5348,10 @@ var Core = class {
4265
5348
  }
4266
5349
  async validate(modelName, allowedData, access, context) {
4267
5350
  const validate = getModelOption(modelName, `validate.${access}`, null);
4268
- if ((0, import_utils21.isFunction)(validate)) {
5351
+ if ((0, import_utils22.isFunction)(validate)) {
4269
5352
  const permissions = this.getGlobalPermissions();
4270
5353
  return validate.call(this.req, allowedData, permissions, context);
4271
- } else if ((0, import_utils21.isBoolean)(validate) || (0, import_utils21.isArray)(validate)) {
5354
+ } else if ((0, import_utils22.isBoolean)(validate) || (0, import_utils22.isArray)(validate)) {
4272
5355
  return validate;
4273
5356
  } else {
4274
5357
  return true;
@@ -4293,7 +5376,7 @@ var Core = class {
4293
5376
  const changeOptions = getModelOption(modelName, `onChange`, {});
4294
5377
  for (let x = 0; x < context.modifiedPaths.length; x++) {
4295
5378
  const mpath = context.modifiedPaths[x];
4296
- if ((0, import_utils21.isFunction)(changeOptions[mpath])) {
5379
+ if ((0, import_utils22.isFunction)(changeOptions[mpath])) {
4297
5380
  await changeOptions[mpath].call(
4298
5381
  this.req,
4299
5382
  context.originalDocumentSnapshot[mpath],
@@ -4317,7 +5400,7 @@ var Core = class {
4317
5400
  async genDocPermissions(modelName, doc, access, context) {
4318
5401
  const docPermissionsFn = getModelOption(modelName, `docPermissions.${access}`, null);
4319
5402
  let docPermissions = {};
4320
- if ((0, import_utils21.isFunction)(docPermissionsFn)) {
5403
+ if ((0, import_utils22.isFunction)(docPermissionsFn)) {
4321
5404
  const permissions = this.getGlobalPermissions();
4322
5405
  try {
4323
5406
  docPermissions = await docPermissionsFn.call(this.req, doc, permissions, context);
@@ -4368,7 +5451,7 @@ var Core = class {
4368
5451
  readExists ? this.genAllowedFields(modelName, doc, "read") : [],
4369
5452
  updateExists ? this.genAllowedFields(modelName, doc, "update") : []
4370
5453
  ]);
4371
- const viewObj = (0, import_utils21.reduce)(
5454
+ const viewObj = (0, import_utils22.reduce)(
4372
5455
  views,
4373
5456
  (ret, view) => {
4374
5457
  ret[view] = true;
@@ -4376,7 +5459,7 @@ var Core = class {
4376
5459
  },
4377
5460
  {}
4378
5461
  );
4379
- const editObj = (0, import_utils21.reduce)(
5462
+ const editObj = (0, import_utils22.reduce)(
4380
5463
  edits,
4381
5464
  (ret, view) => {
4382
5465
  ret[view] = true;
@@ -4400,9 +5483,9 @@ var Core = class {
4400
5483
  return runDecorateAllHook({ req: this.req, hook: decorateAll, docs, permissions, context });
4401
5484
  }
4402
5485
  runTasks(modelName, docObject, task) {
4403
- const tasks = (0, import_utils21.compact)((0, import_utils21.castArray)(task));
5486
+ const tasks = (0, import_utils22.compact)((0, import_utils22.castArray)(task));
4404
5487
  if (tasks.length === 0) return docObject;
4405
- (0, import_utils21.forEach)(tasks, (task2) => {
5488
+ (0, import_utils22.forEach)(tasks, (task2) => {
4406
5489
  const { type, args, options } = task2;
4407
5490
  switch (type) {
4408
5491
  case "COPY_AND_DEPOPULATE":
@@ -4412,6 +5495,41 @@ var Core = class {
4412
5495
  options
4413
5496
  );
4414
5497
  break;
5498
+ case "COPY":
5499
+ docObject = copyPaths(
5500
+ docObject,
5501
+ args,
5502
+ options
5503
+ );
5504
+ break;
5505
+ case "MOVE":
5506
+ docObject = movePaths(
5507
+ docObject,
5508
+ args,
5509
+ options
5510
+ );
5511
+ break;
5512
+ case "SLICE":
5513
+ docObject = sliceArrays(
5514
+ docObject,
5515
+ args,
5516
+ options
5517
+ );
5518
+ break;
5519
+ case "COUNT":
5520
+ docObject = countPaths(
5521
+ docObject,
5522
+ args,
5523
+ options
5524
+ );
5525
+ break;
5526
+ case "MASK":
5527
+ docObject = maskPaths(
5528
+ docObject,
5529
+ args,
5530
+ options
5531
+ );
5532
+ break;
4415
5533
  }
4416
5534
  });
4417
5535
  return docObject;
@@ -4435,9 +5553,9 @@ var Core = class {
4435
5553
  }
4436
5554
  const [, field, op] = keys2;
4437
5555
  const subOption = getExactModelOption(modelName, `operationAccess.${access}`);
4438
- if ((0, import_utils21.isUndefined)(subOption)) {
5556
+ if ((0, import_utils22.isUndefined)(subOption)) {
4439
5557
  const subFieldOption = getExactModelOption(modelName, `operationAccess.subs.${field}`);
4440
- if ((0, import_utils21.isUndefined)(subFieldOption)) {
5558
+ if ((0, import_utils22.isUndefined)(subFieldOption)) {
4441
5559
  const opOption = getModelOption(modelName, `operationAccess.${op}`);
4442
5560
  return this.canActivate(opOption);
4443
5561
  }
@@ -4489,25 +5607,25 @@ function guard(condition) {
4489
5607
  const permissions = req[PERMISSIONS];
4490
5608
  let cond = condition;
4491
5609
  let phas = (key) => permissions.has(key);
4492
- if ((0, import_utils22.isPlainObject)(condition)) {
5610
+ if ((0, import_utils23.isPlainObject)(condition)) {
4493
5611
  const { modelName, id, condition: _cond } = condition;
4494
5612
  const svc = req.macl.getPublicService(modelName);
4495
5613
  const select = getModelOption(modelName, `alwaysSelectFields.read`, void 0);
4496
- let _id = (0, import_utils22.isString)(id) ? id : null;
4497
- if ((0, import_utils22.isPlainObject)(id)) {
5614
+ let _id = (0, import_utils23.isString)(id) ? id : null;
5615
+ if ((0, import_utils23.isPlainObject)(id)) {
4498
5616
  const { type = "param", key } = id;
4499
5617
  if (type === "param") {
4500
5618
  const paramValue = req.params[key];
4501
- _id = (0, import_utils22.isArray)(paramValue) ? paramValue[0] ?? null : paramValue ?? null;
5619
+ _id = (0, import_utils23.isArray)(paramValue) ? paramValue[0] ?? null : paramValue ?? null;
4502
5620
  } else if (type === "query") {
4503
5621
  const _qval = req.query[key];
4504
- if ((0, import_utils22.isArray)(_qval)) _id = _qval.length > 0 ? _qval[0] : null;
5622
+ if ((0, import_utils23.isArray)(_qval)) _id = _qval.length > 0 ? _qval[0] : null;
4505
5623
  else _id = _qval;
4506
5624
  } else {
4507
5625
  _id = null;
4508
5626
  }
4509
5627
  }
4510
- if (!(0, import_utils22.isString)(_id) || !_id) {
5628
+ if (!(0, import_utils23.isString)(_id) || !_id) {
4511
5629
  return next(new import_express_json_router2.default.clientErrors.BadRequestError());
4512
5630
  }
4513
5631
  const result = await svc._read(_id, { select });
@@ -4519,11 +5637,11 @@ function guard(condition) {
4519
5637
  cond = _cond;
4520
5638
  }
4521
5639
  const { stringHandler, arrayHandler } = createValidator(phas);
4522
- if ((0, import_utils22.isString)(cond)) {
5640
+ if ((0, import_utils23.isString)(cond)) {
4523
5641
  if (stringHandler(cond)) return next();
4524
- } else if ((0, import_utils22.isArray)(cond)) {
5642
+ } else if ((0, import_utils23.isArray)(cond)) {
4525
5643
  if (arrayHandler(cond)) return next();
4526
- } else if ((0, import_utils22.isFunction)(cond)) {
5644
+ } else if ((0, import_utils23.isFunction)(cond)) {
4527
5645
  const result = await cond.call(req, permissions);
4528
5646
  if (!!result) return next();
4529
5647
  }
@@ -4536,7 +5654,7 @@ var import_express2 = __toESM(require("express"));
4536
5654
 
4537
5655
  // src/routers/model-router.ts
4538
5656
  var import_express_json_router5 = __toESM(require("@web-ts-toolkit/express-json-router"));
4539
- var import_utils25 = require("@web-ts-toolkit/utils");
5657
+ var import_utils27 = require("@web-ts-toolkit/utils");
4540
5658
 
4541
5659
  // src/routers/router-mutation.ts
4542
5660
  var MODEL_BUILD_TIME_OPTION_KEYS = /* @__PURE__ */ new Set([
@@ -4665,6 +5783,7 @@ var import_express_json_router4 = __toESM(require("@web-ts-toolkit/express-json-
4665
5783
 
4666
5784
  // src/validation/common.ts
4667
5785
  var import_zod2 = require("zod");
5786
+ var import_utils24 = require("@web-ts-toolkit/utils");
4668
5787
  var stringOrStringArray = import_zod2.z.union([import_zod2.z.string(), import_zod2.z.array(import_zod2.z.string())]);
4669
5788
  var safeIntegerString = (min, message) => import_zod2.z.string().regex(/^\d+$/, message).refine((value) => Number.isSafeInteger(Number(value)), "Expected a safe integer").refine((value) => Number(value) >= min, message);
4670
5789
  var queryBooleanString = import_zod2.z.enum(["true", "false"]);
@@ -4725,7 +5844,8 @@ var subPopulateSchema = import_zod2.z.union([
4725
5844
  select: projectionSchema.optional()
4726
5845
  }).passthrough()
4727
5846
  ]);
4728
- var includeItemSchema = import_zod2.z.object({
5847
+ var legacyIncludeItemSchema = import_zod2.z.object({
5848
+ mode: import_zod2.z.enum(["legacy"]).optional(),
4729
5849
  model: import_zod2.z.string().min(1),
4730
5850
  op: import_zod2.z.enum(["list", "read", "count"]),
4731
5851
  path: import_zod2.z.string().min(1),
@@ -4734,8 +5854,139 @@ var includeItemSchema = import_zod2.z.object({
4734
5854
  foreignField: import_zod2.z.string().min(1),
4735
5855
  args: import_zod2.z.unknown().optional().describe("Forwarded to list/read includes. Count includes ignore pagination fields so counts remain exact."),
4736
5856
  options: import_zod2.z.unknown().optional()
4737
- }).passthrough();
4738
- var includeSchema = import_zod2.z.union([includeItemSchema, import_zod2.z.array(includeItemSchema)]);
5857
+ }).passthrough().superRefine((entry, ctx) => {
5858
+ if (entry.filter !== void 0 && containsParentMarkerShape(entry.filter)) {
5859
+ ctx.addIssue({
5860
+ code: import_zod2.z.ZodIssueCode.custom,
5861
+ message: 'Legacy includes must not contain $parent markers; use a correlated include (mode: "correlated")',
5862
+ path: ["filter"]
5863
+ });
5864
+ }
5865
+ });
5866
+ var parentRefSchema = import_zod2.z.object({ $parent: import_zod2.z.string().min(1) }).strict().superRefine((value, ctx) => {
5867
+ if (!isValidParentRefPath(value.$parent)) {
5868
+ ctx.addIssue({
5869
+ code: import_zod2.z.ZodIssueCode.custom,
5870
+ message: `Invalid $parent reference path: ${value.$parent}`,
5871
+ path: ["$parent"]
5872
+ });
5873
+ }
5874
+ });
5875
+ var correlatedIdSchema = import_zod2.z.union([import_zod2.z.string().min(1), parentRefSchema]);
5876
+ var correlatedFilterSchema = import_zod2.z.record(import_zod2.z.string(), import_zod2.z.unknown()).superRefine((filter2, ctx) => {
5877
+ for (const issue of validateCorrelatedFilterTemplate(filter2)) {
5878
+ ctx.addIssue({
5879
+ code: import_zod2.z.ZodIssueCode.custom,
5880
+ message: issue.message,
5881
+ path: issue.path.map(String)
5882
+ });
5883
+ }
5884
+ });
5885
+ var correlatedEmptyOptionsSchema = import_zod2.z.unknown().refine(
5886
+ (value) => value === void 0 || (0, import_utils24.isPlainObject)(value) && Object.keys(value).length === 0,
5887
+ "Correlated include options must be absent or empty"
5888
+ ).optional();
5889
+ var correlatedPathSchema = import_zod2.z.string().min(1).superRefine((path, ctx) => {
5890
+ if (!isValidCorrelatedOutputPath(path)) {
5891
+ ctx.addIssue({
5892
+ code: import_zod2.z.ZodIssueCode.custom,
5893
+ message: `Invalid correlated include output path: ${path}`,
5894
+ path: []
5895
+ });
5896
+ }
5897
+ });
5898
+ var checkArgsMarkers = (select, sort, ctx) => {
5899
+ const selectIssue = findArgsMarkerIssue(select, "args.select");
5900
+ if (selectIssue) {
5901
+ ctx.addIssue({ code: import_zod2.z.ZodIssueCode.custom, message: selectIssue.message, path: ["select"] });
5902
+ }
5903
+ const sortIssue = findArgsMarkerIssue(sort, "args.sort");
5904
+ if (sortIssue) {
5905
+ ctx.addIssue({ code: import_zod2.z.ZodIssueCode.custom, message: sortIssue.message, path: ["sort"] });
5906
+ }
5907
+ };
5908
+ var correlatedReadArgsSchema = import_zod2.z.object({
5909
+ select: projectionSchema.optional(),
5910
+ sort: sortSchema.optional(),
5911
+ include: import_zod2.z.lazy(() => correlatedNestedIncludeSchema).optional()
5912
+ }).strict().superRefine((args, ctx) => checkArgsMarkers(args.select, args.sort, ctx));
5913
+ var correlatedListArgsSchema = import_zod2.z.object({
5914
+ select: projectionSchema.optional(),
5915
+ sort: sortSchema.optional(),
5916
+ skip: import_zod2.z.union([nonNegativeIntegerSchema, nonNegativeIntegerString]).optional(),
5917
+ limit: import_zod2.z.union([positiveIntegerSchema, positiveIntegerString]).optional(),
5918
+ page: import_zod2.z.union([nonNegativeIntegerSchema, nonNegativeIntegerString]).optional(),
5919
+ pageSize: import_zod2.z.union([positiveIntegerSchema, positiveIntegerString]).optional(),
5920
+ include: import_zod2.z.lazy(() => correlatedNestedIncludeSchema).optional()
5921
+ }).strict().superRefine((args, ctx) => checkArgsMarkers(args.select, args.sort, ctx));
5922
+ var correlatedCountArgsSchema = import_zod2.z.unknown().refine(
5923
+ (value) => value === void 0 || (0, import_utils24.isPlainObject)(value) && Object.keys(value).length === 0,
5924
+ "Count includes accept no args"
5925
+ ).optional();
5926
+ var correlatedReadByIdSchema = import_zod2.z.object({
5927
+ mode: import_zod2.z.literal("correlated"),
5928
+ model: import_zod2.z.string().min(1),
5929
+ op: import_zod2.z.literal("read"),
5930
+ path: correlatedPathSchema,
5931
+ id: correlatedIdSchema,
5932
+ args: correlatedReadArgsSchema.optional(),
5933
+ options: correlatedEmptyOptionsSchema
5934
+ }).strict();
5935
+ var correlatedReadByFilterSchema = import_zod2.z.object({
5936
+ mode: import_zod2.z.literal("correlated"),
5937
+ model: import_zod2.z.string().min(1),
5938
+ op: import_zod2.z.literal("read"),
5939
+ path: correlatedPathSchema,
5940
+ filter: correlatedFilterSchema,
5941
+ args: correlatedReadArgsSchema.optional(),
5942
+ options: correlatedEmptyOptionsSchema
5943
+ }).strict();
5944
+ var correlatedListSchema = import_zod2.z.object({
5945
+ mode: import_zod2.z.literal("correlated"),
5946
+ model: import_zod2.z.string().min(1),
5947
+ op: import_zod2.z.literal("list"),
5948
+ path: correlatedPathSchema,
5949
+ filter: correlatedFilterSchema,
5950
+ args: correlatedListArgsSchema.optional(),
5951
+ options: correlatedEmptyOptionsSchema
5952
+ }).strict();
5953
+ var correlatedCountSchema = import_zod2.z.object({
5954
+ mode: import_zod2.z.literal("correlated"),
5955
+ model: import_zod2.z.string().min(1),
5956
+ op: import_zod2.z.literal("count"),
5957
+ path: correlatedPathSchema,
5958
+ filter: correlatedFilterSchema,
5959
+ args: correlatedCountArgsSchema,
5960
+ options: correlatedEmptyOptionsSchema
5961
+ }).strict();
5962
+ var includeItemSchema = import_zod2.z.union([
5963
+ legacyIncludeItemSchema,
5964
+ correlatedReadByIdSchema,
5965
+ correlatedReadByFilterSchema,
5966
+ correlatedListSchema,
5967
+ correlatedCountSchema
5968
+ ]);
5969
+ var correlatedNestedIncludeSchema = import_zod2.z.union([includeItemSchema, import_zod2.z.array(includeItemSchema)]);
5970
+ var rejectDuplicateCorrelatedPaths = (items, ctx) => {
5971
+ const seen = /* @__PURE__ */ new Set();
5972
+ items.forEach((item, index) => {
5973
+ if ((0, import_utils24.isPlainObject)(item) && item.mode === "correlated" && typeof item.path === "string") {
5974
+ if (seen.has(item.path)) {
5975
+ ctx.addIssue({
5976
+ code: import_zod2.z.ZodIssueCode.custom,
5977
+ message: `Duplicate correlated include output path: ${item.path}`,
5978
+ path: [index, "path"]
5979
+ });
5980
+ } else {
5981
+ seen.add(item.path);
5982
+ }
5983
+ }
5984
+ });
5985
+ };
5986
+ var includeSchema = import_zod2.z.union([
5987
+ includeItemSchema,
5988
+ import_zod2.z.array(includeItemSchema).superRefine(rejectDuplicateCorrelatedPaths)
5989
+ ]);
4739
5990
  var fieldsSchema = import_zod2.z.array(import_zod2.z.string().min(1));
4740
5991
  var taskSchema = import_zod2.z.object({
4741
5992
  type: import_zod2.z.string().min(1),
@@ -5272,7 +6523,7 @@ var listBodySchema = import_zod3.z.object({
5272
6523
  }).passthrough().superRefine((body, ctx) => rejectKeys(body, ctx, ["query"]));
5273
6524
  var countBodySchema = import_zod3.z.object({
5274
6525
  filter: objectOrArraySchema.optional()
5275
- }).passthrough().superRefine((body, ctx) => rejectKeys(body, ctx, ["query", "access", "options"]));
6526
+ }).passthrough().superRefine((body, ctx) => rejectKeys(body, ctx, ["query", "access", "options", "include"]));
5276
6527
  var readFilterBodySchema = import_zod3.z.object({
5277
6528
  filter: objectOrArraySchema.optional(),
5278
6529
  select: projectionSchema.optional(),
@@ -5627,7 +6878,7 @@ var rootDataQueryEntrySchema = import_zod5.z.union([
5627
6878
  var rootQuerySchema = import_zod5.z.array(import_zod5.z.union([rootModelQueryEntrySchema, rootDataQueryEntrySchema]));
5628
6879
 
5629
6880
  // src/routers/shared.ts
5630
- var import_utils23 = require("@web-ts-toolkit/utils");
6881
+ var import_utils25 = require("@web-ts-toolkit/utils");
5631
6882
 
5632
6883
  // src/routers/model-router-collection-routes.ts
5633
6884
  function setModelCollectionRoutes(context) {
@@ -5645,14 +6896,14 @@ function setModelCollectionRoutes(context) {
5645
6896
  await context.assertAllowed(req, "list");
5646
6897
  const { skip, limit, page, page_size, skim, include_permissions, include_count, include_extra_headers } = parseQuery(requestSchemas.listQuery, req.query);
5647
6898
  const svc = context.getPublicService(req);
5648
- const includeCount = (0, import_utils23.parseBooleanString)(include_count);
5649
- const includeExtraHeaders = (0, import_utils23.parseBooleanString)(include_extra_headers);
6899
+ const includeCount = (0, import_utils25.parseBooleanString)(include_count);
6900
+ const includeExtraHeaders = (0, import_utils25.parseBooleanString)(include_extra_headers);
5650
6901
  const result = await svc._list(
5651
6902
  {},
5652
6903
  { skip, limit, page, pageSize: page_size },
5653
6904
  {
5654
- skim: (0, import_utils23.parseBooleanString)(skim),
5655
- includePermissions: (0, import_utils23.parseBooleanString)(include_permissions),
6905
+ skim: (0, import_utils25.parseBooleanString)(skim),
6906
+ includePermissions: (0, import_utils25.parseBooleanString)(include_permissions),
5656
6907
  includeCount
5657
6908
  }
5658
6909
  );
@@ -5707,7 +6958,7 @@ function setModelCollectionRoutes(context) {
5707
6958
  context.getRequestSchema("requestSchemas.create")
5708
6959
  );
5709
6960
  const svc = context.getPublicService(req);
5710
- const result = await svc._create(data, {}, { includePermissions: (0, import_utils23.parseBooleanString)(include_permissions) });
6961
+ const result = await svc._create(data, {}, { includePermissions: (0, import_utils25.parseBooleanString)(include_permissions) });
5711
6962
  handleResultError(result);
5712
6963
  return formatModelCreatedResponse(result);
5713
6964
  });
@@ -5743,7 +6994,7 @@ function setModelCollectionRoutes(context) {
5743
6994
  finalData,
5744
6995
  { select: finalSelect, populate: finalPopulate, tasks: finalTasks },
5745
6996
  {
5746
- includePermissions: includePermissions ?? (0, import_utils23.parseBooleanString)(include_permissions),
6997
+ includePermissions: includePermissions ?? (0, import_utils25.parseBooleanString)(include_permissions),
5747
6998
  populateAccess
5748
6999
  }
5749
7000
  );
@@ -5835,8 +7086,8 @@ function setModelDocumentRoutes(context) {
5835
7086
  id,
5836
7087
  {},
5837
7088
  {
5838
- includePermissions: (0, import_utils23.parseBooleanString)(include_permissions),
5839
- tryList: (0, import_utils23.parseBooleanString)(try_list)
7089
+ includePermissions: (0, import_utils25.parseBooleanString)(include_permissions),
7090
+ tryList: (0, import_utils25.parseBooleanString)(try_list)
5840
7091
  }
5841
7092
  );
5842
7093
  handleResultError(result);
@@ -5921,8 +7172,8 @@ function setModelDocumentRoutes(context) {
5921
7172
  data,
5922
7173
  {},
5923
7174
  {
5924
- returningAll: (0, import_utils23.parseBooleanString)(returning_all),
5925
- includePermissions: (0, import_utils23.parseBooleanString)(include_permissions)
7175
+ returningAll: (0, import_utils25.parseBooleanString)(returning_all),
7176
+ includePermissions: (0, import_utils25.parseBooleanString)(include_permissions)
5926
7177
  }
5927
7178
  );
5928
7179
  handleResultError(result);
@@ -5968,8 +7219,8 @@ function setModelDocumentRoutes(context) {
5968
7219
  finalData,
5969
7220
  { select: finalSelect, populate: finalPopulate, tasks: finalTasks },
5970
7221
  {
5971
- returningAll: returningAll ?? (0, import_utils23.parseBooleanString)(returning_all),
5972
- includePermissions: includePermissions ?? (0, import_utils23.parseBooleanString)(include_permissions),
7222
+ returningAll: returningAll ?? (0, import_utils25.parseBooleanString)(returning_all),
7223
+ includePermissions: includePermissions ?? (0, import_utils25.parseBooleanString)(include_permissions),
5973
7224
  populateAccess
5974
7225
  }
5975
7226
  );
@@ -5997,8 +7248,8 @@ function setModelDocumentRoutes(context) {
5997
7248
  body,
5998
7249
  {},
5999
7250
  {
6000
- returningAll: (0, import_utils23.parseBooleanString)(returning_all),
6001
- includePermissions: (0, import_utils23.parseBooleanString)(include_permissions)
7251
+ returningAll: (0, import_utils25.parseBooleanString)(returning_all),
7252
+ includePermissions: (0, import_utils25.parseBooleanString)(include_permissions)
6002
7253
  }
6003
7254
  );
6004
7255
  handleResultError(result);
@@ -6042,8 +7293,8 @@ function setModelDocumentRoutes(context) {
6042
7293
  finalData,
6043
7294
  { select: finalSelect, populate: finalPopulate, tasks: finalTasks },
6044
7295
  {
6045
- returningAll: returningAll ?? (0, import_utils23.parseBooleanString)(returning_all),
6046
- includePermissions: includePermissions ?? (0, import_utils23.parseBooleanString)(include_permissions),
7296
+ returningAll: returningAll ?? (0, import_utils25.parseBooleanString)(returning_all),
7297
+ includePermissions: includePermissions ?? (0, import_utils25.parseBooleanString)(include_permissions),
6047
7298
  populateAccess
6048
7299
  }
6049
7300
  );
@@ -6281,11 +7532,11 @@ function setModelSubDocumentRoutes(context) {
6281
7532
  }
6282
7533
 
6283
7534
  // src/openapi/route-registration.ts
6284
- var import_utils24 = require("@web-ts-toolkit/utils");
7535
+ var import_utils26 = require("@web-ts-toolkit/utils");
6285
7536
  function registerOpenApiRoute(runtime, basePath, defaultTag, route) {
6286
7537
  runtime.registerOpenApiRoute({
6287
7538
  ...route,
6288
- path: (0, import_utils24.normalizeUrlPath)(basePath + route.path),
7539
+ path: (0, import_utils26.normalizeUrlPath)(basePath + route.path),
6289
7540
  tags: route.tags ?? [defaultTag]
6290
7541
  });
6291
7542
  }
@@ -6293,8 +7544,8 @@ function registerOpenApiRoute(runtime, basePath, defaultTag, route) {
6293
7544
  // src/routers/model-router.ts
6294
7545
  var clientErrors2 = import_express_json_router5.default.clientErrors;
6295
7546
  function setOption(parentKey, optionKey, option) {
6296
- const key = (0, import_utils25.isUndefined)(option) ? parentKey : `${parentKey}.${optionKey}`;
6297
- const value = (0, import_utils25.isUndefined)(option) ? optionKey : option;
7547
+ const key = (0, import_utils27.isUndefined)(option) ? parentKey : `${parentKey}.${optionKey}`;
7548
+ const value = (0, import_utils27.isUndefined)(option) ? optionKey : option;
6298
7549
  assertMutableRouterOption("model", key);
6299
7550
  this.runtime.setModelOption(this.modelName, key, value);
6300
7551
  return this;
@@ -6456,7 +7707,7 @@ var ModelRouter = class _ModelRouter {
6456
7707
  this.runtime = runtime;
6457
7708
  this.runtime.setModelOptions(modelName, initialOptions);
6458
7709
  this.options = this.runtime.getModelOptions(modelName);
6459
- this.fullBasePath = (0, import_utils25.normalizeUrlPath)(this.options.parentPath + this.options.basePath);
7710
+ this.fullBasePath = (0, import_utils27.normalizeUrlPath)(this.options.parentPath + this.options.basePath);
6460
7711
  this.modelName = modelName;
6461
7712
  this.router = new import_express_json_router5.default(this.options.basePath, createSetCore(this.runtime), accessRouterResponseHandler);
6462
7713
  this.model = new model_default(modelName, this.runtime);
@@ -6536,13 +7787,13 @@ var ModelRouter = class _ModelRouter {
6536
7787
  }
6537
7788
  logEndpoints() {
6538
7789
  runWithRuntime(this.runtime, () => {
6539
- (0, import_utils25.forEach)(this.router.getEndpoints(), ({ method, path }) => {
6540
- logInfoMessage(`${(0, import_utils25.padEnd)(method, 6)} ${(0, import_utils25.normalizeUrlPath)(this.options.parentPath + path)}`);
7790
+ (0, import_utils27.forEach)(this.router.getEndpoints(), ({ method, path }) => {
7791
+ logInfoMessage(`${(0, import_utils27.padEnd)(method, 6)} ${(0, import_utils27.normalizeUrlPath)(this.options.parentPath + path)}`);
6541
7792
  });
6542
7793
  });
6543
7794
  }
6544
7795
  set(keyOrOptions, value) {
6545
- if (arguments.length === 2 && (0, import_utils25.isString)(keyOrOptions)) {
7796
+ if (arguments.length === 2 && (0, import_utils27.isString)(keyOrOptions)) {
6546
7797
  assertMutableRouterOption("model", keyOrOptions);
6547
7798
  this.runtime.setModelOption(
6548
7799
  this.modelName,
@@ -6550,7 +7801,7 @@ var ModelRouter = class _ModelRouter {
6550
7801
  value
6551
7802
  );
6552
7803
  }
6553
- if (arguments.length === 1 && (0, import_utils25.isPlainObject)(keyOrOptions)) {
7804
+ if (arguments.length === 1 && (0, import_utils27.isPlainObject)(keyOrOptions)) {
6554
7805
  assertMutableRouterOptions("model", keyOrOptions);
6555
7806
  this.runtime.setModelOptions(this.modelName, keyOrOptions);
6556
7807
  }
@@ -6573,7 +7824,7 @@ var ModelRouter = class _ModelRouter {
6573
7824
 
6574
7825
  // src/routers/root-router.ts
6575
7826
  var import_express_json_router6 = __toESM(require("@web-ts-toolkit/express-json-router"));
6576
- var import_utils26 = require("@web-ts-toolkit/utils");
7827
+ var import_utils28 = require("@web-ts-toolkit/utils");
6577
7828
 
6578
7829
  // src/core-data.ts
6579
7830
  var DataCore = class {
@@ -6839,7 +8090,7 @@ var RootRouter = class {
6839
8090
  this.setRoutes();
6840
8091
  }
6841
8092
  registerOpenApiRoute(route) {
6842
- registerOpenApiRoute(this.runtime, (0, import_utils26.normalizeUrlPath)(this.basename), "root", route);
8093
+ registerOpenApiRoute(this.runtime, (0, import_utils28.normalizeUrlPath)(this.basename), "root", route);
6843
8094
  }
6844
8095
  wrapResult(index, item, result) {
6845
8096
  return {
@@ -6900,7 +8151,7 @@ var RootRouter = class {
6900
8151
  groupItemsByOrder(items) {
6901
8152
  const groups = /* @__PURE__ */ new Map();
6902
8153
  items.forEach((item, index) => {
6903
- const order = (0, import_utils26.isNumber)(item.order) ? item.order : 0;
8154
+ const order = (0, import_utils28.isNumber)(item.order) ? item.order : 0;
6904
8155
  const group = groups.get(order);
6905
8156
  if (group) {
6906
8157
  group.push({ index, item });
@@ -6971,7 +8222,7 @@ var RootRouter = class {
6971
8222
  const arrResult = await this.runGroup(req, groupedItems[x]);
6972
8223
  results.push(...arrResult);
6973
8224
  }
6974
- return (0, import_utils26.orderBy)(results, ["index"], ["asc"]);
8225
+ return (0, import_utils28.orderBy)(results, ["index"], ["asc"]);
6975
8226
  });
6976
8227
  this.registerOpenApiRoute({
6977
8228
  method: "post",
@@ -6989,11 +8240,11 @@ var RootRouter = class {
6989
8240
 
6990
8241
  // src/routers/data-router.ts
6991
8242
  var import_express_json_router7 = __toESM(require("@web-ts-toolkit/express-json-router"));
6992
- var import_utils27 = require("@web-ts-toolkit/utils");
8243
+ var import_utils29 = require("@web-ts-toolkit/utils");
6993
8244
  var clientErrors4 = import_express_json_router7.default.clientErrors;
6994
8245
  function setOption2(parentKey, optionKey, option) {
6995
- const key = (0, import_utils27.isUndefined)(option) ? parentKey : `${parentKey}.${optionKey}`;
6996
- const value = (0, import_utils27.isUndefined)(option) ? optionKey : option;
8246
+ const key = (0, import_utils29.isUndefined)(option) ? parentKey : `${parentKey}.${optionKey}`;
8247
+ const value = (0, import_utils29.isUndefined)(option) ? optionKey : option;
6997
8248
  assertMutableRouterOption("data", key);
6998
8249
  this.runtime.setDataOption(this.dataName, key, value);
6999
8250
  return this;
@@ -7034,7 +8285,7 @@ var DataRouter = class {
7034
8285
  this.runtime = runtime;
7035
8286
  this.runtime.setDataOptions(dataName, initialOptions);
7036
8287
  this.options = this.runtime.getDataOptions(dataName);
7037
- this.fullBasePath = (0, import_utils27.normalizeUrlPath)(this.options.parentPath + this.options.basePath);
8288
+ this.fullBasePath = (0, import_utils29.normalizeUrlPath)(this.options.parentPath + this.options.basePath);
7038
8289
  this.dataName = dataName;
7039
8290
  this.router = new import_express_json_router7.default(this.options.basePath, createSetDataCore(this.runtime), accessRouterResponseHandler);
7040
8291
  this.setCollectionRoutes();
@@ -7067,8 +8318,8 @@ var DataRouter = class {
7067
8318
  req.query
7068
8319
  );
7069
8320
  const svc = this.getService(req);
7070
- const includeCount = (0, import_utils23.parseBooleanString)(include_count);
7071
- const includeExtraHeaders = (0, import_utils23.parseBooleanString)(include_extra_headers);
8321
+ const includeCount = (0, import_utils25.parseBooleanString)(include_count);
8322
+ const includeExtraHeaders = (0, import_utils25.parseBooleanString)(include_extra_headers);
7072
8323
  const result = await svc.find(
7073
8324
  {},
7074
8325
  { skip, limit, page, pageSize: page_size },
@@ -7186,11 +8437,11 @@ var DataRouter = class {
7186
8437
  });
7187
8438
  }
7188
8439
  set(keyOrOptions, value) {
7189
- if (arguments.length === 2 && (0, import_utils27.isString)(keyOrOptions)) {
8440
+ if (arguments.length === 2 && (0, import_utils29.isString)(keyOrOptions)) {
7190
8441
  assertMutableRouterOption("data", keyOrOptions);
7191
8442
  this.runtime.setDataOption(this.dataName, keyOrOptions, value);
7192
8443
  }
7193
- if (arguments.length === 1 && (0, import_utils27.isPlainObject)(keyOrOptions)) {
8444
+ if (arguments.length === 1 && (0, import_utils29.isPlainObject)(keyOrOptions)) {
7194
8445
  assertMutableRouterOptions("data", keyOrOptions);
7195
8446
  this.runtime.setDataOptions(this.dataName, keyOrOptions);
7196
8447
  }
@@ -7313,7 +8564,7 @@ function createRuntimeApi(runtime) {
7313
8564
  const wtt2 = runtimeMiddleware;
7314
8565
  wtt2.runtime = runtime;
7315
8566
  wtt2.createRouter = function(modelName, options) {
7316
- if ((0, import_utils28.isUndefined)(options)) {
8567
+ if ((0, import_utils30.isUndefined)(options)) {
7317
8568
  return new RootRouter(modelName, runtime);
7318
8569
  }
7319
8570
  if (typeof modelName === "string") {
@@ -7334,10 +8585,10 @@ function createRuntimeApi(runtime) {
7334
8585
  };
7335
8586
  wtt2.combineRoutes = combineRoutes;
7336
8587
  wtt2.set = function(keyOrOptions, value) {
7337
- if (arguments.length === 2 && (0, import_utils28.isString)(keyOrOptions)) {
8588
+ if (arguments.length === 2 && (0, import_utils30.isString)(keyOrOptions)) {
7338
8589
  return runtime.setGlobalOption(keyOrOptions, value);
7339
8590
  }
7340
- if (arguments.length === 1 && (0, import_utils28.isPlainObject)(keyOrOptions)) {
8591
+ if (arguments.length === 1 && (0, import_utils30.isPlainObject)(keyOrOptions)) {
7341
8592
  return runtime.setGlobalOptions(keyOrOptions);
7342
8593
  }
7343
8594
  };