forest_admin_agent 1.0.0.pre.beta.22 → 1.0.0.pre.beta.23

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: b5714273d6e7144f00eb531b7a93d0e908698bd1dac7984dc47affd15a7514f6
4
- data.tar.gz: 3b47af0daae4bc4e02b944f0d21be13aa18fe5e7abb9bfcfa03f2ea3272028bd
3
+ metadata.gz: c342682e272a5886928f4c48de0e35dbd6bcf013c4f888aa56bac238cca695da
4
+ data.tar.gz: 300d73bdac310e91c2aa4a044d095b782bd362a0fb3c6309871fad0b1fff2930
5
5
  SHA512:
6
- metadata.gz: 93044527fdfb1a06f49408044479258b1cccb6af15c8025cf26310f01c684ae6b5c4657bd25496eb69d911eaa77daae83ec82ec551469bd15cacea61a3b46687
7
- data.tar.gz: f93863c6d28342854e2e453795bca3c3892c6a09c0d91a44fcb42fae57b107c17f6daa3e9694d04e0862bb46e1f95ff2d14f04c199ab0c2a5436434ca5090ca4
6
+ metadata.gz: 50d05943fa75c8559e127bd181f91bb36dc64352237126381d4bd1fd769fa0e51389b74051e1f7cdc55f7c6747fbdb2c91259cf135c6e2e7e7665c5cef8cefcd
7
+ data.tar.gz: be53569ca0ad92511dc33b118693445e5a2a522faa98fd24e4d3662bf70eaa3ac7f9614cc1dccc6db444f8fe204d39db43eca1709f8efe4e528f7fef2b200ebf
@@ -34,6 +34,7 @@ admin work on any Ruby application."
34
34
  spec.require_paths = ["lib"]
35
35
 
36
36
  spec.add_dependency "activesupport", ">= 6.1"
37
+ spec.add_dependency "deepsort", "~> 0.4.5"
37
38
  spec.add_dependency "dry-container", "~> 0.11"
38
39
  spec.add_dependency "faraday", "~> 2.7"
39
40
  spec.add_dependency "filecache", "~> 1.0"
@@ -63,9 +63,7 @@ module ForestAdminAgent
63
63
  return unless @has_env_secret
64
64
 
65
65
  cache = @container.resolve(:cache)
66
- cache.get_or_set 'config' do
67
- @options.to_h
68
- end
66
+ cache.set('config', @options.to_h)
69
67
  end
70
68
 
71
69
  def build_logger
@@ -9,6 +9,7 @@ module ForestAdminAgent
9
9
  # api_charts_routes,
10
10
  System::HealthCheck.new.routes,
11
11
  Security::Authentication.new.routes,
12
+ Charts::Charts.new.routes,
12
13
  Resources::Count.new.routes,
13
14
  Resources::Delete.new.routes,
14
15
  Resources::List.new.routes,
