MM-56083 Add PatchMultipleMembersNotifyProps plugin API (#25690)

* Add ChannelStore.UpdateMultipleMembersNotifyProps

* Make UpdateMultipleMembersNotifyProps return updated values from the DB

* Add UpdateChannelMembersNotifications plugin API

* Extract i18n

* Fix style

* Make layers

* Change to PatchMultipleMembersNotifyProps

* Add limit to PatchChannelMembersNotifyProps

* Add additional unit tests

* Address feedback

* Lowercase decodeJSON

* Have PatchMultipleMembersNotifyProps update LastUpdateAt

* Fix tests that relied on unreliable return order

* Fix i18n
Этот коммит содержится в:
Harrison Healey
2024-01-11 13:24:52 -05:00
коммит произвёл GitHub
родитель aafe7439af
Коммит 4d96c11314
21 изменённых файлов: 780 добавлений и 37 удалений

Просмотреть файл

@@ -2090,6 +2090,24 @@ func (s *OpenTracingLayerChannelStore) MigrateChannelMembers(fromChannelID strin
return result, err
}
func (s *OpenTracingLayerChannelStore) PatchMultipleMembersNotifyProps(members []*model.ChannelMemberIdentifier, notifyProps map[string]string) ([]*model.ChannelMember, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.PatchMultipleMembersNotifyProps")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.PatchMultipleMembersNotifyProps(members, notifyProps)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) PermanentDelete(ctx request.CTX, channelID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.PermanentDelete")

Просмотреть файл

@@ -2279,6 +2279,27 @@ func (s *RetryLayerChannelStore) MigrateChannelMembers(fromChannelID string, fro
}
func (s *RetryLayerChannelStore) PatchMultipleMembersNotifyProps(members []*model.ChannelMemberIdentifier, notifyProps map[string]string) ([]*model.ChannelMember, error) {
tries := 0
for {
result, err := s.ChannelStore.PatchMultipleMembersNotifyProps(members, notifyProps)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) PermanentDelete(ctx request.CTX, channelID string) error {
tries := 0

Просмотреть файл

@@ -1922,6 +1922,88 @@ func (s SqlChannelStore) UpdateMemberNotifyProps(channelID, userID string, props
return dbMember.ToModel(), err
}
// PatchMultipleMembersNotifyProps updates the NotifyProps of multiple channel members at once without modifying
// unspecified fields.
//
// Note that the returned array may not be in the same order as the provided IDs.
func (s SqlChannelStore) PatchMultipleMembersNotifyProps(members []*model.ChannelMemberIdentifier, notifyProps map[string]string) ([]*model.ChannelMember, error) {
if len(notifyProps) == 0 {
return nil, errors.New("PatchMultipleMembersNotifyProps: No notifyProps specified")
}
if err := model.IsChannelMemberNotifyPropsValid(notifyProps, true); err != nil {
return nil, err
}
// Make the where clause first since it'll be used multiple times
whereClause := sq.Or{}
for _, member := range members {
whereClause = append(whereClause, sq.And{
sq.Eq{"ChannelId": member.ChannelId},
sq.Eq{"UserId": member.UserId},
})
}
// Update the channel members
builder := s.getQueryBuilder().Update("ChannelMembers")
if s.DriverName() == model.DatabaseDriverPostgres {
jsonNotifyProps := string(model.ToJSON(notifyProps))
builder = builder.Set("notifyprops", sq.Expr("notifyprops || ?::jsonb", jsonNotifyProps))
} else {
// Unpack the keys and values to pass to MySQL
jsonArgs, jsonSQL := constructMySQLJSONArgs(notifyProps)
jsonExpr := sq.Expr(fmt.Sprintf("JSON_SET(NotifyProps, %s)", jsonSQL), jsonArgs...)
// Example: UPDATE ChannelMembers
// SET NotifyProps = JSON_SET(NotifyProps, '$.mark_unread', '"yes"' [, ...])
// WHERE ...
builder = builder.Set("NotifyProps", jsonExpr)
}
builder = builder.Set("LastUpdateAt", model.GetMillis())
builder = builder.Where(whereClause)
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return nil, errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
transaction.trace = true
result, err := transaction.ExecBuilder(builder)
if err != nil {
return nil, errors.Wrap(err, "PatchMultipleMembersNotifyProps: Failed to update ChannelMembers")
} else if count, _ := result.RowsAffected(); count != int64(len(members)) {
return nil, errors.Wrap(err, "PatchMultipleMembersNotifyProps: Unable to update all ChannelMembers, some must not exist")
}
// Get the updated channel members
selectSQL, selectArgs, err := s.channelMembersForTeamWithSchemeSelectQuery.
Where(whereClause).ToSql()
if err != nil {
return nil, errors.Wrapf(err, "PatchMultipleMembersNotifyProps_Select_ToSql")
}
var dbMembers []*channelMemberWithSchemeRoles
if err := transaction.Select(&dbMembers, selectSQL, selectArgs...); err != nil {
return nil, errors.Wrapf(err, "PatchMultipleMembersNotifyProps: Failed to get updated ChannelMembers")
}
if err := transaction.Commit(); err != nil {
return nil, errors.Wrap(err, "commit_transaction")
}
updated := make([]*model.ChannelMember, len(dbMembers))
for i, dbMember := range dbMembers {
updated[i] = dbMember.ToModel()
}
return updated, nil
}
func (s SqlChannelStore) GetMembers(channelID string, offset, limit int) (model.ChannelMembers, error) {
sql, args, err := s.channelMembersForTeamWithSchemeSelectQuery.
Where(sq.Eq{

Просмотреть файл

@@ -225,6 +225,7 @@ type ChannelStore interface {
// UpdateMemberNotifyProps patches the notifyProps field with the given props map.
// It replaces existing fields and creates new ones which don't exist.
UpdateMemberNotifyProps(channelID, userID string, props map[string]string) (*model.ChannelMember, error)
PatchMultipleMembersNotifyProps(members []*model.ChannelMemberIdentifier, notifyProps map[string]string) ([]*model.ChannelMember, error)
GetMembers(channelID string, offset, limit int) (model.ChannelMembers, error)
GetMember(ctx context.Context, channelID string, userID string) (*model.ChannelMember, error)
GetMemberLastViewedAt(ctx context.Context, channelID string, userID string) (int64, error)

Просмотреть файл

@@ -90,6 +90,7 @@ func TestChannelStore(t *testing.T, rctx request.CTX, ss store.Store, s SqlStore
t.Run("SaveMultipleMembers", func(t *testing.T) { testChannelSaveMultipleMembers(t, rctx, ss) })
t.Run("UpdateMember", func(t *testing.T) { testChannelUpdateMember(t, rctx, ss) })
t.Run("UpdateMemberNotifyProps", func(t *testing.T) { testChannelUpdateMemberNotifyProps(t, rctx, ss) })
t.Run("PatchMultipleMembersNotifyProps", func(t *testing.T) { testChannelPatchMultipleMembersNotifyProps(t, rctx, ss) })
t.Run("UpdateMultipleMembers", func(t *testing.T) { testChannelUpdateMultipleMembers(t, rctx, ss) })
t.Run("RemoveMember", func(t *testing.T) { testChannelRemoveMember(t, rctx, ss) })
t.Run("RemoveMembers", func(t *testing.T) { testChannelRemoveMembers(t, rctx, ss) })
@@ -3283,6 +3284,133 @@ func testChannelUpdateMemberNotifyProps(t *testing.T, rctx request.CTX, ss store
})
}
func testChannelPatchMultipleMembersNotifyProps(t *testing.T, rctx request.CTX, ss store.Store) {
t.Run("should save multiple channel members' notify props at once", func(t *testing.T) {
channel1, err := ss.Channel().Save(&model.Channel{
Name: model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
channel2, err := ss.Channel().Save(&model.Channel{
Name: model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
user1, err := ss.User().Save(&model.User{Username: model.NewId(), Email: MakeEmail()})
require.NoError(t, err)
user2, err := ss.User().Save(&model.User{Username: model.NewId(), Email: MakeEmail()})
require.NoError(t, err)
original1, err := ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: channel1.Id,
UserId: user1.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, err)
original2, err := ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: channel1.Id,
UserId: user2.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, err)
original3, err := ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: channel2.Id,
UserId: user1.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, err)
require.Equal(t, model.ChannelNotifyDefault, original1.NotifyProps[model.DesktopNotifyProp])
require.Equal(t, model.ChannelAutoFollowThreadsOff, original1.NotifyProps[model.ChannelAutoFollowThreads])
require.Equal(t, "", original1.NotifyProps["test_key"])
require.Equal(t, model.ChannelNotifyDefault, original2.NotifyProps[model.DesktopNotifyProp])
require.Equal(t, model.ChannelAutoFollowThreadsOff, original2.NotifyProps[model.ChannelAutoFollowThreads])
require.Equal(t, "", original2.NotifyProps["test_key"])
require.Equal(t, model.ChannelNotifyDefault, original3.NotifyProps[model.DesktopNotifyProp])
require.Equal(t, model.ChannelAutoFollowThreadsOff, original3.NotifyProps[model.ChannelAutoFollowThreads])
require.Equal(t, "", original3.NotifyProps["test_key"])
// Sleep for 1ms to ensure that the LastUpdateAt will change
time.Sleep(1 * time.Millisecond)
// Save the channel members
updated, err := ss.Channel().PatchMultipleMembersNotifyProps(
[]*model.ChannelMemberIdentifier{
{
ChannelId: original1.ChannelId,
UserId: original1.UserId,
},
{
ChannelId: original2.ChannelId,
UserId: original2.UserId,
},
{
ChannelId: original3.ChannelId,
UserId: original3.UserId,
},
},
map[string]string{
model.ChannelAutoFollowThreads: model.ChannelAutoFollowThreadsOff,
"test_key": "test_value",
},
)
require.NoError(t, err)
// Ensure the specified fields changed and that the unspecified fields did not
assert.Equal(t, original1.NotifyProps[model.DesktopNotifyProp], updated[0].NotifyProps[model.DesktopNotifyProp])
assert.Equal(t, model.ChannelAutoFollowThreadsOff, updated[0].NotifyProps[model.ChannelAutoFollowThreads])
assert.Equal(t, "test_value", updated[0].NotifyProps["test_key"])
assert.Equal(t, original2.NotifyProps[model.DesktopNotifyProp], updated[1].NotifyProps[model.DesktopNotifyProp])
assert.Equal(t, model.ChannelAutoFollowThreadsOff, updated[1].NotifyProps[model.ChannelAutoFollowThreads])
assert.Equal(t, "test_value", updated[1].NotifyProps["test_key"])
assert.Equal(t, original3.NotifyProps[model.DesktopNotifyProp], updated[2].NotifyProps[model.DesktopNotifyProp])
assert.Equal(t, model.ChannelAutoFollowThreadsOff, updated[2].NotifyProps[model.ChannelAutoFollowThreads])
assert.Equal(t, "test_value", updated[2].NotifyProps["test_key"])
assert.Equal(t, original1.NotifyProps[model.DesktopNotifyProp], updated[0].NotifyProps[model.DesktopNotifyProp])
assert.Equal(t, original2.NotifyProps[model.DesktopNotifyProp], updated[1].NotifyProps[model.DesktopNotifyProp])
assert.Equal(t, original3.NotifyProps[model.DesktopNotifyProp], updated[2].NotifyProps[model.DesktopNotifyProp])
// Ensure that LastUpdateAt was updated
assert.Greater(t, updated[0].LastUpdateAt, original1.LastUpdateAt)
assert.Greater(t, updated[1].LastUpdateAt, original2.LastUpdateAt)
assert.Greater(t, updated[2].LastUpdateAt, original3.LastUpdateAt)
})
t.Run("should not allow saving invalid notify props", func(t *testing.T) {
channel, err := ss.Channel().Save(&model.Channel{
Name: model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
user, err := ss.User().Save(&model.User{Username: model.NewId(), Email: MakeEmail()})
require.NoError(t, err)
_, err = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: channel.Id,
UserId: user.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, err)
// Save the channel member
_, err = ss.Channel().PatchMultipleMembersNotifyProps(
[]*model.ChannelMemberIdentifier{
{
ChannelId: channel.Id,
UserId: user.Id,
},
},
map[string]string{
model.MarkUnreadNotifyProp: "garbage",
},
)
assert.Error(t, err)
})
}
func testChannelRemoveMember(t *testing.T, rctx request.CTX, ss store.Store) {
u1, err := ss.User().Save(&model.User{Username: model.NewId(), Email: MakeEmail()})
require.NoError(t, err)

Просмотреть файл

@@ -1943,6 +1943,32 @@ func (_m *ChannelStore) MigrateChannelMembers(fromChannelID string, fromUserID s
return r0, r1
}
// PatchMultipleMembersNotifyProps provides a mock function with given fields: members, notifyProps
func (_m *ChannelStore) PatchMultipleMembersNotifyProps(members []*model.ChannelMemberIdentifier, notifyProps map[string]string) ([]*model.ChannelMember, error) {
ret := _m.Called(members, notifyProps)
var r0 []*model.ChannelMember
var r1 error
if rf, ok := ret.Get(0).(func([]*model.ChannelMemberIdentifier, map[string]string) ([]*model.ChannelMember, error)); ok {
return rf(members, notifyProps)
}
if rf, ok := ret.Get(0).(func([]*model.ChannelMemberIdentifier, map[string]string) []*model.ChannelMember); ok {
r0 = rf(members, notifyProps)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.ChannelMember)
}
}
if rf, ok := ret.Get(1).(func([]*model.ChannelMemberIdentifier, map[string]string) error); ok {
r1 = rf(members, notifyProps)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// PermanentDelete provides a mock function with given fields: ctx, channelID
func (_m *ChannelStore) PermanentDelete(ctx request.CTX, channelID string) error {
ret := _m.Called(ctx, channelID)

Просмотреть файл

@@ -1950,6 +1950,22 @@ func (s *TimerLayerChannelStore) MigrateChannelMembers(fromChannelID string, fro
return result, err
}
func (s *TimerLayerChannelStore) PatchMultipleMembersNotifyProps(members []*model.ChannelMemberIdentifier, notifyProps map[string]string) ([]*model.ChannelMember, error) {
start := time.Now()
result, err := s.ChannelStore.PatchMultipleMembersNotifyProps(members, notifyProps)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.PatchMultipleMembersNotifyProps", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) PermanentDelete(ctx request.CTX, channelID string) error {
start := time.Now()