@terreno/announcements 57.4.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 (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +50 -0
  3. package/biome.jsonc +20 -0
  4. package/bunfig.toml +2 -0
  5. package/dist/announcementsApp.d.ts +9 -0
  6. package/dist/announcementsApp.js +820 -0
  7. package/dist/help.d.ts +30 -0
  8. package/dist/help.js +72 -0
  9. package/dist/helpRoutes.d.ts +8 -0
  10. package/dist/helpRoutes.js +233 -0
  11. package/dist/index.d.ts +9 -0
  12. package/dist/index.js +34 -0
  13. package/dist/models/announcement.d.ts +3 -0
  14. package/dist/models/announcement.js +214 -0
  15. package/dist/models/announcementAcknowledgement.d.ts +2 -0
  16. package/dist/models/announcementAcknowledgement.js +41 -0
  17. package/dist/models/announcementClickEvent.d.ts +3 -0
  18. package/dist/models/announcementClickEvent.js +56 -0
  19. package/dist/models/announcementImpression.d.ts +3 -0
  20. package/dist/models/announcementImpression.js +50 -0
  21. package/dist/overview.d.ts +14 -0
  22. package/dist/overview.js +310 -0
  23. package/dist/pending.d.ts +64 -0
  24. package/dist/pending.js +242 -0
  25. package/dist/types.d.ts +135 -0
  26. package/dist/types.js +2 -0
  27. package/package.json +57 -0
  28. package/src/announcementsApp.ts +722 -0
  29. package/src/help.ts +98 -0
  30. package/src/helpRoutes.ts +160 -0
  31. package/src/index.ts +45 -0
  32. package/src/models/announcement.ts +189 -0
  33. package/src/models/announcementAcknowledgement.ts +50 -0
  34. package/src/models/announcementClickEvent.ts +68 -0
  35. package/src/models/announcementImpression.ts +62 -0
  36. package/src/overview.ts +228 -0
  37. package/src/pending.ts +286 -0
  38. package/src/tests/announcementsApp.test.ts +844 -0
  39. package/src/tests/bunSetup.ts +9 -0
  40. package/src/tests/help.test.ts +29 -0
  41. package/src/tests/helpRoutes.test.ts +291 -0
  42. package/src/tests/index.test.ts +57 -0
  43. package/src/tests/overview.test.ts +351 -0
  44. package/src/tests/pending.test.ts +262 -0
  45. package/src/types.ts +157 -0
  46. package/tsconfig.json +33 -0
@@ -0,0 +1,820 @@
1
+ "use strict";
2
+ var __assign = (this && this.__assign) || function () {
3
+ __assign = Object.assign || function(t) {
4
+ for (var s, i = 1, n = arguments.length; i < n; i++) {
5
+ s = arguments[i];
6
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
7
+ t[p] = s[p];
8
+ }
9
+ return t;
10
+ };
11
+ return __assign.apply(this, arguments);
12
+ };
13
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
14
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
15
+ return new (P || (P = Promise))(function (resolve, reject) {
16
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
17
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
18
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
19
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
20
+ });
21
+ };
22
+ var __generator = (this && this.__generator) || function (thisArg, body) {
23
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
24
+ return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
25
+ function verb(n) { return function (v) { return step([n, v]); }; }
26
+ function step(op) {
27
+ if (f) throw new TypeError("Generator is already executing.");
28
+ while (g && (g = 0, op[0] && (_ = 0)), _) try {
29
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
30
+ if (y = 0, t) op = [op[0] & 2, t.value];
31
+ switch (op[0]) {
32
+ case 0: case 1: t = op; break;
33
+ case 4: _.label++; return { value: op[1], done: false };
34
+ case 5: _.label++; y = op[1]; op = [0]; continue;
35
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
36
+ default:
37
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
38
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
39
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
40
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
41
+ if (t[2]) _.ops.pop();
42
+ _.trys.pop(); continue;
43
+ }
44
+ op = body.call(thisArg, _);
45
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
46
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
47
+ }
48
+ };
49
+ var __values = (this && this.__values) || function(o) {
50
+ var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0;
51
+ if (m) return m.call(o);
52
+ if (o && typeof o.length === "number") return {
53
+ next: function () {
54
+ if (o && i >= o.length) o = void 0;
55
+ return { value: o && o[i++], done: !o };
56
+ }
57
+ };
58
+ throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
59
+ };
60
+ var __read = (this && this.__read) || function (o, n) {
61
+ var m = typeof Symbol === "function" && o[Symbol.iterator];
62
+ if (!m) return o;
63
+ var i = m.call(o), r, ar = [], e;
64
+ try {
65
+ while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
66
+ }
67
+ catch (error) { e = { error: error }; }
68
+ finally {
69
+ try {
70
+ if (r && !r.done && (m = i["return"])) m.call(i);
71
+ }
72
+ finally { if (e) throw e.error; }
73
+ }
74
+ return ar;
75
+ };
76
+ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
77
+ if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
78
+ if (ar || !(i in from)) {
79
+ if (!ar) ar = Array.prototype.slice.call(from, 0, i);
80
+ ar[i] = from[i];
81
+ }
82
+ }
83
+ return to.concat(ar || Array.prototype.slice.call(from));
84
+ };
85
+ Object.defineProperty(exports, "__esModule", { value: true });
86
+ exports.AnnouncementsApp = void 0;
87
+ var api_1 = require("@terreno/api");
88
+ var express_1 = require("express");
89
+ var luxon_1 = require("luxon");
90
+ var helpRoutes_1 = require("./helpRoutes");
91
+ var announcement_1 = require("./models/announcement");
92
+ var announcementAcknowledgement_1 = require("./models/announcementAcknowledgement");
93
+ var announcementClickEvent_1 = require("./models/announcementClickEvent");
94
+ var announcementImpression_1 = require("./models/announcementImpression");
95
+ var overview_1 = require("./overview");
96
+ var pending_1 = require("./pending");
97
+ var DEFAULT_BASE_PATH = "/announcements";
98
+ var defaultIsStaff = function (user) { return user.admin === true; };
99
+ var parsePlatform = function (req) {
100
+ var _a, _b;
101
+ var queryPlatform = req.query.platform;
102
+ if ((0, announcementImpression_1.isValidPlatform)(queryPlatform)) {
103
+ return queryPlatform;
104
+ }
105
+ var userAgent = (_b = (_a = req.get("user-agent")) === null || _a === void 0 ? void 0 : _a.toLowerCase()) !== null && _b !== void 0 ? _b : "";
106
+ if (userAgent.includes("iphone") || userAgent.includes("ipad")) {
107
+ return "ios";
108
+ }
109
+ if (userAgent.includes("android")) {
110
+ return "android";
111
+ }
112
+ return "web";
113
+ };
114
+ var requireAdmin = function (user) {
115
+ if (!(user === null || user === void 0 ? void 0 : user.admin)) {
116
+ throw new api_1.APIError({ status: 403, title: "Admin access required" });
117
+ }
118
+ };
119
+ var requireOverviewAccess = function (_a) { return __awaiter(void 0, [_a], void 0, function (_b) {
120
+ var permissions_1, permissions_1_1, permission, isAllowed, e_1_1;
121
+ var e_1, _c;
122
+ var permissions = _b.permissions, user = _b.user;
123
+ return __generator(this, function (_d) {
124
+ switch (_d.label) {
125
+ case 0:
126
+ if (permissions.length === 0) {
127
+ throw new api_1.APIError({ status: 403, title: "Admin access required" });
128
+ }
129
+ _d.label = 1;
130
+ case 1:
131
+ _d.trys.push([1, 6, 7, 8]);
132
+ permissions_1 = __values(permissions), permissions_1_1 = permissions_1.next();
133
+ _d.label = 2;
134
+ case 2:
135
+ if (!!permissions_1_1.done) return [3 /*break*/, 5];
136
+ permission = permissions_1_1.value;
137
+ return [4 /*yield*/, permission("list", user)];
138
+ case 3:
139
+ isAllowed = _d.sent();
140
+ if (!isAllowed) {
141
+ throw new api_1.APIError({ status: 403, title: "Admin access required" });
142
+ }
143
+ _d.label = 4;
144
+ case 4:
145
+ permissions_1_1 = permissions_1.next();
146
+ return [3 /*break*/, 2];
147
+ case 5: return [3 /*break*/, 8];
148
+ case 6:
149
+ e_1_1 = _d.sent();
150
+ e_1 = { error: e_1_1 };
151
+ return [3 /*break*/, 8];
152
+ case 7:
153
+ try {
154
+ if (permissions_1_1 && !permissions_1_1.done && (_c = permissions_1.return)) _c.call(permissions_1);
155
+ }
156
+ finally { if (e_1) throw e_1.error; }
157
+ return [7 /*endfinally*/];
158
+ case 8: return [2 /*return*/];
159
+ }
160
+ });
161
+ }); };
162
+ var getUserId = function (user) {
163
+ var _a;
164
+ var userId = (_a = user._id) !== null && _a !== void 0 ? _a : user.id;
165
+ if (!userId) {
166
+ throw new api_1.APIError({ status: 401, title: "Authentication required" });
167
+ }
168
+ return String(userId);
169
+ };
170
+ var hasPrimaryAction = function (announcement) {
171
+ var _a, _b, _c, _d;
172
+ var label = (_b = (_a = announcement.primaryAction) === null || _a === void 0 ? void 0 : _a.label) === null || _b === void 0 ? void 0 : _b.trim();
173
+ var url = (_d = (_c = announcement.primaryAction) === null || _c === void 0 ? void 0 : _c.url) === null || _d === void 0 ? void 0 : _d.trim();
174
+ return Boolean(label && url);
175
+ };
176
+ var resolveClickPlatform = function (req, bodyPlatform) {
177
+ if (bodyPlatform !== undefined && bodyPlatform !== null) {
178
+ if (!(0, announcementImpression_1.isValidPlatform)(bodyPlatform)) {
179
+ throw new api_1.APIError({ status: 400, title: "Invalid platform" });
180
+ }
181
+ return bodyPlatform;
182
+ }
183
+ return parsePlatform(req);
184
+ };
185
+ var requireVisibleAnnouncement = function (_a) { return __awaiter(void 0, [_a], void 0, function (_b) {
186
+ var visible;
187
+ var announcement = _b.announcement, isStaff = _b.isStaff, matchAudience = _b.matchAudience, platform = _b.platform, queryVersion = _b.queryVersion, user = _b.user;
188
+ return __generator(this, function (_c) {
189
+ switch (_c.label) {
190
+ case 0:
191
+ if (!announcement) {
192
+ throw new api_1.APIError({ status: 404, title: "Announcement not found" });
193
+ }
194
+ return [4 /*yield*/, (0, pending_1.isAnnouncementVisibleToUser)({
195
+ announcement: announcement,
196
+ isStaff: isStaff,
197
+ matchAudience: matchAudience,
198
+ platform: platform,
199
+ queryVersion: queryVersion,
200
+ user: user,
201
+ })];
202
+ case 1:
203
+ visible = _c.sent();
204
+ if (!visible) {
205
+ throw new api_1.APIError({ status: 404, title: "Announcement not found" });
206
+ }
207
+ return [2 /*return*/, announcement];
208
+ }
209
+ });
210
+ }); };
211
+ var AnnouncementsApp = /** @class */ (function () {
212
+ function AnnouncementsApp(options) {
213
+ this.options = options !== null && options !== void 0 ? options : {};
214
+ }
215
+ AnnouncementsApp.prototype.adminContribution = function () {
216
+ return {
217
+ customScreens: [
218
+ {
219
+ displayName: "Overview",
220
+ group: "Announcements",
221
+ icon: "bullhorn",
222
+ name: "announcements",
223
+ },
224
+ ],
225
+ models: [
226
+ {
227
+ admin: {
228
+ defaultSort: "-priority,-publishedAt",
229
+ displayName: "All announcements",
230
+ group: "Announcements",
231
+ listDisplayLinks: ["title"],
232
+ listFields: [
233
+ "title",
234
+ "status",
235
+ "priority",
236
+ "version",
237
+ "publishedAt",
238
+ "expiresAt",
239
+ "acknowledgementPolicy",
240
+ ],
241
+ searchFields: ["title"],
242
+ sortableFields: ["title", "status", "priority", "version", "publishedAt", "created"],
243
+ },
244
+ model: announcement_1.Announcement,
245
+ routePath: "/announcements",
246
+ },
247
+ {
248
+ admin: {
249
+ adminPermissions: {
250
+ create: [],
251
+ delete: [],
252
+ list: [api_1.Permissions.IsAdmin],
253
+ read: [api_1.Permissions.IsAdmin],
254
+ update: [],
255
+ },
256
+ defaultSort: "-acknowledgedAt",
257
+ displayName: "Acknowledgements",
258
+ group: "Announcements",
259
+ listFields: ["userId", "announcementId", "version", "acknowledgedAt"],
260
+ },
261
+ model: announcementAcknowledgement_1.AnnouncementAcknowledgement,
262
+ routePath: "/announcement-acknowledgements",
263
+ },
264
+ {
265
+ admin: {
266
+ adminPermissions: {
267
+ create: [],
268
+ delete: [],
269
+ list: [api_1.Permissions.IsAdmin],
270
+ read: [api_1.Permissions.IsAdmin],
271
+ update: [],
272
+ },
273
+ defaultSort: "-viewedAt",
274
+ displayName: "Impressions",
275
+ group: "Announcements",
276
+ listFields: ["userId", "announcementId", "version", "viewedAt", "platform"],
277
+ },
278
+ model: announcementImpression_1.AnnouncementImpression,
279
+ routePath: "/announcement-impressions",
280
+ },
281
+ {
282
+ admin: {
283
+ adminPermissions: {
284
+ create: [],
285
+ delete: [],
286
+ list: [api_1.Permissions.IsAdmin],
287
+ read: [api_1.Permissions.IsAdmin],
288
+ update: [],
289
+ },
290
+ defaultSort: "-clickedAt",
291
+ displayName: "Click events",
292
+ group: "Announcements",
293
+ listFields: ["userId", "announcementId", "version", "action", "clickedAt", "platform"],
294
+ },
295
+ model: announcementClickEvent_1.AnnouncementClickEvent,
296
+ routePath: "/announcement-click-events",
297
+ },
298
+ ],
299
+ };
300
+ };
301
+ AnnouncementsApp.prototype.register = function (app, openApi) {
302
+ var _this = this;
303
+ var _a, _b, _c, _d, _e, _f;
304
+ var basePath = (_a = this.options.basePath) !== null && _a !== void 0 ? _a : DEFAULT_BASE_PATH;
305
+ var defaultAcknowledgementPolicy = (_b = this.options.defaultAcknowledgementPolicy) !== null && _b !== void 0 ? _b : "dismiss-only";
306
+ var overviewPermissions = (_c = this.options.adminOverviewPermissions) !== null && _c !== void 0 ? _c : [api_1.Permissions.IsAdmin];
307
+ var matchAudience = (_d = this.options.matchAudience) !== null && _d !== void 0 ? _d : (function () { return true; });
308
+ var isStaff = (_e = this.options.isStaff) !== null && _e !== void 0 ? _e : defaultIsStaff;
309
+ var matchesAnnouncementAudience = function (user, announcement) { return __awaiter(_this, void 0, void 0, function () {
310
+ return __generator(this, function (_a) {
311
+ if (!(0, pending_1.matchAudienceByType)({ announcement: announcement, isStaff: isStaff, user: user })) {
312
+ return [2 /*return*/, false];
313
+ }
314
+ return [2 /*return*/, matchAudience(user, announcement)];
315
+ });
316
+ }); };
317
+ var routerOptions = __assign(__assign({}, (openApi ? { openApi: openApi } : {})), { permissions: __assign({ create: [api_1.Permissions.IsAdmin], delete: [api_1.Permissions.IsAdmin], list: [api_1.Permissions.IsAdmin], read: [api_1.Permissions.IsAdmin], update: [api_1.Permissions.IsAdmin] }, this.options.permissions), queryFields: ["status", "priority", "title"], sort: "-priority,-publishedAt" });
318
+ var userRouter = (0, express_1.Router)();
319
+ userRouter.get("/pending", (0, api_1.authenticateMiddleware)(), (0, api_1.asyncHandler)(function (req, res) { return __awaiter(_this, void 0, void 0, function () {
320
+ var user, userId, platform, queryVersion, published, audienceFiltered, published_1, published_1_1, announcement, matches, e_2_1, acknowledgements, impressions, pending, current, remainingCount;
321
+ var e_2, _a;
322
+ return __generator(this, function (_b) {
323
+ switch (_b.label) {
324
+ case 0:
325
+ user = req.user;
326
+ if (!user) {
327
+ throw new api_1.APIError({ status: 401, title: "Authentication required" });
328
+ }
329
+ userId = getUserId(user);
330
+ platform = parsePlatform(req);
331
+ queryVersion = (0, pending_1.parseQueryVersion)(req.query.version);
332
+ return [4 /*yield*/, announcement_1.Announcement.find({ status: "published" }).lean()];
333
+ case 1:
334
+ published = (_b.sent());
335
+ audienceFiltered = [];
336
+ _b.label = 2;
337
+ case 2:
338
+ _b.trys.push([2, 7, 8, 9]);
339
+ published_1 = __values(published), published_1_1 = published_1.next();
340
+ _b.label = 3;
341
+ case 3:
342
+ if (!!published_1_1.done) return [3 /*break*/, 6];
343
+ announcement = published_1_1.value;
344
+ return [4 /*yield*/, matchesAnnouncementAudience(user, announcement)];
345
+ case 4:
346
+ matches = _b.sent();
347
+ if (matches) {
348
+ audienceFiltered.push(announcement);
349
+ }
350
+ _b.label = 5;
351
+ case 5:
352
+ published_1_1 = published_1.next();
353
+ return [3 /*break*/, 3];
354
+ case 6: return [3 /*break*/, 9];
355
+ case 7:
356
+ e_2_1 = _b.sent();
357
+ e_2 = { error: e_2_1 };
358
+ return [3 /*break*/, 9];
359
+ case 8:
360
+ try {
361
+ if (published_1_1 && !published_1_1.done && (_a = published_1.return)) _a.call(published_1);
362
+ }
363
+ finally { if (e_2) throw e_2.error; }
364
+ return [7 /*endfinally*/];
365
+ case 9: return [4 /*yield*/, announcementAcknowledgement_1.AnnouncementAcknowledgement.find({ userId: userId }).select("announcementId version")];
366
+ case 10:
367
+ acknowledgements = _b.sent();
368
+ return [4 /*yield*/, announcementImpression_1.AnnouncementImpression.find({ userId: userId }).select("announcementId version")];
369
+ case 11:
370
+ impressions = _b.sent();
371
+ pending = (0, pending_1.selectPendingAnnouncements)({
372
+ acknowledgements: acknowledgements.map(function (ack) { return ({
373
+ announcementId: ack.announcementId.toString(),
374
+ version: ack.version,
375
+ }); }),
376
+ announcements: audienceFiltered,
377
+ defaultAcknowledgementPolicy: defaultAcknowledgementPolicy,
378
+ impressions: impressions.map(function (impression) { return ({
379
+ announcementId: impression.announcementId.toString(),
380
+ version: impression.version,
381
+ }); }),
382
+ platform: platform,
383
+ queryVersion: queryVersion,
384
+ });
385
+ current = pending[0]
386
+ ? (0, announcement_1.toAnnouncementPublic)(pending[0], defaultAcknowledgementPolicy)
387
+ : null;
388
+ remainingCount = Math.max(pending.length - (current ? 1 : 0), 0);
389
+ api_1.logger.info("Pending announcements fetched", {
390
+ currentId: current === null || current === void 0 ? void 0 : current.id,
391
+ platform: platform,
392
+ remainingCount: remainingCount,
393
+ userId: userId,
394
+ });
395
+ return [2 /*return*/, res.json({ data: { current: current, remainingCount: remainingCount } })];
396
+ }
397
+ });
398
+ }); }));
399
+ userRouter.get("/feed", (0, api_1.authenticateMiddleware)(), (0, api_1.asyncHandler)(function (req, res) { return __awaiter(_this, void 0, void 0, function () {
400
+ var user, platform, queryVersion, limit, page, published, visible, published_2, published_2_1, announcement, matchesAudience, e_3_1, sorted, start, pageItems;
401
+ var e_3, _a;
402
+ var _b, _c;
403
+ return __generator(this, function (_d) {
404
+ switch (_d.label) {
405
+ case 0:
406
+ user = req.user;
407
+ if (!user) {
408
+ throw new api_1.APIError({ status: 401, title: "Authentication required" });
409
+ }
410
+ platform = parsePlatform(req);
411
+ queryVersion = (0, pending_1.parseQueryVersion)(req.query.version);
412
+ limit = Math.min(Math.max(Number((_b = req.query.limit) !== null && _b !== void 0 ? _b : 20), 1), 100);
413
+ page = Math.max(Number((_c = req.query.page) !== null && _c !== void 0 ? _c : 1), 1);
414
+ return [4 /*yield*/, announcement_1.Announcement.find({ status: "published" }).lean()];
415
+ case 1:
416
+ published = (_d.sent());
417
+ visible = [];
418
+ _d.label = 2;
419
+ case 2:
420
+ _d.trys.push([2, 7, 8, 9]);
421
+ published_2 = __values(published), published_2_1 = published_2.next();
422
+ _d.label = 3;
423
+ case 3:
424
+ if (!!published_2_1.done) return [3 /*break*/, 6];
425
+ announcement = published_2_1.value;
426
+ return [4 /*yield*/, matchesAnnouncementAudience(user, announcement)];
427
+ case 4:
428
+ matchesAudience = _d.sent();
429
+ if (!matchesAudience) {
430
+ return [3 /*break*/, 5];
431
+ }
432
+ if (!(0, pending_1.isAnnouncementVisibleNow)({ announcement: announcement })) {
433
+ return [3 /*break*/, 5];
434
+ }
435
+ if (!(0, pending_1.matchesPlatform)({ announcement: announcement, platform: platform })) {
436
+ return [3 /*break*/, 5];
437
+ }
438
+ if (!(0, pending_1.passesMinBuildNumber)({ announcement: announcement, queryVersion: queryVersion })) {
439
+ return [3 /*break*/, 5];
440
+ }
441
+ visible.push(announcement);
442
+ _d.label = 5;
443
+ case 5:
444
+ published_2_1 = published_2.next();
445
+ return [3 /*break*/, 3];
446
+ case 6: return [3 /*break*/, 9];
447
+ case 7:
448
+ e_3_1 = _d.sent();
449
+ e_3 = { error: e_3_1 };
450
+ return [3 /*break*/, 9];
451
+ case 8:
452
+ try {
453
+ if (published_2_1 && !published_2_1.done && (_a = published_2.return)) _a.call(published_2);
454
+ }
455
+ finally { if (e_3) throw e_3.error; }
456
+ return [7 /*endfinally*/];
457
+ case 9:
458
+ sorted = visible.sort(function (left, right) {
459
+ var _a, _b, _c, _d;
460
+ if (right.priority !== left.priority) {
461
+ return right.priority - left.priority;
462
+ }
463
+ var rightPublished = (_b = (_a = right.publishedAt) === null || _a === void 0 ? void 0 : _a.getTime()) !== null && _b !== void 0 ? _b : 0;
464
+ var leftPublished = (_d = (_c = left.publishedAt) === null || _c === void 0 ? void 0 : _c.getTime()) !== null && _d !== void 0 ? _d : 0;
465
+ return rightPublished - leftPublished;
466
+ });
467
+ start = (page - 1) * limit;
468
+ pageItems = sorted
469
+ .slice(start, start + limit)
470
+ .map(function (announcement) { return (0, announcement_1.toAnnouncementPublic)(announcement, defaultAcknowledgementPolicy); });
471
+ return [2 /*return*/, res.json({
472
+ data: pageItems,
473
+ page: page,
474
+ total: sorted.length,
475
+ })];
476
+ }
477
+ });
478
+ }); }));
479
+ userRouter.post("/:id/acknowledge", (0, api_1.authenticateMiddleware)(), (0, api_1.asyncHandler)(function (req, res) { return __awaiter(_this, void 0, void 0, function () {
480
+ var user, userId, announcement, _a, existing;
481
+ var _b;
482
+ return __generator(this, function (_c) {
483
+ switch (_c.label) {
484
+ case 0:
485
+ user = req.user;
486
+ if (!user) {
487
+ throw new api_1.APIError({ status: 401, title: "Authentication required" });
488
+ }
489
+ userId = getUserId(user);
490
+ _a = requireVisibleAnnouncement;
491
+ _b = {};
492
+ return [4 /*yield*/, announcement_1.Announcement.findById(req.params.id)];
493
+ case 1: return [4 /*yield*/, _a.apply(void 0, [(_b.announcement = _c.sent(),
494
+ _b.isStaff = isStaff,
495
+ _b.matchAudience = matchAudience,
496
+ _b.platform = parsePlatform(req),
497
+ _b.queryVersion = (0, pending_1.parseQueryVersion)(req.query.version),
498
+ _b.user = user,
499
+ _b)])];
500
+ case 2:
501
+ announcement = _c.sent();
502
+ return [4 /*yield*/, (0, api_1.findOneOrNoneFor)(announcementAcknowledgement_1.AnnouncementAcknowledgement, {
503
+ announcementId: announcement._id,
504
+ userId: userId,
505
+ version: announcement.version,
506
+ })];
507
+ case 3:
508
+ existing = _c.sent();
509
+ if (!!existing) return [3 /*break*/, 5];
510
+ return [4 /*yield*/, announcementAcknowledgement_1.AnnouncementAcknowledgement.create({
511
+ acknowledgedAt: luxon_1.DateTime.utc().toJSDate(),
512
+ announcementId: announcement._id,
513
+ userId: userId,
514
+ version: announcement.version,
515
+ })];
516
+ case 4:
517
+ _c.sent();
518
+ _c.label = 5;
519
+ case 5: return [2 /*return*/, res.json({ data: { acknowledged: true } })];
520
+ }
521
+ });
522
+ }); }));
523
+ userRouter.post("/:id/impression", (0, api_1.authenticateMiddleware)(), (0, api_1.asyncHandler)(function (req, res) { return __awaiter(_this, void 0, void 0, function () {
524
+ var user, userId, bodyPlatform, platform, announcement, _a;
525
+ var _b;
526
+ var _c;
527
+ return __generator(this, function (_d) {
528
+ switch (_d.label) {
529
+ case 0:
530
+ user = req.user;
531
+ if (!user) {
532
+ throw new api_1.APIError({ status: 401, title: "Authentication required" });
533
+ }
534
+ userId = getUserId(user);
535
+ bodyPlatform = (_c = req.body) === null || _c === void 0 ? void 0 : _c.platform;
536
+ platform = (0, announcementImpression_1.isValidPlatform)(bodyPlatform) ? bodyPlatform : parsePlatform(req);
537
+ _a = requireVisibleAnnouncement;
538
+ _b = {};
539
+ return [4 /*yield*/, announcement_1.Announcement.findById(req.params.id)];
540
+ case 1: return [4 /*yield*/, _a.apply(void 0, [(_b.announcement = _d.sent(),
541
+ _b.isStaff = isStaff,
542
+ _b.matchAudience = matchAudience,
543
+ _b.platform = platform,
544
+ _b.queryVersion = (0, pending_1.parseQueryVersion)(req.query.version),
545
+ _b.user = user,
546
+ _b)])];
547
+ case 2:
548
+ announcement = _d.sent();
549
+ return [4 /*yield*/, announcementImpression_1.AnnouncementImpression.create({
550
+ announcementId: announcement._id,
551
+ platform: platform,
552
+ userId: userId,
553
+ version: announcement.version,
554
+ viewedAt: luxon_1.DateTime.utc().toJSDate(),
555
+ })];
556
+ case 3:
557
+ _d.sent();
558
+ return [2 /*return*/, res.json({ data: { recorded: true } })];
559
+ }
560
+ });
561
+ }); }));
562
+ userRouter.post("/:id/click", (0, api_1.authenticateMiddleware)(), (0, api_1.asyncHandler)(function (req, res) { return __awaiter(_this, void 0, void 0, function () {
563
+ var user, userId, body, platform, announcement, _a;
564
+ var _b;
565
+ return __generator(this, function (_c) {
566
+ switch (_c.label) {
567
+ case 0:
568
+ user = req.user;
569
+ if (!user) {
570
+ throw new api_1.APIError({ status: 401, title: "Authentication required" });
571
+ }
572
+ userId = getUserId(user);
573
+ body = req.body;
574
+ platform = resolveClickPlatform(req, body.platform);
575
+ _a = requireVisibleAnnouncement;
576
+ _b = {};
577
+ return [4 /*yield*/, announcement_1.Announcement.findById(req.params.id)];
578
+ case 1: return [4 /*yield*/, _a.apply(void 0, [(_b.announcement = _c.sent(),
579
+ _b.isStaff = isStaff,
580
+ _b.matchAudience = matchAudience,
581
+ _b.platform = platform,
582
+ _b.queryVersion = (0, pending_1.parseQueryVersion)(req.query.version),
583
+ _b.user = user,
584
+ _b)])];
585
+ case 2:
586
+ announcement = _c.sent();
587
+ if (!(0, announcementClickEvent_1.isValidClickAction)(body.action)) {
588
+ throw new api_1.APIError({ status: 400, title: "Invalid click action" });
589
+ }
590
+ if (!hasPrimaryAction(announcement)) {
591
+ throw new api_1.APIError({ status: 400, title: "Announcement has no primary action" });
592
+ }
593
+ return [4 /*yield*/, announcementClickEvent_1.AnnouncementClickEvent.create({
594
+ action: body.action,
595
+ announcementId: announcement._id,
596
+ clickedAt: luxon_1.DateTime.utc().toJSDate(),
597
+ platform: platform,
598
+ userId: userId,
599
+ version: announcement.version,
600
+ })];
601
+ case 3:
602
+ _c.sent();
603
+ return [2 /*return*/, res.json({ data: { recorded: true } })];
604
+ }
605
+ });
606
+ }); }));
607
+ var adminRouter = (0, express_1.Router)();
608
+ var routeOpenApi = openApi ? { openApi: openApi } : undefined;
609
+ adminRouter.get("/config", __spreadArray([
610
+ (0, api_1.authenticateMiddleware)()
611
+ ], __read((routeOpenApi
612
+ ? [
613
+ (0, api_1.createOpenApiBuilder)(routeOpenApi)
614
+ .withTags(["announcements"])
615
+ .withSummary("Get announcement plugin configuration")
616
+ .withResponse(200, {
617
+ data: {
618
+ properties: {
619
+ defaultAcknowledgementPolicy: {
620
+ description: '"required" or "dismiss-only"',
621
+ type: "string",
622
+ },
623
+ },
624
+ type: "object",
625
+ },
626
+ })
627
+ .build(),
628
+ ]
629
+ : [])), false), (0, api_1.asyncHandler)(function (req, res) { return __awaiter(_this, void 0, void 0, function () {
630
+ return __generator(this, function (_a) {
631
+ requireAdmin(req.user);
632
+ return [2 /*return*/, res.json({ data: { defaultAcknowledgementPolicy: defaultAcknowledgementPolicy } })];
633
+ });
634
+ }); }));
635
+ adminRouter.get("/overview", __spreadArray([
636
+ (0, api_1.authenticateMiddleware)()
637
+ ], __read((routeOpenApi
638
+ ? [
639
+ (0, api_1.createOpenApiBuilder)(routeOpenApi)
640
+ .withTags(["announcements"])
641
+ .withSummary("Admin overview of announcements with aggregate metrics")
642
+ .withQueryParameter("page", { type: "number" }, { required: false })
643
+ .withQueryParameter("limit", { type: "number" }, { required: false })
644
+ .withResponse(200, {
645
+ data: {
646
+ items: {
647
+ properties: {
648
+ _id: { type: "string" },
649
+ acknowledgementPolicy: { type: "string" },
650
+ audienceType: { type: "string" },
651
+ displayMode: { type: "string" },
652
+ expiresAt: { type: "string" },
653
+ metrics: {
654
+ properties: {
655
+ acknowledgements: { type: "number" },
656
+ clicks: { type: "number" },
657
+ impressions: { type: "number" },
658
+ },
659
+ type: "object",
660
+ },
661
+ priority: { type: "number" },
662
+ publishedAt: { type: "string" },
663
+ status: { type: "string" },
664
+ title: { type: "string" },
665
+ version: { type: "number" },
666
+ },
667
+ type: "object",
668
+ },
669
+ type: "array",
670
+ },
671
+ limit: { type: "number" },
672
+ more: { type: "boolean" },
673
+ page: { type: "number" },
674
+ total: { type: "number" },
675
+ totals: {
676
+ properties: {
677
+ acknowledgements: { type: "number" },
678
+ announcements: { type: "number" },
679
+ archived: { type: "number" },
680
+ clicks: { type: "number" },
681
+ draft: { type: "number" },
682
+ impressions: { type: "number" },
683
+ published: { type: "number" },
684
+ },
685
+ type: "object",
686
+ },
687
+ })
688
+ .build(),
689
+ ]
690
+ : [])), false), (0, api_1.asyncHandler)(function (req, res) { return __awaiter(_this, void 0, void 0, function () {
691
+ var _a, limit, page, overview;
692
+ return __generator(this, function (_b) {
693
+ switch (_b.label) {
694
+ case 0: return [4 /*yield*/, requireOverviewAccess({ permissions: overviewPermissions, user: req.user })];
695
+ case 1:
696
+ _b.sent();
697
+ _a = (0, overview_1.parseOverviewPagination)(req.query), limit = _a.limit, page = _a.page;
698
+ return [4 /*yield*/, (0, overview_1.fetchAnnouncementOverview)({
699
+ defaultAcknowledgementPolicy: defaultAcknowledgementPolicy,
700
+ limit: limit,
701
+ page: page,
702
+ })];
703
+ case 2:
704
+ overview = _b.sent();
705
+ return [2 /*return*/, res.json(overview)];
706
+ }
707
+ });
708
+ }); }));
709
+ adminRouter.post("/:id/publish", (0, api_1.authenticateMiddleware)(), (0, api_1.asyncHandler)(function (req, res) { return __awaiter(_this, void 0, void 0, function () {
710
+ var announcement;
711
+ return __generator(this, function (_a) {
712
+ switch (_a.label) {
713
+ case 0:
714
+ requireAdmin(req.user);
715
+ return [4 /*yield*/, announcement_1.Announcement.findById(req.params.id)];
716
+ case 1:
717
+ announcement = _a.sent();
718
+ if (!announcement) {
719
+ throw new api_1.APIError({ status: 404, title: "Announcement not found" });
720
+ }
721
+ if (announcement.status !== "draft") {
722
+ throw new api_1.APIError({
723
+ status: 400,
724
+ title: "Only draft announcements can be published",
725
+ });
726
+ }
727
+ announcement.status = "published";
728
+ announcement.publishedAt = luxon_1.DateTime.utc().toJSDate();
729
+ return [4 /*yield*/, announcement.save()];
730
+ case 2:
731
+ _a.sent();
732
+ api_1.logger.info("Announcement published", { announcementId: announcement._id.toString() });
733
+ return [2 /*return*/, res.json({ data: announcement })];
734
+ }
735
+ });
736
+ }); }));
737
+ adminRouter.post("/:id/archive", (0, api_1.authenticateMiddleware)(), (0, api_1.asyncHandler)(function (req, res) { return __awaiter(_this, void 0, void 0, function () {
738
+ var announcement;
739
+ return __generator(this, function (_a) {
740
+ switch (_a.label) {
741
+ case 0:
742
+ requireAdmin(req.user);
743
+ return [4 /*yield*/, announcement_1.Announcement.findById(req.params.id)];
744
+ case 1:
745
+ announcement = _a.sent();
746
+ if (!announcement) {
747
+ throw new api_1.APIError({ status: 404, title: "Announcement not found" });
748
+ }
749
+ if (announcement.status !== "published") {
750
+ throw new api_1.APIError({
751
+ status: 400,
752
+ title: "Only published announcements can be archived",
753
+ });
754
+ }
755
+ announcement.status = "archived";
756
+ announcement.archivedAt = luxon_1.DateTime.utc().toJSDate();
757
+ return [4 /*yield*/, announcement.save()];
758
+ case 2:
759
+ _a.sent();
760
+ api_1.logger.info("Announcement archived", { announcementId: announcement._id.toString() });
761
+ return [2 /*return*/, res.json({ data: announcement })];
762
+ }
763
+ });
764
+ }); }));
765
+ app.use(basePath, userRouter);
766
+ app.use(basePath, adminRouter);
767
+ if ((_f = this.options.help) === null || _f === void 0 ? void 0 : _f.enabled) {
768
+ (0, helpRoutes_1.registerAnnouncementHelpRoutes)({
769
+ app: app,
770
+ basePath: basePath,
771
+ defaultAcknowledgementPolicy: defaultAcknowledgementPolicy,
772
+ isStaff: isStaff,
773
+ matchAudience: matchAudience,
774
+ });
775
+ }
776
+ app.use(basePath, (0, api_1.modelRouter)(announcement_1.Announcement, routerOptions));
777
+ app.use("/announcement-acknowledgements", (0, api_1.modelRouter)(announcementAcknowledgement_1.AnnouncementAcknowledgement, {
778
+ permissions: {
779
+ create: [],
780
+ delete: [],
781
+ list: [api_1.Permissions.IsAdmin],
782
+ read: [api_1.Permissions.IsAdmin],
783
+ update: [],
784
+ },
785
+ populatePaths: [
786
+ { fields: ["title", "version", "status"], path: "announcementId" },
787
+ { fields: ["email", "name"], path: "userId" },
788
+ ],
789
+ }));
790
+ app.use("/announcement-impressions", (0, api_1.modelRouter)(announcementImpression_1.AnnouncementImpression, {
791
+ permissions: {
792
+ create: [],
793
+ delete: [],
794
+ list: [api_1.Permissions.IsAdmin],
795
+ read: [api_1.Permissions.IsAdmin],
796
+ update: [],
797
+ },
798
+ populatePaths: [
799
+ { fields: ["title", "version", "status"], path: "announcementId" },
800
+ { fields: ["email", "name"], path: "userId" },
801
+ ],
802
+ }));
803
+ app.use("/announcement-click-events", (0, api_1.modelRouter)(announcementClickEvent_1.AnnouncementClickEvent, {
804
+ permissions: {
805
+ create: [],
806
+ delete: [],
807
+ list: [api_1.Permissions.IsAdmin],
808
+ read: [api_1.Permissions.IsAdmin],
809
+ update: [],
810
+ },
811
+ populatePaths: [
812
+ { fields: ["title", "version", "status"], path: "announcementId" },
813
+ { fields: ["email", "name"], path: "userId" },
814
+ ],
815
+ }));
816
+ api_1.logger.info("AnnouncementsApp registered", { basePath: basePath, defaultAcknowledgementPolicy: defaultAcknowledgementPolicy });
817
+ };
818
+ return AnnouncementsApp;
819
+ }());
820
+ exports.AnnouncementsApp = AnnouncementsApp;