From 5461db3ffddc8152e39eae9c0cace7c82f3b3434 Mon Sep 17 00:00:00 2001 From: Tim Shedor Date: Sun, 18 Aug 2024 23:25:21 -0700 Subject: [PATCH] eng(supabase): add abstract packages (#402) --- .../.gitignore | 1 + .../CHANGELOG.md | 5 + .../LICENSE | 21 ++ .../README.md | 147 +++++++++++++ .../analysis_options.yaml | 1 + ..._offline_first_with_supabase_abstract.dart | 1 + .../connect_offline_first_with_supabase.dart | 36 ++++ .../pubspec.yaml | 19 ++ 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 + .../runtime_supabase_column_definition.dart | 21 ++ .../lib/src/supabase.dart | 101 +++++++++ .../lib/src/supabase_model.dart | 4 + .../lib/src/supabase_serializable.dart | 37 ++++ packages/brick_supabase_abstract/pubspec.yaml | 17 ++ 18 files changed, 644 insertions(+) create mode 100644 packages/brick_offline_first_with_supabase_abstract/.gitignore create mode 100644 packages/brick_offline_first_with_supabase_abstract/CHANGELOG.md create mode 100644 packages/brick_offline_first_with_supabase_abstract/LICENSE create mode 100644 packages/brick_offline_first_with_supabase_abstract/README.md create mode 100644 packages/brick_offline_first_with_supabase_abstract/analysis_options.yaml create mode 100644 packages/brick_offline_first_with_supabase_abstract/lib/brick_offline_first_with_supabase_abstract.dart create mode 100644 packages/brick_offline_first_with_supabase_abstract/lib/src/connect_offline_first_with_supabase.dart create mode 100644 packages/brick_offline_first_with_supabase_abstract/pubspec.yaml create mode 100644 packages/brick_supabase_abstract/CHANGELOG.md create mode 100644 packages/brick_supabase_abstract/LICENSE create mode 100644 packages/brick_supabase_abstract/README.md create mode 100644 packages/brick_supabase_abstract/analysis_options.yaml create mode 100644 packages/brick_supabase_abstract/lib/brick_supabase_abstract.dart create mode 100644 packages/brick_supabase_abstract/lib/src/runtime_supabase_column_definition.dart create mode 100644 packages/brick_supabase_abstract/lib/src/supabase.dart create mode 100644 packages/brick_supabase_abstract/lib/src/supabase_model.dart create mode 100644 packages/brick_supabase_abstract/lib/src/supabase_serializable.dart create mode 100644 packages/brick_supabase_abstract/pubspec.yaml diff --git a/packages/brick_offline_first_with_supabase_abstract/.gitignore b/packages/brick_offline_first_with_supabase_abstract/.gitignore new file mode 100644 index 00000000..79dcd336 --- /dev/null +++ b/packages/brick_offline_first_with_supabase_abstract/.gitignore @@ -0,0 +1 @@ +!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 new file mode 100644 index 00000000..b5de7397 --- /dev/null +++ b/packages/brick_offline_first_with_supabase_abstract/CHANGELOG.md @@ -0,0 +1,5 @@ +## 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 new file mode 100644 index 00000000..f8548186 --- /dev/null +++ b/packages/brick_offline_first_with_supabase_abstract/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 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 new file mode 100644 index 00000000..96e4711c --- /dev/null +++ b/packages/brick_offline_first_with_supabase_abstract/README.md @@ -0,0 +1,147 @@ +![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 new file mode 100644 index 00000000..f04c6cf0 --- /dev/null +++ b/packages/brick_offline_first_with_supabase_abstract/analysis_options.yaml @@ -0,0 +1 @@ +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 new file mode 100644 index 00000000..ac8637cd --- /dev/null +++ b/packages/brick_offline_first_with_supabase_abstract/lib/brick_offline_first_with_supabase_abstract.dart @@ -0,0 +1 @@ +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/lib/src/connect_offline_first_with_supabase.dart b/packages/brick_offline_first_with_supabase_abstract/lib/src/connect_offline_first_with_supabase.dart new file mode 100644 index 00000000..b40580c2 --- /dev/null +++ b/packages/brick_offline_first_with_supabase_abstract/lib/src/connect_offline_first_with_supabase.dart @@ -0,0 +1,36 @@ +import 'package:brick_sqlite/brick_sqlite.dart'; +import 'package:brick_supabase_abstract/brick_supabase_abstract.dart'; + +/// An annotation used to specify a class to generate code for. +/// +/// Clones the annotated class to two files for processing by their respective builders +class ConnectOfflineFirstWithSupabase { + /// Creates a new [ConnectOfflineFirstWithSupabase] instance. + const ConnectOfflineFirstWithSupabase({ + this.sqliteConfig, + this.supabaseConfig, + }); + + /// Configuration for the [SqliteSerializable] annotation + final SqliteSerializable? sqliteConfig; + + /// Configuration for the [SupabaseSerializable] annotation + final SupabaseSerializable? supabaseConfig; + + /// An instance of [ConnectOfflineFirstWithSupabase] with all fields set to their default + /// values. + static const defaults = ConnectOfflineFirstWithSupabase( + sqliteConfig: SqliteSerializable.defaults, + supabaseConfig: SupabaseSerializable.defaults, + ); + + /// Returns a new [ConnectOfflineFirstWithSupabase] instance with fields equal to the + /// corresponding values in `this`, if not `null`. + /// + /// Otherwise, the returned value has the default value as defined in + /// [defaults]. + ConnectOfflineFirstWithSupabase withDefaults() => ConnectOfflineFirstWithSupabase( + sqliteConfig: sqliteConfig ?? defaults.sqliteConfig, + supabaseConfig: supabaseConfig ?? defaults.supabaseConfig, + ); +} diff --git a/packages/brick_offline_first_with_supabase_abstract/pubspec.yaml b/packages/brick_offline_first_with_supabase_abstract/pubspec.yaml new file mode 100644 index 00000000..6dca6be4 --- /dev/null +++ b/packages/brick_offline_first_with_supabase_abstract/pubspec.yaml @@ -0,0 +1,19 @@ +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_abstract/CHANGELOG.md b/packages/brick_supabase_abstract/CHANGELOG.md new file mode 100644 index 00000000..b5de7397 --- /dev/null +++ b/packages/brick_supabase_abstract/CHANGELOG.md @@ -0,0 +1,5 @@ +## Unreleased + +### 0.0.1 + +Initial diff --git a/packages/brick_supabase_abstract/LICENSE b/packages/brick_supabase_abstract/LICENSE new file mode 100644 index 00000000..3b662c6b --- /dev/null +++ b/packages/brick_supabase_abstract/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_abstract/README.md b/packages/brick_supabase_abstract/README.md new file mode 100644 index 00000000..5e384beb --- /dev/null +++ b/packages/brick_supabase_abstract/README.md @@ -0,0 +1,202 @@ +![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 new file mode 100644 index 00000000..f04c6cf0 --- /dev/null +++ b/packages/brick_supabase_abstract/analysis_options.yaml @@ -0,0 +1 @@ +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 new file mode 100644 index 00000000..a29d8aa0 --- /dev/null +++ b/packages/brick_supabase_abstract/lib/brick_supabase_abstract.dart @@ -0,0 +1,4 @@ +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/lib/src/runtime_supabase_column_definition.dart b/packages/brick_supabase_abstract/lib/src/runtime_supabase_column_definition.dart new file mode 100644 index 00000000..1ea368b7 --- /dev/null +++ b/packages/brick_supabase_abstract/lib/src/runtime_supabase_column_definition.dart @@ -0,0 +1,21 @@ +/// Used to define types in [SupabaseAdapter#supabaseFieldsToColumns]. The build runner package +/// extracts types and associations that would've been otherwise inaccessible at runtime. +class RuntimeSupabaseColumnDefinition { + /// Whether this column relates to another table in Supabase + /// This is true for `Iterable` and `SupabaseModel`. Defaults to `false`. + final bool association; + + final String? associationForeignKey; + + final Type? associationType; + + /// The Supabase column name, **not** the field name. + final String columnName; + + const RuntimeSupabaseColumnDefinition({ + this.association = false, + this.associationForeignKey, + this.associationType, + required this.columnName, + }); +} diff --git a/packages/brick_supabase_abstract/lib/src/supabase.dart b/packages/brick_supabase_abstract/lib/src/supabase.dart new file mode 100644 index 00000000..988ea687 --- /dev/null +++ b/packages/brick_supabase_abstract/lib/src/supabase.dart @@ -0,0 +1,101 @@ +import 'package:brick_core/field_serializable.dart'; + +/// An annotation used to specify how a field is serialized for a [SupabaseAdapter]. +/// Heavily inspired by [JsonKey](https://github.com/dart-lang/json_serializable/blob/master/json_annotation/lib/src/json_key.dart) +class Supabase implements FieldSerializable { + /// The value to use if the source does not contain this key or if the + /// value is `null`. **Only applicable during deserialization.** + /// + /// Must be a primitive type: `bool`, `DateTime`, `double`, `int`, `List`, `Map`, + /// `Set`, or `String`. [defaultValue] must also match the field's `Type`. + @override + final String? defaultValue; + + /// By default, all enums from Supabase are assumed to be delivered as `int`. For APIs that + /// deliver enums as `String` (e.g. `{"party", "baseball", ...}`). Works for Iterable and + /// single field types of `enum`. + /// + /// The type of this field should be an enum. Defaults to `false`. + @override + final bool enumAsString; + + /// 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; + /// } + /// ``` + final String? foreignKey; + + @override + final String? fromGenerator; + + @override + final bool ignore; + + @override + final bool ignoreFrom; + + @override + final bool ignoreTo; + + /// This field reflects a unique index in the Supabase table, such as a primary key, + /// most often `id`. + /// + /// Fields where `unique` is `true` will be used to target upserts and deletes. + final bool unique; + + /// The key name to use when reading and writing values corresponding + /// to the annotated field. + /// + /// Associations should not be annotated with `name`. + /// + /// If `null`, the snake case value of the field is used. + @override + final String? name; + + /// When `true`, `null` fields are handled gracefully when encoding from JSON. + /// This indicates that the payload from Supabase could be `null` and is not related to + /// Dart nullability. Defaults to `false`. + @override + final bool nullable; + + @override + final String? toGenerator; + + /// Creates a new [Supabase] instance. + /// + /// Only required when the default behavior is not desired. + const Supabase({ + this.defaultValue, + bool? enumAsString, + this.fromGenerator, + this.foreignKey, + bool? ignore, + bool? ignoreFrom, + bool? ignoreTo, + bool? unique, + this.name, + bool? nullable, + this.toGenerator, + }) : enumAsString = enumAsString ?? false, + ignore = ignore ?? false, + ignoreFrom = ignoreFrom ?? false, + ignoreTo = ignoreTo ?? false, + unique = unique ?? false, + nullable = nullable ?? false; + + /// An instance of [Supabase] with all fields set to their default values. + static const defaults = Supabase(); +} diff --git a/packages/brick_supabase_abstract/lib/src/supabase_model.dart b/packages/brick_supabase_abstract/lib/src/supabase_model.dart new file mode 100644 index 00000000..a086134b --- /dev/null +++ b/packages/brick_supabase_abstract/lib/src/supabase_model.dart @@ -0,0 +1,4 @@ +import 'package:brick_core/core.dart'; + +/// Models accessible to the [SupabaseProvider] +abstract mixin class SupabaseModel implements Model {} diff --git a/packages/brick_supabase_abstract/lib/src/supabase_serializable.dart b/packages/brick_supabase_abstract/lib/src/supabase_serializable.dart new file mode 100644 index 00000000..a7422ac5 --- /dev/null +++ b/packages/brick_supabase_abstract/lib/src/supabase_serializable.dart @@ -0,0 +1,37 @@ +/// An annotation used to specify a class to generate Supabase code for. +/// +/// Creates a serialize/deserialize function for JSON. +// +// Heavily borrowed/inspired by [JsonSerializable](https://github.com/dart-lang/json_serializable/blob/master/json_annotation/lib/src/json_serializable.dart) +class SupabaseSerializable { + /// Forwards to Supabase's defaultToNull parameter. + final bool defaultToNull; + + /// Forwards to Supabase's ignoreDuplicates parameter. + final bool ignoreDuplicates; + + /// Forwards to Supabase's onConflict parameter. + final String? onConflict; + + /// The Supabase table name to fetch from. For example, `"users"` + /// in `Supabase.instance.client.from("users")`. + /// The schema name is not required. + final String tableName; + + /// Creates a new [SupabaseSerializable] instance. + const SupabaseSerializable({ + this.defaultToNull = true, + this.ignoreDuplicates = false, + this.onConflict, + required this.tableName, + }); + + /// An instance of [SupabaseSerializable] with all fields set to their default + /// values. + static const defaults = SupabaseSerializable( + defaultToNull: true, + ignoreDuplicates: false, + onConflict: null, + tableName: '', + ); +} diff --git a/packages/brick_supabase_abstract/pubspec.yaml b/packages/brick_supabase_abstract/pubspec.yaml new file mode 100644 index 00000000..a3bcc432 --- /dev/null +++ b/packages/brick_supabase_abstract/pubspec.yaml @@ -0,0 +1,17 @@ +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