From 9282bee8365e12effff62051271ca85a52d04c43 Mon Sep 17 00:00:00 2001 From: CalvinLiu123 <41094560+CalvinLiu123@users.noreply.github.com> Date: Wed, 17 Sep 2025 13:55:53 +0700 Subject: [PATCH] commit --- cmd/server/main.go | 49 +++++++ go.mod | 7 + go.sum | 4 + internal/db/connection.go | 43 ++++++ internal/db/group_queries.go | 49 +++++++ internal/db/models.go | 19 +++ internal/db/task_queries.go | 99 ++++++++++++++ internal/handlers/group_handlers.go | 139 ++++++++++++++++++++ internal/handlers/task_handlers.go | 194 ++++++++++++++++++++++++++++ 9 files changed, 603 insertions(+) create mode 100644 cmd/server/main.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/db/connection.go create mode 100644 internal/db/group_queries.go create mode 100644 internal/db/models.go create mode 100644 internal/db/task_queries.go create mode 100644 internal/handlers/group_handlers.go create mode 100644 internal/handlers/task_handlers.go diff --git a/cmd/server/main.go b/cmd/server/main.go new file mode 100644 index 0000000..ace03a8 --- /dev/null +++ b/cmd/server/main.go @@ -0,0 +1,49 @@ +package main + +import ( + "5803024003/internal/db" + "5803024003/internal/handlers" + "log" + "net/http" + + "github.com/gorilla/mux" +) + +func home(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("Hello broskie")) +} + +func main() { + // Initialize database connection + database, err := db.InitDB() + if err != nil { + log.Fatal("Failed to connect to database:", err) + } + defer database.Close() + + // Set database connection for handlers + handlers.SetDatabase(database) + + // Use mux.NewRouter() to initialize Gorilla Mux router + r := mux.NewRouter() + r.HandleFunc("/", home).Methods("GET") + + // Group management routes + r.HandleFunc("/groups", handlers.CreateGroupHandler).Methods("POST") + r.HandleFunc("/groups/{groupId}", handlers.GetGroupHandler).Methods("GET") + r.HandleFunc("/groups/{groupId}", handlers.RemoveGroupHandler).Methods("DELETE") + + // Task management routes + r.HandleFunc("/groups/{groupId}/task", handlers.CreateTaskHandler).Methods("POST") + r.HandleFunc("/groups/{groupId}/task", handlers.DisplayTasksByGroupHandler).Methods("GET") + r.HandleFunc("/task", handlers.DisplayTasksHandler).Methods("GET") + r.HandleFunc("/task/{taskId}", handlers.GetTaskHandler).Methods("GET") + r.HandleFunc("/task/{taskId}", handlers.UpdateTaskHandler).Methods("PUT") + r.HandleFunc("/task/{taskId}", handlers.RemoveTaskHandler).Methods("DELETE") + r.HandleFunc("/task/{taskId}/done", handlers.MarkTaskDoneHandler).Methods("PUT") + + // Use the http.ListenAndServe() function to start a new web server. + log.Print("Starting server on :4000") + err = http.ListenAndServe(":4000", r) + log.Fatal(err) +} \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..dc343ac --- /dev/null +++ b/go.mod @@ -0,0 +1,7 @@ +module 5803024003 + +go 1.25.0 + +require github.com/gorilla/mux v1.8.1 + +require github.com/lib/pq v1.10.9 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..2a964d7 --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= diff --git a/internal/db/connection.go b/internal/db/connection.go new file mode 100644 index 0000000..5144ec0 --- /dev/null +++ b/internal/db/connection.go @@ -0,0 +1,43 @@ +package db + +import ( + "database/sql" + "fmt" + + _ "github.com/lib/pq" +) + +const ( + host = "202.46.28.160" + port = 45432 + user = "5803024003" + password = "pw5803024003" + dbname = "tgs01_5803024003" +) + +// InitDB returns a database connection +func InitDB() (*sql.DB, error) { + // connection string + psqlconn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", host, port, user, password, dbname) + + // open database + db, err := sql.Open("postgres", psqlconn) + if err != nil { + return nil, err + } + + // check db + err = db.Ping() + if err != nil { + return nil, err + } + + fmt.Println("Connected to database!") + return db, nil +} + +func CheckError(err error) { + if err != nil { + panic(err) + } +} \ No newline at end of file diff --git a/internal/db/group_queries.go b/internal/db/group_queries.go new file mode 100644 index 0000000..1d4e3b1 --- /dev/null +++ b/internal/db/group_queries.go @@ -0,0 +1,49 @@ +package db + +import ( + "database/sql" +) + +// CreateGroup inserts a new group into the database +func CreateGroup(db *sql.DB, groupName string) error { + query := `INSERT INTO groups (group_name) VALUES ($1)` + _, err := db.Exec(query, groupName) + return err +} + +// GetGroupByID retrieves a group by its ID +func GetGroupByID(db *sql.DB, groupID int) (*Groups, error) { + query := `SELECT group_id, group_name FROM groups WHERE group_id = $1` + row := db.QueryRow(query, groupID) + + var group Groups + err := row.Scan(&group.GroupId, &group.GroupName) + if err != nil { + return nil, err + } + + return &group, nil +} + +// RemoveGroup deletes a group and all its associated tasks +func RemoveGroup(db *sql.DB, groupID int) error { + tx, err := db.Begin() + if err != nil { + return err + } + defer tx.Rollback() + + // Delete all tasks in the group first + _, err = tx.Exec("DELETE FROM tasks WHERE group_id = $1", groupID) + if err != nil { + return err + } + + // Delete the group + _, err = tx.Exec("DELETE FROM groups WHERE group_id = $1", groupID) + if err != nil { + return err + } + + return tx.Commit() +} \ No newline at end of file diff --git a/internal/db/models.go b/internal/db/models.go new file mode 100644 index 0000000..239e511 --- /dev/null +++ b/internal/db/models.go @@ -0,0 +1,19 @@ +package db + +import ( + "time" +) + +type Groups struct{ + GroupId int `json:"group_id"` + GroupName string `json:"group_name"` +} + +type Tasks struct{ + TaskID int `json:"task_id"` + TaskName string `json:"task_name"` // Maps to "task" column in SQL + TaskDescription string `json:"task_desc"` // Maps to "task_desc" column in SQL (changed from *int to string) + GroupID int `json:"group_id"` + IsDone bool `json:"is_done"` // Maps to "isdone" column in SQL + CreatedAt time.Time `json:"created_at"` +} \ No newline at end of file diff --git a/internal/db/task_queries.go b/internal/db/task_queries.go new file mode 100644 index 0000000..40aa506 --- /dev/null +++ b/internal/db/task_queries.go @@ -0,0 +1,99 @@ +package db + +import ( + "database/sql" + "time" +) + +// CreateTask inserts a new task into the database +func CreateTask(db *sql.DB, taskName string, taskDescription string, groupID int) error { + query := `INSERT INTO tasks (task_name, task_desc, group_id, is_done, created_at) + VALUES ($1, $2, $3, $4, $5)` + _, err := db.Exec(query, taskName, taskDescription, groupID, false, time.Now()) + return err +} + +// GetAllTasks retrieves all tasks from the database +func GetAllTasks(db *sql.DB) ([]Tasks, error) { + query := `SELECT task_id, task_name, task_desc, group_id, is_done, created_at + FROM tasks ORDER BY created_at DESC` + rows, err := db.Query(query) + if err != nil { + return nil, err + } + defer rows.Close() + + var tasks []Tasks + for rows.Next() { + var task Tasks + err := rows.Scan(&task.TaskID, &task.TaskName, &task.TaskDescription, + &task.GroupID, &task.IsDone, &task.CreatedAt) + if err != nil { + return nil, err + } + tasks = append(tasks, task) + } + + return tasks, nil +} + +// GetTaskByID retrieves a specific task by its ID +func GetTaskByID(db *sql.DB, taskID int) (*Tasks, error) { + query := `SELECT task_id, task_name, task_desc, group_id, is_done, created_at + FROM tasks WHERE task_id = $1` + row := db.QueryRow(query, taskID) + + var task Tasks + err := row.Scan(&task.TaskID, &task.TaskName, &task.TaskDescription, + &task.GroupID, &task.IsDone, &task.CreatedAt) + if err != nil { + return nil, err + } + + return &task, nil +} + +// GetTasksByGroupID retrieves all tasks for a specific group +func GetTasksByGroupID(db *sql.DB, groupID int) ([]Tasks, error) { + query := `SELECT task_id, task_name, task_desc, group_id, is_done, created_at + FROM tasks WHERE group_id = $1` + rows, err := db.Query(query, groupID) + if err != nil { + return nil, err + } + defer rows.Close() + + var tasks []Tasks + for rows.Next() { + var task Tasks + err := rows.Scan(&task.TaskID, &task.TaskName, &task.TaskDescription, + &task.GroupID, &task.IsDone, &task.CreatedAt) + if err != nil { + return nil, err + } + tasks = append(tasks, task) + } + + return tasks, nil +} + +// MarkTaskAsDone updates a task's status to completed +func MarkTaskAsDone(db *sql.DB, taskID int) error { + query := `UPDATE tasks SET is_done = true WHERE task_id = $1` + _, err := db.Exec(query, taskID) + return err +} + +// RemoveTask deletes a task from the database +func RemoveTask(db *sql.DB, taskID int) error { + query := `DELETE FROM tasks WHERE task_id = $1` + _, err := db.Exec(query, taskID) + return err +} + +// UpdateTask updates task name and description +func UpdateTask(db *sql.DB, taskID int, taskName string, taskDescription string) error { + query := `UPDATE tasks SET task_name = $1, task_desc = $2 WHERE task_id = $3` + _, err := db.Exec(query, taskName, taskDescription, taskID) + return err +} \ No newline at end of file diff --git a/internal/handlers/group_handlers.go b/internal/handlers/group_handlers.go new file mode 100644 index 0000000..b83d0fe --- /dev/null +++ b/internal/handlers/group_handlers.go @@ -0,0 +1,139 @@ +package handlers + +import ( + "5803024003/internal/db" + "database/sql" + "encoding/json" + "net/http" + "strconv" + + "github.com/gorilla/mux" +) + +type GroupRequest struct { + GroupName string `json:"group_name"` +} + +type GroupResponse struct { + GroupID int `json:"group_id"` + GroupName string `json:"group_name"` +} + +type ErrorResponse struct { + Error string `json:"error"` +} + +// Database connection - you'll need to inject this or use a global variable +var database *sql.DB + +func SetDatabase(db *sql.DB) { + database = db +} + +func CreateGroupHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + var req GroupRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(ErrorResponse{Error: "Invalid JSON format"}) + return + } + + if req.GroupName == "" { + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(ErrorResponse{Error: "Group name is required"}) + return + } + + if err := db.CreateGroup(database, req.GroupName); err != nil { + w.WriteHeader(http.StatusInternalServerError) + json.NewEncoder(w).Encode(ErrorResponse{Error: "Failed to create group"}) + return + } + + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(map[string]string{"message": "Group created successfully"}) +} + +func DisplayTasksByGroupHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + vars := mux.Vars(r) + groupIDStr := vars["groupId"] + + groupID, err := strconv.Atoi(groupIDStr) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(ErrorResponse{Error: "Invalid group ID"}) + return + } + + tasks, err := db.GetTasksByGroupID(database, groupID) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + json.NewEncoder(w).Encode(ErrorResponse{Error: "Failed to retrieve tasks"}) + return + } + + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(tasks) +} + +func RemoveGroupHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + vars := mux.Vars(r) + groupIDStr := vars["groupId"] + + groupID, err := strconv.Atoi(groupIDStr) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(ErrorResponse{Error: "Invalid group ID"}) + return + } + + if err := db.RemoveGroup(database, groupID); err != nil { + w.WriteHeader(http.StatusInternalServerError) + json.NewEncoder(w).Encode(ErrorResponse{Error: "Failed to remove group"}) + return + } + + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(map[string]string{"message": "Group removed successfully"}) +} + +func GetGroupHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + vars := mux.Vars(r) + groupIDStr := vars["groupId"] + + groupID, err := strconv.Atoi(groupIDStr) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(ErrorResponse{Error: "Invalid group ID"}) + return + } + + group, err := db.GetGroupByID(database, groupID) + if err == sql.ErrNoRows { + w.WriteHeader(http.StatusNotFound) + json.NewEncoder(w).Encode(ErrorResponse{Error: "Group not found"}) + return + } + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + json.NewEncoder(w).Encode(ErrorResponse{Error: "Failed to retrieve group"}) + return + } + + response := GroupResponse{ + GroupID: group.GroupId, + GroupName: group.GroupName, + } + + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(response) +} + diff --git a/internal/handlers/task_handlers.go b/internal/handlers/task_handlers.go new file mode 100644 index 0000000..a1322df --- /dev/null +++ b/internal/handlers/task_handlers.go @@ -0,0 +1,194 @@ +package handlers + +import ( + "5803024003/internal/db" + "database/sql" + "encoding/json" + "net/http" + "strconv" + + "github.com/gorilla/mux" +) + +type TaskRequest struct { + TaskName string `json:"task_name"` + TaskDescription string `json:"task_desc"` // Changed from *string to string to match SQL NOT NULL +} + +type TaskUpdateRequest struct { + TaskName string `json:"task_name"` + TaskDescription string `json:"task_desc"` // Changed from *string to string +} + +func CreateTaskHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + vars := mux.Vars(r) + groupIDStr := vars["groupId"] + + groupID, err := strconv.Atoi(groupIDStr) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(ErrorResponse{Error: "Invalid group ID"}) + return + } + + var req TaskRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(ErrorResponse{Error: "Invalid JSON format"}) + return + } + + if req.TaskName == "" { + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(ErrorResponse{Error: "Task name is required"}) + return + } + + if req.TaskDescription == "" { + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(ErrorResponse{Error: "Task description is required"}) + return + } + + if err := db.CreateTask(database, req.TaskName, req.TaskDescription, groupID); err != nil { + w.WriteHeader(http.StatusInternalServerError) + json.NewEncoder(w).Encode(ErrorResponse{Error: "Failed to create task"}) + return + } + + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(map[string]string{"message": "Task created successfully"}) +} + +func DisplayTasksHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + tasks, err := db.GetAllTasks(database) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + json.NewEncoder(w).Encode(ErrorResponse{Error: "Failed to retrieve tasks"}) + return + } + + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(tasks) +} + +func GetTaskHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + vars := mux.Vars(r) + taskIDStr := vars["taskId"] + + taskID, err := strconv.Atoi(taskIDStr) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(ErrorResponse{Error: "Invalid task ID"}) + return + } + + task, err := db.GetTaskByID(database, taskID) + if err == sql.ErrNoRows { + w.WriteHeader(http.StatusNotFound) + json.NewEncoder(w).Encode(ErrorResponse{Error: "Task not found"}) + return + } + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + json.NewEncoder(w).Encode(ErrorResponse{Error: "Failed to retrieve task"}) + return + } + + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(task) +} + +func MarkTaskDoneHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + vars := mux.Vars(r) + taskIDStr := vars["taskId"] + + taskID, err := strconv.Atoi(taskIDStr) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(ErrorResponse{Error: "Invalid task ID"}) + return + } + + if err := db.MarkTaskAsDone(database, taskID); err != nil { + w.WriteHeader(http.StatusInternalServerError) + json.NewEncoder(w).Encode(ErrorResponse{Error: "Failed to mark task as done"}) + return + } + + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(map[string]string{"message": "Task marked as done successfully"}) +} + +func RemoveTaskHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + vars := mux.Vars(r) + taskIDStr := vars["taskId"] + + taskID, err := strconv.Atoi(taskIDStr) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(ErrorResponse{Error: "Invalid task ID"}) + return + } + + if err := db.RemoveTask(database, taskID); err != nil { + w.WriteHeader(http.StatusInternalServerError) + json.NewEncoder(w).Encode(ErrorResponse{Error: "Failed to remove task"}) + return + } + + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(map[string]string{"message": "Task removed successfully"}) +} + +func UpdateTaskHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + vars := mux.Vars(r) + taskIDStr := vars["taskId"] + + taskID, err := strconv.Atoi(taskIDStr) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(ErrorResponse{Error: "Invalid task ID"}) + return + } + + var req TaskUpdateRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(ErrorResponse{Error: "Invalid JSON format"}) + return + } + + if req.TaskName == "" { + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(ErrorResponse{Error: "Task name is required"}) + return + } + + if req.TaskDescription == "" { + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(ErrorResponse{Error: "Task description is required"}) + return + } + + if err := db.UpdateTask(database, taskID, req.TaskName, req.TaskDescription); err != nil { + w.WriteHeader(http.StatusInternalServerError) + json.NewEncoder(w).Encode(ErrorResponse{Error: "Failed to update task"}) + return + } + + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(map[string]string{"message": "Task updated successfully"}) +} \ No newline at end of file