Files
f2bafcf820 [GH-28202]: Added GetGroupsByNames API (#33558)
* feat: Added GetGroupsByNames API
This commit implements the endpoint discussed in issue #28202.

This adds a new API endpoint to get multiple groups by a list of
names.

Previously, when the app received a post with @ mentions that it
didn't recognize, it would attempt to fetch them all as users,
then if some were still missing, it would go one by one attempting
to fetch each as a group. Now we just fetch all the groups at
once, just like we do for users.

Also added unit tests for the new API and it's respective
documentation.

* Added server version to GetGroupsByNames documentation

Co-authored-by: Harrison Healey <[email protected]>

* fix: updated status_profile_polling tests to use new endpoint

* fix: fixed mock test

Was using get for post request

---------

Co-authored-by: Harrison Healey <[email protected]>
2025-08-20 18:20:07 -04:00

1996 lines
63 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"fmt"
"strings"
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/v8/channels/store"
)
type selectType int
const (
selectGroups selectType = iota
selectCountGroups
)
type groupTeam struct {
model.GroupSyncable
TeamId string
}
type groupChannel struct {
model.GroupSyncable
ChannelId string
}
type groupTeamJoin struct {
groupTeam
TeamDisplayName string
TeamType string
}
type groupChannelJoin struct {
groupChannel
ChannelDisplayName string
TeamDisplayName string
TeamType string
ChannelType string
TeamId string
}
type SqlGroupStore struct {
*SqlStore
userGroupsSelectQuery sq.SelectBuilder
groupMembersSelectQuery sq.SelectBuilder
groupMemberUsersSelectQuery sq.SelectBuilder
groupTeamsSelectQuery sq.SelectBuilder
groupChannelsSelectQuery sq.SelectBuilder
}
func newSqlGroupStore(sqlStore *SqlStore) store.GroupStore {
s := &SqlGroupStore{SqlStore: sqlStore}
s.userGroupsSelectQuery = s.getQueryBuilder().
Select(
"UserGroups.Id",
"UserGroups.Name",
"UserGroups.DisplayName",
"UserGroups.Description",
"UserGroups.Source",
"UserGroups.RemoteId",
"UserGroups.CreateAt",
"UserGroups.UpdateAt",
"UserGroups.DeleteAt",
"UserGroups.AllowReference",
).
From("UserGroups")
s.groupMembersSelectQuery = s.getQueryBuilder().
Select(
"GroupMembers.GroupId",
"GroupMembers.UserId",
"GroupMembers.CreateAt",
"GroupMembers.DeleteAt",
).From("GroupMembers")
s.groupMemberUsersSelectQuery = s.getQueryBuilder().
Select(getUsersColumns()...).
From("GroupMembers").
Join("Users ON Users.Id = GroupMembers.UserId")
s.groupTeamsSelectQuery = s.getQueryBuilder().
Select(
"GroupTeams.GroupId",
"GroupTeams.TeamId",
"GroupTeams.AutoAdd",
"GroupTeams.SchemeAdmin",
"GroupTeams.CreateAt",
"GroupTeams.UpdateAt",
"GroupTeams.DeleteAt",
).From("GroupTeams")
s.groupChannelsSelectQuery = s.getQueryBuilder().
Select(
"GroupChannels.GroupId",
"GroupChannels.ChannelId",
"GroupChannels.AutoAdd",
"GroupChannels.SchemeAdmin",
"GroupChannels.CreateAt",
"GroupChannels.UpdateAt",
"GroupChannels.DeleteAt",
).From("GroupChannels")
return s
}
func (s *SqlGroupStore) Create(group *model.Group) (*model.Group, error) {
if group.Id != "" {
return nil, store.NewErrInvalidInput("Group", "id", group.Id)
}
if err := group.IsValidForCreate(); err != nil {
return nil, err
}
group.Id = model.NewId()
group.CreateAt = model.GetMillis()
group.UpdateAt = group.CreateAt
if _, err := s.GetMaster().NamedExec(`INSERT INTO UserGroups
(Id, Name, DisplayName, Description, Source, RemoteId, CreateAt, UpdateAt, DeleteAt, AllowReference)
VALUES
(:Id, :Name, :DisplayName, :Description, :Source, :RemoteId, :CreateAt, :UpdateAt, :DeleteAt, :AllowReference)`, group); err != nil {
if IsUniqueConstraintError(err, []string{"Name", "groups_name_key"}) {
return nil, errors.Wrapf(err, "Group with name %s already exists", *group.Name)
}
return nil, errors.Wrap(err, "failed to save Group")
}
return group, nil
}
func (s *SqlGroupStore) CreateWithUserIds(g *model.GroupWithUserIds) (_ *model.Group, err error) {
if g.Id != "" {
return nil, store.NewErrInvalidInput("Group", "id", g.Id)
}
// Check if group values are formatted correctly
if appErr := g.IsValidForCreate(); appErr != nil {
return nil, appErr
}
// Check Users exist
if err = s.checkUsersExist(g.UserIds); err != nil {
return nil, err
}
g.Id = model.NewId()
g.CreateAt = model.GetMillis()
g.UpdateAt = g.CreateAt
groupInsertQuery, groupInsertArgs, err := s.getQueryBuilder().
Insert("UserGroups").
Columns("Id", "Name", "DisplayName", "Description", "Source", "RemoteId", "CreateAt", "UpdateAt", "DeleteAt", "AllowReference").
Values(g.Id, g.Name, g.DisplayName, g.Description, g.Source, g.RemoteId, g.CreateAt, g.UpdateAt, 0, g.AllowReference).
ToSql()
if err != nil {
return nil, err
}
usersInsertQuery, usersInsertArgs, err := s.buildInsertGroupUsersQuery(g.Id, g.UserIds)
if err != nil {
return nil, err
}
txn, err := s.GetMaster().Beginx()
if err != nil {
return nil, err
}
defer finalizeTransactionX(txn, &err)
// Create a new usergroup
if _, err = txn.Exec(groupInsertQuery, groupInsertArgs...); err != nil {
if IsUniqueConstraintError(err, []string{"Name", "groups_name_key"}) {
return nil, store.NewErrUniqueConstraint("Name")
}
return nil, errors.Wrap(err, "failed to save Group")
}
// Insert the Group Members
if _, err = executePossiblyEmptyQuery(txn, usersInsertQuery, usersInsertArgs...); err != nil {
return nil, err
}
groupGroupQuery := s.userGroupsSelectQuery.
Column("A.Count AS MemberCount").
InnerJoin(`(
SELECT
UserGroups.Id,
COUNT(GroupMembers.UserId) AS Count
FROM
UserGroups
LEFT JOIN GroupMembers ON UserGroups.Id = GroupMembers.GroupId
WHERE
UserGroups.Id = ?
GROUP BY
UserGroups.Id
ORDER BY
UserGroups.DisplayName,
UserGroups.Id
LIMIT
1 OFFSET 0
) AS A ON UserGroups.Id = A.Id`, g.Id).
OrderBy("UserGroups.CreateAt DESC")
var newGroup group
if err = txn.GetBuilder(&newGroup, groupGroupQuery); err != nil {
return nil, err
}
if err = txn.Commit(); err != nil {
return nil, err
}
return newGroup.ToModel(), nil
}
func (s *SqlGroupStore) checkUsersExist(userIDs []string) error {
if len(userIDs) == 0 {
return nil
}
usersSelectQuery := s.getQueryBuilder().
Select("Id").
From("Users").
Where(sq.Eq{"Id": userIDs, "DeleteAt": 0})
var rows []string
err := s.GetReplica().SelectBuilder(&rows, usersSelectQuery)
if err != nil {
return err
}
if len(rows) == len(userIDs) {
return nil
}
retrievedIDs := make(map[string]bool)
for _, userID := range rows {
retrievedIDs[userID] = true
}
for _, userID := range userIDs {
if _, ok := retrievedIDs[userID]; !ok {
return store.NewErrNotFound("User", userID)
}
}
return nil
}
func (s *SqlGroupStore) buildInsertGroupUsersQuery(groupId string, userIds []string) (query string, args []any, err error) {
if len(userIds) > 0 {
builder := s.getQueryBuilder().
Insert("GroupMembers").
Columns("GroupId", "UserId", "CreateAt", "DeleteAt")
for _, userId := range userIds {
builder = builder.Values(groupId, userId, model.GetMillis(), 0)
}
query, args, err = builder.ToSql()
}
return
}
func (s *SqlGroupStore) Get(groupId string) (*model.Group, error) {
var group model.Group
builder := s.userGroupsSelectQuery.
Where(sq.Eq{"Id": groupId})
if err := s.GetReplica().GetBuilder(&group, builder); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Group", groupId)
}
return nil, errors.Wrapf(err, "failed to get Group with id=%s", groupId)
}
return &group, nil
}
func (s *SqlGroupStore) GetByName(name string, opts model.GroupSearchOpts) (*model.Group, error) {
var group model.Group
query := s.userGroupsSelectQuery.
Where(sq.Eq{"Name": name})
if opts.FilterAllowReference {
query = query.Where("AllowReference = true")
}
if err := s.GetReplica().GetBuilder(&group, query); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Group", fmt.Sprintf("name=%s", name))
}
return nil, errors.Wrapf(err, "failed to get Group with name=%s", name)
}
return &group, nil
}
func (s *SqlGroupStore) GetByNames(names []string, viewRestrictions *model.ViewUsersRestrictions) ([]*model.Group, error) {
groups := []*model.Group{}
query := s.userGroupsSelectQuery.Where(sq.Eq{"Name": names})
query = applyViewRestrictionsFilter(query, viewRestrictions, true)
if err := s.GetReplica().SelectBuilder(&groups, query); err != nil {
return nil, errors.Wrap(err, "failed to find Groups by names")
}
return groups, nil
}
func (s *SqlGroupStore) GetByIDs(groupIDs []string) ([]*model.Group, error) {
groups := []*model.Group{}
query := s.userGroupsSelectQuery.Where(sq.Eq{"Id": groupIDs})
if err := s.GetReplica().SelectBuilder(&groups, query); err != nil {
return nil, errors.Wrap(err, "failed to find Groups by ids")
}
return groups, nil
}
func (s *SqlGroupStore) GetByRemoteID(remoteID string, groupSource model.GroupSource) (*model.Group, error) {
var group model.Group
builder := s.userGroupsSelectQuery.
Where(sq.Eq{
"RemoteId": remoteID,
"Source": groupSource,
})
if err := s.GetReplica().GetBuilder(&group, builder); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Group", fmt.Sprintf("remoteId=%s", remoteID))
}
return nil, errors.Wrapf(err, "failed to get Group with remoteId=%s", remoteID)
}
return &group, nil
}
func (s *SqlGroupStore) GetAllBySource(groupSource model.GroupSource) ([]*model.Group, error) {
groups := []*model.Group{}
builder := s.userGroupsSelectQuery.
Where(sq.Eq{
"DeleteAt": 0,
"Source": groupSource,
})
if err := s.GetReplica().SelectBuilder(&groups, builder); err != nil {
return nil, errors.Wrapf(err, "failed to find Groups by groupSource=%v", groupSource)
}
return groups, nil
}
func (s *SqlGroupStore) GetByUser(userID string, opts model.GroupSearchOpts) ([]*model.Group, error) {
groups := []*model.Group{}
builder := s.userGroupsSelectQuery.
Join("GroupMembers ON GroupMembers.GroupId = UserGroups.Id").
Where(sq.Eq{
"GroupMembers.DeleteAt": 0,
"GroupMembers.UserId": userID,
})
if opts.FilterAllowReference {
builder = builder.Where("UserGroups.AllowReference = true")
}
if err := s.GetReplica().SelectBuilder(&groups, builder); err != nil {
return nil, errors.Wrapf(err, "failed to find Groups with userId=%s", userID)
}
return groups, nil
}
func (s *SqlGroupStore) Update(group *model.Group) (*model.Group, error) {
var retrievedGroup model.Group
builder := s.userGroupsSelectQuery.Where(sq.Eq{"Id": group.Id})
if err := s.GetReplica().GetBuilder(&retrievedGroup, builder); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Group", group.Id)
}
return nil, errors.Wrapf(err, "failed to get Group with id=%s", group.Id)
}
// If updating DeleteAt it can only be to 0
if group.DeleteAt != retrievedGroup.DeleteAt && group.DeleteAt != 0 {
return nil, errors.New("DeleteAt should be 0 when updating")
}
// Reset these properties, don't update them based on input
group.CreateAt = retrievedGroup.CreateAt
group.UpdateAt = model.GetMillis()
if err := group.IsValidForUpdate(); err != nil {
return nil, err
}
res, err := s.GetMaster().NamedExec(`UPDATE UserGroups
SET Name=:Name, DisplayName=:DisplayName, Description=:Description, Source=:Source,
RemoteId=:RemoteId, CreateAt=:CreateAt, UpdateAt=:UpdateAt, DeleteAt=:DeleteAt, AllowReference=:AllowReference
WHERE Id=:Id`, group)
if err != nil {
if IsUniqueConstraintError(err, []string{"Name", "groups_name_key"}) {
return nil, store.NewErrUniqueConstraint("Name")
}
return nil, errors.Wrap(err, "failed to update Group")
}
rowsChanged, _ := res.RowsAffected()
if rowsChanged > 1 {
return nil, errors.Wrapf(err, "multiple Groups were update: %d", rowsChanged)
}
return group, nil
}
func (s *SqlGroupStore) Delete(groupID string) (*model.Group, error) {
var group model.Group
builder := s.userGroupsSelectQuery.
Where(sq.Eq{
"Id": groupID,
"DeleteAt": 0,
})
if err := s.GetReplica().GetBuilder(&group, builder); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Group", groupID)
}
return nil, errors.Wrapf(err, "failed to get Group with id=%s", groupID)
}
time := model.GetMillis()
group.DeleteAt = time
group.UpdateAt = time
if _, err := s.GetMaster().Exec(`UPDATE UserGroups
SET DeleteAt=?, UpdateAt=?
WHERE Id=? AND DeleteAt=0`, group.DeleteAt, group.UpdateAt, groupID); err != nil {
return nil, errors.Wrapf(err, "failed to update Group with id=%s", groupID)
}
return &group, nil
}
func (s *SqlGroupStore) Restore(groupID string) (*model.Group, error) {
var group model.Group
builder := s.userGroupsSelectQuery.
Where(sq.And{
sq.Eq{"Id": groupID},
sq.NotEq{"DeleteAt": 0},
})
if err := s.GetReplica().GetBuilder(&group, builder); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Group", groupID)
}
return nil, errors.Wrapf(err, "failed to get Group with id=%s", groupID)
}
group.UpdateAt = model.GetMillis()
group.DeleteAt = 0
if _, err := s.GetMaster().Exec(`UPDATE UserGroups
SET DeleteAt=0, UpdateAt=?
WHERE Id=? AND DeleteAt!=0`, group.UpdateAt, groupID); err != nil {
return nil, errors.Wrapf(err, "failed to update Group with id=%s", groupID)
}
return &group, nil
}
func (s *SqlGroupStore) GetMember(groupID, userID string) (*model.GroupMember, error) {
builder := s.groupMembersSelectQuery.
Where(sq.Eq{"UserId": userID}).
Where(sq.Eq{"GroupId": groupID}).
Where(sq.Eq{"DeleteAt": 0})
var groupMember model.GroupMember
if err := s.GetReplica().GetBuilder(&groupMember, builder); err != nil {
return nil, errors.Wrap(err, "GetMember")
}
return &groupMember, nil
}
func (s *SqlGroupStore) GetMemberUsers(groupID string) ([]*model.User, error) {
groupMembers := []*model.User{}
builder := s.groupMemberUsersSelectQuery.
Where(sq.Eq{
"GroupMembers.DeleteAt": 0,
"Users.DeleteAt": 0,
"GroupMembers.GroupId": groupID,
})
if err := s.GetReplica().SelectBuilder(&groupMembers, builder); err != nil {
return nil, errors.Wrapf(err, "failed to find member Users for Group with id=%s", groupID)
}
return groupMembers, nil
}
func (s *SqlGroupStore) GetMemberUsersPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
return s.GetMemberUsersSortedPage(groupID, page, perPage, viewRestrictions, model.ShowUsername)
}
func (s *SqlGroupStore) GetMemberUsersSortedPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions, teammateNameDisplay string) ([]*model.User, error) {
groupMembers := []*model.User{}
userQuery := s.groupMemberUsersSelectQuery.
Where(sq.Eq{"GroupMembers.DeleteAt": 0}).
Where(sq.Eq{"Users.DeleteAt": 0}).
Where(sq.Eq{"GroupMembers.GroupId": groupID})
userQuery = applyViewRestrictionsFilter(userQuery, viewRestrictions, true)
orderQuery := s.getQueryBuilder().
Select(getUsersColumns()...).
FromSelect(userQuery, "Users")
if teammateNameDisplay == model.ShowNicknameFullName {
orderQuery = orderQuery.OrderBy(`
CASE
WHEN Users.Nickname != '' THEN Users.Nickname
WHEN Users.FirstName != '' AND Users.LastName != '' THEN CONCAT(Users.FirstName, ' ', Users.LastName)
WHEN Users.FirstName != '' THEN Users.FirstName
WHEN Users.LastName != '' THEN Users.LastName
ELSE Users.Username
END`)
} else if teammateNameDisplay == model.ShowFullName {
orderQuery = orderQuery.OrderBy(`
CASE
WHEN Users.FirstName != '' AND Users.LastName != '' THEN CONCAT(Users.FirstName, ' ', Users.LastName)
WHEN Users.FirstName != '' THEN Users.FirstName
WHEN Users.LastName != '' THEN Users.LastName
ELSE Users.Username
END`)
} else {
orderQuery = orderQuery.OrderBy("Users.Username")
}
orderQuery = orderQuery.
Limit(uint64(perPage)).
Offset(uint64(page * perPage))
if err := s.GetReplica().SelectBuilder(&groupMembers, orderQuery); err != nil {
return nil, errors.Wrapf(err, "failed to find member Users for Group with id=%s", groupID)
}
return groupMembers, nil
}
func (s *SqlGroupStore) GetNonMemberUsersPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
groupMembers := []*model.User{}
builder := s.userGroupsSelectQuery.
Where(sq.Eq{"Id": groupID})
if err := s.GetReplica().GetBuilder(&model.Group{}, builder); err != nil {
return nil, errors.Wrap(err, "GetNonMemberUsersPage")
}
builder = s.getQueryBuilder().
Select(getUsersColumns()...).
From("Users").
LeftJoin("GroupMembers ON (GroupMembers.UserId = Users.Id AND GroupMembers.GroupId = ?)", groupID).
Where(sq.Eq{"Users.DeleteAt": 0}).
Where("(GroupMembers.UserID IS NULL OR GroupMembers.DeleteAt != 0)").
Limit(uint64(perPage)).
Offset(uint64(page * perPage)).
OrderBy("Users.Username ASC")
builder = applyViewRestrictionsFilter(builder, viewRestrictions, true)
if err := s.GetReplica().SelectBuilder(&groupMembers, builder); err != nil {
return nil, errors.Wrapf(err, "failed to find member Users for Group with id=%s", groupID)
}
return groupMembers, nil
}
func (s *SqlGroupStore) GetMemberCount(groupID string) (int64, error) {
return s.GetMemberCountWithRestrictions(groupID, nil)
}
func (s *SqlGroupStore) GetMemberCountWithRestrictions(groupID string, viewRestrictions *model.ViewUsersRestrictions) (int64, error) {
query := s.getQueryBuilder().
Select("COUNT(DISTINCT Users.Id)").
From("GroupMembers").
Join("Users ON Users.Id = GroupMembers.UserId").
Where(sq.Eq{"GroupMembers.GroupId": groupID}).
Where(sq.Eq{"Users.DeleteAt": 0}).
Where(sq.Eq{"GroupMembers.DeleteAt": 0})
query = applyViewRestrictionsFilter(query, viewRestrictions, false)
var count int64
if err := s.GetReplica().GetBuilder(&count, query); err != nil {
return int64(0), errors.Wrapf(err, "failed to count member Users for Group with id=%s", groupID)
}
return count, nil
}
func (s *SqlGroupStore) GetMemberUsersInTeam(groupID string, teamID string) ([]*model.User, error) {
groupMembers := []*model.User{}
query := s.groupMemberUsersSelectQuery.
Where(sq.Eq{
"GroupMembers.GroupId": groupID,
"GroupMembers.UserId": s.getQueryBuilder().
Select("TeamMembers.UserId").
From("TeamMembers").
Join("Teams ON Teams.Id = TeamMembers.TeamId").
Where(sq.Eq{
"TeamMembers.TeamId": teamID,
"TeamMembers.DeleteAt": 0,
}),
"GroupMembers.DeleteAt": 0,
"Users.DeleteAt": 0,
})
if err := s.GetReplica().SelectBuilder(&groupMembers, query); err != nil {
return nil, errors.Wrapf(err, "failed to member Users for groupId=%s and teamId=%s", groupID, teamID)
}
return groupMembers, nil
}
func (s *SqlGroupStore) GetMemberUsersNotInChannel(groupID string, channelID string) ([]*model.User, error) {
groupMembers := []*model.User{}
query := s.groupMemberUsersSelectQuery.
Where(sq.Eq{
"GroupMembers.GroupId": groupID,
}).
Where(sq.NotEq{
"GroupMembers.UserId": s.getQueryBuilder().
Select("ChannelMembers.UserId").
From("ChannelMembers").
Where(sq.Eq{
"ChannelMembers.ChannelId": channelID,
}),
}).
Where(sq.Eq{
"GroupMembers.UserId": s.getQueryBuilder().
Select("TeamMembers.UserId").
From("Channels").
Join("TeamMembers ON TeamMembers.TeamId = Channels.TeamId").
Where(sq.Eq{
"Channels.Id": channelID,
"TeamMembers.DeleteAt": 0,
}),
}).
Where(sq.Eq{
"GroupMembers.DeleteAt": 0,
"Users.DeleteAt": 0,
})
if err := s.GetReplica().SelectBuilder(&groupMembers, query); err != nil {
return nil, errors.Wrapf(err, "failed to member Users for groupId=%s and channelId!=%s", groupID, channelID)
}
return groupMembers, nil
}
func (s *SqlGroupStore) UpsertMember(groupID string, userID string) (*model.GroupMember, error) {
members, query, err := s.buildUpsertMembersQuery(groupID, []string{userID})
if err != nil {
return nil, err
}
if _, err = s.GetMaster().ExecBuilder(query); err != nil {
return nil, errors.Wrap(err, "failed to save GroupMember")
}
return members[0], nil
}
func (s *SqlGroupStore) DeleteMember(groupID string, userID string) (*model.GroupMember, error) {
members, query, err := s.buildDeleteMembersQuery(groupID, []string{userID})
if err != nil {
return nil, err
}
if _, err = s.GetMaster().ExecBuilder(query); err != nil {
return nil, errors.Wrapf(err, "failed to update GroupMember with groupId=%s and userId=%s", groupID, userID)
}
return members[0], nil
}
func (s *SqlGroupStore) PermanentDeleteMembersByUser(userId string) error {
builder := s.getQueryBuilder().
Delete("GroupMembers").
Where(sq.Eq{"UserId": userId})
if _, err := s.GetMaster().ExecBuilder(builder); err != nil {
return errors.Wrapf(err, "failed to permanent delete GroupMember with userId=%s", userId)
}
return nil
}
func (s *SqlGroupStore) CreateGroupSyncable(groupSyncable *model.GroupSyncable) (*model.GroupSyncable, error) {
if err := groupSyncable.IsValid(); err != nil {
return nil, err
}
// Reset values that shouldn't be updatable by parameter
groupSyncable.DeleteAt = 0
groupSyncable.CreateAt = model.GetMillis()
groupSyncable.UpdateAt = groupSyncable.CreateAt
var insertErr error
switch groupSyncable.Type {
case model.GroupSyncableTypeTeam:
if _, err := s.Team().Get(groupSyncable.SyncableId); err != nil {
return nil, err
}
_, insertErr = s.GetMaster().NamedExec(`INSERT INTO GroupTeams
(GroupId, AutoAdd, SchemeAdmin, CreateAt, DeleteAt, UpdateAt, TeamId)
VALUES
(:GroupId, :AutoAdd, :SchemeAdmin, :CreateAt, :DeleteAt, :UpdateAt, :TeamId)`, groupSyncableToGroupTeam(groupSyncable))
case model.GroupSyncableTypeChannel:
var channel *model.Channel
channel, err := s.Channel().Get(groupSyncable.SyncableId, false)
if err != nil {
return nil, err
}
_, insertErr = s.GetMaster().NamedExec(`INSERT INTO GroupChannels
(GroupId, AutoAdd, SchemeAdmin, CreateAt, DeleteAt, UpdateAt, ChannelId)
VALUES
(:GroupId, :AutoAdd, :SchemeAdmin, :CreateAt, :DeleteAt, :UpdateAt, :ChannelId)`, groupSyncableToGroupChannel(groupSyncable))
groupSyncable.TeamID = channel.TeamId
default:
return nil, fmt.Errorf("invalid GroupSyncableType: %s", groupSyncable.Type)
}
if insertErr != nil {
return nil, errors.Wrap(insertErr, "unable to insert GroupSyncable")
}
return groupSyncable, nil
}
func (s *SqlGroupStore) GetGroupSyncable(groupID string, syncableID string, syncableType model.GroupSyncableType) (*model.GroupSyncable, error) {
groupSyncable, err := s.getGroupSyncable(groupID, syncableID, syncableType)
if err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("GroupSyncable", fmt.Sprintf("groupId=%s, syncableId=%s, syncableType=%s", groupID, syncableID, syncableType))
}
return nil, errors.Wrapf(err, "failed to find GroupSyncable with groupId=%s, syncableId=%s, syncableType=%s", groupID, syncableID, syncableType)
}
return groupSyncable, nil
}
func (s *SqlGroupStore) getGroupSyncable(groupID string, syncableID string, syncableType model.GroupSyncableType) (*model.GroupSyncable, error) {
var err error
var result any
switch syncableType {
case model.GroupSyncableTypeTeam:
var team groupTeam
err = s.GetReplica().GetBuilder(&team, s.groupTeamsSelectQuery.Where(sq.Eq{
"GroupTeams.GroupId": groupID,
"GroupTeams.TeamId": syncableID,
}))
result = &team
case model.GroupSyncableTypeChannel:
var ch groupChannel
err = s.GetReplica().GetBuilder(&ch, s.groupChannelsSelectQuery.Where(sq.Eq{
"GroupChannels.GroupId": groupID,
"GroupChannels.ChannelId": syncableID,
}))
result = &ch
}
if err != nil {
return nil, err
}
if result == nil {
return nil, sql.ErrNoRows
}
groupSyncable := model.GroupSyncable{}
switch syncableType {
case model.GroupSyncableTypeTeam:
groupTeam := result.(*groupTeam)
groupSyncable.SyncableId = groupTeam.TeamId
groupSyncable.GroupId = groupTeam.GroupId
groupSyncable.AutoAdd = groupTeam.AutoAdd
groupSyncable.CreateAt = groupTeam.CreateAt
groupSyncable.DeleteAt = groupTeam.DeleteAt
groupSyncable.UpdateAt = groupTeam.UpdateAt
groupSyncable.Type = syncableType
case model.GroupSyncableTypeChannel:
groupChannel := result.(*groupChannel)
groupSyncable.SyncableId = groupChannel.ChannelId
groupSyncable.GroupId = groupChannel.GroupId
groupSyncable.AutoAdd = groupChannel.AutoAdd
groupSyncable.CreateAt = groupChannel.CreateAt
groupSyncable.DeleteAt = groupChannel.DeleteAt
groupSyncable.UpdateAt = groupChannel.UpdateAt
groupSyncable.Type = syncableType
default:
return nil, fmt.Errorf("unable to convert syncableType: %s", syncableType.String())
}
return &groupSyncable, nil
}
func (s *SqlGroupStore) GetAllGroupSyncablesByGroupId(groupID string, syncableType model.GroupSyncableType) ([]*model.GroupSyncable, error) {
groupSyncables := []*model.GroupSyncable{}
switch syncableType {
case model.GroupSyncableTypeTeam:
query := s.groupTeamsSelectQuery.
Columns("Teams.DisplayName AS TeamDisplayName", "Teams.Type AS TeamType").
Join("Teams ON Teams.Id = GroupTeams.TeamId").
Where(sq.Eq{
"GroupTeams.GroupId": groupID,
"GroupTeams.DeleteAt": 0,
})
results := []*groupTeamJoin{}
err := s.GetReplica().SelectBuilder(&results, query)
if err != nil {
return nil, errors.Wrapf(err, "failed to find GroupTeams with groupId=%s", groupID)
}
for _, result := range results {
groupSyncable := &model.GroupSyncable{
SyncableId: result.TeamId,
GroupId: result.GroupId,
AutoAdd: result.AutoAdd,
CreateAt: result.CreateAt,
DeleteAt: result.DeleteAt,
UpdateAt: result.UpdateAt,
Type: syncableType,
TeamDisplayName: result.TeamDisplayName,
TeamType: result.TeamType,
SchemeAdmin: result.SchemeAdmin,
}
groupSyncables = append(groupSyncables, groupSyncable)
}
case model.GroupSyncableTypeChannel:
query := s.groupChannelsSelectQuery.
Columns(
"Channels.DisplayName AS ChannelDisplayName",
"Teams.DisplayName AS TeamDisplayName",
"Channels.Type As ChannelType",
"Teams.Type As TeamType",
"Teams.Id AS TeamId",
).Join("Channels ON Channels.Id = GroupChannels.ChannelId").
Join("Teams ON Teams.Id = Channels.TeamId").
Where(sq.Eq{
"GroupChannels.GroupId": groupID,
"GroupChannels.DeleteAt": 0,
})
results := []*groupChannelJoin{}
err := s.GetReplica().SelectBuilder(&results, query)
if err != nil {
return nil, errors.Wrapf(err, "failed to find GroupChannels with groupId=%s", groupID)
}
for _, result := range results {
groupSyncable := &model.GroupSyncable{
SyncableId: result.ChannelId,
GroupId: result.GroupId,
AutoAdd: result.AutoAdd,
CreateAt: result.CreateAt,
DeleteAt: result.DeleteAt,
UpdateAt: result.UpdateAt,
Type: syncableType,
ChannelDisplayName: result.ChannelDisplayName,
ChannelType: result.ChannelType,
TeamDisplayName: result.TeamDisplayName,
TeamType: result.TeamType,
TeamID: result.TeamID,
SchemeAdmin: result.SchemeAdmin,
}
groupSyncables = append(groupSyncables, groupSyncable)
}
}
return groupSyncables, nil
}
func (s *SqlGroupStore) UpdateGroupSyncable(groupSyncable *model.GroupSyncable) (*model.GroupSyncable, error) {
retrievedGroupSyncable, err := s.getGroupSyncable(groupSyncable.GroupId, groupSyncable.SyncableId, groupSyncable.Type)
if err != nil {
if err == sql.ErrNoRows {
return nil, errors.Wrap(store.NewErrNotFound("GroupSyncable", fmt.Sprintf("groupId=%s, syncableId=%s, syncableType=%s", groupSyncable.GroupId, groupSyncable.SyncableId, groupSyncable.Type)), "GroupSyncable not found")
}
return nil, errors.Wrapf(err, "failed to find GroupSyncable with groupId=%s, syncableId=%s, syncableType=%s", groupSyncable.GroupId, groupSyncable.SyncableId, groupSyncable.Type)
}
if err := groupSyncable.IsValid(); err != nil {
return nil, err
}
// If updating DeleteAt it can only be to 0
if groupSyncable.DeleteAt != retrievedGroupSyncable.DeleteAt && groupSyncable.DeleteAt != 0 {
return nil, errors.New("DeleteAt should be 0 when updating")
}
// Reset these properties, don't update them based on input
groupSyncable.CreateAt = retrievedGroupSyncable.CreateAt
groupSyncable.UpdateAt = model.GetMillis()
switch groupSyncable.Type {
case model.GroupSyncableTypeTeam:
_, err = s.GetMaster().NamedExec(`UPDATE GroupTeams
SET AutoAdd=:AutoAdd, SchemeAdmin=:SchemeAdmin, CreateAt=:CreateAt,
DeleteAt=:DeleteAt, UpdateAt=:UpdateAt
WHERE GroupId=:GroupId AND TeamId=:TeamId`, groupSyncableToGroupTeam(groupSyncable))
case model.GroupSyncableTypeChannel:
// We need to get the TeamId so redux can manage channels when teams are unlinked
var channel *model.Channel
channel, channelErr := s.Channel().Get(groupSyncable.SyncableId, false)
if channelErr != nil {
return nil, channelErr
}
_, err = s.GetMaster().NamedExec(`UPDATE GroupChannels
SET AutoAdd=:AutoAdd, SchemeAdmin=:SchemeAdmin, CreateAt=:CreateAt,
DeleteAt=:DeleteAt, UpdateAt=:UpdateAt
WHERE GroupId=:GroupId AND ChannelId=:ChannelId`, groupSyncableToGroupChannel(groupSyncable))
groupSyncable.TeamID = channel.TeamId
default:
return nil, fmt.Errorf("invalid GroupSyncableType: %s", groupSyncable.Type)
}
if err != nil {
return nil, errors.Wrap(err, "failed to update GroupSyncable")
}
return groupSyncable, nil
}
func (s *SqlGroupStore) DeleteGroupSyncable(groupID string, syncableID string, syncableType model.GroupSyncableType) (*model.GroupSyncable, error) {
groupSyncable, err := s.getGroupSyncable(groupID, syncableID, syncableType)
if err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("GroupSyncable", fmt.Sprintf("groupId=%s, syncableId=%s, syncableType=%s", groupID, syncableID, syncableType))
}
return nil, errors.Wrapf(err, "failed to find GroupSyncable with groupId=%s, syncableId=%s, syncableType=%s", groupID, syncableID, syncableType)
}
if groupSyncable.DeleteAt != 0 {
return nil, store.NewErrInvalidInput("GroupSyncable", "<groupId, syncableId, syncableType>", fmt.Sprintf("<%s, %s, %s>", groupSyncable.GroupId, groupSyncable.SyncableId, groupSyncable.Type))
}
time := model.GetMillis()
groupSyncable.DeleteAt = time
groupSyncable.UpdateAt = time
switch groupSyncable.Type {
case model.GroupSyncableTypeTeam:
_, err = s.GetMaster().NamedExec(`UPDATE GroupTeams
SET AutoAdd=:AutoAdd, SchemeAdmin=:SchemeAdmin, CreateAt=:CreateAt,
DeleteAt=:DeleteAt, UpdateAt=:UpdateAt
WHERE GroupId=:GroupId AND TeamId=:TeamId`, groupSyncableToGroupTeam(groupSyncable))
case model.GroupSyncableTypeChannel:
_, err = s.GetMaster().NamedExec(`UPDATE GroupChannels
SET AutoAdd=:AutoAdd, SchemeAdmin=:SchemeAdmin, CreateAt=:CreateAt,
DeleteAt=:DeleteAt, UpdateAt=:UpdateAt
WHERE GroupId=:GroupId AND ChannelId=:ChannelId`, groupSyncableToGroupChannel(groupSyncable))
default:
return nil, fmt.Errorf("invalid GroupSyncableType: %s", groupSyncable.Type)
}
if err != nil {
return nil, errors.Wrap(err, "failed to update GroupSyncable")
}
return groupSyncable, nil
}
func (s *SqlGroupStore) TeamMembersToAdd(since int64, teamID *string, reAddRemovedMembers bool) ([]*model.UserTeamIDPair, error) {
builder := s.getQueryBuilder().Select("GroupMembers.UserId UserID", "GroupTeams.TeamId TeamID").
From("GroupMembers").
Join("GroupTeams ON GroupTeams.GroupId = GroupMembers.GroupId").
Join("UserGroups ON UserGroups.Id = GroupMembers.GroupId").
Join("Teams ON Teams.Id = GroupTeams.TeamId").
Where(sq.Eq{
"UserGroups.DeleteAt": 0,
"GroupTeams.DeleteAt": 0,
"GroupTeams.AutoAdd": true,
"GroupMembers.DeleteAt": 0,
"Teams.DeleteAt": 0,
})
if !reAddRemovedMembers {
builder = builder.
JoinClause("LEFT OUTER JOIN TeamMembers ON TeamMembers.TeamId = GroupTeams.TeamId AND TeamMembers.UserId = GroupMembers.UserId").
Where(sq.Eq{"TeamMembers.UserId": nil}).
Where(sq.Or{
sq.GtOrEq{"GroupMembers.CreateAt": since},
sq.GtOrEq{"GroupTeams.UpdateAt": since},
})
}
if teamID != nil {
builder = builder.Where(sq.Eq{"Teams.Id": *teamID})
}
teamMembers := []*model.UserTeamIDPair{}
if err := s.GetMaster().SelectBuilder(&teamMembers, builder); err != nil {
return nil, errors.Wrap(err, "failed to find UserTeamIDPairs")
}
return teamMembers, nil
}
func (s *SqlGroupStore) ChannelMembersToAdd(since int64, channelID *string, reAddRemovedMembers bool) ([]*model.UserChannelIDPair, error) {
builder := s.getQueryBuilder().Select("GroupMembers.UserId UserID", "GroupChannels.ChannelId ChannelID").
From("GroupMembers").
Join("GroupChannels ON GroupChannels.GroupId = GroupMembers.GroupId").
Join("UserGroups ON UserGroups.Id = GroupMembers.GroupId").
Join("Channels ON Channels.Id = GroupChannels.ChannelId").
Where(sq.Eq{
"UserGroups.DeleteAt": 0,
"GroupChannels.DeleteAt": 0,
"GroupChannels.AutoAdd": true,
"GroupMembers.DeleteAt": 0,
"Channels.DeleteAt": 0,
})
if !reAddRemovedMembers {
builder = builder.
JoinClause("LEFT OUTER JOIN ChannelMemberHistory ON ChannelMemberHistory.ChannelId = GroupChannels.ChannelId AND ChannelMemberHistory.UserId = GroupMembers.UserId").
Where(sq.Eq{
"ChannelMemberHistory.UserId": nil,
"ChannelMemberHistory.LeaveTime": nil,
}).
Where(sq.Or{
sq.GtOrEq{"GroupMembers.CreateAt": since},
sq.GtOrEq{"GroupChannels.UpdateAt": since},
})
}
if channelID != nil {
builder = builder.Where(sq.Eq{"Channels.Id": *channelID})
}
channelMembers := []*model.UserChannelIDPair{}
if err := s.GetMaster().SelectBuilder(&channelMembers, builder); err != nil {
return nil, errors.Wrap(err, "failed to find UserChannelIDPairs")
}
return channelMembers, nil
}
func groupSyncableToGroupTeam(groupSyncable *model.GroupSyncable) *groupTeam {
return &groupTeam{
GroupSyncable: *groupSyncable,
TeamId: groupSyncable.SyncableId,
}
}
func groupSyncableToGroupChannel(groupSyncable *model.GroupSyncable) *groupChannel {
return &groupChannel{
GroupSyncable: *groupSyncable,
ChannelId: groupSyncable.SyncableId,
}
}
func (s *SqlGroupStore) TeamMembersToRemove(teamID *string) ([]*model.TeamMember, error) {
whereStmt := `
(TeamMembers.TeamId,
TeamMembers.UserId)
NOT IN (
SELECT
Teams.Id AS TeamId,
GroupMembers.UserId
FROM
Teams
JOIN GroupTeams ON GroupTeams.TeamId = Teams.Id
JOIN UserGroups ON UserGroups.Id = GroupTeams.GroupId
JOIN GroupMembers ON GroupMembers.GroupId = UserGroups.Id
WHERE
Teams.GroupConstrained = TRUE
AND GroupTeams.DeleteAt = 0
AND UserGroups.DeleteAt = 0
AND Teams.DeleteAt = 0
AND GroupMembers.DeleteAt = 0
GROUP BY
Teams.Id,
GroupMembers.UserId)`
builder := s.getQueryBuilder().Select(
"TeamMembers.TeamId",
"TeamMembers.UserId",
"TeamMembers.Roles",
"TeamMembers.DeleteAt",
"TeamMembers.SchemeUser",
"TeamMembers.SchemeAdmin",
"(TeamMembers.SchemeGuest IS NOT NULL AND TeamMembers.SchemeGuest) AS SchemeGuest",
).
From("TeamMembers").
Join("Teams ON Teams.Id = TeamMembers.TeamId").
LeftJoin("Bots ON Bots.UserId = TeamMembers.UserId").
Where(sq.Eq{"TeamMembers.DeleteAt": 0, "Teams.DeleteAt": 0, "Teams.GroupConstrained": true, "Bots.UserId": nil}).
Where(whereStmt)
if teamID != nil {
builder = builder.Where(sq.Eq{"TeamMembers.TeamId": *teamID})
}
teamMembers := []*model.TeamMember{}
if err := s.GetReplica().SelectBuilder(&teamMembers, builder); err != nil {
return nil, errors.Wrap(err, "failed to find TeamMembers")
}
return teamMembers, nil
}
func (s *SqlGroupStore) CountGroupsByChannel(channelId string, opts model.GroupSearchOpts) (int64, error) {
builder := s.groupsBySyncableBaseQuery(model.GroupSyncableTypeChannel, selectCountGroups, channelId, opts)
var count int64
if err := s.GetReplica().GetBuilder(&count, builder); err != nil {
return int64(0), errors.Wrapf(err, "failed to count Groups by channel with channelId=%s", channelId)
}
return count, nil
}
type group struct {
Id string
Name *string
DisplayName string
Description string
Source model.GroupSource
RemoteId *string
CreateAt int64
UpdateAt int64
DeleteAt int64
HasSyncables bool
MemberCount *int
AllowReference bool
ChannelMemberCount *int
ChannelMemberTimezonesCount *int
}
func (g group) ToModel() *model.Group {
return &model.Group{
Id: g.Id,
Name: g.Name,
DisplayName: g.DisplayName,
Description: g.Description,
Source: g.Source,
RemoteId: g.RemoteId,
CreateAt: g.CreateAt,
UpdateAt: g.UpdateAt,
DeleteAt: g.DeleteAt,
HasSyncables: g.HasSyncables,
AllowReference: g.AllowReference,
MemberCount: g.MemberCount,
ChannelMemberCount: g.ChannelMemberCount,
ChannelMemberTimezonesCount: g.ChannelMemberTimezonesCount,
}
}
type groups []*group
func (groups groups) ToModel() []*model.Group {
res := make([]*model.Group, 0, len(groups))
for _, g := range groups {
res = append(res, g.ToModel())
}
return res
}
type groupWithSchemeAdmin struct {
group
SyncableSchemeAdmin *bool
}
func (g groupWithSchemeAdmin) ToModel() *model.GroupWithSchemeAdmin {
if g.SyncableSchemeAdmin == nil {
g.SyncableSchemeAdmin = model.NewPointer(false)
}
res := &model.GroupWithSchemeAdmin{
Group: *g.group.ToModel(),
SchemeAdmin: g.SyncableSchemeAdmin,
}
return res
}
type groupsWithSchemeAdmin []*groupWithSchemeAdmin
func (groups groupsWithSchemeAdmin) ToModel() []*model.GroupWithSchemeAdmin {
res := make([]*model.GroupWithSchemeAdmin, 0, len(groups))
for _, g := range groups {
res = append(res, g.ToModel())
}
return res
}
type groupAssociatedToChannelWithSchemeAdmin struct {
groupWithSchemeAdmin
ChannelId string
}
func (g groupAssociatedToChannelWithSchemeAdmin) ToModel() *model.GroupsAssociatedToChannelWithSchemeAdmin {
withSchemeAdmin := g.groupWithSchemeAdmin.ToModel()
return &model.GroupsAssociatedToChannelWithSchemeAdmin{
ChannelId: g.ChannelId,
SchemeAdmin: withSchemeAdmin.SchemeAdmin,
Group: withSchemeAdmin.Group,
}
}
type groupsAssociatedToChannelWithSchemeAdmin []groupAssociatedToChannelWithSchemeAdmin
func (groups groupsAssociatedToChannelWithSchemeAdmin) ToModel() []*model.GroupsAssociatedToChannelWithSchemeAdmin {
res := make([]*model.GroupsAssociatedToChannelWithSchemeAdmin, 0, len(groups))
for _, g := range groups {
res = append(res, g.ToModel())
}
return res
}
func (s *SqlGroupStore) GetGroupsByChannel(channelId string, opts model.GroupSearchOpts) ([]*model.GroupWithSchemeAdmin, error) {
builder := s.groupsBySyncableBaseQuery(model.GroupSyncableTypeChannel, selectGroups, channelId, opts)
if opts.PageOpts != nil {
offset := uint64(opts.PageOpts.Page * opts.PageOpts.PerPage)
builder = builder.OrderBy("UserGroups.DisplayName").Limit(uint64(opts.PageOpts.PerPage)).Offset(offset)
}
groups := groupsWithSchemeAdmin{}
if err := s.GetReplica().SelectBuilder(&groups, builder); err != nil {
return nil, errors.Wrapf(err, "failed to find Groups with channelId=%s", channelId)
}
return groups.ToModel(), nil
}
func (s *SqlGroupStore) ChannelMembersToRemove(channelID *string) ([]*model.ChannelMember, error) {
whereStmt := `
(ChannelMembers.ChannelId,
ChannelMembers.UserId)
NOT IN (
SELECT
Channels.Id AS ChannelId,
GroupMembers.UserId
FROM
Channels
JOIN GroupChannels ON GroupChannels.ChannelId = Channels.Id
JOIN UserGroups ON UserGroups.Id = GroupChannels.GroupId
JOIN GroupMembers ON GroupMembers.GroupId = UserGroups.Id
WHERE
Channels.GroupConstrained = TRUE
AND GroupChannels.DeleteAt = 0
AND UserGroups.DeleteAt = 0
AND Channels.DeleteAt = 0
AND GroupMembers.DeleteAt = 0
GROUP BY
Channels.Id,
GroupMembers.UserId)`
builder := s.getQueryBuilder().Select(
"ChannelMembers.ChannelId",
"ChannelMembers.UserId",
"ChannelMembers.LastViewedAt",
"ChannelMembers.MsgCount",
"ChannelMembers.MsgCountRoot",
"ChannelMembers.MentionCount",
"ChannelMembers.MentionCountRoot",
"ChannelMembers.NotifyProps",
"ChannelMembers.LastUpdateAt",
"ChannelMembers.LastUpdateAt",
"ChannelMembers.SchemeUser",
"ChannelMembers.SchemeAdmin",
"(ChannelMembers.SchemeGuest IS NOT NULL AND ChannelMembers.SchemeGuest) AS SchemeGuest",
).
From("ChannelMembers").
Join("Channels ON Channels.Id = ChannelMembers.ChannelId").
LeftJoin("Bots ON Bots.UserId = ChannelMembers.UserId").
Where(sq.Eq{"Channels.DeleteAt": 0, "Channels.GroupConstrained": true, "Bots.UserId": nil}).
Where(whereStmt)
if channelID != nil {
builder = builder.Where(sq.Eq{"ChannelMembers.ChannelId": *channelID})
}
channelMembers := []*model.ChannelMember{}
if err := s.GetReplica().SelectBuilder(&channelMembers, builder); err != nil {
return nil, errors.Wrap(err, "failed to find ChannelMembers")
}
return channelMembers, nil
}
func (s *SqlGroupStore) groupsBySyncableBaseQuery(st model.GroupSyncableType, t selectType, syncableID string, opts model.GroupSearchOpts) sq.SelectBuilder {
var query sq.SelectBuilder
switch t {
case selectGroups:
query = s.userGroupsSelectQuery.
Column("gs.SchemeAdmin AS SyncableSchemeAdmin")
case selectCountGroups:
query = s.getQueryBuilder().
Select("COUNT(*)").
From("UserGroups")
}
if st == model.GroupSyncableTypeTeam {
query = query.
Join("GroupTeams gs ON gs.GroupId = UserGroups.Id").
Where(sq.Eq{
"gs.TeamId": syncableID,
"gs.DeleteAt": 0,
})
} else {
query = query.
Join("GroupChannels gs ON gs.GroupId = UserGroups.Id").
Where(sq.Eq{
"gs.ChannelId": syncableID,
"gs.DeleteAt": 0,
})
}
query = query.
Where(sq.Eq{
"UserGroups.DeleteAt": 0,
})
if opts.IncludeMemberCount && t == selectGroups {
query = query.
Column("coalesce(Members.MemberCount, 0) AS MemberCount").
LeftJoin("(SELECT GroupMembers.GroupId, COUNT(*) AS MemberCount FROM GroupMembers LEFT JOIN Users ON Users.Id = GroupMembers.UserId WHERE GroupMembers.DeleteAt = 0 AND Users.DeleteAt = 0 GROUP BY GroupId) AS Members ON Members.GroupId = UserGroups.Id").
OrderBy("UserGroups.DisplayName")
}
if opts.FilterAllowReference && t == selectGroups {
query = query.Where("UserGroups.AllowReference = true")
}
if opts.Q != "" {
pattern := fmt.Sprintf("%%%s%%", sanitizeSearchTerm(opts.Q, "\\"))
operatorKeyword := "ILIKE"
query = query.Where(fmt.Sprintf("(UserGroups.Name %[1]s ? OR UserGroups.DisplayName %[1]s ?)", operatorKeyword), pattern, pattern)
}
return query
}
func (s *SqlGroupStore) getGroupsAssociatedToChannelsByTeam(teamID string, opts model.GroupSearchOpts) sq.SelectBuilder {
query := s.userGroupsSelectQuery.
Columns("gc.ChannelId", "gc.SchemeAdmin AS SyncableSchemeAdmin").
LeftJoin(`
(SELECT
GroupChannels.GroupId, GroupChannels.ChannelId, GroupChannels.DeleteAt, GroupChannels.SchemeAdmin
FROM
GroupChannels
LEFT JOIN
Channels ON (Channels.Id = GroupChannels.ChannelId)
WHERE
GroupChannels.DeleteAt = 0
AND Channels.DeleteAt = 0
AND Channels.TeamId = ?) AS gc ON gc.GroupId = UserGroups.Id`, teamID).
Where("UserGroups.DeleteAt = 0 AND gc.DeleteAt = 0").
OrderBy("UserGroups.DisplayName")
if opts.IncludeMemberCount {
query = s.userGroupsSelectQuery.
Columns("gc.ChannelId", "coalesce(Members.MemberCount, 0) AS MemberCount", "gc.SchemeAdmin AS SyncableSchemeAdmin").
LeftJoin(`
(SELECT
GroupChannels.ChannelId, GroupChannels.DeleteAt, GroupChannels.GroupId, GroupChannels.SchemeAdmin
FROM
GroupChannels
LEFT JOIN
Channels ON (Channels.Id = GroupChannels.ChannelId)
WHERE
GroupChannels.DeleteAt = 0
AND Channels.DeleteAt = 0
AND Channels.TeamId = ?) AS gc ON gc.GroupId = UserGroups.Id`, teamID).
LeftJoin(`(
SELECT
GroupMembers.GroupId, COUNT(*) AS MemberCount
FROM
GroupMembers
LEFT JOIN
Users ON Users.Id = GroupMembers.UserId
WHERE
GroupMembers.DeleteAt = 0
AND Users.DeleteAt = 0
GROUP BY GroupId) AS Members
ON Members.GroupId = UserGroups.Id`).
Where("UserGroups.DeleteAt = 0 AND gc.DeleteAt = 0").
OrderBy("UserGroups.DisplayName")
}
if opts.FilterAllowReference {
query = query.Where("UserGroups.AllowReference = true")
}
if opts.Q != "" {
pattern := fmt.Sprintf("%%%s%%", sanitizeSearchTerm(opts.Q, "\\"))
operatorKeyword := "ILIKE"
query = query.Where(fmt.Sprintf("(UserGroups.Name %[1]s ? OR UserGroups.DisplayName %[1]s ?)", operatorKeyword), pattern, pattern)
}
return query
}
func (s *SqlGroupStore) CountGroupsByTeam(teamId string, opts model.GroupSearchOpts) (int64, error) {
builder := s.groupsBySyncableBaseQuery(model.GroupSyncableTypeTeam, selectCountGroups, teamId, opts)
var count int64
if err := s.GetReplica().GetBuilder(&count, builder); err != nil {
return int64(0), errors.Wrapf(err, "failed to count Groups with teamId=%s", teamId)
}
return count, nil
}
func (s *SqlGroupStore) GetGroupsByTeam(teamId string, opts model.GroupSearchOpts) ([]*model.GroupWithSchemeAdmin, error) {
builder := s.groupsBySyncableBaseQuery(model.GroupSyncableTypeTeam, selectGroups, teamId, opts)
if opts.PageOpts != nil {
offset := uint64(opts.PageOpts.Page * opts.PageOpts.PerPage)
builder = builder.OrderBy("UserGroups.DisplayName").Limit(uint64(opts.PageOpts.PerPage)).Offset(offset)
}
groups := groupsWithSchemeAdmin{}
if err := s.GetReplica().SelectBuilder(&groups, builder); err != nil {
return nil, errors.Wrapf(err, "failed to find Groups with teamId=%s", teamId)
}
return groups.ToModel(), nil
}
func (s *SqlGroupStore) GetGroupsAssociatedToChannelsByTeam(teamId string, opts model.GroupSearchOpts) (map[string][]*model.GroupWithSchemeAdmin, error) {
builder := s.getGroupsAssociatedToChannelsByTeam(teamId, opts)
if opts.PageOpts != nil {
offset := uint64(opts.PageOpts.Page * opts.PageOpts.PerPage)
builder = builder.OrderBy("UserGroups.DisplayName").Limit(uint64(opts.PageOpts.PerPage)).Offset(offset)
}
tgroups := groupsAssociatedToChannelWithSchemeAdmin{}
if err := s.GetReplica().SelectBuilder(&tgroups, builder); err != nil {
return nil, errors.Wrapf(err, "failed to find Groups with teamId=%s", teamId)
}
groups := map[string][]*model.GroupWithSchemeAdmin{}
for _, tgroup := range tgroups {
group := tgroup.groupWithSchemeAdmin.ToModel()
groups[tgroup.ChannelId] = append(groups[tgroup.ChannelId], group)
}
return groups, nil
}
func (s *SqlGroupStore) GetGroups(page, perPage int, opts model.GroupSearchOpts, viewRestrictions *model.ViewUsersRestrictions) ([]*model.Group, error) {
groupsVar := groups{}
groupsQuery := s.userGroupsSelectQuery
if opts.IncludeMemberCount {
groupsQuery = groupsQuery.Column("coalesce(Members.MemberCount, 0) AS MemberCount")
}
if opts.IncludeChannelMemberCount != "" {
groupsQuery = groupsQuery.Column("coalesce(ChannelMembers.ChannelMemberCount, 0) AS ChannelMemberCount")
if opts.IncludeTimezones {
groupsQuery = groupsQuery.Column("coalesce(ChannelMembers.ChannelMemberTimezonesCount, 0) AS ChannelMemberTimezonesCount")
}
}
if opts.IncludeMemberCount {
countQuery := s.getQueryBuilder().
Select("GroupMembers.GroupId, COUNT(DISTINCT Users.Id) AS MemberCount").
From("GroupMembers").
LeftJoin("Users ON Users.Id = GroupMembers.UserId").
Where(sq.Eq{"GroupMembers.DeleteAt": 0}).
Where(sq.Eq{"Users.DeleteAt": 0}).
GroupBy("GroupId")
countQuery = applyViewRestrictionsFilter(countQuery, viewRestrictions, false)
countString, params, err := countQuery.PlaceholderFormat(sq.Question).ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_groups_tosql")
}
groupsQuery = groupsQuery.
LeftJoin("("+countString+") AS Members ON Members.GroupId = UserGroups.Id", params...)
}
if opts.IncludeChannelMemberCount != "" {
selectStr := "GroupMembers.GroupId, COUNT(ChannelMembers.UserId) AS ChannelMemberCount"
joinStr := ""
if opts.IncludeTimezones {
selectStr += `,
COUNT(DISTINCT
(
CASE WHEN Timezone->>'useAutomaticTimezone' = 'true' AND length(Timezone->>'automaticTimezone') > 0
THEN Timezone->>'automaticTimezone'
WHEN Timezone->>'useAutomaticTimezone' = 'false' AND length(Timezone->>'manualTimezone') > 0
THEN Timezone->>'manualTimezone'
END
)) AS ChannelMemberTimezonesCount`
joinStr = "LEFT JOIN Users ON Users.Id = GroupMembers.UserId"
}
groupsQuery = groupsQuery.
LeftJoin("(SELECT "+selectStr+" FROM ChannelMembers LEFT JOIN GroupMembers ON GroupMembers.UserId = ChannelMembers.UserId AND GroupMembers.DeleteAt = 0 "+joinStr+" WHERE ChannelMembers.ChannelId = ? GROUP BY GroupId) AS ChannelMembers ON ChannelMembers.GroupId = UserGroups.Id", opts.IncludeChannelMemberCount)
}
if opts.FilterHasMember != "" {
groupsQuery = groupsQuery.
LeftJoin("GroupMembers ON GroupMembers.GroupId = UserGroups.Id").
Where("GroupMembers.UserId = ?", opts.FilterHasMember).
Where("GroupMembers.DeleteAt = 0")
}
if opts.Since > 0 {
groupsQuery = groupsQuery.Where(sq.Gt{
"UserGroups.UpdateAt": opts.Since,
})
}
if opts.FilterArchived {
groupsQuery = groupsQuery.Where("UserGroups.DeleteAt > 0")
} else if !opts.IncludeArchived && opts.Since <= 0 {
// Mobile needs to return archived groups when the since parameter is set, will need to keep this for backwards compatibility
groupsQuery = groupsQuery.Where("UserGroups.DeleteAt = 0")
}
if opts.IncludeArchived {
groupsQuery = groupsQuery.OrderBy("CASE WHEN UserGroups.DeleteAt = 0 THEN UserGroups.DisplayName end, CASE WHEN UserGroups.DeleteAt != 0 THEN UserGroups.DisplayName END")
} else {
groupsQuery = groupsQuery.OrderBy("UserGroups.DisplayName")
}
if perPage != 0 {
groupsQuery = groupsQuery.
Limit(uint64(perPage)).
Offset(uint64(page * perPage))
}
if opts.FilterAllowReference {
groupsQuery = groupsQuery.Where("UserGroups.AllowReference = true")
}
if opts.Q != "" {
pattern := fmt.Sprintf("%%%s%%", sanitizeSearchTerm(opts.Q, "\\"))
operatorKeyword := "ILIKE"
groupsQuery = groupsQuery.Where(fmt.Sprintf("(UserGroups.Name %[1]s ? OR UserGroups.DisplayName %[1]s ?)", operatorKeyword), pattern, pattern)
}
if len(opts.NotAssociatedToTeam) == 26 {
groupsQuery = groupsQuery.Where(`
UserGroups.Id NOT IN (
SELECT
Id
FROM
UserGroups
JOIN GroupTeams ON GroupTeams.GroupId = UserGroups.Id
WHERE
GroupTeams.DeleteAt = 0
AND UserGroups.DeleteAt = 0
AND GroupTeams.TeamId = ?
)
`, opts.NotAssociatedToTeam)
}
if len(opts.NotAssociatedToChannel) == 26 {
groupsQuery = groupsQuery.Where(`
UserGroups.Id NOT IN (
SELECT
Id
FROM
UserGroups
JOIN GroupChannels ON GroupChannels.GroupId = UserGroups.Id
WHERE
GroupChannels.DeleteAt = 0
AND UserGroups.DeleteAt = 0
AND GroupChannels.ChannelId = ?
)
`, opts.NotAssociatedToChannel)
}
if opts.FilterParentTeamPermitted && len(opts.NotAssociatedToChannel) == 26 {
groupsQuery = groupsQuery.Where(`
CASE
WHEN (
SELECT
Teams.GroupConstrained
FROM
Teams
JOIN Channels ON Channels.TeamId = Teams.Id
WHERE
Channels.Id = ?
) THEN UserGroups.Id IN (
SELECT
GroupId
FROM
GroupTeams
WHERE
GroupTeams.DeleteAt = 0
AND GroupTeams.TeamId = (
SELECT
TeamId
FROM
Channels
WHERE
Id = ?
)
)
ELSE TRUE
END
`, opts.NotAssociatedToChannel, opts.NotAssociatedToChannel)
}
if opts.Source != "" {
groupsQuery = groupsQuery.Where("UserGroups.Source = ?", opts.Source)
} else if opts.OnlySyncableSources {
sources := model.GetSyncableGroupSources()
sourcePrefixes := model.GetSyncableGroupSourcePrefixes()
orClauses := sq.Or{}
if len(sources) > 0 {
orClauses = append(orClauses, sq.Eq{"UserGroups.Source": sources})
}
for _, prefix := range sourcePrefixes {
orClauses = append(orClauses, sq.Like{"UserGroups.Source": string(prefix) + "%"})
}
groupsQuery = groupsQuery.Where(orClauses)
}
if err := s.GetReplica().SelectBuilder(&groupsVar, groupsQuery); err != nil {
return nil, errors.Wrap(err, "failed to find Groups")
}
return groupsVar.ToModel(), nil
}
func (s *SqlGroupStore) teamMembersMinusGroupMembersQuery(teamID string, groupIDs []string, isCount bool) sq.SelectBuilder {
var builder sq.SelectBuilder
if isCount {
builder = s.getQueryBuilder().Select("count(DISTINCT Users.Id)")
} else {
builder = s.getQueryBuilder().Select().
Columns(getUsersColumns()...).
Column("coalesce(TeamMembers.SchemeGuest, false) SchemeGuest").
Column("TeamMembers.SchemeAdmin").
Column("TeamMembers.SchemeUser")
builder = builder.Column("string_agg(UserGroups.Id, ',') AS GroupIDs")
}
subQuery := s.getQueryBuilder().Select("GroupMembers.UserId").
From("GroupMembers").
Join("UserGroups ON UserGroups.Id = GroupMembers.GroupId").
Where("GroupMembers.DeleteAt = 0").
Where(fmt.Sprintf("GroupMembers.GroupId IN ('%s')", strings.Join(groupIDs, "', '")))
query, _ := subQuery.MustSql()
builder = builder.
From("TeamMembers").
Join("Teams ON Teams.Id = TeamMembers.TeamId").
Join("Users ON Users.Id = TeamMembers.UserId").
LeftJoin("Bots ON Bots.UserId = TeamMembers.UserId").
LeftJoin("GroupMembers ON GroupMembers.UserId = Users.Id").
LeftJoin("UserGroups ON UserGroups.Id = GroupMembers.GroupId").
Where("TeamMembers.DeleteAt = 0").
Where("Teams.DeleteAt = 0").
Where("Users.DeleteAt = 0").
Where("Bots.UserId IS NULL").
Where("Teams.Id = ?", teamID).
Where(fmt.Sprintf("Users.Id NOT IN (%s)", query))
if !isCount {
builder = builder.GroupBy("Users.Id, TeamMembers.SchemeGuest, TeamMembers.SchemeAdmin, TeamMembers.SchemeUser")
}
return builder
}
// TeamMembersMinusGroupMembers returns the set of users on the given team minus the set of users in the given
// groups.
func (s *SqlGroupStore) TeamMembersMinusGroupMembers(teamID string, groupIDs []string, page, perPage int) ([]*model.UserWithGroups, error) {
builder := s.teamMembersMinusGroupMembersQuery(teamID, groupIDs, false)
builder = builder.OrderBy("Users.Username ASC").Limit(uint64(perPage)).Offset(uint64(page * perPage))
users := []*model.UserWithGroups{}
if err := s.GetReplica().SelectBuilder(&users, builder); err != nil {
return nil, errors.Wrap(err, "failed to find UserWithGroups")
}
return users, nil
}
// CountTeamMembersMinusGroupMembers returns the count of the set of users on the given team minus the set of users
// in the given groups.
func (s *SqlGroupStore) CountTeamMembersMinusGroupMembers(teamID string, groupIDs []string) (int64, error) {
queryString, args, err := s.teamMembersMinusGroupMembersQuery(teamID, groupIDs, true).ToSql()
if err != nil {
return 0, errors.Wrap(err, "count_team_members_minus_group_members_tosql")
}
var count int64
if err := s.GetReplica().Get(&count, queryString, args...); err != nil {
return 0, errors.Wrap(err, "failed to count TeamMembers minus GroupMembers")
}
return count, nil
}
func (s *SqlGroupStore) channelMembersMinusGroupMembersQuery(channelID string, groupIDs []string, isCount bool) sq.SelectBuilder {
builder := s.getQueryBuilder().Select()
if isCount {
builder = builder.Column("count(DISTINCT Users.Id)")
} else {
builder = builder.Columns(getUsersColumns()...)
builder = builder.Columns(
"COALESCE(ChannelMembers.SchemeGuest, FALSE) SchemeGuest",
"ChannelMembers.SchemeAdmin",
"ChannelMembers.SchemeUser",
)
builder = builder.Column("string_agg(UserGroups.Id, ',') AS GroupIDs")
}
subQuery := s.getQueryBuilder().Select("GroupMembers.UserId").
From("GroupMembers").
Join("UserGroups ON UserGroups.Id = GroupMembers.GroupId").
Where("GroupMembers.DeleteAt = 0").
Where(fmt.Sprintf("GroupMembers.GroupId IN ('%s')", strings.Join(groupIDs, "', '")))
query, _ := subQuery.MustSql()
builder = builder.
From("ChannelMembers").
Join("Channels ON Channels.Id = ChannelMembers.ChannelId").
Join("Users ON Users.Id = ChannelMembers.UserId").
LeftJoin("Bots ON Bots.UserId = ChannelMembers.UserId").
LeftJoin("GroupMembers ON GroupMembers.UserId = Users.Id").
LeftJoin("UserGroups ON UserGroups.Id = GroupMembers.GroupId").
Where("Channels.DeleteAt = 0").
Where("Users.DeleteAt = 0").
Where("Bots.UserId IS NULL").
Where("Channels.Id = ?", channelID).
Where(fmt.Sprintf("Users.Id NOT IN (%s)", query))
if !isCount {
builder = builder.GroupBy("Users.Id, ChannelMembers.SchemeGuest, ChannelMembers.SchemeAdmin, ChannelMembers.SchemeUser")
}
return builder
}
// ChannelMembersMinusGroupMembers returns the set of users in the given channel minus the set of users in the given
// groups.
func (s *SqlGroupStore) ChannelMembersMinusGroupMembers(channelID string, groupIDs []string, page, perPage int) ([]*model.UserWithGroups, error) {
builder := s.channelMembersMinusGroupMembersQuery(channelID, groupIDs, false)
builder = builder.OrderBy("Users.Username ASC").Limit(uint64(perPage)).Offset(uint64(page * perPage))
users := []*model.UserWithGroups{}
if err := s.GetReplica().SelectBuilder(&users, builder); err != nil {
return nil, errors.Wrap(err, "failed to find UserWithGroups")
}
return users, nil
}
// CountChannelMembersMinusGroupMembers returns the count of the set of users in the given channel minus the set of users
// in the given groups.
func (s *SqlGroupStore) CountChannelMembersMinusGroupMembers(channelID string, groupIDs []string) (int64, error) {
queryString, args, err := s.channelMembersMinusGroupMembersQuery(channelID, groupIDs, true).ToSql()
if err != nil {
return 0, errors.Wrap(err, "count_channel_members_minus_group_members_tosql")
}
var count int64
if err := s.GetReplica().Get(&count, queryString, args...); err != nil {
return 0, errors.Wrap(err, "failed to count ChannelMembers")
}
return count, nil
}
func (s *SqlGroupStore) AdminRoleGroupsForSyncableMember(userID, syncableID string, syncableType model.GroupSyncableType) ([]string, error) {
var groupIds []string
query := fmt.Sprintf(`
SELECT
GroupMembers.GroupId
FROM
GroupMembers
INNER JOIN
Group%[1]ss ON Group%[1]ss.GroupId = GroupMembers.GroupId
WHERE
GroupMembers.UserId = ?
AND GroupMembers.DeleteAt = 0
AND %[1]sId = ?
AND Group%[1]ss.DeleteAt = 0
AND Group%[1]ss.SchemeAdmin = TRUE`, syncableType)
err := s.GetReplica().Select(&groupIds, query, userID, syncableID)
if err != nil {
return nil, errors.Wrap(err, "failed to find Group ids")
}
return groupIds, nil
}
func (s *SqlGroupStore) PermittedSyncableAdmins(syncableID string, syncableType model.GroupSyncableType) ([]string, error) {
builder := s.getQueryBuilder().Select("UserId").
From(fmt.Sprintf("Group%ss", syncableType)).
Join(fmt.Sprintf("GroupMembers ON GroupMembers.GroupId = Group%ss.GroupId AND Group%[1]ss.SchemeAdmin = TRUE AND GroupMembers.DeleteAt = 0", syncableType.String())).Where(fmt.Sprintf("Group%[1]ss.%[1]sId = ?", syncableType.String()), syncableID)
var userIDs []string
if err := s.GetMaster().SelectBuilder(&userIDs, builder); err != nil {
return nil, errors.Wrapf(err, "failed to find User ids")
}
return userIDs, nil
}
func (s *SqlGroupStore) GroupCount() (int64, error) {
return s.countTable("UserGroups")
}
func (s *SqlGroupStore) GroupCountBySource(source model.GroupSource) (int64, error) {
return s.countTableWithSelectAndWhere("COUNT(*)", "UserGroups", sq.Eq{"Source": source, "DeleteAt": 0})
}
func (s *SqlGroupStore) GroupTeamCount() (int64, error) {
return s.countTable("GroupTeams")
}
func (s *SqlGroupStore) GroupChannelCount() (int64, error) {
return s.countTable("GroupChannels")
}
func (s *SqlGroupStore) GroupMemberCount() (int64, error) {
return s.countTable("GroupMembers")
}
func (s *SqlGroupStore) DistinctGroupMemberCount() (int64, error) {
return s.countTableWithSelectAndWhere("COUNT(DISTINCT UserId)", "GroupMembers", nil)
}
func (s *SqlGroupStore) DistinctGroupMemberCountForSource(source model.GroupSource) (int64, error) {
builder := s.getQueryBuilder().
Select("COUNT(DISTINCT GroupMembers.UserId)").
From("GroupMembers").
Join("UserGroups ON GroupMembers.GroupId = UserGroups.Id").
Where(sq.Eq{"UserGroups.Source": source, "GroupMembers.DeleteAt": 0})
var count int64
if err := s.GetReplica().GetBuilder(&count, builder); err != nil {
return 0, errors.Wrapf(err, "failed to select distinct groupmember count for source %q", source)
}
return count, nil
}
func (s *SqlGroupStore) GroupCountWithAllowReference() (int64, error) {
return s.countTableWithSelectAndWhere("COUNT(*)", "UserGroups", sq.Eq{"AllowReference": true, "DeleteAt": 0})
}
func (s *SqlGroupStore) countTable(tableName string) (int64, error) {
return s.countTableWithSelectAndWhere("COUNT(*)", tableName, nil)
}
func (s *SqlGroupStore) countTableWithSelectAndWhere(selectStr, tableName string, whereStmt map[string]any) (int64, error) {
if whereStmt == nil {
whereStmt = sq.Eq{"DeleteAt": 0}
}
query := s.getQueryBuilder().Select(selectStr).From(tableName).Where(whereStmt)
sql, args, err := query.ToSql()
if err != nil {
return 0, errors.Wrap(err, "count_table_with_select_and_where_tosql")
}
var count int64
err = s.GetReplica().Get(&count, sql, args...)
if err != nil {
return 0, errors.Wrapf(err, "failed to count from table %s", tableName)
}
return count, nil
}
func (s *SqlGroupStore) UpsertMembers(groupID string, userIDs []string) ([]*model.GroupMember, error) {
members, query, err := s.buildUpsertMembersQuery(groupID, userIDs)
if err != nil {
return nil, err
}
if _, err = s.GetMaster().ExecBuilder(query); err != nil {
return nil, errors.Wrap(err, "failed to save GroupMember")
}
return members, err
}
func (s *SqlGroupStore) buildUpsertMembersQuery(groupID string, userIDs []string) (members []*model.GroupMember, builder sq.InsertBuilder, err error) {
var retrievedGroup model.Group
// Check Group exists
if err = s.GetReplica().GetBuilder(&retrievedGroup, s.userGroupsSelectQuery.Where(sq.Eq{"UserGroups.Id": groupID})); err != nil {
err = errors.Wrapf(err, "failed to get UserGroup with groupId=%s", groupID)
return
}
// Check Users exist
if err = s.checkUsersExist(userIDs); err != nil {
return
}
builder = s.getQueryBuilder().
Insert("GroupMembers").
Columns("GroupId", "UserId", "CreateAt", "DeleteAt")
members = make([]*model.GroupMember, 0, len(userIDs))
createAt := model.GetMillis()
for _, userId := range userIDs {
member := &model.GroupMember{
GroupId: groupID,
UserId: userId,
CreateAt: createAt,
DeleteAt: 0,
}
builder = builder.Values(member.GroupId, member.UserId, member.CreateAt, member.DeleteAt)
members = append(members, member)
}
builder = builder.SuffixExpr(sq.Expr("ON CONFLICT (groupid, userid) DO UPDATE SET CreateAt = ?, DeleteAt = ?", createAt, 0))
return
}
func (s *SqlGroupStore) DeleteMembers(groupID string, userIDs []string) ([]*model.GroupMember, error) {
members, query, err := s.buildDeleteMembersQuery(groupID, userIDs)
if err != nil {
return nil, err
}
if _, err = s.GetMaster().ExecBuilder(query); err != nil {
return nil, errors.Wrap(err, "failed to delete GroupMembers")
}
return members, err
}
func (s *SqlGroupStore) buildDeleteMembersQuery(groupID string, userIDs []string) (members []*model.GroupMember, builder sq.UpdateBuilder, err error) {
membersSelectQuery := s.groupMembersSelectQuery.
Where(sq.And{
sq.Eq{"GroupMembers.GroupId": groupID},
sq.Eq{"GroupMembers.UserId": userIDs},
sq.Eq{"GroupMembers.DeleteAt": 0},
})
if err = s.GetReplica().SelectBuilder(&members, membersSelectQuery); err != nil {
return
}
if len(members) != len(userIDs) {
retrievedRecords := make(map[string]bool)
for _, member := range members {
retrievedRecords[member.UserId] = true
}
for _, userID := range userIDs {
if _, ok := retrievedRecords[userID]; !ok {
err = store.NewErrNotFound("User", userID)
return
}
}
}
deleteAt := model.GetMillis()
for _, member := range members {
member.DeleteAt = deleteAt
}
builder = s.getQueryBuilder().
Update("GroupMembers").
Set("DeleteAt", deleteAt).
Where(sq.And{
sq.Eq{"GroupId": groupID},
sq.Eq{"UserId": userIDs},
})
return
}