Add Support for Advance Labels ( LabelV2)
Add Support for Custom Migration to keep supporting two database( sqlite and postgres) Migration from Label to LabelV2
This commit is contained in:
parent
6dc8092ff4
commit
0c07b33359
12 changed files with 730 additions and 49 deletions
176
internal/label/handler.go
Normal file
176
internal/label/handler.go
Normal file
|
@ -0,0 +1,176 @@
|
|||
package label
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
auth "donetick.com/core/internal/authorization"
|
||||
lModel "donetick.com/core/internal/label/model"
|
||||
lRepo "donetick.com/core/internal/label/repo"
|
||||
jwt "github.com/appleboy/gin-jwt/v2"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type LabelReq struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Color string `json:"color"`
|
||||
}
|
||||
|
||||
type UpdateLabelReq struct {
|
||||
ID int `json:"id" binding:"required"`
|
||||
LabelReq
|
||||
}
|
||||
|
||||
type Handler struct {
|
||||
lRepo *lRepo.LabelRepository
|
||||
}
|
||||
|
||||
func NewHandler(lRepo *lRepo.LabelRepository) *Handler {
|
||||
return &Handler{
|
||||
lRepo: lRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) getLabels(c *gin.Context) {
|
||||
// get current user:
|
||||
currentUser, ok := auth.CurrentUser(c)
|
||||
if !ok {
|
||||
c.JSON(500, gin.H{
|
||||
"error": "Error getting current user",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
labels, err := h.lRepo.GetUserLabels(c, currentUser.ID, currentUser.CircleID)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{
|
||||
"error": "Error getting labels",
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(200,
|
||||
labels,
|
||||
)
|
||||
}
|
||||
|
||||
func (h *Handler) createLabel(c *gin.Context) {
|
||||
// get current user:
|
||||
currentUser, ok := auth.CurrentUser(c)
|
||||
if !ok {
|
||||
c.JSON(500, gin.H{
|
||||
"error": "Error getting current user",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var req LabelReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{
|
||||
"error": "Error binding label",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
label := &lModel.Label{
|
||||
Name: req.Name,
|
||||
Color: req.Color,
|
||||
CreatedBy: currentUser.ID,
|
||||
}
|
||||
if err := h.lRepo.CreateLabels(c, []*lModel.Label{label}); err != nil {
|
||||
c.JSON(500, gin.H{
|
||||
"error": "Error creating label",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, gin.H{
|
||||
"res": label,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) updateLabel(c *gin.Context) {
|
||||
currentUser, ok := auth.CurrentUser(c)
|
||||
if !ok {
|
||||
c.JSON(500, gin.H{
|
||||
"error": "Error getting current user",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var req UpdateLabelReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{
|
||||
"error": "Error binding label",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
label := &lModel.Label{
|
||||
Name: req.Name,
|
||||
Color: req.Color,
|
||||
ID: req.ID,
|
||||
}
|
||||
if err := h.lRepo.UpdateLabel(c, currentUser.ID, label); err != nil {
|
||||
c.JSON(500, gin.H{
|
||||
"error": "Error updating label",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, gin.H{
|
||||
"res": label,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) deleteLabel(c *gin.Context) {
|
||||
currentUser, ok := auth.CurrentUser(c)
|
||||
// read label id from path:
|
||||
|
||||
if !ok {
|
||||
c.JSON(500, gin.H{
|
||||
"error": "Error getting current user",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
labelIDRaw := c.Param("id")
|
||||
if labelIDRaw == "" {
|
||||
c.JSON(400, gin.H{
|
||||
"error": "Label ID is required",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
labelID, err := strconv.Atoi(labelIDRaw)
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{
|
||||
"error": "Invalid label ID",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// unassociate label from all chores:
|
||||
if err := h.lRepo.DeassignLabelFromAllChoreAndDelete(c, currentUser.ID, labelID); err != nil {
|
||||
c.JSON(500, gin.H{
|
||||
"error": "Error unassociating label from chores",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, gin.H{
|
||||
"res": "Label deleted",
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func Routes(r *gin.Engine, h *Handler, auth *jwt.GinJWTMiddleware) {
|
||||
|
||||
labelRoutes := r.Group("labels")
|
||||
labelRoutes.Use(auth.MiddlewareFunc())
|
||||
{
|
||||
labelRoutes.GET("", h.getLabels)
|
||||
labelRoutes.POST("", h.createLabel)
|
||||
labelRoutes.PUT("", h.updateLabel)
|
||||
labelRoutes.DELETE("/:id", h.deleteLabel)
|
||||
}
|
||||
|
||||
}
|
9
internal/label/model/model.go
Normal file
9
internal/label/model/model.go
Normal file
|
@ -0,0 +1,9 @@
|
|||
package model
|
||||
|
||||
type Label struct {
|
||||
ID int `json:"id" gorm:"primary_key"`
|
||||
Name string `json:"name" gorm:"column:name"`
|
||||
Color string `json:"color" gorm:"column:color"`
|
||||
CircleID *int `json:"-" gorm:"column:circle_id"`
|
||||
CreatedBy int `json:"created_by" gorm:"column:created_by"`
|
||||
}
|
163
internal/label/repo/repository.go
Normal file
163
internal/label/repo/repository.go
Normal file
|
@ -0,0 +1,163 @@
|
|||
package chore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
config "donetick.com/core/config"
|
||||
chModel "donetick.com/core/internal/chore/model"
|
||||
lModel "donetick.com/core/internal/label/model"
|
||||
"donetick.com/core/logging"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
type LabelRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewLabelRepository(db *gorm.DB, cfg *config.Config) *LabelRepository {
|
||||
return &LabelRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *LabelRepository) GetUserLabels(ctx context.Context, userID int, circleID int) ([]*lModel.Label, error) {
|
||||
var labels []*lModel.Label
|
||||
if err := r.db.WithContext(ctx).Where("created_by = ? OR circle_id = ? ", userID, circleID).Find(&labels).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return labels, nil
|
||||
}
|
||||
|
||||
func (r *LabelRepository) CreateLabels(ctx context.Context, labels []*lModel.Label) error {
|
||||
if err := r.db.WithContext(ctx).Create(&labels).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *LabelRepository) GetLabelsByIDs(ctx context.Context, ids []int) ([]*lModel.Label, error) {
|
||||
var labels []*lModel.Label
|
||||
if err := r.db.WithContext(ctx).Where("id IN (?)", ids).Find(&labels).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return labels, nil
|
||||
}
|
||||
|
||||
func (r *LabelRepository) isLabelsAssignableByUser(ctx context.Context, userID int, circleID int, toBeAdded []int, toBeRemoved []int) bool {
|
||||
// combine both toBeAdded and toBeRemoved:
|
||||
labelIDs := append(toBeAdded, toBeRemoved...)
|
||||
|
||||
log := logging.FromContext(ctx)
|
||||
var count int64
|
||||
if err := r.db.WithContext(ctx).Model(&lModel.Label{}).Where("id IN (?) AND (created_by = ? OR circle_id = ?) ", labelIDs, userID, circleID).Count(&count).Error; err != nil {
|
||||
log.Error(err)
|
||||
return false
|
||||
}
|
||||
return count == int64(len(labelIDs))
|
||||
}
|
||||
|
||||
func (r *LabelRepository) AssignLabelsToChore(ctx context.Context, choreID int, userID int, circleID int, toBeAdded []int, toBeRemoved []int) error {
|
||||
if len(toBeAdded) < 1 && len(toBeRemoved) < 1 {
|
||||
return nil
|
||||
}
|
||||
if !r.isLabelsAssignableByUser(ctx, userID, circleID, toBeAdded, toBeRemoved) {
|
||||
return errors.New("labels are not assignable by user")
|
||||
}
|
||||
|
||||
var choreLabels []*chModel.ChoreLabels
|
||||
for _, labelID := range toBeAdded {
|
||||
choreLabels = append(choreLabels, &chModel.ChoreLabels{
|
||||
ChoreID: choreID,
|
||||
LabelID: labelID,
|
||||
UserID: userID,
|
||||
})
|
||||
}
|
||||
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
|
||||
if err := r.db.WithContext(ctx).Where("chore_id = ? AND user_id = ? AND label_id IN (?)", choreID, userID, toBeRemoved).Delete(&chModel.ChoreLabels{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := r.db.WithContext(ctx).Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "chore_id"}, {Name: "label_id"}, {Name: "user_id"}},
|
||||
DoNothing: true,
|
||||
}).Create(&choreLabels).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (r *LabelRepository) DeassignLabelsFromChore(ctx context.Context, choreID int, userID int, labelIDs []int) error {
|
||||
if err := r.db.WithContext(ctx).Where("chore_id = ? AND user_id = ? AND label_id IN (?)", choreID, userID, labelIDs).Delete(&chModel.ChoreLabels{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *LabelRepository) DeassignLabelFromAllChoreAndDelete(ctx context.Context, userID int, labelID int) error {
|
||||
// create one transaction to confirm if the label is owned by the user then delete all ChoreLabels record for this label:
|
||||
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
log := logging.FromContext(ctx)
|
||||
var labelCount int64
|
||||
if err := tx.Model(&lModel.Label{}).Where("id = ? AND created_by = ?", labelID, userID).Count(&labelCount).Error; err != nil {
|
||||
log.Debug(err)
|
||||
return err
|
||||
}
|
||||
if labelCount < 1 {
|
||||
return errors.New("label is not owned by user")
|
||||
}
|
||||
|
||||
if err := tx.Where("label_id = ?", labelID).Delete(&chModel.ChoreLabels{}).Error; err != nil {
|
||||
log.Debug("Error deleting chore labels")
|
||||
return err
|
||||
}
|
||||
// delete the actual label:
|
||||
if err := tx.Where("id = ?", labelID).Delete(&lModel.Label{}).Error; err != nil {
|
||||
log.Debug("Error deleting label")
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (r *LabelRepository) isLabelsOwner(ctx context.Context, userID int, labelIDs []int) bool {
|
||||
var count int64
|
||||
r.db.WithContext(ctx).Model(&lModel.Label{}).Where("id IN (?) AND user_id = ?", labelIDs, userID).Count(&count)
|
||||
return count == 1
|
||||
}
|
||||
|
||||
func (r *LabelRepository) DeleteLabels(ctx context.Context, userID int, ids []int) error {
|
||||
// remove all ChoreLabels record for this:
|
||||
if r.isLabelsOwner(ctx, userID, ids) {
|
||||
return errors.New("labels are not owned by user")
|
||||
}
|
||||
|
||||
tx := r.db.WithContext(ctx).Begin()
|
||||
|
||||
if err := tx.Where("label_id IN (?)", ids).Delete(&chModel.ChoreLabels{}).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.Where("id IN (?)", ids).Delete(&lModel.Label{}).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.Commit().Error; err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *LabelRepository) UpdateLabel(ctx context.Context, userID int, label *lModel.Label) error {
|
||||
|
||||
if err := r.db.WithContext(ctx).Model(&lModel.Label{}).Where("id = ? and created_by = ?", label.ID, userID).Updates(label).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue