diff --git a/backend/db/cardstags.go b/backend/db/cardstags.go index 0dc58d4..5387a75 100644 --- a/backend/db/cardstags.go +++ b/backend/db/cardstags.go @@ -18,7 +18,7 @@ func GetCardTags(cardID int, projectID int) ([]types.FullCardTag, error) { projectID = card.ProjectID } - rows, err := db.Query(`SELECT t.id, t.title, COALESCE(ct.value, '') + rows, err := db.Query(`SELECT t.id, t.title, t.type, COALESCE(ct.value, '') FROM tags t LEFT JOIN cardtags ct ON ct.tag_id = t.id AND ct.card_id = ? WHERE t.project_id = ? @@ -31,7 +31,7 @@ func GetCardTags(cardID int, projectID int) ([]types.FullCardTag, error) { var cardtags []types.FullCardTag for rows.Next() { ct := types.FullCardTag{CardID: cardID} - if err := rows.Scan(&ct.TagID, &ct.TagTitle, &ct.Value); err != nil { + if err := rows.Scan(&ct.TagID, &ct.TagTitle, &ct.TagType, &ct.Value); err != nil { return nil, err } cardtags = append(cardtags, ct) diff --git a/backend/db/main.go b/backend/db/main.go index 9a722d4..e8736ab 100644 --- a/backend/db/main.go +++ b/backend/db/main.go @@ -46,6 +46,12 @@ func InitDB(driver string, connStr string) error { PRIMARY KEY(card_id, tag_id), FOREIGN KEY(card_id) REFERENCES cards(id) FOREIGN KEY(tag_id) REFERENCES tags(id) + ); + CREATE TABLE IF NOT EXISTS tagsoptions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + tag_id INTEGER, + value TEXT, + FOREIGN KEY(tag_id) REFERENCES tags(id) ); `) if err != nil { diff --git a/backend/db/tags.go b/backend/db/tags.go index a732467..d4dedfc 100644 --- a/backend/db/tags.go +++ b/backend/db/tags.go @@ -16,19 +16,25 @@ func CreateTag(t types.Tag) (int, error) { return int(id), nil } -func GetProjectTags(projectID int) ([]types.Tag, error) { +func GetProjectTags(projectID int) ([]types.FullTag, error) { rows, err := db.Query("SELECT * FROM tags WHERE project_id = ?", projectID) if err != nil { return nil, err } defer rows.Close() - var tags []types.Tag + var tags []types.FullTag for rows.Next() { - var t types.Tag + var t types.FullTag if err := rows.Scan(&t.ID, &t.ProjectID, &t.Title, &t.Type); err != nil { return nil, err } + + t.Options, err = GetTagOptions(t.ID) + if err != nil { + return nil, err + } + tags = append(tags, t) } @@ -81,3 +87,61 @@ func ExistTag(id int) (bool, error) { return count > 0, nil } + +func CreateTagOption(to types.TagOption) (int, error) { + res, err := db.Exec("INSERT INTO tagsoptions (tag_id, value) VALUES (?, ?)", to.TagID, to.Value) + if err != nil { + return 0, err + } + + id, err := res.LastInsertId() + if err != nil { + return 0, err + } + + return int(id), nil +} + +func GetTagOptions(tagID int) ([]types.TagOption, error) { + rows, err := db.Query("SELECT * FROM tagsoptions WHERE tag_id = ?", tagID) + if err != nil { + return nil, err + } + defer rows.Close() + + var options []types.TagOption + for rows.Next() { + var to types.TagOption + if err := rows.Scan(&to.ID, &to.TagID, &to.Value); err != nil { + return nil, err + } + options = append(options, to) + } + + if err = rows.Err(); err != nil { + return nil, err + } + + return options, nil +} + +func DeleteTagOption(id int) (int64, error) { + res, err := db.Exec("DELETE FROM tagsoptions WHERE id = ?", id) + if err != nil { + return 0, err + } + return res.RowsAffected() +} + +func UpdateTagOption(to types.TagOption) (int64, error) { + res, err := db.Exec("UPDATE tagsoptions SET tag_id = ?, value = ? WHERE id = ?", to.TagID, to.Value, to.ID) + if err != nil { + return 0, err + } + return res.RowsAffected() +} + +func DeleteTagOptions(tagID int) error { + _, err := db.Exec("DELETE FROM tagsoptions WHERE tag_id = ?", tagID) + return err +} diff --git a/backend/go.mod b/backend/go.mod index 7d30c9c..ccd5f4b 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -11,8 +11,6 @@ require ( github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect - github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect github.com/rivo/uniseg v0.2.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.50.0 // indirect @@ -20,7 +18,4 @@ require ( golang.org/x/sys v0.14.0 // indirect ) -require ( - github.com/json-iterator/go v1.1.12 - github.com/mattn/go-sqlite3 v1.14.19 -) +require github.com/mattn/go-sqlite3 v1.14.19 diff --git a/backend/go.sum b/backend/go.sum index 1c7ad0b..1b8f97a 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -1,14 +1,9 @@ github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gofiber/fiber/v2 v2.51.0 h1:JNACcZy5e2tGApWB2QrRpenTWn0fq0hkFm6k0C86gKQ= github.com/gofiber/fiber/v2 v2.51.0/go.mod h1:xaQRZQJGqnKOQnbQw+ltvku3/h8QxvNi8o6JiJ7Ll0U= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I= github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= @@ -20,15 +15,8 @@ github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZ github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbWfGI= github.com/mattn/go-sqlite3 v1.14.19/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.50.0 h1:H7fweIlBm0rXLs2q0XbalvJ6r0CUPFWK3/bB4N13e9M= diff --git a/backend/handlers/tags.go b/backend/handlers/tags.go index eba7f56..f24d6e5 100644 --- a/backend/handlers/tags.go +++ b/backend/handlers/tags.go @@ -14,6 +14,11 @@ func tagsRouter(router fiber.Router) { router.Get("/:id", GetTag) router.Delete("/:id", DeleteTag) router.Put("/:id", UpdateTag) + router.Post("/:id/options", CreateTagOption) + router.Get("/:id/options", GetTagOptions) + router.Delete("/:id/options/:option_id", DeleteTagOption) + router.Put("/:id/options/:option_id", UpdateTagOption) + router.Delete("/:id/options", DeleteTagOptions) } func CreateTag(c *fiber.Ctx) error { @@ -50,7 +55,7 @@ func GetTag(c *fiber.Ctx) error { return c.SendStatus(fiber.StatusNotFound) } - return c.JSON(tag) + return c.Status(fiber.StatusOK).JSON(tag) } func DeleteTag(c *fiber.Ctx) error { @@ -99,3 +104,171 @@ func UpdateTag(c *fiber.Ctx) error { return c.SendStatus(fiber.StatusOK) } + +func CreateTagOption(c *fiber.Ctx) error { + tagID, err := strconv.Atoi(c.Params("id")) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid tag ID"}) + } + + option := types.TagOption{TagID: tagID} + if err := c.BodyParser(&option); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Cannot parse request"}) + } + + exist, err := db.ExistTag(tagID) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "Error finding tag", + "trace": fmt.Sprint(err), + }) + } + if !exist { + return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Tag not found"}) + } + + id, err := db.CreateTagOption(option) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "Cannot create tag option", + "trace": fmt.Sprint(err), + }) + } + + return c.Status(fiber.StatusCreated).JSON(fiber.Map{"id": id}) +} + +func GetTagOptions(c *fiber.Ctx) error { + tagID, err := strconv.Atoi(c.Params("id")) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid tag ID"}) + } + + exist, err := db.ExistTag(tagID) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "Error finding tag", + "trace": fmt.Sprint(err), + }) + } + if !exist { + return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Tag not found"}) + } + + options, err := db.GetTagOptions(tagID) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "Cannot retrieve tag options", + "trace": fmt.Sprint(err), + }) + } + + return c.Status(fiber.StatusOK).JSON(options) +} + +func DeleteTagOption(c *fiber.Ctx) error { + tagID, err := strconv.Atoi(c.Params("id")) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid tag ID"}) + } + + optionID, err := strconv.Atoi(c.Params("option_id")) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid tag option ID"}) + } + + exist, err := db.ExistTag(tagID) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "Error finding tag", + "trace": fmt.Sprint(err), + }) + } + if !exist { + return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Tag not found"}) + } + + count, err := db.DeleteTagOption(optionID) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "Cannot delete tag option", + "trace": fmt.Sprint(err), + }) + } + + if count == 0 { + return c.SendStatus(fiber.StatusNotFound) + } + + return c.SendStatus(fiber.StatusNoContent) +} + +func UpdateTagOption(c *fiber.Ctx) error { + tagID, err := strconv.Atoi(c.Params("id")) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid tag ID"}) + } + + optionID, err := strconv.Atoi(c.Params("option_id")) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid tag option ID"}) + } + + option := types.TagOption{ID: optionID, TagID: tagID} + if err := c.BodyParser(&option); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Cannot parse request"}) + } + + exist, err := db.ExistTag(tagID) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "Error finding tag", + "trace": fmt.Sprint(err), + }) + } + if !exist { + return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Tag not found"}) + } + + count, err := db.UpdateTagOption(option) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "Cannot update tag option", + "trace": fmt.Sprint(err), + }) + } + + if count == 0 { + return c.SendStatus(fiber.StatusNotFound) + } + + return c.SendStatus(fiber.StatusOK) +} + +func DeleteTagOptions(c *fiber.Ctx) error { + tagID, err := strconv.Atoi(c.Params("id")) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid tag ID"}) + } + + exist, err := db.ExistTag(tagID) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "Error finding tag", + "trace": fmt.Sprint(err), + }) + } + if !exist { + return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Tag not found"}) + } + + err = db.DeleteTagOptions(tagID) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "Cannot delete tag options", + "trace": fmt.Sprint(err), + }) + } + + return c.SendStatus(fiber.StatusNoContent) +} diff --git a/backend/types/cardtag.go b/backend/types/cardtag.go index ddbefb0..b981227 100644 --- a/backend/types/cardtag.go +++ b/backend/types/cardtag.go @@ -10,5 +10,6 @@ type FullCardTag struct { CardID int `json:"card_id"` TagID int `json:"tag_id"` TagTitle string `json:"tag_title"` + TagType int `json:"tag_type"` Value string `json:"value"` } diff --git a/backend/types/tag.go b/backend/types/tag.go index ebf4283..65fb342 100644 --- a/backend/types/tag.go +++ b/backend/types/tag.go @@ -6,3 +6,17 @@ type Tag struct { Title string `json:"title"` Type int `json:"type"` } + +type TagOption struct { + ID int `json:"id"` + TagID int `json:"tag_id"` + Value string `json:"value"` +} + +type FullTag struct { + ID int `json:"id"` + ProjectID int `json:"project_id"` + Title string `json:"title"` + Type int `json:"type"` + Options []TagOption `json:"options"` +}