praxis 2.0.pre.8 → 2.0.pre.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|