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.34",
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-ses": "^3.862.0",
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 { SESClient, SendRawEmailCommand } from '@aws-sdk/client-ses'
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 SESClient({
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
- ses: sesClient,
74
- aws: { SendRawEmailCommand },
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<void>}
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<void>,
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<void>}
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
- vi.mock('@aws-sdk/client-ses', () => {
30
+ // ---------------- Mock AWS SESv2 ----------------
31
+ vi.mock('@aws-sdk/client-sesv2', () => {
30
32
  const mockSesInstance = { mocked: true }
31
- const SESClient = vi.fn(() => mockSesInstance)
32
- globalThis.__mockedSesClient__ = SESClient
33
- globalThis.__mockedSesInstance__ = mockSesInstance
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
- SESClient,
36
- SendRawEmailCommand: 'mocked-command',
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
- it('should create ses transport with credentials', () => {
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: 'us-west-2',
126
+ region: 'eu-central-1',
114
127
  }
115
128
 
116
129
  const transport = TransportFactory.create(config)
117
130
 
118
- expect(globalThis.__mockedSesClient__).toHaveBeenCalledWith({
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
- ses: globalThis.__mockedSesInstance__,
130
- aws: { SendRawEmailCommand: 'mocked-command' },
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 ses transport with defaultProvider fallback', () => {
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.__mockedSesClient__).toHaveBeenCalledWith({
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
- ses: globalThis.__mockedSesInstance__,
155
- aws: { SendRawEmailCommand: 'mocked-command' },
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' })