From 47d8a027daeb10e6134b00e2530d73d419292534 Mon Sep 17 00:00:00 2001 From: Gisle Aune Date: Sun, 10 May 2020 20:50:18 +0200 Subject: [PATCH] add issue tasks. --- database/database.go | 1 + database/drivers/mysqldriver/db.go | 8 + database/drivers/mysqldriver/issetasks.go | 138 ++++++++++++++++++ database/drivers/mysqldriver/issues.go | 1 + database/repositories/issuetaskrepository.go | 14 ++ graph/gqlgen.yml | 6 + graph/resolvers/activity.resolvers.go | 1 + graph/resolvers/issue.resolvers.go | 9 ++ graph/resolvers/issuetask.resolvers.go | 92 ++++++++++++ graph/resolvers/mutation.resolvers.go | 115 +++++++++++++++ graph/scalars/duration.go | 30 ++++ graph/schema/issue.gql | 8 +- graph/schema/issuetask.gql | 98 +++++++++++++ graph/schema/mutation.gql | 10 ++ graph/schema/project.gql | 1 + graph/schema/scalars.gql | 11 +- ...0503195913_create_table_project_status.sql | 2 +- ...20200509143532_create_table_issue_task.sql | 26 ++++ models/issuetask.go | 12 +- models/item.go | 7 - models/log.go | 23 ++- 21 files changed, 598 insertions(+), 15 deletions(-) create mode 100644 database/drivers/mysqldriver/issetasks.go create mode 100644 database/repositories/issuetaskrepository.go create mode 100644 graph/resolvers/issuetask.resolvers.go create mode 100644 graph/scalars/duration.go create mode 100644 graph/schema/issuetask.gql create mode 100644 migrations/mysql/20200509143532_create_table_issue_task.sql diff --git a/database/database.go b/database/database.go index 569820e..ef3d592 100644 --- a/database/database.go +++ b/database/database.go @@ -11,6 +11,7 @@ var ErrDriverNotSupported = errors.New("driver not found or supported") type Database interface { Activities() repositories.ActivityRepository Issues() repositories.IssueRepository + IssueTasks() repositories.IssueTaskRepository Items() repositories.ItemRepository Projects() repositories.ProjectRepository Session() repositories.SessionRepository diff --git a/database/drivers/mysqldriver/db.go b/database/drivers/mysqldriver/db.go index 5ead79a..77dc1c9 100644 --- a/database/drivers/mysqldriver/db.go +++ b/database/drivers/mysqldriver/db.go @@ -16,6 +16,7 @@ type DB struct { db *sqlx.DB activities *activityRepository issues *issueRepository + issueTasks *issueTaskRepository items *itemRepository projects *projectRepository sessions *sessionRepository @@ -31,6 +32,10 @@ func (db *DB) Issues() repositories.IssueRepository { return db.issues } +func (db *DB) IssueTasks() repositories.IssueTaskRepository { + return db.issueTasks +} + func (db *DB) Items() repositories.ItemRepository { return db.items } @@ -81,6 +86,7 @@ func Open(connectionString string) (*DB, error) { return nil, err } + // Setup repositories activities := &activityRepository{db: db} issues := &issueRepository{db: db} items := &itemRepository{db: db} @@ -88,11 +94,13 @@ func Open(connectionString string) (*DB, error) { users := &userRepository{db: db} sessions := &sessionRepository{db: db} projectStatuses := &projectStatusRepository{db: db} + issueTasks := &issueTaskRepository{db: db} return &DB{ db: db, activities: activities, issues: issues, + issueTasks: issueTasks, items: items, projects: projects, users: users, diff --git a/database/drivers/mysqldriver/issetasks.go b/database/drivers/mysqldriver/issetasks.go new file mode 100644 index 0000000..3ca418c --- /dev/null +++ b/database/drivers/mysqldriver/issetasks.go @@ -0,0 +1,138 @@ +package mysqldriver + +import ( + "context" + "database/sql" + "errors" + "fmt" + "git.aiterp.net/stufflog/server/internal/xlerrors" + "git.aiterp.net/stufflog/server/models" + sq "github.com/Masterminds/squirrel" + "github.com/jmoiron/sqlx" + "time" +) + +type issueTaskRepository struct { + db *sqlx.DB +} + +func (r *issueTaskRepository) Find(ctx context.Context, id string) (*models.IssueTask, error) { + issueTask := models.IssueTask{} + err := r.db.GetContext(ctx, &issueTask, "SELECT * FROM issue_task WHERE issue_task_id=?", id) + if err != nil { + if err == sql.ErrNoRows { + return nil, xlerrors.NotFound("Issue task") + } + + return nil, err + } + + return &issueTask, nil +} + +func (r *issueTaskRepository) List(ctx context.Context, filter models.IssueTaskFilter) ([]*models.IssueTask, error) { + q := sq.Select("*").From("issue_task") + if filter.ActivityIDs != nil { + q = q.Where(sq.Eq{"activity_id": filter.ActivityIDs}) + } + if filter.IssueTaskIDs != nil { + q = q.Where(sq.Eq{"issue_id": filter.IssueTaskIDs}) + } + if filter.IssueIDs != nil { + q = q.Where(sq.Eq{"issue_id": filter.IssueIDs}) + } + if filter.MinStage != nil { + q = q.Where(sq.GtOrEq{"status_stage": *filter.MinStage}) + } + if filter.MaxStage != nil { + q = q.Where(sq.LtOrEq{"status_stage": *filter.MaxStage}) + } + + query, args, err := q.ToSql() + if err != nil { + return nil, err + } + + results := make([]*models.IssueTask, 0, 16) + err = r.db.SelectContext(ctx, &results, query, args...) + if err != nil { + if err == sql.ErrNoRows { + return []*models.IssueTask{}, nil + } + + return nil, err + } + + return results, nil +} + +func (r *issueTaskRepository) Insert(ctx context.Context, task models.IssueTask) (*models.IssueTask, error) { + if task.IssueID == "" { + return nil, errors.New("missing issue id") + } + + if task.CreatedTime.IsZero() { + task.CreatedTime = time.Now().Truncate(time.Second) + task.UpdatedTime = task.CreatedTime + } + + tx, err := r.db.BeginTxx(ctx, nil) + if err != nil { + return nil, err + } + + nextID, err := incCounter(ctx, tx, counterKindIssueSubID, task.IssueID) + if err != nil { + _ = tx.Rollback() + return nil, err + } + task.ID = fmt.Sprintf("%s-%d", task.IssueID, nextID) + + _, err = tx.NamedExecContext(ctx, ` + INSERT INTO issue_task ( + issue_task_id, issue_id, activity_id, created_time, + updated_time, due_time, status_stage, status_name, + name, description, estimated_time, estimated_units, + points_multiplier + ) VALUES ( + :issue_task_id, :issue_id, :activity_id, :created_time, + :updated_time, :due_time, :status_stage, :status_name, + :name, :description, :estimated_time, :estimated_units, + :points_multiplier + ) + `, task) + if err != nil { + _ = tx.Rollback() + return nil, err + } + + err = tx.Commit() + if err != nil { + _ = tx.Rollback() + return nil, err + } + + return &task, nil +} + +func (r *issueTaskRepository) Save(ctx context.Context, task models.IssueTask) error { + _, err := r.db.NamedExecContext(ctx, ` + UPDATE issue_task SET + due_time=:due_time, + status_stage=:status_stage, + status_name=:status_name, + name=:name, + description=:description, + estimated_time=:estimated_time, + estimated_units=:estimated_units, + points_multiplier=:points_multiplier + WHERE issue_task_id=:issue_task_id + `, task) + + return err +} + +func (r *issueTaskRepository) Delete(ctx context.Context, task models.IssueTask) error { + _, err := r.db.ExecContext(ctx, "DELETE FROM issue_task WHERE issue_task_id=? LIMIT 1;", task.ID) + return err +} diff --git a/database/drivers/mysqldriver/issues.go b/database/drivers/mysqldriver/issues.go index 7e69db6..6ffa1a4 100644 --- a/database/drivers/mysqldriver/issues.go +++ b/database/drivers/mysqldriver/issues.go @@ -13,6 +13,7 @@ import ( ) var counterKindIssueID = "NextIssueID" +var counterKindIssueSubID = "NextIssueSubID" type issueRepository struct { db *sqlx.DB diff --git a/database/repositories/issuetaskrepository.go b/database/repositories/issuetaskrepository.go new file mode 100644 index 0000000..83d3174 --- /dev/null +++ b/database/repositories/issuetaskrepository.go @@ -0,0 +1,14 @@ +package repositories + +import ( + "context" + "git.aiterp.net/stufflog/server/models" +) + +type IssueTaskRepository interface { + Find(ctx context.Context, id string) (*models.IssueTask, error) + List(ctx context.Context, filter models.IssueTaskFilter) ([]*models.IssueTask, error) + Insert(ctx context.Context, task models.IssueTask) (*models.IssueTask, error) + Save(ctx context.Context, task models.IssueTask) error + Delete(ctx context.Context, task models.IssueTask) error +} diff --git a/graph/gqlgen.yml b/graph/gqlgen.yml index 9d233b0..b5500eb 100644 --- a/graph/gqlgen.yml +++ b/graph/gqlgen.yml @@ -14,6 +14,12 @@ models: fields: unitName: resolver: true + IssueTask: + fields: + estimatedUnits: + resolver: true + Duration: + model: git.aiterp.net/stufflog/server/graph/scalars.Duration resolver: layout: follow-schema diff --git a/graph/resolvers/activity.resolvers.go b/graph/resolvers/activity.resolvers.go index 404577f..c75bc57 100644 --- a/graph/resolvers/activity.resolvers.go +++ b/graph/resolvers/activity.resolvers.go @@ -5,6 +5,7 @@ package resolvers import ( "context" + "git.aiterp.net/stufflog/server/graph/graphcore" "git.aiterp.net/stufflog/server/models" ) diff --git a/graph/resolvers/issue.resolvers.go b/graph/resolvers/issue.resolvers.go index 0de9a96..5fbc7f5 100644 --- a/graph/resolvers/issue.resolvers.go +++ b/graph/resolvers/issue.resolvers.go @@ -76,6 +76,15 @@ func (r *issueResolver) Status(ctx context.Context, obj *models.Issue) (*models. return status, nil } +func (r *issueResolver) Tasks(ctx context.Context, obj *models.Issue, filter *models.IssueTaskFilter) ([]*models.IssueTask, error) { + if filter == nil { + filter = &models.IssueTaskFilter{} + } + filter.IssueTaskIDs = []string{obj.ID} + + return r.Database.IssueTasks().List(ctx, *filter) +} + // Issue returns graphcore.IssueResolver implementation. func (r *Resolver) Issue() graphcore.IssueResolver { return &issueResolver{r} } diff --git a/graph/resolvers/issuetask.resolvers.go b/graph/resolvers/issuetask.resolvers.go new file mode 100644 index 0000000..a1764fd --- /dev/null +++ b/graph/resolvers/issuetask.resolvers.go @@ -0,0 +1,92 @@ +package resolvers + +// This file will be automatically regenerated based on the schema, any resolver implementations +// will be copied through when generating and any unknown code will be moved to the end. + +import ( + "context" + "errors" + "fmt" + "strings" + "time" + + "git.aiterp.net/stufflog/server/graph/graphcore" + "git.aiterp.net/stufflog/server/graph/graphutil" + "git.aiterp.net/stufflog/server/internal/xlerrors" + "git.aiterp.net/stufflog/server/models" +) + +func (r *issueTaskResolver) EstimatedUnits(ctx context.Context, obj *models.IssueTask) (*int, error) { + // TODO: Data loader + activity, err := r.Database.Activities().Find(ctx, obj.ActivityID) + if err != nil { + return nil, err + } + if !activity.Countable || activity.UnitIsTimeSpent { + return nil, nil + } + + return &obj.EstimatedUnits, nil +} + +func (r *issueTaskResolver) Issue(ctx context.Context, obj *models.IssueTask) (*models.Issue, error) { + return r.Database.Issues().Find(ctx, obj.IssueID) +} + +func (r *issueTaskResolver) Activity(ctx context.Context, obj *models.IssueTask) (*models.Activity, error) { + return r.Database.Activities().Find(ctx, obj.ActivityID) +} + +func (r *issueTaskResolver) Status(ctx context.Context, obj *models.IssueTask) (*models.ProjectStatus, error) { + // The project ID is always the prefix before the first dash in the issue ID. + split := strings.SplitN(obj.IssueID, "-", 2) + if len(split) == 0 { + return nil, errors.New("invalid issue ID") + } + projectID := split[0] + + // Shortcut: if description isn't needed, resolve this with issue's properties. + if !graphutil.SelectsAnyField(ctx, "description") { + return &models.ProjectStatus{ + ProjectID: projectID, + Stage: obj.StatusStage, + Name: obj.StatusName, + Description: "FAKE", + }, nil + } + + // Find it in the database. TODO: DataLoader + status, err := r.Database.ProjectStatuses().Find(ctx, projectID, obj.StatusName) + if xlerrors.IsNotFound(err) { + return &models.ProjectStatus{ + ProjectID: projectID, + Stage: obj.StatusStage, + Name: obj.StatusName, + Description: "(Deleted or unknown status)", + }, nil + } else if err != nil { + return nil, err + } + + // If the stage doesn't match, sneakily correct it for next time. + if status.Stage != obj.StatusStage { + updatedTask := *obj + updatedTask.StatusStage = status.Stage + _ = r.Database.IssueTasks().Save(ctx, updatedTask) + } + + return status, nil +} + +func (r *issueTaskResolver) RemainingTime(ctx context.Context, obj *models.IssueTask) (time.Duration, error) { + panic(fmt.Errorf("not implemented")) +} + +func (r *issueTaskResolver) RemainingUnits(ctx context.Context, obj *models.IssueTask) (*int, error) { + panic(fmt.Errorf("not implemented")) +} + +// IssueTask returns graphcore.IssueTaskResolver implementation. +func (r *Resolver) IssueTask() graphcore.IssueTaskResolver { return &issueTaskResolver{r} } + +type issueTaskResolver struct{ *Resolver } diff --git a/graph/resolvers/mutation.resolvers.go b/graph/resolvers/mutation.resolvers.go index 7fa07e9..a0bd7fe 100644 --- a/graph/resolvers/mutation.resolvers.go +++ b/graph/resolvers/mutation.resolvers.go @@ -6,6 +6,7 @@ package resolvers import ( "context" "errors" + "time" "git.aiterp.net/stufflog/server/graph/graphcore" "git.aiterp.net/stufflog/server/internal/xlerrors" @@ -169,6 +170,120 @@ func (r *mutationResolver) CreateIssue(ctx context.Context, input graphcore.Issu return issue, nil } +func (r *mutationResolver) CreateIssueTask(ctx context.Context, input graphcore.IssueTaskCreateInput) (*models.IssueTask, error) { + user := r.Auth.UserFromContext(ctx) + if user == nil { + return nil, xlerrors.PermissionDenied + } + + issue, err := r.Database.Issues().Find(ctx, input.IssueID) + if err != nil { + return nil, err + } + if perm, err := r.Auth.IssuePermission(ctx, *issue); err != nil || !perm.CanManageOwnIssue() { + return nil, xlerrors.PermissionDenied + } + + status, err := r.Database.ProjectStatuses().Find(ctx, issue.ProjectID, input.StatusName) + if err != nil { + return nil, err + } + + activity, err := r.Database.Activities().Find(ctx, input.ActivityID) + if err != nil { + return nil, err + } else if activity.ProjectID != issue.ProjectID { + return nil, xlerrors.NotFound("Activity") + } + + issueTask := &models.IssueTask{ + IssueID: issue.ID, + ActivityID: activity.ID, + CreatedTime: time.Now(), + UpdatedTime: time.Now(), + StatusStage: status.Stage, + StatusName: status.Name, + Name: input.Name, + Description: input.Description, + PointsMultiplier: 1.0, + } + if input.EstimatedUnits != nil && activity.Countable && !activity.UnitIsTimeSpent { + issueTask.EstimatedUnits = *input.EstimatedUnits + } + if input.PointsMultiplier != nil && *input.PointsMultiplier > 0 { + issueTask.PointsMultiplier = *input.PointsMultiplier + } + + issueTask, err = r.Database.IssueTasks().Insert(ctx, *issueTask) + if err != nil { + return nil, err + } + + return issueTask, nil +} + +func (r *mutationResolver) EditIssueTask(ctx context.Context, input graphcore.IssueTaskEditInput) (*models.IssueTask, error) { + user := r.Auth.UserFromContext(ctx) + if user == nil { + return nil, xlerrors.PermissionDenied + } + + task, err := r.Database.IssueTasks().Find(ctx, input.IssueTaskID) + if err != nil { + return nil, err + } + + issue, err := r.Database.Issues().Find(ctx, task.IssueID) + if err != nil { + return nil, err + } + if perm, err := r.Auth.IssuePermission(ctx, *issue); err != nil || !perm.CanManageOwnIssue() { + return nil, xlerrors.PermissionDenied + } + + if input.SetName != nil { + task.Name = *input.SetName + } + if input.SetDescription != nil { + task.Description = *input.SetDescription + } + if input.SetDueTime != nil { + task.DueTime = input.SetDueTime + } + if input.SetEstimatedTime != nil { + task.EstimatedTime = *input.SetEstimatedTime + } + if input.SetEstimatedUnits != nil { + activity, err := r.Database.Activities().Find(ctx, task.ActivityID) + if err != nil { + return nil, err + } + + if activity.Countable && !activity.UnitIsTimeSpent { + task.EstimatedUnits = *input.SetEstimatedUnits + } + } + if input.SetPointsMultiplier != nil && *input.SetPointsMultiplier > 0 { + task.PointsMultiplier = *input.SetPointsMultiplier + } + if input.SetStatusName != nil { + status, err := r.Database.ProjectStatuses().Find(ctx, issue.ProjectID, *input.SetStatusName) + if err != nil { + return nil, err + } + + task.StatusName = status.Name + task.StatusStage = status.Stage + } + + err = r.Database.IssueTasks().Save(ctx, *task) + if err != nil { + return nil, err + } + + return task, nil +} + func (r *mutationResolver) LoginUser(ctx context.Context, input graphcore.UserLoginInput) (*models.User, error) { return r.Auth.Login(ctx, input.Username, input.Password) } diff --git a/graph/scalars/duration.go b/graph/scalars/duration.go new file mode 100644 index 0000000..b3971db --- /dev/null +++ b/graph/scalars/duration.go @@ -0,0 +1,30 @@ +package scalars + +import ( + "fmt" + "github.com/99designs/gqlgen/graphql" + "io" + "strconv" + "time" +) + +func MarshalDuration(d time.Duration) graphql.Marshaler { + return graphql.WriterFunc(func(w io.Writer) { + _, _ = w.Write([]byte(strconv.Itoa(int(d.Milliseconds())))) + }) +} + +func UnmarshalDuration(v interface{}) (time.Duration, error) { + switch v := v.(type) { + case string: + return time.ParseDuration(v) + case int: + return time.Millisecond * time.Duration(v), nil + case int64: + return time.Millisecond * time.Duration(v), nil + case float64: + return time.Millisecond * time.Duration(v), nil + default: + return 0, fmt.Errorf("%T is not a bool", v) + } +} diff --git a/graph/schema/issue.gql b/graph/schema/issue.gql index 7858baa..5a272a0 100644 --- a/graph/schema/issue.gql +++ b/graph/schema/issue.gql @@ -18,10 +18,14 @@ type Issue { project: Project "The issue's owner/creator." owner: User - "The issue assignee." + "The issue's assignee." assignee: User - "The issue status." + "Current status of the issue." status: ProjectStatus! + "Issue tasks." + tasks(filter: IssueTaskFilter): [IssueTask!]! + #"Logs related to this issue." + #logs: [Log!]! } input IssueFilter { diff --git a/graph/schema/issuetask.gql b/graph/schema/issuetask.gql new file mode 100644 index 0000000..80651c9 --- /dev/null +++ b/graph/schema/issuetask.gql @@ -0,0 +1,98 @@ +""" +An issue task is a the main part of an issue. They contain estimates, and which activity +is to be performed. They don't get their own page in UI and should have all their information +presented in a concise manner. +""" +type IssueTask { + "The issue task ID." + id: String! + "The time when the task was created." + createdTime: Time! + "The time when the task was updated." + updatedTime: Time! + "The time when the task is due." + dueTime: Time + "A name for the task." + name: String! + "A short description of the task." + description: String! + "The estimated time." + estimatedTime: Duration! + "The estimated units." + estimatedUnits: Int + "A multiplier for the points earned." + pointsMultiplier: Float! + + "Parent issue." + issue: Issue! + "Activity the task performs." + activity: Activity! + "The status of this task." + status: ProjectStatus! + #"Logs related to this task." + #logs: [Log!]! + + "Remaining time from the logs." + remainingTime: Duration! + "Remaining units (if countable)" + remainingUnits: Int +} + +""" +A subset of the filter for Issue.tasks +""" +input IssueTaskFilter { + "The activity IDs to limit the task list with." + activityIds: [String!] + "The lowest stage (inclusive)." + minStage: Int + "The highest stage (inclusive)." + maxStage: Int +} + +""" +Input for the createIssueTask mutation. +""" +input IssueTaskCreateInput { + "The issue ID to parent to." + issueId: String! + + "The activity ID this task is about." + activityId: String! + "The name of the task." + name: String! + "The description of the task." + description: String! + "Estimated time to perform the task." + estimatedTime: Duration! + "Task status." + statusName: String! + + "Estimate an amount of units. This is required for issues with a countable activity." + estimatedUnits: Int + "Set an optional multiplier for the issue." + pointsMultiplier: Float +} + +""" +Input for the editIssueTask mutation. +""" +input IssueTaskEditInput { + "The issue task to edit." + issueTaskId: String! + + "Update the status." + setStatusName: String + "Set the name." + setName: String + "Set description." + setDescription: String + "Set estimated time." + setEstimatedTime: Duration + "Set estimated units." + setEstimatedUnits: Int + "Set points multiplier." + setPointsMultiplier: Float + "Set due time." + setDueTime: Time +} \ No newline at end of file diff --git a/graph/schema/mutation.gql b/graph/schema/mutation.gql index e471078..c025592 100644 --- a/graph/schema/mutation.gql +++ b/graph/schema/mutation.gql @@ -9,9 +9,19 @@ type Mutation { "Edit an activity." editActivity(input: ActivityEditInput!): Activity! + # ITEM + # ISSUE "Create a new issue." createIssue(input: IssueCreateInput!): Issue! + # ISSUE TASK + "Create a new issue task." + createIssueTask(input: IssueTaskCreateInput!): IssueTask! + "Edit an issue task." + editIssueTask(input: IssueTaskEditInput!): IssueTask! + # ISSUE ITEM + + # LOG # USER "Log in." diff --git a/graph/schema/project.gql b/graph/schema/project.gql index 76847c9..07305de 100644 --- a/graph/schema/project.gql +++ b/graph/schema/project.gql @@ -29,6 +29,7 @@ type ProjectPermission { user: User } +"The project's statuses for issues and issue tasks." type ProjectStatus { "The stage of the status. 0=inactive, 1=pending, 2=active, 3=review, 4=completed, 5=failed, 6=postponed" stage: Int! diff --git a/graph/schema/scalars.gql b/graph/schema/scalars.gql index 53becdd..d547f9a 100644 --- a/graph/schema/scalars.gql +++ b/graph/schema/scalars.gql @@ -1 +1,10 @@ -scalar Time \ No newline at end of file +""" +A ISO3339 timestamp. +""" +scalar Time + +""" +Duration in milliseconds. It will output as an integer, but can be supplied as a float, integer +or a string accepted by the Go time.Duration parser. +""" +scalar Duration \ No newline at end of file diff --git a/migrations/mysql/20200503195913_create_table_project_status.sql b/migrations/mysql/20200503195913_create_table_project_status.sql index 8e049e0..31bce5a 100644 --- a/migrations/mysql/20200503195913_create_table_project_status.sql +++ b/migrations/mysql/20200503195913_create_table_project_status.sql @@ -2,7 +2,7 @@ -- +goose StatementBegin CREATE TABLE project_status ( project_id CHAR(16) NOT NULL, - name VARCHAR(255) NOT NULL, + name CHAR(32) NOT NULL, stage INT NOT NULL, description VARCHAR(255) NOT NULL, diff --git a/migrations/mysql/20200509143532_create_table_issue_task.sql b/migrations/mysql/20200509143532_create_table_issue_task.sql new file mode 100644 index 0000000..ca9a31c --- /dev/null +++ b/migrations/mysql/20200509143532_create_table_issue_task.sql @@ -0,0 +1,26 @@ +-- +goose Up +-- +goose StatementBegin +CREATE TABLE issue_task ( + issue_task_id CHAR(48) PRIMARY KEY, + issue_id CHAR(32) NOT NULL, + activity_id CHAR(16) NOT NULL, + created_time TIMESTAMP NOT NULL, + updated_time TIMESTAMP NOT NULL, + due_time TIMESTAMP, + status_stage INT NOT NULL, + status_name CHAR(32) NOT NULL, + name VARCHAR(255) NOT NULL, + description TEXT NOT NULL, + estimated_time BIGINT NOT NULL, + estimated_units INT NOT NULL, + points_multiplier FLOAT NOT NULL, + + INDEX (issue_id), + INDEX (activity_id) +); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP TABLE issue_task; +-- +goose StatementEnd diff --git a/models/issuetask.go b/models/issuetask.go index baa0d25..7b3031b 100644 --- a/models/issuetask.go +++ b/models/issuetask.go @@ -4,12 +4,12 @@ import "time" // An IssueTask is a task within an issue. type IssueTask struct { - TaskID string `db:"task_id"` + ID string `db:"issue_task_id"` IssueID string `db:"issue_id"` ActivityID string `db:"activity_id"` CreatedTime time.Time `db:"created_time"` UpdatedTime time.Time `db:"updated_time"` - DueTime time.Time `db:"due_time"` + DueTime *time.Time `db:"due_time"` StatusStage int `db:"status_stage"` StatusName string `db:"status_name"` Name string `db:"name"` @@ -18,3 +18,11 @@ type IssueTask struct { EstimatedUnits int `db:"estimated_units"` PointsMultiplier float64 `db:"points_multiplier"` } + +type IssueTaskFilter struct { + IssueTaskIDs []string + IssueIDs []string + ActivityIDs []string + MinStage *int + MaxStage *int +} diff --git a/models/item.go b/models/item.go index e4e4e3e..83595f4 100644 --- a/models/item.go +++ b/models/item.go @@ -13,10 +13,3 @@ type ItemFilter struct { ItemIDs []string Tags []string } - -/* - SELECT i.item_id, i.name FROM item i - LEFT JOIN tag AS t ON t.item_id = i.item_id - WHERE t.tag_name IN ("Groceries") - GROUP by i.item_id; -*/ diff --git a/models/log.go b/models/log.go index 3b12aff..22bf80b 100644 --- a/models/log.go +++ b/models/log.go @@ -1,6 +1,25 @@ package models +import "time" + type Log struct { - ID string `db:"log_id"` - IssueID string `db:"issue_id"` + ID string `db:"log_id"` + IssueID string `db:"issue_id"` + UserID string `db:"user_id"` + IssueTaskID *string `db:"issue_task_id"` + UnitsDone *int `db:"units_done"` + Duration time.Duration `db:"duration"` + Description []string `db:"description"` +} + +type LogFilter struct { + LogIDs []string + IssueIDs []string + IssueTaskIDs []string +} + +type LogItem struct { + LogID string `db:"log_id"` + IssueItemID string `db:"issue_item_id"` + Amount int `db:"amount"` }