mirror of
https://github.com/zebrajr/immich.git
synced 2025-12-06 00:20:20 +01:00
feat: migrate store to sqlite (#21078)
* add store entity and migration * make store service take both isar and drift repos * migrate and switch store on beta timeline state change * chore: make drift variables final * dispose old store before switching repos * use store to update values for beta timeline * change log service to use the proper store * migrate store when beta already enabled * use isar repository to check beta timeline in store service * remove unused update method from store repo * dispose after create * change watchAll signature in store repo * fix test * rename init isar to initDB * request user to close and reopen on beta migration * fix tests * handle empty version in migration * wait for cache to be populated after migration --------- Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
parent
ed3997d844
commit
6f4f79d8cc
BIN
mobile/drift_schemas/main/drift_schema_v8.json
generated
Normal file
BIN
mobile/drift_schemas/main/drift_schema_v8.json
generated
Normal file
Binary file not shown.
|
|
@ -4,7 +4,6 @@ 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/repositories/logger_db.repository.dart';
|
||||
import 'package:immich_mobile/main.dart' as app;
|
||||
import 'package:immich_mobile/providers/db.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||
|
|
@ -40,15 +39,18 @@ class ImmichTestHelper {
|
|||
static Future<void> loadApp(WidgetTester tester) async {
|
||||
await EasyLocalization.ensureInitialized();
|
||||
// Clear all data from Isar (reuse existing instance if available)
|
||||
final db = await Bootstrap.initIsar();
|
||||
final logDb = DriftLogger();
|
||||
await Bootstrap.initDomain(db, logDb);
|
||||
final (isar, drift, logDb) = await Bootstrap.initDB();
|
||||
await Bootstrap.initDomain(isar, drift, logDb);
|
||||
await Store.clear();
|
||||
await db.writeTxn(() => db.clear());
|
||||
await isar.writeTxn(() => isar.clear());
|
||||
// Load main Widget
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [dbProvider.overrideWithValue(db), isarProvider.overrideWithValue(db)],
|
||||
overrides: [
|
||||
dbProvider.overrideWithValue(isar),
|
||||
isarProvider.overrideWithValue(isar),
|
||||
driftProvider.overrideWith(driftOverride(drift)),
|
||||
],
|
||||
child: const app.MainWidget(),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import 'package:logging/logging.dart';
|
|||
/// via [IStoreRepository]
|
||||
class LogService {
|
||||
final LogRepository _logRepository;
|
||||
final IsarStoreRepository _storeRepository;
|
||||
final IStoreRepository _storeRepository;
|
||||
|
||||
final List<LogMessage> _msgBuffer = [];
|
||||
|
||||
|
|
@ -38,7 +38,7 @@ class LogService {
|
|||
|
||||
static Future<LogService> init({
|
||||
required LogRepository logRepository,
|
||||
required IsarStoreRepository storeRepository,
|
||||
required IStoreRepository storeRepository,
|
||||
bool shouldBuffer = true,
|
||||
}) async {
|
||||
_instance ??= await create(
|
||||
|
|
@ -51,7 +51,7 @@ class LogService {
|
|||
|
||||
static Future<LogService> create({
|
||||
required LogRepository logRepository,
|
||||
required IsarStoreRepository storeRepository,
|
||||
required IStoreRepository storeRepository,
|
||||
bool shouldBuffer = true,
|
||||
}) async {
|
||||
final instance = LogService._(logRepository, storeRepository, shouldBuffer);
|
||||
|
|
@ -92,7 +92,7 @@ class LogService {
|
|||
}
|
||||
|
||||
Future<void> setLogLevel(LogLevel level) async {
|
||||
await _storeRepository.insert(StoreKey.logLevel, level.index);
|
||||
await _storeRepository.upsert(StoreKey.logLevel, level.index);
|
||||
Logger.root.level = level.toLevel();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,13 +6,13 @@ 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 IsarStoreRepository _storeRepository;
|
||||
final IStoreRepository _storeRepository;
|
||||
|
||||
/// In-memory cache. Keys are [StoreKey.id]
|
||||
final Map<int, Object?> _cache = {};
|
||||
late final StreamSubscription<StoreDto> _storeUpdateSubscription;
|
||||
late final StreamSubscription<List<StoreDto>> _storeUpdateSubscription;
|
||||
|
||||
StoreService._({required IsarStoreRepository storeRepository}) : _storeRepository = storeRepository;
|
||||
StoreService._({required IStoreRepository 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;
|
||||
|
|
@ -24,27 +24,29 @@ class StoreService {
|
|||
}
|
||||
|
||||
// TODO: Replace the implementation with the one from create after removing the typedef
|
||||
static Future<StoreService> init({required IsarStoreRepository storeRepository}) async {
|
||||
static Future<StoreService> init({required IStoreRepository storeRepository}) async {
|
||||
_instance ??= await create(storeRepository: storeRepository);
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
static Future<StoreService> create({required IsarStoreRepository storeRepository}) async {
|
||||
final instance = StoreService._(storeRepository: storeRepository);
|
||||
await instance._populateCache();
|
||||
static Future<StoreService> create({required IStoreRepository storeRepository}) async {
|
||||
final instance = StoreService._(isarStoreRepository: storeRepository);
|
||||
await instance.populateCache();
|
||||
instance._storeUpdateSubscription = instance._listenForChange();
|
||||
return instance;
|
||||
}
|
||||
|
||||
Future<void> _populateCache() async {
|
||||
Future<void> populateCache() async {
|
||||
final storeValues = await _storeRepository.getAll();
|
||||
for (StoreDto storeValue in storeValues) {
|
||||
_cache[storeValue.key.id] = storeValue.value;
|
||||
}
|
||||
}
|
||||
|
||||
StreamSubscription<StoreDto> _listenForChange() => _storeRepository.watchAll().listen((event) {
|
||||
_cache[event.key.id] = event.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
|
||||
|
|
@ -69,7 +71,7 @@ class StoreService {
|
|||
/// 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.insert(key, value);
|
||||
await _storeRepository.upsert(key, value);
|
||||
_cache[key.id] = value;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
part 'store.entity.g.dart';
|
||||
|
|
@ -11,3 +13,13 @@ class StoreValue {
|
|||
|
||||
const StoreValue(this.id, {this.intValue, this.strValue});
|
||||
}
|
||||
|
||||
class StoreEntity extends Table with DriftDefaultsMixin {
|
||||
IntColumn get id => integer()();
|
||||
|
||||
TextColumn get stringValue => text().nullable()();
|
||||
IntColumn get intValue => integer().nullable()();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
}
|
||||
|
|
|
|||
BIN
mobile/lib/infrastructure/entities/store.entity.drift.dart
generated
Normal file
BIN
mobile/lib/infrastructure/entities/store.entity.drift.dart
generated
Normal file
Binary file not shown.
|
|
@ -18,6 +18,7 @@ import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.
|
|||
import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/stack.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.steps.dart';
|
||||
|
|
@ -58,6 +59,7 @@ class IsarDatabaseRepository implements IDatabaseRepository {
|
|||
StackEntity,
|
||||
PersonEntity,
|
||||
AssetFaceEntity,
|
||||
StoreEntity,
|
||||
],
|
||||
include: {'package:immich_mobile/infrastructure/entities/merged_asset.drift'},
|
||||
)
|
||||
|
|
@ -66,7 +68,7 @@ class Drift extends $Drift implements IDatabaseRepository {
|
|||
: super(executor ?? driftDatabase(name: 'immich', native: const DriftNativeOptions(shareAcrossIsolates: true)));
|
||||
|
||||
@override
|
||||
int get schemaVersion => 7;
|
||||
int get schemaVersion => 8;
|
||||
|
||||
@override
|
||||
MigrationStrategy get migration => MigrationStrategy(
|
||||
|
|
@ -118,6 +120,9 @@ class Drift extends $Drift implements IDatabaseRepository {
|
|||
from6To7: (m, v7) async {
|
||||
await m.createIndex(v7.idxLatLng);
|
||||
},
|
||||
from7To8: (m, v8) async {
|
||||
await m.create(v8.storeEntity);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
|
|
@ -1,16 +1,30 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/store.entity.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/user.repository.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
class IsarStoreRepository extends IsarDatabaseRepository {
|
||||
// Temporary interface until Isar is removed to make the service work with both Isar and Sqlite
|
||||
abstract class IStoreRepository {
|
||||
Future<bool> deleteAll();
|
||||
Stream<List<StoreDto<Object>>> watchAll();
|
||||
Future<void> delete<T>(StoreKey<T> key);
|
||||
Future<bool> upsert<T>(StoreKey<T> key, T value);
|
||||
Future<T?> tryGet<T>(StoreKey<T> key);
|
||||
Stream<T?> watch<T>(StoreKey<T> key);
|
||||
Future<List<StoreDto<Object>>> getAll();
|
||||
}
|
||||
|
||||
class IsarStoreRepository extends IsarDatabaseRepository implements IStoreRepository {
|
||||
final Isar _db;
|
||||
final validStoreKeys = StoreKey.values.map((e) => e.id).toSet();
|
||||
|
||||
IsarStoreRepository(super.db) : _db = db;
|
||||
|
||||
@override
|
||||
Future<bool> deleteAll() async {
|
||||
return await transaction(() async {
|
||||
await _db.storeValues.clear();
|
||||
|
|
@ -18,25 +32,29 @@ class IsarStoreRepository extends IsarDatabaseRepository {
|
|||
});
|
||||
}
|
||||
|
||||
Stream<StoreDto<Object>> watchAll() {
|
||||
@override
|
||||
Stream<List<StoreDto<Object>>> watchAll() {
|
||||
return _db.storeValues
|
||||
.filter()
|
||||
.anyOf(validStoreKeys, (query, id) => query.idEqualTo(id))
|
||||
.watch(fireImmediately: true)
|
||||
.asyncExpand((entities) => Stream.fromFutures(entities.map((e) async => _toUpdateEvent(e))));
|
||||
.asyncMap((entities) => Future.wait(entities.map((entity) => _toUpdateEvent(entity))));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> delete<T>(StoreKey<T> key) async {
|
||||
return await transaction(() async => await _db.storeValues.delete(key.id));
|
||||
}
|
||||
|
||||
Future<bool> insert<T>(StoreKey<T> key, T value) async {
|
||||
@override
|
||||
Future<bool> upsert<T>(StoreKey<T> key, T value) async {
|
||||
return await transaction(() async {
|
||||
await _db.storeValues.put(await _fromValue(key, value));
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<T?> tryGet<T>(StoreKey<T> key) async {
|
||||
final entity = (await _db.storeValues.get(key.id));
|
||||
if (entity == null) {
|
||||
|
|
@ -45,13 +63,7 @@ class IsarStoreRepository extends IsarDatabaseRepository {
|
|||
return await _toValue(key, entity);
|
||||
}
|
||||
|
||||
Future<bool> update<T>(StoreKey<T> key, T value) async {
|
||||
return await transaction(() async {
|
||||
await _db.storeValues.put(await _fromValue(key, value));
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<T?> watch<T>(StoreKey<T> key) async* {
|
||||
yield* _db.storeValues
|
||||
.watchObject(key.id, fireImmediately: true)
|
||||
|
|
@ -88,8 +100,93 @@ class IsarStoreRepository extends IsarDatabaseRepository {
|
|||
return StoreValue(key.id, intValue: intValue, strValue: strValue);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<StoreDto<Object>>> getAll() async {
|
||||
final entities = await _db.storeValues.filter().anyOf(validStoreKeys, (query, id) => query.idEqualTo(id)).findAll();
|
||||
return Future.wait(entities.map((e) => _toUpdateEvent(e)).toList());
|
||||
}
|
||||
}
|
||||
|
||||
class DriftStoreRepository extends DriftDatabaseRepository implements IStoreRepository {
|
||||
final Drift _db;
|
||||
final validStoreKeys = StoreKey.values.map((e) => e.id).toSet();
|
||||
|
||||
DriftStoreRepository(super.db) : _db = db;
|
||||
|
||||
@override
|
||||
Future<bool> deleteAll() async {
|
||||
await _db.storeEntity.deleteAll();
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<StoreDto<Object>>> getAll() async {
|
||||
final query = _db.storeEntity.select()..where((entity) => entity.id.isIn(validStoreKeys));
|
||||
return query.asyncMap((entity) => _toUpdateEvent(entity)).get();
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<List<StoreDto<Object>>> watchAll() {
|
||||
final query = _db.storeEntity.select()..where((entity) => entity.id.isIn(validStoreKeys));
|
||||
|
||||
return query.asyncMap((entity) => _toUpdateEvent(entity)).watch();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> delete<T>(StoreKey<T> key) async {
|
||||
await _db.storeEntity.deleteWhere((entity) => entity.id.equals(key.id));
|
||||
return;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> upsert<T>(StoreKey<T> key, T value) async {
|
||||
await _db.storeEntity.insertOnConflictUpdate(await _fromValue(key, value));
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
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);
|
||||
}
|
||||
|
||||
@override
|
||||
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!),
|
||||
const (UserDto) =>
|
||||
entity.stringValue == null ? null : await DriftUserRepository(_db).get(entity.stringValue!),
|
||||
_ => 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),
|
||||
const (UserDto) => (null, (await DriftUserRepository(_db).upsert(value as UserDto)).id),
|
||||
_ => throw UnsupportedError("Unsupported primitive type: ${key.type} for key: ${key.name}"),
|
||||
};
|
||||
return StoreEntityCompanion(id: Value(key.id), intValue: Value(intValue), stringValue: Value(strValue));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/user.entity.dart' as entity;
|
||||
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
|
|
@ -63,3 +65,40 @@ class IsarUserRepository extends IsarDatabaseRepository {
|
|||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class DriftUserRepository extends DriftDatabaseRepository {
|
||||
final Drift _db;
|
||||
const DriftUserRepository(super.db) : _db = db;
|
||||
|
||||
Future<UserDto?> get(String id) =>
|
||||
_db.managers.userEntity.filter((user) => user.id.equals(id)).getSingleOrNull().then((user) => user?.toDto());
|
||||
|
||||
Future<UserDto> upsert(UserDto user) async {
|
||||
await _db.userEntity.insertOnConflictUpdate(
|
||||
UserEntityCompanion(
|
||||
id: Value(user.id),
|
||||
isAdmin: Value(user.isAdmin),
|
||||
updatedAt: Value(user.updatedAt),
|
||||
name: Value(user.name),
|
||||
email: Value(user.email),
|
||||
hasProfileImage: Value(user.hasProfileImage),
|
||||
profileChangedAt: Value(user.profileChangedAt),
|
||||
),
|
||||
);
|
||||
return user;
|
||||
}
|
||||
}
|
||||
|
||||
extension on UserEntityData {
|
||||
UserDto toDto() {
|
||||
return UserDto(
|
||||
id: id,
|
||||
email: email,
|
||||
name: name,
|
||||
isAdmin: isAdmin,
|
||||
updatedAt: updatedAt,
|
||||
profileChangedAt: profileChangedAt,
|
||||
hasProfileImage: hasProfileImage,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ import 'package:immich_mobile/constants/constants.dart';
|
|||
import 'package:immich_mobile/constants/locales.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/generated/codegen_loader.g.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart';
|
||||
import 'package:immich_mobile/providers/app_life_cycle.provider.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart';
|
||||
import 'package:immich_mobile/providers/db.provider.dart';
|
||||
|
|
@ -41,18 +40,21 @@ import 'package:worker_manager/worker_manager.dart';
|
|||
|
||||
void main() async {
|
||||
ImmichWidgetsBinding();
|
||||
final db = await Bootstrap.initIsar();
|
||||
final logDb = DriftLogger();
|
||||
await Bootstrap.initDomain(db, logDb);
|
||||
final (isar, drift, logDb) = await Bootstrap.initDB();
|
||||
await Bootstrap.initDomain(isar, drift, logDb);
|
||||
await initApp();
|
||||
// Warm-up isolate pool for worker manager
|
||||
await workerManager.init(dynamicSpawning: true);
|
||||
await migrateDatabaseIfNeeded(db);
|
||||
await migrateDatabaseIfNeeded(isar, drift);
|
||||
HttpSSLOptions.apply();
|
||||
|
||||
runApp(
|
||||
ProviderScope(
|
||||
overrides: [dbProvider.overrideWithValue(db), isarProvider.overrideWithValue(db)],
|
||||
overrides: [
|
||||
dbProvider.overrideWithValue(isar),
|
||||
isarProvider.overrideWithValue(isar),
|
||||
driftProvider.overrideWith(driftOverride(drift)),
|
||||
],
|
||||
child: const MainWidget(),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
|
||||
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||
import 'package:immich_mobile/providers/asset.provider.dart';
|
||||
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||
|
|
@ -13,8 +14,8 @@ import 'package:immich_mobile/providers/backup/manual_upload.provider.dart';
|
|||
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||
import 'package:immich_mobile/providers/websocket.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/utils/migration.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
@RoutePage()
|
||||
|
|
@ -28,7 +29,7 @@ class ChangeExperiencePage extends ConsumerStatefulWidget {
|
|||
}
|
||||
|
||||
class _ChangeExperiencePageState extends ConsumerState<ChangeExperiencePage> {
|
||||
bool hasMigrated = false;
|
||||
AsyncValue<bool> hasMigrated = const AsyncValue.loading();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
@ -37,46 +38,60 @@ class _ChangeExperiencePageState extends ConsumerState<ChangeExperiencePage> {
|
|||
}
|
||||
|
||||
Future<void> _handleMigration() async {
|
||||
if (widget.switchingToBeta) {
|
||||
final assetNotifier = ref.read(assetProvider.notifier);
|
||||
if (assetNotifier.mounted) {
|
||||
assetNotifier.dispose();
|
||||
}
|
||||
final albumNotifier = ref.read(albumProvider.notifier);
|
||||
if (albumNotifier.mounted) {
|
||||
albumNotifier.dispose();
|
||||
try {
|
||||
if (widget.switchingToBeta) {
|
||||
final assetNotifier = ref.read(assetProvider.notifier);
|
||||
if (assetNotifier.mounted) {
|
||||
assetNotifier.dispose();
|
||||
}
|
||||
final albumNotifier = ref.read(albumProvider.notifier);
|
||||
if (albumNotifier.mounted) {
|
||||
albumNotifier.dispose();
|
||||
}
|
||||
|
||||
// Cancel uploads
|
||||
await Store.put(StoreKey.backgroundBackup, false);
|
||||
ref
|
||||
.read(backupProvider.notifier)
|
||||
.configureBackgroundBackup(enabled: false, onBatteryInfo: () {}, onError: (_) {});
|
||||
ref.read(backupProvider.notifier).setAutoBackup(false);
|
||||
ref.read(backupProvider.notifier).cancelBackup();
|
||||
ref.read(manualUploadProvider.notifier).cancelBackup();
|
||||
// Start listening to new websocket events
|
||||
ref.read(websocketProvider.notifier).stopListenToOldEvents();
|
||||
ref.read(websocketProvider.notifier).startListeningToBetaEvents();
|
||||
|
||||
final permission = await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission();
|
||||
|
||||
if (permission.isGranted) {
|
||||
await ref.read(backgroundSyncProvider).syncLocal(full: true);
|
||||
await migrateDeviceAssetToSqlite(ref.read(isarProvider), ref.read(driftProvider));
|
||||
await migrateBackupAlbumsToSqlite(ref.read(isarProvider), ref.read(driftProvider));
|
||||
await migrateStoreToSqlite(ref.read(isarProvider), ref.read(driftProvider));
|
||||
}
|
||||
} else {
|
||||
await ref.read(backgroundSyncProvider).cancel();
|
||||
ref.read(websocketProvider.notifier).stopListeningToBetaEvents();
|
||||
ref.read(websocketProvider.notifier).startListeningToOldEvents();
|
||||
await migrateStoreToIsar(ref.read(isarProvider), ref.read(driftProvider));
|
||||
}
|
||||
|
||||
// Cancel uploads
|
||||
await Store.put(StoreKey.backgroundBackup, false);
|
||||
ref
|
||||
.read(backupProvider.notifier)
|
||||
.configureBackgroundBackup(enabled: false, onBatteryInfo: () {}, onError: (_) {});
|
||||
ref.read(backupProvider.notifier).setAutoBackup(false);
|
||||
ref.read(backupProvider.notifier).cancelBackup();
|
||||
ref.read(manualUploadProvider.notifier).cancelBackup();
|
||||
// Start listening to new websocket events
|
||||
ref.read(websocketProvider.notifier).stopListenToOldEvents();
|
||||
ref.read(websocketProvider.notifier).startListeningToBetaEvents();
|
||||
await IsarStoreRepository(ref.read(isarProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta);
|
||||
await DriftStoreRepository(ref.read(driftProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta);
|
||||
|
||||
final permission = await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission();
|
||||
|
||||
if (permission.isGranted) {
|
||||
await ref.read(backgroundSyncProvider).syncLocal(full: true);
|
||||
await migrateDeviceAssetToSqlite(ref.read(isarProvider), ref.read(driftProvider));
|
||||
await migrateBackupAlbumsToSqlite(ref.read(isarProvider), ref.read(driftProvider));
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
HapticFeedback.heavyImpact();
|
||||
hasMigrated = const AsyncValue.data(true);
|
||||
});
|
||||
}
|
||||
} catch (e, s) {
|
||||
Logger("ChangeExperiencePage").severe("Error during migration", e, s);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
hasMigrated = AsyncValue.error(e, s);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
await ref.read(backgroundSyncProvider).cancel();
|
||||
ref.read(websocketProvider.notifier).stopListeningToBetaEvents();
|
||||
ref.read(websocketProvider.notifier).startListeningToOldEvents();
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
HapticFeedback.heavyImpact();
|
||||
hasMigrated = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -89,44 +104,34 @@ class _ChangeExperiencePageState extends ConsumerState<ChangeExperiencePage> {
|
|||
children: [
|
||||
AnimatedSwitcher(
|
||||
duration: Durations.long4,
|
||||
child: hasMigrated
|
||||
? const Icon(Icons.check_circle_rounded, color: Colors.green, size: 48.0)
|
||||
: const SizedBox(width: 50.0, height: 50.0, child: CircularProgressIndicator()),
|
||||
child: hasMigrated.when(
|
||||
data: (data) => const Icon(Icons.check_circle_rounded, color: Colors.green, size: 48.0),
|
||||
error: (error, stackTrace) => const Icon(Icons.error, color: Colors.red, size: 48.0),
|
||||
loading: () => const SizedBox(width: 50.0, height: 50.0, child: CircularProgressIndicator()),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16.0),
|
||||
Center(
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 300.0,
|
||||
child: AnimatedSwitcher(
|
||||
duration: Durations.long4,
|
||||
child: hasMigrated
|
||||
? Text(
|
||||
"Migration success!",
|
||||
style: context.textTheme.titleMedium,
|
||||
textAlign: TextAlign.center,
|
||||
)
|
||||
: Text(
|
||||
"Data migration in progress...\nPlease wait and don't close this page",
|
||||
style: context.textTheme.titleMedium,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 300.0,
|
||||
child: AnimatedSwitcher(
|
||||
duration: Durations.long4,
|
||||
child: hasMigrated.when(
|
||||
data: (data) => Text(
|
||||
"Migration success!\nPlease close and reopen the app to apply changes",
|
||||
style: context.textTheme.titleMedium,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
if (hasMigrated)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 16.0),
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
context.replaceRoute(
|
||||
widget.switchingToBeta ? const TabShellRoute() : const TabControllerRoute(),
|
||||
);
|
||||
},
|
||||
child: const Text("Continue"),
|
||||
),
|
||||
),
|
||||
],
|
||||
error: (error, stackTrace) => Text(
|
||||
"Migration failed!\nError: $error",
|
||||
style: context.textTheme.titleMedium,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
loading: () => Text(
|
||||
"Data migration in progress...\nPlease wait and don't close this page",
|
||||
style: context.textTheme.titleMedium,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -10,9 +10,12 @@ part 'db.provider.g.dart';
|
|||
@Riverpod(keepAlive: true)
|
||||
Isar isar(Ref ref) => throw UnimplementedError('isar');
|
||||
|
||||
final driftProvider = Provider<Drift>((ref) {
|
||||
final drift = Drift();
|
||||
Drift Function(Ref ref) driftOverride(Drift drift) => (ref) {
|
||||
ref.onDispose(() => unawaited(drift.close()));
|
||||
ref.keepAlive();
|
||||
return drift;
|
||||
});
|
||||
};
|
||||
|
||||
final driftProvider = Provider<Drift>(
|
||||
(ref) => throw UnimplementedError("driftProvider must be overridden in the isolate's ProviderContainer before use"),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/entities/backup_album.entity.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart';
|
||||
import 'package:immich_mobile/models/backup/backup_candidate.model.dart';
|
||||
import 'package:immich_mobile/models/backup/current_upload_asset.model.dart';
|
||||
import 'package:immich_mobile/models/backup/error_upload_asset.model.dart';
|
||||
|
|
@ -331,11 +330,16 @@ class BackgroundService {
|
|||
}
|
||||
|
||||
Future<bool> _onAssetsChanged() async {
|
||||
final db = await Bootstrap.initIsar();
|
||||
final logDb = DriftLogger();
|
||||
await Bootstrap.initDomain(db, logDb);
|
||||
final (isar, drift, logDb) = await Bootstrap.initDB();
|
||||
await Bootstrap.initDomain(isar, drift, logDb);
|
||||
|
||||
final ref = ProviderContainer(overrides: [dbProvider.overrideWithValue(db), isarProvider.overrideWithValue(db)]);
|
||||
final ref = ProviderContainer(
|
||||
overrides: [
|
||||
dbProvider.overrideWithValue(isar),
|
||||
isarProvider.overrideWithValue(isar),
|
||||
driftProvider.overrideWith(driftOverride(drift)),
|
||||
],
|
||||
);
|
||||
|
||||
HttpSSLOptions.apply();
|
||||
ref.read(apiServiceProvider).setAccessToken(Store.get(StoreKey.accessToken));
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import 'package:immich_mobile/domain/services/user.service.dart';
|
|||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/exif.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/exif.converter.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/exif.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
|
||||
|
|
@ -116,9 +115,8 @@ class BackupVerificationService {
|
|||
assert(tuple.deleteCandidates.length == tuple.originals.length);
|
||||
final List<Asset> result = [];
|
||||
BackgroundIsolateBinaryMessenger.ensureInitialized(tuple.rootIsolateToken);
|
||||
final db = await Bootstrap.initIsar();
|
||||
final logDb = DriftLogger();
|
||||
await Bootstrap.initDomain(db, logDb);
|
||||
final (isar, drift, logDb) = await Bootstrap.initDB();
|
||||
await Bootstrap.initDomain(isar, drift, logDb);
|
||||
await tuple.fileMediaRepository.enableBackgroundAccess();
|
||||
final ApiService apiService = ApiService();
|
||||
apiService.setEndpoint(tuple.endpoint);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/domain/services/log.service.dart';
|
||||
import 'package:immich_mobile/domain/services/store.service.dart';
|
||||
import 'package:immich_mobile/entities/album.entity.dart';
|
||||
|
|
@ -14,6 +15,7 @@ import 'package:immich_mobile/infrastructure/entities/device_asset.entity.dart';
|
|||
import 'package:immich_mobile/infrastructure/entities/exif.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/log.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
|
||||
|
|
@ -21,18 +23,23 @@ import 'package:isar/isar.dart';
|
|||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
abstract final class Bootstrap {
|
||||
static Future<Isar> initIsar() async {
|
||||
if (Isar.getInstance() != null) {
|
||||
return Isar.getInstance()!;
|
||||
static Future<(Isar isar, Drift drift, DriftLogger logDb)> initDB() async {
|
||||
final drift = Drift();
|
||||
final logDb = DriftLogger();
|
||||
|
||||
Isar? isar = Isar.getInstance();
|
||||
|
||||
if (isar != null) {
|
||||
return (isar, drift, logDb);
|
||||
}
|
||||
|
||||
final dir = await getApplicationDocumentsDirectory();
|
||||
return await Isar.open(
|
||||
isar = await Isar.open(
|
||||
[
|
||||
StoreValueSchema,
|
||||
ExifInfoSchema,
|
||||
AssetSchema,
|
||||
AlbumSchema,
|
||||
ExifInfoSchema,
|
||||
UserSchema,
|
||||
BackupAlbumSchema,
|
||||
DuplicatedAssetSchema,
|
||||
|
|
@ -45,14 +52,19 @@ abstract final class Bootstrap {
|
|||
maxSizeMiB: 2048,
|
||||
inspector: kDebugMode,
|
||||
);
|
||||
|
||||
return (isar, drift, logDb);
|
||||
}
|
||||
|
||||
static Future<void> initDomain(Isar db, DriftLogger logDb, {bool shouldBufferLogs = true}) async {
|
||||
await StoreService.init(storeRepository: IsarStoreRepository(db));
|
||||
static Future<void> initDomain(Isar db, Drift drift, DriftLogger logDb, {bool shouldBufferLogs = true}) async {
|
||||
final isBeta = await IsarStoreRepository(db).tryGet(StoreKey.betaTimeline) ?? false;
|
||||
final IStoreRepository storeRepo = isBeta ? DriftStoreRepository(drift) : IsarStoreRepository(db);
|
||||
|
||||
await StoreService.init(storeRepository: storeRepo);
|
||||
|
||||
await LogService.init(
|
||||
logRepository: LogRepository(logDb),
|
||||
storeRepository: IsarStoreRepository(db),
|
||||
storeRepository: storeRepo,
|
||||
shouldBuffer: shouldBufferLogs,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import 'package:flutter/material.dart';
|
|||
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/infrastructure/repositories/logger_db.repository.dart';
|
||||
import 'package:immich_mobile/providers/db.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/cancel.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||
|
|
@ -35,15 +34,15 @@ Cancelable<T?> runInIsolateGentle<T>({
|
|||
BackgroundIsolateBinaryMessenger.ensureInitialized(token);
|
||||
DartPluginRegistrant.ensureInitialized();
|
||||
|
||||
final db = await Bootstrap.initIsar();
|
||||
final logDb = DriftLogger();
|
||||
await Bootstrap.initDomain(db, logDb, shouldBufferLogs: false);
|
||||
final (isar, drift, logDb) = await Bootstrap.initDB();
|
||||
await Bootstrap.initDomain(isar, drift, logDb, shouldBufferLogs: false);
|
||||
final ref = ProviderContainer(
|
||||
overrides: [
|
||||
// TODO: Remove once isar is removed
|
||||
dbProvider.overrideWithValue(db),
|
||||
isarProvider.overrideWithValue(db),
|
||||
dbProvider.overrideWithValue(isar),
|
||||
isarProvider.overrideWithValue(isar),
|
||||
cancellationProvider.overrideWithValue(cancelledChecker),
|
||||
driftProvider.overrideWith(driftOverride(drift)),
|
||||
],
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import 'package:immich_mobile/infrastructure/entities/exif.entity.dart';
|
|||
import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||
|
|
@ -30,9 +31,10 @@ import 'package:logging/logging.dart';
|
|||
// ignore: import_rule_photo_manager
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
|
||||
const int targetVersion = 13;
|
||||
const int targetVersion = 14;
|
||||
|
||||
Future<void> migrateDatabaseIfNeeded(Isar db) async {
|
||||
Future<void> migrateDatabaseIfNeeded(Isar db, Drift drift) async {
|
||||
final hasVersion = Store.tryGet(StoreKey.version) != null;
|
||||
final int version = Store.get(StoreKey.version, targetVersion);
|
||||
|
||||
if (version < 9) {
|
||||
|
|
@ -58,6 +60,12 @@ Future<void> migrateDatabaseIfNeeded(Isar db) async {
|
|||
await Store.put(StoreKey.photoManagerCustomFilter, true);
|
||||
}
|
||||
|
||||
// This means that the SQLite DB is just created and has no version
|
||||
if (version < 14 || !hasVersion) {
|
||||
await migrateStoreToSqlite(db, drift);
|
||||
await Store.populateCache();
|
||||
}
|
||||
|
||||
if (targetVersion >= 12) {
|
||||
await Store.put(StoreKey.version, targetVersion);
|
||||
return;
|
||||
|
|
@ -215,6 +223,39 @@ Future<void> migrateBackupAlbumsToSqlite(Isar db, Drift drift) async {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> migrateStoreToSqlite(Isar db, Drift drift) async {
|
||||
try {
|
||||
final isarStoreValues = await db.storeValues.where().findAll();
|
||||
await drift.batch((batch) {
|
||||
for (final storeValue in isarStoreValues) {
|
||||
final companion = StoreEntityCompanion(
|
||||
id: Value(storeValue.id),
|
||||
stringValue: Value(storeValue.strValue),
|
||||
intValue: Value(storeValue.intValue),
|
||||
);
|
||||
batch.insert(drift.storeEntity, companion, onConflict: DoUpdate((_) => companion));
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
debugPrint("[MIGRATION] Error while migrating store values to SQLite: $error");
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> migrateStoreToIsar(Isar db, Drift drift) async {
|
||||
try {
|
||||
final driftStoreValues = await drift.storeEntity
|
||||
.select()
|
||||
.map((entity) => StoreValue(entity.id, intValue: entity.intValue, strValue: entity.stringValue))
|
||||
.get();
|
||||
|
||||
await db.writeTxn(() async {
|
||||
await db.storeValues.putAll(driftStoreValues);
|
||||
});
|
||||
} catch (error) {
|
||||
debugPrint("[MIGRATION] Error while migrating store values to Isar: $error");
|
||||
}
|
||||
}
|
||||
|
||||
class _DeviceAsset {
|
||||
final String assetId;
|
||||
final List<int>? hash;
|
||||
|
|
|
|||
|
|
@ -89,7 +89,6 @@ class _BetaTimelineListTileState extends ConsumerState<BetaTimelineListTile> wit
|
|||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
Navigator.of(context).pop();
|
||||
await ref.read(appSettingsServiceProvider).setSetting(AppSettingsEnum.betaTimeline, value);
|
||||
context.router.replaceAll([ChangeExperienceRoute(switchingToBeta: value)]);
|
||||
},
|
||||
child: Text("ok".t(context: context)),
|
||||
|
|
|
|||
|
|
@ -64,12 +64,12 @@ void main() {
|
|||
|
||||
group("Log Service Set Level:", () {
|
||||
setUp(() async {
|
||||
when(() => mockStoreRepo.insert<int>(StoreKey.logLevel, any())).thenAnswer((_) async => true);
|
||||
when(() => mockStoreRepo.upsert<int>(StoreKey.logLevel, any())).thenAnswer((_) async => true);
|
||||
await sut.setLogLevel(LogLevel.shout);
|
||||
});
|
||||
|
||||
test('Updates the log level in store', () {
|
||||
final index = verify(() => mockStoreRepo.insert<int>(StoreKey.logLevel, captureAny())).captured.firstOrNull;
|
||||
final index = verify(() => mockStoreRepo.upsert<int>(StoreKey.logLevel, captureAny())).captured.firstOrNull;
|
||||
expect(index, LogLevel.shout.index);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -16,11 +16,13 @@ final _kBackupFailedSince = DateTime.utc(2023);
|
|||
void main() {
|
||||
late StoreService sut;
|
||||
late IsarStoreRepository mockStoreRepo;
|
||||
late StreamController<StoreDto<Object>> controller;
|
||||
late DriftStoreRepository mockDriftStoreRepo;
|
||||
late StreamController<List<StoreDto<Object>>> controller;
|
||||
|
||||
setUp(() async {
|
||||
controller = StreamController<StoreDto<Object>>.broadcast();
|
||||
controller = StreamController<List<StoreDto<Object>>>.broadcast();
|
||||
mockStoreRepo = MockStoreRepository();
|
||||
mockDriftStoreRepo = MockDriftStoreRepository();
|
||||
// For generics, we need to provide fallback to each concrete type to avoid runtime errors
|
||||
registerFallbackValue(StoreKey.accessToken);
|
||||
registerFallbackValue(StoreKey.backupTriggerDelay);
|
||||
|
|
@ -37,6 +39,16 @@ void main() {
|
|||
);
|
||||
when(() => mockStoreRepo.watchAll()).thenAnswer((_) => controller.stream);
|
||||
|
||||
when(() => mockDriftStoreRepo.getAll()).thenAnswer(
|
||||
(_) async => [
|
||||
const StoreDto(StoreKey.accessToken, _kAccessToken),
|
||||
const StoreDto(StoreKey.backgroundBackup, _kBackgroundBackup),
|
||||
const StoreDto(StoreKey.groupAssetsBy, _kGroupAssetsBy),
|
||||
StoreDto(StoreKey.backupFailedSince, _kBackupFailedSince),
|
||||
],
|
||||
);
|
||||
when(() => mockDriftStoreRepo.watchAll()).thenAnswer((_) => controller.stream);
|
||||
|
||||
sut = await StoreService.create(storeRepository: mockStoreRepo);
|
||||
});
|
||||
|
||||
|
|
@ -58,7 +70,7 @@ void main() {
|
|||
|
||||
test('Listens to stream of store updates', () async {
|
||||
final event = StoreDto(StoreKey.accessToken, _kAccessToken.toUpperCase());
|
||||
controller.add(event);
|
||||
controller.add([event]);
|
||||
|
||||
await pumpEventQueue();
|
||||
|
||||
|
|
@ -83,18 +95,19 @@ void main() {
|
|||
|
||||
group('Store Service put:', () {
|
||||
setUp(() {
|
||||
when(() => mockStoreRepo.insert<String>(any<StoreKey<String>>(), any())).thenAnswer((_) async => true);
|
||||
when(() => mockStoreRepo.upsert<String>(any<StoreKey<String>>(), any())).thenAnswer((_) async => true);
|
||||
when(() => mockDriftStoreRepo.upsert<String>(any<StoreKey<String>>(), any())).thenAnswer((_) async => true);
|
||||
});
|
||||
|
||||
test('Skip insert when value is not modified', () async {
|
||||
await sut.put(StoreKey.accessToken, _kAccessToken);
|
||||
verifyNever(() => mockStoreRepo.insert<String>(StoreKey.accessToken, any()));
|
||||
verifyNever(() => mockStoreRepo.upsert<String>(StoreKey.accessToken, any()));
|
||||
});
|
||||
|
||||
test('Insert value when modified', () async {
|
||||
final newAccessToken = _kAccessToken.toUpperCase();
|
||||
await sut.put(StoreKey.accessToken, newAccessToken);
|
||||
verify(() => mockStoreRepo.insert<String>(StoreKey.accessToken, newAccessToken)).called(1);
|
||||
verify(() => mockStoreRepo.upsert<String>(StoreKey.accessToken, newAccessToken)).called(1);
|
||||
expect(sut.tryGet(StoreKey.accessToken), newAccessToken);
|
||||
});
|
||||
});
|
||||
|
|
@ -105,6 +118,7 @@ void main() {
|
|||
setUp(() {
|
||||
valueController = StreamController<String?>.broadcast();
|
||||
when(() => mockStoreRepo.watch<String>(any<StoreKey<String>>())).thenAnswer((_) => valueController.stream);
|
||||
when(() => mockDriftStoreRepo.watch<String>(any<StoreKey<String>>())).thenAnswer((_) => valueController.stream);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
|
|
@ -129,6 +143,7 @@ void main() {
|
|||
group('Store Service delete:', () {
|
||||
setUp(() {
|
||||
when(() => mockStoreRepo.delete<String>(any<StoreKey<String>>())).thenAnswer((_) async => true);
|
||||
when(() => mockDriftStoreRepo.delete<String>(any<StoreKey<String>>())).thenAnswer((_) async => true);
|
||||
});
|
||||
|
||||
test('Removes the value from the DB', () async {
|
||||
|
|
@ -145,6 +160,7 @@ void main() {
|
|||
group('Store Service clear:', () {
|
||||
setUp(() {
|
||||
when(() => mockStoreRepo.deleteAll()).thenAnswer((_) async => true);
|
||||
when(() => mockDriftStoreRepo.deleteAll()).thenAnswer((_) async => true);
|
||||
});
|
||||
|
||||
test('Clears all values from the store', () async {
|
||||
|
|
|
|||
BIN
mobile/test/drift/main/generated/schema.dart
generated
BIN
mobile/test/drift/main/generated/schema.dart
generated
Binary file not shown.
BIN
mobile/test/drift/main/generated/schema_v8.dart
generated
Normal file
BIN
mobile/test/drift/main/generated/schema_v8.dart
generated
Normal file
Binary file not shown.
|
|
@ -44,7 +44,7 @@ void main() {
|
|||
test('converts int', () async {
|
||||
int? version = await sut.tryGet(StoreKey.version);
|
||||
expect(version, isNull);
|
||||
await sut.insert(StoreKey.version, _kTestVersion);
|
||||
await sut.upsert(StoreKey.version, _kTestVersion);
|
||||
version = await sut.tryGet(StoreKey.version);
|
||||
expect(version, _kTestVersion);
|
||||
});
|
||||
|
|
@ -52,7 +52,7 @@ void main() {
|
|||
test('converts string', () async {
|
||||
String? accessToken = await sut.tryGet(StoreKey.accessToken);
|
||||
expect(accessToken, isNull);
|
||||
await sut.insert(StoreKey.accessToken, _kTestAccessToken);
|
||||
await sut.upsert(StoreKey.accessToken, _kTestAccessToken);
|
||||
accessToken = await sut.tryGet(StoreKey.accessToken);
|
||||
expect(accessToken, _kTestAccessToken);
|
||||
});
|
||||
|
|
@ -60,7 +60,7 @@ void main() {
|
|||
test('converts datetime', () async {
|
||||
DateTime? backupFailedSince = await sut.tryGet(StoreKey.backupFailedSince);
|
||||
expect(backupFailedSince, isNull);
|
||||
await sut.insert(StoreKey.backupFailedSince, _kTestBackupFailed);
|
||||
await sut.upsert(StoreKey.backupFailedSince, _kTestBackupFailed);
|
||||
backupFailedSince = await sut.tryGet(StoreKey.backupFailedSince);
|
||||
expect(backupFailedSince, _kTestBackupFailed);
|
||||
});
|
||||
|
|
@ -68,7 +68,7 @@ void main() {
|
|||
test('converts bool', () async {
|
||||
bool? colorfulInterface = await sut.tryGet(StoreKey.colorfulInterface);
|
||||
expect(colorfulInterface, isNull);
|
||||
await sut.insert(StoreKey.colorfulInterface, _kTestColorfulInterface);
|
||||
await sut.upsert(StoreKey.colorfulInterface, _kTestColorfulInterface);
|
||||
colorfulInterface = await sut.tryGet(StoreKey.colorfulInterface);
|
||||
expect(colorfulInterface, _kTestColorfulInterface);
|
||||
});
|
||||
|
|
@ -76,7 +76,7 @@ void main() {
|
|||
test('converts user', () async {
|
||||
UserDto? user = await sut.tryGet(StoreKey.currentUser);
|
||||
expect(user, isNull);
|
||||
await sut.insert(StoreKey.currentUser, _kTestUser);
|
||||
await sut.upsert(StoreKey.currentUser, _kTestUser);
|
||||
user = await sut.tryGet(StoreKey.currentUser);
|
||||
expect(user, _kTestUser);
|
||||
});
|
||||
|
|
@ -108,10 +108,10 @@ void main() {
|
|||
await _populateStore(db);
|
||||
});
|
||||
|
||||
test('update()', () async {
|
||||
test('upsert()', () async {
|
||||
int? version = await sut.tryGet(StoreKey.version);
|
||||
expect(version, _kTestVersion);
|
||||
await sut.update(StoreKey.version, _kTestVersion + 10);
|
||||
await sut.upsert(StoreKey.version, _kTestVersion + 10);
|
||||
version = await sut.tryGet(StoreKey.version);
|
||||
expect(version, _kTestVersion + 10);
|
||||
});
|
||||
|
|
@ -126,22 +126,29 @@ void main() {
|
|||
final stream = sut.watch(StoreKey.version);
|
||||
expectLater(stream, emitsInOrder([_kTestVersion, _kTestVersion + 10]));
|
||||
await pumpEventQueue();
|
||||
await sut.update(StoreKey.version, _kTestVersion + 10);
|
||||
await sut.upsert(StoreKey.version, _kTestVersion + 10);
|
||||
});
|
||||
|
||||
test('watchAll()', () async {
|
||||
final stream = sut.watchAll();
|
||||
expectLater(
|
||||
stream,
|
||||
emitsInAnyOrder([
|
||||
emits(const StoreDto<Object>(StoreKey.version, _kTestVersion)),
|
||||
emits(StoreDto<Object>(StoreKey.backupFailedSince, _kTestBackupFailed)),
|
||||
emits(const StoreDto<Object>(StoreKey.accessToken, _kTestAccessToken)),
|
||||
emits(const StoreDto<Object>(StoreKey.colorfulInterface, _kTestColorfulInterface)),
|
||||
emits(const StoreDto<Object>(StoreKey.version, _kTestVersion + 10)),
|
||||
emitsInOrder([
|
||||
[
|
||||
const StoreDto<Object>(StoreKey.version, _kTestVersion),
|
||||
StoreDto<Object>(StoreKey.backupFailedSince, _kTestBackupFailed),
|
||||
const StoreDto<Object>(StoreKey.accessToken, _kTestAccessToken),
|
||||
const StoreDto<Object>(StoreKey.colorfulInterface, _kTestColorfulInterface),
|
||||
],
|
||||
[
|
||||
const StoreDto<Object>(StoreKey.version, _kTestVersion + 10),
|
||||
StoreDto<Object>(StoreKey.backupFailedSince, _kTestBackupFailed),
|
||||
const StoreDto<Object>(StoreKey.accessToken, _kTestAccessToken),
|
||||
const StoreDto<Object>(StoreKey.colorfulInterface, _kTestColorfulInterface),
|
||||
],
|
||||
]),
|
||||
);
|
||||
await sut.update(StoreKey.version, _kTestVersion + 10);
|
||||
await sut.upsert(StoreKey.version, _kTestVersion + 10);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ import 'package:mocktail/mocktail.dart';
|
|||
|
||||
class MockStoreRepository extends Mock implements IsarStoreRepository {}
|
||||
|
||||
class MockDriftStoreRepository extends Mock implements DriftStoreRepository {}
|
||||
|
||||
class MockLogRepository extends Mock implements LogRepository {}
|
||||
|
||||
class MockIsarUserRepository extends Mock implements IsarUserRepository {}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user