@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 +39 -23
- package/dist/mod.d.ts.map +1 -1
- package/dist/mod.js +368 -108
- package/dist/mod.js.map +1 -1
- package/package.json +2 -2
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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":";;;;;
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
38
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
50
|
-
|
|
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
|
-
|
|
54
|
-
|
|
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, $
|
|
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
|
-
|
|
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")}
|
|
148
|
-
|
|
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 = $
|
|
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 = $
|
|
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 = $
|
|
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
|
|
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 = $
|
|
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 = $
|
|
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")}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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.
|
|
257
|
-
AND fr.
|
|
258
|
-
|
|
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 = $
|
|
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 = $
|
|
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`, [
|
|
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 = $
|
|
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 = $
|
|
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")}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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 = $
|
|
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 = $
|
|
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
|
|
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")}
|
|
345
|
-
|
|
346
|
-
|
|
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 = $
|
|
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 = $
|
|
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
|
|
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
|
|
392
|
-
|
|
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":";;;;;;;AAmCA,MAAM,cAAc,YAClB,SAAQ,IAAI,YAAY,YAAY,SAAS;AAE/C,IAAI,KAAK,UAAU,qBAAqB,KACtC,SAAQ,IAAI,KAAK,WAAW,qBAAqB,kBAAkB;AAGrE,MAAM,SAAS,UAAU,CAAC,UAAU,UAAW,EAAC;AAChD,MAAM,oBAAoB;AAC1B,MAAM,qCAAqC;AAC3C,MAAM,gCAAgC;;;;;;;;AAkFtC,eAAsB,mCACpBA,KACA,SAAS,UACT,UAAU,MACK;CACf,MAAM,kBAAkB,mBAAmB,OAAO;AAClD,OAAM,QACJ,MACC,+BAA+B,gBAAgB,IAChD,CAAE,GACF,QACD;AACD,OAAM,QACJ,MACC,8BAA8B,gBAAgB;;;;SAK/C,CAAE,GACF,QACD;AACD,OAAM,QACJ,MACC,8BAA8B,gBAAgB;;;;SAK/C,CAAE,GACF,QACD;AACD,OAAM,QACJ,MACC;aACQ,gBAAgB,+BACzB,CAAE,GACF,QACD;AACD,OAAM,QACJ,MACC,8BAA8B,gBAAgB;;;SAI/C,CAAE,GACF,QACD;AACD,OAAM,QACJ,MACC,8BAA8B,gBAAgB;;;uBAG5B,gBAAgB;;SAGnC,CAAE,GACF,QACD;AACD,OAAM,QACJ,MACC;aACQ,gBAAgB,oCACzB,CAAE,GACF,QACD;AACD,OAAM,QACJ,MACC,8BAA8B,gBAAgB;;;SAI/C,CAAE,GACF,QACD;AACD,OAAM,QACJ,MACC,8BAA8B,gBAAgB;;;SAI/C,CAAE,GACF,QACD;AACD,OAAM,QACJ,MACC,8BAA8B,gBAAgB;;;;;SAM/C,CAAE,GACF,QACD;AACD,OAAM,QACJ,MACC;aACQ,gBAAgB,sCACzB,CAAE,GACF,QACD;AACF;;;;;AAMD,IAAa,qBAAb,MAAuE;CACrE,AAAS;CACT,AAAS;CACT,AAAS;CACT,AAAiB;CACjB,AAAiB;CAEjB,YAAYC,SAAoC;AAC9C,OAAK,SAAS,mBAAmB,QAAQ,UAAU,SAAS;AAC5D,OAAK,UAAU,QAAQ,WAAW;AAClC,MAAI,SAAS,SAAS;AACpB,OAAI,QAAQ,OAAO,QAAQ,QAAQ,kBAAkB,KACnD,OAAM,IAAI,UACR;AAGJ,QAAK,UAAU;AACf,QAAK,MAAM,QAAQ;EACpB,OAAM;AACL,OAAI,QAAQ,OAAO,KACjB,OAAM,IAAI,UACR;AAGJ,QAAK,UAAU;GACf,MAAM,aAAa,QAAQ,QAAQ,WAC/B,QAAQ,MACR,QAAQ,IAAI;AAChB,QAAK,MAAM,SAAS,KAAK;IACvB,KAAK,QAAQ;IACb,UAAU,MAAM,CAAE;IAClB,SAAS,KAAK;GACf,EAAC;EACH;EACD,MAAM,QAAQ,mCACZ,KAAK,KACL,KAAK,QACL,KAAK,QACN;AAED,QAAM,MAAM,MAAM,CAAE,EAAC;AACrB,OAAK,QAAQ;CACd;CAED,OAAO,OAAO,gBAA+B;AAC3C,QAAM,KAAK,OAAO;CACnB;;;;;CAMD,MAAM,QAAuB;AAC3B,MAAI;AACF,SAAM,KAAK;EACZ,UAAS;AACR,OAAI,KAAK,QACP,OAAM,KAAK,IAAI,IAAI,EAAE,SAAS,EAAG,EAAC;EAErC;CACF;CAED,MAAM,YAAYC,UAA0C;AAC1D,QAAM,KAAK,aAAa;AACxB,QAAM,KAAK,IAAI,MAAM,OAAO,QAAQ;AAClC,SAAM,KAAK,MAAM,MAAM,cAAc,KAAK,MAAM,YAAY,CAAC,EAAE;AAC/D,QAAK,MAAM,CAAC,UAAU,QAAQ,IAAI,SAAS,SAAS,EAAE;IACpD,MAAM,aAAa,MAAM,UAAU,QAAQ,WAAW;IACtD,MAAM,YAAY,MAAM,UAAU,QAAQ,UAAU;AACpD,UAAM,KAAK,MACT,MACC,cAAc,KAAK,MAAM,YAAY,CAAC;;+CAGvC;KACE;KACA,cAAc,WAAW;KACzB,cAAc,UAAU;IACzB,EACF;GACF;EACF,EAAC;CACH;CAED,MAAM,cAAoD;AACxD,QAAM,KAAK,aAAa;EACxB,MAAM,OAAO,MAAM,KAAK,MAItB,KAAK,MACJ;gBACS,KAAK,MAAM,YAAY,CAAC;4BAEnC;AACD,MAAI,KAAK,SAAS,EAAG;EACrB,MAAMA,WAA4B,CAAE;AACpC,OAAK,MAAM,OAAO,MAAM;GACtB,MAAM,aAAa,oBAAoB,IAAI,gBAAgB;GAC3D,MAAM,YAAY,oBAAoB,IAAI,eAAe;AACzD,OAAI,cAAc,QAAQ,aAAa,KACrC,OAAM,IAAI,UAAU;AAEtB,YAAS,KAAK;IACZ,YAAY,MAAM,UAAU,YAAY,UAAU;IAClD,WAAW,MAAM,UAAU,WAAW,SAAS;GAChD,EAAC;EACH;AACD,SAAO;CACR;CAED,MAAM,WAAWC,IAAUC,UAA4C;AACrE,QAAM,KAAK,aAAa;AACxB,QAAM,KAAK,MACT,KAAK,MACJ,cAAc,KAAK,MAAM,WAAW,CAAC;oCAEtC;GACE;GACA,cAAc,MAAM,SAAS,SAAS,EAAE,QAAQ,UAAW,EAAC,CAAC;GAC7D,SAAS,WAAW,qBAAqB;EAC1C,EACF;CACF;CAED,MAAM,cACJD,IACAE,SAGkB;AAClB,QAAM,KAAK,aAAa;AACxB,SAAO,MAAM,KAAK,IAAI,MAAM,OAAO,QAAQ;GACzC,MAAM,OAAO,MAAM,KAAK,MACtB,MACC;kBACS,KAAK,MAAM,WAAW,CAAC;;uBAGjC,CAAC,EAAG,EACL;GACD,MAAM,MAAM,KAAK;AACjB,OAAI,OAAO,KAAM,QAAO;GACxB,MAAM,WAAW,MAAM,cAAc,IAAI,cAAc;AACvD,OAAI,YAAY,KAAM,QAAO;GAC7B,MAAM,UAAU,MAAM,QAAQ,SAAS;AACvC,OAAI,WAAW,KAAM,QAAO;AAC5B,SAAM,KAAK,MACT,MACC,SAAS,KAAK,MAAM,WAAW,CAAC;;;0BAIjC;IACE,cAAc,MAAM,QAAQ,SAAS,EAAE,QAAQ,UAAW,EAAC,CAAC;IAC5D,QAAQ,WAAW,qBAAqB;IACxC;GACD,EACF;AACD,UAAO;EACR,EAAC;CACH;CAED,MAAM,cAAcF,IAAkD;AACpE,QAAM,KAAK,aAAa;EACxB,MAAM,OAAO,MAAM,KAAK,MACtB,KAAK,MACJ,cAAc,KAAK,MAAM,WAAW,CAAC;;8BAGtC,CAAC,EAAG,EACL;AACD,SAAO,MAAM,cAAc,KAAK,IAAI,cAAc;CACnD;CAED,OAAO,YACLG,UAAwC,CAAE,GACR;AAClC,QAAM,KAAK,aAAa;EACxB,MAAM,EAAE,QAAQ,UAAU,OAAO,OAAO,OAAO,GAAG;EAClD,MAAMC,aAA+B,CAAE;EACvC,IAAI,SAAS;0BACS,KAAK,MAAM,WAAW,CAAC;;AAE7C,MAAI,SAAS,MAAM;AACjB,cAAW,KAAK,MAAM,kBAAkB;AACxC,aAAU,qBAAqB,WAAW,OAAO;EAClD;AACD,MAAI,SAAS,MAAM;AACjB,cAAW,KAAK,MAAM,kBAAkB;AACxC,aAAU,qBAAqB,WAAW,OAAO;EAClD;AACD,WAAS,UAAU,WACf,+CACA;AACJ,MAAI,SAAS,MAAM;AACjB,cAAW,KAAK,MAAM;AACtB,aAAU,UAAU,WAAW,OAAO;EACvC;EACD,MAAM,OAAO,MAAM,KAAK,MACtB,KAAK,KACL,OACA,WACD;AACD,OAAK,MAAM,OAAO,MAAM;GACtB,MAAM,WAAW,MAAM,cAAc,IAAI,cAAc;AACvD,OAAI,YAAY,KAAM,OAAM;EAC7B;CACF;CAED,MAAM,WAAWJ,IAAkD;AACjE,QAAM,KAAK,aAAa;EACxB,MAAM,OAAO,MAAM,KAAK,MACtB,KAAK,MACJ;gBACS,KAAK,MAAM,WAAW,CAAC;wBAEjC,CAAC,EAAG,EACL;AACD,SAAO,MAAM,cAAc,KAAK,IAAI,cAAc;CACnD;CAED,MAAM,gBAAiC;AACrC,QAAM,KAAK,aAAa;EACxB,MAAM,OAAO,MAAM,KAAK,MACtB,KAAK,MACJ;gBACS,KAAK,MAAM,WAAW,CAAC,EAClC;AACD,SAAO,KAAK,IAAI,SAAS;CAC1B;CAED,MAAM,YAAYK,UAAeC,UAAgC;AAC/D,QAAM,KAAK,aAAa;AACxB,MAAI,SAAS,MAAM,KACjB,OAAM,IAAI,UAAU;EAEtB,MAAM,aAAa,SAAS;EAC5B,MAAM,eAAe,MAAM,SAAS,SAAS,EAAE,QAAQ,UAAW,EAAC;AACnE,QAAM,KAAK,IAAI,MAAM,OAAO,QAAQ;AAClC,SAAM,KAAK,kBAAkB,KAAK,SAAS;GAC3C,MAAM,OAAO,MAAM,KAAK,MACtB,MACC;kBACS,KAAK,MAAM,kBAAkB,CAAC;;uBAGxC,CAAC,SAAS,IAAK,EAChB;GACD,MAAM,qBAAqB,KAAK,IAAI;AACpC,SAAM,KAAK,cAAc,KAAK,CAC5B,WAAW,MACX,GAAI,sBAAsB,OAAO,CAAE,IAAG,CAAC,kBAAmB,CAC3D,EAAC;AACF,SAAM,KAAK,MACT,MACC,cAAc,KAAK,MAAM,YAAY,CAAC;;;0DAIvC,CAAC,WAAW,MAAM,cAAc,aAAa,AAAC,EAC/C;AACD,SAAM,KAAK,MACT,MACC,cACC,KAAK,MAAM,kBAAkB,CAC9B;;;4DAID,CAAC,SAAS,MAAM,WAAW,IAAK,EACjC;AACD,OACE,sBAAsB,QAAQ,uBAAuB,WAAW,KAEhE,OAAM,KAAK,gBAAgB,KAAK,mBAAmB;EAEtD,EAAC;CACH;CAED,MAAM,eACJD,UACAE,YAC4B;AAC5B,QAAM,KAAK,aAAa;AACxB,SAAO,MAAM,KAAK,IAAI,MAAM,OAAO,QAAQ;AACzC,SAAM,KAAK,kBAAkB,KAAK,SAAS;GAC3C,MAAM,OAAO,MAAM,KAAK,MACtB,MACC;kBACS,KAAK,MAAM,kBAAkB,CAAC;kBAC9B,KAAK,MAAM,YAAY,CAAC;;;;uBAKlC,CAAC,SAAS,MAAM,WAAW,IAAK,EACjC;GACD,MAAM,MAAM,KAAK;AACjB,OAAI,OAAO,KAAM;AACjB,SAAM,KAAK,MACT,MACC,cAAc,KAAK,MAAM,kBAAkB,CAAC;yCAE7C,CAAC,SAAS,IAAK,EAChB;AACD,SAAM,KAAK,gBAAgB,KAAK,WAAW,KAAK;AAChD,UAAO,MAAM,WAAW,IAAI,WAAW;EACxC,EAAC;CACH;CAED,MAAM,YAAYA,YAAmC;AACnD,QAAM,KAAK,aAAa;EACxB,MAAM,OAAO,MAAM,KAAK,MACtB,KAAK,MACJ;gBACS,KAAK,MAAM,YAAY,CAAC;iCAElC,CAAC,WAAW,IAAK,EAClB;AACD,SAAO,KAAK,SAAS;CACtB;CAED,OAAO,aACLC,UAAyC,CAAE,GACrB;AACtB,QAAM,KAAK,aAAa;EACxB,MAAM,EAAE,SAAS,GAAG,OAAO,GAAG;EAC9B,MAAMJ,aAA+B,CAAE;EACvC,IAAI,SAAS;0BACS,KAAK,MAAM,YAAY,CAAC;;AAE9C,MAAI,SAAS,MAAM;AACjB,cAAW,KAAK,OAAO,OAAO;AAC9B,aAAU,UAAU,WAAW,SAAS,EAAE,WAAW,WAAW,OAAO;EACxE,WAAU,SAAS,GAAG;AACrB,cAAW,KAAK,OAAO;AACvB,aAAU,WAAW,WAAW,OAAO;EACxC;EACD,MAAM,OAAO,MAAM,KAAK,MACtB,KAAK,KACL,OACA,WACD;AACD,OAAK,MAAM,OAAO,MAAM;GACtB,MAAM,QAAQ,MAAM,WAAW,IAAI,WAAW;AAC9C,OAAI,SAAS,KAAM,OAAM;EAC1B;CACF;CAED,MAAM,iBAAkC;AACtC,QAAM,KAAK,aAAa;EACxB,MAAM,OAAO,MAAM,KAAK,MACtB,KAAK,MACJ;gBACS,KAAK,MAAM,YAAY,CAAC,EACnC;AACD,SAAO,KAAK,IAAI,SAAS;CAC1B;CAED,MAAM,cAAcJ,IAAUS,QAA+B;AAC3D,QAAM,KAAK,aAAa;AACxB,QAAM,KAAK,MACT,KAAK,MACJ,cAAc,KAAK,MAAM,eAAe,CAAC;;;0DAI1C,CAAC,IAAI,cAAc,MAAM,OAAO,SAAS,EAAE,QAAQ,UAAW,EAAC,CAAC,AAAC,EAClE;CACF;CAED,MAAM,iBAAiBT,IAAuC;AAC5D,QAAM,KAAK,aAAa;EACxB,MAAM,OAAO,MAAM,KAAK,MACtB,KAAK,MACJ,cAAc,KAAK,MAAM,eAAe,CAAC;;4BAG1C,CAAC,EAAG,EACL;AACD,SAAO,MAAM,YAAY,KAAK,IAAI,YAAY;CAC/C;CAED,MAAM,cAAcA,IAAuC;AACzD,QAAM,KAAK,aAAa;EACxB,MAAM,OAAO,MAAM,KAAK,MACtB,KAAK,MACJ;gBACS,KAAK,MAAM,eAAe,CAAC;wBAErC,CAAC,EAAG,EACL;AACD,SAAO,MAAM,YAAY,KAAK,IAAI,YAAY;CAC/C;CAED,MAAM,YAAYU,YAAiBD,QAA+B;AAChE,QAAM,KAAK,aAAa;AACxB,QAAM,KAAK,MACT,KAAK,MACJ,cAAc,KAAK,MAAM,YAAY,CAAC;;;0DAIvC,CACE,WAAW,MACX,cAAc,MAAM,OAAO,SAAS,EAAE,QAAQ,UAAW,EAAC,CAAC,AAC5D,EACF;CACF;CAED,MAAM,eAAeC,YAA8C;AACjE,QAAM,KAAK,aAAa;EACxB,MAAM,OAAO,MAAM,KAAK,MACtB,KAAK,MACJ,cAAc,KAAK,MAAM,YAAY,CAAC;;4BAGvC,CAAC,WAAW,IAAK,EAClB;AACD,SAAO,MAAM,YAAY,KAAK,IAAI,YAAY;CAC/C;CAED,MAAM,YAAYA,YAA8C;AAC9D,QAAM,KAAK,aAAa;EACxB,MAAM,OAAO,MAAM,KAAK,MACtB,KAAK,MACJ;gBACS,KAAK,MAAM,YAAY,CAAC;iCAElC,CAAC,WAAW,IAAK,EAClB;AACD,SAAO,MAAM,YAAY,KAAK,IAAI,YAAY;CAC/C;CAED,MAAM,KAAKC,WAAiBC,SAAcC,QAA+B;AACvE,QAAM,KAAK,aAAa;AACxB,QAAM,KAAK,MACT,KAAK,MACJ,cAAc,KAAK,MAAM,aAAa,CAAC;;;oBAIxC;GAAC;GAAW,QAAQ;GAAM;EAAO,EAClC;CACF;CAED,MAAM,YAAYF,WAAkC;AAClD,QAAM,KAAK,aAAa;EACxB,MAAM,OAAO,MAAM,KAAK,MACtB,KAAK,MACJ;gBACS,KAAK,MAAM,aAAa,CAAC;gCAEnC,CAAC,SAAU,EACZ;AACD,SAAO,KAAK,IAAI,SAAS;CAC1B;CAED,MAAM,WAAWA,WAA4D;AAC3E,QAAM,KAAK,aAAa;EACxB,MAAM,OAAO,MAAM,KAAK,MAItB,KAAK,MACJ;gBACS,KAAK,MAAM,aAAa,CAAC;;;2BAInC,CAAC,SAAU,EACZ;EACD,MAAMG,SAAiC,CAAE;AACzC,OAAK,MAAM,OAAO,KAChB,QAAO,IAAI,UAAU,IAAI;AAE3B,SAAO;CACR;CAED,AAAQ,MAAMC,MAAsB;AAClC,UAAQ,GAAG,KAAK,OAAO,KAAK,KAAK;CAClC;CAED,MAAc,kBACZlB,KACAQ,UACe;AACf,QAAM,KAAK,MACT,MACC,uEACD,CACE,qCACC,EAAE,KAAK,OAAO,GAAG,SAAS,KAAK,CACjC,EACF;CACF;CAED,MAAc,aACZR,KACAmB,YACe;AACf,QAAM,KAAK,MACT,MACC,uEACD,CACE,gCACC,EAAE,KAAK,OAAO,GAAG,WAAW,CAC9B,EACF;CACF;CAED,MAAc,cACZnB,KACAoB,aACe;EACf,MAAM,oBAAoB,CAAC,GAAG,IAAI,IAAI,YAAa,EAAC,MAAM;AAC1D,OAAK,MAAM,cAAc,kBACvB,OAAM,KAAK,aAAa,KAAK,WAAW;CAE3C;CAED,MAAc,gBACZpB,KACAmB,YACe;AACf,QAAM,KAAK,aAAa,KAAK,WAAW;AACxC,QAAM,KAAK,MACT,MACC,cAAc,KAAK,MAAM,YAAY,CAAC;;;;qBAIxB,KAAK,MAAM,kBAAkB,CAAC;;cAG7C,CAAC,UAAW,EACb;CACF;CAED,MAAc,cAA6B;AACzC,QAAM,KAAK;CACZ;CAED,MAAc,MACZnB,KACAqB,OACAC,aAAwC,CAAE,GAChB;AAC1B,SAAO,MAAM,QAAc,KAAK,OAAO,YAAY,KAAK,QAAQ;CACjE;AACF;AAED,SAAS,mBAAmBC,QAAwB;AAClD,MAAK,kBAAkB,KAAK,OAAO,CACjC,OAAM,IAAI,UAAU;AAEtB,QAAO;AACR;AAED,eAAe,QACbvB,KACAqB,OACAC,aAAwC,CAAE,GAC1C,UAAU,MACgB;AAC1B,QAAO,MAAM,IAAI,OACf,OACA,CAAC,GAAG,UAAW,GACf,EAAE,QAAS,EACZ;AACF;AAED,SAAS,cAAcE,OAAwB;AAC7C,QAAO,KAAK,UAAU,MAAM;AAC7B;AAED,SAAS,aAAaA,OAAkD;AACtE,eAAc,UAAU,YAAY,SAAS;AAC9C;AAED,eAAe,cACbC,MACwC;CACxC,MAAM,aAAa,oBAAoB,KAAK;AAC5C,KAAI,cAAc,KAAM;AACxB,KAAI;EACF,MAAM,WAAW,MAAM,SAAS,WAAW,WAAW;AACtD,MAAI,oBAAoB,UAAU,oBAAoB,SACpD,QAAO;CAEV,SAAQ,OAAO;AACd,SAAO,KAAK,qCAAqC,EAAE,MAAO,EAAC;CAC5D;AACD;AACD;AAED,eAAe,WAAWA,MAA2C;CACnE,MAAM,aAAa,oBAAoB,KAAK;AAC5C,KAAI,cAAc,KAAM;AACxB,KAAI;EACF,MAAM,QAAQ,MAAM,SAAO,WAAW,WAAW;AACjD,MAAI,QAAQ,MAAM,CAAE,QAAO;CAC5B,SAAQ,OAAO;AACd,SAAO,KAAK,mCAAmC,EAAE,MAAO,EAAC;CAC1D;AACD;AACD;AAED,eAAe,YAAYA,MAA4C;CACrE,MAAM,aAAa,oBAAoB,KAAK;AAC5C,KAAI,cAAc,KAAM;AACxB,KAAI;AACF,SAAO,MAAM,OAAO,WAAW,WAAW;CAC3C,SAAQ,OAAO;AACd,SAAO,KAAK,oCAAoC,EAAE,MAAO,EAAC;CAC3D;AACD;AACD;AAED,SAAS,oBACPD,OACqC;AACrC,KAAI,aAAa,MAAM,CAAE,QAAO;AAChC,YAAW,UAAU,SAAU;AAC/B,KAAI;EACF,MAAME,SAAkB,KAAK,MAAM,MAAM;AACzC,MAAI,aAAa,OAAO,CAAE,QAAO;CAClC,QAAO;AACN;CACD;AACD;AACD"}
|
|
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.
|
|
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.
|
|
46
|
+
"@fedify/botkit": "^0.5.0-dev.225+cfc4181c"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"@fedify/fedify": "^2.1.2",
|