diff --git a/MIGRATING.md b/MIGRATING.md index ee8b70c9..ef8ead23 100644 --- a/MIGRATING.md +++ b/MIGRATING.md @@ -1,5 +1,60 @@ # Migrating Between Major Versions +In preparation for Brick 4, `Query` is migrating away from loosely-defined arguments in favor of standardized fields that can be easily deprecated and discovered by analysis. + +**`providerArgs` will be supported until Brick 4 is officially released**. + +It is still recommended you migrate to the new `Query` for new features and long-term support. + +## Improvements + +- `Query#orderBy` will support association ordering and multiple values +- `Query` is constructed with `const` +- `Query#offset` no longer requires companion `limit` parameter +- `brick_sqlite` and `brick_supabase` support association ordering. For example, `Query(orderBy: [OrderBy.desc('assoc', associationField: 'name')])` on `DemoModel` will produce the following SQL statement: + ```sql + 'SELECT DISTINCT `DemoModel`.* FROM `DemoModel` ORDER BY `DemoModelAssoc`.name DESC' + ``` +- `brick_supabase` supports advanced limiting. For example, `Query(limitBy: [LimitBy(1, evaluatedField: 'assoc'))` is the equivalent of `.limit(1, referencedTable: 'demo_model')` + +## Universal Deprecations + +| Old | New | Notes | +| ----------------------------------- | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `Query(providerArgs: {'limit':})` | `Query(limit:)` | `limit` and `limitBy` may be used together, however, `limitBy` will only limit `evaluatedField:` associations | +| `Query(providerArgs: {'offset':})` | `Query(offset:)` | | +| `Query(providerArgs: {'orderBy':})` | `Query(orderBy:)` | `orderBy` is now defined by a class that permits multiple commands. For example, `'orderBy': 'name ASC'` becomes `[OrderBy('name', ascending: true)]`. First-class Brick providers (SQLite and Supabase) also support association-based querying by declaring a `associationField:`. This `associationField` is optional in Supabase but required for `SQLite`. | + +## Package-specific deprecations + +### brick_graphql + +| Old | New | Notes | +| ------------------------------------- | --------------------------------------------------------- | ----- | +| `Query(providerArgs: {'context':})` | `Query(forProviders: [GraphqlProviderQuery(context:)])` | +| `Query(providerArgs: {'operation':})` | `Query(forProviders: [GraphqlProviderQuery(operation:)])` | + +### brick_rest + +| Old | New | Notes | +| ----------------------------------- | ---------------------------------------------------- | ----- | +| `Query(providerArgs: {'request':})` | `Query(forProviders: [RestProviderQuery(request:)])` | | + +### brick_sqlite + +| Old | New | Notes | +| ----------------------------------- | ------------------------------------------------------ | ----- | +| `Query(providerArgs: {'collate':})` | `Query(forProviders: [SqliteProviderQuery(collate:)])` | +| `Query(providerArgs: {'having':})` | `Query(forProviders: [SqliteProviderQuery(having:)])` | +| `Query(providerArgs: {'groupBy':})` | `Query(forProviders: [SqliteProviderQuery(groupBy:)])` | + +### brick_supabase + +| Old | New | Notes | +| -------------------------------------------------- | --- | ------------------------------------- | +| `Query(providerArgs: {'limitReferencedTable':})` | | Removed in favor of `Query(limitBy:)` | +| `Query(providerArgs: {'orderByReferencedTable':})` | | Removed in favor of `Query(orderBy:)` | + ## Migrating from Brick 2 to Brick 3 Brick 3 removes the abstract packages since Sqflite has abstracted its Flutter dependency to [a "common" API](https://pub.dev/packages/sqflite_common). @@ -8,51 +63,51 @@ Brick 3 removes the abstract packages since Sqflite has abstracted its Flutter d ### Breaking Changes -* Primary package files are renamed in line with `pub.dev` standards. - ```shell - for FILE in $(find "lib" -type f -name "*.dart"); do - sed -i '' 's/package:brick_offline_first\/offline_first.dart/package:brick_offline_first\/brick_offline_first.dart/g' $FILE - sed -i '' 's/package:brick_offline_first_with_rest\/offline_first_with_rest.dart/package:brick_offline_first_with_rest\/brick_offline_first_with_rest.dart/g' $FILE - sed -i '' 's/package:brick_offline_first_with_graphql\/offline_first_with_graphql.dart/package:brick_offline_first_with_graphql\/brick_offline_first_with_graphql.dart/g' $FILE - sed -i '' 's/package:brick_rest\/rest.dart/package:brick_rest\/brick_rest.dart/g' $FILE - sed -i '' 's/package:brick_sqlite\/sqlite.dart/package:brick_sqlite\/brick_sqlite.dart/g' $FILE - sed -i '' 's/package:brick_graphql\/graphql.dart/package:brick_graphql\/brick_graphql.dart/g' $FILE - done - ``` - * `brick_offline_first/offline_first.dart` is now `brick_offline_first/brick_offline_first.dart` - * `brick_offline_first_with_rest/offline_first_with_rest.dart` is now `brick_offline_first_with_rest/brick_offline_first_with_rest.dart` - * `brick_offline_first_with_graphql/brick_offline_first_with_graphql.dart` is now `brick_offline_first_with_graphql/brick_offline_first_with_graphql.dart` - * `brick_graphql/graphql.dart` is now `brick_rest/brick_graphql.dart` - * `brick_rest/rest.dart` is now `brick_rest/brick_rest.dart` - * `brick_sqlite/sqlite.dart` is now `brick_sqlite/brick_sqlite.dart` -* `brick_sqlite_abstract/db.dart` is now `brick_sqlite/db.dart`. `brick_sqlite_abstract/sqlite_model.dart` and `brick_sqlite_abstract/annotations.dart` are now exported by `brick_sqlite/brick_sqlite.dart` - ```shell - for FILE in $(find "lib" -type f -name "*.dart"); do - sed -i '' 's/package:brick_sqlite_abstract\/annotations.dart/package:brick_sqlite\/brick_sqlite.dart/g' $FILE - sed -i '' 's/package:brick_sqlite_abstract\/sqlite_model.dart/package:brick_sqlite\/brick_sqlite.dart/g' $FILE - sed -i '' 's/package:brick_sqlite_abstract\/db.dart/package:brick_sqlite\/db.dart/g' $FILE - done - ``` -* `RestAdapter.firstWhereOrNull` and `GraphqlAdapter.firstWhereOrNull` have been removed. Instead `import 'package:collection/collection.dart';` and use the bundled `Iterable` extension `.firstWhereOrNull` -* `RestAdapter.enumValuesByName` and `GraphqlAdapter.enumValuesByName` have been removed. Instead use Dart 2.15's built-in `.values.byName` -* The minimum Dart version has been increased to 2.18 -* `providerArgs` in Brick Rest have changed: `'topLevelKey'` and `'headers'` and `'supplementalTopLevelData'` have been removed (use `'request'`) and `'request'` now accepts a `RestRequest` instead of the HTTP method string. -* `providerArgs` in Brick Graphql have changed: `'document'` and `'variables'` have been removed. Instead, use `'operation'`. -* `analyzer` is now `>= 5` +- Primary package files are renamed in line with `pub.dev` standards. + ```shell + for FILE in $(find "lib" -type f -name "*.dart"); do + sed -i '' 's/package:brick_offline_first\/offline_first.dart/package:brick_offline_first\/brick_offline_first.dart/g' $FILE + sed -i '' 's/package:brick_offline_first_with_rest\/offline_first_with_rest.dart/package:brick_offline_first_with_rest\/brick_offline_first_with_rest.dart/g' $FILE + sed -i '' 's/package:brick_offline_first_with_graphql\/offline_first_with_graphql.dart/package:brick_offline_first_with_graphql\/brick_offline_first_with_graphql.dart/g' $FILE + sed -i '' 's/package:brick_rest\/rest.dart/package:brick_rest\/brick_rest.dart/g' $FILE + sed -i '' 's/package:brick_sqlite\/sqlite.dart/package:brick_sqlite\/brick_sqlite.dart/g' $FILE + sed -i '' 's/package:brick_graphql\/graphql.dart/package:brick_graphql\/brick_graphql.dart/g' $FILE + done + ``` + - `brick_offline_first/offline_first.dart` is now `brick_offline_first/brick_offline_first.dart` + - `brick_offline_first_with_rest/offline_first_with_rest.dart` is now `brick_offline_first_with_rest/brick_offline_first_with_rest.dart` + - `brick_offline_first_with_graphql/brick_offline_first_with_graphql.dart` is now `brick_offline_first_with_graphql/brick_offline_first_with_graphql.dart` + - `brick_graphql/graphql.dart` is now `brick_rest/brick_graphql.dart` + - `brick_rest/rest.dart` is now `brick_rest/brick_rest.dart` + - `brick_sqlite/sqlite.dart` is now `brick_sqlite/brick_sqlite.dart` +- `brick_sqlite_abstract/db.dart` is now `brick_sqlite/db.dart`. `brick_sqlite_abstract/sqlite_model.dart` and `brick_sqlite_abstract/annotations.dart` are now exported by `brick_sqlite/brick_sqlite.dart` + ```shell + for FILE in $(find "lib" -type f -name "*.dart"); do + sed -i '' 's/package:brick_sqlite_abstract\/annotations.dart/package:brick_sqlite\/brick_sqlite.dart/g' $FILE + sed -i '' 's/package:brick_sqlite_abstract\/sqlite_model.dart/package:brick_sqlite\/brick_sqlite.dart/g' $FILE + sed -i '' 's/package:brick_sqlite_abstract\/db.dart/package:brick_sqlite\/db.dart/g' $FILE + done + ``` +- `RestAdapter.firstWhereOrNull` and `GraphqlAdapter.firstWhereOrNull` have been removed. Instead `import 'package:collection/collection.dart';` and use the bundled `Iterable` extension `.firstWhereOrNull` +- `RestAdapter.enumValuesByName` and `GraphqlAdapter.enumValuesByName` have been removed. Instead use Dart 2.15's built-in `.values.byName` +- The minimum Dart version has been increased to 2.18 +- `providerArgs` in Brick Rest have changed: `'topLevelKey'` and `'headers'` and `'supplementalTopLevelData'` have been removed (use `'request'`) and `'request'` now accepts a `RestRequest` instead of the HTTP method string. +- `providerArgs` in Brick Graphql have changed: `'document'` and `'variables'` have been removed. Instead, use `'operation'`. +- `analyzer` is now `>= 5` ### Brick Offline First with Graphql -* `FieldRename`, `Graphql` `GraphqlProvider`, and `GraphqlSerializable` are no longer exported by `offline_first_with_graphql.dart`. Instead, import these file from `package:brick_graphql/brick_graphql.dart` +- `FieldRename`, `Graphql` `GraphqlProvider`, and `GraphqlSerializable` are no longer exported by `offline_first_with_graphql.dart`. Instead, import these file from `package:brick_graphql/brick_graphql.dart` ### Brick Offline First with Rest -* `FieldRename`, `Rest`, `RestProvider`, and `RestSerializable` are no longer exported by `offline_first_with_rest.dart`. Instead, import these file from `package:brick_rest/brick_rest.dart` -* `OfflineFirstWithRestRepository#reattemptForStatusCodes` has been removed from instance-level access. The constructor argument forwards to the `RestOfflineQueueClient`, where it can be accessed if needed. -* `OfflineFirstWithRestRepository#throwTunnerNotFoundExceptions` has been removed. This value was duplicated from `offlineQueueManager`; the queue manager is where the property exclusively lives now. +- `FieldRename`, `Rest`, `RestProvider`, and `RestSerializable` are no longer exported by `offline_first_with_rest.dart`. Instead, import these file from `package:brick_rest/brick_rest.dart` +- `OfflineFirstWithRestRepository#reattemptForStatusCodes` has been removed from instance-level access. The constructor argument forwards to the `RestOfflineQueueClient`, where it can be accessed if needed. +- `OfflineFirstWithRestRepository#throwTunnerNotFoundExceptions` has been removed. This value was duplicated from `offlineQueueManager`; the queue manager is where the property exclusively lives now. #### Improvements -* Listen for SQLite changes via `OfflineFirstWithRestRepository#subscribe` +- Listen for SQLite changes via `OfflineFirstWithRestRepository#subscribe` ### Brick Graphql @@ -112,8 +167,8 @@ This has been consolidated to `'request'`. For example: `providerArgs: { 'reques #### `RestSerializable(requestTransformer:)` -* `RestSerializable`'s `fromKey` and `toKey` have been consolidated to `RestRequest(topLevelKey:)` -* `RestSerializable(endpoint:)` has been replaced in this release by `RestSerializable(requestTransformer:)`. It will be painful to upgrade though with good reason. +- `RestSerializable`'s `fromKey` and `toKey` have been consolidated to `RestRequest(topLevelKey:)` +- `RestSerializable(endpoint:)` has been replaced in this release by `RestSerializable(requestTransformer:)`. It will be painful to upgrade though with good reason. 1. Strongly-typed classes. `endpoint` was a string, which removed analysis in IDEs, permitting errors to escape during runtime. With endpoints as classes, `Query` and `instance` objects will receive type hinting. 1. Fine control over REST requests. Define on a request-level basis what key to pull from or push to. Declare specific HTTP methods like `PATCH` in a class that manages request instead of in distributed `providerArgs`. @@ -191,55 +246,55 @@ Brick 2 focuses on Brick problems encountered at scale. While the primary refact ### Breaking Changes -* Brick no longer expects `lib/app`; it now expects `lib/brick`. - ```shell - mv -r lib/app lib/brick - ``` -* Models are no longer discovered in `lib/app/models`; they are now discovered via `*.model.dart`. They can live in any directory within `lib` and have any prefix. (#38) - ```shell - for FILENAME in lib/brick/models/*; do mv $FILENAME "${FILENAME/dart/model.dart}"; done - ``` -* `brick_offline_first` is now, fundamentally, `brick_offline_first_with_rest`. `brick_offline_first` now serves as an abstract bedrock for offline domains. - ```shell - sed -i '' 's/brick_offline_first:/brick_offline_first_with_rest:/g' pubspec.yaml - for FILE in $(find "lib" -type f -name "*.dart"); do sed -i '' 's/package:brick_offline_first/package:brick_offline_first_with_rest/g' $FILE; done - ``` -* `brick_offline_first_abstract` is now `brick_offline_first_with_rest_abstract` - ```shell - sed -i '' 's/brick_offline_first_abstract:/brick_offline_first_with_rest_abstract:/g' pubspec.yaml - for FILE in $(find "lib" -type f -name "*.dart"); do sed -i '' 's/package:brick_offline_first_abstract/package:brick_offline_first_with_rest_abstract/g' $FILE; done - ``` -* `rest` properties have been removed from `OfflineFirstException`. Use `OfflineFirstWithRestException` instead from `brick_offline_first_with_rest`. -* `OfflineFirstRepository#get(requireRemote:` and `OfflineFirstRepository#getBatched(requireRemote:` has been removed. Instead, use `policy: OfflineFirstGetPolicy.alwaysHydrate` -* `OfflineFirstRepository#get(hydrateUnexisting:` has been removed. Instead, use `policy: OfflineFirstGetPolicy.awaitRemoteWhenNoneExist` (this is the default). -* `OfflineFirstRepository#get(alwaysHydrate:` has been removed. Instead, use `policy: OfflineFirstGetPolicy.alwaysHydrate`. +- Brick no longer expects `lib/app`; it now expects `lib/brick`. + ```shell + mv -r lib/app lib/brick + ``` +- Models are no longer discovered in `lib/app/models`; they are now discovered via `*.model.dart`. They can live in any directory within `lib` and have any prefix. (#38) + ```shell + for FILENAME in lib/brick/models/*; do mv $FILENAME "${FILENAME/dart/model.dart}"; done + ``` +- `brick_offline_first` is now, fundamentally, `brick_offline_first_with_rest`. `brick_offline_first` now serves as an abstract bedrock for offline domains. + ```shell + sed -i '' 's/brick_offline_first:/brick_offline_first_with_rest:/g' pubspec.yaml + for FILE in $(find "lib" -type f -name "*.dart"); do sed -i '' 's/package:brick_offline_first/package:brick_offline_first_with_rest/g' $FILE; done + ``` +- `brick_offline_first_abstract` is now `brick_offline_first_with_rest_abstract` + ```shell + sed -i '' 's/brick_offline_first_abstract:/brick_offline_first_with_rest_abstract:/g' pubspec.yaml + for FILE in $(find "lib" -type f -name "*.dart"); do sed -i '' 's/package:brick_offline_first_abstract/package:brick_offline_first_with_rest_abstract/g' $FILE; done + ``` +- `rest` properties have been removed from `OfflineFirstException`. Use `OfflineFirstWithRestException` instead from `brick_offline_first_with_rest`. +- `OfflineFirstRepository#get(requireRemote:` and `OfflineFirstRepository#getBatched(requireRemote:` has been removed. Instead, use `policy: OfflineFirstGetPolicy.alwaysHydrate` +- `OfflineFirstRepository#get(hydrateUnexisting:` has been removed. Instead, use `policy: OfflineFirstGetPolicy.awaitRemoteWhenNoneExist` (this is the default). +- `OfflineFirstRepository#get(alwaysHydrate:` has been removed. Instead, use `policy: OfflineFirstGetPolicy.alwaysHydrate`. ### Fun Changes -* Utilize `OfflineFirstDeletePolicy`, `OfflineFirstGetPolicy`, and `OfflineFirstUpsertPolicy` to override default behavior. Specific policies will throw an exception when the remote responds with an error (and throw that error) or skip the queue. Existing default behavior is maintained. -* `OfflineFirstRepository#delete` now supports requiring a successful remote with `OfflineFirstDeletePolicy.requireRemote`. If the app is offline, normally handled exceptions (`ClientException` and `SocketException`) are `rethrow`n. (#182) -* `OfflineFirstRepository#upsert` now supports requiring a successful remote with `OfflineFirstUpsertPolicy.requireRemote`. If the app is offline, normally handled exceptions (`ClientException` and `SocketException`) are `rethrow`n. +- Utilize `OfflineFirstDeletePolicy`, `OfflineFirstGetPolicy`, and `OfflineFirstUpsertPolicy` to override default behavior. Specific policies will throw an exception when the remote responds with an error (and throw that error) or skip the queue. Existing default behavior is maintained. +- `OfflineFirstRepository#delete` now supports requiring a successful remote with `OfflineFirstDeletePolicy.requireRemote`. If the app is offline, normally handled exceptions (`ClientException` and `SocketException`) are `rethrow`n. (#182) +- `OfflineFirstRepository#upsert` now supports requiring a successful remote with `OfflineFirstUpsertPolicy.requireRemote`. If the app is offline, normally handled exceptions (`ClientException` and `SocketException`) are `rethrow`n. ### New Packages -* [`brick_graphql`](../packages/brick_graphql). The `GraphqlProvider` interfaces with a GraphQL backend. It uses [gql's Link system](https://github.com/gql-dart/gql/tree/master/links) to integrate with other community-supported functionality. That, and all your variables are autogenerated on every request. -* [`brick_graphql_generators`](../packages/brick_graphql_generators). The perfect companion to `brick_graphql`, this thin layer around `brick_rest_generators` battle-tested core compiles adapters for the GraphQL domain. -* [`brick_json_generators`](../packages/brick_json_generators). The experienced core separated from `brick_rest_generators` permits more code reuse and package creation for JSON-serving remote providers. -* [`brick_offline_first_build`](../packages/brick_offline_first_build). Abstracted from the experienced core of `brick_offline_first_with_rest_build`, these helper generators and utils simplify adding offline capabilites to a domain. -* [`brick_offline_first_with_graphql`](../packages/brick_offline_first_with_graphql). Utilize the GraphQL provider with SQLite and Memory cache. This is a near mirror of `brick_offline_first_with_rest`, save for a few exceptions. First, the OfflineQueueLink must be inserted in the appropriate position in [your client's Link chain](../packages/brick_offline_first_with_graphql#GraphqlOfflineQueueLink). Second, `OfflineFirstWithGraphqlRepository#subscribe` permits streaming updates, including notifications after local providers are updated. -* [`brick_offline_first_with_graphql_abstract`](../packages/brick_offline_first_with_graphql_abstract). Annotations for the GraphQL domain without including Flutter. -* [`brick_offline_first_with_graphql_build`](../packages/brick_offline_first_with_graphql_build). The culmination of `brick_graphql_generators` and `brick_offline_first_build`. +- [`brick_graphql`](../packages/brick_graphql). The `GraphqlProvider` interfaces with a GraphQL backend. It uses [gql's Link system](https://github.com/gql-dart/gql/tree/master/links) to integrate with other community-supported functionality. That, and all your variables are autogenerated on every request. +- [`brick_graphql_generators`](../packages/brick_graphql_generators). The perfect companion to `brick_graphql`, this thin layer around `brick_rest_generators` battle-tested core compiles adapters for the GraphQL domain. +- [`brick_json_generators`](../packages/brick_json_generators). The experienced core separated from `brick_rest_generators` permits more code reuse and package creation for JSON-serving remote providers. +- [`brick_offline_first_build`](../packages/brick_offline_first_build). Abstracted from the experienced core of `brick_offline_first_with_rest_build`, these helper generators and utils simplify adding offline capabilites to a domain. +- [`brick_offline_first_with_graphql`](../packages/brick_offline_first_with_graphql). Utilize the GraphQL provider with SQLite and Memory cache. This is a near mirror of `brick_offline_first_with_rest`, save for a few exceptions. First, the OfflineQueueLink must be inserted in the appropriate position in [your client's Link chain](../packages/brick_offline_first_with_graphql#GraphqlOfflineQueueLink). Second, `OfflineFirstWithGraphqlRepository#subscribe` permits streaming updates, including notifications after local providers are updated. +- [`brick_offline_first_with_graphql_abstract`](../packages/brick_offline_first_with_graphql_abstract). Annotations for the GraphQL domain without including Flutter. +- [`brick_offline_first_with_graphql_build`](../packages/brick_offline_first_with_graphql_build). The culmination of `brick_graphql_generators` and `brick_offline_first_build`. ## Migrating to Brick 1 (Null Safety) ### Breaking Changes -* Because `required` is now a reserved Dart keyword, `required` in `WherePhrase`, `WhereCondition`, `And`, `Or`, and `Where` has been renamed to `isRequired`. -* Field types in models `Set>`, `List>`, and `Future` are no longer supported. Instead, use `Set`, `List`, and `OfflineFirstModel` (the adapters will `await` each). -* `StubOfflineFirstWithRest` is functionally changed. SQLiteFFI has satisfied much of the original stubbing required for this class, and http's testing.dart library is sufficient to not require Mockito. Therefore, `verify` calls will no longer be effective in testing on the client. Instead, pass `StubOfflineFirstWithRest.client` to your `RestProvider#client` with the response values. `StubOfflineFirstWithRestModel` has been removed. Please review [Offline First Testing](https://greenbits.github.io/brick/#/offline_first/testing) for implementation examples. +- Because `required` is now a reserved Dart keyword, `required` in `WherePhrase`, `WhereCondition`, `And`, `Or`, and `Where` has been renamed to `isRequired`. +- Field types in models `Set>`, `List>`, and `Future` are no longer supported. Instead, use `Set`, `List`, and `OfflineFirstModel` (the adapters will `await` each). +- `StubOfflineFirstWithRest` is functionally changed. SQLiteFFI has satisfied much of the original stubbing required for this class, and http's testing.dart library is sufficient to not require Mockito. Therefore, `verify` calls will no longer be effective in testing on the client. Instead, pass `StubOfflineFirstWithRest.client` to your `RestProvider#client` with the response values. `StubOfflineFirstWithRestModel` has been removed. Please review [Offline First Testing](https://greenbits.github.io/brick/#/offline_first/testing) for implementation examples. ### Improvements -* `brick_offline_first`: Priority for the next job to process from the queue - when processing requests in serial - has changed from `'$HTTP_JOBS_CREATED_AT_COLUMN ASC, $HTTP_JOBS_ATTEMPTS_COLUMN DESC, $HTTP_JOBS_UPDATED_AT ASC'` to `'$HTTP_JOBS_CREATED_AT_COLUMN ASC'`; this uses the job column introduced in 0.0.7 (26 May 2020) and will not affect any implementations using 0.0.7 or higher. -* `brick_offline_first`: `RequestSqliteCache` no longer queries cached requests based on headers; requests are rediscovered based on their encoding, URL, request method, and body. Rehydrated (reattempted) requests will be hydrated with headers from the original request. -* Every package is null safe. There is one outstanding dependency - `build_config` - that needs to be migrated, so the generators are not technically "null safe". However, these are dev dependencies and `build_config` isn't imported into Dart code, so upgrading it will be changing numbers. +- `brick_offline_first`: Priority for the next job to process from the queue - when processing requests in serial - has changed from `'$HTTP_JOBS_CREATED_AT_COLUMN ASC, $HTTP_JOBS_ATTEMPTS_COLUMN DESC, $HTTP_JOBS_UPDATED_AT ASC'` to `'$HTTP_JOBS_CREATED_AT_COLUMN ASC'`; this uses the job column introduced in 0.0.7 (26 May 2020) and will not affect any implementations using 0.0.7 or higher. +- `brick_offline_first`: `RequestSqliteCache` no longer queries cached requests based on headers; requests are rediscovered based on their encoding, URL, request method, and body. Rehydrated (reattempted) requests will be hydrated with headers from the original request. +- Every package is null safe. There is one outstanding dependency - `build_config` - that needs to be migrated, so the generators are not technically "null safe". However, these are dev dependencies and `build_config` isn't imported into Dart code, so upgrading it will be changing numbers. diff --git a/analysis_options.yaml b/analysis_options.yaml index 31d83698..870b54ad 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,51 +1,234 @@ include: package:lints/recommended.yaml -# Additional information about this file can be found at -# https://dart.dev/guides/language/analysis-options - linter: rules: + # This list is derived from the list of all available lints located at + # https://github.com/dart-lang/linter/blob/master/example/all.yaml - always_declare_return_types + # - always_put_control_body_on_new_line + # - always_put_required_named_parameters_first + # - always_specify_types - always_use_package_imports + - annotate_overrides + # - avoid_annotating_with_dynamic - avoid_bool_literals_in_conditional_expressions + # - avoid_catches_without_on_clauses + # - avoid_catching_errors + - avoid_classes_with_only_static_members + - avoid_double_and_int_checks + # - avoid_dynamic_calls + - avoid_empty_else + # - avoid_equals_and_hash_code_on_mutable_classes - avoid_escaping_inner_quotes + - avoid_field_initializers_in_const_classes + # - avoid_final_parameters + - avoid_function_literals_in_foreach_calls + - avoid_implementing_value_types + - avoid_init_to_null + - avoid_js_rounded_ints + - avoid_multiple_declarations_per_line + - avoid_null_checks_in_equality_operators + # - avoid_positional_boolean_parameters + - avoid_print + - avoid_private_typedef_functions + - avoid_redundant_argument_values + - avoid_relative_lib_imports + - avoid_renaming_method_parameters + - avoid_return_types_on_setters + - avoid_returning_null_for_void + - avoid_returning_this + - avoid_setters_without_getters + - avoid_shadowing_type_parameters + - avoid_single_cascade_in_expression_statements + - avoid_slow_async_io + - avoid_type_to_string + - avoid_types_as_parameter_names + # - avoid_types_on_closure_parameters + - avoid_unnecessary_containers + - avoid_unused_constructor_parameters - avoid_void_async + - avoid_web_libraries_in_flutter + - await_only_futures - camel_case_extensions - camel_case_types + - cancel_subscriptions + - cascade_invocations + - cast_nullable_to_non_nullable + # - close_sinks + - collection_methods_unrelated_type + - combinators_ordering + - comment_references + - conditional_uri_does_not_exist + - constant_identifier_names + - control_flow_in_finally - curly_braces_in_flow_control_structures + - dangling_library_doc_comments + - depend_on_referenced_packages + - deprecated_consistency + - deprecated_member_use_from_same_package + # - diagnostic_describe_all_properties - directives_ordering - discarded_futures + - do_not_use_environment + - empty_catches + - empty_constructor_bodies + - empty_statements - eol_at_end_of_file + - exhaustive_cases - file_names + - flutter_style_todos + - hash_and_equals + - implementation_imports + - implicit_call_tearoffs + - implicit_reopen - invalid_case_patterns + - join_return_with_assignment + # - leading_newlines_in_multiline_strings + - library_annotations + - library_names + - library_prefixes + - library_private_types_in_public_api + # - lines_longer_than_80_chars + - literal_only_boolean_expressions - matching_super_parameters + - missing_whitespace_between_adjacent_strings + - no_adjacent_strings_in_list + - no_default_cases + - no_duplicate_case_values + - no_leading_underscores_for_library_prefixes + - no_leading_underscores_for_local_identifiers - no_literal_bool_comparisons + - no_logic_in_create_state + - no_runtimeType_toString + - no_self_assignments + - no_wildcard_variable_uses + - non_constant_identifier_names + - noop_primitive_operations + - null_check_on_nullable_type_parameter + - null_closures + - omit_local_variable_types + - one_member_abstracts + - only_throw_errors + - overridden_fields + - package_api_docs + - package_names + - package_prefixed_library_names + - parameter_assignments + - prefer_adjacent_string_concatenation + - prefer_asserts_in_initializer_lists + - prefer_asserts_with_message + - prefer_collection_literals + - prefer_conditional_assignment + - prefer_const_constructors + - prefer_const_constructors_in_immutables + - prefer_const_declarations + - prefer_const_literals_to_create_immutables + - prefer_constructors_over_static_methods + - prefer_contains + # - prefer_double_quotes + # - prefer_expression_function_bodies + - prefer_final_fields + - prefer_final_in_for_each - prefer_final_locals + # - prefer_final_parameters + - prefer_for_elements_to_map_fromIterable + - prefer_foreach + - prefer_function_declarations_over_variables + - prefer_generic_function_type_aliases + - prefer_if_elements_to_conditional_expressions + - prefer_if_null_operators + - prefer_initializing_formals + - prefer_inlined_adds + - prefer_int_literals + - prefer_interpolation_to_compose_strings - prefer_is_empty - prefer_is_not_empty + - prefer_is_not_operator + - prefer_iterable_whereType + - prefer_mixin + - prefer_null_aware_method_calls + - prefer_null_aware_operators + # - prefer_relative_imports - prefer_single_quotes + - prefer_spread_collections + - prefer_typing_uninitialized_variables + - prefer_void_to_null + - provide_deprecation_message + - public_member_api_docs + - recursive_getters - require_trailing_commas + - secure_pubspec_urls + - sized_box_for_whitespace + - sized_box_shrink_expand + - slash_for_doc_comments + - sort_child_properties_last + # - sort_constructors_first + - sort_pub_dependencies + - sort_unnamed_constructors_first + - test_types_in_equals + - throw_in_finally + - tighten_type_of_initializing_formals + - type_annotate_public_apis + - type_init_formals - type_literal_in_constant_pattern - unawaited_futures + # - unnecessary_await_in_return + - unnecessary_brace_in_string_interps + - unnecessary_breaks + - unnecessary_const + - unnecessary_constructor_name + # - unnecessary_final + - unnecessary_getters_setters - unnecessary_lambdas + - unnecessary_late + - unnecessary_library_directive + - unnecessary_new + - unnecessary_null_aware_assignments + - unnecessary_null_aware_operator_on_extension_on_nullable + - unnecessary_null_checks + - unnecessary_null_in_if_null_operators + - unnecessary_nullable_for_final_variable_declarations + - unnecessary_overrides - unnecessary_parenthesis - unnecessary_raw_strings - unnecessary_statements + - unnecessary_string_escapes + - unnecessary_string_interpolations + - unnecessary_this - unnecessary_to_list_in_spreads + # - unreachable_from_main + - unrelated_type_equality_checks + - unsafe_html + - use_build_context_synchronously + - use_colored_box + - use_decorated_box + - use_enums + - use_full_hex_values_for_flutter_colors + - use_function_type_syntax_for_parameters - use_if_null_to_convert_nulls_to_bools + - use_is_even_rather_than_modulo + - use_key_in_widget_constructors + - use_late_for_private_fields_and_variables + - use_named_constants - use_raw_strings + - use_rethrow_when_possible + - use_setters_to_change_properties + - use_string_buffers + - use_string_in_part_of_directives - use_super_parameters + - use_test_throws_matchers - use_to_and_as_if_applicable - - unnecessary_breaks + - valid_regexps + - void_checks analyzer: - exclude: + exclude: - example/ - "**/example/" - example_rest - example_graphql - "**/*.g.dart" - + errors: # override custom always_use_package_imports: error diff --git a/docs/data/providers.md b/docs/data/providers.md index 13309404..64fa0f67 100644 --- a/docs/data/providers.md +++ b/docs/data/providers.md @@ -31,18 +31,25 @@ Underscore prefixing of type declarations ensure that 1) they will likely not co Every public instance method should support a named argument of `{Query query}`. `Query` is the glue between an application and an abstracted provider or repository. It is accessed by both the repository and the provider, but as the last mile, the provider should interpret the `Query` at its barest level. -### `providerArgs:` +### `limit:` -`providerArgs` describe how to interact with a provider's source. +The ceiling for how many results a provider should return from the source. + +``` +Query(limit: 10) +``` + +### `offset:` + +The starting index for a provider's search for results. -```dart -providerArgs: { - // limit describes how many results the provider requires from the source - 'limit': 10, -}, ``` +Query(offset: 10) +``` + +### `forProviders:` -As `providerArgs` can vary from provider to provider and IDE suggestions are unavailable to a string-key map, `providerArgs` should be clearly and accessibly documented within every new provider. +Available arguments can vary from provider to provider; this allows implementations to query exclusive statements from a specific source. ### `where:` diff --git a/docs/graphql/query.md b/docs/graphql/query.md index c8de0aa3..e38913e5 100644 --- a/docs/graphql/query.md +++ b/docs/graphql/query.md @@ -19,7 +19,7 @@ query MyOperation($vars: MyInputClass!) { } ``` -?> `providerArgs['operation'].variables` will **never** be wrapped by `variablesNamespace` +?> `GraphqlProviderQuery#.variables` will **never** be wrapped by `variablesNamespace` ## `where:` diff --git a/docs/sqlite/query.md b/docs/sqlite/query.md index 95be1c2d..e5d78a73 100644 --- a/docs/sqlite/query.md +++ b/docs/sqlite/query.md @@ -4,12 +4,12 @@ The following map exactly to their SQLite keywords. The values will be inserted into a SQLite statement **without being prepared**. -* `collate` -* `having` -* `groupBy` -* `limit` -* `offset` -* `orderBy` +- `collate` +- `having` +- `groupBy` +- `limit` +- `offset` +- `orderBy` As the values are directly inserted, use the field name: @@ -20,7 +20,7 @@ final String lastName; Query( where: [Where.exact('lastName', 'Mustermann')], - providerArgs: {'orderBy': 'lastName ASC'}, + orderBy: [OrderBy('lastName', ascending: true)] ) ``` diff --git a/docs/supabase/query.md b/docs/supabase/query.md index cf49a347..096a2b83 100644 --- a/docs/supabase/query.md +++ b/docs/supabase/query.md @@ -1,16 +1,12 @@ # `Query` Configuration -## `providerArgs:` +## `limit` -| Name | Type | Description | -| -------------------------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | -| `'limit'` | `int` | Forwards to Supabase's `limit` [param](https://supabase.com/docs/reference/dart/limit) in Brick's `#get` action | -| `'limitByReferencedTable'` | `String?` | Forwards to Supabase's `referencedTable` [property](https://supabase.com/docs/reference/dart/limit) | -| `'offset'` | `int` | Start from a specific offset, inclusive. | -| `'orderBy'` | `String` | Use field names not column names and always specify direction.For example, given a `final DateTime createdAt;` field: `{'orderBy': 'createdAt ASC'}`. | -| `'orderByReferencedTable'` | `String?` | Forwards to Supabase's `referencedTable` [property](https://supabase.com/docs/reference/dart/order) | +Forwards to Supabase's `limit` [param](https://supabase.com/docs/reference/dart/limit) in Brick's `#get` action -?> The `ReferencedTable` naming convention is awkward but necessary to not collide with other providers (like `SqliteProvider`) that also use `orderBy` and `limit`. While a `foreign_table.foreign_column` syntax is more Supabase-like, it is not supported in `orderBy` and `limit`. +## `offset` + +Start from a specific offset, inclusive. ## `where:` diff --git a/example_supabase/lib/brick/db/20240920063917.migration.dart b/example_supabase/lib/brick/db/20240920063917.migration.dart index 6e2aef80..a5493fb8 100644 --- a/example_supabase/lib/brick/db/20240920063917.migration.dart +++ b/example_supabase/lib/brick/db/20240920063917.migration.dart @@ -1,6 +1,8 @@ // GENERATED CODE EDIT WITH CAUTION // THIS FILE **WILL NOT** BE REGENERATED // This file should be version controlled and can be manually edited. +// ignore_for_file: public_member_api_docs, constant_identifier_names + part of 'schema.g.dart'; // While migrations are intelligently created, the difference between some commands, such as @@ -21,8 +23,6 @@ const List _migration_20240920063917_up = [ 'Pizza', 'Customer', foreignKeyColumn: 'customer_Customer_brick_id', - onDeleteCascade: false, - onDeleteSetDefault: false, ), ]; diff --git a/example_supabase/lib/brick/models/customer.model.dart b/example_supabase/lib/brick/models/customer.model.dart index 433d28bc..fd017717 100644 --- a/example_supabase/lib/brick/models/customer.model.dart +++ b/example_supabase/lib/brick/models/customer.model.dart @@ -1,3 +1,5 @@ +// ignore_for_file: public_member_api_docs + import 'package:brick_offline_first_with_supabase/brick_offline_first_with_supabase.dart'; import 'package:brick_sqlite/brick_sqlite.dart'; diff --git a/example_supabase/lib/brick/models/pizza.model.dart b/example_supabase/lib/brick/models/pizza.model.dart index 4bae5382..cd090e2c 100644 --- a/example_supabase/lib/brick/models/pizza.model.dart +++ b/example_supabase/lib/brick/models/pizza.model.dart @@ -1,10 +1,12 @@ +// ignore_for_file: public_member_api_docs + 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'; import 'package:pizza_shoppe/brick/models/customer.model.dart'; @ConnectOfflineFirstWithSupabase( - supabaseConfig: SupabaseSerializable(), + supabaseConfig: SupabaseSerializable.defaults, ) class Pizza extends OfflineFirstWithSupabaseModel { /// Read more about `@Sqlite`: https://github.com/GetDutchie/brick/tree/main/packages/brick_sqlite#fields diff --git a/example_supabase/lib/brick/repository.dart b/example_supabase/lib/brick/repository.dart index d039db73..5d4b093c 100644 --- a/example_supabase/lib/brick/repository.dart +++ b/example_supabase/lib/brick/repository.dart @@ -1,3 +1,5 @@ +// ignore_for_file: public_member_api_docs + import 'package:brick_offline_first_with_supabase/brick_offline_first_with_supabase.dart'; import 'package:brick_sqlite/brick_sqlite.dart'; import 'package:brick_sqlite/memory_cache_provider.dart'; @@ -10,6 +12,8 @@ import 'package:supabase_flutter/supabase_flutter.dart'; class Repository extends OfflineFirstWithSupabaseRepository { static late Repository? _singleton; + factory Repository() => _singleton!; + Repository._({ required super.supabaseProvider, required super.sqliteProvider, @@ -18,8 +22,6 @@ class Repository extends OfflineFirstWithSupabaseRepository { super.memoryCacheProvider, }); - factory Repository() => _singleton!; - static Future initializeSupabaseAndConfigure({ required String supabaseUrl, required String supabaseAnonKey, diff --git a/example_supabase/lib/main.dart b/example_supabase/lib/main.dart index 5083a00a..02ef2c03 100644 --- a/example_supabase/lib/main.dart +++ b/example_supabase/lib/main.dart @@ -1,3 +1,5 @@ +// ignore_for_file: public_member_api_docs + import 'package:flutter/material.dart'; import 'package:pizza_shoppe/brick/models/pizza.model.dart'; import 'package:pizza_shoppe/brick/repository.dart'; @@ -11,21 +13,23 @@ Future main() async { supabaseAnonKey: supabaseAnonKey, ); await Repository().initialize(); - runApp(MyApp()); + runApp(const MyApp()); } class MyApp extends StatelessWidget { + const MyApp({super.key}); + @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, - textTheme: TextTheme( - bodyMedium: TextStyle(fontSize: 20.0), + textTheme: const TextTheme( + bodyMedium: TextStyle(fontSize: 20), ), ), - home: MyHomePage(title: 'Flutter Demo Home Page'), + home: const MyHomePage(title: 'Flutter Demo Home Page'), ); } } @@ -33,7 +37,7 @@ class MyApp extends StatelessWidget { class MyHomePage extends StatelessWidget { final String title; - MyHomePage({super.key, required this.title}); + const MyHomePage({super.key, required this.title}); @override Widget build(BuildContext context) { @@ -42,8 +46,9 @@ class MyHomePage extends StatelessWidget { title: Text(title), ), body: Container( - padding: const EdgeInsets.all(20.0), + padding: const EdgeInsets.all(20), child: FutureBuilder( + // ignore: discarded_futures future: Repository().get(), builder: (context, AsyncSnapshot> pizzaList) { final pizzas = pizzaList.data; @@ -63,12 +68,11 @@ class MyHomePage extends StatelessWidget { class PizzaTile extends StatelessWidget { final Pizza pizza; - PizzaTile(this.pizza); + const PizzaTile(this.pizza, {super.key}); @override Widget build(BuildContext context) { return Column( - mainAxisAlignment: MainAxisAlignment.start, children: [ Text('id: ${pizza.id}'), Text('frozen: ${pizza.frozen}'), diff --git a/example_supabase/pubspec.yaml b/example_supabase/pubspec.yaml index d6bf9775..1af16380 100644 --- a/example_supabase/pubspec.yaml +++ b/example_supabase/pubspec.yaml @@ -10,32 +10,32 @@ environment: flutter: ">=3.0.0" dependencies: + brick_offline_first_with_supabase: cupertino_icons: ^1.0.2 flutter: sdk: flutter - brick_offline_first_with_supabase: sqflite: supabase_flutter: dependency_overrides: - brick_offline_first_with_supabase: - path: ../packages/brick_offline_first_with_supabase - brick_supabase: - path: ../packages/brick_supabase - brick_sqlite_generators: - path: ../packages/brick_sqlite_generators - brick_offline_first_with_supabase_build: - path: ../packages/brick_offline_first_with_supabase_build brick_build: path: ../packages/brick_build - brick_supabase_generators: - path: ../packages/brick_supabase_generators + brick_json_generators: + path: ../packages/brick_json_generators brick_offline_first_build: path: ../packages/brick_offline_first_build brick_offline_first_with_rest: path: ../packages/brick_offline_first_with_rest - brick_json_generators: - path: ../packages/brick_json_generators + brick_offline_first_with_supabase: + path: ../packages/brick_offline_first_with_supabase + brick_offline_first_with_supabase_build: + path: ../packages/brick_offline_first_with_supabase_build + brick_sqlite_generators: + path: ../packages/brick_sqlite_generators + brick_supabase: + path: ../packages/brick_supabase + brick_supabase_generators: + path: ../packages/brick_supabase_generators dev_dependencies: brick_offline_first_with_supabase_build: diff --git a/packages/brick_build/CHANGELOG.md b/packages/brick_build/CHANGELOG.md index 1a5f11b0..abca3b23 100644 --- a/packages/brick_build/CHANGELOG.md +++ b/packages/brick_build/CHANGELOG.md @@ -1,6 +1,10 @@ ## Unreleased +## 3.3.0 + - Add documentation to increase pub.dev score +- Update minimum `brick_core` to `1.3.0` +- Update analysis to modern lints ## 3.2.1 diff --git a/packages/brick_build/README.md b/packages/brick_build/README.md index c35f2511..939fcdc8 100644 --- a/packages/brick_build/README.md +++ b/packages/brick_build/README.md @@ -19,6 +19,7 @@ If you're not using `watch`, be sure to run `build` twice for the schema to dete ``` An application directory **will/must** resemble the following: + ``` | my-app |--lib @@ -33,41 +34,41 @@ This ensures a consistent path to access child data, such as models, [by build g - [Glossary](#glossary) - [API Considerations](#api-considerations) - * [Provider](#provider) + - [Provider](#provider) - [Class-level Configuration](#class-level-configuration) - [Field-level Configuration](#field-level-configuration) - * [Query](#query) + - [Query](#query) - [providerArgs:](#providerargs) - [where:](#where) - * [Adapters](#adapters) - * [Models](#models) - * [Repository](#repository) + - [Adapters](#adapters) + - [Models](#models) + - [Repository](#repository) - [Class-level Configuration](#class-level-configuration-1) - [Field-level Configuration](#field-level-configuration-1) - [Associations](#associations) - [Code Generation](#code-generation) - * [Package Setup](#package-setup) - * [Provider](#provider-1) + - [Package Setup](#package-setup) + - [Provider](#provider-1) - [Class-level Configuration](#class-level-configuration-2) - [Field-level Annotation](#discovering-and-interpreting-field-level-annotation) - [Adapters](#adapters-1) - [Invoking the Generators](#invoking-the-generators) - * [Domain](#domain) + - [Domain](#domain) - [Class-level Annotation](#the-class-level-annotation) - [Model Dictionary (brick.g.dart)](#model-dictionary-brickgdart) - [Builder](#builder) - [Testing](#testing) - [Advanced Techniques](#advanced-techniques) - * [Custom Type Checking](#custom-type-checking) + - [Custom Type Checking](#custom-type-checking) - [FAQ](#faq) # Glossary -* **generator** - code producer. The output of a generator is most often a function that converts input to normalized data. The output of a generator does not always constitute a complete file (e.g. one generator is a serializer, another generator is a deserializer, and both generators are combined in a super adapter generator). -* **builder** - a class that interfaces between source files and generator(s) before writing generated code to file(s). They are invoked and configured by build.yaml. Builders are primarly concerned with annotations that exist in the source (e.g. a Flutter app). -* **serdes** - shorthand for serialize/deserialize -* **checker** - an accessible utility that type checks analyzed type from a source. For example, `isBool` for a source of `final bool isDeleted` would return `true`. With a source of `final String isDeleted`, `isBool` would return `false`. -* **domain** - the encompassing system. For example, the `OfflineFirstWithRest` domain builds REST serdes and SQLite serdes within an adapter and is discovered via its own annotation. +- **generator** - code producer. The output of a generator is most often a function that converts input to normalized data. The output of a generator does not always constitute a complete file (e.g. one generator is a serializer, another generator is a deserializer, and both generators are combined in a super adapter generator). +- **builder** - a class that interfaces between source files and generator(s) before writing generated code to file(s). They are invoked and configured by build.yaml. Builders are primarly concerned with annotations that exist in the source (e.g. a Flutter app). +- **serdes** - shorthand for serialize/deserialize +- **checker** - an accessible utility that type checks analyzed type from a source. For example, `isBool` for a source of `final bool isDeleted` would return `true`. With a source of `final String isDeleted`, `isBool` would return `false`. +- **domain** - the encompassing system. For example, the `OfflineFirstWithRest` domain builds REST serdes and SQLite serdes within an adapter and is discovered via its own annotation. # API Considerations @@ -122,18 +123,25 @@ As the field-level annotations are the most often written, they have the most ac Every public instance method should support a named argument of `{Query query}`. `Query` is the glue between an application and an abstracted provider or repository. It is accessed by both the repository and the provider, but as the last mile, the provider should interpret the `Query` at its barest level. -### `providerArgs:` +### `limit:` -`providerArgs` describe how to interact with a provider's source. +The ceiling for how many results a provider should return from the source. -```dart -providerArgs: { - // limit describes how many results the provider requires from the source - 'limit': 10, -}, +``` +Query(limit: 10) +``` + +### `offset:` + +The starting index for a provider's search for results. + +``` +Query(offset: 10) ``` -As `providerArgs` can vary from provider to provider and IDE suggestions are unavailable to a string-key map, `providerArgs` should be clearly and accessibly documented within every new provider. +### `forProviders:` + +Available arguments can vary from provider to provider; this allows implementations to query exclusive statements from a specific source. ### `where:` @@ -299,10 +307,10 @@ brick_cloud_firestore |--|--brick_offline_first_with_cloud_firestore_build ``` -* [ ] If the provider has a Flutter dependency, a separate package for annotations and configuration exists as a `_abstract` package -* [ ] The `_generators` package **does not** include a `build.yaml` (multiple `build.yaml` files can cause race collisions) -* [ ] `Fields`, `SerializeGenerator`, `DeserializeGenerator`, and `ModelSerdesGenerator` can be accessed outside the `_generators` package -* [ ] Only one class-level annotation is discovered per `_build` package +- [ ] If the provider has a Flutter dependency, a separate package for annotations and configuration exists as a `_abstract` package +- [ ] The `_generators` package **does not** include a `build.yaml` (multiple `build.yaml` files can cause race collisions) +- [ ] `Fields`, `SerializeGenerator`, `DeserializeGenerator`, and `ModelSerdesGenerator` can be accessed outside the `_generators` package +- [ ] Only one class-level annotation is discovered per `_build` package ## Provider @@ -324,6 +332,7 @@ These configurations may be injected directly into the adapater (like `endpoint` This class **should not** be used as an annotation. Instead, it is received as a member of a class-level annotation discovered by the [domain](#domain). #### Interpreting Class-level Configurations + Once a class is discovered by a builder, the configuration is pulled from the annotation and expanded into easily-digestible Dart form: ```dart diff --git a/packages/brick_build/lib/src/adapter_generator.dart b/packages/brick_build/lib/src/adapter_generator.dart index 5636a784..49935311 100644 --- a/packages/brick_build/lib/src/adapter_generator.dart +++ b/packages/brick_build/lib/src/adapter_generator.dart @@ -17,45 +17,41 @@ class AdapterGenerator { final String superAdapterName; /// Generated adapter methods - String get allAdapterMethods { - return generators.fold>({}, (acc, generator) { - final expectedOutput = 'Future<${generator.adapterMethodOutputType}>'; - final methodAction = generator.doesDeserialize ? 'from' : 'to'; - final methodArguments = - '${generator.adapterMethodInputType} input, {required provider, covariant ${superAdapterName}Repository? repository}'; - final methodName = '$methodAction${generator.providerName}($methodArguments)'; + String get allAdapterMethods => generators.fold>({}, (acc, generator) { + final expectedOutput = 'Future<${generator.adapterMethodOutputType}>'; + final methodAction = generator.doesDeserialize ? 'from' : 'to'; + final methodArguments = + '${generator.adapterMethodInputType} input, {required provider, covariant ${superAdapterName}Repository? repository}'; + final methodName = '$methodAction${generator.providerName}($methodArguments)'; - acc.add('@override\n$expectedOutput $methodName async => ${generator.adapterMethod};'); - return acc; - }).join('\n'); - } + acc.add('@override\n$expectedOutput $methodName async => ${generator.adapterMethod};'); + return acc; + }).join('\n'); /// Any special instance fields the serdes generator needs to forward to the adapter - String get allInstanceFieldsAndMethods { - return generators.fold>({}, (acc, generator) { - final fromGenerator = - generator.instanceFieldsAndMethods.fold>({}, (acc2, field) { - final didAdd = acc2.add(field); - if (!didAdd) { - throw InvalidGenerationSourceError( - '$field has already been declared by another generator', - ); - } - return acc2; - }); - acc.addAll(fromGenerator); - return acc; - }).join('\n'); - } + String get allInstanceFieldsAndMethods => + generators.fold>({}, (acc, generator) { + final fromGenerator = + generator.instanceFieldsAndMethods.fold>({}, (acc2, field) { + final didAdd = acc2.add(field); + if (!didAdd) { + throw InvalidGenerationSourceError( + '$field has already been declared by another generator', + ); + } + return acc2; + }); + acc.addAll(fromGenerator); + return acc; + }).join('\n'); /// The functions that serialize or deserialize, ultimately used by the adapter method - String get serializerFunctions { - return generators.fold>({}, (acc, generator) { - acc.add(generator.generate()); - return acc; - }).join('\n'); - } + String get serializerFunctions => generators.fold>({}, (acc, generator) { + acc.add(generator.generate()); + return acc; + }).join('\n'); + /// Given a model, outputs generated code to use as a Brick adapter. const AdapterGenerator({ required this.superAdapterName, required this.className, diff --git a/packages/brick_build/lib/src/annotation_finder.dart b/packages/brick_build/lib/src/annotation_finder.dart index fbe8baf1..c50d5834 100644 --- a/packages/brick_build/lib/src/annotation_finder.dart +++ b/packages/brick_build/lib/src/annotation_finder.dart @@ -12,6 +12,7 @@ abstract class AnnotationFinder { /// Holder of previously generated [Annotation]s final _columnExpando = Expando(); + /// Find an [Annotation] per field. AnnotationFinder(); /// Given a field element, retrieve the [Annotation] equivalent diff --git a/packages/brick_build/lib/src/annotation_finder_with_field_rename.dart b/packages/brick_build/lib/src/annotation_finder_with_field_rename.dart index 1057cd88..ff5f1ad0 100644 --- a/packages/brick_build/lib/src/annotation_finder_with_field_rename.dart +++ b/packages/brick_build/lib/src/annotation_finder_with_field_rename.dart @@ -2,6 +2,7 @@ import 'package:brick_build/src/annotation_finder.dart'; import 'package:brick_build/src/utils/string_helpers.dart'; import 'package:brick_core/field_rename.dart'; +/// Rename the field name to the value of [FieldRename] mixin AnnotationFinderWithFieldRename on AnnotationFinder { /// Change serialization key based on the configuration. /// `name` defined with a field annotation takes precedence. diff --git a/packages/brick_build/lib/src/annotation_super_generator.dart b/packages/brick_build/lib/src/annotation_super_generator.dart index cd10c1ea..19fcf30f 100644 --- a/packages/brick_build/lib/src/annotation_super_generator.dart +++ b/packages/brick_build/lib/src/annotation_super_generator.dart @@ -10,8 +10,15 @@ import 'package:source_gen/source_gen.dart'; /// For example, all `@ConnectOfflineFirstWithRest` models may be discovered and passed to /// subsequent generators. abstract class AnnotationSuperGenerator<_Annotation> extends GeneratorForAnnotation<_Annotation> { + /// Name of the parent adapter String get superAdapterName; + /// Output serializing code for all models with the `@_Annotation` annotation. + /// This generator is the annotation that discovers all models in the domain + /// and most commonly invokes sub generators. + /// + /// For example, all `@ConnectOfflineFirstWithRest` models may be discovered and passed to + /// subsequent generators. const AnnotationSuperGenerator() : super(); /// Given an [element] and an [annotation], scaffold all @@ -36,7 +43,11 @@ abstract class AnnotationSuperGenerator<_Annotation> extends GeneratorForAnnotat /// Outputs any sub generators with the exception of the adapter. /// The adapter _should_ include code generated by this method. @override - String generateForAnnotatedElement(element, annotation, buildStep) { + String generateForAnnotatedElement( + Element element, + ConstantReader annotation, + BuildStep buildStep, + ) { final generators = buildGenerators(element, annotation); return generators.fold>([], (acc, generator) { diff --git a/packages/brick_build/lib/src/builders/adapter_builder.dart b/packages/brick_build/lib/src/builders/adapter_builder.dart index b2e24b46..57fe7037 100644 --- a/packages/brick_build/lib/src/builders/adapter_builder.dart +++ b/packages/brick_build/lib/src/builders/adapter_builder.dart @@ -3,13 +3,16 @@ import 'package:brick_build/src/utils/string_helpers.dart'; import 'package:build/build.dart'; /// Writes adapter code (model serialization/deserialization). -/// Outputs to brick/adapters/_adapter.g.dart +/// Outputs to brick/adapters/{MODEL}_adapter.g.dart class AdapterBuilder<_ClassAnnotation> extends BaseBuilder<_ClassAnnotation> { + /// final AnnotationSuperGenerator generator; @override final outputExtension = '.adapter_builder.dart'; + /// Writes adapter code (model serialization/deserialization). + /// Outputs to brick/adapters/{MODEL}_adapter.g.dart AdapterBuilder(this.generator); @override @@ -18,8 +21,7 @@ class AdapterBuilder<_ClassAnnotation> extends BaseBuilder<_ClassAnnotation> { final allOutputs = []; for (final annotatedElement in annotatedElements) { - final stopwatch = Stopwatch(); - stopwatch.start(); + final stopwatch = Stopwatch()..start(); final output = generator.generateAdapter( annotatedElement.element, diff --git a/packages/brick_build/lib/src/builders/aggregate_builder.dart b/packages/brick_build/lib/src/builders/aggregate_builder.dart index 54c7717e..e4103fce 100644 --- a/packages/brick_build/lib/src/builders/aggregate_builder.dart +++ b/packages/brick_build/lib/src/builders/aggregate_builder.dart @@ -1,3 +1,4 @@ +import 'package:analyzer/dart/element/element.dart'; import 'package:brick_build/src/builders/base.dart'; import 'package:build/build.dart'; import 'package:glob/glob.dart'; @@ -21,23 +22,43 @@ class AggregateBuilder implements Builder { /// For example: `['import 'package:brick_sqlite/db.dart';']` final List requiredImports; + /// static final adapterFiles = Glob('lib/brick/adapters/*.g.dart'); + + /// static final importRegex = RegExp(r'(^import\s.*;)', multiLine: true); + + /// static final migrationFiles = Glob('lib/brick/db/*.migration.dart'); + + /// static final modelFiles = Glob('lib/**/*.model.dart'); + + /// static const outputFileName = 'models_and_migrations${BaseBuilder.aggregateExtension}.dart'; + /// Combine all `@ConnectOfflineFirstWithRest` and `@Migratable` classes and annotations + /// + /// Since [LibraryElement] only reads from one file and not an entire directory, all relevant + /// classes and annotation are inserted into copies of all input files. If there is ever a + /// performance concern with build times, start here. Only one file is needed, but it is impossible + /// to access [LibraryReader]s outside the build step of the created asset, and this was the only + /// successful way amongst dozens. + /// This does **not** output a file used by Brick in the app implementation. + /// + /// See the + /// [`build` docs](https://github.com/dart-lang/build/blob/master/docs/writing_an_aggregate_builder.md#defining-your-builder) + /// example for more. const AggregateBuilder({this.requiredImports = const []}); @override Future build(BuildStep buildStep) async { brickLogger.info('Aggregating models and migrations...'); - final imports = {}; - imports.addAll([ + final imports = { 'library big_messy_models_migrations_file;', - ]); - imports.addAll(requiredImports); + ...requiredImports, + }; final files = []; for (final glob in [migrationFiles, modelFiles]) { @@ -61,12 +82,11 @@ class AggregateBuilder implements Builder { } /// All unique `import:package` within a large body of text - static Set findAllImports(String contents) { - return importRegex.allMatches(contents).map((m) => m[0]!).toSet(); - } + static Set findAllImports(String contents) => + importRegex.allMatches(contents).map((m) => m[0]!).toSet(); @override - final buildExtensions = const { - r'$lib$': [outputFileName], - }; + Map> get buildExtensions => const { + r'$lib$': [outputFileName], + }; } diff --git a/packages/brick_build/lib/src/builders/base.dart b/packages/brick_build/lib/src/builders/base.dart index 41dcb6d1..d0542241 100644 --- a/packages/brick_build/lib/src/builders/base.dart +++ b/packages/brick_build/lib/src/builders/base.dart @@ -7,9 +7,12 @@ import 'package:source_gen/source_gen.dart'; export 'package:brick_build/src/annotation_super_generator.dart'; +/// final brickLogger = Logger('Brick'); +/// abstract class BaseBuilder<_ClassAnnotation> implements Builder { + /// Logger get logger => brickLogger; @override @@ -20,8 +23,10 @@ abstract class BaseBuilder<_ClassAnnotation> implements Builder { /// The cached file this will produce String get outputExtension; + /// final typeChecker = TypeChecker.fromRuntime(_ClassAnnotation); + /// static const aggregateExtension = '.brick_aggregate'; /// Classes with the class-level annotation. For example, `ConnectOfflineFirstWithRest`. @@ -42,14 +47,14 @@ abstract class BaseBuilder<_ClassAnnotation> implements Builder { if (!dirName.contains('.dart')) { final dir = Directory(p.join('lib', 'brick', dirName)); - final dirExists = await dir.exists(); + final dirExists = dir.existsSync(); if (!dirExists) { await dir.create(); } } final newFile = File(p.join('lib', 'brick', path)); - final fileExists = await newFile.exists(); + final fileExists = newFile.existsSync(); if (!fileExists) { await newFile.create(); } @@ -60,7 +65,7 @@ abstract class BaseBuilder<_ClassAnnotation> implements Builder { /// Replace contents of file Future replaceWithinFile(String path, Pattern from, String to) async { final file = File(p.join('lib', 'brick', path)); - final fileExists = await file.exists(); + final fileExists = file.existsSync(); if (!fileExists) return null; final contents = await file.readAsString(); diff --git a/packages/brick_build/lib/src/builders/model_dictionary_builder.dart b/packages/brick_build/lib/src/builders/model_dictionary_builder.dart index ef6ae514..532a74b1 100644 --- a/packages/brick_build/lib/src/builders/model_dictionary_builder.dart +++ b/packages/brick_build/lib/src/builders/model_dictionary_builder.dart @@ -1,6 +1,7 @@ import 'package:brick_build/src/builders/aggregate_builder.dart'; import 'package:brick_build/src/builders/base.dart'; import 'package:brick_build/src/model_dictionary_generator.dart'; +import 'package:brick_core/core.dart'; import 'package:build/build.dart'; import 'package:glob/glob.dart'; import 'package:source_gen/source_gen.dart'; @@ -14,13 +15,16 @@ class ModelDictionaryBuilder<_ClassAnnotation> extends BaseBuilder<_ClassAnnotat /// Include both single and double strings around package imports for safety. Regex is not supported. final List expectedImportRemovals; + /// final ModelDictionaryGenerator modelDictionaryGenerator; @override final outputExtension = '.model_dictionary_builder.dart'; + /// static final modelFiles = Glob('lib/**/*.model.dart'); + /// Writes [ModelDictionary] code to connect model and adapters. Outputs to brick/brick.g.dart ModelDictionaryBuilder( this.modelDictionaryGenerator, { this.expectedImportRemovals = const [], @@ -35,14 +39,13 @@ class ModelDictionaryBuilder<_ClassAnnotation> extends BaseBuilder<_ClassAnnotat } final contents = await buildStep.readAsString(buildStep.inputId); - final stopwatch = Stopwatch(); - stopwatch.start(); + final stopwatch = Stopwatch()..start(); final allImports = AggregateBuilder.findAllImports(contents); final classNamesByFileNames = classFilePathsFromAnnotations(annotatedElements, filesToContents); final modelDictionaryOutput = modelDictionaryGenerator.generate(classNamesByFileNames); - allImports.removeAll(["import 'dart:convert';", 'import "dart:convert";']); - allImports.removeAll(expectedImportRemovals); + allImports + .removeAll(["import 'dart:convert';", 'import "dart:convert";', ...expectedImportRemovals]); final analyzedImports = allImports .map((i) => '// ignore: unused_import, unused_shown_name, unnecessary_import\n$i') .join('\n'); @@ -53,17 +56,17 @@ class ModelDictionaryBuilder<_ClassAnnotation> extends BaseBuilder<_ClassAnnotat logStopwatch('Generated brick.g.dart', stopwatch); } + /// static Map classFilePathsFromAnnotations( Iterable annotations, Map filesToContents, - ) { - return { - for (final annotation in annotations) - '${annotation.element.name}': filesToContents.entries - .firstWhere((entry) => entry.value.contains('class ${annotation.element.name} ')) - .key - // Make relative from the `brick/` folder - .replaceAll(RegExp('^lib/'), '../'), - }; - } + ) => + { + for (final annotation in annotations) + '${annotation.element.name}': filesToContents.entries + .firstWhere((entry) => entry.value.contains('class ${annotation.element.name} ')) + .key + // Make relative from the `brick/` folder + .replaceAll(RegExp('^lib/'), '../'), + }; } diff --git a/packages/brick_build/lib/src/model_dictionary_generator.dart b/packages/brick_build/lib/src/model_dictionary_generator.dart index cf6dda0d..64b4074c 100644 --- a/packages/brick_build/lib/src/model_dictionary_generator.dart +++ b/packages/brick_build/lib/src/model_dictionary_generator.dart @@ -6,11 +6,13 @@ abstract class ModelDictionaryGenerator { /// so any methods that incorporate Type definitions should be listed here. /// For example, importing a `SqliteProvider` or a `DatabaseExecutor`. /// Consider adding analyzer ignores to disable 'unused_import' warnings. - final requiredImports = ''; + String get requiredImports => ''; + /// // ignore: constant_identifier_names static const HEADER = '// GENERATED CODE DO NOT EDIT'; + /// Given a list of models, output generated code to use as `brick.g.dart` file const ModelDictionaryGenerator(); /// Adapter part imports @@ -18,6 +20,7 @@ abstract class ModelDictionaryGenerator { .map((k) => "part 'adapters/${StringHelpers.snakeCase(k)}_adapter.g.dart';") .join('\n'); + /// String dictionaryFromFiles(Map classNamesToFileNames) => classNamesToFileNames.keys.map((k) => '$k: ${k}Adapter()').join(',\n '); diff --git a/packages/brick_build/lib/src/provider_serializable_generator.dart b/packages/brick_build/lib/src/provider_serializable_generator.dart index f0ac10c1..cc407d30 100644 --- a/packages/brick_build/lib/src/provider_serializable_generator.dart +++ b/packages/brick_build/lib/src/provider_serializable_generator.dart @@ -27,6 +27,7 @@ abstract class ProviderSerializableGenerator { /// The reader generated from the annotation final ConstantReader reader; + /// Given an element and annotation, output a digestable config ProviderSerializableGenerator( this.element, this.reader, { diff --git a/packages/brick_build/lib/src/serdes_generator.dart b/packages/brick_build/lib/src/serdes_generator.dart index 02c96c83..d71a85d8 100644 --- a/packages/brick_build/lib/src/serdes_generator.dart +++ b/packages/brick_build/lib/src/serdes_generator.dart @@ -29,9 +29,8 @@ abstract class SerdesGenerator + 'await $serializingFunctionName(input, provider: provider, repository: repository)'; /// The expected input type for the [adapterMethod] String get adapterMethodInputType => doesDeserialize ? deserializeInputType : className; @@ -39,6 +38,7 @@ abstract class SerdesGenerator doesDeserialize ? className : serializeOutputType; + /// String get className => element.name; /// Discover factories within the class that rely on the provider. @@ -54,17 +54,16 @@ abstract class SerdesGenerator>([], (acc, field) { - final fieldAnnotation = fields.annotationForField(field); - final serialization = addField(field, fieldAnnotation); - if (serialization != null) { - acc.add(serialization); - } + String get fieldsForGenerator => + fields.stableInstanceFields.fold>([], (acc, field) { + final fieldAnnotation = fields.annotationForField(field); + final serialization = addField(field, fieldAnnotation); + if (serialization != null) { + acc.add(serialization); + } - return acc; - }).join(',\n'); - } + return acc; + }).join(',\n'); /// Code to follow after a class has been instantiated. /// **Must** end with semicolon. @@ -114,16 +113,19 @@ abstract class SerdesGenerator get unignoredFields { - return fields.stableInstanceFields.where((field) { - final annotation = fields.annotationForField(field); - final checker = checkerForType(field.type); - - return (!annotation.ignore && checker.isSerializable) || - checker.isSerializableViaJson(doesDeserialize); - }); - } + Iterable get unignoredFields => fields.stableInstanceFields.where((field) { + final annotation = fields.annotationForField(field); + final checker = checkerForType(field.type); + + return (!annotation.ignore && checker.isSerializable) || + checker.isSerializableViaJson(doesDeserialize); + }); + /// A generator that converts raw input into Dart code or Dart code into raw input. Most + /// [Provider]s will require a `SerdesGenerator` to help the Repository normalize data. + /// + /// [FieldAnnotation] describes the field-level class, such as @`Rest` + /// [_SiblingModel] describes the domain or provider model, such as `SqliteModel` SerdesGenerator(this.element, this.fields); /// Given each field, determine whether it can be added to the serdes function @@ -253,11 +255,11 @@ abstract class SerdesGenerator + fieldAnnotation.nullable && field.type.nullabilitySuffix != NullabilitySuffix.none + ? "data['$name'] == null ? null :" + : ''; /// Convert placeholders in `fromGenerator` and `toGenerator` to functions. String? expandGenerators( @@ -307,14 +308,14 @@ abstract class SerdesGenerator + fieldAnnotation.defaultValue != null ? ' ?? ${fieldAnnotation.defaultValue}' : ''; /// If a custom generator is provided, replace variables with desired values /// Useful for hacking around `const` functions when duplicating logic @@ -417,7 +417,7 @@ abstract class SerdesGenerator(Iterable items, bool Function(T item) test) { - for (var item in items) { + for (final item in items) { if (test(item)) return item; } return null; diff --git a/packages/brick_build/lib/src/utils/fields_for_class.dart b/packages/brick_build/lib/src/utils/fields_for_class.dart index 794daaf8..7fcf3aa4 100644 --- a/packages/brick_build/lib/src/utils/fields_for_class.dart +++ b/packages/brick_build/lib/src/utils/fields_for_class.dart @@ -28,30 +28,28 @@ abstract class FieldsForClass { final allFields = elementInstanceFields.keys.toSet().union(inheritedFields.keys.toSet()); final fields = - allFields.map((e) => _FieldSet(elementInstanceFields[e], inheritedFields[e])).toList(); - - // Sort the fields using the `compare` implementation in _FieldSet - fields.sort(); + allFields.map((e) => _FieldSet(elementInstanceFields[e], inheritedFields[e])).toList() + // Sort the fields using the `compare` implementation in _FieldSet + ..sort(); return fields.map((fs) => fs.field).toList(); } /// ignore private, `static`, and `Function` fields - Iterable get stableInstanceFields { - return sorted.where((field) { - return field.isPublic && - (field.isFinal || field.isConst || field.getter != null) && - !field.isStatic && - !field.type.isDartCoreFunction; - }); - } + Iterable get stableInstanceFields => sorted.where( + (field) => + field.isPublic && + (field.isFinal || field.isConst || field.getter != null) && + !field.isStatic && + !field.type.isDartCoreFunction, + ); - FieldsForClass({required this.element}); + /// Manages all fields of a [ClassElement]. Generously borrowed from JSON Serializable + const FieldsForClass({required this.element}); /// Returns `true` for `int get name => 5` - static bool isComputedGetter(FieldElement field) { - return !field.getter.runtimeType.toString().contains('ImplicitGetter'); - } + static bool isComputedGetter(FieldElement field) => + !field.getter.runtimeType.toString().contains('ImplicitGetter'); } /// Ensures uniqueness of accessible fields within a [ClassElement] @@ -59,8 +57,6 @@ class _FieldSet implements Comparable<_FieldSet> { final FieldElement field; final FieldElement sortField; - _FieldSet._(this.field, this.sortField) : assert(field.name == sortField.name); - factory _FieldSet(FieldElement? classField, FieldElement? superField) { // At least one of these will != null, perhaps both. final fields = [classField, superField].whereType().toList(); @@ -73,6 +69,9 @@ class _FieldSet implements Comparable<_FieldSet> { return _FieldSet._(fields.first, sortField); } + _FieldSet._(this.field, this.sortField) + : assert(field.name == sortField.name, 'fields must be unique'); + @override int compareTo(_FieldSet other) => _sortByLocation(sortField, other.sortField); diff --git a/packages/brick_build/lib/src/utils/shared_checker.dart b/packages/brick_build/lib/src/utils/shared_checker.dart index a54c744f..8d2cd5dd 100644 --- a/packages/brick_build/lib/src/utils/shared_checker.dart +++ b/packages/brick_build/lib/src/utils/shared_checker.dart @@ -22,15 +22,18 @@ class SharedChecker<_SiblingModel extends Model> { /// The checked type final DartType targetType; + /// A utility to legibly assert a [DartType] against core types + /// + /// Optionally declare a model to discover "sibling" models, or models that share + /// the same domain or provider (e.g. `SqliteModel`). SharedChecker(this.targetType); /// Retrieves type argument, i.e. `Type` in `Future` or `List` - DartType get argType { - return (targetType as InterfaceType).typeArguments.first; - } + DartType get argType => (targetType as InterfaceType).typeArguments.first; + /// Type get asPrimitive { - assert(isDartCoreType); + assert(isDartCoreType, 'type must be a core type'); if (isBool) return bool; if (isDateTime) return DateTime; if (isDouble) return double; @@ -39,11 +42,13 @@ class SharedChecker<_SiblingModel extends Model> { return String; } + /// bool get canSerializeArgType { final checker = SharedChecker<_SiblingModel>(argType); return checker.isSerializable; } + /// String? enumDeserializeFactory(String providerName) { if (!isEnum) return null; final element = (targetType as InterfaceType).element as EnumElement; @@ -56,6 +61,7 @@ class SharedChecker<_SiblingModel extends Model> { return null; } + /// String? enumSerializeMethod(String providerName) { if (!isEnum) return null; final element = (targetType as InterfaceType).element as EnumElement; @@ -72,7 +78,7 @@ class SharedChecker<_SiblingModel extends Model> { /// If the constructor can't be found, `null` is returned. ConstructorElement? get fromJsonConstructor { if (targetType.element is ClassElement) { - for (final constructor in (targetType.element as ClassElement).constructors) { + for (final constructor in (targetType.element! as ClassElement).constructors) { if (constructor.name == 'fromJson') return constructor; } } @@ -80,11 +86,10 @@ class SharedChecker<_SiblingModel extends Model> { return null; } - bool get isArgTypeAFuture { - return argType.isDartAsyncFuture || argType.isDartAsyncFutureOr; - } + /// + bool get isArgTypeAFuture => argType.isDartAsyncFuture || argType.isDartAsyncFutureOr; - /// If the sub type has super type [SqliteModel] + /// If the sub type has super type of a related [Model] /// Returns true for `Future`, /// `List>`, and `List`. bool get isArgTypeASibling { @@ -96,34 +101,44 @@ class SharedChecker<_SiblingModel extends Model> { return _siblingClassChecker.isAssignableFromType(argType); } + /// bool get isBool => targetType.isDartCoreBool; /// If this is a [bool], [DateTime], [double], [int], [num], or [String] bool get isDartCoreType => isBool || isDateTime || isDouble || isInt || isNum || isString; + /// bool get isDateTime => _dateTimeChecker.isExactlyType(targetType); + /// bool get isDouble => targetType.isDartCoreDouble; - bool get isEnum { - return targetType is InterfaceType && (targetType as InterfaceType).element is EnumElement; - } + /// + bool get isEnum => + targetType is InterfaceType && (targetType as InterfaceType).element is EnumElement; + /// bool get isFuture => targetType.isDartAsyncFuture || targetType.isDartAsyncFutureOr; + /// bool get isInt => targetType.isDartCoreInt; + /// bool get isIterable => _iterableChecker.isExactlyType(targetType) || _listChecker.isExactlyType(targetType) || _setChecker.isExactlyType(targetType); + /// bool get isList => _listChecker.isExactlyType(targetType); + /// bool get isMap => _mapChecker.isExactlyType(targetType); + /// bool get isNullable => targetType.nullabilitySuffix != NullabilitySuffix.none; + /// bool get isNum => _numChecker.isExactlyType(targetType); /// Not all [Type]s are parseable. For consistency, one catchall before smaller checks @@ -140,6 +155,7 @@ class SharedChecker<_SiblingModel extends Model> { return isDartCoreType || isEnum || isMap || isSibling || (isFuture && canSerializeArgType); } + /// bool isSerializableViaJson(bool doesDeserialize) { if (isIterable) { final argTypeChecker = SharedChecker<_SiblingModel>(argType); @@ -150,6 +166,7 @@ class SharedChecker<_SiblingModel extends Model> { return doesDeserialize ? fromJsonConstructor != null : toJsonMethod != null; } + /// bool get isSet => _setChecker.isExactlyType(targetType); /// If this is a class similarly annotated by the current generator. @@ -157,8 +174,10 @@ class SharedChecker<_SiblingModel extends Model> { /// Useful for verifying whether or not to generate Serialize/Deserializers methods. bool get isSibling => _siblingClassChecker.isAssignableFromType(targetType); + /// If this is a [String] bool get isString => _stringChecker.isExactlyType(targetType); + /// If the type is a `Future` or `FutureOr`, returns the nullability of the type of the Future. bool get isUnFuturedTypeNullable => unFuturedType.nullabilitySuffix != NullabilitySuffix.none; /// Returns type arguments of [targetType]. For example, given `Map`, @@ -177,13 +196,13 @@ class SharedChecker<_SiblingModel extends Model> { /// For example, a field `final Currency amount` with a type definition /// `class Currency extends OfflineFirstSerdes {}` would return `[T, X, Y]`. List get superClassTypeArgs { - final classElement = targetType.element as ClassElement; - if (classElement.supertype?.typeArguments == null || - classElement.supertype!.typeArguments.isEmpty) { + final classElement = targetType.element as ClassElement?; + if (classElement?.supertype?.typeArguments == null || + classElement!.supertype!.typeArguments.isEmpty) { throw InvalidGenerationSourceError( - 'Type argument for ${targetType.getDisplayString(withNullability: true)} is undefined.', + 'Type argument for ${targetType.getDisplayString()} is undefined.', todo: - 'Define the type on class ${targetType.element}, e.g. `extends ${withoutNullability(classElement.supertype!)}`', + 'Define the type on class ${targetType.element}, e.g. `extends ${withoutNullability(classElement!.supertype!)}`', element: targetType.element, ); } @@ -195,7 +214,7 @@ class SharedChecker<_SiblingModel extends Model> { /// If the method can't be found, `null` is returned. MethodElement? get toJsonMethod { if (targetType.element is ClassElement) { - for (final method in (targetType.element as ClassElement).methods) { + for (final method in (targetType.element! as ClassElement).methods) { if (method.name == 'toJson') return method; } } @@ -243,8 +262,7 @@ class SharedChecker<_SiblingModel extends Model> { } /// Print the `DartType` without nullability - static String withoutNullability(DartType type) => - type.getDisplayString(withNullability: true).replaceAll('?', ''); + static String withoutNullability(DartType type) => type.getDisplayString().replaceAll('?', ''); /// Destructs a type to determine the bottom type after going through Futures and Iterables. /// diff --git a/packages/brick_build/lib/src/utils/string_helpers.dart b/packages/brick_build/lib/src/utils/string_helpers.dart index 6647a759..b7b148d1 100644 --- a/packages/brick_build/lib/src/utils/string_helpers.dart +++ b/packages/brick_build/lib/src/utils/string_helpers.dart @@ -1,25 +1,26 @@ +// ignore: avoid_classes_with_only_static_members +/// class StringHelpers { + /// See [_EscapedDartString]. static String escape(String contents) => _EscapedDartString(contents).toString(); /// Convert a camelized string to snake_case /// e.g. `aLongFieldName` becomes `a_long_field_name` /// Taken from [json_serializable](https://github.com/dart-lang/json_serializable/blob/d7e6612cf947e150710007a63b439f8f0c316d42/json_serializable/lib/src/utils.dart#L38-L47) - static String snakeCase(String input) { - return input.replaceAllMapped(RegExp('[A-Z]'), (match) { - var lower = match.group(0)!.toLowerCase(); + static String snakeCase(String input) => input.replaceAllMapped(RegExp('[A-Z]'), (match) { + var lower = match.group(0)!.toLowerCase(); - if (match.start > 0) { - lower = '_$lower'; - } + if (match.start > 0) { + lower = '_$lower'; + } - return lower; - }); - } + return lower; + }); } // Borrowed from [JsonSerializable](https://github.com/dart-lang/json_serializable/blob/9fcee71528f17f8e9e80e90003264e84d048977b/json_serializable/lib/src/utils.dart) -/// Returns a quoted String literal for [value] that can be used in generated +/// Returns a quoted String literal for [contents] that can be used in generated /// Dart code. class _EscapedDartString { final String contents; diff --git a/packages/brick_build/pubspec.yaml b/packages/brick_build/pubspec.yaml index e168d521..3b9c9835 100644 --- a/packages/brick_build/pubspec.yaml +++ b/packages/brick_build/pubspec.yaml @@ -4,14 +4,14 @@ homepage: https://github.com/GetDutchie/brick/tree/main/packages/brick_build issue_tracker: https://github.com/GetDutchie/brick/issues repository: https://github.com/GetDutchie/brick -version: 3.2.1 +version: 3.3.0 environment: sdk: ">=2.18.0 <4.0.0" dependencies: analyzer: ">=6.0.0 <7.0.0" - brick_core: ^1.1.1 + brick_core: ^1.3.0 build: ^2.3.0 dart_style: ">=2.0.0 <3.0.0" glob: ">=2.1.0 <3.0.0" @@ -23,6 +23,6 @@ dependencies: dev_dependencies: brick_build_test: path: ../brick_build_test - build_verify: ^2.0.0 - lints: ^2.0.1 - test: ^1.20.1 + build_verify: + lints: + test: diff --git a/packages/brick_build/test/__helpers__.dart b/packages/brick_build/test/__helpers__.dart index 4bd9fb45..a393f9c7 100644 --- a/packages/brick_build/test/__helpers__.dart +++ b/packages/brick_build/test/__helpers__.dart @@ -41,7 +41,7 @@ class FieldAnnotationFinder extends AnnotationFinder { FieldAnnotationFinder(); @override - FieldAnnotation from(element) => FieldAnnotation(element.name); + FieldAnnotation from(FieldElement element) => FieldAnnotation(element.name); } class TestFields extends FieldsForClass { @@ -59,7 +59,12 @@ class DefaultSerdes extends SerdesGenerator { final providerName = 'DefaultSerdes'; @override - String? coderForField(field, checker, {required fieldAnnotation, required wrappedInFuture}) => + String? coderForField( + FieldElement field, + SharedChecker checker, { + required FieldAnnotation fieldAnnotation, + required bool wrappedInFuture, + }) => null; } @@ -103,7 +108,12 @@ class CustomSerdes extends SerdesGenerator { final repositoryName = 'Some'; @override - String coderForField(field, checker, {required fieldAnnotation, required wrappedInFuture}) { + String coderForField( + FieldElement field, + SharedChecker checker, { + required FieldAnnotation fieldAnnotation, + required bool wrappedInFuture, + }) { final fieldValue = serdesValueForField(field, fieldAnnotation.name, checker: checker); final wrappedCheckerType = wrappedInFuture ? 'Future<${checker.targetType}>' : checker.targetType.toString(); diff --git a/packages/brick_build/test/provider_serializable_generator_test.dart b/packages/brick_build/test/provider_serializable_generator_test.dart index fe23c0f3..249e662f 100644 --- a/packages/brick_build/test/provider_serializable_generator_test.dart +++ b/packages/brick_build/test/provider_serializable_generator_test.dart @@ -5,7 +5,7 @@ import 'package:test/test.dart'; import '__helpers__.dart'; final _generator = TestGenerator(); -final folder = 'provider_serializable_generator'; +const folder = 'provider_serializable_generator'; final generateReader = generateLibraryForFolder(folder); void main() { @@ -15,7 +15,7 @@ void main() { final reader = await generateReader('annotated_method'); expect( () async => await _generator.generate(reader, MockBuildStep()), - throwsA(TypeMatcher()), + throwsA(const TypeMatcher()), ); }); @@ -23,7 +23,7 @@ void main() { final reader = await generateReader('annotated_top_level_variable'); expect( () async => await _generator.generate(reader, MockBuildStep()), - throwsA(TypeMatcher()), + throwsA(const TypeMatcher()), ); }); @@ -31,7 +31,7 @@ void main() { final reader = await generateReader('future_iterable_future'); expect( () async => await _generator.generate(reader, MockBuildStep()), - throwsA(TypeMatcher()), + throwsA(const TypeMatcher()), ); }); }); diff --git a/packages/brick_build/test/serdes_generator_test.dart b/packages/brick_build/test/serdes_generator_test.dart index df535034..3d02ee54 100644 --- a/packages/brick_build/test/serdes_generator_test.dart +++ b/packages/brick_build/test/serdes_generator_test.dart @@ -97,14 +97,14 @@ void main() { }); test('#generate', () { - final defaultOutput = r''' + const defaultOutput = r''' Future _$SimpleFromDefaultSerdes(Map data, {required DefaultSerdesProvider provider, ModelRepository? repository}) async { return Simple(); } '''; - final customOutput = ''' + const customOutput = ''' Future unspecificPublicMethod(Map, {provider, SomeRepository repository}) async { return {'someField': instance.someField as int}..nullableField = true; @@ -120,7 +120,7 @@ Future unspecificPublicMethod(Map, () => SerdesGenerator.digestCustomGeneratorPlaceholders( '%UNDECLARED_VARIABLE%otherserialization', ), - throwsA(TypeMatcher()), + throwsA(const TypeMatcher()), ); }); @@ -129,7 +129,7 @@ Future unspecificPublicMethod(Map, () => SerdesGenerator.digestCustomGeneratorPlaceholders( '%UNDEFINED_VALUE%otherserialization@UNDEFINED_VALUE@', ), - throwsA(TypeMatcher()), + throwsA(const TypeMatcher()), ); // Malformed declaration @@ -137,7 +137,7 @@ Future unspecificPublicMethod(Map, () => SerdesGenerator.digestCustomGeneratorPlaceholders( '%UNDEFINED_VALUE%otherserialization@UNDEFINED_VALUE', ), - throwsA(TypeMatcher()), + throwsA(const TypeMatcher()), ); }); diff --git a/packages/brick_build_test/lib/brick_build_test.dart b/packages/brick_build_test/lib/brick_build_test.dart index 42bef80e..2e84cd56 100644 --- a/packages/brick_build_test/lib/brick_build_test.dart +++ b/packages/brick_build_test/lib/brick_build_test.dart @@ -11,6 +11,7 @@ Future _libraryForFolder(String folder, String filename) async { ); } +/// typedef LibraryGenerator = Future Function(String filename); /// Thunks a reader generator that assumes the filename is prefixed `test_` @@ -31,6 +32,7 @@ Future annotationForFile<_Annotation>(String folder, String fi } // ignore: subtype_of_sealed_class +/// class MockBuildStep extends BuildStep { @override dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); diff --git a/packages/brick_core/CHANGELOG.md b/packages/brick_core/CHANGELOG.md index 74ac30cc..0d479345 100644 --- a/packages/brick_core/CHANGELOG.md +++ b/packages/brick_core/CHANGELOG.md @@ -1,5 +1,20 @@ ## Unreleased +## 1.3.1 + +- `const`antize `Where.exactly` and `OrderBy.{desc|asc}` +- Add deprecation annotation to `Query#copyWith#providerArgs` + +## 1.3.0 + +- **DEPRECATION** `Query(providerArgs: {'limit':})` is now `Query(limit:)` +- **DEPRECATION** `Query(providerArgs: {'offset':})` is now `Query(offset:)` +- **DEPRECATION** `Query(providerArgs: {'orderBy':})` is now `Query(orderBy:)`. `orderBy` is now defined by a class that permits multiple commands. For example, `'orderBy': 'name ASC'` becomes `[OrderBy('name', ascending: true)]`. +- **DEPRECATION** `providerArgs` will be removed in the next major release +- `OrderBy` will support association ordering and multiple values +- `Query` is constructed with `const` +- `Query#offset` no longer requires companion `limit` parameter + ## 1.2.1 - Add `FieldRename` to `FieldSerializable` diff --git a/packages/brick_core/lib/field_serializable.dart b/packages/brick_core/lib/field_serializable.dart index 857c67ce..25c23ffb 100644 --- a/packages/brick_core/lib/field_serializable.dart +++ b/packages/brick_core/lib/field_serializable.dart @@ -24,7 +24,7 @@ abstract class FieldSerializable { /// /// `data` and `provider` is available as the deserialized version of the model. /// - /// Placeholders can be used in the value of this field. + /// Placeholders (i.e. `%DATA_PROPERTY%`) can be used in the value of this field. String? get fromGenerator; /// `true` if the generator should ignore this field completely. @@ -54,7 +54,7 @@ abstract class FieldSerializable { /// /// `instance` and `provider` is available as the invoking model. /// - /// Placeholders can be used in the value of this field. + /// Placeholders (i.e. `%INSTANCE_PROPERTY%`) can be used in the value of this field. String? get toGenerator; /// Placeholder. Replaces with name (e.g. `@Rest(name:)` or `@Sqlite(name:)`). diff --git a/packages/brick_core/lib/query.dart b/packages/brick_core/lib/query.dart index 81089ac1..6a19689e 100644 --- a/packages/brick_core/lib/query.dart +++ b/packages/brick_core/lib/query.dart @@ -1,3 +1,6 @@ export 'package:brick_core/src/query/and_or.dart'; +export 'package:brick_core/src/query/limit_by.dart'; +export 'package:brick_core/src/query/order_by.dart'; +export 'package:brick_core/src/query/provider_query.dart'; export 'package:brick_core/src/query/query.dart'; export 'package:brick_core/src/query/where.dart'; diff --git a/packages/brick_core/lib/src/adapter.dart b/packages/brick_core/lib/src/adapter.dart index 79a555a6..6315cb4f 100644 --- a/packages/brick_core/lib/src/adapter.dart +++ b/packages/brick_core/lib/src/adapter.dart @@ -1,7 +1,10 @@ import 'package:brick_core/src/model.dart'; +import 'package:brick_core/src/provider.dart'; -/// An adapter is a factory that produces an app model. In an effort to normalize data input and +/// An adapter is a factory that produces an app [Model]. In an effort to normalize data input and /// output between [Provider]s, subclasses must pass the data in `Map` format. abstract class Adapter<_Model extends Model> { + /// An adapter is a factory that produces an app [Model]. In an effort to normalize data input and + /// output between [Provider]s, subclasses must pass the data in `Map` format. const Adapter(); } diff --git a/packages/brick_core/lib/src/model.dart b/packages/brick_core/lib/src/model.dart index f46126e0..c29d1050 100644 --- a/packages/brick_core/lib/src/model.dart +++ b/packages/brick_core/lib/src/model.dart @@ -1,6 +1,12 @@ -/// A model can be queried by the [Repository], and if merited by the [Repository] implementation, +import 'package:brick_core/src/model_repository.dart'; +import 'package:brick_core/src/provider.dart'; + +/// A model can be queried by the [ModelRepository], and if merited by the [ModelRepository] implementation, /// the [Provider]. Subclasses may extend [Model] to include Repository-specific needs, /// such as an HTTP endpoint or a table name. abstract class Model { + /// A model can be queried by the [ModelRepository], and if merited by the [ModelRepository] implementation, + /// the [Provider]. Subclasses may extend [Model] to include Repository-specific needs, + /// such as an HTTP endpoint or a table name. const Model(); } diff --git a/packages/brick_core/lib/src/model_dictionary.dart b/packages/brick_core/lib/src/model_dictionary.dart index b272735c..9ed4c97a 100644 --- a/packages/brick_core/lib/src/model_dictionary.dart +++ b/packages/brick_core/lib/src/model_dictionary.dart @@ -1,5 +1,6 @@ import 'package:brick_core/src/adapter.dart'; import 'package:brick_core/src/model.dart'; +import 'package:brick_core/src/provider.dart'; /// A modelDictionary points a [Provider] to the [Model]'s [Adapter]. The [Provider] uses it to construct /// app models from raw data. @@ -12,5 +13,11 @@ abstract class ModelDictionary adapterFor; + /// A modelDictionary points a [Provider] to the [Model]'s [Adapter]. The [Provider] uses it to construct + /// app models from raw data. + /// + /// It should only be instantiated once, even if multiple [Provider]s are used. The end instantiation + /// is left to the end user in case `const` (favored over `final`) can be used for + /// all [Adapter] mappings. const ModelDictionary(this.adapterFor); } diff --git a/packages/brick_core/lib/src/model_repository.dart b/packages/brick_core/lib/src/model_repository.dart index 2a27201e..1bad93e9 100644 --- a/packages/brick_core/lib/src/model_repository.dart +++ b/packages/brick_core/lib/src/model_repository.dart @@ -1,36 +1,42 @@ +// ignore_for_file: type_annotate_public_apis, always_declare_return_types + import 'dart:async'; import 'package:brick_core/src/model.dart'; import 'package:brick_core/src/provider.dart'; import 'package:brick_core/src/query/query.dart'; -/// A Repository is the top-level means of relaying data between [Model]s and [Provider]s. +/// A [ModelRepository] is the top-level means of relaying data between [Model]s and [Provider]s. /// A conventional implementation would adhere to the singleton pattern. /// /// It should handle the app's caching strategy between [Provider]s. For example, if an app has -/// an offline-first caching strategy, the Repository first fetches from a `SqliteProvider` -/// and then a `RestProvider` before returning one result. An app should have one [Repository] for +/// an offline-first caching strategy, the [ModelRepository] first fetches from a `SqliteProvider` +/// and then a `RestProvider` before returning one result. An app should have one `Repository` for /// one data flow (similar to having one Redux store as the source of truth). /// -/// `implement`ing this class is not necessary. It's supplied as a standardized, opinionated way to -/// structure your `Store`. +/// `implement`ing this class is not necessary. abstract class ModelRepository { + /// A [ModelRepository] is the top-level means of relaying data between [Model]s and [Provider]s. + /// A conventional implementation would adhere to the singleton pattern. + /// + /// It should handle the app's caching strategy between [Provider]s. For example, if an app has + /// an offline-first caching strategy, the [ModelRepository] first fetches from a `SqliteProvider` + /// and then a `RestProvider` before returning one result. An app should have one `Repository` for + /// one data flow (similar to having one Redux store as the source of truth). + /// + /// `implement`ing this class is not necessary. const ModelRepository(); /// Delete a model from all [Provider]s. /// - /// Optionally, the repository can - /// be passed to the same provider method with a named argument (`repository: this`) to use in - /// the [Adapter]. - // ignore: always_declare_return_types + /// Optionally, the repository can be passed to the same provider method + /// with a named argument (`repository: this`) to use in the `Adapter`. delete(TModel instance, {Query query}); /// Query for raw data from all [Provider]s. /// - /// Optionally, the repository can - /// be passed to the same provider method with a named argument (`repository: this`) to use in - /// the [Adapter]. - // ignore: always_declare_return_types + /// Optionally, the repository can be passed to the same provider method + /// with a named argument (`repository: this`) to use in the `Adapter`. get({Query query}); /// Perform required setup work. For example, migrating a database, starting a queue, @@ -40,18 +46,17 @@ abstract class ModelRepository { /// Insert or update a model in all [Provider]s /// - /// Optionally, the repository can - /// be passed to the same provider method with a named argument (`repository: this`) to use in - /// the [Adapter]. - // ignore: always_declare_return_types + /// Optionally, the repository can be passed to the same provider method + /// with a named argument (`repository: this`) to use in the `Adapter`. upsert(TModel model, {Query query}); } /// Helper for mono provider systems abstract class SingleProviderRepository implements ModelRepository { - /// The only provider for the store + /// The only provider for the repository final Provider provider; + /// Helper for mono provider systems const SingleProviderRepository(this.provider); /// Remove models from providers diff --git a/packages/brick_core/lib/src/provider.dart b/packages/brick_core/lib/src/provider.dart index 7d0b1dec..3502d4a8 100644 --- a/packages/brick_core/lib/src/provider.dart +++ b/packages/brick_core/lib/src/provider.dart @@ -1,3 +1,6 @@ +// ignore_for_file: type_annotate_public_apis, always_declare_return_types + +import 'package:brick_core/src/adapter.dart'; import 'package:brick_core/src/model.dart'; import 'package:brick_core/src/model_dictionary.dart'; import 'package:brick_core/src/model_repository.dart'; @@ -8,23 +11,20 @@ abstract class Provider { /// The translation between [Adapter]s and [Model]s ModelDictionary get modelDictionary; + /// A [Provider] fetches raw data and creates [Model]s. An app can have many [Provider]s. const Provider(); /// Remove a model instance - // ignore: always_declare_return_types delete(T instance, {Query? query, ModelRepository? repository}); /// Whether a model instance is present. `null` is returned when existence is unknown. /// The model instance is not hydrated in the function output; a `bool` variant /// (e.g. `List`, `Map`) should be returned. - // ignore: always_declare_return_types exists({Query? query, ModelRepository? repository}) => null; /// Query for raw data and construct it with an [Adapter] - // ignore: always_declare_return_types get({Query? query, ModelRepository? repository}); /// Insert or update a model instance - // ignore: always_declare_return_types upsert(T instance, {Query? query, ModelRepository? repository}); } diff --git a/packages/brick_core/lib/src/query/and_or.dart b/packages/brick_core/lib/src/query/and_or.dart index 44ae0a74..a75bbfd0 100644 --- a/packages/brick_core/lib/src/query/and_or.dart +++ b/packages/brick_core/lib/src/query/and_or.dart @@ -2,6 +2,7 @@ import 'package:brick_core/src/query/where.dart'; /// Generate a required condition. class And extends Where { + /// Generate a required condition. const And( super.evaluatedField, ) : super(isRequired: true); @@ -9,6 +10,7 @@ class And extends Where { /// Generate an optional condition. class Or extends Where { + /// Generate an optional condition. const Or( super.evaluatedField, ) : super(isRequired: false); diff --git a/packages/brick_core/lib/src/query/limit_by.dart b/packages/brick_core/lib/src/query/limit_by.dart new file mode 100644 index 00000000..c157a2bb --- /dev/null +++ b/packages/brick_core/lib/src/query/limit_by.dart @@ -0,0 +1,43 @@ +import 'dart:convert'; + +import 'package:brick_core/src/adapter.dart'; + +/// Construct directions for a provider to limit its results. +class LimitBy { + /// The ceiling for how many results can be returned for [evaluatedField]. + final int amount; + + /// Some providers may support limiting based on a model retrieved by the query. + /// This Dart field name should be accessible to the [Adapter]'s definitions + /// (e.g. a `RuntimeSqliteColumnDefinition` map). + final String evaluatedField; + + /// Construct directions for a provider to limit its results. + const LimitBy( + this.amount, { + required this.evaluatedField, + }); + + /// Construct a [LimitBy] from a JSON map. + factory LimitBy.fromJson(Map json) => LimitBy( + json['amount'], + evaluatedField: json['evaluatedField'], + ); + + /// Serialize to JSON + Map toJson() => { + 'amount': amount, + 'evaluatedField': evaluatedField, + }; + + @override + String toString() => jsonEncode(toJson()); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is LimitBy && amount == other.amount && evaluatedField == other.evaluatedField; + + @override + int get hashCode => amount.hashCode ^ evaluatedField.hashCode; +} diff --git a/packages/brick_core/lib/src/query/order_by.dart b/packages/brick_core/lib/src/query/order_by.dart new file mode 100644 index 00000000..445195b9 --- /dev/null +++ b/packages/brick_core/lib/src/query/order_by.dart @@ -0,0 +1,60 @@ +import 'package:brick_core/src/provider.dart'; + +/// Construct directions for a provider to sort its results. +class OrderBy { + /// Defaults to `true`. + final bool ascending; + + /// The Dart name of the field. For example, `myField` when querying `final String myField`. + /// + /// The [Provider] should provide mappings between the field name + /// and the remote source's expected name. + final String evaluatedField; + + /// The Dart name of the field of the association model + /// if the [evaluatedField] is an association. + /// + /// If [evaluatedField] is not an association, this should be `null`. + final String? associationField; + + /// Construct directions for a provider to sort its results. + const OrderBy( + this.evaluatedField, { + this.ascending = true, + this.associationField, + }); + + /// Sort by [ascending] order (A-Z). + const OrderBy.asc(this.evaluatedField, {this.associationField}) : ascending = true; + + /// Sort by descending order (Z-A). + const OrderBy.desc(this.evaluatedField, {this.associationField}) : ascending = false; + + /// Construct an [OrderBy] from a JSON map. + factory OrderBy.fromJson(Map json) => OrderBy( + json['evaluatedField'], + ascending: json['ascending'], + associationField: json['associationField'], + ); + + /// Serialize to JSON + Map toJson() => { + 'ascending': ascending, + if (associationField != null) 'associationField': associationField, + 'evaluatedField': evaluatedField, + }; + + @override + String toString() => '$evaluatedField ${ascending ? 'ASC' : 'DESC'}'; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is OrderBy && + evaluatedField == other.evaluatedField && + ascending == other.ascending && + associationField == other.associationField; + + @override + int get hashCode => evaluatedField.hashCode ^ ascending.hashCode ^ associationField.hashCode; +} diff --git a/packages/brick_core/lib/src/query/provider_query.dart b/packages/brick_core/lib/src/query/provider_query.dart new file mode 100644 index 00000000..3b69439c --- /dev/null +++ b/packages/brick_core/lib/src/query/provider_query.dart @@ -0,0 +1,26 @@ +import 'package:brick_core/src/provider.dart'; + +/// Specify query arguments that are exclusive to a specific [Provider]. +/// For example, configuring a REST's POST method. +/// +/// Implementations must specify the generic type argument as [Provider] +/// will read `Query` for this type. Implementations should also specify +/// equality operators. +/// +/// [Provider] implementations should expect only one [ProviderQuery] per [T]. +abstract class ProviderQuery { + /// `Query` will build a map keyed by this provider, or [T]. + Type get provider => T; + + /// Specify query arguments that are exclusive to a specific [Provider]. + /// For example, configuring a REST's POST method. + /// + /// Implementations must specify the generic type argument as [Provider] + /// will read `Query` for this type. + /// + /// [Provider] implementations should expect only one [ProviderQuery] per [T]. + const ProviderQuery(); + + /// Serialize to JSON + Map toJson(); +} diff --git a/packages/brick_core/lib/src/query/query.dart b/packages/brick_core/lib/src/query/query.dart index 4f9e3ecd..afb22e84 100644 --- a/packages/brick_core/lib/src/query/query.dart +++ b/packages/brick_core/lib/src/query/query.dart @@ -1,27 +1,62 @@ +// ignore_for_file: deprecated_member_use_from_same_package + import 'dart:convert'; +import 'package:brick_core/src/model_repository.dart'; +import 'package:brick_core/src/provider.dart'; +import 'package:brick_core/src/query/limit_by.dart'; +import 'package:brick_core/src/query/order_by.dart'; +import 'package:brick_core/src/query/provider_query.dart'; import 'package:brick_core/src/query/where.dart'; -import 'package:collection/collection.dart' show MapEquality, ListEquality; +import 'package:collection/collection.dart' show ListEquality, MapEquality; const _mapEquality = MapEquality(); const _listEquality = ListEquality(); -/// An interface to request data from a [Provider] or [Repository]. +/// An interface to request data from a [Provider] or [ModelRepository]. class Query { /// How this query interacts with its invoking provider. /// - /// Often the invoking [Repository] will appropriately adjust the [action] when + /// Often the invoking [ModelRepository] will appropriately adjust the [action] when /// interacting with the provider. For example: /// ```dart /// upsert(query) => final q = query.copyWith(action: QueryAction.upsert) /// ``` final QueryAction? action; + /// [Provider]-specific query arguments. + /// Only one [ProviderQuery] per [Provider] is permitted. + final List forProviders; + + /// The response should not exceed this number. + /// For advanced cases, see [limitBy]. + final int? limit; + + /// Directions for limiting associated model results before they're returned to the caller. + /// [limit] will restrict the top-level queried model. + final List limitBy; + + /// The response should start at this index. + final int? offset; + + /// Directions for sorting results before they're returned to the caller. + final List orderBy; + /// Properties that interact with the provider's source. For example, `'limit'`. /// The value **must** be serializable by `jsonEncode`. + @Deprecated('Use limit, offset, limitBy, orderBy, or forProviders instead') final Map providerArgs; - bool get unlimited => providerArgs['limit'] == null || providerArgs['limit'] < 1; + /// Available for [Provider]s to easily access their relevant + /// [ProviderQuery]s. + Map get providerQueries => + forProviders.fold({}, (acc, p) { + acc[p.provider] = p; + return acc; + }); + + /// When [limit] is undefined or less than 1, the query is considered "unlimited". + bool get unlimited => limit == null || limit! < 1; /// Model properties to be interpreted by the [Provider]. /// When creating [WhereCondition]s, the first positional `fieldName` argument @@ -44,30 +79,30 @@ class Query { /// will only return results where the ID is 1 **and** the name is Thomas. final List? where; - Query({ + /// An interface to request data from a [Provider] or [ModelRepository]. + const Query({ this.action, - Map? providerArgs, + this.forProviders = const [], + this.limit, + this.limitBy = const [], + this.offset, + @Deprecated('Use limit, offset, limitBy, orderBy, or forProviders instead.') + this.providerArgs = const {}, + this.orderBy = const [], this.where, - }) : providerArgs = providerArgs ?? {} { - /// Number of results first returned from query; `0` returns all. Must be greater than -1 - if (this.providerArgs['limit'] != null) { - assert(this.providerArgs['limit'] > -1); - } - - /// Offset results returned from query. Must be greater than -1 and must be used with limit - if (this.providerArgs['offset'] != null) { - assert(this.providerArgs['offset'] > -1); - assert(this.providerArgs['limit'] != null); - } - } - - factory Query.fromJson(Map json) { - return Query( - action: json['action'] == null ? null : QueryAction.values[json['action']], - providerArgs: json['providerArgs'], - where: json['where']?.map(WhereCondition.fromJson), - ); - } + }) : assert(limit == null || limit > -1, 'limit must be greater than 0'), + assert(offset == null || offset > -1, 'offset must be greater than 0'); + + /// Deserialize from JSON + factory Query.fromJson(Map json) => Query( + action: json['action'] == null ? null : QueryAction.values[json['action']], + limit: json['limit'] as int?, + limitBy: json['limitBy']?.map(LimitBy.fromJson).toList() ?? [], + offset: json['offset'] as int?, + orderBy: json['orderBy']?.map(OrderBy.fromJson).toList() ?? [], + providerArgs: Map.from(json['providerArgs'] as Map? ?? {}), + where: json['where']?.map(WhereCondition.fromJson), + ); /// Make a _very_ simple query with a single [Where] statement. /// For example `Query.where('id', 1)`. @@ -75,37 +110,51 @@ class Query { /// [limit1] adds a limit param when `true`. Defaults to `false`. factory Query.where( String evaluatedField, - dynamic value, { + value, { Compare? compare, bool limit1 = false, }) { compare ??= Where.defaults.compare; return Query( where: [Where(evaluatedField, value: value, compare: compare)], - providerArgs: { - if (limit1) 'limit': 1, - }, + limit: limit1 ? 1 : null, ); } + /// Reconstruct the [Query] with passed overrides Query copyWith({ QueryAction? action, + List? forProviders, + int? limit, + List? limitBy, + int? offset, + List? orderBy, + @Deprecated('Use limit, offset, limitBy, orderBy, or forProviders instead.') Map? providerArgs, List? where, }) => Query( action: action ?? this.action, + forProviders: forProviders ?? this.forProviders, + limit: limit ?? this.limit, + limitBy: limitBy ?? this.limitBy, + offset: offset ?? this.offset, + orderBy: orderBy ?? this.orderBy, providerArgs: providerArgs ?? this.providerArgs, where: where ?? this.where, ); - Map toJson() { - return { - if (action != null) 'action': QueryAction.values.indexOf(action!), - 'providerArgs': providerArgs, - if (where != null) 'where': where!.map((w) => w.toJson()).toList(), - }; - } + /// Serialize to JSON + Map toJson() => { + if (action != null) 'action': QueryAction.values.indexOf(action!), + if (forProviders.isNotEmpty) 'forProviders': forProviders.map((p) => p.toJson()).toList(), + if (limit != null) 'limit': limit, + if (limitBy.isNotEmpty) 'limitBy': limitBy.map((l) => l.toJson()).toList(), + if (offset != null) 'offset': offset, + if (providerArgs.isNotEmpty) 'providerArgs': providerArgs, + if (orderBy.isNotEmpty) 'orderBy': orderBy.map((s) => s.toJson()).toList(), + if (where != null) 'where': where!.map((w) => w.toJson()).toList(), + }; @override String toString() => jsonEncode(toJson()); @@ -115,11 +164,24 @@ class Query { identical(this, other) || other is Query && action == other.action && + limit == other.limit && + offset == other.offset && + _listEquality.equals(forProviders, other.forProviders) && + _listEquality.equals(limitBy, other.limitBy) && + _listEquality.equals(orderBy, other.orderBy) && _mapEquality.equals(providerArgs, other.providerArgs) && _listEquality.equals(where, other.where); @override - int get hashCode => action.hashCode ^ providerArgs.hashCode ^ where.hashCode; + int get hashCode => + action.hashCode ^ + forProviders.hashCode ^ + limit.hashCode ^ + limitBy.hashCode ^ + offset.hashCode ^ + orderBy.hashCode ^ + providerArgs.hashCode ^ + where.hashCode; } /// How the query interacts with the provider diff --git a/packages/brick_core/lib/src/query/where.dart b/packages/brick_core/lib/src/query/where.dart index 645879f0..4a6f1ad1 100644 --- a/packages/brick_core/lib/src/query/where.dart +++ b/packages/brick_core/lib/src/query/where.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import 'package:brick_core/core.dart'; import 'package:collection/collection.dart' show ListEquality; const _listEquality = ListEquality(); @@ -21,9 +22,9 @@ abstract class WhereCondition { String get evaluatedField; /// Nested conditions. Leave unchanged for [WhereCondition]s that do not nest. - final List? conditions = null; + List? get conditions; - /// The kind of comparison of the [evaluatedField] to the [value]. Defaults to [Compare.equals]. + /// The kind of comparison of the [evaluatedField] to the [value]. Defaults to [Compare.exact]. /// It is the responsibility of the [Provider] to ignore or interpret the requested comparison. Compare get compare; @@ -36,8 +37,18 @@ abstract class WhereCondition { /// The value to compare on the [evaluatedField]. dynamic get value; + /// Lower-level control over the value of a `Query#where` map. + /// + /// Example: + /// ```dart + /// Query(where: [ + /// Where.exact('myField', 'must_match_this_value') + /// Where('myOtherField').contains('must_contain_this_value'), + /// ]) + /// ``` const WhereCondition(); + /// Deserialize from JSON factory WhereCondition.fromJson(Map data) { if (data['subclass'] == 'WherePhrase') { return WherePhrase( @@ -54,16 +65,15 @@ abstract class WhereCondition { ); } - Map toJson() { - return { - 'subclass': runtimeType.toString(), - if (evaluatedField.isNotEmpty) 'evaluatedField': evaluatedField, - 'compare': Compare.values.indexOf(compare), - if (conditions != null) 'conditions': conditions!.map((s) => s.toJson()).toList(), - 'required': isRequired, - if (value != null) 'value': value, - }; - } + /// Serialize to JSON + Map toJson() => { + 'subclass': runtimeType.toString(), + if (evaluatedField.isNotEmpty) 'evaluatedField': evaluatedField, + 'compare': Compare.values.indexOf(compare), + if (conditions != null) 'conditions': conditions!.map((s) => s.toJson()).toList(), + 'required': isRequired, + if (value != null) 'value': value, + }; @override String toString() => jsonEncode(toJson()); @@ -89,6 +99,10 @@ abstract class WhereCondition { value.hashCode; } +/// A condition that evaluates to `true` in the [Provider] should return [Model](s). +/// +/// This class should be exposed by the implemented [ModelRepository] and not imported from +/// this package as repositories may choose to extend or inhibit functionality. class Where extends WhereCondition { @override final String evaluatedField; @@ -96,17 +110,21 @@ class Where extends WhereCondition { @override final Compare compare; + @override + final List? conditions; + @override final bool isRequired; @override final dynamic value; + /// Default values for [Where] static const defaults = Where(''); - /// A condition that evaluates to `true` in the [Provider] should return [Model](s). + /// A condition that evaluates to `true` in the [Provider] should return [Model](s). /// - /// This class should be exposed by the implemented [Repository] and not imported from + /// This class should be exposed by the implemented [ModelRepository] and not imported from /// this package as repositories may choose to extend or inhibit functionality. const Where( this.evaluatedField, { @@ -114,15 +132,19 @@ class Where extends WhereCondition { Compare? compare, bool? isRequired, }) : isRequired = isRequired ?? true, - compare = compare ?? Compare.exact; + compare = compare ?? Compare.exact, + conditions = null; - /// A condition written with brevity. [required] defaults `true`. - factory Where.exact(String evaluatedField, dynamic value, {bool isRequired = true}) => - Where(evaluatedField, value: value, compare: Compare.exact, isRequired: isRequired); + /// A condition written with brevity. [isRequired] defaults `true`. + const Where.exact(this.evaluatedField, this.value, {this.isRequired = true}) + : compare = Compare.exact, + conditions = null; + /// Convenience function to create a [Where] with [Compare.exact]. Where isExactly(dynamic value) => Where(evaluatedField, value: value, compare: Compare.exact, isRequired: isRequired); + /// Convenience function to create a [Where] with [Compare.between]. Where isBetween(dynamic value1, dynamic value2) { assert(value1.runtimeType == value2.runtimeType, 'Comparison values must be the same type'); return Where( @@ -133,15 +155,19 @@ class Where extends WhereCondition { ); } + /// Convenience function to create a [Where] with [Compare.contains]. Where contains(dynamic value) => Where(evaluatedField, value: value, compare: Compare.contains, isRequired: isRequired); + /// Convenience function to create a [Where] with [Compare.doesNotContain]. Where doesNotContain(dynamic value) => Where(evaluatedField, value: value, compare: Compare.doesNotContain, isRequired: isRequired); + /// Convenience function to create a [Where] with [Compare.lessThan]. Where isLessThan(dynamic value) => Where(evaluatedField, value: value, compare: Compare.lessThan, isRequired: isRequired); + /// Convenience function to create a [Where] with [Compare.lessThanOrEqualTo]. Where isLessThanOrEqualTo(dynamic value) => Where( evaluatedField, value: value, @@ -149,9 +175,11 @@ class Where extends WhereCondition { isRequired: isRequired, ); + /// Convenience function to create a [Where] with [Compare.greaterThan]. Where isGreaterThan(dynamic value) => Where(evaluatedField, value: value, compare: Compare.greaterThan, isRequired: isRequired); + /// Convenience function to create a [Where] with [Compare.greaterThanOrEqualTo]. Where isGreaterThanOrEqualTo(dynamic value) => Where( evaluatedField, value: value, @@ -159,6 +187,7 @@ class Where extends WhereCondition { isRequired: isRequired, ); + /// Convenience function to create a [Where] with [Compare.notEqual]. Where isNot(dynamic value) => Where(evaluatedField, value: value, compare: Compare.notEqual, isRequired: isRequired); @@ -191,15 +220,38 @@ class Where extends WhereCondition { } } +/// A collection of conditions that are evaluated together. +/// +/// If mixing `required:true` `required:false` is necessary, use separate [WherePhrase]s. +/// [WherePhrase]s can be mixed with [Where]. +/// +/// Invalid: +/// ```dart +/// WherePhrase([ +/// Where.exact('myField', true), +/// Or('myOtherField').isExactly(0), +/// ]) +/// ``` +/// +/// Valid: +/// ```dart +/// WherePhrase([ +/// Where.exact('myField', true), +/// WherePhrase([ +/// Or('myOtherField').isExactly(0), +/// Or('myOtherField').isExactly(1), +/// )] +/// ]) +/// ``` class WherePhrase extends WhereCondition { @override - final evaluatedField = ''; + String get evaluatedField => ''; @override - final compare = Compare.exact; + Compare get compare => Compare.exact; @override - final value = null; + dynamic get value => null; /// Whether all [conditions] must evaulate to `true` for the query to return results. /// @@ -208,7 +260,6 @@ class WherePhrase extends WhereCondition { final bool isRequired; @override - // ignore: overridden_fields final List conditions; /// A collection of conditions that are evaluated together. @@ -247,7 +298,7 @@ class WherePhrase extends WhereCondition { }) : isRequired = isRequired ?? false; } -/// Specify how to evalute the [value] against the [evaluatedField] in a [WhereCondition]. +/// Specify how to evalute the [WhereCondition.value] against the [WhereCondition.evaluatedField] in a [WhereCondition]. /// For size operators, a left side comparison is done. /// /// For example, [lessThan] would produce `evaluatedField < value` diff --git a/packages/brick_core/pubspec.yaml b/packages/brick_core/pubspec.yaml index 80470e46..bf5c9326 100644 --- a/packages/brick_core/pubspec.yaml +++ b/packages/brick_core/pubspec.yaml @@ -4,16 +4,15 @@ homepage: https://github.com/GetDutchie/brick/tree/main/packages/brick_core issue_tracker: https://github.com/GetDutchie/brick/issues repository: https://github.com/GetDutchie/brick -version: 1.2.1 +version: 1.3.1 environment: - sdk: ">=2.18.0 <4.0.0" + sdk: ">=3.0.0 <4.0.0" dependencies: collection: ">=1.15.0 <2.0.0" dev_dependencies: - mockito: ^5.0.0 - test: ^1.16.5 - lints: ^2.0.1 - dart_style: ">=2.0.0 <3.0.0" + lints: + mockito: + test: diff --git a/packages/brick_core/test/__mocks__.dart b/packages/brick_core/test/__mocks__.dart index 3a719d05..e9a609e7 100644 --- a/packages/brick_core/test/__mocks__.dart +++ b/packages/brick_core/test/__mocks__.dart @@ -1,3 +1,5 @@ +// ignore_for_file: type_annotate_public_apis + import 'package:brick_core/core.dart'; export 'package:brick_core/core.dart'; @@ -13,26 +15,33 @@ class DemoProvider extends Provider { final DemoModelDictionary modelDictionary; @override - bool delete<_Model extends DemoModel>(instance, {query, repository}) { - return true; - } + bool delete<_Model extends DemoModel>( + instance, { + Query? query, + ModelRepository? repository, + }) => + true; @override - bool exists<_Model extends DemoModel>({query, repository}) { - return true; - } + bool exists<_Model extends DemoModel>({Query? query, ModelRepository? repository}) => + true; @override - Future> get<_Model extends DemoModel>({query, repository}) { - final list = []; - list.add(DemoModel('Thomas')); + Future> get<_Model extends DemoModel>({ + Query? query, + ModelRepository? repository, + }) { + final list = [DemoModel('Thomas')]; return Future.value(list); } @override - bool upsert<_Model extends DemoModel>(instance, {query, repository}) { - return true; - } + bool upsert<_Model extends DemoModel>( + instance, { + Query? query, + ModelRepository? repository, + }) => + true; } class DemoModel extends Model { @@ -48,4 +57,4 @@ class DemoAdapter extends Adapter { const Map mappings = { DemoModel: DemoAdapter(), }; -final modelDictionary = DemoModelDictionary(mappings); +const modelDictionary = DemoModelDictionary(mappings); diff --git a/packages/brick_core/test/provider_test.dart b/packages/brick_core/test/provider_test.dart index d5cd5650..aac1af8f 100644 --- a/packages/brick_core/test/provider_test.dart +++ b/packages/brick_core/test/provider_test.dart @@ -1,4 +1,5 @@ import 'package:test/test.dart'; + import '__mocks__.dart'; void main() { @@ -8,7 +9,7 @@ void main() { test('#get', () async { final res = await provider.get(); expect(res, isList); - expect(res.first, TypeMatcher()); + expect(res.first, const TypeMatcher()); expect(res.first.name, 'Thomas'); }); @@ -19,7 +20,7 @@ void main() { test('#modelDictionary', () { expect(provider.modelDictionary.adapterFor.containsKey(DemoModel), isTrue); - expect(provider.modelDictionary.adapterFor[DemoModel], TypeMatcher()); + expect(provider.modelDictionary.adapterFor[DemoModel], const TypeMatcher()); expect(provider.modelDictionary.adapterFor, mappings); }); }); diff --git a/packages/brick_core/test/query/and_or_test.dart b/packages/brick_core/test/query/and_or_test.dart index 07eac39c..6bc38a24 100644 --- a/packages/brick_core/test/query/and_or_test.dart +++ b/packages/brick_core/test/query/and_or_test.dart @@ -6,100 +6,100 @@ void main() { group('WhereConvenienceInterface', () { test('#isExactly', () { expect( - And('id').isExactly(1), - Where('id', value: 1, compare: Compare.exact, isRequired: true), + const And('id').isExactly(1), + const Where('id', value: 1, compare: Compare.exact, isRequired: true), ); expect( - Or('id').isExactly(1), - Where('id', value: 1, compare: Compare.exact, isRequired: false), + const Or('id').isExactly(1), + const Where('id', value: 1, compare: Compare.exact, isRequired: false), ); }); test('#isBetween', () { expect( - And('id').isBetween(1, 42), - Where('id', value: [1, 42], compare: Compare.between, isRequired: true), + const And('id').isBetween(1, 42), + const Where('id', value: [1, 42], compare: Compare.between, isRequired: true), ); expect( - Or('id').isBetween(1, 42), - Where('id', value: [1, 42], compare: Compare.between, isRequired: false), + const Or('id').isBetween(1, 42), + const Where('id', value: [1, 42], compare: Compare.between, isRequired: false), ); }); test('#contains', () { expect( - And('id').contains(1), - Where('id', value: 1, compare: Compare.contains, isRequired: true), + const And('id').contains(1), + const Where('id', value: 1, compare: Compare.contains, isRequired: true), ); expect( - Or('id').contains(1), - Where('id', value: 1, compare: Compare.contains, isRequired: false), + const Or('id').contains(1), + const Where('id', value: 1, compare: Compare.contains, isRequired: false), ); }); test('#doesNotContain', () { expect( - And('id').doesNotContain(1), - Where('id', value: 1, compare: Compare.doesNotContain, isRequired: true), + const And('id').doesNotContain(1), + const Where('id', value: 1, compare: Compare.doesNotContain, isRequired: true), ); expect( - Or('id').doesNotContain(1), - Where('id', value: 1, compare: Compare.doesNotContain, isRequired: false), + const Or('id').doesNotContain(1), + const Where('id', value: 1, compare: Compare.doesNotContain, isRequired: false), ); }); test('#isLessThan', () { expect( - And('id').isLessThan(1), - Where('id', value: 1, compare: Compare.lessThan, isRequired: true), + const And('id').isLessThan(1), + const Where('id', value: 1, compare: Compare.lessThan, isRequired: true), ); expect( - Or('id').isLessThan(1), - Where('id', value: 1, compare: Compare.lessThan, isRequired: false), + const Or('id').isLessThan(1), + const Where('id', value: 1, compare: Compare.lessThan, isRequired: false), ); }); test('#isLessThanOrEqualTo', () { expect( - And('id').isLessThanOrEqualTo(1), - Where('id', value: 1, compare: Compare.lessThanOrEqualTo, isRequired: true), + const And('id').isLessThanOrEqualTo(1), + const Where('id', value: 1, compare: Compare.lessThanOrEqualTo, isRequired: true), ); expect( - Or('id').isLessThanOrEqualTo(1), - Where('id', value: 1, compare: Compare.lessThanOrEqualTo, isRequired: false), + const Or('id').isLessThanOrEqualTo(1), + const Where('id', value: 1, compare: Compare.lessThanOrEqualTo, isRequired: false), ); }); test('#isGreaterThan', () { expect( - And('id').isGreaterThan(1), - Where('id', value: 1, compare: Compare.greaterThan, isRequired: true), + const And('id').isGreaterThan(1), + const Where('id', value: 1, compare: Compare.greaterThan, isRequired: true), ); expect( - Or('id').isGreaterThan(1), - Where('id', value: 1, compare: Compare.greaterThan, isRequired: false), + const Or('id').isGreaterThan(1), + const Where('id', value: 1, compare: Compare.greaterThan, isRequired: false), ); }); test('#isGreaterThanOrEqualTo', () { expect( - And('id').isGreaterThanOrEqualTo(1), - Where('id', value: 1, compare: Compare.greaterThanOrEqualTo, isRequired: true), + const And('id').isGreaterThanOrEqualTo(1), + const Where('id', value: 1, compare: Compare.greaterThanOrEqualTo, isRequired: true), ); expect( - Or('id').isGreaterThanOrEqualTo(1), - Where('id', value: 1, compare: Compare.greaterThanOrEqualTo, isRequired: false), + const Or('id').isGreaterThanOrEqualTo(1), + const Where('id', value: 1, compare: Compare.greaterThanOrEqualTo, isRequired: false), ); }); test('#isNot', () { expect( - And('id').isNot(1), - Where('id', value: 1, compare: Compare.notEqual, isRequired: true), + const And('id').isNot(1), + const Where('id', value: 1, compare: Compare.notEqual, isRequired: true), ); expect( - Or('id').isNot(1), - Where('id', value: 1, compare: Compare.notEqual, isRequired: false), + const Or('id').isNot(1), + const Where('id', value: 1, compare: Compare.notEqual, isRequired: false), ); }); }); diff --git a/packages/brick_core/test/query/limit_by_test.dart b/packages/brick_core/test/query/limit_by_test.dart new file mode 100644 index 00000000..1dcc6b54 --- /dev/null +++ b/packages/brick_core/test/query/limit_by_test.dart @@ -0,0 +1,27 @@ +import 'package:brick_core/src/query/limit_by.dart'; +import 'package:test/test.dart'; + +void main() { + group('LimitBy', () { + test('equality', () { + expect( + const LimitBy(2, evaluatedField: 'name'), + LimitBy.fromJson(const {'amount': 2, 'evaluatedField': 'name'}), + ); + }); + + test('#toJson', () { + expect( + const LimitBy(2, evaluatedField: 'name').toJson(), + {'amount': 2, 'evaluatedField': 'name'}, + ); + }); + + test('.fromJson', () { + expect( + LimitBy.fromJson(const {'amount': 2, 'evaluatedField': 'longerName'}), + const LimitBy(2, evaluatedField: 'longerName'), + ); + }); + }); +} diff --git a/packages/brick_core/test/query/order_by_test.dart b/packages/brick_core/test/query/order_by_test.dart new file mode 100644 index 00000000..a2f1b9a6 --- /dev/null +++ b/packages/brick_core/test/query/order_by_test.dart @@ -0,0 +1,80 @@ +import 'package:brick_core/src/query/order_by.dart'; +import 'package:test/test.dart'; + +void main() { + group('OrderBy', () { + test('equality', () { + expect( + const OrderBy('name', associationField: 'assoc'), + OrderBy.fromJson( + const {'evaluatedField': 'name', 'ascending': true, 'associationField': 'assoc'}, + ), + ); + expect( + const OrderBy('name', ascending: false, associationField: 'assoc'), + OrderBy.fromJson( + const {'evaluatedField': 'name', 'ascending': false, 'associationField': 'assoc'}, + ), + ); + }); + + test('#toJson', () { + expect( + const OrderBy('name').toJson(), + {'evaluatedField': 'name', 'ascending': true}, + ); + expect( + const OrderBy('name', ascending: false, associationField: 'assoc').toJson(), + {'evaluatedField': 'name', 'ascending': false, 'associationField': 'assoc'}, + ); + }); + + test('#toString', () { + expect( + const OrderBy('name').toString(), + 'name ASC', + ); + expect( + const OrderBy('name', ascending: false).toString(), + 'name DESC', + ); + expect( + const OrderBy('name', ascending: false, associationField: 'assoc').toString(), + 'name DESC', + ); + }); + + test('.asc', () { + expect(const OrderBy.asc('name'), const OrderBy('name')); + expect( + const OrderBy.asc('name', associationField: 'assoc'), + const OrderBy('name', associationField: 'assoc'), + ); + }); + + test('.desc', () { + expect(const OrderBy.desc('name'), const OrderBy('name', ascending: false)); + expect( + const OrderBy.desc('name', associationField: 'assoc'), + const OrderBy('name', ascending: false, associationField: 'assoc'), + ); + }); + + test('.fromJson', () { + expect( + OrderBy.fromJson(const {'evaluatedField': 'name', 'ascending': true}), + const OrderBy('name'), + ); + expect( + OrderBy.fromJson(const {'evaluatedField': 'name', 'ascending': false}), + const OrderBy('name', ascending: false), + ); + expect( + OrderBy.fromJson( + const {'evaluatedField': 'name', 'ascending': false, 'associationField': 'assoc'}, + ), + const OrderBy('name', ascending: false, associationField: 'assoc'), + ); + }); + }); +} diff --git a/packages/brick_core/test/query/query_test.dart b/packages/brick_core/test/query/query_test.dart index 8edfe641..80ade070 100644 --- a/packages/brick_core/test/query/query_test.dart +++ b/packages/brick_core/test/query/query_test.dart @@ -1,199 +1,179 @@ +// ignore_for_file: deprecated_member_use_from_same_package + import 'package:brick_core/src/query/query.dart'; import 'package:brick_core/src/query/where.dart'; import 'package:test/test.dart'; void main() { group('Query', () { - group('properties', () { - test('#action', () { - final q = Query(action: QueryAction.delete); - expect(q.action, QueryAction.delete); - }); - - group('#providerArgs', () { - test('#providerArgs.page and #providerArgs.sort', () { - final q = Query(providerArgs: {'page': 1, 'sort': 'by_user_asc'}); + test('#action', () { + const q = Query(action: QueryAction.delete); + expect(q.action, QueryAction.delete); + }); - expect(q.providerArgs['page'], 1); - expect(q.providerArgs['sort'], 'by_user_asc'); - }); + group('#providerArgs', () { + test('#providerArgs.page and #providerArgs.sort', () { + const q = Query(providerArgs: {'page': 1, 'sort': 'by_user_asc'}); - test('#providerArgs.limit', () { - final q0 = Query(providerArgs: {'limit': 0}); - expect(q0.providerArgs['limit'], 0); + expect(q.providerArgs['page'], 1); + expect(q.providerArgs['sort'], 'by_user_asc'); + }); + }); - final q10 = Query(providerArgs: {'limit': 10}); - expect(q10.providerArgs['limit'], 10); + test('#limit', () { + const q0 = Query(limit: 0); + expect(q0.limit, 0); - final q18 = Query(providerArgs: {'limit': 18}); - expect(q18.providerArgs['limit'], 18); + const q10 = Query(limit: 10); + expect(q10.limit, 10); - expect(() => Query(providerArgs: {'limit': -1}), throwsA(TypeMatcher())); - }); + const q18 = Query(limit: 18); + expect(q18.limit, 18); + }); - test('#providerArgs.offset', () { - final q0 = Query(providerArgs: {'limit': 10, 'offset': 0}); - expect(q0.providerArgs['offset'], 0); + test('#offset', () { + const q0 = Query(limit: 10, offset: 0); + expect(q0.offset, 0); - final q10 = Query(providerArgs: {'limit': 10, 'offset': 10}); - expect(q10.providerArgs['offset'], 10); + const q10 = Query(limit: 10, offset: 10); + expect(q10.offset, 10); - final q18 = Query(providerArgs: {'limit': 10, 'offset': 18}); - expect(q18.providerArgs['offset'], 18); + const q18 = Query(limit: 10, offset: 18); + expect(q18.offset, 18); + }); - expect(() => Query(providerArgs: {'offset': -1}), throwsA(TypeMatcher())); + test('#where', () { + const q = Query( + where: [ + Where('name', value: 'Thomas'), + ], + ); - expect(() => Query(providerArgs: {'offset': 1}), throwsA(TypeMatcher())); - }); - }); + expect(q.where!.first.evaluatedField, 'name'); + expect(q.where!.first.value, 'Thomas'); + }); + }); - test('#where', () { - final q = Query( - where: [ - Where('name', value: 'Thomas'), - ], - ); + group('==', () { + test('properties are the same', () { + const q1 = Query( + action: QueryAction.delete, + limit: 3, + offset: 3, + ); + const q2 = Query( + action: QueryAction.delete, + limit: 3, + offset: 3, + ); - expect(q.where!.first.evaluatedField, 'name'); - expect(q.where!.first.value, 'Thomas'); - }); + expect(q1, q2); }); - group('==', () { - test('properties are the same', () { - final q1 = Query( - action: QueryAction.delete, - providerArgs: { - 'limit': 3, - 'offset': 3, - }, - ); - final q2 = Query( - action: QueryAction.delete, - providerArgs: { - 'limit': 3, - 'offset': 3, - }, - ); - - expect(q1, q2); - }); - - test('providerArgs are the same', () { - final q1 = Query(providerArgs: {'name': 'Guy'}); - final q2 = Query(providerArgs: {'name': 'Guy'}); + test('providerArgs are the same', () { + const q1 = Query(providerArgs: {'name': 'Guy'}); + const q2 = Query(providerArgs: {'name': 'Guy'}); - expect(q1, q2); - }); + expect(q1, q2); + }); - test('providerArgs have different values', () { - final q1 = Query(providerArgs: {'name': 'Thomas'}); - final q2 = Query(providerArgs: {'name': 'Guy'}); + test('providerArgs have different values', () { + const q1 = Query(providerArgs: {'name': 'Thomas'}); + const q2 = Query(providerArgs: {'name': 'Guy'}); - expect(q1, isNot(q2)); - }); + expect(q1, isNot(q2)); + }); - test('providerArgs have different keys', () { - final q1 = Query(providerArgs: {'email': 'guy@guy.com'}); - final q2 = Query(providerArgs: {'name': 'Guy'}); + test('providerArgs have different keys', () { + const q1 = Query(providerArgs: {'email': 'guy@guy.com'}); + const q2 = Query(providerArgs: {'name': 'Guy'}); - expect(q1, isNot(q2)); - }); + expect(q1, isNot(q2)); + }); - test('providerArgs are null', () { - final q1 = Query(); - final q2 = Query(providerArgs: {'name': 'Guy'}); - expect(q1, isNot(q2)); + test('providerArgs are null', () { + const q1 = Query(); + const q2 = Query(providerArgs: {'name': 'Guy'}); + expect(q1, isNot(q2)); - final q3 = Query(); - expect(q1, q3); - }); + const q3 = Query(); + expect(q1, q3); }); + }); - group('#copyWith', () { - test('overrides', () { - final q1 = Query(action: QueryAction.insert, providerArgs: {'limit': 10, 'offset': 10}); - final q2 = q1.copyWith(providerArgs: {'limit': 20}); - expect(q2.action, QueryAction.insert); - expect(q2.providerArgs['limit'], 20); - expect(q2.providerArgs['offset'], null); - - final q3 = q1.copyWith(providerArgs: {'limit': 50, 'offset': 20}); - expect(q3.action, QueryAction.insert); - expect(q3.providerArgs['limit'], 50); - expect(q3.providerArgs['offset'], 20); - }); + group('#copyWith', () { + test('overrides', () { + const q1 = Query(action: QueryAction.insert, limit: 10); + final q2 = q1.copyWith(limit: 20); + expect(q2.action, QueryAction.insert); + expect(q2.limit, 20); + expect(q2.offset, null); + + final q3 = q1.copyWith(limit: 50, offset: 20); + expect(q3.action, QueryAction.insert); + expect(q3.limit, 50); + expect(q3.offset, 20); + }); - test('appends', () { - final q1 = Query(action: QueryAction.insert); - final q2 = q1.copyWith(providerArgs: {'limit': 20}); + test('appends', () { + const q1 = Query(action: QueryAction.insert); + final q2 = q1.copyWith(limit: 20); - expect(q1.providerArgs['limit'], null); - expect(q2.action, QueryAction.insert); - expect(q2.providerArgs['limit'], 20); - }); + expect(q1.limit, null); + expect(q2.action, QueryAction.insert); + expect(q2.limit, 20); }); + }); + + test('#toJson', () { + const source = Query( + action: QueryAction.update, + limit: 3, + offset: 3, + ); + + expect( + source.toJson(), + { + 'action': 2, + 'limit': 3, + 'offset': 3, + }, + ); + }); - test('#toJson', () { - final source = Query( + test('.fromJson', () { + final json = { + 'action': 2, + 'limit': 3, + 'offset': 3, + }; + + final result = Query.fromJson(json); + expect( + result, + const Query( action: QueryAction.update, - providerArgs: { - 'limit': 3, - 'offset': 3, - }, - ); + limit: 3, + offset: 3, + ), + ); + }); - expect( - source.toJson(), - { - 'action': 2, - 'providerArgs': { - 'limit': 3, - 'offset': 3, - }, - }, - ); + group('.where', () { + test('required arguments', () { + const expandedQuery = Query(where: [Where('id', value: 2)]); + final factoried = Query.where('id', 2); + expect(factoried, expandedQuery); + expect(Where.firstByField('id', factoried.where)!.value, 2); + expect(factoried.unlimited, isTrue); }); - group('factories', () { - test('.fromJson', () { - final json = { - 'action': 2, - 'providerArgs': { - 'limit': 3, - 'offset': 3, - }, - }; - - final result = Query.fromJson(json); - expect( - result, - Query( - action: QueryAction.update, - providerArgs: { - 'limit': 3, - 'offset': 3, - }, - ), - ); - }); - - group('.where', () { - test('required arguments', () { - final expandedQuery = Query(where: [Where('id', value: 2)]); - final factoried = Query.where('id', 2); - expect(factoried, expandedQuery); - expect(Where.firstByField('id', factoried.where!)!.value, 2); - expect(factoried.unlimited, isTrue); - }); - - test('limit1:true', () { - final expandedQuery = Query(where: [Where('id', value: 2)], providerArgs: {'limit': 1}); - final factoried = Query.where('id', 2, limit1: true); - expect(factoried, expandedQuery); - expect(factoried.unlimited, isFalse); - }); - }); + test('limit1:true', () { + const expandedQuery = Query(where: [Where('id', value: 2)], limit: 1); + final factoried = Query.where('id', 2, limit1: true); + expect(factoried, expandedQuery); + expect(factoried.unlimited, isFalse); }); }); } diff --git a/packages/brick_core/test/query/where_test.dart b/packages/brick_core/test/query/where_test.dart index 1b771272..096d7532 100644 --- a/packages/brick_core/test/query/where_test.dart +++ b/packages/brick_core/test/query/where_test.dart @@ -5,85 +5,85 @@ void main() { group('Where', () { test('#isExactly', () { expect( - Where('id').isExactly(1), - Where('id', value: 1, compare: Compare.exact, isRequired: true), + const Where('id').isExactly(1), + const Where('id', value: 1, compare: Compare.exact, isRequired: true), ); }); test('#isBetween', () { expect( - Where('id').isBetween(1, 42), - Where('id', value: [1, 42], compare: Compare.between, isRequired: true), + const Where('id').isBetween(1, 42), + const Where('id', value: [1, 42], compare: Compare.between, isRequired: true), ); }); test('#contains', () { expect( - Where('id').contains(1), - Where('id', value: 1, compare: Compare.contains, isRequired: true), + const Where('id').contains(1), + const Where('id', value: 1, compare: Compare.contains, isRequired: true), ); }); test('#doesNotContain', () { expect( - Where('id').doesNotContain(1), - Where('id', value: 1, compare: Compare.doesNotContain, isRequired: true), + const Where('id').doesNotContain(1), + const Where('id', value: 1, compare: Compare.doesNotContain, isRequired: true), ); }); test('#isLessThan', () { expect( - Where('id').isLessThan(1), - Where('id', value: 1, compare: Compare.lessThan, isRequired: true), + const Where('id').isLessThan(1), + const Where('id', value: 1, compare: Compare.lessThan, isRequired: true), ); }); test('#isLessThanOrEqualTo', () { expect( - Where('id').isLessThanOrEqualTo(1), - Where('id', value: 1, compare: Compare.lessThanOrEqualTo, isRequired: true), + const Where('id').isLessThanOrEqualTo(1), + const Where('id', value: 1, compare: Compare.lessThanOrEqualTo, isRequired: true), ); }); test('#isGreaterThan', () { expect( - Where('id').isGreaterThan(1), - Where('id', value: 1, compare: Compare.greaterThan, isRequired: true), + const Where('id').isGreaterThan(1), + const Where('id', value: 1, compare: Compare.greaterThan, isRequired: true), ); }); test('#isGreaterThanOrEqualTo', () { expect( - Where('id').isGreaterThanOrEqualTo(1), - Where('id', value: 1, compare: Compare.greaterThanOrEqualTo, isRequired: true), + const Where('id').isGreaterThanOrEqualTo(1), + const Where('id', value: 1, compare: Compare.greaterThanOrEqualTo, isRequired: true), ); }); test('#isNot', () { expect( - Where('id').isNot(1), - Where('id', value: 1, compare: Compare.notEqual, isRequired: true), + const Where('id').isNot(1), + const Where('id', value: 1, compare: Compare.notEqual, isRequired: true), ); }); }); group('.byField', () { test('single field', () { - final conditions = [Where('id', value: 1), Where('name', value: 'Thomas')]; + final conditions = [const Where('id', value: 1), const Where('name', value: 'Thomas')]; final result = Where.byField('id', conditions); - expect(result, [Where('id', value: 1)]); + expect(result, [const Where('id', value: 1)]); }); test('nested fields', () { final conditions = [ - WherePhrase([ + const WherePhrase([ Where('id', value: 1), WherePhrase([ Where('name', value: 'Thomas'), ]), Where('age', value: 42), ]), - Where('lastName', value: 'Guy'), + const Where('lastName', value: 'Guy'), ]; expect(Where.byField('id', conditions).first.value, 1); expect(Where.byField('name', conditions).first.value, 'Thomas'); @@ -93,13 +93,13 @@ void main() { group('.firstByField', () { test('single field', () { - final conditions = [Where.exact('id', 1), Where.exact('name', 'Thomas')]; + final conditions = [const Where.exact('id', 1), const Where.exact('name', 'Thomas')]; final result = Where.firstByField('id', conditions); expect(result, conditions.first); }); test('nested field', () { - final conditions = [Where('id', value: Where('name', value: 'Thomas'))]; + final conditions = [const Where('id', value: Where('name', value: 'Thomas'))]; final topLevelResult = Where.firstByField('id', conditions); final result = Where.firstByField('name', [topLevelResult!.value]); expect(result!.value, 'Thomas'); @@ -108,7 +108,7 @@ void main() { group('WhereCondition', () { test('#toJson', () { - final where = Where('id', value: 1); + const where = Where('id', value: 1); expect(where.toJson(), { 'subclass': 'Where', 'evaluatedField': 'id', @@ -117,7 +117,7 @@ void main() { 'value': 1, }); - final phrase = WherePhrase([Where('id', value: 1)]); + const phrase = WherePhrase([Where('id', value: 1)]); expect(phrase.toJson(), { 'subclass': 'WherePhrase', 'compare': 0, diff --git a/packages/brick_graphql/CHANGELOG.md b/packages/brick_graphql/CHANGELOG.md index 91d03358..5d660010 100644 --- a/packages/brick_graphql/CHANGELOG.md +++ b/packages/brick_graphql/CHANGELOG.md @@ -1,5 +1,13 @@ ## Unreleased +## 3.2.0 + +- **DEPRECATION** `Query(providerArgs: {'context':})` is now `Query(forProviders: [GraphqlProviderQuery(context:)])` +- **DEPRECATION** `Query(providerArgs: {'operation':})` is now `Query(forProviders: [GraphqlProviderQuery(operation:)])` +- New `GraphqlProviderQuery` adds GraphQL-specific support for the new `Query`. +- Upgrade `brick_core` to `1.3.0` +- Update analysis to modern lints + ## 3.1.2 - Loosen constraints for `gql`, `gql_exec`, and `gql_link` diff --git a/packages/brick_graphql/README.md b/packages/brick_graphql/README.md index 0cd31483..ca94ab5f 100644 --- a/packages/brick_graphql/README.md +++ b/packages/brick_graphql/README.md @@ -12,8 +12,8 @@ Since Dart is the source of truth, it may not map 1:1 to the GraphQL contract. B ### `providerArgs:` -* `'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`. +- `'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` @@ -27,7 +27,7 @@ query MyOperation($vars: MyInputClass!) { } ``` -:bulb: `providerArgs['operation'].variables` will **never** be wrapped by `variablesNamespace` +:bulb: `GraphqlProviderQuery#variables` will **never** be wrapped by `variablesNamespace` ## `where:` @@ -93,50 +93,50 @@ query { To reduce copypasta-ing the same GraphQL document and variables, all operations can be set in a single place alongside the model configuration. 1. Create a new class that extends `GraphqlQueryOperationTransformer`: - ```dart - class UserQueryOperationTransformer extends GraphqlQueryOperationTransformer {} - ``` + ```dart + class UserQueryOperationTransformer extends GraphqlQueryOperationTransformer {} + ``` 1. This class has access to every request's `query`, and for `delete` and `upsert`, `instance`. You can use these properties to tell Brick which GraphQL operation to use. - ```dart - class UserQueryOperationTransformer extends GraphqlQueryOperationTransformer { - GraphqlOperation get upsert { - if (query.where != null) { - return GraphqlOperation(document: r''' - mutation UpdateUserName($name: String!) { - updateUserName(input: $input) {} - } - '''); - } - return GraphqlOperation(document: r''' - mutation CreateUser($input: UserInput!) { - createUser(input: $input) {} - } - '''); - } - } - ``` + ```dart + class UserQueryOperationTransformer extends GraphqlQueryOperationTransformer { + GraphqlOperation get upsert { + if (query.where != null) { + return GraphqlOperation(document: r''' + mutation UpdateUserName($name: String!) { + updateUserName(input: $input) {} + } + '''); + } + return GraphqlOperation(document: r''' + mutation CreateUser($input: UserInput!) { + createUser(input: $input) {} + } + '''); + } + } + ``` 1. In complex cases where the entire model is not being transmitted, `variables` can also be supplied. - ```dart - class UserQueryOperationTransformer extends GraphqlQueryOperationTransformer { - GraphqlOperation get upsert { - if (query.where != null) { - return GraphqlOperation( - document: r''' - mutation UpdateUserName($name: String!) { - updateUserName(input: $input) {} - } - ''', - variables: {'name': Where.firstByField('name', query.where)}); - } - return null; - } - } - ``` + ```dart + class UserQueryOperationTransformer extends GraphqlQueryOperationTransformer { + GraphqlOperation get upsert { + if (query.where != null) { + return GraphqlOperation( + document: r''' + mutation UpdateUserName($name: String!) { + updateUserName(input: $input) {} + } + ''', + variables: {'name': Where.firstByField('name', query.where)}); + } + return null; + } + } + ``` 1. Use the class in `GraphqlSerializable`: - ```dart - @GraphqlSerializable( - queryOperationTransformer: UserQueryOperationTransformer.new - ) - ``` + ```dart + @GraphqlSerializable( + queryOperationTransformer: UserQueryOperationTransformer.new + ) + ``` :bulb: Only headers need to be supplied; nodes can be supplied to override default behavior of fetching all fields requested by the model. To use autopopulated nodes provided by the model (with respect to `@Graphql` configuration), use an empty node selection (e.g. `deleteUser(vars: $vars) {}`). diff --git a/packages/brick_graphql/lib/brick_graphql.dart b/packages/brick_graphql/lib/brick_graphql.dart index 01bc9446..4eebacd3 100644 --- a/packages/brick_graphql/lib/brick_graphql.dart +++ b/packages/brick_graphql/lib/brick_graphql.dart @@ -5,5 +5,6 @@ export 'package:brick_graphql/src/graphql_adapter.dart'; export 'package:brick_graphql/src/graphql_model.dart'; export 'package:brick_graphql/src/graphql_model_dictionary.dart'; export 'package:brick_graphql/src/graphql_provider.dart'; +export 'package:brick_graphql/src/graphql_provider_query.dart'; export 'package:brick_graphql/src/runtime_graphql_definition.dart'; export 'package:brick_graphql/src/transformers/graphql_query_operation_transformer.dart'; diff --git a/packages/brick_graphql/lib/src/annotations/graphql.dart b/packages/brick_graphql/lib/src/annotations/graphql.dart index e626a5af..6b4c4f19 100644 --- a/packages/brick_graphql/lib/src/annotations/graphql.dart +++ b/packages/brick_graphql/lib/src/annotations/graphql.dart @@ -1,6 +1,6 @@ import 'package:brick_core/field_serializable.dart'; -/// An annotation used to specify how a field is serialized for a [GraphqlAdapter]. +/// An annotation used to specify how a field is serialized for a `GraphqlAdapter`. /// Heavily inspired by [JsonKey](https://github.com/dart-lang/json_serializable/blob/master/json_annotation/lib/src/json_key.dart) class Graphql implements FieldSerializable { @override diff --git a/packages/brick_graphql/lib/src/graphql_adapter.dart b/packages/brick_graphql/lib/src/graphql_adapter.dart index d80eb6a7..bf5e57be 100644 --- a/packages/brick_graphql/lib/src/graphql_adapter.dart +++ b/packages/brick_graphql/lib/src/graphql_adapter.dart @@ -5,22 +5,26 @@ import 'package:brick_graphql/src/runtime_graphql_definition.dart'; import 'package:brick_graphql/src/transformers/graphql_query_operation_transformer.dart'; class _DefaultGraphqlTransformer extends GraphqlQueryOperationTransformer { - const _DefaultGraphqlTransformer(Query? query, GraphqlModel? instance) : super(null, null); + const _DefaultGraphqlTransformer(Query? _, GraphqlModel? __) : super(null, null); } /// Constructors that convert app models to and from REST abstract class GraphqlAdapter implements Adapter { + /// The transformer to change a [Query] to a [GraphqlOperation] GraphqlQueryOperationTransformer Function(Query?, GraphqlModel?)? get queryOperationTransformer => _DefaultGraphqlTransformer.new; + /// A map of Dart field names to their [RuntimeGraphqlDefinition] Map get fieldsToGraphqlRuntimeDefinition; + /// Deserialize from GraphQL Future fromGraphql( Map input, { required GraphqlProvider provider, ModelRepository? repository, }); + /// Serialize to GraphQL Future> toGraphql( TModel input, { required GraphqlProvider provider, diff --git a/packages/brick_graphql/lib/src/graphql_model.dart b/packages/brick_graphql/lib/src/graphql_model.dart index fb5c18bf..7a1e8477 100644 --- a/packages/brick_graphql/lib/src/graphql_model.dart +++ b/packages/brick_graphql/lib/src/graphql_model.dart @@ -1,3 +1,4 @@ import 'package:brick_core/core.dart'; +/// Models accessible to the `GraphqlProvider` abstract class GraphqlModel implements Model {} diff --git a/packages/brick_graphql/lib/src/graphql_model_dictionary.dart b/packages/brick_graphql/lib/src/graphql_model_dictionary.dart index c97050a7..0ae15a57 100644 --- a/packages/brick_graphql/lib/src/graphql_model_dictionary.dart +++ b/packages/brick_graphql/lib/src/graphql_model_dictionary.dart @@ -4,5 +4,6 @@ import 'package:brick_graphql/src/graphql_model.dart'; /// Associates app models with their [GraphqlAdapter] class GraphqlModelDictionary extends ModelDictionary> { + /// Associates app models with their [GraphqlAdapter] const GraphqlModelDictionary(super.adapterFor); } diff --git a/packages/brick_graphql/lib/src/graphql_provider.dart b/packages/brick_graphql/lib/src/graphql_provider.dart index 5272b640..e797083c 100644 --- a/packages/brick_graphql/lib/src/graphql_provider.dart +++ b/packages/brick_graphql/lib/src/graphql_provider.dart @@ -12,8 +12,11 @@ class GraphqlProvider extends Provider { @override final GraphqlModelDictionary modelDictionary; + /// The invoking [Link] used to access the GraphQL API. + /// Can be overriden by repositories. Link link; + /// Internal use logger. @protected final Logger logger; @@ -29,6 +32,7 @@ class GraphqlProvider extends Provider { /// This **does not** affect variables passed via `providerArgs`. final String? variableNamespace; + /// A [Provider] fetches raw data from GraphQL and creates [Model]s. An app can have many [Provider]s. GraphqlProvider({ required this.modelDictionary, required this.link, @@ -36,7 +40,11 @@ class GraphqlProvider extends Provider { }) : logger = Logger('GraphqlProvider'); @override - Future delete(instance, {query, repository}) async { + Future delete( + TModel instance, { + Query? query, + ModelRepository? repository, + }) async { final request = GraphqlRequest( action: QueryAction.delete, instance: instance, @@ -52,7 +60,10 @@ class GraphqlProvider extends Provider { } @override - Future exists({query, repository}) async { + Future exists({ + Query? query, + ModelRepository? repository, + }) async { final request = GraphqlRequest( action: QueryAction.get, modelDictionary: modelDictionary, @@ -67,7 +78,10 @@ class GraphqlProvider extends Provider { } @override - Future> get({query, repository}) async { + Future> get({ + Query? query, + ModelRepository? repository, + }) async { final adapter = modelDictionary.adapterFor[TModel]!; final request = GraphqlRequest( action: QueryAction.get, @@ -108,6 +122,8 @@ class GraphqlProvider extends Provider { return []; } + /// Invokes the `subscribe` GraphQL operation and returns a [Stream] of [Model]s. + /// The GraphQL API **must** support subscriptions. Stream> subscribe({ Query? query, ModelRepository? repository, @@ -140,7 +156,11 @@ class GraphqlProvider extends Provider { } @override - Future upsert(instance, {query, repository}) async { + Future upsert( + TModel instance, { + Query? query, + ModelRepository? repository, + }) async { final adapter = modelDictionary.adapterFor[TModel]!; final variables = await adapter.toGraphql(instance, provider: this, repository: repository); final request = GraphqlRequest( diff --git a/packages/brick_graphql/lib/src/graphql_provider_query.dart b/packages/brick_graphql/lib/src/graphql_provider_query.dart new file mode 100644 index 00000000..a4cfe08d --- /dev/null +++ b/packages/brick_graphql/lib/src/graphql_provider_query.dart @@ -0,0 +1,36 @@ +import 'package:brick_core/query.dart'; +import 'package:brick_graphql/src/graphql_provider.dart'; +import 'package:brick_graphql/src/transformers/graphql_query_operation_transformer.dart'; +import 'package:gql_exec/gql_exec.dart'; + +/// A [ProviderQuery] for a [GraphqlProvider] for use with [Query] +class GraphqlProviderQuery extends ProviderQuery { + /// Additional context for the GraphQL request + final Context? context; + + /// The GraphQL operation + final GraphqlOperation? operation; + + /// A [ProviderQuery] for a [GraphqlProvider] for use with [Query] + const GraphqlProviderQuery({ + this.context, + this.operation, + }); + + @override + Map toJson() => { + if (context != null) 'context': context.toString(), + if (operation != null) 'operation': operation!.toJson(), + }; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is GraphqlProviderQuery && + runtimeType == other.runtimeType && + context == other.context && + operation == other.operation; + + @override + int get hashCode => context.hashCode ^ operation.hashCode; +} diff --git a/packages/brick_graphql/lib/src/graphql_request.dart b/packages/brick_graphql/lib/src/graphql_request.dart index 15f6029a..68369ce1 100644 --- a/packages/brick_graphql/lib/src/graphql_request.dart +++ b/packages/brick_graphql/lib/src/graphql_request.dart @@ -1,22 +1,29 @@ import 'package:brick_core/core.dart'; -import 'package:brick_graphql/src/graphql_model.dart'; -import 'package:brick_graphql/src/graphql_model_dictionary.dart'; +import 'package:brick_graphql/brick_graphql.dart'; import 'package:brick_graphql/src/transformers/model_fields_document_transformer.dart'; import 'package:gql_exec/gql_exec.dart'; +/// A request to a [GraphqlProvider] class GraphqlRequest { + /// The action to perform on the API final QueryAction action; + /// The instance to use. Not relevant for [QueryAction.get] final TModel? instance; + /// The repository definition of other adapters know the GraphQL final GraphqlModelDictionary modelDictionary; + /// The invoking [Query] final Query? query; + /// The top-level name to nest subsquent variables final String? variableNamespace; + /// Available variables. final Map? variables; + /// A request to a [GraphqlProvider] const GraphqlRequest({ required this.action, this.instance, @@ -26,6 +33,7 @@ class GraphqlRequest { this.variableNamespace, }); + /// The transformed [Request] for use with an eventual `Link` Request? get request { final defaultOperation = ModelFieldsDocumentTransformer.defaultOperation( modelDictionary, @@ -36,20 +44,26 @@ class GraphqlRequest { if (defaultOperation == null) return null; + final context = (query?.providerQueries[GraphqlProvider] as GraphqlProviderQuery?)?.context; + // ignore: deprecated_member_use + final argContextMap = query?.providerArgs['context'] as Map?; + final argContext = argContextMap != null + ? Context.fromMap( + Map.from(argContextMap) + .map((key, value) => MapEntry(value.runtimeType, value)), + ) + : null; + return Request( operation: Operation( document: defaultOperation.document, ), variables: requestVariables ?? {}, - context: query?.providerArgs['context'] != null - ? Context.fromMap( - Map.from(query?.providerArgs['context']) - .map((key, value) => MapEntry(value.runtimeType, value)), - ) - : Context(), + context: context ?? argContext ?? const Context(), ); } + /// Declared variables from the operation and the query Map? get requestVariables { final opVariables = operationVariables(action, instance: instance, query: query); var vars = opVariables ?? variables ?? queryToVariables(query); @@ -57,7 +71,12 @@ class GraphqlRequest { vars = {variableNamespace!: vars}; } - return query?.providerArgs['operation']?.variables ?? vars; + final operation = + (query?.providerQueries[GraphqlProvider] as GraphqlProviderQuery?)?.operation ?? + // ignore: deprecated_member_use + query?.providerArgs['operation'] as GraphqlOperation?; + + return operation?.variables ?? vars; } /// Retrive variables defined by the annotation in [GraphqlQueryOperationTransformer] diff --git a/packages/brick_graphql/lib/src/runtime_graphql_definition.dart b/packages/brick_graphql/lib/src/runtime_graphql_definition.dart index 77626240..f6403fb6 100644 --- a/packages/brick_graphql/lib/src/runtime_graphql_definition.dart +++ b/packages/brick_graphql/lib/src/runtime_graphql_definition.dart @@ -20,6 +20,8 @@ class RuntimeGraphqlDefinition { /// In other words, the runtime type. final Type type; + /// Used to define types in [GraphqlAdapter#fieldsToGraphqlRuntimeDefinition]. The build runner package + /// extracts types and associations that would've been otherwise inaccessible at runtime. const RuntimeGraphqlDefinition({ this.association = false, required this.documentNodeName, diff --git a/packages/brick_graphql/lib/src/transformers/graphql_argument.dart b/packages/brick_graphql/lib/src/transformers/graphql_argument.dart index caafab7c..2c2441eb 100644 --- a/packages/brick_graphql/lib/src/transformers/graphql_argument.dart +++ b/packages/brick_graphql/lib/src/transformers/graphql_argument.dart @@ -1,31 +1,34 @@ import 'package:brick_graphql/src/transformers/graphql_variable.dart'; import 'package:gql/ast.dart'; +/// An internal class to help transform operations class GraphqlArgument { + /// The name of the argument final String name; + /// The variable associated with the argument final GraphqlVariable variable; + /// An internal class to help transform operations const GraphqlArgument({ required this.name, required this.variable, }); - factory GraphqlArgument.fromArgumentNode(ArgumentNode node) { - return GraphqlArgument( - name: node.name.value, - variable: GraphqlVariable( - className: '', - name: (node.value as VariableNode).name.value, - ), - ); - } + /// Convert an [ArgumentNode] to a [GraphqlArgument] + factory GraphqlArgument.fromArgumentNode(ArgumentNode node) => GraphqlArgument( + name: node.name.value, + variable: GraphqlVariable( + className: '', + name: (node.value as VariableNode).name.value, + ), + ); - static List fromOperationNode(OperationDefinitionNode node) { - return (node.selectionSet.selections.first as FieldNode) - .arguments - .map(GraphqlArgument.fromArgumentNode) - .toList() - .cast(); - } + /// Convert an [OperationDefinitionNode] to a list of [GraphqlArgument] + static List fromOperationNode(OperationDefinitionNode node) => + (node.selectionSet.selections.first as FieldNode) + .arguments + .map(GraphqlArgument.fromArgumentNode) + .toList() + .cast(); } diff --git a/packages/brick_graphql/lib/src/transformers/graphql_query_operation_transformer.dart b/packages/brick_graphql/lib/src/transformers/graphql_query_operation_transformer.dart index ae80ccbf..134658bb 100644 --- a/packages/brick_graphql/lib/src/transformers/graphql_query_operation_transformer.dart +++ b/packages/brick_graphql/lib/src/transformers/graphql_query_operation_transformer.dart @@ -1,4 +1,6 @@ import 'package:brick_core/core.dart'; +import 'package:brick_graphql/brick_graphql.dart'; +import 'package:brick_graphql/src/graphql_provider.dart'; /// This class should be subclassed for each model. For example: /// @@ -65,6 +67,23 @@ abstract class GraphqlQueryOperationTransformer { /// ``` GraphqlOperation? get upsert => null; + /// This class should be subclassed for each model. For example: + /// + /// ```dart + /// @GraphqlSerializable( + /// queryOperationTransformer: MyModelOperationTransformer.new, + /// ) + /// class MyModel extends GraphqlModel {} + /// class MyModelOperationTransformer extends GraphqlQueryOperationTransformer { + /// final get = GraphqlOperation( + /// document: r''' + /// query GetPeople() { + /// getPerson() {} + /// } + /// ''' + /// ); + /// } + /// ``` const GraphqlQueryOperationTransformer(this.query, this.instance); } @@ -85,10 +104,13 @@ class GraphqlOperation { /// `variableNamespace` if it is defined. final Map? variables; + /// A cohesive definition for [GraphqlQueryOperationTransformer]'s instance fields. const GraphqlOperation({this.document, this.variables}); + /// Deserialize factory GraphqlOperation.fromJson(Map data) => GraphqlOperation(document: data['document'], variables: data['variables']); + /// Serialize Map toJson() => {'document': document, 'variables': variables}; } diff --git a/packages/brick_graphql/lib/src/transformers/graphql_variable.dart b/packages/brick_graphql/lib/src/transformers/graphql_variable.dart index b6236636..9ecb2304 100644 --- a/packages/brick_graphql/lib/src/transformers/graphql_variable.dart +++ b/packages/brick_graphql/lib/src/transformers/graphql_variable.dart @@ -1,5 +1,6 @@ import 'package:gql/ast.dart'; +/// An internal class to help transform operations class GraphqlVariable { /// The `UpdatePersonInput` in `mutation UpdatePerson($input: UpdatePersonInput)` final String className; @@ -12,23 +13,24 @@ class GraphqlVariable { /// Defaults `false`. final bool nullable; + /// An internal class to help transform operations const GraphqlVariable({ required this.className, required this.name, this.nullable = false, }); - factory GraphqlVariable.fromVariableDefinitionNode(VariableDefinitionNode node) { - return GraphqlVariable( - className: (node.type as NamedTypeNode).name.value, - name: node.variable.name.value, - ); - } + /// Convert a [VariableDefinitionNode] to a [GraphqlVariable] + factory GraphqlVariable.fromVariableDefinitionNode(VariableDefinitionNode node) => + GraphqlVariable( + className: (node.type as NamedTypeNode).name.value, + name: node.variable.name.value, + ); - static List fromOperationNode(OperationDefinitionNode node) { - return node.variableDefinitions - .map(GraphqlVariable.fromVariableDefinitionNode) - .toList() - .cast(); - } + /// Convert an [OperationDefinitionNode] to a list of [GraphqlVariable] + static List fromOperationNode(OperationDefinitionNode node) => + node.variableDefinitions + .map(GraphqlVariable.fromVariableDefinitionNode) + .toList() + .cast(); } diff --git a/packages/brick_graphql/lib/src/transformers/model_fields_document_transformer.dart b/packages/brick_graphql/lib/src/transformers/model_fields_document_transformer.dart index f8adae42..9a84a88e 100644 --- a/packages/brick_graphql/lib/src/transformers/model_fields_document_transformer.dart +++ b/packages/brick_graphql/lib/src/transformers/model_fields_document_transformer.dart @@ -2,13 +2,19 @@ import 'package:brick_core/core.dart'; import 'package:brick_graphql/src/graphql_adapter.dart'; import 'package:brick_graphql/src/graphql_model.dart'; import 'package:brick_graphql/src/graphql_model_dictionary.dart'; +import 'package:brick_graphql/src/graphql_provider.dart'; +import 'package:brick_graphql/src/graphql_provider_query.dart'; import 'package:brick_graphql/src/runtime_graphql_definition.dart'; import 'package:brick_graphql/src/transformers/graphql_argument.dart'; +import 'package:brick_graphql/src/transformers/graphql_query_operation_transformer.dart'; import 'package:brick_graphql/src/transformers/graphql_variable.dart'; import 'package:gql/ast.dart'; import 'package:gql/language.dart' as lang; +/// Convert a [Query] to a [DocumentNode] and variables. +/// This class also interprets associations from adapters and model definitions. class ModelFieldsDocumentTransformer { + /// Data that holds generated variables available at runtime, such as field names. final GraphqlAdapter adapter; /// Generates a document based on the [GraphqlAdapter#fieldsToGraphqlRuntimeDefinition] @@ -42,11 +48,9 @@ class ModelFieldsDocumentTransformer { name: NameNode(value: variable.className), isNonNull: !variable.nullable, ), - defaultValue: DefaultValueNode(value: null), - directives: [], + defaultValue: const DefaultValueNode(value: null), ), ], - directives: [], selectionSet: SelectionSetNode( selections: [ FieldNode( @@ -74,20 +78,24 @@ class ModelFieldsDocumentTransformer { ); } + /// Returns `true` if the operation has subfields bool get hasSubfields { final node = sourceDocument.definitions.first as OperationDefinitionNode; return (node.selectionSet.selections.first as FieldNode).selectionSet?.selections.isNotEmpty ?? false; } + /// A map of all other adapters within the GraphQL domain. final GraphqlModelDictionary modelDictionary; + /// The top-level operation name. String? get operationName { final node = sourceDocument.definitions.first as OperationDefinitionNode; if (hasSubfields) return null; return (node.selectionSet.selections.first as FieldNode).name.value; } + /// The GraphQL document that was passed to the transformer. final DocumentNode sourceDocument; /// Convert an adapter's `#fieldsToGraphqlRuntimeDefinition` to a @@ -104,58 +112,49 @@ class ModelFieldsDocumentTransformer { List _generateNodes( Map fieldsToGraphqlRuntimeDefinition, { bool ignoreAssociations = false, - }) { - return fieldsToGraphqlRuntimeDefinition.entries.fold>([], (nodes, entry) { - nodes.add( - FieldNode( - name: NameNode(value: entry.value.documentNodeName), - alias: null, - arguments: [], - directives: [], - selectionSet: entry.value.association && !ignoreAssociations - ? SelectionSetNode( - selections: _generateNodes( - modelDictionary.adapterFor[entry.value.type]!.fieldsToGraphqlRuntimeDefinition, - ), - ) - : entry.value.subfields.isNotEmpty - ? _generateSubFields(entry.value.subfields) - : null, - ), - ); - - return nodes; - }); - } - - SelectionSetNode _generateSubFields(Map subfields) { - return SelectionSetNode( - selections: subfields.entries.fold>([], (acc, entry) { - acc.add( + }) => + fieldsToGraphqlRuntimeDefinition.entries.fold>([], (nodes, entry) { + nodes.add( FieldNode( - name: NameNode(value: entry.key), - alias: null, - arguments: [], - directives: [], - selectionSet: entry.value.isEmpty ? null : _generateSubFields(entry.value), + name: NameNode(value: entry.value.documentNodeName), + selectionSet: entry.value.association && !ignoreAssociations + ? SelectionSetNode( + selections: _generateNodes( + modelDictionary + .adapterFor[entry.value.type]!.fieldsToGraphqlRuntimeDefinition, + ), + ) + : entry.value.subfields.isNotEmpty + ? _generateSubFields(entry.value.subfields) + : null, ), ); - return acc; - }), - ); - } + return nodes; + }); + + SelectionSetNode _generateSubFields(Map subfields) => SelectionSetNode( + selections: subfields.entries.fold>([], (acc, entry) { + acc.add( + FieldNode( + name: NameNode(value: entry.key), + selectionSet: entry.value.isEmpty ? null : _generateSubFields(entry.value), + ), + ); + + return acc; + }), + ); /// Merge the operation headers from [document] and the generated `#document` nodes. static ModelFieldsDocumentTransformer fromDocument( DocumentNode document, GraphqlModelDictionary modelDictionary, - ) { - return ModelFieldsDocumentTransformer( - document: document, - modelDictionary: modelDictionary, - ); - } + ) => + ModelFieldsDocumentTransformer( + document: document, + modelDictionary: modelDictionary, + ); /// Instead of a [DocumentNode], the raw document is used. /// Only the operation information is retrieved from the supplied document; @@ -173,8 +172,12 @@ class ModelFieldsDocumentTransformer { TModel? instance, Query? query, }) { - if (query?.providerArgs['operation'] != null) { - return fromString(query!.providerArgs['operation'].document, modelDictionary); + final operation = + (query?.providerQueries[GraphqlProvider] as GraphqlProviderQuery?)?.operation ?? + // ignore: deprecated_member_use + query?.providerArgs['operation'] as GraphqlOperation?; + if (operation?.document != null) { + return fromString(operation!.document!, modelDictionary); } final adapter = modelDictionary.adapterFor[TModel]!; diff --git a/packages/brick_graphql/pubspec.yaml b/packages/brick_graphql/pubspec.yaml index 15606b51..b84a8471 100644 --- a/packages/brick_graphql/pubspec.yaml +++ b/packages/brick_graphql/pubspec.yaml @@ -4,21 +4,21 @@ homepage: https://github.com/GetDutchie/brick/tree/main/packages/brick_graphql issue_tracker: https://github.com/GetDutchie/brick/issues repository: https://github.com/GetDutchie/brick -version: 3.1.2 +version: 3.2.0 environment: sdk: ">=2.18.0 <4.0.0" dependencies: - brick_core: ^1.2.1 + brick_core: ^1.3.0 collection: ">=1.15.0 <2.0.0" - logging: ">=1.0.0 <2.0.0" - meta: ">=1.3.0 <2.0.0" gql: ">=0.13.0 <2.0.0" gql_exec: ">=0.3.0 <2.0.0" gql_link: ">=0.4.0 <2.0.0" + logging: ">=1.0.0 <2.0.0" + meta: ">=1.3.0 <2.0.0" dev_dependencies: - mockito: ^5.0.0 - test: ^1.16.5 - lints: ^2.0.1 + lints: + mockito: + test: diff --git a/packages/brick_graphql/test/__helpers__/demo_model_adapter.dart b/packages/brick_graphql/test/__helpers__/demo_model_adapter.dart index 174b6a0b..b39d2e6b 100644 --- a/packages/brick_graphql/test/__helpers__/demo_model_adapter.dart +++ b/packages/brick_graphql/test/__helpers__/demo_model_adapter.dart @@ -1,4 +1,5 @@ import 'package:brick_core/core.dart' show Query; +import 'package:brick_core/src/model_repository.dart'; import 'package:brick_graphql/src/graphql_adapter.dart'; import 'package:brick_graphql/src/graphql_model.dart'; import 'package:brick_graphql/src/graphql_provider.dart'; @@ -11,44 +12,42 @@ Future _$DemoModelFromGraphql( Map data, { GraphqlProvider? provider, repository, -}) async { - return DemoModel( - name: data['full_name'] == null ? null : data['full_name'] as String, - assoc: data['assoc_DemoModelAssoc_brick_id'] == null - ? null - : (data['assoc_DemoModelAssoc_brick_id'] > -1 - ? (await repository?.getAssociation( - Query.where( - 'primaryKey', - data['assoc_DemoModelAssoc_brick_id'] as int, - limit1: true, - ), - )) - ?.first - : null), - complexFieldName: - data['complex_field_name'] == null ? null : data['complex_field_name'] as String, - lastName: data['last_name'] == null ? null : data['last_name'] as String, - simpleBool: data['simple_bool'] == null ? null : data['simple_bool'] == 1, - ); -} +}) async => + DemoModel( + name: data['full_name'] == null ? null : data['full_name'] as String, + assoc: data['assoc_DemoModelAssoc_brick_id'] == null + ? null + : (data['assoc_DemoModelAssoc_brick_id'] > -1 + ? (await repository?.getAssociation( + Query.where( + 'primaryKey', + data['assoc_DemoModelAssoc_brick_id'] as int, + limit1: true, + ), + )) + ?.first + : null), + complexFieldName: + data['complex_field_name'] == null ? null : data['complex_field_name'] as String, + lastName: data['last_name'] == null ? null : data['last_name'] as String, + simpleBool: data['simple_bool'] == null ? null : data['simple_bool'] == 1, + ); Future> _$DemoModelToGraphql( DemoModel instance, { required GraphqlProvider provider, repository, -}) async { - return { - 'complex_field_name': instance.complexFieldName, - 'last_name': instance.lastName, - 'full_name': instance.name, - 'simple_bool': instance.simpleBool == null ? null : (instance.simpleBool! ? 1 : 0), - }; -} +}) async => + { + 'complex_field_name': instance.complexFieldName, + 'last_name': instance.lastName, + 'full_name': instance.name, + 'simple_bool': instance.simpleBool == null ? null : (instance.simpleBool! ? 1 : 0), + }; class DemoModelOperationTransformer extends GraphqlQueryOperationTransformer { @override - GraphqlOperation get delete => GraphqlOperation( + GraphqlOperation get delete => const GraphqlOperation( document: r'''mutation DeleteDemoModel($input: DemoModelInput!) { deleteDemoModel(input: $input) {} }''', @@ -83,7 +82,7 @@ class DemoModelOperationTransformer extends GraphqlQueryOperationTransformer { } @override - GraphqlOperation get upsert => GraphqlOperation( + GraphqlOperation get upsert => const GraphqlOperation( document: r'''mutation UpsertDemoModels($input: DemoModelInput) { upsertDemoModel(input: $input) {} }''', @@ -102,62 +101,51 @@ class DemoModelAdapter extends GraphqlAdapter { @override Future fromGraphql( Map input, { - required provider, - repository, + required GraphqlProvider provider, + ModelRepository? repository, }) async => await _$DemoModelFromGraphql(input, provider: provider, repository: repository); @override - Future> toGraphql(DemoModel input, {required provider, repository}) async => + Future> toGraphql( + DemoModel input, { + required GraphqlProvider provider, + ModelRepository? repository, + }) async => await _$DemoModelToGraphql(input, provider: provider, repository: repository); @override Map get fieldsToGraphqlRuntimeDefinition => { 'primaryKey': const RuntimeGraphqlDefinition( - association: false, documentNodeName: 'primaryKey', - iterable: false, type: int, ), 'id': const RuntimeGraphqlDefinition( - association: false, documentNodeName: 'id', - iterable: false, type: int, ), 'assoc': const RuntimeGraphqlDefinition( association: true, documentNodeName: 'assoc', - iterable: false, type: DemoModelAssoc, ), 'someField': const RuntimeGraphqlDefinition( - association: false, documentNodeName: 'someField', - iterable: false, type: bool, ), 'complexFieldName': const RuntimeGraphqlDefinition( - association: false, documentNodeName: 'complexFieldName', - iterable: false, type: String, ), 'lastName': const RuntimeGraphqlDefinition( - association: false, documentNodeName: 'lastName', - iterable: false, type: String, ), 'name': const RuntimeGraphqlDefinition( - association: false, documentNodeName: 'fullName', - iterable: false, type: String, ), 'simpleBool': const RuntimeGraphqlDefinition( - association: false, documentNodeName: 'simpleBool', - iterable: false, type: bool, ), }; diff --git a/packages/brick_graphql/test/__helpers__/demo_model_assoc_adapter.dart b/packages/brick_graphql/test/__helpers__/demo_model_assoc_adapter.dart index 46f73105..b4d1c681 100644 --- a/packages/brick_graphql/test/__helpers__/demo_model_assoc_adapter.dart +++ b/packages/brick_graphql/test/__helpers__/demo_model_assoc_adapter.dart @@ -1,3 +1,4 @@ +import 'package:brick_core/src/model_repository.dart'; import 'package:brick_graphql/src/graphql_adapter.dart'; import 'package:brick_graphql/src/graphql_model.dart'; import 'package:brick_graphql/src/graphql_provider.dart'; @@ -10,17 +11,15 @@ Future _$DemoModelAssocFromGraphql( Map data, { GraphqlProvider? provider, repository, -}) async { - return DemoModelAssoc(name: data['full_name'] == null ? null : data['full_name'] as String); -} +}) async => + DemoModelAssoc(name: data['full_name'] == null ? null : data['full_name'] as String); Future> _$DemoModelAssocToGraphql( DemoModelAssoc instance, { GraphqlProvider? provider, repository, -}) async { - return {'full_name': instance.name}; -} +}) async => + {'full_name': instance.name}; /// Construct a [DemoModelAssoc] class DemoModelAssocAdapter extends GraphqlAdapter { @@ -32,15 +31,11 @@ class DemoModelAssocAdapter extends GraphqlAdapter { @override final Map fieldsToGraphqlRuntimeDefinition = { 'primaryKey': const RuntimeGraphqlDefinition( - association: false, documentNodeName: 'primaryKey', - iterable: false, type: int, ), 'name': const RuntimeGraphqlDefinition( - association: false, documentNodeName: 'fullName', - iterable: false, type: String, ), }; @@ -48,22 +43,22 @@ class DemoModelAssocAdapter extends GraphqlAdapter { @override Future fromGraphql( Map input, { - required provider, - repository, + required GraphqlProvider provider, + ModelRepository? repository, }) async => await _$DemoModelAssocFromGraphql(input, provider: provider, repository: repository); @override Future> toGraphql( DemoModelAssoc input, { - required provider, - repository, + required GraphqlProvider provider, + ModelRepository? repository, }) async => await _$DemoModelAssocToGraphql(input, provider: provider, repository: repository); } class _DemoModelAssocTransformer extends GraphqlQueryOperationTransformer { @override - GraphqlOperation get get => GraphqlOperation( + GraphqlOperation get get => const GraphqlOperation( document: '''query GetDemoAssocModels() { getDemoAssocModels() {} }''', @@ -80,15 +75,11 @@ class DemoModelAssocWithSubfieldsAdapter extends GraphqlAdapter @override final Map fieldsToGraphqlRuntimeDefinition = { 'primaryKey': const RuntimeGraphqlDefinition( - association: false, documentNodeName: '_brick_id', - iterable: false, type: int, ), 'name': const RuntimeGraphqlDefinition( - association: false, documentNodeName: 'full_name', - iterable: false, subfields: { 'first': {'subfield1': {}}, 'last': {}, @@ -100,15 +91,15 @@ class DemoModelAssocWithSubfieldsAdapter extends GraphqlAdapter @override Future fromGraphql( Map input, { - required provider, - repository, + required GraphqlProvider provider, + ModelRepository? repository, }) async => await _$DemoModelAssocFromGraphql(input, provider: provider, repository: repository); @override Future> toGraphql( DemoModelAssoc input, { - required provider, - repository, + required GraphqlProvider provider, + ModelRepository? repository, }) async => await _$DemoModelAssocToGraphql(input, provider: provider, repository: repository); } diff --git a/packages/brick_graphql/test/__helpers__/stub_response.dart b/packages/brick_graphql/test/__helpers__/stub_response.dart index 38b76990..60ba5aa7 100644 --- a/packages/brick_graphql/test/__helpers__/stub_response.dart +++ b/packages/brick_graphql/test/__helpers__/stub_response.dart @@ -25,7 +25,6 @@ Link stubResponse(Map response, {List? errors}) { data: response, response: {'body': jsonEncode(response)}, errors: errors?.map((e) => GraphQLError(message: e)).toList().cast(), - context: const Context(), ), ]), ); diff --git a/packages/brick_graphql/test/graphql_provider_test.dart b/packages/brick_graphql/test/graphql_provider_test.dart index cb04120d..f8364176 100644 --- a/packages/brick_graphql/test/graphql_provider_test.dart +++ b/packages/brick_graphql/test/graphql_provider_test.dart @@ -9,13 +9,12 @@ GraphqlProvider generateProvider( dynamic response, { List? errors, String? variablesNamespace, -}) { - return GraphqlProvider( - modelDictionary: dictionary, - link: stubResponse({'upsertPerson': response}, errors: errors), - variableNamespace: variablesNamespace, - ); -} +}) => + GraphqlProvider( + modelDictionary: dictionary, + link: stubResponse({'upsertPerson': response}, errors: errors), + variableNamespace: variablesNamespace, + ); void main() { group('GraphqlProvider', () { diff --git a/packages/brick_graphql/test/graphql_request_test.dart b/packages/brick_graphql/test/graphql_request_test.dart index 544dd752..e489be30 100644 --- a/packages/brick_graphql/test/graphql_request_test.dart +++ b/packages/brick_graphql/test/graphql_request_test.dart @@ -12,12 +12,11 @@ import '__mocks__.dart'; GraphqlProvider generateProvider( dynamic response, { List? errors, -}) { - return GraphqlProvider( - modelDictionary: dictionary, - link: stubResponse({'upsertPerson': response}, errors: errors), - ); -} +}) => + GraphqlProvider( + modelDictionary: dictionary, + link: stubResponse({'upsertPerson': response}, errors: errors), + ); class SampleContextEntry extends ContextEntry { final String useEntry; @@ -45,17 +44,31 @@ void main() { ); }); - test('providerArgs#context:', () { + test('GraphqlProviderQuery#context:', () { final request = GraphqlRequest( action: QueryAction.upsert, modelDictionary: provider.modelDictionary, query: Query( + forProviders: [ + GraphqlProviderQuery( + context: const Context().withEntry(const SampleContextEntry('myValue')), + ), + ], + ), + ).request; + expect(request!.context.entry()?.useEntry, 'myValue'); + + final request0 = GraphqlRequest( + action: QueryAction.upsert, + modelDictionary: provider.modelDictionary, + query: const Query( + // ignore: deprecated_member_use providerArgs: { 'context': {'SampleContextEntry': SampleContextEntry('myValue')}, }, ), ).request; - expect(request!.context.entry()?.useEntry, 'myValue'); + expect(request0!.context.entry()?.useEntry, 'myValue'); }); }); @@ -75,26 +88,54 @@ void main() { expect(request.requestVariables, variables); }); - test('providerArgs#variables:', () { + test('GraphqlProviderQuery#operation:', () { final variables = {'name': 'Thomas'}; final request = GraphqlRequest( action: QueryAction.upsert, modelDictionary: provider.modelDictionary, - query: Query(providerArgs: {'operation': GraphqlOperation(variables: variables)}), + query: Query( + forProviders: [ + GraphqlProviderQuery( + operation: GraphqlOperation(variables: variables), + ), + ], + ), ); expect(request.requestVariables, variables); + + final request0 = GraphqlRequest( + action: QueryAction.upsert, + modelDictionary: provider.modelDictionary, + // ignore: deprecated_member_use + query: Query(providerArgs: {'operation': GraphqlOperation(variables: variables)}), + ); + expect(request0.requestVariables, variables); }); test('use providerArgs before passed variables', () { final variables = {'name': 'Thomas'}; final providerVariables = {'name': 'Guy'}; + final request = GraphqlRequest( action: QueryAction.upsert, modelDictionary: provider.modelDictionary, - query: Query(providerArgs: {'operation': GraphqlOperation(variables: providerVariables)}), + query: Query( + forProviders: [ + GraphqlProviderQuery(operation: GraphqlOperation(variables: providerVariables)), + ], + ), variables: variables, ); expect(request.requestVariables, providerVariables); + + final request0 = GraphqlRequest( + action: QueryAction.upsert, + modelDictionary: provider.modelDictionary, + // ignore: deprecated_member_use + query: Query(providerArgs: {'operation': GraphqlOperation(variables: providerVariables)}), + variables: variables, + ); + expect(request0.requestVariables, providerVariables); }); test('without variablesNamespace', () { @@ -144,8 +185,8 @@ void main() { test('skips associations', () { final query = Query( where: [ - Where('lastName').isExactly(1), - Where('assoc').isExactly(Where('name').isExactly(1)), + const Where('lastName').isExactly(1), + const Where('assoc').isExactly(const Where('name').isExactly(1)), ], ); expect(request.queryToVariables(query), {'lastName': 1}); diff --git a/packages/brick_graphql/test/model_fields_document_transformer_test.dart b/packages/brick_graphql/test/model_fields_document_transformer_test.dart index 536858fe..57bfd1cc 100644 --- a/packages/brick_graphql/test/model_fields_document_transformer_test.dart +++ b/packages/brick_graphql/test/model_fields_document_transformer_test.dart @@ -1,5 +1,5 @@ import 'package:brick_core/core.dart'; -import 'package:brick_graphql/src/transformers/graphql_query_operation_transformer.dart'; +import 'package:brick_graphql/brick_graphql.dart'; import 'package:brick_graphql/src/transformers/model_fields_document_transformer.dart'; import 'package:gql/language.dart' as lang; import 'package:test/test.dart'; @@ -133,8 +133,13 @@ mutation UpsertPerson($input: UpsertPersonInput!) { group('.defaultOperation', () { test('with specified document', () { - final query = - Query(providerArgs: {'operation': GraphqlOperation(document: upsertPersonWithNodes)}); + const query = Query( + forProviders: [ + GraphqlProviderQuery( + operation: GraphqlOperation(document: upsertPersonWithNodes), + ), + ], + ); final transformer = ModelFieldsDocumentTransformer.defaultOperation( dictionary, action: QueryAction.get, @@ -143,6 +148,20 @@ mutation UpsertPerson($input: UpsertPersonInput!) { expect( lang.printNode(transformer!.document), startsWith(r'''mutation UpsertPerson($input: UpsertPersonInput!) { + upsertPerson(input: $input) {'''), + ); + + const query0 = + // ignore: deprecated_member_use + Query(providerArgs: {'operation': GraphqlOperation(document: upsertPersonWithNodes)}); + final transformer0 = ModelFieldsDocumentTransformer.defaultOperation( + dictionary, + action: QueryAction.get, + query: query0, + ); + expect( + lang.printNode(transformer0!.document), + startsWith(r'''mutation UpsertPerson($input: UpsertPersonInput!) { upsertPerson(input: $input) {'''), ); }); @@ -197,7 +216,7 @@ mutation UpsertPerson($input: UpsertPersonInput!) { final transformer = ModelFieldsDocumentTransformer.defaultOperation( dictionary, action: QueryAction.get, - query: Query(action: QueryAction.get), + query: const Query(action: QueryAction.get), ); expect( lang.printNode(transformer!.document), @@ -237,7 +256,7 @@ mutation UpsertPerson($input: UpsertPersonInput!) { final transformer = ModelFieldsDocumentTransformer.defaultOperation( dictionary, action: QueryAction.subscribe, - query: Query(action: QueryAction.get), + query: const Query(action: QueryAction.get), ); expect( lang.printNode(transformer!.document), diff --git a/packages/brick_graphql_generators/CHANGELOG.md b/packages/brick_graphql_generators/CHANGELOG.md index cf3c236f..069fc8d1 100644 --- a/packages/brick_graphql_generators/CHANGELOG.md +++ b/packages/brick_graphql_generators/CHANGELOG.md @@ -1,5 +1,10 @@ ## Unreleased +## 3.3.0 + +- Upgrade `brick_core` to `1.3.0` +- Update analysis to modern lints + ## 3.2.1 - Use `renameField` from `brick_build`'s `AnnotationFinderWithFieldRename` mixin diff --git a/packages/brick_graphql_generators/lib/graphql_model_serdes_generator.dart b/packages/brick_graphql_generators/lib/graphql_model_serdes_generator.dart index 44c2dd43..b7105844 100644 --- a/packages/brick_graphql_generators/lib/graphql_model_serdes_generator.dart +++ b/packages/brick_graphql_generators/lib/graphql_model_serdes_generator.dart @@ -15,6 +15,8 @@ class GraphqlModelSerdesGenerator /// should not be included. final String repositoryName; + /// Digest a `graphqlConfig` (`@ConnectOfflineFirstWithGraphQL`) from [reader] and manage serdes generators + /// to and from a `GraphqlProvider`. GraphqlModelSerdesGenerator( super.element, super.reader, { @@ -24,7 +26,7 @@ class GraphqlModelSerdesGenerator @override GraphqlSerializableExtended get config { if (reader.peek(configKey) == null) { - return GraphqlSerializableExtended(); + return const GraphqlSerializableExtended(); } final fieldRenameIndex = diff --git a/packages/brick_graphql_generators/lib/src/graphql_deserialize.dart b/packages/brick_graphql_generators/lib/src/graphql_deserialize.dart index 997588e3..b023a9c6 100644 --- a/packages/brick_graphql_generators/lib/src/graphql_deserialize.dart +++ b/packages/brick_graphql_generators/lib/src/graphql_deserialize.dart @@ -19,6 +19,7 @@ class GraphqlDeserialize extends GraphqlSerdesGenerator ]; } + /// Generate a function to produce a [ClassElement] from GraphQL data GraphqlDeserialize( super.element, super.fields, { diff --git a/packages/brick_graphql_generators/lib/src/graphql_fields.dart b/packages/brick_graphql_generators/lib/src/graphql_fields.dart index f4b52afd..61102aed 100644 --- a/packages/brick_graphql_generators/lib/src/graphql_fields.dart +++ b/packages/brick_graphql_generators/lib/src/graphql_fields.dart @@ -9,12 +9,14 @@ import 'package:brick_graphql_generators/src/graphql_serializable_query_transfor /// Find `@Graphql` given a field class GraphqlAnnotationFinder extends AnnotationFinder with AnnotationFinderWithFieldRename { + /// final GraphqlSerializable? config; + /// Find `@Graphql` given a field GraphqlAnnotationFinder([this.config]); @override - Graphql from(element) { + Graphql from(FieldElement element) { final obj = objectForField(element); if (obj == null) { @@ -53,20 +55,21 @@ class GraphqlAnnotationFinder extends AnnotationFinder if (unconvertedMap == null) return {}; return { for (final entry in unconvertedMap.entries) - entry.key!.toStringValue()!: entry.value?.toStringValue() == null - ? _convertMapToMap(entry.value!.toMapValue()!) - : {}, + entry.key!.toStringValue()!: + entry.value?.toStringValue() == null ? _convertMapToMap(entry.value!.toMapValue()) : {}, }; } } /// Converts all fields to [Graphql]s for later consumption class GraphqlFields extends FieldsForClass { + /// final GraphqlSerializableExtended? config; @override final GraphqlAnnotationFinder finder; + /// Converts all fields to [Graphql]s for later consumption GraphqlFields(ClassElement element, [this.config]) : finder = GraphqlAnnotationFinder(config), super(element: element); diff --git a/packages/brick_graphql_generators/lib/src/graphql_serdes_generator.dart b/packages/brick_graphql_generators/lib/src/graphql_serdes_generator.dart index 94d99584..8b01a27c 100644 --- a/packages/brick_graphql_generators/lib/src/graphql_serdes_generator.dart +++ b/packages/brick_graphql_generators/lib/src/graphql_serdes_generator.dart @@ -2,7 +2,9 @@ import 'package:brick_graphql/brick_graphql.dart'; import 'package:brick_graphql_generators/src/graphql_fields.dart'; import 'package:brick_json_generators/json_serdes_generator.dart'; +/// abstract class GraphqlSerdesGenerator extends JsonSerdesGenerator { + /// GraphqlSerdesGenerator( super.element, GraphqlFields super.fields, { diff --git a/packages/brick_graphql_generators/lib/src/graphql_serializable_query_transformer_extended.dart b/packages/brick_graphql_generators/lib/src/graphql_serializable_query_transformer_extended.dart index 1a7fbf11..458faed5 100644 --- a/packages/brick_graphql_generators/lib/src/graphql_serializable_query_transformer_extended.dart +++ b/packages/brick_graphql_generators/lib/src/graphql_serializable_query_transformer_extended.dart @@ -4,8 +4,17 @@ import 'package:brick_graphql/brick_graphql.dart'; /// however, the function can't be re-interpreted by ConstantReader. /// So the name is grabbed to be used in a later generator. class GraphqlSerializableExtended extends GraphqlSerializable { + /// The interface used to determine the document to send GraphQL. This class + /// will be accessed for all provider and repository operations. + /// + /// Implementing classes of [GraphqlQueryOperationTransformer] must be a `const` + /// constructor. For simplicity, the default constructor tearoff can be provided + /// as a value (`queryOperationTransformer: MyTransformer.new`). final String? queryOperationTransformerName; + /// [GraphqlSerializable] has `queryOperationTransformer`, + /// however, the function can't be re-interpreted by ConstantReader. + /// So the name is grabbed to be used in a later generator. const GraphqlSerializableExtended({ super.fieldRename, this.queryOperationTransformerName, diff --git a/packages/brick_graphql_generators/lib/src/graphql_serialize.dart b/packages/brick_graphql_generators/lib/src/graphql_serialize.dart index 7856d49a..8e0998ae 100644 --- a/packages/brick_graphql_generators/lib/src/graphql_serialize.dart +++ b/packages/brick_graphql_generators/lib/src/graphql_serialize.dart @@ -18,6 +18,7 @@ class GraphqlSerialize extends GraphqlSerdesGenerator with JsonSerialize=2.18.0 <4.0.0" @@ -12,7 +12,7 @@ environment: dependencies: analyzer: ">=6.0.0 <7.0.0" brick_build: ">=3.2.0 <4.0.0" - brick_core: ">=1.2.1 <2.0.0" + brick_core: ">=1.3.0 <2.0.0" brick_graphql: ">=3.0.0 <4.0.0" brick_json_generators: ">=3.0.0 <4.0.0" build: ">=2.0.0 <3.0.0" @@ -20,7 +20,7 @@ dependencies: source_gen: ">=1.2.2 <2.0.0" dev_dependencies: - test: ^1.20.1 - lints: ^2.0.1 brick_build_test: path: ../brick_build_test + lints: + test: diff --git a/packages/brick_graphql_generators/test/graphql_model_serdes_generator/test_annotation_subfields.dart b/packages/brick_graphql_generators/test/graphql_model_serdes_generator/test_annotation_subfields.dart index 6ebb66b6..c97b31d9 100644 --- a/packages/brick_graphql_generators/test/graphql_model_serdes_generator/test_annotation_subfields.dart +++ b/packages/brick_graphql_generators/test/graphql_model_serdes_generator/test_annotation_subfields.dart @@ -1,6 +1,6 @@ import 'package:brick_graphql/brick_graphql.dart'; -final output = r''' +const output = r''' // GENERATED CODE DO NOT EDIT part of '../brick.g.dart'; diff --git a/packages/brick_graphql_generators/test/graphql_model_serdes_generator/test_constructor_member_field_mismatch.dart b/packages/brick_graphql_generators/test/graphql_model_serdes_generator/test_constructor_member_field_mismatch.dart index 89324198..2f63c497 100644 --- a/packages/brick_graphql_generators/test/graphql_model_serdes_generator/test_constructor_member_field_mismatch.dart +++ b/packages/brick_graphql_generators/test/graphql_model_serdes_generator/test_constructor_member_field_mismatch.dart @@ -1,6 +1,6 @@ import 'package:brick_graphql/brick_graphql.dart'; -final output = r''' +const output = r''' Future _$GraphqlConstructorMemberFieldMismatchFromGraphql( Map data, diff --git a/packages/brick_graphql_generators/test/graphql_model_serdes_generator/test_enum_as_string.dart b/packages/brick_graphql_generators/test/graphql_model_serdes_generator/test_enum_as_string.dart index 6b2f7ac9..9bf9a45f 100644 --- a/packages/brick_graphql_generators/test/graphql_model_serdes_generator/test_enum_as_string.dart +++ b/packages/brick_graphql_generators/test/graphql_model_serdes_generator/test_enum_as_string.dart @@ -1,6 +1,6 @@ import 'package:brick_graphql/brick_graphql.dart'; -final output = r''' +const output = r''' Future _$EnumAsStringFromGraphql(Map data, {required GraphqlProvider provider, GraphqlFirstRepository? repository}) async { diff --git a/packages/brick_graphql_generators/test/graphql_model_serdes_generator/test_from_json_to_json.dart b/packages/brick_graphql_generators/test/graphql_model_serdes_generator/test_from_json_to_json.dart index 1c18e954..59d94aa9 100644 --- a/packages/brick_graphql_generators/test/graphql_model_serdes_generator/test_from_json_to_json.dart +++ b/packages/brick_graphql_generators/test/graphql_model_serdes_generator/test_from_json_to_json.dart @@ -1,6 +1,6 @@ import 'package:brick_graphql/brick_graphql.dart'; -final output = r''' +const output = r''' // GENERATED CODE DO NOT EDIT part of '../brick.g.dart'; diff --git a/packages/brick_graphql_generators/test/graphql_model_serdes_generator/test_ignore_from_to.dart b/packages/brick_graphql_generators/test/graphql_model_serdes_generator/test_ignore_from_to.dart index 051c7feb..11dc7d93 100644 --- a/packages/brick_graphql_generators/test/graphql_model_serdes_generator/test_ignore_from_to.dart +++ b/packages/brick_graphql_generators/test/graphql_model_serdes_generator/test_ignore_from_to.dart @@ -1,6 +1,6 @@ import 'package:brick_graphql/brick_graphql.dart'; -final output = r''' +const output = r''' Future _$GraphqlIgnoreFromToFromGraphql( Map data, {required GraphqlProvider provider, diff --git a/packages/brick_graphql_generators/test/graphql_model_serdes_generator/test_runtime_association_definition.dart b/packages/brick_graphql_generators/test/graphql_model_serdes_generator/test_runtime_association_definition.dart index 721981ea..f8988286 100644 --- a/packages/brick_graphql_generators/test/graphql_model_serdes_generator/test_runtime_association_definition.dart +++ b/packages/brick_graphql_generators/test/graphql_model_serdes_generator/test_runtime_association_definition.dart @@ -1,6 +1,6 @@ import 'package:brick_graphql/brick_graphql.dart'; -final output = r''' +const output = r''' // GENERATED CODE DO NOT EDIT part of '../brick.g.dart'; diff --git a/packages/brick_graphql_generators/test/graphql_model_serdes_generator/test_unserializable_field_with_generator.dart b/packages/brick_graphql_generators/test/graphql_model_serdes_generator/test_unserializable_field_with_generator.dart index fcee516d..0630d53d 100644 --- a/packages/brick_graphql_generators/test/graphql_model_serdes_generator/test_unserializable_field_with_generator.dart +++ b/packages/brick_graphql_generators/test/graphql_model_serdes_generator/test_unserializable_field_with_generator.dart @@ -3,7 +3,7 @@ import 'dart:typed_data'; import 'package:brick_graphql/brick_graphql.dart'; -final output = r''' +const output = r''' Future _$GraphqlUnserializableFieldWithGeneratorFromGraphql( Map data, diff --git a/packages/brick_graphql_generators/test/graphql_model_serdes_generator_test.dart b/packages/brick_graphql_generators/test/graphql_model_serdes_generator_test.dart index 0b986b2a..ad249f85 100644 --- a/packages/brick_graphql_generators/test/graphql_model_serdes_generator_test.dart +++ b/packages/brick_graphql_generators/test/graphql_model_serdes_generator_test.dart @@ -18,7 +18,7 @@ import 'graphql_model_serdes_generator/test_unserializable_field_with_generator. as unserializable_field_with_generator; final _generator = TestGenerator(); -final folder = 'graphql_model_serdes_generator'; +const folder = 'graphql_model_serdes_generator'; final generateReader = generateLibraryForFolder(folder); void main() { diff --git a/packages/brick_json_generators/CHANGELOG.md b/packages/brick_json_generators/CHANGELOG.md index 51e5f28a..52b0c150 100644 --- a/packages/brick_json_generators/CHANGELOG.md +++ b/packages/brick_json_generators/CHANGELOG.md @@ -2,6 +2,8 @@ - (test) remove analysis options override for non-standard library prefixes - Revert `.getDisplayString()` change due to Flutter 3.22 being restricted to analyzer <6.4.1. `meta` is pinned to `1.12` in this version of Flutter, and `analyzer >=6.5.0`, where the change was made, requires `meta >= 1.15`. This change will eventually be re-reverted. +- Upgrade `brick_core` to `1.3.0` +- Update analysis to modern lints ## 3.1.1 diff --git a/packages/brick_json_generators/lib/json_deserialize.dart b/packages/brick_json_generators/lib/json_deserialize.dart index 4bd95aab..f7cf03ce 100644 --- a/packages/brick_json_generators/lib/json_deserialize.dart +++ b/packages/brick_json_generators/lib/json_deserialize.dart @@ -4,13 +4,19 @@ import 'package:brick_core/core.dart'; import 'package:brick_core/field_serializable.dart'; import 'package:brick_json_generators/json_serdes_generator.dart'; +/// Default deserialize implementation of [coderForField] for JSON-based providers mixin JsonDeserialize on JsonSerdesGenerator { @override final doesDeserialize = true; @override - String? coderForField(field, checker, {required wrappedInFuture, required fieldAnnotation}) { + String? coderForField( + FieldElement field, + SharedChecker checker, { + required bool wrappedInFuture, + required Annotation fieldAnnotation, + }) { final fieldValue = serdesValueForField(field, fieldAnnotation.name!, checker: checker); final defaultValue = SerdesGenerator.defaultValueSuffix(fieldAnnotation); if (fieldAnnotation.ignoreFrom) return null; @@ -89,12 +95,12 @@ mixin JsonDeserialize if (argTypeChecker.fromJsonConstructor != null) { - final klass = argTypeChecker.targetType.element as ClassElement; + final klass = argTypeChecker.targetType.element! as ClassElement; final parameterType = argTypeChecker.fromJsonConstructor!.parameters.first.type; final nullableSuffix = checker.isNullable ? '?' : ''; return '''$fieldValue$nullableSuffix.map( - (d) => ${klass.displayName}.fromJson(d as ${parameterType.getDisplayString(withNullability: true)}) + (d) => ${klass.displayName}.fromJson(d as ${parameterType.getDisplayString()}) )$castIterable$defaultValue'''; } @@ -134,11 +140,11 @@ mixin JsonDeserialize extends SerdesGenerator { @override @@ -10,6 +11,7 @@ abstract class JsonSerdesGenerator<_Model extends Model, Annotation extends Fiel @override final String repositoryName; + /// Default generator implementation JSON-based providers JsonSerdesGenerator( super.element, super.fields, { diff --git a/packages/brick_json_generators/lib/json_serialize.dart b/packages/brick_json_generators/lib/json_serialize.dart index 1d12d281..5d562653 100644 --- a/packages/brick_json_generators/lib/json_serialize.dart +++ b/packages/brick_json_generators/lib/json_serialize.dart @@ -1,16 +1,23 @@ +import 'package:analyzer/dart/element/element.dart'; import 'package:analyzer/dart/element/nullability_suffix.dart'; import 'package:brick_build/generators.dart'; import 'package:brick_core/core.dart'; import 'package:brick_core/field_serializable.dart'; import 'package:brick_json_generators/json_serdes_generator.dart'; +/// Default serialize implementation of [coderForField] for JSON-based providers mixin JsonSerialize on JsonSerdesGenerator { @override final doesDeserialize = false; @override - String? coderForField(field, checker, {required wrappedInFuture, required fieldAnnotation}) { + String? coderForField( + FieldElement field, + SharedChecker checker, { + required bool wrappedInFuture, + required Annotation fieldAnnotation, + }) { final fieldValue = serdesValueForField(field, fieldAnnotation.name!, checker: checker); if (fieldAnnotation.ignoreTo) return null; diff --git a/packages/brick_json_generators/pubspec.yaml b/packages/brick_json_generators/pubspec.yaml index 4723bddf..01e91202 100644 --- a/packages/brick_json_generators/pubspec.yaml +++ b/packages/brick_json_generators/pubspec.yaml @@ -11,14 +11,14 @@ environment: dependencies: analyzer: ">=6.0.0 <7.0.0" - brick_core: ^1.1.1 brick_build: ">=3.0.0 <4.0.0" + brick_core: ^1.3.0 build: ">=2.0.0 <3.0.0" dart_style: ">=2.0.0 <3.0.0" source_gen: ">=1.2.2 <2.0.0" dev_dependencies: - test: ^1.20.1 - lints: ^2.0.1 brick_build_test: path: ../brick_build_test + lints: + test: diff --git a/packages/brick_offline_first/CHANGELOG.md b/packages/brick_offline_first/CHANGELOG.md index a71b01e2..c9fef904 100644 --- a/packages/brick_offline_first/CHANGELOG.md +++ b/packages/brick_offline_first/CHANGELOG.md @@ -2,6 +2,8 @@ ## 3.4.0 +- Upgrade `brick_core` to `1.3.0` +- Update analysis to modern lints - Change `OfflineFirstRepository#exists` behavior: the check against memory cache will only return `true` if results have been found, otherwise it will continue to the SQLite provider - Forward errors from `OfflineFirstRepository#subscribe` streams to their callers (@sonbs21 #484) diff --git a/packages/brick_offline_first/lib/src/annotations/offline_first_annotation.dart b/packages/brick_offline_first/lib/src/annotations/offline_first_annotation.dart index da953076..859f119f 100644 --- a/packages/brick_offline_first/lib/src/annotations/offline_first_annotation.dart +++ b/packages/brick_offline_first/lib/src/annotations/offline_first_annotation.dart @@ -1,3 +1,5 @@ +import 'package:brick_core/field_serializable.dart'; + /// Low-level field config for the `OfflineFirst` domain. class OfflineFirst { /// When `true` (the default), [where] will be used to fetch associations by their defined keys. @@ -8,7 +10,7 @@ class OfflineFirst { /// configuration `{'id' : "data['assoc']['id']"}`, a REST adapter would generate /// ``` /// await repository?.getAssociation( - /// Query(where: [Where.exact('id', data['assoc']['id])], providerArgs: {'limit': 1}) + /// Query(where: [Where.exact('id', data['assoc']['id])], limit: 1) /// ) /// ``` /// @@ -38,7 +40,7 @@ class OfflineFirst { /// /// If [where] is not defined for an association, Brick will attempt to instantiate the /// association from the data in the payload. When [where] is included, the field will - /// not be generated for the serializer of the remote provider unless [toGenerator] is defined **or** only one pair is defined. + /// not be generated for the serializer of the remote provider unless [FieldSerializable.toGenerator] is defined **or** only one pair is defined. final Map? where; /// Annotates classes that require extra manipulation to map to the expected field type @@ -47,5 +49,6 @@ class OfflineFirst { this.where, }); - static const defaults = OfflineFirst(applyToRemoteDeserialization: true); + /// + static const defaults = OfflineFirst(); } diff --git a/packages/brick_offline_first/lib/src/mixins/destructive_local_sync_from_remote_mixin.dart b/packages/brick_offline_first/lib/src/mixins/destructive_local_sync_from_remote_mixin.dart index 1da4fd4e..313f654e 100644 --- a/packages/brick_offline_first/lib/src/mixins/destructive_local_sync_from_remote_mixin.dart +++ b/packages/brick_offline_first/lib/src/mixins/destructive_local_sync_from_remote_mixin.dart @@ -12,10 +12,10 @@ import 'package:brick_offline_first/src/offline_first_repository.dart'; /// should not be paginated and complete from a single request. mixin DestructiveLocalSyncFromRemoteMixin on OfflineFirstRepository { + /// When [forceLocalSyncFromRemote] is `true`, local instances that do not exist in the [remoteProvider] + /// are destroyed. Further, when `true`, all values from other parameters except [query] are ignored. @override Future> get({ - /// When [forceLocalSyncFromRemote] is `true`, local instances that do not exist in the [remoteProvider] - /// are destroyed. Further, when `true`, all values from other parameters except [query] are ignored. bool forceLocalSyncFromRemote = false, OfflineFirstGetPolicy policy = OfflineFirstGetPolicy.awaitRemoteWhenNoneExist, Query? query, @@ -36,7 +36,7 @@ mixin DestructiveLocalSyncFromRemoteMixin /// do not exist in the [remoteProvider] are destroyed. The data from the [remoteProvider] /// should not be paginated and must be complete from a single request. Future> destructiveLocalSyncFromRemote({Query? query}) async { - query = (query ?? Query()).copyWith(action: QueryAction.get); + query = (query ?? const Query()).copyWith(action: QueryAction.get); logger.finest('#get: $TModel $query'); final remoteResults = await remoteProvider.get(query: query, repository: this); diff --git a/packages/brick_offline_first/lib/src/mixins/get_first_mixin.dart b/packages/brick_offline_first/lib/src/mixins/get_first_mixin.dart index 8a907a5a..0001fe51 100644 --- a/packages/brick_offline_first/lib/src/mixins/get_first_mixin.dart +++ b/packages/brick_offline_first/lib/src/mixins/get_first_mixin.dart @@ -7,7 +7,7 @@ mixin GetFirstMixin /// If no instances exist, a [StateError] is thrown from within Dart's core /// `Iterable#first` method. It is recommended to use [getFirstOrNull] instead. /// - /// Automatically applies `'limit': 1` to the query's `providerArgs` + /// Automatically applies `'limit': 1` to the query. Future getFirst({ OfflineFirstGetPolicy policy = OfflineFirstGetPolicy.awaitRemoteWhenNoneExist, Query? query, @@ -15,7 +15,7 @@ mixin GetFirstMixin }) async { final result = await super.get( policy: policy, - query: query?.copyWith(providerArgs: {'limit': 1}), + query: query?.copyWith(limit: 1), seedOnly: seedOnly, ); @@ -25,7 +25,7 @@ mixin GetFirstMixin /// A safer version of [getFirst] that attempts to get the first instance of [TModel] /// according to the [query], but returns `null` if no instances exist. /// - /// Automatically applies `'limit': 1` to the query's `providerArgs` + /// Automatically applies `'limit': 1` to the query. Future getFirstOrNull({ OfflineFirstGetPolicy policy = OfflineFirstGetPolicy.awaitRemoteWhenNoneExist, Query? query, @@ -33,7 +33,7 @@ mixin GetFirstMixin }) async { final result = await super.get( policy: policy, - query: query?.copyWith(providerArgs: {'limit': 1}), + query: query?.copyWith(limit: 1), seedOnly: seedOnly, ); diff --git a/packages/brick_offline_first/lib/src/models/offline_first_serdes.dart b/packages/brick_offline_first/lib/src/models/offline_first_serdes.dart index 6e6dca17..3cfe3a3e 100644 --- a/packages/brick_offline_first/lib/src/models/offline_first_serdes.dart +++ b/packages/brick_offline_first/lib/src/models/offline_first_serdes.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + /// A class that isn't connected to the `OfflineFirstRepository` but is still used /// by `OfflineFirstModels` (such as a `Cash` class that declares `amount` and `currency`). /// [OfflineFirstSerdes] **must** extend the class in end implementation. diff --git a/packages/brick_offline_first/lib/src/offline_first_adapter.dart b/packages/brick_offline_first/lib/src/offline_first_adapter.dart index b0eab9fb..039fa0a2 100644 --- a/packages/brick_offline_first/lib/src/offline_first_adapter.dart +++ b/packages/brick_offline_first/lib/src/offline_first_adapter.dart @@ -1,10 +1,12 @@ import 'package:brick_offline_first/src/models/offline_first_model.dart'; import 'package:brick_offline_first/src/runtime_offline_first_definition.dart'; -import 'package:brick_sqlite/brick_sqlite.dart' show SqliteAdapter; +import 'package:brick_sqlite/brick_sqlite.dart'; -/// This adapter fetches first from [SqliteProvider] then hydrates with from a remote provider.. +/// This adapter fetches first from [SqliteProvider] then hydrates with from a remote provider. abstract class OfflineFirstAdapter<_Model extends OfflineFirstModel> extends SqliteAdapter<_Model> { + /// Keyed by Dart field name. See [RuntimeOfflineFirstDefinition] Map get fieldsToOfflineFirstRuntimeDefinition => {}; + /// This adapter fetches first from [SqliteProvider] then hydrates with from a remote provider.. OfflineFirstAdapter(); } diff --git a/packages/brick_offline_first/lib/src/offline_first_exception.dart b/packages/brick_offline_first/lib/src/offline_first_exception.dart index 9e1d75a2..d4b184e0 100644 --- a/packages/brick_offline_first/lib/src/offline_first_exception.dart +++ b/packages/brick_offline_first/lib/src/offline_first_exception.dart @@ -1,9 +1,18 @@ +import 'package:brick_sqlite/brick_sqlite.dart'; + +/// An exception thrown by the remote provider or the [SqliteProvider]. +/// An implementation may choose to ignore this error if the remote exception +/// is not important to the requested behavior. class OfflineFirstException implements Exception { - /// The producing error from either [RestProvider] or [SqliteProvider]. + /// The producing error from the remote provider or [SqliteProvider]. final Exception originalError; + /// An exception thrown by the remote provider or the [SqliteProvider]. + /// An implementation may choose to ignore this error if the remote exception + /// is not important to the requested behavior. OfflineFirstException(this.originalError); + /// String get message => originalError.toString(); @override diff --git a/packages/brick_offline_first/lib/src/offline_first_policy.dart b/packages/brick_offline_first/lib/src/offline_first_policy.dart index 7328e95c..e1699705 100644 --- a/packages/brick_offline_first/lib/src/offline_first_policy.dart +++ b/packages/brick_offline_first/lib/src/offline_first_policy.dart @@ -1,3 +1,4 @@ +/// Behaviors for how the repository should handle delete requests enum OfflineFirstDeletePolicy { /// Delete local results before waiting for the remote provider to respond optimisticLocal, @@ -26,6 +27,7 @@ enum OfflineFirstGetPolicy { localOnly, } +/// Behaviors for how the repository should handle upsert requests enum OfflineFirstUpsertPolicy { /// Save results to local before waiting for the remote provider to respond optimisticLocal, diff --git a/packages/brick_offline_first/lib/src/offline_first_repository.dart b/packages/brick_offline_first/lib/src/offline_first_repository.dart index 743eed6c..8d7aa331 100644 --- a/packages/brick_offline_first/lib/src/offline_first_repository.dart +++ b/packages/brick_offline_first/lib/src/offline_first_repository.dart @@ -1,9 +1,8 @@ import 'dart:async'; import 'dart:io'; -import 'package:brick_core/core.dart' show Query, ModelRepository, QueryAction, Provider; -import 'package:brick_offline_first/src/models/offline_first_model.dart'; -import 'package:brick_offline_first/src/offline_first_policy.dart'; +import 'package:brick_core/core.dart' show ModelRepository, Provider, Query, QueryAction; +import 'package:brick_offline_first/brick_offline_first.dart'; import 'package:brick_sqlite/brick_sqlite.dart'; import 'package:brick_sqlite/db.dart'; import 'package:brick_sqlite/memory_cache_provider.dart'; @@ -15,7 +14,7 @@ import 'package:meta/meta.dart'; /// A [ModelRepository] that interacts with a [SqliteProvider] first before using a [Provider] from a remote source. /// /// The `OfflineFirstRepository` should be extended by an implementation in the end class. -/// The implementation can then be accessed via singleton or [InheritedWidget]. +/// The implementation can then be accessed via singleton or `InheritedWidget`. /// For example: /// ```dart /// class MyRepository extends OfflineFirstRepository { @@ -44,7 +43,7 @@ import 'package:meta/meta.dart'; abstract class OfflineFirstRepository implements ModelRepository { /// Refetch results in the background from remote source when any request is made. - /// Defaults to [false]. + /// Defaults to `false`. final bool autoHydrate; /// Required to maintain the same policy for [getAssociation] requests. @@ -57,6 +56,7 @@ abstract class OfflineFirstRepository>>> subscriptions = {}; @@ -74,6 +75,7 @@ abstract class OfflineFirstRepository(instance, query: query); - await notifySubscriptionsWithLocalData(notifyWhenEmpty: true); + await notifySubscriptionsWithLocalData(); } try { await remoteProvider.delete(instance, query: query, repository: this); if (requireRemote) { rowsDeleted = await _deleteLocal(instance, query: query); - await notifySubscriptionsWithLocalData(notifyWhenEmpty: true); + await notifySubscriptionsWithLocalData(); } } on ClientException catch (e) { logger.warning('#delete client failure: $e'); @@ -179,7 +181,7 @@ abstract class OfflineFirstRepository[]; @@ -259,7 +262,8 @@ abstract class OfflineFirstRepository> getFrom(int offset) async { // add offset to the existing query final recursiveQuery = queryWithLimit.copyWith( - providerArgs: {...queryWithLimit.providerArgs, 'offset': offset}, + offset: offset, + providerArgs: {...queryWithLimit.providerArgs}, ); final results = await get( @@ -363,7 +367,7 @@ abstract class OfflineFirstRepository>; } @@ -403,7 +407,7 @@ abstract class OfflineFirstRepository(modelIds, eagerError: true); MapEntry modelWithPrimaryKey(index, id) { - final model = models[index]; - model.primaryKey = id; + final model = models[index]..primaryKey = id; return MapEntry(index, model); } diff --git a/packages/brick_offline_first/lib/src/offline_queue/offline_request_queue.dart b/packages/brick_offline_first/lib/src/offline_queue/offline_request_queue.dart index f471eca4..19a44ef6 100644 --- a/packages/brick_offline_first/lib/src/offline_queue/offline_request_queue.dart +++ b/packages/brick_offline_first/lib/src/offline_queue/offline_request_queue.dart @@ -9,6 +9,7 @@ abstract class OfflineRequestQueue { /// If the queue is processing bool get isRunning => _timer?.isActive ?? false; + /// @protected final Logger logger; @@ -16,12 +17,15 @@ abstract class OfflineRequestQueue { /// not occur as the Timer runs in sub routines or isolates bool _processingInBackground = false; + /// How often requests are reattempted final Duration processingInterval; + /// final RequestSqliteCacheManager requestManager; Timer? _timer; + /// Repeatedly reattempts requests in an interval OfflineRequestQueue({ required this.processingInterval, required this.requestManager, @@ -45,7 +49,7 @@ abstract class OfflineRequestQueue { } } - /// Start the processing queue, resending requests every [interval]. + /// Start the processing queue, resending requests every [processingInterval]. /// Stops the existing timer if it was already running. void start() { stop(); diff --git a/packages/brick_offline_first/lib/src/offline_queue/request_sqlite_cache.dart b/packages/brick_offline_first/lib/src/offline_queue/request_sqlite_cache.dart index 66298cee..33b93afd 100644 --- a/packages/brick_offline_first/lib/src/offline_queue/request_sqlite_cache.dart +++ b/packages/brick_offline_first/lib/src/offline_queue/request_sqlite_cache.dart @@ -2,17 +2,30 @@ import 'package:logging/logging.dart'; import 'package:meta/meta.dart'; import 'package:sqflite_common/sqlite_api.dart' show Database, DatabaseExecutor; -/// Serialize and Deserialize a [Request] from SQLite. +/// Serialize and Deserialize a [TRequest] from SQLite. abstract class RequestSqliteCache { + /// Column that tracks the number of attempts final String attemptColumn; + + /// final String createdAtColumn; + + /// Column that tracks if the request is locked final String lockedColumn; + + /// Column that tracks the primary key final String primaryKeyColumn; + + /// final TRequest request; /// Columns used to uniquely identify the request (e.g. body, headers, url, method). final List requestColumns; + + /// final String tableName; + + /// final String updateAtColumn; /// Matches any HTTP requests that send data (or 'push'). 'Pull' requests most often have an @@ -51,6 +64,7 @@ abstract class RequestSqliteCache { return 0; } + /// @protected Future?> findRequestInDatabase(DatabaseExecutor db) async { final whereStatement = requestColumns.join(' = ? AND '); @@ -101,7 +115,7 @@ abstract class RequestSqliteCache { TRequest sqliteToRequest(Map data); /// Builds request into a new SQLite-insertable row - /// Only available if [request] was initialized from [fromRequest] + /// Only available if [request] was initialized from [sqliteToRequest] /// /// This is a function to ensure `DateTime.now()` is invoked predictably. Map toSqlite(); @@ -121,6 +135,7 @@ abstract class RequestSqliteCache { }); } + /// static Future lockRequest({ required DatabaseExecutor db, required Map data, @@ -130,6 +145,7 @@ abstract class RequestSqliteCache { }) async => await _updateLock(true, data, db, tableName, lockedColumn, primaryKeyColumn); + /// static Future unlockRequest({ required DatabaseExecutor db, required Map data, diff --git a/packages/brick_offline_first/lib/src/offline_queue/request_sqlite_cache_manager.dart b/packages/brick_offline_first/lib/src/offline_queue/request_sqlite_cache_manager.dart index 279cc4e4..215a8abf 100644 --- a/packages/brick_offline_first/lib/src/offline_queue/request_sqlite_cache_manager.dart +++ b/packages/brick_offline_first/lib/src/offline_queue/request_sqlite_cache_manager.dart @@ -1,6 +1,6 @@ import 'package:brick_offline_first/src/offline_queue/request_sqlite_cache.dart'; import 'package:meta/meta.dart'; -import 'package:sqflite_common/sqlite_api.dart' show Database, DatabaseFactory, DatabaseExecutor; +import 'package:sqflite_common/sqlite_api.dart' show Database, DatabaseExecutor, DatabaseFactory; /// Fetch and delete [RequestSqliteCache]s. abstract class RequestSqliteCacheManager { @@ -15,15 +15,25 @@ abstract class RequestSqliteCacheManager { /// With [databaseFactory], this is most commonly the /// `sqlite_common` constant `inMemoryDatabasePath`. final String createdAtColumn; + + /// Database file path final String databaseName; + + /// Column that tracks if the request is locked final String lockedColumn; + + /// Column that tracks the primary key final String primaryKeyColumn; + + /// final String updateAtColumn; Future? _db; + /// final String tableName; + /// String get orderByStatement { if (!serialProcessing) { return '$updateAtColumn ASC'; @@ -39,6 +49,7 @@ abstract class RequestSqliteCacheManager { /// Defaults `true`. final bool serialProcessing; + /// Fetch and delete [RequestSqliteCache]s. RequestSqliteCacheManager( this.databaseName, { required this.createdAtColumn, @@ -68,6 +79,7 @@ abstract class RequestSqliteCacheManager { return result > 0; } + /// Future getDb() { _db ??= databaseFactory.openDatabase(databaseName); @@ -138,7 +150,7 @@ abstract class RequestSqliteCacheManager { tableName, distinct: true, where: '$lockedColumn = ? AND $createdAtColumn <= ?', - whereArgs: [whereLocked ? 1 : 0, nowMinusNextPoll], + whereArgs: [if (whereLocked) 1 else 0, nowMinusNextPoll], orderBy: orderByStatement, limit: 1, ); diff --git a/packages/brick_offline_first/lib/src/runtime_offline_first_definition.dart b/packages/brick_offline_first/lib/src/runtime_offline_first_definition.dart index c4cc9940..2c40c241 100644 --- a/packages/brick_offline_first/lib/src/runtime_offline_first_definition.dart +++ b/packages/brick_offline_first/lib/src/runtime_offline_first_definition.dart @@ -5,6 +5,8 @@ class RuntimeOfflineFirstDefinition { /// `OfflineFirst` annotation final Map where; + /// Used to define types in [OfflineFirstAdapter#fieldsToOfflineFirstRuntimeDefinition]. The build runner package + /// extracts types and associations that would've been otherwise inaccessible at runtime. const RuntimeOfflineFirstDefinition({ required this.where, }); diff --git a/packages/brick_offline_first/pubspec.yaml b/packages/brick_offline_first/pubspec.yaml index bf93246d..55ed7f51 100644 --- a/packages/brick_offline_first/pubspec.yaml +++ b/packages/brick_offline_first/pubspec.yaml @@ -11,7 +11,7 @@ environment: sdk: ">=2.18.0 <4.0.0" dependencies: - brick_core: ^1.1.1 + brick_core: ^1.3.0 brick_sqlite: ">=3.0.0 <4.0.0" collection: ">=1.15.0 <2.0.0" dart_style: ">=2.0.0 <3.0.0" @@ -22,7 +22,7 @@ dependencies: sqflite_common: ">=2.0.0 <3.0.0" dev_dependencies: - lints: ^2.0.1 - mockito: ^5.0.0 - test: ^1.16.5 - sqflite_common_ffi: ^2.0.0 + lints: + mockito: + sqflite_common_ffi: + test: diff --git a/packages/brick_offline_first/test/offline_first/helpers/__mocks__.dart b/packages/brick_offline_first/test/offline_first/helpers/__mocks__.dart index 84986512..7fd5fdd0 100644 --- a/packages/brick_offline_first/test/offline_first/helpers/__mocks__.dart +++ b/packages/brick_offline_first/test/offline_first/helpers/__mocks__.dart @@ -1,3 +1,4 @@ +import 'package:brick_core/src/model_repository.dart'; import 'package:brick_offline_first/brick_offline_first.dart'; import 'package:brick_sqlite/brick_sqlite.dart'; import 'package:brick_sqlite/db.dart'; @@ -11,8 +12,8 @@ part 'horse_adapter.dart'; part 'mounty.dart'; part 'mounty_adapter.dart'; -/// The exact same as [DemoModel], except this class is tracked by the Memory Cache Provider -/// while [DemoModel] is not. +/// The exact same as [Mounty], except this class is tracked by the Memory Cache Provider +/// while [Mounty] is not. class MemoryDemoModel extends Mounty { MemoryDemoModel(String name) : super(name: name); } @@ -45,11 +46,13 @@ class DemoModelMigration extends Migration { } class TestRepository extends OfflineFirstWithTestRepository { - static TestRepository? _singleton; + static late TestRepository? _singleton; /// A hack to similuate a failure in the remote provider static bool throwOnNextRemoteMutation = false; + factory TestRepository() => _singleton!; + TestRepository._( TestProvider testProvider, SqliteProvider sqliteProvider, @@ -60,8 +63,6 @@ class TestRepository extends OfflineFirstWithTestRepository { migrations: {const DemoModelMigration()}, ); - factory TestRepository() => _singleton!; - factory TestRepository.withProviders(TestProvider testProvider, SqliteProvider sqliteProvider) => TestRepository._(testProvider, sqliteProvider); diff --git a/packages/brick_offline_first/test/offline_first/helpers/horse_adapter.dart b/packages/brick_offline_first/test/offline_first/helpers/horse_adapter.dart index b1613bff..cb5f7fc5 100644 --- a/packages/brick_offline_first/test/offline_first/helpers/horse_adapter.dart +++ b/packages/brick_offline_first/test/offline_first/helpers/horse_adapter.dart @@ -72,15 +72,11 @@ class HorseAdapter extends OfflineFirstWithTestAdapter { @override final Map fieldsToSqliteColumns = { 'primaryKey': const RuntimeSqliteColumnDefinition( - association: false, columnName: '_brick_id', - iterable: false, type: int, ), 'name': const RuntimeSqliteColumnDefinition( - association: false, columnName: 'name', - iterable: false, type: String, ), 'mounties': const RuntimeSqliteColumnDefinition( @@ -96,7 +92,11 @@ class HorseAdapter extends OfflineFirstWithTestAdapter { @override final String tableName = 'Horse'; @override - Future afterSave(instance, {required provider, repository}) async { + Future afterSave( + Horse instance, { + required SqliteProvider provider, + ModelRepository? repository, + }) async { if (instance.primaryKey != null) { await Future.wait( instance.mounties.map((s) async { @@ -113,28 +113,28 @@ class HorseAdapter extends OfflineFirstWithTestAdapter { @override Future fromTest( Map input, { - required provider, + required TestProvider provider, covariant OfflineFirstWithTestRepository? repository, }) async => await _$HorseFromTest(input, provider: provider, repository: repository); @override Future> toTest( Horse input, { - required provider, + required TestProvider provider, covariant OfflineFirstWithTestRepository? repository, }) async => await _$HorseToTest(input, provider: provider, repository: repository); @override Future fromSqlite( Map input, { - required provider, + required SqliteProvider provider, covariant OfflineFirstWithTestRepository? repository, }) async => await _$HorseFromSqlite(input, provider: provider, repository: repository); @override Future> toSqlite( Horse input, { - required provider, + required SqliteProvider provider, covariant OfflineFirstWithTestRepository? repository, }) async => await _$HorseToSqlite(input, provider: provider, repository: repository); diff --git a/packages/brick_offline_first/test/offline_first/helpers/mounty_adapter.dart b/packages/brick_offline_first/test/offline_first/helpers/mounty_adapter.dart index d598d460..fba30032 100644 --- a/packages/brick_offline_first/test/offline_first/helpers/mounty_adapter.dart +++ b/packages/brick_offline_first/test/offline_first/helpers/mounty_adapter.dart @@ -40,15 +40,11 @@ class MountyAdapter extends OfflineFirstWithTestAdapter { @override final Map fieldsToSqliteColumns = { 'primaryKey': const RuntimeSqliteColumnDefinition( - association: false, columnName: '_brick_id', - iterable: false, type: int, ), 'name': const RuntimeSqliteColumnDefinition( - association: false, columnName: 'name', - iterable: false, type: String, ), }; @@ -61,28 +57,28 @@ class MountyAdapter extends OfflineFirstWithTestAdapter { @override Future fromTest( Map input, { - required provider, + required TestProvider provider, covariant OfflineFirstWithTestRepository? repository, }) async => await _$MountyFromTest(input, provider: provider, repository: repository); @override Future> toTest( Mounty input, { - required provider, + required TestProvider provider, covariant OfflineFirstWithTestRepository? repository, }) async => await _$MountyToTest(input, provider: provider, repository: repository); @override Future fromSqlite( Map input, { - required provider, + required SqliteProvider provider, covariant OfflineFirstWithTestRepository? repository, }) async => await _$MountyFromSqlite(input, provider: provider, repository: repository); @override Future> toSqlite( Mounty input, { - required provider, + required SqliteProvider provider, covariant OfflineFirstWithTestRepository? repository, }) async => await _$MountyToSqlite(input, provider: provider, repository: repository); diff --git a/packages/brick_offline_first/test/offline_first/helpers/test_domain.dart b/packages/brick_offline_first/test/offline_first/helpers/test_domain.dart index b078d7f4..75e3989e 100644 --- a/packages/brick_offline_first/test/offline_first/helpers/test_domain.dart +++ b/packages/brick_offline_first/test/offline_first/helpers/test_domain.dart @@ -1,3 +1,5 @@ +// ignore_for_file: prefer_mixin + import 'dart:io'; import 'package:brick_core/core.dart'; @@ -9,7 +11,7 @@ import 'package:brick_sqlite/memory_cache_provider.dart'; import '__mocks__.dart'; class TestProvider extends Provider { - var methodsCalled = []; + List methodsCalled = []; @override final TestModelDictionary modelDictionary; @@ -72,6 +74,7 @@ abstract class TestAdapter implements Adapter /// Associates app models with their [TestAdapter] class TestModelDictionary extends ModelDictionary> { + // ignore: use_super_parameters const TestModelDictionary(Map> mappings) : super(mappings); } diff --git a/packages/brick_offline_first/test/offline_first/offline_first_repository_test.dart b/packages/brick_offline_first/test/offline_first/offline_first_repository_test.dart index ddb9a183..ddc24358 100644 --- a/packages/brick_offline_first/test/offline_first/offline_first_repository_test.dart +++ b/packages/brick_offline_first/test/offline_first/offline_first_repository_test.dart @@ -26,7 +26,7 @@ void main() { test('#applyPolicyToQuery', () async { const policy = OfflineFirstGetPolicy.localOnly; - final query = TestRepository().applyPolicyToQuery(Query(), get: policy); + final query = TestRepository().applyPolicyToQuery(const Query(), get: policy); expect(query?.providerArgs, {'policy': policy.index}); }); diff --git a/packages/brick_offline_first_build/CHANGELOG.md b/packages/brick_offline_first_build/CHANGELOG.md index 39aeabaa..ee2ea182 100644 --- a/packages/brick_offline_first_build/CHANGELOG.md +++ b/packages/brick_offline_first_build/CHANGELOG.md @@ -1,5 +1,10 @@ ## Unreleased +## 3.3.0 + +- Upgrade `brick_core` to `1.3.0` +- Update analysis to modern lints + ## 3.2.1 - (test) remove analysis options override for non-standard library prefixes diff --git a/packages/brick_offline_first_build/lib/src/offline_first_checker.dart b/packages/brick_offline_first_build/lib/src/offline_first_checker.dart index 3cc9d464..a9250d53 100644 --- a/packages/brick_offline_first_build/lib/src/offline_first_checker.dart +++ b/packages/brick_offline_first_build/lib/src/offline_first_checker.dart @@ -4,7 +4,9 @@ import 'package:source_gen/source_gen.dart' show TypeChecker; const _serdesClassChecker = TypeChecker.fromRuntime(OfflineFirstSerdes); +/// class OfflineFirstChecker extends SharedChecker { + /// OfflineFirstChecker(super.targetType); @override diff --git a/packages/brick_offline_first_build/lib/src/offline_first_fields.dart b/packages/brick_offline_first_build/lib/src/offline_first_fields.dart index 65d63116..adbb5c6b 100644 --- a/packages/brick_offline_first_build/lib/src/offline_first_fields.dart +++ b/packages/brick_offline_first_build/lib/src/offline_first_fields.dart @@ -7,10 +7,10 @@ class _OfflineFirstSerdesFinder extends AnnotationFinder { _OfflineFirstSerdesFinder(); @override - OfflineFirst from(element) { + OfflineFirst from(FieldElement element) { final obj = objectForField(element); - if (obj == null) return const OfflineFirst(); + if (obj == null) return OfflineFirst.defaults; final where = obj .getField('where') @@ -30,5 +30,6 @@ class OfflineFirstFields extends FieldsForClass { @override final finder = _OfflineFirstSerdesFinder(); + /// Discover all fields with `@OfflineFirst` OfflineFirstFields(ClassElement element) : super(element: element); } diff --git a/packages/brick_offline_first_build/lib/src/offline_first_generator.dart b/packages/brick_offline_first_build/lib/src/offline_first_generator.dart index 749c42ae..6216a6af 100644 --- a/packages/brick_offline_first_build/lib/src/offline_first_generator.dart +++ b/packages/brick_offline_first_build/lib/src/offline_first_generator.dart @@ -13,6 +13,7 @@ abstract class OfflineFirstGenerator<_ClassAnnotation> @override final String superAdapterName; + /// Output serializing code for all models with the @[_ClassAnnotation] annotation const OfflineFirstGenerator({ String? superAdapterName, String? repositoryName, diff --git a/packages/brick_offline_first_build/lib/src/offline_first_json_generators.dart b/packages/brick_offline_first_build/lib/src/offline_first_json_generators.dart index 922f3844..50156faa 100644 --- a/packages/brick_offline_first_build/lib/src/offline_first_json_generators.dart +++ b/packages/brick_offline_first_build/lib/src/offline_first_json_generators.dart @@ -1,4 +1,6 @@ +import 'package:analyzer/dart/element/element.dart'; import 'package:analyzer/dart/element/nullability_suffix.dart'; +import 'package:analyzer/dart/element/type.dart'; import 'package:brick_build/generators.dart'; import 'package:brick_core/core.dart'; import 'package:brick_core/field_serializable.dart'; @@ -11,10 +13,11 @@ import 'package:brick_offline_first_build/brick_offline_first_build.dart'; /// (e.g. `class OfflineFirstRestSerialize extends RestSerialize with OfflineFirstJsonSerialize`) mixin OfflineFirstJsonSerialize on JsonSerialize { + /// OfflineFirstFields get offlineFirstFields; @override - OfflineFirstChecker checkerForType(type) => OfflineFirstChecker(type); + OfflineFirstChecker checkerForType(DartType type) => OfflineFirstChecker(type); @override List get instanceFieldsAndMethods { @@ -49,7 +52,12 @@ mixin OfflineFirstJsonSerialize checker, { + required bool wrappedInFuture, + required Annotation fieldAnnotation, + }) { final offlineFirstAnnotation = offlineFirstFields.annotationForField(field); if (offlineFirstAnnotation.where != null && offlineFirstAnnotation.where!.length > 1) { @@ -122,13 +130,19 @@ mixin OfflineFirstJsonSerialize on JsonDeserialize { + /// OfflineFirstFields get offlineFirstFields; @override - OfflineFirstChecker checkerForType(type) => OfflineFirstChecker(type); + OfflineFirstChecker checkerForType(DartType type) => OfflineFirstChecker(type); @override - String? coderForField(field, checker, {required wrappedInFuture, required fieldAnnotation}) { + String? coderForField( + FieldElement field, + SharedChecker checker, { + required bool wrappedInFuture, + required Annotation fieldAnnotation, + }) { final offlineFirstAnnotation = offlineFirstFields.annotationForField(field); final fieldValue = serdesValueForField(field, fieldAnnotation.name!, checker: checker); final defaultValue = SerdesGenerator.defaultValueSuffix(fieldAnnotation); @@ -198,8 +212,7 @@ mixin OfflineFirstJsonDeserialize ${SharedChecker.withoutNullability(checker.argType)}.$constructorName(c as $serializableType))$castIterable$defaultValue'; } @@ -215,7 +228,7 @@ mixin OfflineFirstJsonDeserialize OfflineFirstChecker(type); + OfflineFirstChecker checkerForType(DartType type) => OfflineFirstChecker(type); @override - SchemaColumn? schemaColumn(column, {required covariant OfflineFirstChecker checker}) { + SchemaColumn? schemaColumn(Sqlite column, {required covariant OfflineFirstChecker checker}) { if (checker.hasSerdes) { final sqliteSerializerType = checker.superClassTypeArgs[1]; final sqliteChecker = checkerForType(sqliteSerializerType); return SchemaColumn( column.name!, - Migration.fromDartPrimitive(sqliteChecker.asPrimitive), + Column.fromDartPrimitive(sqliteChecker.asPrimitive), nullable: column.nullable, unique: column.unique, ); diff --git a/packages/brick_offline_first_build/lib/src/offline_first_sqlite_generators.dart b/packages/brick_offline_first_build/lib/src/offline_first_sqlite_generators.dart index a3b7528c..f493ca5c 100644 --- a/packages/brick_offline_first_build/lib/src/offline_first_sqlite_generators.dart +++ b/packages/brick_offline_first_build/lib/src/offline_first_sqlite_generators.dart @@ -1,10 +1,15 @@ import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/type.dart'; import 'package:brick_build/generators.dart'; +import 'package:brick_core/src/model.dart'; import 'package:brick_offline_first_build/src/offline_first_checker.dart'; +import 'package:brick_sqlite/src/annotations/sqlite.dart'; import 'package:brick_sqlite_generators/generators.dart'; import 'package:brick_sqlite_generators/sqlite_model_serdes_generator.dart'; +/// class OfflineFirstSqliteSerialize extends SqliteSerialize { + /// OfflineFirstSqliteSerialize( super.element, super.fields, { @@ -12,10 +17,15 @@ class OfflineFirstSqliteSerialize extends SqliteSerialize { }); @override - OfflineFirstChecker checkerForType(type) => OfflineFirstChecker(type); + OfflineFirstChecker checkerForType(DartType type) => OfflineFirstChecker(type); @override - String? coderForField(field, checker, {required wrappedInFuture, required fieldAnnotation}) { + String? coderForField( + FieldElement field, + SharedChecker checker, { + required bool wrappedInFuture, + required Sqlite fieldAnnotation, + }) { final fieldValue = serdesValueForField(field, fieldAnnotation.name!, checker: checker); if (checker.isIterable) { @@ -52,7 +62,7 @@ class OfflineFirstSqliteSerialize extends SqliteSerialize { } @override - String uniqueValueForField(fieldName, {required checker}) { + String uniqueValueForField(String? fieldName, {required SharedChecker checker}) { if ((checker as OfflineFirstChecker).hasSerdes) { return '$fieldName.toSqlite()'; } @@ -61,7 +71,9 @@ class OfflineFirstSqliteSerialize extends SqliteSerialize { } } +/// class OfflineFirstSqliteDeserialize extends SqliteDeserialize { + /// OfflineFirstSqliteDeserialize( super.element, super.fields, { @@ -69,10 +81,15 @@ class OfflineFirstSqliteDeserialize extends SqliteDeserialize { }); @override - OfflineFirstChecker checkerForType(type) => OfflineFirstChecker(type); + OfflineFirstChecker checkerForType(DartType type) => OfflineFirstChecker(type); @override - String? coderForField(field, checker, {required wrappedInFuture, required fieldAnnotation}) { + String? coderForField( + FieldElement field, + SharedChecker checker, { + required bool wrappedInFuture, + required Sqlite fieldAnnotation, + }) { final fieldValue = serdesValueForField(field, fieldAnnotation.name!, checker: checker); // Iterable @@ -91,8 +108,7 @@ class OfflineFirstSqliteDeserialize extends SqliteDeserialize { if (argTypeChecker.hasSerdes) { final doesHaveConstructor = hasConstructor(checker.argType); if (doesHaveConstructor) { - final serializableType = - argTypeChecker.superClassTypeArgs.last.getDisplayString(withNullability: true); + final serializableType = argTypeChecker.superClassTypeArgs.last.getDisplayString(); return ''' jsonDecode($fieldValue).map( (c) => $argType.$constructorName(c as $serializableType) @@ -106,8 +122,7 @@ class OfflineFirstSqliteDeserialize extends SqliteDeserialize { if ((checker as OfflineFirstChecker).hasSerdes) { final doesHaveConstructor = hasConstructor(field.type); if (doesHaveConstructor) { - final serializableType = - checker.superClassTypeArgs.last.getDisplayString(withNullability: true); + final serializableType = checker.superClassTypeArgs.last.getDisplayString(); return '${SharedChecker.withoutNullability(field.type)}.$constructorName($fieldValue as $serializableType)'; } } @@ -121,7 +136,9 @@ class OfflineFirstSqliteDeserialize extends SqliteDeserialize { } } +/// class OfflineFirstSqliteModelSerdesGenerator extends SqliteModelSerdesGenerator { + /// OfflineFirstSqliteModelSerdesGenerator( super.element, super.reader, { diff --git a/packages/brick_offline_first_build/pubspec.yaml b/packages/brick_offline_first_build/pubspec.yaml index dfbe4e40..6ceb84df 100644 --- a/packages/brick_offline_first_build/pubspec.yaml +++ b/packages/brick_offline_first_build/pubspec.yaml @@ -4,7 +4,7 @@ homepage: https://github.com/GetDutchie/brick/tree/main/packages/brick_offline_f issue_tracker: https://github.com/GetDutchie/brick/issues repository: https://github.com/GetDutchie/brick -version: 3.2.1 +version: 3.3.0 environment: sdk: ">=2.18.0 <4.0.0" @@ -12,7 +12,7 @@ environment: dependencies: analyzer: ">=6.0.0 <7.0.0" brick_build: ">=3.0.0 <4.0.0" - brick_core: ^1.1.1 + brick_core: ^1.3.0 brick_json_generators: ">=3.0.0 <4.0.0" brick_offline_first: ">=3.0.0 <4.0.0" brick_sqlite: ">=3.0.0 <4.0.0" @@ -29,10 +29,10 @@ dev_dependencies: path: ../brick_build_test brick_offline_first_with_rest: path: ../brick_offline_first_with_rest - brick_rest: any + brick_rest: brick_rest_generators: path: ../brick_rest_generators - build_verify: ^2.0.0 - source_gen_test: ^1.0.0 - test: ^1.20.1 - lints: ^2.0.1 + build_verify: + lints: + source_gen_test: + test: diff --git a/packages/brick_offline_first_build/test/__helpers__.dart b/packages/brick_offline_first_build/test/__helpers__.dart index 32ec958f..b770890f 100644 --- a/packages/brick_offline_first_build/test/__helpers__.dart +++ b/packages/brick_offline_first_build/test/__helpers__.dart @@ -9,8 +9,8 @@ import 'package:brick_rest_generators/rest_model_serdes_generator.dart'; import 'package:source_gen/source_gen.dart'; import 'package:test/test.dart'; -final _generator = OfflineFirstWithTestGenerator(); -final folder = 'offline_first_generator'; +const _generator = OfflineFirstWithTestGenerator(); +const folder = 'offline_first_generator'; final generateReader = generateLibraryForFolder(folder); Future generateExpectation( @@ -100,9 +100,6 @@ class OfflineFirstWithTestGenerator extends OfflineFirstGenerator[]; - generators.addAll(rest.generators); - generators.addAll(sqlite.generators); - return generators; + return [...rest.generators, ...sqlite.generators]; } } diff --git a/packages/brick_offline_first_build/test/offline_first_generator/test_constructor_arguments.dart b/packages/brick_offline_first_build/test/offline_first_generator/test_constructor_arguments.dart index e04c12c3..eeb7daf2 100644 --- a/packages/brick_offline_first_build/test/offline_first_generator/test_constructor_arguments.dart +++ b/packages/brick_offline_first_build/test/offline_first_generator/test_constructor_arguments.dart @@ -4,7 +4,7 @@ import 'package:brick_offline_first_with_rest/brick_offline_first_with_rest.dart @ConnectOfflineFirstWithRest() class OfflineFirstGeneratorArguments extends OfflineFirstModel {} -final repositoryNameAdapterExpectation = r''' +const repositoryNameAdapterExpectation = r''' // GENERATED CODE DO NOT EDIT part of '../brick.g.dart'; @@ -86,7 +86,7 @@ class OfflineFirstGeneratorArgumentsAdapter } '''; -final superAdapterNameAdapterExpectation = r''' +const superAdapterNameAdapterExpectation = r''' // GENERATED CODE DO NOT EDIT part of '../brick.g.dart'; diff --git a/packages/brick_offline_first_build/test/offline_first_generator/test_custom_offline_first_serdes.dart b/packages/brick_offline_first_build/test/offline_first_generator/test_custom_offline_first_serdes.dart index cf79418c..337435a4 100644 --- a/packages/brick_offline_first_build/test/offline_first_generator/test_custom_offline_first_serdes.dart +++ b/packages/brick_offline_first_build/test/offline_first_generator/test_custom_offline_first_serdes.dart @@ -1,7 +1,7 @@ import 'package:brick_offline_first/brick_offline_first.dart' show OfflineFirstSerdes; import 'package:brick_offline_first_with_rest/brick_offline_first_with_rest.dart'; -final output = r''' +const output = r''' Future _$CustomOfflineFirstSerdesFromTest( Map data, {required TestProvider provider, diff --git a/packages/brick_offline_first_build/test/offline_first_generator/test_default_value.dart b/packages/brick_offline_first_build/test/offline_first_generator/test_default_value.dart index 69a7e5ad..21f6674b 100644 --- a/packages/brick_offline_first_build/test/offline_first_generator/test_default_value.dart +++ b/packages/brick_offline_first_build/test/offline_first_generator/test_default_value.dart @@ -2,7 +2,7 @@ import 'package:brick_offline_first_with_rest/brick_offline_first_with_rest.dart import 'package:brick_rest/brick_rest.dart' show Rest; import 'package:brick_sqlite/brick_sqlite.dart'; -final output = r''' +const output = r''' Future _$DefaultValueFromTest(Map data, {required TestProvider provider, OfflineFirstRepository? repository}) async { diff --git a/packages/brick_offline_first_build/test/offline_first_generator/test_enum_factory_serialize.dart b/packages/brick_offline_first_build/test/offline_first_generator/test_enum_factory_serialize.dart index 34b699b1..df09f6d4 100644 --- a/packages/brick_offline_first_build/test/offline_first_generator/test_enum_factory_serialize.dart +++ b/packages/brick_offline_first_build/test/offline_first_generator/test_enum_factory_serialize.dart @@ -1,6 +1,6 @@ import 'package:brick_offline_first_with_rest/brick_offline_first_with_rest.dart'; -final output = r''' +const output = r''' Future _$EnumFactorySerializeFromTest( Map data, {required TestProvider provider, diff --git a/packages/brick_offline_first_build/test/offline_first_generator/test_futures.dart b/packages/brick_offline_first_build/test/offline_first_generator/test_futures.dart index 6d931c35..2d6a41cb 100644 --- a/packages/brick_offline_first_build/test/offline_first_generator/test_futures.dart +++ b/packages/brick_offline_first_build/test/offline_first_generator/test_futures.dart @@ -1,7 +1,7 @@ import 'package:brick_offline_first/brick_offline_first.dart' show OfflineFirstModel; import 'package:brick_offline_first_with_rest/brick_offline_first_with_rest.dart'; -final output = r''' +const output = r''' // GENERATED CODE DO NOT EDIT part of '../brick.g.dart'; diff --git a/packages/brick_offline_first_build/test/offline_first_generator/test_ignore_field.dart b/packages/brick_offline_first_build/test/offline_first_generator/test_ignore_field.dart index 25d1cf5b..5796be53 100644 --- a/packages/brick_offline_first_build/test/offline_first_generator/test_ignore_field.dart +++ b/packages/brick_offline_first_build/test/offline_first_generator/test_ignore_field.dart @@ -2,7 +2,7 @@ import 'package:brick_offline_first_with_rest/brick_offline_first_with_rest.dart import 'package:brick_rest/brick_rest.dart'; import 'package:brick_sqlite/brick_sqlite.dart'; -final output = r''' +const output = r''' Future _$IgnoreFieldFromTest(Map data, {required TestProvider provider, OfflineFirstRepository? repository}) async { diff --git a/packages/brick_offline_first_build/test/offline_first_generator/test_no_final_no_const.dart b/packages/brick_offline_first_build/test/offline_first_generator/test_no_final_no_const.dart index 530b9e50..50582e32 100644 --- a/packages/brick_offline_first_build/test/offline_first_generator/test_no_final_no_const.dart +++ b/packages/brick_offline_first_build/test/offline_first_generator/test_no_final_no_const.dart @@ -1,6 +1,6 @@ import 'package:brick_offline_first_with_rest/brick_offline_first_with_rest.dart'; -final output = r''' +const output = r''' Future _$NoFinalNoConstFromTest(Map data, {required TestProvider provider, OfflineFirstRepository? repository}) async { @@ -42,9 +42,9 @@ Future> _$NoFinalNoConstToSqlite(NoFinalNoConst instance, @ConnectOfflineFirstWithRest() class NoFinalNoConst { int declaredVar = 5; - var regularVar = true; + bool regularVar = true; int get computedField => _privateVarField; int _privateVarField = 0; - set computedField(value) => _privateVarField = value; + set computedField(dynamic value) => _privateVarField = value; } diff --git a/packages/brick_offline_first_build/test/offline_first_generator/test_nullable_field.dart b/packages/brick_offline_first_build/test/offline_first_generator/test_nullable_field.dart index bffa712e..4dac8108 100644 --- a/packages/brick_offline_first_build/test/offline_first_generator/test_nullable_field.dart +++ b/packages/brick_offline_first_build/test/offline_first_generator/test_nullable_field.dart @@ -2,7 +2,7 @@ import 'package:brick_offline_first_with_rest/brick_offline_first_with_rest.dart import 'package:brick_rest/brick_rest.dart'; import 'package:brick_sqlite/brick_sqlite.dart'; -final output = r''' +const output = r''' Future _$NullableFieldFromTest(Map data, {required TestProvider provider, OfflineFirstRepository? repository}) async { @@ -66,7 +66,7 @@ Future> _$NullableFieldToSqlite(NullableField instance, '''; @ConnectOfflineFirstWithRest( - restConfig: RestSerializable(nullable: false), + restConfig: RestSerializable.defaults, sqliteConfig: SqliteSerializable(nullable: false), ) class NullableField { diff --git a/packages/brick_offline_first_build/test/offline_first_generator/test_offline_first_apply_to_remote_deserialization.dart b/packages/brick_offline_first_build/test/offline_first_generator/test_offline_first_apply_to_remote_deserialization.dart index 977a113f..02749d52 100644 --- a/packages/brick_offline_first_build/test/offline_first_generator/test_offline_first_apply_to_remote_deserialization.dart +++ b/packages/brick_offline_first_build/test/offline_first_generator/test_offline_first_apply_to_remote_deserialization.dart @@ -2,7 +2,7 @@ import 'package:brick_offline_first/brick_offline_first.dart'; import 'package:brick_offline_first_with_rest/brick_offline_first_with_rest.dart'; import 'package:brick_rest/brick_rest.dart'; -final output = r''' +const output = r''' // GENERATED CODE DO NOT EDIT part of '../brick.g.dart'; @@ -11,9 +11,8 @@ Future _$OfflineFirstWhereFromTest(Map data, OfflineFirstRepository? repository}) async { return OfflineFirstWhere( applied: await repository - ?.getAssociation(Query( - where: [Where.exact('id', data['id'])], - providerArgs: {'limit': 1})) + ?.getAssociation( + Query(where: [Where.exact('id', data['id'])], limit: 1)) .then((r) => r?.isNotEmpty ?? false ? r!.first : null), notApplied: data['not_applied'] == null ? null diff --git a/packages/brick_offline_first_build/test/offline_first_generator/test_offline_first_serdes_with_type_argument.dart b/packages/brick_offline_first_build/test/offline_first_generator/test_offline_first_serdes_with_type_argument.dart index ffa499a7..2429b74a 100644 --- a/packages/brick_offline_first_build/test/offline_first_generator/test_offline_first_serdes_with_type_argument.dart +++ b/packages/brick_offline_first_build/test/offline_first_generator/test_offline_first_serdes_with_type_argument.dart @@ -1,7 +1,7 @@ import 'package:brick_offline_first/brick_offline_first.dart'; import 'package:brick_offline_first_with_rest/brick_offline_first_with_rest.dart'; -final output = r""" +const output = r""" // GENERATED CODE DO NOT EDIT part of '../brick.g.dart'; diff --git a/packages/brick_offline_first_build/test/offline_first_generator/test_offline_first_where.dart b/packages/brick_offline_first_build/test/offline_first_generator/test_offline_first_where.dart index 681447d7..e9d5b72d 100644 --- a/packages/brick_offline_first_build/test/offline_first_generator/test_offline_first_where.dart +++ b/packages/brick_offline_first_build/test/offline_first_generator/test_offline_first_where.dart @@ -2,7 +2,7 @@ import 'package:brick_offline_first/brick_offline_first.dart'; import 'package:brick_offline_first_with_rest/brick_offline_first_with_rest.dart'; import 'package:brick_rest/brick_rest.dart'; -final output = r''' +const output = r''' // GENERATED CODE DO NOT EDIT part of '../brick.g.dart'; @@ -10,21 +10,22 @@ Future _$OfflineFirstWhereFromTest(Map data, {required TestProvider provider, OfflineFirstRepository? repository}) async { return OfflineFirstWhere( - assoc: - repository!.getAssociation(Query(where: [Where.exact('id', data['id'])], providerArgs: {'limit': 1})).then( - (r) => r!.first), + assoc: repository! + .getAssociation( + Query(where: [Where.exact('id', data['id'])], limit: 1)) + .then((r) => r!.first), assocs: (data['assocs'] ?? []) .map>((s) => repository.getAssociation(Query(where: [Where.exact('id', s), Where.exact('otherVar', s)])).then( (r) => r!.first)) .toList(), - loadedAssoc: - await repository.getAssociation(Query(where: [Where.exact('id', data['id'])], providerArgs: {'limit': 1})).then( - (r) => r?.isNotEmpty ?? false ? r!.first : null), - loadedAssocs: - (await Future.wait((data['loaded_assocs'] ?? []).map>((s) => repository.getAssociation(Query(where: [Where.exact('id', s)])).then((r) => r?.isNotEmpty ?? false ? r!.first : null)))) - .whereType() - .toList(), + loadedAssoc: await repository + .getAssociation( + Query(where: [Where.exact('id', data['id'])], limit: 1)) + .then((r) => r?.isNotEmpty ?? false ? r!.first : null), + loadedAssocs: (await Future.wait((data['loaded_assocs'] ?? []).map>((s) => repository.getAssociation(Query(where: [Where.exact('id', s)])).then((r) => r?.isNotEmpty ?? false ? r!.first : null)))) + .whereType() + .toList(), multiLookupCustomGenerator: (data['multi_lookup_custom_generator'] ?? []) .map>((s) => repository.getAssociation(Query(where: [Where.exact('id', s), Where.exact('otherVar', s)])).then((r) => r!.first)) .toList()); diff --git a/packages/brick_offline_first_build/test/offline_first_generator/test_one_to_many_association.dart b/packages/brick_offline_first_build/test/offline_first_generator/test_one_to_many_association.dart index b2cad810..f7edbf99 100644 --- a/packages/brick_offline_first_build/test/offline_first_generator/test_one_to_many_association.dart +++ b/packages/brick_offline_first_build/test/offline_first_generator/test_one_to_many_association.dart @@ -22,7 +22,7 @@ class SqliteAssoc extends OfflineFirstModel { int key = -1; } -final output = r''' +const output = r''' // GENERATED CODE DO NOT EDIT part of '../brick.g.dart'; diff --git a/packages/brick_offline_first_build/test/offline_first_generator/test_one_to_one_association.dart b/packages/brick_offline_first_build/test/offline_first_generator/test_one_to_one_association.dart index 8acce3c2..f92f437d 100644 --- a/packages/brick_offline_first_build/test/offline_first_generator/test_one_to_one_association.dart +++ b/packages/brick_offline_first_build/test/offline_first_generator/test_one_to_one_association.dart @@ -10,7 +10,7 @@ class SqliteAssoc extends OfflineFirstModel { int key = -1; } -final output = r''' +const output = r''' Future _$SqliteAssocFromTest(Map data, {required TestProvider provider, OfflineFirstRepository? repository}) async { diff --git a/packages/brick_offline_first_build/test/offline_first_generator/test_only_static_members.dart b/packages/brick_offline_first_build/test/offline_first_generator/test_only_static_members.dart index 736a37e9..240d6e8f 100644 --- a/packages/brick_offline_first_build/test/offline_first_generator/test_only_static_members.dart +++ b/packages/brick_offline_first_build/test/offline_first_generator/test_only_static_members.dart @@ -1,6 +1,8 @@ +// ignore_for_file: avoid_classes_with_only_static_members + import 'package:brick_offline_first_with_rest/brick_offline_first_with_rest.dart'; -final output = r''' +const output = r''' Future _$OnlyStaticMembersFromTest(Map data, {required TestProvider provider, OfflineFirstRepository? repository}) async { @@ -33,7 +35,7 @@ Future> _$OnlyStaticMembersToSqlite( class OnlyStaticMembers { // To ensure static members are not considered for serialization. static const answer = 42; - static final reason = 42; + static const reason = 42; static int get understand => 42; } diff --git a/packages/brick_offline_first_build/test/offline_first_generator/test_primitive_fields.dart b/packages/brick_offline_first_build/test/offline_first_generator/test_primitive_fields.dart index 0fe7667e..402fe712 100644 --- a/packages/brick_offline_first_build/test/offline_first_generator/test_primitive_fields.dart +++ b/packages/brick_offline_first_build/test/offline_first_generator/test_primitive_fields.dart @@ -2,7 +2,7 @@ import 'package:brick_offline_first_with_rest/brick_offline_first_with_rest.dart enum Casing { snake, camel } -final output = r''' +const output = r''' Future _$PrimitiveFieldsFromTest(Map data, {required TestProvider provider, OfflineFirstRepository? repository}) async { diff --git a/packages/brick_offline_first_build/test/offline_first_generator/test_unique_offline_first_serdes.dart b/packages/brick_offline_first_build/test/offline_first_generator/test_unique_offline_first_serdes.dart index 1631144c..8ce7def5 100644 --- a/packages/brick_offline_first_build/test/offline_first_generator/test_unique_offline_first_serdes.dart +++ b/packages/brick_offline_first_build/test/offline_first_generator/test_unique_offline_first_serdes.dart @@ -2,7 +2,7 @@ import 'package:brick_offline_first/brick_offline_first.dart' show OfflineFirstS import 'package:brick_offline_first_with_rest/brick_offline_first_with_rest.dart'; import 'package:brick_sqlite/brick_sqlite.dart'; -final output = r""" +const output = r""" // GENERATED CODE DO NOT EDIT part of '../brick.g.dart'; diff --git a/packages/brick_offline_first_build/test/offline_first_generator/test_unrelated_association.dart b/packages/brick_offline_first_build/test/offline_first_generator/test_unrelated_association.dart index c96ee07e..b336f507 100644 --- a/packages/brick_offline_first_build/test/offline_first_generator/test_unrelated_association.dart +++ b/packages/brick_offline_first_build/test/offline_first_generator/test_unrelated_association.dart @@ -2,7 +2,7 @@ import 'package:brick_offline_first_with_rest/brick_offline_first_with_rest.dart class NonSqliteAssoc {} -final output = r''' +const output = r''' Future _$UnreleatedAssociationFromTest( Map data, {required TestProvider provider, diff --git a/packages/brick_offline_first_build/test/offline_first_generator_test.dart b/packages/brick_offline_first_build/test/offline_first_generator_test.dart index be42eca6..c0a626c7 100644 --- a/packages/brick_offline_first_build/test/offline_first_generator_test.dart +++ b/packages/brick_offline_first_build/test/offline_first_generator_test.dart @@ -27,7 +27,7 @@ void main() { group('OfflineFirstJsonGenerators', () { group('constructor arguments', () { test('repositoryName', () async { - final generator = OfflineFirstWithTestGenerator(repositoryName: 'MyCustom'); + const generator = OfflineFirstWithTestGenerator(repositoryName: 'MyCustom'); await generateAdapterExpectation( 'constructor_arguments', constructor_arguments.repositoryNameAdapterExpectation, @@ -36,7 +36,7 @@ void main() { }); test('superAdapterName', () async { - final generator = OfflineFirstWithTestGenerator(superAdapterName: 'SuperDuper'); + const generator = OfflineFirstWithTestGenerator(superAdapterName: 'SuperDuper'); await generateAdapterExpectation( 'constructor_arguments', constructor_arguments.superAdapterNameAdapterExpectation, diff --git a/packages/brick_offline_first_build/test/offline_first_model_dictionary_generator_test.dart b/packages/brick_offline_first_build/test/offline_first_model_dictionary_generator_test.dart index 3a6b6612..4cf56ba8 100644 --- a/packages/brick_offline_first_build/test/offline_first_model_dictionary_generator_test.dart +++ b/packages/brick_offline_first_build/test/offline_first_model_dictionary_generator_test.dart @@ -5,9 +5,9 @@ void main() { group('OfflineFirstModelDictionaryGenerator', () { group('#generate', () { test('basic', () { - final generated = OfflineFirstModelDictionaryGenerator('Rest') + final generated = const OfflineFirstModelDictionaryGenerator('Rest') .generate({'Person': 'person.dart', 'User': 'path/user.dart'}); - final output = ''' + const output = ''' // GENERATED CODE DO NOT EDIT // ignore: unused_import import 'dart:convert'; diff --git a/packages/brick_offline_first_build/test/offline_first_schema_generator/test_with_association.dart b/packages/brick_offline_first_build/test/offline_first_schema_generator/test_with_association.dart index 3c3fa036..3050dc0b 100644 --- a/packages/brick_offline_first_build/test/offline_first_schema_generator/test_with_association.dart +++ b/packages/brick_offline_first_build/test/offline_first_schema_generator/test_with_association.dart @@ -7,7 +7,7 @@ class SqliteAssoc extends OfflineFirstWithRestModel { final int key = -1; } -final output = ''' +const output = ''' // GENERATED CODE DO NOT EDIT // This file should be version controlled import 'package:brick_sqlite/db.dart'; diff --git a/packages/brick_offline_first_build/test/offline_first_schema_generator/test_with_associations.dart b/packages/brick_offline_first_build/test/offline_first_schema_generator/test_with_associations.dart index d02ae279..43d7c11e 100644 --- a/packages/brick_offline_first_build/test/offline_first_schema_generator/test_with_associations.dart +++ b/packages/brick_offline_first_build/test/offline_first_schema_generator/test_with_associations.dart @@ -7,7 +7,7 @@ class SqliteAssoc extends OfflineFirstWithRestModel { final int key = -1; } -final output = ''' +const output = ''' // GENERATED CODE DO NOT EDIT // This file should be version controlled import 'package:brick_sqlite/db.dart'; diff --git a/packages/brick_offline_first_build/test/offline_first_schema_generator/test_with_serdes.dart b/packages/brick_offline_first_build/test/offline_first_schema_generator/test_with_serdes.dart index f1bd6039..ae7f472a 100644 --- a/packages/brick_offline_first_build/test/offline_first_schema_generator/test_with_serdes.dart +++ b/packages/brick_offline_first_build/test/offline_first_schema_generator/test_with_serdes.dart @@ -60,7 +60,7 @@ class WithSerdes extends OfflineFirstWithRestModel { }); } -final output = ''' +const output = ''' // GENERATED CODE EDIT WITH CAUTION // THIS FILE **WILL NOT** BE REGENERATED // This file should be version controlled and can be manually edited. diff --git a/packages/brick_offline_first_build/test/offline_first_schema_generator_test.dart b/packages/brick_offline_first_build/test/offline_first_schema_generator_test.dart index 897232c8..f1bf0e5b 100644 --- a/packages/brick_offline_first_build/test/offline_first_schema_generator_test.dart +++ b/packages/brick_offline_first_build/test/offline_first_schema_generator_test.dart @@ -12,7 +12,7 @@ import 'offline_first_schema_generator/test_with_serdes.dart' as with_serdes; final generator = OfflineFirstSchemaGenerator(); final generateLibrary = generateLibraryForFolder('offline_first_schema_generator'); -final annotationChecker = TypeChecker.fromRuntime(ConnectOfflineFirstWithRest); +const annotationChecker = TypeChecker.fromRuntime(ConnectOfflineFirstWithRest); Future generateOutputForFile(String fileName) async { final reader = await generateLibrary(fileName); diff --git a/packages/brick_offline_first_with_graphql/CHANGELOG.md b/packages/brick_offline_first_with_graphql/CHANGELOG.md index 8ceb3047..b2dc86b2 100644 --- a/packages/brick_offline_first_with_graphql/CHANGELOG.md +++ b/packages/brick_offline_first_with_graphql/CHANGELOG.md @@ -1,6 +1,8 @@ ## Unreleased - Allow a generic type argument for `OfflineFirstWithGraphqlRepository` +- Update analysis to modern lints +- Upgrade `brick_core` to `1.3.0` ## 3.2.0 diff --git a/packages/brick_offline_first_with_graphql/lib/src/graphql_offline_queue_link.dart b/packages/brick_offline_first_with_graphql/lib/src/graphql_offline_queue_link.dart index f113e40e..1dae72d7 100644 --- a/packages/brick_offline_first_with_graphql/lib/src/graphql_offline_queue_link.dart +++ b/packages/brick_offline_first_with_graphql/lib/src/graphql_offline_queue_link.dart @@ -13,6 +13,7 @@ import 'package:logging/logging.dart'; class GraphqlOfflineQueueLink extends Link { final Logger _logger; + /// final GraphqlRequestSqliteCacheManager requestManager; /// A callback triggered when a request failed, but will be reattempted. @@ -21,6 +22,7 @@ class GraphqlOfflineQueueLink extends Link { /// A callback triggered when a request throws an exception during execution. final void Function(Request request, Object error)? onRequestException; + /// Stores all mutation requests in a SQLite database GraphqlOfflineQueueLink( this.requestManager, { this.onReattempt, diff --git a/packages/brick_offline_first_with_graphql/lib/src/graphql_offline_request_queue.dart b/packages/brick_offline_first_with_graphql/lib/src/graphql_offline_request_queue.dart index 07d9c5c2..5a208fdc 100644 --- a/packages/brick_offline_first_with_graphql/lib/src/graphql_offline_request_queue.dart +++ b/packages/brick_offline_first_with_graphql/lib/src/graphql_offline_request_queue.dart @@ -5,10 +5,12 @@ import 'package:brick_offline_first_with_graphql/brick_offline_first_with_graphq import 'package:gql_exec/gql_exec.dart'; import 'package:gql_link/gql_link.dart'; +/// GraphQL implementation of [OfflineRequestQueue] class GraphqlOfflineRequestQueue extends OfflineRequestQueue { /// The client responsible for resending requests final Link link; + /// GraphQL implementation of [OfflineRequestQueue] GraphqlOfflineRequestQueue({ required this.link, required GraphqlRequestSqliteCacheManager requestManager, diff --git a/packages/brick_offline_first_with_graphql/lib/src/graphql_request_sqlite_cache.dart b/packages/brick_offline_first_with_graphql/lib/src/graphql_request_sqlite_cache.dart index f1adf726..64f541bd 100644 --- a/packages/brick_offline_first_with_graphql/lib/src/graphql_request_sqlite_cache.dart +++ b/packages/brick_offline_first_with_graphql/lib/src/graphql_request_sqlite_cache.dart @@ -7,6 +7,7 @@ import 'package:gql_exec/gql_exec.dart'; /// Serialize and Deserialize a [Request] from SQLite. class GraphqlRequestSqliteCache extends RequestSqliteCache { + /// Serialize and Deserialize a [Request] from SQLite. GraphqlRequestSqliteCache(request) : super( attemptColumn: GRAPHQL_JOBS_ATTEMPTS_COLUMN, @@ -41,7 +42,7 @@ class GraphqlRequestSqliteCache extends RequestSqliteCache { } /// Builds request into a new SQLite-insertable row - /// Only available if [request] was initialized from [fromRequest] + /// Only available if [request] was initialized from [sqliteToRequest] /// /// This is a function to ensure `DateTime.now()` is invoked predictably. @override diff --git a/packages/brick_offline_first_with_graphql/lib/src/graphql_request_sqlite_cache_manager.dart b/packages/brick_offline_first_with_graphql/lib/src/graphql_request_sqlite_cache_manager.dart index 4f51f6be..e2fbdbc5 100644 --- a/packages/brick_offline_first_with_graphql/lib/src/graphql_request_sqlite_cache_manager.dart +++ b/packages/brick_offline_first_with_graphql/lib/src/graphql_request_sqlite_cache_manager.dart @@ -6,7 +6,9 @@ import 'package:brick_offline_first/offline_queue.dart'; import 'package:gql/language.dart' as lang; import 'package:gql_exec/gql_exec.dart'; +/// GraphQL implementation of [RequestSqliteCacheManager] class GraphqlRequestSqliteCacheManager extends RequestSqliteCacheManager { + /// GraphQL implementation of [RequestSqliteCacheManager] GraphqlRequestSqliteCacheManager( super.databaseName, { required super.databaseFactory, @@ -72,7 +74,7 @@ const GRAPHQL_JOBS_PRIMARY_KEY_COLUMN = 'id'; /// json-encoded String const GRAPHQL_JOBS_VARIABLES_COLUMN = 'variables'; -// String +/// String const GRAPHQL_JOBS_TABLE_NAME = 'GraphqlJobs'; /// int; millisecondsSinceEpoch diff --git a/packages/brick_offline_first_with_graphql/lib/src/models/offline_first_with_graphql_model.dart b/packages/brick_offline_first_with_graphql/lib/src/models/offline_first_with_graphql_model.dart index 06411709..002574f7 100644 --- a/packages/brick_offline_first_with_graphql/lib/src/models/offline_first_with_graphql_model.dart +++ b/packages/brick_offline_first_with_graphql/lib/src/models/offline_first_with_graphql_model.dart @@ -1,4 +1,6 @@ import 'package:brick_graphql/brick_graphql.dart' show GraphqlModel; import 'package:brick_offline_first/brick_offline_first.dart'; +/// GraphQL-enabled [OfflineFirstModel] +// ignore: prefer_mixin abstract class OfflineFirstWithGraphqlModel extends OfflineFirstModel with GraphqlModel {} diff --git a/packages/brick_offline_first_with_graphql/lib/src/offline_first_graphql_policy.dart b/packages/brick_offline_first_with_graphql/lib/src/offline_first_graphql_policy.dart index 383da2f6..973cdf43 100644 --- a/packages/brick_offline_first_with_graphql/lib/src/offline_first_graphql_policy.dart +++ b/packages/brick_offline_first_with_graphql/lib/src/offline_first_graphql_policy.dart @@ -1,22 +1,31 @@ import 'package:brick_offline_first/brick_offline_first.dart'; +import 'package:brick_offline_first_with_graphql/src/offline_first_with_graphql_repository.dart'; import 'package:gql_exec/gql_exec.dart'; +/// Converts an `OfflineFirstPolicy` to a context object for later use in the request manager. +/// The request manager may choose to ignore the policy if the request is not a mutation. class OfflineFirstGraphqlPolicy extends ContextEntry { + /// [OfflineFirstWithGraphqlRepository.delete] invocations final OfflineFirstDeletePolicy? delete; @override List get fieldsForEquality => [delete, get, upsert]; + /// [OfflineFirstWithGraphqlRepository.get] invocations final OfflineFirstGetPolicy? get; + /// [OfflineFirstWithGraphqlRepository.upsert] invocations final OfflineFirstUpsertPolicy? upsert; + /// Converts an `OfflineFirstPolicy` to a context object for later use in the request manager. + /// The request manager may choose to ignore the policy if the request is not a mutation. const OfflineFirstGraphqlPolicy({ this.delete, this.get, this.upsert, }); + /// Serialize Map toJson() => { if (delete != null) 'delete': delete?.index, if (get != null) 'get': get?.index, diff --git a/packages/brick_offline_first_with_graphql/lib/src/offline_first_with_graphql_adapter.dart b/packages/brick_offline_first_with_graphql/lib/src/offline_first_with_graphql_adapter.dart index fcc95682..2d30c3bc 100644 --- a/packages/brick_offline_first_with_graphql/lib/src/offline_first_with_graphql_adapter.dart +++ b/packages/brick_offline_first_with_graphql/lib/src/offline_first_with_graphql_adapter.dart @@ -1,11 +1,14 @@ -import 'package:brick_graphql/brick_graphql.dart' show GraphqlProvider, GraphqlAdapter; +import 'package:brick_graphql/brick_graphql.dart' show GraphqlAdapter, GraphqlProvider; import 'package:brick_offline_first/brick_offline_first.dart'; import 'package:brick_offline_first_with_graphql/src/models/offline_first_with_graphql_model.dart'; +import 'package:brick_sqlite/brick_sqlite.dart'; export 'package:brick_graphql/src/runtime_graphql_definition.dart'; /// This adapter fetches first from [SqliteProvider] then hydrates with [GraphqlProvider]. abstract class OfflineFirstWithGraphqlAdapter<_Model extends OfflineFirstWithGraphqlModel> + // ignore: prefer_mixin extends OfflineFirstAdapter<_Model> with GraphqlAdapter<_Model> { + /// This adapter fetches first from [SqliteProvider] then hydrates with [GraphqlProvider]. OfflineFirstWithGraphqlAdapter(); } diff --git a/packages/brick_offline_first_with_graphql/lib/src/offline_first_with_graphql_repository.dart b/packages/brick_offline_first_with_graphql/lib/src/offline_first_with_graphql_repository.dart index 83bbfca4..36b2d164 100644 --- a/packages/brick_offline_first_with_graphql/lib/src/offline_first_with_graphql_repository.dart +++ b/packages/brick_offline_first_with_graphql/lib/src/offline_first_with_graphql_repository.dart @@ -22,9 +22,14 @@ abstract class OfflineFirstWithGraphqlRepository< // ignore: overridden_fields final GraphqlProvider remoteProvider; + /// @protected late final GraphqlOfflineRequestQueue offlineRequestQueue; + /// Ensures the [remoteProvider] is a [GraphqlProvider]. All requests to and + /// from the [remoteProvider] pass through a seperate SQLite queue. If the app + /// is unable to make contact with the [remoteProvider], the queue automatically retries in + /// sequence until it receives a response. OfflineFirstWithGraphqlRepository({ super.autoHydrate, required GraphqlProvider graphqlProvider, @@ -43,7 +48,7 @@ abstract class OfflineFirstWithGraphqlRepository< ); /// As some links may consume [OfflineFirstGraphqlPolicy] from the request's - /// context, this adds the policy to the `providerArgs#context` + /// context, this adds the policy to [Query.providerQueries]. @override Query? applyPolicyToQuery( Query? query, { @@ -51,18 +56,31 @@ abstract class OfflineFirstWithGraphqlRepository< OfflineFirstGetPolicy? get, OfflineFirstUpsertPolicy? upsert, }) { + final queryContext = + (query?.providerQueries[GraphqlProvider] as GraphqlProviderQuery?)?.context; + final argContextMap = query?.providerArgs['context'] as Map?; + final argContext = argContextMap != null + ? Context.fromMap( + Map.from(argContextMap) + .map((key, value) => MapEntry(value.runtimeType, value)), + ) + : null; + final context = (queryContext ?? argContext ?? const Context()).withEntry( + OfflineFirstGraphqlPolicy( + delete: delete, + get: get, + upsert: upsert, + ), + ); + return query?.copyWith( - providerArgs: { - ...query.providerArgs, - 'context': { - 'OfflineFirstGraphqlPolicy': OfflineFirstGraphqlPolicy( - delete: delete, - get: get, - upsert: upsert, - ), - ...?query.providerArgs['context'] as Map?, - }, - }, + forProviders: [ + ...query.forProviders, + GraphqlProviderQuery( + operation: (query.providerQueries[GraphqlProvider] as GraphqlProviderQuery?)?.operation, + context: context, + ), + ], ); } @@ -156,7 +174,7 @@ abstract class OfflineFirstWithGraphqlRepository< OfflineFirstGetPolicy policy = OfflineFirstGetPolicy.awaitRemoteWhenNoneExist, Query? query, }) { - query ??= Query(); + query ??= const Query(); if (subscriptions[TModel]?[query] != null) { return subscriptions[TModel]![query]!.stream as Stream>; } diff --git a/packages/brick_offline_first_with_graphql/pubspec.yaml b/packages/brick_offline_first_with_graphql/pubspec.yaml index d027fa3a..573c017b 100644 --- a/packages/brick_offline_first_with_graphql/pubspec.yaml +++ b/packages/brick_offline_first_with_graphql/pubspec.yaml @@ -5,13 +5,13 @@ homepage: https://github.com/GetDutchie/brick/tree/main/packages/brick_offline_f issue_tracker: https://github.com/GetDutchie/brick/issues repository: https://github.com/GetDutchie/brick -version: 3.2.0 +version: 3.3.0 environment: sdk: ">=2.18.0 <4.0.0" dependencies: - brick_core: ^1.1.1 + brick_core: ^1.3.0 brick_graphql: ">=3.0.0 <4.0.0" brick_offline_first: ">=3.0.0 <4.0.0" brick_sqlite: ">=3.0.0 <4.0.0" @@ -23,7 +23,7 @@ dependencies: sqflite_common: ">=2.0.0 <3.0.0" dev_dependencies: - lints: ^2.0.1 - mockito: ^5.0.0 - test: ^1.16.5 - sqflite_common_ffi: ^2.0.0 + lints: + mockito: + sqflite_common_ffi: + test: diff --git a/packages/brick_offline_first_with_graphql/test/__helpers__.dart b/packages/brick_offline_first_with_graphql/test/__helpers__.dart index c7ec7b0a..d2815685 100644 --- a/packages/brick_offline_first_with_graphql/test/__helpers__.dart +++ b/packages/brick_offline_first_with_graphql/test/__helpers__.dart @@ -15,6 +15,7 @@ Link stubGraphqlLink( final link = MockLink(); if (wrapInTopLevelKeyAndArray) { + // ignore: parameter_assignments response = { 'result': [response], }; @@ -28,7 +29,6 @@ Link stubGraphqlLink( data: response, response: {'body': jsonEncode(response)}, errors: errors?.map((e) => GraphQLError(message: e)).toList().cast(), - context: const Context(), ), ]), ); diff --git a/packages/brick_offline_first_with_graphql/test/graphql_offline_request_queue_test.dart b/packages/brick_offline_first_with_graphql/test/graphql_offline_request_queue_test.dart index 1d871ade..36eca764 100644 --- a/packages/brick_offline_first_with_graphql/test/graphql_offline_request_queue_test.dart +++ b/packages/brick_offline_first_with_graphql/test/graphql_offline_request_queue_test.dart @@ -19,8 +19,7 @@ void main() { final queue = GraphqlOfflineRequestQueue( link: offlineClient, requestManager: requestManager, - ); - queue.start(); + )..start(); expect(queue.isRunning, isTrue); queue.stop(); }); @@ -29,8 +28,7 @@ void main() { final queue = GraphqlOfflineRequestQueue( link: offlineClient, requestManager: requestManager, - ); - queue.start(); + )..start(); expect(queue.isRunning, isTrue); queue.stop(); expect(queue.isRunning, isFalse); diff --git a/packages/brick_offline_first_with_graphql/test/graphql_offline_sqlite_cache_manager_test.dart b/packages/brick_offline_first_with_graphql/test/graphql_offline_sqlite_cache_manager_test.dart index d4cb92fe..55807b15 100644 --- a/packages/brick_offline_first_with_graphql/test/graphql_offline_sqlite_cache_manager_test.dart +++ b/packages/brick_offline_first_with_graphql/test/graphql_offline_sqlite_cache_manager_test.dart @@ -37,7 +37,7 @@ void main() { final requestManager = GraphqlRequestSqliteCacheManager( inMemoryDatabasePath, databaseFactory: databaseFactoryFfi, - processingInterval: const Duration(seconds: 0), + processingInterval: Duration.zero, ); setUpAll(() async { @@ -58,7 +58,7 @@ void main() { inMemoryDatabasePath, databaseFactory: databaseFactoryFfi, serialProcessing: false, - processingInterval: const Duration(seconds: 0), + processingInterval: Duration.zero, ); final client = GraphqlOfflineQueueLink(requestManager) .concat(stubGraphqlLink({}, errors: ['Unable to connect'])); @@ -100,7 +100,7 @@ void main() { final request = await requestManager.prepareNextRequestToProcess(); expect(request?.operation.operationName, 'UpsertPerson'); - final asCacheItem = GraphqlRequestSqliteCache(request!); + final asCacheItem = GraphqlRequestSqliteCache(request); await asCacheItem.insertOrUpdate(await requestManager.getDb()); // Do not retry request if the row is locked and serial processing is active final req = await requestManager.prepareNextRequestToProcess(); @@ -143,7 +143,7 @@ void main() { await asCacheItem.insertOrUpdate(await requestManager.getDb()); await asCacheItem.unlock(await requestManager.getDb()); - final requests = await requestManager.unprocessedRequests(onlyLocked: false); + final requests = await requestManager.unprocessedRequests(); expect(requests, hasLength(1)); final lockedRequests = await requestManager.unprocessedRequests(onlyLocked: true); @@ -184,7 +184,7 @@ void main() { ); expect(await requestManager.unprocessedRequests(onlyLocked: true), hasLength(1)); - expect(await requestManager.unprocessedRequests(onlyLocked: false), hasLength(1)); + expect(await requestManager.unprocessedRequests(), hasLength(1)); expect(await requestManager.prepareNextRequestToProcess(), isNull); diff --git a/packages/brick_offline_first_with_graphql/test/test_domain/__mocks__.dart b/packages/brick_offline_first_with_graphql/test/test_domain/__mocks__.dart index 6a207f88..ee8d689f 100644 --- a/packages/brick_offline_first_with_graphql/test/test_domain/__mocks__.dart +++ b/packages/brick_offline_first_with_graphql/test/test_domain/__mocks__.dart @@ -12,8 +12,8 @@ part 'horse_adapter.dart'; part 'mounty.dart'; part 'mounty_adapter.dart'; -/// The exact same as [DemoModel], except this class is tracked by the Memory Cache Provider -/// while [DemoModel] is not. +/// The exact same as [Mounty], except this class is tracked by the Memory Cache Provider +/// while [Mounty] is not. class MemoryDemoModel extends Mounty { MemoryDemoModel(String name) : super(name: name); } diff --git a/packages/brick_offline_first_with_graphql/test/test_domain/horse_adapter.dart b/packages/brick_offline_first_with_graphql/test/test_domain/horse_adapter.dart index 619536d5..31a127cc 100644 --- a/packages/brick_offline_first_with_graphql/test/test_domain/horse_adapter.dart +++ b/packages/brick_offline_first_with_graphql/test/test_domain/horse_adapter.dart @@ -69,7 +69,7 @@ Future> _$HorseToSqlite( class HorseOperationTransformer extends GraphqlQueryOperationTransformer { @override - GraphqlOperation get delete => GraphqlOperation( + GraphqlOperation get delete => const GraphqlOperation( document: r'''mutation DeleteDemoModel($input: DemoModelInput!) { deleteDemoModel(input: $input) {} }''', @@ -104,7 +104,7 @@ class HorseOperationTransformer extends GraphqlQueryOperationTransformer { } @override - GraphqlOperation get upsert => GraphqlOperation( + GraphqlOperation get upsert => const GraphqlOperation( document: r'''mutation UpsertDemoModels($input: DemoModelInput) { upsertDemoModel(input: $input) {} }''', @@ -123,15 +123,11 @@ class HorseAdapter extends OfflineFirstWithGraphqlAdapter { @override final Map fieldsToGraphqlRuntimeDefinition = { 'primaryKey': const RuntimeGraphqlDefinition( - association: false, documentNodeName: '_brick_id', - iterable: false, type: int, ), 'name': const RuntimeGraphqlDefinition( - association: false, documentNodeName: 'name', - iterable: false, type: String, ), 'mounties': const RuntimeGraphqlDefinition( @@ -145,15 +141,11 @@ class HorseAdapter extends OfflineFirstWithGraphqlAdapter { @override final Map fieldsToSqliteColumns = { 'primaryKey': const RuntimeSqliteColumnDefinition( - association: false, columnName: '_brick_id', - iterable: false, type: int, ), 'name': const RuntimeSqliteColumnDefinition( - association: false, columnName: 'name', - iterable: false, type: String, ), 'mounties': const RuntimeSqliteColumnDefinition( @@ -169,7 +161,11 @@ class HorseAdapter extends OfflineFirstWithGraphqlAdapter { @override final String tableName = 'Horse'; @override - Future afterSave(instance, {required provider, repository}) async { + Future afterSave( + Horse instance, { + required SqliteProvider provider, + ModelRepository? repository, + }) async { if (instance.primaryKey != null) { await Future.wait( instance.mounties.map((s) async { @@ -186,28 +182,28 @@ class HorseAdapter extends OfflineFirstWithGraphqlAdapter { @override Future fromGraphql( Map input, { - required provider, + required GraphqlProvider provider, covariant OfflineFirstWithGraphqlRepository? repository, }) async => await _$HorseFromGraphql(input, provider: provider, repository: repository); @override Future> toGraphql( Horse input, { - required provider, + required GraphqlProvider provider, covariant OfflineFirstWithGraphqlRepository? repository, }) async => await _$HorseToGraphql(input, provider: provider, repository: repository); @override Future fromSqlite( Map input, { - required provider, + required SqliteProvider provider, covariant OfflineFirstWithGraphqlRepository? repository, }) async => await _$HorseFromSqlite(input, provider: provider, repository: repository); @override Future> toSqlite( Horse input, { - required provider, + required SqliteProvider provider, covariant OfflineFirstWithGraphqlRepository? repository, }) async => await _$HorseToSqlite(input, provider: provider, repository: repository); diff --git a/packages/brick_offline_first_with_graphql/test/test_domain/mounty_adapter.dart b/packages/brick_offline_first_with_graphql/test/test_domain/mounty_adapter.dart index fa0e68e6..dec9ac82 100644 --- a/packages/brick_offline_first_with_graphql/test/test_domain/mounty_adapter.dart +++ b/packages/brick_offline_first_with_graphql/test/test_domain/mounty_adapter.dart @@ -35,7 +35,7 @@ Future> _$MountyToSqlite( class MountyOperationTransformer extends GraphqlQueryOperationTransformer { @override - GraphqlOperation get delete => GraphqlOperation( + GraphqlOperation get delete => const GraphqlOperation( document: r'''mutation DeleteDemoModel($input: DemoModelInput!) { deleteDemoModel(input: $input) {} }''', @@ -70,7 +70,7 @@ class MountyOperationTransformer extends GraphqlQueryOperationTransformer { } @override - GraphqlOperation get upsert => GraphqlOperation( + GraphqlOperation get upsert => const GraphqlOperation( document: r'''mutation UpsertDemoModels($input: DemoModelInput) { upsertDemoModel(input: $input) {} }''', @@ -89,15 +89,11 @@ class MountyAdapter extends OfflineFirstWithGraphqlAdapter { @override final Map fieldsToSqliteColumns = { 'primaryKey': const RuntimeSqliteColumnDefinition( - association: false, columnName: '_brick_id', - iterable: false, type: int, ), 'name': const RuntimeSqliteColumnDefinition( - association: false, columnName: 'name', - iterable: false, type: String, ), }; @@ -105,9 +101,7 @@ class MountyAdapter extends OfflineFirstWithGraphqlAdapter { @override final Map fieldsToGraphqlRuntimeDefinition = { 'name': const RuntimeGraphqlDefinition( - association: false, documentNodeName: 'name', - iterable: false, type: String, ), }; @@ -120,28 +114,28 @@ class MountyAdapter extends OfflineFirstWithGraphqlAdapter { @override Future fromGraphql( Map input, { - required provider, + required GraphqlProvider provider, covariant OfflineFirstWithGraphqlRepository? repository, }) async => await _$MountyFromGraphql(input, provider: provider, repository: repository); @override Future> toGraphql( Mounty input, { - required provider, + required GraphqlProvider provider, covariant OfflineFirstWithGraphqlRepository? repository, }) async => await _$MountyToGraphql(input, provider: provider, repository: repository); @override Future fromSqlite( Map input, { - required provider, + required SqliteProvider provider, covariant OfflineFirstWithGraphqlRepository? repository, }) async => await _$MountyFromSqlite(input, provider: provider, repository: repository); @override Future> toSqlite( Mounty input, { - required provider, + required SqliteProvider provider, covariant OfflineFirstWithGraphqlRepository? repository, }) async => await _$MountyToSqlite(input, provider: provider, repository: repository); diff --git a/packages/brick_offline_first_with_graphql_build/CHANGELOG.md b/packages/brick_offline_first_with_graphql_build/CHANGELOG.md index 4b50212c..da0ae94e 100644 --- a/packages/brick_offline_first_with_graphql_build/CHANGELOG.md +++ b/packages/brick_offline_first_with_graphql_build/CHANGELOG.md @@ -2,6 +2,7 @@ - (test) remove analysis options override for non-standard library prefixes - Apply minimum constraint on `brick_offline_first_build` to `3.2.0` +- Update analysis to modern lints ## 3.3.0 diff --git a/packages/brick_offline_first_with_graphql_build/lib/builder.dart b/packages/brick_offline_first_with_graphql_build/lib/builder.dart index 4bb42679..1be609cf 100644 --- a/packages/brick_offline_first_with_graphql_build/lib/builder.dart +++ b/packages/brick_offline_first_with_graphql_build/lib/builder.dart @@ -7,23 +7,26 @@ import 'package:build/build.dart'; final _schemaGenerator = OfflineFirstSchemaGenerator(); +/// class OfflineFirstMigrationBuilder extends NewMigrationBuilder { @override final schemaGenerator = _schemaGenerator; } +/// class OfflineFirstSchemaBuilder extends SchemaBuilder { @override final schemaGenerator = _schemaGenerator; } -final offlineFirstGenerator = const OfflineFirstWithGraphqlGenerator( +/// +const offlineFirstGenerator = OfflineFirstWithGraphqlGenerator( superAdapterName: 'OfflineFirstWithGraphql', repositoryName: 'OfflineFirstWithGraphql', ); /// These functions act as builder factories used by `build.yaml` -Builder offlineFirstAggregateBuilder(options) => AggregateBuilder( +Builder offlineFirstAggregateBuilder(_) => const AggregateBuilder( requiredImports: [ "import 'package:brick_offline_first/brick_offline_first.dart';", "import 'package:brick_sqlite/db.dart';", @@ -32,9 +35,13 @@ Builder offlineFirstAggregateBuilder(options) => AggregateBuilder( "import 'package:brick_offline_first_with_graphql/brick_offline_first_with_graphql.dart' show OfflineFirstWithGraphqlRepository, OfflineFirstWithGraphqlAdapter;", ], ); -Builder offlineFirstAdaptersBuilder(options) => + +/// +Builder offlineFirstAdaptersBuilder(_) => AdapterBuilder(offlineFirstGenerator); -Builder offlineFirstModelDictionaryBuilder(options) => + +/// +Builder offlineFirstModelDictionaryBuilder(_) => ModelDictionaryBuilder( const OfflineFirstModelDictionaryGenerator('Graphql'), expectedImportRemovals: [ @@ -42,5 +49,9 @@ Builder offlineFirstModelDictionaryBuilder(options) => "import 'package:brick_offline_first/brick_offline_first.dart';", ], ); -Builder offlineFirstNewMigrationBuilder(options) => OfflineFirstMigrationBuilder(); -Builder offlineFirstSchemaBuilder(options) => OfflineFirstSchemaBuilder(); + +/// +Builder offlineFirstNewMigrationBuilder(_) => OfflineFirstMigrationBuilder(); + +/// +Builder offlineFirstSchemaBuilder(_) => OfflineFirstSchemaBuilder(); diff --git a/packages/brick_offline_first_with_graphql_build/lib/src/offline_first_graphql_generators.dart b/packages/brick_offline_first_with_graphql_build/lib/src/offline_first_graphql_generators.dart index f80687c1..a2a7b2cb 100644 --- a/packages/brick_offline_first_with_graphql_build/lib/src/offline_first_graphql_generators.dart +++ b/packages/brick_offline_first_with_graphql_build/lib/src/offline_first_graphql_generators.dart @@ -50,7 +50,9 @@ class _OfflineFirstGraphqlDeserialize extends GraphqlDeserialize }) : offlineFirstFields = OfflineFirstFields(element); } +/// Produces code for `@ConnectOfflineFirstWithGraphQL` class OfflineFirstGraphqlModelSerdesGenerator extends GraphqlModelSerdesGenerator { + /// Produces code for `@ConnectOfflineFirstWithGraphQL` OfflineFirstGraphqlModelSerdesGenerator( super.element, super.reader, { diff --git a/packages/brick_offline_first_with_graphql_build/lib/src/offline_first_with_graphql_generator.dart b/packages/brick_offline_first_with_graphql_build/lib/src/offline_first_with_graphql_generator.dart index 763ac03b..e2886bef 100644 --- a/packages/brick_offline_first_with_graphql_build/lib/src/offline_first_with_graphql_generator.dart +++ b/packages/brick_offline_first_with_graphql_build/lib/src/offline_first_with_graphql_generator.dart @@ -5,8 +5,10 @@ import 'package:brick_offline_first_with_graphql/brick_offline_first_with_graphq import 'package:brick_offline_first_with_graphql_build/src/offline_first_graphql_generators.dart'; import 'package:source_gen/source_gen.dart'; +/// Produces code for `@ConnectOfflineFirstWithGraphQL` class OfflineFirstWithGraphqlGenerator extends OfflineFirstGenerator { + /// Produces code for `@ConnectOfflineFirstWithGraphQL` const OfflineFirstWithGraphqlGenerator({ super.repositoryName, super.superAdapterName, @@ -22,9 +24,6 @@ class OfflineFirstWithGraphqlGenerator ); final sqlite = OfflineFirstSqliteModelSerdesGenerator(element, annotation, repositoryName: repositoryName); - final generators = []; - generators.addAll(rest.generators); - generators.addAll(sqlite.generators); - return generators; + return [...rest.generators, ...sqlite.generators]; } } diff --git a/packages/brick_offline_first_with_graphql_build/pubspec.yaml b/packages/brick_offline_first_with_graphql_build/pubspec.yaml index 73a432c2..27956e43 100644 --- a/packages/brick_offline_first_with_graphql_build/pubspec.yaml +++ b/packages/brick_offline_first_with_graphql_build/pubspec.yaml @@ -27,10 +27,10 @@ dependencies: source_gen: ">=1.2.2 <2.0.0" dev_dependencies: - brick_core: ^1.1.1 - build_verify: ^2.0.0 - lints: ^2.0.1 - source_gen_test: ^1.0.0 - test: ^1.20.1 brick_build_test: path: ../brick_build_test + brick_core: + build_verify: + lints: + source_gen_test: + test: diff --git a/packages/brick_offline_first_with_graphql_build/test/offline_first_generator/test_custom_serdes.dart b/packages/brick_offline_first_with_graphql_build/test/offline_first_generator/test_custom_serdes.dart index 92f14ddb..1f409586 100644 --- a/packages/brick_offline_first_with_graphql_build/test/offline_first_generator/test_custom_serdes.dart +++ b/packages/brick_offline_first_with_graphql_build/test/offline_first_generator/test_custom_serdes.dart @@ -2,7 +2,7 @@ import 'package:brick_graphql/brick_graphql.dart'; import 'package:brick_offline_first_with_graphql/brick_offline_first_with_graphql.dart'; import 'package:brick_sqlite/brick_sqlite.dart'; -final output = r''' +const output = r''' Future _$CustomSerdesFromGraphql(Map data, {required GraphqlProvider provider, OfflineFirstRepository? repository}) async { diff --git a/packages/brick_offline_first_with_graphql_build/test/offline_first_generator/test_graphql_config_field_rename.dart b/packages/brick_offline_first_with_graphql_build/test/offline_first_generator/test_graphql_config_field_rename.dart index c62c1615..8317e1ce 100644 --- a/packages/brick_offline_first_with_graphql_build/test/offline_first_generator/test_graphql_config_field_rename.dart +++ b/packages/brick_offline_first_with_graphql_build/test/offline_first_generator/test_graphql_config_field_rename.dart @@ -3,7 +3,7 @@ import 'package:brick_graphql/brick_graphql.dart' show GraphqlSerializable; import 'package:brick_offline_first/brick_offline_first.dart'; import 'package:brick_offline_first_with_graphql/brick_offline_first_with_graphql.dart'; -final output = r''' +const output = r''' Future _$GraphqlConfigNoRenameFromGraphql( Map data, {required GraphqlProvider provider, @@ -125,6 +125,7 @@ Future> _$GraphqlConfigPascalRenameToSqlite( '''; @ConnectOfflineFirstWithGraphql( + // ignore: use_named_constants graphqlConfig: GraphqlSerializable(fieldRename: FieldRename.none), ) class GraphqlConfigNoRename extends OfflineFirstModel { diff --git a/packages/brick_offline_first_with_graphql_build/test/offline_first_generator/test_graphql_config_query_operation_transformer.dart b/packages/brick_offline_first_with_graphql_build/test/offline_first_generator/test_graphql_config_query_operation_transformer.dart index 5fedef61..3a1b9014 100644 --- a/packages/brick_offline_first_with_graphql_build/test/offline_first_generator/test_graphql_config_query_operation_transformer.dart +++ b/packages/brick_offline_first_with_graphql_build/test/offline_first_generator/test_graphql_config_query_operation_transformer.dart @@ -1,7 +1,7 @@ import 'package:brick_graphql/brick_graphql.dart'; import 'package:brick_offline_first_with_graphql/brick_offline_first_with_graphql.dart'; -final output = r''' +const output = r''' // GENERATED CODE DO NOT EDIT part of '../brick.g.dart'; @@ -106,7 +106,7 @@ class QueryOperationTransformerExampleAdapter class QueryOperationTransformerExampleTransformer extends GraphqlQueryOperationTransformer { @override - GraphqlOperation get get => GraphqlOperation( + GraphqlOperation get get => const GraphqlOperation( document: ''' query { getAll() diff --git a/packages/brick_offline_first_with_graphql_build/test/offline_first_generator/test_offline_first_where_no_multiple_keys.dart b/packages/brick_offline_first_with_graphql_build/test/offline_first_generator/test_offline_first_where_no_multiple_keys.dart index bb079be0..28a7230d 100644 --- a/packages/brick_offline_first_with_graphql_build/test/offline_first_generator/test_offline_first_where_no_multiple_keys.dart +++ b/packages/brick_offline_first_with_graphql_build/test/offline_first_generator/test_offline_first_where_no_multiple_keys.dart @@ -3,7 +3,7 @@ import 'package:brick_offline_first/brick_offline_first.dart'; import 'package:brick_offline_first_with_graphql/brick_offline_first_with_graphql.dart'; @ConnectOfflineFirstWithGraphql( - graphqlConfig: GraphqlSerializable(), + graphqlConfig: GraphqlSerializable.defaults, ) class GraphqlConfigEndpoint extends OfflineFirstModel { @OfflineFirst(where: {'otherField': "data['value']", 'id': "data['id']"}) diff --git a/packages/brick_offline_first_with_graphql_build/test/offline_first_generator/test_offline_first_where_no_nested_values.dart b/packages/brick_offline_first_with_graphql_build/test/offline_first_generator/test_offline_first_where_no_nested_values.dart index 0317d0de..577fa105 100644 --- a/packages/brick_offline_first_with_graphql_build/test/offline_first_generator/test_offline_first_where_no_nested_values.dart +++ b/packages/brick_offline_first_with_graphql_build/test/offline_first_generator/test_offline_first_where_no_nested_values.dart @@ -3,7 +3,7 @@ import 'package:brick_offline_first/brick_offline_first.dart'; import 'package:brick_offline_first_with_graphql/brick_offline_first_with_graphql.dart'; @ConnectOfflineFirstWithGraphql( - graphqlConfig: GraphqlSerializable(), + graphqlConfig: GraphqlSerializable.defaults, ) class GraphqlConfigEndpoint extends OfflineFirstModel { @OfflineFirst(where: {'otherField': "data['id']['value']"}) diff --git a/packages/brick_offline_first_with_graphql_build/test/offline_first_generator/test_offline_first_where_rename.dart b/packages/brick_offline_first_with_graphql_build/test/offline_first_generator/test_offline_first_where_rename.dart index 8da6625a..d37295d7 100644 --- a/packages/brick_offline_first_with_graphql_build/test/offline_first_generator/test_offline_first_where_rename.dart +++ b/packages/brick_offline_first_with_graphql_build/test/offline_first_generator/test_offline_first_where_rename.dart @@ -1,8 +1,8 @@ -import 'package:brick_graphql/brick_graphql.dart' show GraphqlSerializable, Graphql; +import 'package:brick_graphql/brick_graphql.dart' show Graphql, GraphqlSerializable; import 'package:brick_offline_first/brick_offline_first.dart'; import 'package:brick_offline_first_with_graphql/brick_offline_first_with_graphql.dart'; -final output = r''' +const output = r''' // GENERATED CODE DO NOT EDIT part of '../brick.g.dart'; @@ -12,9 +12,8 @@ Future _$GraphqlConfigEndpointFromGraphql( OfflineFirstRepository? repository}) async { return GraphqlConfigEndpoint( someField: await repository! - .getAssociation(Query( - where: [Where.exact('name', data['name'])], - providerArgs: {'limit': 1})) + .getAssociation( + Query(where: [Where.exact('name', data['name'])], limit: 1)) .then((r) => r!.first)); } @@ -120,7 +119,7 @@ class GraphqlConfigEndpointAdapter '''; @ConnectOfflineFirstWithGraphql( - graphqlConfig: GraphqlSerializable(), + graphqlConfig: GraphqlSerializable.defaults, ) class GraphqlConfigEndpoint extends OfflineFirstModel { @OfflineFirst(where: {'name': "data['name']"}) diff --git a/packages/brick_offline_first_with_graphql_build/test/offline_first_generator/test_specify_field_name.dart b/packages/brick_offline_first_with_graphql_build/test/offline_first_generator/test_specify_field_name.dart index 0a7cf517..449114b1 100644 --- a/packages/brick_offline_first_with_graphql_build/test/offline_first_generator/test_specify_field_name.dart +++ b/packages/brick_offline_first_with_graphql_build/test/offline_first_generator/test_specify_field_name.dart @@ -2,7 +2,7 @@ import 'package:brick_graphql/brick_graphql.dart'; import 'package:brick_offline_first_with_graphql/brick_offline_first_with_graphql.dart'; import 'package:brick_sqlite/brick_sqlite.dart'; -final output = r''' +const output = r''' Future _$SpecifyFieldNameFromGraphql( Map data, {required GraphqlProvider provider, diff --git a/packages/brick_offline_first_with_graphql_build/test/offline_first_generator_test.dart b/packages/brick_offline_first_with_graphql_build/test/offline_first_generator_test.dart index 6d2c68bc..b4d63c28 100644 --- a/packages/brick_offline_first_with_graphql_build/test/offline_first_generator_test.dart +++ b/packages/brick_offline_first_with_graphql_build/test/offline_first_generator_test.dart @@ -11,8 +11,8 @@ import 'offline_first_generator/test_graphql_config_query_operation_transformer. import 'offline_first_generator/test_offline_first_where_rename.dart' as offline_first_where_rename; import 'offline_first_generator/test_specify_field_name.dart' as specify_field_name; -final _generator = OfflineFirstWithGraphqlGenerator(); -final folder = 'offline_first_generator'; +const _generator = OfflineFirstWithGraphqlGenerator(); +const folder = 'offline_first_generator'; final generateReader = generateLibraryForFolder(folder); void main() { diff --git a/packages/brick_offline_first_with_rest/CHANGELOG.md b/packages/brick_offline_first_with_rest/CHANGELOG.md index 9a51f39e..b0084486 100644 --- a/packages/brick_offline_first_with_rest/CHANGELOG.md +++ b/packages/brick_offline_first_with_rest/CHANGELOG.md @@ -1,6 +1,11 @@ ## Unreleased +## 3.3.0 + - Allow a generic type argument for `OfflineFirstWithRestRepository` +- Upgrade `brick_core` to `1.3.0` +- Update analysis to modern lints +- `OfflineFirstException` has been moved to the `brick_offline_first` package ## 3.2.0 diff --git a/packages/brick_offline_first_with_rest/lib/rest_to_offline_first_converter.dart b/packages/brick_offline_first_with_rest/lib/rest_to_offline_first_converter.dart index 046bb805..3560ca7c 100644 --- a/packages/brick_offline_first_with_rest/lib/rest_to_offline_first_converter.dart +++ b/packages/brick_offline_first_with_rest/lib/rest_to_offline_first_converter.dart @@ -1,8 +1,10 @@ import 'dart:convert'; import 'dart:io'; +import 'package:brick_offline_first/brick_offline_first.dart'; import 'package:dart_style/dart_style.dart' as dart_style; import 'package:http/http.dart'; +import 'package:meta/meta.dart'; final _formatter = dart_style.DartFormatter(); @@ -26,9 +28,15 @@ class RestToOfflineFirstConverter { late Client _client; /// Only set client when testing - set client(value) => _client = value; + @visibleForTesting + set client(Client value) => _client = value; + + /// Client get client => _client; + /// Convert a JSON API payload into an [OfflineFirstModel], output via [generate] or [saveToFile]. + /// + /// This will not map associations or non-primitive types. RestToOfflineFirstConverter({ required this.endpoint, this.headers, @@ -67,8 +75,7 @@ class RestToOfflineFirstConverter { /// Produce instance fields String generateFields(Map fields) { - final keys = fields.keys.toList(); - keys.sort(); + final keys = fields.keys.toList()..sort(); return keys.fold>([], (acc, key) { final valueType = fields[key].runtimeType.toString(); return acc..add(' final $valueType ${toCamelCase(key)};'); @@ -77,8 +84,7 @@ class RestToOfflineFirstConverter { /// Produce fields to be invoked in the default constructor String generateConstructorFields(Map fields) { - final keys = fields.keys.toList(); - keys.sort(); + final keys = fields.keys.toList()..sort(); return keys.fold>([], (acc, key) { return acc..add(' this.${toCamelCase(key)}'); }).join(',\n'); diff --git a/packages/brick_offline_first_with_rest/lib/src/models/offline_first_with_rest_model.dart b/packages/brick_offline_first_with_rest/lib/src/models/offline_first_with_rest_model.dart index c8aff4a1..6985a6c3 100644 --- a/packages/brick_offline_first_with_rest/lib/src/models/offline_first_with_rest_model.dart +++ b/packages/brick_offline_first_with_rest/lib/src/models/offline_first_with_rest_model.dart @@ -1,4 +1,6 @@ import 'package:brick_offline_first/brick_offline_first.dart' show OfflineFirstModel; -import 'package:brick_rest/brick_rest.dart' show RestModel; +import 'package:brick_rest/brick_rest.dart'; +/// An offline-first enabled model for use with the [RestProvider] +// ignore: prefer_mixin abstract class OfflineFirstWithRestModel extends OfflineFirstModel with RestModel {} diff --git a/packages/brick_offline_first_with_rest/lib/src/offline_first_exception.dart b/packages/brick_offline_first_with_rest/lib/src/offline_first_exception.dart deleted file mode 100644 index e252087c..00000000 --- a/packages/brick_offline_first_with_rest/lib/src/offline_first_exception.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:brick_rest/brick_rest.dart' show RestException; - -class OfflineFirstException implements Exception { - /// The producing error from either [RestProvider] or [SqliteProvider]. - final Exception originalError; - - OfflineFirstException(this.originalError); - - /// If [originalError] was produced by [RestProvider]. - bool get fromRest => originalError is RestException; - - String get message => originalError.toString(); - - int? get restErrorCode => fromRest ? (originalError as RestException).response.statusCode : null; - - /// Forward errors from a [RestException] response. `null` is returned - /// if [originalError] was not [fromRest]. - Map? get restErrors { - if (!fromRest) return null; - - return (originalError as RestException).errors; - } - - @override - String toString() => message; -} diff --git a/packages/brick_offline_first_with_rest/lib/src/offline_first_with_rest_adapter.dart b/packages/brick_offline_first_with_rest/lib/src/offline_first_with_rest_adapter.dart index 161395de..115c89c8 100644 --- a/packages/brick_offline_first_with_rest/lib/src/offline_first_with_rest_adapter.dart +++ b/packages/brick_offline_first_with_rest/lib/src/offline_first_with_rest_adapter.dart @@ -1,10 +1,12 @@ import 'package:brick_offline_first/brick_offline_first.dart'; import 'package:brick_offline_first_with_rest/src/models/offline_first_with_rest_model.dart'; +import 'package:brick_rest/brick_rest.dart' show RestAdapter, RestProvider; +import 'package:brick_sqlite/brick_sqlite.dart'; -import 'package:brick_rest/brick_rest.dart' show RestProvider, RestAdapter; - -/// This adapter fetches first from [SqliteProvider] then hydrates with [RestProvider]. +/// This adapter holds logic necessary to work with [SqliteProvider] and [RestProvider]. abstract class OfflineFirstWithRestAdapter<_Model extends OfflineFirstWithRestModel> + // ignore: prefer_mixin extends OfflineFirstAdapter<_Model> with RestAdapter<_Model> { + /// This adapter holds logic necessary to work with [SqliteProvider] and [RestProvider]. OfflineFirstWithRestAdapter(); } diff --git a/packages/brick_offline_first_with_rest/lib/src/offline_first_with_rest_exception.dart b/packages/brick_offline_first_with_rest/lib/src/offline_first_with_rest_exception.dart index f0be99ce..510f4ec6 100644 --- a/packages/brick_offline_first_with_rest/lib/src/offline_first_with_rest_exception.dart +++ b/packages/brick_offline_first_with_rest/lib/src/offline_first_with_rest_exception.dart @@ -1,12 +1,17 @@ import 'package:brick_offline_first/brick_offline_first.dart'; -import 'package:brick_rest/brick_rest.dart' show RestException; +import 'package:brick_rest/brick_rest.dart' show RestException, RestProvider; +/// Forwarded exception thrown from a [RestException]. The implementation may choose +/// to ignore this exception if the latest REST data is not important. class OfflineFirstWithRestException extends OfflineFirstException { + /// Forwarded exception thrown from a [RestException]. The implementation may choose + /// to ignore this exception if the latest REST data is not important. OfflineFirstWithRestException(super.originalError); /// If [originalError] was produced by [RestProvider]. bool get fromRest => originalError is RestException; + /// int? get restErrorCode => fromRest ? (originalError as RestException).response.statusCode : null; /// Forward errors from a [RestException] response. `null` is returned diff --git a/packages/brick_offline_first_with_rest/lib/src/offline_first_with_rest_repository.dart b/packages/brick_offline_first_with_rest/lib/src/offline_first_with_rest_repository.dart index 78fd672c..f82746f2 100644 --- a/packages/brick_offline_first_with_rest/lib/src/offline_first_with_rest_repository.dart +++ b/packages/brick_offline_first_with_rest/lib/src/offline_first_with_rest_repository.dart @@ -5,7 +5,7 @@ import 'package:brick_offline_first/offline_queue.dart'; import 'package:brick_offline_first_with_rest/src/models/offline_first_with_rest_model.dart'; import 'package:brick_offline_first_with_rest/src/offline_queue/rest_offline_queue_client.dart'; import 'package:brick_offline_first_with_rest/src/offline_queue/rest_offline_request_queue.dart'; -import 'package:brick_rest/brick_rest.dart' show RestProvider, RestException; +import 'package:brick_rest/brick_rest.dart'; import 'package:http/http.dart' as http; import 'package:meta/meta.dart'; @@ -22,9 +22,14 @@ abstract class OfflineFirstWithRestRepository requestManager; /// If the response returned from the client is one of these error codes, the request @@ -39,10 +40,11 @@ class RestOfflineQueueClient extends http.BaseClient { final Logger _logger; /// Describes the type of policy that came from the request, stringified - /// from the [OfflineFirstPolicy] enum. The property will be removed before + /// from the `OfflineFirstPolicy` enum. The property will be removed before /// forwarding the request to [_inner]. static const policyHeader = 'X-Brick-OfflineFirstPolicy'; + /// Stores all requests in a SQLite database RestOfflineQueueClient( this._inner, this.requestManager, { diff --git a/packages/brick_offline_first_with_rest/lib/src/offline_queue/rest_offline_request_queue.dart b/packages/brick_offline_first_with_rest/lib/src/offline_queue/rest_offline_request_queue.dart index d7643f30..45f16e54 100644 --- a/packages/brick_offline_first_with_rest/lib/src/offline_queue/rest_offline_request_queue.dart +++ b/packages/brick_offline_first_with_rest/lib/src/offline_queue/rest_offline_request_queue.dart @@ -4,10 +4,12 @@ import 'package:brick_offline_first/offline_queue.dart'; import 'package:brick_offline_first_with_rest/src/offline_queue/rest_offline_queue_client.dart'; import 'package:http/http.dart' as http; +/// REST implementation of [OfflineRequestQueue] class RestOfflineRequestQueue extends OfflineRequestQueue { /// The client responsible for resending requests final RestOfflineQueueClient client; + /// REST implementation of [OfflineRequestQueue] RestOfflineRequestQueue({ required this.client, }) : super( diff --git a/packages/brick_offline_first_with_rest/lib/src/offline_queue/rest_request_sqlite_cache.dart b/packages/brick_offline_first_with_rest/lib/src/offline_queue/rest_request_sqlite_cache.dart index 8cd1ad30..06eb4497 100644 --- a/packages/brick_offline_first_with_rest/lib/src/offline_queue/rest_request_sqlite_cache.dart +++ b/packages/brick_offline_first_with_rest/lib/src/offline_queue/rest_request_sqlite_cache.dart @@ -4,9 +4,12 @@ import 'package:brick_offline_first/offline_queue.dart'; import 'package:brick_offline_first_with_rest/src/offline_queue/rest_request_sqlite_cache_manager.dart'; import 'package:http/http.dart' as http; +/// REST implementation of [RequestSqliteCache] class RestRequestSqliteCache extends RequestSqliteCache { + /// bool get requestIsPush => ['POST', 'PUT', 'DELETE', 'PATCH'].contains(request.method); + /// REST implementation of [RequestSqliteCache] RestRequestSqliteCache(http.Request request) : super( attemptColumn: HTTP_JOBS_ATTEMPTS_COLUMN, diff --git a/packages/brick_offline_first_with_rest/lib/src/offline_queue/rest_request_sqlite_cache_manager.dart b/packages/brick_offline_first_with_rest/lib/src/offline_queue/rest_request_sqlite_cache_manager.dart index 72c99433..15589882 100644 --- a/packages/brick_offline_first_with_rest/lib/src/offline_queue/rest_request_sqlite_cache_manager.dart +++ b/packages/brick_offline_first_with_rest/lib/src/offline_queue/rest_request_sqlite_cache_manager.dart @@ -5,7 +5,9 @@ import 'dart:convert'; import 'package:brick_offline_first/offline_queue.dart'; import 'package:http/http.dart' as http; +/// REST implementation of [RequestSqliteCacheManager] class RestRequestSqliteCacheManager extends RequestSqliteCacheManager { + /// REST implementation of [RequestSqliteCacheManager] RestRequestSqliteCacheManager( super.databaseName, { required super.databaseFactory, @@ -96,6 +98,7 @@ const HTTP_JOBS_PRIMARY_KEY_COLUMN = 'id'; /// String const HTTP_JOBS_REQUEST_METHOD_COLUMN = 'request_method'; +/// String const HTTP_JOBS_TABLE_NAME = 'HttpJobs'; /// int; millisecondsSinceEpoch diff --git a/packages/brick_offline_first_with_rest/lib/testing.dart b/packages/brick_offline_first_with_rest/lib/testing.dart index e641be05..e4f5972a 100644 --- a/packages/brick_offline_first_with_rest/lib/testing.dart +++ b/packages/brick_offline_first_with_rest/lib/testing.dart @@ -1,13 +1,16 @@ import 'dart:io'; import 'package:brick_offline_first_with_rest/brick_offline_first_with_rest.dart'; +import 'package:brick_rest/brick_rest.dart'; import 'package:collection/collection.dart'; import 'package:http/http.dart' as http; import 'package:http/testing.dart'; import 'package:path/path.dart' as p; +// ignore: public_member_api_docs enum StubHttpMethod { get, post, put, delete, any } +/// The expected response from a request. class StubOfflineFirstRestResponse { /// The path where the [response] should be returned. Avoid leading slashes. /// @@ -23,6 +26,7 @@ class StubOfflineFirstRestResponse { /// The text returned from the HTTP request. final String response; + /// static String get currentDirectory { // `flutter test` and `dart test` resolve with different values // https://github.com/flutter/flutter/issues/20907 @@ -38,12 +42,14 @@ class StubOfflineFirstRestResponse { return p.join(directory, 'test'); } + /// The expected response from a request. StubOfflineFirstRestResponse( this.response, { required this.endpoint, StubHttpMethod? method, }) : method = method ?? StubHttpMethod.any; + /// factory StubOfflineFirstRestResponse.fromFile( String filePath, { required String endpoint, @@ -108,6 +114,10 @@ class StubOfflineFirstWithRest { return http.Response('endpoint ${req.method} ${req.url} is not stubbed', 422); }); + /// Generate mocks for an [OfflineFirstWithRestModel]. Instantiation automatically stubs REST responses. + /// + /// For convenience, your data structure only needs to be defined once. Include a sample API + /// response in a sibling `/api/` directory (`api` can be changed by overwriting `apiResponse`). StubOfflineFirstWithRest({ required this.baseEndpoint, required this.responses, @@ -120,7 +130,7 @@ class StubOfflineFirstWithRest { /// } /// ``` /// - /// Responses will be returned on all HTTP methods. The [filePath] is + /// Responses will be returned on all HTTP methods. The [endpointsAndFilePaths] is /// **relative to the top-level /test directory**. factory StubOfflineFirstWithRest.fromFiles( String baseEndpoint, diff --git a/packages/brick_offline_first_with_rest/pubspec.yaml b/packages/brick_offline_first_with_rest/pubspec.yaml index d4ae3b24..59f592c1 100644 --- a/packages/brick_offline_first_with_rest/pubspec.yaml +++ b/packages/brick_offline_first_with_rest/pubspec.yaml @@ -5,13 +5,13 @@ homepage: https://github.com/GetDutchie/brick/tree/main/packages/brick_offline_f issue_tracker: https://github.com/GetDutchie/brick/issues repository: https://github.com/GetDutchie/brick -version: 3.2.0 +version: 3.3.0 environment: sdk: ">=2.18.0 <4.0.0" dependencies: - brick_core: ^1.1.1 + brick_core: ^1.3.0 brick_offline_first: ">=3.0.0 <4.0.0" brick_rest: ">=3.0.3 <4.0.0" brick_sqlite: ">=3.0.0 <4.0.0" @@ -24,7 +24,7 @@ dependencies: sqflite_common: ">=2.0.0 <3.0.0" dev_dependencies: - lints: ^2.0.1 - mockito: ^5.0.0 - test: ^1.16.5 - sqflite_common_ffi: ^2.0.0 + lints: + mockito: + sqflite_common_ffi: + test: diff --git a/packages/brick_offline_first_with_rest/test/helpers/__mocks__.dart b/packages/brick_offline_first_with_rest/test/helpers/__mocks__.dart index b721f833..10176dd7 100644 --- a/packages/brick_offline_first_with_rest/test/helpers/__mocks__.dart +++ b/packages/brick_offline_first_with_rest/test/helpers/__mocks__.dart @@ -12,8 +12,8 @@ part 'horse_adapter.dart'; part 'mounty.dart'; part 'mounty_adapter.dart'; -/// The exact same as [DemoModel], except this class is tracked by the Memory Cache Provider -/// while [DemoModel] is not. +/// The exact same as [Mounty], except this class is tracked by the Memory Cache Provider +/// while [Mounty] is not. class MemoryDemoModel extends Mounty { MemoryDemoModel(String name) : super(name: name); } @@ -46,7 +46,8 @@ class DemoModelMigration extends Migration { } class TestRepository extends OfflineFirstWithRestRepository { - static TestRepository? _singleton; + static late TestRepository? _singleton; + factory TestRepository() => _singleton!; TestRepository._( RestProvider restProvider, @@ -61,7 +62,6 @@ class TestRepository extends OfflineFirstWithRestRepository { databaseFactory: databaseFactoryFfi, ), ); - factory TestRepository() => _singleton!; factory TestRepository.withProviders(RestProvider restProvider, SqliteProvider sqliteProvider) => TestRepository._(restProvider, sqliteProvider); diff --git a/packages/brick_offline_first_with_rest/test/helpers/horse_adapter.dart b/packages/brick_offline_first_with_rest/test/helpers/horse_adapter.dart index d8c19e80..b94b112e 100644 --- a/packages/brick_offline_first_with_rest/test/helpers/horse_adapter.dart +++ b/packages/brick_offline_first_with_rest/test/helpers/horse_adapter.dart @@ -72,15 +72,11 @@ class HorseAdapter extends OfflineFirstWithRestAdapter { @override final Map fieldsToSqliteColumns = { 'primaryKey': const RuntimeSqliteColumnDefinition( - association: false, columnName: '_brick_id', - iterable: false, type: int, ), 'name': const RuntimeSqliteColumnDefinition( - association: false, columnName: 'name', - iterable: false, type: String, ), 'mounties': const RuntimeSqliteColumnDefinition( @@ -96,7 +92,11 @@ class HorseAdapter extends OfflineFirstWithRestAdapter { @override final String tableName = 'Horse'; @override - Future afterSave(instance, {required provider, repository}) async { + Future afterSave( + Horse instance, { + required SqliteProvider provider, + ModelRepository? repository, + }) async { if (instance.primaryKey != null) { await Future.wait( instance.mounties.map((s) async { @@ -113,28 +113,28 @@ class HorseAdapter extends OfflineFirstWithRestAdapter { @override Future fromRest( Map input, { - required provider, + required RestProvider provider, covariant OfflineFirstWithRestRepository? repository, }) async => await _$HorseFromRest(input, provider: provider, repository: repository); @override Future> toRest( Horse input, { - required provider, + required RestProvider provider, covariant OfflineFirstWithRestRepository? repository, }) async => await _$HorseToRest(input, provider: provider, repository: repository); @override Future fromSqlite( Map input, { - required provider, + required SqliteProvider provider, covariant OfflineFirstWithRestRepository? repository, }) async => await _$HorseFromSqlite(input, provider: provider, repository: repository); @override Future> toSqlite( Horse input, { - required provider, + required SqliteProvider provider, covariant OfflineFirstWithRestRepository? repository, }) async => await _$HorseToSqlite(input, provider: provider, repository: repository); diff --git a/packages/brick_offline_first_with_rest/test/helpers/mounty.dart b/packages/brick_offline_first_with_rest/test/helpers/mounty.dart index c34c6022..ea4dc796 100644 --- a/packages/brick_offline_first_with_rest/test/helpers/mounty.dart +++ b/packages/brick_offline_first_with_rest/test/helpers/mounty.dart @@ -1,3 +1,5 @@ +// ignore_for_file: avoid_field_initializers_in_const_classes + part of '__mocks__.dart'; class MountyRequestTransformer extends RestRequestTransformer { diff --git a/packages/brick_offline_first_with_rest/test/helpers/mounty_adapter.dart b/packages/brick_offline_first_with_rest/test/helpers/mounty_adapter.dart index acdecfee..57d6f611 100644 --- a/packages/brick_offline_first_with_rest/test/helpers/mounty_adapter.dart +++ b/packages/brick_offline_first_with_rest/test/helpers/mounty_adapter.dart @@ -43,15 +43,11 @@ class MountyAdapter extends OfflineFirstWithRestAdapter { @override final Map fieldsToSqliteColumns = { 'primaryKey': const RuntimeSqliteColumnDefinition( - association: false, columnName: '_brick_id', - iterable: false, type: int, ), 'name': const RuntimeSqliteColumnDefinition( - association: false, columnName: 'name', - iterable: false, type: String, ), }; @@ -64,28 +60,28 @@ class MountyAdapter extends OfflineFirstWithRestAdapter { @override Future fromRest( Map input, { - required provider, + required RestProvider provider, covariant OfflineFirstWithRestRepository? repository, }) async => await _$MountyFromRest(input, provider: provider, repository: repository); @override Future> toRest( Mounty input, { - required provider, + required RestProvider provider, covariant OfflineFirstWithRestRepository? repository, }) async => await _$MountyToRest(input, provider: provider, repository: repository); @override Future fromSqlite( Map input, { - required provider, + required SqliteProvider provider, covariant OfflineFirstWithRestRepository? repository, }) async => await _$MountyFromSqlite(input, provider: provider, repository: repository); @override Future> toSqlite( Mounty input, { - required provider, + required SqliteProvider provider, covariant OfflineFirstWithRestRepository? repository, }) async => await _$MountyToSqlite(input, provider: provider, repository: repository); diff --git a/packages/brick_offline_first_with_rest/test/offline_queue/rest_offline_queue_client_test.dart b/packages/brick_offline_first_with_rest/test/offline_queue/rest_offline_queue_client_test.dart index 1c99791d..bca42c40 100644 --- a/packages/brick_offline_first_with_rest/test/offline_queue/rest_offline_queue_client_test.dart +++ b/packages/brick_offline_first_with_rest/test/offline_queue/rest_offline_queue_client_test.dart @@ -216,7 +216,7 @@ void main() { }); test('onReattempt is not triggered for non-reattemptable status code', () async { - bool callbackTriggered = false; + var callbackTriggered = false; final inner = stubResult(statusCode: 404); final client = RestOfflineQueueClient( @@ -238,7 +238,7 @@ void main() { Object? capturedException; final inner = MockClient((req) async { - throw SocketException('test error'); + throw const SocketException('test error'); }); final client = RestOfflineQueueClient( @@ -257,11 +257,11 @@ void main() { expect(capturedRequest?.method, equals('POST')); expect(capturedRequest?.url, equals(uri)); expect(capturedException, isA()); - expect((capturedException as SocketException).message, equals('test error')); + expect((capturedException! as SocketException).message, equals('test error')); }); test('onRequestException is not triggered for successful request', () async { - bool callbackTriggered = false; + var callbackTriggered = false; final inner = stubResult(statusCode: 200); final client = RestOfflineQueueClient( diff --git a/packages/brick_offline_first_with_rest/test/offline_queue/rest_offline_requeust_queue_test.dart b/packages/brick_offline_first_with_rest/test/offline_queue/rest_offline_requeust_queue_test.dart index d43f582b..a939ea88 100644 --- a/packages/brick_offline_first_with_rest/test/offline_queue/rest_offline_requeust_queue_test.dart +++ b/packages/brick_offline_first_with_rest/test/offline_queue/rest_offline_requeust_queue_test.dart @@ -17,15 +17,13 @@ void main() { group('RestOfflineRequestQueue', () { test('#start', () { - final queue = RestOfflineRequestQueue(client: offlineClient); - queue.start(); + final queue = RestOfflineRequestQueue(client: offlineClient)..start(); expect(queue.isRunning, isTrue); queue.stop(); }); test('#stop', () { - final queue = RestOfflineRequestQueue(client: offlineClient); - queue.start(); + final queue = RestOfflineRequestQueue(client: offlineClient)..start(); expect(queue.isRunning, isTrue); queue.stop(); expect(queue.isRunning, isFalse); diff --git a/packages/brick_offline_first_with_rest/test/offline_queue/rest_request_sqlite_cache_manager_test.dart b/packages/brick_offline_first_with_rest/test/offline_queue/rest_request_sqlite_cache_manager_test.dart index 99a8e0ab..268485d4 100644 --- a/packages/brick_offline_first_with_rest/test/offline_queue/rest_request_sqlite_cache_manager_test.dart +++ b/packages/brick_offline_first_with_rest/test/offline_queue/rest_request_sqlite_cache_manager_test.dart @@ -14,7 +14,7 @@ void main() { final requestManager = RestRequestSqliteCacheManager( inMemoryDatabasePath, databaseFactory: databaseFactoryFfi, - processingInterval: const Duration(seconds: 0), + processingInterval: Duration.zero, ); setUpAll(() async { @@ -36,7 +36,7 @@ void main() { inMemoryDatabasePath, databaseFactory: databaseFactoryFfi, serialProcessing: false, - processingInterval: const Duration(seconds: 0), + processingInterval: Duration.zero, ); final client = RestOfflineQueueClient(inner, requestManager); @@ -106,7 +106,7 @@ void main() { await asCacheItem.insertOrUpdate(await requestManager.getDb()); await asCacheItem.unlock(await requestManager.getDb()); - final requests = await requestManager.unprocessedRequests(onlyLocked: false); + final requests = await requestManager.unprocessedRequests(); expect(requests, hasLength(1)); final lockedRequests = await requestManager.unprocessedRequests(onlyLocked: true); @@ -140,7 +140,7 @@ void main() { ); expect(await requestManager.unprocessedRequests(onlyLocked: true), hasLength(1)); - expect(await requestManager.unprocessedRequests(onlyLocked: false), hasLength(1)); + expect(await requestManager.unprocessedRequests(), hasLength(1)); expect(await requestManager.prepareNextRequestToProcess(), isNull); diff --git a/packages/brick_offline_first_with_rest/test/rest_to_offline_first_converter_test.dart b/packages/brick_offline_first_with_rest/test/rest_to_offline_first_converter_test.dart index 3efaa2c3..913bba69 100644 --- a/packages/brick_offline_first_with_rest/test/rest_to_offline_first_converter_test.dart +++ b/packages/brick_offline_first_with_rest/test/rest_to_offline_first_converter_test.dart @@ -13,16 +13,16 @@ void main() { group('RestToOfflineFirstConverter', () { group('#getRestPayload', () { test('with top-level array', () async { - final converter = RestToOfflineFirstConverter(endpoint: 'http://0.0.0.0:3000/people'); - converter.client = _generateResponse('[{"name": "Thomas"}]'); + final converter = RestToOfflineFirstConverter(endpoint: 'http://0.0.0.0:3000/people') + ..client = _generateResponse('[{"name": "Thomas"}]'); final result = await converter.getRestPayload(); expect(result, {'name': 'Thomas'}); }); test('with top-level map', () async { - final converter = RestToOfflineFirstConverter(endpoint: 'http://0.0.0.0:3000/person'); - converter.client = _generateResponse('[{"name": "Thomas"}]'); + final converter = RestToOfflineFirstConverter(endpoint: 'http://0.0.0.0:3000/person') + ..client = _generateResponse('[{"name": "Thomas"}]'); final result = await converter.getRestPayload(); expect(result, {'name': 'Thomas'}); @@ -32,8 +32,7 @@ void main() { final converter = RestToOfflineFirstConverter( endpoint: 'http://0.0.0.0:3000/person', topLevelKey: 'person', - ); - converter.client = _generateResponse('{ "person": { "name": "Thomas"} }'); + )..client = _generateResponse('{ "person": { "name": "Thomas"} }'); final result = await converter.getRestPayload(); expect(result, {'name': 'Thomas'}); @@ -97,8 +96,8 @@ class People extends OfflineFirstModel { }); test('from rest', () async { - final converter = RestToOfflineFirstConverter(endpoint: 'http://0.0.0.0:3000/people'); - converter.client = _generateResponse('[{"name": "Thomas"}]'); + final converter = RestToOfflineFirstConverter(endpoint: 'http://0.0.0.0:3000/people') + ..client = _generateResponse('[{"name": "Thomas"}]'); final output = await converter.generate(); expect(output, expectedOutput); @@ -108,8 +107,7 @@ class People extends OfflineFirstModel { final converter = RestToOfflineFirstConverter( endpoint: 'http://0.0.0.0:3000/people', topLevelKey: 'people', - ); - converter.client = _generateResponse('{"people": [{"name": "Thomas"}]}'); + )..client = _generateResponse('{"people": [{"name": "Thomas"}]}'); final output = await converter.generate(); expect(output, contains("topLevelKey: 'people',")); diff --git a/packages/brick_offline_first_with_rest_build/CHANGELOG.md b/packages/brick_offline_first_with_rest_build/CHANGELOG.md index 1adbf887..15d87df8 100644 --- a/packages/brick_offline_first_with_rest_build/CHANGELOG.md +++ b/packages/brick_offline_first_with_rest_build/CHANGELOG.md @@ -1,10 +1,10 @@ ## Unreleased -- (test) remove analysis options override for non-standard library prefixes -- Apply minimum constraint on `brick_offline_first_build` to `3.2.0` - ## 3.2.0 +- (test) remove analysis options override for non-standard library prefixes +- Apply minimum constraint on `brick_offline_first_build` to `3.2.0` +- Upgrade `brick_core` to `1.3.0` - Apply standardized lints - Update `analyzer` constraints to `>=6.0.0 <7.0.0` - Format CHANGELOG.md diff --git a/packages/brick_offline_first_with_rest_build/lib/builder.dart b/packages/brick_offline_first_with_rest_build/lib/builder.dart index 90dbf9c2..d187fcee 100644 --- a/packages/brick_offline_first_with_rest_build/lib/builder.dart +++ b/packages/brick_offline_first_with_rest_build/lib/builder.dart @@ -7,23 +7,26 @@ import 'package:build/build.dart'; final _schemaGenerator = OfflineFirstSchemaGenerator(); +/// class OfflineFirstMigrationBuilder extends NewMigrationBuilder { @override final schemaGenerator = _schemaGenerator; } +/// class OfflineFirstSchemaBuilder extends SchemaBuilder { @override final schemaGenerator = _schemaGenerator; } -final offlineFirstGenerator = const OfflineFirstWithRestGenerator( +/// +const offlineFirstGenerator = OfflineFirstWithRestGenerator( superAdapterName: 'OfflineFirstWithRest', repositoryName: 'OfflineFirstWithRest', ); /// These functions act as builder factories used by `build.yaml` -Builder offlineFirstAggregateBuilder(options) => AggregateBuilder( +Builder offlineFirstAggregateBuilder(_) => const AggregateBuilder( requiredImports: [ "import 'package:brick_offline_first_abstract/annotations.dart';", "import 'package:brick_offline_first/brick_offline_first.dart';", @@ -31,9 +34,13 @@ Builder offlineFirstAggregateBuilder(options) => AggregateBuilder( "import 'package:brick_sqlite/db.dart';", ], ); -Builder offlineFirstAdaptersBuilder(options) => + +/// +Builder offlineFirstAdaptersBuilder(_) => AdapterBuilder(offlineFirstGenerator); -Builder offlineFirstModelDictionaryBuilder(options) => + +/// +Builder offlineFirstModelDictionaryBuilder(_) => ModelDictionaryBuilder( const OfflineFirstModelDictionaryGenerator('Rest'), expectedImportRemovals: [ @@ -43,5 +50,9 @@ Builder offlineFirstModelDictionaryBuilder(options) => 'import "package:brick_offline_first/brick_offline_first.dart";', ], ); -Builder offlineFirstNewMigrationBuilder(options) => OfflineFirstMigrationBuilder(); -Builder offlineFirstSchemaBuilder(options) => OfflineFirstSchemaBuilder(); + +/// +Builder offlineFirstNewMigrationBuilder(_) => OfflineFirstMigrationBuilder(); + +/// +Builder offlineFirstSchemaBuilder(_) => OfflineFirstSchemaBuilder(); diff --git a/packages/brick_offline_first_with_rest_build/lib/src/offline_first_rest_generators.dart b/packages/brick_offline_first_with_rest_build/lib/src/offline_first_rest_generators.dart index 7fc054eb..4df1fb6c 100644 --- a/packages/brick_offline_first_with_rest_build/lib/src/offline_first_rest_generators.dart +++ b/packages/brick_offline_first_with_rest_build/lib/src/offline_first_rest_generators.dart @@ -29,7 +29,9 @@ class _OfflineFirstRestDeserialize extends RestDeserialize }) : offlineFirstFields = OfflineFirstFields(element); } +/// class OfflineFirstRestModelSerdesGenerator extends RestModelSerdesGenerator { + /// OfflineFirstRestModelSerdesGenerator( super.element, super.reader, { diff --git a/packages/brick_offline_first_with_rest_build/lib/src/offline_first_with_rest_generator.dart b/packages/brick_offline_first_with_rest_build/lib/src/offline_first_with_rest_generator.dart index 4a7c87fc..1a654077 100644 --- a/packages/brick_offline_first_with_rest_build/lib/src/offline_first_with_rest_generator.dart +++ b/packages/brick_offline_first_with_rest_build/lib/src/offline_first_with_rest_generator.dart @@ -5,7 +5,9 @@ import 'package:brick_offline_first_with_rest/brick_offline_first_with_rest.dart import 'package:brick_offline_first_with_rest_build/src/offline_first_rest_generators.dart'; import 'package:source_gen/source_gen.dart'; +/// class OfflineFirstWithRestGenerator extends OfflineFirstGenerator { + /// const OfflineFirstWithRestGenerator({ super.repositoryName, super.superAdapterName, @@ -18,9 +20,6 @@ class OfflineFirstWithRestGenerator extends OfflineFirstGenerator[]; - generators.addAll(rest.generators); - generators.addAll(sqlite.generators); - return generators; + return [...rest.generators, ...sqlite.generators]; } } diff --git a/packages/brick_offline_first_with_rest_build/pubspec.yaml b/packages/brick_offline_first_with_rest_build/pubspec.yaml index 786bd8f9..2109a14c 100644 --- a/packages/brick_offline_first_with_rest_build/pubspec.yaml +++ b/packages/brick_offline_first_with_rest_build/pubspec.yaml @@ -27,9 +27,10 @@ dependencies: source_gen: ">=1.2.2 <2.0.0" dev_dependencies: - build_verify: ^2.0.0 - source_gen_test: ^1.0.0 - test: ^1.20.1 - lints: ^2.0.1 brick_build_test: path: ../brick_build_test + brick_core: + build_verify: + lints: + source_gen_test: + test: diff --git a/packages/brick_offline_first_with_rest_build/test/offline_first_generator/test_custom_serdes.dart b/packages/brick_offline_first_with_rest_build/test/offline_first_generator/test_custom_serdes.dart index 753cbe87..cce47b4f 100644 --- a/packages/brick_offline_first_with_rest_build/test/offline_first_generator/test_custom_serdes.dart +++ b/packages/brick_offline_first_with_rest_build/test/offline_first_generator/test_custom_serdes.dart @@ -2,7 +2,7 @@ import 'package:brick_offline_first_with_rest/brick_offline_first_with_rest.dart import 'package:brick_rest/brick_rest.dart' show Rest; import 'package:brick_sqlite/brick_sqlite.dart'; -final output = r''' +const output = r''' Future _$CustomSerdesFromRest(Map data, {required RestProvider provider, OfflineFirstRepository? repository}) async { diff --git a/packages/brick_offline_first_with_rest_build/test/offline_first_generator/test_rest_config_endpoint.dart b/packages/brick_offline_first_with_rest_build/test/offline_first_generator/test_rest_config_endpoint.dart index 501e9ed8..830f928b 100644 --- a/packages/brick_offline_first_with_rest_build/test/offline_first_generator/test_rest_config_endpoint.dart +++ b/packages/brick_offline_first_with_rest_build/test/offline_first_generator/test_rest_config_endpoint.dart @@ -2,7 +2,7 @@ import 'package:brick_offline_first/brick_offline_first.dart'; import 'package:brick_offline_first_with_rest/brick_offline_first_with_rest.dart'; import 'package:brick_rest/brick_rest.dart' show RestRequestTransformer, RestSerializable; -final output = r''' +const output = r''' // GENERATED CODE DO NOT EDIT part of '../brick.g.dart'; diff --git a/packages/brick_offline_first_with_rest_build/test/offline_first_generator/test_rest_config_field_rename.dart b/packages/brick_offline_first_with_rest_build/test/offline_first_generator/test_rest_config_field_rename.dart index 22c2ee43..2e0a0379 100644 --- a/packages/brick_offline_first_with_rest_build/test/offline_first_generator/test_rest_config_field_rename.dart +++ b/packages/brick_offline_first_with_rest_build/test/offline_first_generator/test_rest_config_field_rename.dart @@ -3,7 +3,7 @@ import 'package:brick_offline_first/brick_offline_first.dart'; import 'package:brick_offline_first_with_rest/brick_offline_first_with_rest.dart'; import 'package:brick_rest/brick_rest.dart' show RestSerializable; -final output = r''' +const output = r''' Future _$RestConfigNoRenameFromRest( Map data, {required RestProvider provider, @@ -131,7 +131,7 @@ class RestConfigNoRename extends OfflineFirstModel { } @ConnectOfflineFirstWithRest( - restConfig: RestSerializable(fieldRename: FieldRename.snake), + restConfig: RestSerializable.defaults, ) class RestConfigSnakeRename extends OfflineFirstModel { final int someLongField; diff --git a/packages/brick_offline_first_with_rest_build/test/offline_first_generator/test_specify_field_name.dart b/packages/brick_offline_first_with_rest_build/test/offline_first_generator/test_specify_field_name.dart index e5bb3ca5..d7ec7c57 100644 --- a/packages/brick_offline_first_with_rest_build/test/offline_first_generator/test_specify_field_name.dart +++ b/packages/brick_offline_first_with_rest_build/test/offline_first_generator/test_specify_field_name.dart @@ -2,7 +2,7 @@ import 'package:brick_offline_first_with_rest/brick_offline_first_with_rest.dart import 'package:brick_rest/brick_rest.dart' show Rest; import 'package:brick_sqlite/brick_sqlite.dart'; -final output = r''' +const output = r''' Future _$SpecifyFieldNameFromRest(Map data, {required RestProvider provider, OfflineFirstRepository? repository}) async { diff --git a/packages/brick_offline_first_with_rest_build/test/offline_first_generator_test.dart b/packages/brick_offline_first_with_rest_build/test/offline_first_generator_test.dart index 623a07d4..9f70fbaf 100644 --- a/packages/brick_offline_first_with_rest_build/test/offline_first_generator_test.dart +++ b/packages/brick_offline_first_with_rest_build/test/offline_first_generator_test.dart @@ -8,8 +8,8 @@ import 'offline_first_generator/test_rest_config_endpoint.dart' as rest_config_e import 'offline_first_generator/test_rest_config_field_rename.dart' as rest_config_field_rename; import 'offline_first_generator/test_specify_field_name.dart' as specify_field_name; -final _generator = OfflineFirstWithRestGenerator(); -final folder = 'offline_first_generator'; +const _generator = OfflineFirstWithRestGenerator(); +const folder = 'offline_first_generator'; final generateReader = generateLibraryForFolder(folder); void main() { diff --git a/packages/brick_offline_first_with_supabase/CHANGELOG.md b/packages/brick_offline_first_with_supabase/CHANGELOG.md index 936ea7e1..d0bf080f 100644 --- a/packages/brick_offline_first_with_supabase/CHANGELOG.md +++ b/packages/brick_offline_first_with_supabase/CHANGELOG.md @@ -1,5 +1,10 @@ ## Unreleased +## 1.2.0 + +- Upgrade `brick_core` to `1.3.0` +- Update analysis to modern lints + ## 1.1.2 - Support a custom database path when creating the cache manager (#490) diff --git a/packages/brick_offline_first_with_supabase/lib/src/offline_first_with_supabase_adapter.dart b/packages/brick_offline_first_with_supabase/lib/src/offline_first_with_supabase_adapter.dart index 5bc8ab0f..19a32637 100644 --- a/packages/brick_offline_first_with_supabase/lib/src/offline_first_with_supabase_adapter.dart +++ b/packages/brick_offline_first_with_supabase/lib/src/offline_first_with_supabase_adapter.dart @@ -1,9 +1,11 @@ import 'package:brick_offline_first/brick_offline_first.dart'; import 'package:brick_offline_first_with_supabase/src/offline_first_with_supabase_model.dart'; +import 'package:brick_sqlite/brick_sqlite.dart'; import 'package:brick_supabase/brick_supabase.dart'; -/// This adapter fetches first from [SqliteProvider] then hydrates with [SupabaseProvider]. +/// This adapter combines logic from [SqliteProvider] and [SupabaseProvider]. abstract class OfflineFirstWithSupabaseAdapter<_Model extends OfflineFirstWithSupabaseModel> extends OfflineFirstAdapter<_Model> with SupabaseAdapter<_Model> { + /// This adapter combines logic from [SqliteProvider] and [SupabaseProvider]. OfflineFirstWithSupabaseAdapter(); } diff --git a/packages/brick_offline_first_with_supabase/lib/src/offline_first_with_supabase_model.dart b/packages/brick_offline_first_with_supabase/lib/src/offline_first_with_supabase_model.dart index 64cef86d..e4c705a1 100644 --- a/packages/brick_offline_first_with_supabase/lib/src/offline_first_with_supabase_model.dart +++ b/packages/brick_offline_first_with_supabase/lib/src/offline_first_with_supabase_model.dart @@ -1,4 +1,5 @@ import 'package:brick_offline_first/brick_offline_first.dart'; import 'package:brick_supabase/brick_supabase.dart'; +/// Supabase-enabled [OfflineFirstModel] abstract class OfflineFirstWithSupabaseModel extends OfflineFirstModel with SupabaseModel {} diff --git a/packages/brick_offline_first_with_supabase/lib/src/offline_first_with_supabase_repository.dart b/packages/brick_offline_first_with_supabase/lib/src/offline_first_with_supabase_repository.dart index d004a85c..860417b2 100644 --- a/packages/brick_offline_first_with_supabase/lib/src/offline_first_with_supabase_repository.dart +++ b/packages/brick_offline_first_with_supabase/lib/src/offline_first_with_supabase_repository.dart @@ -49,11 +49,16 @@ abstract class OfflineFirstWithSupabaseRepository< @protected final RestOfflineRequestQueue offlineRequestQueue; + /// Tracks the realtime stream controllers @protected @visibleForTesting final Map>>>> supabaseRealtimeSubscriptions = {}; + /// Ensures the [remoteProvider] is a [SupabaseProvider]. + /// + /// Care should be given to attach an offline queue to the provider using the static convenience + /// method [clientQueue]. OfflineFirstWithSupabaseRepository({ super.autoHydrate, super.loggerName, @@ -185,10 +190,11 @@ abstract class OfflineFirstWithSupabaseRepository< return Query( where: fieldsWithValues.entries.map((entry) => Where.exact(entry.key, entry.value)).toList(), - providerArgs: {'limit': 1}, + limit: 1, ); } + /// Convert a query to a [PostgresChangeFilter] for use with [subscribeToRealtime]. @protected @visibleForTesting @visibleForOverriding @@ -256,7 +262,7 @@ abstract class OfflineFirstWithSupabaseRepository< Query? query, String schema = 'public', }) { - query ??= Query(); + query ??= const Query(); if (supabaseRealtimeSubscriptions[TModel]?[eventType]?[query] != null) { return supabaseRealtimeSubscriptions[TModel]![eventType]![query]!.stream diff --git a/packages/brick_offline_first_with_supabase/pubspec.yaml b/packages/brick_offline_first_with_supabase/pubspec.yaml index 8ab938c4..08248487 100644 --- a/packages/brick_offline_first_with_supabase/pubspec.yaml +++ b/packages/brick_offline_first_with_supabase/pubspec.yaml @@ -5,17 +5,17 @@ homepage: https://github.com/GetDutchie/brick/tree/main/packages/brick_offline_f issue_tracker: https://github.com/GetDutchie/brick/issues repository: https://github.com/GetDutchie/brick -version: 1.1.2 +version: 1.2.0 environment: sdk: ">=3.0.0 <4.0.0" dependencies: - brick_core: ">=1.2.0 <2.0.0" + brick_core: ">=1.3.0 <2.0.0" brick_offline_first: ">=3.0.0 <4.0.0" + brick_offline_first_with_rest: ">=3.1.0 <4.0.0" brick_sqlite: ">=3.0.0 <4.0.0" brick_supabase: ">=1.0.0 <2.0.0" - brick_offline_first_with_rest: ">=3.1.0 <4.0.0" http: ">=1.0.0 <2.0.0" logging: ">=1.0.0 <2.0.0" meta: ">=1.3.0 <2.0.0" @@ -23,7 +23,7 @@ dependencies: supabase: ">=2.3.0 <3.0.0" dev_dependencies: - lints: ^2.0.1 - mockito: ^5.0.0 - sqflite_common_ffi: ^2.0.0 - test: ^1.16.5 + lints: + mockito: + sqflite_common_ffi: + test: diff --git a/packages/brick_offline_first_with_supabase/test/__mocks__.dart b/packages/brick_offline_first_with_supabase/test/__mocks__.dart index dd72d9cd..9f055904 100644 --- a/packages/brick_offline_first_with_supabase/test/__mocks__.dart +++ b/packages/brick_offline_first_with_supabase/test/__mocks__.dart @@ -5,6 +5,7 @@ import 'dart:convert'; // ignore: unused_import, unused_shown_name, unnecessary_import import 'package:brick_core/query.dart'; +import 'package:brick_core/src/model_repository.dart'; // ignore: unused_import, unused_shown_name import 'package:brick_offline_first/brick_offline_first.dart' show RuntimeOfflineFirstDefinition; // ignore: unused_import, unused_shown_name, unnecessary_import @@ -19,7 +20,7 @@ import 'package:brick_supabase/brick_supabase.dart'; import 'package:sqflite_common/sqlite_api.dart' show DatabaseExecutor; @ConnectOfflineFirstWithSupabase( - supabaseConfig: SupabaseSerializable(), + supabaseConfig: SupabaseSerializable.defaults, ) class Customer extends OfflineFirstWithSupabaseModel { @Sqlite(unique: true) @@ -55,7 +56,7 @@ class Customer extends OfflineFirstWithSupabaseModel { } @ConnectOfflineFirstWithSupabase( - supabaseConfig: SupabaseSerializable(), + supabaseConfig: SupabaseSerializable.defaults, ) class Pizza extends OfflineFirstWithSupabaseModel { /// Read more about `@Sqlite`: https://github.com/GetDutchie/brick/tree/main/packages/brick_sqlite#fields @@ -155,15 +156,12 @@ class PizzaAdapter extends OfflineFirstWithSupabaseAdapter { @override final fieldsToSupabaseColumns = { 'id': const RuntimeSupabaseColumnDefinition( - association: false, columnName: 'id', ), 'toppings': const RuntimeSupabaseColumnDefinition( - association: false, columnName: 'toppings', ), 'frozen': const RuntimeSupabaseColumnDefinition( - association: false, columnName: 'frozen', ), }; @@ -174,27 +172,20 @@ class PizzaAdapter extends OfflineFirstWithSupabaseAdapter { @override final Map fieldsToSqliteColumns = { 'primaryKey': const RuntimeSqliteColumnDefinition( - association: false, columnName: '_brick_id', - iterable: false, type: int, ), 'id': const RuntimeSqliteColumnDefinition( - association: false, columnName: 'id', - iterable: false, type: int, ), 'toppings': const RuntimeSqliteColumnDefinition( - association: false, columnName: 'toppings', iterable: true, type: Topping, ), 'frozen': const RuntimeSqliteColumnDefinition( - association: false, columnName: 'frozen', - iterable: false, type: bool, ), }; @@ -211,7 +202,7 @@ class PizzaAdapter extends OfflineFirstWithSupabaseAdapter { return null; } - return results.first['_brick_id'] as int; + return results.first['_brick_id']! as int; } @override @@ -220,28 +211,28 @@ class PizzaAdapter extends OfflineFirstWithSupabaseAdapter { @override Future fromSupabase( Map input, { - required provider, + required SupabaseProvider provider, covariant OfflineFirstWithSupabaseRepository? repository, }) async => await _$PizzaFromSupabase(input, provider: provider, repository: repository); @override Future> toSupabase( Pizza input, { - required provider, + required SupabaseProvider provider, covariant OfflineFirstWithSupabaseRepository? repository, }) async => await _$PizzaToSupabase(input, provider: provider, repository: repository); @override Future fromSqlite( Map input, { - required provider, + required SqliteProvider provider, covariant OfflineFirstWithSupabaseRepository? repository, }) async => await _$PizzaFromSqlite(input, provider: provider, repository: repository); @override Future> toSqlite( Pizza input, { - required provider, + required SqliteProvider provider, covariant OfflineFirstWithSupabaseRepository? repository, }) async => await _$PizzaToSqlite(input, provider: provider, repository: repository); @@ -333,22 +324,18 @@ class CustomerAdapter extends OfflineFirstWithSupabaseAdapter { @override final fieldsToSupabaseColumns = { 'id': const RuntimeSupabaseColumnDefinition( - association: false, columnName: 'id', ), 'firstName': const RuntimeSupabaseColumnDefinition( - association: false, columnName: 'first_name', ), 'lastName': const RuntimeSupabaseColumnDefinition( - association: false, columnName: 'last_name', ), 'pizzas': const RuntimeSupabaseColumnDefinition( association: true, columnName: 'pizzas', associationType: Pizza, - associationIsNullable: false, ), }; @override @@ -358,27 +345,19 @@ class CustomerAdapter extends OfflineFirstWithSupabaseAdapter { @override final Map fieldsToSqliteColumns = { 'primaryKey': const RuntimeSqliteColumnDefinition( - association: false, columnName: '_brick_id', - iterable: false, type: int, ), 'id': const RuntimeSqliteColumnDefinition( - association: false, columnName: 'id', - iterable: false, type: int, ), 'firstName': const RuntimeSqliteColumnDefinition( - association: false, columnName: 'first_name', - iterable: false, type: String, ), 'lastName': const RuntimeSqliteColumnDefinition( - association: false, columnName: 'last_name', - iterable: false, type: String, ), 'pizzas': const RuntimeSqliteColumnDefinition( @@ -401,13 +380,17 @@ class CustomerAdapter extends OfflineFirstWithSupabaseAdapter { return null; } - return results.first['_brick_id'] as int; + return results.first['_brick_id']! as int; } @override final String tableName = 'Customer'; @override - Future afterSave(instance, {required provider, repository}) async { + Future afterSave( + Customer instance, { + required SqliteProvider provider, + ModelRepository? 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` = ?', @@ -441,28 +424,28 @@ class CustomerAdapter extends OfflineFirstWithSupabaseAdapter { @override Future fromSupabase( Map input, { - required provider, + required SupabaseProvider provider, covariant OfflineFirstWithSupabaseRepository? repository, }) async => await _$CustomerFromSupabase(input, provider: provider, repository: repository); @override Future> toSupabase( Customer input, { - required provider, + required SupabaseProvider provider, covariant OfflineFirstWithSupabaseRepository? repository, }) async => await _$CustomerToSupabase(input, provider: provider, repository: repository); @override Future fromSqlite( Map input, { - required provider, + required SqliteProvider provider, covariant OfflineFirstWithSupabaseRepository? repository, }) async => await _$CustomerFromSqlite(input, provider: provider, repository: repository); @override Future> toSqlite( Customer input, { - required provider, + required SqliteProvider provider, covariant OfflineFirstWithSupabaseRepository? repository, }) async => await _$CustomerToSqlite(input, provider: provider, repository: repository); @@ -477,14 +460,12 @@ const List _migration_20240906052847_up = [ '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'), diff --git a/packages/brick_offline_first_with_supabase/test/offline_first_with_supabase_repository_test.dart b/packages/brick_offline_first_with_supabase/test/offline_first_with_supabase_repository_test.dart index 4d76a27b..3679b247 100644 --- a/packages/brick_offline_first_with_supabase/test/offline_first_with_supabase_repository_test.dart +++ b/packages/brick_offline_first_with_supabase/test/offline_first_with_supabase_repository_test.dart @@ -24,7 +24,7 @@ class TestRepository extends OfflineFirstWithSupabaseRepository { }, ); - static TestRepository configure(SupabaseMockServer mock) { + factory TestRepository.configure(SupabaseMockServer mock) { final (client, queue) = OfflineFirstWithSupabaseRepository.clientQueue( databaseFactory: databaseFactoryFfi, reattemptForStatusCodes: [], @@ -69,7 +69,7 @@ void main() async { group('#get', () { test('stores locally', () async { - final req = SupabaseRequest(); + const req = SupabaseRequest(); final resp = SupabaseResponse([ await mock.serialize( Customer( @@ -98,8 +98,8 @@ void main() async { }; final supabaseDefinitions = { - 'id': RuntimeSupabaseColumnDefinition(columnName: 'id'), - 'name': RuntimeSupabaseColumnDefinition(columnName: 'name'), + 'id': const RuntimeSupabaseColumnDefinition(columnName: 'id'), + 'name': const RuntimeSupabaseColumnDefinition(columnName: 'name'), }; final query = repository.queryFromSupabaseDeletePayload( @@ -110,7 +110,7 @@ void main() async { expect(query.where, hasLength(1)); expect(query.where!.first.evaluatedField, 'id'); expect(query.where!.first.value, 1); - expect(query.providerArgs, equals({'limit': 1})); + expect(query.limit, 1); }); test('payload entries not present in supabaseDefinitions', () { @@ -120,8 +120,8 @@ void main() async { }; final supabaseDefinitions = { - 'id': RuntimeSupabaseColumnDefinition(columnName: 'id'), - 'name': RuntimeSupabaseColumnDefinition(columnName: 'name'), + 'id': const RuntimeSupabaseColumnDefinition(columnName: 'id'), + 'name': const RuntimeSupabaseColumnDefinition(columnName: 'name'), }; final query = repository.queryFromSupabaseDeletePayload( @@ -132,13 +132,13 @@ void main() async { expect(query.where, hasLength(1)); expect(query.where!.first.evaluatedField, 'id'); expect(query.where!.first.value, 1); - expect(query.providerArgs, equals({'limit': 1})); + expect(query.limit, 1); }); test('empty payload', () { final payload = {}; final supabaseDefinitions = { - 'id': RuntimeSupabaseColumnDefinition(columnName: 'id'), + 'id': const RuntimeSupabaseColumnDefinition(columnName: 'id'), }; final query = repository.queryFromSupabaseDeletePayload( @@ -154,7 +154,7 @@ void main() async { 'unknown_field': 'some value', }; final supabaseDefinitions = { - 'id': RuntimeSupabaseColumnDefinition(columnName: 'id'), + 'id': const RuntimeSupabaseColumnDefinition(columnName: 'id'), }; final query = repository.queryFromSupabaseDeletePayload( @@ -171,8 +171,8 @@ void main() async { }; final supabaseDefinitions = { - 'id': RuntimeSupabaseColumnDefinition(columnName: 'user_id'), - 'name': RuntimeSupabaseColumnDefinition(columnName: 'full_name'), + 'id': const RuntimeSupabaseColumnDefinition(columnName: 'user_id'), + 'name': const RuntimeSupabaseColumnDefinition(columnName: 'full_name'), }; final query = repository.queryFromSupabaseDeletePayload( @@ -183,7 +183,7 @@ void main() async { expect(query.where, hasLength(1)); expect(query.where!.first.evaluatedField, 'id'); expect(query.where!.first.value, 1); - expect(query.providerArgs, equals({'limit': 1})); + expect(query.limit, 1); }); test('multiple columns', () { @@ -193,8 +193,8 @@ void main() async { }; final supabaseDefinitions = { - 'id': RuntimeSupabaseColumnDefinition(columnName: 'user_id'), - 'name': RuntimeSupabaseColumnDefinition(columnName: 'full_name'), + 'id': const RuntimeSupabaseColumnDefinition(columnName: 'user_id'), + 'name': const RuntimeSupabaseColumnDefinition(columnName: 'full_name'), }; final query = repository.queryFromSupabaseDeletePayload( @@ -207,19 +207,19 @@ void main() async { expect(query.where!.first.value, 1); expect(query.where!.last.evaluatedField, 'name'); expect(query.where!.last.value, 'Thomas'); - expect(query.providerArgs, equals({'limit': 1})); + expect(query.limit, 1); }); }); group('#queryToPostgresChangeFilter', () { group('returns null', () { test('for complex queries', () { - final query = Query.where('pizza', Where.exact('id', 2)); + final query = Query.where('pizza', const Where.exact('id', 2)); expect(repository.queryToPostgresChangeFilter(query), isNull); }); test('for empty queries', () { - final query = Query(); + const query = Query(); expect(repository.queryToPostgresChangeFilter(query), isNull); }); @@ -231,19 +231,19 @@ void main() async { group('Compare', () { test('.between', () { - final query = Query(where: [Where('firstName').isBetween(1, 2)]); + final query = Query(where: [const Where('firstName').isBetween(1, 2)]); final filter = repository.queryToPostgresChangeFilter(query); expect(filter, isNull); }); test('.doesNotContain', () { - final query = Query(where: [Where('firstName').doesNotContain('Thomas')]); + final query = Query(where: [const Where('firstName').doesNotContain('Thomas')]); final filter = repository.queryToPostgresChangeFilter(query); expect(filter, isNull); }); test('.exact', () { - final query = Query(where: [Where.exact('firstName', 'Thomas')]); + const query = Query(where: [Where.exact('firstName', 'Thomas')]); final filter = repository.queryToPostgresChangeFilter(query); expect(filter!.type, PostgresChangeFilterType.eq); @@ -252,7 +252,7 @@ void main() async { }); test('.greaterThan', () { - final query = Query(where: [Where('firstName').isGreaterThan('Thomas')]); + final query = Query(where: [const Where('firstName').isGreaterThan('Thomas')]); final filter = repository.queryToPostgresChangeFilter(query); expect(filter!.type, PostgresChangeFilterType.gt); @@ -262,7 +262,7 @@ void main() async { test('.greaterThanOrEqualTo', () { final query = Query( - where: [Where('firstName').isGreaterThanOrEqualTo('Thomas')], + where: [const Where('firstName').isGreaterThanOrEqualTo('Thomas')], ); final filter = repository.queryToPostgresChangeFilter(query); @@ -272,7 +272,7 @@ void main() async { }); test('.lessThan', () { - final query = Query(where: [Where('firstName').isLessThan('Thomas')]); + final query = Query(where: [const Where('firstName').isLessThan('Thomas')]); final filter = repository.queryToPostgresChangeFilter(query); expect(filter!.type, PostgresChangeFilterType.lt); @@ -282,7 +282,7 @@ void main() async { test('.lessThanOrEqualTo', () { final query = Query( - where: [Where('firstName').isLessThanOrEqualTo('Thomas')], + where: [const Where('firstName').isLessThanOrEqualTo('Thomas')], ); final filter = repository.queryToPostgresChangeFilter(query); @@ -292,7 +292,7 @@ void main() async { }); test('.notEqual', () { - final query = Query(where: [Where('firstName').isNot('Thomas')]); + final query = Query(where: [const Where('firstName').isNot('Thomas')]); final filter = repository.queryToPostgresChangeFilter(query); expect(filter!.type, PostgresChangeFilterType.neq); @@ -301,7 +301,7 @@ void main() async { }); test('.contains', () { - final query = Query(where: [Where('firstName').contains('Thomas')]); + final query = Query(where: [const Where('firstName').contains('Thomas')]); final filter = repository.queryToPostgresChangeFilter(query); expect(filter!.type, PostgresChangeFilterType.inFilter); @@ -329,7 +329,7 @@ void main() async { isNotNull, ); await subscription.cancel(); - await Future.delayed(Duration(milliseconds: 10)); + await Future.delayed(const Duration(milliseconds: 10)); }); test('subscription succeeds when policy is non-default .alwaysHydrate', () async { @@ -343,11 +343,11 @@ void main() async { .listen((_) {}); expect(repository.supabaseRealtimeSubscriptions, hasLength(1)); expect( - repository.supabaseRealtimeSubscriptions[Customer]![PostgresChangeEvent.all]!, + repository.supabaseRealtimeSubscriptions[Customer]![PostgresChangeEvent.all], hasLength(1), ); await subscription.cancel(); - await Future.delayed(Duration(milliseconds: 10)); + await Future.delayed(const Duration(milliseconds: 10)); }); test('adds controller and null query to #supabaseRealtimeSubscriptions', () async { @@ -366,7 +366,7 @@ void main() async { isNotNull, ); await subscription.cancel(); - await Future.delayed(Duration(milliseconds: 10)); + await Future.delayed(const Duration(milliseconds: 10)); }); test('cancelling removes from #supabaseRealtimeSubscriptions', () async { @@ -375,7 +375,7 @@ void main() async { expect(repository.supabaseRealtimeSubscriptions[Customer], hasLength(1)); await subscription.cancel(); expect(repository.supabaseRealtimeSubscriptions, hasLength(0)); - await Future.delayed(Duration(milliseconds: 10)); + await Future.delayed(const Duration(milliseconds: 10)); }); test('pausing does not remove from #supabaseRealtimeSubscriptions', () async { @@ -390,7 +390,7 @@ void main() async { isTrue, ); await subscription.cancel(); - await Future.delayed(Duration(milliseconds: 10)); + await Future.delayed(const Duration(milliseconds: 10)); }); }); @@ -438,7 +438,7 @@ void main() async { ]), ); - final req = SupabaseRequest(); + const req = SupabaseRequest(); final resp = SupabaseResponse( await mock.serialize( customer, @@ -480,7 +480,7 @@ void main() async { ]), ); - final req = SupabaseRequest(); + const req = SupabaseRequest(); final resp = SupabaseResponse( await mock.serialize( customer, @@ -530,7 +530,7 @@ void main() async { ]), ); - final req = SupabaseRequest(); + const req = SupabaseRequest(); final resp = SupabaseResponse( await mock.serialize( customer2, @@ -566,7 +566,7 @@ void main() async { ]), ); - final req = SupabaseRequest(); + const req = SupabaseRequest(); final resp = SupabaseResponse( await mock.serialize( customer, @@ -607,7 +607,7 @@ void main() async { ]), ); - final req = SupabaseRequest(); + const req = SupabaseRequest(); final resp = SupabaseResponse( await mock.serialize( customer, @@ -656,7 +656,7 @@ void main() async { ]), ); - final req = SupabaseRequest(); + const req = SupabaseRequest(); final resp = SupabaseResponse( await mock.serialize( customer2, @@ -696,7 +696,7 @@ void main() async { ]), ); - final req = SupabaseRequest(); + const req = SupabaseRequest(); final resp = SupabaseResponse( await mock.serialize( customer1, diff --git a/packages/brick_offline_first_with_supabase_build/CHANGELOG.md b/packages/brick_offline_first_with_supabase_build/CHANGELOG.md index 80618179..b1d7e058 100644 --- a/packages/brick_offline_first_with_supabase_build/CHANGELOG.md +++ b/packages/brick_offline_first_with_supabase_build/CHANGELOG.md @@ -1,6 +1,9 @@ ## Unreleased +## 1.1.0 + - Add documentation to increase pub.dev score +- Update analysis to modern lints ## 1.0.0 diff --git a/packages/brick_offline_first_with_supabase_build/lib/brick_offline_first_with_supabase_build.dart b/packages/brick_offline_first_with_supabase_build/lib/brick_offline_first_with_supabase_build.dart index 554c763b..85601d5a 100644 --- a/packages/brick_offline_first_with_supabase_build/lib/brick_offline_first_with_supabase_build.dart +++ b/packages/brick_offline_first_with_supabase_build/lib/brick_offline_first_with_supabase_build.dart @@ -19,22 +19,27 @@ class OfflineFirstSchemaBuilder extends SchemaBuilder AggregateBuilder( +Builder offlineFirstAggregateBuilder(_) => const 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) => + +/// +Builder offlineFirstAdaptersBuilder(_) => AdapterBuilder(offlineFirstGenerator); -Builder offlineFirstModelDictionaryBuilder(options) => + +/// +Builder offlineFirstModelDictionaryBuilder(_) => ModelDictionaryBuilder( const OfflineFirstModelDictionaryGenerator('Supabase'), expectedImportRemovals: [ @@ -42,5 +47,9 @@ Builder offlineFirstModelDictionaryBuilder(options) => 'import "package:brick_offline_first/brick_offline_first.dart";', ], ); -Builder offlineFirstNewMigrationBuilder(options) => OfflineFirstMigrationBuilder(); -Builder offlineFirstSchemaBuilder(options) => OfflineFirstSchemaBuilder(); + +/// +Builder offlineFirstNewMigrationBuilder(_) => OfflineFirstMigrationBuilder(); + +/// +Builder offlineFirstSchemaBuilder(_) => OfflineFirstSchemaBuilder(); diff --git a/packages/brick_offline_first_with_supabase_build/lib/src/offline_first_supabase_generators.dart b/packages/brick_offline_first_with_supabase_build/lib/src/offline_first_supabase_generators.dart index 24d9a447..974835c2 100644 --- a/packages/brick_offline_first_with_supabase_build/lib/src/offline_first_supabase_generators.dart +++ b/packages/brick_offline_first_with_supabase_build/lib/src/offline_first_supabase_generators.dart @@ -29,7 +29,9 @@ class _OfflineFirstSupabaseDeserialize extends SupabaseDeserialize }) : offlineFirstFields = OfflineFirstFields(element); } +/// class OfflineFirstSupabaseModelSerdesGenerator extends SupabaseModelSerdesGenerator { + /// OfflineFirstSupabaseModelSerdesGenerator( super.element, super.reader, { diff --git a/packages/brick_offline_first_with_supabase_build/lib/src/offline_first_with_supabase_generator.dart b/packages/brick_offline_first_with_supabase_build/lib/src/offline_first_with_supabase_generator.dart index 2c28f043..b6a1426a 100644 --- a/packages/brick_offline_first_with_supabase_build/lib/src/offline_first_with_supabase_generator.dart +++ b/packages/brick_offline_first_with_supabase_build/lib/src/offline_first_with_supabase_generator.dart @@ -5,8 +5,10 @@ import 'package:brick_offline_first_with_supabase/brick_offline_first_with_supab 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 { + /// const OfflineFirstWithSupabaseGenerator({ super.repositoryName, super.superAdapterName, @@ -22,9 +24,6 @@ class OfflineFirstWithSupabaseGenerator ); final sqlite = OfflineFirstSqliteModelSerdesGenerator(element, annotation, repositoryName: repositoryName); - final generators = []; - generators.addAll(supabase.generators); - generators.addAll(sqlite.generators); - return generators; + return [...supabase.generators, ...sqlite.generators]; } } diff --git a/packages/brick_offline_first_with_supabase_build/pubspec.yaml b/packages/brick_offline_first_with_supabase_build/pubspec.yaml index 024a91bb..6543bc26 100644 --- a/packages/brick_offline_first_with_supabase_build/pubspec.yaml +++ b/packages/brick_offline_first_with_supabase_build/pubspec.yaml @@ -4,7 +4,7 @@ homepage: https://github.com/GetDutchie/brick/tree/main/packages/brick_offline_f issue_tracker: https://github.com/GetDutchie/brick/issues repository: https://github.com/GetDutchie/brick -version: 1.0.0 +version: 1.1.0 environment: sdk: ">=3.0.0 <4.0.0" @@ -15,10 +15,10 @@ dependencies: brick_offline_first: ">=3.0.0 <4.0.0" brick_offline_first_build: ">=3.2.0 <4.0.0" brick_offline_first_with_supabase: ">=1.0.0 <2.0.0" - brick_supabase: ">=1.0.0 <2.0.0" - brick_supabase_generators: ">=1.0.0 <2.0.0" brick_sqlite: ">=3.0.0 <4.0.0" brick_sqlite_generators: ">=3.0.0 <4.0.0" + brick_supabase: ">=1.0.0 <2.0.0" + brick_supabase_generators: ">=1.0.0 <2.0.0" build: ">=2.0.0 <3.0.0" dart_style: ">=2.0.0 <3.0.0" logging: ">=1.0.0 <2.0.0" @@ -27,9 +27,9 @@ dependencies: source_gen: ">=1.2.2 <2.0.0" dev_dependencies: - build_verify: ^2.0.0 - source_gen_test: ^1.0.0 - test: ^1.20.1 - lints: ^2.0.1 brick_build_test: path: ../brick_build_test + build_verify: + lints: + source_gen_test: + test: diff --git a/packages/brick_offline_first_with_supabase_build/test/offline_first_generator/test_default_to_null.dart b/packages/brick_offline_first_with_supabase_build/test/offline_first_generator/test_default_to_null.dart index 9b66ed47..2186e490 100644 --- a/packages/brick_offline_first_with_supabase_build/test/offline_first_generator/test_default_to_null.dart +++ b/packages/brick_offline_first_with_supabase_build/test/offline_first_generator/test_default_to_null.dart @@ -2,7 +2,7 @@ import 'package:brick_offline_first/brick_offline_first.dart'; import 'package:brick_offline_first_with_supabase/brick_offline_first_with_supabase.dart'; import 'package:brick_supabase/brick_supabase.dart'; -final output = r''' +const output = r''' // GENERATED CODE DO NOT EDIT part of '../brick.g.dart'; diff --git a/packages/brick_offline_first_with_supabase_build/test/offline_first_generator/test_field_name.dart b/packages/brick_offline_first_with_supabase_build/test/offline_first_generator/test_field_name.dart index 61d91e53..329a3d0a 100644 --- a/packages/brick_offline_first_with_supabase_build/test/offline_first_generator/test_field_name.dart +++ b/packages/brick_offline_first_with_supabase_build/test/offline_first_generator/test_field_name.dart @@ -2,7 +2,7 @@ import 'package:brick_offline_first_with_supabase/brick_offline_first_with_supab import 'package:brick_sqlite/brick_sqlite.dart'; import 'package:brick_supabase/brick_supabase.dart'; -final output = r''' +const output = r''' Future _$SpecifyFieldNameFromSupabase( Map data, {required SupabaseProvider provider, diff --git a/packages/brick_offline_first_with_supabase_build/test/offline_first_generator/test_field_rename.dart b/packages/brick_offline_first_with_supabase_build/test/offline_first_generator/test_field_rename.dart index a8b23e3a..a1242560 100644 --- a/packages/brick_offline_first_with_supabase_build/test/offline_first_generator/test_field_rename.dart +++ b/packages/brick_offline_first_with_supabase_build/test/offline_first_generator/test_field_rename.dart @@ -2,7 +2,7 @@ import 'package:brick_offline_first/brick_offline_first.dart'; import 'package:brick_offline_first_with_supabase/brick_offline_first_with_supabase.dart'; import 'package:brick_supabase/brick_supabase.dart'; -final output = r''' +const output = r''' Future _$SupabaseDefaultFromSupabase(Map data, {required SupabaseProvider provider, OfflineFirstRepository? repository}) async { @@ -176,7 +176,7 @@ Future> _$SupabaseRenameWithOverrideToSqlite( '''; @ConnectOfflineFirstWithSupabase( - supabaseConfig: SupabaseSerializable(), + supabaseConfig: SupabaseSerializable.defaults, ) class SupabaseDefault extends OfflineFirstModel { final int someLongField; @@ -194,7 +194,7 @@ class SupabaseNoRename extends OfflineFirstModel { } @ConnectOfflineFirstWithSupabase( - supabaseConfig: SupabaseSerializable(fieldRename: FieldRename.snake), + supabaseConfig: SupabaseSerializable.defaults, ) class SupabaseSnakeRename extends OfflineFirstModel { final int someLongField; diff --git a/packages/brick_offline_first_with_supabase_build/test/offline_first_generator/test_ignore_duplicates.dart b/packages/brick_offline_first_with_supabase_build/test/offline_first_generator/test_ignore_duplicates.dart index fdb79efc..85e56b16 100644 --- a/packages/brick_offline_first_with_supabase_build/test/offline_first_generator/test_ignore_duplicates.dart +++ b/packages/brick_offline_first_with_supabase_build/test/offline_first_generator/test_ignore_duplicates.dart @@ -2,7 +2,7 @@ import 'package:brick_offline_first/brick_offline_first.dart'; import 'package:brick_offline_first_with_supabase/brick_offline_first_with_supabase.dart'; import 'package:brick_supabase/brick_supabase.dart'; -final output = r''' +const output = r''' // GENERATED CODE DO NOT EDIT part of '../brick.g.dart'; diff --git a/packages/brick_offline_first_with_supabase_build/test/offline_first_generator/test_offline_first_where.dart b/packages/brick_offline_first_with_supabase_build/test/offline_first_generator/test_offline_first_where.dart index 20f0e33d..958e8f23 100644 --- a/packages/brick_offline_first_with_supabase_build/test/offline_first_generator/test_offline_first_where.dart +++ b/packages/brick_offline_first_with_supabase_build/test/offline_first_generator/test_offline_first_where.dart @@ -2,7 +2,7 @@ import 'package:brick_offline_first/brick_offline_first.dart'; import 'package:brick_offline_first_with_supabase/brick_offline_first_with_supabase.dart'; import 'package:brick_supabase/brick_supabase.dart'; -final output = r''' +const output = r''' // GENERATED CODE DO NOT EDIT part of '../brick.g.dart'; @@ -11,19 +11,20 @@ Future _$SupabaseOfflineFirstWhereFromSupabase( {required SupabaseProvider provider, OfflineFirstRepository? repository}) async { return SupabaseOfflineFirstWhere( - association: await repository! - .getAssociation(Query( - where: [Where.exact('id', data["association"]["id"])], - providerArgs: {'limit': 1})) - .then((r) => r!.first), + association: + await repository!.getAssociation(Query(where: [Where.exact('id', data["association"]["id"])], limit: 1)).then( + (r) => r!.first), associations: await Future.wait(data['associations'] ?.map((d) => AssocAdapter() .fromSupabase(d, provider: provider, repository: repository)) .toList() .cast>() ?? []), - nullableAssociations: await Future.wait( - data['nullable_associations']?.map((d) => AssocAdapter().fromSupabase(d, provider: provider, repository: repository)).toList().cast>() ?? [])); + nullableAssociations: await Future.wait(data['nullable_associations'] + ?.map((d) => AssocAdapter().fromSupabase(d, provider: provider, repository: repository)) + .toList() + .cast>() ?? + [])); } Future> _$SupabaseOfflineFirstWhereToSupabase( diff --git a/packages/brick_offline_first_with_supabase_build/test/offline_first_generator/test_on_conflict.dart b/packages/brick_offline_first_with_supabase_build/test/offline_first_generator/test_on_conflict.dart index 50374685..dd3d5150 100644 --- a/packages/brick_offline_first_with_supabase_build/test/offline_first_generator/test_on_conflict.dart +++ b/packages/brick_offline_first_with_supabase_build/test/offline_first_generator/test_on_conflict.dart @@ -2,7 +2,7 @@ import 'package:brick_offline_first/brick_offline_first.dart'; import 'package:brick_offline_first_with_supabase/brick_offline_first_with_supabase.dart'; import 'package:brick_supabase/brick_supabase.dart'; -final output = r''' +const output = r''' // GENERATED CODE DO NOT EDIT part of '../brick.g.dart'; diff --git a/packages/brick_offline_first_with_supabase_build/test/offline_first_generator/test_table_name_defined.dart b/packages/brick_offline_first_with_supabase_build/test/offline_first_generator/test_table_name_defined.dart index 9e8f9143..2dc618a3 100644 --- a/packages/brick_offline_first_with_supabase_build/test/offline_first_generator/test_table_name_defined.dart +++ b/packages/brick_offline_first_with_supabase_build/test/offline_first_generator/test_table_name_defined.dart @@ -2,7 +2,7 @@ import 'package:brick_offline_first/brick_offline_first.dart'; import 'package:brick_offline_first_with_supabase/brick_offline_first_with_supabase.dart'; import 'package:brick_supabase/brick_supabase.dart'; -final output = r''' +const output = r''' // GENERATED CODE DO NOT EDIT part of '../brick.g.dart'; diff --git a/packages/brick_offline_first_with_supabase_build/test/offline_first_generator/test_table_name_undefined.dart b/packages/brick_offline_first_with_supabase_build/test/offline_first_generator/test_table_name_undefined.dart index 530620b2..183fca3b 100644 --- a/packages/brick_offline_first_with_supabase_build/test/offline_first_generator/test_table_name_undefined.dart +++ b/packages/brick_offline_first_with_supabase_build/test/offline_first_generator/test_table_name_undefined.dart @@ -2,7 +2,7 @@ import 'package:brick_offline_first/brick_offline_first.dart'; import 'package:brick_offline_first_with_supabase/brick_offline_first_with_supabase.dart'; import 'package:brick_supabase/brick_supabase.dart'; -final output = r''' +const output = r''' // GENERATED CODE DO NOT EDIT part of '../brick.g.dart'; @@ -105,7 +105,7 @@ class UndefinedCamelizedNameAdapter '''; @ConnectOfflineFirstWithSupabase( - supabaseConfig: SupabaseSerializable(), + supabaseConfig: SupabaseSerializable.defaults, ) class UndefinedCamelizedName extends OfflineFirstModel { final int someLongField; diff --git a/packages/brick_offline_first_with_supabase_build/test/offline_first_generator_test.dart b/packages/brick_offline_first_with_supabase_build/test/offline_first_generator_test.dart index a1946473..bd93bcf1 100644 --- a/packages/brick_offline_first_with_supabase_build/test/offline_first_generator_test.dart +++ b/packages/brick_offline_first_with_supabase_build/test/offline_first_generator_test.dart @@ -12,8 +12,8 @@ import 'offline_first_generator/test_on_conflict.dart' as on_conflict; import 'offline_first_generator/test_table_name_defined.dart' as table_name_defined; import 'offline_first_generator/test_table_name_undefined.dart' as table_name_undefined; -final _generator = OfflineFirstWithSupabaseGenerator(); -final folder = 'offline_first_generator'; +const _generator = OfflineFirstWithSupabaseGenerator(); +const folder = 'offline_first_generator'; final generateReader = generateLibraryForFolder(folder); void main() { diff --git a/packages/brick_rest/CHANGELOG.md b/packages/brick_rest/CHANGELOG.md index 39396c91..17784390 100644 --- a/packages/brick_rest/CHANGELOG.md +++ b/packages/brick_rest/CHANGELOG.md @@ -1,5 +1,12 @@ ## Unreleased +## 3.1.0 + +- **DEPRECATION** `Query(providerArgs: {'request':})` is now `Query(forProviders: [RestProviderQuery(request:)])`. +- Upgrade `brick_core` to `1.3.0` +- Update analysis to modern lints +- New `RestProviderQuery` adds REST-specific support for the new `Query`. + ## 3.0.4 - Access `FieldRename` from `brick_core` instead of declaring within this package diff --git a/packages/brick_rest/README.md b/packages/brick_rest/README.md index 68cfee0c..b6f87240 100644 --- a/packages/brick_rest/README.md +++ b/packages/brick_rest/README.md @@ -63,8 +63,8 @@ class User extends OfflineFirstModel {} ```dart class UserRequestTransformer extends RestRequestTransformer { RestRequest? get get { - if (query?.providerArgs.isNotEmpty && query.providerArgs['limit'] != null) { - return RestRequest(url: "/users?limit=${query.providerArgs['limit']}"); + if (query?.providerArgs.isNotEmpty && query.limit != null) { + return RestRequest(url: "/users?limit=${query.limit}"); } const RestRequest(url: '/users'); } diff --git a/packages/brick_rest/lib/brick_rest.dart b/packages/brick_rest/lib/brick_rest.dart index 5668c7b6..a8dcbd82 100644 --- a/packages/brick_rest/lib/brick_rest.dart +++ b/packages/brick_rest/lib/brick_rest.dart @@ -6,5 +6,6 @@ export 'package:brick_rest/src/rest_adapter.dart'; export 'package:brick_rest/src/rest_model.dart'; export 'package:brick_rest/src/rest_model_dictionary.dart'; export 'package:brick_rest/src/rest_provider.dart'; +export 'package:brick_rest/src/rest_provider_query.dart'; export 'package:brick_rest/src/rest_request.dart'; export 'package:brick_rest/src/rest_request_transformer.dart'; diff --git a/packages/brick_rest/lib/gzip_http_client.dart b/packages/brick_rest/lib/gzip_http_client.dart index 559ffe27..82a12c66 100644 --- a/packages/brick_rest/lib/gzip_http_client.dart +++ b/packages/brick_rest/lib/gzip_http_client.dart @@ -15,6 +15,8 @@ class GZipHttpClient extends http.BaseClient { @protected final http.Client innerClient; + /// Gzip all incoming requests and mutate them so that the payload is encoded. + /// Additionally, (over)writes the header `{'Content-Encoding': 'gzip'}`. GZipHttpClient({ http.Client? innerClient, diff --git a/packages/brick_rest/lib/rest_exception.dart b/packages/brick_rest/lib/rest_exception.dart index 669bb25a..47468614 100644 --- a/packages/brick_rest/lib/rest_exception.dart +++ b/packages/brick_rest/lib/rest_exception.dart @@ -1,11 +1,14 @@ import 'dart:convert'; +import 'package:brick_rest/src/rest_provider.dart'; import 'package:http/http.dart' as http; /// An error class exclusive to the [RestProvider] class RestException implements Exception { + /// The HTTP response that triggered the exception final http.Response response; + /// An error class exclusive to the [RestProvider] RestException(this.response); /// Decoded error messages if included under the top-level key 'errors' in the response. @@ -22,6 +25,7 @@ class RestException implements Exception { return null; } + /// A string representation of the exception String get message => 'statusCode=${response.statusCode} url=${response.request?.url} method=${response.request?.method} body=${response.body}'; diff --git a/packages/brick_rest/lib/src/annotations/rest.dart b/packages/brick_rest/lib/src/annotations/rest.dart index 7c5a6980..70bb4944 100644 --- a/packages/brick_rest/lib/src/annotations/rest.dart +++ b/packages/brick_rest/lib/src/annotations/rest.dart @@ -1,6 +1,6 @@ import 'package:brick_core/field_serializable.dart'; -/// An annotation used to specify how a field is serialized for a [RestAdapter]. +/// An annotation used to specify how a field is serialized for a `RestAdapter`. /// Heavily inspired by [JsonKey](https://github.com/dart-lang/json_serializable/blob/master/json_annotation/lib/src/json_key.dart) class Rest implements FieldSerializable { /// The value to use if the source does not contain this key or if the diff --git a/packages/brick_rest/lib/src/rest_adapter.dart b/packages/brick_rest/lib/src/rest_adapter.dart index 25c8a358..7b9bc68c 100644 --- a/packages/brick_rest/lib/src/rest_adapter.dart +++ b/packages/brick_rest/lib/src/rest_adapter.dart @@ -4,11 +4,12 @@ import 'package:brick_rest/src/rest_provider.dart'; import 'package:brick_rest/src/rest_request_transformer.dart'; class _DefaultRestTransformer extends RestRequestTransformer { - const _DefaultRestTransformer(Query? query, RestModel? instance) : super(null, null); + const _DefaultRestTransformer(Query? _, RestModel? __) : super(null, null); } /// Constructors that convert app models to and from REST abstract class RestAdapter implements Adapter { + /// Deserialize data from a REST response Future fromRest( Map input, { required RestProvider provider, @@ -18,6 +19,7 @@ abstract class RestAdapter implements Adapter /// The endpoint path to access provided a query. Must include a leading slash. RestRequestTransformer Function(Query?, TModel?)? get restRequest => _DefaultRestTransformer.new; + /// Serialize data to a REST endpoint Future> toRest( TModel input, { required RestProvider provider, diff --git a/packages/brick_rest/lib/src/rest_model.dart b/packages/brick_rest/lib/src/rest_model.dart index 564963c2..57ec901a 100644 --- a/packages/brick_rest/lib/src/rest_model.dart +++ b/packages/brick_rest/lib/src/rest_model.dart @@ -1,4 +1,4 @@ import 'package:brick_core/core.dart'; -/// Models accessible to the [RestProvider] +/// Models accessible to the `RestProvider` abstract class RestModel implements Model {} diff --git a/packages/brick_rest/lib/src/rest_model_dictionary.dart b/packages/brick_rest/lib/src/rest_model_dictionary.dart index 07c18d71..bb6d8d1b 100644 --- a/packages/brick_rest/lib/src/rest_model_dictionary.dart +++ b/packages/brick_rest/lib/src/rest_model_dictionary.dart @@ -4,5 +4,6 @@ import 'package:brick_rest/src/rest_model.dart'; /// Associates app models with their [RestAdapter] class RestModelDictionary extends ModelDictionary> { + /// Associates app models with their [RestAdapter] const RestModelDictionary(super.adapterFor); } diff --git a/packages/brick_rest/lib/src/rest_provider.dart b/packages/brick_rest/lib/src/rest_provider.dart index 0dc799cb..b349949f 100644 --- a/packages/brick_rest/lib/src/rest_provider.dart +++ b/packages/brick_rest/lib/src/rest_provider.dart @@ -1,9 +1,12 @@ +// ignore_for_file: deprecated_member_use + import 'dart:convert'; import 'package:brick_core/core.dart'; import 'package:brick_rest/rest_exception.dart'; import 'package:brick_rest/src/rest_model.dart'; import 'package:brick_rest/src/rest_model_dictionary.dart'; +import 'package:brick_rest/src/rest_provider_query.dart'; import 'package:brick_rest/src/rest_request.dart'; import 'package:http/http.dart' as http; import 'package:logging/logging.dart'; @@ -24,9 +27,11 @@ class RestProvider implements Provider { /// All requests pass through this client. http.Client client; + /// Internal logger @protected final Logger logger; + /// Retrieves data from an HTTP endpoint RestProvider( this.baseEndpoint, { required this.modelDictionary, @@ -36,11 +41,17 @@ class RestProvider implements Provider { /// Sends a DELETE request method to the endpoint @override - Future delete(instance, {query, repository}) async { + Future delete( + TModel instance, { + Query? query, + ModelRepository? repository, + }) async { final adapter = modelDictionary.adapterFor[TModel]!; final fromAdapter = adapter.restRequest != null ? adapter.restRequest!(query, instance).delete : null; - final request = (query?.providerArgs['request'] as RestRequest?) ?? fromAdapter; + final request = (query?.providerQueries[RestProvider] as RestProviderQuery?)?.request ?? + (query?.providerArgs['request'] as RestRequest?) ?? + fromAdapter; final url = request?.url; if (url == null) return null; @@ -60,10 +71,15 @@ class RestProvider implements Provider { } @override - Future exists({query, repository}) async { + Future exists({ + Query? query, + ModelRepository? repository, + }) async { final adapter = modelDictionary.adapterFor[TModel]!; final fromAdapter = adapter.restRequest != null ? adapter.restRequest!(query, null).get : null; - final request = (query?.providerArgs['request'] as RestRequest?) ?? fromAdapter; + final request = (query?.providerQueries[RestProvider] as RestProviderQuery?)?.request ?? + (query?.providerArgs['request'] as RestRequest?) ?? + fromAdapter; final url = request?.url; if (url == null) return false; @@ -76,15 +92,16 @@ class RestProvider implements Provider { return statusCodeIsSuccessful(resp.statusCode); } - /// [Query]'s `providerArgs` can extend the [get] functionality: - /// * `'request'` (`RestRequest`) Specifies configurable information about the request like HTTP method or top level key - /// (however, when defined, `['request'].topLevelKey` is prioritized). Note that when no key is defined, the first value is returned - /// regardless of the first key (in the example, `{"id"...}`). @override - Future> get({query, repository}) async { + Future> get({ + Query? query, + ModelRepository? repository, + }) async { final adapter = modelDictionary.adapterFor[TModel]!; final fromAdapter = adapter.restRequest != null ? adapter.restRequest!(query, null).get : null; - final request = (query?.providerArgs['request'] as RestRequest?) ?? fromAdapter; + final request = (query?.providerQueries[RestProvider] as RestProviderQuery?)?.request ?? + (query?.providerArgs['request'] as RestRequest?) ?? + fromAdapter; final url = request?.url; if (url == null) return []; @@ -101,9 +118,7 @@ class RestProvider implements Provider { final body = parsed is Iterable ? parsed : [parsed]; final results = body .where((msg) => msg != null) - .map((msg) { - return adapter.fromRest(msg, provider: this, repository: repository); - }) + .map((msg) => adapter.fromRest(msg, provider: this, repository: repository)) .toList() .cast>(); @@ -114,15 +129,19 @@ class RestProvider implements Provider { } } - /// [Query]'s `providerArgs` can extend the [upsert] functionality: - /// * `'request'` (`RestRequest`) Specifies configurable information about the request like HTTP method or top level key @override - Future upsert(instance, {query, repository}) async { + Future upsert( + TModel instance, { + Query? query, + ModelRepository? repository, + }) async { final adapter = modelDictionary.adapterFor[TModel]!; final body = await adapter.toRest(instance, provider: this, repository: repository); final fromAdapter = adapter.restRequest != null ? adapter.restRequest!(query, instance).upsert : null; - final request = (query?.providerArgs['request'] as RestRequest?) ?? fromAdapter; + final request = (query?.providerQueries[RestProvider] as RestProviderQuery?)?.request ?? + (query?.providerArgs['request'] as RestRequest?) ?? + fromAdapter; final url = request?.url; if (url == null) return null; @@ -156,7 +175,9 @@ class RestProvider implements Provider { /// Expand a query into HTTP headers @protected Map headersForQuery(Query? query, Map? requestHeaders) { - if ((query == null || query.providerArgs['request']?.headers == null) && + final request = (query?.providerQueries[RestProvider] as RestProviderQuery?)?.request ?? + (query?.providerArgs['request'] as RestRequest?); + if ((query == null || request?.headers == null) && requestHeaders == null && defaultHeaders != null) { return defaultHeaders!; @@ -166,7 +187,7 @@ class RestProvider implements Provider { ..addAll({'Content-Type': 'application/json'}) ..addAll(defaultHeaders ?? {}) ..addAll(requestHeaders ?? {}) - ..addAll(query?.providerArgs['request']?.headers ?? {}); + ..addAll(request?.headers ?? {}); } /// If a [key] is defined from the adapter and it is not null in the response, use it to narrow the response. @@ -194,9 +215,11 @@ class RestProvider implements Provider { Map? body, }) async { final combinedBody = body ?? {}; - final url = Uri.parse([baseEndpoint, request.url!].join('')); - final method = - (query?.providerArgs ?? {})['request']?.method ?? request.method ?? operation.httpMethod; + final url = Uri.parse([baseEndpoint, request.url!].join()); + final requestFromQuery = + (query?.providerQueries[RestProvider] as RestProviderQuery?)?.request ?? + (query?.providerArgs['request'] as RestRequest?); + final method = requestFromQuery?.method ?? request.method ?? operation.httpMethod; final headers = headersForQuery(query, request.headers); logger.fine('$method $url'); @@ -208,8 +231,8 @@ class RestProvider implements Provider { } // if supplementalTopLevelData is specified it, insert alongside normal payload - final topLevelData = (query?.providerArgs ?? {})['request']?.supplementalTopLevelData ?? - request.supplementalTopLevelData; + final topLevelData = + requestFromQuery?.supplementalTopLevelData ?? request.supplementalTopLevelData; if (topLevelData != null) { combinedBody.addAll(topLevelData); } @@ -229,11 +252,12 @@ class RestProvider implements Provider { return await client.put(url, body: serializedBody, headers: headers); default: throw StateError( - "Request method $method is unhandled; use providerArgs['request'] or RestRequest#method", + 'Request method $method is unhandled; use RestProviderQuery or RestRequest#method', ); } } + /// Whether the status code is between 200 and 300 static bool statusCodeIsSuccessful(int? statusCode) => statusCode != null && 200 <= statusCode && statusCode < 300; } diff --git a/packages/brick_rest/lib/src/rest_provider_query.dart b/packages/brick_rest/lib/src/rest_provider_query.dart new file mode 100644 index 00000000..0b837801 --- /dev/null +++ b/packages/brick_rest/lib/src/rest_provider_query.dart @@ -0,0 +1,35 @@ +import 'package:brick_core/query.dart'; +import 'package:brick_rest/src/rest_provider.dart'; +import 'package:brick_rest/src/rest_request.dart'; + +/// A REST-specific query definitiion for use in [Query]. +class RestProviderQuery extends ProviderQuery { + /// The [RestRequest] to use for the [Query]. + final RestRequest? request; + + /// A REST-specific query definitiion for use in [Query]. + const RestProviderQuery({ + this.request, + }); + + /// Creates a copy of this [RestProviderQuery] with the given fields replaced. + RestProviderQuery copyWith({ + RestRequest? request, + }) => + RestProviderQuery( + request: request ?? this.request, + ); + + @override + Map toJson() => { + if (request != null) 'request': request?.toJson(), + }; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is RestProviderQuery && runtimeType == other.runtimeType && request == other.request; + + @override + int get hashCode => request.hashCode; +} diff --git a/packages/brick_rest/lib/src/rest_request.dart b/packages/brick_rest/lib/src/rest_request.dart index cebd6735..0921210a 100644 --- a/packages/brick_rest/lib/src/rest_request.dart +++ b/packages/brick_rest/lib/src/rest_request.dart @@ -1,5 +1,6 @@ -/// A cohesive definition for [RestRequestTransformer]'s instance fields. +/// A cohesive definition for `RestRequestTransformer`'s instance fields. class RestRequest { + /// HTTP headers final Map? headers; /// The [HTTP method](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods) @@ -29,7 +30,7 @@ class RestRequest { /// The URL of the endpoint to invoke. This is **appended** to `baseEndpoint`. /// **Example**: /// ```dart - /// if (query.providerArgs['limit'] == 1) { + /// if (query.limit == 1) { /// return "/person/${query.providerArgs['id']}"; /// } /// @@ -37,6 +38,7 @@ class RestRequest { /// ``` final String? url; + /// A cohesive definition for `RestRequestTransformer`'s instance fields. const RestRequest({ this.headers, this.method, @@ -45,23 +47,56 @@ class RestRequest { this.url, }); - factory RestRequest.fromJson(Map data) { - return RestRequest( - headers: data['headers'], - method: data['method'], - supplementalTopLevelData: data['supplementalTopLevelData'], - topLevelKey: data['topLevelKey'], - url: data['url'], - ); - } + /// Deserialize a request from JSON + factory RestRequest.fromJson(Map data) => RestRequest( + headers: data['headers'], + method: data['method'], + supplementalTopLevelData: data['supplementalTopLevelData'], + topLevelKey: data['topLevelKey'], + url: data['url'], + ); - Map toJson() { - return { - 'headers': headers, - 'method': method, - 'supplementalTopLevelData': supplementalTopLevelData, - 'topLevelKey': topLevelKey, - 'url': url, - }; - } + /// Copy a request with overriden parameters + RestRequest copyWith({ + Map? headers, + String? method, + Map? supplementalTopLevelData, + String? topLevelKey, + String? url, + }) => + RestRequest( + headers: headers ?? this.headers, + method: method ?? this.method, + supplementalTopLevelData: supplementalTopLevelData ?? this.supplementalTopLevelData, + topLevelKey: topLevelKey ?? this.topLevelKey, + url: url ?? this.url, + ); + + /// Serialize a request to JSON + Map toJson() => { + 'headers': headers, + 'method': method, + 'supplementalTopLevelData': supplementalTopLevelData, + 'topLevelKey': topLevelKey, + 'url': url, + }; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is RestRequest && + runtimeType == other.runtimeType && + headers == other.headers && + method == other.method && + supplementalTopLevelData == other.supplementalTopLevelData && + topLevelKey == other.topLevelKey && + url == other.url; + + @override + int get hashCode => + headers.hashCode ^ + method.hashCode ^ + supplementalTopLevelData.hashCode ^ + topLevelKey.hashCode ^ + url.hashCode; } diff --git a/packages/brick_rest/lib/src/rest_request_transformer.dart b/packages/brick_rest/lib/src/rest_request_transformer.dart index 4e87155d..3e503675 100644 --- a/packages/brick_rest/lib/src/rest_request_transformer.dart +++ b/packages/brick_rest/lib/src/rest_request_transformer.dart @@ -34,5 +34,20 @@ abstract class RestRequestTransformer { /// The operation used for any inserting or updating data operations. RestRequest? get upsert => null; + /// Specify request formatting (such as `method` or `url`) for each Brick operation. + /// + /// This class should be subclassed for each model. For example: + /// + /// ```dart + /// @RestSerializable( + /// requestTransformer: MyModelOperationTransformer.new, + /// ) + /// class MyModel extends RestModel {} + /// class MyModelOperationTransformer extends RestRequestTransformer { + /// final get = RestRequest( + /// url: 'https://myapi.com/mymodel' + /// ); + /// } + /// ``` const RestRequestTransformer(this.query, this.instance); } diff --git a/packages/brick_rest/pubspec.yaml b/packages/brick_rest/pubspec.yaml index 83c341d8..c640395e 100644 --- a/packages/brick_rest/pubspec.yaml +++ b/packages/brick_rest/pubspec.yaml @@ -4,13 +4,13 @@ homepage: https://github.com/GetDutchie/brick/tree/main/packages/brick_rest issue_tracker: https://github.com/GetDutchie/brick/issues repository: https://github.com/GetDutchie/brick -version: 3.0.4 +version: 3.1.0 environment: sdk: ">=2.18.0 <4.0.0" dependencies: - brick_core: ^1.2.1 + brick_core: ^1.3.0 http: ">=1.0.0 <2.0.0" logging: ">=1.0.0 <2.0.0" meta: ">=1.3.0 <2.0.0" diff --git a/packages/brick_rest/test/__mocks__.dart b/packages/brick_rest/test/__mocks__.dart index dc308cb8..c35b9eaf 100644 --- a/packages/brick_rest/test/__mocks__.dart +++ b/packages/brick_rest/test/__mocks__.dart @@ -1,3 +1,4 @@ +import 'package:brick_core/src/model_repository.dart'; import 'package:brick_rest/brick_rest.dart'; import 'package:http/http.dart' as http; import 'package:http/testing.dart'; @@ -9,9 +10,8 @@ class DemoRestModel extends RestModel { } /// Create [DemoRestModel] from json -Future _$DemoRestModelFromRest(Map json) async { - return DemoRestModel(json['name'] as String); -} +Future _$DemoRestModelFromRest(Map json) async => + DemoRestModel(json['name'] as String); /// Create json from [DemoRestModel] Future> _$DemoRestModelToRest(DemoRestModel instance) async { @@ -31,9 +31,7 @@ class DemoRestRequestTransformer extends RestRequestTransformer { @override RestRequest get get { final url = () { - if (query != null && - query!.providerArgs['limit'] != null && - query!.providerArgs['limit'] > 1) { + if (query != null && query!.limit != null && query!.limit! > 1) { return '/people'; } @@ -50,13 +48,21 @@ class DemoRestRequestTransformer extends RestRequestTransformer { const DemoRestRequestTransformer(super.query, RestModel? super.instance); } -/// Construct a [DemoRestModel] for the [RestRepository] +/// Construct a [DemoRestModel] for the `RestRepository` class DemoRestModelAdapter extends RestAdapter { @override - Future fromRest(data, {required provider, repository}) => + Future fromRest( + Map data, { + required RestProvider provider, + ModelRepository? repository, + }) => _$DemoRestModelFromRest(data); @override - Future> toRest(instance, {required provider, repository}) async => + Future> toRest( + DemoRestModel instance, { + required RestProvider provider, + ModelRepository? repository, + }) async => await _$DemoRestModelToRest(instance); @override @@ -68,13 +74,12 @@ final Map> _restMappings = { }; final restModelDictionary = RestModelDictionary(_restMappings); -MockClient generateClient(String response, {String? requestBody, String? requestMethod}) { - return MockClient((req) async { - final matchesRequestBody = req.body == requestBody || requestBody == null; - final matchesRequestMethod = req.method == requestMethod || requestMethod == null; +MockClient generateClient(String response, {String? requestBody, String? requestMethod}) => + MockClient((req) async { + final matchesRequestBody = req.body == requestBody || requestBody == null; + final matchesRequestMethod = req.method == requestMethod || requestMethod == null; - if (matchesRequestMethod && matchesRequestBody) return http.Response(response, 200); + if (matchesRequestMethod && matchesRequestBody) return http.Response(response, 200); - throw StateError('No response for $response'); - }); -} + throw StateError('No response for $response'); + }); diff --git a/packages/brick_rest/test/rest_provider_query_test.dart b/packages/brick_rest/test/rest_provider_query_test.dart new file mode 100644 index 00000000..4fe78a03 --- /dev/null +++ b/packages/brick_rest/test/rest_provider_query_test.dart @@ -0,0 +1,141 @@ +import 'package:brick_core/core.dart'; +import 'package:brick_rest/brick_rest.dart'; +import 'package:http/http.dart' as http; +import 'package:http/testing.dart'; +import 'package:test/test.dart'; + +import '__mocks__.dart'; + +RestProvider generateProvider(String response, {String? requestBody, String? requestMethod}) => + RestProvider( + 'http://0.0.0.0:3000', + modelDictionary: restModelDictionary, + client: generateClient(response, requestBody: requestBody, requestMethod: requestMethod), + ); + +void main() { + group('RestProviderQuery', () { + test('#headers', () async { + final provider = RestProvider( + 'http://0.0.0.0:3000', + modelDictionary: restModelDictionary, + client: MockClient((req) async { + if (req.method == 'POST' && req.headers['Authorization'] == 'Basic xyz') { + return http.Response('{"name": "Thomas"}', 200); + } + + throw StateError('No response'); + }), + ); + + final instance = DemoRestModel('Guy'); + const query = Query( + forProviders: [ + RestProviderQuery( + request: RestRequest(headers: {'Authorization': 'Basic xyz'}, url: '/'), + ), + ], + ); + final resp = await provider.upsert(instance, query: query); + + expect(resp!.statusCode, 200); + expect(resp.body, '{"name": "Thomas"}'); + }); + + test('#method PUT', () async { + final provider = generateProvider('{"name": "Guy"}', requestMethod: 'PUT'); + + final instance = DemoRestModel('Guy'); + const query = + Query(forProviders: [RestProviderQuery(request: RestRequest(method: 'PUT', url: '/'))]); + final resp = await provider.upsert(instance, query: query); + + expect(resp!.statusCode, 200); + expect(resp.body, '{"name": "Guy"}'); + }); + + test('#method PATCH', () async { + final provider = generateProvider('{"name": "Guy"}', requestMethod: 'PATCH'); + + final instance = DemoRestModel('Guy'); + const query = Query( + forProviders: [RestProviderQuery(request: RestRequest(method: 'PATCH', url: '/'))], + ); + final resp = await provider.upsert(instance, query: query); + + expect(resp!.statusCode, 200); + expect(resp.body, '{"name": "Guy"}'); + }); + + test('#topLevelKey', () async { + final provider = generateProvider( + '{"name": "Thomas"}', + requestMethod: 'POST', + requestBody: '{"top":{"name":"Guy"}}', + ); + + final instance = DemoRestModel('Guy'); + const query = Query( + forProviders: [RestProviderQuery(request: RestRequest(topLevelKey: 'top', url: '/'))], + ); + final resp = await provider.upsert(instance, query: query); + + expect(resp!.statusCode, 200); + expect(resp.body, '{"name": "Thomas"}'); + }); + + group('#supplementalTopLevelData', () { + test('#get', () async { + final provider = generateProvider( + '[{"name": "Thomas"}]', + requestMethod: 'POST', + requestBody: '{"other_name":{"first_name":"Thomas"}}', + ); + + const query = Query( + forProviders: [ + RestProviderQuery( + request: RestRequest( + url: '/', + method: 'POST', + supplementalTopLevelData: { + 'other_name': {'first_name': 'Thomas'}, + }, + ), + ), + ], + ); + final instance = await provider.get(query: query); + + expect(instance.first.name, 'Thomas'); + }); + + test('#upsert', () async { + final provider = generateProvider( + '{"name": "Thomas"}', + requestMethod: 'POST', + requestBody: '{"top":{"name":"Guy"},"other_name":{"first_name":"Thomas"}}', + ); + + final instance = DemoRestModel('Guy'); + const query = Query( + forProviders: [ + RestProviderQuery( + request: RestRequest( + topLevelKey: 'top', + url: '/', + supplementalTopLevelData: { + 'other_name': {'first_name': 'Thomas'}, + }, + ), + ), + ], + ); + final resp = await provider.upsert(instance, query: query); + + expect(resp!.statusCode, 200); + expect(resp.body, '{"name": "Thomas"}'); + }); + }); + }); +} diff --git a/packages/brick_rest/test/rest_provider_test.dart b/packages/brick_rest/test/rest_provider_test.dart index 8bdf2c2b..e670cdb7 100644 --- a/packages/brick_rest/test/rest_provider_test.dart +++ b/packages/brick_rest/test/rest_provider_test.dart @@ -6,13 +6,12 @@ import 'package:test/test.dart'; import '__mocks__.dart'; -RestProvider generateProvider(String response, {String? requestBody, String? requestMethod}) { - return RestProvider( - 'http://0.0.0.0:3000', - modelDictionary: restModelDictionary, - client: generateClient(response, requestBody: requestBody, requestMethod: requestMethod), - ); -} +RestProvider generateProvider(String response, {String? requestBody, String? requestMethod}) => + RestProvider( + 'http://0.0.0.0:3000', + modelDictionary: restModelDictionary, + client: generateClient(response, requestBody: requestBody, requestMethod: requestMethod), + ); void main() { group('RestProvider', () { @@ -33,9 +32,7 @@ void main() { test('#defaultHeaders', () async { final headers = {'Authorization': 'token=12345'}; - final provider = generateProvider('[{"name": "Guy"}]'); - - provider.defaultHeaders = headers; + final provider = generateProvider('[{"name": "Guy"}]')..defaultHeaders = headers; final instance = await provider.get(); expect(instance.first.name, 'Guy'); }); @@ -64,7 +61,8 @@ void main() { ); final instance = DemoRestModel('Guy'); - final query = Query( + const query = Query( + // ignore: deprecated_member_use providerArgs: { 'request': RestRequest(headers: {'Authorization': 'Basic xyz'}, url: '/'), }, @@ -75,11 +73,39 @@ void main() { expect(resp.body, '{"name": "Thomas"}'); }); + test('RestProviderQuery#headers', () async { + final provider = RestProvider( + 'http://0.0.0.0:3000', + modelDictionary: restModelDictionary, + client: MockClient((req) async { + if (req.method == 'POST' && req.headers['Authorization'] == 'Basic xyz') { + return http.Response('{"name": "Thomas"}', 200); + } + + throw StateError('No response'); + }), + ); + + final instance = DemoRestModel('Guy'); + const query = Query( + forProviders: [ + RestProviderQuery( + request: RestRequest(headers: {'Authorization': 'Basic xyz'}, url: '/'), + ), + ], + ); + final resp = await provider.upsert(instance, query: query); + + expect(resp!.statusCode, 200); + expect(resp.body, '{"name": "Thomas"}'); + }); + test('providerArgs["request"].method PUT', () async { final provider = generateProvider('{"name": "Guy"}', requestMethod: 'PUT'); final instance = DemoRestModel('Guy'); - final query = Query(providerArgs: {'request': RestRequest(method: 'PUT', url: '/')}); + // ignore: deprecated_member_use + const query = Query(providerArgs: {'request': RestRequest(method: 'PUT', url: '/')}); final resp = await provider.upsert(instance, query: query); expect(resp!.statusCode, 200); @@ -90,7 +116,8 @@ void main() { final provider = generateProvider('{"name": "Guy"}', requestMethod: 'PATCH'); final instance = DemoRestModel('Guy'); - final query = Query(providerArgs: {'request': RestRequest(method: 'PATCH', url: '/')}); + // ignore: deprecated_member_use + const query = Query(providerArgs: {'request': RestRequest(method: 'PATCH', url: '/')}); final resp = await provider.upsert(instance, query: query); expect(resp!.statusCode, 200); @@ -105,7 +132,8 @@ void main() { ); final instance = DemoRestModel('Guy'); - final query = Query(providerArgs: {'request': RestRequest(topLevelKey: 'top', url: '/')}); + // ignore: deprecated_member_use + const query = Query(providerArgs: {'request': RestRequest(topLevelKey: 'top', url: '/')}); final resp = await provider.upsert(instance, query: query); expect(resp!.statusCode, 200); @@ -120,7 +148,8 @@ void main() { requestBody: '{"other_name":{"first_name":"Thomas"}}', ); - final query = Query( + const query = Query( + // ignore: deprecated_member_use providerArgs: { 'request': RestRequest( url: '/', @@ -144,7 +173,8 @@ void main() { ); final instance = DemoRestModel('Guy'); - final query = Query( + const query = Query( + // ignore: deprecated_member_use providerArgs: { 'request': RestRequest( topLevelKey: 'top', diff --git a/packages/brick_rest_generators/CHANGELOG.md b/packages/brick_rest_generators/CHANGELOG.md index 6fbee7b0..8b8beefb 100644 --- a/packages/brick_rest_generators/CHANGELOG.md +++ b/packages/brick_rest_generators/CHANGELOG.md @@ -1,5 +1,10 @@ ## Unreleased +## 3.3.0 + +- Upgrade `brick_core` to `1.3.0` +- Update analysis to modern lints + ## 3.2.1 - Use `renameField` from `brick_build`'s `AnnotationFinderWithFieldRename` mixin diff --git a/packages/brick_rest_generators/lib/rest_model_serdes_generator.dart b/packages/brick_rest_generators/lib/rest_model_serdes_generator.dart index 421a4804..2c916275 100644 --- a/packages/brick_rest_generators/lib/rest_model_serdes_generator.dart +++ b/packages/brick_rest_generators/lib/rest_model_serdes_generator.dart @@ -14,6 +14,8 @@ class RestModelSerdesGenerator extends ProviderSerializableGenerator { + /// Generate a function to produce a [ClassElement] from REST data RestDeserialize( super.element, super.fields, { diff --git a/packages/brick_rest_generators/lib/src/rest_fields.dart b/packages/brick_rest_generators/lib/src/rest_fields.dart index 790d1cc5..5a2abc89 100644 --- a/packages/brick_rest_generators/lib/src/rest_fields.dart +++ b/packages/brick_rest_generators/lib/src/rest_fields.dart @@ -8,12 +8,14 @@ import 'package:brick_rest_generators/src/rest_serializable_extended.dart'; /// Find `@Rest` given a field class RestAnnotationFinder extends AnnotationFinder with AnnotationFinderWithFieldRename { + /// final RestSerializable? config; + /// Find `@Rest` given a field RestAnnotationFinder([this.config]); @override - Rest from(element) { + Rest from(FieldElement element) { final obj = objectForField(element); if (obj == null) { @@ -51,8 +53,11 @@ class RestAnnotationFinder extends AnnotationFinder class RestFields extends FieldsForClass { @override final RestAnnotationFinder finder; + + /// final RestSerializableExtended? config; + /// Converts all fields to [Rest]s for later consumption RestFields(ClassElement element, [this.config]) : finder = RestAnnotationFinder(config), super(element: element); diff --git a/packages/brick_rest_generators/lib/src/rest_serdes_generator.dart b/packages/brick_rest_generators/lib/src/rest_serdes_generator.dart index c027060f..593ba340 100644 --- a/packages/brick_rest_generators/lib/src/rest_serdes_generator.dart +++ b/packages/brick_rest_generators/lib/src/rest_serdes_generator.dart @@ -2,7 +2,9 @@ import 'package:brick_json_generators/json_serdes_generator.dart'; import 'package:brick_rest/brick_rest.dart'; import 'package:brick_rest_generators/src/rest_fields.dart'; +/// abstract class RestSerdesGenerator extends JsonSerdesGenerator { + /// RestSerdesGenerator( super.element, RestFields super.fields, { diff --git a/packages/brick_rest_generators/lib/src/rest_serializable_extended.dart b/packages/brick_rest_generators/lib/src/rest_serializable_extended.dart index f9bc685e..6fd89964 100644 --- a/packages/brick_rest_generators/lib/src/rest_serializable_extended.dart +++ b/packages/brick_rest_generators/lib/src/rest_serializable_extended.dart @@ -4,8 +4,10 @@ import 'package:brick_rest/brick_rest.dart'; /// however, the function can't be re-interpreted by ConstantReader. /// So the name is grabbed to be used in a later generator. class RestSerializableExtended extends RestSerializable { + /// final String? requestName; + /// const RestSerializableExtended({ super.fieldRename, super.nullable, diff --git a/packages/brick_rest_generators/lib/src/rest_serialize.dart b/packages/brick_rest_generators/lib/src/rest_serialize.dart index 9392c54b..884cde31 100644 --- a/packages/brick_rest_generators/lib/src/rest_serialize.dart +++ b/packages/brick_rest_generators/lib/src/rest_serialize.dart @@ -5,6 +5,7 @@ import 'package:brick_rest_generators/src/rest_serdes_generator.dart'; /// Generate a function to produce a [ClassElement] to REST data class RestSerialize extends RestSerdesGenerator with JsonSerialize { + /// RestSerialize( super.element, super.fields, { diff --git a/packages/brick_rest_generators/pubspec.yaml b/packages/brick_rest_generators/pubspec.yaml index 43de0df8..bdd43532 100644 --- a/packages/brick_rest_generators/pubspec.yaml +++ b/packages/brick_rest_generators/pubspec.yaml @@ -4,7 +4,7 @@ homepage: https://github.com/GetDutchie/brick/tree/main/packages/brick_rest_gene issue_tracker: https://github.com/GetDutchie/brick/issues repository: https://github.com/GetDutchie/brick -version: 3.2.1 +version: 3.3.0 environment: sdk: ">=2.18.0 <4.0.0" @@ -12,7 +12,7 @@ environment: dependencies: analyzer: ">=6.0.0 <7.0.0" brick_build: ">=3.2.0 <4.0.0" - brick_core: ">=1.2.1 <2.0.0" + brick_core: ">=1.3.0 <2.0.0" brick_json_generators: ">=3.0.0 <4.0.0" brick_rest: ">=3.0.4 <4.0.0" build: ">=2.0.0 <3.0.0" @@ -20,7 +20,7 @@ dependencies: source_gen: ">=1.2.2 <2.0.0" dev_dependencies: - test: ^1.20.1 - lints: ^2.0.1 brick_build_test: path: ../brick_build_test + lints: + test: diff --git a/packages/brick_rest_generators/test/rest_model_serdes_generator/test_constructor_member_field_mismatch.dart b/packages/brick_rest_generators/test/rest_model_serdes_generator/test_constructor_member_field_mismatch.dart index 7cff47f7..9781efd9 100644 --- a/packages/brick_rest_generators/test/rest_model_serdes_generator/test_constructor_member_field_mismatch.dart +++ b/packages/brick_rest_generators/test/rest_model_serdes_generator/test_constructor_member_field_mismatch.dart @@ -1,6 +1,6 @@ import 'package:brick_rest/brick_rest.dart'; -final output = r''' +const output = r''' Future _$RestConstructorMemberFieldMismatchFromRest(Map data, {required RestProvider provider, diff --git a/packages/brick_rest_generators/test/rest_model_serdes_generator/test_enum_as_string.dart b/packages/brick_rest_generators/test/rest_model_serdes_generator/test_enum_as_string.dart index c6f3b554..5ac05580 100644 --- a/packages/brick_rest_generators/test/rest_model_serdes_generator/test_enum_as_string.dart +++ b/packages/brick_rest_generators/test/rest_model_serdes_generator/test_enum_as_string.dart @@ -1,6 +1,6 @@ import 'package:brick_rest/brick_rest.dart'; -final output = r''' +const output = r''' Future _$EnumAsStringFromRest(Map data, {required RestProvider provider, RestFirstRepository? repository}) async { return EnumAsString( diff --git a/packages/brick_rest_generators/test/rest_model_serdes_generator/test_ignore_from_to.dart b/packages/brick_rest_generators/test/rest_model_serdes_generator/test_ignore_from_to.dart index 41d26d5d..85ac951d 100644 --- a/packages/brick_rest_generators/test/rest_model_serdes_generator/test_ignore_from_to.dart +++ b/packages/brick_rest_generators/test/rest_model_serdes_generator/test_ignore_from_to.dart @@ -1,6 +1,6 @@ import 'package:brick_rest/brick_rest.dart'; -final output = r''' +const output = r''' Future _$RestIgnoreFromToFromRest(Map data, {required RestProvider provider, RestFirstRepository? repository}) async { return RestIgnoreFromTo( diff --git a/packages/brick_rest_generators/test/rest_model_serdes_generator/test_unserializable_field_with_generator.dart b/packages/brick_rest_generators/test/rest_model_serdes_generator/test_unserializable_field_with_generator.dart index a67923cf..0482c507 100644 --- a/packages/brick_rest_generators/test/rest_model_serdes_generator/test_unserializable_field_with_generator.dart +++ b/packages/brick_rest_generators/test/rest_model_serdes_generator/test_unserializable_field_with_generator.dart @@ -3,7 +3,7 @@ import 'dart:typed_data'; import 'package:brick_rest/brick_rest.dart'; -final output = r''' +const output = r''' Future _$RestUnserializableFieldWithGeneratorFromRest(Map data, {required RestProvider provider, diff --git a/packages/brick_rest_generators/test/rest_model_serdes_generator_test.dart b/packages/brick_rest_generators/test/rest_model_serdes_generator_test.dart index e46d241b..e5def7c5 100644 --- a/packages/brick_rest_generators/test/rest_model_serdes_generator_test.dart +++ b/packages/brick_rest_generators/test/rest_model_serdes_generator_test.dart @@ -14,7 +14,7 @@ import 'rest_model_serdes_generator/test_unserializable_field_with_generator.dar as unserializable_field_with_generator; final _generator = TestGenerator(); -final folder = 'rest_model_serdes_generator'; +const folder = 'rest_model_serdes_generator'; final generateReader = generateLibraryForFolder(folder); void main() { diff --git a/packages/brick_sqlite/CHANGELOG.md b/packages/brick_sqlite/CHANGELOG.md index 98c45907..03601644 100644 --- a/packages/brick_sqlite/CHANGELOG.md +++ b/packages/brick_sqlite/CHANGELOG.md @@ -1,5 +1,24 @@ ## Unreleased +## 3.2.1 + +- Remove `dart:io` dependency + +## 3.2.0 + +- **DEPRECATION** `Query(providerArgs: {'collate':})` is now `Query(forProviders: [SqliteProviderQuery(collate:)])` +- **DEPRECATION** `Query(providerArgs: {'having':})` is now `Query(forProviders: [SqliteProviderQuery(having:)])` +- **DEPRECATION** `Query(providerArgs: {'groupBy':})` is now `Query(forProviders: [SqliteProviderQuery(groupBy:)])` +- Association ordering is supported. For example, `Query(orderBy: [OrderBy.desc('assoc', associationField: 'name')])` on `DemoModel` will produce the following SQL statement: + ```sql + 'SELECT DISTINCT `DemoModel`.* FROM `DemoModel` ORDER BY `DemoModelAssoc`.name DESC' + ``` +- New `SqliteProviderQuery` adds Sqlite-specific support for the new `Query`. +- `Column` enum is enhanced, performing the conversion between Dart and SQLite column types on the enum instead of in `Migration`. +- Barrel files are no longer imported to `src/` implementations +- Upgrade `brick_core` to `1.3.0` +- Update analysis to modern lints + ## 3.1.1 - Expose a generic type for `MemoryCacheProvider` models diff --git a/packages/brick_sqlite/README.md b/packages/brick_sqlite/README.md index 8a1b86ce..f2c4c04b 100644 --- a/packages/brick_sqlite/README.md +++ b/packages/brick_sqlite/README.md @@ -26,7 +26,7 @@ final String lastName; Query( where: [Where.exact('lastName', 'Mustermann')], - providerArgs: {'orderBy': 'lastName ASC'}, + orderBy: [OrderBy.asc('lastName')] ) ``` diff --git a/packages/brick_sqlite/lib/brick_sqlite.dart b/packages/brick_sqlite/lib/brick_sqlite.dart index 560f06aa..58d9f749 100644 --- a/packages/brick_sqlite/lib/brick_sqlite.dart +++ b/packages/brick_sqlite/lib/brick_sqlite.dart @@ -5,3 +5,4 @@ export 'package:brick_sqlite/src/runtime_sqlite_column_definition.dart'; export 'package:brick_sqlite/src/sqlite_adapter.dart'; export 'package:brick_sqlite/src/sqlite_model_dictionary.dart'; export 'package:brick_sqlite/src/sqlite_provider.dart'; +export 'package:brick_sqlite/src/sqlite_provider_query.dart'; diff --git a/packages/brick_sqlite/lib/db.dart b/packages/brick_sqlite/lib/db.dart index 972f4393..fc6ff6b5 100644 --- a/packages/brick_sqlite/lib/db.dart +++ b/packages/brick_sqlite/lib/db.dart @@ -1,3 +1,4 @@ +export 'package:brick_sqlite/src/db/column.dart'; export 'package:brick_sqlite/src/db/migratable.dart'; export 'package:brick_sqlite/src/db/migration.dart'; export 'package:brick_sqlite/src/db/migration_commands/create_index.dart'; diff --git a/packages/brick_sqlite/lib/memory_cache_provider.dart b/packages/brick_sqlite/lib/memory_cache_provider.dart index 9ac8f1f1..944bb881 100644 --- a/packages/brick_sqlite/lib/memory_cache_provider.dart +++ b/packages/brick_sqlite/lib/memory_cache_provider.dart @@ -10,6 +10,7 @@ import 'package:meta/meta.dart'; /// MemoryCacheProvider does not have a type argument due to a build_runner /// exception: https://github.com/dart-lang/sdk/issues/38309 class MemoryCacheProvider extends Provider { + /// @protected final Logger logger = Logger('MemoryCacheProvider'); @@ -29,7 +30,7 @@ class MemoryCacheProvider extends Provider managedModelTypes.contains(type); /// It is strongly recommended to use this provider with smaller, frequently-accessed - /// and shared [TModel]s. + /// and shared [TProviderModel]s. MemoryCacheProvider([ this.managedModelTypes = const [], ]); @@ -46,7 +47,11 @@ class MemoryCacheProvider extends Provider(instance, {query, repository}) { + bool delete( + TModel instance, { + Query? query, + ModelRepository? repository, + }) { if (!manages(TModel)) return false; logger.finest('#delete: $TModel, $instance, $query'); @@ -56,7 +61,10 @@ class MemoryCacheProvider extends Provider? get({query, repository}) { + List? get({ + Query? query, + ModelRepository? repository, + }) { if (!manages(TModel)) return null; managedObjects[TModel] ??= {}; @@ -98,11 +106,15 @@ class MemoryCacheProvider extends Provider(instance, {query, repository}) { + TModel? upsert( + TModel instance, { + Query? query, + ModelRepository? repository, + }) { if (!manages(TModel)) return null; logger.finest('#upsert: $TModel, $instance, $query'); hydrate([instance]); - return managedObjects[TModel]![instance.primaryKey] as TModel; + return managedObjects[TModel]![instance.primaryKey]! as TModel; } } diff --git a/packages/brick_sqlite/lib/src/annotations/sqlite.dart b/packages/brick_sqlite/lib/src/annotations/sqlite.dart index 3b7c9251..3c5179f7 100644 --- a/packages/brick_sqlite/lib/src/annotations/sqlite.dart +++ b/packages/brick_sqlite/lib/src/annotations/sqlite.dart @@ -1,7 +1,8 @@ import 'package:brick_core/field_serializable.dart'; -import 'package:brick_sqlite/src/db/migration.dart' show Column; +import 'package:brick_sqlite/src/annotations/sqlite_serializable.dart'; +import 'package:brick_sqlite/src/db/column.dart'; -export 'package:brick_sqlite/src/db/migration.dart' show Column; +export 'package:brick_sqlite/src/db/column.dart'; /// An annotation used to specify how a field is serialized. /// Heavily inspired by [JsonKey](https://github.com/dart-lang/json_serializable/blob/master/json_annotation/lib/src/json_key.dart) @@ -62,7 +63,7 @@ class Sqlite implements FieldSerializable { @override final bool nullable; - /// When true, deletion of the referenced record by [foreignKeyColumn] on the [foreignTableName] + /// When true, deletion of the referenced record by `foreignKeyColumn` on the `foreignTableName` /// this record. For example, if the foreign table is "departments" and the local table /// is "employees," whenever that department is deleted, "employee" /// will be deleted. Defaults `false`. diff --git a/packages/brick_sqlite/lib/src/annotations/sqlite_serializable.dart b/packages/brick_sqlite/lib/src/annotations/sqlite_serializable.dart index 58f24e71..88da54ab 100644 --- a/packages/brick_sqlite/lib/src/annotations/sqlite_serializable.dart +++ b/packages/brick_sqlite/lib/src/annotations/sqlite_serializable.dart @@ -1,3 +1,5 @@ +import 'package:brick_sqlite/src/annotations/sqlite.dart'; + /// An annotation used to specify a class to generate code for. /// /// Creates a serialize/deserialize function and a Schema output diff --git a/packages/brick_sqlite/lib/src/db/column.dart b/packages/brick_sqlite/lib/src/db/column.dart new file mode 100644 index 00000000..4dbfe118 --- /dev/null +++ b/packages/brick_sqlite/lib/src/db/column.dart @@ -0,0 +1,75 @@ +// Heavily, heavily inspired by [Aqueduct](https://github.com/stablekernel/aqueduct/blob/master/aqueduct/lib/src/db/schema/migration.dart) + +import 'dart:core' as core; +import 'dart:core'; + +/// SQLite data types. +/// +/// While SQLite only supports 5 datatypes, it will still cast these +/// into an [intelligent affinity](https://www.sqlite.org/datatype3.html). +enum Column { + /// No data type + undefined._('', dynamic), + + /// + bigint._('BIGINT', core.num), + + /// + blob._('BLOB', List), + + /// + boolean._('BOOLEAN', bool), + + /// + date._('DATE', DateTime), + + /// + datetime._('DATETIME', DateTime), + + /// + // ignore: constant_identifier_names + Double._('DOUBLE', double), + + /// + integer._('INTEGER', int), + + /// + float._('FLOAT', core.num), + + /// + num._('DOUBLE', core.num), + + /// + text._('TEXT', String), + + /// + varchar._('VARCHAR', String); + + /// The equivalent Dart primitive + final Type dartType; + + /// SQLite equivalent type + final core.String definition; + + const Column._(this.definition, this.dartType); + + /// Convert native Dart to `Column` + factory Column.fromDartPrimitive(Type type) { + switch (type) { + case bool: + return Column.boolean; + case DateTime: + return Column.datetime; + case double: + return Column.Double; + case int: + return Column.integer; + case core.num: + return Column.num; + case String: + return Column.varchar; + default: + return throw ArgumentError('$type not associated with a Column'); + } + } +} diff --git a/packages/brick_sqlite/lib/src/db/migratable.dart b/packages/brick_sqlite/lib/src/db/migratable.dart index af67c87f..b987b2b7 100644 --- a/packages/brick_sqlite/lib/src/db/migratable.dart +++ b/packages/brick_sqlite/lib/src/db/migratable.dart @@ -1,3 +1,4 @@ +import 'package:brick_sqlite/src/db/migration.dart'; import 'package:brick_sqlite/src/db/migration_commands/migration_command.dart'; /// Annotation required by the generator for AOT discoverability. Decorates classes @@ -18,6 +19,8 @@ class Migratable { /// the version should still match `RegExp("^\d+$")`. final String version; + /// Annotation required by the generator for AOT discoverability. Decorates classes + /// that `extends Migration`. const Migratable({ required this.down, required this.up, diff --git a/packages/brick_sqlite/lib/src/db/migration.dart b/packages/brick_sqlite/lib/src/db/migration.dart index b4bd9db9..974ebb43 100644 --- a/packages/brick_sqlite/lib/src/db/migration.dart +++ b/packages/brick_sqlite/lib/src/db/migration.dart @@ -1,126 +1,35 @@ // Heavily, heavily inspired by [Aqueduct](https://github.com/stablekernel/aqueduct/blob/master/aqueduct/lib/src/db/schema/migration.dart) - import 'package:brick_sqlite/src/db/migration_commands/migration_command.dart'; +import 'package:brick_sqlite/src/db/schema/schema.dart'; -/// SQLite data types. -/// -/// While SQLite only supports 5 datatypes, it will still cast these -/// into an [intelligent affinity](https://www.sqlite.org/datatype3.html). -enum Column { - undefined, - bigint, - blob, - boolean, - date, - datetime, - // ignore: constant_identifier_names - Double, - integer, - float, - - /// DOUBLE column type is used - num, - text, - varchar -} - +/// A collection of [MigrationCommand]s to update the [Schema]. abstract class Migration { /// Order to run; should be unique and sequential with other [Migration]s final int version; + /// Desired changes to the [Schema]. final List up; + + /// Reverts [up] final List down; + /// A collection of [MigrationCommand]s to update the [Schema]. const Migration({ required this.version, required this.up, required this.down, }); + /// Alias of [upStatement] String get statement => upStatement; + /// Generate SQL statements for all commands String get upStatement => '${up.map((c) => c.statement).join(';\n')};'; + /// Revert of [upStatement] String get downStatement => '${down.map((c) => c.statement).join(';\n')};'; - /// Convert `Column` to SQLite data types - static String ofDefinition(Column definition) { - switch (definition) { - case Column.bigint: - return 'BIGINT'; - case Column.boolean: - return 'BOOLEAN'; - case Column.blob: - return 'BLOB'; - case Column.date: - return 'DATE'; - case Column.datetime: - return 'DATETIME'; - case Column.Double: - case Column.num: - return 'DOUBLE'; - case Column.integer: - return 'INTEGER'; - case Column.float: - return 'FLOAT'; - case Column.text: - return 'TEXT'; - case Column.varchar: - return 'VARCHAR'; - default: - return throw ArgumentError('$definition not found in Column'); - } - } - - /// Convert native Dart to `Column` - static Column fromDartPrimitive(Type type) { - switch (type) { - case bool: - return Column.boolean; - case DateTime: - return Column.datetime; - case double: - return Column.Double; - case int: - return Column.integer; - case num: - return Column.num; - case String: - return Column.varchar; - default: - return throw ArgumentError('$type not associated with a Column'); - } - } - - /// Convert `Column` to native Dart - static Type toDartPrimitive(Column definition) { - switch (definition) { - case Column.bigint: - return num; - case Column.boolean: - return bool; - case Column.blob: - return List; - case Column.date: - return DateTime; - case Column.datetime: - return DateTime; - case Column.Double: - return double; - case Column.integer: - return int; - case Column.float: - case Column.num: - return num; - case Column.text: - return String; - case Column.varchar: - return String; - default: - return throw ArgumentError('$definition not found in Column'); - } - } - + /// SQL command to produce the migration static String generate(List commands, int version) { final upCommands = commands.map((m) => m.forGenerator); final downCommands = commands.map((m) => m.down?.forGenerator).toList().whereType(); diff --git a/packages/brick_sqlite/lib/src/db/migration_commands/create_index.dart b/packages/brick_sqlite/lib/src/db/migration_commands/create_index.dart index b5493001..45ed1d60 100644 --- a/packages/brick_sqlite/lib/src/db/migration_commands/create_index.dart +++ b/packages/brick_sqlite/lib/src/db/migration_commands/create_index.dart @@ -3,10 +3,13 @@ import 'package:brick_sqlite/src/db/migration_commands/migration_command.dart'; /// Create an index on a table if it doesn't already exists class CreateIndex extends MigrationCommand { + /// final List columns; + /// final String onTable; + /// final bool unique; /// As a migration, this may fail if existing data is in conflict with the index. @@ -18,6 +21,7 @@ class CreateIndex extends MigrationCommand { this.unique = false, }); + /// String get name => generateName(columns, onTable); @override @@ -37,6 +41,7 @@ class CreateIndex extends MigrationCommand { @override MigrationCommand get down => DropIndex(name); + /// Combines columns and table name to create an index name static String generateName(List columns, String onTable) { final columnNames = columns.join('_'); return ['index', onTable, 'on', columnNames].join('_'); diff --git a/packages/brick_sqlite/lib/src/db/migration_commands/drop_column.dart b/packages/brick_sqlite/lib/src/db/migration_commands/drop_column.dart index ee468770..05d0408b 100644 --- a/packages/brick_sqlite/lib/src/db/migration_commands/drop_column.dart +++ b/packages/brick_sqlite/lib/src/db/migration_commands/drop_column.dart @@ -4,9 +4,15 @@ import 'package:brick_sqlite/src/db/migration_commands/migration_command.dart'; /// columns prefixed by `_should_drop` and generate a statement that includes the schema of /// the full table to be `ALTER`ed. class DropColumn extends MigrationCommand { + /// final String name; + + /// final String onTable; + /// SQLite doesn't have a catch-all drop column command. On migrate, the provider can search for + /// columns prefixed by `_should_drop` and generate a statement that includes the schema of + /// the full table to be `ALTER`ed. const DropColumn( this.name, { required this.onTable, diff --git a/packages/brick_sqlite/lib/src/db/migration_commands/drop_index.dart b/packages/brick_sqlite/lib/src/db/migration_commands/drop_index.dart index dcc03de0..c8200391 100644 --- a/packages/brick_sqlite/lib/src/db/migration_commands/drop_index.dart +++ b/packages/brick_sqlite/lib/src/db/migration_commands/drop_index.dart @@ -2,8 +2,10 @@ import 'package:brick_sqlite/src/db/migration_commands/migration_command.dart'; /// Drop index from DB if it exists class DropIndex extends MigrationCommand { + /// final String name; + /// Drop index from DB if it exists const DropIndex(this.name); @override diff --git a/packages/brick_sqlite/lib/src/db/migration_commands/drop_table.dart b/packages/brick_sqlite/lib/src/db/migration_commands/drop_table.dart index 7a2348e9..40b8903a 100644 --- a/packages/brick_sqlite/lib/src/db/migration_commands/drop_table.dart +++ b/packages/brick_sqlite/lib/src/db/migration_commands/drop_table.dart @@ -3,8 +3,10 @@ import 'package:brick_sqlite/src/db/migration_commands/migration_command.dart'; /// Drop table from DB if it exists class DropTable extends MigrationCommand { + /// final String name; + /// Drop table from DB if it exists const DropTable(this.name); @override diff --git a/packages/brick_sqlite/lib/src/db/migration_commands/insert_column.dart b/packages/brick_sqlite/lib/src/db/migration_commands/insert_column.dart index 61500537..b90855e5 100644 --- a/packages/brick_sqlite/lib/src/db/migration_commands/insert_column.dart +++ b/packages/brick_sqlite/lib/src/db/migration_commands/insert_column.dart @@ -1,11 +1,16 @@ -import 'package:brick_sqlite/src/db/migration.dart'; +import 'package:brick_sqlite/src/db/column.dart'; import 'package:brick_sqlite/src/db/migration_commands/drop_column.dart'; import 'package:brick_sqlite/src/db/migration_commands/migration_command.dart'; /// Creates a new SQLite column in a table class InsertColumn extends MigrationCommand { + /// final String name; + + /// Column type final Column definitionType; + + /// final String onTable; /// Column can be `NULL`. Defaults `true`. @@ -20,6 +25,7 @@ class InsertColumn extends MigrationCommand { /// Column has `UNIQUE` constraint. Defaults `false`. final bool unique; + /// Creates a new SQLite column in a table const InsertColumn( this.name, this.definitionType, { @@ -39,16 +45,19 @@ class InsertColumn extends MigrationCommand { } String get _nullStatement => nullable ? 'NULL' : 'NOT NULL'; + String? get _autoincrementStatement { if (!autoincrement) return null; return 'AUTOINCREMENT'; } - String get definition => Migration.ofDefinition(definitionType); + /// + String get definition => definitionType.definition; + String get _addons { - final list = [_autoincrementStatement, _nullStatement, _defaultStatement]; - list.removeWhere((s) => s == null); + final list = [_autoincrementStatement, _nullStatement, _defaultStatement] + ..removeWhere((s) => s == null); return list.join(' '); } @@ -85,12 +94,10 @@ class InsertColumn extends MigrationCommand { @override MigrationCommand get down => DropColumn(name, onTable: onTable); + /// Defaults static const InsertColumn defaults = InsertColumn( 'PLACEHOLDER', Column.varchar, onTable: 'PLACEHOLDER', - autoincrement: false, - nullable: true, - unique: false, ); } diff --git a/packages/brick_sqlite/lib/src/db/migration_commands/insert_foreign_key.dart b/packages/brick_sqlite/lib/src/db/migration_commands/insert_foreign_key.dart index e6028a17..e5bb4ec3 100644 --- a/packages/brick_sqlite/lib/src/db/migration_commands/insert_foreign_key.dart +++ b/packages/brick_sqlite/lib/src/db/migration_commands/insert_foreign_key.dart @@ -4,7 +4,10 @@ import 'package:brick_sqlite/src/db/migration_commands/migration_command.dart'; /// Create a foreign key column to reference another table class InsertForeignKey extends MigrationCommand { + /// Table where the foreign key is defined final String localTableName; + + /// Table referenced by the foreign key final String foreignTableName; /// Defaults to lowercase `${foreignTableName}_brick_id` @@ -20,6 +23,7 @@ class InsertForeignKey extends MigrationCommand { /// usually `NULL` unless otherwise declared. Defaults `false`. final bool onDeleteSetDefault; + /// Create a foreign key column to reference another table const InsertForeignKey( this.localTableName, this.foreignTableName, { @@ -76,9 +80,8 @@ class InsertForeignKey extends MigrationCommand { /// The downside of this pattern is the inevitable data duplication for such many-to-many /// relationships and the inability to query relationships without declaring them on /// parent/child models. - static String joinsTableName(String columnName, {required String localTableName}) { - return ['_brick', localTableName, columnName].join('_'); - } + static String joinsTableName(String columnName, {required String localTableName}) => + ['_brick', localTableName, columnName].join('_'); /// In the rare case of a many-to-many association of the same model, the columns must be prefixed. /// For example, `final List friends` on class `Friend`. diff --git a/packages/brick_sqlite/lib/src/db/migration_commands/insert_table.dart b/packages/brick_sqlite/lib/src/db/migration_commands/insert_table.dart index d432a72c..1000f98b 100644 --- a/packages/brick_sqlite/lib/src/db/migration_commands/insert_table.dart +++ b/packages/brick_sqlite/lib/src/db/migration_commands/insert_table.dart @@ -3,8 +3,10 @@ import 'package:brick_sqlite/src/db/migration_commands/migration_command.dart'; /// Insert table if it doesn't already exist class InsertTable extends MigrationCommand { + /// final String name; + /// Insert table if it doesn't already exist const InsertTable(this.name); @override @@ -20,6 +22,8 @@ class InsertTable extends MigrationCommand { /// Automatically aliased to [rowid](https://www.sqlite.org/lang_createtable.html#rowid). // ignore: constant_identifier_names static const PRIMARY_KEY_COLUMN = '_brick_id'; + + /// Dart field name of the primary key, pulled from the [PRIMARY_KEY_COLUMN] // ignore: constant_identifier_names static const PRIMARY_KEY_FIELD = 'primaryKey'; } diff --git a/packages/brick_sqlite/lib/src/db/migration_commands/migration_command.dart b/packages/brick_sqlite/lib/src/db/migration_commands/migration_command.dart index 33d1218c..f7281241 100644 --- a/packages/brick_sqlite/lib/src/db/migration_commands/migration_command.dart +++ b/packages/brick_sqlite/lib/src/db/migration_commands/migration_command.dart @@ -9,6 +9,7 @@ abstract class MigrationCommand { /// Outputs the opposite command to be used in a generator MigrationCommand? get down => null; + /// Extendible interface for SQLite migrations const MigrationCommand(); /// Alias for [statement] diff --git a/packages/brick_sqlite/lib/src/db/migration_commands/rename_column.dart b/packages/brick_sqlite/lib/src/db/migration_commands/rename_column.dart index 405e1b06..e940b808 100644 --- a/packages/brick_sqlite/lib/src/db/migration_commands/rename_column.dart +++ b/packages/brick_sqlite/lib/src/db/migration_commands/rename_column.dart @@ -2,10 +2,16 @@ import 'package:brick_sqlite/src/db/migration_commands/migration_command.dart'; /// Renames an existing SQLite column in a table class RenameColumn extends MigrationCommand { + /// final String oldName; + + /// final String newName; + + /// final String onTable; + /// Renames an existing SQLite column in a table const RenameColumn( this.oldName, this.newName, { diff --git a/packages/brick_sqlite/lib/src/db/migration_commands/rename_table.dart b/packages/brick_sqlite/lib/src/db/migration_commands/rename_table.dart index 6b09667e..3e474710 100644 --- a/packages/brick_sqlite/lib/src/db/migration_commands/rename_table.dart +++ b/packages/brick_sqlite/lib/src/db/migration_commands/rename_table.dart @@ -2,9 +2,13 @@ import 'package:brick_sqlite/src/db/migration_commands/migration_command.dart'; /// Renames an existing SQLite table class RenameTable extends MigrationCommand { + /// final String oldName; + + /// final String newName; + /// Renames an existing SQLite table const RenameTable( this.oldName, this.newName, diff --git a/packages/brick_sqlite/lib/src/db/migration_manager.dart b/packages/brick_sqlite/lib/src/db/migration_manager.dart index e4f26f41..d2835cdd 100644 --- a/packages/brick_sqlite/lib/src/db/migration_manager.dart +++ b/packages/brick_sqlite/lib/src/db/migration_manager.dart @@ -1,48 +1,40 @@ import 'package:brick_sqlite/src/db/migration.dart'; +import 'package:brick_sqlite/src/db/schema/schema.dart'; import 'package:meta/meta.dart'; /// Holds all migrations and outputs statements for SQLite to consume class MigrationManager { + /// @protected final Set migrations; + /// Holds all migrations and outputs statements for SQLite to consume const MigrationManager(this.migrations); /// Identifies the latest migrations, especially those not yet added to the [Schema] /// The delta between [Schema]'s version and [MigrationManager]'s are unprocessed migrations - int get version { - return latestMigrationVersion(migrations); - } + int get version => latestMigrationVersion(migrations); /// Key/value migrations based on their version - Map get migrationByVersion { - return {for (var m in migrations) m.version: m}; - } + Map get migrationByVersion => {for (final m in migrations) m.version: m}; /// Migrations after a version /// /// [versionNumber] defaults to [version] List migrationsSince([int? versionNumber]) { final number = versionNumber ?? version; - final validMigrations = migrations.where((m) => m.version > number).toList(); - validMigrations.sort((a, b) => a.version.compareTo(b.version)); - return validMigrations; + return migrations.where((m) => m.version > number).toList() + ..sort((a, b) => a.version.compareTo(b.version)); } /// Migrations before and including a version /// /// [versionNumber] defaults to [version] - Map migrationsUntil([int? versionNumber]) { - return migrationByVersion - ..removeWhere((version, _) { - return version > (versionNumber ?? version); - }); - } + Map migrationsUntil([int? versionNumber]) => + migrationByVersion..removeWhere((version, _) => version > (versionNumber ?? version)); /// Migration at a version - Migration? migrationAt(int versionNumber) { - return migrationByVersion[versionNumber]; - } + Migration? migrationAt(int versionNumber) => migrationByVersion[versionNumber]; /// Sort migrations by their version number in ascending order /// and return the latest [Migration] version or `0` if [allMigrations] is empty @@ -51,8 +43,7 @@ class MigrationManager { return 0; } - final versions = allMigrations.map((m) => m.version).toList(); - versions.sort(); + final versions = allMigrations.map((m) => m.version).toList()..sort(); return versions.last; } } diff --git a/packages/brick_sqlite/lib/src/db/schema/schema.dart b/packages/brick_sqlite/lib/src/db/schema/schema.dart index 7eb13418..6fb85e98 100644 --- a/packages/brick_sqlite/lib/src/db/schema/schema.dart +++ b/packages/brick_sqlite/lib/src/db/schema/schema.dart @@ -1,5 +1,6 @@ // Heavily, heavily inspired by [Aqueduct](https://github.com/stablekernel/aqueduct/blob/master/aqueduct/lib/src/db/schema/schema_builder.dart) // Unfortunately, some key differences such as inability to use mirrors and the sqlite vs postgres capabilities make DIY a more palatable option than retrofitting +import 'package:brick_sqlite/src/db/column.dart'; import 'package:brick_sqlite/src/db/migration.dart'; import 'package:brick_sqlite/src/db/migration_commands/create_index.dart'; import 'package:brick_sqlite/src/db/migration_commands/drop_column.dart'; @@ -17,32 +18,37 @@ import 'package:brick_sqlite/src/db/schema/schema_index.dart'; import 'package:brick_sqlite/src/db/schema/schema_table.dart'; import 'package:meta/meta.dart' show visibleForTesting; +/// A definition of all the tables and columns in the SQLite database class Schema { /// The last version successfully migrated to SQLite. /// This should be before or equal to [MigrationManager]'s `#version`. /// if [MigrationManager] is used. final int version; + /// final Set tables; /// Version used to produce this scheme final int generatorVersion; + /// A definition of all the tables and columns in the SQLite database Schema(this.version, {required this.tables, this.generatorVersion = GENERATOR_VERSION}); + /// Generator used to produce this schema; not intended for public use // ignore: constant_identifier_names static const int GENERATOR_VERSION = 1; + /// @visibleForTesting static List expandMigrations(Set migrations) { - final sorted = migrations.toList(); - sorted.sort((a, b) { - if (a.version == b.version) { - return 0; - } + final sorted = migrations.toList() + ..sort((a, b) { + if (a.version == b.version) { + return 0; + } - return a.version > b.version ? 1 : -1; - }); + return a.version > b.version ? 1 : -1; + }); return sorted.map((m) => m.up).expand((c) => c).toList(); } @@ -50,7 +56,7 @@ class Schema { /// Create a schema from a set of migrations. If [version] is not provided, /// the highest migration version will be used factory Schema.fromMigrations(Set migrations, [int? version]) { - assert((version == null) || (version > -1)); + assert((version == null) || (version > -1), 'version must be greater than -1'); version = version ?? MigrationManager.latestMigrationVersion(migrations); final commands = expandMigrations(migrations); final tables = commands.fold({}, _commandToSchema); @@ -61,14 +67,12 @@ class Schema { ); } - /// A sub-function of [fromMigrations], convert a migration command into a `SchemaObject`. + /// A sub-function of [Schema.fromMigrations], convert a migration command into a `SchemaObject`. static Set _commandToSchema(Set tables, MigrationCommand command) { - SchemaTable findTable(String tableName) { - return tables.firstWhere( - (s) => s.name == tableName, - orElse: () => throw StateError('Table $tableName must be inserted first'), - ); - } + SchemaTable findTable(String tableName) => tables.firstWhere( + (s) => s.name == tableName, + orElse: () => throw StateError('Table $tableName must be inserted first'), + ); if (command is InsertTable) { tables.add(SchemaTable(command.name)); @@ -86,6 +90,7 @@ class Schema { } else if (command is RenameTable) { final table = findTable(command.oldName); tables.add(SchemaTable(command.newName, columns: table.columns..toSet())); + // ignore: cascade_invocations tables.remove(table); } else if (command is DropTable) { final table = findTable(command.name); @@ -98,7 +103,6 @@ class Schema { command.definitionType, autoincrement: command.autoincrement, defaultValue: command.defaultValue, - isPrimaryKey: false, nullable: command.nullable, unique: command.unique, ), diff --git a/packages/brick_sqlite/lib/src/db/schema/schema_base.dart b/packages/brick_sqlite/lib/src/db/schema/schema_base.dart index 94c74fe5..7a40687e 100644 --- a/packages/brick_sqlite/lib/src/db/schema/schema_base.dart +++ b/packages/brick_sqlite/lib/src/db/schema/schema_base.dart @@ -1,3 +1,4 @@ +import 'package:brick_sqlite/src/db/migration.dart'; import 'package:brick_sqlite/src/db/migration_commands/migration_command.dart'; /// Generates code for [Migration] from [BaseSchemaObject]ss diff --git a/packages/brick_sqlite/lib/src/db/schema/schema_column.dart b/packages/brick_sqlite/lib/src/db/schema/schema_column.dart index 28c3bb0e..e5515bcd 100644 --- a/packages/brick_sqlite/lib/src/db/schema/schema_column.dart +++ b/packages/brick_sqlite/lib/src/db/schema/schema_column.dart @@ -1,6 +1,6 @@ // Heavily, heavily inspired by [Aqueduct](https://github.com/stablekernel/aqueduct/blob/master/aqueduct/lib/src/db/schema/schema_builder.dart) // Unfortunately, some key differences such as inability to use mirrors and the sqlite vs postgres capabilities make DIY a more palatable option than retrofitting -import 'package:brick_sqlite/src/db/migration.dart' show Column; +import 'package:brick_sqlite/src/db/column.dart'; import 'package:brick_sqlite/src/db/migration_commands/drop_column.dart'; import 'package:brick_sqlite/src/db/migration_commands/insert_column.dart'; import 'package:brick_sqlite/src/db/migration_commands/insert_foreign_key.dart'; @@ -10,20 +10,44 @@ import 'package:brick_sqlite/src/db/schema/schema_base.dart'; /// Describes a column object managed by SQLite /// This should not exist outside of a SchemaTable class SchemaColumn extends BaseSchemaObject { + /// String name; + + /// If this column has an autoincrement value final bool autoincrement; + + /// final Column columnType; + + /// final dynamic defaultValue; + + /// final bool nullable; + + /// final bool isPrimaryKey; + + /// final bool isForeignKey; + + /// final String? foreignTableName; + + /// Remove row when a referenced foreign key is deleted final bool onDeleteCascade; + + /// Update column's value to default when a referenced foreign key is deleted final bool onDeleteSetDefault; + + /// If this column's value is unique within the table final bool unique; + /// String? tableName; + /// Describes a column object managed by SQLite + /// This should not exist outside of a SchemaTable SchemaColumn( this.name, this.columnType, { @@ -40,7 +64,10 @@ class SchemaColumn extends BaseSchemaObject { nullable = nullable ?? InsertColumn.defaults.nullable, unique = unique ?? InsertColumn.defaults.unique, assert(!isPrimaryKey || columnType == Column.integer, 'Primary key must be an integer'), - assert(!isForeignKey || (foreignTableName != null)); + assert( + !isForeignKey || (foreignTableName != null), + 'Foreign key must have a foreign table name', + ); @override String get forGenerator { @@ -63,10 +90,12 @@ class SchemaColumn extends BaseSchemaObject { } if (isForeignKey) { - parts.add('isForeignKey: $isForeignKey'); - parts.add("foreignTableName: '$foreignTableName'"); - parts.add('onDeleteCascade: $onDeleteCascade'); - parts.add('onDeleteSetDefault: $onDeleteSetDefault'); + parts.addAll([ + 'isForeignKey: $isForeignKey', + "foreignTableName: '$foreignTableName'", + 'onDeleteCascade: $onDeleteCascade', + 'onDeleteSetDefault: $onDeleteSetDefault', + ]); } if (unique != InsertColumn.defaults.unique) { diff --git a/packages/brick_sqlite/lib/src/db/schema/schema_difference.dart b/packages/brick_sqlite/lib/src/db/schema/schema_difference.dart index 1ab9bed7..0c2e4472 100644 --- a/packages/brick_sqlite/lib/src/db/schema/schema_difference.dart +++ b/packages/brick_sqlite/lib/src/db/schema/schema_difference.dart @@ -1,3 +1,4 @@ +import 'package:brick_sqlite/src/db/migration.dart'; import 'package:brick_sqlite/src/db/migration_commands/drop_column.dart'; import 'package:brick_sqlite/src/db/migration_commands/drop_table.dart'; import 'package:brick_sqlite/src/db/migration_commands/migration_command.dart'; @@ -9,23 +10,38 @@ import 'package:collection/collection.dart'; /// Compares two schemas to produce migrations that conver the difference class SchemaDifference { + /// final Schema oldSchema; + + /// final Schema newSchema; - SchemaDifference(this.oldSchema, this.newSchema) : assert(oldSchema.version < newSchema.version); + /// Compares two schemas to produce migrations that conver the difference + SchemaDifference(this.oldSchema, this.newSchema) + : assert( + oldSchema.version < newSchema.version, + 'Old schema is a newer version than the new schema', + ); + /// Set get droppedTables => oldSchema.tables.difference(newSchema.tables); + /// Set get insertedTables => newSchema.tables.difference(oldSchema.tables); + /// Set get droppedIndices => _compareIndices(oldSchema, newSchema); + /// Set get createdIndices => _compareIndices(newSchema, oldSchema); + /// Set get droppedColumns => _compareColumns(oldSchema, newSchema); + /// Set get insertedColumns => _compareColumns(newSchema, oldSchema); + /// If there is a significant difference between both schemas bool get hasDifference => droppedTables.isNotEmpty || insertedTables.isNotEmpty || @@ -36,27 +52,22 @@ class SchemaDifference { /// Generates migration commands from the schemas' differences List toMigrationCommands() { - final removedTables = droppedTables.map((item) { - return item.toCommand(shouldDrop: true); - }).cast(); + final removedTables = + droppedTables.map((item) => item.toCommand(shouldDrop: true)).cast(); - // TODO detect if dropped column is a foreign key joins association AND WRITE TEST + // TODOdetect if dropped column is a foreign key joins association AND WRITE TEST // Only drop column if the table isn't being dropped too final removedColumns = droppedColumns - .where((item) { - return !removedTables.any((command) => command.name == item.tableName); - }) + .where((item) => !removedTables.any((command) => command.name == item.tableName)) .map((c) => c.toCommand(shouldDrop: true)) .cast(); final addedColumns = insertedColumns.where((c) => !c.isPrimaryKey).toSet(); final added = [insertedTables, addedColumns] - .map((generatedSet) { - return generatedSet.map((item) { - return item.toCommand(); - }); - }) + .map( + (generatedSet) => generatedSet.map((item) => item.toCommand()), + ) .expand((s) => s) .cast(); @@ -82,6 +93,7 @@ class SchemaDifference { final fromColumns = {}..addAll(fromTable.columns); // Primary keys are added on [InsertTable] + // ignore: cascade_invocations fromColumns.removeWhere((c) => c.isPrimaryKey); toColumns.removeWhere((c) => c.isPrimaryKey); diff --git a/packages/brick_sqlite/lib/src/db/schema/schema_index.dart b/packages/brick_sqlite/lib/src/db/schema/schema_index.dart index 933762a6..c970f037 100644 --- a/packages/brick_sqlite/lib/src/db/schema/schema_index.dart +++ b/packages/brick_sqlite/lib/src/db/schema/schema_index.dart @@ -2,15 +2,21 @@ import 'package:brick_sqlite/src/db/migration_commands/create_index.dart'; import 'package:brick_sqlite/src/db/migration_commands/migration_command.dart'; import 'package:brick_sqlite/src/db/schema/schema_base.dart'; +/// A definition for the schema of an index class SchemaIndex extends BaseSchemaObject { + /// String? name; + /// final List columns; + /// String? tableName; + /// final bool unique; + /// A definition for the schema of an index SchemaIndex({ required this.columns, this.tableName, diff --git a/packages/brick_sqlite/lib/src/db/schema/schema_table.dart b/packages/brick_sqlite/lib/src/db/schema/schema_table.dart index a8e9edab..46e9c50b 100644 --- a/packages/brick_sqlite/lib/src/db/schema/schema_table.dart +++ b/packages/brick_sqlite/lib/src/db/schema/schema_table.dart @@ -10,12 +10,16 @@ import 'package:brick_sqlite/src/db/schema/schema_index.dart'; /// Describes a table object managed by SQLite class SchemaTable extends BaseSchemaObject { + /// final String name; + /// Set columns; + /// Set indices; + /// Describes a table object managed by SQLite SchemaTable( this.name, { Set? columns, diff --git a/packages/brick_sqlite/lib/src/helpers/alter_column_helper.dart b/packages/brick_sqlite/lib/src/helpers/alter_column_helper.dart index ba4ab242..ea927732 100644 --- a/packages/brick_sqlite/lib/src/helpers/alter_column_helper.dart +++ b/packages/brick_sqlite/lib/src/helpers/alter_column_helper.dart @@ -11,15 +11,21 @@ class AlterColumnHelper { /// The command to restructure the table final MigrationCommand command; + /// bool get isDrop => command is DropColumn; + + /// bool get isRename => command is RenameColumn; + + /// bool get isUniqueInsert => command is InsertColumn && (command as InsertColumn).unique; /// Declares if this command requires extra SQLite work to be migrated bool get requiresSchema => isDrop || isRename || isUniqueInsert; + /// String get tableName { - assert(requiresSchema); + assert(requiresSchema, 'Command does not require schema'); if (isDrop) { return (command as DropColumn).onTable; @@ -32,12 +38,14 @@ class AlterColumnHelper { return (command as InsertColumn).onTable; } - AlterColumnHelper(this.command); + /// Workaround for SQLite commands that require altering the table instead of the column. + /// + /// Supports [DropColumn], [RenameColumn], [InsertColumn] + const AlterColumnHelper(this.command); /// Get info about existing columns - Future>> tableInfo(Database db) async { - return await db.rawQuery('PRAGMA table_info("$tableName");'); - } + Future>> tableInfo(Database db) async => + await db.rawQuery('PRAGMA table_info("$tableName");'); /// Create new table with updated column data List> _newColumns(List> columns) { @@ -73,29 +81,28 @@ class AlterColumnHelper { } /// Given new columns, create the SQLite statement - String _newColumnsExpression(List> columns) { - return columns.map((Map column) { - final definition = [column['name'] as String, column['type'] as String]; + String _newColumnsExpression(List> columns) => + columns.map((Map column) { + final definition = [column['name'] as String, column['type'] as String]; - if (column['notnull'] == 1) { - definition.add('NOT NULL'); - } + if (column['notnull'] == 1) { + definition.add('NOT NULL'); + } - if (column['dflt_value'] != null) { - definition.add('DEFAULT ${column['dflt_value']}'); - } + if (column['dflt_value'] != null) { + definition.add('DEFAULT ${column['dflt_value']}'); + } - if (column['pk'] == 1) { - definition.add('PRIMARY KEY'); - } + if (column['pk'] == 1) { + definition.add('PRIMARY KEY'); + } - if (column['unique'] == true) { - definition.add('UNIQUE'); - } + if (column['unique'] == true) { + definition.add('UNIQUE'); + } - return definition.join(' '); - }).join(', '); - } + return definition.join(' '); + }).join(', '); /// Perform the necessary SQLite operation Future execute(Database db) async { diff --git a/packages/brick_sqlite/lib/src/helpers/query_sql_transformer.dart b/packages/brick_sqlite/lib/src/helpers/query_sql_transformer.dart index beb95461..3a1a24eb 100644 --- a/packages/brick_sqlite/lib/src/helpers/query_sql_transformer.dart +++ b/packages/brick_sqlite/lib/src/helpers/query_sql_transformer.dart @@ -1,10 +1,12 @@ -import 'package:brick_core/core.dart' show Query, WhereCondition, Compare, WherePhrase; +import 'package:brick_core/core.dart' show Compare, Query, WhereCondition, WherePhrase; import 'package:brick_sqlite/src/db/migration_commands/insert_foreign_key.dart'; import 'package:brick_sqlite/src/db/migration_commands/insert_table.dart'; import 'package:brick_sqlite/src/models/sqlite_model.dart'; import 'package:brick_sqlite/src/runtime_sqlite_column_definition.dart'; import 'package:brick_sqlite/src/sqlite_adapter.dart'; import 'package:brick_sqlite/src/sqlite_model_dictionary.dart'; +import 'package:brick_sqlite/src/sqlite_provider.dart'; +import 'package:brick_sqlite/src/sqlite_provider_query.dart'; import 'package:meta/meta.dart' show protected; /// Create a prepared SQLite statement for eventual execution. Only [statement] and [values] @@ -16,8 +18,12 @@ import 'package:meta/meta.dart' show protected; /// final results = await (await db).rawQuery(sqliteQuery.statement, sqliteQuery.values); /// ``` class QuerySqlTransformer<_Model extends SqliteModel> { + /// The [SqliteAdapter] of [_Model] final SqliteAdapter adapter; + + /// All other [SqliteAdapter]s and [SqliteModel]s known to the provider final SqliteModelDictionary modelDictionary; + final List _statement = []; final List _where = []; final Set _innerJoins = {}; @@ -40,13 +46,14 @@ class QuerySqlTransformer<_Model extends SqliteModel> { /// Prepared; includes preceeding `WHERE` String get whereClause { if (_where.isNotEmpty) { - final cleanedClause = _cleanWhereClause(_where.join('')); + final cleanedClause = _cleanWhereClause(_where.join()); return 'WHERE $cleanedClause'; } return ''; } + /// All `INNER JOIN` statements String get innerJoins => _innerJoins.join(' '); /// [selectStatement] will output [statement] as a `SELECT FROM`. When false, the [statement] @@ -84,8 +91,9 @@ class QuerySqlTransformer<_Model extends SqliteModel> { _statement.add( AllOtherClausesFragment( - query?.providerArgs ?? {}, + query, fieldsToColumns: adapter.fieldsToSqliteColumns, + modelDictionary: modelDictionary, ).toString(), ); } @@ -94,13 +102,11 @@ class QuerySqlTransformer<_Model extends SqliteModel> { /// prefixed with their `required` operator before being added to the full WHERE clause. /// This is a bad hack to remove leading operators (otherwise it's invalid SQL) /// and should be refactored after experimental use in the wild. - String _cleanWhereClause(String dirtyClause) { - return dirtyClause - .replaceFirst(RegExp('^ (AND|OR)'), '') - .replaceAll(RegExp(r' \( (AND|OR)'), ' (') - .replaceAll(RegExp(r'\(\s+'), '(') - .trim(); - } + String _cleanWhereClause(String dirtyClause) => dirtyClause + .replaceFirst(RegExp('^ (AND|OR)'), '') + .replaceAll(RegExp(r' \( (AND|OR)'), ' (') + .replaceAll(RegExp(r'\(\s+'), '(') + .trim(); /// Recursively step through a `Where` or `WherePhrase` to ouput a condition for `WHERE`. String _expandCondition(WhereCondition condition, [SqliteAdapter? passedAdapter]) { @@ -108,9 +114,10 @@ class QuerySqlTransformer<_Model extends SqliteModel> { // Begin a separate where phrase if (condition is WherePhrase) { - final phrase = condition.conditions.fold('', (acc, phraseCondition) { - return acc + _expandCondition(phraseCondition, passedAdapter); - }); + final phrase = condition.conditions.fold( + '', + (acc, phraseCondition) => acc + _expandCondition(phraseCondition, passedAdapter), + ); if (phrase.isEmpty) return ''; final matcher = condition.isRequired ? 'AND' : 'OR'; @@ -175,20 +182,25 @@ class QuerySqlTransformer<_Model extends SqliteModel> { } } -/// Inner joins +/// Inner joins; not for public use class AssociationFragment { + /// final String foreignTableName; + /// final RuntimeSqliteColumnDefinition definition; + /// final String localTableName; + /// Inner joins; not for public use AssociationFragment({ required this.definition, required this.foreignTableName, required this.localTableName, }); + /// All statements to create the `INNER JOIN` command List toJoinFragment() { const primaryKeyColumn = InsertTable.PRIMARY_KEY_COLUMN; final oneToOneAssociation = !definition.iterable; @@ -213,12 +225,16 @@ class AssociationFragment { /// Column and iterable comparison class WhereColumnFragment { + /// final String column; + /// final WhereCondition condition; + /// String get matcher => condition.isRequired ? 'AND' : 'OR'; + /// The sign used to compare statements generated by [compareSign] String get sign { if (condition.value == null) { if (condition.compare == Compare.exact) return 'IS'; @@ -228,11 +244,13 @@ class WhereColumnFragment { return compareSign(condition.compare); } + /// Values used for the `WHERE` clause final List values = []; /// Computed once after initialization by [generate] late final String? _statement; + /// Column and iterable comparison WhereColumnFragment( this.condition, this.column, @@ -240,6 +258,7 @@ class WhereColumnFragment { _statement = generate(); } + /// SQLite statement from all `WHERE` conditions and values @protected String generate() { if (condition.value is Iterable) { @@ -259,6 +278,7 @@ class WhereColumnFragment { return ' $matcher $column $sign ?'; } + /// Convert special cases to SQLite statements @protected dynamic sqlifiedValue(dynamic value, Compare compare) { if (compare == Compare.contains || compare == Compare.doesNotContain) { @@ -273,6 +293,7 @@ class WhereColumnFragment { @override String toString() => _statement ?? ''; + /// Convert [Compare] values to SQLite-usable operators static String compareSign(Compare compare) { switch (compare) { case Compare.exact: @@ -321,8 +342,14 @@ class WhereColumnFragment { /// Query modifiers such as `LIMIT`, `OFFSET`, etc. that require minimal logic. class AllOtherClausesFragment { + /// final Map fieldsToColumns; - final Map providerArgs; + + /// + final SqliteModelDictionary modelDictionary; + + /// + final Query? query; /// Order matters. For example, LIMIT has to follow an ORDER BY but precede an OFFSET. static const _supportedOperators = { @@ -334,27 +361,53 @@ class AllOtherClausesFragment { 'offset': 'OFFSET', }; - /// These operators declare a column to compare against. The fields provided in [providerArgs] - /// will have to be converted to their column name. - /// For example, `'orderBy': 'createdAt ASC'` must become `ORDER BY created_at ASC`. + /// These operators declare a column to compare against. The fields provided in + /// [Query] or [SqliteProviderQuery] will have to be converted to their column name. + /// For example, `orderBy: [OrderBy.asc('createdAt')]` must become `ORDER BY created_at ASC`. static const _operatorsDeclaringFields = {'ORDER BY', 'GROUP BY', 'HAVING'}; + /// Query modifiers such as `LIMIT`, `OFFSET`, etc. that require minimal logic. AllOtherClausesFragment( - Map? providerArgs, { + this.query, { required this.fieldsToColumns, - }) : providerArgs = providerArgs ?? {}; + required this.modelDictionary, + }); @override String toString() { + final providerQuery = query?.providerQueries[SqliteProvider] as SqliteProviderQuery?; + final argsToSqlStatments = { + ...?query?.providerArgs, + if (providerQuery?.collate != null) 'collate': providerQuery?.collate, + if (query?.limit != null) 'limit': query?.limit, + if (query?.offset != null) 'offset': query?.offset, + if (query?.orderBy.isNotEmpty ?? false) + 'orderBy': query?.orderBy.map((p) { + final isAssociation = fieldsToColumns[p.evaluatedField]?.association ?? false; + if (!isAssociation) return p.toString(); + + if (p.associationField == null) return p.toString(); + + final associationAdapter = + modelDictionary.adapterFor[fieldsToColumns[p.evaluatedField]?.type]; + + return '`${associationAdapter?.tableName}`.${associationAdapter?.fieldsToSqliteColumns[p.associationField]?.columnName} ${p.ascending ? 'ASC' : 'DESC'}'; + }).join(', '), + if (providerQuery?.groupBy != null) 'groupBy': providerQuery?.groupBy, + if (providerQuery?.having != null) 'having': providerQuery?.having, + }; + return _supportedOperators.entries.fold>([], (acc, entry) { final op = entry.value; - var value = providerArgs[entry.key]; + var value = argsToSqlStatments[entry.key]; if (value == null) return acc; if (_operatorsDeclaringFields.contains(op)) { value = value.toString().split(',').fold(value.toString(), (modValue, innerValueClause) { + // TODO(tshedor): revisit and remove providerArgs hacks here after + // providerArgs is fully deprecated final fragment = innerValueClause.trim().split(' '); if (fragment.isEmpty) return modValue; @@ -368,6 +421,19 @@ class AllOtherClausesFragment { return modValue.replaceAll(fieldName, columnName); } + final tableFragment = innerValueClause.trim().split('.'); + if (fragment.isEmpty) return modValue; + + final tabledFieldName = tableFragment.last; + final tabledColumnDefinition = fieldsToColumns[fieldName]; + var tabledColumnName = tabledColumnDefinition?.columnName; + if (tabledColumnName != null && modValue.contains(fieldName)) { + if (columnDefinition!.type == DateTime) { + tabledColumnName = 'datetime($tabledColumnName)'; + } + return modValue.replaceAll(tabledFieldName, tabledColumnName); + } + return modValue; }); } diff --git a/packages/brick_sqlite/lib/src/models/sqlite_model.dart b/packages/brick_sqlite/lib/src/models/sqlite_model.dart index b0e54da4..1cc5a8d3 100644 --- a/packages/brick_sqlite/lib/src/models/sqlite_model.dart +++ b/packages/brick_sqlite/lib/src/models/sqlite_model.dart @@ -1,11 +1,13 @@ import 'package:brick_core/core.dart'; +import 'package:brick_sqlite/src/sqlite_provider.dart'; +/// The default value of the `primaryKey` field. // ignore: constant_identifier_names const int? NEW_RECORD_ID = null; /// Models accessible to the [SqliteProvider]. /// -/// Why isn't this in the SQLite package? It's required by [OfflineFirstModel]. +/// Why isn't this in the SQLite package? It's required by `OfflineFirstModel`. abstract class SqliteModel implements Model { /// DO NOT modify this in the end implementation code. The Repository will update it accordingly. /// It is strongly recommended that this field only be used by Brick's internal queries and not diff --git a/packages/brick_sqlite/lib/src/runtime_sqlite_column_definition.dart b/packages/brick_sqlite/lib/src/runtime_sqlite_column_definition.dart index a7ce2610..7bc19574 100644 --- a/packages/brick_sqlite/lib/src/runtime_sqlite_column_definition.dart +++ b/packages/brick_sqlite/lib/src/runtime_sqlite_column_definition.dart @@ -16,6 +16,8 @@ class RuntimeSqliteColumnDefinition { /// In other words, the runtime type. final Type type; + /// Used to define types in [SqliteAdapter#fieldsToSqliteColumns]. The build runner package + /// extracts types and associations that would've been otherwise inaccessible at runtime. const RuntimeSqliteColumnDefinition({ this.association = false, required this.columnName, diff --git a/packages/brick_sqlite/lib/src/sqlite_adapter.dart b/packages/brick_sqlite/lib/src/sqlite_adapter.dart index b4b610ad..138b0aa3 100644 --- a/packages/brick_sqlite/lib/src/sqlite_adapter.dart +++ b/packages/brick_sqlite/lib/src/sqlite_adapter.dart @@ -1,4 +1,6 @@ import 'package:brick_core/core.dart'; +import 'package:brick_sqlite/src/db/migration.dart'; +import 'package:brick_sqlite/src/db/migration_commands/migration_command.dart'; import 'package:brick_sqlite/src/models/sqlite_model.dart'; import 'package:brick_sqlite/src/runtime_sqlite_column_definition.dart'; import 'package:brick_sqlite/src/sqlite_provider.dart'; @@ -29,11 +31,14 @@ abstract class SqliteAdapter implements Adapter { ModelRepository? repository, }) async {} + /// Future fromSqlite( Map input, { required SqliteProvider provider, ModelRepository? repository, }); + + /// Future> toSqlite( TModel input, { required SqliteProvider provider, diff --git a/packages/brick_sqlite/lib/src/sqlite_model_dictionary.dart b/packages/brick_sqlite/lib/src/sqlite_model_dictionary.dart index 5ae174d3..b6bdd778 100644 --- a/packages/brick_sqlite/lib/src/sqlite_model_dictionary.dart +++ b/packages/brick_sqlite/lib/src/sqlite_model_dictionary.dart @@ -4,5 +4,6 @@ import 'package:brick_sqlite/src/sqlite_adapter.dart'; /// Associates app models with their [SqliteAdapter] class SqliteModelDictionary extends ModelDictionary> { + /// Associates app models with their [SqliteAdapter] const SqliteModelDictionary(super.adapterFor); } diff --git a/packages/brick_sqlite/lib/src/sqlite_provider.dart b/packages/brick_sqlite/lib/src/sqlite_provider.dart index 299b03a4..f3e95c93 100644 --- a/packages/brick_sqlite/lib/src/sqlite_provider.dart +++ b/packages/brick_sqlite/lib/src/sqlite_provider.dart @@ -1,5 +1,3 @@ -import 'dart:io'; - import 'package:brick_core/core.dart'; import 'package:brick_sqlite/src/db/migration.dart'; import 'package:brick_sqlite/src/db/migration_commands/insert_table.dart'; @@ -14,7 +12,7 @@ import 'package:sqflite_common/sqlite_api.dart'; import 'package:sqflite_common/utils/utils.dart' as sqlite_utils; import 'package:synchronized/synchronized.dart'; -/// Retrieves from a Sqlite database +/// Retrieves from a SQLite database class SqliteProvider implements Provider { /// Access the [SQLite](https://github.com/tekartik/sqflite/tree/master/sqflite_common_ffi), /// instance agnostically across platforms. @@ -42,6 +40,7 @@ class SqliteProvider implements Provider implements Provider delete(instance, {query, repository}) async { + Future delete( + TModel instance, { + Query? query, + ModelRepository? repository, + }) async { final adapter = modelDictionary.adapterFor[TModel]!; final db = await getDb(); final existingPrimaryKey = await adapter.primaryKeyByUniqueColumns(instance, db); @@ -71,7 +74,7 @@ class SqliteProvider implements Provider exists({ Query? query, @@ -91,9 +94,11 @@ class SqliteProvider implements Provider + 'SELECT ${match.group(1)}.${InsertTable.PRIMARY_KEY_COLUMN} FROM ${match.group(1)}', + ); } final countQuery = await (await getDb()).rawQuery(statement, sqlQuery.values); @@ -111,17 +116,17 @@ class SqliteProvider implements Provider> get({ - query, - repository, + Query? query, + ModelRepository? repository, }) async { final sqlQuery = QuerySqlTransformer( modelDictionary: modelDictionary, @@ -141,6 +146,7 @@ class SqliteProvider implements Provider lastMigrationVersion() async { final db = await getDb(); @@ -160,7 +166,7 @@ class SqliteProvider implements Provider implements Provider implements Provider (await getDb()).rawQuery(sql, arguments)); if (results.isEmpty || results.first.isEmpty) { // otherwise an empty sql result will generate a blank model @@ -229,35 +233,28 @@ class SqliteProvider implements Provider rawExecute(String sql, [List? arguments]) async { - return await (await getDb()).execute(sql, arguments); - } + Future rawExecute(String sql, [List? arguments]) async => + await (await getDb()).execute(sql, arguments); /// Insert with a raw SQL statement. **Advanced use only**. - Future rawInsert(String sql, [List? arguments]) async { - return await (await getDb()).rawInsert(sql, arguments); - } + Future rawInsert(String sql, [List? arguments]) async => + await (await getDb()).rawInsert(sql, arguments); /// Query with a raw SQL statement. **Advanced use only**. - Future>> rawQuery(String sql, [List? arguments]) async { - return await (await getDb()).rawQuery(sql, arguments); - } + Future>> rawQuery(String sql, [List? arguments]) async => + await (await getDb()).rawQuery(sql, arguments); /// Reset the DB by deleting and recreating it. /// /// **WARNING:** This is a destructive, irrevisible action. Future resetDb() async { - try { - await (await getDb()).close(); + await (await getDb()).close(); - await databaseFactory.deleteDatabase(dbName); + await databaseFactory.deleteDatabase(dbName); - // recreate - _openDb = null; - await getDb(); - } on FileSystemException { - // noop - } + // recreate + _openDb = null; + await getDb(); } /// Perform actions within a database transaction. @@ -265,14 +262,16 @@ class SqliteProvider implements Provider transaction(Future Function(Transaction transaction) callback) async { final db = await getDb(); - return await _lock.synchronized(() async { - return await db.transaction(callback); - }); + return await _lock.synchronized(() async => await db.transaction(callback)); } /// Insert record into SQLite. Returns the primary key of the record inserted @override - Future upsert(instance, {query, repository}) async { + Future upsert( + TModel instance, { + Query? query, + ModelRepository? repository, + }) async { final adapter = modelDictionary.adapterFor[TModel]!; final db = await getDb(); @@ -280,8 +279,8 @@ class SqliteProvider implements Provider((txn) async { + final id = await _lock.synchronized( + () async => await db.transaction((txn) async { final existingPrimaryKey = await adapter.primaryKeyByUniqueColumns(instance, txn); if (instance.isNewRecord && existingPrimaryKey == null) { @@ -299,8 +298,8 @@ class SqliteProvider implements Provider { + /// Defines a value for `COLLATE`. Often this field is used for case insensitive + /// queries where the value is `NOCASE`. + final String? collate; + + /// Defines a value for `GROUP BY`. + final String? groupBy; + + /// Defines a value for `HAVING`. + final String? having; + + /// [SqliteProvider]-specific options for a [Query] + const SqliteProviderQuery({ + this.collate, + this.groupBy, + this.having, + }); + + @override + Map toJson() => { + if (collate != null) 'collate': collate, + if (groupBy != null) 'groupBy': groupBy, + if (having != null) 'having': having, + }; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is SqliteProviderQuery && + runtimeType == other.runtimeType && + collate == other.collate && + groupBy == other.groupBy && + having == other.having; + + @override + int get hashCode => collate.hashCode ^ groupBy.hashCode ^ having.hashCode; +} diff --git a/packages/brick_sqlite/pubspec.yaml b/packages/brick_sqlite/pubspec.yaml index 03358ded..45465e01 100644 --- a/packages/brick_sqlite/pubspec.yaml +++ b/packages/brick_sqlite/pubspec.yaml @@ -4,21 +4,21 @@ homepage: https://github.com/GetDutchie/brick/tree/main/packages/brick_sqlite issue_tracker: https://github.com/GetDutchie/brick/issues repository: https://github.com/GetDutchie/brick -version: 3.1.1+1 +version: 3.2.1 environment: sdk: ">=2.18.0 <4.0.0" dependencies: - brick_core: ^1.1.1 + brick_core: ^1.3.0 collection: ">=1.0.0 <2.0.0" logging: ">=1.0.0 <2.0.0" meta: ">=1.3.0 <2.0.0" sqflite_common: ">=2.0.0 <3.0.0" - synchronized: ^3.0.0 + synchronized: ">=3.0.0 <4.0.0" dev_dependencies: - test: ^1.16.5 - mockito: ^5.0.0 - sqflite_common_ffi: ^2.0.0 - lints: ^2.0.1 + lints: + mockito: + sqflite_common_ffi: + test: diff --git a/packages/brick_sqlite/test/__mocks__.dart b/packages/brick_sqlite/test/__mocks__.dart index 5e820ccd..43f8001b 100644 --- a/packages/brick_sqlite/test/__mocks__.dart +++ b/packages/brick_sqlite/test/__mocks__.dart @@ -1,3 +1,4 @@ +import 'package:brick_sqlite/src/db/column.dart'; import 'package:brick_sqlite/src/db/migration.dart'; import 'package:brick_sqlite/src/db/migration_commands/create_index.dart'; import 'package:brick_sqlite/src/db/migration_commands/insert_column.dart'; @@ -22,21 +23,17 @@ const _demoModelMigrationCommands = [ 'DemoModel', foreignKeyColumn: 'l_DemoModel_brick_id', onDeleteCascade: true, - onDeleteSetDefault: false, ), InsertForeignKey( '_brick_DemoModel_many_assoc', 'DemoModelAssoc', foreignKeyColumn: 'f_DemoModelAssoc_brick_id', onDeleteCascade: true, - onDeleteSetDefault: false, ), InsertForeignKey( 'DemoModel', 'DemoModelAssoc', foreignKeyColumn: 'assoc_DemoModelAssoc_brick_id', - onDeleteCascade: false, - onDeleteSetDefault: false, ), InsertColumn('complex_field_name', Column.varchar, onTable: 'DemoModel'), InsertColumn('last_name', Column.varchar, onTable: 'DemoModel'), diff --git a/packages/brick_sqlite/test/__mocks__/demo_model_adapter.dart b/packages/brick_sqlite/test/__mocks__/demo_model_adapter.dart index 779875fb..719a75dd 100644 --- a/packages/brick_sqlite/test/__mocks__/demo_model_adapter.dart +++ b/packages/brick_sqlite/test/__mocks__/demo_model_adapter.dart @@ -1,4 +1,5 @@ import 'package:brick_core/core.dart' show Query; +import 'package:brick_core/src/model_repository.dart'; import 'package:brick_sqlite/brick_sqlite.dart'; import 'package:sqflite_common/sqlite_api.dart' show DatabaseExecutor; @@ -8,66 +9,64 @@ Future _$DemoModelFromSqlite( Map data, { SqliteProvider? provider, repository, -}) async { - return DemoModel( - name: data['full_name'] == null ? null : data['full_name'] as String, - assoc: data['assoc_DemoModelAssoc_brick_id'] == null - ? null - : (data['assoc_DemoModelAssoc_brick_id'] > -1 - ? (await repository?.getAssociation( - Query.where( - 'primaryKey', - data['assoc_DemoModelAssoc_brick_id'] as int, - limit1: true, - ), - )) - ?.first - : null), - complexFieldName: - data['complex_field_name'] == null ? null : data['complex_field_name'] as String, - lastName: data['last_name'] == null ? null : data['last_name'] as String, - manyAssoc: (await provider?.rawQuery( - 'SELECT DISTINCT `f_DemoModelAssoc_brick_id` FROM `_brick_DemoModel_many_assoc` WHERE l_DemoModel_brick_id = ?', - [data['_brick_id'] as int], - ).then((results) { - final ids = results.map((r) => r['f_DemoModelAssoc_brick_id']); - return Future.wait( - ids.map( - (primaryKey) => repository - ?.getAssociation( - Query.where('primaryKey', primaryKey, limit1: true), - ) - ?.then((r) => (r?.isEmpty ?? true) ? null : r.first), - ), - ); - })) - ?.toList(), - simpleBool: data['simple_bool'] == null ? null : data['simple_bool'] == 1, - simpleTime: data['simple_time'] == null - ? null - : data['simple_time'] == null - ? null - : DateTime.tryParse(data['simple_time'] as String), - )..primaryKey = data['_brick_id'] as int; -} +}) async => + DemoModel( + name: data['full_name'] == null ? null : data['full_name'] as String, + assoc: data['assoc_DemoModelAssoc_brick_id'] == null + ? null + : (data['assoc_DemoModelAssoc_brick_id'] > -1 + ? (await repository?.getAssociation( + Query.where( + 'primaryKey', + data['assoc_DemoModelAssoc_brick_id'] as int, + limit1: true, + ), + )) + ?.first + : null), + complexFieldName: + data['complex_field_name'] == null ? null : data['complex_field_name'] as String, + lastName: data['last_name'] == null ? null : data['last_name'] as String, + manyAssoc: (await provider?.rawQuery( + 'SELECT DISTINCT `f_DemoModelAssoc_brick_id` FROM `_brick_DemoModel_many_assoc` WHERE l_DemoModel_brick_id = ?', + [data['_brick_id'] as int], + ).then((results) { + final ids = results.map((r) => r['f_DemoModelAssoc_brick_id']); + return Future.wait( + ids.map( + (primaryKey) => repository + ?.getAssociation( + Query.where('primaryKey', primaryKey, limit1: true), + ) + ?.then((r) => (r?.isEmpty ?? true) ? null : r.first), + ), + ); + })) + ?.toList(), + simpleBool: data['simple_bool'] == null ? null : data['simple_bool'] == 1, + simpleTime: data['simple_time'] == null + ? null + : data['simple_time'] == null + ? null + : DateTime.tryParse(data['simple_time'] as String), + )..primaryKey = data['_brick_id'] as int; Future> _$DemoModelToSqlite( DemoModel instance, { required SqliteProvider provider, repository, -}) async { - return { - 'assoc_DemoModelAssoc_brick_id': instance.assoc?.primaryKey ?? - (instance.assoc != null - ? await provider.upsert(instance.assoc!, repository: repository) - : null), - 'complex_field_name': instance.complexFieldName, - 'last_name': instance.lastName, - 'full_name': instance.name, - 'simple_bool': instance.simpleBool == null ? null : (instance.simpleBool! ? 1 : 0), - 'simple_time': instance.simpleTime?.toIso8601String(), - }; -} +}) async => + { + 'assoc_DemoModelAssoc_brick_id': instance.assoc?.primaryKey ?? + (instance.assoc != null + ? await provider.upsert(instance.assoc!, repository: repository) + : null), + 'complex_field_name': instance.complexFieldName, + 'last_name': instance.lastName, + 'full_name': instance.name, + 'simple_bool': instance.simpleBool == null ? null : (instance.simpleBool! ? 1 : 0), + 'simple_time': instance.simpleTime?.toIso8601String(), + }; /// Construct a [DemoModel] class DemoModelAdapter extends SqliteAdapter { @@ -76,39 +75,28 @@ class DemoModelAdapter extends SqliteAdapter { @override final Map fieldsToSqliteColumns = { 'primaryKey': const RuntimeSqliteColumnDefinition( - association: false, columnName: '_brick_id', - iterable: false, type: int, ), 'id': const RuntimeSqliteColumnDefinition( - association: false, columnName: 'id', - iterable: false, type: int, ), 'someField': const RuntimeSqliteColumnDefinition( - association: false, columnName: 'some_field', - iterable: false, type: bool, ), 'assoc': const RuntimeSqliteColumnDefinition( association: true, columnName: 'assoc_DemoModelAssoc_brick_id', - iterable: false, type: DemoModelAssoc, ), 'complexFieldName': const RuntimeSqliteColumnDefinition( - association: false, columnName: 'complex_field_name', - iterable: false, type: String, ), 'lastName': const RuntimeSqliteColumnDefinition( - association: false, columnName: 'last_name', - iterable: false, type: String, ), 'manyAssoc': const RuntimeSqliteColumnDefinition( @@ -118,21 +106,15 @@ class DemoModelAdapter extends SqliteAdapter { type: DemoModelAssoc, ), 'name': const RuntimeSqliteColumnDefinition( - association: false, columnName: 'full_name', - iterable: false, type: String, ), 'simpleBool': const RuntimeSqliteColumnDefinition( - association: false, columnName: 'simple_bool', - iterable: false, type: bool, ), - 'simpleTime': RuntimeSqliteColumnDefinition( - association: false, + 'simpleTime': const RuntimeSqliteColumnDefinition( columnName: 'simple_time', - iterable: false, type: DateTime, ), }; @@ -144,7 +126,11 @@ class DemoModelAdapter extends SqliteAdapter { @override final String tableName = 'DemoModel'; @override - Future afterSave(instance, {required provider, repository}) async { + Future afterSave( + DemoModel instance, { + required SqliteProvider provider, + ModelRepository? repository, + }) async { if (instance.primaryKey != null) { final oldColumns = await provider.rawQuery( 'SELECT `f_DemoModelAssoc_brick_id` FROM `_brick_DemoModel_many_assoc` WHERE `l_DemoModel_brick_id` = ?', @@ -155,12 +141,12 @@ class DemoModelAdapter extends SqliteAdapter { final idsToDelete = oldIds.where((id) => !newIds.contains(id)); await Future.wait( - idsToDelete.map((id) async { - return await provider.rawExecute( + idsToDelete.map( + (id) async => await provider.rawExecute( 'DELETE FROM `_brick_DemoModel_many_assoc` WHERE `l_DemoModel_brick_id` = ? AND `f_DemoModelAssoc_brick_id` = ?', [instance.primaryKey, id], - ); - }), + ), + ), ); await Future.wait( @@ -178,9 +164,17 @@ class DemoModelAdapter extends SqliteAdapter { } @override - Future fromSqlite(Map input, {required provider, repository}) async => + Future fromSqlite( + Map input, { + required SqliteProvider provider, + ModelRepository? repository, + }) async => await _$DemoModelFromSqlite(input, provider: provider, repository: repository); @override - Future> toSqlite(DemoModel input, {required provider, repository}) async => + Future> toSqlite( + DemoModel input, { + required SqliteProvider provider, + ModelRepository? repository, + }) async => await _$DemoModelToSqlite(input, provider: provider, repository: repository); } diff --git a/packages/brick_sqlite/test/__mocks__/demo_model_assoc_adapter.dart b/packages/brick_sqlite/test/__mocks__/demo_model_assoc_adapter.dart index 47368f98..aba53fd7 100644 --- a/packages/brick_sqlite/test/__mocks__/demo_model_assoc_adapter.dart +++ b/packages/brick_sqlite/test/__mocks__/demo_model_assoc_adapter.dart @@ -1,3 +1,4 @@ +import 'package:brick_core/src/model_repository.dart'; import 'package:brick_sqlite/brick_sqlite.dart'; // ignore: unused_import, unused_shown_name import 'package:brick_sqlite/db.dart'; @@ -9,18 +10,16 @@ Future _$DemoModelAssocFromSqlite( Map data, { SqliteProvider? provider, repository, -}) async { - return DemoModelAssoc(name: data['full_name'] == null ? null : data['full_name'] as String) - ..primaryKey = data['_brick_id'] as int; -} +}) async => + DemoModelAssoc(name: data['full_name'] == null ? null : data['full_name'] as String) + ..primaryKey = data['_brick_id'] as int; Future> _$DemoModelAssocToSqlite( DemoModelAssoc instance, { SqliteProvider? provider, repository, -}) async { - return {'full_name': instance.name}; -} +}) async => + {'full_name': instance.name}; /// Construct a [DemoModelAssoc] class DemoModelAssocAdapter extends SqliteAdapter { @@ -29,39 +28,28 @@ class DemoModelAssocAdapter extends SqliteAdapter { @override final Map fieldsToSqliteColumns = { 'primaryKey': const RuntimeSqliteColumnDefinition( - association: false, columnName: '_brick_id', - iterable: false, type: int, ), 'id': const RuntimeSqliteColumnDefinition( - association: false, columnName: 'id', - iterable: false, type: int, ), 'someField': const RuntimeSqliteColumnDefinition( - association: false, columnName: 'some_field', - iterable: false, type: bool, ), 'assoc': const RuntimeSqliteColumnDefinition( association: true, columnName: 'assoc_DemoModelAssoc_brick_id', - iterable: false, type: DemoModelAssoc, ), 'complexFieldName': const RuntimeSqliteColumnDefinition( - association: false, columnName: 'complex_field_name', - iterable: false, type: String, ), 'lastName': const RuntimeSqliteColumnDefinition( - association: false, columnName: 'last_name', - iterable: false, type: String, ), 'manyAssoc': const RuntimeSqliteColumnDefinition( @@ -71,15 +59,11 @@ class DemoModelAssocAdapter extends SqliteAdapter { type: DemoModelAssoc, ), 'name': const RuntimeSqliteColumnDefinition( - association: false, columnName: 'full_name', - iterable: false, type: String, ), 'simpleBool': const RuntimeSqliteColumnDefinition( - association: false, columnName: 'simple_bool', - iterable: false, type: bool, ), }; @@ -97,15 +81,15 @@ class DemoModelAssocAdapter extends SqliteAdapter { @override Future fromSqlite( Map input, { - required provider, - repository, + required SqliteProvider provider, + ModelRepository? repository, }) async => await _$DemoModelAssocFromSqlite(input, provider: provider, repository: repository); @override Future> toSqlite( DemoModelAssoc input, { - required provider, - repository, + required SqliteProvider provider, + ModelRepository? repository, }) async => await _$DemoModelAssocToSqlite(input, provider: provider, repository: repository); } diff --git a/packages/brick_sqlite/test/db/__mocks__.dart b/packages/brick_sqlite/test/db/__mocks__.dart index 659287f6..fc2ccc0e 100644 --- a/packages/brick_sqlite/test/db/__mocks__.dart +++ b/packages/brick_sqlite/test/db/__mocks__.dart @@ -1,3 +1,4 @@ +import 'package:brick_sqlite/src/db/column.dart'; import 'package:brick_sqlite/src/db/migration.dart'; import 'package:brick_sqlite/src/db/migration_commands/create_index.dart'; import 'package:brick_sqlite/src/db/migration_commands/drop_index.dart'; diff --git a/packages/brick_sqlite/test/db/column_test.dart b/packages/brick_sqlite/test/db/column_test.dart new file mode 100644 index 00000000..d905c2b6 --- /dev/null +++ b/packages/brick_sqlite/test/db/column_test.dart @@ -0,0 +1,49 @@ +import 'package:brick_sqlite/src/db/column.dart'; +import 'package:test/test.dart'; + +void main() { + group('Column', () { + test('#definition', () { + expect(Column.bigint.definition, 'BIGINT'); + expect(Column.blob.definition, 'BLOB'); + expect(Column.boolean.definition, 'BOOLEAN'); + expect(Column.date.definition, 'DATE'); + expect(Column.datetime.definition, 'DATETIME'); + expect(Column.Double.definition, 'DOUBLE'); + expect(Column.integer.definition, 'INTEGER'); + expect(Column.float.definition, 'FLOAT'); + expect(Column.num.definition, 'DOUBLE'); + expect(Column.text.definition, 'TEXT'); + expect(Column.varchar.definition, 'VARCHAR'); + expect(Column.undefined.definition, ''); + }); + + test('.fromDartPrimitive', () { + expect(Column.fromDartPrimitive(bool), Column.boolean); + expect(Column.fromDartPrimitive(DateTime), Column.datetime); + expect(Column.fromDartPrimitive(double), Column.Double); + expect(Column.fromDartPrimitive(int), Column.integer); + expect(Column.fromDartPrimitive(num), Column.num); + expect(Column.fromDartPrimitive(String), Column.varchar); + expect( + () => Column.fromDartPrimitive(dynamic), + throwsA(const TypeMatcher()), + ); + }); + + test('#dartType', () { + expect(Column.bigint.dartType, num); + expect(Column.blob.dartType, List); + expect(Column.boolean.dartType, bool); + expect(Column.date.dartType, DateTime); + expect(Column.datetime.dartType, DateTime); + expect(Column.Double.dartType, double); + expect(Column.integer.dartType, int); + expect(Column.float.dartType, num); + expect(Column.num.dartType, num); + expect(Column.text.dartType, String); + expect(Column.varchar.dartType, String); + expect(Column.undefined.dartType, dynamic); + }); + }); +} diff --git a/packages/brick_sqlite/test/db/migration_commands_test.dart b/packages/brick_sqlite/test/db/migration_commands_test.dart index 54f5d753..72b42946 100644 --- a/packages/brick_sqlite/test/db/migration_commands_test.dart +++ b/packages/brick_sqlite/test/db/migration_commands_test.dart @@ -1,4 +1,4 @@ -import 'package:brick_sqlite/src/db/migration.dart'; +import 'package:brick_sqlite/src/db/column.dart'; import 'package:brick_sqlite/src/db/migration_commands/create_index.dart'; import 'package:brick_sqlite/src/db/migration_commands/drop_column.dart'; import 'package:brick_sqlite/src/db/migration_commands/drop_index.dart'; diff --git a/packages/brick_sqlite/test/db/migration_manager_test.dart b/packages/brick_sqlite/test/db/migration_manager_test.dart index 6c4b0d0c..5cda808e 100644 --- a/packages/brick_sqlite/test/db/migration_manager_test.dart +++ b/packages/brick_sqlite/test/db/migration_manager_test.dart @@ -21,7 +21,7 @@ void main() { const m2 = Migration2(); const m3 = Migration3(); final manager = MigrationManager({m1, m2, m3}); - final emptyManager = MigrationManager({}); + const emptyManager = MigrationManager({}); test('#migrationsSince', () { expect(manager.migrationsSince(1), hasLength(2)); @@ -49,13 +49,14 @@ void main() { expect(emptyManager.version, 0); - final otherManager = MigrationManager({Migration2(), Migration1()}); + final otherManager = MigrationManager({const Migration2(), const Migration1()}); // Should sort migrations inserted out of order expect(otherManager.version, 2); }); test('.latestMigrationVersion', () { - final version = MigrationManager.latestMigrationVersion([Migration2(), Migration1()]); + final version = + MigrationManager.latestMigrationVersion([const Migration2(), const Migration1()]); expect(version, 2); }); diff --git a/packages/brick_sqlite/test/db/migration_test.dart b/packages/brick_sqlite/test/db/migration_test.dart index e73bd8a8..fd28fef3 100644 --- a/packages/brick_sqlite/test/db/migration_test.dart +++ b/packages/brick_sqlite/test/db/migration_test.dart @@ -27,55 +27,6 @@ class Migration20 extends Migration { void main() { group('Migration', () { - test('.ofDefinition', () { - expect(Migration.ofDefinition(Column.bigint), 'BIGINT'); - expect(Migration.ofDefinition(Column.blob), 'BLOB'); - expect(Migration.ofDefinition(Column.boolean), 'BOOLEAN'); - expect(Migration.ofDefinition(Column.date), 'DATE'); - expect(Migration.ofDefinition(Column.datetime), 'DATETIME'); - expect(Migration.ofDefinition(Column.Double), 'DOUBLE'); - expect(Migration.ofDefinition(Column.integer), 'INTEGER'); - expect(Migration.ofDefinition(Column.float), 'FLOAT'); - expect(Migration.ofDefinition(Column.num), 'DOUBLE'); - expect(Migration.ofDefinition(Column.text), 'TEXT'); - expect(Migration.ofDefinition(Column.varchar), 'VARCHAR'); - expect( - () => Migration.ofDefinition(Column.undefined), - throwsA(const TypeMatcher()), - ); - }); - - test('.fromDartPrimitive', () { - expect(Migration.fromDartPrimitive(bool), Column.boolean); - expect(Migration.fromDartPrimitive(DateTime), Column.datetime); - expect(Migration.fromDartPrimitive(double), Column.Double); - expect(Migration.fromDartPrimitive(int), Column.integer); - expect(Migration.fromDartPrimitive(num), Column.num); - expect(Migration.fromDartPrimitive(String), Column.varchar); - expect( - () => Migration.fromDartPrimitive(dynamic), - throwsA(const TypeMatcher()), - ); - }); - - test('.toDartPrimitive', () { - expect(Migration.toDartPrimitive(Column.bigint), num); - expect(Migration.toDartPrimitive(Column.blob), List); - expect(Migration.toDartPrimitive(Column.boolean), bool); - expect(Migration.toDartPrimitive(Column.date), DateTime); - expect(Migration.toDartPrimitive(Column.datetime), DateTime); - expect(Migration.toDartPrimitive(Column.Double), double); - expect(Migration.toDartPrimitive(Column.integer), int); - expect(Migration.toDartPrimitive(Column.float), num); - expect(Migration.toDartPrimitive(Column.num), num); - expect(Migration.toDartPrimitive(Column.text), String); - expect(Migration.toDartPrimitive(Column.varchar), String); - expect( - () => Migration.toDartPrimitive(Column.undefined), - throwsA(const TypeMatcher()), - ); - }); - group('.generate', () { test('one command', () { final output = Migration.generate([const InsertTable('demo')], 1); @@ -121,8 +72,8 @@ class Migration1 extends Migration { test('multiple commands', () { final commands = [ - InsertTable('demo'), - RenameColumn('first_name', 'last_name', onTable: 'people'), + const InsertTable('demo'), + const RenameColumn('first_name', 'last_name', onTable: 'people'), ]; final output = Migration.generate(commands, 15); @@ -170,8 +121,8 @@ class Migration15 extends Migration { test('null drop commands are not reported', () { final commands = [ - DropColumn('first_name', onTable: 'people'), - DropColumn('last_name', onTable: 'people'), + const DropColumn('first_name', onTable: 'people'), + const DropColumn('last_name', onTable: 'people'), ]; final output = Migration.generate(commands, 15); diff --git a/packages/brick_sqlite/test/db/schema_column_test.dart b/packages/brick_sqlite/test/db/schema_column_test.dart index 06c662ea..062f4fb5 100644 --- a/packages/brick_sqlite/test/db/schema_column_test.dart +++ b/packages/brick_sqlite/test/db/schema_column_test.dart @@ -1,4 +1,4 @@ -import 'package:brick_sqlite/src/db/migration.dart'; +import 'package:brick_sqlite/src/db/column.dart'; import 'package:brick_sqlite/src/db/migration_commands/insert_column.dart'; import 'package:brick_sqlite/src/db/migration_commands/insert_foreign_key.dart'; import 'package:brick_sqlite/src/db/migration_commands/insert_table.dart'; @@ -10,7 +10,7 @@ void main() { test('isPrimaryKey must be int', () { expect( () => SchemaColumn(InsertTable.PRIMARY_KEY_COLUMN, Column.varchar, isPrimaryKey: true), - throwsA(TypeMatcher()), + throwsA(const TypeMatcher()), ); }); @@ -69,8 +69,7 @@ void main() { group('#toCommand', () { test('simple', () { - final column = SchemaColumn('first_name', Column.varchar); - column.tableName = 'demo'; + final column = SchemaColumn('first_name', Column.varchar)..tableName = 'demo'; expect( column.toCommand(), const InsertColumn('first_name', Column.varchar, onTable: 'demo'), @@ -79,8 +78,8 @@ void main() { test('primary key', () { final column = - SchemaColumn('_brick_id', Column.integer, autoincrement: true, isPrimaryKey: true); - column.tableName = 'demo'; + SchemaColumn('_brick_id', Column.integer, autoincrement: true, isPrimaryKey: true) + ..tableName = 'demo'; expect( column.toCommand(), const InsertColumn('_brick_id', Column.integer, onTable: 'demo', autoincrement: true), @@ -88,8 +87,7 @@ void main() { }); test('defaultValue', () { - final column = SchemaColumn('amount', Column.integer, defaultValue: 0); - column.tableName = 'demo'; + final column = SchemaColumn('amount', Column.integer, defaultValue: 0)..tableName = 'demo'; expect( column.toCommand(), const InsertColumn('amount', Column.integer, onTable: 'demo', defaultValue: 0), @@ -97,8 +95,8 @@ void main() { }); test('nullable', () { - final column = SchemaColumn('last_name', Column.varchar, nullable: false); - column.tableName = 'demo'; + final column = SchemaColumn('last_name', Column.varchar, nullable: false) + ..tableName = 'demo'; expect( column.toCommand(), const InsertColumn('last_name', Column.varchar, onTable: 'demo', nullable: false), @@ -107,8 +105,8 @@ void main() { test('association', () { final column = - SchemaColumn('Hat_id', Column.integer, isForeignKey: true, foreignTableName: 'hat'); - column.tableName = 'demo'; + SchemaColumn('Hat_id', Column.integer, isForeignKey: true, foreignTableName: 'hat') + ..tableName = 'demo'; expect( column.toCommand(), const InsertForeignKey('demo', 'hat', foreignKeyColumn: 'Hat_id'), @@ -116,8 +114,7 @@ void main() { }); test('columnType', () { - final column = SchemaColumn('image', Column.blob); - column.tableName = 'demo'; + final column = SchemaColumn('image', Column.blob)..tableName = 'demo'; expect(column.toCommand(), const InsertColumn('image', Column.blob, onTable: 'demo')); }); }); diff --git a/packages/brick_sqlite/test/db/schema_difference_test.dart b/packages/brick_sqlite/test/db/schema_difference_test.dart index b0c7e998..a0794a32 100644 --- a/packages/brick_sqlite/test/db/schema_difference_test.dart +++ b/packages/brick_sqlite/test/db/schema_difference_test.dart @@ -1,4 +1,4 @@ -import 'package:brick_sqlite/src/db/migration.dart'; +import 'package:brick_sqlite/src/db/column.dart'; import 'package:brick_sqlite/src/db/migration_commands/create_index.dart'; import 'package:brick_sqlite/src/db/migration_commands/drop_column.dart'; import 'package:brick_sqlite/src/db/migration_commands/drop_table.dart'; @@ -238,7 +238,7 @@ void main() { expect( diff.toMigrationCommands(), [ - InsertTable('demo'), + const InsertTable('demo'), InsertColumn(column.name, Column.varchar, onTable: column.tableName!), ], ); @@ -254,7 +254,7 @@ void main() { expect( diff.toMigrationCommands(), [ - InsertTable('demo'), + const InsertTable('demo'), InsertColumn(column.name, Column.varchar, onTable: column.tableName!), ], ); @@ -276,7 +276,7 @@ void main() { final newSchema = Schema(1, tables: {}); final diff = SchemaDifference(oldSchema, newSchema); - expect(diff.toMigrationCommands(), [DropTable('demo')]); + expect(diff.toMigrationCommands(), [const DropTable('demo')]); expect(diff.droppedTables, hasLength(1)); expect(diff.insertedTables, isEmpty); }); @@ -320,22 +320,20 @@ void main() { final diff = SchemaDifference(oldSchema, newSchema); expect(diff.toMigrationCommands(), [ - InsertTable('_brick_People_friend'), - InsertForeignKey( + const InsertTable('_brick_People_friend'), + const InsertForeignKey( '_brick_People_friend', 'People', foreignKeyColumn: 'l_People_brick_id', - onDeleteCascade: false, onDeleteSetDefault: true, ), - InsertForeignKey( + const InsertForeignKey( '_brick_People_friend', 'Friend', foreignKeyColumn: 'f_Friend_brick_id', - onDeleteCascade: false, onDeleteSetDefault: true, ), - CreateIndex( + const CreateIndex( columns: ['l_People_brick_id', 'f_Friend_brick_id'], onTable: '_brick_People_friend', unique: true, @@ -368,7 +366,7 @@ void main() { final old = Schema(2, tables: {}); final fresh = Schema(1, tables: {}); - expect(() => SchemaDifference(old, fresh), throwsA(TypeMatcher())); + expect(() => SchemaDifference(old, fresh), throwsA(const TypeMatcher())); }); }); } diff --git a/packages/brick_sqlite/test/db/schema_table_test.dart b/packages/brick_sqlite/test/db/schema_table_test.dart index 67ee252e..679c667c 100644 --- a/packages/brick_sqlite/test/db/schema_table_test.dart +++ b/packages/brick_sqlite/test/db/schema_table_test.dart @@ -1,4 +1,4 @@ -import 'package:brick_sqlite/src/db/migration.dart'; +import 'package:brick_sqlite/src/db/column.dart'; import 'package:brick_sqlite/src/db/migration_commands/drop_table.dart'; import 'package:brick_sqlite/src/db/migration_commands/insert_column.dart'; import 'package:brick_sqlite/src/db/migration_commands/insert_foreign_key.dart'; @@ -38,7 +38,7 @@ void main() { final table = SchemaTable('users', columns: {}); test('shouldDrop:false', () { - expect(table.toCommand(shouldDrop: false), const InsertTable('users')); + expect(table.toCommand(), const InsertTable('users')); }); test('shouldDrop:true', () { @@ -87,7 +87,7 @@ void main() { test('isPrimaryKey must be int', () { expect( () => SchemaColumn(InsertTable.PRIMARY_KEY_COLUMN, Column.varchar, isPrimaryKey: true), - throwsA(TypeMatcher()), + throwsA(const TypeMatcher()), ); }); @@ -139,33 +139,30 @@ void main() { }); test('#toCommand', () { - var column = SchemaColumn('first_name', Column.varchar); - column.tableName = 'demo'; + var column = SchemaColumn('first_name', Column.varchar)..tableName = 'demo'; expect(column.toCommand(), const InsertColumn('first_name', Column.varchar, onTable: 'demo')); - column = SchemaColumn('_brick_id', Column.integer, autoincrement: true, isPrimaryKey: true); - column.tableName = 'demo'; + column = SchemaColumn('_brick_id', Column.integer, autoincrement: true, isPrimaryKey: true) + ..tableName = 'demo'; expect( column.toCommand(), const InsertColumn('_brick_id', Column.integer, onTable: 'demo', autoincrement: true), ); - column = SchemaColumn('amount', Column.integer, defaultValue: 0); - column.tableName = 'demo'; + column = SchemaColumn('amount', Column.integer, defaultValue: 0)..tableName = 'demo'; expect( column.toCommand(), const InsertColumn('amount', Column.integer, onTable: 'demo', defaultValue: 0), ); - column = SchemaColumn('last_name', Column.varchar, nullable: false); - column.tableName = 'demo'; + column = SchemaColumn('last_name', Column.varchar, nullable: false)..tableName = 'demo'; expect( column.toCommand(), const InsertColumn('last_name', Column.varchar, onTable: 'demo', nullable: false), ); - column = SchemaColumn('Hat_id', Column.integer, isForeignKey: true, foreignTableName: 'hat'); - column.tableName = 'demo'; + column = SchemaColumn('Hat_id', Column.integer, isForeignKey: true, foreignTableName: 'hat') + ..tableName = 'demo'; expect(column.toCommand(), const InsertForeignKey('demo', 'hat', foreignKeyColumn: 'Hat_id')); column = SchemaColumn( @@ -174,8 +171,7 @@ void main() { isForeignKey: true, foreignTableName: 'hat', onDeleteCascade: true, - ); - column.tableName = 'demo'; + )..tableName = 'demo'; expect( column.toCommand(), const InsertForeignKey('demo', 'hat', foreignKeyColumn: 'Hat_id', onDeleteCascade: true), @@ -187,8 +183,7 @@ void main() { isForeignKey: true, foreignTableName: 'hat', onDeleteSetDefault: true, - ); - column.tableName = 'demo'; + )..tableName = 'demo'; expect( column.toCommand(), const InsertForeignKey( diff --git a/packages/brick_sqlite/test/db/schema_test.dart b/packages/brick_sqlite/test/db/schema_test.dart index 42211fe7..f1458982 100644 --- a/packages/brick_sqlite/test/db/schema_test.dart +++ b/packages/brick_sqlite/test/db/schema_test.dart @@ -1,3 +1,4 @@ +import 'package:brick_sqlite/src/db/column.dart'; import 'package:brick_sqlite/src/db/migration.dart'; import 'package:brick_sqlite/src/db/migration_commands/insert_table.dart'; import 'package:brick_sqlite/src/db/migration_commands/rename_column.dart'; @@ -50,8 +51,8 @@ void main() { group('RenameTable', () { test('without a prior, relevant insert migration', () { expect( - () => Schema.fromMigrations({Migration0None(), renameTable}), - throwsA(TypeMatcher()), + () => Schema.fromMigrations({const Migration0None(), renameTable}), + throwsA(const TypeMatcher()), ); }); @@ -83,8 +84,8 @@ void main() { group('DropTable', () { test('without a prior, relevant insert migration', () { expect( - () => Schema.fromMigrations({Migration0None(), dropTable}), - throwsA(TypeMatcher()), + () => Schema.fromMigrations({const Migration0None(), dropTable}), + throwsA(const TypeMatcher()), ); }); @@ -103,8 +104,8 @@ void main() { group('InsertColumn', () { test('without a prior, relevant InsertTable migration', () { expect( - () => Schema.fromMigrations({Migration0None(), insertColumn}), - throwsA(TypeMatcher()), + () => Schema.fromMigrations({const Migration0None(), insertColumn}), + throwsA(const TypeMatcher()), ); }); @@ -137,15 +138,15 @@ void main() { group('RenameColumn', () { test('without a prior, relevant InsertTable migration', () { expect( - () => Schema.fromMigrations({Migration0None(), renameColumn}), - throwsA(TypeMatcher()), + () => Schema.fromMigrations({const Migration0None(), renameColumn}), + throwsA(const TypeMatcher()), ); }); test('without a prior, relevant InsertColumn migration', () { expect( () => Schema.fromMigrations({insertTable, renameColumn}), - throwsA(TypeMatcher()), + throwsA(const TypeMatcher()), ); }); @@ -178,8 +179,8 @@ void main() { group('InsertForeignKey', () { test('without a prior, relevant InsertTable migration', () { expect( - () => Schema.fromMigrations({Migration0None(), insertForeignKey}), - throwsA(TypeMatcher()), + () => Schema.fromMigrations({const Migration0None(), insertForeignKey}), + throwsA(const TypeMatcher()), ); }); @@ -301,7 +302,7 @@ void main() { }, ); - final newSchema = Schema.fromMigrations({insertTable, Migration2()}); + final newSchema = Schema.fromMigrations({insertTable, const Migration2()}); expect(newSchema.tables, schema.tables); expect(newSchema.version, schema.version); }); @@ -314,20 +315,23 @@ void main() { }); test("version uses the migrations' largest version if not provided", () { - expect(Schema.fromMigrations({Migration2(), Migration1()}).version, 2); + expect(Schema.fromMigrations({const Migration2(), const Migration1()}).version, 2); }); }); test('.expandMigrations', () { - final migrations = {MigrationInsertTable(), MigrationRenameColumn()}; + final migrations = {const MigrationInsertTable(), const MigrationRenameColumn()}; final commands = Schema.expandMigrations(migrations); // Maintains sort order - expect(commands, [InsertTable('demo'), RenameColumn('name', 'first_name', onTable: 'demo')]); + expect( + commands, + [const InsertTable('demo'), const RenameColumn('name', 'first_name', onTable: 'demo')], + ); }); test('#forGenerator', () { - final schema = Schema.fromMigrations({MigrationInsertTable(), Migration2()}); + final schema = Schema.fromMigrations({const MigrationInsertTable(), const Migration2()}); expect(schema.forGenerator, ''' Schema( diff --git a/packages/brick_sqlite/test/memory_cache_provider_test.dart b/packages/brick_sqlite/test/memory_cache_provider_test.dart index 38cd7c7f..0f91be63 100644 --- a/packages/brick_sqlite/test/memory_cache_provider_test.dart +++ b/packages/brick_sqlite/test/memory_cache_provider_test.dart @@ -36,8 +36,7 @@ void main() { }); test('#delete', () { - final instance = Person(); - instance.primaryKey = 1; + final instance = Person()..primaryKey = 1; provider.hydrate([instance]); expect(provider.managedObjects, isNotEmpty); expect(provider.managedObjects[Person], isNotEmpty); @@ -53,13 +52,12 @@ void main() { }); test('.id queries', () { - final instance = Person(); - instance.primaryKey = 1; + final instance = Person()..primaryKey = 1; provider.hydrate([instance]); final results = provider.get( - query: Query( + query: const Query( where: [Where.exact(InsertTable.PRIMARY_KEY_FIELD, 1)], - providerArgs: {'limit': 1}, + limit: 1, ), ); expect(results, isNotEmpty); @@ -67,8 +65,7 @@ void main() { }); test('unlimited queries', () { - final instance = Person(); - instance.primaryKey = 1; + final instance = Person()..primaryKey = 1; provider.hydrate([instance]); final results = provider.get(); @@ -79,8 +76,7 @@ void main() { test('#hydrate', () { expect(provider.managedObjects, isEmpty); - final instance = Person(); - instance.primaryKey = 1; + final instance = Person()..primaryKey = 1; provider.hydrate([instance]); expect(provider.managedObjects, isNotEmpty); expect(provider.managedObjects[Person], isNotNull); @@ -95,8 +91,7 @@ void main() { }); test('#reset', () { - final instance = Person(); - instance.primaryKey = 1; + final instance = Person()..primaryKey = 1; provider.hydrate([instance]); expect(provider.managedObjects[Person], isNotEmpty); provider.reset(); diff --git a/packages/brick_sqlite/test/query_sql_transformer_test.dart b/packages/brick_sqlite/test/query_sql_transformer_test.dart index 67e72e73..a25dbc17 100644 --- a/packages/brick_sqlite/test/query_sql_transformer_test.dart +++ b/packages/brick_sqlite/test/query_sql_transformer_test.dart @@ -1,5 +1,6 @@ import 'package:brick_core/query.dart'; import 'package:brick_sqlite/src/helpers/query_sql_transformer.dart'; +import 'package:brick_sqlite/src/sqlite_provider_query.dart'; import 'package:sqflite_common/sqlite_api.dart'; import 'package:sqflite_common/src/mixin/factory.dart'; import 'package:test/test.dart'; @@ -21,9 +22,8 @@ class _FakeMethodCall { this.rawFactory = false, }); - factory _FakeMethodCall.fromFactory(String method, dynamic arguments) { - return _FakeMethodCall(method, arguments, rawFactory: true); - } + factory _FakeMethodCall.fromFactory(String method, dynamic arguments) => + _FakeMethodCall(method, arguments, rawFactory: true); @override String toString() { @@ -107,17 +107,17 @@ void main() { query: Query( where: [ WherePhrase([ - Where.exact('id', 1), + const Where.exact('id', 1), const Or('name').isExactly('Guy'), ]), - WherePhrase( + const WherePhrase( [ Where.exact('id', 1), Where.exact('name', 'Guy'), ], isRequired: true, ), - WherePhrase([ + const WherePhrase([ Where.exact('id', 1), Where.exact('name', 'Guy'), ]), @@ -137,7 +137,7 @@ void main() { modelDictionary: dictionary, query: Query( where: [ - Where.exact('id', 1), + const Where.exact('id', 1), WherePhrase( [ const Or('name').isExactly('Thomas'), @@ -194,17 +194,17 @@ void main() { query: Query( where: [ WherePhrase([ - Where.exact('id', 1), + const Where.exact('id', 1), const Or('name').isExactly('Guy'), ]), - WherePhrase( + const WherePhrase( [ Where.exact('id', 1), Where.exact('name', 'Guy'), ], isRequired: true, ), - WherePhrase([ + const WherePhrase([ Where.exact('id', 1), Where.exact('name', 'Guy'), ]), @@ -223,7 +223,7 @@ void main() { 'SELECT COUNT(*) FROM `DemoModel` INNER JOIN `DemoModelAssoc` ON `DemoModel`.assoc_DemoModelAssoc_brick_id = `DemoModelAssoc`._brick_id WHERE `DemoModelAssoc`.id = ?'; final sqliteQuery = QuerySqlTransformer( modelDictionary: dictionary, - query: Query( + query: const Query( where: [ Where.exact('assoc', Where.exact('id', 1)), ], @@ -241,7 +241,7 @@ void main() { 'SELECT COUNT(*) FROM `DemoModel` INNER JOIN `DemoModelAssoc` ON `DemoModel`.assoc_DemoModelAssoc_brick_id = `DemoModelAssoc`._brick_id WHERE `DemoModelAssoc`.id = ? AND `DemoModel`.id = ?'; final sqliteQuery = QuerySqlTransformer( modelDictionary: dictionary, - query: Query( + query: const Query( where: [ Where.exact('assoc', Where.exact('id', 1)), Where.exact('id', 1), @@ -259,7 +259,7 @@ void main() { const statement = 'SELECT COUNT(*) FROM `DemoModel`'; final sqliteQuery = QuerySqlTransformer( modelDictionary: dictionary, - query: Query( + query: const Query( where: [ WherePhrase([], isRequired: false), ], @@ -279,7 +279,7 @@ void main() { 'SELECT DISTINCT `DemoModel`.* FROM `DemoModel` INNER JOIN `DemoModelAssoc` ON `DemoModel`.assoc_DemoModelAssoc_brick_id = `DemoModelAssoc`._brick_id WHERE `DemoModelAssoc`.id = ?'; final sqliteQuery = QuerySqlTransformer( modelDictionary: dictionary, - query: Query( + query: const Query( where: [ Where.exact('assoc', Where.exact('id', 1)), ], @@ -296,7 +296,7 @@ void main() { 'SELECT DISTINCT `DemoModel`.* FROM `DemoModel` INNER JOIN `_brick_DemoModel_many_assoc` ON `DemoModel`._brick_id = `_brick_DemoModel_many_assoc`.l_DemoModel_brick_id INNER JOIN `DemoModelAssoc` ON `DemoModelAssoc`._brick_id = `_brick_DemoModel_many_assoc`.f_DemoModelAssoc_brick_id WHERE `DemoModelAssoc`.full_name = ?'; final sqliteQuery = QuerySqlTransformer( modelDictionary: dictionary, - query: Query( + query: const Query( where: [ Where.exact('manyAssoc', Where.exact('assoc', Where.exact('name', 1))), ], @@ -336,7 +336,7 @@ void main() { 'SELECT DISTINCT `DemoModel`.* FROM `DemoModel` INNER JOIN `_brick_DemoModel_many_assoc` ON `DemoModel`._brick_id = `_brick_DemoModel_many_assoc`.l_DemoModel_brick_id INNER JOIN `DemoModelAssoc` ON `DemoModelAssoc`._brick_id = `_brick_DemoModel_many_assoc`.f_DemoModelAssoc_brick_id WHERE `DemoModelAssoc`.id = ?'; final sqliteQuery = QuerySqlTransformer( modelDictionary: dictionary, - query: Query( + query: const Query( where: [ Where.exact( 'manyAssoc', @@ -355,7 +355,7 @@ void main() { const statement = 'SELECT DISTINCT `DemoModel`.* FROM `DemoModel` COLLATE NOCASE'; final sqliteQuery = QuerySqlTransformer( modelDictionary: dictionary, - query: Query(providerArgs: {'collate': 'NOCASE'}), + query: const Query(providerArgs: {'collate': 'NOCASE'}), ); await db.rawQuery(sqliteQuery.statement, sqliteQuery.values); @@ -367,7 +367,7 @@ void main() { const statement = 'SELECT DISTINCT `DemoModel`.* FROM `DemoModel` GROUP BY id'; final sqliteQuery = QuerySqlTransformer( modelDictionary: dictionary, - query: Query(providerArgs: {'groupBy': 'id'}), + query: const Query(providerArgs: {'groupBy': 'id'}), ); await db.rawQuery(sqliteQuery.statement, sqliteQuery.values); @@ -379,7 +379,7 @@ void main() { const statement = 'SELECT DISTINCT `DemoModel`.* FROM `DemoModel` HAVING id'; final sqliteQuery = QuerySqlTransformer( modelDictionary: dictionary, - query: Query(providerArgs: {'having': 'id'}), + query: const Query(providerArgs: {'having': 'id'}), ); await db.rawQuery(sqliteQuery.statement, sqliteQuery.values); @@ -391,7 +391,7 @@ void main() { const statement = 'SELECT DISTINCT `DemoModel`.* FROM `DemoModel` LIMIT 1'; final sqliteQuery = QuerySqlTransformer( modelDictionary: dictionary, - query: Query(providerArgs: {'limit': 1}), + query: const Query(providerArgs: {'limit': 1}), ); await db.rawQuery(sqliteQuery.statement, sqliteQuery.values); @@ -403,12 +403,7 @@ void main() { const statement = 'SELECT DISTINCT `DemoModel`.* FROM `DemoModel` LIMIT 1 OFFSET 1'; final sqliteQuery = QuerySqlTransformer( modelDictionary: dictionary, - query: Query( - providerArgs: { - 'limit': 1, - 'offset': 1, - }, - ), + query: const Query(providerArgs: {'limit': 1, 'offset': 1}), ); await db.rawQuery(sqliteQuery.statement, sqliteQuery.values); @@ -421,7 +416,7 @@ void main() { const statement = 'SELECT DISTINCT `DemoModel`.* FROM `DemoModel` ORDER BY id DESC'; final sqliteQuery = QuerySqlTransformer( modelDictionary: dictionary, - query: Query(providerArgs: {'orderBy': 'id DESC'}), + query: const Query(providerArgs: {'orderBy': 'id DESC'}), ); await db.rawQuery(sqliteQuery.statement, sqliteQuery.values); @@ -434,7 +429,7 @@ void main() { 'SELECT DISTINCT `DemoModel`.* FROM `DemoModel` ORDER BY last_name DESC'; final sqliteQuery = QuerySqlTransformer( modelDictionary: dictionary, - query: Query(providerArgs: {'orderBy': 'lastName DESC'}), + query: const Query(providerArgs: {'orderBy': 'lastName DESC'}), ); await db.rawQuery(sqliteQuery.statement, sqliteQuery.values); @@ -447,7 +442,7 @@ void main() { const statement = 'SELECT DISTINCT `DemoModel`.* FROM `DemoModel` ORDER BY many_assoc DESC'; final sqliteQuery = QuerySqlTransformer( modelDictionary: dictionary, - query: Query(providerArgs: {'orderBy': 'manyAssoc DESC'}), + query: const Query(providerArgs: {'orderBy': 'manyAssoc DESC'}), ); await db.rawQuery(sqliteQuery.statement, sqliteQuery.values); @@ -460,7 +455,7 @@ void main() { 'SELECT DISTINCT `DemoModel`.* FROM `DemoModel` ORDER BY many_assoc DESC, complex_field_name ASC'; final sqliteQuery = QuerySqlTransformer( modelDictionary: dictionary, - query: Query( + query: const Query( providerArgs: { 'orderBy': 'manyAssoc DESC, complexFieldName ASC', }, @@ -477,11 +472,11 @@ void main() { 'SELECT DISTINCT `DemoModel`.* FROM `DemoModel` ORDER BY complex_field_name ASC GROUP BY complex_field_name HAVING complex_field_name > 1000'; final sqliteQuery = QuerySqlTransformer( modelDictionary: dictionary, - query: Query( + query: const Query( providerArgs: { - 'orderBy': 'complexFieldName ASC', 'having': 'complexFieldName > 1000', 'groupBy': 'complexFieldName', + 'orderBy': 'complexFieldName ASC', }, ), ); @@ -493,11 +488,11 @@ void main() { }); test('date time is converted', () async { - final statement = + const statement = 'SELECT DISTINCT `DemoModel`.* FROM `DemoModel` ORDER BY datetime(simple_time) DESC'; final sqliteQuery = QuerySqlTransformer( modelDictionary: dictionary, - query: Query(providerArgs: {'orderBy': 'simpleTime DESC'}), + query: const Query(providerArgs: {'orderBy': 'simpleTime DESC'}), ); await db.rawQuery(sqliteQuery.statement, sqliteQuery.values); @@ -510,11 +505,11 @@ void main() { // future Brick releases. // https://github.com/GetDutchie/brick/issues/429 test('incorrectly cased columns are forwarded as is', () async { - final statement = + const statement = 'SELECT DISTINCT `DemoModel`.* FROM `DemoModel` ORDER BY complex_field_name DESC'; final sqliteQuery = QuerySqlTransformer( modelDictionary: dictionary, - query: Query(providerArgs: {'orderBy': 'complex_field_name DESC'}), + query: const Query(providerArgs: {'orderBy': 'complex_field_name DESC'}), ); await db.rawQuery(sqliteQuery.statement, sqliteQuery.values); @@ -527,11 +522,205 @@ void main() { // guaranteed in future Brick releases. // https://github.com/GetDutchie/brick/issues/429 test('ordering by association is forwarded as is', () async { - final statement = + const statement = 'SELECT DISTINCT `DemoModel`.* FROM `DemoModel` ORDER BY other_table.complex_field_name DESC'; final sqliteQuery = QuerySqlTransformer( modelDictionary: dictionary, - query: Query(providerArgs: {'orderBy': 'other_table.complex_field_name DESC'}), + query: const Query(providerArgs: {'orderBy': 'other_table.complex_field_name DESC'}), + ); + await db.rawQuery(sqliteQuery.statement, sqliteQuery.values); + + expect(statement, sqliteQuery.statement); + sqliteStatementExpectation(statement); + }); + }); + + group('Query', () { + test('#collate', () async { + const statement = 'SELECT DISTINCT `DemoModel`.* FROM `DemoModel` COLLATE NOCASE'; + final sqliteQuery = QuerySqlTransformer( + modelDictionary: dictionary, + query: const Query(forProviders: [SqliteProviderQuery(collate: 'NOCASE')]), + ); + await db.rawQuery(sqliteQuery.statement, sqliteQuery.values); + + expect(statement, sqliteQuery.statement); + sqliteStatementExpectation(statement); + }); + + test('#groupBy', () async { + const statement = 'SELECT DISTINCT `DemoModel`.* FROM `DemoModel` GROUP BY id'; + final sqliteQuery = QuerySqlTransformer( + modelDictionary: dictionary, + query: const Query(forProviders: [SqliteProviderQuery(groupBy: 'id')]), + ); + await db.rawQuery(sqliteQuery.statement, sqliteQuery.values); + + expect(statement, sqliteQuery.statement); + sqliteStatementExpectation(statement); + }); + + test('#having', () async { + const statement = 'SELECT DISTINCT `DemoModel`.* FROM `DemoModel` HAVING id'; + final sqliteQuery = QuerySqlTransformer( + modelDictionary: dictionary, + query: const Query(forProviders: [SqliteProviderQuery(having: 'id')]), + ); + await db.rawQuery(sqliteQuery.statement, sqliteQuery.values); + + expect(statement, sqliteQuery.statement); + sqliteStatementExpectation(statement); + }); + + test('#limit', () async { + const statement = 'SELECT DISTINCT `DemoModel`.* FROM `DemoModel` LIMIT 1'; + final sqliteQuery = QuerySqlTransformer( + modelDictionary: dictionary, + query: const Query(limit: 1), + ); + await db.rawQuery(sqliteQuery.statement, sqliteQuery.values); + + expect(statement, sqliteQuery.statement); + sqliteStatementExpectation(statement); + }); + + test('#limitBy is ignored', () async { + const statement = 'SELECT DISTINCT `DemoModel`.* FROM `DemoModel`'; + final sqliteQuery = QuerySqlTransformer( + modelDictionary: dictionary, + query: const Query(limitBy: [LimitBy(1, evaluatedField: 'name')]), + ); + await db.rawQuery(sqliteQuery.statement, sqliteQuery.values); + + expect(statement, sqliteQuery.statement); + sqliteStatementExpectation(statement); + }); + + test('#offset', () async { + const statement = 'SELECT DISTINCT `DemoModel`.* FROM `DemoModel` LIMIT 1 OFFSET 1'; + final sqliteQuery = QuerySqlTransformer( + modelDictionary: dictionary, + query: const Query(limit: 1, offset: 1), + ); + await db.rawQuery(sqliteQuery.statement, sqliteQuery.values); + + expect(statement, sqliteQuery.statement); + sqliteStatementExpectation(statement); + }); + + group('#orderBy', () { + test('simple', () async { + const statement = 'SELECT DISTINCT `DemoModel`.* FROM `DemoModel` ORDER BY id DESC'; + final sqliteQuery = QuerySqlTransformer( + modelDictionary: dictionary, + query: const Query(orderBy: [OrderBy.desc('id')]), + ); + await db.rawQuery(sqliteQuery.statement, sqliteQuery.values); + + expect(statement, sqliteQuery.statement); + sqliteStatementExpectation(statement); + }); + + test('discovered columns share similar names', () async { + const statement = + 'SELECT DISTINCT `DemoModel`.* FROM `DemoModel` ORDER BY last_name DESC'; + final sqliteQuery = QuerySqlTransformer( + modelDictionary: dictionary, + query: const Query(orderBy: [OrderBy.desc('lastName')]), + ); + await db.rawQuery(sqliteQuery.statement, sqliteQuery.values); + + expect(statement, sqliteQuery.statement); + sqliteStatementExpectation(statement); + }); + + test('expands field names to column names', () async { + const statement = + 'SELECT DISTINCT `DemoModel`.* FROM `DemoModel` ORDER BY many_assoc DESC'; + final sqliteQuery = QuerySqlTransformer( + modelDictionary: dictionary, + query: const Query(orderBy: [OrderBy.desc('manyAssoc')]), + ); + await db.rawQuery(sqliteQuery.statement, sqliteQuery.values); + + expect(statement, sqliteQuery.statement); + sqliteStatementExpectation(statement); + }); + + test('compound values are expanded to column names', () async { + const statement = + 'SELECT DISTINCT `DemoModel`.* FROM `DemoModel` ORDER BY many_assoc DESC, complex_field_name ASC'; + final sqliteQuery = QuerySqlTransformer( + modelDictionary: dictionary, + query: const Query( + orderBy: [OrderBy.desc('manyAssoc'), OrderBy.asc('complexFieldName')], + ), + ); + await db.rawQuery(sqliteQuery.statement, sqliteQuery.values); + + expect(statement, sqliteQuery.statement); + sqliteStatementExpectation(statement); + }); + }); + + test('fields convert to column names in providerArgs values', () async { + const statement = + 'SELECT DISTINCT `DemoModel`.* FROM `DemoModel` ORDER BY complex_field_name ASC GROUP BY complex_field_name HAVING complex_field_name > 1000'; + final sqliteQuery = QuerySqlTransformer( + modelDictionary: dictionary, + query: const Query( + orderBy: [OrderBy.asc('complexFieldName')], + forProviders: [ + SqliteProviderQuery( + having: 'complexFieldName > 1000', + groupBy: 'complexFieldName', + ), + ], + ), + ); + + await db.rawQuery(sqliteQuery.statement, sqliteQuery.values); + + expect(statement, sqliteQuery.statement); + sqliteStatementExpectation(statement); + }); + + test('date time is converted', () async { + const statement = + 'SELECT DISTINCT `DemoModel`.* FROM `DemoModel` ORDER BY datetime(simple_time) DESC'; + final sqliteQuery = QuerySqlTransformer( + modelDictionary: dictionary, + query: const Query(orderBy: [OrderBy.desc('simpleTime')]), + ); + await db.rawQuery(sqliteQuery.statement, sqliteQuery.values); + + expect(statement, sqliteQuery.statement); + sqliteStatementExpectation(statement); + }); + + // This behavior is not explicitly supported - field names should be used. + // This is considered functionality behavior and is not guaranteed in + // future Brick releases. + // https://github.com/GetDutchie/brick/issues/429 + test('incorrectly cased columns are forwarded as is', () async { + const statement = + 'SELECT DISTINCT `DemoModel`.* FROM `DemoModel` ORDER BY complex_field_name DESC'; + final sqliteQuery = QuerySqlTransformer( + modelDictionary: dictionary, + query: const Query(orderBy: [OrderBy.desc('complex_field_name')]), + ); + await db.rawQuery(sqliteQuery.statement, sqliteQuery.values); + + expect(statement, sqliteQuery.statement); + sqliteStatementExpectation(statement); + }); + + test('ordering by association uses the specified model table', () async { + const statement = + 'SELECT DISTINCT `DemoModel`.* FROM `DemoModel` ORDER BY `DemoModelAssoc`.full_name DESC'; + final sqliteQuery = QuerySqlTransformer( + modelDictionary: dictionary, + query: const Query(orderBy: [OrderBy.desc('assoc', associationField: 'name')]), ); await db.rawQuery(sqliteQuery.statement, sqliteQuery.values); diff --git a/packages/brick_sqlite/test/sqlite_provider_test.dart b/packages/brick_sqlite/test/sqlite_provider_test.dart index 57ee0d04..1c1e0dc7 100644 --- a/packages/brick_sqlite/test/sqlite_provider_test.dart +++ b/packages/brick_sqlite/test/sqlite_provider_test.dart @@ -1,6 +1,5 @@ import 'package:brick_core/core.dart'; import 'package:brick_sqlite/brick_sqlite.dart'; -import 'package:brick_sqlite/db.dart'; import 'package:brick_sqlite/src/db/migration_commands/insert_table.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import 'package:test/test.dart'; @@ -118,7 +117,7 @@ void main() { test('with an offset', () async { await provider.upsert(DemoModel(name: 'Guy')); final existingModels = await provider.get(); - final query = Query(providerArgs: {'limit': 1, 'offset': existingModels.length}); + final query = Query(limit: 1, offset: existingModels.length); final doesExistWithoutModel = await provider.exists(query: query); expect(doesExistWithoutModel, isFalse); @@ -134,7 +133,8 @@ void main() { .upsert(DemoModel(name: 'Guy', manyAssoc: [DemoModelAssoc(name: 'Thomas')])); final query = Query( where: [const Where('manyAssoc').isExactly(const Where('name').isExactly('Thomas'))], - providerArgs: {'limit': 1, 'offset': 1}, + limit: 1, + offset: 1, ); final doesExistWithoutModel = await provider.exists(query: query); diff --git a/packages/brick_sqlite_generators/CHANGELOG.md b/packages/brick_sqlite_generators/CHANGELOG.md index cfc91ccc..68770898 100644 --- a/packages/brick_sqlite_generators/CHANGELOG.md +++ b/packages/brick_sqlite_generators/CHANGELOG.md @@ -1,5 +1,10 @@ ## Unreleased +## 3.3.0 + +- Upgrade `brick_core` to `1.3.0` +- Update analysis to modern lints + ## 3.2.2 - Revert `.getDisplayString()` change due to Flutter 3.22 being restricted to analyzer <6.4.1. `meta` is pinned to `1.12` in this version of Flutter, and `analyzer >=6.5.0`, where the change was made, requires `meta >= 1.15`. This change will eventually be re-reverted. diff --git a/packages/brick_sqlite_generators/lib/sqlite_model_serdes_generator.dart b/packages/brick_sqlite_generators/lib/sqlite_model_serdes_generator.dart index 2ddc37c4..08c8ca43 100644 --- a/packages/brick_sqlite_generators/lib/sqlite_model_serdes_generator.dart +++ b/packages/brick_sqlite_generators/lib/sqlite_model_serdes_generator.dart @@ -12,6 +12,8 @@ class SqliteModelSerdesGenerator extends ProviderSerializableGenerator extends SqliteBaseBuilder<_ClassAnnotation> { @override final outputExtension = '.migration_builder.dart'; + /// Create a new [Migration] from the contents of all `ConnectOfflineFirstWith` models NewMigrationBuilder(); @override @@ -15,14 +17,13 @@ class NewMigrationBuilder<_ClassAnnotation> extends SqliteBaseBuilder<_ClassAnno final fieldses = await sqliteFieldsFromBuildStep(buildStep); final now = DateTime.now().toUtc(); final timestamp = - [now.month, now.day, now.hour, now.minute, now.second].map(_padToTwo).toList().join(''); + [now.month, now.day, now.hour, now.minute, now.second].map(_padToTwo).toList().join(); final version = int.parse('${now.year}$timestamp'); final output = schemaGenerator.createMigration(libraryReader, fieldses, version: version); if (output == null) return; - final stopwatch = Stopwatch(); - stopwatch.start(); + final stopwatch = Stopwatch()..start(); // in a perfect world, the schema would not be edited in such a brittle way // however, reruning the schema generator here doesn't pick up the new migration diff --git a/packages/brick_sqlite_generators/lib/src/builders/sqlite_base_builder.dart b/packages/brick_sqlite_generators/lib/src/builders/sqlite_base_builder.dart index 3d8a125a..5b3730c8 100644 --- a/packages/brick_sqlite_generators/lib/src/builders/sqlite_base_builder.dart +++ b/packages/brick_sqlite_generators/lib/src/builders/sqlite_base_builder.dart @@ -9,11 +9,15 @@ export 'package:brick_build/src/annotation_super_generator.dart'; const _schemaGenerator = SqliteSchemaGenerator(); +/// abstract class SqliteBaseBuilder<_ClassAnnotation> extends BaseBuilder<_ClassAnnotation> { + /// SqliteSchemaGenerator get schemaGenerator => _schemaGenerator; + /// SqliteBaseBuilder(); + /// Future> sqliteFieldsFromBuildStep(BuildStep buildStep) async { final annotatedElements = await getAnnotatedElements(buildStep); return annotatedElements.where((e) => e.element is ClassElement).map((e) { diff --git a/packages/brick_sqlite_generators/lib/src/builders/sqlite_schema_builder.dart b/packages/brick_sqlite_generators/lib/src/builders/sqlite_schema_builder.dart index a53f8fa7..707237f9 100644 --- a/packages/brick_sqlite_generators/lib/src/builders/sqlite_schema_builder.dart +++ b/packages/brick_sqlite_generators/lib/src/builders/sqlite_schema_builder.dart @@ -1,3 +1,4 @@ +import 'package:brick_sqlite/db.dart'; import 'package:brick_sqlite_generators/src/builders/sqlite_base_builder.dart'; import 'package:build/build.dart'; import 'package:source_gen/source_gen.dart'; @@ -7,12 +8,12 @@ class SchemaBuilder<_ClassAnnotation> extends SqliteBaseBuilder<_ClassAnnotation @override final outputExtension = '.schema_builder.dart'; + /// Write a [Schema] from existing migrations. Outputs to brick/db/schema.g.dart SchemaBuilder(); @override Future build(BuildStep buildStep) async { - final stopwatch = Stopwatch(); - stopwatch.start(); + final stopwatch = Stopwatch()..start(); final libraryReader = LibraryReader(await buildStep.inputLibrary); final fieldses = await sqliteFieldsFromBuildStep(buildStep); diff --git a/packages/brick_sqlite_generators/lib/src/sqlite_deserialize.dart b/packages/brick_sqlite_generators/lib/src/sqlite_deserialize.dart index 4d310a50..59f4104d 100644 --- a/packages/brick_sqlite_generators/lib/src/sqlite_deserialize.dart +++ b/packages/brick_sqlite_generators/lib/src/sqlite_deserialize.dart @@ -1,12 +1,14 @@ import 'package:analyzer/dart/element/element.dart'; import 'package:brick_build/generators.dart' show SerdesGenerator, SharedChecker; +import 'package:brick_core/src/model.dart'; import 'package:brick_sqlite/brick_sqlite.dart'; -import 'package:brick_sqlite/db.dart' show InsertTable, InsertForeignKey; +import 'package:brick_sqlite/db.dart' show InsertForeignKey, InsertTable; import 'package:brick_sqlite_generators/src/sqlite_serdes_generator.dart'; import 'package:source_gen/source_gen.dart' show InvalidGenerationSourceError; /// Generate a function to produce a [ClassElement] from SQLite data class SqliteDeserialize<_Model extends SqliteModel> extends SqliteSerdesGenerator<_Model> { + /// Generate a function to produce a [ClassElement] from SQLite data SqliteDeserialize( super.element, super.fields, { @@ -21,7 +23,11 @@ class SqliteDeserialize<_Model extends SqliteModel> extends SqliteSerdesGenerato "..${InsertTable.PRIMARY_KEY_FIELD} = data['${InsertTable.PRIMARY_KEY_COLUMN}'] as int;"; @override - String deserializerNullableClause({required field, required fieldAnnotation, required name}) { + String deserializerNullableClause({ + required FieldElement field, + required Sqlite fieldAnnotation, + required String name, + }) { final checker = checkerForType(field.type); if (checker.isIterable && checker.isArgTypeASibling) { return ''; @@ -35,7 +41,12 @@ class SqliteDeserialize<_Model extends SqliteModel> extends SqliteSerdesGenerato } @override - String? coderForField(field, checker, {required wrappedInFuture, required fieldAnnotation}) { + String? coderForField( + FieldElement field, + SharedChecker checker, { + required bool wrappedInFuture, + required Sqlite fieldAnnotation, + }) { final fieldValue = serdesValueForField(field, fieldAnnotation.name!, checker: checker); final defaultValue = SerdesGenerator.defaultValueSuffix(fieldAnnotation); if (field.name == InsertTable.PRIMARY_KEY_FIELD) { @@ -81,7 +92,7 @@ class SqliteDeserialize<_Model extends SqliteModel> extends SqliteSerdesGenerato if (checker.isArgTypeASibling) { final awaited = wrappedInFuture ? 'async => await' : '=>'; - final query = ''' + const query = ''' Query.where('${InsertTable.PRIMARY_KEY_FIELD}', ${InsertTable.PRIMARY_KEY_FIELD}, limit1: true), '''; final argTypeAsString = SharedChecker.withoutNullability(argType); @@ -137,7 +148,7 @@ class SqliteDeserialize<_Model extends SqliteModel> extends SqliteSerdesGenerato final discoveredByIndex = 'jsonDecode($fieldValue).map((d) => d as int > -1 ? ${SharedChecker.withoutNullability(argType)}.values[d] : null)'; final nullableSuffix = checker.isNullable ? '?' : ''; - return '$discoveredByIndex$nullableSuffix.whereType<${argType.getDisplayString(withNullability: true)}>()$castIterable'; + return '$discoveredByIndex$nullableSuffix.whereType<${argType.getDisplayString()}>()$castIterable'; } // Iterable @@ -147,12 +158,12 @@ class SqliteDeserialize<_Model extends SqliteModel> extends SqliteSerdesGenerato // Iterable if (argTypeChecker.fromJsonConstructor != null) { - final klass = argTypeChecker.targetType.element as ClassElement; + final klass = argTypeChecker.targetType.element! as ClassElement; final parameterType = argTypeChecker.fromJsonConstructor!.parameters.first.type; final nullableSuffix = checker.isNullable ? " ?? '[]'" : ''; return '''jsonDecode($fieldValue$nullableSuffix).map( - (d) => ${klass.displayName}.fromJson(d as ${parameterType.getDisplayString(withNullability: true)}) + (d) => ${klass.displayName}.fromJson(d as ${parameterType.getDisplayString()}) )$castIterable$defaultValue'''; } @@ -208,9 +219,9 @@ class SqliteDeserialize<_Model extends SqliteModel> extends SqliteSerdesGenerato } else if (checker.isMap) { return 'jsonDecode($fieldValue)'; } else if (checker.fromJsonConstructor != null) { - final klass = checker.targetType.element as ClassElement; + final klass = checker.targetType.element! as ClassElement; final parameterType = checker.fromJsonConstructor!.parameters.first.type; - return '${klass.displayName}.fromJson(jsonDecode($fieldValue as String) as ${parameterType.getDisplayString(withNullability: true)})'; + return '${klass.displayName}.fromJson(jsonDecode($fieldValue as String) as ${parameterType.getDisplayString()})'; } return null; diff --git a/packages/brick_sqlite_generators/lib/src/sqlite_fields.dart b/packages/brick_sqlite_generators/lib/src/sqlite_fields.dart index eb4b8a86..39be4390 100644 --- a/packages/brick_sqlite_generators/lib/src/sqlite_fields.dart +++ b/packages/brick_sqlite_generators/lib/src/sqlite_fields.dart @@ -2,16 +2,18 @@ import 'package:analyzer/dart/element/element.dart'; import 'package:brick_build/generators.dart'; -import 'package:brick_sqlite/brick_sqlite.dart' show SqliteSerializable, Sqlite, Column; +import 'package:brick_sqlite/brick_sqlite.dart' show Column, Sqlite, SqliteSerializable; /// Find `@Sqlite` given a field class SqliteAnnotationFinder extends AnnotationFinder { + /// final SqliteSerializable? config; + /// Find `@Sqlite` given a field SqliteAnnotationFinder([this.config]); @override - Sqlite from(element) { + Sqlite from(FieldElement element) { final obj = objectForField(element); if (obj == null) { @@ -58,8 +60,11 @@ class SqliteAnnotationFinder extends AnnotationFinder { class SqliteFields extends FieldsForClass { @override final SqliteAnnotationFinder finder; + + /// final SqliteSerializable? config; + /// Converts all fields to [Sqlite]s for later consumption SqliteFields(ClassElement element, [this.config]) : finder = SqliteAnnotationFinder(config), super(element: element); diff --git a/packages/brick_sqlite_generators/lib/src/sqlite_schema/migration_generator.dart b/packages/brick_sqlite_generators/lib/src/sqlite_schema/migration_generator.dart index ba6875c5..7760537e 100644 --- a/packages/brick_sqlite_generators/lib/src/sqlite_schema/migration_generator.dart +++ b/packages/brick_sqlite_generators/lib/src/sqlite_schema/migration_generator.dart @@ -27,8 +27,11 @@ class _MigrationImpl extends Migration { /// Recreate existing migrations as manageable objects. /// Eventually used in [SchemaDifference] to generate new [Migration]s class MigrationGenerator extends Generator { + /// static final emptySchema = Schema(0, tables: {}); + /// Recreate existing migrations as manageable objects. + /// Eventually used in [SchemaDifference] to generate new [Migration]s const MigrationGenerator(); /// Search [library] for all classes that extend [Migration]. Recreate these in Dart Code @@ -129,7 +132,7 @@ class MigrationGenerator extends Generator { /// Creates a new migration from the delta between the existing migration and a new schema @override - String? generate(library, BuildStep? buildStep, {Schema? newSchema, int? version}) { + String? generate(LibraryReader library, BuildStep? buildStep, {Schema? newSchema, int? version}) { final allMigrations = expandAllMigrations(library); final oldSchema = Schema.fromMigrations(allMigrations.toSet()); diff --git a/packages/brick_sqlite_generators/lib/src/sqlite_schema/sqlite_schema_generator.dart b/packages/brick_sqlite_generators/lib/src/sqlite_schema/sqlite_schema_generator.dart index 15adc264..04346b3d 100644 --- a/packages/brick_sqlite_generators/lib/src/sqlite_schema/sqlite_schema_generator.dart +++ b/packages/brick_sqlite_generators/lib/src/sqlite_schema/sqlite_schema_generator.dart @@ -11,15 +11,18 @@ import 'package:source_gen/source_gen.dart' show LibraryReader; import 'package:source_gen/source_gen.dart'; final _formatter = dart_style.DartFormatter(); + +/// const migrationGenerator = MigrationGenerator(); -/// Produces a [Schema] from all @[OfflineFirst] annotations +/// Produces a [Schema] from all SQLite-enabled annotations class SqliteSchemaGenerator { + /// Produces a [Schema] from all SQLite-enabled annotations const SqliteSchemaGenerator(); /// Complete schema file output /// - /// [classes] are all classes by their table name with the @[OfflineFirst] annotation + /// [fieldses] are all classes by their table name String generate(LibraryReader library, List fieldses) { final newSchema = _createNewSchema(library, fieldses); final existingMigrations = migrationGenerator.expandAllMigrations(library); @@ -113,7 +116,6 @@ class SqliteSchemaGenerator { foreignTableName: localTableName, nullable: foreignTableColumnDefinition.nullable, onDeleteCascade: true, - onDeleteSetDefault: false, ), SchemaColumn( InsertForeignKey.joinsTableForeignColumnName(foreignTableName), @@ -122,7 +124,6 @@ class SqliteSchemaGenerator { foreignTableName: foreignTableName, nullable: foreignTableColumnDefinition.nullable, onDeleteCascade: true, - onDeleteSetDefault: false, ), }, indices: { @@ -142,17 +143,17 @@ class SqliteSchemaGenerator { } SchemaTable _createTable(String tableName, SqliteFields fields) { - final columns = _createColumns(fields).where((c) => c != null).toList(); - columns.insert( - 0, - SchemaColumn( - InsertTable.PRIMARY_KEY_COLUMN, - Column.integer, - autoincrement: true, - isPrimaryKey: true, - nullable: false, - ), - ); + final columns = _createColumns(fields).where((c) => c != null).toList() + ..insert( + 0, + SchemaColumn( + InsertTable.PRIMARY_KEY_COLUMN, + Column.integer, + autoincrement: true, + isPrimaryKey: true, + nullable: false, + ), + ); final indices = _createIndices(tableName, fields); @@ -163,6 +164,7 @@ class SqliteSchemaGenerator { ); } + /// @visibleForOverriding SharedChecker checkerForField(FieldElement field) { var checker = checkerForType(field.type); @@ -205,9 +207,11 @@ class SqliteSchemaGenerator { }); } + /// @visibleForOverriding SharedChecker checkerForType(DartType type) => SharedChecker(type); + /// @mustCallSuper SchemaColumn? schemaColumn(Sqlite column, {required SharedChecker checker}) { if (column.columnType != null) { @@ -222,7 +226,7 @@ class SqliteSchemaGenerator { if (checker.isDartCoreType) { return SchemaColumn( column.name!, - Migration.fromDartPrimitive(checker.asPrimitive), + Column.fromDartPrimitive(checker.asPrimitive), nullable: column.nullable, unique: column.unique, ); @@ -266,6 +270,7 @@ class SqliteSchemaGenerator { return null; } + /// @visibleForOverriding SchemaIndex? schemaIndex(Sqlite column, {required SharedChecker checker}) { final isIterableAssociation = checker.isIterable && checker.isArgTypeASibling; diff --git a/packages/brick_sqlite_generators/lib/src/sqlite_serdes_generator.dart b/packages/brick_sqlite_generators/lib/src/sqlite_serdes_generator.dart index f7ee7f04..bf3ad7fd 100644 --- a/packages/brick_sqlite_generators/lib/src/sqlite_serdes_generator.dart +++ b/packages/brick_sqlite_generators/lib/src/sqlite_serdes_generator.dart @@ -1,10 +1,12 @@ import 'package:analyzer/dart/element/element.dart'; import 'package:brick_build/generators.dart'; +import 'package:brick_core/src/model.dart'; import 'package:brick_sqlite/brick_sqlite.dart'; import 'package:brick_sqlite/db.dart' show InsertForeignKey; import 'package:brick_sqlite_generators/src/sqlite_fields.dart'; import 'package:source_gen/source_gen.dart'; +/// abstract class SqliteSerdesGenerator<_Model extends SqliteModel> extends SerdesGenerator { @override @@ -13,6 +15,7 @@ abstract class SqliteSerdesGenerator<_Model extends SqliteModel> @override final String repositoryName; + /// SqliteSerdesGenerator( super.element, SqliteFields super.fields, { @@ -30,7 +33,7 @@ abstract class SqliteSerdesGenerator<_Model extends SqliteModel> SharedChecker checkerForField(FieldElement field) => checkerForType(field.type); @override - bool ignoreCoderForField(field, annotation, checker) { + bool ignoreCoderForField(FieldElement field, Sqlite annotation, SharedChecker checker) { if (annotation.columnType != null) { if (checker.isSerializable) return false; @@ -57,7 +60,7 @@ abstract class SqliteSerdesGenerator<_Model extends SqliteModel> /// Generate foreign key column if the type is a sibling; /// otherwise, return the field's annotated name; @override - String providerNameForField(annotatedName, {required checker}) { + String providerNameForField(String? annotatedName, {required SharedChecker checker}) { if (checker.isSibling) { return InsertForeignKey.foreignKeyColumnName( SharedChecker.withoutNullability(checker.unFuturedType), diff --git a/packages/brick_sqlite_generators/lib/src/sqlite_serialize.dart b/packages/brick_sqlite_generators/lib/src/sqlite_serialize.dart index 26965cb7..a15fcd7e 100644 --- a/packages/brick_sqlite_generators/lib/src/sqlite_serialize.dart +++ b/packages/brick_sqlite_generators/lib/src/sqlite_serialize.dart @@ -1,14 +1,16 @@ import 'package:analyzer/dart/element/element.dart'; import 'package:analyzer/dart/element/nullability_suffix.dart'; import 'package:brick_build/generators.dart'; +import 'package:brick_core/src/model.dart'; import 'package:brick_sqlite/brick_sqlite.dart'; -import 'package:brick_sqlite/db.dart' show InsertTable, InsertForeignKey; +import 'package:brick_sqlite/db.dart' show InsertForeignKey, InsertTable; import 'package:brick_sqlite_generators/src/sqlite_serdes_generator.dart'; import 'package:meta/meta.dart'; import 'package:source_gen/source_gen.dart' show InvalidGenerationSourceError; /// Generate a function to produce a [ClassElement] to SQLite data class SqliteSerialize<_Model extends SqliteModel> extends SqliteSerdesGenerator<_Model> { + /// Generate a function to produce a [ClassElement] to SQLite data SqliteSerialize( super.element, super.fields, { @@ -18,6 +20,7 @@ class SqliteSerialize<_Model extends SqliteModel> extends SqliteSerdesGenerator< @override final doesDeserialize = false; + /// String get tableName => element.name; @override @@ -69,7 +72,12 @@ class SqliteSerialize<_Model extends SqliteModel> extends SqliteSerdesGenerator< } @override - String? coderForField(field, checker, {required wrappedInFuture, required fieldAnnotation}) { + String? coderForField( + FieldElement field, + SharedChecker checker, { + required bool wrappedInFuture, + required Sqlite fieldAnnotation, + }) { final name = providerNameForField(fieldAnnotation.name, checker: checker); final fieldValue = serdesValueForField(field, fieldAnnotation.name!, checker: checker); if (name == InsertTable.PRIMARY_KEY_COLUMN) { @@ -247,7 +255,7 @@ class SqliteSerialize<_Model extends SqliteModel> extends SqliteSerdesGenerator< } @override - bool ignoreCoderForField(field, annotation, checker) { + bool ignoreCoderForField(FieldElement field, Sqlite annotation, SharedChecker checker) { if (annotation.columnType != null) return false; return super.ignoreCoderForField(field, annotation, checker); } diff --git a/packages/brick_sqlite_generators/pubspec.yaml b/packages/brick_sqlite_generators/pubspec.yaml index cd047fd7..7a932b61 100644 --- a/packages/brick_sqlite_generators/pubspec.yaml +++ b/packages/brick_sqlite_generators/pubspec.yaml @@ -4,7 +4,7 @@ homepage: https://github.com/GetDutchie/brick/tree/main/packages/brick_sqlite_ge issue_tracker: https://github.com/GetDutchie/brick/issues repository: https://github.com/GetDutchie/brick -version: 3.2.2 +version: 3.3.0 environment: sdk: ">=2.18.0 <4.0.0" @@ -12,6 +12,7 @@ environment: dependencies: analyzer: ">=6.0.0 <7.0.0" brick_build: ">=3.2.0 <4.0.0" + brick_core: ">=1.3.0 <2.0.0" brick_sqlite: ">=3.0.0 <4.0.0" build: ">=2.0.0 <3.0.0" dart_style: ">=2.0.0 <3.0.0" @@ -19,7 +20,7 @@ dependencies: source_gen: ">=1.2.2 <2.0.0" dev_dependencies: - test: ^1.20.1 - lints: ^2.0.1 brick_build_test: path: ../brick_build_test + lints: + test: diff --git a/packages/brick_sqlite_generators/test/migration_generator/test_from_identical_schema.dart b/packages/brick_sqlite_generators/test/migration_generator/test_from_identical_schema.dart index 0fa6c2fb..b020434e 100644 --- a/packages/brick_sqlite_generators/test/migration_generator/test_from_identical_schema.dart +++ b/packages/brick_sqlite_generators/test/migration_generator/test_from_identical_schema.dart @@ -16,7 +16,6 @@ class Migration1 extends Migration { final schema = Schema( 2, - generatorVersion: 1, tables: { SchemaTable( 'User', diff --git a/packages/brick_sqlite_generators/test/migration_generator/test_from_new_schema.dart b/packages/brick_sqlite_generators/test/migration_generator/test_from_new_schema.dart index 2bf416fd..f82c8e22 100644 --- a/packages/brick_sqlite_generators/test/migration_generator/test_from_new_schema.dart +++ b/packages/brick_sqlite_generators/test/migration_generator/test_from_new_schema.dart @@ -18,7 +18,6 @@ class Migration1 extends Migration { final schema = Schema( 2, - generatorVersion: 1, tables: { SchemaTable( 'User', @@ -37,7 +36,7 @@ final schema = Schema( }, ); -final output = ''' +const output = ''' // GENERATED CODE EDIT WITH CAUTION // THIS FILE **WILL NOT** BE REGENERATED // This file should be version controlled and can be manually edited. diff --git a/packages/brick_sqlite_generators/test/sqlite_model_serdes_generator/test_after_save_with_association.dart b/packages/brick_sqlite_generators/test/sqlite_model_serdes_generator/test_after_save_with_association.dart index 3723f36b..73865511 100644 --- a/packages/brick_sqlite_generators/test/sqlite_model_serdes_generator/test_after_save_with_association.dart +++ b/packages/brick_sqlite_generators/test/sqlite_model_serdes_generator/test_after_save_with_association.dart @@ -1,6 +1,6 @@ import 'package:brick_sqlite/brick_sqlite.dart'; -final output = r""" +const output = r""" // GENERATED CODE DO NOT EDIT part of '../brick.g.dart'; diff --git a/packages/brick_sqlite_generators/test/sqlite_model_serdes_generator/test_all_field_types.dart b/packages/brick_sqlite_generators/test/sqlite_model_serdes_generator/test_all_field_types.dart index e705ce4f..a8924c98 100644 --- a/packages/brick_sqlite_generators/test/sqlite_model_serdes_generator/test_all_field_types.dart +++ b/packages/brick_sqlite_generators/test/sqlite_model_serdes_generator/test_all_field_types.dart @@ -2,7 +2,7 @@ import 'package:brick_sqlite/brick_sqlite.dart'; enum Casing { snake, camel } -final output = r''' +const output = r''' // GENERATED CODE DO NOT EDIT part of '../brick.g.dart'; diff --git a/packages/brick_sqlite_generators/test/sqlite_model_serdes_generator/test_boolean_fields.dart b/packages/brick_sqlite_generators/test/sqlite_model_serdes_generator/test_boolean_fields.dart index 23178ee3..808f4c52 100644 --- a/packages/brick_sqlite_generators/test/sqlite_model_serdes_generator/test_boolean_fields.dart +++ b/packages/brick_sqlite_generators/test/sqlite_model_serdes_generator/test_boolean_fields.dart @@ -1,6 +1,6 @@ import 'package:brick_sqlite/brick_sqlite.dart'; -final output = r""" +const output = r""" // GENERATED CODE DO NOT EDIT part of '../brick.g.dart'; diff --git a/packages/brick_sqlite_generators/test/sqlite_model_serdes_generator/test_field_with_type_argument.dart b/packages/brick_sqlite_generators/test/sqlite_model_serdes_generator/test_field_with_type_argument.dart index 1cce7aca..d91faccd 100644 --- a/packages/brick_sqlite_generators/test/sqlite_model_serdes_generator/test_field_with_type_argument.dart +++ b/packages/brick_sqlite_generators/test/sqlite_model_serdes_generator/test_field_with_type_argument.dart @@ -1,6 +1,6 @@ import 'package:brick_sqlite/brick_sqlite.dart'; -final output = r""" +const output = r""" // GENERATED CODE DO NOT EDIT part of '../brick.g.dart'; diff --git a/packages/brick_sqlite_generators/test/sqlite_model_serdes_generator/test_sqlite_column_type.dart b/packages/brick_sqlite_generators/test/sqlite_model_serdes_generator/test_sqlite_column_type.dart index d6eae813..3c766d82 100644 --- a/packages/brick_sqlite_generators/test/sqlite_model_serdes_generator/test_sqlite_column_type.dart +++ b/packages/brick_sqlite_generators/test/sqlite_model_serdes_generator/test_sqlite_column_type.dart @@ -1,6 +1,6 @@ import 'package:brick_sqlite/brick_sqlite.dart'; -final output = r""" +const output = r""" // GENERATED CODE DO NOT EDIT part of '../brick.g.dart'; diff --git a/packages/brick_sqlite_generators/test/sqlite_model_serdes_generator/test_sqlite_enum_as_string.dart b/packages/brick_sqlite_generators/test/sqlite_model_serdes_generator/test_sqlite_enum_as_string.dart index cae7259a..3ba83217 100644 --- a/packages/brick_sqlite_generators/test/sqlite_model_serdes_generator/test_sqlite_enum_as_string.dart +++ b/packages/brick_sqlite_generators/test/sqlite_model_serdes_generator/test_sqlite_enum_as_string.dart @@ -1,6 +1,6 @@ import 'package:brick_sqlite/brick_sqlite.dart'; -final output = r""" +const output = r""" // GENERATED CODE DO NOT EDIT part of '../brick.g.dart'; diff --git a/packages/brick_sqlite_generators/test/sqlite_model_serdes_generator/test_sqlite_unique.dart b/packages/brick_sqlite_generators/test/sqlite_model_serdes_generator/test_sqlite_unique.dart index 1b1edcd9..bae23341 100644 --- a/packages/brick_sqlite_generators/test/sqlite_model_serdes_generator/test_sqlite_unique.dart +++ b/packages/brick_sqlite_generators/test/sqlite_model_serdes_generator/test_sqlite_unique.dart @@ -1,6 +1,6 @@ import 'package:brick_sqlite/brick_sqlite.dart'; -final output = r""" +const output = r""" // GENERATED CODE DO NOT EDIT part of '../brick.g.dart'; diff --git a/packages/brick_sqlite_generators/test/sqlite_model_serdes_generator/test_to_json_from_json.dart b/packages/brick_sqlite_generators/test/sqlite_model_serdes_generator/test_to_json_from_json.dart index 0007f26d..5f3979f8 100644 --- a/packages/brick_sqlite_generators/test/sqlite_model_serdes_generator/test_to_json_from_json.dart +++ b/packages/brick_sqlite_generators/test/sqlite_model_serdes_generator/test_to_json_from_json.dart @@ -1,6 +1,6 @@ import 'package:brick_sqlite/brick_sqlite.dart'; -final output = r''' +const output = r''' // GENERATED CODE DO NOT EDIT part of '../brick.g.dart'; diff --git a/packages/brick_sqlite_generators/test/sqlite_model_serdes_generator_test.dart b/packages/brick_sqlite_generators/test/sqlite_model_serdes_generator_test.dart index 20b5cd56..2d10728d 100644 --- a/packages/brick_sqlite_generators/test/sqlite_model_serdes_generator_test.dart +++ b/packages/brick_sqlite_generators/test/sqlite_model_serdes_generator_test.dart @@ -18,7 +18,7 @@ import 'sqlite_model_serdes_generator/test_sqlite_unique.dart' as sqlite_unique; import 'sqlite_model_serdes_generator/test_to_json_from_json.dart' as to_json_from_json; final _generator = TestGenerator(); -final folder = 'sqlite_model_serdes_generator'; +const folder = 'sqlite_model_serdes_generator'; final generateReader = generateLibraryForFolder(folder); void main() { @@ -28,7 +28,7 @@ void main() { final reader = await generateReader('id_field'); expect( () async => await _generator.generate(reader, MockBuildStep()), - throwsA(TypeMatcher()), + throwsA(const TypeMatcher()), ); }); @@ -36,7 +36,7 @@ void main() { final reader = await generateReader('primary_key_field'); expect( () async => await _generator.generate(reader, MockBuildStep()), - throwsA(TypeMatcher()), + throwsA(const TypeMatcher()), ); }); @@ -44,7 +44,7 @@ void main() { final reader = await generateReader('column_type_without_generator'); expect( () async => await _generator.generate(reader, MockBuildStep()), - throwsA(TypeMatcher()), + throwsA(const TypeMatcher()), ); }); }); diff --git a/packages/brick_sqlite_generators/test/sqlite_schema/test_all_field_types.dart b/packages/brick_sqlite_generators/test/sqlite_schema/test_all_field_types.dart index c2a09144..65b11957 100644 --- a/packages/brick_sqlite_generators/test/sqlite_schema/test_all_field_types.dart +++ b/packages/brick_sqlite_generators/test/sqlite_schema/test_all_field_types.dart @@ -2,7 +2,7 @@ import 'package:brick_sqlite/brick_sqlite.dart'; enum Casing { snake, camel } -final output = ''' +const output = ''' // GENERATED CODE DO NOT EDIT // This file should be version controlled import 'package:brick_sqlite/db.dart'; diff --git a/packages/brick_sqlite_generators/test/sqlite_schema/test_from_to_json.dart b/packages/brick_sqlite_generators/test/sqlite_schema/test_from_to_json.dart index 5b27f7fa..9ffd79de 100644 --- a/packages/brick_sqlite_generators/test/sqlite_schema/test_from_to_json.dart +++ b/packages/brick_sqlite_generators/test/sqlite_schema/test_from_to_json.dart @@ -1,6 +1,6 @@ import 'package:brick_sqlite/brick_sqlite.dart'; -final output = ''' +const output = ''' // GENERATED CODE DO NOT EDIT // This file should be version controlled import 'package:brick_sqlite/db.dart'; diff --git a/packages/brick_sqlite_generators/test/sqlite_schema/test_index_annotation.dart b/packages/brick_sqlite_generators/test/sqlite_schema/test_index_annotation.dart index 03a9217c..8b44d82a 100644 --- a/packages/brick_sqlite_generators/test/sqlite_schema/test_index_annotation.dart +++ b/packages/brick_sqlite_generators/test/sqlite_schema/test_index_annotation.dart @@ -1,6 +1,6 @@ import 'package:brick_sqlite/brick_sqlite.dart'; -final output = ''' +const output = ''' // GENERATED CODE DO NOT EDIT // This file should be version controlled import 'package:brick_sqlite/db.dart'; diff --git a/packages/brick_sqlite_generators/test/sqlite_schema/test_nullable.dart b/packages/brick_sqlite_generators/test/sqlite_schema/test_nullable.dart index e508e1e3..4257b988 100644 --- a/packages/brick_sqlite_generators/test/sqlite_schema/test_nullable.dart +++ b/packages/brick_sqlite_generators/test/sqlite_schema/test_nullable.dart @@ -1,6 +1,6 @@ import 'package:brick_sqlite/brick_sqlite.dart'; -final output = ''' +const output = ''' // GENERATED CODE DO NOT EDIT // This file should be version controlled import 'package:brick_sqlite/db.dart'; diff --git a/packages/brick_sqlite_generators/test/sqlite_schema/test_one_to_many_association.dart b/packages/brick_sqlite_generators/test/sqlite_schema/test_one_to_many_association.dart index e52230d4..c507f27b 100644 --- a/packages/brick_sqlite_generators/test/sqlite_schema/test_one_to_many_association.dart +++ b/packages/brick_sqlite_generators/test/sqlite_schema/test_one_to_many_association.dart @@ -10,7 +10,7 @@ class SqliteAssoc extends SqliteModel { final int key = -1; } -final output = ''' +const output = ''' // GENERATED CODE DO NOT EDIT // This file should be version controlled import 'package:brick_sqlite/db.dart'; diff --git a/packages/brick_sqlite_generators/test/sqlite_schema/test_one_to_one_association.dart b/packages/brick_sqlite_generators/test/sqlite_schema/test_one_to_one_association.dart index cc115064..3bd1807f 100644 --- a/packages/brick_sqlite_generators/test/sqlite_schema/test_one_to_one_association.dart +++ b/packages/brick_sqlite_generators/test/sqlite_schema/test_one_to_one_association.dart @@ -10,7 +10,7 @@ class SqliteAssoc extends SqliteModel { final int key = -1; } -final output = ''' +const output = ''' // GENERATED CODE DO NOT EDIT // This file should be version controlled import 'package:brick_sqlite/db.dart'; diff --git a/packages/brick_sqlite_generators/test/sqlite_schema/test_simple.dart b/packages/brick_sqlite_generators/test/sqlite_schema/test_simple.dart index 08365d0a..d5b36267 100644 --- a/packages/brick_sqlite_generators/test/sqlite_schema/test_simple.dart +++ b/packages/brick_sqlite_generators/test/sqlite_schema/test_simple.dart @@ -1,6 +1,6 @@ import 'package:brick_sqlite/brick_sqlite.dart'; -final migrationOutput = ''' +const migrationOutput = ''' // GENERATED CODE EDIT WITH CAUTION // THIS FILE **WILL NOT** BE REGENERATED // This file should be version controlled and can be manually edited. @@ -41,7 +41,7 @@ class Migration1 extends Migration { } '''; -final output = ''' +const output = ''' // GENERATED CODE DO NOT EDIT // This file should be version controlled import 'package:brick_sqlite/db.dart'; diff --git a/packages/brick_sqlite_generators/test/sqlite_schema/test_sqlite_column_type.dart b/packages/brick_sqlite_generators/test/sqlite_schema/test_sqlite_column_type.dart index 01437d2d..a749012e 100644 --- a/packages/brick_sqlite_generators/test/sqlite_schema/test_sqlite_column_type.dart +++ b/packages/brick_sqlite_generators/test/sqlite_schema/test_sqlite_column_type.dart @@ -2,7 +2,7 @@ import 'dart:typed_data'; import 'package:brick_sqlite/brick_sqlite.dart'; -final output = ''' +const output = ''' // GENERATED CODE DO NOT EDIT // This file should be version controlled import 'package:brick_sqlite/db.dart'; diff --git a/packages/brick_sqlite_generators/test/sqlite_schema_generator_test.dart b/packages/brick_sqlite_generators/test/sqlite_schema_generator_test.dart index 91a7972b..e3596599 100644 --- a/packages/brick_sqlite_generators/test/sqlite_schema_generator_test.dart +++ b/packages/brick_sqlite_generators/test/sqlite_schema_generator_test.dart @@ -73,7 +73,7 @@ void main() { }); } -final annotationChecker = TypeChecker.fromRuntime(SqliteSerializable); +const annotationChecker = TypeChecker.fromRuntime(SqliteSerializable); Future>> generateSchemaMap(String filename) async { final reader = await generateReader(filename); diff --git a/packages/brick_supabase/CHANGELOG.md b/packages/brick_supabase/CHANGELOG.md index f0426783..24a1b50a 100644 --- a/packages/brick_supabase/CHANGELOG.md +++ b/packages/brick_supabase/CHANGELOG.md @@ -2,6 +2,16 @@ ## 1.2.0 +- **DEPRECATION** `Query(providerArgs: {'limitReferencedTable':})` has been removed in favor of `Query(limitBy:)` +- **DEPRECATION** `Query(providerArgs: {'orderByReferencedTable':})` has been removed in favor of `Query(orderBy:)` +- Association, plural ordering is supported. For example, `Query(orderBy: [OrderBy.desc('assoc', associationField: 'name')])` on `DemoModel` would produce the PostgREST filter: + ```javascript + orderBy('name', referencedTable: 'association_table') + ``` +- New `SupabaseProviderQuery` adds Supabase-specific support for the new `Query`. +- Advanced, plural limiting is supported. For example, `Query(limitBy: [LimitBy(1, evaluatedField: 'assoc'))` is the equivalent of `.limit(1, referencedTable: 'demo_model')`. `Query#limit` can be used in conjunction on the parent model request. +- Upgrade `brick_core` to `1.3.0` +- Update analysis to modern lints - Add `SupabaseProvider#update` and `SupabaseProvider#insert` to conform to Supabase policy restrictions - Use `columnName` instead of `evaluatedField` in `QuerySupabaseTransformer` when searching for non null associations diff --git a/packages/brick_supabase/README.md b/packages/brick_supabase/README.md index d270f3f4..8a73a691 100644 --- a/packages/brick_supabase/README.md +++ b/packages/brick_supabase/README.md @@ -6,18 +6,6 @@ Connecting [Brick](https://github.com/GetDutchie/brick) with Supabase. ## Supported `Query` Configuration -### `providerArgs:` - -| Name | Type | Description | -| -------------------------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | -| `'limit'` | `int` | Forwards to Supabase's `limit` [param](https://supabase.com/docs/reference/dart/limit) in Brick's `#get` action | -| `'limitByReferencedTable'` | `String?` | Forwards to Supabase's `referencedTable` [property](https://supabase.com/docs/reference/dart/limit) | -| `'offset'` | `int` | Start from a specific offset, inclusive. | -| `'orderBy'` | `String` | Use field names not column names and always specify direction.For example, given a `final DateTime createdAt;` field: `{'orderBy': 'createdAt ASC'}`. | -| `'orderByReferencedTable'` | `String?` | Forwards to Supabase's `referencedTable` [property](https://supabase.com/docs/reference/dart/order) | - -:bulb: The `ReferencedTable` naming convention is awkward but necessary to not collide with other providers (like `SqliteProvider`) that also use `orderBy` and `limit`. While a `foreign_table.foreign_column` syntax is more Supabase-like, it is not supported in `orderBy` and `limit`. - ### `where:` Brick currently does not support all of Supabase's filtering methods. Consider the associated `Compare` enum value to Supabase's method when building a Brick query: diff --git a/packages/brick_supabase/lib/src/annotations/supabase.dart b/packages/brick_supabase/lib/src/annotations/supabase.dart index 413ad9af..8dfda7fe 100644 --- a/packages/brick_supabase/lib/src/annotations/supabase.dart +++ b/packages/brick_supabase/lib/src/annotations/supabase.dart @@ -1,4 +1,5 @@ import 'package:brick_core/field_serializable.dart'; +import 'package:brick_supabase/src/supabase_adapter.dart'; /// An annotation used to specify how a field is serialized for a [SupabaseAdapter]. /// diff --git a/packages/brick_supabase/lib/src/query_supabase_transformer.dart b/packages/brick_supabase/lib/src/query_supabase_transformer.dart index 7bc0e7e4..e8a48e24 100644 --- a/packages/brick_supabase/lib/src/query_supabase_transformer.dart +++ b/packages/brick_supabase/lib/src/query_supabase_transformer.dart @@ -8,58 +8,58 @@ import 'package:supabase/supabase.dart'; /// Create a prepared Supabase URI for eventual execution class QuerySupabaseTransformer<_Model extends SupabaseModel> { + /// final SupabaseAdapter adapter; + /// final SupabaseModelDictionary modelDictionary; - /// Must-haves for the [statement], mainly used to build clauses + /// final Query? query; - /// [selectStatement] will output [statement] as a `SELECT FROM`. When false, the [statement] - /// output will be a `SELECT COUNT(*)`. Defaults `true`. + /// Create a prepared Supabase URI for eventual execution QuerySupabaseTransformer({ required this.modelDictionary, this.query, SupabaseAdapter? adapter, }) : adapter = adapter ?? modelDictionary.adapterFor[_Model]!; - String get selectFields { - return destructureAssociationProperties(adapter.fieldsToSupabaseColumns, _Model).join(','); - } + /// All fields to be selected by the request, including associations and + /// associations' fields + String get selectFields => + destructureAssociationProperties(adapter.fieldsToSupabaseColumns, _Model).join(','); - PostgrestTransformBuilder>> applyProviderArgs( + /// Translate all valid query properties to a composed PostgREST filter + PostgrestTransformBuilder>> applyQuery( PostgrestFilterBuilder>> builder, ) { - if (query?.providerArgs['orderBy'] != null) { - builder = order(builder); - } + var computedBuilder = order(builder); - if (query?.providerArgs['offset'] != null) { - final url = - builder.overrideSearchParams('offset', (query!.providerArgs['offset'] as int).toString()); - builder = builder.copyWithUrl(url); + final offset = query?.offset ?? query?.providerArgs['offset'] as int?; + if (offset != null) { + final url = builder.overrideSearchParams('offset', (offset).toString()); + computedBuilder = computedBuilder.copyWithUrl(url); } - if (query?.providerArgs['limit'] != null) { - return limit(builder); - } - - return builder; + return limit(computedBuilder); } - PostgrestFilterBuilder>> select(SupabaseQueryBuilder builder) { - return (query?.where ?? []).fold(builder.select(selectFields), (acc, condition) { - final whereStatement = expandCondition(condition); - for (final where in whereStatement) { - for (final entry in where.entries) { - final newUri = acc.appendSearchParams(entry.key, entry.value); - acc = acc.copyWithUrl(newUri); + /// + PostgrestFilterBuilder>> select(SupabaseQueryBuilder builder) => + (query?.where ?? []).fold(builder.select(selectFields), (acc, condition) { + final whereStatement = expandCondition(condition); + for (final where in whereStatement) { + for (final entry in where.entries) { + final newUri = acc.appendSearchParams(entry.key, entry.value); + acc = acc.copyWithUrl(newUri); + } } - } - return acc; - }); - } + return acc; + }); + /// Convert association requests to a lookup by inferred association + /// (`assoc(*)`) or by a rename (`my_assoc:assoc(*)`) or by a + /// foreign key (`my_assoc:assoc!fk(*)`). @protected @visibleForTesting List destructureAssociationProperties( @@ -194,36 +194,66 @@ class QuerySupabaseTransformer<_Model extends SupabaseModel> { ]; } + /// Produce a `limit` PostgREST filter from [Query.limit] and [Query.limitBy]. PostgrestTransformBuilder>> limit( PostgrestFilterBuilder>> builder, ) { - if (query?.providerArgs['limit'] == null) return builder; + if (query == null) return builder; - final limit = query!.providerArgs['limit'] as int; - final referencedTable = query!.providerArgs['limitReferencedTable'] as String?; + final topLevelLimit = query?.providerArgs['limit'] as int? ?? query?.limit; + final withTopLevelLimit = topLevelLimit != null + ? PostgrestTransformBuilder( + builder.copyWithUrl(builder.appendSearchParams('limit', topLevelLimit.toString())), + ) + : builder; + final referencedTable = query?.providerArgs['limitReferencedTable'] as String?; final key = referencedTable == null ? 'limit' : '$referencedTable.limit'; - - final url = builder.appendSearchParams(key, '$limit'); - return PostgrestTransformBuilder(builder.copyWithUrl(url)); + final withProviderArgs = topLevelLimit != null + ? PostgrestTransformBuilder( + withTopLevelLimit + .copyWithUrl(builder.appendSearchParams(key, topLevelLimit.toString())), + ) + : withTopLevelLimit; + + return query!.limitBy.fold(withProviderArgs, (acc, limitBy) { + final definition = adapter.fieldsToSupabaseColumns[limitBy.evaluatedField]; + final tableName = modelDictionary.adapterFor[definition?.associationType]?.supabaseTableName; + if (tableName == null) return acc; + + final url = acc.appendSearchParams('$tableName.limit', limitBy.amount.toString()); + return PostgrestTransformBuilder(acc.copyWithUrl(url)); + }); } + /// Produce an `orderBy` PostgREST filter from [Query.orderBy]. @protected @visibleForTesting PostgrestFilterBuilder>> order( PostgrestFilterBuilder>> builder, ) { - if (query?.providerArgs['orderBy'] == null) return builder; + if (query?.providerArgs['orderBy'] == null && (query?.orderBy.isEmpty ?? true)) return builder; - final orderBy = query!.providerArgs['orderBy'] as String; - final ascending = orderBy.toLowerCase().endsWith(' asc'); + final orderBy = query!.providerArgs['orderBy'] as String?; + final ascending = orderBy?.toLowerCase().endsWith(' asc') ?? true; final referencedTable = query!.providerArgs['orderByReferencedTable'] as String?; final key = referencedTable == null ? 'order' : '$referencedTable.order'; - final fieldName = orderBy.split(' ')[0]; - final columnName = adapter.fieldsToSupabaseColumns[fieldName]!.columnName; - final value = '$columnName.${ascending ? 'asc' : 'desc'}.nullslast'; - final url = builder.overrideSearchParams(key, value); - return builder.copyWithUrl(url); + final fieldName = orderBy?.split(' ')[0]; + final columnName = adapter.fieldsToSupabaseColumns[fieldName]?.columnName; + final url = + builder.overrideSearchParams(key, '$columnName.${ascending ? 'asc' : 'desc'}.nullslast'); + final withProviderArgs = orderBy == null ? builder : builder.copyWithUrl(url); + + return query!.orderBy.fold(withProviderArgs, (acc, orderBy) { + final definition = adapter.fieldsToSupabaseColumns[orderBy.evaluatedField]; + final tableName = modelDictionary.adapterFor[definition?.associationType]?.supabaseTableName; + + final url = acc.appendSearchParams( + tableName == null ? 'order' : '$tableName.order', + '${orderBy.associationField ?? orderBy.evaluatedField}.${orderBy.ascending ? 'asc' : 'desc'}.nullslast', + ); + return acc.copyWithUrl(url); + }); } static String _compareToSearchParam(Compare compare) { diff --git a/packages/brick_supabase/lib/src/runtime_supabase_column_definition.dart b/packages/brick_supabase/lib/src/runtime_supabase_column_definition.dart index c743e317..e80b79eb 100644 --- a/packages/brick_supabase/lib/src/runtime_supabase_column_definition.dart +++ b/packages/brick_supabase/lib/src/runtime_supabase_column_definition.dart @@ -26,6 +26,8 @@ class RuntimeSupabaseColumnDefinition { /// Forwarded from `@Supabase(query:)`, this overrides the generated query. final String? query; + /// Used to define types in [SupabaseAdapter#supabaseFieldsToColumns]. The build runner package + /// extracts types and associations that would've been otherwise inaccessible at runtime. const RuntimeSupabaseColumnDefinition({ this.association = false, this.associationIsNullable = false, diff --git a/packages/brick_supabase/lib/src/supabase_adapter.dart b/packages/brick_supabase/lib/src/supabase_adapter.dart index 9ded779a..b396a3ef 100644 --- a/packages/brick_supabase/lib/src/supabase_adapter.dart +++ b/packages/brick_supabase/lib/src/supabase_adapter.dart @@ -1,4 +1,5 @@ import 'package:brick_core/core.dart'; +import 'package:brick_supabase/src/annotations/supabase_serializable.dart'; import 'package:brick_supabase/src/runtime_supabase_column_definition.dart'; import 'package:brick_supabase/src/supabase_model.dart'; import 'package:brick_supabase/src/supabase_provider.dart'; diff --git a/packages/brick_supabase/lib/src/supabase_model.dart b/packages/brick_supabase/lib/src/supabase_model.dart index a086134b..1e16d036 100644 --- a/packages/brick_supabase/lib/src/supabase_model.dart +++ b/packages/brick_supabase/lib/src/supabase_model.dart @@ -1,4 +1,5 @@ import 'package:brick_core/core.dart'; +import 'package:brick_supabase/src/supabase_provider.dart'; /// Models accessible to the [SupabaseProvider] abstract mixin class SupabaseModel implements Model {} diff --git a/packages/brick_supabase/lib/src/supabase_model_dictionary.dart b/packages/brick_supabase/lib/src/supabase_model_dictionary.dart index 1401df8e..b281cd44 100644 --- a/packages/brick_supabase/lib/src/supabase_model_dictionary.dart +++ b/packages/brick_supabase/lib/src/supabase_model_dictionary.dart @@ -5,5 +5,6 @@ import 'package:brick_supabase/src/supabase_model.dart'; /// Associates app models with their [SupabaseAdapter] class SupabaseModelDictionary extends ModelDictionary> { + /// Associates app models with their [SupabaseAdapter] const SupabaseModelDictionary(super.adapterFor); } diff --git a/packages/brick_supabase/lib/src/supabase_provider.dart b/packages/brick_supabase/lib/src/supabase_provider.dart index c7d6315b..be4d8f76 100644 --- a/packages/brick_supabase/lib/src/supabase_provider.dart +++ b/packages/brick_supabase/lib/src/supabase_provider.dart @@ -6,23 +6,35 @@ import 'package:logging/logging.dart'; import 'package:meta/meta.dart'; import 'package:supabase/supabase.dart'; +/// An internal definition for remote requests. +/// In rare cases, a specific `update` or `insert` is preferable to `upsert`; +/// this enum explicitly declares the desired behavior. enum UpsertMethod { + /// Translates to a Supabase `.insert` insert, + + /// Translates to a Supabase `.update` update, + + /// Translates to a Supabase `.upsert` upsert, } -/// Retrieves from an HTTP endpoint +/// Retrieves from a Supabase server class SupabaseProvider implements Provider { + /// The client used to connect to the Supabase server. + /// For some cases, like offline repositories, the offl final SupabaseClient client; /// The glue between app models and generated adapters. @override final SupabaseModelDictionary modelDictionary; + /// @protected final Logger logger; + /// Retrieves from a Supabase server SupabaseProvider( this.client, { required this.modelDictionary, @@ -30,7 +42,11 @@ class SupabaseProvider implements Provider { /// Sends a DELETE request method to the endpoint @override - Future delete(instance, {query, repository}) async { + Future delete( + TModel instance, { + Query? query, + ModelRepository? repository, + }) async { final adapter = modelDictionary.adapterFor[TModel]!; final tableBuilder = client.from(adapter.supabaseTableName); final output = await adapter.toSupabase(instance, provider: this, repository: repository); @@ -48,7 +64,10 @@ class SupabaseProvider implements Provider { } @override - Future exists({query, repository}) async { + Future exists({ + Query? query, + ModelRepository? repository, + }) async { final adapter = modelDictionary.adapterFor[TModel]!; final queryTransformer = QuerySupabaseTransformer(modelDictionary: modelDictionary, query: query); @@ -58,22 +77,17 @@ class SupabaseProvider implements Provider { return resp.count > 0; } - /// [Query]'s `providerArgs` can extend the [get] functionality: - /// * `'limit'` e.g. `{'limit': 10}` - /// * `'limitByReferencedTable'` forwards to Supabase's `referencedTable` property https://supabase.com/docs/reference/dart/limit - /// * `'offset'` Start from a specific offset, inclusive. - /// * `'orderBy'` Use field names not column names and always specify direction. - /// For example, given a `final DateTime createdAt;` field: `{'orderBy': 'createdAt ASC'}`. - /// If the column cannot be found for the first value before a space, the value is left unchanged. - /// * `'orderByReferencedTable'` forwards to Supabase's `referencedTable` property https://supabase.com/docs/reference/dart/order @override - Future> get({query, repository}) async { + Future> get({ + Query? query, + ModelRepository? repository, + }) async { final adapter = modelDictionary.adapterFor[TModel]!; final queryTransformer = QuerySupabaseTransformer(modelDictionary: modelDictionary, query: query); final builder = queryTransformer.select(client.from(adapter.supabaseTableName)); - final resp = await queryTransformer.applyProviderArgs(builder); + final resp = await queryTransformer.applyQuery(builder); return Future.wait( resp @@ -85,7 +99,11 @@ class SupabaseProvider implements Provider { /// In almost all cases, use [upsert]. This method is provided for cases when a table's /// policy permits inserts without updates. - Future insert(instance, {query, repository}) async { + Future insert( + TModel instance, { + Query? query, + ModelRepository? repository, + }) async { final adapter = modelDictionary.adapterFor[TModel]!; final output = await adapter.toSupabase(instance, provider: this, repository: repository); @@ -100,7 +118,11 @@ class SupabaseProvider implements Provider { /// In almost all cases, use [upsert]. This method is provided for cases when a table's /// policy permits updates without inserts. - Future update(instance, {query, repository}) async { + Future update( + TModel instance, { + Query? query, + ModelRepository? repository, + }) async { final adapter = modelDictionary.adapterFor[TModel]!; final output = await adapter.toSupabase(instance, provider: this, repository: repository); @@ -120,7 +142,11 @@ class SupabaseProvider implements Provider { /// For example, given model `Room` has association `Bed` and `Bed` has association `Pillow`, /// when `Room` is upserted, `Pillow` is upserted and then `Bed` is upserted. @override - Future upsert(instance, {query, repository}) async { + Future upsert( + TModel instance, { + Query? query, + ModelRepository? repository, + }) async { final adapter = modelDictionary.adapterFor[TModel]!; final output = await adapter.toSupabase(instance, provider: this, repository: repository); @@ -142,7 +168,7 @@ class SupabaseProvider implements Provider { Query? query, ModelRepository? repository, }) async { - assert(modelDictionary.adapterFor.containsKey(type)); + assert(modelDictionary.adapterFor.containsKey(type), '$type not found in the model dictionary'); final adapter = modelDictionary.adapterFor[type]!; @@ -188,7 +214,7 @@ class SupabaseProvider implements Provider { Query? query, ModelRepository? repository, }) async { - assert(modelDictionary.adapterFor.containsKey(type)); + assert(modelDictionary.adapterFor.containsKey(type), '$type not found in the model dictionary'); final adapter = modelDictionary.adapterFor[type]!; final associations = adapter.fieldsToSupabaseColumns.values diff --git a/packages/brick_supabase/lib/src/testing/supabase_mock_server.dart b/packages/brick_supabase/lib/src/testing/supabase_mock_server.dart index 60e8bce0..6f66d957 100644 --- a/packages/brick_supabase/lib/src/testing/supabase_mock_server.dart +++ b/packages/brick_supabase/lib/src/testing/supabase_mock_server.dart @@ -11,25 +11,36 @@ import 'package:brick_supabase/src/testing/supabase_response.dart'; import 'package:collection/collection.dart'; import 'package:supabase/supabase.dart'; +/// An all-in-one mock for Supabase repsonses in unit tests. class SupabaseMockServer { + /// A mock Supabase API key. Can be a blank string. final String apiKey; + /// A real SupabaseClient that does not connect to Supabase late SupabaseClient client; + /// If a stream listener has been registered bool hasListener = false; + /// The active stream listener StreamSubscription? listener; + /// Active stream listeners final Set listeners = {}; + /// final SupabaseModelDictionary modelDictionary; + /// The created and reassigned [HttpServer] late HttpServer server; + /// The simulated server URL String get serverUrl => 'http://${server.address.host}:${server.port}'; + /// The stubbed websocket that can be listed to for streams WebSocket? webSocket; + /// An all-in-one mock for Supabase repsonses in unit tests. SupabaseMockServer({this.apiKey = 'supabaseKey', required this.modelDictionary}); /// Invoke within a group as `tearDown(mock.tearDown)` @@ -64,6 +75,7 @@ class SupabaseMockServer { } } + /// Handle realtime/stream requests Future handleRealtime( HttpRequest request, Map responses, @@ -92,9 +104,8 @@ class SupabaseMockServer { final realtimeFilter = requestJson['payload']['config']['postgres_changes'].first['filter']; - final matching = responses.entries.firstWhereOrNull((r) { - return realtimeFilter == null || realtimeFilter == r.key.filter; - }); + final matching = responses.entries + .firstWhereOrNull((r) => realtimeFilter == null || realtimeFilter == r.key.filter); if (matching == null) return; @@ -132,6 +143,7 @@ class SupabaseMockServer { }); } + /// Handle regular REST requests HttpResponse handleRest(HttpRequest request, Map responses) { final url = request.uri.toString(); @@ -213,9 +225,12 @@ class SupabaseMockServer { 'payload': { 'ids': [realtimeEvent.index], 'data': { - 'columns': adapter.fieldsToSupabaseColumns.entries.map((entry) { - return {'name': entry.value.columnName, 'type': 'text', 'type_modifier': 4294967295}; - }).toList(), + 'columns': adapter.fieldsToSupabaseColumns.entries + .map( + (entry) => + {'name': entry.value.columnName, 'type': 'text', 'type_modifier': 4294967295}, + ) + .toList(), 'commit_timestamp': '2021-08-01T08:00:30Z', 'errors': null, if (realtimeEvent != PostgresChangeEvent.insert) 'old_record': serialized, diff --git a/packages/brick_supabase/lib/src/testing/supabase_request.dart b/packages/brick_supabase/lib/src/testing/supabase_request.dart index ff40c3ac..036b4f16 100644 --- a/packages/brick_supabase/lib/src/testing/supabase_request.dart +++ b/packages/brick_supabase/lib/src/testing/supabase_request.dart @@ -1,17 +1,24 @@ import 'package:brick_core/query.dart'; +import 'package:brick_supabase/brick_supabase.dart'; import 'package:brick_supabase/src/query_supabase_transformer.dart'; -import 'package:brick_supabase/src/supabase_model.dart'; -import 'package:brick_supabase/src/supabase_model_dictionary.dart'; +import 'package:brick_supabase/src/testing/supabase_mock_server.dart'; +import 'package:brick_supabase/testing.dart'; +/// Construct a request for Supabase data. The primary purpose of this class is to +/// DRY code for URL requests to the [SupabaseMockServer]. For example: +/// `final req = SupabaseRequest();` class SupabaseRequest { /// If `fields` are not provided, they will try to be inferred using the /// [SupabaseMockServer]'s `modelDictionary`. final String? fields; + /// A PostgREST-style filter, such as `id=eq.1` final String? filter; + /// final int? limit; + /// An HTTP request method, e.g. `GET`, `POST`, `PUT`, `DELETE` final String? requestMethod; /// If a `tableName` is not provided, it will try to be inferred using the @@ -19,7 +26,10 @@ class SupabaseRequest { /// `SupabaseAdapter`'s `supabaseTableName`. final String? tableName; - SupabaseRequest({ + /// Construct a request for Supabase data. The primary purpose of this class is to + /// DRY code for URL requests to the [SupabaseMockServer]. For example: + /// `final req = SupabaseRequest();` + const SupabaseRequest({ this.tableName, this.fields, this.filter, @@ -27,6 +37,7 @@ class SupabaseRequest { this.requestMethod = 'GET', }); + /// Convert the request to a PostgREST URL Uri toUri(SupabaseModelDictionary? modelDictionary) { final generatedFields = modelDictionary != null ? SupabaseRequest.fieldsFromDictionary(modelDictionary) diff --git a/packages/brick_supabase/lib/src/testing/supabase_response.dart b/packages/brick_supabase/lib/src/testing/supabase_response.dart index fa3a59b7..c0f8c354 100644 --- a/packages/brick_supabase/lib/src/testing/supabase_response.dart +++ b/packages/brick_supabase/lib/src/testing/supabase_response.dart @@ -1,22 +1,35 @@ +import 'package:brick_supabase/src/testing/supabase_mock_server.dart'; + +/// A simulated response from Supabase. This class is designed to DRY responses +/// when used with the [SupabaseMockServer]. For example: +/// ```dart +/// final resp = SupabaseResponse([ +/// await mock.serialize(MyModel(name: 'Demo 1', id: '1')), +/// await mock.serialize(MyModel(name: 'Demo 2', id: '2')), +/// ]); +/// ``` class SupabaseResponse { + /// The payload that Supabase would have returned. This can be a map or a list of decoded JSON objects final dynamic data; /// All recursively-discovered [realtimeSubsequentReplies] - List get flattenedResponses { - return realtimeSubsequentReplies.fold([this], (acc, r) { - void recurse(SupabaseResponse response) { - acc.add(response); - if (response.realtimeSubsequentReplies.isNotEmpty) { - acc.addAll(response.realtimeSubsequentReplies); - response.realtimeSubsequentReplies.forEach(recurse); + List get flattenedResponses => + realtimeSubsequentReplies.fold([this], (acc, r) { + void recurse(SupabaseResponse response) { + acc.add(response); + if (response.realtimeSubsequentReplies.isNotEmpty) { + acc.addAll(response.realtimeSubsequentReplies); + response.realtimeSubsequentReplies.forEach(recurse); + } } - } - recurse(r); - return acc; - }); - } + recurse(r); + return acc; + }); + /// Additional headers that should be included in the response. + /// Supabase's client sometimes requires header values to properly parse the result + /// such as `{'content-range': '*/1'}` final Map? headers; /// Additional replies sent after this instance's [data]. @@ -29,7 +42,15 @@ class SupabaseResponse { /// Amount of time to delay each [realtimeSubsequentReplies] final Duration realtimeSubsequentReplyDelay; - SupabaseResponse( + /// A simulated response from Supabase. This class is designed to DRY responses + /// when used with the [SupabaseMockServer]. For example: + /// ```dart + /// final resp = SupabaseResponse([ + /// await mock.serialize(MyModel(name: 'Demo 1', id: '1')), + /// await mock.serialize(MyModel(name: 'Demo 2', id: '2')), + /// ]); + /// ``` + const SupabaseResponse( this.data, { this.headers, this.realtimeSubsequentReplies = const [], diff --git a/packages/brick_supabase/pubspec.yaml b/packages/brick_supabase/pubspec.yaml index 0cf40b58..82f06483 100644 --- a/packages/brick_supabase/pubspec.yaml +++ b/packages/brick_supabase/pubspec.yaml @@ -10,12 +10,12 @@ environment: sdk: ">=3.0.0 <4.0.0" dependencies: - brick_core: ">=1.2.1 <2.0.0" + brick_core: ">=1.3.0 <2.0.0" collection: ">=1.15.0 <2.0.0" logging: ">=1.0.0 <2.0.0" meta: ">=1.3.0 <2.0.0" supabase: ">=2.3.0 <3.0.0" dev_dependencies: - lints: ^2.0.1 - test: ^1.16.5 + lints: + test: diff --git a/packages/brick_supabase/test/__mocks__.dart b/packages/brick_supabase/test/__mocks__.dart index b9b32f8f..82a086c6 100644 --- a/packages/brick_supabase/test/__mocks__.dart +++ b/packages/brick_supabase/test/__mocks__.dart @@ -1,3 +1,4 @@ +import 'package:brick_core/src/model_repository.dart'; import 'package:brick_supabase/brick_supabase.dart'; part '__mocks_generated__.dart'; diff --git a/packages/brick_supabase/test/__mocks_generated__.dart b/packages/brick_supabase/test/__mocks_generated__.dart index 0c7ccde3..67e413b0 100644 --- a/packages/brick_supabase/test/__mocks_generated__.dart +++ b/packages/brick_supabase/test/__mocks_generated__.dart @@ -1,28 +1,34 @@ +// ignore_for_file: type_annotate_public_apis + part of '__mocks__.dart'; -Demo _$DemoFromSupabase(Map json) { - return Demo( - id: json['id'] as String, - name: json['name'] as String, - age: json['age'] as int, - ); -} +Demo _$DemoFromSupabase(Map json) => Demo( + id: json['id'] as String, + name: json['name'] as String, + age: json['age'] as int, + ); -Future> _$DemoToSupabase(Demo instance) async { - return { - 'id': instance.id, - 'name': instance.name, - 'age': instance.age, - }; -} +Future> _$DemoToSupabase(Demo instance) async => { + 'id': instance.id, + 'name': instance.name, + 'age': instance.age, + }; class DemoAdapter extends SupabaseAdapter { @override - Future fromSupabase(data, {required provider, repository}) async => + Future fromSupabase( + Map data, { + required SupabaseProvider provider, + ModelRepository? repository, + }) async => _$DemoFromSupabase(data); @override - Future> toSupabase(instance, {required provider, repository}) async => + Future> toSupabase( + Demo instance, { + required SupabaseProvider provider, + ModelRepository? repository, + }) async => await _$DemoToSupabase(instance); @override @@ -31,15 +37,12 @@ class DemoAdapter extends SupabaseAdapter { @override final fieldsToSupabaseColumns = { 'id': const RuntimeSupabaseColumnDefinition( - association: false, columnName: 'id', ), 'name': const RuntimeSupabaseColumnDefinition( - association: false, columnName: 'name', ), 'age': const RuntimeSupabaseColumnDefinition( - association: false, columnName: 'age', query: 'custom_age', ), @@ -58,31 +61,37 @@ class DemoAdapter extends SupabaseAdapter { final uniqueFields = {'id'}; } -DemoNestedAssociationModel _$DemoNestedAssociationModelFromSupabase(Map json) { - return DemoNestedAssociationModel( - id: json['id'] as String, - name: json['name'] as String, - nested: _$DemoAssociationModelFromSupabase(json['nested'] as Map), - ); -} +DemoNestedAssociationModel _$DemoNestedAssociationModelFromSupabase(Map json) => + DemoNestedAssociationModel( + id: json['id'] as String, + name: json['name'] as String, + nested: _$DemoAssociationModelFromSupabase(json['nested'] as Map), + ); Future> _$DemoNestedAssociationModelToSupabase( DemoNestedAssociationModel instance, -) async { - return { - 'id': instance.id, - 'name': instance.name, - 'nested': await _$DemoAssociationModelToSupabase(instance.nested), - }; -} +) async => + { + 'id': instance.id, + 'name': instance.name, + 'nested': await _$DemoAssociationModelToSupabase(instance.nested), + }; class DemoNestedAssociationModelAdapter extends SupabaseAdapter { @override - Future fromSupabase(data, {required provider, repository}) async => + Future fromSupabase( + Map data, { + required SupabaseProvider provider, + ModelRepository? repository, + }) async => _$DemoNestedAssociationModelFromSupabase(data); @override - Future> toSupabase(instance, {required provider, repository}) async => + Future> toSupabase( + DemoNestedAssociationModel instance, { + required SupabaseProvider provider, + ModelRepository? repository, + }) async => await _$DemoNestedAssociationModelToSupabase(instance); @override @@ -91,11 +100,9 @@ class DemoNestedAssociationModelAdapter extends SupabaseAdapter json) { - return DemoAssociationModel( - id: json['id'] as String, - name: json['name'] as String, - assoc: _$DemoFromSupabase(json['assoc'] as Map), - ); -} +DemoAssociationModel _$DemoAssociationModelFromSupabase(Map json) => + DemoAssociationModel( + id: json['id'] as String, + name: json['name'] as String, + assoc: _$DemoFromSupabase(json['assoc'] as Map), + ); Future> _$DemoAssociationModelToSupabase( DemoAssociationModel instance, { provider, repository, -}) async { - return { - 'id': instance.id, - 'name': instance.name, - 'assocs': await Future.wait>( - instance.assocs - ?.map( - (s) => DemoAdapter().toSupabase(s, provider: provider, repository: repository), - ) - .toList() ?? - [], - ), - 'assoc': await _$DemoToSupabase(instance.assoc), - }; -} +}) async => + { + 'id': instance.id, + 'name': instance.name, + 'assocs': await Future.wait>( + instance.assocs + ?.map( + (s) => DemoAdapter().toSupabase(s, provider: provider, repository: repository), + ) + .toList() ?? + [], + ), + 'assoc': await _$DemoToSupabase(instance.assoc), + }; class DemoAssociationModelAdapter extends SupabaseAdapter { @override - Future fromSupabase(data, {required provider, repository}) async => + Future fromSupabase( + Map data, { + required SupabaseProvider provider, + ModelRepository? repository, + }) async => _$DemoAssociationModelFromSupabase(data); @override - Future> toSupabase(instance, {required provider, repository}) async => + Future> toSupabase( + DemoAssociationModel instance, { + required SupabaseProvider provider, + ModelRepository? repository, + }) async => await _$DemoAssociationModelToSupabase(instance); @override @@ -163,16 +176,13 @@ class DemoAssociationModelAdapter extends SupabaseAdapter @override final fieldsToSupabaseColumns = { 'id': const RuntimeSupabaseColumnDefinition( - association: false, columnName: 'id', ), 'name': const RuntimeSupabaseColumnDefinition( - association: false, columnName: 'name', ), 'assoc': const RuntimeSupabaseColumnDefinition( association: true, - associationIsNullable: false, columnName: 'assoc_id', foreignKey: 'assoc_id', associationType: Demo, @@ -198,26 +208,31 @@ class DemoAssociationModelAdapter extends SupabaseAdapter final uniqueFields = {'id'}; } -RecursiveParent _$RecursiveParentFromSupabase(Map json) { - return RecursiveParent( - child: _$RecursiveChildFromSupabase(json['child'] as Map), - parentId: json['parent_id'] as String, - ); -} +RecursiveParent _$RecursiveParentFromSupabase(Map json) => RecursiveParent( + child: _$RecursiveChildFromSupabase(json['child'] as Map), + parentId: json['parent_id'] as String, + ); -Future> _$RecursiveParentToSupabase(RecursiveParent instance) async { - return { - 'child': await _$RecursiveChildToSupabase(instance.child), - }; -} +Future> _$RecursiveParentToSupabase(RecursiveParent instance) async => + { + 'child': await _$RecursiveChildToSupabase(instance.child), + }; class RecursiveParentAdapter extends SupabaseAdapter { @override - Future fromSupabase(data, {required provider, repository}) async => + Future fromSupabase( + Map data, { + required SupabaseProvider provider, + ModelRepository? repository, + }) async => _$RecursiveParentFromSupabase(data); @override - Future> toSupabase(instance, {required provider, repository}) async => + Future> toSupabase( + RecursiveParent instance, { + required SupabaseProvider provider, + ModelRepository? repository, + }) async => await _$RecursiveParentToSupabase(instance); @override @@ -231,7 +246,6 @@ class RecursiveParentAdapter extends SupabaseAdapter { associationType: RecursiveChild, ), 'parentId': const RuntimeSupabaseColumnDefinition( - association: false, columnName: 'parent_id', ), }; @@ -249,29 +263,34 @@ class RecursiveParentAdapter extends SupabaseAdapter { final uniqueFields = {'parentId'}; } -RecursiveChild _$RecursiveChildFromSupabase(Map json) { - return RecursiveChild( - parent: _$RecursiveParentFromSupabase(json['parent'] as Map), - childId: json['child_id'] as String, - otherAssoc: _$DemoFromSupabase(json['other_assoc'] as Map), - ); -} +RecursiveChild _$RecursiveChildFromSupabase(Map json) => RecursiveChild( + parent: _$RecursiveParentFromSupabase(json['parent'] as Map), + childId: json['child_id'] as String, + otherAssoc: _$DemoFromSupabase(json['other_assoc'] as Map), + ); -Future> _$RecursiveChildToSupabase(RecursiveChild instance) async { - return { - 'parent': await _$RecursiveParentToSupabase(instance.parent), - 'child_id': instance.childId, - 'other_assoc': await _$DemoToSupabase(instance.otherAssoc), - }; -} +Future> _$RecursiveChildToSupabase(RecursiveChild instance) async => + { + 'parent': await _$RecursiveParentToSupabase(instance.parent), + 'child_id': instance.childId, + 'other_assoc': await _$DemoToSupabase(instance.otherAssoc), + }; class RecursiveChildAdapter extends SupabaseAdapter { @override - Future fromSupabase(data, {required provider, repository}) async => + Future fromSupabase( + Map data, { + required SupabaseProvider provider, + ModelRepository? repository, + }) async => _$RecursiveChildFromSupabase(data); @override - Future> toSupabase(instance, {required provider, repository}) async => + Future> toSupabase( + RecursiveChild instance, { + required SupabaseProvider provider, + ModelRepository? repository, + }) async => await _$RecursiveChildToSupabase(instance); @override @@ -285,7 +304,6 @@ class RecursiveChildAdapter extends SupabaseAdapter { associationType: RecursiveParent, ), 'childId': const RuntimeSupabaseColumnDefinition( - association: false, columnName: 'child_id', ), 'otherAssoc': const RuntimeSupabaseColumnDefinition( diff --git a/packages/brick_supabase/test/query_supabase_transformer_test.dart b/packages/brick_supabase/test/query_supabase_transformer_test.dart index 776f53dc..291ca4d5 100644 --- a/packages/brick_supabase/test/query_supabase_transformer_test.dart +++ b/packages/brick_supabase/test/query_supabase_transformer_test.dart @@ -9,12 +9,11 @@ import '__mocks__.dart'; QuerySupabaseTransformer _buildTransformer([ Query? query, -]) { - return QuerySupabaseTransformer( - modelDictionary: supabaseModelDictionary, - query: query, - ); -} +]) => + QuerySupabaseTransformer( + modelDictionary: supabaseModelDictionary, + query: query, + ); final _supabaseClient = SupabaseClient( 'http://localhost:3000', @@ -35,9 +34,7 @@ extension _PostgrestBuilderExtension on PostgrestBuilder { } /// Get the decoded query from the URI of the builder. - String get query { - return Uri.decodeQueryComponent(uri.query); - } + String get query => Uri.decodeQueryComponent(uri.query); } void main() { @@ -92,7 +89,7 @@ void main() { }); test('by association field', () { - final query = Query.where('assoc', Where.exact('name', 'Thomas')); + final query = Query.where('assoc', const Where.exact('name', 'Thomas')); final select = _buildTransformer(query) .select(_supabaseClient.from(DemoAssociationModelAdapter().supabaseTableName)); @@ -104,7 +101,7 @@ void main() { }); test('neq', () { - final query = Query( + const query = Query( where: [Where('name', value: 'Jens', compare: Compare.notEqual)], ); final select = _buildTransformer(query) @@ -114,7 +111,7 @@ void main() { }); test('lt/gt/lte/gte', () { - final query = Query( + const query = Query( where: [ Where('age', value: '30', compare: Compare.lessThan), Where('age', value: '18', compare: Compare.greaterThan), @@ -132,7 +129,7 @@ void main() { }); test('contains', () { - final query = Query( + const query = Query( where: [Where('name', value: 'search', compare: Compare.contains)], ); final select = _buildTransformer(query) @@ -142,7 +139,7 @@ void main() { }); test('does not contain', () { - final query = Query( + const query = Query( where: [ Where('name', value: 'search', compare: Compare.doesNotContain), ], @@ -155,78 +152,170 @@ void main() { }); }); - group('#applyProviderArgs', () { + group('#applyQuery', () { test('orderBy', () { - final query = Query(providerArgs: {'orderBy': 'name asc'}); + const result = 'select=id,name,custom_age&order=name.asc.nullslast'; + + const withProviderArgs = Query(providerArgs: {'orderBy': 'name asc'}); + final providerQueryTransformer = _buildTransformer(withProviderArgs); + final providerFilterBuilder = + providerQueryTransformer.select(_supabaseClient.from(DemoAdapter().supabaseTableName)); + final providerTransformBuilder = providerQueryTransformer.applyQuery(providerFilterBuilder); + + expect( + providerTransformBuilder.query, + result, + ); + + const query = Query(orderBy: [OrderBy('name')]); final queryTransformer = _buildTransformer(query); final filterBuilder = queryTransformer.select(_supabaseClient.from(DemoAdapter().supabaseTableName)); - final transformBuilder = queryTransformer.applyProviderArgs(filterBuilder); + final transformBuilder = queryTransformer.applyQuery(filterBuilder); expect( transformBuilder.query, - 'select=id,name,custom_age&order=name.asc.nullslast', + result, ); }); test('orderBy with descending order', () { - final query = Query(providerArgs: {'orderBy': 'name desc'}); + const result = 'select=id,name,custom_age&order=age.desc.nullslast'; + + const query0 = Query(providerArgs: {'orderBy': 'age desc'}); + final queryTransformer0 = _buildTransformer(query0); + final filterBuilder0 = + queryTransformer0.select(_supabaseClient.from(DemoAdapter().supabaseTableName)); + final transformBuilder0 = queryTransformer0.applyQuery(filterBuilder0); + + expect( + transformBuilder0.query, + result, + ); + + const query = Query(orderBy: [OrderBy('age', ascending: false)]); final queryTransformer = _buildTransformer(query); final filterBuilder = queryTransformer.select(_supabaseClient.from(DemoAdapter().supabaseTableName)); - final transformBuilder = queryTransformer.applyProviderArgs(filterBuilder); + final transformBuilder = queryTransformer.applyQuery(filterBuilder); expect( transformBuilder.query, - 'select=id,name,custom_age&order=name.desc.nullslast', + result, ); }); test('orderBy with referenced table', () { - final query = Query( - providerArgs: {'orderBy': 'name desc', 'orderByReferencedTable': 'foreign_tables'}, + const result = + 'select=id,nested_column:demo_associations(id,name,assoc_id:demos!assoc_id(id,name,custom_age),assocs:demos(id,name,custom_age))&demo_associations.order=name.desc.nullslast'; + const query0 = Query( + providerArgs: {'orderBy': 'name desc', 'orderByReferencedTable': 'demo_associations'}, ); - final queryTransformer = _buildTransformer(query); - final filterBuilder = - queryTransformer.select(_supabaseClient.from(DemoAdapter().supabaseTableName)); - final transformBuilder = queryTransformer.applyProviderArgs(filterBuilder); + final queryTransformer0 = _buildTransformer(query0); + final filterBuilder0 = queryTransformer0 + .select(_supabaseClient.from(DemoNestedAssociationModelAdapter().supabaseTableName)); + final transformBuilder0 = queryTransformer0.applyQuery(filterBuilder0); + + expect( + transformBuilder0.query, + result, + ); + + const query = Query( + orderBy: [OrderBy.desc('nested', associationField: 'name')], + ); + final queryTransformer = _buildTransformer(query); + final filterBuilder = queryTransformer + .select(_supabaseClient.from(DemoNestedAssociationModelAdapter().supabaseTableName)); + final transformBuilder = queryTransformer.applyQuery(filterBuilder); expect( transformBuilder.query, - 'select=id,name,custom_age&foreign_tables.order=name.desc.nullslast', + result, + ); + }); + + test('orderBy with referenced table, no association field', () { + const query = Query( + orderBy: [OrderBy.desc('nested')], + ); + final queryTransformer = _buildTransformer(query); + final filterBuilder = queryTransformer + .select(_supabaseClient.from(DemoNestedAssociationModelAdapter().supabaseTableName)); + final transformBuilder = queryTransformer.applyQuery(filterBuilder); + + expect( + transformBuilder.query, + 'select=id,nested_column:demo_associations(id,name,assoc_id:demos!assoc_id(id,name,custom_age),assocs:demos(id,name,custom_age))&demo_associations.order=nested.desc.nullslast', ); }); test('limit', () { - final query = Query(providerArgs: {'limit': 10}); + const result = 'select=id,name,custom_age&limit=10'; + const query0 = Query(providerArgs: {'limit': 10}); + final queryTransformer0 = _buildTransformer(query0); + final filterBuilder0 = + queryTransformer0.select(_supabaseClient.from(DemoAdapter().supabaseTableName)); + final transformBuilder0 = queryTransformer0.applyQuery(filterBuilder0); + + expect(transformBuilder0.query, result); + + const query = Query(limit: 10); final queryTransformer = _buildTransformer(query); final filterBuilder = queryTransformer.select(_supabaseClient.from(DemoAdapter().supabaseTableName)); - final transformBuilder = queryTransformer.applyProviderArgs(filterBuilder); + final transformBuilder = queryTransformer.applyQuery(filterBuilder); - expect(transformBuilder.query, 'select=id,name,custom_age&limit=10'); + expect(transformBuilder.query, result); }); test('limit with referenced table', () { - final query = Query(providerArgs: {'limit': 10, 'limitReferencedTable': 'foreign_tables'}); - final queryTransformer = _buildTransformer(query); - final filterBuilder = - queryTransformer.select(_supabaseClient.from(DemoAdapter().supabaseTableName)); - final transformBuilder = queryTransformer.applyProviderArgs(filterBuilder); + const result = + 'select=id,nested_column:demo_associations(id,name,assoc_id:demos!assoc_id(id,name,custom_age),assocs:demos(id,name,custom_age))&demo_associations.limit=10'; + const query0 = + Query(providerArgs: {'limit': 10, 'limitReferencedTable': 'demo_associations'}); + final queryTransformer0 = _buildTransformer(query0); + final filterBuilder0 = queryTransformer0 + .select(_supabaseClient.from(DemoNestedAssociationModelAdapter().supabaseTableName)); + final transformBuilder0 = queryTransformer0.applyQuery(filterBuilder0); + + expect(transformBuilder0.query, result); + + const query = Query(limitBy: [LimitBy(10, evaluatedField: 'nested')]); + final queryTransformer = _buildTransformer(query); + final filterBuilder = queryTransformer + .select(_supabaseClient.from(DemoNestedAssociationModelAdapter().supabaseTableName)); + final transformBuilder = queryTransformer.applyQuery(filterBuilder); - expect(transformBuilder.query, 'select=id,name,custom_age&foreign_tables.limit=10'); + expect( + transformBuilder.query, + result, + ); }); test('combined orderBy and limit', () { - final query = Query(providerArgs: {'orderBy': 'name desc', 'limit': 20}); + const result = 'select=id,name,custom_age&order=name.desc.nullslast&limit=20'; + + const query0 = Query(providerArgs: {'orderBy': 'name desc', 'limit': 20}); + final queryTransformer0 = _buildTransformer(query0); + final filterBuilder0 = + queryTransformer0.select(_supabaseClient.from(DemoAdapter().supabaseTableName)); + final transformBuilder0 = queryTransformer0.applyQuery(filterBuilder0); + + expect( + transformBuilder0.query, + result, + ); + + const query = Query(limit: 20, orderBy: [OrderBy('name', ascending: false)]); final queryTransformer = _buildTransformer(query); final filterBuilder = queryTransformer.select(_supabaseClient.from(DemoAdapter().supabaseTableName)); - final transformBuilder = queryTransformer.applyProviderArgs(filterBuilder); + final transformBuilder = queryTransformer.applyQuery(filterBuilder); expect( transformBuilder.query, - 'select=id,name,custom_age&order=name.desc.nullslast&limit=20', + result, ); }); }); @@ -273,14 +362,14 @@ void main() { test('missing field', () { final transformer = _buildTransformer(); - expect(transformer.expandCondition(Where.exact('made_up_field', 1)), isEmpty); + expect(transformer.expandCondition(const Where.exact('made_up_field', 1)), isEmpty); }); test('matching a value to an association', () { final transformer = _buildTransformer(); expect( - () => transformer.expandCondition(Where.exact('assoc', 1)), + () => transformer.expandCondition(const Where.exact('assoc', 1)), throwsA(const TypeMatcher()), ); }); @@ -289,7 +378,7 @@ void main() { final transformer = _buildTransformer(); final result = transformer.expandCondition( WherePhrase([ - Where.exact('id', 1), + const Where.exact('id', 1), const Or('name').isExactly('Guy'), ]), ); diff --git a/packages/brick_supabase/test/supabase_provider_test.dart b/packages/brick_supabase/test/supabase_provider_test.dart index 5707f2f8..1130a40d 100644 --- a/packages/brick_supabase/test/supabase_provider_test.dart +++ b/packages/brick_supabase/test/supabase_provider_test.dart @@ -15,7 +15,7 @@ void main() { tearDown(mock.tearDown); test('#delete', () async { - final req = SupabaseRequest( + const req = SupabaseRequest( requestMethod: 'DELETE', filter: 'id=eq.1', ); @@ -29,7 +29,7 @@ void main() { }); test('#exists', () async { - final req = SupabaseRequest(); + const req = SupabaseRequest(); final instance = Demo(age: 1, name: 'Demo 1', id: '1'); final resp = SupabaseResponse( [await mock.serialize(instance)], @@ -43,7 +43,7 @@ void main() { }); test('#get', () async { - final req = SupabaseRequest(); + const req = SupabaseRequest(); final resp = SupabaseResponse([ await mock.serialize(Demo(age: 1, name: 'Demo 1', id: '1')), await mock.serialize(Demo(age: 2, name: 'Demo 2', id: '2')), @@ -61,7 +61,7 @@ void main() { }); test('#insert', () async { - final req = SupabaseRequest( + const req = SupabaseRequest( requestMethod: 'POST', filter: 'id=eq.1', limit: 1, @@ -78,7 +78,7 @@ void main() { }); test('#update', () async { - final req = SupabaseRequest( + const req = SupabaseRequest( requestMethod: 'PATCH', filter: 'id=eq.1', limit: 1, @@ -96,7 +96,7 @@ void main() { group('#upsert', () { test('no associations', () async { - final req = SupabaseRequest( + const req = SupabaseRequest( requestMethod: 'POST', filter: 'id=eq.1', limit: 1, @@ -113,14 +113,14 @@ void main() { }); test('one association', () async { - final demoModelReq = SupabaseRequest( + const demoModelReq = SupabaseRequest( requestMethod: 'POST', filter: 'id=eq.2', limit: 1, ); final demoModelResp = SupabaseResponse(await mock.serialize(Demo(age: 1, name: 'Demo 1', id: '1'))); - final assocReq = SupabaseRequest( + const assocReq = SupabaseRequest( requestMethod: 'POST', filter: 'id=eq.1', limit: 1, diff --git a/packages/brick_supabase_generators/CHANGELOG.md b/packages/brick_supabase_generators/CHANGELOG.md index a8152242..702bb0ad 100644 --- a/packages/brick_supabase_generators/CHANGELOG.md +++ b/packages/brick_supabase_generators/CHANGELOG.md @@ -1,5 +1,10 @@ ## Unreleased +## 1.1.0 + +- Upgrade `brick_core` to `1.3.0` +- Update analysis to modern lints + ## 1.0.2 - Interpret `@Supabase(query:)` in `SupabaseFields` diff --git a/packages/brick_supabase_generators/lib/src/supabase_deserialize.dart b/packages/brick_supabase_generators/lib/src/supabase_deserialize.dart index 3ee5568d..8cea15f9 100644 --- a/packages/brick_supabase_generators/lib/src/supabase_deserialize.dart +++ b/packages/brick_supabase_generators/lib/src/supabase_deserialize.dart @@ -4,9 +4,10 @@ import 'package:brick_supabase/brick_supabase.dart'; import 'package:brick_supabase_generators/src/supabase_fields.dart'; import 'package:brick_supabase_generators/src/supabase_serdes_generator.dart'; -/// Generate a function to produce a [ClassElement] from REST data +/// Generate a function to produce a [ClassElement] from Supabase data class SupabaseDeserialize extends SupabaseSerdesGenerator with JsonDeserialize { + /// Generate a function to produce a [ClassElement] from Supabase data SupabaseDeserialize( super.element, super.fields, { diff --git a/packages/brick_supabase_generators/lib/src/supabase_fields.dart b/packages/brick_supabase_generators/lib/src/supabase_fields.dart index d2e7870d..a1e09e88 100644 --- a/packages/brick_supabase_generators/lib/src/supabase_fields.dart +++ b/packages/brick_supabase_generators/lib/src/supabase_fields.dart @@ -10,10 +10,11 @@ class SupabaseAnnotationFinder extends AnnotationFinder /// Model-level settings final SupabaseSerializable? config; + /// Find `@Supabase` given a field SupabaseAnnotationFinder([this.config]); @override - Supabase from(element) { + Supabase from(FieldElement element) { final obj = objectForField(element); if (obj == null) { @@ -57,8 +58,11 @@ class SupabaseAnnotationFinder extends AnnotationFinder class SupabaseFields extends FieldsForClass { @override final SupabaseAnnotationFinder finder; + + /// final SupabaseSerializable? config; + /// Converts all fields to [Supabase]s for later consumption SupabaseFields(ClassElement element, [this.config]) : finder = SupabaseAnnotationFinder(config), super(element: element); diff --git a/packages/brick_supabase_generators/lib/src/supabase_serdes_generator.dart b/packages/brick_supabase_generators/lib/src/supabase_serdes_generator.dart index 4dd796b5..2868f9e4 100644 --- a/packages/brick_supabase_generators/lib/src/supabase_serdes_generator.dart +++ b/packages/brick_supabase_generators/lib/src/supabase_serdes_generator.dart @@ -2,7 +2,9 @@ import 'package:brick_json_generators/json_serdes_generator.dart'; import 'package:brick_supabase/brick_supabase.dart'; import 'package:brick_supabase_generators/src/supabase_fields.dart'; +/// abstract class SupabaseSerdesGenerator extends JsonSerdesGenerator { + /// SupabaseSerdesGenerator( super.element, SupabaseFields super.fields, { diff --git a/packages/brick_supabase_generators/lib/src/supabase_serialize.dart b/packages/brick_supabase_generators/lib/src/supabase_serialize.dart index e1cb308b..d79c55f2 100644 --- a/packages/brick_supabase_generators/lib/src/supabase_serialize.dart +++ b/packages/brick_supabase_generators/lib/src/supabase_serialize.dart @@ -7,6 +7,7 @@ import 'package:brick_supabase_generators/src/supabase_serdes_generator.dart'; /// Generate a function to produce a [ClassElement] to REST data class SupabaseSerialize extends SupabaseSerdesGenerator with JsonSerialize { + /// Generate a function to produce a [ClassElement] to REST data SupabaseSerialize( super.element, super.fields, { diff --git a/packages/brick_supabase_generators/lib/supabase_model_serdes_generator.dart b/packages/brick_supabase_generators/lib/supabase_model_serdes_generator.dart index 9cbdc541..4fc8ac55 100644 --- a/packages/brick_supabase_generators/lib/supabase_model_serdes_generator.dart +++ b/packages/brick_supabase_generators/lib/supabase_model_serdes_generator.dart @@ -13,6 +13,8 @@ class SupabaseModelSerdesGenerator extends ProviderSerializableGenerator=3.0.0 <4.0.0" @@ -12,7 +12,7 @@ environment: dependencies: analyzer: ">=6.0.0 <7.0.0" brick_build: ">=3.2.0 <4.0.0" - brick_core: ">=1.2.1 <2.0.0" + brick_core: ">=1.3.0 <2.0.0" brick_json_generators: ">=3.1.0 <4.0.0" brick_supabase: ">=1.1.3 <2.0.0" build: ">=2.0.0 <3.0.0" @@ -20,7 +20,7 @@ dependencies: source_gen: ">=1.2.2 <2.0.0" dev_dependencies: - test: ^1.20.1 - lints: ^2.0.1 brick_build_test: path: ../brick_build_test + lints: + test: diff --git a/packages/brick_supabase_generators/test/supabase_model_serdes_generator/test_constructor_member_field_mismatch.dart b/packages/brick_supabase_generators/test/supabase_model_serdes_generator/test_constructor_member_field_mismatch.dart index 5b35bea1..1542bf1b 100644 --- a/packages/brick_supabase_generators/test/supabase_model_serdes_generator/test_constructor_member_field_mismatch.dart +++ b/packages/brick_supabase_generators/test/supabase_model_serdes_generator/test_constructor_member_field_mismatch.dart @@ -1,6 +1,6 @@ import 'package:brick_supabase/brick_supabase.dart'; -final output = r''' +const output = r''' Future _$SupabaseConstructorMemberFieldMismatchFromSupabase( Map data, diff --git a/packages/brick_supabase_generators/test/supabase_model_serdes_generator/test_enum_as_string.dart b/packages/brick_supabase_generators/test/supabase_model_serdes_generator/test_enum_as_string.dart index 93a020b4..4240fb2c 100644 --- a/packages/brick_supabase_generators/test/supabase_model_serdes_generator/test_enum_as_string.dart +++ b/packages/brick_supabase_generators/test/supabase_model_serdes_generator/test_enum_as_string.dart @@ -1,6 +1,6 @@ import 'package:brick_supabase/brick_supabase.dart'; -final output = r''' +const output = r''' Future _$EnumAsStringFromSupabase(Map data, {required SupabaseProvider provider, SupabaseFirstRepository? repository}) async { diff --git a/packages/brick_supabase_generators/test/supabase_model_serdes_generator/test_ignore_from_to.dart b/packages/brick_supabase_generators/test/supabase_model_serdes_generator/test_ignore_from_to.dart index 242207f6..a47ad059 100644 --- a/packages/brick_supabase_generators/test/supabase_model_serdes_generator/test_ignore_from_to.dart +++ b/packages/brick_supabase_generators/test/supabase_model_serdes_generator/test_ignore_from_to.dart @@ -1,6 +1,6 @@ import 'package:brick_supabase/brick_supabase.dart'; -final output = r''' +const output = r''' Future _$SupabaseIgnoreFromToFromSupabase( Map data, {required SupabaseProvider provider, diff --git a/packages/brick_supabase_generators/test/supabase_model_serdes_generator/test_runtime_supabase_column_definition.dart b/packages/brick_supabase_generators/test/supabase_model_serdes_generator/test_runtime_supabase_column_definition.dart index 70921a53..fa9d60b8 100644 --- a/packages/brick_supabase_generators/test/supabase_model_serdes_generator/test_runtime_supabase_column_definition.dart +++ b/packages/brick_supabase_generators/test/supabase_model_serdes_generator/test_runtime_supabase_column_definition.dart @@ -1,6 +1,6 @@ import 'package:brick_supabase/brick_supabase.dart'; -final output = r""" +const output = r""" // GENERATED CODE DO NOT EDIT part of '../brick.g.dart'; diff --git a/packages/brick_supabase_generators/test/supabase_model_serdes_generator/test_unique.dart b/packages/brick_supabase_generators/test/supabase_model_serdes_generator/test_unique.dart index 49a1e0ad..473dccfd 100644 --- a/packages/brick_supabase_generators/test/supabase_model_serdes_generator/test_unique.dart +++ b/packages/brick_supabase_generators/test/supabase_model_serdes_generator/test_unique.dart @@ -1,6 +1,6 @@ import 'package:brick_supabase/brick_supabase.dart'; -final output = r""" +const output = r""" // GENERATED CODE DO NOT EDIT part of '../brick.g.dart'; diff --git a/packages/brick_supabase_generators/test/supabase_model_serdes_generator/test_unserializable_field_with_generator.dart b/packages/brick_supabase_generators/test/supabase_model_serdes_generator/test_unserializable_field_with_generator.dart index 5803a7cd..f9574d1d 100644 --- a/packages/brick_supabase_generators/test/supabase_model_serdes_generator/test_unserializable_field_with_generator.dart +++ b/packages/brick_supabase_generators/test/supabase_model_serdes_generator/test_unserializable_field_with_generator.dart @@ -3,7 +3,7 @@ import 'dart:typed_data'; import 'package:brick_supabase/brick_supabase.dart'; -final output = r''' +const output = r''' Future _$SupabaseUnserializableFieldWithGeneratorFromSupabase( Map data, diff --git a/pubspec.yaml b/pubspec.yaml index 0dd80a77..c0e1cb18 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,8 +1,8 @@ name: brick_melos environment: - sdk: '>=3.0.0 <4.0.0' + sdk: ">=3.0.0 <4.0.0" dev_dependencies: - lints: ^2.1.1 + lints: melos: ^3.1.0