core-services-sdk 1.3.34 → 1.3.36
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "core-services-sdk",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.36",
|
|
4
4
|
"main": "src/index.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "types/index.d.ts",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"homepage": "https://github.com/haim-rubin/core-services-sdk#readme",
|
|
26
26
|
"description": "",
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@aws-sdk/client-
|
|
28
|
+
"@aws-sdk/client-sesv2": "^3.901.0",
|
|
29
29
|
"@aws-sdk/credential-provider-node": "^3.862.0",
|
|
30
30
|
"@sendgrid/mail": "^8.1.5",
|
|
31
31
|
"amqplib": "^0.10.8",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import nodemailer from 'nodemailer'
|
|
2
2
|
import sgMail from '@sendgrid/mail'
|
|
3
3
|
import { defaultProvider } from '@aws-sdk/credential-provider-node'
|
|
4
|
-
import {
|
|
4
|
+
import { SESv2Client, SendEmailCommand } from '@aws-sdk/client-sesv2'
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Factory for creating email transporters based on configuration.
|
|
@@ -57,7 +57,7 @@ export class TransportFactory {
|
|
|
57
57
|
return sgMail // Not a Nodemailer transport, but SendGrid's mail API
|
|
58
58
|
|
|
59
59
|
case 'ses': {
|
|
60
|
-
const sesClient = new
|
|
60
|
+
const sesClient = new SESv2Client({
|
|
61
61
|
region: config.region,
|
|
62
62
|
credentials:
|
|
63
63
|
config.accessKeyId && config.secretAccessKey
|
|
@@ -70,8 +70,8 @@ export class TransportFactory {
|
|
|
70
70
|
|
|
71
71
|
return nodemailer.createTransport({
|
|
72
72
|
SES: {
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
sesClient,
|
|
74
|
+
SendEmailCommand,
|
|
75
75
|
},
|
|
76
76
|
})
|
|
77
77
|
}
|
|
@@ -46,7 +46,7 @@ export const connectQueueService = async ({ host, log }) => {
|
|
|
46
46
|
* Creates a channel from a RabbitMQ connection.
|
|
47
47
|
*
|
|
48
48
|
* @param {{ host: string, log: import('pino').Logger }} options
|
|
49
|
-
* @returns {Promise<amqp.Channel>}
|
|
49
|
+
* @returns {Promise<{ channel: amqp.Channel, connection: amqp.Connection }>}
|
|
50
50
|
*/
|
|
51
51
|
export const createChannel = async ({ host, log }) => {
|
|
52
52
|
const t0 = Date.now()
|
|
@@ -65,7 +65,7 @@ export const createChannel = async ({ host, log }) => {
|
|
|
65
65
|
ms: Date.now() - t0,
|
|
66
66
|
})
|
|
67
67
|
|
|
68
|
-
return channel
|
|
68
|
+
return { channel, connection }
|
|
69
69
|
} catch (err) {
|
|
70
70
|
logger.error(err, {
|
|
71
71
|
event: 'error',
|
|
@@ -96,7 +96,7 @@ const parseMessage = (msgInfo) => {
|
|
|
96
96
|
* @param {boolean} [options.nackOnError=false] - Whether to nack the message on error (default: false)
|
|
97
97
|
* @param {number} [options.prefetch=1] - Max unacked messages per consumer (default: 1)
|
|
98
98
|
*
|
|
99
|
-
* @returns {Promise<
|
|
99
|
+
* @returns {Promise<string>} Returns the consumer tag for later cancellation
|
|
100
100
|
*/
|
|
101
101
|
export const subscribeToQueue = async ({
|
|
102
102
|
log,
|
|
@@ -112,7 +112,7 @@ export const subscribeToQueue = async ({
|
|
|
112
112
|
await channel.assertQueue(queue, { durable: true })
|
|
113
113
|
!!prefetch && (await channel.prefetch(prefetch))
|
|
114
114
|
|
|
115
|
-
channel.consume(queue, async (msgInfo) => {
|
|
115
|
+
const { consumerTag } = await channel.consume(queue, async (msgInfo) => {
|
|
116
116
|
if (!msgInfo) {
|
|
117
117
|
return
|
|
118
118
|
}
|
|
@@ -143,6 +143,9 @@ export const subscribeToQueue = async ({
|
|
|
143
143
|
return
|
|
144
144
|
}
|
|
145
145
|
})
|
|
146
|
+
|
|
147
|
+
logger.debug({ consumerTag }, 'consumer-started')
|
|
148
|
+
return consumerTag
|
|
146
149
|
} catch (err) {
|
|
147
150
|
logger.error(err, {
|
|
148
151
|
event: 'error',
|
|
@@ -164,12 +167,14 @@ export const subscribeToQueue = async ({
|
|
|
164
167
|
* queue: string,
|
|
165
168
|
* onReceive: (data: any, correlationId?: string) => Promise<void>,
|
|
166
169
|
* nackOnError?: boolean
|
|
167
|
-
* }) => Promise<
|
|
168
|
-
* channel: amqp.Channel
|
|
170
|
+
* }) => Promise<string>,
|
|
171
|
+
* channel: amqp.Channel,
|
|
172
|
+
* connection: amqp.Connection,
|
|
173
|
+
* close: () => Promise<void>
|
|
169
174
|
* }>}
|
|
170
175
|
*/
|
|
171
176
|
export const initializeQueue = async ({ host, log }) => {
|
|
172
|
-
const channel = await createChannel({ host, log })
|
|
177
|
+
const { channel, connection } = await createChannel({ host, log })
|
|
173
178
|
const logger = log.child({ op: 'initializeQueue', host: mask(host) })
|
|
174
179
|
|
|
175
180
|
/**
|
|
@@ -224,16 +229,45 @@ export const initializeQueue = async ({ host, log }) => {
|
|
|
224
229
|
* onReceive: (data: any, correlationId?: string) => Promise<void>,
|
|
225
230
|
* nackOnError?: boolean
|
|
226
231
|
* }} options
|
|
227
|
-
* @returns {Promise<
|
|
232
|
+
* @returns {Promise<string>} Returns the consumer tag for later cancellation
|
|
228
233
|
*/
|
|
229
234
|
const subscribe = async ({ queue, onReceive, nackOnError = false }) => {
|
|
230
235
|
return subscribeToQueue({ channel, queue, onReceive, log, nackOnError })
|
|
231
236
|
}
|
|
232
237
|
|
|
238
|
+
/**
|
|
239
|
+
* Gracefully closes the RabbitMQ channel and connection.
|
|
240
|
+
*
|
|
241
|
+
* @returns {Promise<void>}
|
|
242
|
+
*/
|
|
243
|
+
const close = async () => {
|
|
244
|
+
const t0 = Date.now()
|
|
245
|
+
const logChild = logger.child({ op: 'close' })
|
|
246
|
+
|
|
247
|
+
try {
|
|
248
|
+
logChild.debug('closing-channel-and-connection')
|
|
249
|
+
await channel.close()
|
|
250
|
+
await connection.close()
|
|
251
|
+
|
|
252
|
+
logChild.info({
|
|
253
|
+
event: 'ok',
|
|
254
|
+
ms: Date.now() - t0,
|
|
255
|
+
})
|
|
256
|
+
} catch (err) {
|
|
257
|
+
logChild.error(err, {
|
|
258
|
+
event: 'error',
|
|
259
|
+
ms: Date.now() - t0,
|
|
260
|
+
})
|
|
261
|
+
throw err
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
233
265
|
return {
|
|
234
266
|
channel,
|
|
267
|
+
connection,
|
|
235
268
|
publish,
|
|
236
269
|
subscribe,
|
|
270
|
+
close,
|
|
237
271
|
}
|
|
238
272
|
}
|
|
239
273
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// @ts-nocheck
|
|
2
2
|
import nodemailer from 'nodemailer'
|
|
3
|
-
|
|
4
3
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
5
4
|
|
|
5
|
+
// ---------------- Mock Nodemailer ----------------
|
|
6
6
|
vi.mock('nodemailer', () => {
|
|
7
7
|
const createTransport = vi.fn((options) => ({
|
|
8
8
|
type: 'mock-transport',
|
|
@@ -15,6 +15,7 @@ vi.mock('nodemailer', () => {
|
|
|
15
15
|
}
|
|
16
16
|
})
|
|
17
17
|
|
|
18
|
+
// ---------------- Mock SendGrid ----------------
|
|
18
19
|
vi.mock('@sendgrid/mail', () => {
|
|
19
20
|
const setApiKey = vi.fn()
|
|
20
21
|
const send = vi.fn()
|
|
@@ -26,23 +27,31 @@ vi.mock('@sendgrid/mail', () => {
|
|
|
26
27
|
}
|
|
27
28
|
})
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
// ---------------- Mock AWS SESv2 ----------------
|
|
31
|
+
vi.mock('@aws-sdk/client-sesv2', () => {
|
|
30
32
|
const mockSesInstance = { mocked: true }
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
const SESv2Client = vi.fn(() => mockSesInstance)
|
|
34
|
+
const SendEmailCommand = vi.fn(() => 'mocked-command')
|
|
35
|
+
|
|
36
|
+
globalThis.__mockedSesv2Client__ = SESv2Client
|
|
37
|
+
globalThis.__mockedSesv2Instance__ = mockSesInstance
|
|
38
|
+
globalThis.__mockedSendEmailCommand__ = SendEmailCommand
|
|
39
|
+
|
|
34
40
|
return {
|
|
35
|
-
|
|
36
|
-
|
|
41
|
+
__esModule: true,
|
|
42
|
+
SESv2Client,
|
|
43
|
+
SendEmailCommand,
|
|
37
44
|
}
|
|
38
45
|
})
|
|
39
46
|
|
|
47
|
+
// ---------------- Mock AWS Default Provider ----------------
|
|
40
48
|
vi.mock('@aws-sdk/credential-provider-node', () => {
|
|
41
49
|
const defaultProvider = vi.fn(() => 'mocked-default-provider')
|
|
42
50
|
globalThis.__mockedDefaultProvider__ = defaultProvider
|
|
43
|
-
return { defaultProvider }
|
|
51
|
+
return { __esModule: true, defaultProvider }
|
|
44
52
|
})
|
|
45
53
|
|
|
54
|
+
// ---------------- Import the module under test ----------------
|
|
46
55
|
import { TransportFactory } from '../../src/mailer/transport.factory.js'
|
|
47
56
|
|
|
48
57
|
describe('TransportFactory', () => {
|
|
@@ -50,6 +59,7 @@ describe('TransportFactory', () => {
|
|
|
50
59
|
vi.clearAllMocks()
|
|
51
60
|
})
|
|
52
61
|
|
|
62
|
+
// ---------- SMTP ----------
|
|
53
63
|
it('should create smtp transport', () => {
|
|
54
64
|
const config = {
|
|
55
65
|
type: 'smtp',
|
|
@@ -70,6 +80,7 @@ describe('TransportFactory', () => {
|
|
|
70
80
|
expect(transport.type).toBe('mock-transport')
|
|
71
81
|
})
|
|
72
82
|
|
|
83
|
+
// ---------- GMAIL ----------
|
|
73
84
|
it('should create gmail transport', () => {
|
|
74
85
|
const config = {
|
|
75
86
|
type: 'gmail',
|
|
@@ -85,6 +96,7 @@ describe('TransportFactory', () => {
|
|
|
85
96
|
expect(transport.type).toBe('mock-transport')
|
|
86
97
|
})
|
|
87
98
|
|
|
99
|
+
// ---------- SENDGRID ----------
|
|
88
100
|
it('should create sendgrid transport', () => {
|
|
89
101
|
const config = {
|
|
90
102
|
type: 'sendgrid',
|
|
@@ -105,17 +117,18 @@ describe('TransportFactory', () => {
|
|
|
105
117
|
}).toThrow('Missing SendGrid API key')
|
|
106
118
|
})
|
|
107
119
|
|
|
108
|
-
|
|
120
|
+
// ---------- SESv2 ----------
|
|
121
|
+
it('should create sesv2 transport with explicit credentials', () => {
|
|
109
122
|
const config = {
|
|
110
123
|
type: 'ses',
|
|
111
124
|
accessKeyId: 'AKIA...',
|
|
112
125
|
secretAccessKey: 'secret',
|
|
113
|
-
region: '
|
|
126
|
+
region: 'eu-central-1',
|
|
114
127
|
}
|
|
115
128
|
|
|
116
129
|
const transport = TransportFactory.create(config)
|
|
117
130
|
|
|
118
|
-
expect(globalThis.
|
|
131
|
+
expect(globalThis.__mockedSesv2Client__).toHaveBeenCalledWith({
|
|
119
132
|
region: config.region,
|
|
120
133
|
credentials: {
|
|
121
134
|
accessKeyId: config.accessKeyId,
|
|
@@ -126,15 +139,15 @@ describe('TransportFactory', () => {
|
|
|
126
139
|
const args = nodemailer.createTransport.mock.calls[0][0]
|
|
127
140
|
expect(args).toEqual({
|
|
128
141
|
SES: {
|
|
129
|
-
|
|
130
|
-
|
|
142
|
+
sesClient: globalThis.__mockedSesv2Instance__,
|
|
143
|
+
SendEmailCommand: globalThis.__mockedSendEmailCommand__,
|
|
131
144
|
},
|
|
132
145
|
})
|
|
133
146
|
|
|
134
147
|
expect(transport.type).toBe('mock-transport')
|
|
135
148
|
})
|
|
136
149
|
|
|
137
|
-
it('should create
|
|
150
|
+
it('should create sesv2 transport with defaultProvider fallback', () => {
|
|
138
151
|
const config = {
|
|
139
152
|
type: 'ses',
|
|
140
153
|
region: 'us-east-1',
|
|
@@ -143,7 +156,7 @@ describe('TransportFactory', () => {
|
|
|
143
156
|
const transport = TransportFactory.create(config)
|
|
144
157
|
|
|
145
158
|
expect(globalThis.__mockedDefaultProvider__).toHaveBeenCalled()
|
|
146
|
-
expect(globalThis.
|
|
159
|
+
expect(globalThis.__mockedSesv2Client__).toHaveBeenCalledWith({
|
|
147
160
|
region: config.region,
|
|
148
161
|
credentials: 'mocked-default-provider',
|
|
149
162
|
})
|
|
@@ -151,14 +164,15 @@ describe('TransportFactory', () => {
|
|
|
151
164
|
const args = nodemailer.createTransport.mock.calls[0][0]
|
|
152
165
|
expect(args).toEqual({
|
|
153
166
|
SES: {
|
|
154
|
-
|
|
155
|
-
|
|
167
|
+
sesClient: globalThis.__mockedSesv2Instance__,
|
|
168
|
+
SendEmailCommand: globalThis.__mockedSendEmailCommand__,
|
|
156
169
|
},
|
|
157
170
|
})
|
|
158
171
|
|
|
159
172
|
expect(transport.type).toBe('mock-transport')
|
|
160
173
|
})
|
|
161
174
|
|
|
175
|
+
// ---------- INVALID ----------
|
|
162
176
|
it('should throw error for unsupported type', () => {
|
|
163
177
|
expect(() => {
|
|
164
178
|
TransportFactory.create({ type: 'invalid' })
|