@web-ts-toolkit/access-router 0.37.0 → 0.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -5
- package/advanced.d.mts +3 -3
- package/advanced.d.ts +3 -3
- package/advanced.js +170 -85
- package/advanced.mjs +144 -59
- package/index.d.mts +198 -3
- package/index.d.ts +198 -3
- package/index.js +519 -306
- package/index.mjs +540 -328
- package/llms.txt +25 -5
- package/package.json +3 -3
- package/{parsers--DZcTstL.d.mts → parsers-PMWujHUV.d.mts} +32 -204
- package/{parsers--DZcTstL.d.ts → parsers-PMWujHUV.d.ts} +32 -204
- package/processors.d.mts +20 -8
- package/processors.d.ts +20 -8
package/index.mjs
CHANGED
|
@@ -47,12 +47,42 @@ var findElementById = (collection, id) => {
|
|
|
47
47
|
return findElement(collection, { _id: id });
|
|
48
48
|
};
|
|
49
49
|
|
|
50
|
+
// src/helpers/concurrency.ts
|
|
51
|
+
var mapWithConcurrencyLimit = async (items, limit, iteratee) => {
|
|
52
|
+
const results = new Array(items.length);
|
|
53
|
+
let cursor = 0;
|
|
54
|
+
const workerCount = Math.min(Math.max(limit, 1), items.length || 1);
|
|
55
|
+
await Promise.all(
|
|
56
|
+
Array.from({ length: workerCount }, async () => {
|
|
57
|
+
while (cursor < items.length) {
|
|
58
|
+
const current = cursor;
|
|
59
|
+
cursor += 1;
|
|
60
|
+
results[current] = await iteratee(items[current], current);
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
);
|
|
64
|
+
return results;
|
|
65
|
+
};
|
|
66
|
+
var RequestConcurrencyScheduler = class {
|
|
67
|
+
constructor(limit) {
|
|
68
|
+
this.limit = Math.max(1, limit);
|
|
69
|
+
}
|
|
70
|
+
async map(items, iteratee) {
|
|
71
|
+
return mapWithConcurrencyLimit(items, this.limit, (item, index) => iteratee(item, index, true));
|
|
72
|
+
}
|
|
73
|
+
async run(iteratee) {
|
|
74
|
+
const [result] = await this.map([void 0], () => iteratee(true));
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
50
79
|
// src/helpers/document.ts
|
|
51
80
|
import { Document } from "mongoose";
|
|
52
81
|
import { get, isPlainObject as isPlainObject2, pick, set } from "@web-ts-toolkit/utils";
|
|
53
82
|
|
|
54
83
|
// src/helpers/query.ts
|
|
55
84
|
import { flattenDeep, isNil, isPlainObject, isString, reduce } from "@web-ts-toolkit/utils";
|
|
85
|
+
var DEFAULT_LIST_HARD_LIMIT = 1e3;
|
|
56
86
|
var normalizeSafeInteger = (value, min) => {
|
|
57
87
|
if (isNil(value)) return null;
|
|
58
88
|
const parsed = Number(value);
|
|
@@ -65,9 +95,10 @@ function genPagination({
|
|
|
65
95
|
page,
|
|
66
96
|
pageSize
|
|
67
97
|
}, hardLimit) {
|
|
98
|
+
const resolvedHardLimit = normalizeSafeInteger(hardLimit, 1) ?? DEFAULT_LIST_HARD_LIMIT;
|
|
68
99
|
let _skip = 0;
|
|
69
|
-
let _limit = normalizeSafeInteger(limit ?? pageSize, 1) ??
|
|
70
|
-
if (!Number.isSafeInteger(_limit) || _limit >
|
|
100
|
+
let _limit = normalizeSafeInteger(limit ?? pageSize, 1) ?? resolvedHardLimit;
|
|
101
|
+
if (!Number.isSafeInteger(_limit) || _limit > resolvedHardLimit) _limit = resolvedHardLimit;
|
|
71
102
|
const normalizedSkip = normalizeSafeInteger(skip, 0);
|
|
72
103
|
const normalizedPage = normalizeSafeInteger(page, 0);
|
|
73
104
|
if (normalizedSkip !== null) {
|
|
@@ -84,14 +115,6 @@ function genPagination({
|
|
|
84
115
|
}
|
|
85
116
|
return { skip: _skip, limit: _limit };
|
|
86
117
|
}
|
|
87
|
-
function parseSortString(sortString) {
|
|
88
|
-
if (!sortString) return { sortKey: "", sortOrder: "asc" };
|
|
89
|
-
if (sortString.startsWith("-")) {
|
|
90
|
-
return { sortKey: sortString.substring(1), sortOrder: "desc" };
|
|
91
|
-
} else {
|
|
92
|
-
return { sortKey: sortString, sortOrder: "asc" };
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
118
|
function normalizeSelect(select) {
|
|
96
119
|
if (Array.isArray(select)) return flattenDeep(select.map(normalizeSelect));
|
|
97
120
|
if (isPlainObject(select)) {
|
|
@@ -221,8 +244,63 @@ function handleResultError(result) {
|
|
|
221
244
|
}
|
|
222
245
|
}
|
|
223
246
|
|
|
247
|
+
// src/helpers/sort-policy.ts
|
|
248
|
+
var validSortOrders = [1, -1, "asc", "ascending", "desc", "descending"];
|
|
249
|
+
var fieldPathPattern = /^[A-Za-z_][A-Za-z0-9_$]*(\.[A-Za-z_][A-Za-z0-9_$]*)*$/;
|
|
250
|
+
var sortDirection = (order) => order === -1 || order === "desc" || order === "descending" ? "desc" : "asc";
|
|
251
|
+
var sortError = (detail) => ({ detail, pointer: "#/sort" });
|
|
252
|
+
function isValidFieldPath(field) {
|
|
253
|
+
return typeof field === "string" && fieldPathPattern.test(field);
|
|
254
|
+
}
|
|
255
|
+
function isFieldAllowed(field, allowedFields) {
|
|
256
|
+
return new Set(allowedFields.concat(["id", "_id"])).has(field);
|
|
257
|
+
}
|
|
258
|
+
function normalizeSort(sort) {
|
|
259
|
+
if (sort === null || sort === void 0 || sort === "") return { fields: [], errors: [] };
|
|
260
|
+
if (typeof sort === "string") {
|
|
261
|
+
const fields2 = sort.trim().split(/\s+/).filter(Boolean).map((raw) => {
|
|
262
|
+
const desc = raw.startsWith("-");
|
|
263
|
+
return { raw, field: desc ? raw.slice(1) : raw, direction: desc ? "desc" : "asc" };
|
|
264
|
+
});
|
|
265
|
+
const errors2 = fields2.filter(({ field }) => !isValidFieldPath(field)).map(({ raw }) => sortError(`Invalid sort field: ${raw}`));
|
|
266
|
+
return { fields: errors2.length > 0 ? [] : fields2.map(({ field, direction }) => ({ field, direction })), errors: errors2 };
|
|
267
|
+
}
|
|
268
|
+
const entries = sort instanceof Map ? Array.from(sort.entries()) : Array.isArray(sort) ? sort : Object.entries(sort);
|
|
269
|
+
const errors = [];
|
|
270
|
+
const fields = [];
|
|
271
|
+
for (const entry of entries) {
|
|
272
|
+
if (!Array.isArray(entry) || entry.length !== 2) {
|
|
273
|
+
errors.push(sortError("Invalid sort entry: expected [field, order]"));
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
const [field, order] = entry;
|
|
277
|
+
if (!isValidFieldPath(field)) {
|
|
278
|
+
errors.push(sortError(`Invalid sort field: ${String(field)}`));
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
if (!validSortOrders.includes(order)) {
|
|
282
|
+
errors.push(sortError(`Invalid sort order for field: ${field}`));
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
fields.push({ field, direction: sortDirection(order) });
|
|
286
|
+
}
|
|
287
|
+
return { fields: errors.length > 0 ? [] : fields, errors };
|
|
288
|
+
}
|
|
289
|
+
function validateSortFields(sort, allowedFields) {
|
|
290
|
+
const { fields, errors } = normalizeSort(sort);
|
|
291
|
+
if (errors.length > 0) return errors;
|
|
292
|
+
return fields.filter(({ field }) => !isFieldAllowed(field, allowedFields)).map(({ field }) => sortError(`Sort field is not allowed: ${field}`));
|
|
293
|
+
}
|
|
294
|
+
function normalizeSortForOrderBy(sort) {
|
|
295
|
+
const normalized = normalizeSort(sort);
|
|
296
|
+
return {
|
|
297
|
+
fields: normalized.fields.map(({ field }) => field),
|
|
298
|
+
orders: normalized.fields.map(({ direction }) => direction)
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
224
302
|
// src/helpers/index.ts
|
|
225
|
-
import { forEach, isArray as isArray2, isEmpty, isObject, isPlainObject as isPlainObject4, isString as isString3
|
|
303
|
+
import { forEach, isArray as isArray2, isEmpty, isObject, isPlainObject as isPlainObject4, isString as isString3 } from "@web-ts-toolkit/utils";
|
|
226
304
|
|
|
227
305
|
// src/lib.ts
|
|
228
306
|
import { Document as Document2, Schema } from "mongoose";
|
|
@@ -282,26 +360,33 @@ function buildSubPaths(schema) {
|
|
|
282
360
|
});
|
|
283
361
|
return subPaths;
|
|
284
362
|
}
|
|
285
|
-
async function iterateQuery(query, handler) {
|
|
363
|
+
async function iterateQuery(query, handler, scheduler, scheduled = false) {
|
|
286
364
|
if (!isPlainObject4(query)) return query;
|
|
287
365
|
if (!handler) return query;
|
|
288
366
|
const queryObject = query;
|
|
289
|
-
|
|
367
|
+
const entries = Object.entries(queryObject);
|
|
368
|
+
const mapEntry = async ([key, val]) => {
|
|
290
369
|
if (isPlainObject4(val)) {
|
|
291
370
|
const plainValue = val;
|
|
292
371
|
if (plainValue.$$sq) {
|
|
293
|
-
return handler(0 /* SubQuery */, plainValue.$$sq, key);
|
|
372
|
+
return scheduled || !scheduler ? handler(0 /* SubQuery */, plainValue.$$sq, key) : scheduler.run(() => Promise.resolve(handler(0 /* SubQuery */, plainValue.$$sq, key)));
|
|
294
373
|
} else if (plainValue.$$date) {
|
|
295
374
|
return handler(1 /* Date */, plainValue.$$date, key);
|
|
296
375
|
} else {
|
|
297
|
-
return iterateQuery(val, handler);
|
|
376
|
+
return iterateQuery(val, handler, scheduler, scheduled);
|
|
298
377
|
}
|
|
299
378
|
}
|
|
300
379
|
if (isArray2(val)) {
|
|
301
|
-
|
|
380
|
+
const items = val;
|
|
381
|
+
if (scheduler && !scheduled) {
|
|
382
|
+
return scheduler.map(items, (v) => iterateQuery(v, handler, scheduler, true));
|
|
383
|
+
}
|
|
384
|
+
return Promise.all(items.map((v) => iterateQuery(v, handler, scheduler, scheduled)));
|
|
302
385
|
}
|
|
303
386
|
return val;
|
|
304
|
-
}
|
|
387
|
+
};
|
|
388
|
+
const values = scheduler && !scheduled ? await scheduler.map(entries, mapEntry) : await Promise.all(entries.map(mapEntry));
|
|
389
|
+
return Object.fromEntries(entries.map(([key], index) => [key, values[index]]));
|
|
305
390
|
}
|
|
306
391
|
var createValidator = (fn) => {
|
|
307
392
|
const stringHandler = (key) => key.trim().split(" ").every((v) => fn(v));
|
|
@@ -667,6 +752,42 @@ var defaultLogger = createLogger({
|
|
|
667
752
|
|
|
668
753
|
// src/options/manager.ts
|
|
669
754
|
import { assign, get as get2, set as set2 } from "@web-ts-toolkit/utils";
|
|
755
|
+
var toPath = (key) => {
|
|
756
|
+
if (Array.isArray(key)) return key;
|
|
757
|
+
return typeof key === "string" ? key.split(".") : [key];
|
|
758
|
+
};
|
|
759
|
+
var cloneConfigValue = (value) => {
|
|
760
|
+
if (Array.isArray(value)) {
|
|
761
|
+
return value.map((item) => cloneConfigValue(item));
|
|
762
|
+
}
|
|
763
|
+
if (!value || typeof value !== "object") {
|
|
764
|
+
return value;
|
|
765
|
+
}
|
|
766
|
+
if (Object.getPrototypeOf(value) !== Object.prototype) {
|
|
767
|
+
return value;
|
|
768
|
+
}
|
|
769
|
+
return Object.fromEntries(
|
|
770
|
+
Object.entries(value).map(([entryKey, entryValue]) => [
|
|
771
|
+
entryKey,
|
|
772
|
+
cloneConfigValue(entryValue)
|
|
773
|
+
])
|
|
774
|
+
);
|
|
775
|
+
};
|
|
776
|
+
var freezeConfigValue = (value) => {
|
|
777
|
+
if (!value || typeof value !== "object") {
|
|
778
|
+
return value;
|
|
779
|
+
}
|
|
780
|
+
if (Object.isFrozen(value)) {
|
|
781
|
+
return value;
|
|
782
|
+
}
|
|
783
|
+
if (Array.isArray(value) || Object.getPrototypeOf(value) === Object.prototype) {
|
|
784
|
+
for (const nestedValue of Object.values(value)) {
|
|
785
|
+
freezeConfigValue(nestedValue);
|
|
786
|
+
}
|
|
787
|
+
return Object.freeze(value);
|
|
788
|
+
}
|
|
789
|
+
return value;
|
|
790
|
+
};
|
|
670
791
|
var getNestedOption = (manager, key, defaultValue) => {
|
|
671
792
|
const keys2 = String(key).split(".");
|
|
672
793
|
if (keys2.length === 1) {
|
|
@@ -684,8 +805,9 @@ var getNestedOption = (manager, key, defaultValue) => {
|
|
|
684
805
|
return option;
|
|
685
806
|
};
|
|
686
807
|
var OptionsManager = class {
|
|
687
|
-
constructor(defaultOptions) {
|
|
808
|
+
constructor(defaultOptions, options = {}) {
|
|
688
809
|
this.defaultOptions = defaultOptions;
|
|
810
|
+
this.preserveKeys = new Set(options.preserveKeys ?? []);
|
|
689
811
|
this.listeners = {};
|
|
690
812
|
const _this = this;
|
|
691
813
|
this.currentOptions = new Proxy({}, {
|
|
@@ -706,13 +828,34 @@ var OptionsManager = class {
|
|
|
706
828
|
return get2(this.currentOptions, key, defaultValue);
|
|
707
829
|
}
|
|
708
830
|
set(key, value) {
|
|
709
|
-
|
|
831
|
+
const path = toPath(key);
|
|
832
|
+
if (path.length <= 1) {
|
|
833
|
+
set2(this.currentOptions, path, this.preserveKeys.has(String(path[0])) ? value : cloneConfigValue(value));
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
const [rootKey, ...nestedPath] = path;
|
|
837
|
+
const currentRoot = get2(this.currentOptions, [rootKey], void 0);
|
|
838
|
+
const nextRoot = cloneConfigValue(currentRoot ?? {});
|
|
839
|
+
set2(nextRoot, nestedPath, cloneConfigValue(value));
|
|
840
|
+
set2(this.currentOptions, [rootKey], nextRoot);
|
|
710
841
|
}
|
|
711
842
|
fetch() {
|
|
712
|
-
|
|
843
|
+
const cloned = cloneConfigValue(this.currentOptions);
|
|
844
|
+
for (const key of this.preserveKeys) {
|
|
845
|
+
if (key in this.currentOptions) {
|
|
846
|
+
cloned[key] = this.currentOptions[key];
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
return freezeConfigValue(cloned);
|
|
713
850
|
}
|
|
714
851
|
assign(options) {
|
|
715
|
-
|
|
852
|
+
const cloned = cloneConfigValue(options);
|
|
853
|
+
for (const key of this.preserveKeys) {
|
|
854
|
+
if (key in options) {
|
|
855
|
+
cloned[key] = options[key];
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
assign(this.currentOptions, cloned);
|
|
716
859
|
}
|
|
717
860
|
onchange(key, func) {
|
|
718
861
|
set2(this.listeners, key, func);
|
|
@@ -730,7 +873,8 @@ var defaultRequestComplexity = {
|
|
|
730
873
|
maxBulkItems: 100,
|
|
731
874
|
maxIncludeCount: 10,
|
|
732
875
|
maxSubQueryCount: 10,
|
|
733
|
-
maxBulkConcurrency: 10
|
|
876
|
+
maxBulkConcurrency: 10,
|
|
877
|
+
maxHookConcurrency: 10
|
|
734
878
|
};
|
|
735
879
|
var normalizePositiveInteger = (value, fallback) => {
|
|
736
880
|
return Number.isSafeInteger(value) && Number(value) > 0 ? Number(value) : fallback;
|
|
@@ -746,6 +890,10 @@ var resolveRequestComplexity = (options) => ({
|
|
|
746
890
|
maxBulkConcurrency: normalizePositiveInteger(
|
|
747
891
|
options?.maxBulkConcurrency,
|
|
748
892
|
defaultRequestComplexity.maxBulkConcurrency
|
|
893
|
+
),
|
|
894
|
+
maxHookConcurrency: normalizePositiveInteger(
|
|
895
|
+
options?.maxHookConcurrency,
|
|
896
|
+
defaultRequestComplexity.maxHookConcurrency
|
|
749
897
|
)
|
|
750
898
|
});
|
|
751
899
|
function validateRequestComplexity(value, options, scope = "request") {
|
|
@@ -839,7 +987,8 @@ var defaultModelOptions = {
|
|
|
839
987
|
var defaultDataOptions = {
|
|
840
988
|
basePath: null,
|
|
841
989
|
parentPath: "/",
|
|
842
|
-
queryRouteSegment: "__query"
|
|
990
|
+
queryRouteSegment: "__query",
|
|
991
|
+
listHardLimit: DEFAULT_LIST_HARD_LIMIT
|
|
843
992
|
};
|
|
844
993
|
var normalizeBasePath = (name, value) => {
|
|
845
994
|
if (isNil2(value)) {
|
|
@@ -896,17 +1045,30 @@ var classifyPermissionSchema = (permissionSchema, modelPermissionPrefix) => {
|
|
|
896
1045
|
modelPermissionKeys
|
|
897
1046
|
};
|
|
898
1047
|
};
|
|
1048
|
+
var refreshPermissionMetadata = (target) => {
|
|
1049
|
+
const permissionSchema = target.permissionSchema ?? {};
|
|
1050
|
+
const { schemaKeys, globalPermissionKeys, modelPermissionKeys } = classifyPermissionSchema(
|
|
1051
|
+
permissionSchema,
|
|
1052
|
+
target.modelPermissionPrefix ?? ""
|
|
1053
|
+
);
|
|
1054
|
+
target._permissionSchemaKeys = schemaKeys;
|
|
1055
|
+
target._globalPermissionKeys = globalPermissionKeys;
|
|
1056
|
+
target._modelPermissionKeys = modelPermissionKeys;
|
|
1057
|
+
};
|
|
899
1058
|
var AccessRuntime = class {
|
|
900
|
-
constructor() {
|
|
901
|
-
this.globalOptions = new OptionsManager(
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
1059
|
+
constructor(options = {}) {
|
|
1060
|
+
this.globalOptions = new OptionsManager(
|
|
1061
|
+
{
|
|
1062
|
+
requestPermissionField: "_permissions",
|
|
1063
|
+
globalPermissions: () => ({}),
|
|
1064
|
+
requireRegisteredPopulateModels: true,
|
|
1065
|
+
logger: defaultLogger,
|
|
1066
|
+
requestComplexity: defaultRequestComplexity
|
|
1067
|
+
},
|
|
1068
|
+
{ preserveKeys: ["logger"] }
|
|
1069
|
+
).build();
|
|
908
1070
|
this.defaultModelOptions = new OptionsManager({
|
|
909
|
-
listHardLimit:
|
|
1071
|
+
listHardLimit: DEFAULT_LIST_HARD_LIMIT,
|
|
910
1072
|
documentPermissionField: "_permissions",
|
|
911
1073
|
idParam: "id",
|
|
912
1074
|
idField: "_id",
|
|
@@ -924,6 +1086,7 @@ var AccessRuntime = class {
|
|
|
924
1086
|
this.modelAtts = {};
|
|
925
1087
|
this.modelInstances = {};
|
|
926
1088
|
this.openApiRegistry = new OpenApiRegistry();
|
|
1089
|
+
this.allowGlobalModelLookup = options.allowGlobalModelLookup ?? false;
|
|
927
1090
|
ensureMongooseJsonSchemaInitialized();
|
|
928
1091
|
}
|
|
929
1092
|
registerModelInstance(modelName, model) {
|
|
@@ -939,13 +1102,16 @@ var AccessRuntime = class {
|
|
|
939
1102
|
this.modelInstances[modelName] = model;
|
|
940
1103
|
}
|
|
941
1104
|
hasModelInstance(modelName) {
|
|
942
|
-
return modelName in this.modelInstances;
|
|
1105
|
+
return modelName in this.modelInstances || this.allowGlobalModelLookup && modelName in mongoose.models;
|
|
943
1106
|
}
|
|
944
1107
|
getModelInstance(modelName) {
|
|
945
1108
|
const registered = this.modelInstances[modelName];
|
|
946
1109
|
if (registered) return registered;
|
|
1110
|
+
if (!this.allowGlobalModelLookup) return null;
|
|
947
1111
|
const global = mongoose.models[modelName];
|
|
948
|
-
|
|
1112
|
+
if (!global) return null;
|
|
1113
|
+
this.registerModelInstance(modelName, global);
|
|
1114
|
+
return global;
|
|
949
1115
|
}
|
|
950
1116
|
registerOpenApiRoute(route) {
|
|
951
1117
|
this.openApiRegistry.register(route);
|
|
@@ -994,20 +1160,20 @@ var AccessRuntime = class {
|
|
|
994
1160
|
return this.defaultModelOptions.get(key, defaultValue);
|
|
995
1161
|
}
|
|
996
1162
|
createModelOptions(modelName) {
|
|
997
|
-
const model = this.getModelInstance(modelName)
|
|
1163
|
+
const model = this.getModelInstance(modelName);
|
|
1164
|
+
if (!model) {
|
|
1165
|
+
throw new Error(
|
|
1166
|
+
`Runtime model registry missing model "${modelName}". Pass a mongoose.Model instance to createRouter() or register it with registerModelInstance() before using this isolated runtime.`
|
|
1167
|
+
);
|
|
1168
|
+
}
|
|
998
1169
|
const manager = new OptionsManager({
|
|
999
1170
|
...defaultModelOptions,
|
|
1000
1171
|
modelName
|
|
1001
1172
|
});
|
|
1002
|
-
manager.onchange("permissionSchema", function(
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
target.modelPermissionPrefix ?? ""
|
|
1007
|
-
);
|
|
1008
|
-
target._permissionSchemaKeys = schemaKeys;
|
|
1009
|
-
target._globalPermissionKeys = globalPermissionKeys;
|
|
1010
|
-
target._modelPermissionKeys = modelPermissionKeys;
|
|
1173
|
+
manager.onchange("permissionSchema", function(_newval, _key, target) {
|
|
1174
|
+
refreshPermissionMetadata(target);
|
|
1175
|
+
}).onchange("modelPermissionPrefix", function(_newval, _key, target) {
|
|
1176
|
+
refreshPermissionMetadata(target);
|
|
1011
1177
|
}).onchange("basePath", function(newval, key, target) {
|
|
1012
1178
|
target[key] = normalizeBasePath(
|
|
1013
1179
|
modelName,
|
|
@@ -1136,7 +1302,7 @@ var AccessRuntime = class {
|
|
|
1136
1302
|
return get3(this.modelAtts, modelName, []);
|
|
1137
1303
|
}
|
|
1138
1304
|
};
|
|
1139
|
-
var defaultRuntime = new AccessRuntime();
|
|
1305
|
+
var defaultRuntime = new AccessRuntime({ allowGlobalModelLookup: true });
|
|
1140
1306
|
|
|
1141
1307
|
// src/runtime-context.ts
|
|
1142
1308
|
import { AsyncLocalStorage } from "async_hooks";
|
|
@@ -1236,7 +1402,6 @@ import {
|
|
|
1236
1402
|
flatten,
|
|
1237
1403
|
forEach as forEach3,
|
|
1238
1404
|
get as get4,
|
|
1239
|
-
intersectionBy,
|
|
1240
1405
|
isArray as isArray6,
|
|
1241
1406
|
isPlainObject as isPlainObject10,
|
|
1242
1407
|
uniq
|
|
@@ -1298,8 +1463,8 @@ var Base = class {
|
|
|
1298
1463
|
genIDFilter(id) {
|
|
1299
1464
|
return this.req.macl.genIDFilter(this.modelName, id);
|
|
1300
1465
|
}
|
|
1301
|
-
genPopulate(access, populate) {
|
|
1302
|
-
return this.req.macl.genPopulate(this.modelName, access, populate);
|
|
1466
|
+
genPopulate(access, populate, subPaths) {
|
|
1467
|
+
return this.req.macl.genPopulate(this.modelName, access, populate, subPaths);
|
|
1303
1468
|
}
|
|
1304
1469
|
genSelect(access, targetFields, skipChecks, subPaths) {
|
|
1305
1470
|
return this.req.macl.genSelect(this.modelName, access, targetFields, skipChecks, subPaths);
|
|
@@ -1396,6 +1561,55 @@ var Base = class {
|
|
|
1396
1561
|
includePaths
|
|
1397
1562
|
};
|
|
1398
1563
|
}
|
|
1564
|
+
getForeignKeyValues(value) {
|
|
1565
|
+
return flatten(castArray(value));
|
|
1566
|
+
}
|
|
1567
|
+
getForeignKey(value) {
|
|
1568
|
+
return String(value);
|
|
1569
|
+
}
|
|
1570
|
+
buildForeignKeyIndex(rows, foreignField) {
|
|
1571
|
+
const index = /* @__PURE__ */ new Map();
|
|
1572
|
+
for (const row of rows) {
|
|
1573
|
+
for (const value of this.getForeignKeyValues(get4(row, foreignField))) {
|
|
1574
|
+
const key = this.getForeignKey(value);
|
|
1575
|
+
const values = index.get(key);
|
|
1576
|
+
if (values) values.push(row);
|
|
1577
|
+
else index.set(key, [row]);
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
return index;
|
|
1581
|
+
}
|
|
1582
|
+
getIndexedMatches(index, localValue) {
|
|
1583
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1584
|
+
const matches = [];
|
|
1585
|
+
for (const value of this.getForeignKeyValues(localValue)) {
|
|
1586
|
+
for (const row of index.get(this.getForeignKey(value)) ?? []) {
|
|
1587
|
+
if (seen.has(row)) continue;
|
|
1588
|
+
seen.add(row);
|
|
1589
|
+
matches.push(row);
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
return matches;
|
|
1593
|
+
}
|
|
1594
|
+
getIndexedCount(index, localValue) {
|
|
1595
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1596
|
+
for (const value of this.getForeignKeyValues(localValue)) {
|
|
1597
|
+
for (const id of index.get(this.getForeignKey(value)) ?? []) {
|
|
1598
|
+
ids.add(id);
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
return ids.size;
|
|
1602
|
+
}
|
|
1603
|
+
sanitizeIncludeArgs(args) {
|
|
1604
|
+
if (!isPlainObject10(args)) return {};
|
|
1605
|
+
const { overrides: _clientOverrides, ...trustedArgs } = args;
|
|
1606
|
+
return trustedArgs;
|
|
1607
|
+
}
|
|
1608
|
+
assertIncludeForeignField(include) {
|
|
1609
|
+
if (!isValidFieldPath(include.foreignField)) {
|
|
1610
|
+
this.throwClientRequestError("bad_request" /* BadRequest */, `Invalid include foreignField: ${include.foreignField}`);
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1399
1613
|
async includeDocs(docs, include) {
|
|
1400
1614
|
if (!include) return docs;
|
|
1401
1615
|
const includes = compact(castArray(include));
|
|
@@ -1404,25 +1618,52 @@ var Base = class {
|
|
|
1404
1618
|
let docList = isSingle ? [docs] : docs;
|
|
1405
1619
|
for (let x = 0; x < includes.length; x++) {
|
|
1406
1620
|
const include2 = includes[x];
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1621
|
+
this.assertIncludeForeignField(include2);
|
|
1622
|
+
switch (include2.op) {
|
|
1623
|
+
case "count":
|
|
1624
|
+
docList = await this.includeDocsCount(docList, include2);
|
|
1625
|
+
break;
|
|
1626
|
+
case "read":
|
|
1627
|
+
docList = await this.includeDocsRead(docList, include2);
|
|
1628
|
+
break;
|
|
1629
|
+
case "list":
|
|
1630
|
+
docList = await this.includeDocsList(docList, include2);
|
|
1631
|
+
break;
|
|
1411
1632
|
}
|
|
1412
1633
|
}
|
|
1413
1634
|
return isSingle ? docList[0] : docList;
|
|
1414
1635
|
}
|
|
1636
|
+
async includeDocsRead(docs, include) {
|
|
1637
|
+
const { model, path, localField, foreignField, filter: _filters, args = {}, options = {} } = include;
|
|
1638
|
+
const svc = await this.getAuthorizedTargetService(model, "read");
|
|
1639
|
+
for (let y = 0; y < docs.length; y++) {
|
|
1640
|
+
const doc = docs[y];
|
|
1641
|
+
const localValue = get4(doc, localField);
|
|
1642
|
+
const filter2 = { ..._filters ?? {}, [foreignField]: { $in: this.getForeignKeyValues(localValue) } };
|
|
1643
|
+
const trustedOptions = {
|
|
1644
|
+
...options,
|
|
1645
|
+
access: "read",
|
|
1646
|
+
lean: true,
|
|
1647
|
+
includePermissions: false
|
|
1648
|
+
};
|
|
1649
|
+
const result = await svc.findOne(filter2, this.sanitizeIncludeArgs(args), trustedOptions);
|
|
1650
|
+
if (result.success) {
|
|
1651
|
+
setDocValue(doc, path, result.data);
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
return docs;
|
|
1655
|
+
}
|
|
1415
1656
|
async includeDocsList(docs, include) {
|
|
1416
1657
|
const { model, op, path, localField, foreignField, filter: _filters, args = {}, options = {} } = include;
|
|
1417
1658
|
const svc = await this.getAuthorizedTargetService(model, op);
|
|
1418
1659
|
const includeLocalValues = [];
|
|
1419
|
-
forEach3(docs, (doc
|
|
1420
|
-
includeLocalValues.push(get4(doc, localField));
|
|
1660
|
+
forEach3(docs, (doc) => {
|
|
1661
|
+
includeLocalValues.push(...this.getForeignKeyValues(get4(doc, localField)));
|
|
1421
1662
|
});
|
|
1422
1663
|
const filter2 = { ..._filters ?? {}, [foreignField]: { $in: flatten(includeLocalValues) } };
|
|
1423
1664
|
const authorizedFilter = await svc.genFilter(op, filter2);
|
|
1424
1665
|
const trustedArgs = {
|
|
1425
|
-
...args,
|
|
1666
|
+
...this.sanitizeIncludeArgs(args),
|
|
1426
1667
|
overrides: {
|
|
1427
1668
|
filter: authorizedFilter
|
|
1428
1669
|
}
|
|
@@ -1435,58 +1676,42 @@ var Base = class {
|
|
|
1435
1676
|
};
|
|
1436
1677
|
const trustedResult = await svc.find(filter2, trustedArgs, trustedOptions);
|
|
1437
1678
|
if (!trustedResult.success) return docs;
|
|
1679
|
+
const index = this.buildForeignKeyIndex(trustedResult.data, foreignField);
|
|
1438
1680
|
for (let y = 0; y < docs.length; y++) {
|
|
1439
1681
|
const doc = docs[y];
|
|
1440
|
-
const
|
|
1441
|
-
const filterFn = (row) => intersectionBy(castArray(localValue), castArray(get4(row, foreignField)), String).length > 0;
|
|
1442
|
-
const matches = trustedResult.data.filter(filterFn);
|
|
1682
|
+
const matches = this.getIndexedMatches(index, get4(doc, localField));
|
|
1443
1683
|
setDocValue(doc, path, op === "list" ? matches : matches[0]);
|
|
1444
1684
|
}
|
|
1445
1685
|
return docs;
|
|
1446
1686
|
}
|
|
1447
1687
|
async includeDocsCount(docs, include) {
|
|
1448
|
-
const { model, path, localField, foreignField, filter: _filters
|
|
1688
|
+
const { model, path, localField, foreignField, filter: _filters } = include;
|
|
1449
1689
|
const svc = await this.getAuthorizedTargetService(model, "count");
|
|
1450
|
-
const
|
|
1451
|
-
|
|
1452
|
-
includeLocalValues.push(get4(doc, localField));
|
|
1453
|
-
});
|
|
1454
|
-
const filter2 = { ..._filters ?? {}, [foreignField]: { $in: flatten(includeLocalValues) } };
|
|
1455
|
-
const authorizedFilter = await svc.genFilter("list", filter2);
|
|
1456
|
-
const trustedArgs = {
|
|
1457
|
-
...args,
|
|
1458
|
-
select: [foreignField],
|
|
1459
|
-
overrides: {
|
|
1460
|
-
filter: authorizedFilter
|
|
1461
|
-
}
|
|
1462
|
-
};
|
|
1463
|
-
const trustedOptions = {
|
|
1464
|
-
...options,
|
|
1465
|
-
lean: true,
|
|
1466
|
-
includePermissions: false,
|
|
1467
|
-
includeCount: false
|
|
1468
|
-
};
|
|
1469
|
-
const result = await svc.find(filter2, trustedArgs, trustedOptions);
|
|
1690
|
+
const localValues = docs.flatMap((doc) => this.getForeignKeyValues(get4(doc, localField)));
|
|
1691
|
+
const result = await svc.countByFieldValues(foreignField, localValues, _filters ?? {}, "count");
|
|
1470
1692
|
if (!result.success) return docs;
|
|
1471
1693
|
for (let y = 0; y < docs.length; y++) {
|
|
1472
1694
|
const doc = docs[y];
|
|
1473
|
-
|
|
1474
|
-
const filterFn = (row) => intersectionBy(castArray(localValue), castArray(get4(row, foreignField)), String).length > 0;
|
|
1475
|
-
setDocValue(doc, path, result.data.filter(filterFn).length);
|
|
1695
|
+
setDocValue(doc, path, this.getIndexedCount(result.data, get4(doc, localField)));
|
|
1476
1696
|
}
|
|
1477
1697
|
return docs;
|
|
1478
1698
|
}
|
|
1479
|
-
async parseClientData(filter2) {
|
|
1480
|
-
const result = await iterateQuery(
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1699
|
+
async parseClientData(filter2, scheduler = new RequestConcurrencyScheduler(this.getRequestComplexity().maxBulkConcurrency), scheduled = false) {
|
|
1700
|
+
const result = await iterateQuery(
|
|
1701
|
+
filter2,
|
|
1702
|
+
async (fo, val, key) => {
|
|
1703
|
+
switch (fo) {
|
|
1704
|
+
case 0 /* SubQuery */:
|
|
1705
|
+
return this.handleSubQuery(val, key);
|
|
1706
|
+
case 1 /* Date */:
|
|
1707
|
+
return this.handleDate(val, key);
|
|
1708
|
+
default:
|
|
1709
|
+
return null;
|
|
1710
|
+
}
|
|
1711
|
+
},
|
|
1712
|
+
scheduler,
|
|
1713
|
+
scheduled
|
|
1714
|
+
);
|
|
1490
1715
|
return result;
|
|
1491
1716
|
}
|
|
1492
1717
|
async handleSubQuery(sq, key) {
|
|
@@ -1535,14 +1760,74 @@ import {
|
|
|
1535
1760
|
isBoolean as isBoolean3,
|
|
1536
1761
|
isFunction as isFunction3,
|
|
1537
1762
|
omit,
|
|
1538
|
-
pick as
|
|
1763
|
+
pick as pick3,
|
|
1539
1764
|
uniq as uniq2,
|
|
1540
1765
|
uniqBy
|
|
1541
1766
|
} from "@web-ts-toolkit/utils";
|
|
1542
1767
|
import { diff } from "just-diff";
|
|
1543
1768
|
|
|
1544
1769
|
// src/model.ts
|
|
1545
|
-
|
|
1770
|
+
var Model = class {
|
|
1771
|
+
constructor(modelName, runtime) {
|
|
1772
|
+
this.modelName = modelName;
|
|
1773
|
+
this.runtime = runtime ?? null;
|
|
1774
|
+
const resolvedRuntime = runtime ?? getActiveRuntime() ?? defaultRuntime;
|
|
1775
|
+
const registered = resolvedRuntime.getModelInstance(modelName);
|
|
1776
|
+
if (!registered) {
|
|
1777
|
+
throw new Error(
|
|
1778
|
+
`Runtime model registry missing model "${modelName}". Pass a mongoose.Model instance to createRouter() or register it with registerModelInstance() before using this runtime.`
|
|
1779
|
+
);
|
|
1780
|
+
}
|
|
1781
|
+
this.mongooseModel = registered;
|
|
1782
|
+
if (!this.mongooseModel) return;
|
|
1783
|
+
}
|
|
1784
|
+
new() {
|
|
1785
|
+
const doc = new this.mongooseModel();
|
|
1786
|
+
return doc;
|
|
1787
|
+
}
|
|
1788
|
+
create(data) {
|
|
1789
|
+
return this.mongooseModel.create(data);
|
|
1790
|
+
}
|
|
1791
|
+
find({ filter: filter2, select, sort, populate, limit, hardLimit, skip, lean }) {
|
|
1792
|
+
const builder = this.mongooseModel.find(filter2);
|
|
1793
|
+
if (select) builder.select(select);
|
|
1794
|
+
if (skip) builder.skip(Number(skip));
|
|
1795
|
+
const normalizedLimit = Number(limit);
|
|
1796
|
+
if (Number.isSafeInteger(normalizedLimit) && normalizedLimit > 0) {
|
|
1797
|
+
builder.limit(normalizedLimit);
|
|
1798
|
+
} else if (Number.isSafeInteger(hardLimit) && Number(hardLimit) > 0) {
|
|
1799
|
+
builder.limit(Number(hardLimit));
|
|
1800
|
+
}
|
|
1801
|
+
if (sort) builder.sort(sort);
|
|
1802
|
+
if (populate) builder.populate(populate);
|
|
1803
|
+
if (lean) builder.lean();
|
|
1804
|
+
return builder;
|
|
1805
|
+
}
|
|
1806
|
+
findOne({ filter: filter2, select, sort, populate, lean }) {
|
|
1807
|
+
const builder = this.mongooseModel.findOne(filter2);
|
|
1808
|
+
if (select) builder.select(select);
|
|
1809
|
+
if (sort) builder.sort(sort);
|
|
1810
|
+
if (populate) builder.populate(populate);
|
|
1811
|
+
if (lean) builder.lean();
|
|
1812
|
+
return builder;
|
|
1813
|
+
}
|
|
1814
|
+
exists(filter2) {
|
|
1815
|
+
if (!filter2) return null;
|
|
1816
|
+
return this.findOne({ filter: filter2 }).select("_id").lean();
|
|
1817
|
+
}
|
|
1818
|
+
// see https://mongoosejs.com/docs/api.html#query_Query-countDocuments
|
|
1819
|
+
countDocuments(filter2 = {}) {
|
|
1820
|
+
return this.mongooseModel.countDocuments(filter2);
|
|
1821
|
+
}
|
|
1822
|
+
// see https://mongoosejs.com/docs/api.html#model_Model.distinct
|
|
1823
|
+
distinct(field, conditions = {}) {
|
|
1824
|
+
return this.mongooseModel.distinct(field, conditions);
|
|
1825
|
+
}
|
|
1826
|
+
aggregate(pipeline) {
|
|
1827
|
+
return this.mongooseModel.aggregate(pipeline);
|
|
1828
|
+
}
|
|
1829
|
+
};
|
|
1830
|
+
var model_default = Model;
|
|
1546
1831
|
|
|
1547
1832
|
// src/logger-helpers.ts
|
|
1548
1833
|
var SENSITIVE_FILTER_KEYS = /* @__PURE__ */ new Set([
|
|
@@ -1721,138 +2006,8 @@ var error = (error2) => {
|
|
|
1721
2006
|
safeApply(fn, target ?? defaultLogger, [error2]);
|
|
1722
2007
|
};
|
|
1723
2008
|
|
|
1724
|
-
// src/model.ts
|
|
1725
|
-
var safeSortLogError = (msg, ...args) => {
|
|
1726
|
-
const message = args.length > 0 ? `${msg} ${safeStringify(args.length === 1 ? args[0] : args)}` : msg;
|
|
1727
|
-
error(message);
|
|
1728
|
-
};
|
|
1729
|
-
var Model = class {
|
|
1730
|
-
constructor(modelName, runtime) {
|
|
1731
|
-
this.modelName = modelName;
|
|
1732
|
-
this.runtime = runtime ?? null;
|
|
1733
|
-
const resolvedRuntime = runtime ?? getActiveRuntime();
|
|
1734
|
-
const registered = resolvedRuntime?.getModelInstance(modelName) ?? null;
|
|
1735
|
-
const global = mongoose2.models[modelName];
|
|
1736
|
-
this.model = registered ?? global ?? mongoose2.model(modelName);
|
|
1737
|
-
if (!this.model) return;
|
|
1738
|
-
}
|
|
1739
|
-
new() {
|
|
1740
|
-
const doc = new this.model();
|
|
1741
|
-
return doc;
|
|
1742
|
-
}
|
|
1743
|
-
create(data) {
|
|
1744
|
-
return this.model.create(data);
|
|
1745
|
-
}
|
|
1746
|
-
find({ filter: filter2, select, sort, populate, limit, hardLimit, skip, lean }) {
|
|
1747
|
-
if (!this.validateSort(sort)) {
|
|
1748
|
-
sort = null;
|
|
1749
|
-
}
|
|
1750
|
-
const builder = this.model.find(filter2);
|
|
1751
|
-
if (select) builder.select(select);
|
|
1752
|
-
if (skip) builder.skip(Number(skip));
|
|
1753
|
-
const normalizedLimit = Number(limit);
|
|
1754
|
-
if (Number.isSafeInteger(normalizedLimit) && normalizedLimit > 0) {
|
|
1755
|
-
builder.limit(normalizedLimit);
|
|
1756
|
-
} else if (Number.isSafeInteger(hardLimit) && Number(hardLimit) > 0) {
|
|
1757
|
-
builder.limit(Number(hardLimit));
|
|
1758
|
-
}
|
|
1759
|
-
if (sort) builder.sort(sort);
|
|
1760
|
-
if (populate) builder.populate(populate);
|
|
1761
|
-
if (lean) builder.lean();
|
|
1762
|
-
return builder;
|
|
1763
|
-
}
|
|
1764
|
-
validateSort(sort, logError = safeSortLogError) {
|
|
1765
|
-
const validSortOrders = [1, -1, "asc", "ascending", "desc", "descending"];
|
|
1766
|
-
const fieldPathPattern = /^[A-Za-z_$][\w$]*(\.[A-Za-z_$][\w$]*)*$/;
|
|
1767
|
-
const isValidFieldPath = (field) => fieldPathPattern.test(field);
|
|
1768
|
-
if (sort === null || sort === void 0) return true;
|
|
1769
|
-
if (typeof sort === "string") {
|
|
1770
|
-
const fields = sort.trim().split(/\s+/).filter(Boolean);
|
|
1771
|
-
const isValid = fields.every((field) => isValidFieldPath(field.startsWith("-") ? field.slice(1) : field));
|
|
1772
|
-
if (!isValid) {
|
|
1773
|
-
logError("Invalid sort string:", sort);
|
|
1774
|
-
return false;
|
|
1775
|
-
}
|
|
1776
|
-
return true;
|
|
1777
|
-
}
|
|
1778
|
-
if (Array.isArray(sort)) {
|
|
1779
|
-
const isValid = sort.every((pair) => {
|
|
1780
|
-
if (!Array.isArray(pair) || pair.length !== 2) {
|
|
1781
|
-
logError("Invalid sort array element: must be [key, order]", pair);
|
|
1782
|
-
return false;
|
|
1783
|
-
}
|
|
1784
|
-
const [key, order] = pair;
|
|
1785
|
-
if (typeof key !== "string" || !isValidFieldPath(key)) {
|
|
1786
|
-
logError("Invalid sort array key: must be a field path string", key);
|
|
1787
|
-
return false;
|
|
1788
|
-
}
|
|
1789
|
-
if (!validSortOrders.includes(order)) {
|
|
1790
|
-
logError('Invalid sort array order: must be 1, -1, "asc", or "desc"', order);
|
|
1791
|
-
return false;
|
|
1792
|
-
}
|
|
1793
|
-
return true;
|
|
1794
|
-
});
|
|
1795
|
-
return isValid;
|
|
1796
|
-
}
|
|
1797
|
-
if (typeof sort === "object" && !(sort instanceof Map)) {
|
|
1798
|
-
const isValid = Object.entries(sort).every(([key, order]) => {
|
|
1799
|
-
if (!isValidFieldPath(key)) {
|
|
1800
|
-
logError("Invalid sort object key: must be a field path string", key);
|
|
1801
|
-
return false;
|
|
1802
|
-
}
|
|
1803
|
-
if (!validSortOrders.includes(order)) {
|
|
1804
|
-
logError('Invalid sort object value: must be 1, -1, "asc", or "desc"', order);
|
|
1805
|
-
return false;
|
|
1806
|
-
}
|
|
1807
|
-
return true;
|
|
1808
|
-
});
|
|
1809
|
-
return isValid;
|
|
1810
|
-
}
|
|
1811
|
-
if (sort instanceof Map) {
|
|
1812
|
-
const isValid = Array.from(sort.entries()).every(([key, order]) => {
|
|
1813
|
-
if (!isValidFieldPath(key)) {
|
|
1814
|
-
logError("Invalid sort Map key: must be a field path string", key);
|
|
1815
|
-
return false;
|
|
1816
|
-
}
|
|
1817
|
-
if (!validSortOrders.includes(order)) {
|
|
1818
|
-
logError('Invalid sort Map value: must be 1, -1, "asc", or "desc"', order);
|
|
1819
|
-
return false;
|
|
1820
|
-
}
|
|
1821
|
-
return true;
|
|
1822
|
-
});
|
|
1823
|
-
return isValid;
|
|
1824
|
-
}
|
|
1825
|
-
logError("Invalid sort type: must be string, array, object, or Map", sort);
|
|
1826
|
-
return false;
|
|
1827
|
-
}
|
|
1828
|
-
findOne({ filter: filter2, select, sort, populate, lean }) {
|
|
1829
|
-
if (!this.validateSort(sort)) {
|
|
1830
|
-
sort = null;
|
|
1831
|
-
}
|
|
1832
|
-
const builder = this.model.findOne(filter2);
|
|
1833
|
-
if (select) builder.select(select);
|
|
1834
|
-
if (sort) builder.sort(sort);
|
|
1835
|
-
if (populate) builder.populate(populate);
|
|
1836
|
-
if (lean) builder.lean();
|
|
1837
|
-
return builder;
|
|
1838
|
-
}
|
|
1839
|
-
exists(filter2) {
|
|
1840
|
-
if (!filter2) return null;
|
|
1841
|
-
return this.findOne({ filter: filter2 }).select("_id").lean();
|
|
1842
|
-
}
|
|
1843
|
-
// see https://mongoosejs.com/docs/api.html#query_Query-countDocuments
|
|
1844
|
-
countDocuments(filter2 = {}) {
|
|
1845
|
-
return this.model.countDocuments(filter2);
|
|
1846
|
-
}
|
|
1847
|
-
// see https://mongoosejs.com/docs/api.html#model_Model.distinct
|
|
1848
|
-
distinct(field, conditions = {}) {
|
|
1849
|
-
return this.model.distinct(field, conditions);
|
|
1850
|
-
}
|
|
1851
|
-
};
|
|
1852
|
-
var model_default = Model;
|
|
1853
|
-
|
|
1854
2009
|
// src/services/model-subdocument-service.ts
|
|
1855
|
-
import { forEach as forEach4, get as get5, pick as
|
|
2010
|
+
import { forEach as forEach4, get as get5, pick as pick2 } from "@web-ts-toolkit/utils";
|
|
1856
2011
|
async function listSub(service, id, sub, options) {
|
|
1857
2012
|
const { filter: ft, select } = options ?? {};
|
|
1858
2013
|
const filterErrors = validateClientFilter(ft);
|
|
@@ -1866,7 +2021,7 @@ async function listSub(service, id, sub, options) {
|
|
|
1866
2021
|
]);
|
|
1867
2022
|
if (subFilter === false) return { success: false, kind: "error", code: "forbidden" /* Forbidden */ };
|
|
1868
2023
|
result = filterCollection(result, subFilter);
|
|
1869
|
-
if (subSelect) result = result.map((v) =>
|
|
2024
|
+
if (subSelect) result = result.map((v) => pick2(toObject(v), subSelect.concat("_id")));
|
|
1870
2025
|
return { success: true, kind: "list", code: "success" /* Success */, data: result, count: result.length };
|
|
1871
2026
|
}
|
|
1872
2027
|
async function readSub(service, id, sub, subId, options) {
|
|
@@ -1881,7 +2036,7 @@ async function readSub(service, id, sub, subId, options) {
|
|
|
1881
2036
|
if (subFilter === false) return { success: false, kind: "error", code: "forbidden" /* Forbidden */ };
|
|
1882
2037
|
let subdoc = findElement(result, subFilter);
|
|
1883
2038
|
if (!subdoc) return { success: false, kind: "error", code: "not_found" /* NotFound */ };
|
|
1884
|
-
if (subSelect) subdoc =
|
|
2039
|
+
if (subSelect) subdoc = pick2(toObject(subdoc), subSelect.concat(["_id"]));
|
|
1885
2040
|
return { success: true, kind: "single", code: "success" /* Success */, data: subdoc };
|
|
1886
2041
|
}
|
|
1887
2042
|
async function updateSub(service, id, sub, subId, data) {
|
|
@@ -1896,10 +2051,10 @@ async function updateSub(service, id, sub, subId, data) {
|
|
|
1896
2051
|
if (subFilter === false) return { success: false, kind: "error", code: "forbidden" /* Forbidden */ };
|
|
1897
2052
|
let subdoc = findElement(result, subFilter);
|
|
1898
2053
|
if (!subdoc) return { success: false, kind: "error", code: "not_found" /* NotFound */ };
|
|
1899
|
-
const allowedData =
|
|
2054
|
+
const allowedData = pick2(data, subUpdateSelect);
|
|
1900
2055
|
Object.assign(subdoc, allowedData);
|
|
1901
2056
|
await parentDoc.save();
|
|
1902
|
-
if (subReadSelect) subdoc =
|
|
2057
|
+
if (subReadSelect) subdoc = pick2(toObject(subdoc), subReadSelect.concat(["_id"]));
|
|
1903
2058
|
return { success: true, kind: "single", code: "success" /* Success */, data: subdoc };
|
|
1904
2059
|
}
|
|
1905
2060
|
async function bulkUpdateSub(service, id, sub, data) {
|
|
@@ -1925,11 +2080,11 @@ async function bulkUpdateSub(service, id, sub, data) {
|
|
|
1925
2080
|
forEach4(result, (subdoc) => {
|
|
1926
2081
|
const tdata = findElementById(data, subdoc._id);
|
|
1927
2082
|
if (!tdata) return;
|
|
1928
|
-
const allowedData =
|
|
2083
|
+
const allowedData = pick2(tdata, subUpdateSelect);
|
|
1929
2084
|
Object.assign(subdoc, allowedData);
|
|
1930
2085
|
});
|
|
1931
2086
|
await parentDoc.save();
|
|
1932
|
-
if (subReadSelect) result = result.map((v) =>
|
|
2087
|
+
if (subReadSelect) result = result.map((v) => pick2(toObject(v), subReadSelect.concat(["_id"])));
|
|
1933
2088
|
return { success: true, kind: "list", code: "success" /* Success */, data: result, count: result.length };
|
|
1934
2089
|
}
|
|
1935
2090
|
async function createSub(service, id, sub, data, options) {
|
|
@@ -1950,14 +2105,14 @@ async function createSub(service, id, sub, data, options) {
|
|
|
1950
2105
|
service.genQuerySelect("create", null, false, [sub, "sub"]),
|
|
1951
2106
|
service.genQuerySelect("read", null, false, [sub, "sub"])
|
|
1952
2107
|
]);
|
|
1953
|
-
const allowedData = Array.isArray(data) ? data.map((row) =>
|
|
2108
|
+
const allowedData = Array.isArray(data) ? data.map((row) => pick2(row, subCreateSelect)) : pick2(data, subCreateSelect);
|
|
1954
2109
|
if (Array.isArray(allowedData)) {
|
|
1955
2110
|
addFirst === true ? result.unshift(...allowedData) : result.push(...allowedData);
|
|
1956
2111
|
} else {
|
|
1957
2112
|
addFirst === true ? result.unshift(allowedData) : result.push(allowedData);
|
|
1958
2113
|
}
|
|
1959
2114
|
await parentDoc.save();
|
|
1960
|
-
if (subReadSelect) result = result.map((v) =>
|
|
2115
|
+
if (subReadSelect) result = result.map((v) => pick2(toObject(v), subReadSelect.concat(["_id"])));
|
|
1961
2116
|
return { success: true, kind: "list", code: "created" /* Created */, data: result, count: result.length };
|
|
1962
2117
|
}
|
|
1963
2118
|
async function deleteSub(service, id, sub, subId) {
|
|
@@ -1977,7 +2132,8 @@ async function getParentDoc(service, id, sub, args, options) {
|
|
|
1977
2132
|
const { access = "read", lean = false } = options ?? {};
|
|
1978
2133
|
const parentFilter = await service.genFilter(access, await service.genIDFilter(id));
|
|
1979
2134
|
if (parentFilter === false) return null;
|
|
1980
|
-
|
|
2135
|
+
const subPopulate = await service.genPopulate("read", genSubPopulate(sub, populate), [sub, "sub"]);
|
|
2136
|
+
return service.findRawParentDoc({ filter: parentFilter, select: sub, populate: subPopulate, lean });
|
|
1981
2137
|
}
|
|
1982
2138
|
|
|
1983
2139
|
// src/services/model-service-defaults.ts
|
|
@@ -2103,21 +2259,6 @@ var assertModelDocument = (value, modelName, hookName) => {
|
|
|
2103
2259
|
}
|
|
2104
2260
|
throw new Error(`${hookName} hook for model=${modelName} must return a Mongoose document instance`);
|
|
2105
2261
|
};
|
|
2106
|
-
var mapWithConcurrencyLimit = async (items, limit, iteratee) => {
|
|
2107
|
-
const results = new Array(items.length);
|
|
2108
|
-
let cursor = 0;
|
|
2109
|
-
const workerCount = Math.min(Math.max(limit, 1), items.length || 1);
|
|
2110
|
-
await Promise.all(
|
|
2111
|
-
Array.from({ length: workerCount }, async () => {
|
|
2112
|
-
while (cursor < items.length) {
|
|
2113
|
-
const current = cursor;
|
|
2114
|
-
cursor += 1;
|
|
2115
|
-
results[current] = await iteratee(items[current], current);
|
|
2116
|
-
}
|
|
2117
|
-
})
|
|
2118
|
-
);
|
|
2119
|
-
return results;
|
|
2120
|
-
};
|
|
2121
2262
|
var Service = class extends Base {
|
|
2122
2263
|
findRawParentDoc(args) {
|
|
2123
2264
|
return this.model.findOne({
|
|
@@ -2126,6 +2267,12 @@ var Service = class extends Base {
|
|
|
2126
2267
|
populate: args.populate
|
|
2127
2268
|
});
|
|
2128
2269
|
}
|
|
2270
|
+
createModelAdapter(modelName) {
|
|
2271
|
+
return new model_default(modelName);
|
|
2272
|
+
}
|
|
2273
|
+
getModelRouterOptions(modelName) {
|
|
2274
|
+
return getModelOptions(modelName);
|
|
2275
|
+
}
|
|
2129
2276
|
asServiceHookContext(context) {
|
|
2130
2277
|
return context;
|
|
2131
2278
|
}
|
|
@@ -2152,8 +2299,8 @@ var Service = class extends Base {
|
|
|
2152
2299
|
}
|
|
2153
2300
|
constructor(req, modelName) {
|
|
2154
2301
|
super(req, modelName);
|
|
2155
|
-
this.model =
|
|
2156
|
-
this.options =
|
|
2302
|
+
this.model = this.createModelAdapter(modelName);
|
|
2303
|
+
this.options = this.getModelRouterOptions(modelName);
|
|
2157
2304
|
this.defaults = this.options.defaults || {};
|
|
2158
2305
|
this.baseFields = ["_id"];
|
|
2159
2306
|
this.baseFieldsExt = this.baseFields.concat(this.options.documentPermissionField);
|
|
@@ -2172,10 +2319,11 @@ var Service = class extends Base {
|
|
|
2172
2319
|
if (result) return result;
|
|
2173
2320
|
throw error2;
|
|
2174
2321
|
}
|
|
2175
|
-
let [_filter, _select, _populate] = await Promise.all([
|
|
2322
|
+
let [_filter, _select, _populate, allowedSortFields] = await Promise.all([
|
|
2176
2323
|
overrideFilter || this.genFilter(access, parsedFilter),
|
|
2177
2324
|
overrideSelect || this.genQuerySelect(access, select),
|
|
2178
|
-
overridePopulate || this.genPopulate(populateAccess || access, populate)
|
|
2325
|
+
overridePopulate || this.genPopulate(populateAccess || access, populate),
|
|
2326
|
+
this.genAllowedFields({}, access, this.baseFieldsExt)
|
|
2179
2327
|
]);
|
|
2180
2328
|
const { includes, includeLocalFields, includePaths } = this.processInclude(include);
|
|
2181
2329
|
const finalSelect = normalizeSelect(_select).concat(includeLocalFields);
|
|
@@ -2194,13 +2342,18 @@ var Service = class extends Base {
|
|
|
2194
2342
|
this.completeOp("findOne", startedAt, "forbidden" /* Forbidden */, _filter);
|
|
2195
2343
|
return { success: false, kind: "error", code: "forbidden" /* Forbidden */, query };
|
|
2196
2344
|
}
|
|
2345
|
+
const sortErrors = validateSortFields(sort, allowedSortFields);
|
|
2346
|
+
if (sortErrors.length > 0) {
|
|
2347
|
+
this.completeOp("findOne", startedAt, "bad_request" /* BadRequest */, _filter);
|
|
2348
|
+
return { success: false, kind: "error", code: "bad_request" /* BadRequest */, errors: sortErrors, query };
|
|
2349
|
+
}
|
|
2197
2350
|
let doc = await this.model.findOne({ ...query, lean });
|
|
2198
2351
|
if (!doc) {
|
|
2199
2352
|
this.completeOp("findOne", startedAt, "not_found" /* NotFound */, _filter);
|
|
2200
2353
|
return { success: false, kind: "error", code: "not_found" /* NotFound */, query };
|
|
2201
2354
|
}
|
|
2202
2355
|
const context = {
|
|
2203
|
-
mongooseModel: this.model.
|
|
2356
|
+
mongooseModel: this.model.mongooseModel,
|
|
2204
2357
|
modelName: this.modelName,
|
|
2205
2358
|
operation: access,
|
|
2206
2359
|
originalDocumentSnapshot: toObject(doc),
|
|
@@ -2264,11 +2417,12 @@ var Service = class extends Base {
|
|
|
2264
2417
|
if (result) return result;
|
|
2265
2418
|
throw error2;
|
|
2266
2419
|
}
|
|
2267
|
-
const [_filter, _select, _populate, pagination] = await Promise.all([
|
|
2420
|
+
const [_filter, _select, _populate, pagination, allowedSortFields] = await Promise.all([
|
|
2268
2421
|
overrideFilter || this.genFilter("list", parsedFilter),
|
|
2269
2422
|
overrideSelect || this.genQuerySelect("list", select),
|
|
2270
2423
|
overridePopulate || this.genPopulate(populateAccess, populate),
|
|
2271
|
-
genPagination({ skip, limit, page, pageSize }, this.options.listHardLimit)
|
|
2424
|
+
genPagination({ skip, limit, page, pageSize }, this.options.listHardLimit),
|
|
2425
|
+
this.genAllowedFields({}, "list", this.baseFieldsExt)
|
|
2272
2426
|
]);
|
|
2273
2427
|
const finalSelect = normalizeSelect(_select);
|
|
2274
2428
|
const filteredPopulate = isArray7(finalSelect) && isArray7(_populate) ? _populate.filter((p) => finalSelect.includes(p.path.split(".")[0])) : _populate;
|
|
@@ -2291,13 +2445,18 @@ var Service = class extends Base {
|
|
|
2291
2445
|
this.completeOp("find", startedAt, "forbidden" /* Forbidden */, _filter);
|
|
2292
2446
|
return { success: false, kind: "error", code: "forbidden" /* Forbidden */, query };
|
|
2293
2447
|
}
|
|
2448
|
+
const sortErrors = validateSortFields(sort, allowedSortFields);
|
|
2449
|
+
if (sortErrors.length > 0) {
|
|
2450
|
+
this.completeOp("find", startedAt, "bad_request" /* BadRequest */, _filter);
|
|
2451
|
+
return { success: false, kind: "error", code: "bad_request" /* BadRequest */, errors: sortErrors, query };
|
|
2452
|
+
}
|
|
2294
2453
|
let docs = await this.model.find({
|
|
2295
2454
|
...query,
|
|
2296
2455
|
hardLimit: this.options.listHardLimit,
|
|
2297
2456
|
lean
|
|
2298
2457
|
});
|
|
2299
2458
|
const contexts = docs.map((doc) => ({
|
|
2300
|
-
mongooseModel: this.model.
|
|
2459
|
+
mongooseModel: this.model.mongooseModel,
|
|
2301
2460
|
modelName: this.modelName,
|
|
2302
2461
|
operation: "list",
|
|
2303
2462
|
originalDocumentSnapshot: toObject(doc),
|
|
@@ -2360,8 +2519,33 @@ var Service = class extends Base {
|
|
|
2360
2519
|
errors: [{ detail: `Bulk create exceeds maximum item count of ${maxBulkItems}` }]
|
|
2361
2520
|
};
|
|
2362
2521
|
}
|
|
2522
|
+
const parseErrors = [];
|
|
2523
|
+
const parseScheduler = new RequestConcurrencyScheduler(maxBulkConcurrency);
|
|
2363
2524
|
try {
|
|
2364
|
-
|
|
2525
|
+
const parsedData = await parseScheduler.map(dataArr, async (d, index) => {
|
|
2526
|
+
try {
|
|
2527
|
+
return await this.parseClientData(d, parseScheduler, true);
|
|
2528
|
+
} catch (error2) {
|
|
2529
|
+
const result = this.getClientRequestErrorResult(error2);
|
|
2530
|
+
if (!result) throw error2;
|
|
2531
|
+
parseErrors.push({
|
|
2532
|
+
index,
|
|
2533
|
+
code: result.code,
|
|
2534
|
+
errors: (result.errors ?? []).map((issue) => this.formatBulkValidationIssue(issue, isArr ? index : null))
|
|
2535
|
+
});
|
|
2536
|
+
return void 0;
|
|
2537
|
+
}
|
|
2538
|
+
});
|
|
2539
|
+
if (parseErrors.length > 0) {
|
|
2540
|
+
const sortedErrors = parseErrors.slice().sort((a, b) => a.index - b.index);
|
|
2541
|
+
return {
|
|
2542
|
+
success: false,
|
|
2543
|
+
kind: "error",
|
|
2544
|
+
code: sortedErrors[0]?.code ?? "bad_request" /* BadRequest */,
|
|
2545
|
+
errors: sortedErrors.flatMap((entry) => entry.errors)
|
|
2546
|
+
};
|
|
2547
|
+
}
|
|
2548
|
+
dataArr = parsedData;
|
|
2365
2549
|
} catch (error2) {
|
|
2366
2550
|
const result = this.getClientRequestErrorResult(error2);
|
|
2367
2551
|
if (result) return result;
|
|
@@ -2372,14 +2556,14 @@ var Service = class extends Base {
|
|
|
2372
2556
|
const validationErrors = [];
|
|
2373
2557
|
const validationItems = await mapWithConcurrencyLimit(dataArr, maxBulkConcurrency, async (item, index) => {
|
|
2374
2558
|
const context = {
|
|
2375
|
-
mongooseModel: this.model.
|
|
2559
|
+
mongooseModel: this.model.mongooseModel,
|
|
2376
2560
|
modelName: this.modelName,
|
|
2377
2561
|
operation: "create",
|
|
2378
2562
|
originalData: item,
|
|
2379
2563
|
resolvedQuery: resolvedPopulate.length > 0 ? { populate: resolvedPopulate } : {}
|
|
2380
2564
|
};
|
|
2381
2565
|
const allowedFields = await this.genAllowedFields(item, "create");
|
|
2382
|
-
const allowedData =
|
|
2566
|
+
const allowedData = pick3(item, allowedFields);
|
|
2383
2567
|
context.allowedFields = allowedFields;
|
|
2384
2568
|
context.allowedData = allowedData;
|
|
2385
2569
|
const validated = await this.validate(allowedData, "create", context);
|
|
@@ -2515,7 +2699,7 @@ var Service = class extends Base {
|
|
|
2515
2699
|
return { success: false, kind: "error", code: "not_found" /* NotFound */, query };
|
|
2516
2700
|
}
|
|
2517
2701
|
const context = {
|
|
2518
|
-
mongooseModel: this.model.
|
|
2702
|
+
mongooseModel: this.model.mongooseModel,
|
|
2519
2703
|
modelName: this.modelName,
|
|
2520
2704
|
operation: "update",
|
|
2521
2705
|
resolvedQuery: query
|
|
@@ -2536,7 +2720,7 @@ var Service = class extends Base {
|
|
|
2536
2720
|
context.docPermissions = this.getDocPermissions(doc);
|
|
2537
2721
|
context.currentDocument = doc;
|
|
2538
2722
|
const allowedFields = await this.genAllowedFields(doc, "update");
|
|
2539
|
-
const allowedData =
|
|
2723
|
+
const allowedData = pick3(data, allowedFields);
|
|
2540
2724
|
context.allowedFields = allowedFields;
|
|
2541
2725
|
context.allowedData = allowedData;
|
|
2542
2726
|
const validated = await this.validate(allowedData, "update", context);
|
|
@@ -2661,7 +2845,7 @@ var Service = class extends Base {
|
|
|
2661
2845
|
return { success: false, kind: "error", code: "not_found" /* NotFound */, query };
|
|
2662
2846
|
}
|
|
2663
2847
|
const context = {
|
|
2664
|
-
mongooseModel: this.model.
|
|
2848
|
+
mongooseModel: this.model.mongooseModel,
|
|
2665
2849
|
modelName: this.modelName,
|
|
2666
2850
|
operation: "delete",
|
|
2667
2851
|
originalDocumentSnapshot: toObject(doc),
|
|
@@ -2690,22 +2874,11 @@ var Service = class extends Base {
|
|
|
2690
2874
|
};
|
|
2691
2875
|
}
|
|
2692
2876
|
isValidDistinctFieldName(field) {
|
|
2693
|
-
|
|
2694
|
-
if (field.includes("$")) return false;
|
|
2695
|
-
if (field.includes("..")) return false;
|
|
2696
|
-
if (field.startsWith(".") || field.endsWith(".")) return false;
|
|
2697
|
-
if (/\s/.test(field)) return false;
|
|
2698
|
-
return true;
|
|
2877
|
+
return isValidFieldPath(field);
|
|
2699
2878
|
}
|
|
2700
2879
|
async authorizeDistinctField(field) {
|
|
2701
2880
|
const allowedFields = await this.genAllowedFields(null, "read");
|
|
2702
|
-
|
|
2703
|
-
if (allowed === field) return true;
|
|
2704
|
-
if (allowed.startsWith(`${field}.`)) return true;
|
|
2705
|
-
if (field.startsWith(`${allowed}.`)) return true;
|
|
2706
|
-
return false;
|
|
2707
|
-
});
|
|
2708
|
-
if (!isAllowed) {
|
|
2881
|
+
if (!isFieldAllowed(field, allowedFields)) {
|
|
2709
2882
|
return {
|
|
2710
2883
|
success: false,
|
|
2711
2884
|
kind: "error",
|
|
@@ -2743,6 +2916,44 @@ var Service = class extends Base {
|
|
|
2743
2916
|
if (filter2 === false) return { success: false, kind: "error", code: "forbidden" /* Forbidden */, query };
|
|
2744
2917
|
return { success: true, kind: "single", code: "success" /* Success */, data: await this.model.countDocuments(filter2), query };
|
|
2745
2918
|
}
|
|
2919
|
+
async countByFieldValues(foreignField, values, filter2 = {}, access = "list") {
|
|
2920
|
+
if (!isValidFieldPath(foreignField)) {
|
|
2921
|
+
return {
|
|
2922
|
+
success: false,
|
|
2923
|
+
kind: "error",
|
|
2924
|
+
code: "bad_request" /* BadRequest */,
|
|
2925
|
+
errors: [{ detail: `Invalid include foreignField: ${foreignField}` }]
|
|
2926
|
+
};
|
|
2927
|
+
}
|
|
2928
|
+
const filterErrors = this.validateClientFilter(filter2);
|
|
2929
|
+
if (filterErrors.length > 0) return { success: false, kind: "error", code: "bad_request" /* BadRequest */, errors: filterErrors };
|
|
2930
|
+
const uniqueValues = uniqBy(values, (value) => String(value));
|
|
2931
|
+
filter2 = await this.genFilter(access, {
|
|
2932
|
+
...filter2,
|
|
2933
|
+
[foreignField]: { $in: uniqueValues }
|
|
2934
|
+
});
|
|
2935
|
+
const query = { filter: filter2 };
|
|
2936
|
+
if (filter2 === false) return { success: false, kind: "error", code: "forbidden" /* Forbidden */, query };
|
|
2937
|
+
const matchFilter = filter2;
|
|
2938
|
+
const rows = await this.model.aggregate([
|
|
2939
|
+
{ $match: matchFilter },
|
|
2940
|
+
{
|
|
2941
|
+
$project: {
|
|
2942
|
+
foreignValues: {
|
|
2943
|
+
$cond: [{ $isArray: `$${foreignField}` }, `$${foreignField}`, [`$${foreignField}`]]
|
|
2944
|
+
}
|
|
2945
|
+
}
|
|
2946
|
+
},
|
|
2947
|
+
{ $unwind: "$foreignValues" },
|
|
2948
|
+
{ $match: { foreignValues: { $in: uniqueValues } } },
|
|
2949
|
+
{ $group: { _id: "$foreignValues", documentIds: { $addToSet: "$_id" } } }
|
|
2950
|
+
]);
|
|
2951
|
+
const counts = /* @__PURE__ */ new Map();
|
|
2952
|
+
for (const row of rows) {
|
|
2953
|
+
counts.set(String(row._id), new Set(row.documentIds.map((id) => String(id))));
|
|
2954
|
+
}
|
|
2955
|
+
return { success: true, kind: "single", code: "success" /* Success */, data: counts, query };
|
|
2956
|
+
}
|
|
2746
2957
|
getDocPermissions(doc) {
|
|
2747
2958
|
return getDocPermissions(this.modelName, doc);
|
|
2748
2959
|
}
|
|
@@ -2836,7 +3047,7 @@ var Service = class extends Base {
|
|
|
2836
3047
|
};
|
|
2837
3048
|
|
|
2838
3049
|
// src/services/public-service.ts
|
|
2839
|
-
import { pick as
|
|
3050
|
+
import { pick as pick4 } from "@web-ts-toolkit/utils";
|
|
2840
3051
|
var PublicService = class extends Service {
|
|
2841
3052
|
async _list(filter2, args, options) {
|
|
2842
3053
|
const { select, populate, include, sort, skip, limit, page, pageSize, tasks } = this.resolvePublicListArgs(args);
|
|
@@ -2854,7 +3065,7 @@ var PublicService = class extends Service {
|
|
|
2854
3065
|
return result;
|
|
2855
3066
|
}
|
|
2856
3067
|
const docs = await this.decorateAll(result.data, "list", {
|
|
2857
|
-
mongooseModel: this.model.
|
|
3068
|
+
mongooseModel: this.model.mongooseModel,
|
|
2858
3069
|
modelName: this.modelName,
|
|
2859
3070
|
operation: "list",
|
|
2860
3071
|
resolvedQuery: result.query
|
|
@@ -2875,7 +3086,7 @@ var PublicService = class extends Service {
|
|
|
2875
3086
|
let d = toObject(doc);
|
|
2876
3087
|
d = await this.decorate(d, "create", context);
|
|
2877
3088
|
d = this.runTasks(d, tasks);
|
|
2878
|
-
if (select) d =
|
|
3089
|
+
if (select) d = pick4(d, [...normalizeSelect(select), ...this.baseFieldsExt]);
|
|
2879
3090
|
return d;
|
|
2880
3091
|
}
|
|
2881
3092
|
);
|
|
@@ -2903,12 +3114,17 @@ var PublicService = class extends Service {
|
|
|
2903
3114
|
{ skim, includePermissions, access, populateAccess, lean }
|
|
2904
3115
|
);
|
|
2905
3116
|
if (tryList && (!result.success || !result.data)) {
|
|
3117
|
+
const listAllowed = await this.req.macl.isAllowed(this.modelName, "list");
|
|
3118
|
+
if (!listAllowed) {
|
|
3119
|
+
return { success: false, kind: "error", code: "unauthorized" /* Unauthorized */, errors: ["Unauthorized"] };
|
|
3120
|
+
}
|
|
2906
3121
|
access = "list";
|
|
2907
3122
|
result = await this.findById(
|
|
2908
3123
|
id,
|
|
2909
3124
|
{
|
|
2910
3125
|
select,
|
|
2911
3126
|
populate,
|
|
3127
|
+
include,
|
|
2912
3128
|
overrides: { idFilter }
|
|
2913
3129
|
},
|
|
2914
3130
|
{ skim, includePermissions, access, populateAccess, lean }
|
|
@@ -2938,6 +3154,10 @@ var PublicService = class extends Service {
|
|
|
2938
3154
|
{ skim, includePermissions, access, populateAccess, lean }
|
|
2939
3155
|
);
|
|
2940
3156
|
if (tryList && (!result.success || !result.data)) {
|
|
3157
|
+
const listAllowed = await this.req.macl.isAllowed(this.modelName, "list");
|
|
3158
|
+
if (!listAllowed) {
|
|
3159
|
+
return { success: false, kind: "error", code: "unauthorized" /* Unauthorized */, errors: ["Unauthorized"] };
|
|
3160
|
+
}
|
|
2941
3161
|
access = "list";
|
|
2942
3162
|
result = await this.findOne(
|
|
2943
3163
|
filter2,
|
|
@@ -2945,6 +3165,7 @@ var PublicService = class extends Service {
|
|
|
2945
3165
|
select,
|
|
2946
3166
|
sort,
|
|
2947
3167
|
populate,
|
|
3168
|
+
include,
|
|
2948
3169
|
overrides: {}
|
|
2949
3170
|
},
|
|
2950
3171
|
{ skim, includePermissions, access, populateAccess, lean }
|
|
@@ -2970,8 +3191,8 @@ var PublicService = class extends Service {
|
|
|
2970
3191
|
let d = toObject(doc);
|
|
2971
3192
|
d = await this.decorate(d, "update", context);
|
|
2972
3193
|
d = this.runTasks(d, tasks);
|
|
2973
|
-
if (select) d =
|
|
2974
|
-
else if (!returningAll) d =
|
|
3194
|
+
if (select) d = pick4(d, [...normalizeSelect(select), ...this.baseFieldsExt]);
|
|
3195
|
+
else if (!returningAll) d = pick4(d, [...Object.keys(data), "_id"]);
|
|
2975
3196
|
return d;
|
|
2976
3197
|
}
|
|
2977
3198
|
);
|
|
@@ -3093,7 +3314,7 @@ var PublicService = class extends Service {
|
|
|
3093
3314
|
};
|
|
3094
3315
|
|
|
3095
3316
|
// src/services/data-service.ts
|
|
3096
|
-
import { orderBy, pick as
|
|
3317
|
+
import { orderBy, pick as pick5 } from "@web-ts-toolkit/utils";
|
|
3097
3318
|
var DataService = class {
|
|
3098
3319
|
constructor(req, dataName) {
|
|
3099
3320
|
this.req = req;
|
|
@@ -3115,7 +3336,7 @@ var DataService = class {
|
|
|
3115
3336
|
let doc = await findElement(this.data, _filter);
|
|
3116
3337
|
if (!doc) return { success: false, kind: "error", code: "not_found" /* NotFound */, query };
|
|
3117
3338
|
doc = await this.trimOutputFields(doc, access);
|
|
3118
|
-
if (_select.length > 0) doc =
|
|
3339
|
+
if (_select.length > 0) doc = pick5(doc, _select);
|
|
3119
3340
|
return { success: true, kind: "single", code: "success" /* Success */, data: doc, query };
|
|
3120
3341
|
}
|
|
3121
3342
|
async findById(id, args, options) {
|
|
@@ -3134,6 +3355,7 @@ var DataService = class {
|
|
|
3134
3355
|
const filterErrors = validateClientFilter(filter2);
|
|
3135
3356
|
if (filterErrors.length > 0) return { success: false, kind: "error", code: "bad_request" /* BadRequest */, errors: filterErrors };
|
|
3136
3357
|
const { select, sort, skip, limit, page, pageSize } = args ?? {};
|
|
3358
|
+
const hasExplicitLimit = limit != null || pageSize != null;
|
|
3137
3359
|
const [_filter, _select, pagination] = await Promise.all([
|
|
3138
3360
|
this.genFilter("list", filter2),
|
|
3139
3361
|
this.genQuerySelect("list", select),
|
|
@@ -3146,26 +3368,26 @@ var DataService = class {
|
|
|
3146
3368
|
...pagination
|
|
3147
3369
|
};
|
|
3148
3370
|
if (_filter === false) return { success: false, kind: "error", code: "forbidden" /* Forbidden */, query };
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
return { success: false, kind: "error", code: "bad_request" /* BadRequest */, errors: sortFieldErrors, query };
|
|
3153
|
-
}
|
|
3371
|
+
const sortFieldErrors = validateSortFields(sort, await this.genAllowedFields({}, "list"));
|
|
3372
|
+
if (sortFieldErrors.length > 0) {
|
|
3373
|
+
return { success: false, kind: "error", code: "bad_request" /* BadRequest */, errors: sortFieldErrors, query };
|
|
3154
3374
|
}
|
|
3155
3375
|
let docs = await filterCollection(this.data, _filter);
|
|
3156
3376
|
const totalCount = docs.length;
|
|
3157
3377
|
if (sort) {
|
|
3158
|
-
const
|
|
3159
|
-
docs = orderBy(docs,
|
|
3378
|
+
const normalizedSort = normalizeSortForOrderBy(sort);
|
|
3379
|
+
docs = orderBy(docs, normalizedSort.fields, normalizedSort.orders);
|
|
3160
3380
|
}
|
|
3161
3381
|
const pagedDocs = docs.slice(query.skip, query.limit && query.skip + query.limit);
|
|
3162
|
-
const
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
)
|
|
3382
|
+
const { maxHookConcurrency } = this.getRequestComplexity();
|
|
3383
|
+
const trimmed = await mapWithConcurrencyLimit(pagedDocs, maxHookConcurrency, async (doc) => {
|
|
3384
|
+
doc = await this.trimOutputFields(doc, "list");
|
|
3385
|
+
if (_select.length > 0) doc = pick5(doc, _select);
|
|
3386
|
+
return doc;
|
|
3387
|
+
});
|
|
3388
|
+
if (!hasExplicitLimit && trimmed.length === totalCount) {
|
|
3389
|
+
query.limit = trimmed.length;
|
|
3390
|
+
}
|
|
3169
3391
|
return {
|
|
3170
3392
|
success: true,
|
|
3171
3393
|
kind: "list",
|
|
@@ -3182,6 +3404,9 @@ var DataService = class {
|
|
|
3182
3404
|
decorateAll(docs, access, context) {
|
|
3183
3405
|
return this.req.dacl.decorateAll(this.dataName, docs, access, context);
|
|
3184
3406
|
}
|
|
3407
|
+
getRequestComplexity() {
|
|
3408
|
+
return resolveRequestComplexity(getGlobalOption("requestComplexity"));
|
|
3409
|
+
}
|
|
3185
3410
|
genAllowedFields(doc, access, baseFields) {
|
|
3186
3411
|
return this.req.dacl.genAllowedFields(this.dataName, doc, access, baseFields);
|
|
3187
3412
|
}
|
|
@@ -3203,30 +3428,10 @@ var DataService = class {
|
|
|
3203
3428
|
trimOutputFields(doc, access, baseFields) {
|
|
3204
3429
|
return this.pickAllowedFields(doc, access, baseFields);
|
|
3205
3430
|
}
|
|
3206
|
-
async validateSortFields(sort, access) {
|
|
3207
|
-
const fieldPathPattern = /^[A-Za-z_][A-Za-z0-9_$]*(\.[A-Za-z_][A-Za-z0-9_$]*)*$/;
|
|
3208
|
-
const fields = sort.trim().split(/\s+/).filter(Boolean).map((field) => ({
|
|
3209
|
-
raw: field,
|
|
3210
|
-
name: field.startsWith("-") ? field.slice(1) : field
|
|
3211
|
-
}));
|
|
3212
|
-
const errors = [];
|
|
3213
|
-
for (const { raw, name } of fields) {
|
|
3214
|
-
if (!fieldPathPattern.test(name)) {
|
|
3215
|
-
errors.push({ detail: `Invalid sort field: ${raw}`, pointer: `#/sort` });
|
|
3216
|
-
continue;
|
|
3217
|
-
}
|
|
3218
|
-
if (name === "id" || name === "_id") continue;
|
|
3219
|
-
const allowed = await this.genAllowedFields({}, access);
|
|
3220
|
-
if (!allowed.includes(name)) {
|
|
3221
|
-
errors.push({ detail: `Sort field is not allowed: ${name}`, pointer: `#/sort` });
|
|
3222
|
-
}
|
|
3223
|
-
}
|
|
3224
|
-
return errors;
|
|
3225
|
-
}
|
|
3226
3431
|
};
|
|
3227
3432
|
|
|
3228
3433
|
// src/processors.ts
|
|
3229
|
-
import { cloneDeep, forEach as forEach6, get as get7, isArray as isArray8, isPlainObject as isPlainObject12, map
|
|
3434
|
+
import { cloneDeep, forEach as forEach6, get as get7, isArray as isArray8, isPlainObject as isPlainObject12, map, set as set5 } from "@web-ts-toolkit/utils";
|
|
3230
3435
|
var UNSAFE_PATH_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
3231
3436
|
function failUnsafeSegment(path) {
|
|
3232
3437
|
throw new Error(
|
|
@@ -3270,7 +3475,7 @@ var copyAndDepopulate = (docObject, operations, options = { mutable: true, idFie
|
|
|
3270
3475
|
if (leaf.length === 0) {
|
|
3271
3476
|
depopulated = [];
|
|
3272
3477
|
} else if (leaf.every((item) => isPlainObject12(item))) {
|
|
3273
|
-
depopulated =
|
|
3478
|
+
depopulated = map(leaf, idField).map((id, i) => {
|
|
3274
3479
|
if (id === void 0) {
|
|
3275
3480
|
failMissingId(`${src}[${i}]`);
|
|
3276
3481
|
}
|
|
@@ -3819,7 +4024,7 @@ var Core = class {
|
|
|
3819
4024
|
alwaysSelectFields
|
|
3820
4025
|
});
|
|
3821
4026
|
}
|
|
3822
|
-
async genPopulate(modelName, access = "read", _populate = null) {
|
|
4027
|
+
async genPopulate(modelName, access = "read", _populate = null, subPaths = []) {
|
|
3823
4028
|
if (!_populate) return [];
|
|
3824
4029
|
let populate = Array.isArray(_populate) ? _populate : [_populate];
|
|
3825
4030
|
populate = compact3(
|
|
@@ -3830,12 +4035,14 @@ var Core = class {
|
|
|
3830
4035
|
path: p.path,
|
|
3831
4036
|
select: normalizeSelect(p.select)
|
|
3832
4037
|
};
|
|
3833
|
-
const
|
|
4038
|
+
const pathForSourcePermission = subPaths.length > 0 && ret.path.startsWith(`${subPaths[0]}.`) ? ret.path.slice(subPaths[0].length + 1) : ret.path;
|
|
4039
|
+
const parentPath = pathForSourcePermission.includes(".") ? pathForSourcePermission.split(".")[0] : pathForSourcePermission;
|
|
3834
4040
|
const allowedParentPaths = await this.genSelect(
|
|
3835
4041
|
modelName,
|
|
3836
4042
|
populateAccess,
|
|
3837
4043
|
[parentPath],
|
|
3838
|
-
false
|
|
4044
|
+
false,
|
|
4045
|
+
subPaths
|
|
3839
4046
|
);
|
|
3840
4047
|
if (!allowedParentPaths.includes(parentPath)) return null;
|
|
3841
4048
|
const refModelName = getModelRef(modelName, ret.path);
|
|
@@ -4325,7 +4532,7 @@ var includeItemSchema = z2.object({
|
|
|
4325
4532
|
filter: z2.record(z2.string(), z2.unknown()).optional(),
|
|
4326
4533
|
localField: z2.string().min(1),
|
|
4327
4534
|
foreignField: z2.string().min(1),
|
|
4328
|
-
args: z2.unknown().optional(),
|
|
4535
|
+
args: z2.unknown().optional().describe("Forwarded to list/read includes. Count includes ignore pagination fields so counts remain exact."),
|
|
4329
4536
|
options: z2.unknown().optional()
|
|
4330
4537
|
}).passthrough();
|
|
4331
4538
|
var includeSchema = z2.union([includeItemSchema, z2.array(includeItemSchema)]);
|
|
@@ -4862,7 +5069,7 @@ import { z as z4 } from "zod";
|
|
|
4862
5069
|
var dataListBodySchema = z4.object({
|
|
4863
5070
|
filter: objectOrArraySchema.optional(),
|
|
4864
5071
|
select: projectionSchema.optional(),
|
|
4865
|
-
sort:
|
|
5072
|
+
sort: sortSchema.optional(),
|
|
4866
5073
|
skip: z4.union([nonNegativeIntegerSchema, nonNegativeIntegerString]).optional(),
|
|
4867
5074
|
limit: z4.union([positiveIntegerSchema, positiveIntegerString]).optional(),
|
|
4868
5075
|
page: z4.union([nonNegativeIntegerSchema, nonNegativeIntegerString]).optional(),
|
|
@@ -4954,7 +5161,7 @@ var rootModelSubReadArgsSchema = z5.object({
|
|
|
4954
5161
|
var rootModelCountOptionsSchema = z5.object({}).strict().optional();
|
|
4955
5162
|
var rootDataListArgsSchema = z5.object({
|
|
4956
5163
|
select: projectionSchema.optional(),
|
|
4957
|
-
sort:
|
|
5164
|
+
sort: sortSchema.optional(),
|
|
4958
5165
|
skip: z5.union([nonNegativeIntegerSchema, nonNegativeIntegerString]).optional(),
|
|
4959
5166
|
limit: z5.union([positiveIntegerSchema, positiveIntegerString]).optional(),
|
|
4960
5167
|
page: z5.union([nonNegativeIntegerSchema, nonNegativeIntegerString]).optional(),
|
|
@@ -6194,7 +6401,12 @@ async function decorateDataListResult(svc, result, context = {}) {
|
|
|
6194
6401
|
operation: "list",
|
|
6195
6402
|
resolvedQuery: result.query
|
|
6196
6403
|
};
|
|
6197
|
-
const
|
|
6404
|
+
const { maxHookConcurrency } = svc.getRequestComplexity();
|
|
6405
|
+
const decoratedDocs = await mapWithConcurrencyLimit(
|
|
6406
|
+
result.data,
|
|
6407
|
+
maxHookConcurrency,
|
|
6408
|
+
(doc) => svc.decorate(doc, "list", decorateContext)
|
|
6409
|
+
);
|
|
6198
6410
|
const data = await svc.decorateAll(decoratedDocs, "list", decorateContext);
|
|
6199
6411
|
return {
|
|
6200
6412
|
...result,
|
|
@@ -6562,7 +6774,7 @@ var DataRouter = class {
|
|
|
6562
6774
|
const svc = this.getService(req);
|
|
6563
6775
|
const result = await svc.find(
|
|
6564
6776
|
filter2 ?? {},
|
|
6565
|
-
{ select, sort
|
|
6777
|
+
{ select, sort, skip, limit, page, pageSize },
|
|
6566
6778
|
{ includeCount }
|
|
6567
6779
|
);
|
|
6568
6780
|
handleResultError(result);
|