diff --git a/api/cmd/main.go b/api/cmd/main.go index d9cd685..98ae76f 100644 --- a/api/cmd/main.go +++ b/api/cmd/main.go @@ -5,16 +5,16 @@ import ( "syscall" "github.com/echo-webkom/ludo/api/config" + "github.com/echo-webkom/ludo/api/database" "github.com/echo-webkom/ludo/api/server" - "github.com/echo-webkom/ludo/api/service" "github.com/jesperkha/notifier" ) func main() { config := config.Load() notif := notifier.New() - service := service.New(config) - server := server.New(config, service) + db := database.NewTursoDB(config) + server := server.New(config, db) go server.ListenAndServe(notif) diff --git a/api/database/database.go b/api/database/database.go index 46ed36a..b043608 100644 --- a/api/database/database.go +++ b/api/database/database.go @@ -2,21 +2,27 @@ package database type Database interface { GetUserById(id uint) (User, error) + UpdateUser(id uint, user User) error + CreateUser(user User) (uint, error) DeleteUserById(id uint) error GetAllUsers() ([]User, error) - + GetItemById(id uint) (Item, error) - CreateItem(item Item) error + CreateItem(item Item) (uint, error) GetAllItems() ([]Item, error) DeleteItemByID(id uint) error - GetAllItemsFromLits(list List) ([]Item, error) - MoveItemToList(id uint, list List) error - ChangeItemTitle(id uint, title string) error - ChangeItemDescription(id uint, description string) error - - CreateNewRepo(name, url string) error - DeleteRepoById(id uint) error + GetAllItemsFromList(boardId, list uint) ([]Item, error) + MoveItemToList(id uint, list uint) error + UpdateItem(id uint, item Item) error + + GetAllBoards() ([]Board, error) + CreateBoard(board Board) (uint, error) + GetBoardById(id uint) (Board, error) + UpdateBoard(id uint, board Board) error + DeleteBoardById(id uint) error - // Username must be identical to GitHub username. - CreateUser(username string) error + GetBoardUsers(boardId uint) ([]User, error) + GetBoardItems(boardId uint) ([]Item, error) + AddUserToBoard(boardId, userId uint) error + RemoveUserFromBoard(boardId, userId uint) error } diff --git a/api/database/model.go b/api/database/model.go index 0af3117..6cf7436 100644 --- a/api/database/model.go +++ b/api/database/model.go @@ -1,40 +1,35 @@ package database import ( - "gorm.io/gorm" + "time" ) -type List int +type model struct { + ID uint `gorm:"primarykey" json:"id"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} -const ( - BACKLOG = iota - TODO - DOING - REVIEW - DONE -) +type Board struct { + model + Items []Item `json:"items"` + Users []User `gorm:"many2many:board_users;" json:"users"` +} type Item struct { - gorm.Model - RepoID uint - Repo Repo - Title string - Description string - CreatorID uint - Creator User - AssigneeID uint - Assignee User - ConnectedBranch string - List List + model + RepoName string `json:"repoName"` + RepoURL string `json:"repoURL"` + Title string `json:"title"` + Description string `json:"description"` + ConnectedBranch string `json:"connectedBranch"` + List uint `json:"list"` + Creator User `gorm:"many2many:item_user;" json:"creator"` + Assignee User `gorm:"many2many:item_user;" json:"assignee"` } type User struct { - gorm.Model - Name string -} - -type Repo struct { - gorm.Model - Name string - URL string + model + DisplayName string `json:"displayName"` + GithubUsername string `json:"githubUsername"` } diff --git a/api/database/query.go b/api/database/query.go index 4dc0883..4327641 100644 --- a/api/database/query.go +++ b/api/database/query.go @@ -7,18 +7,25 @@ import ( func (db *tursoDB) GetUserById(id uint) (User, error) { var user User if res := db.db.First(&user, id); res.Error != nil { - return User{}, errors.New("could not find usre with id") + return User{}, errors.New("could not find user with id") } return user, nil } -func (db *tursoDB) CreateUser(name string) error { - if res := db.db.Create(User{Name: name}); res.Error != nil { - return errors.New("could not create a new user") +func (db *tursoDB) UpdateUser(id uint, user User) error { + if res := db.db.Model(&User{}).Where("id = ?", id).Updates(user); res.Error != nil { + return errors.New("could not update user") } return nil } +func (db *tursoDB) CreateUser(user User) (uint, error) { + if res := db.db.Create(&user); res.Error != nil { + return 0, errors.New("could not create a new user") + } + return user.ID, nil +} + func (db *tursoDB) DeleteUserById(id uint) error { if res := db.db.Delete(&User{}, id); res.Error != nil { return errors.New("could not delete user") @@ -29,29 +36,29 @@ func (db *tursoDB) DeleteUserById(id uint) error { func (db *tursoDB) GetAllUsers() ([]User, error) { var users []User if res := db.db.Find(&users); res.Error != nil { - return nil, errors.New("courl not get all userts") + return nil, errors.New("could not get all users") } return users, nil } func (db *tursoDB) GetItemById(id uint) (Item, error) { var item Item - if res := db.db.First(&item, id); res.Error != nil { + if res := db.db.Preload("Creator").Preload("Assignee").First(&item, id); res.Error != nil { return Item{}, errors.New("could not get item") } return item, nil } -func (db *tursoDB) CreateItem(item Item) error { +func (db *tursoDB) CreateItem(item Item) (uint, error) { if res := db.db.Create(&item); res.Error != nil { - return errors.New("could not create item") + return 0, errors.New("could not create item") } - return nil + return item.ID, nil } func (db *tursoDB) GetAllItems() ([]Item, error) { var items []Item - if res := db.db.Find(&items); res.Error != nil { + if res := db.db.Preload("Creator").Preload("Assignee").Find(&items); res.Error != nil { return nil, errors.New("could not get all items") } return items, nil @@ -64,46 +71,118 @@ func (db *tursoDB) DeleteItemByID(id uint) error { return nil } -func (db *tursoDB) GetAllItemsFromLits(list List) ([]Item, error) { - var items []Item - if res := db.db.Find(&items, "list = ?", list); res.Error != nil { - return nil, errors.New("could not find all items from list") +func (db *tursoDB) GetAllItemsFromList(boardId, list uint) ([]Item, error) { + var board Board + if res := db.db.Preload("Items", "list = ?", list).First(&board, boardId); res.Error != nil { + return nil, errors.New("could not find board") } - return items, nil + + for i := range board.Items { + db.db.Preload("Creator").Preload("Assignee").First(&board.Items[i], board.Items[i].ID) + } + + return board.Items, nil } -func (db *tursoDB) MoveItemToList(id uint, list List) error { +func (db *tursoDB) MoveItemToList(id uint, list uint) error { if res := db.db.Model(&Item{}).Where("id = ?", id).Update("list", list); res.Error != nil { return errors.New("could not move item") } return nil } -func (db *tursoDB) ChangeItemTitle(id uint, title string) error { - if res := db.db.Model(&Item{}).Where("id = ?", id).Update("title", title); res.Error != nil { - return errors.New("could not change item title") +func (db *tursoDB) UpdateItem(id uint, item Item) error { + if res := db.db.Model(&Item{}).Where("id = ?", id).Updates(item); res.Error != nil { + return errors.New("could not update item") } return nil } -func (db *tursoDB) ChangeItemDescription(id uint, description string) error { - if res := db.db.Model(&Item{}).Where("id = ?", id).Update("description", description); res.Error != nil { - return errors.New("could not change item description") +func (db *tursoDB) GetAllBoards() ([]Board, error) { + var boards []Board + if res := db.db.Preload("Items").Preload("Users").Find(&boards); res.Error != nil { + return nil, errors.New("could not find boards") + } + return boards, nil +} + +func (db *tursoDB) CreateBoard(board Board) (uint, error) { + if res := db.db.Create(&board); res.Error != nil { + return 0, errors.New("could not create a new board") + } + return board.ID, nil +} + +func (db *tursoDB) GetBoardById(id uint) (Board, error) { + var board Board + if res := db.db.Preload("Items").Preload("Users").First(&board, id); res.Error != nil { + return Board{}, errors.New("could not find board") + } + return board, nil +} + +func (db *tursoDB) UpdateBoard(id uint, board Board) error { + if res := db.db.Model(&Board{}).Where("id = ?", id).Updates(board); res.Error != nil { + return errors.New("could not update board") } return nil } -func (db *tursoDB) CreateNewRepo(name, url string) error { - repo := Repo{Name: name, URL: url} - if res := db.db.Create(&repo); res.Error != nil { - return errors.New("could not create new user") +func (db *tursoDB) DeleteBoardById(id uint) error { + if res := db.db.Delete(&Board{}, id); res.Error != nil { + return errors.New("could not delete board") } return nil } -func (db *tursoDB) DeleteRepoById(id uint) error { - if res := db.db.Delete(&Repo{}, id); res.Error != nil { - return errors.New("could not delet the repo") +func (db *tursoDB) GetBoardUsers(boardId uint) ([]User, error) { + var board Board + if res := db.db.Preload("Users").First(&board, boardId); res.Error != nil { + return nil, errors.New("could not find board") } + return board.Users, nil +} + +func (db *tursoDB) GetBoardItems(boardId uint) ([]Item, error) { + var board Board + if res := db.db.Preload("Items").Preload("Items.Creator").Preload("Items.Assignee").First(&board, boardId); res.Error != nil { + return nil, errors.New("could not find board") + } + return board.Items, nil +} + +func (db *tursoDB) AddUserToBoard(boardId, userId uint) error { + var board Board + if res := db.db.First(&board, boardId); res.Error != nil { + return errors.New("could not find board") + } + + var user User + if res := db.db.First(&user, userId); res.Error != nil { + return errors.New("could not find user") + } + + if res := db.db.Model(&board).Association("Users").Append(&user); res != nil { + return errors.New("could not add user to board") + } + return nil } + +func (db *tursoDB) RemoveUserFromBoard(boardId, userId uint) error { + var board Board + if res := db.db.First(&board, boardId); res.Error != nil { + return errors.New("could not find board") + } + + var user User + if res := db.db.First(&user, userId); res.Error != nil { + return errors.New("could not find user") + } + + if res := db.db.Model(&board).Association("Users").Delete(&user); res != nil { + return errors.New("could not remove user from board") + } + + return nil +} \ No newline at end of file diff --git a/api/database/turso.go b/api/database/turso.go index e3656b8..1063569 100644 --- a/api/database/turso.go +++ b/api/database/turso.go @@ -32,7 +32,7 @@ func NewTursoDB(config *config.Config) Database { log.Fatal("could not load remote database", err) } - if err := db.AutoMigrate(&Item{}, &User{}, &Repo{}); err != nil { + if err := db.AutoMigrate(&Item{}, &User{}, &Board{}); err != nil { log.Fatalf("migration: %v", err) } return &tursoDB{db} diff --git a/api/server/handler.go b/api/server/handler.go index c976032..bf49734 100644 --- a/api/server/handler.go +++ b/api/server/handler.go @@ -1,11 +1,420 @@ package server import ( + "encoding/json" + "io" "net/http" + "strconv" + + "github.com/echo-webkom/ludo/api/database" + "github.com/go-chi/chi/v5" ) +type ResponseId struct { + ID uint `json:"id"` +} + +func JSON(w http.ResponseWriter, data any) { + resp, err := json.Marshal(&data) + if err != nil { + http.Error(w, "parse error", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.Write(resp) +} + +func getId(w http.ResponseWriter, id string) uint { + itemId, err := strconv.Atoi(id) + if err != nil || itemId < 0 { + http.Error(w, "not valid item id", http.StatusBadRequest) + return 0 + } + + return uint(itemId) +} + func pingHandler() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("pong")) } } + +func usersRouter(db database.Database) *chi.Mux { + router := chi.NewRouter() + + router.Get("/", func(w http.ResponseWriter, r *http.Request) { + users, err := db.GetAllUsers() + if err != nil { + http.Error(w, "failed to get all users", http.StatusNotFound) + return + } + JSON(w, users) + }) + + router.Post("/", func(w http.ResponseWriter, r *http.Request) { + b, err := io.ReadAll(r.Body) + defer r.Body.Close() + + if err != nil { + http.Error(w, "failed to read body", http.StatusInternalServerError) + return + } + + var user database.User + if err := json.Unmarshal(b, &user); err != nil { + http.Error(w, "invalid user data", http.StatusBadRequest) + return + } + + id, err := db.CreateUser(user) + if err != nil { + http.Error(w, "database error", http.StatusInternalServerError) + return + } + + res := ResponseId{id} + JSON(w, res) + }) + + router.Get("/{userId}", func(w http.ResponseWriter, r *http.Request) { + userIdString := r.PathValue("userId") + userId, err := strconv.Atoi(userIdString) + + if err != nil || userId < 0 { + http.Error(w, "not a valid user id", http.StatusBadRequest) + return + } + + user, err := db.GetUserById(uint(userId)) + if err != nil { + http.Error(w, "could not find user", http.StatusNotFound) + return + } + JSON(w, user) + }) + + router.Delete("/{userId}", func(w http.ResponseWriter, r *http.Request) { + userIdString := r.PathValue("userId") + + userId, err := strconv.Atoi(userIdString) + if err != nil || userId < 0 { + http.Error(w, "not a valid user id", http.StatusBadRequest) + return + } + + if err := db.DeleteUserById(uint(userId)); err != nil { + http.Error(w, "not valid user id", http.StatusNotFound) + return + } + + w.WriteHeader(http.StatusNoContent) + }) + + router.Patch("/{userId}", func(w http.ResponseWriter, r *http.Request) { + userIdString := r.PathValue("userId") + + userId := getId(w, userIdString) + + b, err := io.ReadAll(r.Body) + defer r.Body.Close() + if err != nil { + http.Error(w, "failed to read body", http.StatusInternalServerError) + return + } + + var user database.User + if err := json.Unmarshal(b, &user); err != nil { + http.Error(w, "invalid user data", http.StatusBadRequest) + return + } + + if err := db.UpdateUser(userId, user); err != nil { + http.Error(w, "could not update user", http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusNoContent) + }) + return router +} + +func itemRouter(db database.Database) *chi.Mux { + router := chi.NewRouter() + router.Get("/", func(w http.ResponseWriter, r *http.Request) { + items, err := db.GetAllItems() + if err != nil { + http.Error(w, "could not find items", http.StatusNotFound) + return + } + JSON(w, items) + }) + + router.Post("/", func(w http.ResponseWriter, r *http.Request) { + b, err := io.ReadAll(r.Body) + defer r.Body.Close() + if err != nil { + http.Error(w, "failed to read body", http.StatusInternalServerError) + return + } + + var item database.Item + if err := json.Unmarshal(b, &item); err != nil { + http.Error(w, "invalid item data", http.StatusBadRequest) + return + } + + itemId, err := db.CreateItem(item) + if err != nil { + http.Error(w, "database error", http.StatusInternalServerError) + return + } + + res := ResponseId{itemId} + JSON(w, res) + }) + + router.Get("/{itemId}", func(w http.ResponseWriter, r *http.Request) { + itemIdString := r.PathValue("itemId") + itemId := getId(w, itemIdString) + if itemId == 0 { + return + } + + item, err := db.GetItemById(uint(itemId)) + if err != nil { + http.Error(w, "could not find item", http.StatusNotFound) + return + } + JSON(w, item) + }) + + router.Delete("/{itemId}", func(w http.ResponseWriter, r *http.Request) { + itemIdString := r.PathValue("itemId") + itemId := getId(w, itemIdString) + + if err := db.DeleteItemByID(uint(itemId)); err != nil { + http.Error(w, "could not delete item", http.StatusNotFound) + return + } + + w.WriteHeader(http.StatusNoContent) + }) + + router.Patch("/{itemId}", func(w http.ResponseWriter, r *http.Request) { + itemIdString := r.PathValue("itemId") + itemId := getId(w, itemIdString) + + b, err := io.ReadAll(r.Body) + defer r.Body.Close() + if err != nil { + http.Error(w, "failed to read body", http.StatusInternalServerError) + return + } + + var item database.Item + if err := json.Unmarshal(b, &item); err != nil { + http.Error(w, "invalid item data", http.StatusBadRequest) + return + } + + if err := db.UpdateItem(itemId, item); err != nil { + http.Error(w, "could not update item", http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusNoContent) + }) + + router.Patch("/{itemId}/move/{listId}", func(w http.ResponseWriter, r *http.Request) { + itemIdString := r.PathValue("itemId") + listIdString := r.PathValue("listId") + + itemId := getId(w, itemIdString) + listId := getId(w, listIdString) + + if err := db.MoveItemToList(itemId, listId); err != nil { + http.Error(w, "could not move item to list", http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusNoContent) + }) + + return router +} + +func boardsRouter(db database.Database) *chi.Mux { + router := chi.NewRouter() + + router.Get("/", func(w http.ResponseWriter, r *http.Request) { + boards, err := db.GetAllBoards() + if err != nil { + http.Error(w, "failed to get all boards", http.StatusNotFound) + return + } + + JSON(w, boards) + }) + + router.Post("/", func(w http.ResponseWriter, r *http.Request) { + b, err := io.ReadAll(r.Body) + defer r.Body.Close() + + if err != nil { + http.Error(w, "failed to read body", http.StatusInternalServerError) + return + } + + var board database.Board + if err := json.Unmarshal(b, &board); err != nil { + http.Error(w, "invalid board data", http.StatusBadRequest) + return + } + + id, err := db.CreateBoard(board) + if err != nil { + http.Error(w, "database error", http.StatusInternalServerError) + return + } + + res := ResponseId{id} + JSON(w, res) + }) + + router.Get("/{boardId}", func(w http.ResponseWriter, r *http.Request) { + boardIdString := r.PathValue("boardId") + boardId := getId(w, boardIdString) + + board, err := db.GetBoardById(boardId) + if err != nil { + http.Error(w, "failed to get board by id", http.StatusNotFound) + return + } + + JSON(w, board) + }) + + router.Patch("/{boardId}", func(w http.ResponseWriter, r *http.Request) { + boardIdString := r.PathValue("boardId") + boardId := getId(w, boardIdString) + + b, err := io.ReadAll(r.Body) + defer r.Body.Close() + if err != nil { + http.Error(w, "failed to read body", http.StatusInternalServerError) + return + } + + var board database.Board + if err := json.Unmarshal(b, &board); err != nil { + http.Error(w, "invalid board data", http.StatusBadRequest) + return + } + + if err := db.UpdateBoard(boardId, board); err != nil { + http.Error(w, "could not update board", http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusNoContent) + }) + + router.Delete("/{boardId}", func(w http.ResponseWriter, r *http.Request) { + boardIdString := r.PathValue("boardId") + boardId := getId(w, boardIdString) + + if err := db.DeleteBoardById(boardId); err != nil { + http.Error(w, "could not delete board", http.StatusNotFound) + return + } + + w.WriteHeader(http.StatusNoContent) + }) + + router.Get("/{boardId}/users", func(w http.ResponseWriter, r *http.Request) { + boardIdString := r.PathValue("boardId") + boardId := getId(w, boardIdString) + + + users, err := db.GetBoardUsers(boardId) + if err != nil { + http.Error(w, "could not get board users", http.StatusNotFound) + return + } + + JSON(w, users) + }) + + + router.Get("/{boardId}/items", func(w http.ResponseWriter, r *http.Request) { + boardIdString := r.PathValue("boardId") + boardId := getId(w, boardIdString) + + + items, err := db.GetBoardItems(boardId) + if err != nil { + http.Error(w, "could not get board items", http.StatusNotFound) + return + } + + JSON(w, items) + }) + + + router.Post("/{boardId}/users/{userId}", func(w http.ResponseWriter, r *http.Request) { + boardIdString := r.PathValue("boardId") + userIdString := r.PathValue("userId") + + boardId := getId(w, boardIdString) + + userId := getId(w, userIdString) + + if err := db.AddUserToBoard(boardId, userId); err != nil { + http.Error(w, "could not add user to board", http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusNoContent) + }) + + router.Delete("/{boardId}/users/{userId}", func(w http.ResponseWriter, r *http.Request) { + boardIdString := r.PathValue("boardId") + userIdString := r.PathValue("userId") + + boardId := getId(w, boardIdString) + + userId := getId(w, userIdString) + + if err := db.RemoveUserFromBoard(boardId, userId); err != nil { + http.Error(w, "could not remove user from board", http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusNoContent) + }) + + router.Get("/{boardId}/lists/{listId}/items", func(w http.ResponseWriter, r *http.Request) { + boardIdString := r.PathValue("boardId") + listIdString := r.PathValue("listId") + + boardId := getId(w, boardIdString) + + + listId := getId(w, listIdString) + if listId == 0 { + return + } + + items, err := db.GetAllItemsFromList(boardId, listId) + if err != nil { + http.Error(w, "could not get items from list", http.StatusNotFound) + return + } + + JSON(w, items) + }) + + return router +} \ No newline at end of file diff --git a/api/server/server.go b/api/server/server.go index 456393e..434231b 100644 --- a/api/server/server.go +++ b/api/server/server.go @@ -6,7 +6,7 @@ import ( "net/http" "github.com/echo-webkom/ludo/api/config" - "github.com/echo-webkom/ludo/api/service" + "github.com/echo-webkom/ludo/api/database" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "github.com/jesperkha/notifier" @@ -17,11 +17,14 @@ type Server struct { router http.Handler } -func New(config *config.Config, service *service.Service) *Server { +func New(config *config.Config, db database.Database) *Server { r := chi.NewRouter() r.Use(middleware.Logger) r.Get("/ping", pingHandler()) + r.Mount("/users", usersRouter(db)) + r.Mount("/items", itemRouter(db)) + r.Mount("/boards", boardsRouter(db)) return &Server{ router: r, diff --git a/api/service/service.go b/api/service/service.go deleted file mode 100644 index bb172b3..0000000 --- a/api/service/service.go +++ /dev/null @@ -1,18 +0,0 @@ -package service - -import ( - "github.com/echo-webkom/ludo/api/config" - "github.com/echo-webkom/ludo/api/database" -) - -type Service struct { - config *config.Config - db database.Database -} - -func New(config *config.Config) *Service { - return &Service{ - config, - database.NewTursoDB(config), - } -}