package store import ( "context" "database/sql" "encoding/json" "fmt" "time" "github.com/kandev/kandev/internal/user/models" ) const ( DefaultUserID = "default-user" DefaultUserEmail = "default@kandev.local" ) type sqliteRepository struct { db *sql.DB ownsDB bool } var _ Repository = (*sqliteRepository)(nil) func newSQLiteRepositoryWithDB(dbConn *sql.DB) (*sqliteRepository, error) { return newSQLiteRepository(dbConn, false) } func newSQLiteRepository(dbConn *sql.DB, ownsDB bool) (*sqliteRepository, error) { repo := &sqliteRepository{db: dbConn, ownsDB: ownsDB} if err := repo.initSchema(); err != nil { if ownsDB { if closeErr := dbConn.Close(); closeErr != nil { return nil, fmt.Errorf("failed to close database after schema error: %w", closeErr) } } return nil, fmt.Errorf("failed to initialize schema: %w", err) } return repo, nil } func (r *sqliteRepository) initSchema() error { schema := ` CREATE TABLE IF NOT EXISTS users ( id TEXT PRIMARY KEY, email TEXT NOT NULL, settings TEXT NOT NULL DEFAULT '{}', created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL ); ` if _, err := r.db.Exec(schema); err != nil { return err } return r.ensureDefaultUser() } func (r *sqliteRepository) ensureDefaultUser() error { ctx := context.Background() var count int if err := r.db.QueryRowContext(ctx, "SELECT COUNT(1) FROM users WHERE id = ?", DefaultUserID).Scan(&count); err != nil { return err } if count == 0 { now := time.Now().UTC() _, err := r.db.ExecContext(ctx, ` INSERT INTO users (id, email, settings, created_at, updated_at) VALUES (?, ?, '{}', ?, ?) `, DefaultUserID, DefaultUserEmail, now, now) if err != nil { return err } } return nil } func (r *sqliteRepository) Close() error { if !r.ownsDB { return nil } return r.db.Close() } func (r *sqliteRepository) GetUser(ctx context.Context, id string) (*models.User, error) { row := r.db.QueryRowContext(ctx, ` SELECT id, email, created_at, updated_at FROM users WHERE id = ? `, id) return scanUser(row) } func (r *sqliteRepository) GetDefaultUser(ctx context.Context) (*models.User, error) { return r.GetUser(ctx, DefaultUserID) } func (r *sqliteRepository) GetUserSettings(ctx context.Context, userID string) (*models.UserSettings, error) { row := r.db.QueryRowContext(ctx, ` SELECT settings, updated_at FROM users WHERE id = ? `, userID) settings, err := scanUserSettings(row, userID) if err != nil { return nil, err } return settings, nil } func (r *sqliteRepository) UpsertUserSettings(ctx context.Context, settings *models.UserSettings) error { settings.UpdatedAt = time.Now().UTC() if settings.CreatedAt.IsZero() { settings.CreatedAt = settings.UpdatedAt } settingsPayload, err := json.Marshal(map[string]interface{}{ "workspace_id": settings.WorkspaceID, "board_id": settings.BoardID, "repository_ids": settings.RepositoryIDs, "initial_setup_complete": settings.InitialSetupComplete, "preferred_shell": settings.PreferredShell, "default_editor_id": settings.DefaultEditorID, "enable_preview_on_click": settings.EnablePreviewOnClick, "chat_submit_key": settings.ChatSubmitKey, }) if err != nil { return err } result, err := r.db.ExecContext(ctx, ` UPDATE users SET settings = ?, updated_at = ? WHERE id = ? `, string(settingsPayload), settings.UpdatedAt, settings.UserID) if err == nil { rows, _ := result.RowsAffected() if rows == 0 { return fmt.Errorf("user not found: %s", settings.UserID) } } return err } func scanUser(scanner interface{ Scan(dest ...any) error }) (*models.User, error) { user := &models.User{} if err := scanner.Scan(&user.ID, &user.Email, &user.CreatedAt, &user.UpdatedAt); err != nil { return nil, err } return user, nil } func scanUserSettings(scanner interface{ Scan(dest ...any) error }, userID string) (*models.UserSettings, error) { settings := &models.UserSettings{} var settingsRaw string if err := scanner.Scan(&settingsRaw, &settings.UpdatedAt); err != nil { return nil, err } settings.UserID = userID if settingsRaw == "" || settingsRaw == "{}" { settings.RepositoryIDs = []string{} return settings, nil } var payload struct { WorkspaceID string `json:"workspace_id"` BoardID string `json:"board_id"` RepositoryIDs []string `json:"repository_ids"` InitialSetupComplete bool `json:"initial_setup_complete"` PreferredShell string `json:"preferred_shell"` DefaultEditorID string `json:"default_editor_id"` EnablePreviewOnClick bool `json:"enable_preview_on_click"` ChatSubmitKey string `json:"chat_submit_key"` } if err := json.Unmarshal([]byte(settingsRaw), &payload); err != nil { return nil, err } settings.WorkspaceID = payload.WorkspaceID settings.BoardID = payload.BoardID settings.RepositoryIDs = payload.RepositoryIDs settings.InitialSetupComplete = payload.InitialSetupComplete settings.PreferredShell = payload.PreferredShell settings.DefaultEditorID = payload.DefaultEditorID settings.EnablePreviewOnClick = payload.EnablePreviewOnClick settings.ChatSubmitKey = payload.ChatSubmitKey // Default to "cmd_enter" if empty if settings.ChatSubmitKey == "" { settings.ChatSubmitKey = "cmd_enter" } return settings, nil }