@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.
- package/LICENSE +21 -0
- package/README.md +31 -0
- package/dist/MongooseAdapter.d.ts +9 -19
- package/dist/MongooseAdapter.js +297 -225
- 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() {
|
|
@@ -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
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
const
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
.
|
|
316
|
-
.
|
|
317
|
-
|
|
318
|
-
|
|
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
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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
|
|
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
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
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
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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
|
-
|
|
438
|
-
|
|
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
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
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
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
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
|