@classytic/mongokit 3.1.6 → 3.2.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 +89 -4
- package/dist/actions/index.d.ts +2 -2
- package/dist/actions/index.js +5 -3
- package/dist/ai/index.d.ts +175 -0
- package/dist/ai/index.js +206 -0
- package/dist/chunks/{chunk-M2XHQGZB.js → chunk-44KXLGPO.js} +28 -1
- package/dist/chunks/{chunk-SAKSLT47.js → chunk-5G42WJHC.js} +280 -13
- package/dist/chunks/{chunk-CSLJ2PL2.js → chunk-B64F5ZWE.js} +145 -11
- package/dist/chunks/{chunk-CF6FLC2G.js → chunk-GZBKEPVE.js} +2 -2
- package/dist/chunks/chunk-JWUAVZ3L.js +8 -0
- package/dist/chunks/{chunk-IT7DCOKR.js → chunk-UE2IEXZJ.js} +15 -8
- package/dist/chunks/chunk-URLJFIR7.js +22 -0
- package/dist/chunks/chunk-WSFCRVEQ.js +7 -0
- package/dist/{index-BXSSv1pW.d.ts → index-BDn5fSTE.d.ts} +13 -1
- package/dist/index.d.ts +146 -41
- package/dist/index.js +294 -296
- package/dist/{mongooseToJsonSchema-Cc5AwuDu.d.ts → mongooseToJsonSchema-CaRF_bCN.d.ts} +33 -2
- package/dist/pagination/PaginationEngine.d.ts +1 -1
- package/dist/pagination/PaginationEngine.js +3 -2
- package/dist/plugins/index.d.ts +125 -2
- package/dist/plugins/index.js +5 -3
- package/dist/{types-B5Uv6Ak7.d.ts → types-Jni1KgkP.d.ts} +18 -11
- package/dist/utils/index.d.ts +2 -2
- package/dist/utils/index.js +4 -2
- package/package.json +10 -2
- package/dist/chunks/chunk-VJXDGP3C.js +0 -14
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { create_exports } from './chunk-
|
|
2
|
-
import {
|
|
1
|
+
import { create_exports } from './chunk-GZBKEPVE.js';
|
|
2
|
+
import { warn } from './chunk-URLJFIR7.js';
|
|
3
|
+
import { createError } from './chunk-JWUAVZ3L.js';
|
|
4
|
+
import { __export } from './chunk-WSFCRVEQ.js';
|
|
3
5
|
|
|
4
6
|
// src/actions/index.ts
|
|
5
7
|
var actions_exports = {};
|
|
@@ -76,7 +78,7 @@ async function getOrCreate(Model, query, createData, options = {}) {
|
|
|
76
78
|
{ $setOnInsert: createData },
|
|
77
79
|
{
|
|
78
80
|
upsert: true,
|
|
79
|
-
|
|
81
|
+
returnDocument: "after",
|
|
80
82
|
runValidators: true,
|
|
81
83
|
session: options.session,
|
|
82
84
|
...options.updatePipeline !== void 0 ? { updatePipeline: options.updatePipeline } : {}
|
|
@@ -122,8 +124,9 @@ function parsePopulate2(populate) {
|
|
|
122
124
|
}
|
|
123
125
|
async function update(Model, id, data, options = {}) {
|
|
124
126
|
assertUpdatePipelineAllowed(data, options.updatePipeline);
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
+
const query = { _id: id, ...options.query };
|
|
128
|
+
const document = await Model.findOneAndUpdate(query, data, {
|
|
129
|
+
returnDocument: "after",
|
|
127
130
|
runValidators: true,
|
|
128
131
|
session: options.session,
|
|
129
132
|
...options.updatePipeline !== void 0 ? { updatePipeline: options.updatePipeline } : {}
|
|
@@ -137,7 +140,7 @@ async function updateWithConstraints(Model, id, data, constraints = {}, options
|
|
|
137
140
|
assertUpdatePipelineAllowed(data, options.updatePipeline);
|
|
138
141
|
const query = { _id: id, ...constraints };
|
|
139
142
|
const document = await Model.findOneAndUpdate(query, data, {
|
|
140
|
-
|
|
143
|
+
returnDocument: "after",
|
|
141
144
|
runValidators: true,
|
|
142
145
|
session: options.session,
|
|
143
146
|
...options.updatePipeline !== void 0 ? { updatePipeline: options.updatePipeline } : {}
|
|
@@ -195,7 +198,7 @@ async function updateMany(Model, query, data, options = {}) {
|
|
|
195
198
|
async function updateByQuery(Model, query, data, options = {}) {
|
|
196
199
|
assertUpdatePipelineAllowed(data, options.updatePipeline);
|
|
197
200
|
const document = await Model.findOneAndUpdate(query, data, {
|
|
198
|
-
|
|
201
|
+
returnDocument: "after",
|
|
199
202
|
runValidators: true,
|
|
200
203
|
session: options.session,
|
|
201
204
|
...options.updatePipeline !== void 0 ? { updatePipeline: options.updatePipeline } : {}
|
|
@@ -225,7 +228,8 @@ __export(delete_exports, {
|
|
|
225
228
|
softDelete: () => softDelete
|
|
226
229
|
});
|
|
227
230
|
async function deleteById(Model, id, options = {}) {
|
|
228
|
-
const
|
|
231
|
+
const query = { _id: id, ...options.query };
|
|
232
|
+
const document = await Model.findOneAndDelete(query).session(options.session ?? null);
|
|
229
233
|
if (!document) {
|
|
230
234
|
throw createError(404, "Document not found");
|
|
231
235
|
}
|
|
@@ -254,7 +258,7 @@ async function softDelete(Model, id, options = {}) {
|
|
|
254
258
|
deletedAt: /* @__PURE__ */ new Date(),
|
|
255
259
|
deletedBy: options.userId
|
|
256
260
|
},
|
|
257
|
-
{
|
|
261
|
+
{ returnDocument: "after", session: options.session }
|
|
258
262
|
);
|
|
259
263
|
if (!document) {
|
|
260
264
|
throw createError(404, "Document not found");
|
|
@@ -269,7 +273,7 @@ async function restore(Model, id, options = {}) {
|
|
|
269
273
|
deletedAt: null,
|
|
270
274
|
deletedBy: null
|
|
271
275
|
},
|
|
272
|
-
{
|
|
276
|
+
{ returnDocument: "after", session: options.session }
|
|
273
277
|
);
|
|
274
278
|
if (!document) {
|
|
275
279
|
throw createError(404, "Document not found");
|
|
@@ -292,6 +296,268 @@ __export(aggregate_exports, {
|
|
|
292
296
|
sum: () => sum,
|
|
293
297
|
unwind: () => unwind
|
|
294
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
|
|
295
561
|
async function aggregate(Model, pipeline, options = {}) {
|
|
296
562
|
const aggregation = Model.aggregate(pipeline);
|
|
297
563
|
if (options.session) {
|
|
@@ -305,7 +571,7 @@ async function aggregatePaginate(Model, pipeline, options = {}) {
|
|
|
305
571
|
const skip = (page - 1) * limit;
|
|
306
572
|
const SAFE_LIMIT = 1e3;
|
|
307
573
|
if (limit > SAFE_LIMIT) {
|
|
308
|
-
|
|
574
|
+
warn(
|
|
309
575
|
`[mongokit] Large aggregation limit (${limit}). $facet results must be <16MB. Consider using Repository.aggregatePaginate() for safer handling of large datasets.`
|
|
310
576
|
);
|
|
311
577
|
}
|
|
@@ -384,11 +650,12 @@ async function lookup(Model, lookupOptions) {
|
|
|
384
650
|
}
|
|
385
651
|
});
|
|
386
652
|
} else {
|
|
653
|
+
const safePipeline = lookupOptions.sanitize !== false ? LookupBuilder.sanitizePipeline(pipeline) : pipeline;
|
|
387
654
|
aggPipeline.push({
|
|
388
655
|
$lookup: {
|
|
389
656
|
from,
|
|
390
657
|
...letVars && { let: letVars },
|
|
391
|
-
pipeline,
|
|
658
|
+
pipeline: safePipeline,
|
|
392
659
|
as
|
|
393
660
|
}
|
|
394
661
|
});
|
|
@@ -467,4 +734,4 @@ async function minMax(Model, field, query = {}, options = {}) {
|
|
|
467
734
|
return result[0] || { min: null, max: null };
|
|
468
735
|
}
|
|
469
736
|
|
|
470
|
-
export { actions_exports, aggregate, aggregate_exports, count, deleteById, delete_exports, distinct, exists, getById, getByQuery, getOrCreate, read_exports, update, update_exports };
|
|
737
|
+
export { LookupBuilder, actions_exports, aggregate, aggregate_exports, count, deleteById, delete_exports, distinct, exists, getById, getByQuery, getOrCreate, read_exports, update, update_exports };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { upsert } from './chunk-
|
|
1
|
+
import { upsert } from './chunk-GZBKEPVE.js';
|
|
2
2
|
import { versionKey, byIdKey, byQueryKey, listQueryKey, modelPattern, getFieldsForUser } from './chunk-2ZN65ZOP.js';
|
|
3
|
-
import {
|
|
3
|
+
import { warn, debug } from './chunk-URLJFIR7.js';
|
|
4
|
+
import { createError } from './chunk-JWUAVZ3L.js';
|
|
4
5
|
import mongoose from 'mongoose';
|
|
5
6
|
|
|
6
7
|
// src/plugins/field-filter.plugin.ts
|
|
@@ -137,7 +138,7 @@ function softDeletePlugin(options = {}) {
|
|
|
137
138
|
}
|
|
138
139
|
).catch((err) => {
|
|
139
140
|
if (!err.message.includes("already exists")) {
|
|
140
|
-
|
|
141
|
+
warn(`[softDeletePlugin] Failed to create TTL index: ${err.message}`);
|
|
141
142
|
}
|
|
142
143
|
});
|
|
143
144
|
}
|
|
@@ -194,7 +195,7 @@ function softDeletePlugin(options = {}) {
|
|
|
194
195
|
[deletedByField]: null
|
|
195
196
|
};
|
|
196
197
|
const result = await this.Model.findByIdAndUpdate(id, { $set: updateData }, {
|
|
197
|
-
|
|
198
|
+
returnDocument: "after",
|
|
198
199
|
session: restoreOptions.session
|
|
199
200
|
});
|
|
200
201
|
if (!result) {
|
|
@@ -639,7 +640,7 @@ function subdocumentPlugin() {
|
|
|
639
640
|
const query = { _id: parentId, [`${arrayPath}._id`]: subId };
|
|
640
641
|
const update = { $set: { [`${arrayPath}.$`]: { ...updateData, _id: subId } } };
|
|
641
642
|
const result = await Model.findOneAndUpdate(query, update, {
|
|
642
|
-
|
|
643
|
+
returnDocument: "after",
|
|
643
644
|
runValidators: true,
|
|
644
645
|
session: options.session
|
|
645
646
|
}).exec();
|
|
@@ -675,7 +676,7 @@ function cachePlugin(options) {
|
|
|
675
676
|
let collectionVersion = 0;
|
|
676
677
|
const log = (msg, data) => {
|
|
677
678
|
if (config.debug) {
|
|
678
|
-
|
|
679
|
+
debug(`[mongokit:cache] ${msg}`, data ?? "");
|
|
679
680
|
}
|
|
680
681
|
};
|
|
681
682
|
return {
|
|
@@ -779,7 +780,8 @@ function cachePlugin(options) {
|
|
|
779
780
|
limit,
|
|
780
781
|
after: context.after,
|
|
781
782
|
select: context.select,
|
|
782
|
-
populate: context.populate
|
|
783
|
+
populate: context.populate,
|
|
784
|
+
search: context.search
|
|
783
785
|
};
|
|
784
786
|
const key = listQueryKey(config.prefix, model, collectionVersion, params);
|
|
785
787
|
try {
|
|
@@ -846,7 +848,8 @@ function cachePlugin(options) {
|
|
|
846
848
|
limit,
|
|
847
849
|
after: context.after,
|
|
848
850
|
select: context.select,
|
|
849
|
-
populate: context.populate
|
|
851
|
+
populate: context.populate,
|
|
852
|
+
search: context.search
|
|
850
853
|
};
|
|
851
854
|
const key = listQueryKey(config.prefix, model, collectionVersion, params);
|
|
852
855
|
const ttl = context.cacheTtl ?? config.queryTtl;
|
|
@@ -988,7 +991,15 @@ function cascadePlugin(options) {
|
|
|
988
991
|
}
|
|
989
992
|
};
|
|
990
993
|
if (parallel) {
|
|
991
|
-
await Promise.
|
|
994
|
+
const results = await Promise.allSettled(relations.map(cascadeDelete));
|
|
995
|
+
const failures = results.filter((r) => r.status === "rejected");
|
|
996
|
+
if (failures.length) {
|
|
997
|
+
const err = failures[0].reason;
|
|
998
|
+
if (failures.length > 1) {
|
|
999
|
+
err.message = `${failures.length} cascade deletes failed. First: ${err.message}`;
|
|
1000
|
+
}
|
|
1001
|
+
throw err;
|
|
1002
|
+
}
|
|
992
1003
|
} else {
|
|
993
1004
|
for (const relation of relations) {
|
|
994
1005
|
await cascadeDelete(relation);
|
|
@@ -1077,7 +1088,15 @@ function cascadePlugin(options) {
|
|
|
1077
1088
|
}
|
|
1078
1089
|
};
|
|
1079
1090
|
if (parallel) {
|
|
1080
|
-
await Promise.
|
|
1091
|
+
const results = await Promise.allSettled(relations.map(cascadeDeleteMany));
|
|
1092
|
+
const failures = results.filter((r) => r.status === "rejected");
|
|
1093
|
+
if (failures.length) {
|
|
1094
|
+
const err = failures[0].reason;
|
|
1095
|
+
if (failures.length > 1) {
|
|
1096
|
+
err.message = `${failures.length} cascade deletes failed. First: ${err.message}`;
|
|
1097
|
+
}
|
|
1098
|
+
throw err;
|
|
1099
|
+
}
|
|
1081
1100
|
} else {
|
|
1082
1101
|
for (const relation of relations) {
|
|
1083
1102
|
await cascadeDeleteMany(relation);
|
|
@@ -1089,4 +1108,119 @@ function cascadePlugin(options) {
|
|
|
1089
1108
|
};
|
|
1090
1109
|
}
|
|
1091
1110
|
|
|
1092
|
-
|
|
1111
|
+
// src/plugins/multi-tenant.plugin.ts
|
|
1112
|
+
function multiTenantPlugin(options = {}) {
|
|
1113
|
+
const {
|
|
1114
|
+
tenantField = "organizationId",
|
|
1115
|
+
contextKey = "organizationId",
|
|
1116
|
+
required = true,
|
|
1117
|
+
skipOperations = [],
|
|
1118
|
+
skipWhen,
|
|
1119
|
+
resolveContext
|
|
1120
|
+
} = options;
|
|
1121
|
+
const readOps = ["getById", "getByQuery", "getAll", "aggregatePaginate", "lookupPopulate"];
|
|
1122
|
+
const writeOps = ["create", "createMany", "update", "delete"];
|
|
1123
|
+
const allOps = [...readOps, ...writeOps];
|
|
1124
|
+
return {
|
|
1125
|
+
name: "multi-tenant",
|
|
1126
|
+
apply(repo) {
|
|
1127
|
+
for (const op of allOps) {
|
|
1128
|
+
if (skipOperations.includes(op)) continue;
|
|
1129
|
+
repo.on(`before:${op}`, (context) => {
|
|
1130
|
+
if (skipWhen?.(context, op)) return;
|
|
1131
|
+
let tenantId = context[contextKey];
|
|
1132
|
+
if (!tenantId && resolveContext) {
|
|
1133
|
+
tenantId = resolveContext();
|
|
1134
|
+
if (tenantId) context[contextKey] = tenantId;
|
|
1135
|
+
}
|
|
1136
|
+
if (!tenantId && required) {
|
|
1137
|
+
throw new Error(
|
|
1138
|
+
`[mongokit] Multi-tenant: Missing '${contextKey}' in context for '${op}'. Pass it via options or set required: false.`
|
|
1139
|
+
);
|
|
1140
|
+
}
|
|
1141
|
+
if (!tenantId) return;
|
|
1142
|
+
if (readOps.includes(op)) {
|
|
1143
|
+
if (op === "getAll" || op === "aggregatePaginate" || op === "lookupPopulate") {
|
|
1144
|
+
context.filters = { ...context.filters, [tenantField]: tenantId };
|
|
1145
|
+
} else {
|
|
1146
|
+
context.query = { ...context.query, [tenantField]: tenantId };
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
if (op === "create" && context.data) {
|
|
1150
|
+
context.data[tenantField] = tenantId;
|
|
1151
|
+
}
|
|
1152
|
+
if (op === "createMany" && context.dataArray) {
|
|
1153
|
+
for (const doc of context.dataArray) {
|
|
1154
|
+
doc[tenantField] = tenantId;
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
if (op === "update" || op === "delete") {
|
|
1158
|
+
context.query = { ...context.query, [tenantField]: tenantId };
|
|
1159
|
+
}
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
};
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
// src/plugins/observability.plugin.ts
|
|
1167
|
+
var DEFAULT_OPS = [
|
|
1168
|
+
"create",
|
|
1169
|
+
"createMany",
|
|
1170
|
+
"update",
|
|
1171
|
+
"delete",
|
|
1172
|
+
"getById",
|
|
1173
|
+
"getByQuery",
|
|
1174
|
+
"getAll",
|
|
1175
|
+
"aggregatePaginate",
|
|
1176
|
+
"lookupPopulate"
|
|
1177
|
+
];
|
|
1178
|
+
var timers = /* @__PURE__ */ new WeakMap();
|
|
1179
|
+
function observabilityPlugin(options) {
|
|
1180
|
+
const { onMetric, slowThresholdMs } = options;
|
|
1181
|
+
const ops = options.operations ?? DEFAULT_OPS;
|
|
1182
|
+
return {
|
|
1183
|
+
name: "observability",
|
|
1184
|
+
apply(repo) {
|
|
1185
|
+
for (const op of ops) {
|
|
1186
|
+
repo.on(`before:${op}`, (context) => {
|
|
1187
|
+
timers.set(context, performance.now());
|
|
1188
|
+
});
|
|
1189
|
+
repo.on(`after:${op}`, ({ context }) => {
|
|
1190
|
+
const start = timers.get(context);
|
|
1191
|
+
if (start == null) return;
|
|
1192
|
+
const durationMs = Math.round((performance.now() - start) * 100) / 100;
|
|
1193
|
+
timers.delete(context);
|
|
1194
|
+
if (slowThresholdMs != null && durationMs < slowThresholdMs) return;
|
|
1195
|
+
onMetric({
|
|
1196
|
+
operation: op,
|
|
1197
|
+
model: context.model || repo.model,
|
|
1198
|
+
durationMs,
|
|
1199
|
+
success: true,
|
|
1200
|
+
startedAt: new Date(Date.now() - durationMs),
|
|
1201
|
+
userId: context.user?._id?.toString() || context.user?.id?.toString(),
|
|
1202
|
+
organizationId: context.organizationId?.toString()
|
|
1203
|
+
});
|
|
1204
|
+
});
|
|
1205
|
+
repo.on(`error:${op}`, ({ context, error }) => {
|
|
1206
|
+
const start = timers.get(context);
|
|
1207
|
+
if (start == null) return;
|
|
1208
|
+
const durationMs = Math.round((performance.now() - start) * 100) / 100;
|
|
1209
|
+
timers.delete(context);
|
|
1210
|
+
onMetric({
|
|
1211
|
+
operation: op,
|
|
1212
|
+
model: context.model || repo.model,
|
|
1213
|
+
durationMs,
|
|
1214
|
+
success: false,
|
|
1215
|
+
error: error.message,
|
|
1216
|
+
startedAt: new Date(Date.now() - durationMs),
|
|
1217
|
+
userId: context.user?._id?.toString() || context.user?.id?.toString(),
|
|
1218
|
+
organizationId: context.organizationId?.toString()
|
|
1219
|
+
});
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
};
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
export { aggregateHelpersPlugin, auditLogPlugin, autoInject, batchOperationsPlugin, blockIf, cachePlugin, cascadePlugin, fieldFilterPlugin, immutableField, methodRegistryPlugin, mongoOperationsPlugin, multiTenantPlugin, observabilityPlugin, requireField, softDeletePlugin, subdocumentPlugin, timestampPlugin, uniqueField, validationChainPlugin };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { __export } from './chunk-
|
|
1
|
+
import { __export } from './chunk-WSFCRVEQ.js';
|
|
2
2
|
|
|
3
3
|
// src/actions/create.ts
|
|
4
4
|
var create_exports = {};
|
|
@@ -35,7 +35,7 @@ async function upsert(Model, query, data, options = {}) {
|
|
|
35
35
|
{ $setOnInsert: data },
|
|
36
36
|
{
|
|
37
37
|
upsert: true,
|
|
38
|
-
|
|
38
|
+
returnDocument: "after",
|
|
39
39
|
runValidators: true,
|
|
40
40
|
session: options.session,
|
|
41
41
|
...options.updatePipeline !== void 0 ? { updatePipeline: options.updatePipeline } : {}
|
|
@@ -3,34 +3,41 @@ import 'mongoose';
|
|
|
3
3
|
// src/utils/memory-cache.ts
|
|
4
4
|
function createMemoryCache(maxEntries = 1e3) {
|
|
5
5
|
const cache = /* @__PURE__ */ new Map();
|
|
6
|
-
|
|
6
|
+
let lastCleanup = Date.now();
|
|
7
|
+
const CLEANUP_INTERVAL_MS = 6e4;
|
|
8
|
+
function cleanupIfNeeded() {
|
|
7
9
|
const now = Date.now();
|
|
10
|
+
if (now - lastCleanup < CLEANUP_INTERVAL_MS) return;
|
|
11
|
+
lastCleanup = now;
|
|
8
12
|
for (const [key, entry] of cache) {
|
|
9
|
-
if (entry.expiresAt < now)
|
|
10
|
-
cache.delete(key);
|
|
11
|
-
}
|
|
13
|
+
if (entry.expiresAt < now) cache.delete(key);
|
|
12
14
|
}
|
|
13
15
|
}
|
|
14
16
|
function evictOldest() {
|
|
15
|
-
|
|
17
|
+
while (cache.size >= maxEntries) {
|
|
16
18
|
const firstKey = cache.keys().next().value;
|
|
17
19
|
if (firstKey) cache.delete(firstKey);
|
|
20
|
+
else break;
|
|
18
21
|
}
|
|
19
22
|
}
|
|
20
23
|
return {
|
|
21
24
|
async get(key) {
|
|
22
|
-
cleanup();
|
|
23
25
|
const entry = cache.get(key);
|
|
24
26
|
if (!entry) return null;
|
|
25
27
|
if (entry.expiresAt < Date.now()) {
|
|
26
28
|
cache.delete(key);
|
|
27
29
|
return null;
|
|
28
30
|
}
|
|
31
|
+
cache.delete(key);
|
|
32
|
+
cache.set(key, entry);
|
|
29
33
|
return entry.value;
|
|
30
34
|
},
|
|
31
35
|
async set(key, value, ttl) {
|
|
32
|
-
|
|
33
|
-
|
|
36
|
+
cache.delete(key);
|
|
37
|
+
if (cache.size >= maxEntries) {
|
|
38
|
+
cleanupIfNeeded();
|
|
39
|
+
evictOldest();
|
|
40
|
+
}
|
|
34
41
|
cache.set(key, {
|
|
35
42
|
value,
|
|
36
43
|
expiresAt: Date.now() + ttl * 1e3
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// src/utils/logger.ts
|
|
2
|
+
var noop = () => {
|
|
3
|
+
};
|
|
4
|
+
var current = {
|
|
5
|
+
warn: console.warn.bind(console),
|
|
6
|
+
debug: noop
|
|
7
|
+
};
|
|
8
|
+
function configureLogger(config) {
|
|
9
|
+
if (config === false) {
|
|
10
|
+
current = { warn: noop, debug: noop };
|
|
11
|
+
} else {
|
|
12
|
+
current = { ...current, ...config };
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function warn(message, ...args) {
|
|
16
|
+
current.warn(message, ...args);
|
|
17
|
+
}
|
|
18
|
+
function debug(message, ...args) {
|
|
19
|
+
current.debug(message, ...args);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export { configureLogger, debug, warn };
|