@fedify/botkit-postgres 0.5.0-dev.210 → 0.5.0-dev.225

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.d.ts CHANGED
@@ -1,6 +1,6 @@
1
+ import { ActorScopedRepository, Repository, RepositoryGetFollowersOptions, RepositoryGetMessagesOptions, Uuid } from "@fedify/botkit/repository";
1
2
  import { Actor, Announce, Create, Follow } from "@fedify/vocab";
2
3
  import postgres from "postgres";
3
- import { Repository, RepositoryGetFollowersOptions, RepositoryGetMessagesOptions, Uuid } from "@fedify/botkit/repository";
4
4
 
5
5
  //#region src/mod.d.ts
6
6
  type Queryable = Pick<postgres.Sql, "unsafe">;
@@ -86,28 +86,44 @@ declare class PostgresRepository implements Repository, AsyncDisposable {
86
86
  * repository.
87
87
  */
88
88
  close(): Promise<void>;
89
- setKeyPairs(keyPairs: CryptoKeyPair[]): Promise<void>;
90
- getKeyPairs(): Promise<CryptoKeyPair[] | undefined>;
91
- addMessage(id: Uuid, activity: Create | Announce): Promise<void>;
92
- updateMessage(id: Uuid, updater: (existing: Create | Announce) => Create | Announce | undefined | Promise<Create | Announce | undefined>): Promise<boolean>;
93
- removeMessage(id: Uuid): Promise<Create | Announce | undefined>;
94
- getMessages(options?: RepositoryGetMessagesOptions): AsyncIterable<Create | Announce>;
95
- getMessage(id: Uuid): Promise<Create | Announce | undefined>;
96
- countMessages(): Promise<number>;
97
- addFollower(followId: URL, follower: Actor): Promise<void>;
98
- removeFollower(followId: URL, followerId: URL): Promise<Actor | undefined>;
99
- hasFollower(followerId: URL): Promise<boolean>;
100
- getFollowers(options?: RepositoryGetFollowersOptions): AsyncIterable<Actor>;
101
- countFollowers(): Promise<number>;
102
- addSentFollow(id: Uuid, follow: Follow): Promise<void>;
103
- removeSentFollow(id: Uuid): Promise<Follow | undefined>;
104
- getSentFollow(id: Uuid): Promise<Follow | undefined>;
105
- addFollowee(followeeId: URL, follow: Follow): Promise<void>;
106
- removeFollowee(followeeId: URL): Promise<Follow | undefined>;
107
- getFollowee(followeeId: URL): Promise<Follow | undefined>;
108
- vote(messageId: Uuid, voterId: URL, option: string): Promise<void>;
109
- countVoters(messageId: Uuid): Promise<number>;
110
- countVotes(messageId: Uuid): Promise<Readonly<Record<string, number>>>;
89
+ setKeyPairs(identifier: string, keyPairs: CryptoKeyPair[]): Promise<void>;
90
+ getKeyPairs(identifier: string): Promise<CryptoKeyPair[] | undefined>;
91
+ addMessage(identifier: string, id: Uuid, activity: Create | Announce): Promise<void>;
92
+ updateMessage(identifier: string, id: Uuid, updater: (existing: Create | Announce) => Create | Announce | undefined | Promise<Create | Announce | undefined>): Promise<boolean>;
93
+ removeMessage(identifier: string, id: Uuid): Promise<Create | Announce | undefined>;
94
+ getMessages(identifier: string, options?: RepositoryGetMessagesOptions): AsyncIterable<Create | Announce>;
95
+ getMessage(identifier: string, id: Uuid): Promise<Create | Announce | undefined>;
96
+ countMessages(identifier: string): Promise<number>;
97
+ addFollower(identifier: string, followId: URL, follower: Actor): Promise<void>;
98
+ removeFollower(identifier: string, followId: URL, followerId: URL): Promise<Actor | undefined>;
99
+ hasFollower(identifier: string, followerId: URL): Promise<boolean>;
100
+ getFollowers(identifier: string, options?: RepositoryGetFollowersOptions): AsyncIterable<Actor>;
101
+ countFollowers(identifier: string): Promise<number>;
102
+ addSentFollow(identifier: string, id: Uuid, follow: Follow): Promise<void>;
103
+ removeSentFollow(identifier: string, id: Uuid): Promise<Follow | undefined>;
104
+ getSentFollow(identifier: string, id: Uuid): Promise<Follow | undefined>;
105
+ addFollowee(identifier: string, followeeId: URL, follow: Follow): Promise<void>;
106
+ removeFollowee(identifier: string, followeeId: URL): Promise<Follow | undefined>;
107
+ getFollowee(identifier: string, followeeId: URL): Promise<Follow | undefined>;
108
+ findFollowedBots(followeeId: URL): AsyncIterable<string>;
109
+ vote(identifier: string, messageId: Uuid, voterId: URL, option: string): Promise<void>;
110
+ countVoters(identifier: string, messageId: Uuid): Promise<number>;
111
+ countVotes(identifier: string, messageId: Uuid): Promise<Readonly<Record<string, number>>>;
112
+ /**
113
+ * Migrates data stored by \@fedify/botkit-postgres 0.4, which was not
114
+ * scoped by bot actor identifiers, so that it belongs to the given
115
+ * identifier. Rows carried over from a legacy schema have the
116
+ * empty-string bot ID; this method assigns them to the identifier in
117
+ * a single transaction. It only acts when the schema was actually
118
+ * upgraded from a legacy layout, so data legitimately stored under an
119
+ * empty-string identifier is never touched, and calling it again is
120
+ * a no-op.
121
+ * @param identifier The identifier of the bot actor that adopts the legacy
122
+ * data.
123
+ * @since 0.5.0
124
+ */
125
+ migrate(identifier: string): Promise<void>;
126
+ forIdentifier(identifier: string): ActorScopedRepository;
111
127
  private table;
112
128
  private lockFollowRequest;
113
129
  private lockFollower;
package/dist/mod.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"mod.d.ts","names":[],"sources":["../src/mod.ts"],"sourcesContent":[],"mappings":"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"}
1
+ {"version":3,"file":"mod.d.ts","names":[],"sources":["../src/mod.ts"],"sourcesContent":[],"mappings":"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"}
package/dist/mod.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { ActorScopedRepository } from "@fedify/botkit/repository";
1
2
  import { exportJwk, importJwk } from "@fedify/fedify/sig";
2
3
  import { Temporal, toTemporalInstant } from "@js-temporal/polyfill";
3
4
  import { Activity, Announce, Create, Follow, Object as Object$1, isActor } from "@fedify/vocab";
@@ -11,6 +12,7 @@ const logger = getLogger(["botkit", "postgres"]);
11
12
  const schemaNamePattern = /^[A-Za-z_][A-Za-z0-9_]*$/;
12
13
  const followRequestAdvisoryLockNamespace = 16980;
13
14
  const followerAdvisoryLockNamespace = 16966;
