tangled
alpha
login
or
join now
malpercio.dev
/
atbb
5
fork
atom
WIP! A BB-style forum, on the ATmosphere! We're still working... we'll be back soon when we have something to show off!
node
typescript
hono
htmx
atproto
5
fork
atom
overview
issues
pulls
pipelines
feat(appview): define database schema for all 6 tables
malpercio.dev
1 month ago
c36eb09f
862e5c59
+148
1 changed file
expand all
collapse all
unified
split
packages
appview
src
db
schema.ts
+148
packages/appview/src/db/schema.ts
···
1
1
+
import {
2
2
+
pgTable,
3
3
+
bigserial,
4
4
+
text,
5
5
+
timestamp,
6
6
+
integer,
7
7
+
boolean,
8
8
+
bigint,
9
9
+
uniqueIndex,
10
10
+
index,
11
11
+
} from "drizzle-orm/pg-core";
12
12
+
13
13
+
// ── forums ──────────────────────────────────────────────
14
14
+
// Singleton forum metadata record, owned by Forum DID.
15
15
+
// Key: literal:self (rkey is always "self").
16
16
+
export const forums = pgTable(
17
17
+
"forums",
18
18
+
{
19
19
+
id: bigserial("id", { mode: "bigint" }).primaryKey(),
20
20
+
did: text("did").notNull(),
21
21
+
rkey: text("rkey").notNull(),
22
22
+
cid: text("cid").notNull(),
23
23
+
name: text("name").notNull(),
24
24
+
description: text("description"),
25
25
+
indexedAt: timestamp("indexed_at", { withTimezone: true }).notNull(),
26
26
+
},
27
27
+
(table) => [uniqueIndex("forums_did_rkey_idx").on(table.did, table.rkey)]
28
28
+
);
29
29
+
30
30
+
// ── categories ──────────────────────────────────────────
31
31
+
// Subforum / category definitions, owned by Forum DID.
32
32
+
export const categories = pgTable(
33
33
+
"categories",
34
34
+
{
35
35
+
id: bigserial("id", { mode: "bigint" }).primaryKey(),
36
36
+
did: text("did").notNull(),
37
37
+
rkey: text("rkey").notNull(),
38
38
+
cid: text("cid").notNull(),
39
39
+
name: text("name").notNull(),
40
40
+
description: text("description"),
41
41
+
slug: text("slug"),
42
42
+
sortOrder: integer("sort_order"),
43
43
+
forumId: bigint("forum_id", { mode: "bigint" }).references(() => forums.id),
44
44
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull(),
45
45
+
indexedAt: timestamp("indexed_at", { withTimezone: true }).notNull(),
46
46
+
},
47
47
+
(table) => [
48
48
+
uniqueIndex("categories_did_rkey_idx").on(table.did, table.rkey),
49
49
+
]
50
50
+
);
51
51
+
52
52
+
// ── users ───────────────────────────────────────────────
53
53
+
// Known AT Proto identities. Populated when any record
54
54
+
// from a DID is indexed. DID is the primary key.
55
55
+
export const users = pgTable("users", {
56
56
+
did: text("did").primaryKey(),
57
57
+
handle: text("handle"),
58
58
+
indexedAt: timestamp("indexed_at", { withTimezone: true }).notNull(),
59
59
+
});
60
60
+
61
61
+
// ── memberships ─────────────────────────────────────────
62
62
+
// User membership in a forum. Owned by user DID.
63
63
+
// `did` is both the record owner and the member.
64
64
+
export const memberships = pgTable(
65
65
+
"memberships",
66
66
+
{
67
67
+
id: bigserial("id", { mode: "bigint" }).primaryKey(),
68
68
+
did: text("did")
69
69
+
.notNull()
70
70
+
.references(() => users.did),
71
71
+
rkey: text("rkey").notNull(),
72
72
+
cid: text("cid").notNull(),
73
73
+
forumId: bigint("forum_id", { mode: "bigint" }).references(
74
74
+
() => forums.id
75
75
+
),
76
76
+
forumUri: text("forum_uri").notNull(),
77
77
+
role: text("role"),
78
78
+
roleUri: text("role_uri"),
79
79
+
joinedAt: timestamp("joined_at", { withTimezone: true }),
80
80
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull(),
81
81
+
indexedAt: timestamp("indexed_at", { withTimezone: true }).notNull(),
82
82
+
},
83
83
+
(table) => [
84
84
+
uniqueIndex("memberships_did_rkey_idx").on(table.did, table.rkey),
85
85
+
index("memberships_did_idx").on(table.did),
86
86
+
]
87
87
+
);
88
88
+
89
89
+
// ── posts ───────────────────────────────────────────────
90
90
+
// Unified post model. NULL root/parent = thread starter (topic).
91
91
+
// Non-null root/parent = reply. Mirrors app.bsky.feed.post pattern.
92
92
+
// Owned by user DID.
93
93
+
export const posts = pgTable(
94
94
+
"posts",
95
95
+
{
96
96
+
id: bigserial("id", { mode: "bigint" }).primaryKey(),
97
97
+
did: text("did")
98
98
+
.notNull()
99
99
+
.references(() => users.did),
100
100
+
rkey: text("rkey").notNull(),
101
101
+
cid: text("cid").notNull(),
102
102
+
text: text("text").notNull(),
103
103
+
forumUri: text("forum_uri"),
104
104
+
rootPostId: bigint("root_post_id", { mode: "bigint" }).references(
105
105
+
(): any => posts.id
106
106
+
),
107
107
+
parentPostId: bigint("parent_post_id", { mode: "bigint" }).references(
108
108
+
(): any => posts.id
109
109
+
),
110
110
+
rootUri: text("root_uri"),
111
111
+
parentUri: text("parent_uri"),
112
112
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull(),
113
113
+
indexedAt: timestamp("indexed_at", { withTimezone: true }).notNull(),
114
114
+
deleted: boolean("deleted").notNull().default(false),
115
115
+
},
116
116
+
(table) => [
117
117
+
uniqueIndex("posts_did_rkey_idx").on(table.did, table.rkey),
118
118
+
index("posts_forum_uri_idx").on(table.forumUri),
119
119
+
index("posts_root_post_id_idx").on(table.rootPostId),
120
120
+
]
121
121
+
);
122
122
+
123
123
+
// ── mod_actions ─────────────────────────────────────────
124
124
+
// Moderation actions, owned by Forum DID. Written by AppView
125
125
+
// on behalf of authorized moderators after role verification.
126
126
+
export const modActions = pgTable(
127
127
+
"mod_actions",
128
128
+
{
129
129
+
id: bigserial("id", { mode: "bigint" }).primaryKey(),
130
130
+
did: text("did").notNull(),
131
131
+
rkey: text("rkey").notNull(),
132
132
+
cid: text("cid").notNull(),
133
133
+
action: text("action").notNull(),
134
134
+
subjectDid: text("subject_did"),
135
135
+
subjectPostUri: text("subject_post_uri"),
136
136
+
forumId: bigint("forum_id", { mode: "bigint" }).references(
137
137
+
() => forums.id
138
138
+
),
139
139
+
reason: text("reason"),
140
140
+
createdBy: text("created_by").notNull(),
141
141
+
expiresAt: timestamp("expires_at", { withTimezone: true }),
142
142
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull(),
143
143
+
indexedAt: timestamp("indexed_at", { withTimezone: true }).notNull(),
144
144
+
},
145
145
+
(table) => [
146
146
+
uniqueIndex("mod_actions_did_rkey_idx").on(table.did, table.rkey),
147
147
+
]
148
148
+
);