forest_liana 3.3.0 → 4.0.0.pre.beta.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -5,22 +5,13 @@ module ForestLiana
5
5
  def perform
6
6
  if @params[:group_by_field]
7
7
  timezone_offset = @params[:timezone].to_i
8
- conditions = []
9
- filter_operator = ''
8
+ resource = get_resource().eager_load(@includes)
10
9
 
11
- if @params[:filterType] && @params[:filters]
12
- filter_operator = " #{@params[:filterType]} ".upcase
13
-
14
- @params[:filters].try(:each) do |filter|
15
- operator, filter_value = OperatorValueParser.parse(filter[:value])
16
- conditions << OperatorValueParser.get_condition(filter[:field],
17
- operator, filter_value, @resource, @params[:timezone])
18
- end
10
+ if @params[:filters]
11
+ resource = FiltersParser.new(@params[:filters], resource, @params[:timezone]).apply_filters
19
12
  end
20
13
 
21
- result = get_resource()
22
- .eager_load(@includes)
23
- .where(conditions.join(filter_operator))
14
+ result = resource
24
15
  .group(group_by_field)
25
16
  .order(order)
26
17
  .send(@params[:aggregate].downcase, @params[:aggregate_field])
@@ -17,8 +17,10 @@ module ForestLiana
17
17
  @tables_associated_to_relations_name =
18
18
  ForestLiana::QueryHelper.get_tables_associated_to_relations_name(@resource)
19
19
  @records = search_param
20
- @records = has_many_filter
21
- @records = belongs_to_and_params_filter
20
+
21
+ if @params[:filters]
22
+ @records = FiltersParser.new(@params[:filters], @records, @params[:timezone]).apply_filters
23
+ end
22
24
 
23
25
  if @search
24
26
  ForestLiana.schema_for_resource(@resource).fields.each do |field|
@@ -36,11 +38,6 @@ module ForestLiana
36
38
  @records
37
39
  end
38
40
 
39
- def belongs_to_and_params_filter
40
- conditions = filter_param + belongs_to_filter
41
- @records.where(conditions.join(" #{@params[:filterType]} ".upcase))
42
- end
43
-
44
41
  def format_column_name(table_name, column_name)
45
42
  ForestLiana::AdapterHelper.format_column_name(table_name, column_name)
46
43
  end
@@ -154,127 +151,11 @@ module ForestLiana
154
151
  "LOWER(#{column_name}) LIKE :search_value_for_string"
155
152
  end
156
153
 
157
- def filter_param
158
- conditions = []
159
-
160
- if @params[:filterType] && @params[:filter]
161
-
162
- @params[:filter].each do |field, values|
163
- # ActsAsTaggable
164
- if acts_as_taggable?(field)
165
- tagged_records = @records.tagged_with(values.tr('*', ''))
166
-
167
- if @params[:filterType] == 'and'
168
- @records = tagged_records
169
- elsif @params[:filterType] == 'or'
170
- conditions << acts_as_taggable_query(tagged_records)
171
- end
172
- else
173
- next if association?(field)
174
-
175
- values.split(',').each do |value|
176
- operator, value = OperatorValueParser.parse(value)
177
- conditions << OperatorValueParser.get_condition(field, operator,
178
- value, @resource, @params[:timezone])
179
- end
180
- end
181
- end
182
- end
183
-
184
- conditions
185
- end
186
-
187
- def association?(field)
188
- field = field.split(':').first if field.include?(':')
189
- @resource.reflect_on_association(field.to_sym).present?
190
- end
191
-
192
- def has_many_association?(field)
193
- field = field.split(':').first if field.include?(':')
194
- association = @resource.reflect_on_association(field.to_sym)
195
-
196
- [:has_many, :has_and_belongs_to_many].include?(association.try(:macro))
197
- end
198
-
199
154
  def acts_as_taggable?(field)
200
155
  @resource.try(:taggable?) && @resource.respond_to?(:acts_as_taggable) &&
201
156
  @resource.acts_as_taggable.include?(field)
202
157
  end
203
158
 
