import 'dart:convert'; import 'package:postgres/postgres.dart'; import '../../application/application.dart'; import '../../domain/domain.dart'; final class PostgresShareRepository implements ShareRepository { const PostgresShareRepository(this.connection); final Connection connection; @override Future insertShare({ required Share share, required List recipients, }) async { await connection.execute( Sql.named(''' INSERT INTO shares ( id, sender_user_id, resource_type, payload_json, created_at, revoked_at ) VALUES ( @id::uuid, @sender_user_id::uuid, @resource_type, @payload_json::jsonb, @created_at, @revoked_at ) '''), parameters: { 'id': share.id, 'sender_user_id': share.senderUserId, 'resource_type': share.resourceType.wireName, 'payload_json': jsonEncode(share.payloadJson), 'created_at': share.createdAt, 'revoked_at': share.revokedAt, }, ); for (final recipient in recipients) { await connection.execute( Sql.named(''' INSERT INTO share_recipients ( id, share_id, recipient_user_id, status, responded_at ) VALUES ( @id::uuid, @share_id::uuid, @recipient_user_id::uuid, @status, @responded_at ) ON CONFLICT (share_id, recipient_user_id) DO NOTHING '''), parameters: { 'id': recipient.id, 'share_id': recipient.shareId, 'recipient_user_id': recipient.recipientUserId, 'status': recipient.status.wireName, 'responded_at': recipient.respondedAt, }, ); } } @override Future> listInbox(String recipientUserId) async { final result = await connection.execute( Sql.named(''' SELECT s.id AS share_id, s.sender_user_id, s.resource_type, s.payload_json, s.created_at, s.revoked_at, sr.id AS recipient_id, sr.recipient_user_id, sr.status, sr.responded_at FROM share_recipients sr INNER JOIN shares s ON s.id = sr.share_id WHERE sr.recipient_user_id = @recipient_user_id::uuid ORDER BY s.created_at DESC, s.id DESC '''), parameters: {'recipient_user_id': recipientUserId}, ); return [ for (final row in result) _inboxItemFromValues(row.toColumnMap() as Map), ]; } @override Future findShareById(String shareId) async { final result = await connection.execute( Sql.named(''' SELECT id, sender_user_id, resource_type, payload_json, created_at, revoked_at FROM shares WHERE id = @id::uuid LIMIT 1 '''), parameters: {'id': shareId}, ); return result.isEmpty ? null : _shareFromValues(result.single.toColumnMap() as Map); } @override Future findRecipient({ required String shareId, required String recipientUserId, }) async { final result = await connection.execute( Sql.named(''' SELECT id, share_id, recipient_user_id, status, responded_at FROM share_recipients WHERE share_id = @share_id::uuid AND recipient_user_id = @recipient_user_id::uuid LIMIT 1 '''), parameters: {'share_id': shareId, 'recipient_user_id': recipientUserId}, ); return result.isEmpty ? null : _recipientFromValues( result.single.toColumnMap() as Map, ); } @override Future updateRecipientStatus({ required String recipientId, required ShareRecipientStatus status, required DateTime respondedAt, }) async { await connection.execute( Sql.named(''' UPDATE share_recipients SET status = @status, responded_at = @responded_at WHERE id = @id::uuid '''), parameters: { 'id': recipientId, 'status': status.wireName, 'responded_at': respondedAt.toUtc(), }, ); } @override Future revokeShare({ required String shareId, required DateTime revokedAt, }) async { await connection.execute( Sql.named(''' UPDATE shares SET revoked_at = @revoked_at WHERE id = @id::uuid AND revoked_at IS NULL '''), parameters: {'id': shareId, 'revoked_at': revokedAt.toUtc()}, ); } } ShareInboxItem _inboxItemFromValues(Map values) { return ShareInboxItem( share: _shareFromValues(values), recipient: _recipientFromValues(values), ); } Share _shareFromValues(Map values) { return Share( id: _stringValue(values['share_id'] ?? values['id']), senderUserId: _stringValue(values['sender_user_id']), resourceType: SyncedResourceType.parse( _stringValue(values['resource_type']), ), payloadJson: _payloadValue(values['payload_json']), createdAt: _dateTimeValue(values['created_at']), revokedAt: _nullableDateTimeValue(values['revoked_at']), ); } ShareRecipient _recipientFromValues(Map values) { return ShareRecipient( id: _stringValue(values['recipient_id'] ?? values['id']), shareId: _stringValue(values['share_id']), recipientUserId: _stringValue(values['recipient_user_id']), status: ShareRecipientStatus.parse(_stringValue(values['status'])), respondedAt: _nullableDateTimeValue(values['responded_at']), ); } Map _payloadValue(Object? value) { if (value is Map) { return value; } if (value is Map) { return Map.from(value); } if (value is String) { final decoded = jsonDecode(value); if (decoded is Map) { return decoded; } if (decoded is Map) { return Map.from(decoded); } } throw const FormatException('Expected JSON object payload.'); } String _stringValue(Object? value) { if (value == null) { throw const FormatException('Expected non-null string value.'); } return value.toString(); } DateTime _dateTimeValue(Object? value) { final result = _nullableDateTimeValue(value); if (result == null) { throw const FormatException('Expected non-null DateTime value.'); } return result; } DateTime? _nullableDateTimeValue(Object? value) { if (value == null) { return null; } if (value is DateTime) { return value.toUtc(); } return DateTime.parse(value.toString()).toUtc(); }