@@ -0,0 +1,214 @@
1
+ require 'jsonapi-serializers'
2
+ require 'active_support/inflector'
3
+
4
+ module ForestAdminAgent
5
+ module Routes
6
+ module Charts
7
+ class Charts < AbstractAuthenticatedRoute
8
+ include ForestAdminAgent::Builder
9
+ include ForestAdminAgent::Utils
10
+ include ForestAdminDatasourceToolkit::Components::Query
11
+ include ForestAdminDatasourceToolkit::Components::Query::ConditionTree
12
+ include ForestAdminDatasourceToolkit::Components::Charts
13
+
14
+ attr_reader :filter
15
+
16
+ FORMAT = {
17
+ Day: '%d/%m/%Y',
18
+ Week: 'W%W-%Y',
19
+ Month: '%b %y',
20
+ Year: '%Y'
21
+ }.freeze
22
+
23
+ def setup_routes
24
+ add_route('forest_chart', 'post', '/stats/:collection_name', lambda { |args|
25
+ handle_request(args)
26
+ })
27
+ self
28
+ end
29
+
30
+ def handle_request(args = {})
31
+ build(args)
32
+ @permissions.can_chart?(args[:params])
33
+ @args = args
34
+ self.type = args[:params][:type]
35
+ @filter = Filter.new(
36
+ condition_tree: ConditionTreeFactory.intersect(
37
+ [
38
+ @permissions.get_scope(@collection),
39
+ ForestAdminAgent::Utils::QueryStringParser.parse_condition_tree(
40
+ @collection, args
41
+ )
42
+ ]
43
+ )
44
+ )
45
+
46
+ inject_context_variables
47
+
48
+ { content: Serializer::ForestChartSerializer.serialize(send(:"make_#{@type}")) }
49
+ end
50
+
51
+ private
52
+
53
+ def type=(type)
54
+ chart_types = %w[Value Objective Pie Line Leaderboard]
55
+ unless chart_types.include?(type)
56
+ raise ForestAdminDatasourceToolkit::Exceptions::ForestException, "Invalid Chart type #{type}"
57
+ end
58
+
59
+ @type = type.downcase
60
+ end
61
+
62
+ def inject_context_variables
63
+ user = @permissions.get_user_data(@caller.id)
64
+ team = @permissions.get_team(@caller.rendering_id)
65
+
66
+ context_variables = ForestAdminAgent::Utils::ContextVariables.new(team, user,
67
+ @args[:params][:contextVariables])
68
+ return unless @args[:params][:filter]
69
+
70
+ @filter = @filter.override(condition_tree: ContextVariablesInjector.inject_context_in_filter(
71
+ @filter.condition_tree, context_variables
72
+ ))
73
+ end
74
+
75
+ def make_value
76
+ value = compute_value(@filter)
77
+ previous_value = nil
78
+ is_and_aggregator = @filter.condition_tree&.try(:aggregator) == 'And'
79
+ with_count_previous = @filter.condition_tree&.some_leaf(&:use_interval_operator)
80
+
81
+ if with_count_previous && !is_and_aggregator
82
+ previous_value = compute_value(FilterFactory.get_previous_period_filter(@filter, @caller.timezone))
83
+ end
84
+
85
+ ValueChart.new(value, previous_value)
86
+ end
87
+
88
+ def make_objective
89
+ ObjectiveChart.new(compute_value(@filter))
90
+ end
91
+
92
+ def make_pie
93
+ group_field = @args[:params][:groupByFieldName]
94
+ aggregation = Aggregation.new(
95
+ operation: @args[:params][:aggregator],
96
+ field: @args[:params][:aggregateFieldName],
97
+ groups: group_field ? [{ field: group_field }] : []
98
+ )
99
+
100
+ result = @collection.aggregate(@caller, @filter, aggregation)
101
+
102
+ PieChart.new(result.map { |row| { key: row[:group][group_field], value: row[:value] } })
103
+ end
104
+
105
+ def make_line
106
+ group_by_field_name = @args[:params][:groupByFieldName]
107
+ time_range = @args[:params][:timeRange]
108
+ filter_only_with_values = @filter.override(
109
+ condition_tree: ConditionTree::ConditionTreeFactory.intersect(
110
+ [
111
+ @filter.condition_tree,
112
+ ConditionTree::Nodes::ConditionTreeLeaf.new(group_by_field_name, ConditionTree::Operators::PRESENT)
113
+ ]
114
+ )
115
+ )
116
+ rows = @collection.aggregate(
117
+ @caller,
118
+ filter_only_with_values,
119
+ Aggregation.new(
120
+ operation: @args[:params][:aggregator],
121
+ field: @args[:params][:aggregateField],
122
+ groups: [{ field: group_by_field_name, operation: time_range }]
123
+ )
124
+ )
125
+
126
+ values = {}
127
+ rows.each { |row| values[row[:group][group_by_field_name]] = row[:value] }
128
+ dates = values.keys.sort
129
+ current = dates[0]
130
+ last = dates.last
131
+ result = []
132
+ while current <= last
133
+ result << {
134
+ label: current.strftime(FORMAT[time_range.to_sym]),
135
+ values: { value: values[current] || 0 }
136
+ }
137
+ current += 1.send(time_range.downcase.pluralize.to_sym)
138
+ end
139
+
140
+ LineChart.new(result)
141
+ end
142
+
143
+ def make_leaderboard
144
+ field = @collection.fields[@args[:params][:relationshipFieldName]]
145
+
146
+ if field && field.type == 'OneToMany'
147
+ inverse = ForestAdminDatasourceToolkit::Utils::Collection.get_inverse_relation(
148
+ @collection,
149
+ @args[:params][:relationshipFieldName]
150
+ )
151
+ if inverse
152
+ collection = field.foreign_collection
153
+ filter = @filter.nest(inverse)
154
+ aggregation = Aggregation.new(
155
+ operation: @args[:params][:aggregator],
156
+ field: @args[:params][:aggregateFieldName],
157
+ groups: [{ field: "#{inverse}:#{@args[:params][:labelFieldName]}" }]
158
+ )
159
+ end
160
+ end
161
+
162
+ if field && field.type == 'ManyToMany'
163
+ origin = ForestAdminDatasourceToolkit::Utils::Collection.get_through_origin(
164
+ @collection,
165
+ @args[:params][:relationshipFieldName]
166
+ )
167
+ target = ForestAdminDatasourceToolkit::Utils::Collection.get_through_target(
168
+ @collection,
169
+ @args[:params][:relationshipFieldName]
170
+ )
171
+ if origin && target
172
+ collection = field.through_collection
173
+ filter = @filter.nest(origin)
174
+ aggregation = Aggregation.new(
175
+ operation: @args[:params][:aggregator],
176
+ field: @args[:params][:aggregateFieldName] ? "#{target}:#{@args[:params][:aggregateFieldName]}" : nil,
177
+ groups: [{ field: "#{origin}:#{@args[:params][:labelFieldName]}" }]
178
+ )
179
+ end
180
+ end
181
+
182
+ if collection && filter && aggregation
183
+ rows = @datasource.collection(collection).aggregate(
184
+ @caller,
185
+ filter,
186
+ aggregation,
187
+ @args[:params][:limit]
188
+ )
189
+
190
+ result = rows.map do |row|
191
+ {
192
+ key: row[:group][aggregation.groups[0][:field]],
193
+ value: row[:value]
194
+ }
195
+ end
196
+
197
+ return LeaderboardChart.new(result)
198
+ end
199
+
200
+ raise ForestAdminDatasourceToolkit::Exceptions::ForestException,
201
+ 'Failed to generate leaderboard chart: parameters do not match pre-requisites'
202
+ end
203
+
204
+ def compute_value(filter)
205
+ aggregation = Aggregation.new(operation: @args[:params][:aggregator],
206
+ field: @args[:params][:aggregateFieldName])
207
+ result = @collection.aggregate(@caller, filter, aggregation)
208
+
209
+ result[0][:value] || 0
210
+ end
211
+ end
212
+ end
213
+ end
214
+ end
@@ -33,17 +33,17 @@ module ForestAdminAgent
33
33
  def link_one_to_one_relations(args, record)
