forest_liana 6.0.5 → 6.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 95b8a3f6be7165cf4debb95a67e676c4846cd53dd5226ef6550b0f4a9ccab656
4
- data.tar.gz: eb8b01c7bd02b0c828fd5aa7e27ee8677ff5b1711fab9dc96512d607f5562fac
3
+ metadata.gz: ef9f6868496eb0c7e53ccd20fc0ee0853693a9954d56254dffe3e428be975570
4
+ data.tar.gz: 3b929c3692e18e69d8ce423d93f3ca223dc72a482290ab37342adc649a730dfb
5
5
  SHA512:
6
- metadata.gz: fbdf538274940ded194b63d76426557241122eb3ac5e1f6704e37fa295d4a3eece921d0f0c28546e919c454f580733ab9778960d21a1d758cb19f84ef4bd0771
7
- data.tar.gz: 2540d9a5c6289e23ac9d9d383c2d99f86883c2f493e32e5df5de0c659957696d329ff8f60a9339e7136d783ce3cae04e1d4499f418699e70901e65241a0116b4
6
+ metadata.gz: ebbcc8eb29e6b4dd9e9e95c1dd85b9ccd7c8ddaa73e297abe9ed36994a139becd595f55db56a909b07c9da1c2829fea68404d85f90a99b0f8eb91a460abd377e
7
+ data.tar.gz: 44d9e27b058eb2c3127c9efb5670100f032cda8f606a8a3b96a9ca7fc28d2a6b4ac7b97ca70e3514e5d32521ceb6aaa70c5c57aaf12f0d7e00aa179087e18bf7
@@ -1,9 +1,21 @@
1
1
  module ForestLiana
2
2
  class StatsController < ForestLiana::ApplicationController
3
3
  if Rails::VERSION::MAJOR < 4
4
- before_filter :find_resource, except: [:get_with_live_query]
4
+ before_filter only: [:get] do
5
+ find_resource()
6
+ check_permission('statWithParameters')
7
+ end
8
+ before_filter only: [:get_with_live_query] do
9
+ check_permission('liveQueries')
10
+ end
5
11
  else
6
- before_action :find_resource, except: [:get_with_live_query]
12
+ before_action only: [:get] do
13
+ find_resource()
14
+ check_permission('statWithParameters')
15
+ end
16
+ before_action only: [:get_with_live_query] do
17
+ check_permission('liveQueries')
18
+ end
7
19
  end
8
20
 
9
21
  CHART_TYPE_VALUE = 'Value'
@@ -64,5 +76,38 @@ module ForestLiana
64
76
  render json: {status: 404}, status: :not_found, serializer: nil
65
77
  end
66
78
  end
79
+
80
+ def get_live_query_request_info
81
+ params['query']
82
+ end
83
+
84
+ def get_stat_parameter_request_info
85
+ parameters = Rails::VERSION::MAJOR < 5 ? params.dup : params.permit(params.keys).to_h;
86
+
87
+ # Notice: Removes useless properties
88
+ parameters.delete('timezone');
89
+ parameters.delete('controller');
90
+ parameters.delete('action');
91
+
92
+ return parameters;
93
+ end
94
+
95
+ def check_permission(permission_name)
96
+ begin
97
+ query_request = permission_name == 'liveQueries' ? get_live_query_request_info : get_stat_parameter_request_info;
98
+ checker = ForestLiana::PermissionsChecker.new(
99
+ nil,
100
+ permission_name,
101
+ @rendering_id,
102
+ user_id: forest_user['id'],
103
+ query_request_info: query_request
104
+ )
105
+
106
+ return head :forbidden unless checker.is_authorized?
107
+ rescue => error
108
+ FOREST_LOGGER.error "Stats execution error: #{error}"
109
+ render serializer: nil, json: { status: 400 }, status: :bad_request
110
+ end
111
+ end
67
112
  end
68
113
  end
@@ -4,5 +4,11 @@ module ForestLiana
4
4
  collection_name = ForestLiana.name_for(active_record_class)
5
5
  ForestLiana.apimap.find { |collection| collection.name.to_s == collection_name }
6
6
  end
