22 changed files with 883 additions and 30 deletions
-
1database/database.go
-
7database/drivers/mysqldriver/db.go
-
281database/drivers/mysqldriver/logs.go
-
14database/repositories/logrepository.go
-
3go.mod
-
11go.sum
-
11graph/resolvers/issue.resolvers.go
-
44graph/resolvers/log.resolvers.go
-
156graph/resolvers/mutation.resolvers.go
-
40graph/resolvers/query.resolvers.go
-
4graph/schema/issue.gql
-
110graph/schema/log.gql
-
6graph/schema/mutation.gql
-
5graph/schema/query.gql
-
4internal/generate/ids.go
-
17main.go
-
16migrations/mysql/20200517111706_create_table_issue_item.sql
-
17migrations/mysql/20200523151259_create_table_log.sql
-
18migrations/mysql/20200523151309_create_table_log_item.sql
-
19migrations/mysql/20200524122544_create_table_log_task.sql
-
39models/log.go
-
90services/auth.go
@ -0,0 +1,281 @@ |
|||||
|
package mysqldriver |
||||
|
|
||||
|
import ( |
||||
|
"context" |
||||
|
"database/sql" |
||||
|
"git.aiterp.net/stufflog/server/internal/generate" |
||||
|
"git.aiterp.net/stufflog/server/internal/slerrors" |
||||
|
"git.aiterp.net/stufflog/server/models" |
||||
|
sq "github.com/Masterminds/squirrel" |
||||
|
"github.com/jmoiron/sqlx" |
||||
|
) |
||||
|
|
||||
|
type logRepository struct { |
||||
|
db *sqlx.DB |
||||
|
} |
||||
|
|
||||
|
func (r *logRepository) Find(ctx context.Context, id string) (*models.Log, error) { |
||||
|
log := models.Log{} |
||||
|
err := r.db.GetContext(ctx, &log, "SELECT * FROM log WHERE log_id=?", id) |
||||
|
if err != nil { |
||||
|
if err == sql.ErrNoRows { |
||||
|
return nil, slerrors.NotFound("Log") |
||||
|
} |
||||
|
|
||||
|
return nil, err |
||||
|
} |
||||
|
|
||||
|
err = r.db.SelectContext(ctx, &log.Items, "SELECT * FROM log_item WHERE log_id=?", id) |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
err = r.db.SelectContext(ctx, &log.Tasks, "SELECT * FROM log_task WHERE log_id=?", id) |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
|
||||
|
return &log, nil |
||||
|
} |
||||
|
|
||||
|
func (r *logRepository) List(ctx context.Context, filter models.LogFilter) ([]*models.Log, error) { |
||||
|
q := sq.Select("log.*").From("log").GroupBy("log.log_id").OrderBy("log.date") |
||||
|
if len(filter.IssueItemIDs) > 0 || len(filter.IssueIDs) > 0 { |
||||
|
q = q.LeftJoin("log_item ON log_item.log_id = log.log_id") |
||||
|
} |
||||
|
if len(filter.IssueTaskIDs) > 0 || len(filter.IssueIDs) > 0 { |
||||
|
q = q.LeftJoin("log_task ON log_task.log_id = log.log_id") |
||||
|
} |
||||
|
if len(filter.IssueIDs) > 0 { |
||||
|
q = q.Where(sq.Or{ |
||||
|
sq.Eq{"log_task.issue_id": filter.IssueIDs}, |
||||
|
sq.Eq{"log_item.issue_id": filter.IssueIDs}, |
||||
|
}) |
||||
|
} |
||||
|
if len(filter.IssueItemIDs) > 0 { |
||||
|
q = q.Where(sq.Eq{"log_item.issue_item_id": filter.IssueItemIDs}) |
||||
|
} |
||||
|
if len(filter.IssueTaskIDs) > 0 { |
||||
|
q = q.Where(sq.Eq{"log_task.issue_task_id": filter.IssueTaskIDs}) |
||||
|
} |
||||
|
if len(filter.LogIDs) > 0 { |
||||
|
q = q.Where(sq.Eq{"log.log_id": filter.LogIDs}) |
||||
|
} |
||||
|
if len(filter.UserIDs) > 0 { |
||||
|
q = q.Where(sq.Eq{"log.user_id": filter.UserIDs}) |
||||
|
} |
||||
|
if filter.FromDate != nil { |
||||
|
q = q.Where(sq.GtOrEq{"log.date": *filter.FromDate}) |
||||
|
} |
||||
|
if filter.ToDate != nil { |
||||
|
q = q.Where(sq.LtOrEq{"log.date": *filter.ToDate}) |
||||
|
} |
||||
|
|
||||
|
query, args, err := q.ToSql() |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
|
||||
|
results := make([]*models.Log, 0, 16) |
||||
|
err = r.db.SelectContext(ctx, &results, query, args...) |
||||
|
if err != nil { |
||||
|
if err == sql.ErrNoRows { |
||||
|
return []*models.Log{}, nil |
||||
|
} |
||||
|
|
||||
|
return nil, err |
||||
|
} |
||||
|
|
||||
|
err = r.fill(ctx, results) |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
|
||||
|
return results, nil |
||||
|
} |
||||
|
|
||||
|
func (r *logRepository) Insert(ctx context.Context, log models.Log) (*models.Log, error) { |
||||
|
tx, err := r.db.BeginTxx(ctx, nil) |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
|
||||
|
log.ID = generate.LogID() |
||||
|
|
||||
|
_, err = tx.NamedExecContext(ctx, ` |
||||
|
INSERT INTO log ( |
||||
|
log_id, user_id, date, description |
||||
|
) VALUES ( |
||||
|
:log_id, :user_id, :date, :description |
||||
|
) |
||||
|
`, log) |
||||
|
if err != nil { |
||||
|
_ = tx.Rollback() |
||||
|
return nil, err |
||||
|
} |
||||
|
|
||||
|
for _, item := range log.Items { |
||||
|
item.LogID = log.ID |
||||
|
_, err = tx.NamedExecContext(ctx, ` |
||||
|
INSERT INTO log_item ( |
||||
|
log_id, issue_id, issue_item_id, amount |
||||
|
) VALUES ( |
||||
|
:log_id, :issue_id, :issue_item_id, :amount |
||||
|
) |
||||
|
`, item) |
||||
|
if err != nil { |
||||
|
_ = tx.Rollback() |
||||
|
return nil, err |
||||
|
} |
||||
|
} |
||||
|
for _, task := range log.Tasks { |
||||
|
task.LogID = log.ID |
||||
|
_, err = tx.NamedExecContext(ctx, ` |
||||
|
INSERT INTO log_task ( |
||||
|
log_id, issue_id, issue_task_id, units, duration |
||||
|
) VALUES ( |
||||
|
:log_id, :issue_id, :issue_task_id, :units, :duration |
||||
|
) |
||||
|
`, task) |
||||
|
if err != nil { |
||||
|
_ = tx.Rollback() |
||||
|
return nil, err |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
err = tx.Commit() |
||||
|
if err != nil { |
||||
|
_ = tx.Rollback() |
||||
|
return nil, err |
||||
|
} |
||||
|
|
||||
|
return &log, nil |
||||
|
} |
||||
|
|
||||
|
func (r *logRepository) Save(ctx context.Context, log models.Log) error { |
||||
|
tx, err := r.db.BeginTxx(ctx, nil) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
_, err = tx.NamedExecContext(ctx, ` |
||||
|
UPDATE log SET |
||||
|
date=:date, |
||||
|
description=:description |
||||
|
WHERE log_id=:log_id |
||||
|
`, log) |
||||
|
if err != nil { |
||||
|
_ = tx.Rollback() |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
_, err = tx.ExecContext(ctx, "DELETE FROM log_item WHERE log_id=?", log.ID) |
||||
|
if err != nil { |
||||
|
_ = tx.Rollback() |
||||
|
return err |
||||
|
} |
||||
|
_, err = tx.ExecContext(ctx, "DELETE FROM log_task WHERE log_id=?", log.ID) |
||||
|
if err != nil { |
||||
|
_ = tx.Rollback() |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
for _, item := range log.Items { |
||||
|
_, err = tx.NamedExecContext(ctx, ` |
||||
|
INSERT INTO log_item ( |
||||
|
log_id, issue_id, issue_item_id, amount |
||||
|
) VALUES ( |
||||
|
:log_id, :issue_id, :issue_item_id, :amount |
||||
|
) |
||||
|
`, item) |
||||
|
if err != nil { |
||||
|
_ = tx.Rollback() |
||||
|
return err |
||||
|
} |
||||
|
} |
||||
|
for _, task := range log.Tasks { |
||||
|
_, err = tx.NamedExecContext(ctx, ` |
||||
|
INSERT INTO log_task ( |
||||
|
log_id, issue_id, issue_task_id, units, duration |
||||
|
) VALUES ( |
||||
|
:log_id, :issue_id, :issue_task_id, :units, :duration |
||||
|
) |
||||
|
`, task) |
||||
|
if err != nil { |
||||
|
_ = tx.Rollback() |
||||
|
return err |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
err = tx.Commit() |
||||
|
if err != nil { |
||||
|
_ = tx.Rollback() |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func (r *logRepository) Delete(ctx context.Context, log models.Log) error { |
||||
|
tx, err := r.db.BeginTxx(ctx, nil) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
_, err = tx.ExecContext(ctx, "DELETE FROM log WHERE log_id=?", log.ID) |
||||
|
if err != nil { |
||||
|
_ = tx.Rollback() |
||||
|
return err |
||||
|
} |
||||
|
_, err = tx.ExecContext(ctx, "DELETE FROM log_item WHERE log_id=?", log.ID) |
||||
|
if err != nil { |
||||
|
_ = tx.Rollback() |
||||
|
return err |
||||
|
} |
||||
|
_, err = tx.ExecContext(ctx, "DELETE FROM log_task WHERE log_id=?", log.ID) |
||||
|
if err != nil { |
||||
|
_ = tx.Rollback() |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
return tx.Commit() |
||||
|
} |
||||
|
|
||||
|
func (r *logRepository) fill(ctx context.Context, logs []*models.Log) error { |
||||
|
logMap := make(map[string]int, len(logs)) |
||||
|
ids := make([]string, len(logs)) |
||||
|
for i, log := range logs { |
||||
|
ids[i] = log.ID |
||||
|
logMap[log.ID] = i |
||||
|
} |
||||
|
|
||||
|
itemsQuery, itemsArgs, err := sq.Select("*").From("log_item").Where(sq.Eq{"log_id": ids}).ToSql() |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
tasksQuery, tasksArgs, err := sq.Select("*").From("log_task").Where(sq.Eq{"log_id": ids}).ToSql() |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
items := make([]models.LogItem, 0, len(logs)*3) |
||||
|
err = r.db.SelectContext(ctx, &items, itemsQuery, itemsArgs...) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
for _, item := range items { |
||||
|
log := logs[logMap[item.LogID]] |
||||
|
log.Items = append(log.Items, item) |
||||
|
} |
||||
|
|
||||
|
tasks := make([]models.LogTask, 0, len(logs)*3) |
||||
|
err = r.db.SelectContext(ctx, &tasks, tasksQuery, tasksArgs...) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
for _, task := range tasks { |
||||
|
log := logs[logMap[task.LogID]] |
||||
|
log.Tasks = append(log.Tasks, task) |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
} |
@ -0,0 +1,14 @@ |
|||||
|
package repositories |
||||
|
|
||||
|
import ( |
||||
|
"context" |
||||
|
"git.aiterp.net/stufflog/server/models" |
||||
|
) |
||||
|
|
||||
|
type LogRepository interface { |
||||
|
Find(ctx context.Context, id string) (*models.Log, error) |
||||
|
List(ctx context.Context, filter models.LogFilter) ([]*models.Log, error) |
||||
|
Insert(ctx context.Context, log models.Log) (*models.Log, error) |
||||
|
Save(ctx context.Context, log models.Log) error |
||||
|
Delete(ctx context.Context, log models.Log) error |
||||
|
} |
@ -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" |
||||
|
|
||||
|
"git.aiterp.net/stufflog/server/graph/graphcore" |
||||
|
"git.aiterp.net/stufflog/server/models" |
||||
|
) |
||||
|
|
||||
|
func (r *logResolver) User(ctx context.Context, obj *models.Log) (*models.User, error) { |
||||
|
return r.Database.Users().Find(ctx, obj.UserID) |
||||
|
} |
||||
|
|
||||
|
func (r *logItemResolver) Issue(ctx context.Context, obj *models.LogItem) (*models.Issue, error) { |
||||
|
return r.Database.Issues().Find(ctx, obj.IssueID) |
||||
|
} |
||||
|
|
||||
|
func (r *logItemResolver) Item(ctx context.Context, obj *models.LogItem) (*models.IssueItem, error) { |
||||
|
return r.Database.IssueItems().Find(ctx, obj.IssueItemID) |
||||
|
} |
||||
|
|
||||
|
func (r *logTaskResolver) Issue(ctx context.Context, obj *models.LogTask) (*models.Issue, error) { |
||||
|
return r.Database.Issues().Find(ctx, obj.IssueID) |
||||
|
} |
||||
|
|
||||
|
func (r *logTaskResolver) Task(ctx context.Context, obj *models.LogTask) (*models.IssueTask, error) { |
||||
|
return r.Database.IssueTasks().Find(ctx, obj.IssueTaskID) |
||||
|
} |
||||
|
|
||||
|
// Log returns graphcore.LogResolver implementation.
|
||||
|
func (r *Resolver) Log() graphcore.LogResolver { return &logResolver{r} } |
||||
|
|
||||
|
// LogItem returns graphcore.LogItemResolver implementation.
|
||||
|
func (r *Resolver) LogItem() graphcore.LogItemResolver { return &logItemResolver{r} } |
||||
|
|
||||
|
// LogTask returns graphcore.LogTaskResolver implementation.
|
||||
|
func (r *Resolver) LogTask() graphcore.LogTaskResolver { return &logTaskResolver{r} } |
||||
|
|
||||
|
type logResolver struct{ *Resolver } |
||||
|
type logItemResolver struct{ *Resolver } |
||||
|
type logTaskResolver struct{ *Resolver } |
@ -0,0 +1,110 @@ |
|||||
|
""" |
||||
|
A log is a chunk of logged work, on one or more issues. |
||||
|
""" |
||||
|
type Log { |
||||
|
"The log's ID." |
||||
|
id: String! |
||||
|
"When the log is taking place." |
||||
|
date: Time! |
||||
|
"A description of the log." |
||||
|
description: String! |
||||
|
|
||||
|
"The user that logged the work." |
||||
|
user: User! |
||||
|
"The tasks logged." |
||||
|
tasks: [LogTask!]! |
||||
|
"The items changed." |
||||
|
items: [LogItem!]! |
||||
|
} |
||||
|
|
||||
|
type LogTask { |
||||
|
"Parent issue of the task." |
||||
|
issue: Issue! |
||||
|
"The issue task logged." |
||||
|
task: IssueTask! |
||||
|
"How many units of work is done, if applicable." |
||||
|
units: Int |
||||
|
"Time spent on the issue." |
||||
|
duration: Duration! |
||||
|
} |
||||
|
|
||||
|
""" |
||||
|
Log items are item changes related to a log. |
||||
|
""" |
||||
|
type LogItem { |
||||
|
"Parent issue of the item." |
||||
|
issue: Issue! |
||||
|
"The item that has been acquired." |
||||
|
item: IssueItem! |
||||
|
"The amount of items acquired." |
||||
|
amount: Int! |
||||
|
} |
||||
|
|
||||
|
"Filter for the logs query." |
||||
|
input LogFilter { |
||||
|
"Log IDs to select." |
||||
|
logIds: [String!] |
||||
|
"Limit to the user IDs." |
||||
|
userIds: [String!] |
||||
|
"The issue IDs to limit to." |
||||
|
issueIds: [String!] |
||||
|
"The issue task IDs to limit to." |
||||
|
issueTaskIds: [String!] |
||||
|
"The issue item IDs to limit to." |
||||
|
issueItemIds: [String!] |
||||
|
"Earliest date to get logs from (inclusive)." |
||||
|
fromDate: Time |
||||
|
"Latest date to get logs from (inclusive)." |
||||
|
toDate: Time |
||||
|
} |
||||
|
|
||||
|
"Input for the createLog mutation." |
||||
|
input LogCreateInput { |
||||
|
"When did it take place." |
||||
|
date: Time! |
||||
|
"Describe the logged work." |
||||
|
description: String! |
||||
|
|
||||
|
"Add issue items to the log." |
||||
|
items: [LogItemInput!] |
||||
|
"Add issue tasks to the log." |
||||
|
tasks: [LogTaskInput!] |
||||
|
} |
||||
|
|
||||
|
"Input for the editLog mutation." |
||||
|
input LogEditInput { |
||||
|
"The log to update." |
||||
|
logId: String! |
||||
|
|
||||
|
"Update the time of the log." |
||||
|
setDate: Time |
||||
|
"Update the description of the log." |
||||
|
setDescription: String |
||||
|
|
||||
|
"Add/update one or more items to the log." |
||||
|
updateItems: [LogItemInput!] |
||||
|
"Remove one or more items from the log." |
||||
|
removeItems: [String!] |
||||
|
"Add/update one or more items to the log." |
||||
|
updateTasks: [LogTaskInput!] |
||||
|
"Remove one or more items from the log." |
||||
|
removeTasks: [String!] |
||||
|
} |
||||
|
|
||||
|
"Sub-input for the createLog and editLog mutation." |
||||
|
input LogItemInput { |
||||
|
"The issue item to log ID." |
||||
|
issueItemId: String! |
||||
|
"The amount of items acquired." |
||||
|
amount: Int! |
||||
|
} |
||||
|
|
||||
|
"Sub-input for the createLog and editLog mutation." |
||||
|
input LogTaskInput { |
||||
|
"The issue item to log ID." |
||||
|
issueTaskId: String! |
||||
|
"The amount of units done, if applicable." |
||||
|
units: Int |
||||
|
"How long did it take?" |
||||
|
duration: Duration! |
||||
|
} |
@ -0,0 +1,17 @@ |
|||||
|
-- +goose Up |
||||
|
-- +goose StatementBegin |
||||
|
CREATE TABLE log ( |
||||
|
log_id CHAR(24) NOT NULL PRIMARY KEY, |
||||
|
user_id CHAR(32) NOT NULL, |
||||
|
date TIMESTAMP NOT NULL, |
||||
|
description TEXT NOT NULL, |
||||
|
|
||||
|
INDEX (user_id), |
||||
|
INDEX (date) |
||||
|
); |
||||
|
-- +goose StatementEnd |
||||
|
|
||||
|
-- +goose Down |
||||
|
-- +goose StatementBegin |
||||
|
DROP TABLE log; |
||||
|
-- +goose StatementEnd |
@ -0,0 +1,18 @@ |
|||||
|
-- +goose Up |
||||
|
-- +goose StatementBegin |
||||
|
CREATE TABLE log_item ( |
||||
|
log_id CHAR(24) NOT NULL, |
||||
|
issue_id CHAR(32) NOT NULL, |
||||
|
issue_item_id CHAR(48) NOT NULL, |
||||
|
amount INTEGER NOT NULL, |
||||
|
|
||||
|
PRIMARY KEY(log_id, issue_item_id), |
||||
|
INDEX (issue_item_id), |
||||
|
INDEX (issue_id) |
||||
|
); |
||||
|
-- +goose StatementEnd |
||||
|
|
||||
|
-- +goose Down |
||||
|
-- +goose StatementBegin |
||||
|
DROP TABLE log_item; |
||||
|
-- +goose StatementEnd |
@ -0,0 +1,19 @@ |
|||||
|
-- +goose Up |
||||
|
-- +goose StatementBegin |
||||
|
CREATE TABLE log_task ( |
||||
|
log_id CHAR(24) NOT NULL, |
||||
|
issue_id CHAR(32) NOT NULL, |
||||
|
issue_task_id CHAR(48) NOT NULL, |
||||
|
units INTEGER, |
||||
|
duration BIGINT NOT NULL, |
||||
|
|
||||
|
PRIMARY KEY(log_id, issue_task_id), |
||||
|
INDEX (issue_task_id), |
||||
|
INDEX (issue_id) |
||||
|
); |
||||
|
-- +goose StatementEnd |
||||
|
|
||||
|
-- +goose Down |
||||
|
-- +goose StatementBegin |
||||
|
DROP TABLE log_task; |
||||
|
-- +goose StatementEnd |
Write
Preview
Loading…
Cancel
Save
Reference in new issue