praxis 2.0.pre.18 → 2.0.pre.19

Sign up to get free protection for your applications and to get access to all the features.
Files changed (235) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +54 -0
  3. data/.simplecov +3 -1
  4. data/.travis.yml +2 -1
  5. data/CHANGELOG.md +6 -0
  6. data/CONTRIBUTING.md +2 -79
  7. data/Gemfile +5 -1
  8. data/Guardfile +6 -4
  9. data/LICENSE +0 -2
  10. data/MAINTAINERS.md +1 -0
  11. data/README.md +15 -22
  12. data/Rakefile +4 -2
  13. data/bin/praxis +55 -58
  14. data/lib/praxis/action_definition/headers_dsl_compiler.rb +5 -6
  15. data/lib/praxis/action_definition.rb +64 -94
  16. data/lib/praxis/api_definition.rb +21 -29
  17. data/lib/praxis/api_general_info.rb +55 -66
  18. data/lib/praxis/application.rb +15 -32
  19. data/lib/praxis/blueprint.rb +64 -73
  20. data/lib/praxis/bootloader.rb +24 -33
  21. data/lib/praxis/bootloader_stages/environment.rb +5 -10
  22. data/lib/praxis/bootloader_stages/file_loader.rb +3 -6
  23. data/lib/praxis/bootloader_stages/plugin_config_load.rb +4 -6
  24. data/lib/praxis/bootloader_stages/plugin_config_prepare.rb +2 -2
  25. data/lib/praxis/bootloader_stages/plugin_loader.rb +3 -7
  26. data/lib/praxis/bootloader_stages/plugin_setup.rb +3 -3
  27. data/lib/praxis/bootloader_stages/routing.rb +5 -8
  28. data/lib/praxis/bootloader_stages/subgroup_loader.rb +2 -10
  29. data/lib/praxis/bootloader_stages/warn_unloaded_files.rb +15 -19
  30. data/lib/praxis/callbacks.rb +12 -11
  31. data/lib/praxis/collection.rb +10 -13
  32. data/lib/praxis/config.rb +17 -28
  33. data/lib/praxis/config_hash.rb +2 -1
  34. data/lib/praxis/controller.rb +7 -6
  35. data/lib/praxis/dispatcher.rb +34 -42
  36. data/lib/praxis/docs/open_api/info_object.rb +11 -8
  37. data/lib/praxis/docs/open_api/media_type_object.rb +18 -17
  38. data/lib/praxis/docs/open_api/operation_object.rb +6 -3
  39. data/lib/praxis/docs/open_api/parameter_object.rb +17 -14
  40. data/lib/praxis/docs/open_api/paths_object.rb +11 -9
  41. data/lib/praxis/docs/open_api/request_body_object.rb +14 -13
  42. data/lib/praxis/docs/open_api/response_object.rb +24 -18
  43. data/lib/praxis/docs/open_api/responses_object.rb +3 -1
  44. data/lib/praxis/docs/open_api/schema_object.rb +16 -16
  45. data/lib/praxis/docs/open_api/server_object.rb +5 -2
  46. data/lib/praxis/docs/open_api/tag_object.rb +6 -3
  47. data/lib/praxis/docs/open_api_generator.rb +92 -95
  48. data/lib/praxis/endpoint_definition.rb +60 -77
  49. data/lib/praxis/error_handler.rb +2 -2
  50. data/lib/praxis/exception.rb +2 -0
  51. data/lib/praxis/exceptions/config.rb +3 -1
  52. data/lib/praxis/exceptions/config_load.rb +2 -0
  53. data/lib/praxis/exceptions/config_validation.rb +3 -1
  54. data/lib/praxis/exceptions/invalid_configuration.rb +3 -1
  55. data/lib/praxis/exceptions/invalid_response.rb +3 -1
  56. data/lib/praxis/exceptions/invalid_trait.rb +3 -1
  57. data/lib/praxis/exceptions/stage_not_found.rb +3 -1
  58. data/lib/praxis/exceptions/validation.rb +4 -3
  59. data/lib/praxis/extensions/attribute_filtering/active_record_filter_query_builder.rb +163 -149
  60. data/lib/praxis/extensions/attribute_filtering/active_record_patches/5x.rb +18 -13
  61. data/lib/praxis/extensions/attribute_filtering/active_record_patches/6_0.rb +13 -9
  62. data/lib/praxis/extensions/attribute_filtering/active_record_patches/6_1_plus.rb +14 -11
  63. data/lib/praxis/extensions/attribute_filtering/active_record_patches.rb +12 -9
  64. data/lib/praxis/extensions/attribute_filtering/filter_tree_node.rb +8 -5
  65. data/lib/praxis/extensions/attribute_filtering/filtering_params.rb +89 -65
  66. data/lib/praxis/extensions/attribute_filtering/filters_parser.rb +68 -62
  67. data/lib/praxis/extensions/attribute_filtering.rb +3 -1
  68. data/lib/praxis/extensions/field_expansion.rb +6 -4
  69. data/lib/praxis/extensions/field_selection/active_record_query_selector.rb +10 -8
  70. data/lib/praxis/extensions/field_selection/field_selector.rb +91 -92
  71. data/lib/praxis/extensions/field_selection/sequel_query_selector.rb +12 -12
  72. data/lib/praxis/extensions/field_selection.rb +3 -1
  73. data/lib/praxis/extensions/pagination/active_record_pagination_handler.rb +6 -4
  74. data/lib/praxis/extensions/pagination/header_generator.rb +16 -11
  75. data/lib/praxis/extensions/pagination/ordering_params.rb +29 -28
  76. data/lib/praxis/extensions/pagination/pagination_handler.rb +44 -42
  77. data/lib/praxis/extensions/pagination/pagination_params.rb +29 -48
  78. data/lib/praxis/extensions/pagination/sequel_pagination_handler.rb +8 -7
  79. data/lib/praxis/extensions/pagination.rb +10 -15
  80. data/lib/praxis/extensions/rails_compat/request_methods.rb +3 -4
  81. data/lib/praxis/extensions/rails_compat.rb +2 -0
  82. data/lib/praxis/extensions/rendering.rb +12 -12
  83. data/lib/praxis/field_expander.rb +8 -9
  84. data/lib/praxis/file_group.rb +8 -12
  85. data/lib/praxis/finalizable.rb +1 -0
  86. data/lib/praxis/handlers/json.rb +5 -2
  87. data/lib/praxis/handlers/plain.rb +2 -1
  88. data/lib/praxis/handlers/www_form.rb +6 -3
  89. data/lib/praxis/handlers/{xml-sample.rb → xml_sample.rb} +26 -22
  90. data/lib/praxis/mapper/active_model_compat.rb +13 -10
  91. data/lib/praxis/mapper/resource.rb +171 -180
  92. data/lib/praxis/mapper/selector_generator.rb +106 -112
  93. data/lib/praxis/mapper/sequel_compat.rb +70 -67
  94. data/lib/praxis/media_type.rb +2 -2
  95. data/lib/praxis/media_type_identifier.rb +26 -22
  96. data/lib/praxis/middleware_app.rb +18 -15
  97. data/lib/praxis/multipart/parser.rb +46 -51
  98. data/lib/praxis/multipart/part.rb +78 -110
  99. data/lib/praxis/notifications.rb +2 -4
  100. data/lib/praxis/plugin.rb +11 -18
  101. data/lib/praxis/plugin_concern.rb +12 -15
  102. data/lib/praxis/plugins/mapper_plugin.rb +15 -13
  103. data/lib/praxis/plugins/pagination_plugin.rb +8 -6
  104. data/lib/praxis/plugins/rails_plugin.rb +33 -28
  105. data/lib/praxis/renderer.rb +11 -15
  106. data/lib/praxis/request.rb +46 -47
  107. data/lib/praxis/request_stages/action.rb +4 -6
  108. data/lib/praxis/request_stages/load_request.rb +2 -4
  109. data/lib/praxis/request_stages/request_stage.rb +19 -23
  110. data/lib/praxis/request_stages/response.rb +4 -6
  111. data/lib/praxis/request_stages/validate.rb +3 -5
  112. data/lib/praxis/request_stages/validate_params_and_headers.rb +15 -16
  113. data/lib/praxis/request_stages/validate_payload.rb +25 -27
  114. data/lib/praxis/request_superclassing.rb +3 -3
  115. data/lib/praxis/resource_definition.rb +1 -0
  116. data/lib/praxis/response.rb +13 -25
  117. data/lib/praxis/response_definition.rb +77 -122
  118. data/lib/praxis/response_template.rb +11 -15
  119. data/lib/praxis/responses/http.rb +23 -44
  120. data/lib/praxis/responses/internal_server_error.rb +18 -21
  121. data/lib/praxis/responses/multipart_ok.rb +4 -9
  122. data/lib/praxis/responses/validation_error.rb +8 -15
  123. data/lib/praxis/route.rb +8 -10
  124. data/lib/praxis/router/rack.rb +13 -7
  125. data/lib/praxis/router/simple.rb +10 -5
  126. data/lib/praxis/router.rb +27 -34
  127. data/lib/praxis/routing_config.rb +52 -29
  128. data/lib/praxis/simple_media_type.rb +5 -8
  129. data/lib/praxis/stage.rb +17 -25
  130. data/lib/praxis/tasks/api_docs.rb +15 -15
  131. data/lib/praxis/tasks/console.rb +3 -1
  132. data/lib/praxis/tasks/environment.rb +2 -0
  133. data/lib/praxis/tasks/routes.rb +26 -24
  134. data/lib/praxis/tasks.rb +3 -1
  135. data/lib/praxis/trait.rb +37 -46
  136. data/lib/praxis/types/fuzzy_hash.rb +13 -14
  137. data/lib/praxis/types/media_type_common.rb +11 -10
  138. data/lib/praxis/types/multipart_array/part_definition.rb +14 -17
  139. data/lib/praxis/types/multipart_array.rb +88 -112
  140. data/lib/praxis/validation_handler.rb +5 -3
  141. data/lib/praxis/version.rb +3 -1
  142. data/lib/praxis.rb +4 -5
  143. data/praxis.gemspec +22 -21
  144. data/spec/functional_spec.rb +40 -52
  145. data/spec/praxis/action_definition_spec.rb +36 -46
  146. data/spec/praxis/api_definition_spec.rb +45 -47
  147. data/spec/praxis/api_general_info_spec.rb +28 -29
  148. data/spec/praxis/application_spec.rb +18 -14
  149. data/spec/praxis/blueprint_spec.rb +33 -34
  150. data/spec/praxis/bootloader_spec.rb +32 -30
  151. data/spec/praxis/callbacks_spec.rb +37 -37
  152. data/spec/praxis/collection_spec.rb +18 -25
  153. data/spec/praxis/config_hash_spec.rb +5 -4
  154. data/spec/praxis/config_spec.rb +27 -26
  155. data/spec/praxis/controller_spec.rb +8 -9
  156. data/spec/praxis/endpoint_definition_spec.rb +25 -32
  157. data/spec/praxis/extensions/attribute_filtering/active_record_filter_query_builder_spec.rb +171 -114
  158. data/spec/praxis/extensions/attribute_filtering/filter_tree_node_spec.rb +22 -21
  159. data/spec/praxis/extensions/attribute_filtering/filtering_params_spec.rb +112 -60
  160. data/spec/praxis/extensions/attribute_filtering/filters_parser_spec.rb +37 -38
  161. data/spec/praxis/extensions/field_expansion_spec.rb +8 -10
  162. data/spec/praxis/extensions/field_selection/active_record_query_selector_spec.rb +14 -13
  163. data/spec/praxis/extensions/field_selection/field_selector_spec.rb +9 -16
  164. data/spec/praxis/extensions/field_selection/sequel_query_selector_spec.rb +50 -49
  165. data/spec/praxis/extensions/pagination/active_record_pagination_handler_spec.rb +32 -31
  166. data/spec/praxis/extensions/rendering_spec.rb +9 -9
  167. data/spec/praxis/extensions/support/spec_resources_active_model.rb +32 -49
  168. data/spec/praxis/extensions/support/spec_resources_sequel.rb +48 -48
  169. data/spec/praxis/field_expander_spec.rb +6 -5
  170. data/spec/praxis/file_group_spec.rb +3 -1
  171. data/spec/praxis/handlers/json_spec.rb +6 -5
  172. data/spec/praxis/mapper/resource_spec.rb +27 -30
  173. data/spec/praxis/mapper/selector_generator_spec.rb +50 -50
  174. data/spec/praxis/media_type_identifier_spec.rb +13 -10
  175. data/spec/praxis/media_type_spec.rb +12 -12
  176. data/spec/praxis/middleware_app_spec.rb +23 -22
  177. data/spec/praxis/multipart/parser_spec.rb +7 -9
  178. data/spec/praxis/notifications_spec.rb +4 -4
  179. data/spec/praxis/plugin_concern_spec.rb +5 -6
  180. data/spec/praxis/renderer_spec.rb +10 -9
  181. data/spec/praxis/request_spec.rb +38 -41
  182. data/spec/praxis/request_stages/action_spec.rb +14 -15
  183. data/spec/praxis/request_stages/request_stage_spec.rb +30 -41
  184. data/spec/praxis/request_stages/validate_spec.rb +3 -1
  185. data/spec/praxis/response_definition_spec.rb +79 -92
  186. data/spec/praxis/response_spec.rb +28 -39
  187. data/spec/praxis/responses/internal_server_error_spec.rb +6 -9
  188. data/spec/praxis/responses/validation_error_spec.rb +17 -18
  189. data/spec/praxis/route_spec.rb +4 -7
  190. data/spec/praxis/router_spec.rb +69 -79
  191. data/spec/praxis/routing_config_spec.rb +15 -14
  192. data/spec/praxis/stage_spec.rb +56 -53
  193. data/spec/praxis/trait_spec.rb +17 -18
  194. data/spec/praxis/types/fuzzy_hash_spec.rb +11 -9
  195. data/spec/praxis/types/multipart_array/part_definition_spec.rb +3 -2
  196. data/spec/praxis/types/multipart_array_spec.rb +33 -48
  197. data/spec/spec_app/app/concerns/authenticated.rb +5 -5
  198. data/spec/spec_app/app/concerns/basic_api.rb +3 -1
  199. data/spec/spec_app/app/concerns/log_wrapper.rb +5 -3
  200. data/spec/spec_app/app/controllers/base_class.rb +6 -5
  201. data/spec/spec_app/app/controllers/instances.rb +31 -34
  202. data/spec/spec_app/app/controllers/volumes.rb +6 -6
  203. data/spec/spec_app/app/responses/multipart.rb +1 -2
  204. data/spec/spec_app/app/responses/other_response.rb +2 -2
  205. data/spec/spec_app/config/environment.rb +19 -6
  206. data/spec/spec_app/config.ru +4 -3
  207. data/spec/spec_app/design/api.rb +13 -15
  208. data/spec/spec_app/design/media_types/instance.rb +5 -5
  209. data/spec/spec_app/design/media_types/volume.rb +2 -1
  210. data/spec/spec_app/design/media_types/volume_snapshot.rb +2 -1
  211. data/spec/spec_app/design/resources/instances.rb +9 -15
  212. data/spec/spec_app/design/resources/volume_snapshots.rb +4 -5
  213. data/spec/spec_app/design/resources/volumes.rb +4 -5
  214. data/spec/spec_helper.rb +11 -13
  215. data/spec/support/be_deep_equal_matcher.rb +5 -0
  216. data/spec/support/spec_authorization_plugin.rb +7 -12
  217. data/spec/support/spec_blueprints.rb +2 -1
  218. data/spec/support/spec_complex_authentication_plugin.rb +17 -34
  219. data/spec/support/spec_endpoint_definitions.rb +2 -3
  220. data/spec/support/spec_media_types.rb +28 -35
  221. data/spec/support/spec_resources.rb +20 -18
  222. data/spec/support/spec_simple_authentication_plugin.rb +5 -9
  223. data/tasks/loader.thor +4 -2
  224. data/tasks/thor/app.rb +7 -5
  225. data/tasks/thor/example.rb +23 -22
  226. data/tasks/thor/model.rb +7 -7
  227. data/tasks/thor/scaffold.rb +23 -23
  228. data/tasks/thor/templates/generator/example_app/app/v1/resources/user.rb +0 -8
  229. data/tasks/thor/templates/generator/scaffold/implementation/resources/item.rb +1 -2
  230. metadata +72 -84
  231. data/MAINTAINERS +0 -2
  232. data/TODO.md +0 -25
  233. data/spec/praxis/api_resource_spec.rb +0 -0
  234. data/spec/praxis/dispatcher_spec.rb +0 -0
  235. data/spec/spec_app/app/responses/bulk_response.rb +0 -0
