@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,699 @@
1
+ import { sql } from 'drizzle-orm'
2
+ import {
3
+ bigint,
4
+ boolean,
5
+ index,
6
+ integer,
7
+ jsonb,
8
+ pgTable,
9
+ primaryKey,
10
+ smallint,
11
+ text,
12
+ timestamp,
13
+ uniqueIndex,
14
+ } from 'drizzle-orm/pg-core'
15
+
16
+ import { groupPermissionColumns } from './permission-columns'
17
+
18
+ export const usergroups = pgTable(
19
+ 'usergroups',
20
+ {
21
+ id: integer('id').primaryKey().generatedByDefaultAsIdentity(),
22
+
23
+ key: text('key').notNull(),
24
+ title: text('title').notNull(),
25
+ description: text('description'),
26
+
27
+ badgeToken: text('badge_token'),
28
+
29
+ nameColorLight: text('name_color_light'),
30
+ nameColorDark: text('name_color_dark'),
31
+
32
+ badgeImageLight: text('badge_image_light'),
33
+ badgeImageDark: text('badge_image_dark'),
34
+
35
+ displayOrder: integer('display_order').notNull().default(0),
36
+
37
+ isSystem: boolean('is_system').notNull().default(false),
38
+
39
+ isStaffGroup: boolean('is_staff_group').notNull().default(false),
40
+
41
+ pluginGrantable: boolean('plugin_grantable').notNull().default(false),
42
+
43
+ ...groupPermissionColumns(),
44
+
45
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
46
+ updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
47
+ },
48
+ (t) => [
49
+ uniqueIndex('usergroups_key_key').on(t.key),
50
+ index('usergroups_display_order_idx').on(t.displayOrder),
51
+ ],
52
+ )
53
+
54
+ export const USER_STATES = [
55
+ 'active',
56
+ 'awaiting_activation',
57
+ 'awaiting_approval',
58
+ 'banned',
59
+ 'deleted',
60
+ ] as const
61
+
62
+ export const users = pgTable(
63
+ 'users',
64
+ {
65
+ id: integer('id').primaryKey().generatedByDefaultAsIdentity(),
66
+
67
+ username: text('username').notNull(),
68
+ usernameLower: text('username_lower').notNull(),
69
+
70
+ email: text('email').notNull(),
71
+ emailLower: text('email_lower').notNull(),
72
+
73
+ passwordHash: text('password_hash'),
74
+ passwordAlgo: text('password_algo'),
75
+ passwordChangedAt: timestamp('password_changed_at', { withTimezone: true }),
76
+
77
+ primaryGroupId: integer('primary_group_id')
78
+ .notNull()
79
+ .references(() => usergroups.id, { onDelete: 'restrict' }),
80
+ displayGroupId: integer('display_group_id').references(() => usergroups.id, {
81
+ onDelete: 'set null',
82
+ }),
83
+
84
+ state: text('state').notNull().default('active'),
85
+
86
+ postCount: integer('post_count').notNull().default(0),
87
+ threadCount: integer('thread_count').notNull().default(0),
88
+ reputation: integer('reputation').notNull().default(0),
89
+ warningPoints: smallint('warning_points').notNull().default(0),
90
+
91
+ signature: text('signature').notNull().default(''),
92
+ signatureHtml: text('signature_html'),
93
+ signatureRenderVersion: smallint('signature_render_version').notNull().default(0),
94
+ signatureFormat: smallint('signature_format').notNull().default(1),
95
+ signatureLocked: boolean('signature_locked').notNull().default(false),
96
+ signatureLockedReason: text('signature_locked_reason'),
97
+
98
+ avatarStatus: text('avatar_status').notNull().default('none'),
99
+ avatarKey: text('avatar_key'),
100
+ avatarSourceKey: text('avatar_source_key'),
101
+ avatarWidth: integer('avatar_width'),
102
+ avatarHeight: integer('avatar_height'),
103
+ avatarFailureReason: text('avatar_failure_reason'),
104
+ avatarUpdatedAt: timestamp('avatar_updated_at', { withTimezone: true }),
105
+ avatarLocked: boolean('avatar_locked').notNull().default(false),
106
+ avatarLockedReason: text('avatar_locked_reason'),
107
+
108
+ registrationIpPrefix: text('registration_ip_prefix'),
109
+ lastIpPrefix: text('last_ip_prefix'),
110
+
111
+ lastActiveAt: timestamp('last_active_at', { withTimezone: true }),
112
+ emailVerifiedAt: timestamp('email_verified_at', { withTimezone: true }),
113
+
114
+ suspendedPostingUntil: timestamp('suspended_posting_until', { withTimezone: true }),
115
+ moderatedPostingUntil: timestamp('moderated_posting_until', { withTimezone: true }),
116
+
117
+ timezone: text('timezone').notNull().default('auto'),
118
+ locale: text('locale').notNull().default('auto'),
119
+ postsPerPage: smallint('posts_per_page'),
120
+ threadsPerPage: smallint('threads_per_page'),
121
+ invisible: boolean('invisible').notNull().default(false),
122
+
123
+ location: text('location'),
124
+ website: text('website'),
125
+ bio: text('bio'),
126
+
127
+ deletedAt: timestamp('deleted_at', { withTimezone: true }),
128
+
129
+ legacyId: integer('legacy_id'),
130
+
131
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
132
+ updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
133
+ },
134
+ (t) => [
135
+ uniqueIndex('users_username_lower_key').on(t.usernameLower),
136
+ uniqueIndex('users_email_lower_key').on(t.emailLower),
137
+ uniqueIndex('users_legacy_id_key').on(t.legacyId).where(sql`${t.legacyId} is not null`),
138
+ index('users_primary_group_idx').on(t.primaryGroupId),
139
+ index('users_active_created_idx').on(t.createdAt).where(sql`${t.state} = 'active'`),
140
+ index('users_last_active_idx').on(t.lastActiveAt),
141
+ index('users_signature_render_version_idx')
142
+ .on(t.signatureRenderVersion, t.id)
143
+ .where(sql`${t.signature} <> ''`),
144
+ ],
145
+ )
146
+
147
+ export const userGroupMemberships = pgTable(
148
+ 'user_group_memberships',
149
+ {
150
+ userId: integer('user_id')
151
+ .notNull()
152
+ .references(() => users.id, { onDelete: 'cascade' }),
153
+ groupId: integer('group_id')
154
+ .notNull()
155
+ .references(() => usergroups.id, { onDelete: 'cascade' }),
156
+
157
+ isDisplayGroup: boolean('is_display_group').notNull().default(false),
158
+
159
+ grantedByUserId: integer('granted_by_user_id').references(() => users.id, {
160
+ onDelete: 'set null',
161
+ }),
162
+ grantedAt: timestamp('granted_at', { withTimezone: true }).notNull().defaultNow(),
163
+
164
+ expiresAt: timestamp('expires_at', { withTimezone: true }),
165
+ grantedByPlugin: text('granted_by_plugin'),
166
+ grantReason: text('grant_reason'),
167
+ previousPrimaryGroupId: integer('previous_primary_group_id').references(() => usergroups.id, {
168
+ onDelete: 'set null',
169
+ }),
170
+ },
171
+ (t) => [
172
+ uniqueIndex('user_group_memberships_pkey').on(t.userId, t.groupId),
173
+ index('user_group_memberships_user_idx').on(t.userId),
174
+ index('user_group_memberships_group_idx').on(t.groupId),
175
+ index('user_group_memberships_expiry_idx')
176
+ .on(t.expiresAt)
177
+ .where(sql`${t.expiresAt} is not null`),
178
+ index('user_group_memberships_previous_primary_idx')
179
+ .on(t.userId)
180
+ .where(sql`${t.previousPrimaryGroupId} is not null`),
181
+ ],
182
+ )
183
+
184
+ export const sessions = pgTable(
185
+ 'sessions',
186
+ {
187
+ id: integer('id').primaryKey().generatedByDefaultAsIdentity(),
188
+
189
+ tokenHash: text('token_hash').notNull(),
190
+
191
+ userId: integer('user_id').references(() => users.id, {
192
+ onDelete: 'cascade',
193
+ }),
194
+
195
+ locationPath: text('location_path'),
196
+ locationForumId: integer('location_forum_id'),
197
+ locationThreadId: integer('location_thread_id'),
198
+
199
+ ipPrefix: text('ip_prefix'),
200
+ userAgent: text('user_agent'),
201
+
202
+ supersededBySessionId: integer('superseded_by_session_id'),
203
+ credentialProvedAt: timestamp('credential_proved_at', { withTimezone: true }),
204
+
205
+ lastSeenAt: timestamp('last_seen_at', { withTimezone: true }).notNull().defaultNow(),
206
+ expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),
207
+ revokedAt: timestamp('revoked_at', { withTimezone: true }),
208
+
209
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
210
+ },
211
+ (t) => [
212
+ uniqueIndex('sessions_token_hash_key').on(t.tokenHash),
213
+ index('sessions_last_seen_idx').on(t.lastSeenAt),
214
+ index('sessions_user_idx').on(t.userId),
215
+ index('sessions_expires_idx').on(t.expiresAt),
216
+ ],
217
+ )
218
+
219
+ export const rememberTokens = pgTable(
220
+ 'remember_tokens',
221
+ {
222
+ id: integer('id').primaryKey().generatedByDefaultAsIdentity(),
223
+
224
+ tokenHash: text('token_hash').notNull(),
225
+
226
+ familyId: text('family_id').notNull(),
227
+
228
+ userId: integer('user_id')
229
+ .notNull()
230
+ .references(() => users.id, { onDelete: 'cascade' }),
231
+
232
+ usedAt: timestamp('used_at', { withTimezone: true }),
233
+ revokedAt: timestamp('revoked_at', { withTimezone: true }),
234
+ revokedReason: text('revoked_reason'),
235
+
236
+ expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),
237
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
238
+ },
239
+ (t) => [
240
+ uniqueIndex('remember_tokens_hash_key').on(t.tokenHash),
241
+ index('remember_tokens_family_idx').on(t.familyId),
242
+ index('remember_tokens_user_idx').on(t.userId),
243
+ index('remember_tokens_expires_idx').on(t.expiresAt),
244
+ ],
245
+ )
246
+
247
+ export const userIdentities = pgTable(
248
+ 'user_identities',
249
+ {
250
+ id: integer('id').primaryKey().generatedByDefaultAsIdentity(),
251
+
252
+ userId: integer('user_id')
253
+ .notNull()
254
+ .references(() => users.id, { onDelete: 'cascade' }),
255
+
256
+ provider: text('provider').notNull(),
257
+ subject: text('subject').notNull(),
258
+ label: text('label'),
259
+
260
+ linkedAt: timestamp('linked_at', { withTimezone: true }).notNull().defaultNow(),
261
+ lastUsedAt: timestamp('last_used_at', { withTimezone: true }),
262
+ },
263
+ (t) => [
264
+ uniqueIndex('user_identities_provider_subject_key').on(t.provider, t.subject),
265
+ index('user_identities_user_idx').on(t.userId, t.id),
266
+ ],
267
+ )
268
+
269
+ export const passkeys = pgTable(
270
+ 'passkeys',
271
+ {
272
+ id: integer('id').primaryKey().generatedByDefaultAsIdentity(),
273
+
274
+ userId: integer('user_id')
275
+ .notNull()
276
+ .references(() => users.id, { onDelete: 'cascade' }),
277
+
278
+ credentialId: text('credential_id').notNull(),
279
+ publicKey: text('public_key').notNull(),
280
+ signCount: integer('sign_count').notNull().default(0),
281
+
282
+ label: text('label').notNull(),
283
+ transports: text('transports'),
284
+
285
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
286
+ lastUsedAt: timestamp('last_used_at', { withTimezone: true }),
287
+ },
288
+ (t) => [
289
+ uniqueIndex('passkeys_credential_id_key').on(t.credentialId),
290
+ index('passkeys_user_idx').on(t.userId, t.id),
291
+ ],
292
+ )
293
+
294
+ export const userTwoFactor = pgTable('user_two_factor', {
295
+ userId: integer('user_id')
296
+ .primaryKey()
297
+ .references(() => users.id, { onDelete: 'cascade' }),
298
+
299
+ sealedSecret: text('sealed_secret').notNull(),
300
+
301
+ confirmedAt: timestamp('confirmed_at', { withTimezone: true }),
302
+
303
+ lastStep: bigint('last_step', { mode: 'number' }),
304
+
305
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
306
+ })
307
+
308
+ export const recoveryCodes = pgTable(
309
+ 'recovery_codes',
310
+ {
311
+ id: integer('id').primaryKey().generatedByDefaultAsIdentity(),
312
+
313
+ userId: integer('user_id')
314
+ .notNull()
315
+ .references(() => users.id, { onDelete: 'cascade' }),
316
+
317
+ codeHash: text('code_hash').notNull(),
318
+ usedAt: timestamp('used_at', { withTimezone: true }),
319
+
320
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
321
+ },
322
+ (t) => [
323
+ uniqueIndex('recovery_codes_user_hash_key').on(t.userId, t.codeHash),
324
+ index('recovery_codes_unused_idx').on(t.userId).where(sql`${t.usedAt} is null`),
325
+ ],
326
+ )
327
+
328
+ export const authEvents = pgTable(
329
+ 'auth_events',
330
+ {
331
+ id: integer('id').primaryKey().generatedByDefaultAsIdentity(),
332
+
333
+ userId: integer('user_id').references(() => users.id, { onDelete: 'cascade' }),
334
+
335
+ kind: text('kind').notNull(),
336
+
337
+ ipPrefix: text('ip_prefix'),
338
+ userAgent: text('user_agent'),
339
+
340
+ detail: jsonb('detail').notNull().default({}),
341
+
342
+ at: timestamp('at', { withTimezone: true }).notNull().defaultNow(),
343
+ },
344
+ (t) => [
345
+ index('auth_events_user_idx').on(t.userId, t.id.desc()),
346
+ index('auth_events_recent_idx').on(t.id.desc()),
347
+ index('auth_events_kind_idx').on(t.kind, t.id.desc()),
348
+ ],
349
+ )
350
+
351
+ export const loginAttempts = pgTable(
352
+ 'login_attempts',
353
+ {
354
+ id: integer('id').primaryKey().generatedByDefaultAsIdentity(),
355
+
356
+ bucket: text('bucket').notNull(),
357
+ succeeded: boolean('succeeded').notNull(),
358
+ occurredAt: timestamp('occurred_at', { withTimezone: true }).notNull().defaultNow(),
359
+ },
360
+ (t) => [index('login_attempts_bucket_idx').on(t.bucket, t.occurredAt)],
361
+ )
362
+
363
+ export const credentialTokens = pgTable(
364
+ 'credential_tokens',
365
+ {
366
+ id: integer('id').primaryKey().generatedByDefaultAsIdentity(),
367
+ tokenHash: text('token_hash').notNull(),
368
+
369
+ userId: integer('user_id')
370
+ .notNull()
371
+ .references(() => users.id, { onDelete: 'cascade' }),
372
+
373
+ purpose: text('purpose').notNull(),
374
+ payload: text('payload'),
375
+
376
+ consumedAt: timestamp('consumed_at', { withTimezone: true }),
377
+ expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),
378
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
379
+ },
380
+ (t) => [
381
+ uniqueIndex('credential_tokens_hash_key').on(t.tokenHash),
382
+ index('credential_tokens_user_purpose_idx').on(t.userId, t.purpose),
383
+ index('credential_tokens_expires_idx').on(t.expiresAt),
384
+ ],
385
+ )
386
+
387
+ export const bans = pgTable(
388
+ 'bans',
389
+ {
390
+ id: integer('id').primaryKey().generatedByDefaultAsIdentity(),
391
+
392
+ userId: integer('user_id')
393
+ .notNull()
394
+ .references(() => users.id, { onDelete: 'cascade' }),
395
+
396
+ bannedByUserId: integer('banned_by_user_id').references(() => users.id, {
397
+ onDelete: 'set null',
398
+ }),
399
+
400
+ reason: text('reason'),
401
+ publicReason: text('public_reason'),
402
+
403
+ previousPrimaryGroupId: integer('previous_primary_group_id').references(() => usergroups.id, {
404
+ onDelete: 'set null',
405
+ }),
406
+
407
+ expiresAt: timestamp('expires_at', { withTimezone: true }),
408
+ liftedAt: timestamp('lifted_at', { withTimezone: true }),
409
+
410
+ legacyId: integer('legacy_id'),
411
+
412
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
413
+ },
414
+ (t) => [
415
+ uniqueIndex('bans_active_user_key').on(t.userId).where(sql`${t.liftedAt} is null`),
416
+ index('bans_expires_idx').on(t.expiresAt).where(sql`${t.liftedAt} is null`),
417
+ uniqueIndex('bans_legacy_id_key').on(t.legacyId).where(sql`${t.legacyId} is not null`),
418
+ ],
419
+ )
420
+
421
+ export const banFilters = pgTable(
422
+ 'ban_filters',
423
+ {
424
+ id: integer('id').primaryKey().generatedByDefaultAsIdentity(),
425
+ type: text('type').notNull(),
426
+ pattern: text('pattern').notNull(),
427
+ note: text('note'),
428
+ createdByUserId: integer('created_by_user_id').references(() => users.id, {
429
+ onDelete: 'set null',
430
+ }),
431
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
432
+ },
433
+ (t) => [
434
+ index('ban_filters_type_idx').on(t.type),
435
+ uniqueIndex('ban_filters_type_pattern_key').on(t.type, t.pattern),
436
+ ],
437
+ )
438
+
439
+ export const groupPromotions = pgTable(
440
+ 'group_promotions',
441
+ {
442
+ id: integer('id').primaryKey().generatedByDefaultAsIdentity(),
443
+
444
+ title: text('title').notNull(),
445
+ enabled: boolean('enabled').notNull().default(true),
446
+ displayOrder: integer('display_order').notNull().default(0),
447
+
448
+ minPostCount: integer('min_post_count'),
449
+ minReputation: integer('min_reputation'),
450
+ minDaysRegistered: integer('min_days_registered'),
451
+
452
+ fromPrimaryGroupId: integer('from_primary_group_id').references(() => usergroups.id, {
453
+ onDelete: 'cascade',
454
+ }),
455
+ toPrimaryGroupId: integer('to_primary_group_id')
456
+ .notNull()
457
+ .references(() => usergroups.id, { onDelete: 'cascade' }),
458
+
459
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
460
+ updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
461
+ },
462
+ (t) => [index('group_promotions_order_idx').on(t.displayOrder, t.id)],
463
+ )
464
+
465
+ export const warningTypes = pgTable(
466
+ 'warning_types',
467
+ {
468
+ id: integer('id').primaryKey().generatedByDefaultAsIdentity(),
469
+ title: text('title').notNull(),
470
+ points: smallint('points').notNull().default(1),
471
+ expiryDays: integer('expiry_days'),
472
+ isActive: boolean('is_active').notNull().default(true),
473
+ displayOrder: integer('display_order').notNull().default(0),
474
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
475
+ updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
476
+ },
477
+ (t) => [index('warning_types_order_idx').on(t.displayOrder, t.id)],
478
+ )
479
+
480
+ export const warningLevels = pgTable(
481
+ 'warning_levels',
482
+ {
483
+ id: integer('id').primaryKey().generatedByDefaultAsIdentity(),
484
+ points: smallint('points').notNull(),
485
+ action: text('action').notNull(),
486
+ durationDays: integer('duration_days'),
487
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
488
+ },
489
+ (t) => [uniqueIndex('warning_levels_points_unique').on(t.points)],
490
+ )
491
+
492
+ export const warnings = pgTable(
493
+ 'warnings',
494
+ {
495
+ id: integer('id').primaryKey().generatedByDefaultAsIdentity(),
496
+
497
+ userId: integer('user_id')
498
+ .notNull()
499
+ .references(() => users.id, { onDelete: 'cascade' }),
500
+ issuedByUserId: integer('issued_by_user_id').references(() => users.id, {
501
+ onDelete: 'set null',
502
+ }),
503
+
504
+ typeId: integer('type_id').references(() => warningTypes.id, { onDelete: 'set null' }),
505
+ title: text('title').notNull(),
506
+ points: smallint('points').notNull(),
507
+ reason: text('reason').notNull().default(''),
508
+
509
+ postId: integer('post_id'),
510
+ reportId: integer('report_id'),
511
+
512
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
513
+ expiresAt: timestamp('expires_at', { withTimezone: true }),
514
+
515
+ revokedAt: timestamp('revoked_at', { withTimezone: true }),
516
+ revokedByUserId: integer('revoked_by_user_id').references(() => users.id, {
517
+ onDelete: 'set null',
518
+ }),
519
+ revokeReason: text('revoke_reason'),
520
+
521
+ legacyId: integer('legacy_id'),
522
+ },
523
+ (t) => [
524
+ index('warnings_user_idx').on(t.userId, t.createdAt, t.id),
525
+ uniqueIndex('warnings_legacy_id_key').on(t.legacyId).where(sql`${t.legacyId} is not null`),
526
+ ],
527
+ )
528
+
529
+ export const notifications = pgTable(
530
+ 'notifications',
531
+ {
532
+ id: integer('id').primaryKey().generatedByDefaultAsIdentity(),
533
+
534
+ userId: integer('user_id')
535
+ .notNull()
536
+ .references(() => users.id, { onDelete: 'cascade' }),
537
+
538
+ kind: text('kind').notNull(),
539
+ data: jsonb('data').notNull().default({}),
540
+ href: text('href'),
541
+
542
+ dedupeKey: text('dedupe_key'),
543
+ occurrences: integer('occurrences').notNull().default(1),
544
+
545
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
546
+ updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
547
+ readAt: timestamp('read_at', { withTimezone: true }),
548
+ emailSentAt: timestamp('email_sent_at', { withTimezone: true }),
549
+ pushSentAt: timestamp('push_sent_at', { withTimezone: true }),
550
+ },
551
+ (t) => [
552
+ index('notifications_user_idx').on(t.userId, t.createdAt.desc(), t.id.desc()),
553
+ index('notifications_unread_idx').on(t.userId).where(sql`${t.readAt} is null`),
554
+ uniqueIndex('notifications_dedupe_idx')
555
+ .on(t.userId, t.dedupeKey)
556
+ .where(sql`${t.readAt} is null and ${t.dedupeKey} is not null`),
557
+ ],
558
+ )
559
+
560
+ export const notificationPreferences = pgTable(
561
+ 'notification_preferences',
562
+ {
563
+ userId: integer('user_id')
564
+ .notNull()
565
+ .references(() => users.id, { onDelete: 'cascade' }),
566
+ kind: text('kind').notNull(),
567
+ email: boolean('email'),
568
+ push: boolean('push'),
569
+ updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
570
+ },
571
+ (t) => [primaryKey({ name: 'notification_preferences_pkey', columns: [t.userId, t.kind] })],
572
+ )
573
+
574
+ export const pushSubscriptions = pgTable(
575
+ 'push_subscriptions',
576
+ {
577
+ id: integer('id').primaryKey().generatedByDefaultAsIdentity(),
578
+
579
+ userId: integer('user_id')
580
+ .notNull()
581
+ .references(() => users.id, { onDelete: 'cascade' }),
582
+
583
+ endpoint: text('endpoint').notNull(),
584
+ p256dh: text('p256dh').notNull(),
585
+ auth: text('auth').notNull(),
586
+
587
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
588
+ lastSeenAt: timestamp('last_seen_at', { withTimezone: true }).notNull().defaultNow(),
589
+ },
590
+ (t) => [
591
+ uniqueIndex('push_subscriptions_endpoint_key').on(t.endpoint),
592
+ index('push_subscriptions_user_idx').on(t.userId, t.id),
593
+ ],
594
+ )
595
+
596
+ export const profileFields = pgTable(
597
+ 'profile_fields',
598
+ {
599
+ id: integer('id').primaryKey().generatedByDefaultAsIdentity(),
600
+
601
+ key: text('key').notNull(),
602
+ label: text('label').notNull(),
603
+ description: text('description'),
604
+
605
+ type: text('type').notNull().default('text'),
606
+ options: jsonb('options').notNull().default([]),
607
+ maxLength: integer('max_length'),
608
+
609
+ displayOrder: integer('display_order').notNull().default(0),
610
+ isActive: boolean('is_active').notNull().default(true),
611
+ requiredAtRegistration: boolean('required_at_registration').notNull().default(false),
612
+
613
+ defaultVisible: boolean('default_visible').notNull().default(true),
614
+ defaultEditable: boolean('default_editable').notNull().default(true),
615
+
616
+ showInPostbit: boolean('show_in_postbit').notNull().default(false),
617
+
618
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
619
+ updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
620
+ },
621
+ (t) => [
622
+ uniqueIndex('profile_fields_key_unique').on(t.key),
623
+ index('profile_fields_order_idx').on(t.displayOrder, t.id),
624
+ ],
625
+ )
626
+
627
+ export const profileFieldGroups = pgTable(
628
+ 'profile_field_groups',
629
+ {
630
+ fieldId: integer('field_id')
631
+ .notNull()
632
+ .references(() => profileFields.id, { onDelete: 'cascade' }),
633
+ groupId: integer('group_id')
634
+ .notNull()
635
+ .references(() => usergroups.id, { onDelete: 'cascade' }),
636
+ canView: boolean('can_view'),
637
+ canEdit: boolean('can_edit'),
638
+ },
639
+ (t) => [primaryKey({ name: 'profile_field_groups_pkey', columns: [t.fieldId, t.groupId] })],
640
+ )
641
+
642
+ export const profileFieldValues = pgTable(
643
+ 'profile_field_values',
644
+ {
645
+ userId: integer('user_id')
646
+ .notNull()
647
+ .references(() => users.id, { onDelete: 'cascade' }),
648
+ fieldId: integer('field_id')
649
+ .notNull()
650
+ .references(() => profileFields.id, { onDelete: 'cascade' }),
651
+ value: text('value').notNull().default(''),
652
+ updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
653
+ },
654
+ (t) => [
655
+ primaryKey({ name: 'profile_field_values_pkey', columns: [t.userId, t.fieldId] }),
656
+ index('profile_field_values_user_idx').on(t.userId),
657
+ ],
658
+ )
659
+
660
+ export const userRelations = pgTable(
661
+ 'user_relations',
662
+ {
663
+ userId: integer('user_id')
664
+ .notNull()
665
+ .references(() => users.id, { onDelete: 'cascade' }),
666
+ otherUserId: integer('other_user_id')
667
+ .notNull()
668
+ .references(() => users.id, { onDelete: 'cascade' }),
669
+ kind: text('kind').notNull(),
670
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
671
+ },
672
+ (t) => [
673
+ primaryKey({ name: 'user_relations_pkey', columns: [t.userId, t.otherUserId] }),
674
+ index('user_relations_kind_idx').on(t.userId, t.kind, t.otherUserId),
675
+ index('user_relations_reverse_idx').on(t.otherUserId, t.kind),
676
+ ],
677
+ )
678
+
679
+ export const adminSessions = pgTable(
680
+ 'admin_sessions',
681
+ {
682
+ id: integer('id').primaryKey().generatedByDefaultAsIdentity(),
683
+ userId: integer('user_id')
684
+ .notNull()
685
+ .references(() => users.id, { onDelete: 'cascade' }),
686
+ tokenHash: text('token_hash').notNull(),
687
+ ipPrefix: text('ip_prefix'),
688
+ authenticatedAt: timestamp('authenticated_at', { withTimezone: true }).notNull().defaultNow(),
689
+ lastSeenAt: timestamp('last_seen_at', { withTimezone: true }).notNull().defaultNow(),
690
+ expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),
691
+ revokedAt: timestamp('revoked_at', { withTimezone: true }),
692
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
693
+ },
694
+ (t) => [
695
+ uniqueIndex('admin_sessions_token_hash_key').on(t.tokenHash),
696
+ index('admin_sessions_user_idx').on(t.userId),
697
+ index('admin_sessions_expiry_idx').on(t.expiresAt),
698
+ ],
699
+ )