diff --git a/packages/brick_offline_first/lib/src/mixins/destructive_local_sync_from_remote_mixin.dart b/packages/brick_offline_first/lib/src/mixins/destructive_local_sync_from_remote_mixin.dart index 313f654e..9d7cf603 100644 --- a/packages/brick_offline_first/lib/src/mixins/destructive_local_sync_from_remote_mixin.dart +++ b/packages/brick_offline_first/lib/src/mixins/destructive_local_sync_from_remote_mixin.dart @@ -16,6 +16,7 @@ mixin DestructiveLocalSyncFromRemoteMixin /// are destroyed. Further, when `true`, all values from other parameters except [query] are ignored. @override Future> get({ + OfflineFirstGetPolicy? associationPolicy, bool forceLocalSyncFromRemote = false, OfflineFirstGetPolicy policy = OfflineFirstGetPolicy.awaitRemoteWhenNoneExist, Query? query, diff --git a/packages/brick_offline_first/lib/src/offline_first_repository.dart b/packages/brick_offline_first/lib/src/offline_first_repository.dart index 709eb766..5a900358 100644 --- a/packages/brick_offline_first/lib/src/offline_first_repository.dart +++ b/packages/brick_offline_first/lib/src/offline_first_repository.dart @@ -176,7 +176,9 @@ abstract class OfflineFirstRepository> get({ - OfflineFirstGetPolicy policy = OfflineFirstGetPolicy.awaitRemoteWhenNoneExist, + OfflineFirstGetPolicy? associationPolicy, + OfflineFirstGetPolicy policy = + OfflineFirstGetPolicy.awaitRemoteWhenNoneExist, Query? query, bool seedOnly = false, }) async { @@ -189,7 +191,7 @@ abstract class OfflineFirstRepository(query) && !requireRemote) { final memoryCacheResults = memoryCacheProvider.get(query: query, repository: this); @@ -244,6 +246,7 @@ abstract class OfflineFirstRepository> getBatched({ + OfflineFirstGetPolicy? associationPolicy, int batchSize = 50, OfflineFirstGetPolicy policy = OfflineFirstGetPolicy.awaitRemoteWhenNoneExist, Query? query, @@ -267,6 +270,7 @@ abstract class OfflineFirstRepository( query: recursiveQuery, policy: policy, + associationPolicy: associationPolicy, seedOnly: seedOnly, ); total.addAll(results); @@ -362,6 +366,7 @@ abstract class OfflineFirstRepository> subscribe({ + OfflineFirstGetPolicy? associationPolicy, OfflineFirstGetPolicy policy = OfflineFirstGetPolicy.localOnly, Query? query, }) { @@ -384,7 +389,10 @@ abstract class OfflineFirstRepository(query: query, policy: policy).then( + get( + query: query, + policy: policy, + associationPolicy: associationPolicy,).then( (results) { if (!controller.isClosed) controller.add(results); }, diff --git a/packages/brick_offline_first/test/offline_first/helpers/__mocks__.dart b/packages/brick_offline_first/test/offline_first/helpers/__mocks__.dart index 6410f1c3..9b9a6486 100644 --- a/packages/brick_offline_first/test/offline_first/helpers/__mocks__.dart +++ b/packages/brick_offline_first/test/offline_first/helpers/__mocks__.dart @@ -11,6 +11,8 @@ part 'horse.dart'; part 'horse_adapter.dart'; part 'mounty.dart'; part 'mounty_adapter.dart'; +part 'owner.dart'; +part 'owner_adapter.dart'; /// The exact same as [Mounty], except this class is tracked by the Memory Cache Provider /// while [Mounty] is not. @@ -40,6 +42,13 @@ class DemoModelMigration extends Migration { onDeleteCascade: true, ), InsertColumn('name', Column.varchar, onTable: 'Horse'), + InsertTable('Owner'), + InsertColumn('name', Column.varchar, onTable: 'Owner'), + InsertForeignKey( + 'Horse', + 'Owner', + foreignKeyColumn: '_brick_owner_id', + ), ], down: const [], ); @@ -94,6 +103,7 @@ final Map> testMappings = { Horse: HorseAdapter(), MemoryDemoModel: MountyAdapter(), Mounty: MountyAdapter(), + Owner: OwnerAdapter(), }; final testModelDictionary = TestModelDictionary(testMappings); @@ -102,5 +112,6 @@ final Map> sqliteMappings = { Horse: HorseAdapter(), MemoryDemoModel: MountyAdapter(), Mounty: MountyAdapter(), + Owner: OwnerAdapter(), }; final sqliteModelDictionary = SqliteModelDictionary(sqliteMappings); diff --git a/packages/brick_offline_first/test/offline_first/helpers/horse.dart b/packages/brick_offline_first/test/offline_first/helpers/horse.dart index 85da984e..3f8e3c12 100644 --- a/packages/brick_offline_first/test/offline_first/helpers/horse.dart +++ b/packages/brick_offline_first/test/offline_first/helpers/horse.dart @@ -5,8 +5,7 @@ class Horse extends OfflineFirstWithTestModel { final List mounties; - Horse({ - this.name, - this.mounties = const [], - }); + final Owner? owner; + + Horse({this.name, this.mounties = const [], this.owner}); } diff --git a/packages/brick_offline_first/test/offline_first/helpers/horse_adapter.dart b/packages/brick_offline_first/test/offline_first/helpers/horse_adapter.dart index 214bec40..a58b5554 100644 --- a/packages/brick_offline_first/test/offline_first/helpers/horse_adapter.dart +++ b/packages/brick_offline_first/test/offline_first/helpers/horse_adapter.dart @@ -7,12 +7,17 @@ Future _$HorseFromTest( }) async { return Horse( name: data['name'] as String?, - mounties: await Future.wait( + mounties: await Future.wait( data['mounties'] - ?.map((d) => MountyAdapter().fromTest(d, provider: provider, repository: repository)) + ?.map>((d) => MountyAdapter() + .fromTest(d, provider: provider, repository: repository),) .toList() ?? [], ), + owner: data['owner'] == null + ? null + : await OwnerAdapter().fromTest(data['owner'], + provider: provider, repository: repository,), ); } @@ -25,9 +30,14 @@ Future> _$HorseToTest( 'name': instance.name, 'mounties': await Future.wait>( instance.mounties - .map((s) => MountyAdapter().toTest(s, provider: provider, repository: repository)) + .map((s) => MountyAdapter() + .toTest(s, provider: provider, repository: repository),) .toList(), ), + 'owner': instance.owner == null + ? null + : await OwnerAdapter().toTest(instance.owner!, + provider: provider, repository: repository,), }; } @@ -36,6 +46,13 @@ Future _$HorseFromSqlite( required SqliteProvider provider, OfflineFirstWithTestRepository? repository, }) async { + final ownerId = data['_brick_owner_id']; + final owner = ownerId == null + ? null + : await repository!.getAssociation( + Query.where('primaryKey', ownerId, limit1: true), + ); + return Horse( name: data['name'] == null ? null : data['name'] as String?, mounties: (await provider.rawQuery( @@ -54,6 +71,7 @@ Future _$HorseFromSqlite( ); })) .toList(), + owner: owner?.firstOrNull, )..primaryKey = data['_brick_id'] as int; } @@ -62,7 +80,16 @@ Future> _$HorseToSqlite( required SqliteProvider provider, OfflineFirstWithTestRepository? repository, }) async { - return {'name': instance.name}; + return { + 'name': instance.name, + '_brick_owner_id': (instance.owner == null) + ? null + : instance.owner?.primaryKey ?? + await provider.upsert( + instance.owner!, + repository: repository, + ), + }; } /// Construct a [Horse] @@ -85,6 +112,11 @@ class HorseAdapter extends OfflineFirstWithTestAdapter { iterable: true, type: Mounty, ), + 'owner': const RuntimeSqliteColumnDefinition( + association: true, + columnName: '_brick_owner_id', + type: Owner, + ), }; @override Future primaryKeyByUniqueColumns(Horse instance, DatabaseExecutor executor) async => diff --git a/packages/brick_offline_first/test/offline_first/helpers/owner.dart b/packages/brick_offline_first/test/offline_first/helpers/owner.dart new file mode 100644 index 00000000..2a70a2b2 --- /dev/null +++ b/packages/brick_offline_first/test/offline_first/helpers/owner.dart @@ -0,0 +1,9 @@ +part of '__mocks__.dart'; + +class Owner extends OfflineFirstWithTestModel { + final String? name; + + Owner({ + this.name, + }); +} diff --git a/packages/brick_offline_first/test/offline_first/helpers/owner_adapter.dart b/packages/brick_offline_first/test/offline_first/helpers/owner_adapter.dart new file mode 100644 index 00000000..ac2b18d8 --- /dev/null +++ b/packages/brick_offline_first/test/offline_first/helpers/owner_adapter.dart @@ -0,0 +1,92 @@ +part of '__mocks__.dart'; + +Future _$OwnerFromTest( + Map data, { + required TestProvider provider, + OfflineFirstWithTestRepository? repository, +}) async { + return Owner( + name: data['name'] as String?, + ); +} + +Future> _$OwnerToTest( + Owner instance, { + required TestProvider provider, + OfflineFirstWithTestRepository? repository, +}) async { + return { + 'name': instance.name, + }; +} + +Future _$OwnerFromSqlite( + Map data, { + required SqliteProvider provider, + OfflineFirstWithTestRepository? repository, +}) async { + return Owner( + name: data['name'] == null ? null : data['name'] as String?, + )..primaryKey = data['_brick_id'] as int; +} + +Future> _$OwnerToSqlite( + Owner instance, { + required SqliteProvider provider, + OfflineFirstWithTestRepository? repository, +}) async { + return {'name': instance.name}; +} + +/// Construct a [Owner] +class OwnerAdapter extends OfflineFirstWithTestAdapter { + OwnerAdapter(); + + @override + final fieldsToSqliteColumns = { + 'primaryKey': const RuntimeSqliteColumnDefinition( + columnName: '_brick_id', + type: int, + ), + 'name': const RuntimeSqliteColumnDefinition( + columnName: 'name', + type: String, + ), + }; + @override + Future primaryKeyByUniqueColumns( + Owner instance, DatabaseExecutor executor,) async => + instance.primaryKey; + @override + final tableName = 'Owner'; + + @override + Future fromTest( + Map input, { + required TestProvider provider, + covariant OfflineFirstWithTestRepository? repository, + }) async => + await _$OwnerFromTest(input, provider: provider, repository: repository); + @override + Future> toTest( + Owner input, { + required TestProvider provider, + covariant OfflineFirstWithTestRepository? repository, + }) async => + await _$OwnerToTest(input, provider: provider, repository: repository); + @override + Future fromSqlite( + Map input, { + required SqliteProvider provider, + covariant OfflineFirstWithTestRepository? repository, + }) async => + await _$OwnerFromSqlite(input, + provider: provider, repository: repository,); + @override + Future> toSqlite( + Owner input, { + required SqliteProvider provider, + covariant OfflineFirstWithTestRepository? repository, + }) async => + await _$OwnerToSqlite(input, provider: provider, repository: repository); +} diff --git a/packages/brick_offline_first/test/offline_first/helpers/test_domain.dart b/packages/brick_offline_first/test/offline_first/helpers/test_domain.dart index a07a9280..458a23c1 100644 --- a/packages/brick_offline_first/test/offline_first/helpers/test_domain.dart +++ b/packages/brick_offline_first/test/offline_first/helpers/test_domain.dart @@ -37,7 +37,13 @@ class TestProvider extends Provider { methodsCalled.add('get'); final adapter = modelDictionary.adapterFor[T]!; final data = [ - {'name': 'SqliteName'}, + { + 'name': 'SqliteName', + 'mounties': [ + {'name': 'SqliteName'}, + ], + 'owner': { 'name': 'SqliteName'}, + } ]; final results = data .map((e) => adapter.fromTest(e, provider: this, repository: repository)) diff --git a/packages/brick_offline_first/test/offline_first/offline_first_repository_test.dart b/packages/brick_offline_first/test/offline_first/offline_first_repository_test.dart index a24a45b7..96d24308 100644 --- a/packages/brick_offline_first/test/offline_first/offline_first_repository_test.dart +++ b/packages/brick_offline_first/test/offline_first/offline_first_repository_test.dart @@ -101,7 +101,7 @@ void main() { // The TestProvider does not have unique keys, so Brick can't compare based on the name, // giving the appearance of two distinct records expect(fetchAgain, hasLength(2)); - expect((TestRepository().remoteProvider as TestProvider).methodsCalled, hasLength(2)); + expect((TestRepository().remoteProvider as TestProvider).methodsCalled, hasLength(5)); } finally { TestRepository().memoryCacheProvider.managedModelTypes.remove(Horse); } @@ -121,6 +121,20 @@ void main() { }); }); + test('associationPolicy overrides policy for associations', () async { + await TestRepository().get(policy: OfflineFirstGetPolicy.awaitRemote); + + expect((TestRepository().remoteProvider as TestProvider).methodsCalled, hasLength(2)); + + await TestRepository().get( + associationPolicy: OfflineFirstGetPolicy.localOnly, + policy: OfflineFirstGetPolicy.awaitRemote, + ); + + // Get is only invoked once if the associationPolicy is localOnly + expect((TestRepository().remoteProvider as TestProvider).methodsCalled, hasLength(3)); + }); + test( '#hydrateSqlite / #get requireRest:true', () async { diff --git a/packages/brick_offline_first_with_supabase/lib/src/offline_first_with_supabase_repository.dart b/packages/brick_offline_first_with_supabase/lib/src/offline_first_with_supabase_repository.dart index 1d81df8d..88e6d6b6 100644 --- a/packages/brick_offline_first_with_supabase/lib/src/offline_first_with_supabase_repository.dart +++ b/packages/brick_offline_first_with_supabase/lib/src/offline_first_with_supabase_repository.dart @@ -93,13 +93,16 @@ abstract class OfflineFirstWithSupabaseRepository< @override Future> get({ - OfflineFirstGetPolicy policy = OfflineFirstGetPolicy.awaitRemoteWhenNoneExist, + OfflineFirstGetPolicy? associationPolicy, + OfflineFirstGetPolicy policy = + OfflineFirstGetPolicy.awaitRemoteWhenNoneExist, Query? query, bool seedOnly = false, }) async { try { return await super.get( policy: policy, + associationPolicy: associationPolicy, query: query, seedOnly: seedOnly, ); @@ -257,6 +260,7 @@ abstract class OfflineFirstWithSupabaseRepository< /// is valid. The [Compare] operator is limited to a [PostgresChangeFilterType] equivalent. /// See [_compareToFilterParam] for a precise breakdown. Stream> subscribeToRealtime({ + OfflineFirstGetPolicy? associationPolicy, PostgresChangeEvent eventType = PostgresChangeEvent.all, OfflineFirstGetPolicy policy = OfflineFirstGetPolicy.alwaysHydrate, Query? query, @@ -291,7 +295,7 @@ abstract class OfflineFirstWithSupabaseRepository< case PostgresChangeEvent.all: final localResults = await sqliteProvider.get(repository: this); final remoteResults = - await get(query: query, policy: OfflineFirstGetPolicy.awaitRemote); + await get(query: query, policy: OfflineFirstGetPolicy.awaitRemote, associationPolicy: associationPolicy); final toDelete = localResults.where((r) => !remoteResults.contains(r)); for (final deletableModel in toDelete) { @@ -310,6 +314,7 @@ abstract class OfflineFirstWithSupabaseRepository< final results = await get( query: query, policy: OfflineFirstGetPolicy.localOnly, + associationPolicy: associationPolicy, seedOnly: true, ); if (results.isEmpty) return; @@ -328,6 +333,7 @@ abstract class OfflineFirstWithSupabaseRepository< await get( query: query, policy: OfflineFirstGetPolicy.alwaysHydrate, + associationPolicy: associationPolicy, seedOnly: true, ); @@ -370,7 +376,9 @@ abstract class OfflineFirstWithSupabaseRepository< // Fetch initial data // ignore: discarded_futures - get(query: query, policy: policy).then((results) { + get(query: query, + policy: policy, + associationPolicy: associationPolicy,).then((results) { if (!controller.isClosed) controller.add(results); });