diff --git a/api/README.md b/api/README.md index deebd36..1d038ac 100644 --- a/api/README.md +++ b/api/README.md @@ -29,6 +29,7 @@ Boards are containers for items and have users assosiated with them. A board may | DELETE | `/boards/{boardId}` | - | Delete a board | | GET | `/boards/{boardId}/users` | `[]User` | Get all users in board | | GET | `/boards/{boardId}/items` | `[]Item` | Get all items in board | +| POST | `/boards/{boardId}/items` | `ID` | Create a new item in board | | POST | `/boards/{boardId}/users/{userId}` | - | Add user to board | | DELETE | `/boards/{boardId}/users/{userId}` | - | Remove user from board | | GET | `/boards/{boardId}/status/{status}/items` | `[]Item` | Get all items in board with given `Status` | @@ -37,8 +38,6 @@ Boards are containers for items and have users assosiated with them. A board may | Method | Path | Response Type | Description | | ------ | ---------------------- | ------------- | ----------------------- | -| GET | `/items` | `[]Item` | Get all items | -| POST | `/items` | `ID` | Create an item | | GET | `/items/{itemId}` | `Item` | Get item by ID | | PATCH | `/items/{itemId}` | - | Update an existing item | | DELETE | `/items/{itemId}` | - | Delete an item | diff --git a/api/database/database.go b/api/database/database.go index 5def376..1eb7b1f 100644 --- a/api/database/database.go +++ b/api/database/database.go @@ -4,6 +4,7 @@ import ( "log" "strings" + "github.com/echo-webkom/ludo/pkg/model" _ "github.com/mattn/go-sqlite3" _ "github.com/tursodatabase/libsql-client-go/libsql" "gorm.io/driver/sqlite" @@ -11,12 +12,13 @@ import ( "gorm.io/gorm/logger" ) +// Implements LudoService type Database struct { db *gorm.DB } func newDatabase(db *gorm.DB) *Database { - if err := db.AutoMigrate(&Board{}, &Item{}, &User{}); err != nil { + if err := db.AutoMigrate(&model.Board{}, &model.Item{}, &model.User{}); err != nil { log.Fatalf("migration: %v", err) } return &Database{db} diff --git a/api/database/database_test.go b/api/database/database_test.go index 8adf6f3..d821143 100644 --- a/api/database/database_test.go +++ b/api/database/database_test.go @@ -4,6 +4,7 @@ import ( "os" "testing" + "github.com/echo-webkom/ludo/pkg/model" "github.com/stretchr/testify/assert" ) @@ -18,7 +19,7 @@ func createTestDB(t *testing.T) *Database { }) // Auto migrate schema - err = db.db.AutoMigrate(&Board{}, &Item{}, &User{}) + err = db.db.AutoMigrate(&model.Board{}, &model.Item{}, &model.User{}) assert.NoError(t, err) return db @@ -28,26 +29,26 @@ func TestUserMethods(t *testing.T) { db := createTestDB(t) // Create user - user := User{DisplayName: "Test User", GithubUsername: "testuser"} - id, err := db.CreateUser(user) + user := model.User{DisplayName: "Test User", GithubUsername: "testuser"} + id, err := db.NewUser(user) assert.NoError(t, err) // Get user - u, err := db.GetUserById(id) + u, err := db.User(id) assert.NoError(t, err) assert.Equal(t, "Test User", u.DisplayName) // Get all users - users, err := db.GetAllUsers() + users, err := db.Users() assert.NoError(t, err) assert.Len(t, users, 1) // Delete user - err = db.DeleteUserById(id) + err = db.DeleteUser(id) assert.NoError(t, err) // Confirm user deleted - _, err = db.GetUserById(id) + _, err = db.User(id) assert.Error(t, err) } @@ -55,27 +56,27 @@ func TestBoardMethods(t *testing.T) { db := createTestDB(t) // Create board - board := Board{Title: "Board 1", RepoURL: "https://github.com/example/repo"} - _, err := db.CreateBoard(board) + board := model.Board{Title: "Board 1", RepoURL: "https://github.com/example/repo"} + _, err := db.NewBoard(board) assert.NoError(t, err) // Get boards - boards, err := db.GetAllBoards() + boards, err := db.Boards() assert.NoError(t, err) assert.Len(t, boards, 1) // Get by ID - got, err := db.GetBoardById(boards[0].ID) + got, err := db.Board(boards[0].ID) assert.NoError(t, err) assert.Equal(t, "Board 1", got.Title) // Update board got.Title = "Updated" - err = db.UpdateBoard(got, got.ID) + err = db.UpdateBoard(got.ID, got) assert.NoError(t, err) // Get updated board - got2, err := db.GetBoardById(got.ID) + got2, err := db.Board(got.ID) assert.NoError(t, err) assert.Equal(t, "Updated", got2.Title) @@ -88,27 +89,27 @@ func TestItemMethods(t *testing.T) { db := createTestDB(t) // Create board - board := Board{Title: "Board", RepoURL: "url"} - _, err := db.CreateBoard(board) + board := model.Board{Title: "Board", RepoURL: "url"} + id, err := db.NewBoard(board) assert.NoError(t, err) - boards, _ := db.GetAllBoards() + boards, _ := db.Boards() // Create item - item := Item{BoardID: boards[0].ID, Title: "Item 1", Status: StatusBacklog} - itemID, err := db.CreateItem(item) + item := model.Item{BoardID: boards[0].ID, Title: "Item 1", Status: model.StatusBacklog} + itemID, err := db.NewItem(id, item) assert.NoError(t, err) // Get item - got, err := db.GetItemById(itemID) + got, err := db.Item(itemID) assert.NoError(t, err) assert.Equal(t, "Item 1", got.Title) // Change status - err = db.ChangeItemStatus(itemID, StatusInProgress) + err = db.ChangeItemStatus(itemID, model.StatusInProgress) assert.NoError(t, err) // Get items with status - items, err := db.GetAllItemsWithStatus(boards[0].ID, StatusInProgress) + items, err := db.GetAllItemsWithStatus(boards[0].ID, model.StatusInProgress) assert.NoError(t, err) assert.Len(t, items, 1) @@ -121,27 +122,27 @@ func TestItemMethods(t *testing.T) { assert.Equal(t, "some-data", data) // Delete item - assert.NoError(t, db.DeleteItemByID(itemID)) + assert.NoError(t, db.DeleteItem(itemID)) } func TestBoardUsers(t *testing.T) { db := createTestDB(t) // Create user and board - user := User{DisplayName: "A"} - userId, _ := db.CreateUser(user) - board := Board{Title: "Board"} + user := model.User{DisplayName: "A"} + userId, _ := db.NewUser(user) + board := model.Board{Title: "Board"} - _, err := db.CreateBoard(board) + _, err := db.NewBoard(board) assert.NoError(t, err) - boards, _ := db.GetAllBoards() + boards, _ := db.Boards() // Add user to board assert.NoError(t, db.AddUserToBoard(boards[0].ID, userId)) // Get board users - users, err := db.GetBoardUsers(boards[0].ID) + users, err := db.BoardUsers(boards[0].ID) assert.NoError(t, err) assert.Len(t, users, 1) @@ -149,7 +150,7 @@ func TestBoardUsers(t *testing.T) { assert.NoError(t, db.RemoveUserFromBoard(boards[0].ID, userId)) // Check no users left - users, err = db.GetBoardUsers(boards[0].ID) + users, err = db.BoardUsers(boards[0].ID) assert.NoError(t, err) assert.Len(t, users, 0) } @@ -157,19 +158,21 @@ func TestBoardUsers(t *testing.T) { func TestGetBoardItemsByStatus(t *testing.T) { db := createTestDB(t) - board := Board{Title: "Board"} - _, err := db.CreateBoard(board) + board := model.Board{Title: "Board"} + _, err := db.NewBoard(board) assert.NoError(t, err) - boards, _ := db.GetAllBoards() + boards, _ := db.Boards() + + id := boards[0].ID // Create multiple items with different statuses - db.CreateItem(Item{BoardID: boards[0].ID, Title: "Item 1", Status: StatusBacklog}) - db.CreateItem(Item{BoardID: boards[0].ID, Title: "Item 2", Status: StatusInProgress}) - db.CreateItem(Item{BoardID: boards[0].ID, Title: "Item 3", Status: StatusBacklog}) + db.NewItem(id, model.Item{BoardID: boards[0].ID, Title: "Item 1", Status: model.StatusBacklog}) + db.NewItem(id, model.Item{BoardID: boards[0].ID, Title: "Item 2", Status: model.StatusInProgress}) + db.NewItem(id, model.Item{BoardID: boards[0].ID, Title: "Item 3", Status: model.StatusBacklog}) // Fetch items with StatusBacklog - items, err := db.GetBoardItemsByStatus(boards[0].ID, StatusBacklog) + items, err := db.BoardItemsWithStatus(boards[0].ID, model.StatusBacklog) assert.NoError(t, err) assert.Len(t, items, 2) } diff --git a/api/database/query.go b/api/database/query.go index 45c124d..a5ef59b 100644 --- a/api/database/query.go +++ b/api/database/query.go @@ -1,140 +1,143 @@ package database -func (db *Database) GetUserById(id uint) (User, error) { - var user User +import "github.com/echo-webkom/ludo/pkg/model" + +func (db *Database) User(id uint) (model.User, error) { + var user model.User if err := db.db.First(&user, id).Error; err != nil { - return User{}, err + return model.User{}, err } return user, nil } -func (db *Database) CreateUser(user User) (uint, error) { +func (db *Database) NewUser(user model.User) (uint, error) { if err := db.db.Create(&user).Error; err != nil { return 0, err } return user.ID, nil } -func (db *Database) DeleteUserById(id uint) error { - if err := db.db.Delete(&User{}, id).Error; err != nil { +func (db *Database) DeleteUser(id uint) error { + if err := db.db.Delete(&model.User{}, id).Error; err != nil { return err } return nil } -func (db *Database) GetAllUsers() ([]User, error) { - var users []User +func (db *Database) Users() ([]model.User, error) { + var users []model.User if err := db.db.Find(&users).Error; err != nil { return nil, err } return users, nil } -func (db *Database) GetItemById(id uint) (Item, error) { - var item Item +func (db *Database) Item(id uint) (model.Item, error) { + var item model.Item if err := db.db.First(&item, id).Error; err != nil { - return Item{}, err + return model.Item{}, err } return item, nil } -func (db *Database) CreateItem(item Item) (id uint, err error) { +func (db *Database) NewItem(boardId uint, item model.Item) (id uint, err error) { + item.BoardID = boardId if err := db.db.Create(&item).Error; err != nil { return id, err } return item.ID, nil } -func (db *Database) GetAllItems() ([]Item, error) { - var items []Item +func (db *Database) GetAllItems() ([]model.Item, error) { + var items []model.Item if err := db.db.Find(&items).Error; err != nil { return nil, err } return items, nil } -func (db *Database) DeleteItemByID(id uint) error { - if err := db.db.Delete(&Item{}, id).Error; err != nil { +func (db *Database) DeleteItem(id uint) error { + if err := db.db.Delete(&model.Item{}, id).Error; err != nil { return err } return nil } -func (db *Database) GetAllItemsWithStatus(boardId uint, list Status) ([]Item, error) { - var items []Item +func (db *Database) GetAllItemsWithStatus(boardId uint, list model.Status) ([]model.Item, error) { + var items []model.Item if err := db.db.Where("board_id = ? AND status = ?", boardId, list).Find(&items).Error; err != nil { return nil, err } return items, nil } -func (db *Database) ChangeItemStatus(id uint, list Status) error { - if err := db.db.Model(&Item{}).Where("id = ?", id).Update("status", list).Error; err != nil { +func (db *Database) ChangeItemStatus(id uint, list model.Status) error { + if err := db.db.Model(&model.Item{}).Where("id = ?", id).Update("status", list).Error; err != nil { return err } return nil } func (db *Database) SetItemData(id uint, data string) error { - if err := db.db.Model(&Item{}).Where("id = ?", id).Update("data", data).Error; err != nil { + if err := db.db.Model(&model.Item{}).Where("id = ?", id).Update("data", data).Error; err != nil { return err } return nil } func (db *Database) GetItemData(id uint) (data string, err error) { - var item Item + var item model.Item if err := db.db.First(&item, id).Error; err != nil { return data, err } return item.Data, nil } -func (db *Database) GetAllBoards() (boards []Board, err error) { +func (db *Database) Boards() (boards []model.Board, err error) { if err := db.db.Find(&boards).Error; err != nil { return boards, err } return boards, err } -func (db *Database) CreateBoard(board Board) (id uint, err error) { +func (db *Database) NewBoard(board model.Board) (id uint, err error) { if err := db.db.Create(&board).Error; err != nil { return id, err } return board.ID, nil } -func (db *Database) GetBoardById(id uint) (board Board, err error) { +func (db *Database) Board(id uint) (board model.Board, err error) { if err := db.db.Find(&board, id).Error; err != nil { return board, err } return board, nil } -func (db *Database) UpdateBoard(board Board, id uint) error { - if err := db.db.Model(&Board{}).Where("id = ?", id).Updates(board).Error; err != nil { +func (db *Database) UpdateBoard(id uint, board model.Board) error { + if err := db.db.Model(&model.Board{}).Where("id = ?", id).Updates(board).Error; err != nil { return err } return nil } func (db *Database) DeleteBoard(id uint) error { - if err := db.db.Delete(&Board{}, id).Error; err != nil { + if err := db.db.Delete(&model.Board{}, id).Error; err != nil { return err } return nil } -func (d *Database) GetBoardUsers(boardId uint) ([]User, error) { - var board Board +func (d *Database) BoardUsers(boardId uint) ([]model.User, error) { + var board model.Board if err := d.db.Preload("Users").First(&board, boardId).Error; err != nil { return nil, err } return board.Users, nil } -func (d *Database) GetBoardItems(boardId uint) ([]Item, error) { - var items []Item +func (d *Database) BoardItems(boardId uint) ([]model.Item, error) { + var items []model.Item if err := d.db.Where("board_id = ?", boardId).Find(&items).Error; err != nil { return nil, err } @@ -142,11 +145,11 @@ func (d *Database) GetBoardItems(boardId uint) ([]Item, error) { } func (d *Database) AddUserToBoard(boardId, userId uint) error { - var board Board + var board model.Board if err := d.db.First(&board, boardId).Error; err != nil { return err } - var user User + var user model.User if err := d.db.First(&user, userId).Error; err != nil { return err } @@ -154,35 +157,39 @@ func (d *Database) AddUserToBoard(boardId, userId uint) error { } func (d *Database) RemoveUserFromBoard(boardId, userId uint) error { - var board Board + var board model.Board if err := d.db.First(&board, boardId).Error; err != nil { return err } - var user User + var user model.User if err := d.db.First(&user, userId).Error; err != nil { return err } return d.db.Model(&board).Association("Users").Delete(&user) } -func (d *Database) GetBoardItemsByStatus(boardId uint, status Status) ([]Item, error) { - var items []Item +func (d *Database) BoardItemsWithStatus(boardId uint, status model.Status) ([]model.Item, error) { + var items []model.Item if err := d.db.Where("board_id = ? AND status = ?", boardId, status).Find(&items).Error; err != nil { return nil, err } return items, nil } -func (db *Database) UpdateItem(item Item, id uint) error { - if err := db.db.Model(&Item{}).Where("id = ?", id).Updates(item).Error; err != nil { +func (db *Database) UpdateItem(id uint, item model.Item) error { + if err := db.db.Model(&model.Item{}).Where("id = ?", id).Updates(item).Error; err != nil { return err } return nil } -func (db *Database) UpdateUser(user User, id uint) error { - if err := db.db.Model(&User{}).Where("id = ?", id).Updates(user).Error; err != nil { +func (db *Database) UpdateUser(id uint, user model.User) error { + if err := db.db.Model(&model.User{}).Where("id = ?", id).Updates(user).Error; err != nil { return err } return nil } + +func (db *Database) ItemData(id uint) (string, error) { + return "", nil +} diff --git a/api/server/routes/boards.go b/api/server/routes/boards.go index 77af668..45eb98b 100644 --- a/api/server/routes/boards.go +++ b/api/server/routes/boards.go @@ -4,17 +4,18 @@ import ( "net/http" "strconv" - "github.com/echo-webkom/ludo/api/database" "github.com/echo-webkom/ludo/api/rest" + "github.com/echo-webkom/ludo/pkg/model" + "github.com/echo-webkom/ludo/pkg/service" "github.com/go-chi/chi/v5" ) -func BoardsHandler(db *database.Database) chi.Router { +func BoardsHandler(s service.LudoService) chi.Router { r := chi.NewRouter() // Get all boards r.Get("/", rest.Handler(func(r rest.Request) int { - if boards, err := db.GetAllBoards(); err == nil { + if boards, err := s.Boards(); err == nil { return r.RespondJSON(&boards) } return http.StatusInternalServerError @@ -22,17 +23,17 @@ func BoardsHandler(db *database.Database) chi.Router { // Create board r.Post("/", rest.Handler(func(r rest.Request) int { - var board database.Board + var board model.Board if err := r.ParseJSON(&board); err != nil { return http.StatusBadRequest } - id, err := db.CreateBoard(board) + id, err := s.NewBoard(board) if err != nil { return http.StatusInternalServerError } - return r.RespondJSON(&database.ID{ID: id}) + return r.RespondJSON(&model.ID{ID: id}) })) r.Route("/{id}", func(r chi.Router) { @@ -40,7 +41,7 @@ func BoardsHandler(db *database.Database) chi.Router { // Get board by id r.Get("/", rest.Handler(func(r rest.Request) int { - if board, err := db.GetBoardById(r.ContextValue(idKey).(uint)); err == nil { + if board, err := s.Board(r.ContextValue(idKey).(uint)); err == nil { return r.RespondJSON(&board) } return http.StatusInternalServerError @@ -48,12 +49,12 @@ func BoardsHandler(db *database.Database) chi.Router { // Update board r.Patch("/", rest.Handler(func(r rest.Request) int { - var board database.Board + var board model.Board if err := r.ParseJSON(&board); err != nil { return http.StatusBadRequest } - if err := db.UpdateBoard(board, r.ContextValue(idKey).(uint)); err != nil { + if err := s.UpdateBoard(r.ContextValue(idKey).(uint), board); err != nil { return http.StatusInternalServerError } @@ -62,7 +63,7 @@ func BoardsHandler(db *database.Database) chi.Router { // Delete baord r.Delete("/", rest.Handler(func(r rest.Request) int { - if err := db.DeleteBoard(r.ContextValue(idKey).(uint)); err != nil { + if err := s.DeleteBoard(r.ContextValue(idKey).(uint)); err != nil { return http.StatusInternalServerError } return http.StatusOK @@ -70,12 +71,34 @@ func BoardsHandler(db *database.Database) chi.Router { // Get all items in board r.Get("/items", rest.Handler(func(r rest.Request) int { - if items, err := db.GetBoardItems(r.ContextValue(idKey).(uint)); err == nil { + if items, err := s.BoardItems(r.ContextValue(idKey).(uint)); err == nil { return r.RespondJSON(&items) } return http.StatusInternalServerError })) + // Create item + r.Post("/items", rest.Handler(func(r rest.Request) int { + var item model.Item + if err := r.ParseJSON(&item); err != nil { + return http.StatusBadRequest + } + + boardId := r.ContextValue(idKey).(uint) + + // Check if board exists + if _, err := s.Board(boardId); err != nil { + return http.StatusNotFound + } + + id, err := s.NewItem(boardId, item) + if err != nil { + return http.StatusInternalServerError + } + + return r.RespondJSON(&model.ID{ID: id}) + })) + // Get all items with status r.Get("/status/{status}/items", rest.Handler(func(r rest.Request) int { status, err := strconv.Atoi(r.R.PathValue("status")) @@ -83,7 +106,7 @@ func BoardsHandler(db *database.Database) chi.Router { return http.StatusBadRequest } - items, err := db.GetBoardItemsByStatus(r.ContextValue(idKey).(uint), database.Status(status)) + items, err := s.BoardItemsWithStatus(r.ContextValue(idKey).(uint), model.Status(status)) if err != nil { return http.StatusInternalServerError } @@ -93,7 +116,7 @@ func BoardsHandler(db *database.Database) chi.Router { // Get all users in board r.Get("/users", rest.Handler(func(r rest.Request) int { - if users, err := db.GetBoardUsers(r.ContextValue(idKey).(uint)); err == nil { + if users, err := s.BoardUsers(r.ContextValue(idKey).(uint)); err == nil { return r.RespondJSON(&users) } return http.StatusInternalServerError @@ -106,7 +129,7 @@ func BoardsHandler(db *database.Database) chi.Router { return http.StatusBadRequest } - if err := db.AddUserToBoard(r.ContextValue(idKey).(uint), uint(userId)); err != nil { + if err := s.AddUserToBoard(r.ContextValue(idKey).(uint), uint(userId)); err != nil { return http.StatusInternalServerError } @@ -120,7 +143,7 @@ func BoardsHandler(db *database.Database) chi.Router { return http.StatusBadRequest } - if err := db.RemoveUserFromBoard(r.ContextValue(idKey).(uint), uint(userId)); err != nil { + if err := s.RemoveUserFromBoard(r.ContextValue(idKey).(uint), uint(userId)); err != nil { return http.StatusInternalServerError } diff --git a/api/server/routes/items.go b/api/server/routes/items.go index f4fdfcb..110e40f 100644 --- a/api/server/routes/items.go +++ b/api/server/routes/items.go @@ -4,43 +4,21 @@ import ( "io" "net/http" - "github.com/echo-webkom/ludo/api/database" "github.com/echo-webkom/ludo/api/rest" + "github.com/echo-webkom/ludo/pkg/model" + "github.com/echo-webkom/ludo/pkg/service" "github.com/go-chi/chi/v5" ) -func ItemsHandler(db *database.Database) chi.Router { +func ItemsHandler(s service.LudoService) chi.Router { r := chi.NewRouter() - // Get all items - r.Get("/", rest.Handler(func(r rest.Request) int { - if items, err := db.GetAllItems(); err == nil { - return r.RespondJSON(&items) - } - return http.StatusInternalServerError - })) - - // Create item - r.Post("/", rest.Handler(func(r rest.Request) int { - var item database.Item - if err := r.ParseJSON(&item); err != nil { - return http.StatusBadRequest - } - - id, err := db.CreateItem(item) - if err != nil { - return http.StatusInternalServerError - } - - return r.RespondJSON(&database.ID{ID: id}) - })) - r.Route("/{id}", func(r chi.Router) { r.Use(idMiddleware) // Get item by id r.Get("/", rest.Handler(func(r rest.Request) int { - if item, err := db.GetItemById(r.ContextValue(idKey).(uint)); err == nil { + if item, err := s.Item(r.ContextValue(idKey).(uint)); err == nil { return r.RespondJSON(&item) } return http.StatusInternalServerError @@ -48,12 +26,12 @@ func ItemsHandler(db *database.Database) chi.Router { // Update item r.Patch("/", rest.Handler(func(r rest.Request) int { - var item database.Item + var item model.Item if err := r.ParseJSON(&item); err != nil { return http.StatusBadRequest } - if err := db.UpdateItem(item, r.ContextValue(idKey).(uint)); err != nil { + if err := s.UpdateItem(r.ContextValue(idKey).(uint), item); err != nil { return http.StatusInternalServerError } @@ -62,7 +40,7 @@ func ItemsHandler(db *database.Database) chi.Router { // Delete item by id r.Delete("/", rest.Handler(func(r rest.Request) int { - if err := db.DeleteItemByID(r.ContextValue(idKey).(uint)); err != nil { + if err := s.DeleteItem(r.ContextValue(idKey).(uint)); err != nil { return http.StatusInternalServerError } return http.StatusOK @@ -70,7 +48,7 @@ func ItemsHandler(db *database.Database) chi.Router { // Get item data r.Get("/data", rest.Handler(func(r rest.Request) int { - if item, err := db.GetItemById(r.ContextValue(idKey).(uint)); err == nil { + if item, err := s.Item(r.ContextValue(idKey).(uint)); err == nil { return r.RespondString(item.Data) } return http.StatusInternalServerError @@ -84,7 +62,7 @@ func ItemsHandler(db *database.Database) chi.Router { } id := r.ContextValue(idKey).(uint) - if err := db.SetItemData(id, string(data)); err != nil { + if err := s.SetItemData(id, string(data)); err != nil { return http.StatusInternalServerError } diff --git a/api/server/routes/users.go b/api/server/routes/users.go index 00b0676..01f11b0 100644 --- a/api/server/routes/users.go +++ b/api/server/routes/users.go @@ -3,17 +3,18 @@ package routes import ( "net/http" - "github.com/echo-webkom/ludo/api/database" "github.com/echo-webkom/ludo/api/rest" + "github.com/echo-webkom/ludo/pkg/model" + "github.com/echo-webkom/ludo/pkg/service" "github.com/go-chi/chi/v5" ) -func UsersHandler(db *database.Database) chi.Router { +func UsersHandler(s service.LudoService) chi.Router { r := chi.NewRouter() // Get all users r.Get("/", rest.Handler(func(r rest.Request) int { - if users, err := db.GetAllUsers(); err == nil { + if users, err := s.Users(); err == nil { return r.RespondJSON(&users) } return http.StatusInternalServerError @@ -21,17 +22,17 @@ func UsersHandler(db *database.Database) chi.Router { // Create user r.Post("/", rest.Handler(func(r rest.Request) int { - var user database.User + var user model.User if err := r.ParseJSON(&user); err != nil { return http.StatusBadRequest } - id, err := db.CreateUser(user) + id, err := s.NewUser(user) if err != nil { return http.StatusInternalServerError } - return r.RespondJSON(&database.ID{ID: id}) + return r.RespondJSON(&model.ID{ID: id}) })) r.Route("/{id}", func(r chi.Router) { @@ -39,7 +40,7 @@ func UsersHandler(db *database.Database) chi.Router { // Get user by id r.Get("/", rest.Handler(func(r rest.Request) int { - if user, err := db.GetUserById(r.ContextValue(idKey).(uint)); err == nil { + if user, err := s.User(r.ContextValue(idKey).(uint)); err == nil { return r.RespondJSON(&user) } return http.StatusInternalServerError @@ -47,12 +48,12 @@ func UsersHandler(db *database.Database) chi.Router { // Update user r.Patch("/", rest.Handler(func(r rest.Request) int { - var user database.User + var user model.User if err := r.ParseJSON(&user); err != nil { return http.StatusBadRequest } - if err := db.UpdateUser(user, r.ContextValue(idKey).(uint)); err != nil { + if err := s.UpdateUser(r.ContextValue(idKey).(uint), user); err != nil { return http.StatusInternalServerError } @@ -61,7 +62,7 @@ func UsersHandler(db *database.Database) chi.Router { // Delete user by id r.Delete("/", rest.Handler(func(r rest.Request) int { - if err := db.DeleteUserById(r.ContextValue(idKey).(uint)); err != nil { + if err := s.DeleteUser(r.ContextValue(idKey).(uint)); err != nil { return http.StatusInternalServerError } return http.StatusOK diff --git a/api/server/server_test.go b/api/server/server_test.go index dfaf6f8..f1254e5 100644 --- a/api/server/server_test.go +++ b/api/server/server_test.go @@ -13,6 +13,7 @@ import ( "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/pkg/model" ) func setupTestServer(t *testing.T) (*server.Server, func()) { @@ -53,12 +54,12 @@ func TestUserEndpoints(t *testing.T) { defer cleanup() // Create user - user := database.User{DisplayName: "Test User"} + user := model.User{DisplayName: "Test User"} resp := makeRequest(t, s, http.MethodPost, "/users", &user) if resp.Code != http.StatusOK { t.Fatalf("CreateUser failed, code %d", resp.Code) } - var created database.ID + var created model.ID json.NewDecoder(resp.Body).Decode(&created) // Get user @@ -85,12 +86,12 @@ func TestItemEndpoints(t *testing.T) { defer cleanup() // Create item - item := database.Item{Data: "Test Data"} - resp := makeRequest(t, s, http.MethodPost, "/items", &item) + item := model.Item{Data: "Test Data"} + resp := makeRequest(t, s, http.MethodPost, "/boards/1/items", &item) if resp.Code != http.StatusOK { t.Fatalf("CreateItem failed, code %d", resp.Code) } - var created database.ID + var created model.ID json.NewDecoder(resp.Body).Decode(&created) // Get item @@ -99,12 +100,6 @@ func TestItemEndpoints(t *testing.T) { t.Fatalf("GetItemById failed, code %d", resp.Code) } - // Get all items - resp = makeRequest(t, s, http.MethodGet, "/items", nil) - if resp.Code != http.StatusOK { - t.Fatalf("GetAllItems failed, code %d", resp.Code) - } - // Get item data resp = makeRequest(t, s, http.MethodGet, "/items/"+strconv.Itoa(int(created.ID))+"/data", nil) if resp.Code != http.StatusOK { @@ -129,12 +124,12 @@ func TestBoardEndpoints(t *testing.T) { defer cleanup() // Create board - board := database.Board{Title: "Test Board"} + board := model.Board{Title: "Test Board"} resp := makeRequest(t, s, http.MethodPost, "/boards", &board) if resp.Code != http.StatusOK { t.Fatalf("CreateBoard failed, code %d", resp.Code) } - var createdBoard database.ID + var createdBoard model.ID json.NewDecoder(resp.Body).Decode(&createdBoard) // Get board @@ -150,12 +145,12 @@ func TestBoardEndpoints(t *testing.T) { } // Add user to board (assuming user creation tested before) - user := database.User{DisplayName: "Board User"} + user := model.User{DisplayName: "Board User"} resp = makeRequest(t, s, http.MethodPost, "/users", &user) if resp.Code != http.StatusOK { t.Fatalf("CreateUser failed, code %d", resp.Code) } - var userID database.ID + var userID model.ID json.NewDecoder(resp.Body).Decode(&userID) resp = makeRequest(t, s, http.MethodPost, "/boards/"+strconv.Itoa(int(createdBoard.ID))+"/users/"+strconv.Itoa(int(userID.ID)), nil) diff --git a/board/service/service.go b/board/service/service.go deleted file mode 100644 index c29dc88..0000000 --- a/board/service/service.go +++ /dev/null @@ -1,165 +0,0 @@ -package service - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "net/http" - "net/url" - - "github.com/echo-webkom/ludo/api/database" - "github.com/echo-webkom/ludo/board/config" -) - -type LudoService struct { - config *config.Config -} - -func New(config *config.Config) *LudoService { - return &LudoService{ - config: config, - } -} - -// Send request to API using config URL. Method should be "GET", "POST" etc. -// -// Endpoint is the target endpoint, eg. "/users/12482", notice path values -// should already be formatted. Body is the request body, a reference to the -// object you want to encode as json, nil for empty body. Response is a -// reference to the response type, or nil for no response. -func (l *LudoService) request(method, endpoint string, body, response any) error { - var bodyReader io.Reader - if body != nil { - bodyJson, err := json.Marshal(body) - if err != nil { - return err - } - bodyReader = bytes.NewReader(bodyJson) - } - - url, err := url.JoinPath(l.config.ApiUrl, endpoint) - if err != nil { - return err - } - - req, err := http.NewRequest(method, url, bodyReader) - if err != nil { - return err - } - - res, err := http.DefaultClient.Do(req) - if err != nil { - return err - } - defer res.Body.Close() - - if res.StatusCode == http.StatusOK && response != nil { - b, err := io.ReadAll(res.Body) - if err != nil { - return err - } - return json.Unmarshal(b, response) - } - - return err -} - -func (l *LudoService) GetAllUsers() (users []database.User, err error) { - err = l.request("GET", "/users", nil, &users) - return users, err -} - -func (l *LudoService) CreateUser(user database.User) (id uint, err error) { - var idRes database.ID - err = l.request("POST", "/users", &user, &idRes) - return idRes.ID, err -} - -func (l *LudoService) GetUserById(id uint) (user database.User, err error) { - err = l.request("GET", fmt.Sprintf("/users/%d", id), nil, &user) - return user, err -} - -func (l *LudoService) UpdateUser(id uint, user database.User) error { - return l.request("PATCH", fmt.Sprintf("/users/%d", id), &user, nil) -} - -func (l *LudoService) DeleteUser(id uint) error { - return l.request("DELETE", fmt.Sprintf("/users/%d", id), nil, nil) -} - -func (l *LudoService) GetAllBoards() (boards []database.Board, err error) { - err = l.request("GET", "/boards", nil, &boards) - return boards, err -} - -func (l *LudoService) CreateBoard(board database.Board) (id uint, err error) { - var idRes database.ID - err = l.request("POST", "/boards", &board, &idRes) - return idRes.ID, err -} - -func (l *LudoService) GetBoardById(id uint) (board database.Board, err error) { - err = l.request("GET", fmt.Sprintf("/boards/%d", id), nil, &board) - return board, err -} - -func (l *LudoService) UpdateBoard(id uint, board database.Board) error { - return l.request("PATCH", fmt.Sprintf("/boards/%d", id), &board, nil) -} - -func (l *LudoService) DeleteBoard(id uint) error { - return l.request("DELETE", fmt.Sprintf("/boards/%d", id), nil, nil) -} - -func (l *LudoService) GetAllUsersInBoard(id uint) (users []database.User, err error) { - err = l.request("GET", fmt.Sprintf("/boards/%d/users", id), nil, &users) - return users, err -} - -func (l *LudoService) GetAllItemsInBoard(id uint) (items []database.Item, err error) { - err = l.request("GET", fmt.Sprintf("/boards/%d/items", id), nil, &items) - return items, err -} - -func (l *LudoService) AddUserToBoard(boardId, userId uint) error { - return l.request("POST", fmt.Sprintf("/boards/%d/users/%d", boardId, userId), nil, nil) -} - -func (l *LudoService) RemoveUserFromBoard(boardId, userId uint) error { - return l.request("DELETE", fmt.Sprintf("/boards/%d/users/%d", boardId, userId), nil, nil) -} - -func (l *LudoService) GetAllItemsInBoardList(boardId, list uint) (items []database.Item, err error) { - err = l.request("GET", fmt.Sprintf("/boards/%d/lists/%d/items", boardId, list), nil, &items) - return items, err -} - -func (l *LudoService) GetAllItems() (items []database.Item, err error) { - err = l.request("GET", "/items", nil, &items) - return items, err -} - -func (l *LudoService) CreateItem(item database.Item) (id uint, err error) { - var idRes database.ID - err = l.request("POST", "/items", &item, &idRes) - return idRes.ID, err -} - -func (l *LudoService) GetItemById(id uint) (item database.Item, err error) { - err = l.request("GET", fmt.Sprintf("/items/%d", id), nil, &item) - return item, err -} - -func (l *LudoService) UpdateItem(id uint, item database.Item) error { - return l.request("PATCH", fmt.Sprintf("/items/%d", id), &item, nil) -} - -func (l *LudoService) DeleteItem(id uint) error { - return l.request("DELETE", fmt.Sprintf("/items/%d", id), nil, nil) -} - -func (l *LudoService) MoveItemToList(itemId, list uint) error { - return l.request("PATCH", fmt.Sprintf("/items/%d/move/%d", itemId, list), nil, nil) -} diff --git a/api/database/model.go b/pkg/model/model.go similarity index 98% rename from api/database/model.go rename to pkg/model/model.go index 453295e..635935d 100644 --- a/api/database/model.go +++ b/pkg/model/model.go @@ -1,4 +1,4 @@ -package database +package model import ( "time" diff --git a/pkg/service/api_service.go b/pkg/service/api_service.go new file mode 100644 index 0000000..1819edc --- /dev/null +++ b/pkg/service/api_service.go @@ -0,0 +1,171 @@ +package service + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + + "github.com/echo-webkom/ludo/pkg/model" +) + +type apiService struct { + url string +} + +// Create new LudoService using the Ludo REST API as backend. +func NewApiService(apiUrl string) LudoService { + return &apiService{ + url: apiUrl, + } +} + +// Send request to API using config URL. Method should be "GET", "POST" etc. +// +// Endpoint is the target endpoint, eg. "/users/12482", notice path values +// should already be formatted. Body is the request body, a reference to the +// object you want to encode as json, nil for empty body. Response is a +// reference to the response type, or nil for no response. +func (s *apiService) request(method, endpoint string, body, response any) error { + var bodyReader io.Reader + if body != nil { + bodyJson, err := json.Marshal(body) + if err != nil { + return err + } + bodyReader = bytes.NewReader(bodyJson) + } + + url, err := url.JoinPath(s.url, endpoint) + if err != nil { + return err + } + + req, err := http.NewRequest(method, url, bodyReader) + if err != nil { + return err + } + + res, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + defer res.Body.Close() + + if res.StatusCode == http.StatusOK && response != nil { + b, err := io.ReadAll(res.Body) + if err != nil { + return err + } + return json.Unmarshal(b, response) + } + + return err +} + +func (s *apiService) Users() (users []model.User, err error) { + err = s.request("GET", "/users", nil, &users) + return users, err +} + +func (s *apiService) NewUser(user model.User) (id uint, err error) { + var idRes model.ID + err = s.request("POST", "/users", &user, &idRes) + return idRes.ID, err +} + +func (s *apiService) User(id uint) (user model.User, err error) { + err = s.request("GET", fmt.Sprintf("/users/%d", id), nil, &user) + return user, err +} + +func (s *apiService) UpdateUser(id uint, user model.User) error { + return s.request("PATCH", fmt.Sprintf("/users/%d", id), &user, nil) +} + +func (s *apiService) DeleteUser(id uint) error { + return s.request("DELETE", fmt.Sprintf("/users/%d", id), nil, nil) +} + +func (s *apiService) Boards() (boards []model.Board, err error) { + err = s.request("GET", "/boards", nil, &boards) + return boards, err +} + +func (s *apiService) NewBoard(board model.Board) (id uint, err error) { + var idRes model.ID + err = s.request("POST", "/boards", &board, &idRes) + return idRes.ID, err +} + +func (s *apiService) Board(id uint) (board model.Board, err error) { + err = s.request("GET", fmt.Sprintf("/boards/%d", id), nil, &board) + return board, err +} + +func (s *apiService) UpdateBoard(id uint, board model.Board) error { + return s.request("PATCH", fmt.Sprintf("/boards/%d", id), &board, nil) +} + +func (s *apiService) DeleteBoard(id uint) error { + return s.request("DELETE", fmt.Sprintf("/boards/%d", id), nil, nil) +} + +func (s *apiService) BoardUsers(id uint) (users []model.User, err error) { + err = s.request("GET", fmt.Sprintf("/boards/%d/users", id), nil, &users) + return users, err +} + +func (s *apiService) BoardItems(id uint) (items []model.Item, err error) { + err = s.request("GET", fmt.Sprintf("/boards/%d/items", id), nil, &items) + return items, err +} + +func (s *apiService) AddUserToBoard(boardId, userId uint) error { + return s.request("POST", fmt.Sprintf("/boards/%d/users/%d", boardId, userId), nil, nil) +} + +func (s *apiService) RemoveUserFromBoard(boardId, userId uint) error { + return s.request("DELETE", fmt.Sprintf("/boards/%d/users/%d", boardId, userId), nil, nil) +} + +func (s *apiService) BoardItemsWithStatus(boardId uint, status model.Status) (items []model.Item, err error) { + err = s.request("GET", fmt.Sprintf("/boards/%d/lists/%d/items", boardId, status), nil, &items) + return items, err +} + +func (s *apiService) Items() (items []model.Item, err error) { + err = s.request("GET", "/items", nil, &items) + return items, err +} + +func (s *apiService) NewItem(boardId uint, item model.Item) (id uint, err error) { + var idRes model.ID + err = s.request("POST", "/items", &item, &idRes) + return idRes.ID, err +} + +func (s *apiService) Item(id uint) (item model.Item, err error) { + err = s.request("GET", fmt.Sprintf("/items/%d", id), nil, &item) + return item, err +} + +func (s *apiService) UpdateItem(id uint, item model.Item) error { + return s.request("PATCH", fmt.Sprintf("/items/%d", id), &item, nil) +} + +func (s *apiService) DeleteItem(id uint) error { + return s.request("DELETE", fmt.Sprintf("/items/%d", id), nil, nil) +} + +// TODO: implement set and get item data + +func (s *apiService) SetItemData(itemId uint, data string) error { + return nil +} + +func (s *apiService) ItemData(itemId uint) (data string, err error) { + return data, err +} diff --git a/pkg/service/service.go b/pkg/service/service.go new file mode 100644 index 0000000..09da5e3 --- /dev/null +++ b/pkg/service/service.go @@ -0,0 +1,52 @@ +package service + +import "github.com/echo-webkom/ludo/pkg/model" + +type LudoService interface { + // Get a board by ID. + Board(id uint) (model.Board, error) + // Get all boards. + Boards() ([]model.Board, error) + // Update the board. Replaces all fields with those given. + UpdateBoard(id uint, board model.Board) error + // Delete the board and all its items. + DeleteBoard(id uint) error + // Create a new board. Returns the ID. + NewBoard(board model.Board) (uint, error) + + // Get a user by ID. + User(id uint) (model.User, error) + // Get all users. + Users() ([]model.User, error) + // Update the user. Replaces all fields with those given. + UpdateUser(id uint, user model.User) error + // Delete the user and remove it from all boards. + DeleteUser(id uint) error + // Create a new user. Returns the ID. + NewUser(user model.User) (uint, error) + + // Get all users assigned to a board. + BoardUsers(boardID uint) ([]model.User, error) + // Add an existing user to a board. + AddUserToBoard(boardID uint, userID uint) error + // Remove a user from a board. + RemoveUserFromBoard(boardID uint, userID uint) error + + // Get an item in a board by ID. + Item(itemID uint) (model.Item, error) + // Update an item. Replaces all fields with those given. + UpdateItem(id uint, item model.Item) error + // Delete an item. + DeleteItem(itemID uint) error + // Create a new item in a board. Returns the ID. + NewItem(boardID uint, item model.Item) (uint, error) + // Get all items in a board. + BoardItems(boardID uint) ([]model.Item, error) + // Get all items in a board with a given status. + BoardItemsWithStatus(boardID uint, status model.Status) ([]model.Item, error) + + // Get this item's data segment. + ItemData(itemID uint) (string, error) + // Set this item's data segment. + SetItemData(itemID uint, data string) error +}