@web-ts-toolkit/access-router 0.37.0 → 0.39.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
@@ -104,12 +104,42 @@ var findElementById = (collection, id) => {
104
104
  return findElement(collection, { _id: id });
105
105
  };
106
106
 
107
+ // src/helpers/concurrency.ts
108
+ var mapWithConcurrencyLimit = async (items, limit, iteratee) => {
109
+ const results = new Array(items.length);
110
+ let cursor = 0;
111
+ const workerCount = Math.min(Math.max(limit, 1), items.length || 1);
112
+ await Promise.all(
113
+ Array.from({ length: workerCount }, async () => {
114
+ while (cursor < items.length) {
115
+ const current = cursor;
116
+ cursor += 1;
117
+ results[current] = await iteratee(items[current], current);
118
+ }
119
+ })
120
+ );
121
+ return results;
122
+ };
123
+ var RequestConcurrencyScheduler = class {
124
+ constructor(limit) {
125
+ this.limit = Math.max(1, limit);
126
+ }
127
+ async map(items, iteratee) {
128
+ return mapWithConcurrencyLimit(items, this.limit, (item, index) => iteratee(item, index, true));
129
+ }
130
+ async run(iteratee) {
131
+ const [result] = await this.map([void 0], () => iteratee(true));
132
+ return result;
133
+ }
134
+ };
135
+
107
136
  // src/helpers/document.ts
108
137
  var import_mongoose = require("mongoose");
109
138
  var import_utils3 = require("@web-ts-toolkit/utils");
110
139
 
111
140
  // src/helpers/query.ts
112
141
  var import_utils2 = require("@web-ts-toolkit/utils");
