Browse Source

add issue item stuff.

master
Gisle Aune 5 years ago
parent
commit
e2c3b53d50
  1. 1
      database/database.go
  2. 7
      database/drivers/mysqldriver/db.go
  3. 141
      database/drivers/mysqldriver/issueitems.go
  4. 14
      database/repositories/issueitemrepository.go
  5. 2
      graph/gqlgen.yml
  6. 9
      graph/resolvers/issue.resolvers.go
  7. 44
      graph/resolvers/issueitem.resolvers.go
  8. 63
      graph/resolvers/mutation.resolvers.go
  9. 70
      graph/resolvers/query.resolvers.go
  10. 2
      graph/schema/issue.gql
  11. 75
      graph/schema/issueitem.gql
  12. 4
      graph/schema/mutation.gql
  13. 5
      graph/schema/query.gql
  14. 19
      migrations/mysql/20200517111706_create_table_issue_item.sql
  15. 14
      models/issueitem.go

1
database/database.go

@ -12,6 +12,7 @@ type Database interface {
Activities() repositories.ActivityRepository
Issues() repositories.IssueRepository
IssueTasks() repositories.IssueTaskRepository
IssueItems() repositories.IssueItemRepository
Items() repositories.ItemRepository
Projects() repositories.ProjectRepository
Session() repositories.SessionRepository

7
database/drivers/mysqldriver/db.go

@ -17,6 +17,7 @@ type DB struct {
activities *activityRepository
issues *issueRepository
issueTasks *issueTaskRepository
issueItems *issueItemRepository
items *itemRepository
projects *projectRepository
sessions *sessionRepository
@ -36,6 +37,10 @@ func (db *DB) IssueTasks() repositories.IssueTaskRepository {
return db.issueTasks
}
func (db *DB) IssueItems() repositories.IssueItemRepository {
return db.issueItems
}
func (db *DB) Items() repositories.ItemRepository {
return db.items
}
@ -95,12 +100,14 @@ func Open(connectionString string) (*DB, error) {
sessions := &sessionRepository{db: db}
projectStatuses := &projectStatusRepository{db: db}
issueTasks := &issueTaskRepository{db: db}
issueItems := &issueItemRepository{db: db}
return &DB{
db: db,
activities: activities,
issues: issues,
issueTasks: issueTasks,
issueItems: issueItems,
items: items,
projects: projects,
users: users,

141
database/drivers/mysqldriver/issueitems.go

@ -0,0 +1,141 @@
package mysqldriver
import (
"context"
"database/sql"
"errors"
"fmt"
"git.aiterp.net/stufflog/server/internal/slerrors"
"git.aiterp.net/stufflog/server/models"
sq "github.com/Masterminds/squirrel"
"github.com/jmoiron/sqlx"
)
type issueItemRepository struct {
db *sqlx.DB
}
func (r *issueItemRepository) Find(ctx context.Context, id string) (*models.IssueItem, error) {
issueItem := models.IssueItem{}
err := r.db.GetContext(ctx, &issueItem, "SELECT * FROM issue_item WHERE issue_item_id=?", id)
if err != nil {
if err == sql.ErrNoRows {
return nil, slerrors.NotFound("Issue item")
}
return nil, err
}
return &issueItem, nil
}
func (r *issueItemRepository) List(ctx context.Context, filter models.IssueItemFilter) ([]*models.IssueItem, error) {
q := sq.Select("issue_item.*").From("issue_item").GroupBy("issue_item.issue_item_id")
if len(filter.IssueItemIDs) > 0 {
q = q.Where(sq.Eq{"issue_item_id": filter.IssueIDs})
}
if len(filter.IssueIDs) > 0 {
q = q.Where(sq.Eq{"issue_id": filter.IssueIDs})
}
if len(filter.IssueAssignees) > 0 || len(filter.IssueOwners) > 0 || filter.IssueMinStage != nil || filter.IssueMaxStage != nil {
q = q.Join("issue ON issue.issue_id = issue_item.issue_id")
}
if len(filter.IssueAssignees) > 0 {
q = q.Where(sq.Eq{"issue.assignee_id": filter.IssueAssignees})
}
if len(filter.IssueOwners) > 0 {
q = q.Where(sq.Eq{"issue.owner_id": filter.IssueOwners})
}
if filter.IssueMinStage != nil && filter.IssueMaxStage != nil && *filter.IssueMinStage == *filter.IssueMaxStage {
q = q.Where(sq.Eq{"issue.status_stage": *filter.IssueMinStage})
} else {
if filter.IssueMinStage != nil {
q = q.Where(sq.GtOrEq{"issue.status_stage": *filter.IssueMinStage})
}
if filter.IssueMaxStage != nil {
q = q.Where(sq.LtOrEq{"issue.status_stage": *filter.IssueMaxStage})
}
}
if len(filter.ItemIDs) > 0 {
q = q.Where(sq.Eq{"item_id": filter.IssueIDs})
}
if len(filter.ItemTags) > 0 {
q = q.Join("item_tag ON item_tag.item_id = issue_item.item_id").Where(
sq.Eq{"item_tag.tag": filter.ItemTags},
)
}
if filter.Acquired != nil {
q = q.Where(sq.Eq{"acquired": *filter.Acquired})
}
query, args, err := q.ToSql()
if err != nil {
return nil, err
}
results := make([]*models.IssueItem, 0, 16)
err = r.db.SelectContext(ctx, &results, query, args...)
if err != nil {
if err == sql.ErrNoRows {
return []*models.IssueItem{}, nil
}
return nil, err
}
return results, nil
}
func (r *issueItemRepository) Insert(ctx context.Context, item models.IssueItem) (*models.IssueItem, error) {
if item.IssueID == "" {
return nil, errors.New("missing issue id")
}
tx, err := r.db.BeginTxx(ctx, nil)
if err != nil {
return nil, err
}
nextID, err := incCounter(ctx, tx, counterKindIssueSubID, item.IssueID)
if err != nil {
_ = tx.Rollback()
return nil, err
}
item.ID = fmt.Sprintf("%s-%d", item.IssueID, nextID)
_, err = tx.NamedExecContext(ctx, `
INSERT INTO issue_item (
issue_item_id, issue_id, item_id, quantity, acquired
) VALUES (
:issue_item_id, :issue_id, :item_id, :quantity, :acquired
);
`, item)
if err != nil {
_ = tx.Rollback()
return nil, err
}
err = tx.Commit()
if err != nil {
_ = tx.Rollback()
return nil, err
}
return &item, nil
}
func (r *issueItemRepository) Save(ctx context.Context, item models.IssueItem) error {
_, err := r.db.NamedExecContext(ctx, `
UPDATE issue_item SET
acquired=:acquired,
quantity=:quantity
WHERE issue_item_id=:issue_item_id
`, item)
return err
}
func (r *issueItemRepository) Delete(ctx context.Context, item models.IssueItem) error {
_, err := r.db.ExecContext(ctx, "DELETE FROM issue_item WHERE issue_item_id=? LIMIT 1;", item.ID)
return err
}

14
database/repositories/issueitemrepository.go

@ -0,0 +1,14 @@
package repositories
import (
"context"
"git.aiterp.net/stufflog/server/models"
)
type IssueItemRepository interface {
Find(ctx context.Context, id string) (*models.IssueItem, error)
List(ctx context.Context, filter models.IssueItemFilter) ([]*models.IssueItem, error)
Insert(ctx context.Context, item models.IssueItem) (*models.IssueItem, error)
Save(ctx context.Context, item models.IssueItem) error
Delete(ctx context.Context, item models.IssueItem) error
}

2
graph/gqlgen.yml

@ -20,6 +20,8 @@ models:
resolver: true
Duration:
model: git.aiterp.net/stufflog/server/graph/scalars.Duration
IssueIssueItemFilter:
model: git.aiterp.net/stufflog/server/models.IssueItemFilter
resolver:
layout: follow-schema

9
graph/resolvers/issue.resolvers.go

@ -85,6 +85,15 @@ func (r *issueResolver) Tasks(ctx context.Context, obj *models.Issue, filter *mo
return r.Database.IssueTasks().List(ctx, *filter)
}
func (r *issueResolver) Items(ctx context.Context, obj *models.Issue, filter *models.IssueItemFilter) ([]*models.IssueItem, error) {
if filter == nil {
filter = &models.IssueItemFilter{}
}
filter.IssueIDs = []string{obj.ID}
return r.Database.IssueItems().List(ctx, *filter)
}
// Issue returns graphcore.IssueResolver implementation.
func (r *Resolver) Issue() graphcore.IssueResolver { return &issueResolver{r} }

44
graph/resolvers/issueitem.resolvers.go

@ -0,0 +1,44 @@
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"
"fmt"
"git.aiterp.net/stufflog/server/graph/graphcore"
"git.aiterp.net/stufflog/server/models"
)
func (r *issueItemResolver) Issue(ctx context.Context, obj *models.IssueItem) (*models.Issue, error) {
return r.Database.Issues().Find(ctx, obj.IssueID)
}
func (r *issueItemResolver) Item(ctx context.Context, obj *models.IssueItem) (*models.Item, error) {
return r.Database.Items().Find(ctx, obj.ItemID)
}
func (r *issueItemResolver) Remaining(ctx context.Context, obj *models.IssueItem) (int, error) {
if obj.Acquired {
return 0, nil
}
// TODO: Use logs
return obj.Quantity, nil
}
// IssueItem returns graphcore.IssueItemResolver implementation.
func (r *Resolver) IssueItem() graphcore.IssueItemResolver { return &issueItemResolver{r} }
type issueItemResolver struct{ *Resolver }
// !!! WARNING !!!
// The code below was going to be deleted when updating resolvers. It has been copied here so you have
// one last chance to move it out of harms way if you want. There are two reasons this happens:
// - When renaming or deleting a resolver the old code will be put in here. You can safely delete
// it when you're done.
// - You have helper methods in this file. Move them out to keep these resolver files clean.
func (r *issueItemResolver) Quanity(ctx context.Context, obj *models.IssueItem) (int, error) {
panic(fmt.Errorf("not implemented"))
}

63
graph/resolvers/mutation.resolvers.go

@ -412,6 +412,69 @@ func (r *mutationResolver) EditIssueTask(ctx context.Context, input graphcore.Is
return task, nil
}
func (r *mutationResolver) CreateIssueItem(ctx context.Context, input graphcore.IssueItemCreateInput) (*models.IssueItem, error) {
user := r.Auth.UserFromContext(ctx)
if user == nil {
return nil, slerrors.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, slerrors.PermissionDenied
}
item, err := r.Database.Items().Find(ctx, input.ItemID)
if err != nil {
return nil, err
}
issueItem := &models.IssueItem{
IssueID: issue.ID,
ItemID: item.ID,
Quantity: input.Quanitty,
Acquired: input.Acquired != nil && *input.Acquired,
}
return r.Database.IssueItems().Insert(ctx, *issueItem)
}
func (r *mutationResolver) EditIssueItem(ctx context.Context, input graphcore.IssueItemEditInput) (*models.IssueItem, error) {
user := r.Auth.UserFromContext(ctx)
if user == nil {
return nil, slerrors.PermissionDenied
}
item, err := r.Database.IssueItems().Find(ctx, input.IssueItemID)
if err != nil {
return nil, err
}
issue, err := r.Database.Issues().Find(ctx, item.IssueID)
if err != nil {
return nil, err
}
if perm, err := r.Auth.IssuePermission(ctx, *issue); err != nil || !perm.CanManageOwnIssue() {
return nil, slerrors.PermissionDenied
}
if input.SetAcquired != nil {
item.Acquired = *input.SetAcquired
}
if input.SetQuanitty != nil {
item.Quantity = *input.SetQuanitty
}
err = r.Database.IssueItems().Save(ctx, *item)
if err != nil {
return nil, err
}
return item, nil
}
func (r *mutationResolver) LoginUser(ctx context.Context, input graphcore.UserLoginInput) (*models.User, error) {
return r.Auth.Login(ctx, input.Username, input.Password)
}

70
graph/resolvers/query.resolvers.go

@ -89,6 +89,76 @@ func (r *queryResolver) ItemTags(ctx context.Context) ([]string, error) {
return r.Database.Items().GetTags(ctx)
}
func (r *queryResolver) IssueItem(ctx context.Context, id string) (*models.IssueItem, error) {
user := r.Auth.UserFromContext(ctx)
if user == nil {
return nil, slerrors.PermissionDenied
}
item, err := r.Database.IssueItems().Find(ctx, id)
if err != nil {
return nil, err
}
issue, err := r.Database.Issues().Find(ctx, item.IssueID)
if err != nil {
return nil, err
}
_, err = r.Auth.IssuePermission(ctx, *issue)
if err != nil {
return nil, err
}
return item, nil
}
func (r *queryResolver) IssueItems(ctx context.Context, filter *models.IssueItemFilter) ([]*models.IssueItem, error) {
user := r.Auth.UserFromContext(ctx)
if user == nil {
return nil, slerrors.PermissionDenied
}
if filter == nil {
filter = &models.IssueItemFilter{}
}
items, err := r.Database.IssueItems().List(ctx, *filter)
if err != nil {
return nil, err
}
accessMap := make(map[string]bool)
deleteList := make([]int, 0, len(items))
for i, item := range items {
if access, ok := accessMap[item.IssueID]; ok && access {
continue
} else if ok && !access {
deleteList = append(deleteList, i-len(deleteList))
continue
}
issue, err := r.Database.Issues().Find(ctx, item.IssueID)
if err != nil {
deleteList = append(deleteList, i-len(deleteList))
accessMap[item.IssueID] = true
continue
}
_, err = r.Auth.IssuePermission(ctx, *issue)
if err != nil {
deleteList = append(deleteList, i-len(deleteList))
}
accessMap[issue.ID] = err != nil
}
for _, index := range deleteList {
items = append(items[:index], items[index+1:]...)
}
return items, nil
}
func (r *queryResolver) Project(ctx context.Context, id string) (*models.Project, error) {
user := r.Auth.UserFromContext(ctx)
if user == nil {

2
graph/schema/issue.gql

@ -24,6 +24,8 @@ type Issue {
status: ProjectStatus!
"Issue tasks."
tasks(filter: IssueTaskFilter): [IssueTask!]!
"Issue items."
items(filter: IssueIssueItemFilter): [IssueItem!]!
#"Logs related to this issue."
#logs: [Log!]!
}

75
graph/schema/issueitem.gql

@ -0,0 +1,75 @@
"""
An issue item is a requirement of an item under an issue.
"""
type IssueItem {
"ID of the issue item listing."
id: String!
"The amount of the item associated with an issue."
quantity: Int!
"Whether the full quantity of item has been acquired."
acquired: Boolean!
"Parent issue of the issue item."
issue: Issue!
"The item associated with the issue."
item: Item!
"The amount of items remaining."
remaining: Int!
}
"Input for the items query."
input IssueItemFilter {
"Filter to only these IDs, used primarily by IDs."
issueItemIds: [String!]
"Filter to only these issues."
issueIds: [String!]
"Filter to only issues where these are the asignees."
issueAssignees: [String!]
"Filter to only issues where these are the owners."
issueOwners: [String!]
"Filter by issue minimum stage (inclusive)."
issueMinStage: Int
"Filter by issue maximum stage (inclusive)."
issueMaxStage: Int
"Filter to only list issue items with these items."
itemIds: [String!]
"Filter to only list issue items where the item has these tags."
itemTags: [String!]
"Only listed acquired or non-acquired items."
acquired: Boolean
}
"Input for the items query."
input IssueIssueItemFilter {
"Filter to only these IDs, used primarily by IDs."
issueItemIds: [String!]
"Filter to only list issue items with these items."
itemIds: [String!]
"Filter to only list issue items where the item has these tags."
itemTags: [String!]
"Only listed acquired or non-acquired items."
acquired: Boolean
}
"Input for the createIssueItem mutation."
input IssueItemCreateInput {
"Parent issue."
issueId: String!
"Item to associate with."
itemId: String!
"Quantity of the item."
quanitty: Int!
"Whether the item has already been acquired."
acquired: Boolean
}
"Input for the editIssueItem mutation."
input IssueItemEditInput {
"The ID of the issue item to edit."
issueItemId: String!
"Update the quantity of the item."
setQuanitty: Int
"Update whether the item has been acquired."
setAcquired: Boolean
}

4
graph/schema/mutation.gql

@ -24,6 +24,10 @@ type Mutation {
"Edit an issue task."
editIssueTask(input: IssueTaskEditInput!): IssueTask!
# ISSUE ITEM
"Create an issue item."
createIssueItem(input: IssueItemCreateInput!): IssueItem!
"Edit an issue item."
editIssueItem(input: IssueItemEditInput!): IssueItem!
# LOG

5
graph/schema/query.gql

@ -11,6 +11,11 @@ type Query {
"List item tags."
itemTags: [String!]!
"Find issue item."
issueItem(id: String!): IssueItem!
"List issue items."
issueItems(filter: IssueItemFilter): [IssueItem!]!
"Find project."
project(id: String!): Project!
"List projects."

19
migrations/mysql/20200517111706_create_table_issue_item.sql

@ -0,0 +1,19 @@
-- +goose Up
-- +goose StatementBegin
CREATE TABLE issue_item (
issue_item_id CHAR(48) NOT NULL PRIMARY KEY,
issue_id CHAR(32) NOT NULL,
item_id CHAR(32) NOT NULL,
quantity INTEGER NOT NULL,
acquired BOOLEAN NOT NULL,
INDEX (acquired),
INDEX (issue_id),
INDEX (item_id)
);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP TABLE issue_item;
-- +goose StatementEnd

14
models/issueitem.go

@ -6,5 +6,17 @@ type IssueItem struct {
IssueID string `db:"issue_id"`
ItemID string `db:"item_id"`
Quantity int `db:"quantity"`
Acquired bool `db:"resolved"`
Acquired bool `db:"acquired"`
}
type IssueItemFilter struct {
IssueItemIDs []string
IssueIDs []string
IssueAssignees []string
IssueOwners []string
IssueMinStage *int
IssueMaxStage *int
ItemIDs []string
ItemTags []string
Acquired *bool
}
Loading…
Cancel
Save