@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.
Files changed (205) hide show
  1. package/LICENSE.md +165 -0
  2. package/migrations/0000_initial_schema.sql +538 -0
  3. package/migrations/0001_seed_usergroups.sql +111 -0
  4. package/migrations/0002_group_promotions.sql +25 -0
  5. package/migrations/0003_counter_maintenance.sql +34 -0
  6. package/migrations/0004_post_render_cache.sql +7 -0
  7. package/migrations/0005_reports.sql +51 -0
  8. package/migrations/0006_warnings.sql +91 -0
  9. package/migrations/0007_notifications.sql +44 -0
  10. package/migrations/0008_subscriptions.sql +48 -0
  11. package/migrations/0009_member_preferences.sql +13 -0
  12. package/migrations/0010_profile_fields.sql +53 -0
  13. package/migrations/0011_private_messages.sql +63 -0
  14. package/migrations/0012_user_relations.sql +18 -0
  15. package/migrations/0013_reputation.sql +45 -0
  16. package/migrations/0014_signatures.sql +16 -0
  17. package/migrations/0015_admin_sessions.sql +24 -0
  18. package/migrations/0016_attachments.sql +51 -0
  19. package/migrations/0017_avatars.sql +33 -0
  20. package/migrations/0018_mass_mail.sql +15 -0
  21. package/migrations/0019_word_filters.sql +8 -0
  22. package/migrations/0020_searches.sql +15 -0
  23. package/migrations/0021_presence_stats.sql +18 -0
  24. package/migrations/0022_api_tokens_webhooks.sql +58 -0
  25. package/migrations/0023_install_state.sql +6 -0
  26. package/migrations/0024_component_versions.sql +13 -0
  27. package/migrations/0025_import.sql +25 -0
  28. package/migrations/0026_polls.sql +40 -0
  29. package/migrations/0027_drafts.sql +13 -0
  30. package/migrations/0028_board_vocabulary.sql +32 -0
  31. package/migrations/0029_announcements.sql +16 -0
  32. package/migrations/0030_antispam.sql +18 -0
  33. package/migrations/0031_markdown.sql +24 -0
  34. package/migrations/0032_theme_enablement.sql +1 -0
  35. package/migrations/0033_group_identity.sql +5 -0
  36. package/migrations/0034_search_document_version.sql +4 -0
  37. package/migrations/0035_categories_hold_no_threads.sql +1 -0
  38. package/migrations/0036_expiring_group_grants.sql +6 -0
  39. package/migrations/0037_automatic_timezone.sql +3 -0
  40. package/migrations/0038_primary_group_grants.sql +3 -0
  41. package/migrations/0039_moderator_log_forum_scope.sql +1 -0
  42. package/migrations/0040_moderator_restore_grants.sql +1 -0
  43. package/migrations/0041_drop_placebo_moderator_rights.sql +3 -0
  44. package/migrations/0042_drop_placebo_delete_others.sql +2 -0
  45. package/migrations/0043_federated_identity.sql +28 -0
  46. package/migrations/0044_second_factor.sql +34 -0
  47. package/migrations/0045_drop_dead_task_due_index.sql +1 -0
  48. package/migrations/0046_member_locale.sql +1 -0
  49. package/migrations/0047_navigation_items.sql +34 -0
  50. package/migrations/0048_web_push.sql +16 -0
  51. package/migrations/0049_plugin_health.sql +7 -0
  52. package/migrations/0050_drop_forum_password_grants.sql +1 -0
  53. package/migrations/0051_glossy_barracuda.sql +1 -0
  54. package/migrations/0052_many_professor_monster.sql +2 -0
  55. package/migrations/0053_strange_maximus.sql +14 -0
  56. package/migrations/0054_legacy_import_sources.sql +25 -0
  57. package/migrations/0055_poll_parity.sql +14 -0
  58. package/migrations/0056_marketplace_catalog.sql +11 -0
  59. package/migrations/meta/0000_snapshot.json +4412 -0
  60. package/migrations/meta/0006_snapshot.json +5411 -0
  61. package/migrations/meta/0007_snapshot.json +5634 -0
  62. package/migrations/meta/0008_snapshot.json +5779 -0
  63. package/migrations/meta/0009_snapshot.json +5816 -0
  64. package/migrations/meta/0010_snapshot.json +6117 -0
  65. package/migrations/meta/0011_snapshot.json +6406 -0
  66. package/migrations/meta/0012_snapshot.json +6517 -0
  67. package/migrations/meta/0013_snapshot.json +6722 -0
  68. package/migrations/meta/0014_snapshot.json +6777 -0
  69. package/migrations/meta/0015_snapshot.json +6914 -0
  70. package/migrations/meta/0016_snapshot.json +7192 -0
  71. package/migrations/meta/0017_snapshot.json +7248 -0
  72. package/migrations/meta/0018_snapshot.json +7355 -0
  73. package/migrations/meta/0019_snapshot.json +7419 -0
  74. package/migrations/meta/0020_snapshot.json +7562 -0
  75. package/migrations/meta/0021_snapshot.json +7653 -0
  76. package/migrations/meta/0031_snapshot.json +8162 -0
  77. package/migrations/meta/0032_snapshot.json +8169 -0
  78. package/migrations/meta/0033_snapshot.json +8193 -0
  79. package/migrations/meta/0034_snapshot.json +8221 -0
  80. package/migrations/meta/0036_snapshot.json +8262 -0
  81. package/migrations/meta/0037_snapshot.json +8262 -0
  82. package/migrations/meta/0038_snapshot.json +8293 -0
  83. package/migrations/meta/0039_snapshot.json +8308 -0
  84. package/migrations/meta/0041_snapshot.json +8287 -0
  85. package/migrations/meta/0042_snapshot.json +8274 -0
  86. package/migrations/meta/0043_snapshot.json +8521 -0
  87. package/migrations/meta/0044_snapshot.json +8814 -0
  88. package/migrations/meta/0045_snapshot.json +8797 -0
  89. package/migrations/meta/0046_snapshot.json +8804 -0
  90. package/migrations/meta/0047_snapshot.json +9018 -0
  91. package/migrations/meta/0048_snapshot.json +9145 -0
  92. package/migrations/meta/0049_snapshot.json +9190 -0
  93. package/migrations/meta/0050_snapshot.json +9126 -0
  94. package/migrations/meta/0051_snapshot.json +9132 -0
  95. package/migrations/meta/0052_snapshot.json +9148 -0
  96. package/migrations/meta/0053_snapshot.json +9268 -0
  97. package/migrations/meta/0054_snapshot.json +9378 -0
  98. package/migrations/meta/0056_snapshot.json +9447 -0
  99. package/migrations/meta/_journal.json +405 -0
  100. package/package.json +64 -0
  101. package/src/account-repos.ts +580 -0
  102. package/src/actor-builder.ts +130 -0
  103. package/src/admin-repo.ts +85 -0
  104. package/src/admin-session-repo.ts +205 -0
  105. package/src/admin-undo-repo.ts +128 -0
  106. package/src/announcement-repo.ts +176 -0
  107. package/src/antispam-repo.ts +145 -0
  108. package/src/api-repo.ts +276 -0
  109. package/src/attachment-admin-repo.ts +161 -0
  110. package/src/attachment-repo.ts +240 -0
  111. package/src/authorization-source.ts +138 -0
  112. package/src/avatar-repo.ts +185 -0
  113. package/src/ban-repos.ts +146 -0
  114. package/src/client.ts +85 -0
  115. package/src/content-admin-repo.ts +298 -0
  116. package/src/content-counters.ts +152 -0
  117. package/src/counter-recount.ts +263 -0
  118. package/src/cursor.ts +16 -0
  119. package/src/denormalised-username.ts +34 -0
  120. package/src/discovery-repo.ts +140 -0
  121. package/src/draft-repo.ts +59 -0
  122. package/src/feed-repo.ts +219 -0
  123. package/src/forum-admin-repo.ts +310 -0
  124. package/src/forum-repo.ts +206 -0
  125. package/src/group-admin-repo.ts +256 -0
  126. package/src/group-identity-repo.ts +84 -0
  127. package/src/identity-link-repo.ts +145 -0
  128. package/src/import-repo.ts +123 -0
  129. package/src/import-sink.ts +1377 -0
  130. package/src/index.ts +316 -0
  131. package/src/inline-moderation.ts +344 -0
  132. package/src/install-repo.ts +67 -0
  133. package/src/latest-repo.ts +115 -0
  134. package/src/maintenance-repo.ts +54 -0
  135. package/src/marketplace-repo.ts +111 -0
  136. package/src/member-profile-repo.ts +82 -0
  137. package/src/member-settings-repo.ts +160 -0
  138. package/src/message-repo.ts +384 -0
  139. package/src/migrate-run.ts +18 -0
  140. package/src/migrate.ts +129 -0
  141. package/src/modcp-repo.ts +253 -0
  142. package/src/moderation-queue.ts +279 -0
  143. package/src/navigation-repo.ts +288 -0
  144. package/src/notification-repo.ts +402 -0
  145. package/src/orphan-keys.ts +19 -0
  146. package/src/outbox-repo.ts +78 -0
  147. package/src/permission-version.ts +24 -0
  148. package/src/permissions-map.ts +52 -0
  149. package/src/pglite.fixture.ts +87 -0
  150. package/src/plugin-data.ts +88 -0
  151. package/src/plugin-grants.ts +369 -0
  152. package/src/plugin-health-repo.ts +80 -0
  153. package/src/plugin-purge-repo.ts +91 -0
  154. package/src/plugin-users.ts +39 -0
  155. package/src/poll-repo.ts +334 -0
  156. package/src/post-repo.ts +261 -0
  157. package/src/post-writes.ts +227 -0
  158. package/src/presence-repo.ts +220 -0
  159. package/src/profile-field-repo.ts +182 -0
  160. package/src/promotion-repo.ts +152 -0
  161. package/src/read-state-repo.ts +81 -0
  162. package/src/relation-repo.ts +113 -0
  163. package/src/render-backfill.ts +110 -0
  164. package/src/render-signature.ts +33 -0
  165. package/src/report-repo.ts +349 -0
  166. package/src/reputation-repo.ts +295 -0
  167. package/src/result-rows.ts +13 -0
  168. package/src/row-values.ts +7 -0
  169. package/src/schema/content.ts +451 -0
  170. package/src/schema/identity.ts +699 -0
  171. package/src/schema/index.ts +10 -0
  172. package/src/schema/messages.ts +76 -0
  173. package/src/schema/permission-columns.ts +34 -0
  174. package/src/schema/platform.ts +376 -0
  175. package/src/schema/structure.ts +164 -0
  176. package/src/search-repo.ts +359 -0
  177. package/src/search-store.ts +113 -0
  178. package/src/seed-groups.ts +11 -0
  179. package/src/settings-repo.ts +32 -0
  180. package/src/signature-repo.ts +102 -0
  181. package/src/sql-lists.ts +17 -0
  182. package/src/staff-groups.ts +50 -0
  183. package/src/stats-repo.ts +151 -0
  184. package/src/subscription-repo.ts +350 -0
  185. package/src/system-health-repo.ts +130 -0
  186. package/src/task-repo.ts +104 -0
  187. package/src/theme-admin-repo.ts +172 -0
  188. package/src/theme-repo.ts +43 -0
  189. package/src/thread-audience.ts +45 -0
  190. package/src/thread-counters.ts +107 -0
  191. package/src/thread-repo.ts +255 -0
  192. package/src/thread-surgery.ts +282 -0
  193. package/src/thread-tools.ts +311 -0
  194. package/src/thread-views.ts +41 -0
  195. package/src/thread-writes.ts +356 -0
  196. package/src/two-factor-repo.ts +235 -0
  197. package/src/upgrade-repo.ts +66 -0
  198. package/src/user-admin-repo.ts +367 -0
  199. package/src/user-bulk-repo.ts +397 -0
  200. package/src/user-merge-map.ts +111 -0
  201. package/src/user-merge-repo.ts +304 -0
  202. package/src/visibility-counters.ts +190 -0
  203. package/src/visibility.ts +17 -0
  204. package/src/vocabulary-repo.ts +54 -0
  205. package/src/warning-repo.ts +328 -0
