From 096599a7e664b67ac100db575c02bf8889b5962b Mon Sep 17 00:00:00 2001 From: Tim Shedor Date: Fri, 29 Nov 2024 11:33:49 -0800 Subject: [PATCH 1/5] feat(core): deprecate fields in anticipation of v4 --- packages/brick_core/analysis_options.yaml | 325 ++++++++++++++++++ .../brick_core/lib/field_serializable.dart | 4 +- packages/brick_core/lib/query.dart | 1 + packages/brick_core/lib/src/adapter.dart | 1 + packages/brick_core/lib/src/model.dart | 5 +- .../brick_core/lib/src/model_dictionary.dart | 1 + .../brick_core/lib/src/model_repository.dart | 22 +- .../lib/src/query/provider_query.dart | 13 + packages/brick_core/lib/src/query/query.dart | 76 ++-- .../brick_core/lib/src/query/sort_by.dart | 40 +++ packages/brick_core/lib/src/query/where.dart | 29 +- packages/brick_core/pubspec.yaml | 4 +- packages/brick_core/test/__mocks__.dart | 5 +- packages/brick_core/test/provider_test.dart | 5 +- .../brick_core/test/query/and_or_test.dart | 72 ++-- .../brick_core/test/query/query_test.dart | 137 ++++---- .../brick_core/test/query/sort_by_test.dart | 52 +++ .../brick_core/test/query/where_test.dart | 50 +-- 18 files changed, 652 insertions(+), 190 deletions(-) create mode 100644 packages/brick_core/lib/src/query/provider_query.dart create mode 100644 packages/brick_core/lib/src/query/sort_by.dart create mode 100644 packages/brick_core/test/query/sort_by_test.dart diff --git a/packages/brick_core/analysis_options.yaml b/packages/brick_core/analysis_options.yaml index f04c6cf0..1d5855fb 100644 --- a/packages/brick_core/analysis_options.yaml +++ b/packages/brick_core/analysis_options.yaml @@ -1 +1,326 @@ include: ../../analysis_options.yaml + +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 + - valid_regexps + - void_checks + +analyzer: + exclude: + - example/ + - "**/example/" + - example_rest + - example_graphql + - "**/*.g.dart" + + errors: + # override custom + always_use_package_imports: error + camel_case_extensions: error + camel_case_types: error + curly_braces_in_flow_control_structures: error + directives_ordering: error + file_names: error + prefer_single_quotes: error + prefer_is_empty: error + prefer_is_not_empty: error + require_trailing_commas: error + sort_pub_dependencies: error + unnecessary_statements: error + + # override flutter_lints + avoid_print: error + avoid_unnecessary_containers: error + avoid_web_libraries_in_flutter: error + no_logic_in_create_state: error + prefer_const_constructors: error + prefer_const_constructors_in_immutables: error + prefer_const_declarations: error + prefer_const_literals_to_create_immutables: error + sized_box_for_whitespace: error + sort_child_properties_last: error + use_build_context_synchronously: error + use_full_hex_values_for_flutter_colors: error + use_key_in_widget_constructors: error + + # override recommended lints + always_require_non_null_named_parameters: error + annotate_overrides: error + avoid_function_literals_in_foreach_calls: error + avoid_init_to_null: error + avoid_null_checks_in_equality_operators: error + avoid_renaming_method_parameters: error + avoid_return_types_on_setters: error + avoid_returning_null_for_void: error + avoid_single_cascade_in_expression_statements: error + await_only_futures: error + constant_identifier_names: error + control_flow_in_finally: error + depend_on_referenced_packages: ignore + empty_constructor_bodies: error + empty_statements: error + exhaustive_cases: error + implementation_imports: ignore + invalid_case_patterns: error + library_names: error + library_prefixes: error + library_private_types_in_public_api: error + matching_super_parameters: error + no_leading_underscores_for_library_prefixes: error + no_leading_underscores_for_local_identifiers: error + no_literal_bool_comparisons: error + null_check_on_nullable_type_parameter: error + null_closures: error + overridden_fields: error + package_names: error + prefer_adjacent_string_concatenation: error + prefer_collection_literals: error + prefer_conditional_assignment: error + prefer_contains: error + prefer_equal_for_default_values: error + prefer_final_fields: error + prefer_for_elements_to_map_fromIterable: error + prefer_function_declarations_over_variables: error + prefer_if_null_operators: error + prefer_initializing_formals: error + prefer_inlined_adds: error + prefer_interpolation_to_compose_strings: error + prefer_is_not_operator: error + prefer_null_aware_operators: error + prefer_spread_collections: error + prefer_void_to_null: error + recursive_getters: error + slash_for_doc_comments: error + type_init_formals: error + type_literal_in_constant_pattern: error + unnecessary_brace_in_string_interps: error + unnecessary_breaks: error + unnecessary_const: error + unnecessary_constructor_name: error + unnecessary_getters_setters: error + unnecessary_late: error + unnecessary_new: error + unnecessary_null_aware_assignments: error + unnecessary_null_in_if_null_operators: error + unnecessary_nullable_for_final_variable_declarations: error + unnecessary_string_escapes: error + unnecessary_string_interpolations: error + unnecessary_this: error + use_function_type_syntax_for_parameters: error + use_rethrow_when_possible: error 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..8ce27689 100644 --- a/packages/brick_core/lib/query.dart +++ b/packages/brick_core/lib/query.dart @@ -1,3 +1,4 @@ export 'package:brick_core/src/query/and_or.dart'; export 'package:brick_core/src/query/query.dart'; +export 'package:brick_core/src/query/sort_by.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..9fa09aa4 100644 --- a/packages/brick_core/lib/src/adapter.dart +++ b/packages/brick_core/lib/src/adapter.dart @@ -1,4 +1,5 @@ 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 /// output between [Provider]s, subclasses must pass the data in `Map` format. diff --git a/packages/brick_core/lib/src/model.dart b/packages/brick_core/lib/src/model.dart index f46126e0..f67a1962 100644 --- a/packages/brick_core/lib/src/model.dart +++ b/packages/brick_core/lib/src/model.dart @@ -1,4 +1,7 @@ -/// 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 { diff --git a/packages/brick_core/lib/src/model_dictionary.dart b/packages/brick_core/lib/src/model_dictionary.dart index b272735c..8566e6f1 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. diff --git a/packages/brick_core/lib/src/model_repository.dart b/packages/brick_core/lib/src/model_repository.dart index 2a27201e..cb20a800 100644 --- a/packages/brick_core/lib/src/model_repository.dart +++ b/packages/brick_core/lib/src/model_repository.dart @@ -9,27 +9,24 @@ import 'package:brick_core/src/query/query.dart'; /// /// 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 +/// 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 { 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]. + /// 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 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]. + /// 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 get({Query query}); @@ -40,16 +37,15 @@ 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]. + /// 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 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; const SingleProviderRepository(this.provider); 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..6890b708 --- /dev/null +++ b/packages/brick_core/lib/src/query/provider_query.dart @@ -0,0 +1,13 @@ +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. +/// +/// [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; +} diff --git a/packages/brick_core/lib/src/query/query.dart b/packages/brick_core/lib/src/query/query.dart index 4f9e3ecd..27dd1469 100644 --- a/packages/brick_core/lib/src/query/query.dart +++ b/packages/brick_core/lib/src/query/query.dart @@ -1,27 +1,45 @@ +// 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/provider_query.dart'; +import 'package:brick_core/src/query/sort_by.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. + final List forProviders; + + /// The response should not exceed this number. + final int? limit; + + /// The response should start at this index. + final int? offset; + /// Properties that interact with the provider's source. For example, `'limit'`. /// The value **must** be serializable by `jsonEncode`. + @Deprecated('Use limit, offset, sortBy, or forProviders instead') final Map providerArgs; - bool get unlimited => providerArgs['limit'] == null || providerArgs['limit'] < 1; + final List sortBy; + + 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,27 +62,23 @@ class Query { /// will only return results where the ID is 1 **and** the name is Thomas. final List? where; - Query({ + const Query({ this.action, - Map? providerArgs, + this.forProviders = const [], + this.limit, + this.offset, + @Deprecated('Use limit, offset, sortBy, or forProviders instead.') this.providerArgs = const {}, + this.sortBy = 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']], + limit: json['limit'] as int?, + offset: json['offset'] as int?, providerArgs: json['providerArgs'], + sortBy: json['sortBy']?.map(SortBy.fromJson).toList() ?? [], where: json['where']?.map(WhereCondition.fromJson), ); } @@ -75,34 +89,41 @@ 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, ); } Query copyWith({ QueryAction? action, + int? limit, + int? offset, Map? providerArgs, + List? sortBy, List? where, }) => Query( action: action ?? this.action, + limit: limit ?? this.limit, + offset: offset ?? this.offset, providerArgs: providerArgs ?? this.providerArgs, + sortBy: sortBy ?? this.sortBy, where: where ?? this.where, ); Map toJson() { return { if (action != null) 'action': QueryAction.values.indexOf(action!), + if (limit != null) 'limit': limit, + if (offset != null) 'offset': offset, 'providerArgs': providerArgs, + if (sortBy.isNotEmpty) 'sortBy': sortBy.map((s) => s.toJson()).toList(), if (where != null) 'where': where!.map((w) => w.toJson()).toList(), }; } @@ -115,11 +136,20 @@ class Query { identical(this, other) || other is Query && action == other.action && + limit == other.limit && + offset == other.offset && _mapEquality.equals(providerArgs, other.providerArgs) && + _listEquality.equals(sortBy, other.sortBy) && _listEquality.equals(where, other.where); @override - int get hashCode => action.hashCode ^ providerArgs.hashCode ^ where.hashCode; + int get hashCode => + action.hashCode ^ + limit.hashCode ^ + offset.hashCode ^ + providerArgs.hashCode ^ + sortBy.hashCode ^ + where.hashCode; } /// How the query interacts with the provider diff --git a/packages/brick_core/lib/src/query/sort_by.dart b/packages/brick_core/lib/src/query/sort_by.dart new file mode 100644 index 00000000..d917f80b --- /dev/null +++ b/packages/brick_core/lib/src/query/sort_by.dart @@ -0,0 +1,40 @@ +import 'package:brick_core/core.dart'; +import 'package:brick_core/src/provider.dart'; + +class SortBy { + /// 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; + + /// Defaults to `true`. + final bool ascending; + + const SortBy(this.evaluatedField, {this.ascending = true}); + + factory SortBy.fromJson(Map json) { + return SortBy( + json['evaluatedField'], + ascending: json['ascending'], + ); + } + + Map toJson() { + return { + 'evaluatedField': evaluatedField, + 'ascending': ascending, + }; + } + + @override + String toString() => '$evaluatedField ${ascending ? 'ASC' : 'DESC'}'; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is SortBy && evaluatedField == other.evaluatedField && ascending == other.ascending; + + @override + int get hashCode => evaluatedField.hashCode ^ ascending.hashCode; +} diff --git a/packages/brick_core/lib/src/query/where.dart b/packages/brick_core/lib/src/query/where.dart index 645879f0..e9e3245d 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(); @@ -23,7 +24,7 @@ abstract class WhereCondition { /// Nested conditions. Leave unchanged for [WhereCondition]s that do not nest. final List? conditions = null; - /// 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; @@ -106,7 +107,7 @@ class Where extends WhereCondition { /// 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, { @@ -116,14 +117,14 @@ class Where extends WhereCondition { }) : isRequired = isRequired ?? true, compare = compare ?? Compare.exact; - /// A condition written with brevity. [required] defaults `true`. - factory Where.exact(String evaluatedField, dynamic value, {bool isRequired = true}) => + /// A condition written with brevity. [isRequired] defaults `true`. + factory Where.exact(String evaluatedField, value, {bool isRequired = true}) => Where(evaluatedField, value: value, compare: Compare.exact, isRequired: isRequired); - Where isExactly(dynamic value) => + Where isExactly(value) => Where(evaluatedField, value: value, compare: Compare.exact, isRequired: isRequired); - Where isBetween(dynamic value1, dynamic value2) { + Where isBetween(value1, value2) { assert(value1.runtimeType == value2.runtimeType, 'Comparison values must be the same type'); return Where( evaluatedField, @@ -133,33 +134,33 @@ class Where extends WhereCondition { ); } - Where contains(dynamic value) => + Where contains(value) => Where(evaluatedField, value: value, compare: Compare.contains, isRequired: isRequired); - Where doesNotContain(dynamic value) => + Where doesNotContain(value) => Where(evaluatedField, value: value, compare: Compare.doesNotContain, isRequired: isRequired); - Where isLessThan(dynamic value) => + Where isLessThan(value) => Where(evaluatedField, value: value, compare: Compare.lessThan, isRequired: isRequired); - Where isLessThanOrEqualTo(dynamic value) => Where( + Where isLessThanOrEqualTo(value) => Where( evaluatedField, value: value, compare: Compare.lessThanOrEqualTo, isRequired: isRequired, ); - Where isGreaterThan(dynamic value) => + Where isGreaterThan(value) => Where(evaluatedField, value: value, compare: Compare.greaterThan, isRequired: isRequired); - Where isGreaterThanOrEqualTo(dynamic value) => Where( + Where isGreaterThanOrEqualTo(value) => Where( evaluatedField, value: value, compare: Compare.greaterThanOrEqualTo, isRequired: isRequired, ); - Where isNot(dynamic value) => + Where isNot(value) => Where(evaluatedField, value: value, compare: Compare.notEqual, isRequired: isRequired); /// Recursively find conditions that evaluate a specific field. A field is a member on a model, @@ -247,7 +248,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..c88435cd 100644 --- a/packages/brick_core/pubspec.yaml +++ b/packages/brick_core/pubspec.yaml @@ -13,7 +13,7 @@ dependencies: collection: ">=1.15.0 <2.0.0" dev_dependencies: + dart_style: ">=2.0.0 <3.0.0" + lints: ">=4.0.0 <6.0.0" mockito: ^5.0.0 test: ^1.16.5 - lints: ^2.0.1 - dart_style: ">=2.0.0 <3.0.0" diff --git a/packages/brick_core/test/__mocks__.dart b/packages/brick_core/test/__mocks__.dart index 3a719d05..699fe6de 100644 --- a/packages/brick_core/test/__mocks__.dart +++ b/packages/brick_core/test/__mocks__.dart @@ -24,8 +24,7 @@ class DemoProvider extends Provider { @override Future> get<_Model extends DemoModel>({query, repository}) { - final list = []; - list.add(DemoModel('Thomas')); + final list = [DemoModel('Thomas')]; return Future.value(list); } @@ -48,4 +47,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/query_test.dart b/packages/brick_core/test/query/query_test.dart index 8edfe641..102cde89 100644 --- a/packages/brick_core/test/query/query_test.dart +++ b/packages/brick_core/test/query/query_test.dart @@ -1,3 +1,5 @@ +// 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'; @@ -6,49 +8,58 @@ void main() { group('Query', () { group('properties', () { test('#action', () { - final q = Query(action: QueryAction.delete); + const 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'}); + const q = Query(providerArgs: {'page': 1, 'sort': 'by_user_asc'}); expect(q.providerArgs['page'], 1); expect(q.providerArgs['sort'], 'by_user_asc'); }); test('#providerArgs.limit', () { - final q0 = Query(providerArgs: {'limit': 0}); - expect(q0.providerArgs['limit'], 0); + const q0 = Query(limit: 0); + expect(q0.limit, 0); - final q10 = Query(providerArgs: {'limit': 10}); - expect(q10.providerArgs['limit'], 10); + const q10 = Query(limit: 10); + expect(q10.limit, 10); - final q18 = Query(providerArgs: {'limit': 18}); - expect(q18.providerArgs['limit'], 18); + const q18 = Query(limit: 18); + expect(q18.limit, 18); - expect(() => Query(providerArgs: {'limit': -1}), throwsA(TypeMatcher())); + expect( + () => const Query(limit: -1), + throwsA(const TypeMatcher()), + ); }); test('#providerArgs.offset', () { - final q0 = Query(providerArgs: {'limit': 10, 'offset': 0}); - expect(q0.providerArgs['offset'], 0); + 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())); + expect( + () => const Query(offset: -1), + throwsA(const TypeMatcher()), + ); - expect(() => Query(providerArgs: {'offset': 1}), throwsA(TypeMatcher())); + expect( + () => const Query(offset: 1), + throwsA(const TypeMatcher()), + ); }); }); test('#where', () { - final q = Query( + const q = Query( where: [ Where('name', value: 'Thomas'), ], @@ -61,96 +72,88 @@ void main() { group('==', () { test('properties are the same', () { - final q1 = Query( + const q1 = Query( action: QueryAction.delete, - providerArgs: { - 'limit': 3, - 'offset': 3, - }, + limit: 3, + offset: 3, ); - final q2 = Query( + const q2 = Query( action: QueryAction.delete, - providerArgs: { - 'limit': 3, - 'offset': 3, - }, + limit: 3, + offset: 3, ); expect(q1, q2); }); test('providerArgs are the same', () { - final q1 = Query(providerArgs: {'name': 'Guy'}); - final q2 = Query(providerArgs: {'name': 'Guy'}); + const q1 = Query(providerArgs: {'name': 'Guy'}); + const q2 = Query(providerArgs: {'name': 'Guy'}); expect(q1, q2); }); test('providerArgs have different values', () { - final q1 = Query(providerArgs: {'name': 'Thomas'}); - final q2 = Query(providerArgs: {'name': 'Guy'}); + const q1 = Query(providerArgs: {'name': 'Thomas'}); + const q2 = Query(providerArgs: {'name': 'Guy'}); expect(q1, isNot(q2)); }); test('providerArgs have different keys', () { - final q1 = Query(providerArgs: {'email': 'guy@guy.com'}); - final q2 = Query(providerArgs: {'name': 'Guy'}); + const q1 = Query(providerArgs: {'email': 'guy@guy.com'}); + const q2 = Query(providerArgs: {'name': 'Guy'}); expect(q1, isNot(q2)); }); test('providerArgs are null', () { - final q1 = Query(); - final q2 = Query(providerArgs: {'name': 'Guy'}); + const q1 = Query(); + const q2 = Query(providerArgs: {'name': 'Guy'}); expect(q1, isNot(q2)); - final q3 = Query(); + 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}); + const q1 = Query(action: QueryAction.insert, limit: 10, offset: 10); + final q2 = q1.copyWith(limit: 20); expect(q2.action, QueryAction.insert); - expect(q2.providerArgs['limit'], 20); - expect(q2.providerArgs['offset'], null); + expect(q2.limit, 20); + expect(q2.offset, null); - final q3 = q1.copyWith(providerArgs: {'limit': 50, 'offset': 20}); + final q3 = q1.copyWith(limit: 50, offset: 20); expect(q3.action, QueryAction.insert); - expect(q3.providerArgs['limit'], 50); - expect(q3.providerArgs['offset'], 20); + expect(q3.limit, 50); + expect(q3.offset, 20); }); test('appends', () { - final q1 = Query(action: QueryAction.insert); - final q2 = q1.copyWith(providerArgs: {'limit': 20}); + const q1 = Query(action: QueryAction.insert); + final q2 = q1.copyWith(limit: 20); - expect(q1.providerArgs['limit'], null); + expect(q1.limit, null); expect(q2.action, QueryAction.insert); - expect(q2.providerArgs['limit'], 20); + expect(q2.limit, 20); }); }); test('#toJson', () { - final source = Query( + const source = Query( action: QueryAction.update, - providerArgs: { - 'limit': 3, - 'offset': 3, - }, + limit: 3, + offset: 3, ); expect( source.toJson(), { 'action': 2, - 'providerArgs': { - 'limit': 3, - 'offset': 3, - }, + 'limit': 3, + 'offset': 3, }, ); }); @@ -159,36 +162,32 @@ void main() { test('.fromJson', () { final json = { 'action': 2, - 'providerArgs': { - 'limit': 3, - 'offset': 3, - }, + 'limit': 3, + 'offset': 3, }; final result = Query.fromJson(json); expect( result, - Query( + const Query( action: QueryAction.update, - providerArgs: { - 'limit': 3, - 'offset': 3, - }, + limit: 3, + offset: 3, ), ); }); group('.where', () { test('required arguments', () { - final expandedQuery = Query(where: [Where('id', value: 2)]); + 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(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}); + 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/sort_by_test.dart b/packages/brick_core/test/query/sort_by_test.dart new file mode 100644 index 00000000..addced73 --- /dev/null +++ b/packages/brick_core/test/query/sort_by_test.dart @@ -0,0 +1,52 @@ +// ignore_for_file: deprecated_member_use_from_same_package + +import 'package:brick_core/src/query/sort_by.dart'; +import 'package:test/test.dart'; + +void main() { + group('SortBy', () { + test('equality', () { + expect( + const SortBy('name', ascending: true), + SortBy.fromJson({'evaluatedField': 'name', 'ascending': true}), + ); + expect( + const SortBy('name', ascending: false), + SortBy.fromJson({'evaluatedField': 'name', 'ascending': false}), + ); + }); + + test('#toJson', () { + expect( + const SortBy('name', ascending: true).toJson(), + {'evaluatedField': 'name', 'ascending': true}, + ); + expect( + const SortBy('name', ascending: false).toJson(), + {'evaluatedField': 'name', 'ascending': false}, + ); + }); + + test('#toString', () { + expect( + const SortBy('name', ascending: true).toString(), + 'evaluatedField ASC', + ); + expect( + const SortBy('name', ascending: false).toString(), + 'evaluatedField DESC', + ); + }); + + test('.fromJson', () { + expect( + SortBy.fromJson({'evaluatedField': 'name', 'ascending': true}), + const SortBy('name', ascending: true), + ); + expect( + SortBy.fromJson({'evaluatedField': 'name', 'ascending': false}), + const SortBy('name', ascending: false), + ); + }); + }); +} diff --git a/packages/brick_core/test/query/where_test.dart b/packages/brick_core/test/query/where_test.dart index 1b771272..a778861c 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'); @@ -99,7 +99,7 @@ void main() { }); 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, From 01f7f8eeea5b6e8400be05360ecf56df9d84eb66 Mon Sep 17 00:00:00 2001 From: Tim Shedor Date: Fri, 29 Nov 2024 13:00:54 -0800 Subject: [PATCH 2/5] support query v2 in supabase --- MIGRATING.md | 168 ++++++++++-------- docs/data/providers.md | 23 ++- docs/sqlite/query.md | 14 +- docs/supabase/query.md | 19 +- packages/brick_build/README.md | 61 ++++--- packages/brick_core/analysis_options.yaml | 20 +-- packages/brick_core/lib/query.dart | 3 +- packages/brick_core/lib/src/adapter.dart | 4 +- packages/brick_core/lib/src/model.dart | 3 + .../brick_core/lib/src/model_dictionary.dart | 6 + .../brick_core/lib/src/model_repository.dart | 19 +- packages/brick_core/lib/src/provider.dart | 8 +- packages/brick_core/lib/src/query/and_or.dart | 2 + .../brick_core/lib/src/query/limit_by.dart | 43 +++++ .../brick_core/lib/src/query/order_by.dart | 64 +++++++ packages/brick_core/lib/src/query/query.dart | 83 ++++++--- .../brick_core/lib/src/query/sort_by.dart | 40 ----- packages/brick_core/lib/src/query/where.dart | 101 ++++++++--- packages/brick_core/pubspec.yaml | 4 +- packages/brick_core/test/__mocks__.dart | 30 ++-- .../brick_core/test/query/limit_by_test.dart | 31 ++++ .../brick_core/test/query/order_by_test.dart | 58 ++++++ .../brick_core/test/query/sort_by_test.dart | 52 ------ .../annotations/offline_first_annotation.dart | 2 +- .../lib/src/mixins/get_first_mixin.dart | 8 +- .../lib/src/offline_first_repository.dart | 8 +- .../src/offline_first_json_generators.dart | 2 +- .../test_offline_first_where_rename.dart | 2 +- ...ffline_first_with_supabase_repository.dart | 2 +- ...e_first_with_supabase_repository_test.dart | 8 +- packages/brick_rest/README.md | 4 +- packages/brick_rest/lib/src/rest_request.dart | 2 +- packages/brick_rest/test/__mocks__.dart | 4 +- packages/brick_sqlite/README.md | 2 +- .../src/helpers/query_sql_transformer.dart | 2 +- .../brick_sqlite/lib/src/sqlite_provider.dart | 4 +- .../test/memory_cache_provider_test.dart | 2 +- .../test/query_sql_transformer_test.dart | 24 ++- .../test/sqlite_provider_test.dart | 5 +- packages/brick_supabase/README.md | 11 +- .../lib/src/query_supabase_transformer.dart | 48 +++-- .../lib/src/supabase_provider.dart | 4 - .../test/query_supabase_transformer_test.dart | 16 +- 43 files changed, 635 insertions(+), 381 deletions(-) create mode 100644 packages/brick_core/lib/src/query/limit_by.dart create mode 100644 packages/brick_core/lib/src/query/order_by.dart delete mode 100644 packages/brick_core/lib/src/query/sort_by.dart create mode 100644 packages/brick_core/test/query/limit_by_test.dart create mode 100644 packages/brick_core/test/query/order_by_test.dart delete mode 100644 packages/brick_core/test/query/sort_by_test.dart diff --git a/MIGRATING.md b/MIGRATING.md index ee8b70c9..0160906a 100644 --- a/MIGRATING.md +++ b/MIGRATING.md @@ -1,5 +1,21 @@ # Migrating Between Major Versions +## Migrating from Brick 3 to Brick 4 + +Brick 4 away from loosely-defined `Query` arguments in favor of standardized fields that can be easily deprecated and discovered by analysis. + +### Breaking Changes + +- `Query(providerArgs: {'limit':})` is now `Query(limit:)` +- `Query(providerArgs: {'offset':})` is now `Query(offset:)` +- `Query(providerArgs: {'orderBy':})` is now `Query(orderBy:)`. This is a more significant change than `limit` or `offset`. `orderBy` is now defined by a class that permits multiple commands. For example, `'orderBy': 'name ASC'` becomes `[OrderBy('name', ascending: true)]`. +- `Query(providerArgs: {'restRequest':})` is now `Query(forProviders: [RestProviderQuery(request:)])`. This is a similarly significant chang that allows providers to be detected by static analysis and reduces the need for manual documentation. + +### Improvements + +- `OrderBy` will support association ordering and multiple values +- `Query` is constructed with `const` + ## 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 +24,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 +128,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 +207,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/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/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..62197036 100644 --- a/docs/supabase/query.md +++ b/docs/supabase/query.md @@ -1,14 +1,19 @@ # `Query` Configuration +## `limit` + +Forwards to Supabase's `limit` [param](https://supabase.com/docs/reference/dart/limit) in Brick's `#get` action + +## `offset` + +Start from a specific offset, inclusive. + ## `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) | +| Name | Type | Description | +| -------------------------- | --------- | --------------------------------------------------------------------------------------------------- | --- | +| `'limitByReferencedTable'` | `String?` | Forwards to Supabase's `referencedTable` [property](https://supabase.com/docs/reference/dart/limit) | | +| `'orderByReferencedTable'` | `String?` | Forwards to Supabase's `referencedTable` [property](https://supabase.com/docs/reference/dart/order) | ?> 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`. 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_core/analysis_options.yaml b/packages/brick_core/analysis_options.yaml index 1d5855fb..5008bf6c 100644 --- a/packages/brick_core/analysis_options.yaml +++ b/packages/brick_core/analysis_options.yaml @@ -10,7 +10,7 @@ linter: # - always_specify_types - always_use_package_imports - annotate_overrides - - avoid_annotating_with_dynamic + # - avoid_annotating_with_dynamic - avoid_bool_literals_in_conditional_expressions # - avoid_catches_without_on_clauses # - avoid_catching_errors @@ -20,7 +20,7 @@ linter: - avoid_empty_else # - avoid_equals_and_hash_code_on_mutable_classes - avoid_escaping_inner_quotes - # - avoid_field_initializers_in_const_classes + - avoid_field_initializers_in_const_classes # - avoid_final_parameters - avoid_function_literals_in_foreach_calls - avoid_implementing_value_types @@ -30,8 +30,8 @@ linter: - avoid_null_checks_in_equality_operators # - avoid_positional_boolean_parameters - avoid_print - # - avoid_private_typedef_functions - # - avoid_redundant_argument_values + - avoid_private_typedef_functions + - avoid_redundant_argument_values - avoid_relative_lib_imports - avoid_renaming_method_parameters - avoid_return_types_on_setters @@ -43,7 +43,7 @@ linter: - avoid_slow_async_io - avoid_type_to_string - avoid_types_as_parameter_names - - avoid_types_on_closure_parameters + # - avoid_types_on_closure_parameters - avoid_unnecessary_containers - avoid_unused_constructor_parameters - avoid_void_async @@ -126,7 +126,7 @@ linter: - prefer_constructors_over_static_methods - prefer_contains # - prefer_double_quotes - # - prefer_expression_function_bodies + - prefer_expression_function_bodies - prefer_final_fields - prefer_final_in_for_each - prefer_final_locals @@ -145,7 +145,7 @@ linter: - prefer_is_not_empty - prefer_is_not_operator - prefer_iterable_whereType - # - prefer_mixin + - prefer_mixin - prefer_null_aware_method_calls - prefer_null_aware_operators # - prefer_relative_imports @@ -154,7 +154,7 @@ linter: - prefer_typing_uninitialized_variables - prefer_void_to_null - provide_deprecation_message - # - public_member_api_docs + - public_member_api_docs - recursive_getters - require_trailing_commas - secure_pubspec_urls @@ -168,7 +168,7 @@ linter: - test_types_in_equals - throw_in_finally - tighten_type_of_initializing_formals - # - type_annotate_public_apis + - type_annotate_public_apis - type_init_formals - type_literal_in_constant_pattern - unawaited_futures @@ -207,7 +207,7 @@ linter: - 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_key_in_widget_constructors - use_late_for_private_fields_and_variables - use_named_constants - use_raw_strings diff --git a/packages/brick_core/lib/query.dart b/packages/brick_core/lib/query.dart index 8ce27689..a01ac06b 100644 --- a/packages/brick_core/lib/query.dart +++ b/packages/brick_core/lib/query.dart @@ -1,4 +1,5 @@ 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/query.dart'; -export 'package:brick_core/src/query/sort_by.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 9fa09aa4..6315cb4f 100644 --- a/packages/brick_core/lib/src/adapter.dart +++ b/packages/brick_core/lib/src/adapter.dart @@ -1,8 +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 f67a1962..c29d1050 100644 --- a/packages/brick_core/lib/src/model.dart +++ b/packages/brick_core/lib/src/model.dart @@ -5,5 +5,8 @@ import 'package:brick_core/src/provider.dart'; /// 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 8566e6f1..9ed4c97a 100644 --- a/packages/brick_core/lib/src/model_dictionary.dart +++ b/packages/brick_core/lib/src/model_dictionary.dart @@ -13,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 cb20a800..1bad93e9 100644 --- a/packages/brick_core/lib/src/model_repository.dart +++ b/packages/brick_core/lib/src/model_repository.dart @@ -1,33 +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` +/// 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. 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 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 get({Query query}); /// Perform required setup work. For example, migrating a database, starting a queue, @@ -39,7 +48,6 @@ abstract class ModelRepository { /// /// 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 upsert(TModel model, {Query query}); } @@ -48,6 +56,7 @@ abstract class SingleProviderRepository implements ModelRe /// 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..bc00782b --- /dev/null +++ b/packages/brick_core/lib/src/query/limit_by.dart @@ -0,0 +1,43 @@ +import 'dart:convert'; + +import 'package:brick_core/src/model.dart'; +import 'package:brick_core/src/model_dictionary.dart'; +import 'package:brick_core/src/provider.dart'; + +/// Construct directions for a provider to limit its results. +class LimitBy { + /// The ceiling for how many results can be returned for a [model]. + final int amount; + + /// Some providers may support limiting based on a model retrieved by the query. + /// This [Model] should be accessible to the [Provider]'s [ModelDictionary]. + final Type model; + + /// Construct directions for a provider to limit its results. + const LimitBy( + this.amount, { + required this.model, + }); + + /// Construct a [LimitBy] from a JSON map. + factory LimitBy.fromJson(Map json) => LimitBy( + json['amount'], + model: json['model'], + ); + + /// Serialize to JSON + Map toJson() => { + 'amount': amount, + 'model': model, + }; + + @override + String toString() => jsonEncode(toJson()); + + @override + bool operator ==(Object other) => + identical(this, other) || other is LimitBy && amount == other.amount && model == other.model; + + @override + int get hashCode => amount.hashCode ^ model.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..ace51d75 --- /dev/null +++ b/packages/brick_core/lib/src/query/order_by.dart @@ -0,0 +1,64 @@ +import 'package:brick_core/src/model.dart'; +import 'package:brick_core/src/model_dictionary.dart'; +import 'package:brick_core/src/provider.dart'; +import 'package:meta/meta.dart'; + +/// Construct directions for a provider to sort its results. +@immutable +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; + + /// Some providers may support ordering based on a model retrieved by the query. + /// This [Model] should be accessible to the [Provider]'s [ModelDictionary]. + final Type? model; + + /// Construct directions for a provider to sort its results. + const OrderBy( + this.evaluatedField, { + this.ascending = true, + this.model, + }); + + /// Sort by [ascending] order (A-Z). + factory OrderBy.asc(String evaluatedField, {Type? model}) => + OrderBy(evaluatedField, model: model); + + /// Sort by descending order (Z-A). + factory OrderBy.desc(String evaluatedField, {Type? model}) => + OrderBy(evaluatedField, ascending: false, model: model); + + /// Construct an [OrderBy] from a JSON map. + factory OrderBy.fromJson(Map json) => OrderBy( + json['evaluatedField'], + ascending: json['ascending'], + model: json['model'], + ); + + /// Serialize to JSON + Map toJson() => { + 'ascending': ascending, + 'evaluatedField': evaluatedField, + if (model != null) 'model': model, + }; + + @override + String toString() => '$evaluatedField ${ascending ? 'ASC' : 'DESC'}'; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is OrderBy && + evaluatedField == other.evaluatedField && + ascending == other.ascending && + model == other.model; + + @override + int get hashCode => evaluatedField.hashCode ^ ascending.hashCode ^ model.hashCode; +} diff --git a/packages/brick_core/lib/src/query/query.dart b/packages/brick_core/lib/src/query/query.dart index 27dd1469..70120895 100644 --- a/packages/brick_core/lib/src/query/query.dart +++ b/packages/brick_core/lib/src/query/query.dart @@ -4,8 +4,9 @@ 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/sort_by.dart'; import 'package:brick_core/src/query/where.dart'; import 'package:collection/collection.dart' show ListEquality, MapEquality; @@ -24,21 +25,37 @@ class Query { 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, sortBy, or forProviders instead') + @Deprecated('Use limit, offset, limitBy, orderBy, or forProviders instead') final Map providerArgs; - final List sortBy; + /// 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]. @@ -62,26 +79,29 @@ class Query { /// will only return results where the ID is 1 **and** the name is Thomas. final List? where; + /// An interface to request data from a [Provider] or [ModelRepository]. const Query({ this.action, this.forProviders = const [], this.limit, + this.limitBy = const [], this.offset, - @Deprecated('Use limit, offset, sortBy, or forProviders instead.') this.providerArgs = const {}, - this.sortBy = const [], + @Deprecated('Use limit, offset, limitBy, orderBy, or forProviders instead.') + this.providerArgs = const {}, + this.orderBy = const [], this.where, }); - factory Query.fromJson(Map json) { - return Query( - action: json['action'] == null ? null : QueryAction.values[json['action']], - limit: json['limit'] as int?, - offset: json['offset'] as int?, - providerArgs: json['providerArgs'], - sortBy: json['sortBy']?.map(SortBy.fromJson).toList() ?? [], - where: json['where']?.map(WhereCondition.fromJson), - ); - } + /// 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: json['providerArgs'], + where: json['where']?.map(WhereCondition.fromJson), + ); /// Make a _very_ simple query with a single [Where] statement. /// For example `Query.where('id', 1)`. @@ -100,33 +120,36 @@ class Query { ); } + /// Reconstruct the [Query] with passed overrides Query copyWith({ QueryAction? action, int? limit, + List? limitBy, int? offset, + List? orderBy, Map? providerArgs, - List? sortBy, List? where, }) => Query( action: action ?? this.action, limit: limit ?? this.limit, + limitBy: limitBy ?? this.limitBy, offset: offset ?? this.offset, + orderBy: orderBy ?? this.orderBy, providerArgs: providerArgs ?? this.providerArgs, - sortBy: sortBy ?? this.sortBy, where: where ?? this.where, ); - Map toJson() { - return { - if (action != null) 'action': QueryAction.values.indexOf(action!), - if (limit != null) 'limit': limit, - if (offset != null) 'offset': offset, - 'providerArgs': providerArgs, - if (sortBy.isNotEmpty) 'sortBy': sortBy.map((s) => s.toJson()).toList(), - if (where != null) 'where': where!.map((w) => w.toJson()).toList(), - }; - } + /// Serialize to JSON + Map toJson() => { + if (action != null) 'action': QueryAction.values.indexOf(action!), + if (limit != null) 'limit': limit, + if (limitBy.isNotEmpty) 'limitBy': limitBy.map((l) => l.toJson()).toList(), + if (offset != null) 'offset': offset, + '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()); @@ -138,17 +161,19 @@ class Query { action == other.action && limit == other.limit && offset == other.offset && + _listEquality.equals(limitBy, other.limitBy) && + _listEquality.equals(orderBy, other.orderBy) && _mapEquality.equals(providerArgs, other.providerArgs) && - _listEquality.equals(sortBy, other.sortBy) && _listEquality.equals(where, other.where); @override int get hashCode => action.hashCode ^ limit.hashCode ^ + limitBy.hashCode ^ offset.hashCode ^ + orderBy.hashCode ^ providerArgs.hashCode ^ - sortBy.hashCode ^ where.hashCode; } diff --git a/packages/brick_core/lib/src/query/sort_by.dart b/packages/brick_core/lib/src/query/sort_by.dart deleted file mode 100644 index d917f80b..00000000 --- a/packages/brick_core/lib/src/query/sort_by.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'package:brick_core/core.dart'; -import 'package:brick_core/src/provider.dart'; - -class SortBy { - /// 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; - - /// Defaults to `true`. - final bool ascending; - - const SortBy(this.evaluatedField, {this.ascending = true}); - - factory SortBy.fromJson(Map json) { - return SortBy( - json['evaluatedField'], - ascending: json['ascending'], - ); - } - - Map toJson() { - return { - 'evaluatedField': evaluatedField, - 'ascending': ascending, - }; - } - - @override - String toString() => '$evaluatedField ${ascending ? 'ASC' : 'DESC'}'; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is SortBy && evaluatedField == other.evaluatedField && ascending == other.ascending; - - @override - int get hashCode => evaluatedField.hashCode ^ ascending.hashCode; -} diff --git a/packages/brick_core/lib/src/query/where.dart b/packages/brick_core/lib/src/query/where.dart index e9e3245d..d2c7cd8a 100644 --- a/packages/brick_core/lib/src/query/where.dart +++ b/packages/brick_core/lib/src/query/where.dart @@ -22,7 +22,7 @@ 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.exact]. /// It is the responsibility of the [Provider] to ignore or interpret the requested comparison. @@ -37,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( @@ -55,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()); @@ -90,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; @@ -97,15 +110,19 @@ 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 [ModelRepository] and not imported from /// this package as repositories may choose to extend or inhibit functionality. @@ -115,16 +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. [isRequired] defaults `true`. factory Where.exact(String evaluatedField, value, {bool isRequired = true}) => Where(evaluatedField, value: value, compare: Compare.exact, isRequired: isRequired); - Where isExactly(value) => + /// Convenience function to create a [Where] with [Compare.exact]. + Where isExactly(dynamic value) => Where(evaluatedField, value: value, compare: Compare.exact, isRequired: isRequired); - Where isBetween(value1, value2) { + /// 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( evaluatedField, @@ -134,33 +154,40 @@ class Where extends WhereCondition { ); } - Where contains(value) => + /// Convenience function to create a [Where] with [Compare.contains]. + Where contains(dynamic value) => Where(evaluatedField, value: value, compare: Compare.contains, isRequired: isRequired); - Where doesNotContain(value) => + /// Convenience function to create a [Where] with [Compare.doesNotContain]. + Where doesNotContain(dynamic value) => Where(evaluatedField, value: value, compare: Compare.doesNotContain, isRequired: isRequired); - Where isLessThan(value) => + /// Convenience function to create a [Where] with [Compare.lessThan]. + Where isLessThan(dynamic value) => Where(evaluatedField, value: value, compare: Compare.lessThan, isRequired: isRequired); - Where isLessThanOrEqualTo(value) => Where( + /// Convenience function to create a [Where] with [Compare.lessThanOrEqualTo]. + Where isLessThanOrEqualTo(dynamic value) => Where( evaluatedField, value: value, compare: Compare.lessThanOrEqualTo, isRequired: isRequired, ); - Where isGreaterThan(value) => + /// Convenience function to create a [Where] with [Compare.greaterThan]. + Where isGreaterThan(dynamic value) => Where(evaluatedField, value: value, compare: Compare.greaterThan, isRequired: isRequired); - Where isGreaterThanOrEqualTo(value) => Where( + /// Convenience function to create a [Where] with [Compare.greaterThanOrEqualTo]. + Where isGreaterThanOrEqualTo(dynamic value) => Where( evaluatedField, value: value, compare: Compare.greaterThanOrEqualTo, isRequired: isRequired, ); - Where isNot(value) => + /// Convenience function to create a [Where] with [Compare.notEqual]. + Where isNot(dynamic value) => Where(evaluatedField, value: value, compare: Compare.notEqual, isRequired: isRequired); /// Recursively find conditions that evaluate a specific field. A field is a member on a model, @@ -192,15 +219,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. /// @@ -209,7 +259,6 @@ class WherePhrase extends WhereCondition { final bool isRequired; @override - // ignore: overridden_fields final List conditions; /// A collection of conditions that are evaluated together. diff --git a/packages/brick_core/pubspec.yaml b/packages/brick_core/pubspec.yaml index c88435cd..3755e959 100644 --- a/packages/brick_core/pubspec.yaml +++ b/packages/brick_core/pubspec.yaml @@ -4,10 +4,10 @@ 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: 2.0.0 environment: - sdk: ">=2.18.0 <4.0.0" + sdk: ">=3.0.0 <4.0.0" dependencies: collection: ">=1.15.0 <2.0.0" diff --git a/packages/brick_core/test/__mocks__.dart b/packages/brick_core/test/__mocks__.dart index 699fe6de..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,25 +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}) { + 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 { 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..9d2afbcd --- /dev/null +++ b/packages/brick_core/test/query/limit_by_test.dart @@ -0,0 +1,31 @@ +import 'package:brick_core/src/query/limit_by.dart'; +import 'package:test/test.dart'; + +void main() { + group('LimitBy', () { + test('equality', () { + expect( + const LimitBy(2, model: num), + LimitBy.fromJson(const {'amount': 2, 'model': num}), + ); + }); + + test('#toJson', () { + expect( + const LimitBy(2, model: int).toJson(), + {'amount': 2, 'model': int}, + ); + expect( + const LimitBy(2, model: String).toJson(), + {'amount': 2, 'model': String}, + ); + }); + + test('.fromJson', () { + expect( + LimitBy.fromJson(const {'amount': 2, 'model': String}), + const LimitBy(2, model: String), + ); + }); + }); +} 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..ba3b5ac4 --- /dev/null +++ b/packages/brick_core/test/query/order_by_test.dart @@ -0,0 +1,58 @@ +import 'package:brick_core/src/query/order_by.dart'; +import 'package:test/test.dart'; + +void main() { + group('OrderBy', () { + test('equality', () { + expect( + const OrderBy('name'), + OrderBy.fromJson(const {'evaluatedField': 'name', 'ascending': true}), + ); + expect( + const OrderBy('name', ascending: false), + OrderBy.fromJson(const {'evaluatedField': 'name', 'ascending': false}), + ); + }); + + test('#toJson', () { + expect( + const OrderBy('name').toJson(), + {'evaluatedField': 'name', 'ascending': true}, + ); + expect( + const OrderBy('name', ascending: false).toJson(), + {'evaluatedField': 'name', 'ascending': false}, + ); + }); + + test('#toString', () { + expect( + const OrderBy('name').toString(), + 'name ASC', + ); + expect( + const OrderBy('name', ascending: false).toString(), + 'name DESC', + ); + }); + + test('.asc', () { + expect(OrderBy.asc('name'), const OrderBy('name')); + }); + + test('.desc', () { + expect(OrderBy.asc('name'), const OrderBy('name', ascending: false)); + }); + + 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), + ); + }); + }); +} diff --git a/packages/brick_core/test/query/sort_by_test.dart b/packages/brick_core/test/query/sort_by_test.dart deleted file mode 100644 index addced73..00000000 --- a/packages/brick_core/test/query/sort_by_test.dart +++ /dev/null @@ -1,52 +0,0 @@ -// ignore_for_file: deprecated_member_use_from_same_package - -import 'package:brick_core/src/query/sort_by.dart'; -import 'package:test/test.dart'; - -void main() { - group('SortBy', () { - test('equality', () { - expect( - const SortBy('name', ascending: true), - SortBy.fromJson({'evaluatedField': 'name', 'ascending': true}), - ); - expect( - const SortBy('name', ascending: false), - SortBy.fromJson({'evaluatedField': 'name', 'ascending': false}), - ); - }); - - test('#toJson', () { - expect( - const SortBy('name', ascending: true).toJson(), - {'evaluatedField': 'name', 'ascending': true}, - ); - expect( - const SortBy('name', ascending: false).toJson(), - {'evaluatedField': 'name', 'ascending': false}, - ); - }); - - test('#toString', () { - expect( - const SortBy('name', ascending: true).toString(), - 'evaluatedField ASC', - ); - expect( - const SortBy('name', ascending: false).toString(), - 'evaluatedField DESC', - ); - }); - - test('.fromJson', () { - expect( - SortBy.fromJson({'evaluatedField': 'name', 'ascending': true}), - const SortBy('name', ascending: true), - ); - expect( - SortBy.fromJson({'evaluatedField': 'name', 'ascending': false}), - const SortBy('name', ascending: false), - ); - }); - }); -} 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..a6672ffa 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 @@ -8,7 +8,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) /// ) /// ``` /// 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/offline_first_repository.dart b/packages/brick_offline_first/lib/src/offline_first_repository.dart index 743eed6c..2d5bf2d6 100644 --- a/packages/brick_offline_first/lib/src/offline_first_repository.dart +++ b/packages/brick_offline_first/lib/src/offline_first_repository.dart @@ -235,7 +235,7 @@ abstract class OfflineFirstRepository[]; @@ -259,7 +260,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( 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..ae7684ee 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 @@ -215,7 +215,7 @@ mixin OfflineFirstJsonDeserialize _$GraphqlConfigEndpointFromGraphql( someField: await repository! .getAssociation(Query( where: [Where.exact('name', data['name'])], - providerArgs: {'limit': 1})) + limit: 1)) .then((r) => r!.first)); } 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 2d390c0b..be2c5d85 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 @@ -185,7 +185,7 @@ abstract class OfflineFirstWithSupabaseRepository< return Query( where: fieldsWithValues.entries.map((entry) => Where.exact(entry.key, entry.value)).toList(), - providerArgs: {'limit': 1}, + limit: 1, ); } 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..a5c0f750 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 @@ -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', () { @@ -132,7 +132,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('empty payload', () { @@ -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', () { @@ -207,7 +207,7 @@ 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); }); }); 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/src/rest_request.dart b/packages/brick_rest/lib/src/rest_request.dart index cebd6735..ecc77ada 100644 --- a/packages/brick_rest/lib/src/rest_request.dart +++ b/packages/brick_rest/lib/src/rest_request.dart @@ -29,7 +29,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']}"; /// } /// diff --git a/packages/brick_rest/test/__mocks__.dart b/packages/brick_rest/test/__mocks__.dart index dc308cb8..73a95524 100644 --- a/packages/brick_rest/test/__mocks__.dart +++ b/packages/brick_rest/test/__mocks__.dart @@ -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'; } 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/src/helpers/query_sql_transformer.dart b/packages/brick_sqlite/lib/src/helpers/query_sql_transformer.dart index beb95461..38bcfa39 100644 --- a/packages/brick_sqlite/lib/src/helpers/query_sql_transformer.dart +++ b/packages/brick_sqlite/lib/src/helpers/query_sql_transformer.dart @@ -336,7 +336,7 @@ class AllOtherClausesFragment { /// 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`. + /// For example, `orderBy: [OrderBy.asc('createdAt')]` must become `ORDER BY created_at ASC`. static const _operatorsDeclaringFields = {'ORDER BY', 'GROUP BY', 'HAVING'}; AllOtherClausesFragment( diff --git a/packages/brick_sqlite/lib/src/sqlite_provider.dart b/packages/brick_sqlite/lib/src/sqlite_provider.dart index 299b03a4..d8eb2f6b 100644 --- a/packages/brick_sqlite/lib/src/sqlite_provider.dart +++ b/packages/brick_sqlite/lib/src/sqlite_provider.dart @@ -111,12 +111,12 @@ class SqliteProvider implements Provider> get({ diff --git a/packages/brick_sqlite/test/memory_cache_provider_test.dart b/packages/brick_sqlite/test/memory_cache_provider_test.dart index 38cd7c7f..595a5bf0 100644 --- a/packages/brick_sqlite/test/memory_cache_provider_test.dart +++ b/packages/brick_sqlite/test/memory_cache_provider_test.dart @@ -59,7 +59,7 @@ void main() { final results = provider.get( query: Query( where: [Where.exact(InsertTable.PRIMARY_KEY_FIELD, 1)], - providerArgs: {'limit': 1}, + limit: 1, ), ); expect(results, isNotEmpty); diff --git a/packages/brick_sqlite/test/query_sql_transformer_test.dart b/packages/brick_sqlite/test/query_sql_transformer_test.dart index 67e72e73..3f2a48ce 100644 --- a/packages/brick_sqlite/test/query_sql_transformer_test.dart +++ b/packages/brick_sqlite/test/query_sql_transformer_test.dart @@ -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: Query(limit: 1), ); await db.rawQuery(sqliteQuery.statement, sqliteQuery.values); @@ -404,10 +404,8 @@ void main() { final sqliteQuery = QuerySqlTransformer( modelDictionary: dictionary, query: Query( - providerArgs: { - 'limit': 1, - 'offset': 1, - }, + limit: 1, + offset: 1, ), ); await db.rawQuery(sqliteQuery.statement, sqliteQuery.values); @@ -421,7 +419,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: Query(orderBy: [OrderBy.desc('id')]), ); await db.rawQuery(sqliteQuery.statement, sqliteQuery.values); @@ -434,7 +432,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: Query(orderBy: [OrderBy.desc('lastName')]), ); await db.rawQuery(sqliteQuery.statement, sqliteQuery.values); @@ -447,7 +445,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: Query(orderBy: [OrderBy.desc('manyAssoc')]), ); await db.rawQuery(sqliteQuery.statement, sqliteQuery.values); @@ -461,9 +459,7 @@ void main() { final sqliteQuery = QuerySqlTransformer( modelDictionary: dictionary, query: Query( - providerArgs: { - 'orderBy': 'manyAssoc DESC, complexFieldName ASC', - }, + orderBy: [OrderBy.desc('manyAssoc'), OrderBy.asc('complexFieldName')], ), ); await db.rawQuery(sqliteQuery.statement, sqliteQuery.values); @@ -478,8 +474,8 @@ void main() { final sqliteQuery = QuerySqlTransformer( modelDictionary: dictionary, query: Query( + orderBy: [OrderBy.asc('complexFieldName')], providerArgs: { - 'orderBy': 'complexFieldName ASC', 'having': 'complexFieldName > 1000', 'groupBy': 'complexFieldName', }, @@ -497,7 +493,7 @@ void main() { 'SELECT DISTINCT `DemoModel`.* FROM `DemoModel` ORDER BY datetime(simple_time) DESC'; final sqliteQuery = QuerySqlTransformer( modelDictionary: dictionary, - query: Query(providerArgs: {'orderBy': 'simpleTime DESC'}), + query: Query(orderBy: [OrderBy.desc('simpleTime')]), ); await db.rawQuery(sqliteQuery.statement, sqliteQuery.values); @@ -514,7 +510,7 @@ void main() { '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: Query(orderBy: [OrderBy.desc('complex_field_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..d60070b8 100644 --- a/packages/brick_sqlite/test/sqlite_provider_test.dart +++ b/packages/brick_sqlite/test/sqlite_provider_test.dart @@ -118,7 +118,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 +134,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_supabase/README.md b/packages/brick_supabase/README.md index d270f3f4..fe95fa94 100644 --- a/packages/brick_supabase/README.md +++ b/packages/brick_supabase/README.md @@ -8,13 +8,10 @@ Connecting [Brick](https://github.com/GetDutchie/brick) with Supabase. ### `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) | +| Name | Type | Description | +| -------------------------- | --------- | --------------------------------------------------------------------------------------------------- | +| `'limitByReferencedTable'` | `String?` | Forwards to Supabase's `referencedTable` [property](https://supabase.com/docs/reference/dart/limit) | +| `'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`. diff --git a/packages/brick_supabase/lib/src/query_supabase_transformer.dart b/packages/brick_supabase/lib/src/query_supabase_transformer.dart index 2568d77f..643fa2c1 100644 --- a/packages/brick_supabase/lib/src/query_supabase_transformer.dart +++ b/packages/brick_supabase/lib/src/query_supabase_transformer.dart @@ -30,21 +30,18 @@ class QuerySupabaseTransformer<_Model extends SupabaseModel> { PostgrestTransformBuilder>> applyProviderArgs( PostgrestFilterBuilder>> builder, ) { - if (query?.providerArgs['orderBy'] != null) { + final orderBy = (query?.orderBy.isNotEmpty ?? false) || query?.providerArgs['orderBy'] != null; + if (orderBy) { builder = order(builder); } - if (query?.providerArgs['offset'] != null) { - final url = - builder.overrideSearchParams('offset', (query!.providerArgs['offset'] as int).toString()); + final offset = query?.offset ?? query?.providerArgs['offset'] as int?; + if (offset != null) { + final url = builder.overrideSearchParams('offset', (offset).toString()); builder = builder.copyWithUrl(url); } - if (query?.providerArgs['limit'] != null) { - return limit(builder); - } - - return builder; + return limit(builder); } PostgrestFilterBuilder>> select(SupabaseQueryBuilder builder) { @@ -197,15 +194,22 @@ class QuerySupabaseTransformer<_Model extends SupabaseModel> { PostgrestTransformBuilder>> limit( PostgrestFilterBuilder>> builder, ) { - if (query?.providerArgs['limit'] == null) return builder; + final limit0 = query?.limit ?? query?.providerArgs['limit'] as int?; + final hasLimit = limit0 != null || (query?.limitBy.isNotEmpty ?? false); + if (!hasLimit) return builder; - final limit = query!.providerArgs['limit'] as int; final referencedTable = query!.providerArgs['limitReferencedTable'] as String?; - final key = referencedTable == null ? 'limit' : '$referencedTable.limit'; + final url = builder.appendSearchParams(key, limit0.toString()); + final withProviderArgs = PostgrestTransformBuilder(builder.copyWithUrl(url)); - final url = builder.appendSearchParams(key, '$limit'); - return PostgrestTransformBuilder(builder.copyWithUrl(url)); + return query!.limitBy.fold(withProviderArgs, (builder, limitBy) { + final tableName = modelDictionary.adapterFor[limitBy.model]?.supabaseTableName; + if (tableName == null) return builder; + + final url = builder.appendSearchParams('$tableName.limit', limitBy.amount.toString()); + return PostgrestTransformBuilder(builder.copyWithUrl(url)); + }); } @protected @@ -213,7 +217,7 @@ class QuerySupabaseTransformer<_Model extends SupabaseModel> { 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'); @@ -223,7 +227,19 @@ class QuerySupabaseTransformer<_Model extends SupabaseModel> { final columnName = adapter.fieldsToSupabaseColumns[fieldName]!.columnName; final value = '$columnName.${ascending ? 'asc' : 'desc'}.nullslast'; final url = builder.overrideSearchParams(key, value); - return builder.copyWithUrl(url); + final withProviderArgs = builder.copyWithUrl(url); + + return query!.orderBy.fold(withProviderArgs, (builder, orderBy) { + final tableName = orderBy.model == null + ? null + : modelDictionary.adapterFor[orderBy.model]?.supabaseTableName; + + final url = builder.appendSearchParams( + tableName == null ? 'order' : '$tableName.order', + '${orderBy.evaluatedField}.${ascending ? 'asc' : 'desc'}.nullslast', + ); + return builder.copyWithUrl(url); + }); } static String _compareToSearchParam(Compare compare) { diff --git a/packages/brick_supabase/lib/src/supabase_provider.dart b/packages/brick_supabase/lib/src/supabase_provider.dart index c18066b3..ce355e43 100644 --- a/packages/brick_supabase/lib/src/supabase_provider.dart +++ b/packages/brick_supabase/lib/src/supabase_provider.dart @@ -53,11 +53,7 @@ class SupabaseProvider implements Provider { } /// [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 diff --git a/packages/brick_supabase/test/query_supabase_transformer_test.dart b/packages/brick_supabase/test/query_supabase_transformer_test.dart index 7ef5fd0d..baa77f77 100644 --- a/packages/brick_supabase/test/query_supabase_transformer_test.dart +++ b/packages/brick_supabase/test/query_supabase_transformer_test.dart @@ -157,7 +157,7 @@ void main() { group('#applyProviderArgs', () { test('orderBy', () { - final query = Query(providerArgs: {'orderBy': 'name asc'}); + final query = Query(orderBy: [OrderBy('name')]); final queryTransformer = _buildTransformer(query); final filterBuilder = queryTransformer.select(_supabaseClient.from(DemoAdapter().supabaseTableName)); @@ -170,7 +170,7 @@ void main() { }); test('orderBy with descending order', () { - final query = Query(providerArgs: {'orderBy': 'name desc'}); + final query = Query(orderBy: [OrderBy('name', ascending: false)]); final queryTransformer = _buildTransformer(query); final filterBuilder = queryTransformer.select(_supabaseClient.from(DemoAdapter().supabaseTableName)); @@ -184,7 +184,7 @@ void main() { test('orderBy with referenced table', () { final query = Query( - providerArgs: {'orderBy': 'name desc', 'orderByReferencedTable': 'foreign_tables'}, + orderBy: [OrderBy.desc('name', model: DemoAssociationModel)], ); final queryTransformer = _buildTransformer(query); final filterBuilder = @@ -193,12 +193,12 @@ void main() { expect( transformBuilder.query, - 'select=id,name,custom_age&foreign_tables.order=name.desc.nullslast', + 'select=id,name,custom_age&demo_associations.order=name.desc.nullslast', ); }); test('limit', () { - final query = Query(providerArgs: {'limit': 10}); + final query = Query(limit: 10); final queryTransformer = _buildTransformer(query); final filterBuilder = queryTransformer.select(_supabaseClient.from(DemoAdapter().supabaseTableName)); @@ -208,17 +208,17 @@ void main() { }); test('limit with referenced table', () { - final query = Query(providerArgs: {'limit': 10, 'limitReferencedTable': 'foreign_tables'}); + final query = Query(limitBy: [LimitBy(10, model: DemoAssociationModel)]); final queryTransformer = _buildTransformer(query); final filterBuilder = queryTransformer.select(_supabaseClient.from(DemoAdapter().supabaseTableName)); final transformBuilder = queryTransformer.applyProviderArgs(filterBuilder); - expect(transformBuilder.query, 'select=id,name,custom_age&foreign_tables.limit=10'); + expect(transformBuilder.query, 'select=id,name,custom_age&demo_associations.limit=10'); }); test('combined orderBy and limit', () { - final query = Query(providerArgs: {'orderBy': 'name desc', 'limit': 20}); + final query = Query(limit: 20, orderBy: [OrderBy('name', ascending: false)]); final queryTransformer = _buildTransformer(query); final filterBuilder = queryTransformer.select(_supabaseClient.from(DemoAdapter().supabaseTableName)); From b203afc05f4943ecaa6c9b9801f56da917304ee9 Mon Sep 17 00:00:00 2001 From: Tim Shedor Date: Sat, 14 Dec 2024 12:21:26 -0800 Subject: [PATCH 3/5] eng: support forProviders in RestProvider --- packages/brick_core/lib/query.dart | 1 + .../lib/src/query/provider_query.dart | 12 ++ packages/brick_core/lib/src/query/query.dart | 1 + packages/brick_rest/lib/brick_rest.dart | 1 + .../brick_rest/lib/src/rest_provider.dart | 43 +++--- .../lib/src/rest_provider_query.dart | 18 +++ .../test/rest_provider_query_test.dart | 142 ++++++++++++++++++ .../brick_rest/test/rest_provider_test.dart | 27 ++++ 8 files changed, 228 insertions(+), 17 deletions(-) create mode 100644 packages/brick_rest/lib/src/rest_provider_query.dart create mode 100644 packages/brick_rest/test/rest_provider_query_test.dart diff --git a/packages/brick_core/lib/query.dart b/packages/brick_core/lib/query.dart index a01ac06b..6a19689e 100644 --- a/packages/brick_core/lib/query.dart +++ b/packages/brick_core/lib/query.dart @@ -1,5 +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/query/provider_query.dart b/packages/brick_core/lib/src/query/provider_query.dart index 6890b708..c31181a9 100644 --- a/packages/brick_core/lib/src/query/provider_query.dart +++ b/packages/brick_core/lib/src/query/provider_query.dart @@ -10,4 +10,16 @@ import 'package:brick_core/src/provider.dart'; 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 70120895..de91a9c5 100644 --- a/packages/brick_core/lib/src/query/query.dart +++ b/packages/brick_core/lib/src/query/query.dart @@ -147,6 +147,7 @@ class Query { if (limitBy.isNotEmpty) 'limitBy': limitBy.map((l) => l.toJson()).toList(), if (offset != null) 'offset': offset, 'providerArgs': providerArgs, + if (forProviders.isNotEmpty) 'forProviders': forProviders.map((p) => p.toJson()).toList(), if (orderBy.isNotEmpty) 'orderBy': orderBy.map((s) => s.toJson()).toList(), if (where != null) 'where': where!.map((w) => w.toJson()).toList(), }; 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/src/rest_provider.dart b/packages/brick_rest/lib/src/rest_provider.dart index 0dc799cb..3b8a5d4d 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'; @@ -40,7 +43,9 @@ class RestProvider implements Provider { 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; @@ -63,7 +68,9 @@ class RestProvider implements Provider { Future exists({query, 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 +83,13 @@ 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 { 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 []; @@ -114,15 +119,15 @@ 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 { 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 +161,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 +173,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. @@ -195,8 +202,10 @@ class RestProvider implements Provider { }) async { final combinedBody = body ?? {}; final url = Uri.parse([baseEndpoint, request.url!].join('')); - final method = - (query?.providerArgs ?? {})['request']?.method ?? request.method ?? operation.httpMethod; + 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 +217,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,7 +238,7 @@ 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', ); } } 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..109d166d --- /dev/null +++ b/packages/brick_rest/lib/src/rest_provider_query.dart @@ -0,0 +1,18 @@ +import 'package:brick_core/query.dart'; +import 'package:brick_rest/src/rest_provider.dart'; +import 'package:brick_rest/src/rest_request.dart'; + +class RestProviderQuery extends ProviderQuery { + final RestRequest? request; + + const RestProviderQuery({ + this.request, + }); + + @override + Map toJson() { + return { + if (request != null) 'request': request?.toJson(), + }; + } +} 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..d4a2a522 --- /dev/null +++ b/packages/brick_rest/test/rest_provider_query_test.dart @@ -0,0 +1,142 @@ +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}) { + return 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'); + final 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'); + final 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'); + final 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'); + final 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"}}', + ); + + final 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'); + final 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..32a16a46 100644 --- a/packages/brick_rest/test/rest_provider_test.dart +++ b/packages/brick_rest/test/rest_provider_test.dart @@ -75,6 +75,33 @@ 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'); + final 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'); From b6f83d2f62663570035e182259bb9075896f6c3d Mon Sep 17 00:00:00 2001 From: Tim Shedor Date: Sat, 14 Dec 2024 13:30:23 -0800 Subject: [PATCH 4/5] update rest analysis --- .../brick_core/test/query/query_test.dart | 276 ++++++++------- packages/brick_rest/analysis_options.yaml | 325 ++++++++++++++++++ packages/brick_rest/lib/gzip_http_client.dart | 2 + packages/brick_rest/lib/rest_exception.dart | 4 + .../brick_rest/lib/src/annotations/rest.dart | 2 +- packages/brick_rest/lib/src/rest_adapter.dart | 4 +- packages/brick_rest/lib/src/rest_model.dart | 2 +- .../lib/src/rest_model_dictionary.dart | 1 + .../brick_rest/lib/src/rest_provider.dart | 31 +- .../lib/src/rest_provider_query.dart | 19 +- packages/brick_rest/lib/src/rest_request.dart | 57 ++- .../lib/src/rest_request_transformer.dart | 15 + packages/brick_rest/test/__mocks__.dart | 35 +- .../test/rest_provider_query_test.dart | 25 +- .../brick_rest/test/rest_provider_test.dart | 37 +- 15 files changed, 616 insertions(+), 219 deletions(-) diff --git a/packages/brick_core/test/query/query_test.dart b/packages/brick_core/test/query/query_test.dart index 8c1fdb2a..80ade070 100644 --- a/packages/brick_core/test/query/query_test.dart +++ b/packages/brick_core/test/query/query_test.dart @@ -6,178 +6,174 @@ import 'package:test/test.dart'; void main() { group('Query', () { - group('properties', () { - test('#action', () { - const q = Query(action: QueryAction.delete); - expect(q.action, QueryAction.delete); - }); + test('#action', () { + const q = Query(action: QueryAction.delete); + expect(q.action, QueryAction.delete); + }); - group('#providerArgs', () { - test('#providerArgs.page and #providerArgs.sort', () { - const q = Query(providerArgs: {'page': 1, 'sort': 'by_user_asc'}); + group('#providerArgs', () { + test('#providerArgs.page and #providerArgs.sort', () { + const q = Query(providerArgs: {'page': 1, 'sort': 'by_user_asc'}); - expect(q.providerArgs['page'], 1); - expect(q.providerArgs['sort'], 'by_user_asc'); - }); + expect(q.providerArgs['page'], 1); + expect(q.providerArgs['sort'], 'by_user_asc'); + }); + }); - test('#providerArgs.limit', () { - const q0 = Query(limit: 0); - expect(q0.limit, 0); + test('#limit', () { + const q0 = Query(limit: 0); + expect(q0.limit, 0); - const q10 = Query(limit: 10); - expect(q10.limit, 10); + const q10 = Query(limit: 10); + expect(q10.limit, 10); - const q18 = Query(limit: 18); - expect(q18.limit, 18); - }); + const q18 = Query(limit: 18); + expect(q18.limit, 18); + }); - test('#providerArgs.offset', () { - const q0 = Query(limit: 10, offset: 0); - expect(q0.offset, 0); + test('#offset', () { + const q0 = Query(limit: 10, offset: 0); + expect(q0.offset, 0); - const q10 = Query(limit: 10, offset: 10); - expect(q10.offset, 10); + const q10 = Query(limit: 10, offset: 10); + expect(q10.offset, 10); - const q18 = Query(limit: 10, offset: 18); - expect(q18.offset, 18); - }); - }); + const q18 = Query(limit: 10, offset: 18); + expect(q18.offset, 18); + }); - test('#where', () { - const q = Query( - where: [ - Where('name', value: 'Thomas'), - ], - ); + test('#where', () { + const q = Query( + where: [ + Where('name', value: 'Thomas'), + ], + ); - expect(q.where!.first.evaluatedField, 'name'); - expect(q.where!.first.value, 'Thomas'); - }); + expect(q.where!.first.evaluatedField, 'name'); + expect(q.where!.first.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(q1, q2); - }); + 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, + ); - 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 are the same', () { + const q1 = Query(providerArgs: {'name': 'Guy'}); + const q2 = Query(providerArgs: {'name': 'Guy'}); - test('providerArgs have different values', () { - const q1 = Query(providerArgs: {'name': 'Thomas'}); - const q2 = Query(providerArgs: {'name': 'Guy'}); + expect(q1, q2); + }); - expect(q1, isNot(q2)); - }); + test('providerArgs have different values', () { + const q1 = Query(providerArgs: {'name': 'Thomas'}); + const 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 have different keys', () { + const q1 = Query(providerArgs: {'email': 'guy@guy.com'}); + const q2 = Query(providerArgs: {'name': 'Guy'}); - test('providerArgs are null', () { - const q1 = Query(); - const q2 = Query(providerArgs: {'name': 'Guy'}); - expect(q1, isNot(q2)); + expect(q1, isNot(q2)); + }); - const q3 = Query(); - expect(q1, q3); - }); + test('providerArgs are null', () { + const q1 = Query(); + const q2 = Query(providerArgs: {'name': 'Guy'}); + expect(q1, isNot(q2)); + + const q3 = Query(); + expect(q1, q3); }); + }); - 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); - }); + 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', () { - const q1 = Query(action: QueryAction.insert); - final q2 = q1.copyWith(limit: 20); + test('appends', () { + const q1 = Query(action: QueryAction.insert); + final q2 = q1.copyWith(limit: 20); - expect(q1.limit, null); - expect(q2.action, QueryAction.insert); - expect(q2.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', () { - const source = Query( + test('.fromJson', () { + final json = { + 'action': 2, + 'limit': 3, + 'offset': 3, + }; + + final result = Query.fromJson(json); + expect( + result, + const Query( action: QueryAction.update, limit: 3, offset: 3, - ); + ), + ); + }); - expect( - source.toJson(), - { - 'action': 2, - '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, - 'limit': 3, - 'offset': 3, - }; - - final result = Query.fromJson(json); - expect( - result, - const Query( - action: QueryAction.update, - 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); - }); - - 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); - }); - }); + 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_rest/analysis_options.yaml b/packages/brick_rest/analysis_options.yaml index f04c6cf0..5008bf6c 100644 --- a/packages/brick_rest/analysis_options.yaml +++ b/packages/brick_rest/analysis_options.yaml @@ -1 +1,326 @@ include: ../../analysis_options.yaml + +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 + - valid_regexps + - void_checks + +analyzer: + exclude: + - example/ + - "**/example/" + - example_rest + - example_graphql + - "**/*.g.dart" + + errors: + # override custom + always_use_package_imports: error + camel_case_extensions: error + camel_case_types: error + curly_braces_in_flow_control_structures: error + directives_ordering: error + file_names: error + prefer_single_quotes: error + prefer_is_empty: error + prefer_is_not_empty: error + require_trailing_commas: error + sort_pub_dependencies: error + unnecessary_statements: error + + # override flutter_lints + avoid_print: error + avoid_unnecessary_containers: error + avoid_web_libraries_in_flutter: error + no_logic_in_create_state: error + prefer_const_constructors: error + prefer_const_constructors_in_immutables: error + prefer_const_declarations: error + prefer_const_literals_to_create_immutables: error + sized_box_for_whitespace: error + sort_child_properties_last: error + use_build_context_synchronously: error + use_full_hex_values_for_flutter_colors: error + use_key_in_widget_constructors: error + + # override recommended lints + always_require_non_null_named_parameters: error + annotate_overrides: error + avoid_function_literals_in_foreach_calls: error + avoid_init_to_null: error + avoid_null_checks_in_equality_operators: error + avoid_renaming_method_parameters: error + avoid_return_types_on_setters: error + avoid_returning_null_for_void: error + avoid_single_cascade_in_expression_statements: error + await_only_futures: error + constant_identifier_names: error + control_flow_in_finally: error + depend_on_referenced_packages: ignore + empty_constructor_bodies: error + empty_statements: error + exhaustive_cases: error + implementation_imports: ignore + invalid_case_patterns: error + library_names: error + library_prefixes: error + library_private_types_in_public_api: error + matching_super_parameters: error + no_leading_underscores_for_library_prefixes: error + no_leading_underscores_for_local_identifiers: error + no_literal_bool_comparisons: error + null_check_on_nullable_type_parameter: error + null_closures: error + overridden_fields: error + package_names: error + prefer_adjacent_string_concatenation: error + prefer_collection_literals: error + prefer_conditional_assignment: error + prefer_contains: error + prefer_equal_for_default_values: error + prefer_final_fields: error + prefer_for_elements_to_map_fromIterable: error + prefer_function_declarations_over_variables: error + prefer_if_null_operators: error + prefer_initializing_formals: error + prefer_inlined_adds: error + prefer_interpolation_to_compose_strings: error + prefer_is_not_operator: error + prefer_null_aware_operators: error + prefer_spread_collections: error + prefer_void_to_null: error + recursive_getters: error + slash_for_doc_comments: error + type_init_formals: error + type_literal_in_constant_pattern: error + unnecessary_brace_in_string_interps: error + unnecessary_breaks: error + unnecessary_const: error + unnecessary_constructor_name: error + unnecessary_getters_setters: error + unnecessary_late: error + unnecessary_new: error + unnecessary_null_aware_assignments: error + unnecessary_null_in_if_null_operators: error + unnecessary_nullable_for_final_variable_declarations: error + unnecessary_string_escapes: error + unnecessary_string_interpolations: error + unnecessary_this: error + use_function_type_syntax_for_parameters: error + use_rethrow_when_possible: error 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 3b8a5d4d..b349949f 100644 --- a/packages/brick_rest/lib/src/rest_provider.dart +++ b/packages/brick_rest/lib/src/rest_provider.dart @@ -27,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, @@ -39,7 +41,11 @@ 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; @@ -65,7 +71,10 @@ 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?.providerQueries[RestProvider] as RestProviderQuery?)?.request ?? @@ -84,7 +93,10 @@ class RestProvider implements Provider { } @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?.providerQueries[RestProvider] as RestProviderQuery?)?.request ?? @@ -106,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>(); @@ -120,7 +130,11 @@ class RestProvider implements Provider { } @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 = @@ -201,7 +215,7 @@ class RestProvider implements Provider { Map? body, }) async { final combinedBody = body ?? {}; - final url = Uri.parse([baseEndpoint, request.url!].join('')); + final url = Uri.parse([baseEndpoint, request.url!].join()); final requestFromQuery = (query?.providerQueries[RestProvider] as RestProviderQuery?)?.request ?? (query?.providerArgs['request'] as RestRequest?); @@ -243,6 +257,7 @@ class RestProvider implements Provider { } } + /// 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 index 109d166d..428e5706 100644 --- a/packages/brick_rest/lib/src/rest_provider_query.dart +++ b/packages/brick_rest/lib/src/rest_provider_query.dart @@ -2,17 +2,26 @@ 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, }); @override - Map toJson() { - return { - if (request != null) 'request': request?.toJson(), - }; - } + 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 ecc77ada..2f098b64 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) @@ -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,40 @@ 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, - }; - } + /// 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/test/__mocks__.dart b/packages/brick_rest/test/__mocks__.dart index 73a95524..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 { @@ -48,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 @@ -66,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 index d4a2a522..4fe78a03 100644 --- a/packages/brick_rest/test/rest_provider_query_test.dart +++ b/packages/brick_rest/test/rest_provider_query_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('RestProviderQuery', () { @@ -30,7 +29,7 @@ void main() { ); final instance = DemoRestModel('Guy'); - final query = Query( + const query = Query( forProviders: [ RestProviderQuery( request: RestRequest(headers: {'Authorization': 'Basic xyz'}, url: '/'), @@ -47,7 +46,7 @@ void main() { final provider = generateProvider('{"name": "Guy"}', requestMethod: 'PUT'); final instance = DemoRestModel('Guy'); - final query = + const query = Query(forProviders: [RestProviderQuery(request: RestRequest(method: 'PUT', url: '/'))]); final resp = await provider.upsert(instance, query: query); @@ -59,7 +58,7 @@ void main() { final provider = generateProvider('{"name": "Guy"}', requestMethod: 'PATCH'); final instance = DemoRestModel('Guy'); - final query = Query( + const query = Query( forProviders: [RestProviderQuery(request: RestRequest(method: 'PATCH', url: '/'))], ); final resp = await provider.upsert(instance, query: query); @@ -76,7 +75,7 @@ void main() { ); final instance = DemoRestModel('Guy'); - final query = Query( + const query = Query( forProviders: [RestProviderQuery(request: RestRequest(topLevelKey: 'top', url: '/'))], ); final resp = await provider.upsert(instance, query: query); @@ -93,7 +92,7 @@ void main() { requestBody: '{"other_name":{"first_name":"Thomas"}}', ); - final query = Query( + const query = Query( forProviders: [ RestProviderQuery( request: RestRequest( @@ -119,7 +118,7 @@ void main() { ); final instance = DemoRestModel('Guy'); - final query = Query( + const query = Query( forProviders: [ RestProviderQuery( request: RestRequest( diff --git a/packages/brick_rest/test/rest_provider_test.dart b/packages/brick_rest/test/rest_provider_test.dart index 32a16a46..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: '/'), }, @@ -89,7 +87,7 @@ void main() { ); final instance = DemoRestModel('Guy'); - final query = Query( + const query = Query( forProviders: [ RestProviderQuery( request: RestRequest(headers: {'Authorization': 'Basic xyz'}, url: '/'), @@ -106,7 +104,8 @@ void main() { 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); @@ -117,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); @@ -132,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); @@ -147,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: '/', @@ -171,7 +173,8 @@ void main() { ); final instance = DemoRestModel('Guy'); - final query = Query( + const query = Query( + // ignore: deprecated_member_use providerArgs: { 'request': RestRequest( topLevelKey: 'top', From 92822bfc608a5a622be1c205d3da7e39f3de6fe1 Mon Sep 17 00:00:00 2001 From: Tim Shedor Date: Sat, 14 Dec 2024 14:49:15 -0800 Subject: [PATCH 5/5] update rest build analysis --- .../analysis_options.yaml | 325 ++++++++++++++++++ .../lib/rest_model_serdes_generator.dart | 4 +- .../lib/src/rest_deserialize.dart | 1 + .../lib/src/rest_fields.dart | 7 +- .../lib/src/rest_serdes_generator.dart | 2 + .../lib/src/rest_serializable_extended.dart | 2 + .../lib/src/rest_serialize.dart | 1 + packages/brick_rest_generators/pubspec.yaml | 4 +- ...est_constructor_member_field_mismatch.dart | 2 +- .../test_enum_as_string.dart | 2 +- .../test_ignore_from_to.dart | 2 +- ...t_unserializable_field_with_generator.dart | 2 +- .../rest_model_serdes_generator_test.dart | 2 +- 13 files changed, 347 insertions(+), 9 deletions(-) diff --git a/packages/brick_rest_generators/analysis_options.yaml b/packages/brick_rest_generators/analysis_options.yaml index f04c6cf0..5008bf6c 100644 --- a/packages/brick_rest_generators/analysis_options.yaml +++ b/packages/brick_rest_generators/analysis_options.yaml @@ -1 +1,326 @@ include: ../../analysis_options.yaml + +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 + - valid_regexps + - void_checks + +analyzer: + exclude: + - example/ + - "**/example/" + - example_rest + - example_graphql + - "**/*.g.dart" + + errors: + # override custom + always_use_package_imports: error + camel_case_extensions: error + camel_case_types: error + curly_braces_in_flow_control_structures: error + directives_ordering: error + file_names: error + prefer_single_quotes: error + prefer_is_empty: error + prefer_is_not_empty: error + require_trailing_commas: error + sort_pub_dependencies: error + unnecessary_statements: error + + # override flutter_lints + avoid_print: error + avoid_unnecessary_containers: error + avoid_web_libraries_in_flutter: error + no_logic_in_create_state: error + prefer_const_constructors: error + prefer_const_constructors_in_immutables: error + prefer_const_declarations: error + prefer_const_literals_to_create_immutables: error + sized_box_for_whitespace: error + sort_child_properties_last: error + use_build_context_synchronously: error + use_full_hex_values_for_flutter_colors: error + use_key_in_widget_constructors: error + + # override recommended lints + always_require_non_null_named_parameters: error + annotate_overrides: error + avoid_function_literals_in_foreach_calls: error + avoid_init_to_null: error + avoid_null_checks_in_equality_operators: error + avoid_renaming_method_parameters: error + avoid_return_types_on_setters: error + avoid_returning_null_for_void: error + avoid_single_cascade_in_expression_statements: error + await_only_futures: error + constant_identifier_names: error + control_flow_in_finally: error + depend_on_referenced_packages: ignore + empty_constructor_bodies: error + empty_statements: error + exhaustive_cases: error + implementation_imports: ignore + invalid_case_patterns: error + library_names: error + library_prefixes: error + library_private_types_in_public_api: error + matching_super_parameters: error + no_leading_underscores_for_library_prefixes: error + no_leading_underscores_for_local_identifiers: error + no_literal_bool_comparisons: error + null_check_on_nullable_type_parameter: error + null_closures: error + overridden_fields: error + package_names: error + prefer_adjacent_string_concatenation: error + prefer_collection_literals: error + prefer_conditional_assignment: error + prefer_contains: error + prefer_equal_for_default_values: error + prefer_final_fields: error + prefer_for_elements_to_map_fromIterable: error + prefer_function_declarations_over_variables: error + prefer_if_null_operators: error + prefer_initializing_formals: error + prefer_inlined_adds: error + prefer_interpolation_to_compose_strings: error + prefer_is_not_operator: error + prefer_null_aware_operators: error + prefer_spread_collections: error + prefer_void_to_null: error + recursive_getters: error + slash_for_doc_comments: error + type_init_formals: error + type_literal_in_constant_pattern: error + unnecessary_brace_in_string_interps: error + unnecessary_breaks: error + unnecessary_const: error + unnecessary_constructor_name: error + unnecessary_getters_setters: error + unnecessary_late: error + unnecessary_new: error + unnecessary_null_aware_assignments: error + unnecessary_null_in_if_null_operators: error + unnecessary_nullable_for_final_variable_declarations: error + unnecessary_string_escapes: error + unnecessary_string_interpolations: error + unnecessary_this: error + use_function_type_syntax_for_parameters: error + use_rethrow_when_possible: error 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..1379cf74 100644 --- a/packages/brick_rest_generators/pubspec.yaml +++ b/packages/brick_rest_generators/pubspec.yaml @@ -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() {