diff --git a/.github/dependabot.yml b/.github/dependabot.yml index c70028a..b273525 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,6 +5,10 @@ version: 2 enable-beta-ecosystems: true updates: + - package-ecosystem: "pub" + directory: "packages/idb_provider.dart" + schedule: + interval: "monthly" - package-ecosystem: "pub" directory: "packages/idb_shim_html_compat" schedule: diff --git a/packages/idb_provider/.github/dependabot.yml b/packages/idb_provider/.github/dependabot.yml new file mode 100644 index 0000000..cbb7686 --- /dev/null +++ b/packages/idb_provider/.github/dependabot.yml @@ -0,0 +1,15 @@ +# Dependabot configuration file. +# See https://docs.github.com/en/code-security/dependabot/dependabot-version-updates +version: 2 + +enable-beta-ecosystems: true +updates: + - package-ecosystem: "pub" + directory: "." + schedule: + interval: "monthly" + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" diff --git a/packages/idb_provider/.github/workflows/run_ci.yml b/packages/idb_provider/.github/workflows/run_ci.yml new file mode 100644 index 0000000..89b74b8 --- /dev/null +++ b/packages/idb_provider/.github/workflows/run_ci.yml @@ -0,0 +1,36 @@ +name: Run CI +on: + push: + workflow_dispatch: + schedule: + - cron: '0 0 * * 0' # every sunday at midnight + +jobs: + test: + name: Test on ${{ matrix.os }} / ${{ matrix.dart }} + runs-on: ${{ matrix.os }} + defaults: + run: + working-directory: . + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + dart: stable + - os: ubuntu-latest + dart: beta + - os: ubuntu-latest + dart: dev + - os: windows-latest + dart: stable + - os: macos-latest + dart: stable + steps: + - uses: actions/checkout@v4 + - uses: dart-lang/setup-dart@v1.4 + with: + sdk: ${{ matrix.dart }} + - run: dart --version + - run: dart pub get + - run: dart run tool/run_ci.dart diff --git a/packages/idb_provider/.gitignore b/packages/idb_provider/.gitignore new file mode 100644 index 0000000..3272f11 --- /dev/null +++ b/packages/idb_provider/.gitignore @@ -0,0 +1,24 @@ +# Don’t commit the following directories created by pub. +.dart_tool/ +build/ +packages/ +.packages +.buildlog +.pub/ +.idea/ + +# Or the files created by dart2js. +*.dart.js +*.dart.precompiled.js +*.js_ +*.js.deps +*.js.map + +# Include when developing application packages. +pubspec.lock + +# test output +out + +# Local files +.local/ \ No newline at end of file diff --git a/packages/idb_provider/CHANGELOG.md b/packages/idb_provider/CHANGELOG.md new file mode 100644 index 0000000..4629ced --- /dev/null +++ b/packages/idb_provider/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## 0.1.1 + +- Initial version diff --git a/packages/idb_provider/LICENSE b/packages/idb_provider/LICENSE new file mode 100644 index 0000000..414fa47 --- /dev/null +++ b/packages/idb_provider/LICENSE @@ -0,0 +1,28 @@ +Copyright (c) 2014, alextekartik +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of tekartik_idb_provider.dart nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/packages/idb_provider/README.md b/packages/idb_provider/README.md new file mode 100644 index 0000000..68b7891 --- /dev/null +++ b/packages/idb_provider/README.md @@ -0,0 +1,16 @@ +# tekartik_idb_provider + +**Not maintained** + +Provider pattern on top of indexeddb + +# Dependencies + +```yaml +dependencies: + tekartik_idb_provider: + git: + url: https://github.com/tekartik/idb_shim_more.dart + path: packages/idb_provider + ref: dart3a +``` \ No newline at end of file diff --git a/packages/idb_provider/analysis_options.yaml b/packages/idb_provider/analysis_options.yaml new file mode 100644 index 0000000..7b94e0c --- /dev/null +++ b/packages/idb_provider/analysis_options.yaml @@ -0,0 +1,2 @@ +# tekartik recommended lints (extension over google lints and pedantic) +include: package:tekartik_lints/strict.yaml \ No newline at end of file diff --git a/packages/idb_provider/lib/provider.dart b/packages/idb_provider/lib/provider.dart new file mode 100644 index 0000000..da90a7b --- /dev/null +++ b/packages/idb_provider/lib/provider.dart @@ -0,0 +1,267 @@ +library tekartik_provider; + +import 'package:idb_shim/idb_client.dart'; +import 'package:tekartik_common_utils/common_utils_import.dart'; +import 'package:tekartik_idb_provider/provider.dart'; + +export 'package:tekartik_idb_provider/src/provider/provider_meta.dart'; +export 'package:tekartik_idb_provider/src/provider/provider_row.dart'; +export 'package:tekartik_idb_provider/src/provider/provider_transaction.dart'; + +class DynamicProvider extends Provider { + final List _storeMetas = []; + + @override + void onUpdateDatabase(VersionChangeEvent e) { + for (var meta in _storeMetas) { + var store = db!.createStore(meta!); + for (var indexMeta in meta.indecies) { + store.createIndex(indexMeta!); + } + } + } + + // to call before ready + void addStore(ProviderStoreMeta? storeMeta) { + _storeMetas.add(storeMeta); + } + + // to call before ready + void addStores(ProviderStoresMeta storesMeta) { + for (final storeMeta in storesMeta.stores) { + addStore(storeMeta); + } + } + + DynamicProvider.noMeta(IdbFactory? idbFactory) { + _idbFactory = idbFactory; + } + + DynamicProvider(IdbFactory? idbFactory, [ProviderDbMeta? meta]) { + _idbFactory = idbFactory; + _databaseMeta = meta; + } +} + +abstract class Provider { + IdbFactory? _idbFactory; + ProviderDb? _db; + ProviderDbMeta? _databaseMeta; + + IdbFactory? get idbFactory => _idbFactory; + + ProviderDb? get db => _db; + + Provider(); + + Provider.fromIdb(Database idbDatabase) { + _setDatabase(idbDatabase); + _databaseMeta = db!.meta; + } + + //AppProvider(this.idbFactory); + void init(IdbFactory? idbFactory, String dbName, int dbVersion) { + _idbFactory = idbFactory; + _databaseMeta = ProviderDbMeta(dbName, dbVersion); + } + + // when everything ready + void _setDatabase(Database db) { + _db = ProviderDb(db); + } + + // must be set before being ready + // The provider take ownership of the database + set db(ProviderDb? db) { + if (db == null) { + _db = null; + _ready = null; + _readyCompleter = null; + } else { + if (_ready != null) { + throw 'ready should not have been called before setting the db'; + } else { + _readyCompleter = Completer.sync(); + _db = db; + _idbFactory = db.factory; + _databaseMeta = _db!.meta; + + _ready = _readyCompleter!.future; + _readyCompleter!.complete(this); + } + } + } + + Database? get database => db!.database; + + // must be set before being ready + // The provider take ownership of the database + set database(Database? db) { + if (db == null) { + _db = null; + _ready = null; + _readyCompleter = null; + } else { + if (_ready != null) { + throw 'ready should not have been called before setting the db'; + } else { + _readyCompleter = Completer.sync(); + _setDatabase(db); + _idbFactory = db.factory; + _databaseMeta = _db!.meta; + + _ready = _readyCompleter!.future; + _readyCompleter!.complete(this); + } + } + } + + // during onUpdateOnly + + void close() { + if (db != null) { + db!.close(); + _db = null; + _ready = null; + _readyCompleter = null; + } + } + + // delete content + Future clear() { + final storeNames = db!.database!.objectStoreNames.toList(growable: false); + var globalTrans = ProviderTransactionList(this, storeNames, true); + final futures = >[]; + for (final storeName in storeNames) { + var trans = globalTrans.store(storeName); + futures.add(trans.clear()); + } + return Future.wait(futures).then((_) { + return globalTrans.completed; + }); + } + + // check if database is still opened + bool get isClosed => (_db == null) && (_ready == null); + + // to implement + void onUpdateDatabase(VersionChangeEvent e); + + void _onUpdateDatabase(VersionChangeEvent e) { + _setDatabase(e.database); + onUpdateDatabase(e); + } + + Future? _storesMeta; + + Future? get storesMeta { + _storesMeta ??= Future.sync(() { + final metas = []; + + var storeNames = db!.storeNames.toList(); + final txn = transactionList(storeNames); + for (final storeName in storeNames) { + metas.add(txn.store(storeName).store!.meta); + } + return txn.completed.then((_) { + final meta = ProviderStoresMeta(metas); + return meta; + }); + }); + + return _storesMeta; + } + +// Database db = e.database; +// int version = e.oldVersion; +// +// if (e.oldVersion == 1) { +// db.deleteObjectStore(FILES_STORE); +// +// // dev bug +// if (db.objectStoreNames.contains(METAS_STORE)) { +// db.deleteObjectStore(METAS_STORE); +// } +// } +// +// var objectStore = db.createObjectStore(FILES_STORE, autoIncrement: true); +// Index index = objectStore.createIndex(NAME_INDEX, NAME_FIELD, unique: true); +// +// +// var metaStrore = db.createObjectStore(METAS_STORE, autoIncrement: true); +// Index fileIndex = metaStrore.createIndex(FILE_ID_INDEX, FILE_ID_FIELD, unique: true); +// } + +// Stream Future> metaCursorToList(Stream stream) { +// List list = new List(); +// return stream.listen((CursorWithValue cwv) { +// MetaRow row = new MetaRow.fromCursor(cwv); +// +// list.add(row); +// cwv.next(); +// }).asFuture(list); +// } + + Future delete() { + close(); + return idbFactory!.deleteDatabase(_databaseMeta!.name); + } + + Future? _ready; + Completer? _readyCompleter; + + bool get isReady => _readyCompleter != null && _readyCompleter!.isCompleted; + + Future? get ready { + if (_ready == null) { + _readyCompleter = Completer.sync(); + + _ready = _readyCompleter!.future; + + runZonedGuarded(() { + return _idbFactory! + .open(_databaseMeta!.name, + version: _databaseMeta!.version, + onUpgradeNeeded: _onUpdateDatabase) + .then((Database db) { + _setDatabase(db); + _readyCompleter!.complete(this); + }); + }, (e, StackTrace st) { + print('open failed'); + print(e); + print(st); + _readyCompleter!.completeError(e, st); + }); + } + return _ready; + } + +// default read-only + ProviderStoreTransaction storeTransaction(String storeName, + [bool readWrite = false]) { + return ProviderStoreTransaction(this, storeName, readWrite); + } + + // default read-only + ProviderIndexTransaction indexTransaction(String storeName, String indexName, + [bool readWrite = false]) { + return ProviderIndexTransaction(this, storeName, indexName, readWrite); + } + + ProviderTransactionList transactionList(List storeNames, + [bool readWrite = false]) { + return ProviderTransactionList(this, storeNames, readWrite); + } + + Map toMap() { + final map = {}; + if (_db != null) { + map['db'] = _db!.database!.name; + } + return map; + } + + @override + String toString() => toMap().toString(); +} diff --git a/packages/idb_provider/lib/record_provider.dart b/packages/idb_provider/lib/record_provider.dart new file mode 100644 index 0000000..f3cd7fc --- /dev/null +++ b/packages/idb_provider/lib/record_provider.dart @@ -0,0 +1,637 @@ +library tekartik_idb_provider.record_provider; + +import 'dart:async'; + +import 'package:collection/collection.dart'; +import 'package:tekartik_idb_provider/provider.dart'; + +abstract class DbField { + static const String syncVersion = 'syncVersion'; + static const String version = 'version'; + + // local version (incremented) + static const String dirty = 'dirty'; + static const String deleted = 'deleted'; + static const String syncId = 'syncId'; + static const String kind = 'kind'; +} + +abstract class DbRecordBase { + /// Nullable key + K? get id; + + set id(K? id); + + void fillDbEntry(Map entry); + + void fillFromDbEntry(Map entry); + + Map toDbEntry() { + final entry = {}; + fillDbEntry(entry); + + return entry; + } + + void set(Map map, String key, Object? value) { + if (value != null) { + map[key] = value; + } else { + map.remove(key); + } + } + + @override + String toString() { + final map = {}; + fillDbEntry(map); + if (id != null) { + map['_id'] = id.toString(); + } + return map.toString(); + } + + @override + int get hashCode => + const MapEquality().hash(toDbEntry()) + id.hashCode; + + @override + bool operator ==(Object other) { + if (other is DbRecordBase) { + return (const MapEquality() + .equals(toDbEntry(), other.toDbEntry())) && + id == other.id; + } + return false; + } +} + +abstract class DbRecord extends DbRecordBase { + /* + get id; + + set id(var id); + + @override + bool operator ==(o) { + return (super == (o) && id == o.id); + } + */ +} + +abstract class DbSyncedRecordBase extends DbRecordBase { + //String get kind; + + int? version; + + String? _syncId; + String? _syncVersion; + + String? get syncId => _syncId; + + // will match the tag when synced + String? get syncVersion => _syncVersion; + + void setSyncInfo(String? syncId, String? syncVersion) { + _syncId = syncId; + _syncVersion = syncVersion; + } + + bool? _deleted; + + bool get deleted => _deleted == true; + + // true or false + set deleted(bool deleted) => _deleted = deleted; + + bool? _dirty; + + bool get dirty => _dirty == true; + + // true or false + set dirty(bool dirty) => _dirty = dirty; + + @override + void fillFromDbEntry(Map entry) { + // type = entry[FIELD_TYPE]; already done + version = entry[DbField.version] as int?; + _syncId = entry[DbField.syncId] as String?; + _syncVersion = entry[DbField.syncVersion] as String?; + _deleted = entry[DbField.deleted] as bool?; + _dirty = entry[DbField.dirty] == 1; + } + + @override + void fillDbEntry(Map entry) { + set(entry, DbField.version, version); + set(entry, DbField.syncId, syncId); + set(entry, DbField.syncVersion, syncVersion); + set(entry, DbField.deleted, deleted ? true : null); + set(entry, DbField.dirty, dirty ? 1 : null); + //set(entry, DbField.kind, kind); + } +} + +abstract class DbSyncedRecord extends DbSyncedRecordBase {} + +class DbRecordProviderPutEvent extends DbRecordProviderEvent { + DbRecordBase? record; +} + +class DbRecordProviderDeleteEvent extends DbRecordProviderEvent { + dynamic key; +} + +// not tested +class DbRecordProviderClearEvent extends DbRecordProviderEvent {} + +class DbRecordProviderEvent { + bool? _syncing; + + bool? get syncing => _syncing; + + set syncing(bool? syncing) => _syncing = syncing == true; +} + +// only for writable transaction +abstract class DbRecordProviderTransaction + extends ProviderStoreTransaction { + DbRecordBaseProvider _provider; + + factory DbRecordProviderTransaction( + DbRecordBaseProvider provider, String storeName, + [bool? readWrite = false]) { + if (readWrite == true) { + return DbRecordProviderWriteTransaction(provider, storeName); + } else { + return DbRecordProviderReadTransaction(provider, storeName); + } + } + + /* + @deprecated // discouraged + Future get(K key) async => (await super.get(key); + */ + + DbRecordProviderTransaction._fromList( + this._provider, ProviderTransactionList list, String storeName) + : super.fromList(list, storeName); + + DbRecordProviderTransaction._(DbRecordBaseProvider provider, String storeName, + [bool readWrite = false]) + : _provider = provider, + super(provider.provider, storeName, readWrite); +} + +class DbRecordProviderReadTransaction + extends DbRecordProviderTransaction { + DbRecordProviderReadTransaction( + DbRecordBaseProvider provider, String storeName) + : super._(provider, storeName, false); + + DbRecordProviderReadTransaction.fromList( + super.provider, super.list, super.storeName) + : super._fromList(); +} + +class DbRecordProviderWriteTransaction + extends DbRecordProviderTransaction { + bool get _hasListener => _provider._hasListener; + + List changes = []; + + DbRecordProviderWriteTransaction( + DbRecordBaseProvider provider, String storeName) + : super._(provider, storeName, true); + + DbRecordProviderWriteTransaction.fromList( + super.provider, super.list, super.storeName) + : super._fromList(); + + Future putRecord(T record, {bool? syncing}) { + return super.put(record.toDbEntry(), record.id as K?).then((var key) { + record.id = key; + if (_hasListener) { + changes.add(DbRecordProviderPutEvent() + ..record = record + ..syncing = syncing); + } + return record; + }); + } + + // ignore: avoid_shadowing_type_parameters + Future _throwError() async => throw UnsupportedError( + 'use putRecord, deleteRecord and clearRecords API'); + + @Deprecated('not supported, use record API') + @override + Future add(Map value, [K? key]) => _throwError(); + + @Deprecated('not supported, use record API') + @override + Future put(Map value, [K? key]) => _throwError(); + + @Deprecated('not supported, use record API') + @override + Future delete(K key) => _throwError(); + + @Deprecated('not supported, use record API') + @override + Future clear() => _throwError(); + + Future deleteRecord(K key, {bool? syncing}) { + return super.delete(key).then((_) { + if (_hasListener) { + changes.add(DbRecordProviderDeleteEvent() + ..key = key + ..syncing = syncing); + } + }); + } + + Future clearRecords({bool? syncing}) { + return super.clear().then((_) { + if (_hasListener) { + changes.add(DbRecordProviderClearEvent()..syncing = syncing); + } + }); + } + + @override + Future get completed { + // delayed notification + return super.completed.then((_) { + if (_hasListener && changes.isNotEmpty) { + for (final ctlr in _provider._onChangeCtlrs) { + ctlr.add(changes); + } + } + }); + } +} + +/// +/// A record provider is a provider of a given object type +/// in one store +/// +abstract class DbRecordBaseProvider { + late Provider provider; + + String get store; + + DbRecordProviderReadTransaction get readTransaction => + DbRecordProviderReadTransaction(this, store); + + DbRecordProviderWriteTransaction get writeTransaction => + DbRecordProviderWriteTransaction(this, store); + + DbRecordProviderReadTransaction get storeReadTransaction => + readTransaction; + + DbRecordProviderWriteTransaction get storeWriteTransaction => + writeTransaction; + + DbRecordProviderTransaction storeTransaction(bool? readWrite) => + DbRecordProviderTransaction(this, store, readWrite); + + Future get(K id) async { + var txn = provider.storeTransaction(store); + final record = await _txnGet(txn, id); + await txn.completed; + return record; + } + + T? fromEntry(Map? entry, K? id); + + //@deprecated + Future txnGet(ProviderStoreTransaction txn, K id) => _txnGet(txn, id); + + Future _txnGet(ProviderStoreTransaction txn, K id) async { + var entry = await txn.get(id); + return fromEntry(entry as Map?, id); + } + + Future indexGet(ProviderIndexTransaction txn, dynamic id) { + return txn.get(id).then((Object? entry) { + return txn.getKey(id).then((Object? primaryId) { + return fromEntry(entry as Map?, primaryId as K?); + }); + }); + } + + // transaction from a transaction list + DbRecordProviderReadTransaction txnListReadTransaction( + DbRecordProviderTransactionList txnList) => + DbRecordProviderReadTransaction.fromList(this, txnList, store); + + DbRecordProviderWriteTransaction txnListWriteTransaction( + DbRecordProviderWriteTransactionList txnList) => + DbRecordProviderWriteTransaction.fromList(this, txnList, store); + + // Listener + final List _onChangeCtlrs = []; + + Stream> get onChange { + var ctlr = StreamController>(sync: true); + _onChangeCtlrs.add(ctlr); + return ctlr.stream; + } + + void close() { + for (final ctlr in _onChangeCtlrs) { + ctlr.close(); + } + } + + bool get _hasListener => _onChangeCtlrs.isNotEmpty; +} + +abstract class DbRecordProvider + extends DbRecordBaseProvider { + Future put(T record) async { + var txn = storeTransaction(true); + record = + await txnPut(txn as DbRecordProviderWriteTransaction, record); + await txn.completed; + return record; + } + + Future txnPut(DbRecordProviderWriteTransaction txn, T record) async => + (await txn.putRecord(record)) as T; + + Future delete(K key) async { + var txn = storeTransaction(true); + await txnDelete(txn as DbRecordProviderWriteTransaction, key); + await txn.completed; + } + + Future txnDelete(DbRecordProviderWriteTransaction txn, K key) => + txn.deleteRecord(key); + + Future clear() async { + var txn = storeTransaction(true); + await txnClear(txn as DbRecordProviderWriteTransaction); + return txn.completed; + } + + // Future txnClear(DbRecordProviderWriteTransaction txn) async { await txn.clearRecords(); } + Future txnClear(DbRecordProviderWriteTransaction txn) => txn.clearRecords(); +} + +abstract class DbSyncedRecordProvider + extends DbRecordBaseProvider { + static const String dirtyIndex = DbField.dirty; + + // must be int for indexing + static const String syncIdIndex = DbField.syncId; + + ProviderIndexTransaction indexTransaction(String indexName, + [bool? readWrite]) => + ProviderIndexTransaction.fromStoreTransaction( + storeTransaction(readWrite), indexName); + + Future delete(K id, {bool? syncing}) async { + var txn = storeTransaction(true); + await txnDelete(txn as DbRecordProviderWriteTransaction, id, + syncing: syncing); + await txn.completed; + } + + Future txnRawDelete(DbRecordProviderWriteTransaction txn, K id) => + txn.deleteRecord(id); + + Future txnDelete(DbRecordProviderWriteTransaction txn, K id, + {bool? syncing}) { + return txnGet(txn, id).then((T? existing) { + if (existing != null) { + // Not synced yet or from sync adapter + if (existing.syncId == null || (syncing == true)) { + return txnRawDelete(txn, id); + } else if (!existing.deleted) { + existing.deleted = true; + existing.dirty = true; + existing.version = existing.version! + 1; + return txnRawPut(txn, existing); + } + } + return null; + }); + } + + Future getBySyncId(String syncId) async { + final txn = indexTransaction(syncIdIndex); + final id = await txn.getKey(syncId) as K?; + T? record; + if (id != null) { + record = await txnGet(txn.store!, id); + } + await txn.completed; + return record; + } + + Future txnRawPut(DbRecordProviderWriteTransaction txn, T record) async { + return (await txn.putRecord(record)) as T?; + } + + Future txnPut(DbRecordProviderWriteTransaction txn, T record, + {bool? syncing}) { + syncing = syncing == true; + // remove deleted if set + record.deleted = false; + // never update sync info + // dirty for not sync only + if (syncing) { + record.dirty = false; + } else { + // try to retrieve existing sync info + // list.setSyncInfo(null, null); + record.setSyncInfo(null, null); + record.dirty = true; + } + Future doInsert() { + record.version = 1; + return txnRawPut(txn, record); + } + + if (record.id != null) { + return txnGet(txn, record.id as K).then((T? existingRecord) { + if (existingRecord != null) { + // if not syncing keep existing syncId and syncVersion + if (syncing != true) { + record.setSyncInfo( + existingRecord.syncId, existingRecord.syncVersion); + } + record.version = existingRecord.version! + 1; + return txnRawPut(txn, record); + } else { + return doInsert(); + } + }); + } else { + return doInsert(); + } + } + + Future clear({bool? syncing}) async { + if (syncing != true) { + throw UnimplementedError('force the syncing field to true'); + } + var txn = storeTransaction(true); + await txnClear(txn as DbRecordProviderWriteTransaction, syncing: syncing); + return txn.completed; + } + + Future txnClear(DbRecordProviderWriteTransaction txn, {bool? syncing}) { + if (syncing != true) { + throw UnimplementedError('force the syncing field to true'); + } + return txn.clearRecords(); + } + + /// + /// TODO: Put won't change data (which one) if local version has changed + /// + Future put(T record, {bool? syncing}) async { + var txn = storeTransaction(true); + + record = (await txnPut(txn as DbRecordProviderWriteTransaction, record, + syncing: syncing)) as T; + await txn.completed; + return record; + } + + /// + /// during sync, update the sync version + /// if the local version has changed since, keep the dirty flag + /// other data is not touched + /// the dirty flag is only cleared if the local version has not changed + /// + Future updateSyncInfo(T record, String syncId, String syncVersion) async { + var txn = storeTransaction(true); + var existingRecord = await txnGet(txn, record.id as K); + if (existingRecord != null) { + // Check version before changing the dirty flag + if (record.version == existingRecord.version) { + existingRecord.dirty = false; + } + record = existingRecord; + } + record.setSyncInfo(syncId, syncVersion); + await txnRawPut(txn as DbRecordProviderWriteTransaction, record); + await txn.completed; + } + + Future getFirstDirty() async { + var txn = indexTransaction(dirtyIndex); + var id = await txn.getKey(1) as K?; // 1 is dirty + T? record; + if (id != null) { + record = await txnGet(txn.store!, id); + } + await txn.completed; + return record; + } + +/* + // Delete all records with synchronisation information + Future txnDeleteSyncedRecord(DbRecordProviderWriteTransaction txn) { + ProviderIndexTransaction index = + ProviderIndexTransaction.fromStoreTransaction(txn, syncIdIndex); + index.openCursor().listen((idb.CursorWithValue cwv) { + //print('deleting: ${cwv.primaryKey}'); + cwv.delete(); + }); + } + */ +} + +// only for writable transaction +abstract class DbRecordProviderTransactionList extends ProviderTransactionList { + DbRecordProvidersMixin _provider; + + factory DbRecordProviderTransactionList( + DbRecordProvidersMixin provider, List storeNames, + [bool readWrite = false]) { + if (readWrite) { + return DbRecordProviderWriteTransactionList(provider, storeNames); + } else { + return DbRecordProviderReadTransactionList(provider, storeNames); + } + } + +// DbRecordBaseProvider getRecordProvider(String storeName) => _provider.getRecordProvider(storeName); + + DbRecordProviderTransactionList._( + DbRecordProvidersMixin provider, List storeNames, + [bool readWrite = false]) + : _provider = provider, + super(provider as Provider, storeNames, readWrite); +} + +class DbRecordProviderReadTransactionList + extends DbRecordProviderTransactionList { + DbRecordProviderReadTransactionList( + DbRecordProvidersMixin provider, List storeNames) + : super._(provider, storeNames, false); + + @override + DbRecordProviderReadTransaction store(String storeName) { + return DbRecordProviderReadTransaction.fromList( + _provider.getRecordProvider(storeName)!, this, storeName); + } +} + +class DbRecordProviderWriteTransactionList + extends DbRecordProviderTransactionList { + DbRecordProviderWriteTransactionList( + DbRecordProvidersMixin provider, List storeNames) + : super._(provider, storeNames, true); + + @override + DbRecordProviderTransaction store(String storeName) { + return DbRecordProviderWriteTransaction.fromList( + _provider.getRecordProvider(storeName)!, this, storeName); + } +} + +mixin DbRecordProvidersMapMixin { + late Map providerMap; + + void initAll(Provider provider) { + for (final recordProvider in providerMap.values) { + recordProvider!.provider = provider; + } + } + + DbRecordBaseProvider? getRecordProvider(String storeName) => + providerMap[storeName]; + + void closeAll() { + for (final recordProvider in recordProviders) { + recordProvider.close(); + } + } + + Iterable get recordProviders => + providerMap.values.cast(); +} + +mixin DbRecordProvidersMixin { + DbRecordProviderReadTransactionList dbRecordProviderReadTransactionList( + List storeNames) => + DbRecordProviderReadTransactionList(this, storeNames); + + DbRecordProviderWriteTransactionList writeTransactionList( + List storeNames) => + DbRecordProviderWriteTransactionList(this, storeNames); + + DbRecordProviderWriteTransactionList dbRecordProviderWriteTransactionList( + List storeNames) => + writeTransactionList(storeNames); + + // to implement + DbRecordBaseProvider? getRecordProvider(String storeName); + + Iterable get recordProviders; +} diff --git a/packages/idb_provider/lib/src/provider/provider_meta.dart b/packages/idb_provider/lib/src/provider/provider_meta.dart new file mode 100644 index 0000000..2d45396 --- /dev/null +++ b/packages/idb_provider/lib/src/provider/provider_meta.dart @@ -0,0 +1,269 @@ +import 'package:collection/collection.dart'; +import 'package:idb_shim/idb.dart'; +import 'package:tekartik_common_utils/hash_code_utils.dart'; + +class ProviderDbMeta { + final String name; + final int version; + + ProviderDbMeta(this.name, [int? version]) : version = version ?? 1; + + ProviderDbMeta overrideMeta({String? name, int? version}) { + name ??= this.name; + version ??= this.version; + return ProviderDbMeta(name, version); + } + + @override + int get hashCode => safeHashCode(name) * 17 + safeHashCode(version); + + @override + bool operator ==(other) { + if (other is ProviderDbMeta) { + if (name != other.name) { + return false; + } + if (version != other.version) { + return false; + } + return true; + } + return false; + } + + @override + String toString() => '$name($version)'; +} + +class ProviderDb { + Database? _database; + + Database? get database => _database; + + //Provider _provider; + ProviderDb(this._database); + + ProviderStore createStore(ProviderStoreMeta meta) { + final objectStore = database!.createObjectStore(meta.name, + keyPath: meta.keyPath, autoIncrement: meta.autoIncrement); + return ProviderStore(objectStore); + } + + /// during onUpdateOnly + /// return false if it does not exists + bool deleteStore(String name) { +// dev bug + if (storeNames.contains(name)) { + database!.deleteObjectStore(name); + return true; + } + return false; + } + + Iterable get storeNames { + return database!.objectStoreNames; + } + + ProviderDbMeta? _meta; + + ProviderDbMeta? get meta { + _meta ??= ProviderDbMeta(database!.name, database!.version); + + return _meta; + } + + int get version => meta!.version; + + void close() { + database!.close(); + _database = null; + } + + IdbFactory get factory => _database!.factory; + + @override + String toString() => '$_database'; +} + +class StoreRow {} + +class ProviderStoresMeta { + final Iterable stores; + + ProviderStoresMeta(this.stores); + + @override + int get hashCode => + stores.length * 17 + + (stores.isEmpty ? 0 : safeHashCode(stores.first.hashCode)); + + @override + bool operator ==(other) { + if (other is ProviderStoresMeta) { + return const UnorderedIterableEquality() + .equals(stores, other.stores); + } + return false; + } + + @override + String toString() => '$stores'; +} + +class ProviderStoreMeta { + final String name; + final String? keyPath; + final bool autoIncrement; + + ProviderStoreMeta(this.name, + {this.keyPath, bool? autoIncrement, List? indecies}) + // + : autoIncrement = (autoIncrement == true) + // + , + indecies = (indecies == null) ? [] : indecies; + final List indecies; + + ProviderStoreMeta overrideIndecies(List indecies) { + return ProviderStoreMeta(name, + keyPath: keyPath, autoIncrement: autoIncrement, indecies: indecies); + } + + @override + int get hashCode { + return safeHashCode(name); + } + + @override + bool operator ==(other) { + if (other is ProviderStoreMeta) { + if (other.name != name) { + return false; + } + if (other.keyPath != keyPath) { + return false; + } + if (other.autoIncrement != autoIncrement) { + return false; + } + // order not important for index + if (!(const UnorderedIterableEquality() + .equals(indecies, other.indecies))) { + return false; + } + return true; + } + return false; + } + + @override + String toString() => + '$name($keyPath${autoIncrement ? ' auto' : ''}) $indecies'; +} + +class ProviderStore { + ProviderStoreMeta? _meta; + + ProviderStoreMeta? get meta { + if (_meta == null) { + final indecies = []; + for (final indexName in indexNames) { + final index = this.index(indexName); + indecies.add(index.meta); + } + _meta = ProviderStoreMeta(objectStore.name, + keyPath: objectStore.keyPath as String?, + autoIncrement: objectStore.autoIncrement, + indecies: indecies); + } + return _meta; + } + + final ObjectStore objectStore; + + ProviderStore(this.objectStore); + + ProviderIndex createIndex(ProviderIndexMeta meta) { + final index = objectStore.createIndex(meta.name, meta.keyPath, + unique: meta.unique, multiEntry: meta.multiEntry); + return ProviderIndex(index); + } + + Future count() => objectStore.count(); + + ProviderIndex index(String name) { + final index = objectStore.index(name); + return ProviderIndex(index); + } + + Future get(Object key) => objectStore.getObject(key); + + Future put(Object value, [Object? key]) => objectStore.put(value, key); + + Future add(Object value, [Object? key]) => objectStore.add(value, key); + + Future delete(Object key) => objectStore.delete(key); + + Future clear() => objectStore.clear(); + + List get indexNames => objectStore.indexNames; +} + +class ProviderIndexMeta { + final String name; + final Object keyPath; + final bool unique; + final bool multiEntry; + + ProviderIndexMeta(this.name, this.keyPath, {bool? unique, bool? multiEntry}) + // + : unique = (unique == true), + multiEntry = (multiEntry == true); + + @override + int get hashCode { + // likely content will differ.. + return safeHashCode(name); + } + + @override + bool operator ==(other) { + if (other is ProviderIndexMeta) { + if (other.name != name) { + return false; + } + if (other.keyPath != keyPath) { + return false; + } + if (other.unique != unique) { + return false; + } + if (other.multiEntry != multiEntry) { + return false; + } + return true; + } + return false; + } + + @override + String toString() => + '$name $keyPath${unique ? 'unique' : ''}${multiEntry ? 'multi' : ''}'; +} + +class ProviderIndex { + ProviderIndexMeta? _meta; + + ProviderIndexMeta? get meta { + _meta ??= ProviderIndexMeta(index.name, index.keyPath, + unique: index.unique, multiEntry: index.multiEntry); + + return _meta; + } + + final Index index; + + ProviderIndex(this.index); + + Future count() => index.count(); +} diff --git a/packages/idb_provider/lib/src/provider/provider_row.dart b/packages/idb_provider/lib/src/provider/provider_row.dart new file mode 100644 index 0000000..feab789 --- /dev/null +++ b/packages/idb_provider/lib/src/provider/provider_row.dart @@ -0,0 +1,75 @@ +import 'package:collection/collection.dart'; +import 'package:idb_shim/idb.dart'; +import 'package:tekartik_common_utils/hash_code_utils.dart'; + +abstract class _BaseRow { + K? key; + V? value; + + _BaseRow(); + _BaseRow.from(this.key, this.value); + + @override + int get hashCode => safeHashCode(key); + + @override + String toString() => '$key: $value'; + + @override + bool operator ==(Object other) { + if (other is _BaseRow && other.runtimeType == runtimeType) { + return (key == other.key) && (value == other.value); + } + return false; + } +} + +abstract class _BaseMapRow extends _BaseRow { + _BaseMapRow.from(K super.key, Map super.map) : super.from(); + + @override + int get hashCode => value?.hashCode ?? 0; + @override + bool operator ==(other) { + if (other is _BaseMapRow) { + return const MapEquality().equals(value, other.value); + } + return false; + } + + dynamic operator [](String key) => value![key]; +} + +class StringMapRow extends _BaseMapRow { + StringMapRow.from(super.key, super.value) : super.from(); +} + +class IntMapRow extends _BaseMapRow { + IntMapRow.from(super.key, super.value) : super.from(); +} + +// ignore: library_private_types_in_public_api +abstract class ProviderRowFactory, K, V> { + T newRow(K key, V value); + + T cursorWithValueRow(CursorWithValue cwv) => + newRow(cwv.primaryKey as K, cwv.value as V); +} + +class IntMapProviderRowFactory extends ProviderRowFactory { + @override + IntMapRow newRow(int key, Map value) { + return IntMapRow.from(key, value); + } +} + +final IntMapProviderRowFactory intMapProviderRawFactory = + IntMapProviderRowFactory(); + +class StringMapProviderRowFactory + extends ProviderRowFactory { + @override + StringMapRow newRow(String key, Map value) { + return StringMapRow.from(key, value); + } +} diff --git a/packages/idb_provider/lib/src/provider/provider_transaction.dart b/packages/idb_provider/lib/src/provider/provider_transaction.dart new file mode 100644 index 0000000..3f448ba --- /dev/null +++ b/packages/idb_provider/lib/src/provider/provider_transaction.dart @@ -0,0 +1,292 @@ +import 'package:idb_shim/idb.dart'; +import 'package:tekartik_common_utils/common_utils_import.dart'; +import 'package:tekartik_idb_provider/provider.dart'; + +class ProviderIndexTransaction extends Object + with ProviderSourceTransactionMixin { + Future get completed => _store!.completed; + + ProviderStoreTransaction? _store; + ProviderStoreTransaction? get store => _store; + + ProviderIndexTransaction.fromStoreTransaction(this._store, String indexName) { + _index = _store!.store!.index(indexName); + } + //ProviderIndex get index => _index; + //ProviderStore get store => this; + //ProviderStore get store => super._store; + + @override + Future get(K key) async { + return await _index.index.get(key!) as V?; + } + + Future getKey(K key) { + return _index.index.getKey(key!); + } + + late ProviderIndex _index; + ProviderIndexTransaction( + Provider provider, String storeName, String indexName, + [bool readWrite = false]) // + { + _store = ProviderStoreTransaction(provider, storeName, readWrite); + _index = _store!.store!.index(indexName); + } + + @override + Future count() => _index.count(); + + @override + Stream openRawCursor({K? key, String? direction}) { + return _index.index.openCursor( + // + key: key, + direction: direction); + } + + // @override + Stream openRawKeyCursor({K? key, String? direction}) { + return _index.index.openKeyCursor( + // + key: key, + direction: direction); + } + + Stream openKeyCursor( + {K? key, bool reverse = false, int? limit, int? offset}) { + final direction = reverse ? idbDirectionPrev : null; + final stream = openRawKeyCursor(key: key, direction: direction); + return _limitOffsetStream(stream, limit: limit, offset: offset); + } + + //@override + Stream openCursor( + {K? key, bool reverse = false, int? limit, int? offset}) { + final direction = reverse ? idbDirectionPrev : null; + final stream = openRawCursor(key: key, direction: direction); + return _limitOffsetStream(stream, limit: limit, offset: offset); + } +} + +class RawProviderStoreTransaction + extends ProviderStoreTransaction { + RawProviderStoreTransaction(super.provider, super.storeName, + [super.readWrite]); +} + +class ProviderStoreTransaction + extends ProviderStoreTransactionBase { + @protected + ProviderStoreTransaction(super.provider, super.storeName, [super.readWrite]); + + @protected + ProviderStoreTransaction.fromList( + ProviderTransactionList list, String storeName) + : super._() { + _transaction = list._transaction; + _mode = list._mode; + _store = ProviderStore(_transaction!.objectStore(storeName)); + } +} + +class WriteTransactionMixin {} + +abstract class ProviderWritableSourceTransactionMixin + implements + ProviderSourceTransaction, + ProviderWritableSourceTransaction { + late ProviderStore _store; + + @override + Future add(V value, [K? key]) async { + return await _store.objectStore.add(value!, key) as K; + } + + @override + Future put(V value, [K? key]) async { + return await _store.objectStore.put(value!, key) as K; + } + + @override + Future get(K key) async { + return await _store.objectStore.getObject(key!) as V?; + } +} + +abstract class ProviderSourceTransaction { + /// Get an object by key + Future get(K key); +} + +mixin ProviderSourceTransactionMixin + implements ProviderSourceTransaction { + //Future get(K key); + Future count(); + Stream openRawCursor({String? direction}); + + Stream _limitOffsetStream(Stream rawStream, + {int? limit, int? offset}) { + final ctlr = StreamController(sync: true); + + var count = 0; + + void close() { + if (!ctlr.isClosed) { + ctlr.close(); + } + } + + void onCursorValue(T c) { + if (offset != null && offset > 0) { + c.advance(offset); + } else { + if (limit != null) { + if (count >= limit) { + // stop here + close(); + return; + } + } + ctlr.add(c); + count++; + c.next(); + } + } + + rawStream.listen(onCursorValue, onDone: () { + close(); + }); + + //}).asFuture() { + return ctlr.stream; + } +} + +abstract class ProviderWritableSourceTransaction + implements ProviderSourceTransaction { + /// Add an object + Future add(V value, [K? key]); + + /// Put an object + Future put(V value, [K? key]); +} + +class ProviderStoreTransactionBase extends ProviderTransaction + with + ProviderStoreTransactionMixin, + ProviderSourceTransactionMixin { + ProviderStore? _store; + + // not recommended though + //@deprecated + @override + ProviderStore? get store => _store; + + ProviderStoreTransactionBase._(); + + ProviderStoreTransactionBase(Provider provider, String storeName, + [bool readWrite = false]) { + _mode = readWrite ? idbModeReadWrite : idbModeReadOnly; + + try { + _transaction = provider.db!.database!.transaction(storeName, _mode!); + } catch (e) { + // typically db might have been closed so add some debug information + if (provider.isClosed) { + print('database has been closed'); + } + rethrow; + } + _store = ProviderStore(_transaction!.objectStore(storeName)); + } + + @override + Stream openRawCursor({String? direction}) { + return store!.objectStore.openCursor( + // + direction: direction); + } + + Stream openCursor( + {bool reverse = false, int? limit, int? offset}) { + final direction = reverse ? idbDirectionPrev : null; + final stream = openRawCursor(direction: direction); + return _limitOffsetStream(stream, limit: limit, offset: offset); + } +} + +mixin ProviderStoreTransactionMixin { + ProviderStore? get store; + + ProviderIndexTransaction index(String name) => + ProviderIndexTransaction.fromStoreTransaction( + this as ProviderStoreTransaction, name); + + Future count() => store!.count(); + + Future get(K key) async { + final value = await store!.get(key!); + return value as V?; + } + + Future add(V value, [K? key]) async => + (await store!.add(value!, key)) as K; + + Future put(V value, [K? key]) async => + (await store!.put(value!, key)) as K; + + Future delete(K key) => store!.delete(key!); + + Future clear() => store!.clear(); +} + +//class _ProviderStoreInTransactionList extends Object with ProviderStoreTransactionMixin { +// final ProviderStore store; +// _ProviderStoreInTransactionList(this.store); +//} + +class ProviderTransactionList extends ProviderTransaction { + ProviderTransactionList(Provider provider, Iterable storeNames, + [bool readWrite = false]) { + _mode = readWrite ? idbModeReadWrite : idbModeReadOnly; + _transaction = + provider.db!.database!.transactionList(storeNames.toList(), _mode!); + } + ProviderStoreTransaction store(String storeName) { + return ProviderStoreTransaction.fromList(this, storeName); + } + + ProviderIndexTransaction index(String storeName, String indexName) => + store(storeName).index(indexName); +} + +class ProviderTransaction { + //Provider _provider; + Transaction? _transaction; + String? _mode; + + bool get readWrite => _mode == idbModeReadWrite; + bool get readOnly => _mode == idbModeReadOnly; + Future get completed => _transaction!.completed; + +// +// Future add(Map data) { +// if (_store != null) { +// return _store.add(data); +// } +// // should crash then +// return null; +// } +// +// Future> getById(int id) { +// if (_index != null) { +// return _index.get(id); +// +// } else if (_store != null) { +// return _store.getObject(id); +// } +// // should crash then +// return null; +// } +} diff --git a/packages/idb_provider/pubspec.yaml b/packages/idb_provider/pubspec.yaml new file mode 100644 index 0000000..2499aeb --- /dev/null +++ b/packages/idb_provider/pubspec.yaml @@ -0,0 +1,25 @@ +name: tekartik_idb_provider +version: 0.4.0 +description: Provider pattern on top of indexeddb +publish_to: none +environment: + sdk: ^3.4.0 +dependencies: + tekartik_common_utils: + git: + url: https://github.com/tekartik/common_utils.dart + ref: dart3a + idb_shim: '>=2.6.0+2' + collection: any +dev_dependencies: + process_run: '>=0.10.1' + test: any + dev_build: '>=0.13.4' + build_runner: '>=0.9.0' + build_test: '>=0.10.2' + build_web_compilers: '>2.0.0' + path: + sembast: +dependency_overrides: + # idb_shim: + # path: ../../tekartik/idb_shim.dart/idb_shim diff --git a/packages/idb_provider/test/dynamic_provider_test.dart b/packages/idb_provider/test/dynamic_provider_test.dart new file mode 100644 index 0000000..9f9fee6 --- /dev/null +++ b/packages/idb_provider/test/dynamic_provider_test.dart @@ -0,0 +1,124 @@ +library tekartik_dynamic_provider_test; + +import 'package:tekartik_idb_provider/provider.dart'; + +import 'test_common.dart'; + +void main() { + testMain(idbMemoryContext); +} + +void testMain(TestContext context) { + final idbFactory = context.factory; + group('provider_dynamic', () { + group('raw', () { + //DynamicProvider provider; + + test('database', () { + final provider = DynamicProvider(idbFactory, ProviderDbMeta('test')); + + return provider + .delete() + .then((_) => provider.ready!.then((Provider readyProvider) { + expect(provider, readyProvider); + expect(provider.db!.meta!.name, 'test'); + expect(provider.db!.meta!.version, 1); + expect(provider.db!.storeNames, isEmpty); + provider.close(); + })); + }); + + test('database name version', () { + final provider = + DynamicProvider(idbFactory, ProviderDbMeta('test2', 2)); + + return provider + .delete() + .then((_) => provider.ready!.then((Provider readyProvider) { + expect(provider, readyProvider); + expect(provider.db!.meta!.name, 'test2'); + expect(provider.db!.meta!.version, 2); + expect(provider.db!.storeNames, isEmpty); + provider.close(); + })); + }); + }); + }); + group('more', () { + final providerName = 'test'; + + late DynamicProvider provider; + ProviderTransaction? transaction; + + setUp(() { + transaction = null; + provider = DynamicProvider(idbFactory, ProviderDbMeta(providerName)); + return provider.delete(); + }); + tearDown(() async { + if (transaction != null) { + await transaction!.completed; + } + provider.close(); + }); + + test('one_store', () { + provider.addStore(ProviderStoreMeta('store')); + return provider.ready!.then((Provider readyProvider) { + final txn = provider.storeTransaction('store'); + expect(txn.store!.meta!.name, 'store'); + expect(txn.store!.meta!.keyPath, null); + expect(txn.store!.meta!.autoIncrement, false); + + expect(txn.store!.meta!.indecies, isEmpty); + + // for cleanup + transaction = txn; + }); + }); + + test('multiple_store', () { + provider.addStore( + ProviderStoreMeta('store', keyPath: 'key', autoIncrement: true)); + provider.addStore(ProviderStoreMeta('store2')); + return provider.ready!.then((Provider readyProvider) { + final txn = provider.transactionList(['store', 'store2']); + ProviderStoreTransactionMixin txn1 = txn.store('store'); + expect(txn1.store!.meta!.name, 'store'); + expect(txn1.store!.meta!.keyPath, 'key'); + expect(txn1.store!.meta!.autoIncrement, true); + + expect(txn1.store!.meta!.indecies, isEmpty); + + ProviderStoreTransactionMixin txn2 = txn.store('store2'); + expect(txn2.store!.meta!.name, 'store2'); + expect(txn2.store!.meta!.keyPath, null); + expect(txn2.store!.meta!.autoIncrement, false); + + expect(txn2.store!.meta!.indecies, isEmpty); + +// for cleanup + transaction = txn; + }); + }); + + test('one_index', () { + final indexMeta = ProviderIndexMeta('idx', 'my_key'); + provider.addStore(ProviderStoreMeta('store', indecies: [indexMeta])); + return provider.ready!.then((Provider readyProvider) { + final txn = provider.storeTransaction('store'); + expect(txn.store!.meta!.name, 'store'); + expect(txn.store!.meta!.keyPath, null); + expect(txn.store!.meta!.autoIncrement, false); + + expect(txn.store!.meta!.indecies, [indexMeta]); + + // for cleanup + transaction = txn; + }); + }); + }); +} +//class TestApp extends ConsoleApp { +// +//} diff --git a/packages/idb_provider/test/io_test_common.dart b/packages/idb_provider/test/io_test_common.dart new file mode 100644 index 0000000..d914ba3 --- /dev/null +++ b/packages/idb_provider/test/io_test_common.dart @@ -0,0 +1,23 @@ +library tekartik_idb_provider.test.io_test_common; + +import 'package:idb_shim/idb_client_sembast.dart'; +import 'package:path/path.dart'; +import 'package:sembast/sembast_io.dart'; + +import 'test_common.dart'; + +export 'test_common.dart'; + +class IoTestContext extends SembastTestContext { + IoTestContext() { + sdbFactory = databaseFactoryIo; + factory = IdbFactorySembast(databaseFactoryIo, testOutTopPath); + } +} + +IoTestContext idbIoContext = IoTestContext(); + +String get testScriptPath => 'test'; + +String get testOutTopPath => + join('.dart_tool', 'tekartik_idb_provider', 'test'); diff --git a/packages/idb_provider/test/provider_meta_test.dart b/packages/idb_provider/test/provider_meta_test.dart new file mode 100644 index 0000000..79370b3 --- /dev/null +++ b/packages/idb_provider/test/provider_meta_test.dart @@ -0,0 +1,169 @@ +library tekartik_provider_meta_test; + +import 'package:tekartik_idb_provider/provider.dart'; + +import 'test_common.dart'; + +void main() { + testMain(idbMemoryContext); +} + +void testMain(TestContext context) { + final idbFactory = context.factory; + group('meta', () { + group('raw', () { + test('index', () { + final indexMeta = ProviderIndexMeta('idx', 'my_key'); + final indexMeta2 = ProviderIndexMeta('idx', 'my_key'); + expect(indexMeta, indexMeta2); + expect( + indexMeta, + ProviderIndexMeta('idx', 'my_key', + unique: false, multiEntry: false)); + expect( + indexMeta, + isNot(ProviderIndexMeta('idx', 'my_key', + unique: false, multiEntry: true))); + expect( + indexMeta, + isNot(ProviderIndexMeta('idx', 'my_key', + unique: true, multiEntry: false))); + expect( + indexMeta, + isNot(ProviderIndexMeta('idx', 'my_key2', + unique: false, multiEntry: false))); + expect( + indexMeta, + isNot(ProviderIndexMeta('idx2', 'my_key', + unique: false, multiEntry: false))); + }); + + test('store', () { + var storeMeta = ProviderStoreMeta('str'); + expect(storeMeta, ProviderStoreMeta('str')); + expect(storeMeta, isNot(ProviderStoreMeta('str2'))); + expect(storeMeta, + ProviderStoreMeta('str', keyPath: null, autoIncrement: false)); + expect( + storeMeta, + isNot( + ProviderStoreMeta('str', keyPath: null, autoIncrement: true))); + expect( + storeMeta, + isNot(ProviderStoreMeta('str', + keyPath: 'some', autoIncrement: false))); + expect( + storeMeta, + isNot(ProviderStoreMeta('str2', + keyPath: null, autoIncrement: false))); + + storeMeta = + ProviderStoreMeta('str', keyPath: 'some', autoIncrement: true); + var storeMeta2 = + ProviderStoreMeta('str', keyPath: 'some', autoIncrement: true); + expect(storeMeta, storeMeta2); + final indexMeta = ProviderIndexMeta('idx', 'my_key'); + final indexMeta2 = ProviderIndexMeta('idx', 'my_key'); + + storeMeta = ProviderStoreMeta('str', + keyPath: 'some', autoIncrement: true, indecies: [indexMeta]); + expect(storeMeta, isNot(storeMeta2)); + storeMeta = ProviderStoreMeta('str', + keyPath: 'some', + autoIncrement: true, + indecies: [indexMeta, indexMeta2]); + storeMeta2 = ProviderStoreMeta('str', + keyPath: 'some', + autoIncrement: true, + indecies: [indexMeta2, indexMeta]); + expect(storeMeta, storeMeta2); + }); + + test('stores', () { + final storeMeta = ProviderStoreMeta('str'); + final storeMeta2 = ProviderStoreMeta('str2'); + var storesMeta = ProviderStoresMeta([]); + var storesMeta2 = ProviderStoresMeta([]); + expect(storesMeta, storesMeta2); + storesMeta = ProviderStoresMeta([storeMeta]); + expect(storesMeta, isNot(storesMeta2)); + storesMeta2 = ProviderStoresMeta([storeMeta]); + expect(storesMeta, storesMeta2); + storesMeta2 = ProviderStoresMeta([storeMeta2]); + expect(storesMeta, isNot(storesMeta2)); + storesMeta = ProviderStoresMeta([storeMeta, storeMeta2]); + storesMeta2 = ProviderStoresMeta([storeMeta2, storeMeta]); + expect(storesMeta, storesMeta2); + }); + }); + group('provider', () { + group('more', () { + final providerName = 'test'; + + late DynamicProvider provider; + + setUp(() { + provider = DynamicProvider(idbFactory, ProviderDbMeta(providerName)); + return provider.delete(); + }); + tearDown(() async { + provider.close(); + }); + + Future roundCircle(ProviderStoresMeta storesMeta) { + provider.addStores(storesMeta); + return provider.ready!.then((Provider readyProvider) { + return provider.storesMeta!.then((metas) { + expect(metas, storesMeta); + expect(metas, isNot(same(storesMeta))); + }); + }); + } + + test('one_store', () { + provider.addStore(ProviderStoreMeta('store')); + return provider.ready!.then((Provider readyProvider) { + return provider.storesMeta!.then((metas) { + expect(metas, ProviderStoresMeta([ProviderStoreMeta('store')])); + }); + }); + }); + + test('one_store round_cirle', () { + final meta = ProviderStoresMeta([ + //) + ProviderStoreMeta('store') + ]); + return roundCircle(meta); + }); + test('two_stores', () { + provider.addStore(ProviderStoreMeta('store')); + provider.addStore(ProviderStoreMeta('store1')); + return provider.ready!.then((Provider readyProvider) { + return provider.storesMeta!.then((metas) { + expect( + metas, + ProviderStoresMeta([ + ProviderStoreMeta('store'), + ProviderStoreMeta('store1') + ])); + }); + }); + }); + + test('one_index', () { + final meta = ProviderStoresMeta([ + //) + ProviderStoreMeta('store', indecies: // + [ProviderIndexMeta('idx', 'my_key')] // + ) + ]); + return roundCircle(meta); + }); + }); + }); + }); +} +//class TestApp extends ConsoleApp { +// +//} diff --git a/packages/idb_provider/test/provider_test.dart b/packages/idb_provider/test/provider_test.dart new file mode 100644 index 0000000..8633b47 --- /dev/null +++ b/packages/idb_provider/test/provider_test.dart @@ -0,0 +1,216 @@ +library tekartik_app_provider_test; + +import 'package:idb_shim/idb_client.dart'; +import 'package:tekartik_idb_provider/provider.dart'; + +import 'test_common.dart'; +import 'test_provider.dart'; + +void main() { + testMain(idbMemoryContext); +} + +void testMain(TestContext context) { + final idbFactory = context.factory; + + group('provider', () { + group('row', () { + final providerName = 'test'; + + late DynamicProvider provider; + ProviderTransaction? transaction; + + setUp(() { + provider = DynamicProvider(idbFactory, ProviderDbMeta(providerName)); + return provider.delete(); + }); + tearDown(() async { + await transaction?.completed; + provider.close(); + }); + + test('int_map', () { + provider.addStore(ProviderStoreMeta('store', autoIncrement: true)); + return provider.ready!.then((Provider readyProvider) { + final txn = provider.storeTransaction('store', true); + // for cleanup + transaction = txn; + + return txn.put({'test': 1}).then((key) { + return txn.get(key).then((value) { + final row = + intMapProviderRawFactory.newRow(key as int, value as Map); + + // Cursor + txn.openCursor().listen((cwv) { + final cursorRow = + intMapProviderRawFactory.cursorWithValueRow(cwv); + expect(cursorRow, row); + }); + }); + }); + }); + }); + }); + }); + group('test_provider_open', () { + test('open', () async { + // open normal + final provider = TestProvider(idbFactory); + final provider2 = TestProvider(idbFactory); + await provider.delete().then((_) { + expect(provider.isReady, isFalse); + final done = provider.ready!.then((readyProvider) { + expect(readyProvider, provider); + }); + // not ready yet when opening the db + expect(provider.isReady, isFalse); + return done; + }).then((_) { + // open using an incoming db + expect(provider2.isReady, isFalse); + provider2.db = provider.db; + expect(provider2.isReady, isTrue); + final done = provider2.ready!.then((readyProvider2) { + expect(readyProvider2, provider2); + }); + return done; + }); + + provider.close(); + }); + }); + group('test_provider', () { + final provider = TestProvider(idbFactory); + setUp(() { + return provider.delete().then((_) { + return provider.ready!.then((_) { + //print(provider.db); + }); + }); + }); + tearDown(() { + provider.close(); + }); + test('toString', () { + //print(provider); + expect(provider.toString(), startsWith('{')); + + final anotherProvider = TestProvider(idbFactory); + expect(anotherProvider.toString(), '{}'); + //print(anotherProvider); + }); + test('empty', () { + return provider.count().then((count) { + expect(count, 0); + }); + }); + + test('put/get', () { + return provider.putName('test').then((int key) { + expect(key, 1); + return provider.getName(key).then((String? name) { + expect(name, 'test'); + }); + }); + }); + + test('get/put', () { + return provider.getName(1).then((data) { + expect(data, isNull); + return provider.putName('test').then((int key) { + expect(key, 1); + return provider.getName(key).then((String? name) { + expect(name, 'test'); + }); + }).then((_) { + return provider.count().then((count) { + expect(count, 1); + }); + }); + }); + }); + + Future slowCount() { + final trans = RawProviderStoreTransaction(provider, itemsStore); + var count = 0; + return trans.store!.objectStore + .openCursor( + // + direction: idbDirectionNext, + autoAdvance: false) + .listen((CursorWithValue cwv) { + count++; + }) + .asFuture() + .then((_) { + return count; + }); + } + + test('cursor count', () { + slowCount().then((int count) { + expect(count, 0); + }); + }); + test('getNames', () { + final c1 = 'C1'; + final a2 = 'A2'; + final b3 = 'B3'; + /* + int c1; + int a2; + int b3; + */ + return provider.getNames().then((var list) { + expect(list, isEmpty); + }).then((_) { + return provider.putName(c1).then((int key) { + //c1 = key; + }); + }).then((_) { + return provider.getNames().then((var list) { + expect(list, [c1]); + //expect(list.first, C1); + }); + }).then((_) { + return provider.putName(a2).then((int key) { + //a2 = key; + }); + }).then((_) { + return provider.putName(b3).then((int key) { + //b3 = key; + }); + }).then((_) { + return provider.getNames().then((var list) { + expect(list, [c1, a2, b3]); + //expect(list.first, C1); + }); + }).then((_) { + return provider.getNames(limit: 2).then((var list) { + expect(list, [c1, a2]); + //expect(list.first, C1); + }); + }).then((_) { + return provider.getOrderedNames().then((var list) { + expect(list, [a2, b3, c1]); + //expect(list.first, C1); + }); + }); + }); + + test('put/clear/get', () { + return provider.putName('test').then((int key) { + expect(key, 1); + return provider.clear().then((_) { + return provider.getName(key).then((String? name) { + expect(name, isNull); + }); + }); + }); + }); + }); +} +//class TestApp extends ConsoleApp { +// +//} diff --git a/packages/idb_provider/test/provider_transaction_test.dart b/packages/idb_provider/test/provider_transaction_test.dart new file mode 100644 index 0000000..7e87b9d --- /dev/null +++ b/packages/idb_provider/test/provider_transaction_test.dart @@ -0,0 +1,96 @@ +library tekartik_app_transaction_test; + +import 'package:idb_shim/idb_client.dart'; +import 'package:tekartik_common_utils/common_utils_import.dart'; +import 'package:tekartik_idb_provider/provider.dart'; + +import 'test_common.dart'; + +void main() { + testMain(idbMemoryContext); +} + +void testMain(TestContext context) { + final idbFactory = context.factory; + //devWarning; + // TODO Add store transaction test + group('transaction', () { + //String providerName = 'test'; + final storeName = 'store'; + final indexName = 'index'; + final indexKey = 'my_key'; + + late DynamicProvider provider; + + Future providerSetUp() { + provider = DynamicProvider(idbFactory, ProviderDbMeta(context.dbName)); + return provider.delete().then((_) { + final indexMeta = ProviderIndexMeta(indexName, indexKey); + provider.addStore(ProviderStoreMeta(storeName, + indecies: [indexMeta], autoIncrement: true)); + return provider.ready; + }); + } + + tearDown(() async { + //await transaction?.completed; + provider.close(); + }); + + test('store_cursor', () async { + await providerSetUp(); + final storeTxn = provider.storeTransaction(storeName, true); + // put one with a key one without + storeTxn.put({'value': 'value1'}).unawait(); + storeTxn.put({'value': 'value2'}).unawait(); + await storeTxn.completed; + + final txn = provider.storeTransaction(storeName, false); + final data = []; + //List keyData = []; + txn.openCursor().listen((CursorWithValue cwv) { + data.add(cwv.value as Map); + }); + + // listed last + await txn.completed; + expect(data, [ + {'value': 'value1'}, + {'value': 'value2'} + ]); + }); + + test('store_index', () async { + await providerSetUp(); + final storeTxn = provider.storeTransaction(storeName, true); + // put one with a key one without + storeTxn.put({'value': 'value1'}).unawait(); + storeTxn.put({'my_key': 2, 'value': 'value2'}).unawait(); + storeTxn.put({'value': 'value3'}).unawait(); + storeTxn.put({'my_key': 1, 'value': 'value4'}).unawait(); + await storeTxn.completed; + + final txn = provider.indexTransaction(storeName, indexName); + final data = []; + final keyData = []; + txn.openCursor().listen((CursorWithValue cwv) { + data.add(cwv.value as Map); + }); + txn.openKeyCursor().listen((Cursor c) { + keyData.add(c.key as int); + }); + + // listed last + await txn.completed; + expect(data.length, 2); + expect(data[0], {'my_key': 1, 'value': 'value4'}); + expect(data[1], {'my_key': 2, 'value': 'value2'}); + expect(keyData.length, 2); + expect(keyData[0], 1); + expect(keyData[1], 2); + }); + }); +} +//class TestApp extends ConsoleApp { +// +//} diff --git a/packages/idb_provider/test/record_provider_test.dart b/packages/idb_provider/test/record_provider_test.dart new file mode 100644 index 0000000..c4d3b0b --- /dev/null +++ b/packages/idb_provider/test/record_provider_test.dart @@ -0,0 +1,353 @@ +import 'package:idb_shim/idb_client.dart'; +import 'package:tekartik_idb_provider/provider.dart'; +import 'package:tekartik_idb_provider/record_provider.dart'; + +import 'test_common.dart'; +//IdbFactory idbFactory; + +void main() { + testMain(idbMemoryContext); +} + +const String dbFieldName = 'name'; + +mixin DbBasicRecordMixin { + String? name; + + void fillFromDbEntry(Map entry) { + name = entry[dbFieldName] as String?; + } + + void fillDbEntry(Map entry) { + if (name != null) { + entry[dbFieldName] = name; + } + } +} + +class DbBasicRecordBase extends DbRecordBase with DbBasicRecordMixin { + @override + dynamic id; + DbBasicRecordBase(); + + /// create if null + factory DbBasicRecordBase.fromDbEntry(Map entry) { + final record = DbBasicRecordBase(); + record.fillFromDbEntry(entry); + return record; + } +} + +class DbBasicRecord extends DbRecord with DbBasicRecordMixin { + @override + Object? id; + + DbBasicRecord(); + + /// create if null + static DbBasicRecord? fromDbEntry(Map? entry, String? id) { + if (entry != null && id != null) { + final record = DbBasicRecord()..id = id; + record.fillFromDbEntry(entry); + return record; + } + return null; + } +} + +class DbBasicRecordProvider extends DbRecordProvider { + @override + String get store => DbBasicAppProvider.basicStore; + @override + DbBasicRecord? fromEntry(Map? entry, String? id) => + DbBasicRecord.fromDbEntry(entry, id); +} + +class DbBasicAppProvider extends DynamicProvider + with DbRecordProvidersMixin, DbRecordProvidersMapMixin { + DbBasicRecordProvider basic = DbBasicRecordProvider(); + + // version 1 - initial + static const int dbVersion = 1; + + static const String basicStore = 'basic'; + + static const String defaultDbName = + 'com.tekartik.tekartik_idb_provider.record_test.db'; + + //static const String currentIndex = dbFieldCurrent; + + // _dbVersion for testing + DbBasicAppProvider(IdbFactory idbFactory, String dbName, [int? dbVersion]) + : super.noMeta(idbFactory) { + basic.provider = this; + dbVersion ??= DbBasicAppProvider.dbVersion; + + init(idbFactory, dbName, dbVersion); + + providerMap = { + basicStore: basic, + }; + } + + @override + void close() { + closeAll(); + super.close(); + } + + /* + Future getCurrentProjectProvider() async { + var txn = project.storeTransaction(true); + var index = txn.index(currentIndex); + DbProject dbProject = await index.get(true); + await txn.completed; + ProjectProvider projectProvider; + if (dbProject != null) { + projectProvider = + new ProjectProvider(idbFactory, '${appPackage}-${dbProject.id}.db'); + } + return projectProvider; + } + */ + @override + void onUpdateDatabase(VersionChangeEvent e) { + //devPrint('${e.newVersion}/${e.oldVersion}'); + //Database db = e.database; + //int version = e.oldVersion; + +// if (e.oldVersion == 1) { +// // add index +// // e.transaction +// +// } + if (e.oldVersion < 2) { + //db.deleteObjectStore(ENTRIES_STORE); + + // default erase everything, we don't care we sync + db!.deleteStore(basicStore); + + final nameIndexMeta = ProviderIndexMeta(dbFieldName, dbFieldName); + + final basicStoreMeta = ProviderStoreMeta(basicStore, + autoIncrement: true, indecies: [nameIndexMeta]); + + // ProviderIndex fileIndex = entriesStore.createIndex(indexMeta); + + addStores(ProviderStoresMeta([basicStoreMeta])); + + //providerStore.c + } else if (e.newVersion > 2 && e.oldVersion < 3) { + /* + ObjectStore store = e.transaction.objectStore(basicStore); + //idbDevPrint(store); + ProviderStore providerStore = new ProviderStore(store); + */ + } + + super.onUpdateDatabase(e); + } +} + +void testMain(TestContext context) { + final idbFactory = context.factory; + group('record_provider', () { + group('DbRecordBase', () { + test('equality', () { + final record1 = DbBasicRecordBase(); + final record2 = DbBasicRecordBase(); + expect(record1.hashCode, record2.hashCode); + expect(record1, record2); + + record1.name = 'value'; + + expect(record1.hashCode, isNot(record2.hashCode)); + expect(record1, isNot(record2)); + + record2.name = 'value'; + + expect(record1.hashCode, record2.hashCode); + expect(record1, record2); + }); + }); + + group('DbRecord', () { + test('toString', () { + final record1 = DbBasicRecord(); + + expect(record1.toString(), '{}'); + record1.id = 'key1'; + expect(record1.toString(), '{_id: key1}'); + record1.name = 'test'; + expect(record1.toString(), '{name: test, _id: key1}'); + }); + test('equality', () { + final record1 = DbBasicRecord(); + final record2 = DbBasicRecord(); + expect(record1.hashCode, record2.hashCode); + expect(record1, record2); + + record1.id = 'key'; + + expect(record1.hashCode, isNot(record2.hashCode)); + expect(record1, isNot(record2)); + + record2.id = 'key'; + + expect(record1.hashCode, record2.hashCode); + expect(record1, record2); + + record1.name = 'value'; + + expect(record1.hashCode, isNot(record2.hashCode)); + expect(record1, isNot(record2)); + + record2.name = 'value'; + + expect(record1.hashCode, record2.hashCode); + expect(record1, record2); + }); + }); + + group('access', () { + test('version', () async { + var appProvider = DbBasicAppProvider(idbFactory, context.dbName); + await appProvider.delete(); + await appProvider.ready; + appProvider.close(); + + appProvider = + DbBasicAppProvider(idbFactory, DbBasicAppProvider.defaultDbName, 3); + await appProvider.ready; + }); + + test('open', () async { + final appProvider = DbBasicAppProvider(idbFactory, context.dbName); + await appProvider.delete(); + await appProvider.ready; + + final readTxn = appProvider.basic.readTransaction; + expect(await appProvider.basic.txnGet(readTxn, '_1'), isNull); + await readTxn.completed; + + final record = DbBasicRecord(); + record.name = 'test'; + record.id = '_1'; + + final writeTxn = appProvider.basic.writeTransaction; + + var key = (await writeTxn.putRecord(record)).id; + expect(key, '_1'); + expect((await appProvider.basic.txnGet(writeTxn, '_1'))!.id, '_1'); + await writeTxn.completed; + + var txn = appProvider.basic.storeReadTransaction; + var stream = txn.openCursor(limit: 1); + await stream.listen((CursorWithValue cwv) { + final record = DbBasicRecord.fromDbEntry( + cwv.value as Map, cwv.primaryKey as String)!; + expect(record.id, '_1'); + }).asFuture(); + + await txn.completed; + + var txnList = appProvider.dbRecordProviderReadTransactionList( + [DbBasicAppProvider.basicStore]); + var singleReadTxn = appProvider.basic.txnListReadTransaction(txnList); + expect((await appProvider.basic.txnGet(singleReadTxn, '_1'))!.id, '_1'); + + await txnList.completed; + + var writeTxnList = appProvider.dbRecordProviderWriteTransactionList( + [DbBasicAppProvider.basicStore]); + //txn = appProvider.basic.txnListReadTransaction(txnList); + + var singleWriteTxn = + appProvider.basic.txnListWriteTransaction(writeTxnList); + expect( + (await appProvider.basic.txnGet(singleWriteTxn, '_1'))!.id, '_1'); + await appProvider.basic.txnClear(singleWriteTxn); + expect((await appProvider.basic.txnGet(singleWriteTxn, '_1')), isNull); + + await txnList.completed; + //var txn = basicRecordProvider.store; + //basicRecordProvider.get() + }); + + test('write', () async { + final appProvider = DbBasicAppProvider(idbFactory, context.dbName); + await appProvider.delete(); + await appProvider.ready; + + final writeTxn = appProvider.basic.writeTransaction; + DbRecordProviderTransaction txn = writeTxn; + + final record = DbBasicRecord(); + record.name = 'test'; + record.id = '_1'; + + var key = + (await (txn as DbRecordProviderWriteTransaction).putRecord(record)) + .id; + expect(key, '_1'); + expect((await appProvider.basic.txnGet(txn, '_1'))!.id, '_1'); + await txn.completed; + + txn = appProvider.basic.storeReadTransaction; + var stream = txn.openCursor(limit: 1); + await stream.listen((CursorWithValue cwv) { + final record = DbBasicRecord.fromDbEntry( + cwv.value as Map, cwv.primaryKey as String)!; + expect(record.id, '_1'); + }).asFuture(); + + await txn.completed; + + var txnList = appProvider.dbRecordProviderReadTransactionList( + [DbBasicAppProvider.basicStore]); + txn = appProvider.basic.txnListReadTransaction(txnList); + expect((await appProvider.basic.txnGet(txn, '_1'))!.id, '_1'); + + await txnList.completed; + + var writeTxnList = appProvider.dbRecordProviderWriteTransactionList( + [DbBasicAppProvider.basicStore]); + //txn = appProvider.basic.txnListReadTransaction(txnList); + + txn = appProvider.basic.txnListWriteTransaction(writeTxnList); + expect((await appProvider.basic.txnGet(txn, '_1'))!.id, '_1'); + await appProvider.basic + .txnClear(txn as DbRecordProviderWriteTransaction); + expect((await appProvider.basic.txnGet(txn, '_1')), isNull); + + await txnList.completed; + //var txn = basicRecordProvider.store; + //basicRecordProvider.get() + }); + + test('index', () async { + final appProvider = DbBasicAppProvider(idbFactory, context.dbName); + await appProvider.delete(); + await appProvider.ready; + final writeTxn = appProvider.basic.writeTransaction; + DbRecordProviderTransaction txn = writeTxn; + + final record = DbBasicRecord(); + record.name = 'test'; + record.id = '_1'; + + var key = + (await (txn as DbRecordProviderWriteTransaction).putRecord(record)) + .id; + + txn = appProvider.basic.readTransaction; + var index = txn.index(dbFieldName); + + expect((await appProvider.basic.indexGet(index, 'test'))!.id, key); + + //index. + //expect(key, '_1'); + }); + }); + }); +} diff --git a/packages/idb_provider/test/synced_record_provider_test.dart b/packages/idb_provider/test/synced_record_provider_test.dart new file mode 100644 index 0000000..36c30e8 --- /dev/null +++ b/packages/idb_provider/test/synced_record_provider_test.dart @@ -0,0 +1,649 @@ +import 'package:idb_shim/idb_client.dart'; +import 'package:tekartik_idb_provider/provider.dart'; +import 'package:tekartik_idb_provider/record_provider.dart'; + +import 'test_common.dart'; +//IdbFactory idbFactory; + +void main() { + testMain(idbMemoryContext); +} + +const String dbFieldName = 'name'; + +mixin DbBasicRecordMixin { + String? name; + + void mixinFillFromDbEntry(Map entry) { + name = entry[dbFieldName] as String?; + } + + void mixinFillDbEntry(Map entry) { + if (name != null) { + entry[dbFieldName] = name; + } + } +} + +class DbAutoRecord extends DbSyncedRecordBase + with DbBasicRecordMixin { + @override + int? id; + + /// create if null + static DbAutoRecord? fromDbEntry(Map? entry, int? id) { + if (entry == null || id == null) { + return null; + } + final record = DbAutoRecord()..id = id; + record.fillFromDbEntry(entry); + return record; + } + + @override + void fillFromDbEntry(Map entry) { + super.fillFromDbEntry(entry); + mixinFillFromDbEntry(entry); + } + + @override + void fillDbEntry(Map entry) { + super.fillDbEntry(entry); + mixinFillDbEntry(entry); + } +} + +class DbBasicRecord extends DbSyncedRecordBase + with DbBasicRecordMixin { + DbBasicRecord(); + + /// create if null + static DbBasicRecord? fromDbEntry(Map? entry, String? id) { + if (entry == null) { + return null; + } + final record = DbBasicRecord()..id = id; + record.fillFromDbEntry(entry); + return record; + } + + @override + void fillFromDbEntry(Map entry) { + super.fillFromDbEntry(entry); + mixinFillFromDbEntry(entry); + } + + @override + void fillDbEntry(Map entry) { + super.fillDbEntry(entry); + mixinFillDbEntry(entry); + } + + @override + String? id; +} + +class DbBasicRecordProvider + extends DbSyncedRecordProvider { + @override + String get store => DbBasicAppProvider.basicStore; + + @override + DbBasicRecord? fromEntry(Map? entry, String? id) => + DbBasicRecord.fromDbEntry(entry, id); +} + +class DbAutoRecordProvider extends DbSyncedRecordProvider { + @override + String get store => DbBasicAppProvider.autoStore; + + @override + DbAutoRecord? fromEntry(Map? entry, int? id) => + DbAutoRecord.fromDbEntry(entry, id); +} + +class DbBasicAppProvider extends DynamicProvider + with DbRecordProvidersMixin, DbRecordProvidersMapMixin { + DbBasicRecordProvider basic = DbBasicRecordProvider(); + DbAutoRecordProvider auto = DbAutoRecordProvider(); + + // version 1 - initial + static const int dbVersion = 1; + + static const String basicStore = 'basic'; + static const String autoStore = 'auto'; + + static const String defaultDbName = + 'com.tekartik.tekartik_idb_provider.record_test.db'; + + //static const String currentIndex = dbFieldCurrent; + + // _dbVersion for testing + DbBasicAppProvider(IdbFactory idbFactory, String dbName, [int? dbVersion]) + : super.noMeta(idbFactory) { + dbVersion ??= DbBasicAppProvider.dbVersion; + + init(idbFactory, dbName, dbVersion); + + providerMap = {basicStore: basic, autoStore: auto}; + + initAll(this); + } + + @override + void close() { + closeAll(); + super.close(); + } + + /* + Future getCurrentProjectProvider() async { + var txn = project.storeTransaction(true); + var index = txn.index(currentIndex); + DbProject dbProject = await index.get(true); + await txn.completed; + ProjectProvider projectProvider; + if (dbProject != null) { + projectProvider = + new ProjectProvider(idbFactory, '${appPackage}-${dbProject.id}.db'); + } + return projectProvider; + } + */ + @override + void onUpdateDatabase(VersionChangeEvent e) { + //devPrint('${e.newVersion}/${e.oldVersion}'); + //Database db = e.database; + //int version = e.oldVersion; + +// if (e.oldVersion == 1) { +// // add index +// // e.transaction +// +// } + if (e.oldVersion < 2) { + //db.deleteObjectStore(ENTRIES_STORE); + + // default erase everything, we don't care we sync + db!.deleteStore(basicStore); + db!.deleteStore(autoStore); + + final nameIndexMeta = ProviderIndexMeta(dbFieldName, dbFieldName); + final dirtyIndexMeta = ProviderIndexMeta(DbField.dirty, DbField.dirty); + final syncIdIndexMeta = ProviderIndexMeta(DbField.syncId, DbField.syncId); + final basicStoreMeta = ProviderStoreMeta(basicStore, + indecies: [nameIndexMeta, dirtyIndexMeta, syncIdIndexMeta]); + final autoStoreMeta = ProviderStoreMeta(autoStore, + autoIncrement: true, + indecies: [nameIndexMeta, dirtyIndexMeta, syncIdIndexMeta]); + + // ProviderIndex fileIndex = entriesStore.createIndex(indexMeta); + //devPrint(autoStoreMeta); + addStores(ProviderStoresMeta([basicStoreMeta, autoStoreMeta])); + + //providerStore.c + } else if (e.newVersion > 2 && e.oldVersion < 3) { + /* + ObjectStore store = e.transaction.objectStore(basicStore); + //idbDevPrint(store); + ProviderStore providerStore = new ProviderStore(store); + */ + } + + super.onUpdateDatabase(e); + } +} + +void testMain(TestContext context) { + final idbFactory = context.factory; + group('synced_record_provider', () { + group('DbRecord', () { + test('toString', () { + final record1 = DbBasicRecord(); + + expect(record1.toString(), '{}'); + record1.id = 'key1'; + expect(record1.toString(), '{_id: key1}'); + record1.name = 'test'; + expect(record1.toString(), '{name: test, _id: key1}'); + }); + test('equality', () { + final record1 = DbBasicRecord(); + final record2 = DbBasicRecord(); + expect(record1.hashCode, record2.hashCode); + expect(record1, record2); + + record1.id = 'key'; + + expect(record1.hashCode, isNot(record2.hashCode)); + expect(record1, isNot(record2)); + + record2.id = 'key'; + + expect(record1.hashCode, record2.hashCode); + expect(record1, record2); + + record1.name = 'value'; + + expect(record1.hashCode, isNot(record2.hashCode)); + expect(record1, isNot(record2)); + + record2.name = 'value'; + + expect(record1.hashCode, record2.hashCode); + expect(record1, record2); + }); + }); + + group('access', () { + test('version', () async { + var appProvider = DbBasicAppProvider(idbFactory, context.dbName); + await appProvider.delete(); + await appProvider.ready; + appProvider.close(); + + appProvider = + DbBasicAppProvider(idbFactory, DbBasicAppProvider.defaultDbName, 3); + await appProvider.ready; + }); + + test('open', () async { + final appProvider = DbBasicAppProvider(idbFactory, context.dbName); + await appProvider.delete(); + await appProvider.ready; + + final DbRecordProviderReadTransaction readTxn = + appProvider.basic.readTransaction; + DbRecordProviderTransaction txn = readTxn; + expect(await appProvider.basic.txnGet(readTxn, '_1'), isNull); + await readTxn.completed; + + final record = DbBasicRecord(); + record.name = 'test'; + record.id = '_1'; + final DbRecordProviderWriteTransaction + writeTxn = appProvider.basic.writeTransaction; + txn = writeTxn; + + var key = + (await (txn as DbRecordProviderWriteTransaction).putRecord(record)) + .id; + expect(key, '_1'); + expect((await appProvider.basic.txnGet(txn, '_1'))!.id, '_1'); + await txn.completed; + + txn = appProvider.basic.storeReadTransaction; + var stream = txn.openCursor(limit: 1); + await stream.listen((CursorWithValue cwv) { + final record = DbBasicRecord.fromDbEntry( + cwv.value as Map, cwv.primaryKey as String)!; + expect(record.id, '_1'); + }).asFuture(); + + await txn.completed; + + DbRecordProviderTransactionList txnList = appProvider + .dbRecordProviderReadTransactionList( + [DbBasicAppProvider.basicStore]); + txn = appProvider.basic.txnListReadTransaction(txnList); + expect((await appProvider.basic.txnGet(txn, '_1'))!.id, '_1'); + + await txnList.completed; + + txnList = appProvider.dbRecordProviderWriteTransactionList( + [DbBasicAppProvider.basicStore]); + //txn = appProvider.basic.txnListReadTransaction(txnList); + + txn = appProvider.basic.txnListWriteTransaction( + txnList as DbRecordProviderWriteTransactionList); + expect((await appProvider.basic.txnGet(txn, '_1'))!.id, '_1'); + await appProvider.basic + .txnClear(txn as DbRecordProviderWriteTransaction, syncing: true); + expect((await appProvider.basic.txnGet(txn, '_1')), isNull); + + await txnList.completed; + //var txn = basicRecordProvider.store; + //basicRecordProvider.get() + }); + + test('write', () async { + final appProvider = DbBasicAppProvider(idbFactory, context.dbName); + await appProvider.delete(); + await appProvider.ready; + + final DbRecordProviderWriteTransaction + writeTxn = appProvider.basic.writeTransaction; + DbRecordProviderTransaction txn = writeTxn; + + final record = DbBasicRecord(); + record.name = 'test'; + record.id = '_1'; + + var key = + (await (txn as DbRecordProviderWriteTransaction).putRecord(record)) + .id; + expect(key, '_1'); + expect((await appProvider.basic.txnGet(txn, '_1'))!.id, '_1'); + await txn.completed; + + txn = appProvider.basic.storeReadTransaction; + var stream = txn.openCursor(limit: 1); + await stream.listen((CursorWithValue cwv) { + final record = DbBasicRecord.fromDbEntry( + cwv.value as Map, cwv.primaryKey as String)!; + expect(record.id, '_1'); + }).asFuture(); + + await txn.completed; + + DbRecordProviderTransactionList txnList = appProvider + .dbRecordProviderReadTransactionList( + [DbBasicAppProvider.basicStore]); + txn = appProvider.basic.txnListReadTransaction(txnList); + expect((await appProvider.basic.txnGet(txn, '_1'))!.id, '_1'); + + await txnList.completed; + + txnList = appProvider.dbRecordProviderWriteTransactionList( + [DbBasicAppProvider.basicStore]); + //txn = appProvider.basic.txnListReadTransaction(txnList); + + txn = appProvider.basic.txnListWriteTransaction( + txnList as DbRecordProviderWriteTransactionList); + expect((await appProvider.basic.txnGet(txn, '_1'))!.id, '_1'); + await appProvider.basic + .txnClear(txn as DbRecordProviderWriteTransaction, syncing: true); + expect((await appProvider.basic.txnGet(txn, '_1')), isNull); + + await txnList.completed; + //var txn = basicRecordProvider.store; + //basicRecordProvider.get() + }); + + test('index', () async { + final appProvider = DbBasicAppProvider(idbFactory, context.dbName); + await appProvider.delete(); + await appProvider.ready; + + final DbRecordProviderWriteTransaction + writeTxn = appProvider.basic.writeTransaction; + DbRecordProviderTransaction txn = writeTxn; + + final record = DbBasicRecord(); + record.name = 'test'; + record.id = '_1'; + + var key = + (await (txn as DbRecordProviderWriteTransaction).putRecord(record)) + .id; + await txn.completed; + + txn = appProvider.basic.readTransaction; + var index = txn.index(dbFieldName); + + expect((await appProvider.basic.indexGet(index, 'test'))!.id, key); + + await txn.completed; + //index. + //expect(key, '_1'); + }); + }); + + group('auto', () { + late DbAutoRecordProvider provider; + late DbBasicAppProvider appProvider; + + setUp(() async { + appProvider = DbBasicAppProvider(idbFactory, context.dbName); + await appProvider.delete(); + await appProvider.ready; + provider = appProvider.auto; + }); + tearDown(() { + appProvider.close(); + }); + + test('get_none', () async { + expect(await provider.get(123), isNull); + expect(await provider.getBySyncId('123'), isNull); + }); + + test('put/get/getBySyncId', () async { + DbAutoRecord? project = DbAutoRecord(); + project.name = 'my_name'; + project.setSyncInfo('my_sync_id', 'my_sync_version'); + expect(project.version, isNull); + project = await provider.put(project, syncing: true); + expect(project.id, 1); + expect(project.version, 1); + expect(project.dirty, false); + expect(project.deleted, false); + expect(project.name, 'my_name'); + expect(project.syncId, 'my_sync_id'); + expect(project.syncVersion, 'my_sync_version'); + + project = (await provider.get(project.id!))!; + expect(project.id, 1); + expect(project.dirty, false); + expect(project.deleted, false); + expect(project.version, 1); + expect(project.name, 'my_name'); + expect(project.syncId, 'my_sync_id'); + expect(project.syncVersion, 'my_sync_version'); + + await provider.updateSyncInfo(project, 'my_sync_id', 'my_sync_version'); + + project = await provider.getBySyncId('123'); + expect(project, isNull); + project = (await provider.getBySyncId('my_sync_id'))!; + expect(project.id, 1); + + project = await provider.put(project, syncing: true); + expect(project.dirty, false); + final list2 = (await provider.get(project.id!))!; + expect(list2.id, project.id); + expect(list2.name, project.name); + expect(project.syncId, 'my_sync_id'); + expect(project.syncVersion, 'my_sync_version'); + expect(project.dirty, false); + expect(project.deleted, false); + }); + + test('project_sync_info', () async { + var project = DbAutoRecord(); + project.setSyncInfo('my_sync_id', 'my_sync_version'); + project = await provider.put(project); + expect(project.syncId, null); + expect(project.syncVersion, null); + + // updating won't work if not syncing + project.setSyncInfo('my_sync_id_2', 'my_sync_version_2'); + project = await provider.put(project); + expect(project.syncId, null); + expect(project.syncVersion, null); + + // but will if syncing + project.setSyncInfo('my_sync_id_2', 'my_sync_version_2'); + project = await provider.put(project, syncing: true); + expect(project.syncId, 'my_sync_id_2'); + expect(project.syncVersion, 'my_sync_version_2'); + + // or through direct update + await provider.updateSyncInfo(project, 'my_sync_id', 'my_sync_version'); + project = (await provider.get(project.id!))!; + expect(project.syncId, 'my_sync_id'); + expect(project.syncVersion, 'my_sync_version'); + }); + + test('.list.getFirstDirty(', () async { + expect(await provider.getFirstDirty(), isNull); + DbAutoRecord? list = DbAutoRecord(); + list = await provider.put(list); + expect((await provider.getFirstDirty())!.id, list.id); + }); + + test('delete_none', () async { + // none + await provider.delete(0); + }); + + test('delete', () async { + // none + await provider.delete(0); + + // create for deletion + DbAutoRecord? list = DbAutoRecord(); + list = await provider.put(list); + + await provider.delete(list.id!); + + expect(await provider.get(list.id!), isNull); + + // create for deletion with a syncId (won't be deleted + list = DbAutoRecord()..setSyncInfo('1', null); + list = await provider.put(list, syncing: true); + + await provider.delete(list.id!); + + expect((await provider.get(list.id!))!.deleted, isTrue); + + await provider.delete(list.id!, syncing: true); + expect(await provider.get(list.id!), isNull); + }); + }); + + group('put', () { + late DbBasicRecordProvider basicProvider; + late DbAutoRecordProvider autoProvider; + late DbBasicAppProvider appProvider; + + setUp(() async { + appProvider = DbBasicAppProvider(idbFactory, context.dbName); + await appProvider.delete(); + await appProvider.ready; + basicProvider = appProvider.basic; + autoProvider = appProvider.auto; + }); + tearDown(() { + appProvider.close(); + }); + + test('put', () async { + var dbRecord = DbBasicRecord()..id = 'key'; + dbRecord = await basicProvider.put(dbRecord); + expect(dbRecord.id, 'key'); + }); + + test('put_auto', () async { + var dbRecord = DbAutoRecord(); + + dbRecord = await autoProvider.put(dbRecord); + expect(dbRecord.id, 1); + expect(dbRecord.version, 1); + expect(dbRecord.dirty, true); + + await autoProvider.put(dbRecord); + expect(dbRecord.id, 1); + expect(dbRecord.version, 2); + expect(dbRecord.dirty, true); + + await autoProvider.put(dbRecord, syncing: true); + expect(dbRecord.id, 1); + expect(dbRecord.version, 3); + expect(dbRecord.dirty, false); + + var txn = autoProvider.storeTransaction(true) + as DbRecordProviderWriteTransaction; + dbRecord = (await autoProvider.txnPut(txn, dbRecord))!; + await txn.completed; + expect(dbRecord.id, 1); + expect(dbRecord.version, 4); + expect(dbRecord.dirty, true); + + txn = autoProvider.storeTransaction(true) + as DbRecordProviderWriteTransaction; + dbRecord = (await autoProvider.txnPut(txn, dbRecord, syncing: true))!; + await txn.completed; + expect(dbRecord.id, 1); + expect(dbRecord.version, 5); + expect(dbRecord.dirty, false); + }); + + test('delete_auto', () async { + var dbRecord = DbAutoRecord(); + + // not synced yet + dbRecord.setSyncInfo('1', 'ver'); + dbRecord.deleted = true; + dbRecord = await autoProvider.put(dbRecord); + expect(dbRecord.id, 1); + expect(dbRecord.version, 1); + expect(dbRecord.dirty, true); + expect(dbRecord.syncId, isNull); + expect(dbRecord.syncVersion, isNull); + expect(dbRecord.deleted, false); + + await autoProvider.delete(dbRecord.id!); + expect(await autoProvider.get(dbRecord.id!), isNull); + + // synced + dbRecord = DbAutoRecord(); + dbRecord.setSyncInfo('1', 'ver'); + dbRecord = await autoProvider.put(dbRecord, syncing: true); + expect(dbRecord.id, 2); + expect(dbRecord.version, 1); + expect(dbRecord.dirty, false); + expect(dbRecord.syncId, '1'); + expect(dbRecord.syncVersion, 'ver'); + + await autoProvider.delete(dbRecord.id!); + dbRecord = (await autoProvider.get(dbRecord.id!))!; + expect(dbRecord.id, 2); + expect(dbRecord.version, 2); + expect(dbRecord.dirty, true); + expect(dbRecord.deleted, true); + expect(dbRecord.syncId, '1'); + expect(dbRecord.syncVersion, 'ver'); + + // put again + dbRecord = await autoProvider.put(dbRecord, syncing: true); + expect(dbRecord.id, 2); + expect(dbRecord.version, 3); + expect(dbRecord.dirty, false); + expect(dbRecord.deleted, false); + expect(dbRecord.syncId, '1'); + expect(dbRecord.syncVersion, 'ver'); + + await autoProvider.delete(dbRecord.id!, syncing: true); + expect(await autoProvider.get(dbRecord.id!), isNull); + + // put again + dbRecord = await autoProvider.put(dbRecord, syncing: true); + expect(dbRecord.id, 2); + expect(dbRecord.version, 1); + expect(dbRecord.dirty, false); + expect(dbRecord.syncId, '1'); + expect(dbRecord.syncVersion, 'ver'); + + // delete in transaction + var txn = autoProvider.storeTransaction(true) + as DbRecordProviderWriteTransaction; + await autoProvider.txnDelete(txn, dbRecord.id!); + await txn.completed; + dbRecord = (await autoProvider.get(dbRecord.id!))!; + expect(dbRecord.id, 2); + expect(dbRecord.version, 2); + expect(dbRecord.dirty, true); + expect(dbRecord.deleted, true); + expect(dbRecord.syncId, '1'); + expect(dbRecord.syncVersion, 'ver'); + + txn = autoProvider.storeTransaction(true) + as DbRecordProviderWriteTransaction; + await autoProvider.txnDelete(txn, dbRecord.id!, syncing: true); + await txn.completed; + expect(await autoProvider.get(dbRecord.id!), isNull); + }); + }); + }); +} diff --git a/packages/idb_provider/test/test_common.dart b/packages/idb_provider/test/test_common.dart new file mode 100644 index 0000000..e9d9a5f --- /dev/null +++ b/packages/idb_provider/test/test_common.dart @@ -0,0 +1,28 @@ +library tekartik_idb_provider.test.test_common; + +import 'package:idb_shim/idb_client.dart'; +import 'package:idb_shim/idb_client_memory.dart'; +import 'package:idb_shim/idb_client_sembast.dart'; +import 'package:sembast/sembast.dart' as sdb; + +export 'dart:async'; + +export 'package:idb_shim/idb_client_memory.dart'; +export 'package:idb_shim/src/common/common_meta.dart'; +export 'package:test/test.dart'; + +class TestContext { + static var _id = 0; + late IdbFactory factory; + + String get dbName => 'test-${++_id}.db'; +} + +class SembastTestContext extends TestContext { + late sdb.DatabaseFactory sdbFactory; + + @override + IdbFactorySembast get factory => super.factory as IdbFactorySembast; +} + +TestContext idbMemoryContext = SembastTestContext()..factory = idbFactoryMemory; diff --git a/packages/idb_provider/test/test_provider.dart b/packages/idb_provider/test/test_provider.dart new file mode 100644 index 0000000..9ededd1 --- /dev/null +++ b/packages/idb_provider/test/test_provider.dart @@ -0,0 +1,112 @@ +library test_provider; + +import 'dart:async'; +import 'package:idb_shim/idb_client.dart'; +import 'package:tekartik_idb_provider/provider.dart'; + +int dbVersion = 1; +String databaseName = 'tekartik_app.test.app_provider'; + +String itemsStore = 'items'; +String nameIndex = 'name'; +String nameField = 'name'; + +// String NAME_FIELD = 'name'; + +class TestProvider extends Provider { + TestProvider(IdbFactory? idbFactory) { + init(idbFactory, databaseName, dbVersion); + } + @override + void onUpdateDatabase(VersionChangeEvent e) { + if (e.oldVersion < dbVersion) { + // delete stuff + } + var objectStore = + database!.createObjectStore(itemsStore, autoIncrement: true); + objectStore.createIndex(nameIndex, nameField, unique: false); + } + + @override + Future delete() { + return idbFactory!.deleteDatabase(databaseName); + } + + Future count() { + var trans = RawProviderStoreTransaction(this, itemsStore); + return trans.count().then((int count) { + return trans.completed.then((_) { + return count; + }); + }); + } + + Future> getNames({int? limit, int? offset}) { + var trans = RawProviderStoreTransaction(this, itemsStore); + final names = []; + return trans + .openCursor(limit: limit, offset: offset) + .listen((CursorWithValue cwv) { + names.add((cwv.value as Map)[nameField] as String?); + }) + .asFuture() + .then((_) { + return names; + }); + } + + Future> getOrderedNames({int? limit, int? offset}) { + var trans = + ProviderIndexTransaction(this, itemsStore, nameIndex); + + final names = []; + return trans + .openCursor(limit: limit, offset: offset) + .listen((CursorWithValue cwv) { + names.add((cwv.value as Map)[nameField] as String?); + }) + .asFuture() + .then((_) { + return trans.completed; + }) + .then((_) { + return names; + }); + } + + Future putName(String name) { + var trans = RawProviderStoreTransaction(this, itemsStore, true); + + final data = {}; + data[nameField] = name; + + return trans.add(data).then((key) { + return trans.completed.then((_) { + return key as int; + }); + }); + } + + // null if not found + Future getName(int key) { + var trans = RawProviderStoreTransaction(this, itemsStore); + + return trans.get(key).then((var data) { + return trans.completed.then((_) { + if (data == null) { + return null; + } + return (data as Map)[nameField] as String; + }); + }); + } + + Future get(int key) { + var trans = RawProviderStoreTransaction(this, itemsStore); + return trans.store!.get(key).then((var key) { + return trans.completed.then((_) { + return key as int; + }); + }); + } +} diff --git a/packages/idb_provider/test/test_runner.dart b/packages/idb_provider/test/test_runner.dart new file mode 100644 index 0000000..040c409 --- /dev/null +++ b/packages/idb_provider/test/test_runner.dart @@ -0,0 +1,20 @@ +import 'dynamic_provider_test.dart' as dynamic_provider_test; +import 'provider_meta_test.dart' as provider_meta_test; +import 'provider_test.dart' as provider_test; +import 'provider_transaction_test.dart' as provider_transaction_test; +import 'record_provider_test.dart' as record_provider_test; +import 'synced_record_provider_test.dart' as synced_record_provider_test; +import 'test_common.dart'; + +void main() { + testMain(idbMemoryContext); +} + +void testMain(TestContext context) { + provider_test.testMain(context); + dynamic_provider_test.testMain(context); + provider_meta_test.testMain(context); + provider_transaction_test.testMain(context); + record_provider_test.testMain(context); + synced_record_provider_test.testMain(context); +} diff --git a/packages/idb_provider/test/test_runner_browser.html b/packages/idb_provider/test/test_runner_browser.html new file mode 100644 index 0000000..240417a --- /dev/null +++ b/packages/idb_provider/test/test_runner_browser.html @@ -0,0 +1,16 @@ + + + + + + + provider_test + + + +

