seed.ts
| 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 | */ |
| 7 | import { Database } from "bun:sqlite"; |
| 8 | import { mkdirSync } from "node:fs"; |
| 9 | import path from "node:path"; |
| 10 | import * as argon2 from "argon2"; |
| 11 | import { ADMIN_USERNAME, DB_PATH, REPOS_DIR } from "../constants.ts"; |
| 12 | |
| 13 | const db = new Database(DB_PATH); |
| 14 | db.run("PRAGMA journal_mode=WAL"); |
| 15 | db.run("PRAGMA foreign_keys=ON"); |
| 16 | |
| 17 | // ── Resolve admin user ──────────────────────────────────────────────────────── |
| 18 | const adminRow = db |
| 19 | .query<{ id: number }, [string]>("SELECT id FROM users WHERE username = ?") |
| 20 | .get(ADMIN_USERNAME); |
| 21 | if (!adminRow) { |
| 22 | console.error("Admin user not found. Run `bun run db:init` first."); |
| 23 | process.exit(1); |
| 24 | } |
| 25 | const adminId = adminRow.id; |
| 26 | |
| 27 | // Ensure a second user "alice" exists for variety |
| 28 | let aliceId: number; |
| 29 | const aliceRow = db |
| 30 | .query<{ id: number }, [string]>("SELECT id FROM users WHERE username = ?") |
| 31 | .get("alice"); |
| 32 | if (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 ──────────────────────────────────────────────────────────────────── |
| 50 | function 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) ───────────────────────── |
| 76 | const 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 | |
| 104 | for (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 ──────────────────────────────────── |
| 110 | const demoRepoId = getOrCreateRepo( |
| 111 | "demo", |
| 112 | "Demo repository for pagination testing", |
| 113 | ); |
| 114 | |
| 115 | // Seed 35 issues (mix of open and closed) |
| 116 | const issueCount = |
| 117 | db |
| 118 | .query<{ n: number }, [number]>( |
| 119 | "SELECT COUNT(*) as n FROM issues WHERE repo_id = ?", |
| 120 | ) |
| 121 | .get(demoRepoId)?.n ?? 0; |
| 122 | if (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 |
| 185 | const patchCount = |
| 186 | db |
| 187 | .query<{ n: number }, [number]>( |
| 188 | "SELECT COUNT(*) as n FROM patches WHERE repo_id = ?", |
| 189 | ) |
| 190 | .get(demoRepoId)?.n ?? 0; |
| 191 | if (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 | |
| 255 | db.close(); |
| 256 | console.log("Seed complete."); |
| 257 |