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