@web-ts-toolkit/access-router 0.54.0 → 0.55.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 +311 -8
- package/advanced.d.mts +1 -1
- package/advanced.d.ts +1 -1
- package/advanced.js +612 -155
- package/advanced.mjs +586 -129
- package/index.d.mts +5 -2
- package/index.d.ts +5 -2
- package/index.js +3046 -1904
- package/index.mjs +2954 -1812
- package/llms.txt +2 -0
- package/package.json +3 -3
- package/{parsers-ZSrF7SNu.d.mts → parsers-BrVRL2fg.d.mts} +609 -471
- package/{parsers-ZSrF7SNu.d.ts → parsers-BrVRL2fg.d.ts} +609 -471
package/advanced.mjs
CHANGED
|
@@ -41,7 +41,7 @@ var FilterOperator = /* @__PURE__ */ ((FilterOperator2) => {
|
|
|
41
41
|
})(FilterOperator || {});
|
|
42
42
|
|
|
43
43
|
// src/validation/parsers.ts
|
|
44
|
-
import
|
|
44
|
+
import JsonRouter3 from "@web-ts-toolkit/express-json-router";
|
|
45
45
|
|
|
46
46
|
// src/runtime.ts
|
|
47
47
|
import mongoose from "mongoose";
|
|
@@ -87,10 +87,10 @@ function assertPublicAdmission(input, kind = "envelope") {
|
|
|
87
87
|
const add = (child, childKind, key) => {
|
|
88
88
|
pending.push({ value: child, kind: childKind, path: `${path}.${key}` });
|
|
89
89
|
};
|
|
90
|
-
const
|
|
90
|
+
const record2 = value;
|
|
91
91
|
if (slot !== "data") {
|
|
92
92
|
for (const key of privateControls) {
|
|
93
|
-
if (key in
|
|
93
|
+
if (key in record2) {
|
|
94
94
|
throw new PublicAdmissionError(
|
|
95
95
|
`Unsupported public control ${path}.${key}; use explicit per-call internal service options for backend authorization`
|
|
96
96
|
);
|
|
@@ -106,16 +106,16 @@ function assertPublicAdmission(input, kind = "envelope") {
|
|
|
106
106
|
for (const [key, child] of Object.entries(value)) add(child, key === "$$sq" ? "envelope" : "data", key);
|
|
107
107
|
continue;
|
|
108
108
|
}
|
|
109
|
-
for (const key of ["args", "options", "sqOptions"]) add(
|
|
110
|
-
add(
|
|
111
|
-
add(
|
|
112
|
-
for (const key of ["filter", "match", "data"]) add(
|
|
109
|
+
for (const key of ["args", "options", "sqOptions"]) add(record2[key], "envelope", key);
|
|
110
|
+
add(record2.include, "include", "include");
|
|
111
|
+
add(record2.populate, "populate", "populate");
|
|
112
|
+
for (const key of ["filter", "match", "data"]) add(record2[key], "data", key);
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
115
|
function assertHydrationDefaults(defaults) {
|
|
116
116
|
if (defaults && typeof defaults === "object") {
|
|
117
|
-
const
|
|
118
|
-
if ("hydration" in
|
|
117
|
+
const record2 = defaults;
|
|
118
|
+
if ("hydration" in record2)
|
|
119
119
|
throw new TypeError("defaults.hydration is unsupported; use model hydration configuration");
|
|
120
120
|
for (const bucket of [
|
|
121
121
|
"publicListOptions",
|
|
@@ -131,7 +131,7 @@ function assertHydrationDefaults(defaults) {
|
|
|
131
131
|
"publicCreateOptions",
|
|
132
132
|
"publicUpdateOptions"
|
|
133
133
|
]) {
|
|
134
|
-
const options =
|
|
134
|
+
const options = record2[bucket];
|
|
135
135
|
if (!options || typeof options !== "object" && typeof options !== "function") continue;
|
|
136
136
|
for (const key of ["lean", "hydration"]) {
|
|
137
137
|
if (!(key in options)) continue;
|
|
@@ -181,12 +181,12 @@ import mschema2Jsonschema from "mongoose-schema-jsonschema";
|
|
|
181
181
|
import {
|
|
182
182
|
addLeadingSlash,
|
|
183
183
|
forEach as forEach2,
|
|
184
|
-
get as
|
|
184
|
+
get as get3,
|
|
185
185
|
isArray as isArray4,
|
|
186
186
|
isBoolean,
|
|
187
|
-
isFunction,
|
|
187
|
+
isFunction as isFunction2,
|
|
188
188
|
isNil as isNil2,
|
|
189
|
-
isPlainObject as
|
|
189
|
+
isPlainObject as isPlainObject12,
|
|
190
190
|
isString as isString3,
|
|
191
191
|
keys,
|
|
192
192
|
normalizeUrlPath,
|
|
@@ -196,6 +196,23 @@ import {
|
|
|
196
196
|
// src/helpers/query.ts
|
|
197
197
|
import { flattenDeep, isNil, isPlainObject as isPlainObject2, isString, reduce } from "@web-ts-toolkit/utils";
|
|
198
198
|
var DEFAULT_LIST_HARD_LIMIT = 1e3;
|
|
199
|
+
function normalizeSelect(select) {
|
|
200
|
+
if (Array.isArray(select)) return flattenDeep(select.map(normalizeSelect));
|
|
201
|
+
if (isPlainObject2(select)) {
|
|
202
|
+
return reduce(
|
|
203
|
+
select,
|
|
204
|
+
(ret, val, key) => {
|
|
205
|
+
if (val === 1) ret.push(key);
|
|
206
|
+
else if (val === -1) ret.push(`-${key}`);
|
|
207
|
+
return ret;
|
|
208
|
+
},
|
|
209
|
+
[]
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
if (isString(select))
|
|
213
|
+
return String(select).split(/[\s,]+/).map((v) => v.trim()).filter(Boolean);
|
|
214
|
+
return [];
|
|
215
|
+
}
|
|
199
216
|
|
|
200
217
|
// src/helpers/sort-policy.ts
|
|
201
218
|
var fieldPathPattern = /^[A-Za-z_][A-Za-z0-9_$]*(\.[A-Za-z_][A-Za-z0-9_$]*)*$/;
|
|
@@ -1111,6 +1128,482 @@ function validateRequestComplexity(value, options, scope = "request") {
|
|
|
1111
1128
|
return errors;
|
|
1112
1129
|
}
|
|
1113
1130
|
|
|
1131
|
+
// src/acl/virtual-descriptor.ts
|
|
1132
|
+
import { isFunction, isPlainObject as isPlainObject10 } from "@web-ts-toolkit/utils";
|
|
1133
|
+
var isVirtualContainerValue = (value) => isPlainObject10(value) && "sub" in value;
|
|
1134
|
+
function getScopeVirtualsObject(root, scopePath = []) {
|
|
1135
|
+
let current = root;
|
|
1136
|
+
for (let i = 0; i < scopePath.length; i += 2) {
|
|
1137
|
+
if (!current || scopePath[i + 1] !== "sub") return null;
|
|
1138
|
+
const container = current[scopePath[i]];
|
|
1139
|
+
if (!isVirtualContainerValue(container)) return null;
|
|
1140
|
+
current = container.sub;
|
|
1141
|
+
}
|
|
1142
|
+
return isPlainObject10(current) ? current : null;
|
|
1143
|
+
}
|
|
1144
|
+
function toDescriptor(leaf, shared = {}) {
|
|
1145
|
+
const record2 = isFunction(leaf) ? { get: leaf } : isPlainObject10(leaf) ? leaf : null;
|
|
1146
|
+
if (!record2 || !isFunction(record2.get)) return void 0;
|
|
1147
|
+
const dependsOn = record2.dependsOn ?? shared.dependsOn ?? [];
|
|
1148
|
+
const populated = record2.dependsOnPopulate === void 0 ? shared.dependsOnPopulate : record2.dependsOnPopulate;
|
|
1149
|
+
return {
|
|
1150
|
+
get: record2.get,
|
|
1151
|
+
dependsOn: [...dependsOn],
|
|
1152
|
+
...populated === void 0 ? {} : {
|
|
1153
|
+
dependsOnPopulate: populated.map(({ path, select }) => ({
|
|
1154
|
+
path,
|
|
1155
|
+
...select === void 0 ? {} : { select: Array.isArray(select) ? [...select] : typeof select === "object" ? { ...select } : select }
|
|
1156
|
+
}))
|
|
1157
|
+
}
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
function resolveSnapshotDescriptor(scopeVirtuals, virtualName, access) {
|
|
1161
|
+
const entry = scopeVirtuals?.[virtualName];
|
|
1162
|
+
if (entry === void 0 || isVirtualContainerValue(entry)) return void 0;
|
|
1163
|
+
if (isFunction(entry)) return toDescriptor(entry);
|
|
1164
|
+
if (!isPlainObject10(entry)) return void 0;
|
|
1165
|
+
const record2 = entry;
|
|
1166
|
+
if ("get" in record2) return toDescriptor(record2);
|
|
1167
|
+
return toDescriptor(record2[access], record2) ?? toDescriptor(record2.default, record2);
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
// src/acl/virtual-populate-dependencies.ts
|
|
1171
|
+
import { get as get2, isPlainObject as isPlainObject11 } from "@web-ts-toolkit/utils";
|
|
1172
|
+
|
|
1173
|
+
// src/acl/authorization-snapshot.ts
|
|
1174
|
+
import { isDeepStrictEqual } from "util";
|
|
1175
|
+
import { mongo } from "mongoose";
|
|
1176
|
+
import JsonRouter2 from "@web-ts-toolkit/express-json-router";
|
|
1177
|
+
var ServiceAuthorizationError = class extends JsonRouter2.clientErrors.BadRequestError {
|
|
1178
|
+
constructor(detail) {
|
|
1179
|
+
const errors = [{ detail: `Service authorization: ${detail}` }];
|
|
1180
|
+
super("Bad Request", { errors });
|
|
1181
|
+
this.result = { success: false, kind: "error", code: "bad_request" /* BadRequest */, errors };
|
|
1182
|
+
}
|
|
1183
|
+
};
|
|
1184
|
+
function authorizationError(detail) {
|
|
1185
|
+
throw new ServiceAuthorizationError(detail);
|
|
1186
|
+
}
|
|
1187
|
+
var unsafeKeys = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
1188
|
+
function authorizationRecord(value, label) {
|
|
1189
|
+
if (!value || typeof value !== "object" || Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) {
|
|
1190
|
+
authorizationError(`${label} must be a plain object`);
|
|
1191
|
+
}
|
|
1192
|
+
return value;
|
|
1193
|
+
}
|
|
1194
|
+
function copyAuthorizationValue(value, configuration = false) {
|
|
1195
|
+
const active = /* @__PURE__ */ new WeakSet();
|
|
1196
|
+
let nodes = 0;
|
|
1197
|
+
function copy(input, depth) {
|
|
1198
|
+
if (++nodes > 2e4 || depth > 100) authorizationError("snapshot exceeds structural limits");
|
|
1199
|
+
if (typeof input === "function") {
|
|
1200
|
+
if (configuration === true) return input;
|
|
1201
|
+
authorizationError("functions are not policy values");
|
|
1202
|
+
}
|
|
1203
|
+
if (typeof input === "symbol") authorizationError("symbols are not policy values");
|
|
1204
|
+
if (input === null || typeof input !== "object") return input;
|
|
1205
|
+
if (active.has(input)) authorizationError("cyclic input");
|
|
1206
|
+
active.add(input);
|
|
1207
|
+
try {
|
|
1208
|
+
if (input instanceof Date) return new Date(input.getTime());
|
|
1209
|
+
if (Buffer.isBuffer(input)) return Buffer.from(input);
|
|
1210
|
+
if (input instanceof RegExp) {
|
|
1211
|
+
const result2 = new RegExp(input.source, input.flags);
|
|
1212
|
+
result2.lastIndex = input.lastIndex;
|
|
1213
|
+
return result2;
|
|
1214
|
+
}
|
|
1215
|
+
if (input instanceof mongo.BSON.BSONValue) {
|
|
1216
|
+
try {
|
|
1217
|
+
return mongo.BSON.deserialize(mongo.BSON.serialize({ value: input }), {
|
|
1218
|
+
promoteValues: false,
|
|
1219
|
+
bsonRegExp: true
|
|
1220
|
+
}).value;
|
|
1221
|
+
} catch {
|
|
1222
|
+
authorizationError("unsupported BSON snapshot");
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
if (configuration && input instanceof Map) {
|
|
1226
|
+
return new Map([...input].map(([key, entry]) => [copy(key, depth + 1), copy(entry, depth + 1)]));
|
|
1227
|
+
}
|
|
1228
|
+
if (configuration && input instanceof Set) return new Set([...input].map((entry) => copy(entry, depth + 1)));
|
|
1229
|
+
const array = Array.isArray(input);
|
|
1230
|
+
if (array && input.length > 2e4) authorizationError("snapshot array exceeds structural limits");
|
|
1231
|
+
if (!array) authorizationRecord(input, "snapshot");
|
|
1232
|
+
const result = array ? [] : {};
|
|
1233
|
+
for (const key of Reflect.ownKeys(input)) {
|
|
1234
|
+
if (array && key === "length") continue;
|
|
1235
|
+
if (typeof key !== "string" || unsafeKeys.has(key)) authorizationError("unsafe snapshot key");
|
|
1236
|
+
if (array && !/^(0|[1-9]\d*)$/.test(key)) authorizationError("invalid array property");
|
|
1237
|
+
const descriptor = Object.getOwnPropertyDescriptor(input, key);
|
|
1238
|
+
if (!("value" in descriptor) || !descriptor.enumerable && configuration !== "document")
|
|
1239
|
+
authorizationError("accessor/non-enumerable input");
|
|
1240
|
+
Object.defineProperty(result, key, {
|
|
1241
|
+
value: copy(descriptor.value, depth + 1),
|
|
1242
|
+
enumerable: true,
|
|
1243
|
+
writable: true,
|
|
1244
|
+
configurable: true
|
|
1245
|
+
});
|
|
1246
|
+
}
|
|
1247
|
+
if (array) result.length = input.length;
|
|
1248
|
+
return Object.freeze(result);
|
|
1249
|
+
} finally {
|
|
1250
|
+
active.delete(input);
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
return copy(value, 0);
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
// src/acl/field-catalog.ts
|
|
1257
|
+
var safeSegment = (part) => !!part && part !== "sub" && !["__proto__", "constructor", "prototype", "_doc", "__v"].includes(part) && !/^[+$-]|[\s*]/.test(part) && !part.includes("[") && !part.includes("]") && !/^\d+$/.test(part) && ![...part].some((character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127);
|
|
1258
|
+
var isSafeOutputPath = (path) => path.split(".").every(safeSegment);
|
|
1259
|
+
var inputControlSegments = /* @__PURE__ */ new Set([
|
|
1260
|
+
"_permissions",
|
|
1261
|
+
"_view",
|
|
1262
|
+
"_edit",
|
|
1263
|
+
"documentAuthorization",
|
|
1264
|
+
"documentAuthorizationStage",
|
|
1265
|
+
"resolvedAuthorization",
|
|
1266
|
+
"execution",
|
|
1267
|
+
"executionContext",
|
|
1268
|
+
"serviceExecutionContext",
|
|
1269
|
+
"preparedQuery",
|
|
1270
|
+
"preparedFilter",
|
|
1271
|
+
"preparedModelQuery",
|
|
1272
|
+
"resolvedIdentifier",
|
|
1273
|
+
"skipParsing",
|
|
1274
|
+
"skipAuthorization",
|
|
1275
|
+
"alreadyAuthorized"
|
|
1276
|
+
]);
|
|
1277
|
+
function isSafeInputPath(path, requestIdentity = false) {
|
|
1278
|
+
return isSafeOutputPath(path) && path.split(".").every((part) => !inputControlSegments.has(part) && (requestIdentity || part !== "_id"));
|
|
1279
|
+
}
|
|
1280
|
+
function matchesCatalogPattern(pattern, path, descendants = false) {
|
|
1281
|
+
const expected = pattern.split(".");
|
|
1282
|
+
const actual = path.split(".");
|
|
1283
|
+
return (descendants ? actual.length >= expected.length : actual.length === expected.length) && expected.every((part, index) => part === "$*" || part === actual[index]);
|
|
1284
|
+
}
|
|
1285
|
+
function captureAuthorizationFieldCatalog(schema, virtuals, documentPermissionField = "_permissions") {
|
|
1286
|
+
const paths = /* @__PURE__ */ new Set();
|
|
1287
|
+
const containers = /* @__PURE__ */ new Set();
|
|
1288
|
+
const opaque = /* @__PURE__ */ new Set();
|
|
1289
|
+
const references = /* @__PURE__ */ new Set();
|
|
1290
|
+
const virtualPaths = /* @__PURE__ */ new Set();
|
|
1291
|
+
const virtualScopes = /* @__PURE__ */ new Set();
|
|
1292
|
+
const schemaVirtualPaths = /* @__PURE__ */ new Set();
|
|
1293
|
+
const arrays = /* @__PURE__ */ new Set();
|
|
1294
|
+
const protectedPaths = /* @__PURE__ */ new Set([documentPermissionField, "_doc", "$__", "__v"]);
|
|
1295
|
+
const add = (path) => {
|
|
1296
|
+
if (path.split(".").every((part) => part === "$*" || safeSegment(part))) paths.add(path);
|
|
1297
|
+
};
|
|
1298
|
+
const ancestors = (path) => {
|
|
1299
|
+
const parts = path.split(".");
|
|
1300
|
+
for (let n = 1; n < parts.length; n++) {
|
|
1301
|
+
const parent = parts.slice(0, n).join(".");
|
|
1302
|
+
add(parent);
|
|
1303
|
+
containers.add(parent);
|
|
1304
|
+
}
|
|
1305
|
+
};
|
|
1306
|
+
const visitType = (type, path, active) => {
|
|
1307
|
+
add(path);
|
|
1308
|
+
ancestors(path);
|
|
1309
|
+
const element = type.$embeddedSchemaType ?? type.embeddedSchemaType ?? type.caster;
|
|
1310
|
+
if (type.instance === "Array" || type.$isMongooseDocumentArray) arrays.add(path);
|
|
1311
|
+
if (type.options?.ref || type.options?.refPath || element?.options?.ref || element?.options?.refPath) {
|
|
1312
|
+
references.add(path);
|
|
1313
|
+
return;
|
|
1314
|
+
}
|
|
1315
|
+
if (type.$isSchemaMap || type.instance === "Map") {
|
|
1316
|
+
const value = type.$__schemaType;
|
|
1317
|
+
if (value) visitType(value, `${path}.$*`, active);
|
|
1318
|
+
else opaque.add(path);
|
|
1319
|
+
containers.delete(path);
|
|
1320
|
+
return;
|
|
1321
|
+
}
|
|
1322
|
+
const child = type.schema ?? element?.schema;
|
|
1323
|
+
if (child) {
|
|
1324
|
+
containers.add(path);
|
|
1325
|
+
if (!active.has(child)) visitSchema(child, path, /* @__PURE__ */ new Set([...active, child]));
|
|
1326
|
+
return;
|
|
1327
|
+
}
|
|
1328
|
+
if (type.instance === "Mixed" || element?.instance === "Mixed") opaque.add(path);
|
|
1329
|
+
};
|
|
1330
|
+
const visitSchema = (current, prefix, active) => {
|
|
1331
|
+
const version = current.options.versionKey;
|
|
1332
|
+
if (typeof version === "string") protectedPaths.add(prefix ? `${prefix}.${version}` : version);
|
|
1333
|
+
for (const name of Object.keys(current.virtuals)) {
|
|
1334
|
+
schemaVirtualPaths.add(prefix ? `${prefix}.${name}` : name);
|
|
1335
|
+
}
|
|
1336
|
+
for (const [name, raw] of Object.entries(current.paths)) {
|
|
1337
|
+
if (name.includes("$*")) continue;
|
|
1338
|
+
visitType(raw, prefix ? `${prefix}.${name}` : name, active);
|
|
1339
|
+
}
|
|
1340
|
+
};
|
|
1341
|
+
visitSchema(schema, "", /* @__PURE__ */ new Set([schema]));
|
|
1342
|
+
const persistedPaths = [...paths];
|
|
1343
|
+
const visitVirtuals = (record2, prefix) => {
|
|
1344
|
+
for (const [name, value] of Object.entries(record2 ?? {})) {
|
|
1345
|
+
if (value === void 0) continue;
|
|
1346
|
+
const path = prefix ? `${prefix}.${name}` : name;
|
|
1347
|
+
if (!isSafeOutputPath(path)) continue;
|
|
1348
|
+
if (value && typeof value === "object" && "sub" in value) {
|
|
1349
|
+
if (references.has(path)) continue;
|
|
1350
|
+
virtualScopes.add(path);
|
|
1351
|
+
add(path);
|
|
1352
|
+
containers.add(path);
|
|
1353
|
+
visitVirtuals(value.sub, path);
|
|
1354
|
+
} else {
|
|
1355
|
+
add(path);
|
|
1356
|
+
ancestors(path);
|
|
1357
|
+
virtualPaths.add(path);
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
};
|
|
1361
|
+
visitVirtuals(virtuals, "");
|
|
1362
|
+
const eligible = [...paths].filter((p) => ![...protectedPaths].some((v) => matchesCatalogPattern(v, p, true)));
|
|
1363
|
+
return Object.freeze({
|
|
1364
|
+
paths: Object.freeze(eligible.filter((p) => !p.includes("$*"))),
|
|
1365
|
+
persistedPaths: Object.freeze(persistedPaths),
|
|
1366
|
+
arrayPaths: Object.freeze([...arrays]),
|
|
1367
|
+
schemaVirtualPaths: Object.freeze([...schemaVirtualPaths]),
|
|
1368
|
+
metadataPaths: Object.freeze([documentPermissionField]),
|
|
1369
|
+
dynamicPaths: Object.freeze(eligible.filter((p) => p.includes("$*"))),
|
|
1370
|
+
containerPaths: Object.freeze([...containers]),
|
|
1371
|
+
opaquePaths: Object.freeze([...opaque]),
|
|
1372
|
+
referencePaths: Object.freeze([...references]),
|
|
1373
|
+
virtualPaths: Object.freeze([...virtualPaths]),
|
|
1374
|
+
virtualScopePaths: Object.freeze([...virtualScopes]),
|
|
1375
|
+
protectedPaths: Object.freeze([...protectedPaths])
|
|
1376
|
+
});
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
// src/acl/virtual-populate-dependencies.ts
|
|
1380
|
+
var elementType = (type) => type.$embeddedSchemaType ?? type.embeddedSchemaType ?? type.caster;
|
|
1381
|
+
var join = (prefix, path) => prefix ? `${prefix}.${path}` : path;
|
|
1382
|
+
var within = (path, parent) => path === parent || path.startsWith(`${parent}.`);
|
|
1383
|
+
var accesses = ["default", "list", "create", "read", "update"];
|
|
1384
|
+
var fail = (label, message) => {
|
|
1385
|
+
throw new Error(`Virtual ${label}: dependsOnPopulate ${message}`);
|
|
1386
|
+
};
|
|
1387
|
+
var varyingSchema = (schema) => Object.keys(schema.discriminators ?? {}).length > 0 || schema.discriminatorMapping?.isRoot === false;
|
|
1388
|
+
var varyingModel = (model) => !!model.baseModelName || Object.keys(model.discriminators ?? {}).length > 0 || varyingSchema(model.schema);
|
|
1389
|
+
function record(value, label) {
|
|
1390
|
+
if (!isPlainObject11(value)) return fail(label, "entries must be plain { path, select? } objects");
|
|
1391
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
1392
|
+
const property = Object.getOwnPropertyDescriptor(value, key);
|
|
1393
|
+
if (typeof key !== "string" || !property.enumerable || !("value" in property))
|
|
1394
|
+
fail(label, "requires enumerable data properties");
|
|
1395
|
+
}
|
|
1396
|
+
return value;
|
|
1397
|
+
}
|
|
1398
|
+
function targetSelectionCatalog(schema, options) {
|
|
1399
|
+
const leaves = /* @__PURE__ */ new Set();
|
|
1400
|
+
const containers = /* @__PURE__ */ new Set();
|
|
1401
|
+
const forbidden = /* @__PURE__ */ new Set([options.documentPermissionField ?? "_permissions"]);
|
|
1402
|
+
const visit = (current, prefix, active) => {
|
|
1403
|
+
const version = current.options.versionKey;
|
|
1404
|
+
if (typeof version === "string") forbidden.add(join(prefix, version));
|
|
1405
|
+
for (const name of Object.keys(current.virtuals)) forbidden.add(join(prefix, name));
|
|
1406
|
+
for (const name of Object.keys(current.aliases ?? {})) forbidden.add(join(prefix, name));
|
|
1407
|
+
for (const [name, raw] of Object.entries(current.paths)) {
|
|
1408
|
+
const path = join(prefix, name);
|
|
1409
|
+
if (!isSafeInputPath(path, true)) continue;
|
|
1410
|
+
const parts = path.split(".");
|
|
1411
|
+
for (let n = 1; n < parts.length; n++) containers.add(parts.slice(0, n).join("."));
|
|
1412
|
+
let type = raw;
|
|
1413
|
+
while (type.instance === "Array" && elementType(type)) type = elementType(type);
|
|
1414
|
+
if (type.instance === "Mixed" || type.instance === "Map") {
|
|
1415
|
+
forbidden.add(path);
|
|
1416
|
+
continue;
|
|
1417
|
+
}
|
|
1418
|
+
const child = type.schema ?? raw.schema;
|
|
1419
|
+
if (raw.options?.ref !== void 0 || raw.options?.refPath !== void 0 || type.options?.ref !== void 0 || type.options?.refPath !== void 0) {
|
|
1420
|
+
if (child) forbidden.add(path);
|
|
1421
|
+
else leaves.add(path);
|
|
1422
|
+
continue;
|
|
1423
|
+
}
|
|
1424
|
+
if (child) {
|
|
1425
|
+
if (active.has(child) || varyingSchema(child)) forbidden.add(path);
|
|
1426
|
+
else {
|
|
1427
|
+
containers.add(path);
|
|
1428
|
+
visit(child, path, /* @__PURE__ */ new Set([...active, child]));
|
|
1429
|
+
}
|
|
1430
|
+
} else leaves.add(path);
|
|
1431
|
+
}
|
|
1432
|
+
};
|
|
1433
|
+
visit(schema, "", /* @__PURE__ */ new Set([schema]));
|
|
1434
|
+
const visitVirtuals = (virtuals, prefix) => {
|
|
1435
|
+
for (const [name, entry] of Object.entries(virtuals)) {
|
|
1436
|
+
if (entry === void 0) continue;
|
|
1437
|
+
const path = join(prefix, name);
|
|
1438
|
+
if (isVirtualContainerValue(entry)) visitVirtuals(entry.sub, path);
|
|
1439
|
+
else forbidden.add(path);
|
|
1440
|
+
}
|
|
1441
|
+
};
|
|
1442
|
+
visitVirtuals(options.virtuals ?? {}, "");
|
|
1443
|
+
const allowed = (path) => ![...forbidden].some((blocked) => within(path, blocked));
|
|
1444
|
+
return {
|
|
1445
|
+
leaves: [...leaves].filter(allowed).sort(),
|
|
1446
|
+
paths: new Set([...leaves, ...containers].filter(allowed))
|
|
1447
|
+
};
|
|
1448
|
+
}
|
|
1449
|
+
function normalizeSelection(select, catalog, label) {
|
|
1450
|
+
if (select !== void 0 && typeof select !== "string") {
|
|
1451
|
+
if (Array.isArray(select)) {
|
|
1452
|
+
if (Array.from(select).some((token) => typeof token !== "string")) fail(label, "select arrays require strings");
|
|
1453
|
+
} else {
|
|
1454
|
+
const projection = record(select, label);
|
|
1455
|
+
for (const [path, value] of Object.entries(projection)) {
|
|
1456
|
+
if (!isSafeInputPath(path, true) || value !== 1 && value !== -1)
|
|
1457
|
+
fail(label, "select objects require safe persisted paths with 1/-1 values");
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
const tokens = normalizeSelect(select);
|
|
1462
|
+
for (const token of tokens) {
|
|
1463
|
+
const path = token.startsWith("-") ? token.slice(1) : token;
|
|
1464
|
+
if (!isSafeInputPath(path, true) || !catalog.paths.has(path))
|
|
1465
|
+
fail(label, `select path "${path}" is not an eligible static persisted target path`);
|
|
1466
|
+
}
|
|
1467
|
+
const positive = tokens.filter((token) => !token.startsWith("-"));
|
|
1468
|
+
const negative = tokens.filter((token) => token.startsWith("-")).map((token) => token.slice(1));
|
|
1469
|
+
const selection = catalog.leaves.filter(
|
|
1470
|
+
(path) => (path === "_id" || positive.length === 0 || positive.some((included) => within(path, included))) && !negative.some((excluded) => within(path, excluded))
|
|
1471
|
+
);
|
|
1472
|
+
return {
|
|
1473
|
+
selection: Object.freeze(selection),
|
|
1474
|
+
fetchSelection: Object.freeze([.../* @__PURE__ */ new Set(["_id", ...selection])].sort()),
|
|
1475
|
+
includeId: selection.includes("_id")
|
|
1476
|
+
};
|
|
1477
|
+
}
|
|
1478
|
+
function captureVirtualPopulateDependencies(runtime, modelName, options) {
|
|
1479
|
+
const model = runtime.getModelInstance(modelName);
|
|
1480
|
+
const targets = /* @__PURE__ */ new Map();
|
|
1481
|
+
const scopes = [];
|
|
1482
|
+
const visit = (virtuals, schema, schemaPrefix, scopePath, unsupportedScope) => {
|
|
1483
|
+
const sourcePrefix = scopePath.filter((_, index) => index % 2 === 0).join(".");
|
|
1484
|
+
const captureList = (value, label) => {
|
|
1485
|
+
if (value === void 0) return Object.freeze([]);
|
|
1486
|
+
if (!Array.isArray(value)) return fail(label, "must be an array");
|
|
1487
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1488
|
+
return Object.freeze(
|
|
1489
|
+
Array.from(value, (entry) => {
|
|
1490
|
+
const dependency = record(entry, label);
|
|
1491
|
+
if (Object.keys(dependency).some((key) => key !== "path" && key !== "select"))
|
|
1492
|
+
fail(label, "accepts only path/select");
|
|
1493
|
+
const path = dependency.path;
|
|
1494
|
+
if (typeof path !== "string" || path.includes(".") || !isSafeInputPath(path, true))
|
|
1495
|
+
return fail(label, "path must be a safe scope-local persisted reference key");
|
|
1496
|
+
if (seen.has(path)) fail(label, `duplicate path "${path}"`);
|
|
1497
|
+
seen.add(path);
|
|
1498
|
+
if (within(join(sourcePrefix, path), options.documentPermissionField ?? "_permissions") || path === schema?.options.versionKey)
|
|
1499
|
+
fail(label, "source permission metadata/version paths are not reference inputs");
|
|
1500
|
+
if (!model || !schema || unsupportedScope)
|
|
1501
|
+
return fail(label, "requires a static embedded, non-discriminator source scope");
|
|
1502
|
+
const type = schema.path(join(schemaPrefix, path));
|
|
1503
|
+
const isArray6 = type?.instance === "Array";
|
|
1504
|
+
const scalar = type && (isArray6 ? elementType(type) : type);
|
|
1505
|
+
const ref = type?.options?.ref ?? scalar?.options?.ref;
|
|
1506
|
+
if (!scalar || scalar.instance !== "ObjectId" || typeof ref !== "string" || !ref || type?.options?.refPath !== void 0 || scalar.options?.refPath !== void 0)
|
|
1507
|
+
return fail(label, `path "${path}" requires a static string ref on an ObjectId scalar or 1D array`);
|
|
1508
|
+
let captured = targets.get(ref);
|
|
1509
|
+
if (!captured) {
|
|
1510
|
+
if (ref !== modelName && !runtime.hasModel(ref))
|
|
1511
|
+
return fail(label, `target "${ref}" must be configured first in this runtime`);
|
|
1512
|
+
const targetModel = runtime.getModelInstance(ref);
|
|
1513
|
+
if (!targetModel || targetModel.db !== model.db || model.db.models[ref] !== targetModel || varyingModel(targetModel) || targetModel.schema.path("_id")?.instance !== "ObjectId")
|
|
1514
|
+
return fail(
|
|
1515
|
+
label,
|
|
1516
|
+
`target "${ref}" requires a registered same-connection non-discriminator ObjectId model bound to the source ref`
|
|
1517
|
+
);
|
|
1518
|
+
const targetOptions = ref === modelName ? options : runtime.getModelOptions(ref);
|
|
1519
|
+
const defaults = runtime.getDefaultModelOptions();
|
|
1520
|
+
const readOption = (key) => resolveNestedOption((path2, fallback) => get2(targetOptions, path2, fallback), key, get2(defaults, key));
|
|
1521
|
+
const documentPermissionField = targetOptions.documentPermissionField ?? defaults.documentPermissionField;
|
|
1522
|
+
const catalog = captureAuthorizationFieldCatalog(targetModel.schema, void 0, documentPermissionField);
|
|
1523
|
+
const configuration = copyAuthorizationValue(
|
|
1524
|
+
{
|
|
1525
|
+
catalog,
|
|
1526
|
+
documentPermissionField,
|
|
1527
|
+
operationAccess: { read: readOption("operationAccess.read") },
|
|
1528
|
+
baseFilter: { read: readOption("baseFilter.read") },
|
|
1529
|
+
overrideFilter: { read: readOption("overrideFilter.read") },
|
|
1530
|
+
hydration: targetOptions.hydration
|
|
1531
|
+
},
|
|
1532
|
+
true
|
|
1533
|
+
);
|
|
1534
|
+
captured = {
|
|
1535
|
+
target: Object.freeze({
|
|
1536
|
+
identity: Object.freeze({}),
|
|
1537
|
+
runtime,
|
|
1538
|
+
modelName: ref,
|
|
1539
|
+
model: targetModel,
|
|
1540
|
+
schema: targetModel.schema,
|
|
1541
|
+
configuration
|
|
1542
|
+
}),
|
|
1543
|
+
catalog: targetSelectionCatalog(targetModel.schema, { ...targetOptions, documentPermissionField })
|
|
1544
|
+
};
|
|
1545
|
+
targets.set(ref, captured);
|
|
1546
|
+
}
|
|
1547
|
+
return Object.freeze({
|
|
1548
|
+
path,
|
|
1549
|
+
sourcePath: join(sourcePrefix, path),
|
|
1550
|
+
cardinality: isArray6 ? "array" : "scalar",
|
|
1551
|
+
target: captured.target,
|
|
1552
|
+
...normalizeSelection(dependency.select, captured.catalog, label)
|
|
1553
|
+
});
|
|
1554
|
+
})
|
|
1555
|
+
);
|
|
1556
|
+
};
|
|
1557
|
+
for (const [name, raw] of Object.entries(virtuals)) {
|
|
1558
|
+
if (isVirtualContainerValue(raw)) {
|
|
1559
|
+
const path = join(schemaPrefix, name);
|
|
1560
|
+
const type = schema?.path(path);
|
|
1561
|
+
const element = type && elementType(type);
|
|
1562
|
+
const child = type?.schema ?? element?.schema;
|
|
1563
|
+
const nonEmbedded = [type, element].some(
|
|
1564
|
+
(entry2) => entry2 && (entry2.options?.ref !== void 0 || entry2.options?.refPath !== void 0 || entry2.instance === "Map" || entry2.instance === "Mixed")
|
|
1565
|
+
);
|
|
1566
|
+
visit(
|
|
1567
|
+
raw.sub,
|
|
1568
|
+
child ?? schema,
|
|
1569
|
+
child ? "" : path,
|
|
1570
|
+
[...scopePath, name, "sub"],
|
|
1571
|
+
unsupportedScope || nonEmbedded || !!(child && varyingSchema(child))
|
|
1572
|
+
);
|
|
1573
|
+
continue;
|
|
1574
|
+
}
|
|
1575
|
+
if (!isPlainObject11(raw)) continue;
|
|
1576
|
+
const entry = raw;
|
|
1577
|
+
const label = join(sourcePrefix, name);
|
|
1578
|
+
captureList(entry.dependsOnPopulate, label);
|
|
1579
|
+
if (!("get" in entry))
|
|
1580
|
+
for (const access of accesses) {
|
|
1581
|
+
if (isPlainObject11(entry[access]))
|
|
1582
|
+
captureList(entry[access].dependsOnPopulate, `${label}.${access}`);
|
|
1583
|
+
}
|
|
1584
|
+
const byAccess = {};
|
|
1585
|
+
let declared = false;
|
|
1586
|
+
for (const access of accesses) {
|
|
1587
|
+
const descriptor = resolveSnapshotDescriptor(virtuals, name, access);
|
|
1588
|
+
if (descriptor) {
|
|
1589
|
+
byAccess[access] = captureList(descriptor.dependsOnPopulate, `${label}.${access}`);
|
|
1590
|
+
declared ||= descriptor.dependsOnPopulate !== void 0;
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
if (declared)
|
|
1594
|
+
scopes.push(
|
|
1595
|
+
Object.freeze({
|
|
1596
|
+
scopePath: Object.freeze([...scopePath]),
|
|
1597
|
+
virtualName: name,
|
|
1598
|
+
byAccess: Object.freeze(byAccess)
|
|
1599
|
+
})
|
|
1600
|
+
);
|
|
1601
|
+
}
|
|
1602
|
+
};
|
|
1603
|
+
visit(options.virtuals ?? {}, model?.schema, "", [], !!model && varyingModel(model));
|
|
1604
|
+
return Object.freeze({ scopes: Object.freeze(scopes) });
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1114
1607
|
// src/runtime.ts
|
|
1115
1608
|
var mongooseJsonSchemaInitialized = false;
|
|
1116
1609
|
var ensureMongooseJsonSchemaInitialized = () => {
|
|
@@ -1151,7 +1644,7 @@ var hasModelPermission = (value, modelPermissionPrefix) => {
|
|
|
1151
1644
|
return true;
|
|
1152
1645
|
});
|
|
1153
1646
|
}
|
|
1154
|
-
return
|
|
1647
|
+
return isFunction2(value);
|
|
1155
1648
|
};
|
|
1156
1649
|
var classifyPermissionSchema = (permissionSchema, modelPermissionPrefix) => {
|
|
1157
1650
|
const schemaKeys = Object.keys(permissionSchema);
|
|
@@ -1159,7 +1652,7 @@ var classifyPermissionSchema = (permissionSchema, modelPermissionPrefix) => {
|
|
|
1159
1652
|
const modelPermissionKeys = {};
|
|
1160
1653
|
forEach2(schemaKeys, (schemaKey) => {
|
|
1161
1654
|
const schemaRule = permissionSchema[schemaKey];
|
|
1162
|
-
const ruleEntries =
|
|
1655
|
+
const ruleEntries = isPlainObject12(schemaRule) ? Object.entries(schemaRule) : FIELD_ACCESS_KEYS.map((accessKey) => [accessKey, schemaRule]);
|
|
1163
1656
|
for (let x = 0; x < ruleEntries.length; x++) {
|
|
1164
1657
|
const [accessKey, value] = ruleEntries[x];
|
|
1165
1658
|
if (!isArray4(globalPermissionKeys[accessKey])) {
|
|
@@ -1200,13 +1693,13 @@ var VIRTUAL_UNSUPPORTED_ACCESSES = ["delete", "distinct", "count"];
|
|
|
1200
1693
|
var VIRTUAL_RESERVED_NAMES = /* @__PURE__ */ new Set(["_id", "__v", "id"]);
|
|
1201
1694
|
var VIRTUAL_DANGEROUS_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
1202
1695
|
var VIRTUAL_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_$]*$/;
|
|
1203
|
-
var
|
|
1696
|
+
var isVirtualContainerValue2 = (value) => isPlainObject12(value) && "sub" in value;
|
|
1204
1697
|
var virtualFullPath = (prefix, name) => prefix ? `${prefix}.${name}` : name;
|
|
1205
1698
|
var virtualPathsOverlap = (virtualPath, permissionField) => {
|
|
1206
1699
|
if (!virtualPath || !permissionField) return false;
|
|
1207
1700
|
try {
|
|
1208
|
-
if (
|
|
1209
|
-
if (
|
|
1701
|
+
if (get3(set3({}, virtualPath, true), permissionField) !== void 0) return true;
|
|
1702
|
+
if (get3(set3({}, permissionField, true), virtualPath) !== void 0) return true;
|
|
1210
1703
|
} catch {
|
|
1211
1704
|
return false;
|
|
1212
1705
|
}
|
|
@@ -1294,18 +1787,20 @@ var validateVirtualDependsOn = (dependsOn, entryLabel, storedFields, siblingVirt
|
|
|
1294
1787
|
return list;
|
|
1295
1788
|
};
|
|
1296
1789
|
var validateVirtualLeafValue = (value, entryLabel, storedFields, siblingVirtualNames, sharedDependsOn) => {
|
|
1297
|
-
if (
|
|
1298
|
-
if (!
|
|
1790
|
+
if (isFunction2(value)) return;
|
|
1791
|
+
if (!isPlainObject12(value)) {
|
|
1299
1792
|
throw new Error(`Virtual ${entryLabel}: descriptor must be a getter function or { get, dependsOn }`);
|
|
1300
1793
|
}
|
|
1301
|
-
const
|
|
1302
|
-
if (!
|
|
1794
|
+
const record2 = value;
|
|
1795
|
+
if (!isFunction2(record2.get)) {
|
|
1303
1796
|
throw new Error(`Virtual ${entryLabel}: descriptor.get must be a callable getter function`);
|
|
1304
1797
|
}
|
|
1305
|
-
validateVirtualDependsOn(
|
|
1306
|
-
for (const k of Object.keys(
|
|
1307
|
-
if (k !== "get" && k !== "dependsOn") {
|
|
1308
|
-
throw new Error(
|
|
1798
|
+
validateVirtualDependsOn(record2.dependsOn ?? sharedDependsOn, entryLabel, storedFields, siblingVirtualNames);
|
|
1799
|
+
for (const k of Object.keys(record2)) {
|
|
1800
|
+
if (k !== "get" && k !== "dependsOn" && k !== "dependsOnPopulate") {
|
|
1801
|
+
throw new Error(
|
|
1802
|
+
`Virtual ${entryLabel}: unsupported descriptor key "${k}" (expected get/dependsOn/dependsOnPopulate)`
|
|
1803
|
+
);
|
|
1309
1804
|
}
|
|
1310
1805
|
}
|
|
1311
1806
|
};
|
|
@@ -1314,7 +1809,7 @@ var validateVirtualsScope = (scope, documentPermissionField) => {
|
|
|
1314
1809
|
const leafNames = /* @__PURE__ */ new Set();
|
|
1315
1810
|
for (const [name, raw] of Object.entries(scopeVirtuals)) {
|
|
1316
1811
|
if (raw === void 0) continue;
|
|
1317
|
-
if (
|
|
1812
|
+
if (isVirtualContainerValue2(raw)) continue;
|
|
1318
1813
|
leafNames.add(name);
|
|
1319
1814
|
}
|
|
1320
1815
|
for (const [name, raw] of Object.entries(scopeVirtuals)) {
|
|
@@ -1331,15 +1826,15 @@ var validateVirtualsScope = (scope, documentPermissionField) => {
|
|
|
1331
1826
|
`Virtual ${scopeLabel}${name}: overlaps document permission field "${documentPermissionField}" (equal/ancestor/descendant rejected)`
|
|
1332
1827
|
);
|
|
1333
1828
|
}
|
|
1334
|
-
if (
|
|
1335
|
-
const
|
|
1336
|
-
const extra = Object.keys(
|
|
1829
|
+
if (isVirtualContainerValue2(raw)) {
|
|
1830
|
+
const record3 = raw;
|
|
1831
|
+
const extra = Object.keys(record3).filter((k) => k !== "sub");
|
|
1337
1832
|
if (extra.length > 0) {
|
|
1338
1833
|
throw new Error(
|
|
1339
1834
|
`Virtual ${scopeLabel}${name}: scope-container must only hold "sub" (embedded container "${name}" is not a virtual-leaf collision)`
|
|
1340
1835
|
);
|
|
1341
1836
|
}
|
|
1342
|
-
if (!
|
|
1837
|
+
if (!isPlainObject12(record3.sub)) {
|
|
1343
1838
|
throw new Error(`Virtual ${scopeLabel}${name}: scope-container "sub" must be a plain object`);
|
|
1344
1839
|
}
|
|
1345
1840
|
continue;
|
|
@@ -1349,19 +1844,19 @@ var validateVirtualsScope = (scope, documentPermissionField) => {
|
|
|
1349
1844
|
`Virtual ${scopeLabel}${name}: collides with a stored path (inspected actual Mongoose schema/child scope; getModelAtt alone is insufficient)`
|
|
1350
1845
|
);
|
|
1351
1846
|
}
|
|
1352
|
-
if (
|
|
1353
|
-
if (!
|
|
1847
|
+
if (isFunction2(raw)) continue;
|
|
1848
|
+
if (!isPlainObject12(raw)) {
|
|
1354
1849
|
throw new Error(
|
|
1355
1850
|
`Virtual ${scopeLabel}${name}: malformed descriptor (expected getter, { get, dependsOn }, or per-access record)`
|
|
1356
1851
|
);
|
|
1357
1852
|
}
|
|
1358
|
-
const
|
|
1359
|
-
const hasGet = "get" in
|
|
1360
|
-
const accessKeys = Object.keys(
|
|
1361
|
-
const unsupported2 = Object.keys(
|
|
1362
|
-
(k) => k !== "get" && k !== "dependsOn" && k !== "sub" && !VIRTUAL_SUPPORTED_ACCESSES.includes(k)
|
|
1853
|
+
const record2 = raw;
|
|
1854
|
+
const hasGet = "get" in record2;
|
|
1855
|
+
const accessKeys = Object.keys(record2).filter((k) => VIRTUAL_SUPPORTED_ACCESSES.includes(k));
|
|
1856
|
+
const unsupported2 = Object.keys(record2).filter(
|
|
1857
|
+
(k) => k !== "get" && k !== "dependsOn" && k !== "dependsOnPopulate" && k !== "sub" && !VIRTUAL_SUPPORTED_ACCESSES.includes(k)
|
|
1363
1858
|
);
|
|
1364
|
-
for (const key of Object.keys(
|
|
1859
|
+
for (const key of Object.keys(record2)) {
|
|
1365
1860
|
if (VIRTUAL_UNSUPPORTED_ACCESSES.includes(key)) {
|
|
1366
1861
|
throw new Error(
|
|
1367
1862
|
`Virtual ${scopeLabel}${name}: unsupported access "${key}" (supported: default + list/create/read/update; NOT delete/distinct/count)`
|
|
@@ -1379,21 +1874,21 @@ var validateVirtualsScope = (scope, documentPermissionField) => {
|
|
|
1379
1874
|
`Virtual ${scopeLabel}${name}: cannot mix "get" with per-access keys (use either bare { get, dependsOn } or a per-access record)`
|
|
1380
1875
|
);
|
|
1381
1876
|
}
|
|
1382
|
-
if ("sub" in
|
|
1877
|
+
if ("sub" in record2) {
|
|
1383
1878
|
throw new Error(`Virtual ${scopeLabel}${name}: cannot mix "get" with "sub" (leaf vs embedded container)`);
|
|
1384
1879
|
}
|
|
1385
1880
|
validateVirtualLeafValue(raw, `${scopeLabel}${name}`, storedFields, leafNames, void 0);
|
|
1386
1881
|
continue;
|
|
1387
1882
|
}
|
|
1388
1883
|
if (accessKeys.length > 0) {
|
|
1389
|
-
if ("sub" in
|
|
1884
|
+
if ("sub" in record2) {
|
|
1390
1885
|
throw new Error(`Virtual ${scopeLabel}${name}: cannot mix per-access keys with "sub"`);
|
|
1391
1886
|
}
|
|
1392
|
-
validateVirtualDependsOn(
|
|
1887
|
+
validateVirtualDependsOn(record2.dependsOn, `${scopeLabel}${name}`, storedFields, leafNames);
|
|
1393
1888
|
for (const access of accessKeys) {
|
|
1394
|
-
const leaf =
|
|
1889
|
+
const leaf = record2[access];
|
|
1395
1890
|
if (leaf === void 0) continue;
|
|
1396
|
-
validateVirtualLeafValue(leaf, `${scopeLabel}${name}.${access}`, storedFields, leafNames,
|
|
1891
|
+
validateVirtualLeafValue(leaf, `${scopeLabel}${name}.${access}`, storedFields, leafNames, record2.dependsOn);
|
|
1397
1892
|
}
|
|
1398
1893
|
continue;
|
|
1399
1894
|
}
|
|
@@ -1404,7 +1899,7 @@ var validateVirtualsScope = (scope, documentPermissionField) => {
|
|
|
1404
1899
|
};
|
|
1405
1900
|
var validateVirtualsTree = (model, virtuals, documentPermissionField, modelName) => {
|
|
1406
1901
|
if (virtuals === void 0 || virtuals === null) return;
|
|
1407
|
-
if (!
|
|
1902
|
+
if (!isPlainObject12(virtuals)) {
|
|
1408
1903
|
throw new Error(`Virtuals for model "${modelName}": virtuals must be a plain object mapping names to descriptors`);
|
|
1409
1904
|
}
|
|
1410
1905
|
const rootSchema = model?.schema ?? null;
|
|
@@ -1414,13 +1909,14 @@ var validateVirtualsTree = (model, virtuals, documentPermissionField, modelName)
|
|
|
1414
1909
|
{ scopeVirtuals: rootVirtuals, storedFields: rootStored, scopePrefix: "", scopeLabel: "" },
|
|
1415
1910
|
documentPermissionField
|
|
1416
1911
|
);
|
|
1417
|
-
const visitContainers = (currentVirtuals, currentSchema, currentPrefix) => {
|
|
1912
|
+
const visitContainers = (currentVirtuals, currentSchema, currentPrefix, schemaPrefix = "") => {
|
|
1418
1913
|
for (const [containerName, raw] of Object.entries(currentVirtuals)) {
|
|
1419
|
-
if (raw === void 0 || !
|
|
1420
|
-
const
|
|
1421
|
-
const
|
|
1422
|
-
const
|
|
1423
|
-
const
|
|
1914
|
+
if (raw === void 0 || !isVirtualContainerValue2(raw)) continue;
|
|
1915
|
+
const record2 = raw;
|
|
1916
|
+
const schemaPath = virtualFullPath(schemaPrefix, containerName);
|
|
1917
|
+
const childSchema = getChildSchemaForField(currentSchema, schemaPath);
|
|
1918
|
+
const nestedFields = getNestedChildFields(currentSchema, schemaPath);
|
|
1919
|
+
const currentStored = schemaPrefix ? getNestedChildFields(currentSchema, schemaPrefix) : collectStoredTopLevel(currentSchema);
|
|
1424
1920
|
if (!currentStored.has(containerName)) {
|
|
1425
1921
|
throw new Error(
|
|
1426
1922
|
`Virtual ${containerName}: scope-container "${containerName}" is not a persisted field in its scope`
|
|
@@ -1434,7 +1930,7 @@ var validateVirtualsTree = (model, virtuals, documentPermissionField, modelName)
|
|
|
1434
1930
|
}
|
|
1435
1931
|
const subStored = childSchema ? collectStoredTopLevel(childSchema) : nestedFields;
|
|
1436
1932
|
const subPrefix = currentPrefix ? `${currentPrefix}.${containerName}` : containerName;
|
|
1437
|
-
const subVirtuals =
|
|
1933
|
+
const subVirtuals = record2.sub ?? {};
|
|
1438
1934
|
validateVirtualsScope(
|
|
1439
1935
|
{
|
|
1440
1936
|
scopeVirtuals: subVirtuals,
|
|
@@ -1444,37 +1940,11 @@ var validateVirtualsTree = (model, virtuals, documentPermissionField, modelName)
|
|
|
1444
1940
|
},
|
|
1445
1941
|
documentPermissionField
|
|
1446
1942
|
);
|
|
1447
|
-
|
|
1448
|
-
visitContainers(subVirtuals, nextSchema, subPrefix);
|
|
1943
|
+
visitContainers(subVirtuals, childSchema ?? currentSchema, subPrefix, childSchema ? "" : schemaPath);
|
|
1449
1944
|
}
|
|
1450
1945
|
};
|
|
1451
1946
|
visitContainers(rootVirtuals, rootSchema, "");
|
|
1452
1947
|
};
|
|
1453
|
-
var getScopeVirtualsObject = (rootVirtuals, scopePath) => {
|
|
1454
|
-
if (!rootVirtuals || !isPlainObject10(rootVirtuals)) return null;
|
|
1455
|
-
if (!scopePath || scopePath.length === 0) return rootVirtuals;
|
|
1456
|
-
let current = rootVirtuals;
|
|
1457
|
-
for (let i = 0; i < scopePath.length; i += 2) {
|
|
1458
|
-
const field = scopePath[i];
|
|
1459
|
-
const subMarker = scopePath[i + 1];
|
|
1460
|
-
if (!field || subMarker !== "sub") return null;
|
|
1461
|
-
if (!isPlainObject10(current)) return null;
|
|
1462
|
-
const container = current[field];
|
|
1463
|
-
if (!isVirtualContainerValue(container)) return null;
|
|
1464
|
-
current = container.sub;
|
|
1465
|
-
if (!isPlainObject10(current)) return null;
|
|
1466
|
-
}
|
|
1467
|
-
return current;
|
|
1468
|
-
};
|
|
1469
|
-
var toVirtualDescriptor = (leaf, sharedDependsOn) => {
|
|
1470
|
-
if (isFunction(leaf))
|
|
1471
|
-
return { get: leaf, dependsOn: sharedDependsOn ?? [] };
|
|
1472
|
-
if (!isPlainObject10(leaf)) return null;
|
|
1473
|
-
const record = leaf;
|
|
1474
|
-
if (!isFunction(record.get)) return null;
|
|
1475
|
-
const dependsOn = record.dependsOn ?? sharedDependsOn ?? [];
|
|
1476
|
-
return { get: record.get, dependsOn };
|
|
1477
|
-
};
|
|
1478
1948
|
var AccessRuntime = class {
|
|
1479
1949
|
constructor(options = {}) {
|
|
1480
1950
|
this.globalOptions = new OptionsManager(
|
|
@@ -1736,13 +2206,14 @@ var AccessRuntime = class {
|
|
|
1736
2206
|
setModelOptions(modelName, options) {
|
|
1737
2207
|
assertPublicDefaults(options.defaults);
|
|
1738
2208
|
if ("hydration" in options) options = { ...options, hydration: normalizeHydration(options.hydration) };
|
|
1739
|
-
const manager = this.getOrCreateModelOptions(modelName);
|
|
1740
2209
|
const defaultOptions = this.getDefaultModelOptions();
|
|
1741
|
-
const currentModelOptions =
|
|
2210
|
+
const currentModelOptions = this.modelOptions[modelName]?.fetch() ?? { ...defaultModelOptions, modelName };
|
|
1742
2211
|
const merged = { ...defaultOptions, ...currentModelOptions, ...options };
|
|
1743
2212
|
const model = this.getModelInstance(modelName);
|
|
1744
2213
|
const permissionField = merged.documentPermissionField ?? currentModelOptions.documentPermissionField ?? this.getDefaultModelOption("documentPermissionField") ?? "_permissions";
|
|
1745
2214
|
validateVirtualsTree(model, merged.virtuals, permissionField, modelName);
|
|
2215
|
+
captureVirtualPopulateDependencies(this, modelName, merged);
|
|
2216
|
+
const manager = this.getOrCreateModelOptions(modelName);
|
|
1746
2217
|
manager.assign({ ...defaultOptions, ...currentModelOptions, ...options });
|
|
1747
2218
|
}
|
|
1748
2219
|
setModelOption(modelName, key, value) {
|
|
@@ -1758,11 +2229,11 @@ var AccessRuntime = class {
|
|
|
1758
2229
|
set3(proposed, keyStr.slice("defaults.".length), value);
|
|
1759
2230
|
assertPublicDefaults(proposed);
|
|
1760
2231
|
}
|
|
1761
|
-
const manager = this.
|
|
2232
|
+
const manager = this.modelOptions[modelName];
|
|
1762
2233
|
if (keyStr === "virtuals" || keyStr.startsWith("virtuals.") || keyStr === "documentPermissionField") {
|
|
1763
2234
|
const model = this.getModelInstance(modelName);
|
|
1764
|
-
const currentVirtuals = manager
|
|
1765
|
-
const currentPermissionField = manager
|
|
2235
|
+
const currentVirtuals = manager?.get("virtuals");
|
|
2236
|
+
const currentPermissionField = manager?.get("documentPermissionField") ?? this.getDefaultModelOption("documentPermissionField") ?? "_permissions";
|
|
1766
2237
|
let proposedVirtuals = currentVirtuals;
|
|
1767
2238
|
let proposedPermissionField = currentPermissionField;
|
|
1768
2239
|
if (keyStr === "documentPermissionField") {
|
|
@@ -1776,10 +2247,10 @@ var AccessRuntime = class {
|
|
|
1776
2247
|
const segments = nestedPath.split(".");
|
|
1777
2248
|
let cursor = cloned;
|
|
1778
2249
|
for (let i = 0; i < segments.length - 1; i++) {
|
|
1779
|
-
if (!
|
|
2250
|
+
if (!isPlainObject12(cursor)) break;
|
|
1780
2251
|
cursor = cursor[segments[i]];
|
|
1781
2252
|
}
|
|
1782
|
-
if (
|
|
2253
|
+
if (isPlainObject12(cursor)) {
|
|
1783
2254
|
delete cursor[segments[segments.length - 1]];
|
|
1784
2255
|
}
|
|
1785
2256
|
proposedVirtuals = cloned;
|
|
@@ -1789,8 +2260,14 @@ var AccessRuntime = class {
|
|
|
1789
2260
|
}
|
|
1790
2261
|
}
|
|
1791
2262
|
validateVirtualsTree(model, proposedVirtuals, proposedPermissionField, modelName);
|
|
2263
|
+
captureVirtualPopulateDependencies(this, modelName, {
|
|
2264
|
+
...this.getDefaultModelOptions(),
|
|
2265
|
+
...manager?.fetch(),
|
|
2266
|
+
virtuals: proposedVirtuals,
|
|
2267
|
+
documentPermissionField: proposedPermissionField
|
|
2268
|
+
});
|
|
1792
2269
|
}
|
|
1793
|
-
|
|
2270
|
+
this.getOrCreateModelOptions(modelName).set(key, value);
|
|
1794
2271
|
}
|
|
1795
2272
|
/** Frozen stored snapshot, not fully resolved per-path defaults. Read again for current configuration. */
|
|
1796
2273
|
getModelOptions(modelName) {
|
|
@@ -1829,7 +2306,7 @@ var AccessRuntime = class {
|
|
|
1829
2306
|
const rootVirtuals = manager.get("virtuals");
|
|
1830
2307
|
const scopeVirtuals = getScopeVirtualsObject(rootVirtuals, scopePath);
|
|
1831
2308
|
if (!scopeVirtuals) return [];
|
|
1832
|
-
return Object.entries(scopeVirtuals).filter(([, v]) => v !== void 0 && !
|
|
2309
|
+
return Object.entries(scopeVirtuals).filter(([, v]) => v !== void 0 && !isVirtualContainerValue2(v)).map(([k]) => k);
|
|
1833
2310
|
}
|
|
1834
2311
|
/**
|
|
1835
2312
|
* Whether a field is a registered virtual leaf in a scope (VIRT-01).
|
|
@@ -1843,7 +2320,7 @@ var AccessRuntime = class {
|
|
|
1843
2320
|
const scopeVirtuals = getScopeVirtualsObject(rootVirtuals, scopePath);
|
|
1844
2321
|
if (!scopeVirtuals || !(fieldName in scopeVirtuals)) return false;
|
|
1845
2322
|
const entry = scopeVirtuals[fieldName];
|
|
1846
|
-
if (entry === void 0 ||
|
|
2323
|
+
if (entry === void 0 || isVirtualContainerValue2(entry)) return false;
|
|
1847
2324
|
return true;
|
|
1848
2325
|
}
|
|
1849
2326
|
/**
|
|
@@ -1853,34 +2330,14 @@ var AccessRuntime = class {
|
|
|
1853
2330
|
* interpreted as a getter descriptor when the access is absent: an
|
|
1854
2331
|
* inapplicable registered name stays virtual (excluded from persisted
|
|
1855
2332
|
* projections, write admission, and DB sort/filter/distinct).
|
|
1856
|
-
* Shared
|
|
2333
|
+
* Shared `dependsOn` and `dependsOnPopulate` lists inherit independently;
|
|
2334
|
+
* each defined leaf list replaces its shared list wholesale.
|
|
1857
2335
|
*/
|
|
1858
2336
|
resolveVirtualDescriptor(modelName, virtualName, access, scopePath) {
|
|
1859
2337
|
const manager = this.getOrCreateModelOptions(modelName);
|
|
1860
2338
|
const rootVirtuals = manager.get("virtuals");
|
|
1861
2339
|
const scopeVirtuals = getScopeVirtualsObject(rootVirtuals, scopePath);
|
|
1862
|
-
|
|
1863
|
-
const entry = scopeVirtuals[virtualName];
|
|
1864
|
-
if (entry === void 0 || isVirtualContainerValue(entry)) return void 0;
|
|
1865
|
-
if (isFunction(entry)) return { get: entry, dependsOn: [] };
|
|
1866
|
-
if (!isPlainObject10(entry)) return void 0;
|
|
1867
|
-
const record = entry;
|
|
1868
|
-
if ("get" in record) {
|
|
1869
|
-
const descriptor = toVirtualDescriptor(entry, void 0);
|
|
1870
|
-
return descriptor ?? void 0;
|
|
1871
|
-
}
|
|
1872
|
-
const sharedDependsOn = record.dependsOn;
|
|
1873
|
-
const exact = record[access];
|
|
1874
|
-
if (exact !== void 0) {
|
|
1875
|
-
const descriptor = toVirtualDescriptor(exact, sharedDependsOn);
|
|
1876
|
-
if (descriptor) return descriptor;
|
|
1877
|
-
}
|
|
1878
|
-
const fallback = record.default;
|
|
1879
|
-
if (fallback !== void 0) {
|
|
1880
|
-
const descriptor = toVirtualDescriptor(fallback, sharedDependsOn);
|
|
1881
|
-
if (descriptor) return descriptor;
|
|
1882
|
-
}
|
|
1883
|
-
return void 0;
|
|
2340
|
+
return resolveSnapshotDescriptor(scopeVirtuals, virtualName, access);
|
|
1884
2341
|
}
|
|
1885
2342
|
getModelNames() {
|
|
1886
2343
|
return Object.keys(this.modelOptions);
|
|
@@ -1963,16 +2420,16 @@ var AccessRuntime = class {
|
|
|
1963
2420
|
}
|
|
1964
2421
|
getModelRef(modelName, refPath) {
|
|
1965
2422
|
this.ensureModelMeta(modelName);
|
|
1966
|
-
const value =
|
|
2423
|
+
const value = get3(this.modelRefs, `${modelName}.${refPath}`, null);
|
|
1967
2424
|
return isString3(value) ? value : null;
|
|
1968
2425
|
}
|
|
1969
2426
|
getModelSub(modelName) {
|
|
1970
2427
|
this.ensureModelMeta(modelName);
|
|
1971
|
-
return
|
|
2428
|
+
return get3(this.modelSubs, modelName, []);
|
|
1972
2429
|
}
|
|
1973
2430
|
getModelAtt(modelName) {
|
|
1974
2431
|
this.ensureModelMeta(modelName);
|
|
1975
|
-
return
|
|
2432
|
+
return get3(this.modelAtts, modelName, []);
|
|
1976
2433
|
}
|
|
1977
2434
|
};
|
|
1978
2435
|
var defaultRuntime = new AccessRuntime({ allowGlobalModelLookup: true });
|
|
@@ -1992,7 +2449,7 @@ var getGlobalOption = (key, defaultValue) => {
|
|
|
1992
2449
|
|
|
1993
2450
|
// src/validation/common.ts
|
|
1994
2451
|
import { z as z2 } from "zod";
|
|
1995
|
-
import { isPlainObject as
|
|
2452
|
+
import { isPlainObject as isPlainObject14 } from "@web-ts-toolkit/utils";
|
|
1996
2453
|
|
|
1997
2454
|
// src/validation/public-admission-schema.ts
|
|
1998
2455
|
var admissions = /* @__PURE__ */ new WeakMap();
|
|
@@ -2014,7 +2471,7 @@ function assertTransformedPublicAdmission(schema, value) {
|
|
|
2014
2471
|
}
|
|
2015
2472
|
|
|
2016
2473
|
// src/correlated-includes.ts
|
|
2017
|
-
import { cloneDeep, isArray as isArray5, isPlainObject as
|
|
2474
|
+
import { cloneDeep, isArray as isArray5, isPlainObject as isPlainObject13 } from "@web-ts-toolkit/utils";
|
|
2018
2475
|
var PARENT_MARKER_KEY = "$parent";
|
|
2019
2476
|
var ESCAPE_WRAPPER_KEY = "$escape";
|
|
2020
2477
|
var SUBQUERY_PAYLOAD_KEYS = /* @__PURE__ */ new Set(["$$sq", "$$date"]);
|
|
@@ -2025,10 +2482,10 @@ var FIELD_SEGMENT_PATTERN = /^[A-Za-z_][A-Za-z0-9_$]*$/;
|
|
|
2025
2482
|
var ARRAY_INDEX_SEGMENT_PATTERN = /^(0|[1-9]\d*)$/;
|
|
2026
2483
|
var ownKeys = (value) => Object.keys(value);
|
|
2027
2484
|
function isParentRefMarker(value) {
|
|
2028
|
-
return
|
|
2485
|
+
return isPlainObject13(value) && ownKeys(value).length === 1 && ownKeys(value)[0] === PARENT_MARKER_KEY;
|
|
2029
2486
|
}
|
|
2030
2487
|
function isEscapedParentRef(value) {
|
|
2031
|
-
return
|
|
2488
|
+
return isPlainObject13(value) && ownKeys(value).length === 1 && ownKeys(value)[0] === ESCAPE_WRAPPER_KEY;
|
|
2032
2489
|
}
|
|
2033
2490
|
function hasMarkerKey(value) {
|
|
2034
2491
|
return ownKeys(value).includes(PARENT_MARKER_KEY) || ownKeys(value).includes(ESCAPE_WRAPPER_KEY);
|
|
@@ -2052,7 +2509,7 @@ function containsParentMarkerShape(value) {
|
|
|
2052
2509
|
seen.add(current);
|
|
2053
2510
|
return current.some(visit);
|
|
2054
2511
|
}
|
|
2055
|
-
if (
|
|
2512
|
+
if (isPlainObject13(current)) {
|
|
2056
2513
|
if (seen.has(current)) return false;
|
|
2057
2514
|
seen.add(current);
|
|
2058
2515
|
if (hasMarkerKey(current)) return true;
|
|
@@ -2082,7 +2539,7 @@ function scanTemplateValueNode(value, path, issues) {
|
|
|
2082
2539
|
}
|
|
2083
2540
|
return;
|
|
2084
2541
|
}
|
|
2085
|
-
if (
|
|
2542
|
+
if (isPlainObject13(value)) {
|
|
2086
2543
|
if (hasMarkerKey(value)) {
|
|
2087
2544
|
issues.push({
|
|
2088
2545
|
message: 'Malformed $parent reference: objects carrying $parent must be exactly { "$parent": "<field>" }',
|
|
@@ -2147,7 +2604,7 @@ function scanTemplateObjectEntries(obj, basePath, issues) {
|
|
|
2147
2604
|
});
|
|
2148
2605
|
return;
|
|
2149
2606
|
}
|
|
2150
|
-
if (
|
|
2607
|
+
if (isPlainObject13(clause)) {
|
|
2151
2608
|
scanTemplateObjectEntries(clause, clausePath, issues);
|
|
2152
2609
|
}
|
|
2153
2610
|
});
|
|
@@ -2158,7 +2615,7 @@ function scanTemplateObjectEntries(obj, basePath, issues) {
|
|
|
2158
2615
|
}
|
|
2159
2616
|
function validateCorrelatedFilterTemplate(filter) {
|
|
2160
2617
|
const issues = [];
|
|
2161
|
-
if (!
|
|
2618
|
+
if (!isPlainObject13(filter)) {
|
|
2162
2619
|
issues.push({ message: "Correlated include filter must be an object", path: [] });
|
|
2163
2620
|
return issues;
|
|
2164
2621
|
}
|
|
@@ -2286,7 +2743,7 @@ var correlatedFilterSchema = z2.record(z2.string(), z2.unknown()).superRefine((f
|
|
|
2286
2743
|
}
|
|
2287
2744
|
});
|
|
2288
2745
|
var correlatedEmptyOptionsSchema = z2.unknown().refine(
|
|
2289
|
-
(value) => value === void 0 ||
|
|
2746
|
+
(value) => value === void 0 || isPlainObject14(value) && Object.keys(value).length === 0,
|
|
2290
2747
|
"Correlated include options must be absent or empty"
|
|
2291
2748
|
).optional();
|
|
2292
2749
|
var correlatedPathSchema = z2.string().min(1).superRefine((path, ctx) => {
|
|
@@ -2323,7 +2780,7 @@ var correlatedListArgsSchema = z2.object({
|
|
|
2323
2780
|
include: z2.lazy(() => correlatedNestedIncludeSchema).optional()
|
|
2324
2781
|
}).strict().superRefine((args, ctx) => checkArgsMarkers(args.select, args.sort, ctx));
|
|
2325
2782
|
var correlatedCountArgsSchema = z2.unknown().refine(
|
|
2326
|
-
(value) => value === void 0 ||
|
|
2783
|
+
(value) => value === void 0 || isPlainObject14(value) && Object.keys(value).length === 0,
|
|
2327
2784
|
"Count includes accept no args"
|
|
2328
2785
|
).optional();
|
|
2329
2786
|
var correlatedReadByIdSchema = z2.object({
|
|
@@ -2376,7 +2833,7 @@ var correlatedNestedIncludeSchema = z2.union([includeItemSchema, z2.array(includ
|
|
|
2376
2833
|
var rejectDuplicateCorrelatedPaths = (items, ctx) => {
|
|
2377
2834
|
const seen = /* @__PURE__ */ new Set();
|
|
2378
2835
|
items.forEach((item, index) => {
|
|
2379
|
-
if (
|
|
2836
|
+
if (isPlainObject14(item) && item.mode === "correlated" && typeof item.path === "string") {
|
|
2380
2837
|
if (seen.has(item.path)) {
|
|
2381
2838
|
ctx.addIssue({
|
|
2382
2839
|
code: z2.ZodIssueCode.custom,
|
|
@@ -2414,7 +2871,7 @@ function rejectKeys(body, ctx, keys2) {
|
|
|
2414
2871
|
}
|
|
2415
2872
|
|
|
2416
2873
|
// src/validation/parsers.ts
|
|
2417
|
-
var clientErrors =
|
|
2874
|
+
var clientErrors = JsonRouter3.clientErrors;
|
|
2418
2875
|
function parsePathParam(value, parameter) {
|
|
2419
2876
|
const result = stringOrStringArray.safeParse(value);
|
|
2420
2877
|
if (!result.success) {
|
|
@@ -2846,8 +3303,8 @@ function isArkTypeErrors(value) {
|
|
|
2846
3303
|
if (typeof value !== "object" || value === null) {
|
|
2847
3304
|
return false;
|
|
2848
3305
|
}
|
|
2849
|
-
const
|
|
2850
|
-
return
|
|
3306
|
+
const record2 = value;
|
|
3307
|
+
return record2.arkKind === "errors" || record2[" arkKind"] === "errors";
|
|
2851
3308
|
}
|
|
2852
3309
|
function isThenable(value) {
|
|
2853
3310
|
return (typeof value === "object" || typeof value === "function") && value !== null && "then" in value && typeof value.then === "function";
|