Loggest thine Stuff
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

450 lines
10 KiB

package projects
import (
"context"
"git.aiterp.net/stufflog3/stufflog3/entities"
"git.aiterp.net/stufflog3/stufflog3/internal/genutils"
"git.aiterp.net/stufflog3/stufflog3/internal/validate"
"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"
"strconv"
"strings"
"time"
)
type Service struct {
Scopes *scopes.Service
Stats *stats.Service
Items *items.Service
Auth *auth.Service
Repository Repository
}
func (s *Service) Find(ctx context.Context, id int) (*Result, error) {
sc := s.Scopes.Context(ctx)
project, err := s.Repository.Find(ctx, sc.ID, id)
if err != nil {
return nil, err
}
requirements, requirementStats, err := s.Repository.ListRequirements(ctx, id)
if err != nil {
return nil, err
}
items, err := s.Items.ListScoped(ctx, models.ItemFilter{
ProjectIDs: []int{id},
})
if err != nil {
return nil, err
}
return generateResult(
*project,
sc.Scope,
requirements,
requirementStats,
items,
), nil
}
func (s *Service) List(ctx context.Context) ([]Entry, error) {
projects, err := s.Repository.List(ctx, s.Scopes.Context(ctx).ID)
if err != nil {
return nil, err
}
entries := make([]Entry, 0, len(projects))
for _, project := range projects {
entries = append(entries, generateEntry(project, s.Scopes.Context(ctx).Scope))
}
return entries, nil
}
func (s *Service) ListRequirementAllScopes(ctx context.Context) ([]RequirementResult, error) {
allScopes, err := s.Scopes.Context(ctx).Scopes(ctx)
if err != nil {
return nil, err
}
var res []RequirementResult
for _, scope := range allScopes {
scopedRes, err := s.listRequirements(ctx, scope)
if err != nil {
return nil, err
}
res = append(res, scopedRes...)
}
return res, nil
}
func (s *Service) ListRequirements(ctx context.Context) ([]RequirementResult, error) {
return s.listRequirements(ctx, s.Scopes.Context(ctx).Scope)
}
func (s *Service) listRequirements(ctx context.Context, sc scopes.Result) ([]RequirementResult, error) {
requirements, requirementStats, err := s.Repository.ListRequirementsByScope(ctx, sc.ID)
if err != nil {
return nil, err
}
results := make([]RequirementResult, 0, len(requirements))
for _, req := range requirements {
results = append(results, generateRequirementResult(
req,
sc,
requirementStats,
nil,
))
}
return results, nil
}
func (s *Service) ListByTags(ctx context.Context, tags []string) (interface{}, error) {
projects, err := s.Repository.ListByTags(ctx, s.Scopes.Context(ctx).ID, tags)
if err != nil {
return nil, err
}
entries := make([]Entry, 0, len(projects))
for _, project := range projects {
entries = append(entries, generateEntry(project, s.Scopes.Context(ctx).Scope))
}
return entries, nil
}
func (s *Service) FetchRequirements(ctx context.Context, ids ...int) ([]RequirementResult, error) {
sc := s.Scopes.Context(ctx)
requirements, requirementStats, err := s.Repository.FetchRequirements(ctx, sc.ID, ids...)
if err != nil {
return nil, err
}
items, err := s.Items.ListScoped(ctx, models.ItemFilter{
RequirementIDs: ids,
})
if err != nil {
return nil, err
}
results := make([]RequirementResult, 0, len(requirements))
for _, req := range requirements {
results = append(results, generateRequirementResult(
req,
sc.Scope,
requirementStats,
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",
}
}
err := validate.Tags(nil, project.Tags, nil)
if err != nil {
return nil, err
}
// Allow importing and scripts to mess with the created time, so only set it to now if it's not set.
if project.CreatedTime.IsZero() {
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
}
func (s *Service) Update(ctx context.Context, id int, update models.ProjectUpdate) (*Result, error) {
project, err := s.Find(ctx, id)
if err != nil {
return nil, err
}
err = s.Repository.Update(ctx, project.Project, update)
if err != nil {
return nil, err
}
project.Update(update)
return project, nil
}
func (s *Service) Delete(ctx context.Context, id int) (*Result, error) {
project, err := s.Find(ctx, id)
if err != nil {
return nil, err
}
err = s.Repository.Delete(ctx, project.Project)
if err != nil {
return nil, err
}
return project, nil
}
func (s *Service) FindRequirement(ctx context.Context, id int) (*RequirementResult, error) {
scope := s.Scopes.Context(ctx).Scope
req, reqStats, err := s.Repository.FetchRequirements(ctx, scope.ID, id)
if err != nil {
return nil, err
}
if len(req) == 0 {
return nil, models.NotFoundError("Requirement " + strconv.Itoa(id))
}
reqItems, err := s.Items.ListScoped(ctx, models.ItemFilter{RequirementIDs: []int{id}})
if err != nil {
return nil, err
}
return genutils.Ptr(generateRequirementResult(req[0], scope, reqStats, reqItems)), nil
}
func (s *Service) CreateRequirement(ctx context.Context, id int, requirement entities.Requirement, stats []entities.RequirementStat) (*RequirementResult, error) {
requirement.Name = strings.Trim(requirement.Name, "  \t\r\n")
if requirement.Name == "" {
return nil, models.BadInputError{
Object: "ProjectInput",
Field: "name",
Problem: "Empty name provided",
}
}
if !requirement.Status.Valid() {
return nil, models.BadInputError{
Object: "ProjectInput",
Field: "status",
Problem: "Non-existent status ID",
}
}
err := validate.Tags(nil, requirement.Tags, nil)
if err != nil {
return nil, err
}
project, err := s.Find(ctx, id)
if err != nil {
return nil, err
}
requirement.ScopeID = project.ScopeID
requirement.ProjectID = project.ID
scope := s.Scopes.Context(ctx).Scope
for _, stat := range stats {
if !scope.HasStat(stat.StatID) {
return nil, models.BadInputError{
Object: "ProjectInput",
Field: "stats",
Problem: "Non-existent stat ID",
}
}
}
newRequirement, err := s.Repository.CreateRequirement(ctx, requirement)
if err != nil {
return nil, err
}
goodStats := make([]entities.RequirementStat, 0, len(stats))
for _, stat := range stats {
stat.RequirementID = newRequirement.ID
err = s.Repository.UpsertRequirementStat(ctx, stat)
if err == nil {
goodStats = append(goodStats, stat)
}
}
result := generateRequirementResult(
*newRequirement,
scope,
goodStats,
[]items.Result{},
)
return &result, nil
}
func (s *Service) UpdateRequirement(ctx context.Context, projectID, requirementID int, update models.RequirementUpdate, stats []entities.RequirementStat) (*RequirementResult, error) {
project, err := s.Find(ctx, projectID)
if err != nil {
return nil, err
}
req := project.Requirement(requirementID)
if req == nil {
return nil, models.NotFoundError("Requirement")
}
err = validate.Tags(req.Tags, update.AddTags, update.RemoveTags)
if err != nil {
return nil, err
}
if update.ProjectID != nil && *update.ProjectID != project.ID {
dstProject, err := s.Find(ctx, *update.ProjectID)
if err != nil {
return nil, err
}
if dstProject.ScopeID != project.ScopeID {
return nil, models.ForbiddenError("Requirement cannot be moved across scopes.")
}
project = dstProject
}
err = s.Repository.UpdateRequirement(ctx, req.Requirement, update)
if err != nil {
return nil, err
}
req.Requirement.Update(update)
req.refresh(s.Scopes.Context(ctx).Scope)
needsRefresh := false
for _, stat := range stats {
stat.RequirementID = req.ID
if stat.Required >= 0 {
err = s.Repository.UpsertRequirementStat(ctx, stat)
} else {
err = s.Repository.DeleteRequirementStat(ctx, stat)
}
if err == nil {
if !req.updateStat(stat) {
needsRefresh = true
}
}
}
if needsRefresh {
return s.FindRequirement(ctx, requirementID)
}
return req, nil
}
func (s *Service) DeleteRequirement(ctx context.Context, projectID, requirementID int) (*RequirementResult, error) {
project, err := s.Find(ctx, projectID)
if err != nil {
return nil, err
}
req := project.Requirement(requirementID)
if req == nil {
return nil, models.NotFoundError("Requirement")
}
err = s.Repository.DeleteRequirement(ctx, req.Requirement)
if err != nil {
return nil, err
}
return req, nil
}
func (s *Service) UpsertRequirementStat(ctx context.Context, projectID, requirementID int, input entities.RequirementStat) (*RequirementResult, error) {
project, err := s.Find(ctx, projectID)
if err != nil {
return nil, err
}
req := project.Requirement(requirementID)
if req == nil {
return nil, models.NotFoundError("Requirement")
}
input.RequirementID = requirementID
scope := s.Scopes.Context(ctx).Scope
if !scope.HasStat(input.StatID) {
return nil, models.NotFoundError("Stat")
}
err = s.Repository.UpsertRequirementStat(ctx, input)
if err != nil {
return nil, err
}
project, err = s.Find(ctx, projectID)
if err != nil {
return nil, err
}
req = project.Requirement(requirementID)
if req == nil {
return nil, models.NotFoundError("Requirement")
}
return req, nil
}
func (s *Service) DeleteRequirementStat(ctx context.Context, projectID, requirementID, statID int) (*RequirementResult, error) {
project, err := s.Find(ctx, projectID)
if err != nil {
return nil, err
}
req := project.Requirement(requirementID)
if req == nil {
return nil, models.NotFoundError("Requirement")
}
stat := req.Stat(statID)
if stat == nil {
return nil, models.NotFoundError("Stat")
}
err = s.Repository.DeleteRequirementStat(ctx, entities.RequirementStat{
RequirementID: req.ID,
StatID: statID,
})
if err != nil {
return nil, err
}
for i := range req.Stats {
if req.Stats[i].ID == statID {
req.Stats = append(req.Stats[:i], req.Stats[i+1:]...)
break
}
}
return req, nil
}