7
+
8
+ def self.is_smart_field?(model, field_name)
9
+ collection = self.find_collection_from_model(model)
10
+ field_found = collection.fields.find { |collection_field| collection_field[:field].to_s == field_name } if collection
11
+ field_found && field_found[:is_virtual]
12
+ end
7
13
  end
8
14
  end
@@ -47,30 +47,56 @@ module ForestLiana
47
47
  end
48
48
 
49
49
  def parse_condition(condition)
50
- ensure_valid_condition(condition)
50
+ where = parse_condition_without_smart_field(condition)
51
51
 
52
- operator = condition['operator']
53
- value = condition['value']
54
- field = condition['field']
52
+ field_name = condition['field']
55
53
 
56
- if @operator_date_parser.is_date_operator?(operator)
57
- condition = @operator_date_parser.get_date_filter(operator, value)
58
- return "#{parse_field_name(field)} #{condition}"
54
+ if ForestLiana::SchemaHelper.is_smart_field?(@resource, field_name)
55
+ schema = ForestLiana.schema_for_resource(@resource)
56
+ field_schema = schema.fields.find do |field|
57
+ field[:field].to_s == field_name
58
+ end
59
+
60
+ unless field_schema.try(:[], :filter)
61
+ raise ForestLiana::Errors::NotImplementedMethodError.new("method filter on smart field '#{field_name}' not found")
62
+ end
63
+
64
+ return field_schema[:filter].call(condition, where)
59
65
  end
60
66
 
61
- if is_belongs_to(field)
62
- association = field.partition(':').first.to_sym
63
- association_field = field.partition(':').last
67
+ where
68
+ end
69
+
70
+ def get_association_field_and_resource(field_name)
71
+ if is_belongs_to(field_name)
72
+ association = field_name.partition(':').first.to_sym
73
+ association_field = field_name.partition(':').last
64
74
 
65
75
  unless @resource.reflect_on_association(association)
66
76
  raise ForestLiana::Errors::HTTP422Error.new("Association '#{association}' not found")
67
77
  end
68
78
 
69
79
  current_resource = @resource.reflect_on_association(association).klass
80
+
81
+ return association_field, current_resource
70
82
  else
71
- association_field = field
72
- current_resource = @resource
83
+ return field_name, @resource
73
84
  end
85
+ end
86
+
87
+ def parse_condition_without_smart_field(condition)
88
+ ensure_valid_condition(condition)
89
+
90
+ operator = condition['operator']
91
+ value = condition['value']
92
+ field_name = condition['field']
93
+
94
+ if @operator_date_parser.is_date_operator?(operator)
95
+ condition = @operator_date_parser.get_date_filter(operator, value)
96
+ return "#{parse_field_name(field_name)} #{condition}"
97
+ end
98
+
99
+ association_field, current_resource = get_association_field_and_resource(field_name)
74
100
 
75
101
  # NOTICE: Set the integer value instead of a string if "enum" type
76
102
  # NOTICE: Rails 3 do not have a defined_enums method
@@ -78,7 +104,7 @@ module ForestLiana
78
104
  value = current_resource.defined_enums[association_field][value]
79
105
  end
80
106
 
81
- parsed_field = parse_field_name(field)
107
+ parsed_field = parse_field_name(field_name)
82
108
  parsed_operator = parse_operator(operator)
83
109
  parsed_value = parse_value(operator, value)
84
110
  field_and_operator = "#{parsed_field} #{parsed_operator}"
@@ -149,16 +175,16 @@ module ForestLiana
149
175
 
150
176
  association = get_association_name_for_condition(field)
151
177
  quoted_table_name = ActiveRecord::Base.connection.quote_column_name(association)
152
- quoted_field_name = ActiveRecord::Base.connection.quote_column_name(field.split(':')[1])
178
+ field_name = field.split(':')[1]
153
179
  else
154
180
  quoted_table_name = @resource.quoted_table_name
155
- quoted_field_name = ActiveRecord::Base.connection.quote_column_name(field)
156
181
  current_resource = @resource
182
+ field_name = field
157
183
  end
184
+ quoted_field_name = ActiveRecord::Base.connection.quote_column_name(field_name)
158
185
 
159
186
  column_found = current_resource.columns.find { |column| column.name == field.split(':').last }
