strapi-plugin-notifier 1.0.3 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -14,19 +14,6 @@ const register = async ({ strapi }) => {
14
14
  pluginName: "notifier"
15
15
  }
16
16
  ]);
17
- strapi.cron.add({
18
- notifierCleanup: {
19
- task: async ({ strapi: s }) => {
20
- const settings2 = await s.plugin("notifier").service("settings").getEffective();
21
- const { maxDays, maxPerUser } = settings2.retention;
22
- await s.plugin("notifier").service("notification").cleanupOld(maxDays, maxPerUser);
23
- },
24
- options: {
25
- rule: "0 3 * * *"
26
- // 3 AM daily — configurable in cron settings if needed
27
- }
28
- }
29
- });
30
17
  };
31
18
  const bootstrap = async ({ strapi }) => {
32
19
  const stored = await strapi.store({ environment: "", type: "plugin", name: "notifier" }).get({ key: "settings" });
@@ -34,19 +21,34 @@ const bootstrap = async ({ strapi }) => {
34
21
  const effective = await strapi.plugin("notifier").service("settings").getEffective();
35
22
  await strapi.store({ environment: "", type: "plugin", name: "notifier" }).set({ key: "settings", value: effective });
36
23
  }
24
+ const pluginConfig = strapi.config.get("plugin::notifier", {});
25
+ const rules2 = pluginConfig.rules ?? [];
26
+ if (rules2.length > 0) {
27
+ strapi.plugin("notifier").service("rules").setup(rules2);
28
+ }
29
+ const { retention } = await strapi.plugin("notifier").service("settings").getEffective();
30
+ strapi.cron.add({
31
+ notifierCleanup: {
32
+ task: async ({ strapi: s }) => {
33
+ const s_retention = (await s.plugin("notifier").service("settings").getEffective()).retention;
34
+ await s.plugin("notifier").service("notification").cleanupOld(s_retention.maxDays, s_retention.maxPerUser);
35
+ },
36
+ options: { rule: retention.cleanupCron }
37
+ }
38
+ });
37
39
  };
