> be fist commit

This commit is contained in:
2020-04-13 23:38:41 +02:00
commit 4cce632708
69 changed files with 7852 additions and 0 deletions
+103
View File
@@ -0,0 +1,103 @@
package mysqldriver
import (
"context"
"database/sql"
"git.aiterp.net/stufflog/server/database/repositories"
"github.com/jmoiron/sqlx"
"github.com/pressly/goose"
"strings"
// Mysql Driver
_ "github.com/go-sql-driver/mysql"
)
type DB struct {
db *sqlx.DB
issues *issueRepository
items *itemRepository
projects *projectRepository
sessions *sessionRepository
users *userRepository
}
func (db *DB) Issues() repositories.IssueRepository {
return db.issues
}
func (db *DB) Items() repositories.ItemRepository {
return db.items
}
func (db *DB) Projects() repositories.ProjectRepository {
return db.projects
}
func (db *DB) Session() repositories.SessionRepository {
return db.sessions
}
func (db *DB) Users() repositories.UserRepository {
return db.users
}
func (db *DB) Migrate() error {
err := goose.SetDialect("mysql")
if err != nil {
return err
}
return goose.Up(db.db.DB, "migrations/mysql")
}
func Open(connectionString string) (*DB, error) {
// Ensure parseTime is true
qpos := strings.LastIndexByte(connectionString, '?')
if qpos != -1 {
connectionString += "&parseTime=true"
} else {
connectionString += "?parseTime=true"
}
// Connect to the database
db, err := sqlx.Connect("mysql", connectionString)
if err != nil {
return nil, err
}
// Test the connection
err = db.Ping()
if err != nil {
return nil, err
}
issues := &issueRepository{db: db}
items := &itemRepository{db: db}
projects := &projectRepository{db: db}
users := &userRepository{db: db}
return &DB{
db: db,
issues: issues,
items: items,
projects: projects,
users: users,
}, nil
}
func incCounter(ctx context.Context, tx *sqlx.Tx, kind, name string) (int, error) {
value := 1
err := tx.GetContext(ctx, &value, `
SELECT value FROM counters WHERE kind=? AND name=? FOR UPDATE
`, kind, name)
if err != nil && err != sql.ErrNoRows {
return -1, err
}
_, err = tx.ExecContext(ctx, "REPLACE INTO counters (kind, name, value) VALUES (?, ?, ?)", kind, name, value+1)
if err != nil {
return -1, err
}
return value, nil
}
+78
View File
@@ -0,0 +1,78 @@
package mysqldriver
import (
"github.com/pressly/goose"
"log"
"os"
"testing"
"time"
)
var testDB *DB
func TestMain(m *testing.M) {
testDbConnect := os.Getenv("DB_TEST_CONNECT")
if testDbConnect == "" {
testDbConnect = "xiaoli_test:test1234@(localhost:3306)/xiaoli_test"
}
db, err := Open(testDbConnect)
if err != nil {
log.Println("DB ERROR", err)
os.Exit(1)
return
}
err = goose.SetDialect("mysql")
if err != nil {
log.Println("GOOSE ERROR", err)
os.Exit(1)
return
}
for {
err = goose.Down(db.db.DB, "../../../migrations/mysql")
if err != nil {
break
}
}
err = goose.Up(db.db.DB, "../../../migrations/mysql")
if err != nil && err != goose.ErrNoNextVersion && err != goose.ErrNoCurrentVersion {
log.Println("UP ERROR", err)
os.Exit(3)
return
}
testDB = db
code := m.Run()
os.Exit(code)
}
func clearTable(tableName string) error {
// If you really want to SQL inject the test DB, go right ahead!
_, err := testDB.db.Exec("DELETE FROM " + tableName + " WHERE TRUE")
return err
}
func mustParseTime(str string) time.Time {
d, err := time.Parse(time.RFC3339Nano, str)
if err != nil {
panic(err)
}
return d.UTC()
}
func ptrInt(v int) *int {
return &v
}
func ptrString(v string) *string {
return &v
}
func ptrBool(v bool) *bool {
return &v
}
+169
View File
@@ -0,0 +1,169 @@
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"
)
var counterKindIssueID = "NextIssueID"
type issueRepository struct {
db *sqlx.DB
}
func (r *issueRepository) Find(ctx context.Context, id string) (*models.Issue, error) {
issue := models.Issue{}
err := r.db.GetContext(ctx, &issue, "SELECT * FROM issue WHERE issue_id=?", id)
if err != nil {
if err == sql.ErrNoRows {
return nil, xlerrors.NotFound("Issue")
}
return nil, err
}
return &issue, nil
}
func (r *issueRepository) List(ctx context.Context, filter models.IssueFilter) ([]*models.Issue, error) {
q := sq.Select("*").From("issue").OrderBy("updated_time DESC")
if len(filter.IssueIDs) > 0 {
q = q.Where(sq.Eq{"issue_id": filter.IssueIDs})
}
if len(filter.ProjectIDs) > 0 {
q = q.Where(sq.Eq{"project_id": filter.ProjectIDs})
}
if len(filter.OwnerIDs) > 0 {
q = q.Where(sq.Eq{"owner_id": filter.OwnerIDs})
}
if len(filter.AssigneeIDs) > 0 {
q = q.Where(sq.Eq{"assignee_id": filter.AssigneeIDs})
}
if filter.Search != nil && *filter.Search != "" {
q = q.Where("MATCH (name, title, description) AGAINST (?)", *filter.Search)
}
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})
}
if filter.Limit != nil && *filter.Limit > 0 {
q = q.Limit(uint64(*filter.Limit))
}
query, args, err := q.ToSql()
if err != nil {
return nil, err
}
results := make([]*models.Issue, 0, 16)
err = r.db.SelectContext(ctx, &results, query, args...)
if err != nil {
if err == sql.ErrNoRows {
return []*models.Issue{}, nil
}
return nil, err
}
return results, nil
}
func (r *issueRepository) Insert(ctx context.Context, issue models.Issue) (*models.Issue, error) {
if issue.ProjectID == "" {
return nil, errors.New("missing project id")
}
if issue.CreatedTime.IsZero() {
issue.CreatedTime = time.Now().Truncate(time.Second)
issue.UpdatedTime = issue.CreatedTime
}
tx, err := r.db.BeginTxx(ctx, nil)
if err != nil {
return nil, err
}
nextID, err := incCounter(ctx, tx, counterKindIssueID, issue.ProjectID)
if err != nil {
_ = tx.Rollback()
return nil, err
}
issue.ID = fmt.Sprintf("%s-%d", issue.ProjectID, nextID)
_, err = tx.NamedExecContext(ctx, `
INSERT INTO issue (
issue_id, project_id, owner_id, assignee_id,
status_stage, status_name, created_time,
updated_time, due_time, name, title, description
) VALUES (
:issue_id, :project_id, :owner_id, :assignee_id,
:status_stage, :status_name, :created_time,
:updated_time, :due_time, :name, :title, :description
)
`, issue)
if err != nil {
_ = tx.Rollback()
return nil, err
}
err = tx.Commit()
if err != nil {
_ = tx.Rollback()
return nil, err
}
return &issue, nil
}
func (r *issueRepository) Save(ctx context.Context, issue models.Issue) error {
_, err := r.db.NamedExecContext(ctx, `
UPDATE issue
SET assignee_id=:assignee_id,
status_stage=:status_stage,
status_name=:status_name,
created_time=:created_time,
updated_time=:updated_time,
due_time=:due_time,
name=:name,
title=:title,
description=:description
WHERE issue_id=:issue_id
`, issue)
if err != nil {
return err
}
return nil
}
func (r *issueRepository) Delete(ctx context.Context, issue models.Issue) error {
tx, err := r.db.BeginTxx(ctx, nil)
if err != nil {
return err
}
_, err = tx.ExecContext(ctx, "DELETE FROM issue WHERE issue_id=?", issue.ID)
if err != nil {
_ = tx.Rollback()
return err
}
// TODO: delete from issue_*
err = tx.Commit()
if err != nil {
_ = tx.Rollback()
return err
}
return nil
}
+188
View File
@@ -0,0 +1,188 @@
package mysqldriver
import (
"context"
"git.aiterp.net/stufflog/server/internal/xlerrors"
"git.aiterp.net/stufflog/server/models"
"github.com/stretchr/testify/assert"
"testing"
"time"
)
var issue1 = models.Issue{
ProjectID: "STUFF",
OwnerID: "Admin",
AssigneeID: "",
StatusStage: models.IssueStageActive,
StatusName: "IN PROGRESS",
DueTime: time.Now().UTC().Add(time.Hour * 72).Truncate(time.Hour * 24).Add(time.Hour * 16),
Name: "Do Stuff",
Title: "Do some important stuff.",
Description: "Stuff and things, items and artifacts, objects and creations.",
}
var issue2 = models.Issue{
ProjectID: "MODELING",
OwnerID: "Test",
AssigneeID: "Test",
StatusStage: models.IssueStagePostponed,
StatusName: "TOO HARD",
Name: "Hard Surface Course",
Title: "Run through the Hard Surface modeling course.",
Description: "maek robit",
}
var issue3 = models.Issue{
ProjectID: "MODELING",
OwnerID: "Test",
AssigneeID: "",
StatusStage: models.IssueStagePending,
StatusName: "TO DO",
Name: "Isometric Room Scene",
Title: "Create an isometric room-box scene.",
Description: "furniture and stuff.",
}
var issue3Updated = models.Issue{
ProjectID: "MODELING",
OwnerID: "Test",
AssigneeID: "Admin",
StatusStage: models.IssueStageActive,
StatusName: "WORK IN PROGRESS",
Name: "Room Scene",
Title: "Create a room-box scene.",
Description: "THREE DIMENSIONAL DESK CLUTTER",
}
var issue4 = models.Issue{
ProjectID: "DINNER",
OwnerID: "Test",
AssigneeID: "Test",
StatusStage: models.IssueStagePending,
StatusName: "SHOPPING LIST",
DueTime: mustParseTime("2020-04-22T17:30:00.000+02:00"),
Name: "Spaghetti Carbonara",
Title: "WEDNESDAY 2020-04-22: Spaghetti Carbonara",
Description: "See shopping list",
}
func TestIssueRepository(t *testing.T) {
issues := testDB.issues
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
assert.NoError(t, clearTable("issue"))
// INSERT
result, err := issues.Insert(ctx, issue1)
assert.NoError(t, err)
assert.Equal(t, "STUFF-1", result.ID)
if result != nil {
issue1.ID = result.ID
issue1.CreatedTime = result.CreatedTime.UTC()
issue1.UpdatedTime = result.UpdatedTime.UTC()
}
result, err = issues.Insert(ctx, issue2)
assert.NoError(t, err)
assert.Equal(t, "MODELING-1", result.ID)
if result != nil {
issue2.ID = result.ID
issue2.CreatedTime = result.CreatedTime.UTC()
issue2.UpdatedTime = result.UpdatedTime.UTC()
}
result, err = issues.Insert(ctx, issue3)
assert.NoError(t, err)
assert.Equal(t, "MODELING-2", result.ID)
if result != nil {
issue3.ID = result.ID
issue3.CreatedTime = result.CreatedTime.UTC()
issue3.UpdatedTime = result.UpdatedTime.UTC()
issue3Updated.ID = result.ID
issue3Updated.CreatedTime = result.CreatedTime.UTC()
issue3Updated.UpdatedTime = result.UpdatedTime.UTC()
}
result, err = issues.Insert(ctx, issue4)
assert.NoError(t, err)
assert.Equal(t, "DINNER-1", result.ID)
if result != nil {
issue4.ID = result.ID
issue4.CreatedTime = result.CreatedTime.UTC()
issue4.UpdatedTime = result.UpdatedTime.UTC()
}
// FIND
result, err = issues.Find(ctx, issue1.ID)
assert.NoError(t, err)
assert.Equal(t, &issue1, result)
result, err = issues.Find(ctx, issue3.ID)
assert.NoError(t, err)
assert.Equal(t, &issue3, result)
// FIND't
result, err = issues.Find(ctx, "NONEXISTENT-666")
assert.Error(t, err)
assert.Nil(t, result)
assert.True(t, xlerrors.IsNotFound(err))
// LIST
results, err := issues.List(ctx, models.IssueFilter{})
assert.NoError(t, err)
assert.Equal(t, []*models.Issue{&issue4, &issue2, &issue3, &issue1}, results)
results, err = issues.List(ctx, models.IssueFilter{
ProjectIDs: []string{"DINNER", "MODELING"},
})
assert.NoError(t, err)
assert.Equal(t, []*models.Issue{&issue4, &issue2, &issue3}, results)
results, err = issues.List(ctx, models.IssueFilter{
IssueIDs: []string{"MODELING-2", "DINNER-1"},
})
assert.NoError(t, err)
assert.Equal(t, []*models.Issue{&issue4, &issue3}, results)
results, err = issues.List(ctx, models.IssueFilter{
OwnerIDs: []string{"Test"},
})
assert.NoError(t, err)
assert.Equal(t, []*models.Issue{&issue4, &issue2, &issue3}, results)
results, err = issues.List(ctx, models.IssueFilter{
AssigneeIDs: []string{"", "Admin"},
})
assert.NoError(t, err)
assert.Equal(t, []*models.Issue{&issue3, &issue1}, results)
results, err = issues.List(ctx, models.IssueFilter{
MinStage: ptrInt(models.IssueStageActive),
MaxStage: ptrInt(models.IssueStageReview),
})
assert.NoError(t, err)
assert.Equal(t, []*models.Issue{&issue1}, results)
results, err = issues.List(ctx, models.IssueFilter{
Limit: ptrInt(2),
})
assert.NoError(t, err)
assert.Equal(t, []*models.Issue{&issue4, &issue2}, results)
results, err = issues.List(ctx, models.IssueFilter{
OwnerIDs: []string{"Admin"},
AssigneeIDs: []string{""},
Search: ptrString("stuff"),
})
assert.NoError(t, err)
assert.Equal(t, []*models.Issue{&issue1}, results)
// SAVE
issue3Updated.UpdatedTime = time.Now().UTC().Truncate(time.Second)
err = issues.Save(ctx, issue3Updated)
assert.NoError(t, err)
// FIND after SAVE
result, err = issues.Find(ctx, issue3.ID)
assert.NoError(t, err)
assert.Equal(t, &issue3Updated, result)
// DELETE
err = issues.Delete(ctx, issue4)
assert.NoError(t, err)
// LIST after DELETE and SAVE
results, err = issues.List(ctx, models.IssueFilter{})
assert.NoError(t, err)
assert.Equal(t, []*models.Issue{&issue2, &issue3Updated, &issue1}, results)
}
+215
View File
@@ -0,0 +1,215 @@
package mysqldriver
import (
"context"
"database/sql"
"git.aiterp.net/stufflog/server/internal/generate"
"git.aiterp.net/stufflog/server/internal/xlerrors"
"git.aiterp.net/stufflog/server/models"
sq "github.com/Masterminds/squirrel"
"github.com/jmoiron/sqlx"
)
type itemRepository struct {
db *sqlx.DB
}
func (r *itemRepository) Find(ctx context.Context, id string) (*models.Item, error) {
item := models.Item{}
err := r.db.GetContext(ctx, &item, "SELECT * FROM item WHERE item_id=?", id)
if err != nil {
if err == sql.ErrNoRows {
return nil, xlerrors.NotFound("Project")
}
return nil, err
}
err = r.db.SelectContext(ctx, &item.Tags, "SELECT tag FROM item_tag WHERE item_id=? ORDER BY tag", id)
if err != nil {
return nil, err
}
return &item, nil
}
func (r *itemRepository) List(ctx context.Context, filter models.ItemFilter) ([]*models.Item, error) {
q := sq.Select("item.*").From("item").OrderBy("name")
if len(filter.ItemIDs) > 0 {
q = q.Where(sq.Eq{"item_id": filter.ItemIDs})
}
if len(filter.Tags) > 0 {
q = q.LeftJoin("item_tag ON item_tag.item_id = item.item_id").
Where(sq.Eq{"item_tag.tag": filter.Tags}).
GroupBy("item.item_id")
}
query, args, err := q.ToSql()
if err != nil {
return nil, err
}
results := make([]*models.Item, 0, 16)
err = r.db.SelectContext(ctx, &results, query, args...)
if err != nil {
if err == sql.ErrNoRows {
return []*models.Item{}, nil
}
return nil, err
}
err = r.fillTags(ctx, results)
if err != nil {
return nil, err
}
return results, nil
}
func (r *itemRepository) Insert(ctx context.Context, item models.Item) (*models.Item, error) {
item.ID = generate.ItemID()
tx, err := r.db.BeginTxx(ctx, nil)
if err != nil {
return nil, err
}
_, err = tx.NamedExecContext(ctx, `
INSERT INTO item (item_id, name, description, image_url)
VALUES (:item_id, :name, :description, :image_url)
`, item)
if err != nil {
_ = tx.Rollback()
return nil, err
}
if len(item.Tags) > 0 {
q := sq.Insert("item_tag").Columns("item_id", "tag")
for _, tag := range item.Tags {
q = q.Values(item.ID, tag)
}
tagQuery, args, err := q.ToSql()
if err != nil {
_ = tx.Rollback()
return nil, err
}
_, err = r.db.ExecContext(ctx, tagQuery, args...)
if err != nil {
_ = tx.Rollback()
return nil, err
}
}
err = tx.Commit()
if err != nil {
return nil, err
}
return &item, nil
}
func (r *itemRepository) Save(ctx context.Context, item models.Item) error {
tx, err := r.db.BeginTxx(ctx, nil)
if err != nil {
return err
}
_, err = tx.NamedExecContext(ctx, `
UPDATE item
SET name=:name, description=:description, image_url=:image_url
WHERE item_id=:item_id
`, item)
if err != nil {
_ = tx.Rollback()
return err
}
_, err = r.db.ExecContext(ctx, "DELETE FROM item_tag WHERE item_id=?", item.ID)
if err != nil && err != sql.ErrNoRows {
_ = tx.Rollback()
return err
}
if len(item.Tags) > 0 {
q := sq.Insert("item_tag").Columns("item_id", "tag")
for _, tag := range item.Tags {
q = q.Values(item.ID, tag)
}
tagQuery, args, err := q.ToSql()
if err != nil {
_ = tx.Rollback()
return err
}
_, err = r.db.ExecContext(ctx, tagQuery, args...)
if err != nil {
_ = tx.Rollback()
return err
}
}
err = tx.Commit()
if err != nil {
return err
}
return nil
}
func (r *itemRepository) Delete(ctx context.Context, item models.Item) error {
_, err := r.db.ExecContext(ctx, "DELETE FROM item WHERE item_id=?", item.ID)
if err != nil {
return err
}
_, err = r.db.ExecContext(ctx, "DELETE FROM item_tag WHERE item_id=?", item.ID)
if err != nil {
return err
}
return err
}
func (r *itemRepository) GetTags(ctx context.Context) ([]string, error) {
tags := make([]string, 0, 16)
err := r.db.SelectContext(ctx, &tags, "SELECT DISTINCT(tag) FROM item_tag")
if err != nil {
return nil, err
}
return tags, nil
}
func (r *itemRepository) fillTags(ctx context.Context, items []*models.Item) error {
ids := make([]string, len(items))
idMap := make(map[string]int, len(items))
for i, item := range items {
ids[i] = item.ID
idMap[item.ID] = i
}
query, args, err := sq.Select("*").From("item_tag").Where(sq.Eq{"item_id": ids}).ToSql()
if err != nil {
return err
}
results := make([]struct {
ItemID string `db:"item_id"`
Tag string `db:"tag"`
}, 0, len(items)*4)
err = r.db.SelectContext(ctx, &results, query, args...)
if err != nil {
return err
}
for _, result := range results {
item := items[idMap[result.ItemID]]
item.Tags = append(item.Tags, result.Tag)
}
return nil
}
+142
View File
@@ -0,0 +1,142 @@
package mysqldriver
import (
"context"
"git.aiterp.net/stufflog/server/internal/xlerrors"
"git.aiterp.net/stufflog/server/models"
"github.com/stretchr/testify/assert"
"testing"
"time"
)
var item1ImageURL = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"
var item1 = models.Item{
Name: "Salmon Fillet - 500g",
Description: "Best fish",
Tags: []string{"Groceries"},
ImageURL: &item1ImageURL,
}
var item1Updated = models.Item{
Name: "Salmon Fillet - 450g",
Description: "Do not handle under suspicious circumstances.",
Tags: []string{"Groceries"},
ImageURL: nil,
}
var item2 = models.Item{
Name: "Tape - Basic",
Description: "",
Tags: []string{"Groceries", "Hardware", "Office Supplies"},
ImageURL: nil,
}
var item3 = models.Item{
Name: "Flour - Wheat - 1kg",
Description: "For bread and stuff",
Tags: []string{"Groceries"},
ImageURL: nil,
}
var item4 = models.Item{
Name: "Flour - Wheat - 2kg",
Description: "For more bread and stuff",
Tags: []string{"Groceries"},
ImageURL: nil,
}
func TestItemRepository(t *testing.T) {
items := testDB.items
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
assert.NoError(t, clearTable("item"))
assert.NoError(t, clearTable("item_tag"))
// INSERT
// IDs are random, so test data needs to be changed before comparison.
result, err := items.Insert(ctx, item1)
assert.NoError(t, err)
if result != nil {
item1.ID = result.ID
item1Updated.ID = result.ID
}
assert.Equal(t, item1, *result)
result, err = items.Insert(ctx, item2)
assert.NoError(t, err)
if result != nil {
item2.ID = result.ID
}
assert.Equal(t, item2, *result)
result, err = items.Insert(ctx, item3)
assert.NoError(t, err)
if result != nil {
item3.ID = result.ID
}
assert.Equal(t, item3, *result)
result, err = items.Insert(ctx, item4)
assert.NoError(t, err)
if result != nil {
item4.ID = result.ID
}
assert.Equal(t, item4, *result)
if t.Failed() {
return
}
// FIND
result, err = items.Find(ctx, item1.ID)
assert.NoError(t, err)
assert.Equal(t, &item1, result)
result, err = items.Find(ctx, item2.ID)
assert.NoError(t, err)
assert.Equal(t, &item2, result)
// FIND't
result, err = items.Find(ctx, "Iobviouslyinvalidid")
assert.Error(t, err)
assert.True(t, xlerrors.IsNotFound(err))
assert.Nil(t, result)
// LIST
results, err := items.List(ctx, models.ItemFilter{})
assert.NoError(t, err)
assert.Equal(t, []*models.Item{&item3, &item4, &item1, &item2}, results)
results, err = items.List(ctx, models.ItemFilter{
Tags: []string{"Hardware"},
})
assert.NoError(t, err)
assert.Equal(t, []*models.Item{&item2}, results)
results, err = items.List(ctx, models.ItemFilter{
ItemIDs: []string{item1.ID, item2.ID},
})
assert.NoError(t, err)
assert.Equal(t, []*models.Item{&item1, &item2}, results)
// UPDATE
err = items.Save(ctx, item1Updated)
assert.NoError(t, err)
// FIND after UPDATE
result, err = items.Find(ctx, item1.ID)
assert.NoError(t, err)
assert.NotEqual(t, &item1, result)
assert.Equal(t, &item1Updated, result)
// TAGS
allTags, err := items.GetTags(ctx)
assert.NoError(t, err)
assert.Equal(t, item2.Tags, allTags)
// DELETE
err = items.Delete(ctx, item2)
assert.NoError(t, err)
// LIST after DELETE
results, err = items.List(ctx, models.ItemFilter{})
assert.NoError(t, err)
assert.Equal(t, []*models.Item{&item3, &item4, &item1Updated}, results)
// TAGS after DELETE
allTags, err = items.GetTags(ctx)
assert.NoError(t, err)
assert.Equal(t, item1.Tags, allTags)
}
+139
View File
@@ -0,0 +1,139 @@
package mysqldriver
import (
"context"
"database/sql"
"errors"
"git.aiterp.net/stufflog/server/internal/xlerrors"
"git.aiterp.net/stufflog/server/models"
sq "github.com/Masterminds/squirrel"
"github.com/jmoiron/sqlx"
)
type projectRepository struct {
db *sqlx.DB
}
func (r *projectRepository) Find(ctx context.Context, id string) (*models.Project, error) {
project := models.Project{}
err := r.db.GetContext(ctx, &project, "SELECT * FROM project WHERE project_id=?", id)
if err != nil {
if err == sql.ErrNoRows {
return nil, xlerrors.NotFound("Project")
}
return nil, err
}
return &project, nil
}
func (r *projectRepository) List(ctx context.Context, filter models.ProjectFilter) ([]*models.Project, error) {
q := sq.Select("project.*").From("project").OrderBy("project_id")
if len(filter.ProjectIDs) > 0 {
q = q.Where(sq.Eq{"project_id": filter.ProjectIDs})
}
if filter.Search != nil {
q = q.Where("MATCH (name, description) AGAINST (?)", *filter.Search)
}
if filter.Permission != nil && filter.Permission.Valid() {
q = q.LeftJoin("project_permission ON project.project_id = project_permission.project_id AND project_permission.user_id = ?", filter.Permission.UserID)
if filter.Permission.MaxLevel >= filter.Permission.MinLevel {
q = q.Where(sq.And{
sq.GtOrEq{"project_permission.access_level": filter.Permission.MinLevel},
sq.LtOrEq{"project_permission.access_level": filter.Permission.MaxLevel},
})
} else {
q = q.Where(sq.GtOrEq{"project_permission.access_level": filter.Permission.MinLevel})
}
}
query, args, err := q.ToSql()
if err != nil {
return nil, err
}
results := make([]*models.Project, 0, 16)
err = r.db.SelectContext(ctx, &results, query, args...)
if err != nil {
if err == sql.ErrNoRows {
return []*models.Project{}, nil
}
return nil, err
}
return results, nil
}
func (r *projectRepository) Insert(ctx context.Context, project models.Project) (*models.Project, error) {
if !project.ValidKey() {
return nil, errors.New("invalid project id")
}
_, err := r.db.NamedExecContext(ctx, `
INSERT INTO project (project_id, name, description, daily_points)
VALUES (:project_id, :name, :description, :daily_points)
`, project)
return &project, err
}
func (r *projectRepository) Save(ctx context.Context, project models.Project) error {
_, err := r.db.NamedExecContext(ctx, `
UPDATE project
SET name=:name, description=:description, daily_points=:daily_points
WHERE project_id=:project_id
`, project)
return err
}
func (r *projectRepository) GetPermission(ctx context.Context, project models.Project, user models.User) (*models.ProjectPermission, error) {
permission := models.ProjectPermission{}
err := r.db.GetContext(ctx, &permission, "SELECT * FROM project_permission WHERE project_id=? AND user_id=?", project.ID, user.ID)
if err != nil {
if err == sql.ErrNoRows {
return &models.ProjectPermission{
ProjectID: project.ID,
UserID: user.ID,
}, nil
}
return nil, err
}
return &permission, nil
}
func (r *projectRepository) GetIssuePermission(ctx context.Context, issue models.Issue, user models.User) (*models.ProjectPermission, error) {
return r.GetPermission(ctx, models.Project{ID: issue.ProjectID}, user)
}
func (r *projectRepository) SetPermission(ctx context.Context, permission models.ProjectPermission) error {
_, err := r.db.NamedExecContext(ctx, `
REPLACE INTO project_permission (project_id, user_id, access_level)
VALUES (:project_id, :user_id, :access_level)
`, permission)
return err
}
func (r *projectRepository) Delete(ctx context.Context, project models.Project) error {
_, err := r.db.ExecContext(ctx, "DELETE FROM project WHERE project_id=?", project.ID)
if err != nil {
return err
}
_, err = r.db.ExecContext(ctx, "DELETE FROM project_permission WHERE project_id=?", project.ID)
if err != nil {
return err
}
//_, err = r.db.ExecContext(ctx, "DELETE FROM project_status WHERE project_id=?", project.ID)
//if err != nil {
// return err
//}
return nil
}
@@ -0,0 +1,165 @@
package mysqldriver
import (
"context"
"git.aiterp.net/stufflog/server/internal/xlerrors"
"git.aiterp.net/stufflog/server/models"
"github.com/stretchr/testify/assert"
"testing"
"time"
)
var project1 = models.Project{
ID: "STUFF",
Name: "Stuff and things",
Description: "Items and artifacts",
DailyPoints: 200,
}
var project2 = models.Project{
ID: "MODELING",
Name: "3D Modelling",
Description: "Making stuff.",
DailyPoints: 250,
}
var project3 = models.Project{
ID: "DINNER",
Name: "Dinner",
Description: "Shopping lists and meals.",
DailyPoints: 0,
}
var projectSearch1 = "stuff"
func TestProjectRepository(t *testing.T) {
projects := testDB.projects
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
assert.NoError(t, clearTable("project"))
// INSERT
result, err := projects.Insert(ctx, project1)
assert.NoError(t, err)
assert.Equal(t, project1, *result)
result, err = projects.Insert(ctx, project2)
assert.NoError(t, err)
assert.Equal(t, project2, *result)
result, err = projects.Insert(ctx, project3)
assert.NoError(t, err)
assert.Equal(t, project3, *result)
if t.Failed() {
return
}
// FIND
result, err = projects.Find(ctx, "MODELING")
assert.NoError(t, err)
assert.Equal(t, &project2, result)
// FIND't
result, err = projects.Find(ctx, "BLARGH")
assert.Error(t, err)
assert.True(t, xlerrors.IsNotFound(err))
assert.Nil(t, result)
// LIST
results, err := projects.List(ctx, models.ProjectFilter{})
assert.NoError(t, err)
assert.Equal(t, []*models.Project{&project3, &project2, &project1}, results)
results, err = projects.List(ctx, models.ProjectFilter{
Search: &projectSearch1,
})
assert.NoError(t, err)
assert.Equal(t, []*models.Project{&project2, &project1}, results)
results, err = projects.List(ctx, models.ProjectFilter{
ProjectIDs: []string{"DINNER", "MODELING"},
})
assert.NoError(t, err)
assert.Equal(t, []*models.Project{&project3, &project2}, results)
if t.Failed() {
return
}
// UPDATE
project2fix := project2
project2fix.Name = "3D Modeling"
project2fix.Description = "Modeling 3D stuff."
project2fix.DailyPoints = 150
err = projects.Save(ctx, project2fix)
assert.NoError(t, err)
if t.Failed() {
return
}
// FIND after UPDATE
result, err = projects.Find(ctx, "MODELING")
assert.NoError(t, err)
assert.Equal(t, &project2fix, result)
// DELETE
err = projects.Delete(ctx, project3)
assert.NoError(t, err)
if t.Failed() {
return
}
// LIST after DELETE + UPDATE
results, err = projects.List(ctx, models.ProjectFilter{})
assert.NoError(t, err)
assert.Equal(t, []*models.Project{&project2fix, &project1}, results)
// SET PERMISSION
err = projects.SetPermission(ctx, models.ProjectPermission{
ProjectID: project1.ID,
UserID: user1.ID,
Level: models.ProjectPermissionLevelOwner,
})
assert.NoError(t, err)
err = projects.SetPermission(ctx, models.ProjectPermission{
ProjectID: project2fix.ID,
UserID: user1.ID,
Level: models.ProjectPermissionLevelMember,
})
assert.NoError(t, err)
// GET PERMISSIONS
permission, err := projects.GetPermission(ctx, project1, user1)
assert.NoError(t, err)
assert.Equal(t, project1.ID, permission.ProjectID)
assert.Equal(t, user1.ID, permission.UserID)
assert.Equal(t, models.ProjectPermissionLevelOwner, permission.Level)
permission, err = projects.GetPermission(ctx, project2fix, user1)
assert.NoError(t, err)
assert.Equal(t, models.ProjectPermissionLevelMember, permission.Level)
// GET PERMISSION (default)
permission, err = projects.GetPermission(ctx, project1, user2)
assert.NoError(t, err)
assert.Equal(t, project1.ID, permission.ProjectID)
assert.Equal(t, user2.ID, permission.UserID)
assert.Equal(t, models.ProjectPermissionLevelNoAccess, permission.Level)
// LIST after SET PERMISSION
results, err = projects.List(ctx, models.ProjectFilter{
Permission: &models.ProjectFilterPermission{
UserID: user1.ID,
MinLevel: models.ProjectPermissionLevelMember,
},
})
assert.NoError(t, err)
assert.Equal(t, []*models.Project{&project2fix, &project1}, results)
results, err = projects.List(ctx, models.ProjectFilter{
Permission: &models.ProjectFilterPermission{
UserID: user2.ID,
MinLevel: models.ProjectPermissionLevelMember,
},
})
assert.NoError(t, err)
assert.Equal(t, []*models.Project{}, results)
}
+49
View File
@@ -0,0 +1,49 @@
package mysqldriver
import (
"context"
"database/sql"
"git.aiterp.net/stufflog/server/internal/xlerrors"
"git.aiterp.net/stufflog/server/models"
"github.com/jmoiron/sqlx"
"time"
)
type sessionRepository struct {
db *sqlx.DB
}
func (r *sessionRepository) Find(ctx context.Context, id string) (*models.Session, error) {
session := models.Session{}
err := r.db.GetContext(ctx, &session, "SELECT * FROM session WHERE session_id=?", id)
if err != nil {
if err == sql.ErrNoRows {
return nil, xlerrors.NotFound("Session")
}
return nil, err
} else if time.Now().After(session.ExpiryTime) {
return nil, xlerrors.NotFound("Session")
}
return &session, nil
}
func (r *sessionRepository) Save(ctx context.Context, session models.Session) error {
_, err := r.db.NamedExecContext(ctx, `
REPLACE INTO session (session_id, user_id, expiry_time)
VALUES (:session_id, :user_id, :expiry_time)
`, session)
return err
}
func (r *sessionRepository) Delete(ctx context.Context, session models.Session) error {
_, err := r.db.ExecContext(ctx, "DELETE FROM session WHERE session_id=?", session.ID)
return err
}
func (r *sessionRepository) DeleteExpired(ctx context.Context) error {
_, err := r.db.ExecContext(ctx, "DELETE FROM session WHERE expiry_time<?", time.Now())
return err
}
+99
View File
@@ -0,0 +1,99 @@
package mysqldriver
import (
"context"
"database/sql"
"errors"
"git.aiterp.net/stufflog/server/internal/xlerrors"
"git.aiterp.net/stufflog/server/models"
sq "github.com/Masterminds/squirrel"
"github.com/jmoiron/sqlx"
)
type userRepository struct {
db *sqlx.DB
}
func (r *userRepository) Find(ctx context.Context, id string) (*models.User, error) {
user := models.User{}
err := r.db.GetContext(ctx, &user, "SELECT * FROM user WHERE user_id=?", id)
if err != nil {
if err == sql.ErrNoRows {
return nil, xlerrors.NotFound("User")
}
return nil, err
}
return &user, nil
}
func (r *userRepository) List(ctx context.Context, filter models.UserFilter) ([]*models.User, error) {
q := sq.Select("user.*").From("user").OrderBy("user_id")
if len(filter.UserIDs) > 0 {
q = q.Where(sq.Eq{"user_id": filter.UserIDs})
}
if filter.Active != nil {
q = q.Where(sq.Eq{"active": *filter.Active})
}
if filter.Admin != nil {
q = q.Where(sq.Eq{"admin": *filter.Admin})
}
if filter.Limit != nil && *filter.Limit > 0 {
q = q.Limit(uint64(*filter.Limit))
}
query, args, err := q.ToSql()
if err != nil {
return nil, err
}
results := make([]*models.User, 0, 16)
err = r.db.SelectContext(ctx, &results, query, args...)
if err != nil {
if err == sql.ErrNoRows {
return []*models.User{}, nil
}
return nil, err
}
return results, nil
}
func (r *userRepository) Insert(ctx context.Context, user models.User) (*models.User, error) {
if len(user.Name) < 1 || len([]byte(user.Name)) > 32 {
return nil, errors.New("user id is not valid")
}
_, err := r.db.NamedExecContext(ctx, `
INSERT INTO user (
user_id, name, active, admin, hash
) VALUES (
:user_id, :name, :active, :admin, :hash
)
`, user)
if err != nil {
return nil, err
}
return &user, nil
}
func (r *userRepository) Save(ctx context.Context, user models.User) error {
_, err := r.db.NamedExecContext(ctx, `
UPDATE user
SET name=:name,
hash=:hash,
admin=:admin,
active=:active
WHERE user_id=:user_id
`, user)
return err
}
func (r *userRepository) Delete(ctx context.Context, user models.User) error {
_, err := r.db.ExecContext(ctx, "DELETE FROM user WHERE user_id=?", user.ID)
return err
}
+117
View File
@@ -0,0 +1,117 @@
package mysqldriver
import (
"context"
"git.aiterp.net/stufflog/server/internal/xlerrors"
"git.aiterp.net/stufflog/server/models"
"github.com/stretchr/testify/assert"
"testing"
"time"
)
var user1 = models.User{
ID: "Test",
Name: "Testy Tester",
Active: true,
Admin: false,
Hash: []byte("$2y$12$tj/R/zDHrGy1Jsi57DUuSeCISYvEHb/F37p.9HGlyf72cIXnppeQK"),
}
var user2 = models.User{
ID: "Admin",
Name: "Administrator",
Active: true,
Admin: true,
Hash: []byte("$2y$12$tj/R/zDHrGy1Jsi57DUuSeCISYvEHb/F37p.9HGlyf72cIXnppeQK"),
}
var user2Updated = models.User{
ID: user2.ID,
Name: "Dethroned Dictator",
Active: false,
Admin: false,
Hash: []byte("$2y$12$7MuqYzV59HCtHJlRJCd/vOQIrFcMEMVhyySzJX.WlVtEJH3qHVPU2\n"),
}
func TestUserRepository(t *testing.T) {
users := testDB.users
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
assert.NoError(t, clearTable("user"))
// INSERT
result, err := users.Insert(ctx, user1)
assert.NoError(t, err)
assert.Equal(t, user1, *result)
result, err = users.Insert(ctx, user2)
assert.NoError(t, err)
assert.Equal(t, user2, *result)
// FIND
result, err = users.Find(ctx, user1.ID)
assert.NoError(t, err)
assert.Equal(t, &user1, result)
result, err = users.Find(ctx, user2.ID)
assert.NoError(t, err)
assert.Equal(t, &user2, result)
// FIND't
result, err = users.Find(ctx, "NonExistent")
assert.Error(t, err)
assert.True(t, xlerrors.IsNotFound(err))
assert.Nil(t, result)
// LIST
results, err := users.List(ctx, models.UserFilter{})
assert.NoError(t, err)
assert.Equal(t, []*models.User{&user2, &user1}, results)
results, err = users.List(ctx, models.UserFilter{
Admin: ptrBool(true),
})
assert.NoError(t, err)
assert.Equal(t, []*models.User{&user2}, results)
results, err = users.List(ctx, models.UserFilter{
Active: ptrBool(true),
})
assert.NoError(t, err)
assert.Equal(t, []*models.User{&user2, &user1}, results)
results, err = users.List(ctx, models.UserFilter{
Active: ptrBool(false),
})
assert.NoError(t, err)
assert.Equal(t, []*models.User{}, results)
results, err = users.List(ctx, models.UserFilter{
UserIDs: []string{user1.ID},
})
assert.NoError(t, err)
assert.Equal(t, []*models.User{&user1}, results)
results, err = users.List(ctx, models.UserFilter{
Limit: ptrInt(1),
})
assert.NoError(t, err)
assert.Equal(t, []*models.User{&user2}, results)
// UPDATE
err = users.Save(ctx, user2Updated)
assert.NoError(t, err)
// LIST after UPDATE
results, err = users.List(ctx, models.UserFilter{})
assert.NoError(t, err)
assert.Equal(t, []*models.User{&user2Updated, &user1}, results)
// DELETE
err = users.Delete(ctx, user1)
assert.NoError(t, err)
// LIST after DELETE
results, err = users.List(ctx, models.UserFilter{})
assert.NoError(t, err)
assert.Equal(t, []*models.User{&user2Updated}, results)
// INSERT after DELETE
result, err = users.Insert(ctx, user1)
assert.NoError(t, err)
assert.Equal(t, user1, *result)
}