diff --git a/packages/brick_offline_first/CHANGELOG.md b/packages/brick_offline_first/CHANGELOG.md index 811ffc64..a71b01e2 100644 --- a/packages/brick_offline_first/CHANGELOG.md +++ b/packages/brick_offline_first/CHANGELOG.md @@ -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 diff --git a/packages/brick_offline_first/pubspec.yaml b/packages/brick_offline_first/pubspec.yaml index ffb36231..bf93246d 100644 --- a/packages/brick_offline_first/pubspec.yaml +++ b/packages/brick_offline_first/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: 3.3.0 +version: 3.4.0 environment: sdk: ">=2.18.0 <4.0.0" diff --git a/packages/brick_supabase/CHANGELOG.md b/packages/brick_supabase/CHANGELOG.md index dfa1e6ee..f0426783 100644 --- a/packages/brick_supabase/CHANGELOG.md +++ b/packages/brick_supabase/CHANGELOG.md @@ -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 diff --git a/packages/brick_supabase/lib/src/query_supabase_transformer.dart b/packages/brick_supabase/lib/src/query_supabase_transformer.dart index 2568d77f..7bc0e7e4 100644 --- a/packages/brick_supabase/lib/src/query_supabase_transformer.dart +++ b/packages/brick_supabase/lib/src/query_supabase_transformer.dart @@ -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, ]); diff --git a/packages/brick_supabase/lib/src/supabase_provider.dart b/packages/brick_supabase/lib/src/supabase_provider.dart index c18066b3..c7d6315b 100644 --- a/packages/brick_supabase/lib/src/supabase_provider.dart +++ b/packages/brick_supabase/lib/src/supabase_provider.dart @@ -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 { final SupabaseClient client; @@ -77,6 +83,36 @@ class SupabaseProvider implements Provider { ); } + /// In almost all cases, use [upsert]. This method is provided for cases when a table's + /// policy permits inserts without updates. + Future insert(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 update(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. @@ -101,6 +137,7 @@ class SupabaseProvider implements Provider { @protected Future upsertByType( Map serializedInstance, { + UpsertMethod method = UpsertMethod.upsert, required Type type, Query? query, ModelRepository? repository, @@ -112,10 +149,20 @@ class SupabaseProvider implements Provider { 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]); @@ -136,6 +183,7 @@ class SupabaseProvider implements Provider { @protected Future recursiveAssociationUpsert( Map serializedInstance, { + UpsertMethod method = UpsertMethod.upsert, required Type type, Query? query, ModelRepository? repository, @@ -157,6 +205,7 @@ class SupabaseProvider implements Provider { await recursiveAssociationUpsert( Map.from(serializedInstance[association.columnName]), + method: method, type: association.associationType!, query: query, repository: repository, @@ -166,6 +215,7 @@ class SupabaseProvider implements Provider { return await upsertByType( serializedInstance, + method: method, type: type, query: query, repository: repository, diff --git a/packages/brick_supabase/pubspec.yaml b/packages/brick_supabase/pubspec.yaml index 25ca58d8..0cf40b58 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.1.3 +version: 1.2.0 environment: sdk: ">=3.0.0 <4.0.0" diff --git a/packages/brick_supabase/test/query_supabase_transformer_test.dart b/packages/brick_supabase/test/query_supabase_transformer_test.dart index 7ef5fd0d..776f53dc 100644 --- a/packages/brick_supabase/test/query_supabase_transformer_test.dart +++ b/packages/brick_supabase/test/query_supabase_transformer_test.dart @@ -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', ); }); }); diff --git a/packages/brick_supabase/test/supabase_provider_test.dart b/packages/brick_supabase/test/supabase_provider_test.dart index ea5f1d84..5707f2f8 100644 --- a/packages/brick_supabase/test/supabase_provider_test.dart +++ b/packages/brick_supabase/test/supabase_provider_test.dart @@ -60,6 +60,40 @@ void main() { expect(retrieved[1].age, 2); }); + test('#insert', () async { + final req = SupabaseRequest( + 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(instance); + expect(inserted.id, instance.id); + expect(inserted.age, instance.age); + expect(inserted.name, instance.name); + }); + + test('#update', () async { + final req = SupabaseRequest( + 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(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(