activerecord-filter 1.0.0.alpha

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: bf2f420a770b315c9c5447527d2f54b0f3345e3f
4
+ data.tar.gz: 1bf8eb361be7b60ffddf0ca081a88dd8cb0b485c
5
+ SHA512:
6
+ metadata.gz: acdd1fdab608afd088bdc97a71f1f2cfa5cc22066790ec32d1edba9007264ef5e2307a48bce8b157c2283c1feffc527e6a531928219e0cb9dc2c8790b8d616f4
7
+ data.tar.gz: 908e00ea9062f1157be09479244c4ec401df5d836017e3a6acba87451649c9438171ca3f1c7ca06cadc015c5d8910860627ee607b89c7ce73c9d4cea0777559e
data/README.md ADDED
@@ -0,0 +1,2 @@
1
+ # activerecord-filter
2
+ A safe way to accept user parameters and query against your ActiveRecord Models
@@ -0,0 +1,251 @@
1
+ class ActiveRecord::Base
2
+ class << self
3
+
4
+ def inherited_with_filter(subclass)
5
+ inherited_without_filter(subclass)
6
+ subclass.instance_variable_set('@filters', HashWithIndifferentAccess.new)
7
+ end
8
+ alias_method_chain :inherited, :filter
9
+
10
+ def filter_on(name, lambda)
11
+ @filters[name] = lambda
12
+ end
13
+
14
+ def filter(filters, options={})
15
+ resource = all
16
+ return resource unless filters
17
+
18
+ if filters.is_a? Hash
19
+ filters.each do |key, value|
20
+ if @filters[key]
21
+ #TODO add test for this... not sure how rails does this lambda call,
22
+ # do they run it in a context for merge?
23
+ resource = resource.merge( @filters[key].call(value) )
24
+ else
25
+ resource = resource.filter_for(key, value, options)
26
+ end
27
+ end
28
+ elsif filters.is_a?(Array) || filters.is_a?(Integer)
29
+ resource = resource.filter_for(:id, filters, options)
30
+ end
31
+
32
+ resource
33
+ end
34
+
35
+ def filter_for(key, value, options={})
36
+ column = columns_hash[key.to_s]
37
+ if column && column.array
38
+ all.filter_for_array(key, value, options)
39
+ elsif column
40
+ all.send("filter_for_#{column.type}", key, value, options)
41
+ else
42
+ if relation = reflect_on_association(key)
43
+ self.send("filter_for_#{relation.macro}", relation, value)
44
+ else
45
+ raise ActiveRecord::UnkownFilterError.new(self, key)
46
+ end
47
+ end
48
+ end
49
+
50
+ {
51
+ filter_for_geometry: :itself,
52
+ filter_for_datetime: :to_datetime,
53
+ filter_for_integer: :to_i,
54
+ filter_for_text: :itself,
55
+ filter_for_boolean: :itself,
56
+ filter_for_string: :itself,
57
+ filter_for_decimal: :to_f
58
+ }.each_pair do |method_name, send_method|
59
+ define_method(method_name) do |column, value, options={}|
60
+ table = options[:table_alias] ? arel_table.alias(options[:table_alias]) : arel_table
61
+
62
+ case value
63
+ when Hash
64
+ resource = all
65
+ value.each_pair do |key, value|
66
+ converted_value = value.try(:send, send_method)
67
+ resource = case key.to_sym
68
+ when :greater_than, :gt
69
+ resource.where(table[column].gt(converted_value))
70
+ when :less_than, :lt
71
+ resource.where(table[column].lt(converted_value))
72
+ when :greater_than_or_equal_to, :gteq, :gte
73
+ resource.where(table[column].gteq(converted_value))
74
+ when :less_than_or_equal_to, :lteq, :lte
75
+ resource.where(table[column].lteq(converted_value))
76
+ when :not
77
+ resource.where(table[column].not_eq(converted_value))
78
+ when :not_in
79
+ resource.where(table[column].not_in(value).or(table[column].eq(nil)))
80
+ when :intersects
81
+ # geometry_value = if value.is_a?(Hash) # GeoJSON
82
+ # Arel::Nodes::NamedFunction.new('ST_GeomFromGeoJSON', [JSON.generate(value)])
83
+ # elsif # EWKB
84
+ # elsif # WKB
85
+ # elsif # EWKT
86
+ # elsif # WKT
87
+ # end
88
+
89
+ # TODO us above if to determin if SRID sent
90
+ geometry_value = if value.is_a?(Hash)
91
+ Arel::Nodes::NamedFunction.new('ST_SetSRID', [Arel::Nodes::NamedFunction.new('ST_GeomFromGeoJSON', [Arel::Nodes.build_quoted(JSON.generate(value))]), 4326])
92
+ elsif value[0,1] == "\x00" || value[0,1] == "\x01" || value[0,4] =~ /[0-9a-fA-F]{4}/
93
+ Arel::Nodes::NamedFunction.new('ST_SetSRID', [Arel::Nodes::NamedFunction.new('ST_GeomFromEWKB', [Arel::Nodes.build_quoted(value)]), 4326])
94
+ else
95
+ Arel::Nodes::NamedFunction.new('ST_SetSRID', [Arel::Nodes::NamedFunction.new('ST_GeomFromText', [Arel::Nodes.build_quoted(value)]), 4326])
96
+ end
97
+
98
+ resource.where(Arel::Nodes::NamedFunction.new('ST_Intersects', [table[column], geometry_value]))
99
+ else
100
+ raise "Not Supported: #{key.to_sym}"
101
+ end
102
+ end
103
+ resource
104
+ when Array
105
+ where(table[column].in(value.map { |x| x.send(send_method) }))
106
+ when true, 'true'
107
+ case method_name # columns_hash[column.to_s].try(:type)
108
+ when :filter_for_boolean then where(table[column].eq(value.try(:send, send_method)))
109
+ else where(table[column].not_eq(nil))
110
+ end
111
+ when false, 'false'
112
+ case method_name # columns_hash[column.to_s].try(:type)
113
+ when :filter_for_boolean then where(table[column].eq(value.try(:send, send_method)))
114
+ else where(table[column].eq(nil))
115
+ end
116
+ # when ''
117
+ # # TODO support nil. Currently rails params encode nil as empty strings,
118
+ # # and we can't tell which is desired, so do both
119
+ # where(table[column].eq(value).or(table[column].eq(nil)))
120
+ else
121
+ where(table[column].eq(value.try(:send, send_method)))
122
+ end
123
+ end
124
+ end
125
+
126
+ def filter_for_jsonb(column, value, options = {})
127
+ table = options[:table_alias] ? arel_table.alias(options[:table_alias]) : arel_table
128
+
129
+ case value
130
+ when true, 'true'
131
+ where(table[column].not_eq(nil))
132
+ when false, 'false'
133
+ where(table[column].eq(nil))
134
+ when nil
135
+ where(table[column].eq(nil))
136
+ else
137
+ puts value.inspect
138
+ raise 'Not supported'
139
+ end
140
+ end
141
+
142
+ def filter_for_array(column, value, options={})
143
+ table = options[:table_alias] ? arel_table.alias(options[:table_alias]) : arel_table
144
+
145
+ case value
146
+ when Array
147
+ where(Arel::Nodes::Overlaps.new(table[column], Arel::Attributes::Array.new(value)))
148
+ else
149
+ any_column = Arel::Nodes::NamedFunction.new('ANY', [table[column]])
150
+ predicate = Arel::Nodes::Equality.new(Arel::Nodes.build_quoted(value), any_column)
151
+ where(predicate)
152
+ end
153
+ end
154
+
155
+ def filter_for_has_and_belongs_to_many(relation, value)
156
+ resource = all
157
+
158
+ options = {}
159
+ if resource.klass == relation.klass
160
+ options[:table_alias] = "#{relation.name}_#{relation.klass.table_name}"
161
+ end
162
+
163
+ case value
164
+ when Hash
165
+ resource = resource.joins(relation.name) if !resource.references?(relation.name)
166
+ resource = resource.merge(relation.klass.filter(value, options))
167
+ when Integer
168
+ resource = resource.joins(relation.name) if !resource.references?(relation.name)
169
+ resource = resource.merge(relation.klass.filter(value, options))
170
+ when Array
171
+ resource = resource.joins(relation.name) if !resource.references?(relation.name)
172
+ resource = resource.merge(relation.klass.filter(value, options))
173
+ else
174
+ raise 'Not supported'
175
+ end
176
+
177
+ resource
178
+ end
179
+
180
+ def filter_for_has_many(relation, value)
181
+ resource = all
182
+
183
+ case value
184
+ when Hash
185
+ if relation.options[:through]
186
+ resource = resource.joins(relation.options[:through] => relation.source_reflection_name)
187
+ else
188
+ resource = resource.joins(relation.name) # if !resource.joined?(relation.name)
189
+ end
190
+ resource = resource.merge(relation.klass.filter(value))
191
+ when Array, Integer
192
+ resource = filter_for_has_many(relation, {:id => value})
193
+ when true, 'true'
194
+ counter_cache_column_name = relation.counter_cache_column || "#{relation.plural_name}_count"
195
+ if resource.column_names.include?(counter_cache_column_name)
196
+ resource = resource.where(resource.arel_table[counter_cache_column_name.to_sym].gt(0))
197
+ else
198
+ raise 'Not supported'
199
+ end
200
+ when false, 'false'
201
+ # TODO if the has_many relationship has counter_cache true can just use counter_cache_column method
202
+ counter_cache_column_name = relation.counter_cache_column || "#{relation.plural_name}_count"
203
+ if resource.column_names.include?(counter_cache_column_name)
204
+ resource = resource.where(resource.arel_table[counter_cache_column_name.to_sym].eq(0))
205
+ else
206
+ raise 'Not supported'
207
+ end
208
+ else
209
+ raise 'Not supported'
210
+ end
211
+
212
+ resource
213
+ end
214
+ alias_method :filter_for_has_one, :filter_for_has_many
215
+
216
+ def filter_for_belongs_to(relation, value)
217
+ resource = all
218
+
219
+ case value
220
+ when Array, Integer, NilClass
221
+ resource = resource.where(:"#{relation.foreign_key}" => value)
222
+ when true, 'true'
223
+ resource = resource.where(resource.arel_table[:"#{relation.foreign_key}"].not_eq(nil))
224
+ when false, 'false'
225
+ resource = resource.where(resource.arel_table[:"#{relation.foreign_key}"].eq(nil))
226
+ when Hash
227
+ if relation.polymorphic?
228
+ raise 'no :as' if !value[:as]
229
+ klass = value.delete(:as).classify.constantize
230
+ t1 = resource.arel_table
231
+ t2 = klass.arel_table
232
+ resource = resource.joins(t1.join(t2).on(
233
+ t2[:id].eq(t1["#{relation.name}_id"]).and(t1["#{relation.name}_type"].eq(klass.name))
234
+ ).join_sources.first)
235
+ resource = resource.merge(klass.filter(value))
236
+ else
237
+ resource = resource.joins(relation.name) # if !resource.references?(relation.name)
238
+ resource = resource.merge(relation.klass.filter(value))
239
+ end
240
+ else
241
+ if value.is_a?(String) && value =~ /\A\d+\Z/
242
+ resource = resource.where(:"#{relation.foreign_key}" => value.to_i)
243
+ else
244
+ raise 'Not supported'
245
+ end
246
+ end
247
+ resource
248
+ end
249
+
250
+ end
251
+ end
@@ -0,0 +1,27 @@
1
+ module ActiveRecord::QueryMethods
2
+
3
+ # # TODO: testme and rename to joins?
4
+ # def joined?(assoc)
5
+ # joined_assocs = joins_values.map{ |i| i.is_a?(Hash) ? i.keys : i.to_sym }.flatten
6
+ # joined_assocs.include?(assoc)
7
+ # end
8
+ #
9
+ # # TODO: testme and rename to includes?
10
+ # def included?(assoc)
11
+ # included_assocs = includes_values.map{ |i| i.is_a?(Hash) ? i.keys : i.to_sym }.flatten
12
+ # included_assocs.include?(assoc)
13
+ # end
14
+
15
+ # TODO: testme and rename to
16
+ def references?(assoc)
17
+ references_assocs = references_values.map{ |i| i.is_a?(Hash) ? i.keys : i.to_sym }.flatten
18
+ references_assocs.include?(assoc)
19
+ end
20
+
21
+ end
22
+
23
+ module ActiveRecord::Querying
24
+ # delegate :joined?, :to => :all
25
+ # delegate :included?, :to => :all
26
+ delegate :references?, :to => :all
27
+ end
@@ -0,0 +1,9 @@
1
+ class ActiveRecord::UnkownFilterError < NoMethodError
2
+ attr_reader :klass, :filter
3
+
4
+ def initialize(klass, filter)
5
+ @klass = klass
6
+ @filter = filter.to_s
7
+ super("unkown filter #{filter.inspect} for #{klass}.")
8
+ end
9
+ end
@@ -0,0 +1,5 @@
1
+ module Arel
2
+ module Attributes
3
+ class Array < Attribute; end
4
+ end
5
+ end
@@ -0,0 +1,6 @@
1
+ module Arel
2
+ module Nodes
3
+ class Contains < Binary
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module Arel
2
+ module Nodes
3
+ class Overlaps < Binary
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,30 @@
1
+ module Arel
2
+ module Visitors
3
+ class PostgreSQL
4
+ private
5
+
6
+ def visit_Arel_Nodes_Contains o, collector
7
+ visit o.left, collector
8
+ collector << ' @> '
9
+ visit o.right, collector
10
+ end
11
+
12
+ def visit_Arel_Nodes_Overlaps o, collector
13
+ visit o.left, collector
14
+ collector << ' && '
15
+ visit o.right, collector
16
+ end
17
+
18
+ def visit_Arel_Attributes_Array o, collector
19
+ type = if !o.relation[0]
20
+ ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array.new(nil)
21
+ else
22
+ ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array.new("ActiveRecord::Type::#{o.relation[0].class}".constantize.new)
23
+ end
24
+
25
+ collector << quote(type.type_cast_for_database(o.relation))
26
+ end
27
+
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,9 @@
1
+ require 'active_record'
2
+
3
+ require File.expand_path(File.join(__FILE__, '../../../ext/arel/attributes/array'))
4
+ require File.expand_path(File.join(__FILE__, '../../../ext/arel/nodes/contains'))
5
+ require File.expand_path(File.join(__FILE__, '../../../ext/arel/nodes/overlaps'))
6
+ require File.expand_path(File.join(__FILE__, '../../../ext/arel/visitors/postgresql'))
7
+ require File.expand_path(File.join(__FILE__, '../../../ext/active_record/unkown_filter_error'))
8
+ require File.expand_path(File.join(__FILE__, '../../../ext/active_record/base'))
9
+ require File.expand_path(File.join(__FILE__, '../../../ext/active_record/query_methods'))
@@ -0,0 +1,5 @@
1
+ module ActiveRecord
2
+ module Filter
3
+ VERSION = '1.0.0.alpha'
4
+ end
5
+ end
metadata ADDED
@@ -0,0 +1,199 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: activerecord-filter
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0.alpha
5
+ platform: ruby
6
+ authors:
7
+ - Jon Bracy
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-07-30 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: pg
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: activerecord
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '4.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '4.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: minitest
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: minitest-reporters
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: simplecov
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: sqlite3
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: factory_girl_rails
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: faker
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ description: A safe way to accept user parameters and query against your ActiveRecord
154
+ Models
155
+ email:
156
+ - jonbracy@gmail.com
157
+ executables: []
158
+ extensions: []
159
+ extra_rdoc_files:
160
+ - README.md
161
+ files:
162
+ - README.md
163
+ - ext/active_record/base.rb
164
+ - ext/active_record/query_methods.rb
165
+ - ext/active_record/unkown_filter_error.rb
166
+ - ext/arel/attributes/array.rb
167
+ - ext/arel/nodes/contains.rb
168
+ - ext/arel/nodes/overlaps.rb
169
+ - ext/arel/visitors/postgresql.rb
170
+ - lib/active_record/filter.rb
171
+ - lib/active_record/filter/version.rb
172
+ homepage: https://github.com/malomalo/activerecord-filter
173
+ licenses:
174
+ - MIT
175
+ metadata: {}
176
+ post_install_message:
177
+ rdoc_options:
178
+ - "--main"
179
+ - README.md
180
+ require_paths:
181
+ - lib
182
+ required_ruby_version: !ruby/object:Gem::Requirement
183
+ requirements:
184
+ - - ">="
185
+ - !ruby/object:Gem::Version
186
+ version: '0'
187
+ required_rubygems_version: !ruby/object:Gem::Requirement
188
+ requirements:
189
+ - - ">"
190
+ - !ruby/object:Gem::Version
191
+ version: 1.3.1
192
+ requirements: []
193
+ rubyforge_project:
194
+ rubygems_version: 2.4.5
195
+ signing_key:
196
+ specification_version: 4
197
+ summary: A safe way to accept user parameters and query against your ActiveRecord
198
+ Models
199
+ test_files: []