Browse Source
add item list API endpoint and add filtering to repository. I don't know why I tried to avoid squirrel.
master
add item list API endpoint and add filtering to repository. I don't know why I tried to avoid squirrel.
master
Gisle Aune
2 years ago
23 changed files with 602 additions and 126 deletions
-
16cmd/stufflog3-local/main.go
-
22go.mod
-
13go.sum
-
20models/date.go
-
10models/errors.go
-
15models/generics.go
-
11models/item.go
-
159ports/httpapi/items.go
-
24ports/httpapi/scopes.go
-
131ports/mysql/items.go
-
9scripts/goose-mysql/20220514164924_item_idx_requirement_id.sql
-
9scripts/goose-mysql/20220514165854_item_idx_created_time.sql
-
9scripts/goose-mysql/20220514165858_item_idx_acquired_time.sql
-
9scripts/goose-mysql/20220514165902_item_idx_scheduled_date.sql
-
7usecases/items/repository.go
-
46usecases/items/result.go
-
52usecases/items/service.go
-
82usecases/projects/result.go
-
16usecases/projects/service.go
-
42usecases/scopes/context.go
-
4usecases/scopes/repository.go
-
20usecases/scopes/service.go
-
2usecases/stats/service.go
@ -1,8 +1,28 @@ |
|||
module git.aiterp.net/stufflog3/stufflog3 |
|||
|
|||
go 1.13 |
|||
go 1.18 |
|||
|
|||
require ( |
|||
github.com/Masterminds/squirrel v1.5.2 |
|||
github.com/gin-gonic/gin v1.7.7 |
|||
github.com/go-sql-driver/mysql v1.6.0 |
|||
) |
|||
|
|||
require ( |
|||
github.com/gin-contrib/sse v0.1.0 // indirect |
|||
github.com/go-playground/locales v0.13.0 // indirect |
|||
github.com/go-playground/universal-translator v0.17.0 // indirect |
|||
github.com/go-playground/validator/v10 v10.4.1 // indirect |
|||
github.com/golang/protobuf v1.3.3 // indirect |
|||
github.com/json-iterator/go v1.1.9 // indirect |
|||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect |
|||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect |
|||
github.com/leodido/go-urn v1.2.0 // indirect |
|||
github.com/mattn/go-isatty v0.0.12 // indirect |
|||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect |
|||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 // indirect |
|||
github.com/ugorji/go/codec v1.1.7 // indirect |
|||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 // indirect |
|||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42 // indirect |
|||
gopkg.in/yaml.v2 v2.2.8 // indirect |
|||
) |
@ -0,0 +1,15 @@ |
|||
package models |
|||
|
|||
type betweenContract[T any] interface { |
|||
Before(other T) bool |
|||
IsZero() bool |
|||
} |
|||
|
|||
type Between[T betweenContract[T]] struct { |
|||
Min T `json:"min"` |
|||
Max T `json:"max"` |
|||
} |
|||
|
|||
func (b *Between[T]) Valid() bool { |
|||
return !b.Min.IsZero() && !b.Max.IsZero() && b.Min.Before(b.Max) |
|||
} |
@ -0,0 +1,159 @@ |
|||
package httpapi |
|||
|
|||
import ( |
|||
"fmt" |
|||
"git.aiterp.net/stufflog3/stufflog3/models" |
|||
"git.aiterp.net/stufflog3/stufflog3/usecases/items" |
|||
"github.com/gin-gonic/gin" |
|||
"strconv" |
|||
"strings" |
|||
"time" |
|||
) |
|||
|
|||
func Items(g *gin.RouterGroup, items *items.Service) { |
|||
g.GET("", handler("items", func(c *gin.Context) (interface{}, error) { |
|||
filter := models.ItemFilter{} |
|||
|
|||
if queryOwnerId := c.Query("ownerId"); queryOwnerId != "" { |
|||
filter.OwnerID = &queryOwnerId |
|||
} |
|||
|
|||
queryScheduledMin := c.Query("scheduledMin") |
|||
queryScheduledMax := c.Query("scheduledMax") |
|||
if queryScheduledMin != "" && queryScheduledMax != "" { |
|||
min, err := models.ParseDate(queryScheduledMin) |
|||
if err != nil { |
|||
return nil, models.BadInputError{ |
|||
Object: "Query", |
|||
Field: "scheduledMin", |
|||
Problem: "Invalid from date: " + err.Error(), |
|||
} |
|||
} |
|||
max, err := models.ParseDate(queryScheduledMax) |
|||
if err != nil { |
|||
return nil, models.BadInputError{ |
|||
Object: "Query", |
|||
Field: "scheduledMax", |
|||
Problem: "Invalid to date: " + err.Error(), |
|||
} |
|||
} |
|||
|
|||
filter.ScheduledDate = &models.Between[models.Date]{Min: min, Max: max} |
|||
} |
|||
|
|||
queryCreatedMin := c.Query("createdMin") |
|||
queryCreatedMax := c.Query("createdMax") |
|||
if queryCreatedMin != "" && queryCreatedMax != "" { |
|||
min, err := time.Parse(time.RFC3339, queryCreatedMin) |
|||
if err != nil { |
|||
return nil, models.BadInputError{ |
|||
Object: "Query", |
|||
Field: "createdMin", |
|||
Problem: "Invalid from date: " + err.Error(), |
|||
} |
|||
} |
|||
max, err := time.Parse(time.RFC3339, queryCreatedMax) |
|||
if err != nil { |
|||
return nil, models.BadInputError{ |
|||
Object: "Query", |
|||
Field: "createdMax", |
|||
Problem: "Invalid to date: " + err.Error(), |
|||
} |
|||
} |
|||
|
|||
filter.CreatedTime = &models.Between[time.Time]{Min: min, Max: max} |
|||
} |
|||
|
|||
queryAcquiredMin := c.Query("acquiredMin") |
|||
queryAcquiredMax := c.Query("acquiredMax") |
|||
if queryAcquiredMin != "" && queryAcquiredMax != "" { |
|||
min, err := time.Parse(time.RFC3339, queryAcquiredMin) |
|||
if err != nil { |
|||
return nil, models.BadInputError{ |
|||
Object: "Query", |
|||
Field: "acquiredMin", |
|||
Problem: "Invalid from date: " + err.Error(), |
|||
} |
|||
} |
|||
max, err := time.Parse(time.RFC3339, queryAcquiredMax) |
|||
if err != nil { |
|||
return nil, models.BadInputError{ |
|||
Object: "Query", |
|||
Field: "acquiredMax", |
|||
Problem: "Invalid to date: " + err.Error(), |
|||
} |
|||
} |
|||
|
|||
filter.AcquiredTime = &models.Between[time.Time]{Min: min, Max: max} |
|||
} |
|||
|
|||
anyDateMin := c.Query("anyDateMin") |
|||
anyDateMax := c.Query("anyDateMax") |
|||
if anyDateMin != "" && anyDateMax != "" { |
|||
min, err := time.Parse(time.RFC3339, anyDateMin) |
|||
if err != nil { |
|||
return nil, models.BadInputError{ |
|||
Object: "Query", |
|||
Field: "anyDateMin", |
|||
Problem: "Invalid from date: " + err.Error(), |
|||
} |
|||
} |
|||
max, err := time.Parse(time.RFC3339, anyDateMax) |
|||
if err != nil { |
|||
return nil, models.BadInputError{ |
|||
Object: "Query", |
|||
Field: "anyDateMax", |
|||
Problem: "Invalid to date: " + err.Error(), |
|||
} |
|||
} |
|||
|
|||
minY, minM, minD := min.Date() |
|||
maxY, maxM, maxD := max.Date() |
|||
|
|||
filter.AcquiredTime = &models.Between[time.Time]{Min: min, Max: max} |
|||
filter.CreatedTime = &models.Between[time.Time]{Min: min, Max: max} |
|||
filter.ScheduledDate = &models.Between[models.Date]{ |
|||
Min: models.Date{minY, int(minM), minD}, |
|||
Max: models.Date{maxY, int(maxM), maxD}, |
|||
} |
|||
} |
|||
|
|||
if queryProjectID := c.Query("projectId"); queryProjectID != "" { |
|||
ids := strings.Split(queryProjectID, ",") |
|||
for _, id := range ids { |
|||
parsed, err := strconv.Atoi(id) |
|||
if err != nil { |
|||
return nil, models.BadInputError{ |
|||
Object: "Query", |
|||
Field: fmt.Sprintf("projectId[%d]", len(filter.ProjectIDs)), |
|||
Problem: "Invalid number", |
|||
} |
|||
} |
|||
|
|||
filter.ProjectIDs = append(filter.ProjectIDs, parsed) |
|||
} |
|||
} |
|||
|
|||
if queryRequirementID := c.Query("requirementId"); queryRequirementID != "" { |
|||
if queryRequirementID != "null" { |
|||
ids := strings.Split(queryRequirementID, ",") |
|||
for _, id := range ids { |
|||
parsed, err := strconv.Atoi(id) |
|||
if err != nil { |
|||
return nil, models.BadInputError{ |
|||
Object: "Query", |
|||
Field: fmt.Sprintf("requirementId[%d]", len(filter.RequirementIDs)), |
|||
Problem: "Invalid number", |
|||
} |
|||
} |
|||
|
|||
filter.RequirementIDs = append(filter.RequirementIDs, parsed) |
|||
} |
|||
} else { |
|||
filter.Loose = true |
|||
} |
|||
} |
|||
|
|||
return items.ListScoped(c.Request.Context(), filter) |
|||
})) |
|||
} |
@ -0,0 +1,9 @@ |
|||
-- +goose Up |
|||
-- +goose StatementBegin |
|||
CREATE INDEX item_idx_requirement_id ON item (project_requirement_id); |
|||
-- +goose StatementEnd |
|||
|
|||
-- +goose Down |
|||
-- +goose StatementBegin |
|||
DROP INDEX IF EXISTS item_idx_requirement_id; |
|||
-- +goose StatementEnd |
@ -0,0 +1,9 @@ |
|||
-- +goose Up |
|||
-- +goose StatementBegin |
|||
CREATE INDEX item_idx_created_time ON item (created_time); |
|||
-- +goose StatementEnd |
|||
|
|||
-- +goose Down |
|||
-- +goose StatementBegin |
|||
DROP INDEX IF EXISTS item_idx_created_time; |
|||
-- +goose StatementEnd |
@ -0,0 +1,9 @@ |
|||
-- +goose Up |
|||
-- +goose StatementBegin |
|||
CREATE INDEX item_idx_acquired_time ON item (acquired_time); |
|||
-- +goose StatementEnd |
|||
|
|||
-- +goose Down |
|||
-- +goose StatementBegin |
|||
DROP INDEX IF EXISTS item_idx_acquired_time; |
|||
-- +goose StatementEnd |
@ -0,0 +1,9 @@ |
|||
-- +goose Up |
|||
-- +goose StatementBegin |
|||
CREATE INDEX item_idx_scheduled_date ON item (scheduled_date); |
|||
-- +goose StatementEnd |
|||
|
|||
-- +goose Down |
|||
-- +goose StatementBegin |
|||
DROP INDEX IF EXISTS item_idx_scheduled_date; |
|||
-- +goose StatementEnd |
@ -0,0 +1,46 @@ |
|||
package items |
|||
|
|||
import "git.aiterp.net/stufflog3/stufflog3/entities" |
|||
|
|||
type Result struct { |
|||
entities.Item |
|||
|
|||
Stats []ResultStat `json:"stats"` |
|||
} |
|||
|
|||
type ResultStat struct { |
|||
ID int `json:"id"` |
|||
Name string `json:"name"` |
|||
Weight float64 `json:"weight"` |
|||
Acquired int `json:"acquired"` |
|||
Required int `json:"required"` |
|||
} |
|||
|
|||
func generateResult(item entities.Item, progresses []entities.ItemProgress, stats []entities.Stat) Result { |
|||
res := Result{ |
|||
Item: item, |
|||
Stats: make([]ResultStat, 0, 8), |
|||
} |
|||
|
|||
for _, prog := range progresses { |
|||
if prog.ItemID != item.ID { |
|||
continue |
|||
} |
|||
|
|||
for _, stat := range stats { |
|||
if stat.ID == prog.StatID { |
|||
res.Stats = append(res.Stats, ResultStat{ |
|||
ID: stat.ID, |
|||
Name: stat.Name, |
|||
Weight: stat.Weight, |
|||
Acquired: prog.Acquired, |
|||
Required: prog.Required, |
|||
}) |
|||
|
|||
break |
|||
} |
|||
} |
|||
} |
|||
|
|||
return res |
|||
} |
@ -0,0 +1,42 @@ |
|||
package scopes |
|||
|
|||
import ( |
|||
"context" |
|||
"git.aiterp.net/stufflog3/stufflog3/entities" |
|||
"sync" |
|||
) |
|||
|
|||
type Context struct { |
|||
scopesRepo Repository |
|||
statsRepo StatsLister |
|||
userID string |
|||
|
|||
ID int |
|||
Scope entities.Scope |
|||
|
|||
statsOnce sync.Once |
|||
stats []entities.Stat |
|||
statsError error |
|||
|
|||
scopesOnce sync.Once |
|||
scopes []entities.Scope |
|||
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() { |
|||
c.scopes, c.scopesError = c.scopesRepo.ListUser(ctx, c.userID) |
|||
}) |
|||
|
|||
return c.scopes, c.scopesError |
|||
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue