@clipboard-health/notifications 0.5.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +133 -43
- package/package.json +5 -5
- package/src/index.d.ts +2 -2
- package/src/index.js +2 -2
- package/src/index.js.map +1 -1
- package/src/lib/idempotencyKey.d.ts +46 -0
- package/src/lib/idempotencyKey.js +32 -0
- package/src/lib/idempotencyKey.js.map +1 -0
- package/src/lib/internal/chunkRecipients.d.ts +18 -0
- package/src/lib/internal/chunkRecipients.js +28 -0
- package/src/lib/internal/chunkRecipients.js.map +1 -0
- package/src/lib/internal/createDeterministicHash.d.ts +13 -0
- package/src/lib/internal/createDeterministicHash.js +23 -0
- package/src/lib/internal/createDeterministicHash.js.map +1 -0
- package/src/lib/internal/createTriggerLogParams.d.ts +1 -1
- package/src/lib/internal/createTriggerLogParams.js +2 -2
- package/src/lib/internal/createTriggerLogParams.js.map +1 -1
- package/src/lib/internal/createTriggerTraceOptions.js +6 -3
- package/src/lib/internal/createTriggerTraceOptions.js.map +1 -1
- package/src/lib/internal/idempotencyKeyDoNotImportOutsideNotificationsLibrary.d.ts +35 -0
- package/src/lib/internal/idempotencyKeyDoNotImportOutsideNotificationsLibrary.js +43 -0
- package/src/lib/internal/idempotencyKeyDoNotImportOutsideNotificationsLibrary.js.map +1 -0
- package/src/lib/internal/idempotentKnock.d.ts +5 -0
- package/src/lib/internal/idempotentKnock.js +5 -0
- package/src/lib/internal/idempotentKnock.js.map +1 -1
- package/src/lib/internal/toInlineIdentifyUserRequest.d.ts +3 -0
- package/src/lib/internal/toInlineIdentifyUserRequest.js +12 -0
- package/src/lib/internal/toInlineIdentifyUserRequest.js.map +1 -0
- package/src/lib/internal/toInlineIdentifyUserRequestWithoutUserId.d.ts +3 -0
- package/src/lib/internal/toInlineIdentifyUserRequestWithoutUserId.js +18 -0
- package/src/lib/internal/toInlineIdentifyUserRequestWithoutUserId.js.map +1 -0
- package/src/lib/internal/toTenantSetRequest.d.ts +3 -0
- package/src/lib/internal/toTenantSetRequest.js +9 -0
- package/src/lib/internal/toTenantSetRequest.js.map +1 -0
- package/src/lib/internal/toTriggerBody.d.ts +3 -0
- package/src/lib/internal/toTriggerBody.js +21 -0
- package/src/lib/internal/toTriggerBody.js.map +1 -0
- package/src/lib/notificationClient.d.ts +31 -38
- package/src/lib/notificationClient.js +39 -49
- package/src/lib/notificationClient.js.map +1 -1
- package/src/lib/notificationJobEnqueuer.d.ts +96 -0
- package/src/lib/notificationJobEnqueuer.js +101 -0
- package/src/lib/notificationJobEnqueuer.js.map +1 -0
- package/src/lib/types.d.ts +17 -20
- package/src/lib/types.js.map +1 -1
- package/src/lib/chunkRecipients.d.ts +0 -7
- package/src/lib/chunkRecipients.js +0 -18
- package/src/lib/chunkRecipients.js.map +0 -1
- package/src/lib/createIdempotencyKey.d.ts +0 -17
- package/src/lib/createIdempotencyKey.js +0 -28
- package/src/lib/createIdempotencyKey.js.map +0 -1
- package/src/lib/internal/toKnockBody.d.ts +0 -3
- package/src/lib/internal/toKnockBody.js +0 -35
- package/src/lib/internal/toKnockBody.js.map +0 -1
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.toTenantSetRequest = toTenantSetRequest;
|
|
4
|
+
const toInlineIdentifyUserRequestWithoutUserId_1 = require("./toInlineIdentifyUserRequestWithoutUserId");
|
|
5
|
+
function toTenantSetRequest(request) {
|
|
6
|
+
// Use the same type as user inline identify so provider field names are consistent.
|
|
7
|
+
return (0, toInlineIdentifyUserRequestWithoutUserId_1.toInlineIdentifyUserRequestWithoutUserId)(request);
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=toTenantSetRequest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"toTenantSetRequest.js","sourceRoot":"","sources":["../../../../../../packages/notifications/src/lib/internal/toTenantSetRequest.ts"],"names":[],"mappings":";;AAKA,gDAKC;AAPD,yGAAsG;AAEtG,SAAgB,kBAAkB,CAChC,OAAoD;IAEpD,oFAAoF;IACpF,OAAO,IAAA,mFAAwC,EAAC,OAAO,CAAC,CAAC;AAC3D,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.toTriggerBody = toTriggerBody;
|
|
4
|
+
const toInlineIdentifyUserRequest_1 = require("./toInlineIdentifyUserRequest");
|
|
5
|
+
function toTriggerBody(body) {
|
|
6
|
+
const { actor, cancellationKey, recipients, workplaceId, ...rest } = body;
|
|
7
|
+
return {
|
|
8
|
+
...(actor ? { actor: toRecipient(actor) } : {}),
|
|
9
|
+
...(cancellationKey ? { cancellation_key: cancellationKey } : {}),
|
|
10
|
+
...(workplaceId ? { tenant: workplaceId } : {}),
|
|
11
|
+
recipients: recipients.map(toRecipient),
|
|
12
|
+
...rest,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
function toRecipient(recipient) {
|
|
16
|
+
if (typeof recipient === "string") {
|
|
17
|
+
return recipient;
|
|
18
|
+
}
|
|
19
|
+
return (0, toInlineIdentifyUserRequest_1.toInlineIdentifyUserRequest)(recipient);
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=toTriggerBody.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"toTriggerBody.js","sourceRoot":"","sources":["../../../../../../packages/notifications/src/lib/internal/toTriggerBody.ts"],"names":[],"mappings":";;AAKA,sCAUC;AAZD,+EAA4E;AAE5E,SAAgB,aAAa,CAAC,IAAiB;IAC7C,MAAM,EAAE,KAAK,EAAE,eAAe,EAAE,UAAU,EAAE,WAAW,EAAE,GAAG,IAAI,EAAE,GAAG,IAAI,CAAC;IAE1E,OAAO;QACL,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,WAAW,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/C,GAAG,CAAC,eAAe,CAAC,CAAC,CAAC,EAAE,gBAAgB,EAAE,eAAe,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACjE,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/C,UAAU,EAAE,UAAU,CAAC,GAAG,CAAC,WAAW,CAAC;QACvC,GAAG,IAAI;KACR,CAAC;AACJ,CAAC;AAED,SAAS,WAAW,CAAC,SAA2B;IAC9C,IAAI,OAAO,SAAS,KAAK,QAAQ,EAAE,CAAC;QAClC,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,OAAO,IAAA,yDAA2B,EAAC,SAAS,CAAC,CAAC;AAChD,CAAC"}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { type LogFunction, type
|
|
2
|
-
import {
|
|
3
|
-
import type { AppendPushTokenRequest, AppendPushTokenResponse, LogParams, NotificationClientParams, SignUserTokenRequest, SignUserTokenResponse, Span, Tracer, TriggerRequest, TriggerResponse, UpsertWorkplaceRequest, UpsertWorkplaceResponse } from "./types";
|
|
1
|
+
import { type LogFunction, type ServiceResult } from "@clipboard-health/util-ts";
|
|
2
|
+
import type { AppendPushTokenRequest, AppendPushTokenResponse, LogParams, NotificationClientParams, SignUserTokenRequest, SignUserTokenResponse, Span, TriggerRequest, TriggerResponse, UpsertWorkplaceRequest, UpsertWorkplaceResponse } from "./types";
|
|
4
3
|
export declare const MAXIMUM_RECIPIENTS_COUNT = 1000;
|
|
5
4
|
export declare const ERROR_CODES: {
|
|
6
5
|
readonly expired: "expired";
|
|
@@ -18,9 +17,9 @@ interface NotificationError {
|
|
|
18
17
|
* Client for sending notifications through third-party providers.
|
|
19
18
|
*/
|
|
20
19
|
export declare class NotificationClient {
|
|
21
|
-
protected readonly logger:
|
|
22
|
-
protected readonly provider:
|
|
23
|
-
protected readonly tracer:
|
|
20
|
+
protected readonly logger: NotificationClientParams["logger"];
|
|
21
|
+
protected readonly provider: Required<NotificationClientParams>["provider"];
|
|
22
|
+
protected readonly tracer: NotificationClientParams["tracer"];
|
|
24
23
|
private readonly signingKey;
|
|
25
24
|
/**
|
|
26
25
|
* Creates a new NotificationClient instance.
|
|
@@ -39,44 +38,38 @@ export declare class NotificationClient {
|
|
|
39
38
|
* @returns Promise resolving to either an error or successful response.
|
|
40
39
|
*
|
|
41
40
|
* @example
|
|
42
|
-
* <embedex source="packages/notifications/examples/
|
|
41
|
+
* <embedex source="packages/notifications/examples/exampleNotification.service.ts">
|
|
43
42
|
*
|
|
44
43
|
* ```ts
|
|
45
|
-
* import {
|
|
46
|
-
* import { isSuccess } from "@clipboard-health/util-ts";
|
|
44
|
+
* import { type NotificationClient } from "@clipboard-health/notifications";
|
|
47
45
|
*
|
|
48
|
-
*
|
|
49
|
-
* apiKey: "test-api-key",
|
|
50
|
-
* logger: {
|
|
51
|
-
* info: console.log,
|
|
52
|
-
* warn: console.warn,
|
|
53
|
-
* error: console.error,
|
|
54
|
-
* } as const,
|
|
55
|
-
* tracer: {
|
|
56
|
-
* trace: <T>(_name: string, _options: unknown, fun: (span?: Span | undefined) => T): T => fun(),
|
|
57
|
-
* },
|
|
58
|
-
* });
|
|
46
|
+
* import { type ExampleNotificationJobData } from "./exampleNotification.job";
|
|
59
47
|
*
|
|
60
|
-
*
|
|
61
|
-
* const result = await client.trigger({
|
|
62
|
-
* attempt: (job?.attemptsCount ?? 0) + 1,
|
|
63
|
-
* body: {
|
|
64
|
-
* recipients: ["user-1"],
|
|
65
|
-
* data: { favoriteColor: "blue", secret: "2" },
|
|
66
|
-
* },
|
|
67
|
-
* expiresAt: new Date(Date.now() + 300_000), // 5 minutes
|
|
68
|
-
* idempotencyKey: "welcome-user-4",
|
|
69
|
-
* key: "welcome-email",
|
|
70
|
-
* keysToRedact: ["secret"],
|
|
71
|
-
* });
|
|
48
|
+
* type ExampleNotificationDo = ExampleNotificationJobData & { attempt: number };
|
|
72
49
|
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
50
|
+
* export class ExampleNotificationService {
|
|
51
|
+
* constructor(private readonly client: NotificationClient) {}
|
|
52
|
+
*
|
|
53
|
+
* async sendNotification(params: ExampleNotificationDo) {
|
|
54
|
+
* const { attempt, expiresAt, idempotencyKey, recipients, workflowKey, workplaceId } = params;
|
|
55
|
+
*
|
|
56
|
+
* // Assume this comes from a database and, for example, are used as template variables...
|
|
57
|
+
* const data = { favoriteColor: "blue", secret: "2" };
|
|
58
|
+
*
|
|
59
|
+
* return await this.client.trigger({
|
|
60
|
+
* attempt,
|
|
61
|
+
* body: {
|
|
62
|
+
* recipients,
|
|
63
|
+
* data,
|
|
64
|
+
* workplaceId,
|
|
65
|
+
* },
|
|
66
|
+
* expiresAt,
|
|
67
|
+
* idempotencyKey,
|
|
68
|
+
* workflowKey,
|
|
69
|
+
* keysToRedact: ["secret"],
|
|
70
|
+
* });
|
|
75
71
|
* }
|
|
76
72
|
* }
|
|
77
|
-
*
|
|
78
|
-
* // eslint-disable-next-line unicorn/prefer-top-level-await
|
|
79
|
-
* void triggerNotification({ attemptsCount: 0 });
|
|
80
73
|
* ```
|
|
81
74
|
*
|
|
82
75
|
* </embedex>
|
|
@@ -97,7 +90,7 @@ export declare class NotificationClient {
|
|
|
97
90
|
*/
|
|
98
91
|
signUserToken(params: SignUserTokenRequest): Promise<ServiceResult<SignUserTokenResponse>>;
|
|
99
92
|
/**
|
|
100
|
-
* Updates or creates a workplace (tenant) in
|
|
93
|
+
* Updates or creates a workplace (tenant) in our provider.
|
|
101
94
|
*
|
|
102
95
|
* @returns Promise resolving to either an error or successful response.
|
|
103
96
|
*/
|
|
@@ -5,10 +5,11 @@ const util_ts_1 = require("@clipboard-health/util-ts");
|
|
|
5
5
|
const node_1 = require("@knocklabs/node");
|
|
6
6
|
const createTriggerLogParams_1 = require("./internal/createTriggerLogParams");
|
|
7
7
|
const createTriggerTraceOptions_1 = require("./internal/createTriggerTraceOptions");
|
|
8
|
-
const
|
|
8
|
+
const idempotencyKeyDoNotImportOutsideNotificationsLibrary_1 = require("./internal/idempotencyKeyDoNotImportOutsideNotificationsLibrary");
|
|
9
9
|
const idempotentKnock_1 = require("./internal/idempotentKnock");
|
|
10
10
|
const redact_1 = require("./internal/redact");
|
|
11
|
-
const
|
|
11
|
+
const toTenantSetRequest_1 = require("./internal/toTenantSetRequest");
|
|
12
|
+
const toTriggerBody_1 = require("./internal/toTriggerBody");
|
|
12
13
|
const LOG_PARAMS = {
|
|
13
14
|
trigger: {
|
|
14
15
|
traceName: "notifications.trigger",
|
|
@@ -71,44 +72,38 @@ class NotificationClient {
|
|
|
71
72
|
* @returns Promise resolving to either an error or successful response.
|
|
72
73
|
*
|
|
73
74
|
* @example
|
|
74
|
-
* <embedex source="packages/notifications/examples/
|
|
75
|
+
* <embedex source="packages/notifications/examples/exampleNotification.service.ts">
|
|
75
76
|
*
|
|
76
77
|
* ```ts
|
|
77
|
-
* import {
|
|
78
|
-
* import { isSuccess } from "@clipboard-health/util-ts";
|
|
78
|
+
* import { type NotificationClient } from "@clipboard-health/notifications";
|
|
79
79
|
*
|
|
80
|
-
*
|
|
81
|
-
* apiKey: "test-api-key",
|
|
82
|
-
* logger: {
|
|
83
|
-
* info: console.log,
|
|
84
|
-
* warn: console.warn,
|
|
85
|
-
* error: console.error,
|
|
86
|
-
* } as const,
|
|
87
|
-
* tracer: {
|
|
88
|
-
* trace: <T>(_name: string, _options: unknown, fun: (span?: Span | undefined) => T): T => fun(),
|
|
89
|
-
* },
|
|
90
|
-
* });
|
|
80
|
+
* import { type ExampleNotificationJobData } from "./exampleNotification.job";
|
|
91
81
|
*
|
|
92
|
-
*
|
|
93
|
-
* const result = await client.trigger({
|
|
94
|
-
* attempt: (job?.attemptsCount ?? 0) + 1,
|
|
95
|
-
* body: {
|
|
96
|
-
* recipients: ["user-1"],
|
|
97
|
-
* data: { favoriteColor: "blue", secret: "2" },
|
|
98
|
-
* },
|
|
99
|
-
* expiresAt: new Date(Date.now() + 300_000), // 5 minutes
|
|
100
|
-
* idempotencyKey: "welcome-user-4",
|
|
101
|
-
* key: "welcome-email",
|
|
102
|
-
* keysToRedact: ["secret"],
|
|
103
|
-
* });
|
|
82
|
+
* type ExampleNotificationDo = ExampleNotificationJobData & { attempt: number };
|
|
104
83
|
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
84
|
+
* export class ExampleNotificationService {
|
|
85
|
+
* constructor(private readonly client: NotificationClient) {}
|
|
86
|
+
*
|
|
87
|
+
* async sendNotification(params: ExampleNotificationDo) {
|
|
88
|
+
* const { attempt, expiresAt, idempotencyKey, recipients, workflowKey, workplaceId } = params;
|
|
89
|
+
*
|
|
90
|
+
* // Assume this comes from a database and, for example, are used as template variables...
|
|
91
|
+
* const data = { favoriteColor: "blue", secret: "2" };
|
|
92
|
+
*
|
|
93
|
+
* return await this.client.trigger({
|
|
94
|
+
* attempt,
|
|
95
|
+
* body: {
|
|
96
|
+
* recipients,
|
|
97
|
+
* data,
|
|
98
|
+
* workplaceId,
|
|
99
|
+
* },
|
|
100
|
+
* expiresAt,
|
|
101
|
+
* idempotencyKey,
|
|
102
|
+
* workflowKey,
|
|
103
|
+
* keysToRedact: ["secret"],
|
|
104
|
+
* });
|
|
107
105
|
* }
|
|
108
106
|
* }
|
|
109
|
-
*
|
|
110
|
-
* // eslint-disable-next-line unicorn/prefer-top-level-await
|
|
111
|
-
* void triggerNotification({ attemptsCount: 0 });
|
|
112
107
|
* ```
|
|
113
108
|
*
|
|
114
109
|
* </embedex>
|
|
@@ -121,10 +116,15 @@ class NotificationClient {
|
|
|
121
116
|
return validated;
|
|
122
117
|
}
|
|
123
118
|
try {
|
|
124
|
-
const {
|
|
119
|
+
const { body, idempotencyKey, key, keysToRedact = [], workflowKey } = validated.value;
|
|
120
|
+
const triggerBody = (0, toTriggerBody_1.toTriggerBody)(body);
|
|
125
121
|
this.logTriggerRequest({ logParams, body, keysToRedact });
|
|
126
|
-
const response = await this.provider.workflows.trigger(
|
|
127
|
-
|
|
122
|
+
const response = await this.provider.workflows.trigger(
|
|
123
|
+
/* istanbul ignore next */ workflowKey ?? key, triggerBody, {
|
|
124
|
+
idempotencyKey: idempotencyKey instanceof idempotencyKeyDoNotImportOutsideNotificationsLibrary_1.IdempotencyKeyDoNotImportOutsideNotificationsLibrary
|
|
125
|
+
? idempotencyKey.toHash({ workplaceId: params.body.workplaceId })
|
|
126
|
+
: /* istanbul ignore next */
|
|
127
|
+
idempotencyKey,
|
|
128
128
|
});
|
|
129
129
|
const id = response.workflow_run_id;
|
|
130
130
|
this.logTriggerResponse({ span, response, id, logParams });
|
|
@@ -227,7 +227,7 @@ class NotificationClient {
|
|
|
227
227
|
}
|
|
228
228
|
}
|
|
229
229
|
/**
|
|
230
|
-
* Updates or creates a workplace (tenant) in
|
|
230
|
+
* Updates or creates a workplace (tenant) in our provider.
|
|
231
231
|
*
|
|
232
232
|
* @returns Promise resolving to either an error or successful response.
|
|
233
233
|
*/
|
|
@@ -236,17 +236,7 @@ class NotificationClient {
|
|
|
236
236
|
const logParams = { ...LOG_PARAMS.upsertWorkplace, workplaceId, ...body };
|
|
237
237
|
try {
|
|
238
238
|
this.logger.info(`${logParams.traceName} request`, logParams);
|
|
239
|
-
|
|
240
|
-
const knockBody = {
|
|
241
|
-
...(body.createdAt ? { created_at: body.createdAt.toISOString() } : {}),
|
|
242
|
-
...(body.email ? { email: body.email } : {}),
|
|
243
|
-
...(body.name ? { name: body.name } : {}),
|
|
244
|
-
...(body.phoneNumber
|
|
245
|
-
? { phone_number: (0, formatPhoneNumber_1.formatPhoneNumber)({ phoneNumber: body.phoneNumber }) }
|
|
246
|
-
: {}),
|
|
247
|
-
...(body.timeZone ? { timezone: body.timeZone } : {}),
|
|
248
|
-
};
|
|
249
|
-
const response = await this.provider.tenants.set(workplaceId, knockBody);
|
|
239
|
+
const response = await this.provider.tenants.set(workplaceId, (0, toTenantSetRequest_1.toTenantSetRequest)(body));
|
|
250
240
|
this.logger.info(`${logParams.traceName} response`, {
|
|
251
241
|
...logParams,
|
|
252
242
|
response: { workplaceId: response.id, name: response.name },
|
|
@@ -307,7 +297,7 @@ class NotificationClient {
|
|
|
307
297
|
});
|
|
308
298
|
}
|
|
309
299
|
const now = new Date();
|
|
310
|
-
if (now > expiresAt) {
|
|
300
|
+
if (expiresAt instanceof Date && now > expiresAt) {
|
|
311
301
|
return this.createAndLogError({
|
|
312
302
|
notificationError: {
|
|
313
303
|
code: exports.ERROR_CODES.expired,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"notificationClient.js","sourceRoot":"","sources":["../../../../../packages/notifications/src/lib/notificationClient.ts"],"names":[],"mappings":";;;AAAA,
|
|
1
|
+
{"version":3,"file":"notificationClient.js","sourceRoot":"","sources":["../../../../../packages/notifications/src/lib/notificationClient.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { type BackgroundJobsAdapter } from "@clipboard-health/background-jobs-adapter";
|
|
2
|
+
import { type IdempotencyKey } from "./idempotencyKey";
|
|
3
|
+
import { IdempotencyKeyDoNotImportOutsideNotificationsLibrary } from "./internal/idempotencyKeyDoNotImportOutsideNotificationsLibrary";
|
|
4
|
+
import { type ErrorCode } from "./notificationClient";
|
|
5
|
+
import { type TriggerRequest } from "./types";
|
|
6
|
+
/**
|
|
7
|
+
* Assuming `NotificationClient` is called from a background job with unchanging recipients, only
|
|
8
|
+
* the following are retryable.
|
|
9
|
+
*/
|
|
10
|
+
export declare const RETRYABLE_ERRORS: ErrorCode[];
|
|
11
|
+
type EnqueueParameters = Parameters<BackgroundJobsAdapter["enqueue"]>;
|
|
12
|
+
export interface NotificationEnqueueData {
|
|
13
|
+
idempotencyKey: IdempotencyKey;
|
|
14
|
+
/** @see {@link TriggerRequest.expiresAt} */
|
|
15
|
+
expiresAt: TriggerRequest["expiresAt"];
|
|
16
|
+
/** @see {@link TriggerBody.recipients} */
|
|
17
|
+
recipients: string[];
|
|
18
|
+
/** @see {@link TriggerRequest.workflowKey} */
|
|
19
|
+
workflowKey: string;
|
|
20
|
+
}
|
|
21
|
+
export interface NotificationJobData extends Omit<NotificationEnqueueData, "idempotencyKey"> {
|
|
22
|
+
idempotencyKey: IdempotencyKeyDoNotImportOutsideNotificationsLibrary;
|
|
23
|
+
}
|
|
24
|
+
interface NotificationJobEnqueuerParams {
|
|
25
|
+
adapter: BackgroundJobsAdapter;
|
|
26
|
+
}
|
|
27
|
+
export declare class NotificationJobEnqueuer {
|
|
28
|
+
private readonly adapter;
|
|
29
|
+
constructor(params: NotificationJobEnqueuerParams);
|
|
30
|
+
/**
|
|
31
|
+
* In short: It's important that notification jobs use this method. It enforces best practices to
|
|
32
|
+
* ensure customers don't receive duplicate or stale notifications.
|
|
33
|
+
*
|
|
34
|
+
* @remarks
|
|
35
|
+
* The following are true:
|
|
36
|
+
* 1. There is a maximum of MAXIMUM_RECIPIENTS_COUNT recipients per trigger request.
|
|
37
|
+
* 2. Our notification provider throws if we use the same idempotency key, but the body changes.
|
|
38
|
+
* 3. We want to be able to query for template variables in jobs so we're getting the most
|
|
39
|
+
* up-to-date values.
|
|
40
|
+
*
|
|
41
|
+
* Taken together, we need to ensure each job only contains one chunk of recipients so it either
|
|
42
|
+
* succeeds or fails and retries on its own. If we moved chunking to the
|
|
43
|
+
* `NotificationClient.trigger` method, for example, the following could happen:
|
|
44
|
+
* 1. A job with >MAXIMUM_RECIPIENTS_COUNT recipients runs.
|
|
45
|
+
* 2. The first chunk succeeds, but the second fails because of a transient issue.
|
|
46
|
+
* 3. The job retries, but a first batch template variable changes in the meantime.
|
|
47
|
+
*
|
|
48
|
+
* Now the job will fail indefinitely and the later batches won't get their notifications.
|
|
49
|
+
*
|
|
50
|
+
* Even if you're sure you won't have more >MAXIMUM_RECIPIENTS_COUNT recipients, the method
|
|
51
|
+
* enforces other best practices, like setting `expiresAt` on enqueue instead of calculating it in
|
|
52
|
+
* your job on each run. Doing this usually means it's always in the future and doesn't help
|
|
53
|
+
* prevent stale notifications.
|
|
54
|
+
*
|
|
55
|
+
* So please, use this method if not for customers, then to save fellow engineers time debugging!
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* <embedex source="packages/notifications/examples/enqueueNotificationJob.ts">
|
|
59
|
+
*
|
|
60
|
+
* ```ts
|
|
61
|
+
* import { IdempotencyKey } from "@clipboard-health/notifications";
|
|
62
|
+
*
|
|
63
|
+
* import { ExampleNotificationJob } from "./exampleNotification.job";
|
|
64
|
+
* import { notificationJobEnqueuer } from "./notificationJobEnqueuer";
|
|
65
|
+
*
|
|
66
|
+
* async function enqueueNotificationJob() {
|
|
67
|
+
* await notificationJobEnqueuer.enqueueOneOrMore(ExampleNotificationJob, {
|
|
68
|
+
* // Set expiresAt at enqueue-time so it remains stable across job retries.
|
|
69
|
+
* expiresAt: minutesFromNow(60),
|
|
70
|
+
* // Set idempotencyKey at enqueue-time so it remains stable across job retries.
|
|
71
|
+
* idempotencyKey: new IdempotencyKey({
|
|
72
|
+
* resourceId: "event-123",
|
|
73
|
+
* }),
|
|
74
|
+
* // Set recipients at enqueue-time so they respect our notification provider's limits.
|
|
75
|
+
* recipients: ["user-1"],
|
|
76
|
+
*
|
|
77
|
+
* workflowKey: "event-starting-reminder",
|
|
78
|
+
*
|
|
79
|
+
* // Any additional enqueue-time data passed to the job:
|
|
80
|
+
* workplaceId: "workplace-123",
|
|
81
|
+
* });
|
|
82
|
+
* }
|
|
83
|
+
*
|
|
84
|
+
* // eslint-disable-next-line unicorn/prefer-top-level-await
|
|
85
|
+
* void enqueueNotificationJob();
|
|
86
|
+
*
|
|
87
|
+
* function minutesFromNow(minutes: number) {
|
|
88
|
+
* return new Date(Date.now() + minutes * 60_000);
|
|
89
|
+
* }
|
|
90
|
+
* ```
|
|
91
|
+
*
|
|
92
|
+
* </embedex>
|
|
93
|
+
*/
|
|
94
|
+
enqueueOneOrMore<TEnqueueData extends NotificationEnqueueData>(handlerClassOrInstance: EnqueueParameters[0], data: TEnqueueData, options?: Omit<EnqueueParameters[2], "idempotencyKey" | "unique">): Promise<void>;
|
|
95
|
+
}
|
|
96
|
+
export {};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.NotificationJobEnqueuer = exports.RETRYABLE_ERRORS = void 0;
|
|
4
|
+
const background_jobs_adapter_1 = require("@clipboard-health/background-jobs-adapter");
|
|
5
|
+
const chunkRecipients_1 = require("./internal/chunkRecipients");
|
|
6
|
+
const idempotencyKeyDoNotImportOutsideNotificationsLibrary_1 = require("./internal/idempotencyKeyDoNotImportOutsideNotificationsLibrary");
|
|
7
|
+
const notificationClient_1 = require("./notificationClient");
|
|
8
|
+
/**
|
|
9
|
+
* Assuming `NotificationClient` is called from a background job with unchanging recipients, only
|
|
10
|
+
* the following are retryable.
|
|
11
|
+
*/
|
|
12
|
+
exports.RETRYABLE_ERRORS = [notificationClient_1.ERROR_CODES.unknown];
|
|
13
|
+
class NotificationJobEnqueuer {
|
|
14
|
+
adapter;
|
|
15
|
+
constructor(params) {
|
|
16
|
+
const { adapter } = params;
|
|
17
|
+
this.adapter = adapter;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* In short: It's important that notification jobs use this method. It enforces best practices to
|
|
21
|
+
* ensure customers don't receive duplicate or stale notifications.
|
|
22
|
+
*
|
|
23
|
+
* @remarks
|
|
24
|
+
* The following are true:
|
|
25
|
+
* 1. There is a maximum of MAXIMUM_RECIPIENTS_COUNT recipients per trigger request.
|
|
26
|
+
* 2. Our notification provider throws if we use the same idempotency key, but the body changes.
|
|
27
|
+
* 3. We want to be able to query for template variables in jobs so we're getting the most
|
|
28
|
+
* up-to-date values.
|
|
29
|
+
*
|
|
30
|
+
* Taken together, we need to ensure each job only contains one chunk of recipients so it either
|
|
31
|
+
* succeeds or fails and retries on its own. If we moved chunking to the
|
|
32
|
+
* `NotificationClient.trigger` method, for example, the following could happen:
|
|
33
|
+
* 1. A job with >MAXIMUM_RECIPIENTS_COUNT recipients runs.
|
|
34
|
+
* 2. The first chunk succeeds, but the second fails because of a transient issue.
|
|
35
|
+
* 3. The job retries, but a first batch template variable changes in the meantime.
|
|
36
|
+
*
|
|
37
|
+
* Now the job will fail indefinitely and the later batches won't get their notifications.
|
|
38
|
+
*
|
|
39
|
+
* Even if you're sure you won't have more >MAXIMUM_RECIPIENTS_COUNT recipients, the method
|
|
40
|
+
* enforces other best practices, like setting `expiresAt` on enqueue instead of calculating it in
|
|
41
|
+
* your job on each run. Doing this usually means it's always in the future and doesn't help
|
|
42
|
+
* prevent stale notifications.
|
|
43
|
+
*
|
|
44
|
+
* So please, use this method if not for customers, then to save fellow engineers time debugging!
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* <embedex source="packages/notifications/examples/enqueueNotificationJob.ts">
|
|
48
|
+
*
|
|
49
|
+
* ```ts
|
|
50
|
+
* import { IdempotencyKey } from "@clipboard-health/notifications";
|
|
51
|
+
*
|
|
52
|
+
* import { ExampleNotificationJob } from "./exampleNotification.job";
|
|
53
|
+
* import { notificationJobEnqueuer } from "./notificationJobEnqueuer";
|
|
54
|
+
*
|
|
55
|
+
* async function enqueueNotificationJob() {
|
|
56
|
+
* await notificationJobEnqueuer.enqueueOneOrMore(ExampleNotificationJob, {
|
|
57
|
+
* // Set expiresAt at enqueue-time so it remains stable across job retries.
|
|
58
|
+
* expiresAt: minutesFromNow(60),
|
|
59
|
+
* // Set idempotencyKey at enqueue-time so it remains stable across job retries.
|
|
60
|
+
* idempotencyKey: new IdempotencyKey({
|
|
61
|
+
* resourceId: "event-123",
|
|
62
|
+
* }),
|
|
63
|
+
* // Set recipients at enqueue-time so they respect our notification provider's limits.
|
|
64
|
+
* recipients: ["user-1"],
|
|
65
|
+
*
|
|
66
|
+
* workflowKey: "event-starting-reminder",
|
|
67
|
+
*
|
|
68
|
+
* // Any additional enqueue-time data passed to the job:
|
|
69
|
+
* workplaceId: "workplace-123",
|
|
70
|
+
* });
|
|
71
|
+
* }
|
|
72
|
+
*
|
|
73
|
+
* // eslint-disable-next-line unicorn/prefer-top-level-await
|
|
74
|
+
* void enqueueNotificationJob();
|
|
75
|
+
*
|
|
76
|
+
* function minutesFromNow(minutes: number) {
|
|
77
|
+
* return new Date(Date.now() + minutes * 60_000);
|
|
78
|
+
* }
|
|
79
|
+
* ```
|
|
80
|
+
*
|
|
81
|
+
* </embedex>
|
|
82
|
+
*/
|
|
83
|
+
async enqueueOneOrMore(handlerClassOrInstance, data,
|
|
84
|
+
// The job's idempotency/unique key is set automatically.
|
|
85
|
+
options) {
|
|
86
|
+
await Promise.all((0, chunkRecipients_1.chunkRecipients)({ recipients: data.recipients }).map(async ({ number, recipients }) => {
|
|
87
|
+
const idempotencyKey = new idempotencyKeyDoNotImportOutsideNotificationsLibrary_1.IdempotencyKeyDoNotImportOutsideNotificationsLibrary({
|
|
88
|
+
...data.idempotencyKey,
|
|
89
|
+
chunk: number,
|
|
90
|
+
recipients,
|
|
91
|
+
workflowKey: data.workflowKey,
|
|
92
|
+
});
|
|
93
|
+
await this.adapter.enqueue(handlerClassOrInstance, { ...data, recipients, idempotencyKey }, {
|
|
94
|
+
...(options ? { ...options } : {}),
|
|
95
|
+
[background_jobs_adapter_1.ENQUEUE_FIELD_NAMES[this.adapter.implementation].idempotencyKey]: idempotencyKey.toHash({}),
|
|
96
|
+
});
|
|
97
|
+
}));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
exports.NotificationJobEnqueuer = NotificationJobEnqueuer;
|
|
101
|
+
//# sourceMappingURL=notificationJobEnqueuer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notificationJobEnqueuer.js","sourceRoot":"","sources":["../../../../../packages/notifications/src/lib/notificationJobEnqueuer.ts"],"names":[],"mappings":";;;AAAA,uFAGmD;AAGnD,gEAA6D;AAC7D,0IAAuI;AACvI,6DAAmE;AAOnE;;;GAGG;AACU,QAAA,gBAAgB,GAAgB,CAAC,gCAAW,CAAC,OAAO,CAAC,CAAC;AAyBnE,MAAa,uBAAuB;IACjB,OAAO,CAA2C;IAEnE,YAAY,MAAqC;QAC/C,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,CAAC;QAE3B,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACzB,CAAC;IAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA+DG;IACH,KAAK,CAAC,gBAAgB,CACpB,sBAA4C,EAC5C,IAAkB;IAClB,yDAAyD;IACzD,OAAiE;QAEjE,MAAM,OAAO,CAAC,GAAG,CACf,IAAA,iCAAe,EAAC,EAAE,UAAU,EAAE,IAAI,CAAC,UAAU,EAAE,CAAC,CAAC,GAAG,CAAC,KAAK,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,EAAE,EAAE;YACpF,MAAM,cAAc,GAAG,IAAI,2GAAoD,CAAC;gBAC9E,GAAG,IAAI,CAAC,cAAc;gBACtB,KAAK,EAAE,MAAM;gBACb,UAAU;gBACV,WAAW,EAAE,IAAI,CAAC,WAAW;aAC9B,CAAC,CAAC;YAEH,MAAM,IAAI,CAAC,OAAO,CAAC,OAAO,CACxB,sBAAsB,EACtB,EAAE,GAAG,IAAI,EAAE,UAAU,EAAE,cAAc,EAAE,EACvC;gBACE,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,GAAG,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBAClC,CAAC,6CAAmB,CAAC,IAAI,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC,cAAc,CAAC,EAC/D,cAAc,CAAC,MAAM,CAAC,EAAE,CAAC;aAC5B,CACF,CAAC;QACJ,CAAC,CAAC,CACH,CAAC;IACJ,CAAC;CACF;AApGD,0DAoGC"}
|
package/src/lib/types.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type Logger } from "@clipboard-health/util-ts";
|
|
2
|
+
import { type IdempotencyKeyDoNotImportOutsideNotificationsLibrary } from "./internal/idempotencyKeyDoNotImportOutsideNotificationsLibrary";
|
|
2
3
|
import { type IdempotentKnock } from "./internal/idempotentKnock";
|
|
3
4
|
export type Tags = Record<string, unknown>;
|
|
4
5
|
export interface TraceOptions {
|
|
@@ -123,37 +124,33 @@ export interface TriggerBody {
|
|
|
123
124
|
* Request parameters for triggering a notification.
|
|
124
125
|
*/
|
|
125
126
|
export interface TriggerRequest {
|
|
126
|
-
/**
|
|
127
|
+
/** Workflow key.
|
|
128
|
+
*
|
|
129
|
+
* @deprecated Use `workflowKey` instead.
|
|
130
|
+
*/
|
|
127
131
|
key: string;
|
|
128
|
-
/**
|
|
132
|
+
/** Workflow key. */
|
|
133
|
+
workflowKey?: string;
|
|
134
|
+
/** Trigger payload. */
|
|
129
135
|
body: TriggerBody;
|
|
130
136
|
/**
|
|
131
|
-
*
|
|
132
|
-
* ({@link createIdempotencyKey}) and remains the same across retry logic.
|
|
133
|
-
*
|
|
134
|
-
* If you retry a request with the same idempotency key within 24 hours from the original request,
|
|
135
|
-
* we will return the same response as the original request. Idempotent requests are expected to
|
|
136
|
-
* be identical. To prevent accidental misuse, the client throws an error when incoming parameters
|
|
137
|
-
* don't match those from the original request.
|
|
138
|
-
*
|
|
139
|
-
* Ensure your idempotency key doesn't prevent recipients from receiving notifications. For
|
|
140
|
-
* example, if you use the workflow key and the recipient's ID as the idempotency key, but it's
|
|
141
|
-
* possible the recipient could receive the notification multiple times within the idempotency
|
|
142
|
-
* key's validity window, the recipient will only receive the first notification.
|
|
137
|
+
* @see {@link IdempotencyKeyDoNotImportOutsideNotificationsLibrary}
|
|
143
138
|
*/
|
|
144
|
-
idempotencyKey: string;
|
|
139
|
+
idempotencyKey: string | IdempotencyKeyDoNotImportOutsideNotificationsLibrary;
|
|
145
140
|
/** Array of data keys to redact in logs for privacy. */
|
|
146
141
|
keysToRedact?: string[];
|
|
147
142
|
/**
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
143
|
+
* `expiresAt` prevents stale notifications across retries by dropping the request when `now >
|
|
144
|
+
* expiresAt`. If, for example, you're notifying about an event that starts in one hour, you might
|
|
145
|
+
* set this to one hour from now.
|
|
146
|
+
*
|
|
147
|
+
* If you are not retrying or your notification is never stale, set it to `false`.
|
|
151
148
|
*
|
|
152
|
-
* If you're triggering from a background job, don't set this at the call site
|
|
149
|
+
* If you're triggering from a background job, don't set this at the call site, set it when you
|
|
153
150
|
* enqueue the job. Otherwise, it gets updated each time the job retries, will always be in the
|
|
154
151
|
* future, and won't prevent stale notifications.
|
|
155
152
|
*/
|
|
156
|
-
expiresAt: Date;
|
|
153
|
+
expiresAt: Date | false;
|
|
157
154
|
/** Attempt number for tracing. */
|
|
158
155
|
attempt: number;
|
|
159
156
|
}
|
package/src/lib/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../../../packages/notifications/src/lib/types.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../../../packages/notifications/src/lib/types.ts"],"names":[],"mappings":";;;AA4Ca,QAAA,gBAAgB,GAAG,CAAC,SAAS,EAAE,KAAK,CAAU,CAAC"}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.chunkRecipients = chunkRecipients;
|
|
4
|
-
const util_ts_1 = require("@clipboard-health/util-ts");
|
|
5
|
-
const notificationClient_1 = require("./notificationClient");
|
|
6
|
-
function chunkRecipients(params) {
|
|
7
|
-
const { recipientIds, idempotencyKey } = params;
|
|
8
|
-
if (recipientIds.length === 0) {
|
|
9
|
-
return [{ idempotencyKey, recipientIds: [] }];
|
|
10
|
-
}
|
|
11
|
-
const idChunks = (0, util_ts_1.chunk)(recipientIds, notificationClient_1.MAXIMUM_RECIPIENTS_COUNT);
|
|
12
|
-
const singleChunk = idChunks.length === 1;
|
|
13
|
-
return idChunks.map((ids, index) => ({
|
|
14
|
-
idempotencyKey: singleChunk ? idempotencyKey : `${idempotencyKey}-chunk-${index + 1}`,
|
|
15
|
-
recipientIds: ids,
|
|
16
|
-
}));
|
|
17
|
-
}
|
|
18
|
-
//# sourceMappingURL=chunkRecipients.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"chunkRecipients.js","sourceRoot":"","sources":["../../../../../packages/notifications/src/lib/chunkRecipients.ts"],"names":[],"mappings":";;AAIA,0CAgBC;AApBD,uDAAkD;AAElD,6DAAgE;AAEhE,SAAgB,eAAe,CAAC,MAG/B;IACC,MAAM,EAAE,YAAY,EAAE,cAAc,EAAE,GAAG,MAAM,CAAC;IAEhD,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC9B,OAAO,CAAC,EAAE,cAAc,EAAE,YAAY,EAAE,EAAE,EAAE,CAAC,CAAC;IAChD,CAAC;IAED,MAAM,QAAQ,GAAG,IAAA,eAAK,EAAC,YAAY,EAAE,6CAAwB,CAAC,CAAC;IAC/D,MAAM,WAAW,GAAG,QAAQ,CAAC,MAAM,KAAK,CAAC,CAAC;IAC1C,OAAO,QAAQ,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC;QACnC,cAAc,EAAE,WAAW,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,GAAG,cAAc,UAAU,KAAK,GAAG,CAAC,EAAE;QACrF,YAAY,EAAE,GAAG;KAClB,CAAC,CAAC,CAAC;AACN,CAAC"}
|