@rpcbase/server 0.593.0 → 0.595.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.
Files changed (63) hide show
  1. package/dist/convertHeifToWebp-BT1n72DQ.js +148 -0
  2. package/dist/convertHeifToWebp-BT1n72DQ.js.map +1 -0
  3. package/dist/email-Co1GNjlT.js +7204 -0
  4. package/dist/email-Co1GNjlT.js.map +1 -0
  5. package/dist/handler-BqlcQ9sE.js +245 -0
  6. package/dist/handler-BqlcQ9sE.js.map +1 -0
  7. package/dist/handler-C92_gJkX.js +743 -0
  8. package/dist/handler-C92_gJkX.js.map +1 -0
  9. package/dist/handler-CapJTGzc.js +795 -0
  10. package/dist/handler-CapJTGzc.js.map +1 -0
  11. package/dist/handler-Fk-5fOSm.js +119 -0
  12. package/dist/handler-Fk-5fOSm.js.map +1 -0
  13. package/dist/htmlTemplate.d.ts +16 -0
  14. package/dist/htmlTemplate.d.ts.map +1 -0
  15. package/dist/index.d.ts +1 -0
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +5140 -5286
  18. package/dist/index.js.map +1 -1
  19. package/dist/notifications.js +273 -382
  20. package/dist/notifications.js.map +1 -1
  21. package/dist/postProcessors-iffHvje4.js +89 -0
  22. package/dist/postProcessors-iffHvje4.js.map +1 -0
  23. package/dist/queryWindow-DP8zD0lb.js +555 -0
  24. package/dist/queryWindow-DP8zD0lb.js.map +1 -0
  25. package/dist/render_resend-B1SSQ4f7.js +7 -0
  26. package/dist/render_resend-B1SSQ4f7.js.map +1 -0
  27. package/dist/rts/index.d.ts +2 -1
  28. package/dist/rts/index.d.ts.map +1 -1
  29. package/dist/rts/index.js +990 -1197
  30. package/dist/rts/index.js.map +1 -1
  31. package/dist/schemas-B2fUvgYo.js +4151 -0
  32. package/dist/schemas-B2fUvgYo.js.map +1 -0
  33. package/dist/shared-DfrVDSp0.js +79 -0
  34. package/dist/shared-DfrVDSp0.js.map +1 -0
  35. package/dist/ssrMiddleware.d.ts +7 -3
  36. package/dist/ssrMiddleware.d.ts.map +1 -1
  37. package/dist/uploads/worker.js +139 -168
  38. package/dist/uploads/worker.js.map +1 -1
  39. package/dist/uploads.js +12 -22
  40. package/dist/uploads.js.map +1 -1
  41. package/package.json +1 -1
  42. package/dist/convertHeifToWebp-C-DGXZ2k.js +0 -169
  43. package/dist/convertHeifToWebp-C-DGXZ2k.js.map +0 -1
  44. package/dist/email-BCf24GmK.js +0 -8071
  45. package/dist/email-BCf24GmK.js.map +0 -1
  46. package/dist/handler-BqoKvylN.js +0 -147
  47. package/dist/handler-BqoKvylN.js.map +0 -1
  48. package/dist/handler-CUOJ51-w.js +0 -903
  49. package/dist/handler-CUOJ51-w.js.map +0 -1
  50. package/dist/handler-D-XdgeG_.js +0 -986
  51. package/dist/handler-D-XdgeG_.js.map +0 -1
  52. package/dist/handler-F0gFTzvh.js +0 -275
  53. package/dist/handler-F0gFTzvh.js.map +0 -1
  54. package/dist/postProcessors-D27fGZP0.js +0 -107
  55. package/dist/postProcessors-D27fGZP0.js.map +0 -1
  56. package/dist/queryWindow-Cdr7K-S1.js +0 -714
  57. package/dist/queryWindow-Cdr7K-S1.js.map +0 -1
  58. package/dist/render_resend_false-MiC__Smr.js +0 -6
  59. package/dist/render_resend_false-MiC__Smr.js.map +0 -1
  60. package/dist/schemas-Drf83ni9.js +0 -4517
  61. package/dist/schemas-Drf83ni9.js.map +0 -1
  62. package/dist/shared-CJrm9Wjp.js +0 -104
  63. package/dist/shared-CJrm9Wjp.js.map +0 -1
