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 }