vintasend 0.7.1 → 0.8.1
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 +53 -0
- package/dist/services/notification-backends/base-notification-backend.d.ts +13 -2
- package/dist/services/notification-service.d.ts +99 -15
- package/dist/services/notification-service.js +388 -48
- package/dist/types/notification.d.ts +2 -0
- package/dist/types/one-off-notification.d.ts +2 -0
- package/package.json +1 -1
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'>
|
|
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'>
|
|
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
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
373
|
+
async getAllFutureNotifications(backendIdentifier) {
|
|
374
|
+
return this.getBackend(backendIdentifier).getAllFutureNotifications();
|
|
255
375
|
}
|
|
256
|
-
async getAllFutureNotificationsFromUser(userId) {
|
|
257
|
-
return this.
|
|
376
|
+
async getAllFutureNotificationsFromUser(userId, backendIdentifier) {
|
|
377
|
+
return this.getBackend(backendIdentifier).getAllFutureNotificationsFromUser(userId);
|
|
258
378
|
}
|
|
259
|
-
async getFutureNotificationsFromUser(userId, page, pageSize) {
|
|
260
|
-
return this.
|
|
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.
|
|
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.
|
|
404
|
+
const pendingNotifications = await this.getBackend().getAllPendingNotifications();
|
|
285
405
|
await Promise.all(pendingNotifications.map((notification) => this.send(notification)));
|
|
286
406
|
}
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
|
|
291
|
-
|
|
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.
|
|
419
|
+
async getOneOffNotifications(page, pageSize, backendIdentifier) {
|
|
420
|
+
return this.getBackend(backendIdentifier).getOneOffNotifications(page, pageSize);
|
|
295
421
|
}
|
|
296
|
-
|
|
297
|
-
|
|
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
|
-
|
|
300
|
-
|
|
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
|
-
|
|
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 {
|
|
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.
|
|
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.
|
|
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
|
-
|
|
322
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
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;
|