MM-45317: global drafts endpoints and ws events (#20614)

* MM-23881: global drafts endpoints and ws events

Adds endpoints:
- create/update drafts
- delete draft
- get drafts

Adds WS events:
- draft_updated
- draft_created
- draft_deleted

* Ordering and WS event name fixes

* Adds PostID to the drafts table

In the future the drafts will include edited posts, this commit adds the
post id in the combined pkey of the table.

* Fixes route for deleting a thread draft

* Fixes failed checks

* Fixes migrations

* Fixes migration

* Extract translation strings

* Removes PostID since we won't sync editing posts

* Fixes tests

* Fixes i18n

* Update migrations for global drafts

* update branch with latest master changes

* Add feature flag for global drafts

* Set global drafts feature flag default to true

* Added support for files in drafts

* Fix failing i18n check

* Added support for deleting files in drafts

* Revert "Added support for deleting files in drafts"

This reverts commit 45dfd04a760359de2e8814d652c9ef46daf994f6.

* Triggering new test server

* Add config setting 'AllowSyncedDrafts' for syncing drafts with server

* Triggering new test server

* Triggering new test server

* Add guard for config setting and add initial tests

* Fix i18n and lint errors

* Triggering new test server

* Add tests for drafts

* fix lint issues

* Add tests for model/draft

* Triggering new test server

* Triggering new test server

* Trigger new test server

* Address PR comments

* Change left join to regular join in GetDraftsForUser

* Fix broken test

Maybe consider adding an inclDeleted field if we want to get deleted drafts in the future

* fix translations

* Add store tests for drafts

* fix test naming

* remove comment

* update migrations

* set feature flag default to false

* update migrations

Co-authored-by: Mylon Suren <mylonsuren@gmail.com>
Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
Этот коммит содержится в:
Kyriakos Z
2022-11-24 05:21:40 +02:00
коммит произвёл GitHub
родитель 27db854089
Коммит 5e5769c4ee
33 изменённых файлов: 2575 добавлений и 0 удалений

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

@@ -27,6 +27,7 @@ type OpenTracingLayer struct {
CommandStore store.CommandStore
CommandWebhookStore store.CommandWebhookStore
ComplianceStore store.ComplianceStore
DraftStore store.DraftStore
EmojiStore store.EmojiStore
FileInfoStore store.FileInfoStore
GroupStore store.GroupStore
@@ -93,6 +94,10 @@ func (s *OpenTracingLayer) Compliance() store.ComplianceStore {
return s.ComplianceStore
}
func (s *OpenTracingLayer) Draft() store.DraftStore {
return s.DraftStore
}
func (s *OpenTracingLayer) Emoji() store.EmojiStore {
return s.EmojiStore
}
@@ -261,6 +266,11 @@ type OpenTracingLayerComplianceStore struct {
Root *OpenTracingLayer
}
type OpenTracingLayerDraftStore struct {
store.DraftStore
Root *OpenTracingLayer
}
type OpenTracingLayerEmojiStore struct {
store.EmojiStore
Root *OpenTracingLayer
@@ -3228,6 +3238,96 @@ func (s *OpenTracingLayerComplianceStore) Update(compliance *model.Compliance) (
return result, err
}
func (s *OpenTracingLayerDraftStore) Delete(userID string, channelID string, rootID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "DraftStore.Delete")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.DraftStore.Delete(userID, channelID, rootID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerDraftStore) Get(userID string, channelID string, rootID string) (*model.Draft, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "DraftStore.Get")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.DraftStore.Get(userID, channelID, rootID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerDraftStore) GetDraftsForUser(userID string, teamID string) ([]*model.Draft, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "DraftStore.GetDraftsForUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.DraftStore.GetDraftsForUser(userID, teamID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerDraftStore) Save(d *model.Draft) (*model.Draft, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "DraftStore.Save")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.DraftStore.Save(d)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerDraftStore) Update(d *model.Draft) (*model.Draft, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "DraftStore.Update")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.DraftStore.Update(d)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerEmojiStore) Delete(emoji *model.Emoji, timestamp int64) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "EmojiStore.Delete")
@@ -12681,6 +12781,7 @@ func New(childStore store.Store, ctx context.Context) *OpenTracingLayer {
newStore.CommandStore = &OpenTracingLayerCommandStore{CommandStore: childStore.Command(), Root: &newStore}
newStore.CommandWebhookStore = &OpenTracingLayerCommandWebhookStore{CommandWebhookStore: childStore.CommandWebhook(), Root: &newStore}
newStore.ComplianceStore = &OpenTracingLayerComplianceStore{ComplianceStore: childStore.Compliance(), Root: &newStore}
newStore.DraftStore = &OpenTracingLayerDraftStore{DraftStore: childStore.Draft(), Root: &newStore}
newStore.EmojiStore = &OpenTracingLayerEmojiStore{EmojiStore: childStore.Emoji(), Root: &newStore}
newStore.FileInfoStore = &OpenTracingLayerFileInfoStore{FileInfoStore: childStore.FileInfo(), Root: &newStore}
newStore.GroupStore = &OpenTracingLayerGroupStore{GroupStore: childStore.Group(), Root: &newStore}

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

@@ -30,6 +30,7 @@ type RetryLayer struct {
CommandStore store.CommandStore
CommandWebhookStore store.CommandWebhookStore
ComplianceStore store.ComplianceStore
DraftStore store.DraftStore
EmojiStore store.EmojiStore
FileInfoStore store.FileInfoStore
GroupStore store.GroupStore
@@ -96,6 +97,10 @@ func (s *RetryLayer) Compliance() store.ComplianceStore {
return s.ComplianceStore
}
func (s *RetryLayer) Draft() store.DraftStore {
return s.DraftStore
}
func (s *RetryLayer) Emoji() store.EmojiStore {
return s.EmojiStore
}
@@ -264,6 +269,11 @@ type RetryLayerComplianceStore struct {
Root *RetryLayer
}
type RetryLayerDraftStore struct {
store.DraftStore
Root *RetryLayer
}
type RetryLayerEmojiStore struct {
store.EmojiStore
Root *RetryLayer
@@ -3614,6 +3624,111 @@ func (s *RetryLayerComplianceStore) Update(compliance *model.Compliance) (*model
}
func (s *RetryLayerDraftStore) Delete(userID string, channelID string, rootID string) error {
tries := 0
for {
err := s.DraftStore.Delete(userID, channelID, rootID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerDraftStore) Get(userID string, channelID string, rootID string) (*model.Draft, error) {
tries := 0
for {
result, err := s.DraftStore.Get(userID, channelID, rootID)
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 *RetryLayerDraftStore) GetDraftsForUser(userID string, teamID string) ([]*model.Draft, error) {
tries := 0
for {
result, err := s.DraftStore.GetDraftsForUser(userID, teamID)
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 *RetryLayerDraftStore) Save(d *model.Draft) (*model.Draft, error) {
tries := 0
for {
result, err := s.DraftStore.Save(d)
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 *RetryLayerDraftStore) Update(d *model.Draft) (*model.Draft, error) {
tries := 0
for {
result, err := s.DraftStore.Update(d)
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 *RetryLayerEmojiStore) Delete(emoji *model.Emoji, timestamp int64) error {
tries := 0
@@ -14460,6 +14575,7 @@ func New(childStore store.Store) *RetryLayer {
newStore.CommandStore = &RetryLayerCommandStore{CommandStore: childStore.Command(), Root: &newStore}
newStore.CommandWebhookStore = &RetryLayerCommandWebhookStore{CommandWebhookStore: childStore.CommandWebhook(), Root: &newStore}
newStore.ComplianceStore = &RetryLayerComplianceStore{ComplianceStore: childStore.Compliance(), Root: &newStore}
newStore.DraftStore = &RetryLayerDraftStore{DraftStore: childStore.Draft(), Root: &newStore}
newStore.EmojiStore = &RetryLayerEmojiStore{EmojiStore: childStore.Emoji(), Root: &newStore}
newStore.FileInfoStore = &RetryLayerFileInfoStore{FileInfoStore: childStore.FileInfo(), Root: &newStore}
newStore.GroupStore = &RetryLayerGroupStore{GroupStore: childStore.Group(), Root: &newStore}

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

@@ -54,6 +54,7 @@ func genStore() *mocks.Store {
mock.On("UserTermsOfService").Return(&mocks.UserTermsOfServiceStore{})
mock.On("Webhook").Return(&mocks.WebhookStore{})
mock.On("NotifyAdmin").Return(&mocks.NotifyAdminStore{})
mock.On("Draft").Return(&mocks.DraftStore{})
mock.On("PostPriority").Return(&mocks.PostPriorityStore{})
mock.On("PostAcknowledgement").Return(&mocks.PostAcknowledgementStore{})
return mock

240
store/sqlstore/draft_store.go Обычный файл
Просмотреть файл

@@ -0,0 +1,240 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"sync"
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/einterfaces"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/shared/mlog"
"github.com/mattermost/mattermost-server/v6/store"
)
type SqlDraftStore struct {
*SqlStore
metrics einterfaces.MetricsInterface
maxDraftSizeOnce sync.Once
maxDraftSizeCached int
}
func draftSliceColumns() []string {
return []string{"CreateAt", "UpdateAt", "DeleteAt", "Message", "RootId", "ChannelId", "UserId", "FileIds", "Props"}
}
func draftToSlice(draft *model.Draft) []interface{} {
return []interface{}{
draft.CreateAt,
draft.UpdateAt,
draft.DeleteAt,
draft.Message,
draft.RootId,
draft.ChannelId,
draft.UserId,
model.ArrayToJSON(draft.FileIds),
model.StringInterfaceToJSON(draft.Props),
}
}
func newSqlDraftStore(sqlStore *SqlStore, metrics einterfaces.MetricsInterface) store.DraftStore {
return &SqlDraftStore{
SqlStore: sqlStore,
metrics: metrics,
maxDraftSizeCached: model.PostMessageMaxRunesV1,
}
}
func (s *SqlDraftStore) Get(userId, channelId, rootId string) (*model.Draft, error) {
query := s.getQueryBuilder().
Select("*").
From("Drafts").
Where(sq.Eq{
"UserId": userId,
"ChannelId": channelId,
"RootId": rootId,
"DeleteAt": 0,
})
dt := model.Draft{}
err := s.GetReplicaX().GetBuilder(&dt, query)
if err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Draft", channelId)
}
return nil, errors.Wrapf(err, "failed to find draft with channelid = %s", channelId)
}
return &dt, nil
}
func (s *SqlDraftStore) Save(draft *model.Draft) (*model.Draft, error) {
draft.PreSave()
maxDraftSize := s.GetMaxDraftSize()
if err := draft.IsValid(maxDraftSize); err != nil {
return nil, err
}
builder := s.getQueryBuilder().Insert("Drafts").Columns(draftSliceColumns()...).Values(draftToSlice(draft)...)
query, args, err := builder.ToSql()
if err != nil {
return nil, errors.Wrap(err, "save_draft_tosql")
}
if _, err = s.GetMasterX().Exec(query, args...); err != nil {
return nil, errors.Wrap(err, "failed to save Draft")
}
return draft, nil
}
func (s *SqlDraftStore) Update(draft *model.Draft) (*model.Draft, error) {
draft.PreUpdate()
maxDraftSize := s.GetMaxDraftSize()
if err := draft.IsValid(maxDraftSize); err != nil {
return nil, err
}
query := s.getQueryBuilder().
Update("Drafts").
Set("UpdateAt", draft.UpdateAt).
Set("Message", draft.Message).
Set("Props", draft.Props).
Set("FileIds", draft.FileIds).
Where(sq.Eq{
"UserId": draft.UserId,
"ChannelId": draft.ChannelId,
"RootId": draft.RootId,
"DeleteAt": 0,
})
sql, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrapf(err, "failed to convert to sql")
}
if _, err = s.GetMasterX().Exec(sql, args...); err != nil {
return nil, errors.Wrapf(err, "failed to update Draft with channelid=%s", draft.ChannelId)
}
return draft, nil
}
func (s *SqlDraftStore) GetDraftsForUser(userID, teamID string) ([]*model.Draft, error) {
var drafts []*model.Draft
query := s.getQueryBuilder().
Select("Drafts.*").
From("Drafts").
InnerJoin("ChannelMembers ON ChannelMembers.ChannelId = Drafts.ChannelId").
Where(sq.And{
sq.Eq{"Drafts.DeleteAt": 0},
sq.Eq{"Drafts.UserId": userID},
sq.Eq{"ChannelMembers.UserId": userID},
}).
OrderBy("Drafts.UpdateAt DESC")
if teamID != "" {
query = query.
Join("Channels ON Drafts.ChannelId = Channels.Id").
Where(sq.Or{
sq.Eq{"Channels.TeamId": teamID},
sq.Eq{"Channels.TeamId": ""},
})
}
err := s.GetReplicaX().SelectBuilder(&drafts, query)
if err != nil {
return nil, errors.Wrap(err, "failed to get user drafts")
}
return drafts, nil
}
func (s *SqlDraftStore) Delete(userID, channelID, rootID string) error {
time := model.GetMillis()
query := s.getQueryBuilder().
Update("Drafts").
Set("UpdateAt", time).
Set("DeleteAt", time).
Where(sq.Eq{
"UserId": userID,
"ChannelId": channelID,
"RootId": rootID,
})
sql, args, err := query.ToSql()
if err != nil {
return errors.Wrapf(err, "failed to convert to sql")
}
_, err = s.GetMasterX().Exec(sql, args...)
if err != nil {
return errors.Wrap(err, "failed to delete Draft")
}
return nil
}
// GetMaxDraftSize returns the maximum number of runes that may be stored in a post.
func (s *SqlDraftStore) GetMaxDraftSize() int {
s.maxDraftSizeOnce.Do(func() {
s.maxDraftSizeCached = s.determineMaxDraftSize()
})
return s.maxDraftSizeCached
}
func (s *SqlDraftStore) determineMaxDraftSize() int {
var maxDraftSizeBytes int32
if s.DriverName() == model.DatabaseDriverPostgres {
// The Draft.Message column in Postgres has historically been VARCHAR(4000), but
// may be manually enlarged to support longer drafts.
if err := s.GetReplicaX().Get(&maxDraftSizeBytes, `
SELECT
COALESCE(character_maximum_length, 0)
FROM
information_schema.columns
WHERE
table_name = 'drafts'
AND column_name = 'message'
`); err != nil {
mlog.Warn("Unable to determine the maximum supported draft size", mlog.Err(err))
}
} else if s.DriverName() == model.DatabaseDriverMysql {
// The Draft.Message column in MySQL has historically been TEXT, with a maximum
// limit of 65535.
if err := s.GetReplicaX().Get(&maxDraftSizeBytes, `
SELECT
COALESCE(CHARACTER_MAXIMUM_LENGTH, 0)
FROM
INFORMATION_SCHEMA.COLUMNS
WHERE
table_schema = DATABASE()
AND table_name = 'Drafts'
AND column_name = 'Message'
LIMIT 0, 1
`); err != nil {
mlog.Warn("Unable to determine the maximum supported draft size", mlog.Err(err))
}
} else {
mlog.Warn("No implementation found to determine the maximum supported draft size")
}
// Assume a worst-case representation of four bytes per rune.
maxDraftSize := int(maxDraftSizeBytes) / 4
mlog.Info("Draft.Message has size restrictions", mlog.Int("max_characters", maxDraftSize), mlog.Int32("max_bytes", maxDraftSizeBytes))
return maxDraftSize
}

350
store/sqlstore/draft_store_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,350 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"testing"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/store"
"github.com/mattermost/mattermost-server/v6/store/storetest"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestDraftStore(t *testing.T) {
StoreTestWithSqlStore(t, storetest.TestDraftStore)
}
func TestSaveDraft(t *testing.T) {
StoreTest(t, func(t *testing.T, ss store.Store) {
user := &model.User{
Id: model.NewId(),
}
channel := &model.Channel{
Id: model.NewId(),
}
channel2 := &model.Channel{
Id: model.NewId(),
}
member1 := &model.ChannelMember{
ChannelId: channel.Id,
UserId: user.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
member2 := &model.ChannelMember{
ChannelId: channel2.Id,
UserId: user.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, err := ss.Channel().SaveMember(member1)
require.NoError(t, err)
_, err = ss.Channel().SaveMember(member2)
require.NoError(t, err)
draft1 := &model.Draft{
CreateAt: 00001,
UpdateAt: 00001,
DeleteAt: 0,
UserId: user.Id,
ChannelId: channel.Id,
Message: "draft1",
}
draft2 := &model.Draft{
CreateAt: 00005,
UpdateAt: 00005,
DeleteAt: 0,
UserId: user.Id,
ChannelId: channel2.Id,
Message: "draft2",
}
t.Run("save drafts", func(t *testing.T) {
draftResp, err := ss.Draft().Save(draft1)
assert.NoError(t, err)
assert.Equal(t, draft1.Message, draftResp.Message)
assert.Equal(t, draft1.ChannelId, draftResp.ChannelId)
draftResp, err = ss.Draft().Save(draft2)
assert.NoError(t, err)
assert.Equal(t, draft2.Message, draftResp.Message)
assert.Equal(t, draft2.ChannelId, draftResp.ChannelId)
})
})
}
func TestUpdateDraft(t *testing.T) {
StoreTest(t, func(t *testing.T, ss store.Store) {
user := &model.User{
Id: model.NewId(),
}
channel := &model.Channel{
Id: model.NewId(),
}
channel2 := &model.Channel{
Id: model.NewId(),
}
member1 := &model.ChannelMember{
ChannelId: channel.Id,
UserId: user.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
member2 := &model.ChannelMember{
ChannelId: channel2.Id,
UserId: user.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, err := ss.Channel().SaveMember(member1)
require.NoError(t, err)
_, err = ss.Channel().SaveMember(member2)
require.NoError(t, err)
draft1 := &model.Draft{
CreateAt: 00001,
UpdateAt: 00001,
DeleteAt: 0,
UserId: user.Id,
ChannelId: channel.Id,
Message: "draft1",
}
draft2 := &model.Draft{
CreateAt: 00005,
UpdateAt: 00005,
DeleteAt: 0,
UserId: user.Id,
ChannelId: channel2.Id,
Message: "draft2",
}
t.Run("update drafts", func(t *testing.T) {
draftResp, err := ss.Draft().Update(draft1)
assert.NoError(t, err)
assert.Equal(t, draft1.Message, draftResp.Message)
assert.Equal(t, draft1.ChannelId, draftResp.ChannelId)
draftResp, err = ss.Draft().Update(draft2)
assert.NoError(t, err)
assert.Equal(t, draft2.Message, draftResp.Message)
assert.Equal(t, draft2.ChannelId, draftResp.ChannelId)
})
})
}
func TestDeleteDraft(t *testing.T) {
StoreTest(t, func(t *testing.T, ss store.Store) {
user := &model.User{
Id: model.NewId(),
}
channel := &model.Channel{
Id: model.NewId(),
}
channel2 := &model.Channel{
Id: model.NewId(),
}
member1 := &model.ChannelMember{
ChannelId: channel.Id,
UserId: user.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
member2 := &model.ChannelMember{
ChannelId: channel2.Id,
UserId: user.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, err := ss.Channel().SaveMember(member1)
require.NoError(t, err)
_, err = ss.Channel().SaveMember(member2)
require.NoError(t, err)
draft1 := &model.Draft{
CreateAt: 00001,
UpdateAt: 00001,
DeleteAt: 0,
UserId: user.Id,
ChannelId: channel.Id,
Message: "draft1",
}
draft2 := &model.Draft{
CreateAt: 00005,
UpdateAt: 00005,
DeleteAt: 0,
UserId: user.Id,
ChannelId: channel2.Id,
Message: "draft2",
}
_, err = ss.Draft().Save(draft1)
require.NoError(t, err)
_, err = ss.Draft().Save(draft2)
require.NoError(t, err)
t.Run("delete drafts", func(t *testing.T) {
err := ss.Draft().Delete(user.Id, channel.Id, "")
assert.NoError(t, err)
err = ss.Draft().Delete(user.Id, channel2.Id, "")
assert.NoError(t, err)
})
})
}
func TestGetDraft(t *testing.T) {
StoreTest(t, func(t *testing.T, ss store.Store) {
user := &model.User{
Id: model.NewId(),
}
channel := &model.Channel{
Id: model.NewId(),
}
channel2 := &model.Channel{
Id: model.NewId(),
}
member1 := &model.ChannelMember{
ChannelId: channel.Id,
UserId: user.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
member2 := &model.ChannelMember{
ChannelId: channel2.Id,
UserId: user.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, err := ss.Channel().SaveMember(member1)
require.NoError(t, err)
_, err = ss.Channel().SaveMember(member2)
require.NoError(t, err)
draft1 := &model.Draft{
CreateAt: 00001,
UpdateAt: 00001,
DeleteAt: 0,
UserId: user.Id,
ChannelId: channel.Id,
Message: "draft1",
}
draft2 := &model.Draft{
CreateAt: 00005,
UpdateAt: 00005,
DeleteAt: 0,
UserId: user.Id,
ChannelId: channel2.Id,
Message: "draft2",
}
_, err = ss.Draft().Save(draft1)
require.NoError(t, err)
_, err = ss.Draft().Save(draft2)
require.NoError(t, err)
t.Run("get drafts", func(t *testing.T) {
draftResp, err := ss.Draft().Get(user.Id, channel.Id, "")
assert.NoError(t, err)
assert.Equal(t, draft1.Message, draftResp.Message)
assert.Equal(t, draft1.ChannelId, draftResp.ChannelId)
draftResp, err = ss.Draft().Get(user.Id, channel2.Id, "")
assert.NoError(t, err)
assert.Equal(t, draft2.Message, draftResp.Message)
assert.Equal(t, draft2.ChannelId, draftResp.ChannelId)
})
})
}
func TestGetDraftsForUser(t *testing.T) {
StoreTest(t, func(t *testing.T, ss store.Store) {
user := &model.User{
Id: model.NewId(),
}
channel := &model.Channel{
Id: model.NewId(),
}
channel2 := &model.Channel{
Id: model.NewId(),
}
member1 := &model.ChannelMember{
ChannelId: channel.Id,
UserId: user.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
member2 := &model.ChannelMember{
ChannelId: channel2.Id,
UserId: user.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, err := ss.Channel().SaveMember(member1)
require.NoError(t, err)
_, err = ss.Channel().SaveMember(member2)
require.NoError(t, err)
draft1 := &model.Draft{
CreateAt: 00001,
UpdateAt: 00001,
DeleteAt: 0,
UserId: user.Id,
ChannelId: channel.Id,
Message: "draft1",
}
draft2 := &model.Draft{
CreateAt: 00005,
UpdateAt: 00005,
DeleteAt: 0,
UserId: user.Id,
ChannelId: channel2.Id,
Message: "draft2",
}
_, err = ss.Draft().Save(draft1)
require.NoError(t, err)
_, err = ss.Draft().Save(draft2)
require.NoError(t, err)
t.Run("get drafts", func(t *testing.T) {
draftResp, err := ss.Draft().GetDraftsForUser(user.Id, "")
assert.NoError(t, err)
assert.Equal(t, draft2.Message, draftResp[0].Message)
assert.Equal(t, draft2.ChannelId, draftResp[0].ChannelId)
assert.Equal(t, draft1.Message, draftResp[1].Message)
assert.Equal(t, draft1.ChannelId, draftResp[1].ChannelId)
})
})
}

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

@@ -108,6 +108,7 @@ type SqlStoreStores struct {
UserTermsOfService store.UserTermsOfServiceStore
linkMetadata store.LinkMetadataStore
sharedchannel store.SharedChannelStore
draft store.DraftStore
notifyAdmin store.NotifyAdminStore
postPriority store.PostPriorityStore
postAcknowledgement store.PostAcknowledgementStore
@@ -215,6 +216,7 @@ func New(settings model.SqlSettings, metrics einterfaces.MetricsInterface) *SqlS
store.stores.scheme = newSqlSchemeStore(store)
store.stores.group = newSqlGroupStore(store)
store.stores.productNotices = newSqlProductNoticesStore(store)
store.stores.draft = newSqlDraftStore(store, metrics)
store.stores.notifyAdmin = newSqlNotifyAdminStore(store)
store.stores.postPriority = newSqlPostPriorityStore(store)
store.stores.postAcknowledgement = newSqlPostAcknowledgementStore(store)
@@ -963,6 +965,10 @@ func (ss *SqlStore) PostPriority() store.PostPriorityStore {
return ss.stores.postPriority
}
func (ss *SqlStore) Draft() store.DraftStore {
return ss.stores.draft
}
func (ss *SqlStore) PostAcknowledgement() store.PostAcknowledgementStore {
return ss.stores.postAcknowledgement
}

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

@@ -59,6 +59,7 @@ type Store interface {
UserTermsOfService() UserTermsOfServiceStore
LinkMetadata() LinkMetadataStore
SharedChannel() SharedChannelStore
Draft() DraftStore
MarkSystemRanUnitTests()
Close()
LockToMaster()
@@ -979,6 +980,14 @@ type PostPriorityStore interface {
GetForPosts(ids []string) ([]*model.PostPriority, error)
}
type DraftStore interface {
Save(d *model.Draft) (*model.Draft, error)
Get(userID, channelID, rootID string) (*model.Draft, error)
Delete(userID, channelID, rootID string) error
GetDraftsForUser(userID, teamID string) ([]*model.Draft, error)
Update(d *model.Draft) (*model.Draft, error)
}
type PostAcknowledgementStore interface {
Get(postID, userID string) (*model.PostAcknowledgement, error)
GetForPost(postID string) ([]*model.PostAcknowledgement, error)

13
store/storetest/draft_store.go Обычный файл
Просмотреть файл

@@ -0,0 +1,13 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"testing"
"github.com/mattermost/mattermost-server/v6/store"
)
func TestDraftStore(t *testing.T, ss store.Store, s SqlStore) {
}

121
store/storetest/mocks/DraftStore.go Обычный файл
Просмотреть файл

@@ -0,0 +1,121 @@
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// DraftStore is an autogenerated mock type for the DraftStore type
type DraftStore struct {
mock.Mock
}
// Delete provides a mock function with given fields: userID, channelID, rootID
func (_m *DraftStore) Delete(userID string, channelID string, rootID string) error {
ret := _m.Called(userID, channelID, rootID)
var r0 error
if rf, ok := ret.Get(0).(func(string, string, string) error); ok {
r0 = rf(userID, channelID, rootID)
} else {
r0 = ret.Error(0)
}
return r0
}
// Get provides a mock function with given fields: userID, channelID, rootID
func (_m *DraftStore) Get(userID string, channelID string, rootID string) (*model.Draft, error) {
ret := _m.Called(userID, channelID, rootID)
var r0 *model.Draft
if rf, ok := ret.Get(0).(func(string, string, string) *model.Draft); ok {
r0 = rf(userID, channelID, rootID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Draft)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, string) error); ok {
r1 = rf(userID, channelID, rootID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetDraftsForUser provides a mock function with given fields: userID, teamID
func (_m *DraftStore) GetDraftsForUser(userID string, teamID string) ([]*model.Draft, error) {
ret := _m.Called(userID, teamID)
var r0 []*model.Draft
if rf, ok := ret.Get(0).(func(string, string) []*model.Draft); ok {
r0 = rf(userID, teamID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Draft)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(userID, teamID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Save provides a mock function with given fields: d
func (_m *DraftStore) Save(d *model.Draft) (*model.Draft, error) {
ret := _m.Called(d)
var r0 *model.Draft
if rf, ok := ret.Get(0).(func(*model.Draft) *model.Draft); ok {
r0 = rf(d)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Draft)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.Draft) error); ok {
r1 = rf(d)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Update provides a mock function with given fields: d
func (_m *DraftStore) Update(d *model.Draft) (*model.Draft, error) {
ret := _m.Called(d)
var r0 *model.Draft
if rf, ok := ret.Get(0).(func(*model.Draft) *model.Draft); ok {
r0 = rf(d)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Draft)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.Draft) error); ok {
r1 = rf(d)
} else {
r1 = ret.Error(1)
}
return r0, r1
}

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

@@ -187,6 +187,22 @@ func (_m *Store) Context() context.Context {
return r0
}
// Draft provides a mock function with given fields:
func (_m *Store) Draft() store.DraftStore {
ret := _m.Called()
var r0 store.DraftStore
if rf, ok := ret.Get(0).(func() store.DraftStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.DraftStore)
}
}
return r0
}
// DropAllTables provides a mock function with given fields:
func (_m *Store) DropAllTables() {
_m.Called()

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

@@ -54,6 +54,7 @@ type Store struct {
LinkMetadataStore mocks.LinkMetadataStore
SharedChannelStore mocks.SharedChannelStore
ProductNoticesStore mocks.ProductNoticesStore
DraftStore mocks.DraftStore
context context.Context
NotifyAdminStore mocks.NotifyAdminStore
PostPriorityStore mocks.PostPriorityStore
@@ -95,6 +96,7 @@ func (s *Store) Role() store.RoleStore { return &s.R
func (s *Store) Scheme() store.SchemeStore { return &s.SchemeStore }
func (s *Store) TermsOfService() store.TermsOfServiceStore { return &s.TermsOfServiceStore }
func (s *Store) UserTermsOfService() store.UserTermsOfServiceStore { return &s.UserTermsOfServiceStore }
func (s *Store) Draft() store.DraftStore { return &s.DraftStore }
func (s *Store) ChannelMemberHistory() store.ChannelMemberHistoryStore {
return &s.ChannelMemberHistoryStore
}
@@ -163,6 +165,7 @@ func (s *Store) AssertExpectations(t mock.TestingT) bool {
&s.ThreadStore,
&s.ProductNoticesStore,
&s.SharedChannelStore,
&s.DraftStore,
&s.NotifyAdminStore,
&s.PostPriorityStore,
&s.PostAcknowledgementStore,

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

@@ -26,6 +26,7 @@ type TimerLayer struct {
CommandStore store.CommandStore
CommandWebhookStore store.CommandWebhookStore
ComplianceStore store.ComplianceStore
DraftStore store.DraftStore
EmojiStore store.EmojiStore
FileInfoStore store.FileInfoStore
GroupStore store.GroupStore
@@ -92,6 +93,10 @@ func (s *TimerLayer) Compliance() store.ComplianceStore {
return s.ComplianceStore
}
func (s *TimerLayer) Draft() store.DraftStore {
return s.DraftStore
}
func (s *TimerLayer) Emoji() store.EmojiStore {
return s.EmojiStore
}
@@ -260,6 +265,11 @@ type TimerLayerComplianceStore struct {
Root *TimerLayer
}
type TimerLayerDraftStore struct {
store.DraftStore
Root *TimerLayer
}
type TimerLayerEmojiStore struct {
store.EmojiStore
Root *TimerLayer
@@ -2955,6 +2965,86 @@ func (s *TimerLayerComplianceStore) Update(compliance *model.Compliance) (*model
return result, err
}
func (s *TimerLayerDraftStore) Delete(userID string, channelID string, rootID string) error {
start := time.Now()
err := s.DraftStore.Delete(userID, channelID, rootID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("DraftStore.Delete", success, elapsed)
}
return err
}
func (s *TimerLayerDraftStore) Get(userID string, channelID string, rootID string) (*model.Draft, error) {
start := time.Now()
result, err := s.DraftStore.Get(userID, channelID, rootID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("DraftStore.Get", success, elapsed)
}
return result, err
}
func (s *TimerLayerDraftStore) GetDraftsForUser(userID string, teamID string) ([]*model.Draft, error) {
start := time.Now()
result, err := s.DraftStore.GetDraftsForUser(userID, teamID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("DraftStore.GetDraftsForUser", success, elapsed)
}
return result, err
}
func (s *TimerLayerDraftStore) Save(d *model.Draft) (*model.Draft, error) {
start := time.Now()
result, err := s.DraftStore.Save(d)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("DraftStore.Save", success, elapsed)
}
return result, err
}
func (s *TimerLayerDraftStore) Update(d *model.Draft) (*model.Draft, error) {
start := time.Now()
result, err := s.DraftStore.Update(d)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("DraftStore.Update", success, elapsed)
}
return result, err
}
func (s *TimerLayerEmojiStore) Delete(emoji *model.Emoji, timestamp int64) error {
start := time.Now()
@@ -11424,6 +11514,7 @@ func New(childStore store.Store, metrics einterfaces.MetricsInterface) *TimerLay
newStore.CommandStore = &TimerLayerCommandStore{CommandStore: childStore.Command(), Root: &newStore}
newStore.CommandWebhookStore = &TimerLayerCommandWebhookStore{CommandWebhookStore: childStore.CommandWebhook(), Root: &newStore}
newStore.ComplianceStore = &TimerLayerComplianceStore{ComplianceStore: childStore.Compliance(), Root: &newStore}
newStore.DraftStore = &TimerLayerDraftStore{DraftStore: childStore.Draft(), Root: &newStore}
newStore.EmojiStore = &TimerLayerEmojiStore{EmojiStore: childStore.Emoji(), Root: &newStore}
newStore.FileInfoStore = &TimerLayerFileInfoStore{FileInfoStore: childStore.FileInfo(), Root: &newStore}
newStore.GroupStore = &TimerLayerGroupStore{GroupStore: childStore.Group(), Root: &newStore}