seed.ts
⎇
Raw
1/**
2 * Seed script — populates the live database with enough data to test pagination.
3 * Run with: bun run db:seed
4 *
5 * Safe to run multiple times — skips existing items.
6 */
7import { Database } from "bun:sqlite";
8import { mkdirSync } from "node:fs";
9import path from "node:path";
10import * as argon2 from "argon2";
11import { ADMIN_USERNAME, DB_PATH, REPOS_DIR } from "../constants.ts";
12
13const db = new Database(DB_PATH);
14db.run("PRAGMA journal_mode=WAL");
15db.run("PRAGMA foreign_keys=ON");
16
17// ── Resolve admin user ────────────────────────────────────────────────────────
18const adminRow = db
19 .query<{ id: number }, [string]>("SELECT id FROM users WHERE username = ?")
20 .get(ADMIN_USERNAME);
21if (!adminRow) {
22 console.error("Admin user not found. Run `bun run db:init` first.");
23 process.exit(1);
24}
25const adminId = adminRow.id;
26
27// Ensure a second user "alice" exists for variety
28let aliceId: number;
29const aliceRow = db
30 .query<{ id: number }, [string]>("SELECT id FROM users WHERE username = ?")
31 .get("alice");
32if (aliceRow) {
33 aliceId = aliceRow.id;
34} else {
35 const hash = await argon2.hash("password123");
36 const now = new Date().toISOString();
37 db.run(
38 "INSERT INTO users (username, password_hash, created_at) VALUES (?, ?, ?)",
39 ["alice", hash, now],
40 );
41 aliceId = db
42 .query<{ id: number }, [string]>(
43 "SELECT id FROM users WHERE username = ?",
44 )
45 .get("alice")!.id;
46 console.log("Created user alice (password: password123)");
47}
48
49// ── Helper ────────────────────────────────────────────────────────────────────
50function getOrCreateRepo(name: string, description: string): number {
51 const existing = db
52 .query<{ id: number }, [string]>(
53 "SELECT id FROM repositories WHERE name = ?",
54 )
55 .get(name);
56 if (existing) return existing.id;
57
58 const now = new Date().toISOString();
59 db.run(
60 "INSERT INTO repositories (name, description, is_private, default_branch, created_at) VALUES (?, ?, 0, 'main', ?)",
61 [name, description, now],
62 );
63 const repoPath = path.join(REPOS_DIR, `${name}.git`);
64 mkdirSync(repoPath, { recursive: true });
65 // init bare repo (sync-ish via Bun.spawnSync)
66 Bun.spawnSync(["git", "init", "--bare", "--initial-branch=main", repoPath]);
67 console.log(`Created repo: ${name}`);
68 return db
69 .query<{ id: number }, [string]>(
70 "SELECT id FROM repositories WHERE name = ?",
71 )
72 .get(name)!.id;
73}
74
75// ── 25 extra repositories (for repo list pagination) ─────────────────────────
76const topics = [
77 "A web framework",
78 "CLI toolkit",
79 "Database driver",
80 "Auth library",
81 "Test runner",
82 "Build system",
83 "Linter plugin",
84 "ORM layer",
85 "Cache client",
86 "Queue worker",
87 "API gateway",
88 "Graph engine",
89 "ML utilities",
90 "Crypto helpers",
91 "File watcher",
92 "Schema validator",
93 "Logger library",
94 "Rate limiter",
95 "Metrics exporter",
96 "Job scheduler",
97 "Config manager",
98 "Template engine",
99 "Markdown parser",
100 "Image resizer",
101 "Email sender",
102];
103
104for (let i = 1; i <= 25; i++) {
105 const n = String(i).padStart(2, "0");
106 getOrCreateRepo(`seed-repo-${n}`, topics[i - 1]!);
107}
108
109// ── "demo" repo — seed issues and patches ────────────────────────────────────
110const demoRepoId = getOrCreateRepo(
111 "demo",
112 "Demo repository for pagination testing",
113);
114
115// Seed 35 issues (mix of open and closed)
116const issueCount =
117 db
118 .query<{ n: number }, [number]>(
119 "SELECT COUNT(*) as n FROM issues WHERE repo_id = ?",
120 )
121 .get(demoRepoId)?.n ?? 0;
122if (issueCount < 35) {
123 const start = issueCount + 1;
124 const issueTitles = [
125 "Fix null pointer dereference in parser",
126 "Add dark mode support",
127 "Improve error messages",
128 "Upgrade dependencies to latest",
129 "Memory leak in connection pool",
130 "Race condition in event loop",
131 "Add pagination to commit log",
132 "Slow query on large datasets",
133 "Missing CORS headers",
134 "Typo in README",
135 "Refactor authentication middleware",
136 "Support IPv6 addresses",
137 "Add unit tests for utils",
138 "Broken link in documentation",
139 "Config file not loaded on Windows",
140 "Infinite loop when input is empty",
141 "Add rate limiting",
142 "Log rotation broken",
143 "Session cookie not cleared on logout",
144 "Crash on malformed JSON input",
145 "Add export to CSV feature",
146 "Support custom themes",
147 "API returns 500 on edge case",
148 "Update license to MIT",
149 "Improve startup time",
150 "Handle timeout errors gracefully",
151 "Add health check endpoint",
152 "Support environment variables in config",
153 "Fix XSS in search",
154 "Stale cache after update",
155 "Add OpenAPI spec",
156 "Sort order wrong in list view",
157 "Binary file detection false positive",
158 "Column alignment off in table view",
159 "Wrong timezone in timestamps",
160 ];
161
162 for (let i = start; i <= 35; i++) {
163 const titleIndex = (i - 1) % issueTitles.length;
164 const status = i <= 25 ? "open" : "closed";
165 const authorId = i % 3 === 0 ? aliceId : adminId;
166 const now = new Date(Date.now() - (35 - i) * 3600_000).toISOString();
167 db.run(
168 "INSERT OR IGNORE INTO issues (repo_id, author_id, number, title, body, status, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
169 [
170 demoRepoId,
171 authorId,
172 i,
173 issueTitles[titleIndex]!,
174 `Details for issue #${i}.`,
175 status,
176 now,
177 now,
178 ],
179 );
180 }
181 console.log(`Seeded issues up to #35 in demo repo`);
182}
183
184// Seed 25 patches
185const patchCount =
186 db
187 .query<{ n: number }, [number]>(
188 "SELECT COUNT(*) as n FROM patches WHERE repo_id = ?",
189 )
190 .get(demoRepoId)?.n ?? 0;
191if (patchCount < 25) {
192 const start = patchCount + 1;
193 const SAMPLE_PATCH = [
194 "diff --git a/placeholder.txt b/placeholder.txt",
195 "new file mode 100644",
196 "index 0000000..e69de29",
197 "--- /dev/null",
198 "+++ b/placeholder.txt",
199 "@@ -0,0 +1 @@",
200 "+placeholder",
201 "",
202 ].join("\n");
203
204 const patchTitles = [
205 "Fix null deref",
206 "Add dark mode",
207 "Improve errors",
208 "Upgrade deps",
209 "Fix memory leak",
210 "Fix race condition",
211 "Add pagination",
212 "Optimise query",
213 "Add CORS headers",
214 "Fix typo",
215 "Refactor auth",
216 "Support IPv6",
217 "Add unit tests",
218 "Fix broken link",
219 "Fix Windows config",
220 "Fix infinite loop",
221 "Add rate limiting",
222 "Fix log rotation",
223 "Fix logout cookie",
224 "Handle bad JSON",
225 "Add CSV export",
226 "Custom themes",
227 "Fix 500 error",
228 "Update license",
229 "Improve startup",
230 ];
231
232 for (let i = start; i <= 25; i++) {
233 const titleIndex = (i - 1) % patchTitles.length;
234 const status = i <= 20 ? "open" : "closed";
235 const authorId = i % 3 === 0 ? aliceId : adminId;
236 const now = new Date(Date.now() - (25 - i) * 3600_000).toISOString();
237 db.run(
238 "INSERT OR IGNORE INTO patches (repo_id, author_id, number, title, description, patch_content, status, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
239 [
240 demoRepoId,
241 authorId,
242 i,
243 patchTitles[titleIndex]!,
244 `Description for patch #${i}.`,
245 SAMPLE_PATCH,
246 status,
247 now,
248 now,
249 ],
250 );
251 }
252 console.log(`Seeded patches up to #25 in demo repo`);
253}
254
255db.close();
256console.log("Seed complete.");
257