Gisle Aune
3 years ago
19 changed files with 1058 additions and 120 deletions
-
5api/common.go
-
138api/projects.go
-
98api/stats.go
-
1cmd/stufflog3-server.go
-
12internal/database/database.go
-
6internal/database/mysql/database.go
-
110internal/database/mysql/mysqlcore/db.go
-
21internal/database/mysql/mysqlcore/item.sql.go
-
57internal/database/mysql/mysqlcore/project.sql.go
-
99internal/database/mysql/mysqlcore/stats.sql.go
-
190internal/database/mysql/project.go
-
9internal/database/mysql/queries/item.sql
-
15internal/database/mysql/queries/project.sql
-
0internal/database/mysql/queries/sprint.sql
-
19internal/database/mysql/queries/stats.sql
-
144internal/database/mysql/stats.go
-
32internal/models/project.go
-
32internal/models/stat.go
-
2internal/sqltypes/nullrawmessage.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 |
||||
|
})) |
||||
|
} |
@ -1,3 +1,22 @@ |
|||||
-- name: ListStats :many |
-- name: ListStats :many |
||||
SELECT id, name, description, weight, allowed_amounts FROM stat |
SELECT id, name, description, weight, allowed_amounts FROM stat |
||||
WHERE scope_id = ?; |
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 = ?; |
@ -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 |
||||
|
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue