> be fist commit
This commit is contained in:
Generated
+8
@@ -0,0 +1,8 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
Generated
+5
@@ -0,0 +1,5 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
|
||||
</state>
|
||||
</component>
|
||||
Generated
+11
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
||||
<data-source source="LOCAL" name="xiaoli@localhost" uuid="58ed4679-da13-4e36-bc48-70b2cb7bb1fe">
|
||||
<driver-ref>mariadb</driver-ref>
|
||||
<synchronize>true</synchronize>
|
||||
<jdbc-driver>org.mariadb.jdbc.Driver</jdbc-driver>
|
||||
<jdbc-url>jdbc:mariadb://localhost:3306/xiaoli</jdbc-url>
|
||||
</data-source>
|
||||
</component>
|
||||
</project>
|
||||
Generated
+13
@@ -0,0 +1,13 @@
|
||||
<component name="ProjectDictionaryState">
|
||||
<dictionary name="gisle">
|
||||
<words>
|
||||
<w>blargh</w>
|
||||
<w>blendings</w>
|
||||
<w>graphcore</w>
|
||||
<w>qpos</w>
|
||||
<w>sourcream</w>
|
||||
<w>stufflog</w>
|
||||
<w>xiaoli</w>
|
||||
</words>
|
||||
</dictionary>
|
||||
</component>
|
||||
Generated
+6
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="JavaScriptSettings">
|
||||
<option name="languageLevel" value="ES6" />
|
||||
</component>
|
||||
</project>
|
||||
Generated
+8
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/xiaoli.iml" filepath="$PROJECT_DIR$/.idea/xiaoli.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
Generated
+7
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="SqlDialectMappings">
|
||||
<file url="file://$PROJECT_DIR$/migrations/mysql/20200405173553_create_table_project.sql" dialect="GenericSQL" />
|
||||
<file url="PROJECT" dialect="MariaDB" />
|
||||
</component>
|
||||
</project>
|
||||
Generated
+29
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectTasksOptions">
|
||||
<TaskOptions isEnabled="true">
|
||||
<option name="arguments" value="fmt $FilePath$" />
|
||||
<option name="checkSyntaxErrors" value="true" />
|
||||
<option name="description" />
|
||||
<option name="exitCodeBehavior" value="ERROR" />
|
||||
<option name="fileExtension" value="go" />
|
||||
<option name="immediateSync" value="false" />
|
||||
<option name="name" value="go fmt" />
|
||||
<option name="output" value="$FilePath$" />
|
||||
<option name="outputFilters">
|
||||
<array />
|
||||
</option>
|
||||
<option name="outputFromStdout" value="false" />
|
||||
<option name="program" value="$GoExecPath$" />
|
||||
<option name="runOnExternalChanges" value="false" />
|
||||
<option name="scopeName" value="Project Files" />
|
||||
<option name="trackOnlyRoot" value="true" />
|
||||
<option name="workingDir" value="$ProjectFileDir$" />
|
||||
<envs>
|
||||
<env name="GOROOT" value="$GOROOT$" />
|
||||
<env name="GOPATH" value="$GOPATH$" />
|
||||
<env name="PATH" value="$GoBinDirs$" />
|
||||
</envs>
|
||||
</TaskOptions>
|
||||
</component>
|
||||
</project>
|
||||
Generated
+8
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
module git.aiterp.net/stufflog/server
|
||||
|
||||
go 1.14
|
||||
|
||||
require (
|
||||
github.com/99designs/gqlgen v0.11.3
|
||||
github.com/Masterminds/squirrel v1.2.0
|
||||
github.com/gin-gonic/gin v1.6.2
|
||||
github.com/go-sql-driver/mysql v1.5.0
|
||||
github.com/jmoiron/sqlx v1.2.0
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/pressly/goose v2.6.0+incompatible
|
||||
github.com/stretchr/testify v1.5.1
|
||||
github.com/urfave/cli/v2 v2.2.0
|
||||
github.com/vektah/gqlparser/v2 v2.0.1
|
||||
golang.org/x/crypto v0.0.0-20200403201458-baeed622b8d8
|
||||
)
|
||||
@@ -0,0 +1,143 @@
|
||||
github.com/99designs/gqlgen v0.11.3 h1:oFSxl1DFS9X///uHV3y6CEfpcXWrDUxVblR4Xib2bs4=
|
||||
github.com/99designs/gqlgen v0.11.3/go.mod h1:RgX5GRRdDWNkh4pBrdzNpNPFVsdoUFY2+adM6nb1N+4=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/Masterminds/squirrel v1.2.0 h1:K1NhbTO21BWG47IVR0OnIZuE0LZcXAYqywrC3Ko53KI=
|
||||
github.com/Masterminds/squirrel v1.2.0/go.mod h1:yaPeOnPG5ZRwL9oKdTsO/prlkPbXWZlRVMQ/gGlzIuA=
|
||||
github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
|
||||
github.com/agnivade/levenshtein v1.0.3 h1:M5ZnqLOoZR8ygVq0FfkXsNOKzMCk0xRiow0R5+5VkQ0=
|
||||
github.com/agnivade/levenshtein v1.0.3/go.mod h1:4SFRZbbXWLF4MU1T9Qg0pGgH3Pjs+t6ie5efyrwRJXs=
|
||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
|
||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
|
||||
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=
|
||||
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/trifles v0.0.0-20190318185328-a8d75aae118c h1:TUuUh0Xgj97tLMNtWtNvI9mIV6isjEb9lBMNv+77IGM=
|
||||
github.com/dgryski/trifles v0.0.0-20190318185328-a8d75aae118c/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.6.2 h1:88crIK23zO6TqlQBt+f9FrPJNKm9ZEr7qjp9vl/d5TM=
|
||||
github.com/gin-gonic/gin v1.6.2/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
|
||||
github.com/go-chi/chi v3.3.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
|
||||
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
|
||||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
|
||||
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||
github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
|
||||
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
|
||||
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
|
||||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||
github.com/gorilla/mux v1.6.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/websocket v1.2.0 h1:VJtLvh6VQym50czpZzx07z/kw9EgAxI3x1ZB8taTMQQ=
|
||||
github.com/gorilla/websocket v1.2.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||
github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA=
|
||||
github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
|
||||
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw=
|
||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o=
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk=
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw=
|
||||
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
|
||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||
github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A=
|
||||
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
|
||||
github.com/matryer/moq v0.0.0-20200106131100-75d0ddfc0007/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ=
|
||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-sqlite3 v1.9.0 h1:pDRiWfl+++eC2FEFRy6jXmQlvp4Yh3z1MJKg4UeYM/4=
|
||||
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/mitchellh/mapstructure v0.0.0-20180203102830-a4e142e9c047 h1:zCoDWFD5nrJJVjbXiDZcVhOBSzKn3o9LgRLLMRNuru8=
|
||||
github.com/mitchellh/mapstructure v0.0.0-20180203102830-a4e142e9c047/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
|
||||
github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pressly/goose v2.6.0+incompatible h1:3f8zIQ8rfgP9tyI0Hmcs2YNAqUCL1c+diLe3iU8Qd/k=
|
||||
github.com/pressly/goose v2.6.0+incompatible/go.mod h1:m+QHWCqxR3k8D9l7qfzuC/djtlfzxr34mozWDYEu1z8=
|
||||
github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
|
||||
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
|
||||
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/shurcooL/vfsgen v0.0.0-20180121065927-ffb13db8def0/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw=
|
||||
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
|
||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
|
||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||
github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
|
||||
github.com/urfave/cli/v2 v2.2.0 h1:JTTnM6wKzdA0Jqodd966MVj4vWbbquZykeX1sKbe2C4=
|
||||
github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
|
||||
github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e/go.mod h1:/HUdMve7rvxZma+2ZELQeNh88+003LL7Pf/CZ089j8U=
|
||||
github.com/vektah/gqlparser/v2 v2.0.1 h1:xgl5abVnsd4hkN9rk65OJID9bfcLSMuTaTcZj777q1o=
|
||||
github.com/vektah/gqlparser/v2 v2.0.1/go.mod h1:SyUiHgLATUR8BiYURfTirrTcGpcE+4XkV2se04Px1Ms=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200403201458-baeed622b8d8 h1:fpnn/HnJONpIu6hkXi1u/7rR0NzilgWr4T0JmWkEitk=
|
||||
golang.org/x/crypto v0.0.0-20200403201458-baeed622b8d8/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190515012406-7d7faa4812bd/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20200114235610-7ae403b6b589 h1:rjUrONFu4kLchcZTfp3/96bR8bW8dIa8uz3cR5n0cgM=
|
||||
golang.org/x/tools v0.0.0-20200114235610-7ae403b6b589/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
sourcegraph.com/sourcegraph/appdash v0.0.0-20180110180208-2cc67fd64755/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=
|
||||
sourcegraph.com/sourcegraph/appdash-data v0.0.0-20151005221446-73f23eafcf67/go.mod h1:L5q+DGLGOQFpo1snNEkLOJT2d1YTW66rWNzatr3He1k=
|
||||
@@ -0,0 +1,18 @@
|
||||
schema:
|
||||
- schema/*.gql
|
||||
|
||||
exec:
|
||||
filename: graphcore/exec_gen.go
|
||||
package: graphcore
|
||||
|
||||
model:
|
||||
filename: graphcore/input_gen.go
|
||||
package: graphcore
|
||||
|
||||
resolver:
|
||||
layout: follow-schema
|
||||
dir: resolvers
|
||||
package: resolvers
|
||||
|
||||
autobind:
|
||||
- git.aiterp.net/stufflog/server/models
|
||||
@@ -0,0 +1,30 @@
|
||||
package graph
|
||||
|
||||
import (
|
||||
"git.aiterp.net/stufflog/server/graph/graphcore"
|
||||
"git.aiterp.net/stufflog/server/graph/resolvers"
|
||||
"git.aiterp.net/stufflog/server/services"
|
||||
"github.com/99designs/gqlgen/graphql"
|
||||
"github.com/99designs/gqlgen/graphql/handler"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
//go:generate go run github.com/99designs/gqlgen --verbose --config gqlgen.yml
|
||||
|
||||
// New creates a new GraphQL schema.
|
||||
func New(s services.Bundle) graphql.ExecutableSchema {
|
||||
return graphcore.NewExecutableSchema(graphcore.Config{
|
||||
Resolvers: &resolvers.Resolver{S: s},
|
||||
})
|
||||
}
|
||||
|
||||
func Gin(s services.Bundle) gin.HandlerFunc {
|
||||
schema := New(s)
|
||||
gqlHandler := handler.NewDefaultServer(schema)
|
||||
|
||||
return func(c *gin.Context) {
|
||||
s.Auth.CheckGinSession(c)
|
||||
|
||||
gqlHandler.ServeHTTP(c.Writer, c.Request)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,38 @@
|
||||
// Code generated by github.com/99designs/gqlgen, DO NOT EDIT.
|
||||
|
||||
package graphcore
|
||||
|
||||
import (
|
||||
"git.aiterp.net/stufflog/server/models"
|
||||
)
|
||||
|
||||
// A limited filter for constraining the list of issues within a project.
|
||||
type ProjectIssueFilter struct {
|
||||
// Filter by assignee IDs
|
||||
AssigneeIds []string `json:"assigneeIds"`
|
||||
// Text search
|
||||
Search *string `json:"search"`
|
||||
// Earliest stage (inclusive)
|
||||
MinStage *int `json:"minStage"`
|
||||
// Latest stage (inclusive)
|
||||
MaxStage *int `json:"maxStage"`
|
||||
// Limit the result set
|
||||
Limit *int `json:"limit"`
|
||||
}
|
||||
|
||||
// The permissions of a user within the project.
|
||||
type ProjectPermissions struct {
|
||||
// User ID.
|
||||
UserID string `json:"userId"`
|
||||
// Access level.
|
||||
AccessLevel int `json:"accessLevel"`
|
||||
// The user whose permissions it is. Can be null if the user no longer exists.
|
||||
User *models.User `json:"user"`
|
||||
}
|
||||
|
||||
// Input for loginUser.
|
||||
type UserLoginInput struct {
|
||||
UserID string `json:"userId"`
|
||||
Password string `json:"password"`
|
||||
RememberMe bool `json:"rememberMe"`
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package resolvers
|
||||
|
||||
// This file will be automatically regenerated based on the schema, any resolver implementations
|
||||
// will be copied through when generating and any unknown code will be moved to the end.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"git.aiterp.net/stufflog/server/graph/graphcore"
|
||||
"git.aiterp.net/stufflog/server/models"
|
||||
)
|
||||
|
||||
func (r *issueResolver) Project(ctx context.Context, obj *models.Issue) (*models.Project, error) {
|
||||
panic(fmt.Errorf("not implemented"))
|
||||
}
|
||||
|
||||
func (r *issueResolver) Owner(ctx context.Context, obj *models.Issue) (*models.User, error) {
|
||||
panic(fmt.Errorf("not implemented"))
|
||||
}
|
||||
|
||||
func (r *issueResolver) Assignee(ctx context.Context, obj *models.Issue) (*models.User, error) {
|
||||
panic(fmt.Errorf("not implemented"))
|
||||
}
|
||||
|
||||
// Issue returns graphcore.IssueResolver implementation.
|
||||
func (r *Resolver) Issue() graphcore.IssueResolver { return &issueResolver{r} }
|
||||
|
||||
type issueResolver struct{ *Resolver }
|
||||
@@ -0,0 +1,25 @@
|
||||
package resolvers
|
||||
|
||||
// This file will be automatically regenerated based on the schema, any resolver implementations
|
||||
// will be copied through when generating and any unknown code will be moved to the end.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"git.aiterp.net/stufflog/server/graph/graphcore"
|
||||
"git.aiterp.net/stufflog/server/models"
|
||||
)
|
||||
|
||||
func (r *mutationResolver) LoginUser(ctx context.Context, input *graphcore.UserLoginInput) (*models.User, error) {
|
||||
panic(fmt.Errorf("not implemented"))
|
||||
}
|
||||
|
||||
func (r *mutationResolver) LogoutUser(ctx context.Context) (*models.User, error) {
|
||||
panic(fmt.Errorf("not implemented"))
|
||||
}
|
||||
|
||||
// Mutation returns graphcore.MutationResolver implementation.
|
||||
func (r *Resolver) Mutation() graphcore.MutationResolver { return &mutationResolver{r} }
|
||||
|
||||
type mutationResolver struct{ *Resolver }
|
||||
@@ -0,0 +1,29 @@
|
||||
package resolvers
|
||||
|
||||
// This file will be automatically regenerated based on the schema, any resolver implementations
|
||||
// will be copied through when generating and any unknown code will be moved to the end.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"git.aiterp.net/stufflog/server/graph/graphcore"
|
||||
"git.aiterp.net/stufflog/server/models"
|
||||
)
|
||||
|
||||
func (r *projectResolver) Issues(ctx context.Context, obj *models.Project, filter *graphcore.ProjectIssueFilter) ([]*models.Issue, error) {
|
||||
panic(fmt.Errorf("not implemented"))
|
||||
}
|
||||
|
||||
func (r *projectResolver) Permissions(ctx context.Context, obj *models.Project) ([]*graphcore.ProjectPermissions, error) {
|
||||
panic(fmt.Errorf("not implemented"))
|
||||
}
|
||||
|
||||
func (r *projectResolver) UserPermissions(ctx context.Context, obj *models.Project) (*graphcore.ProjectPermissions, error) {
|
||||
panic(fmt.Errorf("not implemented"))
|
||||
}
|
||||
|
||||
// Project returns graphcore.ProjectResolver implementation.
|
||||
func (r *Resolver) Project() graphcore.ProjectResolver { return &projectResolver{r} }
|
||||
|
||||
type projectResolver struct{ *Resolver }
|
||||
@@ -0,0 +1,37 @@
|
||||
package resolvers
|
||||
|
||||
// This file will be automatically regenerated based on the schema, any resolver implementations
|
||||
// will be copied through when generating and any unknown code will be moved to the end.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"git.aiterp.net/stufflog/server/graph/graphcore"
|
||||
"git.aiterp.net/stufflog/server/models"
|
||||
)
|
||||
|
||||
func (r *queryResolver) Issue(ctx context.Context, id string) (*models.Issue, error) {
|
||||
panic(fmt.Errorf("not implemented"))
|
||||
}
|
||||
|
||||
func (r *queryResolver) Issues(ctx context.Context, filter *models.IssueFilter) ([]*models.Issue, error) {
|
||||
panic(fmt.Errorf("not implemented"))
|
||||
}
|
||||
|
||||
func (r *queryResolver) Project(ctx context.Context, id string) (*models.Project, error) {
|
||||
panic(fmt.Errorf("not implemented"))
|
||||
}
|
||||
|
||||
func (r *queryResolver) Projects(ctx context.Context, filter *models.ProjectFilter) ([]*models.Project, error) {
|
||||
panic(fmt.Errorf("not implemented"))
|
||||
}
|
||||
|
||||
func (r *queryResolver) Session(ctx context.Context) (*models.User, error) {
|
||||
panic(fmt.Errorf("not implemented"))
|
||||
}
|
||||
|
||||
// Query returns graphcore.QueryResolver implementation.
|
||||
func (r *Resolver) Query() graphcore.QueryResolver { return &queryResolver{r} }
|
||||
|
||||
type queryResolver struct{ *Resolver }
|
||||
@@ -0,0 +1,11 @@
|
||||
package resolvers
|
||||
|
||||
import "git.aiterp.net/stufflog/server/services"
|
||||
|
||||
// This file will not be regenerated automatically.
|
||||
//
|
||||
// It serves as dependency injection for your app, add any dependencies you require here.
|
||||
|
||||
type Resolver struct {
|
||||
S services.Bundle
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
type Issue {
|
||||
id: String!
|
||||
projectId: String!
|
||||
ownerId: String!
|
||||
assigneeId: String!
|
||||
statusStage: Int!
|
||||
statusName: String!
|
||||
createdTime: Time!
|
||||
updatedTime: Time!
|
||||
dueTime: Time
|
||||
name: String!
|
||||
title: String!
|
||||
description: String!
|
||||
|
||||
project: Project
|
||||
owner: User
|
||||
assignee: User
|
||||
}
|
||||
|
||||
input IssueFilter {
|
||||
"Filter by issue IDs (mostly used internally by data loaders)"
|
||||
issueIds: [String!]
|
||||
"Filter by project IDs"
|
||||
projectIds: [String!]
|
||||
"Filter by owner IDs"
|
||||
ownerIds: [String!]
|
||||
"Filter by assignee IDs"
|
||||
assigneeIds: [String!]
|
||||
"Text search"
|
||||
search: String
|
||||
"Earliest stage (inclusive)"
|
||||
minStage: Int
|
||||
"Latest stage (inclusive)"
|
||||
maxStage: Int
|
||||
"Limit the result set"
|
||||
limit: Int
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
type Mutation {
|
||||
loginUser(input: UserLoginInput): User!
|
||||
logoutUser: User!
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
type Project {
|
||||
"The ID of the project, which is also the prefix for all its issues."
|
||||
id: String!
|
||||
"The name of the project, used in place of the ID in the UI."
|
||||
name: String!
|
||||
"Description of the project."
|
||||
description: String!
|
||||
"The amount of points given as a bonus towards goals for any activity on a given day."
|
||||
dailyPoints: Int!
|
||||
|
||||
"Get issues within the project."
|
||||
issues(filter: ProjectIssueFilter): [Issue!]!
|
||||
"All users' permissions. Only available to administrators and up."
|
||||
permissions: [ProjectPermissions!]!
|
||||
"Own permissions to the project. Available to any logged in user."
|
||||
userPermissions: ProjectPermissions!
|
||||
}
|
||||
|
||||
"The permissions of a user within the project."
|
||||
type ProjectPermissions {
|
||||
"User ID."
|
||||
userId: String!
|
||||
"Access level."
|
||||
accessLevel: Int!
|
||||
|
||||
"The user whose permissions it is. Can be null if the user no longer exists."
|
||||
user: User
|
||||
}
|
||||
|
||||
type ProjectStatus {
|
||||
"The stage of the status. 0=inactive, 1=pending, 2=active, 3=review, 4=completed, 5=failed, 6=postponed"
|
||||
stage: Int!
|
||||
"The name of the status."
|
||||
name: String!
|
||||
"A description of the status and where it's used."
|
||||
description: String!
|
||||
}
|
||||
|
||||
"Filter for projects query"
|
||||
input ProjectFilter {
|
||||
"Project IDs"
|
||||
projectIds: [String!]
|
||||
"Text search"
|
||||
search: String
|
||||
"User permission"
|
||||
permission: ProjectFilterPermission
|
||||
}
|
||||
|
||||
input ProjectFilterPermission {
|
||||
"User ID"
|
||||
userId: String!
|
||||
"Lowest access level to filter by (inclusive)"
|
||||
minLevel: Int!
|
||||
"Highest access level to filter by (inclusive)"
|
||||
maxLevel: Int!
|
||||
}
|
||||
|
||||
"A limited filter for constraining the list of issues within a project."
|
||||
input ProjectIssueFilter {
|
||||
"Filter by assignee IDs"
|
||||
assigneeIds: [String!]
|
||||
"Text search"
|
||||
search: String
|
||||
"Earliest stage (inclusive)"
|
||||
minStage: Int
|
||||
"Latest stage (inclusive)"
|
||||
maxStage: Int
|
||||
"Limit the result set"
|
||||
limit: Int
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
type Query {
|
||||
"Find issue"
|
||||
issue(id: String!): Issue!
|
||||
"List issues"
|
||||
issues(filter: IssueFilter): [Issue!]!
|
||||
|
||||
"Find project"
|
||||
project(id: String!): Project!
|
||||
"List projects"
|
||||
projects(filter: ProjectFilter): [Project!]!
|
||||
|
||||
"Session checks the user session."
|
||||
session: User
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
scalar Time
|
||||
@@ -0,0 +1,18 @@
|
||||
"User information."
|
||||
type User {
|
||||
"User ID, used when logging in."
|
||||
id: String!
|
||||
"User's display name, which can differ from the user ID."
|
||||
name: String!
|
||||
"Whether the user is active."
|
||||
active: Boolean!
|
||||
"Whether the user has general administrator privileges. This does not grant admin rights on any project."
|
||||
admin: Boolean!
|
||||
}
|
||||
|
||||
"Input for loginUser."
|
||||
input UserLoginInput {
|
||||
userId: String!
|
||||
password: String!
|
||||
rememberMe: Boolean!
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package generate
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
mathRand "math/rand"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
const alphabet = "0123456789abcdefghijklmnopqrstuvwxyz"
|
||||
|
||||
func generateWeak(length int, prefix string) string {
|
||||
result := prefix
|
||||
for len(result) < length {
|
||||
result += string(alphabet[mathRand.Intn(len(alphabet))])
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Generate generates an ID either strongly or weakly depending on the system.
|
||||
func Generate(length int, prefix string) string {
|
||||
var buffer [32]byte
|
||||
|
||||
result := prefix
|
||||
offset := 0
|
||||
|
||||
_, err := rand.Read(buffer[:32])
|
||||
if err != nil {
|
||||
return generateWeak(length, prefix)
|
||||
}
|
||||
|
||||
for len(result) < length {
|
||||
result += strconv.FormatUint(binary.LittleEndian.Uint64(buffer[offset:]), 36)
|
||||
offset += 8
|
||||
|
||||
if offset >= 32 {
|
||||
_, err = rand.Read(buffer[:32])
|
||||
if err != nil {
|
||||
return generateWeak(length, prefix)
|
||||
}
|
||||
|
||||
offset = 0
|
||||
}
|
||||
}
|
||||
|
||||
return result[:length]
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package generate
|
||||
|
||||
func ItemID() string {
|
||||
return Generate(32, "I")
|
||||
}
|
||||
|
||||
func SessionID() string {
|
||||
return Generate(32, "S")
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package xlerrors
|
||||
|
||||
type notFoundError struct {
|
||||
Subject string
|
||||
}
|
||||
|
||||
func (err *notFoundError) Error() string {
|
||||
return err.Subject + " not found"
|
||||
}
|
||||
|
||||
func NotFound(subject string) error {
|
||||
return ¬FoundError{Subject: subject}
|
||||
}
|
||||
|
||||
func IsNotFound(err error) bool {
|
||||
_, ok := err.(*notFoundError)
|
||||
return ok
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"git.aiterp.net/stufflog/server/database"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/pressly/goose"
|
||||
"github.com/urfave/cli/v2"
|
||||
"log"
|
||||
"os"
|
||||
"sort"
|
||||
)
|
||||
|
||||
func main() {
|
||||
app := &cli.App{
|
||||
Name: "xiaoli",
|
||||
Usage: "Issue tracker for your home and hobbies",
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "db-driver",
|
||||
Value: "mysql",
|
||||
Usage: "Database driver",
|
||||
EnvVars: []string{"DATABASE_DRIVER"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "db-connect",
|
||||
Value: "xiaoli_user:stuff1234@(localhost:3306)/xiaoli",
|
||||
Usage: "Database connection string or path",
|
||||
EnvVars: []string{"DATABASE_CONNECT"},
|
||||
},
|
||||
},
|
||||
Commands: []*cli.Command{
|
||||
{
|
||||
Name: "migrate",
|
||||
Usage: "Migrate the configured database",
|
||||
Action: func(c *cli.Context) error {
|
||||
db, err := database.Open(c.String("db-driver"), c.String("db-connect"))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to connect to database")
|
||||
}
|
||||
|
||||
err = db.Migrate()
|
||||
if err == goose.ErrNoNextVersion {
|
||||
log.Println("No more migrations to run")
|
||||
} else if err != nil {
|
||||
return errors.Wrap(err, "Failed to run migration")
|
||||
}
|
||||
|
||||
log.Println("Migration succeeded")
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "server",
|
||||
Usage: "Run the server",
|
||||
Action: func(c *cli.Context) error {
|
||||
_, err := database.Open(c.String("db-driver"), c.String("db-connect"))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to connect to database")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
sort.Sort(cli.FlagsByName(app.Flags))
|
||||
sort.Sort(cli.CommandsByName(app.Commands))
|
||||
|
||||
err := app.Run(os.Args)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
CREATE TABLE project (
|
||||
project_id CHAR(16) NOT NULL PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
daily_points INTEGER NOT NULL,
|
||||
|
||||
FULLTEXT(name, description)
|
||||
);
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
DROP TABLE project;
|
||||
-- +goose StatementEnd
|
||||
@@ -0,0 +1,30 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
CREATE TABLE issue (
|
||||
issue_id CHAR(32) NOT NULL PRIMARY KEY,
|
||||
project_id CHAR(16) NOT NULL,
|
||||
owner_id CHAR(32) NOT NULL,
|
||||
assignee_id CHAR(32) NOT NULL,
|
||||
status_stage INTEGER NOT NULL,
|
||||
status_name CHAR(32) NOT NULL,
|
||||
created_time TIMESTAMP NOT NULL,
|
||||
updated_time TIMESTAMP NOT NULL,
|
||||
due_time TIMESTAMP NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
|
||||
FULLTEXT(name, title, description),
|
||||
INDEX(owner_id),
|
||||
INDEX(assignee_id),
|
||||
INDEX(project_id, status_stage, status_name),
|
||||
INDEX(project_id),
|
||||
INDEX(created_time),
|
||||
INDEX(updated_time)
|
||||
);
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
DROP TABLE issue;
|
||||
-- +goose StatementEnd
|
||||
@@ -0,0 +1,16 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
CREATE TABLE item (
|
||||
item_id CHAR(32) NOT NULL PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
image_url VARCHAR(255),
|
||||
|
||||
FULLTEXT(name, description)
|
||||
);
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
DROP TABLE item;
|
||||
-- +goose StatementEnd
|
||||
@@ -0,0 +1,15 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
CREATE TABLE item_tag (
|
||||
item_id CHAR(32),
|
||||
tag VARCHAR(255),
|
||||
|
||||
PRIMARY KEY (item_id, tag),
|
||||
INDEX (tag)
|
||||
);
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
DROP TABLE item_tag;
|
||||
-- +goose StatementEnd
|
||||
@@ -0,0 +1,15 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
CREATE TABLE user (
|
||||
user_id CHAR(32) NOT NULL PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
active BOOL NOT NULL,
|
||||
admin BOOL NOT NULL,
|
||||
hash VARCHAR(255) NOT NULL
|
||||
);
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
DROP TABLE user;
|
||||
-- +goose StatementEnd
|
||||
@@ -0,0 +1,15 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
CREATE TABLE counters (
|
||||
kind CHAR(16) NOT NULL,
|
||||
name CHAR(32) NOT NULL,
|
||||
value INT NOT NULL,
|
||||
|
||||
PRIMARY KEY (kind, name)
|
||||
);
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
DROP TABLE counters;
|
||||
-- +goose StatementEnd
|
||||
@@ -0,0 +1,16 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
CREATE TABLE project_permission (
|
||||
project_id CHAR(16) NOT NULL,
|
||||
user_id CHAR(32) NOT NULL,
|
||||
access_level INTEGER NOT NULL DEFAULT (0),
|
||||
|
||||
PRIMARY KEY (project_id, user_id),
|
||||
INDEX (user_id)
|
||||
);
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
DROP TABLE project_permission
|
||||
-- +goose StatementEnd
|
||||
@@ -0,0 +1,15 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
CREATE TABLE session (
|
||||
session_id CHAR(32) NOT NULL PRIMARY KEY,
|
||||
user_id CHAR(32) NOT NULL,
|
||||
expiry_time TIMESTAMP NOT NULL,
|
||||
|
||||
INDEX(expiry_time)
|
||||
);
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
DROP TABLE session;
|
||||
-- +goose StatementEnd
|
||||
@@ -0,0 +1,12 @@
|
||||
package models
|
||||
|
||||
// Activity is an activity within a project that can be measured and estimated, for example "writing" or "developing".
|
||||
// The points are for the "gamified" aspect of this.
|
||||
type Activity struct {
|
||||
ID string `db:"activity_id"`
|
||||
ProjectID string `db:"project_id"`
|
||||
Countable bool `db:"countable"`
|
||||
UnitName string `db:"unit_name"`
|
||||
UnitValue float64 `db:"unit_value"`
|
||||
BaseValue float64 `db:"base_value"`
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
// An Goal denotes a goal for a project or an activity within a project.
|
||||
// It can ask for one or more of the time, unit or score. If there is no
|
||||
// activity ID provided, all activities within the project should contribute.
|
||||
// If there are no activity, a non-zero unit amount is not allowed.
|
||||
type Goal struct {
|
||||
ActivityGoalID string `db:"goal_id"`
|
||||
ProjectID string `db:"project_id"`
|
||||
ActivityID string `db:"activity_id"`
|
||||
UserID string `db:"user_id"`
|
||||
TimeAmount time.Duration `db:"time_amount"`
|
||||
AllRequired bool `db:"all_required"`
|
||||
UnitAmount int `db:"unit_amount"`
|
||||
ScoreAmount int `db:"score_amount"`
|
||||
TaskAmount int `db:"task_amount"`
|
||||
StartTime time.Time `db:"start_time"`
|
||||
EndTime time.Time `db:"end_time"`
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
type Issue struct {
|
||||
ID string `db:"issue_id"`
|
||||
ProjectID string `db:"project_id"`
|
||||
OwnerID string `db:"owner_id"`
|
||||
AssigneeID string `db:"assignee_id"`
|
||||
StatusStage int `db:"status_stage"`
|
||||
StatusName string `db:"status_name"`
|
||||
CreatedTime time.Time `db:"created_time"`
|
||||
UpdatedTime time.Time `db:"updated_time"`
|
||||
DueTime time.Time `db:"due_time"`
|
||||
Name string `db:"name"`
|
||||
Title string `db:"title"`
|
||||
Description string `db:"description"`
|
||||
}
|
||||
|
||||
type IssueFilter struct {
|
||||
IssueIDs []string
|
||||
ProjectIDs []string
|
||||
OwnerIDs []string
|
||||
AssigneeIDs []string
|
||||
Search *string
|
||||
MinStage *int
|
||||
MaxStage *int
|
||||
Limit *int
|
||||
}
|
||||
|
||||
const (
|
||||
IssueStageInactive = 0
|
||||
IssueStagePending = 1
|
||||
IssueStageActive = 2
|
||||
IssueStageReview = 3
|
||||
IssueStageCompleted = 4
|
||||
IssueStageFailed = 5
|
||||
IssueStagePostponed = 6
|
||||
)
|
||||
@@ -0,0 +1,9 @@
|
||||
package models
|
||||
|
||||
// An IssueTask is a task within an issue.
|
||||
type IssueItem struct {
|
||||
ID string `db:"issue_item_id"`
|
||||
IssueID string `db:"issue_id"`
|
||||
ItemID string `db:"item_id"`
|
||||
Quantity int `db:"quantity"`
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
// An IssueTask is a task within an issue.
|
||||
type IssueTask struct {
|
||||
TaskID string `db:"task_id"`
|
||||
IssueID string `db:"issue_id"`
|
||||
ActivityID string `db:"activity_id"`
|
||||
CreatedTime time.Time `db:"created_time"`
|
||||
UpdatedTime time.Time `db:"updated_time"`
|
||||
DueTime time.Time `db:"due_time"`
|
||||
StatusStage int `db:"status_stage"`
|
||||
StatusName string `db:"status_name"`
|
||||
Name string `db:"name"`
|
||||
Description string `db:"description"`
|
||||
EstimatedTime time.Duration `db:"estimated_time"`
|
||||
EstimatedUnits int `db:"estimated_units"`
|
||||
PointsMultiplier float64 `db:"points_multiplier"`
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package models
|
||||
|
||||
type Item struct {
|
||||
ID string `db:"item_id"`
|
||||
Name string `db:"name"`
|
||||
Description string `db:"description"`
|
||||
Tags []string `db:"tags"`
|
||||
ImageURL *string `db:"image_url"`
|
||||
}
|
||||
|
||||
type ItemFilter struct {
|
||||
ItemIDs []string
|
||||
Tags []string
|
||||
}
|
||||
|
||||
/*
|
||||
SELECT i.item_id, i.name FROM item i
|
||||
LEFT JOIN tag AS t ON t.item_id = i.item_id
|
||||
WHERE t.tag_name IN ("Groceries")
|
||||
GROUP by i.item_id;
|
||||
*/
|
||||
@@ -0,0 +1,6 @@
|
||||
package models
|
||||
|
||||
type Log struct {
|
||||
ID string `db:"log_id"`
|
||||
IssueID string `db:"issue_id"`
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package models
|
||||
|
||||
// A Project is a vague category for issues. It's use depend on the user's judgement, as it could represent
|
||||
// an entire hobby/job or a single project within one.
|
||||
type Project struct {
|
||||
ID string `db:"project_id"`
|
||||
Name string `db:"name"`
|
||||
Description string `db:"description"`
|
||||
DailyPoints int `db:"daily_points"`
|
||||
}
|
||||
|
||||
type ProjectFilter struct {
|
||||
ProjectIDs []string
|
||||
Search *string
|
||||
Permission *ProjectFilterPermission
|
||||
}
|
||||
|
||||
type ProjectFilterPermission struct {
|
||||
UserID string
|
||||
MinLevel int
|
||||
MaxLevel int
|
||||
}
|
||||
|
||||
func (project *Project) ValidKey() bool {
|
||||
if len(project.ID) < 1 || len(project.ID) > 16 {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, r := range project.ID {
|
||||
if r < 'A' && r > 'Z' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (pfp *ProjectFilterPermission) Valid() bool {
|
||||
return pfp.UserID != "" && pfp.MinLevel > 0
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package models
|
||||
|
||||
// ProjectPermission is a structure that associates a user with a project.
|
||||
type ProjectPermission struct {
|
||||
ProjectID string `db:"project_id"`
|
||||
UserID string `db:"user_id"`
|
||||
Level int `db:"access_level"`
|
||||
}
|
||||
|
||||
const (
|
||||
ProjectPermissionLevelNoAccess int = 0
|
||||
ProjectPermissionLevelObserver int = 1
|
||||
ProjectPermissionLevelMember int = 2
|
||||
ProjectPermissionLevelAdmin int = 3
|
||||
ProjectPermissionLevelOwner int = 4
|
||||
)
|
||||
|
||||
func (permission *ProjectPermission) CanViewAnyIssue() bool {
|
||||
return permission.Level >= ProjectPermissionLevelObserver
|
||||
}
|
||||
|
||||
func (permission *ProjectPermission) CanViewOwnIssue() bool {
|
||||
return permission.Level >= ProjectPermissionLevelObserver
|
||||
}
|
||||
|
||||
func (permission *ProjectPermission) CanManageOwnIssue() bool {
|
||||
return permission.Level >= ProjectPermissionLevelMember
|
||||
}
|
||||
|
||||
func (permission *ProjectPermission) CanManageAnyIssue() bool {
|
||||
return permission.Level >= ProjectPermissionLevelAdmin
|
||||
}
|
||||
|
||||
func (permission *ProjectPermission) CanManagePermissions() bool {
|
||||
return permission.Level >= ProjectPermissionLevelOwner
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package models
|
||||
|
||||
// ProjectStatus is an entry denoting a status a project's issues can have.
|
||||
type ProjectStatus struct {
|
||||
ProjectID string `db:"project_id"`
|
||||
Stage int `db:"status_stage"`
|
||||
Name string `db:"status_name"`
|
||||
Description string `db:"description"`
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
type Session struct {
|
||||
ID string `db:"session_id"`
|
||||
UserID string `db:"user_id"`
|
||||
ExpiryTime time.Time `db:"expiry_time"`
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
ID string `db:"user_id"`
|
||||
Name string `db:"name"`
|
||||
Active bool `db:"active"`
|
||||
Admin bool `db:"admin"`
|
||||
Hash []byte `db:"hash"`
|
||||
}
|
||||
|
||||
func (user *User) CheckPassword(password string) bool {
|
||||
return bcrypt.CompareHashAndPassword(user.Hash, []byte(password)) == nil
|
||||
}
|
||||
|
||||
func (user *User) SetPassword(password string) error {
|
||||
if len(password) < 6 {
|
||||
return errors.New("password too short (min: 6)")
|
||||
}
|
||||
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user.Hash = hash
|
||||
return nil
|
||||
}
|
||||
|
||||
type UserFilter struct {
|
||||
UserIDs []string
|
||||
Active *bool
|
||||
Admin *bool
|
||||
Limit *int
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"git.aiterp.net/stufflog/server/database/repositories"
|
||||
"git.aiterp.net/stufflog/server/internal/generate"
|
||||
"git.aiterp.net/stufflog/server/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
"math/rand"
|
||||
"time"
|
||||
)
|
||||
|
||||
var ErrLoginFailed = errors.New("login failed")
|
||||
var ErrInternalLoginFailure = errors.New("login failed due to internal error")
|
||||
var ErrInternalPermissionFailure = errors.New("permission check failed due to internal error")
|
||||
var ErrPermissionDenied = errors.New("permission denied")
|
||||
|
||||
var authCookieName = "stufflog_cookie"
|
||||
var authCtxKey = "stufflog.auth"
|
||||
var ginCtxKey = "stufflog.gin"
|
||||
|
||||
type Auth struct {
|
||||
users repositories.UserRepository
|
||||
session repositories.SessionRepository
|
||||
projects repositories.ProjectRepository
|
||||
}
|
||||
|
||||
func (auth *Auth) Login(ctx context.Context, username, password string) (*models.User, error) {
|
||||
user, err := auth.users.Find(ctx, username)
|
||||
if err != nil {
|
||||
select {
|
||||
case <-time.After(time.Millisecond * time.Duration(rand.Int63n(100)+100)):
|
||||
case <-ctx.Done():
|
||||
}
|
||||
return nil, ErrLoginFailed
|
||||
}
|
||||
|
||||
if !user.CheckPassword(password) {
|
||||
select {
|
||||
case <-time.After(time.Millisecond * time.Duration(rand.Int63n(50))):
|
||||
case <-ctx.Done():
|
||||
}
|
||||
return nil, ErrLoginFailed
|
||||
}
|
||||
|
||||
session := models.Session{
|
||||
ID: generate.SessionID(),
|
||||
UserID: user.ID,
|
||||
ExpiryTime: time.Now().Add(time.Hour * 168),
|
||||
}
|
||||
err = auth.session.Save(c.Request.Context(), *session)
|
||||
if err != nil {
|
||||
return nil, ErrInternalLoginFailure
|
||||
}
|
||||
|
||||
if c := ctx.Value(ginCtxKey).(*gin.Context); c != nil {
|
||||
c.SetCookie(authCookieName, session.ID, 3600*168, "/", "", false, true)
|
||||
} else {
|
||||
return nil, ErrInternalLoginFailure
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (auth *Auth) UserFromContext(ctx context.Context) *models.User {
|
||||
user, _ := ctx.Value(authCtxKey).(*models.User)
|
||||
return user
|
||||
}
|
||||
|
||||
func (auth *Auth) ProjectPermission(ctx context.Context, project models.Project) (*models.ProjectPermission, error) {
|
||||
user := auth.UserFromContext(ctx)
|
||||
if user == nil {
|
||||
return nil, ErrPermissionDenied
|
||||
}
|
||||
|
||||
permission, err := auth.projects.GetPermission(ctx, project, *user)
|
||||
if err != nil {
|
||||
return nil, ErrInternalPermissionFailure
|
||||
}
|
||||
if permission.Level == models.ProjectPermissionLevelNoAccess {
|
||||
return nil, ErrPermissionDenied
|
||||
}
|
||||
|
||||
return permission, nil
|
||||
}
|
||||
|
||||
func (auth *Auth) IssuePermission(ctx context.Context, issue models.Issue) (*models.ProjectPermission, error) {
|
||||
user := auth.UserFromContext(ctx)
|
||||
if user == nil {
|
||||
return nil, ErrPermissionDenied
|
||||
}
|
||||
|
||||
permission, err := auth.projects.GetIssuePermission(ctx, issue, *user)
|
||||
if err != nil {
|
||||
return nil, ErrInternalPermissionFailure
|
||||
}
|
||||
if permission.Level == models.ProjectPermissionLevelNoAccess {
|
||||
return nil, ErrPermissionDenied
|
||||
}
|
||||
|
||||
return permission, nil
|
||||
}
|
||||
|
||||
func (auth *Auth) CheckGinSession(c *gin.Context) {
|
||||
ctx := context.WithValue(c.Request.Context(), ginCtxKey, authCtxKey)
|
||||
defer func() {
|
||||
c.Request = c.Request.WithContext(ctx)
|
||||
}()
|
||||
|
||||
cookie, err := c.Cookie(authCookieName)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
session, err := auth.session.Find(c.Request.Context(), cookie)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if time.Until(session.ExpiryTime) < time.Hour*167 {
|
||||
session.ExpiryTime = time.Now().Add(time.Hour * 168)
|
||||
_ = auth.session.Save(c.Request.Context(), *session)
|
||||
c.SetCookie(authCookieName, session.ID, 3600*168, "/", "", false, true)
|
||||
}
|
||||
|
||||
user, err := auth.users.Find(c.Request.Context(), cookie)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
ctx = context.WithValue(ctx, authCtxKey, user)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package services
|
||||
|
||||
type Bundle struct {
|
||||
Auth *Auth
|
||||
}
|
||||
Reference in New Issue
Block a user