@meith/db 0.33.4 → 0.35.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.
@@ -1,6 +1,6 @@
1
1
  import { sql } from 'drizzle-orm'
2
2
 
3
- import { PUBLIC_CONTENT } from '@meith/core'
3
+ import { PUBLIC_CONTENT, type ThreadAudience } from '@meith/core'
4
4
  import type {
5
5
  DigestCadence,
6
6
  PendingForUser,
@@ -15,6 +15,7 @@ import type { Database } from './client'
15
15
  import { resultRows } from './result-rows'
16
16
  import { toDate } from './row-values'
17
17
  import { idList } from './sql-lists'
18
+ import { inAudience } from './thread-audience'
18
19
  import { visibleIn } from './visibility'
19
20
 
20
21
  interface TableShape {
@@ -31,11 +32,10 @@ function shapeFor(target: SubscriptionTarget): TableShape {
31
32
  function pendingPosts(
32
33
  userId: number,
33
34
  mode: SubscriptionMode | null,
34
- visibleForumIds: readonly number[] | null,
35
+ audience: ThreadAudience,
35
36
  ): ReturnType<typeof sql> {
36
37
  const byMode = mode === null ? sql`` : sql`and s.mode = ${mode}`
37
- const visible =
38
- visibleForumIds === null ? sql`` : sql`and t.forum_id in ${idList(visibleForumIds)}`
38
+ const visible = sql`and ${inAudience(sql`t.forum_id`, sql`t.author_user_id`, audience)}`
39
39
 
40
40
  return sql`
41
41
  select p.id as post_id, p.thread_id, t.title as thread_title, t.slug as thread_slug,
@@ -155,15 +155,15 @@ export class PostgresSubscriptionRepository implements SubscriptionRepository {
155
155
 
156
156
  async listFor(
157
157
  userId: number,
158
- options: { readonly visibleForumIds: readonly number[]; readonly limit: number },
158
+ options: { readonly audience: ThreadAudience; readonly limit: number },
159
159
  ): Promise<readonly SubscriptionRow[]> {
160
- const visible = idList(options.visibleForumIds)
160
+ const visibleForums = idList(options.audience.forumIds)
161
161
 
162
162
  const rows = resultRows(
163
163
  await this.db.execute(sql`
164
164
  with pending as (
165
165
  select target, target_id, count(*)::int as pending
166
- from (${pendingPosts(userId, null, options.visibleForumIds)}) p
166
+ from (${pendingPosts(userId, null, options.audience)}) p
167
167
  group by target, target_id
168
168
  )
169
169
  select 'thread'::text as target, s.thread_id as target_id, t.title,
@@ -175,7 +175,7 @@ export class PostgresSubscriptionRepository implements SubscriptionRepository {
175
175
  on pending.target = 'thread' and pending.target_id = s.thread_id
176
176
  where s.user_id = ${userId}
177
177
  and ${visibleIn(sql`t.visibility`, PUBLIC_CONTENT)}
178
- and t.forum_id in ${visible}
178
+ and ${inAudience(sql`t.forum_id`, sql`t.author_user_id`, options.audience)}
179
179
 
180
180
  union all
181
181
 
@@ -186,7 +186,7 @@ export class PostgresSubscriptionRepository implements SubscriptionRepository {
186
186
  join forums f on f.id = s.forum_id
187
187
  left join pending
188
188
  on pending.target = 'forum' and pending.target_id = s.forum_id
189
- where s.user_id = ${userId} and s.forum_id in ${visible}
189
+ where s.user_id = ${userId} and s.forum_id in ${visibleForums}
190
190
 
191
191
  order by created_at desc, target_id desc
192
192
  limit ${options.limit}
@@ -269,12 +269,12 @@ export class PostgresSubscriptionRepository implements SubscriptionRepository {
269
269
  async pendingFor(input: {
270
270
  readonly userId: number
271
271
  readonly mode: SubscriptionMode
272
- readonly visibleForumIds: readonly number[]
272
+ readonly audience: ThreadAudience
273
273
  readonly limit: number
274
274
  }): Promise<PendingForUser> {
275
275
  const rows = resultRows(
276
276
  await this.db.execute(sql`
277
- select * from (${pendingPosts(input.userId, input.mode, input.visibleForumIds)}) p
277
+ select * from (${pendingPosts(input.userId, input.mode, input.audience)}) p
278
278
  order by p.post_id
279
279
  limit ${input.limit}
280
280
  `),
@@ -41,7 +41,7 @@ export class PostgresSystemHealthRepository {
41
41
  const rows = resultRows(
42
42
  await this.db.execute(sql`
43
43
  select key, interval_seconds, enabled, last_run_at, next_run_at,
44
- consecutive_failures
44
+ locked_until, consecutive_failures
45
45
  from tasks order by key
46
46
  `),
47
47
  ) as Array<Record<string, unknown>>
@@ -52,6 +52,7 @@ export class PostgresSystemHealthRepository {
52
52
  enabled: row.enabled === true,
53
53
  lastRunAt: row.last_run_at === null ? null : toDate(row.last_run_at),
54
54
  nextRunAt: row.next_run_at === null ? null : toDate(row.next_run_at),
55
+ lockedUntil: row.locked_until === null ? null : toDate(row.locked_until),
55
56
  consecutiveFailures: Number(row.consecutive_failures),
56
57
  }))
57
58
  }
package/src/task-repo.ts CHANGED
@@ -49,6 +49,20 @@ export class PostgresTaskRepository implements TaskRepository {
49
49
  })
50
50
  }
51
51
 
52
+ async renew(input: { taskId: string; now: Date; staleBefore: Date }): Promise<boolean> {
53
+ const leaseMs = input.now.getTime() - input.staleBefore.getTime()
54
+ const lockedUntil = new Date(input.now.getTime() + leaseMs)
55
+ const result = await this.db.execute(sql`
56
+ update tasks
57
+ set locked_until = ${lockedUntil}
58
+ where key = ${input.taskId}
59
+ and locked_until is not null
60
+ and locked_until > ${input.now}
61
+ returning key
62
+ `)
63
+ return resultRows(result).length === 1
64
+ }
65
+
52
66
  async claim(input: {
53
67
  taskId: string
54
68
  now: Date
@@ -122,12 +122,19 @@ export class PostgresThreadRepository implements ThreadRepository {
122
122
  .select({
123
123
  forumId: threads.forumId,
124
124
  authorUserId: threads.authorUserId,
125
+ visibility: threads.visibility,
125
126
  })
126
127
  .from(threads)
127
128
  .where(eq(threads.id, threadId))
128
129
  .limit(1)
129
130
  const row = rows[0]
130
- return row ? { forumId: row.forumId, authorUserId: row.authorUserId } : null
131
+ return row
132
+ ? {
133
+ forumId: row.forumId,
134
+ authorUserId: row.authorUserId,
135
+ visibility: row.visibility as ThreadLocation['visibility'],
136
+ }
137
+ : null
131
138
  }
132
139
 
133
140
  async findById(
@@ -358,7 +358,11 @@ export class PostgresUserBulkRepository {
358
358
  return sql.join(conditions, sql` and `)
359
359
  }
360
360
 
361
- async claimMassMailChunk(massMailId: number, limit: number): Promise<MassMailChunk> {
361
+ async claimMassMailChunk(
362
+ massMailId: number,
363
+ limit: number,
364
+ enqueue: (recipients: readonly MassMailRecipient[]) => Promise<void>,
365
+ ): Promise<MassMailChunk> {
362
366
  return this.db.transaction(async (tx) => {
363
367
  const mails = resultRows(
364
368
  await tx.execute(sql`
@@ -393,6 +397,8 @@ export class PostgresUserBulkRepository {
393
397
  const finished = recipients.length < limit
394
398
  const cursor = recipients.at(-1)?.userId ?? after
395
399
 
400
+ await enqueue(recipients)
401
+
396
402
  await tx.execute(sql`
397
403
  update mass_mails
398
404
  set last_user_id = ${cursor},
@@ -15,6 +15,7 @@ export type DiscardColumn = ReassignColumn
15
15
  export const MERGE_REASSIGN: readonly ReassignColumn[] = [
16
16
  { table: 'admin_log', column: 'user_id' },
17
17
  { table: 'admin_undo_operations', column: 'actor_user_id' },
18
+ { table: 'backup_runs', column: 'requested_by_user_id' },
18
19
  { table: 'attachments', column: 'uploader_user_id' },
19
20
  { table: 'ban_filters', column: 'created_by_user_id' },
20
21
  { table: 'bans', column: 'banned_by_user_id' },