@yaebal/panel 0.0.2 → 0.0.5

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/src/serve.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type IncomingMessage, type Server, type ServerResponse, createServer } from "node:http";
1
+ import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
2
2
 
3
3
  /** options for the node {@link serve} helper. */
4
4
  export interface ServeOptions {
@@ -1,9 +1,23 @@
1
1
  import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
- import { SqlitePanelStore } from "./sqlite.js";
4
3
 
5
- test("SqlitePanelStore: records, lists newest-first, paginates and emits events", () => {
6
- const store = new SqlitePanelStore(); // :memory:
4
+ // `node:sqlite` (and therefore `SqlitePanelStore`) is only available on node 22.5+.
5
+ // on older runtimes a static import of "./sqlite.js" throws ERR_UNKNOWN_BUILTIN_MODULE
6
+ // at module-evaluation time, which would fail the whole file. import it lazily and
7
+ // skip the suite when the builtin is missing instead.
8
+ let SqlitePanelStore: typeof import("./sqlite.js").SqlitePanelStore | undefined;
9
+ try {
10
+ ({ SqlitePanelStore } = await import("./sqlite.js"));
11
+ } catch {
12
+ SqlitePanelStore = undefined;
13
+ }
14
+
15
+ const skip = SqlitePanelStore ? false : "node:sqlite unavailable (requires node 22.5+)";
16
+ // non-undefined alias for the test bodies (only reached when not skipped)
17
+ const Store = SqlitePanelStore as NonNullable<typeof SqlitePanelStore>;
18
+
19
+ test("SqlitePanelStore: records, lists newest-first, paginates and emits events", { skip }, () => {
20
+ const store = new Store(); // :memory:
7
21
  const events: number[] = [];
8
22
  store.subscribe((e) => events.push(e.chatId));
9
23
 
@@ -33,26 +47,50 @@ test("SqlitePanelStore: records, lists newest-first, paginates and emits events"
33
47
  store.close();
34
48
  });
35
49
 
36
- test("SqlitePanelStore persists attachments and media_group_id round-trip", () => {
37
- const store = new SqlitePanelStore();
50
+ test("SqlitePanelStore persists attachments and media_group_id round-trip", { skip }, () => {
51
+ const store = new Store();
38
52
 
39
53
  store.record(
40
- { id: 1, name: "@u" },
54
+ { id: 1, name: "@u", firstName: "Uma", lastName: "Ray", username: "u" },
41
55
  {
42
56
  direction: "in",
43
57
  text: "[photo]",
44
58
  date: 1,
45
59
  attachments: [{ type: "photo", fileId: "f1" }],
46
60
  mediaGroupId: "G1",
61
+ keyboard: {
62
+ type: "inline",
63
+ rows: [[{ text: "Open", kind: "callback", callbackData: "open" }]],
64
+ },
65
+ },
66
+ );
67
+ store.record(
68
+ { id: 1, name: "@u" },
69
+ {
70
+ direction: "in",
71
+ text: "button clicked: open",
72
+ date: 2,
73
+ event: { type: "callback", title: "button clicked", detail: "open", data: "open" },
47
74
  },
48
75
  );
49
- store.record({ id: 1, name: "@u" }, { direction: "out", text: "thanks", date: 2 });
76
+
77
+ const chat = store.chats()[0];
78
+ assert.equal(chat?.firstName, "Uma");
79
+ assert.equal(chat?.lastName, "Ray");
80
+ assert.equal(chat?.username, "u");
81
+ assert.equal(chat?.lastAttachmentType, undefined);
82
+ assert.equal(chat?.lastEventType, "callback");
50
83
 
51
84
  const hist = store.history(1);
52
85
  assert.deepEqual(hist[0]?.attachments, [{ type: "photo", fileId: "f1" }]);
53
86
  assert.equal(hist[0]?.mediaGroupId, "G1");
54
- // plain text message has no attachments/group keys
87
+ assert.deepEqual(hist[0]?.keyboard, {
88
+ type: "inline",
89
+ rows: [[{ text: "Open", kind: "callback", callbackData: "open" }]],
90
+ });
91
+ // event message has no attachments/group keys
55
92
  assert.equal(hist[1]?.attachments, undefined);
56
93
  assert.equal(hist[1]?.mediaGroupId, undefined);
94
+ assert.equal(hist[1]?.event?.type, "callback");
57
95
  store.close();
58
- });
96
+ });
package/src/sqlite.ts CHANGED
@@ -3,11 +3,22 @@ import type {
3
3
  HistoryOptions,
4
4
  PanelAttachment,
5
5
  PanelChat,
6
+ PanelChatRecord,
6
7
  PanelEvent,
8
+ PanelKeyboard,
7
9
  PanelMessage,
10
+ PanelMessageEvent,
8
11
  PanelStore,
9
12
  } from "./index.js";
10
13
 
14
+ function addColumn(db: DatabaseSync, table: string, definition: string): void {
15
+ try {
16
+ db.exec(`ALTER TABLE ${table} ADD COLUMN ${definition}`);
17
+ } catch {
18
+ // Existing sqlite stores from older panel versions simply already have the column.
19
+ }
20
+ }
21
+
11
22
  /** options for {@link SqlitePanelStore}. */
12
23
  export interface SqlitePanelStoreOptions {
13
24
  /** sqlite file path, or `":memory:"` (default). ignored when `db` is provided. */
@@ -33,10 +44,15 @@ export class SqlitePanelStore implements PanelStore {
33
44
  this.#db = options.db ?? new DatabaseSync(options.path ?? ":memory:");
34
45
  this.#db.exec(`
35
46
  CREATE TABLE IF NOT EXISTS panel_chats (
36
- id INTEGER PRIMARY KEY,
37
- name TEXT,
38
- last_text TEXT NOT NULL,
39
- last_date INTEGER NOT NULL
47
+ id INTEGER PRIMARY KEY,
48
+ name TEXT,
49
+ first_name TEXT,
50
+ last_name TEXT,
51
+ username TEXT,
52
+ last_text TEXT NOT NULL,
53
+ last_date INTEGER NOT NULL,
54
+ last_attachment TEXT,
55
+ last_event TEXT
40
56
  );
41
57
  CREATE TABLE IF NOT EXISTS panel_messages (
42
58
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -45,16 +61,26 @@ export class SqlitePanelStore implements PanelStore {
45
61
  text TEXT NOT NULL,
46
62
  date INTEGER NOT NULL,
47
63
  attachments TEXT,
48
- media_group TEXT
64
+ media_group TEXT,
65
+ keyboard TEXT,
66
+ event TEXT
49
67
  );
50
68
  CREATE INDEX IF NOT EXISTS panel_messages_chat ON panel_messages (chat_id, date);
51
69
  `);
70
+
71
+ addColumn(this.#db, "panel_chats", "first_name TEXT");
72
+ addColumn(this.#db, "panel_chats", "last_name TEXT");
73
+ addColumn(this.#db, "panel_chats", "username TEXT");
74
+ addColumn(this.#db, "panel_chats", "last_attachment TEXT");
75
+ addColumn(this.#db, "panel_chats", "last_event TEXT");
76
+ addColumn(this.#db, "panel_messages", "keyboard TEXT");
77
+ addColumn(this.#db, "panel_messages", "event TEXT");
52
78
  }
53
79
 
54
- record(chat: { id: number; name?: string }, message: PanelMessage): void {
80
+ record(chat: PanelChatRecord, message: PanelMessage): void {
55
81
  this.#db
56
82
  .prepare(
57
- "INSERT INTO panel_messages (chat_id, direction, text, date, attachments, media_group) VALUES (?, ?, ?, ?, ?, ?)",
83
+ "INSERT INTO panel_messages (chat_id, direction, text, date, attachments, media_group, keyboard, event) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
58
84
  )
59
85
  .run(
60
86
  chat.id,
@@ -63,18 +89,39 @@ export class SqlitePanelStore implements PanelStore {
63
89
  message.date,
64
90
  message.attachments ? JSON.stringify(message.attachments) : null,
65
91
  message.mediaGroupId ?? null,
92
+ message.keyboard ? JSON.stringify(message.keyboard) : null,
93
+ message.event ? JSON.stringify(message.event) : null,
66
94
  );
67
95
 
68
96
  // keep the name when an outgoing message omits it; COALESCE the existing row's value
69
97
  this.#db
70
98
  .prepare(`
71
- INSERT INTO panel_chats (id, name, last_text, last_date) VALUES (:id, :name, :text, :date)
99
+ INSERT INTO panel_chats (
100
+ id, name, first_name, last_name, username, last_text, last_date, last_attachment, last_event
101
+ ) VALUES (
102
+ :id, :name, :firstName, :lastName, :username, :text, :date, :lastAttachment, :lastEvent
103
+ )
72
104
  ON CONFLICT(id) DO UPDATE SET
73
105
  name = COALESCE(:name, panel_chats.name),
106
+ first_name = COALESCE(:firstName, panel_chats.first_name),
107
+ last_name = COALESCE(:lastName, panel_chats.last_name),
108
+ username = COALESCE(:username, panel_chats.username),
74
109
  last_text = :text,
75
- last_date = :date
110
+ last_date = :date,
111
+ last_attachment = :lastAttachment,
112
+ last_event = :lastEvent
76
113
  `)
77
- .run({ id: chat.id, name: chat.name ?? null, text: message.text, date: message.date });
114
+ .run({
115
+ id: chat.id,
116
+ name: chat.name ?? null,
117
+ firstName: chat.firstName ?? null,
118
+ lastName: chat.lastName ?? null,
119
+ username: chat.username ?? null,
120
+ text: message.text,
121
+ date: message.date,
122
+ lastAttachment: message.attachments?.[0]?.type ?? null,
123
+ lastEvent: message.event?.type ?? null,
124
+ });
78
125
 
79
126
  // brand-new chat with no name → give it a stable fallback label
80
127
  this.#db
@@ -88,15 +135,37 @@ export class SqlitePanelStore implements PanelStore {
88
135
 
89
136
  chats(): PanelChat[] {
90
137
  const rows = this.#db
91
- .prepare("SELECT id, name, last_text, last_date FROM panel_chats ORDER BY last_date DESC")
92
- .all() as Array<{ id: number; name: string | null; last_text: string; last_date: number }>;
93
-
94
- return rows.map((r) => ({
95
- id: r.id,
96
- name: r.name ?? `chat ${r.id}`,
97
- lastText: r.last_text,
98
- lastDate: r.last_date,
99
- }));
138
+ .prepare(
139
+ "SELECT id, name, first_name, last_name, username, last_text, last_date, last_attachment, last_event FROM panel_chats ORDER BY last_date DESC",
140
+ )
141
+ .all() as Array<{
142
+ id: number;
143
+ name: string | null;
144
+ first_name: string | null;
145
+ last_name: string | null;
146
+ username: string | null;
147
+ last_text: string;
148
+ last_date: number;
149
+ last_attachment: PanelChat["lastAttachmentType"] | null;
150
+ last_event: PanelChat["lastEventType"] | null;
151
+ }>;
152
+
153
+ return rows.map((r) => {
154
+ const chat: PanelChat = {
155
+ id: r.id,
156
+ name: r.name ?? `chat ${r.id}`,
157
+ lastText: r.last_text,
158
+ lastDate: r.last_date,
159
+ };
160
+
161
+ if (r.first_name) chat.firstName = r.first_name;
162
+ if (r.last_name) chat.lastName = r.last_name;
163
+ if (r.username) chat.username = r.username;
164
+ if (r.last_attachment) chat.lastAttachmentType = r.last_attachment;
165
+ if (r.last_event) chat.lastEventType = r.last_event;
166
+
167
+ return chat;
168
+ });
100
169
  }
101
170
 
102
171
  history(chatId: number, options?: HistoryOptions): PanelMessage[] {
@@ -106,7 +175,7 @@ export class SqlitePanelStore implements PanelStore {
106
175
  // grab the most recent `limit` rows older than `before`, then return ascending
107
176
  const rows = this.#db
108
177
  .prepare(`
109
- SELECT direction, text, date, attachments, media_group FROM panel_messages
178
+ SELECT direction, text, date, attachments, media_group, keyboard, event FROM panel_messages
110
179
  WHERE chat_id = ? AND date < ?
111
180
  ORDER BY date DESC, id DESC LIMIT ?
112
181
  `)
@@ -116,13 +185,17 @@ export class SqlitePanelStore implements PanelStore {
116
185
  date: number;
117
186
  attachments: string | null;
118
187
  media_group: string | null;
188
+ keyboard: string | null;
189
+ event: string | null;
119
190
  }>;
120
191
 
121
192
  return rows.reverse().map((r) => {
122
193
  const msg: PanelMessage = { direction: r.direction, text: r.text, date: r.date };
123
-
194
+
124
195
  if (r.attachments) msg.attachments = JSON.parse(r.attachments) as PanelAttachment[];
125
196
  if (r.media_group) msg.mediaGroupId = r.media_group;
197
+ if (r.keyboard) msg.keyboard = JSON.parse(r.keyboard) as PanelKeyboard;
198
+ if (r.event) msg.event = JSON.parse(r.event) as PanelMessageEvent;
126
199
 
127
200
  return msg;
128
201
  });