@@ -1,4 +1,4 @@
1
-
1
+ # frozen_string_literal: true
2
2
 
3
3
  module Praxis
4
4
  module Extensions
@@ -10,13 +10,14 @@ module Praxis
10
10
  # This is necessary as (the latest AR code):
11
11
  # * does not carry over "references" in joins if they are not SqlLiterals
12
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
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
14
  # SQL string, as it's already a literal...so our "/" type separators as names won't work without quoting.
15
15
  class QuasiSqlLiteral < Arel::Nodes::SqlLiteral
16
16
  def initialize(quoted:, symbolized:)
17
17
  @symbolized = symbolized
18
18
  super(quoted)
19
19
  end
20
+
20
21
  def to_sym
21
22
  @symbolized
22
23
  end
@@ -27,67 +28,61 @@ module Praxis
27
28
  attr_reader :model, :filters_map
28
29
 
29
30
  # Base query to build upon
30
- def initialize(query: , model:, filters_map:, debug: false)
31
- # 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
+ def initialize(query:, model:, filters_map:, debug: false)
32
+ # 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
32
33
  @initial_query = query
33
34
  @model = model
34
35
  @filters_map = filters_map
35
- @logger = debug ? Logger.new(STDOUT) : nil
36
+ @logger = debug ? Logger.new($stdout) : nil
36
37
  @active_record_version_maj = ActiveRecord.gem_version.segments[0]
