@meith/db 0.34.0 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meith/db",
3
- "version": "0.34.0",
3
+ "version": "0.35.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -22,37 +22,37 @@
22
22
  "dependencies": {
23
23
  "drizzle-orm": "^0.45.2",
24
24
  "postgres": "^3.4.7",
25
- "@meith/accounts": "0.34.0",
26
- "@meith/admin": "0.34.0",
27
- "@meith/antispam": "0.34.0",
28
- "@meith/api": "0.34.0",
29
- "@meith/attachments": "0.34.0",
30
- "@meith/authorization": "0.34.0",
31
- "@meith/avatars": "0.34.0",
32
- "@meith/backup": "0.34.0",
33
- "@meith/board-digest": "0.34.0",
34
- "@meith/core": "0.34.0",
35
- "@meith/drafts": "0.34.0",
36
- "@meith/events": "0.34.0",
37
- "@meith/groups": "0.34.0",
38
- "@meith/forums": "0.34.0",
39
- "@meith/i18n": "0.34.0",
40
- "@meith/markdown": "0.34.0",
41
- "@meith/marketplace": "0.34.0",
42
- "@meith/messages": "0.34.0",
43
- "@meith/moderation": "0.34.0",
44
- "@meith/notifications": "0.34.0",
45
- "@meith/plugin-kit": "0.34.0",
46
- "@meith/polls": "0.34.0",
47
- "@meith/profile-fields": "0.34.0",
48
- "@meith/relations": "0.34.0",
49
- "@meith/reputation": "0.34.0",
50
- "@meith/search": "0.34.0",
51
- "@meith/settings": "0.34.0",
52
- "@meith/subscriptions": "0.34.0",
53
- "@meith/signatures": "0.34.0",
54
- "@meith/tasks": "0.34.0",
55
- "@meith/threads": "0.34.0"
25
+ "@meith/accounts": "0.35.0",
26
+ "@meith/admin": "0.35.0",
27
+ "@meith/antispam": "0.35.0",
28
+ "@meith/api": "0.35.0",
29
+ "@meith/attachments": "0.35.0",
30
+ "@meith/authorization": "0.35.0",
31
+ "@meith/avatars": "0.35.0",
32
+ "@meith/backup": "0.35.0",
33
+ "@meith/board-digest": "0.35.0",
34
+ "@meith/core": "0.35.0",
35
+ "@meith/drafts": "0.35.0",
36
+ "@meith/events": "0.35.0",
37
+ "@meith/forums": "0.35.0",
38
+ "@meith/groups": "0.35.0",
39
+ "@meith/i18n": "0.35.0",
40
+ "@meith/markdown": "0.35.0",
41
+ "@meith/marketplace": "0.35.0",
42
+ "@meith/messages": "0.35.0",
43
+ "@meith/moderation": "0.35.0",
44
+ "@meith/notifications": "0.35.0",
45
+ "@meith/plugin-kit": "0.35.0",
46
+ "@meith/polls": "0.35.0",
47
+ "@meith/profile-fields": "0.35.0",
48
+ "@meith/relations": "0.35.0",
49
+ "@meith/reputation": "0.35.0",
50
+ "@meith/search": "0.35.0",
51
+ "@meith/settings": "0.35.0",
52
+ "@meith/signatures": "0.35.0",
53
+ "@meith/subscriptions": "0.35.0",
54
+ "@meith/tasks": "0.35.0",
55
+ "@meith/threads": "0.35.0"
56
56
  },
