@voltro/plugin-notifications 0.25.0 → 0.27.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/dist/index.d.ts CHANGED
@@ -127,6 +127,8 @@ export declare interface InboxItem {
127
127
  readonly body: string;
128
128
  readonly data: Record<string, unknown>;
129
129
  readonly readAt: string | null;
130
+ /** When the user cleared it out of the inbox. A different state from read. */
131
+ readonly archivedAt: string | null;
130
132
  readonly createdAt: string;
131
133
  readonly tenantId: string | null;
132
134
  }
@@ -139,6 +141,17 @@ export declare const inboxTable: Table<"_voltro_notification_inbox", FieldDefini
139
141
  readonly body: ColumnBuilder<string, "text", boolean>;
140
142
  readonly data: ColumnBuilder<Record<string, unknown>, "json", boolean>;
141
143
  readonly readAt: ColumnBuilder<Date | null, "timestamp", boolean>;
144
+ /**
145
+ * ARCHIVED is a different state from READ, and for a user the more important
146
+ * one: it is what empties the inbox.
147
+ *
148
+ * Reported as the reason an app could not adopt this plugin. `readAt` covers
149
+ * read and `status` on the delivery table is the delivery outcome
150
+ * ('sent' | 'failed' | 'skipped') — neither is an archive, and the word did
151
+ * not appear anywhere in this plugin's types. An inbox a user cannot clear is
152
+ * one they stop opening.
153
+ */
154
+ readonly archivedAt: ColumnBuilder<Date | null, "timestamp", boolean>;
142
155
  readonly createdAt: ColumnBuilder<Date, "timestamp", true>;
143
156
  readonly tenantId: ColumnBuilder<string | null, "text", boolean>;
144
157
  }>, true, "byInboxSubject">;
@@ -147,6 +160,35 @@ export declare const inboxTable: Table<"_voltro_notification_inbox", FieldDefini
147
160
  * midnight, e.g. 22:00→08:00). An empty window (start === end) is never active. */
148
161
  export declare const inQuietHours: (qh: QuietHours, at: Date) => boolean;
149
162
 
163
+ /**
164
+ * WHO an inbox belongs to.
165
+ *
166
+ * `subject.id` is the framework's answer and it is not always the app's. A
167
+ * reporter's addressing unit is the EMPLOYEE: a shift change, an absence
168
+ * request or a task reminder is addressed to a person, and a person does not
169
+ * necessarily have an auth user. Their numbers, on 14 670 rows:
170
+ *
171
+ * employee WITH a userId 4 677 representable
172
+ * employee WITHOUT one 668 NOT — 4 people, 516 of those rows read,
173
+ * most recent 2026-06-19
174
+ *
175
+ * The 668 are live traffic, read by someone, and unaddressable under a
176
+ * subject-only model. Worse is what comes after a migration: every producer
177
+ * would resolve employee → userId and silently deliver NOTHING for anyone
178
+ * without an auth user — the exact failure this plugin's own docstring warns
179
+ * about, one level up and structural rather than accidental.
180
+ *
181
+ * So the app supplies it, the same seam `auth.resolveScopes` already offers for
182
+ * this shape. Absent keeps today's behaviour exactly.
183
+ */
184
+ export declare const makeSubjectId: (resolve: NotificationsPluginOptions["resolveSubjectId"]) => (ctx: {
185
+ request: {
186
+ subject: {
187
+ id?: string | null;
188
+ };
189
+ };
190
+ }) => string;
191
+
150
192
  export declare const memoryNotificationStore: () => NotificationStore;
151
193
 