37
38
  end
38
-
39
+
39
40
  def debug_query(msg, query)
40
- @logger.info(msg + query.to_sql) if @logger
41
+ @logger&.info(msg + query.to_sql)
41
42
  end
42
43
 
43
44
  def generate(filters)
44
45
  # Resolve the names and values first, based on filters_map
45
46
  root_node = _convert_to_treenode(filters)
46
47
  crafted = craft_filter_query(root_node, for_model: @model)
47
- debug_query("SQL due to filters: ", crafted.all)
48
+ debug_query('SQL due to filters: ', crafted.all)
48
49
  crafted
49
50
  end
50
51
 
51
52
  def craft_filter_query(nodetree, for_model:)
52
53
  result = _compute_joins_and_conditions_data(nodetree, model: for_model, parent_reflection: nil)
53
54
  return @initial_query if result[:conditions].empty?
54
-
55
+
55
56
  # Find the root group (usually an AND group) but can be an OR group, or nil if there's only 1 condition
56
57
  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
58
+ root_parent_group = root_parent_group.parent_group until root_parent_group.parent_group.nil?
60
59
 
61
60
  # Process the joins
62
61
  query_with_joins = result[:associations_hash].empty? ? @initial_query : @initial_query.left_outer_joins(result[:associations_hash])
