vintasend 0.2.3 → 0.3.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 +91 -11
- package/dist/index.d.ts +2 -1
- package/dist/index.js +4 -1
- package/dist/services/attachment-manager/base-attachment-manager.d.ts +0 -0
- package/dist/services/attachment-manager/base-attachment-manager.js +1 -0
- package/dist/services/notification-adapters/base-notification-adapter.d.ts +21 -2
- package/dist/services/notification-adapters/base-notification-adapter.js +51 -0
- package/dist/services/notification-backends/base-notification-backend.d.ts +16 -11
- package/dist/services/notification-service.d.ts +40 -8
- package/dist/services/notification-service.js +84 -5
- package/dist/services/notification-template-renderers/base-email-template-renderer.d.ts +2 -2
- package/dist/services/notification-template-renderers/base-notification-template-renderer.d.ts +2 -2
- package/dist/types/notification.d.ts +17 -0
- package/dist/types/one-off-notification.d.ts +68 -0
- package/dist/types/one-off-notification.js +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,9 +5,10 @@ A flexible package for implementing transactional notifications in TypeScript.
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
7
|
* **Storing notifications in a Database**: This package relies on a data store to record all the notifications that will be sent. It also keeps its state column up to date.
|
|
8
|
+
* **One-off notifications**: Send notifications directly to email addresses or phone numbers without requiring a user account. Perfect for prospects, guests, or external contacts.
|
|
8
9
|
* **Scheduling notifications**: Storing notifications to be sent in the future. The notification's context for rendering the template is only evaluated at the moment the notification is sent due to the lib's context generation registry.
|
|
9
10
|
* **Notification context fetched at send time**: On scheduled notifications, the package only gets the notification context (information to render the templates) at the send time, so we always get the most up-to-date information.
|
|
10
|
-
* **Flexible backend**: Your project's database is getting slow after you created the first million notifications? You can migrate to a faster
|
|
11
|
+
* **Flexible backend**: Your project's database is getting slow after you created the first million notifications? You can migrate to a faster NoSQL database in the blink of an eye without affecting how you send the notifications.
|
|
11
12
|
* **Flexible adapters**: Your project probably will need to change how it sends notifications over time. This package allows you to change the adapter without having to change how notification templates are rendered or how the notification themselves are stored.
|
|
12
13
|
* **Flexible template renderers**: Wanna start managing your templates with a third party tool (so non-technical people can help maintain them)? Or even choose a more powerful rendering engine? You can do it independently of how you send the notifications or store them in the database.
|
|
13
14
|
* **Sending notifications in background jobs**: This package supports using job queues to send notifications from separate processes. This may be helpful to free up the HTTP server of processing heavy notifications during the request time.
|
|
@@ -104,40 +105,119 @@ export function sendWelcomeEmail(userId: number) {
|
|
|
104
105
|
}
|
|
105
106
|
```
|
|
106
107
|
|
|
108
|
+
## One-Off Notifications
|
|
109
|
+
|
|
110
|
+
One-off notifications allow you to send notifications directly to an email address or phone number without requiring a user account in your system. This is particularly useful for:
|
|
111
|
+
|
|
112
|
+
- **Prospects**: Send welcome emails or marketing materials to potential customers
|
|
113
|
+
- **Guests**: Invite external participants to events or meetings
|
|
114
|
+
- **External Contacts**: Share information with partners or vendors
|
|
115
|
+
- **Temporary Recipients**: Send one-time notifications without creating user accounts
|
|
116
|
+
|
|
117
|
+
### Key Differences from Regular Notifications
|
|
118
|
+
|
|
119
|
+
| Feature | Regular Notification | One-Off Notification |
|
|
120
|
+
|---------|---------------------|----------------------|
|
|
121
|
+
| **Recipient** | User ID (requires account) | Email/phone directly |
|
|
122
|
+
| **User Data** | Fetched from user table | Provided inline (firstName, lastName) |
|
|
123
|
+
| **Use Case** | Registered users | Prospects, guests, external contacts |
|
|
124
|
+
| **Storage** | Same table with `userId` | Same table with `emailOrPhone` |
|
|
125
|
+
|
|
126
|
+
### Creating One-Off Notifications
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
// Send an immediate one-off notification
|
|
130
|
+
const notification = await vintaSend.createOneOffNotification({
|
|
131
|
+
emailOrPhone: 'prospect@example.com',
|
|
132
|
+
firstName: 'John',
|
|
133
|
+
lastName: 'Doe',
|
|
134
|
+
notificationType: 'EMAIL',
|
|
135
|
+
title: 'Welcome!',
|
|
136
|
+
bodyTemplate: './templates/welcome.html',
|
|
137
|
+
subjectTemplate: 'Welcome to {{companyName}}!',
|
|
138
|
+
contextName: 'welcomeContext',
|
|
139
|
+
contextParameters: { companyName: 'Acme Corp' },
|
|
140
|
+
sendAfter: null, // Send immediately
|
|
141
|
+
extraParams: null,
|
|
142
|
+
});
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
#### Using with Phone Numbers (SMS)
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
// Send SMS to a phone number (requires SMS adapter)
|
|
149
|
+
const smsNotification = await vintaSend.createOneOffNotification({
|
|
150
|
+
emailOrPhone: '+15551234567', // E.164 format recommended
|
|
151
|
+
firstName: 'John',
|
|
152
|
+
lastName: 'Doe',
|
|
153
|
+
notificationType: 'SMS',
|
|
154
|
+
title: 'Welcome SMS',
|
|
155
|
+
bodyTemplate: './templates/welcome-sms.txt',
|
|
156
|
+
subjectTemplate: null, // SMS doesn't use subjects
|
|
157
|
+
contextName: 'welcomeContext',
|
|
158
|
+
contextParameters: { companyName: 'Acme Corp' },
|
|
159
|
+
sendAfter: null,
|
|
160
|
+
extraParams: null,
|
|
161
|
+
});
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Database Schema Considerations
|
|
165
|
+
|
|
166
|
+
One-off notifications are stored in the same table as regular notifications using a unified approach:
|
|
167
|
+
|
|
168
|
+
- **Regular notifications**: Have `userId` set, `emailOrPhone` is null
|
|
169
|
+
- **One-off notifications**: Have `emailOrPhone` set, `userId` is null
|
|
170
|
+
|
|
171
|
+
### Migration Guide
|
|
172
|
+
|
|
173
|
+
If you're adding one-off notification support to an existing installation:
|
|
174
|
+
|
|
175
|
+
1. **Update your Prisma schema** to make `userId` optional and add one-off fields:
|
|
176
|
+
```bash
|
|
177
|
+
# Add the new fields to your schema.prisma
|
|
178
|
+
# Then run:
|
|
179
|
+
prisma migrate dev --name add-one-off-notification-support
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
2. **No code changes required** for existing functionality - all existing notifications continue to work as before.
|
|
183
|
+
|
|
184
|
+
3. **Existing notifications are preserved** - they have `userId` set and `emailOrPhone` as null.
|
|
185
|
+
|
|
107
186
|
## Glossary
|
|
108
187
|
|
|
109
|
-
* **Notification Backend**: It is a class that implements the methods necessary for VintaSend services to create, update, and retrieve Notifications from
|
|
188
|
+
* **Notification Backend**: It is a class that implements the methods necessary for VintaSend services to create, update, and retrieve Notifications from the database.
|
|
110
189
|
* **Notification Adapter**: It is a class that implements the methods necessary for VintaSend services to send Notifications through email, SMS or even push/in-app notifications.
|
|
111
190
|
* **Template Renderer**: It is a class that implements the methods necessary for VintaSend adapter to render the notification body.
|
|
112
191
|
* **Notification Context**: It's the data passed to the templates to render the notification correctly. It's generated when the notification is sent, not on creation time
|
|
113
192
|
* **Context generator**: It's a class defined by the user context generator map with a context name. That class has a `generate` method that, when called, generates the data necessary to render its respective notification.
|
|
114
193
|
* **Context name**: The registered name of a context generator. It's stored in the notification so the context generator is called at the moment the notification will be sent.
|
|
115
194
|
* **Context generators map**: It's an object defined by the user that maps context names to their respective context generators.
|
|
116
|
-
* **Queue service**: Service for enqueueing notifications so they are
|
|
117
|
-
* **Logger**: A class that allows the `NotificationService` to create logs following a format defined by its users.
|
|
195
|
+
* **Queue service**: Service for enqueueing notifications so they are sent by an external service.
|
|
196
|
+
* **Logger**: A class that allows the `NotificationService` to create logs following a format defined by its users.
|
|
197
|
+
* **One-off Notification**: A notification sent directly to an email address or phone number without requiring a user account. Used for prospects, guests, or external contacts.
|
|
198
|
+
* **Regular Notification**: A notification associated with a user account (via userId). Used for registered users in your system.
|
|
118
199
|
|
|
119
200
|
|
|
120
201
|
## Implementations
|
|
121
202
|
|
|
122
|
-
|
|
123
|
-
## Community
|
|
203
|
+
### Community
|
|
124
204
|
|
|
125
205
|
VintaSend has many backend, adapter, and template renderer implementations. If you can't find something that fulfills your needs, the package has very clear interfaces you can implement and achieve the exact behavior you expect without loosing VintaSend's friendly API.
|
|
126
206
|
|
|
127
|
-
|
|
207
|
+
#### Officially supported packages
|
|
128
208
|
|
|
129
|
-
|
|
209
|
+
##### Backends
|
|
130
210
|
|
|
131
211
|
* **[vintasend-prisma](https://github.com/vintasoftware/vintasend-prisma/)**: Uses Prisma Client to manage the notifications in the database.
|
|
132
212
|
|
|
133
|
-
|
|
213
|
+
##### Adapters
|
|
134
214
|
|
|
135
215
|
* **[vintasend-nodemailer](https://github.com/vintasoftware/vintasend-nodemailer/)**: Uses nodemailer to send transactional emails to users.
|
|
136
216
|
|
|
137
|
-
|
|
217
|
+
##### Template Renderers
|
|
138
218
|
* **[vintasend-pug](https://github.com/vintasoftware/vintasend-pug/)**: Renders emails using Pug.
|
|
139
219
|
|
|
140
|
-
|
|
220
|
+
##### Loggers
|
|
141
221
|
* **[vintasend-winston](https://github.com/vintasoftware/vintasend-winston/)**: Uses Winston to allow `NotificationService` to create log entries.
|
|
142
222
|
|
|
143
223
|
## Examples
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
export { VintaSendFactory } from './services/notification-service';
|
|
2
2
|
export type { VintaSend } from './services/notification-service';
|
|
3
|
-
export type { Notification, DatabaseNotification, NotificationInput, NotificationResendWithContextInput, } from './types/notification';
|
|
3
|
+
export type { Notification, DatabaseNotification, NotificationInput, NotificationResendWithContextInput, OneOffNotification, DatabaseOneOffNotification, OneOffNotificationInput, OneOffNotificationResendWithContextInput, AnyNotification, AnyDatabaseNotification, AnyNotificationInput, } from './types/notification';
|
|
4
4
|
export type { ContextGenerator } from './types/notification-context-generators';
|
|
5
5
|
export type { BaseNotificationTypeConfig } from './types/notification-type-config';
|
|
6
6
|
export type { BaseNotificationQueueService } from './services/notification-queue-service/base-notification-queue-service';
|
|
7
7
|
export type { BaseNotificationTemplateRenderer } from './services/notification-template-renderers/base-notification-template-renderer';
|
|
8
8
|
export type { BaseEmailTemplateRenderer } from './services/notification-template-renderers/base-email-template-renderer';
|
|
9
|
+
export { BaseNotificationAdapter, isOneOffNotification } from './services/notification-adapters/base-notification-adapter';
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.VintaSendFactory = void 0;
|
|
3
|
+
exports.isOneOffNotification = exports.BaseNotificationAdapter = exports.VintaSendFactory = void 0;
|
|
4
4
|
var notification_service_1 = require("./services/notification-service");
|
|
5
5
|
Object.defineProperty(exports, "VintaSendFactory", { enumerable: true, get: function () { return notification_service_1.VintaSendFactory; } });
|
|
6
|
+
var base_notification_adapter_1 = require("./services/notification-adapters/base-notification-adapter");
|
|
7
|
+
Object.defineProperty(exports, "BaseNotificationAdapter", { enumerable: true, get: function () { return base_notification_adapter_1.BaseNotificationAdapter; } });
|
|
8
|
+
Object.defineProperty(exports, "isOneOffNotification", { enumerable: true, get: function () { return base_notification_adapter_1.isOneOffNotification; } });
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import type { NotificationType } from '../../types/notification-type';
|
|
2
|
-
import type {
|
|
2
|
+
import type { DatabaseOneOffNotification, AnyDatabaseNotification } from '../../types/notification';
|
|
3
3
|
import type { BaseNotificationTemplateRenderer } from '../notification-template-renderers/base-notification-template-renderer';
|
|
4
4
|
import type { JsonValue } from '../../types/json-values';
|
|
5
5
|
import type { BaseNotificationTypeConfig } from '../../types/notification-type-config';
|
|
6
6
|
import type { BaseNotificationBackend } from '../notification-backends/base-notification-backend';
|
|
7
|
+
/**
|
|
8
|
+
* Type guard to check if a notification is a one-off notification
|
|
9
|
+
*/
|
|
10
|
+
export declare function isOneOffNotification<Config extends BaseNotificationTypeConfig>(notification: AnyDatabaseNotification<Config>): notification is DatabaseOneOffNotification<Config>;
|
|
7
11
|
export declare abstract class BaseNotificationAdapter<TemplateRenderer extends BaseNotificationTemplateRenderer<Config>, Config extends BaseNotificationTypeConfig> {
|
|
8
12
|
protected templateRenderer: TemplateRenderer;
|
|
9
13
|
readonly notificationType: NotificationType;
|
|
@@ -11,6 +15,21 @@ export declare abstract class BaseNotificationAdapter<TemplateRenderer extends B
|
|
|
11
15
|
key: string | null;
|
|
12
16
|
backend: BaseNotificationBackend<Config> | null;
|
|
13
17
|
constructor(templateRenderer: TemplateRenderer, notificationType: NotificationType, enqueueNotifications: boolean);
|
|
14
|
-
send(notification:
|
|
18
|
+
send(notification: AnyDatabaseNotification<Config>, context: JsonValue): Promise<void>;
|
|
19
|
+
/**
|
|
20
|
+
* Get the recipient email address from a notification.
|
|
21
|
+
* For one-off notifications, returns the emailOrPhone field directly.
|
|
22
|
+
* For regular notifications, fetches the email from the user via backend.
|
|
23
|
+
*/
|
|
24
|
+
protected getRecipientEmail(notification: AnyDatabaseNotification<Config>): Promise<string>;
|
|
25
|
+
/**
|
|
26
|
+
* Get the recipient name from a notification.
|
|
27
|
+
* For one-off notifications, returns the firstName and lastName fields directly.
|
|
28
|
+
* For regular notifications, attempts to extract from context or returns empty strings.
|
|
29
|
+
*/
|
|
30
|
+
protected getRecipientName(notification: AnyDatabaseNotification<Config>, context: JsonValue): {
|
|
31
|
+
firstName: string;
|
|
32
|
+
lastName: string;
|
|
33
|
+
};
|
|
15
34
|
injectBackend(backend: BaseNotificationBackend<Config>): void;
|
|
16
35
|
}
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.BaseNotificationAdapter = void 0;
|
|
4
|
+
exports.isOneOffNotification = isOneOffNotification;
|
|
5
|
+
/**
|
|
6
|
+
* Type guard to check if a notification is a one-off notification
|
|
7
|
+
*/
|
|
8
|
+
function isOneOffNotification(notification) {
|
|
9
|
+
return 'emailOrPhone' in notification && 'firstName' in notification && 'lastName' in notification;
|
|
10
|
+
}
|
|
4
11
|
class BaseNotificationAdapter {
|
|
5
12
|
constructor(templateRenderer, notificationType, enqueueNotifications) {
|
|
6
13
|
this.templateRenderer = templateRenderer;
|
|
@@ -17,6 +24,50 @@ class BaseNotificationAdapter {
|
|
|
17
24
|
return Promise.resolve();
|
|
18
25
|
}
|
|
19
26
|
;
|
|
27
|
+
/**
|
|
28
|
+
* Get the recipient email address from a notification.
|
|
29
|
+
* For one-off notifications, returns the emailOrPhone field directly.
|
|
30
|
+
* For regular notifications, fetches the email from the user via backend.
|
|
31
|
+
*/
|
|
32
|
+
async getRecipientEmail(notification) {
|
|
33
|
+
if (isOneOffNotification(notification)) {
|
|
34
|
+
return notification.emailOrPhone;
|
|
35
|
+
}
|
|
36
|
+
// Regular notification - get from user via backend
|
|
37
|
+
if (!this.backend) {
|
|
38
|
+
throw new Error('Backend not injected');
|
|
39
|
+
}
|
|
40
|
+
const userEmail = await this.backend.getUserEmailFromNotification(notification.id);
|
|
41
|
+
if (!userEmail) {
|
|
42
|
+
throw new Error(`User email not found for notification ${notification.id}`);
|
|
43
|
+
}
|
|
44
|
+
return userEmail;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Get the recipient name from a notification.
|
|
48
|
+
* For one-off notifications, returns the firstName and lastName fields directly.
|
|
49
|
+
* For regular notifications, attempts to extract from context or returns empty strings.
|
|
50
|
+
*/
|
|
51
|
+
getRecipientName(notification, context) {
|
|
52
|
+
if (isOneOffNotification(notification)) {
|
|
53
|
+
return {
|
|
54
|
+
firstName: notification.firstName,
|
|
55
|
+
lastName: notification.lastName,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
// Regular notification - try to get from context
|
|
59
|
+
if (context && typeof context === 'object' && !Array.isArray(context)) {
|
|
60
|
+
const jsonContext = context;
|
|
61
|
+
return {
|
|
62
|
+
firstName: (typeof jsonContext.firstName === 'string' ? jsonContext.firstName : '') || '',
|
|
63
|
+
lastName: (typeof jsonContext.lastName === 'string' ? jsonContext.lastName : '') || '',
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
firstName: '',
|
|
68
|
+
lastName: '',
|
|
69
|
+
};
|
|
70
|
+
}
|
|
20
71
|
injectBackend(backend) {
|
|
21
72
|
this.backend = backend;
|
|
22
73
|
}
|
|
@@ -1,25 +1,30 @@
|
|
|
1
1
|
import type { InputJsonValue } from '../../types/json-values';
|
|
2
|
-
import type { DatabaseNotification, Notification } from '../../types/notification';
|
|
2
|
+
import type { DatabaseNotification, Notification, DatabaseOneOffNotification, OneOffNotificationInput, AnyNotification, AnyDatabaseNotification } from '../../types/notification';
|
|
3
3
|
import type { BaseNotificationTypeConfig } from '../../types/notification-type-config';
|
|
4
4
|
export interface BaseNotificationBackend<Config extends BaseNotificationTypeConfig> {
|
|
5
|
-
getAllPendingNotifications(): Promise<
|
|
6
|
-
getPendingNotifications(page: number, pageSize: number): Promise<
|
|
7
|
-
getAllFutureNotifications(): Promise<
|
|
8
|
-
getFutureNotifications(page: number, pageSize: number): Promise<
|
|
5
|
+
getAllPendingNotifications(): Promise<AnyDatabaseNotification<Config>[]>;
|
|
6
|
+
getPendingNotifications(page: number, pageSize: number): Promise<AnyDatabaseNotification<Config>[]>;
|
|
7
|
+
getAllFutureNotifications(): Promise<AnyDatabaseNotification<Config>[]>;
|
|
8
|
+
getFutureNotifications(page: number, pageSize: number): Promise<AnyDatabaseNotification<Config>[]>;
|
|
9
9
|
getAllFutureNotificationsFromUser(userId: Config['UserIdType']): Promise<DatabaseNotification<Config>[]>;
|
|
10
10
|
getFutureNotificationsFromUser(userId: Config['UserIdType'], page: number, pageSize: number): Promise<DatabaseNotification<Config>[]>;
|
|
11
11
|
persistNotification(notification: Omit<Notification<Config>, 'id'>): Promise<DatabaseNotification<Config>>;
|
|
12
|
-
getAllNotifications(): Promise<
|
|
13
|
-
getNotifications(page: number, pageSize: number): Promise<
|
|
14
|
-
bulkPersistNotifications(notifications: Omit<
|
|
12
|
+
getAllNotifications(): Promise<AnyDatabaseNotification<Config>[]>;
|
|
13
|
+
getNotifications(page: number, pageSize: number): Promise<AnyDatabaseNotification<Config>[]>;
|
|
14
|
+
bulkPersistNotifications(notifications: Omit<AnyNotification<Config>, 'id'>[]): Promise<Config['NotificationIdType'][]>;
|
|
15
15
|
persistNotificationUpdate(notificationId: Config['NotificationIdType'], notification: Partial<Omit<Notification<Config>, 'id'>>): Promise<DatabaseNotification<Config>>;
|
|
16
|
-
markAsSent(notificationId: Config['NotificationIdType'], checkIsPending: boolean): Promise<
|
|
17
|
-
markAsFailed(notificationId: Config['NotificationIdType'], checkIsPending: boolean): Promise<
|
|
16
|
+
markAsSent(notificationId: Config['NotificationIdType'], checkIsPending: boolean): Promise<AnyDatabaseNotification<Config>>;
|
|
17
|
+
markAsFailed(notificationId: Config['NotificationIdType'], checkIsPending: boolean): Promise<AnyDatabaseNotification<Config>>;
|
|
18
18
|
markAsRead(notificationId: Config['NotificationIdType'], checkIsSent: boolean): Promise<DatabaseNotification<Config>>;
|
|
19
19
|
cancelNotification(notificationId: Config['NotificationIdType']): Promise<void>;
|
|
20
|
-
getNotification(notificationId: Config['NotificationIdType'], forUpdate: boolean): Promise<
|
|
20
|
+
getNotification(notificationId: Config['NotificationIdType'], forUpdate: boolean): Promise<AnyDatabaseNotification<Config> | null>;
|
|
21
21
|
filterAllInAppUnreadNotifications(userId: Config['UserIdType']): Promise<DatabaseNotification<Config>[]>;
|
|
22
22
|
filterInAppUnreadNotifications(userId: Config['UserIdType'], page: number, pageSize: number): Promise<DatabaseNotification<Config>[]>;
|
|
23
23
|
getUserEmailFromNotification(notificationId: Config['NotificationIdType']): Promise<string | undefined>;
|
|
24
24
|
storeContextUsed(notificationId: Config['NotificationIdType'], context: InputJsonValue): Promise<void>;
|
|
25
|
+
persistOneOffNotification(notification: Omit<OneOffNotificationInput<Config>, 'id'>): Promise<DatabaseOneOffNotification<Config>>;
|
|
26
|
+
persistOneOffNotificationUpdate(notificationId: Config['NotificationIdType'], notification: Partial<Omit<OneOffNotificationInput<Config>, 'id'>>): Promise<DatabaseOneOffNotification<Config>>;
|
|
27
|
+
getOneOffNotification(notificationId: Config['NotificationIdType'], forUpdate: boolean): Promise<DatabaseOneOffNotification<Config> | null>;
|
|
28
|
+
getAllOneOffNotifications(): Promise<DatabaseOneOffNotification<Config>[]>;
|
|
29
|
+
getOneOffNotifications(page: number, pageSize: number): Promise<DatabaseOneOffNotification<Config>[]>;
|
|
25
30
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import type { DatabaseNotification, Notification } from '../types/notification';
|
|
1
|
+
import type { DatabaseNotification, Notification, AnyNotification, AnyDatabaseNotification, DatabaseOneOffNotification } from '../types/notification';
|
|
2
|
+
import type { OneOffNotificationInput } from '../types/one-off-notification';
|
|
2
3
|
import type { JsonObject } from '../types/json-values';
|
|
3
4
|
import type { BaseNotificationTypeConfig } from '../types/notification-type-config';
|
|
4
|
-
import type
|
|
5
|
+
import { type BaseNotificationAdapter } from './notification-adapters/base-notification-adapter';
|
|
5
6
|
import type { BaseNotificationTemplateRenderer } from './notification-template-renderers/base-notification-template-renderer';
|
|
6
7
|
import type { BaseNotificationBackend } from './notification-backends/base-notification-backend';
|
|
7
8
|
import type { BaseLogger } from './loggers/base-logger';
|
|
@@ -21,23 +22,54 @@ export declare class VintaSend<Config extends BaseNotificationTypeConfig, Adapte
|
|
|
21
22
|
private contextGeneratorsMap;
|
|
22
23
|
constructor(adapters: AdaptersList, backend: Backend, logger: Logger, contextGeneratorsMap: Config['ContextMap'], queueService?: QueueService | undefined, options?: VintaSendOptions);
|
|
23
24
|
registerQueueService(queueService: QueueService): void;
|
|
24
|
-
send(notification:
|
|
25
|
+
send(notification: AnyDatabaseNotification<Config>): Promise<void>;
|
|
25
26
|
createNotification(notification: Omit<Notification<Config>, 'id'>): Promise<DatabaseNotification<Config>>;
|
|
26
27
|
updateNotification(notificationId: Config['NotificationIdType'], notification: Partial<Omit<Notification<Config>, 'id'>>): Promise<DatabaseNotification<Config>>;
|
|
27
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Creates and sends a one-off notification.
|
|
30
|
+
* One-off notifications are sent directly to an email/phone without requiring a user account.
|
|
31
|
+
*
|
|
32
|
+
* @param notification - The one-off notification to create (without id)
|
|
33
|
+
* @returns The created database notification
|
|
34
|
+
*/
|
|
35
|
+
createOneOffNotification(notification: Omit<OneOffNotificationInput<Config>, 'id'>): Promise<DatabaseOneOffNotification<Config>>;
|
|
36
|
+
/**
|
|
37
|
+
* Updates a one-off notification and re-sends it if the sendAfter date is in the past.
|
|
38
|
+
*
|
|
39
|
+
* @param notificationId - The ID of the notification to update
|
|
40
|
+
* @param notification - The partial notification data to update
|
|
41
|
+
* @returns The updated database notification
|
|
42
|
+
*/
|
|
43
|
+
updateOneOffNotification(notificationId: Config['NotificationIdType'], notification: Partial<Omit<OneOffNotificationInput<Config>, 'id'>>): Promise<DatabaseOneOffNotification<Config>>;
|
|
44
|
+
/**
|
|
45
|
+
* Validates that an email or phone number has a basic valid format.
|
|
46
|
+
*
|
|
47
|
+
* @param emailOrPhone - The email or phone string to validate
|
|
48
|
+
* @throws Error if the format is invalid
|
|
49
|
+
*/
|
|
50
|
+
private validateEmailOrPhone;
|
|
51
|
+
getAllFutureNotifications(): Promise<AnyDatabaseNotification<Config>[]>;
|
|
28
52
|
getAllFutureNotificationsFromUser(userId: Config['NotificationIdType']): Promise<DatabaseNotification<Config>[]>;
|
|
29
53
|
getFutureNotificationsFromUser(userId: Config['NotificationIdType'], page: number, pageSize: number): Promise<DatabaseNotification<Config>[]>;
|
|
30
|
-
getFutureNotifications(page: number, pageSize: number): Promise<
|
|
54
|
+
getFutureNotifications(page: number, pageSize: number): Promise<AnyDatabaseNotification<Config>[]>;
|
|
31
55
|
getNotificationContext<ContextName extends string & keyof Config['ContextMap']>(contextName: ContextName, parameters: Parameters<ReturnType<typeof this.contextGeneratorsMap.getContextGenerator<ContextName>>['generate']>[0]): Promise<JsonObject>;
|
|
32
56
|
sendPendingNotifications(): Promise<void>;
|
|
33
|
-
getPendingNotifications(page: number, pageSize: number): Promise<
|
|
34
|
-
getNotification(notificationId: Config['NotificationIdType'], forUpdate?: boolean): Promise<
|
|
57
|
+
getPendingNotifications(page: number, pageSize: number): Promise<AnyDatabaseNotification<Config>[]>;
|
|
58
|
+
getNotification(notificationId: Config['NotificationIdType'], forUpdate?: boolean): Promise<AnyDatabaseNotification<Config> | null>;
|
|
59
|
+
/**
|
|
60
|
+
* Gets a one-off notification by ID.
|
|
61
|
+
*
|
|
62
|
+
* @param notificationId - The ID of the one-off notification to retrieve
|
|
63
|
+
* @param forUpdate - Whether the notification is being retrieved for update (default: false)
|
|
64
|
+
* @returns The one-off notification or null if not found
|
|
65
|
+
*/
|
|
66
|
+
getOneOffNotification(notificationId: Config['NotificationIdType'], forUpdate?: boolean): Promise<DatabaseOneOffNotification<Config> | null>;
|
|
35
67
|
markRead(notificationId: Config['NotificationIdType'], checkIsSent?: boolean): Promise<DatabaseNotification<Config>>;
|
|
36
68
|
getInAppUnread(userId: Config['NotificationIdType']): Promise<DatabaseNotification<Config>[]>;
|
|
37
69
|
cancelNotification(notificationId: Config['NotificationIdType']): Promise<void>;
|
|
38
70
|
resendNotification(notificationId: Config['NotificationIdType'], useStoredContextIfAvailable?: boolean): Promise<DatabaseNotification<Config> | undefined>;
|
|
39
71
|
delayedSend(notificationId: Config['NotificationIdType']): Promise<void>;
|
|
40
|
-
bulkPersistNotifications(notifications: Omit<
|
|
72
|
+
bulkPersistNotifications(notifications: Omit<AnyNotification<Config>, 'id'>[]): Promise<Config['NotificationIdType'][]>;
|
|
41
73
|
migrateToBackend<DestinationBackend extends BaseNotificationBackend<Config>>(destinationBackend: DestinationBackend, batchSize?: number): Promise<void>;
|
|
42
74
|
}
|
|
43
75
|
export {};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.VintaSend = exports.VintaSendFactory = void 0;
|
|
4
|
+
const base_notification_adapter_1 = require("./notification-adapters/base-notification-adapter");
|
|
4
5
|
const notification_context_generators_map_1 = require("./notification-context-generators-map");
|
|
5
6
|
class VintaSendFactory {
|
|
6
7
|
create(adapters, backend, logger, contextGeneratorsMap, queueService, options = {
|
|
@@ -119,6 +120,66 @@ class VintaSend {
|
|
|
119
120
|
this.logger.info(`Notification ${notificationId} updated`);
|
|
120
121
|
return updatedNotification;
|
|
121
122
|
}
|
|
123
|
+
/**
|
|
124
|
+
* Creates and sends a one-off notification.
|
|
125
|
+
* One-off notifications are sent directly to an email/phone without requiring a user account.
|
|
126
|
+
*
|
|
127
|
+
* @param notification - The one-off notification to create (without id)
|
|
128
|
+
* @returns The created database notification
|
|
129
|
+
*/
|
|
130
|
+
async createOneOffNotification(notification) {
|
|
131
|
+
// Validate email or phone format
|
|
132
|
+
this.validateEmailOrPhone(notification.emailOrPhone);
|
|
133
|
+
const createdNotification = await this.backend.persistOneOffNotification(notification);
|
|
134
|
+
this.logger.info(`One-off notification ${createdNotification.id} created`);
|
|
135
|
+
if (!notification.sendAfter || notification.sendAfter <= new Date()) {
|
|
136
|
+
this.logger.info(`One-off notification ${createdNotification.id} sent immediately`);
|
|
137
|
+
await this.send(createdNotification);
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
this.logger.info(`One-off notification ${createdNotification.id} scheduled for ${notification.sendAfter}`);
|
|
141
|
+
}
|
|
142
|
+
return createdNotification;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Updates a one-off notification and re-sends it if the sendAfter date is in the past.
|
|
146
|
+
*
|
|
147
|
+
* @param notificationId - The ID of the notification to update
|
|
148
|
+
* @param notification - The partial notification data to update
|
|
149
|
+
* @returns The updated database notification
|
|
150
|
+
*/
|
|
151
|
+
async updateOneOffNotification(notificationId, notification) {
|
|
152
|
+
// Validate email or phone format if provided
|
|
153
|
+
if (notification.emailOrPhone !== undefined) {
|
|
154
|
+
this.validateEmailOrPhone(notification.emailOrPhone);
|
|
155
|
+
}
|
|
156
|
+
const updatedNotification = await this.backend.persistOneOffNotificationUpdate(notificationId, notification);
|
|
157
|
+
this.logger.info(`One-off notification ${notificationId} updated`);
|
|
158
|
+
if (!updatedNotification.sendAfter || updatedNotification.sendAfter <= new Date()) {
|
|
159
|
+
this.logger.info(`One-off notification ${notificationId} sent after update`);
|
|
160
|
+
await this.send(updatedNotification);
|
|
161
|
+
}
|
|
162
|
+
return updatedNotification;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Validates that an email or phone number has a basic valid format.
|
|
166
|
+
*
|
|
167
|
+
* @param emailOrPhone - The email or phone string to validate
|
|
168
|
+
* @throws Error if the format is invalid
|
|
169
|
+
*/
|
|
170
|
+
validateEmailOrPhone(emailOrPhone) {
|
|
171
|
+
// Basic non-empty check
|
|
172
|
+
if (emailOrPhone === '' || emailOrPhone.trim() === '') {
|
|
173
|
+
throw new Error('emailOrPhone cannot be empty');
|
|
174
|
+
}
|
|
175
|
+
// Check if it's an email (has @ with characters before and after)
|
|
176
|
+
const isEmail = /^.+@.+\..+$/.test(emailOrPhone);
|
|
177
|
+
// Check if it's a phone (10-15 digits, optionally starting with +)
|
|
178
|
+
const isPhone = /^\+?[0-9]{10,15}$/.test(emailOrPhone);
|
|
179
|
+
if (!isEmail && !isPhone) {
|
|
180
|
+
throw new Error('Invalid email or phone format');
|
|
181
|
+
}
|
|
182
|
+
}
|
|
122
183
|
async getAllFutureNotifications() {
|
|
123
184
|
return this.backend.getAllFutureNotifications();
|
|
124
185
|
}
|
|
@@ -148,6 +209,16 @@ class VintaSend {
|
|
|
148
209
|
async getNotification(notificationId, forUpdate = false) {
|
|
149
210
|
return this.backend.getNotification(notificationId, forUpdate);
|
|
150
211
|
}
|
|
212
|
+
/**
|
|
213
|
+
* Gets a one-off notification by ID.
|
|
214
|
+
*
|
|
215
|
+
* @param notificationId - The ID of the one-off notification to retrieve
|
|
216
|
+
* @param forUpdate - Whether the notification is being retrieved for update (default: false)
|
|
217
|
+
* @returns The one-off notification or null if not found
|
|
218
|
+
*/
|
|
219
|
+
async getOneOffNotification(notificationId, forUpdate = false) {
|
|
220
|
+
return this.backend.getOneOffNotification(notificationId, forUpdate);
|
|
221
|
+
}
|
|
151
222
|
async markRead(notificationId, checkIsSent = true) {
|
|
152
223
|
const notification = await this.backend.markAsRead(notificationId, checkIsSent);
|
|
153
224
|
this.logger.info(`Notification ${notificationId} marked as read`);
|
|
@@ -169,6 +240,14 @@ class VintaSend {
|
|
|
169
240
|
}
|
|
170
241
|
return;
|
|
171
242
|
}
|
|
243
|
+
// Check if this is a one-off notification (which cannot be resent this way)
|
|
244
|
+
if ((0, base_notification_adapter_1.isOneOffNotification)(notification)) {
|
|
245
|
+
this.logger.error(`Cannot resend one-off notification ${notificationId} using resendNotification. One-off notifications are not supported.`);
|
|
246
|
+
if (this.options.raiseErrorOnFailedSend) {
|
|
247
|
+
throw new Error(`Cannot resend one-off notification ${notificationId}. One-off notifications must be resent using a different method.`);
|
|
248
|
+
}
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
172
251
|
if (notification.sendAfter && notification.sendAfter > new Date()) {
|
|
173
252
|
this.logger.error(`Notification ${notificationId} is scheduled for the future`);
|
|
174
253
|
if (this.options.raiseErrorOnFailedSend) {
|
|
@@ -263,15 +342,15 @@ class VintaSend {
|
|
|
263
342
|
}
|
|
264
343
|
async migrateToBackend(destinationBackend, batchSize = 5000) {
|
|
265
344
|
let pageNumber = 0;
|
|
266
|
-
let
|
|
267
|
-
while (
|
|
345
|
+
let allNotifications = await this.backend.getNotifications(pageNumber, batchSize);
|
|
346
|
+
while (allNotifications.length > 0) {
|
|
268
347
|
pageNumber += 1;
|
|
269
|
-
const
|
|
348
|
+
const notificationsWithoutId = allNotifications.map((notification) => {
|
|
270
349
|
const { id, ...notificationWithoutId } = notification;
|
|
271
350
|
return notificationWithoutId;
|
|
272
351
|
});
|
|
273
|
-
await destinationBackend.bulkPersistNotifications(
|
|
274
|
-
|
|
352
|
+
await destinationBackend.bulkPersistNotifications(notificationsWithoutId);
|
|
353
|
+
allNotifications = await this.backend.getNotifications(pageNumber, batchSize);
|
|
275
354
|
}
|
|
276
355
|
}
|
|
277
356
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { BaseNotificationTemplateRenderer } from './base-notification-template-renderer';
|
|
2
|
-
import type {
|
|
2
|
+
import type { AnyNotification } from '../../types/notification';
|
|
3
3
|
import type { Buffer } from 'node:buffer';
|
|
4
4
|
import type { JsonObject } from '../../types/json-values';
|
|
5
5
|
import type { BaseNotificationTypeConfig } from '../../types/notification-type-config';
|
|
@@ -9,5 +9,5 @@ export type EmailTemplate = {
|
|
|
9
9
|
body: string;
|
|
10
10
|
};
|
|
11
11
|
export interface BaseEmailTemplateRenderer<Config extends BaseNotificationTypeConfig> extends BaseNotificationTemplateRenderer<Config, EmailTemplate> {
|
|
12
|
-
render(notification:
|
|
12
|
+
render(notification: AnyNotification<Config>, context: JsonObject): Promise<EmailTemplate>;
|
|
13
13
|
}
|
package/dist/services/notification-template-renderers/base-notification-template-renderer.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { JsonObject } from '../../types/json-values';
|
|
2
|
-
import type {
|
|
2
|
+
import type { AnyNotification } from '../../types/notification';
|
|
3
3
|
import type { BaseNotificationTypeConfig } from '../../types/notification-type-config';
|
|
4
4
|
export interface BaseNotificationTemplateRenderer<Config extends BaseNotificationTypeConfig, T = unknown> {
|
|
5
|
-
render(notification:
|
|
5
|
+
render(notification: AnyNotification<Config>, context: JsonObject): Promise<T>;
|
|
6
6
|
}
|
|
@@ -2,6 +2,8 @@ import type { InputJsonValue, JsonValue } from './json-values';
|
|
|
2
2
|
import type { NotificationStatus } from './notification-status';
|
|
3
3
|
import type { NotificationType } from './notification-type';
|
|
4
4
|
import type { BaseNotificationTypeConfig } from './notification-type-config';
|
|
5
|
+
import type { DatabaseOneOffNotification, OneOffNotification, OneOffNotificationInput } from './one-off-notification';
|
|
6
|
+
export type { OneOffNotificationInput, OneOffNotificationResendWithContextInput, DatabaseOneOffNotification, OneOffNotification, } from './one-off-notification';
|
|
5
7
|
export type NotificationInput<Config extends BaseNotificationTypeConfig> = {
|
|
6
8
|
userId: Config['UserIdType'];
|
|
7
9
|
notificationType: NotificationType;
|
|
@@ -45,3 +47,18 @@ export type DatabaseNotification<Config extends BaseNotificationTypeConfig> = {
|
|
|
45
47
|
updatedAt?: Date;
|
|
46
48
|
};
|
|
47
49
|
export type Notification<Config extends BaseNotificationTypeConfig> = NotificationInput<Config> | NotificationResendWithContextInput<Config> | DatabaseNotification<Config>;
|
|
50
|
+
/**
|
|
51
|
+
* Union type representing any notification type (regular or one-off).
|
|
52
|
+
* This is useful for methods that handle both notification types.
|
|
53
|
+
*/
|
|
54
|
+
export type AnyNotification<Config extends BaseNotificationTypeConfig> = Notification<Config> | OneOffNotification<Config>;
|
|
55
|
+
/**
|
|
56
|
+
* Union type for database notifications only (regular or one-off).
|
|
57
|
+
* Useful for send methods and database queries.
|
|
58
|
+
*/
|
|
59
|
+
export type AnyDatabaseNotification<Config extends BaseNotificationTypeConfig> = DatabaseNotification<Config> | DatabaseOneOffNotification<Config>;
|
|
60
|
+
/**
|
|
61
|
+
* Union type for notification inputs only (regular or one-off).
|
|
62
|
+
* Useful for creation methods.
|
|
63
|
+
*/
|
|
64
|
+
export type AnyNotificationInput<Config extends BaseNotificationTypeConfig> = NotificationInput<Config> | OneOffNotificationInput<Config>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { InputJsonValue, JsonValue } from './json-values';
|
|
2
|
+
import type { NotificationStatus } from './notification-status';
|
|
3
|
+
import type { NotificationType } from './notification-type';
|
|
4
|
+
import type { BaseNotificationTypeConfig } from './notification-type-config';
|
|
5
|
+
/**
|
|
6
|
+
* Input type for creating a one-off notification.
|
|
7
|
+
* One-off notifications are sent directly to an email/phone without requiring a user account.
|
|
8
|
+
*/
|
|
9
|
+
export type OneOffNotificationInput<Config extends BaseNotificationTypeConfig> = {
|
|
10
|
+
emailOrPhone: string;
|
|
11
|
+
firstName: string;
|
|
12
|
+
lastName: string;
|
|
13
|
+
notificationType: NotificationType;
|
|
14
|
+
title: string | null;
|
|
15
|
+
bodyTemplate: string;
|
|
16
|
+
contextName: string & keyof Config['ContextMap'];
|
|
17
|
+
contextParameters: Parameters<Config['ContextMap'][OneOffNotificationInput<Config>['contextName']]['generate']>[0];
|
|
18
|
+
sendAfter: Date | null;
|
|
19
|
+
subjectTemplate: string | null;
|
|
20
|
+
extraParams: InputJsonValue | null;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Input type for resending a one-off notification with stored context.
|
|
24
|
+
* Similar to OneOffNotificationInput but includes the contextUsed field.
|
|
25
|
+
*/
|
|
26
|
+
export type OneOffNotificationResendWithContextInput<Config extends BaseNotificationTypeConfig> = {
|
|
27
|
+
emailOrPhone: string;
|
|
28
|
+
firstName: string;
|
|
29
|
+
lastName: string;
|
|
30
|
+
notificationType: NotificationType;
|
|
31
|
+
title: string | null;
|
|
32
|
+
bodyTemplate: string;
|
|
33
|
+
contextName: string & keyof Config['ContextMap'];
|
|
34
|
+
contextParameters: Parameters<Config['ContextMap'][OneOffNotificationResendWithContextInput<Config>['contextName']]['generate']>[0];
|
|
35
|
+
contextUsed: ReturnType<Config['ContextMap'][OneOffNotificationResendWithContextInput<Config>['contextName']]['generate']> extends Promise<infer T> ? T : ReturnType<Config['ContextMap'][OneOffNotificationResendWithContextInput<Config>['contextName']]['generate']>;
|
|
36
|
+
sendAfter: Date | null;
|
|
37
|
+
subjectTemplate: string | null;
|
|
38
|
+
extraParams: InputJsonValue | null;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Database representation of a one-off notification.
|
|
42
|
+
* Includes all fields from input plus database-managed fields (id, status, timestamps, etc.)
|
|
43
|
+
*/
|
|
44
|
+
export type DatabaseOneOffNotification<Config extends BaseNotificationTypeConfig> = {
|
|
45
|
+
id: Config['NotificationIdType'];
|
|
46
|
+
emailOrPhone: string;
|
|
47
|
+
firstName: string;
|
|
48
|
+
lastName: string;
|
|
49
|
+
notificationType: NotificationType;
|
|
50
|
+
title: string | null;
|
|
51
|
+
bodyTemplate: string;
|
|
52
|
+
contextName: string & keyof Config['ContextMap'];
|
|
53
|
+
contextParameters: Parameters<Config['ContextMap'][DatabaseOneOffNotification<Config>['contextName']]['generate']>[0];
|
|
54
|
+
sendAfter: Date | null;
|
|
55
|
+
subjectTemplate: string | null;
|
|
56
|
+
status: NotificationStatus;
|
|
57
|
+
contextUsed: null | (ReturnType<Config['ContextMap'][DatabaseOneOffNotification<Config>['contextName']]['generate']> extends Promise<infer T> ? T : ReturnType<Config['ContextMap'][DatabaseOneOffNotification<Config>['contextName']]['generate']>);
|
|
58
|
+
extraParams: JsonValue;
|
|
59
|
+
adapterUsed: string | null;
|
|
60
|
+
sentAt: Date | null;
|
|
61
|
+
readAt: Date | null;
|
|
62
|
+
createdAt?: Date;
|
|
63
|
+
updatedAt?: Date;
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Union type representing any one-off notification (input, resend, or database).
|
|
67
|
+
*/
|
|
68
|
+
export type OneOffNotification<Config extends BaseNotificationTypeConfig> = OneOffNotificationInput<Config> | OneOffNotificationResendWithContextInput<Config> | DatabaseOneOffNotification<Config>;
|