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.0",
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
+ })