@evanp/activitypub-bot 0.18.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -332,7 +332,11 @@ PRs accepted.
332
332
 
333
333
  JavaScript code should use [JavaScript Standard Style](https://standardjs.com).
334
334
 
335
- There is a test suite using the Node [test runner](https://nodejs.org/api/test.html#test-runner). If you add a new feature, add tests for it. If you find a bug and fix it, add a test to make sure it stays fixed.
335
+ There is a test suite using the Node [test runner](https://nodejs.org/api/test.html#test-runner). Run `npm run test` to start the tests. You can run just one test
336
+ by adding it to the command-line, like `npm run test -- tests/botcontext.test.js`. The tests use `TEST_DATABASE_URL` for database configuration and default to `sqlite::memory:` (SQLite in-memory) when it is unset. This lets you test against Postgres or MySQL when needed. NOTE that the tests are destructive af and you should
337
+ not use your production database as a unit test DB!
338
+
339
+ If you add a new feature, add tests for it. If you find a bug and fix it, add a test to make sure it stays fixed.
336
340
 
337
341
  If editing the Readme, please conform to the [standard-readme](https://github.com/RichardLitt/standard-readme) specification.
338
342
 
@@ -315,7 +315,7 @@ export class ActorStorage {
315
315
  `
316
316
  INSERT INTO lastactivity (username, type, object_id, activity_id)
317
317
  VALUES (?, ?, ?, ?)
318
- ON CONFLICT DO UPDATE
318
+ ON CONFLICT (username, type, object_id) DO UPDATE
319
319
  SET activity_id = EXCLUDED.activity_id,
320
320
  updatedAt = CURRENT_TIMESTAMP
321
321
  `,
@@ -332,14 +332,23 @@ export class ActorStorage {
332
332
 
333
333
  async #getCollectionInfo (username, property) {
334
334
  const [result] = await this.#connection.query(
335
- `SELECT first, totalItems, createdAt, updatedAt
335
+ `SELECT
336
+ first,
337
+ totalItems AS totalitems,
338
+ createdAt AS createdat,
339
+ updatedAt AS updatedat
336
340
  FROM actorcollection
337
341
  WHERE username = ? AND property = ?;`,
338
342
  { replacements: [username, property] }
339
343
  )
340
344
  if (result.length > 0) {
341
345
  const row = result[0]
342
- return [row.totalItems, row.first, row.createdAt, row.updatedAt]
346
+ return [
347
+ row.totalitems,
348
+ row.first,
349
+ row.createdat,
350
+ row.updatedat
351
+ ]
343
352
  } else {
344
353
  return [0, 1, null, null]
345
354
  }
@@ -27,7 +27,8 @@ export class BotDataStorage {
27
27
  assert.equal(typeof key, 'string', 'key must be a string')
28
28
  await this.#connection.query(`
29
29
  INSERT INTO botdata (value, username, key) VALUES (?, ?, ?)
30
- ON CONFLICT DO UPDATE SET value = EXCLUDED.value, updatedAt = CURRENT_TIMESTAMP`,
30
+ ON CONFLICT (username, key) DO UPDATE
31
+ SET value = EXCLUDED.value, updatedAt = CURRENT_TIMESTAMP`,
31
32
  { replacements: [JSON.stringify(value), username, key] }
32
33
  )
33
34
  }
@@ -1,63 +1,65 @@
1
1
  export const id = '001-initial'
2
2
 
3
- export async function up (connection) {
3
+ export async function up (connection, queryOptions = {}) {
4
4
  await connection.query(`
5
5
  CREATE TABLE IF NOT EXISTS actorcollection (
6
6
  username varchar(512) NOT NULL,
7
7
  property varchar(512) NOT NULL,
8
8
  first INTEGER NOT NULL,
9
9
  totalItems INTEGER NOT NULL DEFAULT 0,
10
- createdAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
11
- updatedAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
10
+ createdAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
11
+ updatedAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
12
12
  PRIMARY KEY (username, property)
13
13
  );
14
- `)
14
+ `, queryOptions)
15
15
  await connection.query(`
16
16
  CREATE TABLE IF NOT EXISTS actorcollectionpage (
17
17
  username varchar(512) NOT NULL,
18
18
  property varchar(512) NOT NULL,
19
19
  item varchar(512) NOT NULL,
20
20
  page INTEGER NOT NULL,
21
- createdAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
21
+ createdAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
22
22
  PRIMARY KEY (username, property, item)
23
23
  );
24
- `)
24
+ `, queryOptions)
25
25
  await connection.query(
26
26
  `CREATE INDEX IF NOT EXISTS actorcollectionpage_username_property_page
27
- ON actorcollectionpage (username, property, page);`
27
+ ON actorcollectionpage (username, property, page);`,
28
+ queryOptions
28
29
  )
29
30
 
30
31
  await connection.query(`
31
32
  CREATE TABLE IF NOT EXISTS objects (
32
33
  id VARCHAR(512) PRIMARY KEY,
33
34
  data TEXT NOT NULL,
34
- createdAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
35
- updatedAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
35
+ createdAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
36
+ updatedAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
36
37
  )
37
- `)
38
+ `, queryOptions)
38
39
  await connection.query(`
39
40
  CREATE TABLE IF NOT EXISTS collections (
40
41
  id VARCHAR(512) NOT NULL,
41
42
  property VARCHAR(512) NOT NULL,
42
43
  first INTEGER NOT NULL,
43
- createdAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
44
- updatedAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
44
+ createdAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
45
+ updatedAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
45
46
  PRIMARY KEY (id, property)
46
47
  )
47
- `)
48
+ `, queryOptions)
48
49
  await connection.query(`
49
50
  CREATE TABLE IF NOT EXISTS pages (
50
51
  id VARCHAR(512) NOT NULL,
51
52
  property VARCHAR(64) NOT NULL,
52
53
  item VARCHAR(512) NOT NULL,
53
54
  page INTEGER NOT NULL,
54
- createdAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
55
+ createdAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
55
56
  PRIMARY KEY (id, property, item)
56
57
  )
57
- `)
58
+ `, queryOptions)
58
59
  await connection.query(
59
60
  `CREATE INDEX IF NOT EXISTS pages_username_property_page
60
- ON pages (id, property, page);`
61
+ ON pages (id, property, page);`,
62
+ queryOptions
61
63
  )
62
64
 
63
65
  await connection.query(`
@@ -65,11 +67,11 @@ export async function up (connection) {
65
67
  username VARCHAR(512) not null,
66
68
  key VARCHAR(512) not null,
67
69
  value TEXT not null,
68
- createdAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
69
- updatedAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
70
+ createdAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
71
+ updatedAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
70
72
  PRIMARY KEY (username, key)
71
73
  )
72
- `)
74
+ `, queryOptions)
73
75
 
74
76
  await connection.query(`
75
77
  CREATE TABLE IF NOT EXISTS new_keys (
@@ -77,14 +79,14 @@ export async function up (connection) {
77
79
  public_key TEXT,
78
80
  private_key TEXT
79
81
  )
80
- `)
81
- try {
82
+ `, queryOptions)
83
+ if (await hasTable(connection, 'keys', queryOptions)) {
82
84
  await connection.query(`
83
- INSERT OR IGNORE INTO new_keys (username, public_key, private_key)
85
+ INSERT INTO new_keys (username, public_key, private_key)
84
86
  SELECT bot_id, public_key, private_key
85
87
  FROM keys
86
- `)
87
- } catch {
88
+ ON CONFLICT DO NOTHING
89
+ `, queryOptions)
88
90
  }
89
91
 
90
92
  await connection.query(`
@@ -95,13 +97,38 @@ export async function up (connection) {
95
97
  createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
96
98
  updatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP
97
99
  )
98
- `)
99
- try {
100
+ `, queryOptions)
101
+ if (await hasTable(connection, 'remotekeys', queryOptions)) {
100
102
  await connection.query(`
101
- INSERT OR IGNORE INTO new_remotekeys (id, owner, publicKeyPem)
103
+ INSERT INTO new_remotekeys (id, owner, publicKeyPem)
102
104
  SELECT id, owner, publicKeyPem
103
105
  FROM remotekeys
104
- `)
105
- } catch {
106
+ ON CONFLICT DO NOTHING
107
+ `, queryOptions)
106
108
  }
107
109
  }
110
+
111
+ function normalizeTableName (table) {
112
+ if (typeof table === 'string') {
113
+ return table
114
+ .split('.')
115
+ .pop()
116
+ .replaceAll('"', '')
117
+ .toLowerCase()
118
+ }
119
+ if (table && typeof table === 'object') {
120
+ if (typeof table.tableName === 'string') {
121
+ return table.tableName.toLowerCase()
122
+ }
123
+ if (typeof table.name === 'string') {
124
+ return table.name.toLowerCase()
125
+ }
126
+ }
127
+ return ''
128
+ }
129
+
130
+ async function hasTable (connection, tableName, queryOptions = {}) {
131
+ const tables = await connection.getQueryInterface().showAllTables(queryOptions)
132
+ const normalized = tableName.toLowerCase()
133
+ return tables.some((table) => normalizeTableName(table) === normalized)
134
+ }
@@ -1,15 +1,15 @@
1
1
  export const id = '002-last-activity'
2
2
 
3
- export async function up (connection) {
3
+ export async function up (connection, queryOptions = {}) {
4
4
  await connection.query(`
5
- CREATE TABLE lastactivity (
5
+ CREATE TABLE IF NOT EXISTS lastactivity (
6
6
  username varchar(512) NOT NULL,
7
7
  type varchar(512) NOT NULL,
8
8
  object_id varchar(512) NOT NULL,
9
9
  activity_id varchar(512),
10
- createdAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
11
- updatedAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
10
+ createdAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
11
+ updatedAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
12
12
  PRIMARY KEY (username, type, object_id)
13
13
  );
14
- `)
14
+ `, queryOptions)
15
15
  }
@@ -6,25 +6,39 @@ const migrations = [
6
6
  { id: lastId, up: lastUp }
7
7
  ]
8
8
 
9
- export async function runMigrations (connection) {
9
+ async function runMigrationsInternal (connection, queryOptions = {}) {
10
10
  await connection.query(`
11
11
  CREATE TABLE IF NOT EXISTS migrations (
12
12
  id VARCHAR(255) PRIMARY KEY,
13
- ranAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
13
+ ranAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
14
14
  )
15
- `)
15
+ `, queryOptions)
16
16
 
17
- const [rows] = await connection.query('SELECT id FROM migrations')
17
+ const [rows] = await connection.query('SELECT id FROM migrations', queryOptions)
18
18
  const applied = new Set(rows.map((row) => row.id))
19
19
 
20
20
  for (const migration of migrations) {
21
21
  if (applied.has(migration.id)) {
22
22
  continue
23
23
  }
24
- await migration.up(connection)
24
+ await migration.up(connection, queryOptions)
25
25
  await connection.query(
26
- 'INSERT INTO migrations (id) VALUES (?)',
27
- { replacements: [migration.id] }
26
+ 'INSERT INTO migrations (id) VALUES (?) ON CONFLICT (id) DO NOTHING',
27
+ { ...queryOptions, replacements: [migration.id] }
28
28
  )
29
29
  }
30
30
  }
31
+
32
+ export async function runMigrations (connection) {
33
+ if (connection.getDialect() === 'postgres') {
34
+ await connection.transaction(async (transaction) => {
35
+ await connection.query(
36
+ 'SELECT pg_advisory_xact_lock(1600846134, 804024271)',
37
+ { transaction }
38
+ )
39
+ await runMigrationsInternal(connection, { transaction })
40
+ })
41
+ } else {
42
+ await runMigrationsInternal(connection)
43
+ }
44
+ }
@@ -77,19 +77,19 @@ export class ObjectStorage {
77
77
  let first = 1
78
78
  let createdAt = null
79
79
  const row = await this.#connection.query(
80
- 'SELECT first, createdAt FROM collections WHERE id = ? AND property = ?',
80
+ 'SELECT first, createdAt AS createdat FROM collections WHERE id = ? AND property = ?',
81
81
  { replacements: [id, property] }
82
82
  )
83
83
  if (row[0].length > 0) {
84
84
  first = row[0][0].first
85
- createdAt = row[0][0].createdAt
85
+ createdAt = row[0][0].createdat
86
86
  }
87
87
  const count = await this.#connection.query(
88
- 'SELECT COUNT(*) FROM pages WHERE id = ? AND property = ?',
88
+ 'SELECT COUNT(*) AS count FROM pages WHERE id = ? AND property = ?',
89
89
  { replacements: [id, property] }
90
90
  )