204
- def has_many_filter
205
- if @params[:filter]
206
- @params[:filter].each do |field, values|
207
- next if !has_many_association?(field) || acts_as_taggable?(field)
208
-
209
- values.split(',').each do |value|
210
- if field.include?(':')
211
- @records = has_many_subfield_filter(field, value)
212
- else
213
- @records = has_many_field_filter(field, value)
214
- end
215
- end
216
- end
217
- end
218
-
219
- @records
220
- end
221
-
222
- def has_many_field_filter(field, value)
223
- association = @resource.reflect_on_association(field.to_sym)
224
- return if association.blank?
225
-
226
- operator, value = OperatorValueParser.parse(value)
227
-
228
- @records = @records
229
- .select("#{@resource.table_name}.*,
230
- COUNT(#{association.table_name}.id)
231
- #{association.table_name}_has_many_count")
232
- .joins(ArelHelpers.join_association(@resource, association.name,
233
- Arel::Nodes::OuterJoin))
234
- .group("#{@resource.table_name}.id")
235
- .having("COUNT(#{association.table_name}) #{operator} #{value}")
236
- end
237
-
238
- def has_many_subfield_filter(field, value)
239
- field, subfield = field.split(':')
240
-
241
- association = @resource.reflect_on_association(field.to_sym)
242
- return if association.blank?
243
-
244
- operator, value = OperatorValueParser.parse(value)
245
-
246
- @records = @records
247
- .select("#{@resource.table_name}.*,
248
- COUNT(#{association.table_name}.id)
249
- #{association.table_name}_has_many_count")
250
- .joins(ArelHelpers.join_association(@resource, association.name,
251
- Arel::Nodes::OuterJoin))
252
- .group("#{@resource.table_name}.id, #{association.table_name}.#{subfield}")
253
- .having("#{association.table_name}.#{subfield} #{operator} '#{value}'")
254
- end
255
-
256
- def belongs_to_association?(field)
257
- field = field.split(':').first if field.include?(':')
258
- association = @resource.reflect_on_association(field.to_sym)
259
- [:belongs_to, :has_one].include?(association.try(:macro))
260
- end
261
-
262
- def belongs_to_filter
263
- conditions = []
264
-
265
- if @params[:filter]
266
- @params[:filter].each do |field, values|
267
- next unless belongs_to_association?(field)
268
-
269
- values.split(',').each do |value|
270
- conditions << OperatorValueParser.get_has_one_condition(@resource, field, value, @params[:timezone])
271
- end
272
- end
273
- end
274
-
275
- conditions
276
- end
277
-
278
159
  private
279
160
 
280
161
  def text_type?(type_sym)
@@ -4,49 +4,21 @@ module ForestLiana
4
4
 
5
5
  def perform
6
6
  return if @params[:aggregate].blank?
7
- valueCurrent = get_resource().eager_load(@includes)
8
- valuePrevious = get_resource().eager_load(@includes)
9
- filter_date_interval = false
7
+ resource = get_resource().eager_load(@includes)
10
8
 
11
- if @params[:filterType] && @params[:filters]
12
- conditions = []
13
- filter_operator = " #{@params[:filterType]} ".upcase
9
+ if @params[:filters]
10
+ filter_parser = FiltersParser.new(@params[:filters], resource, @params[:timezone])
11
+ resource = filter_parser.apply_filters
12
+ raw_previous_interval = filter_parser.get_previous_interval_condition
14
13
 
15
- @params[:filters].try(:each) do |filter|
16
- operator, filter_value = OperatorValueParser.parse(filter[:value])
17
- conditions << OperatorValueParser.get_condition(filter[:field],
18
- operator, filter_value, @resource, @params[:timezone])
19
- end
20
-
21
- valueCurrent = valueCurrent.where(conditions.join(filter_operator))
22
-
23
- # NOTICE: Search for previous interval value only if the filterType is
24
- # 'AND', it would not be pertinent for a 'OR' filterType.
25
- if @params[:filterType] == 'and'
26
- conditions = []
27
- @params[:filters].try(:each) do |filter|
28
- operator, filter_value = OperatorValueParser.parse(filter[:value])
29
- operator_date_interval_parser = OperatorDateIntervalParser
30
- .new(filter_value, @params[:timezone])
31
- if operator_date_interval_parser.has_previous_interval()
32
- field_name = OperatorValueParser.get_field_name(filter[:field], @resource)
33
- filter = operator_date_interval_parser
34
- .get_interval_date_filter_for_previous_interval()
35
- conditions << "#{field_name} #{filter}"
36
- filter_date_interval = true
37
- else
38
- conditions << OperatorValueParser.get_condition(filter[:field],
39
- operator, filter_value, @resource, @params[:timezone])
40
- end
41
- end
42
-
43
- valuePrevious = valuePrevious.where(conditions.join(filter_operator))
14
+ if raw_previous_interval
15
+ previous_value = filter_parser.apply_filters_on_previous_interval(raw_previous_interval)
44
16
  end
45
17
  end
46
18
 
47
19
  @record = Model::Stat.new(value: {
48
- countCurrent: count(valueCurrent),
49
- countPrevious: filter_date_interval ? count(valuePrevious) : nil
20
+ countCurrent: count(resource),
21
+ countPrevious: previous_value ? count(previous_value) : nil
50
22
  })
51
23
  end
52
24
 
@@ -62,8 +34,7 @@ module ForestLiana
62
34
  end
63
35
  value.send(@params[:aggregate].downcase, aggregate_field)
64
36
  else
65
- value.send(@params[:aggregate].downcase, aggregate_field,
66
- distinct: uniq)
37
+ value.send(@params[:aggregate].downcase, aggregate_field, distinct: uniq)
67
38
  end
68
39
  end
69
40
 
@@ -37,19 +37,9 @@ module ForestLiana
37
37
  self.names_overriden = {}
38
38
  self.meta = {}
39
39
 
40
- @config_dir = 'lib/forest_liana/**/*.rb'
41
-
42
40
  # TODO: Remove once lianas prior to 2.0.0 are not supported anymore.
43
41
  self.names_old_overriden = {}
44
42
 
45
- def self.config_dir=(config_dir)
46
- @config_dir = config_dir
47
- end
48
-
49
- def self.config_dir
50
- Rails.root.join(@config_dir)
51
- end
52
-
53
43
  def self.schema_for_resource resource
54
44
  self.apimap.find do |collection|
55
45
  SchemaUtils.find_model_from_collection_name(collection.name)
@@ -211,7 +211,8 @@ module ForestLiana
211
211
  end
212
212
 
213
213
  def require_lib_forest_liana
214
- Dir.glob(ForestLiana.config_dir).each do |file|
214
+ path = Rails.root.join('lib', 'forest_liana', '**', '*.rb')
215
+ Dir.glob(File.expand_path(path, __FILE__)).each do |file|
215
216
  load file
216
217
  end
217
218
  end
@@ -1,3 +1,3 @@
1
1
  module ForestLiana
2
- VERSION = "3.3.0"
2
+ VERSION = "4.0.0-beta.0"
3
3
  end
@@ -1,4 +1,6 @@
1
1
  class User < ActiveRecord::Base
2
2
  has_many :trees_owned, class_name: 'Tree', inverse_of: :owner
3
3
  has_many :trees_cut, class_name: 'Tree', inverse_of: :cutter
4
+
5
+ enum title: [ :king, :villager, :outlaw ]
4
6
  end
@@ -0,0 +1,5 @@
1
+ class AddAgeToTree < ActiveRecord::Migration
2
+ def up
3
+ add_column :trees, :age, :integer
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class AddTypeToUser < ActiveRecord::Migration
2
+ def up
3
+ add_column :users, :title, :integer
4
+ end
5
+ end
@@ -11,7 +11,7 @@
11
11
  #
12
12
  # It's strongly recommended that you check this file into your version control system.
13
13
 
14
- ActiveRecord::Schema.define(version: 20190226174951) do
14
+ ActiveRecord::Schema.define(version: 20190716135241) do
15
15
 
16
16
  create_table "isle", force: :cascade do |t|
17
17
  t.string "name"
@@ -27,6 +27,7 @@ ActiveRecord::Schema.define(version: 20190226174951) do
27
27
  t.integer "island_id"
28
28
  t.datetime "created_at"
29
29
  t.datetime "updated_at"
30
+ t.integer "age"
30
31
  end
31
32
 
32
33
  add_index "trees", ["cutter_id"], name: "index_trees_on_cutter_id"
@@ -37,6 +38,7 @@ ActiveRecord::Schema.define(version: 20190226174951) do
37
38
  t.string "name"
38
39
  t.datetime "created_at"
39
40
  t.datetime "updated_at"
41
+ t.integer "title"
40
42
  end
41
43
 
42
44
  end
@@ -19,10 +19,20 @@ describe 'Requesting Tree resources', :type => :request do
19
19
  allow_any_instance_of(ForestLiana::PermissionsChecker).to receive(:is_authorized?) { true }
20
20
  end
21
21
 
22
+ token = JWT.encode({
23
+ id: 38,
24
+ email: 'michael.kelso@that70.show',
25
+ first_name: 'Michael',
26
+ last_name: 'Kelso',
27
+ team: 'Operations',
28
+ rendering_id: 16,
29
+ exp: Time.now.to_i + 2.weeks.to_i
30
+ }, ForestLiana.auth_secret, 'HS256')
31
+
22
32
  headers = {
23
33
  'Accept' => 'application/json',
24
34
  'Content-Type' => 'application/json',
25
- 'Authorization' => 'Bearer eyJhbGciOiJIUzI1NiJ9.eyJleHAiOiIxODQ5ODc4ODYzIiwiZGF0YSI6eyJpZCI6IjM4IiwidHlwZSI6InVzZXJzIiwiZGF0YSI6eyJlbWFpbCI6Im1pY2hhZWwua2Vsc29AdGhhdDcwLnNob3ciLCJmaXJzdF9uYW1lIjoiTWljaGFlbCIsImxhc3RfbmFtZSI6IktlbHNvIiwidGVhbXMiOiJPcGVyYXRpb25zIn0sInJlbGF0aW9uc2hpcHMiOnsicmVuZGVyaW5ncyI6eyJkYXRhIjpbeyJ0eXBlIjoicmVuZGVyaW5ncyIsImlkIjoxNn1dfX19fQ.U4Mxi0tq0Ce7y5FRXP47McNPRPhUx37LznQ5E3mJIp4'
35
+ 'Authorization' => "Bearer #{token}"
26
36
  }
27
37
 
28
38
  describe 'index' do
@@ -62,8 +72,10 @@ describe 'Requesting Tree resources', :type => :request do
62
72
  describe 'with a filter on an association that is not a displayed column' do
63
73
  params = {
64
74
  fields: { 'Tree' => 'id,name' },
65
- filterType: 'and',
66
- filter: { 'owner:id' => '$present' },
75
+ filters: JSON.generate({
76
+ field: 'owner:id',
77
+ operator: 'present'
78
+ }),
67
79
  page: { 'number' => '1', 'size' => '10' },
68
80
  searchExtended: '0',
69
81
  sort: '-id',
@@ -0,0 +1,476 @@
1
+ module ForestLiana
2
+ describe FiltersParser do
3
+ let(:timezone) { 'Europe/Paris' }
4
+ let(:resource) { Tree }
5
+ let(:filters) { {} }
6
+ let(:filter_parser) { described_class.new(filters.to_json, resource, timezone) }
7
+ let(:simple_condition_1) { { 'field' => 'name', 'operator' => 'contains', 'value' => 'Tree' } }
8
+ let(:simple_condition_2) { { 'field' => 'name', 'operator' => 'ends_with', 'value' => '3' } }
9
+ let(:simple_condition_3) { { 'field' => 'age', 'operator' => 'greater_than', 'value' => 2 } }
10
+ let(:belongs_to_condition) { { 'field' => 'trees:age', 'operator' => 'less_than', 'value' => 3 } }
11
+ let(:date_condition_1) { { 'field' => 'created_at', 'operator' => 'before', 'value' => 2.hours.ago } }
12
+ let(:date_condition_2) { { 'field' => 'created_at', 'operator' => 'today' } }
13
+ let(:date_condition_3) { { 'field' => 'created_at', 'operator' => 'previous_x_days', 'value' => 2 } }
14
+
15
+ before {
16
+ island = Island.create!(name: "L'île de la muerta")
17
+ king = User.create!(title: :king, name: 'Ben E.')
18
+ villager = User.create!(title: :villager)
19
+ Tree.create!(name: 'Tree n1', age: 1, island: island, owner: king)
20
+ Tree.create!(name: 'Tree n2', age: 3, island: island, created_at: 3.day.ago, owner: king)
21
+ Tree.create!(name: 'Tree n3', age: 4, island: island, owner: king, cutter: villager)
22
+ }
23
+
24
+ after {
25
+ Tree.destroy_all
26
+ User.destroy_all
27
+ Island.destroy_all
28
+ }
29
+
30
+ describe 'initialization' do
31
+ context 'badly formated filters' do
32
+ let(:filter_parser) { described_class.new('{ toto: 1', resource, timezone) }
33
+
34
+ it {
35
+ expect {
36
+ described_class.new('{ toto: 1', resource, timezone)
37
+ }.to raise_error(ForestLiana::Errors::HTTP422Error, 'Invalid filters JSON format')
38
+ }
39
+ end
40
+ end
41
+
42
+ describe 'apply_filters' do
43
+ let(:parsed_filters) { filter_parser.apply_filters }
44
+
45
+ context 'on valid filters' do
46
+ context 'single condtions' do
47
+ context 'not_equal' do
48
+ let(:filters) { { field: 'age', operator: 'not_equal', value: 4 } }
49
+ it { expect(parsed_filters.count).to eq 2 }
50
+ end
51
+
52
+ context 'equal' do
53
+ let(:filters) { { field: 'age', operator: 'equal', value: 4 } }
54
+ it { expect(parsed_filters.count).to eq 1 }
55
+ end
56
+
57
+ context 'greater_than' do
58
+ let(:filters) { { field: 'age', operator: 'greater_than', value: 2 } }
59
+ it { expect(parsed_filters.count).to eq 2 }
60
+ end
61
+
62
+ context 'less_than' do
63
+ let(:filters) { { field: 'age', operator: 'less_than', value: 2 } }
64
+ it { expect(parsed_filters.count).to eq 1 }
65
+ end
66
+
67
+ context 'after' do
68
+ let(:filters) { { field: 'created_at', operator: 'after', value: 1.day.ago } }
69
+ it { expect(parsed_filters.count).to eq 2 }
70
+ end
71
+
72
+ context 'before' do
73
+ let(:filters) { { field: 'created_at', operator: 'before', value: 1.day.ago } }
74
+ it { expect(parsed_filters.count).to eq 1 }
75
+ end
76
+
77
+ context 'contains' do
78
+ let(:filters) { { field: 'name', operator: 'contains', value: 'ree' } }
79
+ it { expect(parsed_filters.count).to eq 3 }
80
+ end
81
+
82
+ context 'not_contains' do
83
+ let(:filters) { { field: 'name', operator: 'not_contains', value: ' ' } }
84
+ it { expect(parsed_filters.count).to eq 0 }
85
+ end
86
+
87
+ context 'starts_with' do
88
+ let(:filters) { { field: 'name', operator: 'starts_with', value: 'o' } }
89
+ it { expect(parsed_filters.count).to eq 0 }
90
+ end
91
+
92
+ context 'ends_with' do
93
+ let(:filters) { { field: 'name', operator: 'ends_with', value: '3' } }
94
+ it { expect(parsed_filters.count).to eq 1 }
95
+ end
96
+
97
+ context 'present' do
98
+ let(:filters) { { field: 'cutter_id', operator: 'present', value: nil } }
99
+ it { expect(parsed_filters.count).to eq 1 }
100
+ end
101
+
102
+ context 'blank' do
103
+ let(:filters) { { field: 'cutter_id', operator: 'blank', value: nil } }
104
+ it { expect(parsed_filters.count).to eq 2 }
105
+ end
106
+ end
107
+
108
+ context 'belongsTo conditions' do
109
+ context 'not_equal' do
110
+ let(:filters) { { field: 'cutter:title', operator: 'not_equal', value: 'king' } }
111
+ it { expect(parsed_filters.count).to eq 1 }
112
+ end
113
+
114
+ context 'equal' do
115
+ let(:filters) { { field: 'cutter:title', operator: 'equal', value: 'king' } }
116
+ it { expect(parsed_filters.count).to eq 0 }
117
+ end
118
+
119
+ context 'contains' do
120
+ let(:filters) { { field: 'owner:title', operator: 'contains', value: 'in' } }
121
+ it { expect(parsed_filters.count).to eq 3 }
122
+ end
123
+
124
+ context 'not_contains' do
125
+ let(:filters) { { field: 'owner:title', operator: 'not_contains', value: 'g' } }
126
+ it { expect(parsed_filters.count).to eq 0 }
127
+ end
128
+
129
+ context 'starts_with' do
130
+ let(:filters) { { field: 'cutter:title', operator: 'starts_with', value: 'v' } }
131
+ it { expect(parsed_filters.count).to eq 1 }
132
+ end
133
+
134
+ context 'two belongsTo' do
135
+ context 'different fields' do
136
+ let(:filters) {
137
+ {
138
+ aggregator: 'or', conditions: [
139
+ { field: 'owner:name', operator: 'contains', value: 'E.' },
140
+ { field: 'cutter:title', operator: 'starts_with', value: 'v' }
141
+ ]
142
+ }
143
+ }
144
+ it { expect(parsed_filters.count).to eq 3 }
145
+ end
146
+
147
+ context 'same fields' do
148
+ let(:filters) {
149
+ {
150
+ aggregator: 'and', conditions: [
151
+ { field: 'owner:name', operator: 'contains', value: 'E.' },
152
+ { field: 'owner:title', operator: 'starts_with', value: 'v' }
153
+ ]
154
+ }
155
+ }
156
+ it { expect(parsed_filters.count).to eq 3 }
157
+ end
158
+ end
159
+ end
160
+
161
+ context 'and aggregator on simple conditions' do
162
+ let(:filters) { { aggregator: 'and', conditions: [simple_condition_1, simple_condition_2] } }
163
+ it { expect(parsed_filters.count).to eq 1 }
164
+ end
165
+
166
+
167
+ context 'complex conditions' do
168
+ context 'and aggregator on simple conditions' do
169
+ let(:filters) {
170
+ {
171
+ aggregator: 'or',
172
+ conditions: [
173
+ {
174
+ aggregator: 'and', conditions: [
175
+ { aggregator: 'or', conditions: [date_condition_1, simple_condition_1] },
176
+ simple_condition_2
177
+ ]
178
+ },
179
+ { field: 'cutter:title', operator: 'starts_with', value: 'v' }
180
+ ]
181
+ }
182
+ }
183
+ it { expect(parsed_filters.count).to eq 1 }
184
+ end
185
+ end
186
+ end
187
+
188
+ context 'on invalid filters' do
189
+ context 'invalid condition format' do
190
+ let(:filters) { { toto: 'Why nut?' } }
191
+ it {
192
+ expect { parsed_filters }.to raise_error(ForestLiana::Errors::HTTP422Error, 'Invalid condition format')
193
+ }
194
+ end
195
+
196
+ context 'array as filter' do
197
+ let(:filters) { [] }
198
+ it {
199
+ expect { parsed_filters }.to raise_error(ForestLiana::Errors::HTTP422Error, 'Filters cannot be a raw value')
200
+ }
201
+ end
202
+
203
+ context 'empty filter' do
204
+ let(:filters) { { } }
205
+ it {
206
+ expect { parsed_filters }.to raise_error(ForestLiana::Errors::HTTP422Error, 'Empty condition in filter')
207
+ }
208
+ end
209
+
210
+ context 'raw value in conditions' do
211
+ let(:filters) { { aggregator: 'and', conditions: [4] } }
212
+ it {
213
+ expect { parsed_filters }.to raise_error(ForestLiana::Errors::HTTP422Error, 'Filters cannot be a raw value')
214
+ }
215
+ end
216
+
217
+ context 'bad field type' do
218
+ let(:filters) { { field: 4, operator: 'oss 117', value: 'tuorp' } }
219
+ it {
220
+ expect { parsed_filters }.to raise_error(ForestLiana::Errors::HTTP422Error, 'Invalid condition format')
221
+ }
222
+ end
223
+
224
+ context 'bad operator type' do
225
+ let(:filters) { { field: 'magnetic', operator: true, value: 'tuorp' } }
226
+ it {
227
+ expect { parsed_filters }.to raise_error(ForestLiana::Errors::HTTP422Error, 'Invalid condition format')
228
+ }
229
+ end
230
+
231
+ context 'unexisting field' do
232
+ let(:filters) { { field: 'magnetic', operator: 'archer', value: 'tuorp' } }
233
+ it {
234
+ expect { parsed_filters }.to raise_error(ForestLiana::Errors::HTTP422Error, 'Field \'magnetic\' not found')
235
+ }
236
+ end
237
+ end
238
+ end
239
+
240
+ describe 'parse_aggregation' do
241
+ let(:query) { filter_parser.parse_aggregation(filters) }
242
+
243
+ context 'when no aggregator' do
244
+ let(:filters) { simple_condition_1 }
245
+ it { expect(resource.where(query).count).to eq 3 }
246
+ end
247
+
248
+ context "'name contains \"Tree\"' 'and' 'name ends_with \"3\"'" do
249
+ let(:filters) { { 'aggregator' => 'and', 'conditions' => [simple_condition_1, simple_condition_2] } }
250
+ it { expect(resource.where(query).count).to eq 1 }
251
+ end
252
+
253
+ context "'name contains \"Tree\"' 'and' 'age greater_than 2'" do
254
+ let(:filters) { { 'aggregator' => 'and', 'conditions' => [simple_condition_1, simple_condition_3] } }
255
+ it { expect(resource.where(query).count).to eq 2 }
256
+ end
257
+
258
+ context "'name ends_with \"3\"' 'and' 'age greater_than 2'" do
259
+ let(:filters) { { 'aggregator' => 'and', 'conditions' => [simple_condition_2, simple_condition_3] } }
260
+ it { expect(resource.where(query).count).to eq 1 }
261
+ end
262
+
263
+ context "'name contains \"Tree\"' 'or' 'name ends_with \"3\"'" do
264
+ let(:filters) { { 'aggregator' => 'or', 'conditions' => [simple_condition_1, simple_condition_2] } }
265
+ it { expect(resource.where(query).count).to eq 3 }
266
+ end
267
+
268
+ context "'name contains \"Tree\"' 'or' 'age greater_than 2'" do
269
+ let(:filters) { { 'aggregator' => 'or', 'conditions' => [simple_condition_1, simple_condition_3] } }
270
+ it { expect(resource.where(query).count).to eq 3 }
271
+ end
272
+
273
+ context "'name ends_with \"3\"' 'or' 'age greater_than 2'" do
274
+ let(:filters) { { 'aggregator' => 'or', 'conditions' => [simple_condition_2, simple_condition_3] } }
275
+ it { expect(resource.where(query).count).to eq 2 }
276
+ end
277
+ end
278
+
279
+ describe 'parse_condition' do
280
+ let(:condition) { simple_condition_2 }
281
+ let(:result) { filter_parser.parse_condition(condition) }
282
+
283
+ context 'on valid condition' do
284
+ it { expect(result).to eq "\"trees\".\"name\" LIKE '%3'" }
285
+ end
286
+
287
+ context 'on belongs_to condition' do
288
+ let(:resource) { Island }
289
+ context 'valid association' do
290
+ let(:condition) { belongs_to_condition }
291
+ it { expect(resource.joins(:trees).where(result).count).to eq 1 }
292
+ end
293
+
294
+ context 'wrong association' do
295
+ let(:condition) { { 'field' => 'rosters:id', 'operator' => 'less_than', 'value' => 3 } }
296
+ it {
297
+ expect {
298
+ filter_parser.parse_condition(condition)
299
+ }.to raise_error(ForestLiana::Errors::HTTP422Error, "Association 'rosters' not found")
300
+ }
301
+ end
302
+ end
303
+
304
+ context 'on time based condition' do
305
+ let(:condition) { date_condition_1 }
306
+ it { expect(resource.where(result).count).to eq 1 }
307
+ end
308
+
309
+ context 'on enum condition field type' do
310
+ let(:resource) { User }
311
+ let(:condition) { { 'field' => 'title', 'operator' => 'equal', 'value' => 'king' } }
312
+ it { expect(resource.where(result).count).to eq 1 }
313
+ end
314
+ end
315
+
316
+ describe 'parse_aggregation_operator' do
317
+ context 'on valid aggregator' do
318
+ it { expect(filter_parser.parse_aggregation_operator('and')).to eq 'AND' }
319
+ it { expect(filter_parser.parse_aggregation_operator('or')).to eq 'OR' }
320
+ end
321
+
322
+ context 'on unknown aggregator' do
323
+ it {
324
+ expect {
325
+ filter_parser.parse_aggregation_operator('sfr')
326
+ }.to raise_error(ForestLiana::Errors::HTTP422Error, "Unknown provided operator 'sfr'")
327
+ }
328
+ end
329
+ end
330
+
331
+ describe 'parse_operator' do
332
+ context 'on valid operators' do
333
+ it { expect(filter_parser.parse_operator 'not').to eq 'NOT' }
334
+ it { expect(filter_parser.parse_operator 'greater_than').to eq '>' }
335
+ it { expect(filter_parser.parse_operator 'after').to eq '>' }
336
+ it { expect(filter_parser.parse_operator 'less_than').to eq '<' }
337
+ it { expect(filter_parser.parse_operator 'before').to eq '<' }
338
+ it { expect(filter_parser.parse_operator 'contains').to eq 'LIKE' }
339
+ it { expect(filter_parser.parse_operator 'starts_with').to eq 'LIKE' }
340
+ it { expect(filter_parser.parse_operator 'ends_with').to eq 'LIKE' }
341
+ it { expect(filter_parser.parse_operator 'not_contains').to eq 'NOT LIKE' }
342
+ it { expect(filter_parser.parse_operator 'not_equal').to eq '!=' }
343
+ it { expect(filter_parser.parse_operator 'present').to eq 'IS NOT' }
344
+ it { expect(filter_parser.parse_operator 'equal').to eq '=' }
345
+ it { expect(filter_parser.parse_operator 'blank').to eq 'IS' }
346
+ end
347
+
348
+ context 'on unknown operator' do
349
+ it {
350
+ expect {
351
+ filter_parser.parse_operator('orange')
352
+ }.to raise_error(ForestLiana::Errors::HTTP422Error, "Unknown provided operator 'orange'")
353
+ }
354
+ end
355
+ end
356
+
357
+ describe 'parse_value' do
358
+ context 'on valid operator' do
359
+ let(:now) { Time.now }
360
+ it { expect(filter_parser.parse_value('not', true)).to eq true }
361
+ it { expect(filter_parser.parse_value('greater_than', 34)).to eq 34 }
362
+ it { expect(filter_parser.parse_value('after', now)).to eq now }
363
+ it { expect(filter_parser.parse_value('less_than', 45)).to eq 45 }
364
+ it { expect(filter_parser.parse_value('before', now)).to eq now }
365
+ it { expect(filter_parser.parse_value('contains', 'toto')).to eq '%toto%'}
366
+ it { expect(filter_parser.parse_value('starts_with', 'a')).to eq 'a%'}
367
+ it { expect(filter_parser.parse_value('ends_with', 'b')).to eq '%b' }
368
+ it { expect(filter_parser.parse_value('not_contains', 'o')).to eq '%o%'}
369
+ it { expect(filter_parser.parse_value('not_equal', 'test')).to eq 'test' }
370
+ it { expect(filter_parser.parse_value('present', nil)).to eq nil }
371
+ it { expect(filter_parser.parse_value('equal', 'yes')).to eq 'yes' }
372
+ it { expect(filter_parser.parse_value('blank', nil)).to eq nil }
373
+ end
374
+
375
+ context 'on unknown operator' do
376
+ it {
377
+ expect {
378
+ filter_parser.parse_value('bouygues', 76)
379
+ }.to raise_error(ForestLiana::Errors::HTTP422Error, "Unknown provided operator 'bouygues'")
380
+ }
381
+ end
382
+ end
383
+
384
+ describe 'parse_field_name' do
385
+ let(:resource) { Island }
386
+ let(:result) { filter_parser.parse_field_name(field_name) }
387
+
388
+ context 'on basic field' do
389
+ context 'existing field' do
390
+ let(:field_name) { 'name' }
391
+ it { expect(result).to eq "\"isle\".\"name\""}
392
+ end
393
+
394
+ context 'not existing field' do
395
+ let(:field_name) { 'gender' }
396
+ it {
397
+ expect { result }
398
+ .to raise_error(ForestLiana::Errors::HTTP422Error, "Field '#{field_name}' not found")
399
+ }
400
+ end
401
+ end
402
+
403
+ context 'on belongs to field' do
404
+ context 'existing field' do
405
+ let(:field_name) { 'trees:age' }
406
+ it { expect(result).to eq "\"trees\".\"age\""}
407
+ end
408
+ context 'not existing field' do
409
+ let(:field_name) { 'hero:age' }
410
+ it {
411
+ expect { result }
412
+ .to raise_error(ForestLiana::Errors::HTTP422Error, "Field '#{field_name}' not found")
413
+ }
414
+ end
415
+ context 'not existing sub field' do
416
+ let(:field_name) { 'trees:super_power' }
417
+ it {
418
+ expect { result }
419
+ .to raise_error(ForestLiana::Errors::HTTP422Error, "Field '#{field_name}' not found")
420
+ }
421
+ end
422
+ end
423
+ end
424
+
425
+ describe 'get_previous_interval_condition' do
426
+ let(:result) { filter_parser.get_previous_interval_condition }
427
+
428
+ context 'flat condition at root' do
429
+ context 'has previous interval' do
430
+ let(:filters) { date_condition_2 }
431
+ it { expect(result).to eq date_condition_2 }
432
+ end
433
+
434
+ context 'has no previous interval' do
435
+ let(:filters) { simple_condition_1 }
436
+ it { expect(result).to eq nil }
437
+ end
438
+ end
439
+
440
+ context "has 'and' aggregator" do
441
+ let(:filters) { { 'aggregator' => 'and', 'conditions' => conditions } }
442
+
443
+ context 'has no interval conditions' do
444
+ let(:conditions) { [simple_condition_2, simple_condition_3] }
445
+ it { expect(result).to eq nil }
446
+ end
447
+
448
+ context 'has nested conditions' do
449
+ let(:conditions) { [date_condition_2, { 'aggregator' => 'and', 'conditions' => [simple_condition_2, simple_condition_3] }] }
450
+ it { expect(result).to eq nil }
451
+ end
452
+
453
+ context 'has more than one interval condition' do
454
+ let(:conditions) { [date_condition_2, date_condition_3] }
455
+ it { expect(result).to eq nil }
456
+ end
457
+
458
+ context 'has only one interval condition' do
459
+ let(:conditions) { [date_condition_2, simple_condition_1] }
460
+ it { expect(result).to eq date_condition_2 }
461
+ end
462
+ end
463
+
464
+ context "has 'or' aggregator" do
465
+ let(:filters) { { 'aggregator' => 'or', 'conditions' => [date_condition_2, simple_condition_2] } }
466
+ it { expect(result).to eq nil }
467
+ end
468
+ end
469
+
470
+ describe 'apply_filters_on_previous_interval' do
471
+ let(:filters) { { 'aggregator' => 'and', 'conditions' => [date_condition_3, simple_condition_1] } }
472
+
473
+ it { expect(filter_parser.apply_filters_on_previous_interval(date_condition_3).count).to eq 1 }
474
+ end
475
+ end
476
+ end