@checkstack/notification-common 0.0.4 → 0.2.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/CHANGELOG.md CHANGED
@@ -1,5 +1,140 @@
1
1
  # @checkstack/notification-common
2
2
 
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 7a23261: ## TanStack Query Integration
8
+
9
+ Migrated all frontend components to use `usePluginClient` hook with TanStack Query integration, replacing the legacy `forPlugin()` pattern.
10
+
11
+ ### New Features
12
+
13
+ - **`usePluginClient` hook**: Provides type-safe access to plugin APIs with `.useQuery()` and `.useMutation()` methods
14
+ - **Automatic request deduplication**: Multiple components requesting the same data share a single network request
15
+ - **Built-in caching**: Configurable stale time and cache duration per query
16
+ - **Loading/error states**: TanStack Query provides `isLoading`, `error`, `isRefetching` states automatically
17
+ - **Background refetching**: Stale data is automatically refreshed when components mount
18
+
19
+ ### Contract Changes
20
+
21
+ All RPC contracts now require `operationType: "query"` or `operationType: "mutation"` metadata:
22
+
23
+ ```typescript
24
+ const getItems = proc()
25
+ .meta({ operationType: "query", access: [access.read] })
26
+ .output(z.array(itemSchema))
27
+ .query();
28
+
29
+ const createItem = proc()
30
+ .meta({ operationType: "mutation", access: [access.manage] })
31
+ .input(createItemSchema)
32
+ .output(itemSchema)
33
+ .mutation();
34
+ ```
35
+
36
+ ### Migration
37
+
38
+ ```typescript
39
+ // Before (forPlugin pattern)
40
+ const api = useApi(myPluginApiRef);
41
+ const [items, setItems] = useState<Item[]>([]);
42
+ useEffect(() => {
43
+ api.getItems().then(setItems);
44
+ }, [api]);
45
+
46
+ // After (usePluginClient pattern)
47
+ const client = usePluginClient(MyPluginApi);
48
+ const { data: items, isLoading } = client.getItems.useQuery({});
49
+ ```
50
+
51
+ ### Bug Fixes
52
+
53
+ - Fixed `rpc.test.ts` test setup for middleware type inference
54
+ - Fixed `SearchDialog` to use `setQuery` instead of deprecated `search` method
55
+ - Fixed null→undefined warnings in notification and queue frontends
56
+
57
+ ### Patch Changes
58
+
59
+ - Updated dependencies [7a23261]
60
+ - @checkstack/common@0.3.0
61
+ - @checkstack/signal-common@0.1.1
62
+
63
+ ## 0.1.0
64
+
65
+ ### Minor Changes
66
+
67
+ - 9faec1f: # Unified AccessRule Terminology Refactoring
68
+
69
+ This release completes a comprehensive terminology refactoring from "permission" to "accessRule" across the entire codebase, establishing a consistent and modern access control vocabulary.
70
+
71
+ ## Changes
72
+
73
+ ### Core Infrastructure (`@checkstack/common`)
74
+
75
+ - Introduced `AccessRule` interface as the primary access control type
76
+ - Added `accessPair()` helper for creating read/manage access rule pairs
77
+ - Added `access()` builder for individual access rules
78
+ - Replaced `Permission` type with `AccessRule` throughout
79
+
80
+ ### API Changes
81
+
82
+ - `env.registerPermissions()` → `env.registerAccessRules()`
83
+ - `meta.permissions` → `meta.access` in RPC contracts
84
+ - `usePermission()` → `useAccess()` in frontend hooks
85
+ - Route `permission:` field → `accessRule:` field
86
+
87
+ ### UI Changes
88
+
89
+ - "Roles & Permissions" tab → "Roles & Access Rules"
90
+ - "You don't have permission..." → "You don't have access..."
91
+ - All permission-related UI text updated
92
+
93
+ ### Documentation & Templates
94
+
95
+ - Updated 18 documentation files with AccessRule terminology
96
+ - Updated 7 scaffolding templates with `accessPair()` pattern
97
+ - All code examples use new AccessRule API
98
+
99
+ ## Migration Guide
100
+
101
+ ### Backend Plugins
102
+
103
+ ```diff
104
+ - import { permissionList } from "./permissions";
105
+ - env.registerPermissions(permissionList);
106
+ + import { accessRules } from "./access";
107
+ + env.registerAccessRules(accessRules);
108
+ ```
109
+
110
+ ### RPC Contracts
111
+
112
+ ```diff
113
+ - .meta({ userType: "user", permissions: [permissions.read.id] })
114
+ + .meta({ userType: "user", access: [access.read] })
115
+ ```
116
+
117
+ ### Frontend Hooks
118
+
119
+ ```diff
120
+ - const canRead = accessApi.usePermission(permissions.read.id);
121
+ + const canRead = accessApi.useAccess(access.read);
122
+ ```
123
+
124
+ ### Routes
125
+
126
+ ```diff
127
+ - permission: permissions.entityRead.id,
128
+ + accessRule: access.read,
129
+ ```
130
+
131
+ ### Patch Changes
132
+
133
+ - Updated dependencies [9faec1f]
134
+ - Updated dependencies [f533141]
135
+ - @checkstack/common@0.2.0
136
+ - @checkstack/signal-common@0.1.0
137
+
3
138
  ## 0.0.4
