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