91
91
  if (count[0].length > 0) {
92
- totalItems = count[0][0]['COUNT(*)']
92
+ totalItems = Number(count[0][0].count ?? 0)
93
93
  }
94
94
  assert.equal(typeof totalItems, 'number', 'totalItems must be a number')
95
95
  assert.ok(totalItems >= 0, 'totalItems must be greater than or equal to 0')
@@ -223,10 +223,10 @@ export class ObjectStorage {
223
223
  assert.equal(typeof page, 'number', 'page must be a number')
224
224
  assert.ok(page >= 1, 'page must be greater than or equal to 1')
225
225
  const rows = await this.#connection.query(
226
- 'SELECT COUNT(*) FROM pages WHERE id = ? AND property = ? AND page = ?',
226
+ 'SELECT COUNT(*) AS count FROM pages WHERE id = ? AND property = ? AND page = ?',
227
227
  { replacements: [id, property, page] }
228
228
  )
229
- return rows[0][0]['COUNT(*)']
229
+ return Number(rows[0][0].count ?? 0)
230
230
  }
231
231
 
232
232
  async removeFromCollection (id, property, object) {
@@ -34,13 +34,13 @@ export class RemoteKeyStorage {
34
34
  async #getCachedPublicKey (id) {
35
35
  this.debug(`#getCachedPublicKey(${id})`)
36
36
  const [result] = await this.#connection.query(
37
- 'SELECT publicKeyPem, owner FROM new_remotekeys WHERE id = ?',
37
+ 'SELECT publicKeyPem AS publickeypem, owner FROM new_remotekeys WHERE id = ?',
38
38
  { replacements: [id] }
39
39
  )
40
40
  if (result.length > 0) {
41
41
  this.debug(`cache hit for ${id}`)
42
42
  return {
43
- publicKeyPem: result[0].publicKeyPem,
43
+ publicKeyPem: result[0].publickeypem,
44
44
  owner: result[0].owner
45
45
  }
46
46
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evanp/activitypub-bot",
3
- "version": "0.18.0",
3
+ "version": "0.19.0",
4
4
  "description": "server-side ActivityPub bot framework",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",