@meith/db 0.31.0 → 0.32.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.
Files changed (41) hide show
  1. package/migrations/0060_webhook_payload_format.sql +1 -0
  2. package/migrations/0061_task_schedule.sql +1 -0
  3. package/migrations/0062_pretty_zombie.sql +12 -0
  4. package/migrations/0063_board_digest.sql +3 -0
  5. package/migrations/0064_redundant_lady_bullseye.sql +2 -0
  6. package/migrations/0065_serious_puff_adder.sql +1 -0
  7. package/migrations/meta/0061_snapshot.json +9515 -0
  8. package/migrations/meta/0062_snapshot.json +9617 -0
  9. package/migrations/meta/0063_snapshot.json +9653 -0
  10. package/migrations/meta/0064_snapshot.json +9667 -0
  11. package/migrations/meta/0065_snapshot.json +9674 -0
  12. package/migrations/meta/_journal.json +42 -0
  13. package/package.json +31 -30
  14. package/src/account-repos.ts +32 -15
  15. package/src/api-repo.ts +192 -21
  16. package/src/attachment-repo.ts +26 -0
  17. package/src/board-digest-repo.ts +45 -0
  18. package/src/content-counters.ts +14 -0
  19. package/src/discovery-repo.ts +22 -0
  20. package/src/draft-repo.ts +40 -1
  21. package/src/feed-token-repo.ts +103 -0
  22. package/src/forum-admin-repo.ts +43 -18
  23. package/src/index.ts +10 -0
  24. package/src/member-settings-repo.ts +25 -1
  25. package/src/moderation-queue.ts +61 -21
  26. package/src/plugin-data.ts +19 -0
  27. package/src/plugin-grants.ts +83 -0
  28. package/src/plugin-role.ts +79 -0
  29. package/src/post-repo.ts +26 -0
  30. package/src/post-writes.ts +32 -2
  31. package/src/presence-repo.ts +29 -1
  32. package/src/read-state-repo.ts +21 -1
  33. package/src/report-repo.ts +151 -7
  34. package/src/schema/content.ts +5 -0
  35. package/src/schema/identity.ts +33 -1
  36. package/src/schema/platform.ts +2 -0
  37. package/src/search-repo.ts +48 -17
  38. package/src/task-repo.ts +19 -7
  39. package/src/thread-writes.ts +7 -5
  40. package/src/upgrade-repo.ts +15 -7
  41. package/src/user-merge-map.ts +1 -0
