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