@fedify/botkit-postgres 0.4.0-dev.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/dist/mod.js ADDED
@@ -0,0 +1,460 @@
1
+ import { exportJwk, importJwk } from "@fedify/fedify/sig";
2
+ import { Temporal, toTemporalInstant } from "@js-temporal/polyfill";
3
+ import { Activity, Announce, Create, Follow, Object as Object$1, isActor } from "@fedify/vocab";
4
+ import { getLogger } from "@logtape/logtape";
5
+ import postgres from "postgres";
6
+
7
+ //#region src/mod.ts
8
+ if (!("Temporal" in globalThis)) Reflect.set(globalThis, "Temporal", Temporal);
9
+ if (Date.prototype.toTemporalInstant == null) Reflect.set(Date.prototype, "toTemporalInstant", toTemporalInstant);
10
+ const logger = getLogger(["botkit", "postgres"]);
11
+ const schemaNamePattern = /^[A-Za-z_][A-Za-z0-9_]*$/;
12
+ const followRequestAdvisoryLockNamespace = 16980;
13
+ const followerAdvisoryLockNamespace = 16966;
14
+ /**
15
+ * Initializes the PostgreSQL schema used by BotKit repositories.
16
+ * @param sql The PostgreSQL client to initialize the schema with.
17
+ * @param schema The PostgreSQL schema name to initialize.
18
+ * @param prepare Whether to use prepared statements for schema queries.
19
+ * @since 0.4.0
20
+ */
21
+ async function initializePostgresRepositorySchema(sql, schema = "botkit", prepare = true) {
22
+ const validatedSchema = validateSchemaName(schema);
23
+ await execute(sql, `CREATE SCHEMA IF NOT EXISTS "${validatedSchema}"`, [], prepare);
24
+ await execute(sql, `CREATE TABLE IF NOT EXISTS "${validatedSchema}"."key_pairs" (
25
+ position INTEGER PRIMARY KEY,
26
+ private_key_jwk JSONB NOT NULL,
27
+ public_key_jwk JSONB NOT NULL
28
+ )`, [], prepare);
29
+ await execute(sql, `CREATE TABLE IF NOT EXISTS "${validatedSchema}"."messages" (
30
+ id TEXT PRIMARY KEY,
31
+ activity_json JSONB NOT NULL,
32
+ published BIGINT
33
+ )`, [], prepare);
34
+ await execute(sql, `CREATE INDEX IF NOT EXISTS "idx_messages_published"
35
+ ON "${validatedSchema}"."messages" (published, id)`, [], prepare);
36
+ await execute(sql, `CREATE TABLE IF NOT EXISTS "${validatedSchema}"."followers" (
37
+ follower_id TEXT PRIMARY KEY,
38
+ actor_json JSONB NOT NULL
39
+ )`, [], prepare);
40
+ await execute(sql, `CREATE TABLE IF NOT EXISTS "${validatedSchema}"."follow_requests" (
41
+ follow_request_id TEXT PRIMARY KEY,
42
+ follower_id TEXT NOT NULL
43
+ REFERENCES "${validatedSchema}"."followers" (follower_id)
44
+ ON DELETE CASCADE
45
+ )`, [], prepare);
46
+ await execute(sql, `CREATE INDEX IF NOT EXISTS "idx_follow_requests_follower"
47
+ ON "${validatedSchema}"."follow_requests" (follower_id)`, [], prepare);
48
+ await execute(sql, `CREATE TABLE IF NOT EXISTS "${validatedSchema}"."sent_follows" (
49
+ id TEXT PRIMARY KEY,
50
+ follow_json JSONB NOT NULL
51
+ )`, [], prepare);
52
+ await execute(sql, `CREATE TABLE IF NOT EXISTS "${validatedSchema}"."followees" (
53
+ followee_id TEXT PRIMARY KEY,
54
+ follow_json JSONB NOT NULL
55
+ )`, [], prepare);
56
+ await execute(sql, `CREATE TABLE IF NOT EXISTS "${validatedSchema}"."poll_votes" (
57
+ message_id TEXT NOT NULL,
58
+ voter_id TEXT NOT NULL,
59
+ option TEXT NOT NULL,
60
+ PRIMARY KEY (message_id, voter_id, option)
61
+ )`, [], prepare);
62
+ await execute(sql, `CREATE INDEX IF NOT EXISTS "idx_poll_votes_message_option"
63
+ ON "${validatedSchema}"."poll_votes" (message_id, option)`, [], prepare);
64
+ }
65
+ /**
66
+ * A repository for storing bot data using PostgreSQL.
67
+ * @since 0.4.0
68
+ */
69
+ var PostgresRepository = class {
70
+ sql;
71
+ schema;
72
+ prepare;
73
+ ownsSql;
74
+ ready;
75
+ constructor(options) {
76
+ this.schema = validateSchemaName(options.schema ?? "botkit");
77
+ this.prepare = options.prepare ?? true;
78
+ if ("sql" in options) {
79
+ if (options.url != null || options.maxConnections != null) throw new TypeError("PostgresRepositoryOptions.sql cannot be combined with PostgresRepositoryOptions.url or PostgresRepositoryOptions.maxConnections.");
80
+ this.ownsSql = false;
81
+ this.sql = options.sql;
82
+ } else {
83
+ if (options.url == null) throw new TypeError("PostgresRepositoryOptions.url must be provided when PostgresRepositoryOptions.sql is absent.");
84
+ this.ownsSql = true;
85
+ const url = typeof options.url === "string" ? options.url : options.url.href;
86
+ this.sql = postgres(url, {
87
+ max: options.maxConnections,
88
+ onnotice: () => {},
89
+ prepare: this.prepare
90
+ });
91
+ }
92
+ const ready = initializePostgresRepositorySchema(this.sql, this.schema, this.prepare);
93
+ ready.catch(() => {});
94
+ this.ready = ready;
95
+ }
96
+ async [Symbol.asyncDispose]() {
97
+ await this.close();
98
+ }
99
+ /**
100
+ * Closes the underlying PostgreSQL connection pool if owned by the
101
+ * repository.
102
+ */
103
+ async close() {
104
+ try {
105
+ await this.ready;
106
+ } finally {
107
+ if (this.ownsSql) await this.sql.end({ timeout: 5 });
108
+ }
109
+ }
110
+ async setKeyPairs(keyPairs) {
111
+ await this.ensureReady();
112
+ await this.sql.begin(async (sql) => {
113
+ await this.query(sql, `DELETE FROM ${this.table("key_pairs")}`);
114
+ for (const [position, keyPair] of keyPairs.entries()) {
115
+ const privateJwk = await exportJwk(keyPair.privateKey);
116
+ const publicJwk = await exportJwk(keyPair.publicKey);
117
+ await this.query(sql, `INSERT INTO ${this.table("key_pairs")}
118
+ (position, private_key_jwk, public_key_jwk)
119
+ VALUES ($1, $2::jsonb, $3::jsonb)`, [
120
+ position,
121
+ serializeJson(privateJwk),
122
+ serializeJson(publicJwk)
123
+ ]);
124
+ }
125
+ });
126
+ }
127
+ async getKeyPairs() {
128
+ await this.ensureReady();
129
+ const rows = await this.query(this.sql, `SELECT private_key_jwk, public_key_jwk
130
+ FROM ${this.table("key_pairs")}
131
+ ORDER BY position ASC`);
132
+ if (rows.length < 1) return void 0;
133
+ const keyPairs = [];
134
+ for (const row of rows) {
135
+ const privateJwk = normalizeJsonObject(row.private_key_jwk);
136
+ const publicJwk = normalizeJsonObject(row.public_key_jwk);
137
+ if (privateJwk == null || publicJwk == null) throw new TypeError("A stored key pair is malformed.");
138
+ keyPairs.push({
139
+ privateKey: await importJwk(privateJwk, "private"),
140
+ publicKey: await importJwk(publicJwk, "public")
141
+ });
142
+ }
143
+ return keyPairs;
144
+ }
145
+ async addMessage(id, activity) {
146
+ await this.ensureReady();
147
+ await this.query(this.sql, `INSERT INTO ${this.table("messages")} (id, activity_json, published)
148
+ VALUES ($1, $2::jsonb, $3)`, [
149
+ id,
150
+ serializeJson(await activity.toJsonLd({ format: "compact" })),
151
+ activity.published?.epochMilliseconds ?? null
152
+ ]);
153
+ }
154
+ async updateMessage(id, updater) {
155
+ await this.ensureReady();
156
+ return await this.sql.begin(async (sql) => {
157
+ const rows = await this.query(sql, `SELECT activity_json
158
+ FROM ${this.table("messages")}
159
+ WHERE id = $1
160
+ FOR UPDATE`, [id]);
161
+ const row = rows[0];
162
+ if (row == null) return false;
163
+ const activity = await parseActivity(row.activity_json);
164
+ if (activity == null) return false;
165
+ const updated = await updater(activity);
166
+ if (updated == null) return false;
167
+ await this.query(sql, `UPDATE ${this.table("messages")}
168
+ SET activity_json = $1::jsonb,
169
+ published = $2
170
+ WHERE id = $3`, [
171
+ serializeJson(await updated.toJsonLd({ format: "compact" })),
172
+ updated.published?.epochMilliseconds ?? null,
173
+ id
174
+ ]);
175
+ return true;
176
+ });
177
+ }
178
+ async removeMessage(id) {
179
+ await this.ensureReady();
180
+ const rows = await this.query(this.sql, `DELETE FROM ${this.table("messages")}
181
+ WHERE id = $1
182
+ RETURNING activity_json`, [id]);
183
+ return await parseActivity(rows[0]?.activity_json);
184
+ }
185
+ async *getMessages(options = {}) {
186
+ await this.ensureReady();
187
+ const { order = "newest", since, until, limit } = options;
188
+ const parameters = [];
189
+ let query = `SELECT activity_json
190
+ FROM ${this.table("messages")}
191
+ WHERE TRUE`;
192
+ if (since != null) {
193
+ parameters.push(since.epochMilliseconds);
194
+ query += ` AND published >= $${parameters.length}`;
195
+ }
196
+ if (until != null) {
197
+ parameters.push(until.epochMilliseconds);
198
+ query += ` AND published <= $${parameters.length}`;
199
+ }
200
+ query += order === "oldest" ? " ORDER BY published ASC NULLS LAST, id ASC" : " ORDER BY published DESC NULLS LAST, id DESC";
201
+ if (limit != null) {
202
+ parameters.push(limit);
203
+ query += ` LIMIT $${parameters.length}`;
204
+ }
205
+ const rows = await this.query(this.sql, query, parameters);
206
+ for (const row of rows) {
207
+ const activity = await parseActivity(row.activity_json);
208
+ if (activity != null) yield activity;
209
+ }
210
+ }
211
+ async getMessage(id) {
212
+ await this.ensureReady();
213
+ const rows = await this.query(this.sql, `SELECT activity_json
214
+ FROM ${this.table("messages")}
215
+ WHERE id = $1`, [id]);
216
+ return await parseActivity(rows[0]?.activity_json);
217
+ }
218
+ async countMessages() {
219
+ await this.ensureReady();
220
+ const rows = await this.query(this.sql, `SELECT COUNT(*)::integer AS count
221
+ FROM ${this.table("messages")}`);
222
+ return rows[0]?.count ?? 0;
223
+ }
224
+ async addFollower(followId, follower) {
225
+ await this.ensureReady();
226
+ if (follower.id == null) throw new TypeError("The follower ID is missing.");
227
+ const followerId = follower.id;
228
+ const followerJson = await follower.toJsonLd({ format: "compact" });
229
+ await this.sql.begin(async (sql) => {
230
+ await this.lockFollowRequest(sql, followId);
231
+ const rows = await this.query(sql, `SELECT follower_id
232
+ FROM ${this.table("follow_requests")}
233
+ WHERE follow_request_id = $1
234
+ FOR UPDATE`, [followId.href]);
235
+ const previousFollowerId = rows[0]?.follower_id;
236
+ await this.lockFollowers(sql, [followerId.href, ...previousFollowerId == null ? [] : [previousFollowerId]]);
237
+ await this.query(sql, `INSERT INTO ${this.table("followers")} (follower_id, actor_json)
238
+ VALUES ($1, $2::jsonb)
239
+ ON CONFLICT (follower_id)
240
+ DO UPDATE SET actor_json = EXCLUDED.actor_json`, [followerId.href, serializeJson(followerJson)]);
241
+ await this.query(sql, `INSERT INTO ${this.table("follow_requests")} (follow_request_id, follower_id)
242
+ VALUES ($1, $2)
243
+ ON CONFLICT (follow_request_id)
244
+ DO UPDATE SET follower_id = EXCLUDED.follower_id`, [followId.href, followerId.href]);
245
+ if (previousFollowerId != null && previousFollowerId !== followerId.href) await this.cleanupFollower(sql, previousFollowerId);
246
+ });
247
+ }
248
+ async removeFollower(followId, followerId) {
249
+ await this.ensureReady();
250
+ return await this.sql.begin(async (sql) => {
251
+ await this.lockFollowRequest(sql, followId);
252
+ const rows = await this.query(sql, `SELECT f.actor_json
253
+ FROM ${this.table("follow_requests")} AS fr
254
+ JOIN ${this.table("followers")} AS f
255
+ ON f.follower_id = fr.follower_id
256
+ WHERE fr.follow_request_id = $1
257
+ AND fr.follower_id = $2
258
+ FOR UPDATE`, [followId.href, followerId.href]);
259
+ const row = rows[0];
260
+ if (row == null) return void 0;
261
+ await this.query(sql, `DELETE FROM ${this.table("follow_requests")}
262
+ WHERE follow_request_id = $1`, [followId.href]);
263
+ await this.cleanupFollower(sql, followerId.href);
264
+ return await parseActor(row.actor_json);
265
+ });
266
+ }
267
+ async hasFollower(followerId) {
268
+ await this.ensureReady();
269
+ const rows = await this.query(this.sql, `SELECT 1 AS exists
270
+ FROM ${this.table("followers")}
271
+ WHERE follower_id = $1`, [followerId.href]);
272
+ return rows.length > 0;
273
+ }
274
+ async *getFollowers(options = {}) {
275
+ await this.ensureReady();
276
+ const { offset = 0, limit } = options;
277
+ const parameters = [];
278
+ let query = `SELECT actor_json
279
+ FROM ${this.table("followers")}
280
+ ORDER BY follower_id ASC`;
281
+ if (limit != null) {
282
+ parameters.push(limit, offset);
283
+ query += ` LIMIT $${parameters.length - 1} OFFSET $${parameters.length}`;
284
+ } else if (offset > 0) {
285
+ parameters.push(offset);
286
+ query += ` OFFSET $${parameters.length}`;
287
+ }
288
+ const rows = await this.query(this.sql, query, parameters);
289
+ for (const row of rows) {
290
+ const actor = await parseActor(row.actor_json);
291
+ if (actor != null) yield actor;
292
+ }
293
+ }
294
+ async countFollowers() {
295
+ await this.ensureReady();
296
+ const rows = await this.query(this.sql, `SELECT COUNT(*)::integer AS count
297
+ FROM ${this.table("followers")}`);
298
+ return rows[0]?.count ?? 0;
299
+ }
300
+ async addSentFollow(id, follow) {
301
+ await this.ensureReady();
302
+ await this.query(this.sql, `INSERT INTO ${this.table("sent_follows")} (id, follow_json)
303
+ VALUES ($1, $2::jsonb)
304
+ ON CONFLICT (id)
305
+ DO UPDATE SET follow_json = EXCLUDED.follow_json`, [id, serializeJson(await follow.toJsonLd({ format: "compact" }))]);
306
+ }
307
+ async removeSentFollow(id) {
308
+ await this.ensureReady();
309
+ const rows = await this.query(this.sql, `DELETE FROM ${this.table("sent_follows")}
310
+ WHERE id = $1
311
+ RETURNING follow_json`, [id]);
312
+ return await parseFollow(rows[0]?.follow_json);
313
+ }
314
+ async getSentFollow(id) {
315
+ await this.ensureReady();
316
+ const rows = await this.query(this.sql, `SELECT follow_json
317
+ FROM ${this.table("sent_follows")}
318
+ WHERE id = $1`, [id]);
319
+ return await parseFollow(rows[0]?.follow_json);
320
+ }
321
+ async addFollowee(followeeId, follow) {
322
+ await this.ensureReady();
323
+ await this.query(this.sql, `INSERT INTO ${this.table("followees")} (followee_id, follow_json)
324
+ VALUES ($1, $2::jsonb)
325
+ ON CONFLICT (followee_id)
326
+ DO UPDATE SET follow_json = EXCLUDED.follow_json`, [followeeId.href, serializeJson(await follow.toJsonLd({ format: "compact" }))]);
327
+ }
328
+ async removeFollowee(followeeId) {
329
+ await this.ensureReady();
330
+ const rows = await this.query(this.sql, `DELETE FROM ${this.table("followees")}
331
+ WHERE followee_id = $1
332
+ RETURNING follow_json`, [followeeId.href]);
333
+ return await parseFollow(rows[0]?.follow_json);
334
+ }
335
+ async getFollowee(followeeId) {
336
+ await this.ensureReady();
337
+ const rows = await this.query(this.sql, `SELECT follow_json
338
+ FROM ${this.table("followees")}
339
+ WHERE followee_id = $1`, [followeeId.href]);
340
+ return await parseFollow(rows[0]?.follow_json);
341
+ }
342
+ async vote(messageId, voterId, option) {
343
+ await this.ensureReady();
344
+ await this.query(this.sql, `INSERT INTO ${this.table("poll_votes")} (message_id, voter_id, option)
345
+ VALUES ($1, $2, $3)
346
+ ON CONFLICT (message_id, voter_id, option)
347
+ DO NOTHING`, [
348
+ messageId,
349
+ voterId.href,
350
+ option
351
+ ]);
352
+ }
353
+ async countVoters(messageId) {
354
+ await this.ensureReady();
355
+ const rows = await this.query(this.sql, `SELECT COUNT(DISTINCT voter_id)::integer AS count
356
+ FROM ${this.table("poll_votes")}
357
+ WHERE message_id = $1`, [messageId]);
358
+ return rows[0]?.count ?? 0;
359
+ }
360
+ async countVotes(messageId) {
361
+ await this.ensureReady();
362
+ const rows = await this.query(this.sql, `SELECT option, COUNT(*)::integer AS count
363
+ FROM ${this.table("poll_votes")}
364
+ WHERE message_id = $1
365
+ GROUP BY option
366
+ ORDER BY option ASC`, [messageId]);
367
+ const result = {};
368
+ for (const row of rows) result[row.option] = row.count;
369
+ return result;
370
+ }
371
+ table(name) {
372
+ return `"${this.schema}"."${name}"`;
373
+ }
374
+ async lockFollowRequest(sql, followId) {
375
+ await this.query(sql, `SELECT pg_catalog.pg_advisory_xact_lock($1, pg_catalog.hashtext($2))`, [followRequestAdvisoryLockNamespace, `${this.schema}:${followId.href}`]);
376
+ }
377
+ async lockFollower(sql, followerId) {
378
+ await this.query(sql, `SELECT pg_catalog.pg_advisory_xact_lock($1, pg_catalog.hashtext($2))`, [followerAdvisoryLockNamespace, `${this.schema}:${followerId}`]);
379
+ }
380
+ async lockFollowers(sql, followerIds) {
381
+ const uniqueFollowerIds = [...new Set(followerIds)].sort();
382
+ for (const followerId of uniqueFollowerIds) await this.lockFollower(sql, followerId);
383
+ }
384
+ async cleanupFollower(sql, followerId) {
385
+ await this.lockFollower(sql, followerId);
386
+ await this.query(sql, `DELETE FROM ${this.table("followers")}
387
+ WHERE follower_id = $1
388
+ AND NOT EXISTS (
389
+ SELECT 1
390
+ FROM ${this.table("follow_requests")}
391
+ WHERE follower_id = $1
392
+ )`, [followerId]);
393
+ }
394
+ async ensureReady() {
395
+ await this.ready;
396
+ }
397
+ async query(sql, query, parameters = []) {
398
+ return await execute(sql, query, parameters, this.prepare);
399
+ }
400
+ };
401
+ function validateSchemaName(schema) {
402
+ if (!schemaNamePattern.test(schema)) throw new TypeError("The PostgreSQL schema name is invalid.");
403
+ return schema;
404
+ }
405
+ async function execute(sql, query, parameters = [], prepare = true) {
406
+ return await sql.unsafe(query, [...parameters], { prepare });
407
+ }
408
+ function serializeJson(value) {
409
+ return JSON.stringify(value);
410
+ }
411
+ function isJsonObject(value) {
412
+ return typeof value === "object" && value != null;
413
+ }
414
+ async function parseActivity(json) {
415
+ const normalized = normalizeJsonObject(json);
416
+ if (normalized == null) return void 0;
417
+ try {
418
+ const activity = await Activity.fromJsonLd(normalized);
419
+ if (activity instanceof Create || activity instanceof Announce) return activity;
420
+ } catch (error) {
421
+ logger.warn("Failed to parse message activity.", { error });
422
+ }
423
+ return void 0;
424
+ }
425
+ async function parseActor(json) {
426
+ const normalized = normalizeJsonObject(json);
427
+ if (normalized == null) return void 0;
428
+ try {
429
+ const actor = await Object$1.fromJsonLd(normalized);
430
+ if (isActor(actor)) return actor;
431
+ } catch (error) {
432
+ logger.warn("Failed to parse follower actor.", { error });
433
+ }
434
+ return void 0;
435
+ }
436
+ async function parseFollow(json) {
437
+ const normalized = normalizeJsonObject(json);
438
+ if (normalized == null) return void 0;
439
+ try {
440
+ return await Follow.fromJsonLd(normalized);
441
+ } catch (error) {
442
+ logger.warn("Failed to parse follow activity.", { error });
443
+ }
444
+ return void 0;
445
+ }
446
+ function normalizeJsonObject(value) {
447
+ if (isJsonObject(value)) return value;
448
+ if (typeof value !== "string") return void 0;
449
+ try {
450
+ const parsed = JSON.parse(value);
451
+ if (isJsonObject(parsed)) return parsed;
452
+ } catch {
453
+ return void 0;
454
+ }
455
+ return void 0;
456
+ }
457
+
458
+ //#endregion
459
+ export { PostgresRepository, initializePostgresRepositorySchema };
460
+ //# sourceMappingURL=mod.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mod.js","names":["sql: Queryable","options: PostgresRepositoryOptions","keyPairs: CryptoKeyPair[]","id: Uuid","activity: Create | Announce","updater: (\n existing: Create | Announce,\n ) => Create | Announce | undefined | Promise<Create | Announce | undefined>","options: RepositoryGetMessagesOptions","parameters: QueryParameter[]","followId: URL","follower: Actor","followerId: URL","options: RepositoryGetFollowersOptions","follow: Follow","followeeId: URL","messageId: Uuid","voterId: URL","option: string","result: Record<string, number>","name: string","followerId: string","followerIds: readonly string[]","query: string","parameters: readonly QueryParameter[]","schema: string","value: unknown","json: unknown","parsed: unknown"],"sources":["../src/mod.ts"],"sourcesContent":["// BotKit by Fedify: A framework for creating ActivityPub bots\n// Copyright (C) 2026 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 { Temporal, toTemporalInstant } from \"@js-temporal/polyfill\";\nimport {\n Activity,\n type Actor,\n Announce,\n Create,\n Follow,\n isActor,\n Object,\n} from \"@fedify/vocab\";\nimport { getLogger } from \"@logtape/logtape\";\nimport postgres from \"postgres\";\n\nif (!(\"Temporal\" in globalThis)) {\n Reflect.set(globalThis, \"Temporal\", Temporal);\n}\nif (Date.prototype.toTemporalInstant == null) {\n Reflect.set(Date.prototype, \"toTemporalInstant\", toTemporalInstant);\n}\n\nconst logger = getLogger([\"botkit\", \"postgres\"]);\nconst schemaNamePattern = /^[A-Za-z_][A-Za-z0-9_]*$/;\nconst followRequestAdvisoryLockNamespace = 0x4254;\nconst followerAdvisoryLockNamespace = 0x4246;\n\ntype Queryable = Pick<postgres.Sql, \"unsafe\">;\ntype QueryParameter = postgres.SerializableParameter;\n\n/**\n * Common options for creating a PostgreSQL repository.\n * @since 0.4.0\n */\ninterface PostgresRepositoryOptionsBase {\n /**\n * The PostgreSQL schema name to use.\n * @default `\"botkit\"`\n */\n readonly schema?: string;\n\n /**\n * Whether to use prepared statements for queries.\n * @default true\n */\n readonly prepare?: boolean;\n}\n\n/**\n * Options for creating a PostgreSQL repository from an injected client.\n * @since 0.4.0\n */\ninterface PostgresRepositoryOptionsWithClient\n extends PostgresRepositoryOptionsBase {\n /**\n * A pre-configured PostgreSQL client to use.\n */\n readonly sql: postgres.Sql;\n\n /**\n * Disallowed when `sql` is provided.\n */\n readonly url?: never;\n\n /**\n * Disallowed when `sql` is provided.\n */\n readonly maxConnections?: never;\n}\n\n/**\n * Options for creating a PostgreSQL repository from a connection string.\n * @since 0.4.0\n */\ninterface PostgresRepositoryOptionsWithUrl\n extends PostgresRepositoryOptionsBase {\n /**\n * A PostgreSQL connection string to connect with.\n */\n readonly url: string | URL;\n\n /**\n * Disallowed when `url` is provided.\n */\n readonly sql?: never;\n\n /**\n * The maximum number of connections for an owned pool.\n */\n readonly maxConnections?: number;\n}\n\n/**\n * Options for creating a PostgreSQL repository.\n * @since 0.4.0\n */\nexport type PostgresRepositoryOptions =\n | PostgresRepositoryOptionsWithClient\n | PostgresRepositoryOptionsWithUrl;\n\n/**\n * Initializes the PostgreSQL schema used by BotKit repositories.\n * @param sql The PostgreSQL client to initialize the schema with.\n * @param schema The PostgreSQL schema name to initialize.\n * @param prepare Whether to use prepared statements for schema queries.\n * @since 0.4.0\n */\nexport async function initializePostgresRepositorySchema(\n sql: Queryable,\n schema = \"botkit\",\n prepare = true,\n): Promise<void> {\n const validatedSchema = validateSchemaName(schema);\n await execute(\n sql,\n `CREATE SCHEMA IF NOT EXISTS \"${validatedSchema}\"`,\n [],\n prepare,\n );\n await execute(\n sql,\n `CREATE TABLE IF NOT EXISTS \"${validatedSchema}\".\"key_pairs\" (\n position INTEGER PRIMARY KEY,\n private_key_jwk JSONB NOT NULL,\n public_key_jwk JSONB NOT NULL\n )`,\n [],\n prepare,\n );\n await execute(\n sql,\n `CREATE TABLE IF NOT EXISTS \"${validatedSchema}\".\"messages\" (\n id TEXT PRIMARY KEY,\n activity_json JSONB NOT NULL,\n published BIGINT\n )`,\n [],\n prepare,\n );\n await execute(\n sql,\n `CREATE INDEX IF NOT EXISTS \"idx_messages_published\"\n ON \"${validatedSchema}\".\"messages\" (published, id)`,\n [],\n prepare,\n );\n await execute(\n sql,\n `CREATE TABLE IF NOT EXISTS \"${validatedSchema}\".\"followers\" (\n follower_id TEXT PRIMARY KEY,\n actor_json JSONB NOT NULL\n )`,\n [],\n prepare,\n );\n await execute(\n sql,\n `CREATE TABLE IF NOT EXISTS \"${validatedSchema}\".\"follow_requests\" (\n follow_request_id TEXT PRIMARY KEY,\n follower_id TEXT NOT NULL\n REFERENCES \"${validatedSchema}\".\"followers\" (follower_id)\n ON DELETE CASCADE\n )`,\n [],\n prepare,\n );\n await execute(\n sql,\n `CREATE INDEX IF NOT EXISTS \"idx_follow_requests_follower\"\n ON \"${validatedSchema}\".\"follow_requests\" (follower_id)`,\n [],\n prepare,\n );\n await execute(\n sql,\n `CREATE TABLE IF NOT EXISTS \"${validatedSchema}\".\"sent_follows\" (\n id TEXT PRIMARY KEY,\n follow_json JSONB NOT NULL\n )`,\n [],\n prepare,\n );\n await execute(\n sql,\n `CREATE TABLE IF NOT EXISTS \"${validatedSchema}\".\"followees\" (\n followee_id TEXT PRIMARY KEY,\n follow_json JSONB NOT NULL\n )`,\n [],\n prepare,\n );\n await execute(\n sql,\n `CREATE TABLE IF NOT EXISTS \"${validatedSchema}\".\"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 prepare,\n );\n await execute(\n sql,\n `CREATE INDEX IF NOT EXISTS \"idx_poll_votes_message_option\"\n ON \"${validatedSchema}\".\"poll_votes\" (message_id, option)`,\n [],\n prepare,\n );\n}\n\n/**\n * A repository for storing bot data using PostgreSQL.\n * @since 0.4.0\n */\nexport class PostgresRepository implements Repository, AsyncDisposable {\n readonly sql: postgres.Sql;\n readonly schema: string;\n readonly prepare: boolean;\n private readonly ownsSql: boolean;\n private readonly ready: Promise<void>;\n\n constructor(options: PostgresRepositoryOptions) {\n this.schema = validateSchemaName(options.schema ?? \"botkit\");\n this.prepare = options.prepare ?? true;\n if (\"sql\" in options) {\n if (options.url != null || options.maxConnections != null) {\n throw new TypeError(\n \"PostgresRepositoryOptions.sql cannot be combined with PostgresRepositoryOptions.url or PostgresRepositoryOptions.maxConnections.\",\n );\n }\n this.ownsSql = false;\n this.sql = options.sql;\n } else {\n if (options.url == null) {\n throw new TypeError(\n \"PostgresRepositoryOptions.url must be provided when PostgresRepositoryOptions.sql is absent.\",\n );\n }\n this.ownsSql = true;\n const url = typeof options.url === \"string\"\n ? options.url\n : options.url.href;\n this.sql = postgres(url, {\n max: options.maxConnections,\n onnotice: () => {},\n prepare: this.prepare,\n });\n }\n const ready = initializePostgresRepositorySchema(\n this.sql,\n this.schema,\n this.prepare,\n );\n // Avoid unhandled rejection warnings before a repository method awaits it.\n ready.catch(() => {});\n this.ready = ready;\n }\n\n async [Symbol.asyncDispose](): Promise<void> {\n await this.close();\n }\n\n /**\n * Closes the underlying PostgreSQL connection pool if owned by the\n * repository.\n */\n async close(): Promise<void> {\n try {\n await this.ready;\n } finally {\n if (this.ownsSql) {\n await this.sql.end({ timeout: 5 });\n }\n }\n }\n\n async setKeyPairs(keyPairs: CryptoKeyPair[]): Promise<void> {\n await this.ensureReady();\n await this.sql.begin(async (sql) => {\n await this.query(sql, `DELETE FROM ${this.table(\"key_pairs\")}`);\n for (const [position, keyPair] of keyPairs.entries()) {\n const privateJwk = await exportJwk(keyPair.privateKey);\n const publicJwk = await exportJwk(keyPair.publicKey);\n await this.query(\n sql,\n `INSERT INTO ${this.table(\"key_pairs\")}\n (position, private_key_jwk, public_key_jwk)\n VALUES ($1, $2::jsonb, $3::jsonb)`,\n [\n position,\n serializeJson(privateJwk),\n serializeJson(publicJwk),\n ],\n );\n }\n });\n }\n\n async getKeyPairs(): Promise<CryptoKeyPair[] | undefined> {\n await this.ensureReady();\n const rows = await this.query<{\n readonly private_key_jwk: unknown;\n readonly public_key_jwk: unknown;\n }>(\n this.sql,\n `SELECT private_key_jwk, public_key_jwk\n FROM ${this.table(\"key_pairs\")}\n ORDER BY position ASC`,\n );\n if (rows.length < 1) return undefined;\n const keyPairs: CryptoKeyPair[] = [];\n for (const row of rows) {\n const privateJwk = normalizeJsonObject(row.private_key_jwk);\n const publicJwk = normalizeJsonObject(row.public_key_jwk);\n if (privateJwk == null || publicJwk == null) {\n throw new TypeError(\"A stored key pair is malformed.\");\n }\n keyPairs.push({\n privateKey: await importJwk(privateJwk, \"private\"),\n publicKey: await importJwk(publicJwk, \"public\"),\n });\n }\n return keyPairs;\n }\n\n async addMessage(id: Uuid, activity: Create | Announce): Promise<void> {\n await this.ensureReady();\n await this.query(\n this.sql,\n `INSERT INTO ${this.table(\"messages\")} (id, activity_json, published)\n VALUES ($1, $2::jsonb, $3)`,\n [\n id,\n serializeJson(await activity.toJsonLd({ format: \"compact\" })),\n activity.published?.epochMilliseconds ?? null,\n ],\n );\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 await this.ensureReady();\n return await this.sql.begin(async (sql) => {\n const rows = await this.query<{ readonly activity_json: unknown }>(\n sql,\n `SELECT activity_json\n FROM ${this.table(\"messages\")}\n WHERE id = $1\n FOR UPDATE`,\n [id],\n );\n const row = rows[0];\n if (row == null) return false;\n const activity = await parseActivity(row.activity_json);\n if (activity == null) return false;\n const updated = await updater(activity);\n if (updated == null) return false;\n await this.query(\n sql,\n `UPDATE ${this.table(\"messages\")}\n SET activity_json = $1::jsonb,\n published = $2\n WHERE id = $3`,\n [\n serializeJson(await updated.toJsonLd({ format: \"compact\" })),\n updated.published?.epochMilliseconds ?? null,\n id,\n ],\n );\n return true;\n });\n }\n\n async removeMessage(id: Uuid): Promise<Create | Announce | undefined> {\n await this.ensureReady();\n const rows = await this.query<{ readonly activity_json: unknown }>(\n this.sql,\n `DELETE FROM ${this.table(\"messages\")}\n WHERE id = $1\n RETURNING activity_json`,\n [id],\n );\n return await parseActivity(rows[0]?.activity_json);\n }\n\n async *getMessages(\n options: RepositoryGetMessagesOptions = {},\n ): AsyncIterable<Create | Announce> {\n await this.ensureReady();\n const { order = \"newest\", since, until, limit } = options;\n const parameters: QueryParameter[] = [];\n let query = `SELECT activity_json\n FROM ${this.table(\"messages\")}\n WHERE TRUE`;\n if (since != null) {\n parameters.push(since.epochMilliseconds);\n query += ` AND published >= $${parameters.length}`;\n }\n if (until != null) {\n parameters.push(until.epochMilliseconds);\n query += ` AND published <= $${parameters.length}`;\n }\n query += order === \"oldest\"\n ? \" ORDER BY published ASC NULLS LAST, id ASC\"\n : \" ORDER BY published DESC NULLS LAST, id DESC\";\n if (limit != null) {\n parameters.push(limit);\n query += ` LIMIT $${parameters.length}`;\n }\n const rows = await this.query<{ readonly activity_json: unknown }>(\n this.sql,\n query,\n parameters,\n );\n for (const row of rows) {\n const activity = await parseActivity(row.activity_json);\n if (activity != null) yield activity;\n }\n }\n\n async getMessage(id: Uuid): Promise<Create | Announce | undefined> {\n await this.ensureReady();\n const rows = await this.query<{ readonly activity_json: unknown }>(\n this.sql,\n `SELECT activity_json\n FROM ${this.table(\"messages\")}\n WHERE id = $1`,\n [id],\n );\n return await parseActivity(rows[0]?.activity_json);\n }\n\n async countMessages(): Promise<number> {\n await this.ensureReady();\n const rows = await this.query<{ readonly count: number }>(\n this.sql,\n `SELECT COUNT(*)::integer AS count\n FROM ${this.table(\"messages\")}`,\n );\n return rows[0]?.count ?? 0;\n }\n\n async addFollower(followId: URL, follower: Actor): Promise<void> {\n await this.ensureReady();\n if (follower.id == null) {\n throw new TypeError(\"The follower ID is missing.\");\n }\n const followerId = follower.id;\n const followerJson = await follower.toJsonLd({ format: \"compact\" });\n await this.sql.begin(async (sql) => {\n await this.lockFollowRequest(sql, followId);\n const rows = await this.query<{ readonly follower_id: string }>(\n sql,\n `SELECT follower_id\n FROM ${this.table(\"follow_requests\")}\n WHERE follow_request_id = $1\n FOR UPDATE`,\n [followId.href],\n );\n const previousFollowerId = rows[0]?.follower_id;\n await this.lockFollowers(sql, [\n followerId.href,\n ...(previousFollowerId == null ? [] : [previousFollowerId]),\n ]);\n await this.query(\n sql,\n `INSERT INTO ${this.table(\"followers\")} (follower_id, actor_json)\n VALUES ($1, $2::jsonb)\n ON CONFLICT (follower_id)\n DO UPDATE SET actor_json = EXCLUDED.actor_json`,\n [followerId.href, serializeJson(followerJson)],\n );\n await this.query(\n sql,\n `INSERT INTO ${\n this.table(\"follow_requests\")\n } (follow_request_id, follower_id)\n VALUES ($1, $2)\n ON CONFLICT (follow_request_id)\n DO UPDATE SET follower_id = EXCLUDED.follower_id`,\n [followId.href, followerId.href],\n );\n if (\n previousFollowerId != null && previousFollowerId !== followerId.href\n ) {\n await this.cleanupFollower(sql, previousFollowerId);\n }\n });\n }\n\n async removeFollower(\n followId: URL,\n followerId: URL,\n ): Promise<Actor | undefined> {\n await this.ensureReady();\n return await this.sql.begin(async (sql) => {\n await this.lockFollowRequest(sql, followId);\n const rows = await this.query<{ readonly actor_json: unknown }>(\n sql,\n `SELECT f.actor_json\n FROM ${this.table(\"follow_requests\")} AS fr\n JOIN ${this.table(\"followers\")} AS f\n ON f.follower_id = fr.follower_id\n WHERE fr.follow_request_id = $1\n AND fr.follower_id = $2\n FOR UPDATE`,\n [followId.href, followerId.href],\n );\n const row = rows[0];\n if (row == null) return undefined;\n await this.query(\n sql,\n `DELETE FROM ${this.table(\"follow_requests\")}\n WHERE follow_request_id = $1`,\n [followId.href],\n );\n await this.cleanupFollower(sql, followerId.href);\n return await parseActor(row.actor_json);\n });\n }\n\n async hasFollower(followerId: URL): Promise<boolean> {\n await this.ensureReady();\n const rows = await this.query<{ readonly exists: number }>(\n this.sql,\n `SELECT 1 AS exists\n FROM ${this.table(\"followers\")}\n WHERE follower_id = $1`,\n [followerId.href],\n );\n return rows.length > 0;\n }\n\n async *getFollowers(\n options: RepositoryGetFollowersOptions = {},\n ): AsyncIterable<Actor> {\n await this.ensureReady();\n const { offset = 0, limit } = options;\n const parameters: QueryParameter[] = [];\n let query = `SELECT actor_json\n FROM ${this.table(\"followers\")}\n ORDER BY follower_id ASC`;\n if (limit != null) {\n parameters.push(limit, offset);\n query += ` LIMIT $${parameters.length - 1} OFFSET $${parameters.length}`;\n } else if (offset > 0) {\n parameters.push(offset);\n query += ` OFFSET $${parameters.length}`;\n }\n const rows = await this.query<{ readonly actor_json: unknown }>(\n this.sql,\n query,\n parameters,\n );\n for (const row of rows) {\n const actor = await parseActor(row.actor_json);\n if (actor != null) yield actor;\n }\n }\n\n async countFollowers(): Promise<number> {\n await this.ensureReady();\n const rows = await this.query<{ readonly count: number }>(\n this.sql,\n `SELECT COUNT(*)::integer AS count\n FROM ${this.table(\"followers\")}`,\n );\n return rows[0]?.count ?? 0;\n }\n\n async addSentFollow(id: Uuid, follow: Follow): Promise<void> {\n await this.ensureReady();\n await this.query(\n this.sql,\n `INSERT INTO ${this.table(\"sent_follows\")} (id, follow_json)\n VALUES ($1, $2::jsonb)\n ON CONFLICT (id)\n DO UPDATE SET follow_json = EXCLUDED.follow_json`,\n [id, serializeJson(await follow.toJsonLd({ format: \"compact\" }))],\n );\n }\n\n async removeSentFollow(id: Uuid): Promise<Follow | undefined> {\n await this.ensureReady();\n const rows = await this.query<{ readonly follow_json: unknown }>(\n this.sql,\n `DELETE FROM ${this.table(\"sent_follows\")}\n WHERE id = $1\n RETURNING follow_json`,\n [id],\n );\n return await parseFollow(rows[0]?.follow_json);\n }\n\n async getSentFollow(id: Uuid): Promise<Follow | undefined> {\n await this.ensureReady();\n const rows = await this.query<{ readonly follow_json: unknown }>(\n this.sql,\n `SELECT follow_json\n FROM ${this.table(\"sent_follows\")}\n WHERE id = $1`,\n [id],\n );\n return await parseFollow(rows[0]?.follow_json);\n }\n\n async addFollowee(followeeId: URL, follow: Follow): Promise<void> {\n await this.ensureReady();\n await this.query(\n this.sql,\n `INSERT INTO ${this.table(\"followees\")} (followee_id, follow_json)\n VALUES ($1, $2::jsonb)\n ON CONFLICT (followee_id)\n DO UPDATE SET follow_json = EXCLUDED.follow_json`,\n [\n followeeId.href,\n serializeJson(await follow.toJsonLd({ format: \"compact\" })),\n ],\n );\n }\n\n async removeFollowee(followeeId: URL): Promise<Follow | undefined> {\n await this.ensureReady();\n const rows = await this.query<{ readonly follow_json: unknown }>(\n this.sql,\n `DELETE FROM ${this.table(\"followees\")}\n WHERE followee_id = $1\n RETURNING follow_json`,\n [followeeId.href],\n );\n return await parseFollow(rows[0]?.follow_json);\n }\n\n async getFollowee(followeeId: URL): Promise<Follow | undefined> {\n await this.ensureReady();\n const rows = await this.query<{ readonly follow_json: unknown }>(\n this.sql,\n `SELECT follow_json\n FROM ${this.table(\"followees\")}\n WHERE followee_id = $1`,\n [followeeId.href],\n );\n return await parseFollow(rows[0]?.follow_json);\n }\n\n async vote(messageId: Uuid, voterId: URL, option: string): Promise<void> {\n await this.ensureReady();\n await this.query(\n this.sql,\n `INSERT INTO ${this.table(\"poll_votes\")} (message_id, voter_id, option)\n VALUES ($1, $2, $3)\n ON CONFLICT (message_id, voter_id, option)\n DO NOTHING`,\n [messageId, voterId.href, option],\n );\n }\n\n async countVoters(messageId: Uuid): Promise<number> {\n await this.ensureReady();\n const rows = await this.query<{ readonly count: number }>(\n this.sql,\n `SELECT COUNT(DISTINCT voter_id)::integer AS count\n FROM ${this.table(\"poll_votes\")}\n WHERE message_id = $1`,\n [messageId],\n );\n return rows[0]?.count ?? 0;\n }\n\n async countVotes(messageId: Uuid): Promise<Readonly<Record<string, number>>> {\n await this.ensureReady();\n const rows = await this.query<{\n readonly option: string;\n readonly count: number;\n }>(\n this.sql,\n `SELECT option, COUNT(*)::integer AS count\n FROM ${this.table(\"poll_votes\")}\n WHERE message_id = $1\n GROUP BY option\n ORDER BY option ASC`,\n [messageId],\n );\n const result: Record<string, number> = {};\n for (const row of rows) {\n result[row.option] = row.count;\n }\n return result;\n }\n\n private table(name: string): string {\n return `\"${this.schema}\".\"${name}\"`;\n }\n\n private async lockFollowRequest(\n sql: Queryable,\n followId: URL,\n ): Promise<void> {\n await this.query(\n sql,\n `SELECT pg_catalog.pg_advisory_xact_lock($1, pg_catalog.hashtext($2))`,\n [\n followRequestAdvisoryLockNamespace,\n `${this.schema}:${followId.href}`,\n ],\n );\n }\n\n private async lockFollower(\n sql: Queryable,\n followerId: string,\n ): Promise<void> {\n await this.query(\n sql,\n `SELECT pg_catalog.pg_advisory_xact_lock($1, pg_catalog.hashtext($2))`,\n [\n followerAdvisoryLockNamespace,\n `${this.schema}:${followerId}`,\n ],\n );\n }\n\n private async lockFollowers(\n sql: Queryable,\n followerIds: readonly string[],\n ): Promise<void> {\n const uniqueFollowerIds = [...new Set(followerIds)].sort();\n for (const followerId of uniqueFollowerIds) {\n await this.lockFollower(sql, followerId);\n }\n }\n\n private async cleanupFollower(\n sql: Queryable,\n followerId: string,\n ): Promise<void> {\n await this.lockFollower(sql, followerId);\n await this.query(\n sql,\n `DELETE FROM ${this.table(\"followers\")}\n WHERE follower_id = $1\n AND NOT EXISTS (\n SELECT 1\n FROM ${this.table(\"follow_requests\")}\n WHERE follower_id = $1\n )`,\n [followerId],\n );\n }\n\n private async ensureReady(): Promise<void> {\n await this.ready;\n }\n\n private async query<TRow extends object>(\n sql: Queryable,\n query: string,\n parameters: readonly QueryParameter[] = [],\n ): Promise<readonly TRow[]> {\n return await execute<TRow>(sql, query, parameters, this.prepare);\n }\n}\n\nfunction validateSchemaName(schema: string): string {\n if (!schemaNamePattern.test(schema)) {\n throw new TypeError(\"The PostgreSQL schema name is invalid.\");\n }\n return schema;\n}\n\nasync function execute<TRow extends object>(\n sql: Queryable,\n query: string,\n parameters: readonly QueryParameter[] = [],\n prepare = true,\n): Promise<readonly TRow[]> {\n return await sql.unsafe<TRow[]>(\n query,\n [...parameters],\n { prepare },\n );\n}\n\nfunction serializeJson(value: unknown): string {\n return JSON.stringify(value);\n}\n\nfunction isJsonObject(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value != null;\n}\n\nasync function parseActivity(\n json: unknown,\n): Promise<Create | Announce | undefined> {\n const normalized = normalizeJsonObject(json);\n if (normalized == null) return undefined;\n try {\n const activity = await Activity.fromJsonLd(normalized);\n if (activity instanceof Create || activity instanceof Announce) {\n return activity;\n }\n } catch (error) {\n logger.warn(\"Failed to parse message activity.\", { error });\n }\n return undefined;\n}\n\nasync function parseActor(json: unknown): Promise<Actor | undefined> {\n const normalized = normalizeJsonObject(json);\n if (normalized == null) return undefined;\n try {\n const actor = await Object.fromJsonLd(normalized);\n if (isActor(actor)) return actor;\n } catch (error) {\n logger.warn(\"Failed to parse follower actor.\", { error });\n }\n return undefined;\n}\n\nasync function parseFollow(json: unknown): Promise<Follow | undefined> {\n const normalized = normalizeJsonObject(json);\n if (normalized == null) return undefined;\n try {\n return await Follow.fromJsonLd(normalized);\n } catch (error) {\n logger.warn(\"Failed to parse follow activity.\", { error });\n }\n return undefined;\n}\n\nfunction normalizeJsonObject(\n value: unknown,\n): Record<string, unknown> | undefined {\n if (isJsonObject(value)) return value;\n if (typeof value !== \"string\") return undefined;\n try {\n const parsed: unknown = JSON.parse(value);\n if (isJsonObject(parsed)) return parsed;\n } catch {\n return undefined;\n }\n return undefined;\n}\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@fedify/botkit-postgres",
3
+ "version": "0.4.0-dev.0",
4
+ "description": "PostgreSQL-based repository for BotKit",
5
+ "license": "AGPL-3.0-only",
6
+ "author": {
7
+ "name": "Hong Minhee",
8
+ "email": "hong@minhee.org",
9
+ "url": "https://hongminhee.org/"
10
+ },
11
+ "homepage": "https://botkit.fedify.dev/",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/fedify-dev/botkit.git",
15
+ "directory": "packages/botkit-postgres"
16
+ },
17
+ "bugs": {
18
+ "url": "https://github.com/fedify-dev/botkit/issues"
19
+ },
20
+ "funding": [
21
+ "https://opencollective.com/fedify",
22
+ "https://github.com/sponsors/dahlia"
23
+ ],
24
+ "engines": {
25
+ "deno": ">=2.0.0",
26
+ "node": ">=22.0.0"
27
+ },
28
+ "type": "module",
29
+ "module": "./dist/mod.js",
30
+ "types": "./dist/mod.d.ts",
31
+ "exports": {
32
+ ".": {
33
+ "types": "./dist/mod.d.ts",
34
+ "import": "./dist/mod.js"
35
+ },
36
+ "./package.json": "./package.json"
37
+ },
38
+ "sideEffects": true,
39
+ "files": [
40
+ "dist",
41
+ "LICENSE",
42
+ "package.json",
43
+ "README.md"
44
+ ],
45
+ "peerDependencies": {
46
+ "@fedify/botkit": "^0.4.0"
47
+ },
48
+ "dependencies": {
49
+ "@fedify/fedify": "^2.1.2",
50
+ "@fedify/vocab": "^2.1.2",
51
+ "@js-temporal/polyfill": "^0.5.1",
52
+ "@logtape/logtape": "^1.3.5",
53
+ "postgres": "^3.4.8"
54
+ },
55
+ "devDependencies": {
56
+ "tsdown": "^0.12.8"
57
+ },
58
+ "scripts": {
59
+ "build": "tsdown",
60
+ "prepublish": "tsdown",
61
+ "test": "tsdown && cd src/ && node --test --experimental-transform-types"
62
+ }
63
+ }