From 234c5eb9e95ca041fc798fd0e9f0b63724b57735 Mon Sep 17 00:00:00 2001 From: Tim Shedor Date: Sun, 18 Aug 2024 23:27:48 -0700 Subject: [PATCH 1/7] eng(supabase): add supabase and offline first with supabase packages --- .../brick_offline_first_with_supabase.yaml | 18 +++ .github/workflows/brick_supabase.yaml | 18 +++ .../.gitignore | 1 + .../CHANGELOG.md | 5 + .../brick_offline_first_with_supabase/LICENSE | 21 +++ .../README.md | 30 ++++ .../analysis_options.yaml | 1 + .../brick_offline_first_with_supabase.dart | 3 + .../offline_first_with_supabase_adapter.dart | 9 ++ .../offline_first_with_supabase_model.dart | 4 + ...ffline_first_with_supabase_repository.dart | 30 ++++ .../pubspec.yaml | 27 ++++ packages/brick_supabase/CHANGELOG.md | 5 + packages/brick_supabase/LICENSE | 21 +++ packages/brick_supabase/README.md | 90 +++++++++++ packages/brick_supabase/analysis_options.yaml | 1 + .../brick_supabase/lib/brick_supabase.dart | 4 + .../lib/src/query_supabase_transformer.dart | 151 ++++++++++++++++++ .../lib/src/supabase_adapter.dart | 37 +++++ .../lib/src/supabase_model_dictionary.dart | 9 ++ .../lib/src/supabase_provider.dart | 97 +++++++++++ packages/brick_supabase/pubspec.yaml | 21 +++ 22 files changed, 603 insertions(+) create mode 100644 .github/workflows/brick_offline_first_with_supabase.yaml create mode 100644 .github/workflows/brick_supabase.yaml create mode 100644 packages/brick_offline_first_with_supabase/.gitignore create mode 100644 packages/brick_offline_first_with_supabase/CHANGELOG.md create mode 100644 packages/brick_offline_first_with_supabase/LICENSE create mode 100644 packages/brick_offline_first_with_supabase/README.md create mode 100644 packages/brick_offline_first_with_supabase/analysis_options.yaml create mode 100644 packages/brick_offline_first_with_supabase/lib/brick_offline_first_with_supabase.dart create mode 100644 packages/brick_offline_first_with_supabase/lib/src/offline_first_with_supabase_adapter.dart create mode 100644 packages/brick_offline_first_with_supabase/lib/src/offline_first_with_supabase_model.dart create mode 100644 packages/brick_offline_first_with_supabase/lib/src/offline_first_with_supabase_repository.dart create mode 100644 packages/brick_offline_first_with_supabase/pubspec.yaml create mode 100644 packages/brick_supabase/CHANGELOG.md create mode 100644 packages/brick_supabase/LICENSE create mode 100644 packages/brick_supabase/README.md create mode 100644 packages/brick_supabase/analysis_options.yaml create mode 100644 packages/brick_supabase/lib/brick_supabase.dart create mode 100644 packages/brick_supabase/lib/src/query_supabase_transformer.dart create mode 100644 packages/brick_supabase/lib/src/supabase_adapter.dart create mode 100644 packages/brick_supabase/lib/src/supabase_model_dictionary.dart create mode 100644 packages/brick_supabase/lib/src/supabase_provider.dart create mode 100644 packages/brick_supabase/pubspec.yaml diff --git a/.github/workflows/brick_offline_first_with_supabase.yaml b/.github/workflows/brick_offline_first_with_supabase.yaml new file mode 100644 index 00000000..181846ed --- /dev/null +++ b/.github/workflows/brick_offline_first_with_supabase.yaml @@ -0,0 +1,18 @@ +name: Brick Offline First with Supabase +on: + push: + branches: + - main + pull_request: + paths: + - "packages/brick_offline_first_with_supabase/**" + - ".github/workflows/brick_offline_first_with_supabase.yaml" + +env: + PUB_ENVIRONMENT: bot.github + +jobs: + analyze_format_test: + uses: ./.github/workflows/reusable-flutter-analyze-format-test.yaml + with: + package: brick_offline_first_with_supabase diff --git a/.github/workflows/brick_supabase.yaml b/.github/workflows/brick_supabase.yaml new file mode 100644 index 00000000..b6f4f6ff --- /dev/null +++ b/.github/workflows/brick_supabase.yaml @@ -0,0 +1,18 @@ +name: Brick Supabase +on: + push: + branches: + - main + pull_request: + paths: + - "packages/brick_supabase/**" + - ".github/workflows/brick_supabase.yaml" + +env: + PUB_ENVIRONMENT: bot.github + +jobs: + analyze_format_test: + uses: ./.github/workflows/reusable-dart-analyze-format-test.yaml + with: + package: brick_supabase diff --git a/packages/brick_offline_first_with_supabase/.gitignore b/packages/brick_offline_first_with_supabase/.gitignore new file mode 100644 index 00000000..79dcd336 --- /dev/null +++ b/packages/brick_offline_first_with_supabase/.gitignore @@ -0,0 +1 @@ +!example/**/*.g.dart diff --git a/packages/brick_offline_first_with_supabase/CHANGELOG.md b/packages/brick_offline_first_with_supabase/CHANGELOG.md new file mode 100644 index 00000000..b5de7397 --- /dev/null +++ b/packages/brick_offline_first_with_supabase/CHANGELOG.md @@ -0,0 +1,5 @@ +## Unreleased + +### 0.0.1 + +Initial diff --git a/packages/brick_offline_first_with_supabase/LICENSE b/packages/brick_offline_first_with_supabase/LICENSE new file mode 100644 index 00000000..3b662c6b --- /dev/null +++ b/packages/brick_offline_first_with_supabase/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) Green Bits, Inc. and its affiliates. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without supabaseriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/brick_offline_first_with_supabase/README.md b/packages/brick_offline_first_with_supabase/README.md new file mode 100644 index 00000000..626ad5ce --- /dev/null +++ b/packages/brick_offline_first_with_supabase/README.md @@ -0,0 +1,30 @@ +![brick_offline_first_with_supabase workflow](https://github.com/GetDutchie/brick/actions/workflows/brick_offline_first_with_supabase.yaml/badge.svg) + +`OfflineFirstWithSupabaseRepository` streamlines the Supabase integration with an `OfflineFirstRepository`. + +The `OfflineFirstWithSupabase` domain uses all the same configurations and annotations as `OfflineFirst`. + +## Models + +### ConnectOfflineFirstWithSupabase + +`@ConnectOfflineFirstWithSupabase` decorates the model that can be serialized by one or more providers. Offline First does not have configuration at the class level and only extends configuration held by its providers: + +```dart +@ConnectOfflineFirstWithSupabase( + supabaseConfig: SupabaseSerializable(), + sqliteConfig: SqliteSerializable(), +) +class MyModel extends OfflineFirstModel {} +``` + +### FAQ + +#### Why can't I declare a model argument? + +Due to [an open analyzer bug](https://github.com/dart-lang/sdk/issues/38309), a custom model cannot be passed to the repository as a type argument. + +## Unsupported Field Types + +- Any unsupported field types from `SupabaseProvider`, or `SqliteProvider` +- Future iterables of future models (i.e. `Future>>`). diff --git a/packages/brick_offline_first_with_supabase/analysis_options.yaml b/packages/brick_offline_first_with_supabase/analysis_options.yaml new file mode 100644 index 00000000..f04c6cf0 --- /dev/null +++ b/packages/brick_offline_first_with_supabase/analysis_options.yaml @@ -0,0 +1 @@ +include: ../../analysis_options.yaml diff --git a/packages/brick_offline_first_with_supabase/lib/brick_offline_first_with_supabase.dart b/packages/brick_offline_first_with_supabase/lib/brick_offline_first_with_supabase.dart new file mode 100644 index 00000000..3fce4164 --- /dev/null +++ b/packages/brick_offline_first_with_supabase/lib/brick_offline_first_with_supabase.dart @@ -0,0 +1,3 @@ +export 'package:brick_offline_first_with_supabase/src/offline_first_with_supabase_adapter.dart'; +export 'package:brick_offline_first_with_supabase/src/offline_first_with_supabase_model.dart'; +export 'package:brick_offline_first_with_supabase/src/offline_first_with_supabase_repository.dart'; diff --git a/packages/brick_offline_first_with_supabase/lib/src/offline_first_with_supabase_adapter.dart b/packages/brick_offline_first_with_supabase/lib/src/offline_first_with_supabase_adapter.dart new file mode 100644 index 00000000..5bc8ab0f --- /dev/null +++ b/packages/brick_offline_first_with_supabase/lib/src/offline_first_with_supabase_adapter.dart @@ -0,0 +1,9 @@ +import 'package:brick_offline_first/brick_offline_first.dart'; +import 'package:brick_offline_first_with_supabase/src/offline_first_with_supabase_model.dart'; +import 'package:brick_supabase/brick_supabase.dart'; + +/// This adapter fetches first from [SqliteProvider] then hydrates with [SupabaseProvider]. +abstract class OfflineFirstWithSupabaseAdapter<_Model extends OfflineFirstWithSupabaseModel> + extends OfflineFirstAdapter<_Model> with SupabaseAdapter<_Model> { + OfflineFirstWithSupabaseAdapter(); +} diff --git a/packages/brick_offline_first_with_supabase/lib/src/offline_first_with_supabase_model.dart b/packages/brick_offline_first_with_supabase/lib/src/offline_first_with_supabase_model.dart new file mode 100644 index 00000000..f0c5a8c2 --- /dev/null +++ b/packages/brick_offline_first_with_supabase/lib/src/offline_first_with_supabase_model.dart @@ -0,0 +1,4 @@ +import 'package:brick_offline_first/brick_offline_first.dart'; +import 'package:brick_supabase_abstract/brick_supabase_abstract.dart'; + +abstract class OfflineFirstWithSupabaseModel extends OfflineFirstModel with SupabaseModel {} 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 new file mode 100644 index 00000000..96791a95 --- /dev/null +++ b/packages/brick_offline_first_with_supabase/lib/src/offline_first_with_supabase_repository.dart @@ -0,0 +1,30 @@ +import 'package:brick_offline_first/brick_offline_first.dart'; +import 'package:brick_offline_first_with_supabase/src/offline_first_with_supabase_model.dart'; +import 'package:brick_supabase/brick_supabase.dart' show SupabaseProvider; + +/// Ensures the [remoteProvider] is a [SupabaseProvider]. +/// +/// OfflineFirstWithSupabaseRepository should accept a type argument such as +/// <_RepositoryModel extends OfflineFirstWithSupabaseModel>, however, this causes a type bound +/// error on runtime. The argument should be reintroduced with a future version of the +/// compiler/analyzer. +abstract class OfflineFirstWithSupabaseRepository + extends OfflineFirstRepository { + /// The type declaration is important here for the rare circumstances that + /// require interfacting with [SupabaseProvider]'s client directly. + @override + // ignore: overridden_fields + final SupabaseProvider remoteProvider; + + OfflineFirstWithSupabaseRepository({ + super.autoHydrate, + super.loggerName, + super.memoryCacheProvider, + required super.migrations, + required SupabaseProvider supabaseProvider, + required super.sqliteProvider, + }) : remoteProvider = supabaseProvider, + super( + remoteProvider: supabaseProvider, + ); +} diff --git a/packages/brick_offline_first_with_supabase/pubspec.yaml b/packages/brick_offline_first_with_supabase/pubspec.yaml new file mode 100644 index 00000000..8227c05f --- /dev/null +++ b/packages/brick_offline_first_with_supabase/pubspec.yaml @@ -0,0 +1,27 @@ +name: brick_offline_first_with_supabase +description: A Brick domain that routes data fetching through local providers + before a Supabase provider. +homepage: https://github.com/GetDutchie/brick/tree/main/packages/brick_offline_first_with_supabase +issue_tracker: https://github.com/GetDutchie/brick/issues +repository: https://github.com/GetDutchie/brick + +version: 0.0.1 + +environment: + sdk: ">=2.18.0 <4.0.0" + +dependencies: + brick_core: ^1.1.1 + brick_offline_first: ">=3.0.0 <4.0.0" + brick_supabase: ">=0.0.1 <2.0.0" + brick_supabase_abstract: ">=0.0.1 <2.0.0" + brick_sqlite: ">=3.0.0 <4.0.0" + logging: ">=1.0.0 <2.0.0" + meta: ">=1.3.0 <2.0.0" + sqflite_common: ">=2.0.0 <3.0.0" + +dev_dependencies: + lints: ^2.0.1 + mockito: ^5.0.0 + test: ^1.16.5 + sqflite_common_ffi: ^2.0.0 diff --git a/packages/brick_supabase/CHANGELOG.md b/packages/brick_supabase/CHANGELOG.md new file mode 100644 index 00000000..b5de7397 --- /dev/null +++ b/packages/brick_supabase/CHANGELOG.md @@ -0,0 +1,5 @@ +## Unreleased + +### 0.0.1 + +Initial diff --git a/packages/brick_supabase/LICENSE b/packages/brick_supabase/LICENSE new file mode 100644 index 00000000..3b662c6b --- /dev/null +++ b/packages/brick_supabase/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) Green Bits, Inc. and its affiliates. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without supabaseriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/brick_supabase/README.md b/packages/brick_supabase/README.md new file mode 100644 index 00000000..c797e2fc --- /dev/null +++ b/packages/brick_supabase/README.md @@ -0,0 +1,90 @@ +![brick_supabase workflow](https://github.com/GetDutchie/brick/actions/workflows/brick_supabase.yaml/badge.svg) + +# Supabase Provider + +Connecting [Brick](https://github.com/GetDutchie/brick) with Supabase. + +## Models + +### `@SupabaseSerializable(tableName:)` + +The Supabase table name must be specified to connect `from`, `upsert` and `delete` invocations: + +```dart +@SupabaseSerializable(tableName: 'users') +class User +``` + +## Fields + +### `@Supabase(unique:)` + +Connect Supabase's primary key (or any other index) to your application code. This is useful for `upsert` and `delete` logic when mutating instances. + +```dart +@Supabase(unique: true, name: 'id') +final int supabaseId; +``` + +### `@Supabase(foreignKey:)` + +Specify the foreign key to use on the table when fetching for a remote association. + +For example, given the `orders` table has a `customer_id` column that associates +the `customers` table, an `Order` class in Dart may look like: + +```dart +@SupabaseSerializeable(tableName: 'orders') +class Order { + @Supabase(foreignKey: 'customer_id') + final Customer customer; +} + +@SupabaseSerializeable(tableName: 'customers') +class Customer { + final int id; +} +``` + +### `@Supabase(enumAsString:)` + +Brick by default assumes enums from a REST API will be delivered as integers matching the index in the Flutter app. However, if your API delivers strings instead, the field can be easily annotated without writing a custom generator. + +Given the API: + +```json +{ "user": { "hats": ["bowler", "birthday"] } } +``` + +Simply convert `hats` into a Dart enum: + +```dart +enum Hat { baseball, bowler, birthday } + +... + +@Supabase(enumAsString: true) +final List hats; +``` + +### `@Supabase(name:)` + +REST keys can be renamed per field. This will override the default set by `SupabaseSerializable#fieldRename`. + +```dart +@Supabase( + name: "full_name" // "full_name" is used in from and to requests to REST instead of "last_name" +) +final String lastName; +``` + +### `@Supabase(ignoreFrom:)` and `@Supabase(ignoreTo:)` + +When true, the field will be ignored by the (de)serializing function in the adapter. + +## Unsupported Field Types + +The following are not serialized to REST. However, unsupported types can still be accessed in the model as non-final fields. + +- Nested `List<>` e.g. `>>` +- Many-to-many associations diff --git a/packages/brick_supabase/analysis_options.yaml b/packages/brick_supabase/analysis_options.yaml new file mode 100644 index 00000000..f04c6cf0 --- /dev/null +++ b/packages/brick_supabase/analysis_options.yaml @@ -0,0 +1 @@ +include: ../../analysis_options.yaml diff --git a/packages/brick_supabase/lib/brick_supabase.dart b/packages/brick_supabase/lib/brick_supabase.dart new file mode 100644 index 00000000..5747d516 --- /dev/null +++ b/packages/brick_supabase/lib/brick_supabase.dart @@ -0,0 +1,4 @@ +export 'package:brick_supabase/src/supabase_adapter.dart'; +export 'package:brick_supabase/src/supabase_model_dictionary.dart'; +export 'package:brick_supabase/src/supabase_provider.dart'; +export 'package:brick_supabase_abstract/brick_supabase_abstract.dart'; diff --git a/packages/brick_supabase/lib/src/query_supabase_transformer.dart b/packages/brick_supabase/lib/src/query_supabase_transformer.dart new file mode 100644 index 00000000..d6475c27 --- /dev/null +++ b/packages/brick_supabase/lib/src/query_supabase_transformer.dart @@ -0,0 +1,151 @@ +import 'package:brick_core/core.dart'; +import 'package:brick_supabase/src/supabase_adapter.dart'; +import 'package:brick_supabase/src/supabase_model_dictionary.dart'; +import 'package:brick_supabase_abstract/brick_supabase_abstract.dart' hide Supabase; +import 'package:supabase_flutter/supabase_flutter.dart'; + +/// Create a prepared SQLite statement for eventual execution. Only [statement] and [values] +/// should be accessed. +/// +/// Example (using SQLFlite): +/// ```dart +/// final sqliteQuery = QuerySqlTransformer(modelDictionary: dict, query: query); +/// final results = await (await db).rawQuery(sqliteQuery.statement, sqliteQuery.values); +/// ``` +class QuerySupabaseTransformer<_Model extends SupabaseModel> { + final SupabaseAdapter adapter; + final SupabaseModelDictionary modelDictionary; + + /// Must-haves for the [statement], mainly used to build clauses + final Query? query; + + /// [selectStatement] will output [statement] as a `SELECT FROM`. When false, the [statement] + /// output will be a `SELECT COUNT(*)`. Defaults `true`. + QuerySupabaseTransformer({ + required this.modelDictionary, + this.query, + }) : adapter = modelDictionary.adapterFor[_Model]!; + + String get selectQuery { + return _destructureAssociation(adapter.fieldsToSupabaseColumns.values).join(',\n '); + } + + PostgrestFilterBuilder>> select() { + final builder = Supabase.instance.client.from(adapter.tableName); + + return (query?.where ?? []).fold(builder.select(selectQuery), (acc, condition) { + final whereStatement = _expandCondition(condition); + for (final where in whereStatement) { + for (final entry in where.entries) { + final newUri = acc.appendSearchParams(entry.key, entry.value); + acc = acc.copyWithUrl(newUri); + } + } + return acc; + }); + } + + List _destructureAssociation(Iterable? columns) { + final selectedFields = []; + + if (columns == null) return selectedFields; + + for (final field in columns) { + if (field.association && field.associationType != null) { + var associationOutput = + '${field.columnName}:${modelDictionary.adapterFor[field.associationType!]?.tableName}'; + if (field.associationForeignKey != null) { + associationOutput += '!${field.associationForeignKey}'; + } + associationOutput += '('; + final fields = _destructureAssociation( + modelDictionary.adapterFor[field.associationType!]?.fieldsToSupabaseColumns.values, + ); + associationOutput += fields.join(',\n '); + associationOutput += ')'; + + selectedFields.add(associationOutput); + continue; + } + + selectedFields.add(field.columnName); + } + + return selectedFields; + } + + String _compareToSearchParam(Compare compare) { + switch (compare) { + case Compare.exact: + return 'eq'; + case Compare.contains: + return 'like'; + case Compare.doesNotContain: + return 'not.like'; + case Compare.greaterThan: + return 'gt'; + case Compare.greaterThanOrEqualTo: + return 'gte'; + case Compare.lessThan: + return 'lt'; + case Compare.lessThanOrEqualTo: + return 'lte'; + case Compare.between: + return 'adj'; + case Compare.notEqual: + return 'neq'; + } + } + + /// Recursively step through a `Where` or `WherePhrase` to ouput a condition for `WHERE`. + List> _expandCondition( + WhereCondition condition, [ + SupabaseAdapter? passedAdapter, + ]) { + passedAdapter ??= adapter; + + // Begin a separate where phrase + if (condition is WherePhrase) { + final conditions = condition.conditions + .map((c) => _expandCondition(c, passedAdapter)) + .expand((c) => c) + .toList(); + + if (condition.isRequired) { + return conditions; + } + + return [ + { + 'or': '(${conditions.map((c) => '${c.keys.first}.${c.values.first}').join(', ')})', + } + ]; + } + + if (!passedAdapter.fieldsToSupabaseColumns.containsKey(condition.evaluatedField)) { + throw ArgumentError( + 'Field ${condition.evaluatedField} on $_Model is not serialized by SQLite', + ); + } + + final definition = passedAdapter.fieldsToSupabaseColumns[condition.evaluatedField]!; + + if (definition.association) { + if (condition.value is! WhereCondition) { + throw ArgumentError( + 'Query value for association ${condition.evaluatedField} on $_Model must be a Where or WherePhrase', + ); + } + + final associationAdapter = modelDictionary.adapterFor[definition.associationType]!; + + return _expandCondition(condition.value as WhereCondition, associationAdapter); + } + + return [ + { + definition.columnName: '${_compareToSearchParam(condition.compare)}.${condition.value}', + } + ]; + } +} diff --git a/packages/brick_supabase/lib/src/supabase_adapter.dart b/packages/brick_supabase/lib/src/supabase_adapter.dart new file mode 100644 index 00000000..c75dd7ab --- /dev/null +++ b/packages/brick_supabase/lib/src/supabase_adapter.dart @@ -0,0 +1,37 @@ +import 'package:brick_core/core.dart'; +import 'package:brick_supabase/src/supabase_provider.dart'; +import 'package:brick_supabase_abstract/brick_supabase_abstract.dart'; + +/// Constructors that convert app models to and from Supabase +abstract class SupabaseAdapter implements Adapter { + /// Used for upserts; forwards to Supabase's `defaultToNull` + bool get defaultToNull; + + /// A dictionary that connects field names to Supabase columns. + Map get fieldsToSupabaseColumns; + + /// Used for upserts; forwards to Supabase's `ignoreDuplicates` + bool get ignoreDuplicates; + + /// Used for upserts; forwards to Supabase's `onConflict` + String? get onConflict; + + /// Declared by the [SupabaseSerializable] `tableName` property + String get tableName; + + /// Unique fields that map to Supabase columns (using [fieldsToSupabaseColumns]) + /// used to target upsert and delete operations. + Set get uniqueFields; + + TModel fromSupabase( + Map input, { + required SupabaseProvider provider, + ModelRepository? repository, + }); + + Future> toSupabase( + TModel input, { + required SupabaseProvider provider, + ModelRepository? repository, + }); +} diff --git a/packages/brick_supabase/lib/src/supabase_model_dictionary.dart b/packages/brick_supabase/lib/src/supabase_model_dictionary.dart new file mode 100644 index 00000000..f81f6d1c --- /dev/null +++ b/packages/brick_supabase/lib/src/supabase_model_dictionary.dart @@ -0,0 +1,9 @@ +import 'package:brick_core/core.dart'; +import 'package:brick_supabase/src/supabase_adapter.dart'; +import 'package:brick_supabase_abstract/brick_supabase_abstract.dart'; + +/// Associates app models with their [SupabaseAdapter] +class SupabaseModelDictionary + extends ModelDictionary> { + const SupabaseModelDictionary(super.adapterFor); +} diff --git a/packages/brick_supabase/lib/src/supabase_provider.dart b/packages/brick_supabase/lib/src/supabase_provider.dart new file mode 100644 index 00000000..5fe3fbae --- /dev/null +++ b/packages/brick_supabase/lib/src/supabase_provider.dart @@ -0,0 +1,97 @@ +import 'package:brick_core/core.dart'; +import 'package:brick_supabase/src/query_supabase_transformer.dart'; +import 'package:brick_supabase/src/supabase_model_dictionary.dart'; +import 'package:brick_supabase_abstract/brick_supabase_abstract.dart' hide Supabase; +import 'package:logging/logging.dart'; +import 'package:meta/meta.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; + +/// Retrieves from an HTTP endpoint +class SupabaseProvider implements Provider { + /// A fully-qualified URL + final String baseEndpoint; + + /// The glue between app models and generated adapters. + @override + final SupabaseModelDictionary modelDictionary; + + @protected + final Logger logger; + + SupabaseProvider( + this.baseEndpoint, { + required this.modelDictionary, + }) : logger = Logger('SupabaseProvider'); + + /// Sends a DELETE request method to the endpoint + @override + Future delete(instance, {query, repository}) async { + final adapter = modelDictionary.adapterFor[TModel]!; + final tableBuilder = Supabase.instance.client.from(adapter.tableName); + final output = await adapter.toSupabase(instance, provider: this, repository: repository); + + final queryTransformer = + QuerySupabaseTransformer(modelDictionary: modelDictionary, query: query); + + final builder = adapter.uniqueFields.fold(tableBuilder.delete(), (acc, uniqueFieldName) { + final columnName = adapter.fieldsToSupabaseColumns[uniqueFieldName]!.columnName; + if (output.containsKey(columnName)) { + return acc.eq(columnName, output[columnName]); + } + return acc; + }); + + final resp = await builder.select(queryTransformer.selectQuery).limit(1).maybeSingle(); + return resp == null; + } + + @override + Future exists({query, repository}) async { + final queryTransformer = + QuerySupabaseTransformer(modelDictionary: modelDictionary, query: query); + final builder = queryTransformer.select(); + + final resp = await builder.count(CountOption.exact); + return resp.count > 0; + } + + @override + Future> get({query, repository}) async { + final adapter = modelDictionary.adapterFor[TModel]!; + final queryTransformer = + QuerySupabaseTransformer(modelDictionary: modelDictionary, query: query); + final builder = queryTransformer.select(); + + final resp = await builder; + + return resp + .map((r) => adapter.fromSupabase(r, repository: repository, provider: this)) + .toList() + .cast(); + } + + @override + Future upsert(instance, {query, repository}) async { + final adapter = modelDictionary.adapterFor[TModel]!; + final tableBuilder = Supabase.instance.client.from(adapter.tableName); + final output = await adapter.toSupabase(instance, provider: this, repository: repository); + + final queryTransformer = + QuerySupabaseTransformer(modelDictionary: modelDictionary, query: query); + + final builder = adapter.uniqueFields.fold(tableBuilder.upsert(output), (acc, uniqueFieldName) { + final columnName = adapter.fieldsToSupabaseColumns[uniqueFieldName]!.columnName; + if (output.containsKey(columnName)) { + return acc.eq(columnName, output[columnName]); + } + return acc; + }); + final resp = await builder.select(queryTransformer.selectQuery).limit(1).maybeSingle(); + + if (resp == null) { + throw StateError('Upsert of $instance failed'); + } + + return adapter.fromSupabase(resp, repository: repository, provider: this) as TModel; + } +} diff --git a/packages/brick_supabase/pubspec.yaml b/packages/brick_supabase/pubspec.yaml new file mode 100644 index 00000000..af88000a --- /dev/null +++ b/packages/brick_supabase/pubspec.yaml @@ -0,0 +1,21 @@ +name: brick_supabase +description: Supabase connector for Brick, a data persistence library. Includes annotations, adapter, model, and provider. +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: 0.0.1 + +environment: + sdk: ">=2.18.0 <4.0.0" + +dependencies: + brick_core: ^1.1.1 + supabase_flutter: ">=2.6.0 <3.0.0" + logging: ">=1.0.0 <2.0.0" + meta: ">=1.3.0 <2.0.0" + brick_supabase_abstract: ">=0.0.1 <2.0.0" + +dev_dependencies: + lints: ^2.0.1 + test: ^1.16.5 From 28d52ec7edee608eea3435c3bbbdc29cf5a0ba46 Mon Sep 17 00:00:00 2001 From: Tim Shedor Date: Sun, 18 Aug 2024 23:36:56 -0700 Subject: [PATCH 2/7] update flutter image --- .github/workflows/reusable-flutter-analyze-format-test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/reusable-flutter-analyze-format-test.yaml b/.github/workflows/reusable-flutter-analyze-format-test.yaml index 8401344d..645952bf 100644 --- a/.github/workflows/reusable-flutter-analyze-format-test.yaml +++ b/.github/workflows/reusable-flutter-analyze-format-test.yaml @@ -15,7 +15,7 @@ jobs: flutter_analyze_format_test: runs-on: ubuntu-latest container: - image: cirrusci/flutter:${{ matrix.flutter_version }} + image: ghcr.io/cirruslabs/flutter:${{ matrix.flutter_version }} strategy: fail-fast: false matrix: From 90c6a22d649ed2ea196919480144939252c289d3 Mon Sep 17 00:00:00 2001 From: Tim Shedor Date: Sun, 18 Aug 2024 23:47:19 -0700 Subject: [PATCH 3/7] fix ci --- .../reusable-dart-analyze-format-test.yaml | 4 ++-- .../reusable-flutter-analyze-format-test.yaml | 15 ++++++++++----- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/.github/workflows/reusable-dart-analyze-format-test.yaml b/.github/workflows/reusable-dart-analyze-format-test.yaml index ad22a27a..81093e4b 100644 --- a/.github/workflows/reusable-dart-analyze-format-test.yaml +++ b/.github/workflows/reusable-dart-analyze-format-test.yaml @@ -29,8 +29,8 @@ jobs: key: ${{ runner.os }}-${{ inputs.package }}-v1-${{ matrix.sdk_version }}-${{ hashFiles(format('packages/{0}/pubspec.yaml', inputs.package)) }} restore-keys: | ${{ runner.os }}-${{ inputs.package }}-v1-${{ matrix.sdk_version }} - - id: checkout - uses: actions/checkout@v3 + + - uses: actions/checkout@v3 - run: dart pub get && dart run melos bootstrap --scope="${{ inputs.package }}" diff --git a/.github/workflows/reusable-flutter-analyze-format-test.yaml b/.github/workflows/reusable-flutter-analyze-format-test.yaml index 645952bf..040834ad 100644 --- a/.github/workflows/reusable-flutter-analyze-format-test.yaml +++ b/.github/workflows/reusable-flutter-analyze-format-test.yaml @@ -32,17 +32,22 @@ jobs: restore-keys: | ${{ runner.os }}-${{ inputs.package }}-v1-${{ matrix.flutter_version }} - - id: checkout - uses: actions/checkout@v3 + - uses: actions/checkout@v3 - run: flutter pub get working-directory: packages/${{ inputs.package }} - - run: dart analyze --fatal-infos lib test + - run: dart analyze --fatal-infos lib working-directory: packages/${{ inputs.package }} - - run: dart format --output=none --line-length 100 --set-exit-if-changed lib test + - run: if [ -d "test" ]; then dart analyze --fatal-infos test; fi working-directory: packages/${{ inputs.package }} - - run: flutter test + - run: dart format --output=none --line-length 100 --set-exit-if-changed lib + working-directory: packages/${{ inputs.package }} + + - run: if [ -d "test" ]; then dart format --output=none --line-length 100 --set-exit-if-changed test; fi + working-directory: packages/${{ inputs.package }} + + - run: if [ -d "test" ]; then flutter test; fi working-directory: packages/${{ inputs.package }} From 124afb335b76670223bd2884476ebbb191c0291e Mon Sep 17 00:00:00 2001 From: Tim Shedor Date: Sun, 18 Aug 2024 23:49:17 -0700 Subject: [PATCH 4/7] switch to flutter ci --- .github/workflows/brick_supabase.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/brick_supabase.yaml b/.github/workflows/brick_supabase.yaml index b6f4f6ff..45d040e4 100644 --- a/.github/workflows/brick_supabase.yaml +++ b/.github/workflows/brick_supabase.yaml @@ -13,6 +13,6 @@ env: jobs: analyze_format_test: - uses: ./.github/workflows/reusable-dart-analyze-format-test.yaml + uses: ./.github/workflows/reusable-flutter-analyze-format-test.yaml with: package: brick_supabase From f75c64589776f0592d605899820a546db2cd8bef Mon Sep 17 00:00:00 2001 From: Tim Shedor Date: Mon, 19 Aug 2024 00:11:58 -0700 Subject: [PATCH 5/7] remove abstract packages --- .../connect_offline_first_with_supabase.dart | 0 .../.gitignore | 1 - .../CHANGELOG.md | 5 - .../LICENSE | 21 -- .../README.md | 147 ------------- .../analysis_options.yaml | 1 - ..._offline_first_with_supabase_abstract.dart | 1 - .../pubspec.yaml | 19 -- .../brick_supabase/lib/brick_supabase.dart | 5 +- .../lib/src/annotations}/supabase.dart | 0 .../annotations}/supabase_serializable.dart | 0 .../lib/src/query_supabase_transformer.dart | 6 +- .../runtime_supabase_column_definition.dart | 0 .../lib/src/supabase_adapter.dart | 2 +- .../lib/src/supabase_model.dart | 0 .../lib/src/supabase_provider.dart | 16 +- packages/brick_supabase/pubspec.yaml | 4 +- packages/brick_supabase_abstract/CHANGELOG.md | 5 - packages/brick_supabase_abstract/LICENSE | 21 -- packages/brick_supabase_abstract/README.md | 202 ------------------ .../analysis_options.yaml | 1 - .../lib/brick_supabase_abstract.dart | 4 - packages/brick_supabase_abstract/pubspec.yaml | 17 -- 23 files changed, 19 insertions(+), 459 deletions(-) rename packages/{brick_offline_first_with_supabase_abstract/lib/src => brick_offline_first_with_supabase/lib/src/annotations}/connect_offline_first_with_supabase.dart (100%) delete mode 100644 packages/brick_offline_first_with_supabase_abstract/.gitignore delete mode 100644 packages/brick_offline_first_with_supabase_abstract/CHANGELOG.md delete mode 100644 packages/brick_offline_first_with_supabase_abstract/LICENSE delete mode 100644 packages/brick_offline_first_with_supabase_abstract/README.md delete mode 100644 packages/brick_offline_first_with_supabase_abstract/analysis_options.yaml delete mode 100644 packages/brick_offline_first_with_supabase_abstract/lib/brick_offline_first_with_supabase_abstract.dart delete mode 100644 packages/brick_offline_first_with_supabase_abstract/pubspec.yaml rename packages/{brick_supabase_abstract/lib/src => brick_supabase/lib/src/annotations}/supabase.dart (100%) rename packages/{brick_supabase_abstract/lib/src => brick_supabase/lib/src/annotations}/supabase_serializable.dart (100%) rename packages/{brick_supabase_abstract => brick_supabase}/lib/src/runtime_supabase_column_definition.dart (100%) rename packages/{brick_supabase_abstract => brick_supabase}/lib/src/supabase_model.dart (100%) delete mode 100644 packages/brick_supabase_abstract/CHANGELOG.md delete mode 100644 packages/brick_supabase_abstract/LICENSE delete mode 100644 packages/brick_supabase_abstract/README.md delete mode 100644 packages/brick_supabase_abstract/analysis_options.yaml delete mode 100644 packages/brick_supabase_abstract/lib/brick_supabase_abstract.dart delete mode 100644 packages/brick_supabase_abstract/pubspec.yaml diff --git a/packages/brick_offline_first_with_supabase_abstract/lib/src/connect_offline_first_with_supabase.dart b/packages/brick_offline_first_with_supabase/lib/src/annotations/connect_offline_first_with_supabase.dart similarity index 100% rename from packages/brick_offline_first_with_supabase_abstract/lib/src/connect_offline_first_with_supabase.dart rename to packages/brick_offline_first_with_supabase/lib/src/annotations/connect_offline_first_with_supabase.dart diff --git a/packages/brick_offline_first_with_supabase_abstract/.gitignore b/packages/brick_offline_first_with_supabase_abstract/.gitignore deleted file mode 100644 index 79dcd336..00000000 --- a/packages/brick_offline_first_with_supabase_abstract/.gitignore +++ /dev/null @@ -1 +0,0 @@ -!example/**/*.g.dart diff --git a/packages/brick_offline_first_with_supabase_abstract/CHANGELOG.md b/packages/brick_offline_first_with_supabase_abstract/CHANGELOG.md deleted file mode 100644 index b5de7397..00000000 --- a/packages/brick_offline_first_with_supabase_abstract/CHANGELOG.md +++ /dev/null @@ -1,5 +0,0 @@ -## Unreleased - -### 0.0.1 - -Initial diff --git a/packages/brick_offline_first_with_supabase_abstract/LICENSE b/packages/brick_offline_first_with_supabase_abstract/LICENSE deleted file mode 100644 index f8548186..00000000 --- a/packages/brick_offline_first_with_supabase_abstract/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) Green Bits, Inc. and its affiliates. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/packages/brick_offline_first_with_supabase_abstract/README.md b/packages/brick_offline_first_with_supabase_abstract/README.md deleted file mode 100644 index 96e4711c..00000000 --- a/packages/brick_offline_first_with_supabase_abstract/README.md +++ /dev/null @@ -1,147 +0,0 @@ -![brick_offline_first_with_rest workflow](https://github.com/GetDutchie/brick/actions/workflows/brick_offline_first_with_rest.yaml/badge.svg) - -`OfflineFirstWithRestRepository` streamlines the REST integration with an `OfflineFirstRepository`. A serial queue is included to track REST requests in a separate SQLite database, only removing requests when a response is returned from the host (i.e. the device has lost internet connectivity). See `OfflineFirstWithRest#reattemptForStatusCodes`. - -The `OfflineFirstWithRest` domain uses all the same configurations and annotations as `OfflineFirst`. - -## Models - -### ConnectOfflineFirstWithRest - -`@ConnectOfflineFirstWithRest` decorates the model that can be serialized by one or more providers. Offline First does not have configuration at the class level and only extends configuration held by its providers: - -```dart -@ConnectOfflineFirstWithRest( - restConfig: RestSerializable(), - sqliteConfig: SqliteSerializable(), -) -class MyModel extends OfflineFirstModel {} -``` - -## Generating Models from a REST Endpoint - -A utility class is provided to make model generation from a JSON API a snap. Given an endpoint, the converter will infer the type of a field and scaffold a class. For example, the following would be saved to the `lib` directory of your project and run `$ dart lib/converter_script.dart`: - -```dart -// lib/converter_script.dart -import 'package:brick_offline_first/rest_to_offline_first_converter.dart'; - -const BASE = "http://0.0.0.0:3000"; -const endpoint = "$BASE/users"; - -final converter = RestToOfflineFirstConverter(endpoint: endpoint); - -void main() { - converter.saveToFile(); -} - -// => dart lib/converter_script.dart -``` - -After the model is generated, double check for `List` and `null` types. While the converter is smart, it's not smarter than you. - -## Testing - -Responses can be stubbed to and from an `OfflineFirstWithRest` repository. For convenience, file data can be used to stub JSON responses from an API: - -```dart -// test/models/api/user.json -{ - "user": { "name" : "Thomas" } -} - -// test/models/user_test.dart -import 'package:brick_sqlite/testing.dart'; -import 'package:my_app/brick/repository.dart'; - -void main() { - group("MySqliteProvider", () { - late MyRepository repository; - setUpAll(() async { - repository = MyRepository( - restProvider: RestProvider( - client: StubOfflineFirstWithRest.fromFiles('http://0.0.0.0:3000', { - 'users': 'api/user.json' - }).client, - ) - ); - - await repository.initialize() - }); - }); -} -``` - -By default, the same response is returned for both `upsert` and `get` methods, with the only variation being in status code. However, responses can be configured for different methods: - -```dart -StubOfflineFirstWithRest( - baseEndpoint: 'http://0.0.0.0:3000', - responses: [ - StubOfflineFirstRestResponse.fromFile('users', 'api/user.json', StubHttpMethod.get), - StubOfflineFirstRestResponse.fromFile('users', 'api/user-post.json', StubHttpMethod.post), - ], -) -``` - -### Stubbing Without Files - -While storing the responses in a file can be convenient and reduce code clutter, responses can be defined inline: - -```dart -StubOfflineFirstWithRest( - baseEndpoint: 'http://0.0.0.0:3000', - responses: [ - StubOfflineFirstRestResponse('users', '{"name":"Bob"'), - StubOfflineFirstRestResponse('users', '{"name":"Alice"'), - ], -) -``` - -### Handling Endpoint Variations - -Variants in the endpoint must be explicitly declared. For example, `/user`, `/users`, `/users?by_first_name=Guy` are all different. When instantiating, specify any expected variants: - -```dart -StubOfflineFirstRestResponse( - endpoints: ["user", "users", "users?by_first_name=Guy"] -) -``` - -### Stubbing Multiple Models - -Rarely will only one model need to be stubbed. All classes in an app can be stubbed efficiently using `StubOfflineFirstWithRest`: - -```dart -setUpAll() async { - final config = { - User: ['user', 'users'], - // Even individual member endpoints must be declared for association fetching - // REST endpoints are manually configured, so the content may vary - Hat: ['hat/1', 'hat/2', 'hats'], - } - final responses = config.entries.map((modelConfig) { - return modelConfig.value.map((endpoint) { - return StubOfflineFirstRestResponse.fromFile( - 'api/${modelConfig.key.toString().toLowerCase()}.json', - endpoint: endpoint, - ); - }); - }).expand((e) => e); - final client = StubOfflineFirstWithRest( - baseEndpoint: 'http://0.0.0.0:3000', - responses: responses, - ).client; -} -``` - -### FAQ - -#### Why can't I declare a model argument? - -Due to [an open analyzer bug](https://github.com/dart-lang/sdk/issues/38309), a custom model cannot be passed to the repository as a type argument. - -## Unsupported Field Types - -* Any unsupported field types from `RestProvider`, or `SqliteProvider` -* Future iterables of future models (i.e. `Future>>`. diff --git a/packages/brick_offline_first_with_supabase_abstract/analysis_options.yaml b/packages/brick_offline_first_with_supabase_abstract/analysis_options.yaml deleted file mode 100644 index f04c6cf0..00000000 --- a/packages/brick_offline_first_with_supabase_abstract/analysis_options.yaml +++ /dev/null @@ -1 +0,0 @@ -include: ../../analysis_options.yaml diff --git a/packages/brick_offline_first_with_supabase_abstract/lib/brick_offline_first_with_supabase_abstract.dart b/packages/brick_offline_first_with_supabase_abstract/lib/brick_offline_first_with_supabase_abstract.dart deleted file mode 100644 index ac8637cd..00000000 --- a/packages/brick_offline_first_with_supabase_abstract/lib/brick_offline_first_with_supabase_abstract.dart +++ /dev/null @@ -1 +0,0 @@ -export 'package:brick_offline_first_with_supabase_abstract/src/connect_offline_first_with_supabase.dart'; diff --git a/packages/brick_offline_first_with_supabase_abstract/pubspec.yaml b/packages/brick_offline_first_with_supabase_abstract/pubspec.yaml deleted file mode 100644 index 6dca6be4..00000000 --- a/packages/brick_offline_first_with_supabase_abstract/pubspec.yaml +++ /dev/null @@ -1,19 +0,0 @@ -name: brick_offline_first_with_supabase_abstract -description: A Brick domain that routes data fetching through local providers - before a Supabase provider. -homepage: https://github.com/GetDutchie/brick/tree/main/packages/brick_offline_first_with_supabase_abstract -issue_tracker: https://github.com/GetDutchie/brick/issues -repository: https://github.com/GetDutchie/brick - -version: 0.0.1 - -environment: - sdk: ">=2.18.0 <4.0.0" - -dependencies: - brick_supabase_abstract: ">=0.0.1 <1.0.0" - brick_sqlite: ">=3.0.0 <4.0.0" - -dev_dependencies: - lints: ^2.0.1 - test: ^1.16.5 diff --git a/packages/brick_supabase/lib/brick_supabase.dart b/packages/brick_supabase/lib/brick_supabase.dart index 5747d516..b2becb5e 100644 --- a/packages/brick_supabase/lib/brick_supabase.dart +++ b/packages/brick_supabase/lib/brick_supabase.dart @@ -1,4 +1,7 @@ +export 'package:brick_supabase/src/annotations/supabase.dart'; +export 'package:brick_supabase/src/annotations/supabase_serializable.dart'; +export 'package:brick_supabase/src/runtime_supabase_column_definition.dart'; export 'package:brick_supabase/src/supabase_adapter.dart'; +export 'package:brick_supabase/src/supabase_model.dart'; export 'package:brick_supabase/src/supabase_model_dictionary.dart'; export 'package:brick_supabase/src/supabase_provider.dart'; -export 'package:brick_supabase_abstract/brick_supabase_abstract.dart'; diff --git a/packages/brick_supabase_abstract/lib/src/supabase.dart b/packages/brick_supabase/lib/src/annotations/supabase.dart similarity index 100% rename from packages/brick_supabase_abstract/lib/src/supabase.dart rename to packages/brick_supabase/lib/src/annotations/supabase.dart diff --git a/packages/brick_supabase_abstract/lib/src/supabase_serializable.dart b/packages/brick_supabase/lib/src/annotations/supabase_serializable.dart similarity index 100% rename from packages/brick_supabase_abstract/lib/src/supabase_serializable.dart rename to packages/brick_supabase/lib/src/annotations/supabase_serializable.dart diff --git a/packages/brick_supabase/lib/src/query_supabase_transformer.dart b/packages/brick_supabase/lib/src/query_supabase_transformer.dart index d6475c27..bc4fe96d 100644 --- a/packages/brick_supabase/lib/src/query_supabase_transformer.dart +++ b/packages/brick_supabase/lib/src/query_supabase_transformer.dart @@ -2,7 +2,7 @@ import 'package:brick_core/core.dart'; import 'package:brick_supabase/src/supabase_adapter.dart'; import 'package:brick_supabase/src/supabase_model_dictionary.dart'; import 'package:brick_supabase_abstract/brick_supabase_abstract.dart' hide Supabase; -import 'package:supabase_flutter/supabase_flutter.dart'; +import 'package:supabase/supabase.dart'; /// Create a prepared SQLite statement for eventual execution. Only [statement] and [values] /// should be accessed. @@ -30,9 +30,7 @@ class QuerySupabaseTransformer<_Model extends SupabaseModel> { return _destructureAssociation(adapter.fieldsToSupabaseColumns.values).join(',\n '); } - PostgrestFilterBuilder>> select() { - final builder = Supabase.instance.client.from(adapter.tableName); - + PostgrestFilterBuilder>> select(SupabaseQueryBuilder builder) { return (query?.where ?? []).fold(builder.select(selectQuery), (acc, condition) { final whereStatement = _expandCondition(condition); for (final where in whereStatement) { diff --git a/packages/brick_supabase_abstract/lib/src/runtime_supabase_column_definition.dart b/packages/brick_supabase/lib/src/runtime_supabase_column_definition.dart similarity index 100% rename from packages/brick_supabase_abstract/lib/src/runtime_supabase_column_definition.dart rename to packages/brick_supabase/lib/src/runtime_supabase_column_definition.dart diff --git a/packages/brick_supabase/lib/src/supabase_adapter.dart b/packages/brick_supabase/lib/src/supabase_adapter.dart index c75dd7ab..b4c8bad0 100644 --- a/packages/brick_supabase/lib/src/supabase_adapter.dart +++ b/packages/brick_supabase/lib/src/supabase_adapter.dart @@ -3,7 +3,7 @@ import 'package:brick_supabase/src/supabase_provider.dart'; import 'package:brick_supabase_abstract/brick_supabase_abstract.dart'; /// Constructors that convert app models to and from Supabase -abstract class SupabaseAdapter implements Adapter { +abstract mixin class SupabaseAdapter implements Adapter { /// Used for upserts; forwards to Supabase's `defaultToNull` bool get defaultToNull; diff --git a/packages/brick_supabase_abstract/lib/src/supabase_model.dart b/packages/brick_supabase/lib/src/supabase_model.dart similarity index 100% rename from packages/brick_supabase_abstract/lib/src/supabase_model.dart rename to packages/brick_supabase/lib/src/supabase_model.dart diff --git a/packages/brick_supabase/lib/src/supabase_provider.dart b/packages/brick_supabase/lib/src/supabase_provider.dart index 5fe3fbae..9923191c 100644 --- a/packages/brick_supabase/lib/src/supabase_provider.dart +++ b/packages/brick_supabase/lib/src/supabase_provider.dart @@ -4,13 +4,15 @@ import 'package:brick_supabase/src/supabase_model_dictionary.dart'; import 'package:brick_supabase_abstract/brick_supabase_abstract.dart' hide Supabase; import 'package:logging/logging.dart'; import 'package:meta/meta.dart'; -import 'package:supabase_flutter/supabase_flutter.dart'; +import 'package:supabase/supabase.dart'; /// Retrieves from an HTTP endpoint class SupabaseProvider implements Provider { /// A fully-qualified URL final String baseEndpoint; + final SupabaseClient client; + /// The glue between app models and generated adapters. @override final SupabaseModelDictionary modelDictionary; @@ -20,6 +22,7 @@ class SupabaseProvider implements Provider { SupabaseProvider( this.baseEndpoint, { + required this.client, required this.modelDictionary, }) : logger = Logger('SupabaseProvider'); @@ -27,7 +30,7 @@ class SupabaseProvider implements Provider { @override Future delete(instance, {query, repository}) async { final adapter = modelDictionary.adapterFor[TModel]!; - final tableBuilder = Supabase.instance.client.from(adapter.tableName); + final tableBuilder = client.from(adapter.tableName); final output = await adapter.toSupabase(instance, provider: this, repository: repository); final queryTransformer = @@ -47,9 +50,10 @@ class SupabaseProvider implements Provider { @override Future exists({query, repository}) async { + final adapter = modelDictionary.adapterFor[TModel]!; final queryTransformer = QuerySupabaseTransformer(modelDictionary: modelDictionary, query: query); - final builder = queryTransformer.select(); + final builder = queryTransformer.select(client.from(adapter.tableName)); final resp = await builder.count(CountOption.exact); return resp.count > 0; @@ -60,7 +64,7 @@ class SupabaseProvider implements Provider { final adapter = modelDictionary.adapterFor[TModel]!; final queryTransformer = QuerySupabaseTransformer(modelDictionary: modelDictionary, query: query); - final builder = queryTransformer.select(); + final builder = queryTransformer.select(client.from(adapter.tableName)); final resp = await builder; @@ -73,13 +77,13 @@ class SupabaseProvider implements Provider { @override Future upsert(instance, {query, repository}) async { final adapter = modelDictionary.adapterFor[TModel]!; - final tableBuilder = Supabase.instance.client.from(adapter.tableName); final output = await adapter.toSupabase(instance, provider: this, repository: repository); final queryTransformer = QuerySupabaseTransformer(modelDictionary: modelDictionary, query: query); - final builder = adapter.uniqueFields.fold(tableBuilder.upsert(output), (acc, uniqueFieldName) { + final builder = adapter.uniqueFields.fold(client.from(adapter.tableName).upsert(output), + (acc, uniqueFieldName) { final columnName = adapter.fieldsToSupabaseColumns[uniqueFieldName]!.columnName; if (output.containsKey(columnName)) { return acc.eq(columnName, output[columnName]); diff --git a/packages/brick_supabase/pubspec.yaml b/packages/brick_supabase/pubspec.yaml index af88000a..77e15497 100644 --- a/packages/brick_supabase/pubspec.yaml +++ b/packages/brick_supabase/pubspec.yaml @@ -7,11 +7,11 @@ repository: https://github.com/GetDutchie/brick version: 0.0.1 environment: - sdk: ">=2.18.0 <4.0.0" + sdk: ">=3.0.0 <4.0.0" dependencies: brick_core: ^1.1.1 - supabase_flutter: ">=2.6.0 <3.0.0" + supabase: ">=2.3.0 <3.0.0" logging: ">=1.0.0 <2.0.0" meta: ">=1.3.0 <2.0.0" brick_supabase_abstract: ">=0.0.1 <2.0.0" diff --git a/packages/brick_supabase_abstract/CHANGELOG.md b/packages/brick_supabase_abstract/CHANGELOG.md deleted file mode 100644 index b5de7397..00000000 --- a/packages/brick_supabase_abstract/CHANGELOG.md +++ /dev/null @@ -1,5 +0,0 @@ -## Unreleased - -### 0.0.1 - -Initial diff --git a/packages/brick_supabase_abstract/LICENSE b/packages/brick_supabase_abstract/LICENSE deleted file mode 100644 index 3b662c6b..00000000 --- a/packages/brick_supabase_abstract/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) Green Bits, Inc. and its affiliates. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without supabaseriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/packages/brick_supabase_abstract/README.md b/packages/brick_supabase_abstract/README.md deleted file mode 100644 index 5e384beb..00000000 --- a/packages/brick_supabase_abstract/README.md +++ /dev/null @@ -1,202 +0,0 @@ -![brick_supabase workflow](https://github.com/GetDutchie/brick/actions/workflows/brick_supabase.yaml/badge.svg) - -# REST Provider - -Connecting [Brick](https://github.com/GetDutchie/brick) with a RESTful API. - -## Supported `Query` Configuration - -### `providerArgs:` - -- `'request'` (`SupabaseRequest`) Specifies configurable information about the request like HTTP method or top level key - -### `where:` - -`SupabaseProvider` does not support any `Query#where` arguments. These should be configured on a model-by-model base by the `SupabaseSerializable#endpoint` argument. - -## Models - -### `@SupabaseSerializable(requestTransformer:)` - -:bulb: `requestTransformer` was added in Brick 3. For upgrading to Brick v3 from v2, please see [the migration guide](https://github.com/GetDutchie/brick/blob/main/MIGRATING.md). - -Every REST API is built differently, and with a fair amount of technical debt. Brick provides flexibility for inconsistent endpoints within any system. Endpoints can also change based on the query. The model adapter will query `endpoint` for `upsert` or `get` or `delete`. - -Since Dart requires annotations to be constants, dynamic functions cannot be used. This is a headache. Instead, the a `const`antized [constructor tearoff](https://medium.com/dartlang/dart-2-15-7e7a598e508a) can be used. The transformers permit dynamically defining the request (method, top level key, url, etc.) at runtime based on query params or if a Dart instance is available (`upsert` and `delete` only) - -```dart -class UserRequestTransformer extends SupabaseRequestTransformer { - final get = const SupabaseRequest(url: '/users'); - const UserRequestTransformer(Query? query, SupabaseModel? instance) : super(query, instance); -} - -@ConnectOfflineFirstWithSupabase( - supabaseConfig: SupabaseSerializable( - requestTransformer: UserRequestTransformer.new; - ) -) -class User extends OfflineFirstModel {} -``` - -Different provider calls will use different transformer fields: - -```dart -class UserRequestTransformer extends SupabaseRequestTransformer { - final get = const SupabaseRequest(url: '/users'); - final delete = SupabaseRequest(url: '/users/${instance.id}'); - - const UserRequestTransformer(Query? query, Model? instance) : super(query, instance); -} - -@ConnectOfflineFirstWithSupabase( - supabaseConfig: SupabaseSerializable( - requestTransformer: UserRequestTransformer.new, - ) -) -class User extends OfflineFirstModel {} -``` - -:warning: If an `SupabaseRequestTransform`'s method field (`get`, `upsert`, `delete`) is `null` or it's `url` is `null`, the request is skipped by the provider. - -#### With Query#providerArgs - -```dart -class UserRequestTransformer extends SupabaseRequestTransformer { - SupabaseRequest? get get { - if (query?.providerArgs.isNotEmpty && query.providerArgs['limit'] != null) { - return SupabaseRequest(url: "/users?limit=${query.providerArgs['limit']}"); - } - const SupabaseRequest(url: '/users'); - } - - final delete = SupabaseRequest(url: '/users/${instance.id}'); - - const UserRequestTransformer(Query? query, SupabaseModel? instance) : super(query, instance); -} - -@ConnectOfflineFirstWithSupabase( - supabaseConfig: SupabaseSerializable( - requestTransformer: UserRequestTransformer.new, - ) -) -class User extends OfflineFirstModel {} -``` - -#### With Query#where - -```dart -class UserRequestTransformer extends SupabaseRequestTransformer { - SupabaseRequest? get get { - if (query?.where != null) { - final id = Where.firstByField('id', query.where)?.value; - if (id != null) return SupabaseRequest(url: "/users/$id"); - } - const SupabaseRequest(url: '/users'); - } - - final delete = SupabaseRequest(url: '/users/${instance.id}'); - - const UserRequestTransformer(Query? query, SupabaseModel? instance) : super(query, instance); -} - -@ConnectOfflineFirstWithSupabase( - supabaseConfig: SupabaseSerializable( - requestTransformer: UserRequestTransformer.new, - ) -) -class User extends OfflineFirstModel {} -``` - -:bulb: For ease of illustration, the code is provided as if the transformer and model logic live in the same file. It's strongly recommended to include the request transformer logic in its own, colocated file (such as `user.model.request.dart`). - -### `@SupabaseRequest(topLevelKey:)` - -Data will most often be nested beneath a top-level key in a JSON response. The key is determined by the following priority: - -1. A `topLevelKey` in `Query#providerArgs['request']` with a non-empty value -1. `topLevelKey` if defined in a `SupabaseRequest` -1. The first discovered key. As a map is effectively an unordered list, relying on this fall through is not recommended. - -```dart -class UserRequestTransformer extends SupabaseRequestTransformer { - final get = const SupabaseRequest(url: '/users', topLevelKey: 'users'); - final upsert = const SupabaseRequest(url: '/users', topLevelKey: 'user'); - const UserRequestTransformer(Query? query, SupabaseModel? instance) : super(query, instance); -} - -@ConnectOfflineFirstWithSupabase( - requestTransformer: UserRequestTransformer.new -) -class User extends OfflineFirstModel {} -``` - -:warning: If the response from REST **is not** a map, the full response is returned instead. - -### `@SupabaseSerializable(fieldRename:)` - -Brick reduces the need to map REST keys to model field names by assuming a standard naming convention. For example: - -```dart -SupabaseSerializable(fieldRename: FieldRename.snake_case) -// on from supabase (get) - "last_name" => final String lastName -// on to supabase (upsert) -final String lastName => "last_name" -``` - -## Fields - -### `@Supabase(enumAsString:)` - -Brick by default assumes enums from a REST API will be delivered as integers matching the index in the Flutter app. However, if your API delivers strings instead, the field can be easily annotated without writing a custom generator. - -Given the API: - -```json -{ "user": { "hats": ["bowler", "birthday"] } } -``` - -Simply convert `hats` into a Dart enum: - -```dart -enum Hat { baseball, bowler, birthday } - -... - -@Supabase(enumAsString: true) -final List hats; -``` - -### `@Supabase(name:)` - -REST keys can be renamed per field. This will override the default set by `SupabaseSerializable#fieldRename`. - -```dart -@Supabase( - name: "full_name" // "full_name" is used in from and to requests to REST instead of "last_name" -) -final String lastName; -``` - -### `@Supabase(ignoreFrom:)` and `@Supabase(ignoreTo:)` - -When true, the field will be ignored by the (de)serializing function in the adapter. - -## GZipping Requests - -All requests to the API endpoint can be compressed with Dart's standard [GZip library](https://api.dart.dev/stable/2.10.4/dart-io/GZipCodec-class.html). All requests will (over)write the `Content-Encoding` header to `{'Content-Encoding': 'gzip'}`. - -```dart -import 'package:brick_supabase/gzip_http_client.dart'; - -final supabaseProvider = SupabaseProvider(client: GZipHttpClient(level: 9)); -``` - -:warning: Your API must be able to accept and decode GZipped requests. - -## Unsupported Field Types - -The following are not serialized to REST. However, unsupported types can still be accessed in the model as non-final fields. - -- Nested `List<>` e.g. `>>` -- Many-to-many associations diff --git a/packages/brick_supabase_abstract/analysis_options.yaml b/packages/brick_supabase_abstract/analysis_options.yaml deleted file mode 100644 index f04c6cf0..00000000 --- a/packages/brick_supabase_abstract/analysis_options.yaml +++ /dev/null @@ -1 +0,0 @@ -include: ../../analysis_options.yaml diff --git a/packages/brick_supabase_abstract/lib/brick_supabase_abstract.dart b/packages/brick_supabase_abstract/lib/brick_supabase_abstract.dart deleted file mode 100644 index a29d8aa0..00000000 --- a/packages/brick_supabase_abstract/lib/brick_supabase_abstract.dart +++ /dev/null @@ -1,4 +0,0 @@ -export 'package:brick_supabase_abstract/src/runtime_supabase_column_definition.dart'; -export 'package:brick_supabase_abstract/src/supabase.dart'; -export 'package:brick_supabase_abstract/src/supabase_model.dart'; -export 'package:brick_supabase_abstract/src/supabase_serializable.dart'; diff --git a/packages/brick_supabase_abstract/pubspec.yaml b/packages/brick_supabase_abstract/pubspec.yaml deleted file mode 100644 index a3bcc432..00000000 --- a/packages/brick_supabase_abstract/pubspec.yaml +++ /dev/null @@ -1,17 +0,0 @@ -name: brick_supabase_abstract -description: Supabase annotations and model/adapter to remove the Flutter restriction on build packages -homepage: https://github.com/GetDutchie/brick/tree/main/packages/brick_supabase_abstract -issue_tracker: https://github.com/GetDutchie/brick/issues -repository: https://github.com/GetDutchie/brick - -version: 0.0.1 - -environment: - sdk: ">=3.0.0 <4.0.0" - -dependencies: - brick_core: ^1.1.1 - -dev_dependencies: - lints: ^2.0.1 - test: ^1.16.5 From c7c68f317d8f11ed8c4bdafc1ff2d218cb6db4d3 Mon Sep 17 00:00:00 2001 From: Tim Shedor Date: Mon, 19 Aug 2024 00:14:25 -0700 Subject: [PATCH 6/7] switch back to dart ci --- .github/workflows/brick_offline_first_with_supabase.yaml | 2 +- .github/workflows/brick_supabase.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/brick_offline_first_with_supabase.yaml b/.github/workflows/brick_offline_first_with_supabase.yaml index 181846ed..0bd12b3e 100644 --- a/.github/workflows/brick_offline_first_with_supabase.yaml +++ b/.github/workflows/brick_offline_first_with_supabase.yaml @@ -13,6 +13,6 @@ env: jobs: analyze_format_test: - uses: ./.github/workflows/reusable-flutter-analyze-format-test.yaml + uses: ./.github/workflows/reusable-dart-analyze-format-test.yaml with: package: brick_offline_first_with_supabase diff --git a/.github/workflows/brick_supabase.yaml b/.github/workflows/brick_supabase.yaml index 45d040e4..b6f4f6ff 100644 --- a/.github/workflows/brick_supabase.yaml +++ b/.github/workflows/brick_supabase.yaml @@ -13,6 +13,6 @@ env: jobs: analyze_format_test: - uses: ./.github/workflows/reusable-flutter-analyze-format-test.yaml + uses: ./.github/workflows/reusable-dart-analyze-format-test.yaml with: package: brick_supabase From ae4af579efa85b495ac5d8211d902eddf5220305 Mon Sep 17 00:00:00 2001 From: Tim Shedor Date: Mon, 19 Aug 2024 08:34:20 -0700 Subject: [PATCH 7/7] switch to UUID --- packages/brick_supabase/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/brick_supabase/README.md b/packages/brick_supabase/README.md index c797e2fc..a70ef8f3 100644 --- a/packages/brick_supabase/README.md +++ b/packages/brick_supabase/README.md @@ -22,8 +22,8 @@ class User Connect Supabase's primary key (or any other index) to your application code. This is useful for `upsert` and `delete` logic when mutating instances. ```dart -@Supabase(unique: true, name: 'id') -final int supabaseId; +@Supabase(unique: true, name: 'uuid') +final String supabaseUuid; ``` ### `@Supabase(foreignKey:)`