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 |
|||
SELECT id, name, description, weight, allowed_amounts FROM stat |
|||
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