service: Make [XmppState] and [PreferencesState] migratable
This commit is contained in:
parent
cf353ce5ce
commit
75811d7dee
@ -30,6 +30,7 @@ files:
|
||||
jid: String?
|
||||
displayName: String?
|
||||
avatarUrl: String?
|
||||
avatarHash: String?
|
||||
conversations:
|
||||
type: List<Conversation>?
|
||||
deserialise: true
|
||||
|
@ -54,16 +54,14 @@ Future<void> performPreStart(PerformPreStartCommand command, { dynamic extra })
|
||||
final id = extra as String;
|
||||
|
||||
final xmpp = GetIt.I.get<XmppService>();
|
||||
final account = await xmpp.getAccountData();
|
||||
final settings = await xmpp.getConnectionSettings();
|
||||
final state = await xmpp.getXmppState();
|
||||
final preferences = await GetIt.I.get<PreferencesService>().getPreferences();
|
||||
|
||||
|
||||
GetIt.I.get<Logger>().finest("account != null: " + (account != null).toString());
|
||||
GetIt.I.get<Logger>().finest("settings != null: " + (settings != null).toString());
|
||||
|
||||
if (account!= null && settings != null) {
|
||||
if (settings != null && settings.jid != null && settings.password != null) {
|
||||
await GetIt.I.get<RosterService>().loadRosterFromDatabase();
|
||||
|
||||
// Check some permissions
|
||||
@ -80,9 +78,10 @@ Future<void> performPreStart(PerformPreStartCommand command, { dynamic extra })
|
||||
sendEvent(
|
||||
PreStartDoneEvent(
|
||||
state: "logged_in",
|
||||
jid: account.jid,
|
||||
displayName: account.displayName,
|
||||
jid: state.jid,
|
||||
displayName: state.displayName,
|
||||
avatarUrl: state.avatarUrl,
|
||||
avatarHash: state.avatarHash,
|
||||
permissionsToRequest: permissions,
|
||||
preferences: preferences,
|
||||
conversations: await GetIt.I.get<DatabaseService>().loadConversations(),
|
||||
@ -264,7 +263,8 @@ Future<void> performRequestDownload(RequestDownloadCommand command, { dynamic ex
|
||||
|
||||
Future<void> performSetAvatar(SetAvatarCommand command, { dynamic extra }) async {
|
||||
await GetIt.I.get<XmppService>().modifyXmppState((state) => state.copyWith(
|
||||
avatarUrl: command.path
|
||||
avatarUrl: command.path,
|
||||
avatarHash: command.hash
|
||||
));
|
||||
GetIt.I.get<AvatarService>().publishAvatar(command.path, command.hash);
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import "dart:convert";
|
||||
|
||||
import "package:moxxyv2/shared/preferences.dart";
|
||||
import "package:moxxyv2/shared/migrator.dart";
|
||||
|
||||
import "package:flutter_secure_storage/flutter_secure_storage.dart";
|
||||
import "package:logging/logging.dart";
|
||||
@ -9,15 +10,66 @@ const currentVersion = 7;
|
||||
const preferencesVersionKey = "prefs_version";
|
||||
const preferencesDataKey = "prefs_data";
|
||||
|
||||
class PreferencesService {
|
||||
int _version = -1;
|
||||
PreferencesState? _preferences;
|
||||
class _PreferencesMigrator extends Migrator<PreferencesState> {
|
||||
final FlutterSecureStorage _storage = const FlutterSecureStorage(
|
||||
aOptions: AndroidOptions(encryptedSharedPreferences: true)
|
||||
);
|
||||
final Logger _log;
|
||||
|
||||
PreferencesService() : _log = Logger("PreferencesService");
|
||||
_PreferencesMigrator() : super(
|
||||
currentVersion,
|
||||
[
|
||||
Migration<PreferencesState>(1, (data) => PreferencesState(
|
||||
sendChatMarkers: data["sendChatMarkers"]!,
|
||||
sendChatStates: data["sendChatStates"]!,
|
||||
showSubscriptionRequests: data["showSubscriptionRequests"]!
|
||||
)),
|
||||
Migration<PreferencesState>(2, (data) => PreferencesState(
|
||||
sendChatMarkers: data["sendChatMarkers"]!,
|
||||
sendChatStates: data["sendChatStates"]!,
|
||||
showSubscriptionRequests: data["showSubscriptionRequests"]!,
|
||||
autoDownloadWifi: data["autoDownloadWifi"]!,
|
||||
autoDownloadMobile: data["autoDownloadMobile"]!
|
||||
)),
|
||||
Migration<PreferencesState>(3, (data) => PreferencesState(
|
||||
sendChatMarkers: data["sendChatMarkers"]!,
|
||||
sendChatStates: data["sendChatStates"]!,
|
||||
showSubscriptionRequests: data["showSubscriptionRequests"]!,
|
||||
autoDownloadWifi: data["autoDownloadWifi"]!,
|
||||
autoDownloadMobile: data["autoDownloadMobile"]!,
|
||||
maximumAutoDownloadSize: data["maximumAutoDownloadSize"]!
|
||||
)),
|
||||
Migration<PreferencesState>(4, (data) => PreferencesState(
|
||||
sendChatMarkers: data["sendChatMarkers"]!,
|
||||
sendChatStates: data["sendChatStates"]!,
|
||||
showSubscriptionRequests: data["showSubscriptionRequests"]!,
|
||||
autoDownloadWifi: data["autoDownloadWifi"]!,
|
||||
autoDownloadMobile: data["autoDownloadMobile"]!,
|
||||
maximumAutoDownloadSize: data["maximumAutoDownloadSize"]!,
|
||||
backgroundPath: data["backgroundPath"]!
|
||||
)),
|
||||
Migration<PreferencesState>(5, (data) => PreferencesState(
|
||||
sendChatMarkers: data["sendChatMarkers"]!,
|
||||
sendChatStates: data["sendChatStates"]!,
|
||||
showSubscriptionRequests: data["showSubscriptionRequests"]!,
|
||||
autoDownloadWifi: data["autoDownloadWifi"]!,
|
||||
autoDownloadMobile: data["autoDownloadMobile"]!,
|
||||
maximumAutoDownloadSize: data["maximumAutoDownloadSize"]!,
|
||||
backgroundPath: data["backgroundPath"]!,
|
||||
isAvatarPublic: data["isAvatarPublic"]!
|
||||
)),
|
||||
Migration<PreferencesState>(6, (data) => PreferencesState(
|
||||
sendChatMarkers: data["sendChatMarkers"]!,
|
||||
sendChatStates: data["sendChatStates"]!,
|
||||
showSubscriptionRequests: data["showSubscriptionRequests"]!,
|
||||
autoDownloadWifi: data["autoDownloadWifi"]!,
|
||||
autoDownloadMobile: data["autoDownloadMobile"]!,
|
||||
maximumAutoDownloadSize: data["maximumAutoDownloadSize"]!,
|
||||
backgroundPath: data["backgroundPath"]!,
|
||||
isAvatarPublic: data["isAvatarPublic"]!,
|
||||
autoAcceptSubscriptionRequests: data["autoAcceptSubscriptionRequests"]
|
||||
))
|
||||
]
|
||||
);
|
||||
|
||||
// TODO: Deduplicate with XmppService. Maybe a StorageService?
|
||||
Future<String?> _readKeyOrNull(String key) async {
|
||||
@ -28,110 +80,44 @@ class PreferencesService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _commitPreferences() async {
|
||||
await _storage.write(key: preferencesVersionKey, value: _version.toString());
|
||||
await _storage.write(key: preferencesDataKey, value: json.encode(_preferences!.toJson()));
|
||||
@override
|
||||
Future<Map<String, dynamic>?> loadRawData() async {
|
||||
final raw = await _readKeyOrNull(preferencesDataKey);
|
||||
if (raw != null) return json.decode(raw);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int?> loadVersion() async {
|
||||
final raw = await _readKeyOrNull(preferencesVersionKey);
|
||||
if (raw != null) return int.parse(raw);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
PreferencesState fromData(Map<String, dynamic> data) => PreferencesState.fromJson(data);
|
||||
|
||||
@override
|
||||
PreferencesState fromDefault() => PreferencesState();
|
||||
|
||||
@override
|
||||
Future<void> commit(int version, PreferencesState data) async {
|
||||
await _storage.write(key: preferencesVersionKey, value: version.toString());
|
||||
await _storage.write(key: preferencesDataKey, value: json.encode(data.toJson()));
|
||||
}
|
||||
}
|
||||
|
||||
class PreferencesService {
|
||||
PreferencesState? _preferences;
|
||||
final _PreferencesMigrator _migrator;
|
||||
final Logger _log;
|
||||
|
||||
PreferencesService() : _migrator = _PreferencesMigrator(), _log = Logger("PreferencesService");
|
||||
|
||||
Future<void> _loadPreferences() async {
|
||||
final version = int.parse((await _readKeyOrNull(preferencesVersionKey)) ?? "-1");
|
||||
final dataRaw = await _readKeyOrNull(preferencesDataKey);
|
||||
|
||||
if (version < 1 || dataRaw == null) {
|
||||
_log.finest("Creating preferences...");
|
||||
_preferences = PreferencesState();
|
||||
_version = currentVersion;
|
||||
await _commitPreferences();
|
||||
} else if(version < 2) {
|
||||
final data = json.decode(dataRaw);
|
||||
|
||||
_log.finest("Upgrading from a 0 < version < 2 to current version");
|
||||
_preferences = PreferencesState(
|
||||
sendChatMarkers: data["sendChatMarkers"]!,
|
||||
sendChatStates: data["sendChatStates"]!,
|
||||
showSubscriptionRequests: data["showSubscriptionRequests"]!
|
||||
);
|
||||
_version = currentVersion;
|
||||
await _commitPreferences();
|
||||
} else if (version < 3) {
|
||||
final data = json.decode(dataRaw);
|
||||
|
||||
_log.finest("Upgrading from a 1 < version < 3 to current version");
|
||||
_preferences = PreferencesState(
|
||||
sendChatMarkers: data["sendChatMarkers"]!,
|
||||
sendChatStates: data["sendChatStates"]!,
|
||||
showSubscriptionRequests: data["showSubscriptionRequests"]!,
|
||||
autoDownloadWifi: data["autoDownloadWifi"]!,
|
||||
autoDownloadMobile: data["autoDownloadMobile"]!
|
||||
);
|
||||
_version = currentVersion;
|
||||
await _commitPreferences();
|
||||
} else if (version < 4) {
|
||||
final data = json.decode(dataRaw);
|
||||
|
||||
_log.finest("Upgrading from a 2 < version < 4 to current version");
|
||||
_preferences = PreferencesState(
|
||||
sendChatMarkers: data["sendChatMarkers"]!,
|
||||
sendChatStates: data["sendChatStates"]!,
|
||||
showSubscriptionRequests: data["showSubscriptionRequests"]!,
|
||||
autoDownloadWifi: data["autoDownloadWifi"]!,
|
||||
autoDownloadMobile: data["autoDownloadMobile"]!,
|
||||
maximumAutoDownloadSize: data["maximumAutoDownloadSize"]!
|
||||
);
|
||||
_version = currentVersion;
|
||||
await _commitPreferences();
|
||||
} else if (version < 5) {
|
||||
final data = json.decode(dataRaw);
|
||||
|
||||
_log.finest("Upgrading from a 4 < version < 5 to current version");
|
||||
_preferences = PreferencesState(
|
||||
sendChatMarkers: data["sendChatMarkers"]!,
|
||||
sendChatStates: data["sendChatStates"]!,
|
||||
showSubscriptionRequests: data["showSubscriptionRequests"]!,
|
||||
autoDownloadWifi: data["autoDownloadWifi"]!,
|
||||
autoDownloadMobile: data["autoDownloadMobile"]!,
|
||||
maximumAutoDownloadSize: data["maximumAutoDownloadSize"]!,
|
||||
backgroundPath: data["backgroundPath"]!
|
||||
);
|
||||
_version = currentVersion;
|
||||
await _commitPreferences();
|
||||
} else if (version < 6) {
|
||||
final data = json.decode(dataRaw);
|
||||
|
||||
_log.finest("Upgrading from a 5 < version < 6 to current version");
|
||||
_preferences = PreferencesState(
|
||||
sendChatMarkers: data["sendChatMarkers"]!,
|
||||
sendChatStates: data["sendChatStates"]!,
|
||||
showSubscriptionRequests: data["showSubscriptionRequests"]!,
|
||||
autoDownloadWifi: data["autoDownloadWifi"]!,
|
||||
autoDownloadMobile: data["autoDownloadMobile"]!,
|
||||
maximumAutoDownloadSize: data["maximumAutoDownloadSize"]!,
|
||||
backgroundPath: data["backgroundPath"]!,
|
||||
isAvatarPublic: data["isAvatarPublic"]!
|
||||
);
|
||||
_version = currentVersion;
|
||||
await _commitPreferences();
|
||||
} else if (version < 7) {
|
||||
final data = json.decode(dataRaw);
|
||||
|
||||
_log.finest("Upgrading from a 6 < version < 7 to current version");
|
||||
_preferences = PreferencesState(
|
||||
sendChatMarkers: data["sendChatMarkers"]!,
|
||||
sendChatStates: data["sendChatStates"]!,
|
||||
showSubscriptionRequests: data["showSubscriptionRequests"]!,
|
||||
autoDownloadWifi: data["autoDownloadWifi"]!,
|
||||
autoDownloadMobile: data["autoDownloadMobile"]!,
|
||||
maximumAutoDownloadSize: data["maximumAutoDownloadSize"]!,
|
||||
backgroundPath: data["backgroundPath"]!,
|
||||
isAvatarPublic: data["isAvatarPublic"]!,
|
||||
autoAcceptSubscriptionRequests: data["autoAcceptSubscriptionRequests"]
|
||||
);
|
||||
_version = currentVersion;
|
||||
await _commitPreferences();
|
||||
} else {
|
||||
_version = currentVersion;
|
||||
_preferences = PreferencesState.fromJson(json.decode(dataRaw));
|
||||
}
|
||||
_preferences = await _migrator.load();
|
||||
}
|
||||
|
||||
Future<PreferencesState> getPreferences() async {
|
||||
@ -144,6 +130,6 @@ class PreferencesService {
|
||||
if (_preferences == null) await _loadPreferences();
|
||||
|
||||
_preferences = func(_preferences!);
|
||||
await _commitPreferences();
|
||||
await _migrator.commit(currentVersion, _preferences!);
|
||||
}
|
||||
}
|
||||
|
@ -194,10 +194,9 @@ void onStart() {
|
||||
]);
|
||||
GetIt.I.registerSingleton<XmppConnection>(connection);
|
||||
|
||||
final account = await xmpp.getAccountData();
|
||||
final settings = await xmpp.getConnectionSettings();
|
||||
|
||||
if (account!= null && settings != null) {
|
||||
if (settings != null && settings.jid != null && settings.password != null) {
|
||||
xmpp.connect(settings, false);
|
||||
}
|
||||
})();
|
||||
|
@ -22,9 +22,11 @@ class XmppState with _$XmppState {
|
||||
String? srid,
|
||||
String? resource,
|
||||
String? jid,
|
||||
String? displayName,
|
||||
String? password,
|
||||
String? lastRosterVersion,
|
||||
@Default("") avatarUrl,
|
||||
@Default("") avatarHash,
|
||||
@Default(false) bool askedStoragePermission
|
||||
}) = _XmppState;
|
||||
|
||||
|
@ -4,8 +4,8 @@ import "dart:convert";
|
||||
import "package:moxxyv2/ui/helpers.dart";
|
||||
import "package:moxxyv2/shared/events.dart";
|
||||
import "package:moxxyv2/shared/helpers.dart";
|
||||
import "package:moxxyv2/shared/account.dart";
|
||||
import "package:moxxyv2/shared/eventhandler.dart";
|
||||
import "package:moxxyv2/shared/migrator.dart";
|
||||
import "package:moxxyv2/shared/models/message.dart";
|
||||
import "package:moxxyv2/xmpp/settings.dart";
|
||||
import "package:moxxyv2/xmpp/jid.dart";
|
||||
@ -37,20 +37,63 @@ import "package:flutter_background_service/flutter_background_service.dart";
|
||||
import "package:logging/logging.dart";
|
||||
import "package:permission_handler/permission_handler.dart";
|
||||
|
||||
const currentXmppStateVersion = 1;
|
||||
const xmppStateKey = "xmppState";
|
||||
const xmppAccountDataKey = "xmppAccount";
|
||||
const xmppStateVersionKey = "xmppState_version";
|
||||
|
||||
class XmppService {
|
||||
class _XmppStateMigrator extends Migrator<XmppState> {
|
||||
final FlutterSecureStorage _storage = const FlutterSecureStorage(
|
||||
aOptions: AndroidOptions(encryptedSharedPreferences: true)
|
||||
);
|
||||
|
||||
_XmppStateMigrator() : super(currentXmppStateVersion, []);
|
||||
|
||||
// TODO: Deduplicate
|
||||
Future<String?> _readKeyOrNull(String key) async {
|
||||
if (await _storage.containsKey(key: key)) {
|
||||
return await _storage.read(key: key);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>?> loadRawData() async {
|
||||
final raw = await _readKeyOrNull(xmppStateKey);
|
||||
if (raw != null) return json.decode(raw);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int?> loadVersion() async {
|
||||
final raw = await _readKeyOrNull(xmppStateVersionKey);
|
||||
if (raw != null) return int.parse(raw);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
XmppState fromData(Map<String, dynamic> data) => XmppState.fromJson(data);
|
||||
|
||||
@override
|
||||
XmppState fromDefault() => XmppState();
|
||||
|
||||
@override
|
||||
Future<void> commit(int version, XmppState data) async {
|
||||
await _storage.write(key: xmppStateVersionKey, value: currentXmppStateVersion.toString());
|
||||
await _storage.write(key: xmppStateKey, value: json.encode(data.toJson()));
|
||||
}
|
||||
}
|
||||
|
||||
class XmppService {
|
||||
final Logger _log;
|
||||
final EventHandler _eventHandler;
|
||||
final _XmppStateMigrator _migrator;
|
||||
bool loginTriggeredFromUI = false;
|
||||
String _currentlyOpenedChatJid;
|
||||
StreamSubscription<ConnectivityResult>? _networkStateSubscription;
|
||||
XmppState? _state;
|
||||
|
||||
ConnectivityResult _currentConnectionType;
|
||||
|
||||
XmppService() :
|
||||
@ -59,6 +102,7 @@ class XmppService {
|
||||
_state = null,
|
||||
_currentConnectionType = ConnectivityResult.none,
|
||||
_eventHandler = EventHandler(),
|
||||
_migrator = _XmppStateMigrator(),
|
||||
_log = Logger("XmppService") {
|
||||
_eventHandler.addMatchers([
|
||||
EventTypeMatcher<ConnectionStateChangedEvent>(_onConnectionStateChanged),
|
||||
@ -77,41 +121,17 @@ class XmppService {
|
||||
]);
|
||||
}
|
||||
|
||||
Future<String?> _readKeyOrNull(String key) async {
|
||||
if (await _storage.containsKey(key: key)) {
|
||||
return await _storage.read(key: key);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<XmppState> getXmppState() async {
|
||||
if (_state != null) return _state!;
|
||||
|
||||
final data = await _readKeyOrNull(xmppStateKey);
|
||||
// GetIt.I.get<Logger>().finest("data != null: " + (data != null).toString());
|
||||
|
||||
if (data == null) {
|
||||
_state = XmppState();
|
||||
await _commitXmppState();
|
||||
_state = await _migrator.load();
|
||||
return _state!;
|
||||
}
|
||||
|
||||
_state = XmppState.fromJson(json.decode(data));
|
||||
return _state!;
|
||||
}
|
||||
|
||||
Future<void> _commitXmppState() async {
|
||||
// final logger = GetIt.I.get<Logger>();
|
||||
// logger.finest("Commiting _xmppState to EncryptedSharedPrefs");
|
||||
// logger.finest("=> ${json.encode(_state!.toJson())}");
|
||||
await _storage.write(key: xmppStateKey, value: json.encode(_state!.toJson()));
|
||||
}
|
||||
|
||||
/// A wrapper to modify the [XmppState] and commit it.
|
||||
Future<void> modifyXmppState(XmppState Function(XmppState) func) async {
|
||||
_state = func(_state!);
|
||||
await _commitXmppState();
|
||||
await _migrator.commit(currentXmppStateVersion, _state!);
|
||||
}
|
||||
|
||||
Future<ConnectionSettings?> getConnectionSettings() async {
|
||||
@ -152,25 +172,6 @@ class XmppService {
|
||||
/// Returns the JID of the chat that is currently opened. Null, if none is open.
|
||||
String? getCurrentlyOpenedChatJid() => _currentlyOpenedChatJid;
|
||||
|
||||
/// Load the [AccountState] from storage. Returns null if not found.
|
||||
Future<AccountState?> getAccountData() async {
|
||||
final data = await _readKeyOrNull(xmppAccountDataKey);
|
||||
if (data == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return AccountState.fromJson(jsonDecode(data));
|
||||
}
|
||||
/// Save [state] to storage such that it can be loaded again by [getAccountData].
|
||||
Future<void> setAccountData(AccountState state) async {
|
||||
return await _storage.write(key: xmppAccountDataKey, value: jsonEncode(state.toJson()));
|
||||
}
|
||||
/// Removes the account data from storage.
|
||||
Future<void> removeAccountData() async {
|
||||
// TODO: This sometimes fails
|
||||
await _storage.delete(key: xmppAccountDataKey);
|
||||
}
|
||||
|
||||
/// Sends a message to [jid] with the body of [body].
|
||||
Future<void> sendMessage({
|
||||
required String body,
|
||||
@ -365,10 +366,11 @@ class XmppService {
|
||||
|
||||
if (loginTriggeredFromUI) {
|
||||
// TODO: Trigger another event so the UI can see this aswell
|
||||
await setAccountData(AccountState(
|
||||
await modifyXmppState((state) => state.copyWith(
|
||||
jid: connection.getConnectionSettings().jid.toString(),
|
||||
displayName: connection.getConnectionSettings().jid.local,
|
||||
avatarUrl: ""
|
||||
avatarUrl: "",
|
||||
avatarHash: ""
|
||||
));
|
||||
}
|
||||
}
|
||||
|
@ -1,16 +0,0 @@
|
||||
import "package:freezed_annotation/freezed_annotation.dart";
|
||||
|
||||
part "account.freezed.dart";
|
||||
part "account.g.dart";
|
||||
|
||||
@freezed
|
||||
class AccountState with _$AccountState {
|
||||
factory AccountState({
|
||||
@Default("") String jid,
|
||||
@Default("") String displayName,
|
||||
@Default("") String avatarUrl
|
||||
}) = _AccountState;
|
||||
|
||||
// JSON serialization
|
||||
factory AccountState.fromJson(Map<String, dynamic> json) => _$AccountStateFromJson(json);
|
||||
}
|
57
lib/shared/migrator.dart
Normal file
57
lib/shared/migrator.dart
Normal file
@ -0,0 +1,57 @@
|
||||
class Migration<T> {
|
||||
final int version;
|
||||
/// Return a version that is upgraded to the newest version.
|
||||
final Function(Map<String, dynamic>) migrationFunction;
|
||||
|
||||
Migration(this.version, this.migrationFunction);
|
||||
|
||||
bool canMigrate(int version) => version <= this.version;
|
||||
}
|
||||
|
||||
abstract class Migrator<T> {
|
||||
final int latestVersion;
|
||||
final List<Migration<T>> migrations;
|
||||
|
||||
Migrator(this.latestVersion, this.migrations) {
|
||||
migrations.sort((a, b) => -1 * a.version.compareTo(b.version));
|
||||
}
|
||||
|
||||
/// Override: Return the raw data or null if not set yet.
|
||||
Future<Map<String, dynamic>?> loadRawData();
|
||||
|
||||
/// Override: Return the version or null if not set yet.
|
||||
Future<int?> loadVersion();
|
||||
|
||||
/// Override: Return [T] from [data] if the data is already at the newest version.
|
||||
T fromData(Map<String, dynamic> data);
|
||||
|
||||
/// Override: If no data is available
|
||||
T fromDefault();
|
||||
|
||||
/// Override: Commit the latest version and data back to the store.
|
||||
Future<void> commit(int version, T data);
|
||||
|
||||
Future<T> load() async {
|
||||
final version = await loadVersion();
|
||||
final data = await loadRawData();
|
||||
if (version == null || data == null) {
|
||||
final ret = fromDefault();
|
||||
await commit(latestVersion, ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
if (version == latestVersion) return fromData(data);
|
||||
|
||||
for (final migration in migrations) {
|
||||
if (migration.canMigrate(version)) {
|
||||
final ret = migration.migrationFunction(data);
|
||||
await commit(latestVersion, ret);
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
final ret = fromDefault();
|
||||
await commit(latestVersion, ret);
|
||||
return ret;
|
||||
}
|
||||
}
|
187
test/migrations.dart
Normal file
187
test/migrations.dart
Normal file
@ -0,0 +1,187 @@
|
||||
import "package:moxxyv2/shared/migrator.dart";
|
||||
|
||||
import "package:test/test.dart";
|
||||
|
||||
class Greeting {
|
||||
final String action;
|
||||
final String entity;
|
||||
final bool beNice;
|
||||
|
||||
Greeting(this.entity, this.action, this.beNice);
|
||||
}
|
||||
|
||||
class TestMigrator extends Migrator<Greeting> {
|
||||
final void Function(int, Greeting) onCommited;
|
||||
TestMigrator(this.onCommited) : super(
|
||||
2, // Latest version
|
||||
[
|
||||
Migration<Greeting>(
|
||||
1,
|
||||
(data) => Greeting(
|
||||
data["name"]!,
|
||||
data["action"]!,
|
||||
true
|
||||
)
|
||||
)
|
||||
]
|
||||
);
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>?> loadRawData() async {
|
||||
return {
|
||||
"name": "Welt",
|
||||
"action": "welcome"
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int?> loadVersion() async => 1;
|
||||
|
||||
@override
|
||||
Greeting fromData(Map<String, dynamic> data) => Greeting(
|
||||
data["name"],
|
||||
data["action"],
|
||||
data["beNice"]
|
||||
);
|
||||
|
||||
@override
|
||||
Greeting fromDefault() => Greeting(
|
||||
"Moxxy",
|
||||
"hug",
|
||||
true
|
||||
);
|
||||
|
||||
Future<void> commit(int version, Greeting data) async {
|
||||
onCommited(version, data);
|
||||
}
|
||||
}
|
||||
|
||||
class NoDataMigrator extends Migrator<Greeting> {
|
||||
final void Function(int, Greeting) onCommited;
|
||||
NoDataMigrator(this.onCommited) : super(
|
||||
2, // Latest version
|
||||
[
|
||||
Migration<Greeting>(
|
||||
1,
|
||||
(data) => Greeting(
|
||||
data["name"]!,
|
||||
data["action"]!,
|
||||
true
|
||||
)
|
||||
)
|
||||
]
|
||||
);
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>?> loadRawData() async => null;
|
||||
|
||||
@override
|
||||
Future<int?> loadVersion() async => null;
|
||||
|
||||
@override
|
||||
Greeting fromData(Map<String, dynamic> data) => Greeting(
|
||||
data["name"],
|
||||
data["action"],
|
||||
data["beNice"]
|
||||
);
|
||||
|
||||
@override
|
||||
Greeting fromDefault() => Greeting(
|
||||
"Moxxyv2",
|
||||
"hug_more",
|
||||
true
|
||||
);
|
||||
|
||||
Future<void> commit(int version, Greeting data) async {
|
||||
onCommited(version, data);
|
||||
}
|
||||
}
|
||||
|
||||
class MultipleStagedMigrator extends Migrator<Greeting> {
|
||||
final void Function(int, Greeting) onCommited;
|
||||
MultipleStagedMigrator(this.onCommited) : super(
|
||||
3, // Latest version
|
||||
[
|
||||
Migration<Greeting>(
|
||||
1,
|
||||
(data) => Greeting(
|
||||
data["name"]!,
|
||||
"hug1",
|
||||
true
|
||||
)
|
||||
),
|
||||
Migration<Greeting>(
|
||||
2,
|
||||
(data) => Greeting(
|
||||
data["name"]!,
|
||||
"hug2",
|
||||
true
|
||||
)
|
||||
)
|
||||
]
|
||||
);
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>?> loadRawData() async {
|
||||
return {
|
||||
"name": "Welt",
|
||||
"action": "welcome"
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int?> loadVersion() async => 2;
|
||||
|
||||
@override
|
||||
Greeting fromData(Map<String, dynamic> data) => Greeting(
|
||||
data["name"],
|
||||
data["action"],
|
||||
data["beNice"]
|
||||
);
|
||||
|
||||
@override
|
||||
Greeting fromDefault() => Greeting(
|
||||
"Moxxyv2",
|
||||
"hug_more",
|
||||
true
|
||||
);
|
||||
|
||||
Future<void> commit(int version, Greeting data) async {
|
||||
onCommited(version, data);
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
test("Test a simple migration", () async {
|
||||
final mig = TestMigrator((v, g) {
|
||||
expect(v, 2);
|
||||
});
|
||||
final greeting = await mig.load();
|
||||
|
||||
expect(greeting.entity, "Welt");
|
||||
expect(greeting.action, "welcome");
|
||||
expect(greeting.beNice, true);
|
||||
});
|
||||
|
||||
test("Test loading data where there was none", () async {
|
||||
final mig = NoDataMigrator((v, g) {
|
||||
expect(v, 2);
|
||||
});
|
||||
final greeting = await mig.load();
|
||||
|
||||
expect(greeting.entity, "Moxxyv2");
|
||||
expect(greeting.action, "hug_more");
|
||||
expect(greeting.beNice, true);
|
||||
});
|
||||
|
||||
test("Test that only the correct stage is ran", () async {
|
||||
final mig = MultipleStagedMigrator((v, g) {
|
||||
expect(v, 3);
|
||||
});
|
||||
final greeting = await mig.load();
|
||||
|
||||
expect(greeting.entity, "Welt");
|
||||
expect(greeting.action, "hug2");
|
||||
expect(greeting.beNice, true);
|
||||
});
|
||||
}
|
Loading…
Reference in New Issue
Block a user