Skip to content

Commit

Permalink
eng(supabase): add generator and offline_first build (#404)
Browse files Browse the repository at this point in the history
  • Loading branch information
tshedor authored Aug 19, 2024
1 parent 0c12ef3 commit f7f3caa
Show file tree
Hide file tree
Showing 40 changed files with 1,052 additions and 12 deletions.
18 changes: 18 additions & 0 deletions .github/workflows/brick_offline_first_with_supabase_build.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: Brick Offline First with Supabase Build
on:
push:
branches:
- main
pull_request:
paths:
- "packages/brick_offline_first_with_supabase_build/**"
- ".github/workflows/brick_offline_first_with_supabase_build.yaml"

env:
PUB_ENVIRONMENT: bot.github

jobs:
analyze_format_test:
uses: ./.github/workflows/reusable-dart-analyze-format-test.yaml
with:
package: brick_offline_first_with_supabase_build
18 changes: 18 additions & 0 deletions .github/workflows/brick_supabase_generators.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: Brick Supabase Generators
on:
push:
branches:
- main
pull_request:
paths:
- "packages/brick_supabase_generators/**"
- ".github/workflows/brick_supabase_generators.yaml"

env:
PUB_ENVIRONMENT: bot.github

jobs:
analyze_format_test:
uses: ./.github/workflows/reusable-dart-analyze-format-test.yaml
with:
package: brick_supabase_generators
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export 'package:brick_offline_first_with_supabase/src/annotations/connect_offline_first_with_supabase.dart';
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';
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import 'package:brick_sqlite/brick_sqlite.dart';
import 'package:brick_supabase_abstract/brick_supabase_abstract.dart';
import 'package:brick_supabase/brick_supabase.dart';

/// An annotation used to specify a class to generate code for.
///
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import 'package:brick_offline_first/brick_offline_first.dart';
import 'package:brick_supabase_abstract/brick_supabase_abstract.dart';
import 'package:brick_supabase/brick_supabase.dart';

abstract class OfflineFirstWithSupabaseModel extends OfflineFirstModel with SupabaseModel {}
1 change: 0 additions & 1 deletion packages/brick_offline_first_with_supabase/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ 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"
Expand Down
5 changes: 5 additions & 0 deletions packages/brick_offline_first_with_supabase_build/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
## Unreleased

### 0.0.1

Initial
21 changes: 21 additions & 0 deletions packages/brick_offline_first_with_supabase_build/LICENSE
Original file line number Diff line number Diff line change
@@ -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.
59 changes: 59 additions & 0 deletions packages/brick_offline_first_with_supabase_build/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
![brick_offline_first_with_supabase_build workflow](https://github.com/GetDutchie/brick/actions/workflows/brick_offline_first_with_supabase_build.yaml/badge.svg)

# Brick Offline First with Supabase Build

Code generator that provides (de)serializing functions for Brick adapters using SupabaseProvider and SqliteProvider within the OfflineFirstWithSupabase domain. Classes annotated with `ConnectOfflineFirstWithSupabase` **and** extending the model `OfflineFirstWithSupabase` will be discovered.

## Setup

`dart:mirrors` will conflict with Flutter, so this package should be imported as a dev dependency and executed before an app's run time.

```yaml
dev_dependencies:
brick_offline_first_with_supabase_build:
```
Build your code:
```shell
cd my_app; (flutter) pub run build_runner build
```

## How does this work?

![OfflineFirst Builder](https://user-images.githubusercontent.com/865897/72175884-1c399900-3392-11ea-8baa-7d50f8db6773.jpg)

1. A class is discovered with the `@ConnectOfflineFirstWithSupabase` annotation.

```dart
@ConnectOfflineFirstWithSupabase(
sqliteConfig: SqliteSerializable(
nullable: false,
),
supabaseConfig: SupabaseSerializable(
tableName: 'users',
)
)
class MyClass extends OfflineFirstWithSupabaseModel
```

1. `OfflineFirstGenerator` expands respective sub configuration from the `@ConnectOfflineFirstWithSupabase` configuration.
1. Instances of `SupabaseFields` and `SqliteFields` are created and passed to their respective generators. This will expand all fields of the class into consumable code. Namely, the `#sorted` method ensures there are no duplicates and the fields are passed in the order they're declared in the class.
1. `SupabaseSerialize`, `SupabaseDeserialize`, `SqliteSerialize`, and `SqliteDeserialize` generators are created from the previous configurations and the aforementioned fields. Since these generators inherit from the same base class, this documentation will continue with `SupabaseSerialize` as the primary example.
1. The fields are iterated through `SupabaseSerialize#coderForField` to generate the transforming code. This function produces output by checking the field's type. For example, `final List<Future<int>> futureNumbers` may produce `'future_numbers': await Future.wait<int>(futureNumbers)`.
1. The output is gathered via `SupabaseSerialize#generate` and wrapped in a function such as `MODELToSupabase()`. All such functions from all generators are included in the output of the adapter generator. As some down-stream providers or repositories may require extra information in the adapter (such as `supabaseEndpoint` or `tableName`), this data is also passed through `#generate`.
1. Now with the complete adapter code, the AdapterBuilder saves `adapters/MODELNAME.g.dart`.
1. Now with all annotated classes having adapter counterparts, a model dictionary is generated and saved to `brick.g.dart` with the ModelDictionaryBuilder.
1. Concurrently, the super generator may produce a new schema that reflects the new data structure. `SqliteSchemaGenerator` generates a new schema. Using `SchemaDifference`, a new migration is created (this will be saved to `db/migrations/VERSION_migration.dart`). The new migration is logged and prepended to the generated code. This will be saved to `db/schema.g.dart` with the SqliteSchemaBuilder. A new migration will be saved to `db/<INCREMENT_VERSION>.g.dart` with the NewMigrationBuilder.

## FAQ

### Why doesn't this library use [JsonSerializable](https://pub.dartlang.org/packages/json_serializable)?

While `JsonSerializable` is an incredibly robust library, it is, in short, opinionated. Just like this library is opinionated. This prevents incorporation in a number of ways:

- `@JsonSerializable` detects serializable models [via a class method check](https://github.com/dart-lang/json_serializable/blob/6a39a76ff8967de50db0f4b344181328269cf978/json_serializable/lib/src/type_helpers/json_helper.dart#L131-L133). Since `@ConnectOfflineFirstWithSupabase` uses an abstracted builder, checking the source class is not effective.
- `@JsonSerializable` only supports enums as strings, not as indexes. While this is admittedly more resilient, it can’t be retrofitted to enums passed as integers from an API.
- Lastly, dynamically applying a configuration is an uphill battle with `ConstantReader` (the annotation would have to be converted into a [digestable format](https://github.com/dart-lang/json_serializable/blob/5cbe2f9b3009cd78c7a55277f5278ea09952340d/json_serializable/lib/src/json_serializable_generator.dart#L103)). While ultimately this could be possible, the library is still unusable because of the aforementioned points.

`JsonSerializable` is an incredibly robust library and should be used for all other scenarios.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
include: ../../analysis_options.yaml

linter:
rules:
library_prefixes: false
82 changes: 82 additions & 0 deletions packages/brick_offline_first_with_supabase_build/build.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Read about `build.yaml` at https://pub.dartlang.org/packages/build_config
builders:
brick_aggregate_builder:
import: "package:brick_offline_first_with_supabase_build/builder.dart"
builder_factories: ["offlineFirstAggregateBuilder"]
build_extensions:
{ "$lib$": ["models_and_migrations.brick_aggregate.dart"] }
build_to: cache
applies_builders: ["source_gen|combining_builder"]
runs_before: ["brick_schema_builder", "brick_model_dictionary_builder"]
auto_apply: dependents
defaults:
generate_for:
exclude:
- lib/brick/adapters/*.dart
- brick/adapters/*.dart
- lib/brick/db/*.dart
- brick/db/*.dart

brick_adapters_builder:
import: "package:brick_offline_first_with_supabase_build/builder.dart"
builder_factories: ["offlineFirstAdaptersBuilder"]
build_extensions: { ".model.dart": [".adapter_build_offline_first.dart"] }
build_to: cache
auto_apply: dependents
applies_builders: ["source_gen|combining_builder"]
defaults:
generate_for:
include:
- lib/**/*.model.dart
- lib/brick/db/*.migration.dart
- lib/brick/db/schema.g.dart
- lib/brick/brick.g.dart
exclude:
- lib/brick/adapters/*.dart
- brick/adapters/*.dart
- lib/brick/db/*.dart
- brick/db/*.dart

brick_schema_builder:
import: "package:brick_offline_first_with_supabase_build/builder.dart"
builder_factories: ["offlineFirstSchemaBuilder"]
build_extensions:
{
".brick_aggregate.dart":
[".brick_aggregate.schema_build_offline_first.dart"],
}
build_to: cache
auto_apply: dependents
required_inputs: [".brick_aggregate.dart"]
applies_builders:
[
"source_gen|combining_builder",
":brick_aggregate_builder",
":brick_new_migration_builder",
]

brick_new_migration_builder:
import: "package:brick_offline_first_with_supabase_build/builder.dart"
builder_factories: ["offlineFirstNewMigrationBuilder"]
build_extensions:
{ ".brick_aggregate.dart": [".brick_aggregate.migration_builder.dart"] }
build_to: cache
auto_apply: dependents
runs_before: ["brick_schema_builder"]
required_inputs: [".brick_aggregate.dart"]
applies_builders:
["source_gen|combining_builder", ":brick_aggregate_builder"]

brick_model_dictionary_builder:
import: "package:brick_offline_first_with_supabase_build/builder.dart"
builder_factories: ["offlineFirstModelDictionaryBuilder"]
build_extensions:
{
".brick_aggregate.dart":
[".brick_aggregate.model_dictionary_build_offline_first.dart"],
}
build_to: cache
auto_apply: dependents
required_inputs: [".brick_aggregate.dart"]
applies_builders:
["source_gen|combining_builder", ":brick_aggregate_builder"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import 'package:brick_build/builders.dart';
import 'package:brick_offline_first_build/brick_offline_first_build.dart';
import 'package:brick_offline_first_with_supabase/brick_offline_first_with_supabase.dart';
import 'package:brick_offline_first_with_supabase_build/src/offline_first_with_supabase_generator.dart';
import 'package:brick_sqlite_generators/builders.dart';
import 'package:build/build.dart';

final _schemaGenerator = OfflineFirstSchemaGenerator();

class OfflineFirstMigrationBuilder extends NewMigrationBuilder<ConnectOfflineFirstWithSupabase> {
@override
final schemaGenerator = _schemaGenerator;
}

class OfflineFirstSchemaBuilder extends SchemaBuilder<ConnectOfflineFirstWithSupabase> {
@override
final schemaGenerator = _schemaGenerator;
}

final offlineFirstGenerator = const OfflineFirstWithSupabaseGenerator(
superAdapterName: 'OfflineFirstWithSupabase',
repositoryName: 'OfflineFirstWithSupabase',
);

/// These functions act as builder factories used by `build.yaml`
Builder offlineFirstAggregateBuilder(options) => AggregateBuilder(
requiredImports: [
"import 'package:brick_offline_first/brick_offline_first.dart';",
"import 'package:brick_core/query.dart';",
"import 'package:brick_sqlite/db.dart';",
],
);
Builder offlineFirstAdaptersBuilder(options) =>
AdapterBuilder<ConnectOfflineFirstWithSupabase>(offlineFirstGenerator);
Builder offlineFirstModelDictionaryBuilder(options) =>
ModelDictionaryBuilder<ConnectOfflineFirstWithSupabase>(
const OfflineFirstModelDictionaryGenerator('Supabase'),
expectedImportRemovals: [
"import 'package:brick_offline_first/brick_offline_first.dart';",
'import "package:brick_offline_first/brick_offline_first.dart";',
],
);
Builder offlineFirstNewMigrationBuilder(options) => OfflineFirstMigrationBuilder();
Builder offlineFirstSchemaBuilder(options) => OfflineFirstSchemaBuilder();
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import 'package:analyzer/dart/element/element.dart';
import 'package:brick_build/generators.dart';
import 'package:brick_offline_first_build/brick_offline_first_build.dart';
import 'package:brick_supabase/brick_supabase.dart';
import 'package:brick_supabase_generators/generators.dart';
import 'package:brick_supabase_generators/supabase_model_serdes_generator.dart';

class _OfflineFirstSupabaseSerialize extends SupabaseSerialize
with OfflineFirstJsonSerialize<SupabaseModel, Supabase> {
@override
final OfflineFirstFields offlineFirstFields;

_OfflineFirstSupabaseSerialize(
super.element,
super.fields, {
required super.repositoryName,
}) : offlineFirstFields = OfflineFirstFields(element);
}

class _OfflineFirstSupabaseDeserialize extends SupabaseDeserialize
with OfflineFirstJsonDeserialize<SupabaseModel, Supabase> {
@override
final OfflineFirstFields offlineFirstFields;

_OfflineFirstSupabaseDeserialize(
super.element,
super.fields, {
required super.repositoryName,
}) : offlineFirstFields = OfflineFirstFields(element);
}

class OfflineFirstSupabaseModelSerdesGenerator extends SupabaseModelSerdesGenerator {
OfflineFirstSupabaseModelSerdesGenerator(
super.element,
super.reader, {
required String super.repositoryName,
});

@override
List<SerdesGenerator> get generators {
final classElement = element as ClassElement;
final fields = SupabaseFields(classElement, config);
return [
_OfflineFirstSupabaseDeserialize(classElement, fields, repositoryName: repositoryName!),
_OfflineFirstSupabaseSerialize(classElement, fields, repositoryName: repositoryName!),
];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import 'package:analyzer/dart/element/element.dart';
import 'package:brick_build/generators.dart';
import 'package:brick_offline_first_build/brick_offline_first_build.dart';
import 'package:brick_offline_first_with_supabase/brick_offline_first_with_supabase.dart';
import 'package:brick_offline_first_with_supabase_build/src/offline_first_supabase_generators.dart';
import 'package:source_gen/source_gen.dart';

class OfflineFirstWithSupabaseGenerator
extends OfflineFirstGenerator<ConnectOfflineFirstWithSupabase> {
const OfflineFirstWithSupabaseGenerator({
super.repositoryName,
super.superAdapterName,
});

/// Given an [element] and an [annotation], scaffold generators
@override
List<SerdesGenerator> buildGenerators(Element element, ConstantReader annotation) {
final supabase = OfflineFirstSupabaseModelSerdesGenerator(
element,
annotation,
repositoryName: repositoryName,
);
final sqlite =
OfflineFirstSqliteModelSerdesGenerator(element, annotation, repositoryName: repositoryName);
final generators = <SerdesGenerator>[];
generators.addAll(supabase.generators);
generators.addAll(sqlite.generators);
return generators;
}
}
Loading

0 comments on commit f7f3caa

Please sign in to comment.