+ + + + + diff --git a/packages/idb_provider/test/test_runner_browser_test.dart b/packages/idb_provider/test/test_runner_browser_test.dart new file mode 100644 index 0000000..bfaf881 --- /dev/null +++ b/packages/idb_provider/test/test_runner_browser_test.dart @@ -0,0 +1,19 @@ +@TestOn('browser') +library tekartik_idb_provider.test.test_runner_browser_test; + +import 'package:idb_shim/idb_browser.dart'; + +import 'test_common.dart'; +import 'test_runner.dart' as test_runner; + +class BrowserContext extends TestContext { + BrowserContext() { + factory = idbFactoryBrowser; + } +} + +BrowserContext idbBrowserContext = BrowserContext(); + +void main() { + test_runner.testMain(idbBrowserContext); +} diff --git a/packages/idb_provider/test/test_runner_io_test.dart b/packages/idb_provider/test/test_runner_io_test.dart new file mode 100644 index 0000000..5135300 --- /dev/null +++ b/packages/idb_provider/test/test_runner_io_test.dart @@ -0,0 +1,9 @@ +@TestOn('vm') +library; + +import 'io_test_common.dart'; +import 'test_runner.dart' as all_common; + +void main() { + all_common.testMain(idbIoContext); +} diff --git a/packages/idb_provider/test/test_runner_memory_test.dart b/packages/idb_provider/test/test_runner_memory_test.dart new file mode 100644 index 0000000..3084a1c --- /dev/null +++ b/packages/idb_provider/test/test_runner_memory_test.dart @@ -0,0 +1,6 @@ +import 'test_common.dart'; +import 'test_runner.dart' as all_common; + +void main() { + all_common.testMain(idbMemoryContext); +} diff --git a/packages/idb_provider/tool/run_ci.dart b/packages/idb_provider/tool/run_ci.dart new file mode 100644 index 0000000..5fa390f --- /dev/null +++ b/packages/idb_provider/tool/run_ci.dart @@ -0,0 +1,5 @@ +import 'package:dev_build/package.dart'; + +Future main() async { + await packageRunCi('.'); +} diff --git a/packages/sembast_web_html_compat/README.md b/packages/sembast_web_html_compat/README.md index 8b55e73..a51e6d7 100644 --- a/packages/sembast_web_html_compat/README.md +++ b/packages/sembast_web_html_compat/README.md @@ -1,39 +1,15 @@ - +# Dependencies -TODO: Put a short description of the package here that helps potential users -know whether this package might be useful for them. - -## Features - -TODO: List what your package can do. Maybe include images, gifs, or videos. - -## Getting started - -TODO: List prerequisites and provide or point to information on how to -start using the package. - -## Usage - -TODO: Include short and useful examples for package users. Add longer examples -to `/example` folder. - -```dart -const like = 'sample'; +```yaml +dependencies: + sembast_web_html_compat: + git: + url: https://github.com/tekartik/idb_shim_more.dart + path: packages/sembast_web_html_compat + ref: dart3a + version: '>=0.3.3' ``` - -## Additional information - -TODO: Tell users more about the package: where to find more information, how to -contribute to the package, how to file issues, what response they can expect -from the package authors, and more. diff --git a/packages/sembast_web_html_compat/pubspec.yaml b/packages/sembast_web_html_compat/pubspec.yaml index 03e0a78..496a4df 100644 --- a/packages/sembast_web_html_compat/pubspec.yaml +++ b/packages/sembast_web_html_compat/pubspec.yaml @@ -16,7 +16,7 @@ dependencies: git: url: https://github.com/tekartik/sembast.dart path: sembast_test - ref: dart2_3 + ref: dart3a idb_shim_html_compat: git: url: https://github.com/tekartik/idb_shim_more.dart