Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

eng: add update and insert to SupabaseProvider #486

Merged
merged 2 commits into from
Dec 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/brick_offline_first/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
## Unreleased

## 3.4.0

- Change `OfflineFirstRepository#exists` behavior: the check against memory cache will only return `true` if results have been found, otherwise it will continue to the SQLite provider
- Forward errors from `OfflineFirstRepository#subscribe` streams to their callers (@sonbs21 #484)

## 3.3.0

Expand Down
2 changes: 1 addition & 1 deletion packages/brick_offline_first/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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: 3.3.0
version: 3.4.0

environment:
sdk: ">=2.18.0 <4.0.0"
Expand Down
5 changes: 5 additions & 0 deletions packages/brick_supabase/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
## Unreleased

## 1.2.0

- Add `SupabaseProvider#update` and `SupabaseProvider#insert` to conform to Supabase policy restrictions
- Use `columnName` instead of `evaluatedField` in `QuerySupabaseTransformer` when searching for non null associations

## 1.1.3

- Add `query:` to `@Supabase` to override the generated query at runtime
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ class QuerySupabaseTransformer<_Model extends SupabaseModel> {
condition.value as WhereCondition, associationAdapter, newLeadingAssociations, [
if (!definition.associationIsNullable)
{
condition.evaluatedField: 'not.is.null',
definition.columnName: 'not.is.null',
},
...associationConditions,
]);
Expand Down
58 changes: 54 additions & 4 deletions packages/brick_supabase/lib/src/supabase_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ import 'package:logging/logging.dart';
import 'package:meta/meta.dart';
import 'package:supabase/supabase.dart';

enum UpsertMethod {
insert,
update,
upsert,
}

/// Retrieves from an HTTP endpoint
class SupabaseProvider implements Provider<SupabaseModel> {
final SupabaseClient client;
Expand Down Expand Up @@ -77,6 +83,36 @@ class SupabaseProvider implements Provider<SupabaseModel> {
);
}

/// In almost all cases, use [upsert]. This method is provided for cases when a table's
/// policy permits inserts without updates.
Future<TModel> insert<TModel extends SupabaseModel>(instance, {query, repository}) async {
final adapter = modelDictionary.adapterFor[TModel]!;
final output = await adapter.toSupabase(instance, provider: this, repository: repository);

return await recursiveAssociationUpsert(
output,
method: UpsertMethod.insert,
type: TModel,
query: query,
repository: repository,
) as TModel;
}

/// In almost all cases, use [upsert]. This method is provided for cases when a table's
/// policy permits updates without inserts.
Future<TModel> update<TModel extends SupabaseModel>(instance, {query, repository}) async {
final adapter = modelDictionary.adapterFor[TModel]!;
final output = await adapter.toSupabase(instance, provider: this, repository: repository);

return await recursiveAssociationUpsert(
output,
method: UpsertMethod.update,
type: TModel,
query: query,
repository: repository,
) as TModel;
}

/// Association models are upserted recursively before the requested instance is upserted.
/// Because it's unknown if there has been any change from the local association to the remote
/// association, all associations and their associations are upserted on a parent's upsert.
Expand All @@ -101,6 +137,7 @@ class SupabaseProvider implements Provider<SupabaseModel> {
@protected
Future<SupabaseModel> upsertByType(
Map<String, dynamic> serializedInstance, {
UpsertMethod method = UpsertMethod.upsert,
required Type type,
Query? query,
ModelRepository<SupabaseModel>? repository,
Expand All @@ -112,10 +149,20 @@ class SupabaseProvider implements Provider<SupabaseModel> {
final queryTransformer =
QuerySupabaseTransformer(adapter: adapter, modelDictionary: modelDictionary, query: query);

final builder = adapter.uniqueFields.fold(
client
.from(adapter.supabaseTableName)
.upsert(serializedInstance, onConflict: adapter.onConflict), (acc, uniqueFieldName) {
final builderFilter = () {
switch (method) {
case UpsertMethod.insert:
return client.from(adapter.supabaseTableName).insert(serializedInstance);
case UpsertMethod.update:
return client.from(adapter.supabaseTableName).update(serializedInstance);
case UpsertMethod.upsert:
return client
.from(adapter.supabaseTableName)
.upsert(serializedInstance, onConflict: adapter.onConflict);
}
}();

final builder = adapter.uniqueFields.fold(builderFilter, (acc, uniqueFieldName) {
final columnName = adapter.fieldsToSupabaseColumns[uniqueFieldName]!.columnName;
if (serializedInstance.containsKey(columnName)) {
return acc.eq(columnName, serializedInstance[columnName]);
Expand All @@ -136,6 +183,7 @@ class SupabaseProvider implements Provider<SupabaseModel> {
@protected
Future<SupabaseModel> recursiveAssociationUpsert(
Map<String, dynamic> serializedInstance, {
UpsertMethod method = UpsertMethod.upsert,
required Type type,
Query? query,
ModelRepository<SupabaseModel>? repository,
Expand All @@ -157,6 +205,7 @@ class SupabaseProvider implements Provider<SupabaseModel> {

await recursiveAssociationUpsert(
Map<String, dynamic>.from(serializedInstance[association.columnName]),
method: method,
type: association.associationType!,
query: query,
repository: repository,
Expand All @@ -166,6 +215,7 @@ class SupabaseProvider implements Provider<SupabaseModel> {

return await upsertByType(
serializedInstance,
method: method,
type: type,
query: query,
repository: repository,
Expand Down
2 changes: 1 addition & 1 deletion packages/brick_supabase/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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.1.3
version: 1.2.0

environment:
sdk: ">=3.0.0 <4.0.0"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ void main() {

expect(
select.query,
'select=id,name,assoc_id:demos!assoc_id(id,name,custom_age),assocs:demos(id,name,custom_age)&demos.name=eq.Thomas&assoc=not.is.null',
'select=id,name,assoc_id:demos!assoc_id(id,name,custom_age),assocs:demos(id,name,custom_age)&demos.name=eq.Thomas&assoc_id=not.is.null',
);
});
});
Expand Down
34 changes: 34 additions & 0 deletions packages/brick_supabase/test/supabase_provider_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,40 @@ void main() {
expect(retrieved[1].age, 2);
});

test('#insert', () async {
final req = SupabaseRequest<Demo>(
requestMethod: 'POST',
filter: 'id=eq.1',
limit: 1,
);
final instance = Demo(age: 1, name: 'Demo 1', id: '1');
final resp = SupabaseResponse(await mock.serialize(instance));
mock.handle({req: resp});

final provider = SupabaseProvider(mock.client, modelDictionary: supabaseModelDictionary);
final inserted = await provider.insert<Demo>(instance);
expect(inserted.id, instance.id);
expect(inserted.age, instance.age);
expect(inserted.name, instance.name);
});

test('#update', () async {
final req = SupabaseRequest<Demo>(
requestMethod: 'PATCH',
filter: 'id=eq.1',
limit: 1,
);
final instance = Demo(age: 1, name: 'Demo 1', id: '1');
final resp = SupabaseResponse(await mock.serialize(instance));
mock.handle({req: resp});

final provider = SupabaseProvider(mock.client, modelDictionary: supabaseModelDictionary);
final inserted = await provider.update<Demo>(instance);
expect(inserted.id, instance.id);
expect(inserted.age, instance.age);
expect(inserted.name, instance.name);
});

group('#upsert', () {
test('no associations', () async {
final req = SupabaseRequest<Demo>(
Expand Down