From 8949c6b13fdc261766c8226b0625c4a6c8fe821c Mon Sep 17 00:00:00 2001 From: Doug Lauder Date: Tue, 8 Nov 2022 11:42:01 -0500 Subject: [PATCH] Delete children when deleting boards and cards (#3943) * delete and undelete handle children --- server/go.mod | 2 +- server/go.sum | 2 + server/services/store/mockstore/mockstore.go | 28 +++ server/services/store/sqlstore/blocks.go | 182 +++++++++++++++++- server/services/store/sqlstore/board.go | 12 +- .../store/sqlstore/boards_and_blocks.go | 12 +- .../services/store/sqlstore/legacy_blocks.go | 5 +- .../services/store/sqlstore/public_methods.go | 10 + server/services/store/sqlstore/util.go | 20 ++ server/services/store/store.go | 4 + server/services/store/storetests/blocks.go | 112 ++++++++++- server/services/store/storetests/util.go | 63 +++++- server/utils/callbackqueue.go | 2 - 13 files changed, 425 insertions(+), 29 deletions(-) diff --git a/server/go.mod b/server/go.mod index 842684fee..1f75c640d 100644 --- a/server/go.mod +++ b/server/go.mod @@ -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 diff --git a/server/go.sum b/server/go.sum index 4d528eafc..c017846a9 100644 --- a/server/go.sum +++ b/server/go.sum @@ -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= diff --git a/server/services/store/mockstore/mockstore.go b/server/services/store/mockstore/mockstore.go index 4f40a22ab..00a260353 100644 --- a/server/services/store/mockstore/mockstore.go +++ b/server/services/store/mockstore/mockstore.go @@ -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() diff --git a/server/services/store/sqlstore/blocks.go b/server/services/store/sqlstore/blocks.go index 8c6454bfa..e84d1b6ed 100644 --- a/server/services/store/sqlstore/blocks.go +++ b/server/services/store/sqlstore/blocks.go @@ -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 +} diff --git a/server/services/store/sqlstore/board.go b/server/services/store/sqlstore/board.go index dfd6f91b4..67f162825 100644 --- a/server/services/store/sqlstore/board.go +++ b/server/services/store/sqlstore/board.go @@ -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) { diff --git a/server/services/store/sqlstore/boards_and_blocks.go b/server/services/store/sqlstore/boards_and_blocks.go index 190daa497..198f57100 100644 --- a/server/services/store/sqlstore/boards_and_blocks.go +++ b/server/services/store/sqlstore/boards_and_blocks.go @@ -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 } diff --git a/server/services/store/sqlstore/legacy_blocks.go b/server/services/store/sqlstore/legacy_blocks.go index 6d440f49a..842f2d258 100644 --- a/server/services/store/sqlstore/legacy_blocks.go +++ b/server/services/store/sqlstore/legacy_blocks.go @@ -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) diff --git a/server/services/store/sqlstore/public_methods.go b/server/services/store/sqlstore/public_methods.go index a9ccf3e44..422633f13 100644 --- a/server/services/store/sqlstore/public_methods.go +++ b/server/services/store/sqlstore/public_methods.go @@ -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) diff --git a/server/services/store/sqlstore/util.go b/server/services/store/sqlstore/util.go index 5abe83257..49fa72897 100644 --- a/server/services/store/sqlstore/util.go +++ b/server/services/store/sqlstore/util.go @@ -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) +} diff --git a/server/services/store/store.go b/server/services/store/store.go index 78c0d1fb0..bc8ec3e2e 100644 --- a/server/services/store/store.go +++ b/server/services/store/store.go @@ -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 { diff --git a/server/services/store/storetests/blocks.go b/server/services/store/storetests/blocks.go index 06e623736..6a7f65e1a 100644 --- a/server/services/store/storetests/blocks.go +++ b/server/services/store/storetests/blocks.go @@ -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)) + }) +} diff --git a/server/services/store/storetests/util.go b/server/services/store/storetests/util.go index 40595d8d3..24d99eead 100644 --- a/server/services/store/storetests/util.go +++ b/server/services/store/storetests/util.go @@ -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 +} diff --git a/server/utils/callbackqueue.go b/server/utils/callbackqueue.go index a967f40f4..f1eadf41e 100644 --- a/server/utils/callbackqueue.go +++ b/server/utils/callbackqueue.go @@ -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())), ) } }()