vintasend 0.1.11 → 0.1.13
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 +21 -0
- package/README.md +134 -7
- package/dist/index.d.ts +5 -1
- package/dist/index.js +1 -3
- package/dist/services/notification-adapters/base-notification-adapter.d.ts +5 -4
- package/dist/services/notification-backends/base-notification-backend.d.ts +14 -14
- package/dist/services/notification-service.d.ts +26 -20
- package/dist/services/notification-service.js +69 -19
- package/dist/services/notification-template-renderers/base-email-template-renderer.d.ts +1 -1
- package/dist/services/notification-template-renderers/base-notification-template-renderer.d.ts +1 -1
- package/dist/types/notification-context-generators.d.ts +4 -0
- package/dist/types/notification-context-generators.js +2 -0
- package/dist/types/notification-type-config.d.ts +3 -12
- package/dist/types/notification.d.ts +25 -15
- package/package.json +1 -1
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
|
-
##
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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,7 @@
|
|
|
1
1
|
export { Notification } from "./types/notification";
|
|
2
2
|
export { NotificationService } from "./services/notification-service";
|
|
3
|
-
export {
|
|
3
|
+
export type { ContextGenerator } from "./types/notification-context-generators";
|
|
4
|
+
export type { BaseNotificationTypeConfig } from "./types/notification-type-config";
|
|
5
|
+
export type { BaseNotificationQueueService } from "./services/notification-queue-service/base-notification-queue-service";
|
|
6
|
+
export type { BaseNotificationTemplateRenderer } from "./services/notification-template-renderers/base-notification-template-renderer";
|
|
7
|
+
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.
|
|
3
|
+
exports.NotificationService = void 0;
|
|
4
4
|
var notification_service_1 = require("./services/notification-service");
|
|
5
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; } });
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import type { NotificationType } from '../../types/notification-type';
|
|
2
|
-
import type {
|
|
2
|
+
import type { DatabaseNotification } from '../../types/notification';
|
|
3
3
|
import type { BaseNotificationTemplateRenderer } from '../notification-template-renderers/base-notification-template-renderer';
|
|
4
4
|
import type { JsonValue } from '../../types/json-values';
|
|
5
5
|
import type { BaseNotificationTypeConfig } from '../../types/notification-type-config';
|
|
6
|
+
import type { BaseNotificationBackend } from '../notification-backends/base-notification-backend';
|
|
6
7
|
export declare abstract class BaseNotificationAdapter<TemplateRenderer extends BaseNotificationTemplateRenderer<Config>, Config extends BaseNotificationTypeConfig> {
|
|
7
8
|
protected templateRenderer: TemplateRenderer;
|
|
8
9
|
readonly notificationType: NotificationType;
|
|
9
10
|
readonly enqueueNotifications: boolean;
|
|
10
11
|
key: string | null;
|
|
11
|
-
backend: Config
|
|
12
|
+
backend: BaseNotificationBackend<Config> | null;
|
|
12
13
|
constructor(templateRenderer: TemplateRenderer, notificationType: NotificationType, enqueueNotifications: boolean);
|
|
13
|
-
send(notification:
|
|
14
|
-
injectBackend(backend: Config
|
|
14
|
+
send(notification: DatabaseNotification<Config>, context: JsonValue): Promise<void>;
|
|
15
|
+
injectBackend(backend: BaseNotificationBackend<Config>): void;
|
|
15
16
|
}
|
|
@@ -2,21 +2,21 @@ import type { InputJsonValue } from '../../types/json-values';
|
|
|
2
2
|
import type { DatabaseNotification, Notification } from '../../types/notification';
|
|
3
3
|
import type { BaseNotificationTypeConfig } from '../../types/notification-type-config';
|
|
4
4
|
export interface BaseNotificationBackend<Config extends BaseNotificationTypeConfig> {
|
|
5
|
-
getAllPendingNotifications(): Promise<DatabaseNotification<Config
|
|
6
|
-
getPendingNotifications(): Promise<DatabaseNotification<Config
|
|
7
|
-
getAllFutureNotifications(): Promise<DatabaseNotification<Config
|
|
8
|
-
getFutureNotifications(): Promise<DatabaseNotification<Config
|
|
9
|
-
getAllFutureNotificationsFromUser(userId: Config["UserIdType"]): Promise<DatabaseNotification<Config
|
|
10
|
-
getFutureNotificationsFromUser(userId: Config["UserIdType"]): Promise<DatabaseNotification<Config
|
|
11
|
-
persistNotification(notification: Omit<Notification<Config
|
|
12
|
-
persistNotificationUpdate(notificationId: Config["NotificationIdType"], notification: Partial<Omit<Notification<Config
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
5
|
+
getAllPendingNotifications(): Promise<DatabaseNotification<Config>[]>;
|
|
6
|
+
getPendingNotifications(): Promise<DatabaseNotification<Config>[]>;
|
|
7
|
+
getAllFutureNotifications(): Promise<DatabaseNotification<Config>[]>;
|
|
8
|
+
getFutureNotifications(): Promise<DatabaseNotification<Config>[]>;
|
|
9
|
+
getAllFutureNotificationsFromUser(userId: Config["UserIdType"]): Promise<DatabaseNotification<Config>[]>;
|
|
10
|
+
getFutureNotificationsFromUser(userId: Config["UserIdType"]): Promise<DatabaseNotification<Config>[]>;
|
|
11
|
+
persistNotification(notification: Omit<Notification<Config>, 'id'>): Promise<DatabaseNotification<Config>>;
|
|
12
|
+
persistNotificationUpdate(notificationId: Config["NotificationIdType"], notification: Partial<Omit<Notification<Config>, 'id'>>): 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
|
-
getNotification(notificationId: Config["NotificationIdType"], forUpdate: boolean): Promise<DatabaseNotification<Config
|
|
18
|
-
filterAllInAppUnreadNotifications(userId: Config["UserIdType"]): Promise<DatabaseNotification<Config
|
|
19
|
-
filterInAppUnreadNotifications(userId: Config["UserIdType"], page: number, pageSize: number): Promise<DatabaseNotification<Config
|
|
17
|
+
getNotification(notificationId: Config["NotificationIdType"], forUpdate: boolean): Promise<DatabaseNotification<Config> | null>;
|
|
18
|
+
filterAllInAppUnreadNotifications(userId: Config["UserIdType"]): Promise<DatabaseNotification<Config>[]>;
|
|
19
|
+
filterInAppUnreadNotifications(userId: Config["UserIdType"], page: number, pageSize: number): Promise<DatabaseNotification<Config>[]>;
|
|
20
20
|
getUserEmailFromNotification(notificationId: Config["NotificationIdType"]): Promise<string | undefined>;
|
|
21
21
|
storeContextUsed(notificationId: Config["NotificationIdType"], context: InputJsonValue): Promise<void>;
|
|
22
22
|
}
|
|
@@ -1,37 +1,43 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type {
|
|
3
|
-
import type {
|
|
4
|
-
import type { JsonObject } from '../types/json-values';
|
|
1
|
+
import type { DatabaseNotification, Notification } from '../types/notification';
|
|
2
|
+
import type { ContextGenerator } from '../types/notification-context-generators';
|
|
3
|
+
import type { JsonObject, JsonPrimitive } from '../types/json-values';
|
|
5
4
|
import type { BaseNotificationTypeConfig } from '../types/notification-type-config';
|
|
5
|
+
import type { BaseNotificationAdapter } from './notification-adapters/base-notification-adapter';
|
|
6
|
+
import type { BaseNotificationTemplateRenderer } from './notification-template-renderers/base-notification-template-renderer';
|
|
7
|
+
import type { BaseNotificationBackend } from './notification-backends/base-notification-backend';
|
|
8
|
+
import type { BaseLogger } from './loggers/base-logger';
|
|
9
|
+
import type { BaseNotificationQueueService } from './notification-queue-service/base-notification-queue-service';
|
|
6
10
|
type NotificationServiceOptions = {
|
|
7
11
|
raiseErrorOnFailedSend: boolean;
|
|
8
12
|
};
|
|
9
|
-
export declare class NotificationService<Config extends BaseNotificationTypeConfig> {
|
|
13
|
+
export declare class NotificationService<Config extends BaseNotificationTypeConfig<ContextGeneratorsMap>, ContextGeneratorsMap extends Record<string, ContextGenerator<Parameters<ContextGeneratorsMap[keyof ContextGeneratorsMap]['generate']>[0] extends Record<string, JsonPrimitive> ? Parameters<ContextGeneratorsMap[keyof ContextGeneratorsMap]['generate']>[0] : never, ReturnType<ContextGeneratorsMap[keyof ContextGeneratorsMap]['generate']> extends JsonObject ? ReturnType<ContextGeneratorsMap[keyof ContextGeneratorsMap]['generate']> : Awaited<ReturnType<ContextGeneratorsMap[keyof ContextGeneratorsMap]['generate']>> extends JsonObject ? Awaited<ReturnType<ContextGeneratorsMap[keyof ContextGeneratorsMap]['generate']>> : never>>> {
|
|
10
14
|
private adapters;
|
|
11
15
|
private backend;
|
|
12
16
|
private logger;
|
|
17
|
+
private contextGeneratorsMap;
|
|
13
18
|
private queueService?;
|
|
14
19
|
private options;
|
|
15
|
-
constructor(adapters: Config[
|
|
16
|
-
registerQueueService(queueService: Config
|
|
17
|
-
send(notification:
|
|
18
|
-
createNotification(notification: Omit<Notification<Config
|
|
19
|
-
updateNotification(notificationId:
|
|
20
|
-
getAllFutureNotifications(): Promise<
|
|
21
|
-
getAllFutureNotificationsFromUser(userId: Config['NotificationIdType']): Promise<
|
|
22
|
-
getFutureNotificationsFromUser(userId: Config['NotificationIdType']): Promise<
|
|
23
|
-
getFutureNotifications(): Promise<
|
|
24
|
-
getNotificationContext
|
|
20
|
+
constructor(adapters: BaseNotificationAdapter<BaseNotificationTemplateRenderer<Config>, Config>[], backend: BaseNotificationBackend<Config>, logger: BaseLogger, contextGeneratorsMap: ContextGeneratorsMap, queueService?: BaseNotificationQueueService<Config> | undefined, options?: NotificationServiceOptions);
|
|
21
|
+
registerQueueService(queueService: BaseNotificationQueueService<Config>): void;
|
|
22
|
+
send(notification: DatabaseNotification<Config>): Promise<void>;
|
|
23
|
+
createNotification(notification: Omit<Notification<Config>, 'id'>): Promise<DatabaseNotification<Config>>;
|
|
24
|
+
updateNotification(notificationId: Config['NotificationIdType'], notification: Partial<Omit<Notification<Config>, 'id'>>): Promise<DatabaseNotification<Config>>;
|
|
25
|
+
getAllFutureNotifications(): Promise<DatabaseNotification<Config>[]>;
|
|
26
|
+
getAllFutureNotificationsFromUser(userId: Config['NotificationIdType']): Promise<DatabaseNotification<Config>[]>;
|
|
27
|
+
getFutureNotificationsFromUser(userId: Config['NotificationIdType']): Promise<DatabaseNotification<Config>[]>;
|
|
28
|
+
getFutureNotifications(): Promise<DatabaseNotification<Config>[]>;
|
|
29
|
+
getNotificationContext<ContextName extends keyof Config['ContextMap']>(contextName: ContextName, parameters: Parameters<ContextGeneratorsMap[ContextName]['generate']>[0]): Promise<ReturnType<ContextGeneratorsMap[keyof ContextGeneratorsMap]["generate"]> extends JsonObject ? ReturnType<ContextGeneratorsMap[keyof ContextGeneratorsMap]["generate"]> : Awaited<ReturnType<ContextGeneratorsMap[keyof ContextGeneratorsMap]["generate"]>> extends JsonObject ? Awaited<ReturnType<ContextGeneratorsMap[keyof ContextGeneratorsMap]["generate"]>> : never>;
|
|
25
30
|
sendPendingNotifications(): Promise<void>;
|
|
26
|
-
getPendingNotifications(): Promise<
|
|
27
|
-
getNotification(notificationId: Config['NotificationIdType'], forUpdate?: boolean): Promise<
|
|
28
|
-
markRead(notificationId: Config['NotificationIdType']): Promise<
|
|
29
|
-
getInAppUnread(userId: Config['NotificationIdType']): Promise<
|
|
31
|
+
getPendingNotifications(): Promise<DatabaseNotification<Config>[]>;
|
|
32
|
+
getNotification(notificationId: Config['NotificationIdType'], forUpdate?: boolean): Promise<DatabaseNotification<Config> | null>;
|
|
33
|
+
markRead(notificationId: Config['NotificationIdType'], checkIsSent?: boolean): Promise<DatabaseNotification<Config>>;
|
|
34
|
+
getInAppUnread(userId: Config['NotificationIdType']): Promise<DatabaseNotification<Config>[]>;
|
|
30
35
|
cancelNotification(notificationId: Config['NotificationIdType']): Promise<void>;
|
|
36
|
+
resendNotification(notificationId: Config['NotificationIdType'], useStoredContextIfAvailable?: boolean): Promise<DatabaseNotification<Config> | undefined>;
|
|
31
37
|
delayedSend(notificationId: Config['NotificationIdType']): Promise<void>;
|
|
32
38
|
}
|
|
33
39
|
export declare class NotificationServiceSingleton {
|
|
34
40
|
private static instance;
|
|
35
|
-
static getInstance<Config extends BaseNotificationTypeConfig>(...args: ConstructorParameters<typeof NotificationService> | []): NotificationService<Config>;
|
|
41
|
+
static getInstance<Config extends BaseNotificationTypeConfig<ContextGeneratorsMap>, ContextGeneratorsMap extends Record<string, ContextGenerator<Parameters<ContextGeneratorsMap[keyof ContextGeneratorsMap]['generate']>[0] extends Record<string, JsonPrimitive> ? Parameters<ContextGeneratorsMap[keyof ContextGeneratorsMap]['generate']>[0] : never, ReturnType<ContextGeneratorsMap[keyof ContextGeneratorsMap]['generate']> extends JsonObject ? ReturnType<ContextGeneratorsMap[keyof ContextGeneratorsMap]['generate']> : Awaited<ReturnType<ContextGeneratorsMap[keyof ContextGeneratorsMap]['generate']>> extends JsonObject ? Awaited<ReturnType<ContextGeneratorsMap[keyof ContextGeneratorsMap]['generate']>> : never>>>(...args: ConstructorParameters<typeof NotificationService> | []): NotificationService<Config, ContextGeneratorsMap>;
|
|
36
42
|
}
|
|
37
43
|
export {};
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.NotificationServiceSingleton = exports.NotificationService = void 0;
|
|
4
|
-
const notification_context_registry_1 = require("./notification-context-registry");
|
|
5
4
|
class NotificationService {
|
|
6
|
-
constructor(adapters, backend, logger, queueService, options = {
|
|
5
|
+
constructor(adapters, backend, logger, contextGeneratorsMap, queueService, options = {
|
|
7
6
|
raiseErrorOnFailedSend: false,
|
|
8
7
|
}) {
|
|
9
8
|
this.adapters = adapters;
|
|
10
9
|
this.backend = backend;
|
|
11
10
|
this.logger = logger;
|
|
11
|
+
this.contextGeneratorsMap = contextGeneratorsMap;
|
|
12
12
|
this.queueService = queueService;
|
|
13
13
|
this.options = options;
|
|
14
14
|
for (const adapter of adapters) {
|
|
@@ -28,7 +28,7 @@ class NotificationService {
|
|
|
28
28
|
return;
|
|
29
29
|
}
|
|
30
30
|
if (!notification.id) {
|
|
31
|
-
throw new Error("Notification
|
|
31
|
+
throw new Error("Notification wasn't created in the database. Please create it first");
|
|
32
32
|
}
|
|
33
33
|
for (const adapter of adaptersOfType) {
|
|
34
34
|
if (adapter.enqueueNotifications) {
|
|
@@ -48,16 +48,21 @@ class NotificationService {
|
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
let context = null;
|
|
51
|
-
|
|
52
|
-
context =
|
|
53
|
-
this.logger.info(`Generated context for notification ${notification.id}`);
|
|
51
|
+
if (notification.contextUsed) {
|
|
52
|
+
context = notification.contextUsed;
|
|
54
53
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
54
|
+
else {
|
|
55
|
+
try {
|
|
56
|
+
context = await this.getNotificationContext(notification.contextName, notification.contextParameters);
|
|
57
|
+
this.logger.info(`Generated context for notification ${notification.id}`);
|
|
58
|
+
}
|
|
59
|
+
catch (contextError) {
|
|
60
|
+
this.logger.error(`Error getting context for notification ${notification.id}: ${contextError}`);
|
|
61
|
+
if (this.options.raiseErrorOnFailedSend) {
|
|
62
|
+
throw contextError;
|
|
63
|
+
}
|
|
64
|
+
return;
|
|
59
65
|
}
|
|
60
|
-
return;
|
|
61
66
|
}
|
|
62
67
|
try {
|
|
63
68
|
this.logger.info(`Sending notification ${notification.id} with adapter ${adapter.key}`);
|
|
@@ -67,7 +72,7 @@ class NotificationService {
|
|
|
67
72
|
catch (sendError) {
|
|
68
73
|
this.logger.error(`Error sending notification ${notification.id} with adapter ${adapter.key}: ${sendError}`);
|
|
69
74
|
try {
|
|
70
|
-
await this.backend.
|
|
75
|
+
await this.backend.markAsFailed(notification.id, true);
|
|
71
76
|
}
|
|
72
77
|
catch (markFailedError) {
|
|
73
78
|
this.logger.error(`Error marking notification ${notification.id} as failed: ${markFailedError}`);
|
|
@@ -75,7 +80,7 @@ class NotificationService {
|
|
|
75
80
|
continue;
|
|
76
81
|
}
|
|
77
82
|
try {
|
|
78
|
-
await this.backend.
|
|
83
|
+
await this.backend.markAsSent(notification.id, true);
|
|
79
84
|
}
|
|
80
85
|
catch (markSentError) {
|
|
81
86
|
this.logger.error(`Error marking notification ${notification.id} as sent: ${markSentError}`);
|
|
@@ -118,8 +123,11 @@ class NotificationService {
|
|
|
118
123
|
return this.backend.getFutureNotifications();
|
|
119
124
|
}
|
|
120
125
|
async getNotificationContext(contextName, parameters) {
|
|
121
|
-
const
|
|
122
|
-
|
|
126
|
+
const context = this.contextGeneratorsMap[contextName].generate(parameters);
|
|
127
|
+
if (context instanceof Promise) {
|
|
128
|
+
return await context;
|
|
129
|
+
}
|
|
130
|
+
return Promise.resolve(context);
|
|
123
131
|
}
|
|
124
132
|
async sendPendingNotifications() {
|
|
125
133
|
const pendingNotifications = await this.backend.getAllPendingNotifications();
|
|
@@ -131,8 +139,8 @@ class NotificationService {
|
|
|
131
139
|
async getNotification(notificationId, forUpdate = false) {
|
|
132
140
|
return this.backend.getNotification(notificationId, forUpdate);
|
|
133
141
|
}
|
|
134
|
-
async markRead(notificationId) {
|
|
135
|
-
const notification = this.backend.
|
|
142
|
+
async markRead(notificationId, checkIsSent = true) {
|
|
143
|
+
const notification = await this.backend.markAsRead(notificationId, checkIsSent);
|
|
136
144
|
this.logger.info(`Notification ${notificationId} marked as read`);
|
|
137
145
|
return notification;
|
|
138
146
|
}
|
|
@@ -143,6 +151,48 @@ class NotificationService {
|
|
|
143
151
|
await this.backend.cancelNotification(notificationId);
|
|
144
152
|
this.logger.info(`Notification ${notificationId} cancelled`);
|
|
145
153
|
}
|
|
154
|
+
async resendNotification(notificationId, useStoredContextIfAvailable = false) {
|
|
155
|
+
const notification = await this.getNotification(notificationId, false);
|
|
156
|
+
if (!notification) {
|
|
157
|
+
this.logger.error(`Notification ${notificationId} not found`);
|
|
158
|
+
if (this.options.raiseErrorOnFailedSend) {
|
|
159
|
+
throw new Error(`Notification ${notificationId} not found`);
|
|
160
|
+
}
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if ((notification === null || notification === void 0 ? void 0 : notification.sendAfter) && notification.sendAfter > new Date()) {
|
|
164
|
+
this.logger.error(`Notification ${notificationId} is scheduled for the future`);
|
|
165
|
+
if (this.options.raiseErrorOnFailedSend) {
|
|
166
|
+
throw new Error(`Notification ${notificationId} is scheduled for the future`);
|
|
167
|
+
}
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (useStoredContextIfAvailable && !notification.contextUsed) {
|
|
171
|
+
this.logger.error(`Context not found for notification ${notificationId}`);
|
|
172
|
+
if (this.options.raiseErrorOnFailedSend) {
|
|
173
|
+
throw new Error(`Context not found for notification ${notificationId}`);
|
|
174
|
+
}
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const notificationResendInput = {
|
|
178
|
+
userId: notification.userId,
|
|
179
|
+
notificationType: notification.notificationType,
|
|
180
|
+
title: notification.title,
|
|
181
|
+
bodyTemplate: notification.bodyTemplate,
|
|
182
|
+
contextName: notification.contextName,
|
|
183
|
+
contextParameters: notification.contextParameters,
|
|
184
|
+
contextUsed: useStoredContextIfAvailable && notification.contextUsed
|
|
185
|
+
? notification.contextUsed
|
|
186
|
+
: await this.getNotificationContext(notification.contextName, notification.contextParameters),
|
|
187
|
+
sendAfter: null,
|
|
188
|
+
subjectTemplate: notification.subjectTemplate,
|
|
189
|
+
extraParams: notification.extraParams,
|
|
190
|
+
};
|
|
191
|
+
const createdNotification = await this.backend.persistNotification(notificationResendInput);
|
|
192
|
+
this.logger.info(`Notification ${createdNotification.id} created for resending notification ${notificationId}`);
|
|
193
|
+
this.send(createdNotification);
|
|
194
|
+
return createdNotification;
|
|
195
|
+
}
|
|
146
196
|
async delayedSend(notificationId) {
|
|
147
197
|
const notification = await this.getNotification(notificationId, false);
|
|
148
198
|
if (!notification) {
|
|
@@ -168,14 +218,14 @@ class NotificationService {
|
|
|
168
218
|
catch (sendError) {
|
|
169
219
|
this.logger.error(`Error sending notification ${notification.id} with adapter ${adapter.key}: ${sendError}`);
|
|
170
220
|
try {
|
|
171
|
-
await this.backend.
|
|
221
|
+
await this.backend.markAsFailed(notification.id, true);
|
|
172
222
|
}
|
|
173
223
|
catch (markFailedError) {
|
|
174
224
|
this.logger.error(`Error marking notification ${notification.id} as failed: ${markFailedError}`);
|
|
175
225
|
}
|
|
176
226
|
}
|
|
177
227
|
try {
|
|
178
|
-
await this.backend.
|
|
228
|
+
await this.backend.markAsSent(notification.id, true);
|
|
179
229
|
}
|
|
180
230
|
catch (markSentError) {
|
|
181
231
|
this.logger.error(`Error marking notification ${notification.id} as sent: ${markSentError}`);
|
|
@@ -9,5 +9,5 @@ export type EmailTemplate = {
|
|
|
9
9
|
body: string;
|
|
10
10
|
};
|
|
11
11
|
export interface BaseEmailTemplateRenderer<Config extends BaseNotificationTypeConfig> extends BaseNotificationTemplateRenderer<Config, EmailTemplate> {
|
|
12
|
-
render(notification: Notification<Config
|
|
12
|
+
render(notification: Notification<Config>, context: JsonObject): Promise<EmailTemplate>;
|
|
13
13
|
}
|
package/dist/services/notification-template-renderers/base-notification-template-renderer.d.ts
CHANGED
|
@@ -2,5 +2,5 @@ import type { JsonObject } from '../../types/json-values';
|
|
|
2
2
|
import type { Notification } from '../../types/notification';
|
|
3
3
|
import type { BaseNotificationTypeConfig } from '../../types/notification-type-config';
|
|
4
4
|
export interface BaseNotificationTemplateRenderer<Config extends BaseNotificationTypeConfig, T = unknown> {
|
|
5
|
-
render(notification: Notification<Config
|
|
5
|
+
render(notification: Notification<Config>, context: JsonObject): Promise<T>;
|
|
6
6
|
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { JsonObject, JsonPrimitive } from './json-values';
|
|
2
|
+
export interface ContextGenerator<Params extends Record<string, JsonPrimitive> = Record<string, JsonPrimitive>, Context extends JsonObject = JsonObject> {
|
|
3
|
+
generate(params: Params): Context | Promise<Context>;
|
|
4
|
+
}
|
|
@@ -1,16 +1,7 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type { BaseNotificationAdapter } from "../services/notification-adapters/base-notification-adapter";
|
|
3
|
-
import type { BaseNotificationBackend } from "../services/notification-backends/base-notification-backend";
|
|
4
|
-
import type { ContextGenerator } from "../services/notification-context-registry";
|
|
5
|
-
import type { BaseNotificationQueueService } from "../services/notification-queue-service/base-notification-queue-service";
|
|
6
|
-
import type { BaseNotificationTemplateRenderer } from "../services/notification-template-renderers/base-notification-template-renderer";
|
|
1
|
+
import type { ContextGenerator } from "./notification-context-generators";
|
|
7
2
|
import type { Identifier } from "./identifier";
|
|
8
|
-
export type BaseNotificationTypeConfig = {
|
|
9
|
-
ContextMap:
|
|
3
|
+
export type BaseNotificationTypeConfig<ContextMapType extends Record<string, ContextGenerator> = Record<string, ContextGenerator>> = {
|
|
4
|
+
ContextMap: ContextMapType;
|
|
10
5
|
NotificationIdType: Identifier;
|
|
11
6
|
UserIdType: Identifier;
|
|
12
|
-
AdaptersList: BaseNotificationAdapter<BaseNotificationTemplateRenderer<BaseNotificationTypeConfig>, BaseNotificationTypeConfig>[];
|
|
13
|
-
Backend: BaseNotificationBackend<BaseNotificationTypeConfig>;
|
|
14
|
-
Logger: BaseLogger;
|
|
15
|
-
QueueService: BaseNotificationQueueService<BaseNotificationTypeConfig>;
|
|
16
7
|
};
|
|
@@ -1,32 +1,42 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type { Identifier } from './identifier';
|
|
3
|
-
import type { InputJsonValue, JsonValue } from './json-values';
|
|
1
|
+
import type { InputJsonValue, JsonObject, JsonValue } from './json-values';
|
|
4
2
|
import type { NotificationStatus } from './notification-status';
|
|
5
3
|
import type { NotificationType } from './notification-type';
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
userId: UserIdType;
|
|
4
|
+
import type { BaseNotificationTypeConfig } from './notification-type-config';
|
|
5
|
+
export type NotificationInput<Config extends BaseNotificationTypeConfig> = {
|
|
6
|
+
userId: Config['UserIdType'];
|
|
9
7
|
notificationType: NotificationType;
|
|
10
8
|
title: string | null;
|
|
11
9
|
bodyTemplate: string;
|
|
12
|
-
contextName: keyof
|
|
13
|
-
contextParameters: Parameters<
|
|
10
|
+
contextName: keyof Config['ContextMap'];
|
|
11
|
+
contextParameters: Parameters<Config['ContextMap'][NotificationInput<Config>['contextName']]['generate']>[0];
|
|
14
12
|
sendAfter: Date | null;
|
|
15
13
|
subjectTemplate: string | null;
|
|
16
14
|
extraParams: InputJsonValue | null;
|
|
17
15
|
};
|
|
18
|
-
export type
|
|
19
|
-
|
|
20
|
-
userId: UserIdType;
|
|
16
|
+
export type NotificationResendWithContextInput<Config extends BaseNotificationTypeConfig> = {
|
|
17
|
+
userId: Config['UserIdType'];
|
|
21
18
|
notificationType: NotificationType;
|
|
22
19
|
title: string | null;
|
|
23
20
|
bodyTemplate: string;
|
|
24
|
-
contextName: keyof
|
|
25
|
-
contextParameters: Parameters<
|
|
21
|
+
contextName: keyof Config['ContextMap'];
|
|
22
|
+
contextParameters: Parameters<Config['ContextMap'][NotificationResendWithContextInput<Config>['contextName']]['generate']>[0];
|
|
23
|
+
contextUsed: ReturnType<Config['ContextMap'][DatabaseNotification<Config>['contextName']]['generate']> extends JsonObject ? ReturnType<Config['ContextMap'][DatabaseNotification<Config>['contextName']]['generate']> : Awaited<ReturnType<Config['ContextMap'][DatabaseNotification<Config>['contextName']]['generate']>>;
|
|
24
|
+
sendAfter: Date | null;
|
|
25
|
+
subjectTemplate: string | null;
|
|
26
|
+
extraParams: InputJsonValue | null;
|
|
27
|
+
};
|
|
28
|
+
export type DatabaseNotification<Config extends BaseNotificationTypeConfig> = {
|
|
29
|
+
id: Config['NotificationIdType'];
|
|
30
|
+
userId: Config['UserIdType'];
|
|
31
|
+
notificationType: NotificationType;
|
|
32
|
+
title: string | null;
|
|
33
|
+
bodyTemplate: string;
|
|
34
|
+
contextName: keyof Config['ContextMap'];
|
|
35
|
+
contextParameters: Parameters<Config['ContextMap'][DatabaseNotification<Config>['contextName']]['generate']>[0];
|
|
26
36
|
sendAfter: Date | null;
|
|
27
37
|
subjectTemplate: string | null;
|
|
28
38
|
status: NotificationStatus;
|
|
29
|
-
contextUsed: ReturnType<
|
|
39
|
+
contextUsed: ReturnType<Config['ContextMap'][DatabaseNotification<Config>['contextName']]['generate']> extends JsonObject ? ReturnType<Config['ContextMap'][DatabaseNotification<Config>['contextName']]['generate']> : Awaited<ReturnType<Config['ContextMap'][DatabaseNotification<Config>['contextName']]['generate']>>;
|
|
30
40
|
extraParams: JsonValue;
|
|
31
41
|
adapterUsed: string | null;
|
|
32
42
|
sentAt: Date | null;
|
|
@@ -34,4 +44,4 @@ export type DatabaseNotification<AvailableContexts extends Record<string, Contex
|
|
|
34
44
|
createdAt?: Date;
|
|
35
45
|
updatedAt?: Date;
|
|
36
46
|
};
|
|
37
|
-
export type Notification<
|
|
47
|
+
export type Notification<Config extends BaseNotificationTypeConfig> = NotificationInput<Config> | NotificationResendWithContextInput<Config> | DatabaseNotification<Config>;
|