@classytic/mongokit 3.1.0 → 3.1.1
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 +625 -465
- package/dist/actions/index.d.ts +2 -2
- package/dist/actions/index.js +3 -515
- package/dist/chunks/chunk-2ZN65ZOP.js +93 -0
- package/dist/chunks/chunk-CF6FLC2G.js +46 -0
- package/dist/chunks/chunk-CSLJ2PL2.js +1092 -0
- package/dist/chunks/chunk-IT7DCOKR.js +299 -0
- package/dist/chunks/chunk-M2XHQGZB.js +361 -0
- package/dist/chunks/chunk-SAKSLT47.js +470 -0
- package/dist/chunks/chunk-VJXDGP3C.js +14 -0
- package/dist/{index-3Nkm_Brq.d.ts → index-C2NCVxJK.d.ts} +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.js +49 -2387
- package/dist/{mongooseToJsonSchema-CUQma8QK.d.ts → mongooseToJsonSchema-BKMxPbPp.d.ts} +1 -1
- package/dist/pagination/PaginationEngine.d.ts +1 -1
- package/dist/pagination/PaginationEngine.js +2 -368
- package/dist/plugins/index.d.ts +1 -1
- package/dist/plugins/index.js +4 -1170
- package/dist/{types-CrSoCuWu.d.ts → types-DA0rs2Jh.d.ts} +99 -5
- package/dist/utils/index.d.ts +2 -2
- package/dist/utils/index.js +3 -398
- package/package.json +8 -3
package/dist/index.js
CHANGED
|
@@ -1,873 +1,15 @@
|
|
|
1
|
+
import { getById, getByQuery, getOrCreate, count, exists, update, deleteById, aggregate, distinct } from './chunks/chunk-SAKSLT47.js';
|
|
2
|
+
export { actions_exports as actions } from './chunks/chunk-SAKSLT47.js';
|
|
3
|
+
import { PaginationEngine } from './chunks/chunk-M2XHQGZB.js';
|
|
4
|
+
export { PaginationEngine } from './chunks/chunk-M2XHQGZB.js';
|
|
5
|
+
export { aggregateHelpersPlugin, auditLogPlugin, autoInject, batchOperationsPlugin, blockIf, cachePlugin, cascadePlugin, fieldFilterPlugin, immutableField, methodRegistryPlugin, mongoOperationsPlugin, requireField, softDeletePlugin, subdocumentPlugin, timestampPlugin, uniqueField, validationChainPlugin } from './chunks/chunk-CSLJ2PL2.js';
|
|
6
|
+
import { create, createMany } from './chunks/chunk-CF6FLC2G.js';
|
|
7
|
+
export { buildCrudSchemasFromModel, buildCrudSchemasFromMongooseSchema, createMemoryCache, getImmutableFields, getSystemManagedFields, isFieldUpdateAllowed, validateUpdateBody } from './chunks/chunk-IT7DCOKR.js';
|
|
8
|
+
export { createFieldPreset, filterResponseData, getFieldsForUser, getMongooseProjection } from './chunks/chunk-2ZN65ZOP.js';
|
|
9
|
+
import { createError } from './chunks/chunk-VJXDGP3C.js';
|
|
10
|
+
export { createError } from './chunks/chunk-VJXDGP3C.js';
|
|
1
11
|
import mongoose from 'mongoose';
|
|
2
12
|
|
|
3
|
-
var __defProp = Object.defineProperty;
|
|
4
|
-
var __export = (target, all) => {
|
|
5
|
-
for (var name in all)
|
|
6
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
// src/utils/error.ts
|
|
10
|
-
function createError(status, message) {
|
|
11
|
-
const error = new Error(message);
|
|
12
|
-
error.status = status;
|
|
13
|
-
return error;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
// src/actions/create.ts
|
|
17
|
-
var create_exports = {};
|
|
18
|
-
__export(create_exports, {
|
|
19
|
-
create: () => create,
|
|
20
|
-
createDefault: () => createDefault,
|
|
21
|
-
createMany: () => createMany,
|
|
22
|
-
upsert: () => upsert
|
|
23
|
-
});
|
|
24
|
-
async function create(Model, data, options = {}) {
|
|
25
|
-
const document = new Model(data);
|
|
26
|
-
await document.save({ session: options.session });
|
|
27
|
-
return document;
|
|
28
|
-
}
|
|
29
|
-
async function createMany(Model, dataArray, options = {}) {
|
|
30
|
-
return Model.insertMany(dataArray, {
|
|
31
|
-
session: options.session,
|
|
32
|
-
ordered: options.ordered !== false
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
async function createDefault(Model, overrides = {}, options = {}) {
|
|
36
|
-
const defaults = {};
|
|
37
|
-
Model.schema.eachPath((path, schemaType) => {
|
|
38
|
-
const schemaOptions = schemaType.options;
|
|
39
|
-
if (schemaOptions.default !== void 0 && path !== "_id") {
|
|
40
|
-
defaults[path] = typeof schemaOptions.default === "function" ? schemaOptions.default() : schemaOptions.default;
|
|
41
|
-
}
|
|
42
|
-
});
|
|
43
|
-
return create(Model, { ...defaults, ...overrides }, options);
|
|
44
|
-
}
|
|
45
|
-
async function upsert(Model, query, data, options = {}) {
|
|
46
|
-
return Model.findOneAndUpdate(
|
|
47
|
-
query,
|
|
48
|
-
{ $setOnInsert: data },
|
|
49
|
-
{
|
|
50
|
-
upsert: true,
|
|
51
|
-
new: true,
|
|
52
|
-
runValidators: true,
|
|
53
|
-
session: options.session,
|
|
54
|
-
...options.updatePipeline !== void 0 ? { updatePipeline: options.updatePipeline } : {}
|
|
55
|
-
}
|
|
56
|
-
);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// src/actions/read.ts
|
|
60
|
-
var read_exports = {};
|
|
61
|
-
__export(read_exports, {
|
|
62
|
-
count: () => count,
|
|
63
|
-
exists: () => exists,
|
|
64
|
-
getAll: () => getAll,
|
|
65
|
-
getById: () => getById,
|
|
66
|
-
getByQuery: () => getByQuery,
|
|
67
|
-
getOrCreate: () => getOrCreate,
|
|
68
|
-
tryGetByQuery: () => tryGetByQuery
|
|
69
|
-
});
|
|
70
|
-
function parsePopulate(populate) {
|
|
71
|
-
if (!populate) return [];
|
|
72
|
-
if (typeof populate === "string") {
|
|
73
|
-
return populate.split(",").map((p) => p.trim());
|
|
74
|
-
}
|
|
75
|
-
if (Array.isArray(populate)) {
|
|
76
|
-
return populate.map((p) => typeof p === "string" ? p.trim() : p);
|
|
77
|
-
}
|
|
78
|
-
return [populate];
|
|
79
|
-
}
|
|
80
|
-
async function getById(Model, id, options = {}) {
|
|
81
|
-
const query = options.query ? Model.findOne({ _id: id, ...options.query }) : Model.findById(id);
|
|
82
|
-
if (options.select) query.select(options.select);
|
|
83
|
-
if (options.populate) query.populate(parsePopulate(options.populate));
|
|
84
|
-
if (options.lean) query.lean();
|
|
85
|
-
if (options.session) query.session(options.session);
|
|
86
|
-
const document = await query.exec();
|
|
87
|
-
if (!document && options.throwOnNotFound !== false) {
|
|
88
|
-
throw createError(404, "Document not found");
|
|
89
|
-
}
|
|
90
|
-
return document;
|
|
91
|
-
}
|
|
92
|
-
async function getByQuery(Model, query, options = {}) {
|
|
93
|
-
const mongoQuery = Model.findOne(query);
|
|
94
|
-
if (options.select) mongoQuery.select(options.select);
|
|
95
|
-
if (options.populate) mongoQuery.populate(parsePopulate(options.populate));
|
|
96
|
-
if (options.lean) mongoQuery.lean();
|
|
97
|
-
if (options.session) mongoQuery.session(options.session);
|
|
98
|
-
const document = await mongoQuery.exec();
|
|
99
|
-
if (!document && options.throwOnNotFound !== false) {
|
|
100
|
-
throw createError(404, "Document not found");
|
|
101
|
-
}
|
|
102
|
-
return document;
|
|
103
|
-
}
|
|
104
|
-
async function tryGetByQuery(Model, query, options = {}) {
|
|
105
|
-
return getByQuery(Model, query, { ...options, throwOnNotFound: false });
|
|
106
|
-
}
|
|
107
|
-
async function getAll(Model, query = {}, options = {}) {
|
|
108
|
-
let mongoQuery = Model.find(query);
|
|
109
|
-
if (options.select) mongoQuery = mongoQuery.select(options.select);
|
|
110
|
-
if (options.populate) mongoQuery = mongoQuery.populate(parsePopulate(options.populate));
|
|
111
|
-
if (options.sort) mongoQuery = mongoQuery.sort(options.sort);
|
|
112
|
-
if (options.limit) mongoQuery = mongoQuery.limit(options.limit);
|
|
113
|
-
if (options.skip) mongoQuery = mongoQuery.skip(options.skip);
|
|
114
|
-
mongoQuery = mongoQuery.lean(options.lean !== false);
|
|
115
|
-
if (options.session) mongoQuery = mongoQuery.session(options.session);
|
|
116
|
-
return mongoQuery.exec();
|
|
117
|
-
}
|
|
118
|
-
async function getOrCreate(Model, query, createData, options = {}) {
|
|
119
|
-
return Model.findOneAndUpdate(
|
|
120
|
-
query,
|
|
121
|
-
{ $setOnInsert: createData },
|
|
122
|
-
{
|
|
123
|
-
upsert: true,
|
|
124
|
-
new: true,
|
|
125
|
-
runValidators: true,
|
|
126
|
-
session: options.session,
|
|
127
|
-
...options.updatePipeline !== void 0 ? { updatePipeline: options.updatePipeline } : {}
|
|
128
|
-
}
|
|
129
|
-
);
|
|
130
|
-
}
|
|
131
|
-
async function count(Model, query = {}, options = {}) {
|
|
132
|
-
return Model.countDocuments(query).session(options.session ?? null);
|
|
133
|
-
}
|
|
134
|
-
async function exists(Model, query, options = {}) {
|
|
135
|
-
return Model.exists(query).session(options.session ?? null);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// src/actions/update.ts
|
|
139
|
-
var update_exports = {};
|
|
140
|
-
__export(update_exports, {
|
|
141
|
-
increment: () => increment,
|
|
142
|
-
pullFromArray: () => pullFromArray,
|
|
143
|
-
pushToArray: () => pushToArray,
|
|
144
|
-
update: () => update,
|
|
145
|
-
updateByQuery: () => updateByQuery,
|
|
146
|
-
updateMany: () => updateMany,
|
|
147
|
-
updateWithConstraints: () => updateWithConstraints,
|
|
148
|
-
updateWithValidation: () => updateWithValidation
|
|
149
|
-
});
|
|
150
|
-
function assertUpdatePipelineAllowed(update2, updatePipeline) {
|
|
151
|
-
if (Array.isArray(update2) && updatePipeline !== true) {
|
|
152
|
-
throw createError(
|
|
153
|
-
400,
|
|
154
|
-
"Update pipelines (array updates) are disabled by default; pass `{ updatePipeline: true }` to explicitly allow pipeline-style updates."
|
|
155
|
-
);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
function parsePopulate2(populate) {
|
|
159
|
-
if (!populate) return [];
|
|
160
|
-
if (typeof populate === "string") {
|
|
161
|
-
return populate.split(",").map((p) => p.trim());
|
|
162
|
-
}
|
|
163
|
-
if (Array.isArray(populate)) {
|
|
164
|
-
return populate.map((p) => typeof p === "string" ? p.trim() : p);
|
|
165
|
-
}
|
|
166
|
-
return [populate];
|
|
167
|
-
}
|
|
168
|
-
async function update(Model, id, data, options = {}) {
|
|
169
|
-
assertUpdatePipelineAllowed(data, options.updatePipeline);
|
|
170
|
-
const document = await Model.findByIdAndUpdate(id, data, {
|
|
171
|
-
new: true,
|
|
172
|
-
runValidators: true,
|
|
173
|
-
session: options.session,
|
|
174
|
-
...options.updatePipeline !== void 0 ? { updatePipeline: options.updatePipeline } : {}
|
|
175
|
-
}).select(options.select || "").populate(parsePopulate2(options.populate)).lean(options.lean ?? false);
|
|
176
|
-
if (!document) {
|
|
177
|
-
throw createError(404, "Document not found");
|
|
178
|
-
}
|
|
179
|
-
return document;
|
|
180
|
-
}
|
|
181
|
-
async function updateWithConstraints(Model, id, data, constraints = {}, options = {}) {
|
|
182
|
-
assertUpdatePipelineAllowed(data, options.updatePipeline);
|
|
183
|
-
const query = { _id: id, ...constraints };
|
|
184
|
-
const document = await Model.findOneAndUpdate(query, data, {
|
|
185
|
-
new: true,
|
|
186
|
-
runValidators: true,
|
|
187
|
-
session: options.session,
|
|
188
|
-
...options.updatePipeline !== void 0 ? { updatePipeline: options.updatePipeline } : {}
|
|
189
|
-
}).select(options.select || "").populate(parsePopulate2(options.populate)).lean(options.lean ?? false);
|
|
190
|
-
return document;
|
|
191
|
-
}
|
|
192
|
-
async function updateWithValidation(Model, id, data, validationOptions = {}, options = {}) {
|
|
193
|
-
const { buildConstraints, validateUpdate } = validationOptions;
|
|
194
|
-
assertUpdatePipelineAllowed(data, options.updatePipeline);
|
|
195
|
-
if (buildConstraints) {
|
|
196
|
-
const constraints = buildConstraints(data);
|
|
197
|
-
const document = await updateWithConstraints(Model, id, data, constraints, options);
|
|
198
|
-
if (document) {
|
|
199
|
-
return { success: true, data: document };
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
const existing = await Model.findById(id).select(options.select || "").lean();
|
|
203
|
-
if (!existing) {
|
|
204
|
-
return {
|
|
205
|
-
success: false,
|
|
206
|
-
error: {
|
|
207
|
-
code: 404,
|
|
208
|
-
message: "Document not found"
|
|
209
|
-
}
|
|
210
|
-
};
|
|
211
|
-
}
|
|
212
|
-
if (validateUpdate) {
|
|
213
|
-
const validation = validateUpdate(existing, data);
|
|
214
|
-
if (!validation.valid) {
|
|
215
|
-
return {
|
|
216
|
-
success: false,
|
|
217
|
-
error: {
|
|
218
|
-
code: 403,
|
|
219
|
-
message: validation.message || "Update not allowed",
|
|
220
|
-
violations: validation.violations
|
|
221
|
-
}
|
|
222
|
-
};
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
const updated = await update(Model, id, data, options);
|
|
226
|
-
return { success: true, data: updated };
|
|
227
|
-
}
|
|
228
|
-
async function updateMany(Model, query, data, options = {}) {
|
|
229
|
-
assertUpdatePipelineAllowed(data, options.updatePipeline);
|
|
230
|
-
const result = await Model.updateMany(query, data, {
|
|
231
|
-
runValidators: true,
|
|
232
|
-
session: options.session,
|
|
233
|
-
...options.updatePipeline !== void 0 ? { updatePipeline: options.updatePipeline } : {}
|
|
234
|
-
});
|
|
235
|
-
return {
|
|
236
|
-
matchedCount: result.matchedCount,
|
|
237
|
-
modifiedCount: result.modifiedCount
|
|
238
|
-
};
|
|
239
|
-
}
|
|
240
|
-
async function updateByQuery(Model, query, data, options = {}) {
|
|
241
|
-
assertUpdatePipelineAllowed(data, options.updatePipeline);
|
|
242
|
-
const document = await Model.findOneAndUpdate(query, data, {
|
|
243
|
-
new: true,
|
|
244
|
-
runValidators: true,
|
|
245
|
-
session: options.session,
|
|
246
|
-
...options.updatePipeline !== void 0 ? { updatePipeline: options.updatePipeline } : {}
|
|
247
|
-
}).select(options.select || "").populate(parsePopulate2(options.populate)).lean(options.lean ?? false);
|
|
248
|
-
if (!document && options.throwOnNotFound !== false) {
|
|
249
|
-
throw createError(404, "Document not found");
|
|
250
|
-
}
|
|
251
|
-
return document;
|
|
252
|
-
}
|
|
253
|
-
async function increment(Model, id, field, value = 1, options = {}) {
|
|
254
|
-
return update(Model, id, { $inc: { [field]: value } }, options);
|
|
255
|
-
}
|
|
256
|
-
async function pushToArray(Model, id, field, value, options = {}) {
|
|
257
|
-
return update(Model, id, { $push: { [field]: value } }, options);
|
|
258
|
-
}
|
|
259
|
-
async function pullFromArray(Model, id, field, value, options = {}) {
|
|
260
|
-
return update(Model, id, { $pull: { [field]: value } }, options);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// src/actions/delete.ts
|
|
264
|
-
var delete_exports = {};
|
|
265
|
-
__export(delete_exports, {
|
|
266
|
-
deleteById: () => deleteById,
|
|
267
|
-
deleteByQuery: () => deleteByQuery,
|
|
268
|
-
deleteMany: () => deleteMany,
|
|
269
|
-
restore: () => restore,
|
|
270
|
-
softDelete: () => softDelete
|
|
271
|
-
});
|
|
272
|
-
async function deleteById(Model, id, options = {}) {
|
|
273
|
-
const document = await Model.findByIdAndDelete(id).session(options.session ?? null);
|
|
274
|
-
if (!document) {
|
|
275
|
-
throw createError(404, "Document not found");
|
|
276
|
-
}
|
|
277
|
-
return { success: true, message: "Deleted successfully" };
|
|
278
|
-
}
|
|
279
|
-
async function deleteMany(Model, query, options = {}) {
|
|
280
|
-
const result = await Model.deleteMany(query).session(options.session ?? null);
|
|
281
|
-
return {
|
|
282
|
-
success: true,
|
|
283
|
-
count: result.deletedCount,
|
|
284
|
-
message: "Deleted successfully"
|
|
285
|
-
};
|
|
286
|
-
}
|
|
287
|
-
async function deleteByQuery(Model, query, options = {}) {
|
|
288
|
-
const document = await Model.findOneAndDelete(query).session(options.session ?? null);
|
|
289
|
-
if (!document && options.throwOnNotFound !== false) {
|
|
290
|
-
throw createError(404, "Document not found");
|
|
291
|
-
}
|
|
292
|
-
return { success: true, message: "Deleted successfully" };
|
|
293
|
-
}
|
|
294
|
-
async function softDelete(Model, id, options = {}) {
|
|
295
|
-
const document = await Model.findByIdAndUpdate(
|
|
296
|
-
id,
|
|
297
|
-
{
|
|
298
|
-
deleted: true,
|
|
299
|
-
deletedAt: /* @__PURE__ */ new Date(),
|
|
300
|
-
deletedBy: options.userId
|
|
301
|
-
},
|
|
302
|
-
{ new: true, session: options.session }
|
|
303
|
-
);
|
|
304
|
-
if (!document) {
|
|
305
|
-
throw createError(404, "Document not found");
|
|
306
|
-
}
|
|
307
|
-
return { success: true, message: "Soft deleted successfully" };
|
|
308
|
-
}
|
|
309
|
-
async function restore(Model, id, options = {}) {
|
|
310
|
-
const document = await Model.findByIdAndUpdate(
|
|
311
|
-
id,
|
|
312
|
-
{
|
|
313
|
-
deleted: false,
|
|
314
|
-
deletedAt: null,
|
|
315
|
-
deletedBy: null
|
|
316
|
-
},
|
|
317
|
-
{ new: true, session: options.session }
|
|
318
|
-
);
|
|
319
|
-
if (!document) {
|
|
320
|
-
throw createError(404, "Document not found");
|
|
321
|
-
}
|
|
322
|
-
return { success: true, message: "Restored successfully" };
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
// src/actions/aggregate.ts
|
|
326
|
-
var aggregate_exports = {};
|
|
327
|
-
__export(aggregate_exports, {
|
|
328
|
-
aggregate: () => aggregate,
|
|
329
|
-
aggregatePaginate: () => aggregatePaginate,
|
|
330
|
-
average: () => average,
|
|
331
|
-
countBy: () => countBy,
|
|
332
|
-
distinct: () => distinct,
|
|
333
|
-
facet: () => facet,
|
|
334
|
-
groupBy: () => groupBy,
|
|
335
|
-
lookup: () => lookup,
|
|
336
|
-
minMax: () => minMax,
|
|
337
|
-
sum: () => sum,
|
|
338
|
-
unwind: () => unwind
|
|
339
|
-
});
|
|
340
|
-
async function aggregate(Model, pipeline, options = {}) {
|
|
341
|
-
const aggregation = Model.aggregate(pipeline);
|
|
342
|
-
if (options.session) {
|
|
343
|
-
aggregation.session(options.session);
|
|
344
|
-
}
|
|
345
|
-
return aggregation.exec();
|
|
346
|
-
}
|
|
347
|
-
async function aggregatePaginate(Model, pipeline, options = {}) {
|
|
348
|
-
const page = parseInt(String(options.page || 1), 10);
|
|
349
|
-
const limit = parseInt(String(options.limit || 10), 10);
|
|
350
|
-
const skip = (page - 1) * limit;
|
|
351
|
-
const SAFE_LIMIT = 1e3;
|
|
352
|
-
if (limit > SAFE_LIMIT) {
|
|
353
|
-
console.warn(
|
|
354
|
-
`[mongokit] Large aggregation limit (${limit}). $facet results must be <16MB. Consider using Repository.aggregatePaginate() for safer handling of large datasets.`
|
|
355
|
-
);
|
|
356
|
-
}
|
|
357
|
-
const facetPipeline = [
|
|
358
|
-
...pipeline,
|
|
359
|
-
{
|
|
360
|
-
$facet: {
|
|
361
|
-
docs: [{ $skip: skip }, { $limit: limit }],
|
|
362
|
-
total: [{ $count: "count" }]
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
];
|
|
366
|
-
const aggregation = Model.aggregate(facetPipeline);
|
|
367
|
-
if (options.session) {
|
|
368
|
-
aggregation.session(options.session);
|
|
369
|
-
}
|
|
370
|
-
const [result] = await aggregation.exec();
|
|
371
|
-
const docs = result.docs || [];
|
|
372
|
-
const total = result.total[0]?.count || 0;
|
|
373
|
-
const pages = Math.ceil(total / limit);
|
|
374
|
-
return {
|
|
375
|
-
docs,
|
|
376
|
-
total,
|
|
377
|
-
page,
|
|
378
|
-
limit,
|
|
379
|
-
pages,
|
|
380
|
-
hasNext: page < pages,
|
|
381
|
-
hasPrev: page > 1
|
|
382
|
-
};
|
|
383
|
-
}
|
|
384
|
-
async function groupBy(Model, field, options = {}) {
|
|
385
|
-
const pipeline = [
|
|
386
|
-
{ $group: { _id: `$${field}`, count: { $sum: 1 } } },
|
|
387
|
-
{ $sort: { count: -1 } }
|
|
388
|
-
];
|
|
389
|
-
if (options.limit) {
|
|
390
|
-
pipeline.push({ $limit: options.limit });
|
|
391
|
-
}
|
|
392
|
-
return aggregate(Model, pipeline, options);
|
|
393
|
-
}
|
|
394
|
-
async function countBy(Model, field, query = {}, options = {}) {
|
|
395
|
-
const pipeline = [];
|
|
396
|
-
if (Object.keys(query).length > 0) {
|
|
397
|
-
pipeline.push({ $match: query });
|
|
398
|
-
}
|
|
399
|
-
pipeline.push(
|
|
400
|
-
{ $group: { _id: `$${field}`, count: { $sum: 1 } } },
|
|
401
|
-
{ $sort: { count: -1 } }
|
|
402
|
-
);
|
|
403
|
-
return aggregate(Model, pipeline, options);
|
|
404
|
-
}
|
|
405
|
-
async function lookup(Model, lookupOptions) {
|
|
406
|
-
const { from, localField, foreignField, as, pipeline = [], let: letVars, query = {}, options = {} } = lookupOptions;
|
|
407
|
-
const aggPipeline = [];
|
|
408
|
-
if (Object.keys(query).length > 0) {
|
|
409
|
-
aggPipeline.push({ $match: query });
|
|
410
|
-
}
|
|
411
|
-
const usePipelineForm = pipeline.length > 0 || letVars;
|
|
412
|
-
if (usePipelineForm) {
|
|
413
|
-
if (pipeline.length === 0 && localField && foreignField) {
|
|
414
|
-
const autoPipeline = [
|
|
415
|
-
{
|
|
416
|
-
$match: {
|
|
417
|
-
$expr: {
|
|
418
|
-
$eq: [`$${foreignField}`, `$$${localField}`]
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
];
|
|
423
|
-
aggPipeline.push({
|
|
424
|
-
$lookup: {
|
|
425
|
-
from,
|
|
426
|
-
let: { [localField]: `$${localField}`, ...letVars || {} },
|
|
427
|
-
pipeline: autoPipeline,
|
|
428
|
-
as
|
|
429
|
-
}
|
|
430
|
-
});
|
|
431
|
-
} else {
|
|
432
|
-
aggPipeline.push({
|
|
433
|
-
$lookup: {
|
|
434
|
-
from,
|
|
435
|
-
...letVars && { let: letVars },
|
|
436
|
-
pipeline,
|
|
437
|
-
as
|
|
438
|
-
}
|
|
439
|
-
});
|
|
440
|
-
}
|
|
441
|
-
} else {
|
|
442
|
-
aggPipeline.push({
|
|
443
|
-
$lookup: {
|
|
444
|
-
from,
|
|
445
|
-
localField,
|
|
446
|
-
foreignField,
|
|
447
|
-
as
|
|
448
|
-
}
|
|
449
|
-
});
|
|
450
|
-
}
|
|
451
|
-
return aggregate(Model, aggPipeline, options);
|
|
452
|
-
}
|
|
453
|
-
async function unwind(Model, field, options = {}) {
|
|
454
|
-
const pipeline = [
|
|
455
|
-
{
|
|
456
|
-
$unwind: {
|
|
457
|
-
path: `$${field}`,
|
|
458
|
-
preserveNullAndEmptyArrays: options.preserveEmpty !== false
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
];
|
|
462
|
-
return aggregate(Model, pipeline, { session: options.session });
|
|
463
|
-
}
|
|
464
|
-
async function facet(Model, facets, options = {}) {
|
|
465
|
-
const pipeline = [{ $facet: facets }];
|
|
466
|
-
return aggregate(Model, pipeline, options);
|
|
467
|
-
}
|
|
468
|
-
async function distinct(Model, field, query = {}, options = {}) {
|
|
469
|
-
return Model.distinct(field, query).session(options.session ?? null);
|
|
470
|
-
}
|
|
471
|
-
async function sum(Model, field, query = {}, options = {}) {
|
|
472
|
-
const pipeline = [];
|
|
473
|
-
if (Object.keys(query).length > 0) {
|
|
474
|
-
pipeline.push({ $match: query });
|
|
475
|
-
}
|
|
476
|
-
pipeline.push({
|
|
477
|
-
$group: {
|
|
478
|
-
_id: null,
|
|
479
|
-
total: { $sum: `$${field}` }
|
|
480
|
-
}
|
|
481
|
-
});
|
|
482
|
-
const result = await aggregate(Model, pipeline, options);
|
|
483
|
-
return result[0]?.total || 0;
|
|
484
|
-
}
|
|
485
|
-
async function average(Model, field, query = {}, options = {}) {
|
|
486
|
-
const pipeline = [];
|
|
487
|
-
if (Object.keys(query).length > 0) {
|
|
488
|
-
pipeline.push({ $match: query });
|
|
489
|
-
}
|
|
490
|
-
pipeline.push({
|
|
491
|
-
$group: {
|
|
492
|
-
_id: null,
|
|
493
|
-
average: { $avg: `$${field}` }
|
|
494
|
-
}
|
|
495
|
-
});
|
|
496
|
-
const result = await aggregate(Model, pipeline, options);
|
|
497
|
-
return result[0]?.average || 0;
|
|
498
|
-
}
|
|
499
|
-
async function minMax(Model, field, query = {}, options = {}) {
|
|
500
|
-
const pipeline = [];
|
|
501
|
-
if (Object.keys(query).length > 0) {
|
|
502
|
-
pipeline.push({ $match: query });
|
|
503
|
-
}
|
|
504
|
-
pipeline.push({
|
|
505
|
-
$group: {
|
|
506
|
-
_id: null,
|
|
507
|
-
min: { $min: `$${field}` },
|
|
508
|
-
max: { $max: `$${field}` }
|
|
509
|
-
}
|
|
510
|
-
});
|
|
511
|
-
const result = await aggregate(Model, pipeline, options);
|
|
512
|
-
return result[0] || { min: null, max: null };
|
|
513
|
-
}
|
|
514
|
-
function encodeCursor(doc, primaryField, sort, version = 1) {
|
|
515
|
-
const primaryValue = doc[primaryField];
|
|
516
|
-
const idValue = doc._id;
|
|
517
|
-
const payload = {
|
|
518
|
-
v: serializeValue(primaryValue),
|
|
519
|
-
t: getValueType(primaryValue),
|
|
520
|
-
id: serializeValue(idValue),
|
|
521
|
-
idType: getValueType(idValue),
|
|
522
|
-
sort,
|
|
523
|
-
ver: version
|
|
524
|
-
};
|
|
525
|
-
return Buffer.from(JSON.stringify(payload)).toString("base64");
|
|
526
|
-
}
|
|
527
|
-
function decodeCursor(token) {
|
|
528
|
-
try {
|
|
529
|
-
const json = Buffer.from(token, "base64").toString("utf-8");
|
|
530
|
-
const payload = JSON.parse(json);
|
|
531
|
-
return {
|
|
532
|
-
value: rehydrateValue(payload.v, payload.t),
|
|
533
|
-
id: rehydrateValue(payload.id, payload.idType),
|
|
534
|
-
sort: payload.sort,
|
|
535
|
-
version: payload.ver
|
|
536
|
-
};
|
|
537
|
-
} catch {
|
|
538
|
-
throw new Error("Invalid cursor token");
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
function validateCursorSort(cursorSort, currentSort) {
|
|
542
|
-
const cursorSortStr = JSON.stringify(cursorSort);
|
|
543
|
-
const currentSortStr = JSON.stringify(currentSort);
|
|
544
|
-
if (cursorSortStr !== currentSortStr) {
|
|
545
|
-
throw new Error("Cursor sort does not match current query sort");
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
function validateCursorVersion(cursorVersion, expectedVersion) {
|
|
549
|
-
if (cursorVersion !== expectedVersion) {
|
|
550
|
-
throw new Error(`Cursor version ${cursorVersion} does not match expected version ${expectedVersion}`);
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
function serializeValue(value) {
|
|
554
|
-
if (value instanceof Date) return value.toISOString();
|
|
555
|
-
if (value instanceof mongoose.Types.ObjectId) return value.toString();
|
|
556
|
-
return value;
|
|
557
|
-
}
|
|
558
|
-
function getValueType(value) {
|
|
559
|
-
if (value instanceof Date) return "date";
|
|
560
|
-
if (value instanceof mongoose.Types.ObjectId) return "objectid";
|
|
561
|
-
if (typeof value === "boolean") return "boolean";
|
|
562
|
-
if (typeof value === "number") return "number";
|
|
563
|
-
if (typeof value === "string") return "string";
|
|
564
|
-
return "unknown";
|
|
565
|
-
}
|
|
566
|
-
function rehydrateValue(serialized, type) {
|
|
567
|
-
switch (type) {
|
|
568
|
-
case "date":
|
|
569
|
-
return new Date(serialized);
|
|
570
|
-
case "objectid":
|
|
571
|
-
return new mongoose.Types.ObjectId(serialized);
|
|
572
|
-
case "boolean":
|
|
573
|
-
return Boolean(serialized);
|
|
574
|
-
case "number":
|
|
575
|
-
return Number(serialized);
|
|
576
|
-
default:
|
|
577
|
-
return serialized;
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
// src/pagination/utils/sort.ts
|
|
582
|
-
function normalizeSort(sort) {
|
|
583
|
-
const normalized = {};
|
|
584
|
-
Object.keys(sort).forEach((key) => {
|
|
585
|
-
if (key !== "_id") normalized[key] = sort[key];
|
|
586
|
-
});
|
|
587
|
-
if (sort._id !== void 0) {
|
|
588
|
-
normalized._id = sort._id;
|
|
589
|
-
}
|
|
590
|
-
return normalized;
|
|
591
|
-
}
|
|
592
|
-
function validateKeysetSort(sort) {
|
|
593
|
-
const keys = Object.keys(sort);
|
|
594
|
-
if (keys.length === 1 && keys[0] !== "_id") {
|
|
595
|
-
const field = keys[0];
|
|
596
|
-
const direction = sort[field];
|
|
597
|
-
return normalizeSort({ [field]: direction, _id: direction });
|
|
598
|
-
}
|
|
599
|
-
if (keys.length === 1 && keys[0] === "_id") {
|
|
600
|
-
return normalizeSort(sort);
|
|
601
|
-
}
|
|
602
|
-
if (keys.length === 2) {
|
|
603
|
-
if (!keys.includes("_id")) {
|
|
604
|
-
throw new Error("Keyset pagination requires _id as tie-breaker");
|
|
605
|
-
}
|
|
606
|
-
const primaryField = keys.find((k) => k !== "_id");
|
|
607
|
-
const primaryDirection = sort[primaryField];
|
|
608
|
-
const idDirection = sort._id;
|
|
609
|
-
if (primaryDirection !== idDirection) {
|
|
610
|
-
throw new Error("_id direction must match primary field direction");
|
|
611
|
-
}
|
|
612
|
-
return normalizeSort(sort);
|
|
613
|
-
}
|
|
614
|
-
throw new Error("Keyset pagination only supports single field + _id");
|
|
615
|
-
}
|
|
616
|
-
function getPrimaryField(sort) {
|
|
617
|
-
const keys = Object.keys(sort);
|
|
618
|
-
return keys.find((k) => k !== "_id") || "_id";
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
// src/pagination/utils/filter.ts
|
|
622
|
-
function buildKeysetFilter(baseFilters, sort, cursorValue, cursorId) {
|
|
623
|
-
const primaryField = Object.keys(sort).find((k) => k !== "_id") || "_id";
|
|
624
|
-
const direction = sort[primaryField];
|
|
625
|
-
const operator = direction === 1 ? "$gt" : "$lt";
|
|
626
|
-
return {
|
|
627
|
-
...baseFilters,
|
|
628
|
-
$or: [
|
|
629
|
-
{ [primaryField]: { [operator]: cursorValue } },
|
|
630
|
-
{
|
|
631
|
-
[primaryField]: cursorValue,
|
|
632
|
-
_id: { [operator]: cursorId }
|
|
633
|
-
}
|
|
634
|
-
]
|
|
635
|
-
};
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
// src/pagination/utils/limits.ts
|
|
639
|
-
function validateLimit(limit, config) {
|
|
640
|
-
const parsed = Number(limit);
|
|
641
|
-
if (!Number.isFinite(parsed) || parsed < 1) {
|
|
642
|
-
return config.defaultLimit || 10;
|
|
643
|
-
}
|
|
644
|
-
return Math.min(Math.floor(parsed), config.maxLimit || 100);
|
|
645
|
-
}
|
|
646
|
-
function validatePage(page, config) {
|
|
647
|
-
const parsed = Number(page);
|
|
648
|
-
if (!Number.isFinite(parsed) || parsed < 1) {
|
|
649
|
-
return 1;
|
|
650
|
-
}
|
|
651
|
-
const sanitized = Math.floor(parsed);
|
|
652
|
-
if (sanitized > (config.maxPage || 1e4)) {
|
|
653
|
-
throw new Error(`Page ${sanitized} exceeds maximum ${config.maxPage || 1e4}`);
|
|
654
|
-
}
|
|
655
|
-
return sanitized;
|
|
656
|
-
}
|
|
657
|
-
function shouldWarnDeepPagination(page, threshold) {
|
|
658
|
-
return page > threshold;
|
|
659
|
-
}
|
|
660
|
-
function calculateSkip(page, limit) {
|
|
661
|
-
return (page - 1) * limit;
|
|
662
|
-
}
|
|
663
|
-
function calculateTotalPages(total, limit) {
|
|
664
|
-
return Math.ceil(total / limit);
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
// src/pagination/PaginationEngine.ts
|
|
668
|
-
var PaginationEngine = class {
|
|
669
|
-
Model;
|
|
670
|
-
config;
|
|
671
|
-
/**
|
|
672
|
-
* Create a new pagination engine
|
|
673
|
-
*
|
|
674
|
-
* @param Model - Mongoose model to paginate
|
|
675
|
-
* @param config - Pagination configuration
|
|
676
|
-
*/
|
|
677
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
678
|
-
constructor(Model, config = {}) {
|
|
679
|
-
this.Model = Model;
|
|
680
|
-
this.config = {
|
|
681
|
-
defaultLimit: config.defaultLimit || 10,
|
|
682
|
-
maxLimit: config.maxLimit || 100,
|
|
683
|
-
maxPage: config.maxPage || 1e4,
|
|
684
|
-
deepPageThreshold: config.deepPageThreshold || 100,
|
|
685
|
-
cursorVersion: config.cursorVersion || 1,
|
|
686
|
-
useEstimatedCount: config.useEstimatedCount || false
|
|
687
|
-
};
|
|
688
|
-
}
|
|
689
|
-
/**
|
|
690
|
-
* Offset-based pagination using skip/limit
|
|
691
|
-
* Best for small datasets and when users need random page access
|
|
692
|
-
* O(n) performance - slower for deep pages
|
|
693
|
-
*
|
|
694
|
-
* @param options - Pagination options
|
|
695
|
-
* @returns Pagination result with total count
|
|
696
|
-
*
|
|
697
|
-
* @example
|
|
698
|
-
* const result = await engine.paginate({
|
|
699
|
-
* filters: { status: 'active' },
|
|
700
|
-
* sort: { createdAt: -1 },
|
|
701
|
-
* page: 1,
|
|
702
|
-
* limit: 20
|
|
703
|
-
* });
|
|
704
|
-
* console.log(result.docs, result.total, result.hasNext);
|
|
705
|
-
*/
|
|
706
|
-
async paginate(options = {}) {
|
|
707
|
-
const {
|
|
708
|
-
filters = {},
|
|
709
|
-
sort = { _id: -1 },
|
|
710
|
-
page = 1,
|
|
711
|
-
limit = this.config.defaultLimit,
|
|
712
|
-
select,
|
|
713
|
-
populate = [],
|
|
714
|
-
lean = true,
|
|
715
|
-
session
|
|
716
|
-
} = options;
|
|
717
|
-
const sanitizedPage = validatePage(page, this.config);
|
|
718
|
-
const sanitizedLimit = validateLimit(limit, this.config);
|
|
719
|
-
const skip = calculateSkip(sanitizedPage, sanitizedLimit);
|
|
720
|
-
let query = this.Model.find(filters);
|
|
721
|
-
if (select) query = query.select(select);
|
|
722
|
-
if (populate && (Array.isArray(populate) ? populate.length : populate)) {
|
|
723
|
-
query = query.populate(populate);
|
|
724
|
-
}
|
|
725
|
-
query = query.sort(sort).skip(skip).limit(sanitizedLimit).lean(lean);
|
|
726
|
-
if (session) query = query.session(session);
|
|
727
|
-
const hasFilters = Object.keys(filters).length > 0;
|
|
728
|
-
const useEstimated = this.config.useEstimatedCount && !hasFilters;
|
|
729
|
-
const [docs, total] = await Promise.all([
|
|
730
|
-
query.exec(),
|
|
731
|
-
useEstimated ? this.Model.estimatedDocumentCount() : this.Model.countDocuments(filters).session(session ?? null)
|
|
732
|
-
]);
|
|
733
|
-
const totalPages = calculateTotalPages(total, sanitizedLimit);
|
|
734
|
-
const warning = shouldWarnDeepPagination(sanitizedPage, this.config.deepPageThreshold) ? `Deep pagination (page ${sanitizedPage}). Consider getAll({ after, sort, limit }) for better performance.` : void 0;
|
|
735
|
-
return {
|
|
736
|
-
method: "offset",
|
|
737
|
-
docs,
|
|
738
|
-
page: sanitizedPage,
|
|
739
|
-
limit: sanitizedLimit,
|
|
740
|
-
total,
|
|
741
|
-
pages: totalPages,
|
|
742
|
-
hasNext: sanitizedPage < totalPages,
|
|
743
|
-
hasPrev: sanitizedPage > 1,
|
|
744
|
-
...warning && { warning }
|
|
745
|
-
};
|
|
746
|
-
}
|
|
747
|
-
/**
|
|
748
|
-
* Keyset (cursor-based) pagination for high-performance streaming
|
|
749
|
-
* Best for large datasets, infinite scroll, real-time feeds
|
|
750
|
-
* O(1) performance - consistent speed regardless of position
|
|
751
|
-
*
|
|
752
|
-
* @param options - Pagination options (sort is required)
|
|
753
|
-
* @returns Pagination result with next cursor
|
|
754
|
-
*
|
|
755
|
-
* @example
|
|
756
|
-
* // First page
|
|
757
|
-
* const page1 = await engine.stream({
|
|
758
|
-
* sort: { createdAt: -1 },
|
|
759
|
-
* limit: 20
|
|
760
|
-
* });
|
|
761
|
-
*
|
|
762
|
-
* // Next page using cursor
|
|
763
|
-
* const page2 = await engine.stream({
|
|
764
|
-
* sort: { createdAt: -1 },
|
|
765
|
-
* after: page1.next,
|
|
766
|
-
* limit: 20
|
|
767
|
-
* });
|
|
768
|
-
*/
|
|
769
|
-
async stream(options) {
|
|
770
|
-
const {
|
|
771
|
-
filters = {},
|
|
772
|
-
sort,
|
|
773
|
-
after,
|
|
774
|
-
limit = this.config.defaultLimit,
|
|
775
|
-
select,
|
|
776
|
-
populate = [],
|
|
777
|
-
lean = true,
|
|
778
|
-
session
|
|
779
|
-
} = options;
|
|
780
|
-
if (!sort) {
|
|
781
|
-
throw createError(400, "sort is required for keyset pagination");
|
|
782
|
-
}
|
|
783
|
-
const sanitizedLimit = validateLimit(limit, this.config);
|
|
784
|
-
const normalizedSort = validateKeysetSort(sort);
|
|
785
|
-
let query = { ...filters };
|
|
786
|
-
if (after) {
|
|
787
|
-
const cursor = decodeCursor(after);
|
|
788
|
-
validateCursorVersion(cursor.version, this.config.cursorVersion);
|
|
789
|
-
validateCursorSort(cursor.sort, normalizedSort);
|
|
790
|
-
query = buildKeysetFilter(query, normalizedSort, cursor.value, cursor.id);
|
|
791
|
-
}
|
|
792
|
-
let mongoQuery = this.Model.find(query);
|
|
793
|
-
if (select) mongoQuery = mongoQuery.select(select);
|
|
794
|
-
if (populate && (Array.isArray(populate) ? populate.length : populate)) {
|
|
795
|
-
mongoQuery = mongoQuery.populate(populate);
|
|
796
|
-
}
|
|
797
|
-
mongoQuery = mongoQuery.sort(normalizedSort).limit(sanitizedLimit + 1).lean(lean);
|
|
798
|
-
if (session) mongoQuery = mongoQuery.session(session);
|
|
799
|
-
const docs = await mongoQuery.exec();
|
|
800
|
-
const hasMore = docs.length > sanitizedLimit;
|
|
801
|
-
if (hasMore) docs.pop();
|
|
802
|
-
const primaryField = getPrimaryField(normalizedSort);
|
|
803
|
-
const nextCursor = hasMore && docs.length > 0 ? encodeCursor(docs[docs.length - 1], primaryField, normalizedSort, this.config.cursorVersion) : null;
|
|
804
|
-
return {
|
|
805
|
-
method: "keyset",
|
|
806
|
-
docs,
|
|
807
|
-
limit: sanitizedLimit,
|
|
808
|
-
hasMore,
|
|
809
|
-
next: nextCursor
|
|
810
|
-
};
|
|
811
|
-
}
|
|
812
|
-
/**
|
|
813
|
-
* Aggregate pipeline with pagination
|
|
814
|
-
* Best for complex queries requiring aggregation stages
|
|
815
|
-
* Uses $facet to combine results and count in single query
|
|
816
|
-
*
|
|
817
|
-
* @param options - Aggregation options
|
|
818
|
-
* @returns Pagination result with total count
|
|
819
|
-
*
|
|
820
|
-
* @example
|
|
821
|
-
* const result = await engine.aggregatePaginate({
|
|
822
|
-
* pipeline: [
|
|
823
|
-
* { $match: { status: 'active' } },
|
|
824
|
-
* { $group: { _id: '$category', count: { $sum: 1 } } },
|
|
825
|
-
* { $sort: { count: -1 } }
|
|
826
|
-
* ],
|
|
827
|
-
* page: 1,
|
|
828
|
-
* limit: 20
|
|
829
|
-
* });
|
|
830
|
-
*/
|
|
831
|
-
async aggregatePaginate(options = {}) {
|
|
832
|
-
const {
|
|
833
|
-
pipeline = [],
|
|
834
|
-
page = 1,
|
|
835
|
-
limit = this.config.defaultLimit,
|
|
836
|
-
session
|
|
837
|
-
} = options;
|
|
838
|
-
const sanitizedPage = validatePage(page, this.config);
|
|
839
|
-
const sanitizedLimit = validateLimit(limit, this.config);
|
|
840
|
-
const skip = calculateSkip(sanitizedPage, sanitizedLimit);
|
|
841
|
-
const facetPipeline = [
|
|
842
|
-
...pipeline,
|
|
843
|
-
{
|
|
844
|
-
$facet: {
|
|
845
|
-
docs: [{ $skip: skip }, { $limit: sanitizedLimit }],
|
|
846
|
-
total: [{ $count: "count" }]
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
];
|
|
850
|
-
const aggregation = this.Model.aggregate(facetPipeline);
|
|
851
|
-
if (session) aggregation.session(session);
|
|
852
|
-
const [result] = await aggregation.exec();
|
|
853
|
-
const docs = result.docs;
|
|
854
|
-
const total = result.total[0]?.count || 0;
|
|
855
|
-
const totalPages = calculateTotalPages(total, sanitizedLimit);
|
|
856
|
-
const warning = shouldWarnDeepPagination(sanitizedPage, this.config.deepPageThreshold) ? `Deep pagination in aggregate (page ${sanitizedPage}). Uses $skip internally.` : void 0;
|
|
857
|
-
return {
|
|
858
|
-
method: "aggregate",
|
|
859
|
-
docs,
|
|
860
|
-
page: sanitizedPage,
|
|
861
|
-
limit: sanitizedLimit,
|
|
862
|
-
total,
|
|
863
|
-
pages: totalPages,
|
|
864
|
-
hasNext: sanitizedPage < totalPages,
|
|
865
|
-
hasPrev: sanitizedPage > 1,
|
|
866
|
-
...warning && { warning }
|
|
867
|
-
};
|
|
868
|
-
}
|
|
869
|
-
};
|
|
870
|
-
|
|
871
13
|
// src/query/LookupBuilder.ts
|
|
872
14
|
var LookupBuilder = class _LookupBuilder {
|
|
873
15
|
options = {};
|
|
@@ -1043,12 +185,12 @@ var LookupBuilder = class _LookupBuilder {
|
|
|
1043
185
|
* ```
|
|
1044
186
|
*/
|
|
1045
187
|
static multiple(lookups) {
|
|
1046
|
-
return lookups.flatMap((
|
|
1047
|
-
const builder = new _LookupBuilder(
|
|
1048
|
-
if (
|
|
1049
|
-
if (
|
|
1050
|
-
if (
|
|
1051
|
-
if (
|
|
188
|
+
return lookups.flatMap((lookup) => {
|
|
189
|
+
const builder = new _LookupBuilder(lookup.from).localField(lookup.localField).foreignField(lookup.foreignField);
|
|
190
|
+
if (lookup.as) builder.as(lookup.as);
|
|
191
|
+
if (lookup.single) builder.single(lookup.single);
|
|
192
|
+
if (lookup.pipeline) builder.pipeline(lookup.pipeline);
|
|
193
|
+
if (lookup.let) builder.let(lookup.let);
|
|
1052
194
|
return builder.build();
|
|
1053
195
|
});
|
|
1054
196
|
}
|
|
@@ -1066,12 +208,12 @@ var LookupBuilder = class _LookupBuilder {
|
|
|
1066
208
|
* ```
|
|
1067
209
|
*/
|
|
1068
210
|
static nested(lookups) {
|
|
1069
|
-
return lookups.flatMap((
|
|
1070
|
-
const builder = new _LookupBuilder(
|
|
1071
|
-
if (
|
|
1072
|
-
if (
|
|
1073
|
-
if (
|
|
1074
|
-
if (
|
|
211
|
+
return lookups.flatMap((lookup, index) => {
|
|
212
|
+
const builder = new _LookupBuilder(lookup.from).localField(lookup.localField).foreignField(lookup.foreignField);
|
|
213
|
+
if (lookup.as) builder.as(lookup.as);
|
|
214
|
+
if (lookup.single !== void 0) builder.single(lookup.single);
|
|
215
|
+
if (lookup.pipeline) builder.pipeline(lookup.pipeline);
|
|
216
|
+
if (lookup.let) builder.let(lookup.let);
|
|
1075
217
|
return builder.build();
|
|
1076
218
|
});
|
|
1077
219
|
}
|
|
@@ -1630,7 +772,8 @@ var Repository = class {
|
|
|
1630
772
|
if (context._cacheHit) {
|
|
1631
773
|
return context._cachedResult;
|
|
1632
774
|
}
|
|
1633
|
-
const
|
|
775
|
+
const finalQuery = context.query || query;
|
|
776
|
+
const result = await getByQuery(this.Model, finalQuery, context);
|
|
1634
777
|
await this._emitHook("after:getByQuery", { context, result });
|
|
1635
778
|
return result;
|
|
1636
779
|
}
|
|
@@ -2011,1494 +1154,23 @@ var Repository = class {
|
|
|
2011
1154
|
return createError(500, error.message || "Internal Server Error");
|
|
2012
1155
|
}
|
|
2013
1156
|
};
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
const fields = getFieldsForUser(user, preset);
|
|
2032
|
-
return fields.join(" ");
|
|
2033
|
-
}
|
|
2034
|
-
function filterObject(obj, allowedFields) {
|
|
2035
|
-
if (!obj || typeof obj !== "object" || Array.isArray(obj)) {
|
|
2036
|
-
return obj;
|
|
2037
|
-
}
|
|
2038
|
-
const filtered = {};
|
|
2039
|
-
for (const field of allowedFields) {
|
|
2040
|
-
if (field in obj) {
|
|
2041
|
-
filtered[field] = obj[field];
|
|
2042
|
-
}
|
|
2043
|
-
}
|
|
2044
|
-
return filtered;
|
|
2045
|
-
}
|
|
2046
|
-
function filterResponseData(data, preset, user = null) {
|
|
2047
|
-
const allowedFields = getFieldsForUser(user, preset);
|
|
2048
|
-
if (Array.isArray(data)) {
|
|
2049
|
-
return data.map((item) => filterObject(item, allowedFields));
|
|
2050
|
-
}
|
|
2051
|
-
return filterObject(data, allowedFields);
|
|
2052
|
-
}
|
|
2053
|
-
function createFieldPreset(config) {
|
|
2054
|
-
return {
|
|
2055
|
-
public: config.public || [],
|
|
2056
|
-
authenticated: config.authenticated || [],
|
|
2057
|
-
admin: config.admin || []
|
|
2058
|
-
};
|
|
2059
|
-
}
|
|
2060
|
-
|
|
2061
|
-
// src/plugins/field-filter.plugin.ts
|
|
2062
|
-
function fieldFilterPlugin(fieldPreset) {
|
|
2063
|
-
return {
|
|
2064
|
-
name: "fieldFilter",
|
|
2065
|
-
apply(repo) {
|
|
2066
|
-
const applyFieldFiltering = (context) => {
|
|
2067
|
-
if (!fieldPreset) return;
|
|
2068
|
-
const user = context.context?.user || context.user;
|
|
2069
|
-
const fields = getFieldsForUser(user, fieldPreset);
|
|
2070
|
-
const presetSelect = fields.join(" ");
|
|
2071
|
-
if (context.select) {
|
|
2072
|
-
context.select = `${presetSelect} ${context.select}`;
|
|
2073
|
-
} else {
|
|
2074
|
-
context.select = presetSelect;
|
|
2075
|
-
}
|
|
2076
|
-
};
|
|
2077
|
-
repo.on("before:getAll", applyFieldFiltering);
|
|
2078
|
-
repo.on("before:getById", applyFieldFiltering);
|
|
2079
|
-
repo.on("before:getByQuery", applyFieldFiltering);
|
|
2080
|
-
}
|
|
2081
|
-
};
|
|
2082
|
-
}
|
|
2083
|
-
|
|
2084
|
-
// src/plugins/timestamp.plugin.ts
|
|
2085
|
-
function timestampPlugin() {
|
|
2086
|
-
return {
|
|
2087
|
-
name: "timestamp",
|
|
2088
|
-
apply(repo) {
|
|
2089
|
-
repo.on("before:create", (context) => {
|
|
2090
|
-
if (!context.data) return;
|
|
2091
|
-
const now = /* @__PURE__ */ new Date();
|
|
2092
|
-
if (!context.data.createdAt) context.data.createdAt = now;
|
|
2093
|
-
if (!context.data.updatedAt) context.data.updatedAt = now;
|
|
2094
|
-
});
|
|
2095
|
-
repo.on("before:update", (context) => {
|
|
2096
|
-
if (!context.data) return;
|
|
2097
|
-
context.data.updatedAt = /* @__PURE__ */ new Date();
|
|
2098
|
-
});
|
|
2099
|
-
}
|
|
2100
|
-
};
|
|
2101
|
-
}
|
|
2102
|
-
|
|
2103
|
-
// src/plugins/audit-log.plugin.ts
|
|
2104
|
-
function auditLogPlugin(logger) {
|
|
2105
|
-
return {
|
|
2106
|
-
name: "auditLog",
|
|
2107
|
-
apply(repo) {
|
|
2108
|
-
repo.on("after:create", ({ context, result }) => {
|
|
2109
|
-
logger?.info?.("Document created", {
|
|
2110
|
-
model: context.model || repo.model,
|
|
2111
|
-
id: result?._id,
|
|
2112
|
-
userId: context.user?._id || context.user?.id,
|
|
2113
|
-
organizationId: context.organizationId
|
|
2114
|
-
});
|
|
2115
|
-
});
|
|
2116
|
-
repo.on("after:update", ({ context, result }) => {
|
|
2117
|
-
logger?.info?.("Document updated", {
|
|
2118
|
-
model: context.model || repo.model,
|
|
2119
|
-
id: context.id || result?._id,
|
|
2120
|
-
userId: context.user?._id || context.user?.id,
|
|
2121
|
-
organizationId: context.organizationId
|
|
2122
|
-
});
|
|
2123
|
-
});
|
|
2124
|
-
repo.on("after:delete", ({ context }) => {
|
|
2125
|
-
logger?.info?.("Document deleted", {
|
|
2126
|
-
model: context.model || repo.model,
|
|
2127
|
-
id: context.id,
|
|
2128
|
-
userId: context.user?._id || context.user?.id,
|
|
2129
|
-
organizationId: context.organizationId
|
|
2130
|
-
});
|
|
2131
|
-
});
|
|
2132
|
-
repo.on("error:create", ({ context, error }) => {
|
|
2133
|
-
logger?.error?.("Create failed", {
|
|
2134
|
-
model: context.model || repo.model,
|
|
2135
|
-
error: error.message,
|
|
2136
|
-
userId: context.user?._id || context.user?.id
|
|
2137
|
-
});
|
|
2138
|
-
});
|
|
2139
|
-
repo.on("error:update", ({ context, error }) => {
|
|
2140
|
-
logger?.error?.("Update failed", {
|
|
2141
|
-
model: context.model || repo.model,
|
|
2142
|
-
id: context.id,
|
|
2143
|
-
error: error.message,
|
|
2144
|
-
userId: context.user?._id || context.user?.id
|
|
2145
|
-
});
|
|
2146
|
-
});
|
|
2147
|
-
repo.on("error:delete", ({ context, error }) => {
|
|
2148
|
-
logger?.error?.("Delete failed", {
|
|
2149
|
-
model: context.model || repo.model,
|
|
2150
|
-
id: context.id,
|
|
2151
|
-
error: error.message,
|
|
2152
|
-
userId: context.user?._id || context.user?.id
|
|
2153
|
-
});
|
|
2154
|
-
});
|
|
2155
|
-
}
|
|
2156
|
-
};
|
|
2157
|
-
}
|
|
2158
|
-
|
|
2159
|
-
// src/plugins/soft-delete.plugin.ts
|
|
2160
|
-
function buildDeletedFilter(deletedField, filterMode, includeDeleted) {
|
|
2161
|
-
if (includeDeleted) {
|
|
2162
|
-
return {};
|
|
2163
|
-
}
|
|
2164
|
-
if (filterMode === "exists") {
|
|
2165
|
-
return { [deletedField]: { $exists: false } };
|
|
2166
|
-
}
|
|
2167
|
-
return { [deletedField]: null };
|
|
2168
|
-
}
|
|
2169
|
-
function buildGetDeletedFilter(deletedField, filterMode) {
|
|
2170
|
-
if (filterMode === "exists") {
|
|
2171
|
-
return { [deletedField]: { $exists: true, $ne: null } };
|
|
2172
|
-
}
|
|
2173
|
-
return { [deletedField]: { $ne: null } };
|
|
2174
|
-
}
|
|
2175
|
-
function softDeletePlugin(options = {}) {
|
|
2176
|
-
const deletedField = options.deletedField || "deletedAt";
|
|
2177
|
-
const deletedByField = options.deletedByField || "deletedBy";
|
|
2178
|
-
const filterMode = options.filterMode || "null";
|
|
2179
|
-
const addRestoreMethod = options.addRestoreMethod !== false;
|
|
2180
|
-
const addGetDeletedMethod = options.addGetDeletedMethod !== false;
|
|
2181
|
-
const ttlDays = options.ttlDays;
|
|
2182
|
-
return {
|
|
2183
|
-
name: "softDelete",
|
|
2184
|
-
apply(repo) {
|
|
2185
|
-
if (ttlDays !== void 0 && ttlDays > 0) {
|
|
2186
|
-
const ttlSeconds = ttlDays * 24 * 60 * 60;
|
|
2187
|
-
repo.Model.collection.createIndex(
|
|
2188
|
-
{ [deletedField]: 1 },
|
|
2189
|
-
{
|
|
2190
|
-
expireAfterSeconds: ttlSeconds,
|
|
2191
|
-
partialFilterExpression: { [deletedField]: { $type: "date" } }
|
|
2192
|
-
}
|
|
2193
|
-
).catch((err) => {
|
|
2194
|
-
if (!err.message.includes("already exists")) {
|
|
2195
|
-
console.warn(`[softDeletePlugin] Failed to create TTL index: ${err.message}`);
|
|
2196
|
-
}
|
|
2197
|
-
});
|
|
2198
|
-
}
|
|
2199
|
-
repo.on("before:delete", async (context) => {
|
|
2200
|
-
if (options.soft !== false) {
|
|
2201
|
-
const updateData = {
|
|
2202
|
-
[deletedField]: /* @__PURE__ */ new Date()
|
|
2203
|
-
};
|
|
2204
|
-
if (context.user) {
|
|
2205
|
-
updateData[deletedByField] = context.user._id || context.user.id;
|
|
2206
|
-
}
|
|
2207
|
-
await repo.Model.findByIdAndUpdate(context.id, updateData, { session: context.session });
|
|
2208
|
-
context.softDeleted = true;
|
|
2209
|
-
}
|
|
2210
|
-
});
|
|
2211
|
-
repo.on("before:getAll", (context) => {
|
|
2212
|
-
if (options.soft !== false) {
|
|
2213
|
-
const deleteFilter = buildDeletedFilter(deletedField, filterMode, !!context.includeDeleted);
|
|
2214
|
-
if (Object.keys(deleteFilter).length > 0) {
|
|
2215
|
-
const existingFilters = context.filters || {};
|
|
2216
|
-
context.filters = {
|
|
2217
|
-
...existingFilters,
|
|
2218
|
-
...deleteFilter
|
|
2219
|
-
};
|
|
2220
|
-
}
|
|
2221
|
-
}
|
|
2222
|
-
});
|
|
2223
|
-
repo.on("before:getById", (context) => {
|
|
2224
|
-
if (options.soft !== false) {
|
|
2225
|
-
const deleteFilter = buildDeletedFilter(deletedField, filterMode, !!context.includeDeleted);
|
|
2226
|
-
if (Object.keys(deleteFilter).length > 0) {
|
|
2227
|
-
context.query = {
|
|
2228
|
-
...context.query || {},
|
|
2229
|
-
...deleteFilter
|
|
2230
|
-
};
|
|
2231
|
-
}
|
|
2232
|
-
}
|
|
2233
|
-
});
|
|
2234
|
-
repo.on("before:getByQuery", (context) => {
|
|
2235
|
-
if (options.soft !== false) {
|
|
2236
|
-
const deleteFilter = buildDeletedFilter(deletedField, filterMode, !!context.includeDeleted);
|
|
2237
|
-
if (Object.keys(deleteFilter).length > 0) {
|
|
2238
|
-
context.query = {
|
|
2239
|
-
...context.query || {},
|
|
2240
|
-
...deleteFilter
|
|
2241
|
-
};
|
|
2242
|
-
}
|
|
2243
|
-
}
|
|
2244
|
-
});
|
|
2245
|
-
if (addRestoreMethod) {
|
|
2246
|
-
const restoreMethod = async function(id, restoreOptions = {}) {
|
|
2247
|
-
const updateData = {
|
|
2248
|
-
[deletedField]: null,
|
|
2249
|
-
[deletedByField]: null
|
|
2250
|
-
};
|
|
2251
|
-
const result = await this.Model.findByIdAndUpdate(id, { $set: updateData }, {
|
|
2252
|
-
new: true,
|
|
2253
|
-
session: restoreOptions.session
|
|
2254
|
-
});
|
|
2255
|
-
if (!result) {
|
|
2256
|
-
const error = new Error(`Document with id '${id}' not found`);
|
|
2257
|
-
error.status = 404;
|
|
2258
|
-
throw error;
|
|
2259
|
-
}
|
|
2260
|
-
await this.emitAsync("after:restore", { id, result });
|
|
2261
|
-
return result;
|
|
2262
|
-
};
|
|
2263
|
-
if (typeof repo.registerMethod === "function") {
|
|
2264
|
-
repo.registerMethod("restore", restoreMethod);
|
|
2265
|
-
} else {
|
|
2266
|
-
repo.restore = restoreMethod.bind(repo);
|
|
2267
|
-
}
|
|
2268
|
-
}
|
|
2269
|
-
if (addGetDeletedMethod) {
|
|
2270
|
-
const getDeletedMethod = async function(params = {}, getDeletedOptions = {}) {
|
|
2271
|
-
const deletedFilter = buildGetDeletedFilter(deletedField, filterMode);
|
|
2272
|
-
const combinedFilters = {
|
|
2273
|
-
...params.filters || {},
|
|
2274
|
-
...deletedFilter
|
|
2275
|
-
};
|
|
2276
|
-
const page = params.page || 1;
|
|
2277
|
-
const limit = params.limit || 20;
|
|
2278
|
-
const skip = (page - 1) * limit;
|
|
2279
|
-
let sortSpec = { [deletedField]: -1 };
|
|
2280
|
-
if (params.sort) {
|
|
2281
|
-
if (typeof params.sort === "string") {
|
|
2282
|
-
const sortOrder = params.sort.startsWith("-") ? -1 : 1;
|
|
2283
|
-
const sortField = params.sort.startsWith("-") ? params.sort.substring(1) : params.sort;
|
|
2284
|
-
sortSpec = { [sortField]: sortOrder };
|
|
2285
|
-
} else {
|
|
2286
|
-
sortSpec = params.sort;
|
|
2287
|
-
}
|
|
2288
|
-
}
|
|
2289
|
-
let query = this.Model.find(combinedFilters).sort(sortSpec).skip(skip).limit(limit);
|
|
2290
|
-
if (getDeletedOptions.session) {
|
|
2291
|
-
query = query.session(getDeletedOptions.session);
|
|
2292
|
-
}
|
|
2293
|
-
if (getDeletedOptions.select) {
|
|
2294
|
-
const selectValue = Array.isArray(getDeletedOptions.select) ? getDeletedOptions.select.join(" ") : getDeletedOptions.select;
|
|
2295
|
-
query = query.select(selectValue);
|
|
2296
|
-
}
|
|
2297
|
-
if (getDeletedOptions.populate) {
|
|
2298
|
-
const populateSpec = getDeletedOptions.populate;
|
|
2299
|
-
if (typeof populateSpec === "string") {
|
|
2300
|
-
query = query.populate(populateSpec.split(",").map((p) => p.trim()));
|
|
2301
|
-
} else if (Array.isArray(populateSpec)) {
|
|
2302
|
-
query = query.populate(populateSpec);
|
|
2303
|
-
} else {
|
|
2304
|
-
query = query.populate(populateSpec);
|
|
2305
|
-
}
|
|
2306
|
-
}
|
|
2307
|
-
if (getDeletedOptions.lean !== false) {
|
|
2308
|
-
query = query.lean();
|
|
2309
|
-
}
|
|
2310
|
-
const [docs, total] = await Promise.all([
|
|
2311
|
-
query.exec(),
|
|
2312
|
-
this.Model.countDocuments(combinedFilters)
|
|
2313
|
-
]);
|
|
2314
|
-
const pages = Math.ceil(total / limit);
|
|
2315
|
-
return {
|
|
2316
|
-
method: "offset",
|
|
2317
|
-
docs,
|
|
2318
|
-
page,
|
|
2319
|
-
limit,
|
|
2320
|
-
total,
|
|
2321
|
-
pages,
|
|
2322
|
-
hasNext: page < pages,
|
|
2323
|
-
hasPrev: page > 1
|
|
2324
|
-
};
|
|
2325
|
-
};
|
|
2326
|
-
if (typeof repo.registerMethod === "function") {
|
|
2327
|
-
repo.registerMethod("getDeleted", getDeletedMethod);
|
|
2328
|
-
} else {
|
|
2329
|
-
repo.getDeleted = getDeletedMethod.bind(repo);
|
|
2330
|
-
}
|
|
2331
|
-
}
|
|
2332
|
-
}
|
|
2333
|
-
};
|
|
2334
|
-
}
|
|
2335
|
-
|
|
2336
|
-
// src/plugins/method-registry.plugin.ts
|
|
2337
|
-
function methodRegistryPlugin() {
|
|
2338
|
-
return {
|
|
2339
|
-
name: "method-registry",
|
|
2340
|
-
apply(repo) {
|
|
2341
|
-
const registeredMethods = [];
|
|
2342
|
-
repo.registerMethod = function(name, fn) {
|
|
2343
|
-
if (repo[name]) {
|
|
2344
|
-
throw new Error(
|
|
2345
|
-
`Cannot register method '${name}': Method already exists on repository. Choose a different name or use a plugin that doesn't conflict.`
|
|
2346
|
-
);
|
|
2347
|
-
}
|
|
2348
|
-
if (!name || typeof name !== "string") {
|
|
2349
|
-
throw new Error("Method name must be a non-empty string");
|
|
2350
|
-
}
|
|
2351
|
-
if (typeof fn !== "function") {
|
|
2352
|
-
throw new Error(`Method '${name}' must be a function`);
|
|
2353
|
-
}
|
|
2354
|
-
repo[name] = fn.bind(repo);
|
|
2355
|
-
registeredMethods.push(name);
|
|
2356
|
-
repo.emit("method:registered", { name, fn });
|
|
2357
|
-
};
|
|
2358
|
-
repo.hasMethod = function(name) {
|
|
2359
|
-
return typeof repo[name] === "function";
|
|
2360
|
-
};
|
|
2361
|
-
repo.getRegisteredMethods = function() {
|
|
2362
|
-
return [...registeredMethods];
|
|
2363
|
-
};
|
|
2364
|
-
}
|
|
2365
|
-
};
|
|
2366
|
-
}
|
|
2367
|
-
|
|
2368
|
-
// src/plugins/validation-chain.plugin.ts
|
|
2369
|
-
function validationChainPlugin(validators = [], options = {}) {
|
|
2370
|
-
const { stopOnFirstError = true } = options;
|
|
2371
|
-
validators.forEach((v, idx) => {
|
|
2372
|
-
if (!v.name || typeof v.name !== "string") {
|
|
2373
|
-
throw new Error(`Validator at index ${idx} missing 'name' (string)`);
|
|
2374
|
-
}
|
|
2375
|
-
if (typeof v.validate !== "function") {
|
|
2376
|
-
throw new Error(`Validator '${v.name}' missing 'validate' function`);
|
|
2377
|
-
}
|
|
2378
|
-
});
|
|
2379
|
-
const validatorsByOperation = {
|
|
2380
|
-
create: [],
|
|
2381
|
-
update: [],
|
|
2382
|
-
delete: [],
|
|
2383
|
-
createMany: []
|
|
2384
|
-
};
|
|
2385
|
-
const allOperationsValidators = [];
|
|
2386
|
-
validators.forEach((v) => {
|
|
2387
|
-
if (!v.operations || v.operations.length === 0) {
|
|
2388
|
-
allOperationsValidators.push(v);
|
|
2389
|
-
} else {
|
|
2390
|
-
v.operations.forEach((op) => {
|
|
2391
|
-
if (validatorsByOperation[op]) {
|
|
2392
|
-
validatorsByOperation[op].push(v);
|
|
2393
|
-
}
|
|
2394
|
-
});
|
|
2395
|
-
}
|
|
2396
|
-
});
|
|
2397
|
-
return {
|
|
2398
|
-
name: "validation-chain",
|
|
2399
|
-
apply(repo) {
|
|
2400
|
-
const getValidatorsForOperation = (operation) => {
|
|
2401
|
-
const specific = validatorsByOperation[operation] || [];
|
|
2402
|
-
return [...allOperationsValidators, ...specific];
|
|
2403
|
-
};
|
|
2404
|
-
const runValidators = async (operation, context) => {
|
|
2405
|
-
const operationValidators = getValidatorsForOperation(operation);
|
|
2406
|
-
const errors = [];
|
|
2407
|
-
for (const validator of operationValidators) {
|
|
2408
|
-
try {
|
|
2409
|
-
await validator.validate(context, repo);
|
|
2410
|
-
} catch (error) {
|
|
2411
|
-
if (stopOnFirstError) {
|
|
2412
|
-
throw error;
|
|
2413
|
-
}
|
|
2414
|
-
errors.push({
|
|
2415
|
-
validator: validator.name,
|
|
2416
|
-
error: error.message || String(error)
|
|
2417
|
-
});
|
|
2418
|
-
}
|
|
2419
|
-
}
|
|
2420
|
-
if (errors.length > 0) {
|
|
2421
|
-
const err = createError(
|
|
2422
|
-
400,
|
|
2423
|
-
`Validation failed: ${errors.map((e) => `[${e.validator}] ${e.error}`).join("; ")}`
|
|
2424
|
-
);
|
|
2425
|
-
err.validationErrors = errors;
|
|
2426
|
-
throw err;
|
|
2427
|
-
}
|
|
2428
|
-
};
|
|
2429
|
-
repo.on("before:create", async (context) => runValidators("create", context));
|
|
2430
|
-
repo.on("before:createMany", async (context) => runValidators("createMany", context));
|
|
2431
|
-
repo.on("before:update", async (context) => runValidators("update", context));
|
|
2432
|
-
repo.on("before:delete", async (context) => runValidators("delete", context));
|
|
2433
|
-
}
|
|
2434
|
-
};
|
|
2435
|
-
}
|
|
2436
|
-
function blockIf(name, operations, condition, errorMessage) {
|
|
2437
|
-
return {
|
|
2438
|
-
name,
|
|
2439
|
-
operations,
|
|
2440
|
-
validate: (context) => {
|
|
2441
|
-
if (condition(context)) {
|
|
2442
|
-
throw createError(403, errorMessage);
|
|
2443
|
-
}
|
|
2444
|
-
}
|
|
2445
|
-
};
|
|
2446
|
-
}
|
|
2447
|
-
function requireField(field, operations = ["create"]) {
|
|
2448
|
-
return {
|
|
2449
|
-
name: `require-${field}`,
|
|
2450
|
-
operations,
|
|
2451
|
-
validate: (context) => {
|
|
2452
|
-
if (!context.data || context.data[field] === void 0 || context.data[field] === null) {
|
|
2453
|
-
throw createError(400, `Field '${field}' is required`);
|
|
2454
|
-
}
|
|
2455
|
-
}
|
|
2456
|
-
};
|
|
2457
|
-
}
|
|
2458
|
-
function autoInject(field, getter, operations = ["create"]) {
|
|
2459
|
-
return {
|
|
2460
|
-
name: `auto-inject-${field}`,
|
|
2461
|
-
operations,
|
|
2462
|
-
validate: (context) => {
|
|
2463
|
-
if (context.data && !(field in context.data)) {
|
|
2464
|
-
const value = getter(context);
|
|
2465
|
-
if (value !== null && value !== void 0) {
|
|
2466
|
-
context.data[field] = value;
|
|
2467
|
-
}
|
|
2468
|
-
}
|
|
2469
|
-
}
|
|
2470
|
-
};
|
|
2471
|
-
}
|
|
2472
|
-
function immutableField(field) {
|
|
2473
|
-
return {
|
|
2474
|
-
name: `immutable-${field}`,
|
|
2475
|
-
operations: ["update"],
|
|
2476
|
-
validate: (context) => {
|
|
2477
|
-
if (context.data && field in context.data) {
|
|
2478
|
-
throw createError(400, `Field '${field}' cannot be modified`);
|
|
2479
|
-
}
|
|
2480
|
-
}
|
|
2481
|
-
};
|
|
2482
|
-
}
|
|
2483
|
-
function uniqueField(field, errorMessage) {
|
|
2484
|
-
return {
|
|
2485
|
-
name: `unique-${field}`,
|
|
2486
|
-
operations: ["create", "update"],
|
|
2487
|
-
validate: async (context, repo) => {
|
|
2488
|
-
if (!context.data || !context.data[field] || !repo) return;
|
|
2489
|
-
const query = { [field]: context.data[field] };
|
|
2490
|
-
const getByQuery2 = repo.getByQuery;
|
|
2491
|
-
if (typeof getByQuery2 !== "function") return;
|
|
2492
|
-
const existing = await getByQuery2.call(repo, query, {
|
|
2493
|
-
select: "_id",
|
|
2494
|
-
lean: true,
|
|
2495
|
-
throwOnNotFound: false
|
|
2496
|
-
});
|
|
2497
|
-
if (existing && String(existing._id) !== String(context.id)) {
|
|
2498
|
-
throw createError(409, errorMessage || `${field} already exists`);
|
|
2499
|
-
}
|
|
2500
|
-
}
|
|
2501
|
-
};
|
|
2502
|
-
}
|
|
2503
|
-
|
|
2504
|
-
// src/plugins/mongo-operations.plugin.ts
|
|
2505
|
-
function mongoOperationsPlugin() {
|
|
2506
|
-
return {
|
|
2507
|
-
name: "mongo-operations",
|
|
2508
|
-
apply(repo) {
|
|
2509
|
-
if (!repo.registerMethod) {
|
|
2510
|
-
throw new Error(
|
|
2511
|
-
"mongoOperationsPlugin requires methodRegistryPlugin. Add methodRegistryPlugin() before mongoOperationsPlugin() in plugins array."
|
|
2512
|
-
);
|
|
2513
|
-
}
|
|
2514
|
-
repo.registerMethod("upsert", async function(query, data, options = {}) {
|
|
2515
|
-
return upsert(this.Model, query, data, options);
|
|
2516
|
-
});
|
|
2517
|
-
const validateAndUpdateNumeric = async function(id, field, value, operator, operationName, options) {
|
|
2518
|
-
if (typeof value !== "number") {
|
|
2519
|
-
throw createError(400, `${operationName} value must be a number`);
|
|
2520
|
-
}
|
|
2521
|
-
return this.update(id, { [operator]: { [field]: value } }, options);
|
|
2522
|
-
};
|
|
2523
|
-
repo.registerMethod("increment", async function(id, field, value = 1, options = {}) {
|
|
2524
|
-
return validateAndUpdateNumeric.call(this, id, field, value, "$inc", "Increment", options);
|
|
2525
|
-
});
|
|
2526
|
-
repo.registerMethod("decrement", async function(id, field, value = 1, options = {}) {
|
|
2527
|
-
return validateAndUpdateNumeric.call(this, id, field, -value, "$inc", "Decrement", options);
|
|
2528
|
-
});
|
|
2529
|
-
const applyOperator = function(id, field, value, operator, options) {
|
|
2530
|
-
return this.update(id, { [operator]: { [field]: value } }, options);
|
|
2531
|
-
};
|
|
2532
|
-
repo.registerMethod("pushToArray", async function(id, field, value, options = {}) {
|
|
2533
|
-
return applyOperator.call(this, id, field, value, "$push", options);
|
|
2534
|
-
});
|
|
2535
|
-
repo.registerMethod("pullFromArray", async function(id, field, value, options = {}) {
|
|
2536
|
-
return applyOperator.call(this, id, field, value, "$pull", options);
|
|
2537
|
-
});
|
|
2538
|
-
repo.registerMethod("addToSet", async function(id, field, value, options = {}) {
|
|
2539
|
-
return applyOperator.call(this, id, field, value, "$addToSet", options);
|
|
2540
|
-
});
|
|
2541
|
-
repo.registerMethod("setField", async function(id, field, value, options = {}) {
|
|
2542
|
-
return applyOperator.call(this, id, field, value, "$set", options);
|
|
2543
|
-
});
|
|
2544
|
-
repo.registerMethod("unsetField", async function(id, fields, options = {}) {
|
|
2545
|
-
const fieldArray = Array.isArray(fields) ? fields : [fields];
|
|
2546
|
-
const unsetObj = fieldArray.reduce((acc, field) => {
|
|
2547
|
-
acc[field] = "";
|
|
2548
|
-
return acc;
|
|
2549
|
-
}, {});
|
|
2550
|
-
return this.update(id, { $unset: unsetObj }, options);
|
|
2551
|
-
});
|
|
2552
|
-
repo.registerMethod("renameField", async function(id, oldName, newName, options = {}) {
|
|
2553
|
-
return this.update(id, { $rename: { [oldName]: newName } }, options);
|
|
2554
|
-
});
|
|
2555
|
-
repo.registerMethod("multiplyField", async function(id, field, multiplier, options = {}) {
|
|
2556
|
-
return validateAndUpdateNumeric.call(this, id, field, multiplier, "$mul", "Multiplier", options);
|
|
2557
|
-
});
|
|
2558
|
-
repo.registerMethod("setMin", async function(id, field, value, options = {}) {
|
|
2559
|
-
return applyOperator.call(this, id, field, value, "$min", options);
|
|
2560
|
-
});
|
|
2561
|
-
repo.registerMethod("setMax", async function(id, field, value, options = {}) {
|
|
2562
|
-
return applyOperator.call(this, id, field, value, "$max", options);
|
|
2563
|
-
});
|
|
2564
|
-
}
|
|
2565
|
-
};
|
|
2566
|
-
}
|
|
2567
|
-
|
|
2568
|
-
// src/plugins/batch-operations.plugin.ts
|
|
2569
|
-
function batchOperationsPlugin() {
|
|
2570
|
-
return {
|
|
2571
|
-
name: "batch-operations",
|
|
2572
|
-
apply(repo) {
|
|
2573
|
-
if (!repo.registerMethod) {
|
|
2574
|
-
throw new Error("batchOperationsPlugin requires methodRegistryPlugin");
|
|
2575
|
-
}
|
|
2576
|
-
repo.registerMethod("updateMany", async function(query, data, options = {}) {
|
|
2577
|
-
const _buildContext = this._buildContext;
|
|
2578
|
-
const context = await _buildContext.call(this, "updateMany", { query, data, options });
|
|
2579
|
-
try {
|
|
2580
|
-
this.emit("before:updateMany", context);
|
|
2581
|
-
if (Array.isArray(data) && options.updatePipeline !== true) {
|
|
2582
|
-
throw createError(
|
|
2583
|
-
400,
|
|
2584
|
-
"Update pipelines (array updates) are disabled by default; pass `{ updatePipeline: true }` to explicitly allow pipeline-style updates."
|
|
2585
|
-
);
|
|
2586
|
-
}
|
|
2587
|
-
const result = await this.Model.updateMany(query, data, {
|
|
2588
|
-
runValidators: true,
|
|
2589
|
-
session: options.session,
|
|
2590
|
-
...options.updatePipeline !== void 0 ? { updatePipeline: options.updatePipeline } : {}
|
|
2591
|
-
}).exec();
|
|
2592
|
-
this.emit("after:updateMany", { context, result });
|
|
2593
|
-
return result;
|
|
2594
|
-
} catch (error) {
|
|
2595
|
-
this.emit("error:updateMany", { context, error });
|
|
2596
|
-
const _handleError = this._handleError;
|
|
2597
|
-
throw _handleError.call(this, error);
|
|
2598
|
-
}
|
|
2599
|
-
});
|
|
2600
|
-
repo.registerMethod("deleteMany", async function(query, options = {}) {
|
|
2601
|
-
const _buildContext = this._buildContext;
|
|
2602
|
-
const context = await _buildContext.call(this, "deleteMany", { query, options });
|
|
2603
|
-
try {
|
|
2604
|
-
this.emit("before:deleteMany", context);
|
|
2605
|
-
const result = await this.Model.deleteMany(query, {
|
|
2606
|
-
session: options.session
|
|
2607
|
-
}).exec();
|
|
2608
|
-
this.emit("after:deleteMany", { context, result });
|
|
2609
|
-
return result;
|
|
2610
|
-
} catch (error) {
|
|
2611
|
-
this.emit("error:deleteMany", { context, error });
|
|
2612
|
-
const _handleError = this._handleError;
|
|
2613
|
-
throw _handleError.call(this, error);
|
|
2614
|
-
}
|
|
2615
|
-
});
|
|
2616
|
-
}
|
|
2617
|
-
};
|
|
2618
|
-
}
|
|
2619
|
-
|
|
2620
|
-
// src/plugins/aggregate-helpers.plugin.ts
|
|
2621
|
-
function aggregateHelpersPlugin() {
|
|
2622
|
-
return {
|
|
2623
|
-
name: "aggregate-helpers",
|
|
2624
|
-
apply(repo) {
|
|
2625
|
-
if (!repo.registerMethod) {
|
|
2626
|
-
throw new Error("aggregateHelpersPlugin requires methodRegistryPlugin");
|
|
2627
|
-
}
|
|
2628
|
-
repo.registerMethod("groupBy", async function(field, options = {}) {
|
|
2629
|
-
const pipeline = [
|
|
2630
|
-
{ $group: { _id: `$${field}`, count: { $sum: 1 } } },
|
|
2631
|
-
{ $sort: { count: -1 } }
|
|
2632
|
-
];
|
|
2633
|
-
if (options.limit) {
|
|
2634
|
-
pipeline.push({ $limit: options.limit });
|
|
2635
|
-
}
|
|
2636
|
-
const aggregate2 = this.aggregate;
|
|
2637
|
-
return aggregate2.call(this, pipeline, options);
|
|
2638
|
-
});
|
|
2639
|
-
const aggregateOperation = async function(field, operator, resultKey, query = {}, options = {}) {
|
|
2640
|
-
const pipeline = [
|
|
2641
|
-
{ $match: query },
|
|
2642
|
-
{ $group: { _id: null, [resultKey]: { [operator]: `$${field}` } } }
|
|
2643
|
-
];
|
|
2644
|
-
const aggregate2 = this.aggregate;
|
|
2645
|
-
const result = await aggregate2.call(this, pipeline, options);
|
|
2646
|
-
return result[0]?.[resultKey] || 0;
|
|
2647
|
-
};
|
|
2648
|
-
repo.registerMethod("sum", async function(field, query = {}, options = {}) {
|
|
2649
|
-
return aggregateOperation.call(this, field, "$sum", "total", query, options);
|
|
2650
|
-
});
|
|
2651
|
-
repo.registerMethod("average", async function(field, query = {}, options = {}) {
|
|
2652
|
-
return aggregateOperation.call(this, field, "$avg", "avg", query, options);
|
|
2653
|
-
});
|
|
2654
|
-
repo.registerMethod("min", async function(field, query = {}, options = {}) {
|
|
2655
|
-
return aggregateOperation.call(this, field, "$min", "min", query, options);
|
|
2656
|
-
});
|
|
2657
|
-
repo.registerMethod("max", async function(field, query = {}, options = {}) {
|
|
2658
|
-
return aggregateOperation.call(this, field, "$max", "max", query, options);
|
|
2659
|
-
});
|
|
2660
|
-
}
|
|
2661
|
-
};
|
|
2662
|
-
}
|
|
2663
|
-
|
|
2664
|
-
// src/plugins/subdocument.plugin.ts
|
|
2665
|
-
function subdocumentPlugin() {
|
|
2666
|
-
return {
|
|
2667
|
-
name: "subdocument",
|
|
2668
|
-
apply(repo) {
|
|
2669
|
-
if (!repo.registerMethod) {
|
|
2670
|
-
throw new Error("subdocumentPlugin requires methodRegistryPlugin");
|
|
2671
|
-
}
|
|
2672
|
-
repo.registerMethod("addSubdocument", async function(parentId, arrayPath, subData, options = {}) {
|
|
2673
|
-
const update2 = this.update;
|
|
2674
|
-
return update2.call(this, parentId, { $push: { [arrayPath]: subData } }, options);
|
|
2675
|
-
});
|
|
2676
|
-
repo.registerMethod("getSubdocument", async function(parentId, arrayPath, subId, options = {}) {
|
|
2677
|
-
const _executeQuery = this._executeQuery;
|
|
2678
|
-
return _executeQuery.call(this, async (Model) => {
|
|
2679
|
-
const parent = await Model.findById(parentId).session(options.session).exec();
|
|
2680
|
-
if (!parent) throw createError(404, "Parent not found");
|
|
2681
|
-
const parentObj = parent;
|
|
2682
|
-
const arrayField = parentObj[arrayPath];
|
|
2683
|
-
if (!arrayField || typeof arrayField.id !== "function") {
|
|
2684
|
-
throw createError(404, "Array field not found");
|
|
2685
|
-
}
|
|
2686
|
-
const sub = arrayField.id(subId);
|
|
2687
|
-
if (!sub) throw createError(404, "Subdocument not found");
|
|
2688
|
-
return options.lean && typeof sub.toObject === "function" ? sub.toObject() : sub;
|
|
2689
|
-
});
|
|
2690
|
-
});
|
|
2691
|
-
repo.registerMethod("updateSubdocument", async function(parentId, arrayPath, subId, updateData, options = {}) {
|
|
2692
|
-
const _executeQuery = this._executeQuery;
|
|
2693
|
-
return _executeQuery.call(this, async (Model) => {
|
|
2694
|
-
const query = { _id: parentId, [`${arrayPath}._id`]: subId };
|
|
2695
|
-
const update2 = { $set: { [`${arrayPath}.$`]: { ...updateData, _id: subId } } };
|
|
2696
|
-
const result = await Model.findOneAndUpdate(query, update2, {
|
|
2697
|
-
new: true,
|
|
2698
|
-
runValidators: true,
|
|
2699
|
-
session: options.session
|
|
2700
|
-
}).exec();
|
|
2701
|
-
if (!result) throw createError(404, "Parent or subdocument not found");
|
|
2702
|
-
return result;
|
|
2703
|
-
});
|
|
2704
|
-
});
|
|
2705
|
-
repo.registerMethod("deleteSubdocument", async function(parentId, arrayPath, subId, options = {}) {
|
|
2706
|
-
const update2 = this.update;
|
|
2707
|
-
return update2.call(this, parentId, { $pull: { [arrayPath]: { _id: subId } } }, options);
|
|
2708
|
-
});
|
|
2709
|
-
}
|
|
2710
|
-
};
|
|
2711
|
-
}
|
|
2712
|
-
|
|
2713
|
-
// src/utils/cache-keys.ts
|
|
2714
|
-
function hashString(str) {
|
|
2715
|
-
let hash = 5381;
|
|
2716
|
-
for (let i = 0; i < str.length; i++) {
|
|
2717
|
-
hash = (hash << 5) + hash ^ str.charCodeAt(i);
|
|
2718
|
-
}
|
|
2719
|
-
return (hash >>> 0).toString(16);
|
|
2720
|
-
}
|
|
2721
|
-
function stableStringify(obj) {
|
|
2722
|
-
if (obj === null || obj === void 0) return "";
|
|
2723
|
-
if (typeof obj !== "object") return String(obj);
|
|
2724
|
-
if (Array.isArray(obj)) {
|
|
2725
|
-
return "[" + obj.map(stableStringify).join(",") + "]";
|
|
2726
|
-
}
|
|
2727
|
-
const sorted = Object.keys(obj).sort().map((key) => `${key}:${stableStringify(obj[key])}`);
|
|
2728
|
-
return "{" + sorted.join(",") + "}";
|
|
2729
|
-
}
|
|
2730
|
-
function byIdKey(prefix, model, id) {
|
|
2731
|
-
return `${prefix}:id:${model}:${id}`;
|
|
2732
|
-
}
|
|
2733
|
-
function byQueryKey(prefix, model, query, options) {
|
|
2734
|
-
const hashInput = stableStringify({ q: query, s: options?.select, p: options?.populate });
|
|
2735
|
-
return `${prefix}:one:${model}:${hashString(hashInput)}`;
|
|
2736
|
-
}
|
|
2737
|
-
function listQueryKey(prefix, model, version, params) {
|
|
2738
|
-
const hashInput = stableStringify({
|
|
2739
|
-
f: params.filters,
|
|
2740
|
-
s: params.sort,
|
|
2741
|
-
pg: params.page,
|
|
2742
|
-
lm: params.limit,
|
|
2743
|
-
af: params.after,
|
|
2744
|
-
sl: params.select,
|
|
2745
|
-
pp: params.populate
|
|
2746
|
-
});
|
|
2747
|
-
return `${prefix}:list:${model}:${version}:${hashString(hashInput)}`;
|
|
2748
|
-
}
|
|
2749
|
-
function versionKey(prefix, model) {
|
|
2750
|
-
return `${prefix}:ver:${model}`;
|
|
2751
|
-
}
|
|
2752
|
-
function modelPattern(prefix, model) {
|
|
2753
|
-
return `${prefix}:*:${model}:*`;
|
|
2754
|
-
}
|
|
2755
|
-
|
|
2756
|
-
// src/plugins/cache.plugin.ts
|
|
2757
|
-
function cachePlugin(options) {
|
|
2758
|
-
const config = {
|
|
2759
|
-
adapter: options.adapter,
|
|
2760
|
-
ttl: options.ttl ?? 60,
|
|
2761
|
-
byIdTtl: options.byIdTtl ?? options.ttl ?? 60,
|
|
2762
|
-
queryTtl: options.queryTtl ?? options.ttl ?? 60,
|
|
2763
|
-
prefix: options.prefix ?? "mk",
|
|
2764
|
-
debug: options.debug ?? false,
|
|
2765
|
-
skipIfLargeLimit: options.skipIf?.largeLimit ?? 100
|
|
2766
|
-
};
|
|
2767
|
-
const stats = {
|
|
2768
|
-
hits: 0,
|
|
2769
|
-
misses: 0,
|
|
2770
|
-
sets: 0,
|
|
2771
|
-
invalidations: 0
|
|
2772
|
-
};
|
|
2773
|
-
let collectionVersion = 0;
|
|
2774
|
-
const log = (msg, data) => {
|
|
2775
|
-
if (config.debug) {
|
|
2776
|
-
console.log(`[mongokit:cache] ${msg}`, data ?? "");
|
|
2777
|
-
}
|
|
2778
|
-
};
|
|
2779
|
-
return {
|
|
2780
|
-
name: "cache",
|
|
2781
|
-
apply(repo) {
|
|
2782
|
-
const model = repo.model;
|
|
2783
|
-
(async () => {
|
|
2784
|
-
try {
|
|
2785
|
-
const cached = await config.adapter.get(versionKey(config.prefix, model));
|
|
2786
|
-
if (cached !== null) {
|
|
2787
|
-
collectionVersion = cached;
|
|
2788
|
-
log(`Initialized version for ${model}:`, collectionVersion);
|
|
2789
|
-
}
|
|
2790
|
-
} catch (e) {
|
|
2791
|
-
log(`Failed to initialize version for ${model}:`, e);
|
|
2792
|
-
}
|
|
2793
|
-
})();
|
|
2794
|
-
async function bumpVersion() {
|
|
2795
|
-
collectionVersion++;
|
|
2796
|
-
try {
|
|
2797
|
-
await config.adapter.set(versionKey(config.prefix, model), collectionVersion, config.ttl * 10);
|
|
2798
|
-
stats.invalidations++;
|
|
2799
|
-
log(`Bumped version for ${model} to:`, collectionVersion);
|
|
2800
|
-
} catch (e) {
|
|
2801
|
-
log(`Failed to bump version for ${model}:`, e);
|
|
2802
|
-
}
|
|
2803
|
-
}
|
|
2804
|
-
async function invalidateById(id) {
|
|
2805
|
-
const key = byIdKey(config.prefix, model, id);
|
|
2806
|
-
try {
|
|
2807
|
-
await config.adapter.del(key);
|
|
2808
|
-
stats.invalidations++;
|
|
2809
|
-
log(`Invalidated byId cache:`, key);
|
|
2810
|
-
} catch (e) {
|
|
2811
|
-
log(`Failed to invalidate byId cache:`, e);
|
|
2812
|
-
}
|
|
2813
|
-
}
|
|
2814
|
-
repo.on("before:getById", async (context) => {
|
|
2815
|
-
if (context.skipCache) {
|
|
2816
|
-
log(`Skipping cache for getById: ${context.id}`);
|
|
2817
|
-
return;
|
|
2818
|
-
}
|
|
2819
|
-
const id = String(context.id);
|
|
2820
|
-
const key = byIdKey(config.prefix, model, id);
|
|
2821
|
-
try {
|
|
2822
|
-
const cached = await config.adapter.get(key);
|
|
2823
|
-
if (cached !== null) {
|
|
2824
|
-
stats.hits++;
|
|
2825
|
-
log(`Cache HIT for getById:`, key);
|
|
2826
|
-
context._cacheHit = true;
|
|
2827
|
-
context._cachedResult = cached;
|
|
2828
|
-
} else {
|
|
2829
|
-
stats.misses++;
|
|
2830
|
-
log(`Cache MISS for getById:`, key);
|
|
2831
|
-
}
|
|
2832
|
-
} catch (e) {
|
|
2833
|
-
log(`Cache error for getById:`, e);
|
|
2834
|
-
stats.misses++;
|
|
2835
|
-
}
|
|
2836
|
-
});
|
|
2837
|
-
repo.on("before:getByQuery", async (context) => {
|
|
2838
|
-
if (context.skipCache) {
|
|
2839
|
-
log(`Skipping cache for getByQuery`);
|
|
2840
|
-
return;
|
|
2841
|
-
}
|
|
2842
|
-
const query = context.query || {};
|
|
2843
|
-
const key = byQueryKey(config.prefix, model, query, {
|
|
2844
|
-
select: context.select,
|
|
2845
|
-
populate: context.populate
|
|
2846
|
-
});
|
|
2847
|
-
try {
|
|
2848
|
-
const cached = await config.adapter.get(key);
|
|
2849
|
-
if (cached !== null) {
|
|
2850
|
-
stats.hits++;
|
|
2851
|
-
log(`Cache HIT for getByQuery:`, key);
|
|
2852
|
-
context._cacheHit = true;
|
|
2853
|
-
context._cachedResult = cached;
|
|
2854
|
-
} else {
|
|
2855
|
-
stats.misses++;
|
|
2856
|
-
log(`Cache MISS for getByQuery:`, key);
|
|
2857
|
-
}
|
|
2858
|
-
} catch (e) {
|
|
2859
|
-
log(`Cache error for getByQuery:`, e);
|
|
2860
|
-
stats.misses++;
|
|
2861
|
-
}
|
|
2862
|
-
});
|
|
2863
|
-
repo.on("before:getAll", async (context) => {
|
|
2864
|
-
if (context.skipCache) {
|
|
2865
|
-
log(`Skipping cache for getAll`);
|
|
2866
|
-
return;
|
|
2867
|
-
}
|
|
2868
|
-
const limit = context.limit;
|
|
2869
|
-
if (limit && limit > config.skipIfLargeLimit) {
|
|
2870
|
-
log(`Skipping cache for large query (limit: ${limit})`);
|
|
2871
|
-
return;
|
|
2872
|
-
}
|
|
2873
|
-
const params = {
|
|
2874
|
-
filters: context.filters,
|
|
2875
|
-
sort: context.sort,
|
|
2876
|
-
page: context.page,
|
|
2877
|
-
limit,
|
|
2878
|
-
after: context.after,
|
|
2879
|
-
select: context.select,
|
|
2880
|
-
populate: context.populate
|
|
2881
|
-
};
|
|
2882
|
-
const key = listQueryKey(config.prefix, model, collectionVersion, params);
|
|
2883
|
-
try {
|
|
2884
|
-
const cached = await config.adapter.get(key);
|
|
2885
|
-
if (cached !== null) {
|
|
2886
|
-
stats.hits++;
|
|
2887
|
-
log(`Cache HIT for getAll:`, key);
|
|
2888
|
-
context._cacheHit = true;
|
|
2889
|
-
context._cachedResult = cached;
|
|
2890
|
-
} else {
|
|
2891
|
-
stats.misses++;
|
|
2892
|
-
log(`Cache MISS for getAll:`, key);
|
|
2893
|
-
}
|
|
2894
|
-
} catch (e) {
|
|
2895
|
-
log(`Cache error for getAll:`, e);
|
|
2896
|
-
stats.misses++;
|
|
2897
|
-
}
|
|
2898
|
-
});
|
|
2899
|
-
repo.on("after:getById", async (payload) => {
|
|
2900
|
-
const { context, result } = payload;
|
|
2901
|
-
if (context._cacheHit) return;
|
|
2902
|
-
if (context.skipCache) return;
|
|
2903
|
-
if (result === null) return;
|
|
2904
|
-
const id = String(context.id);
|
|
2905
|
-
const key = byIdKey(config.prefix, model, id);
|
|
2906
|
-
const ttl = context.cacheTtl ?? config.byIdTtl;
|
|
2907
|
-
try {
|
|
2908
|
-
await config.adapter.set(key, result, ttl);
|
|
2909
|
-
stats.sets++;
|
|
2910
|
-
log(`Cached getById result:`, key);
|
|
2911
|
-
} catch (e) {
|
|
2912
|
-
log(`Failed to cache getById:`, e);
|
|
2913
|
-
}
|
|
2914
|
-
});
|
|
2915
|
-
repo.on("after:getByQuery", async (payload) => {
|
|
2916
|
-
const { context, result } = payload;
|
|
2917
|
-
if (context._cacheHit) return;
|
|
2918
|
-
if (context.skipCache) return;
|
|
2919
|
-
if (result === null) return;
|
|
2920
|
-
const query = context.query || {};
|
|
2921
|
-
const key = byQueryKey(config.prefix, model, query, {
|
|
2922
|
-
select: context.select,
|
|
2923
|
-
populate: context.populate
|
|
2924
|
-
});
|
|
2925
|
-
const ttl = context.cacheTtl ?? config.queryTtl;
|
|
2926
|
-
try {
|
|
2927
|
-
await config.adapter.set(key, result, ttl);
|
|
2928
|
-
stats.sets++;
|
|
2929
|
-
log(`Cached getByQuery result:`, key);
|
|
2930
|
-
} catch (e) {
|
|
2931
|
-
log(`Failed to cache getByQuery:`, e);
|
|
2932
|
-
}
|
|
2933
|
-
});
|
|
2934
|
-
repo.on("after:getAll", async (payload) => {
|
|
2935
|
-
const { context, result } = payload;
|
|
2936
|
-
if (context._cacheHit) return;
|
|
2937
|
-
if (context.skipCache) return;
|
|
2938
|
-
const limit = context.limit;
|
|
2939
|
-
if (limit && limit > config.skipIfLargeLimit) return;
|
|
2940
|
-
const params = {
|
|
2941
|
-
filters: context.filters,
|
|
2942
|
-
sort: context.sort,
|
|
2943
|
-
page: context.page,
|
|
2944
|
-
limit,
|
|
2945
|
-
after: context.after,
|
|
2946
|
-
select: context.select,
|
|
2947
|
-
populate: context.populate
|
|
2948
|
-
};
|
|
2949
|
-
const key = listQueryKey(config.prefix, model, collectionVersion, params);
|
|
2950
|
-
const ttl = context.cacheTtl ?? config.queryTtl;
|
|
2951
|
-
try {
|
|
2952
|
-
await config.adapter.set(key, result, ttl);
|
|
2953
|
-
stats.sets++;
|
|
2954
|
-
log(`Cached getAll result:`, key);
|
|
2955
|
-
} catch (e) {
|
|
2956
|
-
log(`Failed to cache getAll:`, e);
|
|
2957
|
-
}
|
|
2958
|
-
});
|
|
2959
|
-
repo.on("after:create", async () => {
|
|
2960
|
-
await bumpVersion();
|
|
2961
|
-
});
|
|
2962
|
-
repo.on("after:createMany", async () => {
|
|
2963
|
-
await bumpVersion();
|
|
2964
|
-
});
|
|
2965
|
-
repo.on("after:update", async (payload) => {
|
|
2966
|
-
const { context } = payload;
|
|
2967
|
-
const id = String(context.id);
|
|
2968
|
-
await Promise.all([
|
|
2969
|
-
invalidateById(id),
|
|
2970
|
-
bumpVersion()
|
|
2971
|
-
]);
|
|
2972
|
-
});
|
|
2973
|
-
repo.on("after:updateMany", async () => {
|
|
2974
|
-
await bumpVersion();
|
|
2975
|
-
});
|
|
2976
|
-
repo.on("after:delete", async (payload) => {
|
|
2977
|
-
const { context } = payload;
|
|
2978
|
-
const id = String(context.id);
|
|
2979
|
-
await Promise.all([
|
|
2980
|
-
invalidateById(id),
|
|
2981
|
-
bumpVersion()
|
|
2982
|
-
]);
|
|
2983
|
-
});
|
|
2984
|
-
repo.on("after:deleteMany", async () => {
|
|
2985
|
-
await bumpVersion();
|
|
2986
|
-
});
|
|
2987
|
-
repo.invalidateCache = async (id) => {
|
|
2988
|
-
await invalidateById(id);
|
|
2989
|
-
log(`Manual invalidation for ID:`, id);
|
|
2990
|
-
};
|
|
2991
|
-
repo.invalidateListCache = async () => {
|
|
2992
|
-
await bumpVersion();
|
|
2993
|
-
log(`Manual list cache invalidation for ${model}`);
|
|
2994
|
-
};
|
|
2995
|
-
repo.invalidateAllCache = async () => {
|
|
2996
|
-
if (config.adapter.clear) {
|
|
2997
|
-
try {
|
|
2998
|
-
await config.adapter.clear(modelPattern(config.prefix, model));
|
|
2999
|
-
stats.invalidations++;
|
|
3000
|
-
log(`Full cache invalidation for ${model}`);
|
|
3001
|
-
} catch (e) {
|
|
3002
|
-
log(`Failed full cache invalidation for ${model}:`, e);
|
|
3003
|
-
}
|
|
3004
|
-
} else {
|
|
3005
|
-
await bumpVersion();
|
|
3006
|
-
log(`Partial cache invalidation for ${model} (adapter.clear not available)`);
|
|
3007
|
-
}
|
|
3008
|
-
};
|
|
3009
|
-
repo.getCacheStats = () => ({ ...stats });
|
|
3010
|
-
repo.resetCacheStats = () => {
|
|
3011
|
-
stats.hits = 0;
|
|
3012
|
-
stats.misses = 0;
|
|
3013
|
-
stats.sets = 0;
|
|
3014
|
-
stats.invalidations = 0;
|
|
3015
|
-
};
|
|
3016
|
-
}
|
|
3017
|
-
};
|
|
3018
|
-
}
|
|
3019
|
-
function cascadePlugin(options) {
|
|
3020
|
-
const { relations, parallel = true, logger } = options;
|
|
3021
|
-
if (!relations || relations.length === 0) {
|
|
3022
|
-
throw new Error("cascadePlugin requires at least one relation");
|
|
3023
|
-
}
|
|
3024
|
-
return {
|
|
3025
|
-
name: "cascade",
|
|
3026
|
-
apply(repo) {
|
|
3027
|
-
repo.on("after:delete", async (payload) => {
|
|
3028
|
-
const { context } = payload;
|
|
3029
|
-
const deletedId = context.id;
|
|
3030
|
-
if (!deletedId) {
|
|
3031
|
-
logger?.warn?.("Cascade delete skipped: no document ID in context", {
|
|
3032
|
-
model: context.model
|
|
3033
|
-
});
|
|
3034
|
-
return;
|
|
3035
|
-
}
|
|
3036
|
-
const isSoftDelete = context.softDeleted === true;
|
|
3037
|
-
const cascadeDelete = async (relation) => {
|
|
3038
|
-
const RelatedModel = mongoose.models[relation.model];
|
|
3039
|
-
if (!RelatedModel) {
|
|
3040
|
-
logger?.warn?.(`Cascade delete skipped: model '${relation.model}' not found`, {
|
|
3041
|
-
parentModel: context.model,
|
|
3042
|
-
parentId: String(deletedId)
|
|
3043
|
-
});
|
|
3044
|
-
return;
|
|
3045
|
-
}
|
|
3046
|
-
const query = { [relation.foreignKey]: deletedId };
|
|
3047
|
-
try {
|
|
3048
|
-
const shouldSoftDelete = relation.softDelete ?? isSoftDelete;
|
|
3049
|
-
if (shouldSoftDelete) {
|
|
3050
|
-
const updateResult = await RelatedModel.updateMany(
|
|
3051
|
-
query,
|
|
3052
|
-
{
|
|
3053
|
-
deletedAt: /* @__PURE__ */ new Date(),
|
|
3054
|
-
...context.user ? { deletedBy: context.user._id || context.user.id } : {}
|
|
3055
|
-
},
|
|
3056
|
-
{ session: context.session }
|
|
3057
|
-
);
|
|
3058
|
-
logger?.info?.(`Cascade soft-deleted ${updateResult.modifiedCount} documents`, {
|
|
3059
|
-
parentModel: context.model,
|
|
3060
|
-
parentId: String(deletedId),
|
|
3061
|
-
relatedModel: relation.model,
|
|
3062
|
-
foreignKey: relation.foreignKey,
|
|
3063
|
-
count: updateResult.modifiedCount
|
|
3064
|
-
});
|
|
3065
|
-
} else {
|
|
3066
|
-
const deleteResult = await RelatedModel.deleteMany(query, {
|
|
3067
|
-
session: context.session
|
|
3068
|
-
});
|
|
3069
|
-
logger?.info?.(`Cascade deleted ${deleteResult.deletedCount} documents`, {
|
|
3070
|
-
parentModel: context.model,
|
|
3071
|
-
parentId: String(deletedId),
|
|
3072
|
-
relatedModel: relation.model,
|
|
3073
|
-
foreignKey: relation.foreignKey,
|
|
3074
|
-
count: deleteResult.deletedCount
|
|
3075
|
-
});
|
|
3076
|
-
}
|
|
3077
|
-
} catch (error) {
|
|
3078
|
-
logger?.error?.(`Cascade delete failed for model '${relation.model}'`, {
|
|
3079
|
-
parentModel: context.model,
|
|
3080
|
-
parentId: String(deletedId),
|
|
3081
|
-
relatedModel: relation.model,
|
|
3082
|
-
foreignKey: relation.foreignKey,
|
|
3083
|
-
error: error.message
|
|
3084
|
-
});
|
|
3085
|
-
throw error;
|
|
3086
|
-
}
|
|
3087
|
-
};
|
|
3088
|
-
if (parallel) {
|
|
3089
|
-
await Promise.all(relations.map(cascadeDelete));
|
|
3090
|
-
} else {
|
|
3091
|
-
for (const relation of relations) {
|
|
3092
|
-
await cascadeDelete(relation);
|
|
3093
|
-
}
|
|
3094
|
-
}
|
|
3095
|
-
});
|
|
3096
|
-
repo.on("after:deleteMany", async (payload) => {
|
|
3097
|
-
const { context, result } = payload;
|
|
3098
|
-
const query = context.query;
|
|
3099
|
-
if (!query || Object.keys(query).length === 0) {
|
|
3100
|
-
logger?.warn?.("Cascade deleteMany skipped: empty query", {
|
|
3101
|
-
model: context.model
|
|
3102
|
-
});
|
|
3103
|
-
return;
|
|
3104
|
-
}
|
|
3105
|
-
logger?.warn?.("Cascade deleteMany: use before:deleteMany hook for complete cascade support", {
|
|
3106
|
-
model: context.model
|
|
3107
|
-
});
|
|
3108
|
-
});
|
|
3109
|
-
repo.on("before:deleteMany", async (context) => {
|
|
3110
|
-
const query = context.query;
|
|
3111
|
-
if (!query || Object.keys(query).length === 0) {
|
|
3112
|
-
return;
|
|
3113
|
-
}
|
|
3114
|
-
const docs = await repo.Model.find(query, { _id: 1 }).lean().session(context.session ?? null);
|
|
3115
|
-
const ids = docs.map((doc) => doc._id);
|
|
3116
|
-
context._cascadeIds = ids;
|
|
3117
|
-
});
|
|
3118
|
-
const originalAfterDeleteMany = repo._hooks.get("after:deleteMany") || [];
|
|
3119
|
-
repo._hooks.set("after:deleteMany", [
|
|
3120
|
-
...originalAfterDeleteMany,
|
|
3121
|
-
async (payload) => {
|
|
3122
|
-
const { context } = payload;
|
|
3123
|
-
const ids = context._cascadeIds;
|
|
3124
|
-
if (!ids || ids.length === 0) {
|
|
3125
|
-
return;
|
|
3126
|
-
}
|
|
3127
|
-
const isSoftDelete = context.softDeleted === true;
|
|
3128
|
-
const cascadeDeleteMany = async (relation) => {
|
|
3129
|
-
const RelatedModel = mongoose.models[relation.model];
|
|
3130
|
-
if (!RelatedModel) {
|
|
3131
|
-
logger?.warn?.(`Cascade deleteMany skipped: model '${relation.model}' not found`, {
|
|
3132
|
-
parentModel: context.model
|
|
3133
|
-
});
|
|
3134
|
-
return;
|
|
3135
|
-
}
|
|
3136
|
-
const query = { [relation.foreignKey]: { $in: ids } };
|
|
3137
|
-
const shouldSoftDelete = relation.softDelete ?? isSoftDelete;
|
|
3138
|
-
try {
|
|
3139
|
-
if (shouldSoftDelete) {
|
|
3140
|
-
const updateResult = await RelatedModel.updateMany(
|
|
3141
|
-
query,
|
|
3142
|
-
{
|
|
3143
|
-
deletedAt: /* @__PURE__ */ new Date(),
|
|
3144
|
-
...context.user ? { deletedBy: context.user._id || context.user.id } : {}
|
|
3145
|
-
},
|
|
3146
|
-
{ session: context.session }
|
|
3147
|
-
);
|
|
3148
|
-
logger?.info?.(`Cascade soft-deleted ${updateResult.modifiedCount} documents (bulk)`, {
|
|
3149
|
-
parentModel: context.model,
|
|
3150
|
-
parentCount: ids.length,
|
|
3151
|
-
relatedModel: relation.model,
|
|
3152
|
-
foreignKey: relation.foreignKey,
|
|
3153
|
-
count: updateResult.modifiedCount
|
|
3154
|
-
});
|
|
3155
|
-
} else {
|
|
3156
|
-
const deleteResult = await RelatedModel.deleteMany(query, {
|
|
3157
|
-
session: context.session
|
|
3158
|
-
});
|
|
3159
|
-
logger?.info?.(`Cascade deleted ${deleteResult.deletedCount} documents (bulk)`, {
|
|
3160
|
-
parentModel: context.model,
|
|
3161
|
-
parentCount: ids.length,
|
|
3162
|
-
relatedModel: relation.model,
|
|
3163
|
-
foreignKey: relation.foreignKey,
|
|
3164
|
-
count: deleteResult.deletedCount
|
|
3165
|
-
});
|
|
3166
|
-
}
|
|
3167
|
-
} catch (error) {
|
|
3168
|
-
logger?.error?.(`Cascade deleteMany failed for model '${relation.model}'`, {
|
|
3169
|
-
parentModel: context.model,
|
|
3170
|
-
relatedModel: relation.model,
|
|
3171
|
-
foreignKey: relation.foreignKey,
|
|
3172
|
-
error: error.message
|
|
3173
|
-
});
|
|
3174
|
-
throw error;
|
|
3175
|
-
}
|
|
3176
|
-
};
|
|
3177
|
-
if (parallel) {
|
|
3178
|
-
await Promise.all(relations.map(cascadeDeleteMany));
|
|
3179
|
-
} else {
|
|
3180
|
-
for (const relation of relations) {
|
|
3181
|
-
await cascadeDeleteMany(relation);
|
|
3182
|
-
}
|
|
3183
|
-
}
|
|
3184
|
-
}
|
|
3185
|
-
]);
|
|
3186
|
-
}
|
|
3187
|
-
};
|
|
3188
|
-
}
|
|
3189
|
-
|
|
3190
|
-
// src/utils/memory-cache.ts
|
|
3191
|
-
function createMemoryCache(maxEntries = 1e3) {
|
|
3192
|
-
const cache = /* @__PURE__ */ new Map();
|
|
3193
|
-
function cleanup() {
|
|
3194
|
-
const now = Date.now();
|
|
3195
|
-
for (const [key, entry] of cache) {
|
|
3196
|
-
if (entry.expiresAt < now) {
|
|
3197
|
-
cache.delete(key);
|
|
3198
|
-
}
|
|
3199
|
-
}
|
|
3200
|
-
}
|
|
3201
|
-
function evictOldest() {
|
|
3202
|
-
if (cache.size >= maxEntries) {
|
|
3203
|
-
const firstKey = cache.keys().next().value;
|
|
3204
|
-
if (firstKey) cache.delete(firstKey);
|
|
3205
|
-
}
|
|
3206
|
-
}
|
|
3207
|
-
return {
|
|
3208
|
-
async get(key) {
|
|
3209
|
-
cleanup();
|
|
3210
|
-
const entry = cache.get(key);
|
|
3211
|
-
if (!entry) return null;
|
|
3212
|
-
if (entry.expiresAt < Date.now()) {
|
|
3213
|
-
cache.delete(key);
|
|
3214
|
-
return null;
|
|
3215
|
-
}
|
|
3216
|
-
return entry.value;
|
|
3217
|
-
},
|
|
3218
|
-
async set(key, value, ttl) {
|
|
3219
|
-
cleanup();
|
|
3220
|
-
evictOldest();
|
|
3221
|
-
cache.set(key, {
|
|
3222
|
-
value,
|
|
3223
|
-
expiresAt: Date.now() + ttl * 1e3
|
|
3224
|
-
});
|
|
3225
|
-
},
|
|
3226
|
-
async del(key) {
|
|
3227
|
-
cache.delete(key);
|
|
3228
|
-
},
|
|
3229
|
-
async clear(pattern) {
|
|
3230
|
-
if (!pattern) {
|
|
3231
|
-
cache.clear();
|
|
3232
|
-
return;
|
|
3233
|
-
}
|
|
3234
|
-
const regex = new RegExp(
|
|
3235
|
-
"^" + pattern.replace(/\*/g, ".*").replace(/\?/g, ".") + "$"
|
|
3236
|
-
);
|
|
3237
|
-
for (const key of cache.keys()) {
|
|
3238
|
-
if (regex.test(key)) {
|
|
3239
|
-
cache.delete(key);
|
|
3240
|
-
}
|
|
3241
|
-
}
|
|
3242
|
-
}
|
|
3243
|
-
};
|
|
3244
|
-
}
|
|
3245
|
-
function buildCrudSchemasFromMongooseSchema(mongooseSchema, options = {}) {
|
|
3246
|
-
const jsonCreate = buildJsonSchemaFromPaths(mongooseSchema, options);
|
|
3247
|
-
const jsonUpdate = buildJsonSchemaForUpdate(jsonCreate, options);
|
|
3248
|
-
const jsonParams = {
|
|
3249
|
-
type: "object",
|
|
3250
|
-
properties: { id: { type: "string", pattern: "^[0-9a-fA-F]{24}$" } },
|
|
3251
|
-
required: ["id"]
|
|
3252
|
-
};
|
|
3253
|
-
const tree = mongooseSchema?.obj || {};
|
|
3254
|
-
const jsonQuery = buildJsonSchemaForQuery(tree, options);
|
|
3255
|
-
return { createBody: jsonCreate, updateBody: jsonUpdate, params: jsonParams, listQuery: jsonQuery };
|
|
3256
|
-
}
|
|
3257
|
-
function buildCrudSchemasFromModel(mongooseModel, options = {}) {
|
|
3258
|
-
if (!mongooseModel || !mongooseModel.schema) {
|
|
3259
|
-
throw new Error("Invalid mongoose model");
|
|
3260
|
-
}
|
|
3261
|
-
return buildCrudSchemasFromMongooseSchema(mongooseModel.schema, options);
|
|
3262
|
-
}
|
|
3263
|
-
function getImmutableFields(options = {}) {
|
|
3264
|
-
const immutable = [];
|
|
3265
|
-
const fieldRules = options?.fieldRules || {};
|
|
3266
|
-
Object.entries(fieldRules).forEach(([field, rules]) => {
|
|
3267
|
-
if (rules.immutable || rules.immutableAfterCreate) {
|
|
3268
|
-
immutable.push(field);
|
|
3269
|
-
}
|
|
3270
|
-
});
|
|
3271
|
-
(options?.update?.omitFields || []).forEach((f) => {
|
|
3272
|
-
if (!immutable.includes(f)) immutable.push(f);
|
|
3273
|
-
});
|
|
3274
|
-
return immutable;
|
|
3275
|
-
}
|
|
3276
|
-
function getSystemManagedFields(options = {}) {
|
|
3277
|
-
const systemManaged = [];
|
|
3278
|
-
const fieldRules = options?.fieldRules || {};
|
|
3279
|
-
Object.entries(fieldRules).forEach(([field, rules]) => {
|
|
3280
|
-
if (rules.systemManaged) {
|
|
3281
|
-
systemManaged.push(field);
|
|
3282
|
-
}
|
|
3283
|
-
});
|
|
3284
|
-
return systemManaged;
|
|
3285
|
-
}
|
|
3286
|
-
function isFieldUpdateAllowed(fieldName, options = {}) {
|
|
3287
|
-
const immutableFields = getImmutableFields(options);
|
|
3288
|
-
const systemManagedFields = getSystemManagedFields(options);
|
|
3289
|
-
return !immutableFields.includes(fieldName) && !systemManagedFields.includes(fieldName);
|
|
3290
|
-
}
|
|
3291
|
-
function validateUpdateBody(body = {}, options = {}) {
|
|
3292
|
-
const violations = [];
|
|
3293
|
-
const immutableFields = getImmutableFields(options);
|
|
3294
|
-
const systemManagedFields = getSystemManagedFields(options);
|
|
3295
|
-
Object.keys(body).forEach((field) => {
|
|
3296
|
-
if (immutableFields.includes(field)) {
|
|
3297
|
-
violations.push({ field, reason: "Field is immutable" });
|
|
3298
|
-
} else if (systemManagedFields.includes(field)) {
|
|
3299
|
-
violations.push({ field, reason: "Field is system-managed" });
|
|
3300
|
-
}
|
|
3301
|
-
});
|
|
3302
|
-
return {
|
|
3303
|
-
valid: violations.length === 0,
|
|
3304
|
-
violations
|
|
3305
|
-
};
|
|
3306
|
-
}
|
|
3307
|
-
function buildJsonSchemaFromPaths(mongooseSchema, options) {
|
|
3308
|
-
const properties = {};
|
|
3309
|
-
const required = [];
|
|
3310
|
-
const paths = mongooseSchema.paths;
|
|
3311
|
-
const rootFields = /* @__PURE__ */ new Map();
|
|
3312
|
-
for (const [path, schemaType] of Object.entries(paths)) {
|
|
3313
|
-
if (path === "_id" || path === "__v") continue;
|
|
3314
|
-
const parts = path.split(".");
|
|
3315
|
-
const rootField = parts[0];
|
|
3316
|
-
if (!rootFields.has(rootField)) {
|
|
3317
|
-
rootFields.set(rootField, []);
|
|
3318
|
-
}
|
|
3319
|
-
rootFields.get(rootField).push({ path, schemaType });
|
|
3320
|
-
}
|
|
3321
|
-
for (const [rootField, fieldPaths] of rootFields.entries()) {
|
|
3322
|
-
if (fieldPaths.length === 1 && fieldPaths[0].path === rootField) {
|
|
3323
|
-
const schemaType = fieldPaths[0].schemaType;
|
|
3324
|
-
properties[rootField] = schemaTypeToJsonSchema(schemaType);
|
|
3325
|
-
if (schemaType.isRequired) {
|
|
3326
|
-
required.push(rootField);
|
|
3327
|
-
}
|
|
3328
|
-
} else {
|
|
3329
|
-
const nestedSchema = buildNestedJsonSchema(fieldPaths, rootField);
|
|
3330
|
-
properties[rootField] = nestedSchema.schema;
|
|
3331
|
-
if (nestedSchema.required) {
|
|
3332
|
-
required.push(rootField);
|
|
3333
|
-
}
|
|
3334
|
-
}
|
|
3335
|
-
}
|
|
3336
|
-
const schema = { type: "object", properties };
|
|
3337
|
-
if (required.length) schema.required = required;
|
|
3338
|
-
const fieldsToOmit = /* @__PURE__ */ new Set(["createdAt", "updatedAt", "__v"]);
|
|
3339
|
-
(options?.create?.omitFields || []).forEach((f) => fieldsToOmit.add(f));
|
|
3340
|
-
const fieldRules = options?.fieldRules || {};
|
|
3341
|
-
Object.entries(fieldRules).forEach(([field, rules]) => {
|
|
3342
|
-
if (rules.systemManaged) {
|
|
3343
|
-
fieldsToOmit.add(field);
|
|
3344
|
-
}
|
|
3345
|
-
});
|
|
3346
|
-
fieldsToOmit.forEach((field) => {
|
|
3347
|
-
if (schema.properties?.[field]) {
|
|
3348
|
-
delete schema.properties[field];
|
|
3349
|
-
}
|
|
3350
|
-
if (schema.required) {
|
|
3351
|
-
schema.required = schema.required.filter((k) => k !== field);
|
|
3352
|
-
}
|
|
3353
|
-
});
|
|
3354
|
-
const reqOv = options?.create?.requiredOverrides || {};
|
|
3355
|
-
const optOv = options?.create?.optionalOverrides || {};
|
|
3356
|
-
schema.required = schema.required || [];
|
|
3357
|
-
for (const [k, v] of Object.entries(reqOv)) {
|
|
3358
|
-
if (v && !schema.required.includes(k)) schema.required.push(k);
|
|
3359
|
-
}
|
|
3360
|
-
for (const [k, v] of Object.entries(optOv)) {
|
|
3361
|
-
if (v && schema.required) schema.required = schema.required.filter((x) => x !== k);
|
|
3362
|
-
}
|
|
3363
|
-
Object.entries(fieldRules).forEach(([field, rules]) => {
|
|
3364
|
-
if (rules.optional && schema.required) {
|
|
3365
|
-
schema.required = schema.required.filter((x) => x !== field);
|
|
3366
|
-
}
|
|
3367
|
-
});
|
|
3368
|
-
const schemaOverrides = options?.create?.schemaOverrides || {};
|
|
3369
|
-
for (const [k, override] of Object.entries(schemaOverrides)) {
|
|
3370
|
-
if (schema.properties?.[k]) {
|
|
3371
|
-
schema.properties[k] = override;
|
|
3372
|
-
}
|
|
3373
|
-
}
|
|
3374
|
-
if (options?.strictAdditionalProperties === true) {
|
|
3375
|
-
schema.additionalProperties = false;
|
|
3376
|
-
}
|
|
3377
|
-
return schema;
|
|
3378
|
-
}
|
|
3379
|
-
function buildNestedJsonSchema(fieldPaths, rootField) {
|
|
3380
|
-
const properties = {};
|
|
3381
|
-
const required = [];
|
|
3382
|
-
let hasRequiredFields = false;
|
|
3383
|
-
for (const { path, schemaType } of fieldPaths) {
|
|
3384
|
-
const relativePath = path.substring(rootField.length + 1);
|
|
3385
|
-
const parts = relativePath.split(".");
|
|
3386
|
-
if (parts.length === 1) {
|
|
3387
|
-
properties[parts[0]] = schemaTypeToJsonSchema(schemaType);
|
|
3388
|
-
if (schemaType.isRequired) {
|
|
3389
|
-
required.push(parts[0]);
|
|
3390
|
-
hasRequiredFields = true;
|
|
3391
|
-
}
|
|
3392
|
-
} else {
|
|
3393
|
-
const fieldName = parts[0];
|
|
3394
|
-
if (!properties[fieldName]) {
|
|
3395
|
-
properties[fieldName] = { type: "object", properties: {} };
|
|
3396
|
-
}
|
|
3397
|
-
const nestedObj = properties[fieldName];
|
|
3398
|
-
if (!nestedObj.properties) nestedObj.properties = {};
|
|
3399
|
-
const deepPath = parts.slice(1).join(".");
|
|
3400
|
-
nestedObj.properties[deepPath] = schemaTypeToJsonSchema(schemaType);
|
|
3401
|
-
}
|
|
3402
|
-
}
|
|
3403
|
-
const schema = { type: "object", properties };
|
|
3404
|
-
if (required.length) schema.required = required;
|
|
3405
|
-
return { schema, required: hasRequiredFields };
|
|
3406
|
-
}
|
|
3407
|
-
function schemaTypeToJsonSchema(schemaType) {
|
|
3408
|
-
const result = {};
|
|
3409
|
-
const instance = schemaType.instance;
|
|
3410
|
-
const options = schemaType.options || {};
|
|
3411
|
-
if (instance === "String") {
|
|
3412
|
-
result.type = "string";
|
|
3413
|
-
if (typeof options.minlength === "number") result.minLength = options.minlength;
|
|
3414
|
-
if (typeof options.maxlength === "number") result.maxLength = options.maxlength;
|
|
3415
|
-
if (options.match instanceof RegExp) result.pattern = options.match.source;
|
|
3416
|
-
if (options.enum && Array.isArray(options.enum)) result.enum = options.enum;
|
|
3417
|
-
} else if (instance === "Number") {
|
|
3418
|
-
result.type = "number";
|
|
3419
|
-
if (typeof options.min === "number") result.minimum = options.min;
|
|
3420
|
-
if (typeof options.max === "number") result.maximum = options.max;
|
|
3421
|
-
} else if (instance === "Boolean") {
|
|
3422
|
-
result.type = "boolean";
|
|
3423
|
-
} else if (instance === "Date") {
|
|
3424
|
-
result.type = "string";
|
|
3425
|
-
result.format = "date-time";
|
|
3426
|
-
} else if (instance === "ObjectId" || instance === "ObjectID") {
|
|
3427
|
-
result.type = "string";
|
|
3428
|
-
result.pattern = "^[0-9a-fA-F]{24}$";
|
|
3429
|
-
} else if (instance === "Array") {
|
|
3430
|
-
result.type = "array";
|
|
3431
|
-
result.items = { type: "string" };
|
|
3432
|
-
} else {
|
|
3433
|
-
result.type = "object";
|
|
3434
|
-
result.additionalProperties = true;
|
|
3435
|
-
}
|
|
3436
|
-
return result;
|
|
3437
|
-
}
|
|
3438
|
-
function buildJsonSchemaForUpdate(createJson, options) {
|
|
3439
|
-
const clone = JSON.parse(JSON.stringify(createJson));
|
|
3440
|
-
delete clone.required;
|
|
3441
|
-
const fieldsToOmit = /* @__PURE__ */ new Set();
|
|
3442
|
-
(options?.update?.omitFields || []).forEach((f) => fieldsToOmit.add(f));
|
|
3443
|
-
const fieldRules = options?.fieldRules || {};
|
|
3444
|
-
Object.entries(fieldRules).forEach(([field, rules]) => {
|
|
3445
|
-
if (rules.immutable || rules.immutableAfterCreate) {
|
|
3446
|
-
fieldsToOmit.add(field);
|
|
3447
|
-
}
|
|
3448
|
-
});
|
|
3449
|
-
fieldsToOmit.forEach((field) => {
|
|
3450
|
-
if (clone.properties?.[field]) {
|
|
3451
|
-
delete clone.properties[field];
|
|
3452
|
-
}
|
|
3453
|
-
});
|
|
3454
|
-
if (options?.strictAdditionalProperties === true) {
|
|
3455
|
-
clone.additionalProperties = false;
|
|
3456
|
-
}
|
|
3457
|
-
if (options?.update?.requireAtLeastOne === true) {
|
|
3458
|
-
clone.minProperties = 1;
|
|
3459
|
-
}
|
|
3460
|
-
return clone;
|
|
3461
|
-
}
|
|
3462
|
-
function buildJsonSchemaForQuery(_tree, options) {
|
|
3463
|
-
const basePagination = {
|
|
3464
|
-
type: "object",
|
|
3465
|
-
properties: {
|
|
3466
|
-
page: { type: "string" },
|
|
3467
|
-
limit: { type: "string" },
|
|
3468
|
-
sort: { type: "string" },
|
|
3469
|
-
populate: { type: "string" },
|
|
3470
|
-
search: { type: "string" },
|
|
3471
|
-
select: { type: "string" },
|
|
3472
|
-
lean: { type: "string" },
|
|
3473
|
-
includeDeleted: { type: "string" }
|
|
3474
|
-
},
|
|
3475
|
-
additionalProperties: true
|
|
3476
|
-
};
|
|
3477
|
-
const filterable = options?.query?.filterableFields || {};
|
|
3478
|
-
for (const [k, v] of Object.entries(filterable)) {
|
|
3479
|
-
if (basePagination.properties) {
|
|
3480
|
-
basePagination.properties[k] = v && typeof v === "object" && "type" in v ? v : { type: "string" };
|
|
3481
|
-
}
|
|
3482
|
-
}
|
|
3483
|
-
return basePagination;
|
|
3484
|
-
}
|
|
3485
|
-
var QueryParser = class {
|
|
3486
|
-
options;
|
|
3487
|
-
operators = {
|
|
3488
|
-
eq: "$eq",
|
|
3489
|
-
ne: "$ne",
|
|
3490
|
-
gt: "$gt",
|
|
3491
|
-
gte: "$gte",
|
|
3492
|
-
lt: "$lt",
|
|
3493
|
-
lte: "$lte",
|
|
3494
|
-
in: "$in",
|
|
3495
|
-
nin: "$nin",
|
|
3496
|
-
like: "$regex",
|
|
3497
|
-
contains: "$regex",
|
|
3498
|
-
regex: "$regex",
|
|
3499
|
-
exists: "$exists",
|
|
3500
|
-
size: "$size",
|
|
3501
|
-
type: "$type"
|
|
1157
|
+
var QueryParser = class {
|
|
1158
|
+
options;
|
|
1159
|
+
operators = {
|
|
1160
|
+
eq: "$eq",
|
|
1161
|
+
ne: "$ne",
|
|
1162
|
+
gt: "$gt",
|
|
1163
|
+
gte: "$gte",
|
|
1164
|
+
lt: "$lt",
|
|
1165
|
+
lte: "$lte",
|
|
1166
|
+
in: "$in",
|
|
1167
|
+
nin: "$nin",
|
|
1168
|
+
like: "$regex",
|
|
1169
|
+
contains: "$regex",
|
|
1170
|
+
regex: "$regex",
|
|
1171
|
+
exists: "$exists",
|
|
1172
|
+
size: "$size",
|
|
1173
|
+
type: "$type"
|
|
3502
1174
|
};
|
|
3503
1175
|
dangerousOperators;
|
|
3504
1176
|
/**
|
|
@@ -3549,7 +1221,7 @@ var QueryParser = class {
|
|
|
3549
1221
|
after,
|
|
3550
1222
|
cursor,
|
|
3551
1223
|
select,
|
|
3552
|
-
lookup
|
|
1224
|
+
lookup,
|
|
3553
1225
|
aggregate: aggregate2,
|
|
3554
1226
|
...filters
|
|
3555
1227
|
} = query || {};
|
|
@@ -3571,8 +1243,8 @@ var QueryParser = class {
|
|
|
3571
1243
|
if (select) {
|
|
3572
1244
|
parsed.select = this._parseSelect(select);
|
|
3573
1245
|
}
|
|
3574
|
-
if (this.options.enableLookups &&
|
|
3575
|
-
parsed.lookups = this._parseLookups(
|
|
1246
|
+
if (this.options.enableLookups && lookup) {
|
|
1247
|
+
parsed.lookups = this._parseLookups(lookup);
|
|
3576
1248
|
}
|
|
3577
1249
|
if (this.options.enableAggregations && aggregate2) {
|
|
3578
1250
|
parsed.aggregation = this._parseAggregation(aggregate2);
|
|
@@ -3616,10 +1288,10 @@ var QueryParser = class {
|
|
|
3616
1288
|
* // Returns: [{ from: 'departments', localField: 'deptSlug', foreignField: 'slug', single: true }]
|
|
3617
1289
|
* ```
|
|
3618
1290
|
*/
|
|
3619
|
-
_parseLookups(
|
|
3620
|
-
if (!
|
|
1291
|
+
_parseLookups(lookup) {
|
|
1292
|
+
if (!lookup || typeof lookup !== "object") return [];
|
|
3621
1293
|
const lookups = [];
|
|
3622
|
-
const lookupObj =
|
|
1294
|
+
const lookupObj = lookup;
|
|
3623
1295
|
for (const [collectionName, config] of Object.entries(lookupObj)) {
|
|
3624
1296
|
try {
|
|
3625
1297
|
const lookupConfig = this._parseSingleLookup(collectionName, config);
|
|
@@ -3994,16 +1666,6 @@ var QueryParser = class {
|
|
|
3994
1666
|
}
|
|
3995
1667
|
};
|
|
3996
1668
|
|
|
3997
|
-
// src/actions/index.ts
|
|
3998
|
-
var actions_exports = {};
|
|
3999
|
-
__export(actions_exports, {
|
|
4000
|
-
aggregate: () => aggregate_exports,
|
|
4001
|
-
create: () => create_exports,
|
|
4002
|
-
deleteActions: () => delete_exports,
|
|
4003
|
-
read: () => read_exports,
|
|
4004
|
-
update: () => update_exports
|
|
4005
|
-
});
|
|
4006
|
-
|
|
4007
1669
|
// src/index.ts
|
|
4008
1670
|
function createRepository(Model, plugins = [], paginationConfig = {}, options = {}) {
|
|
4009
1671
|
return new Repository(Model, plugins, paginationConfig, options);
|
|
@@ -4016,7 +1678,7 @@ var index_default = Repository;
|
|
|
4016
1678
|
* smart pagination, events, and plugins.
|
|
4017
1679
|
*
|
|
4018
1680
|
* @module @classytic/mongokit
|
|
4019
|
-
* @author
|
|
1681
|
+
* @author Classytic (https://github.com/classytic)
|
|
4020
1682
|
* @license MIT
|
|
4021
1683
|
*
|
|
4022
1684
|
* @example
|
|
@@ -4048,4 +1710,4 @@ var index_default = Repository;
|
|
|
4048
1710
|
* ```
|
|
4049
1711
|
*/
|
|
4050
1712
|
|
|
4051
|
-
export { AggregationBuilder, LookupBuilder,
|
|
1713
|
+
export { AggregationBuilder, LookupBuilder, QueryParser, Repository, createRepository, index_default as default };
|