From 010e055404a2e32aa2f74935790212903e4cb8cb Mon Sep 17 00:00:00 2001 From: Blomios Date: Sun, 19 Jul 2026 21:54:45 +0200 Subject: [PATCH] feat(online): partage client - use cases, inbox cache et import local (ticket #66) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ajoute l'adapter API distant de partage (infrastructure/remote/share_api.dart) et les use cases associés (application/use_cases.dart), avec cache d'inbox et import local des ressources partagées acceptées. Étend le modèle Drift (migration schemaVersion 11→12) et les entités du domaine en conséquence. flutter pub get OK, build_runner OK, dart format appliqué, analyze propre (mêmes infos préexistantes), 124/124 tests verts, build APK debug validé. Co-Authored-By: Claude Opus 4.8 --- lib/application/app_bootstrap.dart | 22 + lib/application/ports.dart | 63 + lib/application/use_cases.dart | 330 +++ lib/domain/entities.dart | 117 ++ lib/infrastructure/local/app_database.dart | 20 +- lib/infrastructure/local/app_database.g.dart | 1793 +++++++++++++++++ .../local/drift_repositories.dart | 464 +++++ lib/infrastructure/local/tables.dart | 51 + lib/infrastructure/remote/remote.dart | 1 + lib/infrastructure/remote/share_api.dart | 188 ++ test/application/use_cases_test.dart | 317 +++ test/presentation/home_screen_test.dart | 80 + 12 files changed, 3445 insertions(+), 1 deletion(-) create mode 100644 lib/infrastructure/remote/share_api.dart diff --git a/lib/application/app_bootstrap.dart b/lib/application/app_bootstrap.dart index 7729c89..ea2417f 100644 --- a/lib/application/app_bootstrap.dart +++ b/lib/application/app_bootstrap.dart @@ -14,6 +14,7 @@ abstract interface class AppDependencies { CloseWorkoutSessionUseCase get closeWorkoutSessionUseCase; WorkoutHistoryUseCases get workoutHistoryUseCases; SyncUseCases get syncUseCases; + ShareUseCases get shareUseCases; } final class AppBootstrap implements AppDependencies { @@ -29,6 +30,7 @@ final class AppBootstrap implements AppDependencies { required this.closeWorkoutSessionUseCase, required this.workoutHistoryUseCases, required this.syncUseCases, + required this.shareUseCases, required this.syncGateway, }); @@ -53,6 +55,8 @@ final class AppBootstrap implements AppDependencies { final WorkoutHistoryUseCases workoutHistoryUseCases; @override final SyncUseCases syncUseCases; + @override + final ShareUseCases shareUseCases; final SyncGateway syncGateway; static Future create() async { @@ -64,6 +68,10 @@ final class AppBootstrap implements AppDependencies { final syncMetadataRepository = DriftSyncMetadataRepository(database); final mappingRepository = DriftRemoteResourceMappingRepository(database); final localSyncChangeRepository = DriftLocalSyncChangeRepository(database); + final shareInboxRepository = DriftShareInboxRepository(database); + final pendingShareActionRepository = DriftPendingShareActionRepository( + database, + ); final programRepository = DriftProgramRepository(database); final templateRepository = DriftWorkoutTemplateRepository(database); final activeSessionRepository = DriftActiveSessionRepository(database); @@ -77,6 +85,9 @@ final class AppBootstrap implements AppDependencies { final remoteSyncApi = HttpRemoteSyncApi( HttpApiClient(baseUrl: Uri.parse(HttpApiClient.defaultBaseUrl)), ); + final remoteShareApi = HttpRemoteShareApi( + HttpApiClient(baseUrl: Uri.parse(HttpApiClient.defaultBaseUrl)), + ); return AppBootstrap._( database: database, @@ -150,6 +161,17 @@ final class AppBootstrap implements AppDependencies { clock: clock, deviceId: originDeviceId, ), + shareUseCases: ShareUseCases( + tokenStore: const SecureStorageAuthTokenStore(), + remoteShareApi: remoteShareApi, + inboxRepository: shareInboxRepository, + pendingActionRepository: pendingShareActionRepository, + localChanges: localSyncChangeRepository, + programRepository: programRepository, + templateRepository: templateRepository, + clock: clock, + ids: ids, + ), syncGateway: const NoOpSyncGateway(), ); } diff --git a/lib/application/ports.dart b/lib/application/ports.dart index c4b6d49..36666d7 100644 --- a/lib/application/ports.dart +++ b/lib/application/ports.dart @@ -259,6 +259,69 @@ abstract interface class LocalSyncChangeRepository { Future applyRemoteItem(RemoteSyncedItem item); } +final class RemoteShareSendResult { + const RemoteShareSendResult({ + required this.shareId, + required this.recipientUserIds, + required this.unresolvedEmails, + }); + + final String shareId; + final List recipientUserIds; + final List unresolvedEmails; +} + +enum ShareSendStatus { sent, notConnected, queued } + +final class ShareSendResult { + const ShareSendResult({ + required this.status, + this.shareId, + this.recipientUserIds = const [], + this.unresolvedEmails = const [], + this.pendingActionId, + }); + + final ShareSendStatus status; + final String? shareId; + final List recipientUserIds; + final List unresolvedEmails; + final String? pendingActionId; +} + +abstract interface class RemoteShareApi { + Future sendShare({ + required ShareResourceType resourceType, + required Map payload, + required List recipientEmails, + required String token, + }); + + Future> fetchInbox(String token); + Future acceptShare(String shareId, String token); + Future declineShare(String shareId, String token); + Future revokeShare(String shareId, String token); +} + +abstract interface class ShareInboxRepository { + Future> listAll(); + Future findByShareId(String shareId); + Future upsert(ShareInboxItem item); + Future upsertAll(List items); + Future markStatus( + String shareId, + ShareInboxStatus status, + DateTime respondedAt, + ); +} + +abstract interface class PendingShareActionRepository { + Future> listPending(); + Future add(PendingShareAction action); + Future markSucceeded(String id, DateTime attemptedAt); + Future markFailed(String id, DateTime attemptedAt); +} + const Object _portsUnchanged = Object(); abstract interface class ExerciseRepository { diff --git a/lib/application/use_cases.dart b/lib/application/use_cases.dart index c3adec3..f5a0340 100644 --- a/lib/application/use_cases.dart +++ b/lib/application/use_cases.dart @@ -235,6 +235,257 @@ final class SyncUseCases { } } +final class ShareUseCases { + const ShareUseCases({ + required this.tokenStore, + required this.remoteShareApi, + required this.inboxRepository, + required this.pendingActionRepository, + required this.localChanges, + required this.programRepository, + required this.templateRepository, + required this.clock, + required this.ids, + }); + + final AuthTokenStore tokenStore; + final RemoteShareApi remoteShareApi; + final ShareInboxRepository inboxRepository; + final PendingShareActionRepository pendingActionRepository; + final LocalSyncChangeRepository localChanges; + final ProgramRepository programRepository; + final WorkoutTemplateRepository templateRepository; + final Clock clock; + final IdGenerator ids; + + Future sendShare({ + required ShareResourceType resourceType, + required String localResourceId, + required List recipientEmails, + }) async { + final emails = recipientEmails + .map((email) => email.trim()) + .where((email) => email.isNotEmpty) + .toList(growable: false); + if (emails.isEmpty) { + throw const DomainException('At least one recipient email is required.'); + } + final payload = await _sharePayload(resourceType, localResourceId); + final token = await tokenStore.readToken(); + if (token == null) { + return const ShareSendResult(status: ShareSendStatus.notConnected); + } + try { + final result = await remoteShareApi.sendShare( + resourceType: resourceType, + payload: payload, + recipientEmails: emails, + token: token, + ); + return ShareSendResult( + status: ShareSendStatus.sent, + shareId: result.shareId, + recipientUserIds: result.recipientUserIds, + unresolvedEmails: result.unresolvedEmails, + ); + } catch (_) { + final action = _pendingShareAction( + actionType: PendingShareActionType.send, + resourceType: resourceType, + payload: payload, + recipientEmails: emails, + ); + await pendingActionRepository.add(action); + return ShareSendResult( + status: ShareSendStatus.queued, + pendingActionId: action.id, + ); + } + } + + Future> refreshInbox() async { + final token = await tokenStore.readToken(); + if (token == null) { + return inboxRepository.listAll(); + } + try { + final items = await remoteShareApi.fetchInbox(token); + await inboxRepository.upsertAll(items); + return inboxRepository.listAll(); + } catch (_) { + return inboxRepository.listAll(); + } + } + + Future acceptShare(String shareId) async { + final token = await tokenStore.readToken(); + if (token == null) { + return; + } + try { + final createdResource = await remoteShareApi.acceptShare(shareId, token); + await localChanges.applyRemoteItem(createdResource); + await inboxRepository.markStatus( + shareId, + ShareInboxStatus.accepted, + clock.now(), + ); + } catch (_) { + await pendingActionRepository.add( + _pendingShareAction( + actionType: PendingShareActionType.accept, + shareId: shareId, + ), + ); + } + } + + Future declineShare(String shareId) async { + final token = await tokenStore.readToken(); + if (token == null) { + return; + } + try { + await remoteShareApi.declineShare(shareId, token); + await inboxRepository.markStatus( + shareId, + ShareInboxStatus.declined, + clock.now(), + ); + } catch (_) { + await pendingActionRepository.add( + _pendingShareAction( + actionType: PendingShareActionType.decline, + shareId: shareId, + ), + ); + } + } + + Future revokeShare(String shareId) async { + final token = await tokenStore.readToken(); + if (token == null) { + return; + } + try { + await remoteShareApi.revokeShare(shareId, token); + await inboxRepository.markStatus( + shareId, + ShareInboxStatus.revoked, + clock.now(), + ); + } catch (_) { + await pendingActionRepository.add( + _pendingShareAction( + actionType: PendingShareActionType.revoke, + shareId: shareId, + ), + ); + } + } + + Future processPendingShareActions() async { + final token = await tokenStore.readToken(); + if (token == null) { + return; + } + final actions = await pendingActionRepository.listPending(); + for (final action in actions) { + final attemptedAt = clock.now(); + try { + await _processPendingShareAction(action, token); + await pendingActionRepository.markSucceeded(action.id, attemptedAt); + } catch (_) { + await pendingActionRepository.markFailed(action.id, attemptedAt); + } + } + } + + Future> _sharePayload( + ShareResourceType resourceType, + String localResourceId, + ) async { + switch (resourceType) { + case ShareResourceType.program: + final program = await programRepository.findById(localResourceId); + if (program == null) { + throw const DomainException('Program not found.'); + } + return _programSharePayload(program); + case ShareResourceType.workoutTemplate: + final template = await templateRepository.findById(localResourceId); + if (template == null) { + throw const DomainException('Workout template not found.'); + } + return _workoutTemplateSharePayload(template); + } + } + + PendingShareAction _pendingShareAction({ + required PendingShareActionType actionType, + String? shareId, + ShareResourceType? resourceType, + Map? payload, + List? recipientEmails, + }) { + return PendingShareAction( + id: ids.newId(), + actionType: actionType, + shareId: shareId, + resourceType: resourceType, + payloadJson: payload == null ? null : jsonEncode(payload), + recipientEmailsJson: recipientEmails == null + ? null + : jsonEncode(recipientEmails), + createdAt: clock.now(), + ); + } + + Future _processPendingShareAction( + PendingShareAction action, + String token, + ) async { + switch (action.actionType) { + case PendingShareActionType.send: + await remoteShareApi.sendShare( + resourceType: action.resourceType!, + payload: _jsonObject(action.payloadJson), + recipientEmails: _jsonStringList(action.recipientEmailsJson), + token: token, + ); + return; + case PendingShareActionType.accept: + final created = await remoteShareApi.acceptShare( + action.shareId!, + token, + ); + await localChanges.applyRemoteItem(created); + await inboxRepository.markStatus( + action.shareId!, + ShareInboxStatus.accepted, + clock.now(), + ); + return; + case PendingShareActionType.decline: + await remoteShareApi.declineShare(action.shareId!, token); + await inboxRepository.markStatus( + action.shareId!, + ShareInboxStatus.declined, + clock.now(), + ); + return; + case PendingShareActionType.revoke: + await remoteShareApi.revokeShare(action.shareId!, token); + await inboxRepository.markStatus( + action.shareId!, + ShareInboxStatus.revoked, + clock.now(), + ); + return; + } + } +} + final class ExerciseUseCases { const ExerciseUseCases({ required this.repository, @@ -2642,6 +2893,85 @@ String _syncKey(SyncResourceType resourceType, String clientId) { return '${resourceType.name}:$clientId'; } +Map _programSharePayload(Program program) => { + 'metadata': _metadataSharePayload(program.metadata), + 'id': program.metadata.id, + 'name': program.name, + 'defaultRestSeconds': program.defaultRestSeconds, + 'exercises': program.exercises + .map((exercise) => exercise.toSnapshotJson()) + .toList(), +}; + +Map _workoutTemplateSharePayload(WorkoutTemplate template) => { + 'metadata': _metadataSharePayload(template.metadata), + 'id': template.metadata.id, + 'name': template.name, + 'lastStartedAt': template.lastStartedAt?.toUtc().toIso8601String(), + 'programs': template.programs + .map( + (program) => { + 'id': program.metadata.id, + 'sourceProgramId': program.sourceProgramId, + 'position': program.position, + 'programNameSnapshot': program.programNameSnapshot, + 'defaultRestSecondsSnapshot': program.defaultRestSecondsSnapshot, + 'programSnapshotJson': program.programSnapshotJson, + }, + ) + .toList(), + 'overrides': template.overrides + .map( + (override) => { + 'id': override.metadata.id, + 'workoutTemplateProgramId': override.workoutTemplateProgramId, + 'snapshotProgramExerciseId': override.snapshotProgramExerciseId, + 'setsCountOverride': override.setsCountOverride, + 'targetTimeSecondsOverride': override.targetTimeSecondsOverride, + 'targetRepsOverride': override.targetRepsOverride, + 'targetScoreOverride': override.targetScoreOverride, + 'targetScoreTimeMsOverride': override.targetScoreTimeMsOverride, + }, + ) + .toList(), +}; + +Map _metadataSharePayload(EntityMetadata metadata) => { + 'id': metadata.id, + 'createdAt': metadata.createdAt.toUtc().toIso8601String(), + 'updatedAt': metadata.updatedAt.toUtc().toIso8601String(), + 'deletedAt': metadata.deletedAt?.toUtc().toIso8601String(), + 'schemaVersion': metadata.schemaVersion, + 'syncState': metadata.syncState.name, + 'localRevision': metadata.localRevision, + 'originDeviceId': metadata.originDeviceId, + 'futureOwnerProfileId': metadata.futureOwnerProfileId, + 'lastSyncedAt': metadata.lastSyncedAt?.toUtc().toIso8601String(), + 'remoteRevision': metadata.remoteRevision, +}; + +Map _jsonObject(String? json) { + if (json == null) { + return const {}; + } + final decoded = jsonDecode(json); + if (decoded is Map) { + return Map.from(decoded); + } + return const {}; +} + +List _jsonStringList(String? json) { + if (json == null) { + return const []; + } + final decoded = jsonDecode(json); + if (decoded is List) { + return decoded.whereType().toList(growable: false); + } + return const []; +} + ScoreInputMode _scoreInputModeFromSnapshot(Object? value) { return switch (value) { 'stopwatch' => ScoreInputMode.stopwatch, diff --git a/lib/domain/entities.dart b/lib/domain/entities.dart index 95975a2..e4045a7 100644 --- a/lib/domain/entities.dart +++ b/lib/domain/entities.dart @@ -16,6 +16,14 @@ enum SetResultStatus { completed, skipped } enum ActiveScoreStopwatchStatus { running, stopped } +enum ShareResourceType { program, workoutTemplate } + +enum ShareInboxStatus { pending, accepted, declined, revoked } + +enum PendingShareActionType { send, accept, decline, revoke } + +enum PendingShareActionStatus { pending, succeeded, failed } + enum ActiveExerciseStepProgressStatus { notStarted, waitingManual, @@ -154,6 +162,115 @@ final class UserAccountSession { } } +final class ShareInboxItem { + ShareInboxItem({ + required String shareId, + required String senderUserId, + required this.resourceType, + required String payloadJson, + required this.status, + required this.createdAt, + this.respondedAt, + }) : shareId = _nonBlank(shareId, 'Share id'), + senderUserId = _nonBlank(senderUserId, 'Sender user id'), + payloadJson = _nonBlank(payloadJson, 'Share payload JSON'); + + final String shareId; + final String senderUserId; + final ShareResourceType resourceType; + final String payloadJson; + final ShareInboxStatus status; + final DateTime createdAt; + final DateTime? respondedAt; + + ShareInboxItem copyWith({ + String? senderUserId, + ShareResourceType? resourceType, + String? payloadJson, + ShareInboxStatus? status, + DateTime? createdAt, + Object? respondedAt = _unchanged, + }) { + return ShareInboxItem( + shareId: shareId, + senderUserId: senderUserId ?? this.senderUserId, + resourceType: resourceType ?? this.resourceType, + payloadJson: payloadJson ?? this.payloadJson, + status: status ?? this.status, + createdAt: createdAt ?? this.createdAt, + respondedAt: respondedAt == _unchanged + ? this.respondedAt + : respondedAt as DateTime?, + ); + } +} + +final class PendingShareAction { + PendingShareAction({ + required String id, + required this.actionType, + this.shareId, + this.resourceType, + this.payloadJson, + this.recipientEmailsJson, + required this.createdAt, + this.lastAttemptAt, + this.attemptCount = 0, + this.status = PendingShareActionStatus.pending, + }) : id = _nonBlank(id, 'Pending share action id') { + _requireNonNegative(attemptCount, 'Share action attempt count'); + if (shareId != null) { + _nonBlank(shareId, 'Share id'); + } + if (payloadJson != null) { + _nonBlank(payloadJson, 'Share payload JSON'); + } + if (recipientEmailsJson != null) { + _nonBlank(recipientEmailsJson, 'Recipient emails JSON'); + } + } + + final String id; + final PendingShareActionType actionType; + final String? shareId; + final ShareResourceType? resourceType; + final String? payloadJson; + final String? recipientEmailsJson; + final DateTime createdAt; + final DateTime? lastAttemptAt; + final int attemptCount; + final PendingShareActionStatus status; + + PendingShareAction copyWith({ + Object? shareId = _unchanged, + ShareResourceType? resourceType, + Object? payloadJson = _unchanged, + Object? recipientEmailsJson = _unchanged, + Object? lastAttemptAt = _unchanged, + int? attemptCount, + PendingShareActionStatus? status, + }) { + return PendingShareAction( + id: id, + actionType: actionType, + shareId: shareId == _unchanged ? this.shareId : shareId as String?, + resourceType: resourceType ?? this.resourceType, + payloadJson: payloadJson == _unchanged + ? this.payloadJson + : payloadJson as String?, + recipientEmailsJson: recipientEmailsJson == _unchanged + ? this.recipientEmailsJson + : recipientEmailsJson as String?, + createdAt: createdAt, + lastAttemptAt: lastAttemptAt == _unchanged + ? this.lastAttemptAt + : lastAttemptAt as DateTime?, + attemptCount: attemptCount ?? this.attemptCount, + status: status ?? this.status, + ); + } +} + final class MediaAsset { const MediaAsset({ required this.metadata, diff --git a/lib/infrastructure/local/app_database.dart b/lib/infrastructure/local/app_database.dart index c3e0347..fae882d 100644 --- a/lib/infrastructure/local/app_database.dart +++ b/lib/infrastructure/local/app_database.dart @@ -19,9 +19,11 @@ part 'app_database.g.dart'; ExerciseSteps, MediaAssets, OnlineAccountSessions, + PendingShareActions, ProgramExercises, Programs, RemoteResourceMappings, + ShareInboxItems, SyncMetadataEntries, WorkoutHistories, WorkoutHistorySetResults, @@ -44,7 +46,7 @@ final class AppDatabase extends _$AppDatabase { } @override - int get schemaVersion => 11; + int get schemaVersion => 12; @override MigrationStrategy get migration { @@ -92,6 +94,9 @@ final class AppDatabase extends _$AppDatabase { if (from < 11) { await _migrateToSchema11(migrator); } + if (from < 12) { + await _migrateToSchema12(migrator); + } await _createIndexes(); }, beforeOpen: (details) async { @@ -140,6 +145,14 @@ final class AppDatabase extends _$AppDatabase { 'CREATE INDEX IF NOT EXISTS idx_remote_resource_mappings_resource ' 'ON remote_resource_mappings (resource_type, client_id)', ); + await customStatement( + 'CREATE INDEX IF NOT EXISTS idx_share_inbox_items_created_at ' + 'ON share_inbox_items (created_at)', + ); + await customStatement( + 'CREATE INDEX IF NOT EXISTS idx_pending_share_actions_status ' + 'ON pending_share_actions (status, created_at)', + ); await customStatement( 'CREATE INDEX IF NOT EXISTS idx_workout_template_programs_template_id ' 'ON workout_template_programs (workout_template_id)', @@ -341,4 +354,9 @@ extension on AppDatabase { await migrator.createTable(syncMetadataEntries); await migrator.createTable(remoteResourceMappings); } + + Future _migrateToSchema12(Migrator migrator) async { + await migrator.createTable(shareInboxItems); + await migrator.createTable(pendingShareActions); + } } diff --git a/lib/infrastructure/local/app_database.g.dart b/lib/infrastructure/local/app_database.g.dart index b39d02d..a5d64b7 100644 --- a/lib/infrastructure/local/app_database.g.dart +++ b/lib/infrastructure/local/app_database.g.dart @@ -15460,6 +15460,656 @@ class OnlineAccountSessionsCompanion } } +class $PendingShareActionsTable extends PendingShareActions + with TableInfo<$PendingShareActionsTable, PendingShareAction> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $PendingShareActionsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _idMeta = const VerificationMeta('id'); + @override + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _actionTypeMeta = const VerificationMeta( + 'actionType', + ); + @override + late final GeneratedColumn actionType = GeneratedColumn( + 'action_type', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _shareIdMeta = const VerificationMeta( + 'shareId', + ); + @override + late final GeneratedColumn shareId = GeneratedColumn( + 'share_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _resourceTypeMeta = const VerificationMeta( + 'resourceType', + ); + @override + late final GeneratedColumn resourceType = GeneratedColumn( + 'resource_type', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _payloadJsonMeta = const VerificationMeta( + 'payloadJson', + ); + @override + late final GeneratedColumn payloadJson = GeneratedColumn( + 'payload_json', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _recipientEmailsJsonMeta = + const VerificationMeta('recipientEmailsJson'); + @override + late final GeneratedColumn recipientEmailsJson = + GeneratedColumn( + 'recipient_emails_json', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _createdAtMeta = const VerificationMeta( + 'createdAt', + ); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: true, + ); + static const VerificationMeta _lastAttemptAtMeta = const VerificationMeta( + 'lastAttemptAt', + ); + @override + late final GeneratedColumn lastAttemptAt = + GeneratedColumn( + 'last_attempt_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + static const VerificationMeta _attemptCountMeta = const VerificationMeta( + 'attemptCount', + ); + @override + late final GeneratedColumn attemptCount = GeneratedColumn( + 'attempt_count', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL CHECK (attempt_count >= 0)', + ); + static const VerificationMeta _statusMeta = const VerificationMeta('status'); + @override + late final GeneratedColumn status = GeneratedColumn( + 'status', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + @override + List get $columns => [ + id, + actionType, + shareId, + resourceType, + payloadJson, + recipientEmailsJson, + createdAt, + lastAttemptAt, + attemptCount, + status, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'pending_share_actions'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } else if (isInserting) { + context.missing(_idMeta); + } + if (data.containsKey('action_type')) { + context.handle( + _actionTypeMeta, + actionType.isAcceptableOrUnknown(data['action_type']!, _actionTypeMeta), + ); + } else if (isInserting) { + context.missing(_actionTypeMeta); + } + if (data.containsKey('share_id')) { + context.handle( + _shareIdMeta, + shareId.isAcceptableOrUnknown(data['share_id']!, _shareIdMeta), + ); + } + if (data.containsKey('resource_type')) { + context.handle( + _resourceTypeMeta, + resourceType.isAcceptableOrUnknown( + data['resource_type']!, + _resourceTypeMeta, + ), + ); + } + if (data.containsKey('payload_json')) { + context.handle( + _payloadJsonMeta, + payloadJson.isAcceptableOrUnknown( + data['payload_json']!, + _payloadJsonMeta, + ), + ); + } + if (data.containsKey('recipient_emails_json')) { + context.handle( + _recipientEmailsJsonMeta, + recipientEmailsJson.isAcceptableOrUnknown( + data['recipient_emails_json']!, + _recipientEmailsJsonMeta, + ), + ); + } + if (data.containsKey('created_at')) { + context.handle( + _createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta), + ); + } else if (isInserting) { + context.missing(_createdAtMeta); + } + if (data.containsKey('last_attempt_at')) { + context.handle( + _lastAttemptAtMeta, + lastAttemptAt.isAcceptableOrUnknown( + data['last_attempt_at']!, + _lastAttemptAtMeta, + ), + ); + } + if (data.containsKey('attempt_count')) { + context.handle( + _attemptCountMeta, + attemptCount.isAcceptableOrUnknown( + data['attempt_count']!, + _attemptCountMeta, + ), + ); + } else if (isInserting) { + context.missing(_attemptCountMeta); + } + if (data.containsKey('status')) { + context.handle( + _statusMeta, + status.isAcceptableOrUnknown(data['status']!, _statusMeta), + ); + } else if (isInserting) { + context.missing(_statusMeta); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + PendingShareAction map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return PendingShareAction( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + actionType: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}action_type'], + )!, + shareId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}share_id'], + ), + resourceType: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}resource_type'], + ), + payloadJson: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}payload_json'], + ), + recipientEmailsJson: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}recipient_emails_json'], + ), + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + lastAttemptAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}last_attempt_at'], + ), + attemptCount: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}attempt_count'], + )!, + status: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}status'], + )!, + ); + } + + @override + $PendingShareActionsTable createAlias(String alias) { + return $PendingShareActionsTable(attachedDatabase, alias); + } +} + +class PendingShareAction extends DataClass + implements Insertable { + final String id; + final String actionType; + final String? shareId; + final String? resourceType; + final String? payloadJson; + final String? recipientEmailsJson; + final DateTime createdAt; + final DateTime? lastAttemptAt; + final int attemptCount; + final String status; + const PendingShareAction({ + required this.id, + required this.actionType, + this.shareId, + this.resourceType, + this.payloadJson, + this.recipientEmailsJson, + required this.createdAt, + this.lastAttemptAt, + required this.attemptCount, + required this.status, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['action_type'] = Variable(actionType); + if (!nullToAbsent || shareId != null) { + map['share_id'] = Variable(shareId); + } + if (!nullToAbsent || resourceType != null) { + map['resource_type'] = Variable(resourceType); + } + if (!nullToAbsent || payloadJson != null) { + map['payload_json'] = Variable(payloadJson); + } + if (!nullToAbsent || recipientEmailsJson != null) { + map['recipient_emails_json'] = Variable(recipientEmailsJson); + } + map['created_at'] = Variable(createdAt); + if (!nullToAbsent || lastAttemptAt != null) { + map['last_attempt_at'] = Variable(lastAttemptAt); + } + map['attempt_count'] = Variable(attemptCount); + map['status'] = Variable(status); + return map; + } + + PendingShareActionsCompanion toCompanion(bool nullToAbsent) { + return PendingShareActionsCompanion( + id: Value(id), + actionType: Value(actionType), + shareId: shareId == null && nullToAbsent + ? const Value.absent() + : Value(shareId), + resourceType: resourceType == null && nullToAbsent + ? const Value.absent() + : Value(resourceType), + payloadJson: payloadJson == null && nullToAbsent + ? const Value.absent() + : Value(payloadJson), + recipientEmailsJson: recipientEmailsJson == null && nullToAbsent + ? const Value.absent() + : Value(recipientEmailsJson), + createdAt: Value(createdAt), + lastAttemptAt: lastAttemptAt == null && nullToAbsent + ? const Value.absent() + : Value(lastAttemptAt), + attemptCount: Value(attemptCount), + status: Value(status), + ); + } + + factory PendingShareAction.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return PendingShareAction( + id: serializer.fromJson(json['id']), + actionType: serializer.fromJson(json['actionType']), + shareId: serializer.fromJson(json['shareId']), + resourceType: serializer.fromJson(json['resourceType']), + payloadJson: serializer.fromJson(json['payloadJson']), + recipientEmailsJson: serializer.fromJson( + json['recipientEmailsJson'], + ), + createdAt: serializer.fromJson(json['createdAt']), + lastAttemptAt: serializer.fromJson(json['lastAttemptAt']), + attemptCount: serializer.fromJson(json['attemptCount']), + status: serializer.fromJson(json['status']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'actionType': serializer.toJson(actionType), + 'shareId': serializer.toJson(shareId), + 'resourceType': serializer.toJson(resourceType), + 'payloadJson': serializer.toJson(payloadJson), + 'recipientEmailsJson': serializer.toJson(recipientEmailsJson), + 'createdAt': serializer.toJson(createdAt), + 'lastAttemptAt': serializer.toJson(lastAttemptAt), + 'attemptCount': serializer.toJson(attemptCount), + 'status': serializer.toJson(status), + }; + } + + PendingShareAction copyWith({ + String? id, + String? actionType, + Value shareId = const Value.absent(), + Value resourceType = const Value.absent(), + Value payloadJson = const Value.absent(), + Value recipientEmailsJson = const Value.absent(), + DateTime? createdAt, + Value lastAttemptAt = const Value.absent(), + int? attemptCount, + String? status, + }) => PendingShareAction( + id: id ?? this.id, + actionType: actionType ?? this.actionType, + shareId: shareId.present ? shareId.value : this.shareId, + resourceType: resourceType.present ? resourceType.value : this.resourceType, + payloadJson: payloadJson.present ? payloadJson.value : this.payloadJson, + recipientEmailsJson: recipientEmailsJson.present + ? recipientEmailsJson.value + : this.recipientEmailsJson, + createdAt: createdAt ?? this.createdAt, + lastAttemptAt: lastAttemptAt.present + ? lastAttemptAt.value + : this.lastAttemptAt, + attemptCount: attemptCount ?? this.attemptCount, + status: status ?? this.status, + ); + PendingShareAction copyWithCompanion(PendingShareActionsCompanion data) { + return PendingShareAction( + id: data.id.present ? data.id.value : this.id, + actionType: data.actionType.present + ? data.actionType.value + : this.actionType, + shareId: data.shareId.present ? data.shareId.value : this.shareId, + resourceType: data.resourceType.present + ? data.resourceType.value + : this.resourceType, + payloadJson: data.payloadJson.present + ? data.payloadJson.value + : this.payloadJson, + recipientEmailsJson: data.recipientEmailsJson.present + ? data.recipientEmailsJson.value + : this.recipientEmailsJson, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + lastAttemptAt: data.lastAttemptAt.present + ? data.lastAttemptAt.value + : this.lastAttemptAt, + attemptCount: data.attemptCount.present + ? data.attemptCount.value + : this.attemptCount, + status: data.status.present ? data.status.value : this.status, + ); + } + + @override + String toString() { + return (StringBuffer('PendingShareAction(') + ..write('id: $id, ') + ..write('actionType: $actionType, ') + ..write('shareId: $shareId, ') + ..write('resourceType: $resourceType, ') + ..write('payloadJson: $payloadJson, ') + ..write('recipientEmailsJson: $recipientEmailsJson, ') + ..write('createdAt: $createdAt, ') + ..write('lastAttemptAt: $lastAttemptAt, ') + ..write('attemptCount: $attemptCount, ') + ..write('status: $status') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + actionType, + shareId, + resourceType, + payloadJson, + recipientEmailsJson, + createdAt, + lastAttemptAt, + attemptCount, + status, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is PendingShareAction && + other.id == this.id && + other.actionType == this.actionType && + other.shareId == this.shareId && + other.resourceType == this.resourceType && + other.payloadJson == this.payloadJson && + other.recipientEmailsJson == this.recipientEmailsJson && + other.createdAt == this.createdAt && + other.lastAttemptAt == this.lastAttemptAt && + other.attemptCount == this.attemptCount && + other.status == this.status); +} + +class PendingShareActionsCompanion extends UpdateCompanion { + final Value id; + final Value actionType; + final Value shareId; + final Value resourceType; + final Value payloadJson; + final Value recipientEmailsJson; + final Value createdAt; + final Value lastAttemptAt; + final Value attemptCount; + final Value status; + final Value rowid; + const PendingShareActionsCompanion({ + this.id = const Value.absent(), + this.actionType = const Value.absent(), + this.shareId = const Value.absent(), + this.resourceType = const Value.absent(), + this.payloadJson = const Value.absent(), + this.recipientEmailsJson = const Value.absent(), + this.createdAt = const Value.absent(), + this.lastAttemptAt = const Value.absent(), + this.attemptCount = const Value.absent(), + this.status = const Value.absent(), + this.rowid = const Value.absent(), + }); + PendingShareActionsCompanion.insert({ + required String id, + required String actionType, + this.shareId = const Value.absent(), + this.resourceType = const Value.absent(), + this.payloadJson = const Value.absent(), + this.recipientEmailsJson = const Value.absent(), + required DateTime createdAt, + this.lastAttemptAt = const Value.absent(), + required int attemptCount, + required String status, + this.rowid = const Value.absent(), + }) : id = Value(id), + actionType = Value(actionType), + createdAt = Value(createdAt), + attemptCount = Value(attemptCount), + status = Value(status); + static Insertable custom({ + Expression? id, + Expression? actionType, + Expression? shareId, + Expression? resourceType, + Expression? payloadJson, + Expression? recipientEmailsJson, + Expression? createdAt, + Expression? lastAttemptAt, + Expression? attemptCount, + Expression? status, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (actionType != null) 'action_type': actionType, + if (shareId != null) 'share_id': shareId, + if (resourceType != null) 'resource_type': resourceType, + if (payloadJson != null) 'payload_json': payloadJson, + if (recipientEmailsJson != null) + 'recipient_emails_json': recipientEmailsJson, + if (createdAt != null) 'created_at': createdAt, + if (lastAttemptAt != null) 'last_attempt_at': lastAttemptAt, + if (attemptCount != null) 'attempt_count': attemptCount, + if (status != null) 'status': status, + if (rowid != null) 'rowid': rowid, + }); + } + + PendingShareActionsCompanion copyWith({ + Value? id, + Value? actionType, + Value? shareId, + Value? resourceType, + Value? payloadJson, + Value? recipientEmailsJson, + Value? createdAt, + Value? lastAttemptAt, + Value? attemptCount, + Value? status, + Value? rowid, + }) { + return PendingShareActionsCompanion( + id: id ?? this.id, + actionType: actionType ?? this.actionType, + shareId: shareId ?? this.shareId, + resourceType: resourceType ?? this.resourceType, + payloadJson: payloadJson ?? this.payloadJson, + recipientEmailsJson: recipientEmailsJson ?? this.recipientEmailsJson, + createdAt: createdAt ?? this.createdAt, + lastAttemptAt: lastAttemptAt ?? this.lastAttemptAt, + attemptCount: attemptCount ?? this.attemptCount, + status: status ?? this.status, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (actionType.present) { + map['action_type'] = Variable(actionType.value); + } + if (shareId.present) { + map['share_id'] = Variable(shareId.value); + } + if (resourceType.present) { + map['resource_type'] = Variable(resourceType.value); + } + if (payloadJson.present) { + map['payload_json'] = Variable(payloadJson.value); + } + if (recipientEmailsJson.present) { + map['recipient_emails_json'] = Variable( + recipientEmailsJson.value, + ); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (lastAttemptAt.present) { + map['last_attempt_at'] = Variable(lastAttemptAt.value); + } + if (attemptCount.present) { + map['attempt_count'] = Variable(attemptCount.value); + } + if (status.present) { + map['status'] = Variable(status.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PendingShareActionsCompanion(') + ..write('id: $id, ') + ..write('actionType: $actionType, ') + ..write('shareId: $shareId, ') + ..write('resourceType: $resourceType, ') + ..write('payloadJson: $payloadJson, ') + ..write('recipientEmailsJson: $recipientEmailsJson, ') + ..write('createdAt: $createdAt, ') + ..write('lastAttemptAt: $lastAttemptAt, ') + ..write('attemptCount: $attemptCount, ') + ..write('status: $status, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + class $ProgramsTable extends Programs with TableInfo<$ProgramsTable, Program> { @override final GeneratedDatabase attachedDatabase; @@ -18839,6 +19489,544 @@ class RemoteResourceMappingsCompanion } } +class $ShareInboxItemsTable extends ShareInboxItems + with TableInfo<$ShareInboxItemsTable, ShareInboxItem> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $ShareInboxItemsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _shareIdMeta = const VerificationMeta( + 'shareId', + ); + @override + late final GeneratedColumn shareId = GeneratedColumn( + 'share_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _senderUserIdMeta = const VerificationMeta( + 'senderUserId', + ); + @override + late final GeneratedColumn senderUserId = GeneratedColumn( + 'sender_user_id', + aliasedName, + false, + additionalChecks: GeneratedColumn.checkTextLength(minTextLength: 1), + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _resourceTypeMeta = const VerificationMeta( + 'resourceType', + ); + @override + late final GeneratedColumn resourceType = GeneratedColumn( + 'resource_type', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _payloadJsonMeta = const VerificationMeta( + 'payloadJson', + ); + @override + late final GeneratedColumn payloadJson = GeneratedColumn( + 'payload_json', + aliasedName, + false, + additionalChecks: GeneratedColumn.checkTextLength(minTextLength: 1), + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _statusMeta = const VerificationMeta('status'); + @override + late final GeneratedColumn status = GeneratedColumn( + 'status', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _createdAtMeta = const VerificationMeta( + 'createdAt', + ); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: true, + ); + static const VerificationMeta _updatedAtMeta = const VerificationMeta( + 'updatedAt', + ); + @override + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: true, + ); + static const VerificationMeta _respondedAtMeta = const VerificationMeta( + 'respondedAt', + ); + @override + late final GeneratedColumn respondedAt = GeneratedColumn( + 'responded_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + shareId, + senderUserId, + resourceType, + payloadJson, + status, + createdAt, + updatedAt, + respondedAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'share_inbox_items'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('share_id')) { + context.handle( + _shareIdMeta, + shareId.isAcceptableOrUnknown(data['share_id']!, _shareIdMeta), + ); + } else if (isInserting) { + context.missing(_shareIdMeta); + } + if (data.containsKey('sender_user_id')) { + context.handle( + _senderUserIdMeta, + senderUserId.isAcceptableOrUnknown( + data['sender_user_id']!, + _senderUserIdMeta, + ), + ); + } else if (isInserting) { + context.missing(_senderUserIdMeta); + } + if (data.containsKey('resource_type')) { + context.handle( + _resourceTypeMeta, + resourceType.isAcceptableOrUnknown( + data['resource_type']!, + _resourceTypeMeta, + ), + ); + } else if (isInserting) { + context.missing(_resourceTypeMeta); + } + if (data.containsKey('payload_json')) { + context.handle( + _payloadJsonMeta, + payloadJson.isAcceptableOrUnknown( + data['payload_json']!, + _payloadJsonMeta, + ), + ); + } else if (isInserting) { + context.missing(_payloadJsonMeta); + } + if (data.containsKey('status')) { + context.handle( + _statusMeta, + status.isAcceptableOrUnknown(data['status']!, _statusMeta), + ); + } else if (isInserting) { + context.missing(_statusMeta); + } + if (data.containsKey('created_at')) { + context.handle( + _createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta), + ); + } else if (isInserting) { + context.missing(_createdAtMeta); + } + if (data.containsKey('updated_at')) { + context.handle( + _updatedAtMeta, + updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta), + ); + } else if (isInserting) { + context.missing(_updatedAtMeta); + } + if (data.containsKey('responded_at')) { + context.handle( + _respondedAtMeta, + respondedAt.isAcceptableOrUnknown( + data['responded_at']!, + _respondedAtMeta, + ), + ); + } + return context; + } + + @override + Set get $primaryKey => {shareId}; + @override + ShareInboxItem map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ShareInboxItem( + shareId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}share_id'], + )!, + senderUserId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}sender_user_id'], + )!, + resourceType: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}resource_type'], + )!, + payloadJson: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}payload_json'], + )!, + status: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}status'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + respondedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}responded_at'], + ), + ); + } + + @override + $ShareInboxItemsTable createAlias(String alias) { + return $ShareInboxItemsTable(attachedDatabase, alias); + } +} + +class ShareInboxItem extends DataClass implements Insertable { + final String shareId; + final String senderUserId; + final String resourceType; + final String payloadJson; + final String status; + final DateTime createdAt; + final DateTime updatedAt; + final DateTime? respondedAt; + const ShareInboxItem({ + required this.shareId, + required this.senderUserId, + required this.resourceType, + required this.payloadJson, + required this.status, + required this.createdAt, + required this.updatedAt, + this.respondedAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['share_id'] = Variable(shareId); + map['sender_user_id'] = Variable(senderUserId); + map['resource_type'] = Variable(resourceType); + map['payload_json'] = Variable(payloadJson); + map['status'] = Variable(status); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || respondedAt != null) { + map['responded_at'] = Variable(respondedAt); + } + return map; + } + + ShareInboxItemsCompanion toCompanion(bool nullToAbsent) { + return ShareInboxItemsCompanion( + shareId: Value(shareId), + senderUserId: Value(senderUserId), + resourceType: Value(resourceType), + payloadJson: Value(payloadJson), + status: Value(status), + createdAt: Value(createdAt), + updatedAt: Value(updatedAt), + respondedAt: respondedAt == null && nullToAbsent + ? const Value.absent() + : Value(respondedAt), + ); + } + + factory ShareInboxItem.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ShareInboxItem( + shareId: serializer.fromJson(json['shareId']), + senderUserId: serializer.fromJson(json['senderUserId']), + resourceType: serializer.fromJson(json['resourceType']), + payloadJson: serializer.fromJson(json['payloadJson']), + status: serializer.fromJson(json['status']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + respondedAt: serializer.fromJson(json['respondedAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'shareId': serializer.toJson(shareId), + 'senderUserId': serializer.toJson(senderUserId), + 'resourceType': serializer.toJson(resourceType), + 'payloadJson': serializer.toJson(payloadJson), + 'status': serializer.toJson(status), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'respondedAt': serializer.toJson(respondedAt), + }; + } + + ShareInboxItem copyWith({ + String? shareId, + String? senderUserId, + String? resourceType, + String? payloadJson, + String? status, + DateTime? createdAt, + DateTime? updatedAt, + Value respondedAt = const Value.absent(), + }) => ShareInboxItem( + shareId: shareId ?? this.shareId, + senderUserId: senderUserId ?? this.senderUserId, + resourceType: resourceType ?? this.resourceType, + payloadJson: payloadJson ?? this.payloadJson, + status: status ?? this.status, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + respondedAt: respondedAt.present ? respondedAt.value : this.respondedAt, + ); + ShareInboxItem copyWithCompanion(ShareInboxItemsCompanion data) { + return ShareInboxItem( + shareId: data.shareId.present ? data.shareId.value : this.shareId, + senderUserId: data.senderUserId.present + ? data.senderUserId.value + : this.senderUserId, + resourceType: data.resourceType.present + ? data.resourceType.value + : this.resourceType, + payloadJson: data.payloadJson.present + ? data.payloadJson.value + : this.payloadJson, + status: data.status.present ? data.status.value : this.status, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + respondedAt: data.respondedAt.present + ? data.respondedAt.value + : this.respondedAt, + ); + } + + @override + String toString() { + return (StringBuffer('ShareInboxItem(') + ..write('shareId: $shareId, ') + ..write('senderUserId: $senderUserId, ') + ..write('resourceType: $resourceType, ') + ..write('payloadJson: $payloadJson, ') + ..write('status: $status, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('respondedAt: $respondedAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + shareId, + senderUserId, + resourceType, + payloadJson, + status, + createdAt, + updatedAt, + respondedAt, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ShareInboxItem && + other.shareId == this.shareId && + other.senderUserId == this.senderUserId && + other.resourceType == this.resourceType && + other.payloadJson == this.payloadJson && + other.status == this.status && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.respondedAt == this.respondedAt); +} + +class ShareInboxItemsCompanion extends UpdateCompanion { + final Value shareId; + final Value senderUserId; + final Value resourceType; + final Value payloadJson; + final Value status; + final Value createdAt; + final Value updatedAt; + final Value respondedAt; + final Value rowid; + const ShareInboxItemsCompanion({ + this.shareId = const Value.absent(), + this.senderUserId = const Value.absent(), + this.resourceType = const Value.absent(), + this.payloadJson = const Value.absent(), + this.status = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.respondedAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + ShareInboxItemsCompanion.insert({ + required String shareId, + required String senderUserId, + required String resourceType, + required String payloadJson, + required String status, + required DateTime createdAt, + required DateTime updatedAt, + this.respondedAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : shareId = Value(shareId), + senderUserId = Value(senderUserId), + resourceType = Value(resourceType), + payloadJson = Value(payloadJson), + status = Value(status), + createdAt = Value(createdAt), + updatedAt = Value(updatedAt); + static Insertable custom({ + Expression? shareId, + Expression? senderUserId, + Expression? resourceType, + Expression? payloadJson, + Expression? status, + Expression? createdAt, + Expression? updatedAt, + Expression? respondedAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (shareId != null) 'share_id': shareId, + if (senderUserId != null) 'sender_user_id': senderUserId, + if (resourceType != null) 'resource_type': resourceType, + if (payloadJson != null) 'payload_json': payloadJson, + if (status != null) 'status': status, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (respondedAt != null) 'responded_at': respondedAt, + if (rowid != null) 'rowid': rowid, + }); + } + + ShareInboxItemsCompanion copyWith({ + Value? shareId, + Value? senderUserId, + Value? resourceType, + Value? payloadJson, + Value? status, + Value? createdAt, + Value? updatedAt, + Value? respondedAt, + Value? rowid, + }) { + return ShareInboxItemsCompanion( + shareId: shareId ?? this.shareId, + senderUserId: senderUserId ?? this.senderUserId, + resourceType: resourceType ?? this.resourceType, + payloadJson: payloadJson ?? this.payloadJson, + status: status ?? this.status, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + respondedAt: respondedAt ?? this.respondedAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (shareId.present) { + map['share_id'] = Variable(shareId.value); + } + if (senderUserId.present) { + map['sender_user_id'] = Variable(senderUserId.value); + } + if (resourceType.present) { + map['resource_type'] = Variable(resourceType.value); + } + if (payloadJson.present) { + map['payload_json'] = Variable(payloadJson.value); + } + if (status.present) { + map['status'] = Variable(status.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (respondedAt.present) { + map['responded_at'] = Variable(respondedAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ShareInboxItemsCompanion(') + ..write('shareId: $shareId, ') + ..write('senderUserId: $senderUserId, ') + ..write('resourceType: $resourceType, ') + ..write('payloadJson: $payloadJson, ') + ..write('status: $status, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('respondedAt: $respondedAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + class $SyncMetadataEntriesTable extends SyncMetadataEntries with TableInfo<$SyncMetadataEntriesTable, SyncMetadataEntry> { @override @@ -26977,12 +28165,17 @@ abstract class _$AppDatabase extends GeneratedDatabase { late final $ExerciseStepsTable exerciseSteps = $ExerciseStepsTable(this); late final $OnlineAccountSessionsTable onlineAccountSessions = $OnlineAccountSessionsTable(this); + late final $PendingShareActionsTable pendingShareActions = + $PendingShareActionsTable(this); late final $ProgramsTable programs = $ProgramsTable(this); late final $ProgramExercisesTable programExercises = $ProgramExercisesTable( this, ); late final $RemoteResourceMappingsTable remoteResourceMappings = $RemoteResourceMappingsTable(this); + late final $ShareInboxItemsTable shareInboxItems = $ShareInboxItemsTable( + this, + ); late final $SyncMetadataEntriesTable syncMetadataEntries = $SyncMetadataEntriesTable(this); late final $WorkoutHistoriesTable workoutHistories = $WorkoutHistoriesTable( @@ -27016,9 +28209,11 @@ abstract class _$AppDatabase extends GeneratedDatabase { exerciseImages, exerciseSteps, onlineAccountSessions, + pendingShareActions, programs, programExercises, remoteResourceMappings, + shareInboxItems, syncMetadataEntries, workoutHistories, workoutHistorySetResults, @@ -36876,6 +38071,329 @@ typedef $$OnlineAccountSessionsTableProcessedTableManager = OnlineAccountSession, PrefetchHooks Function() >; +typedef $$PendingShareActionsTableCreateCompanionBuilder = + PendingShareActionsCompanion Function({ + required String id, + required String actionType, + Value shareId, + Value resourceType, + Value payloadJson, + Value recipientEmailsJson, + required DateTime createdAt, + Value lastAttemptAt, + required int attemptCount, + required String status, + Value rowid, + }); +typedef $$PendingShareActionsTableUpdateCompanionBuilder = + PendingShareActionsCompanion Function({ + Value id, + Value actionType, + Value shareId, + Value resourceType, + Value payloadJson, + Value recipientEmailsJson, + Value createdAt, + Value lastAttemptAt, + Value attemptCount, + Value status, + Value rowid, + }); + +class $$PendingShareActionsTableFilterComposer + extends Composer<_$AppDatabase, $PendingShareActionsTable> { + $$PendingShareActionsTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get actionType => $composableBuilder( + column: $table.actionType, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get shareId => $composableBuilder( + column: $table.shareId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get resourceType => $composableBuilder( + column: $table.resourceType, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get payloadJson => $composableBuilder( + column: $table.payloadJson, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get recipientEmailsJson => $composableBuilder( + column: $table.recipientEmailsJson, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get lastAttemptAt => $composableBuilder( + column: $table.lastAttemptAt, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get attemptCount => $composableBuilder( + column: $table.attemptCount, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get status => $composableBuilder( + column: $table.status, + builder: (column) => ColumnFilters(column), + ); +} + +class $$PendingShareActionsTableOrderingComposer + extends Composer<_$AppDatabase, $PendingShareActionsTable> { + $$PendingShareActionsTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get actionType => $composableBuilder( + column: $table.actionType, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get shareId => $composableBuilder( + column: $table.shareId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get resourceType => $composableBuilder( + column: $table.resourceType, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get payloadJson => $composableBuilder( + column: $table.payloadJson, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get recipientEmailsJson => $composableBuilder( + column: $table.recipientEmailsJson, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get lastAttemptAt => $composableBuilder( + column: $table.lastAttemptAt, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get attemptCount => $composableBuilder( + column: $table.attemptCount, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get status => $composableBuilder( + column: $table.status, + builder: (column) => ColumnOrderings(column), + ); +} + +class $$PendingShareActionsTableAnnotationComposer + extends Composer<_$AppDatabase, $PendingShareActionsTable> { + $$PendingShareActionsTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + GeneratedColumn get actionType => $composableBuilder( + column: $table.actionType, + builder: (column) => column, + ); + + GeneratedColumn get shareId => + $composableBuilder(column: $table.shareId, builder: (column) => column); + + GeneratedColumn get resourceType => $composableBuilder( + column: $table.resourceType, + builder: (column) => column, + ); + + GeneratedColumn get payloadJson => $composableBuilder( + column: $table.payloadJson, + builder: (column) => column, + ); + + GeneratedColumn get recipientEmailsJson => $composableBuilder( + column: $table.recipientEmailsJson, + builder: (column) => column, + ); + + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + GeneratedColumn get lastAttemptAt => $composableBuilder( + column: $table.lastAttemptAt, + builder: (column) => column, + ); + + GeneratedColumn get attemptCount => $composableBuilder( + column: $table.attemptCount, + builder: (column) => column, + ); + + GeneratedColumn get status => + $composableBuilder(column: $table.status, builder: (column) => column); +} + +class $$PendingShareActionsTableTableManager + extends + RootTableManager< + _$AppDatabase, + $PendingShareActionsTable, + PendingShareAction, + $$PendingShareActionsTableFilterComposer, + $$PendingShareActionsTableOrderingComposer, + $$PendingShareActionsTableAnnotationComposer, + $$PendingShareActionsTableCreateCompanionBuilder, + $$PendingShareActionsTableUpdateCompanionBuilder, + ( + PendingShareAction, + BaseReferences< + _$AppDatabase, + $PendingShareActionsTable, + PendingShareAction + >, + ), + PendingShareAction, + PrefetchHooks Function() + > { + $$PendingShareActionsTableTableManager( + _$AppDatabase db, + $PendingShareActionsTable table, + ) : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$PendingShareActionsTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$PendingShareActionsTableOrderingComposer( + $db: db, + $table: table, + ), + createComputedFieldComposer: () => + $$PendingShareActionsTableAnnotationComposer( + $db: db, + $table: table, + ), + updateCompanionCallback: + ({ + Value id = const Value.absent(), + Value actionType = const Value.absent(), + Value shareId = const Value.absent(), + Value resourceType = const Value.absent(), + Value payloadJson = const Value.absent(), + Value recipientEmailsJson = const Value.absent(), + Value createdAt = const Value.absent(), + Value lastAttemptAt = const Value.absent(), + Value attemptCount = const Value.absent(), + Value status = const Value.absent(), + Value rowid = const Value.absent(), + }) => PendingShareActionsCompanion( + id: id, + actionType: actionType, + shareId: shareId, + resourceType: resourceType, + payloadJson: payloadJson, + recipientEmailsJson: recipientEmailsJson, + createdAt: createdAt, + lastAttemptAt: lastAttemptAt, + attemptCount: attemptCount, + status: status, + rowid: rowid, + ), + createCompanionCallback: + ({ + required String id, + required String actionType, + Value shareId = const Value.absent(), + Value resourceType = const Value.absent(), + Value payloadJson = const Value.absent(), + Value recipientEmailsJson = const Value.absent(), + required DateTime createdAt, + Value lastAttemptAt = const Value.absent(), + required int attemptCount, + required String status, + Value rowid = const Value.absent(), + }) => PendingShareActionsCompanion.insert( + id: id, + actionType: actionType, + shareId: shareId, + resourceType: resourceType, + payloadJson: payloadJson, + recipientEmailsJson: recipientEmailsJson, + createdAt: createdAt, + lastAttemptAt: lastAttemptAt, + attemptCount: attemptCount, + status: status, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + ), + ); +} + +typedef $$PendingShareActionsTableProcessedTableManager = + ProcessedTableManager< + _$AppDatabase, + $PendingShareActionsTable, + PendingShareAction, + $$PendingShareActionsTableFilterComposer, + $$PendingShareActionsTableOrderingComposer, + $$PendingShareActionsTableAnnotationComposer, + $$PendingShareActionsTableCreateCompanionBuilder, + $$PendingShareActionsTableUpdateCompanionBuilder, + ( + PendingShareAction, + BaseReferences< + _$AppDatabase, + $PendingShareActionsTable, + PendingShareAction + >, + ), + PendingShareAction, + PrefetchHooks Function() + >; typedef $$ProgramsTableCreateCompanionBuilder = ProgramsCompanion Function({ required String id, @@ -38964,6 +40482,277 @@ typedef $$RemoteResourceMappingsTableProcessedTableManager = RemoteResourceMapping, PrefetchHooks Function() >; +typedef $$ShareInboxItemsTableCreateCompanionBuilder = + ShareInboxItemsCompanion Function({ + required String shareId, + required String senderUserId, + required String resourceType, + required String payloadJson, + required String status, + required DateTime createdAt, + required DateTime updatedAt, + Value respondedAt, + Value rowid, + }); +typedef $$ShareInboxItemsTableUpdateCompanionBuilder = + ShareInboxItemsCompanion Function({ + Value shareId, + Value senderUserId, + Value resourceType, + Value payloadJson, + Value status, + Value createdAt, + Value updatedAt, + Value respondedAt, + Value rowid, + }); + +class $$ShareInboxItemsTableFilterComposer + extends Composer<_$AppDatabase, $ShareInboxItemsTable> { + $$ShareInboxItemsTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get shareId => $composableBuilder( + column: $table.shareId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get senderUserId => $composableBuilder( + column: $table.senderUserId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get resourceType => $composableBuilder( + column: $table.resourceType, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get payloadJson => $composableBuilder( + column: $table.payloadJson, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get status => $composableBuilder( + column: $table.status, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get respondedAt => $composableBuilder( + column: $table.respondedAt, + builder: (column) => ColumnFilters(column), + ); +} + +class $$ShareInboxItemsTableOrderingComposer + extends Composer<_$AppDatabase, $ShareInboxItemsTable> { + $$ShareInboxItemsTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get shareId => $composableBuilder( + column: $table.shareId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get senderUserId => $composableBuilder( + column: $table.senderUserId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get resourceType => $composableBuilder( + column: $table.resourceType, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get payloadJson => $composableBuilder( + column: $table.payloadJson, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get status => $composableBuilder( + column: $table.status, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get respondedAt => $composableBuilder( + column: $table.respondedAt, + builder: (column) => ColumnOrderings(column), + ); +} + +class $$ShareInboxItemsTableAnnotationComposer + extends Composer<_$AppDatabase, $ShareInboxItemsTable> { + $$ShareInboxItemsTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get shareId => + $composableBuilder(column: $table.shareId, builder: (column) => column); + + GeneratedColumn get senderUserId => $composableBuilder( + column: $table.senderUserId, + builder: (column) => column, + ); + + GeneratedColumn get resourceType => $composableBuilder( + column: $table.resourceType, + builder: (column) => column, + ); + + GeneratedColumn get payloadJson => $composableBuilder( + column: $table.payloadJson, + builder: (column) => column, + ); + + GeneratedColumn get status => + $composableBuilder(column: $table.status, builder: (column) => column); + + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + GeneratedColumn get updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => column); + + GeneratedColumn get respondedAt => $composableBuilder( + column: $table.respondedAt, + builder: (column) => column, + ); +} + +class $$ShareInboxItemsTableTableManager + extends + RootTableManager< + _$AppDatabase, + $ShareInboxItemsTable, + ShareInboxItem, + $$ShareInboxItemsTableFilterComposer, + $$ShareInboxItemsTableOrderingComposer, + $$ShareInboxItemsTableAnnotationComposer, + $$ShareInboxItemsTableCreateCompanionBuilder, + $$ShareInboxItemsTableUpdateCompanionBuilder, + ( + ShareInboxItem, + BaseReferences< + _$AppDatabase, + $ShareInboxItemsTable, + ShareInboxItem + >, + ), + ShareInboxItem, + PrefetchHooks Function() + > { + $$ShareInboxItemsTableTableManager( + _$AppDatabase db, + $ShareInboxItemsTable table, + ) : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$ShareInboxItemsTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$ShareInboxItemsTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$ShareInboxItemsTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value shareId = const Value.absent(), + Value senderUserId = const Value.absent(), + Value resourceType = const Value.absent(), + Value payloadJson = const Value.absent(), + Value status = const Value.absent(), + Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), + Value respondedAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => ShareInboxItemsCompanion( + shareId: shareId, + senderUserId: senderUserId, + resourceType: resourceType, + payloadJson: payloadJson, + status: status, + createdAt: createdAt, + updatedAt: updatedAt, + respondedAt: respondedAt, + rowid: rowid, + ), + createCompanionCallback: + ({ + required String shareId, + required String senderUserId, + required String resourceType, + required String payloadJson, + required String status, + required DateTime createdAt, + required DateTime updatedAt, + Value respondedAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => ShareInboxItemsCompanion.insert( + shareId: shareId, + senderUserId: senderUserId, + resourceType: resourceType, + payloadJson: payloadJson, + status: status, + createdAt: createdAt, + updatedAt: updatedAt, + respondedAt: respondedAt, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + ), + ); +} + +typedef $$ShareInboxItemsTableProcessedTableManager = + ProcessedTableManager< + _$AppDatabase, + $ShareInboxItemsTable, + ShareInboxItem, + $$ShareInboxItemsTableFilterComposer, + $$ShareInboxItemsTableOrderingComposer, + $$ShareInboxItemsTableAnnotationComposer, + $$ShareInboxItemsTableCreateCompanionBuilder, + $$ShareInboxItemsTableUpdateCompanionBuilder, + ( + ShareInboxItem, + BaseReferences<_$AppDatabase, $ShareInboxItemsTable, ShareInboxItem>, + ), + ShareInboxItem, + PrefetchHooks Function() + >; typedef $$SyncMetadataEntriesTableCreateCompanionBuilder = SyncMetadataEntriesCompanion Function({ Value id, @@ -43649,6 +45438,8 @@ class $AppDatabaseManager { $$ExerciseStepsTableTableManager(_db, _db.exerciseSteps); $$OnlineAccountSessionsTableTableManager get onlineAccountSessions => $$OnlineAccountSessionsTableTableManager(_db, _db.onlineAccountSessions); + $$PendingShareActionsTableTableManager get pendingShareActions => + $$PendingShareActionsTableTableManager(_db, _db.pendingShareActions); $$ProgramsTableTableManager get programs => $$ProgramsTableTableManager(_db, _db.programs); $$ProgramExercisesTableTableManager get programExercises => @@ -43658,6 +45449,8 @@ class $AppDatabaseManager { _db, _db.remoteResourceMappings, ); + $$ShareInboxItemsTableTableManager get shareInboxItems => + $$ShareInboxItemsTableTableManager(_db, _db.shareInboxItems); $$SyncMetadataEntriesTableTableManager get syncMetadataEntries => $$SyncMetadataEntriesTableTableManager(_db, _db.syncMetadataEntries); $$WorkoutHistoriesTableTableManager get workoutHistories => diff --git a/lib/infrastructure/local/drift_repositories.dart b/lib/infrastructure/local/drift_repositories.dart index e24f9ea..568c132 100644 --- a/lib/infrastructure/local/drift_repositories.dart +++ b/lib/infrastructure/local/drift_repositories.dart @@ -395,7 +395,40 @@ final class DriftLocalSyncChangeRepository ); return true; case SyncResourceType.program: + final program = _programFromPayload(item); + await database.transaction(() async { + await database + .into(database.programs) + .insertOnConflictUpdate(_programCompanion(program)); + for (final exercise in program.exercises) { + await database + .into(database.programExercises) + .insertOnConflictUpdate(_programExerciseCompanion(exercise)); + } + }); + return true; case SyncResourceType.workoutTemplate: + final template = _workoutTemplateFromPayload(item); + await database.transaction(() async { + await database + .into(database.workoutTemplates) + .insertOnConflictUpdate(_workoutTemplateCompanion(template)); + for (final program in template.programs) { + await database + .into(database.workoutTemplatePrograms) + .insertOnConflictUpdate( + _workoutTemplateProgramCompanion(program), + ); + } + for (final override in template.overrides) { + await database + .into(database.workoutTemplateExerciseOverrides) + .insertOnConflictUpdate( + _workoutTemplateExerciseOverrideCompanion(override), + ); + } + }); + return true; case SyncResourceType.workoutHistory: return false; } @@ -483,6 +516,132 @@ final class DriftLocalSyncChangeRepository } } +final class DriftShareInboxRepository implements ShareInboxRepository { + const DriftShareInboxRepository(this.database); + + final db.AppDatabase database; + + @override + Future> listAll() async { + final rows = await (database.select( + database.shareInboxItems, + )..orderBy([(table) => OrderingTerm.desc(table.createdAt)])).get(); + return rows.map(_shareInboxItemFromRow).toList(growable: false); + } + + @override + Future findByShareId(String shareId) async { + final row = await (database.select( + database.shareInboxItems, + )..where((table) => table.shareId.equals(shareId))).getSingleOrNull(); + return row == null ? null : _shareInboxItemFromRow(row); + } + + @override + Future upsert(domain.ShareInboxItem item) async { + await database + .into(database.shareInboxItems) + .insertOnConflictUpdate(_shareInboxItemCompanion(item)); + } + + @override + Future upsertAll(List items) async { + await database.batch((batch) { + batch.insertAllOnConflictUpdate( + database.shareInboxItems, + items.map(_shareInboxItemCompanion).toList(), + ); + }); + } + + @override + Future markStatus( + String shareId, + domain.ShareInboxStatus status, + DateTime respondedAt, + ) async { + await (database.update( + database.shareInboxItems, + )..where((table) => table.shareId.equals(shareId))).write( + db.ShareInboxItemsCompanion( + status: Value(_shareInboxStatusToDb(status)), + respondedAt: Value(respondedAt.toUtc()), + updatedAt: Value(respondedAt.toUtc()), + ), + ); + } +} + +final class DriftPendingShareActionRepository + implements PendingShareActionRepository { + const DriftPendingShareActionRepository(this.database); + + final db.AppDatabase database; + + @override + Future> listPending() async { + final rows = + await (database.select(database.pendingShareActions) + ..where( + (table) => table.status.isIn([ + _pendingShareActionStatusToDb( + domain.PendingShareActionStatus.pending, + ), + _pendingShareActionStatusToDb( + domain.PendingShareActionStatus.failed, + ), + ]), + ) + ..orderBy([(table) => OrderingTerm.asc(table.createdAt)])) + .get(); + return rows.map(_pendingShareActionFromRow).toList(growable: false); + } + + @override + Future add(domain.PendingShareAction action) async { + await database + .into(database.pendingShareActions) + .insertOnConflictUpdate(_pendingShareActionCompanion(action)); + } + + @override + Future markSucceeded(String id, DateTime attemptedAt) async { + await _markActionAttempt( + id, + attemptedAt, + domain.PendingShareActionStatus.succeeded, + ); + } + + @override + Future markFailed(String id, DateTime attemptedAt) async { + await _markActionAttempt( + id, + attemptedAt, + domain.PendingShareActionStatus.failed, + ); + } + + Future _markActionAttempt( + String id, + DateTime attemptedAt, + domain.PendingShareActionStatus status, + ) async { + final row = await (database.select( + database.pendingShareActions, + )..where((table) => table.id.equals(id))).getSingleOrNull(); + await (database.update( + database.pendingShareActions, + )..where((table) => table.id.equals(id))).write( + db.PendingShareActionsCompanion( + lastAttemptAt: Value(attemptedAt.toUtc()), + attemptCount: Value((row?.attemptCount ?? 0) + 1), + status: Value(_pendingShareActionStatusToDb(status)), + ), + ); + } +} + final class DriftProgramRepository implements ProgramRepository { const DriftProgramRepository(this.database); @@ -1846,6 +2005,74 @@ domain.UserAccountSession _userAccountSessionFromRow( ); } +db.ShareInboxItemsCompanion _shareInboxItemCompanion( + domain.ShareInboxItem item, +) { + final updatedAt = item.respondedAt ?? item.createdAt; + return db.ShareInboxItemsCompanion.insert( + shareId: item.shareId, + senderUserId: item.senderUserId, + resourceType: _shareResourceTypeToDb(item.resourceType), + payloadJson: item.payloadJson, + status: _shareInboxStatusToDb(item.status), + createdAt: item.createdAt.toUtc(), + updatedAt: updatedAt.toUtc(), + respondedAt: Value(_utcOrNull(item.respondedAt)), + ); +} + +domain.ShareInboxItem _shareInboxItemFromRow(db.ShareInboxItem row) { + return domain.ShareInboxItem( + shareId: row.shareId, + senderUserId: row.senderUserId, + resourceType: _shareResourceTypeFromDb(row.resourceType), + payloadJson: row.payloadJson, + status: _shareInboxStatusFromDb(row.status), + createdAt: _utc(row.createdAt), + respondedAt: _utcOrNull(row.respondedAt), + ); +} + +db.PendingShareActionsCompanion _pendingShareActionCompanion( + domain.PendingShareAction action, +) { + return db.PendingShareActionsCompanion.insert( + id: action.id, + actionType: _pendingShareActionTypeToDb(action.actionType), + shareId: Value(action.shareId), + resourceType: Value( + action.resourceType == null + ? null + : _shareResourceTypeToDb(action.resourceType!), + ), + payloadJson: Value(action.payloadJson), + recipientEmailsJson: Value(action.recipientEmailsJson), + createdAt: action.createdAt.toUtc(), + lastAttemptAt: Value(_utcOrNull(action.lastAttemptAt)), + attemptCount: action.attemptCount, + status: _pendingShareActionStatusToDb(action.status), + ); +} + +domain.PendingShareAction _pendingShareActionFromRow( + db.PendingShareAction row, +) { + return domain.PendingShareAction( + id: row.id, + actionType: _pendingShareActionTypeFromDb(row.actionType), + shareId: row.shareId, + resourceType: row.resourceType == null + ? null + : _shareResourceTypeFromDb(row.resourceType!), + payloadJson: row.payloadJson, + recipientEmailsJson: row.recipientEmailsJson, + createdAt: _utc(row.createdAt), + lastAttemptAt: _utcOrNull(row.lastAttemptAt), + attemptCount: row.attemptCount, + status: _pendingShareActionStatusFromDb(row.status), + ); +} + db.ProgramsCompanion _programCompanion(domain.Program program) { final values = _metadataValues(program.metadata); return db.ProgramsCompanion( @@ -2825,6 +3052,35 @@ domain.MediaAsset _mediaAssetFromPayload(RemoteSyncedItem item) { ); } +domain.Program _programFromPayload(RemoteSyncedItem item) { + final payload = item.payload; + final metadata = _metadataFromPayload(item); + return domain.Program( + metadata: metadata, + name: _stringFromPayload(payload, 'name', item.clientId), + defaultRestSeconds: payload['defaultRestSeconds'] as int? ?? 0, + exercises: _programExercisesFromPayload(payload['exercises'], metadata), + ); +} + +domain.WorkoutTemplate _workoutTemplateFromPayload(RemoteSyncedItem item) { + final payload = item.payload; + final metadata = _metadataFromPayload(item); + return domain.WorkoutTemplate( + metadata: metadata, + name: _stringFromPayload(payload, 'name', item.clientId), + lastStartedAt: _dateTimeFromPayload(payload['lastStartedAt']), + programs: _workoutTemplateProgramsFromPayload( + payload['programs'], + metadata, + ), + overrides: _workoutTemplateOverridesFromPayload( + payload['overrides'], + metadata, + ), + ); +} + domain.EntityMetadata _metadataFromPayload(RemoteSyncedItem item) { final metadata = item.payload['metadata']; final map = metadata is Map ? Map.from(metadata) : null; @@ -2842,6 +3098,153 @@ domain.EntityMetadata _metadataFromPayload(RemoteSyncedItem item) { ); } +List _programExercisesFromPayload( + Object? value, + domain.EntityMetadata parentMetadata, +) { + if (value is! List) { + return const []; + } + return value + .whereType() + .map((entry) { + final map = Map.from(entry); + final id = _stringFromPayload(map, 'id', 'program-exercise'); + return domain.ProgramExercise( + metadata: _childMetadataFromPayload(map, id, parentMetadata), + programId: parentMetadata.id, + sourceExerciseId: null, + position: map['position'] as int? ?? 0, + exerciseNameSnapshot: _stringFromPayload( + map, + 'exerciseNameSnapshot', + id, + ), + exerciseDescriptionSnapshot: + map['exerciseDescriptionSnapshot'] as String?, + exerciseImageMediaIdSnapshot: + map['exerciseImageMediaIdSnapshot'] as String?, + exerciseImageMediaIdsSnapshot: _stringListFromPayload( + map['exerciseImageMediaIdsSnapshot'] ?? + map['imageMediaIdsSnapshot'], + ), + exerciseVideoMediaIdSnapshot: + map['exerciseVideoMediaIdSnapshot'] as String?, + exerciseStepsSnapshot: _stepsFromPayload( + map['exerciseStepsSnapshot'], + ), + exerciseArchivedSnapshot: map['exerciseArchivedSnapshot'] == true, + availableTimeSnapshot: map['availableTimeSnapshot'] == true, + availableRepsSnapshot: map['availableRepsSnapshot'] == true, + availableScoreSnapshot: map['availableScoreSnapshot'] == true, + scoreInputModeSnapshot: _scoreInputModeFromDb( + map['scoreInputModeSnapshot'] as String? ?? 'manual', + ), + scoreLabelSnapshot: map['scoreLabelSnapshot'] as String?, + scoreUnitSnapshot: map['scoreUnitSnapshot'] as String?, + setsCount: map['setsCount'] as int? ?? 1, + timeEnabled: map['timeEnabled'] == true, + repsEnabled: map['repsEnabled'] == true, + scoreEnabled: map['scoreEnabled'] == true, + targetTimeSeconds: map['targetTimeSeconds'] as int?, + targetReps: map['targetReps'] as int?, + targetScore: (map['targetScore'] as num?)?.toDouble(), + targetScoreTimeMs: map['targetScoreTimeMs'] as int?, + restSecondsOverride: map['restSecondsOverride'] as int?, + ); + }) + .toList(growable: false); +} + +List _workoutTemplateProgramsFromPayload( + Object? value, + domain.EntityMetadata parentMetadata, +) { + if (value is! List) { + return const []; + } + return value + .whereType() + .map((entry) { + final map = Map.from(entry); + final id = _stringFromPayload(map, 'id', 'template-program'); + return domain.WorkoutTemplateProgram( + metadata: _childMetadataFromPayload(map, id, parentMetadata), + workoutTemplateId: parentMetadata.id, + sourceProgramId: null, + position: map['position'] as int? ?? 0, + programNameSnapshot: _stringFromPayload( + map, + 'programNameSnapshot', + id, + ), + defaultRestSecondsSnapshot: + map['defaultRestSecondsSnapshot'] as int? ?? 0, + programSnapshotJson: + map['programSnapshotJson'] as String? ?? '{"exercises":[]}', + ); + }) + .toList(growable: false); +} + +List +_workoutTemplateOverridesFromPayload( + Object? value, + domain.EntityMetadata parentMetadata, +) { + if (value is! List) { + return const []; + } + return value + .whereType() + .map((entry) { + final map = Map.from(entry); + final id = _stringFromPayload(map, 'id', 'template-override'); + return domain.WorkoutTemplateExerciseOverride( + metadata: _childMetadataFromPayload(map, id, parentMetadata), + workoutTemplateProgramId: _stringFromPayload( + map, + 'workoutTemplateProgramId', + '', + ), + snapshotProgramExerciseId: _stringFromPayload( + map, + 'snapshotProgramExerciseId', + '', + ), + setsCountOverride: map['setsCountOverride'] as int?, + targetTimeSecondsOverride: map['targetTimeSecondsOverride'] as int?, + targetRepsOverride: map['targetRepsOverride'] as int?, + targetScoreOverride: (map['targetScoreOverride'] as num?)?.toDouble(), + targetScoreTimeMsOverride: map['targetScoreTimeMsOverride'] as int?, + ); + }) + .toList(growable: false); +} + +domain.EntityMetadata _childMetadataFromPayload( + Map payload, + String id, + domain.EntityMetadata parentMetadata, +) { + final metadata = payload['metadata']; + final map = metadata is Map ? Map.from(metadata) : null; + return domain.EntityMetadata( + id: id, + createdAt: + _dateTimeFromPayload(map?['createdAt']) ?? parentMetadata.createdAt, + updatedAt: + _dateTimeFromPayload(map?['updatedAt']) ?? parentMetadata.updatedAt, + deletedAt: _dateTimeFromPayload(map?['deletedAt']), + schemaVersion: + map?['schemaVersion'] as int? ?? parentMetadata.schemaVersion, + syncState: parentMetadata.syncState, + localRevision: map?['localRevision'] as int? ?? 0, + originDeviceId: + map?['originDeviceId'] as String? ?? parentMetadata.originDeviceId, + ); +} + String _stringFromPayload( Map payload, String key, @@ -2965,6 +3368,67 @@ OnlineSyncStatus _onlineSyncStatusFromDb(String value) => switch (value) { _ => throw domain.DomainException('Unknown sync status: $value'), }; +String _shareResourceTypeToDb(domain.ShareResourceType type) => switch (type) { + domain.ShareResourceType.program => 'program', + domain.ShareResourceType.workoutTemplate => 'workoutTemplate', +}; + +domain.ShareResourceType _shareResourceTypeFromDb(String value) => + switch (value) { + 'program' => domain.ShareResourceType.program, + 'workoutTemplate' => domain.ShareResourceType.workoutTemplate, + _ => throw domain.DomainException('Unknown share resource type: $value'), + }; + +String _shareInboxStatusToDb(domain.ShareInboxStatus status) => + switch (status) { + domain.ShareInboxStatus.pending => 'pending', + domain.ShareInboxStatus.accepted => 'accepted', + domain.ShareInboxStatus.declined => 'declined', + domain.ShareInboxStatus.revoked => 'revoked', + }; + +domain.ShareInboxStatus _shareInboxStatusFromDb(String value) => + switch (value) { + 'pending' => domain.ShareInboxStatus.pending, + 'accepted' => domain.ShareInboxStatus.accepted, + 'declined' => domain.ShareInboxStatus.declined, + 'revoked' => domain.ShareInboxStatus.revoked, + _ => throw domain.DomainException('Unknown share inbox status: $value'), + }; + +String _pendingShareActionTypeToDb(domain.PendingShareActionType type) => + switch (type) { + domain.PendingShareActionType.send => 'send', + domain.PendingShareActionType.accept => 'accept', + domain.PendingShareActionType.decline => 'decline', + domain.PendingShareActionType.revoke => 'revoke', + }; + +domain.PendingShareActionType _pendingShareActionTypeFromDb(String value) => + switch (value) { + 'send' => domain.PendingShareActionType.send, + 'accept' => domain.PendingShareActionType.accept, + 'decline' => domain.PendingShareActionType.decline, + 'revoke' => domain.PendingShareActionType.revoke, + _ => throw domain.DomainException('Unknown share action type: $value'), + }; + +String _pendingShareActionStatusToDb(domain.PendingShareActionStatus status) => + switch (status) { + domain.PendingShareActionStatus.pending => 'pending', + domain.PendingShareActionStatus.succeeded => 'succeeded', + domain.PendingShareActionStatus.failed => 'failed', + }; + +domain.PendingShareActionStatus _pendingShareActionStatusFromDb(String value) => + switch (value) { + 'pending' => domain.PendingShareActionStatus.pending, + 'succeeded' => domain.PendingShareActionStatus.succeeded, + 'failed' => domain.PendingShareActionStatus.failed, + _ => throw domain.DomainException('Unknown share action status: $value'), + }; + String _activeStatusToDb(domain.ActiveWorkoutStatus status) => switch (status) { domain.ActiveWorkoutStatus.running => 'running', domain.ActiveWorkoutStatus.paused => 'paused', diff --git a/lib/infrastructure/local/tables.dart b/lib/infrastructure/local/tables.dart index 3e7f315..36d534d 100644 --- a/lib/infrastructure/local/tables.dart +++ b/lib/infrastructure/local/tables.dart @@ -92,6 +92,57 @@ class RemoteResourceMappings extends Table { ]; } +class ShareInboxItems extends Table { + @override + String get tableName => 'share_inbox_items'; + + TextColumn get shareId => text()(); + TextColumn get senderUserId => text().withLength(min: 1)(); + TextColumn get resourceType => text()(); + TextColumn get payloadJson => text().withLength(min: 1)(); + TextColumn get status => text()(); + DateTimeColumn get createdAt => dateTime()(); + DateTimeColumn get updatedAt => dateTime()(); + DateTimeColumn get respondedAt => dateTime().nullable()(); + + @override + Set get primaryKey => {shareId}; + + @override + List get customConstraints => [ + "CHECK (resource_type IN ('program', 'workoutTemplate'))", + "CHECK (status IN ('pending', 'accepted', 'declined', 'revoked'))", + ]; +} + +class PendingShareActions extends Table { + @override + String get tableName => 'pending_share_actions'; + + TextColumn get id => text()(); + TextColumn get actionType => text()(); + TextColumn get shareId => text().nullable()(); + TextColumn get resourceType => text().nullable()(); + TextColumn get payloadJson => text().nullable()(); + TextColumn get recipientEmailsJson => text().nullable()(); + DateTimeColumn get createdAt => dateTime()(); + DateTimeColumn get lastAttemptAt => dateTime().nullable()(); + IntColumn get attemptCount => + integer().customConstraint('NOT NULL CHECK (attempt_count >= 0)')(); + TextColumn get status => text()(); + + @override + Set get primaryKey => {id}; + + @override + List get customConstraints => [ + "CHECK (action_type IN ('send', 'accept', 'decline', 'revoke'))", + "CHECK (resource_type IS NULL OR resource_type IN " + "('program', 'workoutTemplate'))", + "CHECK (status IN ('pending', 'succeeded', 'failed'))", + ]; +} + class MediaAssets extends SyncableTable { @override String get tableName => 'media_assets'; diff --git a/lib/infrastructure/remote/remote.dart b/lib/infrastructure/remote/remote.dart index c6e54b6..eb3c0d7 100644 --- a/lib/infrastructure/remote/remote.dart +++ b/lib/infrastructure/remote/remote.dart @@ -1,3 +1,4 @@ export 'auth_api.dart'; export 'http_api_client.dart'; +export 'share_api.dart'; export 'sync_api.dart'; diff --git a/lib/infrastructure/remote/share_api.dart b/lib/infrastructure/remote/share_api.dart new file mode 100644 index 0000000..72df141 --- /dev/null +++ b/lib/infrastructure/remote/share_api.dart @@ -0,0 +1,188 @@ +import 'dart:convert'; + +import '../../application/application.dart'; +import '../../domain/domain.dart'; +import 'http_api_client.dart'; + +final class HttpRemoteShareApi implements RemoteShareApi { + const HttpRemoteShareApi(this.client); + + final HttpApiClient client; + + @override + Future sendShare({ + required ShareResourceType resourceType, + required Map payload, + required List recipientEmails, + required String token, + }) async { + final response = await client.postJson( + '/shares', + bearerToken: token, + body: { + 'resourceType': _shareResourceTypeToWire(resourceType), + 'payload': payload, + 'recipientEmails': recipientEmails, + }, + expectedStatuses: const {201}, + ); + return RemoteShareSendResult( + shareId: _requiredString(response, 'shareId'), + recipientUserIds: _stringList(response, 'recipientUserIds'), + unresolvedEmails: _stringList(response, 'unresolvedEmails'), + ); + } + + @override + Future> fetchInbox(String token) async { + final response = await client.getJson('/shares/inbox', bearerToken: token); + return _list( + response, + 'items', + ).map((item) => _inboxItemFromJson(_map(item))).toList(); + } + + @override + Future acceptShare(String shareId, String token) async { + final response = await client.postJson( + '/shares/$shareId/accept', + bearerToken: token, + ); + return _syncedItemFromJson(_map(response['createdResource'])); + } + + @override + Future declineShare(String shareId, String token) { + return client.postEmpty('/shares/$shareId/decline', bearerToken: token); + } + + @override + Future revokeShare(String shareId, String token) { + return client.postEmpty('/shares/$shareId/revoke', bearerToken: token); + } +} + +ShareInboxItem _inboxItemFromJson(Map json) { + return ShareInboxItem( + shareId: _requiredString(json, 'shareId'), + senderUserId: _requiredString(json, 'senderUserId'), + resourceType: _shareResourceTypeFromWire( + _requiredString(json, 'resourceType'), + ), + payloadJson: _jsonObjectString(json['payload']), + status: _shareInboxStatusFromWire(_requiredString(json, 'status')), + createdAt: _requiredDateTime(json, 'createdAt'), + respondedAt: _optionalDateTime(json, 'respondedAt'), + ); +} + +RemoteSyncedItem _syncedItemFromJson(Map json) { + return RemoteSyncedItem( + resourceType: _syncResourceTypeFromWire( + _requiredString(json, 'resourceType'), + ), + clientId: _requiredString(json, 'clientId'), + serverId: _requiredString(json, 'serverId'), + schemaVersion: _requiredInt(json, 'schemaVersion'), + clientUpdatedAt: _requiredDateTime(json, 'clientUpdatedAt'), + serverUpdatedAt: _requiredDateTime(json, 'serverUpdatedAt'), + deletedAt: _optionalDateTime(json, 'deletedAt'), + payload: _map(json['payload']), + ); +} + +String _shareResourceTypeToWire(ShareResourceType type) => switch (type) { + ShareResourceType.program => 'program', + ShareResourceType.workoutTemplate => 'workoutTemplate', +}; + +ShareResourceType _shareResourceTypeFromWire(String value) => switch (value) { + 'program' => ShareResourceType.program, + 'workoutTemplate' => ShareResourceType.workoutTemplate, + _ => throw RemoteAuthException( + RemoteAuthFailure.unknown, + 'Unknown share resource type: $value', + ), +}; + +ShareInboxStatus _shareInboxStatusFromWire(String value) => switch (value) { + 'pending' => ShareInboxStatus.pending, + 'accepted' => ShareInboxStatus.accepted, + 'declined' => ShareInboxStatus.declined, + 'revoked' => ShareInboxStatus.revoked, + _ => throw RemoteAuthException( + RemoteAuthFailure.unknown, + 'Unknown share status: $value', + ), +}; + +SyncResourceType _syncResourceTypeFromWire(String value) => switch (value) { + 'exercise' => SyncResourceType.exercise, + 'program' => SyncResourceType.program, + 'workoutTemplate' => SyncResourceType.workoutTemplate, + 'workoutHistory' => SyncResourceType.workoutHistory, + 'mediaAsset' => SyncResourceType.mediaAsset, + _ => throw RemoteAuthException( + RemoteAuthFailure.unknown, + 'Unknown sync resource type: $value', + ), +}; + +List _list(Map json, String key) { + final value = json[key]; + if (value is List) { + return value.cast(); + } + return const []; +} + +List _stringList(Map json, String key) { + return _list(json, key).whereType().toList(growable: false); +} + +Map _map(Object? value) { + if (value is Map) { + return Map.from(value); + } + throw const RemoteAuthException( + RemoteAuthFailure.unknown, + 'Expected JSON object.', + ); +} + +String _requiredString(Map json, String key) { + final value = json[key]; + if (value is String && value.trim().isNotEmpty) { + return value; + } + throw RemoteAuthException( + RemoteAuthFailure.unknown, + '$key is missing from share response.', + ); +} + +int _requiredInt(Map json, String key) { + final value = json[key]; + if (value is int) { + return value; + } + throw RemoteAuthException( + RemoteAuthFailure.unknown, + '$key is missing from share response.', + ); +} + +DateTime _requiredDateTime(Map json, String key) { + return DateTime.parse(_requiredString(json, key)).toUtc(); +} + +DateTime? _optionalDateTime(Map json, String key) { + final value = json[key]; + return value is String && value.trim().isNotEmpty + ? DateTime.parse(value).toUtc() + : null; +} + +String _jsonObjectString(Object? value) { + return jsonEncode(_map(value)); +} diff --git a/test/application/use_cases_test.dart b/test/application/use_cases_test.dart index 1c044ec..aaf241b 100644 --- a/test/application/use_cases_test.dart +++ b/test/application/use_cases_test.dart @@ -1361,6 +1361,118 @@ void main() { expect(metadataRepository.metadata.lastFailureAt, isNotNull); }); + test( + 'ShareUseCases sendShare returns neutral result without token', + () async { + final remoteShareApi = _FakeRemoteShareApi(); + final pendingRepository = _FakePendingShareActionRepository(); + final programRepository = _FakeProgramRepository() + ..programs.add(_shareableProgram()); + + final result = + await _shareUseCase( + tokenStore: _FakeAuthTokenStore(), + remoteShareApi: remoteShareApi, + pendingActionRepository: pendingRepository, + programRepository: programRepository, + ).sendShare( + resourceType: ShareResourceType.program, + localResourceId: 'program-1', + recipientEmails: const ['friend@example.com'], + ); + + expect(result.status, ShareSendStatus.notConnected); + expect(remoteShareApi.sendCalls, 0); + expect(pendingRepository.actions, isEmpty); + }, + ); + + test('ShareUseCases sendShare queues action when network fails', () async { + final remoteShareApi = _FakeRemoteShareApi() + ..exception = const RemoteAuthException(RemoteAuthFailure.network); + final pendingRepository = _FakePendingShareActionRepository(); + final programRepository = _FakeProgramRepository() + ..programs.add(_shareableProgram()); + + final result = + await _shareUseCase( + remoteShareApi: remoteShareApi, + pendingActionRepository: pendingRepository, + programRepository: programRepository, + ).sendShare( + resourceType: ShareResourceType.program, + localResourceId: 'program-1', + recipientEmails: const ['friend@example.com'], + ); + + expect(result.status, ShareSendStatus.queued); + expect( + pendingRepository.actions.single.actionType, + PendingShareActionType.send, + ); + expect( + pendingRepository.actions.single.resourceType, + ShareResourceType.program, + ); + expect(pendingRepository.actions.single.payloadJson, contains('Programme')); + }); + + test('ShareUseCases acceptShare imports returned resource locally', () async { + final remoteShareApi = _FakeRemoteShareApi() + ..acceptResult = _remoteSharedProgramItem(); + final localChanges = _FakeLocalSyncChangeRepository(); + final inboxRepository = _FakeShareInboxRepository() + ..items.add(_shareInboxItem(status: ShareInboxStatus.pending)); + + await _shareUseCase( + remoteShareApi: remoteShareApi, + localChanges: localChanges, + inboxRepository: inboxRepository, + ).acceptShare('share-1'); + + expect(localChanges.appliedItems.single.clientId, 'shared-program-1'); + expect(inboxRepository.items.single.status, ShareInboxStatus.accepted); + }); + + test('ShareUseCases refreshInbox updates the local cache', () async { + final remoteShareApi = _FakeRemoteShareApi() + ..inboxItems = [_shareInboxItem(status: ShareInboxStatus.pending)]; + final inboxRepository = _FakeShareInboxRepository(); + + final items = await _shareUseCase( + remoteShareApi: remoteShareApi, + inboxRepository: inboxRepository, + ).refreshInbox(); + + expect(items.single.shareId, 'share-1'); + expect(inboxRepository.items.single.senderUserId, 'sender-1'); + }); + + test('ShareUseCases decline and revoke update local statuses', () async { + final inboxRepository = _FakeShareInboxRepository() + ..items.add(_shareInboxItem(status: ShareInboxStatus.pending)) + ..items.add( + _shareInboxItem(shareId: 'share-2', status: ShareInboxStatus.pending), + ); + final useCase = _shareUseCase(inboxRepository: inboxRepository); + + await useCase.declineShare('share-1'); + await useCase.revokeShare('share-2'); + + expect( + inboxRepository.items + .singleWhere((item) => item.shareId == 'share-1') + .status, + ShareInboxStatus.declined, + ); + expect( + inboxRepository.items + .singleWhere((item) => item.shareId == 'share-2') + .status, + ShareInboxStatus.revoked, + ); + }); + test('score result enforces manual xor stopwatch values', () { expect( () => ActiveSetResult( @@ -1734,6 +1846,211 @@ RemoteSyncedItem _remoteExerciseItem({required DateTime clientUpdatedAt}) { ); } +ShareUseCases _shareUseCase({ + _FakeAuthTokenStore? tokenStore, + _FakeRemoteShareApi? remoteShareApi, + _FakeShareInboxRepository? inboxRepository, + _FakePendingShareActionRepository? pendingActionRepository, + _FakeLocalSyncChangeRepository? localChanges, + _FakeProgramRepository? programRepository, + _FakeWorkoutTemplateRepository? templateRepository, +}) { + return ShareUseCases( + tokenStore: tokenStore ?? (_FakeAuthTokenStore()..token = 'token-1'), + remoteShareApi: remoteShareApi ?? _FakeRemoteShareApi(), + inboxRepository: inboxRepository ?? _FakeShareInboxRepository(), + pendingActionRepository: + pendingActionRepository ?? _FakePendingShareActionRepository(), + localChanges: localChanges ?? _FakeLocalSyncChangeRepository(), + programRepository: programRepository ?? _FakeProgramRepository(), + templateRepository: templateRepository ?? _FakeWorkoutTemplateRepository(), + clock: _FakeClock(DateTime.utc(2026, 7, 17, 12)), + ids: _FakeIds(), + ); +} + +final class _FakeRemoteShareApi implements RemoteShareApi { + Exception? exception; + RemoteSyncedItem? acceptResult; + List inboxItems = const []; + var sendCalls = 0; + var acceptCalls = 0; + var declineCalls = 0; + var revokeCalls = 0; + + @override + Future acceptShare(String shareId, String token) async { + acceptCalls += 1; + final error = exception; + if (error != null) { + throw error; + } + return acceptResult ?? _remoteSharedProgramItem(); + } + + @override + Future declineShare(String shareId, String token) async { + declineCalls += 1; + final error = exception; + if (error != null) { + throw error; + } + } + + @override + Future> fetchInbox(String token) async { + final error = exception; + if (error != null) { + throw error; + } + return inboxItems; + } + + @override + Future revokeShare(String shareId, String token) async { + revokeCalls += 1; + final error = exception; + if (error != null) { + throw error; + } + } + + @override + Future sendShare({ + required ShareResourceType resourceType, + required Map payload, + required List recipientEmails, + required String token, + }) async { + sendCalls += 1; + final error = exception; + if (error != null) { + throw error; + } + return const RemoteShareSendResult( + shareId: 'share-1', + recipientUserIds: ['server-user-2'], + unresolvedEmails: [], + ); + } +} + +final class _FakeShareInboxRepository implements ShareInboxRepository { + final items = []; + + @override + Future findByShareId(String shareId) async { + return items.where((item) => item.shareId == shareId).firstOrNull; + } + + @override + Future> listAll() async => List.of(items); + + @override + Future markStatus( + String shareId, + ShareInboxStatus status, + DateTime respondedAt, + ) async { + final index = items.indexWhere((item) => item.shareId == shareId); + if (index == -1) { + return; + } + items[index] = items[index].copyWith( + status: status, + respondedAt: respondedAt, + ); + } + + @override + Future upsert(ShareInboxItem item) async { + items.removeWhere((existing) => existing.shareId == item.shareId); + items.add(item); + } + + @override + Future upsertAll(List items) async { + for (final item in items) { + await upsert(item); + } + } +} + +final class _FakePendingShareActionRepository + implements PendingShareActionRepository { + final actions = []; + + @override + Future add(PendingShareAction action) async { + actions.add(action); + } + + @override + Future> listPending() async { + return actions + .where((action) => action.status != PendingShareActionStatus.succeeded) + .toList(); + } + + @override + Future markFailed(String id, DateTime attemptedAt) async { + _mark(id, attemptedAt, PendingShareActionStatus.failed); + } + + @override + Future markSucceeded(String id, DateTime attemptedAt) async { + _mark(id, attemptedAt, PendingShareActionStatus.succeeded); + } + + void _mark(String id, DateTime attemptedAt, PendingShareActionStatus status) { + final index = actions.indexWhere((action) => action.id == id); + if (index == -1) { + return; + } + final action = actions[index]; + actions[index] = action.copyWith( + lastAttemptAt: attemptedAt, + attemptCount: action.attemptCount + 1, + status: status, + ); + } +} + +Program _shareableProgram() { + return Program( + metadata: _metadata('program-1'), + name: 'Programme', + defaultRestSeconds: 60, + ); +} + +ShareInboxItem _shareInboxItem({ + String shareId = 'share-1', + required ShareInboxStatus status, +}) { + return ShareInboxItem( + shareId: shareId, + senderUserId: 'sender-1', + resourceType: ShareResourceType.program, + payloadJson: '{"name":"Programme partagé"}', + status: status, + createdAt: DateTime.utc(2026, 7, 17, 12), + ); +} + +RemoteSyncedItem _remoteSharedProgramItem() { + return RemoteSyncedItem( + resourceType: SyncResourceType.program, + clientId: 'shared-program-1', + serverId: 'server-program-1', + schemaVersion: 1, + clientUpdatedAt: DateTime.utc(2026, 7, 17, 12), + serverUpdatedAt: DateTime.utc(2026, 7, 17, 12, 1), + deletedAt: null, + payload: const {'id': 'shared-program-1', 'name': 'Programme partagé'}, + ); +} + ExerciseUseCases _exerciseUseCase( _FakeExerciseRepository repository, { _FakeProgramRepository? programRepository, diff --git a/test/presentation/home_screen_test.dart b/test/presentation/home_screen_test.dart index ab5c238..1da6a49 100644 --- a/test/presentation/home_screen_test.dart +++ b/test/presentation/home_screen_test.dart @@ -146,6 +146,17 @@ final class _FakeBootstrap implements AppDependencies { clock: _FakeClock(DateTime.utc(2026, 7, 17, 12)), deviceId: 'device-1', ), + shareUseCases = ShareUseCases( + tokenStore: _FakeAuthTokenStore(), + remoteShareApi: _FakeRemoteShareApi(), + inboxRepository: _FakeShareInboxRepository(), + pendingActionRepository: _FakePendingShareActionRepository(), + localChanges: _FakeLocalSyncChangeRepository(), + programRepository: _FakeProgramRepository(), + templateRepository: _FakeWorkoutTemplateRepository(), + clock: _FakeClock(DateTime.utc(2026, 7, 17, 12)), + ids: _FakeIds(), + ), activeWorkoutSessionUseCases = ActiveWorkoutSessionUseCases( sessionRepository: activeRepository, templateRepository: _FakeWorkoutTemplateRepository(), @@ -207,6 +218,9 @@ final class _FakeBootstrap implements AppDependencies { @override final SyncUseCases syncUseCases; + @override + final ShareUseCases shareUseCases; + @override final ExerciseUseCases exerciseUseCases; @@ -390,6 +404,72 @@ final class _FakeLocalSyncChangeRepository ) async {} } +final class _FakeRemoteShareApi implements RemoteShareApi { + @override + Future acceptShare(String shareId, String token) async { + throw const RemoteAuthException(RemoteAuthFailure.network); + } + + @override + Future declineShare(String shareId, String token) async {} + + @override + Future> fetchInbox(String token) async => const []; + + @override + Future revokeShare(String shareId, String token) async {} + + @override + Future sendShare({ + required ShareResourceType resourceType, + required Map payload, + required List recipientEmails, + required String token, + }) async { + return const RemoteShareSendResult( + shareId: 'share-1', + recipientUserIds: [], + unresolvedEmails: [], + ); + } +} + +final class _FakeShareInboxRepository implements ShareInboxRepository { + @override + Future findByShareId(String shareId) async => null; + + @override + Future> listAll() async => const []; + + @override + Future markStatus( + String shareId, + ShareInboxStatus status, + DateTime respondedAt, + ) async {} + + @override + Future upsert(ShareInboxItem item) async {} + + @override + Future upsertAll(List items) async {} +} + +final class _FakePendingShareActionRepository + implements PendingShareActionRepository { + @override + Future add(PendingShareAction action) async {} + + @override + Future> listPending() async => const []; + + @override + Future markFailed(String id, DateTime attemptedAt) async {} + + @override + Future markSucceeded(String id, DateTime attemptedAt) async {} +} + final class _FakeActiveSessionRepository implements ActiveSessionRepository { ActiveWorkoutSession? session; final restStates = [];