@zenith-open/zenithcms-db-mongodb 0.1.0 → 1.0.0-beta.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,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() {
@@ -203,8 +184,9 @@ export class MongooseAdapter {
203
184
  email: { type: String, required: true },
204
185
  collectionName: { type: String, required: true },
205
186
  documentId: { type: String, required: true },
187
+ siteId: { type: String, required: false },
206
188
  lastActive: { type: Number, required: true },
207
- }, { timestamps: false, strict: true });
189
+ }, { timestamps: false, strict: true, collection: 'z_presence' });
208
190
  mongoose.model('z_presence', schema);
209
191
  }
210
192
  if (!mongoose.models['Lock']) {
@@ -266,12 +248,14 @@ export class MongooseAdapter {
266
248
  resolvedCollection = 'AuditLog';
267
249
  if (collection === 'versions' || collection === 'z_versions')
268
250
  resolvedCollection = 'Version';
251
+ if (collection === 'comments')
252
+ resolvedCollection = 'Comment';
269
253
  const model = this.models[resolvedCollection] || mongoose.models[resolvedCollection];
270
254
  if (!model)
271
255
  throw new Error(`Collection "${collection}" not registered`);
272
256
  return model;
273
257
  }
274
- _getCacheKey(collection, query, options) {
258
+ _getCacheKey(method, collection, query, options) {
275
259
  const sortObject = (obj) => {
276
260
  if (obj === null || typeof obj !== 'object')
277
261
  return obj;
@@ -284,103 +268,132 @@ export class MongooseAdapter {
284
268
  };
285
269
  const siteId = options?.siteId || options?.tenantId || globalThis.zenithAls?.getStore()?.siteId;
286
270
  const enrichedQuery = siteId ? { ...query, siteId } : query;
287
- return `${collection}:${JSON.stringify(sortObject(enrichedQuery))}:${JSON.stringify(sortObject(options))}`;
271
+ return `${method}:${collection}:${JSON.stringify(sortObject(enrichedQuery))}:${JSON.stringify(sortObject(options))}`;
288
272
  }
289
273
  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')) {
274
+ return this._withCircuitBreaker(async () => {
275
+ const cacheKey = this._getCacheKey('find', collection, query, options);
276
+ const cached = await this.cache.get(cacheKey);
277
+ if (cached)
278
+ return cached;
279
+ const globalAot = globalThis.zenithAotBridge;
280
+ if (globalAot && globalAot.hasQuery(collection, 'find')) {
281
+ const model = this.getModel(collection);
282
+ const docs = await globalAot.executeQuery(collection, 'find', mongoose.connection.db, model, this._normalizeQuery(query, options), options);
283
+ await this.cache.set(cacheKey, docs, collection);
284
+ return docs;
285
+ }
296
286
  const model = this.getModel(collection);
297
- const docs = await globalAot.executeQuery(collection, 'find', mongoose.connection.db, model, this._normalizeQuery(query, options), options);
287
+ const normalizedQuery = this._normalizeQuery(query, options);
288
+ console.log(`[DEBUG] MongooseAdapter.find(${collection}):`, JSON.stringify(normalizedQuery));
289
+ const q = model.find(normalizedQuery).maxTimeMS(30000);
290
+ if (options.select)
291
+ q.select(options.select);
292
+ if (options.populate) {
293
+ const populateArr = Array.isArray(options.populate) ? options.populate : [options.populate];
294
+ populateArr.forEach((p) => q.populate(p));
295
+ }
296
+ const requestedLimit = options.limit ?? DEFAULT_QUERY_LIMIT;
297
+ const limit = Math.min(requestedLimit, MAX_QUERY_LIMIT);
298
+ const docs = (await q
299
+ .sort(options.sort || { createdAt: -1 })
300
+ .skip(options.skip || 0)
301
+ .limit(limit)
302
+ .session(options.session)
303
+ .lean()
304
+ .exec());
298
305
  await this.cache.set(cacheKey, docs, collection);
299
306
  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;
307
+ });
321
308
  }
322
309
  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;
310
+ return this._withCircuitBreaker(async () => {
311
+ const cacheKey = this._getCacheKey('findOne', collection, query, options);
312
+ const cached = await this.cache.get(cacheKey);
313
+ if (cached)
314
+ return cached;
315
+ const model = this.getModel(collection);
316
+ const q = model.findOne(this._normalizeQuery(query, options)).maxTimeMS(30000);
317
+ if (options.select)
318
+ q.select(options.select);
319
+ if (options.populate) {
320
+ const populateArr = Array.isArray(options.populate) ? options.populate : [options.populate];
321
+ populateArr.forEach((p) => q.populate(p));
322
+ }
323
+ const doc = (await q
324
+ .session(options.session)
325
+ .lean()
326
+ .exec());
327
+ if (doc)
328
+ await this.cache.set(cacheKey, doc, collection);
329
+ return doc;
330
+ });
331
+ }
332
+ async findMany(collection, ids, options = {}) {
333
+ return this._withCircuitBreaker(async () => {
334
+ if (!ids || ids.length === 0)
335
+ return [];
336
+ const model = this.getModel(collection);
337
+ const siteId = options?.siteId || options?.tenantId || globalThis.zenithAls?.getStore()?.siteId;
338
+ const query = { _id: { $in: ids } };
339
+ if (siteId && siteId !== 'global') {
340
+ query.siteId = siteId;
341
+ }
342
+ const docs = await model
343
+ .find(query)
344
+ .session(options.session)
345
+ .maxTimeMS(30000)
346
+ .lean()
347
+ .exec();
348
+ return docs;
349
+ });
342
350
  }
343
351
  async _invalidateCache(collection) {
344
352
  await this.cache.invalidate(collection);
345
353
  }
346
354
  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')) {
355
+ return this._withCircuitBreaker(async () => {
356
+ // Inject tenant scoping into created documents
357
+ const siteId = options?.siteId || options?.tenantId || globalThis.zenithAls?.getStore()?.siteId;
358
+ const enrichedData = siteId && !data.siteId
359
+ ? { ...data, siteId }
360
+ : data;
361
+ const globalAot = globalThis.zenithAotBridge;
362
+ if (globalAot && globalAot.hasQuery(collection, 'create')) {
363
+ const model = this.getModel(collection);
364
+ const doc = await globalAot.executeQuery(collection, 'create', mongoose.connection.db, model, enrichedData, options);
365
+ await this._invalidateCache(collection);
366
+ return doc;
367
+ }
354
368
  const model = this.getModel(collection);
355
- const doc = await globalAot.executeQuery(collection, 'create', mongoose.connection.db, model, enrichedData, options);
369
+ const [doc] = await model.create([enrichedData], { session: options.session });
356
370
  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();
371
+ return doc.toObject();
372
+ });
363
373
  }
