@web-ts-toolkit/access-router 0.45.0 → 0.47.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 +255 -9
- package/advanced.d.mts +1 -1
- package/advanced.d.ts +1 -1
- package/advanced.js +40 -17
- package/advanced.mjs +42 -18
- package/index.d.mts +5 -186
- package/index.d.ts +5 -186
- package/index.js +622 -291
- package/index.mjs +485 -152
- package/llms.txt +68 -3
- package/package.json +3 -3
- package/{parsers-DppKW-Fd.d.mts → parsers-BidvJUu7.d.mts} +353 -9
- package/{parsers-DppKW-Fd.d.ts → parsers-BidvJUu7.d.ts} +353 -9
package/index.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
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 isArray12, isFunction as isFunction6, isPlainObject 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 {
|
|
@@ -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
|
|
@@ -298,6 +320,41 @@ function normalizeSortForOrderBy(sort) {
|
|
|
298
320
|
orders: normalized.fields.map(({ direction }) => direction)
|
|
299
321
|
};
|
|
300
322
|
}
|
|
323
|
+
function sanitizeSortFields(sort, allowedFields) {
|
|
324
|
+
const { fields, errors } = normalizeSort(sort);
|
|
325
|
+
if (errors.length > 0) return { sort: void 0, stripped: [], errors };
|
|
326
|
+
const allowedSet = new Set(allowedFields.concat(["id", "_id"]));
|
|
327
|
+
const kept = fields.filter(({ field }) => allowedSet.has(field));
|
|
328
|
+
const stripped = fields.filter(({ field }) => !allowedSet.has(field)).map(({ field }) => field);
|
|
329
|
+
if (stripped.length === 0) return { sort, stripped, errors: [] };
|
|
330
|
+
if (kept.length === 0) return { sort: void 0, stripped, errors: [] };
|
|
331
|
+
if (typeof sort === "string") {
|
|
332
|
+
return {
|
|
333
|
+
sort: kept.map(({ field, direction }) => direction === "desc" ? `-${field}` : field).join(" "),
|
|
334
|
+
stripped,
|
|
335
|
+
errors: []
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
if (sort instanceof Map) {
|
|
339
|
+
return {
|
|
340
|
+
sort: new Map(kept.map(({ field, direction }) => [field, direction])),
|
|
341
|
+
stripped,
|
|
342
|
+
errors: []
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
if (Array.isArray(sort)) {
|
|
346
|
+
return {
|
|
347
|
+
sort: kept.map(({ field, direction }) => [field, direction]),
|
|
348
|
+
stripped,
|
|
349
|
+
errors: []
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
return {
|
|
353
|
+
sort: Object.fromEntries(kept.map(({ field, direction }) => [field, direction])),
|
|
354
|
+
stripped,
|
|
355
|
+
errors: []
|
|
356
|
+
};
|
|
357
|
+
}
|
|
301
358
|
|
|
302
359
|
// src/helpers/index.ts
|
|
303
360
|
import { forEach, isArray as isArray2, isEmpty, isObject, isPlainObject as isPlainObject4, isString as isString3 } from "@web-ts-toolkit/utils";
|
|
@@ -1165,6 +1222,7 @@ var AccessRuntime = class {
|
|
|
1165
1222
|
this.modelAtts = {};
|
|
1166
1223
|
this.modelInstances = {};
|
|
1167
1224
|
this.openApiRegistry = new OpenApiRegistry();
|
|
1225
|
+
this.openApiPathPrefix = "";
|
|
1168
1226
|
this.allowGlobalModelLookup = options.allowGlobalModelLookup ?? false;
|
|
1169
1227
|
ensureMongooseJsonSchemaInitialized();
|
|
1170
1228
|
}
|
|
@@ -1193,7 +1251,24 @@ var AccessRuntime = class {
|
|
|
1193
1251
|
return global;
|
|
1194
1252
|
}
|
|
1195
1253
|
registerOpenApiRoute(route) {
|
|
1196
|
-
this.openApiRegistry.register(
|
|
1254
|
+
this.openApiRegistry.register(
|
|
1255
|
+
this.openApiPathPrefix ? { ...route, path: normalizeUrlPath(`${this.openApiPathPrefix}/${route.path}`) } : route
|
|
1256
|
+
);
|
|
1257
|
+
}
|
|
1258
|
+
/**
|
|
1259
|
+
* Compose OpenAPI paths while synchronously constructing mounted routers.
|
|
1260
|
+
* Prefixes apply before collision checks; prior descriptors and Express paths
|
|
1261
|
+
* are unchanged. Nested calls compose and the prior scope is restored even on
|
|
1262
|
+
* failure. The callback must finish registration synchronously (no async work).
|
|
1263
|
+
*/
|
|
1264
|
+
withOpenApiPathPrefix(prefix, build) {
|
|
1265
|
+
const previous = this.openApiPathPrefix;
|
|
1266
|
+
this.openApiPathPrefix = normalizeUrlPath(`${previous}/${prefix}`);
|
|
1267
|
+
try {
|
|
1268
|
+
return build();
|
|
1269
|
+
} finally {
|
|
1270
|
+
this.openApiPathPrefix = previous;
|
|
1271
|
+
}
|
|
1197
1272
|
}
|
|
1198
1273
|
/**
|
|
1199
1274
|
* Enable strict OpenAPI collision detection for the remainder of this
|
|
@@ -1567,6 +1642,7 @@ import {
|
|
|
1567
1642
|
get as get4,
|
|
1568
1643
|
isArray as isArray7,
|
|
1569
1644
|
isPlainObject as isPlainObject12,
|
|
1645
|
+
set as set3,
|
|
1570
1646
|
uniq
|
|
1571
1647
|
} from "@web-ts-toolkit/utils";
|
|
1572
1648
|
|
|
@@ -1878,7 +1954,8 @@ function resolveValueNode(template, parent, bare) {
|
|
|
1878
1954
|
if (!isParentRefMarker(inner) || typeof inner.$parent !== "string" || (inner.$parent?.length ?? 0) === 0) {
|
|
1879
1955
|
throw new CorrelatedReferenceError('Invalid $escape payload: expected { "$escape": { "$parent": "<field>" } }');
|
|
1880
1956
|
}
|
|
1881
|
-
|
|
1957
|
+
const literal = literalParentRefObject(inner.$parent);
|
|
1958
|
+
return bare ? { $eq: literal } : literal;
|
|
1882
1959
|
}
|
|
1883
1960
|
if (isParentRefMarker(template)) {
|
|
1884
1961
|
const path = template.$parent;
|
|
@@ -2007,8 +2084,28 @@ function isCorrelatedInclude(value) {
|
|
|
2007
2084
|
return isPlainObject11(value) && value.mode === "correlated";
|
|
2008
2085
|
}
|
|
2009
2086
|
|
|
2087
|
+
// src/helpers/request-work.ts
|
|
2088
|
+
var states = /* @__PURE__ */ new WeakMap();
|
|
2089
|
+
function getRequestWorkState(req, runtime) {
|
|
2090
|
+
let owners = states.get(req);
|
|
2091
|
+
if (!owners) {
|
|
2092
|
+
owners = /* @__PURE__ */ new WeakMap();
|
|
2093
|
+
states.set(req, owners);
|
|
2094
|
+
}
|
|
2095
|
+
let state = owners.get(runtime);
|
|
2096
|
+
if (!state) {
|
|
2097
|
+
state = {
|
|
2098
|
+
totalQueries: 0,
|
|
2099
|
+
scheduler: new RequestConcurrencyScheduler(
|
|
2100
|
+
resolveRequestComplexity(runtime.getGlobalOption("requestComplexity")).maxBulkConcurrency
|
|
2101
|
+
)
|
|
2102
|
+
};
|
|
2103
|
+
owners.set(runtime, state);
|
|
2104
|
+
}
|
|
2105
|
+
return state;
|
|
2106
|
+
}
|
|
2107
|
+
|
|
2010
2108
|
// src/services/base.ts
|
|
2011
|
-
var correlatedExecStates = /* @__PURE__ */ new WeakMap();
|
|
2012
2109
|
function getCorrelatedTemplateDepth(includes) {
|
|
2013
2110
|
let max = 0;
|
|
2014
2111
|
const visit = (entries, depth) => {
|
|
@@ -2058,6 +2155,7 @@ function validateClientFilter(filter2) {
|
|
|
2058
2155
|
}
|
|
2059
2156
|
var Base = class {
|
|
2060
2157
|
constructor(req, modelName) {
|
|
2158
|
+
this.requestRuntime = getActiveRuntime() ?? defaultRuntime;
|
|
2061
2159
|
this.req = req;
|
|
2062
2160
|
this.modelName = modelName;
|
|
2063
2161
|
}
|
|
@@ -2195,6 +2293,7 @@ var Base = class {
|
|
|
2195
2293
|
}
|
|
2196
2294
|
seenCorrelatedPaths.add(entry.path);
|
|
2197
2295
|
}
|
|
2296
|
+
this.assertIncludeOutputPaths([...legacy, ...correlated]);
|
|
2198
2297
|
let includeLocalFields = [];
|
|
2199
2298
|
let includePaths = [];
|
|
2200
2299
|
forEach3(legacy, (inc) => {
|
|
@@ -2211,6 +2310,39 @@ var Base = class {
|
|
|
2211
2310
|
correlatedReferenceFields: uniq(compact(correlatedReferenceFields))
|
|
2212
2311
|
};
|
|
2213
2312
|
}
|
|
2313
|
+
validateIncludeOutputPath(path, modelName) {
|
|
2314
|
+
const permissionField = getModelOption(modelName, "documentPermissionField");
|
|
2315
|
+
if (get4(set3({}, path, true), permissionField) !== void 0 || get4(set3({}, permissionField, true), path) !== void 0) {
|
|
2316
|
+
this.throwClientRequestError(
|
|
2317
|
+
"bad_request" /* BadRequest */,
|
|
2318
|
+
`Include output path ${path} overlaps document permission field ${permissionField} on model ${modelName}`
|
|
2319
|
+
);
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2322
|
+
assertIncludeOutputPaths(include) {
|
|
2323
|
+
const pending = [{ entries: include, modelName: this.modelName }];
|
|
2324
|
+
const visited = /* @__PURE__ */ new WeakMap();
|
|
2325
|
+
while (pending.length > 0) {
|
|
2326
|
+
const { entries, modelName } = pending.pop();
|
|
2327
|
+
if (modelName !== this.modelName && !getModelNames().includes(modelName)) {
|
|
2328
|
+
this.throwClientRequestError("bad_request" /* BadRequest */, `Model ${modelName} not found`);
|
|
2329
|
+
}
|
|
2330
|
+
for (const entry of entries) {
|
|
2331
|
+
if (!isPlainObject12(entry)) continue;
|
|
2332
|
+
const models = visited.get(entry) ?? /* @__PURE__ */ new Set();
|
|
2333
|
+
if (models.has(modelName)) continue;
|
|
2334
|
+
models.add(modelName);
|
|
2335
|
+
visited.set(entry, models);
|
|
2336
|
+
if (typeof entry.path === "string" && entry.path) {
|
|
2337
|
+
this.validateIncludeOutputPath(entry.path, modelName);
|
|
2338
|
+
}
|
|
2339
|
+
if ((entry.op === "read" || entry.op === "list") && typeof entry.model === "string") {
|
|
2340
|
+
const nested = entry.args?.include;
|
|
2341
|
+
if (nested) pending.push({ entries: compact(castArray(nested)), modelName: entry.model });
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2214
2346
|
getForeignKeyValues(value) {
|
|
2215
2347
|
return flatten(castArray(value));
|
|
2216
2348
|
}
|
|
@@ -2345,6 +2477,7 @@ var Base = class {
|
|
|
2345
2477
|
const svc = await this.getAuthorizedTargetService(model, "count");
|
|
2346
2478
|
const localValues = docs.flatMap((doc) => this.getForeignKeyValues(get4(doc, localField)));
|
|
2347
2479
|
const result = await svc.countByFieldValues(foreignField, localValues, _filters ?? {}, "count");
|
|
2480
|
+
if (!result.success && result.code === "bad_request" /* BadRequest */) throw new ClientRequestError(result);
|
|
2348
2481
|
if (!result.success) return docs;
|
|
2349
2482
|
for (let y = 0; y < docs.length; y++) {
|
|
2350
2483
|
const doc = docs[y];
|
|
@@ -2353,16 +2486,7 @@ var Base = class {
|
|
|
2353
2486
|
return docs;
|
|
2354
2487
|
}
|
|
2355
2488
|
getCorrelatedExecState() {
|
|
2356
|
-
|
|
2357
|
-
let state = correlatedExecStates.get(key);
|
|
2358
|
-
if (!state) {
|
|
2359
|
-
state = {
|
|
2360
|
-
totalQueries: 0,
|
|
2361
|
-
scheduler: new RequestConcurrencyScheduler(this.getRequestComplexity().maxBulkConcurrency)
|
|
2362
|
-
};
|
|
2363
|
-
correlatedExecStates.set(key, state);
|
|
2364
|
-
}
|
|
2365
|
-
return state;
|
|
2489
|
+
return getRequestWorkState(this.req, this.requestRuntime);
|
|
2366
2490
|
}
|
|
2367
2491
|
claimCorrelatedQuerySlot() {
|
|
2368
2492
|
const limits = this.getRequestComplexity();
|
|
@@ -2761,7 +2885,7 @@ var Base = class {
|
|
|
2761
2885
|
// src/services/service.ts
|
|
2762
2886
|
import {
|
|
2763
2887
|
castArray as castArray2,
|
|
2764
|
-
cloneDeep as
|
|
2888
|
+
cloneDeep as cloneDeep4,
|
|
2765
2889
|
compact as compact2,
|
|
2766
2890
|
isArray as isArray8,
|
|
2767
2891
|
isBoolean as isBoolean3,
|
|
@@ -2774,6 +2898,26 @@ import {
|
|
|
2774
2898
|
import { diff } from "just-diff";
|
|
2775
2899
|
|
|
2776
2900
|
// src/model.ts
|
|
2901
|
+
import { cloneDeep as cloneDeep3 } from "@web-ts-toolkit/utils";
|
|
2902
|
+
function admitModelPersistence(adapter, scheduler) {
|
|
2903
|
+
return {
|
|
2904
|
+
modelName: adapter.modelName,
|
|
2905
|
+
mongooseModel: adapter.mongooseModel,
|
|
2906
|
+
new: () => adapter.new(),
|
|
2907
|
+
// Mongoose create(array) otherwise starts all document saves at once. Keep
|
|
2908
|
+
// the adapter's array input/output contract, with one permit per item; all
|
|
2909
|
+
// items are submitted even if one rejects, as with unordered create(array).
|
|
2910
|
+
create: (data) => Array.isArray(data) ? Promise.all(data.map((item) => scheduler.work(async () => (await adapter.create([item]))[0]))) : scheduler.work(() => adapter.create(data)),
|
|
2911
|
+
find: (props) => scheduler.work(() => adapter.find(props)),
|
|
2912
|
+
findOne: (props) => scheduler.work(() => adapter.findOne(props)),
|
|
2913
|
+
exists: (filter2) => scheduler.work(() => adapter.exists(filter2)),
|
|
2914
|
+
countDocuments: (filter2) => scheduler.work(() => adapter.countDocuments(filter2)),
|
|
2915
|
+
distinct: (field, conditions) => scheduler.work(() => adapter.distinct(field, conditions)),
|
|
2916
|
+
aggregate: (pipeline) => scheduler.work(() => adapter.aggregate(pipeline)),
|
|
2917
|
+
// Casting builds no persistence work and must not acquire a recursive permit.
|
|
2918
|
+
...adapter.castFilter ? { castFilter: (filter2) => adapter.castFilter(filter2) } : {}
|
|
2919
|
+
};
|
|
2920
|
+
}
|
|
2777
2921
|
var Model = class {
|
|
2778
2922
|
constructor(modelName, runtime) {
|
|
2779
2923
|
this.modelName = modelName;
|
|
@@ -2826,6 +2970,11 @@ var Model = class {
|
|
|
2826
2970
|
countDocuments(filter2 = {}) {
|
|
2827
2971
|
return this.mongooseModel.countDocuments(filter2);
|
|
2828
2972
|
}
|
|
2973
|
+
/** Cast a copy using this model's query schema/setters, without executing query middleware or persistence. */
|
|
2974
|
+
castFilter(filter2) {
|
|
2975
|
+
const query = this.mongooseModel.countDocuments(cloneDeep3(filter2));
|
|
2976
|
+
return query.cast(this.mongooseModel);
|
|
2977
|
+
}
|
|
2829
2978
|
// see https://mongoosejs.com/docs/api.html#model_Model.distinct
|
|
2830
2979
|
distinct(field, conditions = {}) {
|
|
2831
2980
|
return this.mongooseModel.distinct(field, conditions);
|
|
@@ -2836,6 +2985,39 @@ var Model = class {
|
|
|
2836
2985
|
};
|
|
2837
2986
|
var model_default = Model;
|
|
2838
2987
|
|
|
2988
|
+
// src/helpers/apply-update.ts
|
|
2989
|
+
import { isPlainObject as isPlainObject13, set as set4 } from "@web-ts-toolkit/utils";
|
|
2990
|
+
function applyUpdate(target, data, allowedFields) {
|
|
2991
|
+
const wholeFields = /* @__PURE__ */ new Set();
|
|
2992
|
+
const partialContainers = /* @__PURE__ */ new Set();
|
|
2993
|
+
const collectBoundary = (shape, prefix = "") => {
|
|
2994
|
+
for (const [key, value] of Object.entries(shape)) {
|
|
2995
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
2996
|
+
if (value === true) wholeFields.add(path);
|
|
2997
|
+
else {
|
|
2998
|
+
partialContainers.add(path);
|
|
2999
|
+
collectBoundary(value, path);
|
|
3000
|
+
}
|
|
3001
|
+
}
|
|
3002
|
+
};
|
|
3003
|
+
for (const field of allowedFields) {
|
|
3004
|
+
collectBoundary(set4({}, field, true));
|
|
3005
|
+
}
|
|
3006
|
+
const apply = (values, prefix = "") => {
|
|
3007
|
+
for (const [key, value] of Object.entries(values)) {
|
|
3008
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
3009
|
+
if (partialContainers.has(path) && !wholeFields.has(path) && (isPlainObject13(value) || Array.isArray(value))) {
|
|
3010
|
+
apply(value, path);
|
|
3011
|
+
} else if (isDocument2(target)) {
|
|
3012
|
+
target.set(path, value);
|
|
3013
|
+
} else {
|
|
3014
|
+
set4(target, path, value);
|
|
3015
|
+
}
|
|
3016
|
+
}
|
|
3017
|
+
};
|
|
3018
|
+
apply(data ?? {});
|
|
3019
|
+
}
|
|
3020
|
+
|
|
2839
3021
|
// src/logger-helpers.ts
|
|
2840
3022
|
var SENSITIVE_FILTER_KEYS = /* @__PURE__ */ new Set([
|
|
2841
3023
|
"password",
|
|
@@ -3015,6 +3197,30 @@ var error = (error2) => {
|
|
|
3015
3197
|
|
|
3016
3198
|
// src/services/model-subdocument-service.ts
|
|
3017
3199
|
import { forEach as forEach4, get as get5, pick as pick2 } from "@web-ts-toolkit/utils";
|
|
3200
|
+
async function visibleMutationRows(service, isAllowed, parentId, sub, rows, response) {
|
|
3201
|
+
const accesses = ["read", `subs.${sub}.read`];
|
|
3202
|
+
if (response === "create") accesses.push(`subs.${sub}.list`);
|
|
3203
|
+
const allowed = await Promise.all(accesses.map(isAllowed));
|
|
3204
|
+
if (allowed.some((value) => !value)) return [];
|
|
3205
|
+
const parentFilter = await service.genFilter("read", { _id: parentId });
|
|
3206
|
+
if (parentFilter === false) return [];
|
|
3207
|
+
const parent = await service.findRawParentDoc({
|
|
3208
|
+
filter: { $and: [{ _id: parentId }, parentFilter] },
|
|
3209
|
+
select: "_id",
|
|
3210
|
+
populate: [],
|
|
3211
|
+
lean: true
|
|
3212
|
+
});
|
|
3213
|
+
if (!parent) return [];
|
|
3214
|
+
const targetFilter = response === "create" ? {} : response === "single" ? { _id: rows[0]?._id } : { _id: { $in: rows.map((row) => row._id) } };
|
|
3215
|
+
const [readFilter, listFilter, select] = await Promise.all([
|
|
3216
|
+
service.genFilter(`subs.${sub}.read`, targetFilter),
|
|
3217
|
+
response === "create" ? service.genFilter(`subs.${sub}.list`) : Promise.resolve({}),
|
|
3218
|
+
service.genQuerySelect("read", null, false, [sub, "sub"])
|
|
3219
|
+
]);
|
|
3220
|
+
if (readFilter === false || listFilter === false) return [];
|
|
3221
|
+
const visible = filterCollection(filterCollection(rows, listFilter), readFilter);
|
|
3222
|
+
return select ? visible.map((row) => pick2(toObject(row), select.concat("_id"))) : visible;
|
|
3223
|
+
}
|
|
3018
3224
|
async function listSub(service, id, sub, options) {
|
|
3019
3225
|
const { filter: ft, select } = options ?? {};
|
|
3020
3226
|
const filterErrors = validateClientFilter(ft);
|
|
@@ -3046,25 +3252,24 @@ async function readSub(service, id, sub, subId, options) {
|
|
|
3046
3252
|
if (subSelect) subdoc = pick2(toObject(subdoc), subSelect.concat(["_id"]));
|
|
3047
3253
|
return { success: true, kind: "single", code: "success" /* Success */, data: subdoc };
|
|
3048
3254
|
}
|
|
3049
|
-
async function updateSub(service, id, sub, subId, data) {
|
|
3255
|
+
async function updateSub(service, persist, isAllowed, id, sub, subId, data) {
|
|
3050
3256
|
const parentDoc = await getParentDoc(service, id, sub, null, { access: "update" });
|
|
3051
3257
|
if (!parentDoc) return { success: false, kind: "error", code: "not_found" /* NotFound */ };
|
|
3052
3258
|
const result = get5(parentDoc, sub);
|
|
3053
|
-
const [subFilter,
|
|
3259
|
+
const [subFilter, subUpdateSelect] = await Promise.all([
|
|
3054
3260
|
service.genFilter(`subs.${sub}.update`, { _id: subId }),
|
|
3055
|
-
service.genQuerySelect("read", null, false, [sub, "sub"]),
|
|
3056
3261
|
service.genQuerySelect("update", null, false, [sub, "sub"])
|
|
3057
3262
|
]);
|
|
3058
3263
|
if (subFilter === false) return { success: false, kind: "error", code: "forbidden" /* Forbidden */ };
|
|
3059
|
-
|
|
3264
|
+
const subdoc = findElement(result, subFilter);
|
|
3060
3265
|
if (!subdoc) return { success: false, kind: "error", code: "not_found" /* NotFound */ };
|
|
3061
3266
|
const allowedData = pick2(data, subUpdateSelect);
|
|
3062
|
-
|
|
3063
|
-
await parentDoc.save();
|
|
3064
|
-
|
|
3065
|
-
return { success: true, kind: "single", code: "success" /* Success */, data:
|
|
3267
|
+
applyUpdate(subdoc, allowedData, subUpdateSelect);
|
|
3268
|
+
await persist(() => parentDoc.save());
|
|
3269
|
+
const visible = await visibleMutationRows(service, isAllowed, parentDoc._id, sub, [subdoc], "single");
|
|
3270
|
+
return { success: true, kind: "single", code: "success" /* Success */, data: visible[0] ?? null };
|
|
3066
3271
|
}
|
|
3067
|
-
async function bulkUpdateSub(service, id, sub, data) {
|
|
3272
|
+
async function bulkUpdateSub(service, persist, isAllowed, id, sub, data) {
|
|
3068
3273
|
const { maxBulkItems } = service.getRequestComplexity();
|
|
3069
3274
|
if (data.length > maxBulkItems) {
|
|
3070
3275
|
return {
|
|
@@ -3077,24 +3282,23 @@ async function bulkUpdateSub(service, id, sub, data) {
|
|
|
3077
3282
|
const parentDoc = await getParentDoc(service, id, sub, null, { access: "update" });
|
|
3078
3283
|
if (!parentDoc) return { success: false, kind: "error", code: "not_found" /* NotFound */ };
|
|
3079
3284
|
let result = get5(parentDoc, sub);
|
|
3080
|
-
const [subFilter,
|
|
3285
|
+
const [subFilter, subUpdateSelect] = await Promise.all([
|
|
3081
3286
|
service.genFilter(`subs.${sub}.update`, { _id: { $in: data.map((v) => v._id) } }),
|
|
3082
|
-
service.genQuerySelect("read", null, false, [sub, "sub"]),
|
|
3083
3287
|
service.genQuerySelect("update", null, false, [sub, "sub"])
|
|
3084
3288
|
]);
|
|
3085
3289
|
if (subFilter === false) return { success: false, kind: "error", code: "forbidden" /* Forbidden */ };
|
|
3086
|
-
result = filterCollection(result, subFilter);
|
|
3290
|
+
result = filterCollection(result, subFilter).filter((subdoc) => findElementById(data, subdoc._id));
|
|
3087
3291
|
forEach4(result, (subdoc) => {
|
|
3088
3292
|
const tdata = findElementById(data, subdoc._id);
|
|
3089
3293
|
if (!tdata) return;
|
|
3090
3294
|
const allowedData = pick2(tdata, subUpdateSelect);
|
|
3091
|
-
|
|
3295
|
+
applyUpdate(subdoc, allowedData, subUpdateSelect);
|
|
3092
3296
|
});
|
|
3093
|
-
await parentDoc.save();
|
|
3094
|
-
|
|
3297
|
+
await persist(() => parentDoc.save());
|
|
3298
|
+
result = await visibleMutationRows(service, isAllowed, parentDoc._id, sub, result, "bulk");
|
|
3095
3299
|
return { success: true, kind: "list", code: "success" /* Success */, data: result, count: result.length };
|
|
3096
3300
|
}
|
|
3097
|
-
async function createSub(service, id, sub, data, options) {
|
|
3301
|
+
async function createSub(service, persist, isAllowed, id, sub, data, options) {
|
|
3098
3302
|
const { addFirst } = options ?? {};
|
|
3099
3303
|
const { maxBulkItems } = service.getRequestComplexity();
|
|
3100
3304
|
if (Array.isArray(data) && data.length > maxBulkItems) {
|
|
@@ -3108,21 +3312,18 @@ async function createSub(service, id, sub, data, options) {
|
|
|
3108
3312
|
const parentDoc = await getParentDoc(service, id, sub, null, { access: "update" });
|
|
3109
3313
|
if (!parentDoc) return { success: false, kind: "error", code: "not_found" /* NotFound */ };
|
|
3110
3314
|
let result = get5(parentDoc, sub);
|
|
3111
|
-
const
|
|
3112
|
-
service.genQuerySelect("create", null, false, [sub, "sub"]),
|
|
3113
|
-
service.genQuerySelect("read", null, false, [sub, "sub"])
|
|
3114
|
-
]);
|
|
3315
|
+
const subCreateSelect = await service.genQuerySelect("create", null, false, [sub, "sub"]);
|
|
3115
3316
|
const allowedData = Array.isArray(data) ? data.map((row) => pick2(row, subCreateSelect)) : pick2(data, subCreateSelect);
|
|
3116
3317
|
if (Array.isArray(allowedData)) {
|
|
3117
3318
|
addFirst === true ? result.unshift(...allowedData) : result.push(...allowedData);
|
|
3118
3319
|
} else {
|
|
3119
3320
|
addFirst === true ? result.unshift(allowedData) : result.push(allowedData);
|
|
3120
3321
|
}
|
|
3121
|
-
await parentDoc.save();
|
|
3122
|
-
|
|
3322
|
+
await persist(() => parentDoc.save());
|
|
3323
|
+
result = await visibleMutationRows(service, isAllowed, parentDoc._id, sub, result, "create");
|
|
3123
3324
|
return { success: true, kind: "list", code: "created" /* Created */, data: result, count: result.length };
|
|
3124
3325
|
}
|
|
3125
|
-
async function deleteSub(service, id, sub, subId) {
|
|
3326
|
+
async function deleteSub(service, persist, id, sub, subId) {
|
|
3126
3327
|
const parentDoc = await getParentDoc(service, id, sub, null, { access: "update" });
|
|
3127
3328
|
if (!parentDoc) return { success: false, kind: "error", code: "not_found" /* NotFound */ };
|
|
3128
3329
|
const result = get5(parentDoc, sub);
|
|
@@ -3131,7 +3332,7 @@ async function deleteSub(service, id, sub, subId) {
|
|
|
3131
3332
|
const subdoc = findElement(result, subFilter);
|
|
3132
3333
|
if (!subdoc) return { success: false, kind: "error", code: "not_found" /* NotFound */ };
|
|
3133
3334
|
await ("deleteOne" in subdoc ? subdoc.deleteOne?.() : subdoc.remove?.());
|
|
3134
|
-
await parentDoc.save();
|
|
3335
|
+
await persist(() => parentDoc.save());
|
|
3135
3336
|
return { success: true, kind: "single", code: "success" /* Success */, data: subdoc._id };
|
|
3136
3337
|
}
|
|
3137
3338
|
async function getParentDoc(service, id, sub, args, options) {
|
|
@@ -3290,6 +3491,15 @@ var shouldKeepCorrelatedRef = (ref, keep) => {
|
|
|
3290
3491
|
return false;
|
|
3291
3492
|
};
|
|
3292
3493
|
var Service = class extends Base {
|
|
3494
|
+
constructor(req, modelName) {
|
|
3495
|
+
super(req, modelName);
|
|
3496
|
+
this.persist = (operation) => this.getCorrelatedExecState().scheduler.work(operation);
|
|
3497
|
+
this.model = admitModelPersistence(this.createModelAdapter(modelName), this.getCorrelatedExecState().scheduler);
|
|
3498
|
+
this.options = this.getModelRouterOptions(modelName);
|
|
3499
|
+
this.defaults = this.options.defaults || {};
|
|
3500
|
+
this.baseFields = ["_id"];
|
|
3501
|
+
this.baseFieldsExt = this.baseFields.concat(this.options.documentPermissionField);
|
|
3502
|
+
}
|
|
3293
3503
|
findRawParentDoc(args) {
|
|
3294
3504
|
return this.model.findOne({
|
|
3295
3505
|
...args,
|
|
@@ -3306,6 +3516,10 @@ var Service = class extends Base {
|
|
|
3306
3516
|
asServiceHookContext(context) {
|
|
3307
3517
|
return context;
|
|
3308
3518
|
}
|
|
3519
|
+
getEffectiveAllowedSortFields(allowedSortFields) {
|
|
3520
|
+
const extra = this.options?.sortableFields ?? [];
|
|
3521
|
+
return extra.length > 0 ? [...allowedSortFields, ...extra] : allowedSortFields;
|
|
3522
|
+
}
|
|
3309
3523
|
beginOp(op, filter2, extra) {
|
|
3310
3524
|
const startedAt = Date.now();
|
|
3311
3525
|
debug({
|
|
@@ -3327,14 +3541,6 @@ var Service = class extends Base {
|
|
|
3327
3541
|
...extra ?? {}
|
|
3328
3542
|
});
|
|
3329
3543
|
}
|
|
3330
|
-
constructor(req, modelName) {
|
|
3331
|
-
super(req, modelName);
|
|
3332
|
-
this.model = this.createModelAdapter(modelName);
|
|
3333
|
-
this.options = this.getModelRouterOptions(modelName);
|
|
3334
|
-
this.defaults = this.options.defaults || {};
|
|
3335
|
-
this.baseFields = ["_id"];
|
|
3336
|
-
this.baseFieldsExt = this.baseFields.concat(this.options.documentPermissionField);
|
|
3337
|
-
}
|
|
3338
3544
|
async findOne(filter2, args, options) {
|
|
3339
3545
|
const filterErrors = this.validateClientFilter(filter2);
|
|
3340
3546
|
if (filterErrors.length > 0) return { success: false, kind: "error", code: "bad_request" /* BadRequest */, errors: filterErrors };
|
|
@@ -3342,30 +3548,36 @@ var Service = class extends Base {
|
|
|
3342
3548
|
const { skim, includePermissions, access, populateAccess, lean } = this.resolveFindOneOptions(options);
|
|
3343
3549
|
const { filter: overrideFilter, select: overrideSelect, populate: overridePopulate } = overrides ?? {};
|
|
3344
3550
|
let parsedFilter;
|
|
3551
|
+
let processedInclude;
|
|
3345
3552
|
try {
|
|
3346
|
-
|
|
3553
|
+
processedInclude = this.processInclude(include);
|
|
3554
|
+
parsedFilter = overrideFilter === false ? false : await this.parseClientData(filter2);
|
|
3347
3555
|
} catch (error2) {
|
|
3348
3556
|
const result = this.getClientRequestErrorResult(error2);
|
|
3349
3557
|
if (result) return result;
|
|
3350
3558
|
throw error2;
|
|
3351
3559
|
}
|
|
3352
3560
|
let [_filter, _select, _populate, allowedSortFields] = await Promise.all([
|
|
3353
|
-
overrideFilter
|
|
3561
|
+
overrideFilter ?? this.genFilter(access, parsedFilter),
|
|
3354
3562
|
overrideSelect || this.genQuerySelect(access, select),
|
|
3355
3563
|
overridePopulate || this.genPopulate(populateAccess || access, populate),
|
|
3356
3564
|
this.genAllowedFields({}, access, this.baseFieldsExt)
|
|
3357
3565
|
]);
|
|
3358
|
-
const { includes, correlatedIncludes, includeLocalFields, includePaths, correlatedReferenceFields } =
|
|
3566
|
+
const { includes, correlatedIncludes, includeLocalFields, includePaths, correlatedReferenceFields } = processedInclude;
|
|
3359
3567
|
const correlatedOutputPaths = correlatedIncludes.map((entry) => entry.path);
|
|
3360
3568
|
const finalSelect = normalizeSelect(_select).concat(includeLocalFields, correlatedReferenceFields);
|
|
3569
|
+
const effectiveAllowedSortFields = this.getEffectiveAllowedSortFields(allowedSortFields);
|
|
3570
|
+
const stripDisallowedSort = this.options?.stripDisallowedSort ?? false;
|
|
3571
|
+
const sanitizedSort = stripDisallowedSort ? sanitizeSortFields(sort, effectiveAllowedSortFields) : null;
|
|
3572
|
+
const effectiveSort = stripDisallowedSort ? sanitizedSort?.sort : sort;
|
|
3361
3573
|
const query = {
|
|
3362
3574
|
filter: _filter,
|
|
3363
3575
|
select: finalSelect,
|
|
3364
|
-
sort,
|
|
3576
|
+
sort: effectiveSort,
|
|
3365
3577
|
populate: _populate
|
|
3366
3578
|
};
|
|
3367
3579
|
const startedAt = this.beginOp("findOne", _filter, {
|
|
3368
|
-
sort,
|
|
3580
|
+
sort: effectiveSort,
|
|
3369
3581
|
selectCount: finalSelect.length,
|
|
3370
3582
|
populateCount: Array.isArray(_populate) ? _populate.length : _populate ? 1 : 0
|
|
3371
3583
|
});
|
|
@@ -3373,10 +3585,17 @@ var Service = class extends Base {
|
|
|
3373
3585
|
this.completeOp("findOne", startedAt, "forbidden" /* Forbidden */, _filter);
|
|
3374
3586
|
return { success: false, kind: "error", code: "forbidden" /* Forbidden */, query };
|
|
3375
3587
|
}
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3588
|
+
if (stripDisallowedSort) {
|
|
3589
|
+
if (sanitizedSort && sanitizedSort.errors.length > 0) {
|
|
3590
|
+
this.completeOp("findOne", startedAt, "bad_request" /* BadRequest */, _filter);
|
|
3591
|
+
return { success: false, kind: "error", code: "bad_request" /* BadRequest */, errors: sanitizedSort.errors, query };
|
|
3592
|
+
}
|
|
3593
|
+
} else {
|
|
3594
|
+
const sortErrors = validateSortFields(sort, effectiveAllowedSortFields);
|
|
3595
|
+
if (sortErrors.length > 0) {
|
|
3596
|
+
this.completeOp("findOne", startedAt, "bad_request" /* BadRequest */, _filter);
|
|
3597
|
+
return { success: false, kind: "error", code: "bad_request" /* BadRequest */, errors: sortErrors, query };
|
|
3598
|
+
}
|
|
3380
3599
|
}
|
|
3381
3600
|
let doc = await this.model.findOne({ ...query, lean });
|
|
3382
3601
|
if (!doc) {
|
|
@@ -3390,7 +3609,7 @@ var Service = class extends Base {
|
|
|
3390
3609
|
originalDocumentSnapshot: toObject(doc),
|
|
3391
3610
|
resolvedQuery: query
|
|
3392
3611
|
};
|
|
3393
|
-
const correlatedSnapshots = [
|
|
3612
|
+
const correlatedSnapshots = [cloneDeep4(toObject(doc))];
|
|
3394
3613
|
try {
|
|
3395
3614
|
doc = await this.includeDocs(doc, includes);
|
|
3396
3615
|
if (correlatedIncludes.length > 0) {
|
|
@@ -3437,7 +3656,8 @@ var Service = class extends Base {
|
|
|
3437
3656
|
const { select, populate, include, overrides } = this.resolveFindByIdArgs(args);
|
|
3438
3657
|
const { skim, includePermissions, access, populateAccess, lean } = this.resolveFindByIdOptions(options);
|
|
3439
3658
|
const { select: overrideSelect, populate: overridePopulate, idFilter: overrideIdFilter } = overrides ?? {};
|
|
3440
|
-
const filter2 = overrideIdFilter
|
|
3659
|
+
const filter2 = overrideIdFilter ?? await this.genIDFilter(id);
|
|
3660
|
+
if (filter2 === false) return { success: false, kind: "error", code: "forbidden" /* Forbidden */, query: { filter: filter2 } };
|
|
3441
3661
|
return this.findOne(
|
|
3442
3662
|
filter2,
|
|
3443
3663
|
{
|
|
@@ -3459,15 +3679,17 @@ var Service = class extends Base {
|
|
|
3459
3679
|
const { skim, includePermissions, includeCount, populateAccess, lean } = this.resolveFindOptions(options);
|
|
3460
3680
|
const { filter: overrideFilter, select: overrideSelect, populate: overridePopulate } = overrides ?? {};
|
|
3461
3681
|
let parsedFilter;
|
|
3682
|
+
let processedInclude;
|
|
3462
3683
|
try {
|
|
3463
|
-
|
|
3684
|
+
processedInclude = this.processInclude(include);
|
|
3685
|
+
parsedFilter = overrideFilter === false ? false : await this.parseClientData(filter2);
|
|
3464
3686
|
} catch (error2) {
|
|
3465
3687
|
const result = this.getClientRequestErrorResult(error2);
|
|
3466
3688
|
if (result) return result;
|
|
3467
3689
|
throw error2;
|
|
3468
3690
|
}
|
|
3469
3691
|
const [_filter, _select, _populate, pagination, allowedSortFields] = await Promise.all([
|
|
3470
|
-
overrideFilter
|
|
3692
|
+
overrideFilter ?? this.genFilter("list", parsedFilter),
|
|
3471
3693
|
overrideSelect || this.genQuerySelect("list", select),
|
|
3472
3694
|
overridePopulate || this.genPopulate(populateAccess, populate),
|
|
3473
3695
|
genPagination({ skip, limit, page, pageSize }, this.options.listHardLimit),
|
|
@@ -3475,17 +3697,21 @@ var Service = class extends Base {
|
|
|
3475
3697
|
]);
|
|
3476
3698
|
const finalSelect = normalizeSelect(_select);
|
|
3477
3699
|
const filteredPopulate = isArray8(finalSelect) && isArray8(_populate) ? _populate.filter((p) => finalSelect.includes(p.path.split(".")[0])) : _populate;
|
|
3478
|
-
const { includes, correlatedIncludes, includeLocalFields, includePaths, correlatedReferenceFields } =
|
|
3700
|
+
const { includes, correlatedIncludes, includeLocalFields, includePaths, correlatedReferenceFields } = processedInclude;
|
|
3479
3701
|
const correlatedOutputPaths = correlatedIncludes.map((entry) => entry.path);
|
|
3702
|
+
const effectiveAllowedSortFields = this.getEffectiveAllowedSortFields(allowedSortFields);
|
|
3703
|
+
const stripDisallowedSort = this.options?.stripDisallowedSort ?? false;
|
|
3704
|
+
const sanitizedSort = stripDisallowedSort ? sanitizeSortFields(sort, effectiveAllowedSortFields) : null;
|
|
3705
|
+
const effectiveSort = stripDisallowedSort ? sanitizedSort?.sort : sort;
|
|
3480
3706
|
const query = {
|
|
3481
3707
|
filter: _filter,
|
|
3482
3708
|
select: finalSelect.concat(includeLocalFields, correlatedReferenceFields),
|
|
3483
3709
|
populate: filteredPopulate,
|
|
3484
|
-
sort,
|
|
3710
|
+
sort: effectiveSort,
|
|
3485
3711
|
...pagination
|
|
3486
3712
|
};
|
|
3487
3713
|
const startedAt = this.beginOp("find", _filter, {
|
|
3488
|
-
sort,
|
|
3714
|
+
sort: effectiveSort,
|
|
3489
3715
|
skip: pagination.skip,
|
|
3490
3716
|
limit: pagination.limit,
|
|
3491
3717
|
selectCount: finalSelect.concat(includeLocalFields, correlatedReferenceFields).length,
|
|
@@ -3495,10 +3721,17 @@ var Service = class extends Base {
|
|
|
3495
3721
|
this.completeOp("find", startedAt, "forbidden" /* Forbidden */, _filter);
|
|
3496
3722
|
return { success: false, kind: "error", code: "forbidden" /* Forbidden */, query };
|
|
3497
3723
|
}
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3724
|
+
if (stripDisallowedSort) {
|
|
3725
|
+
if (sanitizedSort && sanitizedSort.errors.length > 0) {
|
|
3726
|
+
this.completeOp("find", startedAt, "bad_request" /* BadRequest */, _filter);
|
|
3727
|
+
return { success: false, kind: "error", code: "bad_request" /* BadRequest */, errors: sanitizedSort.errors, query };
|
|
3728
|
+
}
|
|
3729
|
+
} else {
|
|
3730
|
+
const sortErrors = validateSortFields(sort, effectiveAllowedSortFields);
|
|
3731
|
+
if (sortErrors.length > 0) {
|
|
3732
|
+
this.completeOp("find", startedAt, "bad_request" /* BadRequest */, _filter);
|
|
3733
|
+
return { success: false, kind: "error", code: "bad_request" /* BadRequest */, errors: sortErrors, query };
|
|
3734
|
+
}
|
|
3502
3735
|
}
|
|
3503
3736
|
let docs = await this.model.find({
|
|
3504
3737
|
...query,
|
|
@@ -3513,7 +3746,7 @@ var Service = class extends Base {
|
|
|
3513
3746
|
resolvedQuery: query
|
|
3514
3747
|
}));
|
|
3515
3748
|
const _decorate = isFunction3(decorate) ? decorate : (v) => v;
|
|
3516
|
-
const correlatedSnapshots = docs.map((doc) =>
|
|
3749
|
+
const correlatedSnapshots = docs.map((doc) => cloneDeep4(toObject(doc)));
|
|
3517
3750
|
try {
|
|
3518
3751
|
docs = await this.includeDocs(docs, includes);
|
|
3519
3752
|
if (correlatedIncludes.length > 0) {
|
|
@@ -3685,7 +3918,7 @@ var Service = class extends Base {
|
|
|
3685
3918
|
}
|
|
3686
3919
|
if (includeDocPermissions) doc = await this.addDocPermissions(doc, "create", contexts[index]);
|
|
3687
3920
|
if (includePermissions) doc = await this.addFieldPermissions(doc, "read", contexts[index]);
|
|
3688
|
-
if (resolvedPopulate.length > 0) await populateDoc(doc, resolvedPopulate);
|
|
3921
|
+
if (resolvedPopulate.length > 0) await this.persist(() => populateDoc(doc, resolvedPopulate));
|
|
3689
3922
|
doc = await this.trimOutputFields(doc, "read", this.baseFieldsExt);
|
|
3690
3923
|
let outputDoc = await _decorate(doc, contexts[index]);
|
|
3691
3924
|
if (!includePermissions) outputDoc = this.addEmptyPermissions(outputDoc);
|
|
@@ -3741,6 +3974,13 @@ var Service = class extends Base {
|
|
|
3741
3974
|
data: doc
|
|
3742
3975
|
};
|
|
3743
3976
|
}
|
|
3977
|
+
/**
|
|
3978
|
+
* Update at authorized policy paths: admitted leaves preserve omitted siblings;
|
|
3979
|
+
* whole-authorized objects/arrays replace. Shared by updateById and existing-row
|
|
3980
|
+
* upsert. Validation precedes trusted prepare output (not re-filtered); partial
|
|
3981
|
+
* prepare containers preserve omissions. Transform retains explicit document-setter
|
|
3982
|
+
* replacement authority. Send nested client JSON, not literal dotted update keys.
|
|
3983
|
+
*/
|
|
3744
3984
|
async updateOne(filter2, data, args, options, decorate) {
|
|
3745
3985
|
const filterErrors = this.validateClientFilter(filter2);
|
|
3746
3986
|
if (filterErrors.length > 0) return { success: false, kind: "error", code: "bad_request" /* BadRequest */, errors: filterErrors };
|
|
@@ -3748,7 +3988,7 @@ var Service = class extends Base {
|
|
|
3748
3988
|
const { skim, includePermissions, populateAccess } = this.resolveUpdateOneOptions(options);
|
|
3749
3989
|
const { filter: overrideFilter, populate: overridePopulate } = overrides ?? {};
|
|
3750
3990
|
const [_filter, _populate] = await Promise.all([
|
|
3751
|
-
overrideFilter
|
|
3991
|
+
overrideFilter ?? this.genFilter("update", filter2),
|
|
3752
3992
|
overridePopulate || this.genPopulate(populateAccess, populate)
|
|
3753
3993
|
]);
|
|
3754
3994
|
const query = { filter: _filter, populate: _populate };
|
|
@@ -3803,11 +4043,11 @@ var Service = class extends Base {
|
|
|
3803
4043
|
}
|
|
3804
4044
|
const prepared = await this.prepare(allowedData, "update", context);
|
|
3805
4045
|
context.preparedData = prepared;
|
|
3806
|
-
|
|
4046
|
+
applyUpdate(doc, prepared, allowedFields);
|
|
3807
4047
|
context.modifiedPaths = doc.modifiedPaths();
|
|
3808
4048
|
doc = assertModelDocument(await this.transform(doc, "update", context), this.modelName, "transform");
|
|
3809
4049
|
context.currentDocument = doc;
|
|
3810
|
-
doc = await doc.save();
|
|
4050
|
+
doc = await this.persist(() => doc.save());
|
|
3811
4051
|
const diffExcludeFields = [this.options.documentPermissionField, "__v"];
|
|
3812
4052
|
this.asServiceHookContext(context).diff = (d) => {
|
|
3813
4053
|
context.changes = diff(
|
|
@@ -3827,7 +4067,7 @@ var Service = class extends Base {
|
|
|
3827
4067
|
}
|
|
3828
4068
|
if (includeDocPermissions) doc = await this.addDocPermissions(doc, "update", context);
|
|
3829
4069
|
if (includePermissions) doc = await this.addFieldPermissions(doc, "update", context);
|
|
3830
|
-
if (_populate) await populateDoc(doc, _populate);
|
|
4070
|
+
if (_populate) await this.persist(() => populateDoc(doc, _populate));
|
|
3831
4071
|
doc = await this.trimOutputFields(doc, "read", this.baseFieldsExt);
|
|
3832
4072
|
let outputDoc = doc;
|
|
3833
4073
|
if (isFunction3(decorate)) outputDoc = await decorate(outputDoc, context);
|
|
@@ -3839,7 +4079,8 @@ var Service = class extends Base {
|
|
|
3839
4079
|
const { populate, overrides } = this.resolveUpdateByIdArgs(args);
|
|
3840
4080
|
const { skim, includePermissions, populateAccess } = this.resolveUpdateByIdOptions(options);
|
|
3841
4081
|
const { populate: overridePopulate, idFilter: overrideIdFilter } = overrides;
|
|
3842
|
-
const filter2 = overrideIdFilter
|
|
4082
|
+
const filter2 = overrideIdFilter ?? await this.genIDFilter(id);
|
|
4083
|
+
if (filter2 === false) return { success: false, kind: "error", code: "forbidden" /* Forbidden */, query: { filter: filter2 } };
|
|
3843
4084
|
return this.updateOne(
|
|
3844
4085
|
filter2,
|
|
3845
4086
|
data,
|
|
@@ -3859,7 +4100,7 @@ var Service = class extends Base {
|
|
|
3859
4100
|
const { populate, overrides } = this.resolveUpsertArgs(args);
|
|
3860
4101
|
const { skim, includePermissions, populateAccess } = this.resolveUpsertOptions(options);
|
|
3861
4102
|
const { filter: overrideFilter, populate: overridePopulate } = overrides ?? {};
|
|
3862
|
-
const _filter = await (overrideFilter
|
|
4103
|
+
const _filter = await (overrideFilter ?? this.genFilter("update", filter2));
|
|
3863
4104
|
const query = { filter: _filter };
|
|
3864
4105
|
const startedAt = this.beginOp("upsert", _filter);
|
|
3865
4106
|
if (_filter === false) {
|
|
@@ -3919,7 +4160,9 @@ var Service = class extends Base {
|
|
|
3919
4160
|
resolvedQuery: query
|
|
3920
4161
|
};
|
|
3921
4162
|
await this.beforeDelete(doc, context);
|
|
3922
|
-
await
|
|
4163
|
+
await this.persist(
|
|
4164
|
+
() => "deleteOne" in doc ? doc.deleteOne() : doc.remove()
|
|
4165
|
+
);
|
|
3923
4166
|
context.finalDocumentSnapshot = toObject(doc);
|
|
3924
4167
|
await this.afterDelete(doc, context);
|
|
3925
4168
|
this.completeOp("delete", startedAt, "success" /* Success */, filter2);
|
|
@@ -3930,6 +4173,7 @@ var Service = class extends Base {
|
|
|
3930
4173
|
if (filterErrors.length > 0) return { success: false, kind: "error", code: "bad_request" /* BadRequest */, errors: filterErrors };
|
|
3931
4174
|
const { access, includeId } = this.resolveExistsOptions(options);
|
|
3932
4175
|
filter2 = await this.genFilter(access, filter2);
|
|
4176
|
+
if (filter2 === false) return { success: false, kind: "error", code: "forbidden" /* Forbidden */, query: { filter: filter2 } };
|
|
3933
4177
|
const result = await this.model.exists(filter2);
|
|
3934
4178
|
return {
|
|
3935
4179
|
success: true,
|
|
@@ -3999,6 +4243,13 @@ var Service = class extends Base {
|
|
|
3999
4243
|
query
|
|
4000
4244
|
};
|
|
4001
4245
|
}
|
|
4246
|
+
/**
|
|
4247
|
+
* Group target document IDs by original parent-key aliases using one aggregate.
|
|
4248
|
+
* Resolves the requested row policy (legacy count includes pass `count`), casts
|
|
4249
|
+
* authorized filters/foreign operands with the target schema, and deduplicates IDs
|
|
4250
|
+
* independently of pagination. Cast failures return BadRequest; false policy returns
|
|
4251
|
+
* Forbidden. Casting does not execute countDocuments middleware/plugins.
|
|
4252
|
+
*/
|
|
4002
4253
|
async countByFieldValues(foreignField, values, filter2 = {}, access = "list") {
|
|
4003
4254
|
if (!isValidFieldPath(foreignField)) {
|
|
4004
4255
|
return {
|
|
@@ -4017,7 +4268,25 @@ var Service = class extends Base {
|
|
|
4017
4268
|
});
|
|
4018
4269
|
const query = { filter: filter2 };
|
|
4019
4270
|
if (filter2 === false) return { success: false, kind: "error", code: "forbidden" /* Forbidden */, query };
|
|
4020
|
-
|
|
4271
|
+
let matchFilter = filter2;
|
|
4272
|
+
let castValues = uniqueValues;
|
|
4273
|
+
try {
|
|
4274
|
+
if (this.model.castFilter) {
|
|
4275
|
+
matchFilter = this.model.castFilter(matchFilter);
|
|
4276
|
+
const foreignFilter = this.model.castFilter({ [foreignField]: { $in: uniqueValues } });
|
|
4277
|
+
castValues = foreignFilter[foreignField]?.$in;
|
|
4278
|
+
if (!Array.isArray(castValues) || castValues.length !== uniqueValues.length) {
|
|
4279
|
+
throw new Error("Foreign field query casting must retain the join operands");
|
|
4280
|
+
}
|
|
4281
|
+
}
|
|
4282
|
+
} catch {
|
|
4283
|
+
return {
|
|
4284
|
+
success: false,
|
|
4285
|
+
kind: "error",
|
|
4286
|
+
code: "bad_request" /* BadRequest */,
|
|
4287
|
+
errors: [{ detail: "Invalid grouped include count filter or foreign-key value" }]
|
|
4288
|
+
};
|
|
4289
|
+
}
|
|
4021
4290
|
const rows = await this.model.aggregate([
|
|
4022
4291
|
{ $match: matchFilter },
|
|
4023
4292
|
{
|
|
@@ -4028,12 +4297,20 @@ var Service = class extends Base {
|
|
|
4028
4297
|
}
|
|
4029
4298
|
},
|
|
4030
4299
|
{ $unwind: "$foreignValues" },
|
|
4031
|
-
{ $match: { foreignValues: { $in:
|
|
4300
|
+
{ $match: { foreignValues: { $in: castValues } } },
|
|
4032
4301
|
{ $group: { _id: "$foreignValues", documentIds: { $addToSet: "$_id" } } }
|
|
4033
4302
|
]);
|
|
4034
|
-
const
|
|
4303
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
4035
4304
|
for (const row of rows) {
|
|
4036
|
-
|
|
4305
|
+
const key = String(row._id);
|
|
4306
|
+
const ids = grouped.get(key) ?? /* @__PURE__ */ new Set();
|
|
4307
|
+
for (const id of row.documentIds) ids.add(String(id));
|
|
4308
|
+
grouped.set(key, ids);
|
|
4309
|
+
}
|
|
4310
|
+
const counts = /* @__PURE__ */ new Map();
|
|
4311
|
+
for (let i = 0; i < uniqueValues.length; i++) {
|
|
4312
|
+
const ids = grouped.get(String(castValues[i]));
|
|
4313
|
+
if (ids) counts.set(String(uniqueValues[i]), ids);
|
|
4037
4314
|
}
|
|
4038
4315
|
return { success: true, kind: "single", code: "success" /* Success */, data: counts, query };
|
|
4039
4316
|
}
|
|
@@ -4112,17 +4389,60 @@ var Service = class extends Base {
|
|
|
4112
4389
|
async readSub(id, sub, subId, options) {
|
|
4113
4390
|
return readSub(this, id, sub, subId, options);
|
|
4114
4391
|
}
|
|
4392
|
+
/**
|
|
4393
|
+
* Apply authorized policy-path updates, preserving protected siblings, then save.
|
|
4394
|
+
* Output requires post-save parent read and subdocument read operation/row access;
|
|
4395
|
+
* hidden output is successful `data: null` (direct/root-entry 200), not a failed write.
|
|
4396
|
+
* Visible output uses read fields plus `_id`. Do not retry solely for null output.
|
|
4397
|
+
*/
|
|
4115
4398
|
async updateSub(id, sub, subId, data) {
|
|
4116
|
-
return updateSub(
|
|
4399
|
+
return updateSub(
|
|
4400
|
+
this,
|
|
4401
|
+
this.persist,
|
|
4402
|
+
(access) => this.req.macl.isAllowed(this.modelName, access),
|
|
4403
|
+
id,
|
|
4404
|
+
sub,
|
|
4405
|
+
subId,
|
|
4406
|
+
data
|
|
4407
|
+
);
|
|
4117
4408
|
}
|
|
4409
|
+
/**
|
|
4410
|
+
* Save authorized targeted updates, then return readable targets in stored order.
|
|
4411
|
+
* Requires parent/subdocument read access for output, not subdocument list access;
|
|
4412
|
+
* uses read fields plus `_id`. Hidden output succeeds with `data: [], count: 0`
|
|
4413
|
+
* (direct/root-entry 200); count measures visible rows, not all updated rows.
|
|
4414
|
+
*/
|
|
4118
4415
|
async bulkUpdateSub(id, sub, data) {
|
|
4119
|
-
return bulkUpdateSub(
|
|
4416
|
+
return bulkUpdateSub(
|
|
4417
|
+
this,
|
|
4418
|
+
this.persist,
|
|
4419
|
+
(access) => this.req.macl.isAllowed(this.modelName, access),
|
|
4420
|
+
id,
|
|
4421
|
+
sub,
|
|
4422
|
+
castArray2(data)
|
|
4423
|
+
);
|
|
4120
4424
|
}
|
|
4425
|
+
/**
|
|
4426
|
+
* Append/prepend admitted rows, then return the visible full array (including when
|
|
4427
|
+
* input is empty). Output requires post-save parent read and subdocument list AND
|
|
4428
|
+
* read guards/row filters; fields use read projection plus `_id`.
|
|
4429
|
+
* Hidden output succeeds with code `created`, `data: [], count: 0`
|
|
4430
|
+
* (direct/root-entry 201). Visible count/order follow filtered stored rows.
|
|
4431
|
+
* Empty output does not mean the write failed; addFirst is a service option.
|
|
4432
|
+
*/
|
|
4121
4433
|
async createSub(id, sub, data, options) {
|
|
4122
|
-
return createSub(
|
|
4434
|
+
return createSub(
|
|
4435
|
+
this,
|
|
4436
|
+
this.persist,
|
|
4437
|
+
(access) => this.req.macl.isAllowed(this.modelName, access),
|
|
4438
|
+
id,
|
|
4439
|
+
sub,
|
|
4440
|
+
data,
|
|
4441
|
+
options
|
|
4442
|
+
);
|
|
4123
4443
|
}
|
|
4124
4444
|
async deleteSub(id, sub, subId) {
|
|
4125
|
-
return deleteSub(this, id, sub, subId);
|
|
4445
|
+
return deleteSub(this, this.persist, id, sub, subId);
|
|
4126
4446
|
}
|
|
4127
4447
|
async getParentDoc(id, sub, args, options) {
|
|
4128
4448
|
return getParentDoc(this, id, sub, args, options);
|
|
@@ -4196,6 +4516,7 @@ var PublicService = class extends Service {
|
|
|
4196
4516
|
},
|
|
4197
4517
|
{ skim, includePermissions, access, populateAccess, lean }
|
|
4198
4518
|
);
|
|
4519
|
+
if (!result.success && (result.code === "bad_request" /* BadRequest */ || result.code === "forbidden" /* Forbidden */)) return result;
|
|
4199
4520
|
if (tryList && (!result.success || !result.data)) {
|
|
4200
4521
|
const listAllowed = await this.req.macl.isAllowed(this.modelName, "list");
|
|
4201
4522
|
if (!listAllowed) {
|
|
@@ -4236,6 +4557,7 @@ var PublicService = class extends Service {
|
|
|
4236
4557
|
},
|
|
4237
4558
|
{ skim, includePermissions, access, populateAccess, lean }
|
|
4238
4559
|
);
|
|
4560
|
+
if (!result.success && (result.code === "bad_request" /* BadRequest */ || result.code === "forbidden" /* Forbidden */)) return result;
|
|
4239
4561
|
if (tryList && (!result.success || !result.data)) {
|
|
4240
4562
|
const listAllowed = await this.req.macl.isAllowed(this.modelName, "list");
|
|
4241
4563
|
if (!listAllowed) {
|
|
@@ -4284,6 +4606,11 @@ var PublicService = class extends Service {
|
|
|
4284
4606
|
}
|
|
4285
4607
|
return result;
|
|
4286
4608
|
}
|
|
4609
|
+
/**
|
|
4610
|
+
* With an _id, check update-policy existence then update: terminal denial returns
|
|
4611
|
+
* Forbidden, an allowed miss remains Unauthorized, and neither creates a row.
|
|
4612
|
+
* Without an _id, use the create path.
|
|
4613
|
+
*/
|
|
4287
4614
|
async _upsert(data, args, options) {
|
|
4288
4615
|
const idKey = this.getIdentifier();
|
|
4289
4616
|
if (idKey !== "_id") {
|
|
@@ -4514,7 +4841,7 @@ var DataService = class {
|
|
|
4514
4841
|
};
|
|
4515
4842
|
|
|
4516
4843
|
// src/processors.ts
|
|
4517
|
-
import { cloneDeep as
|
|
4844
|
+
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";
|
|
4518
4845
|
var UNSAFE_PATH_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
4519
4846
|
var DEFAULT_MAX_SLICE = 1e3;
|
|
4520
4847
|
var DEFAULT_MASK_REPLACEMENT = "***";
|
|
@@ -4549,7 +4876,7 @@ function resolveLeafParents(obj, src) {
|
|
|
4549
4876
|
for (let ind = 0; ind < segs.length - 1; ind += 1) {
|
|
4550
4877
|
const seg = segs[ind];
|
|
4551
4878
|
targets = targets.reduce((ret, target) => {
|
|
4552
|
-
if (!
|
|
4879
|
+
if (!isPlainObject15(target)) return ret;
|
|
4553
4880
|
const targetObject = target;
|
|
4554
4881
|
const next = targetObject[seg];
|
|
4555
4882
|
if (isArray9(next)) {
|
|
@@ -4563,7 +4890,7 @@ function resolveLeafParents(obj, src) {
|
|
|
4563
4890
|
const leaf = segs[segs.length - 1];
|
|
4564
4891
|
const pairs = [];
|
|
4565
4892
|
forEach6(targets, (target) => {
|
|
4566
|
-
if (!
|
|
4893
|
+
if (!isPlainObject15(target)) return;
|
|
4567
4894
|
pairs.push({ parent: target, leaf });
|
|
4568
4895
|
});
|
|
4569
4896
|
return pairs;
|
|
@@ -4571,7 +4898,7 @@ function resolveLeafParents(obj, src) {
|
|
|
4571
4898
|
var copyAndDepopulate = (docObject, operations, options = { mutable: true, idField: "_id" }) => {
|
|
4572
4899
|
const mutable = options.mutable !== false;
|
|
4573
4900
|
const idField = options.idField ?? "_id";
|
|
4574
|
-
const obj = mutable ? docObject :
|
|
4901
|
+
const obj = mutable ? docObject : cloneDeep5(docObject);
|
|
4575
4902
|
forEach6(Array.isArray(operations) ? operations : [], (op) => {
|
|
4576
4903
|
if (!op || typeof op.src !== "string" || typeof op.dest !== "string") return;
|
|
4577
4904
|
const src = op.src;
|
|
@@ -4584,7 +4911,7 @@ var copyAndDepopulate = (docObject, operations, options = { mutable: true, idFie
|
|
|
4584
4911
|
forEach6(segs, (seg, ind) => {
|
|
4585
4912
|
if (segs.length === ind + 1) {
|
|
4586
4913
|
forEach6(targets, (target) => {
|
|
4587
|
-
if (!
|
|
4914
|
+
if (!isPlainObject15(target)) return;
|
|
4588
4915
|
const targetObject = target;
|
|
4589
4916
|
const leaf = targetObject[seg];
|
|
4590
4917
|
const sourceForDest = leaf;
|
|
@@ -4592,7 +4919,7 @@ var copyAndDepopulate = (docObject, operations, options = { mutable: true, idFie
|
|
|
4592
4919
|
if (isArray9(leaf)) {
|
|
4593
4920
|
if (leaf.length === 0) {
|
|
4594
4921
|
depopulated = [];
|
|
4595
|
-
} else if (leaf.every((item) =>
|
|
4922
|
+
} else if (leaf.every((item) => isPlainObject15(item))) {
|
|
4596
4923
|
depopulated = map(leaf, idField).map((id, i) => {
|
|
4597
4924
|
if (id === void 0) {
|
|
4598
4925
|
failMissingId(`${src}[${i}]`);
|
|
@@ -4602,7 +4929,7 @@ var copyAndDepopulate = (docObject, operations, options = { mutable: true, idFie
|
|
|
4602
4929
|
} else {
|
|
4603
4930
|
return;
|
|
4604
4931
|
}
|
|
4605
|
-
} else if (
|
|
4932
|
+
} else if (isPlainObject15(leaf)) {
|
|
4606
4933
|
const id = get7(leaf, idField);
|
|
4607
4934
|
if (id === void 0) {
|
|
4608
4935
|
failMissingId(src);
|
|
@@ -4611,12 +4938,12 @@ var copyAndDepopulate = (docObject, operations, options = { mutable: true, idFie
|
|
|
4611
4938
|
} else {
|
|
4612
4939
|
return;
|
|
4613
4940
|
}
|
|
4614
|
-
|
|
4615
|
-
|
|
4941
|
+
set6(targetObject, dest, sourceForDest);
|
|
4942
|
+
set6(targetObject, seg, depopulated);
|
|
4616
4943
|
});
|
|
4617
4944
|
} else {
|
|
4618
4945
|
targets = targets.reduce((ret, target) => {
|
|
4619
|
-
if (!
|
|
4946
|
+
if (!isPlainObject15(target)) return ret;
|
|
4620
4947
|
const targetObject = target;
|
|
4621
4948
|
const next = targetObject[seg];
|
|
4622
4949
|
if (isArray9(next)) {
|
|
@@ -4633,7 +4960,7 @@ var copyAndDepopulate = (docObject, operations, options = { mutable: true, idFie
|
|
|
4633
4960
|
};
|
|
4634
4961
|
var copyPaths = (docObject, operations, options = { mutable: true }) => {
|
|
4635
4962
|
const mutable = options.mutable !== false;
|
|
4636
|
-
const obj = mutable ? docObject :
|
|
4963
|
+
const obj = mutable ? docObject : cloneDeep5(docObject);
|
|
4637
4964
|
forEach6(Array.isArray(operations) ? operations : [], (op) => {
|
|
4638
4965
|
if (!op || typeof op.src !== "string" || typeof op.dest !== "string") return;
|
|
4639
4966
|
const src = op.src;
|
|
@@ -4644,14 +4971,14 @@ var copyPaths = (docObject, operations, options = { mutable: true }) => {
|
|
|
4644
4971
|
if (src === dest) return;
|
|
4645
4972
|
forEach6(resolveLeafParents(obj, src), ({ parent, leaf }) => {
|
|
4646
4973
|
if (!hasOwn(parent, leaf)) return;
|
|
4647
|
-
|
|
4974
|
+
set6(parent, dest, cloneDeep5(parent[leaf]));
|
|
4648
4975
|
});
|
|
4649
4976
|
});
|
|
4650
4977
|
return obj;
|
|
4651
4978
|
};
|
|
4652
4979
|
var movePaths = (docObject, operations, options = { mutable: true }) => {
|
|
4653
4980
|
const mutable = options.mutable !== false;
|
|
4654
|
-
const obj = mutable ? docObject :
|
|
4981
|
+
const obj = mutable ? docObject : cloneDeep5(docObject);
|
|
4655
4982
|
forEach6(Array.isArray(operations) ? operations : [], (op) => {
|
|
4656
4983
|
if (!op || typeof op.src !== "string" || typeof op.dest !== "string") return;
|
|
4657
4984
|
const src = op.src;
|
|
@@ -4662,8 +4989,8 @@ var movePaths = (docObject, operations, options = { mutable: true }) => {
|
|
|
4662
4989
|
if (src === dest) return;
|
|
4663
4990
|
forEach6(resolveLeafParents(obj, src), ({ parent, leaf }) => {
|
|
4664
4991
|
if (!hasOwn(parent, leaf)) return;
|
|
4665
|
-
const value =
|
|
4666
|
-
|
|
4992
|
+
const value = cloneDeep5(parent[leaf]);
|
|
4993
|
+
set6(parent, dest, value);
|
|
4667
4994
|
if (dest === leaf || dest.startsWith(`${leaf}.`)) return;
|
|
4668
4995
|
delete parent[leaf];
|
|
4669
4996
|
});
|
|
@@ -4674,7 +5001,7 @@ var sliceArrays = (docObject, operations, options = { mutable: true, maxSlice: D
|
|
|
4674
5001
|
const mutable = options.mutable !== false;
|
|
4675
5002
|
const maxSlice = options.maxSlice ?? DEFAULT_MAX_SLICE;
|
|
4676
5003
|
assertNonNegativeSafeInteger(maxSlice, "maxSlice", "sliceArrays");
|
|
4677
|
-
const obj = mutable ? docObject :
|
|
5004
|
+
const obj = mutable ? docObject : cloneDeep5(docObject);
|
|
4678
5005
|
forEach6(Array.isArray(operations) ? operations : [], (op) => {
|
|
4679
5006
|
if (!op || typeof op.src !== "string") return;
|
|
4680
5007
|
const src = op.src;
|
|
@@ -4698,9 +5025,9 @@ var sliceArrays = (docObject, operations, options = { mutable: true, maxSlice: D
|
|
|
4698
5025
|
const end = typeof limit === "undefined" ? void 0 : skip + limit;
|
|
4699
5026
|
const window = leafValue.slice(skip, end);
|
|
4700
5027
|
if (typeof rawDest === "undefined") {
|
|
4701
|
-
|
|
5028
|
+
set6(parent, leaf, window);
|
|
4702
5029
|
} else {
|
|
4703
|
-
|
|
5030
|
+
set6(parent, rawDest, window);
|
|
4704
5031
|
}
|
|
4705
5032
|
});
|
|
4706
5033
|
});
|
|
@@ -4708,7 +5035,7 @@ var sliceArrays = (docObject, operations, options = { mutable: true, maxSlice: D
|
|
|
4708
5035
|
};
|
|
4709
5036
|
var countPaths = (docObject, operations, options = { mutable: true }) => {
|
|
4710
5037
|
const mutable = options.mutable !== false;
|
|
4711
|
-
const obj = mutable ? docObject :
|
|
5038
|
+
const obj = mutable ? docObject : cloneDeep5(docObject);
|
|
4712
5039
|
forEach6(Array.isArray(operations) ? operations : [], (op) => {
|
|
4713
5040
|
if (!op || typeof op.src !== "string" || typeof op.dest !== "string") return;
|
|
4714
5041
|
const src = op.src;
|
|
@@ -4722,21 +5049,21 @@ var countPaths = (docObject, operations, options = { mutable: true }) => {
|
|
|
4722
5049
|
let count;
|
|
4723
5050
|
if (isArray9(leafValue)) {
|
|
4724
5051
|
count = leafValue.length;
|
|
4725
|
-
} else if (
|
|
5052
|
+
} else if (isPlainObject15(leafValue)) {
|
|
4726
5053
|
count = Object.keys(leafValue).length;
|
|
4727
5054
|
} else if (leafValue === null || typeof leafValue === "undefined") {
|
|
4728
5055
|
count = 0;
|
|
4729
5056
|
} else {
|
|
4730
5057
|
count = 1;
|
|
4731
5058
|
}
|
|
4732
|
-
|
|
5059
|
+
set6(parent, dest, count);
|
|
4733
5060
|
});
|
|
4734
5061
|
});
|
|
4735
5062
|
return obj;
|
|
4736
5063
|
};
|
|
4737
5064
|
var maskPaths = (docObject, operations, options = { mutable: true }) => {
|
|
4738
5065
|
const mutable = options.mutable !== false;
|
|
4739
|
-
const obj = mutable ? docObject :
|
|
5066
|
+
const obj = mutable ? docObject : cloneDeep5(docObject);
|
|
4740
5067
|
forEach6(Array.isArray(operations) ? operations : [], (op) => {
|
|
4741
5068
|
if (!op || typeof op.src !== "string") return;
|
|
4742
5069
|
const src = op.src;
|
|
@@ -4745,7 +5072,7 @@ var maskPaths = (docObject, operations, options = { mutable: true }) => {
|
|
|
4745
5072
|
const replacement = Object.prototype.hasOwnProperty.call(op, "replacement") ? op.replacement : DEFAULT_MASK_REPLACEMENT;
|
|
4746
5073
|
forEach6(resolveLeafParents(obj, src), ({ parent, leaf }) => {
|
|
4747
5074
|
if (!hasOwn(parent, leaf)) return;
|
|
4748
|
-
|
|
5075
|
+
set6(parent, leaf, replacement);
|
|
4749
5076
|
});
|
|
4750
5077
|
});
|
|
4751
5078
|
return obj;
|
|
@@ -4798,7 +5125,7 @@ import {
|
|
|
4798
5125
|
isEmpty as isEmpty2,
|
|
4799
5126
|
isEqual,
|
|
4800
5127
|
isFunction as isFunction4,
|
|
4801
|
-
isPlainObject as
|
|
5128
|
+
isPlainObject as isPlainObject16,
|
|
4802
5129
|
isString as isString5
|
|
4803
5130
|
} from "@web-ts-toolkit/utils";
|
|
4804
5131
|
|
|
@@ -4839,10 +5166,10 @@ var permission_default = Permission;
|
|
|
4839
5166
|
|
|
4840
5167
|
// src/core-shared.ts
|
|
4841
5168
|
function isAndFilter(filter2) {
|
|
4842
|
-
return !!filter2 &&
|
|
5169
|
+
return !!filter2 && isPlainObject16(filter2) && Object.keys(filter2).length === 1 && isArray10(filter2.$and);
|
|
4843
5170
|
}
|
|
4844
5171
|
function isMergeableClause(filter2) {
|
|
4845
|
-
return !!filter2 &&
|
|
5172
|
+
return !!filter2 && isPlainObject16(filter2) && Object.keys(filter2).every((key) => !key.startsWith("$"));
|
|
4846
5173
|
}
|
|
4847
5174
|
function optimizeAndFilter(clausesInput) {
|
|
4848
5175
|
const clauses = [];
|
|
@@ -4860,7 +5187,7 @@ function optimizeAndFilter(clausesInput) {
|
|
|
4860
5187
|
if (isArray10(value)) {
|
|
4861
5188
|
return `[${value.map(dedupeKey).join(",")}]`;
|
|
4862
5189
|
}
|
|
4863
|
-
if (
|
|
5190
|
+
if (isPlainObject16(value)) {
|
|
4864
5191
|
return `{${Object.keys(value).sort().map((key) => `${key}:${dedupeKey(value[key])}`).join(",")}}`;
|
|
4865
5192
|
}
|
|
4866
5193
|
return JSON.stringify(value);
|
|
@@ -4902,7 +5229,7 @@ function optimizeAndFilter(clausesInput) {
|
|
|
4902
5229
|
}
|
|
4903
5230
|
function normalizeFilter(filter2) {
|
|
4904
5231
|
if (filter2 === false) return false;
|
|
4905
|
-
if (!filter2 || !
|
|
5232
|
+
if (!filter2 || !isPlainObject16(filter2) || isEmpty2(filter2)) return null;
|
|
4906
5233
|
if (!isAndFilter(filter2)) {
|
|
4907
5234
|
return filter2;
|
|
4908
5235
|
}
|
|
@@ -4969,7 +5296,7 @@ async function setRequestPermissions(req) {
|
|
|
4969
5296
|
const globalPermissions = getGlobalOption("globalPermissions");
|
|
4970
5297
|
if (!isFunction4(globalPermissions)) return;
|
|
4971
5298
|
const permissions = await globalPermissions.call(req, req);
|
|
4972
|
-
if (
|
|
5299
|
+
if (isPlainObject16(permissions)) req[requestPermissionField] = permissions;
|
|
4973
5300
|
else if (isArray10(permissions)) req[requestPermissionField] = arrayToRecord(permissions);
|
|
4974
5301
|
else if (isString5(permissions)) req[requestPermissionField] = { [permissions]: true };
|
|
4975
5302
|
owners.set(requestPermissionField, activeRuntime);
|
|
@@ -5189,6 +5516,7 @@ async function resolveSelectForRequest({
|
|
|
5189
5516
|
}
|
|
5190
5517
|
|
|
5191
5518
|
// src/core.ts
|
|
5519
|
+
var emptyPermissionLeaf = () => ({ $: "_" });
|
|
5192
5520
|
var Core = class {
|
|
5193
5521
|
constructor(req) {
|
|
5194
5522
|
this.cachedPermissions = null;
|
|
@@ -5392,7 +5720,7 @@ var Core = class {
|
|
|
5392
5720
|
}
|
|
5393
5721
|
addEmptyPermissions(modelName, doc) {
|
|
5394
5722
|
const docPermissionField = getModelOption(modelName, "documentPermissionField");
|
|
5395
|
-
setDocValue(doc, docPermissionField, { _view:
|
|
5723
|
+
setDocValue(doc, docPermissionField, { _view: emptyPermissionLeaf(), _edit: emptyPermissionLeaf() });
|
|
5396
5724
|
return doc;
|
|
5397
5725
|
}
|
|
5398
5726
|
async addDocPermissions(modelName, doc, access, context) {
|
|
@@ -5442,8 +5770,8 @@ var Core = class {
|
|
|
5442
5770
|
},
|
|
5443
5771
|
{}
|
|
5444
5772
|
);
|
|
5445
|
-
setDocValue(doc, `${docPermissionField}._view`, viewObj);
|
|
5446
|
-
setDocValue(doc, `${docPermissionField}._edit`, editObj);
|
|
5773
|
+
setDocValue(doc, `${docPermissionField}._view`, Object.keys(viewObj).length > 0 ? viewObj : emptyPermissionLeaf());
|
|
5774
|
+
setDocValue(doc, `${docPermissionField}._edit`, Object.keys(editObj).length > 0 ? editObj : emptyPermissionLeaf());
|
|
5447
5775
|
return doc;
|
|
5448
5776
|
}
|
|
5449
5777
|
async decorate(modelName, doc, access, context) {
|
|
@@ -5582,12 +5910,12 @@ function guard(condition) {
|
|
|
5582
5910
|
const permissions = req[PERMISSIONS];
|
|
5583
5911
|
let cond = condition;
|
|
5584
5912
|
let phas = (key) => permissions.has(key);
|
|
5585
|
-
if (
|
|
5913
|
+
if (isPlainObject17(condition)) {
|
|
5586
5914
|
const { modelName, id, condition: _cond } = condition;
|
|
5587
5915
|
const svc = req.macl.getPublicService(modelName);
|
|
5588
5916
|
const select = getModelOption(modelName, `alwaysSelectFields.read`, void 0);
|
|
5589
5917
|
let _id = isString7(id) ? id : null;
|
|
5590
|
-
if (
|
|
5918
|
+
if (isPlainObject17(id)) {
|
|
5591
5919
|
const { type = "param", key } = id;
|
|
5592
5920
|
if (type === "param") {
|
|
5593
5921
|
const paramValue = req.params[key];
|
|
@@ -5629,7 +5957,7 @@ import express2 from "express";
|
|
|
5629
5957
|
|
|
5630
5958
|
// src/routers/model-router.ts
|
|
5631
5959
|
import JsonRouter5 from "@web-ts-toolkit/express-json-router";
|
|
5632
|
-
import { forEach as forEach8, isPlainObject as
|
|
5960
|
+
import { forEach as forEach8, isPlainObject as isPlainObject19, isString as isString8, isUndefined as isUndefined2, normalizeUrlPath as normalizeUrlPath3, padEnd } from "@web-ts-toolkit/utils";
|
|
5633
5961
|
|
|
5634
5962
|
// src/routers/router-mutation.ts
|
|
5635
5963
|
var MODEL_BUILD_TIME_OPTION_KEYS = /* @__PURE__ */ new Set([
|
|
@@ -5758,7 +6086,7 @@ import JsonRouter4 from "@web-ts-toolkit/express-json-router";
|
|
|
5758
6086
|
|
|
5759
6087
|
// src/validation/common.ts
|
|
5760
6088
|
import { z as z2 } from "zod";
|
|
5761
|
-
import { isPlainObject as
|
|
6089
|
+
import { isPlainObject as isPlainObject18 } from "@web-ts-toolkit/utils";
|
|
5762
6090
|
var stringOrStringArray = z2.union([z2.string(), z2.array(z2.string())]);
|
|
5763
6091
|
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);
|
|
5764
6092
|
var queryBooleanString = z2.enum(["true", "false"]);
|
|
@@ -5858,7 +6186,7 @@ var correlatedFilterSchema = z2.record(z2.string(), z2.unknown()).superRefine((f
|
|
|
5858
6186
|
}
|
|
5859
6187
|
});
|
|
5860
6188
|
var correlatedEmptyOptionsSchema = z2.unknown().refine(
|
|
5861
|
-
(value) => value === void 0 ||
|
|
6189
|
+
(value) => value === void 0 || isPlainObject18(value) && Object.keys(value).length === 0,
|
|
5862
6190
|
"Correlated include options must be absent or empty"
|
|
5863
6191
|
).optional();
|
|
5864
6192
|
var correlatedPathSchema = z2.string().min(1).superRefine((path, ctx) => {
|
|
@@ -5895,7 +6223,7 @@ var correlatedListArgsSchema = z2.object({
|
|
|
5895
6223
|
include: z2.lazy(() => correlatedNestedIncludeSchema).optional()
|
|
5896
6224
|
}).strict().superRefine((args, ctx) => checkArgsMarkers(args.select, args.sort, ctx));
|
|
5897
6225
|
var correlatedCountArgsSchema = z2.unknown().refine(
|
|
5898
|
-
(value) => value === void 0 ||
|
|
6226
|
+
(value) => value === void 0 || isPlainObject18(value) && Object.keys(value).length === 0,
|
|
5899
6227
|
"Count includes accept no args"
|
|
5900
6228
|
).optional();
|
|
5901
6229
|
var correlatedReadByIdSchema = z2.object({
|
|
@@ -5945,7 +6273,7 @@ var correlatedNestedIncludeSchema = z2.union([includeItemSchema, z2.array(includ
|
|
|
5945
6273
|
var rejectDuplicateCorrelatedPaths = (items, ctx) => {
|
|
5946
6274
|
const seen = /* @__PURE__ */ new Set();
|
|
5947
6275
|
items.forEach((item, index) => {
|
|
5948
|
-
if (
|
|
6276
|
+
if (isPlainObject18(item) && item.mode === "correlated" && typeof item.path === "string") {
|
|
5949
6277
|
if (seen.has(item.path)) {
|
|
5950
6278
|
ctx.addIssue({
|
|
5951
6279
|
code: z2.ZodIssueCode.custom,
|
|
@@ -6144,19 +6472,12 @@ function fromJoi(schema) {
|
|
|
6144
6472
|
}
|
|
6145
6473
|
function fromAjv(validator) {
|
|
6146
6474
|
return async (value) => {
|
|
6147
|
-
|
|
6148
|
-
if (isThenable(outcome)) {
|
|
6475
|
+
if (validator.$async === true) {
|
|
6149
6476
|
try {
|
|
6150
|
-
const data = await
|
|
6151
|
-
if (data === false) {
|
|
6152
|
-
return {
|
|
6153
|
-
success: false,
|
|
6154
|
-
issues: normalizeAjvIssues(validator.errors ?? [])
|
|
6155
|
-
};
|
|
6156
|
-
}
|
|
6477
|
+
const data = await validator(value);
|
|
6157
6478
|
return {
|
|
6158
6479
|
success: true,
|
|
6159
|
-
data
|
|
6480
|
+
data
|
|
6160
6481
|
};
|
|
6161
6482
|
} catch (error2) {
|
|
6162
6483
|
if (!isAjvValidationError(error2)) {
|
|
@@ -6164,11 +6485,21 @@ function fromAjv(validator) {
|
|
|
6164
6485
|
}
|
|
6165
6486
|
return {
|
|
6166
6487
|
success: false,
|
|
6167
|
-
issues: normalizeAjvIssues(error2.errors
|
|
6488
|
+
issues: normalizeAjvIssues(error2.errors)
|
|
6168
6489
|
};
|
|
6169
6490
|
}
|
|
6170
6491
|
}
|
|
6171
|
-
const
|
|
6492
|
+
const outcome = validator(value);
|
|
6493
|
+
if (isThenable(outcome)) {
|
|
6494
|
+
void Promise.resolve(outcome).catch(() => {
|
|
6495
|
+
});
|
|
6496
|
+
throw new TypeError(
|
|
6497
|
+
"fromAjv: promise/thenable validators must declare $async: true and resolve with validated data"
|
|
6498
|
+
);
|
|
6499
|
+
}
|
|
6500
|
+
if (typeof outcome !== "boolean") {
|
|
6501
|
+
throw new TypeError("fromAjv: synchronous validators must return a boolean verdict");
|
|
6502
|
+
}
|
|
6172
6503
|
if (outcome) {
|
|
6173
6504
|
return {
|
|
6174
6505
|
success: true,
|
|
@@ -6177,7 +6508,9 @@ function fromAjv(validator) {
|
|
|
6177
6508
|
}
|
|
6178
6509
|
return {
|
|
6179
6510
|
success: false,
|
|
6180
|
-
|
|
6511
|
+
// Normalize immediately, copying message/path before another call can
|
|
6512
|
+
// replace the errors array or mutate its entries.
|
|
6513
|
+
issues: normalizeAjvIssues(validator.errors ?? [])
|
|
6181
6514
|
};
|
|
6182
6515
|
};
|
|
6183
6516
|
}
|
|
@@ -6407,14 +6740,14 @@ function isArkTypeErrors(value) {
|
|
|
6407
6740
|
return record.arkKind === "errors" || record[" arkKind"] === "errors";
|
|
6408
6741
|
}
|
|
6409
6742
|
function isThenable(value) {
|
|
6410
|
-
return typeof value === "object" && value !== null && "then" in value && typeof value.then === "function";
|
|
6743
|
+
return (typeof value === "object" || typeof value === "function") && value !== null && "then" in value && typeof value.then === "function";
|
|
6411
6744
|
}
|
|
6412
6745
|
function isAjvValidationError(error2) {
|
|
6413
6746
|
if (typeof error2 !== "object" || error2 === null || !("errors" in error2)) {
|
|
6414
6747
|
return false;
|
|
6415
6748
|
}
|
|
6416
|
-
const
|
|
6417
|
-
return
|
|
6749
|
+
const candidate = error2;
|
|
6750
|
+
return candidate.ajv === true && candidate.validation === true && Array.isArray(candidate.errors);
|
|
6418
6751
|
}
|
|
6419
6752
|
function isVineValidationError(error2) {
|
|
6420
6753
|
if (typeof error2 !== "object" || error2 === null || !("messages" in error2)) {
|
|
@@ -7507,11 +7840,11 @@ function setModelSubDocumentRoutes(context) {
|
|
|
7507
7840
|
}
|
|
7508
7841
|
|
|
7509
7842
|
// src/openapi/route-registration.ts
|
|
7510
|
-
import { normalizeUrlPath } from "@web-ts-toolkit/utils";
|
|
7843
|
+
import { normalizeUrlPath as normalizeUrlPath2 } from "@web-ts-toolkit/utils";
|
|
7511
7844
|
function registerOpenApiRoute(runtime, basePath, defaultTag, route) {
|
|
7512
7845
|
runtime.registerOpenApiRoute({
|
|
7513
7846
|
...route,
|
|
7514
|
-
path:
|
|
7847
|
+
path: normalizeUrlPath2(basePath + route.path),
|
|
7515
7848
|
tags: route.tags ?? [defaultTag]
|
|
7516
7849
|
});
|
|
7517
7850
|
}
|
|
@@ -7682,7 +8015,7 @@ var ModelRouter = class _ModelRouter {
|
|
|
7682
8015
|
this.runtime = runtime;
|
|
7683
8016
|
this.runtime.setModelOptions(modelName, initialOptions);
|
|
7684
8017
|
this.options = this.runtime.getModelOptions(modelName);
|
|
7685
|
-
this.fullBasePath =
|
|
8018
|
+
this.fullBasePath = normalizeUrlPath3(this.options.parentPath + this.options.basePath);
|
|
7686
8019
|
this.modelName = modelName;
|
|
7687
8020
|
this.router = new JsonRouter5(this.options.basePath, createSetCore(this.runtime), accessRouterResponseHandler);
|
|
7688
8021
|
this.model = new model_default(modelName, this.runtime);
|
|
@@ -7763,7 +8096,7 @@ var ModelRouter = class _ModelRouter {
|
|
|
7763
8096
|
logEndpoints() {
|
|
7764
8097
|
runWithRuntime(this.runtime, () => {
|
|
7765
8098
|
forEach8(this.router.getEndpoints(), ({ method, path }) => {
|
|
7766
|
-
logInfoMessage(`${padEnd(method, 6)} ${
|
|
8099
|
+
logInfoMessage(`${padEnd(method, 6)} ${normalizeUrlPath3(this.options.parentPath + path)}`);
|
|
7767
8100
|
});
|
|
7768
8101
|
});
|
|
7769
8102
|
}
|
|
@@ -7776,7 +8109,7 @@ var ModelRouter = class _ModelRouter {
|
|
|
7776
8109
|
value
|
|
7777
8110
|
);
|
|
7778
8111
|
}
|
|
7779
|
-
if (arguments.length === 1 &&
|
|
8112
|
+
if (arguments.length === 1 && isPlainObject19(keyOrOptions)) {
|
|
7780
8113
|
assertMutableRouterOptions("model", keyOrOptions);
|
|
7781
8114
|
this.runtime.setModelOptions(this.modelName, keyOrOptions);
|
|
7782
8115
|
}
|
|
@@ -7799,7 +8132,7 @@ var ModelRouter = class _ModelRouter {
|
|
|
7799
8132
|
|
|
7800
8133
|
// src/routers/root-router.ts
|
|
7801
8134
|
import JsonRouter6 from "@web-ts-toolkit/express-json-router";
|
|
7802
|
-
import { isNumber as _isNumber, normalizeUrlPath as
|
|
8135
|
+
import { isNumber as _isNumber, normalizeUrlPath as normalizeUrlPath4, orderBy as _orderBy } from "@web-ts-toolkit/utils";
|
|
7803
8136
|
|
|
7804
8137
|
// src/core-data.ts
|
|
7805
8138
|
var DataCore = class {
|
|
@@ -8065,7 +8398,7 @@ var RootRouter = class {
|
|
|
8065
8398
|
this.setRoutes();
|
|
8066
8399
|
}
|
|
8067
8400
|
registerOpenApiRoute(route) {
|
|
8068
|
-
registerOpenApiRoute(this.runtime,
|
|
8401
|
+
registerOpenApiRoute(this.runtime, normalizeUrlPath4(this.basename), "root", route);
|
|
8069
8402
|
}
|
|
8070
8403
|
wrapResult(index, item, result) {
|
|
8071
8404
|
return {
|
|
@@ -8215,7 +8548,7 @@ var RootRouter = class {
|
|
|
8215
8548
|
|
|
8216
8549
|
// src/routers/data-router.ts
|
|
8217
8550
|
import JsonRouter7 from "@web-ts-toolkit/express-json-router";
|
|
8218
|
-
import { isPlainObject as
|
|
8551
|
+
import { isPlainObject as isPlainObject20, isString as isString9, isUndefined as isUndefined3, normalizeUrlPath as normalizeUrlPath5 } from "@web-ts-toolkit/utils";
|
|
8219
8552
|
var clientErrors4 = JsonRouter7.clientErrors;
|
|
8220
8553
|
function setOption2(parentKey, optionKey, option) {
|
|
8221
8554
|
const key = isUndefined3(option) ? parentKey : `${parentKey}.${optionKey}`;
|
|
@@ -8260,7 +8593,7 @@ var DataRouter = class {
|
|
|
8260
8593
|
this.runtime = runtime;
|
|
8261
8594
|
this.runtime.setDataOptions(dataName, initialOptions);
|
|
8262
8595
|
this.options = this.runtime.getDataOptions(dataName);
|
|
8263
|
-
this.fullBasePath =
|
|
8596
|
+
this.fullBasePath = normalizeUrlPath5(this.options.parentPath + this.options.basePath);
|
|
8264
8597
|
this.dataName = dataName;
|
|
8265
8598
|
this.router = new JsonRouter7(this.options.basePath, createSetDataCore(this.runtime), accessRouterResponseHandler);
|
|
8266
8599
|
this.setCollectionRoutes();
|
|
@@ -8416,7 +8749,7 @@ var DataRouter = class {
|
|
|
8416
8749
|
assertMutableRouterOption("data", keyOrOptions);
|
|
8417
8750
|
this.runtime.setDataOption(this.dataName, keyOrOptions, value);
|
|
8418
8751
|
}
|
|
8419
|
-
if (arguments.length === 1 &&
|
|
8752
|
+
if (arguments.length === 1 && isPlainObject20(keyOrOptions)) {
|
|
8420
8753
|
assertMutableRouterOptions("data", keyOrOptions);
|
|
8421
8754
|
this.runtime.setDataOptions(this.dataName, keyOrOptions);
|
|
8422
8755
|
}
|
|
@@ -8563,7 +8896,7 @@ function createRuntimeApi(runtime) {
|
|
|
8563
8896
|
if (arguments.length === 2 && isString10(keyOrOptions)) {
|
|
8564
8897
|
return runtime.setGlobalOption(keyOrOptions, value);
|
|
8565
8898
|
}
|
|
8566
|
-
if (arguments.length === 1 &&
|
|
8899
|
+
if (arguments.length === 1 && isPlainObject21(keyOrOptions)) {
|
|
8567
8900
|
return runtime.setGlobalOptions(keyOrOptions);
|
|
8568
8901
|
}
|
|
8569
8902
|
};
|