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