15
+ const schemaUpgradeAdvisoryLockNamespace = 16971;
14
16
  /**
15
17
  * Initializes the PostgreSQL schema used by BotKit repositories.
16
18
  * @param sql The PostgreSQL client to initialize the schema with.
@@ -21,46 +23,229 @@ const followerAdvisoryLockNamespace = 16966;
21
23
  async function initializePostgresRepositorySchema(sql, schema = "botkit", prepare = true) {
22
24
  const validatedSchema = validateSchemaName(schema);
23
25
  await execute(sql, `CREATE SCHEMA IF NOT EXISTS "${validatedSchema}"`, [], prepare);
26
+ await upgradeLegacySchema(sql, validatedSchema, prepare);
27
+ await execute(sql, `CREATE TABLE IF NOT EXISTS "${validatedSchema}"."botkit_metadata" (
28
+ "key" TEXT PRIMARY KEY,
29
+ value TEXT NOT NULL
30
+ )`, [], prepare);
24
31
  await execute(sql, `CREATE TABLE IF NOT EXISTS "${validatedSchema}"."key_pairs" (
25
- position INTEGER PRIMARY KEY,
32
+ bot_id TEXT NOT NULL,
33
+ position INTEGER NOT NULL,
26
34
  private_key_jwk JSONB NOT NULL,
27
- public_key_jwk JSONB NOT NULL
35
+ public_key_jwk JSONB NOT NULL,
36
+ PRIMARY KEY (bot_id, position)
28
37
  )`, [], prepare);
29
38
  await execute(sql, `CREATE TABLE IF NOT EXISTS "${validatedSchema}"."messages" (
30
- id TEXT PRIMARY KEY,
39
+ bot_id TEXT NOT NULL,
40
+ id TEXT NOT NULL,
31
41
  activity_json JSONB NOT NULL,
32
- published BIGINT
42
+ published BIGINT,
43
+ PRIMARY KEY (bot_id, id)
33
44
  )`, [], prepare);
34
45
  await execute(sql, `CREATE INDEX IF NOT EXISTS "idx_messages_published"
35
- ON "${validatedSchema}"."messages" (published, id)`, [], prepare);
46
+ ON "${validatedSchema}"."messages" (bot_id, published, id)`, [], prepare);
36
47
  await execute(sql, `CREATE TABLE IF NOT EXISTS "${validatedSchema}"."followers" (
37
- follower_id TEXT PRIMARY KEY,
38
- actor_json JSONB NOT NULL
48
+ bot_id TEXT NOT NULL,
49
+ follower_id TEXT NOT NULL,
50
+ actor_json JSONB NOT NULL,
51
+ PRIMARY KEY (bot_id, follower_id)
39
52
  )`, [], prepare);
40
53
  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)
54
+ bot_id TEXT NOT NULL,
55
+ follow_request_id TEXT NOT NULL,
56
+ follower_id TEXT NOT NULL,
57
+ PRIMARY KEY (bot_id, follow_request_id),
58
+ FOREIGN KEY (bot_id, follower_id)
59
+ REFERENCES "${validatedSchema}"."followers" (bot_id, follower_id)
44
60
  ON DELETE CASCADE
61
+ DEFERRABLE INITIALLY IMMEDIATE
45
62
  )`, [], prepare);
46
63
  await execute(sql, `CREATE INDEX IF NOT EXISTS "idx_follow_requests_follower"
47
- ON "${validatedSchema}"."follow_requests" (follower_id)`, [], prepare);
64
+ ON "${validatedSchema}"."follow_requests" (bot_id, follower_id)`, [], prepare);
48
65
  await execute(sql, `CREATE TABLE IF NOT EXISTS "${validatedSchema}"."sent_follows" (
49
- id TEXT PRIMARY KEY,
50
- follow_json JSONB NOT NULL
66
+ bot_id TEXT NOT NULL,
67
+ id TEXT NOT NULL,
68
+ follow_json JSONB NOT NULL,
69
+ PRIMARY KEY (bot_id, id)
51
70
  )`, [], prepare);
52
71
  await execute(sql, `CREATE TABLE IF NOT EXISTS "${validatedSchema}"."followees" (
53
- followee_id TEXT PRIMARY KEY,
54
- follow_json JSONB NOT NULL
72
+ bot_id TEXT NOT NULL,
73
+ followee_id TEXT NOT NULL,
74
+ follow_json JSONB NOT NULL,
75
+ PRIMARY KEY (bot_id, followee_id)
55
76
  )`, [], prepare);
77
+ await execute(sql, `CREATE INDEX IF NOT EXISTS "idx_followees_followee_id"
78
+ ON "${validatedSchema}"."followees" (followee_id)`, [], prepare);
56
79
  await execute(sql, `CREATE TABLE IF NOT EXISTS "${validatedSchema}"."poll_votes" (
80
+ bot_id TEXT NOT NULL,
57
81
  message_id TEXT NOT NULL,
58
82
  voter_id TEXT NOT NULL,
59
83
  option TEXT NOT NULL,
60
- PRIMARY KEY (message_id, voter_id, option)
84
+ PRIMARY KEY (bot_id, message_id, voter_id, option)
61
85
  )`, [], prepare);
62
86
  await execute(sql, `CREATE INDEX IF NOT EXISTS "idx_poll_votes_message_option"
63
- ON "${validatedSchema}"."poll_votes" (message_id, option)`, [], prepare);
87
+ ON "${validatedSchema}"."poll_votes" (bot_id, message_id, option)`, [], prepare);
88
+ }
89
+ const upgradableTables = [
90
+ "key_pairs",
91
+ "messages",
92
+ "followers",
93
+ "follow_requests",
94
+ "sent_follows",
95
+ "followees",
96
+ "poll_votes"
97
+ ];
98
+ /**
99
+ * Upgrades tables created by \@fedify/botkit-postgres 0.4, which had no
100
+ * `bot_id` column, into the bot-scoped schema. Existing rows get the
101
+ * empty-string bot ID; use {@link PostgresRepository.migrate} to assign them
102
+ * to a bot actor identifier.
103
+ *
104
+ * The whole upgrade is sent as a single multi-statement query without
105
+ * parameters, which PostgreSQL executes over the simple query protocol in
106
+ * one implicit transaction on one connection, so it is atomic even when
107
+ * `sql` is a connection pool.
108
+ */
109
+ async function upgradeLegacySchema(sql, schema, prepare) {
110
+ const rows = await execute(sql, `SELECT t.table_name
111
+ FROM information_schema.tables t
112
+ WHERE t.table_schema = $1
113
+ AND t.table_name = ANY($2)
114
+ AND NOT EXISTS (
115
+ SELECT 1
116
+ FROM information_schema.columns c
117
+ WHERE c.table_schema = t.table_schema
118
+ AND c.table_name = t.table_name
119
+ AND c.column_name = 'bot_id'
120
+ )`, [schema, [...upgradableTables]], prepare);
121
+ if (rows.length < 1) return;
122
+ const tables = rows.map((row) => row.table_name);
123
+ logger.info("Upgrading legacy tables without a bot_id column: {tables}.", { tables });
124
+ const legacyTable = (table) => `EXISTS (SELECT 1
125
+ FROM information_schema.tables t
126
+ WHERE t.table_schema = '${schema}' AND t.table_name = '${table}')
127
+ AND NOT EXISTS (SELECT 1
128
+ FROM information_schema.columns c
129
+ WHERE c.table_schema = '${schema}' AND c.table_name = '${table}'
130
+ AND c.column_name = 'bot_id')`;
131
+ const block = `
132
+ DO $botkit_upgrade$
133
+ DECLARE
134
+ upgraded boolean := false;
135
+ BEGIN
136
+ PERFORM pg_catalog.pg_advisory_xact_lock(
137
+ ${schemaUpgradeAdvisoryLockNamespace},
138
+ pg_catalog.hashtext('${schema}')
139
+ );
140
+
141
+ IF ${legacyTable("follow_requests")} THEN
142
+ -- The old foreign key referenced followers (follower_id) only; it
143
+ -- has to go away before the followers primary key changes:
144
+ ALTER TABLE "${schema}"."follow_requests"
145
+ DROP CONSTRAINT IF EXISTS "follow_requests_follower_id_fkey";
146
+ END IF;
147
+
148
+ IF ${legacyTable("key_pairs")} THEN
149
+ ALTER TABLE "${schema}"."key_pairs"
150
+ ADD COLUMN bot_id TEXT NOT NULL DEFAULT '';
151
+ ALTER TABLE "${schema}"."key_pairs" ALTER COLUMN bot_id DROP DEFAULT;
152
+ ALTER TABLE "${schema}"."key_pairs"
153
+ DROP CONSTRAINT IF EXISTS "key_pairs_pkey";
154
+ ALTER TABLE "${schema}"."key_pairs" ADD PRIMARY KEY (bot_id, position);
155
+ upgraded := true;
156
+ END IF;
157
+
158
+ IF ${legacyTable("messages")} THEN
159
+ ALTER TABLE "${schema}"."messages"
160
+ ADD COLUMN bot_id TEXT NOT NULL DEFAULT '';
161
+ ALTER TABLE "${schema}"."messages" ALTER COLUMN bot_id DROP DEFAULT;
162
+ ALTER TABLE "${schema}"."messages"
163
+ DROP CONSTRAINT IF EXISTS "messages_pkey";
164
+ ALTER TABLE "${schema}"."messages" ADD PRIMARY KEY (bot_id, id);
165
+ DROP INDEX IF EXISTS "${schema}"."idx_messages_published";
166
+ upgraded := true;
167
+ END IF;
168
+
169
+ IF ${legacyTable("followers")} THEN
170
+ ALTER TABLE "${schema}"."followers"
171
+ ADD COLUMN bot_id TEXT NOT NULL DEFAULT '';
172
+ ALTER TABLE "${schema}"."followers" ALTER COLUMN bot_id DROP DEFAULT;
173
+ ALTER TABLE "${schema}"."followers"
174
+ DROP CONSTRAINT IF EXISTS "followers_pkey";
175
+ ALTER TABLE "${schema}"."followers"
176
+ ADD PRIMARY KEY (bot_id, follower_id);
177
+ upgraded := true;
178
+ END IF;
179
+
180
+ IF ${legacyTable("follow_requests")} THEN
181
+ ALTER TABLE "${schema}"."follow_requests"
182
+ ADD COLUMN bot_id TEXT NOT NULL DEFAULT '';
183
+ ALTER TABLE "${schema}"."follow_requests"
184
+ ALTER COLUMN bot_id DROP DEFAULT;
185
+ ALTER TABLE "${schema}"."follow_requests"
186
+ DROP CONSTRAINT IF EXISTS "follow_requests_pkey";
187
+ ALTER TABLE "${schema}"."follow_requests"
188
+ ADD PRIMARY KEY (bot_id, follow_request_id);
189
+ ALTER TABLE "${schema}"."follow_requests"
190
+ ADD FOREIGN KEY (bot_id, follower_id)
191
+ REFERENCES "${schema}"."followers" (bot_id, follower_id)
192
+ ON DELETE CASCADE
193
+ DEFERRABLE INITIALLY IMMEDIATE;
194
+ DROP INDEX IF EXISTS "${schema}"."idx_follow_requests_follower";
195
+ upgraded := true;
196
+ END IF;
197
+
198
+ IF ${legacyTable("sent_follows")} THEN
199
+ ALTER TABLE "${schema}"."sent_follows"
200
+ ADD COLUMN bot_id TEXT NOT NULL DEFAULT '';
201
+ ALTER TABLE "${schema}"."sent_follows"
202
+ ALTER COLUMN bot_id DROP DEFAULT;
203
+ ALTER TABLE "${schema}"."sent_follows"
204
+ DROP CONSTRAINT IF EXISTS "sent_follows_pkey";
205
+ ALTER TABLE "${schema}"."sent_follows" ADD PRIMARY KEY (bot_id, id);
206
+ upgraded := true;
207
+ END IF;
208
+
209
+ IF ${legacyTable("followees")} THEN
210
+ ALTER TABLE "${schema}"."followees"
211
+ ADD COLUMN bot_id TEXT NOT NULL DEFAULT '';
212
+ ALTER TABLE "${schema}"."followees" ALTER COLUMN bot_id DROP DEFAULT;
213
+ ALTER TABLE "${schema}"."followees"
214
+ DROP CONSTRAINT IF EXISTS "followees_pkey";
215
+ ALTER TABLE "${schema}"."followees"
216
+ ADD PRIMARY KEY (bot_id, followee_id);
217
+ upgraded := true;
218
+ END IF;
219
+
220
+ IF ${legacyTable("poll_votes")} THEN
221
+ ALTER TABLE "${schema}"."poll_votes"
222
+ ADD COLUMN bot_id TEXT NOT NULL DEFAULT '';
223
+ ALTER TABLE "${schema}"."poll_votes" ALTER COLUMN bot_id DROP DEFAULT;
224
+ ALTER TABLE "${schema}"."poll_votes"
225
+ DROP CONSTRAINT IF EXISTS "poll_votes_pkey";
226
+ ALTER TABLE "${schema}"."poll_votes"
227
+ ADD PRIMARY KEY (bot_id, message_id, voter_id, option);
228
+ DROP INDEX IF EXISTS "${schema}"."idx_poll_votes_message_option";
229
+ upgraded := true;
230
+ END IF;
231
+
232
+ IF upgraded THEN
233
+ -- The marker lets migrate() distinguish rows carried over from
234
+ -- a legacy schema (bot_id = '') from data legitimately stored under
235
+ -- an empty-string identifier:
236
+ CREATE TABLE IF NOT EXISTS "${schema}"."botkit_metadata" (
237
+ "key" TEXT PRIMARY KEY,
238
+ value TEXT NOT NULL
239
+ );
240
+ INSERT INTO "${schema}"."botkit_metadata" ("key", value)
241
+ VALUES ('legacy_data', '1')
242
+ ON CONFLICT ("key") DO NOTHING;
243
+ END IF;
244
+ END
245
+ $botkit_upgrade$
246
+ `;
247
+ await execute(sql, block, [], false);
248
+ logger.info("Finished upgrading legacy tables.");
64
249
  }
