diff --git a/catalyst_voices/apps/voices/lib/configs/bootstrap.dart b/catalyst_voices/apps/voices/lib/configs/bootstrap.dart index 6a1848cacef8..a1a9ee53d5ae 100644 --- a/catalyst_voices/apps/voices/lib/configs/bootstrap.dart +++ b/catalyst_voices/apps/voices/lib/configs/bootstrap.dart @@ -182,8 +182,8 @@ Future cleanUpStorages({ Future cleanUpUserDataFromDatabase() async { final db = Dependencies.instance.get(); - await db.draftsDao.deleteWhere(); - await db.favoritesDao.deleteAll(); + await db.localDocumentsV2Dao.deleteWhere(); + await db.localMetadataDao.deleteWhere(); } @visibleForTesting diff --git a/catalyst_voices/apps/voices/lib/dependency/dependencies.dart b/catalyst_voices/apps/voices/lib/dependency/dependencies.dart index 22642094b4cf..8b4bb9158db3 100644 --- a/catalyst_voices/apps/voices/lib/dependency/dependencies.dart +++ b/catalyst_voices/apps/voices/lib/dependency/dependencies.dart @@ -165,6 +165,7 @@ final class Dependencies extends DependencyProvider { get(), get(), get(), + get(), get(), get(), get(), @@ -261,11 +262,6 @@ final class Dependencies extends DependencyProvider { get(), ); }) - ..registerLazySingleton(() { - return DatabaseDocumentFavoriteSource( - get(), - ); - }) ..registerLazySingleton(() { return CatGatewayDocumentDataSource( get(), @@ -285,7 +281,6 @@ final class Dependencies extends DependencyProvider { get(), get(), get(), - get(), ); }) ..registerLazySingleton(() => const DocumentMapperImpl()) diff --git a/catalyst_voices/docs/performance/proposals_query.csv b/catalyst_voices/docs/performance/proposals_query.csv index 88039092a5fc..c117d4437f94 100644 --- a/catalyst_voices/docs/performance/proposals_query.csv +++ b/catalyst_voices/docs/performance/proposals_query.csv @@ -34,4 +34,13 @@ docs_count ,filer ,avg_duration ,PR ,note 7008 ,categories:finals ,0:00:01.294485 ,#3622 ,- 14008 ,categories ,0:00:02.108506 ,#3622 ,- 14008 ,categories:drafts ,0:00:01.585000 ,#3622 ,- -14008 ,categories:finals ,0:00:05.024950 ,#3622 ,- \ No newline at end of file +14008 ,categories:finals ,0:00:05.024950 ,#3622 ,- +712 ,categories ,0:00:00.139314 ,#3747 ,- +712 ,categories:drafts ,0:00:00.138364 ,#3747 ,- +712 ,categories:finals ,0:00:00.148680 ,#3747 ,- +7008 ,categories ,0:00:00.168405 ,#3747 ,- +7008 ,categories:drafts ,0:00:00.190470 ,#3747 ,- +7008 ,categories:finals ,0:00:00.167145 ,#3747 ,- +14008 ,categories ,0:00:00.253610 ,#3747 ,- +14008 ,categories:drafts ,0:00:00.266390 ,#3747 ,- +14008 ,categories:finals ,0:00:00.247054 ,#3747 ,- \ No newline at end of file diff --git a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/proposal/proposal_cubit.dart b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/proposal/proposal_cubit.dart index c6916004fa72..47a1eebbf7b1 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/proposal/proposal_cubit.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/proposal/proposal_cubit.dart @@ -25,6 +25,7 @@ final class ProposalCubit extends Cubit final ProposalService _proposalService; final CommentService _commentService; final CampaignService _campaignService; + final DocumentsService _documentsService; final DocumentMapper _documentMapper; final VotingBallotBuilder _ballotBuilder; final VotingService _votingService; @@ -40,6 +41,7 @@ final class ProposalCubit extends Cubit this._proposalService, this._commentService, this._campaignService, + this._documentsService, this._documentMapper, this._ballotBuilder, this._votingService, @@ -89,7 +91,7 @@ final class ProposalCubit extends Cubit final commentTemplate = await _commentService.getCommentTemplateFor( category: proposal.document.metadata.categoryId, ); - final isFavorite = await _proposalService.watchIsFavoritesProposal(ref: ref).first; + final isFavorite = await _documentsService.isFavorite(ref); _cache = _cache.copyWith( proposal: Optional(proposal), diff --git a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/proposal_builder/proposal_builder_bloc.dart b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/proposal_builder/proposal_builder_bloc.dart index 47075ced6493..7917f9b35b2f 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/proposal_builder/proposal_builder_bloc.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/proposal_builder/proposal_builder_bloc.dart @@ -941,12 +941,22 @@ final class ProposalBuilderBloc extends Bloc _randomBytes(int length) { - return List.generate(length, (index) => _random.nextInt(256)); - } - - static DateTime _randomDateTime() { - final timestamp = _timestamp; - _timestamp++; - - return DateTime.fromMillisecondsSinceEpoch(timestamp); - } -} - -abstract final class DocumentWithMetadataFactory { - static DocumentEntityWithMetadata build({ - DocumentDataContent? content, - DocumentDataMetadata? metadata, - DateTime? createdAt, - }) { - final document = DocumentFactory.build( - content: content, - metadata: metadata, - createdAt: createdAt, - ); - - final documentMetadata = DocumentMetadataFieldKey.values.map((fieldKey) { - return switch (fieldKey) { - DocumentMetadataFieldKey.title => DocumentMetadataFactory.build( - ver: document.metadata.version, - fieldKey: fieldKey, - - fieldValue: 'Document[${document.metadata.version}] title', - ), - }; - }).toList(); - - return (document: document, metadata: documentMetadata); - } -} - -abstract final class DraftFactory { - static DocumentDraftEntity build({ - DocumentDataContent? content, - DocumentDataMetadata? metadata, - String? title, - }) { - content ??= const DocumentDataContent({}); - - metadata ??= DocumentDataMetadata( - type: DocumentType.proposalDocument, - selfRef: DocumentRefFactory.draftRef(), - ); - - title ??= 'Draft[${metadata.id}] title'; - - final id = UuidHiLo.from(metadata.id); - final ver = UuidHiLo.from(metadata.version); - - return DocumentDraftEntity( - idHi: id.high, - idLo: id.low, - verHi: ver.high, - verLo: ver.low, - type: metadata.type, - content: content, - metadata: metadata, - title: title, - ); - } -} diff --git a/catalyst_voices/packages/internal/catalyst_voices_dev/lib/src/catalyst_voices_models/document/document_ref_factory.dart b/catalyst_voices/packages/internal/catalyst_voices_dev/lib/src/catalyst_voices_models/document/document_ref_factory.dart new file mode 100644 index 000000000000..8e3db8bb34f1 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_dev/lib/src/catalyst_voices_models/document/document_ref_factory.dart @@ -0,0 +1,44 @@ +import 'dart:math'; + +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:flutter/foundation.dart'; +import 'package:uuid_plus/uuid_plus.dart'; + +abstract final class DocumentRefFactory { + static final Random _random = Random(57342052346526); + static var _timestamp = DateTime.now().millisecondsSinceEpoch; + + static DraftRef draftRef() { + return DraftRef.first(randomUuidV7()); + } + + static String randomUuidV7() { + return const UuidV7().generate( + options: V7Options( + _randomDateTime().millisecondsSinceEpoch, + _randomBytes(10), + ), + ); + } + + static SignedDocumentRef signedDocumentRef() { + return SignedDocumentRef.first(randomUuidV7()); + } + + static String uuidV7At(DateTime dateTime) { + final ts = dateTime.millisecondsSinceEpoch; + final rand = Uint8List.fromList([42, 0, 0, 0, 0, 0, 0, 0, 0, 0]); + return const UuidV7().generate(options: V7Options(ts, rand)); + } + + static List _randomBytes(int length) { + return List.generate(length, (index) => _random.nextInt(256)); + } + + static DateTime _randomDateTime() { + final timestamp = _timestamp; + _timestamp++; + + return DateTime.fromMillisecondsSinceEpoch(timestamp); + } +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/catalyst_voices_models.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/catalyst_voices_models.dart index 58fbc1b94f41..fa6f7e043dc7 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/catalyst_voices_models.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/catalyst_voices_models.dart @@ -97,9 +97,6 @@ export 'proposal/proposal_or_document.dart'; export 'proposal/proposal_version.dart'; export 'proposal/proposal_votes.dart'; export 'proposal/proposal_with_context.dart'; -export 'proposals/proposals_count.dart'; -export 'proposals/proposals_count_filters.dart'; -export 'proposals/proposals_filters.dart'; export 'proposals/proposals_filters_v2.dart'; export 'proposals/proposals_order.dart'; export 'proposals/proposals_total_ask_filters.dart'; diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/document_ref.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/document_ref.dart index fffb38acfda4..f105d5e6712a 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/document_ref.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/document_ref.dart @@ -42,6 +42,8 @@ sealed class DocumentRef extends Equatable implements Comparable { /// Whether the ref specifies the document [version]. bool get isExact => version != null; + bool get isLoose => !isExact; + @override List get props => [id, version]; diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposals/proposals_count.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposals/proposals_count.dart deleted file mode 100644 index ed6f7eae9864..000000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposals/proposals_count.dart +++ /dev/null @@ -1,85 +0,0 @@ -import 'package:catalyst_voices_models/catalyst_voices_models.dart' show ProposalsFilterType; -import 'package:equatable/equatable.dart'; - -final class ProposalsCount extends Equatable { - final int total; - final int drafts; - final int finals; - final int favorites; - final int favoritesFinals; - final int my; - final int myFinals; - final int voted; - - const ProposalsCount({ - this.total = 0, - this.drafts = 0, - this.finals = 0, - this.favorites = 0, - this.favoritesFinals = 0, - this.my = 0, - this.myFinals = 0, - this.voted = 0, - }); - - @override - List get props => [ - total, - drafts, - finals, - favorites, - favoritesFinals, - my, - myFinals, - voted, - ]; - - ProposalsCount copyWith({ - int? total, - int? drafts, - int? finals, - int? favorites, - int? favoritesFinals, - int? my, - int? myFinals, - int? voted, - }) { - return ProposalsCount( - total: total ?? this.total, - drafts: drafts ?? this.drafts, - finals: finals ?? this.finals, - favorites: favorites ?? this.favorites, - favoritesFinals: favoritesFinals ?? this.favoritesFinals, - my: my ?? this.my, - myFinals: myFinals ?? this.myFinals, - voted: voted ?? this.voted, - ); - } - - int ofType(ProposalsFilterType type) { - return switch (type) { - ProposalsFilterType.total => total, - ProposalsFilterType.drafts => drafts, - ProposalsFilterType.finals => finals, - ProposalsFilterType.favorites => favorites, - ProposalsFilterType.favoritesFinals => favoritesFinals, - ProposalsFilterType.my => my, - ProposalsFilterType.myFinals => myFinals, - ProposalsFilterType.voted => voted, - }; - } - - @override - String toString() { - return 'ProposalsCount(' - 'total[$total], ' - 'drafts[$drafts], ' - 'finals[$finals], ' - 'favorites[$favorites], ' - 'favoritesFinals[$favoritesFinals], ' - 'my[$my], ' - 'myFinals[$myFinals], ' - 'voted[$voted]' - ')'; - } -} diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposals/proposals_count_filters.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposals/proposals_count_filters.dart deleted file mode 100644 index 5e7bc2827696..000000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposals/proposals_count_filters.dart +++ /dev/null @@ -1,30 +0,0 @@ -import 'package:catalyst_voices_models/catalyst_voices_models.dart'; -import 'package:equatable/equatable.dart'; - -final class ProposalsCountFilters extends Equatable { - final CatalystId? author; - final bool? onlyAuthor; - final SignedDocumentRef? category; - final String? searchQuery; - final Duration? maxAge; - final CampaignFilters? campaign; - - const ProposalsCountFilters({ - this.author, - this.onlyAuthor, - this.category, - this.searchQuery, - this.maxAge, - this.campaign, - }); - - @override - List get props => [ - author, - onlyAuthor, - category, - searchQuery, - maxAge, - campaign, - ]; -} diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposals/proposals_filters.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposals/proposals_filters.dart deleted file mode 100644 index 9eded976ca98..000000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposals/proposals_filters.dart +++ /dev/null @@ -1,101 +0,0 @@ -import 'package:catalyst_voices_models/catalyst_voices_models.dart'; -import 'package:equatable/equatable.dart'; - -final class ProposalsFilters extends Equatable { - final ProposalsFilterType type; - final CatalystId? author; - final bool? onlyAuthor; - final SignedDocumentRef? category; - final String? searchQuery; - final Duration? maxAge; - final CampaignFilters? campaign; - - const ProposalsFilters({ - this.type = ProposalsFilterType.total, - this.author, - this.onlyAuthor, - this.category, - this.searchQuery, - this.maxAge, - this.campaign, - }); - - ProposalsFilters.forActiveCampaign({ - this.type = ProposalsFilterType.total, - this.author, - this.onlyAuthor, - this.category, - this.searchQuery, - this.maxAge, - }) : campaign = CampaignFilters.active(); - - @override - List get props => [ - type, - author, - onlyAuthor, - category, - searchQuery, - maxAge, - campaign, - ]; - - ProposalsFilters copyWith({ - ProposalsFilterType? type, - Optional? author, - Optional? onlyAuthor, - Optional? category, - Optional? searchQuery, - Optional? maxAge, - Optional? campaign, - }) { - return ProposalsFilters( - type: type ?? this.type, - author: author.dataOr(this.author), - onlyAuthor: onlyAuthor.dataOr(this.onlyAuthor), - category: category.dataOr(this.category), - searchQuery: searchQuery.dataOr(this.searchQuery), - maxAge: maxAge.dataOr(this.maxAge), - campaign: campaign.dataOr(this.campaign), - ); - } - - ProposalsCountFilters toCountFilters() { - return ProposalsCountFilters( - author: author, - onlyAuthor: onlyAuthor, - category: category, - searchQuery: searchQuery, - maxAge: maxAge, - campaign: campaign, - ); - } - - @override - String toString() => - 'ProposalsFilters(' - 'type[${type.name}], ' - 'author[$author], ' - 'onlyAuthor[$onlyAuthor], ' - 'category[$category], ' - 'searchQuery[$searchQuery], ' - 'maxAge[$maxAge], ' - 'campaign[$campaign]' - ')'; -} - -enum ProposalsFilterType { - total, - drafts, - finals, - favorites, - favoritesFinals, - my, - myFinals, - voted; - - bool get isFavorite => - this == ProposalsFilterType.favorites || this == ProposalsFilterType.favoritesFinals; - - bool get isMy => this == ProposalsFilterType.my || this == ProposalsFilterType.myFinals; -} diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposals/proposals_filters_v2.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposals/proposals_filters_v2.dart index e2d2bf298af0..304bda72b5c5 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposals/proposals_filters_v2.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposals/proposals_filters_v2.dart @@ -17,6 +17,11 @@ final class ProposalsCampaignFilters extends Equatable { return ProposalsCampaignFilters(categoriesIds: categoriesIds); } + factory ProposalsCampaignFilters.from(Campaign campaign) { + final categoriesIds = campaign.categories.map((e) => e.selfRef.id).toSet(); + return ProposalsCampaignFilters(categoriesIds: categoriesIds); + } + @override List get props => [categoriesIds]; diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/catalyst_voices_repositories.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/catalyst_voices_repositories.dart index d53be17fc0b0..084b72f8b2b2 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/catalyst_voices_repositories.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/catalyst_voices_repositories.dart @@ -18,7 +18,8 @@ export 'document/source/database_drafts_data_source.dart'; export 'document/source/document_data_local_source.dart'; export 'document/source/document_data_remote_source.dart'; export 'document/source/document_data_source.dart'; -export 'document/source/document_favorites_source.dart'; +export 'document/source/local_document_data_local_source.dart'; +export 'document/source/signed_document_data_local_source.dart'; export 'dto/document/document_dto.dart' show DocumentExt; export 'logging/logging_settings_storage.dart'; export 'proposal/proposal_repository.dart' show ProposalRepository; diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/comment/comment_repository.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/comment/comment_repository.dart index 31b0e28e7caf..4cf6179c352d 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/comment/comment_repository.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/comment/comment_repository.dart @@ -105,7 +105,7 @@ final class DocumentsCommentRepository implements CommentRepository { .watchDocuments( type: DocumentType.commentDocument, refGetter: (data) => data.metadata.template!, - refTo: ref, + referencing: ref, ) .map( (documents) { diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/catalyst_database.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/catalyst_database.dart index 63ff2104eaf5..493119bd6e82 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/catalyst_database.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/catalyst_database.dart @@ -1,21 +1,13 @@ import 'package:catalyst_voices_repositories/src/database/catalyst_database.drift.dart'; import 'package:catalyst_voices_repositories/src/database/catalyst_database_config.dart'; -import 'package:catalyst_voices_repositories/src/database/dao/documents_dao.dart'; import 'package:catalyst_voices_repositories/src/database/dao/documents_v2_dao.dart'; -import 'package:catalyst_voices_repositories/src/database/dao/drafts_dao.dart'; -import 'package:catalyst_voices_repositories/src/database/dao/favorites_dao.dart'; -import 'package:catalyst_voices_repositories/src/database/dao/proposals_dao.dart'; +import 'package:catalyst_voices_repositories/src/database/dao/documents_v2_local_metadata_dao.dart'; +import 'package:catalyst_voices_repositories/src/database/dao/local_draft_documents_v2_dao.dart'; import 'package:catalyst_voices_repositories/src/database/dao/proposals_v2_dao.dart'; import 'package:catalyst_voices_repositories/src/database/migration/drift_migration_strategy.dart'; import 'package:catalyst_voices_repositories/src/database/table/document_authors.dart'; -import 'package:catalyst_voices_repositories/src/database/table/documents.dart'; -import 'package:catalyst_voices_repositories/src/database/table/documents.drift.dart'; -import 'package:catalyst_voices_repositories/src/database/table/documents_favorite.dart'; import 'package:catalyst_voices_repositories/src/database/table/documents_local_metadata.dart'; -import 'package:catalyst_voices_repositories/src/database/table/documents_metadata.dart'; import 'package:catalyst_voices_repositories/src/database/table/documents_v2.dart'; -import 'package:catalyst_voices_repositories/src/database/table/drafts.dart'; -import 'package:catalyst_voices_repositories/src/database/table/drafts.drift.dart'; import 'package:catalyst_voices_repositories/src/database/table/local_documents_drafts.dart'; import 'package:drift/drift.dart'; import 'package:drift_flutter/drift_flutter.dart'; @@ -34,18 +26,11 @@ abstract interface class CatalystDatabase { QueryInterceptor? interceptor, }) = DriftCatalystDatabase.withConfig; - /// Contains all operations related to [DocumentEntity] which is db specific. - /// Do not confuse it with other documents. - DocumentsDao get documentsDao; - DocumentsV2Dao get documentsV2Dao; - /// Contains all operations related to [DocumentDraftEntity] which is db - /// specific. Do not confuse it with other documents / drafts. - DraftsDao get draftsDao; + LocalDraftDocumentsV2Dao get localDocumentsV2Dao; - /// Contains all operations related to fav status of documents. - FavoritesDao get favoritesDao; + DocumentsV2LocalMetadataDao get localMetadataDao; /// Allows to await completion of pending operations. /// @@ -53,9 +38,6 @@ abstract interface class CatalystDatabase { @visibleForTesting Future get pendingOperations; - /// Specialized version of [DocumentsDao]. - ProposalsDao get proposalsDao; - ProposalsV2Dao get proposalsV2Dao; Future analyze(); @@ -71,22 +53,16 @@ abstract interface class CatalystDatabase { @DriftDatabase( tables: [ - Documents, - DocumentsMetadata, - DocumentsFavorites, - Drafts, DocumentsV2, DocumentAuthors, DocumentsLocalMetadata, LocalDocumentsDrafts, ], daos: [ - DriftDocumentsDao, - DriftFavoritesDao, - DriftDraftsDao, - DriftProposalsDao, DriftDocumentsV2Dao, DriftProposalsV2Dao, + DriftDocumentsV2LocalMetadataDao, + DriftLocalDraftDocumentsV2Dao, ], queries: {}, views: [], @@ -121,17 +97,14 @@ class DriftCatalystDatabase extends $DriftCatalystDatabase implements CatalystDa return DriftCatalystDatabase(connection); } - @override - DocumentsDao get documentsDao => driftDocumentsDao; - @override DocumentsV2Dao get documentsV2Dao => driftDocumentsV2Dao; @override - DraftsDao get draftsDao => driftDraftsDao; + LocalDraftDocumentsV2Dao get localDocumentsV2Dao => driftLocalDraftDocumentsV2Dao; @override - FavoritesDao get favoritesDao => driftFavoritesDao; + DocumentsV2LocalMetadataDao get localMetadataDao => driftDocumentsV2LocalMetadataDao; @override MigrationStrategy get migration { @@ -147,9 +120,6 @@ class DriftCatalystDatabase extends $DriftCatalystDatabase implements CatalystDa await customSelect('select 1').get(); } - @override - ProposalsDao get proposalsDao => driftProposalsDao; - @override ProposalsV2Dao get proposalsV2Dao => driftProposalsV2Dao; diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/dao/documents_dao.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/dao/documents_dao.dart deleted file mode 100644 index ca4d30040b85..000000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/dao/documents_dao.dart +++ /dev/null @@ -1,491 +0,0 @@ -import 'package:catalyst_voices_models/catalyst_voices_models.dart'; -import 'package:catalyst_voices_repositories/src/database/catalyst_database.dart'; -import 'package:catalyst_voices_repositories/src/database/dao/documents_dao.drift.dart'; -import 'package:catalyst_voices_repositories/src/database/query/jsonb_expressions.dart'; -import 'package:catalyst_voices_repositories/src/database/table/documents.dart'; -import 'package:catalyst_voices_repositories/src/database/table/documents.drift.dart'; -import 'package:catalyst_voices_repositories/src/database/table/documents_metadata.dart'; -import 'package:catalyst_voices_repositories/src/database/table/drafts.dart'; -import 'package:catalyst_voices_repositories/src/database/typedefs.dart'; -import 'package:collection/collection.dart'; -import 'package:drift/drift.dart'; -import 'package:drift/extensions/json1.dart'; -import 'package:flutter/foundation.dart'; - -/// Exposes only public operation on documents, and related, tables. -abstract interface class DocumentsDao { - /// Counts documents matching required [ref] id and optional [ref] ver. - /// - /// If [ref] is null counts all documents. - /// - /// If [ref] ver is not specified it will return count of all version - /// matching [ref] id. - Future count({DocumentRef? ref}); - - /// Counts unique documents. All versions of same document are counted as 1. - Future countDocuments(); - - @visibleForTesting - Future countDocumentsMetadata(); - - /// Counts documents of specified [type] - /// that reference a given document [ref]. - /// - /// [ref] is the reference to the parent document being referenced - /// [type] is the type of documents to count (e.g., comments, reactions, etc.) - /// - /// Returns the count of documents matching both the type and reference. - Future countRefDocumentByType({ - required DocumentRef ref, - required DocumentType type, - }); - - /// Deletes all documents. Cascades to metadata. - /// - /// If [keepTemplatesForLocalDrafts] is true keeps templates referred by local drafts. - Future deleteAll({ - bool keepTemplatesForLocalDrafts, - }); - - /// If version is specified in [ref] returns this version or null. - /// Returns newest version with matching id or null of none found. - Future query({required DocumentRef ref}); - - /// Returns all entities. If same document have different versions - /// all will be returned. - /// - /// Optionally matching [ref] or [type]. - Future> queryAll({ - DocumentRef? ref, - DocumentType? type, - }); - - Future queryLatestDocumentData({ - CatalystId? authorId, - }); - - /// Returns document with matching refTo and type. - /// It return only lates version of document matching [refTo] - Future queryRefToDocumentData({ - required DocumentRef refTo, - DocumentType? type, - }); - - /// Returns a list of version of ref object. - /// Can be used to get versions count. - Future> queryVersionsOfId({required String id}); - - /// Inserts all documents and metadata. On conflicts ignores duplicates. - Future saveAll( - Iterable documentsWithMetadata, - ); - - /// Same as [query] but emits updates. - Stream watch({required DocumentRef ref}); - - /// Similar to [queryAll] but emits when new records are inserted or deleted. - /// Returns all entities. If same document have different versions - /// all will be returned unless [unique] is true. - /// When [unique] is true, only latest versions of each document are returned. - /// Optional [limit] parameter limits the number of returned documents. - Stream> watchAll({ - bool unique = false, - int? limit, - DocumentType? type, - CatalystId? authorId, - DocumentRef? refTo, - }); - - /// Watches for new comments that are reference by ref. - Stream watchCount({ - DocumentRef? refTo, - DocumentType? type, - }); - - Stream watchRefToDocumentData({ - required DocumentRef refTo, - required DocumentType type, - }); -} - -@DriftAccessor( - tables: [ - Documents, - DocumentsMetadata, - Drafts, - ], -) -class DriftDocumentsDao extends DatabaseAccessor - with $DriftDocumentsDaoMixin - implements DocumentsDao { - DriftDocumentsDao(super.attachedDatabase); - - @override - Future count({DocumentRef? ref}) { - if (ref == null) { - return documents.count().getSingle(); - } else { - return documents.count(where: (row) => _filterRef(row, ref)).getSingle(); - } - } - - @override - Future countDocuments() { - final count = documents.idHi.count(distinct: true); - - final select = selectOnly(documents) - ..addColumns([ - documents.idHi, - documents.idLo, - count, - ]); - - return select.map((row) => row.read(count)).get().then((count) => count.firstOrNull ?? 0); - } - - @override - Future countDocumentsMetadata() { - final count = documentsMetadata.verHi.count(distinct: true); - - final select = selectOnly(documentsMetadata) - ..addColumns([ - documentsMetadata.verHi, - documentsMetadata.verLo, - count, - ]); - - return select.map((row) => row.read(count)).get().then((count) => count.firstOrNull ?? 0); - } - - @override - Future countRefDocumentByType({ - required DocumentRef ref, - required DocumentType type, - }) async { - final query = select(documents) - ..where( - (row) => Expression.and([ - row.metadata.jsonExtract(r'$.type').equals(type.uuid), - row.metadata.jsonExtract(r'$.ref.id').equals(ref.id), - if (ref.version != null) - row.metadata.jsonExtract(r'$.ref.version').equals(ref.version!), - ]), - ); - - final docs = await query.get(); - return docs.length; - } - - @override - Future deleteAll({ - bool keepTemplatesForLocalDrafts = false, - }) async { - final query = delete(documents); - - if (keepTemplatesForLocalDrafts) { - final templateId = drafts.metadata.jsonExtract(r'$.template.id'); - - query.where((documents) { - return notExistsQuery( - selectOnly(drafts, distinct: true) - ..addColumns([ - templateId, - ]) - ..where( - documents.metadata.jsonExtract(r'$.selfRef.id').equalsExp(templateId), - ), - ); - }); - } - - final deletedRows = await query.go(); - - if (kDebugMode) { - debugPrint('DocumentsDao: Deleted[$deletedRows] rows'); - } - - return deletedRows; - } - - @override - Future query({required DocumentRef ref}) { - return _selectRef(ref).get().then((value) => value.firstOrNull); - } - - @override - Future> queryAll({ - DocumentRef? ref, - DocumentType? type, - }) { - final query = select(documents); - - if (ref != null) { - query.where((tbl) => _filterRef(tbl, ref, filterVersion: false)); - } - if (type != null) { - query.where((doc) => doc.type.equals(type.uuid)); - } - - return query.get(); - } - - @visibleForTesting - Future> queryDocumentsByMatchedDocumentNodeIdValue({ - required DocumentNodeId nodeId, - required String value, - DocumentType? type, - required String content, - }) async { - final query = select(documents) - ..where( - (tbl) => BaseJsonQueryExpression( - jsonContent: content, - nodeId: nodeId, - searchValue: value, - ), - ); - - if (type != null) { - query.where((doc) => doc.type.equals(type.uuid)); - } - - return query.get(); - } - - @override - Future queryLatestDocumentData({ - CatalystId? authorId, - }) { - final query = select(documents) - ..orderBy([(t) => OrderingTerm.desc(t.verHi)]) - ..limit(1); - - if (authorId != null) { - query.where((tbl) => tbl.metadata.isAuthor(authorId)); - } - - return query.getSingleOrNull(); - } - - @override - Future queryRefToDocumentData({ - required DocumentRef refTo, - DocumentType? type, - }) async { - final query = select(documents) - ..where( - (row) => Expression.and([ - if (type != null) row.type.equals(type.uuid), - row.metadata.jsonExtract(r'$.ref.id').equals(refTo.id), - if (refTo.version != null) - row.metadata.jsonExtract(r'$.ref.version').equals(refTo.version!), - ]), - ) - ..orderBy([ - (u) => OrderingTerm.desc(u.verHi), - ]) - ..limit(1); - - return query.getSingleOrNull(); - } - - @override - Future> queryVersionsOfId({required String id}) { - final query = select(documents) - ..where( - (tbl) => _filterRef( - tbl, - SignedDocumentRef(id: id), - filterVersion: false, - ), - ) - ..orderBy([ - (u) => OrderingTerm.desc(u.verHi), - ]); - - return query.get(); - } - - @override - Future saveAll( - Iterable documentsWithMetadata, - ) async { - final documents = documentsWithMetadata.map((e) => e.document); - final metadata = documentsWithMetadata.expand((e) => e.metadata); - - await batch((batch) { - batch - ..insertAll( - this.documents, - documents, - mode: InsertMode.insertOrIgnore, - ) - ..insertAll( - documentsMetadata, - metadata, - mode: InsertMode.insertOrIgnore, - ); - }); - } - - @override - Stream watch({required DocumentRef ref}) { - return _selectRef(ref).watch().map((event) => event.firstOrNull).distinct(_entitiesEquals); - } - - /// When [unique] is true, only latest versions of each document are returned. - @override - Stream> watchAll({ - bool unique = false, - int? limit, - DocumentType? type, - CatalystId? authorId, - DocumentRef? refTo, - }) { - final query = select(documents); - - if (type != null) { - query.where((doc) => doc.type.equals(type.uuid)); - } - if (authorId != null) { - query.where((tbl) => tbl.metadata.isAuthor(authorId)); - } - if (refTo != null) { - query.where( - (row) => Expression.and([ - row.metadata.jsonExtract(r'$.ref.id').equals(refTo.id), - if (refTo.version != null) - row.metadata.jsonExtract(r'$.ref.version').equals(refTo.version!), - ]), - ); - } - - query.orderBy([ - (t) => OrderingTerm( - expression: t.verHi, - mode: OrderingMode.desc, - ), - ]); - - if (unique) { - final latestDocumentRef = alias(documents, 'latestDocumentRef'); - final maxVerHi = latestDocumentRef.verHi.max(); - final latestDocumentQuery = selectOnly(latestDocumentRef, distinct: true) - ..addColumns([ - latestDocumentRef.idHi, - latestDocumentRef.idLo, - maxVerHi, - latestDocumentRef.verLo, - ]) - ..where(latestDocumentRef.type.equalsValue(DocumentType.proposalDocument)) - ..groupBy([latestDocumentRef.idHi + latestDocumentRef.idLo]); - - final verSubquery = Subquery(latestDocumentQuery, 'latestDocumentRef'); - - final uniqueQuery = query.join([ - innerJoin( - verSubquery, - Expression.and([ - verSubquery.ref(maxVerHi).equalsExp(documents.verHi), - verSubquery.ref(latestDocumentRef.verLo).equalsExp(documents.verLo), - ]), - useColumns: false, - ), - ]); - - if (limit != null) { - uniqueQuery.limit(limit); - } - - return uniqueQuery.map((row) => row.readTable(documents)).watch(); - } - - if (limit != null) { - query.limit(limit); - } - return query.watch(); - } - - @override - Stream watchCount({ - DocumentRef? refTo, - DocumentType? type, - }) { - final query = select(documents) - ..where( - (row) { - return Expression.and([ - if (type != null) row.metadata.jsonExtract(r'$.type').equals(type.uuid), - if (refTo != null) row.metadata.jsonExtract(r'$.ref.id').equals(refTo.id), - if (refTo?.version != null) - row.metadata.jsonExtract(r'$.ref.version').equals(refTo!.version!), - ]); - }, - ); - - return query.watch().map((comments) => comments.length).distinct(); - } - - @override - Stream watchRefToDocumentData({ - required DocumentRef refTo, - required DocumentType type, - }) { - final query = select(documents) - ..where( - (row) => Expression.and([ - row.metadata.jsonExtract(r'$.type').equals(type.uuid), - row.metadata.jsonExtract(r'$.ref.id').equals(refTo.id), - if (refTo.version != null) - row.metadata.jsonExtract(r'$.ref.version').equals(refTo.version!), - ]), - ) - ..orderBy([ - (t) => OrderingTerm( - expression: t.verHi, - mode: OrderingMode.desc, - ), - ]); - - return query.watch().map((event) => event.firstOrNull).distinct(_entitiesEquals); - } - - bool _entitiesEquals(DocumentEntity? previous, DocumentEntity? next) { - final previousId = (previous?.idHi, previous?.idLo); - final nextId = (next?.idHi, next?.idLo); - - final previousVer = (previous?.verHi, previous?.verLo); - final nextVer = (next?.verHi, next?.verLo); - - return previousId == nextId && previousVer == nextVer; - } - - Expression _filterRef( - $DocumentsTable row, - DocumentRef ref, { - bool filterVersion = true, - }) { - final id = UuidHiLo.from(ref.id); - final ver = UuidHiLo.fromNullable(ref.version); - - return Expression.and([ - row.idHi.equals(id.high), - row.idLo.equals(id.low), - if (ver != null && filterVersion) ...[ - row.verHi.equals(ver.high), - row.verLo.equals(ver.low), - ], - ]); - } - - SimpleSelectStatement<$DocumentsTable, DocumentEntity> _selectRef( - DocumentRef ref, - ) { - return select(documents) - ..where((tbl) => _filterRef(tbl, ref)) - ..orderBy([ - (u) => OrderingTerm.desc(u.verHi), - ]) - ..limit(1); - } -} diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/dao/documents_v2_dao.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/dao/documents_v2_dao.dart index bac519b03dc8..f2c332a9b8b6 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/dao/documents_v2_dao.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/dao/documents_v2_dao.dart @@ -9,60 +9,128 @@ import 'package:collection/collection.dart'; import 'package:drift/drift.dart'; abstract interface class DocumentsV2Dao { - /// Returns the total number of documents in the table. - Future count(); + /// Counts the number of documents matching the provided filters. + /// + /// [type] filters by the document type (e.g., proposal, comment). + /// [ref] filters by the document's own identity. + /// - If [DocumentRef.isExact], counts matches for that specific version. + /// - If [DocumentRef.isLoose], counts all versions of that document ID. + /// [referencing] filters documents that *reference* the given target. + /// - Example: Count all comments ([type]=comment) that point to proposal X ([referencing]=X). + Future count({ + DocumentType? type, + DocumentRef? ref, + DocumentRef? referencing, + }); + + /// Deletes documents from the database, preserving those with types in [excludeTypes]. + /// + /// If [excludeTypes] is null or empty, this may delete *all* documents (implementation dependent). + /// Typically used for cache invalidation or cleaning up old data while keeping + /// certain important types (e.g. keeping local drafts or templates). + /// + /// Returns the number of deleted rows. + Future deleteWhere({ + List? excludeTypes, + }); - /// Checks if a document exists by its reference. + /// Checks if a document exists in the database. /// - /// If [ref] is exact (has version), checks for the specific version. - /// If loose (no version), checks if any version with the id exists. - /// Returns true if the document exists, false otherwise. + /// [ref] determines the scope of the check: + /// - [SignedDocumentRef.exact]: Returns true only if that specific version exists. + /// - [SignedDocumentRef.loose]: Returns true if *any* version of that ID exists. Future exists(DocumentRef ref); - /// Filters and returns only the DocumentRefs from [refs] that exist in the database. + /// Filters a list of references, returning only those that exist in the database. /// - /// Optimized for performance: Uses a single query to fetch all relevant (id, ver) pairs - /// for unique ids in [refs], then checks existence in memory. - /// - For exact refs: Matches specific id and ver. - /// - For loose refs: Checks if any version for the id exists. - /// Suitable for synchronizing many documents with minimal database round-trips. + /// This is useful for bulk validation. + /// - For exact refs, it checks for exact matches. + /// - For loose refs, it checks if any version of the ID exists. Future> filterExisting(List refs); - /// Retrieves a document by its reference. + /// Retrieves a single document matching the criteria. + /// + /// If multiple documents match (e.g. querying by loose ref or type only), + /// the one with the latest [DocumentEntityV2.createdAt] timestamp is returned. /// - /// If [ref] is exact (has version), returns the specific version. - /// If loose (no version), returns the latest version by createdAt. - /// Returns null if no matching document is found. - Future getDocument(DocumentRef ref); + /// Returns `null` if no matching document is found. + Future getDocument({ + DocumentType? type, + DocumentRef? ref, + DocumentRef? referencing, + CatalystId? author, + }); + + /// Retrieves a list of documents matching the criteria with pagination. + /// + /// [latestOnly] - If `true`, only the most recent version (by [DocumentEntityV2.createdAt]) + /// of each unique document ID is returned. If `false`, all versions are returned. + /// [limit] - The maximum number of documents to return (clamped to 999). + /// [offset] - The number of documents to skip. + /// + /// Note: Ensure the implementation applies a deterministic `orderBy` clause + /// (usually `createdAt` DESC) to ensure stable pagination. + Future> getDocuments({ + DocumentType? type, + DocumentRef? ref, + DocumentRef? referencing, + CampaignFilters? filters, + bool latestOnly, + int limit, + int offset, + }); - /// Finds the latest version of a document. + /// Finds the latest version of a document given a reference. /// - /// Takes a [ref] (which can be loose or exact) and returns a [DocumentRef] - /// pointing to the latest known version of that document. + /// Even if [ref] points to an older version (exact), this method will find + /// the version with the newest [DocumentEntityV2.createdAt] timestamp for that [DocumentRef.id]. + /// + /// Returns `null` if the document ID does not exist in the database. Future getLatestOf(DocumentRef ref); - /// Saves a single document, ignoring if it conflicts on {id, ver}. + /// Saves a single document and its associated authors. /// - /// Delegates to [saveAll] for consistent conflict handling and reuse. + /// This is a convenience wrapper around [saveAll]. Future save(DocumentWithAuthorsEntity entity); - /// Saves multiple documents in a batch operation, ignoring conflicts. + /// Saves multiple documents and their authors in a single transaction. /// - /// [entries] is a list of DocumentEntity instances. - /// Uses insertOrIgnore to skip on primary key conflicts ({id, ver}). + /// Uses `INSERT OR IGNORE` conflict resolution. If a document with the same + /// `id` and `ver` already exists, the new record is ignored. Future saveAll(List entries); - /// Watches for a list of documents that match the given criteria. + /// Watches for changes and emits the count of documents matching the filters. /// - /// This method returns a stream that emits a new list of documents whenever - /// the underlying data changes. - /// - [type]: Optional filter to only include documents of a specific [DocumentType]. - /// - [filters]: Optional campaign filter. - /// - [latestOnly] is true only newest version per id is returned. - /// - [limit]: The maximum number of documents to return. - /// - [offset]: The number of documents to skip for pagination. + /// Emits a new value whenever the underlying tables change in a way that + /// affects the count. + Stream watchCount({ + DocumentType? type, + DocumentRef? ref, + DocumentRef? referencing, + }); + + /// Watches for changes to a specific document query. + /// + /// Emits the updated document (or null) whenever a matching record is + /// inserted, updated, or deleted. + Stream watchDocument({ + DocumentType? type, + DocumentRef? ref, + DocumentRef? referencing, + CatalystId? author, + }); + + /// Watches for changes and emits a list of documents. + /// + /// This stream automatically updates when new documents are synced or + /// existing ones are modified. + /// + /// Note: Large limits or complex filters in a watch stream can impact performance + /// as the query is re-run on every write to the `documents_v2` table. Stream> watchDocuments({ DocumentType? type, + DocumentRef? ref, + DocumentRef? referencing, CampaignFilters? filters, bool latestOnly, int limit, @@ -82,8 +150,29 @@ class DriftDocumentsV2Dao extends DatabaseAccessor DriftDocumentsV2Dao(super.attachedDatabase); @override - Future count() { - return documentsV2.count().getSingleOrNull().then((value) => value ?? 0); + Future count({ + DocumentType? type, + DocumentRef? ref, + DocumentRef? referencing, + }) { + return _queryCount( + type: type, + ref: ref, + referencing: referencing, + ).getSingle().then((value) => value ?? 0); + } + + @override + Future deleteWhere({ + List? excludeTypes, + }) { + final query = delete(documentsV2); + + if (excludeTypes != null) { + query.where((tbl) => tbl.type.isNotInValues(excludeTypes)); + } + + return query.go(); } @override @@ -138,18 +227,39 @@ class DriftDocumentsV2Dao extends DatabaseAccessor } @override - Future getDocument(DocumentRef ref) { - final query = select(documentsV2)..where((tbl) => tbl.id.equals(ref.id)); - - if (ref.isExact) { - query.where((tbl) => tbl.ver.equals(ref.version!)); - } else { - query - ..orderBy([(tbl) => OrderingTerm.desc(tbl.createdAt)]) - ..limit(1); - } + Future getDocument({ + DocumentType? type, + DocumentRef? ref, + DocumentRef? referencing, + CatalystId? author, + }) { + return _queryDocument( + type: type, + ref: ref, + referencing: referencing, + author: author, + ).getSingleOrNull(); + } - return query.getSingleOrNull(); + @override + Future> getDocuments({ + DocumentType? type, + DocumentRef? ref, + DocumentRef? referencing, + CampaignFilters? filters, + bool latestOnly = false, + int limit = 200, + int offset = 0, + }) { + return _queryDocuments( + type: type, + ref: ref, + referencing: referencing, + filters: filters, + latestOnly: latestOnly, + limit: limit, + offset: offset, + ).get(); } @override @@ -197,27 +307,172 @@ class DriftDocumentsV2Dao extends DatabaseAccessor }); } + @override + Stream watchCount({ + DocumentType? type, + DocumentRef? ref, + DocumentRef? referencing, + }) { + return _queryCount( + type: type, + ref: ref, + referencing: referencing, + ).watchSingle().map((value) => value ?? 0); + } + + @override + Stream watchDocument({ + DocumentType? type, + DocumentRef? ref, + DocumentRef? referencing, + CatalystId? author, + }) { + return _queryDocument( + type: type, + ref: ref, + referencing: referencing, + author: author, + ).watchSingleOrNull(); + } + @override Stream> watchDocuments({ DocumentType? type, + DocumentRef? ref, + DocumentRef? referencing, CampaignFilters? filters, bool latestOnly = false, int limit = 200, int offset = 0, }) { - final effectiveLimit = limit.clamp(0, 999); + return _queryDocuments( + type: type, + ref: ref, + referencing: referencing, + filters: filters, + latestOnly: latestOnly, + limit: limit, + offset: offset, + ).watch(); + } + + Selectable _queryCount({ + DocumentType? type, + DocumentRef? ref, + DocumentRef? referencing, + }) { + final count = countAll(); + final query = selectOnly(documentsV2)..addColumns([count]); + + if (type != null) { + query.where(documentsV2.type.equalsValue(type)); + } + + if (ref != null) { + query.where(documentsV2.id.equals(ref.id)); + + if (ref.isExact) { + query.where(documentsV2.ver.equals(ref.version!)); + } + } + + if (referencing != null) { + query.where(documentsV2.refId.equals(referencing.id)); + if (referencing.isExact) { + query.where(documentsV2.refVer.equals(referencing.version!)); + } + } + + return query.map((row) => row.read(count)); + } + + Selectable _queryDocument({ + DocumentType? type, + DocumentRef? ref, + DocumentRef? referencing, + CatalystId? author, + }) { final query = select(documentsV2); - if (filters != null) { - query.where((tbl) => tbl.categoryId.isIn(filters.categoriesIds)); + if (ref != null) { + query.where((tbl) => tbl.id.equals(ref.id)); + + if (ref.isExact) { + query.where((tbl) => tbl.ver.equals(ref.version!)); + } + } + + if (referencing != null) { + query.where((tbl) => tbl.refId.equals(referencing.id)); + + if (referencing.isExact) { + query.where((tbl) => tbl.refVer.equals(referencing.version!)); + } } if (type != null) { query.where((tbl) => tbl.type.equalsValue(type)); } - if (latestOnly) { + if (author != null) { + final significant = author.toSignificant(); + query.where((tbl) { + final authorQuery = selectOnly(documentAuthors) + ..addColumns([const Constant(1)]) + ..where(documentAuthors.documentId.equalsExp(tbl.id)) + ..where(documentAuthors.documentVer.equalsExp(tbl.ver)) + ..where(documentAuthors.authorIdSignificant.equals(significant.toUri().toString())); + return existsQuery(authorQuery); + }); + } + + query + ..orderBy([ + (tbl) => OrderingTerm.desc(tbl.createdAt), + ]) + ..limit(1); + + return query; + } + + SimpleSelectStatement<$DocumentsV2Table, DocumentEntityV2> _queryDocuments({ + DocumentType? type, + DocumentRef? ref, + DocumentRef? referencing, + CampaignFilters? filters, + required bool latestOnly, + required int limit, + required int offset, + }) { + final effectiveLimit = limit.clamp(0, 999); + final query = select(documentsV2); + + if (type != null) { + query.where((tbl) => tbl.type.equalsValue(type)); + } + + if (ref != null) { + query.where((tbl) => tbl.id.equals(ref.id)); + + if (ref.isExact) { + query.where((tbl) => tbl.ver.equals(ref.version!)); + } + } + + if (referencing != null) { + query.where((tbl) => tbl.refId.equals(referencing.id)); + + if (referencing.isExact) { + query.where((tbl) => tbl.refVer.equals(referencing.version!)); + } + } + + if (filters != null) { + query.where((tbl) => tbl.categoryId.isIn(filters.categoriesIds)); + } + + if (latestOnly && ref?.version == null) { final inner = alias(documentsV2, 'inner'); query.where((tbl) { @@ -230,8 +485,10 @@ class DriftDocumentsV2Dao extends DatabaseAccessor }); } - query.limit(effectiveLimit, offset: offset); + query + ..orderBy([(t) => OrderingTerm.desc(t.createdAt)]) + ..limit(effectiveLimit, offset: offset); - return query.watch(); + return query; } } diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/dao/documents_v2_local_metadata_dao.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/dao/documents_v2_local_metadata_dao.dart new file mode 100644 index 000000000000..d6eb2a1c82f9 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/dao/documents_v2_local_metadata_dao.dart @@ -0,0 +1,50 @@ +import 'package:catalyst_voices_repositories/src/database/catalyst_database.dart'; +import 'package:catalyst_voices_repositories/src/database/dao/documents_v2_local_metadata_dao.drift.dart'; +import 'package:catalyst_voices_repositories/src/database/table/documents_local_metadata.dart'; +import 'package:drift/drift.dart'; + +abstract interface class DocumentsV2LocalMetadataDao { + /// Deletes all local metadata records from the database. + /// + /// This operation is typically used to clear all user-specific data, + /// such as 'favorite' status. + /// Returns the number of rows that were deleted. + Future deleteWhere(); + + /// Checks if a document with the given [id] is marked as a favorite. + /// + /// Returns `true` if the document is a favorite, otherwise `false`. + /// If the document is not found, it also returns `false`. + Future isFavorite(String id); +} + +@DriftAccessor( + tables: [ + DocumentsLocalMetadata, + ], +) +class DriftDocumentsV2LocalMetadataDao extends DatabaseAccessor + with $DriftDocumentsV2LocalMetadataDaoMixin + implements DocumentsV2LocalMetadataDao { + DriftDocumentsV2LocalMetadataDao(super.attachedDatabase); + + @override + Future deleteWhere() { + final query = delete(documentsLocalMetadata); + + return query.go(); + } + + @override + Future isFavorite(String id) { + final query = selectOnly(documentsLocalMetadata) + ..addColumns([documentsLocalMetadata.isFavorite]) + ..where(documentsLocalMetadata.id.equals(id)) + ..limit(1); + + return query + .map((row) => row.read(documentsLocalMetadata.isFavorite)) + .getSingleOrNull() + .then((value) => value ?? false); + } +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/dao/drafts_dao.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/dao/drafts_dao.dart deleted file mode 100644 index c476c7bb882e..000000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/dao/drafts_dao.dart +++ /dev/null @@ -1,253 +0,0 @@ -import 'package:catalyst_voices_models/catalyst_voices_models.dart'; -import 'package:catalyst_voices_repositories/src/database/catalyst_database.dart'; -import 'package:catalyst_voices_repositories/src/database/dao/drafts_dao.drift.dart'; -import 'package:catalyst_voices_repositories/src/database/query/jsonb_expressions.dart'; -import 'package:catalyst_voices_repositories/src/database/table/drafts.dart'; -import 'package:catalyst_voices_repositories/src/database/table/drafts.drift.dart'; -import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; -import 'package:collection/collection.dart'; -import 'package:drift/drift.dart'; -import 'package:flutter/foundation.dart'; - -/// Exposes only public operation on drafts, and related, tables. -abstract interface class DraftsDao { - /// Counts drafts matching required [ref] id and optional [ref] ver. - /// - /// If ref is null it will count all drafts. - /// - /// If [ref] ver is not specified it will return count of all version - /// matching [ref] id. - Future count({DocumentRef? ref}); - - /// Clears table. - Future deleteAll(); - - /// Deletes a document draft with [ref]. - /// - /// If [ref] is null then all drafts are deleted. - Future deleteWhere({DraftRef ref}); - - /// If version is specified in [ref] returns this version or null. - /// Returns newest version with matching id or null of none found. - Future query({required DocumentRef ref}); - - /// Returns all drafts. - /// - /// Optionally matching [ref]. - Future> queryAll({ - DocumentRef? ref, - }); - - Future queryLatest({ - CatalystId? authorId, - }); - - Future> queryVersionsOfId({required String id}); - - /// Singular version of [saveAll]. Does not run in transaction. - Future save(DocumentDraftEntity draft); - - /// Inserts all drafts. On conflicts updates. - Future saveAll(Iterable drafts); - - /// Updates matching [ref] records with [content]. - /// - /// Be aware that if version is not specified all version of [ref] id - /// will be updated. - Future updateContent({ - required DraftRef ref, - required DocumentDataContent content, - }); - - /// Same as [query] but emits updates. - Stream watch({required DocumentRef ref}); - - Stream> watchAll({ - int? limit, - DocumentType? type, - CatalystId? authorId, - }); -} - -@DriftAccessor( - tables: [ - Drafts, - ], -) -class DriftDraftsDao extends DatabaseAccessor - with $DriftDraftsDaoMixin - implements DraftsDao { - DriftDraftsDao(super.attachedDatabase); - - @override - Future count({DocumentRef? ref}) { - if (ref == null) { - return drafts.count().getSingle(); - } else { - return drafts.count(where: (row) => _filterRef(row, ref)).getSingle(); - } - } - - @override - Future deleteAll() => delete(drafts).go(); - - @override - Future deleteWhere({DraftRef? ref}) async { - if (ref == null) { - await drafts.deleteAll(); - } else { - await drafts.deleteWhere((row) => _filterRef(row, ref)); - } - } - - @override - Future query({required DocumentRef ref}) { - return _selectRef(ref).get().then((value) => value.firstOrNull); - } - - @override - Future> queryAll({ - DocumentRef? ref, - }) { - final query = select(drafts); - - if (ref != null) { - query.where((tbl) => _filterRef(tbl, ref, filterVersion: false)); - } - - return query.get(); - } - - @override - Future queryLatest({ - CatalystId? authorId, - }) { - final query = select(drafts) - ..orderBy([(t) => OrderingTerm.desc(t.verHi)]) - ..limit(1); - - if (authorId != null) { - query.where((tbl) => tbl.metadata.isAuthor(authorId)); - } - - return query.getSingleOrNull(); - } - - @override - Future> queryVersionsOfId({required String id}) { - final query = select(drafts) - ..where( - (tbl) => _filterRef( - tbl, - DraftRef(id: id), - filterVersion: false, - ), - ) - ..orderBy([ - (u) => OrderingTerm.desc(u.verHi), - ]); - - return query.get(); - } - - @override - Future save(DocumentDraftEntity draft) async { - await into(drafts).insert(draft, mode: InsertMode.insertOrReplace); - } - - @override - Future saveAll(Iterable drafts) async { - await batch((batch) { - batch.insertAll( - this.drafts, - drafts, - mode: InsertMode.insertOrReplace, - ); - }); - } - - @override - Future updateContent({ - required DocumentRef ref, - required DocumentDataContent content, - }) async { - final insertable = DraftsCompanion( - content: Value(content), - title: content.title != null ? Value(content.title!) : const Value.absent(), - ); - final query = update(drafts)..where((tbl) => _filterRef(tbl, ref)); - - final updatedRows = await query.write(insertable); - - if (kDebugMode) { - debugPrint('DraftsDao: Updated[$updatedRows] $ref rows'); - } - } - - @override - Stream watch({required DocumentRef ref}) { - return _selectRef(ref).watch().map((event) => event.firstOrNull); - } - - @override - Stream> watchAll({ - int? limit, - DocumentType? type, - CatalystId? authorId, - }) { - final query = select(drafts); - - if (type != null) { - query.where((doc) => doc.type.equals(type.uuid)); - } - if (authorId != null) { - final searchId = authorId.toSignificant().toUri().toStringWithoutScheme(); - - query.where( - (doc) => CustomExpression("json_extract(metadata, '\$.authors') LIKE '%$searchId%'"), - ); - } - - query.orderBy([ - (t) => OrderingTerm( - expression: t.verHi, - mode: OrderingMode.desc, - ), - ]); - - if (limit != null) { - query.limit(limit); - } - - return query.watch(); - } - - Expression _filterRef( - $DraftsTable row, - DocumentRef ref, { - bool filterVersion = true, - }) { - final id = UuidHiLo.from(ref.id); - final ver = UuidHiLo.fromNullable(ref.version); - - return Expression.and([ - row.idHi.equals(id.high), - row.idLo.equals(id.low), - if (ver != null && filterVersion) ...[ - row.verHi.equals(ver.high), - row.verLo.equals(ver.low), - ], - ]); - } - - SimpleSelectStatement<$DraftsTable, DocumentDraftEntity> _selectRef( - DocumentRef ref, - ) { - return select(drafts) - ..where((tbl) => _filterRef(tbl, ref)) - ..orderBy([ - (u) => OrderingTerm.desc(u.verHi), - ]) - ..limit(1); - } -} diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/dao/favorites_dao.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/dao/favorites_dao.dart deleted file mode 100644 index 5d6d1d9eda87..000000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/dao/favorites_dao.dart +++ /dev/null @@ -1,103 +0,0 @@ -import 'package:catalyst_voices_models/catalyst_voices_models.dart'; -import 'package:catalyst_voices_repositories/catalyst_voices_repositories.dart'; -import 'package:catalyst_voices_repositories/src/database/catalyst_database.dart'; -import 'package:catalyst_voices_repositories/src/database/dao/favorites_dao.drift.dart'; -import 'package:catalyst_voices_repositories/src/database/table/documents_favorite.dart'; -import 'package:drift/drift.dart'; - -@DriftAccessor( - tables: [ - DocumentsFavorites, - ], -) -/// Exposes only public operations on favorites documents. It saves only document id and type. -class DriftFavoritesDao extends DatabaseAccessor - with $DriftFavoritesDaoMixin - implements FavoritesDao { - DriftFavoritesDao(super.attachedDatabase); - - @override - Future deleteAll() => delete(documentsFavorites).go(); - - @override - Future deleteWhere({required String id}) async { - final idHiLo = UuidHiLo.from(id); - - final query = delete(documentsFavorites) - ..where((tbl) { - return Expression.and([ - tbl.idHi.equals(idHiLo.high), - tbl.idLo.equals(idHiLo.low), - ]); - }); - - await query.go(); - - // When marking document as fav we want to rebuild documents streams. - db.markTablesUpdated([db.documents]); - } - - @override - Future save(DocumentFavoriteEntity entity) async { - await into(documentsFavorites).insert( - entity, - mode: InsertMode.insertOrIgnore, - ); - - // When marking document as fav we want to rebuild documents streams. - db.markTablesUpdated([db.documents]); - } - - @override - Stream watch({required String id}) { - final idHiLo = UuidHiLo.from(id); - final select = selectOnly(documentsFavorites) - ..where( - Expression.and([ - documentsFavorites.idHi.equals(idHiLo.high), - documentsFavorites.idLo.equals(idHiLo.low), - ]), - ) - ..addColumns([ - documentsFavorites.isFavorite, - ]); - - return select - .map((row) => row.read(documentsFavorites.isFavorite)) - .watchSingleOrNull() - .map((isFavorite) => isFavorite ?? false); - } - - @override - Stream> watchAll({DocumentType? type}) { - final select = selectOnly(documentsFavorites) - ..addColumns([ - documentsFavorites.idHi, - documentsFavorites.idLo, - ]); - - if (type != null) { - select.where(documentsFavorites.type.equalsValue(type)); - } - - return select.map((row) { - final udHiLo = UuidHiLo( - high: row.read(documentsFavorites.idHi)!, - low: row.read(documentsFavorites.idLo)!, - ); - return udHiLo.uuid; - }).watch(); - } -} - -abstract interface class FavoritesDao { - Future deleteAll(); - - Future deleteWhere({required String id}); - - Future save(DocumentFavoriteEntity entity); - - Stream watch({required String id}); - - Stream> watchAll({DocumentType? type}); -} diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/dao/local_draft_documents_v2_dao.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/dao/local_draft_documents_v2_dao.dart new file mode 100644 index 000000000000..8e04799cf4b0 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/dao/local_draft_documents_v2_dao.dart @@ -0,0 +1,402 @@ +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_repositories/catalyst_voices_repositories.dart'; +import 'package:catalyst_voices_repositories/src/database/catalyst_database.dart'; +import 'package:catalyst_voices_repositories/src/database/dao/local_draft_documents_v2_dao.drift.dart'; +import 'package:catalyst_voices_repositories/src/database/table/local_documents_drafts.dart'; +import 'package:catalyst_voices_repositories/src/database/table/local_documents_drafts.drift.dart'; +import 'package:drift/drift.dart'; + +@DriftAccessor( + tables: [ + LocalDocumentsDrafts, + ], +) +class DriftLocalDraftDocumentsV2Dao extends DatabaseAccessor + with $DriftLocalDraftDocumentsV2DaoMixin + implements LocalDraftDocumentsV2Dao { + DriftLocalDraftDocumentsV2Dao(super.attachedDatabase); + + @override + Future count({ + DocumentType? type, + DocumentRef? ref, + DocumentRef? referencing, + }) { + return _queryCount( + type: type, + ref: ref, + referencing: referencing, + ).getSingle().then((value) => value ?? 0); + } + + @override + Future deleteWhere({ + DocumentRef? ref, + List? excludeTypes, + }) { + final query = delete(localDocumentsDrafts); + + if (excludeTypes != null) { + query.where((tbl) => tbl.type.isNotInValues(excludeTypes)); + } + + return query.go(); + } + + @override + Future exists(DocumentRef ref) { + final query = selectOnly(localDocumentsDrafts) + ..addColumns([const Constant(1)]) + ..where(localDocumentsDrafts.id.equals(ref.id)); + + if (ref.isExact) { + query.where(localDocumentsDrafts.ver.equals(ref.version!)); + } + + query.limit(1); + + return query.getSingleOrNull().then((result) => result != null); + } + + @override + Future> filterExisting(List refs) async { + if (refs.isEmpty) return []; + + final uniqueIds = refs.map((ref) => ref.id).toSet(); + + // Single query: Fetch all (id, ver) for matching ids + final query = selectOnly(localDocumentsDrafts) + ..addColumns([localDocumentsDrafts.id, localDocumentsDrafts.ver]) + ..where(localDocumentsDrafts.id.isIn(uniqueIds)); + + final rows = await query.map( + (row) { + final id = row.read(localDocumentsDrafts.id)!; + final ver = row.read(localDocumentsDrafts.ver)!; + return (id: id, ver: ver); + }, + ).get(); + + final idToVers = >{}; + for (final pair in rows) { + idToVers.update( + pair.id, + (value) => value..add(pair.ver), + ifAbsent: () => {pair.ver}, + ); + } + + return refs.where((ref) { + final vers = idToVers[ref.id]; + if (vers == null || vers.isEmpty) return false; + + return !ref.isExact || vers.contains(ref.version); + }).toList(); + } + + @override + Future getDocument({ + DocumentType? type, + DocumentRef? ref, + DocumentRef? referencing, + }) { + return _queryDocument(type: type, ref: ref, referencing: referencing).getSingleOrNull(); + } + + @override + Future> getDocuments({ + DocumentType? type, + DocumentRef? ref, + DocumentRef? referencing, + CampaignFilters? filters, + bool latestOnly = false, + int limit = 200, + int offset = 0, + }) { + return _queryDocuments( + type: type, + ref: ref, + referencing: referencing, + filters: filters, + latestOnly: latestOnly, + limit: limit, + offset: offset, + ).get(); + } + + @override + Future getLatestOf(DocumentRef ref) { + final query = selectOnly(localDocumentsDrafts) + ..addColumns([localDocumentsDrafts.id, localDocumentsDrafts.ver]) + ..where(localDocumentsDrafts.id.equals(ref.id)) + ..orderBy([OrderingTerm.desc(localDocumentsDrafts.createdAt)]) + ..limit(1); + + return query + .map( + (row) => DraftRef( + id: row.read(localDocumentsDrafts.id)!, + version: row.read(localDocumentsDrafts.ver), + ), + ) + .getSingleOrNull(); + } + + @override + Future saveAll(List entries) async { + await batch((batch) { + batch.insertAll( + localDocumentsDrafts, + entries, + mode: InsertMode.insertOrReplace, + ); + }); + } + + @override + Future updateContent({ + required DocumentRef ref, + required DocumentDataContent content, + }) async { + final insertable = LocalDocumentsDraftsCompanion(content: Value(content)); + + final query = update(localDocumentsDrafts)..where((tbl) => tbl.id.equals(ref.id)); + + if (ref.isExact) { + query.where((tbl) => tbl.ver.equals(ref.version!)); + } + + await query.write(insertable); + } + + @override + Stream watchCount({ + DocumentType? type, + DocumentRef? ref, + DocumentRef? referencing, + }) { + return _queryCount( + type: type, + ref: ref, + referencing: referencing, + ).watchSingle().map((value) => value ?? 0); + } + + @override + Stream watchDocument({ + DocumentType? type, + DocumentRef? ref, + DocumentRef? referencing, + }) { + return _queryDocument(type: type, ref: ref, referencing: referencing).watchSingleOrNull(); + } + + @override + Stream> watchDocuments({ + DocumentType? type, + DocumentRef? ref, + DocumentRef? referencing, + CampaignFilters? filters, + bool latestOnly = false, + int limit = 200, + int offset = 0, + }) { + return _queryDocuments( + type: type, + ref: ref, + referencing: referencing, + filters: filters, + latestOnly: latestOnly, + limit: limit, + offset: offset, + ).watch(); + } + + Selectable _queryCount({ + DocumentType? type, + DocumentRef? ref, + DocumentRef? referencing, + }) { + final count = countAll(); + final query = selectOnly(localDocumentsDrafts)..addColumns([count]); + + if (type != null) { + query.where(localDocumentsDrafts.type.equalsValue(type)); + } + + if (ref != null) { + query.where(localDocumentsDrafts.id.equals(ref.id)); + + if (ref.isExact) { + query.where(localDocumentsDrafts.ver.equals(ref.version!)); + } + } + + if (referencing != null) { + query.where(localDocumentsDrafts.refId.equals(referencing.id)); + + if (referencing.isExact) { + query.where(localDocumentsDrafts.refVer.equals(referencing.version!)); + } + } + + return query.map((row) => row.read(count)); + } + + Selectable _queryDocument({ + DocumentType? type, + DocumentRef? ref, + DocumentRef? referencing, + }) { + final query = select(localDocumentsDrafts); + + if (ref != null) { + query.where((tbl) => tbl.id.equals(ref.id)); + + if (ref.isExact) { + query.where((tbl) => tbl.ver.equals(ref.version!)); + } + } + + if (referencing != null) { + query.where((tbl) => tbl.refId.equals(referencing.id)); + + if (referencing.isExact) { + query.where((tbl) => tbl.refVer.equals(referencing.version!)); + } + } + + if (type != null) { + query.where((tbl) => tbl.type.equalsValue(type)); + } + + query + ..orderBy([ + (tbl) => OrderingTerm.desc(tbl.createdAt), + ]) + ..limit(1); + + return query; + } + + SimpleSelectStatement<$LocalDocumentsDraftsTable, LocalDocumentDraftEntity> _queryDocuments({ + DocumentType? type, + DocumentRef? ref, + DocumentRef? referencing, + CampaignFilters? filters, + required bool latestOnly, + required int limit, + required int offset, + }) { + final effectiveLimit = limit.clamp(0, 999); + final query = select(localDocumentsDrafts); + + if (type != null) { + query.where((tbl) => tbl.type.equalsValue(type)); + } + + if (ref != null) { + query.where((tbl) => tbl.id.equals(ref.id)); + + if (ref.isExact) { + query.where((tbl) => tbl.ver.equals(ref.version!)); + } + } + + if (referencing != null) { + query.where((tbl) => tbl.refId.equals(referencing.id)); + + if (referencing.isExact) { + query.where((tbl) => tbl.refVer.equals(referencing.version!)); + } + } + + if (filters != null) { + query.where((tbl) => tbl.categoryId.isIn(filters.categoriesIds)); + } + + if (latestOnly && ref?.version == null) { + final inner = alias(localDocumentsDrafts, 'inner'); + + query.where((tbl) { + final maxCreatedAt = subqueryExpression( + selectOnly(inner) + ..addColumns([inner.createdAt.max()]) + ..where(inner.id.equalsExp(tbl.id)), + ); + return tbl.createdAt.equalsExp(maxCreatedAt); + }); + } + + query + ..orderBy([(t) => OrderingTerm.desc(t.createdAt)]) + ..limit(effectiveLimit, offset: offset); + + return query; + } +} + +/// This interface is very similar to [DocumentsV2Dao] so see methods explanation there. +abstract interface class LocalDraftDocumentsV2Dao { + Future count({ + DocumentType? type, + DocumentRef? ref, + DocumentRef? referencing, + }); + + Future deleteWhere({ + DocumentRef? ref, + List? excludeTypes, + }); + + Future exists(DocumentRef ref); + + Future> filterExisting(List refs); + + Future getDocument({ + DocumentType? type, + DocumentRef? ref, + DocumentRef? referencing, + }); + + Future> getDocuments({ + DocumentType? type, + DocumentRef? ref, + DocumentRef? referencing, + CampaignFilters? filters, + bool latestOnly, + int limit, + int offset, + }); + + Future getLatestOf(DocumentRef ref); + + Future saveAll(List entries); + + Future updateContent({ + required DocumentRef ref, + required DocumentDataContent content, + }); + + Stream watchCount({ + DocumentType? type, + DocumentRef? ref, + DocumentRef? referencing, + }); + + Stream watchDocument({ + DocumentType? type, + DocumentRef? ref, + DocumentRef? referencing, + }); + + Stream> watchDocuments({ + DocumentType? type, + DocumentRef? ref, + DocumentRef? referencing, + CampaignFilters? filters, + bool latestOnly, + int limit, + int offset, + }); +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/dao/proposals_dao.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/dao/proposals_dao.dart deleted file mode 100644 index 85dc8c3b41dc..000000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/dao/proposals_dao.dart +++ /dev/null @@ -1,836 +0,0 @@ -import 'dart:async'; - -import 'package:catalyst_voices_models/catalyst_voices_models.dart'; -import 'package:catalyst_voices_repositories/catalyst_voices_repositories.dart'; -import 'package:catalyst_voices_repositories/src/database/catalyst_database.dart'; -import 'package:catalyst_voices_repositories/src/database/dao/proposals_dao.drift.dart'; -import 'package:catalyst_voices_repositories/src/database/query/jsonb_expressions.dart'; -import 'package:catalyst_voices_repositories/src/database/table/documents.dart'; -import 'package:catalyst_voices_repositories/src/database/table/documents.drift.dart' - show $DocumentsTable; -import 'package:catalyst_voices_repositories/src/database/table/documents_favorite.dart'; -import 'package:catalyst_voices_repositories/src/dto/proposal/proposal_submission_action_dto.dart'; -import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; -import 'package:collection/collection.dart'; -import 'package:drift/drift.dart'; -import 'package:drift/extensions/json1.dart'; -import 'package:equatable/equatable.dart'; -import 'package:flutter/foundation.dart'; -import 'package:rxdart/rxdart.dart'; - -/// Exposes only public operation on proposals, and related tables. -/// This is a wrapper around [DocumentsDao] and [DraftsDao] to provide a single interface for proposals. -/// Since proposals are composed of multiple documents (template, action, comments, etc.) we need to -/// join multiple tables to get all the information about a proposal, which make sense to create this specialized dao. -@DriftAccessor( - tables: [ - Documents, - DocumentsMetadata, - DocumentsFavorites, - ], -) -class DriftProposalsDao extends DatabaseAccessor - with $DriftProposalsDaoMixin - implements ProposalsDao { - DriftProposalsDao(super.attachedDatabase); - - // TODO(dt-iohk): it seems that this method doesn't correctly filter by ProposalsFilterType.my - // since it does not check for author, consider to use another type which doesn't have "my" case. - - // TODO(damian-molinski): filters is only used for campaign and type. - @override - Future> queryProposals({ - SignedDocumentRef? categoryRef, - required ProposalsFilters filters, - }) async { - if ([ - filters.author, - filters.onlyAuthor, - filters.category, - filters.searchQuery, - filters.maxAge, - ].nonNulls.isNotEmpty) { - if (kDebugMode) { - print('queryProposals supports only campaign and type filters'); - } - } - - final latestProposalRef = alias(documents, 'latestProposalRef'); - final proposal = alias(documents, 'proposal'); - - final maxVerHi = latestProposalRef.verHi.max(); - final latestProposalsQuery = selectOnly(latestProposalRef, distinct: true) - ..addColumns([ - latestProposalRef.idHi, - latestProposalRef.idLo, - maxVerHi, - latestProposalRef.verLo, - ]) - ..where(latestProposalRef.type.equalsValue(DocumentType.proposalDocument)) - ..groupBy([latestProposalRef.idHi + latestProposalRef.idLo]); - - final verSubquery = Subquery(latestProposalsQuery, 'latestProposalRef'); - - final mainQuery = - select(proposal).join([ - innerJoin( - verSubquery, - Expression.and([ - verSubquery.ref(maxVerHi).equalsExp(proposal.verHi), - verSubquery.ref(latestProposalRef.verLo).equalsExp(proposal.verLo), - ]), - useColumns: false, - ), - ]) - ..where( - Expression.and([ - proposal.type.equalsValue(DocumentType.proposalDocument), - proposal.metadata.jsonExtract(r'$.template').isNotNull(), - proposal.metadata.jsonExtract(r'$.categoryId.id').isNotNull(), - if (filters.campaign != null) - proposal.metadata - .jsonExtract(r'$.categoryId.id') - .isIn(filters.campaign!.categoriesIds), - ]), - ) - ..orderBy([OrderingTerm.asc(proposal.verHi)]); - - if (categoryRef != null) { - mainQuery.where(proposal.metadata.isCategory(categoryRef)); - } - - final ids = await _getFilterTypeIds(filters.type); - - final include = ids.include; - if (include != null) { - final highs = include.map((e) => e.high); - final lows = include.map((e) => e.low); - mainQuery.where( - Expression.and([ - proposal.idHi.isIn(highs), - proposal.idLo.isIn(lows), - ]), - ); - } - - final exclude = ids.exclude; - if (exclude != null) { - final highs = exclude.map((e) => e.high); - final lows = exclude.map((e) => e.low); - - mainQuery.where( - Expression.and([ - proposal.idHi.isNotIn(highs), - proposal.idLo.isNotIn(lows), - ]), - ); - } - - final proposals = await mainQuery - .map((row) => row.readTable(proposal)) - .get() - .then((entities) => entities.map(_buildJoinedProposal).toList().wait); - - return proposals; - } - - @override - Future> queryProposalsPage({ - required PageRequest request, - required ProposalsFilters filters, - required ProposalsOrder order, - }) async { - final author = filters.author; - final searchQuery = filters.searchQuery; - - final latestProposalRef = alias(documents, 'latestProposalRef'); - final proposal = alias(documents, 'proposal'); - - final maxVerHi = latestProposalRef.verHi.max(); - final latestProposalsQuery = selectOnly(latestProposalRef, distinct: true) - ..addColumns([ - latestProposalRef.idHi, - latestProposalRef.idLo, - maxVerHi, - latestProposalRef.verLo, - ]) - ..where(latestProposalRef.type.equalsValue(DocumentType.proposalDocument)) - ..groupBy([latestProposalRef.idHi + latestProposalRef.idLo]); - - final verSubquery = Subquery(latestProposalsQuery, 'latestProposalRef'); - - final mainQuery = - select(proposal).join([ - innerJoin( - verSubquery, - Expression.and([ - verSubquery.ref(maxVerHi).equalsExp(proposal.verHi), - verSubquery.ref(latestProposalRef.verLo).equalsExp(proposal.verLo), - ]), - useColumns: false, - ), - ]) - ..where( - Expression.and([ - proposal.type.equalsValue(DocumentType.proposalDocument), - // Safe check for invalid proposals - proposal.metadata.jsonExtract(r'$.template').isNotNull(), - proposal.metadata.jsonExtract(r'$.categoryId').isNotNull(), - if (filters.campaign != null) - proposal.metadata - .jsonExtract(r'$.categoryId.id') - .isIn(filters.campaign!.categoriesIds), - ]), - ) - ..orderBy(order.terms(proposal)) - ..limit(request.size, offset: request.page * request.size); - - final ids = await _getFilterTypeIds(filters.type); - - final include = ids.include; - if (include != null) { - final highs = include.map((e) => e.high); - final lows = include.map((e) => e.low); - mainQuery.where( - Expression.and([ - proposal.idHi.isIn(highs), - proposal.idLo.isIn(lows), - ]), - ); - } - - final exclude = ids.exclude; - if (exclude != null) { - final highs = exclude.map((e) => e.high); - final lows = exclude.map((e) => e.low); - - mainQuery.where( - Expression.and([ - proposal.idHi.isNotIn(highs), - proposal.idLo.isNotIn(lows), - ]), - ); - } - - if ((filters.onlyAuthor ?? false) || filters.type.isMy) { - if (author != null) { - mainQuery.where(proposal.metadata.isAuthor(author)); - } else { - return Page( - page: request.page, - maxPerPage: request.size, - total: 0, - items: List.empty(), - ); - } - } - - if (filters.category != null) { - mainQuery.where(proposal.metadata.isCategory(filters.category!)); - } - - if (searchQuery != null) { - // TODO(damian-molinski): Check if documentsMetadata can be used. - mainQuery.where(proposal.search(searchQuery)); - } - - final maxAge = filters.maxAge; - if (maxAge != null) { - final now = DateTimeExt.now(utc: true); - final oldestDateTime = now.subtract(maxAge); - final uuid = UuidUtils.buildV7At(oldestDateTime); - final hiLo = UuidHiLo.from(uuid); - mainQuery.where(proposal.verHi.isBiggerThanValue(hiLo.high)); - } - - final proposals = await mainQuery - .map((row) => row.readTable(proposal)) - .get() - .then((entities) => entities.map(_buildJoinedProposal).wait); - - final total = await watchCount( - filters: filters.toCountFilters(), - ).first.then((count) => count.ofType(filters.type)); - - return Page( - page: request.page, - maxPerPage: request.size, - total: total, - items: proposals, - ); - } - - @override - Stream watchCount({ - required ProposalsCountFilters filters, - }) { - final stream = _getProposalsRefsStream(filters: filters); - - return _transformRefsStreamToCount(stream, author: filters.author); - } - - @override - Stream> watchProposalsPage({ - required PageRequest request, - required ProposalsFilters filters, - required ProposalsOrder order, - }) async* { - yield await queryProposalsPage(request: request, filters: filters, order: order); - - yield* connection.streamQueries - .updatesForSync(TableUpdateQuery.onAllTables([documents, documentsFavorites])) - .debounceTime(const Duration(milliseconds: 10)) - .asyncMap((event) { - return queryProposalsPage(request: request, filters: filters, order: order); - }); - } - - // TODO(damian-molinski): Make this more specialized per case. - // for example proposals list does not need all versions, just count. - Future _buildJoinedProposal( - DocumentEntity proposal, - ) async { - assert( - proposal.type == DocumentType.proposalDocument, - 'Invalid document type', - ); - - var proposalRef = proposal.metadata.selfRef; - - final latestAction = await _getProposalsLatestAction( - proposalId: proposalRef.id, - ).then((value) => value.singleOrNull); - - var effectiveProposal = proposal; - - if (latestAction != null && latestAction.proposalRef != proposalRef) { - final latestActionProposal = await _getDocument(latestAction.proposalRef); - - effectiveProposal = latestActionProposal; - } - - proposalRef = effectiveProposal.metadata.selfRef; - - final templateFuture = _getDocument(effectiveProposal.metadata.template!); - final actionFuture = _maybeGetDocument(latestAction?.selfRef); - final commentsCountFuture = _getProposalCommentsCount(proposalRef); - final versionsFuture = _getProposalVersions(proposalRef.id); - - final (template, action, commentsCount, versions) = await ( - templateFuture, - actionFuture, - commentsCountFuture, - versionsFuture, - ).wait; - - return JoinedProposalEntity( - proposal: effectiveProposal, - template: template, - action: action, - commentsCount: commentsCount, - versions: versions, - ); - } - - Future<_IdsFilter> _excludeHiddenProposalsFilter() { - return _getProposalsLatestAction() - .then((value) { - return value - .where((e) => e.action.isHidden) - .map((e) => e.proposalRef.id) - .map(UuidHiLo.from); - }) - .then(_IdsFilter.exclude); - } - - Future<_IdsFilter> _excludeNotDraftProposalsFilter() { - return _getProposalsLatestAction() - .then( - (value) { - return value - .where((element) => !element.action.isDraft) - .map((e) => e.proposalRef.id) - .map(UuidHiLo.from); - }, - ) - .then(_IdsFilter.exclude); - } - - Future> _getAuthorProposalsLooseRefs({ - required CatalystId author, - }) { - final query = selectOnly(documents) - ..addColumns([ - documents.idHi, - documents.idLo, - ]) - ..where( - Expression.and([ - documents.type.equalsValue(DocumentType.proposalDocument), - documents.metadata.isAuthor(author), - ]), - ); - - return query.map((row) { - final id = UuidHiLo( - high: row.read(documents.idHi)!, - low: row.read(documents.idLo)!, - ); - - return SignedDocumentRef.loose(id: id.uuid); - }).get(); - } - - Future _getDocument(DocumentRef ref) async { - final document = await _maybeGetDocument(ref); - assert(document != null, 'Did not found document with ref[$ref]'); - return document!; - } - - Future> _getFavoritesRefs() { - final query = selectOnly(documentsFavorites) - ..addColumns([ - documentsFavorites.idHi, - documentsFavorites.idLo, - ]) - ..where( - Expression.and([ - documentsFavorites.type.equalsValue(DocumentType.proposalDocument), - documentsFavorites.isFavorite.equals(true), - ]), - ); - - return query.map((row) { - final id = UuidHiLo( - high: row.read(documentsFavorites.idHi)!, - low: row.read(documentsFavorites.idLo)!, - ); - - return SignedDocumentRef.loose(id: id.uuid); - }).get(); - } - - Future<_IdsFilter> _getFilterTypeIds(ProposalsFilterType type) { - switch (type) { - case ProposalsFilterType.total: - return _excludeHiddenProposalsFilter(); - case ProposalsFilterType.drafts: - return _excludeNotDraftProposalsFilter(); - case ProposalsFilterType.finals: - return _includeFinalProposalsFilter(); - case ProposalsFilterType.favorites: - return _includeFavoriteRefsExcludingHiddenProposalsFilter(); - case ProposalsFilterType.favoritesFinals: - return _includeFinalFavoriteRefsProposalsFilter(); - case ProposalsFilterType.my: - return _excludeHiddenProposalsFilter(); - case ProposalsFilterType.myFinals: - return _includeFinalProposalsFilter(); - case ProposalsFilterType.voted: - return _includeVotedRefsExcludingHiddenProposalsFilter(); - } - } - - Future _getProposalCommentsCount(DocumentRef ref) { - final id = ref.id; - final ver = ref.version; - - final amountOfComment = documents.rowId.count(); - - final query = selectOnly(documents) - ..addColumns([ - amountOfComment, - ]) - ..where( - Expression.and([ - documents.type.equalsValue(DocumentType.commentDocument), - documents.metadata.jsonExtract(r'$.ref.id').equals(id), - if (ver != null) documents.metadata.jsonExtract(r'$.ref.version').equals(ver), - ]), - ) - ..orderBy([OrderingTerm.desc(documents.verHi)]); - - return query - .map((row) => row.read(amountOfComment)) - .getSingleOrNull() - .then((value) => value ?? 0); - } - - Future> _getProposalsLatestAction({ - String? proposalId, - }) { - final refId = documents.metadata.jsonExtract(r'$.ref.id'); - final refVer = documents.metadata.jsonExtract(r'$.ref.version'); - final query = selectOnly(documents, distinct: true) - ..addColumns([ - documents.idHi, - documents.idLo, - documents.verHi, - documents.verLo, - refId, - refVer, - documents.content, - ]) - ..where( - Expression.and([ - documents.type.equalsValue(DocumentType.proposalActionDocument), - refId.isNotNull(), - if (proposalId != null) refId.equals(proposalId), - ]), - ) - ..orderBy([OrderingTerm.desc(documents.verHi)]); - - return query - .map((row) { - final selfRef = row.readSelfRef(documents); - final proposalRef = SignedDocumentRef( - id: row.read(refId)!, - version: row.read(refVer), - ); - - final content = row.readWithConverter(documents.content); - final rawAction = content?.data['action']; - final actionDto = rawAction is String - ? ProposalSubmissionActionDto.fromJson(rawAction) - : null; - final action = actionDto?.toModel(); - - return _ProposalActions( - selfRef: selfRef, - proposalRef: proposalRef, - action: action ?? ProposalSubmissionAction.draft, - ); - }) - .get() - .then((entities) { - final grouped = {}; - - for (final entry in entities.nonNulls) { - // 1st element per ref is latest. See orderBy. - final id = entry.proposalRef.id; - if (!grouped.containsKey(id)) { - grouped[id] = entry; - } - } - - return grouped.values.toList(); - }); - } - - Stream> _getProposalsRefsStream({ - ProposalsCountFilters? filters, - }) { - final author = filters?.author; - final searchQuery = filters?.searchQuery; - - final query = selectOnly(documents) - ..addColumns([ - documents.idHi, - documents.idLo, - documents.verHi, - documents.verLo, - ]) - ..where( - Expression.and([ - documents.type.equalsValue(DocumentType.proposalDocument), - // Safe check for invalid proposals - documents.metadata.jsonExtract(r'$.template').isNotNull(), - documents.metadata.jsonExtract(r'$.categoryId').isNotNull(), - if (filters?.campaign != null) - documents.metadata - .jsonExtract(r'$.categoryId.id') - .isIn(filters!.campaign!.categoriesIds), - ]), - ) - ..orderBy([OrderingTerm.desc(documents.verHi)]) - ..groupBy([documents.idHi + documents.idLo]); - - if ((filters?.onlyAuthor ?? false) && author != null) { - query.where(documents.metadata.isAuthor(author)); - } - - if (filters?.category != null) { - query.where(documents.metadata.isCategory(filters!.category!)); - } - - if (searchQuery != null) { - // TODO(damian-molinski): Check if documentsMetadata can be used. - query.where(documents.search(searchQuery)); - } - - final maxAge = filters?.maxAge; - if (maxAge != null) { - final now = DateTimeExt.now(utc: true); - final oldestDateTime = now.subtract(maxAge); - final uuid = UuidUtils.buildV7At(oldestDateTime); - final hiLo = UuidHiLo.from(uuid); - query.where(documents.verHi.isBiggerThanValue(hiLo.high)); - } - - return query.map((row) => row.readSelfRef(documents)).watch(); - } - - Future> _getProposalVersions(String id) { - final idHiLo = UuidHiLo.from(id); - - final query = selectOnly(documents) - ..addColumns([ - documents.verHi, - documents.verLo, - ]) - ..where( - Expression.and([ - documents.idHi.equals(idHiLo.high), - documents.idLo.equals(idHiLo.low), - ]), - ) - ..orderBy([OrderingTerm.desc(documents.verHi)]); - - return query.map((row) { - final high = row.read(documents.verHi)!; - final low = row.read(documents.verLo)!; - return UuidHiLo(high: high, low: low).uuid; - }).get(); - } - - Future> _getVotedRefs() async { - // TODO(dt-iohk): query refs of voted proposals - return []; - } - - Future<_IdsFilter> _includeFavoriteRefsExcludingHiddenProposalsFilter() { - return _getFavoritesRefs() - .then((favoriteRefs) { - return _getProposalsLatestAction().then((actions) async { - final hiddenProposalsIds = actions - .where((e) => e.action.isHidden) - .map((e) => e.proposalRef.id); - - return favoriteRefs - .map((e) => e.id) - .whereNot(hiddenProposalsIds.contains) - .map(UuidHiLo.from); - }); - }) - .then(_IdsFilter.include); - } - - Future<_IdsFilter> _includeFinalFavoriteRefsProposalsFilter() { - return _getFavoritesRefs() - .then((favoriteRefs) { - return _getProposalsLatestAction().then((actions) async { - final finalProposalIds = actions - .where((e) => e.action.isFinal) - .map((e) => e.proposalRef.id); - - return favoriteRefs - .map((e) => e.id) - .where(finalProposalIds.contains) - .map(UuidHiLo.from); - }); - }) - .then(_IdsFilter.include); - } - - Future<_IdsFilter> _includeFinalProposalsFilter() { - return _getProposalsLatestAction() - .then( - (value) { - return value - .where((element) => element.action.isFinal) - .map((e) => e.proposalRef.id) - .map(UuidHiLo.from); - }, - ) - .then(_IdsFilter.include); - } - - Future<_IdsFilter> _includeVotedRefsExcludingHiddenProposalsFilter() { - return _getVotedRefs() - .then((votedRefs) { - return _getProposalsLatestAction().then((actions) async { - final hiddenProposalsIds = actions - .where((e) => e.action.isHidden) - .map((e) => e.proposalRef.id); - - return votedRefs - .map((e) => e.id) - .whereNot(hiddenProposalsIds.contains) - .map(UuidHiLo.from); - }); - }) - .then(_IdsFilter.include); - } - - Future> _maybeGetAuthorProposalsLooseRefs({ - CatalystId? author, - }) { - if (author == null) { - return Future(List.empty); - } - - return _getAuthorProposalsLooseRefs(author: author); - } - - Future _maybeGetDocument(DocumentRef? ref) { - if (ref == null) { - return Future.value(); - } - - final id = UuidHiLo.from(ref.id); - final ver = UuidHiLo.fromNullable(ref.version); - - final query = select(documents) - ..where( - (tbl) => Expression.and([ - tbl.idHi.equals(id.high), - tbl.idLo.equals(id.low), - if (ver != null) ...[ - tbl.verHi.equals(ver.high), - tbl.verLo.equals(ver.low), - ], - ]), - ) - ..limit(1); - - return query.getSingleOrNull(); - } - - Stream _transformRefsStreamToCount( - Stream> source, { - CatalystId? author, - }) { - return source.asyncMap((allRefs) async { - final latestActions = await _getProposalsLatestAction(); - final hiddenRefs = latestActions - .where((element) => element.action.isHidden) - .map((e) => e.proposalRef); - final finalsRefs = latestActions - .where((element) => element.action.isFinal) - .map((e) => e.proposalRef); - - final favoritesRefs = await _getFavoritesRefs(); - final votedRefs = await _getVotedRefs(); - final myRefs = await _maybeGetAuthorProposalsLooseRefs(author: author); - - final notHidden = allRefs.where((ref) => hiddenRefs.none((myRef) => myRef.id == ref.id)); - - final total = notHidden; - final finals = notHidden.where((ref) => finalsRefs.any((myRef) => myRef.id == ref.id)); - final favorites = notHidden.where((ref) => favoritesRefs.any((fav) => fav.id == ref.id)); - final favoritesFinals = finals.where((ref) => favoritesRefs.any((fav) => fav.id == ref.id)); - final my = notHidden.where((ref) => myRefs.any((myRef) => myRef.id == ref.id)); - final myFinals = my.where((ref) => finalsRefs.any((myRef) => myRef.id == ref.id)); - final votedOn = notHidden.where((ref) => votedRefs.any((voted) => voted.id == ref.id)); - - return ProposalsCount( - total: total.length, - drafts: total.length - finals.length, - finals: finals.length, - favorites: favorites.length, - favoritesFinals: favoritesFinals.length, - my: my.length, - myFinals: myFinals.length, - voted: votedOn.length, - ); - }); - } -} - -abstract interface class ProposalsDao { - Future> queryProposals({ - SignedDocumentRef? categoryRef, - required ProposalsFilters filters, - }); - - Future> queryProposalsPage({ - required PageRequest request, - required ProposalsFilters filters, - required ProposalsOrder order, - }); - - Stream watchCount({ - required ProposalsCountFilters filters, - }); - - Stream> watchProposalsPage({ - required PageRequest request, - required ProposalsFilters filters, - required ProposalsOrder order, - }); -} - -final class _IdsFilter extends Equatable { - final Iterable? include; - final Iterable? exclude; - - // ignore: unused_element_parameter - const _IdsFilter({this.include, this.exclude}); - - const _IdsFilter.exclude(this.exclude) : include = null; - - const _IdsFilter.include(this.include) : exclude = null; - - @override - List get props => [include, exclude]; -} - -final class _ProposalActions extends Equatable { - final SignedDocumentRef selfRef; - final SignedDocumentRef proposalRef; - final ProposalSubmissionAction action; - - const _ProposalActions({ - required this.selfRef, - required this.proposalRef, - required this.action, - }); - - @override - List get props => [selfRef, proposalRef, action]; -} - -extension on ProposalSubmissionAction { - bool get isDraft => this == ProposalSubmissionAction.draft; - - bool get isFinal => this == ProposalSubmissionAction.aFinal; - - bool get isHidden => this == ProposalSubmissionAction.hide; -} - -extension on TypedResult { - SignedDocumentRef readSelfRef($DocumentsTable table) { - final idHiLo = (read(table.idHi)!, read(table.idLo)!); - final verHiLo = (read(table.verHi)!, read(table.verLo)!); - - final id = UuidHiLo(high: idHiLo.$1, low: idHiLo.$2).uuid; - final ver = UuidHiLo(high: verHiLo.$1, low: verHiLo.$2).uuid; - - return SignedDocumentRef(id: id, version: ver); - } -} - -extension on ProposalsOrder { - List terms($DocumentsTable table) { - return switch (this) { - Alphabetical() => [ - OrderingTerm.asc(table.content.title.collate(Collate.noCase), nulls: NullsOrder.last), - OrderingTerm.desc(table.verHi), - ], - Budget(:final isAscending) => [ - OrderingTerm( - expression: table.content.requestedFunds, - mode: isAscending ? OrderingMode.asc : OrderingMode.desc, - nulls: NullsOrder.last, - ), - OrderingTerm.desc(table.verHi), - ], - UpdateDate(:final isAscending) => [ - OrderingTerm( - expression: table.verHi, - mode: isAscending ? OrderingMode.asc : OrderingMode.desc, - ), - ], - }; - } -} diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/dao/proposals_v2_dao.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/dao/proposals_v2_dao.dart index 5da89ee5609f..a10c0b41c5a6 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/dao/proposals_v2_dao.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/dao/proposals_v2_dao.dart @@ -570,34 +570,25 @@ class DriftProposalsV2Dao extends DatabaseAccessor /// - Identifies the newest version of each proposal /// - Uses: idx_documents_v2_type_id /// - /// 2. **version_lists** - /// - Collects all version ids for each proposal into comma-separated string - /// - Ordered by ver ASC for consistent version history - /// - Used to show version dropdown in UI - /// - /// 3. **latest_actions** + /// 2. **latest_actions** /// - Groups all proposal actions by ref_id and finds MAX(ver) /// - Ensures we only check the most recent action per proposal /// - Uses: idx_documents_v2_type_ref_id /// - /// 4. **action_status** + /// 3. **action_status** /// - Joins actual action documents with latest_actions /// - Extracts action type ('draft'/'final'/'hide') from JSON content /// - Extracts ref_ver which may point to specific proposal version /// - COALESCE defaults to 'draft' when action field is missing /// - Uses: idx_documents_v2_type_ref_id_ver /// - /// 5. **effective_proposals** + /// 4. **effective_proposals** /// - Applies version resolution logic: /// * Hide action: Filtered out by WHERE NOT EXISTS /// * Final action with ref_ver: Uses ref_ver (specific pinned version) /// * Final action without ref_ver OR draft OR no action: Uses max_ver (latest) /// - LEFT JOIN ensures proposals without actions are included (default to draft) /// - /// 6. **comments_count** - /// - Counts comments per proposal version - /// - Joins on both ref_id and ref_ver for version-specific counts - /// /// **Final Query:** /// - Joins documents_v2 with effective_proposals to get full document data /// - LEFT JOINs with comments, favorites, and template for enrichment @@ -625,24 +616,12 @@ class DriftProposalsV2Dao extends DatabaseAccessor final cteQuery = ''' - WITH latest_proposals AS ( + WITH latest_proposals AS ( SELECT id, MAX(ver) as max_ver FROM documents_v2 WHERE type = ? GROUP BY id ), - version_lists AS ( - SELECT - id, - GROUP_CONCAT(ver, ',') as version_ids_str - FROM ( - SELECT id, ver - FROM documents_v2 - WHERE type = ? - ORDER BY id, ver ASC - ) - GROUP BY id - ), latest_actions AS ( SELECT ref_id, MAX(ver) as max_action_ver FROM documents_v2 @@ -661,39 +640,46 @@ class DriftProposalsV2Dao extends DatabaseAccessor effective_proposals AS ( SELECT lp.id, + -- Business Logic: Use specific version if final, otherwise latest CASE WHEN ast.action_type = 'final' AND ast.ref_ver IS NOT NULL AND ast.ref_ver != '' THEN ast.ref_ver ELSE lp.max_ver END as ver, - ast.action_type, - vl.version_ids_str + ast.action_type FROM latest_proposals lp LEFT JOIN action_status ast ON lp.id = ast.ref_id - LEFT JOIN version_lists vl ON lp.id = vl.id WHERE NOT EXISTS ( + -- Business Logic: Hide action hides all versions SELECT 1 FROM action_status hidden WHERE hidden.ref_id = lp.id AND hidden.action_type = 'hide' ) - ), - comments_count AS ( - SELECT - c.ref_id, - c.ref_ver, - COUNT(*) as count - FROM documents_v2 c - WHERE c.type = ? - GROUP BY c.ref_id, c.ref_ver ) SELECT $proposalColumns, $templateColumns, - ep.action_type, - ep.version_ids_str, - COALESCE(cc.count, 0) as comments_count, + ep.action_type, + + -- Only executes for the rows in the page + ( + SELECT GROUP_CONCAT(v_list.ver, ',') + FROM ( + SELECT ver + FROM documents_v2 v_sub + WHERE v_sub.id = p.id AND v_sub.type = ? + ORDER BY v_sub.ver ASC + ) v_list + ) as version_ids_str, + + -- Only executes for the rows in the page + ( + SELECT COUNT(*) + FROM documents_v2 c + WHERE c.ref_id = p.id AND c.ref_ver = p.ver AND c.type = ? + ) as comments_count, + COALESCE(dlm.is_favorite, 0) as is_favorite FROM documents_v2 p INNER JOIN effective_proposals ep ON p.id = ep.id AND p.ver = ep.ver - LEFT JOIN comments_count cc ON p.id = cc.ref_id AND p.ver = cc.ref_ver LEFT JOIN documents_local_metadata dlm ON p.id = dlm.id LEFT JOIN documents_v2 t ON p.template_id = t.id AND p.template_ver = t.ver AND t.type = ? WHERE p.type = ? $whereClause @@ -714,13 +700,20 @@ class DriftProposalsV2Dao extends DatabaseAccessor return customSelect( cteQuery, variables: [ - Variable.withString(DocumentType.proposalDocument.uuid), + // CTE Variables + // latest_proposals, latest_actions, action_status Variable.withString(DocumentType.proposalDocument.uuid), Variable.withString(DocumentType.proposalActionDocument.uuid), Variable.withString(DocumentType.proposalActionDocument.uuid), + // Select Subquery Variables (Order matters!) + // version_ids_str subquery, comments_count subquery + Variable.withString(DocumentType.proposalDocument.uuid), Variable.withString(DocumentType.commentDocument.uuid), + // Main Join Variables + // template join, main WHERE Variable.withString(DocumentType.proposalTemplate.uuid), Variable.withString(DocumentType.proposalDocument.uuid), + // Limit/Offset Variable.withInt(size), Variable.withInt(page * size), ], diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/database.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/database.dart index f0648e320a42..d5fee119da88 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/database.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/database.dart @@ -1,13 +1,6 @@ export 'catalyst_database.dart' show CatalystDatabase; export 'catalyst_database_config.dart'; -export 'dao/documents_dao.dart' show DocumentsDao; -export 'dao/drafts_dao.dart' show DraftsDao; -export 'dao/favorites_dao.dart' show FavoritesDao; -export 'dao/proposals_dao.dart' show ProposalsDao; -export 'model/joined_proposal_entity.dart'; -export 'table/documents.drift.dart' show DocumentEntity; -export 'table/documents_favorite.drift.dart' show DocumentFavoriteEntity; -export 'table/documents_metadata.dart'; -export 'table/documents_metadata.drift.dart' show DocumentMetadataEntity; -export 'table/drafts.drift.dart' show DocumentDraftEntity; -export 'typedefs.dart'; +export 'dao/documents_v2_dao.dart' show DocumentsV2Dao; +export 'dao/documents_v2_local_metadata_dao.dart' show DocumentsV2LocalMetadataDao; +export 'dao/local_draft_documents_v2_dao.dart' show LocalDraftDocumentsV2Dao; +export 'dao/proposals_v2_dao.dart' show ProposalsV2Dao; diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/model/joined_proposal_entity.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/model/joined_proposal_entity.dart deleted file mode 100644 index 64e17f3c2455..000000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/model/joined_proposal_entity.dart +++ /dev/null @@ -1,30 +0,0 @@ -import 'package:catalyst_voices_repositories/catalyst_voices_repositories.dart'; -import 'package:equatable/equatable.dart'; - -/// Specialized entity that represents a proposal. -/// -/// It is a result of joining multiple tables to get all the information about a proposal. -final class JoinedProposalEntity extends Equatable { - final DocumentEntity proposal; - final DocumentEntity template; - final DocumentEntity? action; - final int commentsCount; - final List versions; - - const JoinedProposalEntity({ - required this.proposal, - required this.template, - this.action, - this.commentsCount = 0, - this.versions = const [], - }); - - @override - List get props => [ - proposal, - template, - action, - commentsCount, - versions, - ]; -} diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/query/jsonb_expressions.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/query/jsonb_expressions.dart deleted file mode 100644 index 8365e339f5e4..000000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/query/jsonb_expressions.dart +++ /dev/null @@ -1,200 +0,0 @@ -import 'package:catalyst_voices_models/catalyst_voices_models.dart'; -import 'package:catalyst_voices_repositories/src/database/table/documents.dart' show Documents; -import 'package:catalyst_voices_repositories/src/database/table/documents.drift.dart'; -import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; -import 'package:drift/drift.dart'; -import 'package:drift/extensions/json1.dart'; - -class BaseJsonQueryExpression extends Expression { - final String jsonContent; - final NodeId nodeId; - final String searchValue; - final bool useExactMatch; - - const BaseJsonQueryExpression({ - required this.jsonContent, - required this.nodeId, - required this.searchValue, - this.useExactMatch = false, - }); - - @override - void writeInto(GenerationContext context) { - final sql = JsonBExpressions.generateSqlForJsonQuery( - jsonContent: jsonContent, - nodeId: nodeId, - searchValue: searchValue, - ); - - context.buffer.write(sql); - } -} - -final class ContainsAuthorId extends BaseJsonQueryExpression { - ContainsAuthorId({ - required CatalystId id, - }) : super( - searchValue: id.toSignificant().toUri().toStringWithoutScheme(), - nodeId: ProposalMetadata.authorsNode, - jsonContent: 'metadata', - ); -} - -final class ContainsContentAuthorName extends BaseJsonQueryExpression { - ContainsContentAuthorName({ - required String query, - }) : super( - searchValue: query, - nodeId: ProposalDocument.authorNameNodeId, - jsonContent: 'content', - ); -} - -final class ContainsMetadataAuthorName extends BaseJsonQueryExpression { - ContainsMetadataAuthorName({ - required String query, - }) : super( - searchValue: query, - nodeId: ProposalMetadata.authorsNode, - jsonContent: 'metadata', - ); -} - -final class ContainsTitle extends BaseJsonQueryExpression { - ContainsTitle({ - required String query, - }) : super( - searchValue: query, - nodeId: ProposalDocument.titleNodeId, - jsonContent: 'content', - ); -} - -class JsonBExpressions { - const JsonBExpressions(); - - static String generateSqlForJsonQuery({ - required String jsonContent, - required NodeId nodeId, - required String searchValue, - bool useExactMatch = false, - }) { - final valueComparison = useExactMatch ? "= '$searchValue'" : "LIKE '%$searchValue%'"; - final handler = WildcardPathHandler.fromNodeId(nodeId); - final wildcardPaths = handler.getWildcardPaths; - - if (!handler.hasWildcard || wildcardPaths == null) { - return _queryJsonExtract( - jsonContent: jsonContent, - nodeId: nodeId, - valueComparison: valueComparison, - ); - } - - final arrayPath = wildcardPaths.prefix.value.isEmpty ? '' : wildcardPaths.prefix.asPath; - final fieldName = wildcardPaths.suffix?.asPath; - - if (wildcardPaths.prefix.value.isEmpty) { - return _queryJsonTreeForKey( - jsonContent: jsonContent, - fieldName: fieldName?.substring(2), - valueComparison: valueComparison, - ); - } - - if (fieldName != null) { - return _queryJsonEachForWildcard( - jsonContent: jsonContent, - arrayPath: arrayPath, - fieldName: fieldName, - valueComparison: valueComparison, - ); - } - - return _queryJsonTreeForWildcard( - jsonContent: jsonContent, - arrayPath: arrayPath, - valueComparison: valueComparison, - ); - } - - static String _queryJsonEachForWildcard({ - required String jsonContent, - required String arrayPath, - required String fieldName, - required String valueComparison, - }) { - return "EXISTS (SELECT 1 FROM json_each(json_extract($jsonContent, '$arrayPath')) WHERE json_extract(value, '$fieldName') $valueComparison)"; - } - - static String _queryJsonExtract({ - required String jsonContent, - required NodeId nodeId, - required String valueComparison, - }) { - return "json_extract($jsonContent, '${nodeId.asPath}') $valueComparison"; - } - - static String _queryJsonTreeForKey({ - required String jsonContent, - required String? fieldName, - required String valueComparison, - }) { - return "EXISTS (SELECT 1 FROM json_tree($jsonContent) WHERE key LIKE '$fieldName' AND json_tree.value $valueComparison)"; - } - - static String _queryJsonTreeForWildcard({ - required String jsonContent, - required String arrayPath, - required String valueComparison, - }) { - return "EXISTS (SELECT 1 FROM json_tree($jsonContent, '$arrayPath') WHERE json_tree.value $valueComparison)"; - } -} - -extension on NodeId { - /// Converts [NodeId] into jsonb well-formatted path argument. - /// - /// This relies on fact that [NodeId] (especially [DocumentNodeId]) is already following - /// convention of separating paths with '.' (dots). - /// - /// Read more: https://sqlite.org/json1.html#path_arguments. - String get asPath => '\$.$value'; -} - -/// Extension allowing extraction of commonly used values from [DocumentDataContent] in a type-safe way. -extension ContentColumnExt on GeneratedColumnWithTypeConverter { - Expression get requestedFunds => jsonExtract(ProposalDocument.requestedFundsNodeId.asPath); - - Expression get title => jsonExtract(ProposalDocument.titleNodeId.asPath); - - Expression hasTitle(String query) => ContainsTitle(query: query); -} - -/// Extension allowing extraction of commonly used values from [Documents] table in a type-safe way. -extension DocumentTableExt on $DocumentsTable { - Expression search(String query) { - return Expression.or( - [ - ...metadata.hasAuthorName(query), - content.hasTitle(query), - ], - ); - } -} - -/// Extension allowing extraction of commonly used values from [DocumentDataMetadata] in a type-safe way. -extension MetadataColumnExt on GeneratedColumnWithTypeConverter { - List> hasAuthorName(String name) { - return [ - ContainsMetadataAuthorName(query: name), - ContainsContentAuthorName(query: name), - ]; - } - - Expression isAuthor(CatalystId id) => ContainsAuthorId(id: id); - - Expression isCategory(SignedDocumentRef ref) { - return jsonExtract(ProposalMetadata.categoryIdNode.asPath).equals(ref.id); - } -} diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/table/converter/document_converters.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/table/converter/document_converters.dart index 40bf6592721b..f445781b99d4 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/table/converter/document_converters.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/table/converter/document_converters.dart @@ -1,12 +1,8 @@ import 'package:catalyst_voices_models/catalyst_voices_models.dart'; -import 'package:catalyst_voices_repositories/src/dto/document/document_data_dto.dart'; import 'package:drift/drift.dart'; typedef DocumentContentJsonBConverter = JsonTypeConverter2; -typedef DocumentMetadataJsonBConverter = - JsonTypeConverter2; - abstract final class DocumentConverters { /// Converts [DocumentType] to String for text column. static const TypeConverter type = _DocumentTypeConverter(); @@ -17,13 +13,6 @@ abstract final class DocumentConverters { fromJson: (json) => DocumentDataContent(json! as Map), toJson: (content) => content.data, ); - - /// Converts [DocumentDataMetadata] into json for bloc column. - /// Required for jsonb queries. - static final DocumentMetadataJsonBConverter metadata = TypeConverter.jsonb( - fromJson: (json) => DocumentDataMetadataDto.fromJson(json! as Map).toModel(), - toJson: (metadata) => DocumentDataMetadataDto.fromModel(metadata).toJson(), - ); } final class _DocumentTypeConverter extends TypeConverter { diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/table/documents.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/table/documents.dart deleted file mode 100644 index 0917d6089c1e..000000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/table/documents.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:catalyst_voices_models/catalyst_voices_models.dart'; -import 'package:catalyst_voices_repositories/src/database/table/mixin/document_table_mixin.dart'; -import 'package:catalyst_voices_repositories/src/database/table/mixin/id_table_mixin.dart'; -import 'package:catalyst_voices_repositories/src/database/table/mixin/ver_table_mixin.dart'; -import 'package:drift/drift.dart'; - -/// This table stores a record of each document (including its content and -/// related metadata). -/// -/// Its representation of [DocumentData] class. -@TableIndex(name: 'idx_doc_type', columns: {#type}) -@TableIndex(name: 'idx_unique_ver', columns: {#verHi, #verLo}, unique: true) -@DataClassName('DocumentEntity') -class Documents extends Table with IdHiLoTableMixin, VerHiLoTableMixin, DocumentTableMixin { - DateTimeColumn get createdAt => dateTime()(); - - @override - Set>? get primaryKey => { - idHi, - idLo, - verHi, - verLo, - }; -} diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/table/documents_favorite.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/table/documents_favorite.dart deleted file mode 100644 index 775c38483600..000000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/table/documents_favorite.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:catalyst_voices_repositories/src/database/table/converter/document_converters.dart'; -import 'package:catalyst_voices_repositories/src/database/table/mixin/id_table_mixin.dart'; -import 'package:drift/drift.dart'; - -@TableIndex(name: 'idx_fav_type', columns: {#type}) -@TableIndex(name: 'idx_fav_unique_id', columns: {#idHi, #idLo}, unique: true) -@DataClassName('DocumentFavoriteEntity') -class DocumentsFavorites extends Table with IdHiLoTableMixin { - BoolColumn get isFavorite => boolean()(); - - @override - Set get primaryKey => { - idHi, - idLo, - }; - - TextColumn get type => text().map(DocumentConverters.type)(); -} diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/table/documents_metadata.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/table/documents_metadata.dart deleted file mode 100644 index 1b32814ce256..000000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/table/documents_metadata.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'package:catalyst_voices_repositories/src/database/table/documents.drift.dart'; -import 'package:catalyst_voices_repositories/src/database/table/mixin/ver_table_mixin.dart'; -import 'package:drift/drift.dart'; - -/// Syntax sugar num to working with [DocumentsMetadata] queries -/// so keys names are strongly defined here. -/// -/// Add new fields as needed. -enum DocumentMetadataFieldKey { - title, -} - -/// This table breaks out metadata into a key-value structure for each -/// document version, enabling more granular or indexed queries -@TableIndex( - name: 'idx_doc_metadata_key_value', - columns: {#fieldKey, #fieldValue}, -) -@DataClassName('DocumentMetadataEntity') -class DocumentsMetadata extends Table with VerHiLoTableMixin { - @override - List get customConstraints => [ - /// Referring with two columns throws a - /// "SqliteException(1): foreign key mismatch" - /// But when doing it explicitly it no longer complains - 'FOREIGN KEY("ver_hi", "ver_lo") REFERENCES "${$DocumentsTable.$name}"("ver_hi", "ver_lo") ON DELETE CASCADE ON UPDATE CASCADE', - ]; - - /// e.g. 'category', 'title', 'description' - TextColumn get fieldKey => textEnum()(); - - /// The actual value (for category, title, description, etc.) - TextColumn get fieldValue => text()(); - - @override - Set get primaryKey => { - verHi, - verLo, - fieldKey, - }; -} diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/table/drafts.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/table/drafts.dart deleted file mode 100644 index 7164fed83336..000000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/table/drafts.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:catalyst_voices_repositories/src/database/table/documents_metadata.dart'; -import 'package:catalyst_voices_repositories/src/database/table/mixin/document_table_mixin.dart'; -import 'package:catalyst_voices_repositories/src/database/table/mixin/id_table_mixin.dart'; -import 'package:catalyst_voices_repositories/src/database/table/mixin/ver_table_mixin.dart'; -import 'package:drift/drift.dart'; - -/// This table holds in-progress (draft) versions of documents that are not yet -/// been made public or submitted. -/// -/// [content] will be encrypted in future but we still need to be able to -/// search for drafts against fields like [title] for example. -/// -/// In future we may need to delete [title] or add more columns for search -/// purposes. If there will be too many requirements we may introduce -/// DraftsMetadata table, similar to [DocumentsMetadata]. -@TableIndex(name: 'idx_draft_type', columns: {#type}) -@DataClassName('DocumentDraftEntity') -class Drafts extends Table with IdHiLoTableMixin, VerHiLoTableMixin, DocumentTableMixin { - @override - Set>? get primaryKey => { - idHi, - idLo, - verHi, - verLo, - }; - - /// not encrypted title for search - TextColumn get title => text()(); -} diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/table/mixin/document_table_mixin.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/table/mixin/document_table_mixin.dart deleted file mode 100644 index ef56eb724d5c..000000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/table/mixin/document_table_mixin.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:catalyst_voices_models/catalyst_voices_models.dart'; -import 'package:catalyst_voices_repositories/src/database/table/converter/document_converters.dart'; -import 'package:drift/drift.dart'; - -mixin DocumentTableMixin on Table { - /// Encoded version of [DocumentData.content] - /// - /// Uses jsonb - BlobColumn get content => blob().map(DocumentConverters.content)(); - - /// Encoded version of [DocumentData.metadata] - /// - /// Uses jsonb - BlobColumn get metadata => blob().map(DocumentConverters.metadata)(); - - /// Refers to [DocumentType] uuid. - TextColumn get type => text().map(DocumentConverters.type)(); -} diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/table/mixin/id_table_mixin.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/table/mixin/id_table_mixin.dart deleted file mode 100644 index 48937e49339e..000000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/table/mixin/id_table_mixin.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:catalyst_voices_models/catalyst_voices_models.dart'; -import 'package:drift/drift.dart'; - -/// Commonly used pattern for representing uuid as id. -/// -/// See [UuidHiLo]. -mixin IdHiLoTableMixin on Table { - Int64Column get idHi => int64()(); - - Int64Column get idLo => int64()(); -} diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/table/mixin/ver_table_mixin.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/table/mixin/ver_table_mixin.dart deleted file mode 100644 index e0db6b3f6eac..000000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/table/mixin/ver_table_mixin.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:catalyst_voices_models/catalyst_voices_models.dart'; -import 'package:drift/drift.dart'; - -/// Commonly used pattern for representing uuid as ver. -/// -/// See [UuidHiLo]. -mixin VerHiLoTableMixin on Table { - Int64Column get verHi => int64()(); - - Int64Column get verLo => int64()(); -} diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/typedefs.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/typedefs.dart deleted file mode 100644 index ee69d75993dd..000000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/typedefs.dart +++ /dev/null @@ -1,8 +0,0 @@ -import 'package:catalyst_voices_repositories/src/database/table/documents.drift.dart'; -import 'package:catalyst_voices_repositories/src/database/table/documents_metadata.drift.dart'; - -/// Each document has list of metadata rows -typedef DocumentEntityWithMetadata = ({ - DocumentEntity document, - List metadata, -}); diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/document/document_repository.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/document/document_repository.dart index 1f4e5bfaaf0d..9e2e9b709acd 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/document/document_repository.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/document/document_repository.dart @@ -25,7 +25,6 @@ abstract interface class DocumentRepository { DraftDataSource drafts, SignedDocumentDataSource localDocuments, DocumentDataRemoteSource remoteDocuments, - DocumentFavoriteSource favoriteDocuments, ) = DocumentRepositoryImpl; /// Analyzes the database to gather statistics and potentially optimize it. @@ -50,7 +49,7 @@ abstract interface class DocumentRepository { }); /// Returns all matching [DocumentData] to given [ref]. - Future> getAllDocumentsData({ + Future> findAllVersions({ required DocumentRef ref, }); @@ -88,14 +87,14 @@ abstract interface class DocumentRepository { /// Returns latest matching [DocumentRef] version with same id as [ref]. Future getLatestOf({required DocumentRef ref}); - /// Returns count of documents matching [ref] id and [type]. + /// Returns count of documents matching [referencing] id and [type]. Future getRefCount({ - required DocumentRef ref, + required DocumentRef referencing, required DocumentType type, }); Future getRefToDocumentData({ - required DocumentRef refTo, + required DocumentRef referencing, required DocumentType type, }); @@ -113,10 +112,7 @@ abstract interface class DocumentRepository { required List refs, }); - /// Similar to [watchIsDocumentFavorite] but stops after first emit. - Future isDocumentFavorite({ - required DocumentRef ref, - }); + Future isFavorite(DocumentRef ref); /// Parses a document [data] previously encoded by [encodeDocumentForExport]. /// @@ -169,13 +165,6 @@ abstract interface class DocumentRepository { required CatalystId authorId, }); - /// Updates fav status matching [ref]. - Future updateDocumentFavorite({ - required DocumentRef ref, - required DocumentType type, - required bool isFavorite, - }); - /// In context of [document] selfRef: /// /// - [DraftRef] -> Creates/updates a local document draft. @@ -188,16 +177,9 @@ abstract interface class DocumentRepository { required DocumentData document, }); - /// Emits list of all favorite ids. - /// - /// All returned ids are loose and won't specify version. - Stream> watchAllDocumentsFavoriteIds({ - DocumentType? type, - }); - /// Emits number of matching documents Stream watchCount({ - DocumentRef? refTo, + DocumentRef? referencing, DocumentType? type, }); @@ -222,24 +204,19 @@ abstract interface class DocumentRepository { bool unique = false, bool getLocalDrafts = false, CatalystId? authorId, - DocumentRef? refTo, - }); - - /// Emits changes to fav status of [ref]. - Stream watchIsDocumentFavorite({ - required DocumentRef ref, + DocumentRef? referencing, }); /// Looking for document with matching refTo and type. - /// It return documents data that have a reference that matches [refTo] + /// It return documents data that have a reference that matches [referencing] /// /// This method is used when we want to find a document that has a reference /// to a document that we are looking for. /// /// For example, we want to find latest document action that were made - /// on a [refTo] document. + /// on a [referencing] document. Stream watchRefToDocumentData({ - required DocumentRef refTo, + required DocumentRef referencing, required DocumentType type, }); } @@ -249,7 +226,6 @@ final class DocumentRepositoryImpl implements DocumentRepository { final DraftDataSource _drafts; final SignedDocumentDataSource _localDocuments; final DocumentDataRemoteSource _remoteDocuments; - final DocumentFavoriteSource _favoriteDocuments; final _documentDataLock = Lock(); @@ -258,7 +234,6 @@ final class DocumentRepositoryImpl implements DocumentRepository { this._drafts, this._localDocuments, this._remoteDocuments, - this._favoriteDocuments, ); @override @@ -279,10 +254,10 @@ final class DocumentRepositoryImpl implements DocumentRepository { } @override - Future> getAllDocumentsData({required DocumentRef ref}) async { + Future> findAllVersions({required DocumentRef ref}) async { final all = switch (ref) { - DraftRef() => await _drafts.getAll(ref: ref), - SignedDocumentRef() => await _localDocuments.getAll(ref: ref), + DraftRef() => await _drafts.findAll(ref: ref), + SignedDocumentRef() => await _localDocuments.findAll(ref: ref), }..sort(); return all; @@ -292,8 +267,8 @@ final class DocumentRepositoryImpl implements DocumentRepository { Future> getAllVersionsOfId({ required String id, }) async { - final localRefs = await _localDocuments.queryVersionsOfId(id: id); - final drafts = await _drafts.queryVersionsOfId(id: id); + final localRefs = await _localDocuments.findAll(ref: SignedDocumentRef(id: id)); + final drafts = await _drafts.findAll(ref: DraftRef(id: id)); return [...drafts, ...localRefs]; } @@ -303,22 +278,28 @@ final class DocumentRepositoryImpl implements DocumentRepository { required DocumentRef ref, bool useCache = true, }) async { - return switch (ref) { - SignedDocumentRef() => _getSignedDocumentData(ref: ref, useCache: useCache), + final documentData = switch (ref) { + SignedDocumentRef() => await _getSignedDocumentData(ref: ref, useCache: useCache), DraftRef() when !useCache => throw DocumentNotFoundException( ref: ref, message: '$ref can not be resolved while not using cache', ), - DraftRef() => _getDraftDocumentData(ref: ref), + DraftRef() => await _getDraftDocumentData(ref: ref), }; + + if (documentData == null) { + throw DocumentNotFoundException(ref: ref); + } + + return documentData; } @override Future getLatestDocument({ CatalystId? authorId, }) async { - final latestDocument = await _localDocuments.getLatest(authorId: authorId); - final latestDraft = await _drafts.getLatest(authorId: authorId); + final latestDocument = await _localDocuments.findFirst(authorId: authorId); + final latestDraft = await _drafts.findFirst(); return [latestDocument, latestDraft].nonNulls.sorted((a, b) => a.compareTo(b)).firstOrNull; } @@ -326,28 +307,28 @@ final class DocumentRepositoryImpl implements DocumentRepository { // TODO(damian-molinski): consider also checking with remote source. @override Future getLatestOf({required DocumentRef ref}) async { - final draft = await _drafts.getLatestOf(ref: ref); + final draft = await _drafts.getLatestRefOf(ref); if (draft != null) { return draft; } - return _localDocuments.getLatestOf(ref: ref); + return _localDocuments.getLatestRefOf(ref); } @override Future getRefCount({ - required DocumentRef ref, + required DocumentRef referencing, required DocumentType type, }) { - return _localDocuments.getRefCount(ref: ref, type: type); + return _localDocuments.count(referencing: referencing, type: type); } @override Future getRefToDocumentData({ - required DocumentRef refTo, + required DocumentRef referencing, required DocumentType type, }) { - return _localDocuments.getRefToDocumentData(refTo: refTo, type: type); + return _localDocuments.findFirst(referencing: referencing, type: type); } @override @@ -378,10 +359,8 @@ final class DocumentRepositoryImpl implements DocumentRepository { } @override - Future isDocumentFavorite({required DocumentRef ref}) { - assert(!ref.isExact, 'Favorite ref have to be loose!'); - - return _favoriteDocuments.watchIsDocumentFavorite(ref.id).first; + Future isFavorite(DocumentRef ref) { + return _db.localMetadataDao.isFavorite(ref.id); } @override @@ -405,9 +384,9 @@ final class DocumentRepositoryImpl implements DocumentRepository { bool includeLocalDrafts = false, }) async { List documents; - final localDocuments = await _localDocuments.queryVersionsOfId(id: id); + final localDocuments = await _localDocuments.findAll(ref: SignedDocumentRef(id: id)); if (includeLocalDrafts) { - final localDrafts = await _drafts.queryVersionsOfId(id: id); + final localDrafts = await _drafts.findAll(ref: DraftRef(id: id)); documents = [...localDocuments, ...localDrafts]; } else { documents = localDocuments; @@ -430,10 +409,9 @@ final class DocumentRepositoryImpl implements DocumentRepository { Future removeAll({ bool keepLocalDrafts = false, }) async { - final deletedDrafts = keepLocalDrafts ? 0 : await _drafts.deleteAll(); - final deletedDocuments = keepLocalDrafts - ? await _localDocuments.deleteAllRespectingLocalDrafts() - : await _localDocuments.deleteAll(); + final deletedDrafts = keepLocalDrafts ? 0 : await _drafts.delete(); + final excludeTypes = keepLocalDrafts ? [DocumentType.proposalTemplate] : null; + final deletedDocuments = await _localDocuments.delete(excludeTypes: excludeTypes); return deletedDrafts + deletedDocuments; } @@ -470,21 +448,6 @@ final class DocumentRepositoryImpl implements DocumentRepository { return newDocument.metadata.selfRef; } - @override - Future updateDocumentFavorite({ - required DocumentRef ref, - required DocumentType type, - required bool isFavorite, - }) { - assert(!ref.isExact, 'Favorite ref have to be loose!'); - - return _favoriteDocuments.updateDocumentFavorite( - ref.id, - type: type, - isFavorite: isFavorite, - ); - } - @override Future upsertDocument({ required DocumentData document, @@ -505,15 +468,15 @@ final class DocumentRepositoryImpl implements DocumentRepository { bool getLocalDrafts = false, DocumentType? type, CatalystId? authorId, - DocumentRef? refTo, + DocumentRef? referencing, }) { final localDocs = _localDocuments .watchAll( - limit: limit, - unique: unique, + latestOnly: unique, type: type, authorId: authorId, - refTo: refTo, + referencing: referencing, + limit: limit ?? 200, ) .asyncMap( (documents) async => _processDocuments( @@ -528,9 +491,8 @@ final class DocumentRepositoryImpl implements DocumentRepository { final localDrafts = _drafts .watchAll( - limit: limit, type: type, - authorId: authorId, + limit: limit ?? 100, ) .asyncMap( (documents) async => _processDocuments( @@ -555,20 +517,13 @@ final class DocumentRepositoryImpl implements DocumentRepository { .distinct(listEquals); } - @override - Stream> watchAllDocumentsFavoriteIds({ - DocumentType? type, - }) { - return _favoriteDocuments.watchAllFavoriteIds(type: type); - } - @override Stream watchCount({ - DocumentRef? refTo, + DocumentRef? referencing, DocumentType? type, }) { return _localDocuments.watchCount( - refTo: refTo, + referencing: referencing, type: type, ); } @@ -602,7 +557,7 @@ final class DocumentRepositoryImpl implements DocumentRepository { bool unique = false, bool getLocalDrafts = false, CatalystId? authorId, - DocumentRef? refTo, + DocumentRef? referencing, }) { return watchAllDocuments( refGetter: refGetter, @@ -611,7 +566,7 @@ final class DocumentRepositoryImpl implements DocumentRepository { unique: unique, getLocalDrafts: getLocalDrafts, authorId: authorId, - refTo: refTo, + referencing: referencing, ); } @@ -647,29 +602,22 @@ final class DocumentRepositoryImpl implements DocumentRepository { }); } - @override - Stream watchIsDocumentFavorite({required DocumentRef ref}) { - assert(!ref.isExact, 'Favorite ref have to be loose!'); - - return _favoriteDocuments.watchIsDocumentFavorite(ref.id); - } - @override Stream watchRefToDocumentData({ - required DocumentRef refTo, + required DocumentRef referencing, required DocumentType type, ValueResolver refGetter = _templateResolver, }) { - return _localDocuments.watchRefToDocumentData(refTo: refTo, type: type).distinct(); + return _localDocuments.watch(referencing: referencing, type: type).distinct(); } - Future _getDraftDocumentData({ + Future _getDraftDocumentData({ required DraftRef ref, }) async { - return _drafts.get(ref: ref); + return _drafts.get(ref); } - Future _getSignedDocumentData({ + Future _getSignedDocumentData({ required SignedDocumentRef ref, bool useCache = true, }) async { @@ -683,12 +631,12 @@ final class DocumentRepositoryImpl implements DocumentRepository { final isCached = useCache && await _localDocuments.exists(ref: ref); if (isCached) { - return _localDocuments.get(ref: ref); + return _localDocuments.get(ref); } - final document = await _remoteDocuments.get(ref: ref); + final document = await _remoteDocuments.get(ref); - if (useCache) { + if (useCache && document != null) { await _localDocuments.save(data: document); } diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/document/source/database_documents_data_source.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/document/source/database_documents_data_source.dart index 49f7a91e63f4..f34444dbdfea 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/document/source/database_documents_data_source.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/document/source/database_documents_data_source.dart @@ -24,13 +24,19 @@ final class DatabaseDocumentsDataSource ); @override - Future deleteAll() { - return _database.documentsDao.deleteAll(); + Future count({ + DocumentType? type, + DocumentRef? ref, + DocumentRef? referencing, + }) { + return _database.documentsV2Dao.count(type: type, ref: ref, referencing: referencing); } @override - Future deleteAllRespectingLocalDrafts() { - return _database.documentsDao.deleteAll(keepTemplatesForLocalDrafts: true); + Future delete({ + List? excludeTypes, + }) { + return _database.documentsV2Dao.deleteWhere(excludeTypes: excludeTypes); } @override @@ -44,58 +50,44 @@ final class DatabaseDocumentsDataSource } @override - Future get({required DocumentRef ref}) async { - final entity = await _database.documentsV2Dao.getDocument(ref); - if (entity == null) { - throw DocumentNotFoundException(ref: ref); - } - - return entity.toModel(); - } - - @override - Future> getAll({required DocumentRef ref}) { - return _database.documentsDao - .queryAll(ref: ref) + Future> findAll({ + DocumentType? type, + DocumentRef? ref, + DocumentRef? referencing, + bool latestOnly = false, + int limit = 200, + int offset = 0, + }) { + return _database.documentsV2Dao + .getDocuments( + type: type, + ref: ref, + referencing: referencing, + latestOnly: latestOnly, + limit: limit, + offset: offset, + ) .then((value) => value.map((e) => e.toModel()).toList()); } @override - Future getLatest({ + Future findFirst({ + DocumentType? type, + DocumentRef? ref, + DocumentRef? referencing, CatalystId? authorId, }) { - return _database.documentsDao - .queryLatestDocumentData(authorId: authorId) + return _database.documentsV2Dao + .getDocument(type: type, ref: ref, referencing: referencing, author: authorId) .then((value) => value?.toModel()); } @override - Future getLatestOf({required DocumentRef ref}) { - return _database.documentsV2Dao.getLatestOf(ref); - } + Future get(DocumentRef ref) => findFirst(ref: ref); @override - Future> getProposals({ - SignedDocumentRef? categoryRef, - required ProposalsFilterType type, - }) { - return _database.proposalsDao - .queryProposals( - categoryRef: categoryRef, - filters: ProposalsFilters.forActiveCampaign(type: type), - ) - .then((value) => value.map((e) => e.toModel()).toList()); - } - - @override - Future> getProposalsPage({ - required PageRequest request, - required ProposalsFilters filters, - required ProposalsOrder order, - }) { - return _database.proposalsDao - .queryProposalsPage(request: request, filters: filters, order: order) - .then((page) => page.map((e) => e.toModel())); + Future getLatestRefOf(DocumentRef ref) { + return _database.documentsV2Dao.getLatestOf(ref); } @override @@ -106,30 +98,6 @@ final class DatabaseDocumentsDataSource return _database.proposalsV2Dao.getProposalsTotalTask(filters: filters, nodeId: nodeId); } - @override - Future getRefCount({ - required DocumentRef ref, - required DocumentType type, - }) { - return _database.documentsDao.countRefDocumentByType(ref: ref, type: type); - } - - @override - Future getRefToDocumentData({ - required DocumentRef refTo, - DocumentType? type, - }) { - return _database.documentsDao - .queryRefToDocumentData(refTo: refTo, type: type) - .then((e) => e?.toModel()); - } - - @override - Future> queryVersionsOfId({required String id}) async { - final documentEntities = await _database.documentsDao.queryVersionsOfId(id: id); - return documentEntities.map((e) => e.toModel()).toList(); - } - @override Future save({required DocumentData data}) => saveAll([data]); @@ -151,40 +119,53 @@ final class DatabaseDocumentsDataSource } @override - Stream watch({required DocumentRef ref}) { - return _database.documentsDao.watch(ref: ref).map((entity) => entity?.toModel()); + Stream watch({ + DocumentType? type, + DocumentRef? ref, + DocumentRef? referencing, + }) { + return _database.documentsV2Dao + .watchDocument(type: type, ref: ref, referencing: referencing) + .distinct() + .map((value) => value?.toModel()); } @override Stream> watchAll({ - int? limit, - required bool unique, DocumentType? type, + DocumentRef? ref, + DocumentRef? referencing, CatalystId? authorId, - DocumentRef? refTo, + bool latestOnly = false, + int limit = 200, + int offset = 0, }) { - return _database.documentsDao - .watchAll( - limit: limit, - unique: unique, + return _database.documentsV2Dao + .watchDocuments( type: type, - authorId: authorId, - refTo: refTo, + ref: ref, + referencing: referencing, + latestOnly: latestOnly, + limit: limit, + offset: offset, ) - .map((entities) { - return List.from(entities.map((e) => e.toModel())); - }); + .distinct(listEquals) + .map((value) => value.map((e) => e.toModel()).toList()); } @override Stream watchCount({ - DocumentRef? refTo, DocumentType? type, + DocumentRef? ref, + DocumentRef? referencing, }) { - return _database.documentsDao.watchCount( - refTo: refTo, - type: type, - ); + return _database.documentsV2Dao + .watchCount( + type: type, + ref: ref, + referencing: referencing, + ) + .distinct(); } @override @@ -202,16 +183,10 @@ final class DatabaseDocumentsDataSource if (!tr.finished) unawaited(tr.finish()); }, ) + .distinct() .map((page) => page.map((data) => data.toModel())); } - @override - Stream watchProposalsCount({ - required ProposalsCountFilters filters, - }) { - return _database.proposalsDao.watchCount(filters: filters); - } - @override Stream watchProposalsCountV2({ ProposalsFiltersV2 filters = const ProposalsFiltersV2(), @@ -222,18 +197,7 @@ final class DatabaseDocumentsDataSource (_) { if (!tr.finished) unawaited(tr.finish()); }, - ); - } - - @override - Stream> watchProposalsPage({ - required PageRequest request, - required ProposalsFilters filters, - required ProposalsOrder order, - }) { - return _database.proposalsDao - .watchProposalsPage(request: request, filters: filters, order: order) - .map((page) => page.map((e) => e.toModel())); + ).distinct(); } @override @@ -255,25 +219,6 @@ final class DatabaseDocumentsDataSource .distinct(listEquals) .map((event) => event.map((e) => e.toModel()).toList()); } - - @override - Stream watchRefToDocumentData({ - required DocumentRef refTo, - required DocumentType type, - }) { - return _database.documentsDao - .watchRefToDocumentData(refTo: refTo, type: type) - .map((e) => e?.toModel()); - } -} - -extension on DocumentEntity { - DocumentData toModel() { - return DocumentData( - metadata: metadata, - content: content, - ); - } } extension on DocumentEntityV2 { @@ -287,7 +232,6 @@ extension on DocumentEntityV2 { reply: replyId.toRef(replyVer), section: section, categoryId: categoryId.toRef(categoryVer), - // TODO(damian-molinski): Make sure to add unit tests authors: authors.isEmpty ? null : authors.split(',').map(CatalystId.parse).toList(), ), content: content, @@ -340,18 +284,6 @@ extension on DocumentData { } } -extension on JoinedProposalEntity { - ProposalDocumentData toModel() { - return ProposalDocumentData( - proposal: proposal.toModel(), - template: template.toModel(), - action: action?.toModel(), - commentsCount: commentsCount, - versions: versions, - ); - } -} - extension on JoinedProposalBriefEntity { JoinedProposalBriefData toModel() { final proposalDocumentData = proposal.toModel(); diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/document/source/database_drafts_data_source.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/document/source/database_drafts_data_source.dart index a18412ea1547..553e62e537e7 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/document/source/database_drafts_data_source.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/document/source/database_drafts_data_source.dart @@ -1,5 +1,8 @@ import 'package:catalyst_voices_models/catalyst_voices_models.dart'; import 'package:catalyst_voices_repositories/catalyst_voices_repositories.dart'; +import 'package:catalyst_voices_repositories/src/database/table/local_documents_drafts.drift.dart'; +import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; +import 'package:flutter/foundation.dart'; /// Encryption will be added later here as drafts are not public final class DatabaseDraftsDataSource implements DraftDataSource { @@ -10,56 +13,70 @@ final class DatabaseDraftsDataSource implements DraftDataSource { ); @override - Future delete({required DraftRef ref}) async { - await _database.draftsDao.deleteWhere(ref: ref); + Future count({ + DocumentType? type, + DocumentRef? ref, + DocumentRef? referencing, + }) { + return _database.localDocumentsV2Dao.count(type: type, ref: ref, referencing: referencing); } @override - Future deleteAll() => _database.draftsDao.deleteAll(); + Future delete({ + DocumentRef? ref, + List? excludeTypes, + }) { + return _database.localDocumentsV2Dao.deleteWhere(ref: ref, excludeTypes: excludeTypes); + } @override Future exists({required DocumentRef ref}) { - return _database.draftsDao.count(ref: ref).then((count) => count > 0); + return _database.localDocumentsV2Dao.exists(ref); } @override Future> filterExisting(List refs) { - // TODO(damian-molinski): not implemented - return Future(() => []); - } - - @override - Future get({required DocumentRef ref}) async { - final entity = await _database.draftsDao.query(ref: ref); - if (entity == null) { - throw DraftNotFoundException(ref: ref); - } - - return entity.toModel(); + return _database.localDocumentsV2Dao.filterExisting(refs); } @override - Future> getAll({required DocumentRef ref}) { - return _database.draftsDao - .queryAll(ref: ref) + Future> findAll({ + DocumentType? type, + DocumentRef? ref, + DocumentRef? referencing, + bool latestOnly = false, + int limit = 100, + int offset = 0, + }) { + return _database.localDocumentsV2Dao + .getDocuments( + type: type, + ref: ref, + referencing: referencing, + latestOnly: latestOnly, + limit: limit, + offset: offset, + ) .then((value) => value.map((e) => e.toModel()).toList()); } @override - Future getLatest({CatalystId? authorId}) { - return _database.draftsDao.queryLatest(authorId: authorId).then((value) => value?.toModel()); + Future findFirst({ + DocumentType? type, + DocumentRef? ref, + DocumentRef? referencing, + }) { + return _database.localDocumentsV2Dao + .getDocument(type: type, ref: ref, referencing: referencing) + .then((value) => value?.toModel()); } @override - Future getLatestOf({required DocumentRef ref}) async { - // TODO(damian-molinski): not implemented - return null; - } + Future get(DocumentRef ref) => findFirst(ref: ref); @override - Future> queryVersionsOfId({required String id}) async { - final documentEntities = await _database.draftsDao.queryVersionsOfId(id: id); - return documentEntities.map((e) => e.toModel()).toList(); + Future getLatestRefOf(DocumentRef ref) async { + return _database.localDocumentsV2Dao.getLatestOf(ref); } @override @@ -67,55 +84,100 @@ final class DatabaseDraftsDataSource implements DraftDataSource { @override Future saveAll(Iterable data) async { - // TODO(damian-molinski): migrate to V2 - /*final entries = data.map((e) => e.toEntity()).toList(); + final entries = data.map((e) => e.toEntity()).toList(); - await _database.localDraftsV2Dao.saveAll(entries);*/ + await _database.localDocumentsV2Dao.saveAll(entries); } @override - Future update({ + Future updateContent({ required DraftRef ref, required DocumentDataContent content, }) async { - await _database.draftsDao.updateContent(ref: ref, content: content); + await _database.localDocumentsV2Dao.updateContent(ref: ref, content: content); } @override - Stream watch({required DocumentRef ref}) { - return _database.draftsDao.watch(ref: ref).map((entity) => entity?.toModel()); + Stream watch({ + DocumentType? type, + DocumentRef? ref, + DocumentRef? referencing, + }) { + return _database.localDocumentsV2Dao + .watchDocument(type: type, ref: ref, referencing: referencing) + .distinct() + .map((value) => value?.toModel()); } @override Stream> watchAll({ - int? limit, DocumentType? type, - CatalystId? authorId, + DocumentRef? ref, + DocumentRef? referencing, + bool latestOnly = false, + int limit = 100, + int offset = 0, }) { - return _database.draftsDao - .watchAll( + return _database.localDocumentsV2Dao + .watchDocuments( + type: type, + ref: ref, + referencing: referencing, + latestOnly: latestOnly, limit: limit, + offset: offset, + ) + .distinct(listEquals) + .map((value) => value.map((e) => e.toModel()).toList()); + } + + @override + Stream watchCount({ + DocumentType? type, + DocumentRef? ref, + DocumentRef? referencing, + }) { + return _database.localDocumentsV2Dao + .watchCount( type: type, - authorId: authorId, + ref: ref, + referencing: referencing, ) - .map((event) { - final list = List.from(event.map((e) => e.toModel())); - return list; - }); + .distinct(); } } -extension on DocumentDraftEntity { +extension on LocalDocumentDraftEntity { DocumentData toModel() { return DocumentData( - metadata: metadata, + metadata: DocumentDataMetadata( + type: type, + selfRef: DraftRef(id: id, version: ver), + ref: refId.toRef(refVer), + template: templateId.toRef(templateVer), + reply: replyId.toRef(replyVer), + section: section, + categoryId: categoryId.toRef(categoryVer), + authors: authors.isEmpty ? null : authors.split(',').map(CatalystId.parse).toList(), + ), content: content, ); } } +extension on String? { + SignedDocumentRef? toRef([String? ver]) { + final id = this; + if (id == null) { + return null; + } + + return SignedDocumentRef(id: id, version: ver); + } +} + extension on DocumentData { - /*LocalDocumentDraftEntity toEntity() { + LocalDocumentDraftEntity toEntity() { return LocalDocumentDraftEntity( content: content, id: metadata.id, @@ -133,5 +195,5 @@ extension on DocumentData { authors: metadata.authors?.map((e) => e.toUri().toString()).join(',') ?? '', createdAt: metadata.version.dateTime, ); - }*/ + } } diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/document/source/document_data_local_source.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/document/source/document_data_local_source.dart index 9e45b5e0de0d..f7f333ec5a7b 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/document/source/document_data_local_source.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/document/source/document_data_local_source.dart @@ -1,76 +1,97 @@ import 'package:catalyst_voices_models/catalyst_voices_models.dart'; import 'package:catalyst_voices_repositories/catalyst_voices_repositories.dart'; -/// Base interface to interact with locally document data. +/// Base interface to interact with locally stored document data (Database/Storage). +/// +/// This interface handles common CRUD operations and reactive streams for +/// both [SignedDocumentDataSource] and [DraftDataSource]. abstract interface class DocumentDataLocalSource implements DocumentDataSource { - Future deleteAll(); + /// Counts the number of documents matching the provided filters. + /// + /// * [type]: Filter by the [DocumentType] (e.g., Proposal, Comment). + /// * [ref]: Filter by the specific identity of the document (ID/Version). + /// * [referencing]: Filter for documents that reference *this* target [referencing]. + /// (e.g., Find all comments pointing to Proposal X). + Future count({ + DocumentType? type, + DocumentRef? ref, + DocumentRef? referencing, + }); + + /// Deletes documents matching the provided filters. + /// + /// * [excludeTypes]: If provided, deletes all documents *except* those + /// matching the types in this list. + /// + /// Returns the number of records deleted. + Future delete({ + List? excludeTypes, + }); + /// Checks if a specific document exists in local storage. Future exists({required DocumentRef ref}); + /// Checks a list of [refs] and returns a subset list containing only + /// the references that actually exist in the local storage. Future> filterExisting(List refs); - Future> getAll({required DocumentRef ref}); - - Future getLatest({ - CatalystId? authorId, + /// Retrieves a list of documents matching the provided filters. + /// + /// * [latestOnly]: If `true`, only the most recent version of each + /// document ID is returned. + /// * [limit] and [offset]: Used for pagination. + Future> findAll({ + DocumentType? type, + DocumentRef? ref, + DocumentRef? referencing, + bool latestOnly, + int limit, + int offset, }); - Future> queryVersionsOfId({required String id}); + /// Retrieves a single document matching the provided filters. + /// + /// Returns `null` if no document matches or if multiple match (depending on impl). + /// Generally used when the filter is expected to yield a unique result. + Future findFirst({ + DocumentType? type, + DocumentRef? ref, + DocumentRef? referencing, + }); + /// Persists a single [DocumentData] object to local storage. + /// + /// If the document already exists, it should be updated (upsert). Future save({required DocumentData data}); + /// Persists multiple [DocumentData] objects to local storage in a batch. Future saveAll(Iterable data); - Stream watch({required DocumentRef ref}); -} - -/// See [DatabaseDraftsDataSource]. -abstract interface class DraftDataSource implements DocumentDataLocalSource { - Future delete({ - required DraftRef ref, - }); - - Future update({ - required DraftRef ref, - required DocumentDataContent content, - }); - - Stream> watchAll({ - int? limit, + /// Watches for changes to a single document matching the filters. + /// + /// Emits a new value whenever the matching document is updated or inserted. + Stream watch({ DocumentType? type, - CatalystId? authorId, - }); -} - -/// See [DatabaseDocumentsDataSource]. -abstract interface class SignedDocumentDataSource implements DocumentDataLocalSource { - Future deleteAllRespectingLocalDrafts(); - - Future getRefCount({ - required DocumentRef ref, - required DocumentType type, - }); - - Future getRefToDocumentData({ - required DocumentRef refTo, - required DocumentType type, + DocumentRef? ref, + DocumentRef? referencing, }); + /// Watches for changes to a list of documents matching the filters. + /// + /// Emits a new list whenever any document matching the criteria changes. Stream> watchAll({ - int? limit, - required bool unique, DocumentType? type, - CatalystId? authorId, - DocumentRef? refTo, + DocumentRef? ref, + DocumentRef? referencing, + bool latestOnly, + int limit, + int offset, }); + /// Watches the count of documents matching the filters. Stream watchCount({ - DocumentRef? refTo, DocumentType? type, - }); - - Stream watchRefToDocumentData({ - required DocumentRef refTo, - required DocumentType type, + DocumentRef? ref, + DocumentRef? referencing, }); } diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/document/source/document_data_remote_source.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/document/source/document_data_remote_source.dart index cd8db83d71c0..fd43680f61e0 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/document/source/document_data_remote_source.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/document/source/document_data_remote_source.dart @@ -18,7 +18,7 @@ final class CatGatewayDocumentDataSource implements DocumentDataRemoteSource { ); @override - Future get({required DocumentRef ref}) async { + Future get(DocumentRef ref) async { final bytes = await _api.gateway .apiV1DocumentDocumentIdGet( documentId: ref.id, @@ -31,7 +31,7 @@ final class CatGatewayDocumentDataSource implements DocumentDataRemoteSource { } @override - Future getLatestOf({required DocumentRef ref}) async { + Future getLatestRefOf(DocumentRef ref) async { final ver = await getLatestVersion(ref.id); if (ver == null) { return null; diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/document/source/document_data_source.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/document/source/document_data_source.dart index 2135bb86bdf8..60849a3514b9 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/document/source/document_data_source.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/document/source/document_data_source.dart @@ -1,8 +1,17 @@ import 'package:catalyst_voices_models/catalyst_voices_models.dart'; -//ignore: one_member_abstracts +/// A base interface for retrieving document data from any source (Local, Remote, Memory). abstract interface class DocumentDataSource { - Future get({required DocumentRef ref}); + /// Retrieves a specific document by its unique reference. + /// + /// Returns `null` if the document with the specific [DocumentRef.id] and + /// [DocumentRef.version] is not found. + Future get(DocumentRef ref); - Future getLatestOf({required DocumentRef ref}); + /// Resolves the reference to the latest available version of a document chain. + /// + /// If [ref] points to an older version, this returns the [DocumentRef] + /// for the most recent version of that document ID. + /// Returns `null` if the document ID does not exist in the source. + Future getLatestRefOf(DocumentRef ref); } diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/document/source/document_favorites_source.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/document/source/document_favorites_source.dart deleted file mode 100644 index bb8c8b3a8a64..000000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/document/source/document_favorites_source.dart +++ /dev/null @@ -1,60 +0,0 @@ -import 'package:catalyst_voices_models/catalyst_voices_models.dart'; -import 'package:catalyst_voices_repositories/catalyst_voices_repositories.dart'; - -final class DatabaseDocumentFavoriteSource implements DocumentFavoriteSource { - final CatalystDatabase _database; - - DatabaseDocumentFavoriteSource( - this._database, - ); - - @override - Future deleteAll() => _database.favoritesDao.deleteAll(); - - @override - Future updateDocumentFavorite( - String id, { - required DocumentType type, - required bool isFavorite, - }) { - if (!isFavorite) { - return _database.favoritesDao.deleteWhere(id: id); - } - - final idHiLo = UuidHiLo.from(id); - final entity = DocumentFavoriteEntity( - idHi: idHiLo.high, - idLo: idHiLo.low, - isFavorite: isFavorite, - type: type, - ); - - return _database.favoritesDao.save(entity); - } - - @override - Stream> watchAllFavoriteIds({DocumentType? type}) { - return _database.favoritesDao.watchAll(type: type); - } - - @override - Stream watchIsDocumentFavorite(String id) { - return _database.favoritesDao.watch(id: id); - } -} - -abstract interface class DocumentFavoriteSource { - Future deleteAll(); - - Future updateDocumentFavorite( - String id, { - required DocumentType type, - required bool isFavorite, - }); - - Stream> watchAllFavoriteIds({ - DocumentType? type, - }); - - Stream watchIsDocumentFavorite(String id); -} diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/document/source/local_document_data_local_source.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/document/source/local_document_data_local_source.dart new file mode 100644 index 000000000000..8efc50347f48 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/document/source/local_document_data_local_source.dart @@ -0,0 +1,27 @@ +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_repositories/catalyst_voices_repositories.dart'; + +/// Interface for accessing mutable draft documents. +/// +/// Drafts are local-only, unsigned work-in-progress documents. +/// See [DatabaseDraftsDataSource]. +abstract interface class DraftDataSource implements DocumentDataLocalSource { + /// Deletes drafts matching the criteria. + /// + /// * [ref]: If provided, deletes the specific draft. + /// * [excludeTypes]: Deletes all drafts NOT matching these types (often used for cleanup). + @override + Future delete({ + DocumentRef? ref, + List? excludeTypes, + }); + + /// Updates the content of an existing draft identified by [ref]. + /// + /// This is distinct from [save] as it implies modifying the payload + /// of an existing entity without necessarily creating a new version/ID. + Future updateContent({ + required DraftRef ref, + required DocumentDataContent content, + }); +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/document/source/proposal_document_data_local_source.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/document/source/proposal_document_data_local_source.dart index 2ef41dfff69c..10742cd962ab 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/document/source/proposal_document_data_local_source.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/document/source/proposal_document_data_local_source.dart @@ -5,21 +5,6 @@ import 'package:catalyst_voices_models/catalyst_voices_models.dart'; /// implement those queries in abstract way without know all logic related /// just to proposals. abstract interface class ProposalDocumentDataLocalSource { - /// Used to retrieve all proposals. Offers way to filter proposals by passing - /// category ref and proposal filter type. - /// - /// If [categoryRef] is null then all proposals are returned. - Future> getProposals({ - SignedDocumentRef? categoryRef, - required ProposalsFilterType type, - }); - - Future> getProposalsPage({ - required PageRequest request, - required ProposalsFilters filters, - required ProposalsOrder order, - }); - Future getProposalsTotalTask({ required NodeId nodeId, required ProposalsTotalAskFilters filters, @@ -36,20 +21,10 @@ abstract interface class ProposalDocumentDataLocalSource { ProposalsFiltersV2 filters, }); - Stream watchProposalsCount({ - required ProposalsCountFilters filters, - }); - Stream watchProposalsCountV2({ ProposalsFiltersV2 filters, }); - Stream> watchProposalsPage({ - required PageRequest request, - required ProposalsFilters filters, - required ProposalsOrder order, - }); - Stream watchProposalsTotalTask({ required NodeId nodeId, required ProposalsTotalAskFilters filters, diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/document/source/signed_document_data_local_source.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/document/source/signed_document_data_local_source.dart new file mode 100644 index 000000000000..8633bd971a3d --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/document/source/signed_document_data_local_source.dart @@ -0,0 +1,34 @@ +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_repositories/catalyst_voices_repositories.dart'; + +/// Interface for accessing immutable, cryptographically signed documents. +/// +/// Signed documents are final and cannot be modified, only superseded by +/// newer versions. +/// See [DatabaseDocumentsDataSource]. +abstract interface class SignedDocumentDataSource implements DocumentDataLocalSource { + /// Retrieves a single signed document matching the filters. + /// + /// * [authorId]: Filters documents authored by a specific [CatalystId]. + @override + Future findFirst({ + DocumentType? type, + DocumentRef? ref, + DocumentRef? referencing, + CatalystId? authorId, + }); + + /// Watches for changes to a list of signed documents. + /// + /// * [authorId]: Filters documents authored by a specific [CatalystId]. + @override + Stream> watchAll({ + DocumentType? type, + DocumentRef? ref, + DocumentRef? referencing, + CatalystId? authorId, + bool latestOnly, + int limit, + int offset, + }); +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/proposal/proposal_repository.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/proposal/proposal_repository.dart index bfc50c44b47b..c07db6d69596 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/proposal/proposal_repository.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/proposal/proposal_repository.dart @@ -31,19 +31,6 @@ abstract interface class ProposalRepository { required DocumentRef ref, }); - Future> getProposals({ - SignedDocumentRef? categoryRef, - required ProposalsFilterType type, - }); - - /// Fetches all proposals for page matching [request] as well as - /// [filters]. - Future> getProposalsPage({ - required PageRequest request, - required ProposalsFilters filters, - required ProposalsOrder order, - }); - /// Returns [ProposalTemplate] for matching [ref]. /// /// Source of data depends whether [ref] is [SignedDocumentRef] or [DraftRef]. @@ -79,18 +66,18 @@ abstract interface class ProposalRepository { Future upsertDraftProposal({required DocumentData document}); Stream watchCommentsCount({ - DocumentRef? refTo, + DocumentRef? referencing, }); Stream> watchLatestProposals({int? limit}); - /// Watches for [ProposalSubmissionAction] that were made on [refTo] document. + /// Watches for [ProposalSubmissionAction] that were made on [referencing] document. /// /// As making action on document not always creates a new document ref /// we need to watch for actions on a document that has a reference to - /// [refTo] document. + /// [referencing] document. Stream watchProposalPublish({ - required DocumentRef refTo, + required DocumentRef referencing, }); Stream> watchProposalsBriefPage({ @@ -99,20 +86,10 @@ abstract interface class ProposalRepository { ProposalsFiltersV2 filters, }); - Stream watchProposalsCount({ - required ProposalsCountFilters filters, - }); - Stream watchProposalsCountV2({ ProposalsFiltersV2 filters, }); - Stream> watchProposalsPage({ - required PageRequest request, - required ProposalsFilters filters, - required ProposalsOrder order, - }); - Stream> watchProposalTemplates({ required CampaignFilters filters, }); @@ -153,7 +130,7 @@ final class ProposalRepositoryImpl implements ProposalRepository { }) async { final documentData = await _documentRepository.getDocumentData(ref: ref); final commentsCount = await _documentRepository.getRefCount( - ref: ref, + referencing: ref, type: DocumentType.commentDocument, ); final proposalPublish = await getProposalPublishForRef(ref: ref); @@ -179,7 +156,7 @@ final class ProposalRepositoryImpl implements ProposalRepository { required DocumentRef ref, }) async { final data = await _documentRepository.getRefToDocumentData( - refTo: ref, + referencing: ref, type: DocumentType.proposalActionDocument, ); @@ -187,27 +164,6 @@ final class ProposalRepositoryImpl implements ProposalRepository { return _getProposalPublish(ref: ref, action: action); } - @override - Future> getProposals({ - SignedDocumentRef? categoryRef, - required ProposalsFilterType type, - }) async { - return _proposalsLocalSource - .getProposals(type: type, categoryRef: categoryRef) - .then((value) => value.map(_buildProposalData).toList()); - } - - @override - Future> getProposalsPage({ - required PageRequest request, - required ProposalsFilters filters, - required ProposalsOrder order, - }) { - return _proposalsLocalSource - .getProposalsPage(request: request, filters: filters, order: order) - .then((value) => value.map(_buildProposalData)); - } - @override Future getProposalTemplate({ required DocumentRef ref, @@ -297,10 +253,10 @@ final class ProposalRepositoryImpl implements ProposalRepository { @override Stream watchCommentsCount({ - DocumentRef? refTo, + DocumentRef? referencing, }) { return _documentRepository.watchCount( - refTo: refTo, + referencing: referencing, type: DocumentType.commentDocument, ); } @@ -330,17 +286,17 @@ final class ProposalRepositoryImpl implements ProposalRepository { @override Stream watchProposalPublish({ - required DocumentRef refTo, + required DocumentRef referencing, }) { return _documentRepository .watchRefToDocumentData( - refTo: refTo, + referencing: referencing, type: DocumentType.proposalActionDocument, ) .map((data) { final action = _buildProposalActionData(data); - return _getProposalPublish(ref: refTo, action: action); + return _getProposalPublish(ref: referencing, action: action); }); } @@ -357,13 +313,6 @@ final class ProposalRepositoryImpl implements ProposalRepository { ); } - @override - Stream watchProposalsCount({ - required ProposalsCountFilters filters, - }) { - return _proposalsLocalSource.watchProposalsCount(filters: filters); - } - @override Stream watchProposalsCountV2({ ProposalsFiltersV2 filters = const ProposalsFiltersV2(), @@ -371,17 +320,6 @@ final class ProposalRepositoryImpl implements ProposalRepository { return _proposalsLocalSource.watchProposalsCountV2(filters: filters); } - @override - Stream> watchProposalsPage({ - required PageRequest request, - required ProposalsFilters filters, - required ProposalsOrder order, - }) { - return _proposalsLocalSource - .watchProposalsPage(request: request, filters: filters, order: order) - .map((value) => value.map(_buildProposalData)); - } - @override Stream> watchProposalTemplates({ required CampaignFilters filters, @@ -428,31 +366,6 @@ final class ProposalRepositoryImpl implements ProposalRepository { return dto.action.toModel(); } - ProposalData _buildProposalData(ProposalDocumentData data) { - final action = _buildProposalActionData(data.action); - - final publish = switch (action) { - ProposalSubmissionAction.aFinal => ProposalPublish.submittedProposal, - ProposalSubmissionAction.draft || null => ProposalPublish.publishedDraft, - ProposalSubmissionAction.hide => throw ArgumentError( - 'Proposal(${data.proposal.metadata.selfRef}) is ' - 'unsupported ${ProposalSubmissionAction.hide}. Make sure to filter ' - 'out hidden proposals before this code is reached.', - ), - }; - - final document = _buildProposalDocument( - documentData: data.proposal, - templateData: data.template, - ); - - return ProposalData( - document: document, - publish: publish, - commentsCount: data.commentsCount, - ); - } - ProposalDocument _buildProposalDocument({ required DocumentData documentData, required DocumentData templateData, diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/database/catalyst_database_test.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/database/catalyst_database_test.dart index a825a76f3365..b1d9f4670a16 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/database/catalyst_database_test.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/database/catalyst_database_test.dart @@ -1,7 +1,7 @@ -import 'package:catalyst_voices_dev/catalyst_voices_dev.dart'; import 'package:catalyst_voices_repositories/src/database/catalyst_database.dart'; import 'package:flutter_test/flutter_test.dart'; +import '../utils/document_with_authors_factory.dart'; import 'connection/test_connection.dart'; import 'drift_test_platforms.dart'; @@ -22,29 +22,20 @@ void main() { 'clear removes all documents and drafts', () async { // Given - final drafts = List.generate(5, (index) => DraftFactory.build()); - final documents = List.generate( - 5, - (index) => DocumentWithMetadataFactory.build(), - ); + final documents = List.generate(5, (index) => DocumentWithAuthorsFactory.create()); // When - await database.documentsDao.saveAll(documents); - await database.draftsDao.saveAll(drafts); + await database.documentsV2Dao.saveAll(documents); // Then - final draftsCountBefore = await database.draftsDao.count(); - final documentsCountBefore = await database.documentsDao.count(); + final documentsCountBefore = await database.documentsV2Dao.count(); - expect(draftsCountBefore, drafts.length); expect(documentsCountBefore, documents.length); await database.clear(); - final draftsCountAfter = await database.draftsDao.count(); - final documentsCountAfter = await database.documentsDao.count(); + final documentsCountAfter = await database.documentsV2Dao.count(); - expect(draftsCountAfter, isZero); expect(documentsCountAfter, isZero); }, onPlatform: driftOnPlatforms, diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/database/dao/documents_dao_test.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/database/dao/documents_dao_test.dart deleted file mode 100644 index 772785ecae2c..000000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/database/dao/documents_dao_test.dart +++ /dev/null @@ -1,1249 +0,0 @@ -import 'package:catalyst_voices_dev/catalyst_voices_dev.dart'; -import 'package:catalyst_voices_models/catalyst_voices_models.dart'; -import 'package:catalyst_voices_repositories/src/database/catalyst_database.dart'; -import 'package:catalyst_voices_repositories/src/database/dao/documents_dao.dart'; -import 'package:catalyst_voices_repositories/src/database/database.dart'; -import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; -import 'package:collection/collection.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:uuid_plus/uuid_plus.dart'; - -import '../connection/test_connection.dart'; -import '../drift_test_platforms.dart'; - -void main() { - late DriftCatalystDatabase database; - - // ignore: unnecessary_lambdas - setUpAll(() { - DummyCatalystIdFactory.registerDummyKeyFactory(); - }); - - setUp(() async { - final connection = await buildTestConnection(); - database = DriftCatalystDatabase(connection); - }); - - tearDown(() async { - await database.close(); - }); - - group(DocumentsDao, () { - group('save all', () { - test( - 'documents can be queried back correctly', - () async { - // Given - final documentsWithMetadata = _generateDocumentEntitiesWithMetadata(10); - final expectedDocuments = documentsWithMetadata.map((e) => e.document); - - // When - await database.documentsDao.saveAll(documentsWithMetadata); - - // Then - final documents = await database.documentsDao.queryAll(); - - expect(documents, expectedDocuments); - }, - onPlatform: driftOnPlatforms, - ); - - test( - 'conflicting documents are ignored', - () async { - // Given - final documentsWithMetadata = _generateDocumentEntitiesWithMetadata(20); - final expectedDocuments = documentsWithMetadata.map((e) => e.document); - - // When - final firstBatch = documentsWithMetadata.sublist(0, 10); - final secondBatch = documentsWithMetadata.sublist(5); - - await database.documentsDao.saveAll(firstBatch); - await database.documentsDao.saveAll(secondBatch); - - // Then - final documents = await database.documentsDao.queryAll(); - - expect(documents, expectedDocuments); - }, - onPlatform: driftOnPlatforms, - ); - }); - - group('query', () { - test( - 'stream emits data when new entities are saved', - () async { - // Given - final documentsWithMetadata = _generateDocumentEntitiesWithMetadata(1); - final expectedDocuments = documentsWithMetadata.map((e) => e.document).toList(); - - // When - final documentsStream = database.documentsDao.watchAll(); - - await database.documentsDao.saveAll(documentsWithMetadata); - - // Then - expect( - documentsStream, - emitsInOrder([ - // later we inserting documents - equals(expectedDocuments), - ]), - ); - }, - onPlatform: driftOnPlatforms, - ); - - test( - 'returns specific version matching exact ref', - () async { - // Given - final documentsWithMetadata = _generateDocumentEntitiesWithMetadata(2); - final document = documentsWithMetadata.first.document; - final ref = SignedDocumentRef( - id: document.metadata.id, - version: document.metadata.version, - ); - - // When - await database.documentsDao.saveAll(documentsWithMetadata); - - // Then - final entity = await database.documentsDao.query(ref: ref); - - expect(entity, isNotNull); - - final id = UuidHiLo(high: entity!.idHi, low: entity.idLo); - final ver = UuidHiLo(high: entity.verHi, low: entity.verLo); - - expect(id.uuid, ref.id); - expect(ver.uuid, ref.version); - }, - onPlatform: driftOnPlatforms, - ); - - test( - 'returns newest version when ver is not specified', - () async { - // Given - final id = DocumentRefFactory.randomUuidV7(); - final firstVersionId = const Uuid().v7( - config: V7Options( - DateTime(2025, 2, 10).millisecondsSinceEpoch, - null, - ), - ); - final secondVersionId = const Uuid().v7( - config: V7Options( - DateTime(2025, 2, 11).millisecondsSinceEpoch, - null, - ), - ); - - const secondContent = DocumentDataContent({'title': 'Dev'}); - final documentsWithMetadata = [ - DocumentWithMetadataFactory.build( - content: const DocumentDataContent({'title': 'D'}), - metadata: DocumentDataMetadata( - type: DocumentType.proposalDocument, - selfRef: SignedDocumentRef( - id: id, - version: firstVersionId, - ), - ), - ), - DocumentWithMetadataFactory.build( - content: secondContent, - metadata: DocumentDataMetadata( - type: DocumentType.proposalDocument, - selfRef: SignedDocumentRef( - id: id, - version: secondVersionId, - ), - ), - ), - ]; - final document = documentsWithMetadata.first.document; - final ref = SignedDocumentRef(id: document.metadata.id); - - // When - await database.documentsDao.saveAll(documentsWithMetadata); - - // Then - final entity = await database.documentsDao.query(ref: ref); - - expect(entity, isNotNull); - - expect(entity!.metadata.id, id); - expect(entity.metadata.version, secondVersionId); - expect(entity.content, secondContent); - }, - onPlatform: driftOnPlatforms, - ); - - test( - 'returns null when id does not match any id', - () async { - // Given - final documentsWithMetadata = _generateDocumentEntitiesWithMetadata(2); - final ref = SignedDocumentRef(id: DocumentRefFactory.randomUuidV7()); - - // When - await database.documentsDao.saveAll(documentsWithMetadata); - - // Then - final entity = await database.documentsDao.query(ref: ref); - - expect(entity, isNull); - }, - onPlatform: driftOnPlatforms, - ); - - test( - 'Return latest unique documents', - () async { - final id = DocumentRefFactory.randomUuidV7(); - final version = DocumentRefFactory.randomUuidV7(); - final version2 = DocumentRefFactory.randomUuidV7(); - - final document = DocumentWithMetadataFactory.build( - metadata: DocumentDataMetadata( - type: DocumentType.proposalDocument, - selfRef: SignedDocumentRef(id: id, version: version), - ), - ); - final document2 = DocumentWithMetadataFactory.build( - metadata: DocumentDataMetadata( - type: DocumentType.proposalDocument, - selfRef: SignedDocumentRef(id: id, version: version2), - ), - ); - final documentsStream = database.documentsDao.watchAll(unique: true).asBroadcastStream(); - - await database.documentsDao.saveAll([document]); - final firstEmission = await documentsStream.first; - await database.documentsDao.saveAll([document2]); - final secondEmission = await documentsStream.first; - - expect(firstEmission, equals([document.document])); - expect(secondEmission, equals([document2.document])); - expect(secondEmission.length, equals(1)); - expect( - secondEmission.first.metadata.selfRef.version, - equals(version2), - ); - }, - onPlatform: driftOnPlatforms, - ); - - test( - 'Returns latest document limited by quantity if provided', - () async { - // Given - final documentsWithMetadata = _generateDocumentEntitiesWithMetadata(20); - - final expectedDocuments = - documentsWithMetadata - .map((e) => e.document) - .groupListsBy((doc) => '${doc.idHi}-${doc.idLo}') - .values - .map( - (versions) => versions.reduce((a, b) { - // Compare versions (higher version wins) - final compareHi = b.verHi.compareTo(a.verHi); - if (compareHi != 0) return compareHi > 0 ? b : a; - return b.verLo.compareTo(a.verLo) > 0 ? b : a; - }), - ) - .toList() - ..sort((a, b) { - // Sort by version descending - final compareHi = b.verHi.compareTo(a.verHi); - if (compareHi != 0) return compareHi; - return b.verLo.compareTo(a.verLo); - }); - - final limitedExpectedDocuments = expectedDocuments.take(7).toList(); - - // When - final documentsStream = database.documentsDao.watchAll(limit: 7, unique: true); - - await database.documentsDao.saveAll(documentsWithMetadata.reversed); - - // Then - expect( - documentsStream, - emitsInOrder([ - equals(limitedExpectedDocuments), - ]), - ); - - expect( - limitedExpectedDocuments.length, - equals(7), - reason: 'should have 7 documents', - ); - - final uniqueIds = limitedExpectedDocuments.map((d) => '${d.idHi}-${d.idLo}').toSet(); - expect( - uniqueIds.length, - equals(limitedExpectedDocuments.length), - reason: 'should have unique document IDs', - ); - }, - onPlatform: driftOnPlatforms, - ); - - test( - 'returns latest version when document has more than 1 version', - () async { - final id = DocumentRefFactory.randomUuidV7(); - final v1 = DocumentRefFactory.randomUuidV7(); - final v2 = DocumentRefFactory.randomUuidV7(); - - final documentsWithMetadata = [v1, v2].map((version) { - final metadata = DocumentDataMetadata( - type: DocumentType.proposalDocument, - selfRef: SignedDocumentRef( - id: id, - version: version, - ), - ); - return DocumentWithMetadataFactory.build(metadata: metadata); - }).toList(); - - // When - final documentsStream = database.documentsDao.watchAll(limit: 7, unique: true); - - await database.documentsDao.saveAll(documentsWithMetadata); - // Then - expect( - documentsStream, - emitsInOrder([ - predicate>( - (documents) { - if (documents.length != 1) return false; - final doc = documents.first; - return doc.metadata.version == v2; - }, - 'should return document with version $v2', - ), - ]), - ); - }, - onPlatform: driftOnPlatforms, - ); - - test( - 'emits new version of recent document', - () async { - // Generate base ID - final id = DocumentRefFactory.randomUuidV7(); - - // Create versions with enforced order (v2 is newer than v1) - final v1 = DocumentRefFactory.randomUuidV7(); - final v2 = DocumentRefFactory.randomUuidV7(); - - final documentsWithMetadata = DocumentWithMetadataFactory.build( - metadata: DocumentDataMetadata( - type: DocumentType.proposalDocument, - selfRef: SignedDocumentRef( - id: id, - version: v1, - ), - ), - ); - - final newVersion = DocumentWithMetadataFactory.build( - metadata: DocumentDataMetadata( - type: DocumentType.proposalDocument, - selfRef: SignedDocumentRef( - id: id, - version: v2, - ), - ), - ); - - // When - final documentsStream = database.documentsDao - .watchAll(limit: 7, unique: true) - .asBroadcastStream(); - - // Save first version and wait for emission - await database.documentsDao.saveAll([documentsWithMetadata]); - final firstEmission = await documentsStream.first; - - // Save second version and wait for emission - await database.documentsDao.saveAll([newVersion]); - final secondEmission = await documentsStream.first; - - // Then verify both emissions - expect(firstEmission, equals([documentsWithMetadata.document])); - expect(secondEmission, equals([newVersion.document])); - }, - onPlatform: driftOnPlatforms, - ); - - test( - 'emits new document when is inserted', - () async { - // Generate base ID - final id1 = DocumentRefFactory.randomUuidV7(); - - // Create versions with enforced order (v2 is newer than v1) - final v1 = DocumentRefFactory.randomUuidV7(); - - final id2 = DocumentRefFactory.randomUuidV7(); - final v2 = DocumentRefFactory.randomUuidV7(); - - final document1 = DocumentWithMetadataFactory.build( - metadata: DocumentDataMetadata( - type: DocumentType.proposalDocument, - selfRef: SignedDocumentRef( - id: id1, - version: v1, - ), - ), - ); - - final document2 = DocumentWithMetadataFactory.build( - metadata: DocumentDataMetadata( - type: DocumentType.proposalDocument, - selfRef: SignedDocumentRef( - id: id2, - version: v2, - ), - ), - ); - - // When - final documentsStream = database.documentsDao - .watchAll(limit: 1, unique: true) - .asBroadcastStream(); - - await database.documentsDao.saveAll([document1]); - final firstEmission = await documentsStream.first; - - await database.documentsDao.saveAll([document2]); - final secondEmission = await documentsStream.first; - - // Then verify both emissions - expect(firstEmission, equals([document1.document])); - expect( - secondEmission, - equals([document2.document]), - ); - }, - onPlatform: driftOnPlatforms, - ); - - test( - 'all documents with from same account are returned ' - 'even when username changes', - () async { - // Given - final originalId = DummyCatalystIdFactory.create(username: 'damian'); - final updatedId = originalId.copyWith(username: const Optional('dev')); - - final document1 = DocumentWithMetadataFactory.build( - metadata: DocumentDataMetadata( - type: DocumentType.proposalDocument, - selfRef: DocumentRefFactory.signedDocumentRef(), - authors: [originalId], - ), - ); - - final document2 = DocumentWithMetadataFactory.build( - metadata: DocumentDataMetadata( - type: DocumentType.proposalDocument, - selfRef: DocumentRefFactory.signedDocumentRef(), - authors: [updatedId], - ), - ); - - final docs = [document1, document2]; - final refs = docs.map((e) => e.document.metadata.selfRef).toList(); - - // When - await database.documentsDao.saveAll(docs); - - // Then - final stream = database.documentsDao.watchAll(authorId: updatedId); - - expect( - stream, - emitsInOrder([ - allOf( - hasLength(docs.length), - everyElement( - predicate((document) { - return refs.contains(document.metadata.selfRef); - }), - ), - ), - ]), - ); - }, - onPlatform: driftOnPlatforms, - ); - - test( - 'queryRefToDocumentData returns correct document', - () async { - final document1 = DocumentWithMetadataFactory.build( - metadata: DocumentDataMetadata( - type: DocumentType.proposalDocument, - selfRef: DocumentRefFactory.signedDocumentRef(), - ), - ); - final document2 = DocumentWithMetadataFactory.build( - metadata: DocumentDataMetadata( - type: DocumentType.proposalDocument, - selfRef: DocumentRefFactory.signedDocumentRef(), - ref: document1.document.metadata.selfRef, - ), - ); - - await database.documentsDao.saveAll([document1, document2]); - - final document = await database.documentsDao.queryRefToDocumentData( - refTo: document1.document.metadata.selfRef, - type: DocumentType.proposalDocument, - ); - - expect( - document?.metadata.selfRef, - document2.document.metadata.selfRef, - ); - }, - onPlatform: driftOnPlatforms, - ); - - test( - 'watchRefToDocumentData emits correct document and updates', - () async { - // Given - final baseDocument = DocumentWithMetadataFactory.build( - metadata: DocumentDataMetadata( - type: DocumentType.proposalDocument, - selfRef: DocumentRefFactory.signedDocumentRef(), - ), - ); - - final referencingDocument = DocumentWithMetadataFactory.build( - metadata: DocumentDataMetadata( - type: DocumentType.commentTemplate, - selfRef: DocumentRefFactory.signedDocumentRef(), - ref: baseDocument.document.metadata.selfRef, - ), - ); - - final newerVersion = DocumentWithMetadataFactory.build( - metadata: DocumentDataMetadata( - type: DocumentType.commentTemplate, - selfRef: SignedDocumentRef( - id: referencingDocument.document.metadata.id, - version: DocumentRefFactory.randomUuidV7(), - ), - ref: baseDocument.document.metadata.selfRef, - ), - ); - - // When - final documentsStream = database.documentsDao - .watchRefToDocumentData( - refTo: baseDocument.document.metadata.selfRef, - type: DocumentType.commentTemplate, - ) - .asBroadcastStream(); - - await database.documentsDao.saveAll([baseDocument, referencingDocument]); - final firstEmission = await documentsStream.first; - - await database.documentsDao.saveAll([newerVersion]); - final secondEmission = await documentsStream.first; - - // Then - expect( - firstEmission?.metadata.selfRef, - referencingDocument.document.metadata.selfRef, - ); - expect( - secondEmission?.metadata.selfRef, - newerVersion.document.metadata.selfRef, - ); - expect( - secondEmission?.metadata.id, - referencingDocument.document.metadata.id, - ); - }, - onPlatform: driftOnPlatforms, - ); - group('wildcard support', () { - test( - 'can query documents by matched DocumentNodeId value with wildcard', - () async { - final templateRef = DocumentRefFactory.randomUuidV7(); - - final proposalRef1 = DocumentRefFactory.randomUuidV7(); - final proposalRef2 = DocumentRefFactory.randomUuidV7(); - - const content1 = DocumentDataContent({ - 'setup': { - 'title': { - 'title': 'Milestone 2', - }, - }, - 'milestones': { - 'milestones': { - 'milestone_list': [ - { - 'title': 'Milestone 1', - 'cost': 100, - }, - { - 'title': 'Milestone 2', - 'cost': 200, - }, - ], - }, - }, - }); - - const content2 = DocumentDataContent({ - 'setup': { - 'title': { - 'title': 'Milestone 2', - }, - }, - 'milestones': { - 'milestones': { - 'milestone_list': [ - { - 'title': 'Milestone 1', - 'cost': 100, - }, - { - 'title': 'Milestone 1', - 'outputs': 'Milestone 2', - 'cost': 200, - }, - ], - }, - }, - }); - - final ref1 = SignedDocumentRef(id: proposalRef1, version: proposalRef1); - final ref2 = SignedDocumentRef(id: proposalRef2, version: proposalRef2); - - final doc1 = DocumentWithMetadataFactory.build( - content: content1, - metadata: DocumentDataMetadata( - type: DocumentType.proposalDocument, - selfRef: ref1, - template: SignedDocumentRef(id: templateRef, version: templateRef), - ), - ); - - final doc2 = DocumentWithMetadataFactory.build( - content: content2, - metadata: DocumentDataMetadata( - type: DocumentType.proposalDocument, - selfRef: ref2, - template: SignedDocumentRef(id: templateRef, version: templateRef), - ), - ); - - await database.documentsDao.saveAll([doc1, doc2]); - - // When: query for documents with milestone_list.*.title == 'Milestone 2' - final results = await (database.documentsDao as DriftDocumentsDao) - .queryDocumentsByMatchedDocumentNodeIdValue( - nodeId: DocumentNodeId.fromString('milestones.milestones.milestone_list.*.title'), - value: 'Milestone 2', - type: DocumentType.proposalDocument, - content: 'content', - ); - - // Then: only doc1 should be returned - final refs = results.map((e) => e.metadata.selfRef).toList(); - expect(refs, contains(ref1)); - expect(refs, isNot(contains(ref2))); - }, - onPlatform: driftOnPlatforms, - ); - - test( - 'can query documents by matched DocumentNodeId value without wildcard', - () async { - final templateRef = DocumentRefFactory.randomUuidV7(); - - final proposalRef1 = DocumentRefFactory.randomUuidV7(); - - const content1 = DocumentDataContent({ - 'setup': { - 'proposer': { - 'applicant': 'John Doe', - }, - 'title': { - 'title': 'Milestone 2', - }, - }, - 'milestones': { - 'milestones': { - 'milestone_list': [ - { - 'title': 'Milestone 1', - 'cost': 100, - }, - { - 'title': 'Milestone 2', - 'cost': 200, - }, - ], - }, - }, - }); - - final ref1 = SignedDocumentRef(id: proposalRef1, version: proposalRef1); - - final doc1 = DocumentWithMetadataFactory.build( - content: content1, - metadata: DocumentDataMetadata( - type: DocumentType.proposalDocument, - selfRef: ref1, - template: SignedDocumentRef(id: templateRef, version: templateRef), - ), - ); - - await database.documentsDao.saveAll([doc1]); - - final results = await (database.documentsDao as DriftDocumentsDao) - .queryDocumentsByMatchedDocumentNodeIdValue( - nodeId: ProposalDocument.authorNameNodeId, - value: 'John Doe', - type: DocumentType.proposalDocument, - content: 'content', - ); - - final refs = results.map((e) => e.metadata.selfRef).toList(); - expect(refs, contains(ref1)); - }, - onPlatform: driftOnPlatforms, - ); - - test( - 'can query documents by matched DocumentNodeId value with wildcard at the beginning', - () async { - final templateRef = DocumentRefFactory.randomUuidV7(); - - final proposalRef1 = DocumentRefFactory.randomUuidV7(); - final proposalRef2 = DocumentRefFactory.randomUuidV7(); - - const content1 = DocumentDataContent({ - 'setup': { - 'title': { - 'title': 'Milestone 2', - 'subtitle': 'Subtitle', - }, - }, - 'milestones': { - 'milestones': { - 'milestone_list': [ - { - 'title': 'Milestone 1', - 'cost': 100, - }, - { - 'title': 'Milestone 2', - 'cost': 200, - }, - ], - }, - }, - }); - - const content2 = DocumentDataContent({ - 'setup': { - 'title': { - 'title': 'Milestone 2', - }, - }, - 'milestones': { - 'milestones': { - 'milestone_list': [ - { - 'title': 'Milestone 1', - 'cost': 100, - }, - { - 'title': 'Milestone 1', - 'outputs': 'Milestone 2', - 'cost': 200, - }, - ], - }, - }, - }); - - final ref1 = SignedDocumentRef(id: proposalRef1, version: proposalRef1); - final ref2 = SignedDocumentRef(id: proposalRef2, version: proposalRef2); - - final doc1 = DocumentWithMetadataFactory.build( - content: content1, - metadata: DocumentDataMetadata( - type: DocumentType.proposalDocument, - selfRef: ref1, - template: SignedDocumentRef(id: templateRef, version: templateRef), - ), - ); - - final doc2 = DocumentWithMetadataFactory.build( - content: content2, - metadata: DocumentDataMetadata( - type: DocumentType.proposalDocument, - selfRef: ref2, - template: SignedDocumentRef(id: templateRef, version: templateRef), - ), - ); - - await database.documentsDao.saveAll([doc1, doc2]); - - // When: query for documents with milestone_list.*.title == 'Milestone 2' - final results = await (database.documentsDao as DriftDocumentsDao) - .queryDocumentsByMatchedDocumentNodeIdValue( - nodeId: DocumentNodeId.fromString('*.subtitle'), - value: 'Subtitle', - type: DocumentType.proposalDocument, - content: 'content', - ); - - // Then: only doc1 should be returned - final refs = results.map((e) => e.metadata.selfRef).toList(); - expect(refs, contains(ref1)); - expect(refs, isNot(contains(ref2))); - }, - onPlatform: driftOnPlatforms, - ); - }); - }); - - group('count', () { - test( - 'document returns expected number', - () async { - // Given - final dateTime = DateTimeExt.now(); - - final documentsWithMetadata = List.generate( - 20, - (index) => DocumentWithMetadataFactory.build( - metadata: DocumentDataMetadata( - type: DocumentType.proposalDocument, - selfRef: _buildRefAt(dateTime.add(Duration(seconds: index))), - ), - ), - ); - - // When - await database.documentsDao.saveAll(documentsWithMetadata); - - // Then - final count = await database.documentsDao.countDocuments(); - - expect(count, documentsWithMetadata.length); - }, - onPlatform: driftOnPlatforms, - ); - - test( - 'two versions of same document will be counted as one', - () async { - // Given - final id = DocumentRefFactory.randomUuidV7(); - final documentsWithMetadata = List.generate( - 2, - (index) { - final metadata = DocumentDataMetadata( - type: DocumentType.proposalDocument, - selfRef: SignedDocumentRef(id: id, version: DocumentRefFactory.randomUuidV7()), - ); - return DocumentWithMetadataFactory.build(metadata: metadata); - }, - ); - - // When - await database.documentsDao.saveAll(documentsWithMetadata); - - // Then - final count = await database.documentsDao.countDocuments(); - - expect(count, 1); - }, - onPlatform: driftOnPlatforms, - ); - - test( - 'where without ver counts all versions', - () async { - // Given - final id = DocumentRefFactory.randomUuidV7(); - final documentsWithMetadata = List.generate( - 2, - (index) { - final metadata = DocumentDataMetadata( - type: DocumentType.proposalDocument, - selfRef: SignedDocumentRef(id: id, version: DocumentRefFactory.randomUuidV7()), - ); - return DocumentWithMetadataFactory.build(metadata: metadata); - }, - ); - - final expectedCount = documentsWithMetadata.length; - final ref = SignedDocumentRef(id: id); - - // When - await database.documentsDao.saveAll(documentsWithMetadata); - - // Then - final count = await database.documentsDao.count(ref: ref); - - expect(count, expectedCount); - }, - onPlatform: driftOnPlatforms, - ); - - test( - 'where with ver counts only matching results', - () async { - // Given - final id = DocumentRefFactory.randomUuidV7(); - final documentsWithMetadata = List.generate( - 2, - (index) { - final metadata = DocumentDataMetadata( - type: DocumentType.proposalDocument, - selfRef: SignedDocumentRef(id: id, version: DocumentRefFactory.randomUuidV7()), - ); - return DocumentWithMetadataFactory.build(metadata: metadata); - }, - ); - final version = documentsWithMetadata.first.document.metadata.version; - final ref = SignedDocumentRef(id: id, version: version); - - // When - await database.documentsDao.saveAll(documentsWithMetadata); - - // Then - final count = await database.documentsDao.count(ref: ref); - - expect(count, 1); - }, - onPlatform: driftOnPlatforms, - ); - - test( - 'where returns correct value when ' - 'many different documents are found', - () async { - // Given - final documentsWithMetadata = List.generate( - 10, - (index) { - final metadata = DocumentDataMetadata( - type: DocumentType.proposalDocument, - selfRef: DocumentRefFactory.signedDocumentRef(), - ); - return DocumentWithMetadataFactory.build(metadata: metadata); - }, - ); - final document = documentsWithMetadata.last.document; - final id = document.metadata.id; - final version = document.metadata.version; - final ref = SignedDocumentRef(id: id, version: version); - - // When - await database.documentsDao.saveAll(documentsWithMetadata); - - // Then - final count = await database.documentsDao.count(ref: ref); - - expect(count, 1); - }, - onPlatform: driftOnPlatforms, - ); - - test( - 'Counts comments for specific proposal document version', - () async { - final proposalId = DocumentRefFactory.randomUuidV7(); - final versionId = DocumentRefFactory.randomUuidV7(); - final proposalRef = SignedDocumentRef( - id: proposalId, - version: versionId, - ); - final proposal = DocumentWithMetadataFactory.build( - metadata: DocumentDataMetadata( - type: DocumentType.proposalDocument, - selfRef: proposalRef, - ), - ); - - await database.documentsDao.saveAll([proposal]); - - final comments = List.generate( - 10, - (index) => DocumentWithMetadataFactory.build( - metadata: DocumentDataMetadata( - type: DocumentType.commentTemplate, - selfRef: DocumentRefFactory.signedDocumentRef(), - ref: proposalRef, - ), - ), - ); - final otherComments = List.generate( - 5, - (index) => DocumentWithMetadataFactory.build( - metadata: DocumentDataMetadata( - type: DocumentType.commentTemplate, - selfRef: DocumentRefFactory.signedDocumentRef(), - ref: DocumentRefFactory.signedDocumentRef(), - ), - ), - ); - await database.documentsDao.saveAll([...comments, ...otherComments]); - - final count = await database.documentsDao.countRefDocumentByType( - ref: proposalRef, - type: DocumentType.commentTemplate, - ); - - expect(count, equals(10)); - }, - onPlatform: driftOnPlatforms, - ); - - test( - 'Count versions of specific document', - () async { - final proposalId = DocumentRefFactory.randomUuidV7(); - final versionId = DocumentRefFactory.randomUuidV7(); - final proposalRef = SignedDocumentRef( - id: proposalId, - version: versionId, - ); - final proposal = DocumentWithMetadataFactory.build( - metadata: DocumentDataMetadata( - type: DocumentType.proposalDocument, - selfRef: proposalRef, - ), - ); - - await database.documentsDao.saveAll([proposal]); - - final versions = List.generate( - 10, - (index) { - return DocumentWithMetadataFactory.build( - metadata: DocumentDataMetadata( - type: DocumentType.proposalDocument, - selfRef: SignedDocumentRef( - id: proposalId, - version: DocumentRefFactory.randomUuidV7(), - ), - ref: proposalRef, - ), - ); - }, - ); - - await database.documentsDao.saveAll(versions); - - final ids = await database.documentsDao.queryVersionsOfId( - id: proposalId, - ); - - expect(ids.length, equals(11)); - }, - onPlatform: driftOnPlatforms, - ); - - test( - 'Watches comments count', - () async { - final proposalId = DocumentRefFactory.randomUuidV7(); - final versionId = DocumentRefFactory.randomUuidV7(); - final proposalId2 = DocumentRefFactory.randomUuidV7(); - - final proposalRef = SignedDocumentRef( - id: proposalId, - version: versionId, - ); - final proposal = DocumentWithMetadataFactory.build( - metadata: DocumentDataMetadata( - type: DocumentType.proposalDocument, - selfRef: proposalRef, - ), - ); - - final proposalRef2 = SignedDocumentRef( - id: proposalId2, - version: versionId, - ); - - await database.documentsDao.saveAll([proposal]); - - final comments = List.generate(2, (index) { - return DocumentWithMetadataFactory.build( - metadata: DocumentDataMetadata( - type: DocumentType.commentTemplate, - selfRef: DocumentRefFactory.signedDocumentRef(), - ref: proposalRef, - ), - ); - }); - - final otherComment = DocumentWithMetadataFactory.build( - metadata: DocumentDataMetadata( - type: DocumentType.commentTemplate, - selfRef: DocumentRefFactory.signedDocumentRef(), - ref: proposalRef2, - ), - ); - - await database.documentsDao.saveAll([comments.first, otherComment]); - - final documentCount = database.documentsDao - .watchCount( - refTo: proposalRef, - type: DocumentType.commentTemplate, - ) - .asBroadcastStream(); - - final firstEmission = await documentCount.first; - - expect(firstEmission, equals(1)); - - await database.documentsDao.saveAll([comments.last]); - final secondEmission = await documentCount.first; - expect(secondEmission, equals(2)); - }, - onPlatform: driftOnPlatforms, - ); - }); - - group('delete all', () { - test( - 'removes all documents', - () async { - // Given - final documentsWithMetadata = List.generate( - 5, - (index) => DocumentWithMetadataFactory.build(), - ); - - // When - await database.documentsDao.saveAll(documentsWithMetadata); - - // Then - final countBefore = await database.documentsDao.countDocuments(); - expect(countBefore, isNonZero); - - await database.documentsDao.deleteAll(); - - final countAfter = await database.documentsDao.countDocuments(); - expect(countAfter, isZero); - }, - onPlatform: driftOnPlatforms, - ); - - test( - 'cascades metadata', - () async { - // Given - final documentsWithMetadata = List.generate( - 5, - (index) => DocumentWithMetadataFactory.build(), - ); - - // When - await database.documentsDao.saveAll(documentsWithMetadata); - - // Then - final before = await database.documentsDao.countDocumentsMetadata(); - expect(before, isNonZero); - - await database.documentsDao.deleteAll(); - - final after = await database.documentsDao.countDocumentsMetadata(); - expect(after, isZero); - }, - onPlatform: driftOnPlatforms, - ); - - test( - 'templates used in local drafts are kept when flat is true', - () async { - // Given - final template = DocumentWithMetadataFactory.build( - metadata: DocumentDataMetadata( - type: DocumentType.proposalTemplate, - selfRef: DocumentRefFactory.signedDocumentRef(), - ), - ); - - final localDraft = DraftFactory.build( - metadata: DocumentDataMetadata( - type: DocumentType.proposalDocument, - selfRef: DocumentRefFactory.draftRef(), - template: template.document.metadata.selfRef as SignedDocumentRef, - ), - ); - - final randomDocuments = List.generate( - 10, - (index) => DocumentWithMetadataFactory.build(), - ); - - final allDocuments = [ - template, - ...randomDocuments, - ]; - - final allDrafts = [ - localDraft, - ]; - - // When - await database.documentsDao.saveAll(allDocuments); - await database.draftsDao.saveAll(allDrafts); - - // Then - await database.documentsDao.deleteAll(keepTemplatesForLocalDrafts: true); - - final documentsCount = await database.documentsDao.count(); - final draftsCount = await database.draftsDao.count(); - - expect(documentsCount, 1); - expect(draftsCount, 1); - }, - onPlatform: driftOnPlatforms, - ); - }); - }); -} - -SignedDocumentRef _buildRefAt(DateTime dateTime) { - final config = V7Options(dateTime.millisecondsSinceEpoch, null); - final val = const Uuid().v7(config: config); - return SignedDocumentRef.first(val); -} - -List _generateDocumentEntitiesWithMetadata(int count) { - return List.generate( - count, - (index) => DocumentWithMetadataFactory.build(), - ); -} diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/database/dao/documents_v2_dao_test.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/database/dao/documents_v2_dao_test.dart index 6fcf37bfc12e..6eb01435a73b 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/database/dao/documents_v2_dao_test.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/database/dao/documents_v2_dao_test.dart @@ -1,17 +1,16 @@ // ignore_for_file: avoid_redundant_argument_values +import 'dart:typed_data'; import 'package:catalyst_voices_dev/catalyst_voices_dev.dart'; import 'package:catalyst_voices_models/catalyst_voices_models.dart'; import 'package:catalyst_voices_repositories/src/database/catalyst_database.dart'; import 'package:catalyst_voices_repositories/src/database/dao/documents_v2_dao.dart'; import 'package:catalyst_voices_repositories/src/database/model/document_with_authors_entity.dart'; -import 'package:catalyst_voices_repositories/src/database/table/document_authors.drift.dart'; import 'package:catalyst_voices_repositories/src/database/table/documents_v2.drift.dart'; import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:uuid_plus/uuid_plus.dart'; +import '../../utils/document_with_authors_factory.dart'; import '../connection/test_connection.dart'; void main() { @@ -72,6 +71,275 @@ void main() { // Then expect(result, 2); }); + + test('filters by type and returns matching count', () async { + // Given + final proposal = _createTestDocumentEntity( + id: 'proposal-id', + ver: 'proposal-ver', + type: DocumentType.proposalDocument, + ); + final comment = _createTestDocumentEntity( + id: 'comment-id', + ver: 'comment-ver', + type: DocumentType.commentDocument, + ); + final template = _createTestDocumentEntity( + id: 'template-id', + ver: 'template-ver', + type: DocumentType.proposalTemplate, + ); + await dao.saveAll([proposal, comment, template]); + + // When + final result = await dao.count(type: DocumentType.proposalDocument); + + // Then + expect(result, 1); + }); + + test('returns zero when no documents match type filter', () async { + // Given + final comment = _createTestDocumentEntity( + id: 'comment-id', + ver: 'comment-ver', + type: DocumentType.commentDocument, + ); + await dao.save(comment); + + // When + final result = await dao.count(type: DocumentType.proposalDocument); + + // Then + expect(result, 0); + }); + + test('filters by loose ref and returns all versions count', () async { + // Given + final v1 = _createTestDocumentEntity(id: 'multi-id', ver: 'ver-1'); + final v2 = _createTestDocumentEntity(id: 'multi-id', ver: 'ver-2'); + final other = _createTestDocumentEntity(id: 'other-id', ver: 'other-ver'); + await dao.saveAll([v1, v2, other]); + + // When + final result = await dao.count( + ref: const SignedDocumentRef.loose(id: 'multi-id'), + ); + + // Then + expect(result, 2); + }); + + test('filters by exact ref and returns single match', () async { + // Given + final v1 = _createTestDocumentEntity(id: 'multi-id', ver: 'ver-1'); + final v2 = _createTestDocumentEntity(id: 'multi-id', ver: 'ver-2'); + await dao.saveAll([v1, v2]); + + // When + final result = await dao.count( + ref: const SignedDocumentRef.exact(id: 'multi-id', version: 'ver-1'), + ); + + // Then + expect(result, 1); + }); + + test('returns zero for non-existing ref', () async { + // Given + final entity = _createTestDocumentEntity(id: 'existing-id', ver: 'existing-ver'); + await dao.save(entity); + + // When + final result = await dao.count( + ref: const SignedDocumentRef.exact(id: 'non-existent', version: 'non-ver'), + ); + + // Then + expect(result, 0); + }); + + test('filters by loose refTo and returns documents referencing id', () async { + // Given + final proposal = _createTestDocumentEntity( + id: 'proposal-id', + ver: 'proposal-ver', + type: DocumentType.proposalDocument, + ); + final action1 = _createTestDocumentEntity( + id: 'action-1', + ver: 'action-ver-1', + type: DocumentType.proposalActionDocument, + refId: 'proposal-id', + refVer: 'proposal-ver', + ); + final action2 = _createTestDocumentEntity( + id: 'action-2', + ver: 'action-ver-2', + type: DocumentType.proposalActionDocument, + refId: 'proposal-id', + refVer: 'proposal-ver-2', + ); + final unrelated = _createTestDocumentEntity( + id: 'unrelated', + ver: 'unrelated-ver', + type: DocumentType.proposalActionDocument, + refId: 'other-proposal', + refVer: 'other-ver', + ); + await dao.saveAll([proposal, action1, action2, unrelated]); + + // When + final result = await dao.count( + referencing: const SignedDocumentRef.loose(id: 'proposal-id'), + ); + + // Then + expect(result, 2); + }); + + test('filters by exact refTo and returns documents referencing exact version', () async { + // Given + final action1 = _createTestDocumentEntity( + id: 'action-1', + ver: 'action-ver-1', + type: DocumentType.proposalActionDocument, + refId: 'proposal-id', + refVer: 'proposal-ver-1', + ); + final action2 = _createTestDocumentEntity( + id: 'action-2', + ver: 'action-ver-2', + type: DocumentType.proposalActionDocument, + refId: 'proposal-id', + refVer: 'proposal-ver-2', + ); + await dao.saveAll([action1, action2]); + + // When + final result = await dao.count( + referencing: const SignedDocumentRef.exact( + id: 'proposal-id', + version: 'proposal-ver-1', + ), + ); + + // Then + expect(result, 1); + }); + + test('returns zero when no documents match refTo filter', () async { + // Given + final action = _createTestDocumentEntity( + id: 'action-id', + ver: 'action-ver', + type: DocumentType.proposalActionDocument, + refId: 'proposal-id', + refVer: 'proposal-ver', + ); + await dao.save(action); + + // When + final result = await dao.count( + referencing: const SignedDocumentRef.loose(id: 'non-existent-proposal'), + ); + + // Then + expect(result, 0); + }); + + test('combines type and ref filters', () async { + // Given + final proposal1 = _createTestDocumentEntity( + id: 'proposal-id', + ver: 'ver-1', + type: DocumentType.proposalDocument, + ); + final proposal2 = _createTestDocumentEntity( + id: 'proposal-id', + ver: 'ver-2', + type: DocumentType.proposalDocument, + ); + final comment = _createTestDocumentEntity( + id: 'proposal-id', + ver: 'ver-3', + type: DocumentType.commentDocument, + ); + await dao.saveAll([proposal1, proposal2, comment]); + + // When + final result = await dao.count( + type: DocumentType.proposalDocument, + ref: const SignedDocumentRef.loose(id: 'proposal-id'), + ); + + // Then + expect(result, 2); + }); + + test('combines type and refTo filters', () async { + // Given + final action = _createTestDocumentEntity( + id: 'action-id', + ver: 'action-ver', + type: DocumentType.proposalActionDocument, + refId: 'proposal-id', + refVer: 'proposal-ver', + ); + final comment = _createTestDocumentEntity( + id: 'comment-id', + ver: 'comment-ver', + type: DocumentType.commentDocument, + refId: 'proposal-id', + refVer: 'proposal-ver', + ); + await dao.saveAll([action, comment]); + + // When + final result = await dao.count( + type: DocumentType.proposalActionDocument, + referencing: const SignedDocumentRef.loose(id: 'proposal-id'), + ); + + // Then + expect(result, 1); + }); + + test('combines all three filters', () async { + // Given + final action1 = _createTestDocumentEntity( + id: 'action-id', + ver: 'ver-1', + type: DocumentType.proposalActionDocument, + refId: 'proposal-id', + refVer: 'proposal-ver', + ); + final action2 = _createTestDocumentEntity( + id: 'action-id', + ver: 'ver-2', + type: DocumentType.proposalActionDocument, + refId: 'proposal-id', + refVer: 'proposal-ver', + ); + final action3 = _createTestDocumentEntity( + id: 'other-action', + ver: 'ver-3', + type: DocumentType.proposalActionDocument, + refId: 'proposal-id', + refVer: 'proposal-ver', + ); + await dao.saveAll([action1, action2, action3]); + + // When + final result = await dao.count( + type: DocumentType.proposalActionDocument, + ref: const SignedDocumentRef.loose(id: 'action-id'), + referencing: const SignedDocumentRef.loose(id: 'proposal-id'), + ); + + // Then + expect(result, 2); + }); }); group('exists', () { @@ -298,7 +566,7 @@ void main() { const ref = SignedDocumentRef.exact(id: 'non-existent-id', version: 'non-existent-ver'); // When - final result = await dao.getDocument(ref); + final result = await dao.getDocument(ref: ref); // Then expect(result, isNull); @@ -313,7 +581,7 @@ void main() { const ref = SignedDocumentRef.exact(id: 'test-id', version: 'test-ver'); // When - final result = await dao.getDocument(ref); + final result = await dao.getDocument(ref: ref); // Then expect(result, isNotNull); @@ -329,10 +597,10 @@ void main() { // And const ref = SignedDocumentRef.exact(id: 'test-id', version: 'wrong-ver'); - // When: getDocument is called - final result = await dao.getDocument(ref); + // When + final result = await dao.getDocument(ref: ref); - // Then: Returns null + // Then expect(result, isNull); }); @@ -351,7 +619,7 @@ void main() { const ref = SignedDocumentRef.loose(id: 'test-id'); // When - final result = await dao.getDocument(ref); + final result = await dao.getDocument(ref: ref); // Then expect(result, isNotNull); @@ -368,142 +636,501 @@ void main() { const ref = SignedDocumentRef.loose(id: 'non-existent-id'); // When - final result = await dao.getDocument(ref); + final result = await dao.getDocument(ref: ref); // Then expect(result, isNull); }); - }); - group('saveAll', () { - test('does nothing for empty list', () async { + test('filters by type and returns matching document', () async { // Given - final entities = []; + final proposal = _createTestDocumentEntity( + id: 'proposal-id', + ver: 'proposal-ver', + type: DocumentType.proposalDocument, + ); + final comment = _createTestDocumentEntity( + id: 'comment-id', + ver: 'comment-ver', + type: DocumentType.commentDocument, + ); + await dao.saveAll([proposal, comment]); // When - await dao.saveAll(entities); + final result = await dao.getDocument(type: DocumentType.proposalDocument); // Then - final count = await dao.count(); - expect(count, 0); + expect(result, isNotNull); + expect(result!.id, 'proposal-id'); + expect(result.type, DocumentType.proposalDocument); }); - test('inserts new documents', () async { + test('returns null when no documents match type filter', () async { // Given - final entities = [ - _createTestDocumentEntity(), - _createTestDocumentEntity(), - ]; + final comment = _createTestDocumentEntity( + id: 'comment-id', + ver: 'comment-ver', + type: DocumentType.commentDocument, + ); + await dao.save(comment); // When - await dao.saveAll(entities); + final result = await dao.getDocument(type: DocumentType.proposalDocument); // Then - final saved = await db.select(db.documentsV2).get(); - final savedIds = saved.map((e) => e.id); - final expectedIds = entities.map((e) => e.doc.id); - - expect(savedIds, expectedIds); + expect(result, isNull); }); - test('ignores conflicts on existing {id, ver}', () async { + test('returns latest document when filtering by type only', () async { // Given - final existing = _createTestDocumentEntity( - id: 'test-id', - ver: 'test-ver', - contentData: {'key': 'original'}, - ); - await dao.save(existing); + final oldCreatedAt = DateTime.utc(2023, 1, 1); + final newerCreatedAt = DateTime.utc(2024, 1, 1); - // And - final entities = [ - _createTestDocumentEntity( - id: 'test-id', - ver: 'test-ver', - contentData: {'key': 'modified'}, - ), - _createTestDocumentEntity(id: 'new-id', ver: 'new-ver'), - ]; + final oldVer = _buildUuidV7At(oldCreatedAt); + final newerVer = _buildUuidV7At(newerCreatedAt); + + final oldProposal = _createTestDocumentEntity( + id: 'proposal-1', + ver: oldVer, + type: DocumentType.proposalDocument, + ); + final newProposal = _createTestDocumentEntity( + id: 'proposal-2', + ver: newerVer, + type: DocumentType.proposalDocument, + ); + await dao.saveAll([oldProposal, newProposal]); // When - await dao.saveAll(entities); + final result = await dao.getDocument(type: DocumentType.proposalDocument); // Then - final saved = await db.select(db.documentsV2).get(); - expect(saved.length, 2); - final existingAfter = saved.firstWhere((e) => e.id == 'test-id'); - expect(existingAfter.content.data['key'], 'original'); - expect(saved.any((e) => e.id == 'new-id'), true); + expect(result, isNotNull); }); - test('handles mixed inserts and ignores atomically', () async { + test('filters by loose refTo and returns latest referencing document', () async { // Given - final existing1 = _createTestDocumentEntity(id: 'existing-1', ver: 'ver-1'); - final existing2 = _createTestDocumentEntity(id: 'existing-2', ver: 'ver-2'); - await dao.save(existing1); - await dao.save(existing2); + final oldCreatedAt = DateTime.utc(2023, 1, 1); + final newerCreatedAt = DateTime.utc(2024, 1, 1); - // And: - final entities = [ - _createTestDocumentEntity(id: 'existing-1', ver: 'ver-1'), - _createTestDocumentEntity(id: 'new-1', ver: 'new-ver-1'), - _createTestDocumentEntity(id: 'existing-2', ver: 'ver-2'), - _createTestDocumentEntity(id: 'new-2', ver: 'new-ver-2'), - ]; + final oldVer = _buildUuidV7At(oldCreatedAt); + final newerVer = _buildUuidV7At(newerCreatedAt); + + final action1 = _createTestDocumentEntity( + id: 'action-1', + ver: oldVer, + type: DocumentType.proposalActionDocument, + refId: 'proposal-id', + refVer: 'proposal-ver', + ); + final action2 = _createTestDocumentEntity( + id: 'action-2', + ver: newerVer, + type: DocumentType.proposalActionDocument, + refId: 'proposal-id', + refVer: 'proposal-ver-2', + ); + final unrelated = _createTestDocumentEntity( + id: 'unrelated', + ver: 'unrelated-ver', + type: DocumentType.proposalActionDocument, + refId: 'other-proposal', + refVer: 'other-ver', + ); + await dao.saveAll([action1, action2, unrelated]); // When - await dao.saveAll(entities); + final result = await dao.getDocument( + referencing: const SignedDocumentRef.loose(id: 'proposal-id'), + ); // Then - final saved = await db.select(db.documentsV2).get(); - expect(saved.length, 4); - expect(saved.map((e) => e.id).toSet(), {'existing-1', 'existing-2', 'new-1', 'new-2'}); + expect(result, isNotNull); + expect(result!.refId, 'proposal-id'); }); - }); - group('save', () { - test('inserts new document', () async { + test('filters by exact refTo and returns matching document', () async { // Given - final entity = _createTestDocumentEntity( - id: 'test-id', - ver: '0194d492-1daa-7371-8bd3-c15811b2b063', + final action1 = _createTestDocumentEntity( + id: 'action-1', + ver: 'action-ver-1', + type: DocumentType.proposalActionDocument, + refId: 'proposal-id', + refVer: 'proposal-ver-1', + ); + final action2 = _createTestDocumentEntity( + id: 'action-2', + ver: 'action-ver-2', + type: DocumentType.proposalActionDocument, + refId: 'proposal-id', + refVer: 'proposal-ver-2', ); + await dao.saveAll([action1, action2]); // When - await dao.save(entity); + final result = await dao.getDocument( + referencing: const SignedDocumentRef.exact( + id: 'proposal-id', + version: 'proposal-ver-1', + ), + ); // Then - final saved = await db.select(db.documentsV2).get(); - expect(saved.length, 1); - expect(saved[0].id, 'test-id'); - expect(saved[0].ver, '0194d492-1daa-7371-8bd3-c15811b2b063'); + expect(result, isNotNull); + expect(result!.id, 'action-1'); + expect(result.refVer, 'proposal-ver-1'); }); - test('ignores conflict on existing {id, ver}', () async { + test('returns null when no documents match refTo filter', () async { // Given - final existing = _createTestDocumentEntity( - id: 'test-id', - ver: '0194d492-1daa-7371-8bd3-c15811b2b063', - contentData: {'key': 'original'}, - ); - await dao.save(existing); - - // And - final conflicting = _createTestDocumentEntity( - id: 'test-id', - ver: '0194d492-1daa-7371-8bd3-c15811b2b063', - contentData: {'key': 'modified'}, + final action = _createTestDocumentEntity( + id: 'action-id', + ver: 'action-ver', + type: DocumentType.proposalActionDocument, + refId: 'proposal-id', + refVer: 'proposal-ver', ); + await dao.save(action); // When - await dao.save(conflicting); + final result = await dao.getDocument( + referencing: const SignedDocumentRef.loose(id: 'non-existent-proposal'), + ); // Then - final saved = await db.select(db.documentsV2).get(); - expect(saved.length, 1); - expect(saved[0].content.data['key'], 'original'); + expect(result, isNull); }); - }); + + test('combines type and ref filters', () async { + // Given + final proposal = _createTestDocumentEntity( + id: 'doc-id', + ver: 'ver-1', + type: DocumentType.proposalDocument, + ); + final comment = _createTestDocumentEntity( + id: 'doc-id', + ver: 'ver-2', + type: DocumentType.commentDocument, + ); + await dao.saveAll([proposal, comment]); + + // When + final result = await dao.getDocument( + type: DocumentType.proposalDocument, + ref: const SignedDocumentRef.loose(id: 'doc-id'), + ); + + // Then + expect(result, isNotNull); + expect(result!.id, 'doc-id'); + expect(result.ver, 'ver-1'); + expect(result.type, DocumentType.proposalDocument); + }); + + test('returns null when type and ref filters have no intersection', () async { + // Given + final proposal = _createTestDocumentEntity( + id: 'proposal-id', + ver: 'proposal-ver', + type: DocumentType.proposalDocument, + ); + await dao.save(proposal); + + // When + final result = await dao.getDocument( + type: DocumentType.commentDocument, + ref: const SignedDocumentRef.loose(id: 'proposal-id'), + ); + + // Then + expect(result, isNull); + }); + + test('combines type and refTo filters', () async { + // Given + final action = _createTestDocumentEntity( + id: 'action-id', + ver: 'action-ver', + type: DocumentType.proposalActionDocument, + refId: 'proposal-id', + refVer: 'proposal-ver', + ); + final comment = _createTestDocumentEntity( + id: 'comment-id', + ver: 'comment-ver', + type: DocumentType.commentDocument, + refId: 'proposal-id', + refVer: 'proposal-ver', + ); + await dao.saveAll([action, comment]); + + // When + final result = await dao.getDocument( + type: DocumentType.proposalActionDocument, + referencing: const SignedDocumentRef.loose(id: 'proposal-id'), + ); + + // Then + expect(result, isNotNull); + expect(result!.id, 'action-id'); + expect(result.type, DocumentType.proposalActionDocument); + }); + + test('combines ref and refTo filters', () async { + // Given + final action1 = _createTestDocumentEntity( + id: 'action-id', + ver: 'ver-1', + type: DocumentType.proposalActionDocument, + refId: 'proposal-1', + refVer: 'proposal-ver', + ); + final action2 = _createTestDocumentEntity( + id: 'action-id', + ver: 'ver-2', + type: DocumentType.proposalActionDocument, + refId: 'proposal-2', + refVer: 'proposal-ver', + ); + await dao.saveAll([action1, action2]); + + // When + final result = await dao.getDocument( + ref: const SignedDocumentRef.loose(id: 'action-id'), + referencing: const SignedDocumentRef.loose(id: 'proposal-1'), + ); + + // Then + expect(result, isNotNull); + expect(result!.id, 'action-id'); + expect(result.refId, 'proposal-1'); + }); + + test('combines all three filters', () async { + // Given + final oldCreatedAt = DateTime.utc(2023, 1, 1); + final newerCreatedAt = DateTime.utc(2024, 1, 1); + + final oldVer = _buildUuidV7At(oldCreatedAt); + final newerVer = _buildUuidV7At(newerCreatedAt); + + final action1 = _createTestDocumentEntity( + id: 'action-id', + ver: oldVer, + type: DocumentType.proposalActionDocument, + refId: 'proposal-id', + refVer: 'proposal-ver', + ); + final action2 = _createTestDocumentEntity( + id: 'action-id', + ver: newerVer, + type: DocumentType.proposalActionDocument, + refId: 'proposal-id', + refVer: 'proposal-ver', + ); + final comment = _createTestDocumentEntity( + id: 'action-id', + ver: 'comment-ver', + type: DocumentType.commentDocument, + refId: 'proposal-id', + refVer: 'proposal-ver', + ); + await dao.saveAll([action1, action2, comment]); + + // When + final result = await dao.getDocument( + type: DocumentType.proposalActionDocument, + ref: const SignedDocumentRef.loose(id: 'action-id'), + referencing: const SignedDocumentRef.loose(id: 'proposal-id'), + ); + + // Then + expect(result, isNotNull); + expect(result!.id, 'action-id'); + expect(result.ver, newerVer); + expect(result.type, DocumentType.proposalActionDocument); + expect(result.refId, 'proposal-id'); + }); + + test('returns null when all three filters have no intersection', () async { + // Given + final action = _createTestDocumentEntity( + id: 'action-id', + ver: 'action-ver', + type: DocumentType.proposalActionDocument, + refId: 'proposal-id', + refVer: 'proposal-ver', + ); + await dao.save(action); + + // When + final result = await dao.getDocument( + type: DocumentType.commentDocument, + ref: const SignedDocumentRef.loose(id: 'action-id'), + referencing: const SignedDocumentRef.loose(id: 'proposal-id'), + ); + + // Then + expect(result, isNull); + }); + + test('returns newest document by author', () async { + // Given + final author = _createTestAuthor(name: 'Damian'); + final proposal1 = _createTestDocumentEntity( + id: 'proposal1-id', + ver: _buildUuidV7At(DateTime(2023)), + type: DocumentType.proposalDocument, + authors: author.toUri().toString(), + ); + final newerVer = _buildUuidV7At(DateTime(2024)); + final proposal2 = _createTestDocumentEntity( + id: 'proposal2-id', + ver: newerVer, + type: DocumentType.proposalDocument, + authors: author.toUri().toString(), + ); + await dao.saveAll([proposal1, proposal2]); + + // When + final result = await dao.getDocument(author: author); + + // Then + expect(result, isNotNull); + expect(result?.ver, newerVer); + }); + }); + + group('saveAll', () { + test('does nothing for empty list', () async { + // Given + final entities = []; + + // When + await dao.saveAll(entities); + + // Then + final count = await dao.count(); + expect(count, 0); + }); + + test('inserts new documents', () async { + // Given + final entities = [ + _createTestDocumentEntity(), + _createTestDocumentEntity(), + ]; + + // When + await dao.saveAll(entities); + + // Then + final saved = await db.select(db.documentsV2).get(); + final savedIds = saved.map((e) => e.id); + final expectedIds = entities.map((e) => e.doc.id); + + expect(savedIds, expectedIds); + }); + + test('ignores conflicts on existing {id, ver}', () async { + // Given + final existing = _createTestDocumentEntity( + id: 'test-id', + ver: 'test-ver', + contentData: {'key': 'original'}, + ); + await dao.save(existing); + + // And + final entities = [ + _createTestDocumentEntity( + id: 'test-id', + ver: 'test-ver', + contentData: {'key': 'modified'}, + ), + _createTestDocumentEntity(id: 'new-id', ver: 'new-ver'), + ]; + + // When + await dao.saveAll(entities); + + // Then + final saved = await db.select(db.documentsV2).get(); + expect(saved.length, 2); + final existingAfter = saved.firstWhere((e) => e.id == 'test-id'); + expect(existingAfter.content.data['key'], 'original'); + expect(saved.any((e) => e.id == 'new-id'), true); + }); + + test('handles mixed inserts and ignores atomically', () async { + // Given + final existing1 = _createTestDocumentEntity(id: 'existing-1', ver: 'ver-1'); + final existing2 = _createTestDocumentEntity(id: 'existing-2', ver: 'ver-2'); + await dao.save(existing1); + await dao.save(existing2); + + // And: + final entities = [ + _createTestDocumentEntity(id: 'existing-1', ver: 'ver-1'), + _createTestDocumentEntity(id: 'new-1', ver: 'new-ver-1'), + _createTestDocumentEntity(id: 'existing-2', ver: 'ver-2'), + _createTestDocumentEntity(id: 'new-2', ver: 'new-ver-2'), + ]; + + // When + await dao.saveAll(entities); + + // Then + final saved = await db.select(db.documentsV2).get(); + expect(saved.length, 4); + expect(saved.map((e) => e.id).toSet(), {'existing-1', 'existing-2', 'new-1', 'new-2'}); + }); + }); + + group('save', () { + test('inserts new document', () async { + // Given + final entity = _createTestDocumentEntity( + id: 'test-id', + ver: '0194d492-1daa-7371-8bd3-c15811b2b063', + ); + + // When + await dao.save(entity); + + // Then + final saved = await db.select(db.documentsV2).get(); + expect(saved.length, 1); + expect(saved[0].id, 'test-id'); + expect(saved[0].ver, '0194d492-1daa-7371-8bd3-c15811b2b063'); + }); + + test('ignores conflict on existing {id, ver}', () async { + // Given + final existing = _createTestDocumentEntity( + id: 'test-id', + ver: '0194d492-1daa-7371-8bd3-c15811b2b063', + contentData: {'key': 'original'}, + ); + await dao.save(existing); + + // And + final conflicting = _createTestDocumentEntity( + id: 'test-id', + ver: '0194d492-1daa-7371-8bd3-c15811b2b063', + contentData: {'key': 'modified'}, + ); + + // When + await dao.save(conflicting); + + // Then + final saved = await db.select(db.documentsV2).get(); + expect(saved.length, 1); + expect(saved[0].content.data['key'], 'original'); + }); + }); group('watchDocuments', () { test('emits all documents when no filters applied', () async { @@ -1010,68 +1637,696 @@ void main() { expect(result!.version, versions[3]); }); }); - }); -} -String _buildUuidV7At(DateTime dateTime) { - final ts = dateTime.millisecondsSinceEpoch; - final rand = Uint8List.fromList([42, 0, 0, 0, 0, 0, 0, 0, 0, 0]); - return const UuidV7().generate(options: V7Options(ts, rand)); -} + group('deleteWhere', () { + test('returns zero when database is empty', () async { + // Given: An empty database -DocumentWithAuthorsEntity _createTestDocumentEntity({ - String? id, - String? ver, - Map contentData = const {}, - DocumentType type = DocumentType.proposalDocument, - String? authors, - String? categoryId, - String? categoryVer, - String? refId, - String? refVer, - String? replyId, - String? replyVer, - String? section, - String? templateId, - String? templateVer, -}) { - id ??= DocumentRefFactory.randomUuidV7(); - ver ??= id; - authors ??= ''; + // When + final result = await dao.deleteWhere(); - final docEntity = DocumentEntityV2( - id: id, - ver: ver, - content: DocumentDataContent(contentData), - createdAt: ver.tryDateTime ?? DateTime.timestamp(), - type: type, - authors: authors, - categoryId: categoryId, - categoryVer: categoryVer, - refId: refId, - refVer: refVer, - replyId: replyId, - replyVer: replyVer, + // Then + expect(result, 0); + }); + + test('deletes all documents when no filter is provided', () async { + // Given + final entities = [ + _createTestDocumentEntity( + id: 'id-1', + ver: 'ver-1', + type: DocumentType.proposalDocument, + ), + _createTestDocumentEntity( + id: 'id-2', + ver: 'ver-2', + type: DocumentType.commentDocument, + ), + _createTestDocumentEntity( + id: 'id-3', + ver: 'ver-3', + type: DocumentType.proposalTemplate, + ), + ]; + await dao.saveAll(entities); + + // When + final result = await dao.deleteWhere(); + + // Then + expect(result, 3); + expect(await dao.count(), 0); + }); + + test('deletes documents not in notInType list', () async { + // Given + final proposal = _createTestDocumentEntity( + id: 'proposal-id', + ver: 'proposal-ver', + type: DocumentType.proposalDocument, + ); + final comment = _createTestDocumentEntity( + id: 'comment-id', + ver: 'comment-ver', + type: DocumentType.commentDocument, + ); + final template = _createTestDocumentEntity( + id: 'template-id', + ver: 'template-ver', + type: DocumentType.proposalTemplate, + ); + await dao.saveAll([proposal, comment, template]); + + // When + final result = await dao.deleteWhere( + excludeTypes: [DocumentType.proposalDocument], + ); + + // Then + expect(result, 2); + expect(await dao.count(), 1); + + final remaining = await dao.getDocument( + ref: const SignedDocumentRef.exact(id: 'proposal-id', version: 'proposal-ver'), + ); + expect(remaining, isNotNull); + expect(remaining!.type, DocumentType.proposalDocument); + }); + + test('keeps multiple document types when specified in notInType', () async { + // Given + final proposal = _createTestDocumentEntity( + id: 'proposal-id', + ver: 'proposal-ver', + type: DocumentType.proposalDocument, + ); + final comment = _createTestDocumentEntity( + id: 'comment-id', + ver: 'comment-ver', + type: DocumentType.commentDocument, + ); + final template = _createTestDocumentEntity( + id: 'template-id', + ver: 'template-ver', + type: DocumentType.proposalTemplate, + ); + final action = _createTestDocumentEntity( + id: 'action-id', + ver: 'action-ver', + type: DocumentType.proposalActionDocument, + ); + await dao.saveAll([proposal, comment, template, action]); + + // When + final result = await dao.deleteWhere( + excludeTypes: [ + DocumentType.proposalDocument, + DocumentType.proposalTemplate, + ], + ); + + // Then + expect(result, 2); + expect(await dao.count(), 2); + + final remainingProposal = await dao.getDocument( + ref: const SignedDocumentRef.exact(id: 'proposal-id', version: 'proposal-ver'), + ); + final remainingTemplate = await dao.getDocument( + ref: const SignedDocumentRef.exact(id: 'template-id', version: 'template-ver'), + ); + final deletedComment = await dao.getDocument( + ref: const SignedDocumentRef.exact(id: 'comment-id', version: 'comment-ver'), + ); + final deletedAction = await dao.getDocument( + ref: const SignedDocumentRef.exact(id: 'action-id', version: 'action-ver'), + ); + + expect(remainingProposal, isNotNull); + expect(remainingTemplate, isNotNull); + expect(deletedComment, isNull); + expect(deletedAction, isNull); + }); + + test('deletes all documents when notInType is empty list', () async { + // Given + final entities = [ + _createTestDocumentEntity( + id: 'id-1', + ver: 'ver-1', + type: DocumentType.proposalDocument, + ), + _createTestDocumentEntity( + id: 'id-2', + ver: 'ver-2', + type: DocumentType.commentDocument, + ), + ]; + await dao.saveAll(entities); + + // When + final result = await dao.deleteWhere(excludeTypes: []); + + // Then + expect(result, 2); + expect(await dao.count(), 0); + }); + + test('returns zero when all documents match notInType filter', () async { + // Given + final entities = [ + _createTestDocumentEntity( + id: 'id-1', + ver: 'ver-1', + type: DocumentType.proposalDocument, + ), + _createTestDocumentEntity( + id: 'id-2', + ver: 'ver-2', + type: DocumentType.proposalDocument, + ), + ]; + await dao.saveAll(entities); + + // When + final result = await dao.deleteWhere( + excludeTypes: [DocumentType.proposalDocument], + ); + + // Then + expect(result, 0); + expect(await dao.count(), 2); + }); + + test('handles multiple versions of same document id', () async { + // Given + final v1 = _createTestDocumentEntity( + id: 'multi-id', + ver: 'ver-1', + type: DocumentType.proposalDocument, + ); + final v2 = _createTestDocumentEntity( + id: 'multi-id', + ver: 'ver-2', + type: DocumentType.proposalDocument, + ); + final other = _createTestDocumentEntity( + id: 'other-id', + ver: 'other-ver', + type: DocumentType.commentDocument, + ); + await dao.saveAll([v1, v2, other]); + + // When + final result = await dao.deleteWhere( + excludeTypes: [DocumentType.proposalDocument], + ); + + // Then + expect(result, 1); + expect(await dao.count(), 2); + }); + + test('deletes documents with all different types correctly', () async { + // Given + final entities = [ + _createTestDocumentEntity( + id: 'id-1', + ver: 'ver-1', + type: DocumentType.proposalDocument, + ), + _createTestDocumentEntity( + id: 'id-2', + ver: 'ver-2', + type: DocumentType.commentDocument, + ), + _createTestDocumentEntity( + id: 'id-3', + ver: 'ver-3', + type: DocumentType.reviewDocument, + ), + _createTestDocumentEntity( + id: 'id-4', + ver: 'ver-4', + type: DocumentType.proposalActionDocument, + ), + _createTestDocumentEntity( + id: 'id-5', + ver: 'ver-5', + type: DocumentType.proposalTemplate, + ), + ]; + await dao.saveAll(entities); + + // When + final result = await dao.deleteWhere( + excludeTypes: [ + DocumentType.proposalDocument, + DocumentType.proposalTemplate, + DocumentType.proposalActionDocument, + ], + ); + + // Then + expect(result, 2); + expect(await dao.count(), 3); + }); + + test('performs efficiently with large dataset', () async { + // Given + final entities = List.generate( + 1000, + (i) => _createTestDocumentEntity( + id: 'id-$i', + ver: 'ver-$i', + type: i.isEven ? DocumentType.proposalDocument : DocumentType.commentDocument, + ), + ); + await dao.saveAll(entities); + + // When + final result = await dao.deleteWhere( + excludeTypes: [DocumentType.proposalDocument], + ); + + // Then + expect(result, 500); + expect(await dao.count(), 500); + }); + }); + + group('getDocuments', () { + test('returns empty list for empty database', () async { + // Given: An empty database + + // When + final result = await dao.getDocuments( + latestOnly: false, + limit: 100, + offset: 0, + ); + + // Then + expect(result, isEmpty); + }); + + test('returns all documents when no filters applied', () async { + // Given + final doc1 = _createTestDocumentEntity(id: 'id-1', ver: 'ver-1'); + final doc2 = _createTestDocumentEntity(id: 'id-2', ver: 'ver-1'); + await dao.saveAll([doc1, doc2]); + + // When + final result = await dao.getDocuments( + latestOnly: false, + limit: 100, + offset: 0, + ); + + // Then + expect(result.length, 2); + expect(result.map((e) => e.id), containsAll(['id-1', 'id-2'])); + }); + + test('filters by type', () async { + // Given + final proposal = _createTestDocumentEntity( + id: 'p-1', + type: DocumentType.proposalDocument, + ); + final template = _createTestDocumentEntity( + id: 't-1', + type: DocumentType.proposalTemplate, + ); + await dao.saveAll([proposal, template]); + + // When + final result = await dao.getDocuments( + type: DocumentType.proposalDocument, + latestOnly: false, + limit: 100, + offset: 0, + ); + + // Then + expect(result.length, 1); + expect(result.first.id, 'p-1'); + expect(result.first.type, DocumentType.proposalDocument); + }); + + test('filters by loose ref (returns all versions)', () async { + // Given + final v1 = _createTestDocumentEntity(id: 'doc-1', ver: 'v1'); + final v2 = _createTestDocumentEntity(id: 'doc-1', ver: 'v2'); + final other = _createTestDocumentEntity(id: 'doc-2', ver: 'v1'); + await dao.saveAll([v1, v2, other]); + + // When + final result = await dao.getDocuments( + ref: const SignedDocumentRef.loose(id: 'doc-1'), + latestOnly: false, + limit: 100, + offset: 0, + ); + + // Then + expect(result.length, 2); + expect(result.map((e) => e.ver), containsAll(['v1', 'v2'])); + }); + + test('filters by exact ref', () async { + // Given + final v1 = _createTestDocumentEntity(id: 'doc-1', ver: 'v1'); + final v2 = _createTestDocumentEntity(id: 'doc-1', ver: 'v2'); + await dao.saveAll([v1, v2]); + + // When + final result = await dao.getDocuments( + ref: const SignedDocumentRef.exact(id: 'doc-1', version: 'v1'), + latestOnly: false, + limit: 100, + offset: 0, + ); + + // Then + expect(result.length, 1); + expect(result.first.ver, 'v1'); + }); + + test('filters by refTo (loose)', () async { + // Given + final target = _createTestDocumentEntity(id: 'target-1'); + final ref1 = _createTestDocumentEntity( + id: 'ref-1', + refId: 'target-1', + refVer: 'any', + ); + final ref2 = _createTestDocumentEntity( + id: 'ref-2', + refId: 'target-1', + refVer: 'other', + ); + final other = _createTestDocumentEntity(id: 'other', refId: 'other-target'); + await dao.saveAll([target, ref1, ref2, other]); + + // When + final result = await dao.getDocuments( + referencing: const SignedDocumentRef.loose(id: 'target-1'), + latestOnly: false, + limit: 100, + offset: 0, + ); + + // Then + expect(result.length, 2); + expect(result.map((e) => e.id), containsAll(['ref-1', 'ref-2'])); + }); + + test('filters by refTo (exact)', () async { + // Given + final v1 = _createTestDocumentEntity( + id: 'ref-1', + refId: 'target', + refVer: 'v1', + ); + final v2 = _createTestDocumentEntity( + id: 'ref-2', + refId: 'target', + refVer: 'v2', + ); + await dao.saveAll([v1, v2]); + + // When + final result = await dao.getDocuments( + referencing: const SignedDocumentRef.exact(id: 'target', version: 'v1'), + latestOnly: false, + limit: 100, + offset: 0, + ); + + // Then + expect(result.length, 1); + expect(result.first.id, 'ref-1'); + }); + + test('returns only latest versions when latestOnly is true', () async { + // Given + final oldTime = DateTime.utc(2024, 1, 1); + final newTime = DateTime.utc(2024, 1, 2); + + final doc1V1 = _createTestDocumentEntity( + id: 'doc-1', + ver: _buildUuidV7At(oldTime), + ); + final doc1V2 = _createTestDocumentEntity( + id: 'doc-1', + ver: _buildUuidV7At(newTime), + ); + final doc2 = _createTestDocumentEntity( + id: 'doc-2', + ver: _buildUuidV7At(oldTime), + ); + + await dao.saveAll([doc1V1, doc1V2, doc2]); + + // When + final result = await dao.getDocuments( + latestOnly: true, + limit: 100, + offset: 0, + ); + + // Then + expect(result.length, 2); + expect(result.map((e) => e.id), containsAll(['doc-1', 'doc-2'])); + + final resultDoc1 = result.firstWhere((e) => e.id == 'doc-1'); + expect(resultDoc1.ver, doc1V2.doc.ver); + }); + + test('pagination works with limit and offset', () async { + // Given: 5 documents + final docs = List.generate( + 5, + (i) => _createTestDocumentEntity( + id: 'doc-$i', + ver: _buildUuidV7At(DateTime.utc(2024, 1, 1).add(Duration(minutes: i))), + ), + ); + + await dao.saveAll(docs); + + // When + final result = await dao.getDocuments( + latestOnly: false, + limit: 2, + offset: 1, + ); + + // Then + expect(result.length, 2); + }); + + test('respects campaign filters (categories)', () async { + // Given + final doc1 = _createTestDocumentEntity(id: 'd1', categoryId: 'cat-1'); + final doc2 = _createTestDocumentEntity(id: 'd2', categoryId: 'cat-2'); + final doc3 = _createTestDocumentEntity(id: 'd3', categoryId: 'cat-1'); + await dao.saveAll([doc1, doc2, doc3]); + + // When + final result = await dao.getDocuments( + filters: const CampaignFilters(categoriesIds: ['cat-1']), + latestOnly: false, + limit: 100, + offset: 0, + ); + + // Then + expect(result.length, 2); + expect(result.map((e) => e.id), containsAll(['d1', 'd3'])); + }); + + test('combines type, latestOnly and campaign filters', () async { + // Given + final oldProposal = _createTestDocumentEntity( + id: 'p1', + ver: _buildUuidV7At(DateTime(2023)), + type: DocumentType.proposalDocument, + categoryId: 'cat-A', + ); + final newProposal = _createTestDocumentEntity( + id: 'p1', + ver: _buildUuidV7At(DateTime(2024)), + type: DocumentType.proposalDocument, + categoryId: 'cat-A', + ); + final otherCatProposal = _createTestDocumentEntity( + id: 'p2', + ver: _buildUuidV7At(DateTime(2024)), + type: DocumentType.proposalDocument, + categoryId: 'cat-B', + ); + final wrongType = _createTestDocumentEntity( + id: 't1', + type: DocumentType.proposalTemplate, + categoryId: 'cat-A', + ); + + await dao.saveAll([oldProposal, newProposal, otherCatProposal, wrongType]); + + // When + final result = await dao.getDocuments( + type: DocumentType.proposalDocument, + filters: const CampaignFilters(categoriesIds: ['cat-A']), + latestOnly: true, + limit: 10, + offset: 0, + ); + + // Then + expect(result.length, 1); + expect(result.first.id, 'p1'); + expect(result.first.ver, newProposal.doc.ver); + }); + + test('results should be ordered by createdAt DESC (Newest First)', () async { + // Given: Three documents with distinct creation times + final oldestDate = DateTime.utc(2023, 1, 1); + final middleDate = DateTime.utc(2023, 6, 1); + final newestDate = DateTime.utc(2024, 1, 1); + + final oldestDoc = _createTestDocumentEntity( + id: 'doc-old', + ver: _buildUuidV7At(oldestDate), + ); + final middleDoc = _createTestDocumentEntity( + id: 'doc-mid', + ver: _buildUuidV7At(middleDate), + ); + final newestDoc = _createTestDocumentEntity( + id: 'doc-new', + ver: _buildUuidV7At(newestDate), + ); + + // When: Saved in SCRAMBLED order (Old -> New -> Middle) + await dao.save(oldestDoc); + await dao.save(newestDoc); + await dao.save(middleDoc); + + // And: We query with pagination + final result = await dao.getDocuments( + latestOnly: false, + limit: 10, + offset: 0, + ); + + // Then: We EXPECT them sorted by time (New -> Mid -> Old) + expect(result.length, 3); + + expect( + result[0].id, + 'doc-new', + reason: 'First item should be the newest document', + ); + + expect( + result[1].id, + 'doc-mid', + reason: 'Second item should be the middle document', + ); + + expect( + result[2].id, + 'doc-old', + reason: 'Last item should be the oldest document', + ); + }); + + test('pagination stays consistent across updates', () async { + // Given + final docs = List.generate( + 5, + (i) => _createTestDocumentEntity( + id: 'doc-$i', + ver: _buildUuidV7At(DateTime.utc(2024, 1, 1).add(Duration(minutes: i))), + ), + ); + + await dao.saveAll(docs); + + // When: We fetch Page 1 (size 2) + final page1 = await dao.getDocuments(latestOnly: false, limit: 2, offset: 0); + + // And: We insert a VERY OLD document (simulating a sync of historical data) + final ancientDoc = _createTestDocumentEntity( + id: 'doc-ancient', + ver: _buildUuidV7At(DateTime.utc(2020, 1, 1)), + ); + await dao.save(ancientDoc); + + // And + final page1Again = await dao.getDocuments(latestOnly: false, limit: 2, offset: 0); + + // Then + expect(page1Again[0].id, page1[0].id); + }); + }); + }); +} + +String _buildUuidV7At(DateTime dateTime) => DocumentRefFactory.uuidV7At(dateTime); + +CatalystId _createTestAuthor({ + String? name, + int role0KeySeed = 0, +}) { + final buffer = StringBuffer('id.catalyst://'); + final role0Key = Uint8List.fromList(List.filled(32, role0KeySeed)); + + if (name != null) { + buffer + ..write(name) + ..write('@'); + } + + buffer + ..write('preprod.cardano/') + ..write(base64UrlNoPadEncode(role0Key)); + + return CatalystId.parse(buffer.toString()); +} + +DocumentWithAuthorsEntity _createTestDocumentEntity({ + String? id, + String? ver, + Map contentData = const {}, + DocumentType type = DocumentType.proposalDocument, + String? authors, + String? categoryId, + String? categoryVer, + String? refId, + String? refVer, + String? replyId, + String? replyVer, + String? section, + String? templateId, + String? templateVer, +}) { + return DocumentWithAuthorsFactory.create( + id: id, + ver: ver, + contentData: contentData, + type: type, + authors: authors, + categoryId: categoryId, + categoryVer: categoryVer, + refId: refId, + refVer: refVer, + replyId: replyId, + replyVer: replyVer, section: section, templateId: templateId, templateVer: templateVer, ); - - final authorsEntities = authors - .split(',') - .where((element) => element.trim().isNotEmpty) - .map(CatalystId.tryParse) - .nonNulls - .map( - (e) => DocumentAuthorEntity( - documentId: docEntity.id, - documentVer: docEntity.ver, - authorId: e.toUri().toString(), - authorIdSignificant: e.toSignificant().toUri().toString(), - authorUsername: e.username, - ), - ) - .toList(); - - return DocumentWithAuthorsEntity(docEntity, authorsEntities); } diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/database/dao/documents_v2_local_metadata_dao_test.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/database/dao/documents_v2_local_metadata_dao_test.dart new file mode 100644 index 000000000000..6511469fdb58 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/database/dao/documents_v2_local_metadata_dao_test.dart @@ -0,0 +1,232 @@ +import 'package:catalyst_voices_repositories/src/database/catalyst_database.dart'; +import 'package:catalyst_voices_repositories/src/database/dao/documents_v2_local_metadata_dao.dart'; +import 'package:catalyst_voices_repositories/src/database/table/documents_local_metadata.drift.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../connection/test_connection.dart'; + +void main() { + group(DriftDocumentsV2LocalMetadataDao, () { + late DriftCatalystDatabase db; + late DocumentsV2LocalMetadataDao dao; + + setUp(() async { + final connection = await buildTestConnection(); + db = DriftCatalystDatabase(connection); + dao = db.driftDocumentsV2LocalMetadataDao; + }); + + tearDown(() async { + await db.close(); + }); + + group('deleteWhere', () { + test('returns zero when database is empty', () async { + // Given: An empty database + + // When + final result = await dao.deleteWhere(); + + // Then + expect(result, 0); + }); + + test('deletes single record and returns count', () async { + // Given + await db + .into(db.documentsLocalMetadata) + .insert( + DocumentsLocalMetadataCompanion.insert( + id: 'doc-1', + isFavorite: true, + ), + ); + + // When + final result = await dao.deleteWhere(); + + // Then + expect(result, 1); + }); + + test('deletes all records and returns total count', () async { + // Given + await db + .into(db.documentsLocalMetadata) + .insert( + DocumentsLocalMetadataCompanion.insert( + id: 'doc-1', + isFavorite: true, + ), + ); + await db + .into(db.documentsLocalMetadata) + .insert( + DocumentsLocalMetadataCompanion.insert( + id: 'doc-2', + isFavorite: false, + ), + ); + await db + .into(db.documentsLocalMetadata) + .insert( + DocumentsLocalMetadataCompanion.insert( + id: 'doc-3', + isFavorite: true, + ), + ); + + // When + final result = await dao.deleteWhere(); + + // Then + expect(result, 3); + }); + + test('table is empty after deletion', () async { + // Given + await db + .into(db.documentsLocalMetadata) + .insert( + DocumentsLocalMetadataCompanion.insert( + id: 'doc-1', + isFavorite: true, + ), + ); + await db + .into(db.documentsLocalMetadata) + .insert( + DocumentsLocalMetadataCompanion.insert( + id: 'doc-2', + isFavorite: true, + ), + ); + + // When + await dao.deleteWhere(); + + // Then + final remaining = await db.select(db.documentsLocalMetadata).get(); + expect(remaining, isEmpty); + }); + + test('subsequent delete returns zero', () async { + // Given + await db + .into(db.documentsLocalMetadata) + .insert( + DocumentsLocalMetadataCompanion.insert( + id: 'doc-1', + isFavorite: true, + ), + ); + await dao.deleteWhere(); + + // When + final result = await dao.deleteWhere(); + + // Then + expect(result, 0); + }); + }); + + group('isFavorite', () { + test('returns false when document does not exist', () async { + // Given: An empty database + + // When + final result = await dao.isFavorite('non-existent-id'); + + // Then + expect(result, false); + }); + + test('returns false when document exists but is not favorite', () async { + // Given + await db + .into(db.documentsLocalMetadata) + .insert( + DocumentsLocalMetadataCompanion.insert( + id: 'doc-1', + isFavorite: false, + ), + ); + + // When + final result = await dao.isFavorite('doc-1'); + + // Then + expect(result, false); + }); + + test('returns true when document is marked as favorite', () async { + // Given + await db + .into(db.documentsLocalMetadata) + .insert( + DocumentsLocalMetadataCompanion.insert( + id: 'doc-1', + isFavorite: true, + ), + ); + + // When + final result = await dao.isFavorite('doc-1'); + + // Then + expect(result, true); + }); + + test('returns correct value for specific document among multiple', () async { + // Given + await db + .into(db.documentsLocalMetadata) + .insert( + DocumentsLocalMetadataCompanion.insert( + id: 'doc-1', + isFavorite: true, + ), + ); + await db + .into(db.documentsLocalMetadata) + .insert( + DocumentsLocalMetadataCompanion.insert( + id: 'doc-2', + isFavorite: false, + ), + ); + await db + .into(db.documentsLocalMetadata) + .insert( + DocumentsLocalMetadataCompanion.insert( + id: 'doc-3', + isFavorite: true, + ), + ); + + // When & Then + expect(await dao.isFavorite('doc-1'), true); + expect(await dao.isFavorite('doc-2'), false); + expect(await dao.isFavorite('doc-3'), true); + }); + + test('returns false for non-existent id among existing records', () async { + // Given + await db + .into(db.documentsLocalMetadata) + .insert( + DocumentsLocalMetadataCompanion.insert( + id: 'doc-1', + isFavorite: true, + ), + ); + + // When + final result = await dao.isFavorite('doc-2'); + + // Then + expect(result, false); + }); + }); + }); +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/database/dao/drafts_dao_test.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/database/dao/drafts_dao_test.dart deleted file mode 100644 index d83cadc749df..000000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/database/dao/drafts_dao_test.dart +++ /dev/null @@ -1,439 +0,0 @@ -import 'package:catalyst_voices_dev/catalyst_voices_dev.dart'; -import 'package:catalyst_voices_models/catalyst_voices_models.dart'; -import 'package:catalyst_voices_repositories/catalyst_voices_repositories.dart'; -import 'package:catalyst_voices_repositories/src/database/catalyst_database.dart'; -import 'package:catalyst_voices_repositories/src/database/dao/drafts_dao.dart'; -import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; -import 'package:drift/drift.dart' show Uint8List; -import 'package:flutter_test/flutter_test.dart'; -import 'package:uuid_plus/uuid_plus.dart'; - -import '../connection/test_connection.dart'; -import '../drift_test_platforms.dart'; - -void main() { - late DriftCatalystDatabase database; - - setUp(() async { - final connection = await buildTestConnection(); - database = DriftCatalystDatabase(connection); - }); - - tearDown(() async { - await database.close(); - }); - - group(DriftDraftsDao, () { - group('query', () { - test( - 'returns specific version matching exact ref', - () async { - // Given - final drafts = List.generate( - 2, - (index) => DraftFactory.build(), - ); - final ref = DraftRef( - id: drafts.first.metadata.id, - version: drafts.first.metadata.version, - ); - - // When - await database.draftsDao.saveAll(drafts); - - // Then - final entity = await database.draftsDao.query(ref: ref); - - expect(entity, isNotNull); - - final id = UuidHiLo(high: entity!.idHi, low: entity.idLo); - final ver = UuidHiLo(high: entity.verHi, low: entity.verLo); - - expect(id.uuid, ref.id); - expect(ver.uuid, ref.version); - }, - onPlatform: driftOnPlatforms, - ); - - test( - 'returns newest version when ver is not specified', - () async { - // Given - final id = DocumentRefFactory.randomUuidV7(); - final firstVersionId = const Uuid().v7( - config: V7Options( - DateTime(2025, 2, 10).millisecondsSinceEpoch, - null, - ), - ); - final secondVersionId = const Uuid().v7( - config: V7Options( - DateTime(2025, 2, 11).millisecondsSinceEpoch, - null, - ), - ); - - final drafts = [ - DraftFactory.build( - metadata: DocumentDataMetadata( - type: DocumentType.proposalDocument, - selfRef: DraftRef(id: id, version: firstVersionId), - ), - ), - DraftFactory.build( - metadata: DocumentDataMetadata( - type: DocumentType.proposalDocument, - selfRef: DraftRef(id: id, version: secondVersionId), - ), - ), - ]; - final ref = DraftRef(id: drafts.first.metadata.id); - - // When - await database.draftsDao.saveAll(drafts); - - // Then - final entity = await database.draftsDao.query(ref: ref); - - expect(entity, isNotNull); - - expect(entity!.metadata.id, id); - expect(entity.metadata.version, secondVersionId); - }, - onPlatform: driftOnPlatforms, - ); - - test( - 'returns null when id does not match any id', - () async { - // Given - final drafts = List.generate( - 2, - (index) => DraftFactory.build(), - ); - final ref = DraftRef(id: DocumentRefFactory.randomUuidV7()); - - // When - await database.draftsDao.saveAll(drafts); - - // Then - final entity = await database.draftsDao.query(ref: ref); - - expect(entity, isNull); - }, - onPlatform: driftOnPlatforms, - ); - - test( - 'authors are correctly extracted', - () async { - final authorId1 = CatalystId(host: 'test', role0Key: Uint8List(32)); - final authorId2 = CatalystId(host: 'test1', role0Key: Uint8List(32)); - - final ref = DocumentRefFactory.draftRef(); - // Given - final draft = DraftFactory.build( - metadata: DocumentDataMetadata( - type: DocumentType.proposalDocument, - selfRef: ref, - authors: [ - authorId1, - authorId2, - ], - ), - ); - - await database.draftsDao.save(draft); - final doc = await database.draftsDao.query(ref: ref); - expect( - doc?.metadata.authors, - [ - authorId1, - authorId2, - ], - ); - }, - onPlatform: driftOnPlatforms, - ); - - test( - 'when updating proposal author list is not deleted', - () async { - final authorId1 = CatalystId(host: 'test', role0Key: Uint8List(32)); - final authorId2 = CatalystId(host: 'test1', role0Key: Uint8List(32)); - - final ref = DocumentRefFactory.draftRef(); - // Given - final draft = DraftFactory.build( - metadata: DocumentDataMetadata( - type: DocumentType.proposalDocument, - selfRef: ref, - authors: [ - authorId1, - authorId2, - ], - ), - content: const DocumentDataContent({ - 'title': 'Dev', - }), - ); - - final updateDraft = draft.copyWith( - metadata: draft.metadata.copyWith(), - content: const DocumentDataContent({ - 'title': 'Update', - }), - ); - - await database.draftsDao.save(draft); - await database.draftsDao.save(updateDraft); - - final updated = await database.draftsDao.query(ref: ref); - - expect( - updated?.metadata.authors?.length, - equals(2), - ); - expect( - updated?.metadata.authors, - equals([ - authorId1, - authorId2, - ]), - ); - }, - onPlatform: driftOnPlatforms, - ); - - test( - 'all drafts with from same account are returned ' - 'even when username changes', - () async { - // Given - final originalId = DummyCatalystIdFactory.create(username: 'damian'); - final updatedId = originalId.copyWith(username: const Optional('dev')); - - final draft1 = DraftFactory.build( - metadata: DocumentDataMetadata( - type: DocumentType.proposalDocument, - selfRef: DocumentRefFactory.signedDocumentRef(), - authors: [originalId], - ), - ); - final draft2 = DraftFactory.build( - metadata: DocumentDataMetadata( - type: DocumentType.proposalDocument, - selfRef: DocumentRefFactory.signedDocumentRef(), - authors: [updatedId], - ), - ); - - final drafts = [draft1, draft2]; - final refs = drafts.map((e) => e.metadata.selfRef).toList(); - - // When - await database.draftsDao.saveAll(drafts); - - // Then - final stream = database.draftsDao.watchAll(authorId: updatedId); - - expect( - stream, - emitsInOrder([ - allOf( - hasLength(drafts.length), - everyElement( - predicate((document) { - return refs.contains(document.metadata.selfRef); - }), - ), - ), - ]), - ); - }, - onPlatform: driftOnPlatforms, - ); - }); - - group('count', () { - test( - 'ref without ver includes all versions', - () async { - // Given - final id = DocumentRefFactory.randomUuidV7(); - final drafts = List.generate( - 2, - (index) { - return DraftFactory.build( - metadata: DocumentDataMetadata( - type: DocumentType.proposalDocument, - selfRef: DraftRef( - id: id, - version: DocumentRefFactory.randomUuidV7(), - ), - ), - ); - }, - ); - final ref = DraftRef(id: id); - - // When - await database.draftsDao.saveAll(drafts); - - // Then - final count = await database.draftsDao.count(ref: ref); - - expect(count, drafts.length); - }, - onPlatform: driftOnPlatforms, - ); - - test( - 'ref with ver includes only that version', - () async { - // Given - final id = DocumentRefFactory.randomUuidV7(); - final version = DocumentRefFactory.randomUuidV7(); - final drafts = [ - DraftFactory.build( - metadata: DocumentDataMetadata( - type: DocumentType.proposalDocument, - selfRef: DraftRef(id: id, version: version), - ), - ), - DraftFactory.build( - metadata: DocumentDataMetadata( - type: DocumentType.proposalDocument, - selfRef: DraftRef.first(id), - ), - ), - ]; - final ref = DraftRef(id: id, version: version); - - // When - await database.draftsDao.saveAll(drafts); - - // Then - final count = await database.draftsDao.count(ref: ref); - - expect(count, 1); - }, - onPlatform: driftOnPlatforms, - ); - - test( - 'returns 0 whe no matching drafts are found', - () async { - // Given - final drafts = [ - DraftFactory.build(), - DraftFactory.build(), - ]; - - final ref = DraftRef(id: DocumentRefFactory.randomUuidV7()); - - // When - await database.draftsDao.saveAll(drafts); - - // Then - final count = await database.draftsDao.count(ref: ref); - - expect(count, 0); - }, - onPlatform: driftOnPlatforms, - ); - }); - - group('update', () { - test( - 'replaces content correctly for exact ref', - () async { - // Given - final draft = DraftFactory.build(); - const updatedContent = DocumentDataContent({ - 'title': 'Dev final 2', - 'author': 'dev', - }); - final ref = DraftRef( - id: draft.metadata.id, - version: draft.metadata.version, - ); - - // When - await database.draftsDao.save(draft); - await database.draftsDao.updateContent(ref: ref, content: updatedContent); - - // Then - final entity = await database.draftsDao.query(ref: ref); - - expect(entity, isNotNull); - expect(entity?.content, updatedContent); - }, - onPlatform: driftOnPlatforms, - ); - - test( - 'replaces content for all matching ' - 'ids when ver is not specified', - () async { - // Given - final id = const Uuid().v7(); - final drafts = List.generate( - 5, - (index) => DraftFactory.build( - metadata: DocumentDataMetadata( - type: DocumentType.proposalDocument, - selfRef: DraftRef(id: id, version: const Uuid().v7()), - ), - ), - ); - const updatedContent = DocumentDataContent({ - 'title': 'Dev final 2', - 'author': 'dev', - }); - final ref = DraftRef(id: id); - - // When - await database.draftsDao.saveAll(drafts); - await database.draftsDao.updateContent(ref: ref, content: updatedContent); - - // Then - final entities = await database.draftsDao.queryAll(); - - expect(entities, hasLength(drafts.length)); - expect( - entities.every((element) => element.content == updatedContent), - isTrue, - ); - }, - onPlatform: driftOnPlatforms, - ); - }); - - group('delete', () { - test( - 'inserting and deleting a draft makes the table empty', - () async { - // Given - final ref = DocumentRefFactory.draftRef(); - - final draft = DraftFactory.build( - metadata: DocumentDataMetadata( - type: DocumentType.proposalDocument, - selfRef: ref, - ), - ); - - // When - await database.draftsDao.save(draft); - await database.draftsDao.deleteWhere(ref: ref); - - // Then - final entities = await database.draftsDao.queryAll(); - expect(entities, isEmpty); - }, - onPlatform: driftOnPlatforms, - ); - }); - }); -} diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/database/dao/proposals_dao_test.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/database/dao/proposals_dao_test.dart deleted file mode 100644 index 0314ea5c5fc7..000000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/database/dao/proposals_dao_test.dart +++ /dev/null @@ -1,2150 +0,0 @@ -import 'package:catalyst_cardano_serialization/catalyst_cardano_serialization.dart' show Coin; -import 'package:catalyst_voices_dev/catalyst_voices_dev.dart'; -import 'package:catalyst_voices_models/catalyst_voices_models.dart'; -import 'package:catalyst_voices_repositories/catalyst_voices_repositories.dart'; -import 'package:catalyst_voices_repositories/src/database/catalyst_database.dart'; -import 'package:catalyst_voices_repositories/src/database/dao/proposals_dao.dart'; -import 'package:catalyst_voices_repositories/src/dto/proposal/proposal_submission_action_dto.dart'; -import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; -import 'package:collection/collection.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:uuid_plus/uuid_plus.dart'; - -import '../connection/test_connection.dart'; -import '../drift_test_platforms.dart'; - -void main() { - late DriftCatalystDatabase database; - - // ignore: unnecessary_lambdas - setUpAll(() { - DummyCatalystIdFactory.registerDummyKeyFactory(); - }); - - setUp(() async { - final connection = await buildTestConnection(); - database = DriftCatalystDatabase(connection); - }); - - tearDown(() async { - await database.close(); - }); - - group(DriftProposalsDao, () { - group( - 'watchCount', - () { - test( - 'returns correct total number of ' - 'proposals for empty filters', - () async { - // Given - final proposals = [ - _buildProposal(), - _buildProposal(), - ]; - const filters = ProposalsCountFilters(); - - // When - await database.documentsDao.saveAll(proposals); - - // Then - final count = await database.proposalsDao - .watchCount( - filters: filters, - ) - .first; - - expect(count.total, proposals.length); - }, - onPlatform: driftOnPlatforms, - ); - - test( - 'when two versions of same proposal ' - 'exists there are counted as one', - () async { - // Given - final ref = DocumentRefFactory.signedDocumentRef(); - final proposals = [ - _buildProposal(selfRef: ref), - _buildProposal(selfRef: ref.nextVersion().toSignedDocumentRef()), - ]; - const filters = ProposalsCountFilters(); - - // When - await database.documentsDao.saveAll(proposals); - - // Then - final count = await database.proposalsDao - .watchCount( - filters: filters, - ) - .first; - - expect(count.total, 1); - }, - onPlatform: driftOnPlatforms, - ); - - test( - 'returns one final proposal if final submission is found', - () async { - // Given - final ref = DocumentRefFactory.signedDocumentRef(); - final proposals = [ - _buildProposal(selfRef: ref), - _buildProposal(), - ]; - final actions = [ - _buildProposalAction( - action: ProposalSubmissionActionDto.aFinal, - proposalRef: ref, - ), - ]; - const filters = ProposalsCountFilters(); - - // When - await database.documentsDao.saveAll([...proposals, ...actions]); - - // Then - final count = await database.proposalsDao - .watchCount( - filters: filters, - ) - .first; - - expect(count.finals, 1); - }, - onPlatform: driftOnPlatforms, - ); - - test( - 'returns one final proposal when final submission is ' - 'latest action but old draft action exists', - () async { - // Given - final ref = DocumentRefFactory.signedDocumentRef(); - final proposals = [ - _buildProposal(selfRef: ref), - ]; - final actions = [ - _buildProposalAction( - selfRef: _buildRefAt(DateTime(2025, 04)), - action: ProposalSubmissionActionDto.aFinal, - proposalRef: ref, - ), - _buildProposalAction( - selfRef: _buildRefAt(DateTime(2025, 04, 2)), - action: ProposalSubmissionActionDto.draft, - proposalRef: ref, - ), - _buildProposalAction( - selfRef: _buildRefAt(DateTime(2025, 04, 8)), - action: ProposalSubmissionActionDto.aFinal, - proposalRef: ref, - ), - ]; - const filters = ProposalsCountFilters(); - - // When - await database.documentsDao.saveAll([...proposals, ...actions]); - - // Then - final count = await database.proposalsDao - .watchCount( - filters: filters, - ) - .first; - - expect(count.finals, 1); - }, - onPlatform: driftOnPlatforms, - ); - - test( - 'returns zero final proposal if latest ' - 'submission is draft', - () async { - // Given - final ref = DocumentRefFactory.signedDocumentRef(); - final proposals = [ - _buildProposal(selfRef: ref), - ]; - final actions = [ - _buildProposalAction( - selfRef: _buildRefAt(DateTime(2025, 04, 7)), - action: ProposalSubmissionActionDto.aFinal, - proposalRef: ref, - ), - _buildProposalAction( - selfRef: _buildRefAt(DateTime(2025, 04, 8)), - action: ProposalSubmissionActionDto.draft, - proposalRef: ref, - ), - ]; - const filters = ProposalsCountFilters(); - - // When - await database.documentsDao.saveAll([...proposals, ...actions]); - - // Then - final count = await database.proposalsDao - .watchCount( - filters: filters, - ) - .first; - - expect(count.finals, 0); - }, - onPlatform: driftOnPlatforms, - ); - - test( - 'returns two final proposal when each have ' - 'complex action history', - () async { - // Given - final proposalOneRef = DocumentRefFactory.signedDocumentRef(); - final proposalTwoRef = DocumentRefFactory.signedDocumentRef(); - final proposals = [ - _buildProposal(selfRef: proposalOneRef), - _buildProposal(selfRef: proposalTwoRef), - ]; - final actions = [ - _buildProposalAction( - selfRef: _buildRefAt(DateTime(2025, 04)), - action: ProposalSubmissionActionDto.draft, - proposalRef: proposalOneRef, - ), - _buildProposalAction( - selfRef: _buildRefAt(DateTime(2025, 04, 2)), - action: ProposalSubmissionActionDto.aFinal, - proposalRef: proposalOneRef, - ), - _buildProposalAction( - selfRef: _buildRefAt(DateTime(2025, 04, 7)), - action: ProposalSubmissionActionDto.hide, - proposalRef: proposalTwoRef, - ), - _buildProposalAction( - selfRef: _buildRefAt(DateTime(2025, 04, 8)), - action: ProposalSubmissionActionDto.aFinal, - proposalRef: proposalTwoRef, - ), - ]; - const filters = ProposalsCountFilters(); - - // When - await database.documentsDao.saveAll([...proposals, ...actions]); - - // Then - final count = await database.proposalsDao - .watchCount( - filters: filters, - ) - .first; - - expect(count.finals, 2); - }, - onPlatform: driftOnPlatforms, - ); - - test( - 'returns calculated drafts and finals count', - () async { - // Given - final proposalOneRef = DocumentRefFactory.signedDocumentRef(); - final proposalTwoRef = DocumentRefFactory.signedDocumentRef(); - final proposals = [ - _buildProposal(selfRef: proposalOneRef), - _buildProposal(selfRef: proposalTwoRef), - ]; - final actions = [ - _buildProposalAction( - action: ProposalSubmissionActionDto.aFinal, - proposalRef: proposalOneRef, - ), - _buildProposalAction( - action: ProposalSubmissionActionDto.draft, - proposalRef: proposalTwoRef, - ), - ]; - const filters = ProposalsCountFilters(); - - // When - await database.documentsDao.saveAll([...proposals, ...actions]); - - // Then - final count = await database.proposalsDao - .watchCount( - filters: filters, - ) - .first; - - expect(count.total, 2); - expect(count.drafts, 1); - expect(count.finals, 1); - }, - onPlatform: driftOnPlatforms, - ); - - test( - 'returns correct favorites count', - () async { - // Given - final proposalOneRef = DocumentRefFactory.signedDocumentRef(); - final proposalTwoRef = DocumentRefFactory.signedDocumentRef(); - final proposals = [ - _buildProposal(selfRef: proposalOneRef), - _buildProposal(selfRef: proposalTwoRef), - ]; - final favorites = [ - _buildProposalFavorite(proposalRef: proposalOneRef), - ]; - - const filters = ProposalsCountFilters(); - - // When - await database.documentsDao.saveAll(proposals); - for (final fav in favorites) { - await database.favoritesDao.save(fav); - } - - // Then - final count = await database.proposalsDao - .watchCount( - filters: filters, - ) - .first; - - expect(count.total, 2); - expect(count.favorites, 1); - }, - onPlatform: driftOnPlatforms, - ); - - test( - 'returns correct my count base on author', - () async { - // Given - final userId = DummyCatalystIdFactory.create(username: 'damian'); - final proposalOneRef = DocumentRefFactory.signedDocumentRef(); - final proposalTwoRef = DocumentRefFactory.signedDocumentRef(); - final proposals = [ - _buildProposal(selfRef: proposalOneRef), - _buildProposal(selfRef: proposalTwoRef, author: userId), - ]; - - final filters = ProposalsCountFilters(author: userId); - - // When - await database.documentsDao.saveAll(proposals); - - // Then - final count = await database.proposalsDao - .watchCount( - filters: filters, - ) - .first; - - expect(count.total, 2); - expect(count.my, 1); - }, - onPlatform: driftOnPlatforms, - ); - - test( - 'returns correct count when only author filter is on', - () async { - // Given - final userId = DummyCatalystIdFactory.create(username: 'damian'); - final proposalOneRef = DocumentRefFactory.signedDocumentRef(); - final proposalTwoRef = DocumentRefFactory.signedDocumentRef(); - final proposals = [ - _buildProposal(selfRef: proposalOneRef), - _buildProposal(selfRef: proposalTwoRef, author: userId), - ]; - final favorites = [ - _buildProposalFavorite(proposalRef: proposalOneRef), - ]; - final actions = [ - _buildProposalAction( - action: ProposalSubmissionActionDto.aFinal, - proposalRef: proposalTwoRef, - ), - ]; - - final filters = ProposalsCountFilters( - author: userId, - onlyAuthor: true, - ); - const expectedCount = ProposalsCount( - total: 1, - finals: 1, - my: 1, - myFinals: 1, - ); - - // When - await database.documentsDao.saveAll([...proposals, ...actions]); - - for (final fav in favorites) { - await database.favoritesDao.save(fav); - } - - // Then - final count = await database.proposalsDao - .watchCount( - filters: filters, - ) - .first; - - expect(count, expectedCount); - }, - onPlatform: driftOnPlatforms, - ); - - test( - 'returns correct count when category filter is on', - () async { - // Given - final userId = DummyCatalystIdFactory.create(username: 'damian'); - final categoryId = _getCategoryId(); - - final proposalOneRef = DocumentRefFactory.signedDocumentRef(); - final proposalTwoRef = DocumentRefFactory.signedDocumentRef(); - final proposals = [ - _buildProposal( - selfRef: proposalOneRef, - categoryId: _getCategoryId(index: 1), - ), - _buildProposal( - selfRef: proposalTwoRef, - author: userId, - categoryId: categoryId, - ), - ]; - final favorites = [ - _buildProposalFavorite(proposalRef: proposalOneRef), - ]; - final actions = [ - _buildProposalAction( - action: ProposalSubmissionActionDto.aFinal, - proposalRef: proposalTwoRef, - ), - ]; - - final filters = ProposalsCountFilters(category: categoryId); - const expectedCount = ProposalsCount( - total: 1, - finals: 1, - ); - - // When - await database.documentsDao.saveAll([...proposals, ...actions]); - - for (final fav in favorites) { - await database.favoritesDao.save(fav); - } - - // Then - final count = await database.proposalsDao - .watchCount( - filters: filters, - ) - .first; - - expect(count, expectedCount); - }, - onPlatform: driftOnPlatforms, - ); - - test( - 'returns correct count when search query is not empty', - () async { - // Given - final proposals = [ - _buildProposal(), - _buildProposal(title: 'Explore'), - _buildProposal(title: 'Not this one'), - ]; - - /* cSpell:disable */ - const filters = ProposalsCountFilters(searchQuery: 'Expl'); - /* cSpell:enable */ - const expectedCount = ProposalsCount( - total: 1, - drafts: 1, - ); - - // When - await database.documentsDao.saveAll(proposals); - - // Then - final count = await database.proposalsDao - .watchCount( - filters: filters, - ) - .first; - - expect(count, expectedCount); - }, - onPlatform: driftOnPlatforms, - ); - - test( - 'search is looking up author name in catalystId', - () async { - // Given - const authorName = 'Damian'; - final search = authorName.substring(0, 2); - final userId = DummyCatalystIdFactory.create(username: authorName); - - final proposals = [ - _buildProposal(contentAuthorName: 'Unknown'), - _buildProposal(author: userId), - _buildProposal(contentAuthorName: 'Other'), - ]; - - final filters = ProposalsCountFilters(searchQuery: search); - const expectedCount = ProposalsCount( - total: 1, - drafts: 1, - ); - - // When - await database.documentsDao.saveAll(proposals); - - // Then - final count = await database.proposalsDao - .watchCount( - filters: filters, - ) - .first; - - expect(count, expectedCount); - }, - onPlatform: driftOnPlatforms, - ); - - test( - 'search is looking up author name in content', - () async { - // Given - const authorName = 'Damian'; - final search = authorName.substring(0, 2); - - final proposals = [ - _buildProposal(contentAuthorName: 'Unknown'), - _buildProposal(contentAuthorName: authorName), - _buildProposal(contentAuthorName: 'Other'), - ]; - - final filters = ProposalsCountFilters(searchQuery: search); - const expectedCount = ProposalsCount( - total: 1, - drafts: 1, - ); - - // When - await database.documentsDao.saveAll(proposals); - - // Then - final count = await database.proposalsDao - .watchCount( - filters: filters, - ) - .first; - - expect(count, expectedCount); - }, - onPlatform: driftOnPlatforms, - ); - - test( - 'returns correctly counted proposals', - () async { - // Given - final one = DocumentRefFactory.signedDocumentRef(); - final two = one.nextVersion().toSignedDocumentRef(); - final three = two.nextVersion().toSignedDocumentRef(); - - final proposals = [ - _buildProposal(selfRef: one), - _buildProposal(selfRef: two), - _buildProposal(selfRef: three), - ]; - final actions = [ - _buildProposalAction( - selfRef: DocumentRefFactory.signedDocumentRef(), - action: ProposalSubmissionActionDto.aFinal, - proposalRef: two, - ), - ]; - const filters = ProposalsCountFilters(); - const expectedCount = ProposalsCount( - total: 1, - finals: 1, - ); - - // When - await database.documentsDao.saveAll([...proposals, ...actions]); - - // Then - final count = await database.proposalsDao - .watchCount( - filters: filters, - ) - .first; - - expect(count, expectedCount); - }, - onPlatform: driftOnPlatforms, - ); - - test( - 'hidden proposals are excluded from count', - () async { - // Given - final one = DocumentRefFactory.signedDocumentRef(); - final two = DocumentRefFactory.signedDocumentRef(); - - final proposals = [ - _buildProposal(selfRef: one), - _buildProposal(selfRef: two), - ]; - final actions = [ - _buildProposalAction( - selfRef: DocumentRefFactory.signedDocumentRef(), - action: ProposalSubmissionActionDto.hide, - proposalRef: two, - ), - ]; - const filters = ProposalsCountFilters(); - const expectedCount = ProposalsCount( - total: 1, - drafts: 1, - ); - - // When - await database.documentsDao.saveAll([...proposals, ...actions]); - - // Then - final count = await database.proposalsDao - .watchCount( - filters: filters, - ) - .first; - - expect(count, expectedCount); - }, - onPlatform: driftOnPlatforms, - ); - }, - ); - - group('queryProposalsPage', () { - test( - 'only newest version of proposal is returned', - () async { - // Given - final templateRef = DocumentRefFactory.signedDocumentRef(); - - final ref = _buildRefAt(DateTime(2025, 4, 7)); - final nextRef = _buildRefAt(DateTime(2025, 4, 8)).copyWith(id: ref.id); - final latestRef = _buildRefAt(DateTime(2025, 4, 9)).copyWith(id: ref.id); - - final differentRef = _buildRefAt(DateTime(2025, 4, 12)); - - final templates = [ - _buildProposalTemplate(selfRef: templateRef), - ]; - - final proposals = [ - _buildProposal(selfRef: ref, template: templateRef), - _buildProposal(selfRef: nextRef, template: templateRef), - _buildProposal(selfRef: latestRef, template: templateRef), - _buildProposal(selfRef: differentRef, template: templateRef), - ]; - const request = PageRequest(page: 0, size: 10); - const filters = ProposalsFilters(); - const order = UpdateDate(isAscending: true); - - final expectedRefs = [ - latestRef, - differentRef, - ]; - - // When - await database.documentsDao.saveAll([...templates, ...proposals]); - - // Then - final page = await database.proposalsDao.queryProposalsPage( - request: request, - filters: filters, - order: order, - ); - - expect(page.items.length, 2); - expect(page.items.length, page.total); - - final proposalsRefs = page.items - .map((e) => e.proposal) - .map((entity) => entity.ref) - .toList(); - - expect( - proposalsRefs, - expectedRefs, - ); - }, - onPlatform: driftOnPlatforms, - ); - - test( - 'proposals are split into pages correctly', - () async { - // Given - final templateRef = DocumentRefFactory.signedDocumentRef(); - - final templates = [ - _buildProposalTemplate(selfRef: templateRef), - ]; - - final now = DateTime(2024, 4, 9); - final proposals = List.generate(45, (index) { - return _buildProposal( - selfRef: _buildRefAt(now.subtract(Duration(days: index))), - template: templateRef, - ); - }); - const filters = ProposalsFilters(); - const order = UpdateDate(isAscending: true); - - // When - await database.documentsDao.saveAll([...templates, ...proposals]); - - // Then - const firstRequest = PageRequest(page: 0, size: 25); - final pageZero = await database.proposalsDao.queryProposalsPage( - request: firstRequest, - filters: filters, - order: order, - ); - - expect(pageZero.page, 0); - expect(pageZero.total, proposals.length); - expect(pageZero.items.length, firstRequest.size); - - const secondRequest = PageRequest(page: 1, size: 25); - - final pageOne = await database.proposalsDao.queryProposalsPage( - request: secondRequest, - filters: filters, - order: order, - ); - - expect(pageOne.page, 1); - expect(pageOne.total, proposals.length); - expect( - pageOne.items.length, - proposals.length - pageZero.items.length, - ); - }, - onPlatform: driftOnPlatforms, - ); - - test( - 'proposals category filter works as expected', - () async { - // Given - final templateRef = DocumentRefFactory.signedDocumentRef(); - final categoryId = _getCategoryId(); - - final templates = [ - _buildProposalTemplate(selfRef: templateRef), - ]; - - final proposals = [ - _buildProposal( - selfRef: _buildRefAt(DateTime(2025, 4)), - template: templateRef, - categoryId: categoryId, - ), - _buildProposal( - selfRef: _buildRefAt(DateTime(2025, 4, 2)), - template: templateRef, - categoryId: categoryId, - ), - _buildProposal( - selfRef: _buildRefAt(DateTime(2025, 4, 3)), - template: templateRef, - categoryId: categoryId, - ), - _buildProposal( - template: templateRef, - categoryId: _getCategoryId(index: 1), - ), - ]; - - final expectedRefs = proposals - .sublist(0, 3) - .map((proposal) => proposal.document.ref) - .toList(); - - final filters = ProposalsFilters(category: categoryId); - const order = UpdateDate(isAscending: true); - - // When - await database.documentsDao.saveAll([...templates, ...proposals]); - - // Then - const request = PageRequest(page: 0, size: 25); - final page = await database.proposalsDao.queryProposalsPage( - request: request, - filters: filters, - order: order, - ); - - expect(page.page, 0); - expect(page.total, 3); - expect(page.items.map((e) => e.proposal.ref), expectedRefs); - }, - onPlatform: driftOnPlatforms, - ); - - test( - 'final proposals filter works as expected', - () async { - // Given - final templateRef = DocumentRefFactory.signedDocumentRef(); - - final templates = [ - _buildProposalTemplate(selfRef: templateRef), - ]; - - final proposalRef1 = _buildRefAt(DateTime(2025, 4)); - final proposalRef2 = _buildRefAt(DateTime(2025, 4, 2)); - final proposalRef3 = _buildRefAt(DateTime(2025, 4, 3)); - - final proposals = [ - _buildProposal( - selfRef: proposalRef1, - template: templateRef, - ), - _buildProposal( - selfRef: proposalRef2, - template: templateRef, - ), - _buildProposal( - selfRef: proposalRef3, - template: templateRef, - ), - _buildProposal(template: templateRef), - ]; - - final actions = [ - _buildProposalAction( - action: ProposalSubmissionActionDto.aFinal, - proposalRef: proposalRef1, - ), - _buildProposalAction( - action: ProposalSubmissionActionDto.aFinal, - proposalRef: proposalRef2, - ), - ]; - - final expectedRefs = [ - proposalRef1, - proposalRef2, - ]; - - const filters = ProposalsFilters(type: ProposalsFilterType.finals); - const order = UpdateDate(isAscending: true); - - // When - await database.documentsDao.saveAll([ - ...templates, - ...proposals, - ...actions, - ]); - - // Then - const request = PageRequest(page: 0, size: 25); - final page = await database.proposalsDao.queryProposalsPage( - request: request, - filters: filters, - order: order, - ); - - expect(page.page, 0); - expect(page.total, 2); - expect(page.items.map((e) => e.proposal.ref), expectedRefs); - }, - onPlatform: driftOnPlatforms, - ); - - test( - 'myFinals proposals filter works as expected', - () async { - // Given - final user1 = DummyCatalystIdFactory.create( - role0KeyBytes: base64UrlNoPadDecode('aovqiF+2wmrgcNDaPVsj1Z4Nwjmy9W0hS6jq3rRY5Mo='), - username: 'user1', - ); - final user2 = DummyCatalystIdFactory.create( - role0KeyBytes: base64UrlNoPadDecode('wHxjq8XGj5MwgRbCqi4tTY/5qvmBDk4ld1Z/AQ1chD8='), - username: 'user2', - ); - final templateRef = DocumentRefFactory.signedDocumentRef(); - - final templates = [ - _buildProposalTemplate(selfRef: templateRef), - ]; - - final proposalRef1 = _buildRefAt(DateTime(2025, 4)); - final proposalRef2 = _buildRefAt(DateTime(2025, 4, 2)); - final proposalRef3 = _buildRefAt(DateTime(2025, 4, 3)); - - final proposals = [ - _buildProposal( - selfRef: proposalRef1, - template: templateRef, - author: user1, - ), - _buildProposal( - selfRef: proposalRef2, - template: templateRef, - author: user2, - ), - _buildProposal( - selfRef: proposalRef3, - template: templateRef, - author: user2, - ), - _buildProposal(template: templateRef), - ]; - - final actions = [ - _buildProposalAction( - action: ProposalSubmissionActionDto.aFinal, - proposalRef: proposalRef1, - ), - _buildProposalAction( - action: ProposalSubmissionActionDto.aFinal, - proposalRef: proposalRef2, - ), - ]; - - final expectedRefs = [ - proposalRef1, - ]; - - final filters = ProposalsFilters( - type: ProposalsFilterType.myFinals, - author: user1, - ); - - // When - await database.documentsDao.saveAll([ - ...templates, - ...proposals, - ...actions, - ]); - - // Then - const request = PageRequest(page: 0, size: 25); - final page = await database.proposalsDao.queryProposalsPage( - request: request, - filters: filters, - order: const Alphabetical(), - ); - - expect(page.page, 0); - expect(page.total, 1); - expect(page.items.map((e) => e.proposal.ref), expectedRefs); - }, - onPlatform: driftOnPlatforms, - ); - - test( - 'final proposals is one with latest action as final', - () async { - // Given - final templateRef = DocumentRefFactory.signedDocumentRef(); - - final templates = [ - _buildProposalTemplate(selfRef: templateRef), - ]; - - final proposalRef1 = _buildRefAt(DateTime(2025, 4)); - final proposalRef2 = _buildRefAt(DateTime(2025, 4, 2)); - final proposalRef3 = _buildRefAt(DateTime(2025, 4, 3)); - - final proposals = [ - _buildProposal( - selfRef: proposalRef1, - template: templateRef, - ), - _buildProposal( - selfRef: proposalRef2, - template: templateRef, - ), - _buildProposal( - selfRef: proposalRef3, - template: templateRef, - ), - _buildProposal(template: templateRef), - ]; - - final actions = [ - _buildProposalAction( - selfRef: _buildRefAt(DateTime(2025, 4, 5)), - action: ProposalSubmissionActionDto.aFinal, - proposalRef: proposalRef1, - ), - _buildProposalAction( - selfRef: _buildRefAt(DateTime(2025, 4)), - action: ProposalSubmissionActionDto.draft, - proposalRef: proposalRef1, - ), - ]; - - final expectedRefs = [ - proposalRef1, - ]; - - const filters = ProposalsFilters(type: ProposalsFilterType.finals); - const order = UpdateDate(isAscending: true); - - // When - await database.documentsDao.saveAll([ - ...templates, - ...proposals, - ...actions, - ]); - - // Then - const request = PageRequest(page: 0, size: 25); - final page = await database.proposalsDao.queryProposalsPage( - request: request, - filters: filters, - order: order, - ); - - expect(page.page, 0); - expect(page.total, 1); - expect(page.items.map((e) => e.proposal.ref), expectedRefs); - }, - onPlatform: driftOnPlatforms, - ); - - test( - 'JoinedProposal is build correctly ', - () async { - // Given - final templateRef = DocumentRefFactory.signedDocumentRef(); - - final templates = [ - _buildProposalTemplate(selfRef: templateRef), - ]; - - final proposalRef1 = _buildRefAt(DateTime(2025, 4)); - final proposalRef2 = _buildRefAt(DateTime(2025, 4, 2)).copyWith(id: proposalRef1.id); - final proposalRef3 = _buildRefAt(DateTime(2025, 4, 3)).copyWith(id: proposalRef1.id); - - final proposals = [ - _buildProposal( - selfRef: proposalRef1, - template: templateRef, - ), - _buildProposal( - selfRef: proposalRef2, - template: templateRef, - ), - _buildProposal( - selfRef: proposalRef3, - template: templateRef, - ), - ]; - - final actions = [ - _buildProposalAction( - selfRef: _buildRefAt(DateTime(2025, 4, 5)), - action: ProposalSubmissionActionDto.aFinal, - proposalRef: proposalRef2, - ), - _buildProposalAction( - selfRef: _buildRefAt(DateTime(2025, 4)), - action: ProposalSubmissionActionDto.draft, - proposalRef: proposalRef1, - ), - ]; - - final comments = [ - _buildProposalComment(proposalRef: proposalRef1), - _buildProposalComment(proposalRef: proposalRef2), - _buildProposalComment(proposalRef: proposalRef2), - _buildProposalComment(proposalRef: proposalRef3), - ]; - - const filters = ProposalsFilters(); - const order = UpdateDate(isAscending: true); - - // When - await database.documentsDao.saveAll([ - ...templates, - ...proposals, - ...actions, - ...comments, - ]); - - // Then - const request = PageRequest(page: 0, size: 25); - final page = await database.proposalsDao.queryProposalsPage( - request: request, - filters: filters, - order: order, - ); - - expect(page.page, 0); - expect(page.total, 1); - - final joinedProposal = page.items.single; - - expect(joinedProposal.proposal, proposals[1].document); - expect(joinedProposal.template, templates[0].document); - expect(joinedProposal.action, actions[0].document); - expect(joinedProposal.commentsCount, 2); - expect( - joinedProposal.versions, - proposals.map((e) => e.document.ref.version).toList().reversed, - ); - }, - onPlatform: driftOnPlatforms, - ); - - test( - 'search query is looking up catalystId and proposal content ', - () async { - // Given - const authorName = 'Damian'; - final searchQuery = authorName.substring(0, 3); - - final templateRef = DocumentRefFactory.signedDocumentRef(); - - final templates = [ - _buildProposalTemplate(selfRef: templateRef), - ]; - - final proposals = [ - _buildProposal( - template: templateRef, - author: DummyCatalystIdFactory.create(username: authorName), - title: '11', - ), - _buildProposal( - template: templateRef, - contentAuthorName: authorName, - title: '22', - ), - _buildProposal( - template: templateRef, - contentAuthorName: 'Different one', - title: 'Test', - ), - ]; - - final expectedRefs = [ - proposals[0].document.metadata.selfRef, - proposals[1].document.metadata.selfRef, - ]; - - final actions = []; - final comments = []; - - final filters = ProposalsFilters(searchQuery: searchQuery); - const order = UpdateDate(isAscending: true); - - // When - await database.documentsDao.saveAll([ - ...templates, - ...proposals, - ...actions, - ...comments, - ]); - - // Then - const request = PageRequest(page: 0, size: 25); - final page = await database.proposalsDao.queryProposalsPage( - request: request, - filters: filters, - order: order, - ); - - expect(page.page, 0); - expect(page.total, 2); - - final refs = page.items.map((e) => e.proposal.metadata.selfRef).toList(); - - expect(refs, hasLength(expectedRefs.length)); - expect(refs, containsAll(expectedRefs)); - }, - onPlatform: driftOnPlatforms, - ); - - test( - 'hidden proposals are filtered out when pointing to older version', - () async { - // Given - final templateRef = DocumentRefFactory.signedDocumentRef(); - final proposalRef = DocumentRefFactory.signedDocumentRef(); - final nextProposalRef = proposalRef.nextVersion().toSignedDocumentRef(); - - final templates = [ - _buildProposalTemplate(selfRef: templateRef), - ]; - - final proposals = [ - _buildProposal( - selfRef: proposalRef, - template: templateRef, - ), - _buildProposal( - selfRef: nextProposalRef, - template: templateRef, - ), - ]; - - const expectedRefs = []; - - final actions = [ - _buildProposalAction( - selfRef: _buildRefAt(DateTime(2025, 5, 2)), - action: ProposalSubmissionActionDto.aFinal, - proposalRef: proposalRef, - ), - _buildProposalAction( - selfRef: _buildRefAt(DateTime(2025, 5, 20)), - action: ProposalSubmissionActionDto.hide, - proposalRef: proposalRef, - ), - ]; - final comments = []; - - const filters = ProposalsFilters(); - const order = UpdateDate(isAscending: true); - - // When - await database.documentsDao.saveAll([ - ...templates, - ...proposals, - ...actions, - ...comments, - ]); - - // Then - const request = PageRequest(page: 0, size: 25); - final page = await database.proposalsDao.queryProposalsPage( - request: request, - filters: filters, - order: order, - ); - - expect(page.page, 0); - expect(page.total, expectedRefs.length); - - final refs = page.items.map((e) => e.proposal.metadata.selfRef).toList(); - - expect(refs, hasLength(expectedRefs.length)); - expect(refs, containsAll(expectedRefs)); - }, - onPlatform: driftOnPlatforms, - ); - - test( - 'order alphabetical works against title', - () async { - // Given - final templateRef = DocumentRefFactory.signedDocumentRef(); - const titles = [ - 'Abc', - 'Bcd', - 'cde', - ]; - - final templates = [ - _buildProposalTemplate(selfRef: templateRef), - ]; - - final proposals = titles.map((title) { - return _buildProposal( - selfRef: DocumentRefFactory.signedDocumentRef(), - template: templateRef, - title: title, - ); - }).shuffled(); - - final actions = []; - final comments = []; - - const filters = ProposalsFilters(); - const order = Alphabetical(); - - // When - await database.documentsDao.saveAll([ - ...templates, - ...proposals, - ...actions, - ...comments, - ]); - - // Then - const request = PageRequest(page: 0, size: 25); - final page = await database.proposalsDao.queryProposalsPage( - request: request, - filters: filters, - order: order, - ); - - expect(page.page, 0); - - final proposalsTitles = page.items.map((e) => e.proposal.content.title).toList(); - - expect(proposalsTitles, containsAllInOrder(titles)); - }, - onPlatform: driftOnPlatforms, - ); - - test( - 'order budget asc works against content path', - () async { - // Given - final templateRef = DocumentRefFactory.signedDocumentRef(); - const budgets = [ - Coin.fromWholeAda(100000), - Coin.fromWholeAda(199999), - Coin.fromWholeAda(200000), - ]; - - final templates = [ - _buildProposalTemplate(selfRef: templateRef), - ]; - - final proposals = budgets.map((requestedFund) { - return _buildProposal( - selfRef: DocumentRefFactory.signedDocumentRef(), - template: templateRef, - requestedFunds: requestedFund, - ); - }).shuffled(); - - final actions = []; - final comments = []; - - const filters = ProposalsFilters(); - const order = Budget(isAscending: true); - - // When - await database.documentsDao.saveAll([ - ...templates, - ...proposals, - ...actions, - ...comments, - ]); - - // Then - const request = PageRequest(page: 0, size: 25); - final page = await database.proposalsDao.queryProposalsPage( - request: request, - filters: filters, - order: order, - ); - - expect(page.page, 0); - - final proposalsBudgets = page.items - .map((e) => e.proposal.content.requestedFunds) - .toList(); - - expect(proposalsBudgets, containsAllInOrder(budgets)); - }, - onPlatform: driftOnPlatforms, - ); - - test( - 'order budget desc works against content path', - () async { - // Given - final templateRef = DocumentRefFactory.signedDocumentRef(); - const budgets = [ - Coin.fromWholeAda(200000), - Coin.fromWholeAda(199999), - Coin.fromWholeAda(100000), - ]; - - final templates = [ - _buildProposalTemplate(selfRef: templateRef), - ]; - - final proposals = budgets.map((requestedFund) { - return _buildProposal( - selfRef: DocumentRefFactory.signedDocumentRef(), - template: templateRef, - requestedFunds: requestedFund, - ); - }).shuffled(); - - final actions = []; - final comments = []; - - const filters = ProposalsFilters(); - const order = Budget(isAscending: false); - - // When - await database.documentsDao.saveAll([ - ...templates, - ...proposals, - ...actions, - ...comments, - ]); - - // Then - const request = PageRequest(page: 0, size: 25); - final page = await database.proposalsDao.queryProposalsPage( - request: request, - filters: filters, - order: order, - ); - - expect(page.page, 0); - - final proposalsBudgets = page.items - .map((e) => e.proposal.content.requestedFunds) - .toList(); - - expect(proposalsBudgets, containsAllInOrder(budgets)); - }, - onPlatform: driftOnPlatforms, - ); - - test( - 'order updateDate asc works against content path', - () async { - // Given - final templateRef = DocumentRefFactory.signedDocumentRef(); - final dates = [ - DateTime.utc(2025, 5, 10), - DateTime.utc(2025, 5, 20), - DateTime.utc(2025, 5, 29), - ]; - - final templates = [ - _buildProposalTemplate(selfRef: templateRef), - ]; - - final proposals = dates.map((date) { - return _buildProposal( - selfRef: _buildRefAt(date), - template: templateRef, - ); - }).shuffled(); - - final actions = []; - final comments = []; - - const filters = ProposalsFilters(); - const order = UpdateDate(isAscending: true); - - // When - await database.documentsDao.saveAll([ - ...templates, - ...proposals, - ...actions, - ...comments, - ]); - - // Then - const request = PageRequest(page: 0, size: 25); - final page = await database.proposalsDao.queryProposalsPage( - request: request, - filters: filters, - order: order, - ); - - expect(page.page, 0); - - final proposalsDates = page.items - .map((e) => UuidHiLo(high: e.proposal.verHi, low: e.proposal.verLo).dateTime) - .toList(); - - expect(proposalsDates, containsAllInOrder(dates)); - }, - onPlatform: driftOnPlatforms, - ); - - test( - 'order updateDate desc works against content path', - () async { - // Given - final templateRef = DocumentRefFactory.signedDocumentRef(); - final dates = [ - DateTime.utc(2025, 5, 29), - DateTime.utc(2025, 5, 20), - DateTime.utc(2025, 5, 10), - ]; - - final templates = [ - _buildProposalTemplate(selfRef: templateRef), - ]; - - final proposals = dates.map((date) { - return _buildProposal( - selfRef: _buildRefAt(date), - template: templateRef, - ); - }).shuffled(); - - final actions = []; - final comments = []; - - const filters = ProposalsFilters(); - const order = UpdateDate(isAscending: false); - - // When - await database.documentsDao.saveAll([ - ...templates, - ...proposals, - ...actions, - ...comments, - ]); - - // Then - const request = PageRequest(page: 0, size: 25); - final page = await database.proposalsDao.queryProposalsPage( - request: request, - filters: filters, - order: order, - ); - - expect(page.page, 0); - - final proposalsDates = page.items - .map((e) => UuidHiLo(high: e.proposal.verHi, low: e.proposal.verLo).dateTime) - .toList(); - - expect(proposalsDates, containsAllInOrder(dates)); - }, - onPlatform: driftOnPlatforms, - ); - - test( - 'latest version value is one ordered against', - () async { - // Given - final templateRef = DocumentRefFactory.signedDocumentRef(); - final proposalRef = SignedDocumentRef.first(_buildUuidAt(DateTime.utc(2025, 5, 10))); - final latestProposalRef = proposalRef.copyWith( - version: Optional(_buildUuidAt(DateTime.utc(2025, 5, 29))), - ); - - const expectedBudgets = [ - Coin.fromWholeAda(30000), - Coin.fromWholeAda(2000), - ]; - final refsBudgets = { - proposalRef: const Coin.fromWholeAda(10000), - latestProposalRef: expectedBudgets[0], - DocumentRefFactory.signedDocumentRef(): expectedBudgets[1], - }; - - final templates = [ - _buildProposalTemplate(selfRef: templateRef), - ]; - - final proposals = refsBudgets.entries.map( - (entity) { - return _buildProposal( - selfRef: entity.key, - template: templateRef, - requestedFunds: entity.value, - ); - }, - ).shuffled(); - - final actions = []; - final comments = []; - - const filters = ProposalsFilters(); - const order = Budget(isAscending: false); - - // When - await database.documentsDao.saveAll([ - ...templates, - ...proposals, - ...actions, - ...comments, - ]); - - // Then - const request = PageRequest(page: 0, size: 25); - final page = await database.proposalsDao.queryProposalsPage( - request: request, - filters: filters, - order: order, - ); - - expect(page.page, 0); - - final proposalsBudgets = page.items - .map((e) => e.proposal.content.requestedFunds) - .toList(); - - expect(proposalsBudgets, containsAllInOrder(expectedBudgets)); - }, - onPlatform: driftOnPlatforms, - ); - }); - group('queryProposals', () { - test( - 'returns only newest version of each proposal', - () async { - // Given - final templateRef = DocumentRefFactory.signedDocumentRef(); - - final ref = _buildRefAt(DateTime(2025, 4, 7)); - final nextRef = _buildRefAt(DateTime(2025, 4, 8)).copyWith(id: ref.id); - final latestRef = _buildRefAt(DateTime(2025, 4, 9)).copyWith(id: ref.id); - - final differentRef = _buildRefAt(DateTime(2025, 4, 12)); - - final templates = [ - _buildProposalTemplate(selfRef: templateRef), - ]; - - final proposals = [ - _buildProposal(selfRef: ref, template: templateRef), - _buildProposal(selfRef: nextRef, template: templateRef), - _buildProposal(selfRef: latestRef, template: templateRef), - _buildProposal(selfRef: differentRef, template: templateRef), - ]; - - final expectedRefs = [ - latestRef, - differentRef, - ]; - - // When - await database.documentsDao.saveAll([...templates, ...proposals]); - - // Then - final result = await database.proposalsDao.queryProposals( - filters: const ProposalsFilters(), - ); - - expect(result.length, 2); - expect( - result.map((e) => e.proposal.ref), - expectedRefs, - ); - }, - onPlatform: driftOnPlatforms, - ); - - test( - 'filters by category when categoryRef is provided', - () async { - // Given - final templateRef = DocumentRefFactory.signedDocumentRef(); - - final templates = [ - _buildProposalTemplate(selfRef: templateRef), - ]; - - final proposals = [ - _buildProposal( - selfRef: _buildRefAt(DateTime(2025, 4)), - template: templateRef, - ), - _buildProposal( - selfRef: _buildRefAt(DateTime(2025, 4, 2)), - template: templateRef, - categoryId: _getCategoryId(index: 1), - ), - _buildProposal( - selfRef: _buildRefAt(DateTime(2025, 4, 3)), - template: templateRef, - categoryId: _getCategoryId(index: 1), - ), - ]; - - final expectedRefs = proposals - .where( - (p) => p.document.metadata.categoryId == _getCategoryId(index: 1), - ) - .map((proposal) => proposal.document.ref) - .toList(); - - // When - await database.documentsDao.saveAll([...templates, ...proposals]); - - // Then - final result = await database.proposalsDao.queryProposals( - categoryRef: _getCategoryId(index: 1), - filters: const ProposalsFilters(), - ); - - expect(result.length, 2); - expect( - result.map((e) => e.proposal.ref).toList(), - expectedRefs, - ); - }, - onPlatform: driftOnPlatforms, - ); - - test( - 'filters final proposals correctly', - () async { - // Given - final templateRef = DocumentRefFactory.signedDocumentRef(); - - final templates = [ - _buildProposalTemplate(selfRef: templateRef), - ]; - - final proposalRef1 = _buildRefAt(DateTime(2025, 4)); - final proposalRef2 = _buildRefAt(DateTime(2025, 4, 2)); - final proposalRef3 = _buildRefAt(DateTime(2025, 4, 3)); - - final proposals = [ - _buildProposal( - selfRef: proposalRef1, - template: templateRef, - ), - _buildProposal( - selfRef: proposalRef2, - template: templateRef, - ), - _buildProposal( - selfRef: proposalRef3, - template: templateRef, - ), - ]; - - final actions = [ - _buildProposalAction( - action: ProposalSubmissionActionDto.aFinal, - proposalRef: proposalRef1, - ), - _buildProposalAction( - action: ProposalSubmissionActionDto.aFinal, - proposalRef: proposalRef2, - ), - ]; - - final expectedRefs = [ - proposalRef1, - proposalRef2, - ]; - - // When - await database.documentsDao.saveAll([ - ...templates, - ...proposals, - ...actions, - ]); - - // Then - final result = await database.proposalsDao.queryProposals( - filters: const ProposalsFilters(type: ProposalsFilterType.finals), - ); - - expect(result.length, 2); - expect( - result.map((e) => e.proposal.ref), - expectedRefs, - ); - }, - onPlatform: driftOnPlatforms, - ); - - test( - 'returns correct JoinedProposal structure', - () async { - // Given - final templateRef = DocumentRefFactory.signedDocumentRef(); - - final templates = [ - _buildProposalTemplate(selfRef: templateRef), - ]; - - final baseTime = DateTime(2025, 4); - final proposalRef1 = _buildRefAt(baseTime); - final proposalRef2 = _buildRefAt( - baseTime.add(const Duration(days: 1)), - ).copyWith(id: proposalRef1.id); - final proposalRef3 = _buildRefAt( - baseTime.add(const Duration(days: 2)), - ).copyWith(id: proposalRef1.id); - - final proposals = [ - _buildProposal( - selfRef: proposalRef1, - template: templateRef, - ), - _buildProposal( - selfRef: proposalRef2, - template: templateRef, - ), - _buildProposal( - selfRef: proposalRef3, - template: templateRef, - ), - ]; - - final actionTime = baseTime.add(const Duration(days: 3)); - final actions = [ - _buildProposalAction( - selfRef: _buildRefAt(actionTime), - action: ProposalSubmissionActionDto.aFinal, - proposalRef: proposalRef2, - ), - ]; - - final comments = [ - _buildProposalComment(proposalRef: proposalRef1), - _buildProposalComment(proposalRef: proposalRef2), - _buildProposalComment(proposalRef: proposalRef2), - _buildProposalComment(proposalRef: proposalRef3), - ]; - - // When - await database.documentsDao.saveAll([ - ...templates, - ...proposals, - ...actions, - ...comments, - ]); - - // Then - final result = await database.proposalsDao.queryProposals( - filters: const ProposalsFilters(), - ); - - expect(result.length, 1); - - final joinedProposal = result.single; - - // Since there's a final action pointing to proposalRef2, - // that should be the effective proposal version - expect(joinedProposal.proposal, proposals[1].document); - expect(joinedProposal.template, templates[0].document); - expect(joinedProposal.action, actions[0].document); - expect( - joinedProposal.commentsCount, - 2, - ); - expect( - joinedProposal.versions, - proposals.map((e) => e.document.ref.version).toList().reversed, - ); - - expect(joinedProposal.proposal.ref, proposalRef2); - expect(joinedProposal.action?.metadata.ref, proposalRef2); - }, - onPlatform: driftOnPlatforms, - ); - - test( - 'order alphabetical works case-insensitively with null titles', - () async { - // Given - final templateRef = SignedDocumentRef.generateFirstRef(); - const titles = [ - 'ABC', - 'bcd', - null, - 'Xyz', - 'aabc', //cspell:disable-line - ]; - - final templates = [ - _buildProposalTemplate(selfRef: templateRef), - ]; - - final proposals = titles.map((title) { - return _buildProposal( - selfRef: SignedDocumentRef.generateFirstRef(), - template: templateRef, - title: title, - ); - }).shuffled(); - - final actions = []; - final comments = []; - - const filters = ProposalsFilters(); - const order = Alphabetical(); - - // When - await database.documentsDao.saveAll([ - ...templates, - ...proposals, - ...actions, - ...comments, - ]); - - // Then - const request = PageRequest(page: 0, size: 25); - final page = await database.proposalsDao.queryProposalsPage( - request: request, - filters: filters, - order: order, - ); - - expect(page.page, 0); - - final proposalsTitles = page.items.map((e) => e.proposal.content.title).toList(); - - final expectedOrder = [ - 'aabc', //cspell:disable-line - 'ABC', - 'bcd', - 'Xyz', - null, - ]; - - expect(proposalsTitles, containsAllInOrder(expectedOrder)); - }, - onPlatform: driftOnPlatforms, - ); - - test( - 'order alphabetical works case-insensitively', - () async { - // Given - final templateRef = SignedDocumentRef.generateFirstRef(); - const titles = [ - 'Bravo', - 'Lima', - 'Test', - 'alpha', - 'beta', - 'leet', - 'tango', - ]; - - final templates = [ - _buildProposalTemplate(selfRef: templateRef), - ]; - - final proposals = titles.map((title) { - return _buildProposal( - selfRef: SignedDocumentRef.generateFirstRef(), - template: templateRef, - title: title, - ); - }).shuffled(); - - final actions = []; - final comments = []; - - const filters = ProposalsFilters(); - const order = Alphabetical(); - - // When - await database.documentsDao.saveAll([ - ...templates, - ...proposals, - ...actions, - ...comments, - ]); - - // Then - const request = PageRequest(page: 0, size: 25); - final page = await database.proposalsDao.queryProposalsPage( - request: request, - filters: filters, - order: order, - ); - - expect(page.page, 0); - - final proposalsTitles = page.items.map((e) => e.proposal.content.title).toList(); - - final expectedOrder = [ - 'alpha', - 'beta', - 'Bravo', - 'leet', - 'Lima', - 'tango', - 'Test', - ]; - - expect(proposalsTitles, containsAllInOrder(expectedOrder)); - }, - onPlatform: driftOnPlatforms, - ); - }); - }); -} - -final _dummyCategoriesCache = {}; - -DocumentEntityWithMetadata _buildProposal({ - SignedDocumentRef? selfRef, - SignedDocumentRef? template, - String? title, - CatalystId? author, - String? contentAuthorName, - SignedDocumentRef? categoryId, - Coin? requestedFunds, -}) { - final metadata = DocumentDataMetadata( - type: DocumentType.proposalDocument, - selfRef: selfRef ?? DocumentRefFactory.signedDocumentRef(), - template: template ?? DocumentRefFactory.signedDocumentRef(), - authors: [ - if (author != null) author, - ], - categoryId: categoryId ?? _getCategoryId(), - ); - final content = DocumentDataContent({ - if (title != null || contentAuthorName != null) - 'setup': { - if (contentAuthorName != null) - 'proposer': { - 'applicant': contentAuthorName, - }, - if (title != null) - 'title': { - 'title': title, - }, - }, - if (requestedFunds != null) - 'summary': { - 'budget': { - 'requestedFunds': requestedFunds.ada.toInt(), - }, - }, - }); - - final document = DocumentFactory.build( - content: content, - metadata: metadata, - ); - - final metadataEntities = [ - if (title != null) - DocumentMetadataFactory.build( - ver: metadata.selfRef.version, - fieldKey: DocumentMetadataFieldKey.title, - fieldValue: title, - ), - ]; - - return (document: document, metadata: metadataEntities); -} - -DocumentEntityWithMetadata _buildProposalAction({ - DocumentRef? selfRef, - required ProposalSubmissionActionDto action, - required DocumentRef proposalRef, -}) { - final metadata = DocumentDataMetadata( - type: DocumentType.proposalActionDocument, - selfRef: selfRef ?? DocumentRefFactory.signedDocumentRef(), - ref: proposalRef, - ); - final dto = ProposalSubmissionActionDocumentDto(action: action); - final content = DocumentDataContent(dto.toJson()); - - final document = DocumentFactory.build( - content: content, - metadata: metadata, - ); - - const metadataEntities = []; - - return (document: document, metadata: metadataEntities); -} - -DocumentEntityWithMetadata _buildProposalComment({ - SignedDocumentRef? selfRef, - required DocumentRef proposalRef, -}) { - final metadata = DocumentDataMetadata( - type: DocumentType.commentDocument, - selfRef: selfRef ?? DocumentRefFactory.signedDocumentRef(), - ref: proposalRef, - ); - const content = DocumentDataContent({}); - - final document = DocumentFactory.build( - content: content, - metadata: metadata, - ); - - final metadataEntities = []; - - return (document: document, metadata: metadataEntities); -} - -DocumentFavoriteEntity _buildProposalFavorite({ - required DocumentRef proposalRef, -}) { - final hiLo = UuidHiLo.from(proposalRef.id); - return DocumentFavoriteEntity( - idHi: hiLo.high, - idLo: hiLo.low, - isFavorite: true, - type: DocumentType.proposalDocument, - ); -} - -DocumentEntityWithMetadata _buildProposalTemplate({ - SignedDocumentRef? selfRef, -}) { - final metadata = DocumentDataMetadata( - type: DocumentType.proposalTemplate, - selfRef: selfRef ?? DocumentRefFactory.signedDocumentRef(), - ); - const content = DocumentDataContent({}); - - final document = DocumentFactory.build( - content: content, - metadata: metadata, - ); - - final metadataEntities = []; - - return (document: document, metadata: metadataEntities); -} - -SignedDocumentRef _buildRefAt(DateTime dateTime) { - return SignedDocumentRef.first(_buildUuidAt(dateTime)); -} - -String _buildUuidAt(DateTime dateTime) { - final config = V7Options(dateTime.millisecondsSinceEpoch, null); - return const Uuid().v7(config: config); -} - -SignedDocumentRef _getCategoryId({ - int index = 0, -}) { - return activeConstantDocumentRefs.elementAtOrNull(index)?.category ?? - _dummyCategoriesCache.putIfAbsent(index, DocumentRefFactory.signedDocumentRef); -} - -extension on DocumentEntity { - SignedDocumentRef get ref { - return SignedDocumentRef( - id: UuidHiLo(high: idHi, low: idLo).uuid, - version: UuidHiLo(high: verHi, low: verLo).uuid, - ); - } -} diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/database/dao/proposals_v2_dao_test.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/database/dao/proposals_v2_dao_test.dart index 71a07956a167..df405a577eb7 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/database/dao/proposals_v2_dao_test.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/database/dao/proposals_v2_dao_test.dart @@ -6,16 +6,14 @@ import 'package:catalyst_voices_models/catalyst_voices_models.dart'; import 'package:catalyst_voices_repositories/src/database/catalyst_database.dart'; import 'package:catalyst_voices_repositories/src/database/dao/proposals_v2_dao.dart'; import 'package:catalyst_voices_repositories/src/database/model/document_with_authors_entity.dart'; -import 'package:catalyst_voices_repositories/src/database/table/document_authors.drift.dart'; import 'package:catalyst_voices_repositories/src/database/table/documents_local_metadata.drift.dart'; -import 'package:catalyst_voices_repositories/src/database/table/documents_v2.drift.dart'; import 'package:catalyst_voices_repositories/src/dto/proposal/proposal_submission_action_dto.dart'; import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; import 'package:drift/drift.dart' hide isNull, isNotNull; import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:uuid_plus/uuid_plus.dart'; +import '../../utils/document_with_authors_factory.dart'; import '../connection/test_connection.dart'; void main() { @@ -4979,11 +4977,7 @@ void main() { }); } -String _buildUuidV7At(DateTime dateTime) { - final ts = dateTime.millisecondsSinceEpoch; - final rand = Uint8List.fromList([42, 0, 0, 0, 0, 0, 0, 0, 0, 0]); - return const UuidV7().generate(options: V7Options(ts, rand)); -} +String _buildUuidV7At(DateTime dateTime) => DocumentRefFactory.uuidV7At(dateTime); CatalystId _createTestAuthor({ String? name, @@ -5033,16 +5027,12 @@ DocumentWithAuthorsEntity _createTestDocumentEntity({ String? templateId, String? templateVer, }) { - id ??= DocumentRefFactory.randomUuidV7(); - ver ??= id; - authors ??= ''; - - final docEntity = DocumentEntityV2( + return DocumentWithAuthorsFactory.create( id: id, ver: ver, - content: DocumentDataContent(contentData), - createdAt: createdAt ?? ver.tryDateTime ?? DateTime.now(), + contentData: contentData, type: type, + createdAt: createdAt, authors: authors, categoryId: categoryId, categoryVer: categoryVer, @@ -5054,24 +5044,6 @@ DocumentWithAuthorsEntity _createTestDocumentEntity({ templateId: templateId, templateVer: templateVer, ); - - final authorsEntities = authors - .split(',') - .where((element) => element.trim().isNotEmpty) - .map(CatalystId.tryParse) - .nonNulls - .map( - (e) => DocumentAuthorEntity( - documentId: docEntity.id, - documentVer: docEntity.ver, - authorId: e.toUri().toString(), - authorIdSignificant: e.toSignificant().toUri().toString(), - authorUsername: e.username, - ), - ) - .toList(); - - return DocumentWithAuthorsEntity(docEntity, authorsEntities); } int _seedRole0KeySeedGetter(String name) => 0; diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/database/query/jsonb_expressions_test.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/database/query/jsonb_expressions_test.dart deleted file mode 100644 index 2144f819ba96..000000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/database/query/jsonb_expressions_test.dart +++ /dev/null @@ -1,55 +0,0 @@ -import 'package:catalyst_voices_models/catalyst_voices_models.dart'; -import 'package:catalyst_voices_repositories/src/database/query/jsonb_expressions.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - group(JsonBExpressions, () { - test('generateSqlForJsonQuery handles simple paths correctly', () { - const handler = NodeId('user.name'); - final sql = JsonBExpressions.generateSqlForJsonQuery( - jsonContent: 'content', - nodeId: handler, - searchValue: 'John', - ); - - expect(sql, contains(r"json_extract(content, '$.user.name')")); - expect(sql, contains("LIKE '%John%'")); - }); - - test('generateSqlForJsonQuery handles exact match correctly', () { - const handler = NodeId('user.id'); - final sql = JsonBExpressions.generateSqlForJsonQuery( - jsonContent: 'content', - nodeId: handler, - searchValue: '123', - useExactMatch: true, - ); - - expect(sql, contains(r"json_extract(content, '$.user.id') = '123'")); - }); - - test('generateSqlForJsonQuery handles wildcard array correctly', () { - const handler = NodeId('items.*'); - final sql = JsonBExpressions.generateSqlForJsonQuery( - jsonContent: 'content', - nodeId: handler, - searchValue: 'test', - ); - - expect(sql, contains(r"SELECT 1 FROM json_tree(content, '$.items')")); - expect(sql, contains("WHERE json_tree.value LIKE '%test%'")); - }); - - test('generateSqlForJsonQuery handles wildcard with field correctly', () { - const handler = NodeId('items.*.name'); - final sql = JsonBExpressions.generateSqlForJsonQuery( - jsonContent: 'content', - nodeId: handler, - searchValue: 'test', - ); - - expect(sql, contains(r"SELECT 1 FROM json_each(json_extract(content, '$.items'))")); - expect(sql, contains(r"WHERE json_extract(value, '$.name') LIKE '%test%'")); - }); - }); -} diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/document/document_repository_test.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/document/document_repository_test.dart index 104c71096fcb..7f71f4ec7b32 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/document/document_repository_test.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/document/document_repository_test.dart @@ -23,7 +23,6 @@ void main() { late DraftDataSource draftsSource; late SignedDocumentDataSource localDocuments; late DocumentDataRemoteSource remoteDocuments; - late DocumentFavoriteSource favoriteDocuments; setUp(() async { final connection = await buildTestConnection(); @@ -32,14 +31,12 @@ void main() { draftsSource = DatabaseDraftsDataSource(database); localDocuments = DatabaseDocumentsDataSource(database, const CatalystProfiler.noop()); remoteDocuments = _MockDocumentDataRemoteSource(); - favoriteDocuments = DatabaseDocumentFavoriteSource(database); repository = DocumentRepositoryImpl( database, draftsSource, localDocuments, remoteDocuments, - favoriteDocuments, ); }); @@ -69,10 +66,10 @@ void main() { ); when( - () => remoteDocuments.get(ref: template.ref), + () => remoteDocuments.get(template.ref), ).thenAnswer((_) => Future.value(template)); when( - () => remoteDocuments.get(ref: proposal.ref), + () => remoteDocuments.get(proposal.ref), ).thenAnswer((_) => Future.value(proposal)); // When @@ -100,10 +97,10 @@ void main() { template: templateRef, ); - when(() => remoteDocuments.get(ref: templateRef)).thenAnswer( + when(() => remoteDocuments.get(templateRef)).thenAnswer( (_) => Future.error(DocumentNotFoundException(ref: templateRef)), ); - when(() => remoteDocuments.get(ref: proposal.ref)).thenAnswer( + when(() => remoteDocuments.get(proposal.ref)).thenAnswer( (_) => Future.error(DocumentNotFoundException(ref: templateRef)), ); @@ -136,14 +133,14 @@ void main() { final ref = documentData.ref; - when(() => remoteDocuments.get(ref: ref)).thenAnswer((_) => Future.value(documentData)); + when(() => remoteDocuments.get(ref)).thenAnswer((_) => Future.value(documentData)); // When await repository.getDocumentData(ref: ref); await repository.getDocumentData(ref: ref); // Then - verify(() => remoteDocuments.get(ref: ref)).called(1); + verify(() => remoteDocuments.get(ref)).called(1); }, onPlatform: driftOnPlatforms, ); @@ -165,7 +162,7 @@ void main() { when(() => remoteDocuments.getLatestVersion(id)).thenAnswer((_) => Future.value(version)); when( - () => remoteDocuments.get(ref: exactRef), + () => remoteDocuments.get(exactRef), ).thenAnswer((_) => Future.value(documentData)); // When @@ -173,7 +170,7 @@ void main() { // Then verify(() => remoteDocuments.getLatestVersion(id)).called(1); - verify(() => remoteDocuments.get(ref: exactRef)).called(1); + verify(() => remoteDocuments.get(exactRef)).called(1); }, onPlatform: driftOnPlatforms, ); @@ -192,10 +189,10 @@ void main() { final proposal = DocumentDataFactory.build(template: templateRef); when( - () => remoteDocuments.get(ref: template.ref), + () => remoteDocuments.get(template.ref), ).thenAnswer((_) => Future.value(template)); when( - () => remoteDocuments.get(ref: proposal.ref), + () => remoteDocuments.get(proposal.ref), ).thenAnswer((_) => Future.value(proposal)); // When @@ -236,13 +233,13 @@ void main() { final proposal2 = DocumentDataFactory.build(template: templateRef); when( - () => remoteDocuments.get(ref: template.ref), + () => remoteDocuments.get(template.ref), ).thenAnswer((_) => Future.value(template)); when( - () => remoteDocuments.get(ref: proposal1.ref), + () => remoteDocuments.get(proposal1.ref), ).thenAnswer((_) => Future.value(proposal1)); when( - () => remoteDocuments.get(ref: proposal2.ref), + () => remoteDocuments.get(proposal2.ref), ).thenAnswer((_) => Future.value(proposal2)); // When @@ -265,7 +262,7 @@ void main() { expect(proposals[0]!.data.ref, proposal1.ref); expect(proposals[1]!.data.ref, proposal2.ref); - verify(() => remoteDocuments.get(ref: template.ref)).called(1); + verify(() => remoteDocuments.get(template.ref)).called(1); }, onPlatform: driftOnPlatforms, ); diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/utils/document_with_authors_factory.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/utils/document_with_authors_factory.dart new file mode 100644 index 000000000000..1ca52200443a --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/utils/document_with_authors_factory.dart @@ -0,0 +1,68 @@ +import 'package:catalyst_voices_dev/catalyst_voices_dev.dart'; +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_repositories/src/database/model/document_with_authors_entity.dart'; +import 'package:catalyst_voices_repositories/src/database/table/document_authors.drift.dart'; +import 'package:catalyst_voices_repositories/src/database/table/documents_v2.drift.dart'; +import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; + +final class DocumentWithAuthorsFactory { + DocumentWithAuthorsFactory._(); + + static DocumentWithAuthorsEntity create({ + String? id, + String? ver, + Map contentData = const {}, + DocumentType type = DocumentType.proposalDocument, + DateTime? createdAt, + String? authors, + String? categoryId, + String? categoryVer, + String? refId, + String? refVer, + String? replyId, + String? replyVer, + String? section, + String? templateId, + String? templateVer, + }) { + id ??= DocumentRefFactory.randomUuidV7(); + ver ??= id; + authors ??= ''; + + final docEntity = DocumentEntityV2( + id: id, + ver: ver, + content: DocumentDataContent(contentData), + createdAt: createdAt ?? ver.tryDateTime ?? DateTime.now(), + type: type, + authors: authors, + categoryId: categoryId, + categoryVer: categoryVer, + refId: refId, + refVer: refVer, + replyId: replyId, + replyVer: replyVer, + section: section, + templateId: templateId, + templateVer: templateVer, + ); + + final authorsEntities = authors + .split(',') + .where((element) => element.trim().isNotEmpty) + .map(CatalystId.tryParse) + .nonNulls + .map( + (e) => DocumentAuthorEntity( + documentId: docEntity.id, + documentVer: docEntity.ver, + authorId: e.toUri().toString(), + authorIdSignificant: e.toSignificant().toUri().toString(), + authorUsername: e.username, + ), + ) + .toList(); + + return DocumentWithAuthorsEntity(docEntity, authorsEntities); + } +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/documents/documents_service.dart b/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/documents/documents_service.dart index 95408a343777..68aea347a710 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/documents/documents_service.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/documents/documents_service.dart @@ -21,6 +21,8 @@ abstract interface class DocumentsService { /// if [keepLocalDrafts] is true local drafts and their templates will be kept. Future clear({bool keepLocalDrafts}); + Future isFavorite(DocumentRef ref); + /// Returns all matching [DocumentData] for given [ref]. Future> lookup(DocumentRef ref); @@ -56,9 +58,14 @@ final class DocumentsServiceImpl implements DocumentsService { return _documentRepository.removeAll(keepLocalDrafts: keepLocalDrafts); } + @override + Future isFavorite(DocumentRef ref) { + return _documentRepository.isFavorite(ref); + } + @override Future> lookup(DocumentRef ref) { - return _documentRepository.getAllDocumentsData(ref: ref); + return _documentRepository.findAllVersions(ref: ref); } @override diff --git a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/proposal/proposal_service.dart b/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/proposal/proposal_service.dart index bc5f71efd2a5..0c48b9e96726 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/proposal/proposal_service.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/proposal/proposal_service.dart @@ -50,9 +50,6 @@ abstract interface class ProposalService { required SignedDocumentRef categoryId, }); - /// Similar to [watchFavoritesProposalsIds] stops after first emit. - Future> getFavoritesProposalsIds(); - Future getLatestProposalVersion({required DocumentRef ref}); Future getProposal({ @@ -63,12 +60,6 @@ abstract interface class ProposalService { required DocumentRef ref, }); - Future> getProposalsPage({ - required PageRequest request, - required ProposalsFilters filters, - required ProposalsOrder order, - }); - Future getProposalTemplate({ required DocumentRef ref, }); @@ -121,14 +112,6 @@ abstract interface class ProposalService { required SignedDocumentRef categoryId, }); - /// Fetches favorites proposals ids of the user - Stream> watchFavoritesProposalsIds(); - - /// Emits when proposal fav status changes. - Stream watchIsFavoritesProposal({ - required DocumentRef ref, - }); - /// Streams changes to [isMaxProposalsLimitReached]. Stream watchMaxProposalsLimitReached(); @@ -138,23 +121,11 @@ abstract interface class ProposalService { ProposalsFiltersV2 filters, }); - Stream watchProposalsCount({ - required ProposalsCountFilters filters, - }); - Stream watchProposalsCountV2({ ProposalsFiltersV2 filters, }); - Stream> watchProposalsPage({ - required PageRequest request, - required ProposalsFilters filters, - required ProposalsOrder order, - }); - Stream> watchUserProposals(); - - Stream watchUserProposalsCount(); } final class ProposalServiceImpl implements ProposalService { @@ -242,13 +213,6 @@ final class ProposalServiceImpl implements ProposalService { ); } - @override - Future> getFavoritesProposalsIds() { - return _documentRepository - .watchAllDocumentsFavoriteIds(type: DocumentType.proposalDocument) - .first; - } - @override Future getLatestProposalVersion({required DocumentRef ref}) async { final latest = await _documentRepository.getLatestOf(ref: ref); @@ -282,61 +246,6 @@ final class ProposalServiceImpl implements ProposalService { ); } - @override - Future> getProposalsPage({ - required PageRequest request, - required ProposalsFilters filters, - required ProposalsOrder order, - }) async { - // TODO(LynxxLynx): Only for mocking! Remove this when we start supporting writing votes to the database - final originalFilters = filters; - if (filters.type == ProposalsFilterType.voted) { - filters = filters.copyWith(type: ProposalsFilterType.total); - } - - final proposalsPage = await _proposalRepository - .getProposalsPage(request: request, filters: filters, order: order) - .then(_mapProposalDataPage); - var proposals = proposalsPage.items; - - // TODO(LynxxLynx): Only for mocking! Remove this when we start supporting writing votes to the database - if (originalFilters.type == ProposalsFilterType.voted) { - final votedProposals = _castedVotesObserver.votes; - final votedProposalIds = votedProposals.map((vote) => vote.proposal).toSet(); - proposals = proposals - .where((proposal) => votedProposalIds.contains(proposal.selfRef)) - .toList(); - } - - final categoriesRefs = proposals.map((proposal) => proposal.categoryRef).toSet(); - - // If we are getting proposals then campaign needs to be active - // Getting whole campaign with list of categories saves time then calling to get each category separately - // for each proposal - final activeCampaign = _activeCampaignObserver.campaign; - - final categories = Map.fromEntries( - categoriesRefs.map((ref) { - final category = activeCampaign!.categories.firstWhere( - (category) => category.selfRef == ref, - ); - return MapEntry(ref.id, category); - }), - ); - - final proposalsWithContext = proposals - .map( - (proposal) => ProposalWithContext( - proposal: proposal, - category: categories[proposal.categoryRef.id]!, - user: const ProposalUserContext(), - ), - ) - .toList(); - - return proposalsPage.copyWithItems(proposalsWithContext); - } - @override Future getProposalTemplate({ required DocumentRef ref, @@ -492,23 +401,38 @@ final class ProposalServiceImpl implements ProposalService { ); } - @override - Stream> watchFavoritesProposalsIds() { - return _documentRepository.watchAllDocumentsFavoriteIds( - type: DocumentType.proposalDocument, - ); - } - - @override - Stream watchIsFavoritesProposal({required DocumentRef ref}) { - return _documentRepository.watchIsDocumentFavorite(ref: ref.toLoose()); - } - @override Stream watchMaxProposalsLimitReached() { - return watchUserProposalsCount().map((count) { - return count.finals >= ProposalDocument.maxSubmittedProposalsPerUser; - }); + final activeAccountId = _userService.watchUnlockedActiveAccount + .map((account) => account?.catalystId) + .distinct(); + final activeCampaign = _activeCampaignObserver.watchCampaign.distinct(); + + return Rx.combineLatest2( + activeAccountId, + activeCampaign, + (author, campaign) { + if (author == null || campaign == null) { + return null; + } + + return ProposalsFiltersV2( + status: ProposalStatusFilter.aFinal, + author: author, + campaign: ProposalsCampaignFilters.from(campaign), + ); + }, + ).distinct().switchMap( + (filters) { + if (filters == null) { + return Stream.value(true); + } + + return _proposalRepository + .watchProposalsCountV2(filters: filters) + .map((event) => event >= ProposalDocument.maxSubmittedProposalsPerUser); + }, + ); } @override @@ -542,22 +466,6 @@ final class ProposalServiceImpl implements ProposalService { ); } - @override - Stream watchProposalsCount({ - required ProposalsCountFilters filters, - }) { - final proposalsCount = _proposalRepository.watchProposalsCount(filters: filters); - - // TODO(LynxxLynx): Remove this when we start supporting writing votes to the database - return proposalsCount.switchMap((count) { - return _castedVotesObserver.watchCastedVotes.map((votedProposals) { - return count.copyWith( - voted: votedProposals.length, - ); - }); - }); - } - @override Stream watchProposalsCountV2({ ProposalsFiltersV2 filters = const ProposalsFiltersV2(), @@ -570,106 +478,63 @@ final class ProposalServiceImpl implements ProposalService { } @override - Stream> watchProposalsPage({ - required PageRequest request, - required ProposalsFilters filters, - required ProposalsOrder order, - }) { - return _proposalRepository - .watchProposalsPage(request: request, filters: filters, order: order) - .asyncMap(_mapProposalDataPage); - } + Stream> watchUserProposals() { + return _userService.watchUnlockedActiveAccount.distinct().switchMap((account) { + if (account == null) return const Stream.empty(); + + final authorId = account.catalystId; + if (!account.hasRole(AccountRole.proposer)) { + return const Stream.empty(); + } + + return _proposalRepository + .watchUserProposals(authorId: authorId) + .distinct() + .switchMap>((documents) async* { + if (documents.isEmpty) { + yield []; + return; + } + final proposalsDataStreams = await Future.wait( + documents.map(_createProposalDataStream).toList(), + ); - @override - Stream> watchUserProposals() async* { - yield* _userService // - .watchUser - .distinct() - .switchMap(_userWhenUnlockedStream) - .switchMap((user) { - if (user == null) return const Stream.empty(); - - final authorId = user.activeAccount?.catalystId; - if (!_isProposer(user) || authorId == null) { - return const Stream.empty(); - } + yield* Rx.combineLatest( + proposalsDataStreams, + (List proposalsData) async { + // Note. one is null and two versions of same id. + final validProposalsData = proposalsData.whereType().toList(); - return _proposalRepository - .watchUserProposals(authorId: authorId) - .distinct() - .switchMap>((documents) async* { - if (documents.isEmpty) { - yield []; - return; - } - final proposalsDataStreams = await Future.wait( - documents.map(_createProposalDataStream).toList(), + final groupedProposals = groupBy( + validProposalsData, + (data) => data.document.metadata.selfRef.id, ); - yield* Rx.combineLatest( - proposalsDataStreams, - (List proposalsData) async { - // Note. one is null and two versions of same id. - final validProposalsData = proposalsData.whereType().toList(); - - final groupedProposals = groupBy( - validProposalsData, - (data) => data.document.metadata.selfRef.id, - ); - - final filteredProposalsData = groupedProposals.values - .map((group) { - if (group.any( - (p) => p.publish != ProposalPublish.localDraft, - )) { - return group.where( - (p) => p.publish != ProposalPublish.localDraft, - ); - } - return group; - }) - .expand((group) => group) - .toList(); - - final proposalsWithVersions = await Future.wait( - filteredProposalsData.map((proposalData) async { - final versions = await _getDetailVersionsOfProposal(proposalData); - return DetailProposal.fromData(proposalData, versions); - }), - ); - return proposalsWithVersions; - }, - ).switchMap(Stream.fromFuture); - }); - }); - } - - @override - Stream watchUserProposalsCount() { - return _userService // - .watchUser - .distinct() - .switchMap(_userWhenUnlockedStream) - .switchMap((user) { - if (user == null) return const Stream.empty(); - - final authorId = user.activeAccount?.catalystId; - if (!_isProposer(user) || authorId == null) { - // user is not eligible for creating proposals - return const Stream.empty(); - } - - final activeCampaign = _activeCampaignObserver.campaign; - final categoriesIds = activeCampaign?.categories.map((e) => e.selfRef.id).toList(); - - final filters = ProposalsCountFilters( - author: authorId, - onlyAuthor: true, - campaign: categoriesIds != null ? CampaignFilters(categoriesIds: categoriesIds) : null, - ); - - return watchProposalsCount(filters: filters); - }); + final filteredProposalsData = groupedProposals.values + .map((group) { + if (group.any( + (p) => p.publish != ProposalPublish.localDraft, + )) { + return group.where( + (p) => p.publish != ProposalPublish.localDraft, + ); + } + return group; + }) + .expand((group) => group) + .toList(); + + final proposalsWithVersions = await Future.wait( + filteredProposalsData.map((proposalData) async { + final versions = await _getDetailVersionsOfProposal(proposalData); + return DetailProposal.fromData(proposalData, versions); + }), + ); + return proposalsWithVersions; + }, + ).switchMap(Stream.fromFuture); + }); + }); } // TODO(damian-molinski): Remove this when voteBy is implemented. @@ -689,11 +554,11 @@ final class ProposalServiceImpl implements ProposalService { final selfRef = doc.metadata.selfRef; final commentsCountStream = _proposalRepository.watchCommentsCount( - refTo: selfRef, + referencing: selfRef, ); return Rx.combineLatest2( - _proposalRepository.watchProposalPublish(refTo: selfRef), + _proposalRepository.watchProposalPublish(referencing: selfRef), commentsCountStream, (ProposalPublish? publishState, int commentsCount) { if (publishState == null) return null; @@ -746,10 +611,6 @@ final class ProposalServiceImpl implements ProposalService { return account.catalystId; } - bool _isProposer(User user) { - return user.activeAccount?.roles.contains(AccountRole.proposer) ?? false; - } - ProposalBriefData _mapJoinedProposalBriefData( JoinedProposalBriefData data, List draftVotes, @@ -781,32 +642,4 @@ final class ProposalServiceImpl implements ProposalService { votes: isFinal ? ProposalBriefDataVotes(draft: draftVote, casted: castedVote) : null, ); } - - Future> _mapProposalDataPage(Page page) async { - final proposals = await page.items.map( - (item) async { - final versions = await _proposalRepository - .queryVersionsOfId( - id: item.document.metadata.selfRef.id, - includeLocalDrafts: true, - ) - .then( - (value) => value.map((e) => e.metadata.selfRef.version!).whereType().toList(), - ); - - return Proposal.fromData(item, versions); - }, - ).wait; - - return page.copyWithItems(proposals); - } - - Stream _userWhenUnlockedStream(User user) { - final activeAccount = user.activeAccount; - - if (activeAccount == null) return Stream.value(null); - - final isUnlockedStream = activeAccount.keychain.watchIsUnlocked; - return isUnlockedStream.map((isUnlocked) => isUnlocked ? user : null); - } } diff --git a/catalyst_voices/packages/internal/catalyst_voices_services/test/src/proposal/proposal_service_test.dart b/catalyst_voices/packages/internal/catalyst_voices_services/test/src/proposal/proposal_service_test.dart index 64aac0907c43..a5aa7e8ce072 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_services/test/src/proposal/proposal_service_test.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_services/test/src/proposal/proposal_service_test.dart @@ -39,11 +39,11 @@ void main() { ); registerFallbackValue(const SignedDocumentRef(id: 'fallback-id')); - registerFallbackValue(const ProposalsCountFilters()); + registerFallbackValue(const ProposalsFiltersV2()); when( () => mockDocumentRepository.watchCount( - refTo: any(named: 'refTo'), + referencing: any(named: 'referencing'), type: DocumentType.commentDocument, ), ).thenAnswer((_) => Stream.fromIterable([5])); @@ -60,15 +60,18 @@ void main() { keychain: MockKeychain(), isActive: true, ); - final user = User.optional(accounts: [account]); - const proposalsCount = ProposalsCount( - finals: ProposalDocument.maxSubmittedProposalsPerUser + 1, - ); + final campaign = Campaign.f15(); + const proposalsCount = ProposalDocument.maxSubmittedProposalsPerUser + 1; - when(() => mockUserService.watchUser).thenAnswer((_) => Stream.value(user)); + when( + () => mockUserService.watchUnlockedActiveAccount, + ).thenAnswer((_) => Stream.value(account)); + when( + () => mockActiveCampaignObserver.watchCampaign, + ).thenAnswer((_) => Stream.value(campaign)); when( - () => mockProposalRepository.watchProposalsCount( + () => mockProposalRepository.watchProposalsCountV2( filters: any(named: 'filters'), ), ).thenAnswer((_) => Stream.value(proposalsCount));