> be fist commit
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"git.aiterp.net/stufflog/server/database/drivers/mysqldriver"
|
||||
"git.aiterp.net/stufflog/server/database/repositories"
|
||||
)
|
||||
|
||||
var ErrDriverNotSupported = errors.New("driver not found or supported")
|
||||
|
||||
type Database interface {
|
||||
Issues() repositories.IssueRepository
|
||||
Items() repositories.ItemRepository
|
||||
Projects() repositories.ProjectRepository
|
||||
Session() repositories.SessionRepository
|
||||
Users() repositories.UserRepository
|
||||
|
||||
// Migrate the database.
|
||||
Migrate() error
|
||||
}
|
||||
|
||||
func Open(driver, connect string) (Database, error) {
|
||||
switch driver {
|
||||
case "mysql":
|
||||
return mysqldriver.Open(connect)
|
||||
default:
|
||||
return nil, ErrDriverNotSupported
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.aiterp.net/stufflog/server/models"
|
||||
)
|
||||
|
||||
type IssueRepository interface {
|
||||
Find(ctx context.Context, id string) (*models.Issue, error)
|
||||
List(ctx context.Context, filter models.IssueFilter) ([]*models.Issue, error)
|
||||
Insert(ctx context.Context, issue models.Issue) (*models.Issue, error)
|
||||
Save(ctx context.Context, issue models.Issue) error
|
||||
Delete(ctx context.Context, issue models.Issue) error
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.aiterp.net/stufflog/server/models"
|
||||
)
|
||||
|
||||
type ItemRepository interface {
|
||||
Find(ctx context.Context, id string) (*models.Item, error)
|
||||
List(ctx context.Context, filter models.ItemFilter) ([]*models.Item, error)
|
||||
Insert(ctx context.Context, item models.Item) (*models.Item, error)
|
||||
Save(ctx context.Context, item models.Item) error
|
||||
Delete(ctx context.Context, item models.Item) error
|
||||
GetTags(ctx context.Context) ([]string, error)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.aiterp.net/stufflog/server/models"
|
||||
)
|
||||
|
||||
type ProjectRepository interface {
|
||||
Find(ctx context.Context, id string) (*models.Project, error)
|
||||
List(ctx context.Context, filter models.ProjectFilter) ([]*models.Project, error)
|
||||
Insert(ctx context.Context, project models.Project) (*models.Project, error)
|
||||
Save(ctx context.Context, project models.Project) error
|
||||
GetPermission(ctx context.Context, project models.Project, user models.User) (*models.ProjectPermission, error)
|
||||
GetIssuePermission(ctx context.Context, issue models.Issue, user models.User) (*models.ProjectPermission, error)
|
||||
SetPermission(ctx context.Context, permission models.ProjectPermission) error
|
||||
Delete(ctx context.Context, project models.Project) error
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.aiterp.net/stufflog/server/models"
|
||||
)
|
||||
|
||||
type SessionRepository interface {
|
||||
Find(ctx context.Context, id string) (*models.Session, error)
|
||||
Save(ctx context.Context, session models.Session) error
|
||||
Delete(ctx context.Context, session models.Session) error
|
||||
DeleteExpired(ctx context.Context) error
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.aiterp.net/stufflog/server/models"
|
||||
)
|
||||
|
||||
type UserRepository interface {
|
||||
Find(ctx context.Context, id string) (*models.User, error)
|
||||
List(ctx context.Context, filter models.UserFilter) ([]*models.User, error)
|
||||
Insert(ctx context.Context, user models.User) (*models.User, error)
|
||||
Save(ctx context.Context, user models.User) error
|
||||
Delete(ctx context.Context, user models.User) error
|
||||
}
|
||||
Reference in New Issue
Block a user