@velocelab/message 0.1.2 → 0.1.3
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/index.d.ts +2 -1
- package/dist/index.js +5 -3
- package/package.json +3 -2
- package/src/index.ts +4 -3
- package/tests/service.test.mjs +43 -0
package/dist/index.d.ts
CHANGED
|
@@ -66,7 +66,8 @@ declare module "yumeri" {
|
|
|
66
66
|
export declare class MessageService extends Service {
|
|
67
67
|
private readonly typesById;
|
|
68
68
|
private readonly db;
|
|
69
|
-
private readonly
|
|
69
|
+
private readonly context;
|
|
70
|
+
private readonly core;
|
|
70
71
|
constructor(ctx: Context);
|
|
71
72
|
registerType(owner: Context, type: MessageType): void;
|
|
72
73
|
types(): MessageType[];
|
package/dist/index.js
CHANGED
|
@@ -12,8 +12,9 @@ const record = (row) => ({ id: Number(row.id), userId: row.user_id, type: row.ty
|
|
|
12
12
|
export class MessageService extends Service {
|
|
13
13
|
typesById;
|
|
14
14
|
db;
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
context;
|
|
16
|
+
core;
|
|
17
|
+
constructor(ctx) { super(ctx); this.context = ctx; const core = ctx.getCore(); this.core = core; this.typesById = registries.get(core) ?? new Map(); registries.set(core, this.typesById); this.db = ctx.component.database; }
|
|
17
18
|
registerType(owner, type) { if (this.typesById.has(type.id))
|
|
18
19
|
throw new Error(`Message type already registered: ${type.id}`); this.typesById.set(type.id, type); owner.affect(() => { if (this.typesById.get(type.id) === type)
|
|
19
20
|
this.typesById.delete(type.id); }); }
|
|
@@ -31,7 +32,8 @@ export class MessageService extends Service {
|
|
|
31
32
|
result = record({ ...existing, ...data, type: type.id });
|
|
32
33
|
}
|
|
33
34
|
else
|
|
34
|
-
result = record(await this.db.create("messages", { ...key, ...data, created_at: now }));
|
|
35
|
+
result = record(await this.db.create("messages", { ...key, ...data, created_at: now })); const notification = this.context.component.notification ?? this.core.getService("notification", this.context); if (notification)
|
|
36
|
+
await notification.send({ userId: input.userId, title: input.title, body: input.subtitle, tag: `message-${type.id}-${input.dedupeKey}`, url: input.action?.href }); return result; }
|
|
35
37
|
async remove(userId, id) { await this.db.remove("messages", { id, user_id: userId }); }
|
|
36
38
|
async markRead(userId, id) { await this.db.update("messages", { id, user_id: userId }, { read: true, updated_at: new Date().toISOString() }); }
|
|
37
39
|
async list(userId) { return (await this.db.select("messages", { user_id: userId })).sort((a, b) => String(b.updated_at).localeCompare(String(a.updated_at))).map(record); }
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@velocelab/message",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"dev": "src/index.ts",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
8
8
|
"scripts": {
|
|
9
|
-
"build": "../../node_modules/.bin/tsc -p tsconfig.json && ../../node_modules/.bin/vite build"
|
|
9
|
+
"build": "../../node_modules/.bin/tsc -p tsconfig.json && ../../node_modules/.bin/vite build",
|
|
10
|
+
"test": "node --test tests/*.test.mjs"
|
|
10
11
|
},
|
|
11
12
|
"dependencies": {
|
|
12
13
|
"@velocelab/dashboard": "^0.1.5",
|
package/src/index.ts
CHANGED
|
@@ -23,13 +23,14 @@ const record = (row: StoredMessage): MessageRecord => ({ id: Number(row.id), use
|
|
|
23
23
|
export class MessageService extends Service {
|
|
24
24
|
private readonly typesById: Map<string, MessageType>;
|
|
25
25
|
private readonly db: Database;
|
|
26
|
-
private readonly
|
|
27
|
-
|
|
26
|
+
private readonly context: Context;
|
|
27
|
+
private readonly core: Core;
|
|
28
|
+
constructor(ctx: Context) { super(ctx); this.context = ctx; const core = ctx.getCore(); this.core = core; this.typesById = registries.get(core) ?? new Map(); registries.set(core, this.typesById); this.db = ctx.component.database as Database; }
|
|
28
29
|
registerType(owner: Context, type: MessageType) { if (this.typesById.has(type.id)) throw new Error(`Message type already registered: ${type.id}`); this.typesById.set(type.id, type); owner.affect(() => { if (this.typesById.get(type.id) === type) this.typesById.delete(type.id); }); }
|
|
29
30
|
types() { return [...this.typesById.values()]; }
|
|
30
31
|
async preferences(userId: number) { return await this.db.select("message_preferences", { user_id: userId }); }
|
|
31
32
|
async setEnabled(userId: number, plugin: string, type: string, enabled: boolean) { if (!this.types().some(item => item.plugin === plugin && (!type || item.id === type))) throw Error("Unknown message type"); const key = { user_id: userId, plugin, type }; const existing = await this.db.selectOne("message_preferences", key); if (existing) await this.db.update("message_preferences", key, { enabled }); else await this.db.create("message_preferences", { ...key, enabled }); }
|
|
32
|
-
async send(input: MessageInput): Promise<MessageRecord | undefined> { const type = this.typesById.get(input.type); if (!type) throw Error(`Message type is not registered: ${input.type}`); const prefs = await this.preferences(input.userId); if (prefs.some(item => item.plugin === type.plugin && !item.enabled && (!item.type || item.type === type.id))) return; const now = new Date().toISOString(); const action = input.action ? JSON.stringify(input.action) : ""; const key = { user_id: input.userId, type: type.id, dedupe_key: `${type.id}:${input.dedupeKey}` }; const data = { icon: input.icon || "bell", title: input.title, subtitle: input.subtitle || "", action, source: type.plugin, read: false, updated_at: now }; const existing = await this.db.selectOne("messages", key); let result: MessageRecord; if (existing) { await this.db.update("messages", { id: existing.id }, data); result = record({ ...existing, ...data, type: type.id }); } else result = record(await this.db.create("messages", { ...key, ...data, created_at: now }));
|
|
33
|
+
async send(input: MessageInput): Promise<MessageRecord | undefined> { const type = this.typesById.get(input.type); if (!type) throw Error(`Message type is not registered: ${input.type}`); const prefs = await this.preferences(input.userId); if (prefs.some(item => item.plugin === type.plugin && !item.enabled && (!item.type || item.type === type.id))) return; const now = new Date().toISOString(); const action = input.action ? JSON.stringify(input.action) : ""; const key = { user_id: input.userId, type: type.id, dedupe_key: `${type.id}:${input.dedupeKey}` }; const data = { icon: input.icon || "bell", title: input.title, subtitle: input.subtitle || "", action, source: type.plugin, read: false, updated_at: now }; const existing = await this.db.selectOne("messages", key); let result: MessageRecord; if (existing) { await this.db.update("messages", { id: existing.id }, data); result = record({ ...existing, ...data, type: type.id }); } else result = record(await this.db.create("messages", { ...key, ...data, created_at: now })); const notification = (this.context.component.notification as NotificationService | undefined) ?? this.core.getService("notification", this.context) as NotificationService | undefined; if (notification) await notification.send({ userId: input.userId, title: input.title, body: input.subtitle, tag: `message-${type.id}-${input.dedupeKey}`, url: input.action?.href }); return result; }
|
|
33
34
|
async remove(userId: number, id: number) { await this.db.remove("messages", { id, user_id: userId }); }
|
|
34
35
|
async markRead(userId: number, id: number) { await this.db.update("messages", { id, user_id: userId }, { read: true, updated_at: new Date().toISOString() }); }
|
|
35
36
|
async list(userId: number) { return (await this.db.select("messages", { user_id: userId })).sort((a, b) => String(b.updated_at).localeCompare(String(a.updated_at))).map(record); }
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import { MessageService } from "../dist/index.js";
|
|
4
|
+
|
|
5
|
+
function setup({ coreNotification = true, injectedNotification = false } = {}) {
|
|
6
|
+
const persisted = [];
|
|
7
|
+
const notifications = [];
|
|
8
|
+
const db = {
|
|
9
|
+
async select() { return []; },
|
|
10
|
+
async selectOne() { return undefined; },
|
|
11
|
+
async create(_table, value) { const row = { ...value, id: persisted.length + 1 }; persisted.push(row); return row; },
|
|
12
|
+
};
|
|
13
|
+
class NotificationService {
|
|
14
|
+
async send(input) { notifications.push(input); }
|
|
15
|
+
}
|
|
16
|
+
const core = {
|
|
17
|
+
services: coreNotification ? { notification: NotificationService } : {},
|
|
18
|
+
getService(name, context) { const ServiceType = this.services[name]; return ServiceType ? new ServiceType(context) : undefined; },
|
|
19
|
+
};
|
|
20
|
+
const context = {
|
|
21
|
+
component: { database: db, ...(injectedNotification ? { notification: new NotificationService() } : {}) },
|
|
22
|
+
getCore() { return core; },
|
|
23
|
+
};
|
|
24
|
+
const service = new MessageService(context);
|
|
25
|
+
service.registerType({ affect() {} }, { id: "test:message", plugin: "test", label: "Test" });
|
|
26
|
+
return { notifications, persisted, service };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
test("resolves notification service from Core when caller context does not inject it", async () => {
|
|
30
|
+
const f = setup();
|
|
31
|
+
const result = await f.service.send({ userId: 4, type: "test:message", dedupeKey: "one", title: "Update" });
|
|
32
|
+
assert.equal(result?.title, "Update");
|
|
33
|
+
assert.equal(f.persisted.length, 1);
|
|
34
|
+
assert.equal(f.notifications.length, 1);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("persists message when notification service is not registered", async () => {
|
|
38
|
+
const f = setup({ coreNotification: false });
|
|
39
|
+
const result = await f.service.send({ userId: 4, type: "test:message", dedupeKey: "one", title: "Update" });
|
|
40
|
+
assert.equal(result?.title, "Update");
|
|
41
|
+
assert.equal(f.persisted.length, 1);
|
|
42
|
+
assert.equal(f.notifications.length, 0);
|
|
43
|
+
});
|