vintasend 0.2.2 → 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 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 no-sql database with a blink of an eye without affecting how you send the notifications.
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 da database.
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 send by an external service.
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
- ### Officially supported packages
207
+ #### Officially supported packages
128
208
 
129
- #### Backends
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
- #### Adapters
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
- #### Template Renderers
217
+ ##### Template Renderers
138
218
  * **[vintasend-pug](https://github.com/vintasoftware/vintasend-pug/)**: Renders emails using Pug.
139
219
 
140
- #### Loggers
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; } });
@@ -1,9 +1,13 @@
1
1
  import type { NotificationType } from '../../types/notification-type';
2
- import type { DatabaseNotification } from '../../types/notification';
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: DatabaseNotification<Config>, context: JsonValue): Promise<void>;
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<DatabaseNotification<Config>[]>;
6
- getPendingNotifications(page: number, pageSize: number): Promise<DatabaseNotification<Config>[]>;
7
- getAllFutureNotifications(): Promise<DatabaseNotification<Config>[]>;
8
- getFutureNotifications(page: number, pageSize: number): Promise<DatabaseNotification<Config>[]>;
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<DatabaseNotification<Config>[]>;
13
- getNotifications(page: number, pageSize: number): Promise<DatabaseNotification<Config>[]>;
14
- bulkPersistNotifications(notifications: Omit<Notification<Config>, 'id'>[]): Promise<Config['NotificationIdType'][]>;
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<DatabaseNotification<Config>>;
17
- markAsFailed(notificationId: Config['NotificationIdType'], checkIsPending: boolean): Promise<DatabaseNotification<Config>>;
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<DatabaseNotification<Config> | null>;
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 { BaseNotificationAdapter } from './notification-adapters/base-notification-adapter';
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: DatabaseNotification<Config>): Promise<void>;
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
- getAllFutureNotifications(): Promise<DatabaseNotification<Config>[]>;
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<DatabaseNotification<Config>[]>;
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<DatabaseNotification<Config>[]>;
34
- getNotification(notificationId: Config['NotificationIdType'], forUpdate?: boolean): Promise<DatabaseNotification<Config> | null>;
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<Notification<Config>, 'id'>[]): Promise<Config['NotificationIdType'][]>;
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 notifications = await this.backend.getNotifications(pageNumber, batchSize);
267
- while (notifications.length > 0) {
345
+ let allNotifications = await this.backend.getNotifications(pageNumber, batchSize);
346
+ while (allNotifications.length > 0) {
268
347
  pageNumber += 1;
269
- const notificationsWitoutId = notifications.map((notification) => {
348
+ const notificationsWithoutId = allNotifications.map((notification) => {
270
349
  const { id, ...notificationWithoutId } = notification;
271
350
  return notificationWithoutId;
272
351
  });
273
- await destinationBackend.bulkPersistNotifications(notificationsWitoutId);
274
- notifications = await this.backend.getNotifications(pageNumber, batchSize);
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 { Notification } from '../../types/notification';
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: Notification<Config>, context: JsonObject): Promise<EmailTemplate>;
12
+ render(notification: AnyNotification<Config>, context: JsonObject): Promise<EmailTemplate>;
13
13
  }
@@ -1,6 +1,6 @@
1
1
  import type { JsonObject } from '../../types/json-values';
2
- import type { Notification } from '../../types/notification';
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: Notification<Config>, context: JsonObject): Promise<T>;
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>;
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vintasend",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "main": "dist/index.js",
5
5
  "files": [
6
6
  "dist"