package services import ( "context" "github.com/AchievementNetwork/stringset" "github.com/gissleh/stufflog/database" "github.com/gissleh/stufflog/internal/auth" "github.com/gissleh/stufflog/internal/slerrors" "github.com/gissleh/stufflog/models" "golang.org/x/sync/errgroup" ) // Loader loads the stuff. type Loader struct { DB database.Database } func (l *Loader) FindGroup(ctx context.Context, id string) (*models.GroupResult, error) { group, err := l.DB.Groups().Find(ctx, id) if err != nil { return nil, err } if group.UserID != auth.UserID(ctx) { return nil, slerrors.NotFound("Goal") } result := &models.GroupResult{Group: *group} result.Items, err = l.DB.Items().List(ctx, models.ItemFilter{ UserID: auth.UserID(ctx), GroupIDs: []string{group.ID}, }) if err != nil { return nil, err } return result, nil } func (l *Loader) ListGroups(ctx context.Context, filter models.GroupFilter) ([]*models.GroupResult, error) { filter.UserID = auth.UserID(ctx) groups, err := l.DB.Groups().List(ctx, filter) if err != nil { return nil, err } groupIDs := make([]string, 0, len(groups)) for _, group := range groups { groupIDs = append(groupIDs, group.ID) } items, err := l.DB.Items().List(ctx, models.ItemFilter{ UserID: auth.UserID(ctx), GroupIDs: groupIDs, }) results := make([]*models.GroupResult, len(groups)) for i, group := range groups { results[i] = &models.GroupResult{Group: *group, Items: []*models.Item{}} for _, item := range items { if item.GroupID == group.ID { results[i].Items = append(results[i].Items, item) } } } return results, nil } func (l *Loader) FindItem(ctx context.Context, id string) (*models.ItemResult, error) { item, err := l.DB.Items().Find(ctx, id) if err != nil { return nil, err } if item.UserID != auth.UserID(ctx) { return nil, slerrors.NotFound("Item") } result := &models.ItemResult{Item: *item} result.Group, err = l.DB.Groups().Find(ctx, item.GroupID) if err != nil { return nil, err } return result, nil } func (l *Loader) ListItems(ctx context.Context, filter models.ItemFilter) ([]*models.ItemResult, error) { filter.UserID = auth.UserID(ctx) items, err := l.DB.Items().List(ctx, filter) if err != nil { return nil, err } groupIDs := make([]string, 0, len(items)) for _, item := range items { groupIDs = append(groupIDs, item.GroupID) } groups, err := l.DB.Groups().List(ctx, models.GroupFilter{ UserID: auth.UserID(ctx), IDs: groupIDs, }) results := make([]*models.ItemResult, len(items)) for i, item := range items { results[i] = &models.ItemResult{Item: *item} for _, group := range groups { if item.GroupID == group.ID { results[i].Group = group break } } } return results, nil } func (l *Loader) FindLog(ctx context.Context, id string) (*models.LogResult, error) { log, err := l.DB.Logs().Find(ctx, id) if err != nil { return nil, err } if log.UserID != auth.UserID(ctx) { return nil, slerrors.NotFound("Goal") } result := &models.LogResult{ Log: *log, Task: nil, } task, err := l.DB.Tasks().Find(ctx, log.TaskID) if err != nil { return nil, err } project, err := l.DB.Projects().Find(ctx, task.ProjectID) if err != nil { return nil, err } result.Task = &models.TaskWithProject{ Task: *task, Project: project, } result.Item, _ = l.DB.Items().Find(ctx, log.ItemID) if log.SecondaryItemID != nil { result.SecondaryItem, _ = l.DB.Items().Find(ctx, *log.SecondaryItemID) } return result, nil } func (l *Loader) ListLogs(ctx context.Context, filter models.LogFilter) ([]*models.LogResult, error) { filter.UserID = auth.UserID(ctx) logs, err := l.DB.Logs().List(ctx, filter) if err != nil { return nil, err } taskIDs := stringset.New() itemIDs := stringset.New() projectIDs := stringset.New() for _, log := range logs { taskIDs.Add(log.TaskID) itemIDs.Add(log.ItemID) if log.SecondaryItemID != nil { itemIDs.Add(*log.SecondaryItemID) } } tasks, err := l.DB.Tasks().List(ctx, models.TaskFilter{ UserID: auth.UserID(ctx), IDs: taskIDs.Strings(), }) if err != nil { return nil, err } for _, task := range tasks { projectIDs.Add(task.ProjectID) } items, err := l.DB.Items().List(ctx, models.ItemFilter{ UserID: auth.UserID(ctx), IDs: itemIDs.Strings(), }) if err != nil { return nil, err } projects, err := l.DB.Projects().List(ctx, models.ProjectFilter{ UserID: auth.UserID(ctx), IDs: projectIDs.Strings(), }) if err != nil { return nil, err } results := make([]*models.LogResult, len(logs)) for i, log := range logs { results[i] = &models.LogResult{ Log: *log, Task: nil, } for _, task := range tasks { if task.ID == log.TaskID { results[i].Task = &models.TaskWithProject{Task: *task} break } } if results[i].Task != nil { for _, project := range projects { if project.ID == results[i].Task.ProjectID { results[i].Task.Project = project break } } } for _, item := range items { if item.ID == log.ItemID { results[i].Item = item } if log.SecondaryItemID != nil && item.ID == *log.SecondaryItemID { results[i].SecondaryItem = item } } } return results, nil } func (l *Loader) FindProject(ctx context.Context, id string) (*models.ProjectResult, error) { project, err := l.DB.Projects().Find(ctx, id) if err != nil { return nil, err } if project.UserID != auth.UserID(ctx) { return nil, slerrors.NotFound("Goal") } result := &models.ProjectResult{Project: *project} tasks, err := l.DB.Tasks().List(ctx, models.TaskFilter{ UserID: auth.UserID(ctx), ProjectIDs: []string{project.ID}, }) taskIDs := make([]string, 0, len(tasks)) itemIDs := stringset.New() for _, task := range tasks { taskIDs = append(taskIDs, task.ID) itemIDs.Add(task.ItemID) } logs, err := l.DB.Logs().List(ctx, models.LogFilter{ UserID: auth.UserID(ctx), TaskIDs: taskIDs, }) if err != nil { return nil, err } items, err := l.DB.Items().List(ctx, models.ItemFilter{ UserID: auth.UserID(ctx), IDs: itemIDs.Strings(), }) if err != nil { return nil, err } result.Tasks = make([]*models.TaskResult, len(tasks)) for i, task := range tasks { result.Tasks[i] = &models.TaskResult{ Logs: []*models.Log{}, } result.Tasks[i].Task = *task for _, log := range logs { if log.TaskID == task.ID { result.Tasks[i].Logs = append(result.Tasks[i].Logs, log) } } for _, item := range items { if item.ID == task.ItemID { result.Tasks[i].Item = item break } } for _, log := range result.Tasks[i].Logs { result.Tasks[i].CompletedAmount += log.Amount(result.Tasks[i].ItemID) } } return result, nil } func (l *Loader) ListProjects(ctx context.Context, filter models.ProjectFilter) ([]*models.ProjectResult, error) { filter.UserID = auth.UserID(ctx) projects, err := l.DB.Projects().List(ctx, filter) if err != nil { return nil, err } projectIDs := make([]string, 0, len(projects)) for _, project := range projects { projectIDs = append(projectIDs, project.ID) } tasks, links, err := l.DB.Tasks().ListWithLinks(ctx, models.TaskFilter{ UserID: auth.UserID(ctx), ProjectIDs: projectIDs, }) if err != nil { return nil, err } taskIDs := make([]string, 0, len(tasks)) itemIDs := stringset.New() for _, task := range tasks { taskIDs = append(taskIDs, task.ID) itemIDs.Add(task.ItemID) } logs, err := l.DB.Logs().List(ctx, models.LogFilter{ UserID: auth.UserID(ctx), TaskIDs: taskIDs, }) if err != nil { return nil, err } items, err := l.DB.Items().List(ctx, models.ItemFilter{ UserID: auth.UserID(ctx), IDs: itemIDs.Strings(), }) if err != nil { return nil, err } results := make([]*models.ProjectResult, len(projects)) for i, project := range projects { results[i] = &models.ProjectResult{Project: *project} results[i].Tasks = make([]*models.TaskResult, 0, 16) for _, task := range tasks { if task.ProjectID != project.ID { foundLink := false for _, link := range links { if link.TaskID == task.ID && link.ProjectID == project.ID { foundLink = true break } } if !foundLink { continue } } taskResult := &models.TaskResult{ Task: *task, Logs: []*models.Log{}, } for _, log := range logs { if log.TaskID == task.ID { taskResult.Logs = append(taskResult.Logs, log) } } for _, item := range items { if item.ID == task.ItemID { taskResult.Item = item break } } for _, log := range taskResult.Logs { taskResult.CompletedAmount += log.Amount(taskResult.ItemID) } results[i].Tasks = append(results[i].Tasks, taskResult) } } return results, nil } func (l *Loader) FindProjectGroup(ctx context.Context, id string) (*models.ProjectGroupResult, error) { group, err := l.DB.ProjectGroups().Find(ctx, id) if err != nil { return nil, err } projects, err := l.ListProjects(ctx, models.ProjectFilter{ UserID: auth.UserID(ctx), ProjectGroupIDs: []string{group.ID}, }) if err != nil { return nil, err } result := &models.ProjectGroupResult{ ProjectGroup: *group, Projects: projects, } result.RecountTasks() return result, nil } func (l *Loader) ListProjectGroups(ctx context.Context) ([]*models.ProjectGroupResult, error) { groups, err := l.DB.ProjectGroups().List(ctx, models.ProjectGroupFilter{UserID: auth.UserID(ctx)}) if err != nil { return nil, err } ids := make([]string, 0, len(groups)) for _, group := range groups { ids = append(ids, group.ID) } projects, err := l.ListProjects(ctx, models.ProjectFilter{ UserID: auth.UserID(ctx), ProjectGroupIDs: ids, }) results := make([]*models.ProjectGroupResult, 0, len(groups)+1) for _, group := range groups { matchingProjects := make([]*models.ProjectResult, 0, len(projects)/len(groups)) for _, project := range projects { if *project.GroupID == group.ID { matchingProjects = append(matchingProjects, project) } } result := &models.ProjectGroupResult{ ProjectGroup: *group, Projects: matchingProjects, } result.RecountTasks() results = append(results, result) } ungroupedProjects, err := l.ListProjects(ctx, models.ProjectFilter{ UserID: auth.UserID(ctx), Ungrouped: true, }) if err != nil { return nil, err } if len(ungroupedProjects) > 0 { result := &models.ProjectGroupResult{ ProjectGroup: models.ProjectGroup{ ID: "META_UNGROUPED", Name: "Ungrouped Projects", Abbreviation: "OTHER", CategoryNames: map[string]string{}, }, Projects: ungroupedProjects, } result.RecountTasks() results = append(results, result) } return results, nil } func (l *Loader) FindTask(ctx context.Context, id string) (*models.TaskResult, error) { task, err := l.DB.Tasks().Find(ctx, id) if err != nil { return nil, err } if task.UserID != auth.UserID(ctx) { return nil, slerrors.NotFound("Goal") } result := &models.TaskResult{Task: *task} result.Item, _ = l.DB.Items().Find(ctx, task.ItemID) result.Project, _ = l.DB.Projects().Find(ctx, task.ProjectID) result.Logs, err = l.DB.Logs().List(ctx, models.LogFilter{ UserID: task.UserID, TaskIDs: []string{task.ID}, }) if err != nil { return nil, err } for _, log := range result.Logs { result.CompletedAmount += log.Amount(result.ItemID) } return result, nil } func (l *Loader) ListTasks(ctx context.Context, filter models.TaskFilter) ([]*models.TaskResult, error) { filter.UserID = auth.UserID(ctx) tasks, err := l.DB.Tasks().List(ctx, filter) if err != nil { return nil, err } if len(tasks) == 0 { return []*models.TaskResult{}, nil } taskIDs := make([]string, 0, len(tasks)) itemIDs := stringset.New() projectIDs := stringset.New() for _, task := range tasks { taskIDs = append(taskIDs, task.ID) itemIDs.Add(task.ItemID) projectIDs.Add(task.ProjectID) } logs, err := l.DB.Logs().List(ctx, models.LogFilter{ UserID: auth.UserID(ctx), TaskIDs: taskIDs, }) if err != nil { return nil, err } items, err := l.DB.Items().List(ctx, models.ItemFilter{ UserID: auth.UserID(ctx), IDs: itemIDs.Strings(), }) if err != nil { return nil, err } projects, err := l.DB.Projects().List(ctx, models.ProjectFilter{ UserID: auth.UserID(ctx), IDs: projectIDs.Strings(), }) if err != nil { return nil, err } results := make([]*models.TaskResult, 0, len(tasks)) for _, task := range tasks { result := &models.TaskResult{ Task: *task, Logs: []*models.Log{}, } for _, log := range logs { if log.TaskID == task.ID { result.Logs = append(result.Logs, log) } } for _, item := range items { if item.ID == task.ItemID { result.Item = item break } } for _, project := range projects { if project.ID == task.ProjectID { result.Project = project break } } for _, log := range result.Logs { result.CompletedAmount += log.Amount(result.ItemID) } results = append(results, result) } return results, nil } func (l *Loader) FindGoal(ctx context.Context, id string) (*models.GoalResult, error) { goal, err := l.DB.Goals().Find(ctx, id) if err != nil { return nil, err } if goal.UserID != auth.UserID(ctx) { return nil, slerrors.NotFound("Goal") } return l.populateGoals(ctx, goal) } func (l *Loader) ListGoals(ctx context.Context, filter models.GoalFilter) ([]*models.GoalResult, error) { filter.UserID = auth.UserID(ctx) goals, err := l.DB.Goals().List(ctx, filter) if err != nil { return nil, err } results := make([]*models.GoalResult, len(goals)) eg := errgroup.Group{} for i := range results { index := i // Required to avoid race condition. eg.Go(func() error { res, err := l.populateGoals(ctx, goals[index]) if err != nil { return err } results[index] = res return nil }) } err = eg.Wait() if err != nil { return nil, err } return results, nil } func (l *Loader) populateGoals(ctx context.Context, goal *models.Goal) (*models.GoalResult, error) { userID := auth.UserID(ctx) result := &models.GoalResult{ Goal: *goal, Group: nil, Items: nil, Logs: nil, CompletedAmount: 0, } result.Group, _ = l.DB.Groups().Find(ctx, goal.GroupID) if result.Group != nil { // Get items items, err := l.DB.Items().List(ctx, models.ItemFilter{ UserID: userID, GroupIDs: []string{goal.GroupID}, }) if err != nil { return nil, err } itemIDs := make([]string, 0, len(items)) for _, item := range items { result.Items = append(result.Items, &models.GoalResultItem{ Item: *item, CompletedAmount: 0, }) itemIDs = append(itemIDs, item.ID) } // Get logs logs, err := l.DB.Logs().List(ctx, models.LogFilter{ UserID: userID, ItemIDs: itemIDs, MinTime: &goal.StartTime, MaxTime: &goal.EndTime, }) if err != nil { return nil, err } // Get tasks taskIDs := stringset.New() for _, log := range logs { taskIDs.Add(log.TaskID) } tasks, err := l.DB.Tasks().List(ctx, models.TaskFilter{ UserID: userID, IDs: taskIDs.Strings(), }) if err != nil { return nil, err } projectIDs := stringset.New() for _, task := range tasks { projectIDs.Add(task.ProjectID) } projects, err := l.DB.Projects().List(ctx, models.ProjectFilter{ UserID: userID, IDs: projectIDs.Strings(), }) // Apply logs result.Logs = make([]*models.GoalResultLog, 0, len(logs)) for _, log := range logs { resultLog := &models.GoalResultLog{ LogResult: models.LogResult{Log: *log}, } contributes := false for _, task := range tasks { if task.ID == log.TaskID { resultLog.Task = &models.TaskWithProject{Task: *task} for _, project := range projects { if project.ID == task.ProjectID { resultLog.Task.Project = project break } } break } } for _, item := range result.Items { amount := log.Amount(item.ID) if amount > 0 && goal.Accepts(&item.Item, &resultLog.Task.Task) { item.CompletedAmount += amount if goal.Unweighted { if item.GroupWeight > 0 { result.CompletedAmount += amount } } else { result.CompletedAmount += amount * item.GroupWeight } contributes = true } else { amount = 0 } if item.ID == log.ItemID { resultLog.Item = &item.Item if amount > 0 { resultLog.ItemCounted = true } if log.SecondaryItemID == nil { break } } if log.SecondaryItemID != nil && item.ID == *log.SecondaryItemID { if amount > 0 { resultLog.SecondaryItemCounted = true } resultLog.SecondaryItem = &item.Item } } if contributes { result.Logs = append(result.Logs, resultLog) } } } return result, nil }