@webtypen/webframez-core 0.3.61 → 0.3.63

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.
@@ -553,6 +553,14 @@ class QueueWorkerCommand extends ConsoleCommand_1.ConsoleCommand {
553
553
  throw new Error(`Notification queue-job '${job._id.toString()}' has no _notification reference.`);
554
554
  }
555
555
  jobNotification = yield NotificationService_1.NotificationService.getNotification(job._notification);
556
+ const targetContext = yield NotificationService_1.NotificationService.resolveTarget({
557
+ target: jobNotification.target,
558
+ target_id: jobNotification.target_id,
559
+ });
560
+ if (!(targetContext === null || targetContext === void 0 ? void 0 : targetContext.targetModel)) {
561
+ throw new Error(`Notification queue-job '${job._id.toString()}' references a missing target.`);
562
+ }
563
+ NotificationService_1.NotificationService.attachTargetModel(jobNotification, targetContext.targetModel);
556
564
  job.notification = jobNotification;
557
565
  yield NotificationService_1.NotificationService.setChangingStatus(jobNotification, "running");
558
566
  }
@@ -628,9 +636,11 @@ class QueueWorkerCommand extends ConsoleCommand_1.ConsoleCommand {
628
636
  });
629
637
  const errorMessage = e instanceof Error ? e.stack || e.message : String(e);
630
638
  const endedAt = new Date();
639
+ let notificationFailureStatusSaved = false;
631
640
  if (jobNotification) {
632
641
  try {
633
642
  yield NotificationService_1.NotificationService.setChangingStatus(jobNotification, "error", errorMessage);
643
+ notificationFailureStatusSaved = true;
634
644
  }
635
645
  catch (notificationError) {
636
646
  yield ErrorHandler_1.ErrorHandler.report(notificationError, {
@@ -648,6 +658,29 @@ class QueueWorkerCommand extends ConsoleCommand_1.ConsoleCommand {
648
658
  });
649
659
  }
650
660
  }
661
+ if (!notificationFailureStatusSaved && job.notification_queue_job && job._notification) {
662
+ try {
663
+ const notificationFound = yield NotificationService_1.NotificationService.setQueueJobFailureStatus(job._notification, errorMessage);
664
+ if (!notificationFound) {
665
+ throw new Error(`Notification queue-job '${job._id.toString()}' references a missing notification '${job._notification.toString()}'.`);
666
+ }
667
+ }
668
+ catch (notificationError) {
669
+ yield ErrorHandler_1.ErrorHandler.report(notificationError, {
670
+ scope: "job",
671
+ source: "queue.worker.notification.status.fallback",
672
+ job: {
673
+ id: job && job._id ? job._id.toString() : undefined,
674
+ number: job && job.number ? job.number : undefined,
675
+ jobclass: job && job.jobclass ? job.jobclass : undefined,
676
+ worker: this.workerKey,
677
+ },
678
+ metadata: {
679
+ notification: job._notification.toString(),
680
+ },
681
+ });
682
+ }
683
+ }
651
684
  job.status = "failed";
652
685
  job.ended_at = endedAt;
653
686
  job.executions[0].status = "failed";
@@ -80,6 +80,7 @@ class ConsoleApplication {
80
80
  kernel: options && options.kernel ? options.kernel : null,
81
81
  options,
82
82
  });
83
+ NotificationService_1.NotificationService.registerTypes(this.modulesLoader.getNotifications());
83
84
  if (options && options.datatables) {
84
85
  DatatableRegistry_1.DatatableRegistry.registerMany(options.datatables);
85
86
  }
@@ -1,6 +1,7 @@
1
1
  import { KernelAiRegistrations } from "../Ai/AiTypes";
2
2
  import { ApiScopeRegistrationClass } from "../Api/ApiTypes";
3
3
  import { KernelMcpRegistrations } from "../Mcp/McpTypes";
