Skip to content

Commit 73346d9

Browse files
committed
Release vintasend-pug@0.10.0
1 parent 106c21f commit 73346d9

6 files changed

Lines changed: 650 additions & 5 deletions

File tree

package.json

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "vintasend-pug",
3-
"version": "0.9.1",
3+
"version": "0.10.0",
44
"description": "",
55
"main": "dist/index.js",
66
"scripts": {
@@ -11,13 +11,17 @@
1111
"test:coverage": "vitest run --coverage"
1212
},
1313
"files": [
14-
"dist"
14+
"dist",
15+
"src/scripts/compile-pug-templates.ts"
1516
],
17+
"bin": {
18+
"compile-pug-templates": "dist/scripts/compile-pug-templates.js"
19+
},
1620
"author": "Hugo Bessa",
1721
"license": "MIT",
1822
"dependencies": {
1923
"pug": "^3.0.3",
20-
"vintasend": "^0.9.1"
24+
"vintasend": "^0.10.0"
2125
},
2226
"devDependencies": {
2327
"@types/pug": "^2.0.10",
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
import type { ContextGenerator } from 'vintasend/dist/services/notification-context-registry';
2+
import type { DatabaseNotification } from 'vintasend/dist/types/notification';
3+
import type { BaseLogger } from 'vintasend/dist/services/loggers/base-logger';
4+
import { PugInlineEmailTemplateRendererFactory, PugInlineEmailTemplateRenderer } from '../pug-inline-email-template-renderer';
5+
6+
type MockConfig = {
7+
ContextMap: { testContext: ContextGenerator };
8+
NotificationIdType: string;
9+
UserIdType: string;
10+
};
11+
12+
describe('PugInlineEmailTemplateRenderer', () => {
13+
let renderer: PugInlineEmailTemplateRenderer<MockConfig>;
14+
let mockNotification: DatabaseNotification<MockConfig>;
15+
16+
// Define template strings inline (plain text mode with interpolation)
17+
const templates = {
18+
'test-notification': `html
19+
head
20+
title Email
21+
body
22+
h1 Hello #{name}!
23+
p Your message: #{message}`,
24+
'test-subject': `| Welcome #{name}`,
25+
'invalid-template': `p= undefinedVariable.nonExistentProperty`,
26+
'invalid-subject': `| = undefinedVariable.nonExistentProperty`,
27+
};
28+
29+
beforeEach(() => {
30+
renderer = new PugInlineEmailTemplateRendererFactory<MockConfig>().create(templates);
31+
mockNotification = {
32+
id: '123',
33+
notificationType: 'EMAIL' as const,
34+
contextName: 'testContext',
35+
contextParameters: {},
36+
userId: '456',
37+
title: 'Test Notification',
38+
bodyTemplate: 'test-notification',
39+
subjectTemplate: 'test-subject',
40+
extraParams: {},
41+
contextUsed: null,
42+
adapterUsed: null,
43+
status: 'PENDING_SEND' as const,
44+
sentAt: null,
45+
readAt: null,
46+
sendAfter: new Date(),
47+
gitCommitSha: null,
48+
};
49+
});
50+
51+
it('should render email template with context', async () => {
52+
const context = {
53+
name: 'John',
54+
message: 'Hello World',
55+
};
56+
57+
const result = await renderer.render(mockNotification, context);
58+
59+
expect(result.subject).toBe('Welcome John');
60+
expect(result.body).toContain('Hello John!');
61+
expect(result.body).toContain('Your message: Hello World');
62+
});
63+
64+
it('should render email template from template content', async () => {
65+
const result = await renderer.renderFromTemplateContent(
66+
mockNotification,
67+
{
68+
subject: '| Welcome #{name}',
69+
body: 'p Hello #{name}!\np Message: #{message}',
70+
},
71+
{
72+
name: 'John',
73+
message: 'Hello World',
74+
},
75+
);
76+
77+
expect(result.subject).toBe('Welcome John');
78+
expect(result.body).toContain('Hello John!');
79+
expect(result.body).toContain('Message: Hello World');
80+
});
81+
82+
it('should throw when renderFromTemplateContent receives empty subject', async () => {
83+
await expect(
84+
renderer.renderFromTemplateContent(
85+
mockNotification,
86+
{
87+
subject: null,
88+
body: 'p Body',
89+
},
90+
{},
91+
),
92+
).rejects.toThrow('Subject template is required');
93+
});
94+
95+
it('should throw error when subject template is missing', async () => {
96+
const notification = {
97+
...mockNotification,
98+
id: 'test-notification',
99+
bodyTemplate: 'test-notification',
100+
subjectTemplate: null,
101+
userId: 'user123',
102+
};
103+
104+
await expect(renderer.render(notification, {})).rejects.toThrow('Subject template is required');
105+
});
106+
107+
it('should handle empty context', async () => {
108+
const notification = {
109+
...mockNotification,
110+
id: 'test-notification',
111+
bodyTemplate: 'test-notification',
112+
subjectTemplate: 'test-subject',
113+
userId: 'user123',
114+
};
115+
116+
const result = await renderer.render(notification, {});
117+
118+
expect(result.subject).toBe('Welcome ');
119+
expect(result.body).toContain('Hello !');
120+
expect(result.body).toContain('Your message: ');
121+
});
122+
123+
it('should throw error when template key not found', async () => {
124+
const notification = {
125+
...mockNotification,
126+
subjectTemplate: 'non-existent-subject',
127+
};
128+
129+
await expect(renderer.render(notification, {})).rejects.toThrow('Subject template "non-existent-subject" not found in templates');
130+
});
131+
132+
it('should throw error when body template not found', async () => {
133+
const notification = {
134+
...mockNotification,
135+
bodyTemplate: 'non-existent-body',
136+
};
137+
138+
await expect(renderer.render(notification, {})).rejects.toThrow('Body template "non-existent-body" not found in templates');
139+
});
140+
141+
it('should handle template runtime errors', async () => {
142+
const notification = {
143+
...mockNotification,
144+
bodyTemplate: 'invalid-template',
145+
subjectTemplate: 'invalid-subject',
146+
};
147+
148+
await expect(renderer.render(notification, { undefinedVariable: undefined })).rejects.toThrow();
149+
});
150+
151+
it('should throw error when bodyTemplate is empty', async () => {
152+
const notification = {
153+
...mockNotification,
154+
bodyTemplate: '',
155+
};
156+
157+
await expect(renderer.render(notification, { name: 'Test' })).rejects.toThrow('Body template is required');
158+
});
159+
160+
it('should throw error when subjectTemplate is empty', async () => {
161+
const notification = {
162+
...mockNotification,
163+
subjectTemplate: '',
164+
};
165+
166+
await expect(renderer.render(notification, { name: 'Test' })).rejects.toThrow('Subject template is required');
167+
});
168+
169+
it('should create renderer with empty templates object', () => {
170+
const emptyRenderer = new PugInlineEmailTemplateRendererFactory<MockConfig>().create({});
171+
expect(emptyRenderer).toBeDefined();
172+
});
173+
174+
it('should support logger injection', () => {
175+
const mockLogger: BaseLogger = {
176+
info: vi.fn(),
177+
error: vi.fn(),
178+
warn: vi.fn(),
179+
};
180+
181+
renderer.injectLogger(mockLogger);
182+
183+
// biome-ignore lint/complexity/useLiteralKeys: accessing private attribute
184+
expect((renderer as any).logger).toBe(mockLogger);
185+
});
186+
187+
it('should use logger when rendering fails', async () => {
188+
const mockLogger: BaseLogger = {
189+
info: vi.fn(),
190+
error: vi.fn(),
191+
warn: vi.fn(),
192+
};
193+
194+
renderer.injectLogger(mockLogger);
195+
196+
const notification = {
197+
...mockNotification,
198+
bodyTemplate: 'invalid-template',
199+
subjectTemplate: 'invalid-subject',
200+
};
201+
202+
await expect(renderer.render(notification, { undefinedVariable: undefined })).rejects.toThrow();
203+
expect(mockLogger.error).toHaveBeenCalledWith(expect.stringContaining('[PugInlineEmailTemplateRenderer] Error rendering body template'));
204+
});
205+
});

src/index.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
1-
export { PugEmailTemplateRendererFactory } from './pug-email-template-renderer';
2-
export type { PugEmailTemplateRenderer } from './pug-email-template-renderer';
1+
export { PugEmailTemplateRendererFactory } from './pug-email-template-renderer';
2+
export type { PugEmailTemplateRenderer } from './pug-email-template-renderer';
3+
export { PugInlineEmailTemplateRendererFactory } from './pug-inline-email-template-renderer';
4+
export type { PugInlineEmailTemplateRenderer } from './pug-inline-email-template-renderer';
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import * as pug from 'pug';
2+
import { BaseEmailTemplateRenderer } from 'vintasend';
3+
import type { EmailTemplateContent } from 'vintasend/dist/services/notification-template-renderers/base-email-template-renderer';
4+
import type { JsonObject } from 'vintasend/dist/types/json-values';
5+
import type { DatabaseNotification } from 'vintasend/dist/types/notification';
6+
import type { BaseNotificationTypeConfig } from 'vintasend/dist/types/notification-type-config';
7+
import type { BaseLogger } from 'vintasend/dist/services/loggers/base-logger';
8+
9+
10+
/**
11+
* Custom email template renderer that compiles Pug templates from strings
12+
* instead of reading from file paths.
13+
*
14+
* This is necessary for bot deployments where templates are embedded as constants
15+
* rather than separate files.
16+
*/
17+
export class PugInlineEmailTemplateRenderer<Config extends BaseNotificationTypeConfig>
18+
implements BaseEmailTemplateRenderer<Config>
19+
{
20+
private templates: Record<string, string>;
21+
private logger: BaseLogger | null = null;
22+
23+
constructor(generatedTemplates: Record<string, string>) {
24+
this.templates = generatedTemplates;
25+
}
26+
27+
/**
28+
* Inject logger (called by VintaSend when logger exists)
29+
*/
30+
injectLogger(logger: BaseLogger): void {
31+
this.logger = logger;
32+
}
33+
34+
async render(
35+
notification: DatabaseNotification<Config>,
36+
context: JsonObject
37+
): Promise<{ subject: string; body: string }> {
38+
// Check if body template is provided
39+
const bodyTemplateKey = notification.bodyTemplate;
40+
if (!bodyTemplateKey) {
41+
throw new Error('Body template is required');
42+
}
43+
if (!(bodyTemplateKey in this.templates)) {
44+
throw new Error(`Body template "${bodyTemplateKey}" not found in templates`);
45+
}
46+
47+
// Check if subject template is provided
48+
const subjectTemplateKey = notification.subjectTemplate;
49+
if (!subjectTemplateKey) {
50+
throw new Error('Subject template is required');
51+
}
52+
if (!(subjectTemplateKey in this.templates)) {
53+
throw new Error(`Subject template "${subjectTemplateKey}" not found in templates`);
54+
}
55+
56+
let body: string;
57+
try {
58+
// Compile and render the body template from string
59+
const bodyTemplate = pug.compile(this.templates[bodyTemplateKey]);
60+
body = bodyTemplate(context);
61+
} catch (error) {
62+
if (this.logger) {
63+
this.logger.error('[PugInlineEmailTemplateRenderer] Error rendering body template');
64+
}
65+
throw error;
66+
}
67+
68+
let subject: string;
69+
try {
70+
// Compile and render the subject template from string
71+
const subjectTemplate = pug.compile(this.templates[subjectTemplateKey]);
72+
subject = subjectTemplate(context);
73+
} catch (error) {
74+
if (this.logger) {
75+
this.logger.error('[PugInlineEmailTemplateRenderer] Error rendering subject template');
76+
}
77+
throw error;
78+
}
79+
80+
return { subject, body };
81+
}
82+
83+
async renderFromTemplateContent(
84+
notification: DatabaseNotification<Config>,
85+
templateContent: EmailTemplateContent,
86+
context: JsonObject,
87+
): Promise<{ subject: string; body: string }> {
88+
this.logger?.info(
89+
`[PugInlineEmailTemplateRenderer] Rendering template from content for notification ${notification.id}`,
90+
);
91+
92+
let body: string;
93+
try {
94+
const bodyTemplate = pug.compile(templateContent.body);
95+
body = bodyTemplate(context);
96+
} catch (error) {
97+
if (this.logger) {
98+
this.logger.error('[PugInlineEmailTemplateRenderer] Error rendering body template content');
99+
}
100+
throw error;
101+
}
102+
103+
if (!templateContent.subject) {
104+
throw new Error('Subject template is required');
105+
}
106+
107+
let subject: string;
108+
try {
109+
const subjectTemplate = pug.compile(templateContent.subject);
110+
subject = subjectTemplate(context);
111+
} catch (error) {
112+
if (this.logger) {
113+
this.logger.error('[PugInlineEmailTemplateRenderer] Error rendering subject template content');
114+
}
115+
throw error;
116+
}
117+
118+
return { subject, body };
119+
}
120+
}
121+
122+
export class PugInlineEmailTemplateRendererFactory<Config extends BaseNotificationTypeConfig> {
123+
create(generatedTemplates: Record<string, string>): PugInlineEmailTemplateRenderer<Config> {
124+
return new PugInlineEmailTemplateRenderer<Config>(generatedTemplates);
125+
}
126+
}

0 commit comments

Comments
 (0)