diff --git a/README.md b/README.md index 180c9da9..95a8e83c 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,17 @@ An intuitive way to work with persistent data in Dart. ## [Full documentation](https://getdutchie.github.io/brick/) +- [GraphQL](https://getdutchie.github.io/brick/#/offline_first/offline_first_with_graphql_repository) +- [REST](https://getdutchie.github.io/brick/#/offline_first/offline_first_with_rest_repository) +- [Supabase](https://getdutchie.github.io/brick/#/offline_first/offline_first_with_supabase_repository?id=repository-configuration) + ## Why Brick? - Out-of-the-box [offline access](packages/brick_offline_first) to data - [Handle and hide](packages/brick_build) complex serialization/deserialization logic -- Single [access point](docs/data/repositories.md) and opinionated DSL -- Automatic, [intelligently-generated migrations](docs/sqlite.md#intelligent-migrations) -- Legible [querying interface](docs/data/query.md) +- Single [access point](https://getdutchie.github.io/brick/#/data/repositories) and opinionated DSL +- Automatic, [intelligently-generated migrations](https://getdutchie.github.io/brick/#/sqlite) +- Legible [querying interface](https://getdutchie.github.io/brick/#/data/query) ## What is Brick? @@ -37,7 +41,7 @@ Brick is an extensible query interface for Dart applications. It's an [all-in-on ``` 1. Add [models](docs/data/models.md) that contain your app logic. Models **must be** saved with the `.model.dart` suffix (i.e. `lib/brick/models/person.model.dart`). 1. Run `dart run build_runner build` to generate your models and [sometimes migrations](docs/sqlite.md#intelligent-migrations). Rerun after every new model change or `dart run build_runner watch` for automatic generations. You'll need to run this again after your first migration. -1. Extend [an existing repository](docs/data/repositories.md) or create your own: +1. Extend [an existing repository](docs/data/repositories.md) or create your own (Supabase has [some exceptions](https://getdutchie.github.io/brick/#/offline_first/offline_first_with_supabase_repository)): ```dart // lib/brick/repository.dart @@ -71,3 +75,83 @@ Brick is an extensible query interface for Dart applications. It's an [all-in-on ``` 1. Profit. + +## Usage + +Create a model as the app's business logic: + +```dart +// brick/models/user.dart +@ConnectOfflineFirstWithRest() +class User extends OfflineFirstWithRestModel {} +``` + +And generate (de)serializing code to fetch to and from multiple providers: + +```bash +$ (flutter) pub run build_runner build +``` + +### Fetching Data + +A repository fetches and returns data across multiple providers. It's the single access point for data in your app: + +```dart +class MyRepository extends OfflineFirstWithRestRepository { + MyRepository(); +} + +final repository = MyRepository(); + +// Now the models can be queried: +final users = await repository.get(); +``` + +Behind the scenes, this repository could poll a memory cache, then SQLite, then a REST API. The repository intelligently determines how and when to use each of the providers to return the fastest, most reliable data. + +```dart +// Queries can be general: +final query = Query(where: [Where('lastName').contains('Muster')]); +final users = await repository.get(query: query); + +// Or singular: +final query = Query.where('email', 'user@example.com', limit1: true); +final user = await repository.get(query: query); +``` + +Queries can also receive **reactive updates**. The subscribed stream receives all models from its query whenever the local copy is updated (e.g. when the data is hydrated in another part of the app): + +```dart +final users = repository.subscribe().listen((users) {}) +``` + +### Mutating Data + +Once a model has been created, it's sent to the repository and back out to _each_ provider: + +```dart +final user = User(); +await repository.upsert(user); +``` + +### Associating Data + +Repositories can support associations and automatic (de)serialization of child models. + +```dart +class Hat extends OfflineFirstWithRestModel { + final String color; + Hat({this.color}); +} +class User extends OfflineFirstWithRestModel { + // user has many hats + final List hats; +} + +final query = Query.where('hats', Where('color').isExactly('brown')); +final usersWithBrownHats = repository.get(query: query); +``` + +Brick natively [serializes primitives, associations, and more](packages/brick_offline_first/example/lib/brick/models/kitchen_sink.model.dart). + +If it's still murky, [check out Learn](https://getdutchie.github.io/brick/#/README?id=learn) for videos, tutorials, and examples that break down Brick. diff --git a/docs/_sidebar.md b/docs/_sidebar.md index 58328069..ffb5c7f5 100644 --- a/docs/_sidebar.md +++ b/docs/_sidebar.md @@ -8,7 +8,7 @@ - [Fetching data](data/query.md) - [Providers](data/providers.md) - [Repositories](data/repositories.md) -- [Supabase](supabase/repository.md) +- [Supabase](supabase/fields.md) - [Model Config](supabase/models.md) - [Field Config](supabase/fields.md) - [Querying](supabase/query.md) @@ -29,6 +29,7 @@ - [Offline First](offline_first/fields.md) - [Model Config](offline_first/models.md) - [Field Config](offline_first/fields.md) + - [Policies](offline_first/policies.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) diff --git a/docs/data/query.md b/docs/data/query.md index 2c27e1a7..56eeb8ea 100644 --- a/docs/data/query.md +++ b/docs/data/query.md @@ -19,19 +19,19 @@ Query.where('lastName', 'Mustermann') // note this is lastName and not name or l Querying can be done with `Where` or `WherePhrase`: -1) `WherePhrase` is a collection of `Where` statements. -2) `WherePhrase` can't contain mixed `required:` because this will output invalid SQL. For example, when it's mixed: `WHERE id = 2 AND name = 'Thomas' OR name = 'Guy'`. The OR needs to be its own phrase: `WHERE (id = 2 AND name = 'Thomas') OR (name = 'Guy')`. -3) `WherePhrase` can be intermixed with `Where`. - ```dart - [ - Where('id').isExactly(2), - WherePhrase([ - Or('name').isExactly('Guy'), - Or('name').isExactly('Thomas') - ], required: false) - ] - // => (id == 2) || (name == 'Thomas' || name == 'Guy') - ``` +1. `WherePhrase` is a collection of `Where` statements. +2. `WherePhrase` can't contain mixed `required:` because this will output invalid SQL. For example, when it's mixed: `WHERE id = 2 AND name = 'Thomas' OR name = 'Guy'`. The OR needs to be its own phrase: `WHERE (id = 2 AND name = 'Thomas') OR (name = 'Guy')`. +3. `WherePhrase` can be intermixed with `Where`. + ```dart + [ + Where('id').isExactly(2), + WherePhrase([ + Or('name').isExactly('Guy'), + Or('name').isExactly('Thomas') + ], required: false) + ] + // => (id == 2) || (name == 'Thomas' || name == 'Guy') + ``` !> Queried enum values should map to a primitive. Plainly, **always include `.index`**: `Where('type').isExactly(MyEnumType.value.index)`. @@ -57,15 +57,15 @@ Fields can be compared to their values beyond an exact match (the default). Where('name', value: 'Thomas', compare: Compare.contains); ``` -* `between` -* `contains` -* `doesNotContain` -* `exact` -* `greaterThan` -* `greaterThanOrEqualTo` -* `lessThan` -* `lessThanOrEqualTo` -* `notEqual` +- `between` +- `contains` +- `doesNotContain` +- `exact` +- `greaterThan` +- `greaterThanOrEqualTo` +- `lessThan` +- `lessThanOrEqualTo` +- `notEqual` Please note that the provider is ultimately responsible for supporting `Where` queries. @@ -97,7 +97,7 @@ Query(where: [ // => (name == 'Thomas' || age != 42) && (height > 182 && height < 186 && country == 'France') ``` -?> If expanded `WherePhrase`s become unlegible, helpers `And` and `Or` can be used: +?> If expanded `WherePhrase`s become illegible, helpers `And` and `Or` can be used: ```dart Query(where: [ diff --git a/docs/graphql/fields.md b/docs/graphql/fields.md index 90e7f652..59c3ed19 100644 --- a/docs/graphql/fields.md +++ b/docs/graphql/fields.md @@ -1,5 +1,3 @@ -?> The GraphQL domain is currently in Alpha. APIs are subject to change. - # Field Configuration ## Annotations @@ -11,7 +9,7 @@ Brick by default assumes enums from a GraphQL API will be delivered as integers Given the API: ```json -{ "user": { "hats": [ "bowler", "birthday" ] } } +{ "user": { "hats": ["bowler", "birthday"] } } ``` Simply convert `hats` into a Dart enum: @@ -80,5 +78,5 @@ query { The following are not serialized to GraphQL. However, unsupported types can still be accessed in the model as non-final fields. -* Nested `List<>` e.g. `>>` -* Many-to-many associations +- Nested `List<>` e.g. `>>` +- Many-to-many associations diff --git a/docs/graphql/query.md b/docs/graphql/query.md index 5c82d04c..c8de0aa3 100644 --- a/docs/graphql/query.md +++ b/docs/graphql/query.md @@ -1,13 +1,11 @@ -?> The GraphQL domain is currently in Alpha. APIs are subject to change. - # `Query` Configuration ## `providerArgs:` -| Name | Type | Description | -|---|---|---| -| `'operation'` | `GraphqlOperation` | apply this operation instead of one of the defaults from `graphqlOperationTransformer`. The document subfields **will not** be populated by the model. | -| `'context'` | `Map` | apply this as the context to the request instead of an empty object. Useful for subsequent consumers/`Link`s of the request. The key should be the runtime type of the `ContextEntry`. | +| Name | Type | Description | +| ------------- | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `'operation'` | `GraphqlOperation` | apply this operation instead of one of the defaults from `graphqlOperationTransformer`. The document subfields **will not** be populated by the model. | +| `'context'` | `Map` | apply this as the context to the request instead of an empty object. Useful for subsequent consumers/`Link`s of the request. The key should be the runtime type of the `ContextEntry`. | #### `variablesNamespace` @@ -47,4 +45,5 @@ final variables = { !> Association values within `Where` **are not** converted to variables !> Multiple `where` keys (`OfflineFirst(where: {'id': 'data["id"]', 'otherVar': 'data["otherVar"]'})`) or nested properties (`OfflineFirst(where: {'id': 'data["subfield"]["id"]})`) will not generate. -* `@OfflineFirst(where:` only supports extremely simple renames. Multiple `where` keys (`OfflineFirst(where: {'id': 'data["id"]', 'otherVar': 'data["otherVar"]'})`) or nested properties (`OfflineFirst(where: {'id': 'data["subfield"]["id"]})`) will be ignored. Be sure to use `@Graphql(name:)` to rename the generated document field. + +- `@OfflineFirst(where:` only supports extremely simple renames. Multiple `where` keys (`OfflineFirst(where: {'id': 'data["id"]', 'otherVar': 'data["otherVar"]'})`) or nested properties (`OfflineFirst(where: {'id': 'data["subfield"]["id"]})`) will be ignored. Be sure to use `@Graphql(name:)` to rename the generated document field. diff --git a/docs/home.md b/docs/home.md index 46b0e514..b3173f32 100644 --- a/docs/home.md +++ b/docs/home.md @@ -61,10 +61,11 @@ ## Learn -- Video: [Brick Architecture](https://www.youtube.com/watch?v=2noLcro9iIw). An explanation of Brick parlance with a supplemental analogy. +- Video: [Brick Architecture](https://www.youtube.com/watch?v=2noLcro9iIw). An explanation of Brick parlance with a [supplemental analogy](https://medium.com/flutter-community/brick-your-app-five-compelling-reasons-and-a-pizza-analogy-to-make-your-data-accessible-8d802e1e526e). - Video: [Brick Basics](https://www.youtube.com/watch?v=jm5i7e_BQq0). An overview of essential Brick mechanics. - Example: [Simple Associations using the OfflineFirstWithGraphql domain](https://github.com/GetDutchie/brick/blob/main/example_graphql) - Example: [Simple Associations using the OfflineFirstWithRest domain](https://github.com/GetDutchie/brick/blob/main/example) +- Example: [Simple Associations using the OfflineFirstWithSupabase domain](https://github.com/GetDutchie/brick/blob/main/example_supabase) - Tutorial: [Setting up a simple app with Brick](http://www.flutterbyexample.com/#/posts/2_adding_a_repository) ## Glossary diff --git a/docs/index.html b/docs/index.html index 42d9afce..d12afde2 100644 --- a/docs/index.html +++ b/docs/index.html @@ -9,10 +9,10 @@ - + diff --git a/docs/introduction/usage.md b/docs/introduction/usage.md index eee9b0f1..faefeb95 100644 --- a/docs/introduction/usage.md +++ b/docs/introduction/usage.md @@ -41,12 +41,10 @@ final query = Query.where('email', 'user@example.com', limit1: true); final user = await repository.get(query: query); ``` -For continuous updates, queries can also be streams. The stream receives all models from its query whenever the local copy is updated: +Queries can also receive **reactive updates**. The subscribed stream receives all models from its query whenever the local copy is updated (e.g. when the data is hydrated in another part of the app): ```dart -final subscription repository.subscribe().listen((users) { - -}) +final users = repository.subscribe().listen((users) {}) ``` ## Mutating Data diff --git a/docs/offline_first/offline_first_with_graphql_repository.md b/docs/offline_first/offline_first_with_graphql_repository.md index 1742d31a..3b31104a 100644 --- a/docs/offline_first/offline_first_with_graphql_repository.md +++ b/docs/offline_first/offline_first_with_graphql_repository.md @@ -1,5 +1,3 @@ -?> The GraphQL domain is currently in Alpha. APIs are subject to change. - # Offline First With GraphQL Repository `OfflineFirstWithGraphqlRepository` streamlines the GraphQL integration with an `OfflineFirstRepository`. A serial queue is included to track GraphQL mutations in a separate SQLite database, only removing requests when a response is returned from the host (i.e. the device has lost internet connectivity). diff --git a/docs/offline_first/offline_first_with_supabase_repository.md b/docs/offline_first/offline_first_with_supabase_repository.md index bcb1399e..9177f721 100644 --- a/docs/offline_first/offline_first_with_supabase_repository.md +++ b/docs/offline_first/offline_first_with_supabase_repository.md @@ -74,7 +74,7 @@ When using [supabase_flutter](https://pub.dev/packages/supabase_flutter), create ```dart final (client, queue) = OfflineFirstWithSupabaseRepository.clientQueue(databaseFactory: databaseFactory); -await Supabase.initialize(httpClient: client) +await Supabase.initialize(url: supabaseUrl, anonKey: supabaseAnonKey, httpClient: client) final supabaseProvider = SupabaseProvider(Supabase.instance.client, modelDictionary: ...) ``` diff --git a/docs/offline_first/policies.md b/docs/offline_first/policies.md new file mode 100644 index 00000000..cb729d65 --- /dev/null +++ b/docs/offline_first/policies.md @@ -0,0 +1,41 @@ +# Offline First Policies + +Repository methods can be invoked with policies to prioritize data sources. For example, a request may need to skip the offline queue or the response must come from a remote source. It is strongly encouraged to use a policy (e.g. `Repository().get(policy: .requireRemote))`) instead of directly accessing a provider (e.g. `Repository().restPovider.get()`) + +## OfflineFirstDeletePolicy + +### `optimisticLocal` + +Delete local results before waiting for the remote provider to respond + +### `requireRemote` + +Delete local results after remote responds; local results are not deleted if remote responds with any exception + +## OfflineFirstGetPolicy + +### `alwaysHydrate` + +Ensures data is fetched from the remote provider(s) at each invocation. This hydration is unawaited and is not guaranteed to complete before results are returned. This can be expensive to perform for some queries; see [`awaitRemoteWhenNoneExist`](#awaitremotewhennoneexist) for a more performant option or [`awaitRemote`](#awaitremote) to await the hydration before returning results. + +### `awaitRemote` + +Ensures results must be updated from the remote proivder(s) before returning if the app is online. An empty array will be returned if the app is offline. + +### `awaitRemoteWhenNoneExist` + +Retrieves from the remote provider(s) if the query returns no results from the local provider(s). + +### `localOnly` + +Do not request from the remote provider(s) + +## OfflineFirstUpsertPolicy + +### `optimisticLocal` + +Save results to local before waiting for the remote provider to respond + +### `requireRemote` + +Save results to local after remote responds; local results are not saved if remote responds with any exception diff --git a/example_supabase/lib/brick/models/customer.model.dart b/example_supabase/lib/brick/models/customer.model.dart index aa659e7d..433d28bc 100644 --- a/example_supabase/lib/brick/models/customer.model.dart +++ b/example_supabase/lib/brick/models/customer.model.dart @@ -1,10 +1,7 @@ import 'package:brick_offline_first_with_supabase/brick_offline_first_with_supabase.dart'; import 'package:brick_sqlite/brick_sqlite.dart'; -import 'package:brick_supabase/brick_supabase.dart'; -@ConnectOfflineFirstWithSupabase( - supabaseConfig: SupabaseSerializable(), -) +@ConnectOfflineFirstWithSupabase() class Customer extends OfflineFirstWithSupabaseModel { @Sqlite(unique: true) final String id; diff --git a/packages/brick_offline_first_with_supabase/README.md b/packages/brick_offline_first_with_supabase/README.md index dc387aa1..296976e9 100644 --- a/packages/brick_offline_first_with_supabase/README.md +++ b/packages/brick_offline_first_with_supabase/README.md @@ -60,7 +60,7 @@ When using [supabase_flutter](https://pub.dev/packages/supabase_flutter), create ```dart final (client, queue) = OfflineFirstWithSupabaseRepository.clientQueue(databaseFactory: databaseFactory); -await Supabase.initialize(httpClient: client) +await Supabase.initialize(url: supabaseUrl, anonKey: supabaseAnonKey, httpClient: client) final supabaseProvider = SupabaseProvider(Supabase.instance.client, modelDictionary: ...) ```