replica-failover-mongodb-ts 3.0.3 → 3.0.8
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/dist/config/connectionManager.js +157 -75
- package/dist/config/metrics.js +20 -2
- package/dist/index.js +15 -0
- package/dist/nestjs/index.js +18 -0
- package/dist/nestjs/node-balancer.decorators.js +9 -0
- package/dist/nestjs/node-balancer.module.js +65 -0
- package/dist/scripts/test_nestjs_usage.js +131 -0
- package/dist/scripts/test_read_pref.js +87 -0
- package/dist/server.js +2 -1
- package/package.json +13 -2
|
@@ -20,24 +20,34 @@ const websocket_1 = require("./websocket");
|
|
|
20
20
|
const metrics_1 = require("./metrics");
|
|
21
21
|
class ConnectionManager {
|
|
22
22
|
constructor(opts) {
|
|
23
|
-
var _a;
|
|
23
|
+
var _a, _b, _c;
|
|
24
24
|
this.primaryClient = null;
|
|
25
25
|
this.primaryDb = null;
|
|
26
|
+
this.secondaryClients = [];
|
|
27
|
+
this.rrIndex = 0;
|
|
26
28
|
this.nodes = [];
|
|
27
29
|
this.replicaUri = opts.replicaUri;
|
|
28
30
|
this.nodes = (opts.nodes || []).map((u, i) => ({ uri: u, name: `node${i + 1}` }));
|
|
29
31
|
this.dbName = opts.dbName || 'node-balancer';
|
|
30
32
|
this.healthCheckIntervalMs = (_a = opts.healthCheckIntervalMs) !== null && _a !== void 0 ? _a : 5000;
|
|
31
33
|
this.webhookUrl = opts.webhookUrl;
|
|
34
|
+
this.maxPoolSize = (_b = opts.maxPoolSize) !== null && _b !== void 0 ? _b : 20;
|
|
35
|
+
this.minPoolSize = (_c = opts.minPoolSize) !== null && _c !== void 0 ? _c : 1;
|
|
32
36
|
}
|
|
33
37
|
init() {
|
|
34
38
|
return __awaiter(this, void 0, void 0, function* () {
|
|
35
39
|
logger_1.logger.info('ConnectionManager: init() starting');
|
|
36
|
-
//
|
|
40
|
+
// 1. Try Replica URI
|
|
37
41
|
if (this.replicaUri) {
|
|
38
42
|
logger_1.logger.info(`Trying replica URI: ${this.replicaUri}`);
|
|
39
43
|
try {
|
|
40
|
-
const c = new mongodb_1.MongoClient(this.replicaUri, {
|
|
44
|
+
const c = new mongodb_1.MongoClient(this.replicaUri, {
|
|
45
|
+
monitorCommands: true,
|
|
46
|
+
minPoolSize: this.minPoolSize,
|
|
47
|
+
maxPoolSize: this.maxPoolSize
|
|
48
|
+
});
|
|
49
|
+
// Attach events BEFORE connecting to capture initial pool creation (optional but good)
|
|
50
|
+
this.attachPoolMonitor(c, 'primary', 'replica-uri');
|
|
41
51
|
yield c.connect();
|
|
42
52
|
const isWritable = yield this.checkWritable(c);
|
|
43
53
|
if (isWritable) {
|
|
@@ -55,38 +65,73 @@ class ConnectionManager {
|
|
|
55
65
|
logger_1.logger.warn(`Replica URI connect failed: ${err.message}`);
|
|
56
66
|
}
|
|
57
67
|
}
|
|
58
|
-
// Fallback:
|
|
68
|
+
// 2. Fallback: Multi-node Manual Connection
|
|
69
|
+
logger_1.logger.info('Initializing multi-node connection...');
|
|
70
|
+
let connectedCount = 0;
|
|
59
71
|
for (const n of this.nodes) {
|
|
60
|
-
logger_1.logger.info(`
|
|
72
|
+
logger_1.logger.info(`Connecting to node ${n.uri}`);
|
|
61
73
|
try {
|
|
62
|
-
const c = new mongodb_1.MongoClient(n.uri, {
|
|
74
|
+
const c = new mongodb_1.MongoClient(n.uri, {
|
|
75
|
+
directConnection: true,
|
|
76
|
+
monitorCommands: true,
|
|
77
|
+
minPoolSize: this.minPoolSize,
|
|
78
|
+
maxPoolSize: this.maxPoolSize
|
|
79
|
+
});
|
|
80
|
+
this.attachPoolMonitor(c, 'unknown', n.uri); // Type unknown until verified
|
|
63
81
|
yield c.connect();
|
|
82
|
+
connectedCount++;
|
|
64
83
|
const writable = yield this.checkWritable(c);
|
|
65
|
-
if (writable) {
|
|
66
|
-
logger_1.logger.info(`Found
|
|
84
|
+
if (writable && !this.primaryClient) {
|
|
85
|
+
logger_1.logger.info(`Found Primary node at ${n.uri}`);
|
|
67
86
|
this.attachClient(c);
|
|
68
|
-
|
|
69
|
-
|
|
87
|
+
// Re-tag metrics if needed?
|
|
88
|
+
// Metrics are tagged by 'node' URI, so type label 'unknown' might be set once.
|
|
89
|
+
// Ideally we update the label but exposed gauges don't support re-labeling easily without removing.
|
|
90
|
+
// For now, node URI is the main identifier.
|
|
70
91
|
}
|
|
71
92
|
else {
|
|
72
|
-
|
|
93
|
+
logger_1.logger.info(`Connected to Secondary node at ${n.uri}`);
|
|
94
|
+
this.secondaryClients.push(c);
|
|
73
95
|
}
|
|
74
96
|
}
|
|
75
97
|
catch (err) {
|
|
76
98
|
logger_1.logger.warn(`Node connect failed ${n.uri}: ${err.message}`);
|
|
77
99
|
}
|
|
78
100
|
}
|
|
79
|
-
|
|
101
|
+
if (connectedCount === 0) {
|
|
102
|
+
throw new Error('No available MongoDB nodes found. Check your cluster.');
|
|
103
|
+
}
|
|
104
|
+
if (!this.primaryClient) {
|
|
105
|
+
logger_1.logger.warn('Initialized without a Primary node! System in Read-Only mode until a node becomes writable.');
|
|
106
|
+
}
|
|
107
|
+
this.startHealthChecks();
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
attachPoolMonitor(client, type, nodeUri) {
|
|
111
|
+
const label = { type, node: nodeUri };
|
|
112
|
+
client.on('connectionCreated', () => metrics_1.poolSize.inc(label));
|
|
113
|
+
client.on('connectionClosed', () => metrics_1.poolSize.dec(label));
|
|
114
|
+
client.on('connectionCheckedOut', () => {
|
|
115
|
+
metrics_1.poolCheckedOut.inc(label);
|
|
116
|
+
});
|
|
117
|
+
client.on('connectionCheckedIn', () => {
|
|
118
|
+
metrics_1.poolCheckedOut.dec(label);
|
|
80
119
|
});
|
|
120
|
+
// 'connectionCheckOutStarted' indicates a request entered the queue (or is about to grab one)
|
|
121
|
+
// 'connectionCheckOutFailed' indicates it failed to get one (timeout)
|
|
122
|
+
// We can use these to track queue depth approximately.
|
|
123
|
+
client.on('connectionCheckOutStarted', () => metrics_1.poolWaitQueue.inc(label));
|
|
124
|
+
client.on('connectionCheckOutFailed', () => metrics_1.poolWaitQueue.dec(label));
|
|
125
|
+
// When successfully checked out, it also leaves the queue
|
|
126
|
+
client.on('connectionCheckedOut', () => metrics_1.poolWaitQueue.dec(label));
|
|
81
127
|
}
|
|
82
128
|
attachClient(client) {
|
|
83
129
|
this.primaryClient = client;
|
|
84
130
|
this.primaryDb = client.db(this.dbName);
|
|
85
|
-
// register monitoring events on the client
|
|
86
131
|
client.on('topologyDescriptionChanged', (td) => {
|
|
87
132
|
var _a;
|
|
88
|
-
logger_1.logger.info(`topologyDescriptionChanged: ${JSON.stringify(this.summarizeTopology(td))}`);
|
|
89
133
|
const summary = this.summarizeTopology(td);
|
|
134
|
+
// logger.info(`topologyDescriptionChanged: ${JSON.stringify(summary)}`);
|
|
90
135
|
this.recordEvent('topologyDescriptionChanged', { td: summary }).catch(() => { });
|
|
91
136
|
(_a = (0, websocket_1.getIO)()) === null || _a === void 0 ? void 0 : _a.emit('topology-change', summary);
|
|
92
137
|
});
|
|
@@ -96,7 +141,7 @@ class ConnectionManager {
|
|
|
96
141
|
this.sendAlert('serverHeartbeatFailed', event).catch(() => { });
|
|
97
142
|
});
|
|
98
143
|
client.on('serverHeartbeatSucceeded', (event) => {
|
|
99
|
-
|
|
144
|
+
// verbose
|
|
100
145
|
});
|
|
101
146
|
client.on('close', () => {
|
|
102
147
|
logger_1.logger.warn('MongoClient close event');
|
|
@@ -106,7 +151,6 @@ class ConnectionManager {
|
|
|
106
151
|
metrics_1.connectionStatus.set(1);
|
|
107
152
|
}
|
|
108
153
|
summarizeTopology(td) {
|
|
109
|
-
// gentle summary - driver topologyDescription shape may vary
|
|
110
154
|
try {
|
|
111
155
|
return {
|
|
112
156
|
servers: Object.keys(td.servers || {}).map((k) => ({
|
|
@@ -123,15 +167,14 @@ class ConnectionManager {
|
|
|
123
167
|
return __awaiter(this, void 0, void 0, function* () {
|
|
124
168
|
try {
|
|
125
169
|
const admin = client.db('admin');
|
|
126
|
-
// isWritablePrimary command is supported
|
|
127
170
|
const res = yield admin.command({ isWritablePrimary: 1 }).catch(() => null);
|
|
128
171
|
if (res && res.isWritablePrimary)
|
|
129
172
|
return true;
|
|
130
|
-
// fallback: isMaster / hello
|
|
131
173
|
const info = yield admin.command({ hello: 1 }).catch(() => null);
|
|
132
|
-
if (info && (info.isWritablePrimary ||
|
|
174
|
+
if (info && (info.isWritablePrimary ||
|
|
175
|
+
info.isWritablePrimary === true ||
|
|
176
|
+
info.ismaster === true))
|
|
133
177
|
return true;
|
|
134
|
-
// if driver can't tell, assume writable if connected and write to a temp collection test (careful)
|
|
135
178
|
return false;
|
|
136
179
|
}
|
|
137
180
|
catch (err) {
|
|
@@ -144,19 +187,14 @@ class ConnectionManager {
|
|
|
144
187
|
return __awaiter(this, void 0, void 0, function* () {
|
|
145
188
|
var _a;
|
|
146
189
|
try {
|
|
147
|
-
const event = {
|
|
148
|
-
ts: new Date(),
|
|
149
|
-
level: 'event',
|
|
150
|
-
type,
|
|
151
|
-
payload,
|
|
152
|
-
};
|
|
190
|
+
const event = { ts: new Date(), level: 'event', type, payload };
|
|
153
191
|
(_a = (0, websocket_1.getIO)()) === null || _a === void 0 ? void 0 : _a.emit('log', event);
|
|
154
192
|
if (!this.primaryDb)
|
|
155
193
|
return;
|
|
156
194
|
yield this.primaryDb.collection('logs').insertOne(event);
|
|
157
195
|
}
|
|
158
196
|
catch (err) {
|
|
159
|
-
|
|
197
|
+
// logger.warn('recordEvent failed: ' + (err as Error).message);
|
|
160
198
|
}
|
|
161
199
|
});
|
|
162
200
|
}
|
|
@@ -182,70 +220,108 @@ class ConnectionManager {
|
|
|
182
220
|
}
|
|
183
221
|
healthCheckLoop() {
|
|
184
222
|
return __awaiter(this, void 0, void 0, function* () {
|
|
185
|
-
//
|
|
223
|
+
// 1. Check Primary
|
|
186
224
|
if (this.primaryClient) {
|
|
187
225
|
const ok = yield this.checkWritable(this.primaryClient).catch(() => false);
|
|
188
|
-
if (ok)
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
226
|
+
if (!ok) {
|
|
227
|
+
logger_1.logger.warn('Primary client no longer writable. Demoting to potential secondary.');
|
|
228
|
+
this.secondaryClients.push(this.primaryClient);
|
|
229
|
+
this.primaryClient = null;
|
|
230
|
+
this.primaryDb = null;
|
|
231
|
+
metrics_1.connectionStatus.set(0);
|
|
193
232
|
}
|
|
194
|
-
catch (_a) { }
|
|
195
|
-
this.primaryClient = null;
|
|
196
|
-
this.primaryDb = null;
|
|
197
|
-
metrics_1.connectionStatus.set(0);
|
|
198
233
|
}
|
|
199
|
-
//
|
|
200
|
-
for (
|
|
234
|
+
// 2. Check Secondaries
|
|
235
|
+
for (let i = this.secondaryClients.length - 1; i >= 0; i--) {
|
|
236
|
+
const sec = this.secondaryClients[i];
|
|
201
237
|
try {
|
|
202
|
-
|
|
203
|
-
yield c.connect();
|
|
204
|
-
const writable = yield this.checkWritable(c);
|
|
205
|
-
if (writable) {
|
|
206
|
-
logger_1.logger.info(`Health-check: promoted ${n.uri} to primary connection`);
|
|
207
|
-
this.attachClient(c);
|
|
208
|
-
yield this.recordEvent('promote', { node: n.uri });
|
|
209
|
-
yield this.sendAlert('promote', { node: n.uri, message: 'Promoted new primary connection' });
|
|
210
|
-
metrics_1.failoverCount.inc();
|
|
211
|
-
return;
|
|
212
|
-
}
|
|
213
|
-
else {
|
|
214
|
-
yield c.close();
|
|
215
|
-
}
|
|
238
|
+
yield sec.db('admin').command({ ping: 1 });
|
|
216
239
|
}
|
|
217
240
|
catch (err) {
|
|
218
|
-
logger_1.logger.
|
|
241
|
+
logger_1.logger.warn('Secondary node lost connection. Removing.');
|
|
242
|
+
try {
|
|
243
|
+
yield sec.close();
|
|
244
|
+
}
|
|
245
|
+
catch (_a) { }
|
|
246
|
+
this.secondaryClients.splice(i, 1);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
// 3. Promote if needed
|
|
250
|
+
if (!this.primaryClient) {
|
|
251
|
+
// logger.warn('No Primary! Searching among secondaries...');
|
|
252
|
+
for (let i = 0; i < this.secondaryClients.length; i++) {
|
|
253
|
+
const client = this.secondaryClients[i];
|
|
254
|
+
const isWritable = yield this.checkWritable(client);
|
|
255
|
+
if (isWritable) {
|
|
256
|
+
logger_1.logger.info('Promoting secondary to Primary!');
|
|
257
|
+
this.attachClient(client);
|
|
258
|
+
this.secondaryClients.splice(i, 1);
|
|
259
|
+
yield this.recordEvent('promote', { message: 'Promoted secondary to primary' });
|
|
260
|
+
yield this.sendAlert('promote', { message: 'Promoted new primary connection' });
|
|
261
|
+
metrics_1.failoverCount.inc();
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
219
264
|
}
|
|
220
265
|
}
|
|
221
|
-
// no writable found
|
|
222
|
-
logger_1.logger.error('Health-check: no writable nodes found.');
|
|
223
|
-
yield this.recordEvent('no-writable', {});
|
|
224
|
-
yield this.sendAlert('no-writable', { message: 'CRITICAL: No writable nodes found in cluster!' });
|
|
225
266
|
});
|
|
226
267
|
}
|
|
268
|
+
getSecondary() {
|
|
269
|
+
if (this.secondaryClients.length === 0)
|
|
270
|
+
return null;
|
|
271
|
+
const c = this.secondaryClients[this.rrIndex % this.secondaryClients.length];
|
|
272
|
+
this.rrIndex++;
|
|
273
|
+
return c;
|
|
274
|
+
}
|
|
227
275
|
getDb() {
|
|
228
276
|
return this.primaryDb;
|
|
229
277
|
}
|
|
230
|
-
// Generic wrappers that log operations to collection 'logs'
|
|
231
278
|
read(collectionName_1, op_1) {
|
|
232
|
-
return __awaiter(this, arguments, void 0, function* (collectionName, op, meta = {}) {
|
|
233
|
-
|
|
279
|
+
return __awaiter(this, arguments, void 0, function* (collectionName, op, meta = {}, readPref = 'primary') {
|
|
280
|
+
let clientToUse = this.primaryClient;
|
|
281
|
+
let effectivePref = readPref;
|
|
282
|
+
if (readPref === 'secondary') {
|
|
283
|
+
const sec = this.getSecondary();
|
|
284
|
+
if (sec) {
|
|
285
|
+
clientToUse = sec;
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
throw new Error('No secondary node available for read preference "secondary"');
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
else if (readPref === 'secondaryPreferred') {
|
|
292
|
+
const sec = this.getSecondary();
|
|
293
|
+
if (sec) {
|
|
294
|
+
clientToUse = sec;
|
|
295
|
+
effectivePref = 'secondary';
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
clientToUse = this.primaryClient;
|
|
299
|
+
effectivePref = 'primary';
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
if (!clientToUse) {
|
|
303
|
+
throw new Error('No active connection available for requested read preference');
|
|
304
|
+
}
|
|
305
|
+
const db = clientToUse.db(this.dbName);
|
|
234
306
|
const start = Date.now();
|
|
235
307
|
try {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
const res = yield op(db.collection(collectionName));
|
|
308
|
+
const collection = db.collection(collectionName);
|
|
309
|
+
const res = yield op(collection);
|
|
239
310
|
const took = Date.now() - start;
|
|
240
311
|
yield this.safeLog({
|
|
241
312
|
ts: new Date(),
|
|
242
313
|
op: 'read',
|
|
243
314
|
collection: collectionName,
|
|
244
315
|
success: true,
|
|
245
|
-
meta,
|
|
316
|
+
meta: Object.assign(Object.assign({}, meta), { readPref: effectivePref }),
|
|
246
317
|
durationMs: took,
|
|
247
318
|
});
|
|
248
|
-
metrics_1.operationDuration.observe({
|
|
319
|
+
metrics_1.operationDuration.observe({
|
|
320
|
+
operation: 'read',
|
|
321
|
+
collection: collectionName,
|
|
322
|
+
success: 'true',
|
|
323
|
+
read_preference: effectivePref
|
|
324
|
+
}, took / 1000);
|
|
249
325
|
return res;
|
|
250
326
|
}
|
|
251
327
|
catch (err) {
|
|
@@ -256,10 +332,15 @@ class ConnectionManager {
|
|
|
256
332
|
collection: collectionName,
|
|
257
333
|
success: false,
|
|
258
334
|
error: err.message,
|
|
259
|
-
meta,
|
|
335
|
+
meta: Object.assign(Object.assign({}, meta), { readPref: effectivePref }),
|
|
260
336
|
durationMs: took,
|
|
261
337
|
});
|
|
262
|
-
metrics_1.operationDuration.observe({
|
|
338
|
+
metrics_1.operationDuration.observe({
|
|
339
|
+
operation: 'read',
|
|
340
|
+
collection: collectionName,
|
|
341
|
+
success: 'false',
|
|
342
|
+
read_preference: effectivePref
|
|
343
|
+
}, took / 1000);
|
|
263
344
|
throw err;
|
|
264
345
|
}
|
|
265
346
|
});
|
|
@@ -306,14 +387,13 @@ class ConnectionManager {
|
|
|
306
387
|
try {
|
|
307
388
|
(_a = (0, websocket_1.getIO)()) === null || _a === void 0 ? void 0 : _a.emit('log', doc);
|
|
308
389
|
if (!this.primaryDb) {
|
|
309
|
-
|
|
310
|
-
logger_1.logger.info(JSON.stringify(doc));
|
|
390
|
+
// logger.warn('safeLog: no primaryDb, skipping db log.');
|
|
311
391
|
return;
|
|
312
392
|
}
|
|
313
393
|
yield this.primaryDb.collection('logs').insertOne(doc);
|
|
314
394
|
}
|
|
315
395
|
catch (err) {
|
|
316
|
-
|
|
396
|
+
// silent fail for log
|
|
317
397
|
}
|
|
318
398
|
});
|
|
319
399
|
}
|
|
@@ -321,11 +401,13 @@ class ConnectionManager {
|
|
|
321
401
|
return __awaiter(this, void 0, void 0, function* () {
|
|
322
402
|
if (this.healthInterval)
|
|
323
403
|
clearInterval(this.healthInterval);
|
|
324
|
-
if (this.primaryClient)
|
|
404
|
+
if (this.primaryClient)
|
|
325
405
|
yield this.primaryClient.close().catch(() => { });
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
406
|
+
for (const c of this.secondaryClients)
|
|
407
|
+
yield c.close().catch(() => { });
|
|
408
|
+
this.primaryClient = null;
|
|
409
|
+
this.primaryDb = null;
|
|
410
|
+
this.secondaryClients = [];
|
|
329
411
|
});
|
|
330
412
|
}
|
|
331
413
|
}
|
package/dist/config/metrics.js
CHANGED
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.operationDuration = exports.failoverCount = exports.connectionStatus = exports.register = void 0;
|
|
6
|
+
exports.poolWaitQueue = exports.poolCheckedOut = exports.poolSize = exports.operationDuration = exports.failoverCount = exports.connectionStatus = exports.register = void 0;
|
|
7
7
|
const prom_client_1 = __importDefault(require("prom-client"));
|
|
8
8
|
// Create a Registry
|
|
9
9
|
exports.register = new prom_client_1.default.Registry();
|
|
@@ -23,7 +23,25 @@ exports.failoverCount = new prom_client_1.default.Counter({
|
|
|
23
23
|
exports.operationDuration = new prom_client_1.default.Histogram({
|
|
24
24
|
name: 'node_balancer_operation_duration_seconds',
|
|
25
25
|
help: 'Duration of database operations in seconds',
|
|
26
|
-
labelNames: ['operation', 'collection', 'success'],
|
|
26
|
+
labelNames: ['operation', 'collection', 'success', 'read_preference'],
|
|
27
27
|
buckets: [0.01, 0.05, 0.1, 0.5, 1, 2, 5],
|
|
28
28
|
registers: [exports.register]
|
|
29
29
|
});
|
|
30
|
+
exports.poolSize = new prom_client_1.default.Gauge({
|
|
31
|
+
name: 'node_balancer_pool_size',
|
|
32
|
+
help: 'Current size of the connection pool',
|
|
33
|
+
labelNames: ['type', 'node'],
|
|
34
|
+
registers: [exports.register]
|
|
35
|
+
});
|
|
36
|
+
exports.poolCheckedOut = new prom_client_1.default.Gauge({
|
|
37
|
+
name: 'node_balancer_pool_checked_out',
|
|
38
|
+
help: 'Number of connections currently checked out',
|
|
39
|
+
labelNames: ['type', 'node'],
|
|
40
|
+
registers: [exports.register]
|
|
41
|
+
});
|
|
42
|
+
exports.poolWaitQueue = new prom_client_1.default.Gauge({
|
|
43
|
+
name: 'node_balancer_pool_wait_queue',
|
|
44
|
+
help: 'Number of requests waiting for a connection',
|
|
45
|
+
labelNames: ['type', 'node'],
|
|
46
|
+
registers: [exports.register]
|
|
47
|
+
});
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
2
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
17
|
exports.ConnectionManager = void 0;
|
|
4
18
|
var connectionManager_1 = require("./config/connectionManager");
|
|
5
19
|
Object.defineProperty(exports, "ConnectionManager", { enumerable: true, get: function () { return connectionManager_1.ConnectionManager; } });
|
|
20
|
+
__exportStar(require("./nestjs"), exports);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./node-balancer.module"), exports);
|
|
18
|
+
__exportStar(require("./node-balancer.decorators"), exports);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.NODE_BALANCER_CONNECTION = void 0;
|
|
4
|
+
exports.InjectConnectionManager = InjectConnectionManager;
|
|
5
|
+
const common_1 = require("@nestjs/common");
|
|
6
|
+
exports.NODE_BALANCER_CONNECTION = 'NODE_BALANCER_CONNECTION';
|
|
7
|
+
function InjectConnectionManager() {
|
|
8
|
+
return (0, common_1.Inject)(exports.NODE_BALANCER_CONNECTION);
|
|
9
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
9
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
10
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
11
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
12
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
13
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
14
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
15
|
+
});
|
|
16
|
+
};
|
|
17
|
+
var NodeBalancerModule_1;
|
|
18
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
+
exports.NodeBalancerModule = void 0;
|
|
20
|
+
const common_1 = require("@nestjs/common");
|
|
21
|
+
const connectionManager_1 = require("../config/connectionManager");
|
|
22
|
+
const node_balancer_decorators_1 = require("./node-balancer.decorators");
|
|
23
|
+
let NodeBalancerModule = NodeBalancerModule_1 = class NodeBalancerModule {
|
|
24
|
+
static forRoot(options) {
|
|
25
|
+
const connectionProvider = {
|
|
26
|
+
provide: node_balancer_decorators_1.NODE_BALANCER_CONNECTION,
|
|
27
|
+
useFactory: () => __awaiter(this, void 0, void 0, function* () {
|
|
28
|
+
console.log('DEBUG: NodeBalancerModule useFactory called');
|
|
29
|
+
const manager = new connectionManager_1.ConnectionManager(options);
|
|
30
|
+
console.log('DEBUG: Manager created, initializing...');
|
|
31
|
+
yield manager.init();
|
|
32
|
+
console.log('DEBUG: Manager initialized successfully');
|
|
33
|
+
return manager;
|
|
34
|
+
}),
|
|
35
|
+
};
|
|
36
|
+
return {
|
|
37
|
+
module: NodeBalancerModule_1,
|
|
38
|
+
providers: [connectionProvider],
|
|
39
|
+
exports: [connectionProvider],
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
static forRootAsync(options) {
|
|
43
|
+
const connectionProvider = {
|
|
44
|
+
provide: node_balancer_decorators_1.NODE_BALANCER_CONNECTION,
|
|
45
|
+
useFactory: (...args) => __awaiter(this, void 0, void 0, function* () {
|
|
46
|
+
const config = yield options.useFactory(...args);
|
|
47
|
+
const manager = new connectionManager_1.ConnectionManager(config);
|
|
48
|
+
yield manager.init();
|
|
49
|
+
return manager;
|
|
50
|
+
}),
|
|
51
|
+
inject: options.inject || [],
|
|
52
|
+
};
|
|
53
|
+
return {
|
|
54
|
+
module: NodeBalancerModule_1,
|
|
55
|
+
imports: options.imports || [],
|
|
56
|
+
providers: [connectionProvider],
|
|
57
|
+
exports: [connectionProvider],
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
exports.NodeBalancerModule = NodeBalancerModule;
|
|
62
|
+
exports.NodeBalancerModule = NodeBalancerModule = NodeBalancerModule_1 = __decorate([
|
|
63
|
+
(0, common_1.Global)(),
|
|
64
|
+
(0, common_1.Module)({})
|
|
65
|
+
], NodeBalancerModule);
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
12
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
13
|
+
};
|
|
14
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
15
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
16
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
17
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
18
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
19
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
20
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
21
|
+
});
|
|
22
|
+
};
|
|
23
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
+
require("reflect-metadata");
|
|
25
|
+
const common_1 = require("@nestjs/common");
|
|
26
|
+
const core_1 = require("@nestjs/core");
|
|
27
|
+
const node_balancer_module_1 = require("../nestjs/node-balancer.module");
|
|
28
|
+
const node_balancer_decorators_1 = require("../nestjs/node-balancer.decorators");
|
|
29
|
+
const connectionManager_1 = require("../config/connectionManager");
|
|
30
|
+
process.on('unhandledRejection', (reason, p) => {
|
|
31
|
+
console.error('Unhandled Rejection at:', p, 'reason:', reason);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
});
|
|
34
|
+
process.on('uncaughtException', (err) => {
|
|
35
|
+
console.error('Uncaught Exception:', err);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
});
|
|
38
|
+
let AppService = class AppService {
|
|
39
|
+
constructor(connectionManager) {
|
|
40
|
+
this.connectionManager = connectionManager;
|
|
41
|
+
}
|
|
42
|
+
check() {
|
|
43
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
44
|
+
if (this.connectionManager) {
|
|
45
|
+
console.log('✅ ConnectionManager injected successfully!');
|
|
46
|
+
const db = this.connectionManager.getDb();
|
|
47
|
+
console.log(`✅ Database context available: ${db ? 'Yes' : 'No (init might be pending or failed)'}`);
|
|
48
|
+
return 'ok';
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
console.error('❌ ConnectionManager failed to inject.');
|
|
52
|
+
return 'fail';
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
AppService = __decorate([
|
|
58
|
+
(0, common_1.Injectable)(),
|
|
59
|
+
__param(0, (0, node_balancer_decorators_1.InjectConnectionManager)()),
|
|
60
|
+
__metadata("design:paramtypes", [connectionManager_1.ConnectionManager])
|
|
61
|
+
], AppService);
|
|
62
|
+
let AppController = class AppController {
|
|
63
|
+
constructor(appService) {
|
|
64
|
+
this.appService = appService;
|
|
65
|
+
}
|
|
66
|
+
getHello() {
|
|
67
|
+
return this.appService.check();
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
__decorate([
|
|
71
|
+
(0, common_1.Get)(),
|
|
72
|
+
__metadata("design:type", Function),
|
|
73
|
+
__metadata("design:paramtypes", []),
|
|
74
|
+
__metadata("design:returntype", void 0)
|
|
75
|
+
], AppController.prototype, "getHello", null);
|
|
76
|
+
AppController = __decorate([
|
|
77
|
+
(0, common_1.Controller)(),
|
|
78
|
+
__metadata("design:paramtypes", [AppService])
|
|
79
|
+
], AppController);
|
|
80
|
+
let AppModule = class AppModule {
|
|
81
|
+
};
|
|
82
|
+
AppModule = __decorate([
|
|
83
|
+
(0, common_1.Module)({
|
|
84
|
+
imports: [
|
|
85
|
+
node_balancer_module_1.NodeBalancerModule.forRoot({
|
|
86
|
+
nodes: ['mongodb://localhost:27017', 'mongodb://localhost:27018', 'mongodb://localhost:27019'],
|
|
87
|
+
minPoolSize: 1,
|
|
88
|
+
maxPoolSize: 5,
|
|
89
|
+
dbName: 'test_nest'
|
|
90
|
+
})
|
|
91
|
+
],
|
|
92
|
+
controllers: [AppController],
|
|
93
|
+
providers: [AppService],
|
|
94
|
+
})
|
|
95
|
+
], AppModule);
|
|
96
|
+
function manualTest() {
|
|
97
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
98
|
+
console.log('DEBUG: Starting manual test...');
|
|
99
|
+
try {
|
|
100
|
+
const m = new connectionManager_1.ConnectionManager({
|
|
101
|
+
nodes: ['mongodb://localhost:27017', 'mongodb://localhost:27018', 'mongodb://localhost:27019'],
|
|
102
|
+
minPoolSize: 1,
|
|
103
|
+
maxPoolSize: 5
|
|
104
|
+
});
|
|
105
|
+
yield m.init();
|
|
106
|
+
console.log('DEBUG: Manual test passed');
|
|
107
|
+
// await m.close(); // Keep it open or close? Close.
|
|
108
|
+
}
|
|
109
|
+
catch (e) {
|
|
110
|
+
console.error('DEBUG: Manual test failed', e);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
function bootstrap() {
|
|
115
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
116
|
+
console.log('🚀 Starting NestJS Context...');
|
|
117
|
+
yield manualTest();
|
|
118
|
+
try {
|
|
119
|
+
const app = yield core_1.NestFactory.createApplicationContext(AppModule, { logger: ['error', 'warn', 'debug', 'verbose'] });
|
|
120
|
+
const service = app.get(AppService);
|
|
121
|
+
yield service.check();
|
|
122
|
+
yield app.close();
|
|
123
|
+
console.log('✅ NestJS Test Finished.');
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
console.error('❌ NestJS Boot failed:', error);
|
|
127
|
+
process.exit(1);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
bootstrap();
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
const connectionManager_1 = require("../config/connectionManager");
|
|
16
|
+
const dotenv_1 = __importDefault(require("dotenv"));
|
|
17
|
+
dotenv_1.default.config({ path: '.env.local' });
|
|
18
|
+
function run() {
|
|
19
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
20
|
+
const envNodes = process.env.MONGODB_NODES || '';
|
|
21
|
+
const nodes = envNodes ? envNodes.split(',') : [
|
|
22
|
+
'mongodb://localhost:27017',
|
|
23
|
+
'mongodb://localhost:27018',
|
|
24
|
+
'mongodb://localhost:27019'
|
|
25
|
+
];
|
|
26
|
+
console.log('--- Testing Read Preference and Multi-Node support ---');
|
|
27
|
+
console.log(`Nodes: ${nodes.join(', ')}`);
|
|
28
|
+
const db = new connectionManager_1.ConnectionManager({
|
|
29
|
+
nodes,
|
|
30
|
+
healthCheckIntervalMs: 2000,
|
|
31
|
+
maxPoolSize: 5,
|
|
32
|
+
minPoolSize: 1
|
|
33
|
+
});
|
|
34
|
+
try {
|
|
35
|
+
yield db.init();
|
|
36
|
+
console.log('✅ Initialization complete.');
|
|
37
|
+
// Test Write (Primary)
|
|
38
|
+
console.log('📝 Testing Write (Primary)...');
|
|
39
|
+
try {
|
|
40
|
+
yield db.write('test_reads', (col) => __awaiter(this, void 0, void 0, function* () {
|
|
41
|
+
yield col.insertOne({ test: 'read_pref', date: new Date() });
|
|
42
|
+
}));
|
|
43
|
+
console.log('✅ Write successful.');
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
console.warn('⚠️ Write failed (No Primary?), skipping to read tests... Error:', err.message);
|
|
47
|
+
}
|
|
48
|
+
// Test Read (Primary default)
|
|
49
|
+
console.log('📖 Testing Read (Primary Default)...');
|
|
50
|
+
try {
|
|
51
|
+
const res1 = yield db.read('test_reads', (col) => __awaiter(this, void 0, void 0, function* () {
|
|
52
|
+
return col.findOne({ test: 'read_pref' });
|
|
53
|
+
}));
|
|
54
|
+
console.log('✅ Read Primary result:', res1 === null || res1 === void 0 ? void 0 : res1._id);
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
console.warn('⚠️ Read Primary failed (No Primary?), skipping... Error:', err.message);
|
|
58
|
+
}
|
|
59
|
+
// Test Read (Secondary Strict)
|
|
60
|
+
console.log('📖 Testing Read (Secondary Strict)...');
|
|
61
|
+
try {
|
|
62
|
+
const res2 = yield db.read('test_reads', (col) => __awaiter(this, void 0, void 0, function* () {
|
|
63
|
+
return col.findOne({ test: 'read_pref' });
|
|
64
|
+
}), {}, 'secondary'); // <--- requesting secondary
|
|
65
|
+
console.log('✅ Read Secondary result:', res2 === null || res2 === void 0 ? void 0 : res2._id);
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
console.error('❌ Read Secondary failed:', err.message);
|
|
69
|
+
}
|
|
70
|
+
// Test Read (Secondary Preferred)
|
|
71
|
+
console.log('📖 Testing Read (Secondary Preferred)...');
|
|
72
|
+
const res3 = yield db.read('test_reads', (col) => __awaiter(this, void 0, void 0, function* () {
|
|
73
|
+
return col.findOne({ test: 'read_pref' });
|
|
74
|
+
}), {}, 'secondaryPreferred');
|
|
75
|
+
console.log('✅ Read SecondaryPreferred result:', res3 === null || res3 === void 0 ? void 0 : res3._id);
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
console.error('❌ Test failed:', err);
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
yield db.close();
|
|
82
|
+
console.log('👋 Connection closed.');
|
|
83
|
+
process.exit(0);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
run();
|
package/dist/server.js
CHANGED
|
@@ -23,7 +23,8 @@ app_1.default.get('/metrics', (req, res) => __awaiter(void 0, void 0, void 0, fu
|
|
|
23
23
|
res.end(yield metrics_1.register.metrics());
|
|
24
24
|
}
|
|
25
25
|
catch (ex) {
|
|
26
|
-
|
|
26
|
+
logger_1.logger.error('Error while serving /metrics endpoint', ex);
|
|
27
|
+
res.status(500).end('Internal server error');
|
|
27
28
|
}
|
|
28
29
|
}));
|
|
29
30
|
const server = app_1.default.listen(PORT, () => {
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "replica-failover-mongodb-ts",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.8",
|
|
4
4
|
"description": "O sistema replica um dos conceitos básicos da arquitetura de sistemas distribuído, na qual ele tem uma API em express.js com TypeScript, e usando o mongoDB com replicaSET e failover, consegue automáticamente garantir disponibilidade e redundância de dados das requisições.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"scripts": {
|
|
8
|
+
"test": "node dist/scripts/test_read_pref.js",
|
|
8
9
|
"dev": "nodemon --exec ts-node src/server.ts",
|
|
9
10
|
"build": "tsc",
|
|
10
11
|
"start": "node dist/server.js",
|
|
@@ -29,6 +30,11 @@
|
|
|
29
30
|
"keywords": [],
|
|
30
31
|
"author": "",
|
|
31
32
|
"license": "ISC",
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0",
|
|
35
|
+
"@nestjs/core": "^8.0.0 || ^9.0.0 || ^10.0.0",
|
|
36
|
+
"rxjs": "^7.0.0"
|
|
37
|
+
},
|
|
32
38
|
"dependencies": {
|
|
33
39
|
"@types/blessed": "^0.1.26",
|
|
34
40
|
"@types/inquirer": "^9.0.9",
|
|
@@ -46,11 +52,16 @@
|
|
|
46
52
|
"winston": "^3.18.3"
|
|
47
53
|
},
|
|
48
54
|
"devDependencies": {
|
|
55
|
+
"@nestjs/common": "^11.1.13",
|
|
56
|
+
"@nestjs/core": "^11.1.13",
|
|
57
|
+
"@nestjs/platform-express": "^11.1.13",
|
|
49
58
|
"@types/express": "^5.0.3",
|
|
50
59
|
"@types/morgan": "^1.9.10",
|
|
51
60
|
"@types/node": "^24.8.0",
|
|
52
61
|
"nodemon": "^3.1.10",
|
|
62
|
+
"reflect-metadata": "^0.2.2",
|
|
63
|
+
"rxjs": "^7.8.2",
|
|
53
64
|
"ts-node": "^10.9.2",
|
|
54
|
-
"typescript": "^5.
|
|
65
|
+
"typescript": "^5.6.3"
|
|
55
66
|
}
|
|
56
67
|
}
|