From 5f7ea18bcc6822f7efc52e7b57f922ca22ce50f8 Mon Sep 17 00:00:00 2001 From: Artem Kalachyan Date: Thu, 7 Oct 2021 16:18:20 +0300 Subject: [PATCH] Implement initial Hive to Sembast migration --- README.md | 2 +- lib/src/adapters/persistent_json_api.dart | 148 ++++++++++++++-------- lib/src/db_adapters/json_api.dart | 17 +++ lib/src/hive_adapters/json_api.dart | 27 ---- pubspec.yaml | 3 +- test/flutter_rest_data_test.dart | 57 ++++++--- test/helpers.dart | 7 - 7 files changed, 154 insertions(+), 107 deletions(-) create mode 100644 lib/src/db_adapters/json_api.dart delete mode 100644 lib/src/hive_adapters/json_api.dart delete mode 100644 test/helpers.dart diff --git a/README.md b/README.md index f5c31be..ce36752 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # flutter_rest_data -This package is built on top of [rest_data](https://github.com/algonauti/dart-rest-data). It uses [hive](https://github.com/hivedb/hive) to store on local device all data coming from your REST backend. +This package is built on top of [rest_data](https://github.com/algonauti/dart-rest-data). It uses [sembast](https://github.com/tekartik/sembast.dart) to store on local device all data coming from your REST backend. [![CI](https://github.com/algonauti/flutter-rest-data/workflows/CI/badge.svg)](https://github.com/algonauti/flutter-rest-data/actions) diff --git a/lib/src/adapters/persistent_json_api.dart b/lib/src/adapters/persistent_json_api.dart index f89f5c4..dad623b 100644 --- a/lib/src/adapters/persistent_json_api.dart +++ b/lib/src/adapters/persistent_json_api.dart @@ -1,11 +1,20 @@ -import 'package:hive/hive.dart'; -import 'package:hive_flutter/hive_flutter.dart'; +import 'package:path/path.dart'; +import 'package:path_provider/path_provider.dart'; import 'package:rest_data/rest_data.dart'; +import 'package:sembast/sembast.dart'; +import 'package:sembast/sembast_io.dart'; +import 'package:sembast/sembast_memory.dart'; + +import 'package:flutter_rest_data/src/db_adapters/json_api.dart'; import '../exceptions.dart'; -import '../hive_adapters/json_api.dart'; class PersistentJsonApiAdapter extends JsonApiAdapter { + JsonApiStoreAdapter _storeAdapter = JsonApiStoreAdapter(); + late DatabaseFactory _dbFactory; + late Database database; + late String _dbName; + PersistentJsonApiAdapter( String hostname, String apiPath, { @@ -26,38 +35,48 @@ class PersistentJsonApiAdapter extends JsonApiAdapter { isOnline = false; } - Future init() async { - await Hive.initFlutter(); - registerAdapters(); + Future init({String databaseName = 'persistent_json_api.db'}) async { + _dbName = databaseName; + _dbFactory = databaseFactoryIo; + var dbPath = await _buildDbPath(_dbName); + await _openDatabase(dbPath); } - void initTest() { - registerAdapters(); + Future initTest() async { + _dbName = 'flutter_rest_data.db'; + _dbFactory = databaseFactoryMemoryFs; + await _openDatabase(_dbName); } - void registerAdapters() { - Hive.registerAdapter(JsonApiHiveAdapter()); + Future _openDatabase(String dbPath) async { + database = await _dbFactory.openDatabase(dbPath); } - Future dispose() => Hive.close(); + Future _buildDbPath(String dbName) async { + var dir = await getApplicationDocumentsDirectory(); + await dir.create(recursive: true); + return join(dir.path, dbName); + } - Future dropBoxes() => Hive.deleteFromDisk(); + Future dispose() => database.close(); + + Future dropStores() => _dbFactory.deleteDatabase(_dbName); // TODO // - when connection gets back: - // - invoke super.save() on each doc in the 'added' box (endpoint can be computed from type) - // - invoke super.delete() on each doc in the 'removed' box (endpoint can be computed from type) + // - invoke super.save() on each doc in the 'added' store (endpoint can be computed from type) + // - invoke super.delete() on each doc in the 'removed' store (endpoint can be computed from type) @override Future fetch(String endpoint, String id) async { JsonApiDocument? doc; if (isOnline) { doc = await super.fetch(endpoint, id); - await boxPutOne(endpoint, doc); + await storePutOne(endpoint, doc); } else { doc = id.contains('added') ? (await findAdded(id)) - : (await boxGetOne(endpoint, id)); + : (await storeGetOne(endpoint, id)); } return doc!; } @@ -67,7 +86,7 @@ class PersistentJsonApiAdapter extends JsonApiAdapter { JsonApiManyDocument docs; if (isOnline) { docs = await super.findAll(endpoint); - await boxPutMany(endpoint, docs); + await storePutMany(endpoint, docs); } else { docs = await findAllPersisted(endpoint); cacheMany(endpoint, docs); @@ -81,11 +100,11 @@ class PersistentJsonApiAdapter extends JsonApiAdapter { JsonApiManyDocument docs; if (isOnline) { docs = await super.query(endpoint, params); - await boxPutMany(endpoint, docs); + await storePutMany(endpoint, docs); } else { if (params.containsKey('filter[id]')) { List ids = params['filter[id]']!.split(','); - docs = await boxGetMany(endpoint, ids); + docs = await storeGetMany(endpoint, ids); } else { docs = await findAllPersisted(endpoint); if (filter != null) { @@ -105,11 +124,11 @@ class PersistentJsonApiAdapter extends JsonApiAdapter { JsonApiDocument doc; if (isOnline) { doc = await super.save(endpoint, document); - await boxPutOne(endpoint, doc); + await storePutOne(endpoint, doc); } else { // TODO Handle Update case doc = document; - int id = await boxAdd(endpoint, doc); + int id = await storeAdd(endpoint, doc); doc.id = 'added:$id'; cache(endpoint, doc); } @@ -121,74 +140,95 @@ class PersistentJsonApiAdapter extends JsonApiAdapter { if (isOnline) { return super.performDelete(endpoint, doc); } else { - var removedBox = await openBox('removed'); - await removedBox.put('$endpoint:${doc.id}', doc); - (await openBox(endpoint)).delete(doc.id); + var removedStore = openStringKeyStore('removed'); + await removedStore + .record('$endpoint:${doc.id}') + .put(database, _storeAdapter.toMap(doc)); + await openStringKeyStore(endpoint).record(doc.id!).delete(database); } } - Future> openBox(String name) => - Hive.openBox(name); + Future storeGetOne(String endpoint, String id) async { + var store = openStringKeyStore(endpoint); + + var doc = await store.record(id).get(database); - Future boxGetOne(String endpoint, String id) async { - var box = await openBox(endpoint); - var doc = box.get(id); if (doc == null) { throw LocalRecordNotFoundException(); } - return doc; + return _storeAdapter.fromMap(doc); } - Future boxGetMany( + Future storeGetMany( String endpoint, Iterable ids, ) async { - var box = await openBox(endpoint); - List docs = - ids.map((id) => box.get(id)).whereType().toList(); + var store = openStringKeyStore(endpoint); + + var docs = await store.records(ids).get(database).then( + (maps) => maps + .where((doc) => doc != null) + .map((doc) => _storeAdapter.fromMap(doc!)), + ); + if (ids.isNotEmpty && docs.isEmpty) { throw LocalRecordNotFoundException(); } return JsonApiManyDocument(docs); } - Future boxPutOne(String endpoint, JsonApiDocument doc) async { - var box = await openBox(endpoint); - await box.put(doc.id, doc); + Future storePutOne(String endpoint, JsonApiDocument doc) async { + var store = openStringKeyStore(endpoint); + + await store.record(doc.id!).put(database, _storeAdapter.toMap(doc)); } - Future boxPutMany(String endpoint, JsonApiManyDocument docs) async { - var box = await openBox(endpoint); - var puts = []; - docs.forEach((doc) { - puts.add(box.put(doc.id, doc)); + Future storePutMany(String endpoint, JsonApiManyDocument docs) async { + var store = openStringKeyStore(endpoint); + + await database.transaction((transaction) async { + for (var doc in docs) { + await store.record(doc.id!).put(transaction, _storeAdapter.toMap(doc)); + } }); - await Future.wait(puts); } - Future boxAdd(String endpoint, JsonApiDocument doc) async { - var box = await openBox('added'); - int id = await box.add(doc); + Future storeAdd(String endpoint, JsonApiDocument doc) async { + var store = openIntKeyStore('added'); + int id = await store.add(database, _storeAdapter.toMap(doc)); return id; } Future> addedByEndpoint(String endpoint) async { - var box = await openBox('added'); - List docs = - box.values.where((doc) => doc.endpoint == endpoint).toList(); - return docs; + var store = openIntKeyStore('added'); + var docs = await store.find( + database, + finder: Finder(filter: Filter.matches('endpoint', endpoint)), + ); + return docs.map((e) => _storeAdapter.fromMap(e.value)).toList(); } Future findAdded(String id) async { var key = int.parse(id.replaceAll('added:', '')); - var box = await openBox('added'); - return box.get(key); + var store = openIntKeyStore('added'); + var map = await store.record(key).get(database); + return map == null ? null : _storeAdapter.fromMap(map); } Future findAllPersisted(String endpoint) async { - var box = await openBox(endpoint); - List docs = box.values.toList(); + var store = openStringKeyStore(endpoint); + final docs = (await store.find(database, finder: Finder())) + .map((e) => _storeAdapter.fromMap(e.value)) + .toList(); docs.addAll(await addedByEndpoint(endpoint)); return JsonApiManyDocument(docs); } + + StoreRef> openStringKeyStore(String name) { + return stringMapStoreFactory.store(name); + } + + StoreRef> openIntKeyStore(String name) { + return intMapStoreFactory.store(name); + } } diff --git a/lib/src/db_adapters/json_api.dart b/lib/src/db_adapters/json_api.dart new file mode 100644 index 0000000..7f4b047 --- /dev/null +++ b/lib/src/db_adapters/json_api.dart @@ -0,0 +1,17 @@ +import 'dart:convert'; + +import 'package:rest_data/rest_data.dart'; + +class JsonApiStoreAdapter { + final JsonApiSerializer serializer = JsonApiSerializer(); + + JsonApiDocument fromMap(Map map) { + String raw = json.encode(map); + return serializer.deserialize(raw); + } + + Map toMap(JsonApiDocument document) { + String raw = serializer.serialize(document, withIncluded: true); + return json.decode(raw); + } +} diff --git a/lib/src/hive_adapters/json_api.dart b/lib/src/hive_adapters/json_api.dart deleted file mode 100644 index 7d27b06..0000000 --- a/lib/src/hive_adapters/json_api.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; - -import 'package:hive/hive.dart'; -import 'package:rest_data/rest_data.dart'; - -class JsonApiHiveAdapter extends TypeAdapter { - @override - int get typeId => 81; - - final JsonApiSerializer serializer = JsonApiSerializer(); - final ZLibCodec zip = ZLibCodec(); - - @override - JsonApiDocument read(BinaryReader reader) { - var compressedBytes = reader.readByteList(); - String json = utf8.decode(zip.decode(compressedBytes)); - return serializer.deserialize(json); - } - - @override - void write(BinaryWriter writer, JsonApiDocument document) { - String json = serializer.serialize(document, withIncluded: true); - var compressedBytes = zip.encode(utf8.encode(json)); - writer.writeByteList(compressedBytes); - } -} diff --git a/pubspec.yaml b/pubspec.yaml index 41bbbc8..b429352 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -8,7 +8,8 @@ environment: sdk: ">=2.12.0 <3.0.0" dependencies: - hive_flutter: ^1.1.0 + sembast: ^3.1.1 + path_provider: ^2.0.5 rest_data: ^1.1.0 flutter: sdk: flutter diff --git a/test/flutter_rest_data_test.dart b/test/flutter_rest_data_test.dart index 472935d..ef299e0 100644 --- a/test/flutter_rest_data_test.dart +++ b/test/flutter_rest_data_test.dart @@ -1,22 +1,23 @@ import 'package:flutter_rest_data/flutter_rest_data.dart'; import 'package:flutter_test/flutter_test.dart'; - -import 'helpers.dart'; +import 'package:collection/collection.dart'; +import 'package:sembast/sembast.dart' as sembast; void main() { group('PersistentJsonApiAdapter', () { late PersistentJsonApiAdapter adapter; - setUpAll(() { - initHive(); - adapter = - PersistentJsonApiAdapter('host.example.com', '/path/to/rest/api'); - adapter.registerAdapters(); - }); + before() async { + adapter = PersistentJsonApiAdapter( + 'host.example.com', + '/path/to/rest/api', + ); + await adapter.initTest(); + } - tearDownAll(() async { - await adapter.dropBoxes(); - }); + after() async { + await adapter.dropStores(); + } group('when offline', () { JsonApiDocument doc1 = createJsonApiDocument('1'); @@ -24,20 +25,30 @@ void main() { JsonApiDocument doc3 = createJsonApiDocument('3'); JsonApiManyDocument docs = JsonApiManyDocument([doc1, doc2, doc3]); + setUp(before); + tearDown(after); + setUp(() async { adapter.setOffline(); - await adapter.boxPutMany('docs', docs); + await adapter.storePutMany('docs', docs); }); - test('find() returns requested JsonApiDocument from Hive', () async { + test('find() returns requested JsonApiDocument from database', () async { var doc = await adapter.find('docs', '1'); expect(doc is JsonApiDocument, isTrue); expect(doc.id, '1'); - expect(doc.attributes == doc1.attributes, isTrue); - expect(doc.relationships == doc1.relationships, isTrue); + expect( + DeepCollectionEquality().equals(doc.attributes, doc1.attributes), + isTrue, + ); + expect( + DeepCollectionEquality() + .equals(doc.relationships, doc1.relationships), + isTrue, + ); }); - test('findMany() returns requested JsonApiDocument objects from Hive', + test('findMany() returns requested JsonApiDocument objects from database', () async { var requestedIds = ['1', '2']; var returnedDocs = await adapter.findMany('docs', requestedIds); @@ -48,7 +59,8 @@ void main() { expect(requestedIds.toSet().containsAll(returnedIds), isTrue); }); - test('findAll() returns all JsonApiDocument objects from Hive', () async { + test('findAll() returns all JsonApiDocument objects from database', + () async { var allIds = ['1', '2', '3']; var returnedDocs = await adapter.findAll('docs'); expect(returnedDocs is JsonApiManyDocument, isTrue); @@ -57,6 +69,17 @@ void main() { expect(returnedIds.toSet().containsAll(allIds), isTrue); expect(allIds.toSet().containsAll(returnedIds), isTrue); }); + + test('performDelete() moves document to "removed" store', () async { + await adapter.performDelete('docs', doc1); + var remaining = await adapter.findAll('docs'); + expect(remaining.length, docs.length - 1); + + var removed = await adapter + .openStringKeyStore('removed') + .find(adapter.database, finder: sembast.Finder()); + expect(removed.length, 1); + }); }); }); } diff --git a/test/helpers.dart b/test/helpers.dart deleted file mode 100644 index 0be2026..0000000 --- a/test/helpers.dart +++ /dev/null @@ -1,7 +0,0 @@ -import 'dart:io'; - -import 'package:hive/hive.dart'; - -void initHive() { - Hive.init(Directory.current.path); -}