@@ -1,400 +1,291 @@
1
- import { randomUUID } from "node:crypto";
1
+ import { n as sendEmail } from "./email-Co1GNjlT.js";
2
2
  import { models } from "@rpcbase/db";
3
- import { s as sendEmail } from "./email-BCf24GmK.js";
4
- const routes = Object.entries({
5
- .../* @__PURE__ */ Object.assign({ "./api/notifications/handler.ts": () => import("./handler-CUOJ51-w.js") })
6
- }).reduce((acc, [path, mod]) => {
7
- acc[path.replace("./api/", "@rpcbase/server/notifications/api/")] = mod;
8
- return acc;
3
+ import { randomUUID } from "node:crypto";
4
+ //#region src/notifications/routes.ts
5
+ var routes = Object.entries({ .../* #__PURE__ */ Object.assign({ "./api/notifications/handler.ts": () => import("./handler-C92_gJkX.js") }) }).reduce((acc, [path, mod]) => {
6
+ acc[path.replace("./api/", "@rpcbase/server/notifications/api/")] = mod;
7
+ return acc;
9
8
  }, {});
10
- const normalizeRequiredString = (value, name) => {
11
- const normalized = typeof value === "string" ? value.trim() : "";
12
- if (!normalized) {
13
- throw new Error(`${name} is required`);
14
- }
15
- return normalized;
16
- };
17
- const normalizeOptionalString = (value) => {
18
- const normalized = typeof value === "string" ? value.trim() : "";
19
- return normalized || void 0;
9
+ //#endregion
10
+ //#region src/notifications/createNotification.ts
11
+ var normalizeRequiredString = (value, name) => {
12
+ const normalized = typeof value === "string" ? value.trim() : "";
13
+ if (!normalized) throw new Error(`${name} is required`);
14
+ return normalized;
20
15
  };
21
- const normalizeUrlMode = (value) => {
22
- return value === "merge_current_search" ? "merge_current_search" : "navigate";
16
+ var normalizeOptionalString = (value) => {
17
+ return (typeof value === "string" ? value.trim() : "") || void 0;
23
18
  };
24
- const isRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
25
- const toNotificationDataString = (value) => {
26
- if (typeof value === "string") {
27
- const trimmed = value.trim();
28
- return trimmed || void 0;
29
- }
30
- if (typeof value === "number" && Number.isFinite(value)) return String(value);
31
- if (typeof value === "boolean") return String(value);
32
- if (typeof value === "bigint") return value.toString();
33
- if (value instanceof Date && Number.isFinite(value.getTime())) return value.toISOString();
34
- return void 0;
19
+ var normalizeUrlMode = (value) => {
20
+ return value === "merge_current_search" ? "merge_current_search" : "navigate";
35
21
  };
36
- const normalizeNotificationActionLink = (url, urlMode) => {
37
- const normalizedUrl = normalizeOptionalString(url);
38
- if (!normalizedUrl) return void 0;
39
- if (urlMode === "navigate") return normalizedUrl;
40
- try {
41
- const parsed = new URL(normalizedUrl, "https://rpcbase.local/");
42
- return `${parsed.search}${parsed.hash}` || void 0;
43
- } catch {
44
- return normalizedUrl.startsWith("?") ? normalizedUrl : void 0;
45
- }
22
+ var isRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
23
+ var toNotificationDataString = (value) => {
24
+ if (typeof value === "string") return value.trim() || void 0;
25
+ if (typeof value === "number" && Number.isFinite(value)) return String(value);
26
+ if (typeof value === "boolean") return String(value);
27
+ if (typeof value === "bigint") return value.toString();
28
+ if (value instanceof Date && Number.isFinite(value.getTime())) return value.toISOString();
46
29
  };
47
- const buildNotificationData = (input) => {
48
- const data = {};
49
- for (const [key, value] of Object.entries(input.metadata ?? {})) {
50
- const normalized = toNotificationDataString(value);
51
- if (normalized) {
52
- data[key] = normalized;
53
- }
54
- }
55
- if (input.data) {
56
- Object.assign(data, input.data);
57
- }
58
- if (input.link) {
59
- data.link = input.link;
60
- if (input.urlMode !== "navigate") {
61
- data.linkMode = input.urlMode;
62
- }
63
- }
64
- return Object.keys(data).length > 0 ? data : void 0;
30
+ var normalizeNotificationActionLink = (url, urlMode) => {
31
+ const normalizedUrl = normalizeOptionalString(url);
32
+ if (!normalizedUrl) return void 0;
33
+ if (urlMode === "navigate") return normalizedUrl;
34
+ try {
35
+ const parsed = new URL(normalizedUrl, "https://rpcbase.local/");
36
+ return `${parsed.search}${parsed.hash}` || void 0;
37
+ } catch {
38
+ return normalizedUrl.startsWith("?") ? normalizedUrl : void 0;
39
+ }
65
40
  };
66
- const buildNotificationPlatform = (platform, link) => {
67
- if (!link) return platform;
68
- const webpush = isRecord(platform?.webpush) ? platform.webpush : {};
69
- const fcmOptions = isRecord(webpush.fcmOptions) ? webpush.fcmOptions : {};
70
- return {
71
- ...platform,
72
- webpush: {
73
- ...webpush,
74
- fcmOptions: {
75
- ...fcmOptions,
76
- link
77
- }
78
- }
79
- };
41
+ var buildNotificationData = (input) => {
42
+ const data = {};
43
+ for (const [key, value] of Object.entries(input.metadata ?? {})) {
44
+ const normalized = toNotificationDataString(value);
45
+ if (normalized) data[key] = normalized;
46
+ }
47
+ if (input.data) Object.assign(data, input.data);
48
+ if (input.link) {
49
+ data.link = input.link;
50
+ if (input.urlMode !== "navigate") data.linkMode = input.urlMode;
51
+ }
52
+ return Object.keys(data).length > 0 ? data : void 0;
80
53
  };
81
- const createNotificationDocument = (input) => {
82
- const userId = normalizeRequiredString(input.userId, "createNotification: userId");
83
- const title = normalizeRequiredString(input.title, "createNotification: title");
84
- const topic = normalizeOptionalString(input.topic);
85
- const tag = normalizeOptionalString(input.tag);
86
- const body = normalizeOptionalString(input.body);
87
- const image = normalizeOptionalString(input.image);
88
- const urlMode = normalizeUrlMode(input.urlMode);
89
- const link = normalizeNotificationActionLink(input.url, urlMode);
90
- const data = buildNotificationData({
91
- data: input.data,
92
- metadata: input.metadata,
93
- link,
94
- urlMode
95
- });
96
- const platform = buildNotificationPlatform(input.platform, link);
97
- return {
98
- userId,
99
- ...topic ? {
100
- topic
101
- } : {},
102
- ...tag ? {
103
- tag
104
- } : {},
105
- deliveryId: randomUUID(),
106
- title,
107
- ...body ? {
108
- body
109
- } : {},
110
- ...image ? {
111
- image
112
- } : {},
113
- ...data ? {
114
- data
115
- } : {},
116
- ...platform ? {
117
- platform
118
- } : {},
119
- drawerActive: true,
120
- createdAt: /* @__PURE__ */ new Date()
121
- };
54
+ var buildNotificationPlatform = (platform, link) => {
55
+ if (!link) return platform;
56
+ const webpush = isRecord(platform?.webpush) ? platform.webpush : {};
57
+ const fcmOptions = isRecord(webpush.fcmOptions) ? webpush.fcmOptions : {};
58
+ return {
59
+ ...platform,
60
+ webpush: {
61
+ ...webpush,
62
+ fcmOptions: {
63
+ ...fcmOptions,
64
+ link
65
+ }
66
+ }
67
+ };
122
68
  };
123
- const assertCreateNotificationsForUserInput = (input) => {
124
- if (typeof input.userId !== "string" || input.userId.trim().length === 0) {
125
- throw new Error("createNotificationsForUser: userId is required");
126
- }
127
- if (!Array.isArray(input.notifications)) {
128
- throw new Error("createNotificationsForUser: notifications must be an array");
129
- }
69
+ var createNotificationDocument = (input) => {
70
+ const userId = normalizeRequiredString(input.userId, "createNotification: userId");
71
+ const title = normalizeRequiredString(input.title, "createNotification: title");
72
+ const topic = normalizeOptionalString(input.topic);
73
+ const tag = normalizeOptionalString(input.tag);
74
+ const body = normalizeOptionalString(input.body);
75
+ const image = normalizeOptionalString(input.image);
76
+ const urlMode = normalizeUrlMode(input.urlMode);
77
+ const link = normalizeNotificationActionLink(input.url, urlMode);
78
+ const data = buildNotificationData({
79
+ data: input.data,
80
+ metadata: input.metadata,
81
+ link,
82
+ urlMode
83
+ });
84
+ const platform = buildNotificationPlatform(input.platform, link);
85
+ return {
86
+ userId,
87
+ ...topic ? { topic } : {},
88
+ ...tag ? { tag } : {},
89
+ deliveryId: randomUUID(),
90
+ title,
91
+ ...body ? { body } : {},
92
+ ...image ? { image } : {},
93
+ ...data ? { data } : {},
94
+ ...platform ? { platform } : {},
95
+ drawerActive: true,
96
+ createdAt: /* @__PURE__ */ new Date()
97
+ };
130
98
  };
131
- const createNotification = async (ctx, input, ability) => {
132
- if (!ability.can("create", "RBNotification")) {
133
- throw new Error("createNotification: forbidden");
134
- }
135
- const modelCtx = {
136
- req: ctx.req,
137
- ability
138
- };
139
- const [NotificationModel, NotificationDeliveryModel] = await Promise.all([models.getUnsafe("RBNotification", modelCtx), models.getUnsafe("RBNotificationDelivery", modelCtx)]);
140
- const notification = createNotificationDocument(input);
141
- const delivery = await NotificationDeliveryModel.create(notification);
142
- const doc = notification.tag ? await NotificationModel.findOneAndReplace({
143
- userId: notification.userId,
144
- tag: notification.tag,
145
- archivedAt: {
146
- $exists: false
147
- }
148
- }, notification, {
149
- new: true,
150
- upsert: true,
151
- sort: {
152
- drawerActive: -1,
153
- createdAt: -1
154
- }
155
- }) : await NotificationModel.create(notification);
156
- if (!doc) {
157
- throw new Error("createNotification: failed to persist notification");
158
- }
159
- await NotificationDeliveryModel.updateOne({
160
- _id: delivery._id
161
- }, {
162
- $set: {
163
- notificationId: doc._id.toString()
164
- }
165
- });
166
- return {
167
- id: doc._id.toString()
168
- };
99
+ var assertCreateNotificationsForUserInput = (input) => {
100
+ if (typeof input.userId !== "string" || input.userId.trim().length === 0) throw new Error("createNotificationsForUser: userId is required");
101
+ if (!Array.isArray(input.notifications)) throw new Error("createNotificationsForUser: notifications must be an array");
169
102
  };
170
- const createNotificationsForUser = async (ctx, input, ability) => {
171
- assertCreateNotificationsForUserInput(input);
172
- if (input.notifications.length === 0) {
173
- return {
174
- ids: []
175
- };
176
- }
177
- if (!ability.can("create", "RBNotification")) {
178
- throw new Error("createNotificationsForUser: forbidden");
179
- }
180
- const modelCtx = {
181
- req: ctx.req,
182
- ability
183
- };
184
- const [NotificationModel, NotificationDeliveryModel] = await Promise.all([models.getUnsafe("RBNotification", modelCtx), models.getUnsafe("RBNotificationDelivery", modelCtx)]);
185
- const ids = [];
186
- for (const notification of input.notifications) {
187
- const document = createNotificationDocument({
188
- ...notification,
189
- userId: input.userId
190
- });
191
- const delivery = await NotificationDeliveryModel.create(document);
192
- const doc = document.tag ? await NotificationModel.findOneAndReplace({
193
- userId: document.userId,
194
- tag: document.tag,
195
- archivedAt: {
196
- $exists: false
197
- }
198
- }, document, {
199
- new: true,
200
- upsert: true,
201
- sort: {
202
- drawerActive: -1,
203
- createdAt: -1
204
- }
205
- }) : await NotificationModel.create(document);
206
- if (!doc) {
207
- throw new Error("createNotificationsForUser: failed to persist notification");
208
- }
209
- await NotificationDeliveryModel.updateOne({
210
- _id: delivery._id
211
- }, {
212
- $set: {
213
- notificationId: doc._id.toString()
214
- }
215
- });
216
- ids.push(doc._id.toString());
217
- }
218
- return {
219
- ids
220
- };
103
+ var createNotification = async (ctx, input, ability) => {
104
+ if (!ability.can("create", "RBNotification")) throw new Error("createNotification: forbidden");
105
+ const modelCtx = {
106
+ req: ctx.req,
107
+ ability
108
+ };
109
+ const [NotificationModel, NotificationDeliveryModel] = await Promise.all([models.getUnsafe("RBNotification", modelCtx), models.getUnsafe("RBNotificationDelivery", modelCtx)]);
110
+ const notification = createNotificationDocument(input);
111
+ const delivery = await NotificationDeliveryModel.create(notification);
112
+ const doc = notification.tag ? await NotificationModel.findOneAndReplace({
113
+ userId: notification.userId,
114
+ tag: notification.tag,
115
+ archivedAt: { $exists: false }
116
+ }, notification, {
117
+ new: true,
118
+ upsert: true,
119
+ sort: {
120
+ drawerActive: -1,
121
+ createdAt: -1
122
+ }
123
+ }) : await NotificationModel.create(notification);
124
+ if (!doc) throw new Error("createNotification: failed to persist notification");
125
+ await NotificationDeliveryModel.updateOne({ _id: delivery._id }, { $set: { notificationId: doc._id.toString() } });
126
+ return { id: doc._id.toString() };
221
127
  };
222
- const DAY_MS = 24 * 60 * 60 * 1e3;
223
- const WEEK_MS = 7 * DAY_MS;
224
- const getDigestWindowMs = (frequency) => {
225
- if (frequency === "daily") return DAY_MS;
226
- if (frequency === "weekly") return WEEK_MS;
227
- return 0;
128
+ var createNotificationsForUser = async (ctx, input, ability) => {
129
+ assertCreateNotificationsForUserInput(input);
130
+ if (input.notifications.length === 0) return { ids: [] };
131
+ if (!ability.can("create", "RBNotification")) throw new Error("createNotificationsForUser: forbidden");
132
+ const modelCtx = {
133
+ req: ctx.req,
134
+ ability
135
+ };
136
+ const [NotificationModel, NotificationDeliveryModel] = await Promise.all([models.getUnsafe("RBNotification", modelCtx), models.getUnsafe("RBNotificationDelivery", modelCtx)]);
137
+ const ids = [];
138
+ for (const notification of input.notifications) {
139
+ const document = createNotificationDocument({
140
+ ...notification,
141
+ userId: input.userId
142
+ });
143
+ const delivery = await NotificationDeliveryModel.create(document);
144
+ const doc = document.tag ? await NotificationModel.findOneAndReplace({
145
+ userId: document.userId,
146
+ tag: document.tag,
147
+ archivedAt: { $exists: false }
148
+ }, document, {
149
+ new: true,
150
+ upsert: true,
151
+ sort: {
152
+ drawerActive: -1,
153
+ createdAt: -1
154
+ }
155
+ }) : await NotificationModel.create(document);
156
+ if (!doc) throw new Error("createNotificationsForUser: failed to persist notification");
157
+ await NotificationDeliveryModel.updateOne({ _id: delivery._id }, { $set: { notificationId: doc._id.toString() } });
158
+ ids.push(doc._id.toString());
159
+ }
160
+ return { ids };
228
161
  };
229
- const formatIso = (value) => {
230
- if (!(value instanceof Date)) return void 0;
231
- return value.toISOString();
162
+ //#endregion
163
+ //#region src/notifications/digest.ts
164
+ var DAY_MS = 1440 * 60 * 1e3;
165
+ var WEEK_MS = 7 * DAY_MS;
166
+ var getDigestWindowMs = (frequency) => {
167
+ if (frequency === "daily") return DAY_MS;
168
+ if (frequency === "weekly") return WEEK_MS;
169
+ return 0;
232
170
  };
233
- const getNotificationLink = (notification) => {
234
- const dataLink = notification.data?.link?.trim();
235
- if (dataLink) return dataLink;
236
- const platform = notification.platform;
237
- if (!platform || typeof platform !== "object" || Array.isArray(platform)) return "";
238
- const webpush = platform.webpush;
239
- if (!webpush || typeof webpush !== "object" || Array.isArray(webpush)) return "";
240
- const fcmOptions = webpush.fcmOptions;
241
- if (!fcmOptions || typeof fcmOptions !== "object" || Array.isArray(fcmOptions)) return "";
242
- const link = fcmOptions.link;
243
- return typeof link === "string" ? link.trim() : "";
171
+ var formatIso = (value) => {
172
+ if (!(value instanceof Date)) return void 0;
173
+ return value.toISOString();
244
174
  };
245
- const buildPreferencesByTopic = (settings) => {
246
- const record = {};
247
- const raw = settings?.topicPreferences;
248
- if (!Array.isArray(raw)) return record;
249
- for (const pref of raw) {
250
- if (!pref || typeof pref !== "object") continue;
251
- const topic = typeof pref.topic === "string" ? pref.topic.trim() : "";
252
- if (!topic) continue;
253
- const emailDigest = pref.emailDigest;
254
- record[topic] = {
255
- emailDigest: emailDigest === true
256
- };
257
- }
258
- return record;
175
+ var getNotificationLink = (notification) => {
176
+ const dataLink = notification.data?.link?.trim();
177
+ if (dataLink) return dataLink;
178
+ const platform = notification.platform;
179
+ if (!platform || typeof platform !== "object" || Array.isArray(platform)) return "";
180
+ const webpush = platform.webpush;
181
+ if (!webpush || typeof webpush !== "object" || Array.isArray(webpush)) return "";
182
+ const fcmOptions = webpush.fcmOptions;
183
+ if (!fcmOptions || typeof fcmOptions !== "object" || Array.isArray(fcmOptions)) return "";
184
+ const link = fcmOptions.link;
185
+ return typeof link === "string" ? link.trim() : "";
259
186
  };
260
- const sendNotificationsDigestForUser = async (ctx, {
261
- userId,
262
- ability,
263
- force = false
264
- }) => {
265
- const modelCtx = {
266
- req: ctx.req,
267
- ability
268
- };
269
- const SettingsModel = await models.get("RBNotificationSettings", modelCtx);
270
- const NotificationModel = await models.get("RBNotification", modelCtx);
271
- const settings = await SettingsModel.findOne({
272
- userId
273
- }).lean();
274
- const digestFrequencyRaw = typeof settings?.digestFrequency === "string" ? settings.digestFrequency : "weekly";
275
- const digestFrequency = digestFrequencyRaw === "daily" || digestFrequencyRaw === "weekly" || digestFrequencyRaw === "off" ? digestFrequencyRaw : "weekly";
276
- if (digestFrequency === "off") {
277
- return {
278
- ok: true,
279
- sent: false,
280
- skippedReason: "digest_off"
281
- };
282
- }
283
- const now = /* @__PURE__ */ new Date();
284
- const windowMs = getDigestWindowMs(digestFrequency);
285
- const lastSentAt = settings?.lastDigestSentAt instanceof Date ? settings.lastDigestSentAt : null;
286
- const since = lastSentAt ?? new Date(now.getTime() - windowMs);
287
- if (!force && lastSentAt && now.getTime() - lastSentAt.getTime() < windowMs) {
288
- return {
289
- ok: true,
290
- sent: false,
291
- skippedReason: "not_due"
292
- };
293
- }
294
- const preferencesByTopic = buildPreferencesByTopic(settings);
295
- const disabledTopics = Object.entries(preferencesByTopic).filter(([, pref]) => pref.emailDigest === false).map(([topic]) => topic);
296
- const query = {
297
- userId,
298
- archivedAt: {
299
- $exists: false
300
- },
301
- readAt: {
302
- $exists: false
303
- },
304
- createdAt: {
305
- $gt: since
306
- }
307
- };
308
- if (disabledTopics.length > 0) {
309
- query.topic = {
310
- $nin: disabledTopics
311
- };
312
- }
313
- const notifications = await NotificationModel.find(query).sort({
314
- createdAt: -1
315
- }).limit(50).lean();
316
- if (!notifications.length) {
317
- await SettingsModel.updateOne({
318
- userId
319
- }, {
320
- $set: {
321
- lastDigestSentAt: now
322
- },
323
- $setOnInsert: {
324
- userId,
325
- digestFrequency: "weekly"
326
- }
327
- }, {
328
- upsert: true
329
- });
330
- return {
331
- ok: true,
332
- sent: false,
333
- skippedReason: "empty"
334
- };
335
- }
336
- const UserModel = await models.getGlobal("RBUser", ctx);
337
- const user = await UserModel.findById(userId, {
338
- email: 1
339
- }).lean();
340
- const email = typeof user?.email === "string" ? user.email.trim() : "";
341
- if (!email) {
342
- return {
343
- ok: true,
344
- sent: false,
345
- skippedReason: "missing_email"
346
- };
347
- }
348
- const subject = "Notifications digest";
349
- const rows = notifications.map((n) => {
350
- const title = typeof n.title === "string" ? n.title : "";
351
- const body = typeof n.body === "string" ? n.body : "";
352
- const url = getNotificationLink(n);
353
- const createdAt = formatIso(n.createdAt) ?? "";
354
- 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>`;
355
- return line;
356
- }).join("");
357
- const html = `<div><p>Here is your notifications digest:</p><ul>${rows}</ul></div>`;
358
- let emailResult;
359
- try {
360
- emailResult = await sendEmail({
361
- to: email,
362
- subject,
363
- html
364
- }, {
365
- missingTransport: "skip"
366
- });
367
- } catch (error) {
368
- return {
369
- ok: false,
370
- error: error instanceof Error ? error.message : "Email delivery failed"
371
- };
372
- }
373
- await SettingsModel.updateOne({
374
- userId
375
- }, {
376
- $set: {
377
- lastDigestSentAt: now
378
- },
379
- $setOnInsert: {
380
- userId,
381
- digestFrequency: "weekly"
382
- }
383
- }, {
384
- upsert: true
385
- });
386
- return {
387
- ok: true,
388
- sent: emailResult.skipped !== true,
389
- ...emailResult.skipped ? {
390
- skippedReason: "email_skipped"
391
- } : {}
392
- };
187
+ var buildPreferencesByTopic = (settings) => {
188
+ const record = {};
189
+ const raw = settings?.topicPreferences;
190
+ if (!Array.isArray(raw)) return record;
191
+ for (const pref of raw) {
192
+ if (!pref || typeof pref !== "object") continue;
193
+ const topic = typeof pref.topic === "string" ? pref.topic.trim() : "";
194
+ if (!topic) continue;
195
+ record[topic] = { emailDigest: pref.emailDigest === true };
196
+ }
197
+ return record;
393
198
  };
394
- export {
395
- createNotification,
396
- createNotificationsForUser,
397
- routes,
398
- sendNotificationsDigestForUser
199
+ var sendNotificationsDigestForUser = async (ctx, { userId, ability, force = false }) => {
200
+ const modelCtx = {
201
+ req: ctx.req,
202
+ ability
203
+ };
204
+ const SettingsModel = await models.get("RBNotificationSettings", modelCtx);
205
+ const NotificationModel = await models.get("RBNotification", modelCtx);
206
+ const settings = await SettingsModel.findOne({ userId }).lean();
207
+ const digestFrequencyRaw = typeof settings?.digestFrequency === "string" ? settings.digestFrequency : "weekly";
208
+ const digestFrequency = digestFrequencyRaw === "daily" || digestFrequencyRaw === "weekly" || digestFrequencyRaw === "off" ? digestFrequencyRaw : "weekly";
209
+ if (digestFrequency === "off") return {
210
+ ok: true,
211
+ sent: false,
212
+ skippedReason: "digest_off"
213
+ };
214
+ const now = /* @__PURE__ */ new Date();
215
+ const windowMs = getDigestWindowMs(digestFrequency);
216
+ const lastSentAt = settings?.lastDigestSentAt instanceof Date ? settings.lastDigestSentAt : null;
217
+ const since = lastSentAt ?? new Date(now.getTime() - windowMs);
218
+ if (!force && lastSentAt && now.getTime() - lastSentAt.getTime() < windowMs) return {
219
+ ok: true,
220
+ sent: false,
221
+ skippedReason: "not_due"
222
+ };
223
+ const preferencesByTopic = buildPreferencesByTopic(settings);
224
+ const disabledTopics = Object.entries(preferencesByTopic).filter(([, pref]) => pref.emailDigest === false).map(([topic]) => topic);
225
+ const query = {
226
+ userId,
227
+ archivedAt: { $exists: false },
228
+ readAt: { $exists: false },
229
+ createdAt: { $gt: since }
230
+ };
231
+ if (disabledTopics.length > 0) query.topic = { $nin: disabledTopics };
232
+ const notifications = await NotificationModel.find(query).sort({ createdAt: -1 }).limit(50).lean();
233
+ if (!notifications.length) {
234
+ await SettingsModel.updateOne({ userId }, {
235
+ $set: { lastDigestSentAt: now },
236
+ $setOnInsert: {
237
+ userId,
238
+ digestFrequency: "weekly"
239
+ }
240
+ }, { upsert: true });
241
+ return {
242
+ ok: true,
243
+ sent: false,
244
+ skippedReason: "empty"
245
+ };
246
+ }
247
+ const user = await (await models.getGlobal("RBUser", ctx)).findById(userId, { email: 1 }).lean();
248
+ const email = typeof user?.email === "string" ? user.email.trim() : "";
249
+ if (!email) return {
250
+ ok: true,
251
+ sent: false,
252
+ skippedReason: "missing_email"
253
+ };
254
+ const subject = "Notifications digest";
255
+ const html = `<div><p>Here is your notifications digest:</p><ul>${notifications.map((n) => {
256
+ const title = typeof n.title === "string" ? n.title : "";
257
+ const body = typeof n.body === "string" ? n.body : "";
258
+ const url = getNotificationLink(n);
259
+ const createdAt = formatIso(n.createdAt) ?? "";
260
+ 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>`;
261
+ }).join("")}</ul></div>`;
262
+ let emailResult;
263
+ try {
264
+ emailResult = await sendEmail({
265
+ to: email,
266
+ subject,
267
+ html
268
+ }, { missingTransport: "skip" });
269
+ } catch (error) {
270
+ return {
271
+ ok: false,
272
+ error: error instanceof Error ? error.message : "Email delivery failed"
273
+ };
274
+ }
275
+ await SettingsModel.updateOne({ userId }, {
276
+ $set: { lastDigestSentAt: now },
277
+ $setOnInsert: {
278
+ userId,
279
+ digestFrequency: "weekly"
280
+ }
281
+ }, { upsert: true });
282
+ return {
283
+ ok: true,
284
+ sent: emailResult.skipped !== true,
285
+ ...emailResult.skipped ? { skippedReason: "email_skipped" } : {}
286
+ };
399
287
  };
400
- //# sourceMappingURL=notifications.js.map
288
+ //#endregion
289
+ export { createNotification, createNotificationsForUser, routes, sendNotificationsDigestForUser };
290
+
291
+ //# sourceMappingURL=notifications.js.map