vintasend 0.7.1 → 0.8.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
@@ -106,6 +106,59 @@ export function sendWelcomeEmail(userId: number) {
106
106
  }
107
107
  ```
108
108
 
109
+ ## Multi-Backend Configuration
110
+
111
+ VintaSend supports configuring multiple backends for redundancy, data distribution, and migration use cases.
112
+
113
+ ### Basic Setup
114
+
115
+ ```typescript
116
+ import { VintaSendFactory } from 'vintasend';
117
+
118
+ const vintasend = new VintaSendFactory<NotificationTypeConfig>().create({
119
+ adapters,
120
+ backend: primaryBackend,
121
+ additionalBackends: [replicaBackend],
122
+ logger,
123
+ contextGeneratorsMap,
124
+ });
125
+ ```
126
+
127
+ ### How It Works
128
+
129
+ - **Writes**: VintaSend writes to the primary backend first, then replicates to additional backends on a best-effort basis.
130
+ - **Reads**: Read methods use the primary backend by default, but support optional backend targeting by identifier.
131
+
132
+ ```typescript
133
+ // Read from primary backend (default)
134
+ const notification = await vintasend.getNotification(notificationId);
135
+
136
+ // Read from a specific backend
137
+ const notificationFromReplica = await vintasend.getNotification(
138
+ notificationId,
139
+ false,
140
+ 'replica-backend',
141
+ );
142
+ ```
143
+
144
+ ### Backend Management Operations
145
+
146
+ ```typescript
147
+ const report = await vintasend.verifyNotificationSync(notificationId);
148
+
149
+ if (!report.synced) {
150
+ await vintasend.replicateNotification(notificationId);
151
+ }
152
+
153
+ const backendStats = await vintasend.getBackendSyncStats();
154
+ ```
155
+
156
+ ### Failure Handling
157
+
158
+ - Primary backend failures fail the operation.
159
+ - Additional backend replication failures are logged and do not fail the primary operation.
160
+ - This keeps primary workflows available while still enabling redundancy.
161
+
109
162
  ## Attachment Support
110
163
 
111
164
  VintaSend supports file attachments for notifications with an extensible architecture that allows you to choose your preferred storage backend.
@@ -87,13 +87,22 @@ export declare const DEFAULT_BACKEND_FILTER_CAPABILITIES: {
87
87
  'stringLookups.caseInsensitive': boolean;
88
88
  };
89
89
  export interface BaseNotificationBackend<Config extends BaseNotificationTypeConfig> {
90
+ /**
91
+ * Get a unique identifier for this backend instance.
92
+ *
93
+ * Used to distinguish between multiple backend instances in a multi-backend setup.
94
+ * When not implemented, callers should use a fallback identifier strategy.
95
+ */
96
+ getBackendIdentifier?(): string;
90
97
  getAllPendingNotifications(): Promise<AnyDatabaseNotification<Config>[]>;
91
98
  getPendingNotifications(page: number, pageSize: number): Promise<AnyDatabaseNotification<Config>[]>;
92
99
  getAllFutureNotifications(): Promise<AnyDatabaseNotification<Config>[]>;
93
100
  getFutureNotifications(page: number, pageSize: number): Promise<AnyDatabaseNotification<Config>[]>;
94
101
  getAllFutureNotificationsFromUser(userId: Config['UserIdType']): Promise<DatabaseNotification<Config>[]>;
95
102
  getFutureNotificationsFromUser(userId: Config['UserIdType'], page: number, pageSize: number): Promise<DatabaseNotification<Config>[]>;
96
- persistNotification(notification: Omit<Notification<Config>, 'id'>): Promise<DatabaseNotification<Config>>;
103
+ persistNotification(notification: Omit<Notification<Config>, 'id'> & {
104
+ id?: Config['NotificationIdType'];
105
+ }): Promise<DatabaseNotification<Config>>;
97
106
  getAllNotifications(): Promise<AnyDatabaseNotification<Config>[]>;
98
107
  getNotifications(page: number, pageSize: number): Promise<AnyDatabaseNotification<Config>[]>;
99
108
  bulkPersistNotifications(notifications: Omit<AnyNotification<Config>, 'id'>[]): Promise<Config['NotificationIdType'][]>;
@@ -107,7 +116,9 @@ export interface BaseNotificationBackend<Config extends BaseNotificationTypeConf
107
116
  filterInAppUnreadNotifications(userId: Config['UserIdType'], page: number, pageSize: number): Promise<DatabaseNotification<Config>[]>;
108
117
  getUserEmailFromNotification(notificationId: Config['NotificationIdType']): Promise<string | undefined>;
109
118
  storeAdapterAndContextUsed(notificationId: Config['NotificationIdType'], adapterKey: string, context: InputJsonValue): Promise<void>;
110
- persistOneOffNotification(notification: Omit<OneOffNotificationInput<Config>, 'id'>): Promise<DatabaseOneOffNotification<Config>>;
119
+ persistOneOffNotification(notification: Omit<OneOffNotificationInput<Config>, 'id'> & {
120
+ id?: Config['NotificationIdType'];
121
+ }): Promise<DatabaseOneOffNotification<Config>>;
111
122
  persistOneOffNotificationUpdate(notificationId: Config['NotificationIdType'], notification: Partial<Omit<OneOffNotificationInput<Config>, 'id'>>): Promise<DatabaseOneOffNotification<Config>>;
112
123
  getOneOffNotification(notificationId: Config['NotificationIdType'], forUpdate: boolean): Promise<DatabaseOneOffNotification<Config> | null>;
113
124
  getAllOneOffNotifications(): Promise<DatabaseOneOffNotification<Config>[]>;
@@ -22,6 +22,7 @@ type RenderEmailTemplateContextInput<Config extends BaseNotificationTypeConfig>
22
22
  type VintaSendFactoryCreateParams<Config extends BaseNotificationTypeConfig, AdaptersList extends BaseNotificationAdapter<BaseNotificationTemplateRenderer<Config>, Config>[], Backend extends BaseNotificationBackend<Config>, Logger extends BaseLogger, QueueService extends BaseNotificationQueueService<Config>, AttachmentMgr extends BaseAttachmentManager> = {
23
23
  adapters: AdaptersList;
24
24
  backend: Backend;
25
+ additionalBackends?: Backend[];
25
26
  logger: Logger;
26
27
  contextGeneratorsMap: BaseNotificationTypeConfig['ContextMap'];
27
28
  queueService?: QueueService;
@@ -60,7 +61,7 @@ export declare class VintaSendFactory<Config extends BaseNotificationTypeConfig>
60
61
  /**
61
62
  * @deprecated Use the object parameter overload instead.
62
63
  */
63
- create<AdaptersList extends BaseNotificationAdapter<BaseNotificationTemplateRenderer<Config>, Config>[], Backend extends BaseNotificationBackend<Config>, Logger extends BaseLogger, QueueService extends BaseNotificationQueueService<Config>, AttachmentMgr extends BaseAttachmentManager>(adapters: AdaptersList, backend: Backend, logger: Logger, contextGeneratorsMap: BaseNotificationTypeConfig['ContextMap'], queueService?: QueueService, attachmentManager?: AttachmentMgr, options?: VintaSendOptions, gitCommitShaProvider?: BaseGitCommitShaProvider): VintaSend<Config, AdaptersList, Backend, Logger, QueueService, AttachmentMgr>;
64
+ create<AdaptersList extends BaseNotificationAdapter<BaseNotificationTemplateRenderer<Config>, Config>[], Backend extends BaseNotificationBackend<Config>, Logger extends BaseLogger, QueueService extends BaseNotificationQueueService<Config>, AttachmentMgr extends BaseAttachmentManager>(adapters: AdaptersList, backend: Backend, logger: Logger, contextGeneratorsMap: BaseNotificationTypeConfig['ContextMap'], queueService?: QueueService, attachmentManager?: AttachmentMgr, options?: VintaSendOptions, gitCommitShaProvider?: BaseGitCommitShaProvider, additionalBackends?: Backend[]): VintaSend<Config, AdaptersList, Backend, Logger, QueueService, AttachmentMgr>;
64
65
  }
65
66
  export declare class VintaSend<Config extends BaseNotificationTypeConfig, AdaptersList extends BaseNotificationAdapter<BaseNotificationTemplateRenderer<Config>, Config>[], Backend extends BaseNotificationBackend<Config>, Logger extends BaseLogger, QueueService extends BaseNotificationQueueService<Config>, AttachmentMgr extends BaseAttachmentManager> {
66
67
  private adapters;
@@ -71,7 +72,25 @@ export declare class VintaSend<Config extends BaseNotificationTypeConfig, Adapte
71
72
  private options;
72
73
  private gitCommitShaProvider?;
73
74
  private contextGeneratorsMap;
74
- constructor(adapters: AdaptersList, backend: Backend, logger: Logger, contextGeneratorsMap: Config['ContextMap'], queueService?: QueueService | undefined, attachmentManager?: AttachmentMgr | undefined, options?: VintaSendOptions, gitCommitShaProvider?: BaseGitCommitShaProvider | undefined);
75
+ private backends;
76
+ private primaryBackendIdentifier;
77
+ /**
78
+ * Creates a VintaSend instance with one primary backend and optional additional backends.
79
+ *
80
+ * In multi-backend mode:
81
+ * - writes execute on the primary backend first
82
+ * - additional backends receive best-effort replication
83
+ * - reads default to primary unless a backend identifier is provided
84
+ */
85
+ constructor(adapters: AdaptersList, backend: Backend, logger: Logger, contextGeneratorsMap: Config['ContextMap'], queueService?: QueueService | undefined, attachmentManager?: AttachmentMgr | undefined, options?: VintaSendOptions, gitCommitShaProvider?: BaseGitCommitShaProvider | undefined, additionalBackends?: Backend[]);
86
+ private getBackendIdentifier;
87
+ private getBackend;
88
+ private getAdditionalBackends;
89
+ getPrimaryBackendIdentifier(): string;
90
+ getAllBackendIdentifiers(): string[];
91
+ getAdditionalBackendIdentifiers(): string[];
92
+ hasBackend(identifier: string): boolean;
93
+ private executeMultiBackendWrite;
75
94
  registerQueueService(queueService: QueueService): void;
76
95
  private normalizeGitCommitSha;
77
96
  private resolveGitCommitShaForExecution;
@@ -103,19 +122,34 @@ export declare class VintaSend<Config extends BaseNotificationTypeConfig, Adapte
103
122
  * @throws Error if the format is invalid
104
123
  */
105
124
  private validateEmailOrPhone;
106
- getAllFutureNotifications(): Promise<AnyDatabaseNotification<Config>[]>;
107
- getAllFutureNotificationsFromUser(userId: Config['NotificationIdType']): Promise<DatabaseNotification<Config>[]>;
108
- getFutureNotificationsFromUser(userId: Config['NotificationIdType'], page: number, pageSize: number): Promise<DatabaseNotification<Config>[]>;
109
- getFutureNotifications(page: number, pageSize: number): Promise<AnyDatabaseNotification<Config>[]>;
125
+ getAllFutureNotifications(backendIdentifier?: string): Promise<AnyDatabaseNotification<Config>[]>;
126
+ getAllFutureNotificationsFromUser(userId: Config['NotificationIdType'], backendIdentifier?: string): Promise<DatabaseNotification<Config>[]>;
127
+ getFutureNotificationsFromUser(userId: Config['NotificationIdType'], page: number, pageSize: number, backendIdentifier?: string): Promise<DatabaseNotification<Config>[]>;
128
+ getFutureNotifications(page: number, pageSize: number, backendIdentifier?: string): Promise<AnyDatabaseNotification<Config>[]>;
110
129
  getNotificationContext<ContextName extends string & keyof Config['ContextMap']>(contextName: ContextName, parameters: Parameters<ReturnType<typeof this.contextGeneratorsMap.getContextGenerator<ContextName>>['generate']>[0]): Promise<JsonObject>;
111
130
  renderEmailTemplateFromContent(notification: AnyDatabaseNotification<Config>, templateContent: EmailTemplateContent, contextInput: RenderEmailTemplateContextInput<Config>): Promise<EmailTemplate>;
112
131
  sendPendingNotifications(): Promise<void>;
113
- getPendingNotifications(page: number, pageSize: number): Promise<AnyDatabaseNotification<Config>[]>;
114
- getNotifications(page: number, pageSize: number): Promise<AnyDatabaseNotification<Config>[]>;
115
- getOneOffNotifications(page: number, pageSize: number): Promise<DatabaseOneOffNotification<Config>[]>;
116
- getNotification(notificationId: Config['NotificationIdType'], forUpdate?: boolean): Promise<AnyDatabaseNotification<Config> | null>;
117
- filterNotifications(filter: NotificationFilterFields<Config>, page: number, pageSize: number): Promise<AnyDatabaseNotification<Config>[]>;
118
- getBackendSupportedFilterCapabilities(): Promise<{
132
+ /**
133
+ * Gets pending notifications from the primary backend by default or from a specific backend.
134
+ */
135
+ getPendingNotifications(page: number, pageSize: number, backendIdentifier?: string): Promise<AnyDatabaseNotification<Config>[]>;
136
+ /**
137
+ * Gets notifications from the primary backend by default or from a specific backend.
138
+ */
139
+ getNotifications(page: number, pageSize: number, backendIdentifier?: string): Promise<AnyDatabaseNotification<Config>[]>;
140
+ getOneOffNotifications(page: number, pageSize: number, backendIdentifier?: string): Promise<DatabaseOneOffNotification<Config>[]>;
141
+ /**
142
+ * Gets a notification by ID from the primary backend by default or from a specific backend.
143
+ */
144
+ getNotification(notificationId: Config['NotificationIdType'], forUpdate?: boolean, backendIdentifier?: string): Promise<AnyDatabaseNotification<Config> | null>;
145
+ /**
146
+ * Filters notifications in the primary backend by default or in a specific backend.
147
+ */
148
+ filterNotifications(filter: NotificationFilterFields<Config>, page: number, pageSize: number, backendIdentifier?: string): Promise<AnyDatabaseNotification<Config>[]>;
149
+ /**
150
+ * Returns the effective filter capabilities for the primary backend by default or for a specific backend.
151
+ */
152
+ getBackendSupportedFilterCapabilities(backendIdentifier?: string): Promise<{
119
153
  'logical.and': boolean;
120
154
  'logical.or': boolean;
121
155
  'logical.not': boolean;
@@ -143,15 +177,65 @@ export declare class VintaSend<Config extends BaseNotificationTypeConfig, Adapte
143
177
  *
144
178
  * @param notificationId - The ID of the one-off notification to retrieve
145
179
  * @param forUpdate - Whether the notification is being retrieved for update (default: false)
180
+ * @param backendIdentifier - Optional backend identifier. When omitted, the primary backend is used.
146
181
  * @returns The one-off notification or null if not found
147
182
  */
148
- getOneOffNotification(notificationId: Config['NotificationIdType'], forUpdate?: boolean): Promise<DatabaseOneOffNotification<Config> | null>;
183
+ getOneOffNotification(notificationId: Config['NotificationIdType'], forUpdate?: boolean, backendIdentifier?: string): Promise<DatabaseOneOffNotification<Config> | null>;
149
184
  markRead(notificationId: Config['NotificationIdType'], checkIsSent?: boolean): Promise<DatabaseNotification<Config>>;
150
- getInAppUnread(userId: Config['NotificationIdType']): Promise<DatabaseNotification<Config>[]>;
185
+ /**
186
+ * Gets unread in-app notifications from the primary backend by default or from a specific backend.
187
+ */
188
+ getInAppUnread(userId: Config['NotificationIdType'], backendIdentifier?: string): Promise<DatabaseNotification<Config>[]>;
151
189
  cancelNotification(notificationId: Config['NotificationIdType']): Promise<void>;
152
190
  resendNotification(notificationId: Config['NotificationIdType'], useStoredContextIfAvailable?: boolean): Promise<DatabaseNotification<Config> | undefined>;
153
191
  delayedSend(notificationId: Config['NotificationIdType']): Promise<void>;
154
192
  bulkPersistNotifications(notifications: Omit<AnyNotification<Config>, 'id'>[]): Promise<Config['NotificationIdType'][]>;
155
- migrateToBackend<DestinationBackend extends BaseNotificationBackend<Config>>(destinationBackend: DestinationBackend, batchSize?: number): Promise<void>;
193
+ private normalizeValueForSyncComparison;
194
+ /**
195
+ * Verifies whether a notification is synchronized across all configured backends.
196
+ *
197
+ * The report includes backend-level existence/errors and field-level discrepancies
198
+ * when comparing additional backends against the primary backend.
199
+ */
200
+ verifyNotificationSync(notificationId: Config['NotificationIdType']): Promise<{
201
+ synced: boolean;
202
+ backends: Record<string, {
203
+ exists: boolean;
204
+ notification?: AnyDatabaseNotification<Config>;
205
+ error?: string;
206
+ }>;
207
+ discrepancies: string[];
208
+ }>;
209
+ /**
210
+ * Replicates one notification from the primary backend to all additional backends.
211
+ *
212
+ * If a notification already exists in an additional backend, it is updated.
213
+ * Otherwise, it is created.
214
+ */
215
+ replicateNotification(notificationId: Config['NotificationIdType']): Promise<{
216
+ successes: string[];
217
+ failures: {
218
+ backend: string;
219
+ error: string;
220
+ }[];
221
+ }>;
222
+ /**
223
+ * Returns a lightweight health snapshot for each configured backend.
224
+ */
225
+ getBackendSyncStats(): Promise<{
226
+ backends: Record<string, {
227
+ totalNotifications: number;
228
+ status: 'healthy' | 'error';
229
+ error?: string;
230
+ }>;
231
+ }>;
232
+ /**
233
+ * Migrates notifications from a source backend (primary by default) to a destination backend.
234
+ *
235
+ * @param destinationBackend - Backend receiving migrated records
236
+ * @param batchSize - Page size used while iterating source records
237
+ * @param sourceBackendIdentifier - Optional source backend identifier. Defaults to primary backend.
238
+ */
239
+ migrateToBackend<DestinationBackend extends BaseNotificationBackend<Config>>(destinationBackend: DestinationBackend, batchSize?: number, sourceBackendIdentifier?: string): Promise<void>;
156
240
  }
157
241
  export {};
@@ -7,14 +7,14 @@ const notification_context_generators_map_1 = require("./notification-context-ge
7
7
  class VintaSendFactory {
8
8
  create(adaptersOrParams, backend, logger, contextGeneratorsMap, queueService, attachmentManager, options = {
9
9
  raiseErrorOnFailedSend: false,
10
- }, gitCommitShaProvider) {
10
+ }, gitCommitShaProvider, additionalBackends) {
11
11
  var _a;
12
12
  if (!Array.isArray(adaptersOrParams)) {
13
13
  return new VintaSend(adaptersOrParams.adapters, adaptersOrParams.backend, adaptersOrParams.logger, adaptersOrParams.contextGeneratorsMap, adaptersOrParams.queueService, adaptersOrParams.attachmentManager, (_a = adaptersOrParams.options) !== null && _a !== void 0 ? _a : {
14
14
  raiseErrorOnFailedSend: false,
15
- }, adaptersOrParams.gitCommitShaProvider);
15
+ }, adaptersOrParams.gitCommitShaProvider, adaptersOrParams.additionalBackends);
16
16
  }
17
- return new VintaSend(adaptersOrParams, backend, logger, contextGeneratorsMap, queueService, attachmentManager, options, gitCommitShaProvider);
17
+ return new VintaSend(adaptersOrParams, backend, logger, contextGeneratorsMap, queueService, attachmentManager, options, gitCommitShaProvider, additionalBackends);
18
18
  }
19
19
  }
20
20
  exports.VintaSendFactory = VintaSendFactory;
@@ -25,9 +25,17 @@ function hasAttachmentManagerInjection(backend) {
25
25
  typeof backend.injectAttachmentManager === 'function');
26
26
  }
27
27
  class VintaSend {
28
+ /**
29
+ * Creates a VintaSend instance with one primary backend and optional additional backends.
30
+ *
31
+ * In multi-backend mode:
32
+ * - writes execute on the primary backend first
33
+ * - additional backends receive best-effort replication
34
+ * - reads default to primary unless a backend identifier is provided
35
+ */
28
36
  constructor(adapters, backend, logger, contextGeneratorsMap, queueService, attachmentManager, options = {
29
37
  raiseErrorOnFailedSend: false,
30
- }, gitCommitShaProvider) {
38
+ }, gitCommitShaProvider, additionalBackends = []) {
31
39
  this.adapters = adapters;
32
40
  this.backend = backend;
33
41
  this.logger = logger;
@@ -36,6 +44,25 @@ class VintaSend {
36
44
  this.options = options;
37
45
  this.gitCommitShaProvider = gitCommitShaProvider;
38
46
  this.contextGeneratorsMap = new notification_context_generators_map_1.NotificationContextGeneratorsMap(contextGeneratorsMap);
47
+ this.backends = new Map();
48
+ this.primaryBackendIdentifier = this.getBackendIdentifier(backend);
49
+ this.backends.set(this.primaryBackendIdentifier, backend);
50
+ for (const additionalBackend of additionalBackends) {
51
+ const additionalBackendIdentifier = this.getBackendIdentifier(additionalBackend);
52
+ if (this.backends.has(additionalBackendIdentifier)) {
53
+ throw new Error(`Duplicate backend identifier: ${additionalBackendIdentifier}`);
54
+ }
55
+ this.backends.set(additionalBackendIdentifier, additionalBackend);
56
+ if (typeof additionalBackend.injectLogger === 'function') {
57
+ additionalBackend.injectLogger(logger);
58
+ }
59
+ if (this.attachmentManager && hasAttachmentManagerInjection(additionalBackend)) {
60
+ additionalBackend.injectAttachmentManager(this.attachmentManager);
61
+ }
62
+ }
63
+ if (this.getAdditionalBackends().length !== additionalBackends.length) {
64
+ throw new Error('Invalid additional backends configuration');
65
+ }
39
66
  for (const adapter of adapters) {
40
67
  adapter.injectBackend(backend);
41
68
  adapter.injectLogger(logger);
@@ -55,6 +82,56 @@ class VintaSend {
55
82
  backend.injectAttachmentManager(this.attachmentManager);
56
83
  }
57
84
  }
85
+ getBackendIdentifier(backend) {
86
+ if (typeof backend.getBackendIdentifier === 'function') {
87
+ return backend.getBackendIdentifier();
88
+ }
89
+ return `backend-${this.backends.size}`;
90
+ }
91
+ getBackend(identifier) {
92
+ if (!identifier) {
93
+ return this.backend;
94
+ }
95
+ const backend = this.backends.get(identifier);
96
+ if (!backend) {
97
+ throw new Error(`Backend not found: ${identifier}`);
98
+ }
99
+ return backend;
100
+ }
101
+ getAdditionalBackends() {
102
+ return Array.from(this.backends.entries())
103
+ .filter(([identifier]) => identifier !== this.primaryBackendIdentifier)
104
+ .map(([, backend]) => backend);
105
+ }
106
+ getPrimaryBackendIdentifier() {
107
+ return this.primaryBackendIdentifier;
108
+ }
109
+ getAllBackendIdentifiers() {
110
+ return Array.from(this.backends.keys());
111
+ }
112
+ getAdditionalBackendIdentifiers() {
113
+ return this.getAllBackendIdentifiers().filter((identifier) => identifier !== this.primaryBackendIdentifier);
114
+ }
115
+ hasBackend(identifier) {
116
+ return this.backends.has(identifier);
117
+ }
118
+ async executeMultiBackendWrite(operation, primaryWrite, additionalWrite) {
119
+ const primaryResult = await primaryWrite(this.backend);
120
+ if (!additionalWrite) {
121
+ return primaryResult;
122
+ }
123
+ for (const additionalBackend of this.getAdditionalBackends()) {
124
+ const backendIdentifier = this.getBackendIdentifier(additionalBackend);
125
+ try {
126
+ await additionalWrite(additionalBackend, primaryResult);
127
+ this.logger.info(`${operation} replicated to backend ${backendIdentifier}`);
128
+ }
129
+ catch (replicationError) {
130
+ this.logger.error(`Failed to replicate ${operation} to backend ${backendIdentifier}: ${replicationError}`);
131
+ }
132
+ }
133
+ return primaryResult;
134
+ }
58
135
  registerQueueService(queueService) {
59
136
  this.queueService = queueService;
60
137
  }
@@ -85,19 +162,26 @@ class VintaSend {
85
162
  const oneOffNotificationUpdate = {
86
163
  gitCommitSha,
87
164
  };
88
- return this.backend.persistOneOffNotificationUpdate(notification.id, oneOffNotificationUpdate);
165
+ return this.executeMultiBackendWrite('persistOneOffNotificationGitCommitSha', async (backend) => {
166
+ return backend.persistOneOffNotificationUpdate(notification.id, oneOffNotificationUpdate);
167
+ }, async (backend) => {
168
+ await backend.persistOneOffNotificationUpdate(notification.id, oneOffNotificationUpdate);
169
+ });
89
170
  }
90
171
  const notificationUpdate = {
91
172
  gitCommitSha,
92
173
  };
93
- return this.backend.persistNotificationUpdate(notification.id, notificationUpdate);
174
+ return this.executeMultiBackendWrite('persistNotificationGitCommitSha', async (backend) => {
175
+ return backend.persistNotificationUpdate(notification.id, notificationUpdate);
176
+ }, async (backend) => {
177
+ await backend.persistNotificationUpdate(notification.id, notificationUpdate);
178
+ });
94
179
  }
95
180
  async resolveAndPersistGitCommitShaForExecution(notification) {
96
181
  const gitCommitSha = await this.resolveGitCommitShaForExecution();
97
182
  return this.persistGitCommitShaForExecution(notification, gitCommitSha);
98
183
  }
99
184
  async send(notification) {
100
- var _a;
101
185
  const notificationWithExecutionGitCommitSha = await this.resolveAndPersistGitCommitShaForExecution(notification);
102
186
  const adaptersOfType = this.adapters.filter((adapter) => adapter.notificationType === notificationWithExecutionGitCommitSha.notificationType);
103
187
  if (adaptersOfType.length === 0) {
@@ -152,7 +236,11 @@ class VintaSend {
152
236
  catch (sendError) {
153
237
  this.logger.error(`Error sending notification ${notificationWithExecutionGitCommitSha.id} with adapter ${adapter.key}: ${sendError}`);
154
238
  try {
155
- await this.backend.markAsFailed(notificationWithExecutionGitCommitSha.id, true);
239
+ await this.executeMultiBackendWrite('markAsFailed', async (backend) => {
240
+ return backend.markAsFailed(notificationWithExecutionGitCommitSha.id, true);
241
+ }, async (backend) => {
242
+ await backend.markAsFailed(notificationWithExecutionGitCommitSha.id, true);
243
+ });
156
244
  }
157
245
  catch (markFailedError) {
158
246
  this.logger.error(`Error marking notification ${notificationWithExecutionGitCommitSha.id} as failed: ${markFailedError}`);
@@ -160,13 +248,23 @@ class VintaSend {
160
248
  continue;
161
249
  }
162
250
  try {
163
- await this.backend.markAsSent(notificationWithExecutionGitCommitSha.id, true);
251
+ await this.executeMultiBackendWrite('markAsSent', async (backend) => {
252
+ return backend.markAsSent(notificationWithExecutionGitCommitSha.id, true);
253
+ }, async (backend) => {
254
+ await backend.markAsSent(notificationWithExecutionGitCommitSha.id, true);
255
+ });
164
256
  }
165
257
  catch (markSentError) {
166
258
  this.logger.error(`Error marking notification ${notificationWithExecutionGitCommitSha.id} as sent: ${markSentError}`);
167
259
  }
168
260
  try {
169
- await this.backend.storeAdapterAndContextUsed(notificationWithExecutionGitCommitSha.id, (_a = adapter.key) !== null && _a !== void 0 ? _a : 'unknown', context !== null && context !== void 0 ? context : {});
261
+ await this.executeMultiBackendWrite('storeAdapterAndContextUsed', async (backend) => {
262
+ var _a;
263
+ await backend.storeAdapterAndContextUsed(notificationWithExecutionGitCommitSha.id, (_a = adapter.key) !== null && _a !== void 0 ? _a : 'unknown', context !== null && context !== void 0 ? context : {});
264
+ }, async (backend) => {
265
+ var _a;
266
+ await backend.storeAdapterAndContextUsed(notificationWithExecutionGitCommitSha.id, (_a = adapter.key) !== null && _a !== void 0 ? _a : 'unknown', context !== null && context !== void 0 ? context : {});
267
+ });
170
268
  }
171
269
  catch (storeContextError) {
172
270
  this.logger.error(`Error storing adapter and context for notification ${notificationWithExecutionGitCommitSha.id}: ${storeContextError}`);
@@ -174,7 +272,14 @@ class VintaSend {
174
272
  }
175
273
  }
176
274
  async createNotification(notification) {
177
- const createdNotification = await this.backend.persistNotification(notification);
275
+ const createdNotification = await this.executeMultiBackendWrite('createNotification', async (backend) => {
276
+ return backend.persistNotification(notification);
277
+ }, async (backend, primaryResult) => {
278
+ await backend.persistNotification({
279
+ ...notification,
280
+ id: primaryResult.id,
281
+ });
282
+ });
178
283
  this.logger.info(`Notification ${createdNotification.id} created`);
179
284
  if (!notification.sendAfter || notification.sendAfter <= new Date()) {
180
285
  this.logger.info(`Notification ${createdNotification.id} sent immediately because sendAfter is null or in the past`);
@@ -186,7 +291,11 @@ class VintaSend {
186
291
  return createdNotification;
187
292
  }
188
293
  async updateNotification(notificationId, notification) {
189
- const updatedNotification = this.backend.persistNotificationUpdate(notificationId, notification);
294
+ const updatedNotification = this.executeMultiBackendWrite('updateNotification', async (backend) => {
295
+ return backend.persistNotificationUpdate(notificationId, notification);
296
+ }, async (backend) => {
297
+ await backend.persistNotificationUpdate(notificationId, notification);
298
+ });
190
299
  this.logger.info(`Notification ${notificationId} updated`);
191
300
  return updatedNotification;
192
301
  }
@@ -200,7 +309,14 @@ class VintaSend {
200
309
  async createOneOffNotification(notification) {
201
310
  // Validate email or phone format
202
311
  this.validateEmailOrPhone(notification.emailOrPhone);
203
- const createdNotification = await this.backend.persistOneOffNotification(notification);
312
+ const createdNotification = await this.executeMultiBackendWrite('createOneOffNotification', async (backend) => {
313
+ return backend.persistOneOffNotification(notification);
314
+ }, async (backend, primaryResult) => {
315
+ await backend.persistOneOffNotification({
316
+ ...notification,
317
+ id: primaryResult.id,
318
+ });
319
+ });
204
320
  this.logger.info(`One-off notification ${createdNotification.id} created`);
205
321
  if (!notification.sendAfter || notification.sendAfter <= new Date()) {
206
322
  this.logger.info(`One-off notification ${createdNotification.id} sent immediately`);
@@ -223,7 +339,11 @@ class VintaSend {
223
339
  if (notification.emailOrPhone !== undefined) {
224
340
  this.validateEmailOrPhone(notification.emailOrPhone);
225
341
  }
226
- const updatedNotification = await this.backend.persistOneOffNotificationUpdate(notificationId, notification);
342
+ const updatedNotification = await this.executeMultiBackendWrite('updateOneOffNotification', async (backend) => {
343
+ return backend.persistOneOffNotificationUpdate(notificationId, notification);
344
+ }, async (backend) => {
345
+ await backend.persistOneOffNotificationUpdate(notificationId, notification);
346
+ });
227
347
  this.logger.info(`One-off notification ${notificationId} updated`);
228
348
  if (!updatedNotification.sendAfter || updatedNotification.sendAfter <= new Date()) {
229
349
  this.logger.info(`One-off notification ${notificationId} sent after update`);
@@ -250,17 +370,17 @@ class VintaSend {
250
370
  throw new Error('Invalid email or phone format');
251
371
  }
252
372
  }
253
- async getAllFutureNotifications() {
254
- return this.backend.getAllFutureNotifications();
373
+ async getAllFutureNotifications(backendIdentifier) {
374
+ return this.getBackend(backendIdentifier).getAllFutureNotifications();
255
375
  }
256
- async getAllFutureNotificationsFromUser(userId) {
257
- return this.backend.getAllFutureNotificationsFromUser(userId);
376
+ async getAllFutureNotificationsFromUser(userId, backendIdentifier) {
377
+ return this.getBackend(backendIdentifier).getAllFutureNotificationsFromUser(userId);
258
378
  }
259
- async getFutureNotificationsFromUser(userId, page, pageSize) {
260
- return this.backend.getFutureNotificationsFromUser(userId, page, pageSize);
379
+ async getFutureNotificationsFromUser(userId, page, pageSize, backendIdentifier) {
380
+ return this.getBackend(backendIdentifier).getFutureNotificationsFromUser(userId, page, pageSize);
261
381
  }
262
- async getFutureNotifications(page, pageSize) {
263
- return this.backend.getFutureNotifications(page, pageSize);
382
+ async getFutureNotifications(page, pageSize, backendIdentifier) {
383
+ return this.getBackend(backendIdentifier).getFutureNotifications(page, pageSize);
264
384
  }
265
385
  async getNotificationContext(contextName, parameters) {
266
386
  const context = this.contextGeneratorsMap.getContextGenerator(contextName).generate(parameters);
@@ -281,48 +401,78 @@ class VintaSend {
281
401
  return adapter.renderFromTemplateContent(notification, templateContent, context);
282
402
  }
283
403
  async sendPendingNotifications() {
284
- const pendingNotifications = await this.backend.getAllPendingNotifications();
404
+ const pendingNotifications = await this.getBackend().getAllPendingNotifications();
285
405
  await Promise.all(pendingNotifications.map((notification) => this.send(notification)));
286
406
  }
287
- async getPendingNotifications(page, pageSize) {
288
- return this.backend.getPendingNotifications(page, pageSize);
407
+ /**
408
+ * Gets pending notifications from the primary backend by default or from a specific backend.
409
+ */
410
+ async getPendingNotifications(page, pageSize, backendIdentifier) {
411
+ return this.getBackend(backendIdentifier).getPendingNotifications(page, pageSize);
289
412
  }
290
- async getNotifications(page, pageSize) {
291
- return this.backend.getNotifications(page, pageSize);
413
+ /**
414
+ * Gets notifications from the primary backend by default or from a specific backend.
415
+ */
416
+ async getNotifications(page, pageSize, backendIdentifier) {
417
+ return this.getBackend(backendIdentifier).getNotifications(page, pageSize);
292
418
  }
293
- async getOneOffNotifications(page, pageSize) {
294
- return this.backend.getOneOffNotifications(page, pageSize);
419
+ async getOneOffNotifications(page, pageSize, backendIdentifier) {
420
+ return this.getBackend(backendIdentifier).getOneOffNotifications(page, pageSize);
295
421
  }
296
- async getNotification(notificationId, forUpdate = false) {
297
- return this.backend.getNotification(notificationId, forUpdate);
422
+ /**
423
+ * Gets a notification by ID from the primary backend by default or from a specific backend.
424
+ */
425
+ async getNotification(notificationId, forUpdate = false, backendIdentifier) {
426
+ return this.getBackend(backendIdentifier).getNotification(notificationId, forUpdate);
298
427
  }
299
- async filterNotifications(filter, page, pageSize) {
300
- return this.backend.filterNotifications(filter, page, pageSize);
428
+ /**
429
+ * Filters notifications in the primary backend by default or in a specific backend.
430
+ */
431
+ async filterNotifications(filter, page, pageSize, backendIdentifier) {
432
+ return this.getBackend(backendIdentifier).filterNotifications(filter, page, pageSize);
301
433
  }
302
- async getBackendSupportedFilterCapabilities() {
434
+ /**
435
+ * Returns the effective filter capabilities for the primary backend by default or for a specific backend.
436
+ */
437
+ async getBackendSupportedFilterCapabilities(backendIdentifier) {
303
438
  var _a, _b, _c;
304
- return { ...base_notification_backend_1.DEFAULT_BACKEND_FILTER_CAPABILITIES, ...((_c = (_b = (_a = this.backend).getFilterCapabilities) === null || _b === void 0 ? void 0 : _b.call(_a)) !== null && _c !== void 0 ? _c : {}) };
439
+ return {
440
+ ...base_notification_backend_1.DEFAULT_BACKEND_FILTER_CAPABILITIES,
441
+ ...((_c = (_b = (_a = this.getBackend(backendIdentifier)).getFilterCapabilities) === null || _b === void 0 ? void 0 : _b.call(_a)) !== null && _c !== void 0 ? _c : {}),
442
+ };
305
443
  }
306
444
  /**
307
445
  * Gets a one-off notification by ID.
308
446
  *
309
447
  * @param notificationId - The ID of the one-off notification to retrieve
310
448
  * @param forUpdate - Whether the notification is being retrieved for update (default: false)
449
+ * @param backendIdentifier - Optional backend identifier. When omitted, the primary backend is used.
311
450
  * @returns The one-off notification or null if not found
312
451
  */
313
- async getOneOffNotification(notificationId, forUpdate = false) {
314
- return this.backend.getOneOffNotification(notificationId, forUpdate);
452
+ async getOneOffNotification(notificationId, forUpdate = false, backendIdentifier) {
453
+ return this.getBackend(backendIdentifier).getOneOffNotification(notificationId, forUpdate);
315
454
  }
316
455
  async markRead(notificationId, checkIsSent = true) {
317
- const notification = await this.backend.markAsRead(notificationId, checkIsSent);
456
+ const notification = await this.executeMultiBackendWrite('markRead', async (backend) => {
457
+ return backend.markAsRead(notificationId, checkIsSent);
458
+ }, async (backend) => {
459
+ await backend.markAsRead(notificationId, checkIsSent);
460
+ });
318
461
  this.logger.info(`Notification ${notificationId} marked as read`);
319
462
  return notification;
320
463
  }
321
- async getInAppUnread(userId) {
322
- return this.backend.filterAllInAppUnreadNotifications(userId);
464
+ /**
465
+ * Gets unread in-app notifications from the primary backend by default or from a specific backend.
466
+ */
467
+ async getInAppUnread(userId, backendIdentifier) {
468
+ return this.getBackend(backendIdentifier).filterAllInAppUnreadNotifications(userId);
323
469
  }
324
470
  async cancelNotification(notificationId) {
325
- await this.backend.cancelNotification(notificationId);
471
+ await this.executeMultiBackendWrite('cancelNotification', async (backend) => {
472
+ await backend.cancelNotification(notificationId);
473
+ }, async (backend) => {
474
+ await backend.cancelNotification(notificationId);
475
+ });
326
476
  this.logger.info(`Notification ${notificationId} cancelled`);
327
477
  }
328
478
  async resendNotification(notificationId, useStoredContextIfAvailable = false) {
@@ -415,32 +565,222 @@ class VintaSend {
415
565
  catch (sendError) {
416
566
  this.logger.error(`Error sending notification ${notificationWithExecutionGitCommitSha.id} with adapter ${adapter.key}: ${sendError}`);
417
567
  try {
418
- await this.backend.markAsFailed(notificationWithExecutionGitCommitSha.id, true);
568
+ await this.executeMultiBackendWrite('markAsFailed', async (backend) => {
569
+ return backend.markAsFailed(notificationWithExecutionGitCommitSha.id, true);
570
+ }, async (backend) => {
571
+ await backend.markAsFailed(notificationWithExecutionGitCommitSha.id, true);
572
+ });
419
573
  }
420
574
  catch (markFailedError) {
421
575
  this.logger.error(`Error marking notification ${notificationWithExecutionGitCommitSha.id} as failed: ${markFailedError}`);
422
576
  }
423
577
  }
424
578
  try {
425
- await this.backend.markAsSent(notificationWithExecutionGitCommitSha.id, true);
579
+ await this.executeMultiBackendWrite('markAsSent', async (backend) => {
580
+ return backend.markAsSent(notificationWithExecutionGitCommitSha.id, true);
581
+ }, async (backend) => {
582
+ await backend.markAsSent(notificationWithExecutionGitCommitSha.id, true);
583
+ });
426
584
  }
427
585
  catch (markSentError) {
428
586
  this.logger.error(`Error marking notification ${notificationWithExecutionGitCommitSha.id} as sent: ${markSentError}`);
429
587
  }
430
588
  }
431
589
  try {
432
- await this.backend.storeAdapterAndContextUsed(notificationWithExecutionGitCommitSha.id, lastAdapterKey, context);
590
+ await this.executeMultiBackendWrite('storeAdapterAndContextUsed', async (backend) => {
591
+ await backend.storeAdapterAndContextUsed(notificationWithExecutionGitCommitSha.id, lastAdapterKey, context);
592
+ }, async (backend) => {
593
+ await backend.storeAdapterAndContextUsed(notificationWithExecutionGitCommitSha.id, lastAdapterKey, context);
594
+ });
433
595
  }
434
596
  catch (storeContextError) {
435
597
  this.logger.error(`Error storing adapter and context for notification ${notificationWithExecutionGitCommitSha.id}: ${storeContextError}`);
436
598
  }
437
599
  }
438
600
  async bulkPersistNotifications(notifications) {
439
- return this.backend.bulkPersistNotifications(notifications);
601
+ return this.executeMultiBackendWrite('bulkPersistNotifications', async (backend) => {
602
+ return backend.bulkPersistNotifications(notifications);
603
+ }, async (backend, createdIds) => {
604
+ const notificationsWithIds = notifications.map((notification, index) => {
605
+ return {
606
+ ...notification,
607
+ id: createdIds[index],
608
+ };
609
+ });
610
+ await backend.bulkPersistNotifications(notificationsWithIds);
611
+ });
612
+ }
613
+ normalizeValueForSyncComparison(value) {
614
+ if (value === null) {
615
+ return 'null';
616
+ }
617
+ if (value === undefined) {
618
+ return 'undefined';
619
+ }
620
+ if (value instanceof Date) {
621
+ return value.toISOString();
622
+ }
623
+ if (typeof value === 'object') {
624
+ try {
625
+ return JSON.stringify(value);
626
+ }
627
+ catch (_a) {
628
+ return '[unserializable-object]';
629
+ }
630
+ }
631
+ return String(value);
632
+ }
633
+ /**
634
+ * Verifies whether a notification is synchronized across all configured backends.
635
+ *
636
+ * The report includes backend-level existence/errors and field-level discrepancies
637
+ * when comparing additional backends against the primary backend.
638
+ */
639
+ async verifyNotificationSync(notificationId) {
640
+ var _a, _b, _c;
641
+ const report = {
642
+ synced: true,
643
+ backends: {},
644
+ discrepancies: [],
645
+ };
646
+ for (const [identifier, backend] of this.backends.entries()) {
647
+ try {
648
+ const notification = await backend.getNotification(notificationId, false);
649
+ report.backends[identifier] = {
650
+ exists: notification !== null,
651
+ notification: notification !== null && notification !== void 0 ? notification : undefined,
652
+ };
653
+ }
654
+ catch (error) {
655
+ report.backends[identifier] = {
656
+ exists: false,
657
+ error: String(error),
658
+ };
659
+ report.discrepancies.push(`Backend ${identifier}: ${String(error)}`);
660
+ report.synced = false;
661
+ }
662
+ }
663
+ const primaryNotification = (_a = report.backends[this.primaryBackendIdentifier]) === null || _a === void 0 ? void 0 : _a.notification;
664
+ if (!primaryNotification) {
665
+ report.synced = false;
666
+ report.discrepancies.push('Notification not found in primary backend');
667
+ return report;
668
+ }
669
+ for (const [identifier, backendReport] of Object.entries(report.backends)) {
670
+ if (identifier === this.primaryBackendIdentifier) {
671
+ continue;
672
+ }
673
+ if (!backendReport.exists) {
674
+ report.synced = false;
675
+ report.discrepancies.push(`Notification missing in backend: ${identifier}`);
676
+ continue;
677
+ }
678
+ if (((_b = backendReport.notification) === null || _b === void 0 ? void 0 : _b.status) !== primaryNotification.status) {
679
+ report.synced = false;
680
+ report.discrepancies.push(`Status mismatch in ${identifier}: ${String((_c = backendReport.notification) === null || _c === void 0 ? void 0 : _c.status)} vs ${String(primaryNotification.status)}`);
681
+ }
682
+ const primaryNotificationRecord = primaryNotification;
683
+ const backendNotificationRecord = backendReport.notification;
684
+ const fieldsToCompare = [
685
+ 'notificationType',
686
+ 'title',
687
+ 'bodyTemplate',
688
+ 'subjectTemplate',
689
+ 'contextName',
690
+ 'contextParameters',
691
+ 'contextUsed',
692
+ 'extraParams',
693
+ 'adapterUsed',
694
+ 'sendAfter',
695
+ 'sentAt',
696
+ 'readAt',
697
+ 'createdAt',
698
+ 'updatedAt',
699
+ 'gitCommitSha',
700
+ ];
701
+ for (const fieldName of fieldsToCompare) {
702
+ const primaryValue = this.normalizeValueForSyncComparison(primaryNotificationRecord[fieldName]);
703
+ const backendValue = this.normalizeValueForSyncComparison(backendNotificationRecord[fieldName]);
704
+ if (primaryValue !== backendValue) {
705
+ report.synced = false;
706
+ report.discrepancies.push(`Field mismatch in ${identifier} for ${fieldName}: ${backendValue} vs ${primaryValue}`);
707
+ }
708
+ }
709
+ }
710
+ return report;
711
+ }
712
+ /**
713
+ * Replicates one notification from the primary backend to all additional backends.
714
+ *
715
+ * If a notification already exists in an additional backend, it is updated.
716
+ * Otherwise, it is created.
717
+ */
718
+ async replicateNotification(notificationId) {
719
+ const primaryNotification = await this.backend.getNotification(notificationId, false);
720
+ if (!primaryNotification) {
721
+ throw new Error(`Notification ${String(notificationId)} not found in primary backend`);
722
+ }
723
+ const result = {
724
+ successes: [],
725
+ failures: [],
726
+ };
727
+ for (const backend of this.getAdditionalBackends()) {
728
+ const backendIdentifier = this.getBackendIdentifier(backend);
729
+ try {
730
+ const existingNotification = await backend.getNotification(notificationId, false);
731
+ if (existingNotification) {
732
+ await backend.persistNotificationUpdate(notificationId, primaryNotification);
733
+ }
734
+ else {
735
+ await backend.persistNotification(primaryNotification);
736
+ }
737
+ result.successes.push(backendIdentifier);
738
+ }
739
+ catch (error) {
740
+ result.failures.push({
741
+ backend: backendIdentifier,
742
+ error: String(error),
743
+ });
744
+ }
745
+ }
746
+ return result;
440
747
  }
441
- async migrateToBackend(destinationBackend, batchSize = 5000) {
748
+ /**
749
+ * Returns a lightweight health snapshot for each configured backend.
750
+ */
751
+ async getBackendSyncStats() {
752
+ const stats = {
753
+ backends: {},
754
+ };
755
+ for (const [identifier, backend] of this.backends.entries()) {
756
+ try {
757
+ const notifications = await backend.getAllNotifications();
758
+ stats.backends[identifier] = {
759
+ totalNotifications: notifications.length,
760
+ status: 'healthy',
761
+ };
762
+ }
763
+ catch (error) {
764
+ stats.backends[identifier] = {
765
+ totalNotifications: 0,
766
+ status: 'error',
767
+ error: String(error),
768
+ };
769
+ }
770
+ }
771
+ return stats;
772
+ }
773
+ /**
774
+ * Migrates notifications from a source backend (primary by default) to a destination backend.
775
+ *
776
+ * @param destinationBackend - Backend receiving migrated records
777
+ * @param batchSize - Page size used while iterating source records
778
+ * @param sourceBackendIdentifier - Optional source backend identifier. Defaults to primary backend.
779
+ */
780
+ async migrateToBackend(destinationBackend, batchSize = 5000, sourceBackendIdentifier) {
781
+ const sourceBackend = this.getBackend(sourceBackendIdentifier);
442
782
  let pageNumber = 0;
443
- let allNotifications = await this.backend.getNotifications(pageNumber, batchSize);
783
+ let allNotifications = await sourceBackend.getNotifications(pageNumber, batchSize);
444
784
  while (allNotifications.length > 0) {
445
785
  pageNumber += 1;
446
786
  const notificationsWithoutId = allNotifications.map((notification) => {
@@ -448,7 +788,7 @@ class VintaSend {
448
788
  return notificationWithoutId;
449
789
  });
450
790
  await destinationBackend.bulkPersistNotifications(notificationsWithoutId);
451
- allNotifications = await this.backend.getNotifications(pageNumber, batchSize);
791
+ allNotifications = await sourceBackend.getNotifications(pageNumber, batchSize);
452
792
  }
453
793
  }
454
794
  }
@@ -6,6 +6,7 @@ import type { BaseNotificationTypeConfig } from './notification-type-config';
6
6
  import type { DatabaseOneOffNotification, OneOffNotification, OneOffNotificationInput } from './one-off-notification';
7
7
  export type { DatabaseOneOffNotification, OneOffNotification, OneOffNotificationInput, OneOffNotificationResendWithContextInput, } from './one-off-notification';
8
8
  export type NotificationInput<Config extends BaseNotificationTypeConfig> = {
9
+ id?: Config['NotificationIdType'];
9
10
  userId: Config['UserIdType'];
10
11
  notificationType: NotificationType;
11
12
  title: string | null;
@@ -19,6 +20,7 @@ export type NotificationInput<Config extends BaseNotificationTypeConfig> = {
19
20
  attachments?: NotificationAttachment[];
20
21
  };
21
22
  export type NotificationResendWithContextInput<Config extends BaseNotificationTypeConfig> = {
23
+ id?: Config['NotificationIdType'];
22
24
  userId: Config['UserIdType'];
23
25
  notificationType: NotificationType;
24
26
  title: string | null;
@@ -8,6 +8,7 @@ import type { BaseNotificationTypeConfig } from './notification-type-config';
8
8
  * One-off notifications are sent directly to an email/phone without requiring a user account.
9
9
  */
10
10
  export type OneOffNotificationInput<Config extends BaseNotificationTypeConfig> = {
11
+ id?: Config['NotificationIdType'];
11
12
  emailOrPhone: string;
12
13
  firstName: string;
13
14
  lastName: string;
@@ -27,6 +28,7 @@ export type OneOffNotificationInput<Config extends BaseNotificationTypeConfig> =
27
28
  * Similar to OneOffNotificationInput but includes the contextUsed field.
28
29
  */
29
30
  export type OneOffNotificationResendWithContextInput<Config extends BaseNotificationTypeConfig> = {
31
+ id?: Config['NotificationIdType'];
30
32
  emailOrPhone: string;
31
33
  firstName: string;
32
34
  lastName: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vintasend",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "main": "dist/index.js",
5
5
  "files": [
6
6
  "dist"