Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

166 changes: 157 additions & 9 deletions src/services/__tests__/sendgrid.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,17 +87,165 @@ describe('SendGridService Integration Tests', () => {
});
});

describe('listTemplates', () => {
it('should return an array of templates', async () => {
const templates = await service.listTemplates();

expect(Array.isArray(templates)).toBe(true);
if (templates.length > 0) {
expect(templates[0]).toHaveProperty('id');
expect(templates[0]).toHaveProperty('name');
expect(templates[0]).toHaveProperty('generation');
describe('Template Management', () => {
let testTemplateId: string;
let testVersionId: string;

afterAll(async () => {
if (testTemplateId) {
try {
await service.deleteTemplate(testTemplateId);
} catch (error) {
console.error('Error cleaning up test template:', error);
}
}
});

it('should create a template', async () => {
const templateName = `Test Template ${new Date().getTime()}`;

const template = await service.createTemplate({
name: templateName,
generation: 'dynamic'
});

testTemplateId = template.id;
expect(template).toBeDefined();
expect(template.id).toBeDefined();
expect(template.name).toBe(templateName);
expect(template.generation).toBe('dynamic');
});

it('should update a template name', async () => {
const newName = `Updated Template ${new Date().getTime()}`;

const updatedTemplate = await service.updateTemplate(testTemplateId, {
name: newName
});

expect(updatedTemplate).toBeDefined();
expect(updatedTemplate.name).toBe(newName);
});

it('should duplicate a template', async () => {
const duplicateName = `Duplicate Template ${new Date().getTime()}`;

const duplicatedTemplate = await service.duplicateTemplate(testTemplateId, {
name: duplicateName
});

expect(duplicatedTemplate).toBeDefined();
expect(duplicatedTemplate.id).toBeDefined();
expect(duplicatedTemplate.id).not.toBe(testTemplateId);
expect(duplicatedTemplate.name).toBe(duplicateName);

// Clean up duplicated template
await service.deleteTemplate(duplicatedTemplate.id);
});

it('should list templates with pagination', async () => {
const templateResponse = await service.listTemplates({
generations: 'dynamic',
page_size: 10
});

expect(templateResponse).toBeDefined();
expect(Array.isArray(templateResponse.result)).toBe(true);
expect(templateResponse._metadata).toBeDefined();
});

it('should create a template version', async () => {
const versionName = `Test Version ${new Date().getTime()}`;

const version = await service.createTemplateVersion(testTemplateId, {
name: versionName,
subject: 'Test Subject {{name}}',
html_content: '<h1>Hello {{name}}</h1>',
plain_content: 'Hello {{name}}',
active: 1,
generate_plain_content: false
});

testVersionId = version.id;
expect(version).toBeDefined();
expect(version.id).toBeDefined();
expect(version.name).toBe(versionName);
expect(version.subject).toBe('Test Subject {{name}}');
expect(version.active).toBe(1);
});

it('should get a template version', async () => {
const version = await service.getTemplateVersion(testTemplateId, testVersionId);

expect(version).toBeDefined();
expect(version.id).toBe(testVersionId);
expect(version.template_id).toBe(testTemplateId);
});

it('should update a template version', async () => {
const updatedSubject = 'Updated Subject {{name}}';

const updatedVersion = await service.updateTemplateVersion(testTemplateId, testVersionId, {
name: `Updated Version ${new Date().getTime()}`,
subject: updatedSubject,
html_content: '<h1>Updated Hello {{name}}</h1>',
plain_content: 'Updated Hello {{name}}'
});

expect(updatedVersion).toBeDefined();
expect(updatedVersion.subject).toBe(updatedSubject);
});

it('should activate a template version', async () => {
// Create a second version
const newVersion = await service.createTemplateVersion(testTemplateId, {
name: `Version 2 ${new Date().getTime()}`,
subject: 'Version 2 Subject',
html_content: '<h1>Version 2</h1>',
plain_content: 'Version 2',
active: 0
});

// Activate the new version
const activatedVersion = await service.activateTemplateVersion(testTemplateId, newVersion.id);

expect(activatedVersion).toBeDefined();
expect(activatedVersion.active).toBe(1);

// Clean up
await service.deleteTemplateVersion(testTemplateId, newVersion.id);
});

it('should delete a template version', async () => {
// Create a version to delete
const versionToDelete = await service.createTemplateVersion(testTemplateId, {
name: `Delete Me ${new Date().getTime()}`,
subject: 'Delete Me',
html_content: '<h1>Delete Me</h1>',
plain_content: 'Delete Me'
});

// Delete the version
await service.deleteTemplateVersion(testTemplateId, versionToDelete.id);

// Verify it's deleted by trying to get it
try {
await service.getTemplateVersion(testTemplateId, versionToDelete.id);
throw new Error('Version was not deleted');
} catch (error: any) {
// Expect 404 error since version should be deleted
expect(error.code).toBe(404);
}
});

it('should get a template by ID', async () => {
const template = await service.getTemplate(testTemplateId);

expect(template).toBeDefined();
expect(template.id).toBe(testTemplateId);
expect(template.versions).toBeDefined();
expect(Array.isArray(template.versions)).toBe(true);
});
});

describe('getStats', () => {
Expand Down
122 changes: 85 additions & 37 deletions src/services/sendgrid.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Client } from '@sendgrid/client';
import sgMail from '@sendgrid/mail';
import { SendGridContact, SendGridList, SendGridTemplate, SendGridStats, SendGridSingleSend } from '../types/index.js';
import { SendGridContact, SendGridList, SendGridTemplate, SendGridStats, SendGridSingleSend, SendGridListTemplatesResponse, SendGridTemplateVersionCreateParams, SendGridTemplateVersion } from '../types/index.js';

export class SendGridService {
private client: Client;
Expand Down Expand Up @@ -155,61 +155,67 @@ export class SendGridService {
// Template Management
async createTemplate(params: {
name: string;
html_content: string;
plain_content: string;
subject: string;
generation?: 'legacy' | 'dynamic';
}): Promise<SendGridTemplate> {
const [response] = await this.client.request({
method: 'POST',
url: '/v3/templates',
body: {
name: params.name,
generation: 'dynamic'
generation: params.generation || 'dynamic'
}
});

const templateId = (response.body as { id: string }).id;

// Create the first version of the template
const [versionResponse] = await this.client.request({
method: 'POST',
url: `/v3/templates/${templateId}/versions`,
body: {
template_id: templateId,
name: `${params.name} v1`,
subject: params.subject,
html_content: params.html_content,
plain_content: params.plain_content,
active: 1
}
return response.body as SendGridTemplate;
}

async updateTemplate(templateId: string, params: {
name: string;
}): Promise<SendGridTemplate> {
const [response] = await this.client.request({
method: 'PATCH',
url: `/v3/templates/${templateId}`,
body: params
});
return response.body as SendGridTemplate;
}

return {
id: templateId,
name: params.name,
generation: 'dynamic',
updated_at: new Date().toISOString(),
versions: [{
id: (versionResponse.body as { id: string }).id,
template_id: templateId,
active: 1,
name: `${params.name} v1`,
html_content: params.html_content,
plain_content: params.plain_content,
subject: params.subject
}]
};
async duplicateTemplate(templateId: string, params: {
name?: string;
}): Promise<SendGridTemplate> {
const [response] = await this.client.request({
method: 'POST',
url: `/v3/templates/${templateId}`,
body: params
});
return response.body as SendGridTemplate;
}

async listTemplates(): Promise<SendGridTemplate[]> {
async listTemplates(params?: {
generations?: 'legacy' | 'dynamic' | 'legacy,dynamic';
page_size?: number;
page_token?: string;
}): Promise<SendGridListTemplatesResponse> {
const [response] = await this.client.request({
method: 'GET',
url: '/v3/templates',
qs: {
generations: 'dynamic'
generations: params?.generations || 'legacy',
page_size: params?.page_size,
page_token: params?.page_token
}
});
return ((response.body as { templates: SendGridTemplate[] }).templates || []);

// The SendGrid API returns templates directly as an array when using the v3/templates endpoint
// We need to wrap it in the expected structure
if (Array.isArray(response.body)) {
return {
result: response.body,
_metadata: response.headers['x-metadata'] ? JSON.parse(response.headers['x-metadata']) : undefined
};
}

return response.body as SendGridListTemplatesResponse;
}

async getTemplate(templateId: string): Promise<SendGridTemplate> {
Expand All @@ -227,6 +233,48 @@ export class SendGridService {
});
}

// Template Version Management
async createTemplateVersion(templateId: string, params: SendGridTemplateVersionCreateParams): Promise<SendGridTemplateVersion> {
const [response] = await this.client.request({
method: 'POST',
url: `/v3/templates/${templateId}/versions`,
body: params
});
return response.body as SendGridTemplateVersion;
}

async getTemplateVersion(templateId: string, versionId: string): Promise<SendGridTemplateVersion> {
const [response] = await this.client.request({
method: 'GET',
url: `/v3/templates/${templateId}/versions/${versionId}`
});
return response.body as SendGridTemplateVersion;
}

async updateTemplateVersion(templateId: string, versionId: string, params: SendGridTemplateVersionCreateParams): Promise<SendGridTemplateVersion> {
const [response] = await this.client.request({
method: 'PATCH',
url: `/v3/templates/${templateId}/versions/${versionId}`,
body: params
});
return response.body as SendGridTemplateVersion;
}

async deleteTemplateVersion(templateId: string, versionId: string): Promise<void> {
await this.client.request({
method: 'DELETE',
url: `/v3/templates/${templateId}/versions/${versionId}`
});
}

async activateTemplateVersion(templateId: string, versionId: string): Promise<SendGridTemplateVersion> {
const [response] = await this.client.request({
method: 'POST',
url: `/v3/templates/${templateId}/versions/${versionId}/activate`
});
return response.body as SendGridTemplateVersion;
}

// Email Validation
async validateEmail(email: string) {
const [response] = await this.client.request({
Expand Down
Loading