diff --git a/packages/brick_offline_first_with_supabase/CHANGELOG.md b/packages/brick_offline_first_with_supabase/CHANGELOG.md index d0bf080f..e0f23754 100644 --- a/packages/brick_offline_first_with_supabase/CHANGELOG.md +++ b/packages/brick_offline_first_with_supabase/CHANGELOG.md @@ -1,5 +1,9 @@ ## Unreleased +## 1.3.0 + +- If a model requested in a realtime subscription has an association, an extra fetch is performed (#514) + ## 1.2.0 - Upgrade `brick_core` to `1.3.0` 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 860417b2..1d81df8d 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 @@ -318,6 +318,22 @@ abstract class OfflineFirstWithSupabaseRepository< memoryCacheProvider.delete(results.first, repository: this); case PostgresChangeEvent.insert || PostgresChangeEvent.update: + // The supabase payload is not configurable and will not supply associations. + // For models that have associations, an additional network call must be + // made to retrieve all scoped data. + final modelHasAssociations = adapter.fieldsToSupabaseColumns.entries + .any((entry) => entry.value.association && !entry.value.associationIsNullable); + + if (modelHasAssociations) { + await get( + query: query, + policy: OfflineFirstGetPolicy.alwaysHydrate, + seedOnly: true, + ); + + return; + } + final instance = await adapter.fromSupabase( payload.newRecord, provider: remoteProvider, diff --git a/packages/brick_offline_first_with_supabase/pubspec.yaml b/packages/brick_offline_first_with_supabase/pubspec.yaml index 08248487..f03a8a0e 100644 --- a/packages/brick_offline_first_with_supabase/pubspec.yaml +++ b/packages/brick_offline_first_with_supabase/pubspec.yaml @@ -5,7 +5,7 @@ homepage: https://github.com/GetDutchie/brick/tree/main/packages/brick_offline_f issue_tracker: https://github.com/GetDutchie/brick/issues repository: https://github.com/GetDutchie/brick -version: 1.2.0 +version: 1.3.0 environment: sdk: ">=3.0.0 <4.0.0" diff --git a/packages/brick_offline_first_with_supabase/test/__mocks__.dart b/packages/brick_offline_first_with_supabase/test/__mocks__.dart index 9f055904..97e63afc 100644 --- a/packages/brick_offline_first_with_supabase/test/__mocks__.dart +++ b/packages/brick_offline_first_with_supabase/test/__mocks__.dart @@ -74,6 +74,15 @@ class Pizza extends OfflineFirstWithSupabaseModel { required this.toppings, required this.frozen, }); + + @override + int get hashCode => id.hashCode ^ frozen.hashCode; + + @override + bool operator ==(Object other) => other is Pizza && other.id == id && other.frozen == frozen; + + @override + String toString() => 'Pizza(id: $id, toppings: $toppings, frozen: $frozen)'; } enum Topping { olive, pepperoni } diff --git a/packages/brick_offline_first_with_supabase/test/offline_first_with_supabase_repository_test.dart b/packages/brick_offline_first_with_supabase/test/offline_first_with_supabase_repository_test.dart index 3679b247..24405079 100644 --- a/packages/brick_offline_first_with_supabase/test/offline_first_with_supabase_repository_test.dart +++ b/packages/brick_offline_first_with_supabase/test/offline_first_with_supabase_repository_test.dart @@ -433,20 +433,26 @@ void main() async { customers, emitsInOrder([ [], - [], + [customer], [customer], ]), ); - const req = SupabaseRequest(); - final resp = SupabaseResponse( - await mock.serialize( - customer, - realtimeEvent: PostgresChangeEvent.insert, - repository: repository, + mock.handle({ + const SupabaseRequest(realtime: true): SupabaseResponse( + await mock.serialize( + customer, + realtimeEvent: PostgresChangeEvent.insert, + repository: repository, + ), ), - ); - mock.handle({req: resp}); + const SupabaseRequest(): SupabaseResponse([ + await mock.serialize( + customer, + repository: repository, + ), + ]), + }); // Wait for request to be handled await Future.delayed(const Duration(milliseconds: 200)); @@ -474,13 +480,12 @@ void main() async { expect( customers, emitsInOrder([ - [customer], [customer], [], ]), ); - const req = SupabaseRequest(); + const req = SupabaseRequest(realtime: true); final resp = SupabaseResponse( await mock.serialize( customer, @@ -515,6 +520,22 @@ void main() async { ], ); + mock.handle({ + const SupabaseRequest(realtime: true): SupabaseResponse( + await mock.serialize( + customer2, + realtimeEvent: PostgresChangeEvent.update, + repository: repository, + ), + ), + const SupabaseRequest(): SupabaseResponse([ + await mock.serialize( + customer2, + repository: repository, + ), + ]), + }); + final id = await repository.sqliteProvider.upsert(customer1, repository: repository); expect(id, isNotNull); @@ -524,21 +545,11 @@ void main() async { expect( customers, emitsInOrder([ - [customer1], [customer1], [customer2], + [customer2], ]), ); - - const req = SupabaseRequest(); - final resp = SupabaseResponse( - await mock.serialize( - customer2, - realtimeEvent: PostgresChangeEvent.update, - repository: repository, - ), - ); - mock.handle({req: resp}); }); group('as .all and ', () { @@ -556,26 +567,32 @@ void main() async { await repository.sqliteProvider.get(repository: repository); expect(sqliteResults, isEmpty); + mock.handle({ + const SupabaseRequest(realtime: true): SupabaseResponse( + await mock.serialize( + customer, + realtimeEvent: PostgresChangeEvent.insert, + repository: repository, + ), + ), + const SupabaseRequest(): SupabaseResponse([ + await mock.serialize( + customer, + repository: repository, + ), + ]), + }); + final customers = repository.subscribeToRealtime(); expect( customers, emitsInOrder([ [], - [], + [customer], [customer], ]), ); - const req = SupabaseRequest(); - final resp = SupabaseResponse( - await mock.serialize( - customer, - realtimeEvent: PostgresChangeEvent.insert, - repository: repository, - ), - ); - mock.handle({req: resp}); - // Wait for request to be handled await Future.delayed(const Duration(milliseconds: 100)); @@ -601,13 +618,12 @@ void main() async { expect( customers, emitsInOrder([ - [customer], [customer], [], ]), ); - const req = SupabaseRequest(); + const req = SupabaseRequest(realtime: true); final resp = SupabaseResponse( await mock.serialize( customer, @@ -650,70 +666,71 @@ void main() async { expect( customers, emitsInOrder([ - [customer1], [customer1], [customer2], + [customer2], ]), ); - const req = SupabaseRequest(); - final resp = SupabaseResponse( - await mock.serialize( - customer2, - realtimeEvent: PostgresChangeEvent.update, - repository: repository, + mock.handle({ + const SupabaseRequest(realtime: true): SupabaseResponse( + await mock.serialize( + customer2, + realtimeEvent: PostgresChangeEvent.update, + repository: repository, + ), ), - ); - mock.handle({req: resp}); + const SupabaseRequest(): SupabaseResponse( + [ + await mock.serialize( + customer2, + repository: repository, + ), + ], + ), + }); }); test('with multiple events', () async { - final customer1 = Customer( + final pizza1 = Pizza( id: 1, - firstName: 'Thomas', - lastName: 'Guy', - pizzas: [ - Pizza(id: 2, toppings: [Topping.pepperoni], frozen: false), - ], + toppings: [], + frozen: false, ); - final customer2 = Customer( + final pizza2 = Pizza( id: 1, - firstName: 'Guy', - lastName: 'Thomas', - pizzas: [ - Pizza(id: 2, toppings: [Topping.pepperoni], frozen: false), - ], + toppings: [], + frozen: true, ); - final customers = repository.subscribeToRealtime(); + final pizzas = repository.subscribeToRealtime(); expect( - customers, + pizzas, emitsInOrder([ [], - [], - [customer1], - [customer2], + [pizza1], + [pizza2], ]), ); - const req = SupabaseRequest(); - final resp = SupabaseResponse( - await mock.serialize( - customer1, - realtimeEvent: PostgresChangeEvent.insert, - repository: repository, - ), - realtimeSubsequentReplies: [ - SupabaseResponse( - await mock.serialize( - customer2, - realtimeEvent: PostgresChangeEvent.update, - repository: repository, - ), + mock.handle({ + const SupabaseRequest(realtime: true): SupabaseResponse( + await mock.serialize( + pizza1, + realtimeEvent: PostgresChangeEvent.insert, + repository: repository, ), - ], - ); - mock.handle({req: resp}); + realtimeSubsequentReplies: [ + SupabaseResponse( + await mock.serialize( + pizza2, + realtimeEvent: PostgresChangeEvent.update, + repository: repository, + ), + ), + ], + ), + }); }); }); }); diff --git a/packages/brick_supabase/CHANGELOG.md b/packages/brick_supabase/CHANGELOG.md index 24a1b50a..6aee21a3 100644 --- a/packages/brick_supabase/CHANGELOG.md +++ b/packages/brick_supabase/CHANGELOG.md @@ -1,5 +1,10 @@ ## Unreleased +## 1.3.0 + +- When testing realtime responses, `realtime: true` must be defined in `SupabaseRequest`. This also resolves a duplicate `emits` bug in tests; the most common resolution is to remove the first duplicated expected response (e.g. `emitsInOrder([[], [], [resp]])` becomes `emitsInOrder([[], [resp]])`) +- Associations are not serialized in the `SupabaseResponse`; only subscribed table data is provided + ## 1.2.0 - **DEPRECATION** `Query(providerArgs: {'limitReferencedTable':})` has been removed in favor of `Query(limitBy:)` diff --git a/packages/brick_supabase/lib/src/testing/supabase_mock_server.dart b/packages/brick_supabase/lib/src/testing/supabase_mock_server.dart index 6f66d957..ebef13bf 100644 --- a/packages/brick_supabase/lib/src/testing/supabase_mock_server.dart +++ b/packages/brick_supabase/lib/src/testing/supabase_mock_server.dart @@ -105,7 +105,7 @@ class SupabaseMockServer { final realtimeFilter = requestJson['payload']['config']['postgres_changes'].first['filter']; final matching = responses.entries - .firstWhereOrNull((r) => realtimeFilter == null || realtimeFilter == r.key.filter); + .firstWhereOrNull((r) => r.key.realtime && realtimeFilter == r.key.filter); if (matching == null) return; @@ -148,6 +148,7 @@ class SupabaseMockServer { final url = request.uri.toString(); final matchingRequest = responses.entries.firstWhereOrNull((r) { + if (r.key.realtime) return false; final matchesRequestMethod = r.key.requestMethod == request.method || r.key.requestMethod == null; final matchesPath = request.uri.path == r.key.toUri(modelDictionary).path; @@ -213,10 +214,10 @@ class SupabaseMockServer { // Delete records from realtime are strictly unique/indexed fields; // uniqueness is not tracked by [RuntimeSupabaseColumnDefinition] // so filtering out associations is the closest simulation of an incomplete payload - if (realtimeEvent == PostgresChangeEvent.delete) { - for (final value in adapter.fieldsToSupabaseColumns.values) { - if (value.association) serialized.remove(value.columnName); - } + // + // Associations are not provided by insert/update either + for (final value in adapter.fieldsToSupabaseColumns.values) { + if (value.association) serialized.remove(value.columnName); } return { diff --git a/packages/brick_supabase/lib/src/testing/supabase_request.dart b/packages/brick_supabase/lib/src/testing/supabase_request.dart index 036b4f16..3b8c692c 100644 --- a/packages/brick_supabase/lib/src/testing/supabase_request.dart +++ b/packages/brick_supabase/lib/src/testing/supabase_request.dart @@ -18,6 +18,9 @@ class SupabaseRequest { /// final int? limit; + /// + final bool realtime; + /// An HTTP request method, e.g. `GET`, `POST`, `PUT`, `DELETE` final String? requestMethod; @@ -34,6 +37,7 @@ class SupabaseRequest { this.fields, this.filter, this.limit, + this.realtime = false, this.requestMethod = 'GET', }); @@ -45,13 +49,15 @@ class SupabaseRequest { final generatedTableName = modelDictionary != null ? modelDictionary.adapterFor[TModel]?.supabaseTableName : tableName; + final prefix = realtime ? 'realtime' : 'rest'; + if (requestMethod == 'DELETE') { - final url = '/rest/v1/$generatedTableName${filter != null ? '?$filter&' : '?'}'; + final url = '/$prefix/v1/$generatedTableName${filter != null ? '?$filter&' : '?'}'; return Uri.parse(url); } final url = - '/rest/v1/$generatedTableName${filter != null ? '?$filter&' : '?'}select=${Uri.encodeComponent(generatedFields ?? '')}${limit != null ? '&limit=$limit' : ''}'; + '/$prefix/v1/$generatedTableName${filter != null ? '?$filter&' : '?'}select=${Uri.encodeComponent(generatedFields ?? '')}${limit != null ? '&limit=$limit' : ''}'; return Uri.parse(url); } diff --git a/packages/brick_supabase/pubspec.yaml b/packages/brick_supabase/pubspec.yaml index 82f06483..c9dd1245 100644 --- a/packages/brick_supabase/pubspec.yaml +++ b/packages/brick_supabase/pubspec.yaml @@ -4,7 +4,7 @@ homepage: https://github.com/GetDutchie/brick/tree/main/packages/brick_supabase issue_tracker: https://github.com/GetDutchie/brick/issues repository: https://github.com/GetDutchie/brick -version: 1.2.0 +version: 1.3.0 environment: sdk: ">=3.0.0 <4.0.0"