diff --git a/api/common.go b/api/common.go index 5c62acd..17d9d58 100644 --- a/api/common.go +++ b/api/common.go @@ -9,6 +9,7 @@ import ( "github.com/gin-gonic/gin" "net/http" "strconv" + "strings" ) func handler(key string, callback func(c *gin.Context) (interface{}, error)) gin.HandlerFunc { @@ -26,7 +27,7 @@ func handler(key string, callback func(c *gin.Context) (interface{}, error)) gin StatusLabels: models.StatusLabels, } - if s.Stats != nil { + if s.Stats != nil && !strings.Contains(c.FullPath(), "/stats/") { options.Stats = make(map[int]models.Stat) for _, stat := range s.Stats { options.Stats[stat.ID] = stat @@ -73,7 +74,7 @@ func scopeIDMiddleware(db database.Database) gin.HandlerFunc { return } - scope, err := db.Scopes().Find(c.Request.Context(), scopeID, c.Request.Method != "GET") + scope, err := db.Scopes().Find(c.Request.Context(), scopeID, strings.Contains(c.FullPath(), "/stats/") || c.Request.Method != "GET") if err != nil || !scope.HasMember(auth.UserID(c)) { c.AbortWithStatusJSON(http.StatusUnauthorized, slerrors.ErrorResponse{ Code: http.StatusNotFound, diff --git a/api/projects.go b/api/projects.go index 887dcd5..b5ed46f 100644 --- a/api/projects.go +++ b/api/projects.go @@ -47,4 +47,142 @@ func Projects(g *gin.RouterGroup, db database.Database) { return db.Projects(getScope(c).ID).Create(c.Request.Context(), *project) })) + + g.PUT("/:id", handler("project", func(c *gin.Context) (interface{}, error) { + id, err := reqInt(c, "id") + if err != nil { + return nil, err + } + + update := &models.ProjectUpdate{} + err = c.BindJSON(update) + if err != nil { + return nil, slerrors.BadRequest("Invalid JSON input: " + err.Error()) + } + + project, err := db.Projects(getScope(c).ID).Find(c.Request.Context(), id) + if err != nil { + return nil, err + } + + return db.Projects(getScope(c).ID).Update(c.Request.Context(), *project, *update) + })) + + g.POST("/:id/requirements", handler("project", func(c *gin.Context) (interface{}, error) { + id, err := reqInt(c, "id") + if err != nil { + return nil, err + } + + requirement := &models.ProjectRequirement{} + err = c.BindJSON(requirement) + if err != nil { + return nil, slerrors.BadRequest("Invalid JSON input: " + err.Error()) + } + + if requirement.Name == "" { + return nil, slerrors.BadRequest("Requirement name cannot be empty.") + } + if !requirement.Status.Valid() { + return nil, slerrors.BadRequest("Bad requirement status.") + } + for _, stat := range requirement.Stats { + if getScope(c).Stat(stat.ID) == nil { + return nil, slerrors.Forbidden("One or more stats are not part of scope.") + } + if stat.Required < 0 { + return nil, slerrors.BadRequest("-1 stats are not allowed when creating project requirements.") + } + } + + project, err := db.Projects(getScope(c).ID).Find(c.Request.Context(), id) + if err != nil { + return nil, err + } + + return db.Projects(getScope(c).ID).AddRequirement(c.Request.Context(), project.ProjectEntry, *requirement) + })) + + g.PUT("/:id/requirements/:rid", handler("project", func(c *gin.Context) (interface{}, error) { + id, err := reqInt(c, "id") + if err != nil { + return nil, err + } + rid, err := reqInt(c, "rid") + if err != nil { + return nil, err + } + + update := &models.ProjectRequirementUpdate{} + err = c.BindJSON(update) + if err != nil { + return nil, slerrors.BadRequest("Invalid JSON input: " + err.Error()) + } + + if update.Name != nil && *update.Name == "" { + return nil, slerrors.BadRequest("Requirement name cannot be empty.") + } + if update.Status != nil && !update.Status.Valid() { + return nil, slerrors.BadRequest("Bad requirement status.") + } + for _, stat := range update.Stats { + if getScope(c).Stat(stat.ID) == nil { + return nil, slerrors.Forbidden("One or more stats are not part of scope.") + } + } + + project, err := db.Projects(getScope(c).ID).Find(c.Request.Context(), id) + if err != nil { + return nil, err + } + requirement := project.Requirement(rid) + if requirement == nil { + return nil, slerrors.NotFound("Project requirement") + } + + return db.Projects(getScope(c).ID).UpdateRequirement(c.Request.Context(), project.ProjectEntry, *requirement, *update) + })) + + g.DELETE("/:id/requirements/:rid", handler("project", func(c *gin.Context) (interface{}, error) { + deleteItems := c.Query("deleteItems") == "true" + id, err := reqInt(c, "id") + if err != nil { + return nil, err + } + rid, err := reqInt(c, "rid") + if err != nil { + return nil, err + } + + project, err := db.Projects(getScope(c).ID).Find(c.Request.Context(), id) + if err != nil { + return nil, err + } + requirement := project.Requirement(rid) + if requirement == nil { + return nil, slerrors.NotFound("Project requirement") + } + + return db.Projects(getScope(c).ID).DeleteRequirement(c.Request.Context(), project.ProjectEntry, *requirement, deleteItems) + })) + + g.DELETE("/:id", handler("project", func(c *gin.Context) (interface{}, error) { + deleteItems := c.Query("deleteItems") == "true" + id, err := reqInt(c, "id") + if err != nil { + return nil, err + } + + project, err := db.Projects(getScope(c).ID).Find(c.Request.Context(), id) + if err != nil { + return nil, err + } + + err = db.Projects(getScope(c).ID).Delete(c.Request.Context(), project.ProjectEntry, deleteItems) + if err != nil { + return nil, err + } + + return project, nil + })) } diff --git a/api/stats.go b/api/stats.go new file mode 100644 index 0000000..8d38752 --- /dev/null +++ b/api/stats.go @@ -0,0 +1,98 @@ +package api + +import ( + "git.aiterp.net/stufflog3/stufflog3-api/internal/database" + "git.aiterp.net/stufflog3/stufflog3-api/internal/models" + "git.aiterp.net/stufflog3/stufflog3-api/internal/slerrors" + "github.com/gin-gonic/gin" +) + +func Stats(g *gin.RouterGroup, db database.Database) { + g.Use(scopeIDMiddleware(db)) + + g.GET("/", handler("stats", func(c *gin.Context) (interface{}, error) { + return getScope(c).Stats, nil + })) + + g.GET("/:id", handler("stat", func(c *gin.Context) (interface{}, error) { + id, err := reqInt(c, "id") + if err != nil { + return nil, err + } + + stat := getScope(c).Stat(id) + if stat == nil { + return nil, slerrors.NotFound("Stat") + } + + return stat, nil + })) + + g.POST("/", handler("stat", func(c *gin.Context) (interface{}, error) { + stat := &models.Stat{} + err := c.BindJSON(stat) + if err != nil { + return nil, slerrors.BadRequest("Invalid JSON input: " + err.Error()) + } + + if stat.Name == "" { + return nil, slerrors.BadRequest("Project name cannot be blank") + } + if stat.Weight < 0 { + return nil, slerrors.BadRequest("Negative weight not allowed") + } + for key, value := range stat.AllowedAmounts { + if value <= 0 { + return nil, slerrors.BadRequest("Invalid allowed amount: " + key + " (0/-1 only allowed when updating to delete them)") + } + } + + return db.Stats(getScope(c).ID).Create(c.Request.Context(), *stat) + })) + + g.PUT("/:id", handler("stat", func(c *gin.Context) (interface{}, error) { + id, err := reqInt(c, "id") + if err != nil { + return nil, err + } + + update := &models.StatUpdate{} + err = c.BindJSON(update) + if err != nil { + return nil, slerrors.BadRequest("Invalid JSON input: " + err.Error()) + } + + stat := getScope(c).Stat(id) + if stat == nil { + return nil, slerrors.NotFound("Stat") + } + + if update.Name != nil && *update.Name == "" { + return nil, slerrors.BadRequest("Project name cannot be blank") + } + if update.Weight != nil && *update.Weight < 0 { + return nil, slerrors.BadRequest("Negative weight not allowed") + } + + return db.Stats(getScope(c).ID).Update(c.Request.Context(), *stat, *update) + })) + + g.DELETE("/:id", handler("stat", func(c *gin.Context) (interface{}, error) { + id, err := reqInt(c, "id") + if err != nil { + return nil, err + } + + stat := getScope(c).Stat(id) + if stat == nil { + return nil, slerrors.NotFound("Stat") + } + + err = db.Stats(getScope(c).ID).Delete(c.Request.Context(), *stat) + if err != nil { + return nil, err + } + + return stat, nil + })) +} diff --git a/cmd/stufflog3-server.go b/cmd/stufflog3-server.go index 23a1d5c..ec8e0b6 100644 --- a/cmd/stufflog3-server.go +++ b/cmd/stufflog3-server.go @@ -36,6 +36,7 @@ func main() { api.Scopes(server.Group("/api/scopes"), db) api.Items(server.Group("/api/scope/:scope_id/items"), db) api.Projects(server.Group("/api/scope/:scope_id/projects"), db) + api.Stats(server.Group("/api/scope/:scope_id/stats"), db) exitSignal := make(chan os.Signal) signal.Notify(exitSignal, os.Interrupt, os.Kill, syscall.SIGTERM) diff --git a/internal/database/database.go b/internal/database/database.go index 6da42b1..b616eae 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -29,7 +29,7 @@ type StatRepository interface { Find(ctx context.Context, id int) (*models.Stat, error) List(ctx context.Context) ([]models.Stat, error) Create(ctx context.Context, stat models.Stat) (*models.Stat, error) - Update(ctx context.Context, stat models.Stat) error + Update(ctx context.Context, stat models.Stat, update models.StatUpdate) (*models.Stat, error) Delete(ctx context.Context, stat models.Stat) error } @@ -55,10 +55,10 @@ type ProjectRepository interface { Find(ctx context.Context, id int) (*models.Project, error) List(ctx context.Context) ([]models.ProjectEntry, error) Create(ctx context.Context, project models.Project) (*models.Project, error) - Update(ctx context.Context, project models.Project) (*models.Project, error) - Delete(ctx context.Context, project models.ProjectEntry) error + Update(ctx context.Context, project models.Project, update models.ProjectUpdate) (*models.Project, error) + Delete(ctx context.Context, project models.ProjectEntry, deleteItems bool) error - AddRequirement(ctx context.Context, project models.ProjectEntry, requirement models.ProjectRequirement) - UpdateRequirement(ctx context.Context, project models.ProjectEntry, requirement models.ProjectRequirement, update models.ProjectRequirementUpdate) - DeleteRequirement(ctx context.Context, project models.ProjectEntry, requirement models.ProjectRequirement, deleteItems bool) + AddRequirement(ctx context.Context, project models.ProjectEntry, requirement models.ProjectRequirement) (*models.Project, error) + UpdateRequirement(ctx context.Context, project models.ProjectEntry, requirement models.ProjectRequirement, update models.ProjectRequirementUpdate) (*models.Project, error) + DeleteRequirement(ctx context.Context, project models.ProjectEntry, requirement models.ProjectRequirement, deleteItems bool) (*models.Project, error) } diff --git a/internal/database/mysql/database.go b/internal/database/mysql/database.go index 8509f2f..f4e7a9b 100644 --- a/internal/database/mysql/database.go +++ b/internal/database/mysql/database.go @@ -21,8 +21,10 @@ func (d *Database) Scopes() database.ScopeRepository { } func (d *Database) Stats(scopeID int) database.StatRepository { - //TODO implement me - panic("implement me") + return &statsRepository{ + db: d.db, q: d.q, + scopeID: scopeID, + } } func (d *Database) Items(scopeID int) database.ItemRepository { diff --git a/internal/database/mysql/mysqlcore/db.go b/internal/database/mysql/mysqlcore/db.go index 0c22692..a00084d 100644 --- a/internal/database/mysql/mysqlcore/db.go +++ b/internal/database/mysql/mysqlcore/db.go @@ -24,12 +24,27 @@ func New(db DBTX) *Queries { func Prepare(ctx context.Context, db DBTX) (*Queries, error) { q := Queries{db: db} var err error + if q.cLearItemStatProgressByStatStmt, err = db.PrepareContext(ctx, cLearItemStatProgressByStat); err != nil { + return nil, fmt.Errorf("error preparing query CLearItemStatProgressByStat: %w", err) + } if q.clearItemProjectRequirementStmt, err = db.PrepareContext(ctx, clearItemProjectRequirement); err != nil { return nil, fmt.Errorf("error preparing query ClearItemProjectRequirement: %w", err) } + if q.clearItemProjectRequirementByProjectIDStmt, err = db.PrepareContext(ctx, clearItemProjectRequirementByProjectID); err != nil { + return nil, fmt.Errorf("error preparing query ClearItemProjectRequirementByProjectID: %w", err) + } if q.clearItemStatProgressStmt, err = db.PrepareContext(ctx, clearItemStatProgress); err != nil { return nil, fmt.Errorf("error preparing query ClearItemStatProgress: %w", err) } + if q.deleteAllProjectRequirementStatsStmt, err = db.PrepareContext(ctx, deleteAllProjectRequirementStats); err != nil { + return nil, fmt.Errorf("error preparing query DeleteAllProjectRequirementStats: %w", err) + } + if q.deleteAllProjectRequirementStatsByStatStmt, err = db.PrepareContext(ctx, deleteAllProjectRequirementStatsByStat); err != nil { + return nil, fmt.Errorf("error preparing query DeleteAllProjectRequirementStatsByStat: %w", err) + } + if q.deleteAllProjectRequirementsStmt, err = db.PrepareContext(ctx, deleteAllProjectRequirements); err != nil { + return nil, fmt.Errorf("error preparing query DeleteAllProjectRequirements: %w", err) + } if q.deleteAllScopeMembersStmt, err = db.PrepareContext(ctx, deleteAllScopeMembers); err != nil { return nil, fmt.Errorf("error preparing query DeleteAllScopeMembers: %w", err) } @@ -48,12 +63,18 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) { if q.deleteProjectRequirementStmt, err = db.PrepareContext(ctx, deleteProjectRequirement); err != nil { return nil, fmt.Errorf("error preparing query DeleteProjectRequirement: %w", err) } + if q.deleteProjectRequirementStatStmt, err = db.PrepareContext(ctx, deleteProjectRequirementStat); err != nil { + return nil, fmt.Errorf("error preparing query DeleteProjectRequirementStat: %w", err) + } if q.deleteScopeStmt, err = db.PrepareContext(ctx, deleteScope); err != nil { return nil, fmt.Errorf("error preparing query DeleteScope: %w", err) } if q.deleteScopeMemberStmt, err = db.PrepareContext(ctx, deleteScopeMember); err != nil { return nil, fmt.Errorf("error preparing query DeleteScopeMember: %w", err) } + if q.deleteStatStmt, err = db.PrepareContext(ctx, deleteStat); err != nil { + return nil, fmt.Errorf("error preparing query DeleteStat: %w", err) + } if q.getItemStmt, err = db.PrepareContext(ctx, getItem); err != nil { return nil, fmt.Errorf("error preparing query GetItem: %w", err) } @@ -75,6 +96,9 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) { if q.getScopeWithDisplayNameStmt, err = db.PrepareContext(ctx, getScopeWithDisplayName); err != nil { return nil, fmt.Errorf("error preparing query GetScopeWithDisplayName: %w", err) } + if q.getStatStmt, err = db.PrepareContext(ctx, getStat); err != nil { + return nil, fmt.Errorf("error preparing query GetStat: %w", err) + } if q.insertItemStmt, err = db.PrepareContext(ctx, insertItem); err != nil { return nil, fmt.Errorf("error preparing query InsertItem: %w", err) } @@ -87,6 +111,9 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) { if q.insertScopeStmt, err = db.PrepareContext(ctx, insertScope); err != nil { return nil, fmt.Errorf("error preparing query InsertScope: %w", err) } + if q.insertStatStmt, err = db.PrepareContext(ctx, insertStat); err != nil { + return nil, fmt.Errorf("error preparing query InsertStat: %w", err) + } if q.listItemStatProgressStmt, err = db.PrepareContext(ctx, listItemStatProgress); err != nil { return nil, fmt.Errorf("error preparing query ListItemStatProgress: %w", err) } @@ -141,6 +168,9 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) { if q.replaceItemStatProgressStmt, err = db.PrepareContext(ctx, replaceItemStatProgress); err != nil { return nil, fmt.Errorf("error preparing query ReplaceItemStatProgress: %w", err) } + if q.replaceProjectRequirementStatStmt, err = db.PrepareContext(ctx, replaceProjectRequirementStat); err != nil { + return nil, fmt.Errorf("error preparing query ReplaceProjectRequirementStat: %w", err) + } if q.updateItemStmt, err = db.PrepareContext(ctx, updateItem); err != nil { return nil, fmt.Errorf("error preparing query UpdateItem: %w", err) } @@ -156,21 +186,49 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) { if q.updateScopeMemberStmt, err = db.PrepareContext(ctx, updateScopeMember); err != nil { return nil, fmt.Errorf("error preparing query UpdateScopeMember: %w", err) } + if q.updateStatStmt, err = db.PrepareContext(ctx, updateStat); err != nil { + return nil, fmt.Errorf("error preparing query UpdateStat: %w", err) + } return &q, nil } func (q *Queries) Close() error { var err error + if q.cLearItemStatProgressByStatStmt != nil { + if cerr := q.cLearItemStatProgressByStatStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing cLearItemStatProgressByStatStmt: %w", cerr) + } + } if q.clearItemProjectRequirementStmt != nil { if cerr := q.clearItemProjectRequirementStmt.Close(); cerr != nil { err = fmt.Errorf("error closing clearItemProjectRequirementStmt: %w", cerr) } } + if q.clearItemProjectRequirementByProjectIDStmt != nil { + if cerr := q.clearItemProjectRequirementByProjectIDStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing clearItemProjectRequirementByProjectIDStmt: %w", cerr) + } + } if q.clearItemStatProgressStmt != nil { if cerr := q.clearItemStatProgressStmt.Close(); cerr != nil { err = fmt.Errorf("error closing clearItemStatProgressStmt: %w", cerr) } } + if q.deleteAllProjectRequirementStatsStmt != nil { + if cerr := q.deleteAllProjectRequirementStatsStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing deleteAllProjectRequirementStatsStmt: %w", cerr) + } + } + if q.deleteAllProjectRequirementStatsByStatStmt != nil { + if cerr := q.deleteAllProjectRequirementStatsByStatStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing deleteAllProjectRequirementStatsByStatStmt: %w", cerr) + } + } + if q.deleteAllProjectRequirementsStmt != nil { + if cerr := q.deleteAllProjectRequirementsStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing deleteAllProjectRequirementsStmt: %w", cerr) + } + } if q.deleteAllScopeMembersStmt != nil { if cerr := q.deleteAllScopeMembersStmt.Close(); cerr != nil { err = fmt.Errorf("error closing deleteAllScopeMembersStmt: %w", cerr) @@ -201,6 +259,11 @@ func (q *Queries) Close() error { err = fmt.Errorf("error closing deleteProjectRequirementStmt: %w", cerr) } } + if q.deleteProjectRequirementStatStmt != nil { + if cerr := q.deleteProjectRequirementStatStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing deleteProjectRequirementStatStmt: %w", cerr) + } + } if q.deleteScopeStmt != nil { if cerr := q.deleteScopeStmt.Close(); cerr != nil { err = fmt.Errorf("error closing deleteScopeStmt: %w", cerr) @@ -211,6 +274,11 @@ func (q *Queries) Close() error { err = fmt.Errorf("error closing deleteScopeMemberStmt: %w", cerr) } } + if q.deleteStatStmt != nil { + if cerr := q.deleteStatStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing deleteStatStmt: %w", cerr) + } + } if q.getItemStmt != nil { if cerr := q.getItemStmt.Close(); cerr != nil { err = fmt.Errorf("error closing getItemStmt: %w", cerr) @@ -246,6 +314,11 @@ func (q *Queries) Close() error { err = fmt.Errorf("error closing getScopeWithDisplayNameStmt: %w", cerr) } } + if q.getStatStmt != nil { + if cerr := q.getStatStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing getStatStmt: %w", cerr) + } + } if q.insertItemStmt != nil { if cerr := q.insertItemStmt.Close(); cerr != nil { err = fmt.Errorf("error closing insertItemStmt: %w", cerr) @@ -266,6 +339,11 @@ func (q *Queries) Close() error { err = fmt.Errorf("error closing insertScopeStmt: %w", cerr) } } + if q.insertStatStmt != nil { + if cerr := q.insertStatStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing insertStatStmt: %w", cerr) + } + } if q.listItemStatProgressStmt != nil { if cerr := q.listItemStatProgressStmt.Close(); cerr != nil { err = fmt.Errorf("error closing listItemStatProgressStmt: %w", cerr) @@ -356,6 +434,11 @@ func (q *Queries) Close() error { err = fmt.Errorf("error closing replaceItemStatProgressStmt: %w", cerr) } } + if q.replaceProjectRequirementStatStmt != nil { + if cerr := q.replaceProjectRequirementStatStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing replaceProjectRequirementStatStmt: %w", cerr) + } + } if q.updateItemStmt != nil { if cerr := q.updateItemStmt.Close(); cerr != nil { err = fmt.Errorf("error closing updateItemStmt: %w", cerr) @@ -381,6 +464,11 @@ func (q *Queries) Close() error { err = fmt.Errorf("error closing updateScopeMemberStmt: %w", cerr) } } + if q.updateStatStmt != nil { + if cerr := q.updateStatStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing updateStatStmt: %w", cerr) + } + } return err } @@ -418,101 +506,123 @@ func (q *Queries) queryRow(ctx context.Context, stmt *sql.Stmt, query string, ar } type Queries struct { - db DBTX - tx *sql.Tx - clearItemProjectRequirementStmt *sql.Stmt - clearItemStatProgressStmt *sql.Stmt - deleteAllScopeMembersStmt *sql.Stmt - deleteItemStmt *sql.Stmt - deleteItemForRequirementStmt *sql.Stmt - deleteItemStatProgressStmt *sql.Stmt - deleteProjectStmt *sql.Stmt - deleteProjectRequirementStmt *sql.Stmt - deleteScopeStmt *sql.Stmt - deleteScopeMemberStmt *sql.Stmt - getItemStmt *sql.Stmt - getItemStatProgressBetweenStmt *sql.Stmt - getProjectStmt *sql.Stmt - getProjectRequirementStmt *sql.Stmt - getScopeStmt *sql.Stmt - getScopeDisplayNameStmt *sql.Stmt - getScopeWithDisplayNameStmt *sql.Stmt - insertItemStmt *sql.Stmt - insertProjectStmt *sql.Stmt - insertProjectRequirementStmt *sql.Stmt - insertScopeStmt *sql.Stmt - listItemStatProgressStmt *sql.Stmt - listItemStatProgressMultiStmt *sql.Stmt - listItemsAcquiredBetweenStmt *sql.Stmt - listItemsByProjectStmt *sql.Stmt - listItemsCreatedBetweenStmt *sql.Stmt - listItemsCreatedBetweenNoScopeStmt *sql.Stmt - listItemsLooseBetweenStmt *sql.Stmt - listItemsLooseBetweenNoScopeStmt *sql.Stmt - listItemsScheduledBetweenStmt *sql.Stmt - listItemsScheduledBetweenNoScopeStmt *sql.Stmt - listProjectEntriesStmt *sql.Stmt - listProjectRequirementStatsStmt *sql.Stmt - listProjectRequirementsByProjectIDStmt *sql.Stmt - listScopeMembersStmt *sql.Stmt - listScopesStmt *sql.Stmt - listScopesByUserStmt *sql.Stmt - listStatsStmt *sql.Stmt - replaceItemStatProgressStmt *sql.Stmt - updateItemStmt *sql.Stmt - updateProjectStmt *sql.Stmt - updateProjectRequirementStmt *sql.Stmt - updateScopeStmt *sql.Stmt - updateScopeMemberStmt *sql.Stmt + db DBTX + tx *sql.Tx + cLearItemStatProgressByStatStmt *sql.Stmt + clearItemProjectRequirementStmt *sql.Stmt + clearItemProjectRequirementByProjectIDStmt *sql.Stmt + clearItemStatProgressStmt *sql.Stmt + deleteAllProjectRequirementStatsStmt *sql.Stmt + deleteAllProjectRequirementStatsByStatStmt *sql.Stmt + deleteAllProjectRequirementsStmt *sql.Stmt + deleteAllScopeMembersStmt *sql.Stmt + deleteItemStmt *sql.Stmt + deleteItemForRequirementStmt *sql.Stmt + deleteItemStatProgressStmt *sql.Stmt + deleteProjectStmt *sql.Stmt + deleteProjectRequirementStmt *sql.Stmt + deleteProjectRequirementStatStmt *sql.Stmt + deleteScopeStmt *sql.Stmt + deleteScopeMemberStmt *sql.Stmt + deleteStatStmt *sql.Stmt + getItemStmt *sql.Stmt + getItemStatProgressBetweenStmt *sql.Stmt + getProjectStmt *sql.Stmt + getProjectRequirementStmt *sql.Stmt + getScopeStmt *sql.Stmt + getScopeDisplayNameStmt *sql.Stmt + getScopeWithDisplayNameStmt *sql.Stmt + getStatStmt *sql.Stmt + insertItemStmt *sql.Stmt + insertProjectStmt *sql.Stmt + insertProjectRequirementStmt *sql.Stmt + insertScopeStmt *sql.Stmt + insertStatStmt *sql.Stmt + listItemStatProgressStmt *sql.Stmt + listItemStatProgressMultiStmt *sql.Stmt + listItemsAcquiredBetweenStmt *sql.Stmt + listItemsByProjectStmt *sql.Stmt + listItemsCreatedBetweenStmt *sql.Stmt + listItemsCreatedBetweenNoScopeStmt *sql.Stmt + listItemsLooseBetweenStmt *sql.Stmt + listItemsLooseBetweenNoScopeStmt *sql.Stmt + listItemsScheduledBetweenStmt *sql.Stmt + listItemsScheduledBetweenNoScopeStmt *sql.Stmt + listProjectEntriesStmt *sql.Stmt + listProjectRequirementStatsStmt *sql.Stmt + listProjectRequirementsByProjectIDStmt *sql.Stmt + listScopeMembersStmt *sql.Stmt + listScopesStmt *sql.Stmt + listScopesByUserStmt *sql.Stmt + listStatsStmt *sql.Stmt + replaceItemStatProgressStmt *sql.Stmt + replaceProjectRequirementStatStmt *sql.Stmt + updateItemStmt *sql.Stmt + updateProjectStmt *sql.Stmt + updateProjectRequirementStmt *sql.Stmt + updateScopeStmt *sql.Stmt + updateScopeMemberStmt *sql.Stmt + updateStatStmt *sql.Stmt } func (q *Queries) WithTx(tx *sql.Tx) *Queries { return &Queries{ - db: tx, - tx: tx, - clearItemProjectRequirementStmt: q.clearItemProjectRequirementStmt, - clearItemStatProgressStmt: q.clearItemStatProgressStmt, - deleteAllScopeMembersStmt: q.deleteAllScopeMembersStmt, - deleteItemStmt: q.deleteItemStmt, - deleteItemForRequirementStmt: q.deleteItemForRequirementStmt, - deleteItemStatProgressStmt: q.deleteItemStatProgressStmt, - deleteProjectStmt: q.deleteProjectStmt, - deleteProjectRequirementStmt: q.deleteProjectRequirementStmt, - deleteScopeStmt: q.deleteScopeStmt, - deleteScopeMemberStmt: q.deleteScopeMemberStmt, - getItemStmt: q.getItemStmt, - getItemStatProgressBetweenStmt: q.getItemStatProgressBetweenStmt, - getProjectStmt: q.getProjectStmt, - getProjectRequirementStmt: q.getProjectRequirementStmt, - getScopeStmt: q.getScopeStmt, - getScopeDisplayNameStmt: q.getScopeDisplayNameStmt, - getScopeWithDisplayNameStmt: q.getScopeWithDisplayNameStmt, - insertItemStmt: q.insertItemStmt, - insertProjectStmt: q.insertProjectStmt, - insertProjectRequirementStmt: q.insertProjectRequirementStmt, - insertScopeStmt: q.insertScopeStmt, - listItemStatProgressStmt: q.listItemStatProgressStmt, - listItemStatProgressMultiStmt: q.listItemStatProgressMultiStmt, - listItemsAcquiredBetweenStmt: q.listItemsAcquiredBetweenStmt, - listItemsByProjectStmt: q.listItemsByProjectStmt, - listItemsCreatedBetweenStmt: q.listItemsCreatedBetweenStmt, - listItemsCreatedBetweenNoScopeStmt: q.listItemsCreatedBetweenNoScopeStmt, - listItemsLooseBetweenStmt: q.listItemsLooseBetweenStmt, - listItemsLooseBetweenNoScopeStmt: q.listItemsLooseBetweenNoScopeStmt, - listItemsScheduledBetweenStmt: q.listItemsScheduledBetweenStmt, - listItemsScheduledBetweenNoScopeStmt: q.listItemsScheduledBetweenNoScopeStmt, - listProjectEntriesStmt: q.listProjectEntriesStmt, - listProjectRequirementStatsStmt: q.listProjectRequirementStatsStmt, - listProjectRequirementsByProjectIDStmt: q.listProjectRequirementsByProjectIDStmt, - listScopeMembersStmt: q.listScopeMembersStmt, - listScopesStmt: q.listScopesStmt, - listScopesByUserStmt: q.listScopesByUserStmt, - listStatsStmt: q.listStatsStmt, - replaceItemStatProgressStmt: q.replaceItemStatProgressStmt, - updateItemStmt: q.updateItemStmt, - updateProjectStmt: q.updateProjectStmt, - updateProjectRequirementStmt: q.updateProjectRequirementStmt, - updateScopeStmt: q.updateScopeStmt, - updateScopeMemberStmt: q.updateScopeMemberStmt, + db: tx, + tx: tx, + cLearItemStatProgressByStatStmt: q.cLearItemStatProgressByStatStmt, + clearItemProjectRequirementStmt: q.clearItemProjectRequirementStmt, + clearItemProjectRequirementByProjectIDStmt: q.clearItemProjectRequirementByProjectIDStmt, + clearItemStatProgressStmt: q.clearItemStatProgressStmt, + deleteAllProjectRequirementStatsStmt: q.deleteAllProjectRequirementStatsStmt, + deleteAllProjectRequirementStatsByStatStmt: q.deleteAllProjectRequirementStatsByStatStmt, + deleteAllProjectRequirementsStmt: q.deleteAllProjectRequirementsStmt, + deleteAllScopeMembersStmt: q.deleteAllScopeMembersStmt, + deleteItemStmt: q.deleteItemStmt, + deleteItemForRequirementStmt: q.deleteItemForRequirementStmt, + deleteItemStatProgressStmt: q.deleteItemStatProgressStmt, + deleteProjectStmt: q.deleteProjectStmt, + deleteProjectRequirementStmt: q.deleteProjectRequirementStmt, + deleteProjectRequirementStatStmt: q.deleteProjectRequirementStatStmt, + deleteScopeStmt: q.deleteScopeStmt, + deleteScopeMemberStmt: q.deleteScopeMemberStmt, + deleteStatStmt: q.deleteStatStmt, + getItemStmt: q.getItemStmt, + getItemStatProgressBetweenStmt: q.getItemStatProgressBetweenStmt, + getProjectStmt: q.getProjectStmt, + getProjectRequirementStmt: q.getProjectRequirementStmt, + getScopeStmt: q.getScopeStmt, + getScopeDisplayNameStmt: q.getScopeDisplayNameStmt, + getScopeWithDisplayNameStmt: q.getScopeWithDisplayNameStmt, + getStatStmt: q.getStatStmt, + insertItemStmt: q.insertItemStmt, + insertProjectStmt: q.insertProjectStmt, + insertProjectRequirementStmt: q.insertProjectRequirementStmt, + insertScopeStmt: q.insertScopeStmt, + insertStatStmt: q.insertStatStmt, + listItemStatProgressStmt: q.listItemStatProgressStmt, + listItemStatProgressMultiStmt: q.listItemStatProgressMultiStmt, + listItemsAcquiredBetweenStmt: q.listItemsAcquiredBetweenStmt, + listItemsByProjectStmt: q.listItemsByProjectStmt, + listItemsCreatedBetweenStmt: q.listItemsCreatedBetweenStmt, + listItemsCreatedBetweenNoScopeStmt: q.listItemsCreatedBetweenNoScopeStmt, + listItemsLooseBetweenStmt: q.listItemsLooseBetweenStmt, + listItemsLooseBetweenNoScopeStmt: q.listItemsLooseBetweenNoScopeStmt, + listItemsScheduledBetweenStmt: q.listItemsScheduledBetweenStmt, + listItemsScheduledBetweenNoScopeStmt: q.listItemsScheduledBetweenNoScopeStmt, + listProjectEntriesStmt: q.listProjectEntriesStmt, + listProjectRequirementStatsStmt: q.listProjectRequirementStatsStmt, + listProjectRequirementsByProjectIDStmt: q.listProjectRequirementsByProjectIDStmt, + listScopeMembersStmt: q.listScopeMembersStmt, + listScopesStmt: q.listScopesStmt, + listScopesByUserStmt: q.listScopesByUserStmt, + listStatsStmt: q.listStatsStmt, + replaceItemStatProgressStmt: q.replaceItemStatProgressStmt, + replaceProjectRequirementStatStmt: q.replaceProjectRequirementStatStmt, + updateItemStmt: q.updateItemStmt, + updateProjectStmt: q.updateProjectStmt, + updateProjectRequirementStmt: q.updateProjectRequirementStmt, + updateScopeStmt: q.updateScopeStmt, + updateScopeMemberStmt: q.updateScopeMemberStmt, + updateStatStmt: q.updateStatStmt, } } diff --git a/internal/database/mysql/mysqlcore/item.sql.go b/internal/database/mysql/mysqlcore/item.sql.go index 2f7e726..4a3fcfc 100644 --- a/internal/database/mysql/mysqlcore/item.sql.go +++ b/internal/database/mysql/mysqlcore/item.sql.go @@ -13,6 +13,15 @@ import ( "git.aiterp.net/stufflog3/stufflog3-api/internal/sqltypes" ) +const cLearItemStatProgressByStat = `-- name: CLearItemStatProgressByStat :exec +DELETE FROM item_stat_progress WHERE stat_id = ? +` + +func (q *Queries) CLearItemStatProgressByStat(ctx context.Context, statID int) error { + _, err := q.exec(ctx, q.cLearItemStatProgressByStatStmt, cLearItemStatProgressByStat, statID) + return err +} + const clearItemProjectRequirement = `-- name: ClearItemProjectRequirement :exec UPDATE item SET project_requirement_id = NULL WHERE project_requirement_id = ? ` @@ -22,6 +31,18 @@ func (q *Queries) ClearItemProjectRequirement(ctx context.Context, projectRequir return err } +const clearItemProjectRequirementByProjectID = `-- name: ClearItemProjectRequirementByProjectID :exec +UPDATE item i +LEFT JOIN project_requirement pr ON pr.id = i.project_requirement_id +SET i.project_requirement_id = NULL +WHERE pr.project_id = ? +` + +func (q *Queries) ClearItemProjectRequirementByProjectID(ctx context.Context, projectID int) error { + _, err := q.exec(ctx, q.clearItemProjectRequirementByProjectIDStmt, clearItemProjectRequirementByProjectID, projectID) + return err +} + const clearItemStatProgress = `-- name: ClearItemStatProgress :exec DELETE FROM item_stat_progress WHERE item_id = ? ` diff --git a/internal/database/mysql/mysqlcore/project.sql.go b/internal/database/mysql/mysqlcore/project.sql.go index 9becf3d..6390d46 100644 --- a/internal/database/mysql/mysqlcore/project.sql.go +++ b/internal/database/mysql/mysqlcore/project.sql.go @@ -11,6 +11,33 @@ import ( "time" ) +const deleteAllProjectRequirementStats = `-- name: DeleteAllProjectRequirementStats :exec +DELETE FROM project_requirement_stat WHERE project_requirement_id = ? +` + +func (q *Queries) DeleteAllProjectRequirementStats(ctx context.Context, projectRequirementID int) error { + _, err := q.exec(ctx, q.deleteAllProjectRequirementStatsStmt, deleteAllProjectRequirementStats, projectRequirementID) + return err +} + +const deleteAllProjectRequirementStatsByStat = `-- name: DeleteAllProjectRequirementStatsByStat :exec +DELETE FROM project_requirement_stat WHERE stat_id = ? +` + +func (q *Queries) DeleteAllProjectRequirementStatsByStat(ctx context.Context, statID int) error { + _, err := q.exec(ctx, q.deleteAllProjectRequirementStatsByStatStmt, deleteAllProjectRequirementStatsByStat, statID) + return err +} + +const deleteAllProjectRequirements = `-- name: DeleteAllProjectRequirements :exec +DELETE FROM project_requirement WHERE project_id = ? +` + +func (q *Queries) DeleteAllProjectRequirements(ctx context.Context, projectID int) error { + _, err := q.exec(ctx, q.deleteAllProjectRequirementsStmt, deleteAllProjectRequirements, projectID) + return err +} + const deleteProject = `-- name: DeleteProject :exec DELETE FROM project WHERE id = ? ` @@ -29,6 +56,20 @@ func (q *Queries) DeleteProjectRequirement(ctx context.Context, id int) error { return err } +const deleteProjectRequirementStat = `-- name: DeleteProjectRequirementStat :exec +DELETE FROM project_requirement_stat WHERE project_requirement_id = ? AND stat_id = ? +` + +type DeleteProjectRequirementStatParams struct { + ProjectRequirementID int + StatID int +} + +func (q *Queries) DeleteProjectRequirementStat(ctx context.Context, arg DeleteProjectRequirementStatParams) error { + _, err := q.exec(ctx, q.deleteProjectRequirementStatStmt, deleteProjectRequirementStat, arg.ProjectRequirementID, arg.StatID) + return err +} + const getProject = `-- name: GetProject :one SELECT id, scope_id, author_id, name, status, description, created_time FROM project WHERE id = ? ` @@ -232,6 +273,22 @@ func (q *Queries) ListProjectRequirementsByProjectID(ctx context.Context, projec return items, nil } +const replaceProjectRequirementStat = `-- name: ReplaceProjectRequirementStat :exec +REPLACE INTO project_requirement_stat (project_requirement_id, stat_id, required) +VALUES (?, ?, ?) +` + +type ReplaceProjectRequirementStatParams struct { + ProjectRequirementID int + StatID int + Required int +} + +func (q *Queries) ReplaceProjectRequirementStat(ctx context.Context, arg ReplaceProjectRequirementStatParams) error { + _, err := q.exec(ctx, q.replaceProjectRequirementStatStmt, replaceProjectRequirementStat, arg.ProjectRequirementID, arg.StatID, arg.Required) + return err +} + const updateProject = `-- name: UpdateProject :exec UPDATE project SET name = ?, diff --git a/internal/database/mysql/mysqlcore/stats.sql.go b/internal/database/mysql/mysqlcore/stats.sql.go index 0db08ff..6bf5feb 100644 --- a/internal/database/mysql/mysqlcore/stats.sql.go +++ b/internal/database/mysql/mysqlcore/stats.sql.go @@ -7,10 +7,79 @@ package mysqlcore import ( "context" + "database/sql" "git.aiterp.net/stufflog3/stufflog3-api/internal/sqltypes" ) +const deleteStat = `-- name: DeleteStat :exec +DELETE FROM stat WHERE id = ? AND scope_id = ? +` + +type DeleteStatParams struct { + ID int + ScopeID int +} + +func (q *Queries) DeleteStat(ctx context.Context, arg DeleteStatParams) error { + _, err := q.exec(ctx, q.deleteStatStmt, deleteStat, arg.ID, arg.ScopeID) + return err +} + +const getStat = `-- name: GetStat :one +SELECT id, name, description, weight, allowed_amounts FROM stat +WHERE scope_id = ? AND id = ? +` + +type GetStatParams struct { + ScopeID int + ID int +} + +type GetStatRow struct { + ID int + Name string + Description string + Weight float64 + AllowedAmounts sqltypes.NullRawMessage +} + +func (q *Queries) GetStat(ctx context.Context, arg GetStatParams) (GetStatRow, error) { + row := q.queryRow(ctx, q.getStatStmt, getStat, arg.ScopeID, arg.ID) + var i GetStatRow + err := row.Scan( + &i.ID, + &i.Name, + &i.Description, + &i.Weight, + &i.AllowedAmounts, + ) + return i, err +} + +const insertStat = `-- name: InsertStat :execresult +INSERT INTO stat (scope_id, name, description, weight, allowed_amounts) +VALUES (?, ?, ?, ?, ?) +` + +type InsertStatParams struct { + ScopeID int + Name string + Description string + Weight float64 + AllowedAmounts sqltypes.NullRawMessage +} + +func (q *Queries) InsertStat(ctx context.Context, arg InsertStatParams) (sql.Result, error) { + return q.exec(ctx, q.insertStatStmt, insertStat, + arg.ScopeID, + arg.Name, + arg.Description, + arg.Weight, + arg.AllowedAmounts, + ) +} + const listStats = `-- name: ListStats :many SELECT id, name, description, weight, allowed_amounts FROM stat WHERE scope_id = ? @@ -52,3 +121,33 @@ func (q *Queries) ListStats(ctx context.Context, scopeID int) ([]ListStatsRow, e } return items, nil } + +const updateStat = `-- name: UpdateStat :exec +UPDATE stat SET + name = ?, + description = ?, + weight = ?, + allowed_amounts = ? +WHERE id = ? AND scope_id = ? +` + +type UpdateStatParams struct { + Name string + Description string + Weight float64 + AllowedAmounts sqltypes.NullRawMessage + ID int + ScopeID int +} + +func (q *Queries) UpdateStat(ctx context.Context, arg UpdateStatParams) error { + _, err := q.exec(ctx, q.updateStatStmt, updateStat, + arg.Name, + arg.Description, + arg.Weight, + arg.AllowedAmounts, + arg.ID, + arg.ScopeID, + ) + return err +} diff --git a/internal/database/mysql/project.go b/internal/database/mysql/project.go index 03d43ff..e10f971 100644 --- a/internal/database/mysql/project.go +++ b/internal/database/mysql/project.go @@ -154,27 +154,187 @@ func (r *projectRepository) Create(ctx context.Context, project models.Project) return r.Find(ctx, int(id)) } -func (r *projectRepository) Update(ctx context.Context, project models.Project) (*models.Project, error) { - //TODO implement me - panic("implement me") +func (r *projectRepository) Update(ctx context.Context, project models.Project, update models.ProjectUpdate) (*models.Project, error) { + project.Update(update) + + err := r.q.UpdateProject(ctx, mysqlcore.UpdateProjectParams{ + Name: project.Name, + Status: int(project.Status), + Description: project.Description, + ID: project.ID, + }) + if err != nil { + return nil, err + } + + return r.Find(ctx, project.ID) } -func (r *projectRepository) Delete(ctx context.Context, project models.ProjectEntry) error { - //TODO implement me - panic("implement me") +func (r *projectRepository) Delete(ctx context.Context, project models.ProjectEntry, deleteItems bool) error { + tx, err := r.db.BeginTx(ctx, nil) + if err != nil { + return err + } + defer tx.Rollback() + q := r.q.WithTx(tx) + + err = q.DeleteProject(ctx, project.ID) + if err != nil { + return err + } + + if deleteItems { + reqs, err := q.ListProjectRequirementsByProjectID(ctx, project.ID) + if err != nil { + return err + } + + for _, req := range reqs { + err = q.DeleteItemForRequirement(ctx, sql.NullInt32{Valid: true, Int32: int32(req.ID)}) + if err != nil { + return err + } + + err = q.DeleteAllProjectRequirementStats(ctx, req.ID) + if err != nil { + return err + } + } + } else { + err = q.ClearItemProjectRequirementByProjectID(ctx, project.ID) + if err != nil { + return err + } + } + + return tx.Commit() } -func (r *projectRepository) AddRequirement(ctx context.Context, project models.ProjectEntry, requirement models.ProjectRequirement) { - //TODO implement me - panic("implement me") +func (r *projectRepository) AddRequirement(ctx context.Context, project models.ProjectEntry, requirement models.ProjectRequirement) (*models.Project, error) { + tx, err := r.db.BeginTx(ctx, nil) + if err != nil { + return nil, err + } + defer tx.Rollback() + q := r.q.WithTx(tx) + + res, err := q.InsertProjectRequirement(ctx, mysqlcore.InsertProjectRequirementParams{ + ScopeID: r.scopeID, + ProjectID: project.ID, + Name: requirement.Name, + Status: int(requirement.Status), + Description: requirement.Description, + }) + if err != nil { + return nil, err + } + id, err := res.LastInsertId() + if err != nil { + return nil, err + } + + for _, stat := range requirement.Stats { + err = q.ReplaceProjectRequirementStat(ctx, mysqlcore.ReplaceProjectRequirementStatParams{ + ProjectRequirementID: int(id), + StatID: stat.ID, + Required: stat.Required, + }) + if err != nil { + return nil, err + } + } + + err = tx.Commit() + if err != nil { + return nil, err + } + + return r.Find(ctx, project.ID) } -func (r *projectRepository) UpdateRequirement(ctx context.Context, project models.ProjectEntry, requirement models.ProjectRequirement, update models.ProjectRequirementUpdate) { - //TODO implement me - panic("implement me") +func (r *projectRepository) UpdateRequirement(ctx context.Context, project models.ProjectEntry, requirement models.ProjectRequirement, update models.ProjectRequirementUpdate) (*models.Project, error) { + tx, err := r.db.BeginTx(ctx, nil) + if err != nil { + return nil, err + } + defer tx.Rollback() + q := r.q.WithTx(tx) + + requirement.Update(update) + err = q.UpdateProjectRequirement(ctx, mysqlcore.UpdateProjectRequirementParams{ + Name: requirement.Name, + Status: int(requirement.Status), + Description: requirement.Description, + ID: requirement.ID, + }) + if err != nil { + return nil, err + } + + for _, stat := range requirement.Stats { + if stat.Required < 0 { + err = q.DeleteProjectRequirementStat(ctx, mysqlcore.DeleteProjectRequirementStatParams{ + ProjectRequirementID: requirement.ID, + StatID: stat.ID, + }) + } else { + err = q.ReplaceProjectRequirementStat(ctx, mysqlcore.ReplaceProjectRequirementStatParams{ + ProjectRequirementID: requirement.ID, + StatID: stat.ID, + Required: stat.Required, + }) + } + + if err != nil { + return nil, err + } + } + + err = tx.Commit() + if err != nil { + return nil, err + } + + return r.Find(ctx, project.ID) } -func (r *projectRepository) DeleteRequirement(ctx context.Context, project models.ProjectEntry, requirement models.ProjectRequirement, deleteItems bool) { - //TODO implement me - panic("implement me") +func (r *projectRepository) DeleteRequirement(ctx context.Context, project models.ProjectEntry, requirement models.ProjectRequirement, deleteItems bool) (*models.Project, error) { + tx, err := r.db.BeginTx(ctx, nil) + if err != nil { + return nil, err + } + defer tx.Rollback() + q := r.q.WithTx(tx) + + err = q.DeleteProjectRequirement(ctx, requirement.ID) + if err != nil { + if err == sql.ErrNoRows { + return nil, slerrors.NotFound("Project requirement") + } + return nil, err + } + + if deleteItems { + err = q.DeleteItemForRequirement(ctx, sql.NullInt32{Valid: true, Int32: int32(requirement.ID)}) + if err != nil { + return nil, err + } + } else { + err = q.ClearItemProjectRequirement(ctx, sql.NullInt32{Valid: true, Int32: int32(requirement.ID)}) + if err != nil { + return nil, err + } + + err = q.DeleteAllProjectRequirementStats(ctx, requirement.ID) + if err != nil { + return nil, err + } + } + + err = tx.Commit() + if err != nil { + return nil, err + } + + return r.Find(ctx, project.ID) } diff --git a/internal/database/mysql/queries/item.sql b/internal/database/mysql/queries/item.sql index 4925f5d..59bc567 100644 --- a/internal/database/mysql/queries/item.sql +++ b/internal/database/mysql/queries/item.sql @@ -105,6 +105,12 @@ DELETE FROM item WHERE project_requirement_id = ?; -- name: ClearItemProjectRequirement :exec UPDATE item SET project_requirement_id = NULL WHERE project_requirement_id = ?; +-- name: ClearItemProjectRequirementByProjectID :exec +UPDATE item i +LEFT JOIN project_requirement pr ON pr.id = i.project_requirement_id +SET i.project_requirement_id = NULL +WHERE pr.project_id = ?; + -- name: ReplaceItemStatProgress :exec REPLACE INTO item_stat_progress (item_id, stat_id, acquired, required) VALUES (?, ?, ?, ?); @@ -114,3 +120,6 @@ DELETE FROM item_stat_progress WHERE item_id = ? AND stat_id = ?; -- name: ClearItemStatProgress :exec DELETE FROM item_stat_progress WHERE item_id = ?; + +-- name: CLearItemStatProgressByStat :exec +DELETE FROM item_stat_progress WHERE stat_id = ?; \ No newline at end of file diff --git a/internal/database/mysql/queries/project.sql b/internal/database/mysql/queries/project.sql index 7f71917..b7f35ed 100644 --- a/internal/database/mysql/queries/project.sql +++ b/internal/database/mysql/queries/project.sql @@ -45,4 +45,19 @@ WHERE id = ?; -- name: DeleteProjectRequirement :exec DELETE FROM project_requirement WHERE id = ?; +-- name: DeleteAllProjectRequirements :exec +DELETE FROM project_requirement WHERE project_id = ?; + +-- name: ReplaceProjectRequirementStat :exec +REPLACE INTO project_requirement_stat (project_requirement_id, stat_id, required) +VALUES (?, ?, ?); + +-- name: DeleteProjectRequirementStat :exec +DELETE FROM project_requirement_stat WHERE project_requirement_id = ? AND stat_id = ?; + +-- name: DeleteAllProjectRequirementStats :exec +DELETE FROM project_requirement_stat WHERE project_requirement_id = ?; + +-- name: DeleteAllProjectRequirementStatsByStat :exec +DELETE FROM project_requirement_stat WHERE stat_id = ?; diff --git a/internal/database/mysql/queries/sprint.sql b/internal/database/mysql/queries/sprint.sql new file mode 100644 index 0000000..e69de29 diff --git a/internal/database/mysql/queries/stats.sql b/internal/database/mysql/queries/stats.sql index 2e80c2a..cff1342 100644 --- a/internal/database/mysql/queries/stats.sql +++ b/internal/database/mysql/queries/stats.sql @@ -1,3 +1,22 @@ -- name: ListStats :many SELECT id, name, description, weight, allowed_amounts FROM stat -WHERE scope_id = ?; \ No newline at end of file +WHERE scope_id = ?; + +-- name: GetStat :one +SELECT id, name, description, weight, allowed_amounts FROM stat +WHERE scope_id = ? AND id = ?; + +-- name: InsertStat :execresult +INSERT INTO stat (scope_id, name, description, weight, allowed_amounts) +VALUES (?, ?, ?, ?, ?); + +-- name: UpdateStat :exec +UPDATE stat SET + name = ?, + description = ?, + weight = ?, + allowed_amounts = ? +WHERE id = ? AND scope_id = ?; + +-- name: DeleteStat :exec +DELETE FROM stat WHERE id = ? AND scope_id = ?; \ No newline at end of file diff --git a/internal/database/mysql/stats.go b/internal/database/mysql/stats.go new file mode 100644 index 0000000..39f3e78 --- /dev/null +++ b/internal/database/mysql/stats.go @@ -0,0 +1,144 @@ +package mysql + +import ( + "context" + "database/sql" + "encoding/json" + "git.aiterp.net/stufflog3/stufflog3-api/internal/database/mysql/mysqlcore" + "git.aiterp.net/stufflog3/stufflog3-api/internal/models" + "git.aiterp.net/stufflog3/stufflog3-api/internal/slerrors" + "git.aiterp.net/stufflog3/stufflog3-api/internal/sqltypes" +) + +type statsRepository struct { + db *sql.DB + q *mysqlcore.Queries + scopeID int +} + +func (r *statsRepository) Find(ctx context.Context, id int) (*models.Stat, error) { + row, err := r.q.GetStat(ctx, mysqlcore.GetStatParams{ScopeID: r.scopeID, ID: id}) + if err != nil { + if err == sql.ErrNoRows { + return nil, slerrors.NotFound("Stat") + } + + return nil, err + } + + return r.rowToStat(row), nil +} + +func (r *statsRepository) List(ctx context.Context) ([]models.Stat, error) { + rows, err := r.q.ListStats(ctx, r.scopeID) + if err != nil && err != sql.ErrNoRows { + return nil, err + } + + stats := make([]models.Stat, 0, len(rows)) + for _, row := range rows { + stats = append(stats, *r.rowToStat(mysqlcore.GetStatRow(row))) + } + + return stats, nil +} + +func (r *statsRepository) Create(ctx context.Context, stat models.Stat) (*models.Stat, error) { + allowedAmounts := sqltypes.NullRawMessage{} + if stat.AllowedAmounts != nil && len(stat.AllowedAmounts) > 0 { + allowedAmounts.Valid = true + allowedAmounts.RawMessage, _ = json.Marshal(stat.AllowedAmounts) + } + + res, err := r.q.InsertStat(ctx, mysqlcore.InsertStatParams{ + ScopeID: r.scopeID, + Name: stat.Name, + Description: stat.Description, + Weight: stat.Weight, + AllowedAmounts: allowedAmounts, + }) + if err != nil { + return nil, err + } + + id, err := res.LastInsertId() + if err != nil { + return nil, err + } + + return r.Find(ctx, int(id)) +} + +func (r *statsRepository) Update(ctx context.Context, stat models.Stat, update models.StatUpdate) (*models.Stat, error) { + stat.Update(update) + allowedAmounts := sqltypes.NullRawMessage{} + if stat.AllowedAmounts != nil && len(stat.AllowedAmounts) > 0 { + allowedAmounts.Valid = true + allowedAmounts.RawMessage, _ = json.Marshal(stat.AllowedAmounts) + } + + err := r.q.UpdateStat(ctx, mysqlcore.UpdateStatParams{ + Name: stat.Name, + Description: stat.Description, + Weight: stat.Weight, + AllowedAmounts: allowedAmounts, + ID: stat.ID, + ScopeID: r.scopeID, + }) + if err != nil { + return nil, err + } + + return &stat, nil +} + +func (r *statsRepository) Delete(ctx context.Context, stat models.Stat) error { + tx, err := r.db.BeginTx(ctx, nil) + if err != nil { + return err + } + defer tx.Rollback() + q := r.q.WithTx(tx) + + err = q.DeleteStat(ctx, mysqlcore.DeleteStatParams{ScopeID: r.scopeID, ID: stat.ID}) + if err == sql.ErrNoRows { + return slerrors.NotFound("Stat") + } + if err != nil { + return err + } + + err = q.CLearItemStatProgressByStat(ctx, stat.ID) + if err != nil { + return err + } + err = q.DeleteAllProjectRequirementStatsByStat(ctx, stat.ID) + if err != nil { + return err + } + + // TODO: delete from Sprints + + return tx.Commit() +} + +func (r *statsRepository) rowToStat(row mysqlcore.GetStatRow) *models.Stat { + stat := models.Stat{ + StatEntry: models.StatEntry{ + ID: row.ID, + Name: row.Name, + Weight: row.Weight, + }, + Description: row.Description, + AllowedAmounts: nil, + } + if row.AllowedAmounts.Valid { + stat.AllowedAmounts = make(map[string]int) + _ = json.Unmarshal(row.AllowedAmounts.RawMessage, &stat) + if len(stat.AllowedAmounts) == 0 { + stat.AllowedAmounts = nil + } + } + + return &stat +} diff --git a/internal/models/project.go b/internal/models/project.go index e057439..7964250 100644 --- a/internal/models/project.go +++ b/internal/models/project.go @@ -64,3 +64,35 @@ type Project struct { Description string `json:"description"` Requirements []ProjectRequirement `json:"requirements"` } + +func (project *Project) Update(update ProjectUpdate) { + if update.OwnerID != nil { + project.OwnerID = *update.OwnerID + } + if update.Name != nil { + project.Name = *update.Name + } + if update.Description != nil { + project.Description = *update.Description + } + if update.Status != nil { + project.Status = *update.Status + } +} + +func (project *Project) Requirement(id int) *ProjectRequirement { + for i, requirement := range project.Requirements { + if requirement.ID == id { + return &project.Requirements[i] + } + } + + return nil +} + +type ProjectUpdate struct { + Name *string `json:"name,omitempty"` + OwnerID *string `json:"ownerId,omitempty"` + Status *Status `json:"status,omitempty"` + Description *string `json:"description,omitempty"` +} diff --git a/internal/models/stat.go b/internal/models/stat.go index 2cecee7..3e8501f 100644 --- a/internal/models/stat.go +++ b/internal/models/stat.go @@ -20,6 +20,38 @@ type Stat struct { AllowedAmounts map[string]int `json:"allowedAmounts"` } +func (stat *Stat) Update(update StatUpdate) { + if update.Name != nil { + stat.Name = *update.Name + } + if update.Description != nil { + stat.Description = *update.Description + } + if update.Weight != nil { + stat.Weight = *update.Weight + } + for key, value := range update.AllowedAmounts { + if stat.AllowedAmounts == nil { + stat.AllowedAmounts = make(map[string]int, len(update.AllowedAmounts)) + } + if value <= 0 { + delete(stat.AllowedAmounts, key) + } else { + stat.AllowedAmounts[key] = value + } + } + if len(stat.AllowedAmounts) == 0 { + stat.AllowedAmounts = nil + } +} + +type StatUpdate struct { + Name *string `json:"name"` + Description *string `json:"description"` + Weight *float64 `json:"weight"` + AllowedAmounts map[string]int `json:"allowedAmounts"` +} + func (stat *Stat) AllowsAmount(amount int) bool { if stat == nil { return false @@ -35,4 +67,4 @@ func (stat *Stat) AllowsAmount(amount int) bool { } return false -} \ No newline at end of file +} diff --git a/internal/sqltypes/nullrawmessage.go b/internal/sqltypes/nullrawmessage.go index 6975bba..6fb957f 100644 --- a/internal/sqltypes/nullrawmessage.go +++ b/internal/sqltypes/nullrawmessage.go @@ -32,5 +32,5 @@ func (n NullRawMessage) Value() (driver.Value, error) { return nil, nil } - return n.RawMessage, nil + return []byte(n.RawMessage), nil }