63
62
 
64
63
  # Proc to apply a single condition
65
- apply_single_condition = Proc.new do |condition, associated_query|
64
+ apply_single_condition = proc do |condition, associated_query|
66
65
  colo = condition[:model].columns_hash[condition[:name].to_s]
67
66
  column_prefix = condition[:column_prefix]
68
67
  association_key_column = \
69
- if ref = condition[:parent_reflection]
68
+ if (ref = condition[:parent_reflection])
70
69
  # get the target model of the association(where the assoc pk is)
71
- target_model = condition[:parent_reflection].klass
72
- target_model.columns_hash[condition[:parent_reflection].association_primary_key]
73
- else
74
- nil
70
+ target_model = ref.klass
71
+ target_model.columns_hash[ref.association_primary_key]
75
72
  end
76
73
 
77
74
  # Mark where clause referencing the appropriate alias IF it's not the root table, as there is no association to reference
78
- # If we added root table as a reference, we better make sure it is not quoted, as it actually makes AR to see it as an
75
+ # If we added root table as a reference, we better make sure it is not quoted, as it actually makes AR to see it as an
79
76
  # unmatched reference and eager loads the whole association (it means eager load ALL the things). Not good.
80
- unless for_model.table_name == column_prefix
81
- associated_query = associated_query.references(build_reference_value(column_prefix, query: associated_query))
82
- end
77
+ associated_query = associated_query.references(build_reference_value(column_prefix, query: associated_query)) unless for_model.table_name == column_prefix
83
78
  self.class.add_clause(
84
- query: associated_query,
85
- column_prefix: column_prefix,
86
- column_object: colo,
87
- op: condition[:op],
79
+ query: associated_query,
80
+ column_prefix: column_prefix,
81
+ column_object: colo,
82
+ op: condition[:op],
88
83
  value: condition[:value],
89
84
  fuzzy: condition[:fuzzy],
90
- association_key_column: association_key_column,
85
+ association_key_column: association_key_column
91
86
  )
92
87
  end
93
88
 
@@ -97,7 +92,7 @@ module Praxis
97
92
  if root_parent_group.is_a?(FilteringParams::Condition)
98
93
  # A Single condition it is easy to handle
99
94
  apply_single_condition.call(result[:conditions].first, query_with_joins)
100
- elsif root_parent_group.items.all?{|i| i.is_a?(FilteringParams::Condition)}
95
+ elsif root_parent_group.items.all? { |i| i.is_a?(FilteringParams::Condition) }
101
96
  # Only 1 top level root, with only with simple condition items
102
97
  if root_parent_group.type == :and
103
98
  result[:conditions].reverse.inject(query_with_joins) do |accum, condition|
@@ -113,115 +108,40 @@ module Praxis
113
108
  end
114
109
  end
115
110
  else
116
- raise "Mixing AND and OR conditions is not supported for ActiveRecord <6."
111
+ raise 'Mixing AND and OR conditions is not supported for ActiveRecord <6.'
117
112
  end
118
113
  else # ActiveRecord 6+
119
114
  # Process the conditions in a depth-first order, and return the resulting query
120
115
  _depth_first_traversal(
121
- root_query: query_with_joins,
122
- root_node: root_parent_group,
123
- conditions: result[:conditions],
116
+ root_query: query_with_joins,
117
+ root_node: root_parent_group,
118
+ conditions: result[:conditions],
124
119
  &apply_single_condition
125
120
  )
126
121
  end
127
122
  end
128
123
 