34
34
  args[:params][:data][:relationships]&.map do |field, value|
35
35
  schema = @collection.fields[field]
36
- if schema.type == 'OneToOne'
37
- id = Utils::Id.unpack_id(@collection, value['data']['id'], with_key: true)
38
- foreign_collection = @datasource.collection(schema.foreign_collection)
39
- # Load the value that will be used as origin_key
40
- origin_value = record[schema.origin_key_target]
36
+ next unless schema.type == 'OneToOne'
41
37
 
42
- # update new relation (may update zero or one records).
43
- condition_tree = ConditionTree::ConditionTreeFactory.match_records(foreign_collection, [id])
44
- filter = Filter.new(condition_tree: condition_tree)
45
- foreign_collection.update(@caller, filter, { schema.origin_key => origin_value })
46
- end
38
+ id = Utils::Id.unpack_id(@collection, value['data']['id'], with_key: true)
39
+ foreign_collection = @datasource.collection(schema.foreign_collection)
40
+ # Load the value that will be used as origin_key
41
+ origin_value = record[schema.origin_key_target]
42
+
43
+ # update new relation (may update zero or one records).
44
+ condition_tree = ConditionTree::ConditionTreeFactory.match_records(foreign_collection, [id])
45
+ filter = Filter.new(condition_tree: condition_tree)
46
+ foreign_collection.update(@caller, filter, { schema.origin_key => origin_value })
47
47
  end
48
48
  end
49
49
  end
@@ -0,0 +1,19 @@
1
+ require 'securerandom'
2
+
3
+ module ForestAdminAgent
4
+ module Serializer
5
+ class ForestChartSerializer
6
+ def self.serialize(chart)
7
+ {
8
+ data: {
9
+ id: SecureRandom.uuid,
10
+ type: 'stats',
11
+ attributes: {
12
+ value: chart.serialize
13
+ }
14
+ }
15
+ }
16
+ end
17
+ end
18
+ end
19
+ end
@@ -1,4 +1,5 @@
1
1
  require 'filecache'
2
+ require 'deepsort'
2
3
 
3
4
  module ForestAdminAgent
4
5
  module Services
@@ -56,7 +57,7 @@ module ForestAdminAgent
56
57
  end
57
58
 
58
59
  def can_chart?(parameters)
59
- attributes = sanitize_chart_parameters(parameters)
60
+ attributes = sanitize_chart_parameters(parameters.deep_symbolize_keys)
60
61
  hash_request = "#{attributes[:type]}:#{array_hash(attributes)}"
61
62
  is_allowed = get_chart_data(caller.rendering_id).include?(hash_request)
62
63
 
