@clipboard-health/ai-rules 0.3.0 → 0.3.2
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 +1 -1
- package/backend/AGENTS.md +143 -0
- package/backend/CLAUDE.md +143 -0
- package/frontend/AGENTS.md +3 -3
- package/frontend/CLAUDE.md +3 -3
- package/fullstack/AGENTS.md +146 -3
- package/fullstack/CLAUDE.md +146 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -60,7 +60,7 @@ npm install --save-dev @clipboard-health/ai-rules
|
|
|
60
60
|
git commit -m "feat: add AI coding rules"
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
-
5.
|
|
63
|
+
5. Bonus: For repo-specific rules, create an `OVERLAY.md` file. The generated files instruct agents to read this file if it exists for additional rules.
|
|
64
64
|
|
|
65
65
|
### Updating Rules
|
|
66
66
|
|
package/backend/AGENTS.md
CHANGED
|
@@ -13,6 +13,149 @@
|
|
|
13
13
|
- Requests and responses follow the JSON:API specification, including pagination for listings.
|
|
14
14
|
- Use TypeDoc to document public functions, classes, methods, and complex code blocks.
|
|
15
15
|
|
|
16
|
+
<!-- Source: .ruler/backend/notifications.md -->
|
|
17
|
+
|
|
18
|
+
# Notifications
|
|
19
|
+
|
|
20
|
+
Send notifications through [Knock](https://docs.knock.app) using the `@clipboard-health/notifications` NPM library.
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
<embedex source="packages/notifications/examples/usage.md">
|
|
25
|
+
|
|
26
|
+
1. Search your service for a `NotificationJobEnqueuer` instance. If there isn't one, create and export it:
|
|
27
|
+
|
|
28
|
+
```ts
|
|
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
|
+
```
|
|
39
|
+
|
|
40
|
+
1. Implement a minimal job, calling off to a NestJS service for any business logic and to send the notification.
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import { type BaseHandler } from "@clipboard-health/background-jobs-adapter";
|
|
44
|
+
import { type NotificationData } from "@clipboard-health/notifications";
|
|
45
|
+
import { isFailure, toError } from "@clipboard-health/util-ts";
|
|
46
|
+
|
|
47
|
+
import { type ExampleNotificationService } from "./exampleNotification.service";
|
|
48
|
+
|
|
49
|
+
export type ExampleNotificationData = NotificationData<{
|
|
50
|
+
workplaceId: string;
|
|
51
|
+
}>;
|
|
52
|
+
|
|
53
|
+
export const EXAMPLE_NOTIFICATION_JOB_NAME = "ExampleNotificationJob";
|
|
54
|
+
|
|
55
|
+
// For mongo-jobs, you'll implement HandlerInterface<ExampleNotificationData["Job"]>
|
|
56
|
+
// For background-jobs-postgres, you'll implement Handler<ExampleNotificationData["Job"]>
|
|
57
|
+
export class ExampleNotificationJob implements BaseHandler<ExampleNotificationData["Job"]> {
|
|
58
|
+
public name = EXAMPLE_NOTIFICATION_JOB_NAME;
|
|
59
|
+
|
|
60
|
+
constructor(private readonly service: ExampleNotificationService) {}
|
|
61
|
+
|
|
62
|
+
async perform(data: ExampleNotificationData["Job"], job: { attemptsCount: number }) {
|
|
63
|
+
const result = await this.service.sendNotification({
|
|
64
|
+
...data,
|
|
65
|
+
// Include the job's attempts count for debugging, this is called `retryAttempts` in `background-jobs-postgres`.
|
|
66
|
+
attempt: job.attemptsCount + 1,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
if (isFailure(result)) {
|
|
70
|
+
throw toError(result.error);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
1. Search your service for a constant that stores workflow keys. If there isn't one, create and export it:
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
export const WORKFLOW_KEYS = {
|
|
80
|
+
eventStartingReminder: "event-starting-reminder",
|
|
81
|
+
} as const;
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
1. Enqueue your job:
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
import {
|
|
88
|
+
EXAMPLE_NOTIFICATION_JOB_NAME,
|
|
89
|
+
type ExampleNotificationData,
|
|
90
|
+
} from "./exampleNotification.job";
|
|
91
|
+
import { notificationJobEnqueuer } from "./notificationJobEnqueuer";
|
|
92
|
+
import { WORKFLOW_KEYS } from "./workflowKeys";
|
|
93
|
+
|
|
94
|
+
async function enqueueNotificationJob() {
|
|
95
|
+
await notificationJobEnqueuer.enqueueOneOrMore<ExampleNotificationData["Enqueue"]>(
|
|
96
|
+
EXAMPLE_NOTIFICATION_JOB_NAME,
|
|
97
|
+
// Important: Read the TypeDoc documentation for additional context.
|
|
98
|
+
{
|
|
99
|
+
// Set expiresAt at enqueue-time so it remains stable across job retries.
|
|
100
|
+
// Use date-fns in your service instead of this manual calculation.
|
|
101
|
+
expiresAt: new Date(Date.now() + 60 * 60_000).toISOString(),
|
|
102
|
+
// Set idempotencyKey at enqueue-time so it remains stable across job retries.
|
|
103
|
+
idempotencyKey: {
|
|
104
|
+
resourceId: "event-123",
|
|
105
|
+
},
|
|
106
|
+
// Set recipients at enqueue-time so they respect our notification provider's limits.
|
|
107
|
+
recipients: ["userId-1"],
|
|
108
|
+
|
|
109
|
+
workflowKey: WORKFLOW_KEYS.eventStartingReminder,
|
|
110
|
+
|
|
111
|
+
// Any additional enqueue-time data passed to the job:
|
|
112
|
+
workplaceId: "workplaceId-123",
|
|
113
|
+
},
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// eslint-disable-next-line unicorn/prefer-top-level-await
|
|
118
|
+
void enqueueNotificationJob();
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
1. Trigger the job in your NestJS service:
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
import { type NotificationClient } from "@clipboard-health/notifications";
|
|
125
|
+
|
|
126
|
+
import { type ExampleNotificationData } from "./exampleNotification.job";
|
|
127
|
+
|
|
128
|
+
type ExampleNotificationDo = ExampleNotificationData["Job"] & { attempt: number };
|
|
129
|
+
|
|
130
|
+
export class ExampleNotificationService {
|
|
131
|
+
constructor(private readonly client: NotificationClient) {}
|
|
132
|
+
|
|
133
|
+
async sendNotification(params: ExampleNotificationDo) {
|
|
134
|
+
const { attempt, expiresAt, idempotencyKey, recipients, workflowKey, workplaceId } = params;
|
|
135
|
+
|
|
136
|
+
// Assume this comes from a database and are used as template variables...
|
|
137
|
+
// Use @clipboard-health/date-time's formatShortDateTime in your service for consistency.
|
|
138
|
+
const data = { favoriteColor: "blue", favoriteAt: new Date().toISOString(), secret: "2" };
|
|
139
|
+
|
|
140
|
+
// Important: Read the TypeDoc documentation for additional context.
|
|
141
|
+
return await this.client.trigger({
|
|
142
|
+
attempt,
|
|
143
|
+
body: {
|
|
144
|
+
data,
|
|
145
|
+
recipients,
|
|
146
|
+
workplaceId,
|
|
147
|
+
},
|
|
148
|
+
expiresAt: new Date(expiresAt),
|
|
149
|
+
idempotencyKey,
|
|
150
|
+
keysToRedact: ["secret"],
|
|
151
|
+
workflowKey,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
</embedex>
|
|
158
|
+
|
|
16
159
|
<!-- Source: .ruler/common/codeStyleAndStructure.md -->
|
|
17
160
|
|
|
18
161
|
# Code style and structure
|
package/backend/CLAUDE.md
CHANGED
|
@@ -11,6 +11,149 @@
|
|
|
11
11
|
- Requests and responses follow the JSON:API specification, including pagination for listings.
|
|
12
12
|
- Use TypeDoc to document public functions, classes, methods, and complex code blocks.
|
|
13
13
|
|
|
14
|
+
<!-- Source: .ruler/backend/notifications.md -->
|
|
15
|
+
|
|
16
|
+
# Notifications
|
|
17
|
+
|
|
18
|
+
Send notifications through [Knock](https://docs.knock.app) using the `@clipboard-health/notifications` NPM library.
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
<embedex source="packages/notifications/examples/usage.md">
|
|
23
|
+
|
|
24
|
+
1. Search your service for a `NotificationJobEnqueuer` instance. If there isn't one, create and export it:
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { NotificationJobEnqueuer } from "@clipboard-health/notifications";
|
|
28
|
+
|
|
29
|
+
import { BackgroundJobsService } from "./setup";
|
|
30
|
+
|
|
31
|
+
// Create and export one instance of this in your microservice.
|
|
32
|
+
export const notificationJobEnqueuer = new NotificationJobEnqueuer({
|
|
33
|
+
// Use your instance of `@clipboard-health/mongo-jobs` or `@clipboard-health/background-jobs-postgres` here.
|
|
34
|
+
adapter: new BackgroundJobsService(),
|
|
35
|
+
});
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
1. Implement a minimal job, calling off to a NestJS service for any business logic and to send the notification.
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import { type BaseHandler } from "@clipboard-health/background-jobs-adapter";
|
|
42
|
+
import { type NotificationData } from "@clipboard-health/notifications";
|
|
43
|
+
import { isFailure, toError } from "@clipboard-health/util-ts";
|
|
44
|
+
|
|
45
|
+
import { type ExampleNotificationService } from "./exampleNotification.service";
|
|
46
|
+
|
|
47
|
+
export type ExampleNotificationData = NotificationData<{
|
|
48
|
+
workplaceId: string;
|
|
49
|
+
}>;
|
|
50
|
+
|
|
51
|
+
export const EXAMPLE_NOTIFICATION_JOB_NAME = "ExampleNotificationJob";
|
|
52
|
+
|
|
53
|
+
// For mongo-jobs, you'll implement HandlerInterface<ExampleNotificationData["Job"]>
|
|
54
|
+
// For background-jobs-postgres, you'll implement Handler<ExampleNotificationData["Job"]>
|
|
55
|
+
export class ExampleNotificationJob implements BaseHandler<ExampleNotificationData["Job"]> {
|
|
56
|
+
public name = EXAMPLE_NOTIFICATION_JOB_NAME;
|
|
57
|
+
|
|
58
|
+
constructor(private readonly service: ExampleNotificationService) {}
|
|
59
|
+
|
|
60
|
+
async perform(data: ExampleNotificationData["Job"], job: { attemptsCount: number }) {
|
|
61
|
+
const result = await this.service.sendNotification({
|
|
62
|
+
...data,
|
|
63
|
+
// Include the job's attempts count for debugging, this is called `retryAttempts` in `background-jobs-postgres`.
|
|
64
|
+
attempt: job.attemptsCount + 1,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
if (isFailure(result)) {
|
|
68
|
+
throw toError(result.error);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
1. Search your service for a constant that stores workflow keys. If there isn't one, create and export it:
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
export const WORKFLOW_KEYS = {
|
|
78
|
+
eventStartingReminder: "event-starting-reminder",
|
|
79
|
+
} as const;
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
1. Enqueue your job:
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
import {
|
|
86
|
+
EXAMPLE_NOTIFICATION_JOB_NAME,
|
|
87
|
+
type ExampleNotificationData,
|
|
88
|
+
} from "./exampleNotification.job";
|
|
89
|
+
import { notificationJobEnqueuer } from "./notificationJobEnqueuer";
|
|
90
|
+
import { WORKFLOW_KEYS } from "./workflowKeys";
|
|
91
|
+
|
|
92
|
+
async function enqueueNotificationJob() {
|
|
93
|
+
await notificationJobEnqueuer.enqueueOneOrMore<ExampleNotificationData["Enqueue"]>(
|
|
94
|
+
EXAMPLE_NOTIFICATION_JOB_NAME,
|
|
95
|
+
// Important: Read the TypeDoc documentation for additional context.
|
|
96
|
+
{
|
|
97
|
+
// Set expiresAt at enqueue-time so it remains stable across job retries.
|
|
98
|
+
// Use date-fns in your service instead of this manual calculation.
|
|
99
|
+
expiresAt: new Date(Date.now() + 60 * 60_000).toISOString(),
|
|
100
|
+
// Set idempotencyKey at enqueue-time so it remains stable across job retries.
|
|
101
|
+
idempotencyKey: {
|
|
102
|
+
resourceId: "event-123",
|
|
103
|
+
},
|
|
104
|
+
// Set recipients at enqueue-time so they respect our notification provider's limits.
|
|
105
|
+
recipients: ["userId-1"],
|
|
106
|
+
|
|
107
|
+
workflowKey: WORKFLOW_KEYS.eventStartingReminder,
|
|
108
|
+
|
|
109
|
+
// Any additional enqueue-time data passed to the job:
|
|
110
|
+
workplaceId: "workplaceId-123",
|
|
111
|
+
},
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// eslint-disable-next-line unicorn/prefer-top-level-await
|
|
116
|
+
void enqueueNotificationJob();
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
1. Trigger the job in your NestJS service:
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
import { type NotificationClient } from "@clipboard-health/notifications";
|
|
123
|
+
|
|
124
|
+
import { type ExampleNotificationData } from "./exampleNotification.job";
|
|
125
|
+
|
|
126
|
+
type ExampleNotificationDo = ExampleNotificationData["Job"] & { attempt: number };
|
|
127
|
+
|
|
128
|
+
export class ExampleNotificationService {
|
|
129
|
+
constructor(private readonly client: NotificationClient) {}
|
|
130
|
+
|
|
131
|
+
async sendNotification(params: ExampleNotificationDo) {
|
|
132
|
+
const { attempt, expiresAt, idempotencyKey, recipients, workflowKey, workplaceId } = params;
|
|
133
|
+
|
|
134
|
+
// Assume this comes from a database and are used as template variables...
|
|
135
|
+
// Use @clipboard-health/date-time's formatShortDateTime in your service for consistency.
|
|
136
|
+
const data = { favoriteColor: "blue", favoriteAt: new Date().toISOString(), secret: "2" };
|
|
137
|
+
|
|
138
|
+
// Important: Read the TypeDoc documentation for additional context.
|
|
139
|
+
return await this.client.trigger({
|
|
140
|
+
attempt,
|
|
141
|
+
body: {
|
|
142
|
+
data,
|
|
143
|
+
recipients,
|
|
144
|
+
workplaceId,
|
|
145
|
+
},
|
|
146
|
+
expiresAt: new Date(expiresAt),
|
|
147
|
+
idempotencyKey,
|
|
148
|
+
keysToRedact: ["secret"],
|
|
149
|
+
workflowKey,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
</embedex>
|
|
156
|
+
|
|
14
157
|
<!-- Source: .ruler/common/codeStyleAndStructure.md -->
|
|
15
158
|
|
|
16
159
|
# Code style and structure
|
package/frontend/AGENTS.md
CHANGED
|
@@ -1796,7 +1796,7 @@ npm run lint:fix
|
|
|
1796
1796
|
### Stale Time Configuration
|
|
1797
1797
|
|
|
1798
1798
|
```typescript
|
|
1799
|
-
// Set appropriate staleTime to avoid unnecessary
|
|
1799
|
+
// Set appropriate staleTime to avoid unnecessary refetch
|
|
1800
1800
|
useGetQuery({
|
|
1801
1801
|
url: "/api/resource",
|
|
1802
1802
|
responseSchema: schema,
|
|
@@ -3094,14 +3094,14 @@ Projects often augment MUI's theme with custom properties:
|
|
|
3094
3094
|
declare module "@mui/material/styles" {
|
|
3095
3095
|
interface Theme {
|
|
3096
3096
|
customSpacing: {
|
|
3097
|
+
small: string;
|
|
3097
3098
|
large: string;
|
|
3098
|
-
xlarge: string;
|
|
3099
3099
|
};
|
|
3100
3100
|
}
|
|
3101
3101
|
interface ThemeOptions {
|
|
3102
3102
|
customSpacing?: {
|
|
3103
|
+
small?: string;
|
|
3103
3104
|
large?: string;
|
|
3104
|
-
xlarge?: string;
|
|
3105
3105
|
};
|
|
3106
3106
|
}
|
|
3107
3107
|
}
|
package/frontend/CLAUDE.md
CHANGED
|
@@ -1794,7 +1794,7 @@ npm run lint:fix
|
|
|
1794
1794
|
### Stale Time Configuration
|
|
1795
1795
|
|
|
1796
1796
|
```typescript
|
|
1797
|
-
// Set appropriate staleTime to avoid unnecessary
|
|
1797
|
+
// Set appropriate staleTime to avoid unnecessary refetch
|
|
1798
1798
|
useGetQuery({
|
|
1799
1799
|
url: "/api/resource",
|
|
1800
1800
|
responseSchema: schema,
|
|
@@ -3092,14 +3092,14 @@ Projects often augment MUI's theme with custom properties:
|
|
|
3092
3092
|
declare module "@mui/material/styles" {
|
|
3093
3093
|
interface Theme {
|
|
3094
3094
|
customSpacing: {
|
|
3095
|
+
small: string;
|
|
3095
3096
|
large: string;
|
|
3096
|
-
xlarge: string;
|
|
3097
3097
|
};
|
|
3098
3098
|
}
|
|
3099
3099
|
interface ThemeOptions {
|
|
3100
3100
|
customSpacing?: {
|
|
3101
|
+
small?: string;
|
|
3101
3102
|
large?: string;
|
|
3102
|
-
xlarge?: string;
|
|
3103
3103
|
};
|
|
3104
3104
|
}
|
|
3105
3105
|
}
|
package/fullstack/AGENTS.md
CHANGED
|
@@ -13,6 +13,149 @@
|
|
|
13
13
|
- Requests and responses follow the JSON:API specification, including pagination for listings.
|
|
14
14
|
- Use TypeDoc to document public functions, classes, methods, and complex code blocks.
|
|
15
15
|
|
|
16
|
+
<!-- Source: .ruler/backend/notifications.md -->
|
|
17
|
+
|
|
18
|
+
# Notifications
|
|
19
|
+
|
|
20
|
+
Send notifications through [Knock](https://docs.knock.app) using the `@clipboard-health/notifications` NPM library.
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
<embedex source="packages/notifications/examples/usage.md">
|
|
25
|
+
|
|
26
|
+
1. Search your service for a `NotificationJobEnqueuer` instance. If there isn't one, create and export it:
|
|
27
|
+
|
|
28
|
+
```ts
|
|
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
|
+
```
|
|
39
|
+
|
|
40
|
+
1. Implement a minimal job, calling off to a NestJS service for any business logic and to send the notification.
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import { type BaseHandler } from "@clipboard-health/background-jobs-adapter";
|
|
44
|
+
import { type NotificationData } from "@clipboard-health/notifications";
|
|
45
|
+
import { isFailure, toError } from "@clipboard-health/util-ts";
|
|
46
|
+
|
|
47
|
+
import { type ExampleNotificationService } from "./exampleNotification.service";
|
|
48
|
+
|
|
49
|
+
export type ExampleNotificationData = NotificationData<{
|
|
50
|
+
workplaceId: string;
|
|
51
|
+
}>;
|
|
52
|
+
|
|
53
|
+
export const EXAMPLE_NOTIFICATION_JOB_NAME = "ExampleNotificationJob";
|
|
54
|
+
|
|
55
|
+
// For mongo-jobs, you'll implement HandlerInterface<ExampleNotificationData["Job"]>
|
|
56
|
+
// For background-jobs-postgres, you'll implement Handler<ExampleNotificationData["Job"]>
|
|
57
|
+
export class ExampleNotificationJob implements BaseHandler<ExampleNotificationData["Job"]> {
|
|
58
|
+
public name = EXAMPLE_NOTIFICATION_JOB_NAME;
|
|
59
|
+
|
|
60
|
+
constructor(private readonly service: ExampleNotificationService) {}
|
|
61
|
+
|
|
62
|
+
async perform(data: ExampleNotificationData["Job"], job: { attemptsCount: number }) {
|
|
63
|
+
const result = await this.service.sendNotification({
|
|
64
|
+
...data,
|
|
65
|
+
// Include the job's attempts count for debugging, this is called `retryAttempts` in `background-jobs-postgres`.
|
|
66
|
+
attempt: job.attemptsCount + 1,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
if (isFailure(result)) {
|
|
70
|
+
throw toError(result.error);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
1. Search your service for a constant that stores workflow keys. If there isn't one, create and export it:
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
export const WORKFLOW_KEYS = {
|
|
80
|
+
eventStartingReminder: "event-starting-reminder",
|
|
81
|
+
} as const;
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
1. Enqueue your job:
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
import {
|
|
88
|
+
EXAMPLE_NOTIFICATION_JOB_NAME,
|
|
89
|
+
type ExampleNotificationData,
|
|
90
|
+
} from "./exampleNotification.job";
|
|
91
|
+
import { notificationJobEnqueuer } from "./notificationJobEnqueuer";
|
|
92
|
+
import { WORKFLOW_KEYS } from "./workflowKeys";
|
|
93
|
+
|
|
94
|
+
async function enqueueNotificationJob() {
|
|
95
|
+
await notificationJobEnqueuer.enqueueOneOrMore<ExampleNotificationData["Enqueue"]>(
|
|
96
|
+
EXAMPLE_NOTIFICATION_JOB_NAME,
|
|
97
|
+
// Important: Read the TypeDoc documentation for additional context.
|
|
98
|
+
{
|
|
99
|
+
// Set expiresAt at enqueue-time so it remains stable across job retries.
|
|
100
|
+
// Use date-fns in your service instead of this manual calculation.
|
|
101
|
+
expiresAt: new Date(Date.now() + 60 * 60_000).toISOString(),
|
|
102
|
+
// Set idempotencyKey at enqueue-time so it remains stable across job retries.
|
|
103
|
+
idempotencyKey: {
|
|
104
|
+
resourceId: "event-123",
|
|
105
|
+
},
|
|
106
|
+
// Set recipients at enqueue-time so they respect our notification provider's limits.
|
|
107
|
+
recipients: ["userId-1"],
|
|
108
|
+
|
|
109
|
+
workflowKey: WORKFLOW_KEYS.eventStartingReminder,
|
|
110
|
+
|
|
111
|
+
// Any additional enqueue-time data passed to the job:
|
|
112
|
+
workplaceId: "workplaceId-123",
|
|
113
|
+
},
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// eslint-disable-next-line unicorn/prefer-top-level-await
|
|
118
|
+
void enqueueNotificationJob();
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
1. Trigger the job in your NestJS service:
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
import { type NotificationClient } from "@clipboard-health/notifications";
|
|
125
|
+
|
|
126
|
+
import { type ExampleNotificationData } from "./exampleNotification.job";
|
|
127
|
+
|
|
128
|
+
type ExampleNotificationDo = ExampleNotificationData["Job"] & { attempt: number };
|
|
129
|
+
|
|
130
|
+
export class ExampleNotificationService {
|
|
131
|
+
constructor(private readonly client: NotificationClient) {}
|
|
132
|
+
|
|
133
|
+
async sendNotification(params: ExampleNotificationDo) {
|
|
134
|
+
const { attempt, expiresAt, idempotencyKey, recipients, workflowKey, workplaceId } = params;
|
|
135
|
+
|
|
136
|
+
// Assume this comes from a database and are used as template variables...
|
|
137
|
+
// Use @clipboard-health/date-time's formatShortDateTime in your service for consistency.
|
|
138
|
+
const data = { favoriteColor: "blue", favoriteAt: new Date().toISOString(), secret: "2" };
|
|
139
|
+
|
|
140
|
+
// Important: Read the TypeDoc documentation for additional context.
|
|
141
|
+
return await this.client.trigger({
|
|
142
|
+
attempt,
|
|
143
|
+
body: {
|
|
144
|
+
data,
|
|
145
|
+
recipients,
|
|
146
|
+
workplaceId,
|
|
147
|
+
},
|
|
148
|
+
expiresAt: new Date(expiresAt),
|
|
149
|
+
idempotencyKey,
|
|
150
|
+
keysToRedact: ["secret"],
|
|
151
|
+
workflowKey,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
</embedex>
|
|
158
|
+
|
|
16
159
|
<!-- Source: .ruler/common/codeStyleAndStructure.md -->
|
|
17
160
|
|
|
18
161
|
# Code style and structure
|
|
@@ -1809,7 +1952,7 @@ npm run lint:fix
|
|
|
1809
1952
|
### Stale Time Configuration
|
|
1810
1953
|
|
|
1811
1954
|
```typescript
|
|
1812
|
-
// Set appropriate staleTime to avoid unnecessary
|
|
1955
|
+
// Set appropriate staleTime to avoid unnecessary refetch
|
|
1813
1956
|
useGetQuery({
|
|
1814
1957
|
url: "/api/resource",
|
|
1815
1958
|
responseSchema: schema,
|
|
@@ -3107,14 +3250,14 @@ Projects often augment MUI's theme with custom properties:
|
|
|
3107
3250
|
declare module "@mui/material/styles" {
|
|
3108
3251
|
interface Theme {
|
|
3109
3252
|
customSpacing: {
|
|
3253
|
+
small: string;
|
|
3110
3254
|
large: string;
|
|
3111
|
-
xlarge: string;
|
|
3112
3255
|
};
|
|
3113
3256
|
}
|
|
3114
3257
|
interface ThemeOptions {
|
|
3115
3258
|
customSpacing?: {
|
|
3259
|
+
small?: string;
|
|
3116
3260
|
large?: string;
|
|
3117
|
-
xlarge?: string;
|
|
3118
3261
|
};
|
|
3119
3262
|
}
|
|
3120
3263
|
}
|
package/fullstack/CLAUDE.md
CHANGED
|
@@ -11,6 +11,149 @@
|
|
|
11
11
|
- Requests and responses follow the JSON:API specification, including pagination for listings.
|
|
12
12
|
- Use TypeDoc to document public functions, classes, methods, and complex code blocks.
|
|
13
13
|
|
|
14
|
+
<!-- Source: .ruler/backend/notifications.md -->
|
|
15
|
+
|
|
16
|
+
# Notifications
|
|
17
|
+
|
|
18
|
+
Send notifications through [Knock](https://docs.knock.app) using the `@clipboard-health/notifications` NPM library.
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
<embedex source="packages/notifications/examples/usage.md">
|
|
23
|
+
|
|
24
|
+
1. Search your service for a `NotificationJobEnqueuer` instance. If there isn't one, create and export it:
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { NotificationJobEnqueuer } from "@clipboard-health/notifications";
|
|
28
|
+
|
|
29
|
+
import { BackgroundJobsService } from "./setup";
|
|
30
|
+
|
|
31
|
+
// Create and export one instance of this in your microservice.
|
|
32
|
+
export const notificationJobEnqueuer = new NotificationJobEnqueuer({
|
|
33
|
+
// Use your instance of `@clipboard-health/mongo-jobs` or `@clipboard-health/background-jobs-postgres` here.
|
|
34
|
+
adapter: new BackgroundJobsService(),
|
|
35
|
+
});
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
1. Implement a minimal job, calling off to a NestJS service for any business logic and to send the notification.
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import { type BaseHandler } from "@clipboard-health/background-jobs-adapter";
|
|
42
|
+
import { type NotificationData } from "@clipboard-health/notifications";
|
|
43
|
+
import { isFailure, toError } from "@clipboard-health/util-ts";
|
|
44
|
+
|
|
45
|
+
import { type ExampleNotificationService } from "./exampleNotification.service";
|
|
46
|
+
|
|
47
|
+
export type ExampleNotificationData = NotificationData<{
|
|
48
|
+
workplaceId: string;
|
|
49
|
+
}>;
|
|
50
|
+
|
|
51
|
+
export const EXAMPLE_NOTIFICATION_JOB_NAME = "ExampleNotificationJob";
|
|
52
|
+
|
|
53
|
+
// For mongo-jobs, you'll implement HandlerInterface<ExampleNotificationData["Job"]>
|
|
54
|
+
// For background-jobs-postgres, you'll implement Handler<ExampleNotificationData["Job"]>
|
|
55
|
+
export class ExampleNotificationJob implements BaseHandler<ExampleNotificationData["Job"]> {
|
|
56
|
+
public name = EXAMPLE_NOTIFICATION_JOB_NAME;
|
|
57
|
+
|
|
58
|
+
constructor(private readonly service: ExampleNotificationService) {}
|
|
59
|
+
|
|
60
|
+
async perform(data: ExampleNotificationData["Job"], job: { attemptsCount: number }) {
|
|
61
|
+
const result = await this.service.sendNotification({
|
|
62
|
+
...data,
|
|
63
|
+
// Include the job's attempts count for debugging, this is called `retryAttempts` in `background-jobs-postgres`.
|
|
64
|
+
attempt: job.attemptsCount + 1,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
if (isFailure(result)) {
|
|
68
|
+
throw toError(result.error);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
1. Search your service for a constant that stores workflow keys. If there isn't one, create and export it:
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
export const WORKFLOW_KEYS = {
|
|
78
|
+
eventStartingReminder: "event-starting-reminder",
|
|
79
|
+
} as const;
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
1. Enqueue your job:
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
import {
|
|
86
|
+
EXAMPLE_NOTIFICATION_JOB_NAME,
|
|
87
|
+
type ExampleNotificationData,
|
|
88
|
+
} from "./exampleNotification.job";
|
|
89
|
+
import { notificationJobEnqueuer } from "./notificationJobEnqueuer";
|
|
90
|
+
import { WORKFLOW_KEYS } from "./workflowKeys";
|
|
91
|
+
|
|
92
|
+
async function enqueueNotificationJob() {
|
|
93
|
+
await notificationJobEnqueuer.enqueueOneOrMore<ExampleNotificationData["Enqueue"]>(
|
|
94
|
+
EXAMPLE_NOTIFICATION_JOB_NAME,
|
|
95
|
+
// Important: Read the TypeDoc documentation for additional context.
|
|
96
|
+
{
|
|
97
|
+
// Set expiresAt at enqueue-time so it remains stable across job retries.
|
|
98
|
+
// Use date-fns in your service instead of this manual calculation.
|
|
99
|
+
expiresAt: new Date(Date.now() + 60 * 60_000).toISOString(),
|
|
100
|
+
// Set idempotencyKey at enqueue-time so it remains stable across job retries.
|
|
101
|
+
idempotencyKey: {
|
|
102
|
+
resourceId: "event-123",
|
|
103
|
+
},
|
|
104
|
+
// Set recipients at enqueue-time so they respect our notification provider's limits.
|
|
105
|
+
recipients: ["userId-1"],
|
|
106
|
+
|
|
107
|
+
workflowKey: WORKFLOW_KEYS.eventStartingReminder,
|
|
108
|
+
|
|
109
|
+
// Any additional enqueue-time data passed to the job:
|
|
110
|
+
workplaceId: "workplaceId-123",
|
|
111
|
+
},
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// eslint-disable-next-line unicorn/prefer-top-level-await
|
|
116
|
+
void enqueueNotificationJob();
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
1. Trigger the job in your NestJS service:
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
import { type NotificationClient } from "@clipboard-health/notifications";
|
|
123
|
+
|
|
124
|
+
import { type ExampleNotificationData } from "./exampleNotification.job";
|
|
125
|
+
|
|
126
|
+
type ExampleNotificationDo = ExampleNotificationData["Job"] & { attempt: number };
|
|
127
|
+
|
|
128
|
+
export class ExampleNotificationService {
|
|
129
|
+
constructor(private readonly client: NotificationClient) {}
|
|
130
|
+
|
|
131
|
+
async sendNotification(params: ExampleNotificationDo) {
|
|
132
|
+
const { attempt, expiresAt, idempotencyKey, recipients, workflowKey, workplaceId } = params;
|
|
133
|
+
|
|
134
|
+
// Assume this comes from a database and are used as template variables...
|
|
135
|
+
// Use @clipboard-health/date-time's formatShortDateTime in your service for consistency.
|
|
136
|
+
const data = { favoriteColor: "blue", favoriteAt: new Date().toISOString(), secret: "2" };
|
|
137
|
+
|
|
138
|
+
// Important: Read the TypeDoc documentation for additional context.
|
|
139
|
+
return await this.client.trigger({
|
|
140
|
+
attempt,
|
|
141
|
+
body: {
|
|
142
|
+
data,
|
|
143
|
+
recipients,
|
|
144
|
+
workplaceId,
|
|
145
|
+
},
|
|
146
|
+
expiresAt: new Date(expiresAt),
|
|
147
|
+
idempotencyKey,
|
|
148
|
+
keysToRedact: ["secret"],
|
|
149
|
+
workflowKey,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
</embedex>
|
|
156
|
+
|
|
14
157
|
<!-- Source: .ruler/common/codeStyleAndStructure.md -->
|
|
15
158
|
|
|
16
159
|
# Code style and structure
|
|
@@ -1807,7 +1950,7 @@ npm run lint:fix
|
|
|
1807
1950
|
### Stale Time Configuration
|
|
1808
1951
|
|
|
1809
1952
|
```typescript
|
|
1810
|
-
// Set appropriate staleTime to avoid unnecessary
|
|
1953
|
+
// Set appropriate staleTime to avoid unnecessary refetch
|
|
1811
1954
|
useGetQuery({
|
|
1812
1955
|
url: "/api/resource",
|
|
1813
1956
|
responseSchema: schema,
|
|
@@ -3105,14 +3248,14 @@ Projects often augment MUI's theme with custom properties:
|
|
|
3105
3248
|
declare module "@mui/material/styles" {
|
|
3106
3249
|
interface Theme {
|
|
3107
3250
|
customSpacing: {
|
|
3251
|
+
small: string;
|
|
3108
3252
|
large: string;
|
|
3109
|
-
xlarge: string;
|
|
3110
3253
|
};
|
|
3111
3254
|
}
|
|
3112
3255
|
interface ThemeOptions {
|
|
3113
3256
|
customSpacing?: {
|
|
3257
|
+
small?: string;
|
|
3114
3258
|
large?: string;
|
|
3115
|
-
xlarge?: string;
|
|
3116
3259
|
};
|
|
3117
3260
|
}
|
|
3118
3261
|
}
|
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": "0.3.
|
|
4
|
+
"version": "0.3.2",
|
|
5
5
|
"bugs": "https://github.com/ClipboardHealth/core-utils/issues",
|
|
6
6
|
"devDependencies": {
|
|
7
7
|
"@intellectronica/ruler": "0.3.10"
|