feat(persistence): add pinned message reactions table

This commit is contained in:
Dmitry Zhifarsky
2021-08-28 10:04:05 +03:00
parent 68311a6c6c
commit 5db0efadba
16 changed files with 792 additions and 17 deletions
@@ -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';
@@ -39,8 +39,10 @@ class PinnedMessageDao extends DatabaseAccessor<MoorChatDatabase>
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,
);
@@ -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<MoorChatDatabase>
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<List<Reaction>> 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<List<Reaction>> 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<void> updateReactions(List<Reaction> reactionList) => batch((it) {
it.insertAllOnConflictUpdate(
pinnedMessageReactions,
reactionList.map((r) => r.toPinnedEntity()).toList(),
);
});
/// Deletes all the reactions whose [Reactions.messageId] is
/// present in [messageIds]
Future<void> deleteReactionsByMessageIds(List<String> messageIds) =>
batch((it) {
it.deleteWhere<PinnedMessageReactions, PinnedMessageReactionEntity>(
pinnedMessageReactions,
(r) => r.messageId.isIn(messageIds),
);
});
}
@@ -0,0 +1,13 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'pinned_message_reaction_dao.dart';
// **************************************************************************
// DaoGenerator
// **************************************************************************
mixin _$PinnedMessageReactionDaoMixin on DatabaseAccessor<MoorChatDatabase> {
$PinnedMessageReactionsTable get pinnedMessageReactions =>
attachedDatabase.pinnedMessageReactions;
$UsersTable get users => attachedDatabase.users;
}
@@ -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,
@@ -2769,6 +2769,340 @@ class $PinnedMessagesTable extends PinnedMessages
MapConverter<Object?>();
}
class PinnedMessageReactionEntity extends DataClass
implements Insertable<PinnedMessageReactionEntity> {
/// 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<String, Object?>? extraData;
PinnedMessageReactionEntity(
{required this.userId,
required this.messageId,
required this.type,
required this.createdAt,
required this.score,
this.extraData});
factory PinnedMessageReactionEntity.fromData(
Map<String, dynamic> 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<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
map['user_id'] = Variable<String>(userId);
map['message_id'] = Variable<String>(messageId);
map['type'] = Variable<String>(type);
map['created_at'] = Variable<DateTime>(createdAt);
map['score'] = Variable<int>(score);
if (!nullToAbsent || extraData != null) {
final converter = $PinnedMessageReactionsTable.$converter0;
map['extra_data'] = Variable<String?>(converter.mapToSql(extraData));
}
return map;
}
factory PinnedMessageReactionEntity.fromJson(Map<String, dynamic> json,
{ValueSerializer? serializer}) {
serializer ??= moorRuntimeOptions.defaultSerializer;
return PinnedMessageReactionEntity(
userId: serializer.fromJson<String>(json['userId']),
messageId: serializer.fromJson<String>(json['messageId']),
type: serializer.fromJson<String>(json['type']),
createdAt: serializer.fromJson<DateTime>(json['createdAt']),
score: serializer.fromJson<int>(json['score']),
extraData: serializer.fromJson<Map<String, Object?>?>(json['extraData']),
);
}
@override
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
serializer ??= moorRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'userId': serializer.toJson<String>(userId),
'messageId': serializer.toJson<String>(messageId),
'type': serializer.toJson<String>(type),
'createdAt': serializer.toJson<DateTime>(createdAt),
'score': serializer.toJson<int>(score),
'extraData': serializer.toJson<Map<String, Object?>?>(extraData),
};
}
PinnedMessageReactionEntity copyWith(
{String? userId,
String? messageId,
String? type,
DateTime? createdAt,
int? score,
Value<Map<String, Object?>?> 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<PinnedMessageReactionEntity> {
final Value<String> userId;
final Value<String> messageId;
final Value<String> type;
final Value<DateTime> createdAt;
final Value<int> score;
final Value<Map<String, Object?>?> 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<PinnedMessageReactionEntity> custom({
Expression<String>? userId,
Expression<String>? messageId,
Expression<String>? type,
Expression<DateTime>? createdAt,
Expression<int>? score,
Expression<Map<String, Object?>?>? 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<String>? userId,
Value<String>? messageId,
Value<String>? type,
Value<DateTime>? createdAt,
Value<int>? score,
Value<Map<String, Object?>?>? 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<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
if (userId.present) {
map['user_id'] = Variable<String>(userId.value);
}
if (messageId.present) {
map['message_id'] = Variable<String>(messageId.value);
}
if (type.present) {
map['type'] = Variable<String>(type.value);
}
if (createdAt.present) {
map['created_at'] = Variable<DateTime>(createdAt.value);
}
if (score.present) {
map['score'] = Variable<int>(score.value);
}
if (extraData.present) {
final converter = $PinnedMessageReactionsTable.$converter0;
map['extra_data'] =
Variable<String?>(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<String?> userId = GeneratedColumn<String?>(
'user_id', aliasedName, false,
typeName: 'TEXT', requiredDuringInsert: true);
final VerificationMeta _messageIdMeta = const VerificationMeta('messageId');
late final GeneratedColumn<String?> messageId = GeneratedColumn<String?>(
'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<String?> type = GeneratedColumn<String?>(
'type', aliasedName, false,
typeName: 'TEXT', requiredDuringInsert: true);
final VerificationMeta _createdAtMeta = const VerificationMeta('createdAt');
late final GeneratedColumn<DateTime?> createdAt = GeneratedColumn<DateTime?>(
'created_at', aliasedName, false,
typeName: 'INTEGER',
requiredDuringInsert: false,
defaultValue: currentDateAndTime);
final VerificationMeta _scoreMeta = const VerificationMeta('score');
late final GeneratedColumn<int?> score = GeneratedColumn<int?>(
'score', aliasedName, false,
typeName: 'INTEGER',
requiredDuringInsert: false,
defaultValue: const Constant(0));
final VerificationMeta _extraDataMeta = const VerificationMeta('extraData');
late final GeneratedColumnWithTypeConverter<Map<String, Object?>, String?>
extraData = GeneratedColumn<String?>('extra_data', aliasedName, true,
typeName: 'TEXT', requiredDuringInsert: false)
.withConverter<Map<String, Object?>>(
$PinnedMessageReactionsTable.$converter0);
@override
List<GeneratedColumn> 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<PinnedMessageReactionEntity> 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<GeneratedColumn> get $primaryKey => {messageId, type, userId};
@override
PinnedMessageReactionEntity map(Map<String, dynamic> data,
{String? tablePrefix}) {
return PinnedMessageReactionEntity.fromData(data, _db,
prefix: tablePrefix != null ? '$tablePrefix.' : null);
}
@override
$PinnedMessageReactionsTable createAlias(String alias) {
return $PinnedMessageReactionsTable(_db, alias);
}
static TypeConverter<Map<String, Object?>, String> $converter0 =
MapConverter<Object?>();
}
class ReactionEntity extends DataClass implements Insertable<ReactionEntity> {
/// 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,
@@ -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';
@@ -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')();
}
@@ -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';
@@ -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,
);
}
@@ -318,6 +318,15 @@ class StreamChatPersistenceClient extends ChatPersistenceClient {
);
}
@override
Future<void> updatePinnedMessageReactions(List<Reaction> reactions) {
assert(_debugIsConnected, '');
_logger.info('updatePinnedMessageReactions');
return _readProtected(
() => db!.pinnedMessageReactionDao.updateReactions(reactions),
);
}
@override
Future<void> updateReactions(List<Reaction> reactions) {
assert(_debugIsConnected, '');
@@ -339,6 +348,18 @@ class StreamChatPersistenceClient extends ChatPersistenceClient {
return _readProtected(() => db!.userDao.updateUsers(users));
}
@override
Future<void> deletePinnedMessageReactionsByMessageId(
List<String> messageIds,
) {
assert(_debugIsConnected, '');
_logger.info('deletePinnedMessageReactionsByMessageId');
return _readProtected(
() =>
db!.pinnedMessageReactionDao.deleteReactionsByMessageIds(messageIds),
);
}
@override
Future<void> deleteReactionsByMessageId(List<String> messageIds) {
assert(_debugIsConnected, '');
@@ -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 {}
@@ -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);
},
);
@@ -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<List<Reaction>> _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();
});
}
@@ -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<Reaction>());
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<PinnedMessageReactionEntity>());
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);
});
}
@@ -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 = <String>[];
when(() =>
@@ -514,6 +529,17 @@ void main() {
.called(1);
});
test('deletePinnedMessageReactionsByMessageId', () async {
final messageIds = <String>[];
when(() => mockDatabase.pinnedMessageReactionDao
.deleteReactionsByMessageIds(messageIds))
.thenAnswer((_) => Future.value());
await client.deletePinnedMessageReactionsByMessageId(messageIds);
verify(() => mockDatabase.pinnedMessageReactionDao
.deleteReactionsByMessageIds(messageIds)).called(1);
});
test('deleteMembersByCids', () async {
final cids = <String>[];
when(() => mockDatabase.memberDao.deleteMemberByCids(cids))