57
57
  "devDependencies": {
58
58
  "drizzle-kit": "^0.31.6",
@@ -1,4 +1,4 @@
1
- import { and, asc, eq } from 'drizzle-orm'
1
+ import { and, asc, eq, gt, or, sql } from 'drizzle-orm'
2
2
 
3
3
  import type {
4
4
  LinkIdentityInput,
@@ -136,10 +136,17 @@ export class PostgresPasskeyRepository implements PasskeyRepository {
136
136
  return removed.length > 0
137
137
  }
138
138
 
139
- async markUsed(passkeyId: number, signCount: number, now: Date): Promise<void> {
140
- await this.db
139
+ async markUsed(passkeyId: number, signCount: number, now: Date): Promise<boolean> {
140
+ const rows = await this.db
141
141
  .update(passkeys)
142
142
  .set({ signCount, lastUsedAt: now })
143
- .where(eq(passkeys.id, passkeyId))
143
+ .where(
144
+ and(
145
+ eq(passkeys.id, passkeyId),
146
+ or(eq(sql`${signCount}`, 0), gt(sql`${signCount}`, passkeys.signCount)),
147
+ ),
148
+ )
149
+ .returning({ id: passkeys.id })
150
+ return rows.length > 0
144
151
  }
145
152
  }
@@ -336,6 +336,29 @@ export class PostgresMessageRepository implements MessageRepository {
336
336
  return rows.length
337
337
  }
338
338
 
339
+ async restore(input: {
340
+ readonly userId: number
341
+ readonly copyIds: readonly number[]
342
+ }): Promise<number> {
343
+ if (input.copyIds.length === 0) return 0
344
+
345
+ const rows = resultRows(
346
+ await this.db.execute(sql`
347
+ update private_message_copies
348
+ set folder = case when role = 'author' then 'sent' else 'inbox' end
349
+ where owner_user_id = ${input.userId}
350
+ and folder = 'trash'
351
+ and id in (${sql.join(
352
+ input.copyIds.map((id) => sql`${id}`),
353
+ sql`, `,
354
+ )})
355
+ returning id
356
+ `),
357
+ ) as Array<{ id: number }>
358
+
359
+ return rows.length
360
+ }
361
+
339
362
  async remove(input: {
340
363
  readonly userId: number
341
364
  readonly copyIds: readonly number[]
@@ -266,6 +266,15 @@ export class PostgresNotificationRepository implements NotificationRepository {
266
266
  return rows.length > 0
267
267
  }
268
268
 
269
+ async removeAllPushSubscriptions(userId: number): Promise<number> {
270
+ const rows = resultRows(
271
+ await this.db.execute(sql`
272
+ delete from push_subscriptions where user_id = ${userId} returning id
273
+ `),
274
+ ) as Array<{ id: number }>
275
+ return rows.length
276
+ }
277
+
269
278
  async pushSubscriptionsFor(userId: number): Promise<readonly PushSubscriptionRecord[]> {
270
279
  const rows = resultRows(
271
280
  await this.db.execute(sql`
package/src/post-repo.ts CHANGED
@@ -111,7 +111,7 @@ export class PostgresPostRepository implements PostRepository {
111
111
  where r.post_id = ${postId}
112
112
  and exists (select 1 from posts p where p.id = r.post_id and p.thread_id = ${threadId})
113
113
  union all
114
- select p.revision_count, p.message, p.subject, p.edited_by_user_id,
114
+ select p.revision_count + 1, p.message, p.subject, p.edited_by_user_id,
115
115
  coalesce(u.username, p.author_username, 'Deleted member'),
116
116
  p.edit_reason, coalesce(p.edited_at, p.created_at), true
117
117
  from posts p
@@ -6,6 +6,7 @@ import {
6
6
  CORE_RENDERING,
7
7
  type MarkdownPipeline,
8
8
  renderThrough,
9
+ sourceAsMarkdown,
9
10
  vocabularyOptions,
10
11
  } from '@meith/markdown'
11
12
  import type {
@@ -58,8 +59,9 @@ export class PostgresPostWriteRepository implements PostWriteRepository {
58
59
  const rows = resultRows(
59
60
  await this.db.execute(sql`
60
61
  select p.id, p.thread_id, p.forum_id, p.author_user_id, p.subject, p.message,
61
- p.visibility, p.is_first_post, p.revision_count, p.created_at,
62
- t.slug as thread_slug, t.title as thread_title, t.is_locked,
62
+ p.body_format, p.visibility, p.is_first_post, p.revision_count, p.created_at,
63
+ t.slug as thread_slug, t.title as thread_title,
64
+ t.author_user_id as thread_author_user_id, t.is_locked,
63
65
  t.visibility as thread_visibility,
64
66
  f.slug as forum_slug, f.is_open
65
67
  from posts p
@@ -74,12 +76,14 @@ export class PostgresPostWriteRepository implements PostWriteRepository {
74
76
  author_user_id: number | null
75
77
  subject: string | null
76
78
  message: string
79
+ body_format: number
77
80
  visibility: 'visible' | 'unapproved' | 'deleted'
78
81
  is_first_post: boolean
79
82
  revision_count: number
80
83
  created_at: Date
81
84
  thread_slug: string
82
85
  thread_title: string
86
+ thread_author_user_id: number | null
83
87
  is_locked: boolean
84
88
  thread_visibility: 'visible' | 'unapproved' | 'deleted'
85
89
  forum_slug: string
@@ -96,7 +100,7 @@ export class PostgresPostWriteRepository implements PostWriteRepository {
96
100
  forumId: Number(row.forum_id),
97
101
  authorUserId: row.author_user_id === null ? null : Number(row.author_user_id),
98
102
  subject: row.subject,
99
- message: row.message,
103
+ message: sourceAsMarkdown(row.message, Number(row.body_format)),
100
104
  visibility: row.visibility,
101
105
  isFirstPost: row.is_first_post,
102
106
  revisionCount: Number(row.revision_count),
@@ -106,6 +110,7 @@ export class PostgresPostWriteRepository implements PostWriteRepository {
106
110
  id: Number(row.thread_id),
107
111
  slug: row.thread_slug,
108
112
  title: row.thread_title,
113
+ authorUserId: row.thread_author_user_id === null ? null : Number(row.thread_author_user_id),
109
114
  isLocked: row.is_locked,
110
115
  visibility: row.thread_visibility,
111
116
  },
@@ -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
  `),
@@ -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},