praxis 2.0.pre.11 → 2.0.pre.16
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.ruby-version +1 -1
- data/CHANGELOG.md +22 -0
- data/bin/praxis +6 -0
- data/lib/praxis/api_definition.rb +8 -4
- data/lib/praxis/collection.rb +11 -0
- data/lib/praxis/docs/open_api/response_object.rb +21 -6
- data/lib/praxis/extensions/attribute_filtering.rb +14 -1
- data/lib/praxis/extensions/attribute_filtering/active_record_filter_query_builder.rb +154 -63
- data/lib/praxis/extensions/attribute_filtering/filter_tree_node.rb +3 -2
- data/lib/praxis/extensions/attribute_filtering/filtering_params.rb +46 -43
- data/lib/praxis/extensions/attribute_filtering/filters_parser.rb +193 -0
- data/lib/praxis/mapper/resource.rb +2 -2
- data/lib/praxis/media_type_identifier.rb +11 -1
- data/lib/praxis/response_definition.rb +46 -66
- data/lib/praxis/responses/http.rb +3 -1
- data/lib/praxis/tasks/routes.rb +6 -6
- data/lib/praxis/version.rb +1 -1
- data/spec/praxis/action_definition_spec.rb +3 -1
- data/spec/praxis/extensions/attribute_filtering/active_record_filter_query_builder_spec.rb +259 -172
- data/spec/praxis/extensions/attribute_filtering/filter_tree_node_spec.rb +25 -6
- data/spec/praxis/extensions/attribute_filtering/filtering_params_spec.rb +117 -19
- data/spec/praxis/extensions/attribute_filtering/filters_parser_spec.rb +148 -0
- data/spec/praxis/mapper/resource_spec.rb +3 -3
- data/spec/praxis/media_type_identifier_spec.rb +15 -1
- data/spec/praxis/response_definition_spec.rb +37 -129
- data/tasks/thor/templates/generator/example_app/app/v1/concerns/href.rb +33 -0
- data/tasks/thor/templates/generator/example_app/app/v1/resources/base.rb +4 -0
- data/tasks/thor/templates/generator/example_app/config/environment.rb +1 -1
- data/tasks/thor/templates/generator/scaffold/implementation/resources/item.rb +2 -2
- metadata +9 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2fe1ba3515514e34ad80775b6b68b4fe046fd1806c40d2345c4fa945d34ed92a
|
4
|
+
data.tar.gz: 11934130d0c527d1bc52020f393f2a5167ba3f88c876e81deaf7eaa05c518eff
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a32c98bd77159e59c5192390c2eb683b62930cb3f4b30cbe680b63db5b0c076199b0fb63e5a0a55abb772a92cadef19a8f05d7cedf7eb8218f585ce8b7a01acf
|
7
|
+
data.tar.gz: 2a086210825ac166730d63b9ea7e5baa2420cdeb1772e434028b95de356ce69778d983fededb1125825f5b036bc0a0c13fc9b61536d77a31cc63cd87f6a48c02
|
data/.ruby-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
2.
|
1
|
+
2.7.1
|
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,28 @@
|
|
2
2
|
|
3
3
|
## next
|
4
4
|
|
5
|
+
## 2.0.pre.16
|
6
|
+
|
7
|
+
* Updated `Resource.property` signature to only accept known named arguments (`dependencies` and `though` at this time) to spare anyone else from going insane wondering why their `depednencies` aren't working.
|
8
|
+
* Fixed issue with Filtering Params, that occurred with using the ! or !! operators on String-typed fields.
|
9
|
+
|
10
|
+
## 2.0.pre.14
|
11
|
+
|
12
|
+
* More encoding/decoding robustness for filters.
|
13
|
+
* Specs for how to encode filters are now properly defined by:
|
14
|
+
* The "value" of the filters query string needs to be URI encoded (like any other query string value). This encoding is subject to the normal rules, and therefore "could" leave some of the URI unreserved characters (i.e., 'markers') unencoded depending on the client (Section 2.2 of https://tools.ietf.org/html/rfc2396).
|
15
|
+
* The "values" for any of the conditions in the contents of the filters, however, will need to be properly "escaped" as well (prior to URL-encoding the whole syntax string itself like described above). This means that any match value needs to ensure that it has (at least) "(",")","|","&" and "," escaped as they are reserved characters for the filter expression syntax. For example, if I want to search for a name with value "Rocket&(Pants)", I need to first compose the syntax by: "name=<escaped Rocket&(Pants)>, which is "name=Rocket%26%28Pants%29" and then, just URI encode that query string value for the filters parameter in the URL like any other. For example: "filters=name%3DRocket%2526%2528Pants%2529"
|
16
|
+
* When using a multi-match (csv-separated) list of values, you need to escape each of the values as well, leaving the 'comma' unescape, as that's part of the syntax. Then uri-encode it all for the filters query string parameter value like above.
|
17
|
+
* Now, one can properly differentiate between fuzzy query prefix/postfix, and the literal data to search for (which can be or include '*'). Report that multi-matches (i.e., csv separated values for a single field, which translate into "IN" clauses) is not allowed if fuzzy matches are received (need to use multiple OR clauses for it).
|
18
|
+
|
19
|
+
## 2.0.pre.13
|
20
|
+
|
21
|
+
* Fix filters parser regression, which would incorrectly decode url-encoded values
|
22
|
+
|
23
|
+
## 2.0.pre.12
|
24
|
+
|
25
|
+
* Rebuilt API filters to support a much richer syntax. One can now use ANDs and ORs (with ANDs having order precedence), as well as group them with parenthesis. The same individual filter operands are supported. For example: 'email=*@gmail.com&(friends.first_name=Joe*,Patty|friends.last_name=Smith)
|
26
|
+
|
5
27
|
## 2.0.pre.11
|
6
28
|
|
7
29
|
- Remove MapperPlugin's `set_selectors` (made `selector_generator` lazy instead), and ensure it includes the rendering extensions to the Controllers. Less things to configure if you opt into the Mapper way.
|
data/bin/praxis
CHANGED
@@ -9,6 +9,12 @@ rescue Bundler::GemfileNotFound
|
|
9
9
|
# no-op: we might be installed as a system gem
|
10
10
|
end
|
11
11
|
|
12
|
+
if ARGV[0] == "version"
|
13
|
+
require 'praxis/version'
|
14
|
+
puts "Praxis version #{Praxis::VERSION}"
|
15
|
+
exit 0
|
16
|
+
end
|
17
|
+
|
12
18
|
if ["routes","docs","console"].include? ARGV[0]
|
13
19
|
require 'rake'
|
14
20
|
require 'praxis'
|
@@ -97,8 +97,10 @@ module Praxis
|
|
97
97
|
description( description || 'Standard response for successful HTTP requests.' )
|
98
98
|
|
99
99
|
media_type media_type
|
100
|
-
location location
|
101
|
-
headers
|
100
|
+
location if location
|
101
|
+
headers&.each do |(name, value)|
|
102
|
+
header(name: name, value: value)
|
103
|
+
end
|
102
104
|
end
|
103
105
|
|
104
106
|
api.response_template :created do |media_type: nil, location: nil, headers: nil, description: nil|
|
@@ -106,8 +108,10 @@ module Praxis
|
|
106
108
|
description( description || 'The request has been fulfilled and resulted in a new resource being created.' )
|
107
109
|
|
108
110
|
media_type media_type if media_type
|
109
|
-
location location
|
110
|
-
headers
|
111
|
+
location if location
|
112
|
+
headers&.each do |(name, value)|
|
113
|
+
header(name: name, value: value)
|
114
|
+
end
|
111
115
|
end
|
112
116
|
end
|
113
117
|
|
data/lib/praxis/collection.rb
CHANGED
@@ -32,5 +32,16 @@ module Praxis
|
|
32
32
|
@member_type.domain_model
|
33
33
|
end
|
34
34
|
|
35
|
+
def self.json_schema_type
|
36
|
+
:array
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.as_json_schema(**args)
|
40
|
+
the_type = @attribute && @attribute.type || member_type
|
41
|
+
{
|
42
|
+
type: json_schema_type,
|
43
|
+
items: { '$ref': "#/components/schemas/#{the_type.id}" }
|
44
|
+
}
|
45
|
+
end
|
35
46
|
end
|
36
47
|
end
|
@@ -16,12 +16,27 @@ module Praxis
|
|
16
16
|
|
17
17
|
def dump_response_headers_object( headers )
|
18
18
|
headers.each_with_object({}) do |(name,data),accum|
|
19
|
-
#
|
20
|
-
#
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
19
|
+
# each header comes from Praxis::ResponseDefinition
|
20
|
+
# the keys are the header names, and value can be:
|
21
|
+
# "true" => means it only needs to exist
|
22
|
+
# String => which means that it has to fully match
|
23
|
+
# Regex => which means it has to regexp match it
|
24
|
+
|
25
|
+
# Get the schema from the type (defaulting to string in case the type doesn't have the as_json_schema defined)
|
26
|
+
schema = data[:attribute].type.as_json_schema rescue { type: :string }
|
27
|
+
hash = { description: data[:description] || '', schema: schema }
|
28
|
+
# Note, our Headers in response definition are not full types...they're basically only
|
29
|
+
# strings, which can either match anything, match the exact word or match a regex
|
30
|
+
# they don't even have a description...
|
31
|
+
data_value = data[:value]
|
32
|
+
if data_value.is_a? String
|
33
|
+
hash[:pattern] = "^#{data_value}$" # Exact String match
|
34
|
+
elsif data_value.is_a? Regexp
|
35
|
+
sanitized_pattern = data_value.inspect[1..-2] #inspect returns enclosing '/' characters
|
36
|
+
hash[:pattern] = sanitized_pattern
|
37
|
+
end
|
38
|
+
|
39
|
+
accum[name] = hash
|
25
40
|
end
|
26
41
|
end
|
27
42
|
|
@@ -1,2 +1,15 @@
|
|
1
1
|
require 'praxis/extensions/attribute_filtering/filtering_params'
|
2
|
-
require 'praxis/extensions/attribute_filtering/filter_tree_node'
|
2
|
+
require 'praxis/extensions/attribute_filtering/filter_tree_node'
|
3
|
+
module Praxis
|
4
|
+
module Extensions
|
5
|
+
module AttributeFiltering
|
6
|
+
class MultiMatchWithFuzzyNotAllowedByAdapter < StandardError
|
7
|
+
def initialize
|
8
|
+
msg = 'Matching multiple, comma-separated values with fuzzy matches for a single field is not allowed by this DB adapter'\
|
9
|
+
'Please use multiple OR clauses instead.'
|
10
|
+
super(msg)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -23,14 +23,17 @@ module Praxis
|
|
23
23
|
end
|
24
24
|
|
25
25
|
class ActiveRecordFilterQueryBuilder
|
26
|
-
|
26
|
+
REFERENCES_STRING_SEPARATOR = '/'
|
27
|
+
attr_reader :model, :filters_map
|
27
28
|
|
28
29
|
# Base query to build upon
|
29
30
|
def initialize(query: , model:, filters_map:, debug: false)
|
30
|
-
|
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
|
32
|
+
@initial_query = query
|
31
33
|
@model = model
|
32
34
|
@filters_map = filters_map
|
33
35
|
@logger = debug ? Logger.new(STDOUT) : nil
|
36
|
+
@active_record_version_maj = ActiveRecord.gem_version.segments[0]
|
34
37
|
end
|
35
38
|
|
36
39
|
def debug_query(msg, query)
|
@@ -40,26 +43,103 @@ module Praxis
|
|
40
43
|
def generate(filters)
|
41
44
|
# Resolve the names and values first, based on filters_map
|
42
45
|
root_node = _convert_to_treenode(filters)
|
43
|
-
craft_filter_query(root_node, for_model: @model)
|
44
|
-
debug_query("SQL due to filters: ",
|
45
|
-
|
46
|
+
crafted = craft_filter_query(root_node, for_model: @model)
|
47
|
+
debug_query("SQL due to filters: ", crafted.all)
|
48
|
+
crafted
|
46
49
|
end
|
47
50
|
|
48
51
|
def craft_filter_query(nodetree, for_model:)
|
49
52
|
result = _compute_joins_and_conditions_data(nodetree, model: for_model)
|
50
|
-
@
|
53
|
+
return @initial_query if result[:conditions].empty?
|
51
54
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
+
|
56
|
+
# Find the root group (usually an AND group) but can be an OR group, or nil if there's only 1 condition
|
57
|
+
root_parent_group = result[:conditions].first[:node_object].parent_group || result[:conditions].first[:node_object]
|
58
|
+
while root_parent_group.parent_group != nil
|
59
|
+
root_parent_group = root_parent_group.parent_group
|
60
|
+
end
|
61
|
+
|
62
|
+
# Process the joins
|
63
|
+
query_with_joins = result[:associations_hash].empty? ? @initial_query : @initial_query.joins(result[:associations_hash])
|
64
|
+
|
65
|
+
# Proc to apply a single condition
|
66
|
+
apply_single_condition = Proc.new do |condition, associated_query|
|
67
|
+
colo = condition[:model].columns_hash[condition[:name].to_s]
|
55
68
|
column_prefix = condition[:column_prefix]
|
69
|
+
|
70
|
+
# Mark where clause referencing the appropriate alias IF it's not the root table, as there is no association to reference
|
71
|
+
# 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
|
72
|
+
# unmatched reference and eager loads the whole association (it means eager load ALL the things). Not good.
|
73
|
+
unless for_model.table_name == column_prefix
|
74
|
+
associated_query = associated_query.references(build_reference_value(column_prefix, query: associated_query))
|
75
|
+
end
|
76
|
+
self.class.add_clause(
|
77
|
+
query: associated_query,
|
78
|
+
column_prefix: column_prefix,
|
79
|
+
column_object: colo,
|
80
|
+
op: condition[:op],
|
81
|
+
value: condition[:value],
|
82
|
+
fuzzy: condition[:fuzzy]
|
83
|
+
)
|
84
|
+
end
|
56
85
|
|
57
|
-
|
58
|
-
|
86
|
+
if @active_record_version_maj < 6
|
87
|
+
# ActiveRecord < 6 does not support '.and' so no nested things can be done
|
88
|
+
# But we can still support the case of 1+ flat conditions of the same AND/OR type
|
89
|
+
if root_parent_group.is_a?(FilteringParams::Condition)
|
90
|
+
# A Single condition it is easy to handle
|
91
|
+
apply_single_condition.call(result[:conditions].first, query_with_joins)
|
92
|
+
elsif root_parent_group.items.all?{|i| i.is_a?(FilteringParams::Condition)}
|
93
|
+
# Only 1 top level root, with only with simple condition items
|
94
|
+
if root_parent_group.type == :and
|
95
|
+
result[:conditions].reverse.inject(query_with_joins) do |accum, condition|
|
96
|
+
apply_single_condition.call(condition, accum)
|
97
|
+
end
|
98
|
+
else
|
99
|
+
# To do a flat OR, we need to apply the first condition to the incoming query
|
100
|
+
# and then apply any extra ORs to it. Otherwise Book.or(X).or(X) still matches all books
|
101
|
+
cond1, *rest = result[:conditions].reverse
|
102
|
+
start_query = apply_single_condition.call(cond1, query_with_joins)
|
103
|
+
rest.inject(start_query) do |accum, condition|
|
104
|
+
accum.or(apply_single_condition.call(condition, query_with_joins))
|
105
|
+
end
|
106
|
+
end
|
107
|
+
else
|
108
|
+
raise "Mixing AND and OR conditions is not supported for ActiveRecord <6."
|
109
|
+
end
|
110
|
+
else # ActiveRecord 6+
|
111
|
+
# Process the conditions in a depth-first order, and return the resulting query
|
112
|
+
_depth_first_traversal(
|
113
|
+
root_query: query_with_joins,
|
114
|
+
root_node: root_parent_group,
|
115
|
+
conditions: result[:conditions],
|
116
|
+
&apply_single_condition
|
117
|
+
)
|
59
118
|
end
|
60
119
|
end
|
61
120
|
|
62
121
|
private
|
122
|
+
def _depth_first_traversal(root_query:, root_node:, conditions:, &block)
|
123
|
+
# Save the associated query for non-leaves
|
124
|
+
root_node.associated_query = root_query if root_node.is_a?(FilteringParams::ConditionGroup)
|
125
|
+
|
126
|
+
if root_node.is_a?(FilteringParams::Condition)
|
127
|
+
matching_condition = conditions.find {|cond| cond[:node_object] == root_node }
|
128
|
+
|
129
|
+
# The simplified case of a single top level condition (without a wrapping group)
|
130
|
+
# will need to pass the root query itself
|
131
|
+
associated_query = root_node.parent_group ? root_node.parent_group.associated_query : root_query
|
132
|
+
return yield matching_condition, associated_query
|
133
|
+
else
|
134
|
+
first_query, *rest_queries = root_node.items.map do |child|
|
135
|
+
_depth_first_traversal(root_query: root_query, root_node: child, conditions: conditions, &block)
|
136
|
+
end
|
137
|
+
|
138
|
+
rest_queries.each.inject(first_query) do |q, a_query|
|
139
|
+
root_node.type == :and ? q.and(a_query) : q.or(a_query)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
63
143
|
|
64
144
|
def _mapped_filter(name)
|
65
145
|
target = @filters_map[name]
|
@@ -89,7 +169,9 @@ module Praxis
|
|
89
169
|
if mapped_value.is_a?(Proc)
|
90
170
|
result = mapped_value.call(filter)
|
91
171
|
# Result could be an array of hashes (each hash has name/op/value to identify a condition)
|
92
|
-
result.is_a?(Array) ? result : [result]
|
172
|
+
result_from_proc = result.is_a?(Array) ? result : [result]
|
173
|
+
# Make sure we tack on the node object associated with the filter
|
174
|
+
result_from_proc.map{|hash| hash.merge(node_object: filter[:node_object])}
|
93
175
|
else
|
94
176
|
# For non-procs there's only 1 filter and 1 value (we're just overriding the mapped value)
|
95
177
|
[filter.merge( name: mapped_value)]
|
@@ -109,59 +191,58 @@ module Praxis
|
|
109
191
|
h[name] = result[:associations_hash]
|
110
192
|
conditions += result[:conditions]
|
111
193
|
end
|
112
|
-
column_prefix = nodetree.path == [ALIAS_TABLE_PREFIX] ? model.table_name : nodetree.path.join(
|
113
|
-
#column_prefix = nodetree.path == [ALIAS_TABLE_PREFIX] ? nil : nodetree.path.join('/')
|
194
|
+
column_prefix = nodetree.path == [ALIAS_TABLE_PREFIX] ? model.table_name : nodetree.path.join(REFERENCES_STRING_SEPARATOR)
|
114
195
|
nodetree.conditions.each do |condition|
|
115
196
|
conditions += [condition.merge(column_prefix: column_prefix, model: model)]
|
116
197
|
end
|
117
198
|
{associations_hash: h, conditions: conditions}
|
118
199
|
end
|
119
200
|
|
120
|
-
def add_clause(column_prefix:, column_object:, op:, value:)
|
121
|
-
|
122
|
-
likeval = get_like_value(value)
|
201
|
+
def self.add_clause(query:, column_prefix:, column_object:, op:, value:,fuzzy:)
|
202
|
+
likeval = get_like_value(value,fuzzy)
|
123
203
|
case op
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
204
|
+
when '!' # name! means => name IS NOT NULL (and the incoming value is nil)
|
205
|
+
op = '!='
|
206
|
+
value = nil # Enforce it is indeed nil (should be)
|
207
|
+
when '!!'
|
208
|
+
op = '='
|
209
|
+
value = nil # Enforce it is indeed nil (should be)
|
210
|
+
end
|
211
|
+
|
212
|
+
case op
|
213
|
+
when '='
|
214
|
+
if likeval
|
215
|
+
add_safe_where(query: query, tab: column_prefix, col: column_object, op: 'LIKE', value: likeval)
|
216
|
+
else
|
217
|
+
quoted_right = quote_right_part(query: query, value: value, column_object: column_object, negative: false)
|
218
|
+
query.where("#{quote_column_path(query: query, prefix: column_prefix, column_object: column_object)} #{quoted_right}")
|
219
|
+
end
|
220
|
+
when '!='
|
221
|
+
if likeval
|
222
|
+
add_safe_where(query: query, tab: column_prefix, col: column_object, op: 'NOT LIKE', value: likeval)
|
223
|
+
else
|
224
|
+
quoted_right = quote_right_part(query: query, value: value, column_object: column_object, negative: true)
|
225
|
+
query.where("#{quote_column_path(query: query, prefix: column_prefix, column_object: column_object)} #{quoted_right}")
|
130
226
|
end
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
end
|
146
|
-
when '>'
|
147
|
-
add_safe_where(tab: column_prefix, col: column_object, op: '>', value: value)
|
148
|
-
when '<'
|
149
|
-
add_safe_where(tab: column_prefix, col: column_object, op: '<', value: value)
|
150
|
-
when '>='
|
151
|
-
add_safe_where(tab: column_prefix, col: column_object, op: '>=', value: value)
|
152
|
-
when '<='
|
153
|
-
add_safe_where(tab: column_prefix, col: column_object, op: '<=', value: value)
|
154
|
-
else
|
155
|
-
raise "Unsupported Operator!!! #{op}"
|
156
|
-
end
|
157
|
-
end
|
158
|
-
|
159
|
-
def add_safe_where(tab:, col:, op:, value:)
|
227
|
+
when '>'
|
228
|
+
add_safe_where(query: query, tab: column_prefix, col: column_object, op: '>', value: value)
|
229
|
+
when '<'
|
230
|
+
add_safe_where(query: query, tab: column_prefix, col: column_object, op: '<', value: value)
|
231
|
+
when '>='
|
232
|
+
add_safe_where(query: query, tab: column_prefix, col: column_object, op: '>=', value: value)
|
233
|
+
when '<='
|
234
|
+
add_safe_where(query: query, tab: column_prefix, col: column_object, op: '<=', value: value)
|
235
|
+
else
|
236
|
+
raise "Unsupported Operator!!! #{op}"
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
def self.add_safe_where(query:, tab:, col:, op:, value:)
|
160
241
|
quoted_value = query.connection.quote_default_expression(value,col)
|
161
|
-
query.where("#{quote_column_path(tab, col)} #{op} #{quoted_value}")
|
242
|
+
query.where("#{self.quote_column_path(query: query, prefix: tab, column_object: col)} #{op} #{quoted_value}")
|
162
243
|
end
|
163
244
|
|
164
|
-
def quote_column_path(prefix
|
245
|
+
def self.quote_column_path(query:, prefix:, column_object:)
|
165
246
|
c = query.connection
|
166
247
|
quoted_column = c.quote_column_name(column_object.name)
|
167
248
|
if prefix
|
@@ -172,7 +253,7 @@ module Praxis
|
|
172
253
|
end
|
173
254
|
end
|
174
255
|
|
175
|
-
def quote_right_part(value:, column_object:, negative:)
|
256
|
+
def self.quote_right_part(query:, value:, column_object:, negative:)
|
176
257
|
conn = query.connection
|
177
258
|
if value.nil?
|
178
259
|
no = negative ? ' NOT' : ''
|
@@ -190,12 +271,22 @@ module Praxis
|
|
190
271
|
end
|
191
272
|
|
192
273
|
# Returns nil if the value was not a fuzzzy pattern
|
193
|
-
def get_like_value(value)
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
274
|
+
def self.get_like_value(value,fuzzy)
|
275
|
+
is_fuzzy = fuzzy.is_a?(Array) ? !fuzzy.compact.empty? : fuzzy
|
276
|
+
if is_fuzzy
|
277
|
+
unless value.is_a?(String)
|
278
|
+
raise MultiMatchWithFuzzyNotAllowedByAdapter.new
|
279
|
+
end
|
280
|
+
case fuzzy
|
281
|
+
when :start_end
|
282
|
+
'%'+value+'%'
|
283
|
+
when :start
|
284
|
+
'%'+value
|
285
|
+
when :end
|
286
|
+
value+'%'
|
287
|
+
end
|
288
|
+
else
|
289
|
+
nil
|
199
290
|
end
|
200
291
|
end
|
201
292
|
|
@@ -203,14 +294,14 @@ module Praxis
|
|
203
294
|
maj, min, _ = ActiveRecord.gem_version.segments
|
204
295
|
if maj == 5 || (maj == 6 && min == 0)
|
205
296
|
# In AR 6 (and 6.0) the references are simple strings
|
206
|
-
def build_reference_value(column_prefix)
|
297
|
+
def build_reference_value(column_prefix, query: nil)
|
207
298
|
column_prefix
|
208
299
|
end
|
209
300
|
else
|
210
301
|
# The latest AR versions discard passing references to joins when they're not SqlLiterals ... so let's wrap it
|
211
302
|
# with our class, so that it is a literal (already quoted), but that can still provide the expected "symbol" without quotes
|
212
303
|
# so that our aliasing code can match it.
|
213
|
-
def build_reference_value(column_prefix)
|
304
|
+
def build_reference_value(column_prefix, query:)
|
214
305
|
QuasiSqlLiteral.new(quoted: query.connection.quote_table_name(column_prefix), symbolized: column_prefix.to_sym)
|
215
306
|
end
|
216
307
|
end
|