3
0

upd(system): add team membership apis (#2)

* upd(system): add team membership apis

* fix(all): fix codegen.sh for mac OS

* fix(system): replace, delete and tests
This commit is contained in:
Mitja Z
2018-11-25 11:00:59 +01:00
committed by Tit Petric
parent 338b340998
commit e7ea318f17
11 changed files with 377 additions and 55 deletions
+4 -4
View File
@@ -4,10 +4,10 @@ set -e
_PWD=$PWD
function yellow {
echo -e "\e[33m$@\e[39m"
echo -e "\033[33m$@\033[39m"
}
function green {
echo -e "\e[32m$@\e[39m"
echo -e "\033[32m$@\033[39m"
}
function gofmt {
@@ -56,7 +56,7 @@ types
function database {
yellow "> database"
FOLDERS=$(find -type d -wholename '*/schema/mysql')
FOLDERS=$(find . -type d -wholename '*/schema/mysql')
for FOLDER in $FOLDERS; do
FOLDER=$(dirname $(dirname $FOLDER))
FOLDER=${FOLDER:2}
@@ -70,7 +70,7 @@ database
function files {
yellow "> files"
FOLDERS=$(find -type d -wholename '*/data')
FOLDERS=$(find . -type d -wholename '*/data')
for FOLDER in $FOLDERS; do
FOLDER=$(dirname $FOLDER)
FOLDER=${FOLDER:2}
+30
View File
@@ -242,6 +242,36 @@ An organisation may have many teams. Teams may have many channels available. Acc
| teamID | uint64 | PATH | Source Team ID | N/A | YES |
| destination | uint64 | POST | Destination Team ID | N/A | YES |
## Add member to a team
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/teams/{teamID}/memberAdd` | HTTP/S | POST | Client ID, Session ID |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| teamID | uint64 | PATH | Source Team ID | N/A | YES |
| userID | uint64 | POST | User ID | N/A | YES |
## Remove member from a team
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/teams/{teamID}/memberRemove` | HTTP/S | POST | Client ID, Session ID |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| teamID | uint64 | PATH | Source Team ID | N/A | YES |
| userID | uint64 | POST | User ID | N/A | YES |
+49 -1
View File
@@ -313,6 +313,54 @@
}
]
}
},
{
"name": "memberAdd",
"method": "POST",
"title": "Add member to a team",
"path": "/{teamID}/memberAdd",
"parameters": {
"path": [
{
"type": "uint64",
"name": "teamID",
"required": true,
"title": "Source Team ID"
}
],
"post": [
{
"type": "uint64",
"name": "userID",
"required": true,
"title": "User ID"
}
]
}
},
{
"name": "memberRemove",
"method": "POST",
"title": "Remove member from a team",
"path": "/{teamID}/memberRemove",
"parameters": {
"path": [
{
"type": "uint64",
"name": "teamID",
"required": true,
"title": "Source Team ID"
}
],
"post": [
{
"type": "uint64",
"name": "userID",
"required": true,
"title": "User ID"
}
]
}
}
]
},
@@ -343,4 +391,4 @@
}
]
}
]
]
+48
View File
@@ -175,6 +175,54 @@
}
]
}
},
{
"Name": "memberAdd",
"Method": "POST",
"Title": "Add member to a team",
"Path": "/{teamID}/memberAdd",
"Parameters": {
"path": [
{
"name": "teamID",
"required": true,
"title": "Source Team ID",
"type": "uint64"
}
],
"post": [
{
"name": "userID",
"required": true,
"title": "User ID",
"type": "uint64"
}
]
}
},
{
"Name": "memberRemove",
"Method": "POST",
"Title": "Remove member from a team",
"Path": "/{teamID}/memberRemove",
"Parameters": {
"path": [
{
"name": "teamID",
"required": true,
"title": "Source Team ID",
"type": "uint64"
}
],
"post": [
{
"name": "userID",
"required": true,
"title": "User ID",
"type": "uint64"
}
]
}
}
]
}
+36 -18
View File
@@ -13,15 +13,17 @@ type (
TeamRepository interface {
With(ctx context.Context, db *factory.DB) TeamRepository
FindTeamByID(id uint64) (*types.Team, error)
FindTeams(filter *types.TeamFilter) ([]*types.Team, error)
CreateTeam(mod *types.Team) (*types.Team, error)
UpdateTeam(mod *types.Team) (*types.Team, error)
ArchiveTeamByID(id uint64) error
UnarchiveTeamByID(id uint64) error
DeleteTeamByID(id uint64) error
MergeTeamByID(id, targetTeamID uint64) error
MoveTeamByID(id, targetOrganisationID uint64) error
FindByID(id uint64) (*types.Team, error)
Find(filter *types.TeamFilter) ([]*types.Team, error)
Create(mod *types.Team) (*types.Team, error)
Update(mod *types.Team) (*types.Team, error)
ArchiveByID(id uint64) error
UnarchiveByID(id uint64) error
DeleteByID(id uint64) error
MergeByID(id, targetTeamID uint64) error
MoveByID(id, targetOrganisationID uint64) error
MemberAddByID(id, userID uint64) error
MemberRemoveByID(id, userID uint64) error
}
team struct {
@@ -51,14 +53,14 @@ func (r *team) With(ctx context.Context, db *factory.DB) TeamRepository {
}
}
func (r *team) FindTeamByID(id uint64) (*types.Team, error) {
func (r *team) FindByID(id uint64) (*types.Team, error) {
sql := "SELECT * FROM " + r.teams + " WHERE id = ? AND " + sqlTeamScope
mod := &types.Team{}
return mod, isFound(r.db().Get(mod, sql, id), mod.ID > 0, ErrTeamNotFound)
}
func (r *team) FindTeams(filter *types.TeamFilter) ([]*types.Team, error) {
func (r *team) Find(filter *types.TeamFilter) ([]*types.Team, error) {
rval := make([]*types.Team, 0)
params := make([]interface{}, 0)
@@ -76,35 +78,51 @@ func (r *team) FindTeams(filter *types.TeamFilter) ([]*types.Team, error) {
return rval, r.db().Select(&rval, sql, params...)
}
func (r *team) CreateTeam(mod *types.Team) (*types.Team, error) {
func (r *team) Create(mod *types.Team) (*types.Team, error) {
mod.ID = factory.Sonyflake.NextID()
mod.CreatedAt = time.Now()
return mod, r.db().Insert(r.teams, mod)
}
func (r *team) UpdateTeam(mod *types.Team) (*types.Team, error) {
func (r *team) Update(mod *types.Team) (*types.Team, error) {
mod.UpdatedAt = timeNowPtr()
return mod, r.db().Replace(r.teams, mod)
}
func (r *team) ArchiveTeamByID(id uint64) error {
func (r *team) ArchiveByID(id uint64) error {
return r.updateColumnByID(r.teams, "archived_at", time.Now(), id)
}
func (r *team) UnarchiveTeamByID(id uint64) error {
func (r *team) UnarchiveByID(id uint64) error {
return r.updateColumnByID(r.teams, "archived_at", nil, id)
}
func (r *team) DeleteTeamByID(id uint64) error {
func (r *team) DeleteByID(id uint64) error {
return r.updateColumnByID(r.teams, "deleted_at", time.Now(), id)
}
func (r *team) MergeTeamByID(id, targetTeamID uint64) error {
func (r *team) MergeByID(id, targetTeamID uint64) error {
return ErrNotImplemented
}
func (r *team) MoveTeamByID(id, targetOrganisationID uint64) error {
func (r *team) MoveByID(id, targetOrganisationID uint64) error {
return ErrNotImplemented
}
func (r *team) MemberAddByID(id, userID uint64) error {
mod := &types.TeamMember{
TeamID: id,
UserId: userID,
}
return r.db().Replace(r.members, mod)
}
func (r *team) MemberRemoveByID(id, userID uint64) error {
mod := &types.TeamMember{
TeamID: id,
UserId: userID,
}
return r.db().Delete(r.members, mod, "rel_team", "rel_user")
}
+44 -15
View File
@@ -16,47 +16,76 @@ func TestTeam(t *testing.T) {
return
}
rpo := Team(context.Background(), factory.Database.MustGet())
userRepo := User(context.Background(), factory.Database.MustGet())
user := &types.User{
Name: "John Doe",
Username: "johndoe",
}
user.GeneratePassword("johndoe")
{
u1, err := userRepo.Create(user)
assert(t, err == nil, "User.Create error: %v", err)
assert(t, user.ID == u1.ID, "Changes were not stored")
}
teamRepo := Team(context.Background(), factory.Database.MustGet())
team := &types.Team{
Name: "Test team v1",
}
{
t1, err := rpo.CreateTeam(team)
assert(t, err == nil, "CreateTeam error: %v", err)
t1, err := teamRepo.Create(team)
assert(t, err == nil, "Team.Create error: %v", err)
assert(t, team.Name == t1.Name, "Changes were not stored")
}
{
team.Name = "Test team v2"
t1, err := rpo.UpdateTeam(team)
assert(t, err == nil, "UpdateTeam error: %v", err)
t1, err := teamRepo.Update(team)
assert(t, err == nil, "Team.Update error: %v", err)
assert(t, team.Name == t1.Name, "Changes were not stored")
}
{
t1, err := rpo.FindTeamByID(team.ID)
assert(t, err == nil, "FindTeamByID error: %v", err)
t1, err := teamRepo.FindByID(team.ID)
assert(t, err == nil, "Team.FindByID error: %v", err)
assert(t, team.Name == t1.Name, "Changes were not stored")
}
{
aa, err := rpo.FindTeams(&types.TeamFilter{Query: team.Name})
assert(t, err == nil, "FindTeams error: %v", err)
aa, err := teamRepo.Find(&types.TeamFilter{Query: team.Name})
assert(t, err == nil, "Team.Find error: %v", err)
assert(t, len(aa) > 0, "No results found")
}
{
err := rpo.ArchiveTeamByID(team.ID)
assert(t, err == nil, "ArchiveTeamByID error: %v", err)
err := teamRepo.ArchiveByID(team.ID)
assert(t, err == nil, "Team.ArchiveByID error: %v", err)
}
{
err := rpo.UnarchiveTeamByID(team.ID)
assert(t, err == nil, "UnarchiveTeamByID error: %v", err)
err := teamRepo.UnarchiveByID(team.ID)
assert(t, err == nil, "Team.UnarchiveByID error: %v", err)
}
{
err := rpo.DeleteTeamByID(team.ID)
assert(t, err == nil, "DeleteTeamByID error: %v", err)
err := teamRepo.MemberAddByID(team.ID, user.ID)
assert(t, err == nil, "Team.MemberAddByID error: %v", err)
}
{
err := teamRepo.MemberRemoveByID(team.ID, user.ID)
assert(t, err == nil, "Team.MemberRemoveByID error: %v", err)
}
{
err := teamRepo.DeleteByID(team.ID)
assert(t, err == nil, "Team.DeleteByID error: %v", err)
}
{
err := userRepo.DeleteByID(user.ID)
assert(t, err == nil, "User.DeleteByID error: %v", err)
}
}
+28 -8
View File
@@ -35,18 +35,22 @@ type TeamAPI interface {
Archive(context.Context, *request.TeamArchive) (interface{}, error)
Move(context.Context, *request.TeamMove) (interface{}, error)
Merge(context.Context, *request.TeamMerge) (interface{}, error)
MemberAdd(context.Context, *request.TeamMemberAdd) (interface{}, error)
MemberRemove(context.Context, *request.TeamMemberRemove) (interface{}, error)
}
// HTTP API interface
type Team struct {
List func(http.ResponseWriter, *http.Request)
Create func(http.ResponseWriter, *http.Request)
Edit func(http.ResponseWriter, *http.Request)
Read func(http.ResponseWriter, *http.Request)
Remove func(http.ResponseWriter, *http.Request)
Archive func(http.ResponseWriter, *http.Request)
Move func(http.ResponseWriter, *http.Request)
Merge func(http.ResponseWriter, *http.Request)
List func(http.ResponseWriter, *http.Request)
Create func(http.ResponseWriter, *http.Request)
Edit func(http.ResponseWriter, *http.Request)
Read func(http.ResponseWriter, *http.Request)
Remove func(http.ResponseWriter, *http.Request)
Archive func(http.ResponseWriter, *http.Request)
Move func(http.ResponseWriter, *http.Request)
Merge func(http.ResponseWriter, *http.Request)
MemberAdd func(http.ResponseWriter, *http.Request)
MemberRemove func(http.ResponseWriter, *http.Request)
}
func NewTeam(th TeamAPI) *Team {
@@ -107,6 +111,20 @@ func NewTeam(th TeamAPI) *Team {
return th.Merge(r.Context(), params)
})
},
MemberAdd: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewTeamMemberAdd()
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
return th.MemberAdd(r.Context(), params)
})
},
MemberRemove: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewTeamMemberRemove()
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
return th.MemberRemove(r.Context(), params)
})
},
}
}
@@ -122,6 +140,8 @@ func (th *Team) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http
r.Post("/{teamID}/archive", th.Archive)
r.Post("/{teamID}/move", th.Move)
r.Post("/{teamID}/merge", th.Merge)
r.Post("/{teamID}/memberAdd", th.MemberAdd)
r.Post("/{teamID}/memberRemove", th.MemberRemove)
})
})
}
+96
View File
@@ -398,3 +398,99 @@ func (t *TeamMerge) Fill(r *http.Request) (err error) {
}
var _ RequestFiller = NewTeamMerge()
// Team memberAdd request parameters
type TeamMemberAdd struct {
TeamID uint64 `json:",string"`
UserID uint64 `json:",string"`
}
func NewTeamMemberAdd() *TeamMemberAdd {
return &TeamMemberAdd{}
}
func (t *TeamMemberAdd) Fill(r *http.Request) (err error) {
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(r.Body).Decode(t)
switch {
case err == io.EOF:
err = nil
case err != nil:
return errors.Wrap(err, "error parsing http request body")
}
}
if err = r.ParseForm(); err != nil {
return err
}
get := map[string]string{}
post := map[string]string{}
urlQuery := r.URL.Query()
for name, param := range urlQuery {
get[name] = string(param[0])
}
postVars := r.Form
for name, param := range postVars {
post[name] = string(param[0])
}
t.TeamID = parseUInt64(chi.URLParam(r, "teamID"))
if val, ok := post["userID"]; ok {
t.UserID = parseUInt64(val)
}
return err
}
var _ RequestFiller = NewTeamMemberAdd()
// Team memberRemove request parameters
type TeamMemberRemove struct {
TeamID uint64 `json:",string"`
UserID uint64 `json:",string"`
}
func NewTeamMemberRemove() *TeamMemberRemove {
return &TeamMemberRemove{}
}
func (t *TeamMemberRemove) Fill(r *http.Request) (err error) {
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(r.Body).Decode(t)
switch {
case err == io.EOF:
err = nil
case err != nil:
return errors.Wrap(err, "error parsing http request body")
}
}
if err = r.ParseForm(); err != nil {
return err
}
get := map[string]string{}
post := map[string]string{}
urlQuery := r.URL.Query()
for name, param := range urlQuery {
get[name] = string(param[0])
}
postVars := r.Form
for name, param := range postVars {
post[name] = string(param[0])
}
t.TeamID = parseUInt64(chi.URLParam(r, "teamID"))
if val, ok := post["userID"]; ok {
t.UserID = parseUInt64(val)
}
return err
}
var _ RequestFiller = NewTeamMemberRemove()
+8
View File
@@ -66,3 +66,11 @@ func (ctrl *Team) Merge(ctx context.Context, r *request.TeamMerge) (interface{},
func (ctrl *Team) Move(ctx context.Context, r *request.TeamMove) (interface{}, error) {
return nil, ctrl.svc.team.With(ctx).Move(r.TeamID, r.OrganisationID)
}
func (ctrl *Team) MemberAdd(ctx context.Context, r *request.TeamMemberAdd) (interface{}, error) {
return nil, ctrl.svc.team.With(ctx).Merge(r.TeamID, r.UserID)
}
func (ctrl *Team) MemberRemove(ctx context.Context, r *request.TeamMemberRemove) (interface{}, error) {
return nil, ctrl.svc.team.With(ctx).Move(r.TeamID, r.UserID)
}
+22 -9
View File
@@ -31,6 +31,9 @@ type (
Archive(ID uint64) error
Unarchive(ID uint64) error
Delete(ID uint64) error
MemberAdd(teamID, userID uint64) error
MemberRemove(teamID, userID uint64) error
}
)
@@ -49,56 +52,66 @@ func (svc *team) With(ctx context.Context) TeamService {
func (svc *team) FindByID(id uint64) (*types.Team, error) {
// @todo: permission check if current user has access to this team
return svc.team.FindTeamByID(id)
return svc.team.FindByID(id)
}
func (svc *team) Find(filter *types.TeamFilter) ([]*types.Team, error) {
// @todo: permission check to return only teams that current user has access to
return svc.team.FindTeams(filter)
return svc.team.Find(filter)
}
func (svc *team) Create(mod *types.Team) (*types.Team, error) {
// @todo: permission check if current user can add/edit team
return svc.team.CreateTeam(mod)
return svc.team.Create(mod)
}
func (svc *team) Update(mod *types.Team) (*types.Team, error) {
// @todo: permission check if current user can add/edit team
// @todo: make sure archived & deleted entries can not be edited
return svc.team.UpdateTeam(mod)
return svc.team.Update(mod)
}
func (svc *team) Delete(id uint64) error {
// @todo: make history unavailable
// @todo: notify users that team has been removed (remove from web UI)
// @todo: permissions check if current user can remove team
return svc.team.DeleteTeamByID(id)
return svc.team.DeleteByID(id)
}
func (svc *team) Archive(id uint64) error {
// @todo: make history unavailable
// @todo: notify users that team has been removed (remove from web UI)
// @todo: permissions check if current user can remove team
return svc.team.ArchiveTeamByID(id)
return svc.team.ArchiveByID(id)
}
func (svc *team) Unarchive(id uint64) error {
// @todo: permissions check if current user can unarchive team
// @todo: make history accessible
// @todo: notify users that team has been unarchived
return svc.team.UnarchiveTeamByID(id)
return svc.team.UnarchiveByID(id)
}
func (svc *team) Merge(id, targetTeamID uint64) error {
// @todo: permission check if current user can merge team
return svc.team.MergeTeamByID(id, targetTeamID)
return svc.team.MergeByID(id, targetTeamID)
}
func (svc *team) Move(id, targetOrganisationID uint64) error {
// @todo: permission check if current user can move team to another organisation
return svc.team.MoveTeamByID(id, targetOrganisationID)
return svc.team.MoveByID(id, targetOrganisationID)
}
func (svc *team) MemberAdd(id, userID uint64) error {
// @todo: permission check if current user can add user in to a team
return svc.team.MemberAddByID(id, userID)
}
func (svc *team) MemberRemove(id, userID uint64) error {
// @todo: permission check if current user can remove user from a team
return svc.team.MemberRemoveByID(id, userID)
}
var _ TeamService = &team{}
+12
View File
@@ -0,0 +1,12 @@
package types
type (
TeamMember struct {
TeamID uint64 `db:"rel_team"`
UserId uint64 `db:"rel_user"`
}
TeamMemberFilter struct {
Query string
}
)