@fedify/botkit-sqlite 0.3.0-dev.132

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/README.md ADDED
@@ -0,0 +1,111 @@
1
+ @fedify/botkit-sqlite
2
+ =====================
3
+
4
+ [![JSR][JSR badge]][JSR]
5
+ [![npm][npm badge]][npm]
6
+ [![GitHub Actions][GitHub Actions badge]][GitHub Actions]
7
+
8
+ This package is a [SQLite]-based repository implementation for [BotKit].
9
+ It provides a production-ready data storage solution using the built-in
10
+ `node:sqlite` module, offering better performance and reliability compared to
11
+ in-memory storage while maintaining compatibility with both [Deno] and [Node.js]
12
+ environments.
13
+
14
+ [JSR]: https://jsr.io/@fedify/botkit-sqlite
15
+ [JSR badge]: https://jsr.io/badges/@fedify/botkit-sqlite
16
+ [npm]: https://www.npmjs.com/package/@fedify/botkit-sqlite
17
+ [npm badge]: https://img.shields.io/npm/v/@fedify/botkit-sqlite?logo=npm
18
+ [GitHub Actions]: https://github.com/fedify-dev/botkit/actions/workflows/main.yaml
19
+ [GitHub Actions badge]: https://github.com/fedify-dev/botkit/actions/workflows/main.yaml/badge.svg
20
+ [SQLite]: https://sqlite.org/
21
+ [BotKit]: https://botkit.fedify.dev/
22
+ [Deno]: https://deno.land/
23
+ [Node.js]: https://nodejs.org/
24
+
25
+
26
+ Installation
27
+ ------------
28
+
29
+ ~~~~ sh
30
+ deno add jsr:@fedify/botkit-sqlite
31
+ npm add @fedify/botkit-sqlite
32
+ pnpm add @fedify/botkit-sqlite
33
+ yarn add @fedify/botkit-sqlite
34
+ ~~~~
35
+
36
+
37
+ Usage
38
+ -----
39
+
40
+ The `SqliteRepository` can be used as a drop-in replacement for other repository
41
+ implementations in BotKit:
42
+
43
+ ~~~~ typescript
44
+ import { createBot } from "@fedify/botkit";
45
+ import { SqliteRepository } from "@fedify/botkit-sqlite";
46
+
47
+ const bot = createBot({
48
+ username: "mybot",
49
+ name: "My Bot",
50
+ repository: new SqliteRepository({
51
+ // Use a file-based database for persistence:
52
+ path: "./bot-data.db",
53
+ // Enable WAL mode for better performance (default: true):
54
+ wal: true,
55
+ }),
56
+ // ... other bot configuration
57
+ });
58
+ ~~~~
59
+
60
+ ### Options
61
+
62
+ The `SqliteRepository` constructor accepts the following options:
63
+
64
+ - **`path`** (optional): Path to the SQLite database file. Defaults to
65
+ `":memory:"` for an in-memory database. Use a file path for persistent
66
+ storage.
67
+
68
+ - **`wal`** (optional): Whether to enable write-ahead logging (WAL) mode for
69
+ better performance. Defaults to `true`. Note that WAL mode is automatically
70
+ disabled for in-memory databases.
71
+
72
+ ### Examples
73
+
74
+ #### In-memory database (for testing/development)
75
+
76
+ ~~~~ typescript
77
+ const repository = new SqliteRepository(); // Uses :memory: by default
78
+ ~~~~
79
+
80
+ #### File-based database (for production)
81
+
82
+ ~~~~ typescript
83
+ const repository = new SqliteRepository({
84
+ path: "./data/botkit.db",
85
+ wal: true,
86
+ });
87
+ ~~~~
88
+
89
+
90
+ Features
91
+ --------
92
+
93
+ - **Cross-runtime**: Works with both Deno and Node.js using the `node:sqlite`
94
+ module
95
+
96
+ - **High performance**: Utilizes WAL mode and proper indexing for optimal
97
+ performance
98
+
99
+ - **ACID compliance**: Transactions ensure data integrity and consistency
100
+
101
+ - **Full `Repository` API**: Implements all BotKit repository methods
102
+ including:
103
+
104
+ - Key pair management for ActivityPub signing
105
+ - Message storage and retrieval with temporal filtering
106
+ - Follower and following relationship management
107
+ - Poll voting system
108
+
109
+ - **Resource management**: Implements `Disposable` interface for proper cleanup
110
+
111
+ <!-- cSpell: ignore mybot -->
package/dist/mod.d.ts ADDED
@@ -0,0 +1,67 @@
1
+ import { Temporal, toTemporalInstant } from "@js-temporal/polyfill";
2
+ Date.prototype.toTemporalInstant = toTemporalInstant;
3
+ import { Actor, Announce, Create, Follow } from "@fedify/fedify/vocab";
4
+ import { Repository, RepositoryGetFollowersOptions, RepositoryGetMessagesOptions, Uuid } from "@fedify/botkit/repository";
5
+
6
+ //#region src/mod.d.ts
7
+ /**
8
+ * Options for creating a SQLite repository.
9
+ * @since 0.3.0
10
+ */
11
+ interface SqliteRepositoryOptions {
12
+ /**
13
+ * The path to the SQLite database file.
14
+ * If not provided, an in-memory database will be used.
15
+ */
16
+ readonly path?: string;
17
+ /**
18
+ * Whether to enable Write-Ahead Logging (WAL) mode.
19
+ * @default true
20
+ */
21
+ readonly wal?: boolean;
22
+ }
23
+ /**
24
+ * A repository for storing bot data using SQLite.
25
+ * @since 0.3.0
26
+ */
27
+ declare class SqliteRepository implements Repository, Disposable {
28
+ private readonly db;
29
+ /**
30
+ * Creates a new SQLite repository.
31
+ * @param options The options for creating the repository.
32
+ */
33
+ constructor(options?: SqliteRepositoryOptions);
34
+ [Symbol.dispose](): void;
35
+ /**
36
+ * Closes the database connection.
37
+ */
38
+ close(): void;
39
+ private initializeTables;
40
+ setKeyPairs(keyPairs: CryptoKeyPair[]): Promise<void>;
41
+ getKeyPairs(): Promise<CryptoKeyPair[] | undefined>;
42
+ addMessage(id: Uuid, activity: Create | Announce): Promise<void>;
43
+ updateMessage(id: Uuid, updater: (existing: Create | Announce) => Create | Announce | undefined | Promise<Create | Announce | undefined>): Promise<boolean>;
44
+ removeMessage(id: Uuid): Promise<Create | Announce | undefined>;
45
+ getMessages(options?: RepositoryGetMessagesOptions): AsyncIterable<Create | Announce>;
46
+ getMessage(id: Uuid): Promise<Create | Announce | undefined>;
47
+ countMessages(): Promise<number>;
48
+ addFollower(followRequestId: URL, follower: Actor): Promise<void>;
49
+ removeFollower(followRequestId: URL, actorId: URL): Promise<Actor | undefined>;
50
+ hasFollower(followerId: URL): Promise<boolean>;
51
+ getFollowers(options?: RepositoryGetFollowersOptions): AsyncIterable<Actor>;
52
+ countFollowers(): Promise<number>;
53
+ addSentFollow(id: Uuid, follow: Follow): Promise<void>;
54
+ removeSentFollow(id: Uuid): Promise<Follow | undefined>;
55
+ getSentFollow(id: Uuid): Promise<Follow | undefined>;
56
+ addFollowee(followeeId: URL, follow: Follow): Promise<void>;
57
+ removeFollowee(followeeId: URL): Promise<Follow | undefined>;
58
+ getFollowee(followeeId: URL): Promise<Follow | undefined>;
59
+ vote(messageId: Uuid, voterId: URL, option: string): Promise<void>;
60
+ countVoters(messageId: Uuid): Promise<number>;
61
+ countVotes(messageId: Uuid): Promise<Readonly<Record<string, number>>>;
62
+ }
63
+ //# sourceMappingURL=mod.d.ts.map
64
+
65
+ //#endregion
66
+ export { SqliteRepository, SqliteRepositoryOptions };
67
+ //# sourceMappingURL=mod.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mod.d.ts","names":[],"sources":["../src/mod.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;UAwCiB,uBAAA;;AAAjB;AAkBA;;EAA8B,SAOP,IAAA,CAAA,EAAA,MAAA;EAA4B;;;;EA4HP,SAArB,GAAA,CAAA,EAAA,OAAA;;;;;;AA0CP,cA7KH,gBAAA,YAA4B,UA6KzB,EA7KqC,UA6KrC,CAAA;EAAM,iBAAG,EAAA;EAAQ;;;;EACiC,WAAzB,CAAA,OAAA,CAAA,EAvKlB,uBAuKkB;EAAO,CAxJ7C,MAAA,CAAO,OAAA,GAyJL,EAAA,IAAA;EAAO;;;EAiC8C,KAAzB,CAAA,CAAA,EAAA,IAAA;EAAO,QA4B3B,gBAAA;EAAiC,WAC3B,CAAA,QAAA,EAlIW,aAkIX,EAAA,CAAA,EAlI6B,OAkI7B,CAAA,IAAA,CAAA;EAAM,WAAG,CAAA,CAAA,EA1GL,OA0GK,CA1GG,aA0GH,EAAA,GAAA,SAAA,CAAA;EAAQ,UAA/B,CAAA,EAAA,EAjFkB,IAiFlB,EAAA,QAAA,EAjFkC,MAiFlC,GAjF2C,QAiF3C,CAAA,EAjFsD,OAiFtD,CAAA,IAAA,CAAA;EAAa,aA2CK,CAAA,EAAA,EA7Gf,IA6Ge,EAAA,OAAA,EAAA,CAAA,QAAA,EA3GP,MA2GO,GA3GE,QA2GF,EAAA,GA1Gd,MA0Gc,GA1GL,QA0GK,GAAA,SAAA,GA1GkB,OA0GlB,CA1G0B,MA0G1B,GA1GmC,QA0GnC,GAAA,SAAA,CAAA,CAAA,EAzGlB,OAyGkB,CAAA,OAAA,CAAA;EAAI,aAAW,CAAA,EAAA,EAxEZ,IAwEY,CAAA,EAxEL,OAwEK,CAxEG,MAwEH,GAxEY,QAwEZ,GAAA,SAAA,CAAA;EAAM,WAAG,CAAA,OAAA,CAAA,EA5ClC,4BA4CkC,CAAA,EA3C1C,aA2C0C,CA3C5B,MA2C4B,GA3CnB,QA2CmB,CAAA;EAAQ,UAAzB,CAAA,EAAA,EAAP,IAAO,CAAA,EAAA,OAAA,CAAQ,MAAR,GAAiB,QAAjB,GAAA,SAAA,CAAA;EAAO,aAsBlB,CAAA,CAAA,EAAA,OAAA,CAAA,MAAA,CAAA;EAAO,WAMW,CAAA,eAAA,EAAA,GAAA,EAAA,QAAA,EAAe,KAAf,CAAA,EAAuB,OAAvB,CAAA,IAAA,CAAA;EAAG,cAAY,CAAA,eAAA,EA+B/B,GA/B+B,EAAA,OAAA,EAgCvC,GAhCuC,CAAA,EAiC/C,OAjC+C,CAiCvC,KAjCuC,GAAA,SAAA,CAAA;EAAK,WAAG,CAAA,UAAA,EAkFlC,GAlFkC,CAAA,EAkF5B,OAlF4B,CAAA,OAAA,CAAA;EAAO,YA+B9C,CAAA,OAAA,CAAA,EA4DR,6BA5DQ,CAAA,EA6DhB,aA7DgB,CA6DF,KA7DE,CAAA;EAAG,cACX,CAAA,CAAA,EA4FO,OA5FP,CAAA,MAAA,CAAA;EAAG,aACH,CAAA,EAAA,EAiGa,IAjGb,EAAA,MAAA,EAiG2B,MAjG3B,CAAA,EAiGoC,OAjGpC,CAAA,IAAA,CAAA;EAAK,gBAAb,CAAA,EAAA,EA8GwB,IA9GxB,CAAA,EA8G+B,OA9G/B,CA8GuC,MA9GvC,GAAA,SAAA,CAAA;EAAO,aAiDc,CAAA,EAAA,EAuEA,IAvEA,CAAA,EAuEO,OAvEP,CAuEe,MAvEf,GAAA,SAAA,CAAA;EAAG,WAAG,CAAA,UAAA,EAwFA,GAxFA,EAAA,MAAA,EAwFa,MAxFb,CAAA,EAwFsB,OAxFtB,CAAA,IAAA,CAAA;EAAO,cAS1B,CAAA,UAAA,EA4FsB,GA5FtB,CAAA,EA4F4B,OA5F5B,CA4FoC,MA5FpC,GAAA,SAAA,CAAA;EAAkC,WAC5B,CAAA,UAAA,EAqGa,GArGb,CAAA,EAqGmB,OArGnB,CAqG2B,MArG3B,GAAA,SAAA,CAAA;EAAK,IAAnB,CAAA,SAAA,EA2Ha,IA3Hb,EAAA,OAAA,EA2H4B,GA3H5B,EAAA,MAAA,EAAA,MAAA,CAAA,EA2HkD,OA3HlD,CAAA,IAAA,CAAA;EAAa,WAgCE,CAAA,SAAA,EAqGK,IArGL,CAAA,EAqGY,OArGZ,CAAA,MAAA,CAAA;EAAO,UAMD,CAAA,SAAA,EAyGF,IAzGE,CAAA,EAyGK,OAzGL,CAyGa,QAzGb,CAyGsB,MAzGtB,CAAA,MAAA,EAAA,MAAA,CAAA,CAAA,CAAA"}
package/dist/mod.js ADDED
@@ -0,0 +1,419 @@
1
+
2
+ import { Temporal, toTemporalInstant } from "@js-temporal/polyfill";
3
+ Date.prototype.toTemporalInstant = toTemporalInstant;
4
+
5
+ import { exportJwk, importJwk } from "@fedify/fedify/sig";
6
+ import { Activity, Announce, Create, Follow, Object as Object$1, isActor } from "@fedify/fedify/vocab";
7
+ import { getLogger } from "@logtape/logtape";
8
+ import { DatabaseSync } from "node:sqlite";
9
+
10
+ //#region src/mod.ts
11
+ const logger = getLogger(["botkit", "sqlite"]);
12
+ /**
13
+ * A repository for storing bot data using SQLite.
14
+ * @since 0.3.0
15
+ */
16
+ var SqliteRepository = class {
17
+ db;
18
+ /**
19
+ * Creates a new SQLite repository.
20
+ * @param options The options for creating the repository.
21
+ */
22
+ constructor(options = {}) {
23
+ const { path = ":memory:", wal = true } = options;
24
+ this.db = new DatabaseSync(path);
25
+ this.db.exec("PRAGMA foreign_keys = ON;");
26
+ if (wal && path !== ":memory:") this.db.exec("PRAGMA journal_mode = WAL;");
27
+ this.initializeTables();
28
+ }
29
+ [Symbol.dispose]() {
30
+ this.close();
31
+ }
32
+ /**
33
+ * Closes the database connection.
34
+ */
35
+ close() {
36
+ this.db.close();
37
+ }
38
+ initializeTables() {
39
+ this.db.exec(`
40
+ CREATE TABLE IF NOT EXISTS key_pairs (
41
+ id INTEGER PRIMARY KEY,
42
+ private_key_jwk TEXT NOT NULL,
43
+ public_key_jwk TEXT NOT NULL
44
+ )
45
+ `);
46
+ this.db.exec(`
47
+ CREATE TABLE IF NOT EXISTS messages (
48
+ id TEXT PRIMARY KEY,
49
+ activity_json TEXT NOT NULL,
50
+ published INTEGER
51
+ )
52
+ `);
53
+ this.db.exec(`
54
+ CREATE INDEX IF NOT EXISTS idx_messages_published ON messages(published)
55
+ `);
56
+ this.db.exec(`
57
+ CREATE TABLE IF NOT EXISTS followers (
58
+ follower_id TEXT PRIMARY KEY,
59
+ actor_json TEXT NOT NULL
60
+ )
61
+ `);
62
+ this.db.exec(`
63
+ CREATE TABLE IF NOT EXISTS follow_requests (
64
+ follow_request_id TEXT PRIMARY KEY,
65
+ follower_id TEXT NOT NULL,
66
+ FOREIGN KEY (follower_id) REFERENCES followers(follower_id)
67
+ )
68
+ `);
69
+ this.db.exec(`
70
+ CREATE TABLE IF NOT EXISTS sent_follows (
71
+ id TEXT PRIMARY KEY,
72
+ follow_json TEXT NOT NULL
73
+ )
74
+ `);
75
+ this.db.exec(`
76
+ CREATE TABLE IF NOT EXISTS followees (
77
+ followee_id TEXT PRIMARY KEY,
78
+ follow_json TEXT NOT NULL
79
+ )
80
+ `);
81
+ this.db.exec(`
82
+ CREATE TABLE IF NOT EXISTS poll_votes (
83
+ message_id TEXT NOT NULL,
84
+ voter_id TEXT NOT NULL,
85
+ option TEXT NOT NULL,
86
+ PRIMARY KEY (message_id, voter_id, option)
87
+ )
88
+ `);
89
+ this.db.exec(`
90
+ CREATE INDEX IF NOT EXISTS idx_poll_votes_message_option
91
+ ON poll_votes(message_id, option)
92
+ `);
93
+ }
94
+ async setKeyPairs(keyPairs) {
95
+ const deleteStmt = this.db.prepare("DELETE FROM key_pairs");
96
+ const insertStmt = this.db.prepare(`
97
+ INSERT INTO key_pairs (private_key_jwk, public_key_jwk)
98
+ VALUES (?, ?)
99
+ `);
100
+ this.db.exec("BEGIN TRANSACTION");
101
+ try {
102
+ deleteStmt.run();
103
+ for (const keyPair of keyPairs) {
104
+ const privateJwk = await exportJwk(keyPair.privateKey);
105
+ const publicJwk = await exportJwk(keyPair.publicKey);
106
+ insertStmt.run(JSON.stringify(privateJwk), JSON.stringify(publicJwk));
107
+ }
108
+ this.db.exec("COMMIT");
109
+ } catch (error) {
110
+ this.db.exec("ROLLBACK");
111
+ throw error;
112
+ }
113
+ }
114
+ async getKeyPairs() {
115
+ const stmt = this.db.prepare(`
116
+ SELECT private_key_jwk, public_key_jwk FROM key_pairs
117
+ `);
118
+ const rows = stmt.all();
119
+ if (rows.length === 0) return void 0;
120
+ const keyPairs = [];
121
+ for (const row of rows) {
122
+ const privateJwk = JSON.parse(row.private_key_jwk);
123
+ const publicJwk = JSON.parse(row.public_key_jwk);
124
+ keyPairs.push({
125
+ privateKey: await importJwk(privateJwk, "private"),
126
+ publicKey: await importJwk(publicJwk, "public")
127
+ });
128
+ }
129
+ return keyPairs;
130
+ }
131
+ async addMessage(id, activity) {
132
+ const stmt = this.db.prepare(`
133
+ INSERT INTO messages (id, activity_json, published)
134
+ VALUES (?, ?, ?)
135
+ `);
136
+ const activityJson = JSON.stringify(await activity.toJsonLd({ format: "compact" }));
137
+ const published = activity.published?.epochMilliseconds ?? null;
138
+ stmt.run(id, activityJson, published);
139
+ }
140
+ async updateMessage(id, updater) {
141
+ const selectStmt = this.db.prepare(`
142
+ SELECT activity_json FROM messages WHERE id = ?
143
+ `);
144
+ const row = selectStmt.get(id);
145
+ if (!row) return false;
146
+ const activityData = JSON.parse(row.activity_json);
147
+ const activity = await Activity.fromJsonLd(activityData);
148
+ if (!(activity instanceof Create || activity instanceof Announce)) return false;
149
+ const newActivity = await updater(activity);
150
+ if (newActivity == null) return false;
151
+ const updateStmt = this.db.prepare(`
152
+ UPDATE messages
153
+ SET activity_json = ?, published = ?
154
+ WHERE id = ?
155
+ `);
156
+ const newActivityJson = JSON.stringify(await newActivity.toJsonLd({ format: "compact" }));
157
+ const published = newActivity.published?.epochMilliseconds ?? null;
158
+ updateStmt.run(newActivityJson, published, id);
159
+ return true;
160
+ }
161
+ async removeMessage(id) {
162
+ const selectStmt = this.db.prepare(`
163
+ SELECT activity_json FROM messages WHERE id = ?
164
+ `);
165
+ const row = selectStmt.get(id);
166
+ if (!row) return void 0;
167
+ const deleteStmt = this.db.prepare(`
168
+ DELETE FROM messages WHERE id = ?
169
+ `);
170
+ deleteStmt.run(id);
171
+ try {
172
+ const activityData = JSON.parse(row.activity_json);
173
+ const activity = await Activity.fromJsonLd(activityData);
174
+ if (activity instanceof Create || activity instanceof Announce) return activity;
175
+ } catch (error) {
176
+ logger.warn("Failed to parse removed message activity", {
177
+ id,
178
+ error
179
+ });
180
+ }
181
+ return void 0;
182
+ }
183
+ async *getMessages(options = {}) {
184
+ const { order = "newest", until, since, limit } = options;
185
+ let sql = "SELECT activity_json FROM messages WHERE 1=1";
186
+ const params = [];
187
+ if (since != null) {
188
+ sql += " AND published >= ?";
189
+ params.push(since.epochMilliseconds);
190
+ }
191
+ if (until != null) {
192
+ sql += " AND published <= ?";
193
+ params.push(until.epochMilliseconds);
194
+ }
195
+ sql += order === "oldest" ? " ORDER BY published ASC" : " ORDER BY published DESC";
196
+ if (limit != null) {
197
+ sql += " LIMIT ?";
198
+ params.push(limit);
199
+ }
200
+ const stmt = this.db.prepare(sql);
201
+ const rows = stmt.all(...params);
202
+ for (const row of rows) try {
203
+ const activityData = JSON.parse(row.activity_json);
204
+ const activity = await Activity.fromJsonLd(activityData);
205
+ if (activity instanceof Create || activity instanceof Announce) yield activity;
206
+ } catch (error) {
207
+ logger.warn("Failed to parse message activity", { error });
208
+ continue;
209
+ }
210
+ }
211
+ async getMessage(id) {
212
+ const stmt = this.db.prepare(`
213
+ SELECT activity_json FROM messages WHERE id = ?
214
+ `);
215
+ const row = stmt.get(id);
216
+ if (!row) return void 0;
217
+ try {
218
+ const activityData = JSON.parse(row.activity_json);
219
+ const activity = await Activity.fromJsonLd(activityData);
220
+ if (activity instanceof Create || activity instanceof Announce) return activity;
221
+ } catch (error) {
222
+ logger.warn("Failed to parse message activity", {
223
+ id,
224
+ error
225
+ });
226
+ }
227
+ return void 0;
228
+ }
229
+ countMessages() {
230
+ const stmt = this.db.prepare("SELECT COUNT(*) as count FROM messages");
231
+ const row = stmt.get();
232
+ return Promise.resolve(row.count);
233
+ }
234
+ async addFollower(followRequestId, follower) {
235
+ if (follower.id == null) throw new TypeError("The follower ID is missing.");
236
+ const followerJson = JSON.stringify(await follower.toJsonLd({ format: "compact" }));
237
+ const insertFollowerStmt = this.db.prepare(`
238
+ INSERT OR REPLACE INTO followers (follower_id, actor_json)
239
+ VALUES (?, ?)
240
+ `);
241
+ const insertRequestStmt = this.db.prepare(`
242
+ INSERT OR REPLACE INTO follow_requests (follow_request_id, follower_id)
243
+ VALUES (?, ?)
244
+ `);
245
+ this.db.exec("BEGIN TRANSACTION");
246
+ try {
247
+ insertFollowerStmt.run(follower.id.href, followerJson);
248
+ insertRequestStmt.run(followRequestId.href, follower.id.href);
249
+ this.db.exec("COMMIT");
250
+ } catch (error) {
251
+ this.db.exec("ROLLBACK");
252
+ throw error;
253
+ }
254
+ }
255
+ async removeFollower(followRequestId, actorId) {
256
+ const checkStmt = this.db.prepare(`
257
+ SELECT fr.follower_id, f.actor_json
258
+ FROM follow_requests fr
259
+ JOIN followers f ON fr.follower_id = f.follower_id
260
+ WHERE fr.follow_request_id = ? AND fr.follower_id = ?
261
+ `);
262
+ const row = checkStmt.get(followRequestId.href, actorId.href);
263
+ if (!row) return void 0;
264
+ const deleteRequestStmt = this.db.prepare(`
265
+ DELETE FROM follow_requests WHERE follow_request_id = ?
266
+ `);
267
+ const deleteFollowerStmt = this.db.prepare(`
268
+ DELETE FROM followers WHERE follower_id = ?
269
+ `);
270
+ this.db.exec("BEGIN TRANSACTION");
271
+ try {
272
+ deleteRequestStmt.run(followRequestId.href);
273
+ deleteFollowerStmt.run(actorId.href);
274
+ this.db.exec("COMMIT");
275
+ } catch (error) {
276
+ this.db.exec("ROLLBACK");
277
+ throw error;
278
+ }
279
+ try {
280
+ const actorData = JSON.parse(row.actor_json);
281
+ const actor = await Object$1.fromJsonLd(actorData);
282
+ if (isActor(actor)) return actor;
283
+ } catch (error) {
284
+ logger.warn("Failed to parse removed follower actor", { error });
285
+ }
286
+ return void 0;
287
+ }
288
+ hasFollower(followerId) {
289
+ const stmt = this.db.prepare(`
290
+ SELECT 1 FROM followers WHERE follower_id = ?
291
+ `);
292
+ const row = stmt.get(followerId.href);
293
+ return Promise.resolve(row != null);
294
+ }
295
+ async *getFollowers(options = {}) {
296
+ const { offset = 0, limit } = options;
297
+ let sql = "SELECT actor_json FROM followers ORDER BY follower_id";
298
+ const params = [];
299
+ if (limit != null) {
300
+ sql += " LIMIT ? OFFSET ?";
301
+ params.push(limit, offset);
302
+ } else if (offset > 0) {
303
+ sql += " LIMIT -1 OFFSET ?";
304
+ params.push(offset);
305
+ }
306
+ const stmt = this.db.prepare(sql);
307
+ const rows = stmt.all(...params);
308
+ for (const row of rows) try {
309
+ const actorData = JSON.parse(row.actor_json);
310
+ const actor = await Object$1.fromJsonLd(actorData);
311
+ if (isActor(actor)) yield actor;
312
+ } catch (error) {
313
+ logger.warn("Failed to parse follower actor", { error });
314
+ continue;
315
+ }
316
+ }
317
+ countFollowers() {
318
+ const stmt = this.db.prepare("SELECT COUNT(*) as count FROM followers");
319
+ const row = stmt.get();
320
+ return Promise.resolve(row.count);
321
+ }
322
+ async addSentFollow(id, follow) {
323
+ const stmt = this.db.prepare(`
324
+ INSERT OR REPLACE INTO sent_follows (id, follow_json)
325
+ VALUES (?, ?)
326
+ `);
327
+ const followJson = JSON.stringify(await follow.toJsonLd({ format: "compact" }));
328
+ stmt.run(id, followJson);
329
+ }
330
+ async removeSentFollow(id) {
331
+ const follow = await this.getSentFollow(id);
332
+ if (follow == null) return void 0;
333
+ const stmt = this.db.prepare("DELETE FROM sent_follows WHERE id = ?");
334
+ stmt.run(id);
335
+ return follow;
336
+ }
337
+ async getSentFollow(id) {
338
+ const stmt = this.db.prepare(`
339
+ SELECT follow_json FROM sent_follows WHERE id = ?
340
+ `);
341
+ const row = stmt.get(id);
342
+ if (!row) return void 0;
343
+ try {
344
+ const followData = JSON.parse(row.follow_json);
345
+ return await Follow.fromJsonLd(followData);
346
+ } catch (error) {
347
+ logger.warn("Failed to parse sent follow activity", {
348
+ id,
349
+ error
350
+ });
351
+ return void 0;
352
+ }
353
+ }
354
+ async addFollowee(followeeId, follow) {
355
+ const stmt = this.db.prepare(`
356
+ INSERT OR REPLACE INTO followees (followee_id, follow_json)
357
+ VALUES (?, ?)
358
+ `);
359
+ const followJson = JSON.stringify(await follow.toJsonLd({ format: "compact" }));
360
+ stmt.run(followeeId.href, followJson);
361
+ }
362
+ async removeFollowee(followeeId) {
363
+ const follow = await this.getFollowee(followeeId);
364
+ if (follow == null) return void 0;
365
+ const stmt = this.db.prepare("DELETE FROM followees WHERE followee_id = ?");
366
+ stmt.run(followeeId.href);
367
+ return follow;
368
+ }
369
+ async getFollowee(followeeId) {
370
+ const stmt = this.db.prepare(`
371
+ SELECT follow_json FROM followees WHERE followee_id = ?
372
+ `);
373
+ const row = stmt.get(followeeId.href);
374
+ if (!row) return void 0;
375
+ try {
376
+ const followData = JSON.parse(row.follow_json);
377
+ return await Follow.fromJsonLd(followData);
378
+ } catch (error) {
379
+ logger.warn("Failed to parse followee activity", {
380
+ followeeId: followeeId.href,
381
+ error
382
+ });
383
+ return void 0;
384
+ }
385
+ }
386
+ vote(messageId, voterId, option) {
387
+ const stmt = this.db.prepare(`
388
+ INSERT OR IGNORE INTO poll_votes (message_id, voter_id, option)
389
+ VALUES (?, ?, ?)
390
+ `);
391
+ stmt.run(messageId, voterId.href, option);
392
+ return Promise.resolve();
393
+ }
394
+ countVoters(messageId) {
395
+ const stmt = this.db.prepare(`
396
+ SELECT COUNT(DISTINCT voter_id) as count
397
+ FROM poll_votes
398
+ WHERE message_id = ?
399
+ `);
400
+ const row = stmt.get(messageId);
401
+ return Promise.resolve(row.count);
402
+ }
403
+ countVotes(messageId) {
404
+ const stmt = this.db.prepare(`
405
+ SELECT option, COUNT(*) as count
406
+ FROM poll_votes
407
+ WHERE message_id = ?
408
+ GROUP BY option
409
+ `);
410
+ const rows = stmt.all(messageId);
411
+ const result = {};
412
+ for (const row of rows) result[row.option] = row.count;
413
+ return Promise.resolve(result);
414
+ }
415
+ };
416
+
417
+ //#endregion
418
+ export { SqliteRepository };
419
+ //# sourceMappingURL=mod.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mod.js","names":["options: SqliteRepositoryOptions","keyPairs: CryptoKeyPair[]","id: Uuid","activity: Create | Announce","updater: (\n existing: Create | Announce,\n ) => Create | Announce | undefined | Promise<Create | Announce | undefined>","options: RepositoryGetMessagesOptions","params: (number | string)[]","followRequestId: URL","follower: Actor","actorId: URL","followerId: URL","options: RepositoryGetFollowersOptions","params: number[]","follow: Follow","followeeId: URL","messageId: Uuid","voterId: URL","option: string","result: Record<string, number>"],"sources":["../src/mod.ts"],"sourcesContent":["// BotKit by Fedify: A framework for creating ActivityPub bots\n// Copyright (C) 2025 Hong Minhee <https://hongminhee.org/>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program. If not, see <https://www.gnu.org/licenses/>.\nimport type {\n Repository,\n RepositoryGetFollowersOptions,\n RepositoryGetMessagesOptions,\n Uuid,\n} from \"@fedify/botkit/repository\";\nimport { exportJwk, importJwk } from \"@fedify/fedify/sig\";\nimport {\n Activity,\n type Actor,\n Announce,\n Create,\n Follow,\n isActor,\n Object,\n} from \"@fedify/fedify/vocab\";\nimport { getLogger } from \"@logtape/logtape\";\nimport { DatabaseSync } from \"node:sqlite\";\n\nconst logger = getLogger([\"botkit\", \"sqlite\"]);\n\n/**\n * Options for creating a SQLite repository.\n * @since 0.3.0\n */\nexport interface SqliteRepositoryOptions {\n /**\n * The path to the SQLite database file.\n * If not provided, an in-memory database will be used.\n */\n readonly path?: string;\n\n /**\n * Whether to enable Write-Ahead Logging (WAL) mode.\n * @default true\n */\n readonly wal?: boolean;\n}\n\n/**\n * A repository for storing bot data using SQLite.\n * @since 0.3.0\n */\nexport class SqliteRepository implements Repository, Disposable {\n private readonly db: DatabaseSync;\n\n /**\n * Creates a new SQLite repository.\n * @param options The options for creating the repository.\n */\n constructor(options: SqliteRepositoryOptions = {}) {\n const { path = \":memory:\", wal = true } = options;\n\n this.db = new DatabaseSync(path);\n\n // Enable foreign key constraints\n this.db.exec(\"PRAGMA foreign_keys = ON;\");\n\n if (wal && path !== \":memory:\") {\n this.db.exec(\"PRAGMA journal_mode = WAL;\");\n }\n\n this.initializeTables();\n }\n\n [Symbol.dispose]() {\n this.close();\n }\n\n /**\n * Closes the database connection.\n */\n close(): void {\n this.db.close();\n }\n\n private initializeTables(): void {\n // Key pairs table\n this.db.exec(`\n CREATE TABLE IF NOT EXISTS key_pairs (\n id INTEGER PRIMARY KEY,\n private_key_jwk TEXT NOT NULL,\n public_key_jwk TEXT NOT NULL\n )\n `);\n\n // Messages table\n this.db.exec(`\n CREATE TABLE IF NOT EXISTS messages (\n id TEXT PRIMARY KEY,\n activity_json TEXT NOT NULL,\n published INTEGER\n )\n `);\n\n // Create index on published timestamp for efficient ordering\n this.db.exec(`\n CREATE INDEX IF NOT EXISTS idx_messages_published ON messages(published)\n `);\n\n // Followers table\n this.db.exec(`\n CREATE TABLE IF NOT EXISTS followers (\n follower_id TEXT PRIMARY KEY,\n actor_json TEXT NOT NULL\n )\n `);\n\n // Follow requests mapping table\n this.db.exec(`\n CREATE TABLE IF NOT EXISTS follow_requests (\n follow_request_id TEXT PRIMARY KEY,\n follower_id TEXT NOT NULL,\n FOREIGN KEY (follower_id) REFERENCES followers(follower_id)\n )\n `);\n\n // Sent follows table\n this.db.exec(`\n CREATE TABLE IF NOT EXISTS sent_follows (\n id TEXT PRIMARY KEY,\n follow_json TEXT NOT NULL\n )\n `);\n\n // Followees table\n this.db.exec(`\n CREATE TABLE IF NOT EXISTS followees (\n followee_id TEXT PRIMARY KEY,\n follow_json TEXT NOT NULL\n )\n `);\n\n // Poll votes table\n this.db.exec(`\n CREATE TABLE IF NOT EXISTS poll_votes (\n message_id TEXT NOT NULL,\n voter_id TEXT NOT NULL,\n option TEXT NOT NULL,\n PRIMARY KEY (message_id, voter_id, option)\n )\n `);\n\n // Create index for efficient vote counting\n this.db.exec(`\n CREATE INDEX IF NOT EXISTS idx_poll_votes_message_option \n ON poll_votes(message_id, option)\n `);\n }\n\n async setKeyPairs(keyPairs: CryptoKeyPair[]): Promise<void> {\n const deleteStmt = this.db.prepare(\"DELETE FROM key_pairs\");\n const insertStmt = this.db.prepare(`\n INSERT INTO key_pairs (private_key_jwk, public_key_jwk) \n VALUES (?, ?)\n `);\n\n this.db.exec(\"BEGIN TRANSACTION\");\n try {\n deleteStmt.run();\n\n for (const keyPair of keyPairs) {\n const privateJwk = await exportJwk(keyPair.privateKey);\n const publicJwk = await exportJwk(keyPair.publicKey);\n insertStmt.run(JSON.stringify(privateJwk), JSON.stringify(publicJwk));\n }\n\n this.db.exec(\"COMMIT\");\n } catch (error) {\n this.db.exec(\"ROLLBACK\");\n throw error;\n }\n }\n\n async getKeyPairs(): Promise<CryptoKeyPair[] | undefined> {\n const stmt = this.db.prepare(`\n SELECT private_key_jwk, public_key_jwk FROM key_pairs\n `);\n const rows = stmt.all() as Array<{\n private_key_jwk: string;\n public_key_jwk: string;\n }>;\n\n if (rows.length === 0) return undefined;\n\n const keyPairs: CryptoKeyPair[] = [];\n for (const row of rows) {\n const privateJwk = JSON.parse(row.private_key_jwk);\n const publicJwk = JSON.parse(row.public_key_jwk);\n\n keyPairs.push({\n privateKey: await importJwk(privateJwk, \"private\"),\n publicKey: await importJwk(publicJwk, \"public\"),\n });\n }\n\n return keyPairs;\n }\n\n async addMessage(id: Uuid, activity: Create | Announce): Promise<void> {\n const stmt = this.db.prepare(`\n INSERT INTO messages (id, activity_json, published) \n VALUES (?, ?, ?)\n `);\n\n const activityJson = JSON.stringify(\n await activity.toJsonLd({ format: \"compact\" }),\n );\n const published = activity.published?.epochMilliseconds ?? null;\n\n stmt.run(id, activityJson, published);\n }\n\n async updateMessage(\n id: Uuid,\n updater: (\n existing: Create | Announce,\n ) => Create | Announce | undefined | Promise<Create | Announce | undefined>,\n ): Promise<boolean> {\n const selectStmt = this.db.prepare(`\n SELECT activity_json FROM messages WHERE id = ?\n `);\n const row = selectStmt.get(id) as { activity_json: string } | undefined;\n\n if (!row) return false;\n\n const activityData = JSON.parse(row.activity_json);\n const activity = await Activity.fromJsonLd(activityData);\n\n if (!(activity instanceof Create || activity instanceof Announce)) {\n return false;\n }\n\n const newActivity = await updater(activity);\n if (newActivity == null) return false;\n\n const updateStmt = this.db.prepare(`\n UPDATE messages \n SET activity_json = ?, published = ? \n WHERE id = ?\n `);\n\n const newActivityJson = JSON.stringify(\n await newActivity.toJsonLd({ format: \"compact\" }),\n );\n const published = newActivity.published?.epochMilliseconds ?? null;\n\n updateStmt.run(newActivityJson, published, id);\n return true;\n }\n\n async removeMessage(id: Uuid): Promise<Create | Announce | undefined> {\n const selectStmt = this.db.prepare(`\n SELECT activity_json FROM messages WHERE id = ?\n `);\n const row = selectStmt.get(id) as { activity_json: string } | undefined;\n\n if (!row) return undefined;\n\n const deleteStmt = this.db.prepare(`\n DELETE FROM messages WHERE id = ?\n `);\n deleteStmt.run(id);\n\n try {\n const activityData = JSON.parse(row.activity_json);\n const activity = await Activity.fromJsonLd(activityData);\n\n if (activity instanceof Create || activity instanceof Announce) {\n return activity;\n }\n } catch (error) {\n logger.warn(\"Failed to parse removed message activity\", { id, error });\n }\n\n return undefined;\n }\n\n async *getMessages(\n options: RepositoryGetMessagesOptions = {},\n ): AsyncIterable<Create | Announce> {\n const { order = \"newest\", until, since, limit } = options;\n\n let sql = \"SELECT activity_json FROM messages WHERE 1=1\";\n const params: (number | string)[] = [];\n\n if (since != null) {\n sql += \" AND published >= ?\";\n params.push(since.epochMilliseconds);\n }\n\n if (until != null) {\n sql += \" AND published <= ?\";\n params.push(until.epochMilliseconds);\n }\n\n sql += order === \"oldest\"\n ? \" ORDER BY published ASC\"\n : \" ORDER BY published DESC\";\n\n if (limit != null) {\n sql += \" LIMIT ?\";\n params.push(limit);\n }\n\n const stmt = this.db.prepare(sql);\n const rows = stmt.all(...params) as Array<{ activity_json: string }>;\n\n for (const row of rows) {\n try {\n const activityData = JSON.parse(row.activity_json);\n const activity = await Activity.fromJsonLd(activityData);\n\n if (activity instanceof Create || activity instanceof Announce) {\n yield activity;\n }\n } catch (error) {\n logger.warn(\"Failed to parse message activity\", { error });\n continue;\n }\n }\n }\n\n async getMessage(id: Uuid): Promise<Create | Announce | undefined> {\n const stmt = this.db.prepare(`\n SELECT activity_json FROM messages WHERE id = ?\n `);\n const row = stmt.get(id) as { activity_json: string } | undefined;\n\n if (!row) return undefined;\n\n try {\n const activityData = JSON.parse(row.activity_json);\n const activity = await Activity.fromJsonLd(activityData);\n\n if (activity instanceof Create || activity instanceof Announce) {\n return activity;\n }\n } catch (error) {\n logger.warn(\"Failed to parse message activity\", { id, error });\n }\n\n return undefined;\n }\n\n countMessages(): Promise<number> {\n const stmt = this.db.prepare(\"SELECT COUNT(*) as count FROM messages\");\n const row = stmt.get() as { count: number };\n return Promise.resolve(row.count);\n }\n\n async addFollower(followRequestId: URL, follower: Actor): Promise<void> {\n if (follower.id == null) {\n throw new TypeError(\"The follower ID is missing.\");\n }\n\n const followerJson = JSON.stringify(\n await follower.toJsonLd({ format: \"compact\" }),\n );\n\n const insertFollowerStmt = this.db.prepare(`\n INSERT OR REPLACE INTO followers (follower_id, actor_json) \n VALUES (?, ?)\n `);\n\n const insertRequestStmt = this.db.prepare(`\n INSERT OR REPLACE INTO follow_requests (follow_request_id, follower_id) \n VALUES (?, ?)\n `);\n\n this.db.exec(\"BEGIN TRANSACTION\");\n try {\n insertFollowerStmt.run(follower.id.href, followerJson);\n insertRequestStmt.run(followRequestId.href, follower.id.href);\n this.db.exec(\"COMMIT\");\n } catch (error) {\n this.db.exec(\"ROLLBACK\");\n throw error;\n }\n }\n\n async removeFollower(\n followRequestId: URL,\n actorId: URL,\n ): Promise<Actor | undefined> {\n // Check if the follow request exists and matches the actor\n const checkStmt = this.db.prepare(`\n SELECT fr.follower_id, f.actor_json \n FROM follow_requests fr \n JOIN followers f ON fr.follower_id = f.follower_id \n WHERE fr.follow_request_id = ? AND fr.follower_id = ?\n `);\n\n const row = checkStmt.get(followRequestId.href, actorId.href) as {\n follower_id: string;\n actor_json: string;\n } | undefined;\n\n if (!row) return undefined;\n\n // Remove the follower and follow request\n const deleteRequestStmt = this.db.prepare(`\n DELETE FROM follow_requests WHERE follow_request_id = ?\n `);\n\n const deleteFollowerStmt = this.db.prepare(`\n DELETE FROM followers WHERE follower_id = ?\n `);\n\n this.db.exec(\"BEGIN TRANSACTION\");\n try {\n deleteRequestStmt.run(followRequestId.href);\n deleteFollowerStmt.run(actorId.href);\n this.db.exec(\"COMMIT\");\n } catch (error) {\n this.db.exec(\"ROLLBACK\");\n throw error;\n }\n\n try {\n const actorData = JSON.parse(row.actor_json);\n const actor = await Object.fromJsonLd(actorData);\n\n if (isActor(actor)) {\n return actor;\n }\n } catch (error) {\n logger.warn(\"Failed to parse removed follower actor\", { error });\n }\n\n return undefined;\n }\n\n hasFollower(followerId: URL): Promise<boolean> {\n const stmt = this.db.prepare(`\n SELECT 1 FROM followers WHERE follower_id = ?\n `);\n const row = stmt.get(followerId.href);\n return Promise.resolve(row != null);\n }\n\n async *getFollowers(\n options: RepositoryGetFollowersOptions = {},\n ): AsyncIterable<Actor> {\n const { offset = 0, limit } = options;\n\n let sql = \"SELECT actor_json FROM followers ORDER BY follower_id\";\n const params: number[] = [];\n\n if (limit != null) {\n sql += \" LIMIT ? OFFSET ?\";\n params.push(limit, offset);\n } else if (offset > 0) {\n sql += \" LIMIT -1 OFFSET ?\";\n params.push(offset);\n }\n\n const stmt = this.db.prepare(sql);\n const rows = stmt.all(...params) as { actor_json: string }[];\n\n for (const row of rows) {\n try {\n const actorData = JSON.parse(row.actor_json);\n const actor = await Object.fromJsonLd(actorData);\n\n if (isActor(actor)) {\n yield actor;\n }\n } catch (error) {\n logger.warn(\"Failed to parse follower actor\", { error });\n continue;\n }\n }\n }\n\n countFollowers(): Promise<number> {\n const stmt = this.db.prepare(\"SELECT COUNT(*) as count FROM followers\");\n const row = stmt.get() as { count: number };\n return Promise.resolve(row.count);\n }\n\n async addSentFollow(id: Uuid, follow: Follow): Promise<void> {\n const stmt = this.db.prepare(`\n INSERT OR REPLACE INTO sent_follows (id, follow_json) \n VALUES (?, ?)\n `);\n\n const followJson = JSON.stringify(\n await follow.toJsonLd({ format: \"compact\" }),\n );\n\n stmt.run(id, followJson);\n }\n\n async removeSentFollow(id: Uuid): Promise<Follow | undefined> {\n const follow = await this.getSentFollow(id);\n if (follow == null) return undefined;\n\n const stmt = this.db.prepare(\"DELETE FROM sent_follows WHERE id = ?\");\n stmt.run(id);\n\n return follow;\n }\n\n async getSentFollow(id: Uuid): Promise<Follow | undefined> {\n const stmt = this.db.prepare(`\n SELECT follow_json FROM sent_follows WHERE id = ?\n `);\n const row = stmt.get(id) as { follow_json: string } | undefined;\n\n if (!row) return undefined;\n\n try {\n const followData = JSON.parse(row.follow_json);\n return await Follow.fromJsonLd(followData);\n } catch (error) {\n logger.warn(\"Failed to parse sent follow activity\", { id, error });\n return undefined;\n }\n }\n\n async addFollowee(followeeId: URL, follow: Follow): Promise<void> {\n const stmt = this.db.prepare(`\n INSERT OR REPLACE INTO followees (followee_id, follow_json) \n VALUES (?, ?)\n `);\n\n const followJson = JSON.stringify(\n await follow.toJsonLd({ format: \"compact\" }),\n );\n\n stmt.run(followeeId.href, followJson);\n }\n\n async removeFollowee(followeeId: URL): Promise<Follow | undefined> {\n const follow = await this.getFollowee(followeeId);\n if (follow == null) return undefined;\n\n const stmt = this.db.prepare(\"DELETE FROM followees WHERE followee_id = ?\");\n stmt.run(followeeId.href);\n\n return follow;\n }\n\n async getFollowee(followeeId: URL): Promise<Follow | undefined> {\n const stmt = this.db.prepare(`\n SELECT follow_json FROM followees WHERE followee_id = ?\n `);\n const row = stmt.get(followeeId.href) as\n | { follow_json: string }\n | undefined;\n\n if (!row) return undefined;\n\n try {\n const followData = JSON.parse(row.follow_json);\n return await Follow.fromJsonLd(followData);\n } catch (error) {\n logger.warn(\"Failed to parse followee activity\", {\n followeeId: followeeId.href,\n error,\n });\n return undefined;\n }\n }\n\n vote(messageId: Uuid, voterId: URL, option: string): Promise<void> {\n const stmt = this.db.prepare(`\n INSERT OR IGNORE INTO poll_votes (message_id, voter_id, option) \n VALUES (?, ?, ?)\n `);\n\n stmt.run(messageId, voterId.href, option);\n return Promise.resolve();\n }\n\n countVoters(messageId: Uuid): Promise<number> {\n const stmt = this.db.prepare(`\n SELECT COUNT(DISTINCT voter_id) as count \n FROM poll_votes \n WHERE message_id = ?\n `);\n const row = stmt.get(messageId) as { count: number };\n return Promise.resolve(row.count);\n }\n\n countVotes(messageId: Uuid): Promise<Readonly<Record<string, number>>> {\n const stmt = this.db.prepare(`\n SELECT option, COUNT(*) as count \n FROM poll_votes \n WHERE message_id = ? \n GROUP BY option\n `);\n const rows = stmt.all(messageId) as Array<{\n option: string;\n count: number;\n }>;\n\n const result: Record<string, number> = {};\n for (const row of rows) {\n result[row.option] = row.count;\n }\n\n return Promise.resolve(result);\n }\n}\n"],"mappings":"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"}