160
-
161
- if column_found.nil?
187
+ if column_found.nil? && !ForestLiana::SchemaHelper.is_smart_field?(current_resource, field_name)
162
188
  raise ForestLiana::Errors::HTTP422Error.new("Field '#{field}' not found")
163
189
  end
164
190
 
@@ -6,13 +6,16 @@ module ForestLiana
6
6
  # TODO: handle cache scopes per rendering
7
7
  @@expiration_in_seconds = (ENV['FOREST_PERMISSIONS_EXPIRATION_IN_SECONDS'] || 3600).to_i
8
8
 
9
- def initialize(resource, permission_name, rendering_id, user_id:, smart_action_request_info: nil, collection_list_parameters: nil)
10
- @user_id = user_id
11
- @collection_name = ForestLiana.name_for(resource)
9
+ def initialize(resource, permission_name, rendering_id, user_id: nil, smart_action_request_info: nil, collection_list_parameters: nil, query_request_info: nil)
10
+
11
+ @collection_name = resource.present? ? ForestLiana.name_for(resource) : nil
12
12
  @permission_name = permission_name
13
13
  @rendering_id = rendering_id
14
+
15
+ @user_id = user_id
14
16
  @smart_action_request_info = smart_action_request_info
15
17
  @collection_list_parameters = collection_list_parameters
18
+ @query_request_info = query_request_info
16
19
  end
17
20
 
18
21
  def is_authorized?
@@ -48,6 +51,16 @@ module ForestLiana
48
51
 
49
52
  def is_allowed
50
53
  permissions = get_permissions_content
54
+
55
+ # NOTICE: check liveQueries permissions
56
+ if @permission_name === 'liveQueries'
57
+ return live_query_allowed?
58
+ elsif @permission_name === 'statWithParameters'
59
+ return stat_with_parameters_allowed?
60
+ end
61
+
62
+
63
+
51
64
  if permissions && permissions[@collection_name] &&
52
65
  permissions[@collection_name]['collection']
53
66
  if @permission_name === 'actions'
@@ -98,6 +111,16 @@ module ForestLiana
98
111
  permissions && permissions['data'] && permissions['data']['collections']
99
112
  end
100
113
 
114
+ def get_live_query_permissions_content
115
+ permissions = get_permissions
116
+ permissions && permissions['stats'] && permissions['stats']['queries']
117
+ end
118
+
119
+ def get_stat_with_parameters_content(statPermissionType)
120
+ permissions = get_permissions
121
+ permissions && permissions['stats'] && permissions['stats'][statPermissionType]
122
+ end
123
+
101
124
  def get_last_fetch
102
125
  permissions = get_permissions
103
126
  permissions && permissions['last_fetch']
@@ -138,6 +161,32 @@ module ForestLiana
138
161
  ).is_scope_in_request?(@collection_list_parameters)
139
162
  end
140
163
 
164
+ def live_query_allowed?
165
+ live_queries_permissions = get_live_query_permissions_content
166
+
167
+ return false unless live_queries_permissions
168
+
169
+ # NOTICE: @query_request_info matching an existing live query
170
+ return live_queries_permissions.include? @query_request_info
171
+ end
172
+
173
+ def stat_with_parameters_allowed?
174
+ permissionType = @query_request_info['type'].downcase + 's'
175
+ pool_permissions = get_stat_with_parameters_content(permissionType)
176
+
177
+ return false unless pool_permissions
178
+
179
+ # NOTICE: equivalent to Object.values in js & removes nil values
180
+ array_query_request_info = @query_request_info.values.filter_map{ |x| x unless x.nil? }
181
+
182
+ # NOTICE: pool_permissions contains the @query_request_info
183
+ # we use the intersection between statPermission and @query_request_info
184
+ return pool_permissions.any? {
185
+ |statPermission|
186
+ (array_query_request_info & statPermission.values) == array_query_request_info;
187
+ }
188
+ end
189
+
141
190
  def date_difference_in_seconds(date1, date2)
142
191
  (date1 - date2).to_i
143
192
  end
@@ -46,6 +46,12 @@ module ForestLiana
46
46
  end
47
47
  end
48
48
 
49
+ class NotImplementedMethodError < ExpectedError
50
+ def initialize(message = "Method not implemented")
51
+ super(501, :internal_server_error, message, 'MethodNotImplementedError')
52
+ end
53
+ end
54
+
49
55
  class InconsistentSecretAndRenderingError < ExpectedError
