@meith/db 0.16.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/LICENSE.md +165 -0
- package/migrations/0000_initial_schema.sql +538 -0
- package/migrations/0001_seed_usergroups.sql +111 -0
- package/migrations/0002_group_promotions.sql +25 -0
- package/migrations/0003_counter_maintenance.sql +34 -0
- package/migrations/0004_post_render_cache.sql +7 -0
- package/migrations/0005_reports.sql +51 -0
- package/migrations/0006_warnings.sql +91 -0
- package/migrations/0007_notifications.sql +44 -0
- package/migrations/0008_subscriptions.sql +48 -0
- package/migrations/0009_member_preferences.sql +13 -0
- package/migrations/0010_profile_fields.sql +53 -0
- package/migrations/0011_private_messages.sql +63 -0
- package/migrations/0012_user_relations.sql +18 -0
- package/migrations/0013_reputation.sql +45 -0
- package/migrations/0014_signatures.sql +16 -0
- package/migrations/0015_admin_sessions.sql +24 -0
- package/migrations/0016_attachments.sql +51 -0
- package/migrations/0017_avatars.sql +33 -0
- package/migrations/0018_mass_mail.sql +15 -0
- package/migrations/0019_word_filters.sql +8 -0
- package/migrations/0020_searches.sql +15 -0
- package/migrations/0021_presence_stats.sql +18 -0
- package/migrations/0022_api_tokens_webhooks.sql +58 -0
- package/migrations/0023_install_state.sql +6 -0
- package/migrations/0024_component_versions.sql +13 -0
- package/migrations/0025_import.sql +25 -0
- package/migrations/0026_polls.sql +40 -0
- package/migrations/0027_drafts.sql +13 -0
- package/migrations/0028_board_vocabulary.sql +32 -0
- package/migrations/0029_announcements.sql +16 -0
- package/migrations/0030_antispam.sql +18 -0
- package/migrations/0031_markdown.sql +24 -0
- package/migrations/0032_theme_enablement.sql +1 -0
- package/migrations/0033_group_identity.sql +5 -0
- package/migrations/0034_search_document_version.sql +4 -0
- package/migrations/0035_categories_hold_no_threads.sql +1 -0
- package/migrations/0036_expiring_group_grants.sql +6 -0
- package/migrations/0037_automatic_timezone.sql +3 -0
- package/migrations/0038_primary_group_grants.sql +3 -0
- package/migrations/0039_moderator_log_forum_scope.sql +1 -0
- package/migrations/0040_moderator_restore_grants.sql +1 -0
- package/migrations/0041_drop_placebo_moderator_rights.sql +3 -0
- package/migrations/0042_drop_placebo_delete_others.sql +2 -0
- package/migrations/0043_federated_identity.sql +28 -0
- package/migrations/0044_second_factor.sql +34 -0
- package/migrations/0045_drop_dead_task_due_index.sql +1 -0
- package/migrations/0046_member_locale.sql +1 -0
- package/migrations/0047_navigation_items.sql +34 -0
- package/migrations/0048_web_push.sql +16 -0
- package/migrations/0049_plugin_health.sql +7 -0
- package/migrations/0050_drop_forum_password_grants.sql +1 -0
- package/migrations/0051_glossy_barracuda.sql +1 -0
- package/migrations/0052_many_professor_monster.sql +2 -0
- package/migrations/0053_strange_maximus.sql +14 -0
- package/migrations/0054_legacy_import_sources.sql +25 -0
- package/migrations/0055_poll_parity.sql +14 -0
- package/migrations/0056_marketplace_catalog.sql +11 -0
- package/migrations/meta/0000_snapshot.json +4412 -0
- package/migrations/meta/0006_snapshot.json +5411 -0
- package/migrations/meta/0007_snapshot.json +5634 -0
- package/migrations/meta/0008_snapshot.json +5779 -0
- package/migrations/meta/0009_snapshot.json +5816 -0
- package/migrations/meta/0010_snapshot.json +6117 -0
- package/migrations/meta/0011_snapshot.json +6406 -0
- package/migrations/meta/0012_snapshot.json +6517 -0
- package/migrations/meta/0013_snapshot.json +6722 -0
- package/migrations/meta/0014_snapshot.json +6777 -0
- package/migrations/meta/0015_snapshot.json +6914 -0
- package/migrations/meta/0016_snapshot.json +7192 -0
- package/migrations/meta/0017_snapshot.json +7248 -0
- package/migrations/meta/0018_snapshot.json +7355 -0
- package/migrations/meta/0019_snapshot.json +7419 -0
- package/migrations/meta/0020_snapshot.json +7562 -0
- package/migrations/meta/0021_snapshot.json +7653 -0
- package/migrations/meta/0031_snapshot.json +8162 -0
- package/migrations/meta/0032_snapshot.json +8169 -0
- package/migrations/meta/0033_snapshot.json +8193 -0
- package/migrations/meta/0034_snapshot.json +8221 -0
- package/migrations/meta/0036_snapshot.json +8262 -0
- package/migrations/meta/0037_snapshot.json +8262 -0
- package/migrations/meta/0038_snapshot.json +8293 -0
- package/migrations/meta/0039_snapshot.json +8308 -0
- package/migrations/meta/0041_snapshot.json +8287 -0
- package/migrations/meta/0042_snapshot.json +8274 -0
- package/migrations/meta/0043_snapshot.json +8521 -0
- package/migrations/meta/0044_snapshot.json +8814 -0
- package/migrations/meta/0045_snapshot.json +8797 -0
- package/migrations/meta/0046_snapshot.json +8804 -0
- package/migrations/meta/0047_snapshot.json +9018 -0
- package/migrations/meta/0048_snapshot.json +9145 -0
- package/migrations/meta/0049_snapshot.json +9190 -0
- package/migrations/meta/0050_snapshot.json +9126 -0
- package/migrations/meta/0051_snapshot.json +9132 -0
- package/migrations/meta/0052_snapshot.json +9148 -0
- package/migrations/meta/0053_snapshot.json +9268 -0
- package/migrations/meta/0054_snapshot.json +9378 -0
- package/migrations/meta/0056_snapshot.json +9447 -0
- package/migrations/meta/_journal.json +405 -0
- package/package.json +64 -0
- package/src/account-repos.ts +580 -0
- package/src/actor-builder.ts +130 -0
- package/src/admin-repo.ts +85 -0
- package/src/admin-session-repo.ts +205 -0
- package/src/admin-undo-repo.ts +128 -0
- package/src/announcement-repo.ts +176 -0
- package/src/antispam-repo.ts +145 -0
- package/src/api-repo.ts +276 -0
- package/src/attachment-admin-repo.ts +161 -0
- package/src/attachment-repo.ts +240 -0
- package/src/authorization-source.ts +138 -0
- package/src/avatar-repo.ts +185 -0
- package/src/ban-repos.ts +146 -0
- package/src/client.ts +85 -0
- package/src/content-admin-repo.ts +298 -0
- package/src/content-counters.ts +152 -0
- package/src/counter-recount.ts +263 -0
- package/src/cursor.ts +16 -0
- package/src/denormalised-username.ts +34 -0
- package/src/discovery-repo.ts +140 -0
- package/src/draft-repo.ts +59 -0
- package/src/feed-repo.ts +219 -0
- package/src/forum-admin-repo.ts +310 -0
- package/src/forum-repo.ts +206 -0
- package/src/group-admin-repo.ts +256 -0
- package/src/group-identity-repo.ts +84 -0
- package/src/identity-link-repo.ts +145 -0
- package/src/import-repo.ts +123 -0
- package/src/import-sink.ts +1377 -0
- package/src/index.ts +316 -0
- package/src/inline-moderation.ts +344 -0
- package/src/install-repo.ts +67 -0
- package/src/latest-repo.ts +115 -0
- package/src/maintenance-repo.ts +54 -0
- package/src/marketplace-repo.ts +111 -0
- package/src/member-profile-repo.ts +82 -0
- package/src/member-settings-repo.ts +160 -0
- package/src/message-repo.ts +384 -0
- package/src/migrate-run.ts +18 -0
- package/src/migrate.ts +129 -0
- package/src/modcp-repo.ts +253 -0
- package/src/moderation-queue.ts +279 -0
- package/src/navigation-repo.ts +288 -0
- package/src/notification-repo.ts +402 -0
- package/src/orphan-keys.ts +19 -0
- package/src/outbox-repo.ts +78 -0
- package/src/permission-version.ts +24 -0
- package/src/permissions-map.ts +52 -0
- package/src/pglite.fixture.ts +87 -0
- package/src/plugin-data.ts +88 -0
- package/src/plugin-grants.ts +369 -0
- package/src/plugin-health-repo.ts +80 -0
- package/src/plugin-purge-repo.ts +91 -0
- package/src/plugin-users.ts +39 -0
- package/src/poll-repo.ts +334 -0
- package/src/post-repo.ts +261 -0
- package/src/post-writes.ts +227 -0
- package/src/presence-repo.ts +220 -0
- package/src/profile-field-repo.ts +182 -0
- package/src/promotion-repo.ts +152 -0
- package/src/read-state-repo.ts +81 -0
- package/src/relation-repo.ts +113 -0
- package/src/render-backfill.ts +110 -0
- package/src/render-signature.ts +33 -0
- package/src/report-repo.ts +349 -0
- package/src/reputation-repo.ts +295 -0
- package/src/result-rows.ts +13 -0
- package/src/row-values.ts +7 -0
- package/src/schema/content.ts +451 -0
- package/src/schema/identity.ts +699 -0
- package/src/schema/index.ts +10 -0
- package/src/schema/messages.ts +76 -0
- package/src/schema/permission-columns.ts +34 -0
- package/src/schema/platform.ts +376 -0
- package/src/schema/structure.ts +164 -0
- package/src/search-repo.ts +359 -0
- package/src/search-store.ts +113 -0
- package/src/seed-groups.ts +11 -0
- package/src/settings-repo.ts +32 -0
- package/src/signature-repo.ts +102 -0
- package/src/sql-lists.ts +17 -0
- package/src/staff-groups.ts +50 -0
- package/src/stats-repo.ts +151 -0
- package/src/subscription-repo.ts +350 -0
- package/src/system-health-repo.ts +130 -0
- package/src/task-repo.ts +104 -0
- package/src/theme-admin-repo.ts +172 -0
- package/src/theme-repo.ts +43 -0
- package/src/thread-audience.ts +45 -0
- package/src/thread-counters.ts +107 -0
- package/src/thread-repo.ts +255 -0
- package/src/thread-surgery.ts +282 -0
- package/src/thread-tools.ts +311 -0
- package/src/thread-views.ts +41 -0
- package/src/thread-writes.ts +356 -0
- package/src/two-factor-repo.ts +235 -0
- package/src/upgrade-repo.ts +66 -0
- package/src/user-admin-repo.ts +367 -0
- package/src/user-bulk-repo.ts +397 -0
- package/src/user-merge-map.ts +111 -0
- package/src/user-merge-repo.ts +304 -0
- package/src/visibility-counters.ts +190 -0
- package/src/visibility.ts +17 -0
- package/src/vocabulary-repo.ts +54 -0
- package/src/warning-repo.ts +328 -0
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import { sql } from 'drizzle-orm'
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
PostingRestriction,
|
|
5
|
+
WarningLevel,
|
|
6
|
+
WarningPage,
|
|
7
|
+
WarningRepository,
|
|
8
|
+
WarningRow,
|
|
9
|
+
WarningType,
|
|
10
|
+
} from '@meith/moderation'
|
|
11
|
+
import { parseWarningAction } from '@meith/moderation'
|
|
12
|
+
|
|
13
|
+
import type { Database } from './client'
|
|
14
|
+
import { decodeCursor, encodeCursor } from './cursor'
|
|
15
|
+
import { resultRows } from './result-rows'
|
|
16
|
+
|
|
17
|
+
interface Tx {
|
|
18
|
+
execute(query: ReturnType<typeof sql>): Promise<unknown>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const LIVE = sql`revoked_at is null and (expires_at is null or expires_at > now())`
|
|
22
|
+
|
|
23
|
+
export async function recalculateWarningPoints(tx: Tx, userId: number): Promise<number> {
|
|
24
|
+
const rows = resultRows(
|
|
25
|
+
await tx.execute(sql`
|
|
26
|
+
update users u
|
|
27
|
+
set warning_points = coalesce((
|
|
28
|
+
select sum(w.points)::int from warnings w
|
|
29
|
+
where w.user_id = ${userId} and ${LIVE}
|
|
30
|
+
), 0),
|
|
31
|
+
updated_at = now()
|
|
32
|
+
where u.id = ${userId}
|
|
33
|
+
returning u.warning_points
|
|
34
|
+
`),
|
|
35
|
+
) as Array<{ warning_points: number }>
|
|
36
|
+
return Number(rows[0]?.warning_points ?? 0)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function toRow(row: {
|
|
40
|
+
id: number
|
|
41
|
+
user_id: number
|
|
42
|
+
title: string
|
|
43
|
+
points: number
|
|
44
|
+
reason: string
|
|
45
|
+
issued_by_user_id: number | null
|
|
46
|
+
issued_by_username: string | null
|
|
47
|
+
post_id: number | null
|
|
48
|
+
created_at: Date
|
|
49
|
+
expires_at: Date | null
|
|
50
|
+
revoked_at: Date | null
|
|
51
|
+
revoked_by_username: string | null
|
|
52
|
+
revoke_reason: string | null
|
|
53
|
+
}): WarningRow {
|
|
54
|
+
return {
|
|
55
|
+
id: Number(row.id),
|
|
56
|
+
userId: Number(row.user_id),
|
|
57
|
+
title: row.title,
|
|
58
|
+
points: Number(row.points),
|
|
59
|
+
reason: row.reason,
|
|
60
|
+
issuedByUserId: row.issued_by_user_id === null ? null : Number(row.issued_by_user_id),
|
|
61
|
+
issuedByUsername: row.issued_by_username,
|
|
62
|
+
postId: row.post_id === null ? null : Number(row.post_id),
|
|
63
|
+
createdAt: new Date(row.created_at),
|
|
64
|
+
expiresAt: row.expires_at === null ? null : new Date(row.expires_at),
|
|
65
|
+
revokedAt: row.revoked_at === null ? null : new Date(row.revoked_at),
|
|
66
|
+
revokedByUsername: row.revoked_by_username,
|
|
67
|
+
revokeReason: row.revoke_reason,
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export class PostgresWarningRepository implements WarningRepository {
|
|
72
|
+
constructor(private readonly db: Database) {}
|
|
73
|
+
|
|
74
|
+
async listTypes(): Promise<readonly WarningType[]> {
|
|
75
|
+
const rows = resultRows(
|
|
76
|
+
await this.db.execute(sql`
|
|
77
|
+
select id, title, points, expiry_days from warning_types
|
|
78
|
+
where is_active = true
|
|
79
|
+
order by display_order, id
|
|
80
|
+
`),
|
|
81
|
+
) as Array<{ id: number; title: string; points: number; expiry_days: number | null }>
|
|
82
|
+
|
|
83
|
+
return rows.map((row) => ({
|
|
84
|
+
id: Number(row.id),
|
|
85
|
+
title: row.title,
|
|
86
|
+
points: Number(row.points),
|
|
87
|
+
expiryDays: row.expiry_days === null ? null : Number(row.expiry_days),
|
|
88
|
+
}))
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async listLevels(): Promise<readonly WarningLevel[]> {
|
|
92
|
+
const rows = resultRows(
|
|
93
|
+
await this.db.execute(sql`
|
|
94
|
+
select points, action, duration_days from warning_levels order by points
|
|
95
|
+
`),
|
|
96
|
+
) as Array<{ points: number; action: string; duration_days: number | null }>
|
|
97
|
+
|
|
98
|
+
return rows.flatMap((row) => {
|
|
99
|
+
const action = parseWarningAction(row.action)
|
|
100
|
+
return action === null
|
|
101
|
+
? []
|
|
102
|
+
: [
|
|
103
|
+
{
|
|
104
|
+
points: Number(row.points),
|
|
105
|
+
action,
|
|
106
|
+
durationDays: row.duration_days === null ? null : Number(row.duration_days),
|
|
107
|
+
},
|
|
108
|
+
]
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async findType(typeId: number): Promise<WarningType | null> {
|
|
113
|
+
const rows = resultRows(
|
|
114
|
+
await this.db.execute(sql`
|
|
115
|
+
select id, title, points, expiry_days from warning_types
|
|
116
|
+
where id = ${typeId} and is_active = true
|
|
117
|
+
`),
|
|
118
|
+
) as Array<{ id: number; title: string; points: number; expiry_days: number | null }>
|
|
119
|
+
const row = rows[0]
|
|
120
|
+
if (!row) return null
|
|
121
|
+
return {
|
|
122
|
+
id: Number(row.id),
|
|
123
|
+
title: row.title,
|
|
124
|
+
points: Number(row.points),
|
|
125
|
+
expiryDays: row.expiry_days === null ? null : Number(row.expiry_days),
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async findWarnable(userId: number): Promise<{ id: number; username: string } | null> {
|
|
130
|
+
const rows = resultRows(
|
|
131
|
+
await this.db.execute(sql`
|
|
132
|
+
select id, username from users where id = ${userId} and state <> 'deleted'
|
|
133
|
+
`),
|
|
134
|
+
) as Array<{ id: number; username: string }>
|
|
135
|
+
const row = rows[0]
|
|
136
|
+
return row === undefined ? null : { id: Number(row.id), username: row.username }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async findPostAuthor(postId: number): Promise<number | null> {
|
|
140
|
+
const rows = resultRows(
|
|
141
|
+
await this.db.execute(sql`select author_user_id from posts where id = ${postId}`),
|
|
142
|
+
) as Array<{ author_user_id: number | null }>
|
|
143
|
+
const row = rows[0]
|
|
144
|
+
return row === undefined || row.author_user_id === null ? null : Number(row.author_user_id)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async issue(input: {
|
|
148
|
+
readonly userId: number
|
|
149
|
+
readonly issuedByUserId: number
|
|
150
|
+
readonly typeId: number | null
|
|
151
|
+
readonly title: string
|
|
152
|
+
readonly points: number
|
|
153
|
+
readonly reason: string
|
|
154
|
+
readonly postId: number | null
|
|
155
|
+
readonly expiresAt: Date | null
|
|
156
|
+
readonly at: Date
|
|
157
|
+
}): Promise<{ warningId: number; points: number }> {
|
|
158
|
+
return this.db.transaction(async (tx) => {
|
|
159
|
+
const written = resultRows(
|
|
160
|
+
await tx.execute(sql`
|
|
161
|
+
insert into warnings
|
|
162
|
+
(user_id, issued_by_user_id, type_id, title, points, reason, post_id,
|
|
163
|
+
created_at, expires_at)
|
|
164
|
+
values
|
|
165
|
+
(${input.userId}, ${input.issuedByUserId}, ${input.typeId}, ${input.title},
|
|
166
|
+
${input.points}, ${input.reason}, ${input.postId}, ${input.at},
|
|
167
|
+
${input.expiresAt})
|
|
168
|
+
returning id
|
|
169
|
+
`),
|
|
170
|
+
) as Array<{ id: number }>
|
|
171
|
+
|
|
172
|
+
const points = await recalculateWarningPoints(tx, input.userId)
|
|
173
|
+
|
|
174
|
+
await tx.execute(sql`
|
|
175
|
+
insert into admin_log (user_id, action, detail, created_at)
|
|
176
|
+
values (
|
|
177
|
+
${input.issuedByUserId},
|
|
178
|
+
'warning.issue',
|
|
179
|
+
${JSON.stringify({
|
|
180
|
+
warningId: Number(written[0]!.id),
|
|
181
|
+
userId: input.userId,
|
|
182
|
+
points: input.points,
|
|
183
|
+
total: points,
|
|
184
|
+
})}::jsonb,
|
|
185
|
+
${input.at}
|
|
186
|
+
)
|
|
187
|
+
`)
|
|
188
|
+
|
|
189
|
+
return { warningId: Number(written[0]!.id), points }
|
|
190
|
+
})
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async revoke(input: {
|
|
194
|
+
readonly warningId: number
|
|
195
|
+
readonly actorUserId: number
|
|
196
|
+
readonly reason: string
|
|
197
|
+
readonly at: Date
|
|
198
|
+
}): Promise<{ userId: number; points: number } | null> {
|
|
199
|
+
return this.db.transaction(async (tx) => {
|
|
200
|
+
const moved = resultRows(
|
|
201
|
+
await tx.execute(sql`
|
|
202
|
+
update warnings
|
|
203
|
+
set revoked_at = ${input.at},
|
|
204
|
+
revoked_by_user_id = ${input.actorUserId},
|
|
205
|
+
revoke_reason = ${input.reason}
|
|
206
|
+
where id = ${input.warningId} and revoked_at is null
|
|
207
|
+
returning user_id
|
|
208
|
+
`),
|
|
209
|
+
) as Array<{ user_id: number }>
|
|
210
|
+
const row = moved[0]
|
|
211
|
+
if (!row) return null
|
|
212
|
+
|
|
213
|
+
const userId = Number(row.user_id)
|
|
214
|
+
const points = await recalculateWarningPoints(tx, userId)
|
|
215
|
+
|
|
216
|
+
await tx.execute(sql`
|
|
217
|
+
insert into admin_log (user_id, action, detail, created_at)
|
|
218
|
+
values (
|
|
219
|
+
${input.actorUserId},
|
|
220
|
+
'warning.revoke',
|
|
221
|
+
${JSON.stringify({ warningId: input.warningId, userId, total: points })}::jsonb,
|
|
222
|
+
${input.at}
|
|
223
|
+
)
|
|
224
|
+
`)
|
|
225
|
+
|
|
226
|
+
return { userId, points }
|
|
227
|
+
})
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async pointsFor(userId: number): Promise<number> {
|
|
231
|
+
return this.db.transaction(async (tx) => recalculateWarningPoints(tx, userId))
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async readRestriction(userId: number): Promise<PostingRestriction> {
|
|
235
|
+
const rows = resultRows(
|
|
236
|
+
await this.db.execute(sql`
|
|
237
|
+
select suspended_posting_until, moderated_posting_until
|
|
238
|
+
from users where id = ${userId}
|
|
239
|
+
`),
|
|
240
|
+
) as Array<{
|
|
241
|
+
suspended_posting_until: Date | null
|
|
242
|
+
moderated_posting_until: Date | null
|
|
243
|
+
}>
|
|
244
|
+
const row = rows[0]
|
|
245
|
+
if (!row) return { suspendedUntil: null, moderatedUntil: null }
|
|
246
|
+
return {
|
|
247
|
+
suspendedUntil:
|
|
248
|
+
row.suspended_posting_until === null ? null : new Date(row.suspended_posting_until),
|
|
249
|
+
moderatedUntil:
|
|
250
|
+
row.moderated_posting_until === null ? null : new Date(row.moderated_posting_until),
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async applyRestriction(input: {
|
|
255
|
+
readonly userId: number
|
|
256
|
+
readonly restriction: PostingRestriction
|
|
257
|
+
readonly at: Date
|
|
258
|
+
}): Promise<void> {
|
|
259
|
+
await this.db.execute(sql`
|
|
260
|
+
update users
|
|
261
|
+
set suspended_posting_until = ${input.restriction.suspendedUntil},
|
|
262
|
+
moderated_posting_until = ${input.restriction.moderatedUntil},
|
|
263
|
+
updated_at = now()
|
|
264
|
+
where id = ${input.userId}
|
|
265
|
+
`)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async history(
|
|
269
|
+
userId: number,
|
|
270
|
+
options: { readonly limit: number; readonly after?: string },
|
|
271
|
+
): Promise<WarningPage> {
|
|
272
|
+
const cursor = options.after === undefined ? null : decodeCursor(options.after)
|
|
273
|
+
const after = cursor ? sql`and (w.created_at, w.id) < (${cursor.at}, ${cursor.id})` : sql``
|
|
274
|
+
|
|
275
|
+
const rows = resultRows(
|
|
276
|
+
await this.db.execute(sql`
|
|
277
|
+
select w.id, w.user_id, w.title, w.points, w.reason,
|
|
278
|
+
w.issued_by_user_id, issuer.username as issued_by_username,
|
|
279
|
+
w.post_id, w.created_at, w.expires_at,
|
|
280
|
+
w.revoked_at, revoker.username as revoked_by_username, w.revoke_reason
|
|
281
|
+
from warnings w
|
|
282
|
+
left join users issuer on issuer.id = w.issued_by_user_id
|
|
283
|
+
left join users revoker on revoker.id = w.revoked_by_user_id
|
|
284
|
+
where w.user_id = ${userId} ${after}
|
|
285
|
+
order by w.created_at desc, w.id desc
|
|
286
|
+
limit ${options.limit + 1}
|
|
287
|
+
`),
|
|
288
|
+
) as Parameters<typeof toRow>[0][]
|
|
289
|
+
|
|
290
|
+
const mapped = rows.slice(0, options.limit).map(toRow)
|
|
291
|
+
const last = mapped.at(-1)
|
|
292
|
+
return rows.length > options.limit && last
|
|
293
|
+
? { rows: mapped, nextCursor: encodeCursor(last.createdAt, last.id) }
|
|
294
|
+
: { rows: mapped }
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async expireDue(
|
|
298
|
+
now: Date,
|
|
299
|
+
limit: number,
|
|
300
|
+
): Promise<{ expired: number; userIds: readonly number[] }> {
|
|
301
|
+
const rows = resultRows(
|
|
302
|
+
await this.db.execute(sql`
|
|
303
|
+
select w.id, w.user_id
|
|
304
|
+
from warnings w
|
|
305
|
+
join users u on u.id = w.user_id
|
|
306
|
+
where w.revoked_at is null
|
|
307
|
+
and w.expires_at is not null
|
|
308
|
+
and w.expires_at <= ${now}
|
|
309
|
+
/*
|
|
310
|
+
* Only the members whose cached total still counts this warning.
|
|
311
|
+
* Without it, every past expiry on the board is rediscovered on
|
|
312
|
+
* every tick and the sweep never reaches the newest ones.
|
|
313
|
+
*/
|
|
314
|
+
and u.warning_points > coalesce((
|
|
315
|
+
select sum(x.points)::int from warnings x
|
|
316
|
+
where x.user_id = w.user_id and ${LIVE}
|
|
317
|
+
), 0)
|
|
318
|
+
order by w.expires_at
|
|
319
|
+
limit ${limit}
|
|
320
|
+
`),
|
|
321
|
+
) as Array<{ id: number; user_id: number }>
|
|
322
|
+
|
|
323
|
+
return {
|
|
324
|
+
expired: rows.length,
|
|
325
|
+
userIds: [...new Set(rows.map((row) => Number(row.user_id)))],
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|