import 'package:clock/clock.dart'; import 'package:dio/dio.dart'; import 'package:relationship_saver/core/auth/token_store.dart'; import 'package:relationship_saver/core/network/dio_error_mapper.dart'; import 'package:relationship_saver/core/network/network_constants.dart'; import 'package:relationship_saver/integrations/backend/backend_gateway.dart'; import 'package:relationship_saver/integrations/backend/models/backend_models.dart'; import 'package:relationship_saver/integrations/backend/sync_envelope_validator.dart'; import 'package:uuid/uuid.dart'; /// REST implementation of [BackendGateway] backed by Dio. class BackendGatewayRest implements BackendGateway { BackendGatewayRest({ required Dio dio, required TokenStore tokenStore, Uuid? uuid, Clock? clock, this.deviceId = 'device-local', }) : _dio = dio, _tokenStore = tokenStore, _uuid = uuid ?? const Uuid(), _clock = clock ?? const Clock(); final Dio _dio; final TokenStore _tokenStore; final Uuid _uuid; final Clock _clock; final String deviceId; @override Future acknowledgeSignal({ required String signalId, required SignalAction action, DateTime? at, }) { return _guard(() async { final SignalAckRequest body = SignalAckRequest( action: action, at: at ?? _clock.now(), ); await _dio.post( '/v1/signals/$signalId/ack', data: body.toJson(), ); }); } @override Future getDownloadUrl({required String fileId}) { return _guard(() async { final Response response = await _dio.get( '/v1/files/$fileId', ); return DownloadUrl.fromJson(_asJsonMap(response.data)); }); } @override Future getSignalsFeed({ String? cursor, DateTime? since, int limit = 50, Set? personIds, }) { return _guard(() async { final Map query = {'limit': limit}; if (cursor != null && cursor.isNotEmpty) { query['cursor'] = cursor; } if (since != null) { query['since'] = since.toUtc().toIso8601String(); } if (personIds != null && personIds.isNotEmpty) { query['personId'] = personIds.toList(growable: false); } final Response response = await _dio.get( '/v1/signals', queryParameters: query, ); return SignalsFeed.fromJson(_asJsonMap(response.data)); }); } @override Future initUpload({ required UploadPurpose purpose, required String contentType, required int sizeBytes, }) { return _guard(() async { final UploadInitRequest request = UploadInitRequest( purpose: purpose, contentType: contentType, sizeBytes: sizeBytes, ); final Response response = await _dio.post( '/v1/uploads', data: request.toJson(), ); return UploadInitResult.fromJson(_asJsonMap(response.data)); }); } @override Future me() { return _guard(() async { final Response response = await _dio.get('/v1/me'); return UserProfile.fromJson(_asJsonMap(response.data)); }); } @override Future pullChanges({String? cursor, int limit = 200}) { return _guard(() async { final Map query = {'limit': limit}; if (cursor != null && cursor.isNotEmpty) { query['cursor'] = cursor; } final Response response = await _dio.get( '/v1/sync/pull', queryParameters: query, ); return SyncPullResult.fromJson(_asJsonMap(response.data)); }); } @override Future pushChanges({ required List changes, String? cursor, }) { return _guard(() async { SyncEnvelopeValidator.validateAll(changes); final String idempotencyKey = _uuid.v4(); final SyncPushRequest request = SyncPushRequest( deviceId: deviceId, cursor: cursor, changes: changes, idempotencyKey: idempotencyKey, ); final Response response = await _dio.post( '/v1/sync/push', data: request.toJson(), options: Options( headers: { NetworkConstants.idempotencyKeyHeader: idempotencyKey, }, ), ); return SyncPushResult.fromJson(_asJsonMap(response.data)); }); } @override Future signIn(SignInRequest request) { return _guard(() async { final Response response = await _dio.post( '/v1/auth/sign-in', data: request.toJson(), options: Options( extra: const { NetworkConstants.extraSkipAuth: true, NetworkConstants.extraSkipRefresh: true, }, ), ); final AuthSession session = AuthSession.fromJson( _asJsonMap(response.data), ); await _tokenStore.write(session); return session; }); } @override Future signOut() async { try { await _guard(() async { await _dio.post('/v1/auth/sign-out'); }); } finally { await _tokenStore.clear(); } } Future _guard(Future Function() run) async { try { return await run(); } on DioException catch (error) { throw mapDioException(error); } } Map _asJsonMap(dynamic value) { if (value is Map) { return value; } if (value is Map) { return value.map( (Object? key, Object? val) => MapEntry(key.toString(), val), ); } throw FormatException( 'Expected JSON object response, got ${value.runtimeType}', ); } }