@bernierllc/email 1.0.0 → 1.1.0
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/README.md +76 -217
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -0
- package/dist/simple-email-service.d.ts +58 -0
- package/dist/simple-email-service.d.ts.map +1 -0
- package/dist/simple-email-service.js +416 -0
- package/dist/simple-email-service.js.map +1 -0
- package/dist/types.d.ts +311 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +33 -0
- package/dist/types.js.map +1 -0
- package/package.json +53 -22
- package/.eslintrc.json +0 -112
- package/.flake8 +0 -18
- package/.github/workflows/ci.yml +0 -300
- package/EXTRACTION_SUMMARY.md +0 -265
- package/IMPLEMENTATION_STATUS.md +0 -159
- package/OPEN_SOURCE_SETUP.md +0 -420
- package/PACKAGE_USAGE.md +0 -471
- package/examples/fastapi-example/main.py +0 -257
- package/examples/nextjs-example/next-env.d.ts +0 -13
- package/examples/nextjs-example/package.json +0 -26
- package/examples/nextjs-example/pages/admin/templates.tsx +0 -157
- package/examples/nextjs-example/tsconfig.json +0 -28
- package/packages/core/package.json +0 -70
- package/packages/core/rollup.config.js +0 -37
- package/packages/core/specification.md +0 -416
- package/packages/core/src/adapters/supabase.ts +0 -291
- package/packages/core/src/core/scheduler.ts +0 -356
- package/packages/core/src/core/template-manager.ts +0 -388
- package/packages/core/src/index.ts +0 -30
- package/packages/core/src/providers/base.ts +0 -104
- package/packages/core/src/providers/sendgrid.ts +0 -368
- package/packages/core/src/types/provider.ts +0 -91
- package/packages/core/src/types/scheduled.ts +0 -78
- package/packages/core/src/types/template.ts +0 -97
- package/packages/core/tsconfig.json +0 -23
- package/packages/python/README.md +0 -106
- package/packages/python/email_template_manager/__init__.py +0 -66
- package/packages/python/email_template_manager/config.py +0 -98
- package/packages/python/email_template_manager/core/magic_links.py +0 -245
- package/packages/python/email_template_manager/core/manager.py +0 -344
- package/packages/python/email_template_manager/core/scheduler.py +0 -473
- package/packages/python/email_template_manager/exceptions.py +0 -67
- package/packages/python/email_template_manager/models/magic_link.py +0 -59
- package/packages/python/email_template_manager/models/scheduled.py +0 -78
- package/packages/python/email_template_manager/models/template.py +0 -90
- package/packages/python/email_template_manager/providers/aws_ses.py +0 -44
- package/packages/python/email_template_manager/providers/base.py +0 -94
- package/packages/python/email_template_manager/providers/sendgrid.py +0 -325
- package/packages/python/email_template_manager/providers/smtp.py +0 -44
- package/packages/python/pyproject.toml +0 -133
- package/packages/python/setup.py +0 -93
- package/packages/python/specification.md +0 -930
- package/packages/react/README.md +0 -13
- package/packages/react/package.json +0 -105
- package/packages/react/rollup.config.js +0 -37
- package/packages/react/specification.md +0 -569
- package/packages/react/src/index.ts +0 -20
- package/packages/react/tsconfig.json +0 -24
- package/src/index.js +0 -1
- package/test_package.py +0 -125
|
@@ -1,416 +0,0 @@
|
|
|
1
|
-
# @email-template-manager/core - Core Library Specification
|
|
2
|
-
|
|
3
|
-
## Overview
|
|
4
|
-
|
|
5
|
-
The core library provides the foundational functionality for email template management, scheduling, and delivery. This package is framework-agnostic and can be used in any JavaScript/TypeScript environment.
|
|
6
|
-
|
|
7
|
-
## Core Data Models
|
|
8
|
-
|
|
9
|
-
### EmailTemplate
|
|
10
|
-
|
|
11
|
-
```typescript
|
|
12
|
-
interface EmailTemplate {
|
|
13
|
-
id: string;
|
|
14
|
-
name: string;
|
|
15
|
-
subject: string;
|
|
16
|
-
htmlBody: string;
|
|
17
|
-
textBody: string;
|
|
18
|
-
variables: TemplateVariable[];
|
|
19
|
-
categoryId?: string;
|
|
20
|
-
tags: string[];
|
|
21
|
-
isActive: boolean;
|
|
22
|
-
version: number;
|
|
23
|
-
createdAt: Date;
|
|
24
|
-
updatedAt: Date;
|
|
25
|
-
createdBy: string;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
interface TemplateVariable {
|
|
29
|
-
name: string;
|
|
30
|
-
type: "text" | "number" | "date" | "boolean" | "url";
|
|
31
|
-
description?: string;
|
|
32
|
-
defaultValue?: any;
|
|
33
|
-
required: boolean;
|
|
34
|
-
}
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
### ScheduledEmail
|
|
38
|
-
|
|
39
|
-
```typescript
|
|
40
|
-
interface ScheduledEmail {
|
|
41
|
-
id: string;
|
|
42
|
-
templateId: string;
|
|
43
|
-
recipientEmail: string;
|
|
44
|
-
recipientName?: string;
|
|
45
|
-
scheduledFor: Date;
|
|
46
|
-
triggerType: "immediate" | "relative" | "absolute";
|
|
47
|
-
triggerOffset?: number; // days before/after reference date
|
|
48
|
-
referenceDate?: Date;
|
|
49
|
-
variables: Record<string, any>;
|
|
50
|
-
status: "pending" | "sent" | "failed" | "cancelled" | "paused";
|
|
51
|
-
sentAt?: Date;
|
|
52
|
-
errorMessage?: string;
|
|
53
|
-
retryCount: number;
|
|
54
|
-
maxRetries: number;
|
|
55
|
-
metadata: Record<string, any>;
|
|
56
|
-
}
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
### MagicLink
|
|
60
|
-
|
|
61
|
-
```typescript
|
|
62
|
-
interface MagicLink {
|
|
63
|
-
id: string;
|
|
64
|
-
token: string;
|
|
65
|
-
email: string;
|
|
66
|
-
type: string;
|
|
67
|
-
payload: Record<string, any>;
|
|
68
|
-
expiresAt: Date;
|
|
69
|
-
usedAt?: Date;
|
|
70
|
-
isUsed: boolean;
|
|
71
|
-
metadata: Record<string, any>;
|
|
72
|
-
}
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
### EmailProvider Configuration
|
|
76
|
-
|
|
77
|
-
```typescript
|
|
78
|
-
interface EmailProviderConfig {
|
|
79
|
-
provider: "sendgrid" | "ses" | "mailgun" | "postmark" | "smtp";
|
|
80
|
-
apiKey?: string;
|
|
81
|
-
region?: string;
|
|
82
|
-
endpoint?: string;
|
|
83
|
-
fromEmail: string;
|
|
84
|
-
fromName: string;
|
|
85
|
-
replyTo?: string;
|
|
86
|
-
webhookUrl?: string;
|
|
87
|
-
webhookSecret?: string;
|
|
88
|
-
}
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
## Core Classes
|
|
92
|
-
|
|
93
|
-
### EmailTemplateManager
|
|
94
|
-
|
|
95
|
-
```typescript
|
|
96
|
-
class EmailTemplateManager {
|
|
97
|
-
constructor(config: EmailManagerConfig);
|
|
98
|
-
|
|
99
|
-
// Template CRUD operations
|
|
100
|
-
async createTemplate(
|
|
101
|
-
template: Partial<EmailTemplate>
|
|
102
|
-
): Promise<EmailTemplate>;
|
|
103
|
-
async getTemplate(id: string): Promise<EmailTemplate | null>;
|
|
104
|
-
async updateTemplate(
|
|
105
|
-
id: string,
|
|
106
|
-
updates: Partial<EmailTemplate>
|
|
107
|
-
): Promise<EmailTemplate>;
|
|
108
|
-
async deleteTemplate(id: string): Promise<boolean>;
|
|
109
|
-
async listTemplates(filters?: TemplateFilters): Promise<EmailTemplate[]>;
|
|
110
|
-
|
|
111
|
-
// Template rendering
|
|
112
|
-
async renderTemplate(
|
|
113
|
-
templateId: string,
|
|
114
|
-
variables: Record<string, any>
|
|
115
|
-
): Promise<RenderedTemplate>;
|
|
116
|
-
async previewTemplate(
|
|
117
|
-
template: EmailTemplate,
|
|
118
|
-
variables: Record<string, any>
|
|
119
|
-
): Promise<RenderedTemplate>;
|
|
120
|
-
|
|
121
|
-
// Variable validation
|
|
122
|
-
validateTemplateVariables(
|
|
123
|
-
template: EmailTemplate,
|
|
124
|
-
variables: Record<string, any>
|
|
125
|
-
): ValidationResult;
|
|
126
|
-
}
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
### EmailScheduler
|
|
130
|
-
|
|
131
|
-
```typescript
|
|
132
|
-
class EmailScheduler {
|
|
133
|
-
constructor(
|
|
134
|
-
templateManager: EmailTemplateManager,
|
|
135
|
-
emailProvider: EmailProvider
|
|
136
|
-
);
|
|
137
|
-
|
|
138
|
-
// Scheduling operations
|
|
139
|
-
async scheduleEmail(email: Partial<ScheduledEmail>): Promise<ScheduledEmail>;
|
|
140
|
-
async scheduleBatch(
|
|
141
|
-
emails: Partial<ScheduledEmail>[]
|
|
142
|
-
): Promise<ScheduledEmail[]>;
|
|
143
|
-
async cancelScheduledEmail(id: string): Promise<boolean>;
|
|
144
|
-
async pauseScheduledEmail(id: string): Promise<boolean>;
|
|
145
|
-
async resumeScheduledEmail(id: string): Promise<boolean>;
|
|
146
|
-
|
|
147
|
-
// Processing
|
|
148
|
-
async processPendingEmails(): Promise<ProcessingResult>;
|
|
149
|
-
async retryFailedEmails(): Promise<ProcessingResult>;
|
|
150
|
-
|
|
151
|
-
// Status tracking
|
|
152
|
-
async getScheduledEmail(id: string): Promise<ScheduledEmail | null>;
|
|
153
|
-
async listScheduledEmails(
|
|
154
|
-
filters?: SchedulingFilters
|
|
155
|
-
): Promise<ScheduledEmail[]>;
|
|
156
|
-
}
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
### MagicLinkManager
|
|
160
|
-
|
|
161
|
-
```typescript
|
|
162
|
-
class MagicLinkManager {
|
|
163
|
-
constructor(config: MagicLinkConfig);
|
|
164
|
-
|
|
165
|
-
// Magic link operations
|
|
166
|
-
async generateMagicLink(
|
|
167
|
-
email: string,
|
|
168
|
-
type: string,
|
|
169
|
-
payload: Record<string, any>,
|
|
170
|
-
expiresIn?: number
|
|
171
|
-
): Promise<MagicLink>;
|
|
172
|
-
async validateMagicLink(token: string): Promise<MagicLink | null>;
|
|
173
|
-
async useMagicLink(token: string): Promise<MagicLink>;
|
|
174
|
-
async revokeMagicLink(token: string): Promise<boolean>;
|
|
175
|
-
|
|
176
|
-
// Cleanup
|
|
177
|
-
async cleanupExpiredLinks(): Promise<number>;
|
|
178
|
-
}
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
### EmailProvider (Abstract Base)
|
|
182
|
-
|
|
183
|
-
```typescript
|
|
184
|
-
abstract class EmailProvider {
|
|
185
|
-
constructor(config: EmailProviderConfig);
|
|
186
|
-
|
|
187
|
-
abstract async sendEmail(email: EmailMessage): Promise<SendResult>;
|
|
188
|
-
abstract async sendBatch(emails: EmailMessage[]): Promise<SendResult[]>;
|
|
189
|
-
abstract async getDeliveryStatus(messageId: string): Promise<DeliveryStatus>;
|
|
190
|
-
abstract async processWebhook(
|
|
191
|
-
payload: any,
|
|
192
|
-
signature: string
|
|
193
|
-
): Promise<WebhookEvent[]>;
|
|
194
|
-
}
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
## Utility Functions
|
|
198
|
-
|
|
199
|
-
### Template Rendering
|
|
200
|
-
|
|
201
|
-
```typescript
|
|
202
|
-
// Liquid template engine integration
|
|
203
|
-
function renderLiquidTemplate(
|
|
204
|
-
template: string,
|
|
205
|
-
variables: Record<string, any>
|
|
206
|
-
): string;
|
|
207
|
-
|
|
208
|
-
// HTML sanitization
|
|
209
|
-
function sanitizeHtml(html: string): string;
|
|
210
|
-
|
|
211
|
-
// Text extraction from HTML
|
|
212
|
-
function extractTextFromHtml(html: string): string;
|
|
213
|
-
```
|
|
214
|
-
|
|
215
|
-
### Validation
|
|
216
|
-
|
|
217
|
-
```typescript
|
|
218
|
-
function validateEmailAddress(email: string): boolean;
|
|
219
|
-
function validateTemplateVariables(
|
|
220
|
-
template: EmailTemplate,
|
|
221
|
-
variables: Record<string, any>
|
|
222
|
-
): ValidationResult;
|
|
223
|
-
function validateCronExpression(expression: string): boolean;
|
|
224
|
-
```
|
|
225
|
-
|
|
226
|
-
### Encryption/Security
|
|
227
|
-
|
|
228
|
-
```typescript
|
|
229
|
-
function generateSecureToken(length?: number): string;
|
|
230
|
-
function hashToken(token: string): string;
|
|
231
|
-
function verifyTokenHash(token: string, hash: string): boolean;
|
|
232
|
-
function encryptPayload(payload: Record<string, any>, key: string): string;
|
|
233
|
-
function decryptPayload(encrypted: string, key: string): Record<string, any>;
|
|
234
|
-
```
|
|
235
|
-
|
|
236
|
-
## Configuration
|
|
237
|
-
|
|
238
|
-
### EmailManagerConfig
|
|
239
|
-
|
|
240
|
-
```typescript
|
|
241
|
-
interface EmailManagerConfig {
|
|
242
|
-
// Database connection
|
|
243
|
-
database: {
|
|
244
|
-
type: "postgresql" | "mysql" | "sqlite" | "mongodb";
|
|
245
|
-
connectionString: string;
|
|
246
|
-
ssl?: boolean;
|
|
247
|
-
pool?: {
|
|
248
|
-
min: number;
|
|
249
|
-
max: number;
|
|
250
|
-
idleTimeoutMillis: number;
|
|
251
|
-
};
|
|
252
|
-
};
|
|
253
|
-
|
|
254
|
-
// Email provider
|
|
255
|
-
emailProvider: EmailProviderConfig;
|
|
256
|
-
|
|
257
|
-
// Template settings
|
|
258
|
-
templates: {
|
|
259
|
-
defaultFromEmail: string;
|
|
260
|
-
defaultFromName: string;
|
|
261
|
-
liquidEngine: {
|
|
262
|
-
strictVariables: boolean;
|
|
263
|
-
customFilters?: Record<string, Function>;
|
|
264
|
-
};
|
|
265
|
-
};
|
|
266
|
-
|
|
267
|
-
// Scheduling
|
|
268
|
-
scheduling: {
|
|
269
|
-
defaultMaxRetries: number;
|
|
270
|
-
retryDelayMinutes: number[];
|
|
271
|
-
batchSize: number;
|
|
272
|
-
processingIntervalMinutes: number;
|
|
273
|
-
};
|
|
274
|
-
|
|
275
|
-
// Security
|
|
276
|
-
security: {
|
|
277
|
-
encryptionKey: string;
|
|
278
|
-
tokenLength: number;
|
|
279
|
-
defaultExpiryHours: number;
|
|
280
|
-
};
|
|
281
|
-
|
|
282
|
-
// Logging
|
|
283
|
-
logging: {
|
|
284
|
-
level: "debug" | "info" | "warn" | "error";
|
|
285
|
-
includePayload: boolean;
|
|
286
|
-
};
|
|
287
|
-
}
|
|
288
|
-
```
|
|
289
|
-
|
|
290
|
-
## Event System
|
|
291
|
-
|
|
292
|
-
### EmailEvents
|
|
293
|
-
|
|
294
|
-
```typescript
|
|
295
|
-
interface EmailTemplateEvent {
|
|
296
|
-
type: "template.created" | "template.updated" | "template.deleted";
|
|
297
|
-
templateId: string;
|
|
298
|
-
timestamp: Date;
|
|
299
|
-
metadata: Record<string, any>;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
interface EmailSchedulingEvent {
|
|
303
|
-
type: "email.scheduled" | "email.sent" | "email.failed" | "email.cancelled";
|
|
304
|
-
scheduledEmailId: string;
|
|
305
|
-
timestamp: Date;
|
|
306
|
-
metadata: Record<string, any>;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
interface MagicLinkEvent {
|
|
310
|
-
type: "link.generated" | "link.used" | "link.expired" | "link.revoked";
|
|
311
|
-
linkId: string;
|
|
312
|
-
timestamp: Date;
|
|
313
|
-
metadata: Record<string, any>;
|
|
314
|
-
}
|
|
315
|
-
```
|
|
316
|
-
|
|
317
|
-
### Event Emitter
|
|
318
|
-
|
|
319
|
-
```typescript
|
|
320
|
-
class EmailEventEmitter extends EventEmitter {
|
|
321
|
-
onTemplateEvent(handler: (event: EmailTemplateEvent) => void): void;
|
|
322
|
-
onSchedulingEvent(handler: (event: EmailSchedulingEvent) => void): void;
|
|
323
|
-
onMagicLinkEvent(handler: (event: MagicLinkEvent) => void): void;
|
|
324
|
-
}
|
|
325
|
-
```
|
|
326
|
-
|
|
327
|
-
## Error Handling
|
|
328
|
-
|
|
329
|
-
### Custom Errors
|
|
330
|
-
|
|
331
|
-
```typescript
|
|
332
|
-
class EmailTemplateError extends Error {
|
|
333
|
-
code: string;
|
|
334
|
-
templateId?: string;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
class EmailSchedulingError extends Error {
|
|
338
|
-
code: string;
|
|
339
|
-
scheduledEmailId?: string;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
class MagicLinkError extends Error {
|
|
343
|
-
code: string;
|
|
344
|
-
token?: string;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
class EmailProviderError extends Error {
|
|
348
|
-
code: string;
|
|
349
|
-
provider: string;
|
|
350
|
-
originalError?: Error;
|
|
351
|
-
}
|
|
352
|
-
```
|
|
353
|
-
|
|
354
|
-
## Testing Utilities
|
|
355
|
-
|
|
356
|
-
### Mock Providers
|
|
357
|
-
|
|
358
|
-
```typescript
|
|
359
|
-
class MockEmailProvider extends EmailProvider {
|
|
360
|
-
// Implementation for testing
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
class InMemoryStorage implements StorageAdapter {
|
|
364
|
-
// Implementation for testing
|
|
365
|
-
}
|
|
366
|
-
```
|
|
367
|
-
|
|
368
|
-
### Test Helpers
|
|
369
|
-
|
|
370
|
-
```typescript
|
|
371
|
-
function createTestTemplate(overrides?: Partial<EmailTemplate>): EmailTemplate;
|
|
372
|
-
function createTestScheduledEmail(
|
|
373
|
-
overrides?: Partial<ScheduledEmail>
|
|
374
|
-
): ScheduledEmail;
|
|
375
|
-
function createTestMagicLink(overrides?: Partial<MagicLink>): MagicLink;
|
|
376
|
-
```
|
|
377
|
-
|
|
378
|
-
## Export Structure
|
|
379
|
-
|
|
380
|
-
```typescript
|
|
381
|
-
// Main classes
|
|
382
|
-
export { EmailTemplateManager, EmailScheduler, MagicLinkManager };
|
|
383
|
-
|
|
384
|
-
// Email providers
|
|
385
|
-
export {
|
|
386
|
-
EmailProvider,
|
|
387
|
-
SendGridProvider,
|
|
388
|
-
SESProvider,
|
|
389
|
-
MailgunProvider,
|
|
390
|
-
PostmarkProvider,
|
|
391
|
-
SMTPProvider,
|
|
392
|
-
};
|
|
393
|
-
|
|
394
|
-
// Data models
|
|
395
|
-
export type { EmailTemplate, ScheduledEmail, MagicLink, EmailProviderConfig };
|
|
396
|
-
|
|
397
|
-
// Utilities
|
|
398
|
-
export { renderLiquidTemplate, validateEmailAddress, generateSecureToken };
|
|
399
|
-
|
|
400
|
-
// Events
|
|
401
|
-
export { EmailEventEmitter };
|
|
402
|
-
export type { EmailTemplateEvent, EmailSchedulingEvent, MagicLinkEvent };
|
|
403
|
-
|
|
404
|
-
// Errors
|
|
405
|
-
export {
|
|
406
|
-
EmailTemplateError,
|
|
407
|
-
EmailSchedulingError,
|
|
408
|
-
MagicLinkError,
|
|
409
|
-
EmailProviderError,
|
|
410
|
-
};
|
|
411
|
-
|
|
412
|
-
// Testing
|
|
413
|
-
export { MockEmailProvider, InMemoryStorage, createTestTemplate };
|
|
414
|
-
```
|
|
415
|
-
|
|
416
|
-
This core library provides all the essential functionality needed for email template management while remaining framework-agnostic and easily extensible.
|
|
@@ -1,291 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
Copyright (c) 2025 Bernier LLC
|
|
3
|
-
|
|
4
|
-
This file is licensed to the client under a limited-use license.
|
|
5
|
-
The client may use and modify this code *only within the scope of the project it was delivered for*.
|
|
6
|
-
Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Supabase adapter for EmailTemplateManager
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { createClient, SupabaseClient } from '@supabase/supabase-js';
|
|
14
|
-
import { EmailTemplate, TemplateFilters } from '../types/template';
|
|
15
|
-
|
|
16
|
-
export interface SupabaseConfig {
|
|
17
|
-
url: string;
|
|
18
|
-
key: string;
|
|
19
|
-
templatesTable?: string;
|
|
20
|
-
logsTable?: string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export class SupabaseEmailAdapter {
|
|
24
|
-
private supabase: SupabaseClient;
|
|
25
|
-
private templatesTable: string;
|
|
26
|
-
private logsTable: string;
|
|
27
|
-
|
|
28
|
-
constructor(config: SupabaseConfig) {
|
|
29
|
-
this.supabase = createClient(config.url, config.key);
|
|
30
|
-
this.templatesTable = config.templatesTable || 'email_templates';
|
|
31
|
-
this.logsTable = config.logsTable || 'email_delivery_logs';
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Save template to Supabase
|
|
36
|
-
*/
|
|
37
|
-
async saveTemplate(template: EmailTemplate): Promise<EmailTemplate> {
|
|
38
|
-
const { data, error } = await this.supabase
|
|
39
|
-
.from(this.templatesTable)
|
|
40
|
-
.upsert([
|
|
41
|
-
{
|
|
42
|
-
id: template.id,
|
|
43
|
-
name: template.name,
|
|
44
|
-
subject: template.subject,
|
|
45
|
-
html_body: template.htmlBody,
|
|
46
|
-
text_body: template.textBody,
|
|
47
|
-
variables: template.variables,
|
|
48
|
-
category_id: template.categoryId,
|
|
49
|
-
tags: template.tags,
|
|
50
|
-
is_active: template.isActive,
|
|
51
|
-
version: template.version,
|
|
52
|
-
created_at: template.createdAt?.toISOString(),
|
|
53
|
-
updated_at: template.updatedAt?.toISOString(),
|
|
54
|
-
created_by: template.createdBy,
|
|
55
|
-
metadata: template.metadata,
|
|
56
|
-
},
|
|
57
|
-
])
|
|
58
|
-
.select()
|
|
59
|
-
.single();
|
|
60
|
-
|
|
61
|
-
if (error) {
|
|
62
|
-
throw new Error(`Failed to save template: ${error.message}`);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return this.mapFromDatabase(data);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Load template by ID
|
|
70
|
-
*/
|
|
71
|
-
async loadTemplate(id: string): Promise<EmailTemplate | null> {
|
|
72
|
-
const { data, error } = await this.supabase
|
|
73
|
-
.from(this.templatesTable)
|
|
74
|
-
.select('*')
|
|
75
|
-
.eq('id', id)
|
|
76
|
-
.single();
|
|
77
|
-
|
|
78
|
-
if (error) {
|
|
79
|
-
if (error.code === 'PGRST116') {
|
|
80
|
-
return null; // Not found
|
|
81
|
-
}
|
|
82
|
-
throw new Error(`Failed to load template: ${error.message}`);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
return this.mapFromDatabase(data);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Load template by name
|
|
90
|
-
*/
|
|
91
|
-
async loadTemplateByName(name: string): Promise<EmailTemplate | null> {
|
|
92
|
-
const { data, error } = await this.supabase
|
|
93
|
-
.from(this.templatesTable)
|
|
94
|
-
.select('*')
|
|
95
|
-
.eq('name', name)
|
|
96
|
-
.eq('is_active', true)
|
|
97
|
-
.single();
|
|
98
|
-
|
|
99
|
-
if (error) {
|
|
100
|
-
if (error.code === 'PGRST116') {
|
|
101
|
-
return null; // Not found
|
|
102
|
-
}
|
|
103
|
-
throw new Error(`Failed to load template by name: ${error.message}`);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return this.mapFromDatabase(data);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Load templates with filters
|
|
111
|
-
*/
|
|
112
|
-
async loadTemplates(filters?: TemplateFilters): Promise<EmailTemplate[]> {
|
|
113
|
-
let query = this.supabase.from(this.templatesTable).select('*');
|
|
114
|
-
|
|
115
|
-
if (filters) {
|
|
116
|
-
if (filters.categoryId) {
|
|
117
|
-
query = query.eq('category_id', filters.categoryId);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
if (filters.isActive !== undefined) {
|
|
121
|
-
query = query.eq('is_active', filters.isActive);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
if (filters.createdBy) {
|
|
125
|
-
query = query.eq('created_by', filters.createdBy);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
if (filters.createdAfter) {
|
|
129
|
-
query = query.gte('created_at', filters.createdAfter.toISOString());
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
if (filters.createdBefore) {
|
|
133
|
-
query = query.lte('created_at', filters.createdBefore.toISOString());
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
if (filters.search) {
|
|
137
|
-
query = query.or(
|
|
138
|
-
`name.ilike.%${filters.search}%,subject.ilike.%${filters.search}%`
|
|
139
|
-
);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
if (filters.tags && filters.tags.length > 0) {
|
|
143
|
-
query = query.overlaps('tags', filters.tags);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
const { data, error } = await query.order('created_at', { ascending: false });
|
|
148
|
-
|
|
149
|
-
if (error) {
|
|
150
|
-
throw new Error(`Failed to load templates: ${error.message}`);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
return data?.map(item => this.mapFromDatabase(item)) || [];
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Delete template
|
|
158
|
-
*/
|
|
159
|
-
async deleteTemplate(id: string): Promise<boolean> {
|
|
160
|
-
const { error } = await this.supabase
|
|
161
|
-
.from(this.templatesTable)
|
|
162
|
-
.delete()
|
|
163
|
-
.eq('id', id);
|
|
164
|
-
|
|
165
|
-
if (error) {
|
|
166
|
-
throw new Error(`Failed to delete template: ${error.message}`);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
return true;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* Log email delivery
|
|
174
|
-
*/
|
|
175
|
-
async logEmailDelivery(log: {
|
|
176
|
-
templateName: string;
|
|
177
|
-
recipientEmail: string;
|
|
178
|
-
messageId?: string;
|
|
179
|
-
success: boolean;
|
|
180
|
-
error?: string;
|
|
181
|
-
metadata?: any;
|
|
182
|
-
}): Promise<void> {
|
|
183
|
-
const { error } = await this.supabase
|
|
184
|
-
.from(this.logsTable)
|
|
185
|
-
.insert([
|
|
186
|
-
{
|
|
187
|
-
template_name: log.templateName,
|
|
188
|
-
recipient_email: log.recipientEmail,
|
|
189
|
-
message_id: log.messageId,
|
|
190
|
-
success: log.success,
|
|
191
|
-
error_message: log.error,
|
|
192
|
-
sent_at: new Date().toISOString(),
|
|
193
|
-
metadata: log.metadata,
|
|
194
|
-
},
|
|
195
|
-
]);
|
|
196
|
-
|
|
197
|
-
if (error) {
|
|
198
|
-
console.error('Failed to log email delivery:', error);
|
|
199
|
-
// Don't throw error for logging failures
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* Update email delivery status from webhook
|
|
205
|
-
*/
|
|
206
|
-
async updateDeliveryStatus(
|
|
207
|
-
messageId: string,
|
|
208
|
-
status: string,
|
|
209
|
-
deliveredAt?: Date,
|
|
210
|
-
webhookData?: any
|
|
211
|
-
): Promise<void> {
|
|
212
|
-
const { error } = await this.supabase
|
|
213
|
-
.from(this.logsTable)
|
|
214
|
-
.update({
|
|
215
|
-
delivery_status: status,
|
|
216
|
-
delivered_at: deliveredAt?.toISOString(),
|
|
217
|
-
webhook_data: webhookData,
|
|
218
|
-
})
|
|
219
|
-
.eq('message_id', messageId);
|
|
220
|
-
|
|
221
|
-
if (error) {
|
|
222
|
-
console.error('Failed to update delivery status:', error);
|
|
223
|
-
// Don't throw error for logging failures
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* Get email delivery logs
|
|
229
|
-
*/
|
|
230
|
-
async getDeliveryLogs(filters?: {
|
|
231
|
-
templateName?: string;
|
|
232
|
-
recipientEmail?: string;
|
|
233
|
-
success?: boolean;
|
|
234
|
-
limit?: number;
|
|
235
|
-
offset?: number;
|
|
236
|
-
}): Promise<any[]> {
|
|
237
|
-
let query = this.supabase.from(this.logsTable).select('*');
|
|
238
|
-
|
|
239
|
-
if (filters) {
|
|
240
|
-
if (filters.templateName) {
|
|
241
|
-
query = query.eq('template_name', filters.templateName);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
if (filters.recipientEmail) {
|
|
245
|
-
query = query.eq('recipient_email', filters.recipientEmail);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
if (filters.success !== undefined) {
|
|
249
|
-
query = query.eq('success', filters.success);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
if (filters.limit) {
|
|
253
|
-
query = query.limit(filters.limit);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
if (filters.offset) {
|
|
257
|
-
query = query.range(filters.offset, filters.offset + (filters.limit || 50) - 1);
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
const { data, error } = await query.order('sent_at', { ascending: false });
|
|
262
|
-
|
|
263
|
-
if (error) {
|
|
264
|
-
throw new Error(`Failed to get delivery logs: ${error.message}`);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
return data || [];
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
/**
|
|
271
|
-
* Map database row to EmailTemplate
|
|
272
|
-
*/
|
|
273
|
-
private mapFromDatabase(data: any): EmailTemplate {
|
|
274
|
-
return {
|
|
275
|
-
id: data.id,
|
|
276
|
-
name: data.name,
|
|
277
|
-
subject: data.subject,
|
|
278
|
-
htmlBody: data.html_body,
|
|
279
|
-
textBody: data.text_body,
|
|
280
|
-
variables: data.variables || [],
|
|
281
|
-
categoryId: data.category_id,
|
|
282
|
-
tags: data.tags || [],
|
|
283
|
-
isActive: data.is_active,
|
|
284
|
-
version: data.version,
|
|
285
|
-
createdAt: data.created_at ? new Date(data.created_at) : undefined,
|
|
286
|
-
updatedAt: data.updated_at ? new Date(data.updated_at) : undefined,
|
|
287
|
-
createdBy: data.created_by,
|
|
288
|
-
metadata: data.metadata,
|
|
289
|
-
};
|
|
290
|
-
}
|
|
291
|
-
}
|