* create ChannelBookmarks table

* ChannelBookmark model

* channel bookamrks Store layer

* add GetBookmarksForAllChannelByIdSince

* add channel bookmarks to test store

* Add channel bookmarks to app layer

* remove index for createAt in channel bookmarks migrations

* remove createAt from select channel bookmark query and enable store delete bookmark test

* update reponse of UpdateBookmark

* rename db migration files

* channel bookmarks store update sort order

* channel bookmarks app layer update sort order

* fix lint & tests

* Fix lint and introduce util functions to insert / remove from slice

* remove model etag

* i18n

* defer remove file info after test run

* Fix tests passing the request context

* fix migrations

* fix TestRetry

* Add bookmark permissions (#25560)

* Adds channel bookmarks permissions

* Fix linter

* Remove unnecessary empty lines

* Remove scss change as it's not necessary anymore

* Fix mock store

* Fix mock store and add role entry

* Fix test

* Adds cypress test and update permissions migration to update admin roles

* Adds channel bookmarks roles to default admin roles

* Adds bookmark permissions to default role permissions constant in webapp

* Update mmctl test

* Update permission test after normalising the roles

* fix store tests

* fix app layer tests

* Add new bookmark endpoint (#25624)

* Adds channel bookmarks api scaffold and create endpoint

* Applies review comments to the API docs

* Adds websocket test to create channel bookmark

---------

Co-authored-by: Mattermost Build <build@mattermost.com>

* MM-54426 exclude Channel Bookmarks files from data retention (#25656)

* Augment channel APIs to include bookmarks (#25567)

* update files docs for server 9.4

* Adds update channel bookmark endpoint (#25653)

* Adds update channel bookmark sort order endpoint (#25686)

* Adds update channel bookmark endpoint

* Updates edit app method to return the right deleted bookmark and adds tests

* Adds the update channel bookmark sort order endpoint

* Fix repeated test after merge

* Assign right permissions to each test

* Update store and app layer to return specific errors and add tests

* Adds delete channel bookmark endpoint (#25693)

* Updates edit app method to return the right deleted bookmark and adds tests

* Fix repeated test after merge

* Updates edit app method to return the right deleted bookmark and adds tests

* Adds delete channel bookmark endpoint

* Adds list channel bookmarks endpoint (#25700)

* Add channel moderation to bookmarks (#25716)

* fix migrations index

* fix getChannelsForTeamForUser

* fix getChannelsForTeamForUser

* fix bad merge client4

* fix file api with bookmark permission

* add ChannelBookmarks feature flag

* add missing translations

* Set DB column for type as enum

* use custom type for bookmark query using sqlx

* use transaction when saving bookmark

* return NewErrNotFound instead of Sql.ErrNoRows

* use squirrel for IN query

* add a limit of 1K for records in GetBookmarksForAllChannelByIdSince

* UpdateSortOrder with one single query instead of multiple updates

* fix shadow declaration

* fix channel bookmarks permission string definition in admin console

* fix another shadow declaration

* Fix model conversion

* add SplitSliceInChunks

* remove include bookmarks in channels api

* Cap amount of bookmarks per channel

* add etag back to get channels

* feedback review

* update file info when replacing a bookmark file

* return 501 not implemented when the license is not available

* add detail message when getting channel member on bookmark api

* start audit before permission check on create bookmark api

* use require.Eventuallyf for testing WS events

* remove unnecessary log in app layer

* use require instead of assert to avoid panics

* enforce limit when querying bookmarks since

* prevent to create/update bookmark if file is already attached

* fix lint

* delete file when a bookmark is deleted

* Dot allow to set a fileId and a url at the same time to a bookmark

* fix query to delete a file that belongs to a bookmark

* do not patch the bookmark type

* Server side FeatureFlag check (#26145)

* use ff in server, set ff to false

* turn on FF for unit tests

* defer unset FF for unit tests

* turn ff on for testing

* only allow attaching files that were uploaded for bookmark

* Set feature flag off as default

* fix lint

* update email templates as PR failed

* revert templates

* force the assignment of ID when creating a bookmark

* Fix unit tests

---------

Co-authored-by: Miguel de la Cruz <miguel@mcrx.me>
Co-authored-by: Mattermost Build <build@mattermost.com>
Co-authored-by: Caleb Roseland <caleb@calebroseland.com>
Co-authored-by: Scott Bishel <scott.bishel@mattermost.com>
Этот коммит содержится в:
Elias Nahum
2024-03-12 22:36:05 +08:00
коммит произвёл GitHub
родитель 2480a6c646
Коммит 7e9cd04a8b
66 изменённых файлов: 6578 добавлений и 57 удалений

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

@@ -0,0 +1,380 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"strconv"
sq "github.com/mattermost/squirrel"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/v8/channels/store"
"github.com/mattermost/mattermost/server/v8/channels/utils"
"github.com/pkg/errors"
)
type SqlChannelBookmarkStore struct {
*SqlStore
}
func newSqlChannelBookmarkStore(sqlStore *SqlStore) store.ChannelBookmarkStore {
return &SqlChannelBookmarkStore{sqlStore}
}
func bookmarkWithFileInfoSliceColumns() []string {
return []string{
"cb.Id",
"cb.OwnerId",
"cb.ChannelId",
"cb.FileInfoId",
"cb.CreateAt",
"cb.UpdateAt",
"cb.DeleteAt",
"cb.DisplayName",
"cb.SortOrder",
"cb.LinkUrl",
"cb.ImageUrl",
"cb.Emoji",
"cb.Type",
"COALESCE(cb.OriginalId, '') as OriginalId",
"COALESCE(fi.Id, '') as FileId",
"COALESCE(fi.Name, '') as FileName",
"COALESCE(fi.Extension, '') as Extension",
"COALESCE(fi.Size, 0) as Size",
"COALESCE(fi.MimeType, '') as MimeType",
"COALESCE(fi.Width, 0) as Width",
"COALESCE(fi.Height, 0) as Height",
"COALESCE(fi.HasPreviewImage, false) as HasPreviewImage",
"COALESCE(fi.MiniPreview, '') as MiniPreview",
}
}
func (s *SqlChannelBookmarkStore) ErrorIfBookmarkFileInfoAlreadyAttached(fileId string) error {
existingQuery := s.getSubQueryBuilder().
Select("FileInfoId").
From("ChannelBookmarks").
Where(sq.And{
sq.Eq{"FileInfoId": fileId},
sq.Eq{"DeleteAt": 0},
})
alreadyAttachedQuery := s.getQueryBuilder().
Select("COUNT(*)").
From("FileInfo").
Where(sq.Or{
sq.Expr("Id IN (?)", existingQuery),
sq.And{
sq.Or{
sq.NotEq{"PostId": ""},
sq.NotEq{"CreatorId": model.BookmarkFileOwner},
},
sq.Eq{"Id": fileId},
},
})
var attached int64
err := s.GetReplicaX().GetBuilder(&attached, alreadyAttachedQuery)
if err != nil {
return errors.Wrap(err, "unable_to_save_channel_bookmark")
}
if attached > 0 {
return store.NewErrInvalidInput("ChannelBookmarks", "FileInfoId", fileId)
}
return nil
}
func (s *SqlChannelBookmarkStore) Get(Id string, includeDeleted bool) (*model.ChannelBookmarkWithFileInfo, error) {
query := s.getQueryBuilder().
Select(bookmarkWithFileInfoSliceColumns()...).
From("ChannelBookmarks cb").
LeftJoin("FileInfo fi ON cb.FileInfoId = fi.Id").
Where(sq.Eq{"cb.Id": Id})
if !includeDeleted {
query = query.Where(sq.Eq{"cb.DeleteAt": 0})
}
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "channel_bookmark_getforchanneltsince_tosql")
}
bookmark := model.ChannelBookmarkAndFileInfo{}
if err := s.GetReplicaX().Get(&bookmark, queryString, args...); err != nil {
return nil, store.NewErrNotFound("ChannelBookmark", Id)
}
return bookmark.ToChannelBookmarkWithFileInfo(), nil
}
func (s *SqlChannelBookmarkStore) Save(bookmark *model.ChannelBookmark, increaseSortOrder bool) (b *model.ChannelBookmarkWithFileInfo, err error) {
bookmark.PreSave()
if err := bookmark.IsValid(); err != nil {
return nil, err
}
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return nil, err
}
defer finalizeTransactionX(transaction, &err)
var currentBookmarksCount int64
query := s.getQueryBuilder().
Select("COUNT(*) as count").
From("ChannelBookmarks").
Where(sq.Eq{"ChannelId": bookmark.ChannelId, "DeleteAt": 0})
err = transaction.GetBuilder(&currentBookmarksCount, query)
if err != nil {
return nil, errors.Wrap(err, "failed while getting the count of ChannelBookmarks")
}
if currentBookmarksCount >= model.MaxBookmarksPerChannel {
return nil, store.NewErrLimitExceeded("bookmarks_per_channel", int(currentBookmarksCount), "channelId="+bookmark.ChannelId)
}
if bookmark.FileId != "" {
err = s.ErrorIfBookmarkFileInfoAlreadyAttached(bookmark.FileId)
if err != nil {
return nil, errors.Wrap(err, "unable_to_save_channel_bookmark")
}
}
if increaseSortOrder {
var sortOrder int64
query := s.getQueryBuilder().
Select("COALESCE(MAX(SortOrder), -1) as SortOrder").
From("ChannelBookmarks").
Where(sq.Eq{"ChannelId": bookmark.ChannelId, "DeleteAt": 0})
err = transaction.GetBuilder(&sortOrder, query)
if err != nil {
return nil, errors.Wrap(err, "failed while getting the sortOrder from ChannelBookmarks")
}
bookmark.SortOrder = sortOrder + 1
}
sql, args, sqlErr := s.getQueryBuilder().
Insert("ChannelBookmarks").
Columns("Id", "CreateAt", "UpdateAt", "DeleteAt", "ChannelId", "OwnerId", "FileInfoId", "DisplayName", "SortOrder", "LinkUrl", "ImageUrl", "Emoji", "Type").
Values(bookmark.Id, bookmark.CreateAt, bookmark.UpdateAt, bookmark.DeleteAt, bookmark.ChannelId, bookmark.OwnerId, bookmark.FileId, bookmark.DisplayName, bookmark.SortOrder, bookmark.LinkUrl, bookmark.ImageUrl, bookmark.Emoji, bookmark.Type).
ToSql()
if sqlErr != nil {
return nil, errors.Wrap(err, "insert_channel_bookmark_to_sql")
}
if _, insertErr := transaction.Exec(sql, args...); insertErr != nil {
return nil, errors.Wrap(insertErr, "unable_to_save_channel_bookmark")
}
var fileInfo model.FileInfo
if bookmark.FileId != "" {
query, args, queryErr := s.getQueryBuilder().
Select("Id, Name, Extension, Size, MimeType, Width, Height, HasPreviewImage, MiniPreview").
From("FileInfo").
Where(sq.Eq{"Id": bookmark.FileId}).
ToSql()
if queryErr != nil {
return nil, errors.Wrap(queryErr, "channel_bookmark_get_file_info_to_sql")
}
if queryErr = transaction.Get(&fileInfo, query, args...); queryErr != nil {
return nil, errors.Wrap(queryErr, "unable_to_get_channel_bookmark_file_info")
}
}
err = transaction.Commit()
return bookmark.ToBookmarkWithFileInfo(&fileInfo), err
}
func (s *SqlChannelBookmarkStore) Update(bookmark *model.ChannelBookmark) error {
bookmark.PreUpdate()
if err := bookmark.IsValid(); err != nil {
return err
}
query, args, err := s.getQueryBuilder().
Update("ChannelBookmarks").
Set("DisplayName", bookmark.DisplayName).
Set("SortOrder", bookmark.SortOrder).
Set("LinkUrl", bookmark.LinkUrl).
Set("ImageUrl", bookmark.ImageUrl).
Set("Emoji", bookmark.Emoji).
Set("FileInfoId", bookmark.FileId).
Set("UpdateAt", bookmark.UpdateAt).
Where(sq.Eq{
"Id": bookmark.Id,
"DeleteAt": 0,
}).
ToSql()
if err != nil {
return errors.Wrap(err, "channel_bookmark_update_tosql")
}
res, err := s.GetMasterX().Exec(query, args...)
if err != nil {
return errors.Wrapf(err, "failed to update channel bookmark with id=%s", bookmark.Id)
}
rowsAffected, err := res.RowsAffected()
if err != nil {
return errors.Wrapf(err, "failed to get affected rows after updating bookmark with id=%s", bookmark.Id)
}
if rowsAffected == 0 {
return store.NewErrNotFound("ChannelBookmark", bookmark.Id)
}
return nil
}
func (s *SqlChannelBookmarkStore) UpdateSortOrder(bookmarkId, channelId string, newIndex int64) ([]*model.ChannelBookmarkWithFileInfo, error) {
now := model.GetMillis()
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return nil, err
}
defer finalizeTransactionX(transaction, &err)
bookmarks, err := s.GetBookmarksForChannelSince(channelId, 0)
if err != nil {
return nil, err
}
if (int(newIndex) > len(bookmarks)-1) || newIndex < 0 {
return nil, store.NewErrInvalidInput("ChannelBookmark", "SortOrder", newIndex)
}
currentIndex := -1
var current *model.ChannelBookmarkWithFileInfo
for index, b := range bookmarks {
if b.Id == bookmarkId {
currentIndex = index
current = b
break
}
}
if currentIndex == -1 {
return nil, store.NewErrNotFound("ChannelBookmark", bookmarkId)
}
bookmarks = utils.RemoveElementFromSliceAtIndex(bookmarks, currentIndex)
bookmarks = utils.InsertElementToSliceAtIndex(bookmarks, current, int(newIndex))
caseStmt := sq.Case()
query := s.getQueryBuilder().
Update("ChannelBookmarks")
ids := []string{}
for index, b := range bookmarks {
b.SortOrder = int64(index)
caseStmt = caseStmt.When(sq.Eq{"Id": b.Id}, strconv.FormatInt(int64(index), 10))
ids = append(ids, b.Id)
}
query = query.Set("SortOrder", caseStmt)
query = query.Set("UpdateAt", now)
query = query.Where(sq.Eq{"Id": ids})
queryStr, args, queryErr := query.ToSql()
if queryErr != nil {
return nil, queryErr
}
if _, updateSortOrderErr := transaction.Exec(queryStr, args...); updateSortOrderErr != nil {
return nil, updateSortOrderErr
}
err = transaction.Commit()
return bookmarks, err
}
func (s *SqlChannelBookmarkStore) Delete(bookmarkId string, deleteFile bool) error {
now := model.GetMillis()
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return err
}
defer finalizeTransactionX(transaction, &err)
query, args, err := s.getQueryBuilder().
Update("ChannelBookmarks").
Set("DeleteAt", now).
Set("UpdateAt", now).
Where(sq.Eq{"Id": bookmarkId}).
ToSql()
if err != nil {
return errors.Wrap(err, "channel_bookmark_delete_tosql")
}
_, err = transaction.Exec(query, args...)
if err != nil {
return errors.Wrapf(err, "failed to delete channel bookmark with id=%s", bookmarkId)
}
if deleteFile {
fileIdQuery := s.getSubQueryBuilder().
Select("FileInfoId").
From("ChannelBookmarks").
Where(sq.And{
sq.Eq{"Id": bookmarkId},
sq.Eq{"DeleteAt": 0},
})
fileQuery, fileArgs, fileErr := s.getQueryBuilder().
Update("FileInfo").
Set("DeleteAt", now).
Set("UpdateAt", now).
Where(sq.Expr("Id IN (?)", fileIdQuery)).
ToSql()
if fileErr != nil {
return errors.Wrap(err, "channel_bookmark_delete_tosql")
}
_, err = transaction.Exec(fileQuery, fileArgs...)
if err != nil {
return errors.Wrapf(err, "failed to delete channel bookmark with id=%s", bookmarkId)
}
}
return transaction.Commit()
}
func (s *SqlChannelBookmarkStore) GetBookmarksForChannelSince(channelId string, since int64) ([]*model.ChannelBookmarkWithFileInfo, error) {
query := s.getQueryBuilder().
Select(bookmarkWithFileInfoSliceColumns()...).
From("ChannelBookmarks cb").
LeftJoin("FileInfo fi ON cb.FileInfoId = fi.Id").
Where(sq.Eq{"cb.ChannelId": channelId})
if since > 0 {
query = query.Where(sq.Or{
sq.GtOrEq{"cb.UpdateAt": since},
sq.GtOrEq{"cb.DeleteAt": since},
})
} else {
query = query.Where(sq.Eq{"cb.DeleteAt": 0})
}
query = query.
OrderBy("cb.SortOrder ASC").
OrderBy("cb.DeleteAt ASC").
Limit(model.MaxBookmarksPerChannel * 2) // limit to the double of the cap as an edge case
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "channel_bookmark_getforchanneltsince_tosql")
}
bookmarkRows := []model.ChannelBookmarkAndFileInfo{}
bookmarks := []*model.ChannelBookmarkWithFileInfo{}
if err := s.GetReplicaX().Select(&bookmarkRows, queryString, args...); err != nil {
return nil, errors.Wrapf(err, "failed to find bookmarks")
}
for _, bookmark := range bookmarkRows {
bookmarks = append(bookmarks, bookmark.ToChannelBookmarkWithFileInfo())
}
return bookmarks, nil
}

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

@@ -0,0 +1,14 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"testing"
"github.com/mattermost/mattermost/server/v8/channels/store/storetest"
)
func TestChannelBookmarkStore(t *testing.T) {
StoreTestWithSqlStore(t, storetest.TestChannelBookmarkStore)
}

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

@@ -465,12 +465,12 @@ func (fs SqlFileInfoStore) PermanentDelete(rctx request.CTX, fileId string) erro
func (fs SqlFileInfoStore) PermanentDeleteBatch(rctx request.CTX, endTime int64, limit int64) (int64, error) {
var query string
if fs.DriverName() == "postgres" {
query = "DELETE from FileInfo WHERE Id = any (array (SELECT Id FROM FileInfo WHERE CreateAt < ? LIMIT ?))"
query = "DELETE from FileInfo WHERE Id = any (array (SELECT Id FROM FileInfo WHERE CreateAt < ? AND CreatorId != ? LIMIT ?))"
} else {
query = "DELETE from FileInfo WHERE CreateAt < ? LIMIT ?"
query = "DELETE from FileInfo WHERE CreateAt < ? AND CreatorId != ? LIMIT ?"
}
sqlResult, err := fs.GetMasterX().Exec(query, endTime, limit)
sqlResult, err := fs.GetMasterX().Exec(query, endTime, model.BookmarkFileOwner, limit)
if err != nil {
return 0, errors.Wrap(err, "failed to delete FileInfos in batch")
}

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

@@ -111,6 +111,7 @@ type SqlStoreStores struct {
postPersistentNotification store.PostPersistentNotificationStore
trueUpReview store.TrueUpReviewStore
desktopTokens store.DesktopTokensStore
channelBookmarks store.ChannelBookmarkStore
}
type SqlStore struct {
@@ -236,6 +237,7 @@ func New(settings model.SqlSettings, logger mlog.LoggerIFace, metrics einterface
store.stores.postPersistentNotification = newSqlPostPersistentNotificationStore(store)
store.stores.trueUpReview = newSqlTrueUpReviewStore(store)
store.stores.desktopTokens = newSqlDesktopTokensStore(store, metrics)
store.stores.channelBookmarks = newSqlChannelBookmarkStore(store)
store.stores.preference.(*SqlPreferenceStore).deleteUnusedFeatures()
@@ -1043,6 +1045,10 @@ func (ss *SqlStore) DesktopTokens() store.DesktopTokensStore {
return ss.stores.desktopTokens
}
func (ss *SqlStore) ChannelBookmark() store.ChannelBookmarkStore {
return ss.stores.channelBookmarks
}
func (ss *SqlStore) DropAllTables() {
if ss.DriverName() == model.DatabaseDriverPostgres {
ss.masterX.Exec(`DO