4
139
 
5
140
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/notification-common",
3
- "version": "0.0.4",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
package/src/access.ts ADDED
@@ -0,0 +1,20 @@
1
+ import { access } from "@checkstack/common";
2
+
3
+ /**
4
+ * Access rules for the Notification plugin.
5
+ */
6
+ export const notificationAccess = {
7
+ /**
8
+ * Configure notification settings and send broadcasts.
9
+ */
10
+ admin: access(
11
+ "notification",
12
+ "manage",
13
+ "Configure notification settings and send broadcasts"
14
+ ),
15
+ };
16
+
17
+ /**
18
+ * All access rules for registration with the plugin system.
19
+ */
20
+ export const notificationAccessRules = [notificationAccess.admin];
package/src/index.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export * from "./schemas";
2
- export * from "./permissions";
2
+ export * from "./access";
3
3
  export * from "./rpc-contract";
4
4
  export * from "./signals";
5
5
  export * from "./plugin-metadata";
@@ -1,11 +1,7 @@
1
- import { oc } from "@orpc/contract";
2
1
  import { z } from "zod";
3
- import { permissions } from "./permissions";
2
+ import { notificationAccess } from "./access";
4
3
  import { pluginMetadata } from "./plugin-metadata";
5
- import {
6
- createClientDefinition,
7
- type ProcedureMetadata,
8
- } from "@checkstack/common";
4
+ import { createClientDefinition, proc } from "@checkstack/common";
9
5
  import {
10
6
  NotificationSchema,
11
7
  NotificationGroupSchema,
@@ -14,9 +10,6 @@ import {
14
10
  PaginationInputSchema,
15
11
  } from "./schemas";
16
12
 
17
- // Base builder with full metadata support (userType + permissions)
18
- const _base = oc.$meta<ProcedureMetadata>({});
19
-
20
13
  // Notification RPC Contract
21
14
  export const notificationContract = {
22
15
  // ==========================================================================
@@ -24,10 +17,11 @@ export const notificationContract = {
24
17
  // ==========================================================================
25
18
 
26
19
  // Get current user's notifications (paginated)
27
- getNotifications: _base
28
- .meta({
29
- userType: "user",
30
- })
20
+ getNotifications: proc({
21
+ operationType: "query",
22
+ userType: "user",
23
+ access: [],
24
+ })
31
25
  .input(PaginationInputSchema)
32
26
  .output(
33
27
  z.object({
@@ -37,17 +31,18 @@ export const notificationContract = {
37
31
  ),
38
32
 
39
33
  // Get unread count for badge
40
- getUnreadCount: _base
41
- .meta({
42
- userType: "user",
43
- })
44
- .output(z.object({ count: z.number() })),
34
+ getUnreadCount: proc({
35
+ operationType: "query",
36
+ userType: "user",
37
+ access: [],
38
+ }).output(z.object({ count: z.number() })),
45
39
 
46
40
  // Mark notification(s) as read
47
- markAsRead: _base
48
- .meta({
49
- userType: "user",
50
- })
41
+ markAsRead: proc({
42
+ operationType: "mutation",
43
+ userType: "user",
44
+ access: [],
45
+ })
51
46
  .input(
52
47
  z.object({
53
48
  notificationId: z.string().uuid().optional(), // If not provided, mark all as read
@@ -56,10 +51,11 @@ export const notificationContract = {
56
51
  .output(z.void()),
57
52
 
58
53
  // Delete a notification
59
- deleteNotification: _base
60
- .meta({
61
- userType: "user",
62
- })
54
+ deleteNotification: proc({
55
+ operationType: "mutation",
56
+ userType: "user",
57
+ access: [],
58
+ })
63
59
  .input(z.object({ notificationId: z.string().uuid() }))
64
60
  .output(z.void()),
65
61
 
@@ -68,61 +64,61 @@ export const notificationContract = {
68
64
  // ==========================================================================
69
65
 
70
66
  // Get all available notification groups
71
- getGroups: _base
72
- .meta({
73
- userType: "authenticated", // Services can read groups too
74
- })
75
- .output(z.array(NotificationGroupSchema)),
67
+ getGroups: proc({
68
+ operationType: "query",
69
+ userType: "authenticated",
70
+ access: [],
71
+ }).output(z.array(NotificationGroupSchema)),
76
72
 
77
73
  // Get current user's subscriptions with group details
78
- getSubscriptions: _base
79
- .meta({
80
- userType: "user",
81
- })
82
- .output(z.array(EnrichedSubscriptionSchema)),
74
+ getSubscriptions: proc({
75
+ operationType: "query",
76
+ userType: "user",
77
+ access: [],
78
+ }).output(z.array(EnrichedSubscriptionSchema)),
83
79
 
84
80
  // Subscribe to a notification group
85
- subscribe: _base
86
- .meta({
87
- userType: "user",
88
- })
81
+ subscribe: proc({
82
+ operationType: "mutation",
83
+ userType: "user",
84
+ access: [],
85
+ })
89
86
  .input(z.object({ groupId: z.string() }))
90
87
  .output(z.void()),
91
88
 
92
89
  // Unsubscribe from a notification group
93
- unsubscribe: _base
94
- .meta({
95
- userType: "user",
96
- })
90
+ unsubscribe: proc({
91
+ operationType: "mutation",
92
+ userType: "user",
93
+ access: [],
94
+ })
97
95
  .input(z.object({ groupId: z.string() }))
98
96
  .output(z.void()),
99
97
 
100
98
  // ==========================================================================
101
- // ADMIN SETTINGS ENDPOINTS (userType: "user" with admin permissions)
99
+ // ADMIN SETTINGS ENDPOINTS (userType: "user" with admin access)
102
100
  // ==========================================================================
103
101
 
104
102
  // Get retention schema for DynamicForm
105
- getRetentionSchema: _base
106
- .meta({
107
- userType: "user",
108
- permissions: [permissions.notificationAdmin.id],
109
- })
110
- .output(z.record(z.string(), z.unknown())),
103
+ getRetentionSchema: proc({
104
+ operationType: "query",
105
+ userType: "user",
106
+ access: [notificationAccess.admin],
107
+ }).output(z.record(z.string(), z.unknown())),
111
108
 
112
109
  // Get retention settings
113
- getRetentionSettings: _base
114
- .meta({
115
- userType: "user",
116
- permissions: [permissions.notificationAdmin.id],
117
- })
118
- .output(RetentionSettingsSchema),
110
+ getRetentionSettings: proc({
111
+ operationType: "query",
112
+ userType: "user",
113
+ access: [notificationAccess.admin],
114
+ }).output(RetentionSettingsSchema),
119
115
 
120
116
  // Update retention settings
121
- setRetentionSettings: _base
122
- .meta({
123
- userType: "user",
124
- permissions: [permissions.notificationAdmin.id],
125
- })
117
+ setRetentionSettings: proc({
118
+ operationType: "mutation",
119
+ userType: "user",
120
+ access: [notificationAccess.admin],
121
+ })
126
122
  .input(RetentionSettingsSchema)
127
123
  .output(z.void()),
128
124
 
@@ -131,8 +127,11 @@ export const notificationContract = {
131
127
  // ==========================================================================
132
128
 
133
129
  // Create a notification group (for plugins to register their groups)
134
- createGroup: _base
135
- .meta({ userType: "service" })
130
+ createGroup: proc({
131
+ operationType: "mutation",
132
+ userType: "service",
133
+ access: [],
134
+ })
136
135
  .input(
137
136
  z.object({
138
137
  groupId: z
@@ -150,8 +149,11 @@ export const notificationContract = {
150
149
  .output(z.object({ id: z.string() })),
151
150
 
152
151
  // Delete a notification group
153
- deleteGroup: _base
154
- .meta({ userType: "service" })
152
+ deleteGroup: proc({
153
+ operationType: "mutation",
154
+ userType: "service",
155
+ access: [],
156
+ })
155
157
  .input(
156
158
  z.object({
157
159
  groupId: z.string().describe("Full namespaced group ID to delete"),
@@ -163,8 +165,11 @@ export const notificationContract = {
163
165
  .output(z.object({ success: z.boolean() })),
164
166
 
165
167
  // Get subscribers for a specific notification group
166
- getGroupSubscribers: _base
167
- .meta({ userType: "service" })
168
+ getGroupSubscribers: proc({
169
+ operationType: "query",
170
+ userType: "service",
171
+ access: [],
172
+ })
168
173
  .input(
169
174
  z.object({
170
175
  groupId: z
@@ -175,16 +180,17 @@ export const notificationContract = {
175
180
  .output(z.object({ userIds: z.array(z.string()) })),
176
181
 
177
182
  // Send notifications to a list of users (deduplicated by caller)
178
- notifyUsers: _base
179
- .meta({ userType: "service" })
183
+ notifyUsers: proc({
184
+ operationType: "mutation",
185
+ userType: "service",
186
+ access: [],
187
+ })
180
188
  .input(
181
189
  z.object({
182
190
  userIds: z.array(z.string()),
183
191
  title: z.string(),
184
- /** Notification body in markdown format */
185
192
  body: z.string().describe("Notification body (supports markdown)"),
186
193
  importance: z.enum(["info", "warning", "critical"]).optional(),
187
- /** Primary action button */
188
194
  action: z
189
195
  .object({
190
196
  label: z.string(),
@@ -196,20 +202,19 @@ export const notificationContract = {
196
202
  .output(z.object({ notifiedCount: z.number() })),
197
203
 
198
204
  // Notify all subscribers of multiple groups (deduplicates internally)
199
- // Use this when an event affects multiple groups and you want to avoid
200
- // duplicate notifications for users subscribed to multiple affected groups.
201
- notifyGroups: _base
202
- .meta({ userType: "service" })
205
+ notifyGroups: proc({
206
+ operationType: "mutation",
207
+ userType: "service",
208
+ access: [],
209
+ })
203
210
  .input(
204
211
  z.object({
205
212
  groupIds: z
206
213
  .array(z.string())
207
214
  .describe("Full namespaced group IDs to notify"),
208
215
  title: z.string(),
209
- /** Notification body in markdown format */
210
216
  body: z.string().describe("Notification body (supports markdown)"),
211
217
  importance: z.enum(["info", "warning", "critical"]).optional(),
212
- /** Primary action button */
213
218
  action: z
214
219
  .object({
215
220
  label: z.string(),
@@ -220,11 +225,12 @@ export const notificationContract = {
220
225
  )
221
226
  .output(z.object({ notifiedCount: z.number() })),
222
227
 
223
- // Send transactional notification via ALL enabled strategies (no internal notification created)
224
- // For security-critical messages like password reset, 2FA, account verification, etc.
225
- // Unlike regular notifications, this bypasses user preferences and does not create a bell notification.
226
- sendTransactional: _base
227
- .meta({ userType: "service" })
228
+ // Send transactional notification via ALL enabled strategies
229
+ sendTransactional: proc({
230
+ operationType: "mutation",
231
+ userType: "service",
232
+ access: [],
233
+ })
228
234
  .input(
229
235
  z.object({
230
236
  userId: z.string().describe("User to notify"),
@@ -256,62 +262,57 @@ export const notificationContract = {
256
262
  ),
257
263
 
258
264
  // ==========================================================================
259
- // DELIVERY STRATEGY ADMIN ENDPOINTS (userType: "user" with admin permissions)
265
+ // DELIVERY STRATEGY ADMIN ENDPOINTS (userType: "user" with admin access)
260
266
  // ==========================================================================
261
267
 
262
268
  // Get all registered delivery strategies with current config
263
- getDeliveryStrategies: _base
264
- .meta({
265
- userType: "user",
266
- permissions: [permissions.notificationAdmin.id],
267
- })
268
- .output(
269
- z.array(
270
- z.object({
271
- qualifiedId: z.string(),
272
- displayName: z.string(),
273
- description: z.string().optional(),
274
- icon: z.string().optional(),
275
- ownerPluginId: z.string(),
276
- contactResolution: z.object({
277
- type: z.enum([
278
- "auth-email",
279
- "auth-provider",
280
- "user-config",
281
- "oauth-link",
282
- "custom",
283
- ]),
284
- provider: z.string().optional(),
285
- field: z.string().optional(),
286
- }),
287
- requiresUserConfig: z.boolean(),
288
- requiresOAuthLink: z.boolean(),
289
- configSchema: z.record(z.string(), z.unknown()),
290
- userConfigSchema: z.record(z.string(), z.unknown()).optional(),
291
- /** Layout config schema for admin customization (logo, colors, etc.) */
292
- layoutConfigSchema: z.record(z.string(), z.unknown()).optional(),
293
- enabled: z.boolean(),
294
- config: z.record(z.string(), z.unknown()).optional(),
295
- /** Current layout config values */
296
- layoutConfig: z.record(z.string(), z.unknown()).optional(),
297
- /** Markdown instructions for admins (setup guides, etc.) */
298
- adminInstructions: z.string().optional(),
299
- })
300
- )
301
- ),
269
+ getDeliveryStrategies: proc({
270
+ operationType: "query",
271
+ userType: "user",
272
+ access: [notificationAccess.admin],
273
+ }).output(
274
+ z.array(
275
+ z.object({
276
+ qualifiedId: z.string(),
277
+ displayName: z.string(),
278
+ description: z.string().optional(),
279
+ icon: z.string().optional(),
280
+ ownerPluginId: z.string(),
281
+ contactResolution: z.object({
282
+ type: z.enum([
283
+ "auth-email",
284
+ "auth-provider",
285
+ "user-config",
286
+ "oauth-link",
287
+ "custom",
288
+ ]),
289
+ provider: z.string().optional(),
290
+ field: z.string().optional(),
291
+ }),
292
+ requiresUserConfig: z.boolean(),
293
+ requiresOAuthLink: z.boolean(),
294
+ configSchema: z.record(z.string(), z.unknown()),
295
+ userConfigSchema: z.record(z.string(), z.unknown()).optional(),
296
+ layoutConfigSchema: z.record(z.string(), z.unknown()).optional(),
297
+ enabled: z.boolean(),
298
+ config: z.record(z.string(), z.unknown()).optional(),
299
+ layoutConfig: z.record(z.string(), z.unknown()).optional(),
300
+ adminInstructions: z.string().optional(),
301
+ })
302
+ )
303
+ ),
302
304
 
303
305
  // Update strategy enabled state and config
304
- updateDeliveryStrategy: _base
305
- .meta({
306
- userType: "user",
307
- permissions: [permissions.notificationAdmin.id],
308
- })
306
+ updateDeliveryStrategy: proc({
307
+ operationType: "mutation",
308
+ userType: "user",
309
+ access: [notificationAccess.admin],
310
+ })
309
311
  .input(
310
312
  z.object({
311
313
  strategyId: z.string().describe("Qualified strategy ID"),
312
314
  enabled: z.boolean(),
313
315
  config: z.record(z.string(), z.unknown()).optional(),
314
- /** Layout customization (logo, colors, footer) */
315
316
  layoutConfig: z.record(z.string(), z.unknown()).optional(),
316
317
  })
317
318
  )
@@ -322,7 +323,11 @@ export const notificationContract = {
322
323
  // ==========================================================================
323
324
 
324
325
  // Get available delivery channels for current user
325
- getUserDeliveryChannels: _base.meta({ userType: "user" }).output(
326
+ getUserDeliveryChannels: proc({
327
+ operationType: "query",
328
+ userType: "user",
329
+ access: [],
330
+ }).output(
326
331
  z.array(
327
332
  z.object({
328
333
  strategyId: z.string(),
@@ -340,19 +345,19 @@ export const notificationContract = {
340
345
  enabled: z.boolean(),
341
346
  isConfigured: z.boolean(),
342
347
  linkedAt: z.coerce.date().optional(),
343
- /** JSON Schema for user config (for DynamicForm) */
344
348
  userConfigSchema: z.record(z.string(), z.unknown()).optional(),
345
- /** Current user config values */
346
349
  userConfig: z.record(z.string(), z.unknown()).optional(),
347
- /** Markdown instructions for users (connection guides, etc.) */
348
350
  userInstructions: z.string().optional(),
349
351
  })
350
352
  )
351
353
  ),
352
354
 
353
355
  // Update user's preference for a delivery channel
354
- setUserDeliveryPreference: _base
355
- .meta({ userType: "user" })
356
+ setUserDeliveryPreference: proc({
357
+ operationType: "mutation",
358
+ userType: "user",
359
+ access: [],
360
+ })
356
361
  .input(
357
362
  z.object({
358
363
  strategyId: z.string(),
@@ -363,8 +368,11 @@ export const notificationContract = {
363
368
  .output(z.void()),
364
369
 
365
370
  // Get OAuth link URL for a strategy (starts OAuth flow)
366
- getDeliveryOAuthUrl: _base
367
- .meta({ userType: "user" })
371
+ getDeliveryOAuthUrl: proc({
372
+ operationType: "mutation",
373
+ userType: "user",
374
+ access: [],
375
+ })
368
376
  .input(
369
377
  z.object({
370
378
  strategyId: z.string(),
@@ -374,14 +382,20 @@ export const notificationContract = {
374
382
  .output(z.object({ authUrl: z.string() })),
375
383
 
376
384
  // Unlink OAuth-connected delivery channel
377
- unlinkDeliveryChannel: _base
378
- .meta({ userType: "user" })
385
+ unlinkDeliveryChannel: proc({
386
+ operationType: "mutation",
387
+ userType: "user",
388
+ access: [],
389
+ })
379
390
  .input(z.object({ strategyId: z.string() }))
380
391
  .output(z.void()),
381
392
 
382
393
  // Send a test notification to the current user via a specific strategy
383
- sendTestNotification: _base
384
- .meta({ userType: "user" })
394
+ sendTestNotification: proc({
395
+ operationType: "mutation",
396
+ userType: "user",
397
+ access: [],
398
+ })
385
399
  .input(z.object({ strategyId: z.string() }))
386
400
  .output(
387
401
  z.object({
@@ -1,12 +0,0 @@
1
- import { createPermission } from "@checkstack/common";
2
-
3
- export const permissions = {
4
- /** Configure retention policy and send broadcasts */
5
- notificationAdmin: createPermission(
6
- "notification",
7
- "manage",
8
- "Configure notification settings and send broadcasts"
9
- ),
10
- };
11
-
12
- export const permissionList = Object.values(permissions);