@gzl10/nexus-plugin-notifications 0.16.1
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/dist/client/index.d.ts +56 -0
- package/dist/client/index.js +34 -0
- package/dist/client/index.js.map +1 -0
- package/dist/index.d.ts +249 -0
- package/dist/index.js +588 -0
- package/dist/index.js.map +1 -0
- package/image.png +0 -0
- package/migrations/ntf__1773837014877_auto.js +36 -0
- package/package.json +48 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { NexusClient, NexusApi } from '@gzl10/nexus-client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @module @gzl10/nexus-plugin-notifications/client
|
|
5
|
+
* @description Client API for the Notifications plugin with declaration merging.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
interface Notification {
|
|
9
|
+
id: string;
|
|
10
|
+
type: string;
|
|
11
|
+
title: string;
|
|
12
|
+
message: string;
|
|
13
|
+
read: boolean;
|
|
14
|
+
created_at: string;
|
|
15
|
+
expires_at?: string;
|
|
16
|
+
data?: Record<string, unknown>;
|
|
17
|
+
}
|
|
18
|
+
interface PaginatedNotifications {
|
|
19
|
+
items: Notification[];
|
|
20
|
+
total: number;
|
|
21
|
+
page: number;
|
|
22
|
+
limit: number;
|
|
23
|
+
totalPages: number;
|
|
24
|
+
hasNext: boolean;
|
|
25
|
+
}
|
|
26
|
+
interface NotificationsApi {
|
|
27
|
+
getUnread(): Promise<PaginatedNotifications>;
|
|
28
|
+
getAll(params?: {
|
|
29
|
+
page?: number;
|
|
30
|
+
limit?: number;
|
|
31
|
+
}): Promise<PaginatedNotifications>;
|
|
32
|
+
markRead(id: string): Promise<void>;
|
|
33
|
+
markAllRead(): Promise<{
|
|
34
|
+
count: number;
|
|
35
|
+
}>;
|
|
36
|
+
}
|
|
37
|
+
declare module '@gzl10/nexus-client' {
|
|
38
|
+
interface NexusPluginApis {
|
|
39
|
+
notifications: NotificationsApi;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
declare function createNotificationsApi(client: NexusClient): NotificationsApi;
|
|
43
|
+
/**
|
|
44
|
+
* Registers the Notifications API on a NexusApi instance.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```typescript
|
|
48
|
+
* import { registerNotificationsApi } from '@gzl10/nexus-plugin-notifications/client'
|
|
49
|
+
*
|
|
50
|
+
* registerNotificationsApi(nexus)
|
|
51
|
+
* nexus.plugin('notifications').getUnread()
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
declare function registerNotificationsApi(nexus: NexusApi): void;
|
|
55
|
+
|
|
56
|
+
export { type Notification, type NotificationsApi, type PaginatedNotifications, createNotificationsApi, registerNotificationsApi };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// src/client/index.ts
|
|
2
|
+
function createNotificationsApi(client) {
|
|
3
|
+
return {
|
|
4
|
+
async getUnread() {
|
|
5
|
+
const response = await client.get("/notifications/unread");
|
|
6
|
+
return response.data;
|
|
7
|
+
},
|
|
8
|
+
async getAll(params) {
|
|
9
|
+
const query = new URLSearchParams();
|
|
10
|
+
if (params?.page) query.set("page", String(params.page));
|
|
11
|
+
if (params?.limit) query.set("limit", String(params.limit));
|
|
12
|
+
const qs = query.toString();
|
|
13
|
+
const response = await client.get(
|
|
14
|
+
qs ? `/notifications?${qs}` : "/notifications"
|
|
15
|
+
);
|
|
16
|
+
return response.data;
|
|
17
|
+
},
|
|
18
|
+
async markRead(id) {
|
|
19
|
+
await client.post(`/notifications/${id}/read`);
|
|
20
|
+
},
|
|
21
|
+
async markAllRead() {
|
|
22
|
+
const response = await client.post("/notifications/read-all");
|
|
23
|
+
return response.data;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function registerNotificationsApi(nexus) {
|
|
28
|
+
nexus.extend("notifications", (client) => createNotificationsApi(client));
|
|
29
|
+
}
|
|
30
|
+
export {
|
|
31
|
+
createNotificationsApi,
|
|
32
|
+
registerNotificationsApi
|
|
33
|
+
};
|
|
34
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/client/index.ts"],"sourcesContent":["/**\n * @module @gzl10/nexus-plugin-notifications/client\n * @description Client API for the Notifications plugin with declaration merging.\n */\n\nimport type { NexusClient, NexusApi } from '@gzl10/nexus-client'\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface Notification {\n id: string\n type: string\n title: string\n message: string\n read: boolean\n created_at: string\n expires_at?: string\n data?: Record<string, unknown>\n}\n\nexport interface PaginatedNotifications {\n items: Notification[]\n total: number\n page: number\n limit: number\n totalPages: number\n hasNext: boolean\n}\n\nexport interface NotificationsApi {\n getUnread(): Promise<PaginatedNotifications>\n getAll(params?: { page?: number; limit?: number }): Promise<PaginatedNotifications>\n markRead(id: string): Promise<void>\n markAllRead(): Promise<{ count: number }>\n}\n\n// ============================================================================\n// Declaration merging — typed plugin() calls\n// ============================================================================\n\ndeclare module '@gzl10/nexus-client' {\n interface NexusPluginApis {\n notifications: NotificationsApi\n }\n}\n\n// ============================================================================\n// Factory\n// ============================================================================\n\nexport function createNotificationsApi(client: NexusClient): NotificationsApi {\n return {\n async getUnread() {\n const response = await client.get<PaginatedNotifications>('/notifications/unread')\n return response.data\n },\n\n async getAll(params) {\n const query = new URLSearchParams()\n if (params?.page) query.set('page', String(params.page))\n if (params?.limit) query.set('limit', String(params.limit))\n const qs = query.toString()\n const response = await client.get<PaginatedNotifications>(\n qs ? `/notifications?${qs}` : '/notifications'\n )\n return response.data\n },\n\n async markRead(id: string) {\n await client.post(`/notifications/${id}/read`)\n },\n\n async markAllRead() {\n const response = await client.post<{ count: number }>('/notifications/read-all')\n return response.data\n }\n }\n}\n\n/**\n * Registers the Notifications API on a NexusApi instance.\n *\n * @example\n * ```typescript\n * import { registerNotificationsApi } from '@gzl10/nexus-plugin-notifications/client'\n *\n * registerNotificationsApi(nexus)\n * nexus.plugin('notifications').getUnread()\n * ```\n */\nexport function registerNotificationsApi(nexus: NexusApi): void {\n nexus.extend('notifications', (client) => createNotificationsApi(client))\n}\n"],"mappings":";AAoDO,SAAS,uBAAuB,QAAuC;AAC5E,SAAO;AAAA,IACL,MAAM,YAAY;AAChB,YAAM,WAAW,MAAM,OAAO,IAA4B,uBAAuB;AACjF,aAAO,SAAS;AAAA,IAClB;AAAA,IAEA,MAAM,OAAO,QAAQ;AACnB,YAAM,QAAQ,IAAI,gBAAgB;AAClC,UAAI,QAAQ,KAAM,OAAM,IAAI,QAAQ,OAAO,OAAO,IAAI,CAAC;AACvD,UAAI,QAAQ,MAAO,OAAM,IAAI,SAAS,OAAO,OAAO,KAAK,CAAC;AAC1D,YAAM,KAAK,MAAM,SAAS;AAC1B,YAAM,WAAW,MAAM,OAAO;AAAA,QAC5B,KAAK,kBAAkB,EAAE,KAAK;AAAA,MAChC;AACA,aAAO,SAAS;AAAA,IAClB;AAAA,IAEA,MAAM,SAAS,IAAY;AACzB,YAAM,OAAO,KAAK,kBAAkB,EAAE,OAAO;AAAA,IAC/C;AAAA,IAEA,MAAM,cAAc;AAClB,YAAM,WAAW,MAAM,OAAO,KAAwB,yBAAyB;AAC/E,aAAO,SAAS;AAAA,IAClB;AAAA,EACF;AACF;AAaO,SAAS,yBAAyB,OAAuB;AAC9D,QAAM,OAAO,iBAAiB,CAAC,WAAW,uBAAuB,MAAM,CAAC;AAC1E;","names":[]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { ModuleContext, EventEntityDefinition, PluginManifest } from '@gzl10/nexus-sdk';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Notification types
|
|
6
|
+
*/
|
|
7
|
+
type NotificationType = 'info' | 'warning' | 'error' | 'success';
|
|
8
|
+
/**
|
|
9
|
+
* Notification priority
|
|
10
|
+
*/
|
|
11
|
+
type NotificationPriority = 'low' | 'normal' | 'high' | 'urgent';
|
|
12
|
+
/**
|
|
13
|
+
* Target types for notifications
|
|
14
|
+
*/
|
|
15
|
+
type NotificationTarget = 'all' | 'authenticated' | 'role' | 'users' | 'user';
|
|
16
|
+
/**
|
|
17
|
+
* Notification type options for select fields
|
|
18
|
+
*/
|
|
19
|
+
declare const NOTIFICATION_TYPE_OPTIONS: readonly [{
|
|
20
|
+
readonly value: "info";
|
|
21
|
+
readonly label: {
|
|
22
|
+
readonly en: "Info";
|
|
23
|
+
readonly es: "Información";
|
|
24
|
+
};
|
|
25
|
+
}, {
|
|
26
|
+
readonly value: "success";
|
|
27
|
+
readonly label: {
|
|
28
|
+
readonly en: "Success";
|
|
29
|
+
readonly es: "Éxito";
|
|
30
|
+
};
|
|
31
|
+
}, {
|
|
32
|
+
readonly value: "warning";
|
|
33
|
+
readonly label: {
|
|
34
|
+
readonly en: "Warning";
|
|
35
|
+
readonly es: "Advertencia";
|
|
36
|
+
};
|
|
37
|
+
}, {
|
|
38
|
+
readonly value: "error";
|
|
39
|
+
readonly label: {
|
|
40
|
+
readonly en: "Error";
|
|
41
|
+
readonly es: "Error";
|
|
42
|
+
};
|
|
43
|
+
}];
|
|
44
|
+
/**
|
|
45
|
+
* Notification priority options for select fields
|
|
46
|
+
*/
|
|
47
|
+
declare const NOTIFICATION_PRIORITY_OPTIONS: readonly [{
|
|
48
|
+
readonly value: "low";
|
|
49
|
+
readonly label: {
|
|
50
|
+
readonly en: "Low";
|
|
51
|
+
readonly es: "Baja";
|
|
52
|
+
};
|
|
53
|
+
}, {
|
|
54
|
+
readonly value: "normal";
|
|
55
|
+
readonly label: {
|
|
56
|
+
readonly en: "Normal";
|
|
57
|
+
readonly es: "Normal";
|
|
58
|
+
};
|
|
59
|
+
}, {
|
|
60
|
+
readonly value: "high";
|
|
61
|
+
readonly label: {
|
|
62
|
+
readonly en: "High";
|
|
63
|
+
readonly es: "Alta";
|
|
64
|
+
};
|
|
65
|
+
}, {
|
|
66
|
+
readonly value: "urgent";
|
|
67
|
+
readonly label: {
|
|
68
|
+
readonly en: "Urgent";
|
|
69
|
+
readonly es: "Urgente";
|
|
70
|
+
};
|
|
71
|
+
}];
|
|
72
|
+
/**
|
|
73
|
+
* Notification target type options for select fields
|
|
74
|
+
*/
|
|
75
|
+
declare const NOTIFICATION_TARGET_OPTIONS: readonly [{
|
|
76
|
+
readonly value: "user";
|
|
77
|
+
readonly label: {
|
|
78
|
+
readonly en: "User";
|
|
79
|
+
readonly es: "Usuario";
|
|
80
|
+
};
|
|
81
|
+
}, {
|
|
82
|
+
readonly value: "role";
|
|
83
|
+
readonly label: {
|
|
84
|
+
readonly en: "Role";
|
|
85
|
+
readonly es: "Rol";
|
|
86
|
+
};
|
|
87
|
+
}, {
|
|
88
|
+
readonly value: "all";
|
|
89
|
+
readonly label: {
|
|
90
|
+
readonly en: "All users";
|
|
91
|
+
readonly es: "Todos los usuarios";
|
|
92
|
+
};
|
|
93
|
+
}];
|
|
94
|
+
declare const sendNotificationInputSchema: z.ZodEffects<z.ZodObject<{
|
|
95
|
+
title: z.ZodString;
|
|
96
|
+
message: z.ZodString;
|
|
97
|
+
type: z.ZodPipeline<z.ZodEffects<z.ZodAny, any, unknown>, z.ZodDefault<z.ZodOptional<z.ZodEnum<["info", "warning", "error", "success"]>>>>;
|
|
98
|
+
priority: z.ZodPipeline<z.ZodEffects<z.ZodAny, any, unknown>, z.ZodDefault<z.ZodOptional<z.ZodEnum<["low", "normal", "high", "urgent"]>>>>;
|
|
99
|
+
target_type: z.ZodEnum<["all", "authenticated", "role", "users", "user"]>;
|
|
100
|
+
target_value: z.ZodOptional<z.ZodString>;
|
|
101
|
+
link: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
|
|
102
|
+
expires_at: z.ZodPipeline<z.ZodEffects<z.ZodAny, any, unknown>, z.ZodOptional<z.ZodString>>;
|
|
103
|
+
}, "strip", z.ZodTypeAny, {
|
|
104
|
+
message: string;
|
|
105
|
+
type: "info" | "warning" | "error" | "success";
|
|
106
|
+
title: string;
|
|
107
|
+
priority: "low" | "normal" | "high" | "urgent";
|
|
108
|
+
target_type: "all" | "authenticated" | "role" | "users" | "user";
|
|
109
|
+
link?: string | undefined;
|
|
110
|
+
target_value?: string | undefined;
|
|
111
|
+
expires_at?: string | undefined;
|
|
112
|
+
}, {
|
|
113
|
+
message: string;
|
|
114
|
+
title: string;
|
|
115
|
+
target_type: "all" | "authenticated" | "role" | "users" | "user";
|
|
116
|
+
link?: string | undefined;
|
|
117
|
+
type?: unknown;
|
|
118
|
+
priority?: unknown;
|
|
119
|
+
target_value?: string | undefined;
|
|
120
|
+
expires_at?: unknown;
|
|
121
|
+
}>, {
|
|
122
|
+
message: string;
|
|
123
|
+
type: "info" | "warning" | "error" | "success";
|
|
124
|
+
title: string;
|
|
125
|
+
priority: "low" | "normal" | "high" | "urgent";
|
|
126
|
+
target_type: "all" | "authenticated" | "role" | "users" | "user";
|
|
127
|
+
link?: string | undefined;
|
|
128
|
+
target_value?: string | undefined;
|
|
129
|
+
expires_at?: string | undefined;
|
|
130
|
+
}, {
|
|
131
|
+
message: string;
|
|
132
|
+
title: string;
|
|
133
|
+
target_type: "all" | "authenticated" | "role" | "users" | "user";
|
|
134
|
+
link?: string | undefined;
|
|
135
|
+
type?: unknown;
|
|
136
|
+
priority?: unknown;
|
|
137
|
+
target_value?: string | undefined;
|
|
138
|
+
expires_at?: unknown;
|
|
139
|
+
}>;
|
|
140
|
+
type SendNotificationInput = z.output<typeof sendNotificationInputSchema>;
|
|
141
|
+
/**
|
|
142
|
+
* Notification in the DB
|
|
143
|
+
*/
|
|
144
|
+
interface Notification {
|
|
145
|
+
id: string;
|
|
146
|
+
title: string;
|
|
147
|
+
message: string;
|
|
148
|
+
type: NotificationType;
|
|
149
|
+
priority: NotificationPriority;
|
|
150
|
+
target_type: NotificationTarget;
|
|
151
|
+
target_value: string | null;
|
|
152
|
+
link: string | null;
|
|
153
|
+
expires_at: Date | null;
|
|
154
|
+
created_at: Date;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Payload sent over Socket.IO
|
|
158
|
+
*/
|
|
159
|
+
interface NotificationPayload {
|
|
160
|
+
id: string;
|
|
161
|
+
title: string;
|
|
162
|
+
message: string;
|
|
163
|
+
type: NotificationType;
|
|
164
|
+
priority: NotificationPriority;
|
|
165
|
+
link?: string;
|
|
166
|
+
timestamp: string;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Notifications service
|
|
171
|
+
*/
|
|
172
|
+
declare class NotificationService {
|
|
173
|
+
private db;
|
|
174
|
+
private logger;
|
|
175
|
+
private generateId;
|
|
176
|
+
private nowTimestamp;
|
|
177
|
+
private formatTimestamp;
|
|
178
|
+
private safeJsonParse;
|
|
179
|
+
private socket;
|
|
180
|
+
private eventsNotify;
|
|
181
|
+
private t;
|
|
182
|
+
constructor(ctx: ModuleContext);
|
|
183
|
+
/**
|
|
184
|
+
* Sends a notification
|
|
185
|
+
*/
|
|
186
|
+
send(options: SendNotificationInput): Promise<{
|
|
187
|
+
id: string;
|
|
188
|
+
sent: number;
|
|
189
|
+
}>;
|
|
190
|
+
/**
|
|
191
|
+
* Checks if a user has access to a notification based on its target
|
|
192
|
+
*/
|
|
193
|
+
canUserAccess(notification: Notification, userId: string, roleIds?: string[]): boolean;
|
|
194
|
+
/**
|
|
195
|
+
* Marks a notification as read by a user.
|
|
196
|
+
* If roleIds is provided, verifies access before marking.
|
|
197
|
+
* Uses onConflict to prevent duplicates atomically.
|
|
198
|
+
*/
|
|
199
|
+
markAsRead(notificationId: string, userId: string, roleIds?: string[]): Promise<boolean>;
|
|
200
|
+
/**
|
|
201
|
+
* Marks all unread notifications as read by a user.
|
|
202
|
+
* Batches DB operations for efficiency.
|
|
203
|
+
*/
|
|
204
|
+
markAllAsRead(userId: string, roleIds?: string[], userCreatedAt?: Date | string): Promise<number>;
|
|
205
|
+
/**
|
|
206
|
+
* Gets unread notifications for a user.
|
|
207
|
+
* Only returns notifications created after the user's registration date.
|
|
208
|
+
* Uses LEFT JOIN with notification_reads to determine read status.
|
|
209
|
+
*/
|
|
210
|
+
getUnread(userId: string, roleIds?: string[], userCreatedAt?: Date | string): Promise<Notification[]>;
|
|
211
|
+
/**
|
|
212
|
+
* Gets a notification by ID
|
|
213
|
+
*/
|
|
214
|
+
findById(id: string): Promise<Notification | null>;
|
|
215
|
+
/**
|
|
216
|
+
* Deletes a notification
|
|
217
|
+
*/
|
|
218
|
+
delete(id: string): Promise<boolean>;
|
|
219
|
+
/**
|
|
220
|
+
* Cleans up expired notifications
|
|
221
|
+
*/
|
|
222
|
+
cleanupExpired(): Promise<number>;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Initializes the notifications service
|
|
226
|
+
*/
|
|
227
|
+
declare function initNotificationService(ctx: ModuleContext): NotificationService;
|
|
228
|
+
/**
|
|
229
|
+
* Gets the service instance
|
|
230
|
+
*/
|
|
231
|
+
declare function getNotificationService(): NotificationService;
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Notification (event) - ephemeral notifications with TTL
|
|
235
|
+
*/
|
|
236
|
+
declare const notificationEntity: EventEntityDefinition;
|
|
237
|
+
/**
|
|
238
|
+
* NotificationRead (event) - read log
|
|
239
|
+
*/
|
|
240
|
+
declare const notificationReadEntity: EventEntityDefinition;
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* @module @gzl10/nexus-plugin-notifications
|
|
244
|
+
* @description Real-time in-app notifications via Socket.IO
|
|
245
|
+
*/
|
|
246
|
+
|
|
247
|
+
declare const notificationsPlugin: PluginManifest;
|
|
248
|
+
|
|
249
|
+
export { NOTIFICATION_PRIORITY_OPTIONS, NOTIFICATION_TARGET_OPTIONS, NOTIFICATION_TYPE_OPTIONS, type Notification, type NotificationPayload, type NotificationPriority, NotificationService, type NotificationTarget, type NotificationType, type SendNotificationInput, notificationsPlugin as default, getNotificationService, initNotificationService, notificationEntity, notificationReadEntity, notificationsPlugin, sendNotificationInputSchema };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,588 @@
|
|
|
1
|
+
// src/notifications.entity.ts
|
|
2
|
+
import { useIdField, useTextField, useTextareaField, useSelectField, useUrlField, useDatetimeField, useExpiresAtField } from "@gzl10/nexus-sdk/fields";
|
|
3
|
+
|
|
4
|
+
// src/notifications.service.ts
|
|
5
|
+
var NotificationService = class {
|
|
6
|
+
db;
|
|
7
|
+
logger;
|
|
8
|
+
generateId;
|
|
9
|
+
nowTimestamp;
|
|
10
|
+
formatTimestamp;
|
|
11
|
+
safeJsonParse;
|
|
12
|
+
socket;
|
|
13
|
+
eventsNotify;
|
|
14
|
+
t;
|
|
15
|
+
constructor(ctx) {
|
|
16
|
+
this.db = ctx.db.knex;
|
|
17
|
+
this.logger = ctx.core.logger.child({ service: "notifications" });
|
|
18
|
+
this.generateId = ctx.core.generateId;
|
|
19
|
+
this.nowTimestamp = ctx.db.nowTimestamp;
|
|
20
|
+
this.formatTimestamp = ctx.db.formatTimestamp;
|
|
21
|
+
this.safeJsonParse = ctx.core.safeJsonParse;
|
|
22
|
+
this.socket = ctx.core.socket;
|
|
23
|
+
this.eventsNotify = ctx.events.notify.bind(ctx.events);
|
|
24
|
+
this.t = ctx.db.t;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Sends a notification
|
|
28
|
+
*/
|
|
29
|
+
async send(options) {
|
|
30
|
+
const { target_type, target_value } = options;
|
|
31
|
+
let sent = 0;
|
|
32
|
+
const type = options.type || "info";
|
|
33
|
+
const priority = options.priority || "normal";
|
|
34
|
+
const id = this.generateId();
|
|
35
|
+
const now = this.nowTimestamp(this.db);
|
|
36
|
+
await this.db(this.t("notifications")).insert({
|
|
37
|
+
id,
|
|
38
|
+
title: options.title,
|
|
39
|
+
message: options.message,
|
|
40
|
+
type,
|
|
41
|
+
priority,
|
|
42
|
+
target_type,
|
|
43
|
+
target_value: target_value || null,
|
|
44
|
+
link: options.link || null,
|
|
45
|
+
expires_at: options.expires_at ? this.formatTimestamp(this.db, new Date(options.expires_at)) : null,
|
|
46
|
+
created_at: now
|
|
47
|
+
});
|
|
48
|
+
if (this.socket.isInitialized()) {
|
|
49
|
+
const payload = {
|
|
50
|
+
id,
|
|
51
|
+
title: options.title,
|
|
52
|
+
message: options.message,
|
|
53
|
+
type,
|
|
54
|
+
priority,
|
|
55
|
+
link: options.link || void 0,
|
|
56
|
+
timestamp: now
|
|
57
|
+
};
|
|
58
|
+
switch (target_type) {
|
|
59
|
+
case "all":
|
|
60
|
+
this.socket.emitToAll("notification", payload);
|
|
61
|
+
sent = 1;
|
|
62
|
+
break;
|
|
63
|
+
case "authenticated":
|
|
64
|
+
this.socket.emitToAuthenticated("notification", payload);
|
|
65
|
+
sent = 1;
|
|
66
|
+
break;
|
|
67
|
+
case "role":
|
|
68
|
+
if (target_value) {
|
|
69
|
+
this.socket.emitToRole(target_value, "notification", payload);
|
|
70
|
+
sent = 1;
|
|
71
|
+
}
|
|
72
|
+
break;
|
|
73
|
+
case "users":
|
|
74
|
+
if (target_value) {
|
|
75
|
+
const userIds = this.safeJsonParse(
|
|
76
|
+
target_value,
|
|
77
|
+
[],
|
|
78
|
+
{ target_value, context: "send.users" }
|
|
79
|
+
);
|
|
80
|
+
for (const userId of userIds) {
|
|
81
|
+
this.socket.emitToUser(userId, "notification", payload);
|
|
82
|
+
}
|
|
83
|
+
sent = userIds.length;
|
|
84
|
+
}
|
|
85
|
+
break;
|
|
86
|
+
case "user":
|
|
87
|
+
if (target_value) {
|
|
88
|
+
this.socket.emitToUser(target_value, "notification", payload);
|
|
89
|
+
sent = this.socket.isUserConnected(target_value) ? 1 : 0;
|
|
90
|
+
}
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
this.logger.debug({ id, target_type, sent }, "Notification sent");
|
|
94
|
+
}
|
|
95
|
+
this.eventsNotify("notifications.sent", { id, target_type, target_value, sent });
|
|
96
|
+
return { id, sent };
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Checks if a user has access to a notification based on its target
|
|
100
|
+
*/
|
|
101
|
+
canUserAccess(notification, userId, roleIds) {
|
|
102
|
+
switch (notification.target_type) {
|
|
103
|
+
case "all":
|
|
104
|
+
case "authenticated":
|
|
105
|
+
return true;
|
|
106
|
+
case "role":
|
|
107
|
+
return roleIds ? roleIds.includes(notification.target_value || "") : false;
|
|
108
|
+
case "users": {
|
|
109
|
+
const userIds = this.safeJsonParse(
|
|
110
|
+
notification.target_value || "[]",
|
|
111
|
+
[],
|
|
112
|
+
{ notificationId: notification.id, context: "canUserAccess.users" }
|
|
113
|
+
);
|
|
114
|
+
return userIds.includes(userId);
|
|
115
|
+
}
|
|
116
|
+
case "user":
|
|
117
|
+
return notification.target_value === userId;
|
|
118
|
+
default:
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Marks a notification as read by a user.
|
|
124
|
+
* If roleIds is provided, verifies access before marking.
|
|
125
|
+
* Uses onConflict to prevent duplicates atomically.
|
|
126
|
+
*/
|
|
127
|
+
async markAsRead(notificationId, userId, roleIds) {
|
|
128
|
+
const notification = await this.findById(notificationId);
|
|
129
|
+
if (!notification) return false;
|
|
130
|
+
if (roleIds !== void 0 && !this.canUserAccess(notification, userId, roleIds)) {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
await this.db(this.t("notification_reads")).insert({
|
|
134
|
+
id: this.generateId(),
|
|
135
|
+
notification_id: notificationId,
|
|
136
|
+
user_id: userId,
|
|
137
|
+
read_at: this.nowTimestamp(this.db)
|
|
138
|
+
}).onConflict(["notification_id", "user_id"]).ignore();
|
|
139
|
+
if (this.socket.isInitialized()) {
|
|
140
|
+
this.socket.emitToUser(userId, "notification:read", { notificationId });
|
|
141
|
+
}
|
|
142
|
+
this.logger.debug({ notificationId, userId }, "Notification marked as read");
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Marks all unread notifications as read by a user.
|
|
147
|
+
* Batches DB operations for efficiency.
|
|
148
|
+
*/
|
|
149
|
+
async markAllAsRead(userId, roleIds, userCreatedAt) {
|
|
150
|
+
const unread = await this.getUnread(userId, roleIds, userCreatedAt);
|
|
151
|
+
if (unread.length === 0) return 0;
|
|
152
|
+
const now = this.nowTimestamp(this.db);
|
|
153
|
+
const reads = unread.map((n) => ({
|
|
154
|
+
id: this.generateId(),
|
|
155
|
+
notification_id: n.id,
|
|
156
|
+
user_id: userId,
|
|
157
|
+
read_at: now
|
|
158
|
+
}));
|
|
159
|
+
await this.db.transaction(async (trx) => {
|
|
160
|
+
for (const read of reads) {
|
|
161
|
+
await trx(this.t("notification_reads")).insert(read).onConflict(["notification_id", "user_id"]).ignore();
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
if (this.socket.isInitialized()) {
|
|
165
|
+
this.socket.emitToUser(userId, "notification:read", {
|
|
166
|
+
notificationIds: unread.map((n) => n.id)
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
this.logger.debug({ userId, count: unread.length }, "All notifications marked as read");
|
|
170
|
+
return unread.length;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Gets unread notifications for a user.
|
|
174
|
+
* Only returns notifications created after the user's registration date.
|
|
175
|
+
* Uses LEFT JOIN with notification_reads to determine read status.
|
|
176
|
+
*/
|
|
177
|
+
async getUnread(userId, roleIds, userCreatedAt) {
|
|
178
|
+
const now = this.nowTimestamp(this.db);
|
|
179
|
+
const db = this.db;
|
|
180
|
+
const t = this.t;
|
|
181
|
+
const query = db(t("notifications")).leftJoin(t("notification_reads"), function() {
|
|
182
|
+
this.on(`${t("notifications")}.id`, "=", `${t("notification_reads")}.notification_id`).andOn(`${t("notification_reads")}.user_id`, "=", db.raw("?", [userId]));
|
|
183
|
+
}).whereNull(`${t("notification_reads")}.id`).where(function() {
|
|
184
|
+
this.whereNull(`${t("notifications")}.expires_at`).orWhere(`${t("notifications")}.expires_at`, ">", now);
|
|
185
|
+
}).select(`${t("notifications")}.*`).orderBy(`${t("notifications")}.created_at`, "desc");
|
|
186
|
+
if (userCreatedAt) {
|
|
187
|
+
query.andWhere(`${t("notifications")}.created_at`, ">=", userCreatedAt);
|
|
188
|
+
}
|
|
189
|
+
const notifications = await query;
|
|
190
|
+
return notifications.filter((n) => this.canUserAccess(n, userId, roleIds));
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Gets a notification by ID
|
|
194
|
+
*/
|
|
195
|
+
async findById(id) {
|
|
196
|
+
const notification = await this.db(this.t("notifications")).where("id", id).first();
|
|
197
|
+
return notification || null;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Deletes a notification
|
|
201
|
+
*/
|
|
202
|
+
async delete(id) {
|
|
203
|
+
const deleted = await this.db(this.t("notifications")).where("id", id).delete();
|
|
204
|
+
return deleted > 0;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Cleans up expired notifications
|
|
208
|
+
*/
|
|
209
|
+
async cleanupExpired() {
|
|
210
|
+
const deleted = await this.db(this.t("notifications")).where("expires_at", "<", this.nowTimestamp(this.db)).whereNotNull("expires_at").delete();
|
|
211
|
+
if (deleted > 0) {
|
|
212
|
+
this.logger.info({ deleted }, "Cleaned up expired notifications");
|
|
213
|
+
}
|
|
214
|
+
return deleted;
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
var serviceInstance = null;
|
|
218
|
+
function initNotificationService(ctx) {
|
|
219
|
+
serviceInstance = new NotificationService(ctx);
|
|
220
|
+
return serviceInstance;
|
|
221
|
+
}
|
|
222
|
+
function getNotificationService() {
|
|
223
|
+
if (!serviceInstance) {
|
|
224
|
+
throw new Error("NotificationService not initialized");
|
|
225
|
+
}
|
|
226
|
+
return serviceInstance;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// src/notifications.types.ts
|
|
230
|
+
import { z } from "zod";
|
|
231
|
+
var NOTIFICATION_TYPE_OPTIONS = [
|
|
232
|
+
{ value: "info", label: { en: "Info", es: "Informaci\xF3n" } },
|
|
233
|
+
{ value: "success", label: { en: "Success", es: "\xC9xito" } },
|
|
234
|
+
{ value: "warning", label: { en: "Warning", es: "Advertencia" } },
|
|
235
|
+
{ value: "error", label: { en: "Error", es: "Error" } }
|
|
236
|
+
];
|
|
237
|
+
var NOTIFICATION_PRIORITY_OPTIONS = [
|
|
238
|
+
{ value: "low", label: { en: "Low", es: "Baja" } },
|
|
239
|
+
{ value: "normal", label: { en: "Normal", es: "Normal" } },
|
|
240
|
+
{ value: "high", label: { en: "High", es: "Alta" } },
|
|
241
|
+
{ value: "urgent", label: { en: "Urgent", es: "Urgente" } }
|
|
242
|
+
];
|
|
243
|
+
var NOTIFICATION_TARGET_OPTIONS = [
|
|
244
|
+
{ value: "user", label: { en: "User", es: "Usuario" } },
|
|
245
|
+
{ value: "role", label: { en: "Role", es: "Rol" } },
|
|
246
|
+
{ value: "all", label: { en: "All users", es: "Todos los usuarios" } }
|
|
247
|
+
];
|
|
248
|
+
var emptyToUndefined = z.preprocess((v) => v === "" ? void 0 : v, z.any());
|
|
249
|
+
var sendNotificationInputSchema = z.object({
|
|
250
|
+
title: z.string().min(1, "Title is required"),
|
|
251
|
+
message: z.string().min(1, "Message is required"),
|
|
252
|
+
type: emptyToUndefined.pipe(z.enum(["info", "warning", "error", "success"]).optional().default("info")),
|
|
253
|
+
priority: emptyToUndefined.pipe(z.enum(["low", "normal", "high", "urgent"]).optional().default("normal")),
|
|
254
|
+
target_type: z.enum(["all", "authenticated", "role", "users", "user"]),
|
|
255
|
+
target_value: z.string().optional(),
|
|
256
|
+
link: z.string().url().optional().or(z.literal("")),
|
|
257
|
+
expires_at: emptyToUndefined.pipe(z.string().datetime().optional())
|
|
258
|
+
}).refine(
|
|
259
|
+
(data) => {
|
|
260
|
+
if (["role", "user", "users"].includes(data.target_type)) {
|
|
261
|
+
return !!data.target_value;
|
|
262
|
+
}
|
|
263
|
+
return true;
|
|
264
|
+
},
|
|
265
|
+
{ message: "target_value is required for role, user, and users target types", path: ["target_value"] }
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
// src/notifications.entity.ts
|
|
269
|
+
var notificationEntity = {
|
|
270
|
+
type: "event",
|
|
271
|
+
realtime: "sync",
|
|
272
|
+
immutable: true,
|
|
273
|
+
table: "notifications",
|
|
274
|
+
routePrefix: "/",
|
|
275
|
+
label: { en: "Notification", es: "Notificaci\xF3n" },
|
|
276
|
+
labelPlural: { en: "Notifications", es: "Notificaciones" },
|
|
277
|
+
labelField: "title",
|
|
278
|
+
timestamps: true,
|
|
279
|
+
retention: { days: 30 },
|
|
280
|
+
calendarFrom: "expires_at",
|
|
281
|
+
fields: {
|
|
282
|
+
id: useIdField(),
|
|
283
|
+
title: useTextField({ label: { en: "Title", es: "T\xEDtulo" }, required: true }),
|
|
284
|
+
message: useTextareaField({ label: { en: "Message", es: "Mensaje" }, required: true }),
|
|
285
|
+
type: useSelectField({
|
|
286
|
+
label: { en: "Type", es: "Tipo" },
|
|
287
|
+
options: [...NOTIFICATION_TYPE_OPTIONS],
|
|
288
|
+
defaultValue: "info",
|
|
289
|
+
required: true,
|
|
290
|
+
size: 20
|
|
291
|
+
}),
|
|
292
|
+
priority: useSelectField({
|
|
293
|
+
label: { en: "Priority", es: "Prioridad" },
|
|
294
|
+
options: [...NOTIFICATION_PRIORITY_OPTIONS],
|
|
295
|
+
defaultValue: "normal",
|
|
296
|
+
required: true,
|
|
297
|
+
size: 10
|
|
298
|
+
}),
|
|
299
|
+
target_type: useSelectField({
|
|
300
|
+
label: { en: "Target Type", es: "Tipo de destino" },
|
|
301
|
+
options: [...NOTIFICATION_TARGET_OPTIONS],
|
|
302
|
+
required: true,
|
|
303
|
+
size: 20
|
|
304
|
+
}),
|
|
305
|
+
target_value: useTextField({ label: { en: "Target Value", es: "Valor del destino" } }),
|
|
306
|
+
link: useUrlField({ label: { en: "Link", es: "Enlace" } }),
|
|
307
|
+
expires_at: useExpiresAtField()
|
|
308
|
+
},
|
|
309
|
+
actions: [
|
|
310
|
+
{
|
|
311
|
+
key: "delete",
|
|
312
|
+
scope: "row",
|
|
313
|
+
icon: "mdi:delete-outline",
|
|
314
|
+
label: { en: "Delete Notification", es: "Eliminar notificaci\xF3n" },
|
|
315
|
+
method: "DELETE",
|
|
316
|
+
handler: async (_ctx, input) => {
|
|
317
|
+
const record = input?._record;
|
|
318
|
+
const id = typeof record?.id === "string" ? record.id : void 0;
|
|
319
|
+
if (!id) return { deleted: false };
|
|
320
|
+
const service = getNotificationService();
|
|
321
|
+
const deleted = await service.delete(id);
|
|
322
|
+
return { deleted };
|
|
323
|
+
},
|
|
324
|
+
casl: {
|
|
325
|
+
subject: "Notification",
|
|
326
|
+
permissions: {
|
|
327
|
+
ADMIN: { actions: ["execute"] }
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
],
|
|
332
|
+
casl: {
|
|
333
|
+
subject: "Notification",
|
|
334
|
+
permissions: {
|
|
335
|
+
MANAGER: { actions: ["read"] },
|
|
336
|
+
EDITOR: { actions: ["read"] },
|
|
337
|
+
CONTRIBUTOR: { actions: ["read"] },
|
|
338
|
+
USER: [
|
|
339
|
+
{ actions: ["read"], conditions: { target_type: "user", target_value: "${user.id}" } },
|
|
340
|
+
{ actions: ["read"], conditions: { target_type: "all" } },
|
|
341
|
+
{ actions: ["read"], conditions: { target_type: "authenticated" } }
|
|
342
|
+
],
|
|
343
|
+
VIEWER: [
|
|
344
|
+
{ actions: ["read"], conditions: { target_type: "user", target_value: "${user.id}" } },
|
|
345
|
+
{ actions: ["read"], conditions: { target_type: "all" } },
|
|
346
|
+
{ actions: ["read"], conditions: { target_type: "authenticated" } }
|
|
347
|
+
],
|
|
348
|
+
SUPPORT: { actions: ["read"] }
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
var notificationReadEntity = {
|
|
353
|
+
type: "event",
|
|
354
|
+
immutable: true,
|
|
355
|
+
table: "notification_reads",
|
|
356
|
+
label: { en: "Notification Read", es: "Lectura de notificaci\xF3n" },
|
|
357
|
+
labelPlural: { en: "Notification Reads", es: "Lecturas de notificaciones" },
|
|
358
|
+
labelField: "notification_id",
|
|
359
|
+
retention: { days: 90 },
|
|
360
|
+
fields: {
|
|
361
|
+
id: useIdField(),
|
|
362
|
+
notification_id: useTextField({ label: { en: "Notification ID", es: "ID de notificaci\xF3n" }, required: true, size: 26, hidden: true }),
|
|
363
|
+
user_id: useTextField({ label: { en: "User ID", es: "ID de usuario" }, required: true, size: 26, hidden: true }),
|
|
364
|
+
read_at: useDatetimeField({ label: { en: "Read At", es: "Le\xEDdo el" }, required: true })
|
|
365
|
+
},
|
|
366
|
+
indexes: [
|
|
367
|
+
{ columns: ["notification_id", "user_id"], unique: true }
|
|
368
|
+
],
|
|
369
|
+
casl: {
|
|
370
|
+
subject: "NotificationRead",
|
|
371
|
+
permissions: {
|
|
372
|
+
ADMIN: { actions: ["manage"] }
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
var sendNotificationAction = {
|
|
377
|
+
key: "send",
|
|
378
|
+
scope: "module",
|
|
379
|
+
label: { en: "Send Notification", es: "Enviar notificaci\xF3n" },
|
|
380
|
+
output: {},
|
|
381
|
+
inputSchema: sendNotificationInputSchema,
|
|
382
|
+
handler: async (_ctx, input) => {
|
|
383
|
+
const service = getNotificationService();
|
|
384
|
+
return service.send(input);
|
|
385
|
+
},
|
|
386
|
+
input: {
|
|
387
|
+
title: useTextField({ label: { en: "Title", es: "T\xEDtulo" }, required: true }),
|
|
388
|
+
message: useTextareaField({ label: { en: "Message", es: "Mensaje" }, required: true }),
|
|
389
|
+
target_type: useSelectField({
|
|
390
|
+
label: { en: "Target Type", es: "Tipo de destino" },
|
|
391
|
+
options: [...NOTIFICATION_TARGET_OPTIONS],
|
|
392
|
+
required: true
|
|
393
|
+
}),
|
|
394
|
+
target_value: useTextField({ label: { en: "Target Value", es: "Valor del destino" } }),
|
|
395
|
+
type: useSelectField({
|
|
396
|
+
label: { en: "Type", es: "Tipo" },
|
|
397
|
+
options: [...NOTIFICATION_TYPE_OPTIONS]
|
|
398
|
+
}),
|
|
399
|
+
priority: useSelectField({
|
|
400
|
+
label: { en: "Priority", es: "Prioridad" },
|
|
401
|
+
options: [...NOTIFICATION_PRIORITY_OPTIONS]
|
|
402
|
+
}),
|
|
403
|
+
link: useUrlField({ label: { en: "Link", es: "Enlace" } }),
|
|
404
|
+
expires_at: useDatetimeField({ label: { en: "Expires At", es: "Expira el" } })
|
|
405
|
+
},
|
|
406
|
+
casl: {
|
|
407
|
+
subject: "Notification",
|
|
408
|
+
permissions: {
|
|
409
|
+
ADMIN: { actions: ["execute"] }
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
// src/notifications.routes.ts
|
|
415
|
+
function createNotificationsRoutes(ctx) {
|
|
416
|
+
const router = ctx.createRouter();
|
|
417
|
+
const { auth } = ctx.core.middleware;
|
|
418
|
+
const { UnauthorizedError, ForbiddenError, NotFoundError, ValidationError } = ctx.core.errors;
|
|
419
|
+
const usersService = ctx.services.get("users");
|
|
420
|
+
if (!auth) {
|
|
421
|
+
throw new Error("Auth middleware not found. Ensure auth module loads before notifications.");
|
|
422
|
+
}
|
|
423
|
+
router.get("/unread", auth, async (req, res) => {
|
|
424
|
+
const user = req.user;
|
|
425
|
+
if (!user) throw new UnauthorizedError("AUTH_REQUIRED");
|
|
426
|
+
const service = getNotificationService();
|
|
427
|
+
const roleIds = await usersService.getRoleIds(user.id);
|
|
428
|
+
const notifications = await service.getUnread(user.id, roleIds, user.created_at);
|
|
429
|
+
res.json({
|
|
430
|
+
items: notifications,
|
|
431
|
+
total: notifications.length,
|
|
432
|
+
page: 1,
|
|
433
|
+
limit: notifications.length,
|
|
434
|
+
totalPages: 1,
|
|
435
|
+
hasNext: false
|
|
436
|
+
});
|
|
437
|
+
});
|
|
438
|
+
router.post("/:id/read", auth, async (req, res) => {
|
|
439
|
+
const user = req.user;
|
|
440
|
+
if (!user) throw new UnauthorizedError("AUTH_REQUIRED");
|
|
441
|
+
const id = req.params["id"];
|
|
442
|
+
if (!id) throw new ValidationError("VALIDATION_ERROR");
|
|
443
|
+
const service = getNotificationService();
|
|
444
|
+
const notification = await service.findById(id);
|
|
445
|
+
if (!notification) throw new NotFoundError("RESOURCE_NOT_FOUND");
|
|
446
|
+
const roleIds = await usersService.getRoleIds(user.id);
|
|
447
|
+
if (!service.canUserAccess(notification, user.id, roleIds)) throw new ForbiddenError("FORBIDDEN");
|
|
448
|
+
await service.markAsRead(id, user.id);
|
|
449
|
+
res.status(204).send();
|
|
450
|
+
});
|
|
451
|
+
router.post("/read-all", auth, async (req, res) => {
|
|
452
|
+
const user = req.user;
|
|
453
|
+
if (!user) throw new UnauthorizedError("AUTH_REQUIRED");
|
|
454
|
+
const service = getNotificationService();
|
|
455
|
+
const roleIds = await usersService.getRoleIds(user.id);
|
|
456
|
+
const count = await service.markAllAsRead(user.id, roleIds, user.created_at);
|
|
457
|
+
res.json({ count });
|
|
458
|
+
});
|
|
459
|
+
router.get("/", auth, async (req, res) => {
|
|
460
|
+
const ability = req.ability;
|
|
461
|
+
if (!ability?.can("manage", "all")) throw new ForbiddenError("FORBIDDEN");
|
|
462
|
+
const limit = Math.min(Math.max(parseInt(req.query["limit"], 10) || 50, 1), 200);
|
|
463
|
+
const page = Math.max(parseInt(req.query["page"], 10) || 1, 1);
|
|
464
|
+
const offset = (page - 1) * limit;
|
|
465
|
+
const notifications = await ctx.db.knex(ctx.db.t("notifications")).orderBy("created_at", "desc").limit(limit).offset(offset);
|
|
466
|
+
const countResult = await ctx.db.knex(ctx.db.t("notifications")).count("* as count").first();
|
|
467
|
+
const total = parseInt(String(countResult?.count || 0));
|
|
468
|
+
const totalPages = Math.ceil(total / limit);
|
|
469
|
+
res.json({
|
|
470
|
+
items: notifications,
|
|
471
|
+
total,
|
|
472
|
+
page,
|
|
473
|
+
limit,
|
|
474
|
+
totalPages,
|
|
475
|
+
hasNext: page < totalPages
|
|
476
|
+
});
|
|
477
|
+
});
|
|
478
|
+
const cleanupRateLimit = ctx.core.middleware.rateLimit({ windowMs: 60 * 1e3, max: 1, message: "Cleanup solo puede ejecutarse 1 vez por minuto" });
|
|
479
|
+
router.post("/cleanup", cleanupRateLimit, auth, async (req, res) => {
|
|
480
|
+
const ability = req.ability;
|
|
481
|
+
if (!ability?.can("manage", "all")) throw new ForbiddenError("FORBIDDEN");
|
|
482
|
+
const service = getNotificationService();
|
|
483
|
+
const deleted = await service.cleanupExpired();
|
|
484
|
+
res.json({ deleted });
|
|
485
|
+
});
|
|
486
|
+
return router;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// src/notifications.socket.ts
|
|
490
|
+
function registerNotificationSocketHandlers(ctx) {
|
|
491
|
+
if (!ctx.core.socket.isInitialized()) {
|
|
492
|
+
ctx.core.logger.debug("Socket.IO not initialized, skipping notification handlers");
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
const io = ctx.core.socket.getIO();
|
|
496
|
+
io.on("connection", (socket) => {
|
|
497
|
+
const { userId, roleIds, authenticated } = socket.data;
|
|
498
|
+
if (!authenticated || !userId) {
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
socket.on("notification:read", async (data, callback) => {
|
|
502
|
+
try {
|
|
503
|
+
if (!data?.notificationId || typeof data.notificationId !== "string") {
|
|
504
|
+
callback?.({ success: false, error: "INVALID_INPUT" });
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
const service = getNotificationService();
|
|
508
|
+
const success = await service.markAsRead(data.notificationId, userId, roleIds);
|
|
509
|
+
if (!success) {
|
|
510
|
+
callback?.({ success: false, error: "NOT_FOUND" });
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
callback?.({ success: true });
|
|
514
|
+
} catch (err) {
|
|
515
|
+
ctx.core.logger.error({ err, userId, notificationId: data?.notificationId }, "Error marking notification as read");
|
|
516
|
+
callback?.({ success: false, error: "INTERNAL_ERROR" });
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
socket.on("notifications:read-all", async (callback) => {
|
|
520
|
+
try {
|
|
521
|
+
const service = getNotificationService();
|
|
522
|
+
const user = await ctx.db.knex("users").where("id", userId).select("created_at").first();
|
|
523
|
+
if (!user) {
|
|
524
|
+
callback?.({ success: false, count: 0, error: "USER_NOT_FOUND" });
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
const count = await service.markAllAsRead(userId, roleIds ?? [], user.created_at);
|
|
528
|
+
callback?.({ success: true, count });
|
|
529
|
+
} catch (err) {
|
|
530
|
+
ctx.core.logger.error({ err, userId }, "Error marking all notifications as read");
|
|
531
|
+
callback?.({ success: false, count: 0, error: "INTERNAL_ERROR" });
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
});
|
|
535
|
+
ctx.core.logger.debug("Notification socket handlers registered");
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// src/notifications-module.ts
|
|
539
|
+
var notificationsModule = {
|
|
540
|
+
name: "notifications",
|
|
541
|
+
label: { en: "Notifications", es: "Notificaciones" },
|
|
542
|
+
icon: "mdi:bell-outline",
|
|
543
|
+
description: { en: "Real-time notifications via Socket.IO", es: "Notificaciones en tiempo real v\xEDa Socket.IO" },
|
|
544
|
+
category: "messaging",
|
|
545
|
+
definitions: [
|
|
546
|
+
notificationEntity,
|
|
547
|
+
notificationReadEntity
|
|
548
|
+
],
|
|
549
|
+
actions: [sendNotificationAction],
|
|
550
|
+
routePrefix: "/notifications",
|
|
551
|
+
routes: createNotificationsRoutes,
|
|
552
|
+
init: (ctx) => {
|
|
553
|
+
const service = initNotificationService(ctx);
|
|
554
|
+
ctx.services.register("notifications", service);
|
|
555
|
+
ctx.core.socket.onReady(() => registerNotificationSocketHandlers(ctx));
|
|
556
|
+
ctx.core.logger.debug("Notifications module initialized");
|
|
557
|
+
}
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
// src/index.ts
|
|
561
|
+
var notificationsPlugin = {
|
|
562
|
+
name: "@gzl10/nexus-plugin-notifications",
|
|
563
|
+
code: "ntf",
|
|
564
|
+
label: { en: "Notifications", es: "Notificaciones" },
|
|
565
|
+
icon: "mdi:bell-outline",
|
|
566
|
+
category: "messaging",
|
|
567
|
+
version: "0.14.0",
|
|
568
|
+
description: {
|
|
569
|
+
en: "Real-time in-app notifications via Socket.IO",
|
|
570
|
+
es: "Notificaciones in-app en tiempo real v\xEDa Socket.IO"
|
|
571
|
+
},
|
|
572
|
+
modules: [notificationsModule]
|
|
573
|
+
};
|
|
574
|
+
var index_default = notificationsPlugin;
|
|
575
|
+
export {
|
|
576
|
+
NOTIFICATION_PRIORITY_OPTIONS,
|
|
577
|
+
NOTIFICATION_TARGET_OPTIONS,
|
|
578
|
+
NOTIFICATION_TYPE_OPTIONS,
|
|
579
|
+
NotificationService,
|
|
580
|
+
index_default as default,
|
|
581
|
+
getNotificationService,
|
|
582
|
+
initNotificationService,
|
|
583
|
+
notificationEntity,
|
|
584
|
+
notificationReadEntity,
|
|
585
|
+
notificationsPlugin,
|
|
586
|
+
sendNotificationInputSchema
|
|
587
|
+
};
|
|
588
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/notifications.entity.ts","../src/notifications.service.ts","../src/notifications.types.ts","../src/notifications.routes.ts","../src/notifications.socket.ts","../src/notifications-module.ts","../src/index.ts"],"sourcesContent":["import type { EventEntityDefinition, ActionDefinition } from '@gzl10/nexus-sdk'\nimport { useIdField, useTextField, useTextareaField, useSelectField, useUrlField, useDatetimeField, useExpiresAtField } from '@gzl10/nexus-sdk/fields'\nimport { getNotificationService } from './notifications.service.js'\nimport {\n NOTIFICATION_TYPE_OPTIONS,\n NOTIFICATION_PRIORITY_OPTIONS,\n NOTIFICATION_TARGET_OPTIONS,\n sendNotificationInputSchema\n} from './notifications.types.js'\nimport type { SendNotificationInput } from './notifications.types.js'\n\n/**\n * Notification (event) - ephemeral notifications with TTL\n */\nexport const notificationEntity: EventEntityDefinition = {\n type: 'event',\n realtime: 'sync',\n immutable: true,\n table: 'notifications',\n routePrefix: '/',\n label: { en: 'Notification', es: 'Notificación' },\n labelPlural: { en: 'Notifications', es: 'Notificaciones' },\n labelField: 'title',\n timestamps: true,\n retention: { days: 30 },\n calendarFrom: 'expires_at',\n fields: {\n id: useIdField(),\n title: useTextField({ label: { en: 'Title', es: 'Título' }, required: true }),\n message: useTextareaField({ label: { en: 'Message', es: 'Mensaje' }, required: true }),\n type: useSelectField({\n label: { en: 'Type', es: 'Tipo' },\n options: [...NOTIFICATION_TYPE_OPTIONS],\n defaultValue: 'info',\n required: true,\n size: 20\n }),\n priority: useSelectField({\n label: { en: 'Priority', es: 'Prioridad' },\n options: [...NOTIFICATION_PRIORITY_OPTIONS],\n defaultValue: 'normal',\n required: true,\n size: 10\n }),\n target_type: useSelectField({\n label: { en: 'Target Type', es: 'Tipo de destino' },\n options: [...NOTIFICATION_TARGET_OPTIONS],\n required: true,\n size: 20\n }),\n target_value: useTextField({ label: { en: 'Target Value', es: 'Valor del destino' } }),\n link: useUrlField({ label: { en: 'Link', es: 'Enlace' } }),\n expires_at: useExpiresAtField()\n },\n actions: [\n {\n key: 'delete',\n scope: 'row',\n icon: 'mdi:delete-outline',\n label: { en: 'Delete Notification', es: 'Eliminar notificación' },\n method: 'DELETE',\n handler: async (_ctx, input: unknown) => {\n const record = (input as Record<string, unknown>)?._record as Record<string, unknown> | undefined\n const id = typeof record?.id === 'string' ? record.id : undefined\n if (!id) return { deleted: false }\n const service = getNotificationService()\n const deleted = await service.delete(id)\n return { deleted }\n },\n casl: {\n subject: 'Notification',\n permissions: {\n ADMIN: { actions: ['execute'] }\n }\n }\n } satisfies ActionDefinition\n ],\n casl: {\n subject: 'Notification',\n permissions: {\n MANAGER: { actions: ['read'] },\n EDITOR: { actions: ['read'] },\n CONTRIBUTOR: { actions: ['read'] },\n USER: [\n { actions: ['read'], conditions: { target_type: 'user', target_value: '${user.id}' } },\n { actions: ['read'], conditions: { target_type: 'all' } },\n { actions: ['read'], conditions: { target_type: 'authenticated' } },\n ],\n VIEWER: [\n { actions: ['read'], conditions: { target_type: 'user', target_value: '${user.id}' } },\n { actions: ['read'], conditions: { target_type: 'all' } },\n { actions: ['read'], conditions: { target_type: 'authenticated' } },\n ],\n SUPPORT: { actions: ['read'] }\n }\n }\n}\n\n/**\n * NotificationRead (event) - read log\n */\nexport const notificationReadEntity: EventEntityDefinition = {\n type: 'event',\n immutable: true,\n table: 'notification_reads',\n label: { en: 'Notification Read', es: 'Lectura de notificación' },\n labelPlural: { en: 'Notification Reads', es: 'Lecturas de notificaciones' },\n labelField: 'notification_id',\n retention: { days: 90 },\n fields: {\n id: useIdField(),\n notification_id: useTextField({ label: { en: 'Notification ID', es: 'ID de notificación' }, required: true, size: 26, hidden: true }),\n user_id: useTextField({ label: { en: 'User ID', es: 'ID de usuario' }, required: true, size: 26, hidden: true }),\n read_at: useDatetimeField({ label: { en: 'Read At', es: 'Leído el' }, required: true })\n },\n indexes: [\n { columns: ['notification_id', 'user_id'], unique: true }\n ],\n casl: {\n subject: 'NotificationRead',\n permissions: {\n ADMIN: { actions: ['manage'] }\n }\n }\n}\n\n/**\n * SendNotification (action) - send notification\n */\nexport const sendNotificationAction: ActionDefinition = {\n key: \"send\",\n scope: \"module\",\n label: { en: \"Send Notification\", es: \"Enviar notificación\" },\n output: {},\n inputSchema: sendNotificationInputSchema,\n handler: async (_ctx, input: unknown) => {\n // Usar getNotificationService() directamente, no ctx.services\n // porque createModuleRouters() sobrescribe ctx.services['notifications']\n // con el EventEntityService de notificationEntity\n const service = getNotificationService();\n return service.send(input as SendNotificationInput);\n },\n input: {\n title: useTextField({ label: { en: 'Title', es: 'Título' }, required: true }),\n message: useTextareaField({ label: { en: 'Message', es: 'Mensaje' }, required: true }),\n target_type: useSelectField({\n label: { en: 'Target Type', es: 'Tipo de destino' },\n options: [...NOTIFICATION_TARGET_OPTIONS],\n required: true,\n }),\n target_value: useTextField({ label: { en: 'Target Value', es: 'Valor del destino' } }),\n type: useSelectField({\n label: { en: 'Type', es: 'Tipo' },\n options: [...NOTIFICATION_TYPE_OPTIONS],\n }),\n priority: useSelectField({\n label: { en: 'Priority', es: 'Prioridad' },\n options: [...NOTIFICATION_PRIORITY_OPTIONS],\n }),\n link: useUrlField({ label: { en: 'Link', es: 'Enlace' } }),\n expires_at: useDatetimeField({ label: { en: 'Expires At', es: 'Expira el' } }),\n },\n casl: {\n subject: \"Notification\",\n permissions: {\n ADMIN: { actions: [\"execute\"] },\n },\n },\n};\n","import type { Knex } from 'knex'\nimport type { Logger } from 'pino'\nimport type { ModuleContext } from '@gzl10/nexus-sdk'\nimport type {\n SendNotificationInput,\n Notification,\n NotificationPayload\n} from './notifications.types.js'\n\n/**\n * Notifications service\n */\nexport class NotificationService {\n private db: Knex\n private logger: Logger\n private generateId: () => string\n private nowTimestamp: (db: Knex) => string\n private formatTimestamp: (db: Knex, date?: Date) => string\n private safeJsonParse: ModuleContext['core']['safeJsonParse']\n private socket: ModuleContext['core']['socket']\n private eventsNotify: ModuleContext['events']['notify']\n private t: (name: string) => string\n\n constructor(ctx: ModuleContext) {\n this.db = ctx.db.knex\n this.logger = ctx.core.logger.child({ service: 'notifications' })\n this.generateId = ctx.core.generateId\n this.nowTimestamp = ctx.db.nowTimestamp\n this.formatTimestamp = ctx.db.formatTimestamp\n this.safeJsonParse = ctx.core.safeJsonParse\n this.socket = ctx.core.socket\n this.eventsNotify = ctx.events.notify.bind(ctx.events)\n this.t = ctx.db.t\n }\n\n /**\n * Sends a notification\n */\n async send(options: SendNotificationInput): Promise<{ id: string; sent: number }> {\n const { target_type, target_value } = options\n let sent = 0\n\n const type = options.type || 'info'\n const priority = options.priority || 'normal'\n\n // 1. Persistir notificación\n const id = this.generateId()\n const now = this.nowTimestamp(this.db)\n\n await this.db(this.t('notifications')).insert({\n id,\n title: options.title,\n message: options.message,\n type,\n priority,\n target_type,\n target_value: target_value || null,\n link: options.link || null,\n expires_at: options.expires_at ? this.formatTimestamp(this.db, new Date(options.expires_at)) : null,\n created_at: now\n })\n\n // 2. Emitir por Socket.IO si está inicializado (via ctx.core.socket helpers)\n if (this.socket.isInitialized()) {\n const payload: NotificationPayload = {\n id,\n title: options.title,\n message: options.message,\n type,\n priority,\n link: options.link || undefined,\n timestamp: now\n }\n\n switch (target_type) {\n case 'all':\n this.socket.emitToAll('notification', payload)\n sent = 1 // broadcast, exact count not critical\n break\n\n case 'authenticated':\n this.socket.emitToAuthenticated('notification', payload)\n sent = 1\n break\n\n case 'role':\n if (target_value) {\n this.socket.emitToRole(target_value, 'notification', payload)\n sent = 1\n }\n break\n\n case 'users':\n if (target_value) {\n const userIds = this.safeJsonParse<string[]>(\n target_value,\n [],\n { target_value, context: 'send.users' }\n )\n for (const userId of userIds) {\n this.socket.emitToUser(userId, 'notification', payload)\n }\n sent = userIds.length\n }\n break\n\n case 'user':\n if (target_value) {\n this.socket.emitToUser(target_value, 'notification', payload)\n sent = this.socket.isUserConnected(target_value) ? 1 : 0\n }\n break\n }\n\n this.logger.debug({ id, target_type, sent }, 'Notification sent')\n }\n\n // 3. Emitir evento interno\n this.eventsNotify('notifications.sent', { id, target_type, target_value, sent })\n\n return { id, sent }\n }\n\n /**\n * Checks if a user has access to a notification based on its target\n */\n canUserAccess(notification: Notification, userId: string, roleIds?: string[]): boolean {\n switch (notification.target_type) {\n case 'all':\n case 'authenticated':\n return true\n case 'role':\n return roleIds ? roleIds.includes(notification.target_value || '') : false\n case 'users': {\n const userIds = this.safeJsonParse<string[]>(\n notification.target_value || '[]',\n [],\n { notificationId: notification.id, context: 'canUserAccess.users' }\n )\n return userIds.includes(userId)\n }\n case 'user':\n return notification.target_value === userId\n default:\n return false\n }\n }\n\n /**\n * Marks a notification as read by a user.\n * If roleIds is provided, verifies access before marking.\n * Uses onConflict to prevent duplicates atomically.\n */\n async markAsRead(notificationId: string, userId: string, roleIds?: string[]): Promise<boolean> {\n const notification = await this.findById(notificationId)\n if (!notification) return false\n\n // Access check when roleIds provided (Socket.IO path)\n if (roleIds !== undefined && !this.canUserAccess(notification, userId, roleIds)) {\n return false\n }\n\n await this.db(this.t('notification_reads'))\n .insert({\n id: this.generateId(),\n notification_id: notificationId,\n user_id: userId,\n read_at: this.nowTimestamp(this.db)\n })\n .onConflict(['notification_id', 'user_id'])\n .ignore()\n\n // Emit Socket.IO for tab sync\n if (this.socket.isInitialized()) {\n this.socket.emitToUser(userId, 'notification:read', { notificationId })\n }\n\n this.logger.debug({ notificationId, userId }, 'Notification marked as read')\n return true\n }\n\n /**\n * Marks all unread notifications as read by a user.\n * Batches DB operations for efficiency.\n */\n async markAllAsRead(userId: string, roleIds?: string[], userCreatedAt?: Date | string): Promise<number> {\n const unread = await this.getUnread(userId, roleIds, userCreatedAt)\n if (unread.length === 0) return 0\n\n const now = this.nowTimestamp(this.db)\n\n // Batch insert into notification_reads (UNIQUE constraint prevents duplicates)\n const reads = unread.map(n => ({\n id: this.generateId(),\n notification_id: n.id,\n user_id: userId,\n read_at: now\n }))\n\n // Batch insert with conflict handling — idempotent under concurrent calls\n await this.db.transaction(async (trx) => {\n for (const read of reads) {\n await trx(this.t('notification_reads'))\n .insert(read)\n .onConflict(['notification_id', 'user_id'])\n .ignore()\n }\n })\n\n // Single batch event for tab sync (not N individual events)\n if (this.socket.isInitialized()) {\n this.socket.emitToUser(userId, 'notification:read', {\n notificationIds: unread.map(n => n.id)\n })\n }\n\n this.logger.debug({ userId, count: unread.length }, 'All notifications marked as read')\n return unread.length\n }\n\n /**\n * Gets unread notifications for a user.\n * Only returns notifications created after the user's registration date.\n * Uses LEFT JOIN with notification_reads to determine read status.\n */\n async getUnread(userId: string, roleIds?: string[], userCreatedAt?: Date | string): Promise<Notification[]> {\n const now = this.nowTimestamp(this.db)\n\n const db = this.db\n const t = this.t\n const query = db(t('notifications'))\n .leftJoin(t('notification_reads'), function () {\n this.on(`${t('notifications')}.id`, '=', `${t('notification_reads')}.notification_id`)\n .andOn(`${t('notification_reads')}.user_id`, '=', db.raw('?', [userId]))\n })\n .whereNull(`${t('notification_reads')}.id`)\n .where(function () {\n this.whereNull(`${t('notifications')}.expires_at`).orWhere(`${t('notifications')}.expires_at`, '>', now)\n })\n .select(`${t('notifications')}.*`)\n .orderBy(`${t('notifications')}.created_at`, 'desc')\n\n // Filter out notifications created before the user existed\n if (userCreatedAt) {\n query.andWhere(`${t('notifications')}.created_at`, '>=', userCreatedAt)\n }\n\n const notifications = await query\n\n // Filter by target access in memory\n return notifications.filter((n: Notification) => this.canUserAccess(n, userId, roleIds))\n }\n\n /**\n * Gets a notification by ID\n */\n async findById(id: string): Promise<Notification | null> {\n const notification = await this.db(this.t('notifications')).where('id', id).first()\n return notification || null\n }\n\n /**\n * Deletes a notification\n */\n async delete(id: string): Promise<boolean> {\n const deleted = await this.db(this.t('notifications')).where('id', id).delete()\n return deleted > 0\n }\n\n /**\n * Cleans up expired notifications\n */\n async cleanupExpired(): Promise<number> {\n const deleted = await this.db(this.t('notifications'))\n .where('expires_at', '<', this.nowTimestamp(this.db))\n .whereNotNull('expires_at')\n .delete()\n\n if (deleted > 0) {\n this.logger.info({ deleted }, 'Cleaned up expired notifications')\n }\n\n return deleted\n }\n}\n\nlet serviceInstance: NotificationService | null = null\n\n/**\n * Initializes the notifications service\n */\nexport function initNotificationService(ctx: ModuleContext): NotificationService {\n serviceInstance = new NotificationService(ctx)\n return serviceInstance\n}\n\n/**\n * Gets the service instance\n */\nexport function getNotificationService(): NotificationService {\n if (!serviceInstance) {\n throw new Error('NotificationService not initialized')\n }\n return serviceInstance\n}\n","import { z } from 'zod'\n\n// ============================================================================\n// TYPE ALIASES\n// ============================================================================\n\n/**\n * Notification types\n */\nexport type NotificationType = 'info' | 'warning' | 'error' | 'success'\n\n/**\n * Notification priority\n */\nexport type NotificationPriority = 'low' | 'normal' | 'high' | 'urgent'\n\n/**\n * Target types for notifications\n */\nexport type NotificationTarget = 'all' | 'authenticated' | 'role' | 'users' | 'user'\n\n// ============================================================================\n// SHARED OPTIONS (DRY)\n// ============================================================================\n\n/**\n * Notification type options for select fields\n */\nexport const NOTIFICATION_TYPE_OPTIONS = [\n { value: 'info', label: { en: 'Info', es: 'Información' } },\n { value: 'success', label: { en: 'Success', es: 'Éxito' } },\n { value: 'warning', label: { en: 'Warning', es: 'Advertencia' } },\n { value: 'error', label: { en: 'Error', es: 'Error' } }\n] as const\n\n/**\n * Notification priority options for select fields\n */\nexport const NOTIFICATION_PRIORITY_OPTIONS = [\n { value: 'low', label: { en: 'Low', es: 'Baja' } },\n { value: 'normal', label: { en: 'Normal', es: 'Normal' } },\n { value: 'high', label: { en: 'High', es: 'Alta' } },\n { value: 'urgent', label: { en: 'Urgent', es: 'Urgente' } }\n] as const\n\n/**\n * Notification target type options for select fields\n */\nexport const NOTIFICATION_TARGET_OPTIONS = [\n { value: 'user', label: { en: 'User', es: 'Usuario' } },\n { value: 'role', label: { en: 'Role', es: 'Rol' } },\n { value: 'all', label: { en: 'All users', es: 'Todos los usuarios' } }\n] as const\n\n// ============================================================================\n// ZOD SCHEMAS\n// ============================================================================\n\n/**\n * Input schema for sending notifications\n */\n// Preprocess: convert empty strings to undefined for optional fields\nconst emptyToUndefined = z.preprocess((v) => (v === '' ? undefined : v), z.any())\n\nexport const sendNotificationInputSchema = z.object({\n title: z.string().min(1, 'Title is required'),\n message: z.string().min(1, 'Message is required'),\n type: emptyToUndefined.pipe(z.enum(['info', 'warning', 'error', 'success']).optional().default('info')),\n priority: emptyToUndefined.pipe(z.enum(['low', 'normal', 'high', 'urgent']).optional().default('normal')),\n target_type: z.enum(['all', 'authenticated', 'role', 'users', 'user']),\n target_value: z.string().optional(),\n link: z.string().url().optional().or(z.literal('')),\n expires_at: emptyToUndefined.pipe(z.string().datetime().optional())\n}).refine(\n (data) => {\n if (['role', 'user', 'users'].includes(data.target_type)) {\n return !!data.target_value\n }\n return true\n },\n { message: 'target_value is required for role, user, and users target types', path: ['target_value'] }\n)\n\nexport type SendNotificationInput = z.output<typeof sendNotificationInputSchema>\n\n\n// ============================================================================\n// ENTITY TYPES\n// ============================================================================\n\n/**\n * Notification in the DB\n */\nexport interface Notification {\n id: string\n title: string\n message: string\n type: NotificationType\n priority: NotificationPriority\n target_type: NotificationTarget\n target_value: string | null\n link: string | null\n expires_at: Date | null\n created_at: Date\n}\n\n/**\n * Payload sent over Socket.IO\n */\nexport interface NotificationPayload {\n id: string\n title: string\n message: string\n type: NotificationType\n priority: NotificationPriority\n link?: string\n timestamp: string\n}\n","import type { Request, Response, ModuleContext, Router, AuthRequest, BaseUsersService } from '@gzl10/nexus-sdk'\nimport { getNotificationService } from './notifications.service.js'\n\n/**\n * Notifications Routes\n *\n * Auto-mounted from definitions:\n * - sendNotificationAction (action) -> POST /notifications/send\n * - notificationEntity.actions[delete] (row action) -> DELETE /notifications/delete/:id\n *\n * Manual routes defined here:\n * - GET /notifications (admin list)\n * - GET /notifications/unread (user's unread)\n * - POST /notifications/:id/read (mark as read)\n * - POST /notifications/read-all (mark all as read)\n * - POST /notifications/cleanup (admin cleanup)\n */\nexport function createNotificationsRoutes(ctx: ModuleContext): Router {\n const router = ctx.createRouter()\n const { auth } = ctx.core.middleware\n const { UnauthorizedError, ForbiddenError, NotFoundError, ValidationError } = ctx.core.errors\n const usersService = ctx.services.get<BaseUsersService>('users')\n\n if (!auth) {\n throw new Error('Auth middleware not found. Ensure auth module loads before notifications.')\n }\n\n /**\n * GET /notifications/unread\n * Gets unread notifications for the authenticated user\n */\n router.get('/unread', auth, async (req: Request, res: Response) => {\n const user = (req as AuthRequest).user\n if (!user) throw new UnauthorizedError('AUTH_REQUIRED')\n\n const service = getNotificationService()\n const roleIds = await usersService.getRoleIds(user.id)\n const notifications = await service.getUnread(user.id, roleIds, user.created_at)\n\n res.json({\n items: notifications,\n total: notifications.length,\n page: 1,\n limit: notifications.length,\n totalPages: 1,\n hasNext: false\n })\n })\n\n /**\n * POST /notifications/:id/read\n * Marks a notification as read (validates user has access)\n */\n router.post('/:id/read', auth, async (req: Request, res: Response) => {\n const user = (req as AuthRequest).user\n if (!user) throw new UnauthorizedError('AUTH_REQUIRED')\n\n const id = req.params['id']\n if (!id) throw new ValidationError('VALIDATION_ERROR')\n\n const service = getNotificationService()\n\n // Verify notification exists and user has access\n const notification = await service.findById(id)\n if (!notification) throw new NotFoundError('RESOURCE_NOT_FOUND')\n\n const roleIds = await usersService.getRoleIds(user.id)\n if (!service.canUserAccess(notification, user.id, roleIds)) throw new ForbiddenError('FORBIDDEN')\n\n await service.markAsRead(id, user.id)\n res.status(204).send()\n })\n\n /**\n * POST /notifications/read-all\n * Marks all notifications as read\n */\n router.post('/read-all', auth, async (req: Request, res: Response) => {\n const user = (req as AuthRequest).user\n if (!user) throw new UnauthorizedError('AUTH_REQUIRED')\n\n const service = getNotificationService()\n const roleIds = await usersService.getRoleIds(user.id)\n const count = await service.markAllAsRead(user.id, roleIds, user.created_at)\n\n res.json({ count })\n })\n\n /**\n * GET /notifications\n * Lists all notifications (admin only)\n * Note: Manual route because event entity auto-mount uses different path\n */\n router.get('/', auth, async (req: Request, res: Response) => {\n const ability = (req as AuthRequest).ability\n if (!ability?.can('manage', 'all')) throw new ForbiddenError('FORBIDDEN')\n\n const limit = Math.min(Math.max(parseInt(req.query['limit'] as string, 10) || 50, 1), 200)\n const page = Math.max(parseInt(req.query['page'] as string, 10) || 1, 1)\n const offset = (page - 1) * limit\n\n const notifications = await ctx.db.knex(ctx.db.t('notifications'))\n .orderBy('created_at', 'desc')\n .limit(limit)\n .offset(offset)\n\n const countResult = await ctx.db.knex(ctx.db.t('notifications')).count('* as count').first<{ count: string | number }>()\n const total = parseInt(String(countResult?.count || 0))\n const totalPages = Math.ceil(total / limit)\n\n res.json({\n items: notifications,\n total,\n page,\n limit,\n totalPages,\n hasNext: page < totalPages\n })\n })\n\n /**\n * POST /notifications/cleanup\n * Cleans up expired notifications (admin only)\n */\n const cleanupRateLimit = ctx.core.middleware.rateLimit({ windowMs: 60 * 1000, max: 1, message: 'Cleanup solo puede ejecutarse 1 vez por minuto' })\n router.post('/cleanup', cleanupRateLimit, auth, async (req: Request, res: Response) => {\n const ability = (req as AuthRequest).ability\n if (!ability?.can('manage', 'all')) throw new ForbiddenError('FORBIDDEN')\n\n const service = getNotificationService()\n const deleted = await service.cleanupExpired()\n\n res.json({ deleted })\n })\n\n return router\n}\n","import type { ModuleContext } from '@gzl10/nexus-sdk'\nimport { getNotificationService } from './notifications.service.js'\n\ninterface SocketLike {\n data: { userId?: string; roleIds?: string[]; authenticated?: boolean }\n on(event: string, handler: (...args: unknown[]) => void): void\n}\n\n/**\n * Registers Socket.IO handlers for notifications\n */\nexport function registerNotificationSocketHandlers(ctx: ModuleContext): void {\n if (!ctx.core.socket.isInitialized()) {\n ctx.core.logger.debug('Socket.IO not initialized, skipping notification handlers')\n return\n }\n\n const io = ctx.core.socket.getIO() as unknown as { on(event: string, handler: (socket: SocketLike) => void): void }\n\n io.on('connection', (socket: SocketLike) => {\n const { userId, roleIds, authenticated } = socket.data\n\n if (!authenticated || !userId) {\n return\n }\n\n /**\n * notification:read - Mark notification as read (with access check)\n */\n socket.on('notification:read', async (data: { notificationId: string }, callback?: (res: { success: boolean; error?: string }) => void) => {\n try {\n if (!data?.notificationId || typeof data.notificationId !== 'string') {\n callback?.({ success: false, error: 'INVALID_INPUT' })\n return\n }\n\n const service = getNotificationService()\n // roleIds passed → service verifies access internally (single query)\n const success = await service.markAsRead(data.notificationId, userId, roleIds)\n if (!success) {\n callback?.({ success: false, error: 'NOT_FOUND' })\n return\n }\n callback?.({ success: true })\n } catch (err) {\n ctx.core.logger.error({ err, userId, notificationId: data?.notificationId }, 'Error marking notification as read')\n callback?.({ success: false, error: 'INTERNAL_ERROR' })\n }\n })\n\n /**\n * notifications:read-all - Mark all as read\n */\n socket.on('notifications:read-all', async (callback?: (res: { success: boolean; count: number; error?: string }) => void) => {\n try {\n const service = getNotificationService()\n const user = await ctx.db.knex('users').where('id', userId).select('created_at').first()\n\n if (!user) {\n callback?.({ success: false, count: 0, error: 'USER_NOT_FOUND' })\n return\n }\n\n const count = await service.markAllAsRead(userId, roleIds ?? [], user.created_at)\n callback?.({ success: true, count })\n } catch (err) {\n ctx.core.logger.error({ err, userId }, 'Error marking all notifications as read')\n callback?.({ success: false, count: 0, error: 'INTERNAL_ERROR' })\n }\n })\n })\n\n ctx.core.logger.debug('Notification socket handlers registered')\n}\n","/**\n * @module notifications\n * @description Real-time notifications with Socket.IO delivery and persistence\n */\n\nimport type { ModuleManifest } from '@gzl10/nexus-sdk'\nimport {\n notificationEntity,\n notificationReadEntity,\n sendNotificationAction\n} from './notifications.entity.js'\nimport { createNotificationsRoutes } from './notifications.routes.js'\nimport { initNotificationService, getNotificationService, NotificationService } from './notifications.service.js'\nimport { registerNotificationSocketHandlers } from './notifications.socket.js'\n\nexport {\n NotificationService,\n getNotificationService,\n initNotificationService\n}\n\n// Re-export all types from the centralized types file\nexport type {\n NotificationType,\n NotificationPriority,\n NotificationTarget,\n SendNotificationInput,\n Notification,\n NotificationPayload\n} from './notifications.types.js'\n\nexport {\n NOTIFICATION_TYPE_OPTIONS,\n NOTIFICATION_PRIORITY_OPTIONS,\n NOTIFICATION_TARGET_OPTIONS,\n sendNotificationInputSchema\n} from './notifications.types.js'\n\n/**\n * Real-time notifications module\n */\nexport const notificationsModule: ModuleManifest = {\n name: 'notifications',\n label: { en: 'Notifications', es: 'Notificaciones' },\n icon: 'mdi:bell-outline',\n description: { en: 'Real-time notifications via Socket.IO', es: 'Notificaciones en tiempo real vía Socket.IO' },\n category: 'messaging',\n definitions: [\n notificationEntity,\n notificationReadEntity\n ],\n\n actions: [sendNotificationAction],\n\n routePrefix: '/notifications',\n routes: createNotificationsRoutes,\n\n init: (ctx) => {\n const service = initNotificationService(ctx)\n ctx.services.register('notifications', service)\n\n // Registrar handlers Socket.IO cuando esté listo\n ctx.core.socket.onReady(() => registerNotificationSocketHandlers(ctx))\n\n ctx.core.logger.debug('Notifications module initialized')\n }\n}\n","/**\n * @module @gzl10/nexus-plugin-notifications\n * @description Real-time in-app notifications via Socket.IO\n */\n\nimport type { PluginManifest } from '@gzl10/nexus-sdk'\nimport { notificationsModule } from './notifications-module.js'\n\nexport { NotificationService, getNotificationService, initNotificationService } from './notifications.service.js'\n\nexport type {\n NotificationType,\n NotificationPriority,\n NotificationTarget,\n SendNotificationInput,\n Notification,\n NotificationPayload\n} from './notifications.types.js'\n\nexport {\n NOTIFICATION_TYPE_OPTIONS,\n NOTIFICATION_PRIORITY_OPTIONS,\n NOTIFICATION_TARGET_OPTIONS,\n sendNotificationInputSchema\n} from './notifications.types.js'\n\nexport { notificationEntity, notificationReadEntity } from './notifications.entity.js'\n\nexport const notificationsPlugin: PluginManifest = {\n name: '@gzl10/nexus-plugin-notifications',\n code: 'ntf',\n label: { en: 'Notifications', es: 'Notificaciones' },\n icon: 'mdi:bell-outline',\n category: 'messaging',\n version: '0.14.0',\n description: {\n en: 'Real-time in-app notifications via Socket.IO',\n es: 'Notificaciones in-app en tiempo real vía Socket.IO'\n },\n modules: [notificationsModule]\n}\n\nexport default notificationsPlugin\n"],"mappings":"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","names":[]}
|
package/image.png
ADDED
|
Binary file
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** @param {import('knex').Knex} knex */
|
|
2
|
+
export async function up(knex) {
|
|
3
|
+
if (!(await knex.schema.hasTable('ntf_notifications'))) {
|
|
4
|
+
await knex.schema.createTable('ntf_notifications', (table) => {
|
|
5
|
+
table.string('id', 26).notNullable().primary()
|
|
6
|
+
table.string('title', 255).notNullable()
|
|
7
|
+
table.text('message').notNullable()
|
|
8
|
+
table.string('type', 20).notNullable().defaultTo('info')
|
|
9
|
+
table.string('priority', 10).notNullable().defaultTo('normal')
|
|
10
|
+
table.string('target_type', 20).notNullable()
|
|
11
|
+
table.string('target_value', 255).nullable()
|
|
12
|
+
table.string('link', 500).nullable()
|
|
13
|
+
table.timestamp('expires_at').nullable()
|
|
14
|
+
// timestamps
|
|
15
|
+
table.timestamp('created_at').nullable().defaultTo(knex.fn.now())
|
|
16
|
+
table.timestamp('updated_at').nullable().defaultTo(knex.fn.now())
|
|
17
|
+
})
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (!(await knex.schema.hasTable('ntf_notification_reads'))) {
|
|
21
|
+
await knex.schema.createTable('ntf_notification_reads', (table) => {
|
|
22
|
+
table.string('id', 26).notNullable().primary()
|
|
23
|
+
table.string('notification_id', 26).notNullable()
|
|
24
|
+
table.string('user_id', 26).notNullable()
|
|
25
|
+
table.timestamp('read_at').notNullable()
|
|
26
|
+
table.unique(['notification_id', 'user_id'])
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** @param {import('knex').Knex} knex */
|
|
33
|
+
export async function down(knex) {
|
|
34
|
+
await knex.schema.dropTableIfExists('ntf_notification_reads')
|
|
35
|
+
await knex.schema.dropTableIfExists('ntf_notifications')
|
|
36
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gzl10/nexus-plugin-notifications",
|
|
3
|
+
"version": "0.16.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Nexus Plugin: Real-time in-app notifications via Socket.IO",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"import": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts"
|
|
11
|
+
},
|
|
12
|
+
"./client": {
|
|
13
|
+
"import": "./dist/client/index.js",
|
|
14
|
+
"types": "./dist/client/index.d.ts"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"migrations",
|
|
20
|
+
"image.png"
|
|
21
|
+
],
|
|
22
|
+
"peerDependencies": {
|
|
23
|
+
"@gzl10/nexus-sdk": ">=0.14.0",
|
|
24
|
+
"knex": "^3.0.0",
|
|
25
|
+
"pino": "^9.0.0 || ^10.0.0",
|
|
26
|
+
"zod": "^3.0.0"
|
|
27
|
+
},
|
|
28
|
+
"peerDependenciesMeta": {
|
|
29
|
+
"knex": {
|
|
30
|
+
"optional": true
|
|
31
|
+
},
|
|
32
|
+
"pino": {
|
|
33
|
+
"optional": true
|
|
34
|
+
},
|
|
35
|
+
"zod": {
|
|
36
|
+
"optional": true
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@gzl10/nexus-sdk": "0.16.1",
|
|
41
|
+
"@gzl10/nexus-client": "0.16.1"
|
|
42
|
+
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "tsup",
|
|
45
|
+
"lint": "eslint .",
|
|
46
|
+
"typecheck": "tsc --noEmit"
|
|
47
|
+
}
|
|
48
|
+
}
|