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: []
|