Gisle Aune
3 years ago
14 changed files with 442 additions and 7 deletions
-
7api/project.go
-
93api/projectgroup.go
-
1cmd/stufflog2-lambda/main.go
-
1cmd/stufflog2-local/main.go
-
1database/database.go
-
4database/postgres/db.go
-
13database/postgres/project.go
-
140database/postgres/projectgroups.go
-
4internal/generate/ids.go
-
16migrations/postgres/20210418180353_create_table_project_group.sql
-
11migrations/postgres/20210717135720_add_project_column_project_group_id.sql
-
10models/project.go
-
65models/projectgroup.go
-
83services/loader.go
@ -0,0 +1,93 @@ |
|||||
|
package api |
||||
|
|
||||
|
import ( |
||||
|
"github.com/gin-gonic/gin" |
||||
|
"github.com/gissleh/stufflog/database" |
||||
|
"github.com/gissleh/stufflog/internal/auth" |
||||
|
"github.com/gissleh/stufflog/internal/generate" |
||||
|
"github.com/gissleh/stufflog/internal/slerrors" |
||||
|
"github.com/gissleh/stufflog/models" |
||||
|
"github.com/gissleh/stufflog/services" |
||||
|
) |
||||
|
|
||||
|
func ProjectGroup(g *gin.RouterGroup, db database.Database) { |
||||
|
l := services.Loader{DB: db} |
||||
|
|
||||
|
g.GET("/", handler("projectGroups", func(c *gin.Context) (interface{}, error) { |
||||
|
return l.ListProjectGroups(c) |
||||
|
})) |
||||
|
|
||||
|
g.GET("/:id", handler("projectGroup", func(c *gin.Context) (interface{}, error) { |
||||
|
return l.FindProject(c, c.Param("id")) |
||||
|
})) |
||||
|
|
||||
|
g.POST("/", handler("projectGroup", func(c *gin.Context) (interface{}, error) { |
||||
|
group := models.ProjectGroup{} |
||||
|
err := c.BindJSON(&group) |
||||
|
if err != nil { |
||||
|
return nil, slerrors.BadRequest("Invalid JSON") |
||||
|
} |
||||
|
if group.Abbreviation == "" || group.Name == "" { |
||||
|
return nil, slerrors.BadRequest("Abbreviation and name cannot be left empty.") |
||||
|
} |
||||
|
|
||||
|
group.ID = generate.ProjectGroupID() |
||||
|
group.UserID = auth.UserID(c) |
||||
|
if group.CategoryNames == nil { |
||||
|
group.CategoryNames = make(map[string]string) |
||||
|
} |
||||
|
|
||||
|
err = db.ProjectGroups().Insert(c.Request.Context(), group) |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
|
||||
|
return &group, nil |
||||
|
})) |
||||
|
|
||||
|
g.PUT("/:id", handler("projectGroup", func(c *gin.Context) (interface{}, error) { |
||||
|
update := models.ProjectGroupUpdate{} |
||||
|
err := c.BindJSON(&update) |
||||
|
if err != nil { |
||||
|
return nil, slerrors.BadRequest("Invalid JSON") |
||||
|
} |
||||
|
if (update.Name != nil && *update.Name == "") || (update.Abbreviation != nil && *update.Abbreviation == "") { |
||||
|
return nil, slerrors.BadRequest("Abbreviation and name cannot be left empty.") |
||||
|
} |
||||
|
|
||||
|
group, err := db.ProjectGroups().Find(c, c.Param("id")) |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
if group.UserID != auth.UserID(c) { |
||||
|
return nil, slerrors.NotFound("Project group") |
||||
|
} |
||||
|
|
||||
|
group.Update(update) |
||||
|
|
||||
|
err = db.ProjectGroups().Update(c.Request.Context(), *group) |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
|
||||
|
return group, nil |
||||
|
})) |
||||
|
|
||||
|
g.DELETE("/:id", handler("project", func(c *gin.Context) (interface{}, error) { |
||||
|
group, err := l.FindProjectGroup(c.Request.Context(), c.Param("id")) |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
|
||||
|
if len(group.Projects) > 0 { |
||||
|
return nil, slerrors.Forbidden("You cannot delete non-empty project group.") |
||||
|
} |
||||
|
|
||||
|
err = db.ProjectGroups().Delete(c.Request.Context(), group.ProjectGroup) |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
|
||||
|
return group, nil |
||||
|
})) |
||||
|
} |
@ -0,0 +1,140 @@ |
|||||
|
package postgres |
||||
|
|
||||
|
import ( |
||||
|
"context" |
||||
|
"database/sql" |
||||
|
"database/sql/driver" |
||||
|
"encoding/json" |
||||
|
"errors" |
||||
|
"github.com/gissleh/stufflog/internal/slerrors" |
||||
|
"github.com/gissleh/stufflog/models" |
||||
|
"github.com/jmoiron/sqlx" |
||||
|
) |
||||
|
|
||||
|
type projectGroupRepository struct { |
||||
|
db *sqlx.DB |
||||
|
} |
||||
|
|
||||
|
func (r *projectGroupRepository) Find(ctx context.Context, id string) (*models.ProjectGroup, error) { |
||||
|
res := projectGroupDBO{} |
||||
|
err := r.db.GetContext(ctx, &res, "SELECT * FROM project_group WHERE project_group_id=$1", id) |
||||
|
if err != nil { |
||||
|
if err == sql.ErrNoRows { |
||||
|
return nil, slerrors.NotFound("Project group") |
||||
|
} |
||||
|
|
||||
|
return nil, err |
||||
|
} |
||||
|
|
||||
|
return res.ToProjectGroup(), nil |
||||
|
} |
||||
|
|
||||
|
func (r *projectGroupRepository) List(ctx context.Context, filter models.ProjectGroupFilter) ([]*models.ProjectGroup, error) { |
||||
|
res := make([]*projectGroupDBO, 0, 16) |
||||
|
err := r.db.SelectContext(ctx, &res, "SELECT * FROM project_group WHERE user_id=$1", filter.UserID) |
||||
|
if err != nil { |
||||
|
if err == sql.ErrNoRows { |
||||
|
return []*models.ProjectGroup{}, nil |
||||
|
} |
||||
|
|
||||
|
return nil, err |
||||
|
} |
||||
|
|
||||
|
res2 := make([]*models.ProjectGroup, 0, len(res)) |
||||
|
for _, pdo := range res { |
||||
|
res2 = append(res2, pdo.ToProjectGroup()) |
||||
|
} |
||||
|
|
||||
|
return res2, nil |
||||
|
} |
||||
|
|
||||
|
func (r *projectGroupRepository) Insert(ctx context.Context, group models.ProjectGroup) error { |
||||
|
_, err := r.db.NamedExecContext(ctx, ` |
||||
|
INSERT INTO project_group ( |
||||
|
project_group_id, user_id, name, abbreviation, description, category_names |
||||
|
) VALUES ( |
||||
|
:project_group_id, :user_id, :name, :abbreviation, :description, :category_names |
||||
|
) |
||||
|
`, toProjectGroupDBO(group)) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func (r *projectGroupRepository) Update(ctx context.Context, group models.ProjectGroup) error { |
||||
|
if group.CategoryNames == nil { |
||||
|
group.CategoryNames = make(map[string]string) |
||||
|
} |
||||
|
|
||||
|
_, err := r.db.NamedExecContext(ctx, ` |
||||
|
UPDATE project_group SET |
||||
|
name = :name, |
||||
|
abbreviation = :abbreviation, |
||||
|
description = :description, |
||||
|
category_names = :category_names |
||||
|
WHERE project_group_id = :project_group_id |
||||
|
`, toProjectGroupDBO(group)) |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
func (r *projectGroupRepository) Delete(ctx context.Context, group models.ProjectGroup) error { |
||||
|
_, err := r.db.ExecContext(ctx, `DELETE FROM project_group WHERE project_group_id=$1`, group.ID) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
_, err = r.db.ExecContext(ctx, "UPDATE project SET project_group_id=NULL where project_group_id=$1", group.ID) |
||||
|
if err != nil && err != sql.ErrNoRows { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
type projectGroupDBO struct { |
||||
|
ID string `db:"project_group_id"` |
||||
|
UserID string `db:"user_id"` |
||||
|
Name string `db:"name"` |
||||
|
Description string `db:"description"` |
||||
|
Abbreviation string `db:"abbreviation"` |
||||
|
CategoryNames jsonMap `db:"category_names"` |
||||
|
} |
||||
|
|
||||
|
func (dbo *projectGroupDBO) ToProjectGroup() *models.ProjectGroup { |
||||
|
return &models.ProjectGroup{ |
||||
|
ID: dbo.ID, |
||||
|
UserID: dbo.UserID, |
||||
|
Name: dbo.Name, |
||||
|
Description: dbo.Description, |
||||
|
Abbreviation: dbo.Abbreviation, |
||||
|
CategoryNames: dbo.CategoryNames, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func toProjectGroupDBO(group models.ProjectGroup) projectGroupDBO { |
||||
|
return projectGroupDBO{ |
||||
|
ID: group.ID, |
||||
|
UserID: group.UserID, |
||||
|
Name: group.Name, |
||||
|
Description: group.Description, |
||||
|
Abbreviation: group.Abbreviation, |
||||
|
CategoryNames: group.CategoryNames, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
type jsonMap map[string]string |
||||
|
|
||||
|
func (m jsonMap) Value() (driver.Value, error) { |
||||
|
return json.Marshal(m) |
||||
|
} |
||||
|
|
||||
|
func (m *jsonMap) Scan(value interface{}) error { |
||||
|
b, ok := value.([]byte) |
||||
|
if !ok { |
||||
|
return errors.New("type assertion to []byte failed") |
||||
|
} |
||||
|
|
||||
|
return json.Unmarshal(b, m) |
||||
|
} |
@ -0,0 +1,16 @@ |
|||||
|
-- +goose Up |
||||
|
-- +goose StatementBegin |
||||
|
CREATE TABLE project_group ( |
||||
|
project_group_id CHAR(16) NOT NULL PRIMARY KEY, |
||||
|
user_id CHAR(36) NOT NULL, |
||||
|
name TEXT NOT NULL, |
||||
|
abbreviation TEXT NOT NULL, |
||||
|
description TEXT NOT NULL, |
||||
|
category_names JSONB NOT NULL |
||||
|
); |
||||
|
-- +goose StatementEnd |
||||
|
|
||||
|
-- +goose Down |
||||
|
-- +goose StatementBegin |
||||
|
DROP TABLE IF EXISTS project_group; |
||||
|
-- +goose StatementEnd |
@ -0,0 +1,11 @@ |
|||||
|
-- +goose Up |
||||
|
-- +goose StatementBegin |
||||
|
ALTER TABLE project |
||||
|
ADD COLUMN project_group_id CHAR(16) DEFAULT NULL; |
||||
|
-- +goose StatementEnd |
||||
|
|
||||
|
-- +goose Down |
||||
|
-- +goose StatementBegin |
||||
|
ALTER TABLE project |
||||
|
DROP COLUMN IF EXISTS project_group_id; |
||||
|
-- +goose StatementEnd |
Write
Preview
Loading…
Cancel
Save
Reference in new issue