Skip to content

Commit ea1a95d

Browse files
authored
eng(supabase): expose provider-specific methods on provider #574 (#576)
1 parent 37e6c67 commit ea1a95d

File tree

8 files changed

+283
-224
lines changed

8 files changed

+283
-224
lines changed

packages/brick_offline_first_with_supabase/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## 2.1.0
2+
3+
- Use `SupabaseProvider#subscribeToRealtime` to generate the channel used by `OfflineFirstWithSupabaseRepository#subscribeToRealtime`
4+
- **Breaking Change** protected method `OfflineFirstWithSupabaseRepository#queryToPostgresChangeFilter` has been moved to `SupabaseProvider#queryToPostgresChangeFilter`. Implementations should override this method in `SupabaseProvider` instead.
5+
16
## 2.0.0
27

38
- Dart minimum SDK is updated to `3.4.0`

packages/brick_offline_first_with_supabase/lib/src/offline_first_with_supabase_repository.dart

Lines changed: 69 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -204,30 +204,6 @@ abstract class OfflineFirstWithSupabaseRepository<
204204
);
205205
}
206206

207-
/// Convert a query to a [PostgresChangeFilter] for use with [subscribeToRealtime].
208-
@protected
209-
@visibleForTesting
210-
@visibleForOverriding
211-
PostgresChangeFilter? queryToPostgresChangeFilter<TModel extends TRepositoryModel>(
212-
Query query,
213-
) {
214-
final adapter = remoteProvider.modelDictionary.adapterFor[TModel]!;
215-
if (query.where?.isEmpty ?? true) return null;
216-
final condition = query.where!.first;
217-
final column = adapter.fieldsToSupabaseColumns[condition.evaluatedField]?.columnName;
218-
219-
if (column == null) return null;
220-
221-
final type = _compareToFilterParam(condition.compare);
222-
if (type == null) return null;
223-
224-
return PostgresChangeFilter(
225-
type: type,
226-
column: column,
227-
value: condition.value,
228-
);
229-
}
230-
231207
@override
232208
Future<void> reset() async {
233209
await super.reset();
@@ -264,8 +240,7 @@ abstract class OfflineFirstWithSupabaseRepository<
264240
///
265241
/// [query] is an optional query to filter the data. The query **must be** one level -
266242
/// `Query.where('user', Query.exact('name', 'Tom'))` is invalid but `Query.where('name', 'Tom')`
267-
/// is valid. The [Compare] operator is limited to a [PostgresChangeFilterType] equivalent.
268-
/// See [_compareToFilterParam] for a precise breakdown.
243+
/// is valid. The [Compare] operator is limited to a [PostgresChangeFilterType] equivalent. See [SupabaseProvider.queryToPostgresChangeFilter] for more details.
269244
Stream<List<TModel>> subscribeToRealtime<TModel extends TRepositoryModel>({
270245
PostgresChangeEvent eventType = PostgresChangeEvent.all,
271246
OfflineFirstGetPolicy policy = OfflineFirstGetPolicy.alwaysHydrate,
@@ -284,80 +259,76 @@ abstract class OfflineFirstWithSupabaseRepository<
284259
return subscribe<TModel>(policy: policy, query: query);
285260
}
286261

287-
final channel = remoteProvider.client
288-
.channel(adapter.supabaseTableName)
289-
.onPostgresChanges(
290-
event: eventType,
291-
schema: schema,
292-
table: adapter.supabaseTableName,
293-
filter: queryToPostgresChangeFilter<TModel>(query),
294-
callback: (payload) async {
295-
switch (payload.eventType) {
296-
// This code path is likely never hit; `PostgresChangeEvent.all` is used
297-
// to listen to changes but as far as can be determined is not delivered within
298-
// the payload of the callback.
299-
//
300-
// It's handled just in case this behavior changes.
301-
case PostgresChangeEvent.all:
302-
final localResults = await sqliteProvider.get<TModel>(repository: this);
303-
final remoteResults =
304-
await get<TModel>(query: query, policy: OfflineFirstGetPolicy.awaitRemote);
305-
final toDelete = localResults.where((r) => !remoteResults.contains(r));
306-
307-
for (final deletableModel in toDelete) {
308-
await sqliteProvider.delete<TModel>(deletableModel, repository: this);
309-
memoryCacheProvider.delete<TModel>(deletableModel, repository: this);
310-
}
311-
312-
case PostgresChangeEvent.delete:
313-
final query = queryFromSupabaseDeletePayload(
314-
payload.oldRecord,
315-
supabaseDefinitions: adapter.fieldsToSupabaseColumns,
316-
);
317-
318-
if (query.where?.isEmpty ?? true) return;
319-
320-
final results = await get<TModel>(
321-
query: query,
322-
policy: OfflineFirstGetPolicy.localOnly,
323-
seedOnly: true,
324-
);
325-
if (results.isEmpty) return;
326-
327-
await sqliteProvider.delete<TModel>(results.first, repository: this);
328-
memoryCacheProvider.delete<TModel>(results.first, repository: this);
329-
330-
case PostgresChangeEvent.insert || PostgresChangeEvent.update:
331-
// The supabase payload is not configurable and will not supply associations.
332-
// For models that have associations, an additional network call must be
333-
// made to retrieve all scoped data.
334-
final modelHasAssociations = adapter.fieldsToSupabaseColumns.entries
335-
.any((entry) => entry.value.association && !entry.value.associationIsNullable);
336-
337-
if (modelHasAssociations) {
338-
await get<TModel>(
339-
query: query,
340-
policy: OfflineFirstGetPolicy.alwaysHydrate,
341-
seedOnly: true,
342-
);
343-
344-
return;
345-
}
346-
347-
final instance = await adapter.fromSupabase(
348-
payload.newRecord,
349-
provider: remoteProvider,
350-
repository: this,
351-
);
352-
353-
await sqliteProvider.upsert<TModel>(instance as TModel, repository: this);
354-
memoryCacheProvider.upsert<TModel>(instance, repository: this);
262+
final channel = remoteProvider.subscribeToRealtime<TModel>(
263+
eventType: eventType,
264+
query: query,
265+
schema: schema,
266+
callback: (payload) async {
267+
switch (payload.eventType) {
268+
// This code path is likely never hit; `PostgresChangeEvent.all` is used
269+
// to listen to changes but as far as can be determined is not delivered within
270+
// the payload of the callback.
271+
//
272+
// It's handled just in case this behavior changes.
273+
case PostgresChangeEvent.all:
274+
final localResults = await sqliteProvider.get<TModel>(repository: this);
275+
final remoteResults =
276+
await get<TModel>(query: query, policy: OfflineFirstGetPolicy.awaitRemote);
277+
final toDelete = localResults.where((r) => !remoteResults.contains(r));
278+
279+
for (final deletableModel in toDelete) {
280+
await sqliteProvider.delete<TModel>(deletableModel, repository: this);
281+
memoryCacheProvider.delete<TModel>(deletableModel, repository: this);
355282
}
356283

357-
await notifySubscriptionsWithLocalData<TModel>();
358-
},
359-
)
360-
.subscribe();
284+
case PostgresChangeEvent.delete:
285+
final query = queryFromSupabaseDeletePayload(
286+
payload.oldRecord,
287+
supabaseDefinitions: adapter.fieldsToSupabaseColumns,
288+
);
289+
290+
if (query.where?.isEmpty ?? true) return;
291+
292+
final results = await get<TModel>(
293+
query: query,
294+
policy: OfflineFirstGetPolicy.localOnly,
295+
seedOnly: true,
296+
);
297+
if (results.isEmpty) return;
298+
299+
await sqliteProvider.delete<TModel>(results.first, repository: this);
300+
memoryCacheProvider.delete<TModel>(results.first, repository: this);
301+
302+
case PostgresChangeEvent.insert || PostgresChangeEvent.update:
303+
// The supabase payload is not configurable and will not supply associations.
304+
// For models that have associations, an additional network call must be
305+
// made to retrieve all scoped data.
306+
final modelHasAssociations = adapter.fieldsToSupabaseColumns.entries
307+
.any((entry) => entry.value.association && !entry.value.associationIsNullable);
308+
309+
if (modelHasAssociations) {
310+
await get<TModel>(
311+
query: query,
312+
policy: OfflineFirstGetPolicy.alwaysHydrate,
313+
seedOnly: true,
314+
);
315+
316+
return;
317+
}
318+
319+
final instance = await adapter.fromSupabase(
320+
payload.newRecord,
321+
provider: remoteProvider,
322+
repository: this,
323+
);
324+
325+
await sqliteProvider.upsert<TModel>(instance as TModel, repository: this);
326+
memoryCacheProvider.upsert<TModel>(instance, repository: this);
327+
}
328+
329+
await notifySubscriptionsWithLocalData<TModel>();
330+
},
331+
);
361332

362333
final controller = StreamController<List<TModel>>(
363334
onCancel: () async {
@@ -410,29 +381,6 @@ abstract class OfflineFirstWithSupabaseRepository<
410381
return instance;
411382
}
412383

413-
PostgresChangeFilterType? _compareToFilterParam(Compare compare) {
414-
switch (compare) {
415-
case Compare.exact:
416-
return PostgresChangeFilterType.eq;
417-
case Compare.contains:
418-
return PostgresChangeFilterType.inFilter;
419-
case Compare.greaterThan:
420-
return PostgresChangeFilterType.gt;
421-
case Compare.greaterThanOrEqualTo:
422-
return PostgresChangeFilterType.gte;
423-
case Compare.lessThan:
424-
return PostgresChangeFilterType.lt;
425-
case Compare.lessThanOrEqualTo:
426-
return PostgresChangeFilterType.lte;
427-
case Compare.notEqual:
428-
return PostgresChangeFilterType.neq;
429-
case Compare.between:
430-
return null;
431-
case Compare.doesNotContain:
432-
return null;
433-
}
434-
}
435-
436384
/// This is a convenience method to create the basic offline client and queue.
437385
/// The client is used to add offline capabilities to [SupabaseProvider];
438386
/// the queue is used to add offline to the repository.

packages/brick_offline_first_with_supabase/pubspec.yaml

Lines changed: 2 additions & 2 deletions
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: 2.0.0
8+
version: 2.1.0
99

1010
environment:
1111
sdk: ">=3.4.0 <4.0.0"
@@ -15,7 +15,7 @@ dependencies:
1515
brick_offline_first: ">=4.0.0 <5.0.0"
1616
brick_offline_first_with_rest: ">=4.0.0 <5.0.0"
1717
brick_sqlite: ">=4.0.0 <5.0.0"
18-
brick_supabase: ">=2.0.0 <3.0.0"
18+
brick_supabase: ">=2.1.0 <3.0.0"
1919
http: ">=1.0.0 <2.0.0"
2020
logging: ">=1.0.0 <2.0.0"
2121
meta: ">=1.3.0 <2.0.0"

packages/brick_offline_first_with_supabase/test/offline_first_with_supabase_repository_test.dart

Lines changed: 0 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -211,106 +211,6 @@ void main() {
211211
});
212212
});
213213

214-
group('#queryToPostgresChangeFilter', () {
215-
group('returns null', () {
216-
test('for complex queries', () {
217-
final query = Query.where('pizza', const Where.exact('id', 2));
218-
expect(repository.queryToPostgresChangeFilter<Customer>(query), isNull);
219-
});
220-
221-
test('for empty queries', () {
222-
const query = Query();
223-
expect(repository.queryToPostgresChangeFilter<Customer>(query), isNull);
224-
});
225-
226-
test('for missing columns', () {
227-
final query = Query.where('unknown', 1);
228-
expect(repository.queryToPostgresChangeFilter<Customer>(query), isNull);
229-
});
230-
});
231-
232-
group('Compare', () {
233-
test('.between', () {
234-
final query = Query(where: [const Where('firstName').isBetween(1, 2)]);
235-
final filter = repository.queryToPostgresChangeFilter<Customer>(query);
236-
expect(filter, isNull);
237-
});
238-
239-
test('.doesNotContain', () {
240-
final query = Query(where: [const Where('firstName').doesNotContain('Thomas')]);
241-
final filter = repository.queryToPostgresChangeFilter<Customer>(query);
242-
expect(filter, isNull);
243-
});
244-
245-
test('.exact', () {
246-
const query = Query(where: [Where.exact('firstName', 'Thomas')]);
247-
final filter = repository.queryToPostgresChangeFilter<Customer>(query);
248-
249-
expect(filter!.type, PostgresChangeFilterType.eq);
250-
expect(filter.column, 'first_name');
251-
expect(filter.value, 'Thomas');
252-
});
253-
254-
test('.greaterThan', () {
255-
final query = Query(where: [const Where('firstName').isGreaterThan('Thomas')]);
256-
final filter = repository.queryToPostgresChangeFilter<Customer>(query);
257-
258-
expect(filter!.type, PostgresChangeFilterType.gt);
259-
expect(filter.column, 'first_name');
260-
expect(filter.value, 'Thomas');
261-
});
262-
263-
test('.greaterThanOrEqualTo', () {
264-
final query = Query(
265-
where: [const Where('firstName').isGreaterThanOrEqualTo('Thomas')],
266-
);
267-
final filter = repository.queryToPostgresChangeFilter<Customer>(query);
268-
269-
expect(filter!.type, PostgresChangeFilterType.gte);
270-
expect(filter.column, 'first_name');
271-
expect(filter.value, 'Thomas');
272-
});
273-
274-
test('.lessThan', () {
275-
final query = Query(where: [const Where('firstName').isLessThan('Thomas')]);
276-
final filter = repository.queryToPostgresChangeFilter<Customer>(query);
277-
278-
expect(filter!.type, PostgresChangeFilterType.lt);
279-
expect(filter.column, 'first_name');
280-
expect(filter.value, 'Thomas');
281-
});
282-
283-
test('.lessThanOrEqualTo', () {
284-
final query = Query(
285-
where: [const Where('firstName').isLessThanOrEqualTo('Thomas')],
286-
);
287-
final filter = repository.queryToPostgresChangeFilter<Customer>(query);
288-
289-
expect(filter!.type, PostgresChangeFilterType.lte);
290-
expect(filter.column, 'first_name');
291-
expect(filter.value, 'Thomas');
292-
});
293-
294-
test('.notEqual', () {
295-
final query = Query(where: [const Where('firstName').isNot('Thomas')]);
296-
final filter = repository.queryToPostgresChangeFilter<Customer>(query);
297-
298-
expect(filter!.type, PostgresChangeFilterType.neq);
299-
expect(filter.column, 'first_name');
300-
expect(filter.value, 'Thomas');
301-
});
302-
303-
test('.contains', () {
304-
final query = Query(where: [const Where('firstName').contains('Thomas')]);
305-
final filter = repository.queryToPostgresChangeFilter<Customer>(query);
306-
307-
expect(filter!.type, PostgresChangeFilterType.inFilter);
308-
expect(filter.column, 'first_name');
309-
expect(filter.value, 'Thomas');
310-
});
311-
});
312-
});
313-
314214
group('#supabaseRealtimeSubscriptions', () {
315215
test('adds controller and query to #supabaseRealtimeSubscriptions', () async {
316216
expect(repository.supabaseRealtimeSubscriptions, hasLength(0));

packages/brick_supabase/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## 2.1.0
2+
3+
- Add `SupabaseProvider#subscribeToRealtime` to subscribe to [Supabase channels](https://supabase.com/docs/guides/realtime?queryGroups=language&language=dart).
4+
- Add `SupabaseProvider#queryToPostgresChangeFilter` to convert `Query`s for Supabase subscriptions
5+
16
## 2.0.0
27

38
- **BREAKING CHANGE** `Query(providerArgs:)` is no longer supported; see [1.2.0](#1.2.0) for migration steps

0 commit comments

Comments
 (0)