convex-notifications 1.3.0 → 1.5.1
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/client/index.d.ts +114 -10
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +239 -41
- package/dist/client/index.js.map +1 -1
- package/dist/client/types.d.ts +47 -6
- package/dist/client/types.d.ts.map +1 -1
- package/dist/component/_generated/api.d.ts +8 -0
- package/dist/component/_generated/api.d.ts.map +1 -1
- package/dist/component/_generated/api.js.map +1 -1
- package/dist/component/_generated/component.d.ts +50 -4
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/channels/index.d.ts +28 -0
- package/dist/component/channels/index.d.ts.map +1 -0
- package/dist/component/channels/index.js +28 -0
- package/dist/component/channels/index.js.map +1 -0
- package/dist/component/channels/types.d.ts +66 -0
- package/dist/component/channels/types.d.ts.map +1 -0
- package/dist/component/channels/types.js +8 -0
- package/dist/component/channels/types.js.map +1 -0
- package/dist/component/channels/validators.d.ts +53 -0
- package/dist/component/channels/validators.d.ts.map +1 -0
- package/dist/component/channels/validators.js +101 -0
- package/dist/component/channels/validators.js.map +1 -0
- package/dist/component/convex.config.d.ts +2 -2
- package/dist/component/convex.config.d.ts.map +1 -1
- package/dist/component/convex.config.js +8 -1
- package/dist/component/convex.config.js.map +1 -1
- package/dist/component/crons.d.ts +3 -0
- package/dist/component/crons.d.ts.map +1 -0
- package/dist/component/crons.js +26 -0
- package/dist/component/crons.js.map +1 -0
- package/dist/component/delivery.d.ts +15 -0
- package/dist/component/delivery.d.ts.map +1 -1
- package/dist/component/delivery.js +53 -1
- package/dist/component/delivery.js.map +1 -1
- package/dist/component/fallback.d.ts +101 -0
- package/dist/component/fallback.d.ts.map +1 -0
- package/dist/component/fallback.js +189 -0
- package/dist/component/fallback.js.map +1 -0
- package/dist/component/inbox.d.ts +26 -17
- package/dist/component/inbox.d.ts.map +1 -1
- package/dist/component/inbox.js +80 -31
- package/dist/component/inbox.js.map +1 -1
- package/dist/component/notifications.d.ts +47 -0
- package/dist/component/notifications.d.ts.map +1 -1
- package/dist/component/notifications.js +184 -0
- package/dist/component/notifications.js.map +1 -1
- package/dist/component/preferences.d.ts +4 -0
- package/dist/component/preferences.d.ts.map +1 -1
- package/dist/component/preferences.js +33 -14
- package/dist/component/preferences.js.map +1 -1
- package/dist/component/pushTokens.d.ts +4 -0
- package/dist/component/pushTokens.d.ts.map +1 -1
- package/dist/component/pushTokens.js +34 -13
- package/dist/component/pushTokens.js.map +1 -1
- package/dist/component/retry.d.ts +86 -0
- package/dist/component/retry.d.ts.map +1 -0
- package/dist/component/retry.js +176 -0
- package/dist/component/retry.js.map +1 -0
- package/dist/component/scheduled.d.ts +101 -0
- package/dist/component/scheduled.d.ts.map +1 -0
- package/dist/component/scheduled.js +177 -0
- package/dist/component/scheduled.js.map +1 -0
- package/dist/component/schema.d.ts +117 -4
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/schema.js +77 -4
- package/dist/component/schema.js.map +1 -1
- package/dist/component/validators.d.ts +160 -0
- package/dist/component/validators.d.ts.map +1 -0
- package/dist/component/validators.js +95 -0
- package/dist/component/validators.js.map +1 -0
- package/dist/component/webhooks/index.d.ts +33 -0
- package/dist/component/webhooks/index.d.ts.map +1 -0
- package/dist/component/webhooks/index.js +33 -0
- package/dist/component/webhooks/index.js.map +1 -0
- package/dist/component/webhooks/resend.d.ts +48 -0
- package/dist/component/webhooks/resend.d.ts.map +1 -0
- package/dist/component/webhooks/resend.js +164 -0
- package/dist/component/webhooks/resend.js.map +1 -0
- package/dist/component/webhooks/twilio.d.ts +48 -0
- package/dist/component/webhooks/twilio.d.ts.map +1 -0
- package/dist/component/webhooks/twilio.js +154 -0
- package/dist/component/webhooks/twilio.js.map +1 -0
- package/dist/react/index.d.ts +148 -4
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +154 -3
- package/dist/react/index.js.map +1 -1
- package/package.json +16 -2
- package/src/client/index.test.ts +14 -14
- package/src/client/index.ts +308 -52
- package/src/client/types.ts +46 -6
- package/src/component/_generated/api.ts +8 -0
- package/src/component/_generated/component.ts +61 -9
- package/src/component/channels/channels.test.ts +158 -0
- package/src/component/channels/index.ts +46 -0
- package/src/component/channels/types.ts +70 -0
- package/src/component/channels/validators.ts +119 -0
- package/src/component/convex.config.ts +10 -1
- package/src/component/crons.ts +51 -0
- package/src/component/delivery.test.ts +316 -0
- package/src/component/delivery.ts +61 -1
- package/src/component/fallback.test.ts +315 -0
- package/src/component/fallback.ts +218 -0
- package/src/component/inbox.test.ts +13 -14
- package/src/component/inbox.ts +96 -37
- package/src/component/notifications.ts +211 -0
- package/src/component/preferences.ts +41 -16
- package/src/component/pushTokens.ts +40 -13
- package/src/component/retry.test.ts +340 -0
- package/src/component/retry.ts +210 -0
- package/src/component/scheduled.test.ts +250 -0
- package/src/component/scheduled.ts +203 -0
- package/src/component/schema.ts +94 -4
- package/src/component/validators.ts +132 -0
- package/src/component/webhooks/index.ts +33 -0
- package/src/component/webhooks/resend.ts +237 -0
- package/src/component/webhooks/twilio.ts +201 -0
- package/src/react/index.ts +219 -7
package/dist/client/index.d.ts
CHANGED
|
@@ -3,8 +3,34 @@ import type { NotificationsOptions, NotificationDefinition, RunQueryCtx, RunMuta
|
|
|
3
3
|
import type { PushNotifications } from "@convex-dev/expo-push-notifications";
|
|
4
4
|
import type { Resend } from "@convex-dev/resend";
|
|
5
5
|
import type { Twilio } from "@convex-dev/twilio";
|
|
6
|
-
export type { NotificationsOptions, NotificationDefinition } from "./types.js";
|
|
6
|
+
export type { NotificationsOptions, NotificationDefinition, AuthIdentity } from "./types.js";
|
|
7
7
|
export type { ChannelTemplates, EmailTemplate, InboxTemplate, PushTemplate, SmsTemplate, ChannelConfig, EmailChannelConfig, PushChannelConfig, SmsChannelConfig, DeliveryResult, SendResult, RunQueryCtx, RunMutationCtx, RunActionCtx, } from "./types.js";
|
|
8
|
+
/**
|
|
9
|
+
* Create a typed notification definition.
|
|
10
|
+
*
|
|
11
|
+
* This helper function provides runtime validation and type inference
|
|
12
|
+
* for notification definitions.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* const welcomeNotification = createNotification({
|
|
17
|
+
* event: "user.welcome",
|
|
18
|
+
* dataValidator: v.object({ userName: v.string() }),
|
|
19
|
+
* category: "onboarding",
|
|
20
|
+
* channels: {
|
|
21
|
+
* inbox: {
|
|
22
|
+
* title: (data) => `Welcome, ${data.userName}!`,
|
|
23
|
+
* body: () => "Thanks for joining.",
|
|
24
|
+
* },
|
|
25
|
+
* email: {
|
|
26
|
+
* subject: (data) => `Welcome, ${data.userName}!`,
|
|
27
|
+
* body: (data) => `Hi ${data.userName}, welcome aboard!`,
|
|
28
|
+
* },
|
|
29
|
+
* },
|
|
30
|
+
* });
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export declare function createNotification<T>(definition: NotificationDefinition<T>): NotificationDefinition<T>;
|
|
8
34
|
/**
|
|
9
35
|
* Extended options that include child component clients for delivery.
|
|
10
36
|
*/
|
|
@@ -37,12 +63,21 @@ export declare class Notifications {
|
|
|
37
63
|
component: ComponentApi;
|
|
38
64
|
options: NotificationsWithChannelsOptions;
|
|
39
65
|
constructor(component: ComponentApi, options: NotificationsWithChannelsOptions);
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
66
|
+
/**
|
|
67
|
+
* Resolve the auth identity and return userId + optional tenantId.
|
|
68
|
+
*/
|
|
69
|
+
private resolveAuth;
|
|
70
|
+
/**
|
|
71
|
+
* Resolve channel config, which may be static or a per-tenant function.
|
|
72
|
+
*/
|
|
73
|
+
private resolveChannelConfig;
|
|
74
|
+
list(ctx: RunQueryCtx, paginationOpts: {
|
|
75
|
+
numItems: number;
|
|
76
|
+
cursor: string | null;
|
|
43
77
|
}): Promise<{
|
|
44
|
-
|
|
45
|
-
|
|
78
|
+
page: any[];
|
|
79
|
+
isDone: boolean;
|
|
80
|
+
continueCursor: string;
|
|
46
81
|
}>;
|
|
47
82
|
unreadCount(ctx: RunQueryCtx): Promise<number>;
|
|
48
83
|
markRead(ctx: RunMutationCtx, notificationId: string): Promise<null>;
|
|
@@ -70,6 +105,7 @@ export declare class Notifications {
|
|
|
70
105
|
getPushTokens(ctx: RunQueryCtx): Promise<{
|
|
71
106
|
_id: string;
|
|
72
107
|
_creationTime: number;
|
|
108
|
+
tenantId?: string;
|
|
73
109
|
userId: string;
|
|
74
110
|
token: string;
|
|
75
111
|
platform?: "ios" | "android" | "web";
|
|
@@ -79,6 +115,33 @@ export declare class Notifications {
|
|
|
79
115
|
* Delete a push token.
|
|
80
116
|
*/
|
|
81
117
|
deletePushToken(ctx: RunMutationCtx, token: string): Promise<boolean>;
|
|
118
|
+
/**
|
|
119
|
+
* Schedule a notification for future delivery.
|
|
120
|
+
*
|
|
121
|
+
* @returns The scheduled notification ID
|
|
122
|
+
*/
|
|
123
|
+
schedule<T>(ctx: RunMutationCtx, definition: NotificationDefinition<T>, args: {
|
|
124
|
+
userId: string;
|
|
125
|
+
tenantId?: string;
|
|
126
|
+
data: T;
|
|
127
|
+
scheduledFor: number | Date;
|
|
128
|
+
transactional?: boolean;
|
|
129
|
+
deduplicationKey?: string;
|
|
130
|
+
}): Promise<{
|
|
131
|
+
scheduledNotificationId: string;
|
|
132
|
+
}>;
|
|
133
|
+
/**
|
|
134
|
+
* Cancel a scheduled notification.
|
|
135
|
+
*
|
|
136
|
+
* @returns true if cancelled, false if not found or already processed
|
|
137
|
+
*/
|
|
138
|
+
cancelScheduled(ctx: RunMutationCtx, scheduledNotificationId: string): Promise<boolean>;
|
|
139
|
+
/**
|
|
140
|
+
* Get scheduled notifications for the current user.
|
|
141
|
+
*/
|
|
142
|
+
getScheduledNotifications(ctx: RunQueryCtx, opts?: {
|
|
143
|
+
status?: "pending" | "processing" | "sent" | "failed" | "cancelled";
|
|
144
|
+
}): Promise<any[]>;
|
|
82
145
|
/**
|
|
83
146
|
* Send a notification through all enabled channels.
|
|
84
147
|
*
|
|
@@ -86,6 +149,7 @@ export declare class Notifications {
|
|
|
86
149
|
*/
|
|
87
150
|
send<T>(ctx: RunMutationCtx | RunActionCtx, definition: NotificationDefinition<T>, args: {
|
|
88
151
|
userId: string;
|
|
152
|
+
tenantId?: string;
|
|
89
153
|
data: T;
|
|
90
154
|
transactional?: boolean;
|
|
91
155
|
deduplicationKey?: string;
|
|
@@ -121,12 +185,20 @@ export declare class Notifications {
|
|
|
121
185
|
/**
|
|
122
186
|
* List notifications for the current user (paginated)
|
|
123
187
|
*/
|
|
188
|
+
/**
|
|
189
|
+
* List notifications for the current user (paginated).
|
|
190
|
+
* Uses standard Convex pagination via convex-helpers paginator.
|
|
191
|
+
* Compatible with usePaginatedQuery on the client.
|
|
192
|
+
*/
|
|
124
193
|
list: import("convex/server").RegisteredQuery<"public", {
|
|
125
|
-
|
|
126
|
-
|
|
194
|
+
paginationOpts: {
|
|
195
|
+
numItems: number;
|
|
196
|
+
cursor: string | null;
|
|
197
|
+
};
|
|
127
198
|
}, Promise<{
|
|
128
|
-
|
|
129
|
-
|
|
199
|
+
page: any[];
|
|
200
|
+
isDone: boolean;
|
|
201
|
+
continueCursor: string;
|
|
130
202
|
}>>;
|
|
131
203
|
/**
|
|
132
204
|
* Get unread notification count for the current user
|
|
@@ -161,6 +233,38 @@ export declare class Notifications {
|
|
|
161
233
|
channel: string;
|
|
162
234
|
enabled: boolean;
|
|
163
235
|
}, Promise<string>>;
|
|
236
|
+
/**
|
|
237
|
+
* Register a push notification token for the current user
|
|
238
|
+
*/
|
|
239
|
+
registerPushToken: import("convex/server").RegisteredMutation<"public", {
|
|
240
|
+
token: string;
|
|
241
|
+
platform?: "ios" | "android" | "web";
|
|
242
|
+
deviceId?: string;
|
|
243
|
+
}, Promise<string>>;
|
|
244
|
+
/**
|
|
245
|
+
* Get all push tokens for the current user
|
|
246
|
+
*/
|
|
247
|
+
getPushTokens: import("convex/server").RegisteredQuery<"public", {}, Promise<{
|
|
248
|
+
_id: string;
|
|
249
|
+
_creationTime: number;
|
|
250
|
+
tenantId?: string;
|
|
251
|
+
userId: string;
|
|
252
|
+
token: string;
|
|
253
|
+
platform?: "ios" | "android" | "web";
|
|
254
|
+
deviceId?: string;
|
|
255
|
+
}[]>>;
|
|
256
|
+
/**
|
|
257
|
+
* Delete a push token for the current user
|
|
258
|
+
*/
|
|
259
|
+
deletePushToken: import("convex/server").RegisteredMutation<"public", {
|
|
260
|
+
token: string;
|
|
261
|
+
}, Promise<boolean>>;
|
|
262
|
+
/**
|
|
263
|
+
* Get delivery logs for a notification
|
|
264
|
+
*/
|
|
265
|
+
getDeliveryLogs: import("convex/server").RegisteredQuery<"public", {
|
|
266
|
+
notificationId: string;
|
|
267
|
+
}, Promise<any[]>>;
|
|
164
268
|
};
|
|
165
269
|
}
|
|
166
270
|
export default Notifications;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"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"}
|
package/dist/client/index.js
CHANGED
|
@@ -1,6 +1,55 @@
|
|
|
1
|
-
import { queryGeneric, mutationGeneric } from "convex/server";
|
|
1
|
+
import { queryGeneric, mutationGeneric, paginationOptsValidator } from "convex/server";
|
|
2
2
|
import { v } from "convex/values";
|
|
3
3
|
import { dispatchEmail, dispatchPush, dispatchSms, isActionContext, } from "./adapters.js";
|
|
4
|
+
/**
|
|
5
|
+
* Create a typed notification definition.
|
|
6
|
+
*
|
|
7
|
+
* This helper function provides runtime validation and type inference
|
|
8
|
+
* for notification definitions.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* const welcomeNotification = createNotification({
|
|
13
|
+
* event: "user.welcome",
|
|
14
|
+
* dataValidator: v.object({ userName: v.string() }),
|
|
15
|
+
* category: "onboarding",
|
|
16
|
+
* channels: {
|
|
17
|
+
* inbox: {
|
|
18
|
+
* title: (data) => `Welcome, ${data.userName}!`,
|
|
19
|
+
* body: () => "Thanks for joining.",
|
|
20
|
+
* },
|
|
21
|
+
* email: {
|
|
22
|
+
* subject: (data) => `Welcome, ${data.userName}!`,
|
|
23
|
+
* body: (data) => `Hi ${data.userName}, welcome aboard!`,
|
|
24
|
+
* },
|
|
25
|
+
* },
|
|
26
|
+
* });
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export function createNotification(definition) {
|
|
30
|
+
// Validate required fields
|
|
31
|
+
if (!definition.event || definition.event.trim() === "") {
|
|
32
|
+
throw new Error("Notification definition must have a non-empty 'event' name");
|
|
33
|
+
}
|
|
34
|
+
if (!definition.channels || Object.keys(definition.channels).length === 0) {
|
|
35
|
+
throw new Error("Notification definition must have at least one channel template");
|
|
36
|
+
}
|
|
37
|
+
// Validate that at least inbox is defined (required for all notifications)
|
|
38
|
+
if (!definition.channels.inbox) {
|
|
39
|
+
throw new Error("Notification definition must include an 'inbox' channel template");
|
|
40
|
+
}
|
|
41
|
+
return definition;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Parse an AuthIdentity into userId and optional tenantId.
|
|
45
|
+
* Supports both string (backwards compat) and { userId, tenantId } forms.
|
|
46
|
+
*/
|
|
47
|
+
function parseIdentity(identity) {
|
|
48
|
+
if (typeof identity === "string") {
|
|
49
|
+
return { userId: identity };
|
|
50
|
+
}
|
|
51
|
+
return { userId: identity.userId, tenantId: identity.tenantId };
|
|
52
|
+
}
|
|
4
53
|
export class Notifications {
|
|
5
54
|
component;
|
|
6
55
|
options;
|
|
@@ -8,52 +57,79 @@ export class Notifications {
|
|
|
8
57
|
this.component = component;
|
|
9
58
|
this.options = options;
|
|
10
59
|
}
|
|
11
|
-
|
|
12
|
-
|
|
60
|
+
/**
|
|
61
|
+
* Resolve the auth identity and return userId + optional tenantId.
|
|
62
|
+
*/
|
|
63
|
+
async resolveAuth(ctx) {
|
|
64
|
+
const identity = await this.options.auth(ctx);
|
|
65
|
+
return parseIdentity(identity);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Resolve channel config, which may be static or a per-tenant function.
|
|
69
|
+
*/
|
|
70
|
+
async resolveChannelConfig(tenantId) {
|
|
71
|
+
const channels = this.options.channels;
|
|
72
|
+
if (!channels)
|
|
73
|
+
return undefined;
|
|
74
|
+
if (typeof channels === "function") {
|
|
75
|
+
if (!tenantId) {
|
|
76
|
+
throw new Error("Channel config is a function but no tenantId provided. Use multi-tenant auth or pass tenantId explicitly.");
|
|
77
|
+
}
|
|
78
|
+
return await channels(tenantId);
|
|
79
|
+
}
|
|
80
|
+
return channels;
|
|
81
|
+
}
|
|
82
|
+
async list(ctx, paginationOpts) {
|
|
83
|
+
const { userId, tenantId } = await this.resolveAuth(ctx);
|
|
13
84
|
return await ctx.runQuery(this.component.inbox.list, {
|
|
85
|
+
tenantId,
|
|
14
86
|
userId,
|
|
15
|
-
|
|
87
|
+
paginationOpts,
|
|
16
88
|
});
|
|
17
89
|
}
|
|
18
90
|
async unreadCount(ctx) {
|
|
19
|
-
const userId = await this.
|
|
20
|
-
return await ctx.runQuery(this.component.inbox.unreadCount, { userId });
|
|
91
|
+
const { userId, tenantId } = await this.resolveAuth(ctx);
|
|
92
|
+
return await ctx.runQuery(this.component.inbox.unreadCount, { tenantId, userId });
|
|
21
93
|
}
|
|
22
94
|
async markRead(ctx, notificationId) {
|
|
23
|
-
const userId = await this.
|
|
95
|
+
const { userId, tenantId } = await this.resolveAuth(ctx);
|
|
24
96
|
return await ctx.runMutation(this.component.inbox.markRead, {
|
|
97
|
+
tenantId,
|
|
25
98
|
userId,
|
|
26
99
|
notificationId,
|
|
27
100
|
});
|
|
28
101
|
}
|
|
29
102
|
async markAllRead(ctx) {
|
|
30
|
-
const userId = await this.
|
|
103
|
+
const { userId, tenantId } = await this.resolveAuth(ctx);
|
|
31
104
|
return await ctx.runMutation(this.component.inbox.markAllRead, {
|
|
105
|
+
tenantId,
|
|
32
106
|
userId,
|
|
33
107
|
});
|
|
34
108
|
}
|
|
35
109
|
async archive(ctx, notificationId) {
|
|
36
|
-
const userId = await this.
|
|
110
|
+
const { userId, tenantId } = await this.resolveAuth(ctx);
|
|
37
111
|
return await ctx.runMutation(this.component.inbox.archive, {
|
|
112
|
+
tenantId,
|
|
38
113
|
userId,
|
|
39
114
|
notificationId,
|
|
40
115
|
});
|
|
41
116
|
}
|
|
42
117
|
async getPreferences(ctx) {
|
|
43
|
-
const userId = await this.
|
|
44
|
-
return await ctx.runQuery(this.component.preferences.getPreferences, { userId });
|
|
118
|
+
const { userId, tenantId } = await this.resolveAuth(ctx);
|
|
119
|
+
return await ctx.runQuery(this.component.preferences.getPreferences, { tenantId, userId });
|
|
45
120
|
}
|
|
46
121
|
async updatePreference(ctx, args) {
|
|
47
|
-
const userId = await this.
|
|
48
|
-
return await ctx.runMutation(this.component.preferences.updatePreference, { userId, ...args });
|
|
122
|
+
const { userId, tenantId } = await this.resolveAuth(ctx);
|
|
123
|
+
return await ctx.runMutation(this.component.preferences.updatePreference, { tenantId, userId, ...args });
|
|
49
124
|
}
|
|
50
125
|
/**
|
|
51
126
|
* Register a push notification token for a user.
|
|
52
127
|
* Uses the component's internal push token storage.
|
|
53
128
|
*/
|
|
54
129
|
async registerPushToken(ctx, args) {
|
|
55
|
-
const userId = await this.
|
|
130
|
+
const { userId, tenantId } = await this.resolveAuth(ctx);
|
|
56
131
|
return await ctx.runMutation(this.component.pushTokens.registerPushToken, {
|
|
132
|
+
tenantId,
|
|
57
133
|
userId,
|
|
58
134
|
token: args.token,
|
|
59
135
|
platform: args.platform,
|
|
@@ -64,8 +140,9 @@ export class Notifications {
|
|
|
64
140
|
* Get all push tokens for the current user.
|
|
65
141
|
*/
|
|
66
142
|
async getPushTokens(ctx) {
|
|
67
|
-
const userId = await this.
|
|
143
|
+
const { userId, tenantId } = await this.resolveAuth(ctx);
|
|
68
144
|
return await ctx.runQuery(this.component.pushTokens.getPushTokens, {
|
|
145
|
+
tenantId,
|
|
69
146
|
userId,
|
|
70
147
|
});
|
|
71
148
|
}
|
|
@@ -73,12 +150,78 @@ export class Notifications {
|
|
|
73
150
|
* Delete a push token.
|
|
74
151
|
*/
|
|
75
152
|
async deletePushToken(ctx, token) {
|
|
76
|
-
const userId = await this.
|
|
153
|
+
const { userId, tenantId } = await this.resolveAuth(ctx);
|
|
77
154
|
return await ctx.runMutation(this.component.pushTokens.deletePushToken, {
|
|
155
|
+
tenantId,
|
|
78
156
|
userId,
|
|
79
157
|
token,
|
|
80
158
|
});
|
|
81
159
|
}
|
|
160
|
+
/**
|
|
161
|
+
* Schedule a notification for future delivery.
|
|
162
|
+
*
|
|
163
|
+
* @returns The scheduled notification ID
|
|
164
|
+
*/
|
|
165
|
+
async schedule(ctx, definition, args) {
|
|
166
|
+
const data = args.data;
|
|
167
|
+
const scheduledFor = args.scheduledFor instanceof Date
|
|
168
|
+
? args.scheduledFor.getTime()
|
|
169
|
+
: args.scheduledFor;
|
|
170
|
+
// Validate scheduledFor is in the future
|
|
171
|
+
if (scheduledFor <= Date.now()) {
|
|
172
|
+
throw new Error("scheduledFor must be in the future");
|
|
173
|
+
}
|
|
174
|
+
// Render inbox template for storage
|
|
175
|
+
const inboxTemplate = definition.channels.inbox;
|
|
176
|
+
const title = inboxTemplate
|
|
177
|
+
? inboxTemplate.title(data)
|
|
178
|
+
: definition.event;
|
|
179
|
+
const body = inboxTemplate ? inboxTemplate.body(data) : "";
|
|
180
|
+
// Scope deduplication key to tenant (consistent with send())
|
|
181
|
+
const scopedDeduplicationKey = args.deduplicationKey && args.tenantId
|
|
182
|
+
? `${args.tenantId}:${args.deduplicationKey}`
|
|
183
|
+
: args.deduplicationKey;
|
|
184
|
+
const scheduledNotificationId = await ctx.runMutation(this.component.scheduled.scheduleNotification, {
|
|
185
|
+
tenantId: args.tenantId,
|
|
186
|
+
userId: args.userId,
|
|
187
|
+
event: definition.event,
|
|
188
|
+
category: definition.category,
|
|
189
|
+
title,
|
|
190
|
+
body,
|
|
191
|
+
data: args.data,
|
|
192
|
+
channels: definition.channels,
|
|
193
|
+
scheduledFor,
|
|
194
|
+
transactional: args.transactional,
|
|
195
|
+
deduplicationKey: scopedDeduplicationKey,
|
|
196
|
+
});
|
|
197
|
+
return { scheduledNotificationId };
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Cancel a scheduled notification.
|
|
201
|
+
*
|
|
202
|
+
* @returns true if cancelled, false if not found or already processed
|
|
203
|
+
*/
|
|
204
|
+
async cancelScheduled(ctx, scheduledNotificationId) {
|
|
205
|
+
const { userId, tenantId } = await this.resolveAuth(ctx);
|
|
206
|
+
return await ctx.runMutation(this.component.scheduled.cancelScheduledNotification, {
|
|
207
|
+
tenantId,
|
|
208
|
+
// scheduledNotificationId is a string returned by the component;
|
|
209
|
+
// Convex component IDs are serialized as strings across the boundary.
|
|
210
|
+
id: scheduledNotificationId,
|
|
211
|
+
userId,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Get scheduled notifications for the current user.
|
|
216
|
+
*/
|
|
217
|
+
async getScheduledNotifications(ctx, opts) {
|
|
218
|
+
const { userId, tenantId } = await this.resolveAuth(ctx);
|
|
219
|
+
return await ctx.runQuery(this.component.scheduled.getScheduledNotifications, {
|
|
220
|
+
tenantId,
|
|
221
|
+
userId,
|
|
222
|
+
status: opts?.status,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
82
225
|
/**
|
|
83
226
|
* Send a notification through all enabled channels.
|
|
84
227
|
*
|
|
@@ -86,10 +229,14 @@ export class Notifications {
|
|
|
86
229
|
*/
|
|
87
230
|
async send(ctx, definition, args) {
|
|
88
231
|
const data = args.data;
|
|
232
|
+
const tenantId = args.tenantId;
|
|
89
233
|
const deliveries = [];
|
|
90
|
-
// 1. Check deduplication
|
|
234
|
+
// 1. Check deduplication atomically (scope key to tenant if provided)
|
|
91
235
|
if (args.deduplicationKey) {
|
|
92
|
-
const
|
|
236
|
+
const scopedKey = tenantId
|
|
237
|
+
? `${tenantId}:${args.deduplicationKey}`
|
|
238
|
+
: args.deduplicationKey;
|
|
239
|
+
const isDuplicate = await ctx.runMutation(this.component.notifications.checkAndRecordDeduplication, { key: scopedKey, ttlSeconds: args.deduplicationTtlSeconds ?? 86400 });
|
|
93
240
|
if (isDuplicate) {
|
|
94
241
|
throw new Error("Duplicate notification suppressed by deduplication key");
|
|
95
242
|
}
|
|
@@ -101,6 +248,7 @@ export class Notifications {
|
|
|
101
248
|
: definition.event;
|
|
102
249
|
const body = inboxTemplate ? inboxTemplate.body(data) : "";
|
|
103
250
|
const notificationId = await ctx.runMutation(this.component.notifications.createNotification, {
|
|
251
|
+
tenantId,
|
|
104
252
|
userId: args.userId,
|
|
105
253
|
event: definition.event,
|
|
106
254
|
title,
|
|
@@ -108,13 +256,7 @@ export class Notifications {
|
|
|
108
256
|
data: args.data,
|
|
109
257
|
transactional: args.transactional,
|
|
110
258
|
});
|
|
111
|
-
// 3.
|
|
112
|
-
if (args.deduplicationKey) {
|
|
113
|
-
await ctx.runMutation(this.component.notifications.recordDeduplication, {
|
|
114
|
-
key: args.deduplicationKey,
|
|
115
|
-
ttlSeconds: args.deduplicationTtlSeconds ?? 86400,
|
|
116
|
-
});
|
|
117
|
-
}
|
|
259
|
+
// 3. Deduplication key already recorded atomically in step 1
|
|
118
260
|
// 4. Resolve enabled channels
|
|
119
261
|
const definedChannels = Object.keys(definition.channels);
|
|
120
262
|
let enabledChannels;
|
|
@@ -123,6 +265,7 @@ export class Notifications {
|
|
|
123
265
|
}
|
|
124
266
|
else {
|
|
125
267
|
enabledChannels = await ctx.runQuery(this.component.preferences.resolvePreferences, {
|
|
268
|
+
tenantId,
|
|
126
269
|
userId: args.userId,
|
|
127
270
|
event: definition.event,
|
|
128
271
|
category: definition.category,
|
|
@@ -133,7 +276,7 @@ export class Notifications {
|
|
|
133
276
|
for (const channel of enabledChannels) {
|
|
134
277
|
if (channel === "inbox")
|
|
135
278
|
continue;
|
|
136
|
-
const deliveryResult = await this.dispatchChannel(ctx, channel, definition, args.userId, data, notificationId);
|
|
279
|
+
const deliveryResult = await this.dispatchChannel(ctx, channel, definition, args.userId, data, notificationId, tenantId);
|
|
137
280
|
if (deliveryResult) {
|
|
138
281
|
deliveries.push(deliveryResult);
|
|
139
282
|
}
|
|
@@ -143,20 +286,22 @@ export class Notifications {
|
|
|
143
286
|
/**
|
|
144
287
|
* Dispatch a notification to a specific channel.
|
|
145
288
|
*/
|
|
146
|
-
async dispatchChannel(ctx, channel, definition, userId, data, notificationId) {
|
|
289
|
+
async dispatchChannel(ctx, channel, definition, userId, data, notificationId, tenantId) {
|
|
147
290
|
let rendered;
|
|
148
291
|
let result;
|
|
149
292
|
try {
|
|
293
|
+
// Resolve channel config (may be per-tenant)
|
|
294
|
+
const channelConfig = await this.resolveChannelConfig(tenantId);
|
|
150
295
|
if (channel === "email" && definition.channels.email) {
|
|
151
296
|
const emailTemplate = definition.channels.email;
|
|
152
|
-
const emailConfig =
|
|
297
|
+
const emailConfig = channelConfig?.email;
|
|
153
298
|
const resendClient = this.options.clients?.email;
|
|
154
299
|
// Resolve email address
|
|
155
300
|
const emailResolver = this.options.resolvers?.email;
|
|
156
301
|
if (!emailResolver) {
|
|
157
302
|
throw new Error("Email resolver not configured");
|
|
158
303
|
}
|
|
159
|
-
const toEmail = await emailResolver(ctx, userId);
|
|
304
|
+
const toEmail = await emailResolver(ctx, userId, tenantId);
|
|
160
305
|
if (!toEmail) {
|
|
161
306
|
return {
|
|
162
307
|
channel: "email",
|
|
@@ -164,10 +309,15 @@ export class Notifications {
|
|
|
164
309
|
error: "No email address for user",
|
|
165
310
|
};
|
|
166
311
|
}
|
|
312
|
+
// Resolve "from" address: sender resolver > template override > channel config default
|
|
313
|
+
let fromEmail = emailTemplate.from ?? emailConfig?.defaultFrom ?? "";
|
|
314
|
+
if (tenantId && this.options.senderResolvers?.email) {
|
|
315
|
+
fromEmail = await this.options.senderResolvers.email(ctx, tenantId);
|
|
316
|
+
}
|
|
167
317
|
// Support async html rendering (e.g., React Email)
|
|
168
318
|
const html = emailTemplate.html ? await emailTemplate.html(data) : undefined;
|
|
169
319
|
const renderedEmail = {
|
|
170
|
-
from:
|
|
320
|
+
from: fromEmail,
|
|
171
321
|
to: toEmail,
|
|
172
322
|
subject: emailTemplate.subject(data),
|
|
173
323
|
body: emailTemplate.body(data),
|
|
@@ -175,7 +325,7 @@ export class Notifications {
|
|
|
175
325
|
};
|
|
176
326
|
rendered = renderedEmail;
|
|
177
327
|
if (!renderedEmail.from) {
|
|
178
|
-
throw new Error("No 'from' address configured. Set channels.email.defaultFrom or specify 'from' in the email template.");
|
|
328
|
+
throw new Error("No 'from' address configured. Set channels.email.defaultFrom, configure a senderResolver, or specify 'from' in the email template.");
|
|
179
329
|
}
|
|
180
330
|
// Dispatch via Resend if client is configured
|
|
181
331
|
if (resendClient) {
|
|
@@ -193,7 +343,7 @@ export class Notifications {
|
|
|
193
343
|
}
|
|
194
344
|
else if (channel === "push" && definition.channels.push) {
|
|
195
345
|
const pushTemplate = definition.channels.push;
|
|
196
|
-
const pushConfig =
|
|
346
|
+
const pushConfig = channelConfig?.push;
|
|
197
347
|
const pushClient = this.options.clients?.push;
|
|
198
348
|
const renderedPush = {
|
|
199
349
|
userId,
|
|
@@ -217,14 +367,14 @@ export class Notifications {
|
|
|
217
367
|
}
|
|
218
368
|
else if (channel === "sms" && definition.channels.sms) {
|
|
219
369
|
const smsTemplate = definition.channels.sms;
|
|
220
|
-
const smsConfig =
|
|
370
|
+
const smsConfig = channelConfig?.sms;
|
|
221
371
|
const twilioClient = this.options.clients?.sms;
|
|
222
372
|
// Resolve phone number
|
|
223
373
|
const phoneResolver = this.options.resolvers?.phone;
|
|
224
374
|
if (!phoneResolver) {
|
|
225
375
|
throw new Error("Phone resolver not configured");
|
|
226
376
|
}
|
|
227
|
-
const toPhone = await phoneResolver(ctx, userId);
|
|
377
|
+
const toPhone = await phoneResolver(ctx, userId, tenantId);
|
|
228
378
|
if (!toPhone) {
|
|
229
379
|
return {
|
|
230
380
|
channel: "sms",
|
|
@@ -232,14 +382,19 @@ export class Notifications {
|
|
|
232
382
|
error: "No phone number for user",
|
|
233
383
|
};
|
|
234
384
|
}
|
|
385
|
+
// Resolve "from" number: sender resolver > template override > channel config default
|
|
386
|
+
let fromPhone = smsTemplate.from ?? smsConfig?.defaultFrom ?? "";
|
|
387
|
+
if (tenantId && this.options.senderResolvers?.sms) {
|
|
388
|
+
fromPhone = await this.options.senderResolvers.sms(ctx, tenantId);
|
|
389
|
+
}
|
|
235
390
|
const renderedSms = {
|
|
236
|
-
from:
|
|
391
|
+
from: fromPhone,
|
|
237
392
|
to: toPhone,
|
|
238
393
|
body: smsTemplate.body(data),
|
|
239
394
|
};
|
|
240
395
|
rendered = renderedSms;
|
|
241
396
|
if (!renderedSms.from) {
|
|
242
|
-
throw new Error("No 'from' phone number configured. Set channels.sms.defaultFrom or specify 'from' in the SMS template.");
|
|
397
|
+
throw new Error("No 'from' phone number configured. Set channels.sms.defaultFrom, configure a senderResolver, or specify 'from' in the SMS template.");
|
|
243
398
|
}
|
|
244
399
|
// Dispatch via Twilio if client is configured AND we have action context
|
|
245
400
|
if (twilioClient) {
|
|
@@ -290,6 +445,7 @@ export class Notifications {
|
|
|
290
445
|
? "pending"
|
|
291
446
|
: "failed";
|
|
292
447
|
await ctx.runMutation(this.component.delivery.createDeliveryLog, {
|
|
448
|
+
tenantId,
|
|
293
449
|
notificationId,
|
|
294
450
|
channel,
|
|
295
451
|
status,
|
|
@@ -307,6 +463,7 @@ export class Notifications {
|
|
|
307
463
|
console.error(`[notifications] ${channel} dispatch failed:`, error);
|
|
308
464
|
// Create failed delivery log entry
|
|
309
465
|
await ctx.runMutation(this.component.delivery.createDeliveryLog, {
|
|
466
|
+
tenantId,
|
|
310
467
|
notificationId,
|
|
311
468
|
channel,
|
|
312
469
|
status: "failed",
|
|
@@ -357,16 +514,21 @@ export class Notifications {
|
|
|
357
514
|
/**
|
|
358
515
|
* List notifications for the current user (paginated)
|
|
359
516
|
*/
|
|
517
|
+
/**
|
|
518
|
+
* List notifications for the current user (paginated).
|
|
519
|
+
* Uses standard Convex pagination via convex-helpers paginator.
|
|
520
|
+
* Compatible with usePaginatedQuery on the client.
|
|
521
|
+
*/
|
|
360
522
|
list: queryGeneric({
|
|
361
523
|
args: {
|
|
362
|
-
|
|
363
|
-
cursor: v.optional(v.number()),
|
|
524
|
+
paginationOpts: paginationOptsValidator,
|
|
364
525
|
},
|
|
365
526
|
returns: v.object({
|
|
366
|
-
|
|
367
|
-
|
|
527
|
+
page: v.array(v.any()),
|
|
528
|
+
isDone: v.boolean(),
|
|
529
|
+
continueCursor: v.string(),
|
|
368
530
|
}),
|
|
369
|
-
handler: (ctx, args) => self.list(ctx, args),
|
|
531
|
+
handler: (ctx, args) => self.list(ctx, args.paginationOpts),
|
|
370
532
|
}),
|
|
371
533
|
/**
|
|
372
534
|
* Get unread notification count for the current user
|
|
@@ -421,6 +583,42 @@ export class Notifications {
|
|
|
421
583
|
returns: v.string(),
|
|
422
584
|
handler: (ctx, args) => self.updatePreference(ctx, args),
|
|
423
585
|
}),
|
|
586
|
+
/**
|
|
587
|
+
* Register a push notification token for the current user
|
|
588
|
+
*/
|
|
589
|
+
registerPushToken: mutationGeneric({
|
|
590
|
+
args: {
|
|
591
|
+
token: v.string(),
|
|
592
|
+
platform: v.optional(v.union(v.literal("ios"), v.literal("android"), v.literal("web"))),
|
|
593
|
+
deviceId: v.optional(v.string()),
|
|
594
|
+
},
|
|
595
|
+
returns: v.string(),
|
|
596
|
+
handler: (ctx, args) => self.registerPushToken(ctx, args),
|
|
597
|
+
}),
|
|
598
|
+
/**
|
|
599
|
+
* Get all push tokens for the current user
|
|
600
|
+
*/
|
|
601
|
+
getPushTokens: queryGeneric({
|
|
602
|
+
args: {},
|
|
603
|
+
returns: v.array(v.any()),
|
|
604
|
+
handler: (ctx) => self.getPushTokens(ctx),
|
|
605
|
+
}),
|
|
606
|
+
/**
|
|
607
|
+
* Delete a push token for the current user
|
|
608
|
+
*/
|
|
609
|
+
deletePushToken: mutationGeneric({
|
|
610
|
+
args: { token: v.string() },
|
|
611
|
+
returns: v.boolean(),
|
|
612
|
+
handler: (ctx, args) => self.deletePushToken(ctx, args.token),
|
|
613
|
+
}),
|
|
614
|
+
/**
|
|
615
|
+
* Get delivery logs for a notification
|
|
616
|
+
*/
|
|
617
|
+
getDeliveryLogs: queryGeneric({
|
|
618
|
+
args: { notificationId: v.string() },
|
|
619
|
+
returns: v.array(v.any()),
|
|
620
|
+
handler: (ctx, args) => self.getDeliveryLogs(ctx, args.notificationId),
|
|
621
|
+
}),
|
|
424
622
|
};
|
|
425
623
|
}
|
|
426
624
|
}
|