@classytic/mongokit 3.2.0 → 3.2.2
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 +470 -193
- package/dist/actions/index.d.mts +9 -0
- package/dist/actions/index.mjs +15 -0
- package/dist/aggregate-BAi4Do-X.mjs +767 -0
- package/dist/aggregate-CCHI7F51.d.mts +269 -0
- package/dist/ai/index.d.mts +125 -0
- package/dist/ai/index.mjs +203 -0
- package/dist/cache-keys-C8Z9B5sw.mjs +204 -0
- package/dist/chunk-DQk6qfdC.mjs +18 -0
- package/dist/create-BuO6xt0v.mjs +55 -0
- package/dist/custom-id.plugin-B_zIs6gE.mjs +1818 -0
- package/dist/custom-id.plugin-BzZI4gnE.d.mts +893 -0
- package/dist/index.d.mts +1012 -0
- package/dist/index.mjs +1906 -0
- package/dist/limits-DsNeCx4D.mjs +299 -0
- package/dist/logger-D8ily-PP.mjs +51 -0
- package/dist/mongooseToJsonSchema-COdDEkIJ.mjs +317 -0
- package/dist/{mongooseToJsonSchema-CaRF_bCN.d.ts → mongooseToJsonSchema-Wbvjfwkn.d.mts} +16 -89
- package/dist/pagination/PaginationEngine.d.mts +93 -0
- package/dist/pagination/PaginationEngine.mjs +196 -0
- package/dist/plugins/index.d.mts +3 -0
- package/dist/plugins/index.mjs +3 -0
- package/dist/types-D-gploPr.d.mts +1241 -0
- package/dist/utils/{index.d.ts → index.d.mts} +14 -21
- package/dist/utils/index.mjs +5 -0
- package/package.json +21 -21
- package/dist/actions/index.d.ts +0 -3
- package/dist/actions/index.js +0 -5
- package/dist/ai/index.d.ts +0 -175
- package/dist/ai/index.js +0 -206
- package/dist/chunks/chunk-2ZN65ZOP.js +0 -93
- package/dist/chunks/chunk-44KXLGPO.js +0 -388
- package/dist/chunks/chunk-DEVXDBRL.js +0 -1226
- package/dist/chunks/chunk-I7CWNAJB.js +0 -46
- package/dist/chunks/chunk-JWUAVZ3L.js +0 -8
- package/dist/chunks/chunk-UE2IEXZJ.js +0 -306
- package/dist/chunks/chunk-URLJFIR7.js +0 -22
- package/dist/chunks/chunk-VWKIKZYF.js +0 -737
- package/dist/chunks/chunk-WSFCRVEQ.js +0 -7
- package/dist/index-BDn5fSTE.d.ts +0 -516
- package/dist/index.d.ts +0 -1422
- package/dist/index.js +0 -1893
- package/dist/pagination/PaginationEngine.d.ts +0 -117
- package/dist/pagination/PaginationEngine.js +0 -3
- package/dist/plugins/index.d.ts +0 -922
- package/dist/plugins/index.js +0 -6
- package/dist/types-Jni1KgkP.d.ts +0 -780
- package/dist/utils/index.js +0 -5
|
@@ -1,737 +0,0 @@
|
|
|
1
|
-
import { create_exports } from './chunk-I7CWNAJB.js';
|
|
2
|
-
import { warn } from './chunk-URLJFIR7.js';
|
|
3
|
-
import { createError } from './chunk-JWUAVZ3L.js';
|
|
4
|
-
import { __export } from './chunk-WSFCRVEQ.js';
|
|
5
|
-
|
|
6
|
-
// src/actions/index.ts
|
|
7
|
-
var actions_exports = {};
|
|
8
|
-
__export(actions_exports, {
|
|
9
|
-
aggregate: () => aggregate_exports,
|
|
10
|
-
create: () => create_exports,
|
|
11
|
-
deleteActions: () => delete_exports,
|
|
12
|
-
read: () => read_exports,
|
|
13
|
-
update: () => update_exports
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
// src/actions/read.ts
|
|
17
|
-
var read_exports = {};
|
|
18
|
-
__export(read_exports, {
|
|
19
|
-
count: () => count,
|
|
20
|
-
exists: () => exists,
|
|
21
|
-
getAll: () => getAll,
|
|
22
|
-
getById: () => getById,
|
|
23
|
-
getByQuery: () => getByQuery,
|
|
24
|
-
getOrCreate: () => getOrCreate,
|
|
25
|
-
tryGetByQuery: () => tryGetByQuery
|
|
26
|
-
});
|
|
27
|
-
function parsePopulate(populate) {
|
|
28
|
-
if (!populate) return [];
|
|
29
|
-
if (typeof populate === "string") {
|
|
30
|
-
return populate.split(",").map((p) => p.trim());
|
|
31
|
-
}
|
|
32
|
-
if (Array.isArray(populate)) {
|
|
33
|
-
return populate.map((p) => typeof p === "string" ? p.trim() : p);
|
|
34
|
-
}
|
|
35
|
-
return [populate];
|
|
36
|
-
}
|
|
37
|
-
async function getById(Model, id, options = {}) {
|
|
38
|
-
const query = options.query ? Model.findOne({ _id: id, ...options.query }) : Model.findById(id);
|
|
39
|
-
if (options.select) query.select(options.select);
|
|
40
|
-
if (options.populate) query.populate(parsePopulate(options.populate));
|
|
41
|
-
if (options.lean) query.lean();
|
|
42
|
-
if (options.session) query.session(options.session);
|
|
43
|
-
const document = await query.exec();
|
|
44
|
-
if (!document && options.throwOnNotFound !== false) {
|
|
45
|
-
throw createError(404, "Document not found");
|
|
46
|
-
}
|
|
47
|
-
return document;
|
|
48
|
-
}
|
|
49
|
-
async function getByQuery(Model, query, options = {}) {
|
|
50
|
-
const mongoQuery = Model.findOne(query);
|
|
51
|
-
if (options.select) mongoQuery.select(options.select);
|
|
52
|
-
if (options.populate) mongoQuery.populate(parsePopulate(options.populate));
|
|
53
|
-
if (options.lean) mongoQuery.lean();
|
|
54
|
-
if (options.session) mongoQuery.session(options.session);
|
|
55
|
-
const document = await mongoQuery.exec();
|
|
56
|
-
if (!document && options.throwOnNotFound !== false) {
|
|
57
|
-
throw createError(404, "Document not found");
|
|
58
|
-
}
|
|
59
|
-
return document;
|
|
60
|
-
}
|
|
61
|
-
async function tryGetByQuery(Model, query, options = {}) {
|
|
62
|
-
return getByQuery(Model, query, { ...options, throwOnNotFound: false });
|
|
63
|
-
}
|
|
64
|
-
async function getAll(Model, query = {}, options = {}) {
|
|
65
|
-
let mongoQuery = Model.find(query);
|
|
66
|
-
if (options.select) mongoQuery = mongoQuery.select(options.select);
|
|
67
|
-
if (options.populate) mongoQuery = mongoQuery.populate(parsePopulate(options.populate));
|
|
68
|
-
if (options.sort) mongoQuery = mongoQuery.sort(options.sort);
|
|
69
|
-
if (options.limit) mongoQuery = mongoQuery.limit(options.limit);
|
|
70
|
-
if (options.skip) mongoQuery = mongoQuery.skip(options.skip);
|
|
71
|
-
mongoQuery = mongoQuery.lean(options.lean !== false);
|
|
72
|
-
if (options.session) mongoQuery = mongoQuery.session(options.session);
|
|
73
|
-
return mongoQuery.exec();
|
|
74
|
-
}
|
|
75
|
-
async function getOrCreate(Model, query, createData, options = {}) {
|
|
76
|
-
return Model.findOneAndUpdate(
|
|
77
|
-
query,
|
|
78
|
-
{ $setOnInsert: createData },
|
|
79
|
-
{
|
|
80
|
-
upsert: true,
|
|
81
|
-
new: true,
|
|
82
|
-
runValidators: true,
|
|
83
|
-
session: options.session,
|
|
84
|
-
...options.updatePipeline !== void 0 ? { updatePipeline: options.updatePipeline } : {}
|
|
85
|
-
}
|
|
86
|
-
);
|
|
87
|
-
}
|
|
88
|
-
async function count(Model, query = {}, options = {}) {
|
|
89
|
-
return Model.countDocuments(query).session(options.session ?? null);
|
|
90
|
-
}
|
|
91
|
-
async function exists(Model, query, options = {}) {
|
|
92
|
-
return Model.exists(query).session(options.session ?? null);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// src/actions/update.ts
|
|
96
|
-
var update_exports = {};
|
|
97
|
-
__export(update_exports, {
|
|
98
|
-
increment: () => increment,
|
|
99
|
-
pullFromArray: () => pullFromArray,
|
|
100
|
-
pushToArray: () => pushToArray,
|
|
101
|
-
update: () => update,
|
|
102
|
-
updateByQuery: () => updateByQuery,
|
|
103
|
-
updateMany: () => updateMany,
|
|
104
|
-
updateWithConstraints: () => updateWithConstraints,
|
|
105
|
-
updateWithValidation: () => updateWithValidation
|
|
106
|
-
});
|
|
107
|
-
function assertUpdatePipelineAllowed(update2, updatePipeline) {
|
|
108
|
-
if (Array.isArray(update2) && updatePipeline !== true) {
|
|
109
|
-
throw createError(
|
|
110
|
-
400,
|
|
111
|
-
"Update pipelines (array updates) are disabled by default; pass `{ updatePipeline: true }` to explicitly allow pipeline-style updates."
|
|
112
|
-
);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
function parsePopulate2(populate) {
|
|
116
|
-
if (!populate) return [];
|
|
117
|
-
if (typeof populate === "string") {
|
|
118
|
-
return populate.split(",").map((p) => p.trim());
|
|
119
|
-
}
|
|
120
|
-
if (Array.isArray(populate)) {
|
|
121
|
-
return populate.map((p) => typeof p === "string" ? p.trim() : p);
|
|
122
|
-
}
|
|
123
|
-
return [populate];
|
|
124
|
-
}
|
|
125
|
-
async function update(Model, id, data, options = {}) {
|
|
126
|
-
assertUpdatePipelineAllowed(data, options.updatePipeline);
|
|
127
|
-
const query = { _id: id, ...options.query };
|
|
128
|
-
const document = await Model.findOneAndUpdate(query, data, {
|
|
129
|
-
new: true,
|
|
130
|
-
runValidators: true,
|
|
131
|
-
session: options.session,
|
|
132
|
-
...options.updatePipeline !== void 0 ? { updatePipeline: options.updatePipeline } : {}
|
|
133
|
-
}).select(options.select || "").populate(parsePopulate2(options.populate)).lean(options.lean ?? false);
|
|
134
|
-
if (!document) {
|
|
135
|
-
throw createError(404, "Document not found");
|
|
136
|
-
}
|
|
137
|
-
return document;
|
|
138
|
-
}
|
|
139
|
-
async function updateWithConstraints(Model, id, data, constraints = {}, options = {}) {
|
|
140
|
-
assertUpdatePipelineAllowed(data, options.updatePipeline);
|
|
141
|
-
const query = { _id: id, ...constraints };
|
|
142
|
-
const document = await Model.findOneAndUpdate(query, data, {
|
|
143
|
-
new: true,
|
|
144
|
-
runValidators: true,
|
|
145
|
-
session: options.session,
|
|
146
|
-
...options.updatePipeline !== void 0 ? { updatePipeline: options.updatePipeline } : {}
|
|
147
|
-
}).select(options.select || "").populate(parsePopulate2(options.populate)).lean(options.lean ?? false);
|
|
148
|
-
return document;
|
|
149
|
-
}
|
|
150
|
-
async function updateWithValidation(Model, id, data, validationOptions = {}, options = {}) {
|
|
151
|
-
const { buildConstraints, validateUpdate } = validationOptions;
|
|
152
|
-
assertUpdatePipelineAllowed(data, options.updatePipeline);
|
|
153
|
-
if (buildConstraints) {
|
|
154
|
-
const constraints = buildConstraints(data);
|
|
155
|
-
const document = await updateWithConstraints(Model, id, data, constraints, options);
|
|
156
|
-
if (document) {
|
|
157
|
-
return { success: true, data: document };
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
const existing = await Model.findById(id).select(options.select || "").lean();
|
|
161
|
-
if (!existing) {
|
|
162
|
-
return {
|
|
163
|
-
success: false,
|
|
164
|
-
error: {
|
|
165
|
-
code: 404,
|
|
166
|
-
message: "Document not found"
|
|
167
|
-
}
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
if (validateUpdate) {
|
|
171
|
-
const validation = validateUpdate(existing, data);
|
|
172
|
-
if (!validation.valid) {
|
|
173
|
-
return {
|
|
174
|
-
success: false,
|
|
175
|
-
error: {
|
|
176
|
-
code: 403,
|
|
177
|
-
message: validation.message || "Update not allowed",
|
|
178
|
-
violations: validation.violations
|
|
179
|
-
}
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
const updated = await update(Model, id, data, options);
|
|
184
|
-
return { success: true, data: updated };
|
|
185
|
-
}
|
|
186
|
-
async function updateMany(Model, query, data, options = {}) {
|
|
187
|
-
assertUpdatePipelineAllowed(data, options.updatePipeline);
|
|
188
|
-
const result = await Model.updateMany(query, data, {
|
|
189
|
-
runValidators: true,
|
|
190
|
-
session: options.session,
|
|
191
|
-
...options.updatePipeline !== void 0 ? { updatePipeline: options.updatePipeline } : {}
|
|
192
|
-
});
|
|
193
|
-
return {
|
|
194
|
-
matchedCount: result.matchedCount,
|
|
195
|
-
modifiedCount: result.modifiedCount
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
async function updateByQuery(Model, query, data, options = {}) {
|
|
199
|
-
assertUpdatePipelineAllowed(data, options.updatePipeline);
|
|
200
|
-
const document = await Model.findOneAndUpdate(query, data, {
|
|
201
|
-
new: true,
|
|
202
|
-
runValidators: true,
|
|
203
|
-
session: options.session,
|
|
204
|
-
...options.updatePipeline !== void 0 ? { updatePipeline: options.updatePipeline } : {}
|
|
205
|
-
}).select(options.select || "").populate(parsePopulate2(options.populate)).lean(options.lean ?? false);
|
|
206
|
-
if (!document && options.throwOnNotFound !== false) {
|
|
207
|
-
throw createError(404, "Document not found");
|
|
208
|
-
}
|
|
209
|
-
return document;
|
|
210
|
-
}
|
|
211
|
-
async function increment(Model, id, field, value = 1, options = {}) {
|
|
212
|
-
return update(Model, id, { $inc: { [field]: value } }, options);
|
|
213
|
-
}
|
|
214
|
-
async function pushToArray(Model, id, field, value, options = {}) {
|
|
215
|
-
return update(Model, id, { $push: { [field]: value } }, options);
|
|
216
|
-
}
|
|
217
|
-
async function pullFromArray(Model, id, field, value, options = {}) {
|
|
218
|
-
return update(Model, id, { $pull: { [field]: value } }, options);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// src/actions/delete.ts
|
|
222
|
-
var delete_exports = {};
|
|
223
|
-
__export(delete_exports, {
|
|
224
|
-
deleteById: () => deleteById,
|
|
225
|
-
deleteByQuery: () => deleteByQuery,
|
|
226
|
-
deleteMany: () => deleteMany,
|
|
227
|
-
restore: () => restore,
|
|
228
|
-
softDelete: () => softDelete
|
|
229
|
-
});
|
|
230
|
-
async function deleteById(Model, id, options = {}) {
|
|
231
|
-
const query = { _id: id, ...options.query };
|
|
232
|
-
const document = await Model.findOneAndDelete(query).session(options.session ?? null);
|
|
233
|
-
if (!document) {
|
|
234
|
-
throw createError(404, "Document not found");
|
|
235
|
-
}
|
|
236
|
-
return { success: true, message: "Deleted successfully" };
|
|
237
|
-
}
|
|
238
|
-
async function deleteMany(Model, query, options = {}) {
|
|
239
|
-
const result = await Model.deleteMany(query).session(options.session ?? null);
|
|
240
|
-
return {
|
|
241
|
-
success: true,
|
|
242
|
-
count: result.deletedCount,
|
|
243
|
-
message: "Deleted successfully"
|
|
244
|
-
};
|
|
245
|
-
}
|
|
246
|
-
async function deleteByQuery(Model, query, options = {}) {
|
|
247
|
-
const document = await Model.findOneAndDelete(query).session(options.session ?? null);
|
|
248
|
-
if (!document && options.throwOnNotFound !== false) {
|
|
249
|
-
throw createError(404, "Document not found");
|
|
250
|
-
}
|
|
251
|
-
return { success: true, message: "Deleted successfully" };
|
|
252
|
-
}
|
|
253
|
-
async function softDelete(Model, id, options = {}) {
|
|
254
|
-
const document = await Model.findByIdAndUpdate(
|
|
255
|
-
id,
|
|
256
|
-
{
|
|
257
|
-
deleted: true,
|
|
258
|
-
deletedAt: /* @__PURE__ */ new Date(),
|
|
259
|
-
deletedBy: options.userId
|
|
260
|
-
},
|
|
261
|
-
{ new: true, session: options.session }
|
|
262
|
-
);
|
|
263
|
-
if (!document) {
|
|
264
|
-
throw createError(404, "Document not found");
|
|
265
|
-
}
|
|
266
|
-
return { success: true, message: "Soft deleted successfully" };
|
|
267
|
-
}
|
|
268
|
-
async function restore(Model, id, options = {}) {
|
|
269
|
-
const document = await Model.findByIdAndUpdate(
|
|
270
|
-
id,
|
|
271
|
-
{
|
|
272
|
-
deleted: false,
|
|
273
|
-
deletedAt: null,
|
|
274
|
-
deletedBy: null
|
|
275
|
-
},
|
|
276
|
-
{ new: true, session: options.session }
|
|
277
|
-
);
|
|
278
|
-
if (!document) {
|
|
279
|
-
throw createError(404, "Document not found");
|
|
280
|
-
}
|
|
281
|
-
return { success: true, message: "Restored successfully" };
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// src/actions/aggregate.ts
|
|
285
|
-
var aggregate_exports = {};
|
|
286
|
-
__export(aggregate_exports, {
|
|
287
|
-
aggregate: () => aggregate,
|
|
288
|
-
aggregatePaginate: () => aggregatePaginate,
|
|
289
|
-
average: () => average,
|
|
290
|
-
countBy: () => countBy,
|
|
291
|
-
distinct: () => distinct,
|
|
292
|
-
facet: () => facet,
|
|
293
|
-
groupBy: () => groupBy,
|
|
294
|
-
lookup: () => lookup,
|
|
295
|
-
minMax: () => minMax,
|
|
296
|
-
sum: () => sum,
|
|
297
|
-
unwind: () => unwind
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
// src/query/LookupBuilder.ts
|
|
301
|
-
var BLOCKED_PIPELINE_STAGES = ["$out", "$merge", "$unionWith", "$collStats", "$currentOp", "$listSessions"];
|
|
302
|
-
var DANGEROUS_OPERATORS = ["$where", "$function", "$accumulator", "$expr"];
|
|
303
|
-
var LookupBuilder = class _LookupBuilder {
|
|
304
|
-
options = {};
|
|
305
|
-
constructor(from) {
|
|
306
|
-
if (from) this.options.from = from;
|
|
307
|
-
}
|
|
308
|
-
/**
|
|
309
|
-
* Set the collection to join with
|
|
310
|
-
*/
|
|
311
|
-
from(collection) {
|
|
312
|
-
this.options.from = collection;
|
|
313
|
-
return this;
|
|
314
|
-
}
|
|
315
|
-
/**
|
|
316
|
-
* Set the local field (source collection)
|
|
317
|
-
* IMPORTANT: This field should be indexed for optimal performance
|
|
318
|
-
*/
|
|
319
|
-
localField(field) {
|
|
320
|
-
this.options.localField = field;
|
|
321
|
-
return this;
|
|
322
|
-
}
|
|
323
|
-
/**
|
|
324
|
-
* Set the foreign field (target collection)
|
|
325
|
-
* IMPORTANT: This field should be indexed (preferably unique) for optimal performance
|
|
326
|
-
*/
|
|
327
|
-
foreignField(field) {
|
|
328
|
-
this.options.foreignField = field;
|
|
329
|
-
return this;
|
|
330
|
-
}
|
|
331
|
-
/**
|
|
332
|
-
* Set the output field name
|
|
333
|
-
* Defaults to the collection name if not specified
|
|
334
|
-
*/
|
|
335
|
-
as(fieldName) {
|
|
336
|
-
this.options.as = fieldName;
|
|
337
|
-
return this;
|
|
338
|
-
}
|
|
339
|
-
/**
|
|
340
|
-
* Mark this lookup as returning a single document
|
|
341
|
-
* Automatically unwraps the array result to a single object or null
|
|
342
|
-
*/
|
|
343
|
-
single(isSingle = true) {
|
|
344
|
-
this.options.single = isSingle;
|
|
345
|
-
return this;
|
|
346
|
-
}
|
|
347
|
-
/**
|
|
348
|
-
* Add a pipeline to filter/transform joined documents
|
|
349
|
-
* Useful for filtering, sorting, or limiting joined results
|
|
350
|
-
*
|
|
351
|
-
* @example
|
|
352
|
-
* ```typescript
|
|
353
|
-
* lookup.pipeline([
|
|
354
|
-
* { $match: { status: 'active' } },
|
|
355
|
-
* { $sort: { priority: -1 } },
|
|
356
|
-
* { $limit: 5 }
|
|
357
|
-
* ]);
|
|
358
|
-
* ```
|
|
359
|
-
*/
|
|
360
|
-
pipeline(stages) {
|
|
361
|
-
this.options.pipeline = stages;
|
|
362
|
-
return this;
|
|
363
|
-
}
|
|
364
|
-
/**
|
|
365
|
-
* Set let variables for use in pipeline
|
|
366
|
-
* Allows referencing local document fields in the pipeline
|
|
367
|
-
*/
|
|
368
|
-
let(variables) {
|
|
369
|
-
this.options.let = variables;
|
|
370
|
-
return this;
|
|
371
|
-
}
|
|
372
|
-
/**
|
|
373
|
-
* Build the $lookup aggregation stage(s)
|
|
374
|
-
* Returns an array of pipeline stages including $lookup and optional $unwind
|
|
375
|
-
*
|
|
376
|
-
* IMPORTANT: MongoDB $lookup has two mutually exclusive forms:
|
|
377
|
-
* 1. Simple form: { from, localField, foreignField, as }
|
|
378
|
-
* 2. Pipeline form: { from, let, pipeline, as }
|
|
379
|
-
*
|
|
380
|
-
* When pipeline or let is specified, we use the pipeline form.
|
|
381
|
-
* Otherwise, we use the simpler localField/foreignField form.
|
|
382
|
-
*/
|
|
383
|
-
build() {
|
|
384
|
-
const { from, localField, foreignField, as, single, pipeline, let: letVars } = this.options;
|
|
385
|
-
if (!from) {
|
|
386
|
-
throw new Error('LookupBuilder: "from" collection is required');
|
|
387
|
-
}
|
|
388
|
-
const outputField = as || from;
|
|
389
|
-
const stages = [];
|
|
390
|
-
const usePipelineForm = pipeline || letVars;
|
|
391
|
-
let lookupStage;
|
|
392
|
-
if (usePipelineForm) {
|
|
393
|
-
if (!pipeline || pipeline.length === 0) {
|
|
394
|
-
if (!localField || !foreignField) {
|
|
395
|
-
throw new Error(
|
|
396
|
-
"LookupBuilder: When using pipeline form without a custom pipeline, both localField and foreignField are required to auto-generate the pipeline"
|
|
397
|
-
);
|
|
398
|
-
}
|
|
399
|
-
const autoPipeline = [
|
|
400
|
-
{
|
|
401
|
-
$match: {
|
|
402
|
-
$expr: {
|
|
403
|
-
$eq: [`$${foreignField}`, `$$${localField}`]
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
];
|
|
408
|
-
lookupStage = {
|
|
409
|
-
$lookup: {
|
|
410
|
-
from,
|
|
411
|
-
let: { [localField]: `$${localField}`, ...letVars || {} },
|
|
412
|
-
pipeline: autoPipeline,
|
|
413
|
-
as: outputField
|
|
414
|
-
}
|
|
415
|
-
};
|
|
416
|
-
} else {
|
|
417
|
-
const safePipeline = this.options.sanitize !== false ? _LookupBuilder.sanitizePipeline(pipeline) : pipeline;
|
|
418
|
-
lookupStage = {
|
|
419
|
-
$lookup: {
|
|
420
|
-
from,
|
|
421
|
-
...letVars && { let: letVars },
|
|
422
|
-
pipeline: safePipeline,
|
|
423
|
-
as: outputField
|
|
424
|
-
}
|
|
425
|
-
};
|
|
426
|
-
}
|
|
427
|
-
} else {
|
|
428
|
-
if (!localField || !foreignField) {
|
|
429
|
-
throw new Error("LookupBuilder: localField and foreignField are required for simple lookup");
|
|
430
|
-
}
|
|
431
|
-
lookupStage = {
|
|
432
|
-
$lookup: {
|
|
433
|
-
from,
|
|
434
|
-
localField,
|
|
435
|
-
foreignField,
|
|
436
|
-
as: outputField
|
|
437
|
-
}
|
|
438
|
-
};
|
|
439
|
-
}
|
|
440
|
-
stages.push(lookupStage);
|
|
441
|
-
if (single) {
|
|
442
|
-
stages.push({
|
|
443
|
-
$unwind: {
|
|
444
|
-
path: `$${outputField}`,
|
|
445
|
-
preserveNullAndEmptyArrays: true
|
|
446
|
-
// Keep documents even if no match found
|
|
447
|
-
}
|
|
448
|
-
});
|
|
449
|
-
}
|
|
450
|
-
return stages;
|
|
451
|
-
}
|
|
452
|
-
/**
|
|
453
|
-
* Build and return only the $lookup stage (without $unwind)
|
|
454
|
-
* Useful when you want to handle unwrapping yourself
|
|
455
|
-
*/
|
|
456
|
-
buildLookupOnly() {
|
|
457
|
-
const stages = this.build();
|
|
458
|
-
return stages[0];
|
|
459
|
-
}
|
|
460
|
-
/**
|
|
461
|
-
* Static helper: Create a simple lookup in one line
|
|
462
|
-
*/
|
|
463
|
-
static simple(from, localField, foreignField, options = {}) {
|
|
464
|
-
return new _LookupBuilder(from).localField(localField).foreignField(foreignField).as(options.as || from).single(options.single || false).build();
|
|
465
|
-
}
|
|
466
|
-
/**
|
|
467
|
-
* Static helper: Create multiple lookups at once
|
|
468
|
-
*
|
|
469
|
-
* @example
|
|
470
|
-
* ```typescript
|
|
471
|
-
* const pipeline = LookupBuilder.multiple([
|
|
472
|
-
* { from: 'departments', localField: 'deptSlug', foreignField: 'slug', single: true },
|
|
473
|
-
* { from: 'managers', localField: 'managerId', foreignField: '_id', single: true }
|
|
474
|
-
* ]);
|
|
475
|
-
* ```
|
|
476
|
-
*/
|
|
477
|
-
static multiple(lookups) {
|
|
478
|
-
return lookups.flatMap((lookup2) => {
|
|
479
|
-
const builder = new _LookupBuilder(lookup2.from).localField(lookup2.localField).foreignField(lookup2.foreignField);
|
|
480
|
-
if (lookup2.as) builder.as(lookup2.as);
|
|
481
|
-
if (lookup2.single) builder.single(lookup2.single);
|
|
482
|
-
if (lookup2.pipeline) builder.pipeline(lookup2.pipeline);
|
|
483
|
-
if (lookup2.let) builder.let(lookup2.let);
|
|
484
|
-
return builder.build();
|
|
485
|
-
});
|
|
486
|
-
}
|
|
487
|
-
/**
|
|
488
|
-
* Static helper: Create a nested lookup (lookup within lookup)
|
|
489
|
-
* Useful for multi-level joins like Order -> Product -> Category
|
|
490
|
-
*
|
|
491
|
-
* @example
|
|
492
|
-
* ```typescript
|
|
493
|
-
* // Join orders with products, then products with categories
|
|
494
|
-
* const pipeline = LookupBuilder.nested([
|
|
495
|
-
* { from: 'products', localField: 'productSku', foreignField: 'sku', as: 'product', single: true },
|
|
496
|
-
* { from: 'categories', localField: 'product.categorySlug', foreignField: 'slug', as: 'product.category', single: true }
|
|
497
|
-
* ]);
|
|
498
|
-
* ```
|
|
499
|
-
*/
|
|
500
|
-
static nested(lookups) {
|
|
501
|
-
return lookups.flatMap((lookup2, index) => {
|
|
502
|
-
const builder = new _LookupBuilder(lookup2.from).localField(lookup2.localField).foreignField(lookup2.foreignField);
|
|
503
|
-
if (lookup2.as) builder.as(lookup2.as);
|
|
504
|
-
if (lookup2.single !== void 0) builder.single(lookup2.single);
|
|
505
|
-
if (lookup2.pipeline) builder.pipeline(lookup2.pipeline);
|
|
506
|
-
if (lookup2.let) builder.let(lookup2.let);
|
|
507
|
-
return builder.build();
|
|
508
|
-
});
|
|
509
|
-
}
|
|
510
|
-
/**
|
|
511
|
-
* Sanitize pipeline stages by blocking dangerous stages and operators.
|
|
512
|
-
* Used internally by build() and available for external use (e.g., aggregate.ts).
|
|
513
|
-
*/
|
|
514
|
-
static sanitizePipeline(stages) {
|
|
515
|
-
const sanitized = [];
|
|
516
|
-
for (const stage of stages) {
|
|
517
|
-
if (!stage || typeof stage !== "object") continue;
|
|
518
|
-
const entries = Object.entries(stage);
|
|
519
|
-
if (entries.length !== 1) continue;
|
|
520
|
-
const [op, config] = entries[0];
|
|
521
|
-
if (BLOCKED_PIPELINE_STAGES.includes(op)) {
|
|
522
|
-
warn(`[mongokit] Blocked dangerous pipeline stage in lookup: ${op}`);
|
|
523
|
-
continue;
|
|
524
|
-
}
|
|
525
|
-
if ((op === "$match" || op === "$addFields" || op === "$set") && typeof config === "object" && config !== null) {
|
|
526
|
-
sanitized.push({ [op]: _LookupBuilder._sanitizeDeep(config) });
|
|
527
|
-
} else {
|
|
528
|
-
sanitized.push(stage);
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
return sanitized;
|
|
532
|
-
}
|
|
533
|
-
/**
|
|
534
|
-
* Recursively remove dangerous operators from an expression object.
|
|
535
|
-
*/
|
|
536
|
-
static _sanitizeDeep(config) {
|
|
537
|
-
const sanitized = {};
|
|
538
|
-
for (const [key, value] of Object.entries(config)) {
|
|
539
|
-
if (DANGEROUS_OPERATORS.includes(key)) {
|
|
540
|
-
warn(`[mongokit] Blocked dangerous operator in lookup pipeline: ${key}`);
|
|
541
|
-
continue;
|
|
542
|
-
}
|
|
543
|
-
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
544
|
-
sanitized[key] = _LookupBuilder._sanitizeDeep(value);
|
|
545
|
-
} else if (Array.isArray(value)) {
|
|
546
|
-
sanitized[key] = value.map((item) => {
|
|
547
|
-
if (item && typeof item === "object" && !Array.isArray(item)) {
|
|
548
|
-
return _LookupBuilder._sanitizeDeep(item);
|
|
549
|
-
}
|
|
550
|
-
return item;
|
|
551
|
-
});
|
|
552
|
-
} else {
|
|
553
|
-
sanitized[key] = value;
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
return sanitized;
|
|
557
|
-
}
|
|
558
|
-
};
|
|
559
|
-
|
|
560
|
-
// src/actions/aggregate.ts
|
|
561
|
-
async function aggregate(Model, pipeline, options = {}) {
|
|
562
|
-
const aggregation = Model.aggregate(pipeline);
|
|
563
|
-
if (options.session) {
|
|
564
|
-
aggregation.session(options.session);
|
|
565
|
-
}
|
|
566
|
-
return aggregation.exec();
|
|
567
|
-
}
|
|
568
|
-
async function aggregatePaginate(Model, pipeline, options = {}) {
|
|
569
|
-
const page = parseInt(String(options.page || 1), 10);
|
|
570
|
-
const limit = parseInt(String(options.limit || 10), 10);
|
|
571
|
-
const skip = (page - 1) * limit;
|
|
572
|
-
const SAFE_LIMIT = 1e3;
|
|
573
|
-
if (limit > SAFE_LIMIT) {
|
|
574
|
-
warn(
|
|
575
|
-
`[mongokit] Large aggregation limit (${limit}). $facet results must be <16MB. Consider using Repository.aggregatePaginate() for safer handling of large datasets.`
|
|
576
|
-
);
|
|
577
|
-
}
|
|
578
|
-
const facetPipeline = [
|
|
579
|
-
...pipeline,
|
|
580
|
-
{
|
|
581
|
-
$facet: {
|
|
582
|
-
docs: [{ $skip: skip }, { $limit: limit }],
|
|
583
|
-
total: [{ $count: "count" }]
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
];
|
|
587
|
-
const aggregation = Model.aggregate(facetPipeline);
|
|
588
|
-
if (options.session) {
|
|
589
|
-
aggregation.session(options.session);
|
|
590
|
-
}
|
|
591
|
-
const [result] = await aggregation.exec();
|
|
592
|
-
const docs = result.docs || [];
|
|
593
|
-
const total = result.total[0]?.count || 0;
|
|
594
|
-
const pages = Math.ceil(total / limit);
|
|
595
|
-
return {
|
|
596
|
-
docs,
|
|
597
|
-
total,
|
|
598
|
-
page,
|
|
599
|
-
limit,
|
|
600
|
-
pages,
|
|
601
|
-
hasNext: page < pages,
|
|
602
|
-
hasPrev: page > 1
|
|
603
|
-
};
|
|
604
|
-
}
|
|
605
|
-
async function groupBy(Model, field, options = {}) {
|
|
606
|
-
const pipeline = [
|
|
607
|
-
{ $group: { _id: `$${field}`, count: { $sum: 1 } } },
|
|
608
|
-
{ $sort: { count: -1 } }
|
|
609
|
-
];
|
|
610
|
-
if (options.limit) {
|
|
611
|
-
pipeline.push({ $limit: options.limit });
|
|
612
|
-
}
|
|
613
|
-
return aggregate(Model, pipeline, options);
|
|
614
|
-
}
|
|
615
|
-
async function countBy(Model, field, query = {}, options = {}) {
|
|
616
|
-
const pipeline = [];
|
|
617
|
-
if (Object.keys(query).length > 0) {
|
|
618
|
-
pipeline.push({ $match: query });
|
|
619
|
-
}
|
|
620
|
-
pipeline.push(
|
|
621
|
-
{ $group: { _id: `$${field}`, count: { $sum: 1 } } },
|
|
622
|
-
{ $sort: { count: -1 } }
|
|
623
|
-
);
|
|
624
|
-
return aggregate(Model, pipeline, options);
|
|
625
|
-
}
|
|
626
|
-
async function lookup(Model, lookupOptions) {
|
|
627
|
-
const { from, localField, foreignField, as, pipeline = [], let: letVars, query = {}, options = {} } = lookupOptions;
|
|
628
|
-
const aggPipeline = [];
|
|
629
|
-
if (Object.keys(query).length > 0) {
|
|
630
|
-
aggPipeline.push({ $match: query });
|
|
631
|
-
}
|
|
632
|
-
const usePipelineForm = pipeline.length > 0 || letVars;
|
|
633
|
-
if (usePipelineForm) {
|
|
634
|
-
if (pipeline.length === 0 && localField && foreignField) {
|
|
635
|
-
const autoPipeline = [
|
|
636
|
-
{
|
|
637
|
-
$match: {
|
|
638
|
-
$expr: {
|
|
639
|
-
$eq: [`$${foreignField}`, `$$${localField}`]
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
}
|
|
643
|
-
];
|
|
644
|
-
aggPipeline.push({
|
|
645
|
-
$lookup: {
|
|
646
|
-
from,
|
|
647
|
-
let: { [localField]: `$${localField}`, ...letVars || {} },
|
|
648
|
-
pipeline: autoPipeline,
|
|
649
|
-
as
|
|
650
|
-
}
|
|
651
|
-
});
|
|
652
|
-
} else {
|
|
653
|
-
const safePipeline = lookupOptions.sanitize !== false ? LookupBuilder.sanitizePipeline(pipeline) : pipeline;
|
|
654
|
-
aggPipeline.push({
|
|
655
|
-
$lookup: {
|
|
656
|
-
from,
|
|
657
|
-
...letVars && { let: letVars },
|
|
658
|
-
pipeline: safePipeline,
|
|
659
|
-
as
|
|
660
|
-
}
|
|
661
|
-
});
|
|
662
|
-
}
|
|
663
|
-
} else {
|
|
664
|
-
aggPipeline.push({
|
|
665
|
-
$lookup: {
|
|
666
|
-
from,
|
|
667
|
-
localField,
|
|
668
|
-
foreignField,
|
|
669
|
-
as
|
|
670
|
-
}
|
|
671
|
-
});
|
|
672
|
-
}
|
|
673
|
-
return aggregate(Model, aggPipeline, options);
|
|
674
|
-
}
|
|
675
|
-
async function unwind(Model, field, options = {}) {
|
|
676
|
-
const pipeline = [
|
|
677
|
-
{
|
|
678
|
-
$unwind: {
|
|
679
|
-
path: `$${field}`,
|
|
680
|
-
preserveNullAndEmptyArrays: options.preserveEmpty !== false
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
];
|
|
684
|
-
return aggregate(Model, pipeline, { session: options.session });
|
|
685
|
-
}
|
|
686
|
-
async function facet(Model, facets, options = {}) {
|
|
687
|
-
const pipeline = [{ $facet: facets }];
|
|
688
|
-
return aggregate(Model, pipeline, options);
|
|
689
|
-
}
|
|
690
|
-
async function distinct(Model, field, query = {}, options = {}) {
|
|
691
|
-
return Model.distinct(field, query).session(options.session ?? null);
|
|
692
|
-
}
|
|
693
|
-
async function sum(Model, field, query = {}, options = {}) {
|
|
694
|
-
const pipeline = [];
|
|
695
|
-
if (Object.keys(query).length > 0) {
|
|
696
|
-
pipeline.push({ $match: query });
|
|
697
|
-
}
|
|
698
|
-
pipeline.push({
|
|
699
|
-
$group: {
|
|
700
|
-
_id: null,
|
|
701
|
-
total: { $sum: `$${field}` }
|
|
702
|
-
}
|
|
703
|
-
});
|
|
704
|
-
const result = await aggregate(Model, pipeline, options);
|
|
705
|
-
return result[0]?.total || 0;
|
|
706
|
-
}
|
|
707
|
-
async function average(Model, field, query = {}, options = {}) {
|
|
708
|
-
const pipeline = [];
|
|
709
|
-
if (Object.keys(query).length > 0) {
|
|
710
|
-
pipeline.push({ $match: query });
|
|
711
|
-
}
|
|
712
|
-
pipeline.push({
|
|
713
|
-
$group: {
|
|
714
|
-
_id: null,
|
|
715
|
-
average: { $avg: `$${field}` }
|
|
716
|
-
}
|
|
717
|
-
});
|
|
718
|
-
const result = await aggregate(Model, pipeline, options);
|
|
719
|
-
return result[0]?.average || 0;
|
|
720
|
-
}
|
|
721
|
-
async function minMax(Model, field, query = {}, options = {}) {
|
|
722
|
-
const pipeline = [];
|
|
723
|
-
if (Object.keys(query).length > 0) {
|
|
724
|
-
pipeline.push({ $match: query });
|
|
725
|
-
}
|
|
726
|
-
pipeline.push({
|
|
727
|
-
$group: {
|
|
728
|
-
_id: null,
|
|
729
|
-
min: { $min: `$${field}` },
|
|
730
|
-
max: { $max: `$${field}` }
|
|
731
|
-
}
|
|
732
|
-
});
|
|
733
|
-
const result = await aggregate(Model, pipeline, options);
|
|
734
|
-
return result[0] || { min: null, max: null };
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
export { LookupBuilder, actions_exports, aggregate, aggregate_exports, count, deleteById, delete_exports, distinct, exists, getById, getByQuery, getOrCreate, read_exports, update, update_exports };
|