Gisle Aune
5 years ago
commit
4cce632708
69 changed files with 7852 additions and 0 deletions
-
8.idea/.gitignore
-
5.idea/codeStyles/codeStyleConfig.xml
-
11.idea/dataSources.xml
-
13.idea/dictionaries/gisle.xml
-
6.idea/misc.xml
-
8.idea/modules.xml
-
7.idea/sqldialects.xml
-
29.idea/watcherTasks.xml
-
8.idea/xiaoli.iml
-
29database/database.go
-
103database/drivers/mysqldriver/db.go
-
78database/drivers/mysqldriver/db_test.go
-
169database/drivers/mysqldriver/issues.go
-
188database/drivers/mysqldriver/issues_test.go
-
215database/drivers/mysqldriver/items.go
-
142database/drivers/mysqldriver/items_test.go
-
139database/drivers/mysqldriver/projects.go
-
165database/drivers/mysqldriver/projects_test.go
-
49database/drivers/mysqldriver/session.go
-
99database/drivers/mysqldriver/users.go
-
117database/drivers/mysqldriver/users_test.go
-
14database/repositories/issuerepository.go
-
15database/repositories/itemrepository.go
-
17database/repositories/projectrepository.go
-
13database/repositories/sessionrepository.go
-
14database/repositories/userrepository.go
-
17go.mod
-
143go.sum
-
18graph/gqlgen.yml
-
30graph/graph.go
-
4983graph/graphcore/exec_gen.go
-
38graph/graphcore/input_gen.go
-
29graph/resolvers/issue.resolvers.go
-
25graph/resolvers/mutation.resolvers.go
-
29graph/resolvers/project.resolvers.go
-
37graph/resolvers/query.resolvers.go
-
11graph/resolvers/resolver.go
-
37graph/schema/issue.gql
-
4graph/schema/mutation.gql
-
70graph/schema/project.gql
-
14graph/schema/query.gql
-
1graph/schema/scalars.gql
-
18graph/schema/user.gql
-
48internal/generate/generate.go
-
9internal/generate/ids.go
-
18internal/xlerrors/notfound.go
-
75main.go
-
16migrations/mysql/20200405173553_create_table_project.sql
-
30migrations/mysql/20200406105917_create_table_issue.sql
-
16migrations/mysql/20200406110301_create_table_item.sql
-
15migrations/mysql/20200406110546_create_table_item_tag.sql
-
15migrations/mysql/20200406114528_create_table_user.sql
-
15migrations/mysql/20200406160317_create_table_counters.sql
-
16migrations/mysql/20200409113154_create_table_project_permission.sql
-
15migrations/mysql/20200412174220_create_table_session.sql
-
12models/activity.go
-
21models/goal.go
-
39models/issue.go
-
9models/issueitem.go
-
20models/issuetask.go
-
21models/item.go
-
6models/log.go
-
40models/project.go
-
36models/projectpermission.go
-
9models/projectstatus.go
-
9models/session.go
-
39models/user.go
-
133services/auth.go
-
5services/bundle.go
@ -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/ |
@ -0,0 +1,5 @@ |
|||
<component name="ProjectCodeStyleConfiguration"> |
|||
<state> |
|||
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" /> |
|||
</state> |
|||
</component> |
@ -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> |
@ -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> |
@ -0,0 +1,6 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<project version="4"> |
|||
<component name="JavaScriptSettings"> |
|||
<option name="languageLevel" value="ES6" /> |
|||
</component> |
|||
</project> |
@ -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> |
@ -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> |
@ -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> |
@ -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) |
|||
} |
|||
} |
4983
graph/graphcore/exec_gen.go
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -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 |
|||
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue