core-services-sdk 1.3.0 → 1.3.2
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.2",
|
|
4
4
|
"main": "src/index.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"amqplib": "^0.10.8",
|
|
23
23
|
"aws-sdk": "^2.1692.0",
|
|
24
|
+
"dot": "^1.1.3",
|
|
24
25
|
"http-status": "^2.1.0",
|
|
25
26
|
"mongodb": "^6.17.0",
|
|
26
27
|
"node-fetch": "^3.3.2",
|
package/src/index.js
CHANGED
|
@@ -8,3 +8,8 @@ export { initMailer } from './mailer/index.js'
|
|
|
8
8
|
export { HttpError } from './http/HttpError.js'
|
|
9
9
|
export { Mailer } from './mailer/mailer.service.js'
|
|
10
10
|
export { TransportFactory } from './mailer/transport.factory.js'
|
|
11
|
+
export {
|
|
12
|
+
isItFile,
|
|
13
|
+
loadTemplates,
|
|
14
|
+
getTemplateContent,
|
|
15
|
+
} from './templates/template-loader.js'
|
package/src/rabbit-mq/index.js
CHANGED
|
@@ -47,25 +47,30 @@ const parseMessage = (msgInfo) => {
|
|
|
47
47
|
|
|
48
48
|
/**
|
|
49
49
|
* Subscribes to a queue to receive messages.
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
* }
|
|
50
|
+
*
|
|
51
|
+
* @param {Object} options
|
|
52
|
+
* @param {import('amqplib').Channel} options.channel - RabbitMQ channel
|
|
53
|
+
* @param {string} options.queue - Queue name to subscribe to
|
|
54
|
+
* @param {(data: any) => Promise<void>} options.onReceive - Async handler for incoming message
|
|
55
|
+
* @param {Log} options.log - Logging utility
|
|
56
|
+
* @param {boolean} [options.nackOnError=false] - Whether to nack the message on error (default: false)
|
|
57
|
+
* @param {number} [options.prefetch=1] - Max unacked messages per consumer (default: 1)
|
|
58
|
+
*
|
|
57
59
|
* @returns {Promise<void>}
|
|
58
60
|
*/
|
|
59
61
|
export const subscribeToQueue = async ({
|
|
60
62
|
log,
|
|
61
63
|
queue,
|
|
62
64
|
channel,
|
|
65
|
+
prefetch = 1,
|
|
63
66
|
onReceive,
|
|
64
67
|
nackOnError = false,
|
|
65
68
|
}) => {
|
|
66
69
|
try {
|
|
67
70
|
await channel.assertQueue(queue, { durable: true })
|
|
68
71
|
|
|
72
|
+
!!prefetch && (await channel.prefetch(prefetch))
|
|
73
|
+
|
|
69
74
|
channel.consume(queue, async (msgInfo) => {
|
|
70
75
|
if (!msgInfo) return
|
|
71
76
|
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import dot from 'dot'
|
|
2
|
+
import { lstat, readFile } from 'fs/promises'
|
|
3
|
+
|
|
4
|
+
const { compile } = dot
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Check if the input is a file path.
|
|
8
|
+
* @param {string} filePathOrString - Either a string of template content or a file path to a template.
|
|
9
|
+
* @returns {Promise<boolean>} - True if it's a valid file path, false otherwise.
|
|
10
|
+
*/
|
|
11
|
+
export const isItFile = async (filePathOrString) => {
|
|
12
|
+
try {
|
|
13
|
+
const stats = await lstat(filePathOrString)
|
|
14
|
+
return stats.isFile()
|
|
15
|
+
} catch {
|
|
16
|
+
return false
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Get template content from string or file.
|
|
22
|
+
* @param {string} maybeFilePathOrString - Raw template string or path to template file.
|
|
23
|
+
* @returns {Promise<string>} - The resolved content of the template.
|
|
24
|
+
*/
|
|
25
|
+
export const getTemplateContent = async (maybeFilePathOrString) => {
|
|
26
|
+
return (await isItFile(maybeFilePathOrString))
|
|
27
|
+
? readFile(maybeFilePathOrString, 'utf-8')
|
|
28
|
+
: maybeFilePathOrString
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Load and compile templates using dot.js
|
|
33
|
+
*
|
|
34
|
+
* @param {Record<string, string>} templateSet - An object with keys as template names (e.g. "subject", "html") and values as either raw strings or file paths.
|
|
35
|
+
* @returns {Promise<Record<string, (params: Record<string, any>) => string>>} - A map of compiled template functions.
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* // Inline templates
|
|
39
|
+
* const templates = await loadTemplates({
|
|
40
|
+
* from: '"{{=it.name}}" <{{=it.email}}>',
|
|
41
|
+
* html: '<div>Hello {{=it.name}}, welcome aboard!</div>',
|
|
42
|
+
* subject: 'Welcome to {{=it.appName}}',
|
|
43
|
+
* })
|
|
44
|
+
*
|
|
45
|
+
* const result = {
|
|
46
|
+
* from: templates.from({ name: 'ChatGPT', email: 'chat@gpt.com' }),
|
|
47
|
+
* html: templates.html({ name: 'Haim' }),
|
|
48
|
+
* subject: templates.subject({ appName: 'Authenzify' }),
|
|
49
|
+
* }
|
|
50
|
+
* console.log(result)
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* // Template loaded from files
|
|
54
|
+
* const templates = await loadTemplates({
|
|
55
|
+
* from: './email-templates/from.ejs',
|
|
56
|
+
* html: './email-templates/body.html',
|
|
57
|
+
* subject: './email-templates/subject.ejs',
|
|
58
|
+
* })
|
|
59
|
+
*
|
|
60
|
+
* const output = templates.subject({ appName: 'MyApp' })
|
|
61
|
+
*/
|
|
62
|
+
export const loadTemplates = async (templateSet) => {
|
|
63
|
+
const entries = await Promise.all(
|
|
64
|
+
Object.entries(templateSet).map(async ([key, value]) => {
|
|
65
|
+
const content = await getTemplateContent(value)
|
|
66
|
+
return [key, compile(content)]
|
|
67
|
+
}),
|
|
68
|
+
)
|
|
69
|
+
return Object.fromEntries(entries)
|
|
70
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { join } from 'path'
|
|
2
|
+
import { tmpdir } from 'os'
|
|
3
|
+
import { writeFile, rm, mkdir } from 'fs/promises'
|
|
4
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
|
|
5
|
+
|
|
6
|
+
import { loadTemplates } from '../src/templates/template-loader.js'
|
|
7
|
+
|
|
8
|
+
const tempDir = join(tmpdir(), 'template-tests')
|
|
9
|
+
|
|
10
|
+
const createTempTemplateFile = async (filename, content) => {
|
|
11
|
+
const filePath = join(tempDir, filename)
|
|
12
|
+
await writeFile(filePath, content, 'utf-8')
|
|
13
|
+
return filePath
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe('loadTemplates with real files', () => {
|
|
17
|
+
let subjectPath, htmlPath
|
|
18
|
+
|
|
19
|
+
beforeAll(async () => {
|
|
20
|
+
await rm(tempDir, { recursive: true, force: true }).catch(() => {})
|
|
21
|
+
await mkdir(tempDir, { recursive: true })
|
|
22
|
+
|
|
23
|
+
subjectPath = await createTempTemplateFile(
|
|
24
|
+
'subject.dot',
|
|
25
|
+
'Hello {{=it.name}}',
|
|
26
|
+
)
|
|
27
|
+
htmlPath = await createTempTemplateFile(
|
|
28
|
+
'body.dot',
|
|
29
|
+
'<div>{{=it.body}}</div>',
|
|
30
|
+
)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
afterAll(async () => {
|
|
34
|
+
await rm(tempDir, { recursive: true, force: true })
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('should correctly read and compile templates from file paths', async () => {
|
|
38
|
+
const templates = await loadTemplates({
|
|
39
|
+
subject: subjectPath,
|
|
40
|
+
html: htmlPath,
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
const subjectResult = templates.subject({ name: 'Alice' })
|
|
44
|
+
const htmlResult = templates.html({ body: 'Welcome!' })
|
|
45
|
+
|
|
46
|
+
expect(subjectResult).toBe('Hello Alice')
|
|
47
|
+
expect(htmlResult).toBe('<div>Welcome!</div>')
|
|
48
|
+
})
|
|
49
|
+
})
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import dot from 'dot'
|
|
2
|
+
import * as fs from 'fs/promises'
|
|
3
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
4
|
+
|
|
5
|
+
import { loadTemplates } from '../src/templates/template-loader.js'
|
|
6
|
+
|
|
7
|
+
vi.mock('fs/promises')
|
|
8
|
+
|
|
9
|
+
describe('loadTemplates', () => {
|
|
10
|
+
// @ts-ignore
|
|
11
|
+
const mockCompile = vi.spyOn(dot, 'compile')
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
vi.resetAllMocks()
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('should compile inline template strings correctly', async () => {
|
|
18
|
+
const templates = {
|
|
19
|
+
subject: 'Hello {{=it.name}}',
|
|
20
|
+
html: '<div>{{=it.body}}</div>',
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const compiled = await loadTemplates(templates)
|
|
24
|
+
|
|
25
|
+
expect(typeof compiled.subject).toBe('function')
|
|
26
|
+
expect(typeof compiled.html).toBe('function')
|
|
27
|
+
|
|
28
|
+
// test compiled output
|
|
29
|
+
const result = compiled.subject({ name: 'Haim' })
|
|
30
|
+
expect(result).toBe('Hello Haim')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('should read from file path if input is a file', async () => {
|
|
34
|
+
const fakeFileContent = 'Welcome {{=it.username}}'
|
|
35
|
+
|
|
36
|
+
// @ts-ignore
|
|
37
|
+
fs.lstat.mockResolvedValueOnce({
|
|
38
|
+
isFile: () => true,
|
|
39
|
+
})
|
|
40
|
+
// @ts-ignore
|
|
41
|
+
fs.readFile.mockResolvedValueOnce(fakeFileContent)
|
|
42
|
+
|
|
43
|
+
const compiled = await loadTemplates({
|
|
44
|
+
subject: '/path/to/template.dot',
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
expect(fs.readFile).toHaveBeenCalledWith('/path/to/template.dot', 'utf-8')
|
|
48
|
+
expect(typeof compiled.subject).toBe('function')
|
|
49
|
+
expect(compiled.subject({ username: 'DotUser' })).toBe('Welcome DotUser')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('should treat string as content if path is invalid', async () => {
|
|
53
|
+
// @ts-ignore
|
|
54
|
+
fs.lstat.mockRejectedValueOnce(new Error('File not found'))
|
|
55
|
+
|
|
56
|
+
const compiled = await loadTemplates({
|
|
57
|
+
subject: 'Hi {{=it.x}}',
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
expect(typeof compiled.subject).toBe('function')
|
|
61
|
+
expect(compiled.subject({ x: 'there' })).toBe('Hi there')
|
|
62
|
+
})
|
|
63
|
+
})
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import aws from 'aws-sdk'
|
|
2
|
+
import nodemailer from 'nodemailer'
|
|
3
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
4
|
+
import sgTransport from 'nodemailer-sendgrid-transport'
|
|
5
|
+
|
|
6
|
+
vi.mock('nodemailer', () => {
|
|
7
|
+
const createTransport = vi.fn((options) => ({
|
|
8
|
+
type: 'mock-transport',
|
|
9
|
+
options,
|
|
10
|
+
}))
|
|
11
|
+
return {
|
|
12
|
+
__esModule: true,
|
|
13
|
+
default: { createTransport },
|
|
14
|
+
createTransport,
|
|
15
|
+
}
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
vi.mock('aws-sdk', () => {
|
|
19
|
+
return {
|
|
20
|
+
__esModule: true,
|
|
21
|
+
default: {
|
|
22
|
+
SES: vi.fn(() => 'mocked-ses'),
|
|
23
|
+
},
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
vi.mock('nodemailer-sendgrid-transport', () => {
|
|
28
|
+
return {
|
|
29
|
+
__esModule: true,
|
|
30
|
+
default: vi.fn((opts) => ({
|
|
31
|
+
sendgrid: true,
|
|
32
|
+
options: opts,
|
|
33
|
+
})),
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
import { TransportFactory } from '../src/mailer/transport.factory.js'
|
|
38
|
+
describe('TransportFactory', () => {
|
|
39
|
+
it('should create smtp transport', () => {
|
|
40
|
+
const config = {
|
|
41
|
+
type: 'smtp',
|
|
42
|
+
host: 'smtp.example.com',
|
|
43
|
+
port: 587,
|
|
44
|
+
secure: false,
|
|
45
|
+
auth: { user: 'user', pass: 'pass' },
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const transport = TransportFactory.create(config)
|
|
49
|
+
expect(nodemailer.createTransport).toHaveBeenCalledWith({
|
|
50
|
+
host: config.host,
|
|
51
|
+
port: config.port,
|
|
52
|
+
secure: config.secure,
|
|
53
|
+
auth: config.auth,
|
|
54
|
+
})
|
|
55
|
+
expect(transport.type).toBe('mock-transport')
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('should create gmail transport', () => {
|
|
59
|
+
const config = {
|
|
60
|
+
type: 'gmail',
|
|
61
|
+
auth: { user: 'user@gmail.com', pass: 'pass' },
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const transport = TransportFactory.create(config)
|
|
65
|
+
expect(nodemailer.createTransport).toHaveBeenCalledWith({
|
|
66
|
+
service: 'gmail',
|
|
67
|
+
auth: config.auth,
|
|
68
|
+
})
|
|
69
|
+
expect(transport.type).toBe('mock-transport')
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('should create sendgrid transport', () => {
|
|
73
|
+
const config = {
|
|
74
|
+
type: 'sendgrid',
|
|
75
|
+
apiKey: 'SG.xxxx',
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const transport = TransportFactory.create(config)
|
|
79
|
+
|
|
80
|
+
expect(nodemailer.createTransport).toHaveBeenCalled()
|
|
81
|
+
|
|
82
|
+
const args = nodemailer.createTransport.mock.calls[0][0]
|
|
83
|
+
|
|
84
|
+
expect(args).toEqual({
|
|
85
|
+
sendgrid: true,
|
|
86
|
+
options: {
|
|
87
|
+
auth: { api_key: config.apiKey },
|
|
88
|
+
},
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
expect(transport).toEqual({
|
|
92
|
+
type: 'mock-transport',
|
|
93
|
+
options: {
|
|
94
|
+
sendgrid: true,
|
|
95
|
+
options: {
|
|
96
|
+
auth: { api_key: config.apiKey },
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
})
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('should create ses transport', () => {
|
|
103
|
+
const config = {
|
|
104
|
+
type: 'ses',
|
|
105
|
+
accessKeyId: 'AKIA...',
|
|
106
|
+
secretAccessKey: 'secret',
|
|
107
|
+
region: 'us-west-2',
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
TransportFactory.create(config)
|
|
111
|
+
|
|
112
|
+
expect(nodemailer.createTransport).toHaveBeenCalled()
|
|
113
|
+
const args = nodemailer.createTransport.mock.calls[0][0]
|
|
114
|
+
|
|
115
|
+
expect(args).toEqual({
|
|
116
|
+
SES: {
|
|
117
|
+
ses: 'mocked-ses',
|
|
118
|
+
aws: expect.anything(),
|
|
119
|
+
},
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('should throw error for unsupported type', () => {
|
|
124
|
+
expect(() => {
|
|
125
|
+
TransportFactory.create({ type: 'unsupported' })
|
|
126
|
+
}).toThrow('Unsupported transport type: unsupported')
|
|
127
|
+
})
|
|
128
|
+
})
|