From 70fdd42e58f3c03f42f1a4118d7d776141a72049 Mon Sep 17 00:00:00 2001 From: Dmitry Zhifarsky Date: Sun, 8 Aug 2021 21:42:08 +0300 Subject: [PATCH 1/8] fix(persistence): enable pragma foreign_keys --- .../stream_chat_persistence/lib/src/db/moor_chat_database.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/stream_chat_persistence/lib/src/db/moor_chat_database.dart b/packages/stream_chat_persistence/lib/src/db/moor_chat_database.dart index 7f5bd4d0..4dc2a1e4 100644 --- a/packages/stream_chat_persistence/lib/src/db/moor_chat_database.dart +++ b/packages/stream_chat_persistence/lib/src/db/moor_chat_database.dart @@ -64,6 +64,9 @@ class MoorChatDatabase extends _$MoorChatDatabase { } } }, + beforeOpen: (details) async { + await customStatement('PRAGMA foreign_keys = ON;'); + }, ); /// Deletes all the tables From 8adc38f8e47a3f7b7162d6477680a35ff3fe1c30 Mon Sep 17 00:00:00 2001 From: Dmitry Zhifarsky Date: Sun, 8 Aug 2021 21:42:27 +0300 Subject: [PATCH 2/8] fix(persistence): change mode insert or replace to upsert --- packages/stream_chat_persistence/lib/src/dao/channel_dao.dart | 3 +-- .../stream_chat_persistence/lib/src/dao/channel_query_dao.dart | 3 +-- .../lib/src/dao/connection_event_dao.dart | 3 +-- packages/stream_chat_persistence/lib/src/dao/member_dao.dart | 3 +-- packages/stream_chat_persistence/lib/src/dao/message_dao.dart | 3 +-- .../lib/src/dao/pinned_message_dao.dart | 3 +-- packages/stream_chat_persistence/lib/src/dao/reaction_dao.dart | 3 +-- packages/stream_chat_persistence/lib/src/dao/read_dao.dart | 3 +-- packages/stream_chat_persistence/lib/src/dao/user_dao.dart | 3 +-- 9 files changed, 9 insertions(+), 18 deletions(-) diff --git a/packages/stream_chat_persistence/lib/src/dao/channel_dao.dart b/packages/stream_chat_persistence/lib/src/dao/channel_dao.dart index fc488511..36013f51 100644 --- a/packages/stream_chat_persistence/lib/src/dao/channel_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/channel_dao.dart @@ -42,10 +42,9 @@ class ChannelDao extends DatabaseAccessor /// Updates all the channels using the new [channelList] data Future updateChannels(List channelList) => batch( - (it) => it.insertAll( + (it) => it.insertAllOnConflictUpdate( channels, channelList.map((c) => c.toEntity()).toList(), - mode: InsertMode.insertOrReplace, ), ); } diff --git a/packages/stream_chat_persistence/lib/src/dao/channel_query_dao.dart b/packages/stream_chat_persistence/lib/src/dao/channel_query_dao.dart index 9100e76d..c87b94d4 100644 --- a/packages/stream_chat_persistence/lib/src/dao/channel_query_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/channel_query_dao.dart @@ -45,13 +45,12 @@ class ChannelQueryDao extends DatabaseAccessor } await batch((it) { - it.insertAll( + it.insertAllOnConflictUpdate( channelQueries, cids .map((cid) => ChannelQueryEntity(queryHash: hash, channelCid: cid)) .toList(), - mode: InsertMode.insertOrReplace, ); }); }); diff --git a/packages/stream_chat_persistence/lib/src/dao/connection_event_dao.dart b/packages/stream_chat_persistence/lib/src/dao/connection_event_dao.dart index 0cef23b0..88a03b29 100644 --- a/packages/stream_chat_persistence/lib/src/dao/connection_event_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/connection_event_dao.dart @@ -26,7 +26,7 @@ class ConnectionEventDao extends DatabaseAccessor /// Update stored connection event with latest data Future updateConnectionEvent(Event event) => transaction(() async { final connectionInfo = await select(connectionEvents).getSingleOrNull(); - return into(connectionEvents).insert( + return into(connectionEvents).insertOnConflictUpdate( ConnectionEventEntity( id: 1, type: event.type, @@ -38,7 +38,6 @@ class ConnectionEventDao extends DatabaseAccessor unreadChannels: event.unreadChannels ?? connectionInfo?.unreadChannels, ), - mode: InsertMode.insertOrReplace, ); }); diff --git a/packages/stream_chat_persistence/lib/src/dao/member_dao.dart b/packages/stream_chat_persistence/lib/src/dao/member_dao.dart index 157b0a9a..1f39904c 100644 --- a/packages/stream_chat_persistence/lib/src/dao/member_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/member_dao.dart @@ -32,10 +32,9 @@ class MemberDao extends DatabaseAccessor /// Updates all the members using the new [memberList] data Future updateMembers(String cid, List memberList) async => batch( - (it) => it.insertAll( + (it) => it.insertAllOnConflictUpdate( members, memberList.map((m) => m.toEntity(cid: cid)).toList(), - mode: InsertMode.insertOrReplace, ), ); diff --git a/packages/stream_chat_persistence/lib/src/dao/message_dao.dart b/packages/stream_chat_persistence/lib/src/dao/message_dao.dart index c2e1cde2..eb02e8aa 100644 --- a/packages/stream_chat_persistence/lib/src/dao/message_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/message_dao.dart @@ -171,10 +171,9 @@ class MessageDao extends DatabaseAccessor /// the new [messageList] data Future updateMessages(String cid, List messageList) => batch( (batch) { - batch.insertAll( + batch.insertAllOnConflictUpdate( messages, messageList.map((it) => it.toEntity(cid: cid)).toList(), - mode: InsertMode.insertOrReplace, ); }, ); diff --git a/packages/stream_chat_persistence/lib/src/dao/pinned_message_dao.dart b/packages/stream_chat_persistence/lib/src/dao/pinned_message_dao.dart index af6497f9..e0c4b876 100644 --- a/packages/stream_chat_persistence/lib/src/dao/pinned_message_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/pinned_message_dao.dart @@ -170,10 +170,9 @@ class PinnedMessageDao extends DatabaseAccessor /// the new [messageList] data Future updateMessages(String cid, List messageList) => batch( (batch) { - batch.insertAll( + batch.insertAllOnConflictUpdate( pinnedMessages, messageList.map((it) => it.toPinnedEntity(cid: cid)).toList(), - mode: InsertMode.insertOrReplace, ); }, ); diff --git a/packages/stream_chat_persistence/lib/src/dao/reaction_dao.dart b/packages/stream_chat_persistence/lib/src/dao/reaction_dao.dart index 89da6081..c178ae03 100644 --- a/packages/stream_chat_persistence/lib/src/dao/reaction_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/reaction_dao.dart @@ -42,10 +42,9 @@ class ReactionDao extends DatabaseAccessor /// Updates the reactions data with the new [reactionList] data Future updateReactions(List reactionList) => batch((it) { - it.insertAll( + it.insertAllOnConflictUpdate( reactions, reactionList.map((r) => r.toEntity()).toList(), - mode: InsertMode.insertOrReplace, ); }); diff --git a/packages/stream_chat_persistence/lib/src/dao/read_dao.dart b/packages/stream_chat_persistence/lib/src/dao/read_dao.dart index 05d807fd..007cb3a7 100644 --- a/packages/stream_chat_persistence/lib/src/dao/read_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/read_dao.dart @@ -30,10 +30,9 @@ class ReadDao extends DatabaseAccessor with _$ReadDaoMixin { /// Updates the read data of a particular channel with /// the new [readList] data Future updateReads(String cid, List readList) => batch( - (it) => it.insertAll( + (it) => it.insertAllOnConflictUpdate( reads, readList.map((r) => r.toEntity(cid: cid)).toList(), - mode: InsertMode.insertOrReplace, ), ); } diff --git a/packages/stream_chat_persistence/lib/src/dao/user_dao.dart b/packages/stream_chat_persistence/lib/src/dao/user_dao.dart index 9cdff8e7..b1e29e7b 100644 --- a/packages/stream_chat_persistence/lib/src/dao/user_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/user_dao.dart @@ -14,10 +14,9 @@ class UserDao extends DatabaseAccessor with _$UserDaoMixin { /// Updates the users data with the new [userList] data Future updateUsers(List userList) => batch( - (it) => it.insertAll( + (it) => it.insertAllOnConflictUpdate( users, userList.map((u) => u.toEntity()).toList(), - mode: InsertMode.insertOrReplace, ), ); From 68311a6c6cb4ac8b50c996376e43a151cc3eae3f Mon Sep 17 00:00:00 2001 From: Dmitry Zhifarsky Date: Sun, 8 Aug 2021 21:42:44 +0300 Subject: [PATCH 3/8] test(persistence): update tests after enabling foreign keys --- .../test/src/dao/channel_dao_test.dart | 61 +++++++ .../test/src/dao/member_dao_test.dart | 8 +- .../test/src/dao/message_dao_test.dart | 146 +++++++++++------ .../test/src/dao/pinned_message_dao_test.dart | 150 ++++++++++++------ .../test/src/dao/reaction_dao_test.dart | 5 +- .../test/src/dao/read_dao_test.dart | 6 +- 6 files changed, 278 insertions(+), 98 deletions(-) diff --git a/packages/stream_chat_persistence/test/src/dao/channel_dao_test.dart b/packages/stream_chat_persistence/test/src/dao/channel_dao_test.dart index 9a458574..c0a00100 100644 --- a/packages/stream_chat_persistence/test/src/dao/channel_dao_test.dart +++ b/packages/stream_chat_persistence/test/src/dao/channel_dao_test.dart @@ -61,12 +61,73 @@ void main() { expect(updatedChannel.cid, cid); expect(updatedChannel.type, type); + //Saving a dummy user + const userId = 'userId'; + final dummyUser = User(id: userId); + await database.userDao.updateUsers([dummyUser]); + + // Saving a dummy member + final dummyMember = Member(userId: userId, user: dummyUser); + await database.memberDao.updateMembers(cid, [dummyMember]); + + // Should match the dummy member + final updatedMembers = await database.memberDao.getMembersByCid(cid); + expect(updatedMembers.length, 1); + expect(updatedMembers.first.userId, userId); + + // Saving a dummy message + const messageId = 'messageId'; + final dummyMessage = Message(id: messageId, user: dummyUser); + await database.messageDao.updateMessages(cid, [dummyMessage]); + + // Should match the dummy message + final updatedMessages = await database.messageDao.getMessagesByCid(cid); + expect(updatedMessages.length, 1); + expect(updatedMessages.first.id, messageId); + + // Saving a dummy read + final dummyRead = Read(lastRead: DateTime.now(), user: dummyUser); + await database.readDao.updateReads(cid, [dummyRead]); + + // Should match the dummy read + final updatedReads = await database.readDao.getReadsByCid(cid); + expect(updatedReads.length, 1); + expect(updatedReads.first.user, dummyUser); + + // Saving a dummy reaction + final dummyReaction = + Reaction(type: 'type', messageId: messageId, userId: userId); + await database.reactionDao.updateReactions([dummyReaction]); + + // Should match the dummy reaction + final updatedReactions = + await database.reactionDao.getReactionsByUserId(messageId, userId); + expect(updatedReactions.length, 1); + expect(updatedReactions.first.messageId, messageId); + // Deleting the dummyChannel using cid await channelDao.deleteChannelByCids([cid]); // Fetched channel Should be null final channel = await channelDao.getChannelByCid(cid); expect(channel, isNull); + + // Fetched members for passed cid should be empty + final members = await database.memberDao.getMembersByCid(cid); + expect(members, isEmpty); + + // Fetched messages for passed cid should be empty + final messages = await database.messageDao.getMessagesByCid(cid); + expect(messages, isEmpty); + + // Fetched reads for passed cid should be empty + final reads = await database.readDao.getReadsByCid(cid); + expect(reads, isEmpty); + + // Fetched readtions for passed message id and user id should be empty + final reactions = + await database.reactionDao.getReactionsByUserId(messageId, userId); + expect(reactions, isEmpty); }); test('cids', () async { diff --git a/packages/stream_chat_persistence/test/src/dao/member_dao_test.dart b/packages/stream_chat_persistence/test/src/dao/member_dao_test.dart index 8ce704a4..4ca3880b 100644 --- a/packages/stream_chat_persistence/test/src/dao/member_dao_test.dart +++ b/packages/stream_chat_persistence/test/src/dao/member_dao_test.dart @@ -18,6 +18,7 @@ void main() { }); Future> _prepareTestData(String cid) async { + final channels = [ChannelModel(cid: cid)]; final users = List.generate(3, (index) => User(id: 'testUserId$index')); final memberList = List.generate( 3, @@ -34,12 +35,13 @@ void main() { ), ); await database.userDao.updateUsers(users); + await database.channelDao.updateChannels(channels); await memberDao.updateMembers(cid, memberList); return memberList; } test('getMembersByCid', () async { - const cid = 'testCid'; + const cid = 'test:Cid'; // Should be empty initially final members = await memberDao.getMembersByCid(cid); @@ -70,7 +72,7 @@ void main() { }); test('updateMembers', () async { - const cid = 'testCid'; + const cid = 'test:Cid'; // Preparing test data final memberList = await _prepareTestData(cid); @@ -132,7 +134,7 @@ void main() { }); test('deleteMemberByCids', () async { - const cid = 'testCid'; + const cid = 'test:Cid'; // Preparing test data final members = await _prepareTestData(cid); diff --git a/packages/stream_chat_persistence/test/src/dao/message_dao_test.dart b/packages/stream_chat_persistence/test/src/dao/message_dao_test.dart index fd0a43ce..d069f9f9 100644 --- a/packages/stream_chat_persistence/test/src/dao/message_dao_test.dart +++ b/packages/stream_chat_persistence/test/src/dao/message_dao_test.dart @@ -23,6 +23,7 @@ void main() { bool mapAllThreadToFirstMessage = false, int count = 3, }) async { + final channels = [ChannelModel(cid: cid)]; final users = List.generate(count, (index) => User(id: 'testUserId$index')); final messages = List.generate( count, @@ -98,13 +99,20 @@ void main() { if (quoted) ...quotedMessages, if (threads) ...threadMessages ]; + final reaction = Reaction( + type: 'type', + messageId: allMessages.first.id, + user: users.first, + ); await database.userDao.updateUsers(users); + await database.channelDao.updateChannels(channels); await messageDao.updateMessages(cid, allMessages); + await database.reactionDao.updateReactions([reaction]); return allMessages; } test('deleteMessageByIds', () async { - const cid = 'testCid'; + const cid = 'test:Cid'; // Preparing test data final insertedMessages = await _prepareTestData(cid); @@ -113,66 +121,112 @@ void main() { final messages = await messageDao.getMessagesByCid(cid); expect(messages.length, insertedMessages.length); + final firstMessageId = messages.first.id; + + // Fetched reactions list should have one reaction for given message id + final reactions = await database.reactionDao.getReactions(firstMessageId); + expect(reactions.length, 1); + // Deleting 2 messages from DB await messageDao.deleteMessageByIds( - ['testMessageId${cid}0', 'testMessageId${cid}1'], + [firstMessageId, 'testMessageId${cid}1'], ); // New fetched messages length should 2 less than the // previous fetched messages final newMessages = await messageDao.getMessagesByCid(cid); expect(newMessages.length, messages.length - 2); + + // Reaction for the first message should be deleted too + final newReactions = + await database.reactionDao.getReactions(firstMessageId); + expect(newReactions, isEmpty); }); group('deleteMessageByCids', () { - const cid1 = 'testCid1'; - const cid2 = 'testCid2'; + const cid1 = 'test:Cid1'; + const cid2 = 'test:Cid2'; - test('should delete all the messages of first channel', () async { - // Preparing test data - final cid1InsertedMessages = await _prepareTestData(cid1); - final cid2InsertedMessages = await _prepareTestData(cid2); + test( + 'should delete all the messages and reactions of first channel', + () async { + // Preparing test data + final cid1InsertedMessages = await _prepareTestData(cid1); + final cid2InsertedMessages = await _prepareTestData(cid2); - // Fetched message list should match the test message list length - final cid1Messages = await messageDao.getMessagesByCid(cid1); - final cid2Messages = await messageDao.getMessagesByCid(cid2); - expect(cid1Messages.length, cid1InsertedMessages.length); - expect(cid2Messages.length, cid2InsertedMessages.length); + // Fetched message list should match the test message list length + final cid1Messages = await messageDao.getMessagesByCid(cid1); + final cid2Messages = await messageDao.getMessagesByCid(cid2); + expect(cid1Messages.length, cid1InsertedMessages.length); + expect(cid2Messages.length, cid2InsertedMessages.length); - // Deleting all the messages of cid1 - await messageDao.deleteMessageByCids([cid1]); + // Fetched reactions list should have one reaction for given message id + final cid1firstMessageId = cid1Messages.first.id; + final cid1Reactions = + await database.reactionDao.getReactions(cid1firstMessageId); + expect(cid1Reactions.length, 1); - // Fetched messages length of only cid1 should be empty - final cid1FetchedMessages = await messageDao.getMessagesByCid(cid1); - final cid2FetchedMessages = await messageDao.getMessagesByCid(cid2); - expect(cid1FetchedMessages, isEmpty); - expect(cid2FetchedMessages, isNotEmpty); - }); + // Deleting all the messages of cid1 + await messageDao.deleteMessageByCids([cid1]); - test('should delete all the messages of both channel', () async { - // Preparing test data - final cid1InsertedMessages = await _prepareTestData(cid1); - final cid2InsertedMessages = await _prepareTestData(cid2); + // Fetched messages length of only cid1 should be empty + final cid1FetchedMessages = await messageDao.getMessagesByCid(cid1); + final cid2FetchedMessages = await messageDao.getMessagesByCid(cid2); + expect(cid1FetchedMessages, isEmpty); + expect(cid2FetchedMessages, isNotEmpty); - // Fetched message list should match the test message list length - final cid1Messages = await messageDao.getMessagesByCid(cid1); - final cid2Messages = await messageDao.getMessagesByCid(cid2); - expect(cid1Messages.length, cid1InsertedMessages.length); - expect(cid2Messages.length, cid2InsertedMessages.length); + // Reaction for the first message should be deleted too + final cid1FetchedReactions = + await database.reactionDao.getReactions(cid1firstMessageId); + expect(cid1FetchedReactions, isEmpty); + }, + ); - // Deleting all the messages of cid1 - await messageDao.deleteMessageByCids([cid1, cid2]); + test( + 'should delete all the messages and reactions of both channel', + () async { + // Preparing test data + final cid1InsertedMessages = await _prepareTestData(cid1); + final cid2InsertedMessages = await _prepareTestData(cid2); - // Fetched messages length of both cid1 and cid2 should be empty - final cid1FetchedMessages = await messageDao.getMessagesByCid(cid1); - final cid2FetchedMessages = await messageDao.getMessagesByCid(cid2); - expect(cid1FetchedMessages, isEmpty); - expect(cid2FetchedMessages, isEmpty); - }); + // Fetched message list should match the test message list length + final cid1Messages = await messageDao.getMessagesByCid(cid1); + final cid2Messages = await messageDao.getMessagesByCid(cid2); + expect(cid1Messages.length, cid1InsertedMessages.length); + expect(cid2Messages.length, cid2InsertedMessages.length); + + // Fetched reactions list should have one reaction for given message id + final cid1FirstMessageId = cid1Messages.first.id; + final cid1Reactions = + await database.reactionDao.getReactions(cid1FirstMessageId); + expect(cid1Reactions.length, 1); + final cid2FirstMessageId = cid2Messages.first.id; + final cid2Reactions = + await database.reactionDao.getReactions(cid2FirstMessageId); + expect(cid2Reactions.length, 1); + + // Deleting all the messages of cid1 + await messageDao.deleteMessageByCids([cid1, cid2]); + + // Fetched messages length of both cid1 and cid2 should be empty + final cid1FetchedMessages = await messageDao.getMessagesByCid(cid1); + final cid2FetchedMessages = await messageDao.getMessagesByCid(cid2); + expect(cid1FetchedMessages, isEmpty); + expect(cid2FetchedMessages, isEmpty); + + // Reaction for the first message should be deleted too + final cid1FetchedReactions = + await database.reactionDao.getReactions(cid1FirstMessageId); + expect(cid1FetchedReactions, isEmpty); + final cid2FetchedReactions = + await database.reactionDao.getReactions(cid2FirstMessageId); + expect(cid2FetchedReactions, isEmpty); + }, + ); }); test('getMessageById', () async { - const cid = 'testCid'; + const cid = 'test:Cid'; const id = 'testMessageId${cid}0'; // Should be null initially @@ -190,7 +244,7 @@ void main() { }); test('getThreadMessages', () async { - const cid = 'testCid'; + const cid = 'test:Cid'; // Messages should be empty initially final messages = await messageDao.getThreadMessages(cid); @@ -209,7 +263,7 @@ void main() { }); test('getThreadMessagesByParentId', () async { - const cid = 'testCid'; + const cid = 'test:Cid'; const parentId = 'testMessageId${cid}0'; // Messages should be empty initially @@ -228,7 +282,7 @@ void main() { }); test('getThreadMessagesByParentId along with pagination', () async { - const cid = 'testCid'; + const cid = 'test:Cid'; const parentId = 'testMessageId${cid}0'; const options = PaginationParams( limit: 15, @@ -262,7 +316,7 @@ void main() { }); test('getMessagesByCid', () async { - const cid = 'testCid'; + const cid = 'test:Cid'; // Should be empty initially final messages = await messageDao.getMessagesByCid(cid); @@ -283,7 +337,7 @@ void main() { }); test('getMessagesByCid along with quotedMessage', () async { - const cid = 'testCid'; + const cid = 'test:Cid'; // Should be empty initially final messages = await messageDao.getMessagesByCid(cid); @@ -301,7 +355,7 @@ void main() { }); test('getMessagesByCid along with pagination', () async { - const cid = 'testCid'; + const cid = 'test:Cid'; const limit = 15; const lessThan = 'testMessageId${cid}25'; const greaterThanOrEqual = 'testMessageId${cid}5'; @@ -333,7 +387,7 @@ void main() { }); test('updateMessages', () async { - const cid = 'testCid'; + const cid = 'test:Cid'; // Preparing test data final insertedMessages = await _prepareTestData(cid); diff --git a/packages/stream_chat_persistence/test/src/dao/pinned_message_dao_test.dart b/packages/stream_chat_persistence/test/src/dao/pinned_message_dao_test.dart index 42a54107..1d6a8c66 100644 --- a/packages/stream_chat_persistence/test/src/dao/pinned_message_dao_test.dart +++ b/packages/stream_chat_persistence/test/src/dao/pinned_message_dao_test.dart @@ -23,6 +23,7 @@ void main() { bool mapAllThreadToFirstMessage = false, int count = 3, }) async { + final channels = [ChannelModel(cid: cid)]; final users = List.generate(count, (index) => User(id: 'testUserId$index')); final messages = List.generate( count, @@ -83,13 +84,20 @@ void main() { if (quoted) ...quotedMessages, if (threads) ...threadMessages ]; + final reaction = Reaction( + type: 'type', + messageId: allMessages.first.id, + user: users.first, + ); await database.userDao.updateUsers(users); + await database.channelDao.updateChannels(channels); await pinnedMessageDao.updateMessages(cid, allMessages); + await database.reactionDao.updateReactions([reaction]); return allMessages; } test('deleteMessageByIds', () async { - const cid = 'testCid'; + const cid = 'test:Cid'; // Preparing test data final insertedMessages = await _prepareTestData(cid); @@ -98,66 +106,116 @@ void main() { final messages = await pinnedMessageDao.getMessagesByCid(cid); expect(messages.length, insertedMessages.length); + final firstMessageId = messages.first.id; + + // Fetched reactions list should have one reaction for given message id + final reactions = await database.reactionDao.getReactions(firstMessageId); + expect(reactions.length, 1); + // Deleting 2 messages from DB await pinnedMessageDao.deleteMessageByIds( - ['testMessageId${cid}0', 'testMessageId${cid}1'], + [firstMessageId, 'testMessageId${cid}1'], ); // New fetched messages length should 2 less than the // previous fetched messages final newMessages = await pinnedMessageDao.getMessagesByCid(cid); expect(newMessages.length, messages.length - 2); + + // Reaction for the first message should be deleted too + final newReactions = + await database.reactionDao.getReactions(firstMessageId); + expect(newReactions, isEmpty); }); group('deleteMessageByCids', () { - const cid1 = 'testCid1'; - const cid2 = 'testCid2'; + const cid1 = 'test:Cid1'; + const cid2 = 'test:Cid2'; - test('should delete all the messages of first channel', () async { - // Preparing test data - final cid1InsertedMessages = await _prepareTestData(cid1); - final cid2InsertedMessages = await _prepareTestData(cid2); + test( + 'should delete all the messages and reactions of first channel', + () async { + // Preparing test data + final cid1InsertedMessages = await _prepareTestData(cid1); + final cid2InsertedMessages = await _prepareTestData(cid2); - // Fetched message list should match the test message list length - final cid1Messages = await pinnedMessageDao.getMessagesByCid(cid1); - final cid2Messages = await pinnedMessageDao.getMessagesByCid(cid2); - expect(cid1Messages.length, cid1InsertedMessages.length); - expect(cid2Messages.length, cid2InsertedMessages.length); + // Fetched message list should match the test message list length + final cid1Messages = await pinnedMessageDao.getMessagesByCid(cid1); + final cid2Messages = await pinnedMessageDao.getMessagesByCid(cid2); + expect(cid1Messages.length, cid1InsertedMessages.length); + expect(cid2Messages.length, cid2InsertedMessages.length); - // Deleting all the messages of cid1 - await pinnedMessageDao.deleteMessageByCids([cid1]); + // Fetched reactions list should have one reaction for given message id + final cid1firstMessageId = cid1Messages.first.id; + final cid1Reactions = + await database.reactionDao.getReactions(cid1firstMessageId); + expect(cid1Reactions.length, 1); - // Fetched messages length of only cid1 should be empty - final cid1FetchedMessages = await pinnedMessageDao.getMessagesByCid(cid1); - final cid2FetchedMessages = await pinnedMessageDao.getMessagesByCid(cid2); - expect(cid1FetchedMessages, isEmpty); - expect(cid2FetchedMessages, isNotEmpty); - }); + // Deleting all the messages of cid1 + await pinnedMessageDao.deleteMessageByCids([cid1]); - test('should delete all the messages of both channel', () async { - // Preparing test data - final cid1InsertedMessages = await _prepareTestData(cid1); - final cid2InsertedMessages = await _prepareTestData(cid2); + // Fetched messages length of only cid1 should be empty + final cid1FetchedMessages = + await pinnedMessageDao.getMessagesByCid(cid1); + final cid2FetchedMessages = + await pinnedMessageDao.getMessagesByCid(cid2); + expect(cid1FetchedMessages, isEmpty); + expect(cid2FetchedMessages, isNotEmpty); - // Fetched message list should match the test message list length - final cid1Messages = await pinnedMessageDao.getMessagesByCid(cid1); - final cid2Messages = await pinnedMessageDao.getMessagesByCid(cid2); - expect(cid1Messages.length, cid1InsertedMessages.length); - expect(cid2Messages.length, cid2InsertedMessages.length); + // Reaction for the first message should be deleted too + final cid1FetchedReactions = + await database.reactionDao.getReactions(cid1firstMessageId); + expect(cid1FetchedReactions, isEmpty); + }, + ); - // Deleting all the messages of cid1 - await pinnedMessageDao.deleteMessageByCids([cid1, cid2]); + test( + 'should delete all the messages and reactions of both channel', + () async { + // Preparing test data + final cid1InsertedMessages = await _prepareTestData(cid1); + final cid2InsertedMessages = await _prepareTestData(cid2); - // Fetched messages length of both cid1 and cid2 should be empty - final cid1FetchedMessages = await pinnedMessageDao.getMessagesByCid(cid1); - final cid2FetchedMessages = await pinnedMessageDao.getMessagesByCid(cid2); - expect(cid1FetchedMessages, isEmpty); - expect(cid2FetchedMessages, isEmpty); - }); + // Fetched message list should match the test message list length + final cid1Messages = await pinnedMessageDao.getMessagesByCid(cid1); + final cid2Messages = await pinnedMessageDao.getMessagesByCid(cid2); + expect(cid1Messages.length, cid1InsertedMessages.length); + expect(cid2Messages.length, cid2InsertedMessages.length); + + // Fetched reactions list should have one reaction for given message id + final cid1FirstMessageId = cid1Messages.first.id; + final cid1Reactions = + await database.reactionDao.getReactions(cid1FirstMessageId); + expect(cid1Reactions.length, 1); + final cid2FirstMessageId = cid2Messages.first.id; + final cid2Reactions = + await database.reactionDao.getReactions(cid2FirstMessageId); + expect(cid2Reactions.length, 1); + + // Deleting all the messages of cid1 + await pinnedMessageDao.deleteMessageByCids([cid1, cid2]); + + // Fetched messages length of both cid1 and cid2 should be empty + final cid1FetchedMessages = + await pinnedMessageDao.getMessagesByCid(cid1); + final cid2FetchedMessages = + await pinnedMessageDao.getMessagesByCid(cid2); + expect(cid1FetchedMessages, isEmpty); + expect(cid2FetchedMessages, isEmpty); + + // Reaction for the first message should be deleted too + final cid1FetchedReactions = + await database.reactionDao.getReactions(cid1FirstMessageId); + expect(cid1FetchedReactions, isEmpty); + final cid2FetchedReactions = + await database.reactionDao.getReactions(cid2FirstMessageId); + expect(cid2FetchedReactions, isEmpty); + }, + ); }); test('getMessageById', () async { - const cid = 'testCid'; + const cid = 'test:Cid'; const id = 'testMessageId${cid}0'; // Should be null initially @@ -175,7 +233,7 @@ void main() { }); test('getThreadMessages', () async { - const cid = 'testCid'; + const cid = 'test:Cid'; // Messages should be empty initially final messages = await pinnedMessageDao.getThreadMessages(cid); @@ -194,7 +252,7 @@ void main() { }); test('getThreadMessagesByParentId', () async { - const cid = 'testCid'; + const cid = 'test:Cid'; const parentId = 'testMessageId${cid}0'; // Messages should be empty initially @@ -214,7 +272,7 @@ void main() { }); test('getThreadMessagesByParentId along with pagination', () async { - const cid = 'testCid'; + const cid = 'test:Cid'; const parentId = 'testMessageId${cid}0'; const options = PaginationParams( limit: 15, @@ -248,7 +306,7 @@ void main() { }); test('getMessagesByCid', () async { - const cid = 'testCid'; + const cid = 'test:Cid'; // Should be empty initially final messages = await pinnedMessageDao.getMessagesByCid(cid); @@ -269,7 +327,7 @@ void main() { }); test('getMessagesByCid along with quotedMessage', () async { - const cid = 'testCid'; + const cid = 'test:Cid'; // Should be empty initially final messages = await pinnedMessageDao.getMessagesByCid(cid); @@ -287,7 +345,7 @@ void main() { }); test('getMessagesByCid along with pagination', () async { - const cid = 'testCid'; + const cid = 'test:Cid'; const limit = 15; const lessThan = 'testMessageId${cid}25'; const greaterThanOrEqual = 'testMessageId${cid}5'; @@ -319,7 +377,7 @@ void main() { }); test('updateMessages', () async { - const cid = 'testCid'; + const cid = 'test:Cid'; // Preparing test data final insertedMessages = await _prepareTestData(cid); diff --git a/packages/stream_chat_persistence/test/src/dao/reaction_dao_test.dart b/packages/stream_chat_persistence/test/src/dao/reaction_dao_test.dart index 4edda680..21570d4e 100644 --- a/packages/stream_chat_persistence/test/src/dao/reaction_dao_test.dart +++ b/packages/stream_chat_persistence/test/src/dao/reaction_dao_test.dart @@ -21,6 +21,8 @@ void main() { String? userId, int count = 3, }) async { + const cid = 'test:Cid'; + final channels = [ChannelModel(cid: cid)]; final users = List.generate(count, (index) => User(id: 'testUserId$index')); final message = Message( id: messageId, @@ -50,7 +52,8 @@ void main() { ); await database.userDao.updateUsers(users); - await database.messageDao.updateMessages('testCid', [message]); + await database.channelDao.updateChannels(channels); + await database.messageDao.updateMessages(cid, [message]); await reactionDao.updateReactions(reactions); return reactions; diff --git a/packages/stream_chat_persistence/test/src/dao/read_dao_test.dart b/packages/stream_chat_persistence/test/src/dao/read_dao_test.dart index 287dcad4..ca556632 100644 --- a/packages/stream_chat_persistence/test/src/dao/read_dao_test.dart +++ b/packages/stream_chat_persistence/test/src/dao/read_dao_test.dart @@ -16,6 +16,7 @@ void main() { }); Future> _prepareReadData(String cid, {int count = 3}) async { + final channels = [ChannelModel(cid: cid)]; final users = List.generate(count, (index) => User(id: 'testUserId$index')); final reads = List.generate( count, @@ -27,12 +28,13 @@ void main() { ); await database.userDao.updateUsers(users); + await database.channelDao.updateChannels(channels); await readDao.updateReads(cid, reads); return reads; } test('getReadsByCid', () async { - const cid = 'testCid'; + const cid = 'test:Cid'; // Should be empty initially final reads = await readDao.getReadsByCid(cid); @@ -55,7 +57,7 @@ void main() { }); test('updateReads', () async { - const cid = 'testCid'; + const cid = 'test:Cid'; // Preparing test data final insertedReads = await _prepareReadData(cid); From 5db0efadbae24b8ce7ab395de230c572d96a110b Mon Sep 17 00:00:00 2001 From: Dmitry Zhifarsky Date: Sat, 28 Aug 2021 10:04:05 +0300 Subject: [PATCH 4/8] feat(persistence): add pinned message reactions table --- .../lib/src/dao/dao.dart | 1 + .../lib/src/dao/pinned_message_dao.dart | 6 +- .../src/dao/pinned_message_reaction_dao.dart | 60 ++++ .../dao/pinned_message_reaction_dao.g.dart | 13 + .../lib/src/db/moor_chat_database.dart | 2 + .../lib/src/db/moor_chat_database.g.dart | 339 ++++++++++++++++++ .../lib/src/entity/entity.dart | 1 + .../src/entity/pinned_message_reactions.dart | 13 + .../lib/src/mapper/mapper.dart | 1 + .../pinned_message_reaction_mapper.dart | 29 ++ .../src/stream_chat_persistence_client.dart | 21 ++ .../test/mock_chat_database.dart | 9 + .../test/src/dao/pinned_message_dao_test.dart | 31 +- .../dao/pinned_message_reaction_dao_test.dart | 205 +++++++++++ .../pinned_message_reaction_mapper_test.dart | 52 +++ .../stream_chat_persistence_client_test.dart | 26 ++ 16 files changed, 792 insertions(+), 17 deletions(-) create mode 100644 packages/stream_chat_persistence/lib/src/dao/pinned_message_reaction_dao.dart create mode 100644 packages/stream_chat_persistence/lib/src/dao/pinned_message_reaction_dao.g.dart create mode 100644 packages/stream_chat_persistence/lib/src/entity/pinned_message_reactions.dart create mode 100644 packages/stream_chat_persistence/lib/src/mapper/pinned_message_reaction_mapper.dart create mode 100644 packages/stream_chat_persistence/test/src/dao/pinned_message_reaction_dao_test.dart create mode 100644 packages/stream_chat_persistence/test/src/mapper/pinned_message_reaction_mapper_test.dart diff --git a/packages/stream_chat_persistence/lib/src/dao/dao.dart b/packages/stream_chat_persistence/lib/src/dao/dao.dart index 53c2f372..56998c98 100644 --- a/packages/stream_chat_persistence/lib/src/dao/dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/dao.dart @@ -4,6 +4,7 @@ export 'connection_event_dao.dart'; export 'member_dao.dart'; export 'message_dao.dart'; export 'pinned_message_dao.dart'; +export 'pinned_message_reaction_dao.dart'; export 'reaction_dao.dart'; export 'read_dao.dart'; export 'user_dao.dart'; diff --git a/packages/stream_chat_persistence/lib/src/dao/pinned_message_dao.dart b/packages/stream_chat_persistence/lib/src/dao/pinned_message_dao.dart index e0c4b876..065dc40d 100644 --- a/packages/stream_chat_persistence/lib/src/dao/pinned_message_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/pinned_message_dao.dart @@ -39,8 +39,10 @@ class PinnedMessageDao extends DatabaseAccessor final userEntity = rows.readTableOrNull(users); final pinnedByEntity = rows.readTableOrNull(_pinnedByUsers); final msgEntity = rows.readTable(pinnedMessages); - final latestReactions = await _db.reactionDao.getReactions(msgEntity.id); - final ownReactions = await _db.reactionDao.getReactionsByUserId( + final latestReactions = + await _db.pinnedMessageReactionDao.getReactions(msgEntity.id); + final ownReactions = + await _db.pinnedMessageReactionDao.getReactionsByUserId( msgEntity.id, _db.userId, ); diff --git a/packages/stream_chat_persistence/lib/src/dao/pinned_message_reaction_dao.dart b/packages/stream_chat_persistence/lib/src/dao/pinned_message_reaction_dao.dart new file mode 100644 index 00000000..932bb838 --- /dev/null +++ b/packages/stream_chat_persistence/lib/src/dao/pinned_message_reaction_dao.dart @@ -0,0 +1,60 @@ +import 'package:moor/moor.dart'; +import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; +import 'package:stream_chat_persistence/src/entity/pinned_message_reactions.dart'; +import 'package:stream_chat_persistence/src/entity/users.dart'; +import 'package:stream_chat_persistence/src/mapper/mapper.dart'; + +part 'pinned_message_reaction_dao.g.dart'; + +/// The Data Access Object for operations in [PinnedMessageReactions] table. +@UseDao(tables: [PinnedMessageReactions, Users]) +class PinnedMessageReactionDao extends DatabaseAccessor + with _$PinnedMessageReactionDaoMixin { + /// Creates a new reaction dao instance + PinnedMessageReactionDao(MoorChatDatabase db) : super(db); + + /// Returns all the reactions of a particular message by matching + /// [Reactions.messageId] with [messageId] + Future> getReactions(String messageId) => + (select(pinnedMessageReactions).join([ + leftOuterJoin(users, pinnedMessageReactions.userId.equalsExp(users.id)), + ]) + ..where(pinnedMessageReactions.messageId.equals(messageId)) + ..orderBy([OrderingTerm.asc(pinnedMessageReactions.createdAt)])) + .map((rows) { + final userEntity = rows.readTableOrNull(users); + final reactionEntity = rows.readTable(pinnedMessageReactions); + return reactionEntity.toReaction(user: userEntity?.toUser()); + }).get(); + + /// Returns all the reactions of a particular message + /// added by a particular user by matching + /// [Reactions.messageId] with [messageId] and + /// [Reactions.userId] with [userId] + Future> getReactionsByUserId( + String messageId, + String userId, + ) async { + final reactions = await getReactions(messageId); + return reactions.where((it) => it.userId == userId).toList(); + } + + /// Updates the reactions data with the new [reactionList] data + Future updateReactions(List reactionList) => batch((it) { + it.insertAllOnConflictUpdate( + pinnedMessageReactions, + reactionList.map((r) => r.toPinnedEntity()).toList(), + ); + }); + + /// Deletes all the reactions whose [Reactions.messageId] is + /// present in [messageIds] + Future deleteReactionsByMessageIds(List messageIds) => + batch((it) { + it.deleteWhere( + pinnedMessageReactions, + (r) => r.messageId.isIn(messageIds), + ); + }); +} diff --git a/packages/stream_chat_persistence/lib/src/dao/pinned_message_reaction_dao.g.dart b/packages/stream_chat_persistence/lib/src/dao/pinned_message_reaction_dao.g.dart new file mode 100644 index 00000000..707a7c12 --- /dev/null +++ b/packages/stream_chat_persistence/lib/src/dao/pinned_message_reaction_dao.g.dart @@ -0,0 +1,13 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'pinned_message_reaction_dao.dart'; + +// ************************************************************************** +// DaoGenerator +// ************************************************************************** + +mixin _$PinnedMessageReactionDaoMixin on DatabaseAccessor { + $PinnedMessageReactionsTable get pinnedMessageReactions => + attachedDatabase.pinnedMessageReactions; + $UsersTable get users => attachedDatabase.users; +} diff --git a/packages/stream_chat_persistence/lib/src/db/moor_chat_database.dart b/packages/stream_chat_persistence/lib/src/db/moor_chat_database.dart index 4dc2a1e4..741ac957 100644 --- a/packages/stream_chat_persistence/lib/src/db/moor_chat_database.dart +++ b/packages/stream_chat_persistence/lib/src/db/moor_chat_database.dart @@ -14,6 +14,7 @@ part 'moor_chat_database.g.dart'; Channels, Messages, PinnedMessages, + PinnedMessageReactions, Reactions, Users, Members, @@ -25,6 +26,7 @@ part 'moor_chat_database.g.dart'; ChannelDao, MessageDao, PinnedMessageDao, + PinnedMessageReactionDao, MemberDao, ReactionDao, ReadDao, diff --git a/packages/stream_chat_persistence/lib/src/db/moor_chat_database.g.dart b/packages/stream_chat_persistence/lib/src/db/moor_chat_database.g.dart index a61d1e6e..c0991f3e 100644 --- a/packages/stream_chat_persistence/lib/src/db/moor_chat_database.g.dart +++ b/packages/stream_chat_persistence/lib/src/db/moor_chat_database.g.dart @@ -2769,6 +2769,340 @@ class $PinnedMessagesTable extends PinnedMessages MapConverter(); } +class PinnedMessageReactionEntity extends DataClass + implements Insertable { + /// The id of the user that sent the reaction + final String userId; + + /// The messageId to which the reaction belongs + final String messageId; + + /// The type of the reaction + final String type; + + /// The DateTime on which the reaction is created + final DateTime createdAt; + + /// The score of the reaction (ie. number of reactions sent) + final int score; + + /// Reaction custom extraData + final Map? extraData; + PinnedMessageReactionEntity( + {required this.userId, + required this.messageId, + required this.type, + required this.createdAt, + required this.score, + this.extraData}); + factory PinnedMessageReactionEntity.fromData( + Map data, GeneratedDatabase db, + {String? prefix}) { + final effectivePrefix = prefix ?? ''; + return PinnedMessageReactionEntity( + userId: const StringType() + .mapFromDatabaseResponse(data['${effectivePrefix}user_id'])!, + messageId: const StringType() + .mapFromDatabaseResponse(data['${effectivePrefix}message_id'])!, + type: const StringType() + .mapFromDatabaseResponse(data['${effectivePrefix}type'])!, + createdAt: const DateTimeType() + .mapFromDatabaseResponse(data['${effectivePrefix}created_at'])!, + score: const IntType() + .mapFromDatabaseResponse(data['${effectivePrefix}score'])!, + extraData: $PinnedMessageReactionsTable.$converter0.mapToDart( + const StringType() + .mapFromDatabaseResponse(data['${effectivePrefix}extra_data'])), + ); + } + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['user_id'] = Variable(userId); + map['message_id'] = Variable(messageId); + map['type'] = Variable(type); + map['created_at'] = Variable(createdAt); + map['score'] = Variable(score); + if (!nullToAbsent || extraData != null) { + final converter = $PinnedMessageReactionsTable.$converter0; + map['extra_data'] = Variable(converter.mapToSql(extraData)); + } + return map; + } + + factory PinnedMessageReactionEntity.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= moorRuntimeOptions.defaultSerializer; + return PinnedMessageReactionEntity( + userId: serializer.fromJson(json['userId']), + messageId: serializer.fromJson(json['messageId']), + type: serializer.fromJson(json['type']), + createdAt: serializer.fromJson(json['createdAt']), + score: serializer.fromJson(json['score']), + extraData: serializer.fromJson?>(json['extraData']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= moorRuntimeOptions.defaultSerializer; + return { + 'userId': serializer.toJson(userId), + 'messageId': serializer.toJson(messageId), + 'type': serializer.toJson(type), + 'createdAt': serializer.toJson(createdAt), + 'score': serializer.toJson(score), + 'extraData': serializer.toJson?>(extraData), + }; + } + + PinnedMessageReactionEntity copyWith( + {String? userId, + String? messageId, + String? type, + DateTime? createdAt, + int? score, + Value?> extraData = const Value.absent()}) => + PinnedMessageReactionEntity( + userId: userId ?? this.userId, + messageId: messageId ?? this.messageId, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + score: score ?? this.score, + extraData: extraData.present ? extraData.value : this.extraData, + ); + @override + String toString() { + return (StringBuffer('PinnedMessageReactionEntity(') + ..write('userId: $userId, ') + ..write('messageId: $messageId, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('score: $score, ') + ..write('extraData: $extraData') + ..write(')')) + .toString(); + } + + @override + int get hashCode => $mrjf($mrjc( + userId.hashCode, + $mrjc( + messageId.hashCode, + $mrjc( + type.hashCode, + $mrjc(createdAt.hashCode, + $mrjc(score.hashCode, extraData.hashCode)))))); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is PinnedMessageReactionEntity && + other.userId == this.userId && + other.messageId == this.messageId && + other.type == this.type && + other.createdAt == this.createdAt && + other.score == this.score && + other.extraData == this.extraData); +} + +class PinnedMessageReactionsCompanion + extends UpdateCompanion { + final Value userId; + final Value messageId; + final Value type; + final Value createdAt; + final Value score; + final Value?> extraData; + const PinnedMessageReactionsCompanion({ + this.userId = const Value.absent(), + this.messageId = const Value.absent(), + this.type = const Value.absent(), + this.createdAt = const Value.absent(), + this.score = const Value.absent(), + this.extraData = const Value.absent(), + }); + PinnedMessageReactionsCompanion.insert({ + required String userId, + required String messageId, + required String type, + this.createdAt = const Value.absent(), + this.score = const Value.absent(), + this.extraData = const Value.absent(), + }) : userId = Value(userId), + messageId = Value(messageId), + type = Value(type); + static Insertable custom({ + Expression? userId, + Expression? messageId, + Expression? type, + Expression? createdAt, + Expression? score, + Expression?>? extraData, + }) { + return RawValuesInsertable({ + if (userId != null) 'user_id': userId, + if (messageId != null) 'message_id': messageId, + if (type != null) 'type': type, + if (createdAt != null) 'created_at': createdAt, + if (score != null) 'score': score, + if (extraData != null) 'extra_data': extraData, + }); + } + + PinnedMessageReactionsCompanion copyWith( + {Value? userId, + Value? messageId, + Value? type, + Value? createdAt, + Value? score, + Value?>? extraData}) { + return PinnedMessageReactionsCompanion( + userId: userId ?? this.userId, + messageId: messageId ?? this.messageId, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + score: score ?? this.score, + extraData: extraData ?? this.extraData, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (messageId.present) { + map['message_id'] = Variable(messageId.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (score.present) { + map['score'] = Variable(score.value); + } + if (extraData.present) { + final converter = $PinnedMessageReactionsTable.$converter0; + map['extra_data'] = + Variable(converter.mapToSql(extraData.value)); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PinnedMessageReactionsCompanion(') + ..write('userId: $userId, ') + ..write('messageId: $messageId, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('score: $score, ') + ..write('extraData: $extraData') + ..write(')')) + .toString(); + } +} + +class $PinnedMessageReactionsTable extends PinnedMessageReactions + with TableInfo<$PinnedMessageReactionsTable, PinnedMessageReactionEntity> { + final GeneratedDatabase _db; + final String? _alias; + $PinnedMessageReactionsTable(this._db, [this._alias]); + final VerificationMeta _userIdMeta = const VerificationMeta('userId'); + late final GeneratedColumn userId = GeneratedColumn( + 'user_id', aliasedName, false, + typeName: 'TEXT', requiredDuringInsert: true); + final VerificationMeta _messageIdMeta = const VerificationMeta('messageId'); + late final GeneratedColumn messageId = GeneratedColumn( + 'message_id', aliasedName, false, + typeName: 'TEXT', + requiredDuringInsert: true, + $customConstraints: 'REFERENCES pinned_messages(id) ON DELETE CASCADE'); + final VerificationMeta _typeMeta = const VerificationMeta('type'); + late final GeneratedColumn type = GeneratedColumn( + 'type', aliasedName, false, + typeName: 'TEXT', requiredDuringInsert: true); + final VerificationMeta _createdAtMeta = const VerificationMeta('createdAt'); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + typeName: 'INTEGER', + requiredDuringInsert: false, + defaultValue: currentDateAndTime); + final VerificationMeta _scoreMeta = const VerificationMeta('score'); + late final GeneratedColumn score = GeneratedColumn( + 'score', aliasedName, false, + typeName: 'INTEGER', + requiredDuringInsert: false, + defaultValue: const Constant(0)); + final VerificationMeta _extraDataMeta = const VerificationMeta('extraData'); + late final GeneratedColumnWithTypeConverter, String?> + extraData = GeneratedColumn('extra_data', aliasedName, true, + typeName: 'TEXT', requiredDuringInsert: false) + .withConverter>( + $PinnedMessageReactionsTable.$converter0); + @override + List get $columns => + [userId, messageId, type, createdAt, score, extraData]; + @override + String get aliasedName => _alias ?? 'pinned_message_reactions'; + @override + String get actualTableName => 'pinned_message_reactions'; + @override + VerificationContext validateIntegrity( + Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('user_id')) { + context.handle(_userIdMeta, + userId.isAcceptableOrUnknown(data['user_id']!, _userIdMeta)); + } else if (isInserting) { + context.missing(_userIdMeta); + } + if (data.containsKey('message_id')) { + context.handle(_messageIdMeta, + messageId.isAcceptableOrUnknown(data['message_id']!, _messageIdMeta)); + } else if (isInserting) { + context.missing(_messageIdMeta); + } + if (data.containsKey('type')) { + context.handle( + _typeMeta, type.isAcceptableOrUnknown(data['type']!, _typeMeta)); + } else if (isInserting) { + context.missing(_typeMeta); + } + if (data.containsKey('created_at')) { + context.handle(_createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); + } + if (data.containsKey('score')) { + context.handle( + _scoreMeta, score.isAcceptableOrUnknown(data['score']!, _scoreMeta)); + } + context.handle(_extraDataMeta, const VerificationResult.success()); + return context; + } + + @override + Set get $primaryKey => {messageId, type, userId}; + @override + PinnedMessageReactionEntity map(Map data, + {String? tablePrefix}) { + return PinnedMessageReactionEntity.fromData(data, _db, + prefix: tablePrefix != null ? '$tablePrefix.' : null); + } + + @override + $PinnedMessageReactionsTable createAlias(String alias) { + return $PinnedMessageReactionsTable(_db, alias); + } + + static TypeConverter, String> $converter0 = + MapConverter(); +} + class ReactionEntity extends DataClass implements Insertable { /// The id of the user that sent the reaction final String userId; @@ -4897,6 +5231,8 @@ abstract class _$MoorChatDatabase extends GeneratedDatabase { late final $ChannelsTable channels = $ChannelsTable(this); late final $MessagesTable messages = $MessagesTable(this); late final $PinnedMessagesTable pinnedMessages = $PinnedMessagesTable(this); + late final $PinnedMessageReactionsTable pinnedMessageReactions = + $PinnedMessageReactionsTable(this); late final $ReactionsTable reactions = $ReactionsTable(this); late final $UsersTable users = $UsersTable(this); late final $MembersTable members = $MembersTable(this); @@ -4909,6 +5245,8 @@ abstract class _$MoorChatDatabase extends GeneratedDatabase { late final MessageDao messageDao = MessageDao(this as MoorChatDatabase); late final PinnedMessageDao pinnedMessageDao = PinnedMessageDao(this as MoorChatDatabase); + late final PinnedMessageReactionDao pinnedMessageReactionDao = + PinnedMessageReactionDao(this as MoorChatDatabase); late final MemberDao memberDao = MemberDao(this as MoorChatDatabase); late final ReactionDao reactionDao = ReactionDao(this as MoorChatDatabase); late final ReadDao readDao = ReadDao(this as MoorChatDatabase); @@ -4923,6 +5261,7 @@ abstract class _$MoorChatDatabase extends GeneratedDatabase { channels, messages, pinnedMessages, + pinnedMessageReactions, reactions, users, members, diff --git a/packages/stream_chat_persistence/lib/src/entity/entity.dart b/packages/stream_chat_persistence/lib/src/entity/entity.dart index d5218768..736fe405 100644 --- a/packages/stream_chat_persistence/lib/src/entity/entity.dart +++ b/packages/stream_chat_persistence/lib/src/entity/entity.dart @@ -3,6 +3,7 @@ export 'channels.dart'; export 'connection_events.dart'; export 'members.dart'; export 'messages.dart'; +export 'pinned_message_reactions.dart'; export 'pinned_messages.dart'; export 'reactions.dart'; export 'reads.dart'; diff --git a/packages/stream_chat_persistence/lib/src/entity/pinned_message_reactions.dart b/packages/stream_chat_persistence/lib/src/entity/pinned_message_reactions.dart new file mode 100644 index 00000000..fbc9f5ca --- /dev/null +++ b/packages/stream_chat_persistence/lib/src/entity/pinned_message_reactions.dart @@ -0,0 +1,13 @@ +// coverage:ignore-file +import 'package:moor/moor.dart'; + +import 'package:stream_chat_persistence/src/entity/reactions.dart'; + +/// Represents a [PinnedMessageReactions] table in [MoorChatDatabase]. +@DataClassName('PinnedMessageReactionEntity') +class PinnedMessageReactions extends Reactions { + /// The messageId to which the reaction belongs + @override + TextColumn get messageId => text() + .customConstraint('REFERENCES pinned_messages(id) ON DELETE CASCADE')(); +} diff --git a/packages/stream_chat_persistence/lib/src/mapper/mapper.dart b/packages/stream_chat_persistence/lib/src/mapper/mapper.dart index 66bf3cda..32de5859 100644 --- a/packages/stream_chat_persistence/lib/src/mapper/mapper.dart +++ b/packages/stream_chat_persistence/lib/src/mapper/mapper.dart @@ -3,6 +3,7 @@ export 'event_mapper.dart'; export 'member_mapper.dart'; export 'message_mapper.dart'; export 'pinned_message_mapper.dart'; +export 'pinned_message_reaction_mapper.dart'; export 'reaction_mapper.dart'; export 'read_mapper.dart'; export 'user_mapper.dart'; diff --git a/packages/stream_chat_persistence/lib/src/mapper/pinned_message_reaction_mapper.dart b/packages/stream_chat_persistence/lib/src/mapper/pinned_message_reaction_mapper.dart new file mode 100644 index 00000000..e4602bb0 --- /dev/null +++ b/packages/stream_chat_persistence/lib/src/mapper/pinned_message_reaction_mapper.dart @@ -0,0 +1,29 @@ +import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; + +/// Useful mapping functions for [PinnedMessageReactionEntity] +extension PinnedMessageReactionEntityX on PinnedMessageReactionEntity { + /// Maps a [PinnedMessageReactionEntity] into [Reaction] + Reaction toReaction({User? user}) => Reaction( + extraData: extraData ?? {}, + type: type, + createdAt: createdAt, + userId: userId, + user: user, + messageId: messageId, + score: score, + ); +} + +/// Useful mapping functions for [Reaction] +extension PReactionX on Reaction { + /// Maps a [Reaction] into [ReactionEntity] + PinnedMessageReactionEntity toPinnedEntity() => PinnedMessageReactionEntity( + extraData: extraData, + type: type, + createdAt: createdAt, + userId: userId!, + messageId: messageId!, + score: score, + ); +} diff --git a/packages/stream_chat_persistence/lib/src/stream_chat_persistence_client.dart b/packages/stream_chat_persistence/lib/src/stream_chat_persistence_client.dart index 20c4e327..5df7a124 100644 --- a/packages/stream_chat_persistence/lib/src/stream_chat_persistence_client.dart +++ b/packages/stream_chat_persistence/lib/src/stream_chat_persistence_client.dart @@ -318,6 +318,15 @@ class StreamChatPersistenceClient extends ChatPersistenceClient { ); } + @override + Future updatePinnedMessageReactions(List reactions) { + assert(_debugIsConnected, ''); + _logger.info('updatePinnedMessageReactions'); + return _readProtected( + () => db!.pinnedMessageReactionDao.updateReactions(reactions), + ); + } + @override Future updateReactions(List reactions) { assert(_debugIsConnected, ''); @@ -339,6 +348,18 @@ class StreamChatPersistenceClient extends ChatPersistenceClient { return _readProtected(() => db!.userDao.updateUsers(users)); } + @override + Future deletePinnedMessageReactionsByMessageId( + List messageIds, + ) { + assert(_debugIsConnected, ''); + _logger.info('deletePinnedMessageReactionsByMessageId'); + return _readProtected( + () => + db!.pinnedMessageReactionDao.deleteReactionsByMessageIds(messageIds), + ); + } + @override Future deleteReactionsByMessageId(List messageIds) { assert(_debugIsConnected, ''); diff --git a/packages/stream_chat_persistence/test/mock_chat_database.dart b/packages/stream_chat_persistence/test/mock_chat_database.dart index b421fad1..19b115e5 100644 --- a/packages/stream_chat_persistence/test/mock_chat_database.dart +++ b/packages/stream_chat_persistence/test/mock_chat_database.dart @@ -34,6 +34,12 @@ class MockChatDatabase extends Mock implements MoorChatDatabase { @override ReactionDao get reactionDao => _reactionDao ??= MockReactionDao(); + PinnedMessageReactionDao? _pinnedMessageReactionDao; + + @override + PinnedMessageReactionDao get pinnedMessageReactionDao => + _pinnedMessageReactionDao ??= MockPinnedMessageReactionDao(); + ReadDao? _readDao; @override @@ -70,6 +76,9 @@ class MockMemberDao extends Mock implements MemberDao {} class MockReactionDao extends Mock implements ReactionDao {} +class MockPinnedMessageReactionDao extends Mock + implements PinnedMessageReactionDao {} + class MockReadDao extends Mock implements ReadDao {} class MockChannelQueryDao extends Mock implements ChannelQueryDao {} diff --git a/packages/stream_chat_persistence/test/src/dao/pinned_message_dao_test.dart b/packages/stream_chat_persistence/test/src/dao/pinned_message_dao_test.dart index 1d6a8c66..f8fae456 100644 --- a/packages/stream_chat_persistence/test/src/dao/pinned_message_dao_test.dart +++ b/packages/stream_chat_persistence/test/src/dao/pinned_message_dao_test.dart @@ -92,7 +92,7 @@ void main() { await database.userDao.updateUsers(users); await database.channelDao.updateChannels(channels); await pinnedMessageDao.updateMessages(cid, allMessages); - await database.reactionDao.updateReactions([reaction]); + await database.pinnedMessageReactionDao.updateReactions([reaction]); return allMessages; } @@ -109,7 +109,8 @@ void main() { final firstMessageId = messages.first.id; // Fetched reactions list should have one reaction for given message id - final reactions = await database.reactionDao.getReactions(firstMessageId); + final reactions = + await database.pinnedMessageReactionDao.getReactions(firstMessageId); expect(reactions.length, 1); // Deleting 2 messages from DB @@ -124,7 +125,7 @@ void main() { // Reaction for the first message should be deleted too final newReactions = - await database.reactionDao.getReactions(firstMessageId); + await database.pinnedMessageReactionDao.getReactions(firstMessageId); expect(newReactions, isEmpty); }); @@ -147,8 +148,8 @@ void main() { // Fetched reactions list should have one reaction for given message id final cid1firstMessageId = cid1Messages.first.id; - final cid1Reactions = - await database.reactionDao.getReactions(cid1firstMessageId); + final cid1Reactions = await database.pinnedMessageReactionDao + .getReactions(cid1firstMessageId); expect(cid1Reactions.length, 1); // Deleting all the messages of cid1 @@ -163,8 +164,8 @@ void main() { expect(cid2FetchedMessages, isNotEmpty); // Reaction for the first message should be deleted too - final cid1FetchedReactions = - await database.reactionDao.getReactions(cid1firstMessageId); + final cid1FetchedReactions = await database.pinnedMessageReactionDao + .getReactions(cid1firstMessageId); expect(cid1FetchedReactions, isEmpty); }, ); @@ -184,12 +185,12 @@ void main() { // Fetched reactions list should have one reaction for given message id final cid1FirstMessageId = cid1Messages.first.id; - final cid1Reactions = - await database.reactionDao.getReactions(cid1FirstMessageId); + final cid1Reactions = await database.pinnedMessageReactionDao + .getReactions(cid1FirstMessageId); expect(cid1Reactions.length, 1); final cid2FirstMessageId = cid2Messages.first.id; - final cid2Reactions = - await database.reactionDao.getReactions(cid2FirstMessageId); + final cid2Reactions = await database.pinnedMessageReactionDao + .getReactions(cid2FirstMessageId); expect(cid2Reactions.length, 1); // Deleting all the messages of cid1 @@ -204,11 +205,11 @@ void main() { expect(cid2FetchedMessages, isEmpty); // Reaction for the first message should be deleted too - final cid1FetchedReactions = - await database.reactionDao.getReactions(cid1FirstMessageId); + final cid1FetchedReactions = await database.pinnedMessageReactionDao + .getReactions(cid1FirstMessageId); expect(cid1FetchedReactions, isEmpty); - final cid2FetchedReactions = - await database.reactionDao.getReactions(cid2FirstMessageId); + final cid2FetchedReactions = await database.pinnedMessageReactionDao + .getReactions(cid2FirstMessageId); expect(cid2FetchedReactions, isEmpty); }, ); diff --git a/packages/stream_chat_persistence/test/src/dao/pinned_message_reaction_dao_test.dart b/packages/stream_chat_persistence/test/src/dao/pinned_message_reaction_dao_test.dart new file mode 100644 index 00000000..aa92c5cc --- /dev/null +++ b/packages/stream_chat_persistence/test/src/dao/pinned_message_reaction_dao_test.dart @@ -0,0 +1,205 @@ +import 'dart:math' as math; + +import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_persistence/src/dao/pinned_message_reaction_dao.dart'; +import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; +import 'package:test/test.dart'; + +import '../../stream_chat_persistence_client_test.dart'; + +void main() { + late PinnedMessageReactionDao pinnedMessageReactionDao; + late MoorChatDatabase database; + + setUp(() { + database = testDatabaseProvider('testUserId'); + pinnedMessageReactionDao = database.pinnedMessageReactionDao; + }); + + Future> _prepareReactionData( + String messageId, { + String? userId, + int count = 3, + }) async { + const cid = 'test:Cid'; + final channels = [ChannelModel(cid: cid)]; + final users = List.generate(count, (index) => User(id: 'testUserId$index')); + final message = Message( + id: messageId, + type: 'testType', + user: users.first, + createdAt: DateTime.now(), + shadowed: math.Random().nextBool(), + showInChannel: math.Random().nextBool(), + replyCount: 3, + updatedAt: DateTime.now(), + extraData: const {'extra_test_field': 'extraTestData'}, + text: 'Dummy text', + pinned: math.Random().nextBool(), + pinnedAt: DateTime.now(), + pinnedBy: users.first, + ); + final reactions = List.generate( + count, + (index) => Reaction( + type: 'testType$index', + createdAt: DateTime.now(), + userId: userId ?? users[index].id, + messageId: message.id, + score: count + 3, + extraData: {'extra_test_field': 'extraTestData'}, + ), + ); + + await database.userDao.updateUsers(users); + await database.channelDao.updateChannels(channels); + await database.pinnedMessageDao.updateMessages(cid, [message]); + await pinnedMessageReactionDao.updateReactions(reactions); + + return reactions; + } + + test('getReactions', () async { + const messageId = 'testMessageId'; + + // Should be empty initially + final reactions = await pinnedMessageReactionDao.getReactions(messageId); + expect(reactions, isEmpty); + + // Adding sample reactions + final insertedReactions = await _prepareReactionData(messageId); + expect(insertedReactions, isNotEmpty); + + // Fetched reaction length should match inserted reactions length. + // Every reaction messageId should match the provided messageId. + final fetchedReactions = + await pinnedMessageReactionDao.getReactions(messageId); + expect(fetchedReactions.length, insertedReactions.length); + expect(fetchedReactions.every((it) => it.messageId == messageId), true); + }); + + test('getReactionsByUserId', () async { + const messageId = 'testMessageId'; + const userId = 'testUserId'; + + // Should be empty initially + final reactions = + await pinnedMessageReactionDao.getReactionsByUserId(messageId, userId); + expect(reactions, isEmpty); + + // Adding sample reactions + final insertedReactions = + await _prepareReactionData(messageId, userId: userId); + expect(insertedReactions, isNotEmpty); + + // Fetched reaction length should match inserted reactions length. + // Every reaction messageId should match the provided messageId. + // Every reaction userId should match the provided userId. + final fetchedReactions = + await pinnedMessageReactionDao.getReactionsByUserId(messageId, userId); + expect(fetchedReactions.length, insertedReactions.length); + expect(fetchedReactions.every((it) => it.messageId == messageId), true); + expect(fetchedReactions.every((it) => it.userId == userId), true); + }); + + test('updateReactions', () async { + const messageId = 'testMessageId'; + + // Preparing test data + final reactions = await _prepareReactionData(messageId); + + // Modifying one of the reaction and also adding one new + final copyReaction = reactions.first.copyWith(score: 33); + final newReaction = Reaction( + type: 'testType3', + createdAt: DateTime.now(), + userId: 'testUserId3', + messageId: messageId, + score: 30, + extraData: {'extra_test_field': 'extraTestData'}, + ); + + await pinnedMessageReactionDao.updateReactions([copyReaction, newReaction]); + + // Fetched reaction length should be one more than inserted reactions. + // copyReaction `score` modified field should be 33. + // Fetched reactions should contain the newReaction. + final fetchedReactions = + await pinnedMessageReactionDao.getReactions(messageId); + expect(fetchedReactions.length, reactions.length + 1); + expect( + fetchedReactions + .firstWhere((it) => + it.userId == copyReaction.userId && it.type == copyReaction.type) + .score, + 33, + ); + expect( + fetchedReactions + .where((it) => + it.userId == newReaction.userId && it.type == newReaction.type) + .isNotEmpty, + true, + ); + }); + + group('deleteReactionsByMessageIds', () { + const messageId1 = 'testMessageId1'; + const messageId2 = 'testMessageId2'; + test('should delete all the reactions of first message', () async { + // Preparing test data + final insertedReactions1 = await _prepareReactionData(messageId1); + final insertedReactions2 = await _prepareReactionData(messageId2); + + // Fetched reaction list length should match + // the inserted reactions list length + final reactions1 = + await pinnedMessageReactionDao.getReactions(messageId1); + final reactions2 = + await pinnedMessageReactionDao.getReactions(messageId2); + expect(reactions1.length, insertedReactions1.length); + expect(reactions2.length, insertedReactions2.length); + + // Deleting all the reactions of messageId1 + await pinnedMessageReactionDao.deleteReactionsByMessageIds([messageId1]); + + // Fetched reactions length of only messageId1 should be empty + final fetchedReactions1 = + await pinnedMessageReactionDao.getReactions(messageId1); + final fetchedReactions2 = + await pinnedMessageReactionDao.getReactions(messageId2); + expect(fetchedReactions1, isEmpty); + expect(fetchedReactions2, isNotEmpty); + }); + test('should delete all the messages of both message', () async { + // Preparing test data + final insertedReactions1 = await _prepareReactionData(messageId1); + final insertedReactions2 = await _prepareReactionData(messageId2); + + // Fetched reaction list length should match + // the inserted reactions list length + final reactions1 = + await pinnedMessageReactionDao.getReactions(messageId1); + final reactions2 = + await pinnedMessageReactionDao.getReactions(messageId2); + expect(reactions1.length, insertedReactions1.length); + expect(reactions2.length, insertedReactions2.length); + + // Deleting all the reactions of messageId1 and messageId2 + await pinnedMessageReactionDao + .deleteReactionsByMessageIds([messageId1, messageId2]); + + // Fetched reactions length of both messages should be empty + final fetchedReactions1 = + await pinnedMessageReactionDao.getReactions(messageId1); + final fetchedReactions2 = + await pinnedMessageReactionDao.getReactions(messageId2); + expect(fetchedReactions1, isEmpty); + expect(fetchedReactions2, isEmpty); + }); + }); + + tearDown(() async { + await database.disconnect(); + }); +} diff --git a/packages/stream_chat_persistence/test/src/mapper/pinned_message_reaction_mapper_test.dart b/packages/stream_chat_persistence/test/src/mapper/pinned_message_reaction_mapper_test.dart new file mode 100644 index 00000000..bd8d0ba8 --- /dev/null +++ b/packages/stream_chat_persistence/test/src/mapper/pinned_message_reaction_mapper_test.dart @@ -0,0 +1,52 @@ +import 'package:test/test.dart'; +import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; +import 'package:stream_chat_persistence/src/mapper/pinned_message_reaction_mapper.dart'; + +import '../utils/date_matcher.dart'; + +void main() { + test('toReaction should map the entity into Reaction', () { + final user = User(id: 'testUserId'); + final message = Message(id: 'testMessageId'); + final entity = PinnedMessageReactionEntity( + userId: user.id, + messageId: message.id, + type: 'haha', + score: 33, + createdAt: DateTime.now(), + extraData: {'extra_test_data': 'extraData'}, + ); + + final reaction = entity.toReaction(user: user); + expect(reaction, isA()); + expect(reaction.userId, entity.userId); + expect(reaction.messageId, entity.messageId); + expect(reaction.type, entity.type); + expect(reaction.score, entity.score); + expect(reaction.createdAt, isSameDateAs(entity.createdAt)); + expect(reaction.extraData, entity.extraData); + }); + + test('toEntity should map reaction into PinnedMessageReactionEntity', () { + final user = User(id: 'testUserId'); + final message = Message(id: 'testMessageId'); + final reaction = Reaction( + userId: user.id, + messageId: message.id, + type: 'haha', + score: 33, + createdAt: DateTime.now(), + extraData: {'extra_test_data': 'extraData'}, + ); + + final entity = reaction.toPinnedEntity(); + expect(entity, isA()); + expect(entity.userId, reaction.userId); + expect(entity.messageId, reaction.messageId); + expect(entity.type, reaction.type); + expect(entity.score, reaction.score); + expect(entity.createdAt, isSameDateAs(reaction.createdAt)); + expect(entity.extraData, reaction.extraData); + }); +} diff --git a/packages/stream_chat_persistence/test/stream_chat_persistence_client_test.dart b/packages/stream_chat_persistence/test/stream_chat_persistence_client_test.dart index daaebe61..37e82e93 100644 --- a/packages/stream_chat_persistence/test/stream_chat_persistence_client_test.dart +++ b/packages/stream_chat_persistence/test/stream_chat_persistence_client_test.dart @@ -502,6 +502,21 @@ void main() { .called(1); }); + test('updatePinnedMessageReactions', () async { + final reactions = List.generate( + 3, + (index) => Reaction(type: 'testType$index'), + ); + when(() => + mockDatabase.pinnedMessageReactionDao.updateReactions(reactions)) + .thenAnswer((_) => Future.value()); + + await client.updatePinnedMessageReactions(reactions); + verify(() => + mockDatabase.pinnedMessageReactionDao.updateReactions(reactions)) + .called(1); + }); + test('deleteReactionsByMessageId', () async { final messageIds = []; when(() => @@ -514,6 +529,17 @@ void main() { .called(1); }); + test('deletePinnedMessageReactionsByMessageId', () async { + final messageIds = []; + when(() => mockDatabase.pinnedMessageReactionDao + .deleteReactionsByMessageIds(messageIds)) + .thenAnswer((_) => Future.value()); + + await client.deletePinnedMessageReactionsByMessageId(messageIds); + verify(() => mockDatabase.pinnedMessageReactionDao + .deleteReactionsByMessageIds(messageIds)).called(1); + }); + test('deleteMembersByCids', () async { final cids = []; when(() => mockDatabase.memberDao.deleteMemberByCids(cids)) From b369f189224f922d17994fbf7d9aa3a072d787ba Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 30 Aug 2021 16:22:36 +0530 Subject: [PATCH 5/8] feat(llc): add crud for pinned message reactions Signed-off-by: xsahil03x --- .../lib/src/db/chat_persistence_client.dart | 26 +++++++++++++++++++ .../src/db/chat_persistence_client_test.dart | 9 +++++++ 2 files changed, 35 insertions(+) diff --git a/packages/stream_chat/lib/src/db/chat_persistence_client.dart b/packages/stream_chat/lib/src/db/chat_persistence_client.dart index 2470e403..bf2ef3a8 100644 --- a/packages/stream_chat/lib/src/db/chat_persistence_client.dart +++ b/packages/stream_chat/lib/src/db/chat_persistence_client.dart @@ -170,9 +170,15 @@ abstract class ChatPersistenceClient { /// Updates the reactions data with the new [reactions] data Future updateReactions(List reactions); + /// Updates the pinned message reactions data with the new [reactions] data + Future updatePinnedMessageReactions(List reactions); + /// Deletes all the reactions by [messageIds] Future deleteReactionsByMessageId(List messageIds); + /// Deletes all the pinned messages reactions by [messageIds] + Future deletePinnedMessageReactionsByMessageId(List messageIds); + /// Deletes all the members by channel [cids] Future deleteMembersByCids(List cids); @@ -187,6 +193,12 @@ abstract class ChatPersistenceClient { .map((m) => m.id) .toList(growable: false)); + final deletePinnedMessageReactions = + deletePinnedMessageReactionsByMessageId(channelStates + .expand((it) => it.pinnedMessages) + .map((m) => m.id) + .toList(growable: false)); + final cleanedChannelStates = channelStates.where((it) => it.channel != null); @@ -196,6 +208,7 @@ abstract class ChatPersistenceClient { await Future.wait([ deleteReactions, + deletePinnedMessageReactions, deleteMembers, ]); @@ -211,6 +224,16 @@ abstract class ChatPersistenceClient { ]) .withNullifyer; + final pinnedMessageReactions = cleanedChannelStates + .expand((it) => it.pinnedMessages) + .expand((it) => [ + if (it.ownReactions != null) + ...it.ownReactions!.where((r) => r.userId != null), + if (it.latestReactions != null) + ...it.latestReactions!.where((r) => r.userId != null), + ]) + .withNullifyer; + final users = cleanedChannelStates .map((cs) => [ cs.channel?.createdBy, @@ -261,6 +284,9 @@ abstract class ChatPersistenceClient { updateUsers(users.toList(growable: false)), updateChannels(channels.toList(growable: false)), updateReactions(reactions.toList(growable: false)), + updatePinnedMessageReactions( + pinnedMessageReactions.toList(growable: false), + ), ]); } } diff --git a/packages/stream_chat/test/src/db/chat_persistence_client_test.dart b/packages/stream_chat/test/src/db/chat_persistence_client_test.dart index 22aee1a9..d7de4745 100644 --- a/packages/stream_chat/test/src/db/chat_persistence_client_test.dart +++ b/packages/stream_chat/test/src/db/chat_persistence_client_test.dart @@ -38,6 +38,11 @@ class TestPersistenceClient extends ChatPersistenceClient { Future deleteReactionsByMessageId(List messageIds) => Future.value(); + @override + Future deletePinnedMessageReactionsByMessageId( + List messageIds) => + Future.value(); + @override Future disconnect({bool flush = false}) => throw UnimplementedError(); @@ -116,6 +121,10 @@ class TestPersistenceClient extends ChatPersistenceClient { @override Future updateReactions(List reactions) => Future.value(); + @override + Future updatePinnedMessageReactions(List reactions) => + Future.value(); + @override Future updateReads(String cid, List reads) => Future.value(); From f184da13dbc518d0a4f16eb28a0563b373e6446e Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Tue, 31 Aug 2021 15:52:15 +0530 Subject: [PATCH 6/8] chore(persistence): update schema version Signed-off-by: xsahil03x --- .../lib/src/db/moor_chat_database.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/stream_chat_persistence/lib/src/db/moor_chat_database.dart b/packages/stream_chat_persistence/lib/src/db/moor_chat_database.dart index 741ac957..07f86821 100644 --- a/packages/stream_chat_persistence/lib/src/db/moor_chat_database.dart +++ b/packages/stream_chat_persistence/lib/src/db/moor_chat_database.dart @@ -53,10 +53,13 @@ class MoorChatDatabase extends _$MoorChatDatabase { // you should bump this number whenever you change or add a table definition. @override - int get schemaVersion => 5; + int get schemaVersion => 6; @override MigrationStrategy get migration => MigrationStrategy( + beforeOpen: (details) async { + await customStatement('PRAGMA foreign_keys = ON;'); + }, onUpgrade: (openingDetails, before, after) async { if (before != after) { final m = createMigrator(); @@ -66,9 +69,6 @@ class MoorChatDatabase extends _$MoorChatDatabase { } } }, - beforeOpen: (details) async { - await customStatement('PRAGMA foreign_keys = ON;'); - }, ); /// Deletes all the tables From 7e0997838e1bc06ee45ae83145fdea26779bd73a Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 6 Sep 2021 19:21:28 +0530 Subject: [PATCH 7/8] fix: `updateChannelStates` invocation sequence as per foreign keys relations. Signed-off-by: xsahil03x --- .../lib/src/db/chat_persistence_client.dart | 183 +++++++++--------- .../src/db/chat_persistence_client_test.dart | 28 +-- .../lib/src/dao/member_dao.dart | 20 +- .../lib/src/dao/message_dao.dart | 25 ++- .../lib/src/dao/pinned_message_dao.dart | 25 ++- .../lib/src/dao/read_dao.dart | 19 +- .../lib/src/db/moor_chat_database.dart | 2 +- .../lib/src/db/moor_chat_database.g.dart | 80 ++++---- .../lib/src/entity/members.dart | 5 +- .../lib/src/entity/messages.dart | 4 +- .../lib/src/mapper/message_mapper.dart | 2 +- .../lib/src/mapper/pinned_message_mapper.dart | 3 +- .../src/stream_chat_persistence_client.dart | 24 +-- .../stream_chat_persistence_client_test.dart | 24 ++- 14 files changed, 240 insertions(+), 204 deletions(-) diff --git a/packages/stream_chat/lib/src/db/chat_persistence_client.dart b/packages/stream_chat/lib/src/db/chat_persistence_client.dart index bf2ef3a8..aa0ac245 100644 --- a/packages/stream_chat/lib/src/db/chat_persistence_client.dart +++ b/packages/stream_chat/lib/src/db/chat_persistence_client.dart @@ -143,11 +143,19 @@ abstract class ChatPersistenceClient { /// Updates the message data of a particular channel [cid] with /// the new [messages] data - Future updateMessages(String cid, List messages); + Future updateMessages(String cid, List messages) => + bulkUpdateMessages({cid: messages}); + + /// Bulk updates the message data of multiple channels. + Future bulkUpdateMessages(Map> messages); /// Updates the pinned message data of a particular channel [cid] with /// the new [messages] data - Future updatePinnedMessages(String cid, List messages); + Future updatePinnedMessages(String cid, List messages) => + bulkUpdatePinnedMessages({cid: messages}); + + /// Bulk updates the message data of multiple channels. + Future bulkUpdatePinnedMessages(Map> messages); /// Returns all the threads by parent message of a particular channel by /// providing channel [cid] @@ -158,11 +166,19 @@ abstract class ChatPersistenceClient { /// Updates all the members of a particular channle [cid] /// with the new [members] data - Future updateMembers(String cid, List members); + Future updateMembers(String cid, List members) => + bulkUpdateMembers({cid: members}); + + /// Bulk updates the members data of multiple channels. + Future bulkUpdateMembers(Map> members); /// Updates the read data of a particular channel [cid] with /// the new [reads] data - Future updateReads(String cid, List reads); + Future updateReads(String cid, List reads) => + bulkUpdateReads({cid: reads}); + + /// Bulk updates the read data of multiple channels. + Future bulkUpdateReads(Map> reads); /// Updates the users data with the new [users] data Future updateUsers(List users); @@ -188,104 +204,91 @@ abstract class ChatPersistenceClient { /// Update list of channel states Future updateChannelStates(List channelStates) async { - final deleteReactions = deleteReactionsByMessageId(channelStates - .expand((it) => it.messages) - .map((m) => m.id) - .toList(growable: false)); + final reactionsToDelete = []; + final pinnedReactionsToDelete = []; + final membersToDelete = []; - final deletePinnedMessageReactions = - deletePinnedMessageReactionsByMessageId(channelStates - .expand((it) => it.pinnedMessages) - .map((m) => m.id) - .toList(growable: false)); + final channels = []; + final channelWithMessages = >{}; + final channelWithPinnedMessages = >{}; + final channelWithReads = >{}; + final channelWithMembers = >{}; - final cleanedChannelStates = - channelStates.where((it) => it.channel != null); + final users = []; + final reactions = []; + final pinnedReactions = []; - final deleteMembers = deleteMembersByCids( - cleanedChannelStates.map((it) => it.channel!.cid).toList(growable: false), - ); + for (final state in channelStates) { + final channel = state.channel; + if (channel != null) { + channels.add(channel); + final cid = channel.cid; + final reads = state.read; + final members = state.members; + final messages = state.messages; + final pinnedMessages = state.pinnedMessages; + + // Preparing deletion data + membersToDelete.add(cid); + reactionsToDelete.addAll(state.messages.map((it) => it.id)); + pinnedReactionsToDelete.addAll(state.pinnedMessages.map((it) => it.id)); + + // preparing addition data + channelWithReads[cid] = reads; + channelWithMembers[cid] = members; + channelWithMessages[cid] = messages; + channelWithPinnedMessages[cid] = pinnedMessages; + + List expandReactions(Message message) { + final own = message.ownReactions; + final latest = message.latestReactions; + return [ + if (own != null) ...own.where((r) => r.userId != null), + if (latest != null) ...latest.where((r) => r.userId != null), + ]; + } + + reactions.addAll(messages.expand(expandReactions)); + pinnedReactions.addAll(pinnedMessages.expand(expandReactions)); + + users.addAll([ + channel.createdBy, + ...reads.map((it) => it.user), + ...members.map((it) => it.user), + ...reactions.map((it) => it.user), + ...pinnedReactions.map((it) => it.user), + ].withNullifyer); + } + } + + // Removing old members and reactions data as they may have + // changes over the time. await Future.wait([ - deleteReactions, - deletePinnedMessageReactions, - deleteMembers, + deleteMembersByCids(membersToDelete), + deleteReactionsByMessageId(reactionsToDelete), + deletePinnedMessageReactionsByMessageId(pinnedReactionsToDelete), ]); - final channels = cleanedChannelStates.map((it) => it.channel).withNullifyer; - - final reactions = cleanedChannelStates - .expand((it) => it.messages) - .expand((it) => [ - if (it.ownReactions != null) - ...it.ownReactions!.where((r) => r.userId != null), - if (it.latestReactions != null) - ...it.latestReactions!.where((r) => r.userId != null), - ]) - .withNullifyer; - - final pinnedMessageReactions = cleanedChannelStates - .expand((it) => it.pinnedMessages) - .expand((it) => [ - if (it.ownReactions != null) - ...it.ownReactions!.where((r) => r.userId != null), - if (it.latestReactions != null) - ...it.latestReactions!.where((r) => r.userId != null), - ]) - .withNullifyer; - - final users = cleanedChannelStates - .map((cs) => [ - cs.channel?.createdBy, - ...cs.messages - .map((m) => [ - m.user, - if (m.latestReactions != null) - ...m.latestReactions!.map((r) => r.user), - if (m.ownReactions != null) - ...m.ownReactions!.map((r) => r.user), - ]) - .expand((v) => v), - ...cs.read.map((r) => r.user), - ...cs.members.map((m) => m.user), - ]) - .expand((it) => it) - .withNullifyer; - - final updateMessagesFuture = cleanedChannelStates.map((it) { - final cid = it.channel!.cid; - final messages = it.messages; - return updateMessages(cid, messages.toList(growable: false)); - }).toList(growable: false); - - final updatePinnedMessagesFuture = cleanedChannelStates.map((it) { - final cid = it.channel!.cid; - final messages = it.pinnedMessages; - return updatePinnedMessages(cid, messages.toList(growable: false)); - }).toList(growable: false); - - final updateReadsFuture = cleanedChannelStates.map((it) { - final cid = it.channel!.cid; - final reads = it.read; - return updateReads(cid, reads.toList(growable: false)); - }).toList(growable: false); - - final updateMembersFuture = cleanedChannelStates.map((it) { - final cid = it.channel!.cid; - final members = it.members; - return updateMembers(cid, members.toList(growable: false)); - }).toList(growable: false); - + // Updating first as does not depend on any other table. await Future.wait([ - ...updateMessagesFuture, - ...updatePinnedMessagesFuture, - ...updateReadsFuture, - ...updateMembersFuture, updateUsers(users.toList(growable: false)), updateChannels(channels.toList(growable: false)), + ]); + + // All has a foreign key relation with channels table. + await Future.wait([ + bulkUpdateReads(channelWithReads), + bulkUpdateMembers(channelWithMembers), + bulkUpdateMessages(channelWithMessages), + bulkUpdatePinnedMessages(channelWithPinnedMessages), + ]); + + // Both has a foreign key relation with messages, pinnedMessages table. + await Future.wait([ updateReactions(reactions.toList(growable: false)), updatePinnedMessageReactions( - pinnedMessageReactions.toList(growable: false), + pinnedReactions.toList(growable: false), ), ]); } diff --git a/packages/stream_chat/test/src/db/chat_persistence_client_test.dart b/packages/stream_chat/test/src/db/chat_persistence_client_test.dart index d7de4745..897bbcb0 100644 --- a/packages/stream_chat/test/src/db/chat_persistence_client_test.dart +++ b/packages/stream_chat/test/src/db/chat_persistence_client_test.dart @@ -106,18 +106,6 @@ class TestPersistenceClient extends ChatPersistenceClient { Future updateLastSyncAt(DateTime lastSyncAt) => throw UnimplementedError(); - @override - Future updateMembers(String cid, List members) => - Future.value(); - - @override - Future updateMessages(String cid, List messages) => - Future.value(); - - @override - Future updatePinnedMessages(String cid, List messages) => - Future.value(); - @override Future updateReactions(List reactions) => Future.value(); @@ -126,10 +114,22 @@ class TestPersistenceClient extends ChatPersistenceClient { Future.value(); @override - Future updateReads(String cid, List reads) => Future.value(); + Future updateUsers(List users) => Future.value(); @override - Future updateUsers(List users) => Future.value(); + Future bulkUpdateMembers(Map> members) => + Future.value(); + + @override + Future bulkUpdateMessages(Map> messages) => + Future.value(); + + @override + Future bulkUpdatePinnedMessages(Map> messages) => + Future.value(); + + @override + Future bulkUpdateReads(Map> reads) => Future.value(); } void main() { diff --git a/packages/stream_chat_persistence/lib/src/dao/member_dao.dart b/packages/stream_chat_persistence/lib/src/dao/member_dao.dart index 1f39904c..e471b1b5 100644 --- a/packages/stream_chat_persistence/lib/src/dao/member_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/member_dao.dart @@ -30,13 +30,19 @@ class MemberDao extends DatabaseAccessor }).get(); /// Updates all the members using the new [memberList] data - Future updateMembers(String cid, List memberList) async => - batch( - (it) => it.insertAllOnConflictUpdate( - members, - memberList.map((m) => m.toEntity(cid: cid)).toList(), - ), - ); + Future updateMembers(String cid, List memberList) => + bulkUpdateMembers({cid: memberList}); + + /// Bulk updates the members data of multiple channels + Future bulkUpdateMembers(Map> channelWithMembers) { + final entities = channelWithMembers.entries + .map((entry) => entry.value.map( + (member) => member.toEntity(cid: entry.key), + )) + .expand((it) => it) + .toList(growable: false); + return batch((batch) => batch.insertAllOnConflictUpdate(members, entities)); + } /// Deletes all the members whose [Members.channelCid] is present in [cids] Future deleteMemberByCids(List cids) async => batch((it) { diff --git a/packages/stream_chat_persistence/lib/src/dao/message_dao.dart b/packages/stream_chat_persistence/lib/src/dao/message_dao.dart index eb02e8aa..b7d9a5db 100644 --- a/packages/stream_chat_persistence/lib/src/dao/message_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/message_dao.dart @@ -169,12 +169,21 @@ class MessageDao extends DatabaseAccessor /// Updates the message data of a particular channel with /// the new [messageList] data - Future updateMessages(String cid, List messageList) => batch( - (batch) { - batch.insertAllOnConflictUpdate( - messages, - messageList.map((it) => it.toEntity(cid: cid)).toList(), - ); - }, - ); + Future updateMessages(String cid, List messageList) => + bulkUpdateMessages({cid: messageList}); + + /// Bulk updates the message data of multiple channels + Future bulkUpdateMessages( + Map> channelWithMessages, + ) { + final entities = channelWithMessages.entries + .map((entry) => entry.value.map( + (message) => message.toEntity(cid: entry.key), + )) + .expand((it) => it) + .toList(growable: false); + return batch( + (batch) => batch.insertAllOnConflictUpdate(messages, entities), + ); + } } diff --git a/packages/stream_chat_persistence/lib/src/dao/pinned_message_dao.dart b/packages/stream_chat_persistence/lib/src/dao/pinned_message_dao.dart index 065dc40d..9bc0fa64 100644 --- a/packages/stream_chat_persistence/lib/src/dao/pinned_message_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/pinned_message_dao.dart @@ -170,12 +170,21 @@ class PinnedMessageDao extends DatabaseAccessor /// Updates the message data of a particular channel with /// the new [messageList] data - Future updateMessages(String cid, List messageList) => batch( - (batch) { - batch.insertAllOnConflictUpdate( - pinnedMessages, - messageList.map((it) => it.toPinnedEntity(cid: cid)).toList(), - ); - }, - ); + Future updateMessages(String cid, List messageList) => + bulkUpdateMessages({cid: messageList}); + + /// Bulk updates the message data of multiple channels + Future bulkUpdateMessages( + Map> channelWithMessages, + ) { + final entities = channelWithMessages.entries + .map((entry) => entry.value.map( + (message) => message.toPinnedEntity(cid: entry.key), + )) + .expand((it) => it) + .toList(growable: false); + return batch( + (batch) => batch.insertAllOnConflictUpdate(pinnedMessages, entities), + ); + } } diff --git a/packages/stream_chat_persistence/lib/src/dao/read_dao.dart b/packages/stream_chat_persistence/lib/src/dao/read_dao.dart index 007cb3a7..87bfa19b 100644 --- a/packages/stream_chat_persistence/lib/src/dao/read_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/read_dao.dart @@ -29,10 +29,17 @@ class ReadDao extends DatabaseAccessor with _$ReadDaoMixin { /// Updates the read data of a particular channel with /// the new [readList] data - Future updateReads(String cid, List readList) => batch( - (it) => it.insertAllOnConflictUpdate( - reads, - readList.map((r) => r.toEntity(cid: cid)).toList(), - ), - ); + Future updateReads(String cid, List readList) => + bulkUpdateReads({cid: readList}); + + /// Bulk updates the reads data of multiple channels + Future bulkUpdateReads(Map> channelWithReads) { + final entities = channelWithReads.entries + .map((entry) => entry.value.map( + (read) => read.toEntity(cid: entry.key), + )) + .expand((it) => it) + .toList(growable: false); + return batch((batch) => batch.insertAllOnConflictUpdate(reads, entities)); + } } diff --git a/packages/stream_chat_persistence/lib/src/db/moor_chat_database.dart b/packages/stream_chat_persistence/lib/src/db/moor_chat_database.dart index 07f86821..85f7b2de 100644 --- a/packages/stream_chat_persistence/lib/src/db/moor_chat_database.dart +++ b/packages/stream_chat_persistence/lib/src/db/moor_chat_database.dart @@ -58,7 +58,7 @@ class MoorChatDatabase extends _$MoorChatDatabase { @override MigrationStrategy get migration => MigrationStrategy( beforeOpen: (details) async { - await customStatement('PRAGMA foreign_keys = ON;'); + await customStatement('PRAGMA foreign_keys = ON'); }, onUpgrade: (openingDetails, before, after) async { if (before != after) { diff --git a/packages/stream_chat_persistence/lib/src/db/moor_chat_database.g.dart b/packages/stream_chat_persistence/lib/src/db/moor_chat_database.g.dart index c0991f3e..7360f9ab 100644 --- a/packages/stream_chat_persistence/lib/src/db/moor_chat_database.g.dart +++ b/packages/stream_chat_persistence/lib/src/db/moor_chat_database.g.dart @@ -645,7 +645,7 @@ class MessageEntity extends DataClass implements Insertable { final String? pinnedByUserId; /// The channel cid of which this message is part of - final String? channelCid; + final String channelCid; /// A Map of [messageText] translations. final Map? i18n; @@ -675,7 +675,7 @@ class MessageEntity extends DataClass implements Insertable { this.pinnedAt, this.pinExpires, this.pinnedByUserId, - this.channelCid, + required this.channelCid, this.i18n, this.extraData}); factory MessageEntity.fromData( @@ -728,7 +728,7 @@ class MessageEntity extends DataClass implements Insertable { pinnedByUserId: const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}pinned_by_user_id']), channelCid: const StringType() - .mapFromDatabaseResponse(data['${effectivePrefix}channel_cid']), + .mapFromDatabaseResponse(data['${effectivePrefix}channel_cid'])!, i18n: $MessagesTable.$converter5.mapToDart(const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}i18n'])), extraData: $MessagesTable.$converter6.mapToDart(const StringType() @@ -800,9 +800,7 @@ class MessageEntity extends DataClass implements Insertable { if (!nullToAbsent || pinnedByUserId != null) { map['pinned_by_user_id'] = Variable(pinnedByUserId); } - if (!nullToAbsent || channelCid != null) { - map['channel_cid'] = Variable(channelCid); - } + map['channel_cid'] = Variable(channelCid); if (!nullToAbsent || i18n != null) { final converter = $MessagesTable.$converter5; map['i18n'] = Variable(converter.mapToSql(i18n)); @@ -842,7 +840,7 @@ class MessageEntity extends DataClass implements Insertable { pinnedAt: serializer.fromJson(json['pinnedAt']), pinExpires: serializer.fromJson(json['pinExpires']), pinnedByUserId: serializer.fromJson(json['pinnedByUserId']), - channelCid: serializer.fromJson(json['channelCid']), + channelCid: serializer.fromJson(json['channelCid']), i18n: serializer.fromJson?>(json['i18n']), extraData: serializer.fromJson?>(json['extraData']), ); @@ -873,7 +871,7 @@ class MessageEntity extends DataClass implements Insertable { 'pinnedAt': serializer.toJson(pinnedAt), 'pinExpires': serializer.toJson(pinExpires), 'pinnedByUserId': serializer.toJson(pinnedByUserId), - 'channelCid': serializer.toJson(channelCid), + 'channelCid': serializer.toJson(channelCid), 'i18n': serializer.toJson?>(i18n), 'extraData': serializer.toJson?>(extraData), }; @@ -902,7 +900,7 @@ class MessageEntity extends DataClass implements Insertable { Value pinnedAt = const Value.absent(), Value pinExpires = const Value.absent(), Value pinnedByUserId = const Value.absent(), - Value channelCid = const Value.absent(), + String? channelCid, Value?> i18n = const Value.absent(), Value?> extraData = const Value.absent()}) => MessageEntity( @@ -934,7 +932,7 @@ class MessageEntity extends DataClass implements Insertable { pinExpires: pinExpires.present ? pinExpires.value : this.pinExpires, pinnedByUserId: pinnedByUserId.present ? pinnedByUserId.value : this.pinnedByUserId, - channelCid: channelCid.present ? channelCid.value : this.channelCid, + channelCid: channelCid ?? this.channelCid, i18n: i18n.present ? i18n.value : this.i18n, extraData: extraData.present ? extraData.value : this.extraData, ); @@ -1068,7 +1066,7 @@ class MessagesCompanion extends UpdateCompanion { final Value pinnedAt; final Value pinExpires; final Value pinnedByUserId; - final Value channelCid; + final Value channelCid; final Value?> i18n; final Value?> extraData; const MessagesCompanion({ @@ -1121,12 +1119,13 @@ class MessagesCompanion extends UpdateCompanion { this.pinnedAt = const Value.absent(), this.pinExpires = const Value.absent(), this.pinnedByUserId = const Value.absent(), - this.channelCid = const Value.absent(), + required String channelCid, this.i18n = const Value.absent(), this.extraData = const Value.absent(), }) : id = Value(id), attachments = Value(attachments), - mentionedUsers = Value(mentionedUsers); + mentionedUsers = Value(mentionedUsers), + channelCid = Value(channelCid); static Insertable custom({ Expression? id, Expression? messageText, @@ -1150,7 +1149,7 @@ class MessagesCompanion extends UpdateCompanion { Expression? pinnedAt, Expression? pinExpires, Expression? pinnedByUserId, - Expression? channelCid, + Expression? channelCid, Expression?>? i18n, Expression?>? extraData, }) { @@ -1206,7 +1205,7 @@ class MessagesCompanion extends UpdateCompanion { Value? pinnedAt, Value? pinExpires, Value? pinnedByUserId, - Value? channelCid, + Value? channelCid, Value?>? i18n, Value?>? extraData}) { return MessagesCompanion( @@ -1317,7 +1316,7 @@ class MessagesCompanion extends UpdateCompanion { map['pinned_by_user_id'] = Variable(pinnedByUserId.value); } if (channelCid.present) { - map['channel_cid'] = Variable(channelCid.value); + map['channel_cid'] = Variable(channelCid.value); } if (i18n.present) { final converter = $MessagesTable.$converter5; @@ -1491,11 +1490,10 @@ class $MessagesTable extends Messages typeName: 'TEXT', requiredDuringInsert: false); final VerificationMeta _channelCidMeta = const VerificationMeta('channelCid'); late final GeneratedColumn channelCid = GeneratedColumn( - 'channel_cid', aliasedName, true, + 'channel_cid', aliasedName, false, typeName: 'TEXT', - requiredDuringInsert: false, - $customConstraints: - 'NULLABLE REFERENCES channels(cid) ON DELETE CASCADE'); + requiredDuringInsert: true, + $customConstraints: 'REFERENCES channels(cid) ON DELETE CASCADE'); final VerificationMeta _i18nMeta = const VerificationMeta('i18n'); late final GeneratedColumnWithTypeConverter, String?> i18n = GeneratedColumn('i18n', aliasedName, true, @@ -1634,6 +1632,8 @@ class $MessagesTable extends Messages _channelCidMeta, channelCid.isAcceptableOrUnknown( data['channel_cid']!, _channelCidMeta)); + } else if (isInserting) { + context.missing(_channelCidMeta); } context.handle(_i18nMeta, const VerificationResult.success()); context.handle(_extraDataMeta, const VerificationResult.success()); @@ -1739,7 +1739,7 @@ class PinnedMessageEntity extends DataClass final String? pinnedByUserId; /// The channel cid of which this message is part of - final String? channelCid; + final String channelCid; /// A Map of [messageText] translations. final Map? i18n; @@ -1769,7 +1769,7 @@ class PinnedMessageEntity extends DataClass this.pinnedAt, this.pinExpires, this.pinnedByUserId, - this.channelCid, + required this.channelCid, this.i18n, this.extraData}); factory PinnedMessageEntity.fromData( @@ -1825,7 +1825,7 @@ class PinnedMessageEntity extends DataClass pinnedByUserId: const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}pinned_by_user_id']), channelCid: const StringType() - .mapFromDatabaseResponse(data['${effectivePrefix}channel_cid']), + .mapFromDatabaseResponse(data['${effectivePrefix}channel_cid'])!, i18n: $PinnedMessagesTable.$converter5.mapToDart(const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}i18n'])), extraData: $PinnedMessagesTable.$converter6.mapToDart(const StringType() @@ -1897,9 +1897,7 @@ class PinnedMessageEntity extends DataClass if (!nullToAbsent || pinnedByUserId != null) { map['pinned_by_user_id'] = Variable(pinnedByUserId); } - if (!nullToAbsent || channelCid != null) { - map['channel_cid'] = Variable(channelCid); - } + map['channel_cid'] = Variable(channelCid); if (!nullToAbsent || i18n != null) { final converter = $PinnedMessagesTable.$converter5; map['i18n'] = Variable(converter.mapToSql(i18n)); @@ -1939,7 +1937,7 @@ class PinnedMessageEntity extends DataClass pinnedAt: serializer.fromJson(json['pinnedAt']), pinExpires: serializer.fromJson(json['pinExpires']), pinnedByUserId: serializer.fromJson(json['pinnedByUserId']), - channelCid: serializer.fromJson(json['channelCid']), + channelCid: serializer.fromJson(json['channelCid']), i18n: serializer.fromJson?>(json['i18n']), extraData: serializer.fromJson?>(json['extraData']), ); @@ -1970,7 +1968,7 @@ class PinnedMessageEntity extends DataClass 'pinnedAt': serializer.toJson(pinnedAt), 'pinExpires': serializer.toJson(pinExpires), 'pinnedByUserId': serializer.toJson(pinnedByUserId), - 'channelCid': serializer.toJson(channelCid), + 'channelCid': serializer.toJson(channelCid), 'i18n': serializer.toJson?>(i18n), 'extraData': serializer.toJson?>(extraData), }; @@ -1999,7 +1997,7 @@ class PinnedMessageEntity extends DataClass Value pinnedAt = const Value.absent(), Value pinExpires = const Value.absent(), Value pinnedByUserId = const Value.absent(), - Value channelCid = const Value.absent(), + String? channelCid, Value?> i18n = const Value.absent(), Value?> extraData = const Value.absent()}) => PinnedMessageEntity( @@ -2031,7 +2029,7 @@ class PinnedMessageEntity extends DataClass pinExpires: pinExpires.present ? pinExpires.value : this.pinExpires, pinnedByUserId: pinnedByUserId.present ? pinnedByUserId.value : this.pinnedByUserId, - channelCid: channelCid.present ? channelCid.value : this.channelCid, + channelCid: channelCid ?? this.channelCid, i18n: i18n.present ? i18n.value : this.i18n, extraData: extraData.present ? extraData.value : this.extraData, ); @@ -2165,7 +2163,7 @@ class PinnedMessagesCompanion extends UpdateCompanion { final Value pinnedAt; final Value pinExpires; final Value pinnedByUserId; - final Value channelCid; + final Value channelCid; final Value?> i18n; final Value?> extraData; const PinnedMessagesCompanion({ @@ -2218,12 +2216,13 @@ class PinnedMessagesCompanion extends UpdateCompanion { this.pinnedAt = const Value.absent(), this.pinExpires = const Value.absent(), this.pinnedByUserId = const Value.absent(), - this.channelCid = const Value.absent(), + required String channelCid, this.i18n = const Value.absent(), this.extraData = const Value.absent(), }) : id = Value(id), attachments = Value(attachments), - mentionedUsers = Value(mentionedUsers); + mentionedUsers = Value(mentionedUsers), + channelCid = Value(channelCid); static Insertable custom({ Expression? id, Expression? messageText, @@ -2247,7 +2246,7 @@ class PinnedMessagesCompanion extends UpdateCompanion { Expression? pinnedAt, Expression? pinExpires, Expression? pinnedByUserId, - Expression? channelCid, + Expression? channelCid, Expression?>? i18n, Expression?>? extraData, }) { @@ -2303,7 +2302,7 @@ class PinnedMessagesCompanion extends UpdateCompanion { Value? pinnedAt, Value? pinExpires, Value? pinnedByUserId, - Value? channelCid, + Value? channelCid, Value?>? i18n, Value?>? extraData}) { return PinnedMessagesCompanion( @@ -2414,7 +2413,7 @@ class PinnedMessagesCompanion extends UpdateCompanion { map['pinned_by_user_id'] = Variable(pinnedByUserId.value); } if (channelCid.present) { - map['channel_cid'] = Variable(channelCid.value); + map['channel_cid'] = Variable(channelCid.value); } if (i18n.present) { final converter = $PinnedMessagesTable.$converter5; @@ -2589,11 +2588,10 @@ class $PinnedMessagesTable extends PinnedMessages typeName: 'TEXT', requiredDuringInsert: false); final VerificationMeta _channelCidMeta = const VerificationMeta('channelCid'); late final GeneratedColumn channelCid = GeneratedColumn( - 'channel_cid', aliasedName, true, + 'channel_cid', aliasedName, false, typeName: 'TEXT', - requiredDuringInsert: false, - $customConstraints: - 'NULLABLE REFERENCES channels(cid) ON DELETE CASCADE'); + requiredDuringInsert: true, + $customConstraints: 'REFERENCES channels(cid) ON DELETE CASCADE'); final VerificationMeta _i18nMeta = const VerificationMeta('i18n'); late final GeneratedColumnWithTypeConverter, String?> i18n = GeneratedColumn('i18n', aliasedName, true, @@ -2734,6 +2732,8 @@ class $PinnedMessagesTable extends PinnedMessages _channelCidMeta, channelCid.isAcceptableOrUnknown( data['channel_cid']!, _channelCidMeta)); + } else if (isInserting) { + context.missing(_channelCidMeta); } context.handle(_i18nMeta, const VerificationResult.success()); context.handle(_extraDataMeta, const VerificationResult.success()); diff --git a/packages/stream_chat_persistence/lib/src/entity/members.dart b/packages/stream_chat_persistence/lib/src/entity/members.dart index 8d3d4a57..2773ddaa 100644 --- a/packages/stream_chat_persistence/lib/src/entity/members.dart +++ b/packages/stream_chat_persistence/lib/src/entity/members.dart @@ -39,8 +39,5 @@ class Members extends Table { DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)(); @override - Set get primaryKey => { - userId, - channelCid, - }; + Set get primaryKey => {userId, channelCid}; } diff --git a/packages/stream_chat_persistence/lib/src/entity/messages.dart b/packages/stream_chat_persistence/lib/src/entity/messages.dart index 0aef2b84..108b903f 100644 --- a/packages/stream_chat_persistence/lib/src/entity/messages.dart +++ b/packages/stream_chat_persistence/lib/src/entity/messages.dart @@ -77,8 +77,8 @@ class Messages extends Table { TextColumn get pinnedByUserId => text().nullable()(); /// The channel cid of which this message is part of - TextColumn get channelCid => text().nullable().customConstraint( - 'NULLABLE REFERENCES channels(cid) ON DELETE CASCADE')(); + TextColumn get channelCid => + text().customConstraint('REFERENCES channels(cid) ON DELETE CASCADE')(); /// A Map of [messageText] translations. TextColumn get i18n => text().nullable().map(MapConverter())(); diff --git a/packages/stream_chat_persistence/lib/src/mapper/message_mapper.dart b/packages/stream_chat_persistence/lib/src/mapper/message_mapper.dart index 85692296..f49631a9 100644 --- a/packages/stream_chat_persistence/lib/src/mapper/message_mapper.dart +++ b/packages/stream_chat_persistence/lib/src/mapper/message_mapper.dart @@ -51,7 +51,7 @@ extension MessageEntityX on MessageEntity { /// Useful mapping functions for [Message] extension MessageX on Message { /// Maps a [Message] into [MessageEntity] - MessageEntity toEntity({String? cid}) => MessageEntity( + MessageEntity toEntity({required String cid}) => MessageEntity( id: id, attachments: attachments.map((it) => jsonEncode(it.toData())).toList(), channelCid: cid, diff --git a/packages/stream_chat_persistence/lib/src/mapper/pinned_message_mapper.dart b/packages/stream_chat_persistence/lib/src/mapper/pinned_message_mapper.dart index b1a7849a..d47aa109 100644 --- a/packages/stream_chat_persistence/lib/src/mapper/pinned_message_mapper.dart +++ b/packages/stream_chat_persistence/lib/src/mapper/pinned_message_mapper.dart @@ -51,7 +51,8 @@ extension PinnedMessageEntityX on PinnedMessageEntity { /// Useful mapping functions for [Message] extension PMessageX on Message { /// Maps a [Message] into [PinnedMessageEntity] - PinnedMessageEntity toPinnedEntity({String? cid}) => PinnedMessageEntity( + PinnedMessageEntity toPinnedEntity({required String cid}) => + PinnedMessageEntity( id: id, attachments: attachments.map((it) => jsonEncode(it.toData())).toList(), channelCid: cid, diff --git a/packages/stream_chat_persistence/lib/src/stream_chat_persistence_client.dart b/packages/stream_chat_persistence/lib/src/stream_chat_persistence_client.dart index 5df7a124..d7db7924 100644 --- a/packages/stream_chat_persistence/lib/src/stream_chat_persistence_client.dart +++ b/packages/stream_chat_persistence/lib/src/stream_chat_persistence_client.dart @@ -296,25 +296,25 @@ class StreamChatPersistenceClient extends ChatPersistenceClient { } @override - Future updateMembers(String cid, List members) { + Future bulkUpdateMembers(Map> members) { assert(_debugIsConnected, ''); - _logger.info('updateMembers'); - return _readProtected(() => db!.memberDao.updateMembers(cid, members)); + _logger.info('bulkUpdateMembers'); + return _readProtected(() => db!.memberDao.bulkUpdateMembers(members)); } @override - Future updateMessages(String cid, List messages) { + Future bulkUpdateMessages(Map> messages) { assert(_debugIsConnected, ''); - _logger.info('updateMessages'); - return _readProtected(() => db!.messageDao.updateMessages(cid, messages)); + _logger.info('bulkUpdateMessages'); + return _readProtected(() => db!.messageDao.bulkUpdateMessages(messages)); } @override - Future updatePinnedMessages(String cid, List messages) { + Future bulkUpdatePinnedMessages(Map> messages) { assert(_debugIsConnected, ''); - _logger.info('updatePinnedMessages'); + _logger.info('bulkUpdatePinnedMessages'); return _readProtected( - () => db!.pinnedMessageDao.updateMessages(cid, messages), + () => db!.pinnedMessageDao.bulkUpdateMessages(messages), ); } @@ -335,10 +335,10 @@ class StreamChatPersistenceClient extends ChatPersistenceClient { } @override - Future updateReads(String cid, List reads) { + Future bulkUpdateReads(Map> reads) { assert(_debugIsConnected, ''); - _logger.info('updateReads'); - return _readProtected(() => db!.readDao.updateReads(cid, reads)); + _logger.info('bulkUpdateReads'); + return _readProtected(() => db!.readDao.bulkUpdateReads(reads)); } @override diff --git a/packages/stream_chat_persistence/test/stream_chat_persistence_client_test.dart b/packages/stream_chat_persistence/test/stream_chat_persistence_client_test.dart index 37e82e93..25e926b1 100644 --- a/packages/stream_chat_persistence/test/stream_chat_persistence_client_test.dart +++ b/packages/stream_chat_persistence/test/stream_chat_persistence_client_test.dart @@ -397,23 +397,26 @@ void main() { test('updateMessages', () async { const cid = 'testCid'; final messages = List.generate(3, (index) => Message()); - when(() => mockDatabase.messageDao.updateMessages(cid, messages)) + + when(() => mockDatabase.messageDao.bulkUpdateMessages({cid: messages})) .thenAnswer((_) => Future.value()); await client.updateMessages(cid, messages); - verify(() => mockDatabase.messageDao.updateMessages(cid, messages)) + verify(() => mockDatabase.messageDao.bulkUpdateMessages({cid: messages})) .called(1); }); test('updatePinnedMessages', () async { const cid = 'testCid'; final messages = List.generate(3, (index) => Message()); - when(() => mockDatabase.pinnedMessageDao.updateMessages(cid, messages)) - .thenAnswer((_) => Future.value()); + when( + () => mockDatabase.pinnedMessageDao.bulkUpdateMessages({cid: messages}), + ).thenAnswer((_) => Future.value()); await client.updatePinnedMessages(cid, messages); - verify(() => mockDatabase.pinnedMessageDao.updateMessages(cid, messages)) - .called(1); + verify( + () => mockDatabase.pinnedMessageDao.bulkUpdateMessages({cid: messages}), + ).called(1); }); test('getChannelThreads', () async { @@ -456,11 +459,11 @@ void main() { test('updateMembers', () async { const cid = 'testCid'; final members = List.generate(3, (index) => Member()); - when(() => mockDatabase.memberDao.updateMembers(cid, members)) + when(() => mockDatabase.memberDao.bulkUpdateMembers({cid: members})) .thenAnswer((_) => Future.value()); await client.updateMembers(cid, members); - verify(() => mockDatabase.memberDao.updateMembers(cid, members)) + verify(() => mockDatabase.memberDao.bulkUpdateMembers({cid: members})) .called(1); }); @@ -473,11 +476,12 @@ void main() { lastRead: DateTime.now(), ), ); - when(() => mockDatabase.readDao.updateReads(cid, reads)) + when(() => mockDatabase.readDao.bulkUpdateReads({cid: reads})) .thenAnswer((_) => Future.value()); await client.updateReads(cid, reads); - verify(() => mockDatabase.readDao.updateReads(cid, reads)).called(1); + verify(() => mockDatabase.readDao.bulkUpdateReads({cid: reads})) + .called(1); }); test('updateUsers', () async { From 885c1f2cae5140fa6b8913c60296a85287ca688e Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Tue, 7 Sep 2021 13:10:38 +0530 Subject: [PATCH 8/8] chore(llc, persistence): update CHANGELOG.md Signed-off-by: xsahil03x --- packages/stream_chat/CHANGELOG.md | 33 +++++++++++++++---- packages/stream_chat_persistence/CHANGELOG.md | 10 ++++++ 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index 67a80e17..85bf5956 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -1,3 +1,15 @@ +## Upcoming + +🛑️ Breaking Changes from `2.2.1` + +- Added 6 new methods in `ChatPersistenceClient`. + - `bulkUpdateMessages` + - `bulkUpdatePinnedMessages` + - `bulkUpdateMembers` + - `bulkUpdateReads` + - `updatePinnedMessageReactions` + - `deletePinnedMessageReactionsByMessageId` + ## 2.2.1 🐞 Fixed @@ -13,11 +25,16 @@ ✅ Added -- `User` and `OwnUser` classes now have an `image` property. Setting an image will also set the 'image' key on `extraData`, so `user.image` and `user.extraData['image']` is the same. -- `User` and `OwnUser` classes now have a `name` property. Setting a name will also set the 'name' key on `extraData`, so `user.name` and `user.extraData['name']` is the same. -- `Channel` class now has extra `image` getter and setter. As well as an `updateImage` to do a partial update after a channel has been initialized. -- `Channel` class now has extra `name` getter and setter. As well as an `updateName` to do a partial update after a channel has been initialized. +- `User` and `OwnUser` classes now have an `image` property. Setting an image will also set the 'image' key + on `extraData`, so `user.image` and `user.extraData['image']` is the same. +- `User` and `OwnUser` classes now have a `name` property. Setting a name will also set the 'name' key on `extraData`, + so `user.name` and `user.extraData['name']` is the same. +- `Channel` class now has extra `image` getter and setter. As well as an `updateImage` to do a partial update after a + channel has been initialized. +- `Channel` class now has extra `name` getter and setter. As well as an `updateName` to do a partial update after a + channel has been initialized. - Added slow mode which allows a cooldown period after a user sends a message. + ## 2.1.1 🐞 Fixed @@ -42,7 +59,7 @@ 🐞 Fixed -- [#563](https://github.com/GetStream/stream-chat-flutter/issues/563): `Channel.stopWatching()` not working +- [#563](https://github.com/GetStream/stream-chat-flutter/issues/563): `Channel.stopWatching()` not working - [#575](https://github.com/GetStream/stream-chat-flutter/issues/575): Wrong `OwnUser.*` ## 2.0.0 @@ -70,11 +87,11 @@ 🐞 Fixed -- [#369](https://github.com/GetStream/stream-chat-flutter/issues/369): Client does not return without internet connection +- [#369](https://github.com/GetStream/stream-chat-flutter/issues/369): Client does not return without internet + connection - several minor fixes - performance improvements - ✅ Added - New `Location` enum is introduced for easily changing the client location/baseUrl. @@ -89,6 +106,7 @@ ## 2.0.0-nullsafety.8 🐞 Fixed + - Export `PushProvider` enum ## 2.0.0-nullsafety.7 @@ -124,6 +142,7 @@ - Fix thread reply not working with attachments - Minor fixes + ## 2.0.0-nullsafety.5 - Minor fixes diff --git a/packages/stream_chat_persistence/CHANGELOG.md b/packages/stream_chat_persistence/CHANGELOG.md index 85b9e486..139056bd 100644 --- a/packages/stream_chat_persistence/CHANGELOG.md +++ b/packages/stream_chat_persistence/CHANGELOG.md @@ -1,3 +1,10 @@ +## Upcoming + +- [[#604]](https://github.com/GetStream/stream-chat-flutter/issues/604) Fix cascade deletion by + enabling `pragma foreign_keys`. +- Added a new table `PinnedMessageReactions` and dao `PinnedMessageReactionDao` specifically for pinned messages. +- Updated `stream_chat`: `^2.2.0` -> `TODO` + ## 2.2.0 - Updated llc dependency @@ -11,14 +18,17 @@ ## 2.1.0 ✅ Added + - Added support for `Message.i18n` - Added support for `User.language` ## 2.0.0 + * Migrate this package to null safety * Minor fixes and improvements ## 2.0.0-nullsafety.8 + * Updated llc dependency * Upgraded moor dependencies and generated files with the latest dependency