Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

fix(supabase): pub validation and doc fixes #444

Merged
merged 9 commits into from
Sep 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/_sidebar.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
- [Model Config](offline_first/models.md)
- [Field Config](offline_first/fields.md)
- [Testing](offline_first/testing.md)
- [With Supabase](offline_first/offline_first_with_supabase_repository.md)
- [With GraphQL](offline_first/offline_first_with_graphql_repository.md)
- [With Rest](offline_first/offline_first_with_rest_repository.md)
- [Offline Queue](offline_first/offline_queue.md)
10 changes: 7 additions & 3 deletions docs/offline_first/offline_first_with_supabase_repository.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,10 +138,14 @@ Field types of classes that `extends OfflineFirstWithSupabaseModel` will automat
```dart
class User extends OfflineFirstWithSupabaseModel {
// The foreign key is a relation to the `id` column of the Address table
@Supabase(name: 'address_id')
// Help the SQLite provider connect the association locally to the one provided from remote
@OfflineFirst(where: {'id': "data['address']['id']"})
@Supabase(foreignKey: 'address_id')
final Address address;

// If the association will be created by the app, specify
// a field that maps directly to the foreign key column
// so that Brick can notify Supabase of the association.
@Sqlite(ignore: true)
String get addressId => address.id;
}

class Address extends OfflineFirstWithSupabaseModel{
Expand Down
2 changes: 1 addition & 1 deletion docs/offline_first/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,4 +89,4 @@ setUpAll() async {

## OfflineFirstWithSupabase

See [Supabase Testing](../supabase/testing.md)
See [Supabase Testing](https://getdutchie.github.io/brick/#/supabase/testing)
9 changes: 6 additions & 3 deletions docs/supabase/fields.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,15 @@ final String lastName;

?> By default, Brick renames fields to be snake case when translating to Supabase, but you can change this default in the `@SupabaseSerializable(fieldRename:)` annotation that [decorates models](models.md).

When the annotated field type extends the model's type, the Supabase column should be a foreign key.
!> **Do not use** `name` when annotating an association. Instead, use `foreignKey`.

### `@Supabase(foreignKey:)`

When the annotated field references a `OfflineFirstWithSupabaseModel`, a foreign key can be specified. Supabase's PostgREST API can usually determine the association without specifying the foreign key. However, [when multiple foreign keys exist](https://supabase.com/docs/guides/database/joins-and-nesting?queryGroups=language&language=dart#specifying-the-on-clause-for-joins-with-multiple-foreign-keys) to the same table, guiding Brick to use the right foreign key is required.

```dart
class User extends OfflineFirstWithSupabaseModel{
// The foreign key is a relation to the `id` column of the Address table
@Supabase(name: 'address_id')
@Supabase(foreignKey: 'address_id')
final Address address;
}

Expand Down
49 changes: 49 additions & 0 deletions example_supabase/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/

# IntelliJ related
*.iml
*.ipr
*.iws
.idea/

# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/

# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutterplugins
.flutterpluginsdependencies
.pubcache/
.pub/
/build/

# Symbolication related
app.*.symbols

# Obfuscation related
app.*.map.json

# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release
windows/*
web/*
macos/*
linux/*
android/*
ios/*
45 changes: 45 additions & 0 deletions example_supabase/.metadata
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.

version:
revision: "2663184aa79047d0a33a14a3b607954f8fdd8730"
channel: "stable"

project_type: app

# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730
base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730
- platform: android
create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730
base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730
- platform: ios
create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730
base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730
- platform: linux
create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730
base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730
- platform: macos
create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730
base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730
- platform: web
create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730
base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730
- platform: windows
create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730
base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730

# User provided section

# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- "lib/main.dart"
- "ios/Runner.xcodeproj/project.pbxproj"
38 changes: 21 additions & 17 deletions example_supabase/README.md
Original file line number Diff line number Diff line change
@@ -1,35 +1,39 @@
# Brick with Supabase Example

This minimal example demonstrates how to use Brick with [Supabase](https://supabase.com/). Follow the instructions below to get started.

## Setting up the Supabase project

1. **Create the table**: Run the following SQL command in your Supabase SQL editor to create the customers table:
1. **Create the table**: Run the following SQL command in your Supabase SQL editor to create the customers and pizzas table:

```sql
CREATE TABLE customers (
id UUID PRIMARY KEY,
first_name text NOT NULL,
last_name text NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
id UUID PRIMARY KEY,
first_name text NOT NULL,
last_name text NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE pizzas (
id UUID PRIMARY KEY,
frozen boolean NOT NULL DEFAULT false,
customer_id UUID NOT NULL
);

ALTER TABLE pizzas ADD CONSTRAINT fk_customer FOREIGN KEY (customer_id) REFERENCES customers (id);
```

2. **Insert Dummy Data**: Insert some dummy data into the customers table by running the following SQL command
2. **Insert Dummy Data**: Insert some dummy data:

```sql
INSERT INTO customers (id, first_name, last_name, created_at) VALUES
('a8098c1a-f86e-11da-bd1a-00112444be1e', 'Bruce', 'Fortner', NOW()),
('b8098c1a-f86e-11da-bd1a-00112444be1e', 'Jane', 'Smith', NOW()),
('c8098c1a-f86e-11da-bd1a-00112444be1e', 'Alice', 'Johnson', NOW());
('a8098c1a-f86e-11da-bd1a-00112444be1e', 'Bruce', 'Fortner', NOW()),
('b8098c1a-f86e-11da-bd1a-00112444be1e', 'Jane', 'Smith', NOW()),
('c8098c1a-f86e-11da-bd1a-00112444be1e', 'Alice', 'Johnson', NOW());
INSERT INTO pizzas (id, frozen, customer_id) VALUES
('d8098c1a-f86e-11da-bd1a-00112444be1e', TRUE, 'a8098c1a-f86e-11da-bd1a-00112444be1e'),
('e8098c1a-f86e-11da-bd1a-00112444be1e', FALSE, 'b8098c1a-f86e-11da-bd1a-00112444be1e'),
('f8098c1a-f86e-11da-bd1a-00112444be1e', TRUE, 'c8098c1a-f86e-11da-bd1a-00112444be1e');
```

3. **Enable Anonymous Sign-Ins**: Go to your Supabase dashboard, navigate to Settings > Authentication > User Signups, and enable anonymous sign-ins.

## Setting up the Flutter example project

4. **Update Variables**: Update `main.dart` with your Supabase project URL and anonymous key. You can find these values in the Supabase dashboard under Settings > API.

## Running the Flutter app

5. **Run the Flutter Project**: This example supports iOS and Android. Make sure run `flutter create .` first.
73 changes: 6 additions & 67 deletions example_supabase/lib/brick/adapters/customer_adapter.g.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,47 +4,22 @@ part of '../brick.g.dart';
Future<Customer> _$CustomerFromSupabase(Map<String, dynamic> data,
{required SupabaseProvider provider, OfflineFirstWithSupabaseRepository? repository}) async {
return Customer(
id: data['id'] as int?,
id: data['id'] as String,
firstName: data['first_name'] as String?,
lastName: data['last_name'] as String?,
pizzas: await Future.wait<Pizza>(data['pizzas']
?.map(
(d) => PizzaAdapter().fromSupabase(d, provider: provider, repository: repository))
.toList()
.cast<Future<Pizza>>() ??
[]));
lastName: data['last_name'] as String?);
}

Future<Map<String, dynamic>> _$CustomerToSupabase(Customer instance,
{required SupabaseProvider provider, OfflineFirstWithSupabaseRepository? repository}) async {
return {
'id': instance.id,
'first_name': instance.firstName,
'last_name': instance.lastName,
'pizzas': await Future.wait<Map<String, dynamic>>(instance.pizzas
.map((s) => PizzaAdapter().toSupabase(s, provider: provider, repository: repository))
.toList())
};
return {'id': instance.id, 'first_name': instance.firstName, 'last_name': instance.lastName};
}

Future<Customer> _$CustomerFromSqlite(Map<String, dynamic> data,
{required SqliteProvider provider, OfflineFirstWithSupabaseRepository? repository}) async {
return Customer(
id: data['id'] == null ? null : data['id'] as int?,
id: data['id'] as String,
firstName: data['first_name'] == null ? null : data['first_name'] as String?,
lastName: data['last_name'] == null ? null : data['last_name'] as String?,
pizzas: (await provider.rawQuery(
'SELECT DISTINCT `f_Pizza_brick_id` FROM `_brick_Customer_pizzas` WHERE l_Customer_brick_id = ?',
[data['_brick_id'] as int]).then((results) {
final ids = results.map((r) => r['f_Pizza_brick_id']);
return Future.wait<Pizza>(ids.map((primaryKey) => repository!
.getAssociation<Pizza>(
Query.where('primaryKey', primaryKey, limit1: true),
)
.then((r) => r!.first)));
}))
.toList()
.cast<Pizza>())
lastName: data['last_name'] == null ? null : data['last_name'] as String?)
..primaryKey = data['_brick_id'] as int;
}

Expand Down Expand Up @@ -74,12 +49,6 @@ class CustomerAdapter extends OfflineFirstWithSupabaseAdapter<Customer> {
'lastName': const RuntimeSupabaseColumnDefinition(
association: false,
columnName: 'last_name',
),
'pizzas': const RuntimeSupabaseColumnDefinition(
association: true,
columnName: 'pizzas',
associationType: Pizza,
associationIsNullable: false,
)
};
@override
Expand All @@ -98,7 +67,7 @@ class CustomerAdapter extends OfflineFirstWithSupabaseAdapter<Customer> {
association: false,
columnName: 'id',
iterable: false,
type: int,
type: String,
),
'firstName': const RuntimeSqliteColumnDefinition(
association: false,
Expand All @@ -111,12 +80,6 @@ class CustomerAdapter extends OfflineFirstWithSupabaseAdapter<Customer> {
columnName: 'last_name',
iterable: false,
type: String,
),
'pizzas': const RuntimeSqliteColumnDefinition(
association: true,
columnName: 'pizzas',
iterable: true,
type: Pizza,
)
};
@override
Expand All @@ -134,30 +97,6 @@ class CustomerAdapter extends OfflineFirstWithSupabaseAdapter<Customer> {

@override
final String tableName = 'Customer';
@override
Future<void> afterSave(instance, {required provider, repository}) async {
if (instance.primaryKey != null) {
final pizzasOldColumns = await provider.rawQuery(
'SELECT `f_Pizza_brick_id` FROM `_brick_Customer_pizzas` WHERE `l_Customer_brick_id` = ?',
[instance.primaryKey]);
final pizzasOldIds = pizzasOldColumns.map((a) => a['f_Pizza_brick_id']);
final pizzasNewIds = instance.pizzas.map((s) => s.primaryKey).whereType<int>();
final pizzasIdsToDelete = pizzasOldIds.where((id) => !pizzasNewIds.contains(id));

await Future.wait<void>(pizzasIdsToDelete.map((id) async {
return await provider.rawExecute(
'DELETE FROM `_brick_Customer_pizzas` WHERE `l_Customer_brick_id` = ? AND `f_Pizza_brick_id` = ?',
[instance.primaryKey, id]).catchError((e) => null);
}));

await Future.wait<int?>(instance.pizzas.map((s) async {
final id = s.primaryKey ?? await provider.upsert<Pizza>(s, repository: repository);
return await provider.rawInsert(
'INSERT OR IGNORE INTO `_brick_Customer_pizzas` (`l_Customer_brick_id`, `f_Pizza_brick_id`) VALUES (?, ?)',
[instance.primaryKey, id]);
}));
}
}

@override
Future<Customer> fromSupabase(Map<String, dynamic> input,
Expand Down
Loading