@@ -99,8 +100,6 @@ module ForestAdminAgent
99
100
  def get_scope(collection)
100
101
  permissions = get_scope_and_team_data(caller.rendering_id)
101
102
  scope = permissions[:scopes][collection.name.to_sym]
102
- team = permissions[:team]
103
- user = get_user_data(caller.id)
104
103
 
105
104
  return nil if scope.nil?
106
105
 
@@ -170,16 +169,21 @@ module ForestAdminAgent
170
169
  end
171
170
 
172
171
  def sanitize_chart_parameters(parameters)
173
- # parameters = parameters.to_h
174
172
  parameters.delete(:timezone)
175
173
  parameters.delete(:collection)
176
174
  parameters.delete(:contextVariables)
175
+ # rails
176
+ parameters.delete(:route_alias)
177
+ parameters.delete(:controller)
178
+ parameters.delete(:action)
179
+ parameters.delete(:collection_name)
180
+ parameters.delete(:forest)
177
181
 
178
182
  parameters.select { |_, value| !value.nil? && value != '' }
179
183
  end
180
184
 
181
185
  def array_hash(data)
182
- Digest::SHA1.hexdigest(data.sort.to_h.to_s)
186
+ Digest::SHA1.hexdigest(data.deep_sort.to_h.to_s)
183
187
  end
184
188
 
185
189
  def get_scope_and_team_data(rendering_id)
@@ -197,7 +201,7 @@ module ForestAdminAgent
197
201
  def permission_system?
198
202
  cache.get_or_set('forest.has_permission') do
199
203
  response = fetch('/liana/v4/permissions/environment')
200
- { enable: !response.nil? }
204
+ { enable: response != true }
201
205
  end[:enable]
202
206
  end
203
207
 
@@ -1,4 +1,5 @@
1
1
  require 'jwt'
2
+ require 'active_support'
2
3
  require 'active_support/time'
3
4
 
4
5
  module ForestAdminAgent
@@ -7,7 +7,7 @@ module ForestAdminAgent
7
7
  class SchemaEmitter
8
8
  LIANA_NAME = "forest-rails"
9
9
 
10
- LIANA_VERSION = "1.0.0-beta.22"
10
+ LIANA_VERSION = "1.0.0-beta.23"
11
11
 
12
12
  def self.get_serialized_schema(datasource)
13
13
  schema_path = Facades::Container.cache(:schema_path)
@@ -1,3 +1,3 @@
1
1
  module ForestAdminAgent
2
- VERSION = "1.0.0-beta.22"
2
+ VERSION = "1.0.0-beta.23"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: forest_admin_agent
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0.pre.beta.22
4
+ version: 1.0.0.pre.beta.23
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matthieu
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2023-12-08 00:00:00.000000000 Z
12
+ date: 2023-12-13 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activesupport
@@ -25,6 +25,20 @@ dependencies:
25
25
  - - ">="
26
26
  - !ruby/object:Gem::Version
27
27
  version: '6.1'
28
+ - !ruby/object:Gem::Dependency
29
+ name: deepsort
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - "~>"
33
+ - !ruby/object:Gem::Version
34
+ version: 0.4.5
35
+ type: :runtime
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "~>"
40
+ - !ruby/object:Gem::Version
41
+ version: 0.4.5
28
42
  - !ruby/object:Gem::Dependency
29
43
  name: dry-container
30
44
  requirement: !ruby/object:Gem::Requirement
@@ -229,6 +243,7 @@ files:
229
243
  - lib/forest_admin_agent/routes/abstract_authenticated_route.rb
230
244
  - lib/forest_admin_agent/routes/abstract_related_route.rb
231
245
  - lib/forest_admin_agent/routes/abstract_route.rb
246
+ - lib/forest_admin_agent/routes/charts/charts.rb
232
247
  - lib/forest_admin_agent/routes/resources/count.rb
233
248
  - lib/forest_admin_agent/routes/resources/delete.rb
234
249
  - lib/forest_admin_agent/routes/resources/list.rb
@@ -242,6 +257,7 @@ files:
242
257
  - lib/forest_admin_agent/routes/resources/update.rb
243
258
  - lib/forest_admin_agent/routes/security/authentication.rb
244
259
  - lib/forest_admin_agent/routes/system/health_check.rb
260
+ - lib/forest_admin_agent/serializer/forest_chart_serializer.rb
245
261
  - lib/forest_admin_agent/serializer/forest_serializer.rb
246
262
  - lib/forest_admin_agent/serializer/forest_serializer_override.rb
247
263
  - lib/forest_admin_agent/services/ip_whitelist.rb