50
56
  def initialize(message=ForestLiana::MESSAGES[:SERVER_TRANSACTION][:SECRET_AND_RENDERINGID_INCONSISTENT])
51
57
  super(500, :internal_server_error, message, 'InconsistentSecretAndRenderingError')
@@ -1,3 +1,3 @@
1
1
  module ForestLiana
2
- VERSION = "6.0.5"
2
+ VERSION = "6.2.2"
3
3
  end
@@ -2,4 +2,5 @@ class Island < ActiveRecord::Base
2
2
  self.table_name = 'isle'
3
3
 
4
4
  has_many :trees
5
+ has_one :location
5
6
  end
@@ -0,0 +1,3 @@
1
+ class Location < ActiveRecord::Base
2
+ belongs_to :island
3
+ end
@@ -0,0 +1,2 @@
1
+ class Reference < ActiveRecord::Base
2
+ end
@@ -0,0 +1,8 @@
1
+ class CreateReferences < ActiveRecord::Migration[6.0]
2
+ def change
3
+ create_table :references do |t|
4
+
5
+ t.timestamps
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,10 @@
1
+ class CreateLocations < ActiveRecord::Migration[6.0]
2
+ def change
3
+ create_table :locations do |t|
4
+ t.string :coordinates
5
+ t.references :island, index: true
6
+
7
+ t.timestamps
8
+ end
9
+ end
10
+ end
@@ -10,7 +10,7 @@
10
10
  #
11
11
  # It's strongly recommended that you check this file into your version control system.
12
12
 
13
- ActiveRecord::Schema.define(version: 2019_07_16_135241) do
13
+ ActiveRecord::Schema.define(version: 2021_03_26_140855) do
14
14
 
15
15
  create_table "isle", force: :cascade do |t|
16
16
  t.string "name"
@@ -19,6 +19,19 @@ ActiveRecord::Schema.define(version: 2019_07_16_135241) do
19
19
  t.datetime "updated_at"
20
20
  end
21
21
 
22
+ create_table "locations", force: :cascade do |t|
23
+ t.string "coordinates"
24
+ t.integer "island_id"
25
+ t.datetime "created_at", precision: 6, null: false
26
+ t.datetime "updated_at", precision: 6, null: false
27
+ t.index ["island_id"], name: "index_locations_on_island_id"
28
+ end
29
+
30
+ create_table "references", force: :cascade do |t|
31
+ t.datetime "created_at", precision: 6, null: false
32
+ t.datetime "updated_at", precision: 6, null: false
33
+ end
34
+
22
35
  create_table "trees", force: :cascade do |t|
23
36
  t.string "name"
24
37
  t.integer "owner_id"