129
- private
130
- def _depth_first_traversal(root_query:, root_node:, conditions:, &block)
131
- # Save the associated query for non-leaves
132
- root_node.associated_query = root_query if root_node.is_a?(FilteringParams::ConditionGroup)
133
-
134
- if root_node.is_a?(FilteringParams::Condition)
135
- matching_condition = conditions.find {|cond| cond[:node_object] == root_node }
136
-
137
- # The simplified case of a single top level condition (without a wrapping group)
138
- # will need to pass the root query itself
139
- associated_query = root_node.parent_group ? root_node.parent_group.associated_query : root_query
140
- return yield matching_condition, associated_query
141
- else
142
- first_query, *rest_queries = root_node.items.map do |child|
143
- _depth_first_traversal(root_query: root_query, root_node: child, conditions: conditions, &block)
144
- end
145
-
146
- rest_queries.each.inject(first_query) do |q, a_query|
147
- root_node.type == :and ? q.and(a_query) : q.or(a_query)
148
- end
149
- end
150
- end
151
-
152
- def _mapped_filter(name)
153
- target = @filters_map[name]
154
- unless target
155
- filter_name = name.to_s
156
- if (@model.attribute_names + @model.reflections.keys).include?(filter_name)
157
- # Cache it in the filters mapping (to avoid later lookups), and return it.
158
- @filters_map[name] = name
159
- target = name
160
- end
161
- end
162
- return target
163
- end
164
-
165
- # Resolve and convert from filters, to a more manageable and param-type-independent structure
166
- def _convert_to_treenode(filters)
167
- # Resolve the names and values first, based on filters_map
168
- resolved_array = []
169
- filters.parsed_array.each do |filter|
170
- mapped_value = _mapped_filter(filter[:name])
171
- unless mapped_value
172
- msg = "Filtering by #{filter[:name]} is not allowed. No implementation mapping defined for it has been found \
173
- and there is not a model attribute with this name either.\n" \
174
- "Please add a mapping for #{filter[:name]} in the `filters_mapping` method of the appropriate Resource class"
175
- raise msg
176
- end
177
- bindings_array = \
178
- if mapped_value.is_a?(Proc)
179
- result = mapped_value.call(filter)
180
- # Result could be an array of hashes (each hash has name/op/value to identify a condition)
181
- result_from_proc = result.is_a?(Array) ? result : [result]
182
- # Make sure we tack on the node object associated with the filter
183
- result_from_proc.map{|hash| hash.merge(node_object: filter[:node_object])}
184
- else
185
- # For non-procs there's only 1 filter and 1 value (we're just overriding the mapped value)
186
- [filter.merge( name: mapped_value)]
187
- end
188
- resolved_array = resolved_array + bindings_array
189
- end
190
- FilterTreeNode.new(resolved_array, path: [ALIAS_TABLE_PREFIX])
191
- end
192
-
193
- # Calculate join tree and conditions array for the nodetree object and its children
194
- def _compute_joins_and_conditions_data(nodetree, model:, parent_reflection:)
195
- h = {}
196
- conditions = []
197
- nodetree.children.each do |name, child|
198
- child_reflection = model.reflections[name.to_s]
199
- result = _compute_joins_and_conditions_data(child, model: child_reflection.klass, parent_reflection: child_reflection)
200
- h[name] = result[:associations_hash]
201
-
202
- conditions += result[:conditions]
203
- end
204
-
205
- column_prefix = nodetree.path == [ALIAS_TABLE_PREFIX] ? model.table_name : nodetree.path.join(REFERENCES_STRING_SEPARATOR)
206
- nodetree.conditions.each do |condition|
207
- # If it's a final ! or !! operation on an association from the parent, it means we need to add a condition
208
- # on the existence (or lack of) of the whole associated table
209
- ref = model.reflections[condition[:name].to_s]
210
- if ref && ['!','!!'].include?(condition[:op])
211
- cp = (nodetree.path + [condition[:name].to_s]).join(REFERENCES_STRING_SEPARATOR)
212
- conditions += [condition.merge(column_prefix: cp, model: model, parent_reflection: ref)]
213
- h[condition[:name]] = {}
214
- else
215
- # Save the parent reflection where the condition applies as well (used later to get assoc keys)
216
- conditions += [condition.merge(column_prefix: column_prefix, model: model, parent_reflection: parent_reflection)]
124
+ # not in filters....checks if it's a valid path
125
+ # array of strings
126
+ def self.valid_path?(model, path)
127
+ first_component, *rest = path
128
+ if model.attribute_names.include?(first_component)
129
+ true
130
+ elsif model.reflections.keys.include?(first_component)
131
+ if rest.empty?
132
+ true # Allow associations as a leaf too (as they can have the ! and !! operator)
133
+ else # Follow the association
134
+ nested_model = model.reflections[first_component].klass
135
+ valid_path?(nested_model, rest)
217
136
  end
218
-
137
+ else
138
+ false
219
139
  end
220
- {associations_hash: h, conditions: conditions}
221
140
  end
222
141
 
223
- def self.add_clause(query:, column_prefix:, column_object:, op:, value:,fuzzy:, association_key_column:)
224
- likeval = get_like_value(value,fuzzy)
142
+ # rubocop:disable Metrics/ParameterLists,Naming/MethodParameterName
143
+ def self.add_clause(query:, column_prefix:, column_object:, op:, value:, fuzzy:, association_key_column:)
144
+ likeval = get_like_value(value, fuzzy)
225
145
 
226
146
  association_op = nil
227
147
  case op
@@ -236,7 +156,7 @@ module Praxis
236
156
  end
237
157
 
238
158
  if association_op
239
- neg = association_op == :not_null ? true : false
159
+ neg = association_op == :not_null
240
160
  qr = quote_right_part(query: query, value: nil, column_object: association_key_column, negative: neg)