4
+ import type { NotificationDefinition } from "../Notifications/NotificationService";
4
5
  export declare class ModuleProvider {
5
6
  static key: string;
6
7
  controller: {
@@ -11,6 +12,7 @@ export declare class ModuleProvider {
11
12
  };
12
13
  apiScopes: ApiScopeRegistrationClass[];
13
14
  commands: any[];
15
+ notifications: NotificationDefinition[];
14
16
  mcp?: KernelMcpRegistrations;
15
17
  ai?: KernelAiRegistrations;
16
18
  boot(_context?: any): void;
@@ -7,6 +7,7 @@ class ModuleProvider {
7
7
  this.middleware = {};
8
8
  this.apiScopes = [];
9
9
  this.commands = [];
10
+ this.notifications = [];
10
11
  }
11
12
  boot(_context) { }
12
13
  routes() { }
@@ -1,3 +1,4 @@
1
+ import type { NotificationDefinition } from "../Notifications/NotificationService";
1
2
  export declare class ModulesLoader {
2
3
  private loadedModules;
3
4
  load(modules: any[], context?: any): void;
@@ -6,6 +7,7 @@ export declare class ModulesLoader {
6
7
  };
7
8
  getLoadedModuleInstances(): any[];
8
9
  getCommands(): any[];
10
+ getNotifications(): NotificationDefinition[];
9
11
  initRoutes(): void;
10
12
  loadKernel(kernel: any): void;
11
13
  }
@@ -29,6 +29,16 @@ class ModulesLoader {
29
29
  }
30
30
  return commands;
31
31
  }
32
+ getNotifications() {
33
+ const notifications = [];
34
+ for (const key in this.loadedModules) {
35
+ const moduleNotifications = this.loadedModules[key].notifications;
36
+ if (Array.isArray(moduleNotifications)) {
37
+ notifications.push(...moduleNotifications);
38
+ }
39
+ }
40
+ return notifications;
41
+ }
32
42
  initRoutes() {
33
43
  for (let key in this.loadedModules) {
34
44
  this.loadedModules[key].routes();
@@ -2,17 +2,25 @@ import { Controller } from "../Controller/Controller";
2
2
  import { Request } from "../Router/Request";
3
3
  import { Response } from "../Router/Response";
4
4
  import { Notification } from "./Notification";
5
+ import { NotificationTargetContext } from "./NotificationService";
5
6
  export declare class BaseNotificationsController extends Controller {
6
- endpoint(req: Request, res: Response): Promise<Response>;
7
- protected getUserId(req: Request): Promise<any>;
7
+ endpoint(req: Request, res: Response): Promise<any>;
8
+ private getActionName;
8
9
  protected getNotificationReference(req: Request): any;
9
10
  protected serializeNotification(notification: Notification): object;
11
+ protected createNotification(req: Request, key: string, payload: Record<string, any>, targetContext: NotificationTargetContext): Promise<Notification | null>;
10
12
  private parsePaginationValue;
11
- private unauthorized;
12
13
  private notificationNotFound;
13
- list(req: Request, res: Response): Promise<Response>;
14
- details(req: Request, res: Response): Promise<Response>;
15
- markOneAsRead(req: Request, res: Response): Promise<Response>;
16
- markAllAsRead(req: Request, res: Response): Promise<Response>;
17
- markAllAsViewed(req: Request, res: Response): Promise<Response>;
14
+ private getAuthorizedTarget;
15
+ private getTypeForTarget;
16
+ definition(req: Request, res: Response): Promise<Response | undefined>;
17
+ create(req: Request, res: Response): Promise<Response | undefined>;
18
+ preferences(req: Request, res: Response): Promise<Response | undefined>;
19
+ savePreferences(req: Request, res: Response): Promise<Response | undefined>;
20
+ list(req: Request, res: Response): Promise<Response | undefined>;
21
+ counts(req: Request, res: Response): Promise<Response | undefined>;
22
+ details(req: Request, res: Response): Promise<Response | undefined>;
23
+ markOneAsRead(req: Request, res: Response): Promise<Response | undefined>;
24
+ markAllAsRead(req: Request, res: Response): Promise<Response | undefined>;
25
+ markAllAsViewed(req: Request, res: Response): Promise<Response | undefined>;
18
26
  }
@@ -16,37 +16,27 @@ class BaseNotificationsController extends Controller_1.Controller {
16
16
  endpoint(req, res) {
17
17
  var _a;
18
18
  return __awaiter(this, void 0, void 0, function* () {
19
- const requestType = (_a = req.body) === null || _a === void 0 ? void 0 : _a.__type;
20
- if (requestType === "list") {
21
- return yield this.list(req, res);
19
+ const actionName = this.getActionName((_a = req.body) === null || _a === void 0 ? void 0 : _a.__type);
20
+ const action = actionName ? this[actionName] : null;
21
+ if (typeof action !== "function") {
22
+ return res.status(400).send({ status: "error", message: "Invalid notification request." });
22
23
  }
23
- if (requestType === "details") {
24
- return yield this.details(req, res);
25
- }
26
- if (requestType === "mark_one_as_read") {
27
- return yield this.markOneAsRead(req, res);
28
- }
29
- if (requestType === "mark_all_as_read") {
30
- return yield this.markAllAsRead(req, res);
31
- }
32
- if (requestType === "mark_all_as_viewed") {
33
- return yield this.markAllAsViewed(req, res);
34
- }
35
- return res.status(400).send({
36
- status: "error",
37
- message: "Invalid notification request.",
38
- });
24
+ return yield action.call(this, req, res);
39
25
  });
40
26
  }
41
- getUserId(req) {
42
- var _a, _b, _c, _d, _e;
43
- return __awaiter(this, void 0, void 0, function* () {
44
- const configuredResolver = (_a = NotificationService_1.NotificationService.notificationsConfig) === null || _a === void 0 ? void 0 : _a.getUserIdByRequest;
45
- if (typeof configuredResolver === "function") {
46
- return yield configuredResolver(req);
47
- }
48
- return (_e = (_c = (_b = req.user) === null || _b === void 0 ? void 0 : _b._id) !== null && _c !== void 0 ? _c : (_d = req.user) === null || _d === void 0 ? void 0 : _d.id) !== null && _e !== void 0 ? _e : null;
49
- });
27
+ getActionName(type) {
28
+ return {
29
+ list: "list",
30
+ counts: "counts",
31
+ details: "details",
32
+ mark_one_as_read: "markOneAsRead",
33
+ mark_all_as_read: "markAllAsRead",
34
+ mark_all_as_viewed: "markAllAsViewed",
35
+ definition: "definition",
36
+ create: "create",
37
+ preferences: "preferences",
38
+ save_preferences: "savePreferences",
39
+ }[type];
50
40
  }
51
41
  getNotificationReference(req) {
52
42
  var _a, _b, _c, _d, _e, _f, _g, _h;
@@ -55,6 +45,11 @@ class BaseNotificationsController extends Controller_1.Controller {
55
45
  serializeNotification(notification) {
56
46
  return notification.toArray();
57
47
  }
48
+ createNotification(req, key, payload, targetContext) {
49
+ return __awaiter(this, void 0, void 0, function* () {
50
+ return yield NotificationService_1.NotificationService.create(key, payload, Object.assign(Object.assign({}, targetContext), { request: req }));
51
+ });
52
+ }
58
53
  parsePaginationValue(value, fallback, maximum) {
59
54
  const parsed = Number(value);
60
55
  if (!Number.isFinite(parsed) || parsed < 0)
@@ -62,115 +57,222 @@ class BaseNotificationsController extends Controller_1.Controller {
62
57
  const integer = Math.floor(parsed);
63
58
  return maximum !== undefined ? Math.min(integer, maximum) : integer;
64
59
  }
65
- unauthorized(res) {
66
- return res.status(401).send({
67
- status: "error",
68
- message: "Authenticated user required.",
60
+ notificationNotFound(res) {
61
+ return res.status(404).send({ status: "error", message: "Notification not found." });
62
+ }
63
+ getAuthorizedTarget(req, res) {
64
+ var _a, _b;
65
+ return __awaiter(this, void 0, void 0, function* () {
66
+ try {
67
+ const context = yield NotificationService_1.NotificationService.authorizeTarget({
68
+ target: (_a = req.body) === null || _a === void 0 ? void 0 : _a.target,
69
+ target_id: (_b = req.body) === null || _b === void 0 ? void 0 : _b.target_id,
70
+ request: req,
71
+ });
72
+ if (!context) {
73
+ res.status(404).send({ status: "error", message: "Notification target not found." });
74
+ return null;
75
+ }
76
+ return context;
77
+ }
78
+ catch (error) {
79
+ if (error instanceof NotificationService_1.NotificationTargetValidationError) {
80
+ res.status(400).send({ status: "error", message: error.message });
81
+ return null;
82
+ }
83
+ throw error;
84
+ }
69
85
  });
70
86
  }
71
- notificationNotFound(res) {
72
- return res.status(404).send({
73
- status: "error",
74
- message: "Notification not found.",
87
+ getTypeForTarget(key, target) {
88
+ const type = NotificationService_1.NotificationService.getTypeOrFail(key);
89
+ return type.targets.includes(target) ? type : null;
90
+ }
91
+ definition(req, res) {
92
+ var _a;
93
+ return __awaiter(this, void 0, void 0, function* () {
94
+ const context = yield this.getAuthorizedTarget(req, res);
95
+ if (!context)
96
+ return;
97
+ const key = (_a = req.body) === null || _a === void 0 ? void 0 : _a.key;
98
+ if (typeof key !== "string" || key.trim() === "") {
99
+ return res.status(400).send({ status: "error", message: "Notification type key required." });
100
+ }
101
+ try {
102
+ if (!this.getTypeForTarget(key, context.target))
103
+ throw new Error("not available");
104
+ return res.send({
105
+ status: "success",
106
+ data: { definition: NotificationService_1.NotificationService.getPublicTypeDefinition(key) },
107
+ });
108
+ }
109
+ catch (_error) {
110
+ return res.status(404).send({ status: "error", message: "Notification type not found." });
111
+ }
112
+ });
113
+ }
114
+ create(req, res) {
115
+ var _a, _b;
116
+ return __awaiter(this, void 0, void 0, function* () {
117
+ const context = yield this.getAuthorizedTarget(req, res);
118
+ if (!context)
119
+ return;
120
+ const key = (_a = req.body) === null || _a === void 0 ? void 0 : _a.key;
121
+ if (typeof key !== "string" || key.trim() === "") {
122
+ return res.status(400).send({ status: "error", message: "Notification type key required." });
123
+ }
124
+ const payload = (_b = req.body) === null || _b === void 0 ? void 0 : _b.payload;
125
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
126
+ return res.status(400).send({ status: "error", message: "Notification payload must be an object." });
127
+ }
128
+ let type;
129
+ try {
130
+ type = this.getTypeForTarget(key, context.target);
131
+ }
132
+ catch (_error) {
133
+ type = null;
134
+ }
135
+ if (!type)
136
+ return res.status(404).send({ status: "error", message: "Notification type not found." });
137
+ if ((type.mode || "fixed") !== "changing" || !type.queueJobAutomation) {
138
+ return res.status(400).send({
139
+ status: "error",
140
+ message: "Notification type does not start an automated queue job.",
141
+ });
142
+ }
143
+ try {
144
+ const notification = yield this.createNotification(req, key, payload, context);
145
+ if (!notification) {
146
+ return res.status(403).send({ status: "error", message: "Notification type is disabled." });
147
+ }
148
+ return res.send({ status: "success", data: { entry: this.serializeNotification(notification) } });
149
+ }
150
+ catch (error) {
151
+ return res
152
+ .status(error instanceof NotificationService_1.NotificationPayloadValidationError ||
153
+ error instanceof NotificationService_1.NotificationTargetValidationError
154
+ ? 400
155
+ : 500)
156
+ .send({ status: "error", message: (error === null || error === void 0 ? void 0 : error.message) || "Notification could not be created." });
157
+ }
158
+ });
159
+ }
160
+ preferences(req, res) {
161
+ return __awaiter(this, void 0, void 0, function* () {
162
+ const context = yield this.getAuthorizedTarget(req, res);
163
+ if (!context)
164
+ return;
165
+ const preferences = yield NotificationService_1.NotificationService.getTargetPreferences(context);
166
+ if (!preferences) {
167
+ return res.status(404).send({ status: "error", message: "Notification target not found." });
168
+ }
169
+ return res.send({ status: "success", data: preferences });
170
+ });
171
+ }
172
+ savePreferences(req, res) {
173
+ var _a, _b, _c;
174
+ return __awaiter(this, void 0, void 0, function* () {
175
+ const context = yield this.getAuthorizedTarget(req, res);
176
+ if (!context)
177
+ return;
178
+ const key = (_a = req.body) === null || _a === void 0 ? void 0 : _a.key;
179
+ const enabled = (_b = req.body) === null || _b === void 0 ? void 0 : _b.enabled;
180
+ const outputChannels = (_c = req.body) === null || _c === void 0 ? void 0 : _c.output_channels;
181
+ if (typeof key !== "string" || key.trim() === "") {
182
+ return res.status(400).send({ status: "error", message: "Notification type key required." });
183
+ }
184
+ if (typeof enabled !== "boolean") {
185
+ return res.status(400).send({ status: "error", message: "Enabled must be a boolean." });
186
+ }
187
+ if (!Array.isArray(outputChannels) || outputChannels.some((channel) => typeof channel !== "string")) {
188
+ return res.status(400).send({ status: "error", message: "Output channels must be an array of strings." });
189
+ }
190
+ try {
191
+ const entry = yield NotificationService_1.NotificationService.saveTargetPreference(key, { enabled, output_channels: outputChannels }, context);
192
+ if (!entry)
193
+ return res.status(404).send({ status: "error", message: "Notification target not found." });
194
+ return res.send({ status: "success", data: { entry } });
195
+ }
196
+ catch (error) {
197
+ return res.status(400).send({ status: "error", message: (error === null || error === void 0 ? void 0 : error.message) || "Preferences could not be saved." });
198
+ }
75
199
  });
76
200
  }
77
201
  list(req, res) {
78
202
  var _a, _b;
79
203
  return __awaiter(this, void 0, void 0, function* () {
80
- const userId = yield this.getUserId(req);
81
- if (!userId)
82
- return this.unauthorized(res);
204
+ const context = yield this.getAuthorizedTarget(req, res);
205
+ if (!context)
206
+ return;
83
207
  const offset = this.parsePaginationValue((_a = req.body) === null || _a === void 0 ? void 0 : _a.offset, 0);
84
208
  const limit = this.parsePaginationValue((_b = req.body) === null || _b === void 0 ? void 0 : _b.limit, 10, 100) || 10;
85
- const [loadedNotifications, unreadCount, unviewedCount] = yield Promise.all([
86
- NotificationService_1.NotificationService.loadNotifications({
87
- _user: userId,
88
- offset,
89
- limit: limit + 1,
90
- request: req,
91
- }),
92
- NotificationService_1.NotificationService.getUserUnreadCount(userId, req),
93
- NotificationService_1.NotificationService.getUserUnviewedCount(userId, req),
209
+ const [loaded, unreadCount, unviewedCount] = yield Promise.all([
210
+ NotificationService_1.NotificationService.loadNotifications(Object.assign(Object.assign({}, context), { offset, limit: limit + 1 })),
211
+ NotificationService_1.NotificationService.getUnreadCount(context),
212
+ NotificationService_1.NotificationService.getUnviewedCount(context),
94
213
  ]);
95
- const hasMore = loadedNotifications.length > limit;
96
- const entries = loadedNotifications.slice(0, limit).map((notification) => this.serializeNotification(notification));
97
214
  return res.send({
98
215
  status: "success",
99
216
  data: {
100
- entries,
217
+ entries: loaded.slice(0, limit).map((entry) => this.serializeNotification(entry)),
101
218
  unread_count: unreadCount,
102
219
  unviewed_count: unviewedCount,
103
- has_more: hasMore,
220
+ has_more: loaded.length > limit,
104
221
  },
105
222
  });
106
223
  });
107
224
  }
225
+ counts(req, res) {
226
+ return __awaiter(this, void 0, void 0, function* () {
227
+ const context = yield this.getAuthorizedTarget(req, res);
228
+ if (!context)
229
+ return;
230
+ const [unreadCount, unviewedCount] = yield Promise.all([
231
+ NotificationService_1.NotificationService.getUnreadCount(context),
232
+ NotificationService_1.NotificationService.getUnviewedCount(context),
233
+ ]);
234
+ return res.send({ status: "success", data: { unread_count: unreadCount, unviewed_count: unviewedCount } });
235
+ });
236
+ }
108
237
  details(req, res) {
109
238
  return __awaiter(this, void 0, void 0, function* () {
110
- const userId = yield this.getUserId(req);
111
- if (!userId)
112
- return this.unauthorized(res);
113
- const notificationRef = this.getNotificationReference(req);
114
- if (!notificationRef)
115
- return this.notificationNotFound(res);
116
- const notification = yield NotificationService_1.NotificationService.getNotificationForUser(notificationRef, userId, req);
239
+ const context = yield this.getAuthorizedTarget(req, res);
240
+ if (!context)
241
+ return;
242
+ const notification = yield NotificationService_1.NotificationService.getNotificationForTarget(this.getNotificationReference(req), context);
117
243
  if (!notification)
118
244
  return this.notificationNotFound(res);
119
- return res.send({
120
- status: "success",
121
- data: {
122
- entry: this.serializeNotification(notification),
123
- },
124
- });
245
+ return res.send({ status: "success", data: { entry: this.serializeNotification(notification) } });
125
246
  });
126
247
  }
127
248
  markOneAsRead(req, res) {
128
249
  return __awaiter(this, void 0, void 0, function* () {
129
- const userId = yield this.getUserId(req);
130
- if (!userId)
131
- return this.unauthorized(res);
132
- const notificationRef = this.getNotificationReference(req);
133
- if (!notificationRef)
134
- return this.notificationNotFound(res);
135
- const notification = yield NotificationService_1.NotificationService.getNotificationForUser(notificationRef, userId, req);
250
+ const context = yield this.getAuthorizedTarget(req, res);
251
+ if (!context)
252
+ return;
253
+ const notification = yield NotificationService_1.NotificationService.getNotificationForTarget(this.getNotificationReference(req), context);
136
254
  if (!notification)
137
255
  return this.notificationNotFound(res);
138
- const updatedNotification = yield NotificationService_1.NotificationService.markAsRead(notification);
139
- return res.send({
140
- status: "success",
141
- data: {
142
- entry: this.serializeNotification(updatedNotification),
143
- message: "Notification marked as read.",
144
- },
145
- });
256
+ const entry = yield NotificationService_1.NotificationService.markAsRead(notification);
257
+ return res.send({ status: "success", data: { entry: this.serializeNotification(entry) } });
146
258
  });
147
259
  }
148
260
  markAllAsRead(req, res) {
149
261
  return __awaiter(this, void 0, void 0, function* () {
150
- const userId = yield this.getUserId(req);
151
- if (!userId)
152
- return this.unauthorized(res);
153
- yield NotificationService_1.NotificationService.markAllAsRead(userId, req);
154
- return res.send({
155
- status: "success",
156
- data: {
157
- message: "All notifications marked as read.",
158
- },
159
- });
262
+ const context = yield this.getAuthorizedTarget(req, res);
263
+ if (!context)
264
+ return;
265
+ yield NotificationService_1.NotificationService.markAllAsRead(context);
266
+ return res.send({ status: "success", data: { message: "All notifications marked as read." } });
160
267
  });
161
268
  }
162
269
  markAllAsViewed(req, res) {
163
270
  return __awaiter(this, void 0, void 0, function* () {
164
- const userId = yield this.getUserId(req);
165
- if (!userId)
166
- return this.unauthorized(res);
167
- yield NotificationService_1.NotificationService.markAllAsViewed(userId, req);
168
- return res.send({
169
- status: "success",
170
- data: {
171
- message: "All notifications marked as viewed.",
172
- },
173
- });
271
+ const context = yield this.getAuthorizedTarget(req, res);
272
+ if (!context)
273
+ return;
274
+ yield NotificationService_1.NotificationService.markAllAsViewed(context);
275
+ return res.send({ status: "success", data: { message: "All notifications marked as viewed." } });
174
276
  });
175
277
  }
176
278
  }
@@ -2,11 +2,15 @@ import type { ObjectId } from "mongodb";
2
2
  import { Model } from "../Database/Model";
3
3
  import type { NotificationChangingStatus, NotificationModes, NotificationReadStatus, NotificationViewStatus } from "./NotificationService";
4
4
  export declare class Notification extends Model {
5
+ target?: string;
6
+ target_id?: ObjectId;
7
+ targetModel?: any;
5
8
  key?: string;
6
9
  mode?: NotificationModes;
7
10
  changing_status?: NotificationChangingStatus;
8
11
  changing_error?: string | null;
9
12
  _queue_job?: ObjectId;
13
+ _files?: ObjectId[];
10
14
  read_status?: NotificationReadStatus;
11
15
  read_at?: Date | null;
12
16
  view_status?: NotificationViewStatus;