@web-ts-toolkit/access-router 0.45.0 → 0.46.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -77,14 +77,14 @@ __export(index_exports, {
77
77
  setModelOptions: () => setModelOptions
78
78
  });
79
79
  module.exports = __toCommonJS(index_exports);
80
- var import_utils30 = require("@web-ts-toolkit/utils");
80
+ var import_utils32 = require("@web-ts-toolkit/utils");
81
81
 
82
82
  // src/middleware.ts
83
83
  var import_express_json_router2 = __toESM(require("@web-ts-toolkit/express-json-router"));
84
- var import_utils23 = require("@web-ts-toolkit/utils");
84
+ var import_utils25 = require("@web-ts-toolkit/utils");
85
85
 
86
86
  // src/core.ts
87
- var import_utils22 = require("@web-ts-toolkit/utils");
87
+ var import_utils24 = require("@web-ts-toolkit/utils");
88
88
 
89
89
  // src/runtime.ts
90
90
  var import_mongoose3 = __toESM(require("mongoose"));
@@ -122,8 +122,12 @@ var mapWithConcurrencyLimit = async (items, limit, iteratee) => {
122
122
  };
123
123
  var RequestConcurrencyScheduler = class {
124
124
  constructor(limit) {
125
+ this.activeWork = 0;
126
+ this.waiters = [];
125
127
  this.limit = Math.max(1, limit);
126
128
  }
129
+ // Each map bounds its own orchestration workers. Recursive maps never hold
130
+ // shared work permits while awaiting descendants (including at limit 1).
127
131
  async map(items, iteratee) {
128
132
  return mapWithConcurrencyLimit(items, this.limit, (item, index) => iteratee(item, index, true));
129
133
  }
@@ -131,6 +135,23 @@ var RequestConcurrencyScheduler = class {
131
135
  const [result] = await this.map([void 0], () => iteratee(true));
132
136
  return result;
133
137
  }
138
+ // Only leaf persistence operations belong here, not service/hook callbacks.
139
+ // All work calls on this scheduler share a FIFO admission ceiling.
140
+ async work(operation) {
141
+ if (this.activeWork >= this.limit) {
142
+ await new Promise((resolve) => this.waiters.push(resolve));
143
+ } else {
144
+ this.activeWork += 1;
145
+ }
146
+ try {
147
+ return await operation();
148
+ } finally {
149
+ const next = this.waiters.shift();
150
+ if (next)
151
+ next();
152
+ else this.activeWork -= 1;
153
+ }
154
+ }
134
155
  };
135
156
 
136
157
  // src/helpers/document.ts
@@ -1222,6 +1243,7 @@ var AccessRuntime = class {
1222
1243
  this.modelAtts = {};
1223
1244
  this.modelInstances = {};
1224
1245
  this.openApiRegistry = new OpenApiRegistry();
1246
+ this.openApiPathPrefix = "";
1225
1247
  this.allowGlobalModelLookup = options.allowGlobalModelLookup ?? false;
1226
1248
  ensureMongooseJsonSchemaInitialized();
1227
1249
  }
@@ -1250,7 +1272,24 @@ var AccessRuntime = class {
1250
1272
  return global;
1251
1273
  }
1252
1274
  registerOpenApiRoute(route) {
1253
- this.openApiRegistry.register(route);
1275
+ this.openApiRegistry.register(
1276
+ this.openApiPathPrefix ? { ...route, path: (0, import_utils11.normalizeUrlPath)(`${this.openApiPathPrefix}/${route.path}`) } : route
1277
+ );
1278
+ }
1279
+ /**
1280
+ * Compose OpenAPI paths while synchronously constructing mounted routers.
1281
+ * Prefixes apply before collision checks; prior descriptors and Express paths
1282
+ * are unchanged. Nested calls compose and the prior scope is restored even on
1283
+ * failure. The callback must finish registration synchronously (no async work).
1284
+ */
1285
+ withOpenApiPathPrefix(prefix, build) {
1286
+ const previous = this.openApiPathPrefix;
1287
+ this.openApiPathPrefix = (0, import_utils11.normalizeUrlPath)(`${previous}/${prefix}`);
1288
+ try {
1289
+ return build();
1290
+ } finally {
1291
+ this.openApiPathPrefix = previous;
1292
+ }
1254
1293
  }
1255
1294
  /**
1256
1295
  * Enable strict OpenAPI collision detection for the remainder of this
@@ -1925,7 +1964,8 @@ function resolveValueNode(template, parent, bare) {
1925
1964
  if (!isParentRefMarker(inner) || typeof inner.$parent !== "string" || (inner.$parent?.length ?? 0) === 0) {
1926
1965
  throw new CorrelatedReferenceError('Invalid $escape payload: expected { "$escape": { "$parent": "<field>" } }');
1927
1966
  }
1928
- return literalParentRefObject(inner.$parent);
1967
+ const literal = literalParentRefObject(inner.$parent);
1968
+ return bare ? { $eq: literal } : literal;
1929
1969
  }
1930
1970
  if (isParentRefMarker(template)) {
1931
1971
  const path = template.$parent;
@@ -2054,8 +2094,28 @@ function isCorrelatedInclude(value) {
2054
2094
  return (0, import_utils12.isPlainObject)(value) && value.mode === "correlated";
2055
2095
  }
2056
2096
 
2097
+ // src/helpers/request-work.ts
2098
+ var states = /* @__PURE__ */ new WeakMap();
2099
+ function getRequestWorkState(req, runtime) {
2100
+ let owners = states.get(req);
2101
+ if (!owners) {
2102
+ owners = /* @__PURE__ */ new WeakMap();
2103
+ states.set(req, owners);
2104
+ }
2105
+ let state = owners.get(runtime);
2106
+ if (!state) {
2107
+ state = {
2108
+ totalQueries: 0,
2109
+ scheduler: new RequestConcurrencyScheduler(
2110
+ resolveRequestComplexity(runtime.getGlobalOption("requestComplexity")).maxBulkConcurrency
2111
+ )
2112
+ };
2113
+ owners.set(runtime, state);
2114
+ }
2115
+ return state;
2116
+ }
2117
+
2057
2118
  // src/services/base.ts