241
161
  return query.where("#{quote_column_path(query: query, prefix: column_prefix, column_object: association_key_column)} #{qr}")
242
162
  end
@@ -278,11 +198,14 @@ module Praxis
278
198
  raise "Unsupported Operator!!! #{op}"
279
199
  end
280
200
  end
201
+ # rubocop:enable Metrics/ParameterLists,Naming/MethodParameterName
281
202
 
203
+ # rubocop:disable Naming/MethodParameterName
282
204
  def self.add_safe_where(query:, tab:, col:, op:, value:)
283
- quoted_value = query.connection.quote_default_expression(value,col)
284
- query.where("#{self.quote_column_path(query: query, prefix: tab, column_object: col)} #{op} #{quoted_value}")
205
+ quoted_value = query.connection.quote_default_expression(value, col)
206
+ query.where("#{quote_column_path(query: query, prefix: tab, column_object: col)} #{op} #{quoted_value}")
285
207
  end
208
+ # rubocop:enable Naming/MethodParameterName
286
209
 
287
210
  def self.quote_column_path(query:, prefix:, column_object:)
288
211
  c = query.connection
@@ -299,44 +222,135 @@ module Praxis
299
222
  conn = query.connection
300
223
  if value.nil?
301
224
  no = negative ? ' NOT' : ''
302
- "IS#{no} #{conn.quote_default_expression(value,column_object)}"
225
+ "IS#{no} #{conn.quote_default_expression(value, column_object)}"
303
226
  elsif value.is_a?(Array)
304
227
  no = negative ? 'NOT ' : ''
305
- list = value.map{|v| conn.quote_default_expression(v,column_object)}
228
+ list = value.map { |v| conn.quote_default_expression(v, column_object) }
306
229
  "#{no}IN (#{list.join(',')})"
307
- elsif value && value.is_a?(Range)
308
- raise "TODO!"
230
+ elsif value.is_a?(Range)
231
+ raise 'TODO!'
309
232
  else
310
233
  op = negative ? '<>' : '='
311
- "#{op} #{conn.quote_default_expression(value,column_object)}"
234
+ "#{op} #{conn.quote_default_expression(value, column_object)}"
312
235
  end
313
236
  end
314
237
 
315
238
  # Returns nil if the value was not a fuzzzy pattern
316
- def self.get_like_value(value,fuzzy)
239
+ def self.get_like_value(value, fuzzy)
317
240
  is_fuzzy = fuzzy.is_a?(Array) ? !fuzzy.compact.empty? : fuzzy
318
- if is_fuzzy
319
- unless value.is_a?(String)
320
- raise MultiMatchWithFuzzyNotAllowedByAdapter.new
241
+ return unless is_fuzzy
242
+
243
+ raise MultiMatchWithFuzzyNotAllowedByAdapter unless value.is_a?(String)
244
+
245
+ case fuzzy
246
+ when :start_end
247
+ "%#{value}%"
248
+ when :start
249
+ "%#{value}"
250
+ when :end
251
+ "#{value}%"
252
+ end
253
+ end
254
+
255
+ private
256
+
257
+ def _depth_first_traversal(root_query:, root_node:, conditions:, &block)
258
+ # Save the associated query for non-leaves
259
+ root_node.associated_query = root_query if root_node.is_a?(FilteringParams::ConditionGroup)
260
+
261
+ if root_node.is_a?(FilteringParams::Condition)
262
+ matching_condition = conditions.find { |cond| cond[:node_object] == root_node }
263
+
264
+ # The simplified case of a single top level condition (without a wrapping group)
265
+ # will need to pass the root query itself
266
+ associated_query = root_node.parent_group ? root_node.parent_group.associated_query : root_query
267
+ yield matching_condition, associated_query
268
+ else
269
+ first_query, *rest_queries = root_node.items.map do |child|
270
+ _depth_first_traversal(root_query: root_query, root_node: child, conditions: conditions, &block)
321
271
  end
322
- case fuzzy
323
- when :start_end
324
- '%'+value+'%'
325
- when :start
326
- '%'+value
327
- when :end
328
- value+'%'
272
+
273
+ rest_queries.each.inject(first_query) do |q, a_query|
274
+ root_node.type == :and ? q.and(a_query) : q.or(a_query)
275
+ end
276
+ end
277
+ end
278
+
279
+ def _mapped_filter(name)
280
+ target = @filters_map[name]
281
+ unless target
282
+ path = name.to_s.split('.')
283
+ if self.class.valid_path?(@model, path)
284
+ # Cache it in the filters mapping (to avoid later lookups), and return it.
285
+ @filters_map[name] = name
286
+ target = name
287
+ end
288
+ end
289
+ target
290
+ end
291
+
292
+ # Resolve and convert from filters, to a more manageable and param-type-independent structure
293
+ def _convert_to_treenode(filters)
294
+ # Resolve the names and values first, based on filters_map
295
+ resolved_array = []
296
+ filters.parsed_array.each do |filter|
297
+ mapped_value = _mapped_filter(filter[:name])
298
+ unless mapped_value
299
+ msg = "Filtering by #{filter[:name]} is not allowed. No implementation mapping defined for it has been found \
300
+ and there is not a model attribute with this name either.\n" \
301
+ "Please add a mapping for #{filter[:name]} in the `filters_mapping` method of the appropriate Resource class"
302
+ raise msg
303
+ end
304
+ bindings_array = \
305
+ if mapped_value.is_a?(Proc)
306
+ result = mapped_value.call(filter)
307
+ # Result could be an array of hashes (each hash has name/op/value to identify a condition)
308
+ result_from_proc = result.is_a?(Array) ? result : [result]
309
+ # Make sure we tack on the node object associated with the filter
310
+ result_from_proc.map { |hash| hash.merge(node_object: filter[:node_object]) }
311
+ else
312
+ # For non-procs there's only 1 filter and 1 value (we're just overriding the mapped value)
313
+ [filter.merge(name: mapped_value)]
314
+ end
315
+ resolved_array += bindings_array
316
+ end
317
+ FilterTreeNode.new(resolved_array, path: [ALIAS_TABLE_PREFIX])
318
+ end
319
+
320
+ # Calculate join tree and conditions array for the nodetree object and its children
321
+ def _compute_joins_and_conditions_data(nodetree, model:, parent_reflection:)
322
+ h = {}
323
+ conditions = []
324
+ nodetree.children.each do |name, child|
325
+ child_reflection = model.reflections[name.to_s]
326
+ result = _compute_joins_and_conditions_data(child, model: child_reflection.klass, parent_reflection: child_reflection)
327
+ h[name] = result[:associations_hash]
328
+
329
+ conditions += result[:conditions]
330
+ end
331
+
332
+ column_prefix = nodetree.path == [ALIAS_TABLE_PREFIX] ? model.table_name : nodetree.path.join(REFERENCES_STRING_SEPARATOR)
333
+ nodetree.conditions.each do |condition|
334
+ # If it's a final ! or !! operation on an association from the parent, it means we need to add a condition
335
+ # on the existence (or lack of) of the whole associated table
336
+ ref = model.reflections[condition[:name].to_s]
337
+ if ref && ['!', '!!'].include?(condition[:op])
338
+ cp = (nodetree.path + [condition[:name].to_s]).join(REFERENCES_STRING_SEPARATOR)
339
+ conditions += [condition.merge(column_prefix: cp, model: model, parent_reflection: ref)]
340
+ h[condition[:name]] = {}
341
+ else
342
+ # Save the parent reflection where the condition applies as well (used later to get assoc keys)
343
+ conditions += [condition.merge(column_prefix: column_prefix, model: model, parent_reflection: parent_reflection)]
329
344
  end
