A deployable markdown editor that connects with your self hosted files and lets you edit in a beautiful interface
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}