214 lines
5.8 KiB
Dart
214 lines
5.8 KiB
Dart
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<void> 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<dynamic>(
|
|
'/v1/signals/$signalId/ack',
|
|
data: body.toJson(),
|
|
);
|
|
});
|
|
}
|
|
|
|
@override
|
|
Future<DownloadUrl> getDownloadUrl({required String fileId}) {
|
|
return _guard(() async {
|
|
final Response<dynamic> response = await _dio.get<dynamic>(
|
|
'/v1/files/$fileId',
|
|
);
|
|
return DownloadUrl.fromJson(_asJsonMap(response.data));
|
|
});
|
|
}
|
|
|
|
@override
|
|
Future<SignalsFeed> getSignalsFeed({
|
|
String? cursor,
|
|
DateTime? since,
|
|
int limit = 50,
|
|
Set<String>? personIds,
|
|
}) {
|
|
return _guard(() async {
|
|
final Map<String, dynamic> query = <String, dynamic>{'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<dynamic> response = await _dio.get<dynamic>(
|
|
'/v1/signals',
|
|
queryParameters: query,
|
|
);
|
|
return SignalsFeed.fromJson(_asJsonMap(response.data));
|
|
});
|
|
}
|
|
|
|
@override
|
|
Future<UploadInitResult> 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<dynamic> response = await _dio.post<dynamic>(
|
|
'/v1/uploads',
|
|
data: request.toJson(),
|
|
);
|
|
return UploadInitResult.fromJson(_asJsonMap(response.data));
|
|
});
|
|
}
|
|
|
|
@override
|
|
Future<UserProfile> me() {
|
|
return _guard(() async {
|
|
final Response<dynamic> response = await _dio.get<dynamic>('/v1/me');
|
|
return UserProfile.fromJson(_asJsonMap(response.data));
|
|
});
|
|
}
|
|
|
|
@override
|
|
Future<SyncPullResult> pullChanges({String? cursor, int limit = 200}) {
|
|
return _guard(() async {
|
|
final Map<String, dynamic> query = <String, dynamic>{'limit': limit};
|
|
if (cursor != null && cursor.isNotEmpty) {
|
|
query['cursor'] = cursor;
|
|
}
|
|
|
|
final Response<dynamic> response = await _dio.get<dynamic>(
|
|
'/v1/sync/pull',
|
|
queryParameters: query,
|
|
);
|
|
return SyncPullResult.fromJson(_asJsonMap(response.data));
|
|
});
|
|
}
|
|
|
|
@override
|
|
Future<SyncPushResult> pushChanges({
|
|
required List<ChangeEnvelope> 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<dynamic> response = await _dio.post<dynamic>(
|
|
'/v1/sync/push',
|
|
data: request.toJson(),
|
|
options: Options(
|
|
headers: <String, dynamic>{
|
|
NetworkConstants.idempotencyKeyHeader: idempotencyKey,
|
|
},
|
|
),
|
|
);
|
|
return SyncPushResult.fromJson(_asJsonMap(response.data));
|
|
});
|
|
}
|
|
|
|
@override
|
|
Future<AuthSession> signIn(SignInRequest request) {
|
|
return _guard(() async {
|
|
final Response<dynamic> response = await _dio.post<dynamic>(
|
|
'/v1/auth/sign-in',
|
|
data: request.toJson(),
|
|
options: Options(
|
|
extra: const <String, dynamic>{
|
|
NetworkConstants.extraSkipAuth: true,
|
|
NetworkConstants.extraSkipRefresh: true,
|
|
},
|
|
),
|
|
);
|
|
|
|
final AuthSession session = AuthSession.fromJson(
|
|
_asJsonMap(response.data),
|
|
);
|
|
await _tokenStore.write(session);
|
|
return session;
|
|
});
|
|
}
|
|
|
|
@override
|
|
Future<void> signOut() async {
|
|
try {
|
|
await _guard(() async {
|
|
await _dio.post<dynamic>('/v1/auth/sign-out');
|
|
});
|
|
} finally {
|
|
await _tokenStore.clear();
|
|
}
|
|
}
|
|
|
|
Future<T> _guard<T>(Future<T> Function() run) async {
|
|
try {
|
|
return await run();
|
|
} on DioException catch (error) {
|
|
throw mapDioException(error);
|
|
}
|
|
}
|
|
|
|
Map<String, dynamic> _asJsonMap(dynamic value) {
|
|
if (value is Map<String, dynamic>) {
|
|
return value;
|
|
}
|
|
if (value is Map<Object?, Object?>) {
|
|
return value.map<String, dynamic>(
|
|
(Object? key, Object? val) => MapEntry(key.toString(), val),
|
|
);
|
|
}
|
|
throw FormatException(
|
|
'Expected JSON object response, got ${value.runtimeType}',
|
|
);
|
|
}
|
|
}
|