Skip to content

Commit e473bcd

Browse files
authored
fix(supabase): refetch associations for realtime subscriptions (#514) (#517)
1 parent fe0c05b commit e473bcd

9 files changed

+145
-87
lines changed

packages/brick_offline_first_with_supabase/CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
## Unreleased
22

3+
## 1.3.0
4+
5+
- If a model requested in a realtime subscription has an association, an extra fetch is performed (#514)
6+
37
## 1.2.0
48

59
- Upgrade `brick_core` to `1.3.0`

packages/brick_offline_first_with_supabase/lib/src/offline_first_with_supabase_repository.dart

+16
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,22 @@ abstract class OfflineFirstWithSupabaseRepository<
318318
memoryCacheProvider.delete<TModel>(results.first, repository: this);
319319

320320
case PostgresChangeEvent.insert || PostgresChangeEvent.update:
321+
// The supabase payload is not configurable and will not supply associations.
322+
// For models that have associations, an additional network call must be
323+
// made to retrieve all scoped data.
324+
final modelHasAssociations = adapter.fieldsToSupabaseColumns.entries
325+
.any((entry) => entry.value.association && !entry.value.associationIsNullable);
326+
327+
if (modelHasAssociations) {
328+
await get<TModel>(
329+
query: query,
330+
policy: OfflineFirstGetPolicy.alwaysHydrate,
331+
seedOnly: true,
332+
);
333+
334+
return;
335+
}
336+
321337
final instance = await adapter.fromSupabase(
322338
payload.newRecord,
323339
provider: remoteProvider,

packages/brick_offline_first_with_supabase/pubspec.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ homepage: https://github.com/GetDutchie/brick/tree/main/packages/brick_offline_f
55
issue_tracker: https://github.com/GetDutchie/brick/issues
66
repository: https://github.com/GetDutchie/brick
77

8-
version: 1.2.0
8+
version: 1.3.0
99

1010
environment:
1111
sdk: ">=3.0.0 <4.0.0"

packages/brick_offline_first_with_supabase/test/__mocks__.dart

+9
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,15 @@ class Pizza extends OfflineFirstWithSupabaseModel {
7474
required this.toppings,
7575
required this.frozen,
7676
});
77+
78+
@override
79+
int get hashCode => id.hashCode ^ frozen.hashCode;
80+
81+
@override
82+
bool operator ==(Object other) => other is Pizza && other.id == id && other.frozen == frozen;
83+
84+
@override
85+
String toString() => 'Pizza(id: $id, toppings: $toppings, frozen: $frozen)';
7786
}
7887

7988
enum Topping { olive, pepperoni }

packages/brick_offline_first_with_supabase/test/offline_first_with_supabase_repository_test.dart

+95-78
Original file line numberDiff line numberDiff line change
@@ -433,20 +433,26 @@ void main() async {
433433
customers,
434434
emitsInOrder([
435435
[],
436-
[],
436+
[customer],
437437
[customer],
438438
]),
439439
);
440440

441-
const req = SupabaseRequest<Customer>();
442-
final resp = SupabaseResponse(
443-
await mock.serialize(
444-
customer,
445-
realtimeEvent: PostgresChangeEvent.insert,
446-
repository: repository,
441+
mock.handle({
442+
const SupabaseRequest<Customer>(realtime: true): SupabaseResponse(
443+
await mock.serialize(
444+
customer,
445+
realtimeEvent: PostgresChangeEvent.insert,
446+
repository: repository,
447+
),
447448
),
448-
);
449-
mock.handle({req: resp});
449+
const SupabaseRequest<Customer>(): SupabaseResponse([
450+
await mock.serialize(
451+
customer,
452+
repository: repository,
453+
),
454+
]),
455+
});
450456

451457
// Wait for request to be handled
452458
await Future.delayed(const Duration(milliseconds: 200));
@@ -474,13 +480,12 @@ void main() async {
474480
expect(
475481
customers,
476482
emitsInOrder([
477-
[customer],
478483
[customer],
479484
[],
480485
]),
481486
);
482487

483-
const req = SupabaseRequest<Customer>();
488+
const req = SupabaseRequest<Customer>(realtime: true);
484489
final resp = SupabaseResponse(
485490
await mock.serialize(
486491
customer,
@@ -515,6 +520,22 @@ void main() async {
515520
],
516521
);
517522

523+
mock.handle({
524+
const SupabaseRequest<Customer>(realtime: true): SupabaseResponse(
525+
await mock.serialize(
526+
customer2,
527+
realtimeEvent: PostgresChangeEvent.update,
528+
repository: repository,
529+
),
530+
),
531+
const SupabaseRequest<Customer>(): SupabaseResponse([
532+
await mock.serialize(
533+
customer2,
534+
repository: repository,
535+
),
536+
]),
537+
});
538+
518539
final id =
519540
await repository.sqliteProvider.upsert<Customer>(customer1, repository: repository);
520541
expect(id, isNotNull);
@@ -524,21 +545,11 @@ void main() async {
524545
expect(
525546
customers,
526547
emitsInOrder([
527-
[customer1],
528548
[customer1],
529549
[customer2],
550+
[customer2],
530551
]),
531552
);
532-
533-
const req = SupabaseRequest<Customer>();
534-
final resp = SupabaseResponse(
535-
await mock.serialize(
536-
customer2,
537-
realtimeEvent: PostgresChangeEvent.update,
538-
repository: repository,
539-
),
540-
);
541-
mock.handle({req: resp});
542553
});
543554

544555
group('as .all and ', () {
@@ -556,26 +567,32 @@ void main() async {
556567
await repository.sqliteProvider.get<Customer>(repository: repository);
557568
expect(sqliteResults, isEmpty);
558569

570+
mock.handle({
571+
const SupabaseRequest<Customer>(realtime: true): SupabaseResponse(
572+
await mock.serialize(
573+
customer,
574+
realtimeEvent: PostgresChangeEvent.insert,
575+
repository: repository,
576+
),
577+
),
578+
const SupabaseRequest<Customer>(): SupabaseResponse([
579+
await mock.serialize(
580+
customer,
581+
repository: repository,
582+
),
583+
]),
584+
});
585+
559586
final customers = repository.subscribeToRealtime<Customer>();
560587
expect(
561588
customers,
562589
emitsInOrder([
563590
[],
564-
[],
591+
[customer],
565592
[customer],
566593
]),
567594
);
568595

569-
const req = SupabaseRequest<Customer>();
570-
final resp = SupabaseResponse(
571-
await mock.serialize(
572-
customer,
573-
realtimeEvent: PostgresChangeEvent.insert,
574-
repository: repository,
575-
),
576-
);
577-
mock.handle({req: resp});
578-
579596
// Wait for request to be handled
580597
await Future.delayed(const Duration(milliseconds: 100));
581598

@@ -601,13 +618,12 @@ void main() async {
601618
expect(
602619
customers,
603620
emitsInOrder([
604-
[customer],
605621
[customer],
606622
[],
607623
]),
608624
);
609625

610-
const req = SupabaseRequest<Customer>();
626+
const req = SupabaseRequest<Customer>(realtime: true);
611627
final resp = SupabaseResponse(
612628
await mock.serialize(
613629
customer,
@@ -650,70 +666,71 @@ void main() async {
650666
expect(
651667
customers,
652668
emitsInOrder([
653-
[customer1],
654669
[customer1],
655670
[customer2],
671+
[customer2],
656672
]),
657673
);
658674

659-
const req = SupabaseRequest<Customer>();
660-
final resp = SupabaseResponse(
661-
await mock.serialize(
662-
customer2,
663-
realtimeEvent: PostgresChangeEvent.update,
664-
repository: repository,
675+
mock.handle({
676+
const SupabaseRequest<Customer>(realtime: true): SupabaseResponse(
677+
await mock.serialize(
678+
customer2,
679+
realtimeEvent: PostgresChangeEvent.update,
680+
repository: repository,
681+
),
665682
),
666-
);
667-
mock.handle({req: resp});
683+
const SupabaseRequest<Customer>(): SupabaseResponse(
684+
[
685+
await mock.serialize(
686+
customer2,
687+
repository: repository,
688+
),
689+
],
690+
),
691+
});
668692
});
669693

670694
test('with multiple events', () async {
671-
final customer1 = Customer(
695+
final pizza1 = Pizza(
672696
id: 1,
673-
firstName: 'Thomas',
674-
lastName: 'Guy',
675-
pizzas: [
676-
Pizza(id: 2, toppings: [Topping.pepperoni], frozen: false),
677-
],
697+
toppings: [],
698+
frozen: false,
678699
);
679-
final customer2 = Customer(
700+
final pizza2 = Pizza(
680701
id: 1,
681-
firstName: 'Guy',
682-
lastName: 'Thomas',
683-
pizzas: [
684-
Pizza(id: 2, toppings: [Topping.pepperoni], frozen: false),
685-
],
702+
toppings: [],
703+
frozen: true,
686704
);
687705

688-
final customers = repository.subscribeToRealtime<Customer>();
706+
final pizzas = repository.subscribeToRealtime<Pizza>();
689707
expect(
690-
customers,
708+
pizzas,
691709
emitsInOrder([
692710
[],
693-
[],
694-
[customer1],
695-
[customer2],
711+
[pizza1],
712+
[pizza2],
696713
]),
697714
);
698715

699-
const req = SupabaseRequest<Customer>();
700-
final resp = SupabaseResponse(
701-
await mock.serialize(
702-
customer1,
703-
realtimeEvent: PostgresChangeEvent.insert,
704-
repository: repository,
705-
),
706-
realtimeSubsequentReplies: [
707-
SupabaseResponse(
708-
await mock.serialize(
709-
customer2,
710-
realtimeEvent: PostgresChangeEvent.update,
711-
repository: repository,
712-
),
716+
mock.handle({
717+
const SupabaseRequest<Pizza>(realtime: true): SupabaseResponse(
718+
await mock.serialize<Pizza>(
719+
pizza1,
720+
realtimeEvent: PostgresChangeEvent.insert,
721+
repository: repository,
713722
),
714-
],
715-
);
716-
mock.handle({req: resp});
723+
realtimeSubsequentReplies: [
724+
SupabaseResponse(
725+
await mock.serialize<Pizza>(
726+
pizza2,
727+
realtimeEvent: PostgresChangeEvent.update,
728+
repository: repository,
729+
),
730+
),
731+
],
732+
),
733+
});
717734
});
718735
});
719736
});

packages/brick_supabase/CHANGELOG.md

+5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
## Unreleased
22

3+
## 1.3.0
4+
5+
- 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]])`)
6+
- Associations are not serialized in the `SupabaseResponse`; only subscribed table data is provided
7+
38
## 1.2.0
49

510
- **DEPRECATION** `Query(providerArgs: {'limitReferencedTable':})` has been removed in favor of `Query(limitBy:)`

packages/brick_supabase/lib/src/testing/supabase_mock_server.dart

+6-5
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ class SupabaseMockServer {
105105
final realtimeFilter = requestJson['payload']['config']['postgres_changes'].first['filter'];
106106

107107
final matching = responses.entries
108-
.firstWhereOrNull((r) => realtimeFilter == null || realtimeFilter == r.key.filter);
108+
.firstWhereOrNull((r) => r.key.realtime && realtimeFilter == r.key.filter);
109109

110110
if (matching == null) return;
111111

@@ -148,6 +148,7 @@ class SupabaseMockServer {
148148
final url = request.uri.toString();
149149

150150
final matchingRequest = responses.entries.firstWhereOrNull((r) {
151+
if (r.key.realtime) return false;
151152
final matchesRequestMethod =
152153
r.key.requestMethod == request.method || r.key.requestMethod == null;
153154
final matchesPath = request.uri.path == r.key.toUri(modelDictionary).path;
@@ -213,10 +214,10 @@ class SupabaseMockServer {
213214
// Delete records from realtime are strictly unique/indexed fields;
214215
// uniqueness is not tracked by [RuntimeSupabaseColumnDefinition]
215216
// so filtering out associations is the closest simulation of an incomplete payload
216-
if (realtimeEvent == PostgresChangeEvent.delete) {
217-
for (final value in adapter.fieldsToSupabaseColumns.values) {
218-
if (value.association) serialized.remove(value.columnName);
219-
}
217+
//
218+
// Associations are not provided by insert/update either
219+
for (final value in adapter.fieldsToSupabaseColumns.values) {
220+
if (value.association) serialized.remove(value.columnName);
220221
}
221222

222223
return {

0 commit comments

Comments
 (0)