@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.
@@ -1,5 +1,7 @@
1
- import { create_exports } from './chunk-CF6FLC2G.js';
2
- import { __export, createError } from './chunk-VJXDGP3C.js';
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
- new: true,
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 document = await Model.findByIdAndUpdate(id, data, {
126
- new: true,
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
- new: true,
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
- new: true,
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 document = await Model.findByIdAndDelete(id).session(options.session ?? null);
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
- { new: true, session: options.session }
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
- { new: true, session: options.session }
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
- console.warn(
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-CF6FLC2G.js';
1
+ import { upsert } from './chunk-GZBKEPVE.js';
2
2
  import { versionKey, byIdKey, byQueryKey, listQueryKey, modelPattern, getFieldsForUser } from './chunk-2ZN65ZOP.js';
3
- import { createError } from './chunk-VJXDGP3C.js';
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
- console.warn(`[softDeletePlugin] Failed to create TTL index: ${err.message}`);
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
- new: true,
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
- new: true,
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
- console.log(`[mongokit:cache] ${msg}`, data ?? "");
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.all(relations.map(cascadeDelete));
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.all(relations.map(cascadeDeleteMany));
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
- export { aggregateHelpersPlugin, auditLogPlugin, autoInject, batchOperationsPlugin, blockIf, cachePlugin, cascadePlugin, fieldFilterPlugin, immutableField, methodRegistryPlugin, mongoOperationsPlugin, requireField, softDeletePlugin, subdocumentPlugin, timestampPlugin, uniqueField, validationChainPlugin };
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-VJXDGP3C.js';
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
- new: true,
38
+ returnDocument: "after",
39
39
  runValidators: true,
40
40
  session: options.session,
41
41
  ...options.updatePipeline !== void 0 ? { updatePipeline: options.updatePipeline } : {}
@@ -0,0 +1,8 @@
1
+ // src/utils/error.ts
2
+ function createError(status, message) {
3
+ const error = new Error(message);
4
+ error.status = status;
5
+ return error;
6
+ }
7
+
8
+ export { createError };
@@ -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
- function cleanup() {
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
- if (cache.size >= maxEntries) {
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
- cleanup();
33
- evictOldest();
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 };
@@ -0,0 +1,7 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __export = (target, all) => {
3
+ for (var name in all)
4
+ __defProp(target, name, { get: all[name], enumerable: true });
5
+ };
6
+
7
+ export { __export };