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

test(supabase): ensure supabase stores associations; add testing classes #434

Merged
merged 4 commits into from
Sep 6, 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 @@ -12,6 +12,7 @@
- [Model Config](supabase/models.md)
- [Field Config](supabase/fields.md)
- [Querying](supabase/query.md)
- [Testing](supabase/testing.md)
- [GraphQL](graphql/fields.md)
- [Model Config](graphql/models.md)
- [Field Config](graphql/fields.md)
Expand Down
12 changes: 9 additions & 3 deletions docs/offline_first/testing.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# Testing (OfflineFirstWithRest)
# Testing

## OfflineFirstWithRest

Responses can be stubbed to and from an `OfflineFirstWithRest` repository. For convenience, file data can be used to stub JSON responses from an API:

Expand Down Expand Up @@ -42,7 +44,7 @@ StubOfflineFirstWithRest(
)
```

## Stubbing Without Files
### Stubbing Without Files

While storing the responses in a file can be convenient and reduce code clutter, responses can be defined inline:

Expand All @@ -56,7 +58,7 @@ StubOfflineFirstWithRest(
)
```

## Stubbing Multiple Models
### Stubbing Multiple Models

Rarely will only one model need to be stubbed. All classes in an app can be stubbed efficiently using `StubOfflineFirstWithRest`:

