A deployable markdown editor that connects with your self hosted files and lets you edit in a beautiful interface
at main 489 lines 13 kB view raw
1package database 2 3import ( 4 "database/sql" 5 "fmt" 6 "time" 7) 8 9// CreateUser creates a new user or updates if exists 10func (db *DB) CreateUser(user *User) error { 11 query := ` 12 INSERT INTO users (github_id, username, email, avatar_url, last_repo) 13 VALUES (?, ?, ?, ?, ?) 14 ON CONFLICT(github_id) DO UPDATE SET 15 username = excluded.username, 16 email = excluded.email, 17 avatar_url = excluded.avatar_url, 18 last_repo = excluded.last_repo, 19 updated_at = CURRENT_TIMESTAMP 20 RETURNING id, created_at, updated_at 21 ` 22 23 return db.QueryRow(query, user.GithubID, user.Username, user.Email, user.AvatarURL, user.LastRepo). 24 Scan(&user.ID, &user.CreatedAt, &user.UpdatedAt) 25} 26 27// GetUserByGithubID retrieves a user by their GitHub ID 28func (db *DB) GetUserByGithubID(githubID int) (*User, error) { 29 user := &User{} 30 query := ` 31 SELECT id, github_id, username, email, avatar_url, last_repo, created_at, updated_at 32 FROM users 33 WHERE github_id = ? 34 ` 35 36 err := db.QueryRow(query, githubID).Scan( 37 &user.ID, 38 &user.GithubID, 39 &user.Username, 40 &user.Email, 41 &user.AvatarURL, 42 &user.LastRepo, 43 &user.CreatedAt, 44 &user.UpdatedAt, 45 ) 46 47 if err == sql.ErrNoRows { 48 return nil, fmt.Errorf("user not found") 49 } 50 if err != nil { 51 return nil, fmt.Errorf("failed to get user: %w", err) 52 } 53 54 return user, nil 55} 56 57// GetUserByID retrieves a user by their ID 58func (db *DB) GetUserByID(id int) (*User, error) { 59 user := &User{} 60 query := ` 61 SELECT id, github_id, username, email, avatar_url, last_repo, created_at, updated_at 62 FROM users 63 WHERE id = ? 64 ` 65 66 err := db.QueryRow(query, id).Scan( 67 &user.ID, 68 &user.GithubID, 69 &user.Username, 70 &user.Email, 71 &user.AvatarURL, 72 &user.LastRepo, 73 &user.CreatedAt, 74 &user.UpdatedAt, 75 ) 76 77 if err == sql.ErrNoRows { 78 return nil, fmt.Errorf("user not found") 79 } 80 if err != nil { 81 return nil, fmt.Errorf("failed to get user: %w", err) 82 } 83 84 return user, nil 85} 86 87// SaveAuthToken saves or updates an auth token 88func (db *DB) SaveAuthToken(token *AuthToken) error { 89 query := ` 90 INSERT INTO auth_tokens (user_id, provider, access_token, refresh_token, expires_at) 91 VALUES (?, ?, ?, ?, ?) 92 ON CONFLICT(user_id, provider) DO UPDATE SET 93 access_token = excluded.access_token, 94 refresh_token = excluded.refresh_token, 95 expires_at = excluded.expires_at, 96 created_at = CURRENT_TIMESTAMP 97 RETURNING id, created_at 98 ` 99 100 // Note: In production, these tokens should be encrypted before storage 101 return db.QueryRow(query, 102 token.UserID, 103 token.Provider, 104 token.AccessToken, 105 token.RefreshToken, 106 token.ExpiresAt, 107 ).Scan(&token.ID, &token.CreatedAt) 108} 109 110// GetAuthToken retrieves an auth token for a user and provider 111func (db *DB) GetAuthToken(userID int, provider string) (*AuthToken, error) { 112 token := &AuthToken{} 113 query := ` 114 SELECT id, user_id, provider, access_token, refresh_token, expires_at, created_at 115 FROM auth_tokens 116 WHERE user_id = ? AND provider = ? 117 ` 118 119 err := db.QueryRow(query, userID, provider).Scan( 120 &token.ID, 121 &token.UserID, 122 &token.Provider, 123 &token.AccessToken, 124 &token.RefreshToken, 125 &token.ExpiresAt, 126 &token.CreatedAt, 127 ) 128 129 if err == sql.ErrNoRows { 130 return nil, fmt.Errorf("token not found") 131 } 132 if err != nil { 133 return nil, fmt.Errorf("failed to get token: %w", err) 134 } 135 136 // Note: In production, these tokens should be decrypted after retrieval 137 return token, nil 138} 139 140// SaveDraftContent saves or updates draft content for a file 141func (db *DB) SaveDraftContent(draft *DraftContent) error { 142 query := ` 143 INSERT INTO draft_content (user_id, repo_full_name, file_path, content, change_type, original_path, last_saved_at) 144 VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) 145 ON CONFLICT(user_id, repo_full_name, file_path) DO UPDATE SET 146 content = excluded.content, 147 change_type = excluded.change_type, 148 original_path = excluded.original_path, 149 last_saved_at = CURRENT_TIMESTAMP 150 RETURNING id, last_saved_at 151 ` 152 153 return db.QueryRow(query, 154 draft.UserID, 155 draft.RepoFullName, 156 draft.FilePath, 157 draft.Content, 158 draft.ChangeType, 159 draft.OriginalPath, 160 ).Scan(&draft.ID, &draft.LastSavedAt) 161} 162 163// GetDraftContent retrieves draft content for a file 164func (db *DB) GetDraftContent(userID int, repoFullName, filePath string) (*DraftContent, error) { 165 draft := &DraftContent{} 166 query := ` 167 SELECT id, user_id, repo_full_name, file_path, content, change_type, original_path, last_saved_at 168 FROM draft_content 169 WHERE user_id = ? AND repo_full_name = ? AND file_path = ? 170 ` 171 172 err := db.QueryRow(query, userID, repoFullName, filePath).Scan( 173 &draft.ID, 174 &draft.UserID, 175 &draft.RepoFullName, 176 &draft.FilePath, 177 &draft.Content, 178 &draft.ChangeType, 179 &draft.OriginalPath, 180 &draft.LastSavedAt, 181 ) 182 183 if err == sql.ErrNoRows { 184 return nil, nil // No draft found, return nil without error 185 } 186 if err != nil { 187 return nil, fmt.Errorf("failed to get draft: %w", err) 188 } 189 190 return draft, nil 191} 192 193// DeleteDraftContent deletes draft content for a file 194func (db *DB) DeleteDraftContent(userID int, repoFullName, filePath string) error { 195 query := ` 196 DELETE FROM draft_content 197 WHERE user_id = ? AND repo_full_name = ? AND file_path = ? 198 ` 199 200 _, err := db.Exec(query, userID, repoFullName, filePath) 201 if err != nil { 202 return fmt.Errorf("failed to delete draft: %w", err) 203 } 204 205 return nil 206} 207 208// GetAllDraftContent retrieves all draft content for a repository 209func (db *DB) GetAllDraftContent(userID int, repoFullName string) ([]*DraftContent, error) { 210 query := ` 211 SELECT id, user_id, repo_full_name, file_path, content, change_type, original_path, last_saved_at 212 FROM draft_content 213 WHERE user_id = ? AND repo_full_name = ? 214 ORDER BY last_saved_at DESC 215 ` 216 217 rows, err := db.Query(query, userID, repoFullName) 218 if err != nil { 219 return nil, fmt.Errorf("failed to get all drafts: %w", err) 220 } 221 defer rows.Close() 222 223 var drafts []*DraftContent 224 for rows.Next() { 225 draft := &DraftContent{} 226 err := rows.Scan( 227 &draft.ID, 228 &draft.UserID, 229 &draft.RepoFullName, 230 &draft.FilePath, 231 &draft.Content, 232 &draft.ChangeType, 233 &draft.OriginalPath, 234 &draft.LastSavedAt, 235 ) 236 if err != nil { 237 return nil, fmt.Errorf("failed to scan draft row: %w", err) 238 } 239 drafts = append(drafts, draft) 240 } 241 242 return drafts, nil 243} 244 245// DeleteAllDraftContent deletes all draft content for a repository 246func (db *DB) DeleteAllDraftContent(userID int, repoFullName string) error { 247 query := ` 248 DELETE FROM draft_content 249 WHERE user_id = ? AND repo_full_name = ? 250 ` 251 252 _, err := db.Exec(query, userID, repoFullName) 253 if err != nil { 254 return fmt.Errorf("failed to delete all drafts: %w", err) 255 } 256 257 return nil 258} 259 260// SaveBranchState saves or updates branch state for a repository 261func (db *DB) SaveBranchState(state *BranchState) error { 262 query := ` 263 INSERT INTO branch_states (user_id, repo_full_name, branch_name, base_branch, last_push_at, has_uncommitted_changes, file_paths, updated_at) 264 VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) 265 ON CONFLICT(user_id, repo_full_name) DO UPDATE SET 266 branch_name = excluded.branch_name, 267 base_branch = excluded.base_branch, 268 last_push_at = excluded.last_push_at, 269 has_uncommitted_changes = excluded.has_uncommitted_changes, 270 file_paths = excluded.file_paths, 271 updated_at = CURRENT_TIMESTAMP 272 RETURNING id, created_at, updated_at 273 ` 274 275 return db.QueryRow(query, 276 state.UserID, 277 state.RepoFullName, 278 state.BranchName, 279 state.BaseBranch, 280 state.LastPushAt, 281 state.HasUncommittedChanges, 282 state.FilePaths, 283 ).Scan(&state.ID, &state.CreatedAt, &state.UpdatedAt) 284} 285 286// GetBranchState retrieves branch state for a repository 287func (db *DB) GetBranchState(userID int, repoFullName string) (*BranchState, error) { 288 state := &BranchState{} 289 query := ` 290 SELECT id, user_id, repo_full_name, branch_name, base_branch, last_push_at, has_uncommitted_changes, file_paths, created_at, updated_at 291 FROM branch_states 292 WHERE user_id = ? AND repo_full_name = ? 293 ` 294 295 err := db.QueryRow(query, userID, repoFullName).Scan( 296 &state.ID, 297 &state.UserID, 298 &state.RepoFullName, 299 &state.BranchName, 300 &state.BaseBranch, 301 &state.LastPushAt, 302 &state.HasUncommittedChanges, 303 &state.FilePaths, 304 &state.CreatedAt, 305 &state.UpdatedAt, 306 ) 307 308 if err == sql.ErrNoRows { 309 return nil, nil // No branch state found, return nil without error 310 } 311 if err != nil { 312 return nil, fmt.Errorf("failed to get branch state: %w", err) 313 } 314 315 return state, nil 316} 317 318// DeleteBranchState deletes branch state for a repository 319// UpdateUserRepo updates the last repository selected by a user 320func (db *DB) UpdateUserRepo(userID int, lastRepo string) error { 321 query := ` 322 UPDATE users 323 SET last_repo = ?, updated_at = CURRENT_TIMESTAMP 324 WHERE id = ? 325 ` 326 327 _, err := db.Exec(query, lastRepo, userID) 328 if err != nil { 329 return fmt.Errorf("failed to update user's last repository: %w", err) 330 } 331 332 return nil 333} 334 335// InsertUserRepo adds a new repository to the user_repos table 336func (db *DB) InsertUserRepo(userID int, repoName, repoLink string, lastUsedAt time.Time) error { 337 query := ` 338 INSERT INTO user_repos (user_id, repo_name, repo_link, last_used_at) 339 VALUES (?, ?, ?, ?) 340 ` 341 342 _, err := db.Exec(query, userID, repoName, repoLink, lastUsedAt) 343 if err != nil { 344 return fmt.Errorf("failed to insert user repo: %w", err) 345 } 346 347 return nil 348} 349 350// UpdateLastUsedAt updates the last_used_at for a specific repo 351func (db *DB) UpdateLastUsedAt(userID int, repoName string, lastUsedAt time.Time) error { 352 query := ` 353 UPDATE user_repos 354 SET last_used_at = ?, updated_at = CURRENT_TIMESTAMP 355 WHERE user_id = ? AND repo_name = ? 356 ` 357 358 _, err := db.Exec(query, lastUsedAt, userID, repoName) 359 if err != nil { 360 return fmt.Errorf("failed to update last_used_at: %w", err) 361 } 362 363 return nil 364} 365 366// GetUserRepos retrieves all repositories for a user 367func (db *DB) GetUserRepos(userID int) ([]UserRepo, error) { 368 query := ` 369 SELECT id, user_id, repo_name, repo_link, last_used_at, created_at, updated_at 370 FROM user_repos 371 WHERE user_id = ? 372 ` 373 374 rows, err := db.Query(query, userID) 375 if err != nil { 376 return nil, fmt.Errorf("failed to get user repos: %w", err) 377 } 378 defer rows.Close() 379 380 var repos []UserRepo 381 for rows.Next() { 382 var repo UserRepo 383 if err := rows.Scan( 384 &repo.ID, &repo.UserID, &repo.RepoName, &repo.RepoLink, 385 &repo.LastUsedAt, &repo.CreatedAt, &repo.UpdatedAt, 386 ); err != nil { 387 return nil, fmt.Errorf("failed to scan row: %w", err) 388 } 389 repos = append(repos, repo) 390 } 391 392 return repos, nil 393} 394 395// GetMostRecentUserRepo gets the most recently used repo from user_repos table 396func (db *DB) GetMostRecentUserRepo(userID int) (*UserRepo, error) { 397 query := ` 398 SELECT id, user_id, repo_name, repo_link, last_used_at, created_at, updated_at 399 FROM user_repos 400 WHERE user_id = ? 401 ORDER BY last_used_at DESC 402 LIMIT 1 403 ` 404 405 var repo UserRepo 406 err := db.QueryRow(query, userID).Scan( 407 &repo.ID, &repo.UserID, &repo.RepoName, &repo.RepoLink, 408 &repo.LastUsedAt, &repo.CreatedAt, &repo.UpdatedAt, 409 ) 410 411 if err == sql.ErrNoRows { 412 return nil, nil 413 } 414 if err != nil { 415 return nil, fmt.Errorf("failed to get most recent user repo: %w", err) 416 } 417 418 return &repo, nil 419} 420 421func (db *DB) DeleteBranchState(userID int, repoFullName string) error { 422 query := ` 423 DELETE FROM branch_states 424 WHERE user_id = ? AND repo_full_name = ? 425 ` 426 427 _, err := db.Exec(query, userID, repoFullName) 428 if err != nil { 429 return fmt.Errorf("failed to delete branch state: %w", err) 430 } 431 432 return nil 433} 434 435// LogCacheEvent logs a cache event to the database 436func (db *DB) LogCacheEvent(cacheKey, cacheType, eventType string, userID *int, responseTimeMs int) error { 437 query := ` 438 INSERT INTO cache_stats (cache_key, cache_type, event_type, user_id, response_time_ms) 439 VALUES (?, ?, ?, ?, ?) 440 ` 441 _, err := db.Exec(query, cacheKey, cacheType, eventType, userID, responseTimeMs) 442 return err 443} 444 445// GetCacheStats retrieves cache statistics since a given time 446func (db *DB) GetCacheStats(since time.Time) ([]CacheStat, error) { 447 query := ` 448 SELECT id, cache_key, cache_type, event_type, user_id, response_time_ms, created_at 449 FROM cache_stats 450 WHERE created_at >= ? 451 ORDER BY created_at DESC 452 LIMIT 1000 453 ` 454 rows, err := db.Query(query, since) 455 if err != nil { 456 return nil, err 457 } 458 defer rows.Close() 459 460 var stats []CacheStat 461 for rows.Next() { 462 var s CacheStat 463 err := rows.Scan(&s.ID, &s.CacheKey, &s.CacheType, &s.EventType, &s.UserID, &s.ResponseTimeMs, &s.CreatedAt) 464 if err != nil { 465 return nil, err 466 } 467 stats = append(stats, s) 468 } 469 return stats, nil 470} 471 472// GetCacheHitRate calculates the cache hit rate since a given time 473func (db *DB) GetCacheHitRate(since time.Time) (float64, error) { 474 query := ` 475 SELECT 476 SUM(CASE WHEN event_type = 'hit' THEN 1 ELSE 0 END) as hits, 477 COUNT(*) as total 478 FROM cache_stats 479 WHERE created_at >= ? AND event_type IN ('hit', 'miss') 480 ` 481 482 var hits, total int 483 err := db.QueryRow(query, since).Scan(&hits, &total) 484 if err != nil || total == 0 { 485 return 0, err 486 } 487 488 return float64(hits) / float64(total) * 100, nil 489}