364
374
  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;
373
- }
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
- return doc;
375
+ return this._withCircuitBreaker(async () => {
376
+ const model = this.getModel(collection);
377
+ const siteId = options?.siteId || options?.tenantId || globalThis.zenithAls?.getStore()?.siteId;
378
+ const filter = { _id: id };
379
+ if (siteId)
380
+ filter.siteId = siteId;
381
+ // Atomic optimistic locking: include expected _version in the filter
382
+ if (options.expectedVersion !== undefined) {
383
+ filter._version = options.expectedVersion;
384
+ }
385
+ const doc = await model
386
+ .findOneAndUpdate(filter, { $set: data }, {
387
+ new: true,
388
+ session: options.session,
389
+ runValidators: true,
390
+ })
391
+ .maxTimeMS(30000)
392
+ .lean()
393
+ .exec();
394
+ await this._invalidateCache(collection);
395
+ return doc;
396
+ });
384
397
  }
385
398
  _normalizeQuery(query, options) {
386
399
  const normalized = { ...query };
@@ -390,62 +403,80 @@ export class MongooseAdapter {
390
403
  }
391
404
  // Inject tenant scoping from options to prevent cross-tenant data access
392
405
  const siteId = options?.siteId || options?.tenantId || globalThis.zenithAls?.getStore()?.siteId;
393
- if (siteId && !normalized.siteId) {
406
+ if (siteId && siteId !== 'global' && !normalized.siteId) {
394
407
  normalized.siteId = siteId;
395
408
  }
396
409
  return normalized;
397
410
  }
398
411
  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;
412
+ return this._withCircuitBreaker(async () => {
413
+ const model = this.getModel(collection);
414
+ const normalized = this._normalizeQuery(query, options);
415
+ const returnDoc = options?.returnDocument === 'after' ? true : false;
416
+ const doc = await model
417
+ .findOneAndUpdate(normalized, { $set: update }, {
418
+ new: returnDoc,
419
+ session: options?.session,
420
+ runValidators: true,
421
+ })
422
+ .maxTimeMS(30000)
423
+ .lean()
424
+ .exec();
425
+ return doc;
426
+ });
411
427
  }
