Browse Source

added more endpoints

master
Gisle Aune 2 years ago
parent
commit
35d60da7c5
  1. 2
      cmd/stufflog3-local/main.go
  2. 9
      entities/scope.go
  3. 23
      internal/genutils/json.go
  4. 33
      internal/genutils/set.go
  5. 10
      models/errors.go
  6. 8
      models/status.go
  7. 61
      ports/httpapi/projects.go
  8. 18
      ports/httpapi/scopes.go
  9. 36
      ports/httpapi/stats.go
  10. 2
      ports/mysql/projects.go
  11. 12
      ports/mysql/scopes.go
  12. 85
      ports/mysql/stats.go
  13. 19
      usecases/items/result.go
  14. 14
      usecases/items/service.go
  15. 2
      usecases/projects/repository.go
  16. 20
      usecases/projects/result.go
  17. 64
      usecases/projects/service.go
  18. 9
      usecases/scopes/context.go
  19. 4
      usecases/scopes/repository.go
  20. 40
      usecases/scopes/result.go
  21. 69
      usecases/scopes/service.go
  22. 7
      usecases/sprints/service.go
  23. 2
      usecases/stats/repository.go
  24. 63
      usecases/stats/service.go

2
cmd/stufflog3-local/main.go

@ -62,6 +62,7 @@ func main() {
Repository: db.Items(),
}
projectsService := &projects.Service{
Auth: authService,
Scopes: scopesService,
Stats: statsService,
Items: itemsService,
@ -90,6 +91,7 @@ func main() {
httpapi.Projects(apiV1ScopesSub.Group("/projects"), projectsService)
httpapi.Items(apiV1ScopesSub.Group("/items"), itemsService)
httpapi.Sprints(apiV1ScopesSub.Group("/sprints"), sprintsService)
httpapi.Stats(apiV1ScopesSub.Group("/stats"), statsService)
exitSignal := make(chan os.Signal)
signal.Notify(exitSignal, os.Interrupt, os.Kill, syscall.SIGTERM)

9
entities/scope.go

@ -1,6 +1,7 @@
package entities
import (
"fmt"
"git.aiterp.net/stufflog3/stufflog3/models"
)
@ -11,6 +12,14 @@ type Scope struct {
CustomLabels map[string]string `json:"customLabels"`
}
func (scope *Scope) StatusName(status models.Status) string {
if cl, ok := scope.CustomLabels[fmt.Sprintf("status%d", status)]; ok {
return cl
}
return status.Name()
}
func (scope *Scope) ApplyUpdate(update models.ScopeUpdate) {
if update.Name != nil {
scope.Name = *update.Name

23
internal/genutils/json.go

@ -0,0 +1,23 @@
package genutils
import (
"bytes"
"encoding/json"
)
func ParseJSON[T any](b []byte) (T, error) {
var res T
err := json.Unmarshal(b, &res)
return res, err
}
func ParseJSONArray[T any](b []byte) []T {
res := make([]T, 0, 16)
if b != nil && !bytes.Equal(b, []byte{'n', 'u', 'l', 'l'}) {
_ = json.Unmarshal(b, &res)
}
return res
}

33
internal/genutils/set.go

@ -0,0 +1,33 @@
package genutils
type Set[T comparable] struct {
m map[T]bool
a []T
}
func (set *Set[T]) Add(values ...T) {
if set.m == nil {
set.m = make(map[T]bool, len(values)*4)
}
for _, value := range values {
if set.m[value] {
continue
}
set.m[value] = true
set.a = append(set.a, value)
}
}
func (set *Set[T]) Has(v T) bool {
return set.m[v]
}
func (set *Set[T]) Len() int {
return len(set.a)
}
func (set *Set[T]) Values() []T {
return set.a[:len(set.a):len(set.a)]
}

10
models/errors.go

@ -33,11 +33,11 @@ func (e ForbiddenError) HttpStatus() (int, string, interface{}) {
}
type BadInputError struct {
Object string
Field string
Problem string
Min interface{}
Max interface{}
Object string `json:"object"`
Field string `json:"field,omitempty"`
Problem string `json:"problem"`
Min interface{} `json:"min,omitempty"`
Max interface{} `json:"max,omitempty"`
}
func (e BadInputError) Error() string {

8
models/status.go

@ -6,6 +6,14 @@ func (s Status) Valid() bool {
return s >= 0 && s < maxStatus
}
func (s Status) Name() string {
if s < 0 || s >= maxStatus {
return "(invalid status)"
}
return StatusLabels[s]
}
const (
Blocked Status = iota
Available

61
ports/httpapi/projects.go

@ -1,13 +1,16 @@
package httpapi
import (
"git.aiterp.net/stufflog3/stufflog3/entities"
"git.aiterp.net/stufflog3/stufflog3/internal/genutils"
"git.aiterp.net/stufflog3/stufflog3/models"
"git.aiterp.net/stufflog3/stufflog3/usecases/projects"
"github.com/gin-gonic/gin"
)
func Projects(g *gin.RouterGroup, projects *projects.Service) {
func Projects(g *gin.RouterGroup, service *projects.Service) {
g.GET("", handler("projects", func(c *gin.Context) (interface{}, error) {
return projects.List(c.Request.Context())
return service.List(c.Request.Context())
}))
g.GET("/:project_id", handler("project", func(c *gin.Context) (interface{}, error) {
@ -16,6 +19,58 @@ func Projects(g *gin.RouterGroup, projects *projects.Service) {
return nil, err
}
return projects.Find(c.Request.Context(), id)
return service.Find(c.Request.Context(), id)
}))
g.POST("", handler("project", func(c *gin.Context) (interface{}, error) {
input := entities.Project{}
err := c.BindJSON(&input)
if err != nil {
return nil, models.BadInputError{
Object: "Project",
Problem: "Invalid JSON: " + err.Error(),
}
}
return service.Create(c.Request.Context(), input)
}))
g.GET("/:project_id/requirements", handler("requirements", func(c *gin.Context) (interface{}, error) {
id, err := reqInt(c, "project_id")
if err != nil {
return nil, err
}
project, err := service.Find(c.Request.Context(), id)
if err != nil {
return nil, err
}
return project.Requirements, nil
}))
g.GET("/:project_id/requirements/:requirement_id", handler("requirements", func(c *gin.Context) (interface{}, error) {
id, err := reqInt(c, "project_id")
if err != nil {
return nil, err
}
reqID, err := reqInt(c, "requirement_id")
if err != nil {
return nil, err
}
project, err := service.Find(c.Request.Context(), id)
if err != nil {
return nil, err
}
req := genutils.Find(project.Requirements, func(r projects.RequirementResult) bool {
return r.ID == reqID
})
if err == nil {
return nil, models.NotFoundError("Requirement")
}
return req, nil
}))
}

18
ports/httpapi/scopes.go

@ -66,6 +66,11 @@ func ScopeMiddleware(scopes *scopes.Service, auth *auth.Service) gin.HandlerFunc
}
func Scopes(g *gin.RouterGroup, scopes *scopes.Service) {
type scopeInput struct {
entities.Scope
OwnerName string `json:"ownerName"`
}
g.GET("", handler("scopes", func(c *gin.Context) (interface{}, error) {
return scopes.List(c.Request.Context())
}))
@ -78,4 +83,17 @@ func Scopes(g *gin.RouterGroup, scopes *scopes.Service) {
return scopes.Find(c.Request.Context(), id)
}))
g.POST("", handler("scope", func(c *gin.Context) (interface{}, error) {
input := scopeInput{}
err := c.BindJSON(&input)
if err != nil {
return nil, models.BadInputError{
Object: "Scope",
Problem: "Invalid JSON: " + err.Error(),
}
}
return scopes.Create(c.Request.Context(), input.Scope, input.OwnerName)
}))
}

36
ports/httpapi/stats.go

@ -0,0 +1,36 @@
package httpapi
import (
"git.aiterp.net/stufflog3/stufflog3/entities"
"git.aiterp.net/stufflog3/stufflog3/models"
"git.aiterp.net/stufflog3/stufflog3/usecases/stats"
"github.com/gin-gonic/gin"
)
func Stats(g *gin.RouterGroup, stats *stats.Service) {
g.GET("", handler("stats", func(c *gin.Context) (interface{}, error) {
return stats.ListScoped(c.Request.Context())
}))
g.GET("/:stat_id", handler("stat", func(c *gin.Context) (interface{}, error) {
id, err := reqInt(c, "stat_id")
if err != nil {
return nil, err
}
return stats.Find(c.Request.Context(), id)
}))
g.POST("", handler("stat", func(c *gin.Context) (interface{}, error) {
input := entities.Stat{}
err := c.BindJSON(&input)
if err != nil {
return nil, models.BadInputError{
Object: "Scope",
Problem: "Invalid JSON: " + err.Error(),
}
}
return stats.Create(c.Request.Context(), input)
}))
}

2
ports/mysql/projects.go

@ -61,7 +61,7 @@ func (r *projectRepository) List(ctx context.Context, scopeID int) ([]entities.P
return res, nil
}
func (r *projectRepository) Create(ctx context.Context, project entities.Project) (*entities.Project, error) {
func (r *projectRepository) Insert(ctx context.Context, project entities.Project) (*entities.Project, error) {
res, err := r.q.InsertProject(ctx, mysqlcore.InsertProjectParams{
ScopeID: project.ScopeID,
OwnerID: project.OwnerID,

12
ports/mysql/scopes.go

@ -93,7 +93,7 @@ func (r *scopeRepository) ListUser(ctx context.Context, userID string) ([]entiti
return res, nil
}
func (r *scopeRepository) Create(ctx context.Context, scope entities.Scope, ownerID string) (*entities.Scope, error) {
func (r *scopeRepository) Create(ctx context.Context, scope entities.Scope, owner entities.ScopeMember) (*entities.Scope, error) {
tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
return nil, err
@ -119,6 +119,16 @@ func (r *scopeRepository) Create(ctx context.Context, scope entities.Scope, owne
return nil, err
}
err = q.ReplaceScopeMember(ctx, mysqlcore.ReplaceScopeMemberParams{
ScopeID: int(id),
UserID: owner.UserID,
Name: owner.Name,
Owner: true,
})
if err != nil {
return nil, err
}
err = tx.Commit()
if err != nil {
return nil, err

85
ports/mysql/stats.go

@ -5,8 +5,10 @@ import (
"database/sql"
"encoding/json"
"git.aiterp.net/stufflog3/stufflog3/entities"
"git.aiterp.net/stufflog3/stufflog3/internal/genutils"
"git.aiterp.net/stufflog3/stufflog3/models"
"git.aiterp.net/stufflog3/stufflog3/ports/mysql/mysqlcore"
"github.com/Masterminds/squirrel"
)
type statsRepository struct {
@ -43,38 +45,87 @@ func (r *statsRepository) Find(ctx context.Context, scopeID, statID int) (*entit
}, nil
}
func (r *statsRepository) List(ctx context.Context, scopeID int) ([]entities.Stat, error) {
rows, err := r.q.ListStats(ctx, scopeID)
if err != nil {
if err == sql.ErrNoRows {
return nil, models.NotFoundError("Stat")
func (r *statsRepository) List(ctx context.Context, scopeIDs ...int) ([]entities.Stat, error) {
if len(scopeIDs) == 0 {
return []entities.Stat{}, nil
} else if len(scopeIDs) == 1 {
rows, err := r.q.ListStats(ctx, scopeIDs[0])
if err != nil {
if err == sql.ErrNoRows {
return nil, models.NotFoundError("Stat")
}
return nil, err
}
res := make([]entities.Stat, 0, len(rows))
for _, row := range rows {
var allowedAmounts []models.StatAllowedAmount
if row.AllowedAmounts.Valid {
allowedAmounts = make([]models.StatAllowedAmount, 0, 8)
_ = json.Unmarshal(row.AllowedAmounts.RawMessage, &allowedAmounts)
if len(allowedAmounts) == 0 {
allowedAmounts = nil
}
}
res = append(res, entities.Stat{
ID: row.ID,
ScopeID: row.ScopeID,
Name: row.Name,
Weight: row.Weight,
Description: row.Description,
AllowedAmounts: allowedAmounts,
})
}
return res, nil
}
rows, err := squirrel.
Select("id, scope_id, name, description, weight, allowed_amounts").
From("stat").
Where(squirrel.Eq{"scope_id": scopeIDs}).
RunWith(r.db).
Query()
if err == sql.ErrNoRows {
return []entities.Stat{}, nil
} else if err != nil {
return nil, err
}
res := make([]entities.Stat, 0, len(rows))
for _, row := range rows {
var allowedAmounts []models.StatAllowedAmount
if row.AllowedAmounts.Valid {
allowedAmounts = make([]models.StatAllowedAmount, 0, 8)
_ = json.Unmarshal(row.AllowedAmounts.RawMessage, &allowedAmounts)
if len(allowedAmounts) == 0 {
allowedAmounts = nil
}
stats := make([]entities.Stat, 0, 16)
for rows.Next() {
var row mysqlcore.Stat
if err := rows.Scan(
&row.ID,
&row.ScopeID,
&row.Name,
&row.Description,
&row.Weight,
&row.AllowedAmounts,
); err != nil {
return nil, err
}
res = append(res, entities.Stat{
stats = append(stats, entities.Stat{
ID: row.ID,
ScopeID: row.ScopeID,
Name: row.Name,
Weight: row.Weight,
Description: row.Description,
AllowedAmounts: allowedAmounts,
AllowedAmounts: genutils.ParseJSONArray[models.StatAllowedAmount](row.AllowedAmounts.RawMessage),
})
}
return res, nil
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return stats, nil
}
func (r *statsRepository) Insert(ctx context.Context, stat entities.Stat) (*entities.Stat, error) {

19
usecases/items/result.go

@ -1,6 +1,9 @@
package items
import "git.aiterp.net/stufflog3/stufflog3/entities"
import (
"git.aiterp.net/stufflog3/stufflog3/entities"
"git.aiterp.net/stufflog3/stufflog3/usecases/scopes"
)
type Result struct {
entities.Item
@ -26,25 +29,25 @@ type ResultStat struct {
Required int `json:"required"`
}
func generateResult(item entities.Item, progresses []entities.ItemProgress, stats []entities.Stat) Result {
func generateResult(item entities.Item, scope scopes.Result, progresses []entities.ItemProgress) Result {
res := Result{
Item: item,
Stats: make([]ResultStat, 0, 8),
}
for _, prog := range progresses {
if prog.ItemID != item.ID {
for _, progress := range progresses {
if progress.ItemID != item.ID {
continue
}
for _, stat := range stats {
if stat.ID == prog.StatID {
for _, stat := range scope.Stats {
if stat.ID == progress.StatID {
res.Stats = append(res.Stats, ResultStat{
ID: stat.ID,
Name: stat.Name,
Weight: stat.Weight,
Acquired: prog.Acquired,
Required: prog.Required,
Acquired: progress.Acquired,
Required: progress.Required,
})
break

14
usecases/items/service.go

@ -29,12 +29,7 @@ func (s *Service) Find(ctx context.Context, id int) (*Result, error) {
return nil, err
}
scopeStats, err := sc.Stats(ctx)
if err != nil {
return nil, err
}
result := generateResult(*item, progresses, scopeStats)
result := generateResult(*item, sc.Scope, progresses)
return &result, nil
}
@ -77,14 +72,9 @@ func (s *Service) ListScoped(ctx context.Context, filter models.ItemFilter) ([]R
return nil, err
}
scopeStats, err := sc.Stats(ctx)
if err != nil {
return nil, err
}
res := make([]Result, 0, len(items))
for _, item := range items {
res = append(res, generateResult(item, progresses, scopeStats))
res = append(res, generateResult(item, sc.Scope, progresses))
}
return res, nil

2
usecases/projects/repository.go

@ -9,7 +9,7 @@ import (
type Repository interface {
Find(ctx context.Context, scopeID, projectID int) (*entities.Project, error)
List(ctx context.Context, scopeID int) ([]entities.Project, error)
Create(ctx context.Context, project entities.Project) (*entities.Project, error)
Insert(ctx context.Context, project entities.Project) (*entities.Project, error)
Update(ctx context.Context, project entities.Project, update models.ProjectUpdate) error
Delete(ctx context.Context, project entities.Project) error
FetchRequirements(ctx context.Context, scopeID int, requirementIDs ...int) ([]entities.Requirement, []entities.RequirementStat, error)

20
usecases/projects/result.go

@ -4,6 +4,7 @@ import (
"git.aiterp.net/stufflog3/stufflog3/entities"
"git.aiterp.net/stufflog3/stufflog3/models"
"git.aiterp.net/stufflog3/stufflog3/usecases/items"
"git.aiterp.net/stufflog3/stufflog3/usecases/scopes"
"time"
)
@ -13,6 +14,7 @@ type Entry struct {
CreatedTime time.Time `json:"createdTime"`
Name string `json:"name"`
Status models.Status `json:"status"`
StatusName string `json:"statusName"`
}
func generateEntry(project entities.Project) Entry {
@ -27,6 +29,8 @@ func generateEntry(project entities.Project) Entry {
type Result struct {
entities.Project
OwnerName string `json:"ownerName"`
StatusName string `json:"statusName"`
Requirements []RequirementResult `json:"requirements"`
}
@ -35,6 +39,7 @@ type RequirementResult struct {
Name string `json:"name"`
Description string `json:"description"`
Status models.Status `json:"status"`
StatusName string `json:"statusName"`
Stats []RequirementResultStat `json:"stats"`
Items []items.Result `json:"items"`
}
@ -60,13 +65,15 @@ type RequirementResultStat struct {
func generateResult(
project entities.Project,
scope scopes.Result,
requirement []entities.Requirement,
requirementStats []entities.RequirementStat,
projectItems []items.Result,
stats []entities.Stat,
) Result {
) *Result {
res := Result{
Project: project,
OwnerName: scope.MemberName(project.OwnerID),
StatusName: scope.StatusName(project.Status),
Requirements: make([]RequirementResult, 0, 8),
}
@ -75,20 +82,21 @@ func generateResult(
continue
}
resReq := generateRequirementResult(req, requirementStats, stats, projectItems)
resReq := generateRequirementResult(req, scope, requirementStats, projectItems)
res.Requirements = append(res.Requirements, resReq)
}
return res
return &res
}
func generateRequirementResult(req entities.Requirement, requirementStats []entities.RequirementStat, stats []entities.Stat, projectItems []items.Result) RequirementResult {
func generateRequirementResult(req entities.Requirement, scope scopes.Result, requirementStats []entities.RequirementStat, projectItems []items.Result) RequirementResult {
resReq := RequirementResult{
ID: req.ID,
Name: req.Name,
Description: req.Description,
Status: req.Status,
StatusName: scope.StatusName(req.Status),
Stats: make([]RequirementResultStat, 0, 8),
Items: make([]items.Result, 0, 8),
}
@ -102,7 +110,7 @@ func generateRequirementResult(req entities.Requirement, requirementStats []enti
ID: reqStat.StatID,
Required: reqStat.Required,
}
for _, stat := range stats {
for _, stat := range scope.Stats {
if stat.ID == resStat.ID {
resStat.Name = stat.Name
resStat.Weight = stat.Weight

64
usecases/projects/service.go

@ -2,16 +2,21 @@ package projects
import (
"context"
"git.aiterp.net/stufflog3/stufflog3/entities"
"git.aiterp.net/stufflog3/stufflog3/models"
"git.aiterp.net/stufflog3/stufflog3/usecases/auth"
"git.aiterp.net/stufflog3/stufflog3/usecases/items"
"git.aiterp.net/stufflog3/stufflog3/usecases/scopes"
"git.aiterp.net/stufflog3/stufflog3/usecases/stats"
"strings"
"time"
)
type Service struct {
Scopes *scopes.Service
Stats *stats.Service
Items *items.Service
Auth *auth.Service
Repository Repository
}
@ -36,20 +41,13 @@ func (s *Service) Find(ctx context.Context, id int) (*Result, error) {
return nil, err
}
stats, err := sc.Stats(ctx)
if err != nil {
return nil, err
}
result := generateResult(
return generateResult(
*project,
sc.Scope,
requirements,
requirementStats,
items,
stats,
)
return &result, nil
), nil
}
func (s *Service) List(ctx context.Context) ([]Entry, error) {
@ -81,20 +79,54 @@ func (s *Service) FetchRequirements(ctx context.Context, ids ...int) ([]Requirem
return nil, err
}
stats, err := sc.Stats(ctx)
if err != nil {
return nil, err
}
results := make([]RequirementResult, 0, len(requirements))
for _, req := range requirements {
results = append(results, generateRequirementResult(
req,
sc.Scope,
requirementStats,
stats,
items,
))
}
return results, nil
}
func (s *Service) Create(ctx context.Context, project entities.Project) (*Result, error) {
project.Name = strings.Trim(project.Name, "  \t\r\n")
if project.Name == "" {
return nil, models.BadInputError{
Object: "ProjectInput",
Field: "name",
Problem: "Empty name provided",
}
}
if !project.Status.Valid() {
return nil, models.BadInputError{
Object: "ProjectInput",
Field: "status",
Problem: "Non-existent status ID",
}
}
// This is stufflog 3, so allow importing and scripts to mess with the created time.
if project.CreatedTime.Year() < 2000 {
project.CreatedTime = time.Now()
}
project.OwnerID = s.Auth.GetUser(ctx).ID
project.ScopeID = s.Scopes.Context(ctx).ID
newProject, err := s.Repository.Insert(ctx, project)
if err != nil {
return nil, err
}
return generateResult(
*newProject,
s.Scopes.Context(ctx).Scope,
[]entities.Requirement{},
[]entities.RequirementStat{},
[]items.Result{},
), nil
}

9
usecases/scopes/context.go

@ -23,15 +23,6 @@ type Context struct {
scopesError error
}
// Stats lazy-loads the stats in the context of the first caller.
func (c *Context) Stats(ctx context.Context) ([]entities.Stat, error) {
c.statsOnce.Do(func() {
c.stats, c.statsError = c.statsRepo.List(ctx, c.ID)
})
return c.stats, c.statsError
}
// Scopes lazy-loads the scopes in the context of the first caller.
func (c *Context) Scopes(ctx context.Context) ([]entities.Scope, error) {
c.statsOnce.Do(func() {

4
usecases/scopes/repository.go

@ -10,7 +10,7 @@ type Repository interface {
Find(ctx context.Context, id int) (*entities.Scope, error)
List(ctx context.Context) ([]entities.Scope, error)
ListUser(ctx context.Context, userID string) ([]entities.Scope, error)
Create(ctx context.Context, scope entities.Scope, ownerID string) (*entities.Scope, error)
Create(ctx context.Context, scope entities.Scope, owner entities.ScopeMember) (*entities.Scope, error)
Update(ctx context.Context, scope entities.Scope, update models.ScopeUpdate) error
Delete(ctx context.Context, scope entities.Scope) error
@ -20,5 +20,5 @@ type Repository interface {
}
type StatsLister interface {
List(ctx context.Context, scopeID int) ([]entities.Stat, error)
List(ctx context.Context, scopeIDs ...int) ([]entities.Stat, error)
}

40
usecases/scopes/result.go

@ -1,11 +1,23 @@
package scopes
import "git.aiterp.net/stufflog3/stufflog3/entities"
import (
"git.aiterp.net/stufflog3/stufflog3/entities"
"git.aiterp.net/stufflog3/stufflog3/models"
)
type Result struct {
entities.Scope
Members map[string]ResultMember `json:"members"`
Stats []ResultStat `json:"stats"`
}
func (r *Result) MemberName(id string) string {
if member, ok := r.Members[id]; ok {
return member.Name
}
return ""
}
type ResultMember struct {
@ -14,7 +26,15 @@ type ResultMember struct {
Owner bool `json:"owner"`
}
func generateResult(scope entities.Scope, members []entities.ScopeMember) *Result {
type ResultStat struct {
ID int `json:"id"`
Name string `json:"name"`
Weight float64 `json:"weight"`
Description string `json:"description"`
AllowedAmounts []models.StatAllowedAmount `json:"allowedAmounts,omitempty"`
}
func generateResult(scope entities.Scope, members []entities.ScopeMember, stats []entities.Stat) *Result {
res := Result{Scope: scope, Members: make(map[string]ResultMember, len(members))}
for _, member := range members {
if member.ScopeID != scope.ID {
@ -27,6 +47,22 @@ func generateResult(scope entities.Scope, members []entities.ScopeMember) *Resul
Owner: member.Owner,
}
}
if stats != nil {
res.Stats = make([]ResultStat, 0, len(stats))
for _, stat := range stats {
if stat.ScopeID != scope.ID {
continue
}
res.Stats = append(res.Stats, ResultStat{
ID: stat.ID,
Name: stat.Name,
Weight: stat.Weight,
Description: stat.Description,
AllowedAmounts: stat.AllowedAmounts,
})
}
}
return &res
}

69
usecases/scopes/service.go

@ -6,6 +6,7 @@ import (
"git.aiterp.net/stufflog3/stufflog3/internal/genutils"
"git.aiterp.net/stufflog3/stufflog3/models"
"git.aiterp.net/stufflog3/stufflog3/usecases/auth"
"strings"
)
type Service struct {
@ -51,6 +52,10 @@ func (s *Service) Find(ctx context.Context, id int) (*Result, error) {
if err != nil {
return nil, err
}
stats, err := s.StatsLister.List(ctx, scope.ID)
if err != nil {
return nil, err
}
found := false
for _, member := range members {
@ -63,7 +68,7 @@ func (s *Service) Find(ctx context.Context, id int) (*Result, error) {
return nil, models.NotFoundError("Scope")
}
return generateResult(*scope, members), nil
return generateResult(*scope, members, stats), nil
}
// List lists a scope and their members, and returns it if the logged-in user is part of this list.
@ -86,8 +91,68 @@ func (s *Service) List(ctx context.Context) ([]Result, error) {
if err != nil {
return nil, err
}
stats, err := s.StatsLister.List(ctx, ids...)
if err != nil {
return nil, err
}
return genutils.Map(scopes, func(scope entities.Scope) Result {
return *generateResult(scope, members)
return *generateResult(scope, members, stats)
}), nil
}
func (s *Service) Create(ctx context.Context, scope entities.Scope, ownerName string) (*Result, error) {
user := s.Auth.GetUser(ctx)
if user == nil {
return nil, models.PermissionDeniedError{}
}
scope.Name = strings.Trim(scope.Name, "  \t\r\n")
if scope.Name == "" {
return nil, models.BadInputError{
Object: "ScopeInput",
Field: "name",
Problem: "Empty name provided",
}
}
scope.Abbreviation = strings.ToUpper(strings.Trim(scope.Abbreviation, "  \t\r\n"))
if scope.Abbreviation == "" {
return nil, models.BadInputError{
Object: "ScopeInput",
Field: "abbreviation",
Problem: "Empty abbreviation provided",
}
}
if strings.ContainsAny(scope.Abbreviation, "\t  \n\r\t") {
return nil, models.BadInputError{
Object: "ScopeInput",
Field: "abbreviation",
Problem: "Invalid abbreviation provided",
}
}
ownerName = strings.Trim(ownerName, "  \t\r\n")
if ownerName == "" {
return nil, models.BadInputError{
Object: "ScopeInput",
Field: "ownerName",
Problem: "Empty owner name provided",
}
}
owner := entities.ScopeMember{
ScopeID: -1,
UserID: user.ID,
Name: ownerName,
Owner: true,
}
newScope, err := s.Repository.Create(ctx, scope, owner)
if err != nil {
return nil, err
}
owner.ScopeID = newScope.ID
return generateResult(*newScope, []entities.ScopeMember{owner}, []entities.Stat{}), nil
}

7
usecases/sprints/service.go

@ -81,10 +81,7 @@ func (s *Service) fill(ctx context.Context, sprint entities.Sprint, parts []enti
}
}
allStats, err := sc.Stats(ctx)
if err != nil {
return nil, err
}
allStats := sc.Scope.Stats
progressMap := make(map[int]int, len(allStats))
res.Progress = make([]ResultProgress, 0, len(allStats))
@ -150,7 +147,7 @@ func (s *Service) fill(ctx context.Context, sprint entities.Sprint, parts []enti
res.Requirements = includedRequirements
case models.SprintKindStats:
includedStats := make([]entities.Stat, 0, len(partIDs))
includedStats := make([]scopes.ResultStat, 0, len(partIDs))
for _, stat := range allStats {
for _, id := range partIDs {
if stat.ID == id {

2
usecases/stats/repository.go

@ -8,7 +8,7 @@ import (
type Repository interface {
Find(ctx context.Context, scopeID, statID int) (*entities.Stat, error)
List(ctx context.Context, scopeID int) ([]entities.Stat, error)
List(ctx context.Context, scopeIDs ...int) ([]entities.Stat, error)
Insert(ctx context.Context, stat entities.Stat) (*entities.Stat, error)
Update(ctx context.Context, stat entities.Stat, update models.StatUpdate) error
Delete(ctx context.Context, stat entities.Stat) error

63
usecases/stats/service.go

@ -3,7 +3,10 @@ package stats
import (
"context"
"git.aiterp.net/stufflog3/stufflog3/entities"
"git.aiterp.net/stufflog3/stufflog3/internal/genutils"
"git.aiterp.net/stufflog3/stufflog3/models"
"git.aiterp.net/stufflog3/stufflog3/usecases/scopes"
"strings"
)
type Service struct {
@ -12,6 +15,64 @@ type Service struct {
Repository Repository
}
func (s *Service) List(ctx context.Context) ([]entities.Stat, error) {
func (s *Service) ListScoped(ctx context.Context) ([]entities.Stat, error) {
return s.Repository.List(ctx, s.Scopes.Context(ctx).ID)
}
func (s *Service) Find(ctx context.Context, id int) (*entities.Stat, error) {
return s.Repository.Find(ctx, s.Scopes.Context(ctx).ID, id)
}
func (s *Service) Create(ctx context.Context, stat entities.Stat) (*entities.Stat, error) {
stat.Name = strings.Trim(stat.Name, "  \t\r\n")
if stat.Name == "" {
return nil, models.BadInputError{
Object: "StatInput",
Field: "name",
Problem: "Empty name provided",
}
}
if stat.Weight < 0 {
return nil, models.BadInputError{
Object: "StatInput",
Field: "Weight",
Problem: "Negative weight provided",
Min: 0.0,
}
}
aaValues := genutils.Set[int]{}
aaNames := genutils.Set[string]{}
for _, allowedAmount := range stat.AllowedAmounts {
if aaValues.Has(allowedAmount.Value) {
return nil, models.BadInputError{
Object: "StatInput",
Field: "allowedAmounts",
Problem: "Duplicate value detected",
}
}
if allowedAmount.Value < 0 {
if allowedAmount.Label == "" {
return nil, models.BadInputError{
Object: "StatInput",
Field: "allowedAmounts",
Problem: "Negative valuename for allowed amount label provided",
}
}
}
if allowedAmount.Label == "" {
return nil, models.BadInputError{
Object: "StatInput",
Field: "allowedAmounts",
Problem: "Empty name for allowed amount label provided",
}
}
aaValues.Add(allowedAmount.Value)
aaNames.Add(allowedAmount.Label)
}
stat.ScopeID = s.Scopes.Context(ctx).ID
return s.Repository.Insert(ctx, stat)
}
Loading…
Cancel
Save