38
- const kind = "collectionType";
39
- const collectionName = "notifier_notifications";
40
- const info = {
40
+ const kind$1 = "collectionType";
41
+ const collectionName$1 = "notifier_notifications";
42
+ const info$1 = {
41
43
  singularName: "notification",
42
44
  pluralName: "notifications",
43
45
  displayName: "Notification",
44
46
  description: "Admin-panel notifications managed by strapi-plugin-notifier"
45
47
  };
46
- const options = {
48
+ const options$1 = {
47
49
  draftAndPublish: false
48
50
  };
49
- const pluginOptions = {
51
+ const pluginOptions$1 = {
50
52
  "content-manager": {
51
53
  visible: false
52
54
  },
@@ -54,7 +56,7 @@ const pluginOptions = {
54
56
  visible: false
55
57
  }
56
58
  };
57
- const attributes = {
59
+ const attributes$1 = {
58
60
  title: {
59
61
  type: "string",
60
62
  required: true
@@ -85,6 +87,56 @@ const attributes = {
85
87
  },
86
88
  recipientRole: {
87
89
  type: "string"
90
+ },
91
+ mergeKey: {
92
+ type: "string"
93
+ },
94
+ mergeCount: {
95
+ type: "integer",
96
+ "default": 1
97
+ }
98
+ };
99
+ const schema$1 = {
100
+ kind: kind$1,
101
+ collectionName: collectionName$1,
102
+ info: info$1,
103
+ options: options$1,
104
+ pluginOptions: pluginOptions$1,
105
+ attributes: attributes$1
106
+ };
107
+ const notification$3 = { schema: schema$1 };
108
+ const kind = "collectionType";
109
+ const collectionName = "notifier_preferences";
110
+ const info = {
111
+ singularName: "notification-preference",
112
+ pluralName: "notification-preferences",
113
+ displayName: "Notification Preference",
114
+ description: "Per-user notification opt-out preferences for strapi-plugin-notifier"
115
+ };
116
+ const options = {
117
+ draftAndPublish: false
118
+ };
119
+ const pluginOptions = {
120
+ "content-manager": {
121
+ visible: false
122
+ },
123
+ "content-type-builder": {
124
+ visible: false
125
+ }
126
+ };
127
+ const attributes = {
128
+ userId: {
129
+ type: "integer",
130
+ required: true,
131
+ unique: true
132
+ },
133
+ globalOptOut: {
134
+ type: "boolean",
135
+ "default": false,
136
+ required: true
137
+ },
138
+ mutedTypes: {
139
+ type: "json"
88
140
  }
89
141
  };
90
142
  const schema = {
@@ -95,9 +147,9 @@ const schema = {
95
147
  pluginOptions,
96
148
  attributes
97
149
  };
98
- const notification$3 = { schema };
99
- const contentTypes = { notification: notification$3 };
100
- const UID = "plugin::notifier.notification";
150
+ const notificationPreference = { schema };
151
+ const contentTypes = { notification: notification$3, "notification-preference": notificationPreference };
152
+ const UID$1 = "plugin::notifier.notification";
101
153
  const accessFilter = (userId, roleCodes = []) => ({
102
154
  $or: [
103
155
  { recipientId: userId },
@@ -105,111 +157,205 @@ const accessFilter = (userId, roleCodes = []) => ({
105
157
  { recipientId: null, recipientRole: null }
106
158
  ]
107
159
  });
108
- const notification$2 = ({ strapi }) => ({
109
- findByRecipient(userId, roleCodes, { page = 1, pageSize = 20 } = {}) {
110
- return strapi.db.query(UID).findMany({
111
- where: accessFilter(userId, roleCodes),
112
- orderBy: { createdAt: "desc" },
113
- limit: pageSize,
114
- offset: (page - 1) * pageSize
115
- });
116
- },
117
- countByRecipient(userId, roleCodes) {
118
- return strapi.db.query(UID).count({ where: accessFilter(userId, roleCodes) });
119
- },
120
- countUnread(userId, roleCodes) {
121
- return strapi.db.query(UID).count({
122
- where: { ...accessFilter(userId, roleCodes), read: false }
123
- });
124
- },
125
- async markAsRead(id, userId, roleCodes) {
126
- const existing = await strapi.db.query(UID).findOne({ where: { id, ...accessFilter(userId, roleCodes) } });
127
- if (!existing) return null;
128
- return strapi.db.query(UID).update({ where: { id }, data: { read: true } });
129
- },
130
- markAllAsRead(userId, roleCodes) {
131
- return strapi.db.query(UID).updateMany({
132
- where: { ...accessFilter(userId, roleCodes), read: false },
133
- data: { read: true }
134
- });
135
- },
136
- async delete(id, userId, roleCodes) {
137
- const existing = await strapi.db.query(UID).findOne({ where: { id, ...accessFilter(userId, roleCodes) } });
138
- if (!existing) return null;
139
- return strapi.db.query(UID).delete({ where: { id } });
140
- },
141
- clearAll(userId, roleCodes) {
142
- return strapi.db.query(UID).deleteMany({ where: accessFilter(userId, roleCodes) });
143
- },
144
- create({ title, message, type = "info", url, recipientId, recipientRole }) {
145
- return strapi.db.query(UID).create({
146
- data: { title, message, type, url, read: false, recipientId, recipientRole }
147
- });
148
- },
149
- /** Retention cleanup: remove notifications older than maxDays and enforce per-user cap. */
150
- async cleanupOld(maxDays, maxPerUser) {
151
- if (maxDays > 0) {
152
- const cutoff = /* @__PURE__ */ new Date();
153
- cutoff.setDate(cutoff.getDate() - maxDays);
154
- await strapi.db.query(UID).deleteMany({
155
- where: { createdAt: { $lt: cutoff.toISOString() } }
160
+ const notification$2 = ({ strapi }) => {
161
+ const prefSvc = () => strapi.plugin("notifier").service("preference");
162
+ const optOutFilter = (pref) => {
163
+ if (pref.mutedTypes.length === 0) return {};
164
+ return { type: { $notIn: pref.mutedTypes } };
165
+ };
166
+ return {
167
+ async findByRecipient(userId, roleCodes, { page = 1, pageSize = 20 } = {}) {
168
+ const pref = await prefSvc().get(userId);
169
+ if (pref.globalOptOut) return [];
170
+ return strapi.db.query(UID$1).findMany({
171
+ where: { ...accessFilter(userId, roleCodes), ...optOutFilter(pref) },
172
+ orderBy: { createdAt: "desc" },
173
+ limit: pageSize,
174
+ offset: (page - 1) * pageSize
156
175
  });
157
- }
158
- if (maxPerUser > 0) {
159
- const rows = await strapi.db.query(UID).findMany({
160
- select: ["recipientId"],
161
- where: { recipientId: { $notNull: true } }
176
+ },
177
+ async countByRecipient(userId, roleCodes) {
178
+ const pref = await prefSvc().get(userId);
179
+ if (pref.globalOptOut) return 0;
180
+ return strapi.db.query(UID$1).count({
181
+ where: { ...accessFilter(userId, roleCodes), ...optOutFilter(pref) }
182
+ });
183
+ },
184
+ async countUnread(userId, roleCodes) {
185
+ const pref = await prefSvc().get(userId);
186
+ if (pref.globalOptOut) return 0;
187
+ return strapi.db.query(UID$1).count({
188
+ where: { ...accessFilter(userId, roleCodes), ...optOutFilter(pref), read: false }
162
189
  });
163
- const ids = [...new Set(rows.map((r) => r.recipientId))];
164
- for (const recipientId of ids) {
165
- const total = await strapi.db.query(UID).count({ where: { recipientId } });
166
- if (total > maxPerUser) {
167
- const excess = await strapi.db.query(UID).findMany({
168
- where: { recipientId },
169
- orderBy: { createdAt: "asc" },
170
- limit: total - maxPerUser,
171
- select: ["id"]
172
- });
173
- const excessIds = excess.map((e) => e.id);
174
- await strapi.db.query(UID).deleteMany({ where: { id: { $in: excessIds } } });
190
+ },
191
+ async markAsRead(id, userId, roleCodes) {
192
+ const existing = await strapi.db.query(UID$1).findOne({ where: { id, ...accessFilter(userId, roleCodes) } });
193
+ if (!existing) return null;
194
+ return strapi.db.query(UID$1).update({ where: { id }, data: { read: true } });
195
+ },
196
+ markAllAsRead(userId, roleCodes) {
197
+ return strapi.db.query(UID$1).updateMany({
198
+ where: { ...accessFilter(userId, roleCodes), read: false },
199
+ data: { read: true }
200
+ });
201
+ },
202
+ async delete(id, userId, roleCodes) {
203
+ const existing = await strapi.db.query(UID$1).findOne({ where: { id, ...accessFilter(userId, roleCodes) } });
204
+ if (!existing) return null;
205
+ return strapi.db.query(UID$1).delete({ where: { id } });
206
+ },
207
+ clearAll(userId, roleCodes) {
208
+ return strapi.db.query(UID$1).deleteMany({ where: accessFilter(userId, roleCodes) });
209
+ },
210
+ /**
211
+ * Find a recent notification with the same merge key and recipient.
212
+ * Used by the notifier service to decide whether to merge or create.
213
+ */
214
+ findMergeCandidate(mergeKey, recipientId, recipientRole, windowMs) {
215
+ const cutoff = new Date(Date.now() - windowMs);
216
+ return strapi.db.query(UID$1).findOne({
217
+ where: {
218
+ mergeKey,
219
+ ...recipientId != null ? { recipientId } : { recipientId: null },
220
+ ...recipientRole != null ? { recipientRole } : { recipientRole: null },
221
+ createdAt: { $gt: cutoff.toISOString() }
222
+ },
223
+ orderBy: { createdAt: "desc" }
224
+ });
225
+ },
226
+ /** Increment mergeCount on an existing notification and optionally rewrite its message. */
227
+ mergeInto(id, newCount, cfg, title) {
228
+ const data = { mergeCount: newCount };
229
+ if (cfg.rewriteMessage) {
230
+ data.message = `${newCount}× ${title}`;
231
+ }
232
+ return strapi.db.query(UID$1).update({ where: { id }, data });
233
+ },
234
+ create({ title, message, type = "info", url, recipientId, recipientRole, mergeKey }) {
235
+ return strapi.db.query(UID$1).create({
236
+ data: { title, message, type, url, read: false, recipientId, recipientRole, mergeKey, mergeCount: 1 }
237
+ });
238
+ },
239
+ /** Batch-insert multiple notifications in parallel. Does NOT apply opt-out or merge — callers must handle that. */
240
+ createMany(items) {
241
+ return Promise.all(items.map((item) => this.create(item)));
242
+ },
243
+ /** Retention cleanup: remove notifications older than maxDays and enforce per-user cap. */
244
+ async cleanupOld(maxDays, maxPerUser) {
245
+ if (maxDays > 0) {
246
+ const cutoff = /* @__PURE__ */ new Date();
247
+ cutoff.setDate(cutoff.getDate() - maxDays);
248
+ await strapi.db.query(UID$1).deleteMany({
249
+ where: { createdAt: { $lt: cutoff.toISOString() } }
250
+ });
251
+ }
252
+ if (maxPerUser > 0) {
253
+ const rows = await strapi.db.query(UID$1).findMany({
254
+ select: ["recipientId"],
255
+ where: { recipientId: { $notNull: true } }
256
+ });
257
+ const ids = [...new Set(rows.map((r) => r.recipientId))];
258
+ for (const recipientId of ids) {
259
+ const total = await strapi.db.query(UID$1).count({ where: { recipientId } });
260
+ if (total > maxPerUser) {
261
+ const excess = await strapi.db.query(UID$1).findMany({
262
+ where: { recipientId },
263
+ orderBy: { createdAt: "asc" },
264
+ limit: total - maxPerUser,
265
+ select: ["id"]
266
+ });
267
+ const excessIds = excess.map((e) => e.id);
268
+ await strapi.db.query(UID$1).deleteMany({ where: { id: { $in: excessIds } } });
269
+ }
175
270
  }
176
271
  }
177
272
  }
178
- }
179
- });
273
+ };
274
+ };
180
275
  const notifier = ({ strapi }) => {
181
276
  const svc = () => strapi.plugin("notifier").service("notification");
182
- return {
183
- send({ title, message, type, url, to }) {
277
+ const prefSvc = () => strapi.plugin("notifier").service("preference");
278
+ const settingsSvc = () => strapi.plugin("notifier").service("settings");
279
+ const computeMergeKey = (opts, keyFields) => keyFields.map((f) => opts[f] ?? "").join("\0");
280
+ const applySend = async (opts, mergeCfg) => {
281
+ const recipientId = opts.to?.userId ?? null;
282
+ const recipientRole = opts.to?.role ?? null;
283
+ const type = opts.type ?? "info";
284
+ if (recipientId != null) {
285
+ const pref = await prefSvc().get(recipientId);
286
+ if (prefSvc().isOptedOut(pref, type)) return null;
287
+ }
288
+ if (mergeCfg.enabled) {
289
+ const mergeKey = computeMergeKey(opts, mergeCfg.keyFields);
290
+ const windowMs = mergeCfg.windowMinutes * 60 * 1e3;
291
+ const candidate = await svc().findMergeCandidate(mergeKey, recipientId, recipientRole, windowMs);
292
+ if (candidate) {
293
+ const newCount = (candidate.mergeCount ?? 1) + 1;
294
+ return svc().mergeInto(candidate.id, newCount, mergeCfg, opts.title);
295
+ }
184
296
  return svc().create({
185
- title,
186
- message,
297
+ title: opts.title,
298
+ message: opts.message,
187
299
  type,
188
- url,
189
- recipientId: to?.userId,
190
- recipientRole: to?.role
300
+ url: opts.url,
301
+ recipientId: recipientId ?? void 0,
302
+ recipientRole: recipientRole ?? void 0,
303
+ mergeKey
191
304
  });
305
+ }
306
+ return svc().create({
307
+ title: opts.title,
308
+ message: opts.message,
309
+ type,
310
+ url: opts.url,
311
+ recipientId: recipientId ?? void 0,
312
+ recipientRole: recipientRole ?? void 0
313
+ });
314
+ };
315
+ return {
316
+ async send(opts) {
317
+ const { merge } = await settingsSvc().getEffective();
318
+ return applySend(opts, merge);
319
+ },
320
+ async broadcast(opts) {
321
+ const { merge } = await settingsSvc().getEffective();
322
+ return applySend(opts, merge);
192
323
  },
193
- broadcast(opts) {
194
- return svc().create({ ...opts });
324
+ async toRole(role, opts) {
325
+ const { merge } = await settingsSvc().getEffective();
326
+ return applySend({ ...opts, to: { role } }, merge);
195
327
  },
196
- toRole(role, opts) {
197
- return svc().create({ ...opts, recipientRole: role });
328
+ async toUser(userId, opts) {
329
+ const { merge } = await settingsSvc().getEffective();
330
+ return applySend({ ...opts, to: { userId } }, merge);
198
331
  },
199
- toUser(userId, opts) {
200
- return svc().create({ ...opts, recipientId: userId });
332
+ /**
333
+ * Send multiple notifications in one call. Opt-out and merge are applied per item.
334
+ * Settings are fetched once and reused across all items.
335
+ */
336
+ async sendBatch(items) {
337
+ const { merge } = await settingsSvc().getEffective();
338
+ return Promise.all(items.map((item) => applySend(item, merge)));
201
339
  }
202
340
  };
203
341
  };
204
342
  const DEFAULT_SETTINGS = {
205
343
  retention: {
206
344
  maxDays: 90,
207
- maxPerUser: 500
345
+ maxPerUser: 500,
346
+ cleanupCron: "0 3 * * *"
208
347
  },
209
348
  delivery: {
210
349
  defaultRecipient: "broadcast",
211
350
  allowedRoles: []
212
351
  },
352
+ merge: {
353
+ enabled: false,
354
+ windowMinutes: 60,
355
+ keyFields: ["title", "type"],
356
+ countBadge: true,
357
+ rewriteMessage: false
358
+ },
213
359
  ui: {
214
360
  pollInterval: 3e4,
215
361
  pageSize: 20,
@@ -232,6 +378,7 @@ const DEFAULT_SETTINGS = {
232
378
  const mergeWithDefaults = (overrides = {}) => ({
233
379
  retention: { ...DEFAULT_SETTINGS.retention, ...overrides.retention ?? {} },
234
380
  delivery: { ...DEFAULT_SETTINGS.delivery, ...overrides.delivery ?? {} },
381
+ merge: { ...DEFAULT_SETTINGS.merge, ...overrides.merge ?? {} },
235
382
  ui: {
236
383
  ...DEFAULT_SETTINGS.ui,
237
384
  ...overrides.ui ?? {},
@@ -263,6 +410,7 @@ const settings$2 = ({ strapi }) => ({
263
410
  return {
264
411
  retention: { ...withPluginsConfig.retention, ...fromStore.retention ?? {} },
265
412
  delivery: { ...withPluginsConfig.delivery, ...fromStore.delivery ?? {} },
413
+ merge: { ...withPluginsConfig.merge, ...fromStore.merge ?? {} },
266
414
  ui: {
267
415
  ...withPluginsConfig.ui,
268
416
  ...fromStore.ui ?? {},
@@ -284,6 +432,7 @@ const settings$2 = ({ strapi }) => ({
284
432
  const next = {
285
433
  retention: { ...current.retention, ...patch.retention ?? {} },
286
434
  delivery: { ...current.delivery, ...patch.delivery ?? {} },
435
+ merge: { ...current.merge, ...patch.merge ?? {} },
287
436
  ui: {
288
437
  ...current.ui,
289
438
  ...patch.ui ?? {},
@@ -303,7 +452,150 @@ const settings$2 = ({ strapi }) => ({
303
452
  await getStore(strapi).delete({ key: STORE_KEY });
304
453
  }
305
454
  });
306
- const services = { notification: notification$2, notifier, settings: settings$2 };
455
+ const UID = "plugin::notifier.notification-preference";
456
+ const DEFAULT_PREF = (userId) => ({
457
+ userId,
458
+ globalOptOut: false,
459
+ mutedTypes: []
460
+ });
461
+ const preference$2 = ({ strapi }) => ({
462
+ async get(userId) {
463
+ const row = await strapi.db.query(UID).findOne({ where: { userId } });
464
+ if (!row) return DEFAULT_PREF(userId);
465
+ return {
466
+ userId: row.userId,
467
+ globalOptOut: row.globalOptOut ?? false,
468
+ mutedTypes: row.mutedTypes ?? []
469
+ };
470
+ },
471
+ async upsert(userId, patch) {
472
+ const existing = await strapi.db.query(UID).findOne({ where: { userId } });
473
+ if (existing) {
474
+ const updated = await strapi.db.query(UID).update({
475
+ where: { id: existing.id },
476
+ data: patch
477
+ });
478
+ return {
479
+ userId: updated.userId,
480
+ globalOptOut: updated.globalOptOut ?? false,
481
+ mutedTypes: updated.mutedTypes ?? []
482
+ };
483
+ }
484
+ const created = await strapi.db.query(UID).create({
485
+ data: { userId, globalOptOut: false, mutedTypes: [], ...patch }
486
+ });
487
+ return {
488
+ userId: created.userId,
489
+ globalOptOut: created.globalOptOut ?? false,
490
+ mutedTypes: created.mutedTypes ?? []
491
+ };
492
+ },
493
+ isOptedOut(pref, type) {
494
+ return pref.globalOptOut || pref.mutedTypes.includes(type);
495
+ }
496
+ });
497
+ function getByPath(obj, path) {
498
+ return path.split(".").reduce((acc, key) => {
499
+ if (acc == null || typeof acc !== "object") return void 0;
500
+ return acc[key];
501
+ }, obj);
502
+ }
503
+ function interpolate(template, ctx) {
504
+ return template.replace(/\{\{([^}]+)\}\}/g, (_, path) => {
505
+ const val = getByPath(ctx, path.trim());
506
+ return val == null ? "" : String(val);
507
+ });
508
+ }
509
+ function resolveStr(tpl, ctx, flat) {
510
+ return typeof tpl === "function" ? tpl(ctx) : interpolate(tpl, flat);
511
+ }
512
+ function resolve(tpl, ctx) {
513
+ return typeof tpl === "function" ? tpl(ctx) : tpl;
514
+ }
515
+ function resolveTarget(tpl, ctx, flat) {
516
+ const target = typeof tpl === "function" ? tpl(ctx) : tpl;
517
+ if (!target || target === "broadcast") return {};
518
+ if ("userId" in target) return { to: { userId: target.userId } };
519
+ if ("role" in target) return { to: { role: target.role } };
520
+ if ("userIdFrom" in target) {
521
+ const val = getByPath(flat, target.userIdFrom);
522
+ if (val != null) return { to: { userId: Number(val) } };
523
+ }
524
+ return {};
525
+ }
526
+ async function fire(strapi, notification2, ctx) {
527
+ const flat = ctx;
528
+ const title = resolveStr(notification2.title, ctx, flat);
529
+ const message = notification2.message ? resolveStr(notification2.message, ctx, flat) : void 0;
530
+ const type = resolve(notification2.type, ctx) ?? "info";
531
+ const url = notification2.url ? resolveStr(notification2.url, ctx, flat) : void 0;
532
+ const targeting = resolveTarget(notification2.to, ctx, flat);
533
+ await strapi.plugin("notifier").service("notifier").send({
534
+ title,
535
+ ...message !== void 0 && { message },
536
+ type,
537
+ ...url !== void 0 && { url },
538
+ ...targeting
539
+ });
540
+ }
541
+ const rules = ({ strapi }) => ({
542
+ setup(rules2) {
543
+ for (const rule of rules2) {
544
+ if (rule.on === "lifecycle") {
545
+ const { model, action, when, notification: notification2 } = rule;
546
+ strapi.db.lifecycles.subscribe({
547
+ models: [model],
548
+ async [action](event) {
549
+ try {
550
+ const ctx = {
551
+ entry: event.result ?? event.params?.data ?? {},
552
+ params: event.params,
553
+ model
554
+ };
555
+ if (when && !when(ctx)) return;
556
+ await fire(strapi, notification2, ctx);
557
+ } catch (err) {
558
+ strapi.log.error(`[notifier] lifecycle rule ${model}:${action} failed — ${err}`);
559
+ }
560
+ }
561
+ });
562
+ } else if (rule.on === "event") {
563
+ const { event, filter, when, notification: notification2 } = rule;
564
+ strapi.eventHub.on(event, async (data) => {
565
+ try {
566
+ if (filter) {
567
+ for (const [k, v] of Object.entries(filter)) {
568
+ if (data[k] !== v) return;
569
+ }
570
+ }
571
+ const ctx = { event, ...data };
572
+ if (when && !when(ctx)) return;
573
+ await fire(strapi, notification2, ctx);
574
+ } catch (err) {
575
+ strapi.log.error(`[notifier] event rule "${event}" failed — ${err}`);
576
+ }
577
+ });
578
+ } else if (rule.on === "cron") {
579
+ const { schedule, notification: notification2 } = rule;
580
+ const key = `notifierCron_${Math.random().toString(36).slice(2, 9)}`;
581
+ strapi.cron.add({
582
+ [key]: {
583
+ task: async ({ strapi: s }) => {
584
+ try {
585
+ const ctx = { scheduledAt: /* @__PURE__ */ new Date() };
586
+ await fire(s, notification2, ctx);
587
+ } catch (err) {
588
+ strapi.log.error(`[notifier] cron rule "${schedule}" failed — ${err}`);
589
+ }
590
+ },
591
+ options: { rule: schedule }
592
+ }
593
+ });
594
+ }
595
+ }
596
+ }
597
+ });
598
+ const services = { notification: notification$2, notifier, settings: settings$2, preference: preference$2, rules };
307
599
  const getUserRoleCodes = async (strapi, userId) => {
308
600
  const user = await strapi.db.query("admin::user").findOne({
309
601
  where: { id: userId },
@@ -384,7 +676,25 @@ const settings$1 = ({ strapi }) => ({
384
676
  ctx.body = { data: defaults };
385
677
  }
386
678
  });
387
- const controllers = { notification: notification$1, config: config$1, settings: settings$1 };
679
+ const preference$1 = ({ strapi }) => ({
680
+ async findMine(ctx) {
681
+ const adminUser = ctx.state.user;
682
+ if (!adminUser) return ctx.unauthorized();
683
+ const pref = await strapi.plugin("notifier").service("preference").get(adminUser.id);
684
+ ctx.body = { data: pref };
685
+ },
686
+ async updateMine(ctx) {
687
+ const adminUser = ctx.state.user;
688
+ if (!adminUser) return ctx.unauthorized();
689
+ const { globalOptOut, mutedTypes } = ctx.request.body;
690
+ const patch = {};
691
+ if (typeof globalOptOut === "boolean") patch.globalOptOut = globalOptOut;
692
+ if (Array.isArray(mutedTypes)) patch.mutedTypes = mutedTypes;
693
+ const updated = await strapi.plugin("notifier").service("preference").upsert(adminUser.id, patch);
694
+ ctx.body = { data: updated };
695
+ }
696
+ });
697
+ const controllers = { notification: notification$1, config: config$1, settings: settings$1, preference: preference$1 };
388
698
  const notification = {
389
699
  type: "admin",
390
700
  routes: [
@@ -454,7 +764,24 @@ const settings = {
454
764
  }
455
765
  ]
456
766
  };
457
- const adminRoutes = { notification, config, settings };
767
+ const preference = {
768
+ type: "admin",
769
+ routes: [
770
+ {
771
+ method: "GET",
772
+ path: "/preferences/me",
773
+ handler: "preference.findMine",
774
+ config: { policies: ["admin::isAuthenticatedAdmin"] }
775
+ },
776
+ {
777
+ method: "PUT",
778
+ path: "/preferences/me",
779
+ handler: "preference.updateMine",
780
+ config: { policies: ["admin::isAuthenticatedAdmin"] }
781
+ }
782
+ ]
783
+ };
784
+ const adminRoutes = { notification, config, settings, preference };
458
785
  const index = {
459
786
  register,
460
787
  bootstrap,