@zenith-open/zenithcms-db-mongodb 0.1.0 → 1.0.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +31 -0
- package/dist/MongooseAdapter.d.ts +8 -19
- package/dist/MongooseAdapter.js +254 -220
- package/dist/MongooseAdapter.js.map +1 -1
- package/dist/model-factory.js +3 -0
- package/dist/model-factory.js.map +1 -1
- package/package.json +26 -23
- package/eslint.config.mjs +0 -26
- package/src/MongooseAdapter.ts +0 -643
- package/src/index.ts +0 -2
- package/src/model-factory.ts +0 -179
- package/tsconfig.eslint.json +0 -8
- package/tsconfig.json +0 -11
package/dist/MongooseAdapter.js
CHANGED
|
@@ -1,72 +1,11 @@
|
|
|
1
1
|
import mongoose from 'mongoose';
|
|
2
2
|
import { getModelForCollection } from './model-factory';
|
|
3
|
-
import
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
|
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
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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
|
|
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
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
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
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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
|
-
|
|
438
|
-
|
|
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
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
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
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
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
|