Scaffold backend gateway and integration docs
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
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}',
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user