@endora-commerce/mod-admin-notifications 0.0.0-stage → 0.100.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +49 -2
- package/dist/backend/entities/admin-notification-read.entity.d.ts +15 -0
- package/dist/backend/entities/admin-notification-read.entity.d.ts.map +1 -0
- package/dist/backend/entities/admin-notification-read.entity.js +42 -0
- package/dist/backend/entities/admin-notification-read.entity.js.map +1 -0
- package/dist/backend/entities/admin-notification.entity.d.ts +31 -0
- package/dist/backend/entities/admin-notification.entity.d.ts.map +1 -0
- package/dist/backend/entities/admin-notification.entity.js +95 -0
- package/dist/backend/entities/admin-notification.entity.js.map +1 -0
- package/dist/backend/index.d.ts +25 -0
- package/dist/backend/index.d.ts.map +1 -0
- package/dist/backend/index.js +70 -0
- package/dist/backend/index.js.map +1 -0
- package/dist/backend/plugin.d.ts +26 -0
- package/dist/backend/plugin.d.ts.map +1 -0
- package/dist/backend/plugin.js +17 -0
- package/dist/backend/plugin.js.map +1 -0
- package/dist/backend/routes.admin.d.ts +16 -0
- package/dist/backend/routes.admin.d.ts.map +1 -0
- package/dist/backend/routes.admin.js +78 -0
- package/dist/backend/routes.admin.js.map +1 -0
- package/dist/backend/services/admin-notification-port.d.ts +17 -0
- package/dist/backend/services/admin-notification-port.d.ts.map +1 -0
- package/dist/backend/services/admin-notification-port.js +33 -0
- package/dist/backend/services/admin-notification-port.js.map +1 -0
- package/dist/backend/services/admin-notification-service.d.ts +78 -0
- package/dist/backend/services/admin-notification-service.d.ts.map +1 -0
- package/dist/backend/services/admin-notification-service.js +284 -0
- package/dist/backend/services/admin-notification-service.js.map +1 -0
- package/dist/manifest.d.ts +171 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +46 -0
- package/dist/manifest.js.map +1 -0
- package/dist/migrations/20260611T140350_admin_notifications_init.d.ts +19 -0
- package/dist/migrations/20260611T140350_admin_notifications_init.d.ts.map +1 -0
- package/dist/migrations/20260611T140350_admin_notifications_init.js +82 -0
- package/dist/migrations/20260611T140350_admin_notifications_init.js.map +1 -0
- package/dist/migrations/index.d.ts +27 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/migrations/index.js +29 -0
- package/dist/migrations/index.js.map +1 -0
- package/package.json +63 -3
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql';
|
|
2
|
+
import { AdminNotification, type AdminNotificationAudience } from '../entities/admin-notification.entity.js';
|
|
3
|
+
export interface RecordNotificationInput {
|
|
4
|
+
audience: AdminNotificationAudience;
|
|
5
|
+
targetAdminUserId?: string | null;
|
|
6
|
+
kind: string;
|
|
7
|
+
subjectType?: string | null;
|
|
8
|
+
subjectId?: string | null;
|
|
9
|
+
title: string;
|
|
10
|
+
body?: string | null;
|
|
11
|
+
linkPath?: string | null;
|
|
12
|
+
}
|
|
13
|
+
export interface ListNotificationsInput {
|
|
14
|
+
adminUserId: string;
|
|
15
|
+
unread?: boolean;
|
|
16
|
+
limit?: number;
|
|
17
|
+
/** Opaque cursor (currently the `created_at` ISO + id of the last row of the previous page). */
|
|
18
|
+
cursor?: string | null;
|
|
19
|
+
}
|
|
20
|
+
export interface ListedNotification {
|
|
21
|
+
id: string;
|
|
22
|
+
audience: AdminNotificationAudience;
|
|
23
|
+
kind: string;
|
|
24
|
+
subjectType: string | null;
|
|
25
|
+
subjectId: string | null;
|
|
26
|
+
title: string;
|
|
27
|
+
body: string | null;
|
|
28
|
+
linkPath: string | null;
|
|
29
|
+
createdAt: Date;
|
|
30
|
+
isRead: boolean;
|
|
31
|
+
}
|
|
32
|
+
export interface ListNotificationsResult {
|
|
33
|
+
items: ListedNotification[];
|
|
34
|
+
nextCursor: string | null;
|
|
35
|
+
unreadCount: number;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* AdminNotificationService — owns the read + write paths for the admin
|
|
39
|
+
* bell feed.
|
|
40
|
+
*
|
|
41
|
+
* `record()` is invoked by domain modules (e.g. organizations registration
|
|
42
|
+
* notifier) to push entries. `listForAdmin()` is the bell's feed query and
|
|
43
|
+
* carries the per-admin `isRead` resolution for `audience='all_admins'`
|
|
44
|
+
* rows. Read marking is split between updating `read_at` (for
|
|
45
|
+
* `audience='admin_user'`) and inserting into `admin_notification_reads`
|
|
46
|
+
* (for `audience='all_admins'`).
|
|
47
|
+
*
|
|
48
|
+
* Bounded growth: `pruneStale()` archives entries older than
|
|
49
|
+
* `maxUnreadPerAudience` per `audience`. Default cap = 50. The pruner is
|
|
50
|
+
* intended to run on a schedule or on each new write; the first iteration
|
|
51
|
+
* runs it inline on every `record()` call which keeps the table small
|
|
52
|
+
* without a separate job.
|
|
53
|
+
*/
|
|
54
|
+
export declare class AdminNotificationService {
|
|
55
|
+
private readonly emFactory;
|
|
56
|
+
private readonly maxUnreadPerAudience;
|
|
57
|
+
constructor(emFactory: () => EntityManager, options?: {
|
|
58
|
+
maxUnreadPerAudience?: number;
|
|
59
|
+
});
|
|
60
|
+
record(input: RecordNotificationInput): Promise<AdminNotification>;
|
|
61
|
+
listForAdmin(input: ListNotificationsInput): Promise<ListNotificationsResult>;
|
|
62
|
+
markRead(notificationId: string, adminUserId: string): Promise<void>;
|
|
63
|
+
markAllRead(adminUserId: string): Promise<number>;
|
|
64
|
+
countUnreadForAdmin(adminUserId: string): Promise<number>;
|
|
65
|
+
/**
|
|
66
|
+
* Soft-archive entries beyond `maxUnreadPerAudience` for the given audience.
|
|
67
|
+
* Keeps the most-recent N alive; older ones get `archived_at = now()`.
|
|
68
|
+
*/
|
|
69
|
+
private pruneStale;
|
|
70
|
+
private isVisibleToAdmin;
|
|
71
|
+
private serializeCursor;
|
|
72
|
+
private parseCursor;
|
|
73
|
+
}
|
|
74
|
+
export declare class NotificationNotFoundError extends Error {
|
|
75
|
+
readonly notificationId: string;
|
|
76
|
+
constructor(notificationId: string);
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=admin-notification-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"admin-notification-service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/admin-notification-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAAE,iBAAiB,EAAE,KAAK,yBAAyB,EAAE,MAAM,0CAA0C,CAAC;AAG7G,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,yBAAyB,CAAC;IACpC,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,MAAM,WAAW,sBAAsB;IACrC,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gGAAgG;IAChG,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,yBAAyB,CAAC;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,SAAS,EAAE,IAAI,CAAC;IAChB,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,uBAAuB;IACtC,KAAK,EAAE,kBAAkB,EAAE,CAAC;IAC5B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,wBAAwB;IAIjC,OAAO,CAAC,QAAQ,CAAC,SAAS;IAH5B,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAS;gBAG3B,SAAS,EAAE,MAAM,aAAa,EAC/C,OAAO,CAAC,EAAE;QAAE,oBAAoB,CAAC,EAAE,MAAM,CAAA;KAAE;IAKvC,MAAM,CAAC,KAAK,EAAE,uBAAuB,GAAG,OAAO,CAAC,iBAAiB,CAAC;IA6BlE,YAAY,CAAC,KAAK,EAAE,sBAAsB,GAAG,OAAO,CAAC,uBAAuB,CAAC;IAgG7E,QAAQ,CAAC,cAAc,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA8BpE,WAAW,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IA0CjD,mBAAmB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAwB/D;;;OAGG;YACW,UAAU;IA8CxB,OAAO,CAAC,gBAAgB;IAKxB,OAAO,CAAC,eAAe;IAIvB,OAAO,CAAC,WAAW;CAUpB;AAiBD,qBAAa,yBAA0B,SAAQ,KAAK;aACtB,cAAc,EAAE,MAAM;gBAAtB,cAAc,EAAE,MAAM;CAInD"}
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { AdminNotification } from '../entities/admin-notification.entity.js';
|
|
2
|
+
import { AdminNotificationRead } from '../entities/admin-notification-read.entity.js';
|
|
3
|
+
/**
|
|
4
|
+
* AdminNotificationService — owns the read + write paths for the admin
|
|
5
|
+
* bell feed.
|
|
6
|
+
*
|
|
7
|
+
* `record()` is invoked by domain modules (e.g. organizations registration
|
|
8
|
+
* notifier) to push entries. `listForAdmin()` is the bell's feed query and
|
|
9
|
+
* carries the per-admin `isRead` resolution for `audience='all_admins'`
|
|
10
|
+
* rows. Read marking is split between updating `read_at` (for
|
|
11
|
+
* `audience='admin_user'`) and inserting into `admin_notification_reads`
|
|
12
|
+
* (for `audience='all_admins'`).
|
|
13
|
+
*
|
|
14
|
+
* Bounded growth: `pruneStale()` archives entries older than
|
|
15
|
+
* `maxUnreadPerAudience` per `audience`. Default cap = 50. The pruner is
|
|
16
|
+
* intended to run on a schedule or on each new write; the first iteration
|
|
17
|
+
* runs it inline on every `record()` call which keeps the table small
|
|
18
|
+
* without a separate job.
|
|
19
|
+
*/
|
|
20
|
+
export class AdminNotificationService {
|
|
21
|
+
emFactory;
|
|
22
|
+
maxUnreadPerAudience;
|
|
23
|
+
constructor(emFactory, options) {
|
|
24
|
+
this.emFactory = emFactory;
|
|
25
|
+
this.maxUnreadPerAudience = options?.maxUnreadPerAudience ?? 50;
|
|
26
|
+
}
|
|
27
|
+
async record(input) {
|
|
28
|
+
// command-coverage-ignore: admin notification bookkeeping — system-generated
|
|
29
|
+
// alert record / read-state, not an audited domain-state mutation.
|
|
30
|
+
if (input.audience === 'admin_user' && !input.targetAdminUserId) {
|
|
31
|
+
throw new Error('AdminNotificationService.record: targetAdminUserId is required when audience="admin_user".');
|
|
32
|
+
}
|
|
33
|
+
if (input.audience === 'all_admins' && input.targetAdminUserId) {
|
|
34
|
+
throw new Error('AdminNotificationService.record: targetAdminUserId must be null when audience="all_admins".');
|
|
35
|
+
}
|
|
36
|
+
const em = this.emFactory();
|
|
37
|
+
const entry = em.create(AdminNotification, {
|
|
38
|
+
audience: input.audience,
|
|
39
|
+
targetAdminUserId: input.targetAdminUserId ?? null,
|
|
40
|
+
kind: input.kind,
|
|
41
|
+
subjectType: input.subjectType ?? null,
|
|
42
|
+
subjectId: input.subjectId ?? null,
|
|
43
|
+
title: input.title,
|
|
44
|
+
body: input.body ?? null,
|
|
45
|
+
linkPath: input.linkPath ?? null,
|
|
46
|
+
});
|
|
47
|
+
await em.persistAndFlush(entry);
|
|
48
|
+
await this.pruneStale(input.audience, input.targetAdminUserId ?? null);
|
|
49
|
+
return entry;
|
|
50
|
+
}
|
|
51
|
+
async listForAdmin(input) {
|
|
52
|
+
const limit = Math.min(Math.max(input.limit ?? 25, 1), 100);
|
|
53
|
+
const em = this.emFactory();
|
|
54
|
+
const cursor = input.cursor ? this.parseCursor(input.cursor) : null;
|
|
55
|
+
const whereParts = [];
|
|
56
|
+
// knex `raw` uses positional `?` placeholders (converted to $1..$n for pg
|
|
57
|
+
// in textual order). The SELECT's isRead `?` is textually first, so its
|
|
58
|
+
// binding is prepended when the final params array is assembled below.
|
|
59
|
+
const whereParams = [];
|
|
60
|
+
// Visible to this admin: their personal rows OR broadcasts.
|
|
61
|
+
whereParts.push(`(n."audience" = 'all_admins' or (n."audience" = 'admin_user' and n."target_admin_user_id" = ?))`);
|
|
62
|
+
whereParams.push(input.adminUserId);
|
|
63
|
+
whereParts.push(`n."archived_at" is null`);
|
|
64
|
+
if (input.unread === true) {
|
|
65
|
+
whereParts.push(`(
|
|
66
|
+
(n."audience" = 'admin_user' and n."read_at" is null)
|
|
67
|
+
or (n."audience" = 'all_admins'
|
|
68
|
+
and not exists (
|
|
69
|
+
select 1 from "admin_notification_reads" r
|
|
70
|
+
where r."notification_id" = n."id" and r."admin_user_id" = ?
|
|
71
|
+
))
|
|
72
|
+
)`);
|
|
73
|
+
whereParams.push(input.adminUserId);
|
|
74
|
+
}
|
|
75
|
+
if (cursor) {
|
|
76
|
+
whereParts.push(`(n."created_at", n."id") < (?, ?)`);
|
|
77
|
+
whereParams.push(cursor.createdAt.toISOString());
|
|
78
|
+
whereParams.push(cursor.id);
|
|
79
|
+
}
|
|
80
|
+
const sql = `
|
|
81
|
+
select
|
|
82
|
+
n."id",
|
|
83
|
+
n."audience",
|
|
84
|
+
n."target_admin_user_id" as "targetAdminUserId",
|
|
85
|
+
n."kind",
|
|
86
|
+
n."subject_type" as "subjectType",
|
|
87
|
+
n."subject_id" as "subjectId",
|
|
88
|
+
n."title",
|
|
89
|
+
n."body",
|
|
90
|
+
n."link_path" as "linkPath",
|
|
91
|
+
n."created_at" as "createdAt",
|
|
92
|
+
n."read_at" as "readAt",
|
|
93
|
+
case
|
|
94
|
+
when n."audience" = 'admin_user' then n."read_at" is not null
|
|
95
|
+
else exists (
|
|
96
|
+
select 1 from "admin_notification_reads" r
|
|
97
|
+
where r."notification_id" = n."id" and r."admin_user_id" = ?
|
|
98
|
+
)
|
|
99
|
+
end as "isRead"
|
|
100
|
+
from "admin_notifications" n
|
|
101
|
+
where ${whereParts.join(' and ')}
|
|
102
|
+
order by n."created_at" desc, n."id" desc
|
|
103
|
+
limit ${limit + 1};
|
|
104
|
+
`;
|
|
105
|
+
// The isRead CASE placeholder appears in the SELECT (textually before the
|
|
106
|
+
// WHERE), so its binding (adminUserId) comes first.
|
|
107
|
+
const params = [input.adminUserId, ...whereParams];
|
|
108
|
+
const knex = em.getConnection().getKnex();
|
|
109
|
+
const rows = (await knex.raw(sql, params)).rows;
|
|
110
|
+
const hasMore = rows.length > limit;
|
|
111
|
+
const visibleRows = hasMore ? rows.slice(0, limit) : rows;
|
|
112
|
+
const items = visibleRows.map((row) => ({
|
|
113
|
+
id: row.id,
|
|
114
|
+
audience: row.audience,
|
|
115
|
+
kind: row.kind,
|
|
116
|
+
subjectType: row.subjectType ?? null,
|
|
117
|
+
subjectId: row.subjectId ?? null,
|
|
118
|
+
title: row.title,
|
|
119
|
+
body: row.body ?? null,
|
|
120
|
+
linkPath: row.linkPath ?? null,
|
|
121
|
+
createdAt: new Date(row.createdAt),
|
|
122
|
+
isRead: row.isRead,
|
|
123
|
+
}));
|
|
124
|
+
const nextCursor = hasMore
|
|
125
|
+
? this.serializeCursor({ createdAt: new Date(visibleRows[visibleRows.length - 1].createdAt), id: visibleRows[visibleRows.length - 1].id })
|
|
126
|
+
: null;
|
|
127
|
+
const unreadCount = await this.countUnreadForAdmin(input.adminUserId);
|
|
128
|
+
return { items, nextCursor, unreadCount };
|
|
129
|
+
}
|
|
130
|
+
async markRead(notificationId, adminUserId) {
|
|
131
|
+
// command-coverage-ignore: admin notification bookkeeping — system-generated
|
|
132
|
+
// alert record / read-state, not an audited domain-state mutation.
|
|
133
|
+
const em = this.emFactory();
|
|
134
|
+
const entry = await em.findOne(AdminNotification, { id: notificationId });
|
|
135
|
+
if (!entry) {
|
|
136
|
+
throw new NotificationNotFoundError(notificationId);
|
|
137
|
+
}
|
|
138
|
+
if (!this.isVisibleToAdmin(entry, adminUserId)) {
|
|
139
|
+
throw new NotificationNotFoundError(notificationId);
|
|
140
|
+
}
|
|
141
|
+
if (entry.audience === 'admin_user') {
|
|
142
|
+
if (entry.readAt)
|
|
143
|
+
return;
|
|
144
|
+
entry.readAt = new Date();
|
|
145
|
+
await em.persistAndFlush(entry);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
// Broadcast — record the per-admin read cursor (idempotent).
|
|
149
|
+
const existing = await em.findOne(AdminNotificationRead, {
|
|
150
|
+
notificationId,
|
|
151
|
+
adminUserId,
|
|
152
|
+
});
|
|
153
|
+
if (existing)
|
|
154
|
+
return;
|
|
155
|
+
const cursor = em.create(AdminNotificationRead, {
|
|
156
|
+
notificationId,
|
|
157
|
+
adminUserId,
|
|
158
|
+
});
|
|
159
|
+
await em.persistAndFlush(cursor);
|
|
160
|
+
}
|
|
161
|
+
async markAllRead(adminUserId) {
|
|
162
|
+
const em = this.emFactory();
|
|
163
|
+
const knex = em.getConnection().getKnex();
|
|
164
|
+
let count = 0;
|
|
165
|
+
// Personal rows.
|
|
166
|
+
const personalUpdate = (await knex.raw(`
|
|
167
|
+
update "admin_notifications"
|
|
168
|
+
set "read_at" = now()
|
|
169
|
+
where "audience" = 'admin_user'
|
|
170
|
+
and "target_admin_user_id" = ?
|
|
171
|
+
and "read_at" is null
|
|
172
|
+
and "archived_at" is null
|
|
173
|
+
returning "id";
|
|
174
|
+
`, [adminUserId]));
|
|
175
|
+
count += personalUpdate.rows.length;
|
|
176
|
+
// Broadcasts — insert per-admin read cursors for every visible unread broadcast.
|
|
177
|
+
const broadcastInsert = (await knex.raw(`
|
|
178
|
+
insert into "admin_notification_reads" ("notification_id", "admin_user_id", "read_at")
|
|
179
|
+
select n."id", ?, now()
|
|
180
|
+
from "admin_notifications" n
|
|
181
|
+
where n."audience" = 'all_admins'
|
|
182
|
+
and n."archived_at" is null
|
|
183
|
+
and not exists (
|
|
184
|
+
select 1 from "admin_notification_reads" r
|
|
185
|
+
where r."notification_id" = n."id" and r."admin_user_id" = ?
|
|
186
|
+
)
|
|
187
|
+
on conflict do nothing
|
|
188
|
+
returning "notification_id";
|
|
189
|
+
`, [adminUserId, adminUserId]));
|
|
190
|
+
count += broadcastInsert.rows.length;
|
|
191
|
+
return count;
|
|
192
|
+
}
|
|
193
|
+
async countUnreadForAdmin(adminUserId) {
|
|
194
|
+
const em = this.emFactory();
|
|
195
|
+
const knex = em.getConnection().getKnex();
|
|
196
|
+
const sql = `
|
|
197
|
+
select count(*)::int as "count"
|
|
198
|
+
from "admin_notifications" n
|
|
199
|
+
where n."archived_at" is null
|
|
200
|
+
and (
|
|
201
|
+
(n."audience" = 'admin_user'
|
|
202
|
+
and n."target_admin_user_id" = ?
|
|
203
|
+
and n."read_at" is null)
|
|
204
|
+
or (n."audience" = 'all_admins'
|
|
205
|
+
and not exists (
|
|
206
|
+
select 1 from "admin_notification_reads" r
|
|
207
|
+
where r."notification_id" = n."id" and r."admin_user_id" = ?
|
|
208
|
+
))
|
|
209
|
+
);
|
|
210
|
+
`;
|
|
211
|
+
const result = (await knex.raw(sql, [adminUserId, adminUserId]));
|
|
212
|
+
return result.rows[0]?.count ?? 0;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Soft-archive entries beyond `maxUnreadPerAudience` for the given audience.
|
|
216
|
+
* Keeps the most-recent N alive; older ones get `archived_at = now()`.
|
|
217
|
+
*/
|
|
218
|
+
async pruneStale(audience, targetAdminUserId) {
|
|
219
|
+
const em = this.emFactory();
|
|
220
|
+
const knex = em.getConnection().getKnex();
|
|
221
|
+
if (audience === 'all_admins') {
|
|
222
|
+
await knex.raw(`
|
|
223
|
+
with ranked as (
|
|
224
|
+
select "id",
|
|
225
|
+
row_number() over (order by "created_at" desc, "id" desc) as "rn"
|
|
226
|
+
from "admin_notifications"
|
|
227
|
+
where "audience" = 'all_admins'
|
|
228
|
+
and "archived_at" is null
|
|
229
|
+
)
|
|
230
|
+
update "admin_notifications" n
|
|
231
|
+
set "archived_at" = now()
|
|
232
|
+
from ranked
|
|
233
|
+
where n."id" = ranked."id"
|
|
234
|
+
and ranked."rn" > ?;
|
|
235
|
+
`, [this.maxUnreadPerAudience]);
|
|
236
|
+
}
|
|
237
|
+
else if (targetAdminUserId) {
|
|
238
|
+
await knex.raw(`
|
|
239
|
+
with ranked as (
|
|
240
|
+
select "id",
|
|
241
|
+
row_number() over (order by "created_at" desc, "id" desc) as "rn"
|
|
242
|
+
from "admin_notifications"
|
|
243
|
+
where "audience" = 'admin_user'
|
|
244
|
+
and "target_admin_user_id" = ?
|
|
245
|
+
and "archived_at" is null
|
|
246
|
+
)
|
|
247
|
+
update "admin_notifications" n
|
|
248
|
+
set "archived_at" = now()
|
|
249
|
+
from ranked
|
|
250
|
+
where n."id" = ranked."id"
|
|
251
|
+
and ranked."rn" > ?;
|
|
252
|
+
`, [targetAdminUserId, this.maxUnreadPerAudience]);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
isVisibleToAdmin(entry, adminUserId) {
|
|
256
|
+
if (entry.audience === 'all_admins')
|
|
257
|
+
return true;
|
|
258
|
+
return entry.targetAdminUserId === adminUserId;
|
|
259
|
+
}
|
|
260
|
+
serializeCursor(input) {
|
|
261
|
+
return Buffer.from(`${input.createdAt.toISOString()}|${input.id}`).toString('base64url');
|
|
262
|
+
}
|
|
263
|
+
parseCursor(raw) {
|
|
264
|
+
try {
|
|
265
|
+
const decoded = Buffer.from(raw, 'base64url').toString('utf-8');
|
|
266
|
+
const [iso, id] = decoded.split('|');
|
|
267
|
+
if (!iso || !id)
|
|
268
|
+
return null;
|
|
269
|
+
return { createdAt: new Date(iso), id };
|
|
270
|
+
}
|
|
271
|
+
catch {
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
export class NotificationNotFoundError extends Error {
|
|
277
|
+
notificationId;
|
|
278
|
+
constructor(notificationId) {
|
|
279
|
+
super(`Admin notification ${notificationId} not found or not visible.`);
|
|
280
|
+
this.notificationId = notificationId;
|
|
281
|
+
this.name = 'NotificationNotFoundError';
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
//# sourceMappingURL=admin-notification-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"admin-notification-service.js","sourceRoot":"","sources":["../../../src/backend/services/admin-notification-service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Admin notifications — minimal "bell" surface.
|
|
3
|
+
*
|
|
4
|
+
* Introduced by feature 026 (Organizations) to deliver in-app
|
|
5
|
+
* notifications to admins when a new Organization registers. The
|
|
6
|
+
* module is intentionally generic so future features (low-stock
|
|
7
|
+
* alerts, RFQ awaiting reply, etc.) can write into the same feed.
|
|
8
|
+
*
|
|
9
|
+
* The schema is owned by migration 048_admin_notifications_init.ts.
|
|
10
|
+
*/
|
|
11
|
+
export declare const manifest: {
|
|
12
|
+
id: string;
|
|
13
|
+
name: string;
|
|
14
|
+
version: string;
|
|
15
|
+
dependencies: string[];
|
|
16
|
+
description?: string | undefined;
|
|
17
|
+
acknowledgedDependencies?: {
|
|
18
|
+
moduleId: string;
|
|
19
|
+
port: string;
|
|
20
|
+
reason: string;
|
|
21
|
+
}[] | undefined;
|
|
22
|
+
nonBindingDependencies?: {
|
|
23
|
+
moduleId: string;
|
|
24
|
+
name: string;
|
|
25
|
+
kind: "contributes-to" | "degrades-without" | "refuses-without";
|
|
26
|
+
reason: string;
|
|
27
|
+
whenAbsent?: string | undefined;
|
|
28
|
+
}[] | undefined;
|
|
29
|
+
activation?: {
|
|
30
|
+
settingCode: string;
|
|
31
|
+
default: boolean;
|
|
32
|
+
} | {
|
|
33
|
+
nonDeactivatable: true;
|
|
34
|
+
reason: string;
|
|
35
|
+
} | undefined;
|
|
36
|
+
settings?: {
|
|
37
|
+
moduleCode: string;
|
|
38
|
+
groups: {
|
|
39
|
+
code: string;
|
|
40
|
+
name: string;
|
|
41
|
+
salesChannelCodes?: string[] | undefined;
|
|
42
|
+
isSystemProtected?: boolean | undefined;
|
|
43
|
+
}[];
|
|
44
|
+
settings: {
|
|
45
|
+
code: string;
|
|
46
|
+
name: string;
|
|
47
|
+
valueType: "string" | "number" | "boolean" | "json" | "string_list" | "secret" | "credential_ref";
|
|
48
|
+
defaultValue: unknown;
|
|
49
|
+
description?: string | undefined;
|
|
50
|
+
groupCode?: string | undefined;
|
|
51
|
+
previousDefaultValues?: unknown[] | undefined;
|
|
52
|
+
salesChannelCodes?: string[] | undefined;
|
|
53
|
+
enumOptions?: string[] | undefined;
|
|
54
|
+
configurationType?: string | undefined;
|
|
55
|
+
hidden?: boolean | undefined;
|
|
56
|
+
}[];
|
|
57
|
+
} | undefined;
|
|
58
|
+
i18n?: {
|
|
59
|
+
bundlesDir: string;
|
|
60
|
+
} | undefined;
|
|
61
|
+
docs?: false | {
|
|
62
|
+
dir: string;
|
|
63
|
+
} | undefined;
|
|
64
|
+
demo?: false | {
|
|
65
|
+
summary: string;
|
|
66
|
+
seed: (context: import("@endora-commerce/contracts").ModuleDemoContext<never>) => Promise<import("@endora-commerce/contracts").DemoSeedResult>;
|
|
67
|
+
reset: (context: import("@endora-commerce/contracts").ModuleDemoContext<never>) => Promise<import("@endora-commerce/contracts").DemoResetResult>;
|
|
68
|
+
after?: readonly string[] | undefined;
|
|
69
|
+
package?: string | undefined;
|
|
70
|
+
} | undefined;
|
|
71
|
+
actions?: {
|
|
72
|
+
id: string;
|
|
73
|
+
labelKey: string;
|
|
74
|
+
icon: "Plus" | "Sparkles" | "Settings" | "Search" | "Boxes" | "Layers" | "Menu" | "PlusCircle" | "PlusSquare" | "FilePlus" | "FolderPlus" | "Upload" | "FileUp" | "CloudUpload" | "Download" | "FileDown" | "FileText" | "BookOpen" | "Rss" | "Package" | "Tag" | "ShoppingCart" | "Receipt" | "CreditCard" | "Users" | "UserPlus" | "Inbox" | "ListChecks" | "ClipboardList" | "Image" | "Video" | "LayoutDashboard" | "PanelLeft" | "KeyRound" | "ShieldCheck" | "Edit" | "Archive" | "Box" | "Truck" | "CircleDollarSign" | "Activity" | "LineChart" | "Smartphone" | "Webhook" | "Scale" | "PlugZap" | "PercentDiamond" | "Newspaper" | "Languages" | "Eraser" | "Warehouse" | "TrendingDown" | "Bell" | "PackageOpen" | "Building2" | "Store" | "ClipboardCheck";
|
|
75
|
+
targetRoute: string;
|
|
76
|
+
keywords: string[];
|
|
77
|
+
weight: number;
|
|
78
|
+
descriptionKey?: string | undefined;
|
|
79
|
+
requiredPermission?: string | undefined;
|
|
80
|
+
}[] | undefined;
|
|
81
|
+
permissions?: {
|
|
82
|
+
code: string;
|
|
83
|
+
label: string;
|
|
84
|
+
module?: string | undefined;
|
|
85
|
+
description?: string | undefined;
|
|
86
|
+
requires?: string[] | undefined;
|
|
87
|
+
}[] | undefined;
|
|
88
|
+
transactionalEmails?: {
|
|
89
|
+
code: string;
|
|
90
|
+
name: string;
|
|
91
|
+
variables: {
|
|
92
|
+
key: string;
|
|
93
|
+
label: string;
|
|
94
|
+
sampleValue?: string | undefined;
|
|
95
|
+
description?: string | undefined;
|
|
96
|
+
}[];
|
|
97
|
+
description?: string | undefined;
|
|
98
|
+
group?: string | undefined;
|
|
99
|
+
}[] | undefined;
|
|
100
|
+
capabilities?: string[] | undefined;
|
|
101
|
+
exclusiveCapabilities?: {
|
|
102
|
+
key: string;
|
|
103
|
+
errorCode: string;
|
|
104
|
+
}[] | undefined;
|
|
105
|
+
errorCodes?: {
|
|
106
|
+
code: string;
|
|
107
|
+
tokens?: string[] | undefined;
|
|
108
|
+
}[] | undefined;
|
|
109
|
+
blocks?: {
|
|
110
|
+
name: string;
|
|
111
|
+
labelKey: string;
|
|
112
|
+
category: string;
|
|
113
|
+
contexts: ("invoice" | "email" | "cms" | "newsletter")[];
|
|
114
|
+
fields: Record<string, {
|
|
115
|
+
type: "number" | "object" | "array" | "text" | "textarea" | "select" | "radio" | "external" | "uuid" | "richtext";
|
|
116
|
+
label?: string | undefined;
|
|
117
|
+
required?: boolean | undefined;
|
|
118
|
+
options?: {
|
|
119
|
+
label: string;
|
|
120
|
+
value: string | number;
|
|
121
|
+
}[] | undefined;
|
|
122
|
+
refKind?: string | undefined;
|
|
123
|
+
}>;
|
|
124
|
+
descriptionKey?: string | undefined;
|
|
125
|
+
defaultProps?: Record<string, unknown> | undefined;
|
|
126
|
+
responsiveFields?: string[] | undefined;
|
|
127
|
+
previewIcon?: string | undefined;
|
|
128
|
+
weight?: number | undefined;
|
|
129
|
+
}[] | undefined;
|
|
130
|
+
blockCategories?: {
|
|
131
|
+
key: string;
|
|
132
|
+
titleKey: string;
|
|
133
|
+
contexts: ("invoice" | "email" | "cms" | "newsletter")[];
|
|
134
|
+
weight?: number | undefined;
|
|
135
|
+
visible?: boolean | undefined;
|
|
136
|
+
}[] | undefined;
|
|
137
|
+
env?: {
|
|
138
|
+
name: string;
|
|
139
|
+
describes: {
|
|
140
|
+
en: string;
|
|
141
|
+
pl: string;
|
|
142
|
+
};
|
|
143
|
+
requirement: {
|
|
144
|
+
kind: "required";
|
|
145
|
+
} | {
|
|
146
|
+
kind: "requiredWhen";
|
|
147
|
+
input: string;
|
|
148
|
+
equals: string;
|
|
149
|
+
} | {
|
|
150
|
+
kind: "optional";
|
|
151
|
+
without: {
|
|
152
|
+
en: string;
|
|
153
|
+
pl: string;
|
|
154
|
+
};
|
|
155
|
+
};
|
|
156
|
+
secret: boolean;
|
|
157
|
+
generable: boolean;
|
|
158
|
+
owner: {
|
|
159
|
+
kind: "platform";
|
|
160
|
+
} | {
|
|
161
|
+
kind: "application";
|
|
162
|
+
application: "admin" | "backend" | "storefront";
|
|
163
|
+
} | {
|
|
164
|
+
kind: "module";
|
|
165
|
+
moduleId: string;
|
|
166
|
+
};
|
|
167
|
+
consumers: ("admin" | "backend" | "storefront")[];
|
|
168
|
+
addressOf: "admin" | "backend" | "storefront" | null;
|
|
169
|
+
}[] | undefined;
|
|
170
|
+
};
|
|
171
|
+
//# sourceMappingURL=manifest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAEA;;;;;;;;;GASG;AACH,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmCnB,CAAC"}
|
package/dist/manifest.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { defineModuleManifest } from '@endora-commerce/contracts';
|
|
2
|
+
/**
|
|
3
|
+
* Admin notifications — minimal "bell" surface.
|
|
4
|
+
*
|
|
5
|
+
* Introduced by feature 026 (Organizations) to deliver in-app
|
|
6
|
+
* notifications to admins when a new Organization registers. The
|
|
7
|
+
* module is intentionally generic so future features (low-stock
|
|
8
|
+
* alerts, RFQ awaiting reply, etc.) can write into the same feed.
|
|
9
|
+
*
|
|
10
|
+
* The schema is owned by migration 048_admin_notifications_init.ts.
|
|
11
|
+
*/
|
|
12
|
+
export const manifest = defineModuleManifest({
|
|
13
|
+
id: 'admin_notifications',
|
|
14
|
+
name: 'Admin Notifications',
|
|
15
|
+
description: 'In-app notification feed surfaced in the admin top-nav bell. Generic so any module can write entries.',
|
|
16
|
+
version: '1.0.0',
|
|
17
|
+
// `auth` added with the conversion: the admin routes are gated by
|
|
18
|
+
// `requireAdmin`, which `auth` owns.
|
|
19
|
+
dependencies: ['admin_users', 'auth'],
|
|
20
|
+
settings: {
|
|
21
|
+
moduleCode: 'admin_notifications',
|
|
22
|
+
groups: [{ code: 'admin_notifications', name: 'Admin notifications' }],
|
|
23
|
+
settings: [
|
|
24
|
+
{
|
|
25
|
+
// Feature 073 — the operator's activation control. Platform-wide.
|
|
26
|
+
code: 'admin_notifications.enabled',
|
|
27
|
+
name: 'Admin notifications enabled',
|
|
28
|
+
description: 'Switches the admin notification centre on or off. While off nothing is recorded and the notification screens and their API are gone. Nothing is deleted — entries already recorded are preserved and reappear when you switch it back on.',
|
|
29
|
+
groupCode: 'admin_notifications',
|
|
30
|
+
valueType: 'boolean',
|
|
31
|
+
defaultValue: true,
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
activation: { settingCode: 'admin_notifications.enabled', default: true },
|
|
36
|
+
// Feature 026 checklist — `routes.admin.ts` has gated the bell on this code
|
|
37
|
+
// since 026 while no manifest declared it, so only a role holding `'*'` could
|
|
38
|
+
// open the notification feed. The code keeps its historical spelling for the
|
|
39
|
+
// same reason `audit_log:read` and `integrations:manage` keep theirs: it is
|
|
40
|
+
// the string the route checks, and `module` is what puts the row under the
|
|
41
|
+
// right heading on the role matrix.
|
|
42
|
+
permissions: [
|
|
43
|
+
{ code: 'admin:read', module: 'admin_notifications', label: 'View admin notifications' },
|
|
44
|
+
],
|
|
45
|
+
});
|
|
46
|
+
//# sourceMappingURL=manifest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.js","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AAElE;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,oBAAoB,CAAC;IAC3C,EAAE,EAAE,qBAAqB;IACzB,IAAI,EAAE,qBAAqB;IAC3B,WAAW,EACT,uGAAuG;IACzG,OAAO,EAAE,OAAO;IAChB,kEAAkE;IAClE,qCAAqC;IACrC,YAAY,EAAE,CAAC,aAAa,EAAE,MAAM,CAAC;IACrC,QAAQ,EAAE;QACR,UAAU,EAAE,qBAAqB;QACjC,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,qBAAqB,EAAE,IAAI,EAAE,qBAAqB,EAAE,CAAC;QACtE,QAAQ,EAAE;YACR;gBACE,kEAAkE;gBAClE,IAAI,EAAE,6BAA6B;gBACnC,IAAI,EAAE,6BAA6B;gBACnC,WAAW,EACT,2OAA2O;gBAC7O,SAAS,EAAE,qBAAqB;gBAChC,SAAS,EAAE,SAAS;gBACpB,YAAY,EAAE,IAAI;aACnB;SACF;KACF;IACD,UAAU,EAAE,EAAE,WAAW,EAAE,6BAA6B,EAAE,OAAO,EAAE,IAAI,EAAE;IACzE,4EAA4E;IAC5E,8EAA8E;IAC9E,6EAA6E;IAC7E,4EAA4E;IAC5E,2EAA2E;IAC3E,oCAAoC;IACpC,WAAW,EAAE;QACX,EAAE,IAAI,EAAE,YAAY,EAAE,MAAM,EAAE,qBAAqB,EAAE,KAAK,EAAE,0BAA0B,EAAE;KACzF;CACF,CAAC,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Migration } from '@mikro-orm/migrations';
|
|
2
|
+
/**
|
|
3
|
+
* Admin notifications — minimal "bell" surface introduced by feature 026.
|
|
4
|
+
*
|
|
5
|
+
* One row per notification. `audience='all_admins'` rows are visible to
|
|
6
|
+
* every admin (the read state is tracked per-admin in a future iteration
|
|
7
|
+
* via `admin_notification_reads`; for the first iteration we keep `read_at`
|
|
8
|
+
* on the row itself for `audience='admin_user'` rows only, and rely on the
|
|
9
|
+
* `mark-all-read` endpoint stamping a per-admin "last seen" cursor —
|
|
10
|
+
* implemented in code, not in this schema).
|
|
11
|
+
*
|
|
12
|
+
* Unread cap is enforced by application code (`pruneStale()`), not by the
|
|
13
|
+
* schema.
|
|
14
|
+
*/
|
|
15
|
+
export declare class Migration20260611T140350AdminNotificationsInit extends Migration {
|
|
16
|
+
up(): Promise<void>;
|
|
17
|
+
down(): Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=20260611T140350_admin_notifications_init.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"20260611T140350_admin_notifications_init.d.ts","sourceRoot":"","sources":["../../src/migrations/20260611T140350_admin_notifications_init.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAElD;;;;;;;;;;;;GAYG;AACH,qBAAa,8CAA+C,SAAQ,SAAS;IAC5D,EAAE,IAAI,OAAO,CAAC,IAAI,CAAC;IAgEnB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAMrC"}
|