@clipboard-health/ai-rules 1.5.21 → 1.6.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/backend/AGENTS.md +117 -118
- package/fullstack/AGENTS.md +117 -118
- package/package.json +1 -1
- package/scripts/sync.js +1 -1
package/backend/AGENTS.md
CHANGED
|
@@ -23,173 +23,172 @@ Send notifications through [Knock](https://docs.knock.app) using the `@clipboard
|
|
|
23
23
|
|
|
24
24
|
<embedex source="packages/notifications/examples/usage.md">
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
## `triggerChunked`
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
import { NotificationJobEnqueuer } from "@clipboard-health/notifications";
|
|
30
|
-
|
|
31
|
-
import { BackgroundJobsService } from "./setup";
|
|
32
|
-
|
|
33
|
-
// Create and export one instance of this in your microservice.
|
|
34
|
-
export const notificationJobEnqueuer = new NotificationJobEnqueuer({
|
|
35
|
-
// Use your instance of `@clipboard-health/mongo-jobs` or `@clipboard-health/background-jobs-postgres` here.
|
|
36
|
-
adapter: new BackgroundJobsService(),
|
|
37
|
-
});
|
|
38
|
-
```
|
|
28
|
+
`triggerChunked` stores the full, immutable trigger request at job enqueue time, eliminating issues with stale data, chunking requests to stay under provider limits, and idempotency key conflicts that can occur if the request is updated at job execution time.
|
|
39
29
|
|
|
40
|
-
|
|
30
|
+
1. Search your service for `triggerNotification.constants.ts`, `triggerNotification.job.ts` and `notifications.service.ts`. If they don't exist, create them:
|
|
41
31
|
|
|
42
32
|
```ts
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
type ExampleNotificationData = NotificationData<{
|
|
46
|
-
workplaceId: string;
|
|
47
|
-
}>;
|
|
48
|
-
|
|
49
|
-
export type ExampleNotificationDataJob = ExampleNotificationData["Job"];
|
|
50
|
-
export type ExampleNotificationDataEnqueue = ExampleNotificationData["Enqueue"];
|
|
51
|
-
|
|
52
|
-
export type ExampleNotificationDo = ExampleNotificationDataJob & { attempt: number };
|
|
53
|
-
|
|
54
|
-
export const EXAMPLE_NOTIFICATION_JOB_NAME = "ExampleNotificationJob";
|
|
33
|
+
// triggerNotification.constants.ts
|
|
34
|
+
export const TRIGGER_NOTIFICATION_JOB_NAME = "TriggerNotificationJob";
|
|
55
35
|
```
|
|
56
36
|
|
|
57
|
-
3. Implement a minimal job in the module's logic/job directory if it exists, else module root. The job calls off to a NestJS service for any business logic and to send the notification:
|
|
58
|
-
|
|
59
37
|
```ts
|
|
38
|
+
// triggerNotification.job.ts
|
|
60
39
|
import { type BaseHandler } from "@clipboard-health/background-jobs-adapter";
|
|
40
|
+
import {
|
|
41
|
+
type SerializableTriggerChunkedRequest,
|
|
42
|
+
toTriggerChunkedRequest,
|
|
43
|
+
} from "@clipboard-health/notifications";
|
|
61
44
|
import { isFailure } from "@clipboard-health/util-ts";
|
|
62
45
|
|
|
63
|
-
import {
|
|
64
|
-
EXAMPLE_NOTIFICATION_JOB_NAME,
|
|
65
|
-
type ExampleNotificationDataJob,
|
|
66
|
-
} from "./exampleNotification.constants";
|
|
67
|
-
import { type ExampleNotificationService } from "./exampleNotification.service";
|
|
46
|
+
import { type NotificationsService } from "./notifications.service";
|
|
68
47
|
import { CBHLogger } from "./setup";
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
48
|
+
import { TRIGGER_NOTIFICATION_JOB_NAME } from "./triggerNotification.constants";
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* For mongo-jobs, implement HandlerInterface<SerializableTriggerChunkedRequest>.
|
|
52
|
+
* For background-jobs-postgres, implement Handler<SerializableTriggerChunkedRequest>.
|
|
53
|
+
*/
|
|
54
|
+
export class TriggerNotificationJob implements BaseHandler<SerializableTriggerChunkedRequest> {
|
|
55
|
+
public name = TRIGGER_NOTIFICATION_JOB_NAME;
|
|
74
56
|
private readonly logger = new CBHLogger({
|
|
75
57
|
defaultMeta: {
|
|
76
|
-
logContext:
|
|
58
|
+
logContext: TRIGGER_NOTIFICATION_JOB_NAME,
|
|
77
59
|
},
|
|
78
60
|
});
|
|
79
61
|
|
|
80
|
-
constructor(private readonly service:
|
|
81
|
-
|
|
82
|
-
async perform(
|
|
83
|
-
|
|
62
|
+
public constructor(private readonly service: NotificationsService) {}
|
|
63
|
+
|
|
64
|
+
public async perform(
|
|
65
|
+
data: SerializableTriggerChunkedRequest,
|
|
66
|
+
job: { _id: string; attemptsCount: number; uniqueKey?: string },
|
|
67
|
+
) {
|
|
68
|
+
const metadata = {
|
|
69
|
+
// Include the job's attempts count for debugging, this is called `retryAttempts` in `background-jobs-postgres`.
|
|
70
|
+
attempt: job.attemptsCount + 1,
|
|
71
|
+
jobId: job._id,
|
|
72
|
+
recipientCount: data.body.recipients.length,
|
|
84
73
|
workflowKey: data.workflowKey,
|
|
85
|
-
}
|
|
74
|
+
};
|
|
75
|
+
this.logger.info("Processing", metadata);
|
|
86
76
|
|
|
87
77
|
try {
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
attempt: job.attemptsCount + 1,
|
|
78
|
+
const request = toTriggerChunkedRequest(data, {
|
|
79
|
+
attempt: metadata.attempt,
|
|
80
|
+
idempotencyKey: job.uniqueKey ?? metadata.jobId,
|
|
92
81
|
});
|
|
82
|
+
const result = await this.service.triggerChunked(request);
|
|
93
83
|
|
|
94
84
|
if (isFailure(result)) {
|
|
95
85
|
throw result.error;
|
|
96
86
|
}
|
|
97
87
|
|
|
98
|
-
this.logger.info("Success", {
|
|
99
|
-
workflowKey: data.workflowKey,
|
|
100
|
-
});
|
|
88
|
+
this.logger.info("Success", { ...metadata, response: result.value });
|
|
101
89
|
} catch (error) {
|
|
102
|
-
this.logger.error("Failure", {
|
|
90
|
+
this.logger.error("Failure", { ...metadata, error });
|
|
103
91
|
throw error;
|
|
104
92
|
}
|
|
105
93
|
}
|
|
106
94
|
}
|
|
107
95
|
```
|
|
108
96
|
|
|
109
|
-
4. Search the service for a constant that stores workflow keys. If there isn't one, create and export it. You MUST insert the key in alphabetical order:
|
|
110
|
-
|
|
111
97
|
```ts
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
} as const;
|
|
115
|
-
```
|
|
98
|
+
// notifications.service.ts
|
|
99
|
+
import { NotificationClient } from "@clipboard-health/notifications";
|
|
116
100
|
|
|
117
|
-
|
|
101
|
+
import { CBHLogger, toLogger, tracer } from "./setup";
|
|
118
102
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
EXAMPLE_NOTIFICATION_JOB_NAME,
|
|
122
|
-
type ExampleNotificationDataEnqueue,
|
|
123
|
-
} from "./exampleNotification.constants";
|
|
124
|
-
import { notificationJobEnqueuer } from "./notificationJobEnqueuer";
|
|
125
|
-
import { WORKFLOW_KEYS } from "./workflowKeys";
|
|
103
|
+
export class NotificationsService {
|
|
104
|
+
private readonly client: NotificationClient;
|
|
126
105
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
* service instead of this manual calculation.
|
|
135
|
-
*/
|
|
136
|
-
expiresAt: new Date(Date.now() + 60 * 60_000).toISOString(),
|
|
137
|
-
// Set idempotencyKeyParts at enqueue-time so it remains stable across job retries.
|
|
138
|
-
idempotencyKeyParts: {
|
|
139
|
-
resource: {
|
|
140
|
-
type: "account",
|
|
141
|
-
id: "4e3ffeec-1426-4e54-ad28-83246f8f4e7c",
|
|
142
|
-
},
|
|
143
|
-
},
|
|
144
|
-
// Set recipients at enqueue-time so they respect our notification provider's limits.
|
|
145
|
-
recipients: ["userId-1"],
|
|
146
|
-
|
|
147
|
-
workflowKey: WORKFLOW_KEYS.eventStartingReminder,
|
|
148
|
-
|
|
149
|
-
// Any additional enqueue-time data passed to the job:
|
|
150
|
-
workplaceId: "workplaceId-123",
|
|
151
|
-
},
|
|
152
|
-
);
|
|
153
|
-
}
|
|
106
|
+
constructor() {
|
|
107
|
+
this.client = new NotificationClient({
|
|
108
|
+
apiKey: "YOUR_KNOCK_API_KEY",
|
|
109
|
+
logger: toLogger(new CBHLogger()),
|
|
110
|
+
tracer,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
154
113
|
|
|
155
|
-
|
|
156
|
-
|
|
114
|
+
async triggerChunked(
|
|
115
|
+
params: Parameters<NotificationClient["triggerChunked"]>[0],
|
|
116
|
+
): ReturnType<NotificationClient["triggerChunked"]> {
|
|
117
|
+
return await this.client.triggerChunked(params);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
157
120
|
```
|
|
158
121
|
|
|
159
|
-
|
|
122
|
+
2. Search the service for a constant that stores workflow keys. If there isn't one, create it:
|
|
160
123
|
|
|
161
124
|
```ts
|
|
162
|
-
|
|
125
|
+
/* eslint sort-keys: "error" */
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Alphabetical list of workflow keys.
|
|
129
|
+
*/
|
|
130
|
+
export const WORKFLOW_KEYS = {
|
|
131
|
+
eventStartingReminder: "event-starting-reminder",
|
|
132
|
+
} as const;
|
|
133
|
+
```
|
|
163
134
|
|
|
164
|
-
|
|
135
|
+
3. Build your `SerializableTriggerChunkedRequest` and enqueue your job:
|
|
165
136
|
|
|
166
|
-
|
|
167
|
-
|
|
137
|
+
```ts
|
|
138
|
+
import { type BackgroundJobsAdapter } from "@clipboard-health/background-jobs-adapter";
|
|
139
|
+
import { type SerializableTriggerChunkedRequest } from "@clipboard-health/notifications";
|
|
168
140
|
|
|
169
|
-
|
|
170
|
-
|
|
141
|
+
import { BackgroundJobsService } from "./setup";
|
|
142
|
+
import { TRIGGER_NOTIFICATION_JOB_NAME } from "./triggerNotification.constants";
|
|
143
|
+
import { WORKFLOW_KEYS } from "./workflowKeys";
|
|
171
144
|
|
|
172
|
-
|
|
145
|
+
/**
|
|
146
|
+
* Enqueue a notification job in the same database transaction as the changes it's notifying about.
|
|
147
|
+
* The `session` option is called `transaction` in `background-jobs-postgres`.
|
|
148
|
+
*/
|
|
149
|
+
async function enqueueTriggerNotificationJob(adapter: BackgroundJobsAdapter) {
|
|
150
|
+
// Assume this comes from a database and are used as template variables...
|
|
151
|
+
const notificationData = {
|
|
152
|
+
favoriteColor: "blue",
|
|
173
153
|
// Use @clipboard-health/date-time's formatShortDateTime in your service for consistency.
|
|
174
|
-
|
|
154
|
+
favoriteAt: new Date().toISOString(),
|
|
155
|
+
secret: "2",
|
|
156
|
+
};
|
|
175
157
|
|
|
158
|
+
const jobData: SerializableTriggerChunkedRequest = {
|
|
176
159
|
// Important: Read the TypeDoc documentation for additional context.
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
160
|
+
body: {
|
|
161
|
+
recipients: ["userId-1", "userId-2"],
|
|
162
|
+
data: notificationData,
|
|
163
|
+
},
|
|
164
|
+
// Helpful when controlling notifications with feature flags.
|
|
165
|
+
dryRun: false,
|
|
166
|
+
// Set expiresAt at enqueue-time so it remains stable across job retries. Use date-fns in your
|
|
167
|
+
// service instead of this manual calculation.
|
|
168
|
+
expiresAt: new Date(Date.now() + 60 * 60_000).toISOString(),
|
|
169
|
+
// Keys to redact from logs
|
|
170
|
+
keysToRedact: ["secret"],
|
|
171
|
+
workflowKey: WORKFLOW_KEYS.eventStartingReminder,
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
// Option 1 (default): Automatically use background job ID as idempotency key.
|
|
175
|
+
await adapter.enqueue(TRIGGER_NOTIFICATION_JOB_NAME, jobData, { session: "..." });
|
|
176
|
+
|
|
177
|
+
// Option 2 (advanced): Provide custom idempotency key to job and notification libraries for more
|
|
178
|
+
// control. You'd use this to provide enqueue-time deduplication. For example, if you enqueue when
|
|
179
|
+
// a user clicks a button and only want them to receive one notification.
|
|
180
|
+
await adapter.enqueue(TRIGGER_NOTIFICATION_JOB_NAME, jobData, {
|
|
181
|
+
// Called `idempotencyKey` in `background-jobs-postgres`.
|
|
182
|
+
unique: `meeting-123-reminder`,
|
|
183
|
+
session: "...",
|
|
184
|
+
});
|
|
192
185
|
}
|
|
186
|
+
|
|
187
|
+
// eslint-disable-next-line unicorn/prefer-top-level-await
|
|
188
|
+
void enqueueTriggerNotificationJob(
|
|
189
|
+
// Use your instance of `@clipboard-health/mongo-jobs` or `@clipboard-health/background-jobs-postgres` here.
|
|
190
|
+
new BackgroundJobsService(),
|
|
191
|
+
);
|
|
193
192
|
```
|
|
194
193
|
|
|
195
194
|
</embedex>
|
package/fullstack/AGENTS.md
CHANGED
|
@@ -23,173 +23,172 @@ Send notifications through [Knock](https://docs.knock.app) using the `@clipboard
|
|
|
23
23
|
|
|
24
24
|
<embedex source="packages/notifications/examples/usage.md">
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
## `triggerChunked`
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
import { NotificationJobEnqueuer } from "@clipboard-health/notifications";
|
|
30
|
-
|
|
31
|
-
import { BackgroundJobsService } from "./setup";
|
|
32
|
-
|
|
33
|
-
// Create and export one instance of this in your microservice.
|
|
34
|
-
export const notificationJobEnqueuer = new NotificationJobEnqueuer({
|
|
35
|
-
// Use your instance of `@clipboard-health/mongo-jobs` or `@clipboard-health/background-jobs-postgres` here.
|
|
36
|
-
adapter: new BackgroundJobsService(),
|
|
37
|
-
});
|
|
38
|
-
```
|
|
28
|
+
`triggerChunked` stores the full, immutable trigger request at job enqueue time, eliminating issues with stale data, chunking requests to stay under provider limits, and idempotency key conflicts that can occur if the request is updated at job execution time.
|
|
39
29
|
|
|
40
|
-
|
|
30
|
+
1. Search your service for `triggerNotification.constants.ts`, `triggerNotification.job.ts` and `notifications.service.ts`. If they don't exist, create them:
|
|
41
31
|
|
|
42
32
|
```ts
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
type ExampleNotificationData = NotificationData<{
|
|
46
|
-
workplaceId: string;
|
|
47
|
-
}>;
|
|
48
|
-
|
|
49
|
-
export type ExampleNotificationDataJob = ExampleNotificationData["Job"];
|
|
50
|
-
export type ExampleNotificationDataEnqueue = ExampleNotificationData["Enqueue"];
|
|
51
|
-
|
|
52
|
-
export type ExampleNotificationDo = ExampleNotificationDataJob & { attempt: number };
|
|
53
|
-
|
|
54
|
-
export const EXAMPLE_NOTIFICATION_JOB_NAME = "ExampleNotificationJob";
|
|
33
|
+
// triggerNotification.constants.ts
|
|
34
|
+
export const TRIGGER_NOTIFICATION_JOB_NAME = "TriggerNotificationJob";
|
|
55
35
|
```
|
|
56
36
|
|
|
57
|
-
3. Implement a minimal job in the module's logic/job directory if it exists, else module root. The job calls off to a NestJS service for any business logic and to send the notification:
|
|
58
|
-
|
|
59
37
|
```ts
|
|
38
|
+
// triggerNotification.job.ts
|
|
60
39
|
import { type BaseHandler } from "@clipboard-health/background-jobs-adapter";
|
|
40
|
+
import {
|
|
41
|
+
type SerializableTriggerChunkedRequest,
|
|
42
|
+
toTriggerChunkedRequest,
|
|
43
|
+
} from "@clipboard-health/notifications";
|
|
61
44
|
import { isFailure } from "@clipboard-health/util-ts";
|
|
62
45
|
|
|
63
|
-
import {
|
|
64
|
-
EXAMPLE_NOTIFICATION_JOB_NAME,
|
|
65
|
-
type ExampleNotificationDataJob,
|
|
66
|
-
} from "./exampleNotification.constants";
|
|
67
|
-
import { type ExampleNotificationService } from "./exampleNotification.service";
|
|
46
|
+
import { type NotificationsService } from "./notifications.service";
|
|
68
47
|
import { CBHLogger } from "./setup";
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
48
|
+
import { TRIGGER_NOTIFICATION_JOB_NAME } from "./triggerNotification.constants";
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* For mongo-jobs, implement HandlerInterface<SerializableTriggerChunkedRequest>.
|
|
52
|
+
* For background-jobs-postgres, implement Handler<SerializableTriggerChunkedRequest>.
|
|
53
|
+
*/
|
|
54
|
+
export class TriggerNotificationJob implements BaseHandler<SerializableTriggerChunkedRequest> {
|
|
55
|
+
public name = TRIGGER_NOTIFICATION_JOB_NAME;
|
|
74
56
|
private readonly logger = new CBHLogger({
|
|
75
57
|
defaultMeta: {
|
|
76
|
-
logContext:
|
|
58
|
+
logContext: TRIGGER_NOTIFICATION_JOB_NAME,
|
|
77
59
|
},
|
|
78
60
|
});
|
|
79
61
|
|
|
80
|
-
constructor(private readonly service:
|
|
81
|
-
|
|
82
|
-
async perform(
|
|
83
|
-
|
|
62
|
+
public constructor(private readonly service: NotificationsService) {}
|
|
63
|
+
|
|
64
|
+
public async perform(
|
|
65
|
+
data: SerializableTriggerChunkedRequest,
|
|
66
|
+
job: { _id: string; attemptsCount: number; uniqueKey?: string },
|
|
67
|
+
) {
|
|
68
|
+
const metadata = {
|
|
69
|
+
// Include the job's attempts count for debugging, this is called `retryAttempts` in `background-jobs-postgres`.
|
|
70
|
+
attempt: job.attemptsCount + 1,
|
|
71
|
+
jobId: job._id,
|
|
72
|
+
recipientCount: data.body.recipients.length,
|
|
84
73
|
workflowKey: data.workflowKey,
|
|
85
|
-
}
|
|
74
|
+
};
|
|
75
|
+
this.logger.info("Processing", metadata);
|
|
86
76
|
|
|
87
77
|
try {
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
attempt: job.attemptsCount + 1,
|
|
78
|
+
const request = toTriggerChunkedRequest(data, {
|
|
79
|
+
attempt: metadata.attempt,
|
|
80
|
+
idempotencyKey: job.uniqueKey ?? metadata.jobId,
|
|
92
81
|
});
|
|
82
|
+
const result = await this.service.triggerChunked(request);
|
|
93
83
|
|
|
94
84
|
if (isFailure(result)) {
|
|
95
85
|
throw result.error;
|
|
96
86
|
}
|
|
97
87
|
|
|
98
|
-
this.logger.info("Success", {
|
|
99
|
-
workflowKey: data.workflowKey,
|
|
100
|
-
});
|
|
88
|
+
this.logger.info("Success", { ...metadata, response: result.value });
|
|
101
89
|
} catch (error) {
|
|
102
|
-
this.logger.error("Failure", {
|
|
90
|
+
this.logger.error("Failure", { ...metadata, error });
|
|
103
91
|
throw error;
|
|
104
92
|
}
|
|
105
93
|
}
|
|
106
94
|
}
|
|
107
95
|
```
|
|
108
96
|
|
|
109
|
-
4. Search the service for a constant that stores workflow keys. If there isn't one, create and export it. You MUST insert the key in alphabetical order:
|
|
110
|
-
|
|
111
97
|
```ts
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
} as const;
|
|
115
|
-
```
|
|
98
|
+
// notifications.service.ts
|
|
99
|
+
import { NotificationClient } from "@clipboard-health/notifications";
|
|
116
100
|
|
|
117
|
-
|
|
101
|
+
import { CBHLogger, toLogger, tracer } from "./setup";
|
|
118
102
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
EXAMPLE_NOTIFICATION_JOB_NAME,
|
|
122
|
-
type ExampleNotificationDataEnqueue,
|
|
123
|
-
} from "./exampleNotification.constants";
|
|
124
|
-
import { notificationJobEnqueuer } from "./notificationJobEnqueuer";
|
|
125
|
-
import { WORKFLOW_KEYS } from "./workflowKeys";
|
|
103
|
+
export class NotificationsService {
|
|
104
|
+
private readonly client: NotificationClient;
|
|
126
105
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
* service instead of this manual calculation.
|
|
135
|
-
*/
|
|
136
|
-
expiresAt: new Date(Date.now() + 60 * 60_000).toISOString(),
|
|
137
|
-
// Set idempotencyKeyParts at enqueue-time so it remains stable across job retries.
|
|
138
|
-
idempotencyKeyParts: {
|
|
139
|
-
resource: {
|
|
140
|
-
type: "account",
|
|
141
|
-
id: "4e3ffeec-1426-4e54-ad28-83246f8f4e7c",
|
|
142
|
-
},
|
|
143
|
-
},
|
|
144
|
-
// Set recipients at enqueue-time so they respect our notification provider's limits.
|
|
145
|
-
recipients: ["userId-1"],
|
|
146
|
-
|
|
147
|
-
workflowKey: WORKFLOW_KEYS.eventStartingReminder,
|
|
148
|
-
|
|
149
|
-
// Any additional enqueue-time data passed to the job:
|
|
150
|
-
workplaceId: "workplaceId-123",
|
|
151
|
-
},
|
|
152
|
-
);
|
|
153
|
-
}
|
|
106
|
+
constructor() {
|
|
107
|
+
this.client = new NotificationClient({
|
|
108
|
+
apiKey: "YOUR_KNOCK_API_KEY",
|
|
109
|
+
logger: toLogger(new CBHLogger()),
|
|
110
|
+
tracer,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
154
113
|
|
|
155
|
-
|
|
156
|
-
|
|
114
|
+
async triggerChunked(
|
|
115
|
+
params: Parameters<NotificationClient["triggerChunked"]>[0],
|
|
116
|
+
): ReturnType<NotificationClient["triggerChunked"]> {
|
|
117
|
+
return await this.client.triggerChunked(params);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
157
120
|
```
|
|
158
121
|
|
|
159
|
-
|
|
122
|
+
2. Search the service for a constant that stores workflow keys. If there isn't one, create it:
|
|
160
123
|
|
|
161
124
|
```ts
|
|
162
|
-
|
|
125
|
+
/* eslint sort-keys: "error" */
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Alphabetical list of workflow keys.
|
|
129
|
+
*/
|
|
130
|
+
export const WORKFLOW_KEYS = {
|
|
131
|
+
eventStartingReminder: "event-starting-reminder",
|
|
132
|
+
} as const;
|
|
133
|
+
```
|
|
163
134
|
|
|
164
|
-
|
|
135
|
+
3. Build your `SerializableTriggerChunkedRequest` and enqueue your job:
|
|
165
136
|
|
|
166
|
-
|
|
167
|
-
|
|
137
|
+
```ts
|
|
138
|
+
import { type BackgroundJobsAdapter } from "@clipboard-health/background-jobs-adapter";
|
|
139
|
+
import { type SerializableTriggerChunkedRequest } from "@clipboard-health/notifications";
|
|
168
140
|
|
|
169
|
-
|
|
170
|
-
|
|
141
|
+
import { BackgroundJobsService } from "./setup";
|
|
142
|
+
import { TRIGGER_NOTIFICATION_JOB_NAME } from "./triggerNotification.constants";
|
|
143
|
+
import { WORKFLOW_KEYS } from "./workflowKeys";
|
|
171
144
|
|
|
172
|
-
|
|
145
|
+
/**
|
|
146
|
+
* Enqueue a notification job in the same database transaction as the changes it's notifying about.
|
|
147
|
+
* The `session` option is called `transaction` in `background-jobs-postgres`.
|
|
148
|
+
*/
|
|
149
|
+
async function enqueueTriggerNotificationJob(adapter: BackgroundJobsAdapter) {
|
|
150
|
+
// Assume this comes from a database and are used as template variables...
|
|
151
|
+
const notificationData = {
|
|
152
|
+
favoriteColor: "blue",
|
|
173
153
|
// Use @clipboard-health/date-time's formatShortDateTime in your service for consistency.
|
|
174
|
-
|
|
154
|
+
favoriteAt: new Date().toISOString(),
|
|
155
|
+
secret: "2",
|
|
156
|
+
};
|
|
175
157
|
|
|
158
|
+
const jobData: SerializableTriggerChunkedRequest = {
|
|
176
159
|
// Important: Read the TypeDoc documentation for additional context.
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
160
|
+
body: {
|
|
161
|
+
recipients: ["userId-1", "userId-2"],
|
|
162
|
+
data: notificationData,
|
|
163
|
+
},
|
|
164
|
+
// Helpful when controlling notifications with feature flags.
|
|
165
|
+
dryRun: false,
|
|
166
|
+
// Set expiresAt at enqueue-time so it remains stable across job retries. Use date-fns in your
|
|
167
|
+
// service instead of this manual calculation.
|
|
168
|
+
expiresAt: new Date(Date.now() + 60 * 60_000).toISOString(),
|
|
169
|
+
// Keys to redact from logs
|
|
170
|
+
keysToRedact: ["secret"],
|
|
171
|
+
workflowKey: WORKFLOW_KEYS.eventStartingReminder,
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
// Option 1 (default): Automatically use background job ID as idempotency key.
|
|
175
|
+
await adapter.enqueue(TRIGGER_NOTIFICATION_JOB_NAME, jobData, { session: "..." });
|
|
176
|
+
|
|
177
|
+
// Option 2 (advanced): Provide custom idempotency key to job and notification libraries for more
|
|
178
|
+
// control. You'd use this to provide enqueue-time deduplication. For example, if you enqueue when
|
|
179
|
+
// a user clicks a button and only want them to receive one notification.
|
|
180
|
+
await adapter.enqueue(TRIGGER_NOTIFICATION_JOB_NAME, jobData, {
|
|
181
|
+
// Called `idempotencyKey` in `background-jobs-postgres`.
|
|
182
|
+
unique: `meeting-123-reminder`,
|
|
183
|
+
session: "...",
|
|
184
|
+
});
|
|
192
185
|
}
|
|
186
|
+
|
|
187
|
+
// eslint-disable-next-line unicorn/prefer-top-level-await
|
|
188
|
+
void enqueueTriggerNotificationJob(
|
|
189
|
+
// Use your instance of `@clipboard-health/mongo-jobs` or `@clipboard-health/background-jobs-postgres` here.
|
|
190
|
+
new BackgroundJobsService(),
|
|
191
|
+
);
|
|
193
192
|
```
|
|
194
193
|
|
|
195
194
|
</embedex>
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clipboard-health/ai-rules",
|
|
3
3
|
"description": "Pre-built AI agent rules for consistent coding standards.",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.6.0",
|
|
5
5
|
"bugs": "https://github.com/ClipboardHealth/core-utils/issues",
|
|
6
6
|
"devDependencies": {
|
|
7
7
|
"@intellectronica/ruler": "0.3.20"
|
package/scripts/sync.js
CHANGED
|
@@ -31,7 +31,7 @@ async function sync() {
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
function getProfileFromArguments() {
|
|
34
|
-
const profile = process.argv
|
|
34
|
+
const [_firstArgument, _secondArgument, profile] = process.argv;
|
|
35
35
|
if (!profile || !(profile in constants_1.PROFILES)) {
|
|
36
36
|
console.error("❌ Error: Invalid profile argument");
|
|
37
37
|
console.error(`Usage: npm run sync <profile>`);
|