package/src/task-repo.ts CHANGED
@@ -12,7 +12,12 @@ export class PostgresTaskRepository implements TaskRepository {
12
12
  constructor(private readonly db: Database) {}
13
13
 
14
14
  async ensureRegistered(
15
- definitions: readonly { id: string; intervalSeconds: number }[],
15
+ definitions: readonly {
16
+ id: string
17
+ intervalSeconds: number
18
+ schedule?: string
19
+ firstRunAt?: Date
20
+ }[],
16
21
  ): Promise<void> {
17
22
  if (definitions.length === 0) return
18
23
 
@@ -22,15 +27,19 @@ export class PostgresTaskRepository implements TaskRepository {
22
27
  definitions.map((d) => ({
23
28
  key: d.id,
24
29
  intervalSeconds: d.intervalSeconds,
25
- nextRunAt: NEVER_RUN,
30
+ schedule: d.schedule ?? null,
31
+ nextRunAt: d.firstRunAt ?? NEVER_RUN,
26
32
  })),
27
33
  )
28
34
  .onConflictDoUpdate({
29
35
  target: tasks.key,
30
36
  set: {
31
37
  intervalSeconds: sql`excluded.interval_seconds`,
38
+ schedule: sql`excluded.schedule`,
32
39
  nextRunAt: sql`
33
- case when ${tasks.intervalSeconds} is distinct from excluded.interval_seconds
40
+ case when excluded.schedule is not null
41
+ then ${tasks.nextRunAt}
42
+ when ${tasks.intervalSeconds} is distinct from excluded.interval_seconds
34
43
  then coalesce(${tasks.lastRunAt}, now())
35
44
  + make_interval(secs => excluded.interval_seconds)
36
45
  else ${tasks.nextRunAt}
@@ -79,15 +88,18 @@ export class PostgresTaskRepository implements TaskRepository {
79
88
  success: boolean
80
89
  detail?: Record<string, unknown>
81
90
  error?: string
91
+ nextRunAt?: Date
82
92
  }): Promise<void> {
93
+ const nextRunExpr =
94
+ input.nextRunAt === undefined
95
+ ? sql`${input.finishedAt}::timestamptz + make_interval(secs => interval_seconds)`
96
+ : sql`${input.nextRunAt}::timestamptz`
97
+
83
98
  await this.db.transaction(async (tx) => {
84
99
  await tx.execute(sql`
85
100
  update tasks
86
101
  set locked_until = null,
87
- -- Explicit cast: an untyped bind parameter leaves Postgres
88
- -- unable to resolve timestamp-plus-interval at parse time.
89
- next_run_at = ${input.finishedAt}::timestamptz
90
- + make_interval(secs => interval_seconds),
102
+ next_run_at = ${nextRunExpr},
91
103
  consecutive_failures = ${input.success ? sql`0` : sql`consecutive_failures + 1`}
92
104
  where key = ${input.taskId}
93
105
  `)
@@ -22,7 +22,7 @@ import type {
22
22
  import type { Database } from './client'
23
23
  import { applyCreatedContentCounters } from './content-counters'
24
24
  import { resultRows } from './result-rows'
25
- import { SEARCH_DOCUMENT_VERSION, searchVectorSql } from './search-repo'
25
+ import { readSearchConfig, SEARCH_DOCUMENT_VERSION, searchVectorSql } from './search-repo'
26
26
  import { readBoardVocabulary } from './vocabulary-repo'
27
27
 
28
28
  export class PostgresThreadWriteRepository implements ThreadWriteRepository, ReplyWriteRepository {
@@ -79,6 +79,7 @@ export class PostgresThreadWriteRepository implements ThreadWriteRepository, Rep
79
79
  { source: 'post', viewer: authorRef(record.authorUserId) },
80
80
  vocabularyOptions(vocabulary),
81
81
  )
82
+ const searchConfig = await readSearchConfig(this.db)
82
83
 
83
84
  return this.db.transaction(async (tx) => {
84
85
  const threadRows = resultRows(
@@ -116,7 +117,7 @@ export class PostgresThreadWriteRepository implements ThreadWriteRepository, Rep
116
117
  * is_first_post true), which search-repo.test.ts pins so the
117
118
  * writer and the backfill cannot drift apart.
118
119
  */
119
- ${searchVectorSql(sql`${record.title}`, sql`${record.message}`)},
120
+ ${searchVectorSql(searchConfig, sql`${record.title}`, sql`${record.message}`)},
120
121
  ${SEARCH_DOCUMENT_VERSION})
121
122
  returning id
122
123
  `),
@@ -145,7 +146,7 @@ export class PostgresThreadWriteRepository implements ThreadWriteRepository, Rep
145
146
  if (record.subscribe) {
146
147
  await tx.execute(sql`
147
148
  insert into thread_subscriptions (user_id, thread_id, mode, last_notified_post_id)
148
- values (${record.authorUserId}, ${threadId}, 'instant', ${postId})
149
+ values (${record.authorUserId}, ${threadId}, ${record.subscribeMode ?? 'instant'}, ${postId})
149
150
  on conflict (user_id, thread_id) do nothing
150
151
  `)
151
152
  }
@@ -230,6 +231,7 @@ export class PostgresThreadWriteRepository implements ThreadWriteRepository, Rep
230
231
  { source: 'post', viewer: authorRef(record.authorUserId) },
231
232
  vocabularyOptions(vocabulary),
232
233
  )
234
+ const searchConfig = await readSearchConfig(this.db)
233
235
 
234
236
  return this.db.transaction(async (tx) => {
235
237
  const postRows = resultRows(
@@ -251,7 +253,7 @@ export class PostgresThreadWriteRepository implements ThreadWriteRepository, Rep
251
253
  * post in the thread — forty hits that are all the same thread,
252
254
  * where the opening post already stands for it.
253
255
  */
254
- ${searchVectorSql(sql`${null}`, sql`${record.message}`)},
256
+ ${searchVectorSql(searchConfig, sql`${null}`, sql`${record.message}`)},
255
257
  ${SEARCH_DOCUMENT_VERSION})
256
258
  returning id
257
259
  `),
@@ -274,7 +276,7 @@ export class PostgresThreadWriteRepository implements ThreadWriteRepository, Rep
274
276
  if (record.subscribe) {
275
277
  await tx.execute(sql`
276
278
  insert into thread_subscriptions (user_id, thread_id, mode, last_notified_post_id)
277
- values (${record.authorUserId}, ${record.threadId}, 'instant', ${postId})
279
+ values (${record.authorUserId}, ${record.threadId}, ${record.subscribeMode ?? 'instant'}, ${postId})
278
280
  on conflict (user_id, thread_id) do nothing
279
281
  `)
280
282
  }
@@ -1,6 +1,8 @@
1
1
  import { sql } from 'drizzle-orm'
2
2
 
3
3
  import type { Database } from './client'
4
+ import { ensurePluginDataRole } from './plugin-role'
5
+ import { resultRows } from './result-rows'
4
6
 
5
7
  export async function readVersion(db: Database, component: string): Promise<string | null> {
6
8
  const rows = (await db.execute(sql`
@@ -49,18 +51,24 @@ export async function applyPluginMigration(
49
51
  statements: readonly string[],
50
52
  ): Promise<boolean> {
51
53
  return db.transaction(async (tx) => {
52
- const claimed = (await tx.execute(sql`
53
- insert into plugin_migrations (plugin_key, migration_id)
54
- values (${pluginKey}, ${migrationId})
55
- on conflict (plugin_key, migration_id) do nothing
56
- returning migration_id
57
- `)) as unknown as unknown[]
54
+ const claimed = resultRows(
55
+ await tx.execute(sql`
56
+ insert into plugin_migrations (plugin_key, migration_id)
57
+ values (${pluginKey}, ${migrationId})
58
+ on conflict (plugin_key, migration_id) do nothing
59
+ returning migration_id
60
+ `),
61
+ )
58
62
 
59
- if (claimed.length === 0) return false
63
+ if (claimed.length === 0) {
64
+ await ensurePluginDataRole(tx, pluginKey)
65
+ return false
66
+ }
60
67
 
61
68
  for (const statement of statements) {
62
69
  await tx.execute(sql.raw(statement))
63
70
  }
71
+ await ensurePluginDataRole(tx, pluginKey)
64
72
  return true
65
73
  })
66
74
  }
@@ -74,6 +74,7 @@ export const ACCOUNT_CLOSURE_DISCARD: readonly DiscardColumn[] = [
74
74
  { table: 'recovery_codes', column: 'user_id' },
75
75
  { table: 'api_tokens', column: 'user_id' },
76
76
  { table: 'credential_tokens', column: 'user_id' },
77
+ { table: 'feed_tokens', column: 'user_id' },
77
78
  { table: 'remember_tokens', column: 'user_id' },
78
79
  { table: 'sessions', column: 'user_id' },
79
80
  { table: 'user_identities', column: 'user_id' },