Skip to content

Commit

Permalink
Implement initial Hive to Sembast migration
Browse files Browse the repository at this point in the history
  • Loading branch information
Artem Kalachyan committed Oct 14, 2021
1 parent 5c461d8 commit 5f7ea18
Show file tree
Hide file tree
Showing 7 changed files with 154 additions and 107 deletions.
2 changes: 1 addition & 1 deletion 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)

Expand Down
148 changes: 94 additions & 54 deletions 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, {
Expand All @@ -26,38 +35,48 @@ class PersistentJsonApiAdapter extends JsonApiAdapter {
isOnline = false;
}

Future<void> init() async {
await Hive.initFlutter();
registerAdapters();
Future<void> init({String databaseName = 'persistent_json_api.db'}) async {
_dbName = databaseName;
_dbFactory = databaseFactoryIo;
var dbPath = await _buildDbPath(_dbName);
await _openDatabase(dbPath);
}

void initTest() {
registerAdapters();
Future<void> initTest() async {
_dbName = 'flutter_rest_data.db';
_dbFactory = databaseFactoryMemoryFs;
await _openDatabase(_dbName);
}

void registerAdapters() {
Hive.registerAdapter(JsonApiHiveAdapter());
Future<void> _openDatabase(String dbPath) async {
database = await _dbFactory.openDatabase(dbPath);
}

Future<void> dispose() => Hive.close();
Future<String> _buildDbPath(String dbName) async {
var dir = await getApplicationDocumentsDirectory();
await dir.create(recursive: true);
return join(dir.path, dbName);
}

Future<void> dropBoxes() => Hive.deleteFromDisk();
Future<void> dispose() => database.close();

Future<void> 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<JsonApiDocument> 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!;
}
Expand All @@ -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);
Expand All @@ -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<String> ids = params['filter[id]']!.split(',');
docs = await boxGetMany(endpoint, ids);
docs = await storeGetMany(endpoint, ids);
} else {
docs = await findAllPersisted(endpoint);
if (filter != null) {
Expand All @@ -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);
}
Expand All @@ -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<Box<JsonApiDocument>> openBox(String name) =>
Hive.openBox<JsonApiDocument>(name);
Future<JsonApiDocument> storeGetOne(String endpoint, String id) async {
var store = openStringKeyStore(endpoint);

var doc = await store.record(id).get(database);

Future<JsonApiDocument> 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<JsonApiManyDocument> boxGetMany(
Future<JsonApiManyDocument> storeGetMany(
String endpoint,
Iterable<String> ids,
) async {
var box = await openBox(endpoint);
List<JsonApiDocument> docs =
ids.map((id) => box.get(id)).whereType<JsonApiDocument>().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<void> boxPutOne(String endpoint, JsonApiDocument doc) async {
var box = await openBox(endpoint);
await box.put(doc.id, doc);
Future<void> storePutOne(String endpoint, JsonApiDocument doc) async {
var store = openStringKeyStore(endpoint);

await store.record(doc.id!).put(database, _storeAdapter.toMap(doc));
}

Future<void> boxPutMany(String endpoint, JsonApiManyDocument docs) async {
var box = await openBox(endpoint);
var puts = <Future>[];
docs.forEach((doc) {
puts.add(box.put(doc.id, doc));
Future<void> 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<int> boxAdd(String endpoint, JsonApiDocument doc) async {
var box = await openBox('added');
int id = await box.add(doc);
Future<int> storeAdd(String endpoint, JsonApiDocument doc) async {
var store = openIntKeyStore('added');
int id = await store.add(database, _storeAdapter.toMap(doc));
return id;
}

Future<Iterable<JsonApiDocument>> addedByEndpoint(String endpoint) async {
var box = await openBox('added');
List<JsonApiDocument> 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<JsonApiDocument?> 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<JsonApiManyDocument> findAllPersisted(String endpoint) async {
var box = await openBox(endpoint);
List<JsonApiDocument> 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<String, Map<String, Object?>> openStringKeyStore(String name) {
return stringMapStoreFactory.store(name);
}

StoreRef<int, Map<String, Object?>> openIntKeyStore(String name) {
return intMapStoreFactory.store(name);
}
}
17 changes: 17 additions & 0 deletions 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<String, Object?> map) {
String raw = json.encode(map);
return serializer.deserialize(raw);
}

Map<String, Object?> toMap(JsonApiDocument document) {
String raw = serializer.serialize(document, withIncluded: true);
return json.decode(raw);
}
}
27 changes: 0 additions & 27 deletions lib/src/hive_adapters/json_api.dart

This file was deleted.

3 changes: 2 additions & 1 deletion pubspec.yaml
Expand Up @@ -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
Expand Down

0 comments on commit 5f7ea18

Please sign in to comment.