330
- else
331
- nil
332
345
  end
346
+ { associations_hash: h, conditions: conditions }
333
347
  end
334
348
 
335
349
  # The value that we need to stick in the references method is different in the latest Rails
336
- maj, min, _ = ActiveRecord.gem_version.segments
337
- if maj == 5 || (maj == 6 && min == 0)
350
+ maj, min, = ActiveRecord.gem_version.segments
351
+ if maj == 5 || (maj == 6 && min.zero?)
338
352
  # In AR 6 (and 6.0) the references are simple strings
339
- def build_reference_value(column_prefix, query: nil)
353
+ def build_reference_value(column_prefix, **_args)
340
354
  column_prefix
341
355
  end
342
356
  else
@@ -350,4 +364,4 @@ module Praxis
350
364
  end
351
365
  end
352
366
  end
353
- end
367
+ end
@@ -1,3 +1,6 @@
1
+ # frozen_string_literal: true
2
+ # rubocop:disable all
3
+
1
4
  require 'active_record'
2
5
 
3
6
  module ActiveRecord
@@ -5,7 +8,7 @@ module ActiveRecord
5
8
  class Relation
6
9
  def construct_join_dependency
7
10
  including = eager_load_values + includes_values
8
- # Praxis: inject references into the join dependency
11
+ # Praxis: inject references into the join dependency
9
12
  ActiveRecord::Associations::JoinDependency.new(
10
13
  klass, table, including, references: references_values
11
14
  )
@@ -34,18 +37,20 @@ module ActiveRecord
34
37
 
35
38
  alias_tracker.aliases
36
39
  end
37
-
38
40
  end
41
+
39
42
  module Associations
40
43
  class JoinDependency
41
44
  attr_accessor :references
45
+
42
46
  private
43
- def initialize(base, table, associations, references: )
47
+
48
+ def initialize(base, table, associations, references:)
44
49
  tree = self.class.make_tree associations
45
50
  @references = references # Save the references values into the instance (to use during build)
46
51
  @join_root = JoinBase.new(base, table, build(tree, base))
47
52
  end
48
-
53
+
49
54
  # Praxis: table aliases for is shared for 5x and 6.0
50
55
  def table_aliases_for(parent, node)
51
56
  node.reflection.chain.map do |reflection|
@@ -57,8 +62,8 @@ module ActiveRecord
57
62
  )
58
63
  # through tables do not need a special alias_path alias (as they shouldn't really referenced by the client)
59
64
  if is_root_reflection && node.alias_path
60
- table = table.left if table.is_a?(Arel::Nodes::TableAlias) #un-alias it if necessary
61
- table = table.alias(node.alias_path.join('/'))
65
+ table = table.left if table.is_a?(Arel::Nodes::TableAlias) # un-alias it if necessary
66
+ table = table.alias(node.alias_path.join('/'))
62
67
  end
63
68
  table
64
69
  end
@@ -71,20 +76,20 @@ module ActiveRecord
71
76
  reflection.check_validity!
72
77
  reflection.check_eager_loadable!
73
78
 
74
- if reflection.polymorphic?
75
- raise EagerLoadPolymorphicError.new(reflection)
76
- end
79
+ raise EagerLoadPolymorphicError, reflection if reflection.polymorphic?
80
+
77
81
  # Praxis: set an alias_path in the JoinAssociation if its path matches a requested reference
78
- child_path = (path && !path.empty?) ? path + [name] : nil
82
+ child_path = path && !path.empty? ? path + [name] : nil
79
83
  association = JoinAssociation.new(reflection, build(right, reflection.klass, path: child_path))
80
84
  association.alias_path = child_path if references.include?(child_path.join('/'))
81
- association
85
+ association
82
86
  end
83
87
  end
84
-
85
88
  end
89
+
86
90
  class ActiveRecord::Associations::JoinDependency::JoinAssociation
87
91
  attr_accessor :alias_path
88
92
  end
89
93
  end
