vintasend 0.1.12 → 0.1.14

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2017 Vinta Serviços e Soluções Tecnológicas Ltda
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -2,6 +2,30 @@
2
2
 
3
3
  A flexible package for implementing transactional notifications in TypeScript.
4
4
 
5
+ ## Features
6
+
7
+ * **Storing notifications in a Database**: This package relies on a data store to record all the notifications that will be sent. It also keeps it's state column up to date.
8
+ * **Scheduling notifications**: Storing notifications to be send in the future. The notification's context for rendering the template is only evaluated at the moment the notification is sent due to the lib's context generation registry.
9
+ * **Notification context fetched at send time**: On scheduled notifications, we only get the notification context at the send time, so we always get the most up-to-date information.
10
+ * **Flexible backend**: Your projects database is getting slow after you created the first milion notifications? You can migrate to a faster no-sql database with a blink of an eye without affecting how you send the notifications.
11
+ * **Flexible adapters**: Your project probably will need to change how it sends notifications overtime. This package allows to change the adapter without having to change how notifications templates are rendered or how the notification themselves are stored.
12
+ * **Flexible template renderers**: Wanna start managing your templates with a third party tool (so non-technical people can help maintaining them)? Or even choose a more powerful rendering engine? You can do it independently of how you send the notifications or store them in the database.
13
+ * **Sending notifications in background jobs**: This packages supports enqueing notifications to send it from separate processes. This may be helpful to free up the HTTP server of processing heavy notifications during the request time.
14
+
15
+ ## How does it work?
16
+
17
+ The VintaSend package provides a NotificationService class that allows the user to store and send notification, scheduled or not. It relies on Dependency Injection to define how to store/retrieve, render the notification templates, and send notifications. This architechture allows us to swap each part without changing the code we actually use to send the notifications.
18
+
19
+ ### Scheduled Notifications
20
+
21
+ VintaSend schedules notifications by creating them on the database for sending when the send_after value has passed. The sending isn't done automatically but we have a service method called `sendPendingNotifications` to send all pending notifications found in the database.
22
+
23
+ You need to call the `sendPendingNotifications` service method in a cron job or a tool for running periodic jobs.
24
+
25
+ #### Keeping the content up-to-date in scheduled notifications
26
+
27
+ The NotificationService stores every notification in a database. This helps us to audit and manage our notifications. At the same time, notifications usually have a context that's used to hydrate its template with data. If we stored the cotext directly on the notification records, we'd have to update it anytime the context changes. Instead of storing the context itself, we store a reference to a Context Generator class and the parameters it requires (like ids, flags, types, etc) so we generate the context only when the notification is sent. This ensures we're always getting the most up-to-date context when sending notifications. We also store the generated context after we send the notification, for auditing purposes.
28
+
5
29
  ## Installation
6
30
 
7
31
  ```bash
@@ -10,13 +34,112 @@ npm install vintasend
10
34
  yarn add vintasend
11
35
  ```
12
36
 