412
428
  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,
429
+ return this._withCircuitBreaker(async () => {
430
+ const model = this.getModel(collection);
431
+ const result = await model.updateMany(this._normalizeQuery(query, options), { $set: data }, {
432
+ session: options.session,
433
+ });
434
+ await this._invalidateCache(collection);
435
+ return result.modifiedCount;
416
436
  });
417
- await this._invalidateCache(collection);
418
- return result.modifiedCount;
419
437
  }
420
438
  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;
439
+ return this._withCircuitBreaker(async () => {
440
+ const model = this.getModel(collection);
441
+ const siteId = options?.siteId || options?.tenantId || globalThis.zenithAls?.getStore()?.siteId;
442
+ const objectId = mongoose.Types.ObjectId.isValid(id) && typeof id === 'string' && id.length === 24
443
+ ? new mongoose.Types.ObjectId(id)
444
+ : id;
445
+ const filter = { _id: objectId };
446
+ if (siteId)
447
+ filter.siteId = siteId;
448
+ console.log(`[DEBUG DELETE] collection=${collection} resolvedCollection=${model.modelName} filter=`, filter);
449
+ const result = await model.findOneAndDelete(filter, { session: options.session }).maxTimeMS(30000);
450
+ console.log(`[DEBUG DELETE] result=`, !!result);
451
+ await this._invalidateCache(collection);
452
+ return !!result;
453
+ });
429
454
  }
430
455
  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;
456
+ return this._withCircuitBreaker(async () => {
457
+ const model = this.getModel(collection);
458
+ const result = await model.deleteMany(this._normalizeQuery(query, options), { session: options.session });
459
+ await this._invalidateCache(collection);
460
+ return result.deletedCount;
461
+ });
435
462
  }
436
463
  async count(collection, query, options) {
437
- const model = this.getModel(collection);
438
- return model.countDocuments(this._normalizeQuery(query, options));
464
+ return this._withCircuitBreaker(async () => {
465
+ const model = this.getModel(collection);
466
+ return model.countDocuments(this._normalizeQuery(query, options)).maxTimeMS(30000);
467
+ });
439
468
  }
440
469
  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();
470
+ return this._withCircuitBreaker(async () => {
471
+ const model = this.getModel(collection);
472
+ const enrichedPipeline = [...pipeline];
473
+ // Inject tenant scoping prepend $match stage to prevent cross-tenant data leaks
474
+ const siteId = options?.siteId || options?.tenantId || globalThis.zenithAls?.getStore()?.siteId;
475
+ if (siteId) {
476
+ enrichedPipeline.unshift({ $match: { siteId } });
477
+ }
478
+ return model.aggregate(enrichedPipeline).option({ maxTimeMS: 30000 }).exec();
479
+ });
449
480
  }
450
481
  async transaction(fn) {
451
482
  try {
@@ -538,20 +569,23 @@ export class MongooseAdapter {
538
569
  }));
539
570
  }
540
571
  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();
572
+ return this._withCircuitBreaker(async () => {
573
+ const model = this.getModel(collection);
574
+ const escaped = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
575
+ const regex = { $regex: escaped, $options: 'i' };
576
+ const orQuery = fields.map((f) => ({ [f]: regex }));
577
+ const findQuery = { $or: orQuery };
578
+ const siteId = options?.siteId || options?.tenantId || globalThis.zenithAls?.getStore()?.siteId;
579
+ if (siteId) {
580
+ findQuery.siteId = siteId;
581
+ }
582
+ return model
583
+ .find(findQuery)
584
+ .limit(Math.min(limit, 50))
585
+ .maxTimeMS(30000)
586
+ .lean()
587
+ .exec();
588
+ });
555
589
  }
556
590
  }
557
591
  //# sourceMappingURL=MongooseAdapter.js.map