142
+ var DEFAULT_LIST_HARD_LIMIT = 1e3;
113
143
  var normalizeSafeInteger = (value, min) => {
114
144
  if ((0, import_utils2.isNil)(value)) return null;
115
145
  const parsed = Number(value);
@@ -122,9 +152,10 @@ function genPagination({
122
152
  page,
123
153
  pageSize
124
154
  }, hardLimit) {
155
+ const resolvedHardLimit = normalizeSafeInteger(hardLimit, 1) ?? DEFAULT_LIST_HARD_LIMIT;
125
156
  let _skip = 0;
126
- let _limit = normalizeSafeInteger(limit ?? pageSize, 1) ?? hardLimit;
127
- if (!Number.isSafeInteger(_limit) || _limit > hardLimit) _limit = hardLimit;
157
+ let _limit = normalizeSafeInteger(limit ?? pageSize, 1) ?? resolvedHardLimit;
158
+ if (!Number.isSafeInteger(_limit) || _limit > resolvedHardLimit) _limit = resolvedHardLimit;
128
159
  const normalizedSkip = normalizeSafeInteger(skip, 0);
129
160
  const normalizedPage = normalizeSafeInteger(page, 0);
130
161
  if (normalizedSkip !== null) {
@@ -141,14 +172,6 @@ function genPagination({
141
172
  }
142
173
  return { skip: _skip, limit: _limit };
143
174
  }
144
- function parseSortString(sortString) {
145
- if (!sortString) return { sortKey: "", sortOrder: "asc" };
146
- if (sortString.startsWith("-")) {
147
- return { sortKey: sortString.substring(1), sortOrder: "desc" };
148
- } else {
149
- return { sortKey: sortString, sortOrder: "asc" };
150
- }
151
- }
152
175
  function normalizeSelect(select) {
153
176
  if (Array.isArray(select)) return (0, import_utils2.flattenDeep)(select.map(normalizeSelect));
154
177
  if ((0, import_utils2.isPlainObject)(select)) {
@@ -278,6 +301,61 @@ function handleResultError(result) {
278
301
  }
279
302
  }
280
303
 
304
+ // src/helpers/sort-policy.ts
305
+ var validSortOrders = [1, -1, "asc", "ascending", "desc", "descending"];
306
+ var fieldPathPattern = /^[A-Za-z_][A-Za-z0-9_$]*(\.[A-Za-z_][A-Za-z0-9_$]*)*$/;
307
+ var sortDirection = (order) => order === -1 || order === "desc" || order === "descending" ? "desc" : "asc";
308
+ var sortError = (detail) => ({ detail, pointer: "#/sort" });
309
+ function isValidFieldPath(field) {
310
+ return typeof field === "string" && fieldPathPattern.test(field);
311
+ }
312
+ function isFieldAllowed(field, allowedFields) {
313
+ return new Set(allowedFields.concat(["id", "_id"])).has(field);
314
+ }
315
+ function normalizeSort(sort) {
316
+ if (sort === null || sort === void 0 || sort === "") return { fields: [], errors: [] };
317
+ if (typeof sort === "string") {
318
+ const fields2 = sort.trim().split(/\s+/).filter(Boolean).map((raw) => {
319
+ const desc = raw.startsWith("-");
320
+ return { raw, field: desc ? raw.slice(1) : raw, direction: desc ? "desc" : "asc" };
321
+ });
322
+ const errors2 = fields2.filter(({ field }) => !isValidFieldPath(field)).map(({ raw }) => sortError(`Invalid sort field: ${raw}`));
323
+ return { fields: errors2.length > 0 ? [] : fields2.map(({ field, direction }) => ({ field, direction })), errors: errors2 };
324
+ }
325
+ const entries = sort instanceof Map ? Array.from(sort.entries()) : Array.isArray(sort) ? sort : Object.entries(sort);
326
+ const errors = [];
327
+ const fields = [];
328
+ for (const entry of entries) {
329
+ if (!Array.isArray(entry) || entry.length !== 2) {
330
+ errors.push(sortError("Invalid sort entry: expected [field, order]"));
331
+ continue;
332
+ }
333
+ const [field, order] = entry;
334
+ if (!isValidFieldPath(field)) {
335
+ errors.push(sortError(`Invalid sort field: ${String(field)}`));
336
+ continue;
337
+ }
338
+ if (!validSortOrders.includes(order)) {
339
+ errors.push(sortError(`Invalid sort order for field: ${field}`));
340
+ continue;
341
+ }
342
+ fields.push({ field, direction: sortDirection(order) });
343
+ }
344
+ return { fields: errors.length > 0 ? [] : fields, errors };
345
+ }
346
+ function validateSortFields(sort, allowedFields) {
347
+ const { fields, errors } = normalizeSort(sort);
348
+ if (errors.length > 0) return errors;
349
+ return fields.filter(({ field }) => !isFieldAllowed(field, allowedFields)).map(({ field }) => sortError(`Sort field is not allowed: ${field}`));
350
+ }
351
+ function normalizeSortForOrderBy(sort) {
352
+ const normalized = normalizeSort(sort);
353
+ return {
354
+ fields: normalized.fields.map(({ field }) => field),
355
+ orders: normalized.fields.map(({ direction }) => direction)
356
+ };
357
+ }
358
+
281
359
  // src/helpers/index.ts
282
360
  var import_utils6 = require("@web-ts-toolkit/utils");
283
361
 
@@ -339,26 +417,33 @@ function buildSubPaths(schema) {
339
417
  });
340
418
  return subPaths;
341
419
  }
342
- async function iterateQuery(query, handler) {
420
+ async function iterateQuery(query, handler, scheduler, scheduled = false) {
343
421
  if (!(0, import_utils6.isPlainObject)(query)) return query;
344
422
  if (!handler) return query;
345
423
  const queryObject = query;
346
- return (0, import_utils6.mapValuesAsync)(queryObject, async (val, key) => {
424
+ const entries = Object.entries(queryObject);
425
+ const mapEntry = async ([key, val]) => {
347
426
  if ((0, import_utils6.isPlainObject)(val)) {
348
427
  const plainValue = val;
349
428
  if (plainValue.$$sq) {
350
- return handler(0 /* SubQuery */, plainValue.$$sq, key);
429
+ return scheduled || !scheduler ? handler(0 /* SubQuery */, plainValue.$$sq, key) : scheduler.run(() => Promise.resolve(handler(0 /* SubQuery */, plainValue.$$sq, key)));
351
430
  } else if (plainValue.$$date) {
352
431
  return handler(1 /* Date */, plainValue.$$date, key);
353
432
  } else {
354
- return iterateQuery(val, handler);
433
+ return iterateQuery(val, handler, scheduler, scheduled);
355
434
  }
356
435
  }
357
436
  if ((0, import_utils6.isArray)(val)) {
358
- return Promise.all(val.map((v) => iterateQuery(v, handler)));
437
+ const items = val;
438
+ if (scheduler && !scheduled) {
439
+ return scheduler.map(items, (v) => iterateQuery(v, handler, scheduler, true));
440
+ }
441
+ return Promise.all(items.map((v) => iterateQuery(v, handler, scheduler, scheduled)));
359
442
  }
360
443
  return val;
361
- });
444
+ };
445
+ const values = scheduler && !scheduled ? await scheduler.map(entries, mapEntry) : await Promise.all(entries.map(mapEntry));
446
+ return Object.fromEntries(entries.map(([key], index) => [key, values[index]]));
362
447
  }
363
448
  var createValidator = (fn) => {
364
449
  const stringHandler = (key) => key.trim().split(" ").every((v) => fn(v));
@@ -724,6 +809,42 @@ var defaultLogger = (0, import_winston.createLogger)({
724
809
 
725
810
  // src/options/manager.ts
726
811
  var import_utils9 = require("@web-ts-toolkit/utils");
812
+ var toPath = (key) => {
813
+ if (Array.isArray(key)) return key;
814
+ return typeof key === "string" ? key.split(".") : [key];
815
+ };
816
+ var cloneConfigValue = (value) => {
817
+ if (Array.isArray(value)) {
818
+ return value.map((item) => cloneConfigValue(item));
819
+ }
820
+ if (!value || typeof value !== "object") {
821
+ return value;
822
+ }
823
+ if (Object.getPrototypeOf(value) !== Object.prototype) {
824
+ return value;
825
+ }
826
+ return Object.fromEntries(
827
+ Object.entries(value).map(([entryKey, entryValue]) => [
828
+ entryKey,
829
+ cloneConfigValue(entryValue)
830
+ ])
831
+ );
832
+ };
833
+ var freezeConfigValue = (value) => {
834
+ if (!value || typeof value !== "object") {
835
+ return value;
836
+ }
837
+ if (Object.isFrozen(value)) {
838
+ return value;
839
+ }
840
+ if (Array.isArray(value) || Object.getPrototypeOf(value) === Object.prototype) {
841
+ for (const nestedValue of Object.values(value)) {
842
+ freezeConfigValue(nestedValue);
843
+ }
844
+ return Object.freeze(value);
845
+ }
846
+ return value;
847
+ };
727
848
  var getNestedOption = (manager, key, defaultValue) => {
728
849
  const keys2 = String(key).split(".");
729
850
  if (keys2.length === 1) {
@@ -741,8 +862,9 @@ var getNestedOption = (manager, key, defaultValue) => {
741
862
  return option;
742
863
  };
743
864
  var OptionsManager = class {
744
- constructor(defaultOptions) {
865
+ constructor(defaultOptions, options = {}) {
745
866
  this.defaultOptions = defaultOptions;
867
+ this.preserveKeys = new Set(options.preserveKeys ?? []);
746
868
  this.listeners = {};
747
869
  const _this = this;
748
870
  this.currentOptions = new Proxy({}, {
@@ -763,13 +885,34 @@ var OptionsManager = class {
763
885
  return (0, import_utils9.get)(this.currentOptions, key, defaultValue);
764
886
  }
765
887
  set(key, value) {
766
- (0, import_utils9.set)(this.currentOptions, key, value);
888
+ const path = toPath(key);
889
+ if (path.length <= 1) {
890
+ (0, import_utils9.set)(this.currentOptions, path, this.preserveKeys.has(String(path[0])) ? value : cloneConfigValue(value));
891
+ return;
892
+ }
893
+ const [rootKey, ...nestedPath] = path;
894
+ const currentRoot = (0, import_utils9.get)(this.currentOptions, [rootKey], void 0);
895
+ const nextRoot = cloneConfigValue(currentRoot ?? {});
896
+ (0, import_utils9.set)(nextRoot, nestedPath, cloneConfigValue(value));
897
+ (0, import_utils9.set)(this.currentOptions, [rootKey], nextRoot);
767
898
  }
768
899
  fetch() {
769
- return { ...this.currentOptions };
900
+ const cloned = cloneConfigValue(this.currentOptions);
901
+ for (const key of this.preserveKeys) {
902
+ if (key in this.currentOptions) {
903
+ cloned[key] = this.currentOptions[key];
904
+ }
905
+ }
906
+ return freezeConfigValue(cloned);
770
907
  }
771
908
  assign(options) {
772
- (0, import_utils9.assign)(this.currentOptions, options);
909
+ const cloned = cloneConfigValue(options);
910
+ for (const key of this.preserveKeys) {
911
+ if (key in options) {
912
+ cloned[key] = options[key];
913
+ }
914
+ }
915
+ (0, import_utils9.assign)(this.currentOptions, cloned);
773
916
  }
774
917
  onchange(key, func) {
775
918
  (0, import_utils9.set)(this.listeners, key, func);
@@ -787,7 +930,8 @@ var defaultRequestComplexity = {
787
930
  maxBulkItems: 100,
788
931
  maxIncludeCount: 10,
789
932
  maxSubQueryCount: 10,
790
- maxBulkConcurrency: 10
933
+ maxBulkConcurrency: 10,
934
+ maxHookConcurrency: 10
791
935
  };
792
936
  var normalizePositiveInteger = (value, fallback) => {
793
937
  return Number.isSafeInteger(value) && Number(value) > 0 ? Number(value) : fallback;
@@ -803,6 +947,10 @@ var resolveRequestComplexity = (options) => ({
803
947
  maxBulkConcurrency: normalizePositiveInteger(
804
948
  options?.maxBulkConcurrency,
805
949
  defaultRequestComplexity.maxBulkConcurrency
950
+ ),
951
+ maxHookConcurrency: normalizePositiveInteger(
952
+ options?.maxHookConcurrency,
953
+ defaultRequestComplexity.maxHookConcurrency
806
954
  )
807
955
  });
808
956
  function validateRequestComplexity(value, options, scope = "request") {
@@ -896,7 +1044,8 @@ var defaultModelOptions = {
896
1044
  var defaultDataOptions = {
897
1045
  basePath: null,
898
1046
  parentPath: "/",
899
- queryRouteSegment: "__query"
1047
+ queryRouteSegment: "__query",
1048
+ listHardLimit: DEFAULT_LIST_HARD_LIMIT
900
1049
  };
901
1050
  var normalizeBasePath = (name, value) => {
902
1051
  if ((0, import_utils11.isNil)(value)) {
@@ -953,17 +1102,30 @@ var classifyPermissionSchema = (permissionSchema, modelPermissionPrefix) => {
953
1102
  modelPermissionKeys
954
1103
  };
955
1104
  };
1105
+ var refreshPermissionMetadata = (target) => {
1106
+ const permissionSchema = target.permissionSchema ?? {};
1107
+ const { schemaKeys, globalPermissionKeys, modelPermissionKeys } = classifyPermissionSchema(
1108
+ permissionSchema,
1109
+ target.modelPermissionPrefix ?? ""
1110
+ );
1111
+ target._permissionSchemaKeys = schemaKeys;
1112
+ target._globalPermissionKeys = globalPermissionKeys;
1113
+ target._modelPermissionKeys = modelPermissionKeys;
1114
+ };
956
1115
  var AccessRuntime = class {
957
- constructor() {
958
- this.globalOptions = new OptionsManager({
959
- requestPermissionField: "_permissions",
960
- globalPermissions: () => ({}),
961
- requireRegisteredPopulateModels: true,
962
- logger: defaultLogger,
963
- requestComplexity: defaultRequestComplexity
964
- }).build();
1116
+ constructor(options = {}) {
1117
+ this.globalOptions = new OptionsManager(
1118
+ {
1119
+ requestPermissionField: "_permissions",
1120
+ globalPermissions: () => ({}),
1121
+ requireRegisteredPopulateModels: true,
1122
+ logger: defaultLogger,
1123
+ requestComplexity: defaultRequestComplexity
1124
+ },
1125
+ { preserveKeys: ["logger"] }
1126
+ ).build();
965
1127
  this.defaultModelOptions = new OptionsManager({
966
- listHardLimit: 1e3,
1128
+ listHardLimit: DEFAULT_LIST_HARD_LIMIT,
967
1129
  documentPermissionField: "_permissions",
968
1130
  idParam: "id",
969
1131
  idField: "_id",
@@ -981,6 +1143,7 @@ var AccessRuntime = class {
981
1143
  this.modelAtts = {};
982
1144
  this.modelInstances = {};
983
1145
  this.openApiRegistry = new OpenApiRegistry();
1146
+ this.allowGlobalModelLookup = options.allowGlobalModelLookup ?? false;
984
1147
  ensureMongooseJsonSchemaInitialized();
985
1148
  }
986
1149
  registerModelInstance(modelName, model) {
@@ -996,13 +1159,16 @@ var AccessRuntime = class {
996
1159
  this.modelInstances[modelName] = model;
997
1160
  }
998
1161
  hasModelInstance(modelName) {
999
- return modelName in this.modelInstances;
1162
+ return modelName in this.modelInstances || this.allowGlobalModelLookup && modelName in import_mongoose3.default.models;
1000
1163
  }
1001
1164
  getModelInstance(modelName) {
1002
1165
  const registered = this.modelInstances[modelName];
1003
1166
  if (registered) return registered;
1167
+ if (!this.allowGlobalModelLookup) return null;
1004
1168
  const global = import_mongoose3.default.models[modelName];
1005
- return global ?? null;
1169
+ if (!global) return null;
1170
+ this.registerModelInstance(modelName, global);
1171
+ return global;
1006
1172
  }
1007
1173
  registerOpenApiRoute(route) {
1008
1174
  this.openApiRegistry.register(route);
@@ -1051,20 +1217,20 @@ var AccessRuntime = class {
1051
1217
  return this.defaultModelOptions.get(key, defaultValue);
1052
1218
  }
1053
1219
  createModelOptions(modelName) {
1054
- const model = this.getModelInstance(modelName) ?? import_mongoose3.default.model(modelName);
1220
+ const model = this.getModelInstance(modelName);
1221
+ if (!model) {
1222
+ throw new Error(
1223
+ `Runtime model registry missing model "${modelName}". Pass a mongoose.Model instance to createRouter() or register it with registerModelInstance() before using this isolated runtime.`
1224
+ );
1225
+ }
1055
1226
  const manager = new OptionsManager({
1056
1227
  ...defaultModelOptions,
1057
1228
  modelName
1058
1229
  });
1059
- manager.onchange("permissionSchema", function(newval, key, target) {
1060
- const permissionSchema = newval ?? {};
1061
- const { schemaKeys, globalPermissionKeys, modelPermissionKeys } = classifyPermissionSchema(
1062
- permissionSchema,
1063
- target.modelPermissionPrefix ?? ""
1064
- );
1065
- target._permissionSchemaKeys = schemaKeys;
1066
- target._globalPermissionKeys = globalPermissionKeys;
1067
- target._modelPermissionKeys = modelPermissionKeys;
1230
+ manager.onchange("permissionSchema", function(_newval, _key, target) {
1231
+ refreshPermissionMetadata(target);
1232
+ }).onchange("modelPermissionPrefix", function(_newval, _key, target) {
1233
+ refreshPermissionMetadata(target);
1068
1234
  }).onchange("basePath", function(newval, key, target) {
1069
1235
  target[key] = normalizeBasePath(
1070
1236
  modelName,
@@ -1193,7 +1359,7 @@ var AccessRuntime = class {
1193
1359
  return (0, import_utils11.get)(this.modelAtts, modelName, []);
1194
1360
  }
1195
1361
  };
1196
- var defaultRuntime = new AccessRuntime();
1362
+ var defaultRuntime = new AccessRuntime({ allowGlobalModelLookup: true });
1197
1363
 
1198
1364
  // src/runtime-context.ts
1199
1365
  var import_node_async_hooks = require("async_hooks");
@@ -1345,8 +1511,8 @@ var Base = class {
1345
1511
  genIDFilter(id) {
1346
1512
  return this.req.macl.genIDFilter(this.modelName, id);
1347
1513
  }
1348
- genPopulate(access, populate) {
1349
- return this.req.macl.genPopulate(this.modelName, access, populate);
1514
+ genPopulate(access, populate, subPaths) {
1515
+ return this.req.macl.genPopulate(this.modelName, access, populate, subPaths);
1350
1516
  }
1351
1517
  genSelect(access, targetFields, skipChecks, subPaths) {
1352
1518
  return this.req.macl.genSelect(this.modelName, access, targetFields, skipChecks, subPaths);
@@ -1443,6 +1609,55 @@ var Base = class {
1443
1609
  includePaths
1444
1610
  };
1445
1611
  }
1612
+ getForeignKeyValues(value) {
1613
+ return (0, import_utils12.flatten)((0, import_utils12.castArray)(value));
1614
+ }
1615
+ getForeignKey(value) {
1616
+ return String(value);
1617
+ }
1618
+ buildForeignKeyIndex(rows, foreignField) {
1619
+ const index = /* @__PURE__ */ new Map();
1620
+ for (const row of rows) {
1621
+ for (const value of this.getForeignKeyValues((0, import_utils12.get)(row, foreignField))) {
1622
+ const key = this.getForeignKey(value);
1623
+ const values = index.get(key);
1624
+ if (values) values.push(row);
1625
+ else index.set(key, [row]);
1626
+ }
1627
+ }
1628
+ return index;
1629
+ }
1630
+ getIndexedMatches(index, localValue) {
1631
+ const seen = /* @__PURE__ */ new Set();
1632
+ const matches = [];
1633
+ for (const value of this.getForeignKeyValues(localValue)) {
1634
+ for (const row of index.get(this.getForeignKey(value)) ?? []) {
1635
+ if (seen.has(row)) continue;
1636
+ seen.add(row);
1637
+ matches.push(row);
1638
+ }
1639
+ }
1640
+ return matches;
1641
+ }
1642
+ getIndexedCount(index, localValue) {
1643
+ const ids = /* @__PURE__ */ new Set();
1644
+ for (const value of this.getForeignKeyValues(localValue)) {
1645
+ for (const id of index.get(this.getForeignKey(value)) ?? []) {
1646
+ ids.add(id);
1647
+ }
1648
+ }
1649
+ return ids.size;
1650
+ }
1651
+ sanitizeIncludeArgs(args) {
1652
+ if (!(0, import_utils12.isPlainObject)(args)) return {};
1653
+ const { overrides: _clientOverrides, ...trustedArgs } = args;
1654
+ return trustedArgs;
1655
+ }
1656
+ assertIncludeForeignField(include) {
1657
+ if (!isValidFieldPath(include.foreignField)) {
1658
+ this.throwClientRequestError("bad_request" /* BadRequest */, `Invalid include foreignField: ${include.foreignField}`);
1659
+ }
1660
+ }
1446
1661
  async includeDocs(docs, include) {
1447
1662
  if (!include) return docs;
1448
1663
  const includes = (0, import_utils12.compact)((0, import_utils12.castArray)(include));
@@ -1451,25 +1666,52 @@ var Base = class {
1451
1666
  let docList = isSingle ? [docs] : docs;
1452
1667
  for (let x = 0; x < includes.length; x++) {
1453
1668
  const include2 = includes[x];
1454
- if (include2.op === "count") {
1455
- docList = await this.includeDocsCount(docList, include2);
1456
- } else {
1457
- docList = await this.includeDocsList(docList, include2);
1669
+ this.assertIncludeForeignField(include2);
1670
+ switch (include2.op) {
1671
+ case "count":
1672
+ docList = await this.includeDocsCount(docList, include2);
1673
+ break;
1674
+ case "read":
1675
+ docList = await this.includeDocsRead(docList, include2);
1676
+ break;
1677
+ case "list":
1678
+ docList = await this.includeDocsList(docList, include2);
1679
+ break;
1458
1680
  }
1459
1681
  }
1460
1682
  return isSingle ? docList[0] : docList;
1461
1683
  }
1684
+ async includeDocsRead(docs, include) {
1685
+ const { model, path, localField, foreignField, filter: _filters, args = {}, options = {} } = include;
1686
+ const svc = await this.getAuthorizedTargetService(model, "read");
1687
+ for (let y = 0; y < docs.length; y++) {
1688
+ const doc = docs[y];
1689
+ const localValue = (0, import_utils12.get)(doc, localField);
1690
+ const filter2 = { ..._filters ?? {}, [foreignField]: { $in: this.getForeignKeyValues(localValue) } };
1691
+ const trustedOptions = {
1692
+ ...options,
1693
+ access: "read",
1694
+ lean: true,
1695
+ includePermissions: false
1696
+ };
1697
+ const result = await svc.findOne(filter2, this.sanitizeIncludeArgs(args), trustedOptions);
1698
+ if (result.success) {
1699
+ setDocValue(doc, path, result.data);
1700
+ }
1701
+ }
1702
+ return docs;
1703
+ }
1462
1704
  async includeDocsList(docs, include) {
1463
1705
  const { model, op, path, localField, foreignField, filter: _filters, args = {}, options = {} } = include;
1464
1706
  const svc = await this.getAuthorizedTargetService(model, op);
1465
1707
  const includeLocalValues = [];
1466
- (0, import_utils12.forEach)(docs, (doc, i) => {
1467
- includeLocalValues.push((0, import_utils12.get)(doc, localField));
1708
+ (0, import_utils12.forEach)(docs, (doc) => {
1709
+ includeLocalValues.push(...this.getForeignKeyValues((0, import_utils12.get)(doc, localField)));
1468
1710
  });
1469
1711
  const filter2 = { ..._filters ?? {}, [foreignField]: { $in: (0, import_utils12.flatten)(includeLocalValues) } };
1470
1712
  const authorizedFilter = await svc.genFilter(op, filter2);
1471
1713
  const trustedArgs = {
1472
- ...args,
1714
+ ...this.sanitizeIncludeArgs(args),
1473
1715
  overrides: {
1474
1716
  filter: authorizedFilter
1475
1717
  }
@@ -1482,58 +1724,42 @@ var Base = class {
1482
1724
  };
1483
1725
  const trustedResult = await svc.find(filter2, trustedArgs, trustedOptions);
1484
1726
  if (!trustedResult.success) return docs;
1727
+ const index = this.buildForeignKeyIndex(trustedResult.data, foreignField);
1485
1728
  for (let y = 0; y < docs.length; y++) {
1486
1729
  const doc = docs[y];
1487
- const localValue = (0, import_utils12.get)(doc, localField);
1488
- const filterFn = (row) => (0, import_utils12.intersectionBy)((0, import_utils12.castArray)(localValue), (0, import_utils12.castArray)((0, import_utils12.get)(row, foreignField)), String).length > 0;
1489
- const matches = trustedResult.data.filter(filterFn);
1730
+ const matches = this.getIndexedMatches(index, (0, import_utils12.get)(doc, localField));
1490
1731
  setDocValue(doc, path, op === "list" ? matches : matches[0]);
1491
1732
  }
1492
1733
  return docs;
1493
1734
  }
1494
1735
  async includeDocsCount(docs, include) {
1495
- const { model, path, localField, foreignField, filter: _filters, args = {}, options = {} } = include;
1736
+ const { model, path, localField, foreignField, filter: _filters } = include;
1496
1737
  const svc = await this.getAuthorizedTargetService(model, "count");
1497
- const includeLocalValues = [];
1498
- (0, import_utils12.forEach)(docs, (doc) => {
1499
- includeLocalValues.push((0, import_utils12.get)(doc, localField));
1500
- });
1501
- const filter2 = { ..._filters ?? {}, [foreignField]: { $in: (0, import_utils12.flatten)(includeLocalValues) } };
1502
- const authorizedFilter = await svc.genFilter("list", filter2);
1503
- const trustedArgs = {
1504
- ...args,
1505
- select: [foreignField],
1506
- overrides: {
1507
- filter: authorizedFilter
1508
- }
1509
- };
1510
- const trustedOptions = {
1511
- ...options,
1512
- lean: true,
1513
- includePermissions: false,
1514
- includeCount: false
1515
- };
1516
- const result = await svc.find(filter2, trustedArgs, trustedOptions);
1738
+ const localValues = docs.flatMap((doc) => this.getForeignKeyValues((0, import_utils12.get)(doc, localField)));
1739
+ const result = await svc.countByFieldValues(foreignField, localValues, _filters ?? {}, "count");
1517
1740
  if (!result.success) return docs;
1518
1741
  for (let y = 0; y < docs.length; y++) {
1519
1742
  const doc = docs[y];
1520
- const localValue = (0, import_utils12.get)(doc, localField);
1521
- const filterFn = (row) => (0, import_utils12.intersectionBy)((0, import_utils12.castArray)(localValue), (0, import_utils12.castArray)((0, import_utils12.get)(row, foreignField)), String).length > 0;
1522
- setDocValue(doc, path, result.data.filter(filterFn).length);
1743
+ setDocValue(doc, path, this.getIndexedCount(result.data, (0, import_utils12.get)(doc, localField)));
1523
1744
  }
1524
1745
  return docs;
1525
1746
  }
1526
- async parseClientData(filter2) {
1527
- const result = await iterateQuery(filter2, async (fo, val, key) => {
1528
- switch (fo) {
1529
- case 0 /* SubQuery */:
1530
- return this.handleSubQuery(val, key);
1531
- case 1 /* Date */:
1532
- return this.handleDate(val, key);
1533
- default:
1534
- return null;
1535
- }
1536
- });
1747
+ async parseClientData(filter2, scheduler = new RequestConcurrencyScheduler(this.getRequestComplexity().maxBulkConcurrency), scheduled = false) {
1748
+ const result = await iterateQuery(
1749
+ filter2,
1750
+ async (fo, val, key) => {
1751
+ switch (fo) {
1752
+ case 0 /* SubQuery */:
1753
+ return this.handleSubQuery(val, key);
1754
+ case 1 /* Date */:
1755
+ return this.handleDate(val, key);
1756
+ default:
1757
+ return null;
1758
+ }
1759
+ },
1760
+ scheduler,
1761
+ scheduled
1762
+ );
1537
1763
  return result;
1538
1764
  }
1539
1765
  async handleSubQuery(sq, key) {
@@ -1579,7 +1805,67 @@ var import_utils14 = require("@web-ts-toolkit/utils");
1579
1805
  var import_just_diff = require("just-diff");
1580
1806
 
1581
1807
  // src/model.ts
1582
- var import_mongoose4 = __toESM(require("mongoose"));
1808
+ var Model = class {
1809
+ constructor(modelName, runtime) {
1810
+ this.modelName = modelName;
1811
+ this.runtime = runtime ?? null;
1812
+ const resolvedRuntime = runtime ?? getActiveRuntime() ?? defaultRuntime;
1813
+ const registered = resolvedRuntime.getModelInstance(modelName);
1814
+ if (!registered) {
1815
+ throw new Error(
1816
+ `Runtime model registry missing model "${modelName}". Pass a mongoose.Model instance to createRouter() or register it with registerModelInstance() before using this runtime.`
1817
+ );
1818
+ }
1819
+ this.mongooseModel = registered;
1820
+ if (!this.mongooseModel) return;
1821
+ }
1822
+ new() {
1823
+ const doc = new this.mongooseModel();
1824
+ return doc;
1825
+ }
1826
+ create(data) {
1827
+ return this.mongooseModel.create(data);
1828
+ }
1829
+ find({ filter: filter2, select, sort, populate, limit, hardLimit, skip, lean }) {
1830
+ const builder = this.mongooseModel.find(filter2);
1831
+ if (select) builder.select(select);
1832
+ if (skip) builder.skip(Number(skip));
1833
+ const normalizedLimit = Number(limit);
1834
+ if (Number.isSafeInteger(normalizedLimit) && normalizedLimit > 0) {
1835
+ builder.limit(normalizedLimit);
1836
+ } else if (Number.isSafeInteger(hardLimit) && Number(hardLimit) > 0) {
1837
+ builder.limit(Number(hardLimit));
1838
+ }
1839
+ if (sort) builder.sort(sort);
1840
+ if (populate) builder.populate(populate);
1841
+ if (lean) builder.lean();
1842
+ return builder;
1843
+ }
1844
+ findOne({ filter: filter2, select, sort, populate, lean }) {
1845
+ const builder = this.mongooseModel.findOne(filter2);
1846
+ if (select) builder.select(select);
1847
+ if (sort) builder.sort(sort);
1848
+ if (populate) builder.populate(populate);
1849
+ if (lean) builder.lean();
1850
+ return builder;
1851
+ }
1852
+ exists(filter2) {
1853
+ if (!filter2) return null;
1854
+ return this.findOne({ filter: filter2 }).select("_id").lean();
1855
+ }
1856
+ // see https://mongoosejs.com/docs/api.html#query_Query-countDocuments
1857
+ countDocuments(filter2 = {}) {
1858
+ return this.mongooseModel.countDocuments(filter2);
1859
+ }
1860
+ // see https://mongoosejs.com/docs/api.html#model_Model.distinct
1861
+ distinct(field, conditions = {}) {
1862
+ return this.mongooseModel.distinct(field, conditions);
1863
+ }
1864
+ aggregate(pipeline) {
1865
+ return this.mongooseModel.aggregate(pipeline);
1866
+ }
1867
+ };
1868
+ var model_default = Model;
1583
1869
 
1584
1870
  // src/logger-helpers.ts
1585
1871
  var SENSITIVE_FILTER_KEYS = /* @__PURE__ */ new Set([
@@ -1758,136 +2044,6 @@ var error = (error2) => {
1758
2044
  safeApply(fn, target ?? defaultLogger, [error2]);
1759
2045
  };
1760
2046
 
1761
- // src/model.ts
1762
- var safeSortLogError = (msg, ...args) => {
1763
- const message = args.length > 0 ? `${msg} ${safeStringify(args.length === 1 ? args[0] : args)}` : msg;
1764
- error(message);
1765
- };
1766
- var Model = class {
1767
- constructor(modelName, runtime) {
1768
- this.modelName = modelName;
1769
- this.runtime = runtime ?? null;
1770
- const resolvedRuntime = runtime ?? getActiveRuntime();
1771
- const registered = resolvedRuntime?.getModelInstance(modelName) ?? null;
1772
- const global = import_mongoose4.default.models[modelName];
1773
- this.model = registered ?? global ?? import_mongoose4.default.model(modelName);
1774
- if (!this.model) return;
1775
- }
1776
- new() {
1777
- const doc = new this.model();
1778
- return doc;
1779
- }
1780
- create(data) {
1781
- return this.model.create(data);
1782
- }
1783
- find({ filter: filter2, select, sort, populate, limit, hardLimit, skip, lean }) {
1784
- if (!this.validateSort(sort)) {
1785
- sort = null;
1786
- }
1787
- const builder = this.model.find(filter2);
1788
- if (select) builder.select(select);
1789
- if (skip) builder.skip(Number(skip));
1790
- const normalizedLimit = Number(limit);
1791
- if (Number.isSafeInteger(normalizedLimit) && normalizedLimit > 0) {
1792
- builder.limit(normalizedLimit);
1793
- } else if (Number.isSafeInteger(hardLimit) && Number(hardLimit) > 0) {
1794
- builder.limit(Number(hardLimit));
1795
- }
1796
- if (sort) builder.sort(sort);
1797
- if (populate) builder.populate(populate);
1798
- if (lean) builder.lean();
1799
- return builder;
1800
- }
1801
- validateSort(sort, logError = safeSortLogError) {
1802
- const validSortOrders = [1, -1, "asc", "ascending", "desc", "descending"];
1803
- const fieldPathPattern = /^[A-Za-z_$][\w$]*(\.[A-Za-z_$][\w$]*)*$/;
1804
- const isValidFieldPath = (field) => fieldPathPattern.test(field);
1805
- if (sort === null || sort === void 0) return true;
1806
- if (typeof sort === "string") {
1807
- const fields = sort.trim().split(/\s+/).filter(Boolean);
1808
- const isValid = fields.every((field) => isValidFieldPath(field.startsWith("-") ? field.slice(1) : field));
1809
- if (!isValid) {
1810
- logError("Invalid sort string:", sort);
1811
- return false;
1812
- }
1813
- return true;
1814
- }
1815
- if (Array.isArray(sort)) {
1816
- const isValid = sort.every((pair) => {
1817
- if (!Array.isArray(pair) || pair.length !== 2) {
1818
- logError("Invalid sort array element: must be [key, order]", pair);
1819
- return false;
1820
- }
1821
- const [key, order] = pair;
1822
- if (typeof key !== "string" || !isValidFieldPath(key)) {
1823
- logError("Invalid sort array key: must be a field path string", key);
1824
- return false;
1825
- }
1826
- if (!validSortOrders.includes(order)) {
1827
- logError('Invalid sort array order: must be 1, -1, "asc", or "desc"', order);
1828
- return false;
1829
- }
1830
- return true;
1831
- });
1832
- return isValid;
1833
- }
1834
- if (typeof sort === "object" && !(sort instanceof Map)) {
1835
- const isValid = Object.entries(sort).every(([key, order]) => {
1836
- if (!isValidFieldPath(key)) {
1837
- logError("Invalid sort object key: must be a field path string", key);
1838
- return false;
1839
- }
1840
- if (!validSortOrders.includes(order)) {
1841
- logError('Invalid sort object value: must be 1, -1, "asc", or "desc"', order);
1842
- return false;
1843
- }
1844
- return true;
1845
- });
1846
- return isValid;
1847
- }
1848
- if (sort instanceof Map) {
1849
- const isValid = Array.from(sort.entries()).every(([key, order]) => {
1850
- if (!isValidFieldPath(key)) {
1851
- logError("Invalid sort Map key: must be a field path string", key);
1852
- return false;
1853
- }
1854
- if (!validSortOrders.includes(order)) {
1855
- logError('Invalid sort Map value: must be 1, -1, "asc", or "desc"', order);
1856
- return false;
1857
- }
1858
- return true;
1859
- });
1860
- return isValid;
1861
- }
1862
- logError("Invalid sort type: must be string, array, object, or Map", sort);
1863
- return false;
1864
- }
1865
- findOne({ filter: filter2, select, sort, populate, lean }) {
1866
- if (!this.validateSort(sort)) {
1867
- sort = null;
1868
- }
1869
- const builder = this.model.findOne(filter2);
1870
- if (select) builder.select(select);
1871
- if (sort) builder.sort(sort);
1872
- if (populate) builder.populate(populate);
1873
- if (lean) builder.lean();
1874
- return builder;
1875
- }
1876
- exists(filter2) {
1877
- if (!filter2) return null;
1878
- return this.findOne({ filter: filter2 }).select("_id").lean();
1879
- }
1880
- // see https://mongoosejs.com/docs/api.html#query_Query-countDocuments
1881
- countDocuments(filter2 = {}) {
1882
- return this.model.countDocuments(filter2);
1883
- }
1884
- // see https://mongoosejs.com/docs/api.html#model_Model.distinct
1885
- distinct(field, conditions = {}) {
1886
- return this.model.distinct(field, conditions);
1887
- }
1888
- };
1889
- var model_default = Model;
1890
-
1891
2047
  // src/services/model-subdocument-service.ts
1892
2048
  var import_utils13 = require("@web-ts-toolkit/utils");
1893
2049
  async function listSub(service, id, sub, options) {
@@ -2014,7 +2170,8 @@ async function getParentDoc(service, id, sub, args, options) {
2014
2170
  const { access = "read", lean = false } = options ?? {};
2015
2171
  const parentFilter = await service.genFilter(access, await service.genIDFilter(id));
2016
2172
  if (parentFilter === false) return null;
2017
- return service.findRawParentDoc({ filter: parentFilter, select: sub, populate: genSubPopulate(sub, populate), lean });
2173
+ const subPopulate = await service.genPopulate("read", genSubPopulate(sub, populate), [sub, "sub"]);
2174
+ return service.findRawParentDoc({ filter: parentFilter, select: sub, populate: subPopulate, lean });
2018
2175
  }
2019
2176
 
2020
2177
  // src/services/model-service-defaults.ts
@@ -2140,21 +2297,6 @@ var assertModelDocument = (value, modelName, hookName) => {
2140
2297
  }
2141
2298
  throw new Error(`${hookName} hook for model=${modelName} must return a Mongoose document instance`);
2142
2299
  };
2143
- var mapWithConcurrencyLimit = async (items, limit, iteratee) => {
2144
- const results = new Array(items.length);
2145
- let cursor = 0;
2146
- const workerCount = Math.min(Math.max(limit, 1), items.length || 1);
2147
- await Promise.all(
2148
- Array.from({ length: workerCount }, async () => {
2149
- while (cursor < items.length) {
2150
- const current = cursor;
2151
- cursor += 1;
2152
- results[current] = await iteratee(items[current], current);
2153
- }
2154
- })
2155
- );
2156
- return results;
2157
- };
2158
2300
  var Service = class extends Base {
2159
2301
  findRawParentDoc(args) {
2160
2302
  return this.model.findOne({
@@ -2163,6 +2305,12 @@ var Service = class extends Base {
2163
2305
  populate: args.populate
2164
2306
  });
2165
2307
  }
2308
+ createModelAdapter(modelName) {
2309
+ return new model_default(modelName);
2310
+ }
2311
+ getModelRouterOptions(modelName) {
2312
+ return getModelOptions(modelName);
2313
+ }
2166
2314
  asServiceHookContext(context) {
2167
2315
  return context;
2168
2316
  }
@@ -2189,8 +2337,8 @@ var Service = class extends Base {
2189
2337
  }
2190
2338
  constructor(req, modelName) {
2191
2339
  super(req, modelName);
2192
- this.model = new model_default(modelName);
2193
- this.options = getModelOptions(modelName);
2340
+ this.model = this.createModelAdapter(modelName);
2341
+ this.options = this.getModelRouterOptions(modelName);
2194
2342
  this.defaults = this.options.defaults || {};
2195
2343
  this.baseFields = ["_id"];
2196
2344
  this.baseFieldsExt = this.baseFields.concat(this.options.documentPermissionField);
@@ -2209,10 +2357,11 @@ var Service = class extends Base {
2209
2357
  if (result) return result;
2210
2358
  throw error2;
2211
2359
  }
2212
- let [_filter, _select, _populate] = await Promise.all([
2360
+ let [_filter, _select, _populate, allowedSortFields] = await Promise.all([
2213
2361
  overrideFilter || this.genFilter(access, parsedFilter),
2214
2362
  overrideSelect || this.genQuerySelect(access, select),
2215
- overridePopulate || this.genPopulate(populateAccess || access, populate)
2363
+ overridePopulate || this.genPopulate(populateAccess || access, populate),
2364
+ this.genAllowedFields({}, access, this.baseFieldsExt)
2216
2365
  ]);
2217
2366
  const { includes, includeLocalFields, includePaths } = this.processInclude(include);
2218
2367
  const finalSelect = normalizeSelect(_select).concat(includeLocalFields);
@@ -2231,13 +2380,18 @@ var Service = class extends Base {
2231
2380
  this.completeOp("findOne", startedAt, "forbidden" /* Forbidden */, _filter);
2232
2381
  return { success: false, kind: "error", code: "forbidden" /* Forbidden */, query };
2233
2382
  }
2383
+ const sortErrors = validateSortFields(sort, allowedSortFields);
2384
+ if (sortErrors.length > 0) {
2385
+ this.completeOp("findOne", startedAt, "bad_request" /* BadRequest */, _filter);
2386
+ return { success: false, kind: "error", code: "bad_request" /* BadRequest */, errors: sortErrors, query };
2387
+ }
2234
2388
  let doc = await this.model.findOne({ ...query, lean });
2235
2389
  if (!doc) {
2236
2390
  this.completeOp("findOne", startedAt, "not_found" /* NotFound */, _filter);
2237
2391
  return { success: false, kind: "error", code: "not_found" /* NotFound */, query };
2238
2392
  }
2239
2393
  const context = {
2240
- mongooseModel: this.model.model,
2394
+ mongooseModel: this.model.mongooseModel,
2241
2395
  modelName: this.modelName,
2242
2396
  operation: access,
2243
2397
  originalDocumentSnapshot: toObject(doc),
@@ -2301,11 +2455,12 @@ var Service = class extends Base {
2301
2455
  if (result) return result;
2302
2456
  throw error2;
2303
2457
  }
2304
- const [_filter, _select, _populate, pagination] = await Promise.all([
2458
+ const [_filter, _select, _populate, pagination, allowedSortFields] = await Promise.all([
2305
2459
  overrideFilter || this.genFilter("list", parsedFilter),
2306
2460
  overrideSelect || this.genQuerySelect("list", select),
2307
2461
  overridePopulate || this.genPopulate(populateAccess, populate),
2308
- genPagination({ skip, limit, page, pageSize }, this.options.listHardLimit)
2462
+ genPagination({ skip, limit, page, pageSize }, this.options.listHardLimit),
2463
+ this.genAllowedFields({}, "list", this.baseFieldsExt)
2309
2464
  ]);
2310
2465
  const finalSelect = normalizeSelect(_select);
2311
2466
  const filteredPopulate = (0, import_utils14.isArray)(finalSelect) && (0, import_utils14.isArray)(_populate) ? _populate.filter((p) => finalSelect.includes(p.path.split(".")[0])) : _populate;
@@ -2328,13 +2483,18 @@ var Service = class extends Base {
2328
2483
  this.completeOp("find", startedAt, "forbidden" /* Forbidden */, _filter);
2329
2484
  return { success: false, kind: "error", code: "forbidden" /* Forbidden */, query };
2330
2485
  }
2486
+ const sortErrors = validateSortFields(sort, allowedSortFields);
2487
+ if (sortErrors.length > 0) {
2488
+ this.completeOp("find", startedAt, "bad_request" /* BadRequest */, _filter);
2489
+ return { success: false, kind: "error", code: "bad_request" /* BadRequest */, errors: sortErrors, query };
2490
+ }
2331
2491
  let docs = await this.model.find({
2332
2492
  ...query,
2333
2493
  hardLimit: this.options.listHardLimit,
2334
2494
  lean
2335
2495
  });
2336
2496
  const contexts = docs.map((doc) => ({
2337
- mongooseModel: this.model.model,
2497
+ mongooseModel: this.model.mongooseModel,
2338
2498
  modelName: this.modelName,
2339
2499
  operation: "list",
2340
2500
  originalDocumentSnapshot: toObject(doc),
@@ -2397,8 +2557,33 @@ var Service = class extends Base {
2397
2557
  errors: [{ detail: `Bulk create exceeds maximum item count of ${maxBulkItems}` }]
2398
2558
  };
2399
2559
  }
2560
+ const parseErrors = [];
2561
+ const parseScheduler = new RequestConcurrencyScheduler(maxBulkConcurrency);
2400
2562
  try {
2401
- dataArr = await Promise.all(dataArr.map((d) => this.parseClientData(d)));
2563
+ const parsedData = await parseScheduler.map(dataArr, async (d, index) => {
2564
+ try {
2565
+ return await this.parseClientData(d, parseScheduler, true);
2566
+ } catch (error2) {
2567
+ const result = this.getClientRequestErrorResult(error2);
2568
+ if (!result) throw error2;
2569
+ parseErrors.push({
2570
+ index,
2571
+ code: result.code,
2572
+ errors: (result.errors ?? []).map((issue) => this.formatBulkValidationIssue(issue, isArr ? index : null))
2573
+ });
2574
+ return void 0;
2575
+ }
2576
+ });
2577
+ if (parseErrors.length > 0) {
2578
+ const sortedErrors = parseErrors.slice().sort((a, b) => a.index - b.index);
2579
+ return {
2580
+ success: false,
2581
+ kind: "error",
2582
+ code: sortedErrors[0]?.code ?? "bad_request" /* BadRequest */,
2583
+ errors: sortedErrors.flatMap((entry) => entry.errors)
2584
+ };
2585
+ }
2586
+ dataArr = parsedData;
2402
2587
  } catch (error2) {
2403
2588
  const result = this.getClientRequestErrorResult(error2);
2404
2589
  if (result) return result;
@@ -2409,7 +2594,7 @@ var Service = class extends Base {
2409
2594
  const validationErrors = [];
2410
2595
  const validationItems = await mapWithConcurrencyLimit(dataArr, maxBulkConcurrency, async (item, index) => {
2411
2596
  const context = {
2412
- mongooseModel: this.model.model,
2597
+ mongooseModel: this.model.mongooseModel,
2413
2598
  modelName: this.modelName,
2414
2599
  operation: "create",
2415
2600
  originalData: item,
@@ -2552,7 +2737,7 @@ var Service = class extends Base {
2552
2737
  return { success: false, kind: "error", code: "not_found" /* NotFound */, query };
2553
2738
  }
2554
2739
  const context = {
2555
- mongooseModel: this.model.model,
2740
+ mongooseModel: this.model.mongooseModel,
2556
2741
  modelName: this.modelName,
2557
2742
  operation: "update",
2558
2743
  resolvedQuery: query
@@ -2698,7 +2883,7 @@ var Service = class extends Base {
2698
2883
  return { success: false, kind: "error", code: "not_found" /* NotFound */, query };
2699
2884
  }
2700
2885
  const context = {
2701
- mongooseModel: this.model.model,
2886
+ mongooseModel: this.model.mongooseModel,
2702
2887
  modelName: this.modelName,
2703
2888
  operation: "delete",
2704
2889
  originalDocumentSnapshot: toObject(doc),
@@ -2727,22 +2912,11 @@ var Service = class extends Base {
2727
2912
  };
2728
2913
  }
2729
2914
  isValidDistinctFieldName(field) {
2730
- if (typeof field !== "string" || field.length === 0) return false;
2731
- if (field.includes("$")) return false;
2732
- if (field.includes("..")) return false;
2733
- if (field.startsWith(".") || field.endsWith(".")) return false;
2734
- if (/\s/.test(field)) return false;
2735
- return true;
2915
+ return isValidFieldPath(field);
2736
2916
  }
2737
2917
  async authorizeDistinctField(field) {
2738
2918
  const allowedFields = await this.genAllowedFields(null, "read");
2739
- const isAllowed = allowedFields.some((allowed) => {
2740
- if (allowed === field) return true;
2741
- if (allowed.startsWith(`${field}.`)) return true;
2742
- if (field.startsWith(`${allowed}.`)) return true;
2743
- return false;
2744
- });
2745
- if (!isAllowed) {
2919
+ if (!isFieldAllowed(field, allowedFields)) {
2746
2920
  return {
2747
2921
  success: false,
2748
2922
  kind: "error",
@@ -2780,6 +2954,44 @@ var Service = class extends Base {
2780
2954
  if (filter2 === false) return { success: false, kind: "error", code: "forbidden" /* Forbidden */, query };
2781
2955
  return { success: true, kind: "single", code: "success" /* Success */, data: await this.model.countDocuments(filter2), query };
2782
2956
  }
2957
+ async countByFieldValues(foreignField, values, filter2 = {}, access = "list") {
2958
+ if (!isValidFieldPath(foreignField)) {
2959
+ return {
2960
+ success: false,
2961
+ kind: "error",
2962
+ code: "bad_request" /* BadRequest */,
2963
+ errors: [{ detail: `Invalid include foreignField: ${foreignField}` }]
2964
+ };
2965
+ }
2966
+ const filterErrors = this.validateClientFilter(filter2);
2967
+ if (filterErrors.length > 0) return { success: false, kind: "error", code: "bad_request" /* BadRequest */, errors: filterErrors };
2968
+ const uniqueValues = (0, import_utils14.uniqBy)(values, (value) => String(value));
2969
+ filter2 = await this.genFilter(access, {
2970
+ ...filter2,
2971
+ [foreignField]: { $in: uniqueValues }
2972
+ });
2973
+ const query = { filter: filter2 };
2974
+ if (filter2 === false) return { success: false, kind: "error", code: "forbidden" /* Forbidden */, query };
2975
+ const matchFilter = filter2;
2976
+ const rows = await this.model.aggregate([
2977
+ { $match: matchFilter },
2978
+ {
2979
+ $project: {
2980
+ foreignValues: {
2981
+ $cond: [{ $isArray: `$${foreignField}` }, `$${foreignField}`, [`$${foreignField}`]]
2982
+ }
2983
+ }
2984
+ },
2985
+ { $unwind: "$foreignValues" },
2986
+ { $match: { foreignValues: { $in: uniqueValues } } },
2987
+ { $group: { _id: "$foreignValues", documentIds: { $addToSet: "$_id" } } }
2988
+ ]);
2989
+ const counts = /* @__PURE__ */ new Map();
2990
+ for (const row of rows) {
2991
+ counts.set(String(row._id), new Set(row.documentIds.map((id) => String(id))));
2992
+ }
2993
+ return { success: true, kind: "single", code: "success" /* Success */, data: counts, query };
2994
+ }
2783
2995
  getDocPermissions(doc) {
2784
2996
  return getDocPermissions(this.modelName, doc);
2785
2997
  }
@@ -2891,7 +3103,7 @@ var PublicService = class extends Service {
2891
3103
  return result;
2892
3104
  }
2893
3105
  const docs = await this.decorateAll(result.data, "list", {
2894
- mongooseModel: this.model.model,
3106
+ mongooseModel: this.model.mongooseModel,
2895
3107
  modelName: this.modelName,
2896
3108
  operation: "list",
2897
3109
  resolvedQuery: result.query
@@ -2940,12 +3152,17 @@ var PublicService = class extends Service {
2940
3152
  { skim, includePermissions, access, populateAccess, lean }
2941
3153
  );
2942
3154
  if (tryList && (!result.success || !result.data)) {
3155
+ const listAllowed = await this.req.macl.isAllowed(this.modelName, "list");
3156
+ if (!listAllowed) {
3157
+ return { success: false, kind: "error", code: "unauthorized" /* Unauthorized */, errors: ["Unauthorized"] };
3158
+ }
2943
3159
  access = "list";
2944
3160
  result = await this.findById(
2945
3161
  id,
2946
3162
  {
2947
3163
  select,
2948
3164
  populate,
3165
+ include,
2949
3166
  overrides: { idFilter }
2950
3167
  },
2951
3168
  { skim, includePermissions, access, populateAccess, lean }
@@ -2975,6 +3192,10 @@ var PublicService = class extends Service {
2975
3192
  { skim, includePermissions, access, populateAccess, lean }
2976
3193
  );
2977
3194
  if (tryList && (!result.success || !result.data)) {
3195
+ const listAllowed = await this.req.macl.isAllowed(this.modelName, "list");
3196
+ if (!listAllowed) {
3197
+ return { success: false, kind: "error", code: "unauthorized" /* Unauthorized */, errors: ["Unauthorized"] };
3198
+ }
2978
3199
  access = "list";
2979
3200
  result = await this.findOne(
2980
3201
  filter2,
@@ -2982,6 +3203,7 @@ var PublicService = class extends Service {
2982
3203
  select,
2983
3204
  sort,
2984
3205
  populate,
3206
+ include,
2985
3207
  overrides: {}
2986
3208
  },
2987
3209
  { skim, includePermissions, access, populateAccess, lean }
@@ -3171,6 +3393,7 @@ var DataService = class {
3171
3393
  const filterErrors = validateClientFilter(filter2);
3172
3394
  if (filterErrors.length > 0) return { success: false, kind: "error", code: "bad_request" /* BadRequest */, errors: filterErrors };
3173
3395
  const { select, sort, skip, limit, page, pageSize } = args ?? {};
3396
+ const hasExplicitLimit = limit != null || pageSize != null;
3174
3397
  const [_filter, _select, pagination] = await Promise.all([
3175
3398
  this.genFilter("list", filter2),
3176
3399
  this.genQuerySelect("list", select),
@@ -3183,26 +3406,26 @@ var DataService = class {
3183
3406
  ...pagination
3184
3407
  };
3185
3408
  if (_filter === false) return { success: false, kind: "error", code: "forbidden" /* Forbidden */, query };
3186
- if (sort) {
3187
- const sortFieldErrors = await this.validateSortFields(sort, "list");
3188
- if (sortFieldErrors.length > 0) {
3189
- return { success: false, kind: "error", code: "bad_request" /* BadRequest */, errors: sortFieldErrors, query };
3190
- }
3409
+ const sortFieldErrors = validateSortFields(sort, await this.genAllowedFields({}, "list"));
3410
+ if (sortFieldErrors.length > 0) {
3411
+ return { success: false, kind: "error", code: "bad_request" /* BadRequest */, errors: sortFieldErrors, query };
3191
3412
  }
3192
3413
  let docs = await filterCollection(this.data, _filter);
3193
3414
  const totalCount = docs.length;
3194
3415
  if (sort) {
3195
- const { sortKey, sortOrder } = parseSortString(sort);
3196
- docs = (0, import_utils16.orderBy)(docs, [sortKey], [sortOrder]);
3416
+ const normalizedSort = normalizeSortForOrderBy(sort);
3417
+ docs = (0, import_utils16.orderBy)(docs, normalizedSort.fields, normalizedSort.orders);
3197
3418
  }
3198
3419
  const pagedDocs = docs.slice(query.skip, query.limit && query.skip + query.limit);
3199
- const trimmed = await Promise.all(
3200
- pagedDocs.map(async (doc) => {
3201
- doc = await this.trimOutputFields(doc, "list");
3202
- if (_select.length > 0) doc = (0, import_utils16.pick)(doc, _select);
3203
- return doc;
3204
- })
3205
- );
3420
+ const { maxHookConcurrency } = this.getRequestComplexity();
3421
+ const trimmed = await mapWithConcurrencyLimit(pagedDocs, maxHookConcurrency, async (doc) => {
3422
+ doc = await this.trimOutputFields(doc, "list");
3423
+ if (_select.length > 0) doc = (0, import_utils16.pick)(doc, _select);
3424
+ return doc;
3425
+ });
3426
+ if (!hasExplicitLimit && trimmed.length === totalCount) {
3427
+ query.limit = trimmed.length;
3428
+ }
3206
3429
  return {
3207
3430
  success: true,
3208
3431
  kind: "list",
@@ -3219,6 +3442,9 @@ var DataService = class {
3219
3442
  decorateAll(docs, access, context) {
3220
3443
  return this.req.dacl.decorateAll(this.dataName, docs, access, context);
3221
3444
  }
3445
+ getRequestComplexity() {
3446
+ return resolveRequestComplexity(getGlobalOption("requestComplexity"));
3447
+ }
3222
3448
  genAllowedFields(doc, access, baseFields) {
3223
3449
  return this.req.dacl.genAllowedFields(this.dataName, doc, access, baseFields);
3224
3450
  }
@@ -3240,26 +3466,6 @@ var DataService = class {
3240
3466
  trimOutputFields(doc, access, baseFields) {
3241
3467
  return this.pickAllowedFields(doc, access, baseFields);
3242
3468
  }
3243
- async validateSortFields(sort, access) {
3244
- const fieldPathPattern = /^[A-Za-z_][A-Za-z0-9_$]*(\.[A-Za-z_][A-Za-z0-9_$]*)*$/;
3245
- const fields = sort.trim().split(/\s+/).filter(Boolean).map((field) => ({
3246
- raw: field,
3247
- name: field.startsWith("-") ? field.slice(1) : field
3248
- }));
3249
- const errors = [];
3250
- for (const { raw, name } of fields) {
3251
- if (!fieldPathPattern.test(name)) {
3252
- errors.push({ detail: `Invalid sort field: ${raw}`, pointer: `#/sort` });
3253
- continue;
3254
- }
3255
- if (name === "id" || name === "_id") continue;
3256
- const allowed = await this.genAllowedFields({}, access);
3257
- if (!allowed.includes(name)) {
3258
- errors.push({ detail: `Sort field is not allowed: ${name}`, pointer: `#/sort` });
3259
- }
3260
- }
3261
- return errors;
3262
- }
3263
3469
  };
3264
3470
 
3265
3471
  // src/processors.ts
@@ -3845,7 +4051,7 @@ var Core = class {
3845
4051
  alwaysSelectFields
3846
4052
  });
3847
4053
  }
3848
- async genPopulate(modelName, access = "read", _populate = null) {
4054
+ async genPopulate(modelName, access = "read", _populate = null, subPaths = []) {
3849
4055
  if (!_populate) return [];
3850
4056
  let populate = Array.isArray(_populate) ? _populate : [_populate];
3851
4057
  populate = (0, import_utils21.compact)(
@@ -3856,12 +4062,14 @@ var Core = class {
3856
4062
  path: p.path,
3857
4063
  select: normalizeSelect(p.select)
3858
4064
  };
3859
- const parentPath = ret.path.includes(".") ? ret.path.split(".")[0] : ret.path;
4065
+ const pathForSourcePermission = subPaths.length > 0 && ret.path.startsWith(`${subPaths[0]}.`) ? ret.path.slice(subPaths[0].length + 1) : ret.path;
4066
+ const parentPath = pathForSourcePermission.includes(".") ? pathForSourcePermission.split(".")[0] : pathForSourcePermission;
3860
4067
  const allowedParentPaths = await this.genSelect(
3861
4068
  modelName,
3862
4069
  populateAccess,
3863
4070
  [parentPath],
3864
- false
4071
+ false,
4072
+ subPaths
3865
4073
  );
3866
4074
  if (!allowedParentPaths.includes(parentPath)) return null;
3867
4075
  const refModelName = getModelRef(modelName, ret.path);
@@ -4351,7 +4559,7 @@ var includeItemSchema = import_zod2.z.object({
4351
4559
  filter: import_zod2.z.record(import_zod2.z.string(), import_zod2.z.unknown()).optional(),
4352
4560
  localField: import_zod2.z.string().min(1),
4353
4561
  foreignField: import_zod2.z.string().min(1),
4354
- args: import_zod2.z.unknown().optional(),
4562
+ args: import_zod2.z.unknown().optional().describe("Forwarded to list/read includes. Count includes ignore pagination fields so counts remain exact."),
4355
4563
  options: import_zod2.z.unknown().optional()
4356
4564
  }).passthrough();
4357
4565
  var includeSchema = import_zod2.z.union([includeItemSchema, import_zod2.z.array(includeItemSchema)]);
@@ -4888,7 +5096,7 @@ var import_zod4 = require("zod");
4888
5096
  var dataListBodySchema = import_zod4.z.object({
4889
5097
  filter: objectOrArraySchema.optional(),
4890
5098
  select: projectionSchema.optional(),
4891
- sort: import_zod4.z.string().optional(),
5099
+ sort: sortSchema.optional(),
4892
5100
  skip: import_zod4.z.union([nonNegativeIntegerSchema, nonNegativeIntegerString]).optional(),
4893
5101
  limit: import_zod4.z.union([positiveIntegerSchema, positiveIntegerString]).optional(),
4894
5102
  page: import_zod4.z.union([nonNegativeIntegerSchema, nonNegativeIntegerString]).optional(),
@@ -4980,7 +5188,7 @@ var rootModelSubReadArgsSchema = import_zod5.z.object({
4980
5188
  var rootModelCountOptionsSchema = import_zod5.z.object({}).strict().optional();
4981
5189
  var rootDataListArgsSchema = import_zod5.z.object({
4982
5190
  select: projectionSchema.optional(),
4983
- sort: import_zod5.z.string().optional(),
5191
+ sort: sortSchema.optional(),
4984
5192
  skip: import_zod5.z.union([nonNegativeIntegerSchema, nonNegativeIntegerString]).optional(),
4985
5193
  limit: import_zod5.z.union([positiveIntegerSchema, positiveIntegerString]).optional(),
4986
5194
  page: import_zod5.z.union([nonNegativeIntegerSchema, nonNegativeIntegerString]).optional(),
@@ -6220,7 +6428,12 @@ async function decorateDataListResult(svc, result, context = {}) {
6220
6428
  operation: "list",
6221
6429
  resolvedQuery: result.query
6222
6430
  };
6223
- const decoratedDocs = await Promise.all(result.data.map((doc) => svc.decorate(doc, "list", decorateContext)));
6431
+ const { maxHookConcurrency } = svc.getRequestComplexity();
6432
+ const decoratedDocs = await mapWithConcurrencyLimit(
6433
+ result.data,
6434
+ maxHookConcurrency,
6435
+ (doc) => svc.decorate(doc, "list", decorateContext)
6436
+ );
6224
6437
  const data = await svc.decorateAll(decoratedDocs, "list", decorateContext);
6225
6438
  return {
6226
6439
  ...result,
@@ -6588,7 +6801,7 @@ var DataRouter = class {
6588
6801
  const svc = this.getService(req);
6589
6802
  const result = await svc.find(
6590
6803
  filter2 ?? {},
6591
- { select, sort: typeof sort === "string" ? sort : void 0, skip, limit, page, pageSize },
6804
+ { select, sort, skip, limit, page, pageSize },
6592
6805
  { includeCount }
6593
6806
  );
6594
6807
  handleResultError(result);