package/src/index.ts ADDED
@@ -0,0 +1,316 @@
1
+ export {
2
+ createPostgresAccountStore,
3
+ PostgresAccountRepository,
4
+ PostgresCredentialTokenRepository,
5
+ PostgresLoginAttemptRepository,
6
+ PostgresRememberTokenRepository,
7
+ PostgresSessionRepository,
8
+ } from './account-repos'
9
+ export { ActorBuilder, type ActorBuilderConfig } from './actor-builder'
10
+ export {
11
+ type GroupSummary,
12
+ PostgresAdminRepository,
13
+ type UserSummary,
14
+ } from './admin-repo'
15
+ export {
16
+ PostgresAdminLogRepository,
17
+ PostgresAdminSessionRepository,
18
+ } from './admin-session-repo'
19
+ export {
20
+ ADMIN_UNDO_MAX_SNAPSHOT_BYTES,
21
+ type AdminUndoOperation,
22
+ PostgresAdminUndoRepository,
23
+ } from './admin-undo-repo'
24
+ export {
25
+ type AnnouncementInput,
26
+ type AnnouncementRow,
27
+ PostgresAnnouncementRepository,
28
+ } from './announcement-repo'
29
+ export {
30
+ type CaptchaQuestionRow,
31
+ PostgresCaptchaQuestionRepository,
32
+ PostgresRateLimitBucketStore,
33
+ } from './antispam-repo'
34
+ export {
35
+ type ApiTokenSummary,
36
+ PostgresApiTokenRepository,
37
+ PostgresRateLimitStore,
38
+ PostgresWebhookRepository,
39
+ type WebhookDeliveryRow,
40
+ } from './api-repo'
41
+ export {
42
+ type AttachmentAdminFilter,
43
+ type AttachmentAdminPage,
44
+ type AttachmentAdminRow,
45
+ type AttachmentTotals,
46
+ PostgresAttachmentAdminRepository,
47
+ } from './attachment-admin-repo'
48
+ export { PostgresAttachmentRepository } from './attachment-repo'
49
+ export {
50
+ PostgresAuthorizationSource,
51
+ parseAncestorPath,
52
+ } from './authorization-source'
53
+ export { PostgresAvatarRepository } from './avatar-repo'
54
+ export {
55
+ PostgresBanFilterRepository,
56
+ PostgresBanRepository,
57
+ } from './ban-repos'
58
+ export {
59
+ closeDb,
60
+ createIsolatedDb,
61
+ type Database,
62
+ getDb,
63
+ schema,
64
+ } from './client'
65
+ export {
66
+ type DirectiveRow,
67
+ PostgresContentAdminRepository,
68
+ type SmileyRow,
69
+ type ThreadPrefixRow,
70
+ type WordFilterRow,
71
+ } from './content-admin-repo'
72
+ export {
73
+ applyCreatedContentCounters,
74
+ type CreatedContent,
75
+ PostgresContentCounterRepository,
76
+ rollUpAncestorCounters,
77
+ } from './content-counters'
78
+ export {
79
+ PostgresCounterRecount,
80
+ RECOUNT_PHASES,
81
+ type RecountPhase,
82
+ type RecountRun,
83
+ } from './counter-recount'
84
+ export {
85
+ type DiscoveryPage,
86
+ type DiscoveryQuery,
87
+ type DiscoveryRow,
88
+ type DiscoveryScope,
89
+ PostgresDiscoveryRepository,
90
+ } from './discovery-repo'
91
+ export { PostgresDraftRepository } from './draft-repo'
92
+ export {
93
+ type FeedPost,
94
+ type FeedScope,
95
+ type FeedThread,
96
+ PostgresFeedRepository,
97
+ type SitemapForum,
98
+ type SitemapThread,
99
+ } from './feed-repo'
100
+ export {
101
+ type AppointModeratorInput,
102
+ type ForumOptionsInput,
103
+ MODERATOR_RIGHTS,
104
+ type ModeratorAppointmentRow,
105
+ type ModeratorRight,
106
+ PostgresForumAdminRepository,
107
+ } from './forum-admin-repo'
108
+ export { PostgresForumRepository } from './forum-repo'
109
+ export {
110
+ type GroupIdentityInput,
111
+ type GroupSummaryRow,
112
+ type MembershipChunkResult,
113
+ PostgresGroupAdminRepository,
114
+ } from './group-admin-repo'
115
+ export {
116
+ type GroupIdentity,
117
+ type MemberStanding,
118
+ PostgresGroupIdentityRepository,
119
+ } from './group-identity-repo'
120
+ export {
121
+ PostgresPasskeyRepository,
122
+ PostgresUserIdentityRepository,
123
+ } from './identity-link-repo'
124
+ export {
125
+ currentImportRun,
126
+ finishImportRun,
127
+ type ImportRunRow,
128
+ type ImportSourceName,
129
+ type LegacyKind,
130
+ mapLegacyId,
131
+ resolveLegacyId,
132
+ resolveLegacyIds,
133
+ saveImportProgress,
134
+ startImportRun,
135
+ } from './import-repo'
136
+ export {
137
+ type CopiedAttachment,
138
+ type CopiedAvatar,
139
+ type ImportFileCopier,
140
+ PostgresImportSink,
141
+ } from './import-sink'
142
+ export { PostgresInlineModerationRepository } from './inline-moderation'
143
+ export {
144
+ canConnect,
145
+ countUsers,
146
+ INSTALL_LOCK_KEY,
147
+ isInstalled,
148
+ markInstalled,
149
+ withInstallLock,
150
+ } from './install-repo'
151
+ export {
152
+ type LatestPostRow,
153
+ type LatestScope,
154
+ type LatestThreadRow,
155
+ PostgresLatestRepository,
156
+ } from './latest-repo'
157
+ export { PostgresMaintenanceRepository } from './maintenance-repo'
158
+ export { PostgresMarketplaceCacheRepository } from './marketplace-repo'
159
+ export { PostgresMemberProfileRepository } from './member-profile-repo'
160
+ export { PostgresMemberSettingsRepository } from './member-settings-repo'
161
+ export { PostgresMessageRepository } from './message-repo'
162
+ export { migrationUrl, runMigrations } from './migrate'
163
+ export { PostgresModCpRepository } from './modcp-repo'
164
+ export { PostgresModerationQueueRepository } from './moderation-queue'
165
+ export {
166
+ type NavigationAudience,
167
+ type NavigationDropTarget,
168
+ type NavigationItemInput,
169
+ type NavigationItemRow,
170
+ type PluginNavigationRow,
171
+ PostgresNavigationRepository,
172
+ } from './navigation-repo'
173
+ export { PostgresNotificationRepository } from './notification-repo'
174
+ export { PostgresOutboxReader } from './outbox-repo'
175
+ export {
176
+ forumRowToOverride,
177
+ groupRowToPermissionSet,
178
+ type PermissionRow,
179
+ } from './permissions-map'
180
+ export { bindPluginSql, pluginData } from './plugin-data'
181
+ export {
182
+ expireTimedGroupMemberships,
183
+ PLUGIN_UNGRANTABLE_PERMISSIONS,
184
+ permissionsCarryPower,
185
+ pluginGrants,
186
+ } from './plugin-grants'
187
+ export {
188
+ clearPluginHealth,
189
+ type PluginHealthRecord,
190
+ readPluginHealth,
191
+ recordPluginFailure,
192
+ } from './plugin-health-repo'
193
+ export {
194
+ type PluginPurgeResult,
195
+ pluginOwnedTables,
196
+ purgePlugin,
197
+ } from './plugin-purge-repo'
198
+ export { pluginUsers } from './plugin-users'
199
+ export { PostgresPollRepository } from './poll-repo'
200
+ export { PostgresPostRepository } from './post-repo'
201
+ export { PostgresPostWriteRepository } from './post-writes'
202
+ export {
203
+ ONLINE_WINDOW_MINUTES,
204
+ type OnlineMember,
205
+ type OnlineRecord,
206
+ type OnlineScope,
207
+ type OnlineSnapshot,
208
+ PostgresPresenceRepository,
209
+ } from './presence-repo'
210
+ export { PostgresProfileFieldRepository } from './profile-field-repo'
211
+ export { PostgresPromotionRepository } from './promotion-repo'
212
+ export { PostgresReadStateRepository } from './read-state-repo'
213
+ export { PostgresRelationRepository } from './relation-repo'
214
+ export {
215
+ PostgresRenderBackfill,
216
+ type RenderBackfillRun,
217
+ } from './render-backfill'
218
+ export { syncRenderSignature } from './render-signature'
219
+ export { PostgresReportRepository } from './report-repo'
220
+ export { PostgresReputationRepository } from './reputation-repo'
221
+ export { resultRows } from './result-rows'
222
+ export * from './schema'
223
+ export {
224
+ indexedSubjectSql,
225
+ PostgresSearchRepository,
226
+ type ReindexResult,
227
+ SEARCH_DOCUMENT_VERSION,
228
+ SEARCH_WINDOW,
229
+ searchVectorSql,
230
+ } from './search-repo'
231
+ export {
232
+ type CreateSearchInput,
233
+ ownsSearch,
234
+ PostgresSearchStore,
235
+ type StoredSearch,
236
+ } from './search-store'
237
+ export { SEED_GROUP_KEY, type SeedGroupKey } from './seed-groups'
238
+ export { PostgresSettingsRepository } from './settings-repo'
239
+ export { PostgresSignatureRepository } from './signature-repo'
240
+ export {
241
+ type BoardTotals,
242
+ PostgresStatsRepository,
243
+ type StatsScope,
244
+ type TopPoster,
245
+ type TopThread,
246
+ } from './stats-repo'
247
+ export { PostgresSubscriptionRepository } from './subscription-repo'
248
+ export {
249
+ type BoardVolumes,
250
+ PostgresSystemHealthRepository,
251
+ type RecountStateRow,
252
+ type TaskRunRow,
253
+ } from './system-health-repo'
254
+ export { PostgresTaskRepository } from './task-repo'
255
+ export {
256
+ PostgresThemeAdminRepository,
257
+ parseThemeExport,
258
+ type ThemeExport,
259
+ type ThemeRecord,
260
+ } from './theme-admin-repo'
261
+ export {
262
+ PostgresThemeRepository,
263
+ type ThemeRuntimeRecord,
264
+ type ThemeRuntimeState,
265
+ } from './theme-repo'
266
+ export { PostgresThreadRepository } from './thread-repo'
267
+ export { PostgresThreadSurgeryRepository } from './thread-surgery'
268
+ export { PostgresThreadToolsRepository } from './thread-tools'
269
+ export { PostgresThreadViewBuffer } from './thread-views'
270
+ export { PostgresThreadWriteRepository } from './thread-writes'
271
+ export {
272
+ PostgresAuthEventRepository,
273
+ PostgresRecoveryCodeRepository,
274
+ PostgresTwoFactorRepository,
275
+ } from './two-factor-repo'
276
+ export {
277
+ appliedPluginMigrations,
278
+ applyPluginMigration,
279
+ readVersion,
280
+ readVersions,
281
+ recordVersion,
282
+ } from './upgrade-repo'
283
+ export {
284
+ type AccountState,
285
+ likeFragment,
286
+ PostgresUserAdminRepository,
287
+ type UserAccountInput,
288
+ type UserDetail,
289
+ type UserSearchFilter,
290
+ type UserSearchPage,
291
+ type UserSearchRow,
292
+ } from './user-admin-repo'
293
+ export {
294
+ type MassMailChunk,
295
+ type MassMailRecipient,
296
+ type MassMailRow,
297
+ PostgresUserBulkRepository,
298
+ type PruneChunkResult,
299
+ type PruneCriteria,
300
+ type PrunePreview,
301
+ } from './user-bulk-repo'
302
+ export { mergeMapColumns } from './user-merge-map'
303
+ export {
304
+ type MergeChunkResult,
305
+ type MergePreview,
306
+ PostgresUserMergeRepository,
307
+ } from './user-merge-repo'
308
+ export {
309
+ applyAncestorVisibilityChange,
310
+ applyVisibilityChangeCounters,
311
+ repairForumLastPostChain,
312
+ repairThreadLastPost,
313
+ type VisibilityChange,
314
+ } from './visibility-counters'
315
+ export { readBoardVocabulary, readVocabularySource } from './vocabulary-repo'
316
+ export { PostgresWarningRepository } from './warning-repo'
@@ -0,0 +1,344 @@
1
+ import { sql } from 'drizzle-orm'
2
+
3
+ import type {
4
+ InlineModerationRepository,
5
+ InlineTarget,
6
+ InlineTool,
7
+ MoveDestination,
8
+ QueueSelection,
9
+ } from '@meith/moderation'
10
+
11
+ import type { Database } from './client'
12
+ import { resultRows } from './result-rows'
13
+ import { idList } from './sql-lists'
14
+ import {
15
+ applyAuthorCounts,
16
+ applyForumChain,
17
+ type CounterTx,
18
+ logModeratorAction,
19
+ syncLedger,
20
+ tallyThread,
21
+ } from './thread-counters'
22
+ import { PENDING_APPROVAL } from './visibility'
23
+ import { applyVisibilityChangeCounters, repairForumLastPostChain } from './visibility-counters'
24
+
25
+ interface TargetRow {
26
+ kind: 'thread' | 'post'
27
+ id: number
28
+ forum_id: number
29
+ visibility: InlineTarget['visibility']
30
+ thread_visibility: InlineTarget['visibility']
31
+ is_locked: boolean
32
+ is_sticky: boolean
33
+ }
34
+
35
+ export class PostgresInlineModerationRepository implements InlineModerationRepository {
36
+ constructor(private readonly db: Database) {}
37
+
38
+ async resolve(
39
+ selection: readonly QueueSelection[],
40
+ forumIds: readonly number[],
41
+ ): Promise<readonly InlineTarget[]> {
42
+ if (forumIds.length === 0) return []
43
+ const threadIds = selection.filter((s) => s.kind === 'thread').map((s) => s.id)
44
+ const postIds = selection.filter((s) => s.kind === 'post').map((s) => s.id)
45
+ if (threadIds.length === 0 && postIds.length === 0) return []
46
+
47
+ const rows = resultRows(
48
+ await this.db.execute(sql`
49
+ select 'thread'::text as kind, t.id, t.forum_id, t.visibility,
50
+ t.visibility as thread_visibility, t.is_locked, t.is_sticky
51
+ from threads t
52
+ where t.id in ${idList(threadIds)}
53
+ and t.forum_id in ${idList(forumIds)}
54
+ union all
55
+ select 'post'::text, p.id, t.forum_id, p.visibility,
56
+ t.visibility as thread_visibility, false, false
57
+ from posts p
58
+ join threads t on t.id = p.thread_id
59
+ where p.id in ${idList(postIds)}
60
+ and t.forum_id in ${idList(forumIds)}
61
+ `),
62
+ ) as TargetRow[]
63
+
64
+ return rows.map((row) => ({
65
+ kind: row.kind,
66
+ id: Number(row.id),
67
+ forumId: Number(row.forum_id),
68
+ visibility: row.visibility,
69
+ threadVisibility: row.thread_visibility,
70
+ isLocked: row.is_locked,
71
+ isSticky: row.is_sticky,
72
+ }))
73
+ }
74
+
75
+ async findDestination(forumId: number): Promise<MoveDestination | null> {
76
+ const rows = resultRows(
77
+ await this.db.execute(sql`select id, type, allow_threads from forums where id = ${forumId}`),
78
+ ) as Array<{ id: number; type: MoveDestination['type']; allow_threads: boolean }>
79
+ const row = rows[0]
80
+ return row === undefined
81
+ ? null
82
+ : { id: Number(row.id), type: row.type, allowThreads: row.allow_threads === true }
83
+ }
84
+
85
+ async apply(input: {
86
+ readonly tool: InlineTool
87
+ readonly threadIds: readonly number[]
88
+ readonly postIds: readonly number[]
89
+ readonly toForumId?: number | undefined
90
+ readonly actorUserId: number
91
+ readonly at: Date
92
+ }): Promise<number> {
93
+ if (input.threadIds.length === 0 && input.postIds.length === 0) return 0
94
+
95
+ return this.db.transaction(async (tx) => {
96
+ let applied = 0
97
+ const touched = await forumsOf(tx, input.threadIds, input.postIds)
98
+
99
+ switch (input.tool) {
100
+ case 'lock':
101
+ case 'unlock':
102
+ case 'stick':
103
+ case 'unstick':
104
+ applied = await flagThreads(tx, input.tool, input.threadIds, input.at)
105
+ break
106
+
107
+ case 'approve':
108
+ for (const threadId of input.threadIds) {
109
+ applied += (await approveThread(tx, threadId)) ? 1 : 0
110
+ }
111
+ for (const postId of input.postIds) {
112
+ applied += (await movePost(tx, postId, PENDING_APPROVAL, 'visible')) ? 1 : 0
113
+ }
114
+ break
115
+
116
+ case 'delete':
117
+ case 'restore': {
118
+ const from = input.tool === 'delete' ? 'visible' : 'deleted'
119
+ const to = input.tool === 'delete' ? 'deleted' : 'visible'
120
+ for (const threadId of input.threadIds) {
121
+ applied += (await setThreadVisibility(tx, threadId, from, to, input.at)) ? 1 : 0
122
+ }
123
+ for (const postId of input.postIds) {
124
+ applied += (await movePost(tx, postId, from, to)) ? 1 : 0
125
+ }
126
+ break
127
+ }
128
+
129
+ case 'move': {
130
+ if (input.toForumId === undefined) return 0
131
+ for (const threadId of input.threadIds) {
132
+ applied += (await moveThread(tx, threadId, input.toForumId, input.at)) ? 1 : 0
133
+ }
134
+ break
135
+ }
136
+ }
137
+
138
+ if (applied > 0) {
139
+ await logModeratorAction(
140
+ tx,
141
+ `inline.${input.tool}`,
142
+ input.actorUserId,
143
+ {
144
+ threadIds: input.threadIds,
145
+ postIds: input.postIds,
146
+ applied,
147
+ ...forumKeys([...touched], input.toForumId),
148
+ },
149
+ input.at,
150
+ )
151
+ }
152
+
153
+ return applied
154
+ })
155
+ }
156
+ }
157
+
158
+ async function forumsOf(
159
+ tx: CounterTx,
160
+ threadIds: readonly number[],
161
+ postIds: readonly number[],
162
+ ): Promise<ReadonlySet<number>> {
163
+ const rows = resultRows(
164
+ await tx.execute(sql`
165
+ select forum_id from threads where id in ${idList(threadIds)}
166
+ union
167
+ select forum_id from posts where id in ${idList(postIds)}
168
+ `),
169
+ ) as Array<{ forum_id: number }>
170
+ return new Set(rows.map((row) => Number(row.forum_id)))
171
+ }
172
+
173
+ function forumKeys(
174
+ source: readonly number[],
175
+ toForumId: number | undefined,
176
+ ): Record<string, unknown> {
177
+ if (toForumId === undefined) {
178
+ return source.length === 1 ? { forumId: source[0], forumIds: source } : { forumIds: source }
179
+ }
180
+ const forumIds = [...new Set([...source, toForumId])]
181
+ return source.length === 1
182
+ ? { fromForumId: source[0], toForumId, forumIds }
183
+ : { toForumId, forumIds }
184
+ }
185
+
186
+ async function flagThreads(
187
+ tx: CounterTx,
188
+ tool: 'lock' | 'unlock' | 'stick' | 'unstick',
189
+ threadIds: readonly number[],
190
+ at: Date,
191
+ ): Promise<number> {
192
+ if (threadIds.length === 0) return 0
193
+ const locking = tool === 'lock' || tool === 'unlock'
194
+ const value = tool === 'lock' || tool === 'stick'
195
+
196
+ const moved = resultRows(
197
+ await tx.execute(
198
+ locking
199
+ ? sql`update threads set is_locked = ${value}, updated_at = ${at}
200
+ where id in ${idList(threadIds)} and visibility = 'visible'
201
+ and is_locked <> ${value} returning id`
202
+ : sql`update threads set is_sticky = ${value}, updated_at = ${at}
203
+ where id in ${idList(threadIds)} and visibility = 'visible'
204
+ and is_sticky <> ${value} returning id`,
205
+ ),
206
+ ) as Array<{ id: number }>
207
+ return moved.length
208
+ }
209
+
210
+ async function approveThread(tx: CounterTx, threadId: number): Promise<boolean> {
211
+ const moved = resultRows(
212
+ await tx.execute(sql`
213
+ update threads set visibility = 'visible', updated_at = now()
214
+ where id = ${threadId} and visibility = ${PENDING_APPROVAL}
215
+ returning id, forum_id, first_post_id, author_user_id
216
+ `),
217
+ ) as Array<{
218
+ id: number
219
+ forum_id: number
220
+ first_post_id: number | null
221
+ author_user_id: number | null
222
+ }>
223
+ const thread = moved[0]
224
+ if (!thread) return false
225
+
226
+ if (thread.first_post_id !== null) {
227
+ const post = resultRows(
228
+ await tx.execute(sql`
229
+ update posts set visibility = 'visible'
230
+ where id = ${thread.first_post_id} and visibility = ${PENDING_APPROVAL}
231
+ returning id
232
+ `),
233
+ ) as Array<{ id: number }>
234
+
235
+ if (post[0]) {
236
+ await applyVisibilityChangeCounters(tx, {
237
+ postId: Number(post[0].id),
238
+ threadId: Number(thread.id),
239
+ forumId: Number(thread.forum_id),
240
+ authorId: thread.author_user_id === null ? null : Number(thread.author_user_id),
241
+ isFirstPost: true,
242
+ delta: 1,
243
+ })
244
+ }
245
+ }
246
+ return true
247
+ }
248
+
249
+ async function movePost(tx: CounterTx, postId: number, from: string, to: string): Promise<boolean> {
250
+ const moved = resultRows(
251
+ await tx.execute(sql`
252
+ update posts set visibility = ${to}
253
+ where id = ${postId} and visibility = ${from}
254
+ returning id, thread_id, forum_id, author_user_id, is_first_post
255
+ `),
256
+ ) as Array<{
257
+ id: number
258
+ thread_id: number
259
+ forum_id: number
260
+ author_user_id: number | null
261
+ is_first_post: boolean
262
+ }>
263
+ const post = moved[0]
264
+ if (!post) return false
265
+
266
+ const delta = (to === 'visible' ? 1 : 0) - (from === 'visible' ? 1 : 0)
267
+ if (delta !== 0) {
268
+ await applyVisibilityChangeCounters(tx, {
269
+ postId: Number(post.id),
270
+ threadId: Number(post.thread_id),
271
+ forumId: Number(post.forum_id),
272
+ authorId: post.author_user_id === null ? null : Number(post.author_user_id),
273
+ isFirstPost: post.is_first_post,
274
+ delta: delta as 1 | -1,
275
+ })
276
+ }
277
+ return true
278
+ }
279
+
280
+ async function setThreadVisibility(
281
+ tx: CounterTx,
282
+ threadId: number,
283
+ from: 'visible' | 'deleted',
284
+ to: 'visible' | 'deleted',
285
+ at: Date,
286
+ ): Promise<boolean> {
287
+ const tally = await tallyThread(tx, threadId)
288
+
289
+ const moved = resultRows(
290
+ await tx.execute(sql`
291
+ update threads set visibility = ${to}, updated_at = ${at}
292
+ where id = ${threadId} and visibility = ${from}
293
+ returning forum_id
294
+ `),
295
+ ) as Array<{ forum_id: number }>
296
+ const row = moved[0]
297
+ if (!row) return false
298
+
299
+ const forumId = Number(row.forum_id)
300
+ const delta: 1 | -1 = to === 'visible' ? 1 : -1
301
+
302
+ await applyForumChain(tx, forumId, delta, tally)
303
+ await applyAuthorCounts(tx, delta, tally)
304
+ await syncLedger(tx, threadId, to === 'visible')
305
+ await repairForumLastPostChain(tx, forumId)
306
+ return true
307
+ }
308
+
309
+ async function moveThread(
310
+ tx: CounterTx,
311
+ threadId: number,
312
+ toForumId: number,
313
+ at: Date,
314
+ ): Promise<boolean> {
315
+ const current = resultRows(
316
+ await tx.execute(sql`select forum_id, visibility from threads where id = ${threadId}`),
317
+ ) as Array<{ forum_id: number; visibility: string }>
318
+ const before = current[0]
319
+ if (before?.visibility !== 'visible') return false
320
+
321
+ const fromForumId = Number(before.forum_id)
322
+ if (fromForumId === toForumId) return false
323
+
324
+ const tally = await tallyThread(tx, threadId)
325
+
326
+ const moved = resultRows(
327
+ await tx.execute(sql`
328
+ update threads set forum_id = ${toForumId}, updated_at = ${at}
329
+ where id = ${threadId} and forum_id = ${fromForumId}
330
+ returning id
331
+ `),
332
+ ) as Array<{ id: number }>
333
+ if (moved.length === 0) return false
334
+
335
+ await tx.execute(sql`
336
+ update posts set forum_id = ${toForumId} where thread_id = ${threadId}
337
+ `)
338
+
339
+ await applyForumChain(tx, fromForumId, -1, tally)
340
+ await applyForumChain(tx, toForumId, 1, tally)
341
+ await repairForumLastPostChain(tx, fromForumId)
342
+ await repairForumLastPostChain(tx, toForumId)
343
+ return true
344
+ }