Expand Down Expand Up @@ -84,3 +86,7 @@ setUpAll() async {
```

?> Variants in the endpoint must be explicitly declared. For example, `/user`, `/users`, `/users?by_first_name=Guy` are all different. When instantiating, specify all expected variants.

## OfflineFirstWithSupabase

See [Supabase Testing](../supabase/testing.md)
74 changes: 74 additions & 0 deletions docs/supabase/testing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Testing

## Mocking a Supabase Instance

Quickly create a convenient mock server within test groups. The server should be configured to reset after every test block. Strongly-typed Dart models can be used to protect against code drift.

```dart
import 'package:brick_supabase/testing.dart';
import 'package:test/test.dart'

void main() {
// Pass an instance of your model dictionary to the mock server.
// This permits quick generation of fields and generated responses
final mock = SupabaseMockServer(modelDictionary: supabaseModelDictionary);

group('MyClass', () {
setUp(mock.setUp);

tearDown(mock.tearDown);

test('#myMethod', () async {
// If your request won't exactly match the columns of MyModel, provide
// the query list to the `fields:` parameter
final req = SupabaseRequest<MyModel>();
final resp = SupabaseResponse([
// mock.serialize converts models to expected Supabase payloads
// but you don't need to use it - any jsonEncode-able object
// can be passed to SupabaseRepsonse
await mock.serialize(MyModel(name: 'Demo 1', id: '1')),
await mock.serialize(MyModel(name: 'Demo 2', id: '2')),
]);
// This method stubs the server based on the described requests
// and their matched responses
mock.handle({req: resp});
final provider = SupabaseProvider(mock.client, modelDictionary: supabaseModelDictionary);
final retrieved = await provider.get<MyModel>();
expect(retrieved, hasLength(2));
});
});
}
```

## SupabaseRequest

The request object can be much more detailed. A type argument (e.g. `<MyModel>`) is not necessary if `fields:` are passed as a parameter.

It's important to specify the `filter` parameter for more complex queries or nested association upserts:

```dart
final upsertReq = SupabaseRequest<MyModel>(
requestMethod: 'POST',
// Filter will specify to only return the response if the filter also matches
// This is an important parameter when querying for a specific property
// or using multiple requests/responses
filter: 'id=eq.2',
limit: 1,
);
final associationUpsertReq = SupabaseRequest<AssociationModel>(
requestMethod: 'POST',
filter: 'id=eq.1',
limit: 1,
);
final baseResp = SupabaseResponse(await mock.serialize(MyModel(age: 1, name: 'Demo 1', id: '1')));
final associationResp = SupabaseResponse(
await mock.serialize(AssociationModel(
assoc: MyModel(age: 1, name: 'Nested', id: '2'),
name: 'Demo 1',
id: '1',
)),
);
mock.handle({upsertReq: baseResp, associationUpsertReq: associationResp});
```

?> See [supabase_provider_test.dart](https://github.com/GetDutchie/brick/blob/main/packages/brick_supabase/test/supabase_provider_test.dart) for more practial examples that use all `SupabaseProvider` methods, or [offline_first_with_supabase_repository.dart](https://github.com/GetDutchie/brick/blob/main/packages/brick_offline_first_with_supabase/test/offline_first_with_supabase_repository_test.dart) for mocking with a repository.
28 changes: 18 additions & 10 deletions example_supabase/lib/brick/adapters/customer_adapter.g.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,14 @@ Future<Customer> _$CustomerFromSupabase(Map<String, dynamic> data,

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};
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())
};
}

Future<Customer> _$CustomerFromSqlite(Map<String, dynamic> data,
Expand Down Expand Up @@ -51,7 +58,7 @@ class CustomerAdapter extends OfflineFirstWithSupabaseAdapter<Customer> {
CustomerAdapter();

@override
final tableName = 'customers';
final supabaseTableName = 'customers';
@override
final defaultToNull = true;
@override
Expand All @@ -71,6 +78,8 @@ class CustomerAdapter extends OfflineFirstWithSupabaseAdapter<Customer> {
'pizzas': const RuntimeSupabaseColumnDefinition(
association: true,
columnName: 'pizzas',
associationType: Pizza,
associationIsNullable: false,
)
};
@override
Expand Down Expand Up @@ -132,7 +141,7 @@ class CustomerAdapter extends OfflineFirstWithSupabaseAdapter<Customer> {
'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 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 {
Expand All @@ -141,13 +150,12 @@ class CustomerAdapter extends OfflineFirstWithSupabaseAdapter<Customer> {
[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]);
}) ??
[]);
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]);
}));
}
}

Expand Down
5 changes: 3 additions & 2 deletions example_supabase/lib/brick/adapters/pizza_adapter.g.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ Future<Pizza> _$PizzaFromSupabase(Map<String, dynamic> data,
{required SupabaseProvider provider, OfflineFirstWithSupabaseRepository? repository}) async {
return Pizza(
id: data['id'] as int,
toppings: data['toppings'].map(Topping.values.byName).toList().cast<Topping>(),
toppings:
data['toppings'].whereType<String>().map(Topping.values.byName).toList().cast<Topping>(),
frozen: data['frozen'] as bool);
}

Expand Down Expand Up @@ -45,7 +46,7 @@ class PizzaAdapter extends OfflineFirstWithSupabaseAdapter<Pizza> {
PizzaAdapter();

@override
final tableName = 'pizzas';
final supabaseTableName = 'pizzas';
@override
final defaultToNull = true;
@override
Expand Down
52 changes: 0 additions & 52 deletions example_supabase/lib/brick/db/20200121222037.migration.dart

This file was deleted.

48 changes: 0 additions & 48 deletions example_supabase/lib/brick/db/20210111042657.migration.dart

This file was deleted.

74 changes: 74 additions & 0 deletions example_supabase/lib/brick/db/20240906052847.migration.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// GENERATED CODE EDIT WITH CAUTION
// THIS FILE **WILL NOT** BE REGENERATED
// This file should be version controlled and can be manually edited.
part of 'schema.g.dart';

// While migrations are intelligently created, the difference between some commands, such as
// DropTable vs. RenameTable, cannot be determined. For this reason, please review migrations after
// they are created to ensure the correct inference was made.

// The migration version must **always** mirror the file name

const List<MigrationCommand> _migration_20240906052847_up = [
InsertTable('_brick_Customer_pizzas'),
InsertTable('Customer'),
InsertTable('Pizza'),
InsertForeignKey(
'_brick_Customer_pizzas',
'Customer',
foreignKeyColumn: 'l_Customer_brick_id',
onDeleteCascade: true,
onDeleteSetDefault: false,
),
InsertForeignKey(
'_brick_Customer_pizzas',
'Pizza',
foreignKeyColumn: 'f_Pizza_brick_id',
onDeleteCascade: true,
onDeleteSetDefault: false,
),
InsertColumn('id', Column.integer, onTable: 'Customer', unique: true),
InsertColumn('first_name', Column.varchar, onTable: 'Customer'),
InsertColumn('last_name', Column.varchar, onTable: 'Customer'),
InsertColumn('id', Column.integer, onTable: 'Pizza', unique: true),
InsertColumn('toppings', Column.varchar, onTable: 'Pizza'),
InsertColumn('frozen', Column.boolean, onTable: 'Pizza'),
CreateIndex(
columns: ['l_Customer_brick_id', 'f_Pizza_brick_id'],
onTable: '_brick_Customer_pizzas',
unique: true,
),
];

const List<MigrationCommand> _migration_20240906052847_down = [
DropTable('_brick_Customer_pizzas'),
DropTable('Customer'),
DropTable('Pizza'),
DropColumn('l_Customer_brick_id', onTable: '_brick_Customer_pizzas'),
DropColumn('f_Pizza_brick_id', onTable: '_brick_Customer_pizzas'),
DropColumn('id', onTable: 'Customer'),
DropColumn('first_name', onTable: 'Customer'),
DropColumn('last_name', onTable: 'Customer'),
DropColumn('id', onTable: 'Pizza'),
DropColumn('toppings', onTable: 'Pizza'),
DropColumn('frozen', onTable: 'Pizza'),
DropIndex('index__brick_Customer_pizzas_on_l_Customer_brick_id_f_Pizza_brick_id'),
];

//
// DO NOT EDIT BELOW THIS LINE
//

@Migratable(
version: '20240906052847',
up: _migration_20240906052847_up,
down: _migration_20240906052847_down,
)
class Migration20240906052847 extends Migration {
const Migration20240906052847()
: super(
version: 20240906052847,
up: _migration_20240906052847_up,
down: _migration_20240906052847_down,
);
}
9 changes: 5 additions & 4 deletions example_supabase/lib/brick/db/schema.g.dart
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
// GENERATED CODE DO NOT EDIT
// This file should be version controlled
import 'package:brick_sqlite/db.dart';
part '20210111042657.migration.dart';
part '20200121222037.migration.dart';
part '20240906052847.migration.dart';

/// All intelligently-generated migrations from all `@Migratable` classes on disk
final migrations = <Migration>{const Migration20210111042657(), const Migration20200121222037()};
final migrations = <Migration>{
const Migration20240906052847(),
};

/// A consumable database structure including the latest generated migration.
final schema = Schema(20210111042657, generatorVersion: 1, tables: <SchemaTable>{
final schema = Schema(20240906052847, generatorVersion: 1, tables: <SchemaTable>{
SchemaTable('_brick_Customer_pizzas', columns: <SchemaColumn>{
SchemaColumn('_brick_id', Column.integer,
autoincrement: true, nullable: false, isPrimaryKey: true),
Expand Down
Loading