mirror of
https://github.com/mattermost/focalboard.git
synced 2024-12-24 13:43:12 +02:00
Delete children when deleting boards and cards (#3943)
* delete and undelete handle children
This commit is contained in:
parent
cf97e300c1
commit
8949c6b13f
@ -3,7 +3,7 @@ module github.com/mattermost/focalboard/server
|
||||
go 1.18
|
||||
|
||||
require (
|
||||
github.com/Masterminds/squirrel v1.5.2
|
||||
github.com/Masterminds/squirrel v1.5.3
|
||||
github.com/golang/mock v1.6.0
|
||||
github.com/gorilla/mux v1.8.0
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
|
@ -87,6 +87,8 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym
|
||||
github.com/ClickHouse/clickhouse-go v1.4.3/go.mod h1:EaI/sW7Azgz9UATzd5ZdZHRUhHgv5+JMS9NSr2smCJI=
|
||||
github.com/Masterminds/squirrel v1.5.2 h1:UiOEi2ZX4RCSkpiNDQN5kro/XIBpSRk9iTqdIRPzUXE=
|
||||
github.com/Masterminds/squirrel v1.5.2/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10=
|
||||
github.com/Masterminds/squirrel v1.5.3 h1:YPpoceAcxuzIljlr5iWpNKaql7hLeG1KLSrhvdHpkZc=
|
||||
github.com/Masterminds/squirrel v1.5.3/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10=
|
||||
github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
|
||||
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
|
||||
github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw=
|
||||
|
@ -196,6 +196,20 @@ func (mr *MockStoreMockRecorder) DeleteBlock(arg0, arg1 interface{}) *gomock.Cal
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteBlock", reflect.TypeOf((*MockStore)(nil).DeleteBlock), arg0, arg1)
|
||||
}
|
||||
|
||||
// DeleteBlockRecord mocks base method.
|
||||
func (m *MockStore) DeleteBlockRecord(arg0, arg1 string) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "DeleteBlockRecord", arg0, arg1)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// DeleteBlockRecord indicates an expected call of DeleteBlockRecord.
|
||||
func (mr *MockStoreMockRecorder) DeleteBlockRecord(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteBlockRecord", reflect.TypeOf((*MockStore)(nil).DeleteBlockRecord), arg0, arg1)
|
||||
}
|
||||
|
||||
// DeleteBoard mocks base method.
|
||||
func (m *MockStore) DeleteBoard(arg0, arg1 string) error {
|
||||
m.ctrl.T.Helper()
|
||||
@ -210,6 +224,20 @@ func (mr *MockStoreMockRecorder) DeleteBoard(arg0, arg1 interface{}) *gomock.Cal
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteBoard", reflect.TypeOf((*MockStore)(nil).DeleteBoard), arg0, arg1)
|
||||
}
|
||||
|
||||
// DeleteBoardRecord mocks base method.
|
||||
func (m *MockStore) DeleteBoardRecord(arg0, arg1 string) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "DeleteBoardRecord", arg0, arg1)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// DeleteBoardRecord indicates an expected call of DeleteBoardRecord.
|
||||
func (mr *MockStoreMockRecorder) DeleteBoardRecord(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteBoardRecord", reflect.TypeOf((*MockStore)(nil).DeleteBoardRecord), arg0, arg1)
|
||||
}
|
||||
|
||||
// DeleteBoardsAndBlocks mocks base method.
|
||||
func (m *MockStore) DeleteBoardsAndBlocks(arg0 *model.DeleteBoardsAndBlocks, arg1 string) error {
|
||||
m.ctrl.T.Helper()
|
||||
|
@ -20,10 +20,16 @@ const (
|
||||
descClause = " DESC "
|
||||
)
|
||||
|
||||
type BoardIDNilError struct{}
|
||||
type ErrEmptyBoardID struct{}
|
||||
|
||||
func (re BoardIDNilError) Error() string {
|
||||
return "boardID is nil"
|
||||
func (re ErrEmptyBoardID) Error() string {
|
||||
return "boardID is empty"
|
||||
}
|
||||
|
||||
type ErrLimitExceeded struct{ max int }
|
||||
|
||||
func (le ErrLimitExceeded) Error() string {
|
||||
return fmt.Sprintf("limit exceeded (max=%d)", le.max)
|
||||
}
|
||||
|
||||
func (s *SQLStore) timestampToCharField(name string, as string) string {
|
||||
@ -231,7 +237,7 @@ func (s *SQLStore) blocksFromRows(rows *sql.Rows) ([]*model.Block, error) {
|
||||
|
||||
func (s *SQLStore) insertBlock(db sq.BaseRunner, block *model.Block, userID string) error {
|
||||
if block.BoardID == "" {
|
||||
return BoardIDNilError{}
|
||||
return ErrEmptyBoardID{}
|
||||
}
|
||||
|
||||
fieldsJSON, err := json.Marshal(block.Fields)
|
||||
@ -339,7 +345,7 @@ func (s *SQLStore) patchBlocks(db sq.BaseRunner, blockPatches *model.BlockPatchB
|
||||
func (s *SQLStore) insertBlocks(db sq.BaseRunner, blocks []*model.Block, userID string) error {
|
||||
for _, block := range blocks {
|
||||
if block.BoardID == "" {
|
||||
return BoardIDNilError{}
|
||||
return ErrEmptyBoardID{}
|
||||
}
|
||||
}
|
||||
for i := range blocks {
|
||||
@ -352,6 +358,10 @@ func (s *SQLStore) insertBlocks(db sq.BaseRunner, blocks []*model.Block, userID
|
||||
}
|
||||
|
||||
func (s *SQLStore) deleteBlock(db sq.BaseRunner, blockID string, modifiedBy string) error {
|
||||
return s.deleteBlockAndChildren(db, blockID, modifiedBy, false)
|
||||
}
|
||||
|
||||
func (s *SQLStore) deleteBlockAndChildren(db sq.BaseRunner, blockID string, modifiedBy string, keepChildren bool) error {
|
||||
block, err := s.getBlock(db, blockID)
|
||||
if model.IsErrNotFound(err) {
|
||||
s.logger.Warn("deleteBlock block not found", mlog.String("block_id", blockID))
|
||||
@ -409,7 +419,11 @@ func (s *SQLStore) deleteBlock(db sq.BaseRunner, blockID string, modifiedBy stri
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
if keepChildren {
|
||||
return nil
|
||||
}
|
||||
|
||||
return s.deleteBlockChildren(db, block.BoardID, block.ID, modifiedBy)
|
||||
}
|
||||
|
||||
func (s *SQLStore) undeleteBlock(db sq.BaseRunner, blockID string, modifiedBy string) error {
|
||||
@ -481,7 +495,7 @@ func (s *SQLStore) undeleteBlock(db sq.BaseRunner, blockID string, modifiedBy st
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return s.undeleteBlockChildren(db, block.BoardID, block.ID, modifiedBy)
|
||||
}
|
||||
|
||||
func (s *SQLStore) getBlockCountsByType(db sq.BaseRunner) (map[string]int64, error) {
|
||||
@ -620,7 +634,7 @@ func (s *SQLStore) getBlockHistoryDescendants(db sq.BaseRunner, boardID string,
|
||||
|
||||
rows, err := query.Query()
|
||||
if err != nil {
|
||||
s.logger.Error(`GetBlockHistory ERROR`, mlog.Err(err))
|
||||
s.logger.Error(`GetBlockHistoryDescendants ERROR`, mlog.Err(err))
|
||||
return nil, err
|
||||
}
|
||||
defer s.CloseRows(rows)
|
||||
@ -789,3 +803,155 @@ func (s *SQLStore) duplicateBlock(db sq.BaseRunner, boardID string, blockID stri
|
||||
}
|
||||
return allBlocks, nil
|
||||
}
|
||||
|
||||
func (s *SQLStore) deleteBlockChildren(db sq.BaseRunner, boardID string, parentID string, modifiedBy string) error {
|
||||
now := utils.GetMillis()
|
||||
|
||||
selectQuery := s.getQueryBuilder(db).
|
||||
Select(
|
||||
"board_id",
|
||||
"id",
|
||||
"parent_id",
|
||||
s.escapeField("schema"),
|
||||
"type",
|
||||
"title",
|
||||
"fields",
|
||||
"'"+modifiedBy+"'",
|
||||
"create_at",
|
||||
s.castInt(now, "update_at"),
|
||||
s.castInt(now, "delete_at"),
|
||||
"created_by",
|
||||
).
|
||||
From(s.tablePrefix + "blocks").
|
||||
Where(sq.Eq{"board_id": boardID})
|
||||
|
||||
if parentID != "" {
|
||||
selectQuery = selectQuery.Where(sq.Eq{"parent_id": parentID})
|
||||
}
|
||||
|
||||
insertQuery := s.getQueryBuilder(db).
|
||||
Insert(s.tablePrefix+"blocks_history").
|
||||
Columns(
|
||||
"board_id",
|
||||
"id",
|
||||
"parent_id",
|
||||
s.escapeField("schema"),
|
||||
"type",
|
||||
"title",
|
||||
"fields",
|
||||
"modified_by",
|
||||
"create_at",
|
||||
"update_at",
|
||||
"delete_at",
|
||||
"created_by",
|
||||
).Select(selectQuery)
|
||||
|
||||
if _, err := insertQuery.Exec(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
deleteQuery := s.getQueryBuilder(db).
|
||||
Delete(s.tablePrefix + "blocks").
|
||||
Where(sq.Eq{"board_id": boardID})
|
||||
|
||||
if parentID != "" {
|
||||
deleteQuery = deleteQuery.Where(sq.Eq{"parent_id": parentID})
|
||||
}
|
||||
|
||||
if _, err := deleteQuery.Exec(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SQLStore) undeleteBlockChildren(db sq.BaseRunner, boardID string, parentID string, modifiedBy string) error {
|
||||
if boardID == "" {
|
||||
return ErrEmptyBoardID{}
|
||||
}
|
||||
|
||||
where := fmt.Sprintf("board_id='%s'", boardID)
|
||||
if parentID != "" {
|
||||
where += fmt.Sprintf(" AND parent_id='%s'", parentID)
|
||||
}
|
||||
|
||||
selectQuery := s.getQueryBuilder(db).
|
||||
Select(
|
||||
"bh.board_id",
|
||||
"'' AS channel_id",
|
||||
"bh.id",
|
||||
"bh.parent_id",
|
||||
"bh.schema",
|
||||
"bh.type",
|
||||
"bh.title",
|
||||
"bh.fields",
|
||||
"'"+modifiedBy+"' AS modified_by",
|
||||
"bh.create_at",
|
||||
s.castInt(utils.GetMillis(), "update_at"),
|
||||
s.castInt(0, "delete_at"),
|
||||
"bh.created_by",
|
||||
).
|
||||
From(fmt.Sprintf(`
|
||||
%sblocks_history AS bh,
|
||||
(SELECT id, max(insert_at) AS max_insert_at FROM %sblocks_history WHERE %s GROUP BY id) AS sub`,
|
||||
s.tablePrefix, s.tablePrefix, where)).
|
||||
Where("bh.id=sub.id").
|
||||
Where("bh.insert_at=sub.max_insert_at").
|
||||
Where(sq.NotEq{"bh.delete_at": 0})
|
||||
|
||||
columns := []string{
|
||||
"board_id",
|
||||
"channel_id",
|
||||
"id",
|
||||
"parent_id",
|
||||
s.escapeField("schema"),
|
||||
"type",
|
||||
"title",
|
||||
"fields",
|
||||
"modified_by",
|
||||
"create_at",
|
||||
"update_at",
|
||||
"delete_at",
|
||||
"created_by",
|
||||
}
|
||||
|
||||
insertQuery := s.getQueryBuilder(db).Insert(s.tablePrefix + "blocks").
|
||||
Columns(columns...).
|
||||
Select(selectQuery)
|
||||
|
||||
insertHistoryQuery := s.getQueryBuilder(db).Insert(s.tablePrefix + "blocks_history").
|
||||
Columns(columns...).
|
||||
Select(selectQuery)
|
||||
|
||||
sql, args, err := insertQuery.ToSql()
|
||||
s.logger.Trace("undeleteBlockChildren - insertQuery",
|
||||
mlog.String("sql", sql),
|
||||
mlog.Array("args", args),
|
||||
mlog.Err(err),
|
||||
)
|
||||
|
||||
sql, args, err = insertHistoryQuery.ToSql()
|
||||
s.logger.Trace("undeleteBlockChildren - insertHistoryQuery",
|
||||
mlog.String("sql", sql),
|
||||
mlog.Array("args", args),
|
||||
mlog.Err(err),
|
||||
)
|
||||
|
||||
// insert into blocks table must happen before history table, otherwise the history
|
||||
// table will be changed and the second query will fail to find the same records.
|
||||
result, err := insertQuery.Exec()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rowsAffected, _ := result.RowsAffected()
|
||||
s.logger.Debug("undeleteBlockChildren - insertQuery", mlog.Int64("rows_affected", rowsAffected))
|
||||
|
||||
result, err = insertHistoryQuery.Exec()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rowsAffected, _ = result.RowsAffected()
|
||||
s.logger.Debug("undeleteBlockChildren - insertHistoryQuery", mlog.Int64("rows_affected", rowsAffected))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -422,6 +422,10 @@ func (s *SQLStore) patchBoard(db sq.BaseRunner, boardID string, boardPatch *mode
|
||||
}
|
||||
|
||||
func (s *SQLStore) deleteBoard(db sq.BaseRunner, boardID, userID string) error {
|
||||
return s.deleteBoardAndChildren(db, boardID, userID, false)
|
||||
}
|
||||
|
||||
func (s *SQLStore) deleteBoardAndChildren(db sq.BaseRunner, boardID, userID string, keepChildren bool) error {
|
||||
now := utils.GetMillis()
|
||||
|
||||
board, err := s.getBoard(db, boardID)
|
||||
@ -477,7 +481,11 @@ func (s *SQLStore) deleteBoard(db sq.BaseRunner, boardID, userID string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
if keepChildren {
|
||||
return nil
|
||||
}
|
||||
|
||||
return s.deleteBlockChildren(db, boardID, "", userID)
|
||||
}
|
||||
|
||||
func (s *SQLStore) insertBoardWithAdmin(db sq.BaseRunner, board *model.Board, userID string) (*model.Board, *model.BoardMember, error) {
|
||||
@ -861,7 +869,7 @@ func (s *SQLStore) undeleteBoard(db sq.BaseRunner, boardID string, modifiedBy st
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return s.undeleteBlockChildren(db, board.ID, "", modifiedBy)
|
||||
}
|
||||
|
||||
func (s *SQLStore) getBoardMemberHistory(db sq.BaseRunner, boardID, userID string, limit uint64) ([]*model.BoardMemberHistoryEntry, error) {
|
||||
|
@ -102,13 +102,11 @@ func (s *SQLStore) patchBoardsAndBlocks(db sq.BaseRunner, pbab *model.PatchBoard
|
||||
func (s *SQLStore) deleteBoardsAndBlocks(db sq.BaseRunner, dbab *model.DeleteBoardsAndBlocks, userID string) error {
|
||||
boardIDMap := map[string]bool{}
|
||||
for _, boardID := range dbab.Boards {
|
||||
if err := s.deleteBoard(db, boardID, userID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
boardIDMap[boardID] = true
|
||||
}
|
||||
|
||||
// delete the blocks first, since deleting the board will clean up any children and we'll get
|
||||
// not found errors when deleting the blocks after.
|
||||
for _, blockID := range dbab.Blocks {
|
||||
block, err := s.getBlock(db, blockID)
|
||||
if err != nil {
|
||||
@ -124,6 +122,12 @@ func (s *SQLStore) deleteBoardsAndBlocks(db sq.BaseRunner, dbab *model.DeleteBoa
|
||||
}
|
||||
}
|
||||
|
||||
for _, boardID := range dbab.Boards {
|
||||
if err := s.deleteBoard(db, boardID, userID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -59,6 +59,7 @@ func legacyBoardFields(prefix string) []string {
|
||||
// legacyBlocksFromRows is the old getBlock version that still uses
|
||||
// the old block model. This method is kept to enable the unique IDs
|
||||
// data migration.
|
||||
//
|
||||
//nolint:unused
|
||||
func (s *SQLStore) legacyBlocksFromRows(rows *sql.Rows) ([]*model.Block, error) {
|
||||
results := []*model.Block{}
|
||||
@ -112,6 +113,7 @@ func (s *SQLStore) legacyBlocksFromRows(rows *sql.Rows) ([]*model.Block, error)
|
||||
// getLegacyBlock is the old getBlock version that still uses the old
|
||||
// block model. This method is kept to enable the unique IDs data
|
||||
// migration.
|
||||
//
|
||||
//nolint:unused
|
||||
func (s *SQLStore) getLegacyBlock(db sq.BaseRunner, workspaceID string, blockID string) (*model.Block, error) {
|
||||
query := s.getQueryBuilder(db).
|
||||
@ -156,10 +158,11 @@ func (s *SQLStore) getLegacyBlock(db sq.BaseRunner, workspaceID string, blockID
|
||||
// insertLegacyBlock is the old insertBlock version that still uses
|
||||
// the old block model. This method is kept to enable the unique IDs
|
||||
// data migration.
|
||||
//
|
||||
//nolint:unused
|
||||
func (s *SQLStore) insertLegacyBlock(db sq.BaseRunner, workspaceID string, block *model.Block, userID string) error {
|
||||
if block.BoardID == "" {
|
||||
return BoardIDNilError{}
|
||||
return ErrEmptyBoardID{}
|
||||
}
|
||||
|
||||
fieldsJSON, err := json.Marshal(block.Fields)
|
||||
|
@ -148,6 +148,11 @@ func (s *SQLStore) DeleteBlock(blockID string, modifiedBy string) error {
|
||||
|
||||
}
|
||||
|
||||
func (s *SQLStore) DeleteBlockRecord(blockID string, modifiedBy string) error {
|
||||
return s.deleteBlockRecord(s.db, blockID, modifiedBy)
|
||||
|
||||
}
|
||||
|
||||
func (s *SQLStore) DeleteBoard(boardID string, userID string) error {
|
||||
if s.dbType == model.SqliteDBType {
|
||||
return s.deleteBoard(s.db, boardID, userID)
|
||||
@ -172,6 +177,11 @@ func (s *SQLStore) DeleteBoard(boardID string, userID string) error {
|
||||
|
||||
}
|
||||
|
||||
func (s *SQLStore) DeleteBoardRecord(boardID string, modifiedBy string) error {
|
||||
return s.deleteBoardRecord(s.db, boardID, modifiedBy)
|
||||
|
||||
}
|
||||
|
||||
func (s *SQLStore) DeleteBoardsAndBlocks(dbab *model.DeleteBoardsAndBlocks, userID string) error {
|
||||
if s.dbType == model.SqliteDBType {
|
||||
return s.deleteBoardsAndBlocks(s.db, dbab, userID)
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
sq "github.com/Masterminds/squirrel"
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
"github.com/mattermost/focalboard/server/utils"
|
||||
|
||||
@ -118,3 +119,22 @@ func newErrInvalidDBType(dbType string) error {
|
||||
func (e ErrInvalidDBType) Error() string {
|
||||
return "unsupported database type: " + e.dbType
|
||||
}
|
||||
|
||||
// deleteBoardRecord deletes a boards record without deleting any child records in the blocks table.
|
||||
// FOR UNIT TESTING ONLY.
|
||||
func (s *SQLStore) deleteBoardRecord(db sq.BaseRunner, boardID string, modifiedBy string) error {
|
||||
return s.deleteBoardAndChildren(db, boardID, modifiedBy, true)
|
||||
}
|
||||
|
||||
// deleteBlockRecord deletes a blocks record without deleting any child records in the blocks table.
|
||||
// FOR UNIT TESTING ONLY.
|
||||
func (s *SQLStore) deleteBlockRecord(db sq.BaseRunner, blockID, modifiedBy string) error {
|
||||
return s.deleteBlockAndChildren(db, blockID, modifiedBy, true)
|
||||
}
|
||||
|
||||
func (s *SQLStore) castInt(val int64, as string) string {
|
||||
if s.dbType == model.MysqlDBType {
|
||||
return fmt.Sprintf("cast(%d as unsigned) AS %s", val, as)
|
||||
}
|
||||
return fmt.Sprintf("cast(%d as bigint) AS %s", val, as)
|
||||
}
|
||||
|
@ -165,6 +165,10 @@ type Store interface {
|
||||
GetTeamBoardsInsights(teamID string, userID string, since int64, offset int, limit int, boardIDs []string) (*model.BoardInsightsList, error)
|
||||
GetUserBoardsInsights(teamID string, userID string, since int64, offset int, limit int, boardIDs []string) (*model.BoardInsightsList, error)
|
||||
GetUserTimezone(userID string) (string, error)
|
||||
|
||||
// For unit testing only
|
||||
DeleteBoardRecord(boardID, modifiedBy string) error
|
||||
DeleteBlockRecord(blockID, modifiedBy string) error
|
||||
}
|
||||
|
||||
type NotSupportedError struct {
|
||||
|
@ -74,6 +74,11 @@ func StoreTestBlocksStore(t *testing.T, setup func(t *testing.T) (store.Store, f
|
||||
defer tearDown()
|
||||
testGetBlockMetadata(t, store)
|
||||
})
|
||||
t.Run("UndeleteBlockChildren", func(t *testing.T) {
|
||||
store, tearDown := setup(t)
|
||||
defer tearDown()
|
||||
testUndeleteBlockChildren(t, store)
|
||||
})
|
||||
}
|
||||
|
||||
func testInsertBlock(t *testing.T, store store.Store) {
|
||||
@ -1021,6 +1026,7 @@ func testGetBlockMetadata(t *testing.T, store store.Store) {
|
||||
|
||||
t.Run("get full block history after delete", func(t *testing.T) {
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
// this will delete `block1` and any other blocks with `block1` as parent.
|
||||
err = store.DeleteBlock(blocksToInsert[0].ID, testUserID)
|
||||
require.NoError(t, err)
|
||||
|
||||
@ -1029,15 +1035,14 @@ func testGetBlockMetadata(t *testing.T, store store.Store) {
|
||||
}
|
||||
blocks, err = store.GetBlockHistoryDescendants(boardID, opts)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, blocks, 6)
|
||||
expectedBlock := blocksToInsert[0]
|
||||
block := blocks[0]
|
||||
|
||||
require.Equal(t, expectedBlock.ID, block.ID)
|
||||
// all 5 blocks get a history record for insert, then `block1` gets a record for delete,
|
||||
// and all 3 `block1` children get a record for delete. Thus total is 9.
|
||||
require.Len(t, blocks, 9)
|
||||
})
|
||||
|
||||
t.Run("get full block history after undelete", func(t *testing.T) {
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
// this will undelete `block1` and its children
|
||||
err = store.UndeleteBlock(blocksToInsert[0].ID, testUserID)
|
||||
require.NoError(t, err)
|
||||
|
||||
@ -1046,11 +1051,9 @@ func testGetBlockMetadata(t *testing.T, store store.Store) {
|
||||
}
|
||||
blocks, err = store.GetBlockHistoryDescendants(boardID, opts)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, blocks, 7)
|
||||
expectedBlock := blocksToInsert[0]
|
||||
block := blocks[0]
|
||||
|
||||
require.Equal(t, expectedBlock.ID, block.ID)
|
||||
// previous test put 9 records in history table. In this test 1 record was added for undeleting
|
||||
// `block1` and another 3 for undeleting the children for a total of 13.
|
||||
require.Len(t, blocks, 13)
|
||||
})
|
||||
|
||||
t.Run("get block history of a board with no history", func(t *testing.T) {
|
||||
@ -1061,3 +1064,92 @@ func testGetBlockMetadata(t *testing.T, store store.Store) {
|
||||
require.Empty(t, blocks)
|
||||
})
|
||||
}
|
||||
|
||||
func testUndeleteBlockChildren(t *testing.T, store store.Store) {
|
||||
boards := createTestBoards(t, store, testUserID, 2)
|
||||
boardDelete := boards[0]
|
||||
boardKeep := boards[1]
|
||||
|
||||
// create some blocks to be deleted
|
||||
cardsDelete := createTestCards(t, store, testUserID, boardDelete.ID, 3)
|
||||
blocksDelete := createTestBlocksForCard(t, store, cardsDelete[0].ID, 5)
|
||||
require.Len(t, blocksDelete, 5)
|
||||
|
||||
// create some blocks to keep
|
||||
cardsKeep := createTestCards(t, store, testUserID, boardKeep.ID, 3)
|
||||
blocksKeep := createTestBlocksForCard(t, store, cardsKeep[0].ID, 4)
|
||||
require.Len(t, blocksKeep, 4)
|
||||
|
||||
t.Run("undelete block children for card", func(t *testing.T) {
|
||||
cardDelete := cardsDelete[0]
|
||||
cardKeep := cardsKeep[0]
|
||||
|
||||
// delete a card
|
||||
err := store.DeleteBlock(cardDelete.ID, testUserID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// ensure the card was deleted
|
||||
block, err := store.GetBlock(cardDelete.ID)
|
||||
require.Error(t, err)
|
||||
require.Nil(t, block)
|
||||
|
||||
// ensure the card children were deleted
|
||||
blocks, err := store.GetBlocksWithParentAndType(cardDelete.BoardID, cardDelete.ID, model.TypeText)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, blocks)
|
||||
|
||||
// ensure the other card children remain.
|
||||
blocks, err = store.GetBlocksWithParentAndType(cardKeep.BoardID, cardKeep.ID, model.TypeText)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, blocks, len(blocksKeep))
|
||||
|
||||
// undelete the card
|
||||
err = store.UndeleteBlock(cardDelete.ID, testUserID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// ensure the card was restored
|
||||
block, err = store.GetBlock(cardDelete.ID)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, block)
|
||||
|
||||
// ensure the card children were restored
|
||||
blocks, err = store.GetBlocksWithParentAndType(cardDelete.BoardID, cardDelete.ID, model.TypeText)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, blocks, len(blocksDelete))
|
||||
})
|
||||
|
||||
t.Run("undelete block children for board", func(t *testing.T) {
|
||||
// delete the board
|
||||
err := store.DeleteBoard(boardDelete.ID, testUserID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// ensure the board was deleted
|
||||
board, err := store.GetBoard(boardDelete.ID)
|
||||
require.Error(t, err)
|
||||
require.Nil(t, board)
|
||||
|
||||
// ensure all cards and blocks for the board were deleted
|
||||
blocks, err := store.GetBlocksForBoard(boardDelete.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, blocks)
|
||||
|
||||
// ensure the other board's cards and blocks remain.
|
||||
blocks, err = store.GetBlocksForBoard(boardKeep.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, blocks, len(blocksKeep)+len(cardsKeep))
|
||||
|
||||
// undelete the board
|
||||
err = store.UndeleteBoard(boardDelete.ID, testUserID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// ensure the board was restored
|
||||
board, err = store.GetBoard(boardDelete.ID)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, board)
|
||||
|
||||
// ensure the board's cards and blocks were restored.
|
||||
blocks, err = store.GetBlocksForBoard(boardDelete.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, blocks, len(blocksDelete)+len(cardsDelete))
|
||||
})
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ import (
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
"github.com/mattermost/focalboard/server/services/store"
|
||||
"github.com/mattermost/focalboard/server/utils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@ -36,7 +37,7 @@ func createTestBlocks(t *testing.T, store store.Store, userID string, num int) [
|
||||
block := &model.Block{
|
||||
ID: utils.NewID(utils.IDTypeBlock),
|
||||
BoardID: utils.NewID(utils.IDTypeBoard),
|
||||
Type: "card",
|
||||
Type: model.TypeCard,
|
||||
CreatedBy: userID,
|
||||
}
|
||||
err := store.InsertBlock(block, userID)
|
||||
@ -46,3 +47,63 @@ func createTestBlocks(t *testing.T, store store.Store, userID string, num int) [
|
||||
}
|
||||
return blocks
|
||||
}
|
||||
|
||||
func createTestBlocksForCard(t *testing.T, store store.Store, cardID string, num int) []*model.Block {
|
||||
card, err := store.GetBlock(cardID)
|
||||
require.NoError(t, err)
|
||||
assert.EqualValues(t, model.TypeCard, card.Type)
|
||||
|
||||
var blocks []*model.Block
|
||||
for i := 0; i < num; i++ {
|
||||
block := &model.Block{
|
||||
ID: utils.NewID(utils.IDTypeBlock),
|
||||
BoardID: card.BoardID,
|
||||
Type: model.TypeText,
|
||||
CreatedBy: card.CreatedBy,
|
||||
ParentID: card.ID,
|
||||
Title: fmt.Sprintf("text %d", i),
|
||||
}
|
||||
err := store.InsertBlock(block, card.CreatedBy)
|
||||
require.NoError(t, err)
|
||||
|
||||
blocks = append(blocks, block)
|
||||
}
|
||||
return blocks
|
||||
}
|
||||
|
||||
func createTestCards(t *testing.T, store store.Store, userID string, boardID string, num int) []*model.Block {
|
||||
var blocks []*model.Block
|
||||
for i := 0; i < num; i++ {
|
||||
block := &model.Block{
|
||||
ID: utils.NewID(utils.IDTypeCard),
|
||||
BoardID: boardID,
|
||||
ParentID: boardID,
|
||||
Type: model.TypeCard,
|
||||
CreatedBy: userID,
|
||||
Title: fmt.Sprintf("card %d", i),
|
||||
}
|
||||
err := store.InsertBlock(block, userID)
|
||||
require.NoError(t, err)
|
||||
|
||||
blocks = append(blocks, block)
|
||||
}
|
||||
return blocks
|
||||
}
|
||||
|
||||
func createTestBoards(t *testing.T, store store.Store, userID string, num int) []*model.Board {
|
||||
var boards []*model.Board
|
||||
for i := 0; i < num; i++ {
|
||||
board := &model.Board{
|
||||
ID: utils.NewID(utils.IDTypeBoard),
|
||||
TeamID: testTeamID,
|
||||
Type: "O",
|
||||
CreatedBy: userID,
|
||||
Title: fmt.Sprintf("board %d", i),
|
||||
}
|
||||
boardNew, err := store.InsertBoard(board, userID)
|
||||
require.NoError(t, err)
|
||||
|
||||
boards = append(boards, boardNew)
|
||||
}
|
||||
return boards
|
||||
}
|
||||
|
@ -2,7 +2,6 @@ package utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"runtime/debug"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
@ -123,7 +122,6 @@ func (cn *CallbackQueue) exec(f CallbackFunc) {
|
||||
cn.logger.Error("CallbackQueue callback panic",
|
||||
mlog.String("name", cn.name),
|
||||
mlog.Any("panic", r),
|
||||
mlog.String("stack", string(debug.Stack())),
|
||||
)
|
||||
}
|
||||
}()
|
||||
|
Loading…
Reference in New Issue
Block a user