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:
commit
8c4f0aa5ac
12 changed files with 730 additions and 49 deletions
|
@ -15,6 +15,7 @@ import (
|
||||||
chModel "donetick.com/core/internal/chore/model"
|
chModel "donetick.com/core/internal/chore/model"
|
||||||
chRepo "donetick.com/core/internal/chore/repo"
|
chRepo "donetick.com/core/internal/chore/repo"
|
||||||
cRepo "donetick.com/core/internal/circle/repo"
|
cRepo "donetick.com/core/internal/circle/repo"
|
||||||
|
lRepo "donetick.com/core/internal/label/repo"
|
||||||
nRepo "donetick.com/core/internal/notifier/repo"
|
nRepo "donetick.com/core/internal/notifier/repo"
|
||||||
nps "donetick.com/core/internal/notifier/service"
|
nps "donetick.com/core/internal/notifier/service"
|
||||||
telegram "donetick.com/core/internal/notifier/telegram"
|
telegram "donetick.com/core/internal/notifier/telegram"
|
||||||
|
@ -31,6 +32,10 @@ type ThingTrigger struct {
|
||||||
Condition string `json:"condition"`
|
Condition string `json:"condition"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type LabelReq struct {
|
||||||
|
LabelID int `json:"id" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
type ChoreReq struct {
|
type ChoreReq struct {
|
||||||
Name string `json:"name" binding:"required"`
|
Name string `json:"name" binding:"required"`
|
||||||
FrequencyType chModel.FrequencyType `json:"frequencyType"`
|
FrequencyType chModel.FrequencyType `json:"frequencyType"`
|
||||||
|
@ -46,6 +51,7 @@ type ChoreReq struct {
|
||||||
Notification bool `json:"notification"`
|
Notification bool `json:"notification"`
|
||||||
NotificationMetadata *chModel.NotificationMetadata `json:"notificationMetadata"`
|
NotificationMetadata *chModel.NotificationMetadata `json:"notificationMetadata"`
|
||||||
Labels []string `json:"labels"`
|
Labels []string `json:"labels"`
|
||||||
|
LabelsV2 *[]LabelReq `json:"labelsV2"`
|
||||||
ThingTrigger *ThingTrigger `json:"thingTrigger"`
|
ThingTrigger *ThingTrigger `json:"thingTrigger"`
|
||||||
}
|
}
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
|
@ -55,10 +61,11 @@ type Handler struct {
|
||||||
nPlanner *nps.NotificationPlanner
|
nPlanner *nps.NotificationPlanner
|
||||||
nRepo *nRepo.NotificationRepository
|
nRepo *nRepo.NotificationRepository
|
||||||
tRepo *tRepo.ThingRepository
|
tRepo *tRepo.ThingRepository
|
||||||
|
lRepo *lRepo.LabelRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHandler(cr *chRepo.ChoreRepository, circleRepo *cRepo.CircleRepository, nt *telegram.TelegramNotifier,
|
func NewHandler(cr *chRepo.ChoreRepository, circleRepo *cRepo.CircleRepository, nt *telegram.TelegramNotifier,
|
||||||
np *nps.NotificationPlanner, nRepo *nRepo.NotificationRepository, tRepo *tRepo.ThingRepository) *Handler {
|
np *nps.NotificationPlanner, nRepo *nRepo.NotificationRepository, tRepo *tRepo.ThingRepository, lRepo *lRepo.LabelRepository) *Handler {
|
||||||
return &Handler{
|
return &Handler{
|
||||||
choreRepo: cr,
|
choreRepo: cr,
|
||||||
circleRepo: circleRepo,
|
circleRepo: circleRepo,
|
||||||
|
@ -66,6 +73,7 @@ func NewHandler(cr *chRepo.ChoreRepository, circleRepo *cRepo.CircleRepository,
|
||||||
nPlanner: np,
|
nPlanner: np,
|
||||||
nRepo: nRepo,
|
nRepo: nRepo,
|
||||||
tRepo: tRepo,
|
tRepo: tRepo,
|
||||||
|
lRepo: lRepo,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -262,6 +270,17 @@ func (h *Handler) createChore(c *gin.Context) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
labelsV2 := make([]int, len(*choreReq.LabelsV2))
|
||||||
|
for i, label := range *choreReq.LabelsV2 {
|
||||||
|
labelsV2[i] = int(label.LabelID)
|
||||||
|
}
|
||||||
|
if err := h.lRepo.AssignLabelsToChore(c, createdChore.ID, currentUser.ID, currentUser.CircleID, labelsV2, []int{}); err != nil {
|
||||||
|
c.JSON(500, gin.H{
|
||||||
|
"error": "Error adding labels",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if err := h.choreRepo.UpdateChoreAssignees(c, choreAssignees); err != nil {
|
if err := h.choreRepo.UpdateChoreAssignees(c, choreAssignees); err != nil {
|
||||||
c.JSON(500, gin.H{
|
c.JSON(500, gin.H{
|
||||||
"error": "Error adding chore assignees",
|
"error": "Error adding chore assignees",
|
||||||
|
@ -440,6 +459,38 @@ func (h *Handler) editChore(c *gin.Context) {
|
||||||
labels := strings.Join(escapedLabels, ",")
|
labels := strings.Join(escapedLabels, ",")
|
||||||
stringLabels = &labels
|
stringLabels = &labels
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create a map to store the existing labels for quick lookup
|
||||||
|
oldLabelsMap := make(map[int]struct{})
|
||||||
|
for _, oldLabel := range *oldChore.LabelsV2 {
|
||||||
|
oldLabelsMap[oldLabel.ID] = struct{}{}
|
||||||
|
}
|
||||||
|
newLabelMap := make(map[int]struct{})
|
||||||
|
for _, newLabel := range *choreReq.LabelsV2 {
|
||||||
|
newLabelMap[newLabel.LabelID] = struct{}{}
|
||||||
|
}
|
||||||
|
// check what labels need to be added and what labels need to be deleted:
|
||||||
|
labelsV2ToAdd := make([]int, 0)
|
||||||
|
labelsV2ToBeRemoved := make([]int, 0)
|
||||||
|
|
||||||
|
for _, label := range *choreReq.LabelsV2 {
|
||||||
|
if _, ok := oldLabelsMap[label.LabelID]; !ok {
|
||||||
|
labelsV2ToAdd = append(labelsV2ToAdd, label.LabelID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, oldLabel := range *oldChore.LabelsV2 {
|
||||||
|
if _, ok := newLabelMap[oldLabel.ID]; !ok {
|
||||||
|
labelsV2ToBeRemoved = append(labelsV2ToBeRemoved, oldLabel.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.lRepo.AssignLabelsToChore(c, choreReq.ID, currentUser.ID, currentUser.CircleID, labelsV2ToAdd, labelsV2ToBeRemoved); err != nil {
|
||||||
|
c.JSON(500, gin.H{
|
||||||
|
"error": "Error adding labels",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
updatedChore := &chModel.Chore{
|
updatedChore := &chModel.Chore{
|
||||||
ID: choreReq.ID,
|
ID: choreReq.ID,
|
||||||
Name: choreReq.Name,
|
Name: choreReq.Name,
|
||||||
|
|
|
@ -37,6 +37,7 @@ type Chore struct {
|
||||||
Notification bool `json:"notification" gorm:"column:notification"` // Whether the chore has notification
|
Notification bool `json:"notification" gorm:"column:notification"` // Whether the chore has notification
|
||||||
NotificationMetadata *string `json:"notificationMetadata" gorm:"column:notification_meta"` // Additional notification information
|
NotificationMetadata *string `json:"notificationMetadata" gorm:"column:notification_meta"` // Additional notification information
|
||||||
Labels *string `json:"labels" gorm:"column:labels"` // Labels for the chore
|
Labels *string `json:"labels" gorm:"column:labels"` // Labels for the chore
|
||||||
|
LabelsV2 *[]Label `json:"labelsV2" gorm:"many2many:chore_labels"` // Labels for the chore
|
||||||
CircleID int `json:"circleId" gorm:"column:circle_id;index"` // The circle this chore is in
|
CircleID int `json:"circleId" gorm:"column:circle_id;index"` // The circle this chore is in
|
||||||
CreatedAt time.Time `json:"createdAt" gorm:"column:created_at"` // When the chore was created
|
CreatedAt time.Time `json:"createdAt" gorm:"column:created_at"` // When the chore was created
|
||||||
UpdatedAt time.Time `json:"updatedAt" gorm:"column:updated_at"` // When the chore was last updated
|
UpdatedAt time.Time `json:"updatedAt" gorm:"column:updated_at"` // When the chore was last updated
|
||||||
|
@ -83,16 +84,6 @@ type Tag struct {
|
||||||
Name string `json:"name" gorm:"column:name;unique"`
|
Name string `json:"name" gorm:"column:name;unique"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// type ChoreTag struct {
|
|
||||||
// ChoreID int `json:"choreId" gorm:"primaryKey;autoIncrement:false"`
|
|
||||||
// TagID int `json:"tagId" gorm:"primaryKey;autoIncrement:false"`
|
|
||||||
// }
|
|
||||||
|
|
||||||
// type CircleTag struct {
|
|
||||||
// CircleID int `json:"circleId" gorm:"primaryKey;autoIncrement:false"`
|
|
||||||
// TagID int `json:"tagId" gorm:"primaryKey;autoIncrement:false"`
|
|
||||||
// }
|
|
||||||
|
|
||||||
type ChoreDetail struct {
|
type ChoreDetail struct {
|
||||||
ID int `json:"id" gorm:"column:id"`
|
ID int `json:"id" gorm:"column:id"`
|
||||||
Name string `json:"name" gorm:"column:name"`
|
Name string `json:"name" gorm:"column:name"`
|
||||||
|
@ -106,3 +97,18 @@ type ChoreDetail struct {
|
||||||
Notes *string `json:"notes" gorm:"column:notes"`
|
Notes *string `json:"notes" gorm:"column:notes"`
|
||||||
CreatedBy int `json:"createdBy" gorm:"column:created_by"`
|
CreatedBy int `json:"createdBy" gorm:"column:created_by"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChoreLabels struct {
|
||||||
|
ChoreID int `json:"choreId" gorm:"primaryKey;autoIncrement:false"`
|
||||||
|
LabelID int `json:"labelId" gorm:"primaryKey;autoIncrement:false"`
|
||||||
|
UserID int `json:"userId" gorm:"primaryKey;autoIncrement:false"`
|
||||||
|
Label Label
|
||||||
|
}
|
||||||
|
|
|
@ -35,7 +35,7 @@ func (r *ChoreRepository) CreateChore(c context.Context, chore *chModel.Chore) (
|
||||||
|
|
||||||
func (r *ChoreRepository) GetChore(c context.Context, choreID int) (*chModel.Chore, error) {
|
func (r *ChoreRepository) GetChore(c context.Context, choreID int) (*chModel.Chore, error) {
|
||||||
var chore chModel.Chore
|
var chore chModel.Chore
|
||||||
if err := r.db.Debug().WithContext(c).Model(&chModel.Chore{}).Preload("Assignees").Preload("ThingChore").First(&chore, choreID).Error; err != nil {
|
if err := r.db.Debug().WithContext(c).Model(&chModel.Chore{}).Preload("Assignees").Preload("ThingChore").Preload("LabelsV2").First(&chore, choreID).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &chore, nil
|
return &chore, nil
|
||||||
|
@ -44,7 +44,7 @@ func (r *ChoreRepository) GetChore(c context.Context, choreID int) (*chModel.Cho
|
||||||
func (r *ChoreRepository) GetChores(c context.Context, circleID int, userID int) ([]*chModel.Chore, error) {
|
func (r *ChoreRepository) GetChores(c context.Context, circleID int, userID int) ([]*chModel.Chore, error) {
|
||||||
var chores []*chModel.Chore
|
var chores []*chModel.Chore
|
||||||
// if err := r.db.WithContext(c).Preload("Assignees").Where("is_active = ?", true).Order("next_due_date asc").Find(&chores, "circle_id = ?", circleID).Error; err != nil {
|
// if err := r.db.WithContext(c).Preload("Assignees").Where("is_active = ?", true).Order("next_due_date asc").Find(&chores, "circle_id = ?", circleID).Error; err != nil {
|
||||||
if err := r.db.WithContext(c).Preload("Assignees").Joins("left join chore_assignees on chores.id = chore_assignees.chore_id").Where("chores.circle_id = ? AND (chores.created_by = ? OR chore_assignees.user_id = ?)", circleID, userID, userID).Group("chores.id").Order("next_due_date asc").Find(&chores, "circle_id = ?", circleID).Error; err != nil {
|
if err := r.db.WithContext(c).Preload("Assignees").Preload("LabelsV2").Joins("left join chore_assignees on chores.id = chore_assignees.chore_id").Where("chores.circle_id = ? AND (chores.created_by = ? OR chore_assignees.user_id = ?)", circleID, userID, userID).Group("chores.id").Order("next_due_date asc").Find(&chores, "circle_id = ?", circleID).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return chores, nil
|
return chores, nil
|
||||||
|
|
|
@ -33,13 +33,11 @@ func NewDatabase(cfg *config.Config) (*gorm.DB, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
|
||||||
path := os.Getenv("DT_SQLITE_PATH")
|
path := os.Getenv("DT_SQLITE_PATH")
|
||||||
if path == "" {
|
if path == "" {
|
||||||
db, err = gorm.Open(sqlite.Open("donetick.db"), &gorm.Config{})
|
path = "donetick.db"
|
||||||
} else {
|
|
||||||
db, err = gorm.Open(sqlite.Open(path), &gorm.Config{})
|
|
||||||
}
|
}
|
||||||
|
db, err = gorm.Open(sqlite.Open(path), &gorm.Config{})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
72
internal/database/migration.go
Normal file
72
internal/database/migration.go
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"donetick.com/core/config"
|
||||||
|
chModel "donetick.com/core/internal/chore/model"
|
||||||
|
cModel "donetick.com/core/internal/circle/model"
|
||||||
|
nModel "donetick.com/core/internal/notifier/model"
|
||||||
|
tModel "donetick.com/core/internal/thing/model"
|
||||||
|
uModel "donetick.com/core/internal/user/model" // Pure go SQLite driver, checkout https://github.com/glebarez/sqlite for details
|
||||||
|
migrations "donetick.com/core/migrations"
|
||||||
|
migrate "github.com/rubenv/sql-migrate"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Migration(db *gorm.DB) error {
|
||||||
|
if err := db.AutoMigrate(uModel.User{}, chModel.Chore{},
|
||||||
|
chModel.ChoreHistory{},
|
||||||
|
cModel.Circle{},
|
||||||
|
cModel.UserCircle{},
|
||||||
|
chModel.ChoreAssignees{},
|
||||||
|
nModel.Notification{},
|
||||||
|
uModel.UserPasswordReset{},
|
||||||
|
tModel.Thing{},
|
||||||
|
tModel.ThingChore{},
|
||||||
|
tModel.ThingHistory{},
|
||||||
|
uModel.APIToken{},
|
||||||
|
uModel.UserNotificationTarget{},
|
||||||
|
chModel.Label{},
|
||||||
|
chModel.ChoreLabels{},
|
||||||
|
migrations.Migration{},
|
||||||
|
); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func MigrationScripts(gormDB *gorm.DB, cfg *config.Config) error {
|
||||||
|
migrations := &migrate.FileMigrationSource{
|
||||||
|
Dir: migrationDir(),
|
||||||
|
}
|
||||||
|
|
||||||
|
path := os.Getenv("DT_SQLITE_PATH")
|
||||||
|
if path == "" {
|
||||||
|
path = "donetick.db"
|
||||||
|
}
|
||||||
|
|
||||||
|
db, err := gormDB.DB()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err := migrate.Exec(db, "sqlite3", migrations, migrate.Up)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Printf("Applied %d migrations!\n", n)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func migrationDir() string {
|
||||||
|
_, filename, _, ok := runtime.Caller(1)
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return filepath.Join(filepath.Dir(filename), "../../migrations")
|
||||||
|
}
|
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
|
||||||
|
}
|
17
main.go
17
main.go
|
@ -8,6 +8,7 @@ import (
|
||||||
|
|
||||||
"donetick.com/core/config"
|
"donetick.com/core/config"
|
||||||
"donetick.com/core/frontend"
|
"donetick.com/core/frontend"
|
||||||
|
"donetick.com/core/migrations"
|
||||||
"github.com/gin-contrib/cors"
|
"github.com/gin-contrib/cors"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"go.uber.org/fx"
|
"go.uber.org/fx"
|
||||||
|
@ -21,6 +22,9 @@ import (
|
||||||
cRepo "donetick.com/core/internal/circle/repo"
|
cRepo "donetick.com/core/internal/circle/repo"
|
||||||
"donetick.com/core/internal/database"
|
"donetick.com/core/internal/database"
|
||||||
"donetick.com/core/internal/email"
|
"donetick.com/core/internal/email"
|
||||||
|
label "donetick.com/core/internal/label"
|
||||||
|
lRepo "donetick.com/core/internal/label/repo"
|
||||||
|
|
||||||
notifier "donetick.com/core/internal/notifier"
|
notifier "donetick.com/core/internal/notifier"
|
||||||
nRepo "donetick.com/core/internal/notifier/repo"
|
nRepo "donetick.com/core/internal/notifier/repo"
|
||||||
nps "donetick.com/core/internal/notifier/service"
|
nps "donetick.com/core/internal/notifier/service"
|
||||||
|
@ -31,7 +35,6 @@ import (
|
||||||
uRepo "donetick.com/core/internal/user/repo"
|
uRepo "donetick.com/core/internal/user/repo"
|
||||||
"donetick.com/core/internal/utils"
|
"donetick.com/core/internal/utils"
|
||||||
"donetick.com/core/logging"
|
"donetick.com/core/logging"
|
||||||
"donetick.com/core/migration"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
@ -75,6 +78,10 @@ func main() {
|
||||||
// things
|
// things
|
||||||
fx.Provide(tRepo.NewThingRepository),
|
fx.Provide(tRepo.NewThingRepository),
|
||||||
|
|
||||||
|
// Labels:
|
||||||
|
fx.Provide(lRepo.NewLabelRepository),
|
||||||
|
fx.Provide(label.NewHandler),
|
||||||
|
|
||||||
fx.Provide(thing.NewWebhook),
|
fx.Provide(thing.NewWebhook),
|
||||||
fx.Provide(thing.NewHandler),
|
fx.Provide(thing.NewHandler),
|
||||||
|
|
||||||
|
@ -87,6 +94,7 @@ func main() {
|
||||||
circle.Routes,
|
circle.Routes,
|
||||||
thing.Routes,
|
thing.Routes,
|
||||||
thing.Webhooks,
|
thing.Webhooks,
|
||||||
|
label.Routes,
|
||||||
frontend.Routes,
|
frontend.Routes,
|
||||||
|
|
||||||
func(r *gin.Engine) {},
|
func(r *gin.Engine) {},
|
||||||
|
@ -127,7 +135,12 @@ func newServer(lc fx.Lifecycle, cfg *config.Config, db *gorm.DB, notifier *notif
|
||||||
lc.Append(fx.Hook{
|
lc.Append(fx.Hook{
|
||||||
OnStart: func(context.Context) error {
|
OnStart: func(context.Context) error {
|
||||||
if cfg.Database.Migration {
|
if cfg.Database.Migration {
|
||||||
migration.Migration(db)
|
database.Migration(db)
|
||||||
|
migrations.Run(context.Background(), db)
|
||||||
|
err := database.MigrationScripts(db, cfg)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
notifier.Start(context.Background())
|
notifier.Start(context.Background())
|
||||||
go func() {
|
go func() {
|
||||||
|
|
|
@ -1,30 +0,0 @@
|
||||||
package migration
|
|
||||||
|
|
||||||
import (
|
|
||||||
chModel "donetick.com/core/internal/chore/model"
|
|
||||||
cModel "donetick.com/core/internal/circle/model"
|
|
||||||
nModel "donetick.com/core/internal/notifier/model"
|
|
||||||
tModel "donetick.com/core/internal/thing/model"
|
|
||||||
uModel "donetick.com/core/internal/user/model"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Migration(db *gorm.DB) error {
|
|
||||||
if err := db.AutoMigrate(uModel.User{}, chModel.Chore{},
|
|
||||||
chModel.ChoreHistory{},
|
|
||||||
cModel.Circle{},
|
|
||||||
cModel.UserCircle{},
|
|
||||||
chModel.ChoreAssignees{},
|
|
||||||
nModel.Notification{},
|
|
||||||
uModel.UserPasswordReset{},
|
|
||||||
tModel.Thing{},
|
|
||||||
tModel.ThingChore{},
|
|
||||||
tModel.ThingHistory{},
|
|
||||||
uModel.APIToken{},
|
|
||||||
uModel.UserNotificationTarget{},
|
|
||||||
); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
123
migrations/20241123_migrate_label_to_labels_v2.go
Normal file
123
migrations/20241123_migrate_label_to_labels_v2.go
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
package migrations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"donetick.com/core/logging"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MigrateLabels20241123 struct{}
|
||||||
|
|
||||||
|
func (m MigrateLabels20241123) ID() string {
|
||||||
|
return "20241123_migrate_label_to_labels_v2"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m MigrateLabels20241123) Description() string {
|
||||||
|
return `Migrate label to labels v2 table, Allow more advanced features with labels like assign color`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m MigrateLabels20241123) Down(ctx context.Context, db *gorm.DB) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m MigrateLabels20241123) Up(ctx context.Context, db *gorm.DB) error {
|
||||||
|
log := logging.FromContext(ctx)
|
||||||
|
|
||||||
|
type Label struct {
|
||||||
|
ID int `gorm:"column:id;primary_key"`
|
||||||
|
Name string `gorm:"column:name"`
|
||||||
|
CreatedBy int `gorm:"column:created_by"`
|
||||||
|
CircleID *int `gorm:"column:circle_id"`
|
||||||
|
ChoresId []int `gorm:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Chore struct {
|
||||||
|
Labels *string `gorm:"column:labels"`
|
||||||
|
ID int `gorm:"column:id;primary_key"`
|
||||||
|
CircleID int `gorm:"column:circle_id"`
|
||||||
|
CreatedBy int `gorm:"column:created_by"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChoreLabel struct {
|
||||||
|
ChoreID int `gorm:"column:chore_id"`
|
||||||
|
LabelID int `gorm:"column:label_id"`
|
||||||
|
UserID int `gorm:"column:user_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start a transaction
|
||||||
|
return db.Transaction(func(tx *gorm.DB) error {
|
||||||
|
// Get all chores with labels
|
||||||
|
var choreRecords []Chore
|
||||||
|
if err := tx.Table("chores").Select("id, labels, circle_id, created_by").Find(&choreRecords).Error; err != nil {
|
||||||
|
log.Errorf("Failed to fetch chores with label: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map to store new labels
|
||||||
|
newLabelsMap := make(map[string]Label)
|
||||||
|
for _, choreRecord := range choreRecords {
|
||||||
|
if choreRecord.Labels != nil {
|
||||||
|
labels := strings.Split(*choreRecord.Labels, ",")
|
||||||
|
for _, label := range labels {
|
||||||
|
label = strings.TrimSpace(label)
|
||||||
|
if _, ok := newLabelsMap[label]; !ok {
|
||||||
|
newLabelsMap[label] = Label{
|
||||||
|
Name: label,
|
||||||
|
CreatedBy: choreRecord.CreatedBy,
|
||||||
|
CircleID: &choreRecord.CircleID,
|
||||||
|
ChoresId: []int{choreRecord.ID},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
labelToUpdate := newLabelsMap[label]
|
||||||
|
labelToUpdate.ChoresId = append(labelToUpdate.ChoresId, choreRecord.ID)
|
||||||
|
newLabelsMap[label] = labelToUpdate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert new labels and update chore_labels
|
||||||
|
for labelName, label := range newLabelsMap {
|
||||||
|
// Check if the label already exists
|
||||||
|
var existingLabel Label
|
||||||
|
if err := tx.Table("labels").Where("name = ? AND created_by = ? AND circle_id = ?", labelName, label.CreatedBy, label.CircleID).First(&existingLabel).Error; err != nil {
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
// Insert new label
|
||||||
|
if err := tx.Table("labels").Create(&label).Error; err != nil {
|
||||||
|
log.Errorf("Failed to insert new label: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
existingLabel = label
|
||||||
|
} else {
|
||||||
|
log.Errorf("Failed to check existing label: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare chore_labels for batch insertion
|
||||||
|
var choreLabels []ChoreLabel
|
||||||
|
for _, choreId := range label.ChoresId {
|
||||||
|
choreLabels = append(choreLabels, ChoreLabel{
|
||||||
|
ChoreID: choreId,
|
||||||
|
LabelID: existingLabel.ID,
|
||||||
|
UserID: label.CreatedBy,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Batch insert chore_labels
|
||||||
|
if err := tx.Table("chore_labels").Create(&choreLabels).Error; err != nil {
|
||||||
|
log.Errorf("Failed to insert chore labels: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register this migration
|
||||||
|
func init() {
|
||||||
|
Register(MigrateLabels20241123{})
|
||||||
|
}
|
100
migrations/base.go
Normal file
100
migrations/base.go
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
package migrations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"donetick.com/core/logging"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Migration struct {
|
||||||
|
// DB Representation for migration table, mainly used for tracking the migration status.
|
||||||
|
ID string `json:"id" gorm:"primary_key"`
|
||||||
|
Description string `json:"description" gorm:"column:description"`
|
||||||
|
AppliedAt time.Time `json:"appliedAt" gorm:"column:applied_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MigrationScript interface {
|
||||||
|
// Actual migration script interface which needs to be implemented by each migration script.
|
||||||
|
Description() string
|
||||||
|
ID() string
|
||||||
|
Up(ctx context.Context, db *gorm.DB) error
|
||||||
|
Down(ctx context.Context, db *gorm.DB) error
|
||||||
|
}
|
||||||
|
|
||||||
|
var registry []MigrationScript
|
||||||
|
|
||||||
|
// Register a migration
|
||||||
|
func Register(migration MigrationScript) {
|
||||||
|
registry = append(registry, migration)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort the migrations by ID
|
||||||
|
func sortMigrations(migrations []MigrationScript) []MigrationScript {
|
||||||
|
for i := 0; i < len(migrations); i++ {
|
||||||
|
for j := i + 1; j < len(migrations); j++ {
|
||||||
|
if migrations[i].ID() > migrations[j].ID() {
|
||||||
|
migrations[i], migrations[j] = migrations[j], migrations[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return migrations
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run pending migrations
|
||||||
|
func Run(ctx context.Context, db *gorm.DB) error {
|
||||||
|
log := logging.FromContext(ctx)
|
||||||
|
// Confirm the migrations table exists :)
|
||||||
|
if err := db.AutoMigrate(&Migration{}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort the registry by ID
|
||||||
|
registry = sortMigrations(registry)
|
||||||
|
var successfulCount int
|
||||||
|
var skippedCount int
|
||||||
|
for _, migration := range registry {
|
||||||
|
// Check if migration is already applied
|
||||||
|
var count int64
|
||||||
|
db.Model(&Migration{}).Where("id = ?", migration.ID()).Count(&count)
|
||||||
|
if count > 0 {
|
||||||
|
skippedCount++
|
||||||
|
log.Debug("Skipping migration %s as it is already applied", migration.ID())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the migration
|
||||||
|
log.Infof("Applying migration: %s", migration.ID())
|
||||||
|
if err := migration.Up(ctx, db); err != nil {
|
||||||
|
|
||||||
|
log.Errorf("Failed to apply migration %s: %s", migration.ID(), err)
|
||||||
|
if err := migration.Down(ctx, db); err != nil {
|
||||||
|
log.Errorf("Failed to rollback migration %s: %s\n", migration.ID(), err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record the migration
|
||||||
|
record := Migration{
|
||||||
|
ID: migration.ID(),
|
||||||
|
Description: migration.Description(),
|
||||||
|
AppliedAt: time.Now().UTC(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Create(&record).Error; err != nil {
|
||||||
|
log.Errorf("Failed to record migration %s: %s\n", migration.ID(), err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
successfulCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(registry) == 0 {
|
||||||
|
log.Info("Migratons: No pending migrations")
|
||||||
|
} else {
|
||||||
|
var failedCount = len(registry) - successfulCount - skippedCount
|
||||||
|
log.Infof("Migrations: %d successful, %d failed, %d skipped, %d total in registry", successfulCount, failedCount, skippedCount, len(registry))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue