@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
@@ -0,0 +1,451 @@
1
+ import { sql } from 'drizzle-orm'
2
+ import {
3
+ boolean,
4
+ customType,
5
+ index,
6
+ integer,
7
+ pgTable,
8
+ primaryKey,
9
+ smallint,
10
+ text,
11
+ timestamp,
12
+ uniqueIndex,
13
+ } from 'drizzle-orm/pg-core'
14
+
15
+ import { users } from './identity'
16
+ import { forums } from './structure'
17
+
18
+ export const VISIBILITY = ['visible', 'unapproved', 'deleted'] as const
19
+ export type Visibility = (typeof VISIBILITY)[number]
20
+
21
+ const tsvector = customType<{ data: string; driverData: string }>({
22
+ dataType() {
23
+ return 'tsvector'
24
+ },
25
+ })
26
+
27
+ export const threadPrefixes = pgTable(
28
+ 'thread_prefixes',
29
+ {
30
+ id: integer('id').primaryKey().generatedByDefaultAsIdentity(),
31
+ label: text('label').notNull(),
32
+ token: text('token'),
33
+ displayOrder: integer('display_order').notNull().default(0),
34
+ forumPathPrefix: text('forum_path_prefix'),
35
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
36
+ },
37
+ (t) => [index('thread_prefixes_order_idx').on(t.displayOrder)],
38
+ )
39
+
40
+ export const threads = pgTable(
41
+ 'threads',
42
+ {
43
+ id: integer('id').primaryKey().generatedByDefaultAsIdentity(),
44
+
45
+ forumId: integer('forum_id')
46
+ .notNull()
47
+ .references(() => forums.id, { onDelete: 'cascade' }),
48
+
49
+ title: text('title').notNull(),
50
+ slug: text('slug').notNull(),
51
+
52
+ prefixId: integer('prefix_id').references(() => threadPrefixes.id, {
53
+ onDelete: 'set null',
54
+ }),
55
+
56
+ authorUserId: integer('author_user_id').references(() => users.id, {
57
+ onDelete: 'set null',
58
+ }),
59
+ authorUsername: text('author_username').notNull(),
60
+
61
+ visibility: text('visibility').notNull().default('visible'),
62
+
63
+ isSticky: boolean('is_sticky').notNull().default(false),
64
+ isLocked: boolean('is_locked').notNull().default(false),
65
+
66
+ firstPostId: integer('first_post_id'),
67
+
68
+ replyCount: integer('reply_count').notNull().default(0),
69
+ viewCount: integer('view_count').notNull().default(0),
70
+ ratingTotal: integer('rating_total').notNull().default(0),
71
+ ratingCount: integer('rating_count').notNull().default(0),
72
+
73
+ lastPostId: integer('last_post_id'),
74
+ lastPostUserId: integer('last_post_user_id'),
75
+ lastPostUsername: text('last_post_username'),
76
+ lastPostAt: timestamp('last_post_at', { withTimezone: true }).notNull().defaultNow(),
77
+
78
+ movedToThreadId: integer('moved_to_thread_id'),
79
+
80
+ legacyId: integer('legacy_id'),
81
+
82
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
83
+ updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
84
+ },
85
+ (t) => [
86
+ index('threads_forum_listing_idx')
87
+ .on(t.forumId, t.isSticky.desc(), t.lastPostAt.desc())
88
+ .where(sql`${t.visibility} = 'visible'`),
89
+
90
+ index('threads_forum_listing_all_idx').on(t.forumId, t.isSticky.desc(), t.lastPostAt.desc()),
91
+
92
+ index('threads_author_idx').on(t.authorUserId, t.createdAt.desc()),
93
+ uniqueIndex('threads_legacy_id_key').on(t.legacyId).where(sql`${t.legacyId} is not null`),
94
+ ],
95
+ )
96
+
97
+ export const posts = pgTable(
98
+ 'posts',
99
+ {
100
+ id: integer('id').primaryKey().generatedByDefaultAsIdentity(),
101
+
102
+ threadId: integer('thread_id')
103
+ .notNull()
104
+ .references(() => threads.id, { onDelete: 'cascade' }),
105
+ forumId: integer('forum_id')
106
+ .notNull()
107
+ .references(() => forums.id, { onDelete: 'cascade' }),
108
+
109
+ authorUserId: integer('author_user_id').references(() => users.id, {
110
+ onDelete: 'set null',
111
+ }),
112
+ authorUsername: text('author_username').notNull(),
113
+
114
+ subject: text('subject'),
115
+ message: text('message').notNull(),
116
+
117
+ bodyFormat: smallint('body_format').notNull().default(1),
118
+
119
+ messageHtml: text('message_html'),
120
+ renderVersion: smallint('render_version').notNull().default(0),
121
+ vocabVersion: smallint('vocab_version').notNull().default(0),
122
+
123
+ visibility: text('visibility').notNull().default('visible'),
124
+
125
+ isFirstPost: boolean('is_first_post').notNull().default(false),
126
+
127
+ ipPrefix: text('ip_prefix'),
128
+
129
+ editedAt: timestamp('edited_at', { withTimezone: true }),
130
+ editedByUserId: integer('edited_by_user_id').references(() => users.id, {
131
+ onDelete: 'set null',
132
+ }),
133
+ editReason: text('edit_reason'),
134
+ revisionCount: smallint('revision_count').notNull().default(0),
135
+
136
+ searchVector: tsvector('search_vector'),
137
+ searchVersion: smallint('search_version').notNull().default(1),
138
+
139
+ legacyId: integer('legacy_id'),
140
+
141
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
142
+ },
143
+ (t) => [
144
+ index('posts_thread_visible_idx').on(t.threadId, t.id).where(sql`${t.visibility} = 'visible'`),
145
+
146
+ index('posts_thread_all_idx').on(t.threadId, t.id),
147
+
148
+ index('posts_author_idx').on(t.authorUserId, t.createdAt.desc()),
149
+
150
+ index('posts_render_version_idx').on(t.renderVersion, t.id),
151
+ index('posts_vocab_version_idx').on(t.vocabVersion, t.id),
152
+ index('posts_search_version_idx').on(t.searchVersion, t.id),
153
+ index('posts_forum_visibility_idx')
154
+ .on(t.forumId, t.createdAt.desc())
155
+ .where(sql`${t.visibility} <> 'visible'`),
156
+
157
+ uniqueIndex('posts_legacy_id_key').on(t.legacyId).where(sql`${t.legacyId} is not null`),
158
+
159
+ index('posts_search_vector_idx').using('gin', t.searchVector),
160
+ ],
161
+ )
162
+
163
+ export const postRevisions = pgTable(
164
+ 'post_revisions',
165
+ {
166
+ id: integer('id').primaryKey().generatedByDefaultAsIdentity(),
167
+ postId: integer('post_id')
168
+ .notNull()
169
+ .references(() => posts.id, { onDelete: 'cascade' }),
170
+ revision: smallint('revision').notNull(),
171
+ message: text('message').notNull(),
172
+ subject: text('subject'),
173
+ editedByUserId: integer('edited_by_user_id').references(() => users.id, {
174
+ onDelete: 'set null',
175
+ }),
176
+ editReason: text('edit_reason'),
177
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
178
+ },
179
+ (t) => [uniqueIndex('post_revisions_post_revision_key').on(t.postId, t.revision)],
180
+ )
181
+
182
+ export const threadsRead = pgTable(
183
+ 'threads_read',
184
+ {
185
+ userId: integer('user_id')
186
+ .notNull()
187
+ .references(() => users.id, { onDelete: 'cascade' }),
188
+ threadId: integer('thread_id')
189
+ .notNull()
190
+ .references(() => threads.id, { onDelete: 'cascade' }),
191
+ lastReadPostId: integer('last_read_post_id'),
192
+ readAt: timestamp('read_at', { withTimezone: true }).notNull().defaultNow(),
193
+ },
194
+ (t) => [
195
+ uniqueIndex('threads_read_pkey').on(t.userId, t.threadId),
196
+ index('threads_read_read_at_idx').on(t.readAt),
197
+ ],
198
+ )
199
+
200
+ export const threadViewBuffer = pgTable(
201
+ 'thread_view_buffer',
202
+ {
203
+ threadId: integer('thread_id')
204
+ .primaryKey()
205
+ .references(() => threads.id, { onDelete: 'cascade' }),
206
+ pending: integer('pending').notNull().default(0),
207
+ updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
208
+ },
209
+ (t) => [index('thread_view_buffer_updated_idx').on(t.updatedAt)],
210
+ )
211
+
212
+ export const threadSubscriptions = pgTable(
213
+ 'thread_subscriptions',
214
+ {
215
+ userId: integer('user_id')
216
+ .notNull()
217
+ .references(() => users.id, { onDelete: 'cascade' }),
218
+ threadId: integer('thread_id')
219
+ .notNull()
220
+ .references(() => threads.id, { onDelete: 'cascade' }),
221
+ mode: text('mode').notNull().default('instant'),
222
+ lastNotifiedPostId: integer('last_notified_post_id').notNull().default(0),
223
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
224
+ },
225
+ (t) => [
226
+ uniqueIndex('thread_subscriptions_pkey').on(t.userId, t.threadId),
227
+ index('thread_subscriptions_thread_idx').on(t.threadId),
228
+ index('thread_subscriptions_user_idx').on(t.userId, t.createdAt.desc()),
229
+ index('thread_subscriptions_mode_idx').on(t.mode, t.userId),
230
+ ],
231
+ )
232
+
233
+ export const digestRuns = pgTable(
234
+ 'digest_runs',
235
+ {
236
+ userId: integer('user_id')
237
+ .notNull()
238
+ .references(() => users.id, { onDelete: 'cascade' }),
239
+ cadence: text('cadence').notNull(),
240
+ lastSentAt: timestamp('last_sent_at', { withTimezone: true }).notNull().defaultNow(),
241
+ },
242
+ (t) => [primaryKey({ name: 'digest_runs_pkey', columns: [t.userId, t.cadence] })],
243
+ )
244
+
245
+ export const REPORT_TARGET_KINDS = ['post', 'thread', 'user'] as const
246
+ export type ReportTargetKind = (typeof REPORT_TARGET_KINDS)[number]
247
+
248
+ export const REPORT_STATUSES = ['open', 'resolved', 'rejected'] as const
249
+ export type ReportStatus = (typeof REPORT_STATUSES)[number]
250
+
251
+ export const reports = pgTable(
252
+ 'reports',
253
+ {
254
+ id: integer('id').primaryKey().generatedByDefaultAsIdentity(),
255
+
256
+ targetKind: text('target_kind').notNull(),
257
+ targetId: integer('target_id').notNull(),
258
+
259
+ forumId: integer('forum_id').references(() => forums.id, {
260
+ onDelete: 'set null',
261
+ }),
262
+ threadId: integer('thread_id').references(() => threads.id, {
263
+ onDelete: 'set null',
264
+ }),
265
+ targetLabel: text('target_label').notNull().default(''),
266
+
267
+ reporterUserId: integer('reporter_user_id').references(() => users.id, {
268
+ onDelete: 'set null',
269
+ }),
270
+ reason: text('reason').notNull(),
271
+
272
+ status: text('status').notNull().default('open'),
273
+ assignedToUserId: integer('assigned_to_user_id').references(() => users.id, {
274
+ onDelete: 'set null',
275
+ }),
276
+
277
+ resolvedByUserId: integer('resolved_by_user_id').references(() => users.id, {
278
+ onDelete: 'set null',
279
+ }),
280
+ resolvedAt: timestamp('resolved_at', { withTimezone: true }),
281
+
282
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
283
+ updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
284
+ },
285
+ (t) => [
286
+ index('reports_open_idx').on(t.forumId, t.createdAt).where(sql`${t.status} = 'open'`),
287
+ index('reports_open_global_idx')
288
+ .on(t.createdAt)
289
+ .where(sql`${t.status} = 'open' and ${t.forumId} is null`),
290
+ uniqueIndex('reports_one_open_per_reporter_key')
291
+ .on(t.reporterUserId, t.targetKind, t.targetId)
292
+ .where(sql`${t.status} = 'open' and ${t.reporterUserId} is not null`),
293
+ ],
294
+ )
295
+
296
+ export const reportEvents = pgTable(
297
+ 'report_events',
298
+ {
299
+ id: integer('id').primaryKey().generatedByDefaultAsIdentity(),
300
+ reportId: integer('report_id')
301
+ .notNull()
302
+ .references(() => reports.id, { onDelete: 'cascade' }),
303
+ actorUserId: integer('actor_user_id').references(() => users.id, {
304
+ onDelete: 'set null',
305
+ }),
306
+ kind: text('kind').notNull(),
307
+ note: text('note'),
308
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
309
+ },
310
+ (t) => [index('report_events_report_idx').on(t.reportId, t.createdAt)],
311
+ )
312
+
313
+ export const reputation = pgTable(
314
+ 'reputation',
315
+ {
316
+ id: integer('id').primaryKey().generatedByDefaultAsIdentity(),
317
+ userId: integer('user_id')
318
+ .notNull()
319
+ .references(() => users.id, { onDelete: 'cascade' }),
320
+ givenByUserId: integer('given_by_user_id').references(() => users.id, {
321
+ onDelete: 'set null',
322
+ }),
323
+ postId: integer('post_id').references(() => posts.id, {
324
+ onDelete: 'cascade',
325
+ }),
326
+ points: smallint('points').notNull().default(1),
327
+ comment: text('comment').notNull().default(''),
328
+ legacyId: integer('legacy_id'),
329
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
330
+ updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
331
+ },
332
+ (t) => [
333
+ uniqueIndex('reputation_legacy_id_key').on(t.legacyId).where(sql`${t.legacyId} is not null`),
334
+ uniqueIndex('reputation_profile_unique')
335
+ .on(t.givenByUserId, t.userId)
336
+ .where(sql`${t.postId} is null`),
337
+ uniqueIndex('reputation_post_unique')
338
+ .on(t.givenByUserId, t.postId)
339
+ .where(sql`${t.postId} is not null`),
340
+ index('reputation_user_idx').on(t.userId, t.id.desc()),
341
+ index('reputation_given_idx').on(t.givenByUserId, t.createdAt.desc()),
342
+ ],
343
+ )
344
+
345
+ export const attachments = pgTable(
346
+ 'attachments',
347
+ {
348
+ id: integer('id').primaryKey().generatedByDefaultAsIdentity(),
349
+ postId: integer('post_id').references(() => posts.id, { onDelete: 'cascade' }),
350
+ forumId: integer('forum_id')
351
+ .notNull()
352
+ .references(() => forums.id, { onDelete: 'cascade' }),
353
+ uploaderUserId: integer('uploader_user_id').references(() => users.id, {
354
+ onDelete: 'set null',
355
+ }),
356
+ filename: text('filename').notNull(),
357
+ contentType: text('content_type').notNull(),
358
+ sizeBytes: integer('size_bytes').notNull(),
359
+ storageKey: text('storage_key'),
360
+ sourceKey: text('source_key'),
361
+ thumbnailKey: text('thumbnail_key'),
362
+ width: integer('width'),
363
+ height: integer('height'),
364
+ status: text('status').notNull().default('pending'),
365
+ failureReason: text('failure_reason'),
366
+ downloadCount: integer('download_count').notNull().default(0),
367
+ legacyId: integer('legacy_id'),
368
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
369
+ readyAt: timestamp('ready_at', { withTimezone: true }),
370
+ },
371
+ (t) => [
372
+ uniqueIndex('attachments_legacy_id_key').on(t.legacyId).where(sql`${t.legacyId} is not null`),
373
+ index('attachments_post_idx').on(t.postId),
374
+ index('attachments_uploader_idx').on(t.uploaderUserId),
375
+ index('attachments_pending_idx').on(t.createdAt).where(sql`${t.status} = 'pending'`),
376
+ index('attachments_orphan_idx').on(t.createdAt).where(sql`${t.postId} is null`),
377
+ uniqueIndex('attachments_storage_key_key')
378
+ .on(t.storageKey)
379
+ .where(sql`${t.storageKey} is not null`),
380
+ uniqueIndex('attachments_source_key_key')
381
+ .on(t.sourceKey)
382
+ .where(sql`${t.sourceKey} is not null`),
383
+ ],
384
+ )
385
+
386
+ export const attachmentOrphans = pgTable(
387
+ 'attachment_orphans',
388
+ {
389
+ storageKey: text('storage_key').primaryKey(),
390
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
391
+ },
392
+ (t) => [index('attachment_orphans_age_idx').on(t.createdAt)],
393
+ )
394
+
395
+ export const wordFilters = pgTable('word_filters', {
396
+ id: integer('id').primaryKey().generatedByDefaultAsIdentity(),
397
+ pattern: text('pattern').notNull(),
398
+ replacement: text('replacement').notNull().default(''),
399
+ wholeWord: boolean('whole_word').notNull().default(true),
400
+ enabled: boolean('enabled').notNull().default(true),
401
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
402
+ })
403
+
404
+ export const smilies = pgTable(
405
+ 'smilies',
406
+ {
407
+ id: integer('id').primaryKey().generatedByDefaultAsIdentity(),
408
+ code: text('code').notNull(),
409
+ src: text('src').notNull(),
410
+ alt: text('alt'),
411
+ enabled: boolean('enabled').notNull().default(true),
412
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
413
+ },
414
+ (t) => [uniqueIndex('smilies_code_key').on(t.code)],
415
+ )
416
+
417
+ export const customDirectives = pgTable(
418
+ 'custom_directives',
419
+ {
420
+ id: integer('id').primaryKey().generatedByDefaultAsIdentity(),
421
+ name: text('name').notNull(),
422
+ block: boolean('block').notNull().default(false),
423
+ description: text('description'),
424
+ enabled: boolean('enabled').notNull().default(true),
425
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
426
+ },
427
+ (t) => [uniqueIndex('custom_directives_name_key').on(t.name)],
428
+ )
429
+
430
+ export const announcements = pgTable(
431
+ 'announcements',
432
+ {
433
+ id: integer('id').primaryKey().generatedByDefaultAsIdentity(),
434
+ forumId: integer('forum_id').references(() => forums.id, { onDelete: 'cascade' }),
435
+ title: text('title').notNull(),
436
+ message: text('message').notNull(),
437
+ bodyFormat: smallint('body_format').notNull().default(1),
438
+ authorUserId: integer('author_user_id').references(() => users.id, {
439
+ onDelete: 'set null',
440
+ }),
441
+ authorUsername: text('author_username').notNull().default(''),
442
+ startsAt: timestamp('starts_at', { withTimezone: true }).notNull().defaultNow(),
443
+ endsAt: timestamp('ends_at', { withTimezone: true }),
444
+ enabled: boolean('enabled').notNull().default(true),
445
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
446
+ },
447
+ (t) => [
448
+ index('announcements_live_idx').on(t.startsAt.desc()).where(sql`${t.enabled}`),
449
+ index('announcements_forum_idx').on(t.forumId),
450
+ ],
451
+ )