152
194
  /** The minute-of-day (0–1439) `at` falls on in the IANA `tz`. Uses `Intl` so no
@@ -166,6 +208,13 @@ export declare interface NotificationServiceShape {
166
208
  }) => Promise<ReadonlyArray<InboxItem>>;
167
209
  readonly unreadCount: (subjectId: string) => Promise<number>;
168
210
  readonly markRead: (id: string, subjectId: string) => Promise<boolean>;
211
+ /** Undo a read. An inbox without the way back is a one-way ratchet. */
212
+ readonly markUnread: (id: string, subjectId: string) => Promise<boolean>;
213
+ /** Read everything; returns how many rows changed. */
214
+ readonly markAllRead: (subjectId: string) => Promise<number>;
215
+ /** Archive / unarchive. A different state from read — and the one that
216
+ * empties the inbox, which is why it matters more to a user. */
217
+ readonly setArchived: (id: string, subjectId: string, archived: boolean) => Promise<boolean>;
169
218
  readonly getPreferences: (subjectId: string) => Promise<ReadonlyArray<ChannelPreference>>;
170
219
  readonly setPreference: (pref: ChannelPreference) => Promise<void>;
171
220
  readonly subscribe: (topic: string, subjectId: string, tenantId?: string | null) => Promise<void>;
@@ -180,6 +229,27 @@ export declare interface NotificationServiceShape {
180
229
  export declare const notificationsPlugin: (options?: NotificationsPluginOptions) => VoltroPlugin;
181
230
 
182
231
  export declare interface NotificationsPluginOptions {
232
+ /**
233
+ * Resolve WHO the caller's inbox belongs to, when that is not `subject.id`.
234
+ *
235
+ * The addressing unit is the app's, not the framework's. If yours is an
236
+ * employee, a member or a contact — something that need not have an auth user
237
+ * — return its id here and the whole surface (`inbox`, `unreadCount`,
238
+ * `markRead`, `archive`, preferences, quiet hours) follows.
239
+ *
240
+ * notificationsPlugin({ resolveSubjectId: (ctx) => resolveCallerEmployeeId(ctx) })
241
+ *
242
+ * Absent keeps `subject.id`, so nothing changes for an app whose units line
243
+ * up. Returning `undefined` for one caller falls back the same way rather
244
+ * than failing the read.
245
+ */
246
+ readonly resolveSubjectId?: (ctx: {
247
+ readonly request: {
248
+ readonly subject: {
249
+ readonly id?: string | null;
250
+ };
251
+ };
252
+ }) => string | undefined;
183
253
  /** Delivery channels. The in-app inbox channel is added automatically unless
184
254
  * you pass your own `inApp` channel. Default `[consoleChannel()]` + in-app. */
185
255
  readonly channels?: ReadonlyArray<Channel>;
@@ -198,7 +268,7 @@ export declare interface NotificationsPluginOptions {
198
268
  }
199
269
 
200
270
  export declare interface NotificationStore {
201
- readonly addInbox: (item: Omit<InboxItem, 'id' | 'readAt' | 'createdAt'> & {
271
+ readonly addInbox: (item: Omit<InboxItem, 'id' | 'readAt' | 'archivedAt' | 'createdAt'> & {
202
272
  id: string;
203
273
  createdAt: string;
204
274
  }) => Promise<void>;
@@ -207,6 +277,13 @@ export declare interface NotificationStore {
207
277
  limit?: number;
208
278
  }) => Promise<ReadonlyArray<InboxItem>>;
209
279
  readonly markRead: (id: string, subjectId: string) => Promise<boolean>;
280
+ /** Clear the read stamp — an inbox needs the way back, not just forward. */
281
+ readonly markUnread: (id: string, subjectId: string) => Promise<boolean>;
282
+ /** Read EVERYTHING for this subject. Returns how many rows changed. */
283
+ readonly markAllRead: (subjectId: string) => Promise<number>;
284
+ /** Archive / unarchive — a different state from read, and the one that
285
+ * actually empties the inbox. */
286
+ readonly setArchived: (id: string, subjectId: string, archived: boolean) => Promise<boolean>;
210
287
  readonly unreadCount: (subjectId: string) => Promise<number>;
211
288
  readonly getPreferences: (subjectId: string) => Promise<ReadonlyArray<ChannelPreference>>;
212
289
  readonly setPreference: (pref: ChannelPreference) => Promise<void>;