@samuelaj1/eazisend-service-kit 1.0.5 → 1.0.7
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/index.cjs +207 -2
- package/dist/index.d.cts +79 -2
- package/dist/index.d.ts +79 -2
- package/dist/index.js +206 -2
- package/package.json +12 -10
package/dist/index.cjs
CHANGED
|
@@ -33,6 +33,7 @@ __export(index_exports, {
|
|
|
33
33
|
LogLevelTypes: () => LogLevelTypes,
|
|
34
34
|
elasticSearchRepo: () => elasticSearchRepo,
|
|
35
35
|
loggerRepo: () => loggerRepo,
|
|
36
|
+
rabbitmqRepo: () => rabbitmqRepo,
|
|
36
37
|
registerErrorMiddleware: () => registerErrorMiddleware,
|
|
37
38
|
registerSecurityMiddleware: () => registerSecurityMiddleware,
|
|
38
39
|
registerStandardMiddleware: () => registerStandardMiddleware,
|
|
@@ -66,11 +67,14 @@ var Config = class {
|
|
|
66
67
|
API_GATEWAY_URL;
|
|
67
68
|
GATEWAY_TOKEN;
|
|
68
69
|
ELASTIC_SEARCH_NODE_URL;
|
|
70
|
+
RABBITMQ_URL;
|
|
69
71
|
PORT;
|
|
70
72
|
constructor() {
|
|
71
73
|
this.API_GATEWAY_URL = process.env.API_GATEWAY_URL || "";
|
|
72
74
|
this.GATEWAY_TOKEN = process.env.GATEWAY_TOKEN || "";
|
|
73
75
|
this.ELASTIC_SEARCH_NODE_URL = process.env.ELASTIC_SEARCH_NODE_URL || "";
|
|
76
|
+
this.RABBITMQ_URL = process.env.RABBITMQ_URL || "";
|
|
77
|
+
this.ELASTIC_SEARCH_NODE_URL = process.env.ELASTIC_SEARCH_NODE_URL?.trim() || "";
|
|
74
78
|
this.PORT = process.env.PORT || "";
|
|
75
79
|
}
|
|
76
80
|
};
|
|
@@ -128,11 +132,13 @@ var LoggerRepo = class {
|
|
|
128
132
|
}
|
|
129
133
|
}
|
|
130
134
|
};
|
|
131
|
-
const esTransport = new import_winston_elasticsearch.ElasticsearchTransport(options.elasticsearch);
|
|
132
135
|
this.logger = import_winston.default.createLogger({
|
|
133
136
|
exitOnError: false,
|
|
134
137
|
defaultMeta: { service: identifier },
|
|
135
|
-
transports: [
|
|
138
|
+
transports: [
|
|
139
|
+
new import_winston.default.transports.Console(options.console),
|
|
140
|
+
...elasticsearchNode ? [new import_winston_elasticsearch.ElasticsearchTransport(options.elasticsearch)] : []
|
|
141
|
+
]
|
|
136
142
|
});
|
|
137
143
|
return this;
|
|
138
144
|
};
|
|
@@ -172,12 +178,14 @@ var ElasticsearchRepo = class {
|
|
|
172
178
|
elasticSearchClient;
|
|
173
179
|
/** Creates an Elasticsearch client using the configured cluster URL. */
|
|
174
180
|
constructor() {
|
|
181
|
+
if (!libConfig.ELASTIC_SEARCH_NODE_URL) return;
|
|
175
182
|
this.elasticSearchClient = new import_elasticsearch.Client({
|
|
176
183
|
node: libConfig.ELASTIC_SEARCH_NODE_URL
|
|
177
184
|
});
|
|
178
185
|
}
|
|
179
186
|
/** Elasticsearch connection is established. */
|
|
180
187
|
async establishElasticSearchConnection(args) {
|
|
188
|
+
if (!this.elasticSearchClient) return;
|
|
181
189
|
let isConnected = false;
|
|
182
190
|
while (!isConnected) {
|
|
183
191
|
try {
|
|
@@ -193,6 +201,202 @@ var ElasticsearchRepo = class {
|
|
|
193
201
|
};
|
|
194
202
|
var elasticSearchRepo = new ElasticsearchRepo();
|
|
195
203
|
|
|
204
|
+
// src/repositories/rabbitmq.repo.ts
|
|
205
|
+
var import_amqplib = __toESM(require("amqplib"), 1);
|
|
206
|
+
var RabbitmqRepo = class {
|
|
207
|
+
queueChannel;
|
|
208
|
+
pendingConnection;
|
|
209
|
+
/**
|
|
210
|
+
* Returns the shared channel, creating a connection when necessary.
|
|
211
|
+
*
|
|
212
|
+
* @example
|
|
213
|
+
* ```ts
|
|
214
|
+
* const channel = await rabbitmqRepo.getQueueConnection();
|
|
215
|
+
* ```
|
|
216
|
+
*/
|
|
217
|
+
async getQueueConnection() {
|
|
218
|
+
if (this.queueChannel) {
|
|
219
|
+
return this.queueChannel;
|
|
220
|
+
}
|
|
221
|
+
if (this.pendingConnection) {
|
|
222
|
+
return this.pendingConnection;
|
|
223
|
+
}
|
|
224
|
+
this.pendingConnection = this.createQueueConnection();
|
|
225
|
+
try {
|
|
226
|
+
return await this.pendingConnection;
|
|
227
|
+
} finally {
|
|
228
|
+
this.pendingConnection = void 0;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
async createQueueConnection() {
|
|
232
|
+
let connection;
|
|
233
|
+
try {
|
|
234
|
+
connection = await import_amqplib.default.connect(libConfig.RABBITMQ_URL);
|
|
235
|
+
const channel = await connection.createChannel();
|
|
236
|
+
const clearChannel = () => {
|
|
237
|
+
if (this.queueChannel === channel) {
|
|
238
|
+
this.queueChannel = void 0;
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
channel.on("error", (error) => {
|
|
242
|
+
clearChannel();
|
|
243
|
+
console.error("RabbitmqRepo channel error:", error);
|
|
244
|
+
});
|
|
245
|
+
channel.once("close", clearChannel);
|
|
246
|
+
connection.on("error", (error) => {
|
|
247
|
+
clearChannel();
|
|
248
|
+
console.error("RabbitmqRepo connection error:", error);
|
|
249
|
+
});
|
|
250
|
+
connection.once("close", clearChannel);
|
|
251
|
+
console.log("RabbitmqRepo connected to queue successfully...");
|
|
252
|
+
this.closeConnection(channel, connection);
|
|
253
|
+
this.queueChannel = channel;
|
|
254
|
+
return channel;
|
|
255
|
+
} catch (error) {
|
|
256
|
+
if (connection) {
|
|
257
|
+
await connection.close().catch((closeError) => {
|
|
258
|
+
console.error("RabbitmqRepo error closing failed connection:", closeError);
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
console.error("RabbitmqRepo error creating connection:", error);
|
|
262
|
+
return void 0;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Closes the channel and connection when the application shuts down.
|
|
267
|
+
*/
|
|
268
|
+
closeConnection(channel, connection) {
|
|
269
|
+
let isClosing = false;
|
|
270
|
+
const close = async (signal) => {
|
|
271
|
+
if (isClosing) {
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
isClosing = true;
|
|
275
|
+
try {
|
|
276
|
+
try {
|
|
277
|
+
await channel.close();
|
|
278
|
+
} finally {
|
|
279
|
+
await connection.close();
|
|
280
|
+
}
|
|
281
|
+
console.log(`RabbitmqRepo queue connection closed after ${signal}`);
|
|
282
|
+
} catch (error) {
|
|
283
|
+
console.error("RabbitmqRepo error closing connection:", error);
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
process.once("SIGINT", close);
|
|
287
|
+
process.once("SIGTERM", close);
|
|
288
|
+
connection.once("close", () => {
|
|
289
|
+
process.removeListener("SIGINT", close);
|
|
290
|
+
process.removeListener("SIGTERM", close);
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Establishes the shared RabbitMQ connection during application startup.
|
|
295
|
+
*
|
|
296
|
+
* @example
|
|
297
|
+
* ```ts
|
|
298
|
+
* await rabbitmqRepo.establishQueueConnection();
|
|
299
|
+
* ```
|
|
300
|
+
*/
|
|
301
|
+
async establishQueueConnection() {
|
|
302
|
+
const channel = await this.getQueueConnection();
|
|
303
|
+
if (!channel) {
|
|
304
|
+
throw new Error("No channel found.");
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
async connectQueueToExchange(channel, exchange, queueName, routingKey, exchangeType) {
|
|
308
|
+
await channel.assertExchange(exchange, exchangeType);
|
|
309
|
+
const q = await channel.assertQueue(queueName, { durable: true, autoDelete: false });
|
|
310
|
+
await channel.bindQueue(q.queue, exchange, routingKey);
|
|
311
|
+
return q;
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Publishes a typed payload as JSON using the supplied exchange and routing key.
|
|
315
|
+
*
|
|
316
|
+
* @example
|
|
317
|
+
* ```ts
|
|
318
|
+
* await rabbitmqRepo.publishMessageToQueue<EmailPayload>({
|
|
319
|
+
* exchange: 'notifications',
|
|
320
|
+
* routingKey: 'email',
|
|
321
|
+
* message: { receiverEmail, username, verifyLink, resetLink },
|
|
322
|
+
* logMessage: 'Email notification published',
|
|
323
|
+
* });
|
|
324
|
+
* ```
|
|
325
|
+
*/
|
|
326
|
+
async publishMessageToQueue(args) {
|
|
327
|
+
const serializedMessage = JSON.stringify(args.message);
|
|
328
|
+
if (serializedMessage === void 0) {
|
|
329
|
+
throw new Error("Message must be JSON serializable.");
|
|
330
|
+
}
|
|
331
|
+
const channel = await this.getQueueConnection();
|
|
332
|
+
const { exchange, routingKey, logMessage, exchangeType } = args;
|
|
333
|
+
if (!channel) {
|
|
334
|
+
throw new Error("No channel found.");
|
|
335
|
+
}
|
|
336
|
+
await channel.assertExchange(exchange, exchangeType || "direct");
|
|
337
|
+
channel.publish(exchange, routingKey, Buffer.from(serializedMessage));
|
|
338
|
+
if (logMessage) {
|
|
339
|
+
console.log(logMessage);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Consumes JSON messages and acknowledges them after the callback succeeds.
|
|
344
|
+
* Prefetch defaults to 10 when omitted.
|
|
345
|
+
*
|
|
346
|
+
* @example
|
|
347
|
+
* ```ts
|
|
348
|
+
* await rabbitmqRepo.consumeMessagesFromQueue<EmailPayload>({
|
|
349
|
+
* exchange: 'notifications',
|
|
350
|
+
* queueName: 'email-queue',
|
|
351
|
+
* routingKey: 'email',
|
|
352
|
+
* prefetch: 5,
|
|
353
|
+
* callback: async ({ receiverEmail, username, verifyLink, resetLink }) => {
|
|
354
|
+
* // Process the email.
|
|
355
|
+
* },
|
|
356
|
+
* });
|
|
357
|
+
* ```
|
|
358
|
+
*/
|
|
359
|
+
async consumeMessagesFromQueue(args) {
|
|
360
|
+
const { exchange, queueName, routingKey, callback } = args;
|
|
361
|
+
const prefetch = args.prefetch ?? 10;
|
|
362
|
+
if (!Number.isInteger(prefetch) || prefetch < 1 || prefetch > 65535) {
|
|
363
|
+
throw new Error("Prefetch must be an integer between 1 and 65535.");
|
|
364
|
+
}
|
|
365
|
+
const channel = args.channel ?? await this.getQueueConnection();
|
|
366
|
+
if (!channel) {
|
|
367
|
+
throw new Error("No channel found.");
|
|
368
|
+
}
|
|
369
|
+
const q = await this.connectQueueToExchange(channel, exchange, queueName, routingKey, args.exchangeType ?? "direct");
|
|
370
|
+
await channel.prefetch(prefetch, false);
|
|
371
|
+
await channel.consume(q.queue, async (message) => {
|
|
372
|
+
if (!message) {
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
try {
|
|
376
|
+
let payload;
|
|
377
|
+
try {
|
|
378
|
+
payload = JSON.parse(message.content.toString());
|
|
379
|
+
} catch (error) {
|
|
380
|
+
console.error("RabbitmqRepo invalid JSON message:", error);
|
|
381
|
+
channel.nack(message, false, false);
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
try {
|
|
385
|
+
await callback(payload);
|
|
386
|
+
} catch (error) {
|
|
387
|
+
console.error("RabbitmqRepo message callback failed:", error);
|
|
388
|
+
channel.nack(message, false, true);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
channel.ack(message);
|
|
392
|
+
} catch (error) {
|
|
393
|
+
console.error("RabbitmqRepo error settling message:", error);
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
var rabbitmqRepo = new RabbitmqRepo();
|
|
399
|
+
|
|
196
400
|
// src/middlewares/gateway.middleware.ts
|
|
197
401
|
var import_jsonwebtoken = __toESM(require("jsonwebtoken"), 1);
|
|
198
402
|
function verifyGatewayRequest(serviceId) {
|
|
@@ -229,6 +433,7 @@ function registerErrorMiddleware(app) {
|
|
|
229
433
|
LogLevelTypes,
|
|
230
434
|
elasticSearchRepo,
|
|
231
435
|
loggerRepo,
|
|
436
|
+
rabbitmqRepo,
|
|
232
437
|
registerErrorMiddleware,
|
|
233
438
|
registerSecurityMiddleware,
|
|
234
439
|
registerStandardMiddleware,
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Express, Request, Response, NextFunction, Application } from 'express';
|
|
2
2
|
import { LogEntry } from 'winston';
|
|
3
|
+
import { Channel } from 'amqplib';
|
|
3
4
|
|
|
4
5
|
declare function registerStandardMiddleware(app: Express): void;
|
|
5
6
|
|
|
@@ -27,7 +28,7 @@ declare function runServer(app: Express): void;
|
|
|
27
28
|
|
|
28
29
|
/** Configured Elasticsearch cluster */
|
|
29
30
|
declare class ElasticsearchRepo {
|
|
30
|
-
private elasticSearchClient
|
|
31
|
+
private readonly elasticSearchClient?;
|
|
31
32
|
/** Creates an Elasticsearch client using the configured cluster URL. */
|
|
32
33
|
constructor();
|
|
33
34
|
/** Elasticsearch connection is established. */
|
|
@@ -37,8 +38,84 @@ declare class ElasticsearchRepo {
|
|
|
37
38
|
}
|
|
38
39
|
declare const elasticSearchRepo: ElasticsearchRepo;
|
|
39
40
|
|
|
41
|
+
declare class RabbitmqRepo {
|
|
42
|
+
private queueChannel;
|
|
43
|
+
private pendingConnection;
|
|
44
|
+
/**
|
|
45
|
+
* Returns the shared channel, creating a connection when necessary.
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```ts
|
|
49
|
+
* const channel = await rabbitmqRepo.getQueueConnection();
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
getQueueConnection(): Promise<Channel | undefined>;
|
|
53
|
+
private createQueueConnection;
|
|
54
|
+
/**
|
|
55
|
+
* Closes the channel and connection when the application shuts down.
|
|
56
|
+
*/
|
|
57
|
+
private closeConnection;
|
|
58
|
+
/**
|
|
59
|
+
* Establishes the shared RabbitMQ connection during application startup.
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```ts
|
|
63
|
+
* await rabbitmqRepo.establishQueueConnection();
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
establishQueueConnection(): Promise<void>;
|
|
67
|
+
private connectQueueToExchange;
|
|
68
|
+
/**
|
|
69
|
+
* Publishes a typed payload as JSON using the supplied exchange and routing key.
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```ts
|
|
73
|
+
* await rabbitmqRepo.publishMessageToQueue<EmailPayload>({
|
|
74
|
+
* exchange: 'notifications',
|
|
75
|
+
* routingKey: 'email',
|
|
76
|
+
* message: { receiverEmail, username, verifyLink, resetLink },
|
|
77
|
+
* logMessage: 'Email notification published',
|
|
78
|
+
* });
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
publishMessageToQueue<T>(args: {
|
|
82
|
+
exchange: string;
|
|
83
|
+
routingKey: string;
|
|
84
|
+
message: T;
|
|
85
|
+
logMessage?: string;
|
|
86
|
+
exchangeType?: 'direct' | 'fanout';
|
|
87
|
+
}): Promise<void>;
|
|
88
|
+
/**
|
|
89
|
+
* Consumes JSON messages and acknowledges them after the callback succeeds.
|
|
90
|
+
* Prefetch defaults to 10 when omitted.
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ```ts
|
|
94
|
+
* await rabbitmqRepo.consumeMessagesFromQueue<EmailPayload>({
|
|
95
|
+
* exchange: 'notifications',
|
|
96
|
+
* queueName: 'email-queue',
|
|
97
|
+
* routingKey: 'email',
|
|
98
|
+
* prefetch: 5,
|
|
99
|
+
* callback: async ({ receiverEmail, username, verifyLink, resetLink }) => {
|
|
100
|
+
* // Process the email.
|
|
101
|
+
* },
|
|
102
|
+
* });
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
consumeMessagesFromQueue<T = unknown>(args: {
|
|
106
|
+
exchange: string;
|
|
107
|
+
queueName: string;
|
|
108
|
+
routingKey: string;
|
|
109
|
+
callback: (payload: T) => void | Promise<void>;
|
|
110
|
+
channel?: Channel;
|
|
111
|
+
exchangeType?: 'direct' | 'fanout';
|
|
112
|
+
prefetch?: number;
|
|
113
|
+
}): Promise<void>;
|
|
114
|
+
}
|
|
115
|
+
declare const rabbitmqRepo: RabbitmqRepo;
|
|
116
|
+
|
|
40
117
|
declare function verifyGatewayRequest(serviceId: string): (req: Request, _res: Response, next: NextFunction) => void;
|
|
41
118
|
|
|
42
119
|
declare function registerErrorMiddleware(app: Application): (err: Error, _req: Request, res: Response, _next: NextFunction) => void;
|
|
43
120
|
|
|
44
|
-
export { LogLevelTypes, elasticSearchRepo, loggerRepo, registerErrorMiddleware, registerSecurityMiddleware, registerStandardMiddleware, runServer, verifyGatewayRequest };
|
|
121
|
+
export { LogLevelTypes, elasticSearchRepo, loggerRepo, rabbitmqRepo, registerErrorMiddleware, registerSecurityMiddleware, registerStandardMiddleware, runServer, verifyGatewayRequest };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Express, Request, Response, NextFunction, Application } from 'express';
|
|
2
2
|
import { LogEntry } from 'winston';
|
|
3
|
+
import { Channel } from 'amqplib';
|
|
3
4
|
|
|
4
5
|
declare function registerStandardMiddleware(app: Express): void;
|
|
5
6
|
|
|
@@ -27,7 +28,7 @@ declare function runServer(app: Express): void;
|
|
|
27
28
|
|
|
28
29
|
/** Configured Elasticsearch cluster */
|
|
29
30
|
declare class ElasticsearchRepo {
|
|
30
|
-
private elasticSearchClient
|
|
31
|
+
private readonly elasticSearchClient?;
|
|
31
32
|
/** Creates an Elasticsearch client using the configured cluster URL. */
|
|
32
33
|
constructor();
|
|
33
34
|
/** Elasticsearch connection is established. */
|
|
@@ -37,8 +38,84 @@ declare class ElasticsearchRepo {
|
|
|
37
38
|
}
|
|
38
39
|
declare const elasticSearchRepo: ElasticsearchRepo;
|
|
39
40
|
|
|
41
|
+
declare class RabbitmqRepo {
|
|
42
|
+
private queueChannel;
|
|
43
|
+
private pendingConnection;
|
|
44
|
+
/**
|
|
45
|
+
* Returns the shared channel, creating a connection when necessary.
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```ts
|
|
49
|
+
* const channel = await rabbitmqRepo.getQueueConnection();
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
getQueueConnection(): Promise<Channel | undefined>;
|
|
53
|
+
private createQueueConnection;
|
|
54
|
+
/**
|
|
55
|
+
* Closes the channel and connection when the application shuts down.
|
|
56
|
+
*/
|
|
57
|
+
private closeConnection;
|
|
58
|
+
/**
|
|
59
|
+
* Establishes the shared RabbitMQ connection during application startup.
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```ts
|
|
63
|
+
* await rabbitmqRepo.establishQueueConnection();
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
establishQueueConnection(): Promise<void>;
|
|
67
|
+
private connectQueueToExchange;
|
|
68
|
+
/**
|
|
69
|
+
* Publishes a typed payload as JSON using the supplied exchange and routing key.
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```ts
|
|
73
|
+
* await rabbitmqRepo.publishMessageToQueue<EmailPayload>({
|
|
74
|
+
* exchange: 'notifications',
|
|
75
|
+
* routingKey: 'email',
|
|
76
|
+
* message: { receiverEmail, username, verifyLink, resetLink },
|
|
77
|
+
* logMessage: 'Email notification published',
|
|
78
|
+
* });
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
publishMessageToQueue<T>(args: {
|
|
82
|
+
exchange: string;
|
|
83
|
+
routingKey: string;
|
|
84
|
+
message: T;
|
|
85
|
+
logMessage?: string;
|
|
86
|
+
exchangeType?: 'direct' | 'fanout';
|
|
87
|
+
}): Promise<void>;
|
|
88
|
+
/**
|
|
89
|
+
* Consumes JSON messages and acknowledges them after the callback succeeds.
|
|
90
|
+
* Prefetch defaults to 10 when omitted.
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ```ts
|
|
94
|
+
* await rabbitmqRepo.consumeMessagesFromQueue<EmailPayload>({
|
|
95
|
+
* exchange: 'notifications',
|
|
96
|
+
* queueName: 'email-queue',
|
|
97
|
+
* routingKey: 'email',
|
|
98
|
+
* prefetch: 5,
|
|
99
|
+
* callback: async ({ receiverEmail, username, verifyLink, resetLink }) => {
|
|
100
|
+
* // Process the email.
|
|
101
|
+
* },
|
|
102
|
+
* });
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
consumeMessagesFromQueue<T = unknown>(args: {
|
|
106
|
+
exchange: string;
|
|
107
|
+
queueName: string;
|
|
108
|
+
routingKey: string;
|
|
109
|
+
callback: (payload: T) => void | Promise<void>;
|
|
110
|
+
channel?: Channel;
|
|
111
|
+
exchangeType?: 'direct' | 'fanout';
|
|
112
|
+
prefetch?: number;
|
|
113
|
+
}): Promise<void>;
|
|
114
|
+
}
|
|
115
|
+
declare const rabbitmqRepo: RabbitmqRepo;
|
|
116
|
+
|
|
40
117
|
declare function verifyGatewayRequest(serviceId: string): (req: Request, _res: Response, next: NextFunction) => void;
|
|
41
118
|
|
|
42
119
|
declare function registerErrorMiddleware(app: Application): (err: Error, _req: Request, res: Response, _next: NextFunction) => void;
|
|
43
120
|
|
|
44
|
-
export { LogLevelTypes, elasticSearchRepo, loggerRepo, registerErrorMiddleware, registerSecurityMiddleware, registerStandardMiddleware, runServer, verifyGatewayRequest };
|
|
121
|
+
export { LogLevelTypes, elasticSearchRepo, loggerRepo, rabbitmqRepo, registerErrorMiddleware, registerSecurityMiddleware, registerStandardMiddleware, runServer, verifyGatewayRequest };
|
package/dist/index.js
CHANGED
|
@@ -19,11 +19,14 @@ var Config = class {
|
|
|
19
19
|
API_GATEWAY_URL;
|
|
20
20
|
GATEWAY_TOKEN;
|
|
21
21
|
ELASTIC_SEARCH_NODE_URL;
|
|
22
|
+
RABBITMQ_URL;
|
|
22
23
|
PORT;
|
|
23
24
|
constructor() {
|
|
24
25
|
this.API_GATEWAY_URL = process.env.API_GATEWAY_URL || "";
|
|
25
26
|
this.GATEWAY_TOKEN = process.env.GATEWAY_TOKEN || "";
|
|
26
27
|
this.ELASTIC_SEARCH_NODE_URL = process.env.ELASTIC_SEARCH_NODE_URL || "";
|
|
28
|
+
this.RABBITMQ_URL = process.env.RABBITMQ_URL || "";
|
|
29
|
+
this.ELASTIC_SEARCH_NODE_URL = process.env.ELASTIC_SEARCH_NODE_URL?.trim() || "";
|
|
27
30
|
this.PORT = process.env.PORT || "";
|
|
28
31
|
}
|
|
29
32
|
};
|
|
@@ -81,11 +84,13 @@ var LoggerRepo = class {
|
|
|
81
84
|
}
|
|
82
85
|
}
|
|
83
86
|
};
|
|
84
|
-
const esTransport = new ElasticsearchTransport(options.elasticsearch);
|
|
85
87
|
this.logger = winston.createLogger({
|
|
86
88
|
exitOnError: false,
|
|
87
89
|
defaultMeta: { service: identifier },
|
|
88
|
-
transports: [
|
|
90
|
+
transports: [
|
|
91
|
+
new winston.transports.Console(options.console),
|
|
92
|
+
...elasticsearchNode ? [new ElasticsearchTransport(options.elasticsearch)] : []
|
|
93
|
+
]
|
|
89
94
|
});
|
|
90
95
|
return this;
|
|
91
96
|
};
|
|
@@ -125,12 +130,14 @@ var ElasticsearchRepo = class {
|
|
|
125
130
|
elasticSearchClient;
|
|
126
131
|
/** Creates an Elasticsearch client using the configured cluster URL. */
|
|
127
132
|
constructor() {
|
|
133
|
+
if (!libConfig.ELASTIC_SEARCH_NODE_URL) return;
|
|
128
134
|
this.elasticSearchClient = new Client({
|
|
129
135
|
node: libConfig.ELASTIC_SEARCH_NODE_URL
|
|
130
136
|
});
|
|
131
137
|
}
|
|
132
138
|
/** Elasticsearch connection is established. */
|
|
133
139
|
async establishElasticSearchConnection(args) {
|
|
140
|
+
if (!this.elasticSearchClient) return;
|
|
134
141
|
let isConnected = false;
|
|
135
142
|
while (!isConnected) {
|
|
136
143
|
try {
|
|
@@ -146,6 +153,202 @@ var ElasticsearchRepo = class {
|
|
|
146
153
|
};
|
|
147
154
|
var elasticSearchRepo = new ElasticsearchRepo();
|
|
148
155
|
|
|
156
|
+
// src/repositories/rabbitmq.repo.ts
|
|
157
|
+
import amqp from "amqplib";
|
|
158
|
+
var RabbitmqRepo = class {
|
|
159
|
+
queueChannel;
|
|
160
|
+
pendingConnection;
|
|
161
|
+
/**
|
|
162
|
+
* Returns the shared channel, creating a connection when necessary.
|
|
163
|
+
*
|
|
164
|
+
* @example
|
|
165
|
+
* ```ts
|
|
166
|
+
* const channel = await rabbitmqRepo.getQueueConnection();
|
|
167
|
+
* ```
|
|
168
|
+
*/
|
|
169
|
+
async getQueueConnection() {
|
|
170
|
+
if (this.queueChannel) {
|
|
171
|
+
return this.queueChannel;
|
|
172
|
+
}
|
|
173
|
+
if (this.pendingConnection) {
|
|
174
|
+
return this.pendingConnection;
|
|
175
|
+
}
|
|
176
|
+
this.pendingConnection = this.createQueueConnection();
|
|
177
|
+
try {
|
|
178
|
+
return await this.pendingConnection;
|
|
179
|
+
} finally {
|
|
180
|
+
this.pendingConnection = void 0;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
async createQueueConnection() {
|
|
184
|
+
let connection;
|
|
185
|
+
try {
|
|
186
|
+
connection = await amqp.connect(libConfig.RABBITMQ_URL);
|
|
187
|
+
const channel = await connection.createChannel();
|
|
188
|
+
const clearChannel = () => {
|
|
189
|
+
if (this.queueChannel === channel) {
|
|
190
|
+
this.queueChannel = void 0;
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
channel.on("error", (error) => {
|
|
194
|
+
clearChannel();
|
|
195
|
+
console.error("RabbitmqRepo channel error:", error);
|
|
196
|
+
});
|
|
197
|
+
channel.once("close", clearChannel);
|
|
198
|
+
connection.on("error", (error) => {
|
|
199
|
+
clearChannel();
|
|
200
|
+
console.error("RabbitmqRepo connection error:", error);
|
|
201
|
+
});
|
|
202
|
+
connection.once("close", clearChannel);
|
|
203
|
+
console.log("RabbitmqRepo connected to queue successfully...");
|
|
204
|
+
this.closeConnection(channel, connection);
|
|
205
|
+
this.queueChannel = channel;
|
|
206
|
+
return channel;
|
|
207
|
+
} catch (error) {
|
|
208
|
+
if (connection) {
|
|
209
|
+
await connection.close().catch((closeError) => {
|
|
210
|
+
console.error("RabbitmqRepo error closing failed connection:", closeError);
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
console.error("RabbitmqRepo error creating connection:", error);
|
|
214
|
+
return void 0;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Closes the channel and connection when the application shuts down.
|
|
219
|
+
*/
|
|
220
|
+
closeConnection(channel, connection) {
|
|
221
|
+
let isClosing = false;
|
|
222
|
+
const close = async (signal) => {
|
|
223
|
+
if (isClosing) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
isClosing = true;
|
|
227
|
+
try {
|
|
228
|
+
try {
|
|
229
|
+
await channel.close();
|
|
230
|
+
} finally {
|
|
231
|
+
await connection.close();
|
|
232
|
+
}
|
|
233
|
+
console.log(`RabbitmqRepo queue connection closed after ${signal}`);
|
|
234
|
+
} catch (error) {
|
|
235
|
+
console.error("RabbitmqRepo error closing connection:", error);
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
process.once("SIGINT", close);
|
|
239
|
+
process.once("SIGTERM", close);
|
|
240
|
+
connection.once("close", () => {
|
|
241
|
+
process.removeListener("SIGINT", close);
|
|
242
|
+
process.removeListener("SIGTERM", close);
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Establishes the shared RabbitMQ connection during application startup.
|
|
247
|
+
*
|
|
248
|
+
* @example
|
|
249
|
+
* ```ts
|
|
250
|
+
* await rabbitmqRepo.establishQueueConnection();
|
|
251
|
+
* ```
|
|
252
|
+
*/
|
|
253
|
+
async establishQueueConnection() {
|
|
254
|
+
const channel = await this.getQueueConnection();
|
|
255
|
+
if (!channel) {
|
|
256
|
+
throw new Error("No channel found.");
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
async connectQueueToExchange(channel, exchange, queueName, routingKey, exchangeType) {
|
|
260
|
+
await channel.assertExchange(exchange, exchangeType);
|
|
261
|
+
const q = await channel.assertQueue(queueName, { durable: true, autoDelete: false });
|
|
262
|
+
await channel.bindQueue(q.queue, exchange, routingKey);
|
|
263
|
+
return q;
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Publishes a typed payload as JSON using the supplied exchange and routing key.
|
|
267
|
+
*
|
|
268
|
+
* @example
|
|
269
|
+
* ```ts
|
|
270
|
+
* await rabbitmqRepo.publishMessageToQueue<EmailPayload>({
|
|
271
|
+
* exchange: 'notifications',
|
|
272
|
+
* routingKey: 'email',
|
|
273
|
+
* message: { receiverEmail, username, verifyLink, resetLink },
|
|
274
|
+
* logMessage: 'Email notification published',
|
|
275
|
+
* });
|
|
276
|
+
* ```
|
|
277
|
+
*/
|
|
278
|
+
async publishMessageToQueue(args) {
|
|
279
|
+
const serializedMessage = JSON.stringify(args.message);
|
|
280
|
+
if (serializedMessage === void 0) {
|
|
281
|
+
throw new Error("Message must be JSON serializable.");
|
|
282
|
+
}
|
|
283
|
+
const channel = await this.getQueueConnection();
|
|
284
|
+
const { exchange, routingKey, logMessage, exchangeType } = args;
|
|
285
|
+
if (!channel) {
|
|
286
|
+
throw new Error("No channel found.");
|
|
287
|
+
}
|
|
288
|
+
await channel.assertExchange(exchange, exchangeType || "direct");
|
|
289
|
+
channel.publish(exchange, routingKey, Buffer.from(serializedMessage));
|
|
290
|
+
if (logMessage) {
|
|
291
|
+
console.log(logMessage);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Consumes JSON messages and acknowledges them after the callback succeeds.
|
|
296
|
+
* Prefetch defaults to 10 when omitted.
|
|
297
|
+
*
|
|
298
|
+
* @example
|
|
299
|
+
* ```ts
|
|
300
|
+
* await rabbitmqRepo.consumeMessagesFromQueue<EmailPayload>({
|
|
301
|
+
* exchange: 'notifications',
|
|
302
|
+
* queueName: 'email-queue',
|
|
303
|
+
* routingKey: 'email',
|
|
304
|
+
* prefetch: 5,
|
|
305
|
+
* callback: async ({ receiverEmail, username, verifyLink, resetLink }) => {
|
|
306
|
+
* // Process the email.
|
|
307
|
+
* },
|
|
308
|
+
* });
|
|
309
|
+
* ```
|
|
310
|
+
*/
|
|
311
|
+
async consumeMessagesFromQueue(args) {
|
|
312
|
+
const { exchange, queueName, routingKey, callback } = args;
|
|
313
|
+
const prefetch = args.prefetch ?? 10;
|
|
314
|
+
if (!Number.isInteger(prefetch) || prefetch < 1 || prefetch > 65535) {
|
|
315
|
+
throw new Error("Prefetch must be an integer between 1 and 65535.");
|
|
316
|
+
}
|
|
317
|
+
const channel = args.channel ?? await this.getQueueConnection();
|
|
318
|
+
if (!channel) {
|
|
319
|
+
throw new Error("No channel found.");
|
|
320
|
+
}
|
|
321
|
+
const q = await this.connectQueueToExchange(channel, exchange, queueName, routingKey, args.exchangeType ?? "direct");
|
|
322
|
+
await channel.prefetch(prefetch, false);
|
|
323
|
+
await channel.consume(q.queue, async (message) => {
|
|
324
|
+
if (!message) {
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
try {
|
|
328
|
+
let payload;
|
|
329
|
+
try {
|
|
330
|
+
payload = JSON.parse(message.content.toString());
|
|
331
|
+
} catch (error) {
|
|
332
|
+
console.error("RabbitmqRepo invalid JSON message:", error);
|
|
333
|
+
channel.nack(message, false, false);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
try {
|
|
337
|
+
await callback(payload);
|
|
338
|
+
} catch (error) {
|
|
339
|
+
console.error("RabbitmqRepo message callback failed:", error);
|
|
340
|
+
channel.nack(message, false, true);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
channel.ack(message);
|
|
344
|
+
} catch (error) {
|
|
345
|
+
console.error("RabbitmqRepo error settling message:", error);
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
var rabbitmqRepo = new RabbitmqRepo();
|
|
351
|
+
|
|
149
352
|
// src/middlewares/gateway.middleware.ts
|
|
150
353
|
import jwt from "jsonwebtoken";
|
|
151
354
|
function verifyGatewayRequest(serviceId) {
|
|
@@ -181,6 +384,7 @@ export {
|
|
|
181
384
|
LogLevelTypes,
|
|
182
385
|
elasticSearchRepo,
|
|
183
386
|
loggerRepo,
|
|
387
|
+
rabbitmqRepo,
|
|
184
388
|
registerErrorMiddleware,
|
|
185
389
|
registerSecurityMiddleware,
|
|
186
390
|
registerStandardMiddleware,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@samuelaj1/eazisend-service-kit",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.7",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"build": "tsup"
|
|
6
6
|
},
|
|
@@ -29,25 +29,27 @@
|
|
|
29
29
|
"license": "MIT",
|
|
30
30
|
"type": "module",
|
|
31
31
|
"devDependencies": {
|
|
32
|
+
"@types/amqplib": "^0.10.8",
|
|
33
|
+
"@types/compression": "^1.8.1",
|
|
34
|
+
"@types/cors": "^2.8.19",
|
|
32
35
|
"@types/express": "^5.0.6",
|
|
36
|
+
"@types/hpp": "^0.2.7",
|
|
33
37
|
"@types/jsonwebtoken": "^9.0.10",
|
|
34
38
|
"tsup": "^8.5.1",
|
|
35
|
-
"typescript": "^6.0.3"
|
|
36
|
-
"@types/cors": "^2.8.19",
|
|
37
|
-
"@types/hpp": "^0.2.7",
|
|
38
|
-
"@types/compression": "^1.8.1"
|
|
39
|
+
"typescript": "^6.0.3"
|
|
39
40
|
},
|
|
40
41
|
"dependencies": {
|
|
41
42
|
"@elastic/elasticsearch": "^9.4.2",
|
|
43
|
+
"amqplib": "^2.2.0",
|
|
44
|
+
"compression": "^1.8.1",
|
|
45
|
+
"cors": "^2.8.6",
|
|
42
46
|
"dotenv": "^17.4.2",
|
|
43
47
|
"express": "^5.2.1",
|
|
48
|
+
"helmet": "^8.3.0",
|
|
49
|
+
"hpp": "^0.2.3",
|
|
44
50
|
"http-status-codes": "^2.3.0",
|
|
45
51
|
"jsonwebtoken": "^9.0.3",
|
|
46
52
|
"winston": "^3.19.0",
|
|
47
|
-
"winston-elasticsearch": "^0.19.0"
|
|
48
|
-
"helmet": "^8.3.0",
|
|
49
|
-
"hpp": "^0.2.3",
|
|
50
|
-
"cors": "^2.8.6",
|
|
51
|
-
"compression": "^1.8.1"
|
|
53
|
+
"winston-elasticsearch": "^0.19.0"
|
|
52
54
|
}
|
|
53
55
|
}
|