generic_search 1.3.2
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 +7 -0
- data/lib/generic_search/build_methods.rb +297 -0
- data/lib/generic_search/config.rb +4 -0
- data/lib/generic_search/exception.rb +5 -0
- data/lib/generic_search/messages.rb +8 -0
- data/lib/generic_search/rails_overrides.rb +62 -0
- data/lib/generic_search/validation.rb +181 -0
- data/lib/generic_search/version.rb +3 -0
- data/lib/generic_search.rb +201 -0
- metadata +124 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA1:
|
|
3
|
+
metadata.gz: c7ea1f7c8f6d0091ee267fecfc54a5bbf4aaea91
|
|
4
|
+
data.tar.gz: 3594824bb78a70db17620bfffbd0347fd7c9173e
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 00762261f46fd12bb1fe00aaaa9c21247c8846c9948e8b2e773a8b3928df63b1be0e7b9b55a0d6250630d0f05da9d90bd46b0bfee54440825984f4d44a9a1d59
|
|
7
|
+
data.tar.gz: f8bc639f68cedc7aac59fde4d5da387ddc62cf5c611fc22a7446511673c20b41e87644057efdacd6b3c1c07f2253ee90837a6f84baa4c78abb707a2811bab117
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
module GenericSearch
|
|
2
|
+
|
|
3
|
+
#def self.custom_config
|
|
4
|
+
# {
|
|
5
|
+
# :transitions => {
|
|
6
|
+
# :script_status => {
|
|
7
|
+
# :field => :script_status_id,
|
|
8
|
+
# :lambda => lambda do |operator, value|
|
|
9
|
+
# Status.where("name #{operator} ?", value).collect(&:id)
|
|
10
|
+
# end
|
|
11
|
+
# }
|
|
12
|
+
# }
|
|
13
|
+
# }
|
|
14
|
+
#end
|
|
15
|
+
|
|
16
|
+
module BuildMethods
|
|
17
|
+
|
|
18
|
+
def build_where(query)
|
|
19
|
+
|
|
20
|
+
if query.blank?
|
|
21
|
+
return
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
query_table_list = []
|
|
25
|
+
|
|
26
|
+
query.each_line('),') do |segment|
|
|
27
|
+
segment.gsub!(/,$/, "")
|
|
28
|
+
query_table_list << segment
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
query_per_table = Array.new
|
|
32
|
+
|
|
33
|
+
@joins = []
|
|
34
|
+
|
|
35
|
+
query_table_list.each do |query_item|
|
|
36
|
+
|
|
37
|
+
table, search_params = query_item.to_s.split('(', 2)
|
|
38
|
+
table.strip!
|
|
39
|
+
|
|
40
|
+
column_names = []
|
|
41
|
+
|
|
42
|
+
if table.strip != @base_table
|
|
43
|
+
relation_name = @base_class._table_relation[table.strip]
|
|
44
|
+
@joins << relation_name if relation_name
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
search_params ||= ''
|
|
48
|
+
search_params.gsub!(/\)$/, "")
|
|
49
|
+
|
|
50
|
+
single_param_array = search_params.strip.split(/\s*,\s*/)
|
|
51
|
+
single_param_array_length = single_param_array.length
|
|
52
|
+
|
|
53
|
+
column_values = []
|
|
54
|
+
current_value = single_param_array[0]
|
|
55
|
+
i=0
|
|
56
|
+
|
|
57
|
+
until i >= single_param_array_length
|
|
58
|
+
|
|
59
|
+
next_value = single_param_array[i + 1]
|
|
60
|
+
|
|
61
|
+
if next_value.nil? or next_value.match(/([<>=!~]|between)/)
|
|
62
|
+
column_values << current_value
|
|
63
|
+
current_value = next_value
|
|
64
|
+
else
|
|
65
|
+
current_value << (',' + next_value)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
i = i + 1
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
column_values.each do |search_param|
|
|
72
|
+
search_param.gsub!(/,$/, "")
|
|
73
|
+
|
|
74
|
+
if search_param.match(/(\w+)\s+between\s+(\*?.*)and(\*?.*)/i)
|
|
75
|
+
key, from, to = $1.strip, $2.strip, $3.strip
|
|
76
|
+
|
|
77
|
+
single_search_param = if from.match(/^\d+$/) and to.match(/^\d+$/)
|
|
78
|
+
"\"#{table}\".\"#{key}\" between #{from} and #{to}"
|
|
79
|
+
else
|
|
80
|
+
"\"#{table}\".\"#{key}\" between '#{from.to_time.to_s(:db)}' and '#{to.to_time.to_s(:db)}'"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
else
|
|
84
|
+
search_param.match(/(\w+)(\s*[<>=!~]*\s*)(\*?.*)/)
|
|
85
|
+
key, operator, value = $1.strip, $2.strip, $3.strip
|
|
86
|
+
|
|
87
|
+
if operator == '='
|
|
88
|
+
if value.include? '*'
|
|
89
|
+
value.gsub!('*', '%')
|
|
90
|
+
value = "#{value}"
|
|
91
|
+
operator = 'ILIKE'
|
|
92
|
+
#elsif value.include? '/'
|
|
93
|
+
# value = "(#{value})"
|
|
94
|
+
# operator = 'in'
|
|
95
|
+
#elsif value.include? '|'
|
|
96
|
+
# value = "(#{value})"
|
|
97
|
+
# operator = 'in'
|
|
98
|
+
#else
|
|
99
|
+
# value = "'#{value}'"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
else
|
|
103
|
+
value = "#{value}"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
config = GenericSearch.config[table.intern]
|
|
107
|
+
config = (config and config[:for_where])
|
|
108
|
+
key_intern = key.intern
|
|
109
|
+
|
|
110
|
+
#TODO: Test Requires
|
|
111
|
+
single_search_param = if config and config.has_key?(key_intern)
|
|
112
|
+
config = config[key_intern]
|
|
113
|
+
value = config[:lambda].call(operator, value)
|
|
114
|
+
#subQuery = value.is_a?(Array) ? "in (#{value.blank? ? 'NULL' : value.join(',')})" : "= '#{value}'"
|
|
115
|
+
subQuery = if value.is_a?(Array)
|
|
116
|
+
if value.blank?
|
|
117
|
+
"in (NULL)"
|
|
118
|
+
else
|
|
119
|
+
"in (#{value.join(',')})"
|
|
120
|
+
end
|
|
121
|
+
else
|
|
122
|
+
"= '#{value}'"
|
|
123
|
+
end
|
|
124
|
+
"\"#{table}\".\"#{config[:field]}\" #{subQuery}"
|
|
125
|
+
elsif operator == '=' and value.include?(',') or value.include?('|')
|
|
126
|
+
|
|
127
|
+
value = value.gsub(/\s*,\s*/, ' AND ').gsub(/\s*\|\s*/, ' OR ').split(/(AND|OR)/)
|
|
128
|
+
.collect do |val|
|
|
129
|
+
val.match(/(AND|OR)/) ? val : "\"#{table}\".\"#{key}\" #{operator} '#{val.strip}'"
|
|
130
|
+
end.join(' ')
|
|
131
|
+
|
|
132
|
+
"(#{value})"
|
|
133
|
+
|
|
134
|
+
else
|
|
135
|
+
"\"#{table}\".\"#{key}\" #{operator} '#{value}'"
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
column_names << key
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
query_per_table << single_search_param
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
self.validate_columns(table, column_names, :query)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
self.where = query_per_table.join(' AND ')
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def build_results(results)
|
|
151
|
+
table_selected_fields = {}
|
|
152
|
+
|
|
153
|
+
selects = if results.blank?
|
|
154
|
+
[self.base_class.table_name]
|
|
155
|
+
else
|
|
156
|
+
selects = results.scan(/\w+\(.*?\)/)
|
|
157
|
+
results_clone = results.clone
|
|
158
|
+
|
|
159
|
+
selects.each do |select|
|
|
160
|
+
results_clone.gsub!(select, '')
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
selects += results_clone.split(/,/).delete_if(&:blank?)
|
|
164
|
+
selects
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
selects.each do |select|
|
|
168
|
+
match, table, select_fields = *select.match(/(\w+)\(*([\s\w,]*)\)*/)
|
|
169
|
+
table_selected_fields[table] = select_fields.blank? ? nil : select_fields.strip.split(/\s*,\s*/)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
table_selected_fields.each do |table_name, column_names|
|
|
173
|
+
validate_columns(table_name, column_names, :results)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
#TODO: self.select = table_selected_fields.clone
|
|
177
|
+
self.select = table_selected_fields
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def build_group(group)
|
|
181
|
+
|
|
182
|
+
if group.blank?
|
|
183
|
+
return
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
group_entities = group.scan(/\w+\(.*?\)/)
|
|
187
|
+
|
|
188
|
+
@joins ||= []
|
|
189
|
+
group_table_fields_map = {}
|
|
190
|
+
@group = []
|
|
191
|
+
@group_object_access = []
|
|
192
|
+
|
|
193
|
+
group_entities.each do |group_entity|
|
|
194
|
+
match, table, select_fields = *group_entity.match(/(\w+)\(*([\s\w,]*)\)*/)
|
|
195
|
+
table.strip!
|
|
196
|
+
fields = select_fields.split(/\s*,\s*/)
|
|
197
|
+
group_table_fields_map[table] = fields
|
|
198
|
+
|
|
199
|
+
#@joins << @base_class._table_relation[table.strip] if table != @base_class.table_name
|
|
200
|
+
|
|
201
|
+
table.strip!
|
|
202
|
+
if table != @base_table
|
|
203
|
+
relation_name = (@base_class._table_relation[table] || (@base_class._relation_table[table.intern] ? table.intern : nil))
|
|
204
|
+
table_name = @base_class._table_relation[table] ? table : @base_class._relation_table[table.intern]
|
|
205
|
+
@joins << relation_name if relation_name
|
|
206
|
+
else
|
|
207
|
+
table_name = table
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
fields.each do |field|
|
|
211
|
+
@group << "#{table_name}.#{field}"
|
|
212
|
+
#@group_object_access << (@base_table == table ? field : "#{@base_class._table_relation[table]}.#{field}")
|
|
213
|
+
@group_object_access << (@base_table == table_name ? field : "#{relation_name}.#{field}")
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
group_table_fields_map.each do |table, column_names|
|
|
218
|
+
validate_columns(table, column_names, :group)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def build_sort_order(sort_order)
|
|
225
|
+
self.sort_order = if sort_order.nil?
|
|
226
|
+
"\"#{self.base_table}\".\"id\" ASC"
|
|
227
|
+
elsif sort_order.strip == "\"\"" || sort_order.strip == "\'\'" || sort_order.strip == ''
|
|
228
|
+
nil
|
|
229
|
+
else
|
|
230
|
+
sort_order.strip
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def build_limit(limit)
|
|
235
|
+
#TODO: Test required for group.blank?
|
|
236
|
+
if self.options[:no_results] or !self.group.blank?
|
|
237
|
+
self.limit = nil
|
|
238
|
+
else
|
|
239
|
+
self.limit = limit.blank? ? GenericSearch::DEFAULT_LIMIT : limit
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def build_start(start)
|
|
245
|
+
#TODO: Test required for group.blank?
|
|
246
|
+
self.start = if self.options[:no_results] or !self.group.blank?
|
|
247
|
+
nil
|
|
248
|
+
else
|
|
249
|
+
start.blank? ? 0 : start
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# options = no_results, no_limit_count
|
|
254
|
+
def build_options(options)
|
|
255
|
+
|
|
256
|
+
self.options = {}
|
|
257
|
+
|
|
258
|
+
return if options.blank?
|
|
259
|
+
|
|
260
|
+
if options.include? 'no_results'
|
|
261
|
+
self.options[:no_results] = true
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
if options.include? 'no_limit_count'
|
|
265
|
+
self.options[:no_limit_count] = true
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
if options.include? 'distinct'
|
|
269
|
+
self.options[:distinct] = true
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def build_response
|
|
275
|
+
|
|
276
|
+
self.response = {
|
|
277
|
+
:success => self.status == :ok ? true : false,
|
|
278
|
+
:code => self.status == :ok ? 200 : 400,
|
|
279
|
+
:status => self.status,
|
|
280
|
+
:message => self.messages,
|
|
281
|
+
:client_IP => nil,
|
|
282
|
+
:controller => nil,
|
|
283
|
+
|
|
284
|
+
:server => Socket.gethostname,
|
|
285
|
+
:where_clause => self.where,
|
|
286
|
+
:no_limit_result_count => self.no_limit_count,
|
|
287
|
+
:limit_result_count => self.limit_result_count,
|
|
288
|
+
:timestamp => Time.now,
|
|
289
|
+
:groups => self.grouped_results
|
|
290
|
+
#"groups" => params.has_key?(:no_results) ? [] : @grouped_results
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
end
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
module GenericSearch
|
|
2
|
+
module Messages
|
|
3
|
+
DoubleCloseParan = 'malformed url - double close paren'
|
|
4
|
+
IncorrectUseOfParan = 'malformed url - incorrect use of parentheses'
|
|
5
|
+
DoubleOpenParan = 'malformed url - double open paren'
|
|
6
|
+
DoubleComma = "malformed url - double comma"
|
|
7
|
+
end
|
|
8
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
class ActiveRecord::Base
|
|
2
|
+
|
|
3
|
+
def _generic_search(args)
|
|
4
|
+
if args.is_a? HashWithIndifferentAccess or args.is_a? Hash
|
|
5
|
+
generic_search = GenericSearch::Klass.new(args, self.class)
|
|
6
|
+
generic_search.search
|
|
7
|
+
elsif args.is_a? GenericSearch
|
|
8
|
+
|
|
9
|
+
else
|
|
10
|
+
raise UnknownInputType
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.generic_search config
|
|
16
|
+
GenericSearch.update_config(self.table_name, config[:custom_attributes])
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self._table_relation
|
|
20
|
+
#@@table_relation ||= begin
|
|
21
|
+
# puts "processing..."
|
|
22
|
+
self.reflect_on_all_associations.inject({}) do |hash, assoc_reflection|
|
|
23
|
+
hash[assoc_reflection.table_name] ||= assoc_reflection.name
|
|
24
|
+
hash
|
|
25
|
+
end
|
|
26
|
+
#end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self._relation_table
|
|
30
|
+
#@@relation_table ||= begin
|
|
31
|
+
#puts "processing..."
|
|
32
|
+
self.reflect_on_all_associations.inject({}) do |hash, assoc_reflection|
|
|
33
|
+
hash[assoc_reflection.name] ||= assoc_reflection.table_name
|
|
34
|
+
hash
|
|
35
|
+
end
|
|
36
|
+
#end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
#class ApplicationController
|
|
42
|
+
#
|
|
43
|
+
# def _generic_search
|
|
44
|
+
# base_class = self.class.to_s.gsub('Controller', '').singularize.constantize
|
|
45
|
+
# generic_search = GenericSearch::Klass.new(params, base_class)
|
|
46
|
+
# generic_search.search
|
|
47
|
+
# response = generic_search.response
|
|
48
|
+
#
|
|
49
|
+
# response[:client_IP] = self.request.headers["X-Cluster-Client-Ip"]
|
|
50
|
+
# response[:controller] = controller_name.classify.to_s
|
|
51
|
+
#
|
|
52
|
+
# render :json => generic_search.response
|
|
53
|
+
# end
|
|
54
|
+
#
|
|
55
|
+
# # Restrictions to the controller
|
|
56
|
+
# # generic_search_config(false) # to disable the generic search in a controller
|
|
57
|
+
# # generic_search_config(:model_class => Script) # to disable the generic search in a controller
|
|
58
|
+
# def self.generic_search_config(restrictions)
|
|
59
|
+
# # Restriction logic for controller
|
|
60
|
+
# end
|
|
61
|
+
#
|
|
62
|
+
#end
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
module GenericSearch
|
|
2
|
+
|
|
3
|
+
module Validation
|
|
4
|
+
|
|
5
|
+
def validate
|
|
6
|
+
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def validate_syntax
|
|
10
|
+
|
|
11
|
+
[params[:query], params[:results], params[:group]].each do |query_string|
|
|
12
|
+
|
|
13
|
+
next if query_string.blank?
|
|
14
|
+
|
|
15
|
+
#if query_string.include?(',,')
|
|
16
|
+
if query_string.match(/\s*\,\s*\,\s*/)
|
|
17
|
+
@status = :bad_request
|
|
18
|
+
#@message = "malformed url - double comma"
|
|
19
|
+
@message = GenericSearch::Messages::DoubleComma
|
|
20
|
+
self.errors.add(:base, @message)
|
|
21
|
+
#return false
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
#if query_string.include?('((')
|
|
25
|
+
if query_string.match(/\s*\(\s*\(\s*/)
|
|
26
|
+
@status = :bad_request
|
|
27
|
+
@message = GenericSearch::Messages::DoubleOpenParan
|
|
28
|
+
self.errors.add(:base, @message)
|
|
29
|
+
#return false
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# To check ))
|
|
33
|
+
if query_string.match(/\s*\)\s*\)\s*/)
|
|
34
|
+
@status = :bad_request
|
|
35
|
+
@message = GenericSearch::Messages::DoubleCloseParan
|
|
36
|
+
self.errors.add(:base, @message)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
if query_string.count("()") % 2 == 1
|
|
40
|
+
@status = :bad_request
|
|
41
|
+
@message = GenericSearch::Messages::IncorrectUseOfParan
|
|
42
|
+
self.errors.add(:base, @message)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
#if type.eql?("where_clause")
|
|
47
|
+
# puts "verifying specific legal query_strings for where_clause"
|
|
48
|
+
# if !query_string.include?('(')
|
|
49
|
+
# @status = :bad_request
|
|
50
|
+
# @message = "malformed url - no query arguments"
|
|
51
|
+
# return false
|
|
52
|
+
# end
|
|
53
|
+
#end
|
|
54
|
+
|
|
55
|
+
#return true
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
#def validate_table_columns
|
|
59
|
+
#
|
|
60
|
+
# #self.validate_query
|
|
61
|
+
#
|
|
62
|
+
#end
|
|
63
|
+
|
|
64
|
+
def validate_columns(table_name, column_names, clause)
|
|
65
|
+
|
|
66
|
+
connection = ActiveRecord::Base.connection
|
|
67
|
+
config = GenericSearch.config[table_name.intern]
|
|
68
|
+
config_for_where = (config and config[:for_where]) || {}
|
|
69
|
+
config_for_select = (config and config[:for_select]) || {}
|
|
70
|
+
|
|
71
|
+
if !connection.table_exists?(table_name) and !self.base_class.reflections.has_key?(table_name)
|
|
72
|
+
self.errors.add(:base, "'#{table_name}' table is invalid (in '#{clause}')")
|
|
73
|
+
return
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
if !connection.table_exists?(table_name)
|
|
77
|
+
if !self.base_class.reflections.has_key?(table_name)
|
|
78
|
+
self.errors.add(:base, "'#{table_name}' table is invalid (in '#{clause}')")
|
|
79
|
+
return
|
|
80
|
+
else
|
|
81
|
+
table_name = self.base_class._relation_table[table_name.intern]
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
return if column_names.blank?
|
|
86
|
+
|
|
87
|
+
unknown_columns = []
|
|
88
|
+
|
|
89
|
+
column_names.each do |column_name|
|
|
90
|
+
next if config_for_select and config_for_select.has_key?(column_name.intern)
|
|
91
|
+
next if config_for_where and config_for_where.has_key?(column_name.intern)
|
|
92
|
+
|
|
93
|
+
if !connection.column_exists?(table_name, column_name) and !config_for_select.has_key?(column_name.intern) and !config_for_where.has_key?(column_name.intern)
|
|
94
|
+
unknown_columns << column_name
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
if !unknown_columns.blank?
|
|
99
|
+
self.errors.add(:base, "'#{unknown_columns.join(', ')}' column#{unknown_columns.size > 1 ? 's' : ''} not found in '#{table_name}' table (in '#{clause}')")
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def valid_syntax?(query_string)
|
|
105
|
+
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def validate_query
|
|
109
|
+
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def validate_results
|
|
113
|
+
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def validate_group
|
|
117
|
+
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def validate_sort_order
|
|
121
|
+
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def validate_limit
|
|
125
|
+
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
#def validate_columns(table_name, column_names, clause)
|
|
129
|
+
#
|
|
130
|
+
# connection = ActiveRecord::Base.connection
|
|
131
|
+
#
|
|
132
|
+
# if !connection.table_exists?(table_name) and !self.base_class.reflections.has_key?(table_name.intern)
|
|
133
|
+
# @_uri_errors << "'#{table_name}' is invalid in '#{clause}'"
|
|
134
|
+
# return
|
|
135
|
+
# end
|
|
136
|
+
#
|
|
137
|
+
# if !connection.table_exists?(table_name)
|
|
138
|
+
# if !self.base_class.reflections.has_key?(table_name.intern)
|
|
139
|
+
# @_uri_errors << "'#{table_name}' is invalid in '#{clause}'"
|
|
140
|
+
# return
|
|
141
|
+
# else
|
|
142
|
+
# table_name = self.base_class._relation_table[table_name.intern]
|
|
143
|
+
# end
|
|
144
|
+
# end
|
|
145
|
+
#
|
|
146
|
+
# return if column_names.blank?
|
|
147
|
+
#
|
|
148
|
+
# unknown_columns = []
|
|
149
|
+
#
|
|
150
|
+
# config = GenericSearchMethods.config[table_name.intern]
|
|
151
|
+
# column_names.each do |column_name|
|
|
152
|
+
#
|
|
153
|
+
# next if config and config.has_key?(column_name.intern)
|
|
154
|
+
#
|
|
155
|
+
# if !connection.column_exists?(table_name, column_name)
|
|
156
|
+
# unknown_columns << column_name
|
|
157
|
+
# end
|
|
158
|
+
# end
|
|
159
|
+
#
|
|
160
|
+
# if !unknown_columns.blank?
|
|
161
|
+
# @_uri_errors << "'#{unknown_columns.join(', ')}' column#{unknown_columns.size > 1 ? 's' : ''} not found in '#{table_name}' table in '#{clause}'"
|
|
162
|
+
# end
|
|
163
|
+
#
|
|
164
|
+
#end
|
|
165
|
+
|
|
166
|
+
def validate_table_columns(table, columns)
|
|
167
|
+
#connection = ActiveRecord::Base.connection
|
|
168
|
+
|
|
169
|
+
#if connection.table_exists? table
|
|
170
|
+
#
|
|
171
|
+
#end
|
|
172
|
+
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def validate_uri_syntax
|
|
176
|
+
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
end
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
require 'active_record'
|
|
2
|
+
require 'generic_search/validation'
|
|
3
|
+
require 'generic_search/build_methods'
|
|
4
|
+
require 'generic_search/config'
|
|
5
|
+
require 'generic_search/exception'
|
|
6
|
+
require 'generic_search/rails_overrides'
|
|
7
|
+
require 'generic_search/validation'
|
|
8
|
+
require 'generic_search/messages'
|
|
9
|
+
|
|
10
|
+
module GenericSearch
|
|
11
|
+
|
|
12
|
+
@@generic_search = {
|
|
13
|
+
:config => {}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
def self.update_config(class_name, config)
|
|
17
|
+
@@generic_search[:config][class_name.intern] = config
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.config
|
|
21
|
+
@@generic_search[:config]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
class Klass
|
|
25
|
+
include ActiveModel::Validations
|
|
26
|
+
include GenericSearch::Validation
|
|
27
|
+
include GenericSearch::BuildMethods
|
|
28
|
+
|
|
29
|
+
# Clause attributes
|
|
30
|
+
attr_accessor :where, :includes, :joins, :select, :group, :limit, :start, :sort_order, :options
|
|
31
|
+
|
|
32
|
+
# Utility attributes
|
|
33
|
+
attr_accessor :base_class, :base_table, :grouped_results, :params
|
|
34
|
+
|
|
35
|
+
# Output attributes
|
|
36
|
+
attr_accessor :status, :messages, :response, :results, :no_limit_count, :limit_result_count
|
|
37
|
+
|
|
38
|
+
validate :validate_syntax
|
|
39
|
+
|
|
40
|
+
# ===== To configure generic search in model =====
|
|
41
|
+
#generic_search_config {
|
|
42
|
+
# table_alias: {
|
|
43
|
+
# transitions: transition
|
|
44
|
+
# }
|
|
45
|
+
#}
|
|
46
|
+
# ===== To configure generic search in model =====
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# ==== Hash Inputs: ===
|
|
50
|
+
#{
|
|
51
|
+
# :query => "responsibles(username=ksmanoj)",
|
|
52
|
+
# :results => "scripts(id, name), responsibles",
|
|
53
|
+
# :limit => 5,
|
|
54
|
+
# :options => "no_results"
|
|
55
|
+
#}
|
|
56
|
+
def initialize(params, base_class)
|
|
57
|
+
|
|
58
|
+
self.base_class = base_class.is_a?(String) ? base_class.constantize : base_class
|
|
59
|
+
self.base_table = self.base_class.table_name
|
|
60
|
+
|
|
61
|
+
unless params.is_a?(Hash) or params.is_a?(HashWithIndifferentAccess)
|
|
62
|
+
raise GenericSearch::UnknownInputType
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
self.params = params
|
|
66
|
+
|
|
67
|
+
self.validate_syntax
|
|
68
|
+
|
|
69
|
+
return unless self.errors.blank?
|
|
70
|
+
|
|
71
|
+
self.build_where(params[:query])
|
|
72
|
+
self.build_results(params[:results])
|
|
73
|
+
self.build_group(params[:group])
|
|
74
|
+
self.build_options(params[:options])
|
|
75
|
+
self.build_limit(params[:limit])
|
|
76
|
+
self.build_start(params[:start])
|
|
77
|
+
self.build_sort_order(params[:sort_order])
|
|
78
|
+
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def search
|
|
82
|
+
|
|
83
|
+
unless self.errors.blank?
|
|
84
|
+
self.status = :bad_request
|
|
85
|
+
self.messages = self.errors.full_messages
|
|
86
|
+
self.build_response
|
|
87
|
+
return self.response
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
table_relation_map = self.base_class._table_relation
|
|
91
|
+
relation_table_map = self.base_class._relation_table
|
|
92
|
+
|
|
93
|
+
includes = Hash.new
|
|
94
|
+
json_includes = Hash.new
|
|
95
|
+
|
|
96
|
+
self.select.each do |table, value|
|
|
97
|
+
table = table.strip
|
|
98
|
+
relationship_name = table_relation_map[table]
|
|
99
|
+
relationship_name = table.intern if !relationship_name or relation_table_map[table.intern]
|
|
100
|
+
includes[relationship_name] = {} if table != self.base_table
|
|
101
|
+
json_includes[relationship_name] = {:only => self.select[table]} if table != self.base_table
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# TODO: Solve n+1 issue for custom columns
|
|
105
|
+
#config = GenericSearch.config[self.base_table.intern][:for_select]
|
|
106
|
+
#selected_fields = table_selected_fields[self.base_table]
|
|
107
|
+
#
|
|
108
|
+
#config.each do |custom_column, attrs|
|
|
109
|
+
# if attrs[:include] and selected_fields.include?(custom_column.to_s)
|
|
110
|
+
# selected_fields << attrs[:include]
|
|
111
|
+
# end
|
|
112
|
+
#end
|
|
113
|
+
#
|
|
114
|
+
|
|
115
|
+
self.results = self.base_class.where(self.where).joins(self.joins).includes(self.includes).order(self.sort_order)
|
|
116
|
+
|
|
117
|
+
#result_list = @model_class.joins(@joins).includes(includes).where(@where_clause).distinct
|
|
118
|
+
|
|
119
|
+
# TODO: Test required
|
|
120
|
+
if self.options[:distinct]
|
|
121
|
+
self.results = self.results.uniq
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# ========== Limiting & Grouping =========
|
|
125
|
+
# TODO: If there is a limit then only following count query has to be executed otherwise use .length
|
|
126
|
+
if self.options[:no_limit_count]
|
|
127
|
+
# Note: If no limit or start, then .length will fire the query, from that length is calculated.
|
|
128
|
+
# Additional count query is not required
|
|
129
|
+
self.no_limit_count = (self.limit || self.start) ? self.results.count : self.results.length
|
|
130
|
+
#self.no_limit_count = self.results.count
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# TODO: Test required
|
|
134
|
+
unless self.options[:no_results]
|
|
135
|
+
self.results = self.results.limit(self.limit).offset(self.start)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# TODO: Test required
|
|
139
|
+
if self.group
|
|
140
|
+
group_result_counts = self.results.group(self.group).count
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# ============== Group result processing
|
|
144
|
+
# TODO: Test required
|
|
145
|
+
if group_result_counts
|
|
146
|
+
# Hack to dynamically group the array of objects
|
|
147
|
+
eval_str = @group_object_access.collect { |i| "result.#{i}.to_s" }.join(' + ')
|
|
148
|
+
grouped_result = self.results.group_by { |result| eval eval_str }
|
|
149
|
+
|
|
150
|
+
group_hsh = group_result_counts.collect do |values, group_result_count|
|
|
151
|
+
|
|
152
|
+
values = values.is_a?(Array) ? values.collect(&:to_s) : [values.to_s]
|
|
153
|
+
result_key = values.join('')
|
|
154
|
+
|
|
155
|
+
results = if self.options[:no_results]
|
|
156
|
+
[]
|
|
157
|
+
else
|
|
158
|
+
#grouped_result[result_key].as_json(:include => json_includes, :only => @table_selected_fields[self.base_table])
|
|
159
|
+
grouped_result[result_key].as_json(:include => json_includes, :only => self.select[self.base_table], :methods => self.select[self.base_table])
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
{
|
|
163
|
+
:group_by_field => self.group.join(', '),
|
|
164
|
+
:group_by_value => values.join(','),
|
|
165
|
+
:num_results => group_result_count,
|
|
166
|
+
#:results => grouped_result[result_key]
|
|
167
|
+
:results => results
|
|
168
|
+
}
|
|
169
|
+
end
|
|
170
|
+
else
|
|
171
|
+
|
|
172
|
+
results = if self.options[:no_results]
|
|
173
|
+
[]
|
|
174
|
+
else
|
|
175
|
+
|
|
176
|
+
#self.results.as_json(:include => json_includes, :only => @table_selected_fields[self.base_table])
|
|
177
|
+
#self.results.as_json(:include => json_includes, :only => self.select[self.base_table], :methods => [:address_ids])
|
|
178
|
+
self.results.as_json(:include => json_includes, :only => self.select[self.base_table], :methods => self.select[self.base_table])
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
group_hsh = [{:num_results => self.results.length, :results => results}]
|
|
182
|
+
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
self.grouped_results = group_hsh
|
|
186
|
+
|
|
187
|
+
limit_result_count = 0
|
|
188
|
+
|
|
189
|
+
self.grouped_results.each do |grouped_result|
|
|
190
|
+
#limit_result_count += grouped_result["results"].count
|
|
191
|
+
limit_result_count += grouped_result[:num_results]
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
self.limit_result_count = limit_result_count
|
|
195
|
+
self.status = :ok
|
|
196
|
+
self.build_response
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: generic_search
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.3.2
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Justin Hayes
|
|
8
|
+
- Manoj
|
|
9
|
+
autorequire:
|
|
10
|
+
bindir: exe
|
|
11
|
+
cert_chain: []
|
|
12
|
+
date: 2018-02-12 00:00:00.000000000 Z
|
|
13
|
+
dependencies:
|
|
14
|
+
- !ruby/object:Gem::Dependency
|
|
15
|
+
name: bundler
|
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
|
17
|
+
requirements:
|
|
18
|
+
- - ">="
|
|
19
|
+
- !ruby/object:Gem::Version
|
|
20
|
+
version: '0'
|
|
21
|
+
type: :development
|
|
22
|
+
prerelease: false
|
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
24
|
+
requirements:
|
|
25
|
+
- - ">="
|
|
26
|
+
- !ruby/object:Gem::Version
|
|
27
|
+
version: '0'
|
|
28
|
+
- !ruby/object:Gem::Dependency
|
|
29
|
+
name: rake
|
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
|
31
|
+
requirements:
|
|
32
|
+
- - ">="
|
|
33
|
+
- !ruby/object:Gem::Version
|
|
34
|
+
version: '0'
|
|
35
|
+
type: :development
|
|
36
|
+
prerelease: false
|
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
38
|
+
requirements:
|
|
39
|
+
- - ">="
|
|
40
|
+
- !ruby/object:Gem::Version
|
|
41
|
+
version: '0'
|
|
42
|
+
- !ruby/object:Gem::Dependency
|
|
43
|
+
name: rspec
|
|
44
|
+
requirement: !ruby/object:Gem::Requirement
|
|
45
|
+
requirements:
|
|
46
|
+
- - ">="
|
|
47
|
+
- !ruby/object:Gem::Version
|
|
48
|
+
version: '0'
|
|
49
|
+
type: :development
|
|
50
|
+
prerelease: false
|
|
51
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
52
|
+
requirements:
|
|
53
|
+
- - ">="
|
|
54
|
+
- !ruby/object:Gem::Version
|
|
55
|
+
version: '0'
|
|
56
|
+
- !ruby/object:Gem::Dependency
|
|
57
|
+
name: pry
|
|
58
|
+
requirement: !ruby/object:Gem::Requirement
|
|
59
|
+
requirements:
|
|
60
|
+
- - ">="
|
|
61
|
+
- !ruby/object:Gem::Version
|
|
62
|
+
version: '0'
|
|
63
|
+
type: :development
|
|
64
|
+
prerelease: false
|
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
66
|
+
requirements:
|
|
67
|
+
- - ">="
|
|
68
|
+
- !ruby/object:Gem::Version
|
|
69
|
+
version: '0'
|
|
70
|
+
- !ruby/object:Gem::Dependency
|
|
71
|
+
name: activerecord
|
|
72
|
+
requirement: !ruby/object:Gem::Requirement
|
|
73
|
+
requirements:
|
|
74
|
+
- - ">="
|
|
75
|
+
- !ruby/object:Gem::Version
|
|
76
|
+
version: '0'
|
|
77
|
+
type: :runtime
|
|
78
|
+
prerelease: false
|
|
79
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
80
|
+
requirements:
|
|
81
|
+
- - ">="
|
|
82
|
+
- !ruby/object:Gem::Version
|
|
83
|
+
version: '0'
|
|
84
|
+
description: Search any data from database without writing any code.
|
|
85
|
+
email:
|
|
86
|
+
- jhayes@juniper.net
|
|
87
|
+
- ksmanoj@juniper.net
|
|
88
|
+
executables: []
|
|
89
|
+
extensions: []
|
|
90
|
+
extra_rdoc_files: []
|
|
91
|
+
files:
|
|
92
|
+
- lib/generic_search.rb
|
|
93
|
+
- lib/generic_search/build_methods.rb
|
|
94
|
+
- lib/generic_search/config.rb
|
|
95
|
+
- lib/generic_search/exception.rb
|
|
96
|
+
- lib/generic_search/messages.rb
|
|
97
|
+
- lib/generic_search/rails_overrides.rb
|
|
98
|
+
- lib/generic_search/validation.rb
|
|
99
|
+
- lib/generic_search/version.rb
|
|
100
|
+
homepage: https://rubygems.org/bethink/generic_search
|
|
101
|
+
licenses:
|
|
102
|
+
- MIT
|
|
103
|
+
metadata: {}
|
|
104
|
+
post_install_message:
|
|
105
|
+
rdoc_options: []
|
|
106
|
+
require_paths:
|
|
107
|
+
- lib
|
|
108
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
109
|
+
requirements:
|
|
110
|
+
- - ">="
|
|
111
|
+
- !ruby/object:Gem::Version
|
|
112
|
+
version: '0'
|
|
113
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
114
|
+
requirements:
|
|
115
|
+
- - ">="
|
|
116
|
+
- !ruby/object:Gem::Version
|
|
117
|
+
version: '0'
|
|
118
|
+
requirements: []
|
|
119
|
+
rubyforge_project:
|
|
120
|
+
rubygems_version: 2.4.5.1
|
|
121
|
+
signing_key:
|
|
122
|
+
specification_version: 4
|
|
123
|
+
summary: Search any data from database without writing any code.
|
|
124
|
+
test_files: []
|