3
0

Refactor channel-members repo

This commit is contained in:
Denis Arh
2019-10-19 16:52:40 +02:00
parent f5baa1ce7a
commit fffeb84abb
8 changed files with 115 additions and 120 deletions

View File

@@ -2,8 +2,6 @@ package repository
import (
"context"
"sort"
"strconv"
"strings"
"time"
@@ -57,10 +55,6 @@ func (r channel) table() string {
return "messaging_channel"
}
func (r channel) tableMember() string {
return "messaging_channel_member"
}
func (r channel) columns() []string {
return []string{
"c.id",
@@ -91,29 +85,10 @@ func (r channel) FindByID(ID uint64) (*types.Channel, error) {
// FindByMemberSet searches for channel (group!) with exactly the same membership structure
func (r channel) FindByMemberSet(memberIDs ...uint64) (*types.Channel, error) {
// Make sure members are sorted
sort.Slice(memberIDs, func(i, j int) bool {
return memberIDs[i] < memberIDs[j]
})
// Concatentating members fore
membersConcat := ""
for i := range memberIDs {
// Don't panic, we're adding , in the SQL as well
membersConcat += strconv.FormatUint(memberIDs[i], 10) + ","
}
return r.findOneBy(
squirrel.And{
squirrel.Eq{"type": types.ChannelTypeGroup},
squirrel.
Select("rel_channel").
From(r.tableMember()).
GroupBy("rel_channel").
Having(squirrel.Eq{
"COUNT(*)": len(memberIDs),
"CONCAT(GROUP_CONCAT(rel_user ORDER BY 1 ASC SEPARATOR ','),',')": membersConcat,
}),
squirrel.ConcatExpr("c.id IN (", (channelMember{}).queryExactMembers(memberIDs...), ")"),
})
}
@@ -145,6 +120,8 @@ func (r channel) Find(filter types.ChannelFilter) (set types.ChannelSet, f types
query := r.query()
query = query.Where(squirrel.Eq{"c.archived_at": nil})
if !f.IncludeDeleted {
query = query.Where(squirrel.Eq{"c.deleted_at": nil})
}
@@ -157,10 +134,7 @@ func (r channel) Find(filter types.ChannelFilter) (set types.ChannelSet, f types
if f.CurrentUserID > 0 {
query = query.Where(squirrel.Or{
squirrel.Eq{"c.type": types.ChannelTypePublic},
squirrel.ConcatExpr("c.id IN (", squirrel.
Select("rel_channel").
From(r.tableMember()).
Where(squirrel.Eq{"rel_user": f.CurrentUserID}), ")"),
squirrel.ConcatExpr("c.id IN (", (channelMember{}).queryAnyMember(f.CurrentUserID), ")"),
})
}

View File

@@ -2,7 +2,10 @@ package repository
import (
"context"
"sort"
"strconv"
"github.com/Masterminds/squirrel"
"github.com/titpetric/factory"
"github.com/cortezaproject/corteza-server/messaging/types"
@@ -14,14 +17,11 @@ type (
ChannelMemberRepository interface {
With(ctx context.Context, db *factory.DB) ChannelMemberRepository
Find(filter *types.ChannelMemberFilter) (types.ChannelMemberSet, error)
Find(filter types.ChannelMemberFilter) (types.ChannelMemberSet, error)
Create(mod *types.ChannelMember) (*types.ChannelMember, error)
Update(mod *types.ChannelMember) (*types.ChannelMember, error)
Delete(channelID, userID uint64) error
CountMemberships(userID uint64) (c int, err error)
ChangeMembership(userID, target uint64) error
}
channelMember struct {
@@ -29,28 +29,6 @@ type (
}
)
const (
// subquery that filters out all channels that current user has access to as a member
// or via channel type (public channels)
sqlChannelAccess = ` (
SELECT id
FROM messaging_channel c
LEFT OUTER JOIN messaging_channel_member AS m ON (c.id = m.rel_channel)
WHERE rel_user = ?
UNION
SELECT id
FROM messaging_channel c
WHERE c.type = ?
)`
// Fetching channel members of all channels a specific user has access to
sqlChannelMemberSelect = `SELECT m.*
FROM messaging_channel_member AS m
INNER JOIN messaging_channel AS c ON (m.rel_channel = c.id)
WHERE c.archived_at IS NULL
AND c.deleted_at IS NULL`
)
// ChannelMember creates new instance of channel member repository
func ChannelMember(ctx context.Context, db *factory.DB) ChannelMemberRepository {
return (&channelMember{}).With(ctx, db)
@@ -63,35 +41,81 @@ func (r *channelMember) With(ctx context.Context, db *factory.DB) ChannelMemberR
}
}
// FindMembers fetches membership info
func (r channelMember) table() string {
return "messaging_channel_member"
}
func (r channelMember) columns() []string {
return []string{
"cm.rel_channel",
"cm.rel_user",
"cm.type",
"cm.flag",
"cm.created_at",
"cm.updated_at",
}
}
func (r channelMember) query() squirrel.SelectBuilder {
return squirrel.
Select(r.columns()...).
From(r.table() + " AS cm")
}
// Finds channel ID(s) with any of the members
//
// If channelID > 0 it returns members of a specific channel
// If userID > 0 it returns members of all channels this user is member of
func (r *channelMember) Find(filter *types.ChannelMemberFilter) (types.ChannelMemberSet, error) {
params := make([]interface{}, 0)
mm := types.ChannelMemberSet{}
// Builds a (sub)query that returns list of channel IDs at least one of the members
//
func (r channelMember) queryAnyMember(memberIDs ...uint64) squirrel.SelectBuilder {
return squirrel.
Select("cm.rel_channel").
From(r.table() + " AS cm").
Where(squirrel.Eq{"cm.rel_user": memberIDs})
}
sql := sqlChannelMemberSelect
if filter != nil {
if filter.ComembersOf > 0 {
// scope: only channel we have access to
sql += " AND m.rel_channel IN " + sqlChannelAccess
params = append(params, filter.ComembersOf, types.ChannelTypePublic)
}
if filter.MemberID > 0 {
sql += " AND m.rel_user = ?"
params = append(params, filter.MemberID)
}
if filter.ChannelID > 0 {
sql += " AND m.rel_channel = ?"
params = append(params, filter.ChannelID)
}
// Finds channel ID(s) with exact membership
//
// Builds a (sub)query that returns list of channel IDs that have this exact membership
//
func (r channelMember) queryExactMembers(memberIDs ...uint64) squirrel.SelectBuilder {
if len(memberIDs) == 0 {
return squirrel.
Select("null")
}
return mm, r.db().Select(&mm, sql, params...)
// Make sure members are sorted
sort.Slice(memberIDs, func(i, j int) bool {
return memberIDs[i] < memberIDs[j]
})
// Concatentating members fore
membersConcat := ""
for i := range memberIDs {
// Don't panic, we're adding , in the SQL as well
membersConcat += strconv.FormatUint(memberIDs[i], 10) + ","
}
return r.queryAnyMember(memberIDs...).
GroupBy("cm.rel_channel").
Having(squirrel.Eq{
"COUNT(*)": len(memberIDs),
"CONCAT(GROUP_CONCAT(cm.rel_user ORDER BY 1 ASC SEPARATOR ','),',')": membersConcat,
})
}
// Find fetches membership info
func (r *channelMember) Find(filter types.ChannelMemberFilter) (set types.ChannelMemberSet, err error) {
query := r.query()
if len(filter.MemberID) > 0 {
query = query.Where(squirrel.Eq{"cm.rel_user": filter.MemberID})
}
if len(filter.ChannelID) > 0 {
query = query.Where(squirrel.Eq{"cm.rel_channel": filter.ChannelID})
}
return set, rh.FetchAll(r.db(), query, &set)
}
// Create adds channel membership record
@@ -116,26 +140,3 @@ func (r *channelMember) Delete(channelID, userID uint64) error {
sql := `DELETE FROM messaging_channel_member WHERE rel_channel = ? AND rel_user = ?`
return exec(r.db().Exec(sql, channelID, userID))
}
func (r *channelMember) CountMemberships(userID uint64) (c int, err error) {
return c, r.db().Get(&c,
"SELECT COUNT(*) FROM messaging_channel_member WHERE rel_user = ?",
userID)
}
func (r *channelMember) ChangeMembership(userID, target uint64) (err error) {
// Remove dups
// with an ugly mysql workaround
_, err = r.db().Exec(
"DELETE FROM messaging_channel_member WHERE rel_user = ? "+
"AND rel_channel IN (SELECT rel_channel FROM (SELECT * FROM messaging_channel_member) AS workaround WHERE rel_user = ?)",
userID,
target)
if err != nil {
return err
}
_, err = r.db().Exec("UPDATE messaging_channel_member SET rel_user = ? WHERE rel_user = ?", target, userID)
return err
}

View File

@@ -42,6 +42,19 @@ type (
const (
MESSAGES_MAX_LIMIT = 100
// subquery that filters out all channels that current user has access to as a member
// or via channel type (public channels)
sqlChannelAccess = ` (
SELECT id
FROM messaging_channel c
LEFT OUTER JOIN messaging_channel_member AS m ON (c.id = m.rel_channel)
WHERE rel_user = ?
UNION
SELECT id
FROM messaging_channel c
WHERE c.type = ?
)`
sqlMessageColumns = "id, " +
"COALESCE(type,'') AS type, " +
"message, " +

View File

@@ -159,7 +159,8 @@ func (svc *channel) preloadMembers(cc types.ChannelSet) (err error) {
mm types.ChannelMemberSet
)
if mm, err = svc.cmember.Find(&types.ChannelMemberFilter{ComembersOf: userID}); err != nil {
// Load membership info of all channels
if mm, err = svc.cmember.Find(types.ChannelMemberFilterChannels(cc.IDs()...)); err != nil {
return
} else {
err = cc.Walk(func(ch *types.Channel) error {
@@ -216,7 +217,7 @@ func (svc *channel) FindMembers(channelID uint64) (out types.ChannelMemberSet, e
return
}
out, err = svc.cmember.Find(&types.ChannelMemberFilter{ChannelID: channelID})
out, err = svc.cmember.Find(types.ChannelMemberFilterChannels(channelID))
if err != nil {
return err
}
@@ -549,7 +550,7 @@ func (svc *channel) SetFlag(ID uint64, flag types.ChannelMembershipFlag) (ch *ty
return
}
if members, err := svc.cmember.Find(&types.ChannelMemberFilter{ChannelID: ch.ID, MemberID: userID}); err != nil {
if members, err := svc.cmember.Find(types.ChannelMemberFilter{ChannelID: []uint64{ch.ID}, MemberID: []uint64{userID}}); err != nil {
return err
} else if len(members) == 1 {
membership = members[0]
@@ -671,7 +672,7 @@ func (svc *channel) InviteUser(channelID uint64, memberIDs ...uint64) (out types
}
return out, svc.db.Transaction(func() (err error) {
if existing, err = svc.cmember.Find(&types.ChannelMemberFilter{ChannelID: channelID}); err != nil {
if existing, err = svc.cmember.Find(types.ChannelMemberFilterChannels(channelID)); err != nil {
return
}
@@ -729,7 +730,7 @@ func (svc *channel) AddMember(channelID uint64, memberIDs ...uint64) (out types.
}
return out, svc.db.Transaction(func() (err error) {
if existing, err = svc.cmember.Find(&types.ChannelMemberFilter{ChannelID: channelID}); err != nil {
if existing, err = svc.cmember.Find(types.ChannelMemberFilterChannels(channelID)); err != nil {
return
}
@@ -819,7 +820,7 @@ func (svc *channel) DeleteMember(channelID uint64, memberIDs ...uint64) (err err
}
return svc.db.Transaction(func() (err error) {
if existing, err = svc.cmember.Find(&types.ChannelMemberFilter{ChannelID: channelID}); err != nil {
if existing, err = svc.cmember.Find(types.ChannelMemberFilterChannels(channelID)); err != nil {
return
}
@@ -882,7 +883,7 @@ func (svc *channel) sendChannelEvent(ch *types.Channel) (err error) {
// Preload members, if needed
if len(ch.Members) == 0 || ch.Member == nil {
if mm, err := svc.cmember.Find(&types.ChannelMemberFilter{ChannelID: ch.ID}); err != nil {
if mm, err := svc.cmember.Find(types.ChannelMemberFilterChannels(ch.ID)); err != nil {
return err
} else {
ch.Members = mm.AllMemberIDs()

View File

@@ -247,7 +247,7 @@ func (svc message) Create(in *types.Message) (m *types.Message, err error) {
//
// reset unreads for all members
var mm types.ChannelMemberSet
mm, err = svc.cmember.Find(&types.ChannelMemberFilter{ChannelID: original.ChannelID})
mm, err = svc.cmember.Find(types.ChannelMemberFilterChannels(original.ChannelID))
if err != nil {
return err
}
@@ -910,7 +910,7 @@ func (svc message) findChannelByID(channelID uint64) (ch *types.Channel, err err
if ch, err = svc.channel.FindByID(channelID); err != nil {
return nil, err
} else if mm, err = svc.cmember.Find(&types.ChannelMemberFilter{ChannelID: ch.ID}); err != nil {
} else if mm, err = svc.cmember.Find(types.ChannelMemberFilterChannels(ch.ID)); err != nil {
return nil, err
} else {
ch.Members = mm.AllMemberIDs()

View File

@@ -18,9 +18,8 @@ type (
}
ChannelMemberFilter struct {
ComembersOf uint64
ChannelID uint64
MemberID uint64
ChannelID []uint64
MemberID []uint64
}
ChannelMembershipType string
@@ -37,3 +36,8 @@ const (
ChannelMembershipFlagIgnored ChannelMembershipFlag = "ignored"
ChannelMembershipFlagNone ChannelMembershipFlag = ""
)
// ChannelMemberFilterChannels helper func for building channel member filter with list of channels
func ChannelMemberFilterChannels(ID ...uint64) ChannelMemberFilter {
return ChannelMemberFilter{ChannelID: ID}
}

View File

@@ -18,7 +18,10 @@ func TestChannelSetFlag(t *testing.T) {
h.repoMakeMember(ch, h.cUser)
flagCheck := func(ID uint64, flag string) {
mm, err := h.repoChMember().Find(&types.ChannelMemberFilter{ChannelID: ID, MemberID: h.cUser.ID})
mm, err := h.repoChMember().Find(types.ChannelMemberFilter{
ChannelID: []uint64{ID},
MemberID: []uint64{h.cUser.ID},
})
h.a.NoError(err)
h.a.Len(mm, 1, "expecting 1 member")
h.a.Equal(flag, string(mm[0].Flag), "expecting flags to match")

View File

@@ -47,14 +47,13 @@ func (h helper) repoMakeMember(ch *types.Channel, u *sysTypes.User) *types.Chann
}
func (h helper) repoChAssertNotMember(ch *types.Channel, u *sysTypes.User) {
mm, err := h.repoChMember().Find(&types.ChannelMemberFilter{ChannelID: ch.ID, MemberID: h.cUser.ID})
mm, err := h.repoChMember().Find(types.ChannelMemberFilter{ChannelID: []uint64{ch.ID}, MemberID: []uint64{h.cUser.ID}})
h.a.NoError(err)
h.a.NotNil(mm)
h.a.NotContains(mm.AllMemberIDs(), u.ID, "not expecting to find a member")
}
func (h helper) repoChAssertMember(ch *types.Channel, u *sysTypes.User, typ types.ChannelMembershipType) {
mm, err := h.repoChMember().Find(&types.ChannelMemberFilter{ChannelID: ch.ID, MemberID: h.cUser.ID})
mm, err := h.repoChMember().Find(types.ChannelMemberFilter{ChannelID: []uint64{ch.ID}, MemberID: []uint64{h.cUser.ID}})
h.a.NoError(err)
h.a.NotNil(mm)