index.ts
⎇
Raw
1import { Database as BunDatabase } from "bun:sqlite";
2import { type Generated, Kysely, type Selectable } from "kysely";
3import { BunSqliteDialect } from "kysely-bun-sqlite";
4
5import { DB_PATH } from "../constants.ts";
6
7interface UserTable {
8 id: Generated<number>;
9 username: string;
10 password_hash: string | null;
11 created_at: string;
12 avatar_version: Generated<number>;
13 is_pending: Generated<number>;
14 register_application: string | null;
15}
16
17interface PasskeyTable {
18 id: Generated<number>;
19 user_id: number;
20 credential_id: string;
21 public_key: string;
22 counter: number;
23 created_at: string;
24}
25
26interface SessionTable {
27 id: string;
28 user_id: number;
29 expires_at: string;
30 created_at: string;
31}
32
33interface RepositoryTable {
34 id: Generated<number>;
35 name: string;
36 description: string | null;
37 is_private: number;
38 is_pinned: Generated<number>;
39 default_branch: string;
40 created_at: string;
41 issue_seq: Generated<number>;
42 patch_seq: Generated<number>;
43 issue_template: string | null;
44 patch_template: string | null;
45 allow_user_labels: Generated<number>;
46}
47
48interface IssueTable {
49 id: Generated<number>;
50 repo_id: number;
51 author_id: number | null;
52 number: number;
53 title: string;
54 body: string;
55 status: string;
56 created_at: string;
57 updated_at: string;
58 edited_at: string | null;
59}
60
61interface IssueCommentTable {
62 id: Generated<number>;
63 issue_id: number;
64 author_id: number | null;
65 body: string;
66 created_at: string;
67 edited_at: string | null;
68}
69
70interface IssueReactionTable {
71 id: Generated<number>;
72 issue_id: number;
73 comment_id: number | null;
74 user_id: number;
75 emoji: string;
76}
77
78interface PatchTable {
79 id: Generated<number>;
80 repo_id: number;
81 author_id: number | null;
82 number: number;
83 title: string;
84 description: string;
85 patch_content: string;
86 status: string;
87 author_name: string;
88 author_email: string;
89 created_at: string;
90 updated_at: string;
91 edited_at: string | null;
92 version: string;
93}
94
95interface PatchCommentTable {
96 id: Generated<number>;
97 patch_id: number;
98 author_id: number | null;
99 body: string;
100 created_at: string;
101 edited_at: string | null;
102}
103
104interface PatchReactionTable {
105 id: Generated<number>;
106 patch_id: number;
107 comment_id: number | null;
108 user_id: number;
109 emoji: string;
110}
111
112interface SshKeyTable {
113 id: Generated<number>;
114 user_id: number;
115 name: string;
116 public_key: string;
117 fingerprint: string;
118 created_at: string;
119}
120
121interface ReleaseTable {
122 id: Generated<number>;
123 repo_id: number;
124 tag_name: string | null;
125 name: string;
126 notes: string | null;
127 include_source_code: number;
128 created_at: string;
129}
130
131interface ReleaseAssetTable {
132 id: Generated<number>;
133 release_id: number;
134 filename: string;
135 size: number;
136 content_type: string;
137 created_at: string;
138}
139
140interface LabelTable {
141 id: Generated<number>;
142 repo_id: number;
143 name: string;
144 color: string;
145 created_at: string;
146}
147
148interface IssueLabelTable {
149 issue_id: number;
150 label_id: number;
151}
152
153interface PatchLabelTable {
154 patch_id: number;
155 label_id: number;
156}
157
158export interface Database {
159 users: UserTable;
160 passkeys: PasskeyTable;
161 sessions: SessionTable;
162 repositories: RepositoryTable;
163 issues: IssueTable;
164 issue_comments: IssueCommentTable;
165 issue_reactions: IssueReactionTable;
166 patches: PatchTable;
167 patch_comments: PatchCommentTable;
168 patch_reactions: PatchReactionTable;
169 ssh_keys: SshKeyTable;
170 releases: ReleaseTable;
171 release_assets: ReleaseAssetTable;
172 labels: LabelTable;
173 issue_labels: IssueLabelTable;
174 patch_labels: PatchLabelTable;
175}
176
177// Selectable row types (id is plain number, as returned by queries)
178export type UserRow = Selectable<UserTable>;
179export type PasskeyRow = Selectable<PasskeyTable>;
180export type SessionRow = Selectable<SessionTable>;
181export type RepositoryRow = Selectable<RepositoryTable>;
182export type IssueRow = Selectable<IssueTable>;
183export type IssueCommentRow = Selectable<IssueCommentTable>;
184export type IssueReactionRow = Selectable<IssueReactionTable>;
185export type PatchRow = Selectable<PatchTable>;
186export type PatchCommentRow = Selectable<PatchCommentTable>;
187export type PatchReactionRow = Selectable<PatchReactionTable>;
188export type SshKeyRow = Selectable<SshKeyTable>;
189export type ReleaseRow = Selectable<ReleaseTable>;
190export type ReleaseAssetRow = Selectable<ReleaseAssetTable>;
191export type LabelRow = Selectable<LabelTable>;
192
193const sqlite = new BunDatabase(DB_PATH);
194sqlite.run("PRAGMA journal_mode=WAL");
195sqlite.run("PRAGMA foreign_keys=ON");
196
197// Migration: add is_pending and register_application columns to users if missing
198const userCols = sqlite
199 .query<{ name: string }, []>("PRAGMA table_info(users)")
200 .all();
201if (!userCols.some((c) => c.name === "is_pending")) {
202 sqlite.run(
203 "ALTER TABLE users ADD COLUMN is_pending INTEGER NOT NULL DEFAULT 0",
204 );
205}
206if (!userCols.some((c) => c.name === "register_application")) {
207 sqlite.run("ALTER TABLE users ADD COLUMN register_application TEXT");
208}
209
210// Migration: add version column if missing, then populate any empty values
211const patchCols = sqlite
212 .query<{ name: string }, []>("PRAGMA table_info(patches)")
213 .all();
214if (!patchCols.some((c) => c.name === "version")) {
215 sqlite.run(
216 "ALTER TABLE patches ADD COLUMN version TEXT NOT NULL DEFAULT ''",
217 );
218}
219sqlite.run(
220 "UPDATE patches SET version = lower(hex(randomblob(16))) WHERE version = ''",
221);
222
223export const db = new Kysely<Database>({
224 dialect: new BunSqliteDialect({ database: sqlite }),
225});
226
227// Migration: add allow_user_labels column to repositories if missing
228const repoCols = sqlite
229 .query<{ name: string }, []>("PRAGMA table_info(repositories)")
230 .all();
231if (!repoCols.some((c) => c.name === "allow_user_labels")) {
232 sqlite.run(
233 "ALTER TABLE repositories ADD COLUMN allow_user_labels INTEGER NOT NULL DEFAULT 0",
234 );
235}
236
237// Migration: create labels tables if missing
238sqlite.run(`CREATE TABLE IF NOT EXISTS labels (
239 id INTEGER PRIMARY KEY AUTOINCREMENT,
240 repo_id INTEGER NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,
241 name TEXT NOT NULL,
242 color TEXT NOT NULL DEFAULT '#808080',
243 created_at TEXT NOT NULL,
244 UNIQUE(repo_id, name)
245)`);
246sqlite.run(`CREATE TABLE IF NOT EXISTS issue_labels (
247 issue_id INTEGER NOT NULL REFERENCES issues(id) ON DELETE CASCADE,
248 label_id INTEGER NOT NULL REFERENCES labels(id) ON DELETE CASCADE,
249 PRIMARY KEY (issue_id, label_id)
250)`);
251sqlite.run(`CREATE TABLE IF NOT EXISTS patch_labels (
252 patch_id INTEGER NOT NULL REFERENCES patches(id) ON DELETE CASCADE,
253 label_id INTEGER NOT NULL REFERENCES labels(id) ON DELETE CASCADE,
254 PRIMARY KEY (patch_id, label_id)
255)`);
256
257export async function getRepo(name: string, isAdmin: boolean) {
258 const repo = await db
259 .selectFrom("repositories")
260 .selectAll()
261 .where("name", "=", name)
262 .executeTakeFirst();
263 if (!repo) return null;
264 if (repo.is_private && !isAdmin) return null;
265 return repo;
266}
267