90
- end
94
+ end
95
+ # rubocop:enable all
@@ -1,3 +1,6 @@
1
+ # frozen_string_literal: true
2
+ # rubocop:disable all
3
+
1
4
  # FOR AR < 6.1
2
5
  module ActiveRecord
3
6
  PRAXIS_JOIN_ALIAS_PREFIX = Praxis::Extensions::AttributeFiltering::ALIAS_TABLE_PREFIX
@@ -15,6 +18,7 @@ module ActiveRecord
15
18
  attr_accessor :references
16
19
 
17
20
  private
21
+
18
22
  def initialize(base, table, associations, join_type, references: nil)
19
23
  tree = self.class.make_tree associations
20
24
  @references = references # Save the references values into the instance (to use during build)
@@ -35,8 +39,8 @@ module ActiveRecord
35
39
  )
36
40
  # through tables do not need a special alias_path alias (as they shouldn't really referenced by the client)
37
41
  if is_root_reflection && node.alias_path
38
- table = table.left if table.is_a?(Arel::Nodes::TableAlias) #un-alias it if necessary
39
- table = table.alias(node.alias_path.join('/'))
42
+ table = table.left if table.is_a?(Arel::Nodes::TableAlias) # un-alias it if necessary
43
+ table = table.alias(node.alias_path.join('/'))
40
44
  end
41
45
  table
42
46
  end
@@ -49,20 +53,20 @@ module ActiveRecord
49
53
  reflection.check_validity!
50
54
  reflection.check_eager_loadable!
51
55
 
52
- if reflection.polymorphic?
53
- raise EagerLoadPolymorphicError.new(reflection)
54
- end
56
+ raise EagerLoadPolymorphicError, reflection if reflection.polymorphic?
57
+
55
58
  # Praxis: set an alias_path in the JoinAssociation if its path matches a requested reference
56
- child_path = (path && !path.empty?) ? path + [name] : nil
59
+ child_path = path && !path.empty? ? path + [name] : nil
57
60
  association = JoinAssociation.new(reflection, build(right, reflection.klass, path: child_path))
58
61
  association.alias_path = child_path if references.include?(child_path.join('/'))
59
- association
62
+ association
60
63
  end
61
64
  end
62
-
63
65
  end
66
+
64
67
  class ActiveRecord::Associations::JoinDependency::JoinAssociation
65
68
  attr_accessor :alias_path
66
69
  end
67
70
  end
68
- end
71
+ end
72
+ # rubocop:enable all
@@ -1,10 +1,13 @@
1
+ # frozen_string_literal: true
2
+ # rubocop:disable all
3
+
1
4
  # FOR AR >= 6.1
2
5
  module ActiveRecord
3
6
  PRAXIS_JOIN_ALIAS_PREFIX = Praxis::Extensions::AttributeFiltering::ALIAS_TABLE_PREFIX
4
7
  module Associations
5
8
  class JoinDependency
6
-
7
9
  private
10
+
8
11
  def make_constraints(parent, child, join_type)
9
12
  foreign_table = parent.table
10
13
  foreign_klass = parent.base_klass
@@ -19,7 +22,7 @@ module ActiveRecord
19
22
 
20
23
  table_name = @references[reflection.name.to_sym]
21
24
  # Praxis: set an alias_path in the JoinAssociation if its path matches a requested reference
22
- table_name = @references[child&.alias_path.join('/').to_sym] unless table_name
25
+ table_name ||= @references[child&.alias_path.join('/').to_sym]
23
26
 
24
27
  table = alias_tracker.aliased_table_for(reflection.klass.arel_table, table_name) do
25
28
  name = reflection.alias_candidate(parent.table_name)
@@ -38,21 +41,21 @@ module ActiveRecord
38
41
  reflection.check_validity!
39
42
  reflection.check_eager_loadable!
40
43
 
41
- if reflection.polymorphic?
42
- raise EagerLoadPolymorphicError.new(reflection)
43
- end
44
+ raise EagerLoadPolymorphicError, reflection if reflection.polymorphic?
45
+
44
46
  # Praxis: set an alias_path in the JoinAssociation if its path matches a requested reference
45
- child_path = (path && !path.empty?) ? path + [name] : nil
47
+ child_path = path && !path.empty? ? path + [name] : nil
46
48
  association = JoinAssociation.new(reflection, build(right, reflection.klass, path: child_path))
47
- #association.alias_path = child_path if references.include?(child_path.join('/'))
49
+ # association.alias_path = child_path if references.include?(child_path.join('/'))
48
50
  association.alias_path = child_path # ??? should be the line above no?
49
- association
51
+ association
50
52
  end
51
- end
53
+ end
52
54
  end
53
-
55
+
54
56
  class ActiveRecord::Associations::JoinDependency::JoinAssociation
55
57
  attr_accessor :alias_path
56
58
  end
57
59
  end
58
- end
60
+ end
61
+ # rubocop:enable all
@@ -1,15 +1,18 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'active_record'
2
4
 
3
- maj, min, _ = ActiveRecord.gem_version.segments
5
+ maj, min, = ActiveRecord.gem_version.segments
4
6
 
5
- if maj == 5
6
- require_relative 'active_record_patches/5x.rb'
7
- elsif maj == 6
8
- if min == 0
9
- require_relative 'active_record_patches/6_0.rb'
7
+ case maj
8
+ when 5
9
+ require_relative 'active_record_patches/5x'
10
+ when 6
11
+ if min.zero?
12
+ require_relative 'active_record_patches/6_0'
10
13
  else
11
- require_relative 'active_record_patches/6_1_plus.rb'
14
+ require_relative 'active_record_patches/6_1_plus'
12
15
  end
13
16
  else
14
- raise "Filtering only supported for ActiveRecord >= 5 && <= 6"
15
- end
17
+ raise 'Filtering only supported for ActiveRecord >= 5 && <= 6'
18
+ end