devperf/backend/src/db/index.ts
zyc 44464dd334 feat: DevPerf Dashboard 研发人效看板 v1.0
- 后端:Bun + Hono + Drizzle ORM + SQLite
- 前端:Vue 3 + Naive UI + ECharts
- 项目管理:创建项目 + 绑定 Git 仓库
- OKR 系统:目标/关键结果 CRUD + 进度追踪
- Git 同步:Gitea API 自动同步 commit/PR + 作者关联
- 数据看板:项目 OKR 进度 + KR 状态分布 + 代码活动
- 权限体系:admin/manager/developer/viewer 四级
- Docker 部署:docker-compose + nginx

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 17:57:14 +08:00

214 lines
6.5 KiB
TypeScript

import { drizzle } from 'drizzle-orm/bun-sqlite';
import { Database } from 'bun:sqlite';
import { migrate } from 'drizzle-orm/bun-sqlite/migrator';
import * as schema from './schema';
import { config } from '../config';
import { mkdirSync, existsSync } from 'fs';
import { dirname, resolve } from 'path';
// Ensure data directory exists
const dbDir = dirname(config.DATABASE_PATH);
if (!existsSync(dbDir)) {
mkdirSync(dbDir, { recursive: true });
}
const sqlite = new Database(config.DATABASE_PATH, { create: true });
sqlite.exec('PRAGMA journal_mode = WAL');
sqlite.exec('PRAGMA foreign_keys = ON');
export const db = drizzle(sqlite, { schema });
export { sqlite };
/**
* Run database migrations automatically on startup.
* Uses CREATE TABLE IF NOT EXISTS semantics (via drizzle migrate)
* so it is safe to call on every boot -- already-applied migrations
* are tracked in the drizzle __drizzle_migrations journal table.
*
* The migrationsFolder path is resolved relative to the project root
* (where package.json lives) so it works regardless of cwd.
*/
function autoMigrate() {
try {
// Resolve the drizzle folder relative to this file's location:
// src/db/index.ts -> ../../drizzle
const migrationsFolder = resolve(import.meta.dir, '../../drizzle');
migrate(db, { migrationsFolder });
console.info('[DB] Auto-migration completed successfully.');
} catch (err) {
console.error('[DB] Auto-migration failed:', err);
// Do not crash the process -- tables may already exist from a prior run.
// Fallback: create core tables with raw SQL if drizzle migrations folder is missing.
try {
createTablesIfNotExist();
console.info('[DB] Fallback table creation completed.');
} catch (fallbackErr) {
console.error('[DB] Fallback table creation also failed:', fallbackErr);
}
}
}
/**
* Fallback: create all required tables using raw SQL CREATE TABLE IF NOT EXISTS.
* This is only used when the drizzle migrations folder cannot be found.
*/
function createTablesIfNotExist() {
sqlite.exec(`
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY NOT NULL,
plane_user_id TEXT,
display_name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
git_username TEXT,
role TEXT NOT NULL,
password_hash TEXT NOT NULL,
login_attempts INTEGER DEFAULT 0,
locked_until INTEGER,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS projects (
id TEXT PRIMARY KEY NOT NULL,
plane_project_id TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
identifier TEXT,
last_synced_at INTEGER,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS sprint_snapshots (
id TEXT PRIMARY KEY NOT NULL,
project_id TEXT REFERENCES projects(id),
plane_cycle_id TEXT NOT NULL,
name TEXT NOT NULL,
start_date TEXT,
end_date TEXT,
total_points INTEGER DEFAULT 0,
completed_points INTEGER DEFAULT 0,
total_issues INTEGER DEFAULT 0,
completed_issues INTEGER DEFAULT 0,
burndown_data TEXT,
status TEXT,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS task_snapshots (
id TEXT PRIMARY KEY NOT NULL,
plane_issue_id TEXT NOT NULL,
project_id TEXT REFERENCES projects(id),
sprint_id TEXT REFERENCES sprint_snapshots(id),
title TEXT NOT NULL,
status TEXT,
priority TEXT,
assignee_id TEXT REFERENCES users(id),
story_points INTEGER,
created_at INTEGER,
completed_at INTEGER,
due_date TEXT,
labels TEXT,
updated_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS milestones (
id TEXT PRIMARY KEY NOT NULL,
plane_module_id TEXT NOT NULL,
project_id TEXT REFERENCES projects(id),
name TEXT NOT NULL,
status TEXT,
target_date TEXT,
total_issues INTEGER DEFAULT 0,
completed_issues INTEGER DEFAULT 0,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS git_commits (
id TEXT PRIMARY KEY NOT NULL,
repo_name TEXT NOT NULL,
sha TEXT NOT NULL UNIQUE,
author_email TEXT,
author_name TEXT,
user_id TEXT REFERENCES users(id),
message TEXT,
additions INTEGER DEFAULT 0,
deletions INTEGER DEFAULT 0,
committed_at INTEGER NOT NULL,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS git_prs (
id TEXT PRIMARY KEY NOT NULL,
repo_name TEXT NOT NULL,
external_id INTEGER NOT NULL,
title TEXT,
user_id TEXT REFERENCES users(id),
author_username TEXT,
state TEXT,
additions INTEGER DEFAULT 0,
deletions INTEGER DEFAULT 0,
review_comments INTEGER DEFAULT 0,
created_at INTEGER,
merged_at INTEGER,
merge_time_hours REAL,
updated_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS objectives (
id TEXT PRIMARY KEY NOT NULL,
title TEXT NOT NULL,
owner_id TEXT REFERENCES users(id),
project_id TEXT REFERENCES projects(id),
period TEXT NOT NULL,
progress REAL DEFAULT 0,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS key_results (
id TEXT PRIMARY KEY NOT NULL,
objective_id TEXT NOT NULL REFERENCES objectives(id),
title TEXT NOT NULL,
target_value REAL NOT NULL,
current_value REAL DEFAULT 0,
unit TEXT,
weight REAL DEFAULT 1,
linked_plane_cycle_id TEXT,
linked_plane_module_id TEXT,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS author_mappings (
id TEXT PRIMARY KEY NOT NULL,
git_email TEXT,
git_username TEXT,
user_id TEXT REFERENCES users(id),
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS project_repos (
id TEXT PRIMARY KEY NOT NULL,
project_id TEXT NOT NULL REFERENCES projects(id),
repo_name TEXT NOT NULL,
created_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS sync_logs (
id TEXT PRIMARY KEY NOT NULL,
source TEXT NOT NULL,
status TEXT NOT NULL,
message TEXT,
records_processed INTEGER DEFAULT 0,
synced_at INTEGER NOT NULL
);
`);
}
// Run auto-migration on module load (i.e. on server startup)
autoMigrate();