65
250
  /**
66
251
  * A repository for storing bot data using PostgreSQL.
@@ -107,16 +292,17 @@ var PostgresRepository = class {
107
292
  if (this.ownsSql) await this.sql.end({ timeout: 5 });
108
293
  }
109
294
  }
110
- async setKeyPairs(keyPairs) {
295
+ async setKeyPairs(identifier, keyPairs) {
111
296
  await this.ensureReady();
112
297
  await this.sql.begin(async (sql) => {
113
- await this.query(sql, `DELETE FROM ${this.table("key_pairs")}`);
298
+ await this.query(sql, `DELETE FROM ${this.table("key_pairs")} WHERE bot_id = $1`, [identifier]);
114
299
  for (const [position, keyPair] of keyPairs.entries()) {
115
300
  const privateJwk = await exportJwk(keyPair.privateKey);
116
301
  const publicJwk = await exportJwk(keyPair.publicKey);
117
302
  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)`, [
303
+ (bot_id, position, private_key_jwk, public_key_jwk)
304
+ VALUES ($1, $2, $3::jsonb, $4::jsonb)`, [
305
+ identifier,
120
306
  position,
121
307
  serializeJson(privateJwk),
122
308
  serializeJson(publicJwk)
@@ -124,11 +310,12 @@ var PostgresRepository = class {
124
310
  }
125
311
  });
126
312
  }
127
- async getKeyPairs() {
313
+ async getKeyPairs(identifier) {
128
314
  await this.ensureReady();
129
315
  const rows = await this.query(this.sql, `SELECT private_key_jwk, public_key_jwk
130
316
  FROM ${this.table("key_pairs")}
131
- ORDER BY position ASC`);
317
+ WHERE bot_id = $1
318
+ ORDER BY position ASC`, [identifier]);
132
319
  if (rows.length < 1) return void 0;
133
320
  const keyPairs = [];
134
321
  for (const row of rows) {
@@ -142,22 +329,24 @@ var PostgresRepository = class {
142
329
  }
143
330
  return keyPairs;
144
331
  }
145
- async addMessage(id, activity) {
332
+ async addMessage(identifier, id, activity) {
146
333
  await this.ensureReady();
147
- await this.query(this.sql, `INSERT INTO ${this.table("messages")} (id, activity_json, published)
148
- VALUES ($1, $2::jsonb, $3)`, [
334
+ await this.query(this.sql, `INSERT INTO ${this.table("messages")}
335
+ (bot_id, id, activity_json, published)
336
+ VALUES ($1, $2, $3::jsonb, $4)`, [
337
+ identifier,
149
338
  id,
150
339
  serializeJson(await activity.toJsonLd({ format: "compact" })),
151
340
  activity.published?.epochMilliseconds ?? null
152
341
  ]);
153
342
  }
154
- async updateMessage(id, updater) {
343
+ async updateMessage(identifier, id, updater) {
155
344
  await this.ensureReady();
156
345
  return await this.sql.begin(async (sql) => {
157
346
  const rows = await this.query(sql, `SELECT activity_json
158
347
  FROM ${this.table("messages")}
159
- WHERE id = $1
160
- FOR UPDATE`, [id]);
348
+ WHERE bot_id = $1 AND id = $2
349
+ FOR UPDATE`, [identifier, id]);
161
350
  const row = rows[0];
162
351
  if (row == null) return false;
163
352
  const activity = await parseActivity(row.activity_json);
@@ -167,28 +356,29 @@ var PostgresRepository = class {
167
356
  await this.query(sql, `UPDATE ${this.table("messages")}
168
357
  SET activity_json = $1::jsonb,
169
358
  published = $2
170
- WHERE id = $3`, [
359
+ WHERE bot_id = $3 AND id = $4`, [
171
360
  serializeJson(await updated.toJsonLd({ format: "compact" })),
172
361
  updated.published?.epochMilliseconds ?? null,
362
+ identifier,
173
363
  id
174
364
  ]);
175
365
  return true;
176
366
  });
177
367
  }
178
- async removeMessage(id) {
368
+ async removeMessage(identifier, id) {
179
369
  await this.ensureReady();
180
370
  const rows = await this.query(this.sql, `DELETE FROM ${this.table("messages")}
181
- WHERE id = $1
182
- RETURNING activity_json`, [id]);
371
+ WHERE bot_id = $1 AND id = $2
372
+ RETURNING activity_json`, [identifier, id]);
183
373
  return await parseActivity(rows[0]?.activity_json);
184
374
  }
185
- async *getMessages(options = {}) {
375
+ async *getMessages(identifier, options = {}) {
186
376
  await this.ensureReady();
187
377
  const { order = "newest", since, until, limit } = options;
188
- const parameters = [];
378
+ const parameters = [identifier];
189
379
  let query = `SELECT activity_json
190
380
  FROM ${this.table("messages")}
191
- WHERE TRUE`;
381
+ WHERE bot_id = $1`;
192
382
  if (since != null) {
193
383
  parameters.push(since.epochMilliseconds);
194
384
  query += ` AND published >= $${parameters.length}`;
@@ -208,75 +398,92 @@ var PostgresRepository = class {
208
398
  if (activity != null) yield activity;
209
399
  }
210
400
  }
211
- async getMessage(id) {
401
+ async getMessage(identifier, id) {
212
402
  await this.ensureReady();
213
403
  const rows = await this.query(this.sql, `SELECT activity_json
214
404
  FROM ${this.table("messages")}
215
- WHERE id = $1`, [id]);
405
+ WHERE bot_id = $1 AND id = $2`, [identifier, id]);
216
406
  return await parseActivity(rows[0]?.activity_json);
217
407
  }
218
- async countMessages() {
408
+ async countMessages(identifier) {
219
409
  await this.ensureReady();
220
410
  const rows = await this.query(this.sql, `SELECT COUNT(*)::integer AS count
221
- FROM ${this.table("messages")}`);
411
+ FROM ${this.table("messages")}
412
+ WHERE bot_id = $1`, [identifier]);
222
413
  return rows[0]?.count ?? 0;
223
414
  }
224
- async addFollower(followId, follower) {
415
+ async addFollower(identifier, followId, follower) {
225
416
  await this.ensureReady();
226
417
  if (follower.id == null) throw new TypeError("The follower ID is missing.");
227
418
  const followerId = follower.id;
228
419
  const followerJson = await follower.toJsonLd({ format: "compact" });
229
420
  await this.sql.begin(async (sql) => {
230
- await this.lockFollowRequest(sql, followId);
421
+ await this.lockFollowRequest(sql, identifier, followId);
231
422
  const rows = await this.query(sql, `SELECT follower_id
232
423
  FROM ${this.table("follow_requests")}
233
- WHERE follow_request_id = $1
234
- FOR UPDATE`, [followId.href]);
424
+ WHERE bot_id = $1 AND follow_request_id = $2
425
+ FOR UPDATE`, [identifier, followId.href]);
235
426
  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);
427
+ await this.lockFollowers(sql, identifier, [followerId.href, ...previousFollowerId == null ? [] : [previousFollowerId]]);
428
+ await this.query(sql, `INSERT INTO ${this.table("followers")}
429
+ (bot_id, follower_id, actor_json)
430
+ VALUES ($1, $2, $3::jsonb)
431
+ ON CONFLICT (bot_id, follower_id)
432
+ DO UPDATE SET actor_json = EXCLUDED.actor_json`, [
433
+ identifier,
434
+ followerId.href,
435
+ serializeJson(followerJson)
436
+ ]);
437
+ await this.query(sql, `INSERT INTO ${this.table("follow_requests")}
438
+ (bot_id, follow_request_id, follower_id)
439
+ VALUES ($1, $2, $3)
440
+ ON CONFLICT (bot_id, follow_request_id)
441
+ DO UPDATE SET follower_id = EXCLUDED.follower_id`, [
442
+ identifier,
443
+ followId.href,
444
+ followerId.href
445
+ ]);
446
+ if (previousFollowerId != null && previousFollowerId !== followerId.href) await this.cleanupFollower(sql, identifier, previousFollowerId);
246
447
  });
247
448
  }
248
- async removeFollower(followId, followerId) {
449
+ async removeFollower(identifier, followId, followerId) {
249
450
  await this.ensureReady();
250
451
  return await this.sql.begin(async (sql) => {
251
- await this.lockFollowRequest(sql, followId);
452
+ await this.lockFollowRequest(sql, identifier, followId);
252
453
  const rows = await this.query(sql, `SELECT f.actor_json
253
454
  FROM ${this.table("follow_requests")} AS fr
254
455
  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]);
456
+ ON f.bot_id = fr.bot_id AND f.follower_id = fr.follower_id
457
+ WHERE fr.bot_id = $1
458
+ AND fr.follow_request_id = $2
459
+ AND fr.follower_id = $3
460
+ FOR UPDATE`, [
461
+ identifier,
462
+ followId.href,
463
+ followerId.href
464
+ ]);
259
465
  const row = rows[0];
260
466
  if (row == null) return void 0;
261
467
  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);
468
+ WHERE bot_id = $1 AND follow_request_id = $2`, [identifier, followId.href]);
469
+ await this.cleanupFollower(sql, identifier, followerId.href);
264
470
  return await parseActor(row.actor_json);
265
471
  });
266
472
  }
267
- async hasFollower(followerId) {
473
+ async hasFollower(identifier, followerId) {
268
474
  await this.ensureReady();
269
475
  const rows = await this.query(this.sql, `SELECT 1 AS exists
270
476
  FROM ${this.table("followers")}
271
- WHERE follower_id = $1`, [followerId.href]);
477
+ WHERE bot_id = $1 AND follower_id = $2`, [identifier, followerId.href]);
272
478
  return rows.length > 0;
273
479
  }
274
- async *getFollowers(options = {}) {
480
+ async *getFollowers(identifier, options = {}) {
275
481
  await this.ensureReady();
276
482
  const { offset = 0, limit } = options;
277
- const parameters = [];
483
+ const parameters = [identifier];
278
484
  let query = `SELECT actor_json
279
485
  FROM ${this.table("followers")}
486
+ WHERE bot_id = $1
280
487
  ORDER BY follower_id ASC`;
281
488
  if (limit != null) {
282
489
  parameters.push(limit, offset);
@@ -291,105 +498,158 @@ var PostgresRepository = class {
291
498
  if (actor != null) yield actor;
292
499
  }
293
500
  }
294
- async countFollowers() {
501
+ async countFollowers(identifier) {
295
502
  await this.ensureReady();
296
503
  const rows = await this.query(this.sql, `SELECT COUNT(*)::integer AS count
297
- FROM ${this.table("followers")}`);
504
+ FROM ${this.table("followers")}
505
+ WHERE bot_id = $1`, [identifier]);
298
506
  return rows[0]?.count ?? 0;
299
507
  }
300
- async addSentFollow(id, follow) {
508
+ async addSentFollow(identifier, id, follow) {
301
509
  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" }))]);
510
+ await this.query(this.sql, `INSERT INTO ${this.table("sent_follows")} (bot_id, id, follow_json)
511
+ VALUES ($1, $2, $3::jsonb)
512
+ ON CONFLICT (bot_id, id)
513
+ DO UPDATE SET follow_json = EXCLUDED.follow_json`, [
514
+ identifier,
515
+ id,
516
+ serializeJson(await follow.toJsonLd({ format: "compact" }))
517
+ ]);
306
518
  }
307
- async removeSentFollow(id) {
519
+ async removeSentFollow(identifier, id) {
308
520
  await this.ensureReady();
309
521
  const rows = await this.query(this.sql, `DELETE FROM ${this.table("sent_follows")}
310
- WHERE id = $1
311
- RETURNING follow_json`, [id]);
522
+ WHERE bot_id = $1 AND id = $2
523
+ RETURNING follow_json`, [identifier, id]);
312
524
  return await parseFollow(rows[0]?.follow_json);
313
525
  }
314
- async getSentFollow(id) {
526
+ async getSentFollow(identifier, id) {
315
527
  await this.ensureReady();
316
528
  const rows = await this.query(this.sql, `SELECT follow_json
317
529
  FROM ${this.table("sent_follows")}
318
- WHERE id = $1`, [id]);
530
+ WHERE bot_id = $1 AND id = $2`, [identifier, id]);
319
531
  return await parseFollow(rows[0]?.follow_json);
320
532
  }
321
- async addFollowee(followeeId, follow) {
533
+ async addFollowee(identifier, followeeId, follow) {
322
534
  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" }))]);
535
+ await this.query(this.sql, `INSERT INTO ${this.table("followees")}
536
+ (bot_id, followee_id, follow_json)
537
+ VALUES ($1, $2, $3::jsonb)
538
+ ON CONFLICT (bot_id, followee_id)
539
+ DO UPDATE SET follow_json = EXCLUDED.follow_json`, [
540
+ identifier,
541
+ followeeId.href,
542
+ serializeJson(await follow.toJsonLd({ format: "compact" }))
543
+ ]);
327
544
  }
328
- async removeFollowee(followeeId) {
545
+ async removeFollowee(identifier, followeeId) {
329
546
  await this.ensureReady();
330
547
  const rows = await this.query(this.sql, `DELETE FROM ${this.table("followees")}
331
- WHERE followee_id = $1
332
- RETURNING follow_json`, [followeeId.href]);
548
+ WHERE bot_id = $1 AND followee_id = $2
549
+ RETURNING follow_json`, [identifier, followeeId.href]);
333
550
  return await parseFollow(rows[0]?.follow_json);
334
551
  }
335
- async getFollowee(followeeId) {
552
+ async getFollowee(identifier, followeeId) {
336
553
  await this.ensureReady();
337
554
  const rows = await this.query(this.sql, `SELECT follow_json
338
555
  FROM ${this.table("followees")}
339
- WHERE followee_id = $1`, [followeeId.href]);
556
+ WHERE bot_id = $1 AND followee_id = $2`, [identifier, followeeId.href]);
340
557
  return await parseFollow(rows[0]?.follow_json);
341
558
  }
342
- async vote(messageId, voterId, option) {
559
+ async *findFollowedBots(followeeId) {
560
+ await this.ensureReady();
561
+ const rows = await this.query(this.sql, `SELECT bot_id
562
+ FROM ${this.table("followees")}
563
+ WHERE followee_id = $1
564
+ ORDER BY bot_id ASC`, [followeeId.href]);
565
+ for (const row of rows) yield row.bot_id;
566
+ }
567
+ async vote(identifier, messageId, voterId, option) {
343
568
  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)
569
+ await this.query(this.sql, `INSERT INTO ${this.table("poll_votes")}
570
+ (bot_id, message_id, voter_id, option)
571
+ VALUES ($1, $2, $3, $4)
572
+ ON CONFLICT (bot_id, message_id, voter_id, option)
347
573
  DO NOTHING`, [
574
+ identifier,
348
575
  messageId,
349
576
  voterId.href,
350
577
  option
351
578
  ]);
352
579
  }
353
- async countVoters(messageId) {
580
+ async countVoters(identifier, messageId) {
354
581
  await this.ensureReady();
355
582
  const rows = await this.query(this.sql, `SELECT COUNT(DISTINCT voter_id)::integer AS count
356
583
  FROM ${this.table("poll_votes")}
357
- WHERE message_id = $1`, [messageId]);
584
+ WHERE bot_id = $1 AND message_id = $2`, [identifier, messageId]);
358
585
  return rows[0]?.count ?? 0;
359
586
  }
360
- async countVotes(messageId) {
587
+ async countVotes(identifier, messageId) {
361
588
  await this.ensureReady();
362
589
  const rows = await this.query(this.sql, `SELECT option, COUNT(*)::integer AS count
363
590
  FROM ${this.table("poll_votes")}
364
- WHERE message_id = $1
591
+ WHERE bot_id = $1 AND message_id = $2
365
592
  GROUP BY option
366
- ORDER BY option ASC`, [messageId]);
593
+ ORDER BY option ASC`, [identifier, messageId]);
367
594
  const result = {};
368
595
  for (const row of rows) result[row.option] = row.count;
369
596
  return result;
370
597
  }
598
+ /**
599
+ * Migrates data stored by \@fedify/botkit-postgres 0.4, which was not
600
+ * scoped by bot actor identifiers, so that it belongs to the given
601
+ * identifier. Rows carried over from a legacy schema have the
602
+ * empty-string bot ID; this method assigns them to the identifier in
603
+ * a single transaction. It only acts when the schema was actually
604
+ * upgraded from a legacy layout, so data legitimately stored under an
605
+ * empty-string identifier is never touched, and calling it again is
606
+ * a no-op.
607
+ * @param identifier The identifier of the bot actor that adopts the legacy
608
+ * data.
609
+ * @since 0.5.0
610
+ */
611
+ async migrate(identifier) {
612
+ await this.ensureReady();
613
+ await this.sql.begin(async (sql) => {
614
+ const rows = await this.query(sql, `SELECT value FROM ${this.table("botkit_metadata")}
615
+ WHERE "key" = 'legacy_data'
616
+ FOR UPDATE`);
617
+ if (rows.length < 1) return;
618
+ await execute(sql, "SET CONSTRAINTS ALL DEFERRED", [], false);
619
+ for (const table of upgradableTables) await this.query(sql, `UPDATE "${this.schema}"."${table}"
620
+ SET bot_id = $1
621
+ WHERE bot_id = ''`, [identifier]);
622
+ await this.query(sql, `DELETE FROM ${this.table("botkit_metadata")}
623
+ WHERE "key" = 'legacy_data'`);
624
+ });
625
+ }
626
+ forIdentifier(identifier) {
627
+ return new ActorScopedRepository(this, identifier);
628
+ }
371
629
  table(name) {
372
630
  return `"${this.schema}"."${name}"`;
373
631
  }
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}`]);
632
+ async lockFollowRequest(sql, identifier, followId) {
633
+ await this.query(sql, `SELECT pg_catalog.pg_advisory_xact_lock($1, pg_catalog.hashtext($2))`, [followRequestAdvisoryLockNamespace, `${this.schema}:${identifier}:${followId.href}`]);
376
634
  }
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}`]);
635
+ async lockFollower(sql, identifier, followerId) {
636
+ await this.query(sql, `SELECT pg_catalog.pg_advisory_xact_lock($1, pg_catalog.hashtext($2))`, [followerAdvisoryLockNamespace, `${this.schema}:${identifier}:${followerId}`]);
379
637
  }
