praxis 2.0.pre.8 → 2.0.pre.13
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +2 -0
- data/.ruby-version +1 -1
- data/.travis.yml +1 -3
- data/CHANGELOG.md +33 -0
- data/TODO.md +1 -4
- data/bin/praxis +67 -12
- data/lib/praxis.rb +10 -3
- data/lib/praxis/action_definition.rb +15 -13
- data/lib/praxis/action_definition/headers_dsl_compiler.rb +0 -7
- data/lib/praxis/api_general_info.rb +1 -1
- data/lib/praxis/application.rb +6 -2
- data/lib/praxis/blueprint.rb +357 -0
- data/lib/praxis/bootloader.rb +9 -3
- data/lib/praxis/bootloader_stages/environment.rb +16 -13
- data/lib/praxis/collection.rb +1 -11
- data/lib/praxis/config_hash.rb +44 -0
- data/lib/praxis/docs/{openapi → open_api}/info_object.rb +18 -10
- data/lib/praxis/docs/{openapi → open_api}/media_type_object.rb +0 -0
- data/lib/praxis/docs/{openapi → open_api}/operation_object.rb +0 -0
- data/lib/praxis/docs/{openapi → open_api}/parameter_object.rb +0 -0
- data/lib/praxis/docs/{openapi → open_api}/paths_object.rb +0 -0
- data/lib/praxis/docs/{openapi → open_api}/request_body_object.rb +0 -0
- data/lib/praxis/docs/{openapi → open_api}/response_object.rb +0 -0
- data/lib/praxis/docs/{openapi → open_api}/responses_object.rb +0 -0
- data/lib/praxis/docs/{openapi → open_api}/schema_object.rb +0 -0
- data/lib/praxis/docs/{openapi → open_api}/server_object.rb +0 -0
- data/lib/praxis/docs/{openapi → open_api}/tag_object.rb +0 -0
- data/lib/praxis/docs/open_api_generator.rb +91 -6
- data/lib/praxis/endpoint_definition.rb +273 -0
- data/lib/praxis/extensions/attribute_filtering/active_record_filter_query_builder.rb +182 -58
- data/lib/praxis/extensions/attribute_filtering/filter_tree_node.rb +3 -2
- data/lib/praxis/extensions/attribute_filtering/filtering_params.rb +47 -56
- data/lib/praxis/extensions/attribute_filtering/filters_parser.rb +153 -0
- data/lib/praxis/extensions/attribute_filtering/sequel_filter_query_builder.rb +20 -8
- data/lib/praxis/extensions/field_expansion.rb +3 -36
- data/lib/praxis/extensions/pagination.rb +5 -32
- data/lib/praxis/extensions/pagination/ordering_params.rb +1 -1
- data/lib/praxis/extensions/pagination/pagination_params.rb +6 -4
- data/lib/praxis/field_expander.rb +90 -0
- data/lib/praxis/finalizable.rb +34 -0
- data/lib/praxis/mapper/active_model_compat.rb +4 -0
- data/lib/praxis/mapper/resource.rb +18 -2
- data/lib/praxis/mapper/selector_generator.rb +2 -1
- data/lib/praxis/mapper/sequel_compat.rb +7 -0
- data/lib/praxis/media_type.rb +3 -68
- data/lib/praxis/plugin_concern.rb +1 -1
- data/lib/praxis/plugins/mapper_plugin.rb +24 -15
- data/lib/praxis/plugins/pagination_plugin.rb +34 -4
- data/lib/praxis/renderer.rb +88 -0
- data/lib/praxis/request.rb +1 -1
- data/lib/praxis/resource_definition.rb +2 -311
- data/lib/praxis/response_definition.rb +2 -10
- data/lib/praxis/response_template.rb +3 -3
- data/lib/praxis/router.rb +2 -2
- data/lib/praxis/routing_config.rb +1 -1
- data/lib/praxis/tasks/api_docs.rb +17 -64
- data/lib/praxis/tasks/routes.rb +1 -1
- data/lib/praxis/types/media_type_common.rb +1 -11
- data/lib/praxis/version.rb +1 -1
- data/praxis.gemspec +0 -1
- data/spec/functional_spec.rb +5 -9
- data/spec/praxis/action_definition_spec.rb +12 -20
- data/spec/praxis/blueprint_spec.rb +373 -0
- data/spec/praxis/bootloader_spec.rb +10 -2
- data/spec/praxis/collection_spec.rb +0 -13
- data/spec/praxis/config_hash_spec.rb +64 -0
- data/spec/praxis/{resource_definition_spec.rb → endpoint_definition_spec.rb} +37 -64
- data/spec/praxis/extensions/attribute_filtering/active_record_filter_query_builder_spec.rb +249 -168
- data/spec/praxis/extensions/attribute_filtering/filter_tree_node_spec.rb +25 -6
- data/spec/praxis/extensions/attribute_filtering/filtering_params_spec.rb +190 -8
- data/spec/praxis/extensions/attribute_filtering/filters_parser_spec.rb +140 -0
- data/spec/praxis/extensions/field_expansion_spec.rb +5 -24
- data/spec/praxis/extensions/field_selection/active_record_query_selector_spec.rb +1 -1
- data/spec/praxis/extensions/field_selection/sequel_query_selector_spec.rb +1 -1
- data/spec/praxis/extensions/support/spec_resources_active_model.rb +1 -1
- data/spec/praxis/field_expander_spec.rb +149 -0
- data/spec/praxis/mapper/selector_generator_spec.rb +1 -1
- data/spec/praxis/media_type_identifier_spec.rb +5 -4
- data/spec/praxis/media_type_spec.rb +4 -93
- data/spec/praxis/renderer_spec.rb +188 -0
- data/spec/praxis/response_definition_spec.rb +0 -31
- data/spec/praxis/response_spec.rb +1 -1
- data/spec/praxis/router_spec.rb +8 -8
- data/spec/praxis/routing_config_spec.rb +3 -3
- data/spec/spec_app/app/controllers/instances.rb +13 -7
- data/spec/spec_app/design/media_types/instance.rb +1 -19
- data/spec/spec_app/design/media_types/volume.rb +1 -1
- data/spec/spec_app/design/media_types/volume_snapshot.rb +2 -14
- data/spec/spec_app/design/resources/instances.rb +5 -8
- data/spec/spec_app/design/resources/volume_snapshots.rb +1 -1
- data/spec/spec_app/design/resources/volumes.rb +1 -1
- data/spec/support/spec_authorization_plugin.rb +1 -1
- data/spec/support/spec_blueprints.rb +72 -0
- data/spec/support/{spec_resource_definitions.rb → spec_endpoint_definitions.rb} +2 -2
- data/spec/support/spec_media_types.rb +6 -26
- data/tasks/thor/app.rb +8 -34
- data/tasks/thor/example.rb +51 -285
- data/tasks/thor/model.rb +40 -0
- data/tasks/thor/scaffold.rb +117 -0
- data/tasks/thor/templates/generator/empty_app/.gitignore +0 -1
- data/tasks/thor/templates/generator/empty_app/Gemfile +7 -23
- data/tasks/thor/templates/generator/empty_app/README.md +1 -1
- data/tasks/thor/templates/generator/empty_app/Rakefile +4 -13
- data/tasks/thor/templates/generator/empty_app/{design/response_templates → app/v1/resources}/.empty_directory +0 -0
- data/tasks/thor/templates/generator/empty_app/{design/response_templates → app/v1/resources}/.gitkeep +0 -0
- data/tasks/thor/templates/generator/empty_app/config/environment.rb +26 -17
- data/tasks/thor/templates/generator/empty_app/{design/v1/resources → config/initializers}/.empty_directory +0 -0
- data/tasks/thor/templates/generator/empty_app/{design/v1/resources → config/initializers}/.gitkeep +0 -0
- data/tasks/thor/templates/generator/empty_app/design/v1/endpoints/.empty_directory +0 -0
- data/tasks/thor/templates/generator/empty_app/design/v1/endpoints/.gitkeep +0 -0
- data/tasks/thor/templates/generator/empty_app/docs/.empty_directory +0 -0
- data/tasks/thor/templates/generator/empty_app/docs/.gitkeep +0 -0
- data/tasks/thor/templates/generator/empty_app/spec/spec_helper.rb +14 -9
- data/tasks/thor/templates/generator/example_app/.gitignore +1 -0
- data/tasks/thor/templates/generator/example_app/Gemfile +19 -0
- data/tasks/thor/templates/generator/example_app/Rakefile +61 -0
- data/tasks/thor/templates/generator/example_app/app/models/user.rb +6 -0
- data/tasks/thor/templates/generator/example_app/app/v1/concerns/controller_base.rb +24 -0
- data/tasks/thor/templates/generator/example_app/app/v1/controllers/users.rb +17 -0
- data/tasks/thor/templates/generator/example_app/app/v1/resources/base.rb +11 -0
- data/tasks/thor/templates/generator/example_app/app/v1/resources/user.rb +25 -0
- data/tasks/thor/templates/generator/example_app/config.ru +30 -0
- data/tasks/thor/templates/generator/example_app/config/environment.rb +41 -0
- data/tasks/thor/templates/generator/example_app/db/migrate/20201010101010_create_users_table.rb +12 -0
- data/tasks/thor/templates/generator/example_app/db/seeds.rb +6 -0
- data/tasks/thor/templates/generator/example_app/design/api.rb +18 -0
- data/tasks/thor/templates/generator/example_app/design/v1/endpoints/users.rb +37 -0
- data/tasks/thor/templates/generator/example_app/design/v1/media_types/user.rb +21 -0
- data/tasks/thor/templates/generator/example_app/spec/helpers/database_helper.rb +20 -0
- data/tasks/thor/templates/generator/example_app/spec/spec_helper.rb +42 -0
- data/tasks/thor/templates/generator/example_app/spec/v1/controllers/users_spec.rb +37 -0
- data/tasks/thor/templates/generator/scaffold/design/endpoints/collection.rb +98 -0
- data/tasks/thor/templates/generator/scaffold/design/media_types/item.rb +18 -0
- data/tasks/thor/templates/generator/scaffold/implementation/controllers/collection.rb +77 -0
- data/tasks/thor/templates/generator/scaffold/implementation/resources/base.rb +11 -0
- data/tasks/thor/templates/generator/scaffold/implementation/resources/item.rb +45 -0
- data/tasks/thor/templates/generator/scaffold/models/active_record.rb +6 -0
- data/tasks/thor/templates/generator/scaffold/models/sequel.rb +6 -0
- metadata +64 -136
- data/lib/api_browser/.bowerrc +0 -3
- data/lib/api_browser/.editorconfig +0 -21
- data/lib/api_browser/Gruntfile.js +0 -581
- data/lib/api_browser/app/index.html +0 -59
- data/lib/api_browser/app/js/app.js +0 -48
- data/lib/api_browser/app/js/controllers/action.js +0 -47
- data/lib/api_browser/app/js/controllers/controller.js +0 -10
- data/lib/api_browser/app/js/controllers/menu.js +0 -93
- data/lib/api_browser/app/js/controllers/trait.js +0 -10
- data/lib/api_browser/app/js/controllers/type.js +0 -24
- data/lib/api_browser/app/js/directives/attribute_description.js +0 -56
- data/lib/api_browser/app/js/directives/attribute_table.js +0 -28
- data/lib/api_browser/app/js/directives/conditional_requirements.js +0 -13
- data/lib/api_browser/app/js/directives/fixed_if_fits.js +0 -38
- data/lib/api_browser/app/js/directives/highlight.js +0 -14
- data/lib/api_browser/app/js/directives/menu_item.js +0 -59
- data/lib/api_browser/app/js/directives/no_container.js +0 -8
- data/lib/api_browser/app/js/directives/readable_list.js +0 -87
- data/lib/api_browser/app/js/directives/request_examples.js +0 -31
- data/lib/api_browser/app/js/directives/type_placeholder.js +0 -30
- data/lib/api_browser/app/js/directives/url.js +0 -15
- data/lib/api_browser/app/js/factories/Configuration.js +0 -12
- data/lib/api_browser/app/js/factories/Documentation.js +0 -61
- data/lib/api_browser/app/js/factories/Example.js +0 -51
- data/lib/api_browser/app/js/factories/PageInfo.js +0 -9
- data/lib/api_browser/app/js/factories/normalize_attributes.js +0 -20
- data/lib/api_browser/app/js/factories/prepare_template.js +0 -15
- data/lib/api_browser/app/js/factories/template_for.js +0 -128
- data/lib/api_browser/app/js/filters/attribute_name.js +0 -10
- data/lib/api_browser/app/js/filters/friendly_json.js +0 -5
- data/lib/api_browser/app/js/filters/has_requirement.js +0 -14
- data/lib/api_browser/app/js/filters/header_info.js +0 -9
- data/lib/api_browser/app/js/filters/is_empty.js +0 -8
- data/lib/api_browser/app/js/filters/markdown.js +0 -6
- data/lib/api_browser/app/js/filters/resource_name.js +0 -5
- data/lib/api_browser/app/js/filters/tag_requirement.js +0 -13
- data/lib/api_browser/app/sass/modules/_body.scss +0 -40
- data/lib/api_browser/app/sass/modules/_cloke.scss +0 -8
- data/lib/api_browser/app/sass/modules/_header.scss +0 -10
- data/lib/api_browser/app/sass/modules/_nav.scss +0 -7
- data/lib/api_browser/app/sass/modules/_sidebar.scss +0 -134
- data/lib/api_browser/app/sass/modules/_switch.scss +0 -55
- data/lib/api_browser/app/sass/modules/_table.scss +0 -13
- data/lib/api_browser/app/sass/praxis.scss +0 -70
- data/lib/api_browser/app/sass/variables/_bootstrap-variables.scss +0 -774
- data/lib/api_browser/app/views/action.html +0 -97
- data/lib/api_browser/app/views/builtin/field-selector.html +0 -24
- data/lib/api_browser/app/views/controller.html +0 -55
- data/lib/api_browser/app/views/directives/attribute_description.html +0 -2
- data/lib/api_browser/app/views/directives/attribute_description/default.html +0 -2
- data/lib/api_browser/app/views/directives/attribute_description/example.html +0 -13
- data/lib/api_browser/app/views/directives/attribute_description/headers.html +0 -8
- data/lib/api_browser/app/views/directives/attribute_description/member_options.html +0 -4
- data/lib/api_browser/app/views/directives/attribute_description/values.html +0 -14
- data/lib/api_browser/app/views/directives/attribute_table.html +0 -17
- data/lib/api_browser/app/views/directives/menu_item.html +0 -8
- data/lib/api_browser/app/views/directives/url.html +0 -3
- data/lib/api_browser/app/views/examples/general.html +0 -26
- data/lib/api_browser/app/views/home.html +0 -5
- data/lib/api_browser/app/views/layout.html +0 -8
- data/lib/api_browser/app/views/menu.html +0 -42
- data/lib/api_browser/app/views/navbar.html +0 -9
- data/lib/api_browser/app/views/trait.html +0 -13
- data/lib/api_browser/app/views/type.html +0 -6
- data/lib/api_browser/app/views/type/details.html +0 -33
- data/lib/api_browser/app/views/types/embedded/array.html +0 -2
- data/lib/api_browser/app/views/types/embedded/default.html +0 -12
- data/lib/api_browser/app/views/types/embedded/field-selector.html +0 -13
- data/lib/api_browser/app/views/types/embedded/links.html +0 -11
- data/lib/api_browser/app/views/types/embedded/requirements.html +0 -6
- data/lib/api_browser/app/views/types/embedded/single_req.html +0 -9
- data/lib/api_browser/app/views/types/embedded/struct.html +0 -14
- data/lib/api_browser/app/views/types/label/link.html +0 -1
- data/lib/api_browser/app/views/types/label/primitive.html +0 -1
- data/lib/api_browser/app/views/types/label/primitive_collection.html +0 -1
- data/lib/api_browser/app/views/types/label/type.html +0 -1
- data/lib/api_browser/app/views/types/label/type_collection.html +0 -1
- data/lib/api_browser/app/views/types/main/array.html +0 -22
- data/lib/api_browser/app/views/types/main/default.html +0 -23
- data/lib/api_browser/app/views/types/main/hash.html +0 -23
- data/lib/api_browser/app/views/types/standalone/array.html +0 -3
- data/lib/api_browser/app/views/types/standalone/default.html +0 -18
- data/lib/api_browser/app/views/types/standalone/struct.html +0 -2
- data/lib/api_browser/bower_template.json +0 -41
- data/lib/api_browser/package-lock.json +0 -7110
- data/lib/api_browser/package.json +0 -43
- data/lib/praxis/docs/generator.rb +0 -243
- data/lib/praxis/docs/link_builder.rb +0 -30
- data/lib/praxis/links.rb +0 -135
- data/lib/praxis/types/multipart.rb +0 -109
- data/spec/api_browser/directives/type_placeholder_spec.js +0 -134
- data/spec/api_browser/factories/configuration_spec.js +0 -32
- data/spec/api_browser/factories/documentation_spec.js +0 -100
- data/spec/api_browser/factories/normalize_attributes_spec.js +0 -92
- data/spec/api_browser/factories/template_for_spec.js +0 -67
- data/spec/api_browser/filters/attribute_name_spec.js +0 -23
- data/spec/praxis/types/multipart_spec.rb +0 -112
- data/tasks/thor/templates/generator/empty_app/.rspec +0 -1
- data/tasks/thor/templates/generator/empty_app/Guardfile +0 -3
- data/tasks/thor/templates/generator/empty_app/config/rainbows.rb +0 -57
- data/tasks/thor/templates/generator/empty_app/docs/app.js +0 -1
- data/tasks/thor/templates/generator/empty_app/docs/styles.scss +0 -3
@@ -5,58 +5,166 @@ module Praxis
|
|
5
5
|
module AttributeFiltering
|
6
6
|
ALIAS_TABLE_PREFIX = ''
|
7
7
|
require_relative 'active_record_patches'
|
8
|
+
# Helper class that can present an SqlLiteral string which we have already quoted
|
9
|
+
# ... but! that can properly provide a "to_sym" that has the value unquoted
|
10
|
+
# This is necessary as (the latest AR code):
|
11
|
+
# * does not carry over "references" in joins if they are not SqlLiterals
|
12
|
+
# * but, at the same time, it indexes the references using the .to_sym value (which is really expected to be the normal string, without quotes)
|
13
|
+
# If we pass a normal SqlLiteral, instead of our wrapper, without quoting the table, the current AR code will never quote it to form the
|
14
|
+
# SQL string, as it's already a literal...so our "/" type separators as names won't work without quoting.
|
15
|
+
class QuasiSqlLiteral < Arel::Nodes::SqlLiteral
|
16
|
+
def initialize(quoted:, symbolized:)
|
17
|
+
@symbolized = symbolized
|
18
|
+
super(quoted)
|
19
|
+
end
|
20
|
+
def to_sym
|
21
|
+
@symbolized
|
22
|
+
end
|
23
|
+
end
|
8
24
|
|
9
25
|
class ActiveRecordFilterQueryBuilder
|
10
|
-
attr_reader :
|
26
|
+
attr_reader :model, :filters_map
|
11
27
|
|
12
28
|
# Base query to build upon
|
13
29
|
def initialize(query: , model:, filters_map:, debug: false)
|
14
|
-
|
30
|
+
# Note: Do not make the initial_query an attr reader to make sure we don't count/leak on modifying it. Easier to mostly use class methods
|
31
|
+
@initial_query = query
|
15
32
|
@model = model
|
16
|
-
@
|
33
|
+
@filters_map = filters_map
|
17
34
|
@logger = debug ? Logger.new(STDOUT) : nil
|
35
|
+
@active_record_version_maj = ActiveRecord.gem_version.segments[0]
|
18
36
|
end
|
19
37
|
|
20
|
-
def
|
21
|
-
@logger
|
38
|
+
def debug_query(msg, query)
|
39
|
+
@logger.info(msg + query.to_sql) if @logger
|
22
40
|
end
|
23
41
|
|
24
42
|
def generate(filters)
|
25
43
|
# Resolve the names and values first, based on filters_map
|
26
44
|
root_node = _convert_to_treenode(filters)
|
27
|
-
craft_filter_query(root_node, for_model: @model)
|
28
|
-
|
29
|
-
|
45
|
+
crafted = craft_filter_query(root_node, for_model: @model)
|
46
|
+
debug_query("SQL due to filters: ", crafted.all)
|
47
|
+
crafted
|
30
48
|
end
|
31
49
|
|
32
50
|
def craft_filter_query(nodetree, for_model:)
|
33
51
|
result = _compute_joins_and_conditions_data(nodetree, model: for_model)
|
34
|
-
@
|
52
|
+
return @initial_query if result[:conditions].empty?
|
53
|
+
|
54
|
+
|
55
|
+
# Find the root group (usually an AND group) but can be an OR group, or nil if there's only 1 condition
|
56
|
+
root_parent_group = result[:conditions].first[:node_object].parent_group || result[:conditions].first[:node_object]
|
57
|
+
while root_parent_group.parent_group != nil
|
58
|
+
root_parent_group = root_parent_group.parent_group
|
59
|
+
end
|
35
60
|
|
36
|
-
|
37
|
-
|
38
|
-
|
61
|
+
# Process the joins
|
62
|
+
query_with_joins = result[:associations_hash].empty? ? @initial_query : @initial_query.joins(result[:associations_hash])
|
63
|
+
|
64
|
+
# Proc to apply a single condition
|
65
|
+
apply_single_condition = Proc.new do |condition, associated_query|
|
66
|
+
colo = condition[:model].columns_hash[condition[:name].to_s]
|
39
67
|
column_prefix = condition[:column_prefix]
|
68
|
+
#Mark where clause referencing the appropriate alias
|
69
|
+
associated_query = associated_query.references(build_reference_value(column_prefix, query: associated_query))
|
70
|
+
self.class.add_clause(
|
71
|
+
query: associated_query,
|
72
|
+
column_prefix: column_prefix,
|
73
|
+
column_object: colo,
|
74
|
+
op: condition[:op],
|
75
|
+
value: condition[:value]
|
76
|
+
)
|
77
|
+
end
|
40
78
|
|
41
|
-
|
42
|
-
|
79
|
+
if @active_record_version_maj < 6
|
80
|
+
# ActiveRecord < 6 does not support '.and' so no nested things can be done
|
81
|
+
# But we can still support the case of 1+ flat conditions of the same AND/OR type
|
82
|
+
if root_parent_group.is_a?(FilteringParams::Condition)
|
83
|
+
# A Single condition it is easy to handle
|
84
|
+
apply_single_condition.call(result[:conditions].first, query_with_joins)
|
85
|
+
elsif root_parent_group.items.all?{|i| i.is_a?(FilteringParams::Condition)}
|
86
|
+
# Only 1 top level root, with only with simple condition items
|
87
|
+
if root_parent_group.type == :and
|
88
|
+
result[:conditions].reverse.inject(query_with_joins) do |accum, condition|
|
89
|
+
apply_single_condition.call(condition, accum)
|
90
|
+
end
|
91
|
+
else
|
92
|
+
# To do a flat OR, we need to apply the first condition to the incoming query
|
93
|
+
# and then apply any extra ORs to it. Otherwise Book.or(X).or(X) still matches all books
|
94
|
+
cond1, *rest = result[:conditions].reverse
|
95
|
+
start_query = apply_single_condition.call(cond1, query_with_joins)
|
96
|
+
rest.inject(start_query) do |accum, condition|
|
97
|
+
accum.or(apply_single_condition.call(condition, query_with_joins))
|
98
|
+
end
|
99
|
+
end
|
100
|
+
else
|
101
|
+
raise "Mixing AND and OR conditions is not supported for ActiveRecord <6."
|
102
|
+
end
|
103
|
+
else # ActiveRecord 6+
|
104
|
+
# Process the conditions in a depth-first order, and return the resulting query
|
105
|
+
_depth_first_traversal(
|
106
|
+
root_query: query_with_joins,
|
107
|
+
root_node: root_parent_group,
|
108
|
+
conditions: result[:conditions],
|
109
|
+
&apply_single_condition
|
110
|
+
)
|
43
111
|
end
|
44
112
|
end
|
45
113
|
|
46
114
|
private
|
115
|
+
def _depth_first_traversal(root_query:, root_node:, conditions:, &block)
|
116
|
+
# Save the associated query for non-leaves
|
117
|
+
root_node.associated_query = root_query if root_node.is_a?(FilteringParams::ConditionGroup)
|
118
|
+
|
119
|
+
if root_node.is_a?(FilteringParams::Condition)
|
120
|
+
matching_condition = conditions.find {|cond| cond[:node_object] == root_node }
|
121
|
+
|
122
|
+
# The simplified case of a single top level condition (without a wrapping group)
|
123
|
+
# will need to pass the root query itself
|
124
|
+
associated_query = root_node.parent_group ? root_node.parent_group.associated_query : root_query
|
125
|
+
return yield matching_condition, associated_query
|
126
|
+
else
|
127
|
+
first_query, *rest_queries = root_node.items.map do |child|
|
128
|
+
_depth_first_traversal(root_query: root_query, root_node: child, conditions: conditions, &block)
|
129
|
+
end
|
130
|
+
|
131
|
+
rest_queries.each.inject(first_query) do |q, a_query|
|
132
|
+
root_node.type == :and ? q.and(a_query) : q.or(a_query)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def _mapped_filter(name)
|
138
|
+
target = @filters_map[name]
|
139
|
+
unless target
|
140
|
+
if @model.attribute_names.include?(name.to_s)
|
141
|
+
# Cache it in the filters mapping (to avoid later lookups), and return it.
|
142
|
+
@filters_map[name] = name
|
143
|
+
target = name
|
144
|
+
end
|
145
|
+
end
|
146
|
+
return target
|
147
|
+
end
|
47
148
|
|
48
149
|
# Resolve and convert from filters, to a more manageable and param-type-independent structure
|
49
150
|
def _convert_to_treenode(filters)
|
50
151
|
# Resolve the names and values first, based on filters_map
|
51
152
|
resolved_array = []
|
52
153
|
filters.parsed_array.each do |filter|
|
53
|
-
mapped_value =
|
54
|
-
|
154
|
+
mapped_value = _mapped_filter(filter[:name])
|
155
|
+
unless mapped_value
|
156
|
+
msg = "Filtering by #{filter[:name]} is not allowed. No implementation mapping defined for it has been found \
|
157
|
+
and there is not a model attribute with this name either.\n" \
|
158
|
+
"Please add a mapping for #{filter[:name]} in the `filters_mapping` method of the appropriate Resource class"
|
159
|
+
raise msg
|
160
|
+
end
|
55
161
|
bindings_array = \
|
56
162
|
if mapped_value.is_a?(Proc)
|
57
163
|
result = mapped_value.call(filter)
|
58
164
|
# Result could be an array of hashes (each hash has name/op/value to identify a condition)
|
59
|
-
result.is_a?(Array) ? result : [result]
|
165
|
+
result_from_proc = result.is_a?(Array) ? result : [result]
|
166
|
+
# Make sure we tack on the node object associated with the filter
|
167
|
+
result_from_proc.map{|hash| hash.merge(node_object: filter[:node_object])}
|
60
168
|
else
|
61
169
|
# For non-procs there's only 1 filter and 1 value (we're just overriding the mapped value)
|
62
170
|
[filter.merge( name: mapped_value)]
|
@@ -84,51 +192,51 @@ module Praxis
|
|
84
192
|
{associations_hash: h, conditions: conditions}
|
85
193
|
end
|
86
194
|
|
87
|
-
def add_clause(column_prefix:, column_object:, op:, value:)
|
88
|
-
@query = @query.references(column_prefix) #Mark where clause referencing the appropriate alias
|
195
|
+
def self.add_clause(query:, column_prefix:, column_object:, op:, value:)
|
89
196
|
likeval = get_like_value(value)
|
90
197
|
case op
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
198
|
+
when '!' # name! means => name IS NOT NULL (and the incoming value is nil)
|
199
|
+
op = '!='
|
200
|
+
value = nil # Enforce it is indeed nil (should be)
|
201
|
+
when '!!'
|
202
|
+
op = '='
|
203
|
+
value = nil # Enforce it is indeed nil (should be)
|
204
|
+
end
|
205
|
+
|
206
|
+
case op
|
207
|
+
when '='
|
208
|
+
if likeval
|
209
|
+
add_safe_where(query: query, tab: column_prefix, col: column_object, op: 'LIKE', value: likeval)
|
210
|
+
else
|
211
|
+
quoted_right = quote_right_part(query: query, value: value, column_object: column_object, negative: false)
|
212
|
+
query.where("#{quote_column_path(query: query, prefix: column_prefix, column_object: column_object)} #{quoted_right}")
|
213
|
+
end
|
214
|
+
when '!='
|
215
|
+
if likeval
|
216
|
+
add_safe_where(query: query, tab: column_prefix, col: column_object, op: 'NOT LIKE', value: likeval)
|
217
|
+
else
|
218
|
+
quoted_right = quote_right_part(query: query, value: value, column_object: column_object, negative: true)
|
219
|
+
query.where("#{quote_column_path(query: query, prefix: column_prefix, column_object: column_object)} #{quoted_right}")
|
97
220
|
end
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
end
|
113
|
-
when '>'
|
114
|
-
add_safe_where(tab: column_prefix, col: column_object, op: '>', value: value)
|
115
|
-
when '<'
|
116
|
-
add_safe_where(tab: column_prefix, col: column_object, op: '<', value: value)
|
117
|
-
when '>='
|
118
|
-
add_safe_where(tab: column_prefix, col: column_object, op: '>=', value: value)
|
119
|
-
when '<='
|
120
|
-
add_safe_where(tab: column_prefix, col: column_object, op: '<=', value: value)
|
121
|
-
else
|
122
|
-
raise "Unsupported Operator!!! #{op}"
|
123
|
-
end
|
124
|
-
end
|
125
|
-
|
126
|
-
def add_safe_where(tab:, col:, op:, value:)
|
221
|
+
when '>'
|
222
|
+
add_safe_where(query: query, tab: column_prefix, col: column_object, op: '>', value: value)
|
223
|
+
when '<'
|
224
|
+
add_safe_where(query: query, tab: column_prefix, col: column_object, op: '<', value: value)
|
225
|
+
when '>='
|
226
|
+
add_safe_where(query: query, tab: column_prefix, col: column_object, op: '>=', value: value)
|
227
|
+
when '<='
|
228
|
+
add_safe_where(query: query, tab: column_prefix, col: column_object, op: '<=', value: value)
|
229
|
+
else
|
230
|
+
raise "Unsupported Operator!!! #{op}"
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
def self.add_safe_where(query:, tab:, col:, op:, value:)
|
127
235
|
quoted_value = query.connection.quote_default_expression(value,col)
|
128
|
-
query.where("#{quote_column_path(tab, col)} #{op} #{quoted_value}")
|
236
|
+
query.where("#{self.quote_column_path(query: query, prefix: tab, column_object: col)} #{op} #{quoted_value}")
|
129
237
|
end
|
130
238
|
|
131
|
-
def quote_column_path(prefix
|
239
|
+
def self.quote_column_path(query:, prefix:, column_object:)
|
132
240
|
c = query.connection
|
133
241
|
quoted_column = c.quote_column_name(column_object.name)
|
134
242
|
if prefix
|
@@ -139,7 +247,7 @@ module Praxis
|
|
139
247
|
end
|
140
248
|
end
|
141
249
|
|
142
|
-
def quote_right_part(value:, column_object:, negative:)
|
250
|
+
def self.quote_right_part(query:, value:, column_object:, negative:)
|
143
251
|
conn = query.connection
|
144
252
|
if value.nil?
|
145
253
|
no = negative ? ' NOT' : ''
|
@@ -157,7 +265,7 @@ module Praxis
|
|
157
265
|
end
|
158
266
|
|
159
267
|
# Returns nil if the value was not a fuzzzy pattern
|
160
|
-
def get_like_value(value)
|
268
|
+
def self.get_like_value(value)
|
161
269
|
if value.is_a?(String) && (value[-1] == '*' || value[0] == '*')
|
162
270
|
likeval = value.dup
|
163
271
|
likeval[-1] = '%' if value[-1] == '*'
|
@@ -165,6 +273,22 @@ module Praxis
|
|
165
273
|
likeval
|
166
274
|
end
|
167
275
|
end
|
276
|
+
|
277
|
+
# The value that we need to stick in the references method is different in the latest Rails
|
278
|
+
maj, min, _ = ActiveRecord.gem_version.segments
|
279
|
+
if maj == 5 || (maj == 6 && min == 0)
|
280
|
+
# In AR 6 (and 6.0) the references are simple strings
|
281
|
+
def build_reference_value(column_prefix, query: nil)
|
282
|
+
column_prefix
|
283
|
+
end
|
284
|
+
else
|
285
|
+
# The latest AR versions discard passing references to joins when they're not SqlLiterals ... so let's wrap it
|
286
|
+
# with our class, so that it is a literal (already quoted), but that can still provide the expected "symbol" without quotes
|
287
|
+
# so that our aliasing code can match it.
|
288
|
+
def build_reference_value(column_prefix, query:)
|
289
|
+
QuasiSqlLiteral.new(quoted: query.connection.quote_table_name(column_prefix), symbolized: column_prefix.to_sym)
|
290
|
+
end
|
291
|
+
end
|
168
292
|
end
|
169
293
|
end
|
170
294
|
end
|
@@ -3,7 +3,8 @@ module Praxis
|
|
3
3
|
module AttributeFiltering
|
4
4
|
class FilterTreeNode
|
5
5
|
attr_reader :path, :conditions, :children
|
6
|
-
#
|
6
|
+
# Parsed_filters is an Array of {name: X, op: Y, value: Z} ... exactly the format of the FilteringParams.load method
|
7
|
+
# It can also contain a :node_object
|
7
8
|
def initialize(parsed_filters, path: [])
|
8
9
|
@path = path # Array that marks the tree 'path' to this node (with respect to the absolute root)
|
9
10
|
@conditions = [] # Conditions to apply directly to this node
|
@@ -14,7 +15,7 @@ module Praxis
|
|
14
15
|
if components.empty?
|
15
16
|
return
|
16
17
|
elsif components.size == 1
|
17
|
-
@conditions << hash.slice(:name, :op, :value)
|
18
|
+
@conditions << hash.slice(:name, :op, :value, :node_object)
|
18
19
|
else
|
19
20
|
children_data[components.first] ||= []
|
20
21
|
children_data[components.first] << hash
|
@@ -1,7 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
-
|
2
|
+
require 'praxis/extensions/attribute_filtering/filters_parser'
|
3
|
+
|
3
4
|
#
|
4
|
-
# Attributor type to define and
|
5
|
+
# Attributor type to define and handle the language to express filtering attributes in listings.
|
5
6
|
# Commonly used in a query string parameter value for listing calls.
|
6
7
|
#
|
7
8
|
# The type allows you to restrict the allowable fields (and their types) based on an existing Mediatype.
|
@@ -14,7 +15,7 @@
|
|
14
15
|
# attribute :filters,
|
15
16
|
# Types::FilteringParams.for(MediaTypes::MyType) do
|
16
17
|
# filter 'user.id', using: ['=', '!=']
|
17
|
-
# filter 'name', using: ['=', '!=']
|
18
|
+
# filter 'name', using: ['=', '!=', '!', '!!]
|
18
19
|
# filter 'children.created_at', using: ['>', '>=', '<', '<=']
|
19
20
|
# filter 'display_name', using: ['=', '!='], fuzzy: true
|
20
21
|
# end
|
@@ -26,6 +27,8 @@ module Praxis
|
|
26
27
|
include Attributor::Type
|
27
28
|
include Attributor::Dumpable
|
28
29
|
|
30
|
+
attr_reader :parsed_array
|
31
|
+
|
29
32
|
class DSLCompiler < Attributor::DSLCompiler
|
30
33
|
# "account.id": { operators: ["=", "!="] },
|
31
34
|
# name: { operators: ["=", "!="], fuzzy_match: true },
|
@@ -36,9 +39,9 @@ module Praxis
|
|
36
39
|
end
|
37
40
|
end
|
38
41
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
+
VALUE_OPERATORS = Set.new(['!=', '>=', '<=', '=', '<', '>']).freeze
|
43
|
+
NOVALUE_OPERATORS = Set.new(['!','!!']).freeze
|
44
|
+
AVAILABLE_OPERATORS = Set.new(VALUE_OPERATORS+NOVALUE_OPERATORS).freeze
|
42
45
|
|
43
46
|
# Abstract class, which needs to be used by subclassing it through the .for method, to set the allowed filters
|
44
47
|
# definition should be a hash, keyed by field name, which contains a hash that can have two pieces of metadata
|
@@ -52,7 +55,7 @@ module Praxis
|
|
52
55
|
def for(media_type, **_opts)
|
53
56
|
unless media_type < Praxis::MediaType
|
54
57
|
raise ArgumentError, "Invalid type: #{media_type.name} for Filters. " \
|
55
|
-
'
|
58
|
+
'Using the .for method for defining a filter, requires passing a subclass of a MediaType'
|
56
59
|
end
|
57
60
|
|
58
61
|
::Class.new(self) do
|
@@ -77,9 +80,7 @@ module Praxis
|
|
77
80
|
}
|
78
81
|
end
|
79
82
|
end
|
80
|
-
|
81
|
-
attr_reader :parsed_array
|
82
|
-
|
83
|
+
|
83
84
|
def self.native_type
|
84
85
|
self
|
85
86
|
end
|
@@ -103,7 +104,7 @@ module Praxis
|
|
103
104
|
def self.construct(definition, **options)
|
104
105
|
return self if definition.nil?
|
105
106
|
|
106
|
-
DSLCompiler.new(self, options).parse(*definition)
|
107
|
+
DSLCompiler.new(self, **options).parse(*definition)
|
107
108
|
self
|
108
109
|
end
|
109
110
|
|
@@ -134,11 +135,15 @@ module Praxis
|
|
134
135
|
raise "filter with name #{filter_name} does not correspond to an existing field inside " \
|
135
136
|
" MediaType #{media_type.name}"
|
136
137
|
end
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
138
|
+
if NOVALUE_OPERATORS.include?(op)
|
139
|
+
arr << "#{filter_name}#{op}" # Do not add a value for the operators that don't take it
|
140
|
+
else
|
141
|
+
attr_example = filter_components.inject(mt_example) do |last, name|
|
142
|
+
# we can safely do sends, since we've verified the components are valid
|
143
|
+
last.send(name)
|
144
|
+
end
|
145
|
+
arr << "#{filter_name}#{op}#{attr_example}"
|
146
|
+
end
|
142
147
|
end.join('&')
|
143
148
|
else
|
144
149
|
'name=Joe&date>2017-01-01'
|
@@ -153,35 +158,33 @@ module Praxis
|
|
153
158
|
|
154
159
|
def self.load(filters, _context = Attributor::DEFAULT_ROOT_CONTEXT, **_options)
|
155
160
|
return filters if filters.is_a?(native_type)
|
156
|
-
return new if filters.nil?
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
161
|
+
return new if filters.nil? || filters.blank?
|
162
|
+
|
163
|
+
parsed = Parser.new.parse(filters)
|
164
|
+
|
165
|
+
tree = ConditionGroup.load(parsed)
|
166
|
+
|
167
|
+
rr = tree.flattened_conditions
|
168
|
+
accum = []
|
169
|
+
rr.each do |spec|
|
170
|
+
attr_name = spec[:name]
|
171
|
+
# TODO: Do we need to CGI.unescape things? here or even before??...
|
172
|
+
coerced = \
|
173
|
+
if media_type
|
174
|
+
filter_components = attr_name.to_s.split('.').map(&:to_sym)
|
175
|
+
attr, _enclosing_type = find_filter_attribute(filter_components, media_type)
|
176
|
+
if spec[:values].is_a?(Array)
|
177
|
+
attr_coll = Attributor::Collection.of(attr.type)
|
178
|
+
attr_coll.load(spec[:values])
|
179
|
+
else
|
180
|
+
attr.load(spec[:values])
|
181
|
+
end
|
176
182
|
else
|
177
|
-
|
183
|
+
spec[:values]
|
178
184
|
end
|
179
|
-
|
180
|
-
value
|
181
|
-
end
|
182
|
-
arr.push(name: attr_name, op: match[:operator], value: coerced )
|
185
|
+
accum.push(name: attr_name, op: spec[:op], value: coerced , node_object: spec[:node_object])
|
183
186
|
end
|
184
|
-
new(
|
187
|
+
new(accum)
|
185
188
|
end
|
186
189
|
|
187
190
|
def self.dump(value, **_opts)
|
@@ -217,21 +220,9 @@ module Praxis
|
|
217
220
|
errors << "Operator #{item[:op]} not allowed for filter #{attr_name}"
|
218
221
|
end
|
219
222
|
value_type = attr_filters[:value_type]
|
220
|
-
value = item[:value]
|
221
|
-
if value_type && !value_type.valid_type?(value)
|
222
|
-
# Allow a collection of values of the right type for multimatch (if operators are = or !=)
|
223
|
-
if ['=','!='].include?(item[:op])
|
224
|
-
coll_type = Attributor::Collection.of(value_type)
|
225
|
-
if !coll_type.valid_type?(value)
|
226
|
-
errors << "Invalid type in filter/s value for #{attr_name} " +\
|
227
|
-
"(one or more of the multiple matches in #{value} are not a #{value_type.name.split('::').last})"
|
228
|
-
end
|
229
|
-
else
|
230
|
-
errors << "Invalid type in filter value for #{attr_name} (#{value} using '#{item[:op]}' is not a #{value_type.name.split('::').last})"
|
231
|
-
end
|
232
|
-
end
|
233
|
-
|
234
223
|
next unless value_type == Attributor::String
|
224
|
+
|
225
|
+
value = item[:value]
|
235
226
|
unless value.empty?
|
236
227
|
fuzzy_match = attr_filters[:fuzzy_match]
|
237
228
|
if (value[-1] == '*' || value[0] == '*') && !fuzzy_match
|