fix: missing deletedAt and isVisible columns on mobile (#26414)

* feat: SyncAssetV2

* feat: mobile sync handling

* feat: request correct sync object based on server version

* fix: mobile queries

* chore: sync sql

* fix: test

* chore: switch to mapper

* fix: sql sync
This commit is contained in:
Brandon Wees
2026-02-23 08:50:54 -06:00
committed by GitHub
parent a07d7b0c82
commit e633bc3f24
28 changed files with 9803 additions and 92 deletions
@@ -18,6 +18,7 @@ import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.da
import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart';
import 'package:immich_mobile/repositories/local_files_manager.repository.dart';
import 'package:immich_mobile/utils/semver.dart';
import 'package:mocktail/mocktail.dart';
import 'package:openapi/api.dart';
@@ -66,6 +67,7 @@ void main() {
TestWidgetsFlutterBinding.ensureInitialized();
debugDefaultTargetPlatformOverride = TargetPlatform.android;
registerFallbackValue(LocalAssetStub.image1);
registerFallbackValue(const SemVer(major: 2, minor: 5, patch: 0));
db = Drift(drift.DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true));
await StoreService.init(storeRepository: DriftStoreRepository(db));
@@ -94,11 +96,19 @@ void main() {
when(() => mockAbortCallbackWrapper()).thenReturn(false);
when(() => mockSyncApiRepo.streamChanges(any())).thenAnswer((invocation) async {
when(() => mockSyncApiRepo.streamChanges(any(), serverVersion: any(named: 'serverVersion'))).thenAnswer((
invocation,
) async {
handleEventsCallback = invocation.positionalArguments.first;
});
when(() => mockSyncApiRepo.streamChanges(any(), onReset: any(named: 'onReset'))).thenAnswer((invocation) async {
when(
() => mockSyncApiRepo.streamChanges(
any(),
onReset: any(named: 'onReset'),
serverVersion: any(named: 'serverVersion'),
),
).thenAnswer((invocation) async {
handleEventsCallback = invocation.positionalArguments.first;
});
@@ -106,9 +116,9 @@ void main() {
when(() => mockSyncApiRepo.deleteSyncAck(any())).thenAnswer((_) async => {});
when(() => mockApi.serverInfoApi).thenReturn(mockServerApi);
when(() => mockServerApi.getServerVersion()).thenAnswer(
(_) async => ServerVersionResponseDto(major: 1, minor: 132, patch_: 0),
);
when(
() => mockServerApi.getServerVersion(),
).thenAnswer((_) async => ServerVersionResponseDto(major: 1, minor: 132, patch_: 0));
when(() => mockSyncStreamRepo.updateUsersV1(any())).thenAnswer(successHandler);
when(() => mockSyncStreamRepo.deleteUsersV1(any())).thenAnswer(successHandler);
+4
View File
@@ -22,6 +22,7 @@ import 'schema_v16.dart' as v16;
import 'schema_v17.dart' as v17;
import 'schema_v18.dart' as v18;
import 'schema_v19.dart' as v19;
import 'schema_v20.dart' as v20;
class GeneratedHelper implements SchemaInstantiationHelper {
@override
@@ -65,6 +66,8 @@ class GeneratedHelper implements SchemaInstantiationHelper {
return v18.DatabaseAtV18(db);
case 19:
return v19.DatabaseAtV19(db);
case 20:
return v20.DatabaseAtV20(db);
default:
throw MissingSchemaException(version, versions);
}
@@ -90,5 +93,6 @@ class GeneratedHelper implements SchemaInstantiationHelper {
17,
18,
19,
20,
];
}
File diff suppressed because it is too large Load Diff
@@ -7,6 +7,7 @@ import 'package:immich_mobile/domain/models/sync_event.model.dart';
import 'package:immich_mobile/domain/services/store.service.dart';
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart';
import 'package:immich_mobile/utils/semver.dart';
import 'package:mocktail/mocktail.dart';
import 'package:openapi/api.dart';
@@ -72,8 +73,14 @@ void main() {
Future<void> streamChanges(
Future<void> Function(List<SyncEvent>, Function() abort, Function() reset) onDataCallback,
SemVer serverVersion,
) {
return sut.streamChanges(onDataCallback, batchSize: testBatchSize, httpClient: mockHttpClient);
return sut.streamChanges(
onDataCallback,
batchSize: testBatchSize,
httpClient: mockHttpClient,
serverVersion: serverVersion,
);
}
test('streamChanges stops processing stream when abort is called', () async {
@@ -94,7 +101,7 @@ void main() {
}
}
final streamChangesFuture = streamChanges(onDataCallback);
final streamChangesFuture = streamChanges(onDataCallback, const SemVer(major: 2, minor: 5, patch: 0));
// Give the stream subscription time to start (longer delay to account for mock delay)
await Future.delayed(const Duration(milliseconds: 50));
@@ -145,7 +152,7 @@ void main() {
}
}
final streamChangesFuture = streamChanges(onDataCallback);
final streamChangesFuture = streamChanges(onDataCallback, const SemVer(major: 2, minor: 5, patch: 0));
await Future.delayed(const Duration(milliseconds: 50));
@@ -197,7 +204,7 @@ void main() {
}
}
final streamChangesFuture = streamChanges(onDataCallback);
final streamChangesFuture = streamChanges(onDataCallback, const SemVer(major: 2, minor: 5, patch: 0));
await Future.delayed(const Duration(milliseconds: 50));
@@ -244,7 +251,7 @@ void main() {
onDataCallCount++;
}
final streamChangesFuture = streamChanges(onDataCallback);
final streamChangesFuture = streamChanges(onDataCallback, const SemVer(major: 2, minor: 5, patch: 0));
await Future.delayed(const Duration(milliseconds: 50));
@@ -271,7 +278,7 @@ void main() {
onDataCallCount++;
}
final future = streamChanges(onDataCallback);
final future = streamChanges(onDataCallback, const SemVer(major: 2, minor: 5, patch: 0));
errorBodyController.add(utf8.encode('{"error":"Unauthorized"}'));
await errorBodyController.close();