@@ -0,0 +1,10 @@
1
+ class Forest::Location
2
+ include ForestLiana::Collection
3
+
4
+ collection :Location
5
+
6
+ field :alter_coordinates, type: 'String' do
7
+ object.name + 'XYZ'
8
+ end
9
+
10
+ end
@@ -0,0 +1,15 @@
1
+ class Forest::User
2
+ include ForestLiana::Collection
3
+
4
+ collection :User
5
+
6
+ filter_cap_name = lambda do |condition, where|
7
+ capitalize_name = condition['value'].capitalize
8
+ "name IS '#{capitalize_name}'"
9
+ end
10
+
11
+ field :cap_name, type: 'String', filter: filter_cap_name do
12
+ object.name.upcase
13
+ end
14
+
15
+ end
@@ -0,0 +1,114 @@
1
+ require 'rails_helper'
2
+ require 'json'
3
+
4
+ describe "Stats", type: :request do
5
+
6
+ token = JWT.encode({
7
+ id: 38,
8
+ email: 'michael.kelso@that70.show',
9
+ first_name: 'Michael',
10
+ last_name: 'Kelso',
11
+ team: 'Operations',
12
+ rendering_id: 16,
13
+ exp: Time.now.to_i + 2.weeks.to_i
14
+ }, ForestLiana.auth_secret, 'HS256')
15
+
16
+ headers = {
17
+ 'Accept' => 'application/json',
18
+ 'Content-Type' => 'application/json',
19
+ 'Authorization' => "Bearer #{token}"
20
+ }
21
+
22
+ let(:schema) {
23
+ [
24
+ ForestLiana::Model::Collection.new({
25
+ name: 'Products',
26
+ fields: [],
27
+ actions: []
28
+ })
29
+ ]
30
+ }
31
+
32
+ before do
33
+ allow(ForestLiana).to receive(:apimap).and_return(schema)
34
+ end
35
+
36
+ before(:each) do
37
+ allow(ForestLiana::IpWhitelist).to receive(:retrieve) { true }
38
+ allow(ForestLiana::IpWhitelist).to receive(:is_ip_whitelist_retrieved) { true }
39
+ allow(ForestLiana::IpWhitelist).to receive(:is_ip_valid) { true }
40
+
41
+ allow_any_instance_of(ForestLiana::PermissionsChecker).to receive(:is_authorized?) { true }
42
+
43
+ allow_any_instance_of(ForestLiana::ValueStatGetter).to receive(:perform) { true }
44
+ allow_any_instance_of(ForestLiana::QueryStatGetter).to receive(:perform) { true }
45
+ end
46
+
47
+
48
+
49
+ describe 'POST /stats/:collection' do
50
+ params = { type: 'Value', collection: 'User', aggregate: 'Count' }
51
+
52
+ it 'should respond 200' do
53
+ data = ForestLiana::Model::Stat.new(value: { countCurrent: 0, countPrevious: 0 })
54
+ allow_any_instance_of(ForestLiana::ValueStatGetter).to receive(:record) { data }
55
+ # NOTICE: bypass : find_resource error
56
+ allow_any_instance_of(ForestLiana::StatsController).to receive(:find_resource) { true }
57
+ allow(ForestLiana::QueryHelper).to receive(:get_one_association_names_symbol) { true }
58
+
59
+ post '/forest/stats/Products', params: JSON.dump(params), headers: headers
60
+ expect(response.status).to eq(200)
61
+ end
62
+
63
+ it 'should respond 401 with no headers' do
64
+ post '/forest/stats/Products', params: JSON.dump(params)
65
+ expect(response.status).to eq(401)
66
+ end
67
+
68
+ it 'should respond 404 with non existing collection' do
69
+ allow_any_instance_of(ForestLiana::ValueStatGetter).to receive(:record) { nil }
70
+
71
+ post '/forest/stats/NoCollection', params: {}, headers: headers
72
+ expect(response.status).to eq(404)
73
+ end
74
+
75
+ # it 'should respond 403 Forbidden' do
76
+ # allow_any_instance_of(ForestLiana::PermissionsChecker).to receive(:is_authorized?) { false }
77
+
78
+ # post '/forest/stats/Products', params: JSON.dump(params), headers: headers
79
+ # expect(response.status).to eq(403)
80
+ # end
81
+ end
82
+
83
+ describe 'POST /stats' do
84
+ params = { query: 'SELECT COUNT(*) AS value FROM products;' }
85
+
86
+ it 'should respond 200' do
87
+ data = ForestLiana::Model::Stat.new(value: { value: 0, objective: 0 })
88
+ allow_any_instance_of(ForestLiana::QueryStatGetter).to receive(:record) { data }
89
+
90
+ post '/forest/stats', params: JSON.dump(params), headers: headers
91
+ expect(response.status).to eq(200)
92
+ end
93
+
94
+ it 'should respond 401 with no headers' do
95
+ post '/forest/stats', params: JSON.dump(params)
96
+ expect(response.status).to eq(401)
97
+ end
98
+
99
+ it 'should respond 403 Forbidden' do
100
+ allow_any_instance_of(ForestLiana::PermissionsChecker).to receive(:is_authorized?) { false }
101
+
102
+ post '/forest/stats', params: JSON.dump(params), headers: headers
103
+ expect(response.status).to eq(403)
104
+ end
105
+
106
+ it 'should respond 422 with unprocessable query' do
107
+ allow_any_instance_of(ForestLiana::QueryStatGetter).to receive(:perform) { raise ForestLiana::Errors::LiveQueryError.new }
108
+
109
+ post '/forest/stats', params: JSON.dump(params), headers: headers
110
+ expect(response.status).to eq(422)
111
+ end
112
+ end
113
+
114
+ end