@web-ts-toolkit/access-router 0.32.0 → 0.33.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.mjs CHANGED
@@ -1,16 +1,16 @@
1
1
  // src/index.ts
2
- import { isPlainObject as isPlainObject14, isString as isString12, isUndefined as isUndefined4 } from "@web-ts-toolkit/utils";
2
+ import { isPlainObject as isPlainObject17, isString as isString10, isUndefined as isUndefined4 } from "@web-ts-toolkit/utils";
3
3
 
4
4
  // src/middleware.ts
5
5
  import JsonRouter2 from "@web-ts-toolkit/express-json-router";
6
- import { isArray as isArray10, isFunction as isFunction6, isPlainObject as isPlainObject11, isString as isString7 } from "@web-ts-toolkit/utils";
6
+ import { isArray as isArray11, isFunction as isFunction6, isPlainObject as isPlainObject14, isString as isString7 } from "@web-ts-toolkit/utils";
7
7
 
8
8
  // src/core.ts
9
9
  import {
10
- castArray as castArray5,
10
+ castArray as castArray4,
11
11
  compact as compact3,
12
12
  forEach as forEach7,
13
- isArray as isArray9,
13
+ isArray as isArray10,
14
14
  isBoolean as isBoolean5,
15
15
  isFunction as isFunction5,
16
16
  isString as isString6,
@@ -25,11 +25,11 @@ import {
25
25
  addLeadingSlash,
26
26
  forEach as forEach2,
27
27
  get as get3,
28
- isArray as isArray4,
28
+ isArray as isArray5,
29
29
  isBoolean,
30
30
  isFunction,
31
31
  isNil as isNil2,
32
- isPlainObject as isPlainObject7,
32
+ isPlainObject as isPlainObject9,
33
33
  isString as isString4,
34
34
  keys
35
35
  } from "@web-ts-toolkit/utils";
@@ -52,7 +52,13 @@ import { Document } from "mongoose";
52
52
  import { get, isPlainObject as isPlainObject2, pick, set } from "@web-ts-toolkit/utils";
53
53
 
54
54
  // src/helpers/query.ts
55
- import { flattenDeep, isNaN, isNil, isPlainObject, isString, reduce } from "@web-ts-toolkit/utils";
55
+ import { flattenDeep, isNil, isPlainObject, isString, reduce } from "@web-ts-toolkit/utils";
56
+ var normalizeSafeInteger = (value, min) => {
57
+ if (isNil(value)) return null;
58
+ const parsed = Number(value);
59
+ if (!Number.isSafeInteger(parsed) || parsed < min) return null;
60
+ return parsed;
61
+ };
56
62
  function genPagination({
57
63
  skip,
58
64
  limit,
@@ -60,13 +66,21 @@ function genPagination({
60
66
  pageSize
61
67
  }, hardLimit) {
62
68
  let _skip = 0;
63
- let _limit = Number(limit ?? pageSize);
64
- if (isNaN(_limit) || _limit > hardLimit) _limit = hardLimit;
65
- if (!isNil(skip)) {
66
- _skip = Number(skip);
67
- } else if (!isNil(page)) {
68
- const npage = Number(page);
69
- if (npage > 1) _skip = (npage - 1) * _limit;
69
+ let _limit = normalizeSafeInteger(limit ?? pageSize, 1) ?? hardLimit;
70
+ if (!Number.isSafeInteger(_limit) || _limit > hardLimit) _limit = hardLimit;
71
+ const normalizedSkip = normalizeSafeInteger(skip, 0);
72
+ const normalizedPage = normalizeSafeInteger(page, 0);
73
+ if (normalizedSkip !== null) {
74
+ _skip = normalizedSkip;
75
+ } else if (normalizedPage !== null) {
76
+ const npage = normalizedPage;
77
+ if (npage > 1) {
78
+ const derivedSkip = (npage - 1) * _limit;
79
+ _skip = Number.isSafeInteger(derivedSkip) ? derivedSkip : Number.MAX_SAFE_INTEGER;
80
+ }
81
+ }
82
+ if (!Number.isSafeInteger(_skip) || _skip < 0) {
83
+ return { skip: 0, limit: _limit };
70
84
  }
71
85
  return { skip: _skip, limit: _limit };
72
86
  }
@@ -135,6 +149,7 @@ async function populateDoc(doc, target) {
135
149
  if (isPromise(p)) return p;
136
150
  return "execPopulate" in p && p.execPopulate?.();
137
151
  }
152
+ var normalizeSubPopulate = (populate) => isArray(populate) ? populate.map((item) => isString2(item) ? { path: item } : item) : isString2(populate) ? { path: populate } : populate ?? [];
138
153
  var genSubPopulate = (sub, popul) => {
139
154
  if (!popul) return [];
140
155
  let populate = isArray(popul) ? popul : [popul];
@@ -150,7 +165,7 @@ var genSubPopulate = (sub, popul) => {
150
165
 
151
166
  // src/helpers/errors.ts
152
167
  import JsonRouter from "@web-ts-toolkit/express-json-router";
153
- var { BadRequestError, ForbiddenError, NotFoundError, UnprocessableEntityError } = JsonRouter.clientErrors;
168
+ var { BadRequestError, ForbiddenError, NotFoundError, UnauthorizedError, UnprocessableEntityError } = JsonRouter.clientErrors;
154
169
  function mapCodeToMessage(code) {
155
170
  switch (code) {
156
171
  case "success" /* Success */:
@@ -159,6 +174,8 @@ function mapCodeToMessage(code) {
159
174
  return "Created";
160
175
  case "bad_request" /* BadRequest */:
161
176
  return "Bad Request";
177
+ case "unauthorized" /* Unauthorized */:
178
+ return "Unauthorized";
162
179
  case "forbidden" /* Forbidden */:
163
180
  return "Forbidden";
164
181
  case "not_found" /* NotFound */:
@@ -175,6 +192,8 @@ function mapCodeToStatusCode(code) {
175
192
  return 201 /* Created */;
176
193
  case "bad_request" /* BadRequest */:
177
194
  return 400 /* BadRequest */;
195
+ case "unauthorized" /* Unauthorized */:
196
+ return 401 /* Unauthorized */;
178
197
  case "forbidden" /* Forbidden */:
179
198
  return 403 /* Forbidden */;
180
199
  case "not_found" /* NotFound */:
@@ -185,11 +204,14 @@ function mapCodeToStatusCode(code) {
185
204
  }
186
205
  function handleResultError(result) {
187
206
  if (result.success) return;
207
+ if (result.kind !== "error") return;
188
208
  const { code, errors = [] } = result;
189
209
  const errorOptions = { errors };
190
210
  switch (code) {
191
211
  case "bad_request" /* BadRequest */:
192
212
  throw new BadRequestError("Bad Request", errorOptions);
213
+ case "unauthorized" /* Unauthorized */:
214
+ throw new UnauthorizedError("Unauthorized", errorOptions);
193
215
  case "forbidden" /* Forbidden */:
194
216
  throw new ForbiddenError("Forbidden", errorOptions);
195
217
  case "not_found" /* NotFound */:
@@ -516,22 +538,102 @@ function buildOpenApiSpec(routes, options) {
516
538
  };
517
539
  }
518
540
 
541
+ // src/openapi/errors.ts
542
+ var OpenApiCollisionError = class extends Error {
543
+ constructor(message, details) {
544
+ super(message);
545
+ this.name = "OpenApiCollisionError";
546
+ this.collisionKind = details.collisionKind;
547
+ this.method = details.method;
548
+ this.path = details.path;
549
+ this.operationId = details.operationId;
550
+ this.existing = details.existing;
551
+ this.incoming = details.incoming;
552
+ }
553
+ };
554
+
519
555
  // src/openapi/registry.ts
556
+ var isPlainObject7 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
557
+ var stableKey = (value) => {
558
+ if (value === null || typeof value === "undefined") return "null";
559
+ if (typeof value === "function") return "[Function]";
560
+ if (typeof value !== "object") return JSON.stringify(value);
561
+ if (Array.isArray(value)) {
562
+ return `[${value.map(stableKey).join(",")}]`;
563
+ }
564
+ const obj = value;
565
+ return `{${Object.keys(obj).sort().map((k) => `${JSON.stringify(k)}:${stableKey(obj[k])}`).join(",")}}`;
566
+ };
567
+ var descriptorsEqual = (a, b) => {
568
+ return a.method === b.method && a.path === b.path && (a.operationId ?? "") === (b.operationId ?? "") && (a.acl ?? "") === (b.acl ?? "") && stableKey(a.query) === stableKey(b.query) && stableKey(a.body) === stableKey(b.body) && stableKey(a.pathParams) === stableKey(b.pathParams) && stableKey(a.responses) === stableKey(b.responses) && stableKey(a.tags) === stableKey(b.tags) && (a.summary ?? "") === (b.summary ?? "") && (a.description ?? "") === (b.description ?? "") && Boolean(a.deprecated) === Boolean(b.deprecated);
569
+ };
520
570
  var OpenApiRegistry = class {
521
- constructor() {
571
+ constructor(options = {}) {
522
572
  this.routes = [];
573
+ this.options = {
574
+ rejectConflicts: options.rejectConflicts ?? true,
575
+ rejectDuplicateOperationIds: options.rejectDuplicateOperationIds ?? true
576
+ };
523
577
  }
524
578
  register(route) {
525
579
  const existingIndex = this.routes.findIndex((item) => item.method === route.method && item.path === route.path);
526
580
  if (existingIndex === -1) {
581
+ this.assertOperationIdAvailable(route);
527
582
  this.routes.push(route);
528
583
  return;
529
584
  }
585
+ const existing = this.routes[existingIndex];
586
+ if (route.allowReplace) {
587
+ this.assertOperationIdAvailable(route, existing);
588
+ this.routes[existingIndex] = route;
589
+ return;
590
+ }
591
+ if (descriptorsEqual(existing, route) && (route.idempotent || isPlainObject7(existing) && existing.idempotent)) {
592
+ return;
593
+ }
594
+ if (this.options.rejectConflicts) {
595
+ throw new OpenApiCollisionError(
596
+ `OpenAPI route collision: ${route.method.toUpperCase()} ${route.path} is already registered` + (existing.operationId ? ` as operationId="${existing.operationId}"` : "") + (route.operationId ? `; new registration uses operationId="${route.operationId}"` : "") + ". Set allowReplace:true on the new descriptor to override, or fix the duplicate registration.",
597
+ {
598
+ collisionKind: "path",
599
+ method: route.method,
600
+ path: route.path,
601
+ existing,
602
+ incoming: route
603
+ }
604
+ );
605
+ }
530
606
  this.routes[existingIndex] = route;
531
607
  }
608
+ assertOperationIdAvailable(route, replacing) {
609
+ if (!this.options.rejectDuplicateOperationIds) return;
610
+ if (!route.operationId) return;
611
+ const clash = this.routes.find(
612
+ (item) => item.operationId && item.operationId === route.operationId && item !== replacing
613
+ );
614
+ if (clash) {
615
+ if (route.idempotent && clash.idempotent) return;
616
+ throw new OpenApiCollisionError(
617
+ `OpenAPI operationId collision: "${route.operationId}" is already bound to ${clash.method.toUpperCase()} ${clash.path}. Each operationId must be unique across the spec.`,
618
+ {
619
+ collisionKind: "operationId",
620
+ operationId: route.operationId,
621
+ existing: clash,
622
+ incoming: route
623
+ }
624
+ );
625
+ }
626
+ }
532
627
  getRoutes() {
533
628
  return [...this.routes];
534
629
  }
630
+ clear() {
631
+ this.routes.length = 0;
632
+ }
633
+ setStrictMode(enabled) {
634
+ this.options.rejectConflicts = enabled;
635
+ this.options.rejectDuplicateOperationIds = enabled;
636
+ }
535
637
  getSpec(info) {
536
638
  return buildOpenApiSpec(this.routes, info);
537
639
  }
@@ -553,8 +655,8 @@ var defaultLogger = createLogger({
553
655
  format.label({ label: "[WTT]" }),
554
656
  format.timestamp(),
555
657
  format.printf(({ level, label, message, timestamp, stack, ...meta }) => {
556
- const formattedLabel = colorizer.colorize(level, label);
557
- const formattedLevel = colorizer.colorize(level, `[${level.toUpperCase()}]`);
658
+ const formattedLabel = colorizer.colorize(level, String(label));
659
+ const formattedLevel = colorizer.colorize(level, `[${String(level).toUpperCase()}]`);
558
660
  const formattedMessage = colorizer.colorize(level, inspectValue(stack ?? message));
559
661
  const formattedMeta = Object.keys(meta).length > 0 ? ` ${inspectValue(meta)}` : "";
560
662
  return `${formattedLabel} ${timestamp} ${formattedLevel} ${formattedMessage}${formattedMeta}`;
@@ -589,8 +691,8 @@ var OptionsManager = class {
589
691
  this.currentOptions = new Proxy({}, {
590
692
  set(target, key, value) {
591
693
  const keystr = String(key);
592
- const oldvalue = target[key];
593
- target[key] = value;
694
+ const oldvalue = target[keystr];
695
+ target[keystr] = value;
594
696
  _this.listeners[keystr] && _this.listeners[keystr].call(_this, value, keystr, target, oldvalue);
595
697
  return true;
596
698
  }
@@ -618,8 +720,116 @@ var OptionsManager = class {
618
720
  }
619
721
  };
620
722
 
723
+ // src/request-complexity.ts
724
+ import { isArray as isArray4, isPlainObject as isPlainObject8 } from "@web-ts-toolkit/utils";
725
+ var defaultRequestComplexity = {
726
+ maxDepth: 8,
727
+ maxNodes: 500,
728
+ maxLogicalClauses: 50,
729
+ maxInValues: 100,
730
+ maxBulkItems: 100,
731
+ maxIncludeCount: 10,
732
+ maxSubQueryCount: 10,
733
+ maxBulkConcurrency: 10
734
+ };
735
+ var normalizePositiveInteger = (value, fallback) => {
736
+ return Number.isSafeInteger(value) && Number(value) > 0 ? Number(value) : fallback;
737
+ };
738
+ var resolveRequestComplexity = (options) => ({
739
+ maxDepth: normalizePositiveInteger(options?.maxDepth, defaultRequestComplexity.maxDepth),
740
+ maxNodes: normalizePositiveInteger(options?.maxNodes, defaultRequestComplexity.maxNodes),
741
+ maxLogicalClauses: normalizePositiveInteger(options?.maxLogicalClauses, defaultRequestComplexity.maxLogicalClauses),
742
+ maxInValues: normalizePositiveInteger(options?.maxInValues, defaultRequestComplexity.maxInValues),
743
+ maxBulkItems: normalizePositiveInteger(options?.maxBulkItems, defaultRequestComplexity.maxBulkItems),
744
+ maxIncludeCount: normalizePositiveInteger(options?.maxIncludeCount, defaultRequestComplexity.maxIncludeCount),
745
+ maxSubQueryCount: normalizePositiveInteger(options?.maxSubQueryCount, defaultRequestComplexity.maxSubQueryCount),
746
+ maxBulkConcurrency: normalizePositiveInteger(
747
+ options?.maxBulkConcurrency,
748
+ defaultRequestComplexity.maxBulkConcurrency
749
+ )
750
+ });
751
+ function validateRequestComplexity(value, options, scope = "request") {
752
+ const limits = resolveRequestComplexity(options);
753
+ const errors = [];
754
+ let nodes = 0;
755
+ let logicalClauses = 0;
756
+ let includeCount = 0;
757
+ let subQueryCount = 0;
758
+ const pointer = (path) => path.length === 0 ? "#" : `#/${path.join("/")}`;
759
+ const pushError = (detail, path) => {
760
+ if (errors.length === 0) {
761
+ errors.push({ detail, pointer: pointer(path) });
762
+ }
763
+ };
764
+ const visit = (current, path, depth, parentKey) => {
765
+ if (errors.length > 0) return;
766
+ nodes += 1;
767
+ if (nodes > limits.maxNodes) {
768
+ pushError(`Request exceeds maximum node budget of ${limits.maxNodes}`, path);
769
+ return;
770
+ }
771
+ if (depth > limits.maxDepth) {
772
+ pushError(`Request exceeds maximum depth of ${limits.maxDepth}`, path);
773
+ return;
774
+ }
775
+ if (isArray4(current)) {
776
+ if (parentKey === "$and" || parentKey === "$or" || parentKey === "$nor") {
777
+ logicalClauses += current.length;
778
+ if (logicalClauses > limits.maxLogicalClauses) {
779
+ pushError(`Filter exceeds maximum logical clause budget of ${limits.maxLogicalClauses}`, path);
780
+ return;
781
+ }
782
+ }
783
+ if (parentKey === "$in") {
784
+ if (current.length > limits.maxInValues) {
785
+ pushError(`Filter exceeds maximum $in values of ${limits.maxInValues}`, path);
786
+ return;
787
+ }
788
+ }
789
+ for (let index = 0; index < current.length; index++) {
790
+ visit(current[index], path.concat(index), depth + 1, parentKey);
791
+ if (errors.length > 0) return;
792
+ }
793
+ return;
794
+ }
795
+ if (!isPlainObject8(current)) return;
796
+ for (const [key, child] of Object.entries(current)) {
797
+ if (key === "__proto__" || key === "prototype" || key === "constructor") {
798
+ pushError(`Unsupported key: ${key}`, path.concat(key));
799
+ return;
800
+ }
801
+ if (scope === "request" && key === "include") {
802
+ includeCount += isArray4(child) ? child.length : 1;
803
+ if (includeCount > limits.maxIncludeCount) {
804
+ pushError(`Request exceeds maximum include count of ${limits.maxIncludeCount}`, path.concat(key));
805
+ return;
806
+ }
807
+ }
808
+ if (key === "$$sq") {
809
+ subQueryCount += 1;
810
+ if (subQueryCount > limits.maxSubQueryCount) {
811
+ pushError(`Request exceeds maximum subquery count of ${limits.maxSubQueryCount}`, path.concat(key));
812
+ return;
813
+ }
814
+ visit(child, path.concat(key), depth + 1, key);
815
+ if (errors.length > 0) return;
816
+ continue;
817
+ }
818
+ visit(child, path.concat(key), depth + 1, key);
819
+ if (errors.length > 0) return;
820
+ }
821
+ };
822
+ visit(value, [], 0);
823
+ return errors;
824
+ }
825
+
621
826
  // src/runtime.ts
622
- mschema2Jsonschema(mongoose);
827
+ var mongooseJsonSchemaInitialized = false;
828
+ var ensureMongooseJsonSchemaInitialized = () => {
829
+ if (mongooseJsonSchemaInitialized) return;
830
+ mschema2Jsonschema(mongoose);
831
+ mongooseJsonSchemaInitialized = true;
832
+ };
623
833
  var pluralize = mongoose.pluralize();
624
834
  var FIELD_ACCESS_KEYS = ["list", "create", "read", "update"];
625
835
  var defaultModelOptions = {
@@ -628,6 +838,7 @@ var defaultModelOptions = {
628
838
  };
629
839
  var defaultDataOptions = {
630
840
  basePath: null,
841
+ parentPath: "/",
631
842
  queryRouteSegment: "__query"
632
843
  };
633
844
  var normalizeBasePath = (name, value) => {
@@ -643,9 +854,9 @@ var hasModelPermission = (value, modelPermissionPrefix) => {
643
854
  if (isString4(value)) {
644
855
  return value.trim().split(" ").some((item) => item.startsWith(modelPermissionPrefix));
645
856
  }
646
- if (isArray4(value)) {
857
+ if (isArray5(value)) {
647
858
  return value.some((item) => {
648
- if (isString4(item) || isArray4(item)) {
859
+ if (isString4(item) || isArray5(item)) {
649
860
  return hasModelPermission(item, modelPermissionPrefix);
650
861
  }
651
862
  return true;
@@ -659,13 +870,13 @@ var classifyPermissionSchema = (permissionSchema, modelPermissionPrefix) => {
659
870
  const modelPermissionKeys = {};
660
871
  forEach2(schemaKeys, (schemaKey) => {
661
872
  const schemaRule = permissionSchema[schemaKey];
662
- const ruleEntries = isPlainObject7(schemaRule) ? Object.entries(schemaRule) : FIELD_ACCESS_KEYS.map((accessKey) => [accessKey, schemaRule]);
873
+ const ruleEntries = isPlainObject9(schemaRule) ? Object.entries(schemaRule) : FIELD_ACCESS_KEYS.map((accessKey) => [accessKey, schemaRule]);
663
874
  for (let x = 0; x < ruleEntries.length; x++) {
664
875
  const [accessKey, value] = ruleEntries[x];
665
- if (!isArray4(globalPermissionKeys[accessKey])) {
876
+ if (!isArray5(globalPermissionKeys[accessKey])) {
666
877
  globalPermissionKeys[accessKey] = [];
667
878
  }
668
- if (!isArray4(modelPermissionKeys[accessKey])) {
879
+ if (!isArray5(modelPermissionKeys[accessKey])) {
669
880
  modelPermissionKeys[accessKey] = [];
670
881
  }
671
882
  if (isBoolean(value)) {
@@ -690,7 +901,8 @@ var AccessRuntime = class {
690
901
  this.globalOptions = new OptionsManager({
691
902
  requestPermissionField: "_permissions",
692
903
  globalPermissions: () => ({}),
693
- logger: defaultLogger
904
+ logger: defaultLogger,
905
+ requestComplexity: defaultRequestComplexity
694
906
  }).build();
695
907
  this.defaultModelOptions = new OptionsManager({
696
908
  listHardLimit: 1e3,
@@ -709,14 +921,50 @@ var AccessRuntime = class {
709
921
  this.modelRefs = {};
710
922
  this.modelSubs = {};
711
923
  this.modelAtts = {};
924
+ this.modelInstances = {};
712
925
  this.openApiRegistry = new OpenApiRegistry();
926
+ ensureMongooseJsonSchemaInitialized();
927
+ }
928
+ registerModelInstance(modelName, model) {
929
+ if (!modelName || typeof modelName !== "string") {
930
+ throw new TypeError(`registerModelInstance: modelName must be a non-empty string, received ${typeof modelName}`);
931
+ }
932
+ const existing = this.modelInstances[modelName];
933
+ if (existing && existing !== model) {
934
+ throw new Error(
935
+ `Runtime model registry conflict: model "${modelName}" is already registered to a different mongoose.Model instance on this runtime. Use a distinct model name or a separate runtime.`
936
+ );
937
+ }
938
+ this.modelInstances[modelName] = model;
939
+ }
940
+ hasModelInstance(modelName) {
941
+ return modelName in this.modelInstances;
942
+ }
943
+ getModelInstance(modelName) {
944
+ const registered = this.modelInstances[modelName];
945
+ if (registered) return registered;
946
+ const global = mongoose.models[modelName];
947
+ return global ?? null;
713
948
  }
714
949
  registerOpenApiRoute(route) {
715
950
  this.openApiRegistry.register(route);
716
951
  }
952
+ /**
953
+ * Enable strict OpenAPI collision detection for the remainder of this
954
+ * runtime's lifetime. After this call, conflicting method/path
955
+ * registrations and duplicate operationIds throw {@link OpenApiCollisionError}
956
+ * instead of silently replacing. Existing already-registered routes are
957
+ * not retroactively re-validated.
958
+ */
959
+ enableOpenApiCollisionDetection() {
960
+ this.openApiRegistry.setStrictMode(true);
961
+ }
717
962
  getOpenApiRoutes() {
718
963
  return this.openApiRegistry.getRoutes();
719
964
  }
965
+ clearOpenApiRoutes() {
966
+ this.openApiRegistry.clear();
967
+ }
720
968
  getOpenApiSpec(info) {
721
969
  return this.openApiRegistry.getSpec(info);
722
970
  }
@@ -745,7 +993,7 @@ var AccessRuntime = class {
745
993
  return this.defaultModelOptions.get(key, defaultValue);
746
994
  }
747
995
  createModelOptions(modelName) {
748
- const model = mongoose.model(modelName);
996
+ const model = this.getModelInstance(modelName) ?? mongoose.model(modelName);
749
997
  const manager = new OptionsManager({
750
998
  ...defaultModelOptions,
751
999
  modelName
@@ -861,7 +1109,7 @@ var AccessRuntime = class {
861
1109
  if (modelName in this.modelRefs && modelName in this.modelSubs && modelName in this.modelAtts) {
862
1110
  return;
863
1111
  }
864
- const model = mongoose.models[modelName];
1112
+ const model = this.getModelInstance(modelName);
865
1113
  if (!model) {
866
1114
  this.modelRefs[modelName] = this.modelRefs[modelName] ?? {};
867
1115
  this.modelSubs[modelName] = this.modelSubs[modelName] ?? [];
@@ -891,8 +1139,6 @@ var defaultRuntime = new AccessRuntime();
891
1139
 
892
1140
  // src/runtime-context.ts
893
1141
  import { AsyncLocalStorage } from "async_hooks";
894
- import mongoose2 from "mongoose";
895
- var ACCESS_RUNTIME = /* @__PURE__ */ Symbol("access-runtime");
896
1142
  var runtimeStorage = new AsyncLocalStorage();
897
1143
  function runWithRuntime(runtime, callback) {
898
1144
  return runtimeStorage.run(runtime, callback);
@@ -900,16 +1146,6 @@ function runWithRuntime(runtime, callback) {
900
1146
  function getActiveRuntime() {
901
1147
  return runtimeStorage.getStore() ?? null;
902
1148
  }
903
- function attachRuntimeToModel(modelName, runtime) {
904
- const model = mongoose2.models[modelName];
905
- if (model) {
906
- model[ACCESS_RUNTIME] = runtime;
907
- }
908
- }
909
- function getRuntimeForModelName(modelName) {
910
- const model = mongoose2.models[modelName];
911
- return model?.[ACCESS_RUNTIME] ?? null;
912
- }
913
1149
 
914
1150
  // src/options/global-options.ts
915
1151
  var getRuntime = () => getActiveRuntime() ?? defaultRuntime;
@@ -927,27 +1163,36 @@ var getGlobalOption = (key, defaultValue) => {
927
1163
  };
928
1164
 
929
1165
  // src/options/model-options.ts
930
- var getRuntime2 = (modelName) => getRuntimeForModelName(modelName) ?? getActiveRuntime() ?? defaultRuntime;
1166
+ var getRuntime2 = () => getActiveRuntime() ?? defaultRuntime;
931
1167
  var setModelOptions = (modelName, options) => {
932
- getRuntime2(modelName).setModelOptions(modelName, options);
1168
+ getRuntime2().setModelOptions(modelName, options);
933
1169
  };
934
1170
  var setModelOption = (modelName, key, value) => {
935
- getRuntime2(modelName).setModelOption(modelName, key, value);
1171
+ getRuntime2().setModelOption(modelName, key, value);
936
1172
  };
937
1173
  var getModelOptions = (modelName) => {
938
- return getRuntime2(modelName).getModelOptions(modelName);
1174
+ return getRuntime2().getModelOptions(modelName);
939
1175
  };
940
1176
  var getModelOption = (modelName, key, defaultValue) => {
941
- return getRuntime2(modelName).getModelOption(modelName, key, defaultValue);
1177
+ return getRuntime2().getModelOption(modelName, key, defaultValue);
942
1178
  };
943
1179
  var getExactModelOption = (modelName, key) => {
944
- return getRuntime2(modelName).getExactModelOption(modelName, key);
1180
+ return getRuntime2().getExactModelOption(modelName, key);
945
1181
  };
946
1182
  var getModelNames = () => {
947
1183
  return (getActiveRuntime() ?? defaultRuntime).getModelNames();
948
1184
  };
949
1185
  var getModelJsonSchema = (modelName) => {
950
- return getRuntime2(modelName).getModelJsonSchema(modelName);
1186
+ return getRuntime2().getModelJsonSchema(modelName);
1187
+ };
1188
+ var registerModelInstance = (modelName, model) => {
1189
+ getRuntime2().registerModelInstance(modelName, model);
1190
+ };
1191
+ var hasModelInstance = (modelName) => {
1192
+ return getRuntime2().hasModelInstance(modelName);
1193
+ };
1194
+ var getModelInstance = (modelName) => {
1195
+ return getRuntime2().getModelInstance(modelName);
951
1196
  };
952
1197
 
953
1198
  // src/options/default-model-options.ts
@@ -975,12 +1220,12 @@ var getDataOption = (dataName, key, defaultValue) => {
975
1220
  };
976
1221
 
977
1222
  // src/meta.ts
978
- var getRuntime5 = (modelName) => getRuntimeForModelName(modelName) ?? getActiveRuntime() ?? defaultRuntime;
1223
+ var getRuntime5 = () => getActiveRuntime() ?? defaultRuntime;
979
1224
  var getModelRef = (modelName, refPath) => {
980
- return getRuntime5(modelName).getModelRef(modelName, refPath);
1225
+ return getRuntime5().getModelRef(modelName, refPath);
981
1226
  };
982
1227
  var getModelSub = (modelName) => {
983
- return getRuntime5(modelName).getModelSub(modelName);
1228
+ return getRuntime5().getModelSub(modelName);
984
1229
  };
985
1230
 
986
1231
  // src/services/base.ts
@@ -991,19 +1236,29 @@ import {
991
1236
  forEach as forEach3,
992
1237
  get as get4,
993
1238
  intersectionBy,
994
- isArray as isArray5,
995
- isPlainObject as isPlainObject8,
1239
+ isArray as isArray6,
1240
+ isPlainObject as isPlainObject10,
996
1241
  uniq
997
1242
  } from "@web-ts-toolkit/utils";
1243
+ var ClientRequestError = class extends Error {
1244
+ constructor(result) {
1245
+ super(String(result.code));
1246
+ this.result = result;
1247
+ }
1248
+ };
998
1249
  function validateClientFilter(filter2) {
999
1250
  const errors = [];
1251
+ const complexityErrors = validateRequestComplexity(filter2, getGlobalOption("requestComplexity"), "filter");
1252
+ if (complexityErrors.length > 0) {
1253
+ return complexityErrors.map((error2) => error2.detail);
1254
+ }
1000
1255
  const blockedOperators = /* @__PURE__ */ new Set(["$where", "$expr", "$function", "$accumulator"]);
1001
1256
  const visit = (value, path) => {
1002
- if (isArray5(value)) {
1257
+ if (isArray6(value)) {
1003
1258
  value.forEach((item, index) => visit(item, `${path}[${index}]`));
1004
1259
  return;
1005
1260
  }
1006
- if (!isPlainObject8(value)) return;
1261
+ if (!isPlainObject10(value)) return;
1007
1262
  Object.entries(value).forEach(([key, child]) => {
1008
1263
  const nextPath = path ? `${path}.${key}` : key;
1009
1264
  if (blockedOperators.has(key)) {
@@ -1097,6 +1352,31 @@ var Base = class {
1097
1352
  validateClientFilter(filter2) {
1098
1353
  return validateClientFilter(filter2);
1099
1354
  }
1355
+ getRequestComplexity() {
1356
+ return resolveRequestComplexity(getGlobalOption("requestComplexity"));
1357
+ }
1358
+ getClientRequestErrorResult(error2) {
1359
+ return error2 instanceof ClientRequestError ? error2.result : null;
1360
+ }
1361
+ throwClientRequestError(code, detail) {
1362
+ throw new ClientRequestError({
1363
+ success: false,
1364
+ kind: "error",
1365
+ code,
1366
+ errors: [{ detail }]
1367
+ });
1368
+ }
1369
+ async getAuthorizedTargetService(modelName, op) {
1370
+ const runtime = getActiveRuntime();
1371
+ if (runtime && !runtime.hasModel(modelName)) {
1372
+ this.throwClientRequestError("bad_request" /* BadRequest */, `Model ${modelName} not found`);
1373
+ }
1374
+ const allowed = await this.req.macl.isAllowed(modelName, op);
1375
+ if (!allowed) {
1376
+ this.throwClientRequestError("unauthorized" /* Unauthorized */, "Unauthorized");
1377
+ }
1378
+ return this.req.macl.getPublicService(modelName);
1379
+ }
1100
1380
  processInclude(include) {
1101
1381
  const includes = compact(castArray(include)).filter(({ model, op, path, localField, foreignField }) => {
1102
1382
  return model && op && path && localField && foreignField;
@@ -1119,65 +1399,73 @@ var Base = class {
1119
1399
  if (!include) return docs;
1120
1400
  const includes = compact(castArray(include));
1121
1401
  if (includes.length === 0) return docs;
1122
- const isSingle = !isArray5(docs);
1123
- if (isSingle) docs = [docs];
1402
+ const isSingle = !isArray6(docs);
1403
+ let docList = isSingle ? [docs] : docs;
1124
1404
  for (let x = 0; x < includes.length; x++) {
1125
1405
  const include2 = includes[x];
1126
1406
  if (include2.op === "count") {
1127
- docs = await this.includeDocsCount(docs, include2);
1407
+ docList = await this.includeDocsCount(docList, include2);
1128
1408
  } else {
1129
- docs = await this.includeDocsList(docs, include2);
1409
+ docList = await this.includeDocsList(docList, include2);
1130
1410
  }
1131
1411
  }
1132
- return isSingle ? docs[0] : docs;
1412
+ return isSingle ? docList[0] : docList;
1133
1413
  }
1134
1414
  async includeDocsList(docs, include) {
1135
1415
  const { model, op, path, localField, foreignField, filter: _filters, args = {}, options = {} } = include;
1136
- const svc = this.req.macl.getPublicService(model);
1137
- if (!svc) return docs;
1416
+ const svc = await this.getAuthorizedTargetService(model, op);
1138
1417
  const includeLocalValues = [];
1139
1418
  forEach3(docs, (doc, i) => {
1140
1419
  includeLocalValues.push(get4(doc, localField));
1141
1420
  });
1142
1421
  const filter2 = { ..._filters ?? {}, [foreignField]: { $in: flatten(includeLocalValues) } };
1143
- const result = await svc.find(filter2, args, {
1422
+ const authorizedFilter = await svc.genFilter(op, filter2);
1423
+ const trustedArgs = {
1424
+ ...args,
1425
+ overrides: {
1426
+ filter: authorizedFilter
1427
+ }
1428
+ };
1429
+ const trustedOptions = {
1144
1430
  ...options,
1145
1431
  lean: true,
1146
1432
  includePermissions: false,
1147
1433
  includeCount: false
1148
- });
1149
- if (!result.success) return docs;
1434
+ };
1435
+ const trustedResult = await svc.find(filter2, trustedArgs, trustedOptions);
1436
+ if (!trustedResult.success) return docs;
1150
1437
  for (let y = 0; y < docs.length; y++) {
1151
1438
  const doc = docs[y];
1152
1439
  const localValue = get4(doc, localField);
1153
1440
  const filterFn = (row) => intersectionBy(castArray(localValue), castArray(get4(row, foreignField)), String).length > 0;
1154
- const matches = result.data.filter(filterFn);
1441
+ const matches = trustedResult.data.filter(filterFn);
1155
1442
  setDocValue(doc, path, op === "list" ? matches : matches[0]);
1156
1443
  }
1157
1444
  return docs;
1158
1445
  }
1159
1446
  async includeDocsCount(docs, include) {
1160
1447
  const { model, path, localField, foreignField, filter: _filters, args = {}, options = {} } = include;
1161
- const svc = this.req.macl.getPublicService(model);
1162
- if (!svc) return docs;
1448
+ const svc = await this.getAuthorizedTargetService(model, "count");
1163
1449
  const includeLocalValues = [];
1164
1450
  forEach3(docs, (doc) => {
1165
1451
  includeLocalValues.push(get4(doc, localField));
1166
1452
  });
1167
1453
  const filter2 = { ..._filters ?? {}, [foreignField]: { $in: flatten(includeLocalValues) } };
1168
- const result = await svc.find(
1169
- filter2,
1170
- {
1171
- ...args,
1172
- select: [foreignField]
1173
- },
1174
- {
1175
- ...options,
1176
- lean: true,
1177
- includePermissions: false,
1178
- includeCount: false
1454
+ const authorizedFilter = await svc.genFilter("list", filter2);
1455
+ const trustedArgs = {
1456
+ ...args,
1457
+ select: [foreignField],
1458
+ overrides: {
1459
+ filter: authorizedFilter
1179
1460
  }
1180
- );
1461
+ };
1462
+ const trustedOptions = {
1463
+ ...options,
1464
+ lean: true,
1465
+ includePermissions: false,
1466
+ includeCount: false
1467
+ };
1468
+ const result = await svc.find(filter2, trustedArgs, trustedOptions);
1181
1469
  if (!result.success) return docs;
1182
1470
  for (let y = 0; y < docs.length; y++) {
1183
1471
  const doc = docs[y];
@@ -1202,26 +1490,29 @@ var Base = class {
1202
1490
  }
1203
1491
  async handleSubQuery(sq, key) {
1204
1492
  const { model, op, id, filter: filter2, args, options, sqOptions = {} } = sq;
1205
- const svc = this.req.macl.getPublicService(model);
1206
- if (!svc) return null;
1207
1493
  let result;
1208
1494
  if (op === "list") {
1209
- result = await svc.find(filter2, args, options);
1495
+ const svc = await this.getAuthorizedTargetService(model, "list");
1496
+ result = await svc._list(filter2, args, options);
1210
1497
  } else if (op === "read") {
1498
+ const svc = await this.getAuthorizedTargetService(model, "read");
1499
+ const readOptions = { ...options, tryList: false };
1211
1500
  if (id) {
1212
- result = await svc.findById(id, args, options);
1501
+ result = await svc._read(id, args, readOptions);
1213
1502
  } else if (filter2) {
1214
- result = await svc.findOne(filter2, args, options);
1503
+ result = await svc._readFilter(filter2, args, readOptions);
1215
1504
  } else {
1216
- return null;
1505
+ this.throwClientRequestError("bad_request" /* BadRequest */, `Subquery for field ${key} requires an id or filter`);
1217
1506
  }
1218
1507
  } else {
1219
- return null;
1508
+ this.throwClientRequestError("bad_request" /* BadRequest */, `Unsupported subquery operation: ${op}`);
1509
+ }
1510
+ if (!result.success) {
1511
+ throw new ClientRequestError(result);
1220
1512
  }
1221
- if (!result.success) return null;
1222
1513
  let ret = result.data;
1223
1514
  if (sqOptions.path) {
1224
- ret = isArray5(ret) ? flatten(ret.map((v) => get4(v, sqOptions.path))) : get4(ret, sqOptions.path);
1515
+ ret = isArray6(ret) ? flatten(ret.map((v) => get4(v, sqOptions.path))) : get4(ret, sqOptions.path);
1225
1516
  }
1226
1517
  if (sqOptions.compact) {
1227
1518
  ret = compact(castArray(ret));
@@ -1239,7 +1530,7 @@ var Base = class {
1239
1530
  import {
1240
1531
  castArray as castArray2,
1241
1532
  compact as compact2,
1242
- isArray as isArray6,
1533
+ isArray as isArray7,
1243
1534
  isBoolean as isBoolean3,
1244
1535
  isFunction as isFunction3,
1245
1536
  omit,
@@ -1250,30 +1541,199 @@ import {
1250
1541
  import { diff } from "just-diff";
1251
1542
 
1252
1543
  // src/model.ts
1253
- import mongoose3 from "mongoose";
1544
+ import mongoose2 from "mongoose";
1254
1545
 
1255
- // src/logger.ts
1256
- var passthrough = (level) => (...args) => {
1257
- const globalLogger = getGlobalOption("logger");
1258
- const target = globalLogger?.[level] ?? defaultLogger[level];
1259
- return target.apply(globalLogger ?? defaultLogger, args);
1546
+ // src/logger-helpers.ts
1547
+ var SENSITIVE_FILTER_KEYS = /* @__PURE__ */ new Set([
1548
+ "password",
1549
+ "pwd",
1550
+ "secret",
1551
+ "token",
1552
+ "access_token",
1553
+ "refreshtoken",
1554
+ "api_key",
1555
+ "apikey",
1556
+ "authorization",
1557
+ "authtoken",
1558
+ "credentials",
1559
+ "privatekey",
1560
+ "ssn",
1561
+ "creditcard",
1562
+ "cardnumber",
1563
+ "cvv",
1564
+ "tenantid",
1565
+ "tenant"
1566
+ ]);
1567
+ var SENSITIVE_PAYLOAD_KEYS = new Set(SENSITIVE_FILTER_KEYS);
1568
+ var safeReplace = (value, seen) => {
1569
+ if (value === null || typeof value === "undefined") return value;
1570
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
1571
+ return value;
1572
+ }
1573
+ if (typeof value === "function") return "[Function]";
1574
+ if (typeof value === "object") {
1575
+ if (seen.has(value)) return "[Circular]";
1576
+ seen.add(value);
1577
+ try {
1578
+ if (Array.isArray(value)) {
1579
+ return value.map((entry) => safeReplace(entry, seen));
1580
+ }
1581
+ const out = {};
1582
+ for (const [k, v] of Object.entries(value)) {
1583
+ out[k] = safeReplace(v, seen);
1584
+ }
1585
+ return out;
1586
+ } finally {
1587
+ seen.delete(value);
1588
+ }
1589
+ }
1590
+ return value;
1591
+ };
1592
+ var redactObjectKeys = (value, sensitiveKeys, seen) => {
1593
+ if (value === null || typeof value !== "object") return value;
1594
+ if (seen.has(value)) return "[Circular]";
1595
+ seen.add(value);
1596
+ try {
1597
+ if (Array.isArray(value)) {
1598
+ return value.map((entry) => redactObjectKeys(entry, sensitiveKeys, seen));
1599
+ }
1600
+ const out = {};
1601
+ for (const [k, v] of Object.entries(value)) {
1602
+ const lowered = String(k).toLowerCase();
1603
+ if (sensitiveKeys.has(lowered)) {
1604
+ out[k] = "[REDACTED]";
1605
+ } else {
1606
+ out[k] = redactObjectKeys(v, sensitiveKeys, seen);
1607
+ }
1608
+ }
1609
+ return out;
1610
+ } finally {
1611
+ seen.delete(value);
1612
+ }
1613
+ };
1614
+ var redactFilter = (filter2) => {
1615
+ return redactObjectKeys(filter2, SENSITIVE_FILTER_KEYS, /* @__PURE__ */ new WeakSet());
1616
+ };
1617
+ var redactPayload = (payload) => {
1618
+ return redactObjectKeys(payload, SENSITIVE_PAYLOAD_KEYS, /* @__PURE__ */ new WeakSet());
1619
+ };
1620
+ var safeStringify = (value) => {
1621
+ try {
1622
+ return JSON.stringify(safeReplace(value, /* @__PURE__ */ new WeakSet()));
1623
+ } catch {
1624
+ return "[Unserializable]";
1625
+ }
1626
+ };
1627
+ var countFilterKeys = (filter2) => {
1628
+ if (filter2 === null || typeof filter2 === "undefined") return 0;
1629
+ if (typeof filter2 !== "object") return 0;
1630
+ if (Array.isArray(filter2)) return 0;
1631
+ let count = 0;
1632
+ const stack = [filter2];
1633
+ const seen = /* @__PURE__ */ new WeakSet();
1634
+ while (stack.length > 0) {
1635
+ const current = stack.pop();
1636
+ if (current === null || typeof current !== "object" || Array.isArray(current)) continue;
1637
+ if (seen.has(current)) continue;
1638
+ seen.add(current);
1639
+ for (const [key, value] of Object.entries(current)) {
1640
+ count += 1;
1641
+ if (typeof key === "string" && key.startsWith("$") && value !== null && typeof value === "object") {
1642
+ if (Array.isArray(value)) {
1643
+ for (const entry of value) {
1644
+ if (entry !== null && typeof entry === "object" && !Array.isArray(entry)) {
1645
+ stack.push(entry);
1646
+ }
1647
+ }
1648
+ } else {
1649
+ stack.push(value);
1650
+ }
1651
+ }
1652
+ }
1653
+ }
1654
+ return count;
1260
1655
  };
1261
- var logger = {
1262
- debug: passthrough("debug"),
1263
- info: passthrough("info"),
1264
- warn: passthrough("warn"),
1265
- error: passthrough("error")
1656
+ var summarizeFilter = (filter2) => {
1657
+ return { filterKeyValueCount: countFilterKeys(filter2) };
1658
+ };
1659
+ var resolveLogger = () => {
1660
+ return getGlobalOption("logger") ?? defaultLogger;
1661
+ };
1662
+ var isLevelEnabled = (level) => {
1663
+ const target = resolveLogger();
1664
+ const fn = target?.isLevelEnabled;
1665
+ if (typeof fn === "function") {
1666
+ try {
1667
+ return !!fn.call(target, level);
1668
+ } catch {
1669
+ return true;
1670
+ }
1671
+ }
1672
+ return true;
1673
+ };
1674
+ var buildOpMessage = (ctx) => {
1675
+ const trimmed = { op: ctx.op };
1676
+ if (ctx.modelName) trimmed.modelName = ctx.modelName;
1677
+ if (ctx.dataName) trimmed.dataName = ctx.dataName;
1678
+ if (ctx.page != null) trimmed.page = ctx.page;
1679
+ if (ctx.pageSize != null) trimmed.pageSize = ctx.pageSize;
1680
+ if (ctx.skip != null) trimmed.skip = ctx.skip;
1681
+ if (ctx.limit != null) trimmed.limit = ctx.limit;
1682
+ if (ctx.sort != null) trimmed.sort = ctx.sort;
1683
+ if (ctx.populateCount != null) trimmed.populateCount = ctx.populateCount;
1684
+ if (ctx.selectCount != null) trimmed.selectCount = ctx.selectCount;
1685
+ if (ctx.filterKeyValueCount != null) trimmed.filterKeyValueCount = ctx.filterKeyValueCount;
1686
+ if (ctx.startedAt != null && ctx.durationMs == null) {
1687
+ trimmed.startedAt = ctx.startedAt;
1688
+ }
1689
+ if (ctx.durationMs != null) trimmed.durationMs = ctx.durationMs;
1690
+ if (ctx.resultCode != null) trimmed.resultCode = ctx.resultCode;
1691
+ return safeStringify(trimmed);
1692
+ };
1693
+ var safeApply = (fn, thisArg, args) => {
1694
+ try {
1695
+ fn.apply(thisArg, args);
1696
+ } catch {
1697
+ }
1698
+ };
1699
+ var debug = (ctx) => {
1700
+ if (!isLevelEnabled("debug")) return;
1701
+ const target = resolveLogger();
1702
+ const fn = target?.debug ?? defaultLogger.debug;
1703
+ const message = buildOpMessage(ctx);
1704
+ safeApply(fn, target ?? defaultLogger, [message]);
1705
+ };
1706
+ var warn = (message, meta) => {
1707
+ const target = resolveLogger();
1708
+ const fn = target?.warn ?? defaultLogger.warn;
1709
+ safeApply(fn, target ?? defaultLogger, meta != null ? [message, meta] : [message]);
1710
+ };
1711
+ var logInfoMessage = (message, ...args) => {
1712
+ const target = resolveLogger();
1713
+ const fn = target?.info ?? defaultLogger.info;
1714
+ const payload = args.length > 0 ? [message, ...args] : [message];
1715
+ safeApply(fn, target ?? defaultLogger, payload);
1716
+ };
1717
+ var error = (error2) => {
1718
+ const target = resolveLogger();
1719
+ const fn = target?.error ?? defaultLogger.error;
1720
+ safeApply(fn, target ?? defaultLogger, [error2]);
1266
1721
  };
1267
1722
 
1268
1723
  // src/model.ts
1724
+ var safeSortLogError = (msg, ...args) => {
1725
+ const message = args.length > 0 ? `${msg} ${safeStringify(args.length === 1 ? args[0] : args)}` : msg;
1726
+ error(message);
1727
+ };
1269
1728
  var Model = class {
1270
- constructor(modelName) {
1729
+ constructor(modelName, runtime) {
1271
1730
  this.modelName = modelName;
1272
- this.model = mongoose3.model(modelName);
1731
+ this.runtime = runtime ?? null;
1732
+ const resolvedRuntime = runtime ?? getActiveRuntime();
1733
+ const registered = resolvedRuntime?.getModelInstance(modelName) ?? null;
1734
+ const global = mongoose2.models[modelName];
1735
+ this.model = registered ?? global ?? mongoose2.model(modelName);
1273
1736
  if (!this.model) return;
1274
- this.model.schema.set("optimisticConcurrency", true);
1275
- const currVersionKey = this.model.schema.get("versionKey");
1276
- if (!currVersionKey) this.model.schema.set("versionKey", "__v");
1277
1737
  }
1278
1738
  new() {
1279
1739
  const doc = new this.model();
@@ -1282,20 +1742,25 @@ var Model = class {
1282
1742
  create(data) {
1283
1743
  return this.model.create(data);
1284
1744
  }
1285
- find({ filter: filter2, select, sort, populate, limit, skip, lean }) {
1745
+ find({ filter: filter2, select, sort, populate, limit, hardLimit, skip, lean }) {
1286
1746
  if (!this.validateSort(sort)) {
1287
1747
  sort = null;
1288
1748
  }
1289
1749
  const builder = this.model.find(filter2);
1290
1750
  if (select) builder.select(select);
1291
1751
  if (skip) builder.skip(Number(skip));
1292
- if (limit) builder.limit(Number(limit));
1752
+ const normalizedLimit = Number(limit);
1753
+ if (Number.isSafeInteger(normalizedLimit) && normalizedLimit > 0) {
1754
+ builder.limit(normalizedLimit);
1755
+ } else if (Number.isSafeInteger(hardLimit) && Number(hardLimit) > 0) {
1756
+ builder.limit(Number(hardLimit));
1757
+ }
1293
1758
  if (sort) builder.sort(sort);
1294
1759
  if (populate) builder.populate(populate);
1295
1760
  if (lean) builder.lean();
1296
1761
  return builder;
1297
1762
  }
1298
- validateSort(sort, logError = logger.error) {
1763
+ validateSort(sort, logError = safeSortLogError) {
1299
1764
  const validSortOrders = [1, -1, "asc", "ascending", "desc", "descending"];
1300
1765
  const fieldPathPattern = /^[A-Za-z_$][\w$]*(\.[A-Za-z_$][\w$]*)*$/;
1301
1766
  const isValidFieldPath = (field) => fieldPathPattern.test(field);
@@ -1372,7 +1837,7 @@ var Model = class {
1372
1837
  }
1373
1838
  exists(filter2) {
1374
1839
  if (!filter2) return null;
1375
- return this.findOne(filter2).select("_id").lean();
1840
+ return this.findOne({ filter: filter2 }).select("_id").lean();
1376
1841
  }
1377
1842
  // see https://mongoosejs.com/docs/api.html#query_Query-countDocuments
1378
1843
  countDocuments(filter2 = {}) {
@@ -1389,14 +1854,16 @@ var model_default = Model;
1389
1854
  import { forEach as forEach4, get as get5, pick as pick3 } from "@web-ts-toolkit/utils";
1390
1855
  async function listSub(service, id, sub, options) {
1391
1856
  const { filter: ft, select } = options ?? {};
1857
+ const filterErrors = validateClientFilter(ft);
1858
+ if (filterErrors.length > 0) return { success: false, kind: "error", code: "bad_request" /* BadRequest */, errors: filterErrors };
1392
1859
  const parentDoc = await getParentDoc(service, id, sub, null, { access: "read" });
1393
- if (!parentDoc) return { success: false, code: "not_found" /* NotFound */ };
1860
+ if (!parentDoc) return { success: false, kind: "error", code: "not_found" /* NotFound */ };
1394
1861
  let result = get5(parentDoc, sub);
1395
1862
  const [subFilter, subSelect] = await Promise.all([
1396
1863
  service.genFilter(`subs.${sub}.list`, ft),
1397
1864
  service.genQuerySelect("list", select, false, [sub, "sub"])
1398
1865
  ]);
1399
- if (subFilter === false) return { success: false, code: "forbidden" /* Forbidden */ };
1866
+ if (subFilter === false) return { success: false, kind: "error", code: "forbidden" /* Forbidden */ };
1400
1867
  result = filterCollection(result, subFilter);
1401
1868
  if (subSelect) result = result.map((v) => pick3(toObject(v), subSelect.concat("_id")));
1402
1869
  return { success: true, kind: "list", code: "success" /* Success */, data: result, count: result.length };
@@ -1404,30 +1871,30 @@ async function listSub(service, id, sub, options) {
1404
1871
  async function readSub(service, id, sub, subId, options) {
1405
1872
  const { select, populate } = options ?? {};
1406
1873
  const parentDoc = await getParentDoc(service, id, sub, { populate }, { access: "read" });
1407
- if (!parentDoc) return { success: false, code: "not_found" /* NotFound */ };
1874
+ if (!parentDoc) return { success: false, kind: "error", code: "not_found" /* NotFound */ };
1408
1875
  const result = get5(parentDoc, sub);
1409
1876
  const [subFilter, subSelect] = await Promise.all([
1410
1877
  service.genFilter(`subs.${sub}.read`, { _id: subId }),
1411
1878
  service.genQuerySelect("read", select, false, [sub, "sub"])
1412
1879
  ]);
1413
- if (subFilter === false) return { success: false, code: "forbidden" /* Forbidden */ };
1880
+ if (subFilter === false) return { success: false, kind: "error", code: "forbidden" /* Forbidden */ };
1414
1881
  let subdoc = findElement(result, subFilter);
1415
- if (!subdoc) return { success: false, code: "not_found" /* NotFound */ };
1882
+ if (!subdoc) return { success: false, kind: "error", code: "not_found" /* NotFound */ };
1416
1883
  if (subSelect) subdoc = pick3(toObject(subdoc), subSelect.concat(["_id"]));
1417
1884
  return { success: true, kind: "single", code: "success" /* Success */, data: subdoc };
1418
1885
  }
1419
1886
  async function updateSub(service, id, sub, subId, data) {
1420
1887
  const parentDoc = await getParentDoc(service, id, sub, null, { access: "update" });
1421
- if (!parentDoc) return { success: false, code: "not_found" /* NotFound */ };
1888
+ if (!parentDoc) return { success: false, kind: "error", code: "not_found" /* NotFound */ };
1422
1889
  const result = get5(parentDoc, sub);
1423
1890
  const [subFilter, subReadSelect, subUpdateSelect] = await Promise.all([
1424
1891
  service.genFilter(`subs.${sub}.update`, { _id: subId }),
1425
1892
  service.genQuerySelect("read", null, false, [sub, "sub"]),
1426
1893
  service.genQuerySelect("update", null, false, [sub, "sub"])
1427
1894
  ]);
1428
- if (subFilter === false) return { success: false, code: "forbidden" /* Forbidden */ };
1895
+ if (subFilter === false) return { success: false, kind: "error", code: "forbidden" /* Forbidden */ };
1429
1896
  let subdoc = findElement(result, subFilter);
1430
- if (!subdoc) return { success: false, code: "not_found" /* NotFound */ };
1897
+ if (!subdoc) return { success: false, kind: "error", code: "not_found" /* NotFound */ };
1431
1898
  const allowedData = pick3(data, subUpdateSelect);
1432
1899
  Object.assign(subdoc, allowedData);
1433
1900
  await parentDoc.save();
@@ -1435,15 +1902,24 @@ async function updateSub(service, id, sub, subId, data) {
1435
1902
  return { success: true, kind: "single", code: "success" /* Success */, data: subdoc };
1436
1903
  }
1437
1904
  async function bulkUpdateSub(service, id, sub, data) {
1905
+ const { maxBulkItems } = service.getRequestComplexity();
1906
+ if (data.length > maxBulkItems) {
1907
+ return {
1908
+ success: false,
1909
+ kind: "error",
1910
+ code: "bad_request" /* BadRequest */,
1911
+ errors: [{ detail: `Bulk subdocument update exceeds maximum item count of ${maxBulkItems}` }]
1912
+ };
1913
+ }
1438
1914
  const parentDoc = await getParentDoc(service, id, sub, null, { access: "update" });
1439
- if (!parentDoc) return { success: false, code: "not_found" /* NotFound */ };
1915
+ if (!parentDoc) return { success: false, kind: "error", code: "not_found" /* NotFound */ };
1440
1916
  let result = get5(parentDoc, sub);
1441
1917
  const [subFilter, subReadSelect, subUpdateSelect] = await Promise.all([
1442
1918
  service.genFilter(`subs.${sub}.update`, { _id: { $in: data.map((v) => v._id) } }),
1443
1919
  service.genQuerySelect("read", null, false, [sub, "sub"]),
1444
1920
  service.genQuerySelect("update", null, false, [sub, "sub"])
1445
1921
  ]);
1446
- if (subFilter === false) return { success: false, code: "forbidden" /* Forbidden */ };
1922
+ if (subFilter === false) return { success: false, kind: "error", code: "forbidden" /* Forbidden */ };
1447
1923
  result = filterCollection(result, subFilter);
1448
1924
  forEach4(result, (subdoc) => {
1449
1925
  const tdata = findElementById(data, subdoc._id);
@@ -1457,27 +1933,40 @@ async function bulkUpdateSub(service, id, sub, data) {
1457
1933
  }
1458
1934
  async function createSub(service, id, sub, data, options) {
1459
1935
  const { addFirst } = options ?? {};
1936
+ const { maxBulkItems } = service.getRequestComplexity();
1937
+ if (Array.isArray(data) && data.length > maxBulkItems) {
1938
+ return {
1939
+ success: false,
1940
+ kind: "error",
1941
+ code: "bad_request" /* BadRequest */,
1942
+ errors: [{ detail: `Bulk subdocument create exceeds maximum item count of ${maxBulkItems}` }]
1943
+ };
1944
+ }
1460
1945
  const parentDoc = await getParentDoc(service, id, sub, null, { access: "update" });
1461
- if (!parentDoc) return { success: false, code: "not_found" /* NotFound */ };
1946
+ if (!parentDoc) return { success: false, kind: "error", code: "not_found" /* NotFound */ };
1462
1947
  let result = get5(parentDoc, sub);
1463
1948
  const [subCreateSelect, subReadSelect] = await Promise.all([
1464
1949
  service.genQuerySelect("create", null, false, [sub, "sub"]),
1465
1950
  service.genQuerySelect("read", null, false, [sub, "sub"])
1466
1951
  ]);
1467
- const allowedData = pick3(data, subCreateSelect);
1468
- addFirst === true ? result.unshift(allowedData) : result.push(allowedData);
1952
+ const allowedData = Array.isArray(data) ? data.map((row) => pick3(row, subCreateSelect)) : pick3(data, subCreateSelect);
1953
+ if (Array.isArray(allowedData)) {
1954
+ addFirst === true ? result.unshift(...allowedData) : result.push(...allowedData);
1955
+ } else {
1956
+ addFirst === true ? result.unshift(allowedData) : result.push(allowedData);
1957
+ }
1469
1958
  await parentDoc.save();
1470
1959
  if (subReadSelect) result = result.map((v) => pick3(toObject(v), subReadSelect.concat(["_id"])));
1471
1960
  return { success: true, kind: "list", code: "created" /* Created */, data: result, count: result.length };
1472
1961
  }
1473
1962
  async function deleteSub(service, id, sub, subId) {
1474
1963
  const parentDoc = await getParentDoc(service, id, sub, null, { access: "update" });
1475
- if (!parentDoc) return { success: false, code: "not_found" /* NotFound */ };
1964
+ if (!parentDoc) return { success: false, kind: "error", code: "not_found" /* NotFound */ };
1476
1965
  const result = get5(parentDoc, sub);
1477
1966
  const subFilter = await service.genFilter(`subs.${sub}.delete`, { _id: subId });
1478
- if (subFilter === false) return { success: false, code: "forbidden" /* Forbidden */ };
1967
+ if (subFilter === false) return { success: false, kind: "error", code: "forbidden" /* Forbidden */ };
1479
1968
  const subdoc = findElement(result, subFilter);
1480
- if (!subdoc) return { success: false, code: "not_found" /* NotFound */ };
1969
+ if (!subdoc) return { success: false, kind: "error", code: "not_found" /* NotFound */ };
1481
1970
  await ("deleteOne" in subdoc ? subdoc.deleteOne?.() : subdoc.remove?.());
1482
1971
  await parentDoc.save();
1483
1972
  return { success: true, kind: "single", code: "success" /* Success */, data: subdoc._id };
@@ -1487,7 +1976,7 @@ async function getParentDoc(service, id, sub, args, options) {
1487
1976
  const { access = "read", lean = false } = options ?? {};
1488
1977
  const parentFilter = await service.genFilter(access, await service.genIDFilter(id));
1489
1978
  if (parentFilter === false) return null;
1490
- return service.model.findOne({ filter: parentFilter, select: sub, populate: genSubPopulate(sub, populate), lean });
1979
+ return service.findRawParentDoc({ filter: parentFilter, select: sub, populate: genSubPopulate(sub, populate), lean });
1491
1980
  }
1492
1981
 
1493
1982
  // src/services/model-service-defaults.ts
@@ -1613,10 +2102,53 @@ var assertModelDocument = (value, modelName, hookName) => {
1613
2102
  }
1614
2103
  throw new Error(`${hookName} hook for model=${modelName} must return a Mongoose document instance`);
1615
2104
  };
2105
+ var mapWithConcurrencyLimit = async (items, limit, iteratee) => {
2106
+ const results = new Array(items.length);
2107
+ let cursor = 0;
2108
+ const workerCount = Math.min(Math.max(limit, 1), items.length || 1);
2109
+ await Promise.all(
2110
+ Array.from({ length: workerCount }, async () => {
2111
+ while (cursor < items.length) {
2112
+ const current = cursor;
2113
+ cursor += 1;
2114
+ results[current] = await iteratee(items[current], current);
2115
+ }
2116
+ })
2117
+ );
2118
+ return results;
2119
+ };
1616
2120
  var Service = class extends Base {
2121
+ findRawParentDoc(args) {
2122
+ return this.model.findOne({
2123
+ ...args,
2124
+ filter: args.filter,
2125
+ populate: args.populate
2126
+ });
2127
+ }
1617
2128
  asServiceHookContext(context) {
1618
2129
  return context;
1619
2130
  }
2131
+ beginOp(op, filter2, extra) {
2132
+ const startedAt = Date.now();
2133
+ debug({
2134
+ op,
2135
+ modelName: this.modelName,
2136
+ filterKeyValueCount: summarizeFilter(filter2).filterKeyValueCount,
2137
+ startedAt,
2138
+ ...extra ?? {}
2139
+ });
2140
+ return startedAt;
2141
+ }
2142
+ completeOp(op, startedAt, resultCode, filter2, extra) {
2143
+ debug({
2144
+ op,
2145
+ modelName: this.modelName,
2146
+ filterKeyValueCount: summarizeFilter(filter2).filterKeyValueCount,
2147
+ durationMs: Date.now() - startedAt,
2148
+ resultCode,
2149
+ ...extra ?? {}
2150
+ });
2151
+ }
1620
2152
  constructor(req, modelName) {
1621
2153
  super(req, modelName);
1622
2154
  this.model = new model_default(modelName);
@@ -1627,12 +2159,20 @@ var Service = class extends Base {
1627
2159
  }
1628
2160
  async findOne(filter2, args, options) {
1629
2161
  const filterErrors = this.validateClientFilter(filter2);
1630
- if (filterErrors.length > 0) return { success: false, code: "bad_request" /* BadRequest */, errors: filterErrors };
2162
+ if (filterErrors.length > 0) return { success: false, kind: "error", code: "bad_request" /* BadRequest */, errors: filterErrors };
1631
2163
  const { select, sort, populate, include, overrides } = this.resolveFindOneArgs(args);
1632
2164
  const { skim, includePermissions, access, populateAccess, lean } = this.resolveFindOneOptions(options);
1633
2165
  const { filter: overrideFilter, select: overrideSelect, populate: overridePopulate } = overrides ?? {};
2166
+ let parsedFilter;
2167
+ try {
2168
+ parsedFilter = await this.parseClientData(filter2);
2169
+ } catch (error2) {
2170
+ const result = this.getClientRequestErrorResult(error2);
2171
+ if (result) return result;
2172
+ throw error2;
2173
+ }
1634
2174
  let [_filter, _select, _populate] = await Promise.all([
1635
- overrideFilter || this.genFilter(access, await this.parseClientData(filter2)),
2175
+ overrideFilter || this.genFilter(access, parsedFilter),
1636
2176
  overrideSelect || this.genQuerySelect(access, select),
1637
2177
  overridePopulate || this.genPopulate(populateAccess || access, populate)
1638
2178
  ]);
@@ -1644,10 +2184,20 @@ var Service = class extends Base {
1644
2184
  sort,
1645
2185
  populate: _populate
1646
2186
  };
1647
- logger.debug(JSON.stringify({ op: "findOne", query }));
1648
- if (_filter === false) return { success: false, code: "forbidden" /* Forbidden */, query };
2187
+ const startedAt = this.beginOp("findOne", _filter, {
2188
+ sort,
2189
+ selectCount: finalSelect.length,
2190
+ populateCount: Array.isArray(_populate) ? _populate.length : _populate ? 1 : 0
2191
+ });
2192
+ if (_filter === false) {
2193
+ this.completeOp("findOne", startedAt, "forbidden" /* Forbidden */, _filter);
2194
+ return { success: false, kind: "error", code: "forbidden" /* Forbidden */, query };
2195
+ }
1649
2196
  let doc = await this.model.findOne({ ...query, lean });
1650
- if (!doc) return { success: false, code: "not_found" /* NotFound */, query };
2197
+ if (!doc) {
2198
+ this.completeOp("findOne", startedAt, "not_found" /* NotFound */, _filter);
2199
+ return { success: false, kind: "error", code: "not_found" /* NotFound */, query };
2200
+ }
1651
2201
  const context = {
1652
2202
  mongooseModel: this.model.model,
1653
2203
  modelName: this.modelName,
@@ -1655,7 +2205,16 @@ var Service = class extends Base {
1655
2205
  originalDocumentSnapshot: toObject(doc),
1656
2206
  resolvedQuery: query
1657
2207
  };
1658
- doc = await this.includeDocs(doc, includes);
2208
+ try {
2209
+ doc = await this.includeDocs(doc, includes);
2210
+ } catch (error2) {
2211
+ const result = this.getClientRequestErrorResult(error2);
2212
+ if (result) {
2213
+ this.completeOp("findOne", startedAt, result.code, _filter);
2214
+ return { ...result, query };
2215
+ }
2216
+ throw error2;
2217
+ }
1659
2218
  let includeDocPermissions = includePermissions;
1660
2219
  if (!includeDocPermissions && !skim) {
1661
2220
  includeDocPermissions = this.checkIfModelPermissionExists([access, "read", "update"]);
@@ -1668,6 +2227,7 @@ var Service = class extends Base {
1668
2227
  this.baseFieldsExt.concat(includePaths, normalizeSelect(overrideSelect))
1669
2228
  );
1670
2229
  if (!includePermissions) doc = this.addEmptyPermissions(doc);
2230
+ this.completeOp("findOne", startedAt, "success" /* Success */, _filter);
1671
2231
  return { success: true, kind: "single", code: "success" /* Success */, data: doc, query, context };
1672
2232
  }
1673
2233
  async findById(id, args, options) {
@@ -1691,18 +2251,26 @@ var Service = class extends Base {
1691
2251
  }
1692
2252
  async find(filter2, args, options, decorate) {
1693
2253
  const filterErrors = this.validateClientFilter(filter2);
1694
- if (filterErrors.length > 0) return { success: false, code: "bad_request" /* BadRequest */, errors: filterErrors };
2254
+ if (filterErrors.length > 0) return { success: false, kind: "error", code: "bad_request" /* BadRequest */, errors: filterErrors };
1695
2255
  const { select, populate, include, sort, skip, limit, page, pageSize, overrides } = this.resolveFindArgs(args);
1696
2256
  const { skim, includePermissions, includeCount, populateAccess, lean } = this.resolveFindOptions(options);
1697
2257
  const { filter: overrideFilter, select: overrideSelect, populate: overridePopulate } = overrides ?? {};
2258
+ let parsedFilter;
2259
+ try {
2260
+ parsedFilter = await this.parseClientData(filter2);
2261
+ } catch (error2) {
2262
+ const result = this.getClientRequestErrorResult(error2);
2263
+ if (result) return result;
2264
+ throw error2;
2265
+ }
1698
2266
  const [_filter, _select, _populate, pagination] = await Promise.all([
1699
- overrideFilter || this.genFilter("list", await this.parseClientData(filter2)),
2267
+ overrideFilter || this.genFilter("list", parsedFilter),
1700
2268
  overrideSelect || this.genQuerySelect("list", select),
1701
2269
  overridePopulate || this.genPopulate(populateAccess, populate),
1702
2270
  genPagination({ skip, limit, page, pageSize }, this.options.listHardLimit)
1703
2271
  ]);
1704
2272
  const finalSelect = normalizeSelect(_select);
1705
- const filteredPopulate = isArray6(finalSelect) && isArray6(_populate) ? _populate.filter((p) => finalSelect.includes(p.path.split(".")[0])) : _populate;
2273
+ const filteredPopulate = isArray7(finalSelect) && isArray7(_populate) ? _populate.filter((p) => finalSelect.includes(p.path.split(".")[0])) : _populate;
1706
2274
  const { includes, includeLocalFields, includePaths } = this.processInclude(include);
1707
2275
  const query = {
1708
2276
  filter: _filter,
@@ -1711,10 +2279,20 @@ var Service = class extends Base {
1711
2279
  sort,
1712
2280
  ...pagination
1713
2281
  };
1714
- logger.debug(JSON.stringify({ op: "find", query }));
1715
- if (_filter === false) return { success: false, code: "forbidden" /* Forbidden */, query };
2282
+ const startedAt = this.beginOp("find", _filter, {
2283
+ sort,
2284
+ skip: pagination.skip,
2285
+ limit: pagination.limit,
2286
+ selectCount: finalSelect.concat(includeLocalFields).length,
2287
+ populateCount: Array.isArray(filteredPopulate) ? filteredPopulate.length : filteredPopulate ? 1 : 0
2288
+ });
2289
+ if (_filter === false) {
2290
+ this.completeOp("find", startedAt, "forbidden" /* Forbidden */, _filter);
2291
+ return { success: false, kind: "error", code: "forbidden" /* Forbidden */, query };
2292
+ }
1716
2293
  let docs = await this.model.find({
1717
2294
  ...query,
2295
+ hardLimit: this.options.listHardLimit,
1718
2296
  lean
1719
2297
  });
1720
2298
  const contexts = docs.map((doc) => ({
@@ -1725,7 +2303,16 @@ var Service = class extends Base {
1725
2303
  resolvedQuery: query
1726
2304
  }));
1727
2305
  const _decorate = isFunction3(decorate) ? decorate : (v) => v;
1728
- docs = await this.includeDocs(docs, includes);
2306
+ try {
2307
+ docs = await this.includeDocs(docs, includes);
2308
+ } catch (error2) {
2309
+ const result = this.getClientRequestErrorResult(error2);
2310
+ if (result) {
2311
+ this.completeOp("find", startedAt, result.code, _filter);
2312
+ return { ...result, query };
2313
+ }
2314
+ throw error2;
2315
+ }
1729
2316
  const fieldPermissionAccess = includePermissions ? await this.getFieldPermissionAccess(docs.map((doc) => doc._id)) : void 0;
1730
2317
  docs = await Promise.all(
1731
2318
  docs.map(async (doc, i) => {
@@ -1746,6 +2333,7 @@ var Service = class extends Base {
1746
2333
  return doc;
1747
2334
  })
1748
2335
  );
2336
+ this.completeOp("find", startedAt, "success" /* Success */, _filter);
1749
2337
  return {
1750
2338
  success: true,
1751
2339
  kind: "list",
@@ -1762,67 +2350,96 @@ var Service = class extends Base {
1762
2350
  const { skim, includePermissions, populateAccess } = this.resolveCreateOptions(options);
1763
2351
  const isArr = Array.isArray(data);
1764
2352
  let dataArr = isArr ? data : [data];
1765
- dataArr = await Promise.all(dataArr.map((d) => this.parseClientData(d)));
2353
+ const { maxBulkItems, maxBulkConcurrency } = this.getRequestComplexity();
2354
+ if (dataArr.length > maxBulkItems) {
2355
+ return {
2356
+ success: false,
2357
+ kind: "error",
2358
+ code: "bad_request" /* BadRequest */,
2359
+ errors: [{ detail: `Bulk create exceeds maximum item count of ${maxBulkItems}` }]
2360
+ };
2361
+ }
2362
+ try {
2363
+ dataArr = await Promise.all(dataArr.map((d) => this.parseClientData(d)));
2364
+ } catch (error2) {
2365
+ const result = this.getClientRequestErrorResult(error2);
2366
+ if (result) return result;
2367
+ throw error2;
2368
+ }
1766
2369
  const resolvedPopulate = populate ? await this.genPopulate(populateAccess, populate) : [];
1767
2370
  const contexts = [];
1768
- let validationError = null;
1769
- const items = await Promise.all(
1770
- dataArr.map(async (item, index) => {
1771
- const context = {
1772
- mongooseModel: this.model.model,
1773
- modelName: this.modelName,
1774
- operation: "create",
1775
- originalData: item,
1776
- resolvedQuery: resolvedPopulate.length > 0 ? { populate: resolvedPopulate } : {}
1777
- };
1778
- const allowedFields = await this.genAllowedFields(item, "create");
1779
- const allowedData = pick4(item, allowedFields);
1780
- context.allowedFields = allowedFields;
1781
- context.allowedData = allowedData;
1782
- const validated = await this.validate(allowedData, "create", context);
1783
- if (isBoolean3(validated)) {
1784
- if (!validated) {
1785
- validationError = { success: false, code: "bad_request" /* BadRequest */ };
1786
- return;
1787
- }
1788
- } else if (isArray6(validated)) {
1789
- if (validated.length > 0) {
1790
- validationError = { success: false, code: "bad_request" /* BadRequest */, errors: validated };
1791
- return;
1792
- }
2371
+ const validationErrors = [];
2372
+ const validationItems = await mapWithConcurrencyLimit(dataArr, maxBulkConcurrency, async (item, index) => {
2373
+ const context = {
2374
+ mongooseModel: this.model.model,
2375
+ modelName: this.modelName,
2376
+ operation: "create",
2377
+ originalData: item,
2378
+ resolvedQuery: resolvedPopulate.length > 0 ? { populate: resolvedPopulate } : {}
2379
+ };
2380
+ const allowedFields = await this.genAllowedFields(item, "create");
2381
+ const allowedData = pick4(item, allowedFields);
2382
+ context.allowedFields = allowedFields;
2383
+ context.allowedData = allowedData;
2384
+ const validated = await this.validate(allowedData, "create", context);
2385
+ if (isBoolean3(validated)) {
2386
+ if (!validated) {
2387
+ validationErrors.push({ index, errors: [] });
2388
+ return void 0;
1793
2389
  }
1794
- const preparedData = await this.prepare(allowedData, "create", context);
1795
- context.preparedData = preparedData;
1796
- contexts[index] = context;
1797
- return preparedData;
1798
- })
1799
- );
1800
- if (validationError) return validationError;
2390
+ } else if (isArray7(validated)) {
2391
+ if (validated.length > 0) {
2392
+ validationErrors.push({
2393
+ index,
2394
+ errors: isArr ? validated.map((issue) => this.formatBulkValidationIssue(issue, index)) : validated
2395
+ });
2396
+ return void 0;
2397
+ }
2398
+ }
2399
+ contexts[index] = context;
2400
+ return allowedData;
2401
+ });
2402
+ if (validationErrors.length > 0) {
2403
+ const aggregate = validationErrors.slice().sort((a, b) => a.index - b.index).flatMap((entry) => entry.errors);
2404
+ if (isArr) {
2405
+ return { success: false, kind: "error", code: "bad_request" /* BadRequest */, errors: aggregate };
2406
+ }
2407
+ const single = validationErrors[0];
2408
+ return {
2409
+ success: false,
2410
+ kind: "error",
2411
+ code: "bad_request" /* BadRequest */,
2412
+ errors: single?.errors ?? []
2413
+ };
2414
+ }
2415
+ const items = await mapWithConcurrencyLimit(validationItems, maxBulkConcurrency, async (allowedData, index) => {
2416
+ const preparedData = await this.prepare(allowedData, "create", contexts[index]);
2417
+ contexts[index].preparedData = preparedData;
2418
+ return preparedData;
2419
+ });
1801
2420
  const _decorate = isFunction3(decorate) ? decorate : (v) => v;
1802
2421
  const createdDocs = await this.model.create(items);
1803
- const docs = await Promise.all(
1804
- createdDocs.map(async (doc, index) => {
1805
- contexts[index].currentDocument = doc;
1806
- doc = assertModelDocument(
1807
- await this.afterPersist(doc, "create", contexts[index]),
1808
- this.modelName,
1809
- "afterPersist"
1810
- );
1811
- contexts[index].currentDocument = doc;
1812
- contexts[index].finalDocumentSnapshot = doc.toObject({ virtuals: false });
1813
- let includeDocPermissions = includePermissions;
1814
- if (!includeDocPermissions && !skim) {
1815
- includeDocPermissions = this.checkIfModelPermissionExists(["create", "read", "update"]);
1816
- }
1817
- if (includeDocPermissions) doc = await this.addDocPermissions(doc, "create", contexts[index]);
1818
- if (includePermissions) doc = await this.addFieldPermissions(doc, "read", contexts[index]);
1819
- if (resolvedPopulate.length > 0) await populateDoc(doc, resolvedPopulate);
1820
- doc = await this.trimOutputFields(doc, "read", this.baseFieldsExt);
1821
- let outputDoc = await _decorate(doc, contexts[index]);
1822
- if (!includePermissions) outputDoc = this.addEmptyPermissions(outputDoc);
1823
- return outputDoc;
1824
- })
1825
- );
2422
+ const docs = await mapWithConcurrencyLimit(createdDocs, maxBulkConcurrency, async (doc, index) => {
2423
+ contexts[index].currentDocument = doc;
2424
+ doc = assertModelDocument(
2425
+ await this.afterPersist(doc, "create", contexts[index]),
2426
+ this.modelName,
2427
+ "afterPersist"
2428
+ );
2429
+ contexts[index].currentDocument = doc;
2430
+ contexts[index].finalDocumentSnapshot = doc.toObject({ virtuals: false });
2431
+ let includeDocPermissions = includePermissions;
2432
+ if (!includeDocPermissions && !skim) {
2433
+ includeDocPermissions = this.checkIfModelPermissionExists(["create", "read", "update"]);
2434
+ }
2435
+ if (includeDocPermissions) doc = await this.addDocPermissions(doc, "create", contexts[index]);
2436
+ if (includePermissions) doc = await this.addFieldPermissions(doc, "read", contexts[index]);
2437
+ if (resolvedPopulate.length > 0) await populateDoc(doc, resolvedPopulate);
2438
+ doc = await this.trimOutputFields(doc, "read", this.baseFieldsExt);
2439
+ let outputDoc = await _decorate(doc, contexts[index]);
2440
+ if (!includePermissions) outputDoc = this.addEmptyPermissions(outputDoc);
2441
+ return outputDoc;
2442
+ });
1826
2443
  return {
1827
2444
  success: true,
1828
2445
  kind: "list",
@@ -1832,18 +2449,50 @@ var Service = class extends Base {
1832
2449
  count: docs.length
1833
2450
  };
1834
2451
  }
1835
- async new() {
2452
+ formatBulkValidationIssue(issue, index) {
2453
+ if (!issue || typeof issue !== "object") {
2454
+ return {
2455
+ detail: typeof issue === "string" && issue.length > 0 ? issue : "Bad Request",
2456
+ ...index === null ? {} : { pointer: `#/${index}` }
2457
+ };
2458
+ }
2459
+ const typedIssue = issue;
2460
+ const detail = typedIssue.detail ?? typedIssue.message ?? "Bad Request";
2461
+ if (index === null) {
2462
+ return typedIssue.pointer || typedIssue.path ? {
2463
+ detail,
2464
+ ...typedIssue.pointer ? { pointer: typedIssue.pointer } : {}
2465
+ } : { detail };
2466
+ }
2467
+ if (typedIssue.pointer?.startsWith("#/")) {
2468
+ return { ...typedIssue, detail, pointer: `#/${index}${typedIssue.pointer.slice(1)}` };
2469
+ }
2470
+ if (typedIssue.path) {
2471
+ return { ...typedIssue, detail, pointer: `#/${[index, ...typedIssue.path].join("/")}` };
2472
+ }
2473
+ return { ...typedIssue, detail, pointer: `#/${index}` };
2474
+ }
2475
+ async new(args, options) {
2476
+ const { skim, includePermissions } = options ?? {};
1836
2477
  const data = await this.model.new();
2478
+ let doc = data;
2479
+ doc = await this.trimOutputFields(doc, "create", this.baseFieldsExt);
2480
+ let includeDocPermissions = includePermissions;
2481
+ if (!includeDocPermissions && !skim) {
2482
+ includeDocPermissions = this.checkIfModelPermissionExists(["create", "read", "update"]);
2483
+ }
2484
+ if (includeDocPermissions) doc = await this.addDocPermissions(doc, "create", {});
2485
+ if (!includePermissions) doc = this.addEmptyPermissions(doc);
1837
2486
  return {
1838
2487
  success: true,
1839
2488
  kind: "single",
1840
2489
  code: "success" /* Success */,
1841
- data
2490
+ data: doc
1842
2491
  };
1843
2492
  }
1844
2493
  async updateOne(filter2, data, args, options, decorate) {
1845
2494
  const filterErrors = this.validateClientFilter(filter2);
1846
- if (filterErrors.length > 0) return { success: false, code: "bad_request" /* BadRequest */, errors: filterErrors };
2495
+ if (filterErrors.length > 0) return { success: false, kind: "error", code: "bad_request" /* BadRequest */, errors: filterErrors };
1847
2496
  const { populate, overrides } = this.resolveUpdateOneArgs(args);
1848
2497
  const { skim, includePermissions, populateAccess } = this.resolveUpdateOneOptions(options);
1849
2498
  const { filter: overrideFilter, populate: overridePopulate } = overrides ?? {};
@@ -1852,17 +2501,34 @@ var Service = class extends Base {
1852
2501
  overridePopulate || this.genPopulate(populateAccess, populate)
1853
2502
  ]);
1854
2503
  const query = { filter: _filter, populate: _populate };
1855
- logger.debug(JSON.stringify({ op: "updateOne", query }));
1856
- if (_filter === false) return { success: false, code: "forbidden" /* Forbidden */, query };
2504
+ const startedAt = this.beginOp("updateOne", _filter, {
2505
+ populateCount: Array.isArray(_populate) ? _populate.length : _populate ? 1 : 0
2506
+ });
2507
+ if (_filter === false) {
2508
+ this.completeOp("updateOne", startedAt, "forbidden" /* Forbidden */, _filter);
2509
+ return { success: false, kind: "error", code: "forbidden" /* Forbidden */, query };
2510
+ }
1857
2511
  let doc = await this.model.findOne({ filter: _filter });
1858
- if (!doc) return { success: false, code: "not_found" /* NotFound */, query };
2512
+ if (!doc) {
2513
+ this.completeOp("updateOne", startedAt, "not_found" /* NotFound */, _filter);
2514
+ return { success: false, kind: "error", code: "not_found" /* NotFound */, query };
2515
+ }
1859
2516
  const context = {
1860
2517
  mongooseModel: this.model.model,
1861
2518
  modelName: this.modelName,
1862
2519
  operation: "update",
1863
2520
  resolvedQuery: query
1864
2521
  };
1865
- data = await this.parseClientData(data);
2522
+ try {
2523
+ data = await this.parseClientData(data);
2524
+ } catch (error2) {
2525
+ const result = this.getClientRequestErrorResult(error2);
2526
+ if (result) {
2527
+ this.completeOp("updateOne", startedAt, result.code, _filter);
2528
+ return result;
2529
+ }
2530
+ throw error2;
2531
+ }
1866
2532
  context.originalDocumentSnapshot = doc.toObject({ virtuals: false });
1867
2533
  context.originalData = data;
1868
2534
  doc = await this.addDocPermissions(doc, "update", context);
@@ -1874,9 +2540,15 @@ var Service = class extends Base {
1874
2540
  context.allowedData = allowedData;
1875
2541
  const validated = await this.validate(allowedData, "update", context);
1876
2542
  if (isBoolean3(validated)) {
1877
- if (!validated) return { success: false, code: "bad_request" /* BadRequest */ };
1878
- } else if (isArray6(validated)) {
1879
- if (validated.length > 0) return { success: false, code: "bad_request" /* BadRequest */, errors: validated };
2543
+ if (!validated) {
2544
+ this.completeOp("updateOne", startedAt, "bad_request" /* BadRequest */, _filter);
2545
+ return { success: false, kind: "error", code: "bad_request" /* BadRequest */ };
2546
+ }
2547
+ } else if (isArray7(validated)) {
2548
+ if (validated.length > 0) {
2549
+ this.completeOp("updateOne", startedAt, "bad_request" /* BadRequest */, _filter);
2550
+ return { success: false, kind: "error", code: "bad_request" /* BadRequest */, errors: validated };
2551
+ }
1880
2552
  }
1881
2553
  const prepared = await this.prepare(allowedData, "update", context);
1882
2554
  context.preparedData = prepared;
@@ -1909,6 +2581,7 @@ var Service = class extends Base {
1909
2581
  let outputDoc = doc;
1910
2582
  if (isFunction3(decorate)) outputDoc = await decorate(outputDoc, context);
1911
2583
  if (!includePermissions) outputDoc = this.addEmptyPermissions(outputDoc);
2584
+ this.completeOp("updateOne", startedAt, "success" /* Success */, _filter);
1912
2585
  return { success: true, kind: "single", code: "success" /* Success */, data: outputDoc, input: prepared };
1913
2586
  }
1914
2587
  async updateById(id, data, args = {}, options = {}, decorate) {
@@ -1931,17 +2604,21 @@ var Service = class extends Base {
1931
2604
  }
1932
2605
  async upsert(filter2, data, args, options, decorate) {
1933
2606
  const filterErrors = this.validateClientFilter(filter2);
1934
- if (filterErrors.length > 0) return { success: false, code: "bad_request" /* BadRequest */, errors: filterErrors };
2607
+ if (filterErrors.length > 0) return { success: false, kind: "error", code: "bad_request" /* BadRequest */, errors: filterErrors };
1935
2608
  const { populate, overrides } = this.resolveUpsertArgs(args);
1936
2609
  const { skim, includePermissions, populateAccess } = this.resolveUpsertOptions(options);
1937
2610
  const { filter: overrideFilter, populate: overridePopulate } = overrides ?? {};
1938
2611
  const _filter = await (overrideFilter || this.genFilter("update", filter2));
1939
2612
  const query = { filter: _filter };
1940
- logger.debug(JSON.stringify({ op: "upsert", query }));
1941
- if (_filter === false) return { success: false, code: "forbidden" /* Forbidden */, query };
2613
+ const startedAt = this.beginOp("upsert", _filter);
2614
+ if (_filter === false) {
2615
+ this.completeOp("upsert", startedAt, "forbidden" /* Forbidden */, _filter);
2616
+ return { success: false, kind: "error", code: "forbidden" /* Forbidden */, query };
2617
+ }
1942
2618
  const theone = await this.model.findOne({ filter: _filter });
2619
+ let result;
1943
2620
  if (theone) {
1944
- return this.updateOne(
2621
+ result = await this.updateOne(
1945
2622
  null,
1946
2623
  data,
1947
2624
  {
@@ -1955,7 +2632,7 @@ var Service = class extends Base {
1955
2632
  decorate
1956
2633
  );
1957
2634
  } else {
1958
- return this.create(
2635
+ result = await this.create(
1959
2636
  data,
1960
2637
  { populate },
1961
2638
  {
@@ -1966,14 +2643,22 @@ var Service = class extends Base {
1966
2643
  decorate
1967
2644
  );
1968
2645
  }
2646
+ this.completeOp("upsert", startedAt, result.code, _filter);
2647
+ return result;
1969
2648
  }
1970
2649
  async delete(id) {
1971
2650
  const filter2 = await this.genFilter("delete", await this.genIDFilter(id));
1972
2651
  const query = { filter: filter2 };
1973
- logger.debug(JSON.stringify({ op: "delete", query }));
1974
- if (filter2 === false) return { success: false, code: "forbidden" /* Forbidden */, query };
2652
+ const startedAt = this.beginOp("delete", filter2);
2653
+ if (filter2 === false) {
2654
+ this.completeOp("delete", startedAt, "forbidden" /* Forbidden */, filter2);
2655
+ return { success: false, kind: "error", code: "forbidden" /* Forbidden */, query };
2656
+ }
1975
2657
  let doc = await this.model.findOne({ filter: filter2 });
1976
- if (!doc) return { success: false, code: "not_found" /* NotFound */, query };
2658
+ if (!doc) {
2659
+ this.completeOp("delete", startedAt, "not_found" /* NotFound */, filter2);
2660
+ return { success: false, kind: "error", code: "not_found" /* NotFound */, query };
2661
+ }
1977
2662
  const context = {
1978
2663
  mongooseModel: this.model.model,
1979
2664
  modelName: this.modelName,
@@ -1986,11 +2671,12 @@ var Service = class extends Base {
1986
2671
  await ("deleteOne" in doc ? doc.deleteOne() : doc.remove());
1987
2672
  context.finalDocumentSnapshot = toObject(doc);
1988
2673
  await this.afterDelete(doc, context);
2674
+ this.completeOp("delete", startedAt, "success" /* Success */, filter2);
1989
2675
  return { success: true, kind: "single", code: "success" /* Success */, data: doc._id, query };
1990
2676
  }
1991
2677
  async exists(filter2, options) {
1992
2678
  const filterErrors = this.validateClientFilter(filter2);
1993
- if (filterErrors.length > 0) return { success: false, code: "bad_request" /* BadRequest */, errors: filterErrors };
2679
+ if (filterErrors.length > 0) return { success: false, kind: "error", code: "bad_request" /* BadRequest */, errors: filterErrors };
1994
2680
  const { access, includeId } = this.resolveExistsOptions(options);
1995
2681
  filter2 = await this.genFilter(access, filter2);
1996
2682
  const result = await this.model.exists(filter2);
@@ -2002,22 +2688,58 @@ var Service = class extends Base {
2002
2688
  query: { filter: filter2 }
2003
2689
  };
2004
2690
  }
2691
+ isValidDistinctFieldName(field) {
2692
+ if (typeof field !== "string" || field.length === 0) return false;
2693
+ if (field.includes("$")) return false;
2694
+ if (field.includes("..")) return false;
2695
+ if (field.startsWith(".") || field.endsWith(".")) return false;
2696
+ if (/\s/.test(field)) return false;
2697
+ return true;
2698
+ }
2699
+ async authorizeDistinctField(field) {
2700
+ const allowedFields = await this.genAllowedFields(null, "read");
2701
+ const isAllowed = allowedFields.some((allowed) => {
2702
+ if (allowed === field) return true;
2703
+ if (allowed.startsWith(`${field}.`)) return true;
2704
+ if (field.startsWith(`${allowed}.`)) return true;
2705
+ return false;
2706
+ });
2707
+ if (!isAllowed) {
2708
+ return {
2709
+ success: false,
2710
+ kind: "error",
2711
+ code: "forbidden" /* Forbidden */,
2712
+ errors: [{ detail: `Distinct field not allowed: ${field}` }]
2713
+ };
2714
+ }
2715
+ return null;
2716
+ }
2005
2717
  async distinct(field, args) {
2718
+ if (!this.isValidDistinctFieldName(field)) {
2719
+ return {
2720
+ success: false,
2721
+ kind: "error",
2722
+ code: "bad_request" /* BadRequest */,
2723
+ errors: [{ detail: `Invalid distinct field: ${field}` }]
2724
+ };
2725
+ }
2726
+ const fieldError = await this.authorizeDistinctField(field);
2727
+ if (fieldError) return fieldError;
2006
2728
  let { filter: filter2 } = args ?? {};
2007
2729
  const filterErrors = this.validateClientFilter(filter2);
2008
- if (filterErrors.length > 0) return { success: false, code: "bad_request" /* BadRequest */, errors: filterErrors };
2730
+ if (filterErrors.length > 0) return { success: false, kind: "error", code: "bad_request" /* BadRequest */, errors: filterErrors };
2009
2731
  filter2 = await this.genFilter("read", filter2);
2010
2732
  const query = { filter: filter2 };
2011
- if (filter2 === false) return { success: false, code: "forbidden" /* Forbidden */, query };
2733
+ if (filter2 === false) return { success: false, kind: "error", code: "forbidden" /* Forbidden */, query };
2012
2734
  const result = await this.model.distinct(field, filter2);
2013
2735
  return { success: true, kind: "list", code: "success" /* Success */, data: result, count: result.length, query };
2014
2736
  }
2015
2737
  async count(filter2, access = "list") {
2016
2738
  const filterErrors = this.validateClientFilter(filter2);
2017
- if (filterErrors.length > 0) return { success: false, code: "bad_request" /* BadRequest */, errors: filterErrors };
2739
+ if (filterErrors.length > 0) return { success: false, kind: "error", code: "bad_request" /* BadRequest */, errors: filterErrors };
2018
2740
  filter2 = await this.genFilter(access, filter2);
2019
2741
  const query = { filter: filter2 };
2020
- if (filter2 === false) return { success: false, code: "forbidden" /* Forbidden */, query };
2742
+ if (filter2 === false) return { success: false, kind: "error", code: "forbidden" /* Forbidden */, query };
2021
2743
  return { success: true, kind: "single", code: "success" /* Success */, data: await this.model.countDocuments(filter2), query };
2022
2744
  }
2023
2745
  getDocPermissions(doc) {
@@ -2149,11 +2871,11 @@ var PublicService = class extends Service {
2149
2871
  { populate },
2150
2872
  { skim, includePermissions, populateAccess },
2151
2873
  async (doc, context) => {
2152
- doc = toObject(doc);
2153
- doc = await this.decorate(doc, "create", context);
2154
- doc = this.runTasks(doc, tasks);
2155
- if (select) doc = pick5(doc, [...normalizeSelect(select), ...this.baseFieldsExt]);
2156
- return doc;
2874
+ let d = toObject(doc);
2875
+ d = await this.decorate(d, "create", context);
2876
+ d = this.runTasks(d, tasks);
2877
+ if (select) d = pick5(d, [...normalizeSelect(select), ...this.baseFieldsExt]);
2878
+ return d;
2157
2879
  }
2158
2880
  );
2159
2881
  if (!result.success) {
@@ -2161,8 +2883,8 @@ var PublicService = class extends Service {
2161
2883
  }
2162
2884
  return result;
2163
2885
  }
2164
- async _new() {
2165
- return this.new();
2886
+ async _new(args, options) {
2887
+ return this.new(args, options);
2166
2888
  }
2167
2889
  async _read(id, args, options) {
2168
2890
  const { select, populate, include, tasks } = this.resolvePublicReadArgs(args);
@@ -2244,12 +2966,12 @@ var PublicService = class extends Service {
2244
2966
  { populate },
2245
2967
  { skim, includePermissions, populateAccess },
2246
2968
  async (doc, context) => {
2247
- doc = toObject(doc);
2248
- doc = await this.decorate(doc, "update", context);
2249
- doc = this.runTasks(doc, tasks);
2250
- if (select) doc = pick5(doc, [...normalizeSelect(select), ...this.baseFieldsExt]);
2251
- else if (!returningAll) doc = pick5(doc, [...Object.keys(data), "_id"]);
2252
- return doc;
2969
+ let d = toObject(doc);
2970
+ d = await this.decorate(d, "update", context);
2971
+ d = this.runTasks(d, tasks);
2972
+ if (select) d = pick5(d, [...normalizeSelect(select), ...this.baseFieldsExt]);
2973
+ else if (!returningAll) d = pick5(d, [...Object.keys(data), "_id"]);
2974
+ return d;
2253
2975
  }
2254
2976
  );
2255
2977
  if (!result.success) {
@@ -2260,7 +2982,7 @@ var PublicService = class extends Service {
2260
2982
  async _upsert(data, args, options) {
2261
2983
  const idKey = this.getIdentifier();
2262
2984
  if (idKey !== "_id") {
2263
- return { success: false, code: "bad_request" /* BadRequest */, errors: ["not supported custom id field"] };
2985
+ return { success: false, kind: "error", code: "bad_request" /* BadRequest */, errors: ["not supported custom id field"] };
2264
2986
  }
2265
2987
  const { [idKey]: idVal, ...otherData } = data;
2266
2988
  const upsertId = typeof idVal === "string" ? idVal : void 0;
@@ -2270,7 +2992,7 @@ var PublicService = class extends Service {
2270
2992
  return existing;
2271
2993
  }
2272
2994
  if (!existing.data) {
2273
- return { success: false, code: "unauthorized" /* Unauthorized */, errors: ["Unauthorized"] };
2995
+ return { success: false, kind: "error", code: "unauthorized" /* Unauthorized */, errors: ["Unauthorized"] };
2274
2996
  }
2275
2997
  return this._update(upsertId, otherData, args, options);
2276
2998
  }
@@ -2380,7 +3102,7 @@ var DataService = class {
2380
3102
  }
2381
3103
  async findOne(filter2, args, options) {
2382
3104
  const filterErrors = validateClientFilter(filter2);
2383
- if (filterErrors.length > 0) return { success: false, code: "bad_request" /* BadRequest */, errors: filterErrors };
3105
+ if (filterErrors.length > 0) return { success: false, kind: "error", code: "bad_request" /* BadRequest */, errors: filterErrors };
2384
3106
  const { select } = args ?? {};
2385
3107
  const { access = "read" } = options ?? {};
2386
3108
  let [_filter, _select] = await Promise.all([this.genFilter(access, filter2), this.genQuerySelect(access, select)]);
@@ -2388,9 +3110,9 @@ var DataService = class {
2388
3110
  filter: _filter,
2389
3111
  select: _select
2390
3112
  };
2391
- if (_filter === false) return { success: false, code: "forbidden" /* Forbidden */, query };
3113
+ if (_filter === false) return { success: false, kind: "error", code: "forbidden" /* Forbidden */, query };
2392
3114
  let doc = await findElement(this.data, _filter);
2393
- if (!doc) return { success: false, code: "not_found" /* NotFound */, query };
3115
+ if (!doc) return { success: false, kind: "error", code: "not_found" /* NotFound */, query };
2394
3116
  doc = await this.trimOutputFields(doc, access);
2395
3117
  if (_select.length > 0) doc = pick6(doc, _select);
2396
3118
  return { success: true, kind: "single", code: "success" /* Success */, data: doc, query };
@@ -2409,7 +3131,7 @@ var DataService = class {
2409
3131
  }
2410
3132
  async find(filter2, args, options) {
2411
3133
  const filterErrors = validateClientFilter(filter2);
2412
- if (filterErrors.length > 0) return { success: false, code: "bad_request" /* BadRequest */, errors: filterErrors };
3134
+ if (filterErrors.length > 0) return { success: false, kind: "error", code: "bad_request" /* BadRequest */, errors: filterErrors };
2413
3135
  const { select, sort, skip, limit, page, pageSize } = args ?? {};
2414
3136
  const [_filter, _select, pagination] = await Promise.all([
2415
3137
  this.genFilter("list", filter2),
@@ -2422,27 +3144,33 @@ var DataService = class {
2422
3144
  sort,
2423
3145
  ...pagination
2424
3146
  };
2425
- if (_filter === false) return { success: false, code: "forbidden" /* Forbidden */, query };
3147
+ if (_filter === false) return { success: false, kind: "error", code: "forbidden" /* Forbidden */, query };
3148
+ if (sort) {
3149
+ const sortFieldErrors = await this.validateSortFields(sort, "list");
3150
+ if (sortFieldErrors.length > 0) {
3151
+ return { success: false, kind: "error", code: "bad_request" /* BadRequest */, errors: sortFieldErrors, query };
3152
+ }
3153
+ }
2426
3154
  let docs = await filterCollection(this.data, _filter);
2427
3155
  const totalCount = docs.length;
2428
- docs = await Promise.all(
2429
- docs.map(async (doc) => {
3156
+ if (sort) {
3157
+ const { sortKey, sortOrder } = parseSortString(sort);
3158
+ docs = orderBy(docs, [sortKey], [sortOrder]);
3159
+ }
3160
+ const pagedDocs = docs.slice(query.skip, query.limit && query.skip + query.limit);
3161
+ const trimmed = await Promise.all(
3162
+ pagedDocs.map(async (doc) => {
2430
3163
  doc = await this.trimOutputFields(doc, "list");
2431
3164
  if (_select.length > 0) doc = pick6(doc, _select);
2432
3165
  return doc;
2433
3166
  })
2434
3167
  );
2435
- if (sort) {
2436
- const { sortKey, sortOrder } = parseSortString(sort);
2437
- docs = orderBy(docs, [sortKey], [sortOrder]);
2438
- }
2439
- docs = docs.slice(query.skip, query.limit && query.skip + query.limit);
2440
3168
  return {
2441
3169
  success: true,
2442
3170
  kind: "list",
2443
3171
  code: "success" /* Success */,
2444
- data: docs,
2445
- count: docs.length,
3172
+ data: trimmed,
3173
+ count: trimmed.length,
2446
3174
  totalCount,
2447
3175
  query
2448
3176
  };
@@ -2474,33 +3202,101 @@ var DataService = class {
2474
3202
  trimOutputFields(doc, access, baseFields) {
2475
3203
  return this.pickAllowedFields(doc, access, baseFields);
2476
3204
  }
3205
+ async validateSortFields(sort, access) {
3206
+ const fieldPathPattern = /^[A-Za-z_][A-Za-z0-9_$]*(\.[A-Za-z_][A-Za-z0-9_$]*)*$/;
3207
+ const fields = sort.trim().split(/\s+/).filter(Boolean).map((field) => ({
3208
+ raw: field,
3209
+ name: field.startsWith("-") ? field.slice(1) : field
3210
+ }));
3211
+ const errors = [];
3212
+ for (const { raw, name } of fields) {
3213
+ if (!fieldPathPattern.test(name)) {
3214
+ errors.push({ detail: `Invalid sort field: ${raw}`, pointer: `#/sort` });
3215
+ continue;
3216
+ }
3217
+ if (name === "id" || name === "_id") continue;
3218
+ const allowed = await this.genAllowedFields({}, access);
3219
+ if (!allowed.includes(name)) {
3220
+ errors.push({ detail: `Sort field is not allowed: ${name}`, pointer: `#/sort` });
3221
+ }
3222
+ }
3223
+ return errors;
3224
+ }
2477
3225
  };
2478
3226
 
2479
3227
  // src/processors.ts
2480
- import { castArray as castArray3, cloneDeep, forEach as forEach6, get as get7, isArray as isArray7, map as map2, set as set5 } from "@web-ts-toolkit/utils";
3228
+ import { cloneDeep, forEach as forEach6, get as get7, isArray as isArray8, isPlainObject as isPlainObject12, map as map2, set as set5 } from "@web-ts-toolkit/utils";
3229
+ var UNSAFE_PATH_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
3230
+ function failUnsafeSegment(path) {
3231
+ throw new Error(
3232
+ `copyAndDepopulate: refusing path '${path}' because it contains an unsafe segment (__proto__, prototype, or constructor).`
3233
+ );
3234
+ }
3235
+ function failMissingId(path) {
3236
+ throw new Error(`copyAndDepopulate: path '${path}' resolved to a record missing the configured id field.`);
3237
+ }
3238
+ function assertSafePath(path) {
3239
+ const segs = path.split(".");
3240
+ for (const seg of segs) {
3241
+ if (seg.length === 0) continue;
3242
+ if (UNSAFE_PATH_KEYS.has(seg)) {
3243
+ failUnsafeSegment(path);
3244
+ }
3245
+ }
3246
+ }
2481
3247
  var copyAndDepopulate = (docObject, operations, options = { mutable: true, idField: "_id" }) => {
2482
- const obj = get7(options, "mutable", true) ? docObject : cloneDeep(docObject);
2483
- const idField = get7(options, "idField", "_id");
2484
- forEach6(castArray3(operations), (op) => {
2485
- if (!op.src || !op.dest) return;
3248
+ const mutable = options.mutable !== false;
3249
+ const idField = options.idField ?? "_id";
3250
+ const obj = mutable ? docObject : cloneDeep(docObject);
3251
+ forEach6(Array.isArray(operations) ? operations : [], (op) => {
3252
+ if (!op || typeof op.src !== "string" || typeof op.dest !== "string") return;
3253
+ const src = op.src;
3254
+ const dest = op.dest;
3255
+ if (src.length === 0 || dest.length === 0) return;
3256
+ assertSafePath(src);
3257
+ assertSafePath(dest);
2486
3258
  let targets = [obj];
2487
- const segs = op.src.split(".");
3259
+ const segs = src.split(".");
2488
3260
  forEach6(segs, (seg, ind) => {
2489
3261
  if (segs.length === ind + 1) {
2490
3262
  forEach6(targets, (target) => {
3263
+ if (!isPlainObject12(target)) return;
2491
3264
  const targetObject = target;
2492
- set5(targetObject, op.dest, get7(targetObject, seg));
2493
- set5(
2494
- targetObject,
2495
- seg,
2496
- isArray7(targetObject[seg]) ? map2(targetObject[seg], idField) : get7(targetObject, `${seg}.${idField}`)
2497
- );
3265
+ const leaf = targetObject[seg];
3266
+ const sourceForDest = leaf;
3267
+ let depopulated;
3268
+ if (isArray8(leaf)) {
3269
+ if (leaf.length === 0) {
3270
+ depopulated = [];
3271
+ } else if (leaf.every((item) => isPlainObject12(item))) {
3272
+ depopulated = map2(leaf, idField).map((id, i) => {
3273
+ if (id === void 0) {
3274
+ failMissingId(`${src}[${i}]`);
3275
+ }
3276
+ return id;
3277
+ });
3278
+ } else {
3279
+ return;
3280
+ }
3281
+ } else if (isPlainObject12(leaf)) {
3282
+ const id = get7(leaf, idField);
3283
+ if (id === void 0) {
3284
+ failMissingId(src);
3285
+ }
3286
+ depopulated = id;
3287
+ } else {
3288
+ return;
3289
+ }
3290
+ set5(targetObject, dest, sourceForDest);
3291
+ set5(targetObject, seg, depopulated);
2498
3292
  });
2499
3293
  } else {
2500
3294
  targets = targets.reduce((ret, target) => {
3295
+ if (!isPlainObject12(target)) return ret;
2501
3296
  const targetObject = target;
2502
- if (isArray7(targetObject[seg])) ret.push(...targetObject[seg]);
2503
- else ret.push(targetObject[seg]);
3297
+ const next = targetObject[seg];
3298
+ if (isArray8(next)) ret.push(...next);
3299
+ else if (next !== null && next !== void 0) ret.push(next);
2504
3300
  return ret;
2505
3301
  }, []);
2506
3302
  }
@@ -2548,15 +3344,15 @@ var Cache = class {
2548
3344
 
2549
3345
  // src/core-shared.ts
2550
3346
  import {
2551
- castArray as castArray4,
3347
+ castArray as castArray3,
2552
3348
  arrayToRecord,
2553
3349
  hasOwn as hasOwn2,
2554
- isArray as isArray8,
3350
+ isArray as isArray9,
2555
3351
  isBoolean as isBoolean4,
2556
3352
  isEmpty as isEmpty2,
2557
3353
  isEqual,
2558
3354
  isFunction as isFunction4,
2559
- isPlainObject as isPlainObject10,
3355
+ isPlainObject as isPlainObject13,
2560
3356
  isString as isString5
2561
3357
  } from "@web-ts-toolkit/utils";
2562
3358
 
@@ -2597,10 +3393,10 @@ var permission_default = Permission;
2597
3393
 
2598
3394
  // src/core-shared.ts
2599
3395
  function isAndFilter(filter2) {
2600
- return !!filter2 && isPlainObject10(filter2) && Object.keys(filter2).length === 1 && isArray8(filter2.$and);
3396
+ return !!filter2 && isPlainObject13(filter2) && Object.keys(filter2).length === 1 && isArray9(filter2.$and);
2601
3397
  }
2602
3398
  function isMergeableClause(filter2) {
2603
- return !!filter2 && isPlainObject10(filter2) && Object.keys(filter2).every((key) => !key.startsWith("$"));
3399
+ return !!filter2 && isPlainObject13(filter2) && Object.keys(filter2).every((key) => !key.startsWith("$"));
2604
3400
  }
2605
3401
  function optimizeAndFilter(clausesInput) {
2606
3402
  const clauses = [];
@@ -2614,9 +3410,24 @@ function optimizeAndFilter(clausesInput) {
2614
3410
  clauses.push(clause);
2615
3411
  }
2616
3412
  }
2617
- const dedupedClauses = clauses.filter(
2618
- (clause, index) => clauses.findIndex((item) => isEqual(item, clause)) === index
2619
- );
3413
+ const dedupeKey = (value) => {
3414
+ if (isArray9(value)) {
3415
+ return `[${value.map(dedupeKey).join(",")}]`;
3416
+ }
3417
+ if (isPlainObject13(value)) {
3418
+ return `{${Object.keys(value).sort().map((key) => `${key}:${dedupeKey(value[key])}`).join(",")}}`;
3419
+ }
3420
+ return JSON.stringify(value);
3421
+ };
3422
+ const dedupedClauses = [];
3423
+ const seenClauses = /* @__PURE__ */ new Set();
3424
+ for (let x = 0; x < clauses.length; x++) {
3425
+ const clause = clauses[x];
3426
+ const key = dedupeKey(clause);
3427
+ if (seenClauses.has(key)) continue;
3428
+ seenClauses.add(key);
3429
+ dedupedClauses.push(clause);
3430
+ }
2620
3431
  const mergedClause = {};
2621
3432
  const remainingClauses = [];
2622
3433
  for (let x = 0; x < dedupedClauses.length; x++) {
@@ -2645,7 +3456,7 @@ function optimizeAndFilter(clausesInput) {
2645
3456
  }
2646
3457
  function normalizeFilter(filter2) {
2647
3458
  if (filter2 === false) return false;
2648
- if (!filter2 || !isPlainObject10(filter2) || isEmpty2(filter2)) return null;
3459
+ if (!filter2 || !isPlainObject13(filter2) || isEmpty2(filter2)) return null;
2649
3460
  if (!isAndFilter(filter2)) {
2650
3461
  return filter2;
2651
3462
  }
@@ -2697,8 +3508,8 @@ async function setRequestPermissions(req) {
2697
3508
  const globalPermissions = getGlobalOption("globalPermissions");
2698
3509
  if (!isFunction4(globalPermissions)) return;
2699
3510
  const permissions = await globalPermissions.call(req, req);
2700
- if (isPlainObject10(permissions)) req[requestPermissionField] = permissions;
2701
- else if (isArray8(permissions)) req[requestPermissionField] = arrayToRecord(permissions);
3511
+ if (isPlainObject13(permissions)) req[requestPermissionField] = permissions;
3512
+ else if (isArray9(permissions)) req[requestPermissionField] = arrayToRecord(permissions);
2702
3513
  else if (isString5(permissions)) req[requestPermissionField] = { [permissions]: true };
2703
3514
  }
2704
3515
  async function evaluateRouteGuard(req, permissions, routeGuard) {
@@ -2710,7 +3521,7 @@ async function evaluateRouteGuard(req, permissions, routeGuard) {
2710
3521
  if (isString5(routeGuard)) {
2711
3522
  return stringHandler(routeGuard);
2712
3523
  }
2713
- if (isArray8(routeGuard)) {
3524
+ if (isArray9(routeGuard)) {
2714
3525
  return arrayHandler(routeGuard);
2715
3526
  }
2716
3527
  if (isFunction4(routeGuard)) {
@@ -2719,7 +3530,7 @@ async function evaluateRouteGuard(req, permissions, routeGuard) {
2719
3530
  return false;
2720
3531
  }
2721
3532
  async function callHookChain(req, hook, doc, permissions, context) {
2722
- const hooks = castArray4(hook);
3533
+ const hooks = castArray3(hook);
2723
3534
  for (let x = 0; x < hooks.length; x++) {
2724
3535
  if (isFunction4(hooks[x])) {
2725
3536
  doc = await hooks[x].call(req, doc, permissions, context);
@@ -2748,7 +3559,7 @@ async function collectSchemaFields({
2748
3559
  if (value) fields.push(key);
2749
3560
  } else if (isString5(value)) {
2750
3561
  if (stringHandler(value)) fields.push(key);
2751
- } else if (isArray8(value)) {
3562
+ } else if (isArray9(value)) {
2752
3563
  if (arrayHandler(value)) fields.push(key);
2753
3564
  } else if (isFunction4(value)) {
2754
3565
  if (await value.call(req, ...functionArgs)) fields.push(key);
@@ -2784,7 +3595,7 @@ async function initializeAclRequest({
2784
3595
  await core.setPermissions();
2785
3596
  assignCore(core);
2786
3597
  req[PERMISSIONS] = core.getPermissions();
2787
- req[PERMISSION_KEYS] = req[PERMISSIONS].keys;
3598
+ req[PERMISSION_KEYS] = [...req[PERMISSIONS].keys];
2788
3599
  req[flag] = true;
2789
3600
  return core;
2790
3601
  }
@@ -3028,6 +3839,14 @@ var Core = class {
3028
3839
  if (!allowedParentPaths.includes(parentPath)) return null;
3029
3840
  const refModelName = getModelRef(modelName, ret.path);
3030
3841
  if (!refModelName) return null;
3842
+ const runtime = getActiveRuntime();
3843
+ if (runtime && !runtime.hasModel(refModelName)) {
3844
+ return null;
3845
+ }
3846
+ const allowedTargetOperation = await this.req.macl.isAllowed(refModelName, populateAccess);
3847
+ if (!allowedTargetOperation) {
3848
+ return null;
3849
+ }
3031
3850
  ret.select = await this.genSelect(refModelName, populateAccess, ret.select, false);
3032
3851
  const filter2 = await this.genFilter(refModelName, populateAccess, null);
3033
3852
  if (filter2 === false) return null;
@@ -3043,7 +3862,7 @@ var Core = class {
3043
3862
  if (isFunction5(validate)) {
3044
3863
  const permissions = this.getGlobalPermissions();
3045
3864
  return validate.call(this.req, allowedData, permissions, context);
3046
- } else if (isBoolean5(validate) || isArray9(validate)) {
3865
+ } else if (isBoolean5(validate) || isArray10(validate)) {
3047
3866
  return validate;
3048
3867
  } else {
3049
3868
  return true;
@@ -3096,10 +3915,13 @@ var Core = class {
3096
3915
  const permissions = this.getGlobalPermissions();
3097
3916
  try {
3098
3917
  docPermissions = await docPermissionsFn.call(this.req, doc, permissions, context);
3099
- } catch (error) {
3100
- logger.warn(
3101
- `docPermissions hook failed for model=${modelName} access=${access}: ${error instanceof Error ? error.message : String(error)}`
3102
- );
3918
+ } catch (error2) {
3919
+ warn("docPermissions hook failed; applying empty document permissions", {
3920
+ modelName,
3921
+ access,
3922
+ operation: context.operation,
3923
+ error: error2 instanceof Error ? error2.message : String(error2)
3924
+ });
3103
3925
  }
3104
3926
  }
3105
3927
  return docPermissions;
@@ -3172,7 +3994,7 @@ var Core = class {
3172
3994
  return runDecorateAllHook({ req: this.req, hook: decorateAll, docs, permissions, context });
3173
3995
  }
3174
3996
  runTasks(modelName, docObject, task) {
3175
- const tasks = compact3(castArray5(task));
3997
+ const tasks = compact3(castArray4(task));
3176
3998
  if (tasks.length === 0) return docObject;
3177
3999
  forEach7(tasks, (task2) => {
3178
4000
  const { type, args, options } = task2;
@@ -3257,19 +4079,19 @@ function guard(condition) {
3257
4079
  const permissions = req[PERMISSIONS];
3258
4080
  let cond = condition;
3259
4081
  let phas = (key) => permissions.has(key);
3260
- if (isPlainObject11(condition)) {
4082
+ if (isPlainObject14(condition)) {
3261
4083
  const { modelName, id, condition: _cond } = condition;
3262
4084
  const svc = req.macl.getPublicService(modelName);
3263
4085
  const select = getModelOption(modelName, `alwaysSelectFields.read`, void 0);
3264
4086
  let _id = isString7(id) ? id : null;
3265
- if (isPlainObject11(id)) {
4087
+ if (isPlainObject14(id)) {
3266
4088
  const { type = "param", key } = id;
3267
4089
  if (type === "param") {
3268
4090
  const paramValue = req.params[key];
3269
- _id = isArray10(paramValue) ? paramValue[0] ?? null : paramValue ?? null;
4091
+ _id = isArray11(paramValue) ? paramValue[0] ?? null : paramValue ?? null;
3270
4092
  } else if (type === "query") {
3271
4093
  const _qval = req.query[key];
3272
- if (isArray10(_qval)) _id = _qval.length > 0 ? _qval[0] : null;
4094
+ if (isArray11(_qval)) _id = _qval.length > 0 ? _qval[0] : null;
3273
4095
  else _id = _qval;
3274
4096
  } else {
3275
4097
  _id = null;
@@ -3289,7 +4111,7 @@ function guard(condition) {
3289
4111
  const { stringHandler, arrayHandler } = createValidator(phas);
3290
4112
  if (isString7(cond)) {
3291
4113
  if (stringHandler(cond)) return next();
3292
- } else if (isArray10(cond)) {
4114
+ } else if (isArray11(cond)) {
3293
4115
  if (arrayHandler(cond)) return next();
3294
4116
  } else if (isFunction6(cond)) {
3295
4117
  const result = await cond.call(req, permissions);
@@ -3304,7 +4126,7 @@ import express2 from "express";
3304
4126
 
3305
4127
  // src/routers/model-router.ts
3306
4128
  import JsonRouter5 from "@web-ts-toolkit/express-json-router";
3307
- import { forEach as forEach8, isPlainObject as isPlainObject12, isString as isString9, isUndefined as isUndefined2, normalizeUrlPath as normalizeUrlPath2, padEnd } from "@web-ts-toolkit/utils";
4129
+ import { forEach as forEach8, isPlainObject as isPlainObject15, isString as isString8, isUndefined as isUndefined2, normalizeUrlPath as normalizeUrlPath2, padEnd } from "@web-ts-toolkit/utils";
3308
4130
 
3309
4131
  // src/routers/router-mutation.ts
3310
4132
  var MODEL_BUILD_TIME_OPTION_KEYS = /* @__PURE__ */ new Set([
@@ -3334,9 +4156,37 @@ function assertMutableRouterOptions(kind, options) {
3334
4156
  // src/http/response-pipelines/model-response.ts
3335
4157
  import JsonRouter3 from "@web-ts-toolkit/express-json-router";
3336
4158
 
4159
+ // src/http/response-pipelines/service-result.ts
4160
+ var toPublicErrorResult = (result) => ({
4161
+ success: false,
4162
+ code: result.code,
4163
+ errors: result.errors
4164
+ });
4165
+ var toPublicSingleResult = (result) => ({
4166
+ success: true,
4167
+ kind: "single",
4168
+ code: result.code,
4169
+ data: result.data
4170
+ });
4171
+ var toPublicListResult = (result) => ({
4172
+ success: true,
4173
+ kind: "list",
4174
+ code: result.code,
4175
+ data: result.data,
4176
+ count: result.count,
4177
+ totalCount: result.totalCount ?? null
4178
+ });
4179
+ var toPublicServiceResult = (result) => {
4180
+ if (result.kind === "error") {
4181
+ return toPublicErrorResult(result);
4182
+ }
4183
+ return result.kind === "list" ? toPublicListResult(result) : toPublicSingleResult(result);
4184
+ };
4185
+
3337
4186
  // src/http/response-pipelines/list-response.ts
3338
4187
  var formatListResponse = (req, result, includeCount, includeExtraHeaders) => {
3339
- const { data, count: returnedCount, totalCount } = result;
4188
+ const publicResult = toPublicListResult(result);
4189
+ const { data, count: returnedCount, totalCount } = publicResult;
3340
4190
  const query = result.query ?? {};
3341
4191
  const rawLimit = Number(query.limit ?? 0);
3342
4192
  const skip = Number(query.skip ?? 0);
@@ -3371,16 +4221,18 @@ var formatListResponse = (req, result, includeCount, includeExtraHeaders) => {
3371
4221
  return { data, meta };
3372
4222
  };
3373
4223
  var formatCreatedData = (result) => {
3374
- return result.count === 1 ? result.data[0] : result.data;
4224
+ const publicResult = toPublicListResult(result);
4225
+ return publicResult.count === 1 ? publicResult.data[0] : publicResult.data;
3375
4226
  };
3376
4227
  var formatUpsertCreatedData = (result) => {
3377
- return result.data.length > 0 ? result.data[0] : null;
4228
+ const publicResult = toPublicListResult(result);
4229
+ return publicResult.data.length > 0 ? publicResult.data[0] : null;
3378
4230
  };
3379
4231
 
3380
4232
  // src/http/response-pipelines/model-response.ts
3381
4233
  var success = JsonRouter3.success;
3382
4234
  function unwrapServiceData(result) {
3383
- return result.data;
4235
+ return result.kind === "list" ? toPublicListResult(result).data : toPublicSingleResult(result).data;
3384
4236
  }
3385
4237
  function formatModelListResponse(req, result, includeCount, includeExtraHeaders) {
3386
4238
  return formatListResponse(req, result, includeCount, includeExtraHeaders);
@@ -3389,7 +4241,13 @@ function formatModelCreatedResponse(result) {
3389
4241
  return new success.Created(formatCreatedData(result));
3390
4242
  }
3391
4243
  function formatModelUpsertResponse(result) {
3392
- return result.kind === "list" ? new success.Created(formatUpsertCreatedData(result)) : result.data;
4244
+ if (result.kind === "list") {
4245
+ return new success.Created(formatUpsertCreatedData(result));
4246
+ }
4247
+ if (result.kind === "single") {
4248
+ return toPublicSingleResult(result).data;
4249
+ }
4250
+ throw new Error(`Unexpected ServiceResult kind: ${String(result.kind)}`);
3393
4251
  }
3394
4252
 
3395
4253
  // src/validation/parsers.ts
@@ -3398,9 +4256,12 @@ import JsonRouter4 from "@web-ts-toolkit/express-json-router";
3398
4256
  // src/validation/common.ts
3399
4257
  import { z as z2 } from "zod";
3400
4258
  var stringOrStringArray = z2.union([z2.string(), z2.array(z2.string())]);
4259
+ var safeIntegerString = (min, message) => z2.string().regex(/^\d+$/, message).refine((value) => Number.isSafeInteger(Number(value)), "Expected a safe integer").refine((value) => Number(value) >= min, message);
3401
4260
  var queryBooleanString = z2.enum(["true", "false"]);
3402
- var positiveIntegerString = z2.string().regex(/^\d+$/, "Expected a non-negative integer");
3403
- var nonNegativeIntegerSchema = z2.number().int().min(0);
4261
+ var nonNegativeIntegerString = safeIntegerString(0, "Expected a non-negative integer");
4262
+ var positiveIntegerString = safeIntegerString(1, "Expected a positive integer");
4263
+ var nonNegativeIntegerSchema = z2.number().int().min(0).max(Number.MAX_SAFE_INTEGER);
4264
+ var positiveIntegerSchema = z2.number().int().min(1).max(Number.MAX_SAFE_INTEGER);
3404
4265
  var unknownRecord = z2.record(z2.string(), z2.unknown());
3405
4266
  var projectionObjectSchema = z2.record(z2.string(), z2.union([z2.literal(1), z2.literal(-1)]));
3406
4267
  var projectionSchema = z2.union([z2.string(), z2.array(z2.string()), projectionObjectSchema]);
@@ -3501,6 +4362,10 @@ function parsePathParam(value, parameter) {
3501
4362
  return param;
3502
4363
  }
3503
4364
  function parseQuery(schema, value) {
4365
+ const complexityErrors = validateRequestComplexity(value, getGlobalOption("requestComplexity"), "request");
4366
+ if (complexityErrors.length > 0) {
4367
+ throw new clientErrors.BadRequestError("Bad Request", { errors: complexityErrors });
4368
+ }
3504
4369
  const result = schema.safeParse(value);
3505
4370
  if (!result.success) {
3506
4371
  throwValidationError(normalizeIssues(result.error.issues), void 0, "parameter");
@@ -3508,6 +4373,10 @@ function parseQuery(schema, value) {
3508
4373
  return result.data;
3509
4374
  }
3510
4375
  function parseBody(schema, value) {
4376
+ const complexityErrors = validateRequestComplexity(value ?? {}, getGlobalOption("requestComplexity"), "request");
4377
+ if (complexityErrors.length > 0) {
4378
+ throw new clientErrors.BadRequestError("Bad Request", { errors: complexityErrors });
4379
+ }
3511
4380
  const result = schema.safeParse(value ?? {});
3512
4381
  if (!result.success) {
3513
4382
  throwValidationError(normalizeIssues(result.error.issues), void 0, "pointer");
@@ -3610,10 +4479,10 @@ function fromYup(schema) {
3610
4479
  success: true,
3611
4480
  data
3612
4481
  };
3613
- } catch (error) {
4482
+ } catch (error2) {
3614
4483
  return {
3615
4484
  success: false,
3616
- issues: normalizeYupIssues(error)
4485
+ issues: normalizeYupIssues(error2)
3617
4486
  };
3618
4487
  }
3619
4488
  };
@@ -3716,10 +4585,10 @@ function fromVine(validator) {
3716
4585
  success: true,
3717
4586
  data: output
3718
4587
  };
3719
- } catch (error) {
4588
+ } catch (error2) {
3720
4589
  return {
3721
4590
  success: false,
3722
- issues: normalizeVineError(error)
4591
+ issues: normalizeVineError(error2)
3723
4592
  };
3724
4593
  }
3725
4594
  };
@@ -3740,11 +4609,11 @@ function normalizePathSegment(segment) {
3740
4609
  function isStandardSchemaPathSegment(segment) {
3741
4610
  return typeof segment === "object" && segment !== null && "key" in segment;
3742
4611
  }
3743
- function normalizeYupIssues(error) {
3744
- if (!isYupValidationError(error)) {
4612
+ function normalizeYupIssues(error2) {
4613
+ if (!isYupValidationError(error2)) {
3745
4614
  return [{ message: "Validation failed" }];
3746
4615
  }
3747
- const issues = error.inner?.length ? error.inner : [error];
4616
+ const issues = error2.inner?.length ? error2.inner : [error2];
3748
4617
  return issues.map((issue) => ({
3749
4618
  message: issue.message,
3750
4619
  path: parsePathString(issue.path)
@@ -3795,11 +4664,11 @@ function normalizeSuperstructPath(failure) {
3795
4664
  if (typeof failure.key === "string" || typeof failure.key === "number") return [failure.key];
3796
4665
  return void 0;
3797
4666
  }
3798
- function normalizeVineError(error) {
3799
- if (!isVineValidationError(error)) {
4667
+ function normalizeVineError(error2) {
4668
+ if (!isVineValidationError(error2)) {
3800
4669
  return [{ message: "Validation failed" }];
3801
4670
  }
3802
- return error.messages.map((message) => ({
4671
+ return error2.messages.map((message) => ({
3803
4672
  message: message.message,
3804
4673
  path: normalizeVineField(message)
3805
4674
  }));
@@ -3822,14 +4691,14 @@ function parsePathString(path) {
3822
4691
  if (!path) return void 0;
3823
4692
  return path.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean).map((segment) => /^\d+$/.test(segment) ? Number(segment) : segment);
3824
4693
  }
3825
- function isYupValidationError(error) {
3826
- return typeof error === "object" && error !== null && "message" in error;
4694
+ function isYupValidationError(error2) {
4695
+ return typeof error2 === "object" && error2 !== null && "message" in error2;
3827
4696
  }
3828
4697
  function isArkTypeErrors(value) {
3829
4698
  return Array.isArray(value);
3830
4699
  }
3831
- function isVineValidationError(error) {
3832
- return typeof error === "object" && error !== null && "messages" in error && Array.isArray(error.messages);
4700
+ function isVineValidationError(error2) {
4701
+ return typeof error2 === "object" && error2 !== null && "messages" in error2 && Array.isArray(error2.messages);
3833
4702
  }
3834
4703
  function formatIssue(issue, key, location = "pointer") {
3835
4704
  const path = (issue.path ?? []).map(String);
@@ -3852,9 +4721,9 @@ function buildPointer(path) {
3852
4721
  // src/validation/model-router.ts
3853
4722
  import { z as z3 } from "zod";
3854
4723
  var listQuerySchema = z3.object({
3855
- skip: positiveIntegerString.optional(),
4724
+ skip: nonNegativeIntegerString.optional(),
3856
4725
  limit: positiveIntegerString.optional(),
3857
- page: positiveIntegerString.optional(),
4726
+ page: nonNegativeIntegerString.optional(),
3858
4727
  page_size: positiveIntegerString.optional(),
3859
4728
  skim: queryBooleanString.optional(),
3860
4729
  include_permissions: queryBooleanString.optional(),
@@ -3883,10 +4752,10 @@ var listBodySchema = z3.object({
3883
4752
  populate: populateSchema.optional(),
3884
4753
  include: includeSchema.optional(),
3885
4754
  tasks: tasksSchema.optional(),
3886
- skip: z3.union([nonNegativeIntegerSchema, positiveIntegerString]).optional(),
3887
- limit: z3.union([nonNegativeIntegerSchema, positiveIntegerString]).optional(),
3888
- page: z3.union([nonNegativeIntegerSchema, positiveIntegerString]).optional(),
3889
- pageSize: z3.union([nonNegativeIntegerSchema, positiveIntegerString]).optional(),
4755
+ skip: z3.union([nonNegativeIntegerSchema, nonNegativeIntegerString]).optional(),
4756
+ limit: z3.union([positiveIntegerSchema, positiveIntegerString]).optional(),
4757
+ page: z3.union([nonNegativeIntegerSchema, nonNegativeIntegerString]).optional(),
4758
+ pageSize: z3.union([positiveIntegerSchema, positiveIntegerString]).optional(),
3890
4759
  options: z3.object({
3891
4760
  skim: z3.boolean().optional(),
3892
4761
  includePermissions: z3.boolean().optional(),
@@ -3896,11 +4765,8 @@ var listBodySchema = z3.object({
3896
4765
  }).passthrough().optional()
3897
4766
  }).passthrough().superRefine((body, ctx) => rejectKeys(body, ctx, ["query"]));
3898
4767
  var countBodySchema = z3.object({
3899
- filter: objectOrArraySchema.optional(),
3900
- options: z3.object({
3901
- access: z3.unknown().optional()
3902
- }).passthrough().optional()
3903
- }).passthrough().superRefine((body, ctx) => rejectKeys(body, ctx, ["query", "access"]));
4768
+ filter: objectOrArraySchema.optional()
4769
+ }).passthrough().superRefine((body, ctx) => rejectKeys(body, ctx, ["query", "access", "options"]));
3904
4770
  var readFilterBodySchema = z3.object({
3905
4771
  filter: objectOrArraySchema.optional(),
3906
4772
  select: projectionSchema.optional(),
@@ -3994,10 +4860,10 @@ var dataListBodySchema = z4.object({
3994
4860
  filter: objectOrArraySchema.optional(),
3995
4861
  select: projectionSchema.optional(),
3996
4862
  sort: z4.string().optional(),
3997
- skip: z4.union([nonNegativeIntegerSchema, positiveIntegerString]).optional(),
3998
- limit: z4.union([nonNegativeIntegerSchema, positiveIntegerString]).optional(),
3999
- page: z4.union([nonNegativeIntegerSchema, positiveIntegerString]).optional(),
4000
- pageSize: z4.union([nonNegativeIntegerSchema, positiveIntegerString]).optional(),
4863
+ skip: z4.union([nonNegativeIntegerSchema, nonNegativeIntegerString]).optional(),
4864
+ limit: z4.union([positiveIntegerSchema, positiveIntegerString]).optional(),
4865
+ page: z4.union([nonNegativeIntegerSchema, nonNegativeIntegerString]).optional(),
4866
+ pageSize: z4.union([positiveIntegerSchema, positiveIntegerString]).optional(),
4001
4867
  options: z4.object({
4002
4868
  includeCount: z4.boolean().optional(),
4003
4869
  includeExtraHeaders: z4.boolean().optional()
@@ -4016,17 +4882,17 @@ import { z as z5 } from "zod";
4016
4882
  var rootEntryBaseSchema = {
4017
4883
  target: z5.enum(["model", "data"]),
4018
4884
  name: z5.string().min(1),
4019
- order: z5.number().int().optional()
4885
+ order: nonNegativeIntegerSchema.optional()
4020
4886
  };
4021
4887
  var rootModelListArgsSchema = z5.object({
4022
4888
  select: projectionSchema.optional(),
4023
4889
  populate: populateSchema.optional(),
4024
4890
  include: includeSchema.optional(),
4025
4891
  sort: sortSchema.optional(),
4026
- skip: z5.union([nonNegativeIntegerSchema, positiveIntegerString]).optional(),
4027
- limit: z5.union([nonNegativeIntegerSchema, positiveIntegerString]).optional(),
4028
- page: z5.union([nonNegativeIntegerSchema, positiveIntegerString]).optional(),
4029
- pageSize: z5.union([nonNegativeIntegerSchema, positiveIntegerString]).optional(),
4892
+ skip: z5.union([nonNegativeIntegerSchema, nonNegativeIntegerString]).optional(),
4893
+ limit: z5.union([positiveIntegerSchema, positiveIntegerString]).optional(),
4894
+ page: z5.union([nonNegativeIntegerSchema, nonNegativeIntegerString]).optional(),
4895
+ pageSize: z5.union([positiveIntegerSchema, positiveIntegerString]).optional(),
4030
4896
  tasks: tasksSchema.optional()
4031
4897
  }).passthrough();
4032
4898
  var rootModelListOptionsSchema = z5.object({
@@ -4082,16 +4948,14 @@ var rootModelSubReadArgsSchema = z5.object({
4082
4948
  select: fieldsSchema.optional(),
4083
4949
  populate: subPopulateSchema.optional()
4084
4950
  }).passthrough();
4085
- var rootModelCountOptionsSchema = z5.object({
4086
- access: z5.unknown().optional()
4087
- }).passthrough();
4951
+ var rootModelCountOptionsSchema = z5.object({}).strict().optional();
4088
4952
  var rootDataListArgsSchema = z5.object({
4089
4953
  select: projectionSchema.optional(),
4090
4954
  sort: z5.string().optional(),
4091
- skip: z5.union([nonNegativeIntegerSchema, positiveIntegerString]).optional(),
4092
- limit: z5.union([nonNegativeIntegerSchema, positiveIntegerString]).optional(),
4093
- page: z5.union([nonNegativeIntegerSchema, positiveIntegerString]).optional(),
4094
- pageSize: z5.union([nonNegativeIntegerSchema, positiveIntegerString]).optional()
4955
+ skip: z5.union([nonNegativeIntegerSchema, nonNegativeIntegerString]).optional(),
4956
+ limit: z5.union([positiveIntegerSchema, positiveIntegerString]).optional(),
4957
+ page: z5.union([nonNegativeIntegerSchema, nonNegativeIntegerString]).optional(),
4958
+ pageSize: z5.union([positiveIntegerSchema, positiveIntegerString]).optional()
4095
4959
  }).passthrough();
4096
4960
  var rootDataListOptionsSchema = z5.object({
4097
4961
  includeCount: z5.boolean().optional()
@@ -4099,8 +4963,21 @@ var rootDataListOptionsSchema = z5.object({
4099
4963
  var rootDataReadArgsSchema = z5.object({
4100
4964
  select: projectionSchema.optional()
4101
4965
  }).passthrough();
4966
+ var rootModelNewArgsSchema = z5.object({
4967
+ select: projectionSchema.optional()
4968
+ }).passthrough();
4969
+ var rootModelNewOptionsSchema = z5.object({
4970
+ skim: z5.boolean().optional(),
4971
+ includePermissions: z5.boolean().optional()
4972
+ }).passthrough();
4102
4973
  var rootModelQueryEntrySchema = z5.union([
4103
- z5.object({ ...rootEntryBaseSchema, target: z5.literal("model"), op: z5.literal("new") }).passthrough(),
4974
+ z5.object({
4975
+ ...rootEntryBaseSchema,
4976
+ target: z5.literal("model"),
4977
+ op: z5.literal("new"),
4978
+ args: rootModelNewArgsSchema.optional(),
4979
+ options: rootModelNewOptionsSchema.optional()
4980
+ }).passthrough(),
4104
4981
  z5.object({
4105
4982
  ...rootEntryBaseSchema,
4106
4983
  target: z5.literal("model"),
@@ -4215,7 +5092,7 @@ var rootModelQueryEntrySchema = z5.union([
4215
5092
  op: z5.literal("count"),
4216
5093
  filter: objectOrArraySchema.optional(),
4217
5094
  options: rootModelCountOptionsSchema.optional()
4218
- }).passthrough()
5095
+ }).passthrough().superRefine((body, ctx) => rejectKeys(body, ctx, ["access"]))
4219
5096
  ]);
4220
5097
  var rootDataQueryEntrySchema = z5.union([
4221
5098
  z5.object({
@@ -4374,8 +5251,9 @@ function setModelCollectionRoutes(context) {
4374
5251
  body: defineOpenApiSchemaResolver(getAdvancedCreateOpenApiBody)
4375
5252
  });
4376
5253
  router.get("/new", async (req) => {
5254
+ await context.assertAllowed(req, "new");
4377
5255
  const svc = context.getPublicService(req);
4378
- const result = await svc._new();
5256
+ const result = await svc._new({}, { includePermissions: false });
4379
5257
  handleResultError(result);
4380
5258
  return unwrapServiceData(result);
4381
5259
  });
@@ -4423,16 +5301,13 @@ function setModelDocumentRoutes(context) {
4423
5301
  });
4424
5302
  router.post("/count", async (req) => {
4425
5303
  await context.assertAllowed(req, "count");
4426
- const { filter: filter2, options: countOptions = {} } = await parseBodyWithSchema(
5304
+ const { filter: filter2 } = await parseBodyWithSchema(
4427
5305
  countBodySchema,
4428
5306
  req.body,
4429
5307
  context.getRequestSchema("requestSchemas.count")
4430
5308
  );
4431
5309
  const svc = context.getPublicService(req);
4432
- const result = await svc._count(
4433
- filter2 ?? {},
4434
- countOptions.access
4435
- );
5310
+ const result = await svc._count(filter2 ?? {});
4436
5311
  handleResultError(result);
4437
5312
  return unwrapServiceData(result);
4438
5313
  });
@@ -4710,7 +5585,6 @@ function setModelDocumentRoutes(context) {
4710
5585
  }
4711
5586
 
4712
5587
  // src/routers/model-router-subdocument-routes.ts
4713
- import { isString as isString8 } from "@web-ts-toolkit/utils";
4714
5588
  function setModelSubDocumentRoutes(context) {
4715
5589
  const subs = getModelSub(context.modelName);
4716
5590
  for (let x = 0; x < subs.length; x++) {
@@ -4740,7 +5614,10 @@ function setModelSubDocumentRoutes(context) {
4740
5614
  context.getRequestSchema("requestSchemas.subList")
4741
5615
  );
4742
5616
  const svc = context.getPublicService(req);
4743
- const result = await svc.listSub(id, sub, { filter: body.filter ?? {}, select: body.select ?? [] });
5617
+ const result = await svc.listSub(id, sub, {
5618
+ filter: body.filter ?? {},
5619
+ select: body.select ?? []
5620
+ });
4744
5621
  handleResultError(result);
4745
5622
  return unwrapServiceData(result);
4746
5623
  }
@@ -4800,10 +5677,11 @@ function setModelSubDocumentRoutes(context) {
4800
5677
  req.body,
4801
5678
  context.getRequestSchema("requestSchemas.subRead")
4802
5679
  );
4803
- const populate = body.populate;
4804
- const normalizedPopulate = Array.isArray(populate) ? populate.map((item) => isString8(item) ? { path: item } : item) : isString8(populate) ? { path: populate } : populate ?? [];
4805
5680
  const svc = context.getPublicService(req);
4806
- const result = await svc.readSub(id, sub, subId, { select: body.select ?? [], populate: normalizedPopulate });
5681
+ const result = await svc.readSub(id, sub, subId, {
5682
+ select: body.select ?? [],
5683
+ populate: normalizeSubPopulate(body.populate)
5684
+ });
4807
5685
  handleResultError(result);
4808
5686
  return unwrapServiceData(result);
4809
5687
  }
@@ -4897,7 +5775,7 @@ function setOption(parentKey, optionKey, option) {
4897
5775
  this.runtime.setModelOption(this.modelName, key, value);
4898
5776
  return this;
4899
5777
  }
4900
- var ModelRouter = class {
5778
+ var ModelRouter = class _ModelRouter {
4901
5779
  constructor(modelName, initialOptions, runtime = defaultRuntime) {
4902
5780
  /**
4903
5781
  * The maximum limit of the number of documents returned from the `list` operation.
@@ -5051,17 +5929,21 @@ var ModelRouter = class {
5051
5929
  );
5052
5930
  this.runtime = runtime;
5053
5931
  this.runtime.setModelOptions(modelName, initialOptions);
5054
- attachRuntimeToModel(modelName, this.runtime);
5055
5932
  this.options = this.runtime.getModelOptions(modelName);
5056
5933
  this.fullBasePath = normalizeUrlPath2(this.options.parentPath + this.options.basePath);
5057
5934
  this.modelName = modelName;
5058
5935
  this.router = new JsonRouter5(this.options.basePath, createSetCore(this.runtime), accessRouterResponseHandler);
5059
- this.model = new model_default(modelName);
5936
+ this.model = new model_default(modelName, this.runtime);
5060
5937
  this.setCollectionRoutes();
5061
5938
  this.setDocumentRoutes();
5062
5939
  this.setSubDocumentRoutes();
5063
5940
  this.logEndpoints();
5064
5941
  }
5942
+ static fromModel(model, initialOptions, runtime = defaultRuntime) {
5943
+ const modelName = String(model.modelName);
5944
+ runtime.registerModelInstance(modelName, model);
5945
+ return new _ModelRouter(modelName, initialOptions, runtime);
5946
+ }
5065
5947
  getRequestSchema(key) {
5066
5948
  return this.runtime.getExactModelOption(
5067
5949
  this.modelName,
@@ -5125,13 +6007,13 @@ var ModelRouter = class {
5125
6007
  }
5126
6008
  logEndpoints() {
5127
6009
  runWithRuntime(this.runtime, () => {
5128
- forEach8(this.router.endpoints, ({ method, path }) => {
5129
- logger.info(`${padEnd(method, 6)} ${normalizeUrlPath2(this.options.parentPath + path)}`);
6010
+ forEach8(this.router.getEndpoints(), ({ method, path }) => {
6011
+ logInfoMessage(`${padEnd(method, 6)} ${normalizeUrlPath2(this.options.parentPath + path)}`);
5130
6012
  });
5131
6013
  });
5132
6014
  }
5133
6015
  set(keyOrOptions, value) {
5134
- if (arguments.length === 2 && isString9(keyOrOptions)) {
6016
+ if (arguments.length === 2 && isString8(keyOrOptions)) {
5135
6017
  assertMutableRouterOption("model", keyOrOptions);
5136
6018
  this.runtime.setModelOption(
5137
6019
  this.modelName,
@@ -5139,7 +6021,7 @@ var ModelRouter = class {
5139
6021
  value
5140
6022
  );
5141
6023
  }
5142
- if (arguments.length === 1 && isPlainObject12(keyOrOptions)) {
6024
+ if (arguments.length === 1 && isPlainObject15(keyOrOptions)) {
5143
6025
  assertMutableRouterOptions("model", keyOrOptions);
5144
6026
  this.runtime.setModelOptions(this.modelName, keyOrOptions);
5145
6027
  }
@@ -5162,7 +6044,7 @@ var ModelRouter = class {
5162
6044
 
5163
6045
  // src/routers/root-router.ts
5164
6046
  import JsonRouter6 from "@web-ts-toolkit/express-json-router";
5165
- import { isNumber as _isNumber, isString as isString10, normalizeUrlPath as normalizeUrlPath3, orderBy as _orderBy } from "@web-ts-toolkit/utils";
6047
+ import { isNumber as _isNumber, normalizeUrlPath as normalizeUrlPath3, orderBy as _orderBy } from "@web-ts-toolkit/utils";
5166
6048
 
5167
6049
  // src/core-data.ts
5168
6050
  var DataCore = class {
@@ -5330,16 +6212,19 @@ async function decorateDataSingleResult(svc, result, context = {}) {
5330
6212
 
5331
6213
  // src/routers/root-router.ts
5332
6214
  var clientErrors3 = JsonRouter6.clientErrors;
6215
+ var DEFAULT_MAX_BATCH_ENTRIES = 100;
6216
+ var DEFAULT_MAX_ORDER_GROUPS = 25;
6217
+ var DEFAULT_MAX_CONCURRENT_OPERATIONS = 10;
5333
6218
  var createErrorResult = (code, detail) => ({
5334
6219
  success: false,
6220
+ kind: "error",
5335
6221
  code,
5336
6222
  errors: [{ detail }]
5337
6223
  });
5338
- var normalizeSubPopulate = (populate) => Array.isArray(populate) ? populate.map((item) => isString10(item) ? { path: item } : item) : isString10(populate) ? { path: populate } : populate ?? [];
5339
6224
  var RootRouter = class {
5340
6225
  constructor(options = { basePath: "", operationAccess: true }, runtime = defaultRuntime) {
5341
6226
  this.modelOperations = {
5342
- new: async (req, item) => req.macl.getPublicService(item.name)._new(),
6227
+ new: async (req, item) => req.macl.getPublicService(item.name)._new(item.args, item.options),
5343
6228
  list: async (req, item) => req.macl.getPublicService(item.name)._list(item.filter ?? {}, item.args, item.options),
5344
6229
  read: async (req, item) => {
5345
6230
  const svc = req.macl.getPublicService(item.name);
@@ -5362,7 +6247,7 @@ var RootRouter = class {
5362
6247
  subBulkUpdate: async (req, item) => req.macl.getPublicService(item.name).bulkUpdateSub(item.id, item.sub, item.data),
5363
6248
  subDelete: async (req, item) => req.macl.getPublicService(item.name).deleteSub(item.id, item.sub, item.subId),
5364
6249
  distinct: async (req, item) => req.macl.getPublicService(item.name)._distinct(item.field, { filter: item.filter }),
5365
- count: async (req, item) => req.macl.getPublicService(item.name)._count(item.filter ?? {}, item.options?.access)
6250
+ count: async (req, item) => req.macl.getPublicService(item.name)._count(item.filter ?? {})
5366
6251
  };
5367
6252
  this.dataOperations = {
5368
6253
  list: async (req, item) => {
@@ -5397,10 +6282,16 @@ var RootRouter = class {
5397
6282
  return decorateDataSingleResult(svc, result, { dataName: item.name });
5398
6283
  }
5399
6284
  };
5400
- const { basePath, operationAccess } = options;
6285
+ const { basePath, operationAccess, maxBatchEntries, maxOrderGroups, maxConcurrentOperations } = options;
5401
6286
  this.runtime = runtime;
5402
6287
  this.basename = basePath || "";
5403
6288
  this.operationAccess = operationAccess;
6289
+ this.maxBatchEntries = this.normalizePositiveLimit(maxBatchEntries, DEFAULT_MAX_BATCH_ENTRIES);
6290
+ this.maxOrderGroups = this.normalizePositiveLimit(maxOrderGroups, DEFAULT_MAX_ORDER_GROUPS);
6291
+ this.maxConcurrentOperations = this.normalizePositiveLimit(
6292
+ maxConcurrentOperations,
6293
+ DEFAULT_MAX_CONCURRENT_OPERATIONS
6294
+ );
5404
6295
  this.router = new JsonRouter6(
5405
6296
  this.basename,
5406
6297
  [createSetCore(this.runtime), createSetDataCore(this.runtime)],
@@ -5417,7 +6308,7 @@ var RootRouter = class {
5417
6308
  target: item.target,
5418
6309
  name: item.name,
5419
6310
  op: item.op,
5420
- result,
6311
+ result: toPublicServiceResult(result),
5421
6312
  message: mapCodeToMessage(result.code),
5422
6313
  statusCode: mapCodeToStatusCode(result.code)
5423
6314
  };
@@ -5468,17 +6359,63 @@ var RootRouter = class {
5468
6359
  return this.wrapResult(index, item, await handler(req, item));
5469
6360
  }
5470
6361
  groupItemsByOrder(items) {
5471
- return items.reduce((acc, item, index) => {
5472
- let order = 0;
5473
- if (_isNumber(item.order)) {
5474
- order = item.order < 0 ? 0 : item.order;
6362
+ const groups = /* @__PURE__ */ new Map();
6363
+ items.forEach((item, index) => {
6364
+ const order = _isNumber(item.order) ? item.order : 0;
6365
+ const group = groups.get(order);
6366
+ if (group) {
6367
+ group.push({ index, item });
6368
+ return;
5475
6369
  }
5476
- if (!acc[order]) {
5477
- acc[order] = [];
6370
+ groups.set(order, [{ index, item }]);
6371
+ });
6372
+ return Array.from(groups.entries()).sort(([leftOrder], [rightOrder]) => leftOrder - rightOrder).map(([, group]) => group);
6373
+ }
6374
+ normalizePositiveLimit(value, fallback) {
6375
+ return Number.isSafeInteger(value) && Number(value) > 0 ? Number(value) : fallback;
6376
+ }
6377
+ validateBatch(items) {
6378
+ if (items.length > this.maxBatchEntries) {
6379
+ throw new clientErrors3.BadRequestError("Bad Request", {
6380
+ errors: [{ detail: `Root batch exceeds maximum size of ${this.maxBatchEntries}` }]
6381
+ });
6382
+ }
6383
+ const orders = /* @__PURE__ */ new Set();
6384
+ for (const item of items) {
6385
+ const order = item.order ?? 0;
6386
+ if (!Number.isSafeInteger(order) || order < 0) {
6387
+ throw new clientErrors3.BadRequestError("Bad Request", {
6388
+ errors: [{ detail: "Root batch order must be a non-negative safe integer" }]
6389
+ });
5478
6390
  }
5479
- acc[order].push({ index, item });
5480
- return acc;
5481
- }, []);
6391
+ if (order > this.maxOrderGroups) {
6392
+ throw new clientErrors3.BadRequestError("Bad Request", {
6393
+ errors: [{ detail: `Root batch order exceeds maximum group index of ${this.maxOrderGroups}` }]
6394
+ });
6395
+ }
6396
+ orders.add(order);
6397
+ }
6398
+ if (orders.size > this.maxOrderGroups) {
6399
+ throw new clientErrors3.BadRequestError("Bad Request", {
6400
+ errors: [{ detail: `Root batch exceeds maximum order groups of ${this.maxOrderGroups}` }]
6401
+ });
6402
+ }
6403
+ }
6404
+ async runGroup(req, entries) {
6405
+ const results = new Array(entries.length);
6406
+ let cursor = 0;
6407
+ const workerCount = Math.min(this.maxConcurrentOperations, entries.length);
6408
+ await Promise.all(
6409
+ Array.from({ length: workerCount }, async () => {
6410
+ while (cursor < entries.length) {
6411
+ const current = cursor;
6412
+ cursor += 1;
6413
+ const { item, index } = entries[current];
6414
+ results[current] = await this.processOp(req, item, index);
6415
+ }
6416
+ })
6417
+ );
6418
+ return results;
5482
6419
  }
5483
6420
  async assertAllowed(req) {
5484
6421
  const allowed = await req.macl.canActivate(this.operationAccess);
@@ -5488,10 +6425,11 @@ var RootRouter = class {
5488
6425
  this.router.post("", async (req) => {
5489
6426
  await this.assertAllowed(req);
5490
6427
  const items = parseBody(rootQuerySchema, req.body);
6428
+ this.validateBatch(items);
5491
6429
  const groupedItems = this.groupItemsByOrder(items);
5492
6430
  const results = [];
5493
6431
  for (let x = 0; x < groupedItems.length; x++) {
5494
- const arrResult = await Promise.all(groupedItems[x].map(({ item, index }) => this.processOp(req, item, index)));
6432
+ const arrResult = await this.runGroup(req, groupedItems[x]);
5495
6433
  results.push(...arrResult);
5496
6434
  }
5497
6435
  return _orderBy(results, ["index"], ["asc"]);
@@ -5501,7 +6439,8 @@ var RootRouter = class {
5501
6439
  path: "",
5502
6440
  operationId: "root.query",
5503
6441
  summary: "Execute batched model and data operations",
5504
- body: rootQuerySchema
6442
+ body: rootQuerySchema,
6443
+ idempotent: true
5505
6444
  });
5506
6445
  }
5507
6446
  get routes() {
@@ -5511,7 +6450,7 @@ var RootRouter = class {
5511
6450
 
5512
6451
  // src/routers/data-router.ts
5513
6452
  import JsonRouter7 from "@web-ts-toolkit/express-json-router";
5514
- import { isPlainObject as isPlainObject13, isString as isString11, isUndefined as isUndefined3, normalizeUrlPath as normalizeUrlPath4 } from "@web-ts-toolkit/utils";
6453
+ import { isPlainObject as isPlainObject16, isString as isString9, isUndefined as isUndefined3, normalizeUrlPath as normalizeUrlPath4 } from "@web-ts-toolkit/utils";
5515
6454
  var clientErrors4 = JsonRouter7.clientErrors;
5516
6455
  function setOption2(parentKey, optionKey, option) {
5517
6456
  const key = isUndefined3(option) ? parentKey : `${parentKey}.${optionKey}`;
@@ -5648,7 +6587,7 @@ var DataRouter = class {
5648
6587
  const result = await svc.findById(id, {}, {});
5649
6588
  handleResultError(result);
5650
6589
  const decoratedResult = await decorateDataSingleResult(svc, result);
5651
- return decoratedResult.data;
6590
+ return unwrapServiceData(decoratedResult);
5652
6591
  });
5653
6592
  this.registerOpenApiRoute({
5654
6593
  method: "get",
@@ -5667,7 +6606,7 @@ var DataRouter = class {
5667
6606
  const result = await svc.findOne(filter2 ?? {}, { select }, {});
5668
6607
  handleResultError(result);
5669
6608
  const decoratedResult = await decorateDataSingleResult(svc, result);
5670
- return decoratedResult.data;
6609
+ return unwrapServiceData(decoratedResult);
5671
6610
  });
5672
6611
  this.registerOpenApiRoute({
5673
6612
  method: "post",
@@ -5690,7 +6629,7 @@ var DataRouter = class {
5690
6629
  const result = await svc.findById(id, { select }, {});
5691
6630
  handleResultError(result);
5692
6631
  const decoratedResult = await decorateDataSingleResult(svc, result);
5693
- return decoratedResult.data;
6632
+ return unwrapServiceData(decoratedResult);
5694
6633
  });
5695
6634
  this.registerOpenApiRoute({
5696
6635
  method: "post",
@@ -5703,11 +6642,11 @@ var DataRouter = class {
5703
6642
  });
5704
6643
  }
5705
6644
  set(keyOrOptions, value) {
5706
- if (arguments.length === 2 && isString11(keyOrOptions)) {
6645
+ if (arguments.length === 2 && isString9(keyOrOptions)) {
5707
6646
  assertMutableRouterOption("data", keyOrOptions);
5708
6647
  this.runtime.setDataOption(this.dataName, keyOrOptions, value);
5709
6648
  }
5710
- if (arguments.length === 1 && isPlainObject13(keyOrOptions)) {
6649
+ if (arguments.length === 1 && isPlainObject16(keyOrOptions)) {
5711
6650
  assertMutableRouterOptions("data", keyOrOptions);
5712
6651
  this.runtime.setDataOptions(this.dataName, keyOrOptions);
5713
6652
  }
@@ -5735,6 +6674,9 @@ import JsonRouter8 from "@web-ts-toolkit/express-json-router";
5735
6674
  import express from "express";
5736
6675
  import { posix } from "path";
5737
6676
  var escapeHtml = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
6677
+ var escapeInlineScriptJson = (value) => {
6678
+ return value.replace(/<\/script/gi, "<\\/script").replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
6679
+ };
5738
6680
  var renderDocsHtml = (specPath, title, cssUrl, bundleUrl) => `<!doctype html>
5739
6681
  <html lang="en">
5740
6682
  <head>
@@ -5748,7 +6690,7 @@ var renderDocsHtml = (specPath, title, cssUrl, bundleUrl) => `<!doctype html>
5748
6690
  <script src="${escapeHtml(bundleUrl)}"></script>
5749
6691
  <script>
5750
6692
  window.ui = SwaggerUIBundle({
5751
- url: ${JSON.stringify(specPath)},
6693
+ url: ${escapeInlineScriptJson(JSON.stringify(specPath))},
5752
6694
  dom_id: '#swagger-ui',
5753
6695
  deepLinking: true,
5754
6696
  presets: [SwaggerUIBundle.presets.apis],
@@ -5803,10 +6745,10 @@ var accessRouterResponseHandler = JsonRouter8.createHandler({
5803
6745
  errorFormat: JsonRouter8.ErrorFormats.rfc9457,
5804
6746
  rfc9457ContentType: "application/problem+json"
5805
6747
  });
5806
- accessRouterResponseHandler.errorMessageProvider = function(error) {
5807
- const errorLike = error;
5808
- logger.error(error);
5809
- return errorLike.message || errorLike._message || String(error);
6748
+ accessRouterResponseHandler.errorMessageProvider = function(error2) {
6749
+ const errorLike = error2;
6750
+ error(error2);
6751
+ return errorLike.message || errorLike._message || String(error2);
5810
6752
  };
5811
6753
 
5812
6754
  // src/plugins.ts
@@ -5819,6 +6761,7 @@ function permissionsPlugin(schema, options) {
5819
6761
  }
5820
6762
 
5821
6763
  // src/index.ts
6764
+ var isMongooseModel = (value) => typeof value === "function" && value !== null && "modelName" in value && "schema" in value;
5822
6765
  function createRuntimeApi(runtime) {
5823
6766
  const runtimeMiddleware = function() {
5824
6767
  return createSetCore(runtime);
@@ -5826,8 +6769,18 @@ function createRuntimeApi(runtime) {
5826
6769
  const wtt2 = runtimeMiddleware;
5827
6770
  wtt2.runtime = runtime;
5828
6771
  wtt2.createRouter = function(modelName, options) {
5829
- const resolvedModelName = typeof modelName === "string" ? modelName : modelName && "modelName" in modelName ? modelName.modelName : modelName;
5830
- return isUndefined4(options) ? new RootRouter(modelName, runtime) : new ModelRouter(resolvedModelName, options, runtime);
6772
+ if (isUndefined4(options)) {
6773
+ return new RootRouter(modelName, runtime);
6774
+ }
6775
+ if (typeof modelName === "string") {
6776
+ return new ModelRouter(modelName, options, runtime);
6777
+ }
6778
+ if (isMongooseModel(modelName)) {
6779
+ const name = String(modelName.modelName);
6780
+ runtime.registerModelInstance(name, modelName);
6781
+ return ModelRouter.fromModel(modelName, options, runtime);
6782
+ }
6783
+ throw new TypeError(`createRouter: unsupported first argument type: ${typeof modelName}`);
5831
6784
  };
5832
6785
  wtt2.createDataRouter = function(dataName, options) {
5833
6786
  return new DataRouter(dataName, options, runtime);
@@ -5837,10 +6790,10 @@ function createRuntimeApi(runtime) {
5837
6790
  };
5838
6791
  wtt2.combineRoutes = combineRoutes;
5839
6792
  wtt2.set = function(keyOrOptions, value) {
5840
- if (arguments.length === 2 && isString12(keyOrOptions)) {
6793
+ if (arguments.length === 2 && isString10(keyOrOptions)) {
5841
6794
  return runtime.setGlobalOption(keyOrOptions, value);
5842
6795
  }
5843
- if (arguments.length === 1 && isPlainObject14(keyOrOptions)) {
6796
+ if (arguments.length === 1 && isPlainObject17(keyOrOptions)) {
5844
6797
  return runtime.setGlobalOptions(keyOrOptions);
5845
6798
  }
5846
6799
  };
@@ -5858,6 +6811,9 @@ function createRuntimeApi(runtime) {
5858
6811
  wtt2.setDefaultModelOption = runtime.setDefaultModelOption.bind(runtime);
5859
6812
  wtt2.getDefaultModelOptions = runtime.getDefaultModelOptions.bind(runtime);
5860
6813
  wtt2.getDefaultModelOption = runtime.getDefaultModelOption.bind(runtime);
6814
+ wtt2.registerModelInstance = runtime.registerModelInstance.bind(runtime);
6815
+ wtt2.hasModelInstance = runtime.hasModelInstance.bind(runtime);
6816
+ wtt2.getModelInstance = runtime.getModelInstance.bind(runtime);
5861
6817
  wtt2.RootRouter = RootRouter;
5862
6818
  wtt2.ModelRouter = ModelRouter;
5863
6819
  wtt2.DataRouter = DataRouter;
@@ -5873,12 +6829,15 @@ export {
5873
6829
  AccessRuntime,
5874
6830
  DataRouter,
5875
6831
  ModelRouter,
6832
+ OpenApiCollisionError,
6833
+ OpenApiRegistry,
5876
6834
  RootRouter,
5877
6835
  acl,
5878
6836
  combineRoutes,
5879
6837
  createAccessRuntime,
5880
6838
  createOpenApiRouter,
5881
6839
  index_default as default,
6840
+ defaultRuntime,
5882
6841
  defineRequestSchema,
5883
6842
  fromAjv,
5884
6843
  fromArkType,
@@ -5894,12 +6853,19 @@ export {
5894
6853
  getDefaultModelOptions,
5895
6854
  getGlobalOption,
5896
6855
  getGlobalOptions,
6856
+ getModelInstance,
5897
6857
  getModelJsonSchema,
5898
6858
  getModelNames,
5899
6859
  getModelOption,
5900
6860
  getModelOptions,
5901
6861
  guard,
6862
+ hasModelInstance,
6863
+ isLevelEnabled,
5902
6864
  permissionsPlugin,
6865
+ redactFilter,
6866
+ redactPayload,
6867
+ registerModelInstance,
6868
+ safeStringify,
5903
6869
  setDefaultModelOption,
5904
6870
  setDefaultModelOptions,
5905
6871
  setGlobalOption,