380
- async lockFollowers(sql, followerIds) {
638
+ async lockFollowers(sql, identifier, followerIds) {
381
639
  const uniqueFollowerIds = [...new Set(followerIds)].sort();
382
- for (const followerId of uniqueFollowerIds) await this.lockFollower(sql, followerId);
640
+ for (const followerId of uniqueFollowerIds) await this.lockFollower(sql, identifier, followerId);
383
641
  }
384
- async cleanupFollower(sql, followerId) {
385
- await this.lockFollower(sql, followerId);
642
+ async cleanupFollower(sql, identifier, followerId) {
643
+ await this.lockFollower(sql, identifier, followerId);
386
644
  await this.query(sql, `DELETE FROM ${this.table("followers")}
387
- WHERE follower_id = $1
645
+ WHERE bot_id = $1
646
+ AND follower_id = $2
388
647
  AND NOT EXISTS (
389
648
  SELECT 1
390
649
  FROM ${this.table("follow_requests")}
391
- WHERE follower_id = $1
392
- )`, [followerId]);
650
+ WHERE bot_id = $1
651
+ AND follower_id = $2
652
+ )`, [identifier, followerId]);
393
653
  }
394
654
  async ensureReady() {
395
655
  await this.ready;
package/dist/mod.js.map CHANGED
@@ -1 +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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"}
1
+ {"version":3,"file":"mod.js","names":["sql: Queryable","schema: string","prepare: boolean","table: string","options: PostgresRepositoryOptions","identifier: string","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[]","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 {\n ActorScopedRepository,\n type Repository,\n type RepositoryGetFollowersOptions,\n type RepositoryGetMessagesOptions,\n type 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;\nconst schemaUpgradeAdvisoryLockNamespace = 0x424b;\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 upgradeLegacySchema(sql, validatedSchema, prepare);\n await execute(\n sql,\n `CREATE TABLE IF NOT EXISTS \"${validatedSchema}\".\"botkit_metadata\" (\n \"key\" TEXT PRIMARY KEY,\n value TEXT NOT NULL\n )`,\n [],\n prepare,\n );\n await execute(\n sql,\n `CREATE TABLE IF NOT EXISTS \"${validatedSchema}\".\"key_pairs\" (\n bot_id TEXT NOT NULL,\n position INTEGER NOT NULL,\n private_key_jwk JSONB NOT NULL,\n public_key_jwk JSONB NOT NULL,\n PRIMARY KEY (bot_id, position)\n )`,\n [],\n prepare,\n );\n await execute(\n sql,\n `CREATE TABLE IF NOT EXISTS \"${validatedSchema}\".\"messages\" (\n bot_id TEXT NOT NULL,\n id TEXT NOT NULL,\n activity_json JSONB NOT NULL,\n published BIGINT,\n PRIMARY KEY (bot_id, id)\n )`,\n [],\n prepare,\n );\n await execute(\n sql,\n `CREATE INDEX IF NOT EXISTS \"idx_messages_published\"\n ON \"${validatedSchema}\".\"messages\" (bot_id, published, id)`,\n [],\n prepare,\n );\n await execute(\n sql,\n `CREATE TABLE IF NOT EXISTS \"${validatedSchema}\".\"followers\" (\n bot_id TEXT NOT NULL,\n follower_id TEXT NOT NULL,\n actor_json JSONB NOT NULL,\n PRIMARY KEY (bot_id, follower_id)\n )`,\n [],\n prepare,\n );\n await execute(\n sql,\n `CREATE TABLE IF NOT EXISTS \"${validatedSchema}\".\"follow_requests\" (\n bot_id TEXT NOT NULL,\n follow_request_id TEXT NOT NULL,\n follower_id TEXT NOT NULL,\n PRIMARY KEY (bot_id, follow_request_id),\n FOREIGN KEY (bot_id, follower_id)\n REFERENCES \"${validatedSchema}\".\"followers\" (bot_id, follower_id)\n ON DELETE CASCADE\n DEFERRABLE INITIALLY IMMEDIATE\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\" (bot_id, follower_id)`,\n [],\n prepare,\n );\n await execute(\n sql,\n `CREATE TABLE IF NOT EXISTS \"${validatedSchema}\".\"sent_follows\" (\n bot_id TEXT NOT NULL,\n id TEXT NOT NULL,\n follow_json JSONB NOT NULL,\n PRIMARY KEY (bot_id, id)\n )`,\n [],\n prepare,\n );\n await execute(\n sql,\n `CREATE TABLE IF NOT EXISTS \"${validatedSchema}\".\"followees\" (\n bot_id TEXT NOT NULL,\n followee_id TEXT NOT NULL,\n follow_json JSONB NOT NULL,\n PRIMARY KEY (bot_id, followee_id)\n )`,\n [],\n prepare,\n );\n await execute(\n sql,\n `CREATE INDEX IF NOT EXISTS \"idx_followees_followee_id\"\n ON \"${validatedSchema}\".\"followees\" (followee_id)`,\n [],\n prepare,\n );\n await execute(\n sql,\n `CREATE TABLE IF NOT EXISTS \"${validatedSchema}\".\"poll_votes\" (\n bot_id TEXT NOT NULL,\n message_id TEXT NOT NULL,\n voter_id TEXT NOT NULL,\n option TEXT NOT NULL,\n PRIMARY KEY (bot_id, 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\" (bot_id, message_id, option)`,\n [],\n prepare,\n );\n}\n\nconst upgradableTables = [\n \"key_pairs\",\n \"messages\",\n \"followers\",\n \"follow_requests\",\n \"sent_follows\",\n \"followees\",\n \"poll_votes\",\n] as const;\n\n/**\n * Upgrades tables created by \\@fedify/botkit-postgres 0.4, which had no\n * `bot_id` column, into the bot-scoped schema. Existing rows get the\n * empty-string bot ID; use {@link PostgresRepository.migrate} to assign them\n * to a bot actor identifier.\n *\n * The whole upgrade is sent as a single multi-statement query without\n * parameters, which PostgreSQL executes over the simple query protocol in\n * one implicit transaction on one connection, so it is atomic even when\n * `sql` is a connection pool.\n */\nasync function upgradeLegacySchema(\n sql: Queryable,\n schema: string,\n prepare: boolean,\n): Promise<void> {\n const rows = await execute<{ readonly table_name: string }>(\n sql,\n `SELECT t.table_name\n FROM information_schema.tables t\n WHERE t.table_schema = $1\n AND t.table_name = ANY($2)\n AND NOT EXISTS (\n SELECT 1\n FROM information_schema.columns c\n WHERE c.table_schema = t.table_schema\n AND c.table_name = t.table_name\n AND c.column_name = 'bot_id'\n )`,\n [schema, [...upgradableTables]],\n prepare,\n );\n if (rows.length < 1) return;\n const tables = rows.map((row) => row.table_name);\n logger.info(\n \"Upgrading legacy tables without a bot_id column: {tables}.\",\n { tables },\n );\n // Multiple processes can start against the same legacy schema at once, so\n // the whole upgrade runs inside one PL/pgSQL block: an advisory lock\n // serializes it, and every table is re-checked under the lock, so the\n // process that lost the race finds nothing left to do. The detection\n // query above is merely a fast path for already-upgraded schemas.\n const legacyTable = (table: string) =>\n `EXISTS (SELECT 1\n FROM information_schema.tables t\n WHERE t.table_schema = '${schema}' AND t.table_name = '${table}')\n AND NOT EXISTS (SELECT 1\n FROM information_schema.columns c\n WHERE c.table_schema = '${schema}' AND c.table_name = '${table}'\n AND c.column_name = 'bot_id')`;\n const block = `\n DO $botkit_upgrade$\n DECLARE\n upgraded boolean := false;\n BEGIN\n PERFORM pg_catalog.pg_advisory_xact_lock(\n ${schemaUpgradeAdvisoryLockNamespace},\n pg_catalog.hashtext('${schema}')\n );\n\n IF ${legacyTable(\"follow_requests\")} THEN\n -- The old foreign key referenced followers (follower_id) only; it\n -- has to go away before the followers primary key changes:\n ALTER TABLE \"${schema}\".\"follow_requests\"\n DROP CONSTRAINT IF EXISTS \"follow_requests_follower_id_fkey\";\n END IF;\n\n IF ${legacyTable(\"key_pairs\")} THEN\n ALTER TABLE \"${schema}\".\"key_pairs\"\n ADD COLUMN bot_id TEXT NOT NULL DEFAULT '';\n ALTER TABLE \"${schema}\".\"key_pairs\" ALTER COLUMN bot_id DROP DEFAULT;\n ALTER TABLE \"${schema}\".\"key_pairs\"\n DROP CONSTRAINT IF EXISTS \"key_pairs_pkey\";\n ALTER TABLE \"${schema}\".\"key_pairs\" ADD PRIMARY KEY (bot_id, position);\n upgraded := true;\n END IF;\n\n IF ${legacyTable(\"messages\")} THEN\n ALTER TABLE \"${schema}\".\"messages\"\n ADD COLUMN bot_id TEXT NOT NULL DEFAULT '';\n ALTER TABLE \"${schema}\".\"messages\" ALTER COLUMN bot_id DROP DEFAULT;\n ALTER TABLE \"${schema}\".\"messages\"\n DROP CONSTRAINT IF EXISTS \"messages_pkey\";\n ALTER TABLE \"${schema}\".\"messages\" ADD PRIMARY KEY (bot_id, id);\n DROP INDEX IF EXISTS \"${schema}\".\"idx_messages_published\";\n upgraded := true;\n END IF;\n\n IF ${legacyTable(\"followers\")} THEN\n ALTER TABLE \"${schema}\".\"followers\"\n ADD COLUMN bot_id TEXT NOT NULL DEFAULT '';\n ALTER TABLE \"${schema}\".\"followers\" ALTER COLUMN bot_id DROP DEFAULT;\n ALTER TABLE \"${schema}\".\"followers\"\n DROP CONSTRAINT IF EXISTS \"followers_pkey\";\n ALTER TABLE \"${schema}\".\"followers\"\n ADD PRIMARY KEY (bot_id, follower_id);\n upgraded := true;\n END IF;\n\n IF ${legacyTable(\"follow_requests\")} THEN\n ALTER TABLE \"${schema}\".\"follow_requests\"\n ADD COLUMN bot_id TEXT NOT NULL DEFAULT '';\n ALTER TABLE \"${schema}\".\"follow_requests\"\n ALTER COLUMN bot_id DROP DEFAULT;\n ALTER TABLE \"${schema}\".\"follow_requests\"\n DROP CONSTRAINT IF EXISTS \"follow_requests_pkey\";\n ALTER TABLE \"${schema}\".\"follow_requests\"\n ADD PRIMARY KEY (bot_id, follow_request_id);\n ALTER TABLE \"${schema}\".\"follow_requests\"\n ADD FOREIGN KEY (bot_id, follower_id)\n REFERENCES \"${schema}\".\"followers\" (bot_id, follower_id)\n ON DELETE CASCADE\n DEFERRABLE INITIALLY IMMEDIATE;\n DROP INDEX IF EXISTS \"${schema}\".\"idx_follow_requests_follower\";\n upgraded := true;\n END IF;\n\n IF ${legacyTable(\"sent_follows\")} THEN\n ALTER TABLE \"${schema}\".\"sent_follows\"\n ADD COLUMN bot_id TEXT NOT NULL DEFAULT '';\n ALTER TABLE \"${schema}\".\"sent_follows\"\n ALTER COLUMN bot_id DROP DEFAULT;\n ALTER TABLE \"${schema}\".\"sent_follows\"\n DROP CONSTRAINT IF EXISTS \"sent_follows_pkey\";\n ALTER TABLE \"${schema}\".\"sent_follows\" ADD PRIMARY KEY (bot_id, id);\n upgraded := true;\n END IF;\n\n IF ${legacyTable(\"followees\")} THEN\n ALTER TABLE \"${schema}\".\"followees\"\n ADD COLUMN bot_id TEXT NOT NULL DEFAULT '';\n ALTER TABLE \"${schema}\".\"followees\" ALTER COLUMN bot_id DROP DEFAULT;\n ALTER TABLE \"${schema}\".\"followees\"\n DROP CONSTRAINT IF EXISTS \"followees_pkey\";\n ALTER TABLE \"${schema}\".\"followees\"\n ADD PRIMARY KEY (bot_id, followee_id);\n upgraded := true;\n END IF;\n\n IF ${legacyTable(\"poll_votes\")} THEN\n ALTER TABLE \"${schema}\".\"poll_votes\"\n ADD COLUMN bot_id TEXT NOT NULL DEFAULT '';\n ALTER TABLE \"${schema}\".\"poll_votes\" ALTER COLUMN bot_id DROP DEFAULT;\n ALTER TABLE \"${schema}\".\"poll_votes\"\n DROP CONSTRAINT IF EXISTS \"poll_votes_pkey\";\n ALTER TABLE \"${schema}\".\"poll_votes\"\n ADD PRIMARY KEY (bot_id, message_id, voter_id, option);\n DROP INDEX IF EXISTS \"${schema}\".\"idx_poll_votes_message_option\";\n upgraded := true;\n END IF;\n\n IF upgraded THEN\n -- The marker lets migrate() distinguish rows carried over from\n -- a legacy schema (bot_id = '') from data legitimately stored under\n -- an empty-string identifier:\n CREATE TABLE IF NOT EXISTS \"${schema}\".\"botkit_metadata\" (\n \"key\" TEXT PRIMARY KEY,\n value TEXT NOT NULL\n );\n INSERT INTO \"${schema}\".\"botkit_metadata\" (\"key\", value)\n VALUES ('legacy_data', '1')\n ON CONFLICT (\"key\") DO NOTHING;\n END IF;\n END\n $botkit_upgrade$\n `;\n // DO blocks cannot be prepared:\n await execute(sql, block, [], false);\n logger.info(\"Finished upgrading legacy tables.\");\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(\n identifier: string,\n keyPairs: CryptoKeyPair[],\n ): Promise<void> {\n await this.ensureReady();\n await this.sql.begin(async (sql) => {\n await this.query(\n sql,\n `DELETE FROM ${this.table(\"key_pairs\")} WHERE bot_id = $1`,\n [identifier],\n );\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 (bot_id, position, private_key_jwk, public_key_jwk)\n VALUES ($1, $2, $3::jsonb, $4::jsonb)`,\n [\n identifier,\n position,\n serializeJson(privateJwk),\n serializeJson(publicJwk),\n ],\n );\n }\n });\n }\n\n async getKeyPairs(identifier: string): 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 WHERE bot_id = $1\n ORDER BY position ASC`,\n [identifier],\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(\n identifier: string,\n id: Uuid,\n activity: Create | Announce,\n ): Promise<void> {\n await this.ensureReady();\n await this.query(\n this.sql,\n `INSERT INTO ${this.table(\"messages\")}\n (bot_id, id, activity_json, published)\n VALUES ($1, $2, $3::jsonb, $4)`,\n [\n identifier,\n id,\n serializeJson(await activity.toJsonLd({ format: \"compact\" })),\n activity.published?.epochMilliseconds ?? null,\n ],\n );\n }\n\n async updateMessage(\n identifier: string,\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 bot_id = $1 AND id = $2\n FOR UPDATE`,\n [identifier, 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 bot_id = $3 AND id = $4`,\n [\n serializeJson(await updated.toJsonLd({ format: \"compact\" })),\n updated.published?.epochMilliseconds ?? null,\n identifier,\n id,\n ],\n );\n return true;\n });\n }\n\n async removeMessage(\n identifier: string,\n id: Uuid,\n ): 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 bot_id = $1 AND id = $2\n RETURNING activity_json`,\n [identifier, id],\n );\n return await parseActivity(rows[0]?.activity_json);\n }\n\n async *getMessages(\n identifier: string,\n options: RepositoryGetMessagesOptions = {},\n ): AsyncIterable<Create | Announce> {\n await this.ensureReady();\n const { order = \"newest\", since, until, limit } = options;\n const parameters: QueryParameter[] = [identifier];\n let query = `SELECT activity_json\n FROM ${this.table(\"messages\")}\n WHERE bot_id = $1`;\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(\n identifier: string,\n id: Uuid,\n ): 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 bot_id = $1 AND id = $2`,\n [identifier, id],\n );\n return await parseActivity(rows[0]?.activity_json);\n }\n\n async countMessages(identifier: string): 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 WHERE bot_id = $1`,\n [identifier],\n );\n return rows[0]?.count ?? 0;\n }\n\n async addFollower(\n identifier: string,\n followId: URL,\n follower: Actor,\n ): 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, identifier, 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 bot_id = $1 AND follow_request_id = $2\n FOR UPDATE`,\n [identifier, followId.href],\n );\n const previousFollowerId = rows[0]?.follower_id;\n await this.lockFollowers(sql, identifier, [\n followerId.href,\n ...(previousFollowerId == null ? [] : [previousFollowerId]),\n ]);\n await this.query(\n sql,\n `INSERT INTO ${this.table(\"followers\")}\n (bot_id, follower_id, actor_json)\n VALUES ($1, $2, $3::jsonb)\n ON CONFLICT (bot_id, follower_id)\n DO UPDATE SET actor_json = EXCLUDED.actor_json`,\n [identifier, followerId.href, serializeJson(followerJson)],\n );\n await this.query(\n sql,\n `INSERT INTO ${this.table(\"follow_requests\")}\n (bot_id, follow_request_id, follower_id)\n VALUES ($1, $2, $3)\n ON CONFLICT (bot_id, follow_request_id)\n DO UPDATE SET follower_id = EXCLUDED.follower_id`,\n [identifier, followId.href, followerId.href],\n );\n if (\n previousFollowerId != null && previousFollowerId !== followerId.href\n ) {\n await this.cleanupFollower(sql, identifier, previousFollowerId);\n }\n });\n }\n\n async removeFollower(\n identifier: string,\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, identifier, 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.bot_id = fr.bot_id AND f.follower_id = fr.follower_id\n WHERE fr.bot_id = $1\n AND fr.follow_request_id = $2\n AND fr.follower_id = $3\n FOR UPDATE`,\n [identifier, 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 bot_id = $1 AND follow_request_id = $2`,\n [identifier, followId.href],\n );\n await this.cleanupFollower(sql, identifier, followerId.href);\n return await parseActor(row.actor_json);\n });\n }\n\n async hasFollower(identifier: string, 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 bot_id = $1 AND follower_id = $2`,\n [identifier, followerId.href],\n );\n return rows.length > 0;\n }\n\n async *getFollowers(\n identifier: string,\n options: RepositoryGetFollowersOptions = {},\n ): AsyncIterable<Actor> {\n await this.ensureReady();\n const { offset = 0, limit } = options;\n const parameters: QueryParameter[] = [identifier];\n let query = `SELECT actor_json\n FROM ${this.table(\"followers\")}\n WHERE bot_id = $1\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(identifier: string): 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 WHERE bot_id = $1`,\n [identifier],\n );\n return rows[0]?.count ?? 0;\n }\n\n async addSentFollow(\n identifier: string,\n id: Uuid,\n follow: Follow,\n ): Promise<void> {\n await this.ensureReady();\n await this.query(\n this.sql,\n `INSERT INTO ${this.table(\"sent_follows\")} (bot_id, id, follow_json)\n VALUES ($1, $2, $3::jsonb)\n ON CONFLICT (bot_id, id)\n DO UPDATE SET follow_json = EXCLUDED.follow_json`,\n [\n identifier,\n id,\n serializeJson(await follow.toJsonLd({ format: \"compact\" })),\n ],\n );\n }\n\n async removeSentFollow(\n identifier: string,\n id: Uuid,\n ): 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 bot_id = $1 AND id = $2\n RETURNING follow_json`,\n [identifier, id],\n );\n return await parseFollow(rows[0]?.follow_json);\n }\n\n async getSentFollow(\n identifier: string,\n id: Uuid,\n ): 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 bot_id = $1 AND id = $2`,\n [identifier, id],\n );\n return await parseFollow(rows[0]?.follow_json);\n }\n\n async addFollowee(\n identifier: string,\n followeeId: URL,\n follow: Follow,\n ): Promise<void> {\n await this.ensureReady();\n await this.query(\n this.sql,\n `INSERT INTO ${this.table(\"followees\")}\n (bot_id, followee_id, follow_json)\n VALUES ($1, $2, $3::jsonb)\n ON CONFLICT (bot_id, followee_id)\n DO UPDATE SET follow_json = EXCLUDED.follow_json`,\n [\n identifier,\n followeeId.href,\n serializeJson(await follow.toJsonLd({ format: \"compact\" })),\n ],\n );\n }\n\n async removeFollowee(\n identifier: string,\n followeeId: URL,\n ): 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 bot_id = $1 AND followee_id = $2\n RETURNING follow_json`,\n [identifier, followeeId.href],\n );\n return await parseFollow(rows[0]?.follow_json);\n }\n\n async getFollowee(\n identifier: string,\n followeeId: URL,\n ): 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 bot_id = $1 AND followee_id = $2`,\n [identifier, followeeId.href],\n );\n return await parseFollow(rows[0]?.follow_json);\n }\n\n async *findFollowedBots(followeeId: URL): AsyncIterable<string> {\n await this.ensureReady();\n const rows = await this.query<{ readonly bot_id: string }>(\n this.sql,\n `SELECT bot_id\n FROM ${this.table(\"followees\")}\n WHERE followee_id = $1\n ORDER BY bot_id ASC`,\n [followeeId.href],\n );\n for (const row of rows) yield row.bot_id;\n }\n\n async vote(\n identifier: string,\n messageId: Uuid,\n voterId: URL,\n option: string,\n ): Promise<void> {\n await this.ensureReady();\n await this.query(\n this.sql,\n `INSERT INTO ${this.table(\"poll_votes\")}\n (bot_id, message_id, voter_id, option)\n VALUES ($1, $2, $3, $4)\n ON CONFLICT (bot_id, message_id, voter_id, option)\n DO NOTHING`,\n [identifier, messageId, voterId.href, option],\n );\n }\n\n async countVoters(identifier: string, 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 bot_id = $1 AND message_id = $2`,\n [identifier, messageId],\n );\n return rows[0]?.count ?? 0;\n }\n\n async countVotes(\n identifier: string,\n messageId: Uuid,\n ): 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 bot_id = $1 AND message_id = $2\n GROUP BY option\n ORDER BY option ASC`,\n [identifier, 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 /**\n * Migrates data stored by \\@fedify/botkit-postgres 0.4, which was not\n * scoped by bot actor identifiers, so that it belongs to the given\n * identifier. Rows carried over from a legacy schema have the\n * empty-string bot ID; this method assigns them to the identifier in\n * a single transaction. It only acts when the schema was actually\n * upgraded from a legacy layout, so data legitimately stored under an\n * empty-string identifier is never touched, and calling it again is\n * a no-op.\n * @param identifier The identifier of the bot actor that adopts the legacy\n * data.\n * @since 0.5.0\n */\n async migrate(identifier: string): Promise<void> {\n await this.ensureReady();\n await this.sql.begin(async (sql) => {\n const rows = await this.query<{ readonly value: string }>(\n sql,\n `SELECT value FROM ${this.table(\"botkit_metadata\")}\n WHERE \"key\" = 'legacy_data'\n FOR UPDATE`,\n );\n if (rows.length < 1) return;\n // The followers and follow_requests rows move in tandem, which\n // temporarily breaks the foreign key between them; defer the check to\n // the commit:\n await execute(sql, \"SET CONSTRAINTS ALL DEFERRED\", [], false);\n for (const table of upgradableTables) {\n await this.query(\n sql,\n `UPDATE \"${this.schema}\".\"${table}\"\n SET bot_id = $1\n WHERE bot_id = ''`,\n [identifier],\n );\n }\n await this.query(\n sql,\n `DELETE FROM ${this.table(\"botkit_metadata\")}\n WHERE \"key\" = 'legacy_data'`,\n );\n });\n }\n\n forIdentifier(identifier: string): ActorScopedRepository {\n return new ActorScopedRepository(this, identifier);\n }\n\n private table(name: string): string {\n return `\"${this.schema}\".\"${name}\"`;\n }\n\n private async lockFollowRequest(\n sql: Queryable,\n identifier: string,\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}:${identifier}:${followId.href}`,\n ],\n );\n }\n\n private async lockFollower(\n sql: Queryable,\n identifier: string,\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}:${identifier}:${followerId}`,\n ],\n );\n }\n\n private async lockFollowers(\n sql: Queryable,\n identifier: string,\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, identifier, followerId);\n }\n }\n\n private async cleanupFollower(\n sql: Queryable,\n identifier: string,\n followerId: string,\n ): Promise<void> {\n await this.lockFollower(sql, identifier, followerId);\n await this.query(\n sql,\n `DELETE FROM ${this.table(\"followers\")}\n WHERE bot_id = $1\n AND follower_id = $2\n AND NOT EXISTS (\n SELECT 1\n FROM ${this.table(\"follow_requests\")}\n WHERE bot_id = $1\n AND follower_id = $2\n )`,\n [identifier, 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fedify/botkit-postgres",
3
- "version": "0.5.0-dev.210",
3
+ "version": "0.5.0-dev.225",
4
4
  "description": "PostgreSQL-based repository for BotKit",
5
5
  "license": "AGPL-3.0-only",
6
6
  "author": {
@@ -43,7 +43,7 @@
43
43
  "README.md"
44
44
  ],
45
45
  "peerDependencies": {
46
- "@fedify/botkit": "^0.4.0"
46
+ "@fedify/botkit": "^0.5.0-dev.225+cfc4181c"
47
47
  },
48
48
  "dependencies": {
49
49
  "@fedify/fedify": "^2.1.2",