core-services-sdk 1.3.0 → 1.3.1
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.1",
|
|
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'
|
|
@@ -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,97 @@
|
|
|
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
|
+
return {
|
|
8
|
+
default: {
|
|
9
|
+
createTransport: vi.fn(() => ({ type: 'mock-transport' })),
|
|
10
|
+
},
|
|
11
|
+
}
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
vi.mock('aws-sdk', async () => {
|
|
15
|
+
const actual = await vi.importActual('aws-sdk')
|
|
16
|
+
return {
|
|
17
|
+
...actual,
|
|
18
|
+
SES: vi.fn().mockImplementation(() => 'mocked-ses'),
|
|
19
|
+
}
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
vi.mock('nodemailer-sendgrid-transport', () => ({
|
|
23
|
+
__esModule: true,
|
|
24
|
+
default: vi.fn().mockImplementation((opts) => {
|
|
25
|
+
const transport = { sendgrid: true, options: opts }
|
|
26
|
+
return transport
|
|
27
|
+
}),
|
|
28
|
+
}))
|
|
29
|
+
|
|
30
|
+
import { TransportFactory } from '../src/mailer/transport.factory.js'
|
|
31
|
+
describe('TransportFactory', () => {
|
|
32
|
+
it('should create smtp transport', () => {
|
|
33
|
+
const config = {
|
|
34
|
+
type: 'smtp',
|
|
35
|
+
host: 'smtp.example.com',
|
|
36
|
+
port: 587,
|
|
37
|
+
secure: false,
|
|
38
|
+
auth: { user: 'user', pass: 'pass' },
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const transport = TransportFactory.create(config)
|
|
42
|
+
expect(nodemailer.createTransport).toHaveBeenCalledWith({
|
|
43
|
+
host: config.host,
|
|
44
|
+
port: config.port,
|
|
45
|
+
secure: config.secure,
|
|
46
|
+
auth: config.auth,
|
|
47
|
+
})
|
|
48
|
+
expect(transport.type).toBe('mock-transport')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('should create gmail transport', () => {
|
|
52
|
+
const config = {
|
|
53
|
+
type: 'gmail',
|
|
54
|
+
auth: { user: 'user@gmail.com', pass: 'pass' },
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const transport = TransportFactory.create(config)
|
|
58
|
+
expect(nodemailer.createTransport).toHaveBeenCalledWith({
|
|
59
|
+
service: 'gmail',
|
|
60
|
+
auth: config.auth,
|
|
61
|
+
})
|
|
62
|
+
expect(transport.type).toBe('mock-transport')
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('should create sendgrid transport', () => {
|
|
66
|
+
const config = {
|
|
67
|
+
type: 'sendgrid',
|
|
68
|
+
apiKey: 'SG.xxxx',
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const transport = TransportFactory.create(config)
|
|
72
|
+
expect(transport.sendgrid).toBe(true)
|
|
73
|
+
expect(transport.options.auth.api_key).toBe(config.apiKey)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('should create ses transport', () => {
|
|
77
|
+
const config = {
|
|
78
|
+
type: 'ses',
|
|
79
|
+
accessKeyId: 'AKIA...',
|
|
80
|
+
secretAccessKey: 'secret',
|
|
81
|
+
region: 'us-west-2',
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const transport = TransportFactory.create(config)
|
|
85
|
+
expect(nodemailer.createTransport).toHaveBeenCalled()
|
|
86
|
+
|
|
87
|
+
expect(nodemailer.createTransport.mock.calls[0][0]).toHaveProperty(
|
|
88
|
+
'SES.ses',
|
|
89
|
+
)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('should throw error for unsupported type', () => {
|
|
93
|
+
expect(() => {
|
|
94
|
+
TransportFactory.create({ type: 'unsupported' })
|
|
95
|
+
}).toThrow('Unsupported transport type: unsupported')
|
|
96
|
+
})
|
|
97
|
+
})
|