Compare commits

..

1 Commits

Author SHA1 Message Date
shenlong-tanwen e716448148 refactor: cleanup legacy store 2026-06-18 01:29:42 +05:30
28 changed files with 231 additions and 731 deletions
@@ -1,10 +1,10 @@
import 'dart:async';
import 'package:drift/drift.dart' show Value;
import 'package:drift/drift.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/models/app_metadata_key.dart';
import 'package:immich_mobile/domain/models/session.model.dart';
import 'package:immich_mobile/domain/utils/background_sync.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/main.dart' as app;
@@ -34,14 +34,14 @@ void main() {
server = await FakeImmichServer.start();
await ApiService().resolveAndSetEndpoint(server.endpoint);
await drift.delete(drift.userEntity).go();
await Store.delete(StoreKey.legacySyncMigrationStatus);
await (drift.appMetadataEntity.delete()..where((t) => t.key.equals(AppMetadataKey.syncMigrationStatus.name))).go();
});
tearDown(() async {
await workerManagerPatch.dispose();
await server.close();
await Store.delete(StoreKey.legacyServerEndpoint);
await Store.delete(StoreKey.legacySyncMigrationStatus);
await (drift.sessionEntity.delete()..where((t) => t.key.equals(SessionKey.serverEndpoint.name))).go();
await (drift.appMetadataEntity.delete()..where((t) => t.key.equals(AppMetadataKey.syncMigrationStatus.name))).go();
});
void sendUser(SyncStream stream, String id, String name) {
@@ -3,7 +3,7 @@ import 'dart:async';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/store.dart';
import 'package:immich_mobile/main.dart' as app;
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
import 'package:immich_mobile/utils/bootstrap.dart';
-86
View File
@@ -1,86 +0,0 @@
/// Key for each possible value in the `Store`.
/// Defines the data type for each value
enum StoreKey<T> {
deviceId<String>._(4),
// Legacy keys that have been migrated to the new metadata store
legacyVersion<int>._(0),
legacyManageLocalMediaAndroid<bool>._(137),
legacySyncMigrationStatus<String>._(1013),
legacyAdvancedTroubleshooting<bool>._(114),
legacyEnableHapticFeedback<bool>._(126),
legacyReadonlyModeEnabled<bool>._(138),
legacyServerUrl<String>._(10),
legacyAccessToken<String>._(11),
legacyServerEndpoint<String>._(12),
legacyBackupRequireCharging<bool>._(7),
legacyBackupTriggerDelay<int>._(8),
legacySyncAlbums<bool>._(131),
legacyEnableBackup<bool>._(1003),
legacyUseWifiForUploadVideos<bool>._(1004),
legacyUseWifiForUploadPhotos<bool>._(1005),
legacySelectedAlbumSortOrder<int>._(113),
legacySelectedAlbumSortReverse<bool>._(123),
legacyAlbumGridView<bool>._(140),
legacyAutoEndpointSwitching<bool>._(132),
legacyPreferredWifiName<String>._(133),
legacyLocalEndpoint<String>._(134),
legacyExternalEndpointList<String>._(135),
legacyCustomHeaders<String>._(127),
legacyLoopVideo<bool>._(117),
legacyLoadOriginalVideo<bool>._(136),
legacyAutoPlayVideo<bool>._(139),
legacyTapToNavigate<bool>._(141),
legacyPreferRemoteImage<bool>._(116),
legacyLoadOriginal<bool>._(101),
legacyPrimaryColor<String>._(128),
legacyDynamicTheme<bool>._(129),
legacyColorfulInterface<bool>._(130),
legacyThemeMode<String>._(102),
legacyCleanupKeepFavorites<bool>._(1008),
legacyCleanupKeepMediaType<int>._(1009),
legacyCleanupKeepAlbumIds<String>._(1010),
legacyCleanupCutoffDaysAgo<int>._(1011),
legacyCleanupDefaultsInitialized<bool>._(1012),
legacyTilesPerRow<int>._(103),
legacyGroupAssetsBy<int>._(105),
legacyStorageIndicator<bool>._(109),
legacyMapRelativeDate<int>._(119),
legacyMapShowFavoriteOnly<bool>._(118),
legacyMapIncludeArchived<bool>._(121),
legacyMapThemeMode<int>._(124),
legacyMapwithPartners<bool>._(125),
legacyLogLevel<int>._(115);
const StoreKey._(this.id);
final int id;
Type get type => T;
}
class StoreDto<T> {
final StoreKey<T> key;
final T? value;
const StoreDto(this.key, this.value);
@override
String toString() {
return '''
StoreDto: {
key: $key,
value: ${value ?? '<NA>'},
}''';
}
@override
bool operator ==(covariant StoreDto<T> other) {
if (identical(this, other)) {
return true;
}
return other.key == key && other.value == value;
}
@override
int get hashCode => key.hashCode ^ value.hashCode;
}
@@ -7,11 +7,11 @@ import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/services/log.service.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/platform_extensions.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
import 'package:immich_mobile/infrastructure/store.dart';
import 'package:immich_mobile/platform/background_worker_api.g.dart';
import 'package:immich_mobile/platform/background_worker_lock_api.g.dart';
import 'package:immich_mobile/providers/background_sync.provider.dart';
@@ -314,6 +314,6 @@ Future<void> backgroundSyncNativeEntrypoint() async {
WidgetsFlutterBinding.ensureInitialized();
DartPluginRegistrant.ensureInitialized();
final (drift, logDB) = await Bootstrap.initDomain(shouldBufferLogs: false, listenStoreUpdates: false);
final (drift, logDB) = await Bootstrap.initDomain(shouldBufferLogs: false);
await BackgroundWorkerBgService(drift: drift, driftLogger: logDB).init();
}
@@ -1,110 +0,0 @@
import 'dart:async';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
/// Provides access to a persistent key-value store with an in-memory cache.
/// Listens for repository changes to keep the cache updated.
class StoreService {
final DriftStoreRepository _storeRepository;
/// In-memory cache. Keys are [StoreKey.id]
final Map<int, Object?> _cache = {};
StreamSubscription<List<StoreDto>>? _storeUpdateSubscription;
StoreService._({required DriftStoreRepository isarStoreRepository}) : _storeRepository = isarStoreRepository;
// TODO: Temporary typedef to make minimal changes. Remove this and make the presentation layer access store through a provider
static StoreService? _instance;
static StoreService get I {
if (_instance == null) {
throw UnsupportedError("StoreService not initialized. Call init() first");
}
return _instance!;
}
// TODO: Replace the implementation with the one from create after removing the typedef
static Future<StoreService> init({required DriftStoreRepository storeRepository, bool listenUpdates = true}) async {
_instance ??= await create(storeRepository: storeRepository, listenUpdates: listenUpdates);
return _instance!;
}
static Future<StoreService> create({required DriftStoreRepository storeRepository, bool listenUpdates = true}) async {
final instance = StoreService._(isarStoreRepository: storeRepository);
await instance.populateCache();
if (listenUpdates) {
instance._storeUpdateSubscription = instance._listenForChange();
}
return instance;
}
Future<void> populateCache() async {
final storeValues = await _storeRepository.getAll();
for (StoreDto storeValue in storeValues) {
_cache[storeValue.key.id] = storeValue.value;
}
}
StreamSubscription<List<StoreDto>> _listenForChange() => _storeRepository.watchAll().listen((events) {
for (final event in events) {
_cache[event.key.id] = event.value;
}
});
/// Disposes the store and cancels the subscription. To reuse the store call init() again
Future<void> dispose() async {
await _storeUpdateSubscription?.cancel();
_storeUpdateSubscription = null;
_cache.clear();
// Allow a subsequent init() (e.g. when a worker isolate is reused) to
// create a fresh instance instead of returning this disposed one.
if (identical(_instance, this)) {
_instance = null;
}
}
/// Returns the cached value for [key], or `null`
T? tryGet<T>(StoreKey<T> key) => _cache[key.id] as T?;
/// Returns the stored value for [key] or [defaultValue].
/// Throws [StoreKeyNotFoundException] if value and [defaultValue] are null.
T get<T>(StoreKey<T> key, [T? defaultValue]) {
final value = tryGet(key) ?? defaultValue;
if (value == null) {
throw StoreKeyNotFoundException(key);
}
return value;
}
/// Stores the [value] for the [key]. Skips write if value hasn't changed.
Future<void> put<U extends StoreKey<T>, T>(U key, T value) async {
if (_cache[key.id] == value) {
return;
}
await _storeRepository.upsert(key, value);
_cache[key.id] = value;
}
/// Returns a stream that emits the value for [key] on change.
Stream<T?> watch<T>(StoreKey<T> key) => _storeRepository.watch(key);
/// Removes the value for [key]
Future<void> delete<T>(StoreKey<T> key) async {
await _storeRepository.delete(key);
_cache.remove(key.id);
}
/// Clears all values from the store (cache and DB)
Future<void> clear() async {
await _storeRepository.deleteAll();
_cache.clear();
}
}
class StoreKeyNotFoundException implements Exception {
final StoreKey key;
const StoreKeyNotFoundException(this.key);
@override
String toString() => "Key - <${key.name}> not available in Store";
}
-4
View File
@@ -1,4 +0,0 @@
import 'package:immich_mobile/domain/services/store.service.dart';
// ignore: non_constant_identifier_names
final Store = StoreService.I;
@@ -1,4 +1,3 @@
import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/models/app_metadata_key.dart';
import 'package:immich_mobile/infrastructure/entities/app_metadata.entity.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
@@ -20,8 +19,8 @@ class AppMetadataRepository {
.insertOnConflictUpdate(
AppMetadataEntityCompanion.insert(
key: key.name,
value: Value(key.encode(value)),
updatedAt: Value(DateTime.now()),
value: .new(key.encode(value)),
updatedAt: .new(DateTime.now()),
),
);
}
@@ -1,78 +0,0 @@
import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
class DriftStoreRepository extends DriftDatabaseRepository {
final Drift _db;
final validStoreKeys = StoreKey.values.map((e) => e.id).toSet();
DriftStoreRepository(super.db) : _db = db;
Future<bool> deleteAll() async {
await _db.storeEntity.deleteAll();
return true;
}
Future<List<StoreDto<Object>>> getAll() async {
final query = _db.storeEntity.select()..where((entity) => entity.id.isIn(validStoreKeys));
return query.asyncMap((entity) => _toUpdateEvent(entity)).get();
}
Stream<List<StoreDto<Object>>> watchAll() {
final query = _db.storeEntity.select()..where((entity) => entity.id.isIn(validStoreKeys));
return query.asyncMap((entity) => _toUpdateEvent(entity)).watch();
}
Future<void> delete<T>(StoreKey<T> key) async {
await _db.storeEntity.deleteWhere((entity) => entity.id.equals(key.id));
return;
}
Future<bool> upsert<T>(StoreKey<T> key, T value) async {
await _db.storeEntity.insertOnConflictUpdate(await _fromValue(key, value));
return true;
}
Future<T?> tryGet<T>(StoreKey<T> key) async {
final entity = await _db.managers.storeEntity.filter((entity) => entity.id.equals(key.id)).getSingleOrNull();
if (entity == null) {
return null;
}
return await _toValue(key, entity);
}
Stream<T?> watch<T>(StoreKey<T> key) async* {
final query = _db.storeEntity.select()..where((entity) => entity.id.equals(key.id));
yield* query.watchSingleOrNull().asyncMap((e) async => e == null ? null : await _toValue(key, e));
}
Future<StoreDto<Object>> _toUpdateEvent(StoreEntityData entity) async {
final key = StoreKey.values.firstWhere((e) => e.id == entity.id) as StoreKey<Object>;
final value = await _toValue(key, entity);
return StoreDto(key, value);
}
Future<T?> _toValue<T>(StoreKey<T> key, StoreEntityData entity) async =>
switch (key.type) {
const (int) => entity.intValue,
const (String) => entity.stringValue,
const (bool) => entity.intValue == 1,
const (DateTime) => entity.intValue == null ? null : DateTime.fromMillisecondsSinceEpoch(entity.intValue!),
_ => null,
}
as T?;
Future<StoreEntityCompanion> _fromValue<T>(StoreKey<T> key, T value) async {
final (int? intValue, String? strValue) = switch (key.type) {
const (int) => (value as int, null),
const (String) => (null, value as String),
const (bool) => ((value as bool) ? 1 : 0, null),
const (DateTime) => ((value as DateTime).millisecondsSinceEpoch, null),
_ => throw UnsupportedError("Unsupported primitive type: ${key.type} for key: ${key.name}"),
};
return StoreEntityCompanion(id: Value(key.id), intValue: Value(intValue), stringValue: Value(strValue));
}
}
+105
View File
@@ -0,0 +1,105 @@
import 'package:drift/drift.dart';
import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
enum LegacyStoreKey {
deviceId(4),
legacyVersion(0),
legacyManageLocalMediaAndroid(137),
legacySyncMigrationStatus(1013),
legacyAdvancedTroubleshooting(114),
legacyEnableHapticFeedback(126),
legacyReadonlyModeEnabled(138),
legacyServerUrl(10),
legacyAccessToken(11),
legacyServerEndpoint(12),
legacyBackupRequireCharging(7),
legacyBackupTriggerDelay(8),
legacySyncAlbums(131),
legacyEnableBackup(1003),
legacyUseWifiForUploadVideos(1004),
legacyUseWifiForUploadPhotos(1005),
legacySelectedAlbumSortOrder(113),
legacySelectedAlbumSortReverse(123),
legacyAlbumGridView(140),
legacyAutoEndpointSwitching(132),
legacyPreferredWifiName(133),
legacyLocalEndpoint(134),
legacyExternalEndpointList(135),
legacyCustomHeaders(127),
legacyLoopVideo(117),
legacyLoadOriginalVideo(136),
legacyAutoPlayVideo(139),
legacyTapToNavigate(141),
legacyPreferRemoteImage(116),
legacyLoadOriginal(101),
legacyPrimaryColor(128),
legacyDynamicTheme(129),
legacyColorfulInterface(130),
legacyThemeMode(102),
legacyCleanupKeepFavorites(1008),
legacyCleanupKeepMediaType(1009),
legacyCleanupKeepAlbumIds(1010),
legacyCleanupCutoffDaysAgo(1011),
legacyCleanupDefaultsInitialized(1012),
legacyTilesPerRow(103),
legacyGroupAssetsBy(105),
legacyStorageIndicator(109),
legacyMapRelativeDate(119),
legacyMapShowFavoriteOnly(118),
legacyMapIncludeArchived(121),
legacyMapThemeMode(124),
legacyMapwithPartners(125),
legacyLogLevel(115);
const LegacyStoreKey(this.id);
final int id;
}
class DeviceIdStore {
DeviceIdStore._(this._db);
final Drift _db;
String? _deviceId;
static DeviceIdStore? _instance;
static DeviceIdStore get I => _instance ?? (throw StateError('DeviceIdStore not initialized. Call init() first'));
static Future<DeviceIdStore> init(Drift db) async {
final instance = DeviceIdStore._(db);
final row = await (db.storeEntity.select()..where((t) => t.id.equals(LegacyStoreKey.deviceId.id)))
.getSingleOrNull();
instance._deviceId = row?.stringValue;
return _instance = instance;
}
String? get deviceId => _deviceId;
String get requireDeviceId => _deviceId ?? (throw StateError('deviceId not set'));
Future<void> setDeviceId(String value) async {
if (_deviceId == value) {
return;
}
await _db.storeEntity.insertOnConflictUpdate(
StoreEntityCompanion(id: Value(LegacyStoreKey.deviceId.id), stringValue: Value(value)),
);
_deviceId = value;
}
Future<void> clear() async {
await _db.storeEntity.deleteAll();
_deviceId = null;
}
Future<void> dispose() async {
_deviceId = null;
if (identical(_instance, this)) {
_instance = null;
}
}
}
// ignore: non_constant_identifier_names
DeviceIdStore get Store => DeviceIdStore.I;
+3 -4
View File
@@ -4,10 +4,9 @@ import 'package:flutter_udid/flutter_udid.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/models/session.model.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/domain/services/user.service.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/store.dart';
import 'package:immich_mobile/models/auth/auth_state.model.dart';
import 'package:immich_mobile/models/auth/login_response.model.dart';
import 'package:immich_mobile/providers/api.provider.dart';
@@ -136,7 +135,7 @@ class AuthNotifier extends StateNotifier<AuthState> {
await _widgetService.writeCredentials(serverEndpoint, accessToken, customHeaders);
// Get the deviceid from the store if it exists, otherwise generate a new one
String deviceId = Store.tryGet(StoreKey.deviceId) ?? await FlutterUdid.consistentUdid;
String deviceId = Store.deviceId ?? await FlutterUdid.consistentUdid;
UserDto? user = await _userService.tryGetMyUser();
@@ -148,7 +147,7 @@ class AuthNotifier extends StateNotifier<AuthState> {
// If the user information is successfully retrieved, update the store
// Due to the flow of the code, this will always happen on first login
user = serverUser;
await Store.put(StoreKey.deviceId, deviceId);
await Store.setDeviceId(deviceId);
}
} on ApiException catch (error, stackTrace) {
if (error.code == 401) {
@@ -1,4 +0,0 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/services/store.service.dart';
final storeServiceProvider = Provider((_) => StoreService.I);
@@ -8,14 +8,13 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/models/asset/asset_metadata.model.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/platform_extensions.dart';
import 'package:immich_mobile/infrastructure/repositories/backup.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/session.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
import 'package:immich_mobile/infrastructure/store.dart';
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
import 'package:immich_mobile/providers/infrastructure/storage.provider.dart';
import 'package:immich_mobile/repositories/asset_media.repository.dart';
@@ -390,7 +389,7 @@ class BackgroundUploadService {
final serverEndpoint = SessionRepository.instance.session.serverEndpoint!;
final url = Uri.parse('$serverEndpoint/assets').toString();
final headers = ApiService.getRequestHeaders();
final deviceId = Store.get(StoreKey.deviceId);
final deviceId = Store.requireDeviceId;
final (baseDirectory, directory, filename) = await Task.split(filePath: file.path);
final fieldsMap = {
'filename': originalFileName ?? filename,
@@ -5,14 +5,13 @@ import 'dart:io';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/asset/asset_metadata.model.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/network_capability_extensions.dart';
import 'package:immich_mobile/extensions/platform_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/infrastructure/repositories/backup.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
import 'package:immich_mobile/infrastructure/store.dart';
import 'package:immich_mobile/platform/connectivity_api.g.dart';
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
import 'package:immich_mobile/providers/infrastructure/storage.provider.dart';
@@ -317,7 +316,7 @@ class ForegroundUploadService {
}
final originalFileName = entity.isLivePhoto ? p.setExtension(fileName, p.extension(file.path)) : fileName;
final deviceId = Store.get(StoreKey.deviceId);
final deviceId = Store.requireDeviceId;
final fields = {
// deviceAssetId/deviceId required by server v2.7.5 and below (drop in v4.0 per #27818).
@@ -430,7 +429,7 @@ class ForegroundUploadService {
final fields = {
// deviceAssetId/deviceId required by server v2.7.5 and below (drop in v4.0 per #27818).
'deviceAssetId': deviceAssetId,
'deviceId': Store.get(StoreKey.deviceId),
'deviceId': Store.requireDeviceId,
'fileCreatedAt': fileCreatedAt.toUtc().toIso8601String(),
'fileModifiedAt': fileModifiedAt.toUtc().toIso8601String(),
'isFavorite': 'false',
+3 -5
View File
@@ -1,7 +1,6 @@
import 'package:background_downloader/background_downloader.dart';
import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/services/log.service.dart';
import 'package:immich_mobile/domain/services/store.service.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/log.repository.dart';
@@ -9,7 +8,7 @@ import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.d
import 'package:immich_mobile/infrastructure/repositories/session.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/network.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
import 'package:immich_mobile/infrastructure/store.dart';
import 'package:photo_manager/photo_manager.dart';
void configureFileDownloaderNotifications() {
@@ -43,14 +42,13 @@ void configureFileDownloaderNotifications() {
}
abstract final class Bootstrap {
static Future<(Drift, DriftLogger)> initDomain({bool listenStoreUpdates = true, bool shouldBufferLogs = true}) async {
static Future<(Drift, DriftLogger)> initDomain({bool shouldBufferLogs = true}) async {
await configureSqliteCache();
final (db, updatePool) = await openSqliteConnectionWithUpdatePool(name: 'immich');
final drift = Drift.sqlite(db, updatePool);
final logDb = DriftLogger.sqlite(await openSqliteConnection(name: 'immich_logs'));
final DriftStoreRepository storeRepo = DriftStoreRepository(drift);
await StoreService.init(storeRepository: storeRepo, listenUpdates: listenStoreUpdates);
await DeviceIdStore.init(drift);
await SessionRepository.ensureInitialized(drift);
+2 -2
View File
@@ -4,7 +4,7 @@ import 'dart:ui';
import 'package:flutter/services.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/services/log.service.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/store.dart';
import 'package:immich_mobile/providers/infrastructure/cancel.provider.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
import 'package:immich_mobile/utils/bootstrap.dart';
@@ -34,7 +34,7 @@ Cancelable<T?> runInIsolateGentle<T>({
DartPluginRegistrant.ensureInitialized();
final log = Logger("IsolateLogger");
final (drift, logDb) = await Bootstrap.initDomain(shouldBufferLogs: false, listenStoreUpdates: false);
final (drift, logDb) = await Bootstrap.initDomain(shouldBufferLogs: false);
final ref = ProviderContainer(
overrides: [cancellationProvider.overrideWithValue(onCancel), driftProvider.overrideWith(driftOverride(drift))],
);
+85 -77
View File
@@ -11,9 +11,7 @@ import 'package:immich_mobile/domain/models/config/app_config.dart';
import 'package:immich_mobile/domain/models/log.model.dart';
import 'package:immich_mobile/domain/models/session.model.dart';
import 'package:immich_mobile/domain/models/settings_key.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/models/timeline.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/entities/app_metadata.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/session.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/settings.entity.drift.dart';
@@ -22,54 +20,59 @@ import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/network.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/session.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
import 'package:immich_mobile/infrastructure/store.dart';
import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart';
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
Future<void> migrateDatabaseIfNeeded(Drift drift) async {
final metadataRepository = AppMetadataRepository(drift);
final int version = await metadataRepository.get(AppMetadataKey.version);
if (version < 25) {
await _migrateTo25();
}
if (version < 26) {
await _migrateTo26(drift);
}
if (version < 27) {
await _migrateTo27(drift);
}
if (version < 28) {
await _migrateTo28(drift);
}
if (version < 29) {
await _migrateTo29(drift);
final legacyStore = await _readLegacyStore(drift);
if (version < 25) {
await _migrateTo25(drift, legacyStore);
}
if (version < 26) {
await _migrateTo26(drift, legacyStore);
}
if (version < 27) {
await _migrateTo27(drift, legacyStore);
}
if (version < 28) {
await _migrateTo28(drift, legacyStore);
}
await _migrateTo29(drift, legacyStore);
}
await metadataRepository.set(AppMetadataKey.version, kCurrentVersion);
return;
}
Future<void> _migrateTo25() async {
final accessToken = Store.tryGet(.legacyAccessToken);
Future<Map<int, Object?>> _readLegacyStore(Drift drift) async {
final rows = await drift.storeEntity.select().get();
return {for (final row in rows) row.id: row.stringValue ?? row.intValue};
}
Future<void> _migrateTo25(Drift drift, Map<int, Object?> legacyStore) async {
final migrator = _StoreMigrator.settings(drift, legacyStore);
final accessToken = migrator.readLegacyStoreString(.legacyAccessToken);
if (accessToken == null || accessToken.isEmpty) {
return;
}
final urls = <String>[];
final serverEndpoint = Store.tryGet(.legacyServerEndpoint);
final serverEndpoint = migrator.readLegacyStoreString(.legacyServerEndpoint);
if (serverEndpoint != null && serverEndpoint.isNotEmpty) {
urls.add(serverEndpoint);
}
final localEndpoint = Store.tryGet(.legacyLocalEndpoint);
final localEndpoint = migrator.readLegacyStoreString(.legacyLocalEndpoint);
if (localEndpoint != null && localEndpoint.isNotEmpty) {
urls.add(localEndpoint);
}
final externalJson = Store.tryGet(.legacyExternalEndpointList);
final externalJson = migrator.readLegacyStoreString(.legacyExternalEndpointList);
if (externalJson != null) {
final List<dynamic> list = jsonDecode(externalJson);
for (final entry in list) {
@@ -83,7 +86,7 @@ Future<void> _migrateTo25() async {
return;
}
final customHeadersStr = Store.get(.legacyCustomHeaders, "");
final customHeadersStr = migrator.readLegacyStoreString(.legacyCustomHeaders) ?? "";
final headers = customHeadersStr.isEmpty
? const <String, String>{}
: (jsonDecode(customHeadersStr) as Map).cast<String, String>();
@@ -91,8 +94,8 @@ Future<void> _migrateTo25() async {
await NetworkRepository.setHeaders(headers, urls, token: accessToken);
}
Future<void> _migrateTo26(Drift drift) async {
final migrator = _StoreMigrator.settings(drift);
Future<void> _migrateTo26(Drift drift, Map<int, Object?> legacyStore) async {
final migrator = _StoreMigrator.settings(drift, legacyStore);
await migrator.migrateEnumIndex(.legacyLogLevel, .logLevel, LogLevel.values);
// Theme
await migrator.migrateEnumName(.legacyThemeMode, .themeMode, ThemeMode.values);
@@ -100,7 +103,7 @@ Future<void> _migrateTo26(Drift drift) async {
await migrator.migrateBool(.legacyDynamicTheme, .themeDynamic);
await migrator.migrateBool(.legacyColorfulInterface, .themeColorfulInterface);
// Cleanup
final cleanupKeepAlbumIds = await migrator.readLegacyStoreString(.legacyCleanupKeepAlbumIds);
final cleanupKeepAlbumIds = migrator.readLegacyStoreString(.legacyCleanupKeepAlbumIds);
if (cleanupKeepAlbumIds != null) {
final ids = cleanupKeepAlbumIds.split(',').where((id) => id.isNotEmpty).toList();
migrator.stage(.legacyCleanupKeepAlbumIds, .cleanupKeepAlbumIds, ids);
@@ -129,9 +132,9 @@ Future<void> _migrateTo26(Drift drift) async {
await migrator.migrateBool(.legacyTapToNavigate, .viewerTapToNavigate);
// Network
await migrator.migrateBool(.legacyAutoEndpointSwitching, .networkAutoEndpointSwitching);
final preferredWifiName = await migrator.readLegacyStoreString(.legacyPreferredWifiName);
final preferredWifiName = migrator.readLegacyStoreString(.legacyPreferredWifiName);
migrator.stage(.legacyPreferredWifiName, .networkPreferredWifiName, preferredWifiName);
final localEndpoint = await migrator.readLegacyStoreString(.legacyLocalEndpoint);
final localEndpoint = migrator.readLegacyStoreString(.legacyLocalEndpoint);
migrator.stage(.legacyLocalEndpoint, .networkLocalEndpoint, localEndpoint);
await _migrateExternalEndpointList(migrator);
await _migrateCustomHeaders(migrator);
@@ -149,8 +152,8 @@ Future<void> _migrateTo26(Drift drift) async {
await migrator.complete();
}
Future<void> _migrateTo27(Drift drift) async {
final migrator = _StoreMigrator.session(drift);
Future<void> _migrateTo27(Drift drift, Map<int, Object?> legacyStore) async {
final migrator = _StoreMigrator.session(drift, legacyStore);
await migrator.migrateString(.legacyServerUrl, .serverUrl);
await migrator.migrateString(.legacyAccessToken, .accessToken);
await migrator.migrateString(.legacyServerEndpoint, .serverEndpoint);
@@ -159,8 +162,8 @@ Future<void> _migrateTo27(Drift drift) async {
await SessionRepository.instance.refresh();
}
Future<void> _migrateTo28(Drift drift) async {
final migrator = _StoreMigrator.settings(drift);
Future<void> _migrateTo28(Drift drift, Map<int, Object?> legacyStore) async {
final migrator = _StoreMigrator.settings(drift, legacyStore);
await migrator.migrateBool(.legacyAdvancedTroubleshooting, .advancedTroubleshooting);
await migrator.migrateBool(.legacyEnableHapticFeedback, .advancedEnableHapticFeedback);
await migrator.migrateBool(.legacyReadonlyModeEnabled, .advancedReadonlyModeEnabled);
@@ -169,10 +172,10 @@ Future<void> _migrateTo28(Drift drift) async {
await SettingsRepository.instance.refresh();
}
Future<void> _migrateTo29(Drift drift) async {
final migrator = _StoreMigrator.appMetadata(drift);
Future<void> _migrateTo29(Drift drift, Map<int, Object?> legacyStore) async {
final migrator = _StoreMigrator.appMetadata(drift, legacyStore);
final rawStatus = await migrator.readLegacyStoreString(.legacySyncMigrationStatus);
final rawStatus = migrator.readLegacyStoreString(.legacySyncMigrationStatus);
if (rawStatus != null) {
final decoded = jsonDecode(rawStatus);
final migrations = decoded is List ? decoded.whereType<String>().toList() : <String>[];
@@ -184,7 +187,7 @@ Future<void> _migrateTo29(Drift drift) async {
}
Future<void> _migrateAlbumSortMode(_StoreMigrator<SettingsKey> migrator) async {
final raw = await migrator.readLegacyStoreInt(.legacySelectedAlbumSortOrder);
final raw = migrator.readLegacyStoreInt(.legacySelectedAlbumSortOrder);
final mode = AlbumSortMode.values.firstWhereOrNull((e) => raw != null && e.storeIndex == raw);
if (mode == null) {
return;
@@ -194,7 +197,7 @@ Future<void> _migrateAlbumSortMode(_StoreMigrator<SettingsKey> migrator) async {
}
Future<void> _migrateExternalEndpointList(_StoreMigrator<SettingsKey> migrator) async {
final raw = await migrator.readLegacyStoreString(.legacyExternalEndpointList);
final raw = migrator.readLegacyStoreString(.legacyExternalEndpointList);
if (raw == null) {
return;
}
@@ -214,11 +217,11 @@ Future<void> _migrateExternalEndpointList(_StoreMigrator<SettingsKey> migrator)
// ignore invalid entries
}
migrator.stage(StoreKey.legacyExternalEndpointList, SettingsKey.networkExternalEndpointList, urls);
migrator.stage(.legacyExternalEndpointList, SettingsKey.networkExternalEndpointList, urls);
}
Future<void> _migrateCustomHeaders(_StoreMigrator<SettingsKey> migrator) async {
final raw = await migrator.readLegacyStoreString(.legacyCustomHeaders);
final raw = migrator.readLegacyStoreString(.legacyCustomHeaders);
if (raw == null) {
return;
}
@@ -237,14 +240,21 @@ Future<void> _migrateCustomHeaders(_StoreMigrator<SettingsKey> migrator) async {
// ignore invalid entries
}
migrator.stage(StoreKey.legacyCustomHeaders, SettingsKey.networkCustomHeaders, headers);
migrator.stage(.legacyCustomHeaders, SettingsKey.networkCustomHeaders, headers);
}
class _StoreMigrator<K extends Enum> {
_StoreMigrator._(this._db, {required this.encode, required this.readDefault, required this.insertRow});
_StoreMigrator._(
this._db,
this._legacyStore, {
required this.encode,
required this.readDefault,
required this.insertRow,
});
static _StoreMigrator<SettingsKey> settings(Drift db) => _StoreMigrator<SettingsKey>._(
static _StoreMigrator<SettingsKey> settings(Drift db, Map<int, Object?> legacyStore) => _StoreMigrator<SettingsKey>._(
db,
legacyStore,
encode: (key, value) => key.encode(value),
readDefault: (key) => defaultConfig.read(key),
insertRow: (batch, name, value) => batch.insert(
@@ -254,8 +264,9 @@ class _StoreMigrator<K extends Enum> {
),
);
static _StoreMigrator<SessionKey> session(Drift db) => _StoreMigrator<SessionKey>._(
static _StoreMigrator<SessionKey> session(Drift db, Map<int, Object?> legacyStore) => _StoreMigrator<SessionKey>._(
db,
legacyStore,
encode: (key, value) => key.encode(value),
readDefault: (key) => defaultSession.read(key),
insertRow: (batch, name, value) => batch.insert(
@@ -265,26 +276,29 @@ class _StoreMigrator<K extends Enum> {
),
);
static _StoreMigrator<AppMetadataKey> appMetadata(Drift db) => _StoreMigrator<AppMetadataKey>._(
db,
encode: (key, value) => key.encode(value),
readDefault: (_) => null,
insertRow: (batch, name, value) => batch.insert(
db.appMetadataEntity,
AppMetadataEntityCompanion(key: Value(name), value: Value(value)),
mode: InsertMode.insertOrReplace,
),
);
static _StoreMigrator<AppMetadataKey> appMetadata(Drift db, Map<int, Object?> legacyStore) =>
_StoreMigrator<AppMetadataKey>._(
db,
legacyStore,
encode: (key, value) => key.encode(value),
readDefault: (_) => null,
insertRow: (batch, name, value) => batch.insert(
db.appMetadataEntity,
AppMetadataEntityCompanion(key: Value(name), value: Value(value)),
mode: InsertMode.insertOrReplace,
),
);
final Drift _db;
final Map<int, Object?> _legacyStore;
final String Function(K key, Object value) encode;
final Object? Function(K key) readDefault;
final void Function(Batch batch, String name, String? value) insertRow;
final Map<K, Object?> _cache = {};
final List<int> _migratedStoreIds = [];
Future<void> migrateEnumIndex<T extends Enum>(StoreKey<int> legacyKey, K newKey, List<T> values) async {
final index = await readLegacyStoreInt(legacyKey);
Future<void> migrateEnumIndex<T extends Enum>(LegacyStoreKey legacyKey, K newKey, List<T> values) async {
final index = readLegacyStoreInt(legacyKey);
if (index == null) {
return;
}
@@ -298,8 +312,8 @@ class _StoreMigrator<K extends Enum> {
_migratedStoreIds.add(legacyKey.id);
}
Future<void> migrateEnumName<T extends Enum>(StoreKey<String> legacyKey, K newKey, List<T> values) async {
final name = await readLegacyStoreString(legacyKey);
Future<void> migrateEnumName<T extends Enum>(LegacyStoreKey legacyKey, K newKey, List<T> values) async {
final name = readLegacyStoreString(legacyKey);
if (name == null) {
return;
}
@@ -313,8 +327,8 @@ class _StoreMigrator<K extends Enum> {
_migratedStoreIds.add(legacyKey.id);
}
Future<void> migrateBool(StoreKey<bool> legacyKey, K newKey) async {
final intValue = await readLegacyStoreInt(legacyKey);
Future<void> migrateBool(LegacyStoreKey legacyKey, K newKey) async {
final intValue = readLegacyStoreInt(legacyKey);
if (intValue == null) {
return;
}
@@ -323,8 +337,8 @@ class _StoreMigrator<K extends Enum> {
_migratedStoreIds.add(legacyKey.id);
}
Future<void> migrateInt(StoreKey<int> legacyKey, K newKey) async {
final intValue = await readLegacyStoreInt(legacyKey);
Future<void> migrateInt(LegacyStoreKey legacyKey, K newKey) async {
final intValue = readLegacyStoreInt(legacyKey);
if (intValue == null) {
return;
}
@@ -333,8 +347,8 @@ class _StoreMigrator<K extends Enum> {
_migratedStoreIds.add(legacyKey.id);
}
Future<void> migrateString(StoreKey<String> legacyKey, K newKey) async {
final value = await readLegacyStoreString(legacyKey);
Future<void> migrateString(LegacyStoreKey legacyKey, K newKey) async {
final value = readLegacyStoreString(legacyKey);
if (value == null || value.isEmpty) {
return;
}
@@ -343,12 +357,12 @@ class _StoreMigrator<K extends Enum> {
_migratedStoreIds.add(legacyKey.id);
}
Future<void> migrateNullableString(StoreKey<String> legacyKey, K newKey) async {
_cache[newKey] = await readLegacyStoreString(legacyKey);
Future<void> migrateNullableString(LegacyStoreKey legacyKey, K newKey) async {
_cache[newKey] = readLegacyStoreString(legacyKey);
_migratedStoreIds.add(legacyKey.id);
}
void stage(StoreKey legacyKey, K newKey, Object? value) {
void stage(LegacyStoreKey legacyKey, K newKey, Object? value) {
_cache[newKey] = value;
_migratedStoreIds.add(legacyKey.id);
}
@@ -367,15 +381,9 @@ class _StoreMigrator<K extends Enum> {
await deleteLegacyStoreRows(_migratedStoreIds);
}
Future<String?> readLegacyStoreString(StoreKey key) async {
final row = await (_db.storeEntity.select()..where((t) => t.id.equals(key.id))).getSingleOrNull();
return row?.stringValue;
}
String? readLegacyStoreString(LegacyStoreKey key) => _legacyStore[key.id] as String?;
Future<int?> readLegacyStoreInt(StoreKey key) async {
final row = await (_db.storeEntity.select()..where((t) => t.id.equals(key.id))).getSingleOrNull();
return row?.intValue;
}
int? readLegacyStoreInt(LegacyStoreKey key) => _legacyStore[key.id] as int?;
Future<void> deleteLegacyStoreRows(List<int> ids) async {
if (ids.isEmpty) {
-3
View File
@@ -1,11 +1,8 @@
import 'package:immich_mobile/domain/services/partner.service.dart';
import 'package:immich_mobile/domain/services/store.service.dart';
import 'package:immich_mobile/domain/utils/background_sync.dart';
import 'package:immich_mobile/platform/native_sync_api.g.dart';
import 'package:mocktail/mocktail.dart';
class MockStoreService extends Mock implements StoreService {}
class MockBackgroundSyncManager extends Mock implements BackgroundSyncManager {}
class MockNativeSyncApi extends Mock implements NativeSyncApi {}
@@ -5,12 +5,10 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:immich_mobile/domain/models/app_metadata_key.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/services/local_sync.service.dart';
import 'package:immich_mobile/domain/services/store.service.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
import 'package:immich_mobile/infrastructure/store.dart';
import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart';
import 'package:immich_mobile/platform/native_sync_api.g.dart';
import 'package:immich_mobile/repositories/asset_media.repository.dart';
@@ -37,7 +35,7 @@ void main() {
debugDefaultTargetPlatformOverride = TargetPlatform.android;
db = Drift(drift.DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true));
await StoreService.init(storeRepository: DriftStoreRepository(db));
await DeviceIdStore.init(db);
});
tearDownAll(() async {
@@ -1,154 +0,0 @@
import 'dart:async';
import 'package:flutter_test/flutter_test.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/services/store.service.dart';
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
import 'package:mocktail/mocktail.dart';
import '../../infrastructure/repository.mock.dart';
const _kAccessToken = '#ThisIsAToken';
const _kAdvancedTroubleshooting = false;
const _kVersion = 2;
void main() {
late StoreService sut;
late DriftStoreRepository mockDriftStoreRepo;
late StreamController<List<StoreDto<Object>>> controller;
setUp(() async {
controller = StreamController<List<StoreDto<Object>>>.broadcast();
mockDriftStoreRepo = MockDriftStoreRepository();
// For generics, we need to provide fallback to each concrete type to avoid runtime errors
registerFallbackValue(StoreKey.legacyAccessToken);
registerFallbackValue(StoreKey.legacyVersion);
registerFallbackValue(StoreKey.legacyAdvancedTroubleshooting);
when(() => mockDriftStoreRepo.getAll()).thenAnswer(
(_) async => [
const StoreDto(StoreKey.legacyAccessToken, _kAccessToken),
const StoreDto(StoreKey.legacyAdvancedTroubleshooting, _kAdvancedTroubleshooting),
const StoreDto(StoreKey.legacyVersion, _kVersion),
],
);
when(() => mockDriftStoreRepo.watchAll()).thenAnswer((_) => controller.stream);
sut = await StoreService.create(storeRepository: mockDriftStoreRepo);
});
tearDown(() async {
unawaited(sut.dispose());
await controller.close();
});
group("Store Service Init:", () {
test('Populates the internal cache on init', () {
verify(() => mockDriftStoreRepo.getAll()).called(1);
expect(sut.tryGet(StoreKey.legacyAccessToken), _kAccessToken);
expect(sut.tryGet(StoreKey.legacyAdvancedTroubleshooting), _kAdvancedTroubleshooting);
expect(sut.tryGet(StoreKey.legacyVersion), _kVersion);
// Other keys should be null
expect(sut.tryGet(StoreKey.deviceId), isNull);
});
test('Listens to stream of store updates', () async {
final event = StoreDto(StoreKey.legacyAccessToken, _kAccessToken.toUpperCase());
controller.add([event]);
await pumpEventQueue();
verify(() => mockDriftStoreRepo.watchAll()).called(1);
expect(sut.tryGet(StoreKey.legacyAccessToken), _kAccessToken.toUpperCase());
});
});
group('Store Service get:', () {
test('Returns the stored value for the given key', () {
expect(sut.get(StoreKey.legacyAccessToken), _kAccessToken);
});
test('Throws StoreKeyNotFoundException for nonexistent keys', () {
expect(() => sut.get(StoreKey.deviceId), throwsA(isA<StoreKeyNotFoundException>()));
});
test('Returns the stored value for the given key or the defaultValue', () {
expect(sut.get(StoreKey.legacyBackupTriggerDelay, 5), 5);
});
});
group('Store Service put:', () {
setUp(() {
when(() => mockDriftStoreRepo.upsert<String>(any<StoreKey<String>>(), any())).thenAnswer((_) async => true);
});
test('Skip insert when value is not modified', () async {
await sut.put(StoreKey.legacyAccessToken, _kAccessToken);
verifyNever(() => mockDriftStoreRepo.upsert<String>(StoreKey.legacyAccessToken, any()));
});
test('Insert value when modified', () async {
final newAccessToken = _kAccessToken.toUpperCase();
await sut.put(StoreKey.legacyAccessToken, newAccessToken);
verify(() => mockDriftStoreRepo.upsert<String>(StoreKey.legacyAccessToken, newAccessToken)).called(1);
expect(sut.tryGet(StoreKey.legacyAccessToken), newAccessToken);
});
});
group('Store Service watch:', () {
late StreamController<String?> valueController;
setUp(() {
valueController = StreamController<String?>.broadcast();
when(() => mockDriftStoreRepo.watch<String>(any<StoreKey<String>>())).thenAnswer((_) => valueController.stream);
});
tearDown(() async {
await valueController.close();
});
test('Watches a specific key for changes', () async {
final stream = sut.watch(StoreKey.legacyAccessToken);
final events = <String?>[_kAccessToken, _kAccessToken.toUpperCase(), null, _kAccessToken.toLowerCase()];
unawaited(expectLater(stream, emitsInOrder(events)));
for (final event in events) {
valueController.add(event);
}
await pumpEventQueue();
verify(() => mockDriftStoreRepo.watch<String>(StoreKey.legacyAccessToken)).called(1);
});
});
group('Store Service delete:', () {
setUp(() {
when(() => mockDriftStoreRepo.delete<String>(any<StoreKey<String>>())).thenAnswer((_) async => true);
});
test('Removes the value from the DB', () async {
await sut.delete(StoreKey.legacyAccessToken);
verify(() => mockDriftStoreRepo.delete<String>(StoreKey.legacyAccessToken)).called(1);
});
test('Removes the value from the cache', () async {
await sut.delete(StoreKey.legacyAccessToken);
expect(sut.tryGet(StoreKey.legacyAccessToken), isNull);
});
});
group('Store Service clear:', () {
setUp(() {
when(() => mockDriftStoreRepo.deleteAll()).thenAnswer((_) async => true);
});
test('Clears all values from the store', () async {
await sut.clear();
verify(() => mockDriftStoreRepo.deleteAll()).called(1);
expect(sut.tryGet(StoreKey.legacyAccessToken), isNull);
expect(sut.tryGet(StoreKey.legacyAdvancedTroubleshooting), isNull);
expect(sut.tryGet(StoreKey.legacyVersion), isNull);
});
});
}
@@ -7,12 +7,10 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:immich_mobile/domain/models/app_metadata_key.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/sync_event.model.dart';
import 'package:immich_mobile/domain/services/store.service.dart';
import 'package:immich_mobile/domain/services/sync_stream.service.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
import 'package:immich_mobile/infrastructure/store.dart';
import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart';
@@ -63,7 +61,7 @@ void main() {
registerFallbackValue(const <String>[]);
db = Drift(drift.DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true));
await StoreService.init(storeRepository: DriftStoreRepository(db));
await DeviceIdStore.init(db);
});
tearDownAll(() async {
@@ -1,152 +0,0 @@
import 'dart:async';
import 'package:drift/drift.dart' hide isNull;
import 'package:drift/native.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
const _kTestAccessToken = "#TestToken";
const _kTestVersion = 10;
const _kTestAdvancedTroubleshooting = false;
Future<void> _populateStore(Drift db) async {
await db.batch((batch) async {
batch.insert(
db.storeEntity,
StoreEntityCompanion(
id: Value(StoreKey.legacyAdvancedTroubleshooting.id),
intValue: const Value(_kTestAdvancedTroubleshooting ? 1 : 0),
stringValue: const Value(null),
),
);
batch.insert(
db.storeEntity,
StoreEntityCompanion(
id: Value(StoreKey.legacyAccessToken.id),
intValue: const Value(null),
stringValue: const Value(_kTestAccessToken),
),
);
batch.insert(
db.storeEntity,
StoreEntityCompanion(
id: Value(StoreKey.legacyVersion.id),
intValue: const Value(_kTestVersion),
stringValue: const Value(null),
),
);
});
}
void main() {
late Drift db;
late DriftStoreRepository sut;
setUp(() async {
db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true));
sut = DriftStoreRepository(db);
});
tearDown(() async {
await db.close();
});
group('Store Repository converters:', () {
test('converts int', () async {
int? version = await sut.tryGet(StoreKey.legacyVersion);
expect(version, isNull);
await sut.upsert(StoreKey.legacyVersion, _kTestVersion);
version = await sut.tryGet(StoreKey.legacyVersion);
expect(version, _kTestVersion);
});
test('converts string', () async {
String? accessToken = await sut.tryGet(StoreKey.legacyAccessToken);
expect(accessToken, isNull);
await sut.upsert(StoreKey.legacyAccessToken, _kTestAccessToken);
accessToken = await sut.tryGet(StoreKey.legacyAccessToken);
expect(accessToken, _kTestAccessToken);
});
test('converts bool', () async {
bool? advancedTroubleshooting = await sut.tryGet(StoreKey.legacyAdvancedTroubleshooting);
expect(advancedTroubleshooting, isNull);
await sut.upsert(StoreKey.legacyAdvancedTroubleshooting, _kTestAdvancedTroubleshooting);
advancedTroubleshooting = await sut.tryGet(StoreKey.legacyAdvancedTroubleshooting);
expect(advancedTroubleshooting, _kTestAdvancedTroubleshooting);
});
});
group('Store Repository Deletes:', () {
setUp(() async {
await _populateStore(db);
});
test('delete()', () async {
bool? advancedTroubleshooting = await sut.tryGet(StoreKey.legacyAdvancedTroubleshooting);
expect(advancedTroubleshooting, isFalse);
await sut.delete(StoreKey.legacyAdvancedTroubleshooting);
advancedTroubleshooting = await sut.tryGet(StoreKey.legacyAdvancedTroubleshooting);
expect(advancedTroubleshooting, isNull);
});
test('deleteAll()', () async {
final count = await db.storeEntity.count().getSingle();
expect(count, isNot(isZero));
await sut.deleteAll();
unawaited(expectLater(await db.storeEntity.count().getSingle(), isZero));
});
});
group('Store Repository Updates:', () {
setUp(() async {
await _populateStore(db);
});
test('upsert()', () async {
int? version = await sut.tryGet(StoreKey.legacyVersion);
expect(version, _kTestVersion);
await sut.upsert(StoreKey.legacyVersion, _kTestVersion + 10);
version = await sut.tryGet(StoreKey.legacyVersion);
expect(version, _kTestVersion + 10);
});
});
group('Store Repository Watchers:', () {
setUp(() async {
await _populateStore(db);
});
test('watch()', () async {
final stream = sut.watch(StoreKey.legacyVersion);
unawaited(expectLater(stream, emitsInOrder([_kTestVersion, _kTestVersion + 10])));
await pumpEventQueue();
await sut.upsert(StoreKey.legacyVersion, _kTestVersion + 10);
});
test('watchAll()', () async {
final stream = sut.watchAll();
unawaited(
expectLater(
stream,
emitsInOrder([
[
const StoreDto<Object>(StoreKey.legacyVersion, _kTestVersion),
const StoreDto<Object>(StoreKey.legacyAccessToken, _kTestAccessToken),
const StoreDto<Object>(StoreKey.legacyAdvancedTroubleshooting, _kTestAdvancedTroubleshooting),
],
[
const StoreDto<Object>(StoreKey.legacyVersion, _kTestVersion + 10),
const StoreDto<Object>(StoreKey.legacyAccessToken, _kTestAccessToken),
const StoreDto<Object>(StoreKey.legacyAdvancedTroubleshooting, _kTestAdvancedTroubleshooting),
],
]),
),
);
await sut.upsert(StoreKey.legacyVersion, _kTestVersion + 10);
});
});
}
@@ -6,9 +6,8 @@ import 'package:drift/native.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart' as http;
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/db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
import 'package:immich_mobile/infrastructure/store.dart';
import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart';
import 'package:immich_mobile/utils/semver.dart';
import 'package:mocktail/mocktail.dart';
@@ -41,7 +40,7 @@ void main() {
setUpAll(() async {
final db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true));
await StoreService.init(storeRepository: DriftStoreRepository(db));
await DeviceIdStore.init(db);
});
setUp(() {
@@ -8,7 +8,6 @@ import 'package:immich_mobile/infrastructure/repositories/settings.repository.da
import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/sync_migration.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart';
@@ -19,8 +18,6 @@ import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
import 'package:immich_mobile/repositories/upload.repository.dart';
import 'package:mocktail/mocktail.dart';
class MockDriftStoreRepository extends Mock implements DriftStoreRepository {}
class MockSettingsRepository extends Mock implements SettingsRepository {}
class MockAppMetadataRepository extends Mock implements AppMetadataRepository {}
@@ -7,9 +7,8 @@ import 'package:drift/native.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/services/store.service.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
import 'package:immich_mobile/infrastructure/store.dart';
import 'package:immich_mobile/models/map/map_state.model.dart';
import 'package:immich_mobile/providers/locale_provider.dart';
import 'package:immich_mobile/providers/map/map_state.provider.dart';
@@ -33,7 +32,7 @@ void main() {
setUp(() async {
mapState = const MapState(themeMode: ThemeMode.dark);
mapStateNotifier = MockMapStateNotifier(mapState);
await StoreService.init(storeRepository: DriftStoreRepository(db));
await DeviceIdStore.init(db);
overrides = [
mapStateNotifierProvider.overrideWith(() => mapStateNotifier),
localeProvider.overrideWithValue(const Locale("en")),
@@ -2,10 +2,8 @@ import 'package:drift/drift.dart' as drift;
import 'package:drift/native.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:immich_mobile/domain/services/store.service.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
import 'package:immich_mobile/infrastructure/store.dart';
import 'package:immich_mobile/repositories/download.repository.dart';
import 'package:immich_mobile/services/action.service.dart';
import 'package:mocktail/mocktail.dart';
@@ -36,7 +34,7 @@ void main() {
debugDefaultTargetPlatformOverride = TargetPlatform.android;
db = Drift(drift.DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true));
await StoreService.init(storeRepository: DriftStoreRepository(db));
await DeviceIdStore.init(db);
});
tearDownAll(() async {
+2 -3
View File
@@ -2,10 +2,9 @@ import 'package:drift/drift.dart' hide isNull;
import 'package:drift/native.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:immich_mobile/domain/services/store.service.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/session.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
import 'package:immich_mobile/infrastructure/store.dart';
import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart';
import 'package:immich_mobile/services/auth.service.dart';
import 'package:mocktail/mocktail.dart';
@@ -44,7 +43,7 @@ void main() {
setUpAll(() async {
WidgetsFlutterBinding.ensureInitialized();
db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true));
await StoreService.init(storeRepository: DriftStoreRepository(db));
await DeviceIdStore.init(db);
await SessionRepository.ensureInitialized(db);
});
@@ -8,13 +8,10 @@ import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/session.model.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/services/store.service.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/session.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
import 'package:immich_mobile/infrastructure/store.dart';
import 'package:immich_mobile/services/background_upload.service.dart';
import 'package:mocktail/mocktail.dart';
@@ -39,11 +36,11 @@ void main() {
(MethodCall methodCall) async => 'test',
);
db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true));
await StoreService.init(storeRepository: DriftStoreRepository(db));
await DeviceIdStore.init(db);
await SettingsRepository.ensureInitialized(db);
await SessionRepository.ensureInitialized(db);
await SessionRepository.instance.write(SessionKey.serverEndpoint, 'https://demo.immich.app');
await Store.put(StoreKey.deviceId, 'test-device-id');
await Store.setDeviceId('test-device-id');
});
setUp(() {
+2 -3
View File
@@ -6,11 +6,10 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/locales.dart';
import 'package:immich_mobile/domain/models/session.model.dart';
import 'package:immich_mobile/domain/services/store.service.dart';
import 'package:immich_mobile/generated/codegen_loader.g.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/session.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
import 'package:immich_mobile/infrastructure/store.dart';
import '../test_utils.dart';
@@ -25,7 +24,7 @@ class PresentationContext {
TestUtils.init();
if (_db == null) {
final db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true));
await StoreService.init(storeRepository: DriftStoreRepository(db), listenUpdates: false);
await DeviceIdStore.init(db);
await SessionRepository.ensureInitialized(db);
await SessionRepository.instance.write(SessionKey.serverEndpoint, serverEndpoint);
_db = db;