13
- ## Features
14
- * **Storing notifications in a Database**: This package relies on a data store to record all the notifications that will be sent. It also keeps it's state column up to date.
15
- * **Scheduling notifications**: Storing notifications to be send in the future. The notification's context for rendering the template is only evaluated at the moment the notification is sent due to the lib's context generation registry.
16
- * **Notification context fetched at send time**: On scheduled notifications, we only get the notification context at the send time, so we always get the most up-to-date information.
17
- * **Flexible backend**: Your projects database is getting slow after you created the first milion notifications? You can migrate to a faster no-sql database with a blink of an eye without affecting how you send the notifications.
18
- * **Flexible adapters**: Your project probably will need to change how it sends notifications overtime. This package allows to change the adapter without having to change how notifications templates are rendered or how the notification themselves are stored.
19
- * **Flexible template renderers**: Wanna start managing your templates with a third party tool (so non-technical people can help maintaining them)? Or even choose a more powerful rendering engine? You can do it independently of how you send the notifications or store them in the database.
37
+ ## Getting Started
38
+
39
+ To start using VintaSend you just need to initialize the notification service and start using it to manage your notifications.
40
+
41
+
42
+ ```typescript
43
+ import { NotificationService } from 'vintasend';
44
+
45
+ // context map for generating the context of each notification
46
+ export const contextGeneratorsMap = {
47
+ welcome: {
48
+ generate: async ({ userId }: { userId: number }): { firstName: string } => {
49
+ const user = await getUserById(userId); // example
50
+ return {
51
+ firstName: user.firstName,
52
+ };
53
+ }
54
+ },
55
+ } as const;
56
+
57
+ // type config definition, so all modules use the same types
58
+ export type NotificationTypeConfig = {
59
+ ContextMap: typeof contextGeneratorsMap;
60
+ NotificationIdType: number;
61
+ UserIdType: number;
62
+ };
63
+
64
+ export function getNotificationService() {
65
+ /*
66
+ Function to instanciate the notificationService
67
+ The Backend, Template Renderer, Logger, and Adapter used here are not included
68
+ here and should be installed and imported separately or manually defined if
69
+ the existing implementations don't support the specific use-case.
70
+ */
71
+ const backend = new MyNotificationBackend<NotificationTypeConfig>();
72
+ const templateRenderer = new MyTemplateRenderer<NotificationTypeConfig>();
73
+ const adapter = new MyNotificationAdapter<
74
+ typeof templateRenderer,
75
+ NotificationTypeConfig
76
+ >(templateRenderer, true)
77
+ return new NotificationService<NotificationTypeConfig>(
78
+ [adapter],
79
+ backend,
80
+ new MyLogger(loggerOptions),
81
+ contextGeneratorsMap,
82
+ );
83
+ }
84
+
85
+ export function sendWelcomeEmail(userId: number) {
86
+ /* sends the Welcome email to a user */
87
+ const vintasend = getNotificationService();
88
+ const now = new Date();
89
+
90
+ vintasend.createNotification({
91
+ userId: user.id,
92
+ notificationType: 'EMAIL',
93
+ title: 'Welcome Email',
94
+ contextName: 'welcome',
95
+ contextParameters: { userId },
96
+ sendAfter: now,
97
+ bodyTemplate: './src/email-templates/auth/welcome/welcome-body.html.template',
98
+ subjectTemplate: './src/email-templates/auth/welcome/welcome-subject.txt.template',
99
+ extraParams: {},
100
+ });
101
+ }
102
+ ```
103
+
104
+ ## Glossary
105
+
106
+ * **Notification Backend**: It is a class that implements the methods necessary for VintaSend services to create, update, and retrieve Notifications from da database.
107
+ * **Notification Adapter**: It is a class that implements the methods necessary for VintaSend services to send Notifications through email, SMS or even push/in-app notifications.
108
+ * **Template Renderer**: It is a class that implements the methods necessary for VintaSend adapter to render the notification body.
109
+ * **Notification Context**: It's the data passed to the templates to render the notification correctly. It's generated when the notification is sent, not on creation time
110
+ * **Context generator**: It's a class defined by the user context generator map with a context name. That class has a `generate` method that, when called, generates the data necessary to render its respective notification.
111
+ * **Context name**: The registered name of a context generator. It's stored in the notification so the context generator is called at the moment the notification will be sent.
112
+ * **Context generators map**: It's an object defined by the user that maps context names to their respective context generators.
113
+ * **Queue service**: Service for enqueueing notifications so they are send by an external service.
114
+ * **Logger**: A class that allows the `NotificationService` to create logs following a format defined by its users.
115
+
116
+
117
+ ## Implementations
118
+
119
+
120
+ ## Community
121
+
122
+ VintaSend has many backend, adapter, and template renderer implementations. If you can't find something that fulfills your needs, the package has very clear interfaces you can implement and achieve the exact behavior you expect without loosing VintaSend's friendly API.
123
+
124
+ ### Officially supported packages
125
+
126
+ #### Backends
127
+
128
+ * **[vintasend-prisma](https://github.com/vintasoftware/vintasend-prisma/)**: Uses Prisma Client to manage the notifications in the database.
129
+
130
+ #### Adapters
131
+
132
+ * **[vintasend-nodemailer](https://github.com/vintasoftware/vintasend-nodemailer/)**: Uses nodemailer to send transactional emails to users.
133
+
134
+ #### Template Renderers
135
+ * **[vintasend-pug](https://github.com/vintasoftware/vintasend-pug/)**: Renders emails using Pug.
136
+
137
+ #### Loggers
138
+ * **[vintasend-winston](https://github.com/vintasoftware/vintasend-winston/)**: Uses Winston to allow `NotificationService` to create log entries.
139
+
140
+ ## Examples
141
+
142
+ Examples of how to use VintaSend in different context are available on the [vintasend-ts-examples repository](https://github.com/vintasoftware/vintasend-ts-examples).
20
143
 
21
144
  ## Development
22
145
 
@@ -38,6 +161,10 @@ yarn test
38
161
 
39
162
  Feel free to open issues and submit pull requests.
40
163
 
164
+ ### Creating new implementations
165
+
166
+ There's a template project for creating new implementations that already includes basic dependencies, configuration for tests, and Github Actions hooks. You can find it in [vintasend-ts/src/implementations/vintasend-implementation-template](https://github.com/vintasoftware/vintasend-ts/tree/main/src/implementations/vintasend-implementation-template)
167
+
41
168
  ## License
42
169
 
43
170
  This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
package/dist/index.d.ts CHANGED
@@ -1,3 +1,8 @@
1
1
  export { Notification } from "./types/notification";
2
- export { NotificationService } from "./services/notification-service";
3
- export { NotificationContextRegistry } from "./services/notification-context-registry";
2
+ export { VintaSendFactory } from "./services/notification-service";
3
+ export type { VintaSend } from "./services/notification-service";
4
+ export type { ContextGenerator } from "./types/notification-context-generators";
5
+ export type { BaseNotificationTypeConfig } from "./types/notification-type-config";
6
+ export type { BaseNotificationQueueService } from "./services/notification-queue-service/base-notification-queue-service";
7
+ export type { BaseNotificationTemplateRenderer } from "./services/notification-template-renderers/base-notification-template-renderer";
8
+ export type { BaseEmailTemplateRenderer } from "./services/notification-template-renderers/base-email-template-renderer";
package/dist/index.js CHANGED
@@ -1,7 +1,5 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.NotificationContextRegistry = exports.NotificationService = void 0;
3
+ exports.VintaSendFactory = void 0;
4
4
  var notification_service_1 = require("./services/notification-service");
5
- Object.defineProperty(exports, "NotificationService", { enumerable: true, get: function () { return notification_service_1.NotificationService; } });
6
- var notification_context_registry_1 = require("./services/notification-context-registry");
7
- Object.defineProperty(exports, "NotificationContextRegistry", { enumerable: true, get: function () { return notification_context_registry_1.NotificationContextRegistry; } });
5
+ Object.defineProperty(exports, "VintaSendFactory", { enumerable: true, get: function () { return notification_service_1.VintaSendFactory; } });
@@ -10,9 +10,9 @@ export interface BaseNotificationBackend<Config extends BaseNotificationTypeConf
10
10
  getFutureNotificationsFromUser(userId: Config["UserIdType"]): Promise<DatabaseNotification<Config>[]>;
11
11
  persistNotification(notification: Omit<Notification<Config>, 'id'>): Promise<DatabaseNotification<Config>>;
12
12
  persistNotificationUpdate(notificationId: Config["NotificationIdType"], notification: Partial<Omit<Notification<Config>, 'id'>>): Promise<DatabaseNotification<Config>>;
13
- markPendingAsSent(notificationId: Config["NotificationIdType"]): Promise<DatabaseNotification<Config>>;
14
- markPendingAsFailed(notificationId: Config["NotificationIdType"]): Promise<DatabaseNotification<Config>>;
15
- markSentAsRead(notificationId: Config["NotificationIdType"]): Promise<DatabaseNotification<Config>>;
13
+ markAsSent(notificationId: Config["NotificationIdType"], checkIsPending: boolean): Promise<DatabaseNotification<Config>>;
14
+ markAsFailed(notificationId: Config["NotificationIdType"], checkIsPending: boolean): Promise<DatabaseNotification<Config>>;
15
+ markAsRead(notificationId: Config["NotificationIdType"], checkIsSent: boolean): Promise<DatabaseNotification<Config>>;
16
16
  cancelNotification(notificationId: Config["NotificationIdType"]): Promise<void>;
17
17
  getNotification(notificationId: Config["NotificationIdType"], forUpdate: boolean): Promise<DatabaseNotification<Config> | null>;
18
18
  filterAllInAppUnreadNotifications(userId: Config["UserIdType"]): Promise<DatabaseNotification<Config>[]>;
@@ -0,0 +1,8 @@
1
+ import type { ContextGenerator } from "../types/notification-context-generators";
2
+ export declare class NotificationContextGeneratorsMap<ContextMapType extends {
3
+ [key: string]: ContextGenerator;
4
+ }> {
5
+ private contextGenerators;
6
+ constructor(contextGenerators: ContextMapType);
7
+ getContextGenerator<ContextName extends string & keyof ContextMapType>(contextName: ContextName): ContextMapType[ContextName];
8
+ }
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NotificationContextGeneratorsMap = void 0;
4
+ class NotificationContextGeneratorsMap {
5
+ constructor(contextGenerators) {
6
+ this.contextGenerators = contextGenerators;
7
+ }
8
+ getContextGenerator(contextName) {
9
+ return this.contextGenerators[contextName];
10
+ }
11
+ }
12
+ exports.NotificationContextGeneratorsMap = NotificationContextGeneratorsMap;
@@ -6,35 +6,36 @@ import type { BaseNotificationTemplateRenderer } from './notification-template-r
6
6
  import type { BaseNotificationBackend } from './notification-backends/base-notification-backend';
7
7
  import type { BaseLogger } from './loggers/base-logger';
8
8
  import type { BaseNotificationQueueService } from './notification-queue-service/base-notification-queue-service';
9
- type NotificationServiceOptions = {
9
+ type VintaSendOptions = {
10
10
  raiseErrorOnFailedSend: boolean;
11
11
  };
12
- export declare class NotificationService<Config extends BaseNotificationTypeConfig> {
12
+ export declare class VintaSendFactory<Config extends BaseNotificationTypeConfig> {
13
+ create<AdaptersList extends BaseNotificationAdapter<BaseNotificationTemplateRenderer<Config>, Config>[], Backend extends BaseNotificationBackend<Config>, Logger extends BaseLogger, QueueService extends BaseNotificationQueueService<Config>>(adapters: AdaptersList, backend: Backend, logger: Logger, contextGeneratorsMap: BaseNotificationTypeConfig['ContextMap'], queueService?: QueueService, options?: VintaSendOptions): VintaSend<Config, AdaptersList, Backend, Logger, QueueService>;
14
+ }
15
+ export declare class VintaSend<Config extends BaseNotificationTypeConfig, AdaptersList extends BaseNotificationAdapter<BaseNotificationTemplateRenderer<Config>, Config>[], Backend extends BaseNotificationBackend<Config>, Logger extends BaseLogger, QueueService extends BaseNotificationQueueService<Config>> {
13
16
  private adapters;
14
17
  private backend;
15
18
  private logger;
16
19
  private queueService?;
17
20
  private options;
18
- constructor(adapters: BaseNotificationAdapter<BaseNotificationTemplateRenderer<Config>, Config>[], backend: BaseNotificationBackend<Config>, logger: BaseLogger, queueService?: BaseNotificationQueueService<Config> | undefined, options?: NotificationServiceOptions);
19
- registerQueueService(queueService: BaseNotificationQueueService<Config>): void;
21
+ private contextGeneratorsMap;
22
+ constructor(adapters: AdaptersList, backend: Backend, logger: Logger, contextGeneratorsMap: Config['ContextMap'], queueService?: QueueService | undefined, options?: VintaSendOptions);
23
+ registerQueueService(queueService: QueueService): void;
20
24
  send(notification: DatabaseNotification<Config>): Promise<void>;
21
- createNotification(notification: Omit<Notification<Config>, 'id'>): Promise<Notification<Config>>;
25
+ createNotification(notification: Omit<Notification<Config>, 'id'>): Promise<DatabaseNotification<Config>>;
22
26
  updateNotification(notificationId: Config['NotificationIdType'], notification: Partial<Omit<Notification<Config>, 'id'>>): Promise<DatabaseNotification<Config>>;
23
27
  getAllFutureNotifications(): Promise<DatabaseNotification<Config>[]>;
24
28
  getAllFutureNotificationsFromUser(userId: Config['NotificationIdType']): Promise<DatabaseNotification<Config>[]>;
25
29
  getFutureNotificationsFromUser(userId: Config['NotificationIdType']): Promise<DatabaseNotification<Config>[]>;
26
30
  getFutureNotifications(): Promise<DatabaseNotification<Config>[]>;
27
- getNotificationContext(contextName: keyof Config['ContextMap'], parameters: Parameters<Config['ContextMap'][keyof Config['ContextMap']]['generate']>[0]): Promise<JsonObject>;
31
+ getNotificationContext<ContextName extends string & keyof Config['ContextMap']>(contextName: ContextName, parameters: Parameters<ReturnType<typeof this.contextGeneratorsMap.getContextGenerator<ContextName>>['generate']>[0]): Promise<JsonObject>;
28
32
  sendPendingNotifications(): Promise<void>;
29
33
  getPendingNotifications(): Promise<DatabaseNotification<Config>[]>;
30
34
  getNotification(notificationId: Config['NotificationIdType'], forUpdate?: boolean): Promise<DatabaseNotification<Config> | null>;
31
- markRead(notificationId: Config['NotificationIdType']): Promise<DatabaseNotification<Config>>;
35
+ markRead(notificationId: Config['NotificationIdType'], checkIsSent?: boolean): Promise<DatabaseNotification<Config>>;
32
36
  getInAppUnread(userId: Config['NotificationIdType']): Promise<DatabaseNotification<Config>[]>;
33
37
  cancelNotification(notificationId: Config['NotificationIdType']): Promise<void>;
38
+ resendNotification(notificationId: Config['NotificationIdType'], useStoredContextIfAvailable?: boolean): Promise<DatabaseNotification<Config> | undefined>;
34
39
  delayedSend(notificationId: Config['NotificationIdType']): Promise<void>;
35
40
  }
36
- export declare class NotificationServiceSingleton {
37
- private static instance;
38
- static getInstance<Config extends BaseNotificationTypeConfig>(...args: ConstructorParameters<typeof NotificationService> | []): NotificationService<Config>;
39
- }
40
41
  export {};
@@ -1,9 +1,17 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.NotificationServiceSingleton = exports.NotificationService = void 0;
4
- const notification_context_registry_1 = require("./notification-context-registry");
5
- class NotificationService {
6
- constructor(adapters, backend, logger, queueService, options = {
3
+ exports.VintaSend = exports.VintaSendFactory = void 0;
4
+ const notification_context_generators_map_1 = require("./notification-context-generators-map");
5
+ class VintaSendFactory {
6
+ create(adapters, backend, logger, contextGeneratorsMap, queueService, options = {
7
+ raiseErrorOnFailedSend: false,
8
+ }) {
9
+ return new VintaSend(adapters, backend, logger, contextGeneratorsMap, queueService, options);
10
+ }
11
+ }
12
+ exports.VintaSendFactory = VintaSendFactory;
13
+ class VintaSend {
14
+ constructor(adapters, backend, logger, contextGeneratorsMap, queueService, options = {
7
15
  raiseErrorOnFailedSend: false,
8
16
  }) {
9
17
  this.adapters = adapters;
@@ -11,6 +19,7 @@ class NotificationService {
11
19
  this.logger = logger;
12
20
  this.queueService = queueService;
13
21
  this.options = options;
22
+ this.contextGeneratorsMap = new notification_context_generators_map_1.NotificationContextGeneratorsMap(contextGeneratorsMap);
14
23
  for (const adapter of adapters) {
15
24
  adapter.injectBackend(backend);
16
25
  }
@@ -48,16 +57,21 @@ class NotificationService {
48
57
  }
49
58
  }
50
59
  let context = null;
51
- try {
52
- context = await this.getNotificationContext(notification.contextName, notification.contextParameters);
53
- this.logger.info(`Generated context for notification ${notification.id}`);
60
+ if (notification.contextUsed) {
61
+ context = notification.contextUsed;
54
62
  }
55
- catch (contextError) {
56
- this.logger.error(`Error getting context for notification ${notification.id}: ${contextError}`);
57
- if (this.options.raiseErrorOnFailedSend) {
58
- throw contextError;
63
+ else {
64
+ try {
65
+ context = await this.getNotificationContext(notification.contextName, notification.contextParameters);
66
+ this.logger.info(`Generated context for notification ${notification.id}`);
67
+ }
68
+ catch (contextError) {
69
+ this.logger.error(`Error getting context for notification ${notification.id}: ${contextError}`);
70
+ if (this.options.raiseErrorOnFailedSend) {
71
+ throw contextError;
72
+ }
73
+ return;
59
74
  }
60
- return;
61
75
  }
62
76
  try {
63
77
  this.logger.info(`Sending notification ${notification.id} with adapter ${adapter.key}`);
@@ -67,7 +81,7 @@ class NotificationService {
67
81
  catch (sendError) {
68
82
  this.logger.error(`Error sending notification ${notification.id} with adapter ${adapter.key}: ${sendError}`);
69
83
  try {
70
- await this.backend.markPendingAsFailed(notification.id);
84
+ await this.backend.markAsFailed(notification.id, true);
71
85
  }
72
86
  catch (markFailedError) {
73
87
  this.logger.error(`Error marking notification ${notification.id} as failed: ${markFailedError}`);
@@ -75,7 +89,7 @@ class NotificationService {
75
89
  continue;
76
90
  }
77
91
  try {
78
- await this.backend.markPendingAsSent(notification.id);
92
+ await this.backend.markAsSent(notification.id, true);
79
93
  }
80
94
  catch (markSentError) {
81
95
  this.logger.error(`Error marking notification ${notification.id} as sent: ${markSentError}`);
@@ -118,8 +132,11 @@ class NotificationService {
118
132
  return this.backend.getFutureNotifications();
119
133
  }
120
134
  async getNotificationContext(contextName, parameters) {
121
- const contextRegistry = notification_context_registry_1.NotificationContextRegistry.getInstance();
122
- return contextRegistry.getContext(contextName, parameters);
135
+ const context = this.contextGeneratorsMap.getContextGenerator(contextName).generate(parameters);
136
+ if (context instanceof Promise) {
137
+ return await context;
138
+ }
139
+ return Promise.resolve(context);
123
140
  }
124
141
  async sendPendingNotifications() {
125
142
  const pendingNotifications = await this.backend.getAllPendingNotifications();
@@ -131,8 +148,8 @@ class NotificationService {
131
148
  async getNotification(notificationId, forUpdate = false) {
132
149
  return this.backend.getNotification(notificationId, forUpdate);
133
150
  }
134
- async markRead(notificationId) {
135
- const notification = this.backend.markSentAsRead(notificationId);
151
+ async markRead(notificationId, checkIsSent = true) {
152
+ const notification = await this.backend.markAsRead(notificationId, checkIsSent);
136
153
  this.logger.info(`Notification ${notificationId} marked as read`);
137
154
  return notification;
138
155
  }
@@ -143,6 +160,59 @@ class NotificationService {
143
160
  await this.backend.cancelNotification(notificationId);
144
161
  this.logger.info(`Notification ${notificationId} cancelled`);
145
162
  }
163
+ async resendNotification(notificationId, useStoredContextIfAvailable = false) {
164
+ const notification = await this.getNotification(notificationId, false);
165
+ if (!notification) {
166
+ this.logger.error(`Notification ${notificationId} not found`);
167
+ if (this.options.raiseErrorOnFailedSend) {
168
+ throw new Error(`Notification ${notificationId} not found`);
169
+ }
170
+ return;
171
+ }
172
+ if (notification.sendAfter && notification.sendAfter > new Date()) {
173
+ this.logger.error(`Notification ${notificationId} is scheduled for the future`);
174
+ if (this.options.raiseErrorOnFailedSend) {
175
+ throw new Error(`Notification ${notificationId} is scheduled for the future`);
176
+ }
177
+ return;
178
+ }
179
+ if (useStoredContextIfAvailable && !notification.contextUsed) {
180
+ this.logger.error(`Context not found for notification ${notificationId}`);
181
+ if (this.options.raiseErrorOnFailedSend) {
182
+ throw new Error(`Context not found for notification ${notificationId}`);
183
+ }
184
+ return;
185
+ }
186
+ const notificationResendInputWithoutContext = {
187
+ userId: notification.userId,
188
+ notificationType: notification.notificationType,
189
+ title: notification.title,
190
+ bodyTemplate: notification.bodyTemplate,
191
+ contextName: notification.contextName,
192
+ contextParameters: notification.contextParameters,
193
+ sendAfter: null,
194
+ subjectTemplate: notification.subjectTemplate,
195
+ extraParams: notification.extraParams,
196
+ };
197
+ let createdNotification;
198
+ if (useStoredContextIfAvailable && notification.contextUsed) {
199
+ const notificationResendInput = {
200
+ ...notificationResendInputWithoutContext,
201
+ contextUsed: notification.contextUsed,
202
+ };
203
+ createdNotification = await this.backend.persistNotification(notificationResendInput);
204
+ }
205
+ else {
206
+ const notificationResendInput = {
207
+ ...notificationResendInputWithoutContext,
208
+ contextUsed: await this.getNotificationContext(notification.contextName, notification.contextParameters),
209
+ };
210
+ createdNotification = await this.backend.persistNotification(notificationResendInput);
211
+ }
212
+ this.logger.info(`Notification ${createdNotification.id} created for resending notification ${notificationId}`);
213
+ this.send(createdNotification);
214
+ return createdNotification;
215
+ }
146
216
  async delayedSend(notificationId) {
147
217
  const notification = await this.getNotification(notificationId, false);
148
218
  if (!notification) {
@@ -168,14 +238,14 @@ class NotificationService {
168
238
  catch (sendError) {
169
239
  this.logger.error(`Error sending notification ${notification.id} with adapter ${adapter.key}: ${sendError}`);
170
240
  try {
171
- await this.backend.markPendingAsFailed(notification.id);
241
+ await this.backend.markAsFailed(notification.id, true);
172
242
  }
173
243
  catch (markFailedError) {
174
244
  this.logger.error(`Error marking notification ${notification.id} as failed: ${markFailedError}`);
175
245
  }
176
246
  }
177
247
  try {
178
- await this.backend.markPendingAsSent(notification.id);
248
+ await this.backend.markAsSent(notification.id, true);
179
249
  }
180
250
  catch (markSentError) {
181
251
  this.logger.error(`Error marking notification ${notification.id} as sent: ${markSentError}`);
@@ -189,17 +259,4 @@ class NotificationService {
189
259
  }
190
260
  }
191
261
  }
192
- exports.NotificationService = NotificationService;
193
- // biome-ignore lint/complexity/noStaticOnlyClass: <explanation>
194
- class NotificationServiceSingleton {
195
- static getInstance(...args) {
196
- if (!NotificationServiceSingleton.instance) {
197
- if (!args || args.length === 0) {
198
- throw new Error('NotificationServiceSingleton is not initialized. Please call getInstance with the required arguments');
199
- }
200
- NotificationServiceSingleton.instance = new NotificationService(...args);
201
- }
202
- return NotificationServiceSingleton.instance;
203
- }
204
- }
205
- exports.NotificationServiceSingleton = NotificationServiceSingleton;
262
+ exports.VintaSend = VintaSend;
@@ -0,0 +1,4 @@
1
+ import type { JsonObject, JsonPrimitive } from './json-values';
2
+ export interface ContextGenerator<Params extends Record<string, JsonPrimitive> = Record<string, JsonPrimitive>> {
3
+ generate(params: Params): JsonObject | Promise<JsonObject>;
4
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -1,4 +1,4 @@
1
- import type { ContextGenerator } from "../services/notification-context-registry";
1
+ import type { ContextGenerator } from "./notification-context-generators";
2
2
  import type { Identifier } from "./identifier";
3
3
  export type BaseNotificationTypeConfig = {
4
4
  ContextMap: Record<string, ContextGenerator>;
@@ -3,29 +3,40 @@ import type { NotificationStatus } from './notification-status';
3
3
  import type { NotificationType } from './notification-type';
4
4
  import type { BaseNotificationTypeConfig } from './notification-type-config';
5
5
  export type NotificationInput<Config extends BaseNotificationTypeConfig> = {
6
- id: undefined;
7
6
  userId: Config['UserIdType'];
8
7
  notificationType: NotificationType;
9
8
  title: string | null;
10
9
  bodyTemplate: string;
11
- contextName: keyof Config['ContextMap'];
10
+ contextName: string & keyof Config['ContextMap'];
12
11
  contextParameters: Parameters<Config['ContextMap'][NotificationInput<Config>['contextName']]['generate']>[0];
13
12
  sendAfter: Date | null;
14
13
  subjectTemplate: string | null;
15
14
  extraParams: InputJsonValue | null;
16
15
  };
16
+ export type NotificationResendWithContextInput<Config extends BaseNotificationTypeConfig> = {
17
+ userId: Config['UserIdType'];
18
+ notificationType: NotificationType;
19
+ title: string | null;
20
+ bodyTemplate: string;
21
+ contextName: string & keyof Config['ContextMap'];
22
+ contextParameters: Parameters<Config['ContextMap'][NotificationResendWithContextInput<Config>['contextName']]['generate']>[0];
23
+ contextUsed: ReturnType<Config['ContextMap'][NotificationResendWithContextInput<Config>['contextName']]['generate']> extends Promise<infer T> ? T : ReturnType<Config['ContextMap'][NotificationResendWithContextInput<Config>['contextName']]['generate']>;
24
+ sendAfter: Date | null;
25
+ subjectTemplate: string | null;
26
+ extraParams: InputJsonValue | null;
27
+ };
17
28
  export type DatabaseNotification<Config extends BaseNotificationTypeConfig> = {
18
29
  id: Config['NotificationIdType'];
19
30
  userId: Config['UserIdType'];
20
31
  notificationType: NotificationType;
21
32
  title: string | null;
22
33
  bodyTemplate: string;
23
- contextName: keyof Config['ContextMap'];
24
- contextParameters: Parameters<Config['ContextMap'][NotificationInput<Config>['contextName']]['generate']>[0];
34
+ contextName: string & keyof Config['ContextMap'];
35
+ contextParameters: Parameters<Config['ContextMap'][DatabaseNotification<Config>['contextName']]['generate']>[0];
25
36
  sendAfter: Date | null;
26
37
  subjectTemplate: string | null;
27
38
  status: NotificationStatus;
28
- contextUsed: ReturnType<Config['ContextMap'][NotificationInput<Config>['contextName']]['generate']> | null;
39
+ contextUsed: ReturnType<Config['ContextMap'][DatabaseNotification<Config>['contextName']]['generate']> extends Promise<infer T> ? T : ReturnType<Config['ContextMap'][DatabaseNotification<Config>['contextName']]['generate']>;
29
40
  extraParams: JsonValue;
30
41
  adapterUsed: string | null;
31
42
  sentAt: Date | null;
@@ -33,4 +44,4 @@ export type DatabaseNotification<Config extends BaseNotificationTypeConfig> = {
33
44
  createdAt?: Date;
34
45
  updatedAt?: Date;
35
46
  };
36
- export type Notification<Config extends BaseNotificationTypeConfig> = NotificationInput<Config> | DatabaseNotification<Config>;
47
+ export type Notification<Config extends BaseNotificationTypeConfig> = NotificationInput<Config> | NotificationResendWithContextInput<Config> | DatabaseNotification<Config>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vintasend",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "main": "dist/index.js",
5
5
  "files": [
6
6
  "dist"