@classytic/mongokit 3.3.2 → 3.4.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 +137 -7
- package/dist/PaginationEngine-nY04eGUM.mjs +290 -0
- package/dist/actions/index.d.mts +2 -9
- package/dist/actions/index.mjs +3 -5
- package/dist/ai/index.d.mts +1 -1
- package/dist/ai/index.mjs +3 -3
- package/dist/chunk-CfYAbeIz.mjs +13 -0
- package/dist/{limits-s1-d8rWb.mjs → cursor-CHToazHy.mjs} +122 -171
- package/dist/{logger-D8ily-PP.mjs → error-Bpbi_NKo.mjs} +34 -22
- package/dist/{cache-keys-CzFwVnLy.mjs → field-selection-reyDRzXf.mjs} +110 -112
- package/dist/{aggregate-BkOG9qwr.d.mts → index-BuoZIZ15.d.mts} +132 -129
- package/dist/index.d.mts +549 -543
- package/dist/index.mjs +33 -101
- package/dist/{mongooseToJsonSchema-D_i2Am_O.mjs → mongooseToJsonSchema-B6Qyl8BK.mjs} +13 -12
- package/dist/{mongooseToJsonSchema-B6O2ED3n.d.mts → mongooseToJsonSchema-RX9YfJLu.d.mts} +24 -17
- package/dist/pagination/PaginationEngine.d.mts +1 -1
- package/dist/pagination/PaginationEngine.mjs +2 -209
- package/dist/plugins/index.d.mts +1 -2
- package/dist/plugins/index.mjs +2 -3
- package/dist/sort-C-BJEWUZ.mjs +57 -0
- package/dist/{types-pVY0w1Pp.d.mts → types-COINbsdL.d.mts} +57 -27
- package/dist/{aggregate-BClp040M.mjs → update-DGKMmBgG.mjs} +575 -565
- package/dist/utils/index.d.mts +2 -2
- package/dist/utils/index.mjs +4 -5
- package/dist/{custom-id.plugin-BJ3FSnzt.d.mts → validation-chain.plugin-BNoaKDOm.d.mts} +832 -832
- package/dist/{custom-id.plugin-FInXDsUX.mjs → validation-chain.plugin-da3fOo8A.mjs} +2410 -2246
- package/package.json +11 -6
- package/dist/chunk-DQk6qfdC.mjs +0 -18
|
@@ -1,412 +1,5 @@
|
|
|
1
|
-
import { t as __exportAll } from "./chunk-
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
//#region src/actions/create.ts
|
|
5
|
-
var create_exports = /* @__PURE__ */ __exportAll({
|
|
6
|
-
create: () => create,
|
|
7
|
-
createDefault: () => createDefault,
|
|
8
|
-
createMany: () => createMany,
|
|
9
|
-
upsert: () => upsert
|
|
10
|
-
});
|
|
11
|
-
/**
|
|
12
|
-
* Create single document
|
|
13
|
-
*/
|
|
14
|
-
async function create(Model, data, options = {}) {
|
|
15
|
-
const document = new Model(data);
|
|
16
|
-
await document.save({ session: options.session });
|
|
17
|
-
return document;
|
|
18
|
-
}
|
|
19
|
-
/**
|
|
20
|
-
* Create multiple documents
|
|
21
|
-
*/
|
|
22
|
-
async function createMany(Model, dataArray, options = {}) {
|
|
23
|
-
return Model.insertMany(dataArray, {
|
|
24
|
-
session: options.session,
|
|
25
|
-
ordered: options.ordered !== false
|
|
26
|
-
});
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* Create with defaults (useful for initialization)
|
|
30
|
-
*/
|
|
31
|
-
async function createDefault(Model, overrides = {}, options = {}) {
|
|
32
|
-
const defaults = {};
|
|
33
|
-
Model.schema.eachPath((path, schemaType) => {
|
|
34
|
-
const schemaOptions = schemaType.options;
|
|
35
|
-
if (schemaOptions.default !== void 0 && path !== "_id") defaults[path] = typeof schemaOptions.default === "function" ? schemaOptions.default() : schemaOptions.default;
|
|
36
|
-
});
|
|
37
|
-
return create(Model, {
|
|
38
|
-
...defaults,
|
|
39
|
-
...overrides
|
|
40
|
-
}, options);
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* Upsert (create or update)
|
|
44
|
-
*/
|
|
45
|
-
async function upsert(Model, query, data, options = {}) {
|
|
46
|
-
return Model.findOneAndUpdate(query, { $setOnInsert: data }, {
|
|
47
|
-
upsert: true,
|
|
48
|
-
returnDocument: "after",
|
|
49
|
-
runValidators: true,
|
|
50
|
-
session: options.session,
|
|
51
|
-
...options.updatePipeline !== void 0 ? { updatePipeline: options.updatePipeline } : {}
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
//#endregion
|
|
56
|
-
//#region src/actions/read.ts
|
|
57
|
-
var read_exports = /* @__PURE__ */ __exportAll({
|
|
58
|
-
count: () => count,
|
|
59
|
-
exists: () => exists,
|
|
60
|
-
getAll: () => getAll,
|
|
61
|
-
getById: () => getById,
|
|
62
|
-
getByQuery: () => getByQuery,
|
|
63
|
-
getOrCreate: () => getOrCreate,
|
|
64
|
-
tryGetByQuery: () => tryGetByQuery
|
|
65
|
-
});
|
|
66
|
-
/**
|
|
67
|
-
* Parse populate specification into consistent format
|
|
68
|
-
*/
|
|
69
|
-
function parsePopulate$1(populate) {
|
|
70
|
-
if (!populate) return [];
|
|
71
|
-
if (typeof populate === "string") return populate.split(",").map((p) => p.trim());
|
|
72
|
-
if (Array.isArray(populate)) return populate.map((p) => typeof p === "string" ? p.trim() : p);
|
|
73
|
-
return [populate];
|
|
74
|
-
}
|
|
75
|
-
/**
|
|
76
|
-
* Get document by ID
|
|
77
|
-
*
|
|
78
|
-
* @param Model - Mongoose model
|
|
79
|
-
* @param id - Document ID
|
|
80
|
-
* @param options - Query options
|
|
81
|
-
* @returns Document or null
|
|
82
|
-
* @throws Error if document not found and throwOnNotFound is true
|
|
83
|
-
*/
|
|
84
|
-
async function getById(Model, id, options = {}) {
|
|
85
|
-
const query = options.query ? Model.findOne({
|
|
86
|
-
_id: id,
|
|
87
|
-
...options.query
|
|
88
|
-
}) : Model.findById(id);
|
|
89
|
-
if (options.select) query.select(options.select);
|
|
90
|
-
if (options.populate) query.populate(parsePopulate$1(options.populate));
|
|
91
|
-
if (options.lean) query.lean();
|
|
92
|
-
if (options.session) query.session(options.session);
|
|
93
|
-
if (options.readPreference) query.read(options.readPreference);
|
|
94
|
-
const document = await query.exec();
|
|
95
|
-
if (!document && options.throwOnNotFound !== false) throw createError(404, "Document not found");
|
|
96
|
-
return document;
|
|
97
|
-
}
|
|
98
|
-
/**
|
|
99
|
-
* Get document by query
|
|
100
|
-
*
|
|
101
|
-
* @param Model - Mongoose model
|
|
102
|
-
* @param query - MongoDB query
|
|
103
|
-
* @param options - Query options
|
|
104
|
-
* @returns Document or null
|
|
105
|
-
* @throws Error if document not found and throwOnNotFound is true
|
|
106
|
-
*/
|
|
107
|
-
async function getByQuery(Model, query, options = {}) {
|
|
108
|
-
const mongoQuery = Model.findOne(query);
|
|
109
|
-
if (options.select) mongoQuery.select(options.select);
|
|
110
|
-
if (options.populate) mongoQuery.populate(parsePopulate$1(options.populate));
|
|
111
|
-
if (options.lean) mongoQuery.lean();
|
|
112
|
-
if (options.session) mongoQuery.session(options.session);
|
|
113
|
-
if (options.readPreference) mongoQuery.read(options.readPreference);
|
|
114
|
-
const document = await mongoQuery.exec();
|
|
115
|
-
if (!document && options.throwOnNotFound !== false) throw createError(404, "Document not found");
|
|
116
|
-
return document;
|
|
117
|
-
}
|
|
118
|
-
/**
|
|
119
|
-
* Get document by query without throwing (returns null if not found)
|
|
120
|
-
*/
|
|
121
|
-
async function tryGetByQuery(Model, query, options = {}) {
|
|
122
|
-
return getByQuery(Model, query, {
|
|
123
|
-
...options,
|
|
124
|
-
throwOnNotFound: false
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
/**
|
|
128
|
-
* Get all documents (basic query without pagination)
|
|
129
|
-
* For pagination, use Repository.paginate() or Repository.stream()
|
|
130
|
-
*/
|
|
131
|
-
async function getAll(Model, query = {}, options = {}) {
|
|
132
|
-
let mongoQuery = Model.find(query);
|
|
133
|
-
if (options.select) mongoQuery = mongoQuery.select(options.select);
|
|
134
|
-
if (options.populate) mongoQuery = mongoQuery.populate(parsePopulate$1(options.populate));
|
|
135
|
-
if (options.sort) mongoQuery = mongoQuery.sort(options.sort);
|
|
136
|
-
if (options.limit) mongoQuery = mongoQuery.limit(options.limit);
|
|
137
|
-
if (options.skip) mongoQuery = mongoQuery.skip(options.skip);
|
|
138
|
-
mongoQuery = mongoQuery.lean(options.lean !== false);
|
|
139
|
-
if (options.session) mongoQuery = mongoQuery.session(options.session);
|
|
140
|
-
if (options.readPreference) mongoQuery = mongoQuery.read(options.readPreference);
|
|
141
|
-
return mongoQuery.exec();
|
|
142
|
-
}
|
|
143
|
-
/**
|
|
144
|
-
* Get or create document (upsert)
|
|
145
|
-
*/
|
|
146
|
-
async function getOrCreate(Model, query, createData, options = {}) {
|
|
147
|
-
return Model.findOneAndUpdate(query, { $setOnInsert: createData }, {
|
|
148
|
-
upsert: true,
|
|
149
|
-
returnDocument: "after",
|
|
150
|
-
runValidators: true,
|
|
151
|
-
session: options.session,
|
|
152
|
-
...options.updatePipeline !== void 0 ? { updatePipeline: options.updatePipeline } : {}
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
/**
|
|
156
|
-
* Count documents matching query
|
|
157
|
-
*/
|
|
158
|
-
async function count(Model, query = {}, options = {}) {
|
|
159
|
-
const q = Model.countDocuments(query).session(options.session ?? null);
|
|
160
|
-
if (options.readPreference) q.read(options.readPreference);
|
|
161
|
-
return q;
|
|
162
|
-
}
|
|
163
|
-
/**
|
|
164
|
-
* Check if document exists
|
|
165
|
-
*/
|
|
166
|
-
async function exists(Model, query, options = {}) {
|
|
167
|
-
const q = Model.exists(query).session(options.session ?? null);
|
|
168
|
-
if (options.readPreference) q.read(options.readPreference);
|
|
169
|
-
return q;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
//#endregion
|
|
173
|
-
//#region src/actions/update.ts
|
|
174
|
-
var update_exports = /* @__PURE__ */ __exportAll({
|
|
175
|
-
increment: () => increment,
|
|
176
|
-
pullFromArray: () => pullFromArray,
|
|
177
|
-
pushToArray: () => pushToArray,
|
|
178
|
-
update: () => update,
|
|
179
|
-
updateByQuery: () => updateByQuery,
|
|
180
|
-
updateMany: () => updateMany,
|
|
181
|
-
updateWithConstraints: () => updateWithConstraints,
|
|
182
|
-
updateWithValidation: () => updateWithValidation
|
|
183
|
-
});
|
|
184
|
-
function assertUpdatePipelineAllowed(update, updatePipeline) {
|
|
185
|
-
if (Array.isArray(update) && updatePipeline !== true) throw createError(400, "Update pipelines (array updates) are disabled by default; pass `{ updatePipeline: true }` to explicitly allow pipeline-style updates.");
|
|
186
|
-
}
|
|
187
|
-
/**
|
|
188
|
-
* Parse populate specification into consistent format
|
|
189
|
-
*/
|
|
190
|
-
function parsePopulate(populate) {
|
|
191
|
-
if (!populate) return [];
|
|
192
|
-
if (typeof populate === "string") return populate.split(",").map((p) => p.trim());
|
|
193
|
-
if (Array.isArray(populate)) return populate.map((p) => typeof p === "string" ? p.trim() : p);
|
|
194
|
-
return [populate];
|
|
195
|
-
}
|
|
196
|
-
/**
|
|
197
|
-
* Update by ID
|
|
198
|
-
*/
|
|
199
|
-
async function update(Model, id, data, options = {}) {
|
|
200
|
-
assertUpdatePipelineAllowed(data, options.updatePipeline);
|
|
201
|
-
const query = {
|
|
202
|
-
_id: id,
|
|
203
|
-
...options.query
|
|
204
|
-
};
|
|
205
|
-
const document = await Model.findOneAndUpdate(query, data, {
|
|
206
|
-
returnDocument: "after",
|
|
207
|
-
runValidators: true,
|
|
208
|
-
session: options.session,
|
|
209
|
-
...options.updatePipeline !== void 0 ? { updatePipeline: options.updatePipeline } : {},
|
|
210
|
-
...options.arrayFilters ? { arrayFilters: options.arrayFilters } : {}
|
|
211
|
-
}).select(options.select || "").populate(parsePopulate(options.populate)).lean(options.lean ?? false);
|
|
212
|
-
if (!document) throw createError(404, "Document not found");
|
|
213
|
-
return document;
|
|
214
|
-
}
|
|
215
|
-
/**
|
|
216
|
-
* Update with query constraints (optimized)
|
|
217
|
-
* Returns null if constraints not met (not an error)
|
|
218
|
-
*/
|
|
219
|
-
async function updateWithConstraints(Model, id, data, constraints = {}, options = {}) {
|
|
220
|
-
assertUpdatePipelineAllowed(data, options.updatePipeline);
|
|
221
|
-
const query = {
|
|
222
|
-
_id: id,
|
|
223
|
-
...constraints
|
|
224
|
-
};
|
|
225
|
-
return await Model.findOneAndUpdate(query, data, {
|
|
226
|
-
returnDocument: "after",
|
|
227
|
-
runValidators: true,
|
|
228
|
-
session: options.session,
|
|
229
|
-
...options.updatePipeline !== void 0 ? { updatePipeline: options.updatePipeline } : {},
|
|
230
|
-
...options.arrayFilters ? { arrayFilters: options.arrayFilters } : {}
|
|
231
|
-
}).select(options.select || "").populate(parsePopulate(options.populate)).lean(options.lean ?? false);
|
|
232
|
-
}
|
|
233
|
-
/**
|
|
234
|
-
* Update with validation (smart optimization)
|
|
235
|
-
* 1-query on success, 2-queries for detailed errors
|
|
236
|
-
*/
|
|
237
|
-
async function updateWithValidation(Model, id, data, validationOptions = {}, options = {}) {
|
|
238
|
-
const { buildConstraints, validateUpdate } = validationOptions;
|
|
239
|
-
assertUpdatePipelineAllowed(data, options.updatePipeline);
|
|
240
|
-
if (buildConstraints) {
|
|
241
|
-
const document = await updateWithConstraints(Model, id, data, buildConstraints(data), options);
|
|
242
|
-
if (document) return {
|
|
243
|
-
success: true,
|
|
244
|
-
data: document
|
|
245
|
-
};
|
|
246
|
-
}
|
|
247
|
-
const findQuery = {
|
|
248
|
-
_id: id,
|
|
249
|
-
...options.query
|
|
250
|
-
};
|
|
251
|
-
const existing = await Model.findOne(findQuery).select(options.select || "").session(options.session ?? null).lean();
|
|
252
|
-
if (!existing) return {
|
|
253
|
-
success: false,
|
|
254
|
-
error: {
|
|
255
|
-
code: 404,
|
|
256
|
-
message: "Document not found"
|
|
257
|
-
}
|
|
258
|
-
};
|
|
259
|
-
if (validateUpdate) {
|
|
260
|
-
const validation = validateUpdate(existing, data);
|
|
261
|
-
if (!validation.valid) return {
|
|
262
|
-
success: false,
|
|
263
|
-
error: {
|
|
264
|
-
code: 403,
|
|
265
|
-
message: validation.message || "Update not allowed",
|
|
266
|
-
violations: validation.violations
|
|
267
|
-
}
|
|
268
|
-
};
|
|
269
|
-
}
|
|
270
|
-
return {
|
|
271
|
-
success: true,
|
|
272
|
-
data: await update(Model, id, data, options)
|
|
273
|
-
};
|
|
274
|
-
}
|
|
275
|
-
/**
|
|
276
|
-
* Update many documents
|
|
277
|
-
*/
|
|
278
|
-
async function updateMany(Model, query, data, options = {}) {
|
|
279
|
-
assertUpdatePipelineAllowed(data, options.updatePipeline);
|
|
280
|
-
const result = await Model.updateMany(query, data, {
|
|
281
|
-
runValidators: true,
|
|
282
|
-
session: options.session,
|
|
283
|
-
...options.updatePipeline !== void 0 ? { updatePipeline: options.updatePipeline } : {},
|
|
284
|
-
...options.arrayFilters ? { arrayFilters: options.arrayFilters } : {}
|
|
285
|
-
});
|
|
286
|
-
return {
|
|
287
|
-
matchedCount: result.matchedCount,
|
|
288
|
-
modifiedCount: result.modifiedCount
|
|
289
|
-
};
|
|
290
|
-
}
|
|
291
|
-
/**
|
|
292
|
-
* Update by query
|
|
293
|
-
*/
|
|
294
|
-
async function updateByQuery(Model, query, data, options = {}) {
|
|
295
|
-
assertUpdatePipelineAllowed(data, options.updatePipeline);
|
|
296
|
-
const document = await Model.findOneAndUpdate(query, data, {
|
|
297
|
-
returnDocument: "after",
|
|
298
|
-
runValidators: true,
|
|
299
|
-
session: options.session,
|
|
300
|
-
...options.updatePipeline !== void 0 ? { updatePipeline: options.updatePipeline } : {},
|
|
301
|
-
...options.arrayFilters ? { arrayFilters: options.arrayFilters } : {}
|
|
302
|
-
}).select(options.select || "").populate(parsePopulate(options.populate)).lean(options.lean ?? false);
|
|
303
|
-
if (!document && options.throwOnNotFound !== false) throw createError(404, "Document not found");
|
|
304
|
-
return document;
|
|
305
|
-
}
|
|
306
|
-
/**
|
|
307
|
-
* Increment field
|
|
308
|
-
*/
|
|
309
|
-
async function increment(Model, id, field, value = 1, options = {}) {
|
|
310
|
-
return update(Model, id, { $inc: { [field]: value } }, options);
|
|
311
|
-
}
|
|
312
|
-
/**
|
|
313
|
-
* Push to array
|
|
314
|
-
*/
|
|
315
|
-
async function pushToArray(Model, id, field, value, options = {}) {
|
|
316
|
-
return update(Model, id, { $push: { [field]: value } }, options);
|
|
317
|
-
}
|
|
318
|
-
/**
|
|
319
|
-
* Pull from array
|
|
320
|
-
*/
|
|
321
|
-
async function pullFromArray(Model, id, field, value, options = {}) {
|
|
322
|
-
return update(Model, id, { $pull: { [field]: value } }, options);
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
//#endregion
|
|
326
|
-
//#region src/actions/delete.ts
|
|
327
|
-
var delete_exports = /* @__PURE__ */ __exportAll({
|
|
328
|
-
deleteById: () => deleteById,
|
|
329
|
-
deleteByQuery: () => deleteByQuery,
|
|
330
|
-
deleteMany: () => deleteMany,
|
|
331
|
-
restore: () => restore,
|
|
332
|
-
softDelete: () => softDelete
|
|
333
|
-
});
|
|
334
|
-
/**
|
|
335
|
-
* Delete by ID
|
|
336
|
-
*/
|
|
337
|
-
async function deleteById(Model, id, options = {}) {
|
|
338
|
-
const query = {
|
|
339
|
-
_id: id,
|
|
340
|
-
...options.query
|
|
341
|
-
};
|
|
342
|
-
if (!await Model.findOneAndDelete(query).session(options.session ?? null)) throw createError(404, "Document not found");
|
|
343
|
-
return {
|
|
344
|
-
success: true,
|
|
345
|
-
message: "Deleted successfully",
|
|
346
|
-
id: String(id)
|
|
347
|
-
};
|
|
348
|
-
}
|
|
349
|
-
/**
|
|
350
|
-
* Delete many documents
|
|
351
|
-
*/
|
|
352
|
-
async function deleteMany(Model, query, options = {}) {
|
|
353
|
-
return {
|
|
354
|
-
success: true,
|
|
355
|
-
count: (await Model.deleteMany(query).session(options.session ?? null)).deletedCount,
|
|
356
|
-
message: "Deleted successfully"
|
|
357
|
-
};
|
|
358
|
-
}
|
|
359
|
-
/**
|
|
360
|
-
* Delete by query
|
|
361
|
-
*/
|
|
362
|
-
async function deleteByQuery(Model, query, options = {}) {
|
|
363
|
-
const document = await Model.findOneAndDelete(query).session(options.session ?? null);
|
|
364
|
-
if (!document && options.throwOnNotFound !== false) throw createError(404, "Document not found");
|
|
365
|
-
return {
|
|
366
|
-
success: true,
|
|
367
|
-
message: "Deleted successfully",
|
|
368
|
-
...document ? { id: String(document._id) } : {}
|
|
369
|
-
};
|
|
370
|
-
}
|
|
371
|
-
/**
|
|
372
|
-
* Soft delete (set deleted flag)
|
|
373
|
-
*/
|
|
374
|
-
async function softDelete(Model, id, options = {}) {
|
|
375
|
-
if (!await Model.findByIdAndUpdate(id, {
|
|
376
|
-
deleted: true,
|
|
377
|
-
deletedAt: /* @__PURE__ */ new Date(),
|
|
378
|
-
deletedBy: options.userId
|
|
379
|
-
}, {
|
|
380
|
-
returnDocument: "after",
|
|
381
|
-
session: options.session
|
|
382
|
-
})) throw createError(404, "Document not found");
|
|
383
|
-
return {
|
|
384
|
-
success: true,
|
|
385
|
-
message: "Soft deleted successfully",
|
|
386
|
-
id: String(id),
|
|
387
|
-
soft: true
|
|
388
|
-
};
|
|
389
|
-
}
|
|
390
|
-
/**
|
|
391
|
-
* Restore soft deleted document
|
|
392
|
-
*/
|
|
393
|
-
async function restore(Model, id, options = {}) {
|
|
394
|
-
if (!await Model.findByIdAndUpdate(id, {
|
|
395
|
-
deleted: false,
|
|
396
|
-
deletedAt: null,
|
|
397
|
-
deletedBy: null
|
|
398
|
-
}, {
|
|
399
|
-
returnDocument: "after",
|
|
400
|
-
session: options.session
|
|
401
|
-
})) throw createError(404, "Document not found");
|
|
402
|
-
return {
|
|
403
|
-
success: true,
|
|
404
|
-
message: "Restored successfully",
|
|
405
|
-
id: String(id)
|
|
406
|
-
};
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
//#endregion
|
|
1
|
+
import { t as __exportAll } from "./chunk-CfYAbeIz.mjs";
|
|
2
|
+
import { a as warn, t as createError } from "./error-Bpbi_NKo.mjs";
|
|
410
3
|
//#region src/query/LookupBuilder.ts
|
|
411
4
|
/** Stages that are never valid inside a $lookup pipeline */
|
|
412
5
|
const BLOCKED_PIPELINE_STAGES = [
|
|
@@ -417,12 +10,14 @@ const BLOCKED_PIPELINE_STAGES = [
|
|
|
417
10
|
"$currentOp",
|
|
418
11
|
"$listSessions"
|
|
419
12
|
];
|
|
420
|
-
/** Operators that can enable arbitrary code execution
|
|
13
|
+
/** Operators that can enable arbitrary code execution.
|
|
14
|
+
* Note: $expr is intentionally NOT blocked — it's needed for pipeline-form
|
|
15
|
+
* $lookup correlations (let + $match.$expr) and is a comparison operator,
|
|
16
|
+
* not a code execution vector like $where/$function/$accumulator. */
|
|
421
17
|
const DANGEROUS_OPERATORS = [
|
|
422
18
|
"$where",
|
|
423
19
|
"$function",
|
|
424
|
-
"$accumulator"
|
|
425
|
-
"$expr"
|
|
20
|
+
"$accumulator"
|
|
426
21
|
];
|
|
427
22
|
/**
|
|
428
23
|
* Fluent builder for MongoDB $lookup aggregation stage
|
|
@@ -498,6 +93,14 @@ var LookupBuilder = class LookupBuilder {
|
|
|
498
93
|
return this;
|
|
499
94
|
}
|
|
500
95
|
/**
|
|
96
|
+
* Control pipeline sanitization (default: true)
|
|
97
|
+
* Set to false for auto-generated pipelines that are known safe
|
|
98
|
+
*/
|
|
99
|
+
sanitize(enabled) {
|
|
100
|
+
this.options.sanitize = enabled;
|
|
101
|
+
return this;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
501
104
|
* Build the $lookup aggregation stage(s)
|
|
502
105
|
* Returns an array of pipeline stages including $lookup and optional $unwind
|
|
503
106
|
*
|
|
@@ -529,10 +132,16 @@ var LookupBuilder = class LookupBuilder {
|
|
|
529
132
|
} };
|
|
530
133
|
} else {
|
|
531
134
|
const safePipeline = this.options.sanitize !== false ? LookupBuilder.sanitizePipeline(pipeline) : pipeline;
|
|
135
|
+
let effectiveLet = letVars;
|
|
136
|
+
let effectivePipeline = safePipeline;
|
|
137
|
+
if (localField && foreignField && !letVars) {
|
|
138
|
+
effectiveLet = { lookupJoinVal: `$${localField}` };
|
|
139
|
+
effectivePipeline = [{ $match: { $expr: { $eq: [`$${foreignField}`, "$$lookupJoinVal"] } } }, ...safePipeline];
|
|
140
|
+
}
|
|
532
141
|
lookupStage = { $lookup: {
|
|
533
142
|
from,
|
|
534
|
-
...
|
|
535
|
-
pipeline:
|
|
143
|
+
...effectiveLet && { let: effectiveLet },
|
|
144
|
+
pipeline: effectivePipeline,
|
|
536
145
|
as: outputField
|
|
537
146
|
} };
|
|
538
147
|
}
|
|
@@ -581,8 +190,29 @@ var LookupBuilder = class LookupBuilder {
|
|
|
581
190
|
const builder = new LookupBuilder(lookup.from).localField(lookup.localField).foreignField(lookup.foreignField);
|
|
582
191
|
if (lookup.as) builder.as(lookup.as);
|
|
583
192
|
if (lookup.single) builder.single(lookup.single);
|
|
584
|
-
if (lookup.
|
|
585
|
-
|
|
193
|
+
if (lookup.select) {
|
|
194
|
+
let projection;
|
|
195
|
+
if (typeof lookup.select === "string") {
|
|
196
|
+
projection = {};
|
|
197
|
+
for (const field of lookup.select.split(",").map((f) => f.trim())) if (field.startsWith("-")) projection[field.substring(1)] = 0;
|
|
198
|
+
else projection[field] = 1;
|
|
199
|
+
} else projection = lookup.select;
|
|
200
|
+
const joinStage = { $match: { $expr: { $eq: [`$${lookup.foreignField}`, "$$lookupJoinVal"] } } };
|
|
201
|
+
const existing = lookup.pipeline || [];
|
|
202
|
+
builder.pipeline([
|
|
203
|
+
joinStage,
|
|
204
|
+
...existing,
|
|
205
|
+
{ $project: projection }
|
|
206
|
+
]);
|
|
207
|
+
builder.let({
|
|
208
|
+
lookupJoinVal: `$${lookup.localField}`,
|
|
209
|
+
...lookup.let || {}
|
|
210
|
+
});
|
|
211
|
+
builder.sanitize(false);
|
|
212
|
+
} else if (lookup.pipeline) {
|
|
213
|
+
builder.pipeline(lookup.pipeline);
|
|
214
|
+
if (lookup.let) builder.let(lookup.let);
|
|
215
|
+
} else if (lookup.let) builder.let(lookup.let);
|
|
586
216
|
return builder.build();
|
|
587
217
|
});
|
|
588
218
|
}
|
|
@@ -600,7 +230,7 @@ var LookupBuilder = class LookupBuilder {
|
|
|
600
230
|
* ```
|
|
601
231
|
*/
|
|
602
232
|
static nested(lookups) {
|
|
603
|
-
return lookups.flatMap((lookup,
|
|
233
|
+
return lookups.flatMap((lookup, _index) => {
|
|
604
234
|
const builder = new LookupBuilder(lookup.from).localField(lookup.localField).foreignField(lookup.foreignField);
|
|
605
235
|
if (lookup.as) builder.as(lookup.as);
|
|
606
236
|
if (lookup.single !== void 0) builder.single(lookup.single);
|
|
@@ -649,187 +279,567 @@ var LookupBuilder = class LookupBuilder {
|
|
|
649
279
|
return sanitized;
|
|
650
280
|
}
|
|
651
281
|
};
|
|
652
|
-
|
|
653
282
|
//#endregion
|
|
654
|
-
//#region src/actions/aggregate.ts
|
|
655
|
-
var aggregate_exports = /* @__PURE__ */ __exportAll({
|
|
656
|
-
aggregate: () => aggregate,
|
|
657
|
-
aggregatePaginate: () => aggregatePaginate,
|
|
658
|
-
average: () => average,
|
|
659
|
-
countBy: () => countBy,
|
|
660
|
-
distinct: () => distinct,
|
|
661
|
-
facet: () => facet,
|
|
662
|
-
groupBy: () => groupBy,
|
|
663
|
-
lookup: () => lookup,
|
|
664
|
-
minMax: () => minMax,
|
|
665
|
-
sum: () => sum,
|
|
666
|
-
unwind: () => unwind
|
|
283
|
+
//#region src/actions/aggregate.ts
|
|
284
|
+
var aggregate_exports = /* @__PURE__ */ __exportAll({
|
|
285
|
+
aggregate: () => aggregate,
|
|
286
|
+
aggregatePaginate: () => aggregatePaginate,
|
|
287
|
+
average: () => average,
|
|
288
|
+
countBy: () => countBy,
|
|
289
|
+
distinct: () => distinct,
|
|
290
|
+
facet: () => facet,
|
|
291
|
+
groupBy: () => groupBy,
|
|
292
|
+
lookup: () => lookup,
|
|
293
|
+
minMax: () => minMax,
|
|
294
|
+
sum: () => sum,
|
|
295
|
+
unwind: () => unwind
|
|
296
|
+
});
|
|
297
|
+
/**
|
|
298
|
+
* Execute aggregation pipeline
|
|
299
|
+
*/
|
|
300
|
+
async function aggregate(Model, pipeline, options = {}) {
|
|
301
|
+
const aggregation = Model.aggregate(pipeline);
|
|
302
|
+
if (options.session) aggregation.session(options.session);
|
|
303
|
+
return aggregation.exec();
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Aggregate with pagination using native MongoDB $facet
|
|
307
|
+
* WARNING: $facet results must be <16MB. For larger results (limit >1000),
|
|
308
|
+
* consider using Repository.aggregatePaginate() or splitting into separate queries.
|
|
309
|
+
*/
|
|
310
|
+
async function aggregatePaginate(Model, pipeline, options = {}) {
|
|
311
|
+
const page = parseInt(String(options.page || 1), 10);
|
|
312
|
+
const limit = parseInt(String(options.limit || 10), 10);
|
|
313
|
+
const skip = (page - 1) * limit;
|
|
314
|
+
if (limit > 1e3) warn(`[mongokit] Large aggregation limit (${limit}). $facet results must be <16MB. Consider using Repository.aggregatePaginate() for safer handling of large datasets.`);
|
|
315
|
+
const facetPipeline = [...pipeline, { $facet: {
|
|
316
|
+
docs: [{ $skip: skip }, { $limit: limit }],
|
|
317
|
+
total: [{ $count: "count" }]
|
|
318
|
+
} }];
|
|
319
|
+
const aggregation = Model.aggregate(facetPipeline);
|
|
320
|
+
if (options.session) aggregation.session(options.session);
|
|
321
|
+
const [result] = await aggregation.exec();
|
|
322
|
+
const docs = result.docs || [];
|
|
323
|
+
const total = result.total[0]?.count || 0;
|
|
324
|
+
const pages = Math.ceil(total / limit);
|
|
325
|
+
return {
|
|
326
|
+
docs,
|
|
327
|
+
total,
|
|
328
|
+
page,
|
|
329
|
+
limit,
|
|
330
|
+
pages,
|
|
331
|
+
hasNext: page < pages,
|
|
332
|
+
hasPrev: page > 1
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Group documents by field value
|
|
337
|
+
*/
|
|
338
|
+
async function groupBy(Model, field, options = {}) {
|
|
339
|
+
const pipeline = [{ $group: {
|
|
340
|
+
_id: `$${field}`,
|
|
341
|
+
count: { $sum: 1 }
|
|
342
|
+
} }, { $sort: { count: -1 } }];
|
|
343
|
+
if (options.limit) pipeline.push({ $limit: options.limit });
|
|
344
|
+
return aggregate(Model, pipeline, options);
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Count by field values
|
|
348
|
+
*/
|
|
349
|
+
async function countBy(Model, field, query = {}, options = {}) {
|
|
350
|
+
const pipeline = [];
|
|
351
|
+
if (Object.keys(query).length > 0) pipeline.push({ $match: query });
|
|
352
|
+
pipeline.push({ $group: {
|
|
353
|
+
_id: `$${field}`,
|
|
354
|
+
count: { $sum: 1 }
|
|
355
|
+
} }, { $sort: { count: -1 } });
|
|
356
|
+
return aggregate(Model, pipeline, options);
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Lookup (join) with another collection
|
|
360
|
+
*
|
|
361
|
+
* MongoDB $lookup has two mutually exclusive forms:
|
|
362
|
+
* 1. Simple form: { from, localField, foreignField, as }
|
|
363
|
+
* 2. Pipeline form: { from, let, pipeline, as }
|
|
364
|
+
*
|
|
365
|
+
* This function automatically selects the appropriate form based on parameters.
|
|
366
|
+
*/
|
|
367
|
+
async function lookup(Model, lookupOptions) {
|
|
368
|
+
const { from, localField, foreignField, as, pipeline = [], let: letVars, query = {}, options = {} } = lookupOptions;
|
|
369
|
+
const aggPipeline = [];
|
|
370
|
+
if (Object.keys(query).length > 0) aggPipeline.push({ $match: query });
|
|
371
|
+
const builder = new LookupBuilder(from).localField(localField).foreignField(foreignField).as(as || from);
|
|
372
|
+
if (lookupOptions.single) builder.single(lookupOptions.single);
|
|
373
|
+
if (pipeline.length > 0) builder.pipeline(pipeline);
|
|
374
|
+
if (letVars) builder.let(letVars);
|
|
375
|
+
if (lookupOptions.sanitize === false) builder.sanitize(false);
|
|
376
|
+
aggPipeline.push(...builder.build());
|
|
377
|
+
return aggregate(Model, aggPipeline, options);
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Unwind array field
|
|
381
|
+
*/
|
|
382
|
+
async function unwind(Model, field, options = {}) {
|
|
383
|
+
return aggregate(Model, [{ $unwind: {
|
|
384
|
+
path: `$${field}`,
|
|
385
|
+
preserveNullAndEmptyArrays: options.preserveEmpty !== false
|
|
386
|
+
} }], { session: options.session });
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Facet search (multiple aggregations in one query)
|
|
390
|
+
*/
|
|
391
|
+
async function facet(Model, facets, options = {}) {
|
|
392
|
+
return aggregate(Model, [{ $facet: facets }], options);
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Get distinct values
|
|
396
|
+
*/
|
|
397
|
+
async function distinct(Model, field, query = {}, options = {}) {
|
|
398
|
+
const q = Model.distinct(field, query).session(options.session ?? null);
|
|
399
|
+
if (options.readPreference) q.read(options.readPreference);
|
|
400
|
+
return q;
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Calculate sum
|
|
404
|
+
*/
|
|
405
|
+
async function sum(Model, field, query = {}, options = {}) {
|
|
406
|
+
const pipeline = [];
|
|
407
|
+
if (Object.keys(query).length > 0) pipeline.push({ $match: query });
|
|
408
|
+
pipeline.push({ $group: {
|
|
409
|
+
_id: null,
|
|
410
|
+
total: { $sum: `$${field}` }
|
|
411
|
+
} });
|
|
412
|
+
return (await aggregate(Model, pipeline, options))[0]?.total || 0;
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Calculate average
|
|
416
|
+
*/
|
|
417
|
+
async function average(Model, field, query = {}, options = {}) {
|
|
418
|
+
const pipeline = [];
|
|
419
|
+
if (Object.keys(query).length > 0) pipeline.push({ $match: query });
|
|
420
|
+
pipeline.push({ $group: {
|
|
421
|
+
_id: null,
|
|
422
|
+
average: { $avg: `$${field}` }
|
|
423
|
+
} });
|
|
424
|
+
return (await aggregate(Model, pipeline, options))[0]?.average || 0;
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Min/Max
|
|
428
|
+
*/
|
|
429
|
+
async function minMax(Model, field, query = {}, options = {}) {
|
|
430
|
+
const pipeline = [];
|
|
431
|
+
if (Object.keys(query).length > 0) pipeline.push({ $match: query });
|
|
432
|
+
pipeline.push({ $group: {
|
|
433
|
+
_id: null,
|
|
434
|
+
min: { $min: `$${field}` },
|
|
435
|
+
max: { $max: `$${field}` }
|
|
436
|
+
} });
|
|
437
|
+
return (await aggregate(Model, pipeline, options))[0] || {
|
|
438
|
+
min: null,
|
|
439
|
+
max: null
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
//#endregion
|
|
443
|
+
//#region src/actions/create.ts
|
|
444
|
+
var create_exports = /* @__PURE__ */ __exportAll({
|
|
445
|
+
create: () => create,
|
|
446
|
+
createDefault: () => createDefault,
|
|
447
|
+
createMany: () => createMany,
|
|
448
|
+
upsert: () => upsert
|
|
449
|
+
});
|
|
450
|
+
/**
|
|
451
|
+
* Create single document
|
|
452
|
+
*/
|
|
453
|
+
async function create(Model, data, options = {}) {
|
|
454
|
+
const document = new Model(data);
|
|
455
|
+
await document.save({ session: options.session });
|
|
456
|
+
return document;
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Create multiple documents
|
|
460
|
+
*/
|
|
461
|
+
async function createMany(Model, dataArray, options = {}) {
|
|
462
|
+
return Model.insertMany(dataArray, {
|
|
463
|
+
session: options.session,
|
|
464
|
+
ordered: options.ordered !== false
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Create with defaults (useful for initialization)
|
|
469
|
+
*/
|
|
470
|
+
async function createDefault(Model, overrides = {}, options = {}) {
|
|
471
|
+
const defaults = {};
|
|
472
|
+
Model.schema.eachPath((path, schemaType) => {
|
|
473
|
+
const schemaOptions = schemaType.options;
|
|
474
|
+
if (schemaOptions.default !== void 0 && path !== "_id") defaults[path] = typeof schemaOptions.default === "function" ? schemaOptions.default() : schemaOptions.default;
|
|
475
|
+
});
|
|
476
|
+
return create(Model, {
|
|
477
|
+
...defaults,
|
|
478
|
+
...overrides
|
|
479
|
+
}, options);
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Upsert (create or update)
|
|
483
|
+
*/
|
|
484
|
+
async function upsert(Model, query, data, options = {}) {
|
|
485
|
+
return Model.findOneAndUpdate(query, { $setOnInsert: data }, {
|
|
486
|
+
upsert: true,
|
|
487
|
+
returnDocument: "after",
|
|
488
|
+
runValidators: true,
|
|
489
|
+
session: options.session,
|
|
490
|
+
...options.updatePipeline !== void 0 ? { updatePipeline: options.updatePipeline } : {}
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
//#endregion
|
|
494
|
+
//#region src/actions/delete.ts
|
|
495
|
+
var delete_exports = /* @__PURE__ */ __exportAll({
|
|
496
|
+
deleteById: () => deleteById,
|
|
497
|
+
deleteByQuery: () => deleteByQuery,
|
|
498
|
+
deleteMany: () => deleteMany,
|
|
499
|
+
restore: () => restore,
|
|
500
|
+
softDelete: () => softDelete
|
|
501
|
+
});
|
|
502
|
+
/**
|
|
503
|
+
* Delete by ID
|
|
504
|
+
*/
|
|
505
|
+
async function deleteById(Model, id, options = {}) {
|
|
506
|
+
const query = {
|
|
507
|
+
_id: id,
|
|
508
|
+
...options.query
|
|
509
|
+
};
|
|
510
|
+
if (!await Model.findOneAndDelete(query).session(options.session ?? null)) throw createError(404, "Document not found");
|
|
511
|
+
return {
|
|
512
|
+
success: true,
|
|
513
|
+
message: "Deleted successfully",
|
|
514
|
+
id: String(id)
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Delete many documents
|
|
519
|
+
*/
|
|
520
|
+
async function deleteMany(Model, query, options = {}) {
|
|
521
|
+
return {
|
|
522
|
+
success: true,
|
|
523
|
+
count: (await Model.deleteMany(query).session(options.session ?? null)).deletedCount,
|
|
524
|
+
message: "Deleted successfully"
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Delete by query
|
|
529
|
+
*/
|
|
530
|
+
async function deleteByQuery(Model, query, options = {}) {
|
|
531
|
+
const document = await Model.findOneAndDelete(query).session(options.session ?? null);
|
|
532
|
+
if (!document && options.throwOnNotFound !== false) throw createError(404, "Document not found");
|
|
533
|
+
return {
|
|
534
|
+
success: true,
|
|
535
|
+
message: "Deleted successfully",
|
|
536
|
+
...document ? { id: String(document._id) } : {}
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* Soft delete (set deleted flag)
|
|
541
|
+
*/
|
|
542
|
+
async function softDelete(Model, id, options = {}) {
|
|
543
|
+
if (!await Model.findByIdAndUpdate(id, {
|
|
544
|
+
deleted: true,
|
|
545
|
+
deletedAt: /* @__PURE__ */ new Date(),
|
|
546
|
+
deletedBy: options.userId
|
|
547
|
+
}, {
|
|
548
|
+
returnDocument: "after",
|
|
549
|
+
session: options.session
|
|
550
|
+
})) throw createError(404, "Document not found");
|
|
551
|
+
return {
|
|
552
|
+
success: true,
|
|
553
|
+
message: "Soft deleted successfully",
|
|
554
|
+
id: String(id),
|
|
555
|
+
soft: true
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Restore soft deleted document
|
|
560
|
+
*/
|
|
561
|
+
async function restore(Model, id, options = {}) {
|
|
562
|
+
if (!await Model.findByIdAndUpdate(id, {
|
|
563
|
+
deleted: false,
|
|
564
|
+
deletedAt: null,
|
|
565
|
+
deletedBy: null
|
|
566
|
+
}, {
|
|
567
|
+
returnDocument: "after",
|
|
568
|
+
session: options.session
|
|
569
|
+
})) throw createError(404, "Document not found");
|
|
570
|
+
return {
|
|
571
|
+
success: true,
|
|
572
|
+
message: "Restored successfully",
|
|
573
|
+
id: String(id)
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
//#endregion
|
|
577
|
+
//#region src/actions/read.ts
|
|
578
|
+
var read_exports = /* @__PURE__ */ __exportAll({
|
|
579
|
+
count: () => count,
|
|
580
|
+
exists: () => exists,
|
|
581
|
+
getAll: () => getAll,
|
|
582
|
+
getById: () => getById,
|
|
583
|
+
getByQuery: () => getByQuery,
|
|
584
|
+
getOrCreate: () => getOrCreate,
|
|
585
|
+
tryGetByQuery: () => tryGetByQuery
|
|
667
586
|
});
|
|
668
587
|
/**
|
|
669
|
-
*
|
|
588
|
+
* Parse populate specification into consistent format
|
|
589
|
+
*/
|
|
590
|
+
function parsePopulate$1(populate) {
|
|
591
|
+
if (!populate) return [];
|
|
592
|
+
if (typeof populate === "string") return populate.split(",").map((p) => p.trim());
|
|
593
|
+
if (Array.isArray(populate)) return populate.map((p) => typeof p === "string" ? p.trim() : p);
|
|
594
|
+
return [populate];
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Get document by ID
|
|
598
|
+
*
|
|
599
|
+
* @param Model - Mongoose model
|
|
600
|
+
* @param id - Document ID
|
|
601
|
+
* @param options - Query options
|
|
602
|
+
* @returns Document or null
|
|
603
|
+
* @throws Error if document not found and throwOnNotFound is true
|
|
604
|
+
*/
|
|
605
|
+
async function getById(Model, id, options = {}) {
|
|
606
|
+
const query = options.query ? Model.findOne({
|
|
607
|
+
_id: id,
|
|
608
|
+
...options.query
|
|
609
|
+
}) : Model.findById(id);
|
|
610
|
+
if (options.select) query.select(options.select);
|
|
611
|
+
if (options.populate) query.populate(parsePopulate$1(options.populate));
|
|
612
|
+
if (options.lean) query.lean();
|
|
613
|
+
if (options.session) query.session(options.session);
|
|
614
|
+
if (options.readPreference) query.read(options.readPreference);
|
|
615
|
+
const document = await query.exec();
|
|
616
|
+
if (!document && options.throwOnNotFound !== false) throw createError(404, "Document not found");
|
|
617
|
+
return document;
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* Get document by query
|
|
621
|
+
*
|
|
622
|
+
* @param Model - Mongoose model
|
|
623
|
+
* @param query - MongoDB query
|
|
624
|
+
* @param options - Query options
|
|
625
|
+
* @returns Document or null
|
|
626
|
+
* @throws Error if document not found and throwOnNotFound is true
|
|
627
|
+
*/
|
|
628
|
+
async function getByQuery(Model, query, options = {}) {
|
|
629
|
+
const mongoQuery = Model.findOne(query);
|
|
630
|
+
if (options.select) mongoQuery.select(options.select);
|
|
631
|
+
if (options.populate) mongoQuery.populate(parsePopulate$1(options.populate));
|
|
632
|
+
if (options.lean) mongoQuery.lean();
|
|
633
|
+
if (options.session) mongoQuery.session(options.session);
|
|
634
|
+
if (options.readPreference) mongoQuery.read(options.readPreference);
|
|
635
|
+
const document = await mongoQuery.exec();
|
|
636
|
+
if (!document && options.throwOnNotFound !== false) throw createError(404, "Document not found");
|
|
637
|
+
return document;
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* Get document by query without throwing (returns null if not found)
|
|
641
|
+
*/
|
|
642
|
+
async function tryGetByQuery(Model, query, options = {}) {
|
|
643
|
+
return getByQuery(Model, query, {
|
|
644
|
+
...options,
|
|
645
|
+
throwOnNotFound: false
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* Get all documents (basic query without pagination)
|
|
650
|
+
* For pagination, use Repository.paginate() or Repository.stream()
|
|
651
|
+
*/
|
|
652
|
+
async function getAll(Model, query = {}, options = {}) {
|
|
653
|
+
let mongoQuery = Model.find(query);
|
|
654
|
+
if (options.select) mongoQuery = mongoQuery.select(options.select);
|
|
655
|
+
if (options.populate) mongoQuery = mongoQuery.populate(parsePopulate$1(options.populate));
|
|
656
|
+
if (options.sort) mongoQuery = mongoQuery.sort(options.sort);
|
|
657
|
+
if (options.limit) mongoQuery = mongoQuery.limit(options.limit);
|
|
658
|
+
if (options.skip) mongoQuery = mongoQuery.skip(options.skip);
|
|
659
|
+
if (options.lean !== false) mongoQuery = mongoQuery.lean();
|
|
660
|
+
if (options.session) mongoQuery = mongoQuery.session(options.session);
|
|
661
|
+
if (options.readPreference) mongoQuery = mongoQuery.read(options.readPreference);
|
|
662
|
+
return mongoQuery.exec();
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* Get or create document (upsert)
|
|
666
|
+
*/
|
|
667
|
+
async function getOrCreate(Model, query, createData, options = {}) {
|
|
668
|
+
return Model.findOneAndUpdate(query, { $setOnInsert: createData }, {
|
|
669
|
+
upsert: true,
|
|
670
|
+
returnDocument: "after",
|
|
671
|
+
runValidators: true,
|
|
672
|
+
session: options.session,
|
|
673
|
+
...options.updatePipeline !== void 0 ? { updatePipeline: options.updatePipeline } : {}
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* Count documents matching query
|
|
670
678
|
*/
|
|
671
|
-
async function
|
|
672
|
-
const
|
|
673
|
-
if (options.
|
|
674
|
-
return
|
|
679
|
+
async function count(Model, query = {}, options = {}) {
|
|
680
|
+
const q = Model.countDocuments(query).session(options.session ?? null);
|
|
681
|
+
if (options.readPreference) q.read(options.readPreference);
|
|
682
|
+
return q;
|
|
675
683
|
}
|
|
676
684
|
/**
|
|
677
|
-
*
|
|
678
|
-
* WARNING: $facet results must be <16MB. For larger results (limit >1000),
|
|
679
|
-
* consider using Repository.aggregatePaginate() or splitting into separate queries.
|
|
685
|
+
* Check if document exists
|
|
680
686
|
*/
|
|
681
|
-
async function
|
|
682
|
-
const
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
limit,
|
|
701
|
-
pages,
|
|
702
|
-
hasNext: page < pages,
|
|
703
|
-
hasPrev: page > 1
|
|
704
|
-
};
|
|
687
|
+
async function exists(Model, query, options = {}) {
|
|
688
|
+
const q = Model.exists(query).session(options.session ?? null);
|
|
689
|
+
if (options.readPreference) q.read(options.readPreference);
|
|
690
|
+
return q;
|
|
691
|
+
}
|
|
692
|
+
//#endregion
|
|
693
|
+
//#region src/actions/update.ts
|
|
694
|
+
var update_exports = /* @__PURE__ */ __exportAll({
|
|
695
|
+
increment: () => increment,
|
|
696
|
+
pullFromArray: () => pullFromArray,
|
|
697
|
+
pushToArray: () => pushToArray,
|
|
698
|
+
update: () => update,
|
|
699
|
+
updateByQuery: () => updateByQuery,
|
|
700
|
+
updateMany: () => updateMany,
|
|
701
|
+
updateWithConstraints: () => updateWithConstraints,
|
|
702
|
+
updateWithValidation: () => updateWithValidation
|
|
703
|
+
});
|
|
704
|
+
function assertUpdatePipelineAllowed(update, updatePipeline) {
|
|
705
|
+
if (Array.isArray(update) && updatePipeline !== true) throw createError(400, "Update pipelines (array updates) are disabled by default; pass `{ updatePipeline: true }` to explicitly allow pipeline-style updates.");
|
|
705
706
|
}
|
|
706
707
|
/**
|
|
707
|
-
*
|
|
708
|
+
* Parse populate specification into consistent format
|
|
708
709
|
*/
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
if (options.limit) pipeline.push({ $limit: options.limit });
|
|
715
|
-
return aggregate(Model, pipeline, options);
|
|
710
|
+
function parsePopulate(populate) {
|
|
711
|
+
if (!populate) return [];
|
|
712
|
+
if (typeof populate === "string") return populate.split(",").map((p) => p.trim());
|
|
713
|
+
if (Array.isArray(populate)) return populate.map((p) => typeof p === "string" ? p.trim() : p);
|
|
714
|
+
return [populate];
|
|
716
715
|
}
|
|
717
716
|
/**
|
|
718
|
-
*
|
|
717
|
+
* Update by ID
|
|
719
718
|
*/
|
|
720
|
-
async function
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
719
|
+
async function update(Model, id, data, options = {}) {
|
|
720
|
+
assertUpdatePipelineAllowed(data, options.updatePipeline);
|
|
721
|
+
const query = {
|
|
722
|
+
_id: id,
|
|
723
|
+
...options.query
|
|
724
|
+
};
|
|
725
|
+
const document = await Model.findOneAndUpdate(query, data, {
|
|
726
|
+
returnDocument: "after",
|
|
727
|
+
runValidators: true,
|
|
728
|
+
session: options.session,
|
|
729
|
+
...options.updatePipeline !== void 0 ? { updatePipeline: options.updatePipeline } : {},
|
|
730
|
+
...options.arrayFilters ? { arrayFilters: options.arrayFilters } : {}
|
|
731
|
+
}).select(options.select || "").populate(parsePopulate(options.populate)).lean(options.lean ?? false);
|
|
732
|
+
if (!document) throw createError(404, "Document not found");
|
|
733
|
+
return document;
|
|
728
734
|
}
|
|
729
735
|
/**
|
|
730
|
-
*
|
|
731
|
-
*
|
|
732
|
-
* MongoDB $lookup has two mutually exclusive forms:
|
|
733
|
-
* 1. Simple form: { from, localField, foreignField, as }
|
|
734
|
-
* 2. Pipeline form: { from, let, pipeline, as }
|
|
735
|
-
*
|
|
736
|
-
* This function automatically selects the appropriate form based on parameters.
|
|
736
|
+
* Update with query constraints (optimized)
|
|
737
|
+
* Returns null if constraints not met (not an error)
|
|
737
738
|
*/
|
|
738
|
-
async function
|
|
739
|
-
|
|
740
|
-
const
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
as
|
|
752
|
-
} });
|
|
753
|
-
} else {
|
|
754
|
-
const safePipeline = lookupOptions.sanitize !== false ? LookupBuilder.sanitizePipeline(pipeline) : pipeline;
|
|
755
|
-
aggPipeline.push({ $lookup: {
|
|
756
|
-
from,
|
|
757
|
-
...letVars && { let: letVars },
|
|
758
|
-
pipeline: safePipeline,
|
|
759
|
-
as
|
|
760
|
-
} });
|
|
761
|
-
}
|
|
762
|
-
else aggPipeline.push({ $lookup: {
|
|
763
|
-
from,
|
|
764
|
-
localField,
|
|
765
|
-
foreignField,
|
|
766
|
-
as
|
|
767
|
-
} });
|
|
768
|
-
return aggregate(Model, aggPipeline, options);
|
|
739
|
+
async function updateWithConstraints(Model, id, data, constraints = {}, options = {}) {
|
|
740
|
+
assertUpdatePipelineAllowed(data, options.updatePipeline);
|
|
741
|
+
const query = {
|
|
742
|
+
_id: id,
|
|
743
|
+
...constraints
|
|
744
|
+
};
|
|
745
|
+
return await Model.findOneAndUpdate(query, data, {
|
|
746
|
+
returnDocument: "after",
|
|
747
|
+
runValidators: true,
|
|
748
|
+
session: options.session,
|
|
749
|
+
...options.updatePipeline !== void 0 ? { updatePipeline: options.updatePipeline } : {},
|
|
750
|
+
...options.arrayFilters ? { arrayFilters: options.arrayFilters } : {}
|
|
751
|
+
}).select(options.select || "").populate(parsePopulate(options.populate)).lean(options.lean ?? false);
|
|
769
752
|
}
|
|
770
753
|
/**
|
|
771
|
-
*
|
|
754
|
+
* Update with validation (smart optimization)
|
|
755
|
+
* 1-query on success, 2-queries for detailed errors
|
|
772
756
|
*/
|
|
773
|
-
async function
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
757
|
+
async function updateWithValidation(Model, id, data, validationOptions = {}, options = {}) {
|
|
758
|
+
const { buildConstraints, validateUpdate } = validationOptions;
|
|
759
|
+
assertUpdatePipelineAllowed(data, options.updatePipeline);
|
|
760
|
+
if (buildConstraints) {
|
|
761
|
+
const document = await updateWithConstraints(Model, id, data, buildConstraints(data), options);
|
|
762
|
+
if (document) return {
|
|
763
|
+
success: true,
|
|
764
|
+
data: document
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
const findQuery = {
|
|
768
|
+
_id: id,
|
|
769
|
+
...options.query
|
|
770
|
+
};
|
|
771
|
+
const existing = await Model.findOne(findQuery).select(options.select || "").session(options.session ?? null).lean();
|
|
772
|
+
if (!existing) return {
|
|
773
|
+
success: false,
|
|
774
|
+
error: {
|
|
775
|
+
code: 404,
|
|
776
|
+
message: "Document not found"
|
|
777
|
+
}
|
|
778
|
+
};
|
|
779
|
+
if (validateUpdate) {
|
|
780
|
+
const validation = validateUpdate(existing, data);
|
|
781
|
+
if (!validation.valid) return {
|
|
782
|
+
success: false,
|
|
783
|
+
error: {
|
|
784
|
+
code: 403,
|
|
785
|
+
message: validation.message || "Update not allowed",
|
|
786
|
+
violations: validation.violations
|
|
787
|
+
}
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
return {
|
|
791
|
+
success: true,
|
|
792
|
+
data: await update(Model, id, data, options)
|
|
793
|
+
};
|
|
778
794
|
}
|
|
779
795
|
/**
|
|
780
|
-
*
|
|
796
|
+
* Update many documents
|
|
781
797
|
*/
|
|
782
|
-
async function
|
|
783
|
-
|
|
798
|
+
async function updateMany(Model, query, data, options = {}) {
|
|
799
|
+
assertUpdatePipelineAllowed(data, options.updatePipeline);
|
|
800
|
+
const result = await Model.updateMany(query, data, {
|
|
801
|
+
runValidators: true,
|
|
802
|
+
session: options.session,
|
|
803
|
+
...options.updatePipeline !== void 0 ? { updatePipeline: options.updatePipeline } : {},
|
|
804
|
+
...options.arrayFilters ? { arrayFilters: options.arrayFilters } : {}
|
|
805
|
+
});
|
|
806
|
+
return {
|
|
807
|
+
matchedCount: result.matchedCount,
|
|
808
|
+
modifiedCount: result.modifiedCount
|
|
809
|
+
};
|
|
784
810
|
}
|
|
785
811
|
/**
|
|
786
|
-
*
|
|
812
|
+
* Update by query
|
|
787
813
|
*/
|
|
788
|
-
async function
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
814
|
+
async function updateByQuery(Model, query, data, options = {}) {
|
|
815
|
+
assertUpdatePipelineAllowed(data, options.updatePipeline);
|
|
816
|
+
const document = await Model.findOneAndUpdate(query, data, {
|
|
817
|
+
returnDocument: "after",
|
|
818
|
+
runValidators: true,
|
|
819
|
+
session: options.session,
|
|
820
|
+
...options.updatePipeline !== void 0 ? { updatePipeline: options.updatePipeline } : {},
|
|
821
|
+
...options.arrayFilters ? { arrayFilters: options.arrayFilters } : {}
|
|
822
|
+
}).select(options.select || "").populate(parsePopulate(options.populate)).lean(options.lean ?? false);
|
|
823
|
+
if (!document && options.throwOnNotFound !== false) throw createError(404, "Document not found");
|
|
824
|
+
return document;
|
|
792
825
|
}
|
|
793
826
|
/**
|
|
794
|
-
*
|
|
827
|
+
* Increment field
|
|
795
828
|
*/
|
|
796
|
-
async function
|
|
797
|
-
|
|
798
|
-
if (Object.keys(query).length > 0) pipeline.push({ $match: query });
|
|
799
|
-
pipeline.push({ $group: {
|
|
800
|
-
_id: null,
|
|
801
|
-
total: { $sum: `$${field}` }
|
|
802
|
-
} });
|
|
803
|
-
return (await aggregate(Model, pipeline, options))[0]?.total || 0;
|
|
829
|
+
async function increment(Model, id, field, value = 1, options = {}) {
|
|
830
|
+
return update(Model, id, { $inc: { [field]: value } }, options);
|
|
804
831
|
}
|
|
805
832
|
/**
|
|
806
|
-
*
|
|
833
|
+
* Push to array
|
|
807
834
|
*/
|
|
808
|
-
async function
|
|
809
|
-
|
|
810
|
-
if (Object.keys(query).length > 0) pipeline.push({ $match: query });
|
|
811
|
-
pipeline.push({ $group: {
|
|
812
|
-
_id: null,
|
|
813
|
-
average: { $avg: `$${field}` }
|
|
814
|
-
} });
|
|
815
|
-
return (await aggregate(Model, pipeline, options))[0]?.average || 0;
|
|
835
|
+
async function pushToArray(Model, id, field, value, options = {}) {
|
|
836
|
+
return update(Model, id, { $push: { [field]: value } }, options);
|
|
816
837
|
}
|
|
817
838
|
/**
|
|
818
|
-
*
|
|
839
|
+
* Pull from array
|
|
819
840
|
*/
|
|
820
|
-
async function
|
|
821
|
-
|
|
822
|
-
if (Object.keys(query).length > 0) pipeline.push({ $match: query });
|
|
823
|
-
pipeline.push({ $group: {
|
|
824
|
-
_id: null,
|
|
825
|
-
min: { $min: `$${field}` },
|
|
826
|
-
max: { $max: `$${field}` }
|
|
827
|
-
} });
|
|
828
|
-
return (await aggregate(Model, pipeline, options))[0] || {
|
|
829
|
-
min: null,
|
|
830
|
-
max: null
|
|
831
|
-
};
|
|
841
|
+
async function pullFromArray(Model, id, field, value, options = {}) {
|
|
842
|
+
return update(Model, id, { $pull: { [field]: value } }, options);
|
|
832
843
|
}
|
|
833
|
-
|
|
834
844
|
//#endregion
|
|
835
|
-
export {
|
|
845
|
+
export { LookupBuilder as _, getById as a, read_exports as c, create as d, createMany as f, distinct as g, aggregate_exports as h, exists as i, deleteById as l, upsert as m, update_exports as n, getByQuery as o, create_exports as p, count as r, getOrCreate as s, update as t, delete_exports as u };
|