@web-ts-toolkit/access-router 0.31.5 → 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.js CHANGED
@@ -32,12 +32,15 @@ __export(index_exports, {
32
32
  AccessRuntime: () => AccessRuntime,
33
33
  DataRouter: () => DataRouter,
34
34
  ModelRouter: () => ModelRouter,
35
+ OpenApiCollisionError: () => OpenApiCollisionError,
36
+ OpenApiRegistry: () => OpenApiRegistry,
35
37
  RootRouter: () => RootRouter,
36
38
  acl: () => acl,
37
39
  combineRoutes: () => combineRoutes,
38
40
  createAccessRuntime: () => createAccessRuntime,
39
41
  createOpenApiRouter: () => createOpenApiRouter,
40
42
  default: () => index_default,
43
+ defaultRuntime: () => defaultRuntime,
41
44
  defineRequestSchema: () => defineRequestSchema,
42
45
  fromAjv: () => fromAjv,
43
46
  fromArkType: () => fromArkType,
@@ -53,12 +56,19 @@ __export(index_exports, {
53
56
  getDefaultModelOptions: () => getDefaultModelOptions,
54
57
  getGlobalOption: () => getGlobalOption,
55
58
  getGlobalOptions: () => getGlobalOptions,
59
+ getModelInstance: () => getModelInstance,
56
60
  getModelJsonSchema: () => getModelJsonSchema,
57
61
  getModelNames: () => getModelNames,
58
62
  getModelOption: () => getModelOption,
59
63
  getModelOptions: () => getModelOptions,
60
64
  guard: () => guard,
65
+ hasModelInstance: () => hasModelInstance,
66
+ isLevelEnabled: () => isLevelEnabled,
61
67
  permissionsPlugin: () => permissionsPlugin,
68
+ redactFilter: () => redactFilter,
69
+ redactPayload: () => redactPayload,
70
+ registerModelInstance: () => registerModelInstance,
71
+ safeStringify: () => safeStringify,
62
72
  setDefaultModelOption: () => setDefaultModelOption,
63
73
  setDefaultModelOptions: () => setDefaultModelOptions,
64
74
  setGlobalOption: () => setGlobalOption,
@@ -71,15 +81,15 @@ var import_utils28 = require("@web-ts-toolkit/utils");
71
81
 
72
82
  // src/middleware.ts
73
83
  var import_express_json_router2 = __toESM(require("@web-ts-toolkit/express-json-router"));
74
- var import_utils21 = require("@web-ts-toolkit/utils");
84
+ var import_utils22 = require("@web-ts-toolkit/utils");
75
85
 
76
86
  // src/core.ts
77
- var import_utils20 = require("@web-ts-toolkit/utils");
87
+ var import_utils21 = require("@web-ts-toolkit/utils");
78
88
 
79
89
  // src/runtime.ts
80
90
  var import_mongoose3 = __toESM(require("mongoose"));
81
91
  var import_mongoose_schema_jsonschema = __toESM(require("mongoose-schema-jsonschema"));
82
- var import_utils10 = require("@web-ts-toolkit/utils");
92
+ var import_utils11 = require("@web-ts-toolkit/utils");
83
93
 
84
94
  // src/helpers/collection.ts
85
95
  var import_sift = __toESM(require("sift"));
@@ -100,6 +110,12 @@ var import_utils3 = require("@web-ts-toolkit/utils");
100
110
 
101
111
  // src/helpers/query.ts
102
112
  var import_utils2 = require("@web-ts-toolkit/utils");
113
+ var normalizeSafeInteger = (value, min) => {
114
+ if ((0, import_utils2.isNil)(value)) return null;
115
+ const parsed = Number(value);
116
+ if (!Number.isSafeInteger(parsed) || parsed < min) return null;
117
+ return parsed;
118
+ };
103
119
  function genPagination({
104
120
  skip,
105
121
  limit,
@@ -107,13 +123,21 @@ function genPagination({
107
123
  pageSize
108
124
  }, hardLimit) {
109
125
  let _skip = 0;
110
- let _limit = Number(limit ?? pageSize);
111
- if ((0, import_utils2.isNaN)(_limit) || _limit > hardLimit) _limit = hardLimit;
112
- if (!(0, import_utils2.isNil)(skip)) {
113
- _skip = Number(skip);
114
- } else if (!(0, import_utils2.isNil)(page)) {
115
- const npage = Number(page);
116
- if (npage > 1) _skip = (npage - 1) * _limit;
126
+ let _limit = normalizeSafeInteger(limit ?? pageSize, 1) ?? hardLimit;
127
+ if (!Number.isSafeInteger(_limit) || _limit > hardLimit) _limit = hardLimit;
128
+ const normalizedSkip = normalizeSafeInteger(skip, 0);
129
+ const normalizedPage = normalizeSafeInteger(page, 0);
130
+ if (normalizedSkip !== null) {
131
+ _skip = normalizedSkip;
132
+ } else if (normalizedPage !== null) {
133
+ const npage = normalizedPage;
134
+ if (npage > 1) {
135
+ const derivedSkip = (npage - 1) * _limit;
136
+ _skip = Number.isSafeInteger(derivedSkip) ? derivedSkip : Number.MAX_SAFE_INTEGER;
137
+ }
138
+ }
139
+ if (!Number.isSafeInteger(_skip) || _skip < 0) {
140
+ return { skip: 0, limit: _limit };
117
141
  }
118
142
  return { skip: _skip, limit: _limit };
119
143
  }
@@ -182,6 +206,7 @@ async function populateDoc(doc, target) {
182
206
  if ((0, import_utils4.isPromise)(p)) return p;
183
207
  return "execPopulate" in p && p.execPopulate?.();
184
208
  }
209
+ var normalizeSubPopulate = (populate) => (0, import_utils4.isArray)(populate) ? populate.map((item) => (0, import_utils4.isString)(item) ? { path: item } : item) : (0, import_utils4.isString)(populate) ? { path: populate } : populate ?? [];
185
210
  var genSubPopulate = (sub, popul) => {
186
211
  if (!popul) return [];
187
212
  let populate = (0, import_utils4.isArray)(popul) ? popul : [popul];
@@ -197,7 +222,7 @@ var genSubPopulate = (sub, popul) => {
197
222
 
198
223
  // src/helpers/errors.ts
199
224
  var import_express_json_router = __toESM(require("@web-ts-toolkit/express-json-router"));
200
- var { BadRequestError, ForbiddenError, NotFoundError, UnprocessableEntityError } = import_express_json_router.default.clientErrors;
225
+ var { BadRequestError, ForbiddenError, NotFoundError, UnauthorizedError, UnprocessableEntityError } = import_express_json_router.default.clientErrors;
201
226
  function mapCodeToMessage(code) {
202
227
  switch (code) {
203
228
  case "success" /* Success */:
@@ -206,6 +231,8 @@ function mapCodeToMessage(code) {
206
231
  return "Created";
207
232
  case "bad_request" /* BadRequest */:
208
233
  return "Bad Request";
234
+ case "unauthorized" /* Unauthorized */:
235
+ return "Unauthorized";
209
236
  case "forbidden" /* Forbidden */:
210
237
  return "Forbidden";
211
238
  case "not_found" /* NotFound */:
@@ -222,6 +249,8 @@ function mapCodeToStatusCode(code) {
222
249
  return 201 /* Created */;
223
250
  case "bad_request" /* BadRequest */:
224
251
  return 400 /* BadRequest */;
252
+ case "unauthorized" /* Unauthorized */:
253
+ return 401 /* Unauthorized */;
225
254
  case "forbidden" /* Forbidden */:
226
255
  return 403 /* Forbidden */;
227
256
  case "not_found" /* NotFound */:
@@ -232,11 +261,14 @@ function mapCodeToStatusCode(code) {
232
261
  }
233
262
  function handleResultError(result) {
234
263
  if (result.success) return;
264
+ if (result.kind !== "error") return;
235
265
  const { code, errors = [] } = result;
236
266
  const errorOptions = { errors };
237
267
  switch (code) {
238
268
  case "bad_request" /* BadRequest */:
239
269
  throw new BadRequestError("Bad Request", errorOptions);
270
+ case "unauthorized" /* Unauthorized */:
271
+ throw new UnauthorizedError("Unauthorized", errorOptions);
240
272
  case "forbidden" /* Forbidden */:
241
273
  throw new ForbiddenError("Forbidden", errorOptions);
242
274
  case "not_found" /* NotFound */:
@@ -563,22 +595,102 @@ function buildOpenApiSpec(routes, options) {
563
595
  };
564
596
  }
565
597
 
598
+ // src/openapi/errors.ts
599
+ var OpenApiCollisionError = class extends Error {
600
+ constructor(message, details) {
601
+ super(message);
602
+ this.name = "OpenApiCollisionError";
603
+ this.collisionKind = details.collisionKind;
604
+ this.method = details.method;
605
+ this.path = details.path;
606
+ this.operationId = details.operationId;
607
+ this.existing = details.existing;
608
+ this.incoming = details.incoming;
609
+ }
610
+ };
611
+
566
612
  // src/openapi/registry.ts
613
+ var isPlainObject7 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
614
+ var stableKey = (value) => {
615
+ if (value === null || typeof value === "undefined") return "null";
616
+ if (typeof value === "function") return "[Function]";
617
+ if (typeof value !== "object") return JSON.stringify(value);
618
+ if (Array.isArray(value)) {
619
+ return `[${value.map(stableKey).join(",")}]`;
620
+ }
621
+ const obj = value;
622
+ return `{${Object.keys(obj).sort().map((k) => `${JSON.stringify(k)}:${stableKey(obj[k])}`).join(",")}}`;
623
+ };
624
+ var descriptorsEqual = (a, b) => {
625
+ 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);
626
+ };
567
627
  var OpenApiRegistry = class {
568
- constructor() {
628
+ constructor(options = {}) {
569
629
  this.routes = [];
630
+ this.options = {
631
+ rejectConflicts: options.rejectConflicts ?? true,
632
+ rejectDuplicateOperationIds: options.rejectDuplicateOperationIds ?? true
633
+ };
570
634
  }
571
635
  register(route) {
572
636
  const existingIndex = this.routes.findIndex((item) => item.method === route.method && item.path === route.path);
573
637
  if (existingIndex === -1) {
638
+ this.assertOperationIdAvailable(route);
574
639
  this.routes.push(route);
575
640
  return;
576
641
  }
642
+ const existing = this.routes[existingIndex];
643
+ if (route.allowReplace) {
644
+ this.assertOperationIdAvailable(route, existing);
645
+ this.routes[existingIndex] = route;
646
+ return;
647
+ }
648
+ if (descriptorsEqual(existing, route) && (route.idempotent || isPlainObject7(existing) && existing.idempotent)) {
649
+ return;
650
+ }
651
+ if (this.options.rejectConflicts) {
652
+ throw new OpenApiCollisionError(
653
+ `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.",
654
+ {
655
+ collisionKind: "path",
656
+ method: route.method,
657
+ path: route.path,
658
+ existing,
659
+ incoming: route
660
+ }
661
+ );
662
+ }
577
663
  this.routes[existingIndex] = route;
578
664
  }
665
+ assertOperationIdAvailable(route, replacing) {
666
+ if (!this.options.rejectDuplicateOperationIds) return;
667
+ if (!route.operationId) return;
668
+ const clash = this.routes.find(
669
+ (item) => item.operationId && item.operationId === route.operationId && item !== replacing
670
+ );
671
+ if (clash) {
672
+ if (route.idempotent && clash.idempotent) return;
673
+ throw new OpenApiCollisionError(
674
+ `OpenAPI operationId collision: "${route.operationId}" is already bound to ${clash.method.toUpperCase()} ${clash.path}. Each operationId must be unique across the spec.`,
675
+ {
676
+ collisionKind: "operationId",
677
+ operationId: route.operationId,
678
+ existing: clash,
679
+ incoming: route
680
+ }
681
+ );
682
+ }
683
+ }
579
684
  getRoutes() {
580
685
  return [...this.routes];
581
686
  }
687
+ clear() {
688
+ this.routes.length = 0;
689
+ }
690
+ setStrictMode(enabled) {
691
+ this.options.rejectConflicts = enabled;
692
+ this.options.rejectDuplicateOperationIds = enabled;
693
+ }
582
694
  getSpec(info) {
583
695
  return buildOpenApiSpec(this.routes, info);
584
696
  }
@@ -600,8 +712,8 @@ var defaultLogger = (0, import_winston.createLogger)({
600
712
  import_winston.format.label({ label: "[WTT]" }),
601
713
  import_winston.format.timestamp(),
602
714
  import_winston.format.printf(({ level, label, message, timestamp, stack, ...meta }) => {
603
- const formattedLabel = colorizer.colorize(level, label);
604
- const formattedLevel = colorizer.colorize(level, `[${level.toUpperCase()}]`);
715
+ const formattedLabel = colorizer.colorize(level, String(label));
716
+ const formattedLevel = colorizer.colorize(level, `[${String(level).toUpperCase()}]`);
605
717
  const formattedMessage = colorizer.colorize(level, inspectValue(stack ?? message));
606
718
  const formattedMeta = Object.keys(meta).length > 0 ? ` ${inspectValue(meta)}` : "";
607
719
  return `${formattedLabel} ${timestamp} ${formattedLevel} ${formattedMessage}${formattedMeta}`;
@@ -636,8 +748,8 @@ var OptionsManager = class {
636
748
  this.currentOptions = new Proxy({}, {
637
749
  set(target, key, value) {
638
750
  const keystr = String(key);
639
- const oldvalue = target[key];
640
- target[key] = value;
751
+ const oldvalue = target[keystr];
752
+ target[keystr] = value;
641
753
  _this.listeners[keystr] && _this.listeners[keystr].call(_this, value, keystr, target, oldvalue);
642
754
  return true;
643
755
  }
@@ -665,8 +777,116 @@ var OptionsManager = class {
665
777
  }
666
778
  };
667
779
 
780
+ // src/request-complexity.ts
781
+ var import_utils10 = require("@web-ts-toolkit/utils");
782
+ var defaultRequestComplexity = {
783
+ maxDepth: 8,
784
+ maxNodes: 500,
785
+ maxLogicalClauses: 50,
786
+ maxInValues: 100,
787
+ maxBulkItems: 100,
788
+ maxIncludeCount: 10,
789
+ maxSubQueryCount: 10,
790
+ maxBulkConcurrency: 10
791
+ };
792
+ var normalizePositiveInteger = (value, fallback) => {
793
+ return Number.isSafeInteger(value) && Number(value) > 0 ? Number(value) : fallback;
794
+ };
795
+ var resolveRequestComplexity = (options) => ({
796
+ maxDepth: normalizePositiveInteger(options?.maxDepth, defaultRequestComplexity.maxDepth),
797
+ maxNodes: normalizePositiveInteger(options?.maxNodes, defaultRequestComplexity.maxNodes),
798
+ maxLogicalClauses: normalizePositiveInteger(options?.maxLogicalClauses, defaultRequestComplexity.maxLogicalClauses),
799
+ maxInValues: normalizePositiveInteger(options?.maxInValues, defaultRequestComplexity.maxInValues),
800
+ maxBulkItems: normalizePositiveInteger(options?.maxBulkItems, defaultRequestComplexity.maxBulkItems),
801
+ maxIncludeCount: normalizePositiveInteger(options?.maxIncludeCount, defaultRequestComplexity.maxIncludeCount),
802
+ maxSubQueryCount: normalizePositiveInteger(options?.maxSubQueryCount, defaultRequestComplexity.maxSubQueryCount),
803
+ maxBulkConcurrency: normalizePositiveInteger(
804
+ options?.maxBulkConcurrency,
805
+ defaultRequestComplexity.maxBulkConcurrency
806
+ )
807
+ });
808
+ function validateRequestComplexity(value, options, scope = "request") {
809
+ const limits = resolveRequestComplexity(options);
810
+ const errors = [];
811
+ let nodes = 0;
812
+ let logicalClauses = 0;
813
+ let includeCount = 0;
814
+ let subQueryCount = 0;
815
+ const pointer = (path) => path.length === 0 ? "#" : `#/${path.join("/")}`;
816
+ const pushError = (detail, path) => {
817
+ if (errors.length === 0) {
818
+ errors.push({ detail, pointer: pointer(path) });
819
+ }
820
+ };
821
+ const visit = (current, path, depth, parentKey) => {
822
+ if (errors.length > 0) return;
823
+ nodes += 1;
824
+ if (nodes > limits.maxNodes) {
825
+ pushError(`Request exceeds maximum node budget of ${limits.maxNodes}`, path);
826
+ return;
827
+ }
828
+ if (depth > limits.maxDepth) {
829
+ pushError(`Request exceeds maximum depth of ${limits.maxDepth}`, path);
830
+ return;
831
+ }
832
+ if ((0, import_utils10.isArray)(current)) {
833
+ if (parentKey === "$and" || parentKey === "$or" || parentKey === "$nor") {
834
+ logicalClauses += current.length;
835
+ if (logicalClauses > limits.maxLogicalClauses) {
836
+ pushError(`Filter exceeds maximum logical clause budget of ${limits.maxLogicalClauses}`, path);
837
+ return;
838
+ }
839
+ }
840
+ if (parentKey === "$in") {
841
+ if (current.length > limits.maxInValues) {
842
+ pushError(`Filter exceeds maximum $in values of ${limits.maxInValues}`, path);
843
+ return;
844
+ }
845
+ }
846
+ for (let index = 0; index < current.length; index++) {
847
+ visit(current[index], path.concat(index), depth + 1, parentKey);
848
+ if (errors.length > 0) return;
849
+ }
850
+ return;
851
+ }
852
+ if (!(0, import_utils10.isPlainObject)(current)) return;
853
+ for (const [key, child] of Object.entries(current)) {
854
+ if (key === "__proto__" || key === "prototype" || key === "constructor") {
855
+ pushError(`Unsupported key: ${key}`, path.concat(key));
856
+ return;
857
+ }
858
+ if (scope === "request" && key === "include") {
859
+ includeCount += (0, import_utils10.isArray)(child) ? child.length : 1;
860
+ if (includeCount > limits.maxIncludeCount) {
861
+ pushError(`Request exceeds maximum include count of ${limits.maxIncludeCount}`, path.concat(key));
862
+ return;
863
+ }
864
+ }
865
+ if (key === "$$sq") {
866
+ subQueryCount += 1;
867
+ if (subQueryCount > limits.maxSubQueryCount) {
868
+ pushError(`Request exceeds maximum subquery count of ${limits.maxSubQueryCount}`, path.concat(key));
869
+ return;
870
+ }
871
+ visit(child, path.concat(key), depth + 1, key);
872
+ if (errors.length > 0) return;
873
+ continue;
874
+ }
875
+ visit(child, path.concat(key), depth + 1, key);
876
+ if (errors.length > 0) return;
877
+ }
878
+ };
879
+ visit(value, [], 0);
880
+ return errors;
881
+ }
882
+
668
883
  // src/runtime.ts
669
- (0, import_mongoose_schema_jsonschema.default)(import_mongoose3.default);
884
+ var mongooseJsonSchemaInitialized = false;
885
+ var ensureMongooseJsonSchemaInitialized = () => {
886
+ if (mongooseJsonSchemaInitialized) return;
887
+ (0, import_mongoose_schema_jsonschema.default)(import_mongoose3.default);
888
+ mongooseJsonSchemaInitialized = true;
889
+ };
670
890
  var pluralize = import_mongoose3.default.pluralize();
671
891
  var FIELD_ACCESS_KEYS = ["list", "create", "read", "update"];
672
892
  var defaultModelOptions = {
@@ -675,47 +895,48 @@ var defaultModelOptions = {
675
895
  };
676
896
  var defaultDataOptions = {
677
897
  basePath: null,
898
+ parentPath: "/",
678
899
  queryRouteSegment: "__query"
679
900
  };
680
901
  var normalizeBasePath = (name, value) => {
681
- if ((0, import_utils10.isNil)(value)) {
902
+ if ((0, import_utils11.isNil)(value)) {
682
903
  return `/${pluralize(name)}`;
683
904
  }
684
- return (0, import_utils10.isString)(value) ? (0, import_utils10.addLeadingSlash)(value) : "";
905
+ return (0, import_utils11.isString)(value) ? (0, import_utils11.addLeadingSlash)(value) : "";
685
906
  };
686
907
  var hasModelPermission = (value, modelPermissionPrefix) => {
687
908
  if (!modelPermissionPrefix) {
688
909
  return true;
689
910
  }
690
- if ((0, import_utils10.isString)(value)) {
911
+ if ((0, import_utils11.isString)(value)) {
691
912
  return value.trim().split(" ").some((item) => item.startsWith(modelPermissionPrefix));
692
913
  }
693
- if ((0, import_utils10.isArray)(value)) {
914
+ if ((0, import_utils11.isArray)(value)) {
694
915
  return value.some((item) => {
695
- if ((0, import_utils10.isString)(item) || (0, import_utils10.isArray)(item)) {
916
+ if ((0, import_utils11.isString)(item) || (0, import_utils11.isArray)(item)) {
696
917
  return hasModelPermission(item, modelPermissionPrefix);
697
918
  }
698
919
  return true;
699
920
  });
700
921
  }
701
- return (0, import_utils10.isFunction)(value);
922
+ return (0, import_utils11.isFunction)(value);
702
923
  };
703
924
  var classifyPermissionSchema = (permissionSchema, modelPermissionPrefix) => {
704
925
  const schemaKeys = Object.keys(permissionSchema);
705
926
  const globalPermissionKeys = {};
706
927
  const modelPermissionKeys = {};
707
- (0, import_utils10.forEach)(schemaKeys, (schemaKey) => {
928
+ (0, import_utils11.forEach)(schemaKeys, (schemaKey) => {
708
929
  const schemaRule = permissionSchema[schemaKey];
709
- const ruleEntries = (0, import_utils10.isPlainObject)(schemaRule) ? Object.entries(schemaRule) : FIELD_ACCESS_KEYS.map((accessKey) => [accessKey, schemaRule]);
930
+ const ruleEntries = (0, import_utils11.isPlainObject)(schemaRule) ? Object.entries(schemaRule) : FIELD_ACCESS_KEYS.map((accessKey) => [accessKey, schemaRule]);
710
931
  for (let x = 0; x < ruleEntries.length; x++) {
711
932
  const [accessKey, value] = ruleEntries[x];
712
- if (!(0, import_utils10.isArray)(globalPermissionKeys[accessKey])) {
933
+ if (!(0, import_utils11.isArray)(globalPermissionKeys[accessKey])) {
713
934
  globalPermissionKeys[accessKey] = [];
714
935
  }
715
- if (!(0, import_utils10.isArray)(modelPermissionKeys[accessKey])) {
936
+ if (!(0, import_utils11.isArray)(modelPermissionKeys[accessKey])) {
716
937
  modelPermissionKeys[accessKey] = [];
717
938
  }
718
- if ((0, import_utils10.isBoolean)(value)) {
939
+ if ((0, import_utils11.isBoolean)(value)) {
719
940
  globalPermissionKeys[accessKey].push(schemaKey);
720
941
  return;
721
942
  }
@@ -737,7 +958,8 @@ var AccessRuntime = class {
737
958
  this.globalOptions = new OptionsManager({
738
959
  requestPermissionField: "_permissions",
739
960
  globalPermissions: () => ({}),
740
- logger: defaultLogger
961
+ logger: defaultLogger,
962
+ requestComplexity: defaultRequestComplexity
741
963
  }).build();
742
964
  this.defaultModelOptions = new OptionsManager({
743
965
  listHardLimit: 1e3,
@@ -756,14 +978,50 @@ var AccessRuntime = class {
756
978
  this.modelRefs = {};
757
979
  this.modelSubs = {};
758
980
  this.modelAtts = {};
981
+ this.modelInstances = {};
759
982
  this.openApiRegistry = new OpenApiRegistry();
983
+ ensureMongooseJsonSchemaInitialized();
984
+ }
985
+ registerModelInstance(modelName, model) {
986
+ if (!modelName || typeof modelName !== "string") {
987
+ throw new TypeError(`registerModelInstance: modelName must be a non-empty string, received ${typeof modelName}`);
988
+ }
989
+ const existing = this.modelInstances[modelName];
990
+ if (existing && existing !== model) {
991
+ throw new Error(
992
+ `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.`
993
+ );
994
+ }
995
+ this.modelInstances[modelName] = model;
996
+ }
997
+ hasModelInstance(modelName) {
998
+ return modelName in this.modelInstances;
999
+ }
1000
+ getModelInstance(modelName) {
1001
+ const registered = this.modelInstances[modelName];
1002
+ if (registered) return registered;
1003
+ const global = import_mongoose3.default.models[modelName];
1004
+ return global ?? null;
760
1005
  }
761
1006
  registerOpenApiRoute(route) {
762
1007
  this.openApiRegistry.register(route);
763
1008
  }
1009
+ /**
1010
+ * Enable strict OpenAPI collision detection for the remainder of this
1011
+ * runtime's lifetime. After this call, conflicting method/path
1012
+ * registrations and duplicate operationIds throw {@link OpenApiCollisionError}
1013
+ * instead of silently replacing. Existing already-registered routes are
1014
+ * not retroactively re-validated.
1015
+ */
1016
+ enableOpenApiCollisionDetection() {
1017
+ this.openApiRegistry.setStrictMode(true);
1018
+ }
764
1019
  getOpenApiRoutes() {
765
1020
  return this.openApiRegistry.getRoutes();
766
1021
  }
1022
+ clearOpenApiRoutes() {
1023
+ this.openApiRegistry.clear();
1024
+ }
767
1025
  getOpenApiSpec(info) {
768
1026
  return this.openApiRegistry.getSpec(info);
769
1027
  }
@@ -792,7 +1050,7 @@ var AccessRuntime = class {
792
1050
  return this.defaultModelOptions.get(key, defaultValue);
793
1051
  }
794
1052
  createModelOptions(modelName) {
795
- const model = import_mongoose3.default.model(modelName);
1053
+ const model = this.getModelInstance(modelName) ?? import_mongoose3.default.model(modelName);
796
1054
  const manager = new OptionsManager({
797
1055
  ...defaultModelOptions,
798
1056
  modelName
@@ -809,7 +1067,7 @@ var AccessRuntime = class {
809
1067
  }).onchange("basePath", function(newval, key, target) {
810
1068
  target[key] = normalizeBasePath(
811
1069
  modelName,
812
- (0, import_utils10.isString)(newval) || (0, import_utils10.isNil)(newval) ? newval : void 0
1070
+ (0, import_utils11.isString)(newval) || (0, import_utils11.isNil)(newval) ? newval : void 0
813
1071
  );
814
1072
  }).build();
815
1073
  this.modelJsonSchemas[modelName] = model.jsonSchema();
@@ -867,7 +1125,7 @@ var AccessRuntime = class {
867
1125
  manager.onchange("basePath", function(newval, key, target) {
868
1126
  target[key] = normalizeBasePath(
869
1127
  dataName,
870
- (0, import_utils10.isString)(newval) || (0, import_utils10.isNil)(newval) ? newval : void 0
1128
+ (0, import_utils11.isString)(newval) || (0, import_utils11.isNil)(newval) ? newval : void 0
871
1129
  );
872
1130
  }).build();
873
1131
  return manager;
@@ -908,7 +1166,7 @@ var AccessRuntime = class {
908
1166
  if (modelName in this.modelRefs && modelName in this.modelSubs && modelName in this.modelAtts) {
909
1167
  return;
910
1168
  }
911
- const model = import_mongoose3.default.models[modelName];
1169
+ const model = this.getModelInstance(modelName);
912
1170
  if (!model) {
913
1171
  this.modelRefs[modelName] = this.modelRefs[modelName] ?? {};
914
1172
  this.modelSubs[modelName] = this.modelSubs[modelName] ?? [];
@@ -918,28 +1176,26 @@ var AccessRuntime = class {
918
1176
  const schema = model.schema;
919
1177
  this.modelRefs[modelName] = buildRefs(schema.tree);
920
1178
  this.modelSubs[modelName] = buildSubPaths(schema.tree);
921
- this.modelAtts[modelName] = (0, import_utils10.keys)(schema.obj);
1179
+ this.modelAtts[modelName] = (0, import_utils11.keys)(schema.obj);
922
1180
  }
923
1181
  getModelRef(modelName, refPath) {
924
1182
  this.ensureModelMeta(modelName);
925
- const value = (0, import_utils10.get)(this.modelRefs, `${modelName}.${refPath}`, null);
926
- return (0, import_utils10.isString)(value) ? value : null;
1183
+ const value = (0, import_utils11.get)(this.modelRefs, `${modelName}.${refPath}`, null);
1184
+ return (0, import_utils11.isString)(value) ? value : null;
927
1185
  }
928
1186
  getModelSub(modelName) {
929
1187
  this.ensureModelMeta(modelName);
930
- return (0, import_utils10.get)(this.modelSubs, modelName, []);
1188
+ return (0, import_utils11.get)(this.modelSubs, modelName, []);
931
1189
  }
932
1190
  getModelAtt(modelName) {
933
1191
  this.ensureModelMeta(modelName);
934
- return (0, import_utils10.get)(this.modelAtts, modelName, []);
1192
+ return (0, import_utils11.get)(this.modelAtts, modelName, []);
935
1193
  }
936
1194
  };
937
1195
  var defaultRuntime = new AccessRuntime();
938
1196
 
939
1197
  // src/runtime-context.ts
940
1198
  var import_node_async_hooks = require("async_hooks");
941
- var import_mongoose4 = __toESM(require("mongoose"));
942
- var ACCESS_RUNTIME = /* @__PURE__ */ Symbol("access-runtime");
943
1199
  var runtimeStorage = new import_node_async_hooks.AsyncLocalStorage();
944
1200
  function runWithRuntime(runtime, callback) {
945
1201
  return runtimeStorage.run(runtime, callback);
@@ -947,16 +1203,6 @@ function runWithRuntime(runtime, callback) {
947
1203
  function getActiveRuntime() {
948
1204
  return runtimeStorage.getStore() ?? null;
949
1205
  }
950
- function attachRuntimeToModel(modelName, runtime) {
951
- const model = import_mongoose4.default.models[modelName];
952
- if (model) {
953
- model[ACCESS_RUNTIME] = runtime;
954
- }
955
- }
956
- function getRuntimeForModelName(modelName) {
957
- const model = import_mongoose4.default.models[modelName];
958
- return model?.[ACCESS_RUNTIME] ?? null;
959
- }
960
1206
 
961
1207
  // src/options/global-options.ts
962
1208
  var getRuntime = () => getActiveRuntime() ?? defaultRuntime;
@@ -974,27 +1220,36 @@ var getGlobalOption = (key, defaultValue) => {
974
1220
  };
975
1221
 
976
1222
  // src/options/model-options.ts
977
- var getRuntime2 = (modelName) => getRuntimeForModelName(modelName) ?? getActiveRuntime() ?? defaultRuntime;
1223
+ var getRuntime2 = () => getActiveRuntime() ?? defaultRuntime;
978
1224
  var setModelOptions = (modelName, options) => {
979
- getRuntime2(modelName).setModelOptions(modelName, options);
1225
+ getRuntime2().setModelOptions(modelName, options);
980
1226
  };
981
1227
  var setModelOption = (modelName, key, value) => {
982
- getRuntime2(modelName).setModelOption(modelName, key, value);
1228
+ getRuntime2().setModelOption(modelName, key, value);
983
1229
  };
984
1230
  var getModelOptions = (modelName) => {
985
- return getRuntime2(modelName).getModelOptions(modelName);
1231
+ return getRuntime2().getModelOptions(modelName);
986
1232
  };
987
1233
  var getModelOption = (modelName, key, defaultValue) => {
988
- return getRuntime2(modelName).getModelOption(modelName, key, defaultValue);
1234
+ return getRuntime2().getModelOption(modelName, key, defaultValue);
989
1235
  };
990
1236
  var getExactModelOption = (modelName, key) => {
991
- return getRuntime2(modelName).getExactModelOption(modelName, key);
1237
+ return getRuntime2().getExactModelOption(modelName, key);
992
1238
  };
993
1239
  var getModelNames = () => {
994
1240
  return (getActiveRuntime() ?? defaultRuntime).getModelNames();
995
1241
  };
996
1242
  var getModelJsonSchema = (modelName) => {
997
- return getRuntime2(modelName).getModelJsonSchema(modelName);
1243
+ return getRuntime2().getModelJsonSchema(modelName);
1244
+ };
1245
+ var registerModelInstance = (modelName, model) => {
1246
+ getRuntime2().registerModelInstance(modelName, model);
1247
+ };
1248
+ var hasModelInstance = (modelName) => {
1249
+ return getRuntime2().hasModelInstance(modelName);
1250
+ };
1251
+ var getModelInstance = (modelName) => {
1252
+ return getRuntime2().getModelInstance(modelName);
998
1253
  };
999
1254
 
1000
1255
  // src/options/default-model-options.ts
@@ -1022,25 +1277,35 @@ var getDataOption = (dataName, key, defaultValue) => {
1022
1277
  };
1023
1278
 
1024
1279
  // src/meta.ts
1025
- var getRuntime5 = (modelName) => getRuntimeForModelName(modelName) ?? getActiveRuntime() ?? defaultRuntime;
1280
+ var getRuntime5 = () => getActiveRuntime() ?? defaultRuntime;
1026
1281
  var getModelRef = (modelName, refPath) => {
1027
- return getRuntime5(modelName).getModelRef(modelName, refPath);
1282
+ return getRuntime5().getModelRef(modelName, refPath);
1028
1283
  };
1029
1284
  var getModelSub = (modelName) => {
1030
- return getRuntime5(modelName).getModelSub(modelName);
1285
+ return getRuntime5().getModelSub(modelName);
1031
1286
  };
1032
1287
 
1033
1288
  // src/services/base.ts
1034
- var import_utils11 = require("@web-ts-toolkit/utils");
1289
+ var import_utils12 = require("@web-ts-toolkit/utils");
1290
+ var ClientRequestError = class extends Error {
1291
+ constructor(result) {
1292
+ super(String(result.code));
1293
+ this.result = result;
1294
+ }
1295
+ };
1035
1296
  function validateClientFilter(filter2) {
1036
1297
  const errors = [];
1298
+ const complexityErrors = validateRequestComplexity(filter2, getGlobalOption("requestComplexity"), "filter");
1299
+ if (complexityErrors.length > 0) {
1300
+ return complexityErrors.map((error2) => error2.detail);
1301
+ }
1037
1302
  const blockedOperators = /* @__PURE__ */ new Set(["$where", "$expr", "$function", "$accumulator"]);
1038
1303
  const visit = (value, path) => {
1039
- if ((0, import_utils11.isArray)(value)) {
1304
+ if ((0, import_utils12.isArray)(value)) {
1040
1305
  value.forEach((item, index) => visit(item, `${path}[${index}]`));
1041
1306
  return;
1042
1307
  }
1043
- if (!(0, import_utils11.isPlainObject)(value)) return;
1308
+ if (!(0, import_utils12.isPlainObject)(value)) return;
1044
1309
  Object.entries(value).forEach(([key, child]) => {
1045
1310
  const nextPath = path ? `${path}.${key}` : key;
1046
1311
  if (blockedOperators.has(key)) {
@@ -1134,18 +1399,43 @@ var Base = class {
1134
1399
  validateClientFilter(filter2) {
1135
1400
  return validateClientFilter(filter2);
1136
1401
  }
1402
+ getRequestComplexity() {
1403
+ return resolveRequestComplexity(getGlobalOption("requestComplexity"));
1404
+ }
1405
+ getClientRequestErrorResult(error2) {
1406
+ return error2 instanceof ClientRequestError ? error2.result : null;
1407
+ }
1408
+ throwClientRequestError(code, detail) {
1409
+ throw new ClientRequestError({
1410
+ success: false,
1411
+ kind: "error",
1412
+ code,
1413
+ errors: [{ detail }]
1414
+ });
1415
+ }
1416
+ async getAuthorizedTargetService(modelName, op) {
1417
+ const runtime = getActiveRuntime();
1418
+ if (runtime && !runtime.hasModel(modelName)) {
1419
+ this.throwClientRequestError("bad_request" /* BadRequest */, `Model ${modelName} not found`);
1420
+ }
1421
+ const allowed = await this.req.macl.isAllowed(modelName, op);
1422
+ if (!allowed) {
1423
+ this.throwClientRequestError("unauthorized" /* Unauthorized */, "Unauthorized");
1424
+ }
1425
+ return this.req.macl.getPublicService(modelName);
1426
+ }
1137
1427
  processInclude(include) {
1138
- const includes = (0, import_utils11.compact)((0, import_utils11.castArray)(include)).filter(({ model, op, path, localField, foreignField }) => {
1428
+ const includes = (0, import_utils12.compact)((0, import_utils12.castArray)(include)).filter(({ model, op, path, localField, foreignField }) => {
1139
1429
  return model && op && path && localField && foreignField;
1140
1430
  });
1141
1431
  let includeLocalFields = [];
1142
1432
  let includePaths = [];
1143
- (0, import_utils11.forEach)(includes, (inc) => {
1433
+ (0, import_utils12.forEach)(includes, (inc) => {
1144
1434
  includeLocalFields.push(inc.localField);
1145
1435
  includePaths.push(inc.path);
1146
1436
  });
1147
- includeLocalFields = (0, import_utils11.uniq)((0, import_utils11.compact)(includeLocalFields));
1148
- includePaths = (0, import_utils11.uniq)((0, import_utils11.compact)(includePaths));
1437
+ includeLocalFields = (0, import_utils12.uniq)((0, import_utils12.compact)(includeLocalFields));
1438
+ includePaths = (0, import_utils12.uniq)((0, import_utils12.compact)(includePaths));
1149
1439
  return {
1150
1440
  includes,
1151
1441
  includeLocalFields,
@@ -1154,72 +1444,80 @@ var Base = class {
1154
1444
  }
1155
1445
  async includeDocs(docs, include) {
1156
1446
  if (!include) return docs;
1157
- const includes = (0, import_utils11.compact)((0, import_utils11.castArray)(include));
1447
+ const includes = (0, import_utils12.compact)((0, import_utils12.castArray)(include));
1158
1448
  if (includes.length === 0) return docs;
1159
- const isSingle = !(0, import_utils11.isArray)(docs);
1160
- if (isSingle) docs = [docs];
1449
+ const isSingle = !(0, import_utils12.isArray)(docs);
1450
+ let docList = isSingle ? [docs] : docs;
1161
1451
  for (let x = 0; x < includes.length; x++) {
1162
1452
  const include2 = includes[x];
1163
1453
  if (include2.op === "count") {
1164
- docs = await this.includeDocsCount(docs, include2);
1454
+ docList = await this.includeDocsCount(docList, include2);
1165
1455
  } else {
1166
- docs = await this.includeDocsList(docs, include2);
1456
+ docList = await this.includeDocsList(docList, include2);
1167
1457
  }
1168
1458
  }
1169
- return isSingle ? docs[0] : docs;
1459
+ return isSingle ? docList[0] : docList;
1170
1460
  }
1171
1461
  async includeDocsList(docs, include) {
1172
1462
  const { model, op, path, localField, foreignField, filter: _filters, args = {}, options = {} } = include;
1173
- const svc = this.req.macl.getPublicService(model);
1174
- if (!svc) return docs;
1463
+ const svc = await this.getAuthorizedTargetService(model, op);
1175
1464
  const includeLocalValues = [];
1176
- (0, import_utils11.forEach)(docs, (doc, i) => {
1177
- includeLocalValues.push((0, import_utils11.get)(doc, localField));
1465
+ (0, import_utils12.forEach)(docs, (doc, i) => {
1466
+ includeLocalValues.push((0, import_utils12.get)(doc, localField));
1178
1467
  });
1179
- const filter2 = { ..._filters ?? {}, [foreignField]: { $in: (0, import_utils11.flatten)(includeLocalValues) } };
1180
- const result = await svc.find(filter2, args, {
1468
+ const filter2 = { ..._filters ?? {}, [foreignField]: { $in: (0, import_utils12.flatten)(includeLocalValues) } };
1469
+ const authorizedFilter = await svc.genFilter(op, filter2);
1470
+ const trustedArgs = {
1471
+ ...args,
1472
+ overrides: {
1473
+ filter: authorizedFilter
1474
+ }
1475
+ };
1476
+ const trustedOptions = {
1181
1477
  ...options,
1182
1478
  lean: true,
1183
1479
  includePermissions: false,
1184
1480
  includeCount: false
1185
- });
1186
- if (!result.success) return docs;
1481
+ };
1482
+ const trustedResult = await svc.find(filter2, trustedArgs, trustedOptions);
1483
+ if (!trustedResult.success) return docs;
1187
1484
  for (let y = 0; y < docs.length; y++) {
1188
1485
  const doc = docs[y];
1189
- const localValue = (0, import_utils11.get)(doc, localField);
1190
- const filterFn = (row) => (0, import_utils11.intersectionBy)((0, import_utils11.castArray)(localValue), (0, import_utils11.castArray)((0, import_utils11.get)(row, foreignField)), String).length > 0;
1191
- const matches = result.data.filter(filterFn);
1486
+ const localValue = (0, import_utils12.get)(doc, localField);
1487
+ const filterFn = (row) => (0, import_utils12.intersectionBy)((0, import_utils12.castArray)(localValue), (0, import_utils12.castArray)((0, import_utils12.get)(row, foreignField)), String).length > 0;
1488
+ const matches = trustedResult.data.filter(filterFn);
1192
1489
  setDocValue(doc, path, op === "list" ? matches : matches[0]);
1193
1490
  }
1194
1491
  return docs;
1195
1492
  }
1196
1493
  async includeDocsCount(docs, include) {
1197
1494
  const { model, path, localField, foreignField, filter: _filters, args = {}, options = {} } = include;
1198
- const svc = this.req.macl.getPublicService(model);
1199
- if (!svc) return docs;
1495
+ const svc = await this.getAuthorizedTargetService(model, "count");
1200
1496
  const includeLocalValues = [];
1201
- (0, import_utils11.forEach)(docs, (doc) => {
1202
- includeLocalValues.push((0, import_utils11.get)(doc, localField));
1497
+ (0, import_utils12.forEach)(docs, (doc) => {
1498
+ includeLocalValues.push((0, import_utils12.get)(doc, localField));
1203
1499
  });
1204
- const filter2 = { ..._filters ?? {}, [foreignField]: { $in: (0, import_utils11.flatten)(includeLocalValues) } };
1205
- const result = await svc.find(
1206
- filter2,
1207
- {
1208
- ...args,
1209
- select: [foreignField]
1210
- },
1211
- {
1212
- ...options,
1213
- lean: true,
1214
- includePermissions: false,
1215
- includeCount: false
1500
+ const filter2 = { ..._filters ?? {}, [foreignField]: { $in: (0, import_utils12.flatten)(includeLocalValues) } };
1501
+ const authorizedFilter = await svc.genFilter("list", filter2);
1502
+ const trustedArgs = {
1503
+ ...args,
1504
+ select: [foreignField],
1505
+ overrides: {
1506
+ filter: authorizedFilter
1216
1507
  }
1217
- );
1508
+ };
1509
+ const trustedOptions = {
1510
+ ...options,
1511
+ lean: true,
1512
+ includePermissions: false,
1513
+ includeCount: false
1514
+ };
1515
+ const result = await svc.find(filter2, trustedArgs, trustedOptions);
1218
1516
  if (!result.success) return docs;
1219
1517
  for (let y = 0; y < docs.length; y++) {
1220
1518
  const doc = docs[y];
1221
- const localValue = (0, import_utils11.get)(doc, localField);
1222
- const filterFn = (row) => (0, import_utils11.intersectionBy)((0, import_utils11.castArray)(localValue), (0, import_utils11.castArray)((0, import_utils11.get)(row, foreignField)), String).length > 0;
1519
+ const localValue = (0, import_utils12.get)(doc, localField);
1520
+ const filterFn = (row) => (0, import_utils12.intersectionBy)((0, import_utils12.castArray)(localValue), (0, import_utils12.castArray)((0, import_utils12.get)(row, foreignField)), String).length > 0;
1223
1521
  setDocValue(doc, path, result.data.filter(filterFn).length);
1224
1522
  }
1225
1523
  return docs;
@@ -1239,29 +1537,32 @@ var Base = class {
1239
1537
  }
1240
1538
  async handleSubQuery(sq, key) {
1241
1539
  const { model, op, id, filter: filter2, args, options, sqOptions = {} } = sq;
1242
- const svc = this.req.macl.getPublicService(model);
1243
- if (!svc) return null;
1244
1540
  let result;
1245
1541
  if (op === "list") {
1246
- result = await svc.find(filter2, args, options);
1542
+ const svc = await this.getAuthorizedTargetService(model, "list");
1543
+ result = await svc._list(filter2, args, options);
1247
1544
  } else if (op === "read") {
1545
+ const svc = await this.getAuthorizedTargetService(model, "read");
1546
+ const readOptions = { ...options, tryList: false };
1248
1547
  if (id) {
1249
- result = await svc.findById(id, args, options);
1548
+ result = await svc._read(id, args, readOptions);
1250
1549
  } else if (filter2) {
1251
- result = await svc.findOne(filter2, args, options);
1550
+ result = await svc._readFilter(filter2, args, readOptions);
1252
1551
  } else {
1253
- return null;
1552
+ this.throwClientRequestError("bad_request" /* BadRequest */, `Subquery for field ${key} requires an id or filter`);
1254
1553
  }
1255
1554
  } else {
1256
- return null;
1555
+ this.throwClientRequestError("bad_request" /* BadRequest */, `Unsupported subquery operation: ${op}`);
1556
+ }
1557
+ if (!result.success) {
1558
+ throw new ClientRequestError(result);
1257
1559
  }
1258
- if (!result.success) return null;
1259
1560
  let ret = result.data;
1260
1561
  if (sqOptions.path) {
1261
- ret = (0, import_utils11.isArray)(ret) ? (0, import_utils11.flatten)(ret.map((v) => (0, import_utils11.get)(v, sqOptions.path))) : (0, import_utils11.get)(ret, sqOptions.path);
1562
+ ret = (0, import_utils12.isArray)(ret) ? (0, import_utils12.flatten)(ret.map((v) => (0, import_utils12.get)(v, sqOptions.path))) : (0, import_utils12.get)(ret, sqOptions.path);
1262
1563
  }
1263
1564
  if (sqOptions.compact) {
1264
- ret = (0, import_utils11.compact)((0, import_utils11.castArray)(ret));
1565
+ ret = (0, import_utils12.compact)((0, import_utils12.castArray)(ret));
1265
1566
  }
1266
1567
  return ret;
1267
1568
  }
@@ -1273,34 +1574,203 @@ var Base = class {
1273
1574
  };
1274
1575
 
1275
1576
  // src/services/service.ts
1276
- var import_utils13 = require("@web-ts-toolkit/utils");
1577
+ var import_utils14 = require("@web-ts-toolkit/utils");
1277
1578
  var import_just_diff = require("just-diff");
1278
1579
 
1279
1580
  // src/model.ts
1280
- var import_mongoose5 = __toESM(require("mongoose"));
1581
+ var import_mongoose4 = __toESM(require("mongoose"));
1281
1582
 
1282
- // src/logger.ts
1283
- var passthrough = (level) => (...args) => {
1284
- const globalLogger = getGlobalOption("logger");
1285
- const target = globalLogger?.[level] ?? defaultLogger[level];
1286
- return target.apply(globalLogger ?? defaultLogger, args);
1583
+ // src/logger-helpers.ts
1584
+ var SENSITIVE_FILTER_KEYS = /* @__PURE__ */ new Set([
1585
+ "password",
1586
+ "pwd",
1587
+ "secret",
1588
+ "token",
1589
+ "access_token",
1590
+ "refreshtoken",
1591
+ "api_key",
1592
+ "apikey",
1593
+ "authorization",
1594
+ "authtoken",
1595
+ "credentials",
1596
+ "privatekey",
1597
+ "ssn",
1598
+ "creditcard",
1599
+ "cardnumber",
1600
+ "cvv",
1601
+ "tenantid",
1602
+ "tenant"
1603
+ ]);
1604
+ var SENSITIVE_PAYLOAD_KEYS = new Set(SENSITIVE_FILTER_KEYS);
1605
+ var safeReplace = (value, seen) => {
1606
+ if (value === null || typeof value === "undefined") return value;
1607
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
1608
+ return value;
1609
+ }
1610
+ if (typeof value === "function") return "[Function]";
1611
+ if (typeof value === "object") {
1612
+ if (seen.has(value)) return "[Circular]";
1613
+ seen.add(value);
1614
+ try {
1615
+ if (Array.isArray(value)) {
1616
+ return value.map((entry) => safeReplace(entry, seen));
1617
+ }
1618
+ const out = {};
1619
+ for (const [k, v] of Object.entries(value)) {
1620
+ out[k] = safeReplace(v, seen);
1621
+ }
1622
+ return out;
1623
+ } finally {
1624
+ seen.delete(value);
1625
+ }
1626
+ }
1627
+ return value;
1628
+ };
1629
+ var redactObjectKeys = (value, sensitiveKeys, seen) => {
1630
+ if (value === null || typeof value !== "object") return value;
1631
+ if (seen.has(value)) return "[Circular]";
1632
+ seen.add(value);
1633
+ try {
1634
+ if (Array.isArray(value)) {
1635
+ return value.map((entry) => redactObjectKeys(entry, sensitiveKeys, seen));
1636
+ }
1637
+ const out = {};
1638
+ for (const [k, v] of Object.entries(value)) {
1639
+ const lowered = String(k).toLowerCase();
1640
+ if (sensitiveKeys.has(lowered)) {
1641
+ out[k] = "[REDACTED]";
1642
+ } else {
1643
+ out[k] = redactObjectKeys(v, sensitiveKeys, seen);
1644
+ }
1645
+ }
1646
+ return out;
1647
+ } finally {
1648
+ seen.delete(value);
1649
+ }
1650
+ };
1651
+ var redactFilter = (filter2) => {
1652
+ return redactObjectKeys(filter2, SENSITIVE_FILTER_KEYS, /* @__PURE__ */ new WeakSet());
1653
+ };
1654
+ var redactPayload = (payload) => {
1655
+ return redactObjectKeys(payload, SENSITIVE_PAYLOAD_KEYS, /* @__PURE__ */ new WeakSet());
1656
+ };
1657
+ var safeStringify = (value) => {
1658
+ try {
1659
+ return JSON.stringify(safeReplace(value, /* @__PURE__ */ new WeakSet()));
1660
+ } catch {
1661
+ return "[Unserializable]";
1662
+ }
1663
+ };
1664
+ var countFilterKeys = (filter2) => {
1665
+ if (filter2 === null || typeof filter2 === "undefined") return 0;
1666
+ if (typeof filter2 !== "object") return 0;
1667
+ if (Array.isArray(filter2)) return 0;
1668
+ let count = 0;
1669
+ const stack = [filter2];
1670
+ const seen = /* @__PURE__ */ new WeakSet();
1671
+ while (stack.length > 0) {
1672
+ const current = stack.pop();
1673
+ if (current === null || typeof current !== "object" || Array.isArray(current)) continue;
1674
+ if (seen.has(current)) continue;
1675
+ seen.add(current);
1676
+ for (const [key, value] of Object.entries(current)) {
1677
+ count += 1;
1678
+ if (typeof key === "string" && key.startsWith("$") && value !== null && typeof value === "object") {
1679
+ if (Array.isArray(value)) {
1680
+ for (const entry of value) {
1681
+ if (entry !== null && typeof entry === "object" && !Array.isArray(entry)) {
1682
+ stack.push(entry);
1683
+ }
1684
+ }
1685
+ } else {
1686
+ stack.push(value);
1687
+ }
1688
+ }
1689
+ }
1690
+ }
1691
+ return count;
1692
+ };
1693
+ var summarizeFilter = (filter2) => {
1694
+ return { filterKeyValueCount: countFilterKeys(filter2) };
1287
1695
  };
1288
- var logger = {
1289
- debug: passthrough("debug"),
1290
- info: passthrough("info"),
1291
- warn: passthrough("warn"),
1292
- error: passthrough("error")
1696
+ var resolveLogger = () => {
1697
+ return getGlobalOption("logger") ?? defaultLogger;
1698
+ };
1699
+ var isLevelEnabled = (level) => {
1700
+ const target = resolveLogger();
1701
+ const fn = target?.isLevelEnabled;
1702
+ if (typeof fn === "function") {
1703
+ try {
1704
+ return !!fn.call(target, level);
1705
+ } catch {
1706
+ return true;
1707
+ }
1708
+ }
1709
+ return true;
1710
+ };
1711
+ var buildOpMessage = (ctx) => {
1712
+ const trimmed = { op: ctx.op };
1713
+ if (ctx.modelName) trimmed.modelName = ctx.modelName;
1714
+ if (ctx.dataName) trimmed.dataName = ctx.dataName;
1715
+ if (ctx.page != null) trimmed.page = ctx.page;
1716
+ if (ctx.pageSize != null) trimmed.pageSize = ctx.pageSize;
1717
+ if (ctx.skip != null) trimmed.skip = ctx.skip;
1718
+ if (ctx.limit != null) trimmed.limit = ctx.limit;
1719
+ if (ctx.sort != null) trimmed.sort = ctx.sort;
1720
+ if (ctx.populateCount != null) trimmed.populateCount = ctx.populateCount;
1721
+ if (ctx.selectCount != null) trimmed.selectCount = ctx.selectCount;
1722
+ if (ctx.filterKeyValueCount != null) trimmed.filterKeyValueCount = ctx.filterKeyValueCount;
1723
+ if (ctx.startedAt != null && ctx.durationMs == null) {
1724
+ trimmed.startedAt = ctx.startedAt;
1725
+ }
1726
+ if (ctx.durationMs != null) trimmed.durationMs = ctx.durationMs;
1727
+ if (ctx.resultCode != null) trimmed.resultCode = ctx.resultCode;
1728
+ return safeStringify(trimmed);
1729
+ };
1730
+ var safeApply = (fn, thisArg, args) => {
1731
+ try {
1732
+ fn.apply(thisArg, args);
1733
+ } catch {
1734
+ }
1735
+ };
1736
+ var debug = (ctx) => {
1737
+ if (!isLevelEnabled("debug")) return;
1738
+ const target = resolveLogger();
1739
+ const fn = target?.debug ?? defaultLogger.debug;
1740
+ const message = buildOpMessage(ctx);
1741
+ safeApply(fn, target ?? defaultLogger, [message]);
1742
+ };
1743
+ var warn = (message, meta) => {
1744
+ const target = resolveLogger();
1745
+ const fn = target?.warn ?? defaultLogger.warn;
1746
+ safeApply(fn, target ?? defaultLogger, meta != null ? [message, meta] : [message]);
1747
+ };
1748
+ var logInfoMessage = (message, ...args) => {
1749
+ const target = resolveLogger();
1750
+ const fn = target?.info ?? defaultLogger.info;
1751
+ const payload = args.length > 0 ? [message, ...args] : [message];
1752
+ safeApply(fn, target ?? defaultLogger, payload);
1753
+ };
1754
+ var error = (error2) => {
1755
+ const target = resolveLogger();
1756
+ const fn = target?.error ?? defaultLogger.error;
1757
+ safeApply(fn, target ?? defaultLogger, [error2]);
1293
1758
  };
1294
1759
 
1295
1760
  // src/model.ts
1761
+ var safeSortLogError = (msg, ...args) => {
1762
+ const message = args.length > 0 ? `${msg} ${safeStringify(args.length === 1 ? args[0] : args)}` : msg;
1763
+ error(message);
1764
+ };
1296
1765
  var Model = class {
1297
- constructor(modelName) {
1766
+ constructor(modelName, runtime) {
1298
1767
  this.modelName = modelName;
1299
- this.model = import_mongoose5.default.model(modelName);
1768
+ this.runtime = runtime ?? null;
1769
+ const resolvedRuntime = runtime ?? getActiveRuntime();
1770
+ const registered = resolvedRuntime?.getModelInstance(modelName) ?? null;
1771
+ const global = import_mongoose4.default.models[modelName];
1772
+ this.model = registered ?? global ?? import_mongoose4.default.model(modelName);
1300
1773
  if (!this.model) return;
1301
- this.model.schema.set("optimisticConcurrency", true);
1302
- const currVersionKey = this.model.schema.get("versionKey");
1303
- if (!currVersionKey) this.model.schema.set("versionKey", "__v");
1304
1774
  }
1305
1775
  new() {
1306
1776
  const doc = new this.model();
@@ -1309,20 +1779,25 @@ var Model = class {
1309
1779
  create(data) {
1310
1780
  return this.model.create(data);
1311
1781
  }
1312
- find({ filter: filter2, select, sort, populate, limit, skip, lean }) {
1782
+ find({ filter: filter2, select, sort, populate, limit, hardLimit, skip, lean }) {
1313
1783
  if (!this.validateSort(sort)) {
1314
1784
  sort = null;
1315
1785
  }
1316
1786
  const builder = this.model.find(filter2);
1317
1787
  if (select) builder.select(select);
1318
1788
  if (skip) builder.skip(Number(skip));
1319
- if (limit) builder.limit(Number(limit));
1789
+ const normalizedLimit = Number(limit);
1790
+ if (Number.isSafeInteger(normalizedLimit) && normalizedLimit > 0) {
1791
+ builder.limit(normalizedLimit);
1792
+ } else if (Number.isSafeInteger(hardLimit) && Number(hardLimit) > 0) {
1793
+ builder.limit(Number(hardLimit));
1794
+ }
1320
1795
  if (sort) builder.sort(sort);
1321
1796
  if (populate) builder.populate(populate);
1322
1797
  if (lean) builder.lean();
1323
1798
  return builder;
1324
1799
  }
1325
- validateSort(sort, logError = logger.error) {
1800
+ validateSort(sort, logError = safeSortLogError) {
1326
1801
  const validSortOrders = [1, -1, "asc", "ascending", "desc", "descending"];
1327
1802
  const fieldPathPattern = /^[A-Za-z_$][\w$]*(\.[A-Za-z_$][\w$]*)*$/;
1328
1803
  const isValidFieldPath = (field) => fieldPathPattern.test(field);
@@ -1399,7 +1874,7 @@ var Model = class {
1399
1874
  }
1400
1875
  exists(filter2) {
1401
1876
  if (!filter2) return null;
1402
- return this.findOne(filter2).select("_id").lean();
1877
+ return this.findOne({ filter: filter2 }).select("_id").lean();
1403
1878
  }
1404
1879
  // see https://mongoosejs.com/docs/api.html#query_Query-countDocuments
1405
1880
  countDocuments(filter2 = {}) {
@@ -1413,98 +1888,122 @@ var Model = class {
1413
1888
  var model_default = Model;
1414
1889
 
1415
1890
  // src/services/model-subdocument-service.ts
1416
- var import_utils12 = require("@web-ts-toolkit/utils");
1891
+ var import_utils13 = require("@web-ts-toolkit/utils");
1417
1892
  async function listSub(service, id, sub, options) {
1418
1893
  const { filter: ft, select } = options ?? {};
1894
+ const filterErrors = validateClientFilter(ft);
1895
+ if (filterErrors.length > 0) return { success: false, kind: "error", code: "bad_request" /* BadRequest */, errors: filterErrors };
1419
1896
  const parentDoc = await getParentDoc(service, id, sub, null, { access: "read" });
1420
- if (!parentDoc) return { success: false, code: "not_found" /* NotFound */ };
1421
- let result = (0, import_utils12.get)(parentDoc, sub);
1897
+ if (!parentDoc) return { success: false, kind: "error", code: "not_found" /* NotFound */ };
1898
+ let result = (0, import_utils13.get)(parentDoc, sub);
1422
1899
  const [subFilter, subSelect] = await Promise.all([
1423
1900
  service.genFilter(`subs.${sub}.list`, ft),
1424
1901
  service.genQuerySelect("list", select, false, [sub, "sub"])
1425
1902
  ]);
1426
- if (subFilter === false) return { success: false, code: "forbidden" /* Forbidden */ };
1903
+ if (subFilter === false) return { success: false, kind: "error", code: "forbidden" /* Forbidden */ };
1427
1904
  result = filterCollection(result, subFilter);
1428
- if (subSelect) result = result.map((v) => (0, import_utils12.pick)(toObject(v), subSelect.concat("_id")));
1905
+ if (subSelect) result = result.map((v) => (0, import_utils13.pick)(toObject(v), subSelect.concat("_id")));
1429
1906
  return { success: true, kind: "list", code: "success" /* Success */, data: result, count: result.length };
1430
1907
  }
1431
1908
  async function readSub(service, id, sub, subId, options) {
1432
1909
  const { select, populate } = options ?? {};
1433
1910
  const parentDoc = await getParentDoc(service, id, sub, { populate }, { access: "read" });
1434
- if (!parentDoc) return { success: false, code: "not_found" /* NotFound */ };
1435
- const result = (0, import_utils12.get)(parentDoc, sub);
1911
+ if (!parentDoc) return { success: false, kind: "error", code: "not_found" /* NotFound */ };
1912
+ const result = (0, import_utils13.get)(parentDoc, sub);
1436
1913
  const [subFilter, subSelect] = await Promise.all([
1437
1914
  service.genFilter(`subs.${sub}.read`, { _id: subId }),
1438
1915
  service.genQuerySelect("read", select, false, [sub, "sub"])
1439
1916
  ]);
1440
- if (subFilter === false) return { success: false, code: "forbidden" /* Forbidden */ };
1917
+ if (subFilter === false) return { success: false, kind: "error", code: "forbidden" /* Forbidden */ };
1441
1918
  let subdoc = findElement(result, subFilter);
1442
- if (!subdoc) return { success: false, code: "not_found" /* NotFound */ };
1443
- if (subSelect) subdoc = (0, import_utils12.pick)(toObject(subdoc), subSelect.concat(["_id"]));
1919
+ if (!subdoc) return { success: false, kind: "error", code: "not_found" /* NotFound */ };
1920
+ if (subSelect) subdoc = (0, import_utils13.pick)(toObject(subdoc), subSelect.concat(["_id"]));
1444
1921
  return { success: true, kind: "single", code: "success" /* Success */, data: subdoc };
1445
1922
  }
1446
1923
  async function updateSub(service, id, sub, subId, data) {
1447
1924
  const parentDoc = await getParentDoc(service, id, sub, null, { access: "update" });
1448
- if (!parentDoc) return { success: false, code: "not_found" /* NotFound */ };
1449
- const result = (0, import_utils12.get)(parentDoc, sub);
1925
+ if (!parentDoc) return { success: false, kind: "error", code: "not_found" /* NotFound */ };
1926
+ const result = (0, import_utils13.get)(parentDoc, sub);
1450
1927
  const [subFilter, subReadSelect, subUpdateSelect] = await Promise.all([
1451
1928
  service.genFilter(`subs.${sub}.update`, { _id: subId }),
1452
1929
  service.genQuerySelect("read", null, false, [sub, "sub"]),
1453
1930
  service.genQuerySelect("update", null, false, [sub, "sub"])
1454
1931
  ]);
1455
- if (subFilter === false) return { success: false, code: "forbidden" /* Forbidden */ };
1932
+ if (subFilter === false) return { success: false, kind: "error", code: "forbidden" /* Forbidden */ };
1456
1933
  let subdoc = findElement(result, subFilter);
1457
- if (!subdoc) return { success: false, code: "not_found" /* NotFound */ };
1458
- const allowedData = (0, import_utils12.pick)(data, subUpdateSelect);
1934
+ if (!subdoc) return { success: false, kind: "error", code: "not_found" /* NotFound */ };
1935
+ const allowedData = (0, import_utils13.pick)(data, subUpdateSelect);
1459
1936
  Object.assign(subdoc, allowedData);
1460
1937
  await parentDoc.save();
1461
- if (subReadSelect) subdoc = (0, import_utils12.pick)(toObject(subdoc), subReadSelect.concat(["_id"]));
1938
+ if (subReadSelect) subdoc = (0, import_utils13.pick)(toObject(subdoc), subReadSelect.concat(["_id"]));
1462
1939
  return { success: true, kind: "single", code: "success" /* Success */, data: subdoc };
1463
1940
  }
1464
1941
  async function bulkUpdateSub(service, id, sub, data) {
1942
+ const { maxBulkItems } = service.getRequestComplexity();
1943
+ if (data.length > maxBulkItems) {
1944
+ return {
1945
+ success: false,
1946
+ kind: "error",
1947
+ code: "bad_request" /* BadRequest */,
1948
+ errors: [{ detail: `Bulk subdocument update exceeds maximum item count of ${maxBulkItems}` }]
1949
+ };
1950
+ }
1465
1951
  const parentDoc = await getParentDoc(service, id, sub, null, { access: "update" });
1466
- if (!parentDoc) return { success: false, code: "not_found" /* NotFound */ };
1467
- let result = (0, import_utils12.get)(parentDoc, sub);
1952
+ if (!parentDoc) return { success: false, kind: "error", code: "not_found" /* NotFound */ };
1953
+ let result = (0, import_utils13.get)(parentDoc, sub);
1468
1954
  const [subFilter, subReadSelect, subUpdateSelect] = await Promise.all([
1469
1955
  service.genFilter(`subs.${sub}.update`, { _id: { $in: data.map((v) => v._id) } }),
1470
1956
  service.genQuerySelect("read", null, false, [sub, "sub"]),
1471
1957
  service.genQuerySelect("update", null, false, [sub, "sub"])
1472
1958
  ]);
1473
- if (subFilter === false) return { success: false, code: "forbidden" /* Forbidden */ };
1959
+ if (subFilter === false) return { success: false, kind: "error", code: "forbidden" /* Forbidden */ };
1474
1960
  result = filterCollection(result, subFilter);
1475
- (0, import_utils12.forEach)(result, (subdoc) => {
1961
+ (0, import_utils13.forEach)(result, (subdoc) => {
1476
1962
  const tdata = findElementById(data, subdoc._id);
1477
1963
  if (!tdata) return;
1478
- const allowedData = (0, import_utils12.pick)(tdata, subUpdateSelect);
1964
+ const allowedData = (0, import_utils13.pick)(tdata, subUpdateSelect);
1479
1965
  Object.assign(subdoc, allowedData);
1480
1966
  });
1481
1967
  await parentDoc.save();
1482
- if (subReadSelect) result = result.map((v) => (0, import_utils12.pick)(toObject(v), subReadSelect.concat(["_id"])));
1968
+ if (subReadSelect) result = result.map((v) => (0, import_utils13.pick)(toObject(v), subReadSelect.concat(["_id"])));
1483
1969
  return { success: true, kind: "list", code: "success" /* Success */, data: result, count: result.length };
1484
1970
  }
1485
1971
  async function createSub(service, id, sub, data, options) {
1486
1972
  const { addFirst } = options ?? {};
1973
+ const { maxBulkItems } = service.getRequestComplexity();
1974
+ if (Array.isArray(data) && data.length > maxBulkItems) {
1975
+ return {
1976
+ success: false,
1977
+ kind: "error",
1978
+ code: "bad_request" /* BadRequest */,
1979
+ errors: [{ detail: `Bulk subdocument create exceeds maximum item count of ${maxBulkItems}` }]
1980
+ };
1981
+ }
1487
1982
  const parentDoc = await getParentDoc(service, id, sub, null, { access: "update" });
1488
- if (!parentDoc) return { success: false, code: "not_found" /* NotFound */ };
1489
- let result = (0, import_utils12.get)(parentDoc, sub);
1983
+ if (!parentDoc) return { success: false, kind: "error", code: "not_found" /* NotFound */ };
1984
+ let result = (0, import_utils13.get)(parentDoc, sub);
1490
1985
  const [subCreateSelect, subReadSelect] = await Promise.all([
1491
1986
  service.genQuerySelect("create", null, false, [sub, "sub"]),
1492
1987
  service.genQuerySelect("read", null, false, [sub, "sub"])
1493
1988
  ]);
1494
- const allowedData = (0, import_utils12.pick)(data, subCreateSelect);
1495
- addFirst === true ? result.unshift(allowedData) : result.push(allowedData);
1989
+ const allowedData = Array.isArray(data) ? data.map((row) => (0, import_utils13.pick)(row, subCreateSelect)) : (0, import_utils13.pick)(data, subCreateSelect);
1990
+ if (Array.isArray(allowedData)) {
1991
+ addFirst === true ? result.unshift(...allowedData) : result.push(...allowedData);
1992
+ } else {
1993
+ addFirst === true ? result.unshift(allowedData) : result.push(allowedData);
1994
+ }
1496
1995
  await parentDoc.save();
1497
- if (subReadSelect) result = result.map((v) => (0, import_utils12.pick)(toObject(v), subReadSelect.concat(["_id"])));
1996
+ if (subReadSelect) result = result.map((v) => (0, import_utils13.pick)(toObject(v), subReadSelect.concat(["_id"])));
1498
1997
  return { success: true, kind: "list", code: "created" /* Created */, data: result, count: result.length };
1499
1998
  }
1500
1999
  async function deleteSub(service, id, sub, subId) {
1501
2000
  const parentDoc = await getParentDoc(service, id, sub, null, { access: "update" });
1502
- if (!parentDoc) return { success: false, code: "not_found" /* NotFound */ };
1503
- const result = (0, import_utils12.get)(parentDoc, sub);
2001
+ if (!parentDoc) return { success: false, kind: "error", code: "not_found" /* NotFound */ };
2002
+ const result = (0, import_utils13.get)(parentDoc, sub);
1504
2003
  const subFilter = await service.genFilter(`subs.${sub}.delete`, { _id: subId });
1505
- if (subFilter === false) return { success: false, code: "forbidden" /* Forbidden */ };
2004
+ if (subFilter === false) return { success: false, kind: "error", code: "forbidden" /* Forbidden */ };
1506
2005
  const subdoc = findElement(result, subFilter);
1507
- if (!subdoc) return { success: false, code: "not_found" /* NotFound */ };
2006
+ if (!subdoc) return { success: false, kind: "error", code: "not_found" /* NotFound */ };
1508
2007
  await ("deleteOne" in subdoc ? subdoc.deleteOne?.() : subdoc.remove?.());
1509
2008
  await parentDoc.save();
1510
2009
  return { success: true, kind: "single", code: "success" /* Success */, data: subdoc._id };
@@ -1514,7 +2013,7 @@ async function getParentDoc(service, id, sub, args, options) {
1514
2013
  const { access = "read", lean = false } = options ?? {};
1515
2014
  const parentFilter = await service.genFilter(access, await service.genIDFilter(id));
1516
2015
  if (parentFilter === false) return null;
1517
- return service.model.findOne({ filter: parentFilter, select: sub, populate: genSubPopulate(sub, populate), lean });
2016
+ return service.findRawParentDoc({ filter: parentFilter, select: sub, populate: genSubPopulate(sub, populate), lean });
1518
2017
  }
1519
2018
 
1520
2019
  // src/services/model-service-defaults.ts
@@ -1640,10 +2139,53 @@ var assertModelDocument = (value, modelName, hookName) => {
1640
2139
  }
1641
2140
  throw new Error(`${hookName} hook for model=${modelName} must return a Mongoose document instance`);
1642
2141
  };
2142
+ var mapWithConcurrencyLimit = async (items, limit, iteratee) => {
2143
+ const results = new Array(items.length);
2144
+ let cursor = 0;
2145
+ const workerCount = Math.min(Math.max(limit, 1), items.length || 1);
2146
+ await Promise.all(
2147
+ Array.from({ length: workerCount }, async () => {
2148
+ while (cursor < items.length) {
2149
+ const current = cursor;
2150
+ cursor += 1;
2151
+ results[current] = await iteratee(items[current], current);
2152
+ }
2153
+ })
2154
+ );
2155
+ return results;
2156
+ };
1643
2157
  var Service = class extends Base {
2158
+ findRawParentDoc(args) {
2159
+ return this.model.findOne({
2160
+ ...args,
2161
+ filter: args.filter,
2162
+ populate: args.populate
2163
+ });
2164
+ }
1644
2165
  asServiceHookContext(context) {
1645
2166
  return context;
1646
2167
  }
2168
+ beginOp(op, filter2, extra) {
2169
+ const startedAt = Date.now();
2170
+ debug({
2171
+ op,
2172
+ modelName: this.modelName,
2173
+ filterKeyValueCount: summarizeFilter(filter2).filterKeyValueCount,
2174
+ startedAt,
2175
+ ...extra ?? {}
2176
+ });
2177
+ return startedAt;
2178
+ }
2179
+ completeOp(op, startedAt, resultCode, filter2, extra) {
2180
+ debug({
2181
+ op,
2182
+ modelName: this.modelName,
2183
+ filterKeyValueCount: summarizeFilter(filter2).filterKeyValueCount,
2184
+ durationMs: Date.now() - startedAt,
2185
+ resultCode,
2186
+ ...extra ?? {}
2187
+ });
2188
+ }
1647
2189
  constructor(req, modelName) {
1648
2190
  super(req, modelName);
1649
2191
  this.model = new model_default(modelName);
@@ -1654,12 +2196,20 @@ var Service = class extends Base {
1654
2196
  }
1655
2197
  async findOne(filter2, args, options) {
1656
2198
  const filterErrors = this.validateClientFilter(filter2);
1657
- if (filterErrors.length > 0) return { success: false, code: "bad_request" /* BadRequest */, errors: filterErrors };
2199
+ if (filterErrors.length > 0) return { success: false, kind: "error", code: "bad_request" /* BadRequest */, errors: filterErrors };
1658
2200
  const { select, sort, populate, include, overrides } = this.resolveFindOneArgs(args);
1659
2201
  const { skim, includePermissions, access, populateAccess, lean } = this.resolveFindOneOptions(options);
1660
2202
  const { filter: overrideFilter, select: overrideSelect, populate: overridePopulate } = overrides ?? {};
2203
+ let parsedFilter;
2204
+ try {
2205
+ parsedFilter = await this.parseClientData(filter2);
2206
+ } catch (error2) {
2207
+ const result = this.getClientRequestErrorResult(error2);
2208
+ if (result) return result;
2209
+ throw error2;
2210
+ }
1661
2211
  let [_filter, _select, _populate] = await Promise.all([
1662
- overrideFilter || this.genFilter(access, await this.parseClientData(filter2)),
2212
+ overrideFilter || this.genFilter(access, parsedFilter),
1663
2213
  overrideSelect || this.genQuerySelect(access, select),
1664
2214
  overridePopulate || this.genPopulate(populateAccess || access, populate)
1665
2215
  ]);
@@ -1671,10 +2221,20 @@ var Service = class extends Base {
1671
2221
  sort,
1672
2222
  populate: _populate
1673
2223
  };
1674
- logger.debug(JSON.stringify({ op: "findOne", query }));
1675
- if (_filter === false) return { success: false, code: "forbidden" /* Forbidden */, query };
2224
+ const startedAt = this.beginOp("findOne", _filter, {
2225
+ sort,
2226
+ selectCount: finalSelect.length,
2227
+ populateCount: Array.isArray(_populate) ? _populate.length : _populate ? 1 : 0
2228
+ });
2229
+ if (_filter === false) {
2230
+ this.completeOp("findOne", startedAt, "forbidden" /* Forbidden */, _filter);
2231
+ return { success: false, kind: "error", code: "forbidden" /* Forbidden */, query };
2232
+ }
1676
2233
  let doc = await this.model.findOne({ ...query, lean });
1677
- if (!doc) return { success: false, code: "not_found" /* NotFound */, query };
2234
+ if (!doc) {
2235
+ this.completeOp("findOne", startedAt, "not_found" /* NotFound */, _filter);
2236
+ return { success: false, kind: "error", code: "not_found" /* NotFound */, query };
2237
+ }
1678
2238
  const context = {
1679
2239
  mongooseModel: this.model.model,
1680
2240
  modelName: this.modelName,
@@ -1682,7 +2242,16 @@ var Service = class extends Base {
1682
2242
  originalDocumentSnapshot: toObject(doc),
1683
2243
  resolvedQuery: query
1684
2244
  };
1685
- doc = await this.includeDocs(doc, includes);
2245
+ try {
2246
+ doc = await this.includeDocs(doc, includes);
2247
+ } catch (error2) {
2248
+ const result = this.getClientRequestErrorResult(error2);
2249
+ if (result) {
2250
+ this.completeOp("findOne", startedAt, result.code, _filter);
2251
+ return { ...result, query };
2252
+ }
2253
+ throw error2;
2254
+ }
1686
2255
  let includeDocPermissions = includePermissions;
1687
2256
  if (!includeDocPermissions && !skim) {
1688
2257
  includeDocPermissions = this.checkIfModelPermissionExists([access, "read", "update"]);
@@ -1695,6 +2264,7 @@ var Service = class extends Base {
1695
2264
  this.baseFieldsExt.concat(includePaths, normalizeSelect(overrideSelect))
1696
2265
  );
1697
2266
  if (!includePermissions) doc = this.addEmptyPermissions(doc);
2267
+ this.completeOp("findOne", startedAt, "success" /* Success */, _filter);
1698
2268
  return { success: true, kind: "single", code: "success" /* Success */, data: doc, query, context };
1699
2269
  }
1700
2270
  async findById(id, args, options) {
@@ -1718,18 +2288,26 @@ var Service = class extends Base {
1718
2288
  }
1719
2289
  async find(filter2, args, options, decorate) {
1720
2290
  const filterErrors = this.validateClientFilter(filter2);
1721
- if (filterErrors.length > 0) return { success: false, code: "bad_request" /* BadRequest */, errors: filterErrors };
2291
+ if (filterErrors.length > 0) return { success: false, kind: "error", code: "bad_request" /* BadRequest */, errors: filterErrors };
1722
2292
  const { select, populate, include, sort, skip, limit, page, pageSize, overrides } = this.resolveFindArgs(args);
1723
2293
  const { skim, includePermissions, includeCount, populateAccess, lean } = this.resolveFindOptions(options);
1724
2294
  const { filter: overrideFilter, select: overrideSelect, populate: overridePopulate } = overrides ?? {};
2295
+ let parsedFilter;
2296
+ try {
2297
+ parsedFilter = await this.parseClientData(filter2);
2298
+ } catch (error2) {
2299
+ const result = this.getClientRequestErrorResult(error2);
2300
+ if (result) return result;
2301
+ throw error2;
2302
+ }
1725
2303
  const [_filter, _select, _populate, pagination] = await Promise.all([
1726
- overrideFilter || this.genFilter("list", await this.parseClientData(filter2)),
2304
+ overrideFilter || this.genFilter("list", parsedFilter),
1727
2305
  overrideSelect || this.genQuerySelect("list", select),
1728
2306
  overridePopulate || this.genPopulate(populateAccess, populate),
1729
2307
  genPagination({ skip, limit, page, pageSize }, this.options.listHardLimit)
1730
2308
  ]);
1731
2309
  const finalSelect = normalizeSelect(_select);
1732
- const filteredPopulate = (0, import_utils13.isArray)(finalSelect) && (0, import_utils13.isArray)(_populate) ? _populate.filter((p) => finalSelect.includes(p.path.split(".")[0])) : _populate;
2310
+ const filteredPopulate = (0, import_utils14.isArray)(finalSelect) && (0, import_utils14.isArray)(_populate) ? _populate.filter((p) => finalSelect.includes(p.path.split(".")[0])) : _populate;
1733
2311
  const { includes, includeLocalFields, includePaths } = this.processInclude(include);
1734
2312
  const query = {
1735
2313
  filter: _filter,
@@ -1738,10 +2316,20 @@ var Service = class extends Base {
1738
2316
  sort,
1739
2317
  ...pagination
1740
2318
  };
1741
- logger.debug(JSON.stringify({ op: "find", query }));
1742
- if (_filter === false) return { success: false, code: "forbidden" /* Forbidden */, query };
2319
+ const startedAt = this.beginOp("find", _filter, {
2320
+ sort,
2321
+ skip: pagination.skip,
2322
+ limit: pagination.limit,
2323
+ selectCount: finalSelect.concat(includeLocalFields).length,
2324
+ populateCount: Array.isArray(filteredPopulate) ? filteredPopulate.length : filteredPopulate ? 1 : 0
2325
+ });
2326
+ if (_filter === false) {
2327
+ this.completeOp("find", startedAt, "forbidden" /* Forbidden */, _filter);
2328
+ return { success: false, kind: "error", code: "forbidden" /* Forbidden */, query };
2329
+ }
1743
2330
  let docs = await this.model.find({
1744
2331
  ...query,
2332
+ hardLimit: this.options.listHardLimit,
1745
2333
  lean
1746
2334
  });
1747
2335
  const contexts = docs.map((doc) => ({
@@ -1751,8 +2339,17 @@ var Service = class extends Base {
1751
2339
  originalDocumentSnapshot: toObject(doc),
1752
2340
  resolvedQuery: query
1753
2341
  }));
1754
- const _decorate = (0, import_utils13.isFunction)(decorate) ? decorate : (v) => v;
1755
- docs = await this.includeDocs(docs, includes);
2342
+ const _decorate = (0, import_utils14.isFunction)(decorate) ? decorate : (v) => v;
2343
+ try {
2344
+ docs = await this.includeDocs(docs, includes);
2345
+ } catch (error2) {
2346
+ const result = this.getClientRequestErrorResult(error2);
2347
+ if (result) {
2348
+ this.completeOp("find", startedAt, result.code, _filter);
2349
+ return { ...result, query };
2350
+ }
2351
+ throw error2;
2352
+ }
1756
2353
  const fieldPermissionAccess = includePermissions ? await this.getFieldPermissionAccess(docs.map((doc) => doc._id)) : void 0;
1757
2354
  docs = await Promise.all(
1758
2355
  docs.map(async (doc, i) => {
@@ -1773,6 +2370,7 @@ var Service = class extends Base {
1773
2370
  return doc;
1774
2371
  })
1775
2372
  );
2373
+ this.completeOp("find", startedAt, "success" /* Success */, _filter);
1776
2374
  return {
1777
2375
  success: true,
1778
2376
  kind: "list",
@@ -1789,67 +2387,96 @@ var Service = class extends Base {
1789
2387
  const { skim, includePermissions, populateAccess } = this.resolveCreateOptions(options);
1790
2388
  const isArr = Array.isArray(data);
1791
2389
  let dataArr = isArr ? data : [data];
1792
- dataArr = await Promise.all(dataArr.map((d) => this.parseClientData(d)));
2390
+ const { maxBulkItems, maxBulkConcurrency } = this.getRequestComplexity();
2391
+ if (dataArr.length > maxBulkItems) {
2392
+ return {
2393
+ success: false,
2394
+ kind: "error",
2395
+ code: "bad_request" /* BadRequest */,
2396
+ errors: [{ detail: `Bulk create exceeds maximum item count of ${maxBulkItems}` }]
2397
+ };
2398
+ }
2399
+ try {
2400
+ dataArr = await Promise.all(dataArr.map((d) => this.parseClientData(d)));
2401
+ } catch (error2) {
2402
+ const result = this.getClientRequestErrorResult(error2);
2403
+ if (result) return result;
2404
+ throw error2;
2405
+ }
1793
2406
  const resolvedPopulate = populate ? await this.genPopulate(populateAccess, populate) : [];
1794
2407
  const contexts = [];
1795
- let validationError = null;
1796
- const items = await Promise.all(
1797
- dataArr.map(async (item, index) => {
1798
- const context = {
1799
- mongooseModel: this.model.model,
1800
- modelName: this.modelName,
1801
- operation: "create",
1802
- originalData: item,
1803
- resolvedQuery: resolvedPopulate.length > 0 ? { populate: resolvedPopulate } : {}
1804
- };
1805
- const allowedFields = await this.genAllowedFields(item, "create");
1806
- const allowedData = (0, import_utils13.pick)(item, allowedFields);
1807
- context.allowedFields = allowedFields;
1808
- context.allowedData = allowedData;
1809
- const validated = await this.validate(allowedData, "create", context);
1810
- if ((0, import_utils13.isBoolean)(validated)) {
1811
- if (!validated) {
1812
- validationError = { success: false, code: "bad_request" /* BadRequest */ };
1813
- return;
1814
- }
1815
- } else if ((0, import_utils13.isArray)(validated)) {
1816
- if (validated.length > 0) {
1817
- validationError = { success: false, code: "bad_request" /* BadRequest */, errors: validated };
1818
- return;
1819
- }
2408
+ const validationErrors = [];
2409
+ const validationItems = await mapWithConcurrencyLimit(dataArr, maxBulkConcurrency, async (item, index) => {
2410
+ const context = {
2411
+ mongooseModel: this.model.model,
2412
+ modelName: this.modelName,
2413
+ operation: "create",
2414
+ originalData: item,
2415
+ resolvedQuery: resolvedPopulate.length > 0 ? { populate: resolvedPopulate } : {}
2416
+ };
2417
+ const allowedFields = await this.genAllowedFields(item, "create");
2418
+ const allowedData = (0, import_utils14.pick)(item, allowedFields);
2419
+ context.allowedFields = allowedFields;
2420
+ context.allowedData = allowedData;
2421
+ const validated = await this.validate(allowedData, "create", context);
2422
+ if ((0, import_utils14.isBoolean)(validated)) {
2423
+ if (!validated) {
2424
+ validationErrors.push({ index, errors: [] });
2425
+ return void 0;
1820
2426
  }
1821
- const preparedData = await this.prepare(allowedData, "create", context);
1822
- context.preparedData = preparedData;
1823
- contexts[index] = context;
1824
- return preparedData;
1825
- })
1826
- );
1827
- if (validationError) return validationError;
1828
- const _decorate = (0, import_utils13.isFunction)(decorate) ? decorate : (v) => v;
1829
- const createdDocs = await this.model.create(items);
1830
- const docs = await Promise.all(
1831
- createdDocs.map(async (doc, index) => {
1832
- contexts[index].currentDocument = doc;
1833
- doc = assertModelDocument(
1834
- await this.afterPersist(doc, "create", contexts[index]),
1835
- this.modelName,
1836
- "afterPersist"
1837
- );
1838
- contexts[index].currentDocument = doc;
1839
- contexts[index].finalDocumentSnapshot = doc.toObject({ virtuals: false });
1840
- let includeDocPermissions = includePermissions;
1841
- if (!includeDocPermissions && !skim) {
1842
- includeDocPermissions = this.checkIfModelPermissionExists(["create", "read", "update"]);
2427
+ } else if ((0, import_utils14.isArray)(validated)) {
2428
+ if (validated.length > 0) {
2429
+ validationErrors.push({
2430
+ index,
2431
+ errors: isArr ? validated.map((issue) => this.formatBulkValidationIssue(issue, index)) : validated
2432
+ });
2433
+ return void 0;
1843
2434
  }
1844
- if (includeDocPermissions) doc = await this.addDocPermissions(doc, "create", contexts[index]);
1845
- if (includePermissions) doc = await this.addFieldPermissions(doc, "read", contexts[index]);
1846
- if (resolvedPopulate.length > 0) await populateDoc(doc, resolvedPopulate);
1847
- doc = await this.trimOutputFields(doc, "read", this.baseFieldsExt);
1848
- let outputDoc = await _decorate(doc, contexts[index]);
1849
- if (!includePermissions) outputDoc = this.addEmptyPermissions(outputDoc);
1850
- return outputDoc;
1851
- })
1852
- );
2435
+ }
2436
+ contexts[index] = context;
2437
+ return allowedData;
2438
+ });
2439
+ if (validationErrors.length > 0) {
2440
+ const aggregate = validationErrors.slice().sort((a, b) => a.index - b.index).flatMap((entry) => entry.errors);
2441
+ if (isArr) {
2442
+ return { success: false, kind: "error", code: "bad_request" /* BadRequest */, errors: aggregate };
2443
+ }
2444
+ const single = validationErrors[0];
2445
+ return {
2446
+ success: false,
2447
+ kind: "error",
2448
+ code: "bad_request" /* BadRequest */,
2449
+ errors: single?.errors ?? []
2450
+ };
2451
+ }
2452
+ const items = await mapWithConcurrencyLimit(validationItems, maxBulkConcurrency, async (allowedData, index) => {
2453
+ const preparedData = await this.prepare(allowedData, "create", contexts[index]);
2454
+ contexts[index].preparedData = preparedData;
2455
+ return preparedData;
2456
+ });
2457
+ const _decorate = (0, import_utils14.isFunction)(decorate) ? decorate : (v) => v;
2458
+ const createdDocs = await this.model.create(items);
2459
+ const docs = await mapWithConcurrencyLimit(createdDocs, maxBulkConcurrency, async (doc, index) => {
2460
+ contexts[index].currentDocument = doc;
2461
+ doc = assertModelDocument(
2462
+ await this.afterPersist(doc, "create", contexts[index]),
2463
+ this.modelName,
2464
+ "afterPersist"
2465
+ );
2466
+ contexts[index].currentDocument = doc;
2467
+ contexts[index].finalDocumentSnapshot = doc.toObject({ virtuals: false });
2468
+ let includeDocPermissions = includePermissions;
2469
+ if (!includeDocPermissions && !skim) {
2470
+ includeDocPermissions = this.checkIfModelPermissionExists(["create", "read", "update"]);
2471
+ }
2472
+ if (includeDocPermissions) doc = await this.addDocPermissions(doc, "create", contexts[index]);
2473
+ if (includePermissions) doc = await this.addFieldPermissions(doc, "read", contexts[index]);
2474
+ if (resolvedPopulate.length > 0) await populateDoc(doc, resolvedPopulate);
2475
+ doc = await this.trimOutputFields(doc, "read", this.baseFieldsExt);
2476
+ let outputDoc = await _decorate(doc, contexts[index]);
2477
+ if (!includePermissions) outputDoc = this.addEmptyPermissions(outputDoc);
2478
+ return outputDoc;
2479
+ });
1853
2480
  return {
1854
2481
  success: true,
1855
2482
  kind: "list",
@@ -1859,18 +2486,50 @@ var Service = class extends Base {
1859
2486
  count: docs.length
1860
2487
  };
1861
2488
  }
1862
- async new() {
2489
+ formatBulkValidationIssue(issue, index) {
2490
+ if (!issue || typeof issue !== "object") {
2491
+ return {
2492
+ detail: typeof issue === "string" && issue.length > 0 ? issue : "Bad Request",
2493
+ ...index === null ? {} : { pointer: `#/${index}` }
2494
+ };
2495
+ }
2496
+ const typedIssue = issue;
2497
+ const detail = typedIssue.detail ?? typedIssue.message ?? "Bad Request";
2498
+ if (index === null) {
2499
+ return typedIssue.pointer || typedIssue.path ? {
2500
+ detail,
2501
+ ...typedIssue.pointer ? { pointer: typedIssue.pointer } : {}
2502
+ } : { detail };
2503
+ }
2504
+ if (typedIssue.pointer?.startsWith("#/")) {
2505
+ return { ...typedIssue, detail, pointer: `#/${index}${typedIssue.pointer.slice(1)}` };
2506
+ }
2507
+ if (typedIssue.path) {
2508
+ return { ...typedIssue, detail, pointer: `#/${[index, ...typedIssue.path].join("/")}` };
2509
+ }
2510
+ return { ...typedIssue, detail, pointer: `#/${index}` };
2511
+ }
2512
+ async new(args, options) {
2513
+ const { skim, includePermissions } = options ?? {};
1863
2514
  const data = await this.model.new();
2515
+ let doc = data;
2516
+ doc = await this.trimOutputFields(doc, "create", this.baseFieldsExt);
2517
+ let includeDocPermissions = includePermissions;
2518
+ if (!includeDocPermissions && !skim) {
2519
+ includeDocPermissions = this.checkIfModelPermissionExists(["create", "read", "update"]);
2520
+ }
2521
+ if (includeDocPermissions) doc = await this.addDocPermissions(doc, "create", {});
2522
+ if (!includePermissions) doc = this.addEmptyPermissions(doc);
1864
2523
  return {
1865
2524
  success: true,
1866
2525
  kind: "single",
1867
2526
  code: "success" /* Success */,
1868
- data
2527
+ data: doc
1869
2528
  };
1870
2529
  }
1871
2530
  async updateOne(filter2, data, args, options, decorate) {
1872
2531
  const filterErrors = this.validateClientFilter(filter2);
1873
- if (filterErrors.length > 0) return { success: false, code: "bad_request" /* BadRequest */, errors: filterErrors };
2532
+ if (filterErrors.length > 0) return { success: false, kind: "error", code: "bad_request" /* BadRequest */, errors: filterErrors };
1874
2533
  const { populate, overrides } = this.resolveUpdateOneArgs(args);
1875
2534
  const { skim, includePermissions, populateAccess } = this.resolveUpdateOneOptions(options);
1876
2535
  const { filter: overrideFilter, populate: overridePopulate } = overrides ?? {};
@@ -1879,31 +2538,54 @@ var Service = class extends Base {
1879
2538
  overridePopulate || this.genPopulate(populateAccess, populate)
1880
2539
  ]);
1881
2540
  const query = { filter: _filter, populate: _populate };
1882
- logger.debug(JSON.stringify({ op: "updateOne", query }));
1883
- if (_filter === false) return { success: false, code: "forbidden" /* Forbidden */, query };
2541
+ const startedAt = this.beginOp("updateOne", _filter, {
2542
+ populateCount: Array.isArray(_populate) ? _populate.length : _populate ? 1 : 0
2543
+ });
2544
+ if (_filter === false) {
2545
+ this.completeOp("updateOne", startedAt, "forbidden" /* Forbidden */, _filter);
2546
+ return { success: false, kind: "error", code: "forbidden" /* Forbidden */, query };
2547
+ }
1884
2548
  let doc = await this.model.findOne({ filter: _filter });
1885
- if (!doc) return { success: false, code: "not_found" /* NotFound */, query };
2549
+ if (!doc) {
2550
+ this.completeOp("updateOne", startedAt, "not_found" /* NotFound */, _filter);
2551
+ return { success: false, kind: "error", code: "not_found" /* NotFound */, query };
2552
+ }
1886
2553
  const context = {
1887
2554
  mongooseModel: this.model.model,
1888
2555
  modelName: this.modelName,
1889
2556
  operation: "update",
1890
2557
  resolvedQuery: query
1891
2558
  };
1892
- data = await this.parseClientData(data);
2559
+ try {
2560
+ data = await this.parseClientData(data);
2561
+ } catch (error2) {
2562
+ const result = this.getClientRequestErrorResult(error2);
2563
+ if (result) {
2564
+ this.completeOp("updateOne", startedAt, result.code, _filter);
2565
+ return result;
2566
+ }
2567
+ throw error2;
2568
+ }
1893
2569
  context.originalDocumentSnapshot = doc.toObject({ virtuals: false });
1894
2570
  context.originalData = data;
1895
2571
  doc = await this.addDocPermissions(doc, "update", context);
1896
2572
  context.docPermissions = this.getDocPermissions(doc);
1897
2573
  context.currentDocument = doc;
1898
2574
  const allowedFields = await this.genAllowedFields(doc, "update");
1899
- const allowedData = (0, import_utils13.pick)(data, allowedFields);
2575
+ const allowedData = (0, import_utils14.pick)(data, allowedFields);
1900
2576
  context.allowedFields = allowedFields;
1901
2577
  context.allowedData = allowedData;
1902
2578
  const validated = await this.validate(allowedData, "update", context);
1903
- if ((0, import_utils13.isBoolean)(validated)) {
1904
- if (!validated) return { success: false, code: "bad_request" /* BadRequest */ };
1905
- } else if ((0, import_utils13.isArray)(validated)) {
1906
- if (validated.length > 0) return { success: false, code: "bad_request" /* BadRequest */, errors: validated };
2579
+ if ((0, import_utils14.isBoolean)(validated)) {
2580
+ if (!validated) {
2581
+ this.completeOp("updateOne", startedAt, "bad_request" /* BadRequest */, _filter);
2582
+ return { success: false, kind: "error", code: "bad_request" /* BadRequest */ };
2583
+ }
2584
+ } else if ((0, import_utils14.isArray)(validated)) {
2585
+ if (validated.length > 0) {
2586
+ this.completeOp("updateOne", startedAt, "bad_request" /* BadRequest */, _filter);
2587
+ return { success: false, kind: "error", code: "bad_request" /* BadRequest */, errors: validated };
2588
+ }
1907
2589
  }
1908
2590
  const prepared = await this.prepare(allowedData, "update", context);
1909
2591
  context.preparedData = prepared;
@@ -1915,10 +2597,10 @@ var Service = class extends Base {
1915
2597
  const diffExcludeFields = [this.options.documentPermissionField, "__v"];
1916
2598
  this.asServiceHookContext(context).diff = (d) => {
1917
2599
  context.changes = (0, import_just_diff.diff)(
1918
- (0, import_utils13.omit)(context.originalDocumentSnapshot, diffExcludeFields),
1919
- (0, import_utils13.omit)(d.toObject({ virtuals: false }), diffExcludeFields)
2600
+ (0, import_utils14.omit)(context.originalDocumentSnapshot, diffExcludeFields),
2601
+ (0, import_utils14.omit)(d.toObject({ virtuals: false }), diffExcludeFields)
1920
2602
  ) || [];
1921
- context.modifiedPaths = (0, import_utils13.uniq)(context.changes.map((di) => di.path.length > 0 ? String(di.path[0]) : ""));
2603
+ context.modifiedPaths = (0, import_utils14.uniq)(context.changes.map((di) => di.path.length > 0 ? String(di.path[0]) : ""));
1922
2604
  };
1923
2605
  doc = assertModelDocument(await this.afterPersist(doc, "update", context), this.modelName, "afterPersist");
1924
2606
  context.currentDocument = doc;
@@ -1934,8 +2616,9 @@ var Service = class extends Base {
1934
2616
  if (_populate) await populateDoc(doc, _populate);
1935
2617
  doc = await this.trimOutputFields(doc, "read", this.baseFieldsExt);
1936
2618
  let outputDoc = doc;
1937
- if ((0, import_utils13.isFunction)(decorate)) outputDoc = await decorate(outputDoc, context);
2619
+ if ((0, import_utils14.isFunction)(decorate)) outputDoc = await decorate(outputDoc, context);
1938
2620
  if (!includePermissions) outputDoc = this.addEmptyPermissions(outputDoc);
2621
+ this.completeOp("updateOne", startedAt, "success" /* Success */, _filter);
1939
2622
  return { success: true, kind: "single", code: "success" /* Success */, data: outputDoc, input: prepared };
1940
2623
  }
1941
2624
  async updateById(id, data, args = {}, options = {}, decorate) {
@@ -1958,17 +2641,21 @@ var Service = class extends Base {
1958
2641
  }
1959
2642
  async upsert(filter2, data, args, options, decorate) {
1960
2643
  const filterErrors = this.validateClientFilter(filter2);
1961
- if (filterErrors.length > 0) return { success: false, code: "bad_request" /* BadRequest */, errors: filterErrors };
2644
+ if (filterErrors.length > 0) return { success: false, kind: "error", code: "bad_request" /* BadRequest */, errors: filterErrors };
1962
2645
  const { populate, overrides } = this.resolveUpsertArgs(args);
1963
2646
  const { skim, includePermissions, populateAccess } = this.resolveUpsertOptions(options);
1964
2647
  const { filter: overrideFilter, populate: overridePopulate } = overrides ?? {};
1965
2648
  const _filter = await (overrideFilter || this.genFilter("update", filter2));
1966
2649
  const query = { filter: _filter };
1967
- logger.debug(JSON.stringify({ op: "upsert", query }));
1968
- if (_filter === false) return { success: false, code: "forbidden" /* Forbidden */, query };
2650
+ const startedAt = this.beginOp("upsert", _filter);
2651
+ if (_filter === false) {
2652
+ this.completeOp("upsert", startedAt, "forbidden" /* Forbidden */, _filter);
2653
+ return { success: false, kind: "error", code: "forbidden" /* Forbidden */, query };
2654
+ }
1969
2655
  const theone = await this.model.findOne({ filter: _filter });
2656
+ let result;
1970
2657
  if (theone) {
1971
- return this.updateOne(
2658
+ result = await this.updateOne(
1972
2659
  null,
1973
2660
  data,
1974
2661
  {
@@ -1982,7 +2669,7 @@ var Service = class extends Base {
1982
2669
  decorate
1983
2670
  );
1984
2671
  } else {
1985
- return this.create(
2672
+ result = await this.create(
1986
2673
  data,
1987
2674
  { populate },
1988
2675
  {
@@ -1993,14 +2680,22 @@ var Service = class extends Base {
1993
2680
  decorate
1994
2681
  );
1995
2682
  }
2683
+ this.completeOp("upsert", startedAt, result.code, _filter);
2684
+ return result;
1996
2685
  }
1997
2686
  async delete(id) {
1998
2687
  const filter2 = await this.genFilter("delete", await this.genIDFilter(id));
1999
2688
  const query = { filter: filter2 };
2000
- logger.debug(JSON.stringify({ op: "delete", query }));
2001
- if (filter2 === false) return { success: false, code: "forbidden" /* Forbidden */, query };
2689
+ const startedAt = this.beginOp("delete", filter2);
2690
+ if (filter2 === false) {
2691
+ this.completeOp("delete", startedAt, "forbidden" /* Forbidden */, filter2);
2692
+ return { success: false, kind: "error", code: "forbidden" /* Forbidden */, query };
2693
+ }
2002
2694
  let doc = await this.model.findOne({ filter: filter2 });
2003
- if (!doc) return { success: false, code: "not_found" /* NotFound */, query };
2695
+ if (!doc) {
2696
+ this.completeOp("delete", startedAt, "not_found" /* NotFound */, filter2);
2697
+ return { success: false, kind: "error", code: "not_found" /* NotFound */, query };
2698
+ }
2004
2699
  const context = {
2005
2700
  mongooseModel: this.model.model,
2006
2701
  modelName: this.modelName,
@@ -2013,11 +2708,12 @@ var Service = class extends Base {
2013
2708
  await ("deleteOne" in doc ? doc.deleteOne() : doc.remove());
2014
2709
  context.finalDocumentSnapshot = toObject(doc);
2015
2710
  await this.afterDelete(doc, context);
2711
+ this.completeOp("delete", startedAt, "success" /* Success */, filter2);
2016
2712
  return { success: true, kind: "single", code: "success" /* Success */, data: doc._id, query };
2017
2713
  }
2018
2714
  async exists(filter2, options) {
2019
2715
  const filterErrors = this.validateClientFilter(filter2);
2020
- if (filterErrors.length > 0) return { success: false, code: "bad_request" /* BadRequest */, errors: filterErrors };
2716
+ if (filterErrors.length > 0) return { success: false, kind: "error", code: "bad_request" /* BadRequest */, errors: filterErrors };
2021
2717
  const { access, includeId } = this.resolveExistsOptions(options);
2022
2718
  filter2 = await this.genFilter(access, filter2);
2023
2719
  const result = await this.model.exists(filter2);
@@ -2029,22 +2725,58 @@ var Service = class extends Base {
2029
2725
  query: { filter: filter2 }
2030
2726
  };
2031
2727
  }
2728
+ isValidDistinctFieldName(field) {
2729
+ if (typeof field !== "string" || field.length === 0) return false;
2730
+ if (field.includes("$")) return false;
2731
+ if (field.includes("..")) return false;
2732
+ if (field.startsWith(".") || field.endsWith(".")) return false;
2733
+ if (/\s/.test(field)) return false;
2734
+ return true;
2735
+ }
2736
+ async authorizeDistinctField(field) {
2737
+ const allowedFields = await this.genAllowedFields(null, "read");
2738
+ const isAllowed = allowedFields.some((allowed) => {
2739
+ if (allowed === field) return true;
2740
+ if (allowed.startsWith(`${field}.`)) return true;
2741
+ if (field.startsWith(`${allowed}.`)) return true;
2742
+ return false;
2743
+ });
2744
+ if (!isAllowed) {
2745
+ return {
2746
+ success: false,
2747
+ kind: "error",
2748
+ code: "forbidden" /* Forbidden */,
2749
+ errors: [{ detail: `Distinct field not allowed: ${field}` }]
2750
+ };
2751
+ }
2752
+ return null;
2753
+ }
2032
2754
  async distinct(field, args) {
2755
+ if (!this.isValidDistinctFieldName(field)) {
2756
+ return {
2757
+ success: false,
2758
+ kind: "error",
2759
+ code: "bad_request" /* BadRequest */,
2760
+ errors: [{ detail: `Invalid distinct field: ${field}` }]
2761
+ };
2762
+ }
2763
+ const fieldError = await this.authorizeDistinctField(field);
2764
+ if (fieldError) return fieldError;
2033
2765
  let { filter: filter2 } = args ?? {};
2034
2766
  const filterErrors = this.validateClientFilter(filter2);
2035
- if (filterErrors.length > 0) return { success: false, code: "bad_request" /* BadRequest */, errors: filterErrors };
2767
+ if (filterErrors.length > 0) return { success: false, kind: "error", code: "bad_request" /* BadRequest */, errors: filterErrors };
2036
2768
  filter2 = await this.genFilter("read", filter2);
2037
2769
  const query = { filter: filter2 };
2038
- if (filter2 === false) return { success: false, code: "forbidden" /* Forbidden */, query };
2770
+ if (filter2 === false) return { success: false, kind: "error", code: "forbidden" /* Forbidden */, query };
2039
2771
  const result = await this.model.distinct(field, filter2);
2040
2772
  return { success: true, kind: "list", code: "success" /* Success */, data: result, count: result.length, query };
2041
2773
  }
2042
2774
  async count(filter2, access = "list") {
2043
2775
  const filterErrors = this.validateClientFilter(filter2);
2044
- if (filterErrors.length > 0) return { success: false, code: "bad_request" /* BadRequest */, errors: filterErrors };
2776
+ if (filterErrors.length > 0) return { success: false, kind: "error", code: "bad_request" /* BadRequest */, errors: filterErrors };
2045
2777
  filter2 = await this.genFilter(access, filter2);
2046
2778
  const query = { filter: filter2 };
2047
- if (filter2 === false) return { success: false, code: "forbidden" /* Forbidden */, query };
2779
+ if (filter2 === false) return { success: false, kind: "error", code: "forbidden" /* Forbidden */, query };
2048
2780
  return { success: true, kind: "single", code: "success" /* Success */, data: await this.model.countDocuments(filter2), query };
2049
2781
  }
2050
2782
  getDocPermissions(doc) {
@@ -2096,7 +2828,7 @@ var Service = class extends Base {
2096
2828
  return resolveExistsOptions(this, options);
2097
2829
  }
2098
2830
  async getFieldPermissionAccess(ids) {
2099
- const uniqueIds = (0, import_utils13.compact)((0, import_utils13.uniqBy)(ids, (id) => String(id)).map((id) => String(id)));
2831
+ const uniqueIds = (0, import_utils14.compact)((0, import_utils14.uniqBy)(ids, (id) => String(id)).map((id) => String(id)));
2100
2832
  if (uniqueIds.length === 0) {
2101
2833
  return {
2102
2834
  readIds: /* @__PURE__ */ new Set(),
@@ -2126,7 +2858,7 @@ var Service = class extends Base {
2126
2858
  return updateSub(this, id, sub, subId, data);
2127
2859
  }
2128
2860
  async bulkUpdateSub(id, sub, data) {
2129
- return bulkUpdateSub(this, id, sub, (0, import_utils13.castArray)(data));
2861
+ return bulkUpdateSub(this, id, sub, (0, import_utils14.castArray)(data));
2130
2862
  }
2131
2863
  async createSub(id, sub, data, options) {
2132
2864
  return createSub(this, id, sub, data, options);
@@ -2140,7 +2872,7 @@ var Service = class extends Base {
2140
2872
  };
2141
2873
 
2142
2874
  // src/services/public-service.ts
2143
- var import_utils14 = require("@web-ts-toolkit/utils");
2875
+ var import_utils15 = require("@web-ts-toolkit/utils");
2144
2876
  var PublicService = class extends Service {
2145
2877
  async _list(filter2, args, options) {
2146
2878
  const { select, populate, include, sort, skip, limit, page, pageSize, tasks } = this.resolvePublicListArgs(args);
@@ -2176,11 +2908,11 @@ var PublicService = class extends Service {
2176
2908
  { populate },
2177
2909
  { skim, includePermissions, populateAccess },
2178
2910
  async (doc, context) => {
2179
- doc = toObject(doc);
2180
- doc = await this.decorate(doc, "create", context);
2181
- doc = this.runTasks(doc, tasks);
2182
- if (select) doc = (0, import_utils14.pick)(doc, [...normalizeSelect(select), ...this.baseFieldsExt]);
2183
- return doc;
2911
+ let d = toObject(doc);
2912
+ d = await this.decorate(d, "create", context);
2913
+ d = this.runTasks(d, tasks);
2914
+ if (select) d = (0, import_utils15.pick)(d, [...normalizeSelect(select), ...this.baseFieldsExt]);
2915
+ return d;
2184
2916
  }
2185
2917
  );
2186
2918
  if (!result.success) {
@@ -2188,8 +2920,8 @@ var PublicService = class extends Service {
2188
2920
  }
2189
2921
  return result;
2190
2922
  }
2191
- async _new() {
2192
- return this.new();
2923
+ async _new(args, options) {
2924
+ return this.new(args, options);
2193
2925
  }
2194
2926
  async _read(id, args, options) {
2195
2927
  const { select, populate, include, tasks } = this.resolvePublicReadArgs(args);
@@ -2271,12 +3003,12 @@ var PublicService = class extends Service {
2271
3003
  { populate },
2272
3004
  { skim, includePermissions, populateAccess },
2273
3005
  async (doc, context) => {
2274
- doc = toObject(doc);
2275
- doc = await this.decorate(doc, "update", context);
2276
- doc = this.runTasks(doc, tasks);
2277
- if (select) doc = (0, import_utils14.pick)(doc, [...normalizeSelect(select), ...this.baseFieldsExt]);
2278
- else if (!returningAll) doc = (0, import_utils14.pick)(doc, [...Object.keys(data), "_id"]);
2279
- return doc;
3006
+ let d = toObject(doc);
3007
+ d = await this.decorate(d, "update", context);
3008
+ d = this.runTasks(d, tasks);
3009
+ if (select) d = (0, import_utils15.pick)(d, [...normalizeSelect(select), ...this.baseFieldsExt]);
3010
+ else if (!returningAll) d = (0, import_utils15.pick)(d, [...Object.keys(data), "_id"]);
3011
+ return d;
2280
3012
  }
2281
3013
  );
2282
3014
  if (!result.success) {
@@ -2287,7 +3019,7 @@ var PublicService = class extends Service {
2287
3019
  async _upsert(data, args, options) {
2288
3020
  const idKey = this.getIdentifier();
2289
3021
  if (idKey !== "_id") {
2290
- return { success: false, code: "bad_request" /* BadRequest */, errors: ["not supported custom id field"] };
3022
+ return { success: false, kind: "error", code: "bad_request" /* BadRequest */, errors: ["not supported custom id field"] };
2291
3023
  }
2292
3024
  const { [idKey]: idVal, ...otherData } = data;
2293
3025
  const upsertId = typeof idVal === "string" ? idVal : void 0;
@@ -2297,7 +3029,7 @@ var PublicService = class extends Service {
2297
3029
  return existing;
2298
3030
  }
2299
3031
  if (!existing.data) {
2300
- return { success: false, code: "unauthorized" /* Unauthorized */, errors: ["Unauthorized"] };
3032
+ return { success: false, kind: "error", code: "unauthorized" /* Unauthorized */, errors: ["Unauthorized"] };
2301
3033
  }
2302
3034
  return this._update(upsertId, otherData, args, options);
2303
3035
  }
@@ -2397,7 +3129,7 @@ var PublicService = class extends Service {
2397
3129
  };
2398
3130
 
2399
3131
  // src/services/data-service.ts
2400
- var import_utils15 = require("@web-ts-toolkit/utils");
3132
+ var import_utils16 = require("@web-ts-toolkit/utils");
2401
3133
  var DataService = class {
2402
3134
  constructor(req, dataName) {
2403
3135
  this.req = req;
@@ -2407,7 +3139,7 @@ var DataService = class {
2407
3139
  }
2408
3140
  async findOne(filter2, args, options) {
2409
3141
  const filterErrors = validateClientFilter(filter2);
2410
- if (filterErrors.length > 0) return { success: false, code: "bad_request" /* BadRequest */, errors: filterErrors };
3142
+ if (filterErrors.length > 0) return { success: false, kind: "error", code: "bad_request" /* BadRequest */, errors: filterErrors };
2411
3143
  const { select } = args ?? {};
2412
3144
  const { access = "read" } = options ?? {};
2413
3145
  let [_filter, _select] = await Promise.all([this.genFilter(access, filter2), this.genQuerySelect(access, select)]);
@@ -2415,11 +3147,11 @@ var DataService = class {
2415
3147
  filter: _filter,
2416
3148
  select: _select
2417
3149
  };
2418
- if (_filter === false) return { success: false, code: "forbidden" /* Forbidden */, query };
3150
+ if (_filter === false) return { success: false, kind: "error", code: "forbidden" /* Forbidden */, query };
2419
3151
  let doc = await findElement(this.data, _filter);
2420
- if (!doc) return { success: false, code: "not_found" /* NotFound */, query };
3152
+ if (!doc) return { success: false, kind: "error", code: "not_found" /* NotFound */, query };
2421
3153
  doc = await this.trimOutputFields(doc, access);
2422
- if (_select.length > 0) doc = (0, import_utils15.pick)(doc, _select);
3154
+ if (_select.length > 0) doc = (0, import_utils16.pick)(doc, _select);
2423
3155
  return { success: true, kind: "single", code: "success" /* Success */, data: doc, query };
2424
3156
  }
2425
3157
  async findById(id, args, options) {
@@ -2436,7 +3168,7 @@ var DataService = class {
2436
3168
  }
2437
3169
  async find(filter2, args, options) {
2438
3170
  const filterErrors = validateClientFilter(filter2);
2439
- if (filterErrors.length > 0) return { success: false, code: "bad_request" /* BadRequest */, errors: filterErrors };
3171
+ if (filterErrors.length > 0) return { success: false, kind: "error", code: "bad_request" /* BadRequest */, errors: filterErrors };
2440
3172
  const { select, sort, skip, limit, page, pageSize } = args ?? {};
2441
3173
  const [_filter, _select, pagination] = await Promise.all([
2442
3174
  this.genFilter("list", filter2),
@@ -2449,27 +3181,33 @@ var DataService = class {
2449
3181
  sort,
2450
3182
  ...pagination
2451
3183
  };
2452
- if (_filter === false) return { success: false, code: "forbidden" /* Forbidden */, query };
3184
+ if (_filter === false) return { success: false, kind: "error", code: "forbidden" /* Forbidden */, query };
3185
+ if (sort) {
3186
+ const sortFieldErrors = await this.validateSortFields(sort, "list");
3187
+ if (sortFieldErrors.length > 0) {
3188
+ return { success: false, kind: "error", code: "bad_request" /* BadRequest */, errors: sortFieldErrors, query };
3189
+ }
3190
+ }
2453
3191
  let docs = await filterCollection(this.data, _filter);
2454
3192
  const totalCount = docs.length;
2455
- docs = await Promise.all(
2456
- docs.map(async (doc) => {
3193
+ if (sort) {
3194
+ const { sortKey, sortOrder } = parseSortString(sort);
3195
+ docs = (0, import_utils16.orderBy)(docs, [sortKey], [sortOrder]);
3196
+ }
3197
+ const pagedDocs = docs.slice(query.skip, query.limit && query.skip + query.limit);
3198
+ const trimmed = await Promise.all(
3199
+ pagedDocs.map(async (doc) => {
2457
3200
  doc = await this.trimOutputFields(doc, "list");
2458
- if (_select.length > 0) doc = (0, import_utils15.pick)(doc, _select);
3201
+ if (_select.length > 0) doc = (0, import_utils16.pick)(doc, _select);
2459
3202
  return doc;
2460
3203
  })
2461
3204
  );
2462
- if (sort) {
2463
- const { sortKey, sortOrder } = parseSortString(sort);
2464
- docs = (0, import_utils15.orderBy)(docs, [sortKey], [sortOrder]);
2465
- }
2466
- docs = docs.slice(query.skip, query.limit && query.skip + query.limit);
2467
3205
  return {
2468
3206
  success: true,
2469
3207
  kind: "list",
2470
3208
  code: "success" /* Success */,
2471
- data: docs,
2472
- count: docs.length,
3209
+ data: trimmed,
3210
+ count: trimmed.length,
2473
3211
  totalCount,
2474
3212
  query
2475
3213
  };
@@ -2501,33 +3239,101 @@ var DataService = class {
2501
3239
  trimOutputFields(doc, access, baseFields) {
2502
3240
  return this.pickAllowedFields(doc, access, baseFields);
2503
3241
  }
3242
+ async validateSortFields(sort, access) {
3243
+ const fieldPathPattern = /^[A-Za-z_][A-Za-z0-9_$]*(\.[A-Za-z_][A-Za-z0-9_$]*)*$/;
3244
+ const fields = sort.trim().split(/\s+/).filter(Boolean).map((field) => ({
3245
+ raw: field,
3246
+ name: field.startsWith("-") ? field.slice(1) : field
3247
+ }));
3248
+ const errors = [];
3249
+ for (const { raw, name } of fields) {
3250
+ if (!fieldPathPattern.test(name)) {
3251
+ errors.push({ detail: `Invalid sort field: ${raw}`, pointer: `#/sort` });
3252
+ continue;
3253
+ }
3254
+ if (name === "id" || name === "_id") continue;
3255
+ const allowed = await this.genAllowedFields({}, access);
3256
+ if (!allowed.includes(name)) {
3257
+ errors.push({ detail: `Sort field is not allowed: ${name}`, pointer: `#/sort` });
3258
+ }
3259
+ }
3260
+ return errors;
3261
+ }
2504
3262
  };
2505
3263
 
2506
3264
  // src/processors.ts
2507
- var import_utils16 = require("@web-ts-toolkit/utils");
3265
+ var import_utils17 = require("@web-ts-toolkit/utils");
3266
+ var UNSAFE_PATH_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
3267
+ function failUnsafeSegment(path) {
3268
+ throw new Error(
3269
+ `copyAndDepopulate: refusing path '${path}' because it contains an unsafe segment (__proto__, prototype, or constructor).`
3270
+ );
3271
+ }
3272
+ function failMissingId(path) {
3273
+ throw new Error(`copyAndDepopulate: path '${path}' resolved to a record missing the configured id field.`);
3274
+ }
3275
+ function assertSafePath(path) {
3276
+ const segs = path.split(".");
3277
+ for (const seg of segs) {
3278
+ if (seg.length === 0) continue;
3279
+ if (UNSAFE_PATH_KEYS.has(seg)) {
3280
+ failUnsafeSegment(path);
3281
+ }
3282
+ }
3283
+ }
2508
3284
  var copyAndDepopulate = (docObject, operations, options = { mutable: true, idField: "_id" }) => {
2509
- const obj = (0, import_utils16.get)(options, "mutable", true) ? docObject : (0, import_utils16.cloneDeep)(docObject);
2510
- const idField = (0, import_utils16.get)(options, "idField", "_id");
2511
- (0, import_utils16.forEach)((0, import_utils16.castArray)(operations), (op) => {
2512
- if (!op.src || !op.dest) return;
3285
+ const mutable = options.mutable !== false;
3286
+ const idField = options.idField ?? "_id";
3287
+ const obj = mutable ? docObject : (0, import_utils17.cloneDeep)(docObject);
3288
+ (0, import_utils17.forEach)(Array.isArray(operations) ? operations : [], (op) => {
3289
+ if (!op || typeof op.src !== "string" || typeof op.dest !== "string") return;
3290
+ const src = op.src;
3291
+ const dest = op.dest;
3292
+ if (src.length === 0 || dest.length === 0) return;
3293
+ assertSafePath(src);
3294
+ assertSafePath(dest);
2513
3295
  let targets = [obj];
2514
- const segs = op.src.split(".");
2515
- (0, import_utils16.forEach)(segs, (seg, ind) => {
3296
+ const segs = src.split(".");
3297
+ (0, import_utils17.forEach)(segs, (seg, ind) => {
2516
3298
  if (segs.length === ind + 1) {
2517
- (0, import_utils16.forEach)(targets, (target) => {
3299
+ (0, import_utils17.forEach)(targets, (target) => {
3300
+ if (!(0, import_utils17.isPlainObject)(target)) return;
2518
3301
  const targetObject = target;
2519
- (0, import_utils16.set)(targetObject, op.dest, (0, import_utils16.get)(targetObject, seg));
2520
- (0, import_utils16.set)(
2521
- targetObject,
2522
- seg,
2523
- (0, import_utils16.isArray)(targetObject[seg]) ? (0, import_utils16.map)(targetObject[seg], idField) : (0, import_utils16.get)(targetObject, `${seg}.${idField}`)
2524
- );
3302
+ const leaf = targetObject[seg];
3303
+ const sourceForDest = leaf;
3304
+ let depopulated;
3305
+ if ((0, import_utils17.isArray)(leaf)) {
3306
+ if (leaf.length === 0) {
3307
+ depopulated = [];
3308
+ } else if (leaf.every((item) => (0, import_utils17.isPlainObject)(item))) {
3309
+ depopulated = (0, import_utils17.map)(leaf, idField).map((id, i) => {
3310
+ if (id === void 0) {
3311
+ failMissingId(`${src}[${i}]`);
3312
+ }
3313
+ return id;
3314
+ });
3315
+ } else {
3316
+ return;
3317
+ }
3318
+ } else if ((0, import_utils17.isPlainObject)(leaf)) {
3319
+ const id = (0, import_utils17.get)(leaf, idField);
3320
+ if (id === void 0) {
3321
+ failMissingId(src);
3322
+ }
3323
+ depopulated = id;
3324
+ } else {
3325
+ return;
3326
+ }
3327
+ (0, import_utils17.set)(targetObject, dest, sourceForDest);
3328
+ (0, import_utils17.set)(targetObject, seg, depopulated);
2525
3329
  });
2526
3330
  } else {
2527
3331
  targets = targets.reduce((ret, target) => {
3332
+ if (!(0, import_utils17.isPlainObject)(target)) return ret;
2528
3333
  const targetObject = target;
2529
- if ((0, import_utils16.isArray)(targetObject[seg])) ret.push(...targetObject[seg]);
2530
- else ret.push(targetObject[seg]);
3334
+ const next = targetObject[seg];
3335
+ if ((0, import_utils17.isArray)(next)) ret.push(...next);
3336
+ else if (next !== null && next !== void 0) ret.push(next);
2531
3337
  return ret;
2532
3338
  }, []);
2533
3339
  }
@@ -2574,10 +3380,10 @@ var Cache = class {
2574
3380
  };
2575
3381
 
2576
3382
  // src/core-shared.ts
2577
- var import_utils18 = require("@web-ts-toolkit/utils");
3383
+ var import_utils19 = require("@web-ts-toolkit/utils");
2578
3384
 
2579
3385
  // src/permission.ts
2580
- var import_utils17 = require("@web-ts-toolkit/utils");
3386
+ var import_utils18 = require("@web-ts-toolkit/utils");
2581
3387
  var Permission = class {
2582
3388
  constructor(permissions) {
2583
3389
  this.permissions = { ...permissions };
@@ -2593,7 +3399,7 @@ var Permission = class {
2593
3399
  return [permissionOrPermissions].concat(permissions);
2594
3400
  }
2595
3401
  hasKey(permission) {
2596
- return (0, import_utils17.hasOwn)(this.permissions, permission);
3402
+ return (0, import_utils18.hasOwn)(this.permissions, permission);
2597
3403
  }
2598
3404
  has(permission) {
2599
3405
  return this.permissions[permission] === true;
@@ -2613,10 +3419,10 @@ var permission_default = Permission;
2613
3419
 
2614
3420
  // src/core-shared.ts
2615
3421
  function isAndFilter(filter2) {
2616
- return !!filter2 && (0, import_utils18.isPlainObject)(filter2) && Object.keys(filter2).length === 1 && (0, import_utils18.isArray)(filter2.$and);
3422
+ return !!filter2 && (0, import_utils19.isPlainObject)(filter2) && Object.keys(filter2).length === 1 && (0, import_utils19.isArray)(filter2.$and);
2617
3423
  }
2618
3424
  function isMergeableClause(filter2) {
2619
- return !!filter2 && (0, import_utils18.isPlainObject)(filter2) && Object.keys(filter2).every((key) => !key.startsWith("$"));
3425
+ return !!filter2 && (0, import_utils19.isPlainObject)(filter2) && Object.keys(filter2).every((key) => !key.startsWith("$"));
2620
3426
  }
2621
3427
  function optimizeAndFilter(clausesInput) {
2622
3428
  const clauses = [];
@@ -2630,9 +3436,24 @@ function optimizeAndFilter(clausesInput) {
2630
3436
  clauses.push(clause);
2631
3437
  }
2632
3438
  }
2633
- const dedupedClauses = clauses.filter(
2634
- (clause, index) => clauses.findIndex((item) => (0, import_utils18.isEqual)(item, clause)) === index
2635
- );
3439
+ const dedupeKey = (value) => {
3440
+ if ((0, import_utils19.isArray)(value)) {
3441
+ return `[${value.map(dedupeKey).join(",")}]`;
3442
+ }
3443
+ if ((0, import_utils19.isPlainObject)(value)) {
3444
+ return `{${Object.keys(value).sort().map((key) => `${key}:${dedupeKey(value[key])}`).join(",")}}`;
3445
+ }
3446
+ return JSON.stringify(value);
3447
+ };
3448
+ const dedupedClauses = [];
3449
+ const seenClauses = /* @__PURE__ */ new Set();
3450
+ for (let x = 0; x < clauses.length; x++) {
3451
+ const clause = clauses[x];
3452
+ const key = dedupeKey(clause);
3453
+ if (seenClauses.has(key)) continue;
3454
+ seenClauses.add(key);
3455
+ dedupedClauses.push(clause);
3456
+ }
2636
3457
  const mergedClause = {};
2637
3458
  const remainingClauses = [];
2638
3459
  for (let x = 0; x < dedupedClauses.length; x++) {
@@ -2643,7 +3464,7 @@ function optimizeAndFilter(clausesInput) {
2643
3464
  }
2644
3465
  let canMerge = true;
2645
3466
  for (const [key, value] of Object.entries(clause)) {
2646
- if ((0, import_utils18.hasOwn)(mergedClause, key) && !(0, import_utils18.isEqual)(mergedClause[key], value)) {
3467
+ if ((0, import_utils19.hasOwn)(mergedClause, key) && !(0, import_utils19.isEqual)(mergedClause[key], value)) {
2647
3468
  canMerge = false;
2648
3469
  break;
2649
3470
  }
@@ -2654,24 +3475,24 @@ function optimizeAndFilter(clausesInput) {
2654
3475
  }
2655
3476
  Object.assign(mergedClause, clause);
2656
3477
  }
2657
- const finalClauses = (0, import_utils18.isEmpty)(mergedClause) ? remainingClauses : [mergedClause, ...remainingClauses];
3478
+ const finalClauses = (0, import_utils19.isEmpty)(mergedClause) ? remainingClauses : [mergedClause, ...remainingClauses];
2658
3479
  if (finalClauses.length === 0) return null;
2659
3480
  if (finalClauses.length === 1) return finalClauses[0];
2660
3481
  return { $and: finalClauses };
2661
3482
  }
2662
3483
  function normalizeFilter(filter2) {
2663
3484
  if (filter2 === false) return false;
2664
- if (!filter2 || !(0, import_utils18.isPlainObject)(filter2) || (0, import_utils18.isEmpty)(filter2)) return null;
3485
+ if (!filter2 || !(0, import_utils19.isPlainObject)(filter2) || (0, import_utils19.isEmpty)(filter2)) return null;
2665
3486
  if (!isAndFilter(filter2)) {
2666
3487
  return filter2;
2667
3488
  }
2668
3489
  return optimizeAndFilter(filter2.$and);
2669
3490
  }
2670
3491
  async function resolveIdentifierFilter(req, idField, resolveIdFilter, id) {
2671
- if ((0, import_utils18.isFunction)(resolveIdFilter)) {
3492
+ if ((0, import_utils19.isFunction)(resolveIdFilter)) {
2672
3493
  return await resolveIdFilter.call(req, id);
2673
3494
  }
2674
- if ((0, import_utils18.isString)(idField)) {
3495
+ if ((0, import_utils19.isString)(idField)) {
2675
3496
  return { [idField]: id };
2676
3497
  }
2677
3498
  return { _id: id };
@@ -2687,11 +3508,11 @@ async function resolveAccessFilter({
2687
3508
  }) {
2688
3509
  let nextFilter = normalizeFilter(filter2);
2689
3510
  const overrideFilterFn = getOption(`overrideFilter.${access}`, null);
2690
- if ((0, import_utils18.isFunction)(overrideFilterFn)) {
3511
+ if ((0, import_utils19.isFunction)(overrideFilterFn)) {
2691
3512
  nextFilter = normalizeFilter(await overrideFilterFn.call(req, nextFilter, permissions));
2692
3513
  }
2693
3514
  const baseFilterFn = getOption(`baseFilter.${access}`, null);
2694
- if (!(0, import_utils18.isFunction)(baseFilterFn)) return nextFilter || {};
3515
+ if (!(0, import_utils19.isFunction)(baseFilterFn)) return nextFilter || {};
2695
3516
  const baseFilter = normalizeFilter(
2696
3517
  cache.has(cacheKey) ? cache.get(cacheKey) : await baseFilterFn.call(req, permissions)
2697
3518
  );
@@ -2711,33 +3532,33 @@ async function setRequestPermissions(req) {
2711
3532
  const requestPermissionField = getGlobalOption("requestPermissionField");
2712
3533
  if (req[requestPermissionField]) return;
2713
3534
  const globalPermissions = getGlobalOption("globalPermissions");
2714
- if (!(0, import_utils18.isFunction)(globalPermissions)) return;
3535
+ if (!(0, import_utils19.isFunction)(globalPermissions)) return;
2715
3536
  const permissions = await globalPermissions.call(req, req);
2716
- if ((0, import_utils18.isPlainObject)(permissions)) req[requestPermissionField] = permissions;
2717
- else if ((0, import_utils18.isArray)(permissions)) req[requestPermissionField] = (0, import_utils18.arrayToRecord)(permissions);
2718
- else if ((0, import_utils18.isString)(permissions)) req[requestPermissionField] = { [permissions]: true };
3537
+ if ((0, import_utils19.isPlainObject)(permissions)) req[requestPermissionField] = permissions;
3538
+ else if ((0, import_utils19.isArray)(permissions)) req[requestPermissionField] = (0, import_utils19.arrayToRecord)(permissions);
3539
+ else if ((0, import_utils19.isString)(permissions)) req[requestPermissionField] = { [permissions]: true };
2719
3540
  }
2720
3541
  async function evaluateRouteGuard(req, permissions, routeGuard) {
2721
3542
  const phas = (key) => permissions.has(key);
2722
3543
  const { stringHandler, arrayHandler } = createValidator(phas);
2723
- if ((0, import_utils18.isBoolean)(routeGuard)) {
3544
+ if ((0, import_utils19.isBoolean)(routeGuard)) {
2724
3545
  return routeGuard === true;
2725
3546
  }
2726
- if ((0, import_utils18.isString)(routeGuard)) {
3547
+ if ((0, import_utils19.isString)(routeGuard)) {
2727
3548
  return stringHandler(routeGuard);
2728
3549
  }
2729
- if ((0, import_utils18.isArray)(routeGuard)) {
3550
+ if ((0, import_utils19.isArray)(routeGuard)) {
2730
3551
  return arrayHandler(routeGuard);
2731
3552
  }
2732
- if ((0, import_utils18.isFunction)(routeGuard)) {
3553
+ if ((0, import_utils19.isFunction)(routeGuard)) {
2733
3554
  return routeGuard.call(req, permissions);
2734
3555
  }
2735
3556
  return false;
2736
3557
  }
2737
3558
  async function callHookChain(req, hook, doc, permissions, context) {
2738
- const hooks = (0, import_utils18.castArray)(hook);
3559
+ const hooks = (0, import_utils19.castArray)(hook);
2739
3560
  for (let x = 0; x < hooks.length; x++) {
2740
- if ((0, import_utils18.isFunction)(hooks[x])) {
3561
+ if ((0, import_utils19.isFunction)(hooks[x])) {
2741
3562
  doc = await hooks[x].call(req, doc, permissions, context);
2742
3563
  }
2743
3564
  }
@@ -2760,13 +3581,13 @@ async function collectSchemaFields({
2760
3581
  if (baseFields.includes(key)) continue;
2761
3582
  const val = permissionSchema[key];
2762
3583
  const value = val && val[access] || val;
2763
- if ((0, import_utils18.isBoolean)(value)) {
3584
+ if ((0, import_utils19.isBoolean)(value)) {
2764
3585
  if (value) fields.push(key);
2765
- } else if ((0, import_utils18.isString)(value)) {
3586
+ } else if ((0, import_utils19.isString)(value)) {
2766
3587
  if (stringHandler(value)) fields.push(key);
2767
- } else if ((0, import_utils18.isArray)(value)) {
3588
+ } else if ((0, import_utils19.isArray)(value)) {
2768
3589
  if (arrayHandler(value)) fields.push(key);
2769
- } else if ((0, import_utils18.isFunction)(value)) {
3590
+ } else if ((0, import_utils19.isFunction)(value)) {
2770
3591
  if (await value.call(req, ...functionArgs)) fields.push(key);
2771
3592
  }
2772
3593
  }
@@ -2800,7 +3621,7 @@ async function initializeAclRequest({
2800
3621
  await core.setPermissions();
2801
3622
  assignCore(core);
2802
3623
  req[PERMISSIONS] = core.getPermissions();
2803
- req[PERMISSION_KEYS] = req[PERMISSIONS].keys;
3624
+ req[PERMISSION_KEYS] = [...req[PERMISSIONS].keys];
2804
3625
  req[flag] = true;
2805
3626
  return core;
2806
3627
  }
@@ -2855,7 +3676,7 @@ async function runDecorateAllHook({
2855
3676
  }
2856
3677
 
2857
3678
  // src/acl/select-resolution.ts
2858
- var import_utils19 = require("@web-ts-toolkit/utils");
3679
+ var import_utils20 = require("@web-ts-toolkit/utils");
2859
3680
  async function collectAllowedFieldsForRequest({
2860
3681
  req,
2861
3682
  permissionSchema,
@@ -2923,15 +3744,15 @@ async function resolveSelectForRequest({
2923
3744
  const excludeall = normalizedSelect.every((v) => v.startsWith("-"));
2924
3745
  if (excludeall) {
2925
3746
  normalizedSelect = normalizedSelect.map((v) => v.substring(1));
2926
- fields = (0, import_utils19.difference)(fields, normalizedSelect);
3747
+ fields = (0, import_utils20.difference)(fields, normalizedSelect);
2927
3748
  if (excludeid) fields.push("-_id");
2928
3749
  } else {
2929
- fields = (0, import_utils19.intersection)(normalizedSelect, fields.concat(excludeid ? "-_id" : "_id"));
3750
+ fields = (0, import_utils20.intersection)(normalizedSelect, fields.concat(excludeid ? "-_id" : "_id"));
2930
3751
  }
2931
3752
  }
2932
3753
  return fields.concat(alwaysSelectFields);
2933
3754
  }
2934
- return (0, import_utils19.intersection)(normalizedSelect, fields);
3755
+ return (0, import_utils20.intersection)(normalizedSelect, fields);
2935
3756
  }
2936
3757
 
2937
3758
  // src/core.ts
@@ -2945,10 +3766,10 @@ var Core = class {
2945
3766
  getIdentifier(modelName) {
2946
3767
  const resolveIdFilter = getModelOption(modelName, "resolveIdFilter");
2947
3768
  const idField = getModelOption(modelName, "idField");
2948
- if ((0, import_utils20.isFunction)(resolveIdFilter)) {
3769
+ if ((0, import_utils21.isFunction)(resolveIdFilter)) {
2949
3770
  return null;
2950
3771
  }
2951
- if ((0, import_utils20.isString)(idField)) {
3772
+ if ((0, import_utils21.isString)(idField)) {
2952
3773
  return idField;
2953
3774
  }
2954
3775
  return "_id";
@@ -3026,11 +3847,11 @@ var Core = class {
3026
3847
  async genPopulate(modelName, access = "read", _populate = null) {
3027
3848
  if (!_populate) return [];
3028
3849
  let populate = Array.isArray(_populate) ? _populate : [_populate];
3029
- populate = (0, import_utils20.compact)(
3850
+ populate = (0, import_utils21.compact)(
3030
3851
  await Promise.all(
3031
3852
  populate.map(async (p) => {
3032
- const populateAccess = !(0, import_utils20.isString)(p) && p.access ? p.access : access;
3033
- const ret = (0, import_utils20.isString)(p) ? { path: p } : {
3853
+ const populateAccess = !(0, import_utils21.isString)(p) && p.access ? p.access : access;
3854
+ const ret = (0, import_utils21.isString)(p) ? { path: p } : {
3034
3855
  path: p.path,
3035
3856
  select: normalizeSelect(p.select)
3036
3857
  };
@@ -3044,6 +3865,14 @@ var Core = class {
3044
3865
  if (!allowedParentPaths.includes(parentPath)) return null;
3045
3866
  const refModelName = getModelRef(modelName, ret.path);
3046
3867
  if (!refModelName) return null;
3868
+ const runtime = getActiveRuntime();
3869
+ if (runtime && !runtime.hasModel(refModelName)) {
3870
+ return null;
3871
+ }
3872
+ const allowedTargetOperation = await this.req.macl.isAllowed(refModelName, populateAccess);
3873
+ if (!allowedTargetOperation) {
3874
+ return null;
3875
+ }
3047
3876
  ret.select = await this.genSelect(refModelName, populateAccess, ret.select, false);
3048
3877
  const filter2 = await this.genFilter(refModelName, populateAccess, null);
3049
3878
  if (filter2 === false) return null;
@@ -3056,10 +3885,10 @@ var Core = class {
3056
3885
  }
3057
3886
  async validate(modelName, allowedData, access, context) {
3058
3887
  const validate = getModelOption(modelName, `validate.${access}`, null);
3059
- if ((0, import_utils20.isFunction)(validate)) {
3888
+ if ((0, import_utils21.isFunction)(validate)) {
3060
3889
  const permissions = this.getGlobalPermissions();
3061
3890
  return validate.call(this.req, allowedData, permissions, context);
3062
- } else if ((0, import_utils20.isBoolean)(validate) || (0, import_utils20.isArray)(validate)) {
3891
+ } else if ((0, import_utils21.isBoolean)(validate) || (0, import_utils21.isArray)(validate)) {
3063
3892
  return validate;
3064
3893
  } else {
3065
3894
  return true;
@@ -3084,7 +3913,7 @@ var Core = class {
3084
3913
  const changeOptions = getModelOption(modelName, `onChange`, {});
3085
3914
  for (let x = 0; x < context.modifiedPaths.length; x++) {
3086
3915
  const mpath = context.modifiedPaths[x];
3087
- if ((0, import_utils20.isFunction)(changeOptions[mpath])) {
3916
+ if ((0, import_utils21.isFunction)(changeOptions[mpath])) {
3088
3917
  await changeOptions[mpath].call(
3089
3918
  this.req,
3090
3919
  context.originalDocumentSnapshot[mpath],
@@ -3108,14 +3937,17 @@ var Core = class {
3108
3937
  async genDocPermissions(modelName, doc, access, context) {
3109
3938
  const docPermissionsFn = getModelOption(modelName, `docPermissions.${access}`, null);
3110
3939
  let docPermissions = {};
3111
- if ((0, import_utils20.isFunction)(docPermissionsFn)) {
3940
+ if ((0, import_utils21.isFunction)(docPermissionsFn)) {
3112
3941
  const permissions = this.getGlobalPermissions();
3113
3942
  try {
3114
3943
  docPermissions = await docPermissionsFn.call(this.req, doc, permissions, context);
3115
- } catch (error) {
3116
- logger.warn(
3117
- `docPermissions hook failed for model=${modelName} access=${access}: ${error instanceof Error ? error.message : String(error)}`
3118
- );
3944
+ } catch (error2) {
3945
+ warn("docPermissions hook failed; applying empty document permissions", {
3946
+ modelName,
3947
+ access,
3948
+ operation: context.operation,
3949
+ error: error2 instanceof Error ? error2.message : String(error2)
3950
+ });
3119
3951
  }
3120
3952
  }
3121
3953
  return docPermissions;
@@ -3156,7 +3988,7 @@ var Core = class {
3156
3988
  readExists ? this.genAllowedFields(modelName, doc, "read") : [],
3157
3989
  updateExists ? this.genAllowedFields(modelName, doc, "update") : []
3158
3990
  ]);
3159
- const viewObj = (0, import_utils20.reduce)(
3991
+ const viewObj = (0, import_utils21.reduce)(
3160
3992
  views,
3161
3993
  (ret, view) => {
3162
3994
  ret[view] = true;
@@ -3164,7 +3996,7 @@ var Core = class {
3164
3996
  },
3165
3997
  {}
3166
3998
  );
3167
- const editObj = (0, import_utils20.reduce)(
3999
+ const editObj = (0, import_utils21.reduce)(
3168
4000
  edits,
3169
4001
  (ret, view) => {
3170
4002
  ret[view] = true;
@@ -3188,9 +4020,9 @@ var Core = class {
3188
4020
  return runDecorateAllHook({ req: this.req, hook: decorateAll, docs, permissions, context });
3189
4021
  }
3190
4022
  runTasks(modelName, docObject, task) {
3191
- const tasks = (0, import_utils20.compact)((0, import_utils20.castArray)(task));
4023
+ const tasks = (0, import_utils21.compact)((0, import_utils21.castArray)(task));
3192
4024
  if (tasks.length === 0) return docObject;
3193
- (0, import_utils20.forEach)(tasks, (task2) => {
4025
+ (0, import_utils21.forEach)(tasks, (task2) => {
3194
4026
  const { type, args, options } = task2;
3195
4027
  switch (type) {
3196
4028
  case "COPY_AND_DEPOPULATE":
@@ -3221,9 +4053,9 @@ var Core = class {
3221
4053
  }
3222
4054
  const [, field, op] = keys2;
3223
4055
  const subOption = getExactModelOption(modelName, `operationAccess.${access}`);
3224
- if ((0, import_utils20.isUndefined)(subOption)) {
4056
+ if ((0, import_utils21.isUndefined)(subOption)) {
3225
4057
  const subFieldOption = getExactModelOption(modelName, `operationAccess.subs.${field}`);
3226
- if ((0, import_utils20.isUndefined)(subFieldOption)) {
4058
+ if ((0, import_utils21.isUndefined)(subFieldOption)) {
3227
4059
  const opOption = getModelOption(modelName, `operationAccess.${op}`);
3228
4060
  return this.canActivate(opOption);
3229
4061
  }
@@ -3273,25 +4105,25 @@ function guard(condition) {
3273
4105
  const permissions = req[PERMISSIONS];
3274
4106
  let cond = condition;
3275
4107
  let phas = (key) => permissions.has(key);
3276
- if ((0, import_utils21.isPlainObject)(condition)) {
4108
+ if ((0, import_utils22.isPlainObject)(condition)) {
3277
4109
  const { modelName, id, condition: _cond } = condition;
3278
4110
  const svc = req.macl.getPublicService(modelName);
3279
4111
  const select = getModelOption(modelName, `alwaysSelectFields.read`, void 0);
3280
- let _id = (0, import_utils21.isString)(id) ? id : null;
3281
- if ((0, import_utils21.isPlainObject)(id)) {
4112
+ let _id = (0, import_utils22.isString)(id) ? id : null;
4113
+ if ((0, import_utils22.isPlainObject)(id)) {
3282
4114
  const { type = "param", key } = id;
3283
4115
  if (type === "param") {
3284
4116
  const paramValue = req.params[key];
3285
- _id = (0, import_utils21.isArray)(paramValue) ? paramValue[0] ?? null : paramValue ?? null;
4117
+ _id = (0, import_utils22.isArray)(paramValue) ? paramValue[0] ?? null : paramValue ?? null;
3286
4118
  } else if (type === "query") {
3287
4119
  const _qval = req.query[key];
3288
- if ((0, import_utils21.isArray)(_qval)) _id = _qval.length > 0 ? _qval[0] : null;
4120
+ if ((0, import_utils22.isArray)(_qval)) _id = _qval.length > 0 ? _qval[0] : null;
3289
4121
  else _id = _qval;
3290
4122
  } else {
3291
4123
  _id = null;
3292
4124
  }
3293
4125
  }
3294
- if (!(0, import_utils21.isString)(_id) || !_id) {
4126
+ if (!(0, import_utils22.isString)(_id) || !_id) {
3295
4127
  return next(new import_express_json_router2.default.clientErrors.BadRequestError());
3296
4128
  }
3297
4129
  const result = await svc._read(_id, { select });
@@ -3303,11 +4135,11 @@ function guard(condition) {
3303
4135
  cond = _cond;
3304
4136
  }
3305
4137
  const { stringHandler, arrayHandler } = createValidator(phas);
3306
- if ((0, import_utils21.isString)(cond)) {
4138
+ if ((0, import_utils22.isString)(cond)) {
3307
4139
  if (stringHandler(cond)) return next();
3308
- } else if ((0, import_utils21.isArray)(cond)) {
4140
+ } else if ((0, import_utils22.isArray)(cond)) {
3309
4141
  if (arrayHandler(cond)) return next();
3310
- } else if ((0, import_utils21.isFunction)(cond)) {
4142
+ } else if ((0, import_utils22.isFunction)(cond)) {
3311
4143
  const result = await cond.call(req, permissions);
3312
4144
  if (!!result) return next();
3313
4145
  }
@@ -3350,9 +4182,37 @@ function assertMutableRouterOptions(kind, options) {
3350
4182
  // src/http/response-pipelines/model-response.ts
3351
4183
  var import_express_json_router3 = __toESM(require("@web-ts-toolkit/express-json-router"));
3352
4184
 
4185
+ // src/http/response-pipelines/service-result.ts
4186
+ var toPublicErrorResult = (result) => ({
4187
+ success: false,
4188
+ code: result.code,
4189
+ errors: result.errors
4190
+ });
4191
+ var toPublicSingleResult = (result) => ({
4192
+ success: true,
4193
+ kind: "single",
4194
+ code: result.code,
4195
+ data: result.data
4196
+ });
4197
+ var toPublicListResult = (result) => ({
4198
+ success: true,
4199
+ kind: "list",
4200
+ code: result.code,
4201
+ data: result.data,
4202
+ count: result.count,
4203
+ totalCount: result.totalCount ?? null
4204
+ });
4205
+ var toPublicServiceResult = (result) => {
4206
+ if (result.kind === "error") {
4207
+ return toPublicErrorResult(result);
4208
+ }
4209
+ return result.kind === "list" ? toPublicListResult(result) : toPublicSingleResult(result);
4210
+ };
4211
+
3353
4212
  // src/http/response-pipelines/list-response.ts
3354
4213
  var formatListResponse = (req, result, includeCount, includeExtraHeaders) => {
3355
- const { data, count: returnedCount, totalCount } = result;
4214
+ const publicResult = toPublicListResult(result);
4215
+ const { data, count: returnedCount, totalCount } = publicResult;
3356
4216
  const query = result.query ?? {};
3357
4217
  const rawLimit = Number(query.limit ?? 0);
3358
4218
  const skip = Number(query.skip ?? 0);
@@ -3387,16 +4247,18 @@ var formatListResponse = (req, result, includeCount, includeExtraHeaders) => {
3387
4247
  return { data, meta };
3388
4248
  };
3389
4249
  var formatCreatedData = (result) => {
3390
- return result.count === 1 ? result.data[0] : result.data;
4250
+ const publicResult = toPublicListResult(result);
4251
+ return publicResult.count === 1 ? publicResult.data[0] : publicResult.data;
3391
4252
  };
3392
4253
  var formatUpsertCreatedData = (result) => {
3393
- return result.data.length > 0 ? result.data[0] : null;
4254
+ const publicResult = toPublicListResult(result);
4255
+ return publicResult.data.length > 0 ? publicResult.data[0] : null;
3394
4256
  };
3395
4257
 
3396
4258
  // src/http/response-pipelines/model-response.ts
3397
4259
  var success = import_express_json_router3.default.success;
3398
4260
  function unwrapServiceData(result) {
3399
- return result.data;
4261
+ return result.kind === "list" ? toPublicListResult(result).data : toPublicSingleResult(result).data;
3400
4262
  }
3401
4263
  function formatModelListResponse(req, result, includeCount, includeExtraHeaders) {
3402
4264
  return formatListResponse(req, result, includeCount, includeExtraHeaders);
@@ -3405,7 +4267,13 @@ function formatModelCreatedResponse(result) {
3405
4267
  return new success.Created(formatCreatedData(result));
3406
4268
  }
3407
4269
  function formatModelUpsertResponse(result) {
3408
- return result.kind === "list" ? new success.Created(formatUpsertCreatedData(result)) : result.data;
4270
+ if (result.kind === "list") {
4271
+ return new success.Created(formatUpsertCreatedData(result));
4272
+ }
4273
+ if (result.kind === "single") {
4274
+ return toPublicSingleResult(result).data;
4275
+ }
4276
+ throw new Error(`Unexpected ServiceResult kind: ${String(result.kind)}`);
3409
4277
  }
3410
4278
 
3411
4279
  // src/validation/parsers.ts
@@ -3414,9 +4282,12 @@ var import_express_json_router4 = __toESM(require("@web-ts-toolkit/express-json-
3414
4282
  // src/validation/common.ts
3415
4283
  var import_zod2 = require("zod");
3416
4284
  var stringOrStringArray = import_zod2.z.union([import_zod2.z.string(), import_zod2.z.array(import_zod2.z.string())]);
4285
+ var safeIntegerString = (min, message) => import_zod2.z.string().regex(/^\d+$/, message).refine((value) => Number.isSafeInteger(Number(value)), "Expected a safe integer").refine((value) => Number(value) >= min, message);
3417
4286
  var queryBooleanString = import_zod2.z.enum(["true", "false"]);
3418
- var positiveIntegerString = import_zod2.z.string().regex(/^\d+$/, "Expected a non-negative integer");
3419
- var nonNegativeIntegerSchema = import_zod2.z.number().int().min(0);
4287
+ var nonNegativeIntegerString = safeIntegerString(0, "Expected a non-negative integer");
4288
+ var positiveIntegerString = safeIntegerString(1, "Expected a positive integer");
4289
+ var nonNegativeIntegerSchema = import_zod2.z.number().int().min(0).max(Number.MAX_SAFE_INTEGER);
4290
+ var positiveIntegerSchema = import_zod2.z.number().int().min(1).max(Number.MAX_SAFE_INTEGER);
3420
4291
  var unknownRecord = import_zod2.z.record(import_zod2.z.string(), import_zod2.z.unknown());
3421
4292
  var projectionObjectSchema = import_zod2.z.record(import_zod2.z.string(), import_zod2.z.union([import_zod2.z.literal(1), import_zod2.z.literal(-1)]));
3422
4293
  var projectionSchema = import_zod2.z.union([import_zod2.z.string(), import_zod2.z.array(import_zod2.z.string()), projectionObjectSchema]);
@@ -3517,6 +4388,10 @@ function parsePathParam(value, parameter) {
3517
4388
  return param;
3518
4389
  }
3519
4390
  function parseQuery(schema, value) {
4391
+ const complexityErrors = validateRequestComplexity(value, getGlobalOption("requestComplexity"), "request");
4392
+ if (complexityErrors.length > 0) {
4393
+ throw new clientErrors.BadRequestError("Bad Request", { errors: complexityErrors });
4394
+ }
3520
4395
  const result = schema.safeParse(value);
3521
4396
  if (!result.success) {
3522
4397
  throwValidationError(normalizeIssues(result.error.issues), void 0, "parameter");
@@ -3524,6 +4399,10 @@ function parseQuery(schema, value) {
3524
4399
  return result.data;
3525
4400
  }
3526
4401
  function parseBody(schema, value) {
4402
+ const complexityErrors = validateRequestComplexity(value ?? {}, getGlobalOption("requestComplexity"), "request");
4403
+ if (complexityErrors.length > 0) {
4404
+ throw new clientErrors.BadRequestError("Bad Request", { errors: complexityErrors });
4405
+ }
3527
4406
  const result = schema.safeParse(value ?? {});
3528
4407
  if (!result.success) {
3529
4408
  throwValidationError(normalizeIssues(result.error.issues), void 0, "pointer");
@@ -3626,10 +4505,10 @@ function fromYup(schema) {
3626
4505
  success: true,
3627
4506
  data
3628
4507
  };
3629
- } catch (error) {
4508
+ } catch (error2) {
3630
4509
  return {
3631
4510
  success: false,
3632
- issues: normalizeYupIssues(error)
4511
+ issues: normalizeYupIssues(error2)
3633
4512
  };
3634
4513
  }
3635
4514
  };
@@ -3732,10 +4611,10 @@ function fromVine(validator) {
3732
4611
  success: true,
3733
4612
  data: output
3734
4613
  };
3735
- } catch (error) {
4614
+ } catch (error2) {
3736
4615
  return {
3737
4616
  success: false,
3738
- issues: normalizeVineError(error)
4617
+ issues: normalizeVineError(error2)
3739
4618
  };
3740
4619
  }
3741
4620
  };
@@ -3756,11 +4635,11 @@ function normalizePathSegment(segment) {
3756
4635
  function isStandardSchemaPathSegment(segment) {
3757
4636
  return typeof segment === "object" && segment !== null && "key" in segment;
3758
4637
  }
3759
- function normalizeYupIssues(error) {
3760
- if (!isYupValidationError(error)) {
4638
+ function normalizeYupIssues(error2) {
4639
+ if (!isYupValidationError(error2)) {
3761
4640
  return [{ message: "Validation failed" }];
3762
4641
  }
3763
- const issues = error.inner?.length ? error.inner : [error];
4642
+ const issues = error2.inner?.length ? error2.inner : [error2];
3764
4643
  return issues.map((issue) => ({
3765
4644
  message: issue.message,
3766
4645
  path: parsePathString(issue.path)
@@ -3811,11 +4690,11 @@ function normalizeSuperstructPath(failure) {
3811
4690
  if (typeof failure.key === "string" || typeof failure.key === "number") return [failure.key];
3812
4691
  return void 0;
3813
4692
  }
3814
- function normalizeVineError(error) {
3815
- if (!isVineValidationError(error)) {
4693
+ function normalizeVineError(error2) {
4694
+ if (!isVineValidationError(error2)) {
3816
4695
  return [{ message: "Validation failed" }];
3817
4696
  }
3818
- return error.messages.map((message) => ({
4697
+ return error2.messages.map((message) => ({
3819
4698
  message: message.message,
3820
4699
  path: normalizeVineField(message)
3821
4700
  }));
@@ -3838,14 +4717,14 @@ function parsePathString(path) {
3838
4717
  if (!path) return void 0;
3839
4718
  return path.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean).map((segment) => /^\d+$/.test(segment) ? Number(segment) : segment);
3840
4719
  }
3841
- function isYupValidationError(error) {
3842
- return typeof error === "object" && error !== null && "message" in error;
4720
+ function isYupValidationError(error2) {
4721
+ return typeof error2 === "object" && error2 !== null && "message" in error2;
3843
4722
  }
3844
4723
  function isArkTypeErrors(value) {
3845
4724
  return Array.isArray(value);
3846
4725
  }
3847
- function isVineValidationError(error) {
3848
- return typeof error === "object" && error !== null && "messages" in error && Array.isArray(error.messages);
4726
+ function isVineValidationError(error2) {
4727
+ return typeof error2 === "object" && error2 !== null && "messages" in error2 && Array.isArray(error2.messages);
3849
4728
  }
3850
4729
  function formatIssue(issue, key, location = "pointer") {
3851
4730
  const path = (issue.path ?? []).map(String);
@@ -3868,9 +4747,9 @@ function buildPointer(path) {
3868
4747
  // src/validation/model-router.ts
3869
4748
  var import_zod3 = require("zod");
3870
4749
  var listQuerySchema = import_zod3.z.object({
3871
- skip: positiveIntegerString.optional(),
4750
+ skip: nonNegativeIntegerString.optional(),
3872
4751
  limit: positiveIntegerString.optional(),
3873
- page: positiveIntegerString.optional(),
4752
+ page: nonNegativeIntegerString.optional(),
3874
4753
  page_size: positiveIntegerString.optional(),
3875
4754
  skim: queryBooleanString.optional(),
3876
4755
  include_permissions: queryBooleanString.optional(),
@@ -3899,10 +4778,10 @@ var listBodySchema = import_zod3.z.object({
3899
4778
  populate: populateSchema.optional(),
3900
4779
  include: includeSchema.optional(),
3901
4780
  tasks: tasksSchema.optional(),
3902
- skip: import_zod3.z.union([nonNegativeIntegerSchema, positiveIntegerString]).optional(),
3903
- limit: import_zod3.z.union([nonNegativeIntegerSchema, positiveIntegerString]).optional(),
3904
- page: import_zod3.z.union([nonNegativeIntegerSchema, positiveIntegerString]).optional(),
3905
- pageSize: import_zod3.z.union([nonNegativeIntegerSchema, positiveIntegerString]).optional(),
4781
+ skip: import_zod3.z.union([nonNegativeIntegerSchema, nonNegativeIntegerString]).optional(),
4782
+ limit: import_zod3.z.union([positiveIntegerSchema, positiveIntegerString]).optional(),
4783
+ page: import_zod3.z.union([nonNegativeIntegerSchema, nonNegativeIntegerString]).optional(),
4784
+ pageSize: import_zod3.z.union([positiveIntegerSchema, positiveIntegerString]).optional(),
3906
4785
  options: import_zod3.z.object({
3907
4786
  skim: import_zod3.z.boolean().optional(),
3908
4787
  includePermissions: import_zod3.z.boolean().optional(),
@@ -3912,11 +4791,8 @@ var listBodySchema = import_zod3.z.object({
3912
4791
  }).passthrough().optional()
3913
4792
  }).passthrough().superRefine((body, ctx) => rejectKeys(body, ctx, ["query"]));
3914
4793
  var countBodySchema = import_zod3.z.object({
3915
- filter: objectOrArraySchema.optional(),
3916
- options: import_zod3.z.object({
3917
- access: import_zod3.z.unknown().optional()
3918
- }).passthrough().optional()
3919
- }).passthrough().superRefine((body, ctx) => rejectKeys(body, ctx, ["query", "access"]));
4794
+ filter: objectOrArraySchema.optional()
4795
+ }).passthrough().superRefine((body, ctx) => rejectKeys(body, ctx, ["query", "access", "options"]));
3920
4796
  var readFilterBodySchema = import_zod3.z.object({
3921
4797
  filter: objectOrArraySchema.optional(),
3922
4798
  select: projectionSchema.optional(),
@@ -4010,10 +4886,10 @@ var dataListBodySchema = import_zod4.z.object({
4010
4886
  filter: objectOrArraySchema.optional(),
4011
4887
  select: projectionSchema.optional(),
4012
4888
  sort: import_zod4.z.string().optional(),
4013
- skip: import_zod4.z.union([nonNegativeIntegerSchema, positiveIntegerString]).optional(),
4014
- limit: import_zod4.z.union([nonNegativeIntegerSchema, positiveIntegerString]).optional(),
4015
- page: import_zod4.z.union([nonNegativeIntegerSchema, positiveIntegerString]).optional(),
4016
- pageSize: import_zod4.z.union([nonNegativeIntegerSchema, positiveIntegerString]).optional(),
4889
+ skip: import_zod4.z.union([nonNegativeIntegerSchema, nonNegativeIntegerString]).optional(),
4890
+ limit: import_zod4.z.union([positiveIntegerSchema, positiveIntegerString]).optional(),
4891
+ page: import_zod4.z.union([nonNegativeIntegerSchema, nonNegativeIntegerString]).optional(),
4892
+ pageSize: import_zod4.z.union([positiveIntegerSchema, positiveIntegerString]).optional(),
4017
4893
  options: import_zod4.z.object({
4018
4894
  includeCount: import_zod4.z.boolean().optional(),
4019
4895
  includeExtraHeaders: import_zod4.z.boolean().optional()
@@ -4032,17 +4908,17 @@ var import_zod5 = require("zod");
4032
4908
  var rootEntryBaseSchema = {
4033
4909
  target: import_zod5.z.enum(["model", "data"]),
4034
4910
  name: import_zod5.z.string().min(1),
4035
- order: import_zod5.z.number().int().optional()
4911
+ order: nonNegativeIntegerSchema.optional()
4036
4912
  };
4037
4913
  var rootModelListArgsSchema = import_zod5.z.object({
4038
4914
  select: projectionSchema.optional(),
4039
4915
  populate: populateSchema.optional(),
4040
4916
  include: includeSchema.optional(),
4041
4917
  sort: sortSchema.optional(),
4042
- skip: import_zod5.z.union([nonNegativeIntegerSchema, positiveIntegerString]).optional(),
4043
- limit: import_zod5.z.union([nonNegativeIntegerSchema, positiveIntegerString]).optional(),
4044
- page: import_zod5.z.union([nonNegativeIntegerSchema, positiveIntegerString]).optional(),
4045
- pageSize: import_zod5.z.union([nonNegativeIntegerSchema, positiveIntegerString]).optional(),
4918
+ skip: import_zod5.z.union([nonNegativeIntegerSchema, nonNegativeIntegerString]).optional(),
4919
+ limit: import_zod5.z.union([positiveIntegerSchema, positiveIntegerString]).optional(),
4920
+ page: import_zod5.z.union([nonNegativeIntegerSchema, nonNegativeIntegerString]).optional(),
4921
+ pageSize: import_zod5.z.union([positiveIntegerSchema, positiveIntegerString]).optional(),
4046
4922
  tasks: tasksSchema.optional()
4047
4923
  }).passthrough();
4048
4924
  var rootModelListOptionsSchema = import_zod5.z.object({
@@ -4098,16 +4974,14 @@ var rootModelSubReadArgsSchema = import_zod5.z.object({
4098
4974
  select: fieldsSchema.optional(),
4099
4975
  populate: subPopulateSchema.optional()
4100
4976
  }).passthrough();
4101
- var rootModelCountOptionsSchema = import_zod5.z.object({
4102
- access: import_zod5.z.unknown().optional()
4103
- }).passthrough();
4977
+ var rootModelCountOptionsSchema = import_zod5.z.object({}).strict().optional();
4104
4978
  var rootDataListArgsSchema = import_zod5.z.object({
4105
4979
  select: projectionSchema.optional(),
4106
4980
  sort: import_zod5.z.string().optional(),
4107
- skip: import_zod5.z.union([nonNegativeIntegerSchema, positiveIntegerString]).optional(),
4108
- limit: import_zod5.z.union([nonNegativeIntegerSchema, positiveIntegerString]).optional(),
4109
- page: import_zod5.z.union([nonNegativeIntegerSchema, positiveIntegerString]).optional(),
4110
- pageSize: import_zod5.z.union([nonNegativeIntegerSchema, positiveIntegerString]).optional()
4981
+ skip: import_zod5.z.union([nonNegativeIntegerSchema, nonNegativeIntegerString]).optional(),
4982
+ limit: import_zod5.z.union([positiveIntegerSchema, positiveIntegerString]).optional(),
4983
+ page: import_zod5.z.union([nonNegativeIntegerSchema, nonNegativeIntegerString]).optional(),
4984
+ pageSize: import_zod5.z.union([positiveIntegerSchema, positiveIntegerString]).optional()
4111
4985
  }).passthrough();
4112
4986
  var rootDataListOptionsSchema = import_zod5.z.object({
4113
4987
  includeCount: import_zod5.z.boolean().optional()
@@ -4115,8 +4989,21 @@ var rootDataListOptionsSchema = import_zod5.z.object({
4115
4989
  var rootDataReadArgsSchema = import_zod5.z.object({
4116
4990
  select: projectionSchema.optional()
4117
4991
  }).passthrough();
4992
+ var rootModelNewArgsSchema = import_zod5.z.object({
4993
+ select: projectionSchema.optional()
4994
+ }).passthrough();
4995
+ var rootModelNewOptionsSchema = import_zod5.z.object({
4996
+ skim: import_zod5.z.boolean().optional(),
4997
+ includePermissions: import_zod5.z.boolean().optional()
4998
+ }).passthrough();
4118
4999
  var rootModelQueryEntrySchema = import_zod5.z.union([
4119
- import_zod5.z.object({ ...rootEntryBaseSchema, target: import_zod5.z.literal("model"), op: import_zod5.z.literal("new") }).passthrough(),
5000
+ import_zod5.z.object({
5001
+ ...rootEntryBaseSchema,
5002
+ target: import_zod5.z.literal("model"),
5003
+ op: import_zod5.z.literal("new"),
5004
+ args: rootModelNewArgsSchema.optional(),
5005
+ options: rootModelNewOptionsSchema.optional()
5006
+ }).passthrough(),
4120
5007
  import_zod5.z.object({
4121
5008
  ...rootEntryBaseSchema,
4122
5009
  target: import_zod5.z.literal("model"),
@@ -4231,7 +5118,7 @@ var rootModelQueryEntrySchema = import_zod5.z.union([
4231
5118
  op: import_zod5.z.literal("count"),
4232
5119
  filter: objectOrArraySchema.optional(),
4233
5120
  options: rootModelCountOptionsSchema.optional()
4234
- }).passthrough()
5121
+ }).passthrough().superRefine((body, ctx) => rejectKeys(body, ctx, ["access"]))
4235
5122
  ]);
4236
5123
  var rootDataQueryEntrySchema = import_zod5.z.union([
4237
5124
  import_zod5.z.object({
@@ -4260,7 +5147,7 @@ var rootDataQueryEntrySchema = import_zod5.z.union([
4260
5147
  var rootQuerySchema = import_zod5.z.array(import_zod5.z.union([rootModelQueryEntrySchema, rootDataQueryEntrySchema]));
4261
5148
 
4262
5149
  // src/routers/shared.ts
4263
- var import_utils22 = require("@web-ts-toolkit/utils");
5150
+ var import_utils23 = require("@web-ts-toolkit/utils");
4264
5151
 
4265
5152
  // src/routers/model-router-collection-routes.ts
4266
5153
  function setModelCollectionRoutes(context) {
@@ -4278,14 +5165,14 @@ function setModelCollectionRoutes(context) {
4278
5165
  await context.assertAllowed(req, "list");
4279
5166
  const { skip, limit, page, page_size, skim, include_permissions, include_count, include_extra_headers } = parseQuery(requestSchemas.listQuery, req.query);
4280
5167
  const svc = context.getPublicService(req);
4281
- const includeCount = (0, import_utils22.parseBooleanString)(include_count);
4282
- const includeExtraHeaders = (0, import_utils22.parseBooleanString)(include_extra_headers);
5168
+ const includeCount = (0, import_utils23.parseBooleanString)(include_count);
5169
+ const includeExtraHeaders = (0, import_utils23.parseBooleanString)(include_extra_headers);
4283
5170
  const result = await svc._list(
4284
5171
  {},
4285
5172
  { skip, limit, page, pageSize: page_size },
4286
5173
  {
4287
- skim: (0, import_utils22.parseBooleanString)(skim),
4288
- includePermissions: (0, import_utils22.parseBooleanString)(include_permissions),
5174
+ skim: (0, import_utils23.parseBooleanString)(skim),
5175
+ includePermissions: (0, import_utils23.parseBooleanString)(include_permissions),
4289
5176
  includeCount
4290
5177
  }
4291
5178
  );
@@ -4340,7 +5227,7 @@ function setModelCollectionRoutes(context) {
4340
5227
  context.getRequestSchema("requestSchemas.create")
4341
5228
  );
4342
5229
  const svc = context.getPublicService(req);
4343
- const result = await svc._create(data, {}, { includePermissions: (0, import_utils22.parseBooleanString)(include_permissions) });
5230
+ const result = await svc._create(data, {}, { includePermissions: (0, import_utils23.parseBooleanString)(include_permissions) });
4344
5231
  handleResultError(result);
4345
5232
  return formatModelCreatedResponse(result);
4346
5233
  });
@@ -4374,7 +5261,7 @@ function setModelCollectionRoutes(context) {
4374
5261
  data,
4375
5262
  { select, populate, tasks },
4376
5263
  {
4377
- includePermissions: includePermissions ?? (0, import_utils22.parseBooleanString)(include_permissions),
5264
+ includePermissions: includePermissions ?? (0, import_utils23.parseBooleanString)(include_permissions),
4378
5265
  populateAccess
4379
5266
  }
4380
5267
  );
@@ -4390,8 +5277,9 @@ function setModelCollectionRoutes(context) {
4390
5277
  body: defineOpenApiSchemaResolver(getAdvancedCreateOpenApiBody)
4391
5278
  });
4392
5279
  router.get("/new", async (req) => {
5280
+ await context.assertAllowed(req, "new");
4393
5281
  const svc = context.getPublicService(req);
4394
- const result = await svc._new();
5282
+ const result = await svc._new({}, { includePermissions: false });
4395
5283
  handleResultError(result);
4396
5284
  return unwrapServiceData(result);
4397
5285
  });
@@ -4439,16 +5327,13 @@ function setModelDocumentRoutes(context) {
4439
5327
  });
4440
5328
  router.post("/count", async (req) => {
4441
5329
  await context.assertAllowed(req, "count");
4442
- const { filter: filter2, options: countOptions = {} } = await parseBodyWithSchema(
5330
+ const { filter: filter2 } = await parseBodyWithSchema(
4443
5331
  countBodySchema,
4444
5332
  req.body,
4445
5333
  context.getRequestSchema("requestSchemas.count")
4446
5334
  );
4447
5335
  const svc = context.getPublicService(req);
4448
- const result = await svc._count(
4449
- filter2 ?? {},
4450
- countOptions.access
4451
- );
5336
+ const result = await svc._count(filter2 ?? {});
4452
5337
  handleResultError(result);
4453
5338
  return unwrapServiceData(result);
4454
5339
  });
@@ -4468,8 +5353,8 @@ function setModelDocumentRoutes(context) {
4468
5353
  id,
4469
5354
  {},
4470
5355
  {
4471
- includePermissions: (0, import_utils22.parseBooleanString)(include_permissions),
4472
- tryList: (0, import_utils22.parseBooleanString)(try_list)
5356
+ includePermissions: (0, import_utils23.parseBooleanString)(include_permissions),
5357
+ tryList: (0, import_utils23.parseBooleanString)(try_list)
4473
5358
  }
4474
5359
  );
4475
5360
  handleResultError(result);
@@ -4554,8 +5439,8 @@ function setModelDocumentRoutes(context) {
4554
5439
  data,
4555
5440
  {},
4556
5441
  {
4557
- returningAll: (0, import_utils22.parseBooleanString)(returning_all),
4558
- includePermissions: (0, import_utils22.parseBooleanString)(include_permissions)
5442
+ returningAll: (0, import_utils23.parseBooleanString)(returning_all),
5443
+ includePermissions: (0, import_utils23.parseBooleanString)(include_permissions)
4559
5444
  }
4560
5445
  );
4561
5446
  handleResultError(result);
@@ -4593,8 +5478,8 @@ function setModelDocumentRoutes(context) {
4593
5478
  data,
4594
5479
  { select, populate, tasks },
4595
5480
  {
4596
- returningAll: returningAll ?? (0, import_utils22.parseBooleanString)(returning_all),
4597
- includePermissions: includePermissions ?? (0, import_utils22.parseBooleanString)(include_permissions),
5481
+ returningAll: returningAll ?? (0, import_utils23.parseBooleanString)(returning_all),
5482
+ includePermissions: includePermissions ?? (0, import_utils23.parseBooleanString)(include_permissions),
4598
5483
  populateAccess
4599
5484
  }
4600
5485
  );
@@ -4622,8 +5507,8 @@ function setModelDocumentRoutes(context) {
4622
5507
  body,
4623
5508
  {},
4624
5509
  {
4625
- returningAll: (0, import_utils22.parseBooleanString)(returning_all),
4626
- includePermissions: (0, import_utils22.parseBooleanString)(include_permissions)
5510
+ returningAll: (0, import_utils23.parseBooleanString)(returning_all),
5511
+ includePermissions: (0, import_utils23.parseBooleanString)(include_permissions)
4627
5512
  }
4628
5513
  );
4629
5514
  handleResultError(result);
@@ -4659,8 +5544,8 @@ function setModelDocumentRoutes(context) {
4659
5544
  data,
4660
5545
  { select, populate, tasks },
4661
5546
  {
4662
- returningAll: returningAll ?? (0, import_utils22.parseBooleanString)(returning_all),
4663
- includePermissions: includePermissions ?? (0, import_utils22.parseBooleanString)(include_permissions),
5547
+ returningAll: returningAll ?? (0, import_utils23.parseBooleanString)(returning_all),
5548
+ includePermissions: includePermissions ?? (0, import_utils23.parseBooleanString)(include_permissions),
4664
5549
  populateAccess
4665
5550
  }
4666
5551
  );
@@ -4726,7 +5611,6 @@ function setModelDocumentRoutes(context) {
4726
5611
  }
4727
5612
 
4728
5613
  // src/routers/model-router-subdocument-routes.ts
4729
- var import_utils23 = require("@web-ts-toolkit/utils");
4730
5614
  function setModelSubDocumentRoutes(context) {
4731
5615
  const subs = getModelSub(context.modelName);
4732
5616
  for (let x = 0; x < subs.length; x++) {
@@ -4756,7 +5640,10 @@ function setModelSubDocumentRoutes(context) {
4756
5640
  context.getRequestSchema("requestSchemas.subList")
4757
5641
  );
4758
5642
  const svc = context.getPublicService(req);
4759
- const result = await svc.listSub(id, sub, { filter: body.filter ?? {}, select: body.select ?? [] });
5643
+ const result = await svc.listSub(id, sub, {
5644
+ filter: body.filter ?? {},
5645
+ select: body.select ?? []
5646
+ });
4760
5647
  handleResultError(result);
4761
5648
  return unwrapServiceData(result);
4762
5649
  }
@@ -4816,10 +5703,11 @@ function setModelSubDocumentRoutes(context) {
4816
5703
  req.body,
4817
5704
  context.getRequestSchema("requestSchemas.subRead")
4818
5705
  );
4819
- const populate = body.populate;
4820
- const normalizedPopulate = Array.isArray(populate) ? populate.map((item) => (0, import_utils23.isString)(item) ? { path: item } : item) : (0, import_utils23.isString)(populate) ? { path: populate } : populate ?? [];
4821
5706
  const svc = context.getPublicService(req);
4822
- const result = await svc.readSub(id, sub, subId, { select: body.select ?? [], populate: normalizedPopulate });
5707
+ const result = await svc.readSub(id, sub, subId, {
5708
+ select: body.select ?? [],
5709
+ populate: normalizeSubPopulate(body.populate)
5710
+ });
4823
5711
  handleResultError(result);
4824
5712
  return unwrapServiceData(result);
4825
5713
  }
@@ -4913,7 +5801,7 @@ function setOption(parentKey, optionKey, option) {
4913
5801
  this.runtime.setModelOption(this.modelName, key, value);
4914
5802
  return this;
4915
5803
  }
4916
- var ModelRouter = class {
5804
+ var ModelRouter = class _ModelRouter {
4917
5805
  constructor(modelName, initialOptions, runtime = defaultRuntime) {
4918
5806
  /**
4919
5807
  * The maximum limit of the number of documents returned from the `list` operation.
@@ -5067,17 +5955,21 @@ var ModelRouter = class {
5067
5955
  );
5068
5956
  this.runtime = runtime;
5069
5957
  this.runtime.setModelOptions(modelName, initialOptions);
5070
- attachRuntimeToModel(modelName, this.runtime);
5071
5958
  this.options = this.runtime.getModelOptions(modelName);
5072
5959
  this.fullBasePath = (0, import_utils25.normalizeUrlPath)(this.options.parentPath + this.options.basePath);
5073
5960
  this.modelName = modelName;
5074
5961
  this.router = new import_express_json_router5.default(this.options.basePath, createSetCore(this.runtime), accessRouterResponseHandler);
5075
- this.model = new model_default(modelName);
5962
+ this.model = new model_default(modelName, this.runtime);
5076
5963
  this.setCollectionRoutes();
5077
5964
  this.setDocumentRoutes();
5078
5965
  this.setSubDocumentRoutes();
5079
5966
  this.logEndpoints();
5080
5967
  }
5968
+ static fromModel(model, initialOptions, runtime = defaultRuntime) {
5969
+ const modelName = String(model.modelName);
5970
+ runtime.registerModelInstance(modelName, model);
5971
+ return new _ModelRouter(modelName, initialOptions, runtime);
5972
+ }
5081
5973
  getRequestSchema(key) {
5082
5974
  return this.runtime.getExactModelOption(
5083
5975
  this.modelName,
@@ -5141,8 +6033,8 @@ var ModelRouter = class {
5141
6033
  }
5142
6034
  logEndpoints() {
5143
6035
  runWithRuntime(this.runtime, () => {
5144
- (0, import_utils25.forEach)(this.router.endpoints, ({ method, path }) => {
5145
- logger.info(`${(0, import_utils25.padEnd)(method, 6)} ${(0, import_utils25.normalizeUrlPath)(this.options.parentPath + path)}`);
6036
+ (0, import_utils25.forEach)(this.router.getEndpoints(), ({ method, path }) => {
6037
+ logInfoMessage(`${(0, import_utils25.padEnd)(method, 6)} ${(0, import_utils25.normalizeUrlPath)(this.options.parentPath + path)}`);
5146
6038
  });
5147
6039
  });
5148
6040
  }
@@ -5346,16 +6238,19 @@ async function decorateDataSingleResult(svc, result, context = {}) {
5346
6238
 
5347
6239
  // src/routers/root-router.ts
5348
6240
  var clientErrors3 = import_express_json_router6.default.clientErrors;
6241
+ var DEFAULT_MAX_BATCH_ENTRIES = 100;
6242
+ var DEFAULT_MAX_ORDER_GROUPS = 25;
6243
+ var DEFAULT_MAX_CONCURRENT_OPERATIONS = 10;
5349
6244
  var createErrorResult = (code, detail) => ({
5350
6245
  success: false,
6246
+ kind: "error",
5351
6247
  code,
5352
6248
  errors: [{ detail }]
5353
6249
  });
5354
- var normalizeSubPopulate = (populate) => Array.isArray(populate) ? populate.map((item) => (0, import_utils26.isString)(item) ? { path: item } : item) : (0, import_utils26.isString)(populate) ? { path: populate } : populate ?? [];
5355
6250
  var RootRouter = class {
5356
6251
  constructor(options = { basePath: "", operationAccess: true }, runtime = defaultRuntime) {
5357
6252
  this.modelOperations = {
5358
- new: async (req, item) => req.macl.getPublicService(item.name)._new(),
6253
+ new: async (req, item) => req.macl.getPublicService(item.name)._new(item.args, item.options),
5359
6254
  list: async (req, item) => req.macl.getPublicService(item.name)._list(item.filter ?? {}, item.args, item.options),
5360
6255
  read: async (req, item) => {
5361
6256
  const svc = req.macl.getPublicService(item.name);
@@ -5378,7 +6273,7 @@ var RootRouter = class {
5378
6273
  subBulkUpdate: async (req, item) => req.macl.getPublicService(item.name).bulkUpdateSub(item.id, item.sub, item.data),
5379
6274
  subDelete: async (req, item) => req.macl.getPublicService(item.name).deleteSub(item.id, item.sub, item.subId),
5380
6275
  distinct: async (req, item) => req.macl.getPublicService(item.name)._distinct(item.field, { filter: item.filter }),
5381
- count: async (req, item) => req.macl.getPublicService(item.name)._count(item.filter ?? {}, item.options?.access)
6276
+ count: async (req, item) => req.macl.getPublicService(item.name)._count(item.filter ?? {})
5382
6277
  };
5383
6278
  this.dataOperations = {
5384
6279
  list: async (req, item) => {
@@ -5413,10 +6308,16 @@ var RootRouter = class {
5413
6308
  return decorateDataSingleResult(svc, result, { dataName: item.name });
5414
6309
  }
5415
6310
  };
5416
- const { basePath, operationAccess } = options;
6311
+ const { basePath, operationAccess, maxBatchEntries, maxOrderGroups, maxConcurrentOperations } = options;
5417
6312
  this.runtime = runtime;
5418
6313
  this.basename = basePath || "";
5419
6314
  this.operationAccess = operationAccess;
6315
+ this.maxBatchEntries = this.normalizePositiveLimit(maxBatchEntries, DEFAULT_MAX_BATCH_ENTRIES);
6316
+ this.maxOrderGroups = this.normalizePositiveLimit(maxOrderGroups, DEFAULT_MAX_ORDER_GROUPS);
6317
+ this.maxConcurrentOperations = this.normalizePositiveLimit(
6318
+ maxConcurrentOperations,
6319
+ DEFAULT_MAX_CONCURRENT_OPERATIONS
6320
+ );
5420
6321
  this.router = new import_express_json_router6.default(
5421
6322
  this.basename,
5422
6323
  [createSetCore(this.runtime), createSetDataCore(this.runtime)],
@@ -5433,7 +6334,7 @@ var RootRouter = class {
5433
6334
  target: item.target,
5434
6335
  name: item.name,
5435
6336
  op: item.op,
5436
- result,
6337
+ result: toPublicServiceResult(result),
5437
6338
  message: mapCodeToMessage(result.code),
5438
6339
  statusCode: mapCodeToStatusCode(result.code)
5439
6340
  };
@@ -5484,17 +6385,63 @@ var RootRouter = class {
5484
6385
  return this.wrapResult(index, item, await handler(req, item));
5485
6386
  }
5486
6387
  groupItemsByOrder(items) {
5487
- return items.reduce((acc, item, index) => {
5488
- let order = 0;
5489
- if ((0, import_utils26.isNumber)(item.order)) {
5490
- order = item.order < 0 ? 0 : item.order;
6388
+ const groups = /* @__PURE__ */ new Map();
6389
+ items.forEach((item, index) => {
6390
+ const order = (0, import_utils26.isNumber)(item.order) ? item.order : 0;
6391
+ const group = groups.get(order);
6392
+ if (group) {
6393
+ group.push({ index, item });
6394
+ return;
6395
+ }
6396
+ groups.set(order, [{ index, item }]);
6397
+ });
6398
+ return Array.from(groups.entries()).sort(([leftOrder], [rightOrder]) => leftOrder - rightOrder).map(([, group]) => group);
6399
+ }
6400
+ normalizePositiveLimit(value, fallback) {
6401
+ return Number.isSafeInteger(value) && Number(value) > 0 ? Number(value) : fallback;
6402
+ }
6403
+ validateBatch(items) {
6404
+ if (items.length > this.maxBatchEntries) {
6405
+ throw new clientErrors3.BadRequestError("Bad Request", {
6406
+ errors: [{ detail: `Root batch exceeds maximum size of ${this.maxBatchEntries}` }]
6407
+ });
6408
+ }
6409
+ const orders = /* @__PURE__ */ new Set();
6410
+ for (const item of items) {
6411
+ const order = item.order ?? 0;
6412
+ if (!Number.isSafeInteger(order) || order < 0) {
6413
+ throw new clientErrors3.BadRequestError("Bad Request", {
6414
+ errors: [{ detail: "Root batch order must be a non-negative safe integer" }]
6415
+ });
5491
6416
  }
5492
- if (!acc[order]) {
5493
- acc[order] = [];
6417
+ if (order > this.maxOrderGroups) {
6418
+ throw new clientErrors3.BadRequestError("Bad Request", {
6419
+ errors: [{ detail: `Root batch order exceeds maximum group index of ${this.maxOrderGroups}` }]
6420
+ });
5494
6421
  }
5495
- acc[order].push({ index, item });
5496
- return acc;
5497
- }, []);
6422
+ orders.add(order);
6423
+ }
6424
+ if (orders.size > this.maxOrderGroups) {
6425
+ throw new clientErrors3.BadRequestError("Bad Request", {
6426
+ errors: [{ detail: `Root batch exceeds maximum order groups of ${this.maxOrderGroups}` }]
6427
+ });
6428
+ }
6429
+ }
6430
+ async runGroup(req, entries) {
6431
+ const results = new Array(entries.length);
6432
+ let cursor = 0;
6433
+ const workerCount = Math.min(this.maxConcurrentOperations, entries.length);
6434
+ await Promise.all(
6435
+ Array.from({ length: workerCount }, async () => {
6436
+ while (cursor < entries.length) {
6437
+ const current = cursor;
6438
+ cursor += 1;
6439
+ const { item, index } = entries[current];
6440
+ results[current] = await this.processOp(req, item, index);
6441
+ }
6442
+ })
6443
+ );
6444
+ return results;
5498
6445
  }
5499
6446
  async assertAllowed(req) {
5500
6447
  const allowed = await req.macl.canActivate(this.operationAccess);
@@ -5504,10 +6451,11 @@ var RootRouter = class {
5504
6451
  this.router.post("", async (req) => {
5505
6452
  await this.assertAllowed(req);
5506
6453
  const items = parseBody(rootQuerySchema, req.body);
6454
+ this.validateBatch(items);
5507
6455
  const groupedItems = this.groupItemsByOrder(items);
5508
6456
  const results = [];
5509
6457
  for (let x = 0; x < groupedItems.length; x++) {
5510
- const arrResult = await Promise.all(groupedItems[x].map(({ item, index }) => this.processOp(req, item, index)));
6458
+ const arrResult = await this.runGroup(req, groupedItems[x]);
5511
6459
  results.push(...arrResult);
5512
6460
  }
5513
6461
  return (0, import_utils26.orderBy)(results, ["index"], ["asc"]);
@@ -5517,7 +6465,8 @@ var RootRouter = class {
5517
6465
  path: "",
5518
6466
  operationId: "root.query",
5519
6467
  summary: "Execute batched model and data operations",
5520
- body: rootQuerySchema
6468
+ body: rootQuerySchema,
6469
+ idempotent: true
5521
6470
  });
5522
6471
  }
5523
6472
  get routes() {
@@ -5600,8 +6549,8 @@ var DataRouter = class {
5600
6549
  req.query
5601
6550
  );
5602
6551
  const svc = this.getService(req);
5603
- const includeCount = (0, import_utils22.parseBooleanString)(include_count);
5604
- const includeExtraHeaders = (0, import_utils22.parseBooleanString)(include_extra_headers);
6552
+ const includeCount = (0, import_utils23.parseBooleanString)(include_count);
6553
+ const includeExtraHeaders = (0, import_utils23.parseBooleanString)(include_extra_headers);
5605
6554
  const result = await svc.find(
5606
6555
  {},
5607
6556
  { skip, limit, page, pageSize: page_size },
@@ -5664,7 +6613,7 @@ var DataRouter = class {
5664
6613
  const result = await svc.findById(id, {}, {});
5665
6614
  handleResultError(result);
5666
6615
  const decoratedResult = await decorateDataSingleResult(svc, result);
5667
- return decoratedResult.data;
6616
+ return unwrapServiceData(decoratedResult);
5668
6617
  });
5669
6618
  this.registerOpenApiRoute({
5670
6619
  method: "get",
@@ -5683,7 +6632,7 @@ var DataRouter = class {
5683
6632
  const result = await svc.findOne(filter2 ?? {}, { select }, {});
5684
6633
  handleResultError(result);
5685
6634
  const decoratedResult = await decorateDataSingleResult(svc, result);
5686
- return decoratedResult.data;
6635
+ return unwrapServiceData(decoratedResult);
5687
6636
  });
5688
6637
  this.registerOpenApiRoute({
5689
6638
  method: "post",
@@ -5706,7 +6655,7 @@ var DataRouter = class {
5706
6655
  const result = await svc.findById(id, { select }, {});
5707
6656
  handleResultError(result);
5708
6657
  const decoratedResult = await decorateDataSingleResult(svc, result);
5709
- return decoratedResult.data;
6658
+ return unwrapServiceData(decoratedResult);
5710
6659
  });
5711
6660
  this.registerOpenApiRoute({
5712
6661
  method: "post",
@@ -5751,6 +6700,9 @@ var import_express_json_router8 = __toESM(require("@web-ts-toolkit/express-json-
5751
6700
  var import_express = __toESM(require("express"));
5752
6701
  var import_node_path = require("path");
5753
6702
  var escapeHtml = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
6703
+ var escapeInlineScriptJson = (value) => {
6704
+ return value.replace(/<\/script/gi, "<\\/script").replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
6705
+ };
5754
6706
  var renderDocsHtml = (specPath, title, cssUrl, bundleUrl) => `<!doctype html>
5755
6707
  <html lang="en">
5756
6708
  <head>
@@ -5764,7 +6716,7 @@ var renderDocsHtml = (specPath, title, cssUrl, bundleUrl) => `<!doctype html>
5764
6716
  <script src="${escapeHtml(bundleUrl)}"></script>
5765
6717
  <script>
5766
6718
  window.ui = SwaggerUIBundle({
5767
- url: ${JSON.stringify(specPath)},
6719
+ url: ${escapeInlineScriptJson(JSON.stringify(specPath))},
5768
6720
  dom_id: '#swagger-ui',
5769
6721
  deepLinking: true,
5770
6722
  presets: [SwaggerUIBundle.presets.apis],
@@ -5819,10 +6771,10 @@ var accessRouterResponseHandler = import_express_json_router8.default.createHand
5819
6771
  errorFormat: import_express_json_router8.default.ErrorFormats.rfc9457,
5820
6772
  rfc9457ContentType: "application/problem+json"
5821
6773
  });
5822
- accessRouterResponseHandler.errorMessageProvider = function(error) {
5823
- const errorLike = error;
5824
- logger.error(error);
5825
- return errorLike.message || errorLike._message || String(error);
6774
+ accessRouterResponseHandler.errorMessageProvider = function(error2) {
6775
+ const errorLike = error2;
6776
+ error(error2);
6777
+ return errorLike.message || errorLike._message || String(error2);
5826
6778
  };
5827
6779
 
5828
6780
  // src/plugins.ts
@@ -5835,6 +6787,7 @@ function permissionsPlugin(schema, options) {
5835
6787
  }
5836
6788
 
5837
6789
  // src/index.ts
6790
+ var isMongooseModel = (value) => typeof value === "function" && value !== null && "modelName" in value && "schema" in value;
5838
6791
  function createRuntimeApi(runtime) {
5839
6792
  const runtimeMiddleware = function() {
5840
6793
  return createSetCore(runtime);
@@ -5842,8 +6795,18 @@ function createRuntimeApi(runtime) {
5842
6795
  const wtt2 = runtimeMiddleware;
5843
6796
  wtt2.runtime = runtime;
5844
6797
  wtt2.createRouter = function(modelName, options) {
5845
- const resolvedModelName = typeof modelName === "string" ? modelName : modelName && "modelName" in modelName ? modelName.modelName : modelName;
5846
- return (0, import_utils28.isUndefined)(options) ? new RootRouter(modelName, runtime) : new ModelRouter(resolvedModelName, options, runtime);
6798
+ if ((0, import_utils28.isUndefined)(options)) {
6799
+ return new RootRouter(modelName, runtime);
6800
+ }
6801
+ if (typeof modelName === "string") {
6802
+ return new ModelRouter(modelName, options, runtime);
6803
+ }
6804
+ if (isMongooseModel(modelName)) {
6805
+ const name = String(modelName.modelName);
6806
+ runtime.registerModelInstance(name, modelName);
6807
+ return ModelRouter.fromModel(modelName, options, runtime);
6808
+ }
6809
+ throw new TypeError(`createRouter: unsupported first argument type: ${typeof modelName}`);
5847
6810
  };
5848
6811
  wtt2.createDataRouter = function(dataName, options) {
5849
6812
  return new DataRouter(dataName, options, runtime);
@@ -5874,6 +6837,9 @@ function createRuntimeApi(runtime) {
5874
6837
  wtt2.setDefaultModelOption = runtime.setDefaultModelOption.bind(runtime);
5875
6838
  wtt2.getDefaultModelOptions = runtime.getDefaultModelOptions.bind(runtime);
5876
6839
  wtt2.getDefaultModelOption = runtime.getDefaultModelOption.bind(runtime);
6840
+ wtt2.registerModelInstance = runtime.registerModelInstance.bind(runtime);
6841
+ wtt2.hasModelInstance = runtime.hasModelInstance.bind(runtime);
6842
+ wtt2.getModelInstance = runtime.getModelInstance.bind(runtime);
5877
6843
  wtt2.RootRouter = RootRouter;
5878
6844
  wtt2.ModelRouter = ModelRouter;
5879
6845
  wtt2.DataRouter = DataRouter;
@@ -5890,11 +6856,14 @@ var index_default = acl;
5890
6856
  AccessRuntime,
5891
6857
  DataRouter,
5892
6858
  ModelRouter,
6859
+ OpenApiCollisionError,
6860
+ OpenApiRegistry,
5893
6861
  RootRouter,
5894
6862
  acl,
5895
6863
  combineRoutes,
5896
6864
  createAccessRuntime,
5897
6865
  createOpenApiRouter,
6866
+ defaultRuntime,
5898
6867
  defineRequestSchema,
5899
6868
  fromAjv,
5900
6869
  fromArkType,
@@ -5910,12 +6879,19 @@ var index_default = acl;
5910
6879
  getDefaultModelOptions,
5911
6880
  getGlobalOption,
5912
6881
  getGlobalOptions,
6882
+ getModelInstance,
5913
6883
  getModelJsonSchema,
5914
6884
  getModelNames,
5915
6885
  getModelOption,
5916
6886
  getModelOptions,
5917
6887
  guard,
6888
+ hasModelInstance,
6889
+ isLevelEnabled,
5918
6890
  permissionsPlugin,
6891
+ redactFilter,
6892
+ redactPayload,
6893
+ registerModelInstance,
6894
+ safeStringify,
5919
6895
  setDefaultModelOption,
5920
6896
  setDefaultModelOptions,
5921
6897
  setGlobalOption,