@zenith-open/zenithcms-db-mongodb 0.1.0 → 1.0.0-beta.10

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,72 +1,11 @@
1
1
  import mongoose from 'mongoose';
2
2
  import { getModelForCollection } from './model-factory';
3
- import NodeCache from 'node-cache';
4
- import Redis from 'ioredis';
3
+ import { createCacheLayer } from '@zenith-open/zenithcms-db-common';
5
4
  import pino from 'pino';
6
5
  const logger = pino();
7
6
  // Hard ceiling on query result size to prevent memory exhaustion / DoS
8
7
  const MAX_QUERY_LIMIT = 500;
9
8
  const DEFAULT_QUERY_LIMIT = 100;
10
- export class LocalCacheLayer {
11
- cache;
12
- constructor() {
13
- this.cache = new NodeCache({ stdTTL: 60, checkperiod: 120 });
14
- }
15
- async get(key) {
16
- return this.cache.get(key);
17
- }
18
- async set(key, value, collection) {
19
- this.cache.set(key, value);
20
- }
21
- async invalidate(collection) {
22
- const keys = this.cache.keys();
23
- const targets = keys.filter((k) => k.startsWith(`${collection}:`));
24
- this.cache.del(targets);
25
- }
26
- }
27
- export class RedisCacheLayer {
28
- redis;
29
- constructor(redisUrl) {
30
- this.redis = new Redis(redisUrl, {
31
- maxRetriesPerRequest: 3,
32
- });
33
- logger.info('MongooseAdapter: Redis_Cache_Layer Initialized');
34
- }
35
- async get(key) {
36
- try {
37
- const data = await this.redis.get(key);
38
- return data ? JSON.parse(data) : undefined;
39
- }
40
- catch (error) {
41
- logger.warn({ error: error.message }, 'RedisCacheLayer: Get failed');
42
- return undefined;
43
- }
44
- }
45
- async set(key, value, collection) {
46
- try {
47
- const setKey = `zenith:cache:collection:${collection}`;
48
- await this.redis.setex(key, 60, JSON.stringify(value));
49
- await this.redis.sadd(setKey, key);
50
- await this.redis.expire(setKey, 120);
51
- }
52
- catch (error) {
53
- logger.warn({ error: error.message }, 'RedisCacheLayer: Set failed');
54
- }
55
- }
56
- async invalidate(collection) {
57
- try {
58
- const setKey = `zenith:cache:collection:${collection}`;
59
- const keys = await this.redis.smembers(setKey);
60
- if (keys.length > 0) {
61
- await this.redis.del(...keys);
62
- }
63
- await this.redis.del(setKey);
64
- }
65
- catch (error) {
66
- logger.warn({ error: error.message }, 'RedisCacheLayer: Invalidate failed');
67
- }
68
- }
69
- }
70
9
  /**
71
10
  * Mongoose Database Adapter — Hardened Edition
72
11
  * ──────────────────────────────────────────
@@ -78,32 +17,74 @@ export class MongooseAdapter {
78
17
  name = 'mongoose';
79
18
  models = {};
80
19
  cache;
81
- constructor(uri) {
82
- this.uri = uri;
83
- const redisUrl = process.env.REDIS_URL;
84
- if (redisUrl) {
85
- this.cache = new RedisCacheLayer(redisUrl);
20
+ consecutiveFailures = 0;
21
+ circuitBreakerCooldown = 0;
22
+ CIRCUIT_BREAKER_THRESHOLD = 10;
23
+ CIRCUIT_BREAKER_RESET_TIMEOUT_MS = 15000;
24
+ async _withCircuitBreaker(operation) {
25
+ if (this.consecutiveFailures >= this.CIRCUIT_BREAKER_THRESHOLD) {
26
+ if (Date.now() < this.circuitBreakerCooldown) {
27
+ throw new Error('Database Circuit Breaker Open: Too many consecutive failures. Rejecting request to prevent cascade overload.');
28
+ }
29
+ else {
30
+ // Half-open state
31
+ this.consecutiveFailures = this.CIRCUIT_BREAKER_THRESHOLD - 1;
32
+ }
86
33
  }
87
- else {
88
- this.cache = new LocalCacheLayer();
89
- logger.warn('MongooseAdapter: Local_Cache_Layer Initialized (Warning: Cache desync risk under horizontal scaling)');
34
+ try {
35
+ const result = await operation();
36
+ this.consecutiveFailures = 0; // reset
37
+ return result;
38
+ }
39
+ catch (err) {
40
+ this.consecutiveFailures++;
41
+ if (this.consecutiveFailures === this.CIRCUIT_BREAKER_THRESHOLD) {
42
+ this.circuitBreakerCooldown = Date.now() + this.CIRCUIT_BREAKER_RESET_TIMEOUT_MS;
43
+ logger.error(`[MongooseAdapter] Circuit Breaker TRIPPED. DB operations suspended for ${this.CIRCUIT_BREAKER_RESET_TIMEOUT_MS}ms`);
44
+ }
45
+ throw err;
90
46
  }
47
+ }
48
+ constructor(uri) {
49
+ this.uri = uri;
50
+ this.cache = createCacheLayer('MongooseAdapter');
91
51
  logger.info('MongooseAdapter: Neural_Cache_Layer Initialized');
92
52
  }
53
+ getNativeClient() {
54
+ return mongoose.connection;
55
+ }
56
+ async executeRaw(query, params) {
57
+ // Mongoose adapter doesn't natively use raw SQL, so we'll throw or return null
58
+ // depending on usage. For now, just throw an unsupported error.
59
+ throw new Error('executeRaw is not supported on MongooseAdapter');
60
+ }
93
61
  async connect() {
94
- try {
95
- const poolMax = parseInt(process.env.DB_POOL_SIZE || '10', 10);
96
- await mongoose.connect(this.uri, {
97
- serverSelectionTimeoutMS: 5000,
98
- socketTimeoutMS: 45000,
99
- maxPoolSize: poolMax,
100
- });
101
- logger.info('MongooseAdapter: Connected to MongoDB');
102
- this._initSystemModels();
103
- }
104
- catch (error) {
105
- logger.error({ error: error.message }, 'MongooseAdapter: Connection failed');
106
- throw error;
62
+ const poolMax = parseInt(process.env.DB_POOL_SIZE || '10', 10);
63
+ const maxRetries = parseInt(process.env.DB_CONNECT_MAX_RETRIES || '5', 10);
64
+ const retryDelay = parseInt(process.env.DB_CONNECT_RETRY_DELAY_MS || '3000', 10);
65
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
66
+ try {
67
+ await mongoose.connect(this.uri, {
68
+ serverSelectionTimeoutMS: parseInt(process.env.DB_SERVER_SELECTION_TIMEOUT_MS || '5000', 10),
69
+ socketTimeoutMS: parseInt(process.env.DB_SOCKET_TIMEOUT_MS || '45000', 10),
70
+ maxPoolSize: poolMax,
71
+ autoIndex: process.env.ZENITH_AUTO_MIGRATE !== 'false',
72
+ });
73
+ logger.info({ attempt }, 'MongooseAdapter: Connected to MongoDB');
74
+ this._initSystemModels();
75
+ return;
76
+ }
77
+ catch (error) {
78
+ logger.error({ attempt, error: error.message }, 'MongooseAdapter: Connection failed');
79
+ if (attempt < maxRetries) {
80
+ logger.info({ nextAttemptIn: retryDelay }, 'Retrying MongoDB connection...');
81
+ await new Promise((resolve) => setTimeout(resolve, retryDelay));
82
+ }
83
+ else {
84
+ logger.fatal('Could not connect to MongoDB after all retries. Exiting.');
85
+ process.exit(1);
86
+ }
87
+ }
107
88
  }
108
89
  }
109
90
  async disconnect() {
@@ -175,6 +156,18 @@ export class MongooseAdapter {
175
156
  }, { strict: true });
176
157
  mongoose.model('z_migrations', schema);
177
158
  }
159
+ if (!mongoose.models['z_api_keys']) {
160
+ const schema = new mongoose.Schema({
161
+ name: { type: String, required: true },
162
+ key: { type: String, required: true, index: true },
163
+ role: { type: String, required: true },
164
+ expiresAt: { type: Date },
165
+ siteId: { type: String, index: true },
166
+ revoked: { type: Boolean, default: false },
167
+ lastUsed: { type: Date },
168
+ }, { timestamps: true, strict: true });
169
+ mongoose.model('z_api_keys', schema);
170
+ }
178
171
  if (!mongoose.models['z_collections']) {
179
172
  const schema = new mongoose.Schema({
180
173
  name: { type: String, required: true },
@@ -203,8 +196,9 @@ export class MongooseAdapter {
203
196
  email: { type: String, required: true },
204
197
  collectionName: { type: String, required: true },
205
198
  documentId: { type: String, required: true },
199
+ siteId: { type: String, required: false },
206
200
  lastActive: { type: Number, required: true },
207
- }, { timestamps: false, strict: true });
201
+ }, { timestamps: false, strict: true, collection: 'z_presence' });
208
202
  mongoose.model('z_presence', schema);
209
203
  }
210
204
  if (!mongoose.models['Lock']) {
@@ -266,12 +260,14 @@ export class MongooseAdapter {
266
260
  resolvedCollection = 'AuditLog';
267
261
  if (collection === 'versions' || collection === 'z_versions')
268
262
  resolvedCollection = 'Version';
263
+ if (collection === 'comments')
264
+ resolvedCollection = 'Comment';
269
265
  const model = this.models[resolvedCollection] || mongoose.models[resolvedCollection];
270
266
  if (!model)
271
267
  throw new Error(`Collection "${collection}" not registered`);
272
268
  return model;
273
269
  }
274
- _getCacheKey(collection, query, options) {
270
+ _getCacheKey(method, collection, query, options) {
275
271
  const sortObject = (obj) => {
276
272
  if (obj === null || typeof obj !== 'object')
277
273
  return obj;
@@ -284,168 +280,241 @@ export class MongooseAdapter {
284
280
  };
285
281
  const siteId = options?.siteId || options?.tenantId || globalThis.zenithAls?.getStore()?.siteId;
286
282
  const enrichedQuery = siteId ? { ...query, siteId } : query;
287
- return `${collection}:${JSON.stringify(sortObject(enrichedQuery))}:${JSON.stringify(sortObject(options))}`;
283
+ return `${method}:${collection}:${JSON.stringify(sortObject(enrichedQuery))}:${JSON.stringify(sortObject(options))}`;
288
284
  }
289
285
  async find(collection, query, options = {}) {
290
- const cacheKey = this._getCacheKey(collection, query, options);
291
- const cached = await this.cache.get(cacheKey);
292
- if (cached)
293
- return cached;
294
- const globalAot = globalThis.zenithAotBridge;
295
- if (globalAot && globalAot.hasQuery(collection, 'find')) {
286
+ return this._withCircuitBreaker(async () => {
287
+ const cacheKey = this._getCacheKey('find', collection, query, options);
288
+ const cached = await this.cache.get(cacheKey);
289
+ if (cached)
290
+ return cached.map(doc => this._normalizeOutput(doc));
291
+ const globalAot = globalThis.zenithAotBridge;
292
+ if (globalAot && globalAot.hasQuery(collection, 'find')) {
293
+ const model = this.getModel(collection);
294
+ const docs = await globalAot.executeQuery(collection, 'find', mongoose.connection.db, model, this._normalizeQuery(query, options), options);
295
+ const mappedDocs = docs.map((doc) => this._normalizeOutput(doc));
296
+ await this.cache.set(cacheKey, mappedDocs, collection);
297
+ return mappedDocs;
298
+ }
296
299
  const model = this.getModel(collection);
297
- const docs = await globalAot.executeQuery(collection, 'find', mongoose.connection.db, model, this._normalizeQuery(query, options), options);
298
- await this.cache.set(cacheKey, docs, collection);
299
- return docs;
300
- }
301
- const model = this.getModel(collection);
302
- const normalizedQuery = this._normalizeQuery(query, options);
303
- const q = model.find(normalizedQuery);
304
- if (options.select)
305
- q.select(options.select);
306
- if (options.populate) {
307
- const populateArr = Array.isArray(options.populate) ? options.populate : [options.populate];
308
- populateArr.forEach((p) => q.populate(p));
309
- }
310
- const requestedLimit = options.limit ?? DEFAULT_QUERY_LIMIT;
311
- const limit = Math.min(requestedLimit, MAX_QUERY_LIMIT);
312
- const docs = (await q
313
- .sort(options.sort || { createdAt: -1 })
314
- .skip(options.skip || 0)
315
- .limit(limit)
316
- .session(options.session)
317
- .lean()
318
- .exec());
319
- await this.cache.set(cacheKey, docs, collection);
320
- return docs;
300
+ const normalizedQuery = this._normalizeQuery(query, options);
301
+ console.log(`[DEBUG] MongooseAdapter.find(${collection}):`, JSON.stringify(normalizedQuery));
302
+ const q = model.find(normalizedQuery).maxTimeMS(30000);
303
+ if (options.select)
304
+ q.select(options.select);
305
+ if (options.populate) {
306
+ const populateArr = Array.isArray(options.populate) ? options.populate : [options.populate];
307
+ populateArr.forEach((p) => q.populate(p));
308
+ }
309
+ const requestedLimit = options.limit ?? DEFAULT_QUERY_LIMIT;
310
+ const limit = Math.min(requestedLimit, MAX_QUERY_LIMIT);
311
+ const docs = (await q
312
+ .sort(options.sort || { createdAt: -1 })
313
+ .skip(options.skip || 0)
314
+ .limit(limit)
315
+ .session(options.session)
316
+ .lean()
317
+ .exec());
318
+ const mappedDocs = docs.map(doc => this._normalizeOutput(doc));
319
+ await this.cache.set(cacheKey, mappedDocs, collection);
320
+ return mappedDocs;
321
+ });
321
322
  }
322
323
  async findOne(collection, query, options = {}) {
323
- const cacheKey = this._getCacheKey(collection, query, options);
324
- const cached = await this.cache.get(cacheKey);
325
- if (cached)
326
- return cached;
327
- const model = this.getModel(collection);
328
- const q = model.findOne(this._normalizeQuery(query, options));
329
- if (options.select)
330
- q.select(options.select);
331
- if (options.populate) {
332
- const populateArr = Array.isArray(options.populate) ? options.populate : [options.populate];
333
- populateArr.forEach((p) => q.populate(p));
334
- }
335
- const doc = (await q
336
- .session(options.session)
337
- .lean()
338
- .exec());
339
- if (doc)
340
- await this.cache.set(cacheKey, doc, collection);
341
- return doc;
324
+ return this._withCircuitBreaker(async () => {
325
+ const cacheKey = this._getCacheKey('findOne', collection, query, options);
326
+ const cached = await this.cache.get(cacheKey);
327
+ if (cached)
328
+ return this._normalizeOutput(cached);
329
+ const model = this.getModel(collection);
330
+ const q = model.findOne(this._normalizeQuery(query, options)).maxTimeMS(30000);
331
+ if (options.select)
332
+ q.select(options.select);
333
+ if (options.populate) {
334
+ const populateArr = Array.isArray(options.populate) ? options.populate : [options.populate];
335
+ populateArr.forEach((p) => q.populate(p));
336
+ }
337
+ const doc = (await q
338
+ .session(options.session)
339
+ .lean()
340
+ .exec());
341
+ if (doc) {
342
+ const mappedDoc = this._normalizeOutput(doc);
343
+ await this.cache.set(cacheKey, mappedDoc, collection);
344
+ return mappedDoc;
345
+ }
346
+ return null;
347
+ });
348
+ }
349
+ async findMany(collection, ids, options = {}) {
350
+ return this._withCircuitBreaker(async () => {
351
+ if (!ids || ids.length === 0)
352
+ return [];
353
+ const model = this.getModel(collection);
354
+ const siteId = options?.siteId || options?.tenantId || globalThis.zenithAls?.getStore()?.siteId;
355
+ const query = { _id: { $in: ids } };
356
+ if (siteId && siteId !== 'global') {
357
+ query.siteId = siteId;
358
+ }
359
+ const docs = await model
360
+ .find(query)
361
+ .session(options.session)
362
+ .maxTimeMS(30000)
363
+ .lean()
364
+ .exec();
365
+ return docs.map(doc => this._normalizeOutput(doc));
366
+ });
342
367
  }
343
368
  async _invalidateCache(collection) {
344
369
  await this.cache.invalidate(collection);
345
370
  }
346
371
  async create(collection, data, options = {}) {
347
- // Inject tenant scoping into created documents
348
- const siteId = options?.siteId || options?.tenantId || globalThis.zenithAls?.getStore()?.siteId;
349
- const enrichedData = siteId && !data.siteId
350
- ? { ...data, siteId }
351
- : data;
352
- const globalAot = globalThis.zenithAotBridge;
353
- if (globalAot && globalAot.hasQuery(collection, 'create')) {
372
+ return this._withCircuitBreaker(async () => {
373
+ // Inject tenant scoping into created documents
374
+ const siteId = options?.siteId || options?.tenantId || globalThis.zenithAls?.getStore()?.siteId;
375
+ const enrichedData = siteId && !data.siteId
376
+ ? { ...data, siteId }
377
+ : data;
378
+ const globalAot = globalThis.zenithAotBridge;
379
+ if (globalAot && globalAot.hasQuery(collection, 'create')) {
380
+ const model = this.getModel(collection);
381
+ const doc = await globalAot.executeQuery(collection, 'create', mongoose.connection.db, model, enrichedData, options);
382
+ await this._invalidateCache(collection);
383
+ return doc;
384
+ }
354
385
  const model = this.getModel(collection);
355
- const doc = await globalAot.executeQuery(collection, 'create', mongoose.connection.db, model, enrichedData, options);
386
+ const [doc] = await model.create([enrichedData], { session: options.session });
356
387
  await this._invalidateCache(collection);
357
- return doc;
358
- }
359
- const model = this.getModel(collection);
360
- const [doc] = await model.create([enrichedData], { session: options.session });
361
- await this._invalidateCache(collection);
362
- return doc.toObject();
388
+ return this._normalizeOutput(doc.toObject());
389
+ });
363
390
  }
364
391
  async update(collection, id, data, options = {}) {
365
- const model = this.getModel(collection);
366
- const siteId = options?.siteId || options?.tenantId || globalThis.zenithAls?.getStore()?.siteId;
367
- const filter = { _id: id };
368
- if (siteId)
369
- filter.siteId = siteId;
370
- // Atomic optimistic locking: include expected _version in the filter
371
- if (options.expectedVersion !== undefined) {
372
- filter._version = options.expectedVersion;
392
+ return this._withCircuitBreaker(async () => {
393
+ const model = this.getModel(collection);
394
+ const siteId = options?.siteId || options?.tenantId || globalThis.zenithAls?.getStore()?.siteId;
395
+ const objectId = mongoose.Types.ObjectId.isValid(id) && typeof id === 'string' && id.length === 24
396
+ ? new mongoose.Types.ObjectId(id)
397
+ : id;
398
+ const filter = { _id: objectId };
399
+ if (siteId)
400
+ filter.siteId = siteId;
401
+ // Atomic optimistic locking: include expected _version in the filter
402
+ if (options.expectedVersion !== undefined) {
403
+ filter._version = options.expectedVersion;
404
+ }
405
+ const doc = await model
406
+ .findOneAndUpdate(filter, { $set: data }, {
407
+ new: true,
408
+ session: options.session,
409
+ runValidators: true,
410
+ })
411
+ .maxTimeMS(30000)
412
+ .lean()
413
+ .exec();
414
+ await this._invalidateCache(collection);
415
+ return doc ? this._normalizeOutput(doc) : null;
416
+ });
417
+ }
418
+ _normalizeOutput(doc) {
419
+ if (!doc || typeof doc !== 'object')
420
+ return doc;
421
+ if ('_id' in doc) {
422
+ doc.id = doc._id?.toString() || doc._id;
423
+ delete doc._id;
373
424
  }
374
- const doc = await model
375
- .findOneAndUpdate(filter, { $set: data }, {
376
- new: true,
377
- session: options.session,
378
- runValidators: true,
379
- })
380
- .lean()
381
- .exec();
382
- await this._invalidateCache(collection);
383
425
  return doc;
384
426
  }
385
427
  _normalizeQuery(query, options) {
386
- const normalized = { ...query };
387
- if ('id' in normalized) {
388
- normalized._id = normalized.id;
389
- delete normalized.id;
390
- }
428
+ const processQuery = (q) => {
429
+ const normalized = { ...q };
430
+ if ('id' in normalized) {
431
+ normalized._id = normalized.id;
432
+ delete normalized.id;
433
+ }
434
+ for (const key of Object.keys(normalized)) {
435
+ if (key === '$or' || key === '$and') {
436
+ normalized[key] = normalized[key].map((sub) => processQuery(sub));
437
+ }
438
+ }
439
+ return normalized;
440
+ };
441
+ const normalized = processQuery(query);
391
442
  // Inject tenant scoping from options to prevent cross-tenant data access
392
443
  const siteId = options?.siteId || options?.tenantId || globalThis.zenithAls?.getStore()?.siteId;
393
- if (siteId && !normalized.siteId) {
444
+ if (siteId && siteId !== 'global' && !normalized.siteId) {
394
445
  normalized.siteId = siteId;
395
446
  }
396
447
  return normalized;
397
448
  }
398
449
  async findOneAndUpdate(collection, query, update, options) {
399
- const model = this.getModel(collection);
400
- const normalized = this._normalizeQuery(query, options);
401
- const returnDoc = options?.returnDocument === 'after' ? true : false;
402
- const doc = await model
403
- .findOneAndUpdate(normalized, { $set: update }, {
404
- new: returnDoc,
405
- session: options?.session,
406
- runValidators: true,
407
- })
408
- .lean()
409
- .exec();
410
- return doc;
450
+ return this._withCircuitBreaker(async () => {
451
+ const model = this.getModel(collection);
452
+ const normalized = this._normalizeQuery(query, options);
453
+ const returnDoc = options?.returnDocument === 'after' ? true : false;
454
+ const doc = await model
455
+ .findOneAndUpdate(normalized, { $set: update }, {
456
+ new: returnDoc,
457
+ session: options?.session,
458
+ runValidators: true,
459
+ })
460
+ .maxTimeMS(30000)
461
+ .lean()
462
+ .exec();
463
+ return doc ? this._normalizeOutput(doc) : null;
464
+ });
411
465
  }
412
466
  async updateMany(collection, query, data, options = {}) {
413
- const model = this.getModel(collection);
414
- const result = await model.updateMany(this._normalizeQuery(query, options), { $set: data }, {
415
- session: options.session,
467
+ return this._withCircuitBreaker(async () => {
468
+ const model = this.getModel(collection);
469
+ const result = await model.updateMany(this._normalizeQuery(query, options), { $set: data }, {
470
+ session: options.session,
471
+ });
472
+ await this._invalidateCache(collection);
473
+ return result.modifiedCount;
416
474
  });
417
- await this._invalidateCache(collection);
418
- return result.modifiedCount;
419
475
  }
420
476
  async delete(collection, id, options = {}) {
421
- const model = this.getModel(collection);
422
- const siteId = options?.siteId || options?.tenantId || globalThis.zenithAls?.getStore()?.siteId;
423
- const filter = { _id: id };
424
- if (siteId)
425
- filter.siteId = siteId;
426
- const result = await model.findOneAndDelete(filter, { session: options.session });
427
- await this._invalidateCache(collection);
428
- return !!result;
477
+ return this._withCircuitBreaker(async () => {
478
+ const model = this.getModel(collection);
479
+ const siteId = options?.siteId || options?.tenantId || globalThis.zenithAls?.getStore()?.siteId;
480
+ const objectId = mongoose.Types.ObjectId.isValid(id) && typeof id === 'string' && id.length === 24
481
+ ? new mongoose.Types.ObjectId(id)
482
+ : id;
483
+ const filter = { _id: objectId };
484
+ if (siteId)
485
+ filter.siteId = siteId;
486
+ console.log(`[DEBUG DELETE] collection=${collection} resolvedCollection=${model.modelName} filter=`, filter);
487
+ const result = await model.findOneAndDelete(filter, { session: options.session }).maxTimeMS(30000);
488
+ console.log(`[DEBUG DELETE] result=`, !!result);
489
+ await this._invalidateCache(collection);
490
+ return !!result;
491
+ });
429
492
  }
430
493
  async deleteMany(collection, query, options = {}) {
431
- const model = this.getModel(collection);
432
- const result = await model.deleteMany(this._normalizeQuery(query, options), { session: options.session });
433
- await this._invalidateCache(collection);
434
- return result.deletedCount;
494
+ return this._withCircuitBreaker(async () => {
495
+ const model = this.getModel(collection);
496
+ const result = await model.deleteMany(this._normalizeQuery(query, options), { session: options.session });
497
+ await this._invalidateCache(collection);
498
+ return result.deletedCount;
499
+ });
435
500
  }
436
501
  async count(collection, query, options) {
437
- const model = this.getModel(collection);
438
- return model.countDocuments(this._normalizeQuery(query, options));
502
+ return this._withCircuitBreaker(async () => {
503
+ const model = this.getModel(collection);
504
+ return model.countDocuments(this._normalizeQuery(query, options)).maxTimeMS(30000);
505
+ });
439
506
  }
440
507
  async aggregate(collection, pipeline, options) {
441
- const model = this.getModel(collection);
442
- const enrichedPipeline = [...pipeline];
443
- // Inject tenant scoping — prepend $match stage to prevent cross-tenant data leaks
444
- const siteId = options?.siteId || options?.tenantId || globalThis.zenithAls?.getStore()?.siteId;
445
- if (siteId) {
446
- enrichedPipeline.unshift({ $match: { siteId } });
447
- }
448
- return model.aggregate(enrichedPipeline).exec();
508
+ return this._withCircuitBreaker(async () => {
509
+ const model = this.getModel(collection);
510
+ const enrichedPipeline = [...pipeline];
511
+ // Inject tenant scoping prepend $match stage to prevent cross-tenant data leaks
512
+ const siteId = options?.siteId || options?.tenantId || globalThis.zenithAls?.getStore()?.siteId;
513
+ if (siteId) {
514
+ enrichedPipeline.unshift({ $match: { siteId } });
515
+ }
516
+ return model.aggregate(enrichedPipeline).option({ maxTimeMS: 30000 }).exec();
517
+ });
449
518
  }
450
519
  async transaction(fn) {
451
520
  try {
@@ -538,20 +607,23 @@ export class MongooseAdapter {
538
607
  }));
539
608
  }
540
609
  async search(collection, query, fields, limit = 10, options) {
541
- const model = this.getModel(collection);
542
- const escaped = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
543
- const regex = { $regex: escaped, $options: 'i' };
544
- const orQuery = fields.map((f) => ({ [f]: regex }));
545
- const findQuery = { $or: orQuery };
546
- const siteId = options?.siteId || options?.tenantId || globalThis.zenithAls?.getStore()?.siteId;
547
- if (siteId) {
548
- findQuery.siteId = siteId;
549
- }
550
- return model
551
- .find(findQuery)
552
- .limit(Math.min(limit, 50))
553
- .lean()
554
- .exec();
610
+ return this._withCircuitBreaker(async () => {
611
+ const model = this.getModel(collection);
612
+ const escaped = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
613
+ const regex = { $regex: escaped, $options: 'i' };
614
+ const orQuery = fields.map((f) => ({ [f]: regex }));
615
+ const findQuery = { $or: orQuery };
616
+ const siteId = options?.siteId || options?.tenantId || globalThis.zenithAls?.getStore()?.siteId;
617
+ if (siteId) {
618
+ findQuery.siteId = siteId;
619
+ }
620
+ return model
621
+ .find(findQuery)
622
+ .limit(Math.min(limit, 50))
623
+ .maxTimeMS(30000)
624
+ .lean()
625
+ .exec();
626
+ });
555
627
  }
556
628
  }
557
629
  //# sourceMappingURL=MongooseAdapter.js.map