2058
- var correlatedExecStates = /* @__PURE__ */ new WeakMap();
2059
2119
  function getCorrelatedTemplateDepth(includes) {
2060
2120
  let max = 0;
2061
2121
  const visit = (entries, depth) => {
@@ -2105,6 +2165,7 @@ function validateClientFilter(filter2) {
2105
2165
  }
2106
2166
  var Base = class {
2107
2167
  constructor(req, modelName) {
2168
+ this.requestRuntime = getActiveRuntime() ?? defaultRuntime;
2108
2169
  this.req = req;
2109
2170
  this.modelName = modelName;
2110
2171
  }
@@ -2242,6 +2303,7 @@ var Base = class {
2242
2303
  }
2243
2304
  seenCorrelatedPaths.add(entry.path);
2244
2305
  }
2306
+ this.assertIncludeOutputPaths([...legacy, ...correlated]);
2245
2307
  let includeLocalFields = [];
2246
2308
  let includePaths = [];
2247
2309
  (0, import_utils13.forEach)(legacy, (inc) => {
@@ -2258,6 +2320,39 @@ var Base = class {
2258
2320
  correlatedReferenceFields: (0, import_utils13.uniq)((0, import_utils13.compact)(correlatedReferenceFields))
2259
2321
  };
2260
2322
  }
2323
+ validateIncludeOutputPath(path, modelName) {
2324
+ const permissionField = getModelOption(modelName, "documentPermissionField");
2325
+ if ((0, import_utils13.get)((0, import_utils13.set)({}, path, true), permissionField) !== void 0 || (0, import_utils13.get)((0, import_utils13.set)({}, permissionField, true), path) !== void 0) {
2326
+ this.throwClientRequestError(
2327
+ "bad_request" /* BadRequest */,
2328
+ `Include output path ${path} overlaps document permission field ${permissionField} on model ${modelName}`
2329
+ );
2330
+ }
2331
+ }
2332
+ assertIncludeOutputPaths(include) {
2333
+ const pending = [{ entries: include, modelName: this.modelName }];
2334
+ const visited = /* @__PURE__ */ new WeakMap();
2335
+ while (pending.length > 0) {
2336
+ const { entries, modelName } = pending.pop();
2337
+ if (modelName !== this.modelName && !getModelNames().includes(modelName)) {
2338
+ this.throwClientRequestError("bad_request" /* BadRequest */, `Model ${modelName} not found`);
2339
+ }
2340
+ for (const entry of entries) {
2341
+ if (!(0, import_utils13.isPlainObject)(entry)) continue;
2342
+ const models = visited.get(entry) ?? /* @__PURE__ */ new Set();
2343
+ if (models.has(modelName)) continue;
2344
+ models.add(modelName);
2345
+ visited.set(entry, models);
2346
+ if (typeof entry.path === "string" && entry.path) {
2347
+ this.validateIncludeOutputPath(entry.path, modelName);
2348
+ }
2349
+ if ((entry.op === "read" || entry.op === "list") && typeof entry.model === "string") {
2350
+ const nested = entry.args?.include;
2351
+ if (nested) pending.push({ entries: (0, import_utils13.compact)((0, import_utils13.castArray)(nested)), modelName: entry.model });
2352
+ }
2353
+ }
2354
+ }
2355
+ }
2261
2356
  getForeignKeyValues(value) {
2262
2357
  return (0, import_utils13.flatten)((0, import_utils13.castArray)(value));
2263
2358
  }
@@ -2392,6 +2487,7 @@ var Base = class {
2392
2487
  const svc = await this.getAuthorizedTargetService(model, "count");
2393
2488
  const localValues = docs.flatMap((doc) => this.getForeignKeyValues((0, import_utils13.get)(doc, localField)));
2394
2489
  const result = await svc.countByFieldValues(foreignField, localValues, _filters ?? {}, "count");
2490
+ if (!result.success && result.code === "bad_request" /* BadRequest */) throw new ClientRequestError(result);
2395
2491
  if (!result.success) return docs;
2396
2492
  for (let y = 0; y < docs.length; y++) {
2397
2493
  const doc = docs[y];
@@ -2400,16 +2496,7 @@ var Base = class {
2400
2496
  return docs;
2401
2497
  }
2402
2498
  getCorrelatedExecState() {
2403
- const key = this.req;
2404
- let state = correlatedExecStates.get(key);
2405
- if (!state) {
2406
- state = {
2407
- totalQueries: 0,
2408
- scheduler: new RequestConcurrencyScheduler(this.getRequestComplexity().maxBulkConcurrency)
2409
- };
2410
- correlatedExecStates.set(key, state);
2411
- }
2412
- return state;
2499
+ return getRequestWorkState(this.req, this.requestRuntime);
2413
2500
  }
2414
2501
  claimCorrelatedQuerySlot() {
2415
2502
  const limits = this.getRequestComplexity();
@@ -2806,10 +2893,30 @@ var Base = class {
2806
2893
  };
2807
2894
 
2808
2895
  // src/services/service.ts
2809
- var import_utils15 = require("@web-ts-toolkit/utils");
2896
+ var import_utils17 = require("@web-ts-toolkit/utils");
2810
2897
  var import_just_diff = require("just-diff");
2811
2898
 
2812
2899
  // src/model.ts
2900
+ var import_utils14 = require("@web-ts-toolkit/utils");
2901
+ function admitModelPersistence(adapter, scheduler) {
2902
+ return {
2903
+ modelName: adapter.modelName,
2904
+ mongooseModel: adapter.mongooseModel,
2905
+ new: () => adapter.new(),
2906
+ // Mongoose create(array) otherwise starts all document saves at once. Keep
2907
+ // the adapter's array input/output contract, with one permit per item; all
2908
+ // items are submitted even if one rejects, as with unordered create(array).
2909
+ create: (data) => Array.isArray(data) ? Promise.all(data.map((item) => scheduler.work(async () => (await adapter.create([item]))[0]))) : scheduler.work(() => adapter.create(data)),
2910
+ find: (props) => scheduler.work(() => adapter.find(props)),
2911
+ findOne: (props) => scheduler.work(() => adapter.findOne(props)),
2912
+ exists: (filter2) => scheduler.work(() => adapter.exists(filter2)),
2913
+ countDocuments: (filter2) => scheduler.work(() => adapter.countDocuments(filter2)),
2914
+ distinct: (field, conditions) => scheduler.work(() => adapter.distinct(field, conditions)),
2915
+ aggregate: (pipeline) => scheduler.work(() => adapter.aggregate(pipeline)),
2916
+ // Casting builds no persistence work and must not acquire a recursive permit.
2917
+ ...adapter.castFilter ? { castFilter: (filter2) => adapter.castFilter(filter2) } : {}
2918
+ };
2919
+ }
2813
2920
  var Model = class {
2814
2921
  constructor(modelName, runtime) {
2815
2922
  this.modelName = modelName;
@@ -2862,6 +2969,11 @@ var Model = class {
2862
2969
  countDocuments(filter2 = {}) {
2863
2970
  return this.mongooseModel.countDocuments(filter2);
2864
2971
  }
2972
+ /** Cast a copy using this model's query schema/setters, without executing query middleware or persistence. */
2973
+ castFilter(filter2) {
2974
+ const query = this.mongooseModel.countDocuments((0, import_utils14.cloneDeep)(filter2));
2975
+ return query.cast(this.mongooseModel);
2976
+ }
2865
2977
  // see https://mongoosejs.com/docs/api.html#model_Model.distinct
2866
2978
  distinct(field, conditions = {}) {
2867
2979
  return this.mongooseModel.distinct(field, conditions);
@@ -2872,6 +2984,39 @@ var Model = class {
2872
2984
  };
2873
2985
  var model_default = Model;
2874
2986
 
2987
+ // src/helpers/apply-update.ts
2988
+ var import_utils15 = require("@web-ts-toolkit/utils");
2989
+ function applyUpdate(target, data, allowedFields) {
2990
+ const wholeFields = /* @__PURE__ */ new Set();
2991
+ const partialContainers = /* @__PURE__ */ new Set();
2992
+ const collectBoundary = (shape, prefix = "") => {
2993
+ for (const [key, value] of Object.entries(shape)) {
2994
+ const path = prefix ? `${prefix}.${key}` : key;
2995
+ if (value === true) wholeFields.add(path);
2996
+ else {
2997
+ partialContainers.add(path);
2998
+ collectBoundary(value, path);
2999
+ }
3000
+ }
3001
+ };
3002
+ for (const field of allowedFields) {
3003
+ collectBoundary((0, import_utils15.set)({}, field, true));
3004
+ }
3005
+ const apply = (values, prefix = "") => {
3006
+ for (const [key, value] of Object.entries(values)) {
3007
+ const path = prefix ? `${prefix}.${key}` : key;
3008
+ if (partialContainers.has(path) && !wholeFields.has(path) && ((0, import_utils15.isPlainObject)(value) || Array.isArray(value))) {
3009
+ apply(value, path);
3010
+ } else if (isDocument2(target)) {
3011
+ target.set(path, value);
3012
+ } else {
3013
+ (0, import_utils15.set)(target, path, value);
3014
+ }
3015
+ }
3016
+ };
3017
+ apply(data ?? {});
3018
+ }
3019
+
2875
3020
  // src/logger-helpers.ts
2876
3021
  var SENSITIVE_FILTER_KEYS = /* @__PURE__ */ new Set([
2877
3022
  "password",
@@ -3050,28 +3195,52 @@ var error = (error2) => {
3050
3195
  };
3051
3196
 
3052
3197
  // src/services/model-subdocument-service.ts
3053
- var import_utils14 = require("@web-ts-toolkit/utils");
3198
+ var import_utils16 = require("@web-ts-toolkit/utils");
3199
+ async function visibleMutationRows(service, isAllowed, parentId, sub, rows, response) {
3200
+ const accesses = ["read", `subs.${sub}.read`];
3201
+ if (response === "create") accesses.push(`subs.${sub}.list`);
3202
+ const allowed = await Promise.all(accesses.map(isAllowed));
3203
+ if (allowed.some((value) => !value)) return [];
3204
+ const parentFilter = await service.genFilter("read", { _id: parentId });
3205
+ if (parentFilter === false) return [];
3206
+ const parent = await service.findRawParentDoc({
3207
+ filter: { $and: [{ _id: parentId }, parentFilter] },
3208
+ select: "_id",
3209
+ populate: [],
3210
+ lean: true
3211
+ });
3212
+ if (!parent) return [];
3213
+ const targetFilter = response === "create" ? {} : response === "single" ? { _id: rows[0]?._id } : { _id: { $in: rows.map((row) => row._id) } };
3214
+ const [readFilter, listFilter, select] = await Promise.all([
3215
+ service.genFilter(`subs.${sub}.read`, targetFilter),
3216
+ response === "create" ? service.genFilter(`subs.${sub}.list`) : Promise.resolve({}),
3217
+ service.genQuerySelect("read", null, false, [sub, "sub"])
3218
+ ]);
3219
+ if (readFilter === false || listFilter === false) return [];
3220
+ const visible = filterCollection(filterCollection(rows, listFilter), readFilter);
3221
+ return select ? visible.map((row) => (0, import_utils16.pick)(toObject(row), select.concat("_id"))) : visible;
3222
+ }
3054
3223
  async function listSub(service, id, sub, options) {
3055
3224
  const { filter: ft, select } = options ?? {};
3056
3225
  const filterErrors = validateClientFilter(ft);
3057
3226
  if (filterErrors.length > 0) return { success: false, kind: "error", code: "bad_request" /* BadRequest */, errors: filterErrors };
3058
3227
  const parentDoc = await getParentDoc(service, id, sub, null, { access: "read" });
3059
3228
  if (!parentDoc) return { success: false, kind: "error", code: "not_found" /* NotFound */ };
3060
- let result = (0, import_utils14.get)(parentDoc, sub);
3229
+ let result = (0, import_utils16.get)(parentDoc, sub);
3061
3230
  const [subFilter, subSelect] = await Promise.all([
3062
3231
  service.genFilter(`subs.${sub}.list`, ft),
3063
3232
  service.genQuerySelect("list", select, false, [sub, "sub"])
3064
3233
  ]);
3065
3234
  if (subFilter === false) return { success: false, kind: "error", code: "forbidden" /* Forbidden */ };
3066
3235
  result = filterCollection(result, subFilter);
3067
- if (subSelect) result = result.map((v) => (0, import_utils14.pick)(toObject(v), subSelect.concat("_id")));
3236
+ if (subSelect) result = result.map((v) => (0, import_utils16.pick)(toObject(v), subSelect.concat("_id")));
3068
3237
  return { success: true, kind: "list", code: "success" /* Success */, data: result, count: result.length };
3069
3238
  }
3070
3239
  async function readSub(service, id, sub, subId, options) {
3071
3240
  const { select, populate } = options ?? {};
3072
3241
  const parentDoc = await getParentDoc(service, id, sub, { populate }, { access: "read" });
3073
3242
  if (!parentDoc) return { success: false, kind: "error", code: "not_found" /* NotFound */ };
3074
- const result = (0, import_utils14.get)(parentDoc, sub);
3243
+ const result = (0, import_utils16.get)(parentDoc, sub);
3075
3244
  const [subFilter, subSelect] = await Promise.all([
3076
3245
  service.genFilter(`subs.${sub}.read`, { _id: subId }),
3077
3246
  service.genQuerySelect("read", select, false, [sub, "sub"])
@@ -3079,28 +3248,27 @@ async function readSub(service, id, sub, subId, options) {
3079
3248
  if (subFilter === false) return { success: false, kind: "error", code: "forbidden" /* Forbidden */ };
3080
3249
  let subdoc = findElement(result, subFilter);
3081
3250
  if (!subdoc) return { success: false, kind: "error", code: "not_found" /* NotFound */ };
3082
- if (subSelect) subdoc = (0, import_utils14.pick)(toObject(subdoc), subSelect.concat(["_id"]));
3251
+ if (subSelect) subdoc = (0, import_utils16.pick)(toObject(subdoc), subSelect.concat(["_id"]));
3083
3252
  return { success: true, kind: "single", code: "success" /* Success */, data: subdoc };
3084
3253
  }
3085
- async function updateSub(service, id, sub, subId, data) {
3254
+ async function updateSub(service, persist, isAllowed, id, sub, subId, data) {
3086
3255
  const parentDoc = await getParentDoc(service, id, sub, null, { access: "update" });
3087
3256
  if (!parentDoc) return { success: false, kind: "error", code: "not_found" /* NotFound */ };
3088
- const result = (0, import_utils14.get)(parentDoc, sub);
3089
- const [subFilter, subReadSelect, subUpdateSelect] = await Promise.all([
3257
+ const result = (0, import_utils16.get)(parentDoc, sub);
3258
+ const [subFilter, subUpdateSelect] = await Promise.all([
3090
3259
  service.genFilter(`subs.${sub}.update`, { _id: subId }),
3091
- service.genQuerySelect("read", null, false, [sub, "sub"]),
3092
3260
  service.genQuerySelect("update", null, false, [sub, "sub"])
3093
3261
  ]);
3094
3262
  if (subFilter === false) return { success: false, kind: "error", code: "forbidden" /* Forbidden */ };
3095
- let subdoc = findElement(result, subFilter);
3263
+ const subdoc = findElement(result, subFilter);
3096
3264
  if (!subdoc) return { success: false, kind: "error", code: "not_found" /* NotFound */ };
3097
- const allowedData = (0, import_utils14.pick)(data, subUpdateSelect);
3098
- Object.assign(subdoc, allowedData);
3099
- await parentDoc.save();
3100
- if (subReadSelect) subdoc = (0, import_utils14.pick)(toObject(subdoc), subReadSelect.concat(["_id"]));
3101
- return { success: true, kind: "single", code: "success" /* Success */, data: subdoc };
3265
+ const allowedData = (0, import_utils16.pick)(data, subUpdateSelect);
3266
+ applyUpdate(subdoc, allowedData, subUpdateSelect);
3267
+ await persist(() => parentDoc.save());
3268
+ const visible = await visibleMutationRows(service, isAllowed, parentDoc._id, sub, [subdoc], "single");
3269
+ return { success: true, kind: "single", code: "success" /* Success */, data: visible[0] ?? null };
3102
3270
  }
3103
- async function bulkUpdateSub(service, id, sub, data) {
3271
+ async function bulkUpdateSub(service, persist, isAllowed, id, sub, data) {
3104
3272
  const { maxBulkItems } = service.getRequestComplexity();
3105
3273
  if (data.length > maxBulkItems) {
3106
3274
  return {
@@ -3112,25 +3280,24 @@ async function bulkUpdateSub(service, id, sub, data) {
3112
3280
  }
3113
3281
  const parentDoc = await getParentDoc(service, id, sub, null, { access: "update" });
3114
3282
  if (!parentDoc) return { success: false, kind: "error", code: "not_found" /* NotFound */ };
3115
- let result = (0, import_utils14.get)(parentDoc, sub);
3116
- const [subFilter, subReadSelect, subUpdateSelect] = await Promise.all([
3283
+ let result = (0, import_utils16.get)(parentDoc, sub);
3284
+ const [subFilter, subUpdateSelect] = await Promise.all([
3117
3285
  service.genFilter(`subs.${sub}.update`, { _id: { $in: data.map((v) => v._id) } }),
3118
- service.genQuerySelect("read", null, false, [sub, "sub"]),
3119
3286
  service.genQuerySelect("update", null, false, [sub, "sub"])
3120
3287
  ]);
3121
3288
  if (subFilter === false) return { success: false, kind: "error", code: "forbidden" /* Forbidden */ };
3122
- result = filterCollection(result, subFilter);
3123
- (0, import_utils14.forEach)(result, (subdoc) => {
3289
+ result = filterCollection(result, subFilter).filter((subdoc) => findElementById(data, subdoc._id));
3290
+ (0, import_utils16.forEach)(result, (subdoc) => {
3124
3291
  const tdata = findElementById(data, subdoc._id);
3125
3292
  if (!tdata) return;
3126
- const allowedData = (0, import_utils14.pick)(tdata, subUpdateSelect);
3127
- Object.assign(subdoc, allowedData);
3293
+ const allowedData = (0, import_utils16.pick)(tdata, subUpdateSelect);
3294
+ applyUpdate(subdoc, allowedData, subUpdateSelect);
3128
3295
  });
3129
- await parentDoc.save();
3130
- if (subReadSelect) result = result.map((v) => (0, import_utils14.pick)(toObject(v), subReadSelect.concat(["_id"])));
3296
+ await persist(() => parentDoc.save());
3297
+ result = await visibleMutationRows(service, isAllowed, parentDoc._id, sub, result, "bulk");
3131
3298
  return { success: true, kind: "list", code: "success" /* Success */, data: result, count: result.length };
3132
3299
  }
3133
- async function createSub(service, id, sub, data, options) {
3300
+ async function createSub(service, persist, isAllowed, id, sub, data, options) {
3134
3301
  const { addFirst } = options ?? {};
3135
3302
  const { maxBulkItems } = service.getRequestComplexity();
3136
3303
  if (Array.isArray(data) && data.length > maxBulkItems) {
@@ -3143,31 +3310,28 @@ async function createSub(service, id, sub, data, options) {
3143
3310
  }
3144
3311
  const parentDoc = await getParentDoc(service, id, sub, null, { access: "update" });
3145
3312
  if (!parentDoc) return { success: false, kind: "error", code: "not_found" /* NotFound */ };
3146
- let result = (0, import_utils14.get)(parentDoc, sub);
3147
- const [subCreateSelect, subReadSelect] = await Promise.all([
3148
- service.genQuerySelect("create", null, false, [sub, "sub"]),
3149
- service.genQuerySelect("read", null, false, [sub, "sub"])
3150
- ]);
3151
- const allowedData = Array.isArray(data) ? data.map((row) => (0, import_utils14.pick)(row, subCreateSelect)) : (0, import_utils14.pick)(data, subCreateSelect);
3313
+ let result = (0, import_utils16.get)(parentDoc, sub);
3314
+ const subCreateSelect = await service.genQuerySelect("create", null, false, [sub, "sub"]);
3315
+ const allowedData = Array.isArray(data) ? data.map((row) => (0, import_utils16.pick)(row, subCreateSelect)) : (0, import_utils16.pick)(data, subCreateSelect);
3152
3316
  if (Array.isArray(allowedData)) {
3153
3317
  addFirst === true ? result.unshift(...allowedData) : result.push(...allowedData);
3154
3318
  } else {
3155
3319
  addFirst === true ? result.unshift(allowedData) : result.push(allowedData);
3156
3320
  }
3157
- await parentDoc.save();
3158
- if (subReadSelect) result = result.map((v) => (0, import_utils14.pick)(toObject(v), subReadSelect.concat(["_id"])));
3321
+ await persist(() => parentDoc.save());
3322
+ result = await visibleMutationRows(service, isAllowed, parentDoc._id, sub, result, "create");
3159
3323
  return { success: true, kind: "list", code: "created" /* Created */, data: result, count: result.length };
3160
3324
  }
3161
- async function deleteSub(service, id, sub, subId) {
3325
+ async function deleteSub(service, persist, id, sub, subId) {
3162
3326
  const parentDoc = await getParentDoc(service, id, sub, null, { access: "update" });
3163
3327
  if (!parentDoc) return { success: false, kind: "error", code: "not_found" /* NotFound */ };
3164
- const result = (0, import_utils14.get)(parentDoc, sub);
3328
+ const result = (0, import_utils16.get)(parentDoc, sub);
3165
3329
  const subFilter = await service.genFilter(`subs.${sub}.delete`, { _id: subId });
3166
3330
  if (subFilter === false) return { success: false, kind: "error", code: "forbidden" /* Forbidden */ };
3167
3331
  const subdoc = findElement(result, subFilter);
3168
3332
  if (!subdoc) return { success: false, kind: "error", code: "not_found" /* NotFound */ };
3169
3333
  await ("deleteOne" in subdoc ? subdoc.deleteOne?.() : subdoc.remove?.());
3170
- await parentDoc.save();
3334
+ await persist(() => parentDoc.save());
3171
3335
  return { success: true, kind: "single", code: "success" /* Success */, data: subdoc._id };
3172
3336
  }
3173
3337
  async function getParentDoc(service, id, sub, args, options) {
@@ -3326,6 +3490,15 @@ var shouldKeepCorrelatedRef = (ref, keep) => {
3326
3490
  return false;
3327
3491
  };
3328
3492
  var Service = class extends Base {
3493
+ constructor(req, modelName) {
3494
+ super(req, modelName);
3495
+ this.persist = (operation) => this.getCorrelatedExecState().scheduler.work(operation);
3496
+ this.model = admitModelPersistence(this.createModelAdapter(modelName), this.getCorrelatedExecState().scheduler);
3497
+ this.options = this.getModelRouterOptions(modelName);
3498
+ this.defaults = this.options.defaults || {};
3499
+ this.baseFields = ["_id"];
3500
+ this.baseFieldsExt = this.baseFields.concat(this.options.documentPermissionField);
3501
+ }
3329
3502
  findRawParentDoc(args) {
3330
3503
  return this.model.findOne({
3331
3504
  ...args,
@@ -3363,14 +3536,6 @@ var Service = class extends Base {
3363
3536
  ...extra ?? {}
3364
3537
  });
3365
3538
  }
3366
- constructor(req, modelName) {
3367
- super(req, modelName);
3368
- this.model = this.createModelAdapter(modelName);
3369
- this.options = this.getModelRouterOptions(modelName);
3370
- this.defaults = this.options.defaults || {};
3371
- this.baseFields = ["_id"];
3372
- this.baseFieldsExt = this.baseFields.concat(this.options.documentPermissionField);
3373
- }
3374
3539
  async findOne(filter2, args, options) {
3375
3540
  const filterErrors = this.validateClientFilter(filter2);
3376
3541
  if (filterErrors.length > 0) return { success: false, kind: "error", code: "bad_request" /* BadRequest */, errors: filterErrors };
@@ -3378,20 +3543,22 @@ var Service = class extends Base {
3378
3543
  const { skim, includePermissions, access, populateAccess, lean } = this.resolveFindOneOptions(options);
3379
3544
  const { filter: overrideFilter, select: overrideSelect, populate: overridePopulate } = overrides ?? {};
3380
3545
  let parsedFilter;
3546
+ let processedInclude;
3381
3547
  try {
3382
- parsedFilter = await this.parseClientData(filter2);
3548
+ processedInclude = this.processInclude(include);
3549
+ parsedFilter = overrideFilter === false ? false : await this.parseClientData(filter2);
3383
3550
  } catch (error2) {
3384
3551
  const result = this.getClientRequestErrorResult(error2);
3385
3552
  if (result) return result;
3386
3553
  throw error2;
3387
3554
  }
3388
3555
  let [_filter, _select, _populate, allowedSortFields] = await Promise.all([
3389
- overrideFilter || this.genFilter(access, parsedFilter),
3556
+ overrideFilter ?? this.genFilter(access, parsedFilter),
3390
3557
  overrideSelect || this.genQuerySelect(access, select),
3391
3558
  overridePopulate || this.genPopulate(populateAccess || access, populate),
3392
3559
  this.genAllowedFields({}, access, this.baseFieldsExt)
3393
3560
  ]);
3394
- const { includes, correlatedIncludes, includeLocalFields, includePaths, correlatedReferenceFields } = this.processInclude(include);
3561
+ const { includes, correlatedIncludes, includeLocalFields, includePaths, correlatedReferenceFields } = processedInclude;
3395
3562
  const correlatedOutputPaths = correlatedIncludes.map((entry) => entry.path);
3396
3563
  const finalSelect = normalizeSelect(_select).concat(includeLocalFields, correlatedReferenceFields);
3397
3564
  const query = {
@@ -3426,7 +3593,7 @@ var Service = class extends Base {
3426
3593
  originalDocumentSnapshot: toObject(doc),
3427
3594
  resolvedQuery: query
3428
3595
  };
3429
- const correlatedSnapshots = [(0, import_utils15.cloneDeep)(toObject(doc))];
3596
+ const correlatedSnapshots = [(0, import_utils17.cloneDeep)(toObject(doc))];
3430
3597
  try {
3431
3598
  doc = await this.includeDocs(doc, includes);
3432
3599
  if (correlatedIncludes.length > 0) {
@@ -3473,7 +3640,8 @@ var Service = class extends Base {
3473
3640
  const { select, populate, include, overrides } = this.resolveFindByIdArgs(args);
3474
3641
  const { skim, includePermissions, access, populateAccess, lean } = this.resolveFindByIdOptions(options);
3475
3642
  const { select: overrideSelect, populate: overridePopulate, idFilter: overrideIdFilter } = overrides ?? {};
3476
- const filter2 = overrideIdFilter || await this.genIDFilter(id);
3643
+ const filter2 = overrideIdFilter ?? await this.genIDFilter(id);
3644
+ if (filter2 === false) return { success: false, kind: "error", code: "forbidden" /* Forbidden */, query: { filter: filter2 } };
3477
3645
  return this.findOne(
3478
3646
  filter2,
3479
3647
  {
@@ -3495,23 +3663,25 @@ var Service = class extends Base {
3495
3663
  const { skim, includePermissions, includeCount, populateAccess, lean } = this.resolveFindOptions(options);
3496
3664
  const { filter: overrideFilter, select: overrideSelect, populate: overridePopulate } = overrides ?? {};
3497
3665
  let parsedFilter;
3666
+ let processedInclude;
3498
3667
  try {
3499
- parsedFilter = await this.parseClientData(filter2);
3668
+ processedInclude = this.processInclude(include);
3669
+ parsedFilter = overrideFilter === false ? false : await this.parseClientData(filter2);
3500
3670
  } catch (error2) {
3501
3671
  const result = this.getClientRequestErrorResult(error2);
3502
3672
  if (result) return result;
3503
3673
  throw error2;
3504
3674
  }
3505
3675
  const [_filter, _select, _populate, pagination, allowedSortFields] = await Promise.all([
3506
- overrideFilter || this.genFilter("list", parsedFilter),
3676
+ overrideFilter ?? this.genFilter("list", parsedFilter),
3507
3677
  overrideSelect || this.genQuerySelect("list", select),
3508
3678
  overridePopulate || this.genPopulate(populateAccess, populate),
3509
3679
  genPagination({ skip, limit, page, pageSize }, this.options.listHardLimit),
3510
3680
  this.genAllowedFields({}, "list", this.baseFieldsExt)
3511
3681
  ]);
3512
3682
  const finalSelect = normalizeSelect(_select);
3513
- const filteredPopulate = (0, import_utils15.isArray)(finalSelect) && (0, import_utils15.isArray)(_populate) ? _populate.filter((p) => finalSelect.includes(p.path.split(".")[0])) : _populate;
3514
- const { includes, correlatedIncludes, includeLocalFields, includePaths, correlatedReferenceFields } = this.processInclude(include);
3683
+ const filteredPopulate = (0, import_utils17.isArray)(finalSelect) && (0, import_utils17.isArray)(_populate) ? _populate.filter((p) => finalSelect.includes(p.path.split(".")[0])) : _populate;
3684
+ const { includes, correlatedIncludes, includeLocalFields, includePaths, correlatedReferenceFields } = processedInclude;
3515
3685
  const correlatedOutputPaths = correlatedIncludes.map((entry) => entry.path);
3516
3686
  const query = {
3517
3687
  filter: _filter,
@@ -3548,8 +3718,8 @@ var Service = class extends Base {
3548
3718
  originalDocumentSnapshot: toObject(doc),
3549
3719
  resolvedQuery: query
3550
3720
  }));
3551
- const _decorate = (0, import_utils15.isFunction)(decorate) ? decorate : (v) => v;
3552
- const correlatedSnapshots = docs.map((doc) => (0, import_utils15.cloneDeep)(toObject(doc)));
3721
+ const _decorate = (0, import_utils17.isFunction)(decorate) ? decorate : (v) => v;
3722
+ const correlatedSnapshots = docs.map((doc) => (0, import_utils17.cloneDeep)(toObject(doc)));
3553
3723
  try {
3554
3724
  docs = await this.includeDocs(docs, includes);
3555
3725
  if (correlatedIncludes.length > 0) {
@@ -3665,16 +3835,16 @@ var Service = class extends Base {
3665
3835
  resolvedQuery: resolvedPopulate.length > 0 ? { populate: resolvedPopulate } : {}
3666
3836
  };
3667
3837
  const allowedFields = await this.genAllowedFields(item, "create");
3668
- const allowedData = (0, import_utils15.pick)(item, allowedFields);
3838
+ const allowedData = (0, import_utils17.pick)(item, allowedFields);
3669
3839
  context.allowedFields = allowedFields;
3670
3840
  context.allowedData = allowedData;
3671
3841
  const validated = await this.validate(allowedData, "create", context);
3672
- if ((0, import_utils15.isBoolean)(validated)) {
3842
+ if ((0, import_utils17.isBoolean)(validated)) {
3673
3843
  if (!validated) {
3674
3844
  validationErrors.push({ index, errors: [] });
3675
3845
  return void 0;
3676
3846
  }
3677
- } else if ((0, import_utils15.isArray)(validated)) {
3847
+ } else if ((0, import_utils17.isArray)(validated)) {
3678
3848
  if (validated.length > 0) {
3679
3849
  validationErrors.push({
3680
3850
  index,
@@ -3704,7 +3874,7 @@ var Service = class extends Base {
3704
3874
  contexts[index].preparedData = preparedData;
3705
3875
  return preparedData;
3706
3876
  });
3707
- const _decorate = (0, import_utils15.isFunction)(decorate) ? decorate : (v) => v;
3877
+ const _decorate = (0, import_utils17.isFunction)(decorate) ? decorate : (v) => v;
3708
3878
  const createdDocs = await this.model.create(items);
3709
3879
  const docs = await mapWithConcurrencyLimit(createdDocs, maxBulkConcurrency, async (doc, index) => {
3710
3880
  contexts[index].currentDocument = doc;
@@ -3721,7 +3891,7 @@ var Service = class extends Base {
3721
3891
  }
3722
3892
  if (includeDocPermissions) doc = await this.addDocPermissions(doc, "create", contexts[index]);
3723
3893
  if (includePermissions) doc = await this.addFieldPermissions(doc, "read", contexts[index]);
3724
- if (resolvedPopulate.length > 0) await populateDoc(doc, resolvedPopulate);
3894
+ if (resolvedPopulate.length > 0) await this.persist(() => populateDoc(doc, resolvedPopulate));
3725
3895
  doc = await this.trimOutputFields(doc, "read", this.baseFieldsExt);
3726
3896
  let outputDoc = await _decorate(doc, contexts[index]);
3727
3897
  if (!includePermissions) outputDoc = this.addEmptyPermissions(outputDoc);
@@ -3777,6 +3947,13 @@ var Service = class extends Base {
3777
3947
  data: doc
3778
3948
  };
3779
3949
  }
3950
+ /**
3951
+ * Update at authorized policy paths: admitted leaves preserve omitted siblings;
3952
+ * whole-authorized objects/arrays replace. Shared by updateById and existing-row
3953
+ * upsert. Validation precedes trusted prepare output (not re-filtered); partial
3954
+ * prepare containers preserve omissions. Transform retains explicit document-setter
3955
+ * replacement authority. Send nested client JSON, not literal dotted update keys.
3956
+ */
3780
3957
  async updateOne(filter2, data, args, options, decorate) {
3781
3958
  const filterErrors = this.validateClientFilter(filter2);
3782
3959
  if (filterErrors.length > 0) return { success: false, kind: "error", code: "bad_request" /* BadRequest */, errors: filterErrors };
@@ -3784,7 +3961,7 @@ var Service = class extends Base {
3784
3961
  const { skim, includePermissions, populateAccess } = this.resolveUpdateOneOptions(options);
3785
3962
  const { filter: overrideFilter, populate: overridePopulate } = overrides ?? {};
3786
3963
  const [_filter, _populate] = await Promise.all([
3787
- overrideFilter || this.genFilter("update", filter2),
3964
+ overrideFilter ?? this.genFilter("update", filter2),
3788
3965
  overridePopulate || this.genPopulate(populateAccess, populate)
3789
3966
  ]);
3790
3967
  const query = { filter: _filter, populate: _populate };
@@ -3822,16 +3999,16 @@ var Service = class extends Base {
3822
3999
  context.docPermissions = this.getDocPermissions(doc);
3823
4000
  context.currentDocument = doc;
3824
4001
  const allowedFields = await this.genAllowedFields(doc, "update");
3825
- const allowedData = (0, import_utils15.pick)(data, allowedFields);
4002
+ const allowedData = (0, import_utils17.pick)(data, allowedFields);
3826
4003
  context.allowedFields = allowedFields;
3827
4004
  context.allowedData = allowedData;
3828
4005
  const validated = await this.validate(allowedData, "update", context);
3829
- if ((0, import_utils15.isBoolean)(validated)) {
4006
+ if ((0, import_utils17.isBoolean)(validated)) {
3830
4007
  if (!validated) {
3831
4008
  this.completeOp("updateOne", startedAt, "bad_request" /* BadRequest */, _filter);
3832
4009
  return { success: false, kind: "error", code: "bad_request" /* BadRequest */ };
3833
4010
  }
3834
- } else if ((0, import_utils15.isArray)(validated)) {
4011
+ } else if ((0, import_utils17.isArray)(validated)) {
3835
4012
  if (validated.length > 0) {
3836
4013
  this.completeOp("updateOne", startedAt, "bad_request" /* BadRequest */, _filter);
3837
4014
  return { success: false, kind: "error", code: "bad_request" /* BadRequest */, errors: validated };
@@ -3839,18 +4016,18 @@ var Service = class extends Base {
3839
4016
  }
3840
4017
  const prepared = await this.prepare(allowedData, "update", context);
3841
4018
  context.preparedData = prepared;
3842
- Object.assign(doc, prepared);
4019
+ applyUpdate(doc, prepared, allowedFields);
3843
4020
  context.modifiedPaths = doc.modifiedPaths();
3844
4021
  doc = assertModelDocument(await this.transform(doc, "update", context), this.modelName, "transform");
3845
4022
  context.currentDocument = doc;
3846
- doc = await doc.save();
4023
+ doc = await this.persist(() => doc.save());
3847
4024
  const diffExcludeFields = [this.options.documentPermissionField, "__v"];
3848
4025
  this.asServiceHookContext(context).diff = (d) => {
3849
4026
  context.changes = (0, import_just_diff.diff)(
3850
- (0, import_utils15.omit)(context.originalDocumentSnapshot, diffExcludeFields),
3851
- (0, import_utils15.omit)(d.toObject({ virtuals: false }), diffExcludeFields)
4027
+ (0, import_utils17.omit)(context.originalDocumentSnapshot, diffExcludeFields),
4028
+ (0, import_utils17.omit)(d.toObject({ virtuals: false }), diffExcludeFields)
3852
4029
  ) || [];
3853
- context.modifiedPaths = (0, import_utils15.uniq)(context.changes.map((di) => di.path.length > 0 ? String(di.path[0]) : ""));
4030
+ context.modifiedPaths = (0, import_utils17.uniq)(context.changes.map((di) => di.path.length > 0 ? String(di.path[0]) : ""));
3854
4031
  };
3855
4032
  doc = assertModelDocument(await this.afterPersist(doc, "update", context), this.modelName, "afterPersist");
3856
4033
  context.currentDocument = doc;
@@ -3863,10 +4040,10 @@ var Service = class extends Base {
3863
4040
  }
3864
4041
  if (includeDocPermissions) doc = await this.addDocPermissions(doc, "update", context);
3865
4042
  if (includePermissions) doc = await this.addFieldPermissions(doc, "update", context);
3866
- if (_populate) await populateDoc(doc, _populate);
4043
+ if (_populate) await this.persist(() => populateDoc(doc, _populate));
3867
4044
  doc = await this.trimOutputFields(doc, "read", this.baseFieldsExt);
3868
4045
  let outputDoc = doc;
3869
- if ((0, import_utils15.isFunction)(decorate)) outputDoc = await decorate(outputDoc, context);
4046
+ if ((0, import_utils17.isFunction)(decorate)) outputDoc = await decorate(outputDoc, context);
3870
4047
  if (!includePermissions) outputDoc = this.addEmptyPermissions(outputDoc);
3871
4048
  this.completeOp("updateOne", startedAt, "success" /* Success */, _filter);
3872
4049
  return { success: true, kind: "single", code: "success" /* Success */, data: outputDoc, input: prepared };
@@ -3875,7 +4052,8 @@ var Service = class extends Base {
3875
4052
  const { populate, overrides } = this.resolveUpdateByIdArgs(args);
3876
4053
  const { skim, includePermissions, populateAccess } = this.resolveUpdateByIdOptions(options);
3877
4054
  const { populate: overridePopulate, idFilter: overrideIdFilter } = overrides;
3878
- const filter2 = overrideIdFilter || await this.genIDFilter(id);
4055
+ const filter2 = overrideIdFilter ?? await this.genIDFilter(id);
4056
+ if (filter2 === false) return { success: false, kind: "error", code: "forbidden" /* Forbidden */, query: { filter: filter2 } };
3879
4057
  return this.updateOne(
3880
4058
  filter2,
3881
4059
  data,
@@ -3895,7 +4073,7 @@ var Service = class extends Base {
3895
4073
  const { populate, overrides } = this.resolveUpsertArgs(args);
3896
4074
  const { skim, includePermissions, populateAccess } = this.resolveUpsertOptions(options);
3897
4075
  const { filter: overrideFilter, populate: overridePopulate } = overrides ?? {};
3898
- const _filter = await (overrideFilter || this.genFilter("update", filter2));
4076
+ const _filter = await (overrideFilter ?? this.genFilter("update", filter2));
3899
4077
  const query = { filter: _filter };
3900
4078
  const startedAt = this.beginOp("upsert", _filter);
3901
4079
  if (_filter === false) {
@@ -3955,7 +4133,9 @@ var Service = class extends Base {
3955
4133
  resolvedQuery: query
3956
4134
  };
3957
4135
  await this.beforeDelete(doc, context);
3958
- await ("deleteOne" in doc ? doc.deleteOne() : doc.remove());
4136
+ await this.persist(
4137
+ () => "deleteOne" in doc ? doc.deleteOne() : doc.remove()
4138
+ );
3959
4139
  context.finalDocumentSnapshot = toObject(doc);
3960
4140
  await this.afterDelete(doc, context);
3961
4141
  this.completeOp("delete", startedAt, "success" /* Success */, filter2);
@@ -3966,6 +4146,7 @@ var Service = class extends Base {
3966
4146
  if (filterErrors.length > 0) return { success: false, kind: "error", code: "bad_request" /* BadRequest */, errors: filterErrors };
3967
4147
  const { access, includeId } = this.resolveExistsOptions(options);
3968
4148
  filter2 = await this.genFilter(access, filter2);
4149
+ if (filter2 === false) return { success: false, kind: "error", code: "forbidden" /* Forbidden */, query: { filter: filter2 } };
3969
4150
  const result = await this.model.exists(filter2);
3970
4151
  return {
3971
4152
  success: true,
@@ -4035,6 +4216,13 @@ var Service = class extends Base {
4035
4216
  query
4036
4217
  };
4037
4218
  }
4219
+ /**
4220
+ * Group target document IDs by original parent-key aliases using one aggregate.
4221
+ * Resolves the requested row policy (legacy count includes pass `count`), casts
4222
+ * authorized filters/foreign operands with the target schema, and deduplicates IDs
4223
+ * independently of pagination. Cast failures return BadRequest; false policy returns
4224
+ * Forbidden. Casting does not execute countDocuments middleware/plugins.
4225
+ */
4038
4226
  async countByFieldValues(foreignField, values, filter2 = {}, access = "list") {
4039
4227
  if (!isValidFieldPath(foreignField)) {
4040
4228
  return {
@@ -4046,14 +4234,32 @@ var Service = class extends Base {
4046
4234
  }
4047
4235
  const filterErrors = this.validateClientFilter(filter2);
4048
4236
  if (filterErrors.length > 0) return { success: false, kind: "error", code: "bad_request" /* BadRequest */, errors: filterErrors };
4049
- const uniqueValues = (0, import_utils15.uniqBy)(values, (value) => String(value));
4237
+ const uniqueValues = (0, import_utils17.uniqBy)(values, (value) => String(value));
4050
4238
  filter2 = await this.genFilter(access, {
4051
4239
  ...filter2,
4052
4240
  [foreignField]: { $in: uniqueValues }
4053
4241
  });
4054
4242
  const query = { filter: filter2 };
4055
4243
  if (filter2 === false) return { success: false, kind: "error", code: "forbidden" /* Forbidden */, query };
4056
- const matchFilter = filter2;
4244
+ let matchFilter = filter2;
4245
+ let castValues = uniqueValues;
4246
+ try {
4247
+ if (this.model.castFilter) {
4248
+ matchFilter = this.model.castFilter(matchFilter);
4249
+ const foreignFilter = this.model.castFilter({ [foreignField]: { $in: uniqueValues } });
4250
+ castValues = foreignFilter[foreignField]?.$in;
4251
+ if (!Array.isArray(castValues) || castValues.length !== uniqueValues.length) {
4252
+ throw new Error("Foreign field query casting must retain the join operands");
4253
+ }
4254
+ }
4255
+ } catch {
4256
+ return {
4257
+ success: false,
4258
+ kind: "error",
4259
+ code: "bad_request" /* BadRequest */,
4260
+ errors: [{ detail: "Invalid grouped include count filter or foreign-key value" }]
4261
+ };
4262
+ }
4057
4263
  const rows = await this.model.aggregate([
4058
4264
  { $match: matchFilter },
4059
4265
  {
@@ -4064,12 +4270,20 @@ var Service = class extends Base {
4064
4270
  }
4065
4271
  },
4066
4272
  { $unwind: "$foreignValues" },
4067
- { $match: { foreignValues: { $in: uniqueValues } } },
4273
+ { $match: { foreignValues: { $in: castValues } } },
4068
4274
  { $group: { _id: "$foreignValues", documentIds: { $addToSet: "$_id" } } }
4069
4275
  ]);
4070
- const counts = /* @__PURE__ */ new Map();
4276
+ const grouped = /* @__PURE__ */ new Map();
4071
4277
  for (const row of rows) {
4072
- counts.set(String(row._id), new Set(row.documentIds.map((id) => String(id))));
4278
+ const key = String(row._id);
4279
+ const ids = grouped.get(key) ?? /* @__PURE__ */ new Set();
4280
+ for (const id of row.documentIds) ids.add(String(id));
4281
+ grouped.set(key, ids);
4282
+ }
4283
+ const counts = /* @__PURE__ */ new Map();
4284
+ for (let i = 0; i < uniqueValues.length; i++) {
4285
+ const ids = grouped.get(String(castValues[i]));
4286
+ if (ids) counts.set(String(uniqueValues[i]), ids);
4073
4287
  }
4074
4288
  return { success: true, kind: "single", code: "success" /* Success */, data: counts, query };
4075
4289
  }
@@ -4122,7 +4336,7 @@ var Service = class extends Base {
4122
4336
  return resolveExistsOptions(this, options);
4123
4337
  }
4124
4338
  async getFieldPermissionAccess(ids) {
4125
- const uniqueIds = (0, import_utils15.compact)((0, import_utils15.uniqBy)(ids, (id) => String(id)).map((id) => String(id)));
4339
+ const uniqueIds = (0, import_utils17.compact)((0, import_utils17.uniqBy)(ids, (id) => String(id)).map((id) => String(id)));
4126
4340
  if (uniqueIds.length === 0) {
4127
4341
  return {
4128
4342
  readIds: /* @__PURE__ */ new Set(),
@@ -4148,17 +4362,60 @@ var Service = class extends Base {
4148
4362
  async readSub(id, sub, subId, options) {
4149
4363
  return readSub(this, id, sub, subId, options);
4150
4364
  }
4365
+ /**
4366
+ * Apply authorized policy-path updates, preserving protected siblings, then save.
4367
+ * Output requires post-save parent read and subdocument read operation/row access;
4368
+ * hidden output is successful `data: null` (direct/root-entry 200), not a failed write.
4369
+ * Visible output uses read fields plus `_id`. Do not retry solely for null output.
4370
+ */
4151
4371
  async updateSub(id, sub, subId, data) {
4152
- return updateSub(this, id, sub, subId, data);
4372
+ return updateSub(
4373
+ this,
4374
+ this.persist,
4375
+ (access) => this.req.macl.isAllowed(this.modelName, access),
4376
+ id,
4377
+ sub,
4378
+ subId,
4379
+ data
4380
+ );
4153
4381
  }
4382
+ /**
4383
+ * Save authorized targeted updates, then return readable targets in stored order.
4384
+ * Requires parent/subdocument read access for output, not subdocument list access;
4385
+ * uses read fields plus `_id`. Hidden output succeeds with `data: [], count: 0`
4386
+ * (direct/root-entry 200); count measures visible rows, not all updated rows.
4387
+ */
4154
4388
  async bulkUpdateSub(id, sub, data) {
4155
- return bulkUpdateSub(this, id, sub, (0, import_utils15.castArray)(data));
4389
+ return bulkUpdateSub(
4390
+ this,
4391
+ this.persist,
4392
+ (access) => this.req.macl.isAllowed(this.modelName, access),
4393
+ id,
4394
+ sub,
4395
+ (0, import_utils17.castArray)(data)
4396
+ );
4156
4397
  }
4398
+ /**
4399
+ * Append/prepend admitted rows, then return the visible full array (including when
4400
+ * input is empty). Output requires post-save parent read and subdocument list AND
4401
+ * read guards/row filters; fields use read projection plus `_id`.
4402
+ * Hidden output succeeds with code `created`, `data: [], count: 0`
4403
+ * (direct/root-entry 201). Visible count/order follow filtered stored rows.
4404
+ * Empty output does not mean the write failed; addFirst is a service option.
4405
+ */
4157
4406
  async createSub(id, sub, data, options) {
4158
- return createSub(this, id, sub, data, options);
4407
+ return createSub(
4408
+ this,
4409
+ this.persist,
4410
+ (access) => this.req.macl.isAllowed(this.modelName, access),
4411
+ id,
4412
+ sub,
4413
+ data,
4414
+ options
4415
+ );
4159
4416
  }
4160
4417
  async deleteSub(id, sub, subId) {
4161
- return deleteSub(this, id, sub, subId);
4418
+ return deleteSub(this, this.persist, id, sub, subId);
4162
4419
  }
4163
4420
  async getParentDoc(id, sub, args, options) {
4164
4421
  return getParentDoc(this, id, sub, args, options);
@@ -4166,7 +4423,7 @@ var Service = class extends Base {
4166
4423
  };
4167
4424
 
4168
4425
  // src/services/public-service.ts
4169
- var import_utils16 = require("@web-ts-toolkit/utils");
4426
+ var import_utils18 = require("@web-ts-toolkit/utils");
4170
4427
  var PublicService = class extends Service {
4171
4428
  async _list(filter2, args, options) {
4172
4429
  const { select, populate, include, sort, skip, limit, page, pageSize, tasks } = this.resolvePublicListArgs(args);
@@ -4205,7 +4462,7 @@ var PublicService = class extends Service {
4205
4462
  let d = toObject(doc);
4206
4463
  d = await this.decorate(d, "create", context);
4207
4464
  d = this.runTasks(d, tasks);
4208
- if (select) d = (0, import_utils16.pick)(d, [...normalizeSelect(select), ...this.baseFieldsExt]);
4465
+ if (select) d = (0, import_utils18.pick)(d, [...normalizeSelect(select), ...this.baseFieldsExt]);
4209
4466
  return d;
4210
4467
  }
4211
4468
  );
@@ -4232,6 +4489,7 @@ var PublicService = class extends Service {
4232
4489
  },
4233
4490
  { skim, includePermissions, access, populateAccess, lean }
4234
4491
  );
4492
+ if (!result.success && (result.code === "bad_request" /* BadRequest */ || result.code === "forbidden" /* Forbidden */)) return result;
4235
4493
  if (tryList && (!result.success || !result.data)) {
4236
4494
  const listAllowed = await this.req.macl.isAllowed(this.modelName, "list");
4237
4495
  if (!listAllowed) {
@@ -4272,6 +4530,7 @@ var PublicService = class extends Service {
4272
4530
  },
4273
4531
  { skim, includePermissions, access, populateAccess, lean }
4274
4532
  );
4533
+ if (!result.success && (result.code === "bad_request" /* BadRequest */ || result.code === "forbidden" /* Forbidden */)) return result;
4275
4534
  if (tryList && (!result.success || !result.data)) {
4276
4535
  const listAllowed = await this.req.macl.isAllowed(this.modelName, "list");
4277
4536
  if (!listAllowed) {
@@ -4310,8 +4569,8 @@ var PublicService = class extends Service {
4310
4569
  let d = toObject(doc);
4311
4570
  d = await this.decorate(d, "update", context);
4312
4571
  d = this.runTasks(d, tasks);
4313
- if (select) d = (0, import_utils16.pick)(d, [...normalizeSelect(select), ...this.baseFieldsExt]);
4314
- else if (!returningAll) d = (0, import_utils16.pick)(d, [...Object.keys(data), "_id"]);
4572
+ if (select) d = (0, import_utils18.pick)(d, [...normalizeSelect(select), ...this.baseFieldsExt]);
4573
+ else if (!returningAll) d = (0, import_utils18.pick)(d, [...Object.keys(data), "_id"]);
4315
4574
  return d;
4316
4575
  }
4317
4576
  );
@@ -4320,6 +4579,11 @@ var PublicService = class extends Service {
4320
4579
  }
4321
4580
  return result;
4322
4581
  }
4582
+ /**
4583
+ * With an _id, check update-policy existence then update: terminal denial returns
4584
+ * Forbidden, an allowed miss remains Unauthorized, and neither creates a row.
4585
+ * Without an _id, use the create path.
4586
+ */
4323
4587
  async _upsert(data, args, options) {
4324
4588
  const idKey = this.getIdentifier();
4325
4589
  if (idKey !== "_id") {
@@ -4433,7 +4697,7 @@ var PublicService = class extends Service {
4433
4697
  };
4434
4698
 
4435
4699
  // src/services/data-service.ts
4436
- var import_utils17 = require("@web-ts-toolkit/utils");
4700
+ var import_utils19 = require("@web-ts-toolkit/utils");
4437
4701
  var DataService = class {
4438
4702
  constructor(req, dataName) {
4439
4703
  this.req = req;
@@ -4455,7 +4719,7 @@ var DataService = class {
4455
4719
  let doc = await findElement(this.data, _filter);
4456
4720
  if (!doc) return { success: false, kind: "error", code: "not_found" /* NotFound */, query };
4457
4721
  doc = await this.trimOutputFields(doc, access);
4458
- if (_select.length > 0) doc = (0, import_utils17.pick)(doc, _select);
4722
+ if (_select.length > 0) doc = (0, import_utils19.pick)(doc, _select);
4459
4723
  return { success: true, kind: "single", code: "success" /* Success */, data: doc, query };
4460
4724
  }
4461
4725
  async findById(id, args, options) {
@@ -4495,13 +4759,13 @@ var DataService = class {
4495
4759
  const totalCount = docs.length;
4496
4760
  if (sort) {
4497
4761
  const normalizedSort = normalizeSortForOrderBy(sort);
4498
- docs = (0, import_utils17.orderBy)(docs, normalizedSort.fields, normalizedSort.orders);
4762
+ docs = (0, import_utils19.orderBy)(docs, normalizedSort.fields, normalizedSort.orders);
4499
4763
  }
4500
4764
  const pagedDocs = docs.slice(query.skip, query.limit && query.skip + query.limit);
4501
4765
  const { maxHookConcurrency } = this.getRequestComplexity();
4502
4766
  const trimmed = await mapWithConcurrencyLimit(pagedDocs, maxHookConcurrency, async (doc) => {
4503
4767
  doc = await this.trimOutputFields(doc, "list");
4504
- if (_select.length > 0) doc = (0, import_utils17.pick)(doc, _select);
4768
+ if (_select.length > 0) doc = (0, import_utils19.pick)(doc, _select);
4505
4769
  return doc;
4506
4770
  });
4507
4771
  if (!hasExplicitLimit && trimmed.length === totalCount) {
@@ -4550,7 +4814,7 @@ var DataService = class {
4550
4814
  };
4551
4815
 
4552
4816
  // src/processors.ts
4553
- var import_utils18 = require("@web-ts-toolkit/utils");
4817
+ var import_utils20 = require("@web-ts-toolkit/utils");
4554
4818
  var UNSAFE_PATH_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
4555
4819
  var DEFAULT_MAX_SLICE = 1e3;
4556
4820
  var DEFAULT_MASK_REPLACEMENT = "***";
@@ -4585,10 +4849,10 @@ function resolveLeafParents(obj, src) {
4585
4849
  for (let ind = 0; ind < segs.length - 1; ind += 1) {
4586
4850
  const seg = segs[ind];
4587
4851
  targets = targets.reduce((ret, target) => {
4588
- if (!(0, import_utils18.isPlainObject)(target)) return ret;
4852
+ if (!(0, import_utils20.isPlainObject)(target)) return ret;
4589
4853
  const targetObject = target;
4590
4854
  const next = targetObject[seg];
4591
- if ((0, import_utils18.isArray)(next)) {
4855
+ if ((0, import_utils20.isArray)(next)) {
4592
4856
  for (const item of next) {
4593
4857
  ret.push(item);
4594
4858
  }
@@ -4598,8 +4862,8 @@ function resolveLeafParents(obj, src) {
4598
4862
  }
4599
4863
  const leaf = segs[segs.length - 1];
4600
4864
  const pairs = [];
4601
- (0, import_utils18.forEach)(targets, (target) => {
4602
- if (!(0, import_utils18.isPlainObject)(target)) return;
4865
+ (0, import_utils20.forEach)(targets, (target) => {
4866
+ if (!(0, import_utils20.isPlainObject)(target)) return;
4603
4867
  pairs.push({ parent: target, leaf });
4604
4868
  });
4605
4869
  return pairs;
@@ -4607,8 +4871,8 @@ function resolveLeafParents(obj, src) {
4607
4871
  var copyAndDepopulate = (docObject, operations, options = { mutable: true, idField: "_id" }) => {
4608
4872
  const mutable = options.mutable !== false;
4609
4873
  const idField = options.idField ?? "_id";
4610
- const obj = mutable ? docObject : (0, import_utils18.cloneDeep)(docObject);
4611
- (0, import_utils18.forEach)(Array.isArray(operations) ? operations : [], (op) => {
4874
+ const obj = mutable ? docObject : (0, import_utils20.cloneDeep)(docObject);
4875
+ (0, import_utils20.forEach)(Array.isArray(operations) ? operations : [], (op) => {
4612
4876
  if (!op || typeof op.src !== "string" || typeof op.dest !== "string") return;
4613
4877
  const src = op.src;
4614
4878
  const dest = op.dest;
@@ -4617,19 +4881,19 @@ var copyAndDepopulate = (docObject, operations, options = { mutable: true, idFie
4617
4881
  assertSafePath(dest);
4618
4882
  let targets = [obj];
4619
4883
  const segs = src.split(".");
4620
- (0, import_utils18.forEach)(segs, (seg, ind) => {
4884
+ (0, import_utils20.forEach)(segs, (seg, ind) => {
4621
4885
  if (segs.length === ind + 1) {
4622
- (0, import_utils18.forEach)(targets, (target) => {
4623
- if (!(0, import_utils18.isPlainObject)(target)) return;
4886
+ (0, import_utils20.forEach)(targets, (target) => {
4887
+ if (!(0, import_utils20.isPlainObject)(target)) return;
4624
4888
  const targetObject = target;
4625
4889
  const leaf = targetObject[seg];
4626
4890
  const sourceForDest = leaf;
4627
4891
  let depopulated;
4628
- if ((0, import_utils18.isArray)(leaf)) {
4892
+ if ((0, import_utils20.isArray)(leaf)) {
4629
4893
  if (leaf.length === 0) {
4630
4894
  depopulated = [];
4631
- } else if (leaf.every((item) => (0, import_utils18.isPlainObject)(item))) {
4632
- depopulated = (0, import_utils18.map)(leaf, idField).map((id, i) => {
4895
+ } else if (leaf.every((item) => (0, import_utils20.isPlainObject)(item))) {
4896
+ depopulated = (0, import_utils20.map)(leaf, idField).map((id, i) => {
4633
4897
  if (id === void 0) {
4634
4898
  failMissingId(`${src}[${i}]`);
4635
4899
  }
@@ -4638,8 +4902,8 @@ var copyAndDepopulate = (docObject, operations, options = { mutable: true, idFie
4638
4902
  } else {
4639
4903
  return;
4640
4904
  }
4641
- } else if ((0, import_utils18.isPlainObject)(leaf)) {
4642
- const id = (0, import_utils18.get)(leaf, idField);
4905
+ } else if ((0, import_utils20.isPlainObject)(leaf)) {
4906
+ const id = (0, import_utils20.get)(leaf, idField);
4643
4907
  if (id === void 0) {
4644
4908
  failMissingId(src);
4645
4909
  }
@@ -4647,15 +4911,15 @@ var copyAndDepopulate = (docObject, operations, options = { mutable: true, idFie
4647
4911
  } else {
4648
4912
  return;
4649
4913
  }
4650
- (0, import_utils18.set)(targetObject, dest, sourceForDest);
4651
- (0, import_utils18.set)(targetObject, seg, depopulated);
4914
+ (0, import_utils20.set)(targetObject, dest, sourceForDest);
4915
+ (0, import_utils20.set)(targetObject, seg, depopulated);
4652
4916
  });
4653
4917
  } else {
4654
4918
  targets = targets.reduce((ret, target) => {
4655
- if (!(0, import_utils18.isPlainObject)(target)) return ret;
4919
+ if (!(0, import_utils20.isPlainObject)(target)) return ret;
4656
4920
  const targetObject = target;
4657
4921
  const next = targetObject[seg];
4658
- if ((0, import_utils18.isArray)(next)) {
4922
+ if ((0, import_utils20.isArray)(next)) {
4659
4923
  for (const item of next) {
4660
4924
  ret.push(item);
4661
4925
  }
@@ -4669,8 +4933,8 @@ var copyAndDepopulate = (docObject, operations, options = { mutable: true, idFie
4669
4933
  };
4670
4934
  var copyPaths = (docObject, operations, options = { mutable: true }) => {
4671
4935
  const mutable = options.mutable !== false;
4672
- const obj = mutable ? docObject : (0, import_utils18.cloneDeep)(docObject);
4673
- (0, import_utils18.forEach)(Array.isArray(operations) ? operations : [], (op) => {
4936
+ const obj = mutable ? docObject : (0, import_utils20.cloneDeep)(docObject);
4937
+ (0, import_utils20.forEach)(Array.isArray(operations) ? operations : [], (op) => {
4674
4938
  if (!op || typeof op.src !== "string" || typeof op.dest !== "string") return;
4675
4939
  const src = op.src;
4676
4940
  const dest = op.dest;
@@ -4678,17 +4942,17 @@ var copyPaths = (docObject, operations, options = { mutable: true }) => {
4678
4942
  assertSafePath(src, "copyPaths");
4679
4943
  assertSafePath(dest, "copyPaths");
4680
4944
  if (src === dest) return;
4681
- (0, import_utils18.forEach)(resolveLeafParents(obj, src), ({ parent, leaf }) => {
4682
- if (!(0, import_utils18.hasOwn)(parent, leaf)) return;
4683
- (0, import_utils18.set)(parent, dest, (0, import_utils18.cloneDeep)(parent[leaf]));
4945
+ (0, import_utils20.forEach)(resolveLeafParents(obj, src), ({ parent, leaf }) => {
4946
+ if (!(0, import_utils20.hasOwn)(parent, leaf)) return;
4947
+ (0, import_utils20.set)(parent, dest, (0, import_utils20.cloneDeep)(parent[leaf]));
4684
4948
  });
4685
4949
  });
4686
4950
  return obj;
4687
4951
  };
4688
4952
  var movePaths = (docObject, operations, options = { mutable: true }) => {
4689
4953
  const mutable = options.mutable !== false;
4690
- const obj = mutable ? docObject : (0, import_utils18.cloneDeep)(docObject);
4691
- (0, import_utils18.forEach)(Array.isArray(operations) ? operations : [], (op) => {
4954
+ const obj = mutable ? docObject : (0, import_utils20.cloneDeep)(docObject);
4955
+ (0, import_utils20.forEach)(Array.isArray(operations) ? operations : [], (op) => {
4692
4956
  if (!op || typeof op.src !== "string" || typeof op.dest !== "string") return;
4693
4957
  const src = op.src;
4694
4958
  const dest = op.dest;
@@ -4696,10 +4960,10 @@ var movePaths = (docObject, operations, options = { mutable: true }) => {
4696
4960
  assertSafePath(src, "movePaths");
4697
4961
  assertSafePath(dest, "movePaths");
4698
4962
  if (src === dest) return;
4699
- (0, import_utils18.forEach)(resolveLeafParents(obj, src), ({ parent, leaf }) => {
4700
- if (!(0, import_utils18.hasOwn)(parent, leaf)) return;
4701
- const value = (0, import_utils18.cloneDeep)(parent[leaf]);
4702
- (0, import_utils18.set)(parent, dest, value);
4963
+ (0, import_utils20.forEach)(resolveLeafParents(obj, src), ({ parent, leaf }) => {
4964
+ if (!(0, import_utils20.hasOwn)(parent, leaf)) return;
4965
+ const value = (0, import_utils20.cloneDeep)(parent[leaf]);
4966
+ (0, import_utils20.set)(parent, dest, value);
4703
4967
  if (dest === leaf || dest.startsWith(`${leaf}.`)) return;
4704
4968
  delete parent[leaf];
4705
4969
  });
@@ -4710,8 +4974,8 @@ var sliceArrays = (docObject, operations, options = { mutable: true, maxSlice: D
4710
4974
  const mutable = options.mutable !== false;
4711
4975
  const maxSlice = options.maxSlice ?? DEFAULT_MAX_SLICE;
4712
4976
  assertNonNegativeSafeInteger(maxSlice, "maxSlice", "sliceArrays");
4713
- const obj = mutable ? docObject : (0, import_utils18.cloneDeep)(docObject);
4714
- (0, import_utils18.forEach)(Array.isArray(operations) ? operations : [], (op) => {
4977
+ const obj = mutable ? docObject : (0, import_utils20.cloneDeep)(docObject);
4978
+ (0, import_utils20.forEach)(Array.isArray(operations) ? operations : [], (op) => {
4715
4979
  if (!op || typeof op.src !== "string") return;
4716
4980
  const src = op.src;
4717
4981
  if (src.length === 0) return;
@@ -4728,15 +4992,15 @@ var sliceArrays = (docObject, operations, options = { mutable: true, maxSlice: D
4728
4992
  assertNonNegativeSafeInteger(op.limit, "limit", "sliceArrays");
4729
4993
  limit = Math.min(op.limit, maxSlice);
4730
4994
  }
4731
- (0, import_utils18.forEach)(resolveLeafParents(obj, src), ({ parent, leaf }) => {
4995
+ (0, import_utils20.forEach)(resolveLeafParents(obj, src), ({ parent, leaf }) => {
4732
4996
  const leafValue = parent[leaf];
4733
- if (!(0, import_utils18.isArray)(leafValue)) return;
4997
+ if (!(0, import_utils20.isArray)(leafValue)) return;
4734
4998
  const end = typeof limit === "undefined" ? void 0 : skip + limit;
4735
4999
  const window = leafValue.slice(skip, end);
4736
5000
  if (typeof rawDest === "undefined") {
4737
- (0, import_utils18.set)(parent, leaf, window);
5001
+ (0, import_utils20.set)(parent, leaf, window);
4738
5002
  } else {
4739
- (0, import_utils18.set)(parent, rawDest, window);
5003
+ (0, import_utils20.set)(parent, rawDest, window);
4740
5004
  }
4741
5005
  });
4742
5006
  });
@@ -4744,44 +5008,44 @@ var sliceArrays = (docObject, operations, options = { mutable: true, maxSlice: D
4744
5008
  };
4745
5009
  var countPaths = (docObject, operations, options = { mutable: true }) => {
4746
5010
  const mutable = options.mutable !== false;
4747
- const obj = mutable ? docObject : (0, import_utils18.cloneDeep)(docObject);
4748
- (0, import_utils18.forEach)(Array.isArray(operations) ? operations : [], (op) => {
5011
+ const obj = mutable ? docObject : (0, import_utils20.cloneDeep)(docObject);
5012
+ (0, import_utils20.forEach)(Array.isArray(operations) ? operations : [], (op) => {
4749
5013
  if (!op || typeof op.src !== "string" || typeof op.dest !== "string") return;
4750
5014
  const src = op.src;
4751
5015
  const dest = op.dest;
4752
5016
  if (src.length === 0 || dest.length === 0) return;
4753
5017
  assertSafePath(src, "countPaths");
4754
5018
  assertSafePath(dest, "countPaths");
4755
- (0, import_utils18.forEach)(resolveLeafParents(obj, src), ({ parent, leaf }) => {
4756
- if (!(0, import_utils18.hasOwn)(parent, leaf)) return;
5019
+ (0, import_utils20.forEach)(resolveLeafParents(obj, src), ({ parent, leaf }) => {
5020
+ if (!(0, import_utils20.hasOwn)(parent, leaf)) return;
4757
5021
  const leafValue = parent[leaf];
4758
5022
  let count;
4759
- if ((0, import_utils18.isArray)(leafValue)) {
5023
+ if ((0, import_utils20.isArray)(leafValue)) {
4760
5024
  count = leafValue.length;
4761
- } else if ((0, import_utils18.isPlainObject)(leafValue)) {
5025
+ } else if ((0, import_utils20.isPlainObject)(leafValue)) {
4762
5026
  count = Object.keys(leafValue).length;
4763
5027
  } else if (leafValue === null || typeof leafValue === "undefined") {
4764
5028
  count = 0;
4765
5029
  } else {
4766
5030
  count = 1;
4767
5031
  }
4768
- (0, import_utils18.set)(parent, dest, count);
5032
+ (0, import_utils20.set)(parent, dest, count);
4769
5033
  });
4770
5034
  });
4771
5035
  return obj;
4772
5036
  };
4773
5037
  var maskPaths = (docObject, operations, options = { mutable: true }) => {
4774
5038
  const mutable = options.mutable !== false;
4775
- const obj = mutable ? docObject : (0, import_utils18.cloneDeep)(docObject);
4776
- (0, import_utils18.forEach)(Array.isArray(operations) ? operations : [], (op) => {
5039
+ const obj = mutable ? docObject : (0, import_utils20.cloneDeep)(docObject);
5040
+ (0, import_utils20.forEach)(Array.isArray(operations) ? operations : [], (op) => {
4777
5041
  if (!op || typeof op.src !== "string") return;
4778
5042
  const src = op.src;
4779
5043
  if (src.length === 0) return;
4780
5044
  assertSafePath(src, "maskPaths");
4781
5045
  const replacement = Object.prototype.hasOwnProperty.call(op, "replacement") ? op.replacement : DEFAULT_MASK_REPLACEMENT;
4782
- (0, import_utils18.forEach)(resolveLeafParents(obj, src), ({ parent, leaf }) => {
4783
- if (!(0, import_utils18.hasOwn)(parent, leaf)) return;
4784
- (0, import_utils18.set)(parent, leaf, replacement);
5046
+ (0, import_utils20.forEach)(resolveLeafParents(obj, src), ({ parent, leaf }) => {
5047
+ if (!(0, import_utils20.hasOwn)(parent, leaf)) return;
5048
+ (0, import_utils20.set)(parent, leaf, replacement);
4785
5049
  });
4786
5050
  });
4787
5051
  return obj;
@@ -4825,10 +5089,10 @@ var Cache = class {
4825
5089
  };
4826
5090
 
4827
5091
  // src/core-shared.ts
4828
- var import_utils20 = require("@web-ts-toolkit/utils");
5092
+ var import_utils22 = require("@web-ts-toolkit/utils");
4829
5093
 
4830
5094
  // src/permission.ts
4831
- var import_utils19 = require("@web-ts-toolkit/utils");
5095
+ var import_utils21 = require("@web-ts-toolkit/utils");
4832
5096
  var Permission = class {
4833
5097
  constructor(permissions) {
4834
5098
  this.permissions = { ...permissions };
@@ -4844,7 +5108,7 @@ var Permission = class {
4844
5108
  return [permissionOrPermissions].concat(permissions);
4845
5109
  }
4846
5110
  hasKey(permission) {
4847
- return (0, import_utils19.hasOwn)(this.permissions, permission);
5111
+ return (0, import_utils21.hasOwn)(this.permissions, permission);
4848
5112
  }
4849
5113
  has(permission) {
4850
5114
  return this.permissions[permission] === true;
@@ -4864,10 +5128,10 @@ var permission_default = Permission;
4864
5128
 
4865
5129
  // src/core-shared.ts
4866
5130
  function isAndFilter(filter2) {
4867
- return !!filter2 && (0, import_utils20.isPlainObject)(filter2) && Object.keys(filter2).length === 1 && (0, import_utils20.isArray)(filter2.$and);
5131
+ return !!filter2 && (0, import_utils22.isPlainObject)(filter2) && Object.keys(filter2).length === 1 && (0, import_utils22.isArray)(filter2.$and);
4868
5132
  }
4869
5133
  function isMergeableClause(filter2) {
4870
- return !!filter2 && (0, import_utils20.isPlainObject)(filter2) && Object.keys(filter2).every((key) => !key.startsWith("$"));
5134
+ return !!filter2 && (0, import_utils22.isPlainObject)(filter2) && Object.keys(filter2).every((key) => !key.startsWith("$"));
4871
5135
  }
4872
5136
  function optimizeAndFilter(clausesInput) {
4873
5137
  const clauses = [];
@@ -4882,10 +5146,10 @@ function optimizeAndFilter(clausesInput) {
4882
5146
  }
4883
5147
  }
4884
5148
  const dedupeKey = (value) => {
4885
- if ((0, import_utils20.isArray)(value)) {
5149
+ if ((0, import_utils22.isArray)(value)) {
4886
5150
  return `[${value.map(dedupeKey).join(",")}]`;
4887
5151
  }
4888
- if ((0, import_utils20.isPlainObject)(value)) {
5152
+ if ((0, import_utils22.isPlainObject)(value)) {
4889
5153
  return `{${Object.keys(value).sort().map((key) => `${key}:${dedupeKey(value[key])}`).join(",")}}`;
4890
5154
  }
4891
5155
  return JSON.stringify(value);
@@ -4909,7 +5173,7 @@ function optimizeAndFilter(clausesInput) {
4909
5173
  }
4910
5174
  let canMerge = true;
4911
5175
  for (const [key, value] of Object.entries(clause)) {
4912
- if ((0, import_utils20.hasOwn)(mergedClause, key) && !(0, import_utils20.isEqual)(mergedClause[key], value)) {
5176
+ if ((0, import_utils22.hasOwn)(mergedClause, key) && !(0, import_utils22.isEqual)(mergedClause[key], value)) {
4913
5177
  canMerge = false;
4914
5178
  break;
4915
5179
  }
@@ -4920,24 +5184,24 @@ function optimizeAndFilter(clausesInput) {
4920
5184
  }
4921
5185
  Object.assign(mergedClause, clause);
4922
5186
  }
4923
- const finalClauses = (0, import_utils20.isEmpty)(mergedClause) ? remainingClauses : [mergedClause, ...remainingClauses];
5187
+ const finalClauses = (0, import_utils22.isEmpty)(mergedClause) ? remainingClauses : [mergedClause, ...remainingClauses];
4924
5188
  if (finalClauses.length === 0) return null;
4925
5189
  if (finalClauses.length === 1) return finalClauses[0];
4926
5190
  return { $and: finalClauses };
4927
5191
  }
4928
5192
  function normalizeFilter(filter2) {
4929
5193
  if (filter2 === false) return false;
4930
- if (!filter2 || !(0, import_utils20.isPlainObject)(filter2) || (0, import_utils20.isEmpty)(filter2)) return null;
5194
+ if (!filter2 || !(0, import_utils22.isPlainObject)(filter2) || (0, import_utils22.isEmpty)(filter2)) return null;
4931
5195
  if (!isAndFilter(filter2)) {
4932
5196
  return filter2;
4933
5197
  }
4934
5198
  return optimizeAndFilter(filter2.$and);
4935
5199
  }
4936
5200
  async function resolveIdentifierFilter(req, idField, resolveIdFilter, id) {
4937
- if ((0, import_utils20.isFunction)(resolveIdFilter)) {
5201
+ if ((0, import_utils22.isFunction)(resolveIdFilter)) {
4938
5202
  return await resolveIdFilter.call(req, id);
4939
5203
  }
4940
- if ((0, import_utils20.isString)(idField)) {
5204
+ if ((0, import_utils22.isString)(idField)) {
4941
5205
  return { [idField]: id };
4942
5206
  }
4943
5207
  return { _id: id };
@@ -4953,12 +5217,12 @@ async function resolveAccessFilter({
4953
5217
  }) {
4954
5218
  let nextFilter = normalizeFilter(filter2);
4955
5219
  const overrideFilterFn = getOption(`overrideFilter.${access}`, null);
4956
- if (nextFilter !== false && (0, import_utils20.isFunction)(overrideFilterFn)) {
5220
+ if (nextFilter !== false && (0, import_utils22.isFunction)(overrideFilterFn)) {
4957
5221
  nextFilter = normalizeFilter(await overrideFilterFn.call(req, nextFilter, permissions));
4958
5222
  }
4959
5223
  if (nextFilter === false) return false;
4960
5224
  const baseFilterFn = getOption(`baseFilter.${access}`, null);
4961
- if (!(0, import_utils20.isFunction)(baseFilterFn)) return nextFilter ?? {};
5225
+ if (!(0, import_utils22.isFunction)(baseFilterFn)) return nextFilter ?? {};
4962
5226
  const baseFilter = normalizeFilter(
4963
5227
  cache.has(cacheKey) ? cache.get(cacheKey) : await baseFilterFn.call(req, permissions)
4964
5228
  );
@@ -4992,34 +5256,34 @@ async function setRequestPermissions(req) {
4992
5256
  if (owner === activeRuntime) return;
4993
5257
  if (hasFieldValue && owner === void 0) return;
4994
5258
  const globalPermissions = getGlobalOption("globalPermissions");
4995
- if (!(0, import_utils20.isFunction)(globalPermissions)) return;
5259
+ if (!(0, import_utils22.isFunction)(globalPermissions)) return;
4996
5260
  const permissions = await globalPermissions.call(req, req);
4997
- if ((0, import_utils20.isPlainObject)(permissions)) req[requestPermissionField] = permissions;
4998
- else if ((0, import_utils20.isArray)(permissions)) req[requestPermissionField] = (0, import_utils20.arrayToRecord)(permissions);
4999
- else if ((0, import_utils20.isString)(permissions)) req[requestPermissionField] = { [permissions]: true };
5261
+ if ((0, import_utils22.isPlainObject)(permissions)) req[requestPermissionField] = permissions;
5262
+ else if ((0, import_utils22.isArray)(permissions)) req[requestPermissionField] = (0, import_utils22.arrayToRecord)(permissions);
5263
+ else if ((0, import_utils22.isString)(permissions)) req[requestPermissionField] = { [permissions]: true };
5000
5264
  owners.set(requestPermissionField, activeRuntime);
5001
5265
  }
5002
5266
  async function evaluateRouteGuard(req, permissions, routeGuard) {
5003
5267
  const phas = (key) => permissions.has(key);
5004
5268
  const { stringHandler, arrayHandler } = createValidator(phas);
5005
- if ((0, import_utils20.isBoolean)(routeGuard)) {
5269
+ if ((0, import_utils22.isBoolean)(routeGuard)) {
5006
5270
  return routeGuard === true;
5007
5271
  }
5008
- if ((0, import_utils20.isString)(routeGuard)) {
5272
+ if ((0, import_utils22.isString)(routeGuard)) {
5009
5273
  return stringHandler(routeGuard);
5010
5274
  }
5011
- if ((0, import_utils20.isArray)(routeGuard)) {
5275
+ if ((0, import_utils22.isArray)(routeGuard)) {
5012
5276
  return arrayHandler(routeGuard);
5013
5277
  }
5014
- if ((0, import_utils20.isFunction)(routeGuard)) {
5278
+ if ((0, import_utils22.isFunction)(routeGuard)) {
5015
5279
  return routeGuard.call(req, permissions);
5016
5280
  }
5017
5281
  return false;
5018
5282
  }
5019
5283
  async function callHookChain(req, hook, doc, permissions, context) {
5020
- const hooks = (0, import_utils20.castArray)(hook);
5284
+ const hooks = (0, import_utils22.castArray)(hook);
5021
5285
  for (let x = 0; x < hooks.length; x++) {
5022
- if ((0, import_utils20.isFunction)(hooks[x])) {
5286
+ if ((0, import_utils22.isFunction)(hooks[x])) {
5023
5287
  doc = await hooks[x].call(req, doc, permissions, context);
5024
5288
  }
5025
5289
  }
@@ -5042,13 +5306,13 @@ async function collectSchemaFields({
5042
5306
  if (baseFields.includes(key)) continue;
5043
5307
  const val = permissionSchema[key];
5044
5308
  const value = val && val[access] || val;
5045
- if ((0, import_utils20.isBoolean)(value)) {
5309
+ if ((0, import_utils22.isBoolean)(value)) {
5046
5310
  if (value) fields.push(key);
5047
- } else if ((0, import_utils20.isString)(value)) {
5311
+ } else if ((0, import_utils22.isString)(value)) {
5048
5312
  if (stringHandler(value)) fields.push(key);
5049
- } else if ((0, import_utils20.isArray)(value)) {
5313
+ } else if ((0, import_utils22.isArray)(value)) {
5050
5314
  if (arrayHandler(value)) fields.push(key);
5051
- } else if ((0, import_utils20.isFunction)(value)) {
5315
+ } else if ((0, import_utils22.isFunction)(value)) {
5052
5316
  if (await value.call(req, ...functionArgs)) fields.push(key);
5053
5317
  }
5054
5318
  }
@@ -5134,7 +5398,7 @@ async function runDecorateAllHook({
5134
5398
  }
5135
5399
 
5136
5400
  // src/acl/select-resolution.ts
5137
- var import_utils21 = require("@web-ts-toolkit/utils");
5401
+ var import_utils23 = require("@web-ts-toolkit/utils");
5138
5402
  async function collectAllowedFieldsForRequest({
5139
5403
  req,
5140
5404
  permissionSchema,
@@ -5202,15 +5466,15 @@ async function resolveSelectForRequest({
5202
5466
  const excludeall = normalizedSelect.every((v) => v.startsWith("-"));
5203
5467
  if (excludeall) {
5204
5468
  normalizedSelect = normalizedSelect.map((v) => v.substring(1));
5205
- fields = (0, import_utils21.difference)(fields, normalizedSelect);
5469
+ fields = (0, import_utils23.difference)(fields, normalizedSelect);
5206
5470
  if (excludeid) fields.push("-_id");
5207
5471
  } else {
5208
- fields = (0, import_utils21.intersection)(normalizedSelect, fields.concat(excludeid ? "-_id" : "_id"));
5472
+ fields = (0, import_utils23.intersection)(normalizedSelect, fields.concat(excludeid ? "-_id" : "_id"));
5209
5473
  }
5210
5474
  }
5211
5475
  return fields.concat(alwaysSelectFields);
5212
5476
  }
5213
- return (0, import_utils21.intersection)(normalizedSelect, fields);
5477
+ return (0, import_utils23.intersection)(normalizedSelect, fields);
5214
5478
  }
5215
5479
 
5216
5480
  // src/core.ts
@@ -5225,10 +5489,10 @@ var Core = class {
5225
5489
  getIdentifier(modelName) {
5226
5490
  const resolveIdFilter = getModelOption(modelName, "resolveIdFilter");
5227
5491
  const idField = getModelOption(modelName, "idField");
5228
- if ((0, import_utils22.isFunction)(resolveIdFilter)) {
5492
+ if ((0, import_utils24.isFunction)(resolveIdFilter)) {
5229
5493
  return null;
5230
5494
  }
5231
- if ((0, import_utils22.isString)(idField)) {
5495
+ if ((0, import_utils24.isString)(idField)) {
5232
5496
  return idField;
5233
5497
  }
5234
5498
  return "_id";
@@ -5306,11 +5570,11 @@ var Core = class {
5306
5570
  async genPopulate(modelName, access = "read", _populate = null, subPaths = []) {
5307
5571
  if (!_populate) return [];
5308
5572
  let populate = Array.isArray(_populate) ? _populate : [_populate];
5309
- populate = (0, import_utils22.compact)(
5573
+ populate = (0, import_utils24.compact)(
5310
5574
  await Promise.all(
5311
5575
  populate.map(async (p) => {
5312
- const populateAccess = !(0, import_utils22.isString)(p) && p.access ? p.access : access;
5313
- const ret = (0, import_utils22.isString)(p) ? { path: p } : {
5576
+ const populateAccess = !(0, import_utils24.isString)(p) && p.access ? p.access : access;
5577
+ const ret = (0, import_utils24.isString)(p) ? { path: p } : {
5314
5578
  path: p.path,
5315
5579
  select: normalizeSelect(p.select)
5316
5580
  };
@@ -5348,10 +5612,10 @@ var Core = class {
5348
5612
  }
5349
5613
  async validate(modelName, allowedData, access, context) {
5350
5614
  const validate = getModelOption(modelName, `validate.${access}`, null);
5351
- if ((0, import_utils22.isFunction)(validate)) {
5615
+ if ((0, import_utils24.isFunction)(validate)) {
5352
5616
  const permissions = this.getGlobalPermissions();
5353
5617
  return validate.call(this.req, allowedData, permissions, context);
5354
- } else if ((0, import_utils22.isBoolean)(validate) || (0, import_utils22.isArray)(validate)) {
5618
+ } else if ((0, import_utils24.isBoolean)(validate) || (0, import_utils24.isArray)(validate)) {
5355
5619
  return validate;
5356
5620
  } else {
5357
5621
  return true;
@@ -5376,7 +5640,7 @@ var Core = class {
5376
5640
  const changeOptions = getModelOption(modelName, `onChange`, {});
5377
5641
  for (let x = 0; x < context.modifiedPaths.length; x++) {
5378
5642
  const mpath = context.modifiedPaths[x];
5379
- if ((0, import_utils22.isFunction)(changeOptions[mpath])) {
5643
+ if ((0, import_utils24.isFunction)(changeOptions[mpath])) {
5380
5644
  await changeOptions[mpath].call(
5381
5645
  this.req,
5382
5646
  context.originalDocumentSnapshot[mpath],
@@ -5400,7 +5664,7 @@ var Core = class {
5400
5664
  async genDocPermissions(modelName, doc, access, context) {
5401
5665
  const docPermissionsFn = getModelOption(modelName, `docPermissions.${access}`, null);
5402
5666
  let docPermissions = {};
5403
- if ((0, import_utils22.isFunction)(docPermissionsFn)) {
5667
+ if ((0, import_utils24.isFunction)(docPermissionsFn)) {
5404
5668
  const permissions = this.getGlobalPermissions();
5405
5669
  try {
5406
5670
  docPermissions = await docPermissionsFn.call(this.req, doc, permissions, context);
@@ -5451,7 +5715,7 @@ var Core = class {
5451
5715
  readExists ? this.genAllowedFields(modelName, doc, "read") : [],
5452
5716
  updateExists ? this.genAllowedFields(modelName, doc, "update") : []
5453
5717
  ]);
5454
- const viewObj = (0, import_utils22.reduce)(
5718
+ const viewObj = (0, import_utils24.reduce)(
5455
5719
  views,
5456
5720
  (ret, view) => {
5457
5721
  ret[view] = true;
@@ -5459,7 +5723,7 @@ var Core = class {
5459
5723
  },
5460
5724
  {}
5461
5725
  );
5462
- const editObj = (0, import_utils22.reduce)(
5726
+ const editObj = (0, import_utils24.reduce)(
5463
5727
  edits,
5464
5728
  (ret, view) => {
5465
5729
  ret[view] = true;
@@ -5483,9 +5747,9 @@ var Core = class {
5483
5747
  return runDecorateAllHook({ req: this.req, hook: decorateAll, docs, permissions, context });
5484
5748
  }
5485
5749
  runTasks(modelName, docObject, task) {
5486
- const tasks = (0, import_utils22.compact)((0, import_utils22.castArray)(task));
5750
+ const tasks = (0, import_utils24.compact)((0, import_utils24.castArray)(task));
5487
5751
  if (tasks.length === 0) return docObject;
5488
- (0, import_utils22.forEach)(tasks, (task2) => {
5752
+ (0, import_utils24.forEach)(tasks, (task2) => {
5489
5753
  const { type, args, options } = task2;
5490
5754
  switch (type) {
5491
5755
  case "COPY_AND_DEPOPULATE":
@@ -5553,9 +5817,9 @@ var Core = class {
5553
5817
  }
5554
5818
  const [, field, op] = keys2;
5555
5819
  const subOption = getExactModelOption(modelName, `operationAccess.${access}`);
5556
- if ((0, import_utils22.isUndefined)(subOption)) {
5820
+ if ((0, import_utils24.isUndefined)(subOption)) {
5557
5821
  const subFieldOption = getExactModelOption(modelName, `operationAccess.subs.${field}`);
5558
- if ((0, import_utils22.isUndefined)(subFieldOption)) {
5822
+ if ((0, import_utils24.isUndefined)(subFieldOption)) {
5559
5823
  const opOption = getModelOption(modelName, `operationAccess.${op}`);
5560
5824
  return this.canActivate(opOption);
5561
5825
  }
@@ -5607,25 +5871,25 @@ function guard(condition) {
5607
5871
  const permissions = req[PERMISSIONS];
5608
5872
  let cond = condition;
5609
5873
  let phas = (key) => permissions.has(key);
5610
- if ((0, import_utils23.isPlainObject)(condition)) {
5874
+ if ((0, import_utils25.isPlainObject)(condition)) {
5611
5875
  const { modelName, id, condition: _cond } = condition;
5612
5876
  const svc = req.macl.getPublicService(modelName);
5613
5877
  const select = getModelOption(modelName, `alwaysSelectFields.read`, void 0);
5614
- let _id = (0, import_utils23.isString)(id) ? id : null;
5615
- if ((0, import_utils23.isPlainObject)(id)) {
5878
+ let _id = (0, import_utils25.isString)(id) ? id : null;
5879
+ if ((0, import_utils25.isPlainObject)(id)) {
5616
5880
  const { type = "param", key } = id;
5617
5881
  if (type === "param") {
5618
5882
  const paramValue = req.params[key];
5619
- _id = (0, import_utils23.isArray)(paramValue) ? paramValue[0] ?? null : paramValue ?? null;
5883
+ _id = (0, import_utils25.isArray)(paramValue) ? paramValue[0] ?? null : paramValue ?? null;
5620
5884
  } else if (type === "query") {
5621
5885
  const _qval = req.query[key];
5622
- if ((0, import_utils23.isArray)(_qval)) _id = _qval.length > 0 ? _qval[0] : null;
5886
+ if ((0, import_utils25.isArray)(_qval)) _id = _qval.length > 0 ? _qval[0] : null;
5623
5887
  else _id = _qval;
5624
5888
  } else {
5625
5889
  _id = null;
5626
5890
  }
5627
5891
  }
5628
- if (!(0, import_utils23.isString)(_id) || !_id) {
5892
+ if (!(0, import_utils25.isString)(_id) || !_id) {
5629
5893
  return next(new import_express_json_router2.default.clientErrors.BadRequestError());
5630
5894
  }
5631
5895
  const result = await svc._read(_id, { select });
@@ -5637,11 +5901,11 @@ function guard(condition) {
5637
5901
  cond = _cond;
5638
5902
  }
5639
5903
  const { stringHandler, arrayHandler } = createValidator(phas);
5640
- if ((0, import_utils23.isString)(cond)) {
5904
+ if ((0, import_utils25.isString)(cond)) {
5641
5905
  if (stringHandler(cond)) return next();
5642
- } else if ((0, import_utils23.isArray)(cond)) {
5906
+ } else if ((0, import_utils25.isArray)(cond)) {
5643
5907
  if (arrayHandler(cond)) return next();
5644
- } else if ((0, import_utils23.isFunction)(cond)) {
5908
+ } else if ((0, import_utils25.isFunction)(cond)) {
5645
5909
  const result = await cond.call(req, permissions);
5646
5910
  if (!!result) return next();
5647
5911
  }
@@ -5654,7 +5918,7 @@ var import_express2 = __toESM(require("express"));
5654
5918
 
5655
5919
  // src/routers/model-router.ts
5656
5920
  var import_express_json_router5 = __toESM(require("@web-ts-toolkit/express-json-router"));
5657
- var import_utils27 = require("@web-ts-toolkit/utils");
5921
+ var import_utils29 = require("@web-ts-toolkit/utils");
5658
5922
 
5659
5923
  // src/routers/router-mutation.ts
5660
5924
  var MODEL_BUILD_TIME_OPTION_KEYS = /* @__PURE__ */ new Set([
@@ -5783,7 +6047,7 @@ var import_express_json_router4 = __toESM(require("@web-ts-toolkit/express-json-
5783
6047
 
5784
6048
  // src/validation/common.ts
5785
6049
  var import_zod2 = require("zod");
5786
- var import_utils24 = require("@web-ts-toolkit/utils");
6050
+ var import_utils26 = require("@web-ts-toolkit/utils");
5787
6051
  var stringOrStringArray = import_zod2.z.union([import_zod2.z.string(), import_zod2.z.array(import_zod2.z.string())]);
5788
6052
  var safeIntegerString = (min, message) => import_zod2.z.string().regex(/^\d+$/, message).refine((value) => Number.isSafeInteger(Number(value)), "Expected a safe integer").refine((value) => Number(value) >= min, message);
5789
6053
  var queryBooleanString = import_zod2.z.enum(["true", "false"]);
@@ -5883,7 +6147,7 @@ var correlatedFilterSchema = import_zod2.z.record(import_zod2.z.string(), import
5883
6147
  }
5884
6148
  });
5885
6149
  var correlatedEmptyOptionsSchema = import_zod2.z.unknown().refine(
5886
- (value) => value === void 0 || (0, import_utils24.isPlainObject)(value) && Object.keys(value).length === 0,
6150
+ (value) => value === void 0 || (0, import_utils26.isPlainObject)(value) && Object.keys(value).length === 0,
5887
6151
  "Correlated include options must be absent or empty"
5888
6152
  ).optional();
5889
6153
  var correlatedPathSchema = import_zod2.z.string().min(1).superRefine((path, ctx) => {
@@ -5920,7 +6184,7 @@ var correlatedListArgsSchema = import_zod2.z.object({
5920
6184
  include: import_zod2.z.lazy(() => correlatedNestedIncludeSchema).optional()
5921
6185
  }).strict().superRefine((args, ctx) => checkArgsMarkers(args.select, args.sort, ctx));
5922
6186
  var correlatedCountArgsSchema = import_zod2.z.unknown().refine(
5923
- (value) => value === void 0 || (0, import_utils24.isPlainObject)(value) && Object.keys(value).length === 0,
6187
+ (value) => value === void 0 || (0, import_utils26.isPlainObject)(value) && Object.keys(value).length === 0,
5924
6188
  "Count includes accept no args"
5925
6189
  ).optional();
5926
6190
  var correlatedReadByIdSchema = import_zod2.z.object({
@@ -5970,7 +6234,7 @@ var correlatedNestedIncludeSchema = import_zod2.z.union([includeItemSchema, impo
5970
6234
  var rejectDuplicateCorrelatedPaths = (items, ctx) => {
5971
6235
  const seen = /* @__PURE__ */ new Set();
5972
6236
  items.forEach((item, index) => {
5973
- if ((0, import_utils24.isPlainObject)(item) && item.mode === "correlated" && typeof item.path === "string") {
6237
+ if ((0, import_utils26.isPlainObject)(item) && item.mode === "correlated" && typeof item.path === "string") {
5974
6238
  if (seen.has(item.path)) {
5975
6239
  ctx.addIssue({
5976
6240
  code: import_zod2.z.ZodIssueCode.custom,
@@ -6169,19 +6433,12 @@ function fromJoi(schema) {
6169
6433
  }
6170
6434
  function fromAjv(validator) {
6171
6435
  return async (value) => {
6172
- const outcome = validator(value);
6173
- if (isThenable(outcome)) {
6436
+ if (validator.$async === true) {
6174
6437
  try {
6175
- const data = await outcome;
6176
- if (data === false) {
6177
- return {
6178
- success: false,
6179
- issues: normalizeAjvIssues(validator.errors ?? [])
6180
- };
6181
- }
6438
+ const data = await validator(value);
6182
6439
  return {
6183
6440
  success: true,
6184
- data: data === true ? value : data
6441
+ data
6185
6442
  };
6186
6443
  } catch (error2) {
6187
6444
  if (!isAjvValidationError(error2)) {
@@ -6189,11 +6446,21 @@ function fromAjv(validator) {
6189
6446
  }
6190
6447
  return {
6191
6448
  success: false,
6192
- issues: normalizeAjvIssues(error2.errors ?? [])
6449
+ issues: normalizeAjvIssues(error2.errors)
6193
6450
  };
6194
6451
  }
6195
6452
  }
6196
- const errorsSnapshot = validator.errors ? [...validator.errors] : [];
6453
+ const outcome = validator(value);
6454
+ if (isThenable(outcome)) {
6455
+ void Promise.resolve(outcome).catch(() => {
6456
+ });
6457
+ throw new TypeError(
6458
+ "fromAjv: promise/thenable validators must declare $async: true and resolve with validated data"
6459
+ );
6460
+ }
6461
+ if (typeof outcome !== "boolean") {
6462
+ throw new TypeError("fromAjv: synchronous validators must return a boolean verdict");
6463
+ }
6197
6464
  if (outcome) {
6198
6465
  return {
6199
6466
  success: true,
@@ -6202,7 +6469,9 @@ function fromAjv(validator) {
6202
6469
  }
6203
6470
  return {
6204
6471
  success: false,
6205
- issues: normalizeAjvIssues(errorsSnapshot)
6472
+ // Normalize immediately, copying message/path before another call can
6473
+ // replace the errors array or mutate its entries.
6474
+ issues: normalizeAjvIssues(validator.errors ?? [])
6206
6475
  };
6207
6476
  };
6208
6477
  }
@@ -6432,14 +6701,14 @@ function isArkTypeErrors(value) {
6432
6701
  return record.arkKind === "errors" || record[" arkKind"] === "errors";
6433
6702
  }
6434
6703
  function isThenable(value) {
6435
- return typeof value === "object" && value !== null && "then" in value && typeof value.then === "function";
6704
+ return (typeof value === "object" || typeof value === "function") && value !== null && "then" in value && typeof value.then === "function";
6436
6705
  }
6437
6706
  function isAjvValidationError(error2) {
6438
6707
  if (typeof error2 !== "object" || error2 === null || !("errors" in error2)) {
6439
6708
  return false;
6440
6709
  }
6441
- const errors = error2.errors;
6442
- return errors == null || Array.isArray(errors);
6710
+ const candidate = error2;
6711
+ return candidate.ajv === true && candidate.validation === true && Array.isArray(candidate.errors);
6443
6712
  }
6444
6713
  function isVineValidationError(error2) {
6445
6714
  if (typeof error2 !== "object" || error2 === null || !("messages" in error2)) {
@@ -6878,7 +7147,7 @@ var rootDataQueryEntrySchema = import_zod5.z.union([
6878
7147
  var rootQuerySchema = import_zod5.z.array(import_zod5.z.union([rootModelQueryEntrySchema, rootDataQueryEntrySchema]));
6879
7148
 
6880
7149
  // src/routers/shared.ts
6881
- var import_utils25 = require("@web-ts-toolkit/utils");
7150
+ var import_utils27 = require("@web-ts-toolkit/utils");
6882
7151
 
6883
7152
  // src/routers/model-router-collection-routes.ts
6884
7153
  function setModelCollectionRoutes(context) {
@@ -6896,14 +7165,14 @@ function setModelCollectionRoutes(context) {
6896
7165
  await context.assertAllowed(req, "list");
6897
7166
  const { skip, limit, page, page_size, skim, include_permissions, include_count, include_extra_headers } = parseQuery(requestSchemas.listQuery, req.query);
6898
7167
  const svc = context.getPublicService(req);
6899
- const includeCount = (0, import_utils25.parseBooleanString)(include_count);
6900
- const includeExtraHeaders = (0, import_utils25.parseBooleanString)(include_extra_headers);
7168
+ const includeCount = (0, import_utils27.parseBooleanString)(include_count);
7169
+ const includeExtraHeaders = (0, import_utils27.parseBooleanString)(include_extra_headers);
6901
7170
  const result = await svc._list(
6902
7171
  {},
6903
7172
  { skip, limit, page, pageSize: page_size },
6904
7173
  {
6905
- skim: (0, import_utils25.parseBooleanString)(skim),
6906
- includePermissions: (0, import_utils25.parseBooleanString)(include_permissions),
7174
+ skim: (0, import_utils27.parseBooleanString)(skim),
7175
+ includePermissions: (0, import_utils27.parseBooleanString)(include_permissions),
6907
7176
  includeCount
6908
7177
  }
6909
7178
  );
@@ -6958,7 +7227,7 @@ function setModelCollectionRoutes(context) {
6958
7227
  context.getRequestSchema("requestSchemas.create")
6959
7228
  );
6960
7229
  const svc = context.getPublicService(req);
6961
- const result = await svc._create(data, {}, { includePermissions: (0, import_utils25.parseBooleanString)(include_permissions) });
7230
+ const result = await svc._create(data, {}, { includePermissions: (0, import_utils27.parseBooleanString)(include_permissions) });
6962
7231
  handleResultError(result);
6963
7232
  return formatModelCreatedResponse(result);
6964
7233
  });
@@ -6994,7 +7263,7 @@ function setModelCollectionRoutes(context) {
6994
7263
  finalData,
6995
7264
  { select: finalSelect, populate: finalPopulate, tasks: finalTasks },
6996
7265
  {
6997
- includePermissions: includePermissions ?? (0, import_utils25.parseBooleanString)(include_permissions),
7266
+ includePermissions: includePermissions ?? (0, import_utils27.parseBooleanString)(include_permissions),
6998
7267
  populateAccess
6999
7268
  }
7000
7269
  );
@@ -7086,8 +7355,8 @@ function setModelDocumentRoutes(context) {
7086
7355
  id,
7087
7356
  {},
7088
7357
  {
7089
- includePermissions: (0, import_utils25.parseBooleanString)(include_permissions),
7090
- tryList: (0, import_utils25.parseBooleanString)(try_list)
7358
+ includePermissions: (0, import_utils27.parseBooleanString)(include_permissions),
7359
+ tryList: (0, import_utils27.parseBooleanString)(try_list)
7091
7360
  }
7092
7361
  );
7093
7362
  handleResultError(result);
@@ -7172,8 +7441,8 @@ function setModelDocumentRoutes(context) {
7172
7441
  data,
7173
7442
  {},
7174
7443
  {
7175
- returningAll: (0, import_utils25.parseBooleanString)(returning_all),
7176
- includePermissions: (0, import_utils25.parseBooleanString)(include_permissions)
7444
+ returningAll: (0, import_utils27.parseBooleanString)(returning_all),
7445
+ includePermissions: (0, import_utils27.parseBooleanString)(include_permissions)
7177
7446
  }
7178
7447
  );
7179
7448
  handleResultError(result);
@@ -7219,8 +7488,8 @@ function setModelDocumentRoutes(context) {
7219
7488
  finalData,
7220
7489
  { select: finalSelect, populate: finalPopulate, tasks: finalTasks },
7221
7490
  {
7222
- returningAll: returningAll ?? (0, import_utils25.parseBooleanString)(returning_all),
7223
- includePermissions: includePermissions ?? (0, import_utils25.parseBooleanString)(include_permissions),
7491
+ returningAll: returningAll ?? (0, import_utils27.parseBooleanString)(returning_all),
7492
+ includePermissions: includePermissions ?? (0, import_utils27.parseBooleanString)(include_permissions),
7224
7493
  populateAccess
7225
7494
  }
7226
7495
  );
@@ -7248,8 +7517,8 @@ function setModelDocumentRoutes(context) {
7248
7517
  body,
7249
7518
  {},
7250
7519
  {
7251
- returningAll: (0, import_utils25.parseBooleanString)(returning_all),
7252
- includePermissions: (0, import_utils25.parseBooleanString)(include_permissions)
7520
+ returningAll: (0, import_utils27.parseBooleanString)(returning_all),
7521
+ includePermissions: (0, import_utils27.parseBooleanString)(include_permissions)
7253
7522
  }
7254
7523
  );
7255
7524
  handleResultError(result);
@@ -7293,8 +7562,8 @@ function setModelDocumentRoutes(context) {
7293
7562
  finalData,
7294
7563
  { select: finalSelect, populate: finalPopulate, tasks: finalTasks },
7295
7564
  {
7296
- returningAll: returningAll ?? (0, import_utils25.parseBooleanString)(returning_all),
7297
- includePermissions: includePermissions ?? (0, import_utils25.parseBooleanString)(include_permissions),
7565
+ returningAll: returningAll ?? (0, import_utils27.parseBooleanString)(returning_all),
7566
+ includePermissions: includePermissions ?? (0, import_utils27.parseBooleanString)(include_permissions),
7298
7567
  populateAccess
7299
7568
  }
7300
7569
  );
@@ -7532,11 +7801,11 @@ function setModelSubDocumentRoutes(context) {
7532
7801
  }
7533
7802
 
7534
7803
  // src/openapi/route-registration.ts
7535
- var import_utils26 = require("@web-ts-toolkit/utils");
7804
+ var import_utils28 = require("@web-ts-toolkit/utils");
7536
7805
  function registerOpenApiRoute(runtime, basePath, defaultTag, route) {
7537
7806
  runtime.registerOpenApiRoute({
7538
7807
  ...route,
7539
- path: (0, import_utils26.normalizeUrlPath)(basePath + route.path),
7808
+ path: (0, import_utils28.normalizeUrlPath)(basePath + route.path),
7540
7809
  tags: route.tags ?? [defaultTag]
7541
7810
  });
7542
7811
  }
@@ -7544,8 +7813,8 @@ function registerOpenApiRoute(runtime, basePath, defaultTag, route) {
7544
7813
  // src/routers/model-router.ts
7545
7814
  var clientErrors2 = import_express_json_router5.default.clientErrors;
7546
7815
  function setOption(parentKey, optionKey, option) {
7547
- const key = (0, import_utils27.isUndefined)(option) ? parentKey : `${parentKey}.${optionKey}`;
7548
- const value = (0, import_utils27.isUndefined)(option) ? optionKey : option;
7816
+ const key = (0, import_utils29.isUndefined)(option) ? parentKey : `${parentKey}.${optionKey}`;
7817
+ const value = (0, import_utils29.isUndefined)(option) ? optionKey : option;
7549
7818
  assertMutableRouterOption("model", key);
7550
7819
  this.runtime.setModelOption(this.modelName, key, value);
7551
7820
  return this;
@@ -7707,7 +7976,7 @@ var ModelRouter = class _ModelRouter {
7707
7976
  this.runtime = runtime;
7708
7977
  this.runtime.setModelOptions(modelName, initialOptions);
7709
7978
  this.options = this.runtime.getModelOptions(modelName);
7710
- this.fullBasePath = (0, import_utils27.normalizeUrlPath)(this.options.parentPath + this.options.basePath);
7979
+ this.fullBasePath = (0, import_utils29.normalizeUrlPath)(this.options.parentPath + this.options.basePath);
7711
7980
  this.modelName = modelName;
7712
7981
  this.router = new import_express_json_router5.default(this.options.basePath, createSetCore(this.runtime), accessRouterResponseHandler);
7713
7982
  this.model = new model_default(modelName, this.runtime);
@@ -7787,13 +8056,13 @@ var ModelRouter = class _ModelRouter {
7787
8056
  }
7788
8057
  logEndpoints() {
7789
8058
  runWithRuntime(this.runtime, () => {
7790
- (0, import_utils27.forEach)(this.router.getEndpoints(), ({ method, path }) => {
7791
- logInfoMessage(`${(0, import_utils27.padEnd)(method, 6)} ${(0, import_utils27.normalizeUrlPath)(this.options.parentPath + path)}`);
8059
+ (0, import_utils29.forEach)(this.router.getEndpoints(), ({ method, path }) => {
8060
+ logInfoMessage(`${(0, import_utils29.padEnd)(method, 6)} ${(0, import_utils29.normalizeUrlPath)(this.options.parentPath + path)}`);
7792
8061
  });
7793
8062
  });
7794
8063
  }
7795
8064
  set(keyOrOptions, value) {
7796
- if (arguments.length === 2 && (0, import_utils27.isString)(keyOrOptions)) {
8065
+ if (arguments.length === 2 && (0, import_utils29.isString)(keyOrOptions)) {
7797
8066
  assertMutableRouterOption("model", keyOrOptions);
7798
8067
  this.runtime.setModelOption(
7799
8068
  this.modelName,
@@ -7801,7 +8070,7 @@ var ModelRouter = class _ModelRouter {
7801
8070
  value
7802
8071
  );
7803
8072
  }
7804
- if (arguments.length === 1 && (0, import_utils27.isPlainObject)(keyOrOptions)) {
8073
+ if (arguments.length === 1 && (0, import_utils29.isPlainObject)(keyOrOptions)) {
7805
8074
  assertMutableRouterOptions("model", keyOrOptions);
7806
8075
  this.runtime.setModelOptions(this.modelName, keyOrOptions);
7807
8076
  }
@@ -7824,7 +8093,7 @@ var ModelRouter = class _ModelRouter {
7824
8093
 
7825
8094
  // src/routers/root-router.ts
7826
8095
  var import_express_json_router6 = __toESM(require("@web-ts-toolkit/express-json-router"));
7827
- var import_utils28 = require("@web-ts-toolkit/utils");
8096
+ var import_utils30 = require("@web-ts-toolkit/utils");
7828
8097
 
7829
8098
  // src/core-data.ts
7830
8099
  var DataCore = class {
@@ -8090,7 +8359,7 @@ var RootRouter = class {
8090
8359
  this.setRoutes();
8091
8360
  }
8092
8361
  registerOpenApiRoute(route) {
8093
- registerOpenApiRoute(this.runtime, (0, import_utils28.normalizeUrlPath)(this.basename), "root", route);
8362
+ registerOpenApiRoute(this.runtime, (0, import_utils30.normalizeUrlPath)(this.basename), "root", route);
8094
8363
  }
8095
8364
  wrapResult(index, item, result) {
8096
8365
  return {
@@ -8151,7 +8420,7 @@ var RootRouter = class {
8151
8420
  groupItemsByOrder(items) {
8152
8421
  const groups = /* @__PURE__ */ new Map();
8153
8422
  items.forEach((item, index) => {
8154
- const order = (0, import_utils28.isNumber)(item.order) ? item.order : 0;
8423
+ const order = (0, import_utils30.isNumber)(item.order) ? item.order : 0;
8155
8424
  const group = groups.get(order);
8156
8425
  if (group) {
8157
8426
  group.push({ index, item });
@@ -8222,7 +8491,7 @@ var RootRouter = class {
8222
8491
  const arrResult = await this.runGroup(req, groupedItems[x]);
8223
8492
  results.push(...arrResult);
8224
8493
  }
8225
- return (0, import_utils28.orderBy)(results, ["index"], ["asc"]);
8494
+ return (0, import_utils30.orderBy)(results, ["index"], ["asc"]);
8226
8495
  });
8227
8496
  this.registerOpenApiRoute({
8228
8497
  method: "post",
@@ -8240,11 +8509,11 @@ var RootRouter = class {
8240
8509
 
8241
8510
  // src/routers/data-router.ts
8242
8511
  var import_express_json_router7 = __toESM(require("@web-ts-toolkit/express-json-router"));
8243
- var import_utils29 = require("@web-ts-toolkit/utils");
8512
+ var import_utils31 = require("@web-ts-toolkit/utils");
8244
8513
  var clientErrors4 = import_express_json_router7.default.clientErrors;
8245
8514
  function setOption2(parentKey, optionKey, option) {
8246
- const key = (0, import_utils29.isUndefined)(option) ? parentKey : `${parentKey}.${optionKey}`;
8247
- const value = (0, import_utils29.isUndefined)(option) ? optionKey : option;
8515
+ const key = (0, import_utils31.isUndefined)(option) ? parentKey : `${parentKey}.${optionKey}`;
8516
+ const value = (0, import_utils31.isUndefined)(option) ? optionKey : option;
8248
8517
  assertMutableRouterOption("data", key);
8249
8518
  this.runtime.setDataOption(this.dataName, key, value);
8250
8519
  return this;
@@ -8285,7 +8554,7 @@ var DataRouter = class {
8285
8554
  this.runtime = runtime;
8286
8555
  this.runtime.setDataOptions(dataName, initialOptions);
8287
8556
  this.options = this.runtime.getDataOptions(dataName);
8288
- this.fullBasePath = (0, import_utils29.normalizeUrlPath)(this.options.parentPath + this.options.basePath);
8557
+ this.fullBasePath = (0, import_utils31.normalizeUrlPath)(this.options.parentPath + this.options.basePath);
8289
8558
  this.dataName = dataName;
8290
8559
  this.router = new import_express_json_router7.default(this.options.basePath, createSetDataCore(this.runtime), accessRouterResponseHandler);
8291
8560
  this.setCollectionRoutes();
@@ -8318,8 +8587,8 @@ var DataRouter = class {
8318
8587
  req.query
8319
8588
  );
8320
8589
  const svc = this.getService(req);
8321
- const includeCount = (0, import_utils25.parseBooleanString)(include_count);
8322
- const includeExtraHeaders = (0, import_utils25.parseBooleanString)(include_extra_headers);
8590
+ const includeCount = (0, import_utils27.parseBooleanString)(include_count);
8591
+ const includeExtraHeaders = (0, import_utils27.parseBooleanString)(include_extra_headers);
8323
8592
  const result = await svc.find(
8324
8593
  {},
8325
8594
  { skip, limit, page, pageSize: page_size },
@@ -8437,11 +8706,11 @@ var DataRouter = class {
8437
8706
  });
8438
8707
  }
8439
8708
  set(keyOrOptions, value) {
8440
- if (arguments.length === 2 && (0, import_utils29.isString)(keyOrOptions)) {
8709
+ if (arguments.length === 2 && (0, import_utils31.isString)(keyOrOptions)) {
8441
8710
  assertMutableRouterOption("data", keyOrOptions);
8442
8711
  this.runtime.setDataOption(this.dataName, keyOrOptions, value);
8443
8712
  }
8444
- if (arguments.length === 1 && (0, import_utils29.isPlainObject)(keyOrOptions)) {
8713
+ if (arguments.length === 1 && (0, import_utils31.isPlainObject)(keyOrOptions)) {
8445
8714
  assertMutableRouterOptions("data", keyOrOptions);
8446
8715
  this.runtime.setDataOptions(this.dataName, keyOrOptions);
8447
8716
  }
@@ -8564,7 +8833,7 @@ function createRuntimeApi(runtime) {
8564
8833
  const wtt2 = runtimeMiddleware;
8565
8834
  wtt2.runtime = runtime;
8566
8835
  wtt2.createRouter = function(modelName, options) {
8567
- if ((0, import_utils30.isUndefined)(options)) {
8836
+ if ((0, import_utils32.isUndefined)(options)) {
8568
8837
  return new RootRouter(modelName, runtime);
8569
8838
  }
8570
8839
  if (typeof modelName === "string") {
@@ -8585,10 +8854,10 @@ function createRuntimeApi(runtime) {
8585
8854
  };
8586
8855
  wtt2.combineRoutes = combineRoutes;
8587
8856
  wtt2.set = function(keyOrOptions, value) {
8588
- if (arguments.length === 2 && (0, import_utils30.isString)(keyOrOptions)) {
8857
+ if (arguments.length === 2 && (0, import_utils32.isString)(keyOrOptions)) {
8589
8858
  return runtime.setGlobalOption(keyOrOptions, value);
8590
8859
  }
8591
- if (arguments.length === 1 && (0, import_utils30.isPlainObject)(keyOrOptions)) {
8860
+ if (arguments.length === 1 && (0, import_utils32.isPlainObject)(keyOrOptions)) {
8592
8861
  return runtime.setGlobalOptions(keyOrOptions);
8593
8862
  }
8594
8863
  };