@rpcbase/server 0.525.0 → 0.526.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/email-Dzauaq11.js +12449 -0
- package/dist/email-Dzauaq11.js.map +1 -0
- package/dist/handler-BLwgdQv-.js +544 -0
- package/dist/handler-BLwgdQv-.js.map +1 -0
- package/dist/handler-Cq-OJ0Rf.js +182 -0
- package/dist/handler-Cq-OJ0Rf.js.map +1 -0
- package/dist/handler-Cq6MsoD4.js +124 -0
- package/dist/handler-Cq6MsoD4.js.map +1 -0
- package/dist/handler-Cx_ZP_NB.js +749 -0
- package/dist/handler-Cx_ZP_NB.js.map +1 -0
- package/dist/index.js +4783 -4935
- package/dist/index.js.map +1 -1
- package/dist/notifications.js +134 -199
- package/dist/notifications.js.map +1 -1
- package/dist/queryExecutor-DYVlCvns.js +295 -0
- package/dist/queryExecutor-DYVlCvns.js.map +1 -0
- package/dist/render_resend-CQb8_8G7.js +7 -0
- package/dist/render_resend-CQb8_8G7.js.map +1 -0
- package/dist/rts/index.js +581 -725
- package/dist/rts/index.js.map +1 -1
- package/dist/schemas-BR3K5Luo.js +3824 -0
- package/dist/schemas-BR3K5Luo.js.map +1 -0
- package/dist/shared-BfMSZm2P.js +87 -0
- package/dist/shared-BfMSZm2P.js.map +1 -0
- package/dist/ssrMiddleware.d.ts +1 -1
- package/dist/uploads.js +71 -84
- package/dist/uploads.js.map +1 -1
- package/package.json +9 -9
- package/dist/email-DK8uUU4X.js +0 -8045
- package/dist/email-DK8uUU4X.js.map +0 -1
- package/dist/handler-0rPClEv4.js +0 -663
- package/dist/handler-0rPClEv4.js.map +0 -1
- package/dist/handler-3uwH4f67.js +0 -924
- package/dist/handler-3uwH4f67.js.map +0 -1
- package/dist/handler-BsauvgjA.js +0 -153
- package/dist/handler-BsauvgjA.js.map +0 -1
- package/dist/handler-DnSJAQ_B.js +0 -203
- package/dist/handler-DnSJAQ_B.js.map +0 -1
- package/dist/queryExecutor-Bg1GGL3j.js +0 -407
- package/dist/queryExecutor-Bg1GGL3j.js.map +0 -1
- package/dist/render_resend_false-MiC__Smr.js +0 -6
- package/dist/render_resend_false-MiC__Smr.js.map +0 -1
- package/dist/schemas-Cjdjgehl.js +0 -4225
- package/dist/schemas-Cjdjgehl.js.map +0 -1
- package/dist/shared-BqZiSOmf.js +0 -111
- package/dist/shared-BqZiSOmf.js.map +0 -1
package/dist/notifications.js
CHANGED
|
@@ -1,206 +1,141 @@
|
|
|
1
|
+
import { t as sendEmail } from "./email-Dzauaq11.js";
|
|
1
2
|
import { models } from "@rpcbase/db";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
acc[path.replace("./api/", "@rpcbase/server/notifications/api/")] = mod;
|
|
7
|
-
return acc;
|
|
3
|
+
//#region src/notifications/routes.ts
|
|
4
|
+
var routes = Object.entries({ .../* @__PURE__ */ Object.assign({ "./api/notifications/handler.ts": () => import("./handler-BLwgdQv-.js") }) }).reduce((acc, [path, mod]) => {
|
|
5
|
+
acc[path.replace("./api/", "@rpcbase/server/notifications/api/")] = mod;
|
|
6
|
+
return acc;
|
|
8
7
|
}, {});
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
title,
|
|
31
|
-
...body ? {
|
|
32
|
-
body
|
|
33
|
-
} : {},
|
|
34
|
-
...url ? {
|
|
35
|
-
url
|
|
36
|
-
} : {},
|
|
37
|
-
...input.metadata ? {
|
|
38
|
-
metadata: input.metadata
|
|
39
|
-
} : {}
|
|
40
|
-
});
|
|
41
|
-
return {
|
|
42
|
-
id: doc._id.toString()
|
|
43
|
-
};
|
|
8
|
+
//#endregion
|
|
9
|
+
//#region src/notifications/createNotification.ts
|
|
10
|
+
var createNotification = async (ctx, input, ability) => {
|
|
11
|
+
const userId = input.userId.trim();
|
|
12
|
+
const title = input.title.trim();
|
|
13
|
+
if (!userId) throw new Error("createNotification: userId is required");
|
|
14
|
+
if (!title) throw new Error("createNotification: title is required");
|
|
15
|
+
const topic = typeof input.topic === "string" ? input.topic.trim() : "";
|
|
16
|
+
const body = typeof input.body === "string" ? input.body.trim() : "";
|
|
17
|
+
const url = typeof input.url === "string" ? input.url.trim() : "";
|
|
18
|
+
return { id: (await (await models.get("RBNotification", {
|
|
19
|
+
req: ctx.req,
|
|
20
|
+
ability
|
|
21
|
+
})).create({
|
|
22
|
+
userId,
|
|
23
|
+
...topic ? { topic } : {},
|
|
24
|
+
title,
|
|
25
|
+
...body ? { body } : {},
|
|
26
|
+
...url ? { url } : {},
|
|
27
|
+
...input.metadata ? { metadata: input.metadata } : {}
|
|
28
|
+
}))._id.toString() };
|
|
44
29
|
};
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
30
|
+
//#endregion
|
|
31
|
+
//#region src/notifications/digest.ts
|
|
32
|
+
var DAY_MS = 1440 * 60 * 1e3;
|
|
33
|
+
var WEEK_MS = 7 * DAY_MS;
|
|
34
|
+
var getDigestWindowMs = (frequency) => {
|
|
35
|
+
if (frequency === "daily") return DAY_MS;
|
|
36
|
+
if (frequency === "weekly") return WEEK_MS;
|
|
37
|
+
return 0;
|
|
51
38
|
};
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
39
|
+
var formatIso = (value) => {
|
|
40
|
+
if (!(value instanceof Date)) return void 0;
|
|
41
|
+
return value.toISOString();
|
|
55
42
|
};
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
return record;
|
|
43
|
+
var buildPreferencesByTopic = (settings) => {
|
|
44
|
+
const record = {};
|
|
45
|
+
const raw = settings?.topicPreferences;
|
|
46
|
+
if (!Array.isArray(raw)) return record;
|
|
47
|
+
for (const pref of raw) {
|
|
48
|
+
if (!pref || typeof pref !== "object") continue;
|
|
49
|
+
const topic = typeof pref.topic === "string" ? pref.topic.trim() : "";
|
|
50
|
+
if (!topic) continue;
|
|
51
|
+
record[topic] = { emailDigest: pref.emailDigest === true };
|
|
52
|
+
}
|
|
53
|
+
return record;
|
|
70
54
|
};
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
return {
|
|
154
|
-
ok: true,
|
|
155
|
-
sent: false,
|
|
156
|
-
skippedReason: "missing_email"
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
const subject = "Notifications digest";
|
|
160
|
-
const rows = notifications.map((n) => {
|
|
161
|
-
const title = typeof n.title === "string" ? n.title : "";
|
|
162
|
-
const body = typeof n.body === "string" ? n.body : "";
|
|
163
|
-
const url = typeof n.url === "string" ? n.url : "";
|
|
164
|
-
const createdAt = formatIso(n.createdAt) ?? "";
|
|
165
|
-
const line = url ? `<li><strong>${title}</strong><br>${body}<br><a href="${url}">${url}</a><br><small>${createdAt}</small></li>` : `<li><strong>${title}</strong><br>${body}<br><small>${createdAt}</small></li>`;
|
|
166
|
-
return line;
|
|
167
|
-
}).join("");
|
|
168
|
-
const html = `<div><p>Here is your notifications digest:</p><ul>${rows}</ul></div>`;
|
|
169
|
-
const emailResult = await sendEmail({
|
|
170
|
-
to: email,
|
|
171
|
-
subject,
|
|
172
|
-
html
|
|
173
|
-
});
|
|
174
|
-
if (emailResult.error) {
|
|
175
|
-
return {
|
|
176
|
-
ok: false,
|
|
177
|
-
error: emailResult.error
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
await SettingsModel.updateOne({
|
|
181
|
-
userId
|
|
182
|
-
}, {
|
|
183
|
-
$set: {
|
|
184
|
-
lastDigestSentAt: now
|
|
185
|
-
},
|
|
186
|
-
$setOnInsert: {
|
|
187
|
-
userId,
|
|
188
|
-
digestFrequency: "weekly"
|
|
189
|
-
}
|
|
190
|
-
}, {
|
|
191
|
-
upsert: true
|
|
192
|
-
});
|
|
193
|
-
return {
|
|
194
|
-
ok: true,
|
|
195
|
-
sent: emailResult.skipped !== true,
|
|
196
|
-
...emailResult.skipped ? {
|
|
197
|
-
skippedReason: "email_skipped"
|
|
198
|
-
} : {}
|
|
199
|
-
};
|
|
55
|
+
var sendNotificationsDigestForUser = async (ctx, { userId, ability, force = false }) => {
|
|
56
|
+
const modelCtx = {
|
|
57
|
+
req: ctx.req,
|
|
58
|
+
ability
|
|
59
|
+
};
|
|
60
|
+
const SettingsModel = await models.get("RBNotificationSettings", modelCtx);
|
|
61
|
+
const NotificationModel = await models.get("RBNotification", modelCtx);
|
|
62
|
+
const settings = await SettingsModel.findOne({ userId }).lean();
|
|
63
|
+
const digestFrequencyRaw = typeof settings?.digestFrequency === "string" ? settings.digestFrequency : "weekly";
|
|
64
|
+
const digestFrequency = digestFrequencyRaw === "daily" || digestFrequencyRaw === "weekly" || digestFrequencyRaw === "off" ? digestFrequencyRaw : "weekly";
|
|
65
|
+
if (digestFrequency === "off") return {
|
|
66
|
+
ok: true,
|
|
67
|
+
sent: false,
|
|
68
|
+
skippedReason: "digest_off"
|
|
69
|
+
};
|
|
70
|
+
const now = /* @__PURE__ */ new Date();
|
|
71
|
+
const windowMs = getDigestWindowMs(digestFrequency);
|
|
72
|
+
const lastSentAt = settings?.lastDigestSentAt instanceof Date ? settings.lastDigestSentAt : null;
|
|
73
|
+
const since = lastSentAt ?? new Date(now.getTime() - windowMs);
|
|
74
|
+
if (!force && lastSentAt && now.getTime() - lastSentAt.getTime() < windowMs) return {
|
|
75
|
+
ok: true,
|
|
76
|
+
sent: false,
|
|
77
|
+
skippedReason: "not_due"
|
|
78
|
+
};
|
|
79
|
+
const preferencesByTopic = buildPreferencesByTopic(settings);
|
|
80
|
+
const disabledTopics = Object.entries(preferencesByTopic).filter(([, pref]) => pref.emailDigest === false).map(([topic]) => topic);
|
|
81
|
+
const query = {
|
|
82
|
+
userId,
|
|
83
|
+
archivedAt: { $exists: false },
|
|
84
|
+
readAt: { $exists: false },
|
|
85
|
+
createdAt: { $gt: since }
|
|
86
|
+
};
|
|
87
|
+
if (disabledTopics.length > 0) query.topic = { $nin: disabledTopics };
|
|
88
|
+
const notifications = await NotificationModel.find(query).sort({ createdAt: -1 }).limit(50).lean();
|
|
89
|
+
if (!notifications.length) {
|
|
90
|
+
await SettingsModel.updateOne({ userId }, {
|
|
91
|
+
$set: { lastDigestSentAt: now },
|
|
92
|
+
$setOnInsert: {
|
|
93
|
+
userId,
|
|
94
|
+
digestFrequency: "weekly"
|
|
95
|
+
}
|
|
96
|
+
}, { upsert: true });
|
|
97
|
+
return {
|
|
98
|
+
ok: true,
|
|
99
|
+
sent: false,
|
|
100
|
+
skippedReason: "empty"
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
const user = await (await models.getGlobal("RBUser", ctx)).findById(userId, { email: 1 }).lean();
|
|
104
|
+
const email = typeof user?.email === "string" ? user.email.trim() : "";
|
|
105
|
+
if (!email) return {
|
|
106
|
+
ok: true,
|
|
107
|
+
sent: false,
|
|
108
|
+
skippedReason: "missing_email"
|
|
109
|
+
};
|
|
110
|
+
const emailResult = await sendEmail({
|
|
111
|
+
to: email,
|
|
112
|
+
subject: "Notifications digest",
|
|
113
|
+
html: `<div><p>Here is your notifications digest:</p><ul>${notifications.map((n) => {
|
|
114
|
+
const title = typeof n.title === "string" ? n.title : "";
|
|
115
|
+
const body = typeof n.body === "string" ? n.body : "";
|
|
116
|
+
const url = typeof n.url === "string" ? n.url : "";
|
|
117
|
+
const createdAt = formatIso(n.createdAt) ?? "";
|
|
118
|
+
return url ? `<li><strong>${title}</strong><br>${body}<br><a href="${url}">${url}</a><br><small>${createdAt}</small></li>` : `<li><strong>${title}</strong><br>${body}<br><small>${createdAt}</small></li>`;
|
|
119
|
+
}).join("")}</ul></div>`
|
|
120
|
+
});
|
|
121
|
+
if (emailResult.error) return {
|
|
122
|
+
ok: false,
|
|
123
|
+
error: emailResult.error
|
|
124
|
+
};
|
|
125
|
+
await SettingsModel.updateOne({ userId }, {
|
|
126
|
+
$set: { lastDigestSentAt: now },
|
|
127
|
+
$setOnInsert: {
|
|
128
|
+
userId,
|
|
129
|
+
digestFrequency: "weekly"
|
|
130
|
+
}
|
|
131
|
+
}, { upsert: true });
|
|
132
|
+
return {
|
|
133
|
+
ok: true,
|
|
134
|
+
sent: emailResult.skipped !== true,
|
|
135
|
+
...emailResult.skipped ? { skippedReason: "email_skipped" } : {}
|
|
136
|
+
};
|
|
200
137
|
};
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
};
|
|
206
|
-
//# sourceMappingURL=notifications.js.map
|
|
138
|
+
//#endregion
|
|
139
|
+
export { createNotification, routes, sendNotificationsDigestForUser };
|
|
140
|
+
|
|
141
|
+
//# sourceMappingURL=notifications.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"notifications.js","sources":["../src/notifications/routes.ts","../src/notifications/createNotification.ts","../src/notifications/digest.ts"],"sourcesContent":["export const routes = Object.entries({\n ...import.meta.glob(\"./api/**/handler.ts\"),\n}).reduce<Record<string, unknown>>((acc, [path, mod]) => {\n acc[path.replace(\"./api/\", \"@rpcbase/server/notifications/api/\")] = mod\n return acc\n}, {})\n\n","import type { Ctx } from \"@rpcbase/api\"\nimport { models } from \"@rpcbase/db\"\nimport type { AppAbility } from \"@rpcbase/db/acl\"\n\n\nexport type CreateNotificationInput = {\n userId: string\n topic?: string\n title: string\n body?: string\n url?: string\n metadata?: Record<string, unknown>\n}\n\nexport const createNotification = async (\n ctx: Ctx,\n input: CreateNotificationInput,\n ability: AppAbility,\n): Promise<{ id: string }> => {\n const userId = input.userId.trim()\n const title = input.title.trim()\n if (!userId) {\n throw new Error(\"createNotification: userId is required\")\n }\n if (!title) {\n throw new Error(\"createNotification: title is required\")\n }\n\n const topic = typeof input.topic === \"string\" ? input.topic.trim() : \"\"\n const body = typeof input.body === \"string\" ? input.body.trim() : \"\"\n const url = typeof input.url === \"string\" ? input.url.trim() : \"\"\n\n const NotificationModel = await models.get(\"RBNotification\", { req: ctx.req, ability })\n const doc = await NotificationModel.create({\n userId,\n ...(topic ? { topic } : {}),\n title,\n ...(body ? { body } : {}),\n ...(url ? { url } : {}),\n ...(input.metadata ? { metadata: input.metadata } : {}),\n })\n\n return { id: doc._id.toString() }\n}\n","import type { Ctx } from \"@rpcbase/api\"\nimport { models, type IRBNotification, type IRBNotificationSettings } from \"@rpcbase/db\"\nimport type { AppAbility } from \"@rpcbase/db/acl\"\n\nimport { sendEmail } from \"../email\"\n\n\nconst DAY_MS = 24 * 60 * 60 * 1000\nconst WEEK_MS = 7 * DAY_MS\n\ntype DigestFrequency = \"off\" | \"daily\" | \"weekly\"\n\ntype NotificationDoc = IRBNotification & { _id: unknown }\ntype SettingsDoc = IRBNotificationSettings & { _id: unknown }\n\nconst getDigestWindowMs = (frequency: DigestFrequency): number => {\n if (frequency === \"daily\") return DAY_MS\n if (frequency === \"weekly\") return WEEK_MS\n return 0\n}\n\nconst formatIso = (value: unknown): string | undefined => {\n if (!(value instanceof Date)) return undefined\n return value.toISOString()\n}\n\nconst buildPreferencesByTopic = (settings: SettingsDoc | null): Record<string, { emailDigest: boolean }> => {\n const record: Record<string, { emailDigest: boolean }> = {}\n const raw = settings?.topicPreferences\n if (!Array.isArray(raw)) return record\n for (const pref of raw) {\n if (!pref || typeof pref !== \"object\") continue\n const topic = typeof (pref as { topic?: unknown }).topic === \"string\" ? (pref as { topic: string }).topic.trim() : \"\"\n if (!topic) continue\n const emailDigest = (pref as { emailDigest?: unknown }).emailDigest\n record[topic] = { emailDigest: emailDigest === true }\n }\n return record\n}\n\nexport const sendNotificationsDigestForUser = async (\n ctx: Ctx,\n {\n userId,\n ability,\n force = false,\n }: {\n userId: string\n ability: AppAbility\n force?: boolean\n },\n): Promise<{ ok: true; sent: boolean; skippedReason?: string } | { ok: false; error: string }> => {\n const modelCtx = { req: ctx.req, ability }\n const SettingsModel = await models.get(\"RBNotificationSettings\", modelCtx)\n const NotificationModel = await models.get(\"RBNotification\", modelCtx)\n\n const settings = (await SettingsModel.findOne({ userId }).lean()) as SettingsDoc | null\n\n const digestFrequencyRaw = typeof settings?.digestFrequency === \"string\" ? settings.digestFrequency : \"weekly\"\n const digestFrequency: DigestFrequency =\n digestFrequencyRaw === \"daily\" || digestFrequencyRaw === \"weekly\" || digestFrequencyRaw === \"off\"\n ? digestFrequencyRaw\n : \"weekly\"\n\n if (digestFrequency === \"off\") {\n return { ok: true, sent: false, skippedReason: \"digest_off\" }\n }\n\n const now = new Date()\n const windowMs = getDigestWindowMs(digestFrequency)\n const lastSentAt = settings?.lastDigestSentAt instanceof Date ? settings.lastDigestSentAt : null\n const since = lastSentAt ?? new Date(now.getTime() - windowMs)\n\n if (!force && lastSentAt && now.getTime() - lastSentAt.getTime() < windowMs) {\n return { ok: true, sent: false, skippedReason: \"not_due\" }\n }\n\n const preferencesByTopic = buildPreferencesByTopic(settings)\n const disabledTopics = Object.entries(preferencesByTopic)\n .filter(([, pref]) => pref.emailDigest === false)\n .map(([topic]) => topic)\n\n const query: Record<string, unknown> = {\n userId,\n archivedAt: { $exists: false },\n readAt: { $exists: false },\n createdAt: { $gt: since },\n }\n\n if (disabledTopics.length > 0) {\n query.topic = { $nin: disabledTopics }\n }\n\n const notifications = (await NotificationModel.find(query)\n .sort({ createdAt: -1 })\n .limit(50)\n .lean()) as NotificationDoc[]\n\n if (!notifications.length) {\n await SettingsModel.updateOne(\n { userId },\n { $set: { lastDigestSentAt: now }, $setOnInsert: { userId, digestFrequency: \"weekly\" } },\n { upsert: true },\n )\n return { ok: true, sent: false, skippedReason: \"empty\" }\n }\n\n const UserModel = await models.getGlobal(\"RBUser\", ctx)\n const user = (await UserModel.findById(userId, { email: 1 }).lean()) as { email?: unknown } | null\n const email = typeof user?.email === \"string\" ? user.email.trim() : \"\"\n if (!email) {\n return { ok: true, sent: false, skippedReason: \"missing_email\" }\n }\n\n const subject = \"Notifications digest\"\n const rows = notifications\n .map((n) => {\n const title = typeof n.title === \"string\" ? n.title : \"\"\n const body = typeof n.body === \"string\" ? n.body : \"\"\n const url = typeof n.url === \"string\" ? n.url : \"\"\n const createdAt = formatIso(n.createdAt) ?? \"\"\n const line = url\n ? `<li><strong>${title}</strong><br>${body}<br><a href=\"${url}\">${url}</a><br><small>${createdAt}</small></li>`\n : `<li><strong>${title}</strong><br>${body}<br><small>${createdAt}</small></li>`\n return line\n })\n .join(\"\")\n\n const html = `<div><p>Here is your notifications digest:</p><ul>${rows}</ul></div>`\n\n const emailResult = await sendEmail({ to: email, subject, html })\n if (emailResult.error) {\n return { ok: false, error: emailResult.error }\n }\n\n await SettingsModel.updateOne(\n { userId },\n { $set: { lastDigestSentAt: now }, $setOnInsert: { userId, digestFrequency: \"weekly\" } },\n { upsert: true },\n )\n\n return { ok: true, sent: emailResult.skipped !== true, ...(emailResult.skipped ? { skippedReason: \"email_skipped\" } : {}) }\n}\n"],"names":["routes","Object","entries","import","reduce","acc","path","mod","replace","createNotification","ctx","input","ability","userId","trim","title","Error","topic","body","url","NotificationModel","models","get","req","doc","create","metadata","id","_id","toString","DAY_MS","WEEK_MS","getDigestWindowMs","frequency","formatIso","value","Date","undefined","toISOString","buildPreferencesByTopic","settings","record","raw","topicPreferences","Array","isArray","pref","emailDigest","sendNotificationsDigestForUser","force","modelCtx","SettingsModel","findOne","lean","digestFrequencyRaw","digestFrequency","ok","sent","skippedReason","now","windowMs","lastSentAt","lastDigestSentAt","since","getTime","preferencesByTopic","disabledTopics","filter","map","query","archivedAt","$exists","readAt","createdAt","$gt","length","$nin","notifications","find","sort","limit","updateOne","$set","$setOnInsert","upsert","UserModel","getGlobal","user","findById","email","subject","rows","n","line","join","html","emailResult","sendEmail","to","error","skipped"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"notifications.js","names":["routes","Object","entries","import","meta","glob","reduce","Record","acc","path","mod","replace","Ctx","models","AppAbility","CreateNotificationInput","userId","topic","title","body","url","metadata","Record","createNotification","ctx","input","ability","Promise","id","trim","Error","NotificationModel","get","req","doc","create","_id","toString","Ctx","models","IRBNotification","IRBNotificationSettings","AppAbility","sendEmail","DAY_MS","WEEK_MS","DigestFrequency","NotificationDoc","_id","SettingsDoc","getDigestWindowMs","frequency","formatIso","value","Date","undefined","toISOString","buildPreferencesByTopic","settings","Record","emailDigest","record","raw","topicPreferences","Array","isArray","pref","topic","trim","sendNotificationsDigestForUser","ctx","userId","ability","force","Promise","ok","sent","skippedReason","error","modelCtx","req","SettingsModel","get","NotificationModel","findOne","lean","digestFrequencyRaw","digestFrequency","now","windowMs","lastSentAt","lastDigestSentAt","since","getTime","preferencesByTopic","disabledTopics","Object","entries","filter","map","query","archivedAt","$exists","readAt","createdAt","$gt","length","$nin","notifications","find","sort","limit","updateOne","$set","$setOnInsert","upsert","UserModel","getGlobal","user","findById","email","subject","rows","n","title","body","url","line","join","html","emailResult","to","skipped"],"sources":["../src/notifications/routes.ts","../src/notifications/createNotification.ts","../src/notifications/digest.ts"],"sourcesContent":["export const routes = Object.entries({\n ...import.meta.glob(\"./api/**/handler.ts\"),\n}).reduce<Record<string, unknown>>((acc, [path, mod]) => {\n acc[path.replace(\"./api/\", \"@rpcbase/server/notifications/api/\")] = mod\n return acc\n}, {})\n\n","import type { Ctx } from \"@rpcbase/api\"\nimport { models } from \"@rpcbase/db\"\nimport type { AppAbility } from \"@rpcbase/db/acl\"\n\n\nexport type CreateNotificationInput = {\n userId: string\n topic?: string\n title: string\n body?: string\n url?: string\n metadata?: Record<string, unknown>\n}\n\nexport const createNotification = async (\n ctx: Ctx,\n input: CreateNotificationInput,\n ability: AppAbility,\n): Promise<{ id: string }> => {\n const userId = input.userId.trim()\n const title = input.title.trim()\n if (!userId) {\n throw new Error(\"createNotification: userId is required\")\n }\n if (!title) {\n throw new Error(\"createNotification: title is required\")\n }\n\n const topic = typeof input.topic === \"string\" ? input.topic.trim() : \"\"\n const body = typeof input.body === \"string\" ? input.body.trim() : \"\"\n const url = typeof input.url === \"string\" ? input.url.trim() : \"\"\n\n const NotificationModel = await models.get(\"RBNotification\", { req: ctx.req, ability })\n const doc = await NotificationModel.create({\n userId,\n ...(topic ? { topic } : {}),\n title,\n ...(body ? { body } : {}),\n ...(url ? { url } : {}),\n ...(input.metadata ? { metadata: input.metadata } : {}),\n })\n\n return { id: doc._id.toString() }\n}\n","import type { Ctx } from \"@rpcbase/api\"\nimport { models, type IRBNotification, type IRBNotificationSettings } from \"@rpcbase/db\"\nimport type { AppAbility } from \"@rpcbase/db/acl\"\n\nimport { sendEmail } from \"../email\"\n\n\nconst DAY_MS = 24 * 60 * 60 * 1000\nconst WEEK_MS = 7 * DAY_MS\n\ntype DigestFrequency = \"off\" | \"daily\" | \"weekly\"\n\ntype NotificationDoc = IRBNotification & { _id: unknown }\ntype SettingsDoc = IRBNotificationSettings & { _id: unknown }\n\nconst getDigestWindowMs = (frequency: DigestFrequency): number => {\n if (frequency === \"daily\") return DAY_MS\n if (frequency === \"weekly\") return WEEK_MS\n return 0\n}\n\nconst formatIso = (value: unknown): string | undefined => {\n if (!(value instanceof Date)) return undefined\n return value.toISOString()\n}\n\nconst buildPreferencesByTopic = (settings: SettingsDoc | null): Record<string, { emailDigest: boolean }> => {\n const record: Record<string, { emailDigest: boolean }> = {}\n const raw = settings?.topicPreferences\n if (!Array.isArray(raw)) return record\n for (const pref of raw) {\n if (!pref || typeof pref !== \"object\") continue\n const topic = typeof (pref as { topic?: unknown }).topic === \"string\" ? (pref as { topic: string }).topic.trim() : \"\"\n if (!topic) continue\n const emailDigest = (pref as { emailDigest?: unknown }).emailDigest\n record[topic] = { emailDigest: emailDigest === true }\n }\n return record\n}\n\nexport const sendNotificationsDigestForUser = async (\n ctx: Ctx,\n {\n userId,\n ability,\n force = false,\n }: {\n userId: string\n ability: AppAbility\n force?: boolean\n },\n): Promise<{ ok: true; sent: boolean; skippedReason?: string } | { ok: false; error: string }> => {\n const modelCtx = { req: ctx.req, ability }\n const SettingsModel = await models.get(\"RBNotificationSettings\", modelCtx)\n const NotificationModel = await models.get(\"RBNotification\", modelCtx)\n\n const settings = (await SettingsModel.findOne({ userId }).lean()) as SettingsDoc | null\n\n const digestFrequencyRaw = typeof settings?.digestFrequency === \"string\" ? settings.digestFrequency : \"weekly\"\n const digestFrequency: DigestFrequency =\n digestFrequencyRaw === \"daily\" || digestFrequencyRaw === \"weekly\" || digestFrequencyRaw === \"off\"\n ? digestFrequencyRaw\n : \"weekly\"\n\n if (digestFrequency === \"off\") {\n return { ok: true, sent: false, skippedReason: \"digest_off\" }\n }\n\n const now = new Date()\n const windowMs = getDigestWindowMs(digestFrequency)\n const lastSentAt = settings?.lastDigestSentAt instanceof Date ? settings.lastDigestSentAt : null\n const since = lastSentAt ?? new Date(now.getTime() - windowMs)\n\n if (!force && lastSentAt && now.getTime() - lastSentAt.getTime() < windowMs) {\n return { ok: true, sent: false, skippedReason: \"not_due\" }\n }\n\n const preferencesByTopic = buildPreferencesByTopic(settings)\n const disabledTopics = Object.entries(preferencesByTopic)\n .filter(([, pref]) => pref.emailDigest === false)\n .map(([topic]) => topic)\n\n const query: Record<string, unknown> = {\n userId,\n archivedAt: { $exists: false },\n readAt: { $exists: false },\n createdAt: { $gt: since },\n }\n\n if (disabledTopics.length > 0) {\n query.topic = { $nin: disabledTopics }\n }\n\n const notifications = (await NotificationModel.find(query)\n .sort({ createdAt: -1 })\n .limit(50)\n .lean()) as NotificationDoc[]\n\n if (!notifications.length) {\n await SettingsModel.updateOne(\n { userId },\n { $set: { lastDigestSentAt: now }, $setOnInsert: { userId, digestFrequency: \"weekly\" } },\n { upsert: true },\n )\n return { ok: true, sent: false, skippedReason: \"empty\" }\n }\n\n const UserModel = await models.getGlobal(\"RBUser\", ctx)\n const user = (await UserModel.findById(userId, { email: 1 }).lean()) as { email?: unknown } | null\n const email = typeof user?.email === \"string\" ? user.email.trim() : \"\"\n if (!email) {\n return { ok: true, sent: false, skippedReason: \"missing_email\" }\n }\n\n const subject = \"Notifications digest\"\n const rows = notifications\n .map((n) => {\n const title = typeof n.title === \"string\" ? n.title : \"\"\n const body = typeof n.body === \"string\" ? n.body : \"\"\n const url = typeof n.url === \"string\" ? n.url : \"\"\n const createdAt = formatIso(n.createdAt) ?? \"\"\n const line = url\n ? `<li><strong>${title}</strong><br>${body}<br><a href=\"${url}\">${url}</a><br><small>${createdAt}</small></li>`\n : `<li><strong>${title}</strong><br>${body}<br><small>${createdAt}</small></li>`\n return line\n })\n .join(\"\")\n\n const html = `<div><p>Here is your notifications digest:</p><ul>${rows}</ul></div>`\n\n const emailResult = await sendEmail({ to: email, subject, html })\n if (emailResult.error) {\n return { ok: false, error: emailResult.error }\n }\n\n await SettingsModel.updateOne(\n { userId },\n { $set: { lastDigestSentAt: now }, $setOnInsert: { userId, digestFrequency: \"weekly\" } },\n { upsert: true },\n )\n\n return { ok: true, sent: emailResult.skipped !== true, ...(emailResult.skipped ? { skippedReason: \"email_skipped\" } : {}) }\n}\n"],"mappings":"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"}
|