From 26d612f72058668375c7e9f0976e327a2066e3ad Mon Sep 17 00:00:00 2001 From: AndrewRoe34 Date: Mon, 29 Jul 2024 11:08:19 -0400 Subject: [PATCH] implemented db integration --- go.mod | 1 + go.sum | 2 + internal/cobra/cmd.go | 2 +- internal/cobra/task.go | 104 +++++++++++++++----- internal/service/task.go | 64 +++++++++---- internal/service/testdata.go | 8 +- internal/sqlite/sqlite.go | 181 ++++++++++++++++++++++++++++++----- internal/types/task.go | 3 +- 8 files changed, 296 insertions(+), 69 deletions(-) diff --git a/go.mod b/go.mod index a0782b2..9083a4a 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.22.5 require ( github.com/charmbracelet/bubbletea v0.26.6 + github.com/mattn/go-sqlite3 v1.14.22 github.com/spf13/cobra v1.8.1 github.com/spf13/viper v1.19.0 ) diff --git a/go.sum b/go.sum index b62b36a..d929d43 100644 --- a/go.sum +++ b/go.sum @@ -35,6 +35,8 @@ github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2J github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= diff --git a/internal/cobra/cmd.go b/internal/cobra/cmd.go index a197e44..bf70de9 100644 --- a/internal/cobra/cmd.go +++ b/internal/cobra/cmd.go @@ -19,7 +19,7 @@ func NewCmd(repo *service.TaskRepo) *Cmd { func (c *Cmd) Execute() { rootCmd := c.RootCmd() - rootCmd.AddCommand(c.AddCmd(), c.ModCmd(), c.GetCmd(), c.ListCmd(), c.DoneCmd(), c.UndoCmd(), c.NoteCmd(), c.ImportCmd(), c.ExportCmd()) + rootCmd.AddCommand(c.AddCmd(), c.ModCmd(), c.DeleteCmd(), c.GetCmd(), c.ListCmd(), c.DoneCmd(), c.UndoCmd(), c.NoteCmd(), c.ImportCmd(), c.ExportCmd()) if err := rootCmd.Execute(); err != nil { fmt.Println(err) diff --git a/internal/cobra/task.go b/internal/cobra/task.go index 1e204dc..1959c4d 100644 --- a/internal/cobra/task.go +++ b/internal/cobra/task.go @@ -55,8 +55,12 @@ gt add "Setup database" @ 11-3 +project p := 5 ti.priority = &p } - t := types.NewTask(1, *ti.desc, *ti.priority, ti.addTags, nil, ti.startAt, ti.endAt) - fmt.Printf("Added task %d.\n", t.ID) + t := types.NewTask(*ti.desc, *ti.priority, ti.addTags, nil, ti.startAt, ti.endAt) + i, err := c.repo.AddTask(t) + if err != nil { + log.Fatal(err) + } + fmt.Printf("Added task %d.\n", i) }, } @@ -109,6 +113,7 @@ func (c *Cmd) ModCmd() *cobra.Command { fmt.Printf("Task %d not found.\n", *ti.id) return } + fmt.Printf("Task %d '%s' has been updated:\n", t.ID, t.Desc) if ti.desc != nil { fmt.Printf(" - Description updated to '%s'\n", *ti.desc) @@ -126,6 +131,7 @@ func (c *Cmd) ModCmd() *cobra.Command { } if ti.startAt != nil { t.StartAt = ti.startAt + t.EndAt = nil } if ti.endAt != nil { t.EndAt = ti.endAt @@ -138,6 +144,12 @@ func (c *Cmd) ModCmd() *cobra.Command { fmt.Printf("%s %s\n", t.StartAt.Format("Mon, 02 Jan 2006"), t.StartAt.Format(time.Kitchen)) } } + curr := time.Now() + t.UpdatedAt = &curr + err = c.repo.UpdateTask(t) + if err != nil { + log.Fatal(err) + } fmt.Println("Update complete. 1 task modified.") }, } @@ -154,16 +166,15 @@ func (c *Cmd) NoteCmd() *cobra.Command { if err != nil { log.Fatal(err) } - t, err := c.repo.GetTask(id) + d, err := c.repo.GetDesc(id) if err != nil { log.Fatal(err) } - //check if task exists, because of nil pointer dereference - if t == nil { - fmt.Printf("Task %d not found.\n", id) - return + err = c.repo.AddNote(id, n) + if err != nil { + log.Fatal(err) } - fmt.Printf("Task %d '%s' has been updated with a new note:\n", t.ID, t.Desc) + fmt.Printf("Task %d '%s' has been updated with a new note:\n", id, d) fmt.Printf(" - Note: \"%s\"\n", n) fmt.Println("1 task updated with a note.") }, @@ -206,7 +217,15 @@ func (c *Cmd) DoneCmd() *cobra.Command { fmt.Printf("Task %d not found.\n", i) continue } - fmt.Printf("Finished task %d '%s'.\n", i, t.Desc) + if t.Finished { + fmt.Printf("Task %d already finished.\n", i) + } else { + err = c.repo.UpdateStatus(i, true) + if err != nil { + return + } + fmt.Printf("Finished task %d '%s'.\n", i, t.Desc) + } } if len(ids) > 1 { fmt.Printf("Finished %d tasks.\n", len(ids)) @@ -237,7 +256,15 @@ func (c *Cmd) UndoCmd() *cobra.Command { fmt.Printf("Task %d not found.\n", i) continue } - fmt.Printf("Reverted task %d '%s' to incomplete.\n", i, t.Desc) + if !t.Finished { + fmt.Printf("Task %d already incomplete.\n", i) + } else { + err = c.repo.UpdateStatus(i, false) + if err != nil { + return + } + fmt.Printf("Reverted task %d '%s' to incomplete.\n", i, t.Desc) + } } if len(ids) > 1 { fmt.Printf("Reverted %d tasks.\n", len(ids)) @@ -255,14 +282,42 @@ func (c *Cmd) DeleteCmd() *cobra.Command { Short: "Deletes tasks by ID", Args: cobra.MinimumNArgs(1), Run: func(cmd *cobra.Command, args []string) { - /* - Preparing to delete tasks with IDs: 1, 3, 5 - - Task 1: 'finish some work' - - Task 3: 'study BSTs' - - Task 5: 'clean the house' - - Are you sure you want to delete these tasks? (y/n): - */ + ids, err := parseGet(args) + if err != nil { + log.Fatal(err) + } + var tasks []*types.Task + for _, i := range ids { + t, err := c.repo.GetTask(i) + if err != nil { + log.Fatal(err) + } + tasks = append(tasks, t) + } + fmt.Printf("Preparing to delete tasks with ") + if len(ids) == 1 { + fmt.Printf("ID: %d\n", ids[0]) + } else { + fmt.Printf("IDs: %d", ids[0]) + for j := 1; j < len(ids); j++ { + fmt.Printf(", %d", ids[j]) + } + } + for _, t := range tasks { + fmt.Printf(" - Task %d: '%s'\n", t.ID, t.Desc) + } + if len(ids) == 1 { + fmt.Printf("\nAre you sure you want to delete this task? (y/n): ") + } else { + fmt.Printf("\nAre you sure you want to delete these tasks? (y/n): ") + } + // todo need to prompt user for input (check whether first char matches 'y') + for _, i := range ids { + err = c.repo.DeleteTask(i) + if err != nil { + log.Fatal(err) + } + } }, } return deleteCmd @@ -320,7 +375,12 @@ func displayTask(task *types.Task) { // Display last modified time fmt.Printf("Last modified %s\n", task.UpdatedAt.Format(time.RFC1123)) - fmt.Printf("\nNotes:\nThu, 18 Jul 2024 00:50:46 EDT - unexpected issue came up, am resolving now") + fmt.Printf("\nNotes:\n") + if task.Notes != nil { + for _, n := range task.Notes { + fmt.Printf(" - %s\n", n) + } + } } // / formatTask prints a task in the desired format @@ -350,15 +410,15 @@ func formatTask(task *types.Task) string { } // Format the output string with additional spaces for the 'Due' column - return fmt.Sprintf("%d %s %-30s %d %-13s %s ", // Adjusted format string with extra spaces + return fmt.Sprintf("%-6d %s %-30s %d %-13s %s ", // Adjusted format string with extra spaces task.ID, status, task.Desc, task.Priority, tags, due) } // displayTasks prints a list of tasks in the desired format func displayTasks(tasks []*types.Task) { // Print header - fmt.Println("ID Status Desc Priority Tags Due ") - fmt.Println("------------------------------------------------------------------------------------------------------") + fmt.Println("ID Status Desc Priority Tags Due ") + fmt.Println("---------------------------------------------------------------------------------------------------------") // Print each task for _, task := range tasks { diff --git a/internal/service/task.go b/internal/service/task.go index f442736..925d8a9 100644 --- a/internal/service/task.go +++ b/internal/service/task.go @@ -2,22 +2,23 @@ package service import ( "database/sql" + "github.com/EvoSched/gotask/internal/sqlite" "github.com/EvoSched/gotask/internal/types" ) type TaskRepoQuery interface { GetTask(id int) (*types.Task, error) GetTasks() ([]*types.Task, error) - // todo need to add interface functions for getting tasks by priority, tags, and time + GetDesc(id int) (string, error) + GetNotes(id int) ([]string, error) } type TaskRepoStmt interface { - AddTask(task *types.Task) error - EditTask(id int, task *types.Task) error - RemoveTask(id int) error - CompleteTask(id int) error - IncompleteTask(id int) error + AddTask(task *types.Task) (int, error) AddNote(id int, note string) error + UpdateStatus(id int, status bool) error + UpdateTask(task *types.Task) error + DeleteTask(db *sql.DB, id int) error } type TaskRepo struct { @@ -28,24 +29,51 @@ func NewTaskRepo(db *sql.DB) *TaskRepo { return &TaskRepo{db} } +func (r *TaskRepo) GetDesc(id int) (string, error) { + return sqlite.QueryDesc(r.db, id) +} + +func (r *TaskRepo) GetNotes(id int) ([]string, error) { + return sqlite.QueryNotes(r.db, id) +} + func (r *TaskRepo) GetTask(id int) (*types.Task, error) { - //TODO: sql query + t, err := sqlite.QueryTask(r.db, id) + if err != nil { + return nil, err + } + n, err := sqlite.QueryNotes(r.db, id) + if err != nil { + return nil, err + } + t.Notes = append(t.Notes, n...) + return &t, nil +} - for _, task := range tasks { - if task.ID == id { - return task, nil - } +func (r *TaskRepo) GetTasks() ([]*types.Task, error) { + return sqlite.QueryTasks(r.db) +} + +func (r *TaskRepo) AddTask(task *types.Task) (int, error) { + err := sqlite.InsertTask(r.db, task) + if err != nil { + return 0, err } + return sqlite.QueryLastID(r.db) +} - return nil, nil +func (r *TaskRepo) AddNote(id int, note string) error { + return sqlite.InsertNote(r.db, id, note) } -func (r *TaskRepo) GetTasks() ([]*types.Task, error) { - //TODO: sql query +func (r *TaskRepo) UpdateStatus(id int, status bool) error { + return sqlite.UpdateStatus(r.db, id, status) +} - // todo remove when integrating SQL (this is purely for displaying mock data - tasks[1].Finished = true - tasks[3].Finished = true +func (r *TaskRepo) UpdateTask(task *types.Task) error { + return sqlite.UpdateTask(r.db, task) +} - return tasks, nil +func (r *TaskRepo) DeleteTask(id int) error { + return sqlite.DeleteTask(r.db, id) } diff --git a/internal/service/testdata.go b/internal/service/testdata.go index 4481841..6b8dcc4 100644 --- a/internal/service/testdata.go +++ b/internal/service/testdata.go @@ -12,8 +12,8 @@ var date = time.Date(curr.Year(), curr.Month(), curr.Day(), 23, 59, 0, 0, time.U // sample data to test command functions var tasks = []*types.Task{ - types.NewTask(1, "finish project3", 5, []string{"MA", "CS"}, []string{"comment1"}, &start, nil), - types.NewTask(2, "study BSTs", 8, []string{"CS"}, []string{"comment2"}, &start, &end), - types.NewTask(3, "lunch with Edgar", 2, []string{"Fun"}, []string{"comment3"}, nil, nil), - types.NewTask(4, "meeting for db proposal", 5, []string{"Project"}, []string{"comment4"}, &date, nil), + types.NewTask("finish project3", 5, []string{"MA", "CS"}, []string{"comment1"}, &start, nil), + types.NewTask("study BSTs", 8, []string{"CS"}, []string{"comment2"}, &start, &end), + types.NewTask("lunch with Edgar", 2, []string{"Fun"}, []string{"comment3"}, nil, nil), + types.NewTask("meeting for db proposal", 5, []string{"Project"}, []string{"comment4"}, &date, nil), } diff --git a/internal/sqlite/sqlite.go b/internal/sqlite/sqlite.go index 788b99f..5a53807 100644 --- a/internal/sqlite/sqlite.go +++ b/internal/sqlite/sqlite.go @@ -2,9 +2,10 @@ package sqlite import ( "database/sql" - "os" - "github.com/EvoSched/gotask/internal/config" + "github.com/EvoSched/gotask/internal/types" + _ "github.com/mattn/go-sqlite3" // Import go-sqlite3 library + "os" ) const ( @@ -13,7 +14,7 @@ const ( func NewSQLite(config *config.SQLite) (*sql.DB, error) { // todo create database if it doesn't already exist - flag, err := setupDB(config) + err := setupDB(config) if err != nil { return nil, err } @@ -22,41 +23,177 @@ func NewSQLite(config *config.SQLite) (*sql.DB, error) { return nil, err } // we just created our database file, need to create tables now - if flag { - err = createTable(db) + err = createTaskTable(db) + if err != nil { + return nil, err + } + err = createNoteTable(db) + if err != nil { + return nil, err } return db, err } -func setupDB(config *config.SQLite) (bool, error) { +func setupDB(config *config.SQLite) error { if _, err := os.Stat(config.Database); os.IsNotExist(err) { file, err := os.Create(config.Database) if err != nil { - return false, err + return err } file.Close() - return true, nil } - return false, nil + return nil +} + +func createTaskTable(db *sql.DB) error { + stmt, err := db.Prepare(`CREATE TABLE IF NOT EXISTS task ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "desc" TEXT NOT NULL, + "priority" INTEGER NOT NULL, + "start_at" DATETIME, + "end_at" DATETIME, + "updated_at" DATETIME NOT NULL, + "finished" INTEGER NOT NULL CHECK (finished IN (0,1)) +);`) + if err != nil { + return err + } + _, err = stmt.Exec() + return err +} + +func createNoteTable(db *sql.DB) error { + stmt, err := db.Prepare(`CREATE TABLE IF NOT EXISTS note ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "task_id" INTEGER NOT NULL, + "comment" TEXT NOT NULL +);`) + if err != nil { + return err + } + _, err = stmt.Exec() + return err +} + +func QueryTask(db *sql.DB, id int) (types.Task, error) { + var task types.Task + row := db.QueryRow(`SELECT id, desc, priority, start_at, end_at, updated_at, finished FROM task WHERE id = ?`, id) + err := row.Scan(&task.ID, &task.Desc, &task.Priority, &task.StartAt, &task.EndAt, &task.UpdatedAt, &task.Finished) + if err != nil { + return task, err + } + return task, nil +} + +func QueryTasks(db *sql.DB) ([]*types.Task, error) { + rows, err := db.Query(`SELECT id, desc, priority, start_at, end_at, updated_at, finished FROM task`) + if err != nil { + return nil, err + } + defer rows.Close() + + var tasks []*types.Task + for rows.Next() { + var task types.Task + err := rows.Scan(&task.ID, &task.Desc, &task.Priority, &task.StartAt, &task.EndAt, &task.UpdatedAt, &task.Finished) + if err != nil { + return nil, err + } + tasks = append(tasks, &task) + } + return tasks, nil +} + +func QueryDesc(db *sql.DB, id int) (string, error) { + var desc string + row := db.QueryRow(`SELECT desc FROM task WHERE id = ?`, id) + err := row.Scan(&desc) + if err != nil { + return "", err + } + return desc, nil +} + +func QueryNotes(db *sql.DB, id int) ([]string, error) { + rows, err := db.Query(`SELECT comment FROM note WHERE task_id = ?`, id) + if err != nil { + return nil, err + } + defer rows.Close() + + var notes []string + for rows.Next() { + var n string + err := rows.Scan(&n) + if err != nil { + return nil, err + } + notes = append(notes, n) + } + return notes, nil +} + +func QueryLastID(db *sql.DB) (int, error) { + // Query to find the maximum existing ID + row := db.QueryRow(`SELECT COALESCE(MAX(id), 0) FROM task`) + + var id int + if err := row.Scan(&id); err != nil { + return 0, err + } + + return id, nil +} + +func InsertTask(db *sql.DB, task *types.Task) error { + stmt, err := db.Prepare(`INSERT INTO task(desc, priority, start_at, end_at, updated_at, finished) VALUES(?, ?, ?, ?, ?, ?)`) + if err != nil { + return err + } + defer stmt.Close() + + _, err = stmt.Exec(task.Desc, task.Priority, task.StartAt, task.EndAt, task.UpdatedAt, task.Finished) + return err +} + +func InsertNote(db *sql.DB, id int, note string) error { + stmt, err := db.Prepare(`INSERT INTO note(task_id, comment) VALUES(?, ?)`) + if err != nil { + return err + } + defer stmt.Close() + + _, err = stmt.Exec(id, note) + return err } -func createTable(db *sql.DB) error { - tableSQL := `CREATE TABLE task ( - "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - "desc" TEXT NOT NULL, - "priority" INTEGER NOT NULL, - "start_at" DATETIME, - "end_at" DATETIME, - "created_at" DATETIME NOT NULL , - "finished" INTEGER NOT NULL CHECK (finished IN (0,1)) - );` +func UpdateStatus(db *sql.DB, id int, status bool) error { + stmt, err := db.Prepare(`UPDATE task SET finished = ? WHERE id = ?`) + if err != nil { + return err + } + defer stmt.Close() - statement, err := db.Prepare(tableSQL) // Prepare SQL Statement + _, err = stmt.Exec(status, id) + return err +} + +func UpdateTask(db *sql.DB, task *types.Task) error { + stmt, err := db.Prepare(`UPDATE task SET desc = ?, priority = ?, start_at = ?, end_at = ?, updated_at = ?, finished = ? WHERE id = ?`) if err != nil { return err } + defer stmt.Close() + _, err = stmt.Exec(task.Desc, task.Priority, task.StartAt, task.EndAt, task.UpdatedAt, task.Finished, task.ID) + return err +} - // Execute SQL Statements - _, err = statement.Exec() +func DeleteTask(db *sql.DB, id int) error { + stmt, err := db.Prepare(`DELETE FROM task WHERE id = ?`) + if err != nil { + return err + } + defer stmt.Close() + _, err = stmt.Exec(id) return err } diff --git a/internal/types/task.go b/internal/types/task.go index 93e7d5f..5159e3b 100644 --- a/internal/types/task.go +++ b/internal/types/task.go @@ -16,10 +16,9 @@ type Task struct { Finished bool } -func NewTask(id int, desc string, priority int, tags []string, comments []string, startAt *time.Time, endAt *time.Time) *Task { +func NewTask(desc string, priority int, tags []string, comments []string, startAt *time.Time, endAt *time.Time) *Task { now := time.Now() return &Task{ - ID: id, Desc: desc, Priority: priority, Tags: tags,