forest_admin_agent 1.12.13 → 1.12.15

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.
Files changed (28) hide show
  1. checksums.yaml +4 -4
  2. data/lib/forest_admin_agent/routes/abstract_authenticated_route.rb +8 -6
  3. data/lib/forest_admin_agent/routes/abstract_related_route.rb +9 -7
  4. data/lib/forest_admin_agent/routes/abstract_route.rb +7 -3
  5. data/lib/forest_admin_agent/routes/action/actions.rb +31 -25
  6. data/lib/forest_admin_agent/routes/capabilities/collections.rb +4 -4
  7. data/lib/forest_admin_agent/routes/charts/api_chart_collection.rb +10 -10
  8. data/lib/forest_admin_agent/routes/charts/api_chart_datasource.rb +8 -7
  9. data/lib/forest_admin_agent/routes/charts/charts.rb +64 -66
  10. data/lib/forest_admin_agent/routes/request_context.rb +17 -0
  11. data/lib/forest_admin_agent/routes/resources/count.rb +9 -9
  12. data/lib/forest_admin_agent/routes/resources/csv.rb +11 -11
  13. data/lib/forest_admin_agent/routes/resources/delete.rb +20 -19
  14. data/lib/forest_admin_agent/routes/resources/list.rb +14 -14
  15. data/lib/forest_admin_agent/routes/resources/native_query.rb +9 -9
  16. data/lib/forest_admin_agent/routes/resources/related/associate_related.rb +32 -29
  17. data/lib/forest_admin_agent/routes/resources/related/count_related.rb +9 -9
  18. data/lib/forest_admin_agent/routes/resources/related/csv_related.rb +10 -9
  19. data/lib/forest_admin_agent/routes/resources/related/dissociate_related.rb +63 -40
  20. data/lib/forest_admin_agent/routes/resources/related/list_related.rb +12 -11
  21. data/lib/forest_admin_agent/routes/resources/related/update_related.rb +60 -52
  22. data/lib/forest_admin_agent/routes/resources/show.rb +8 -8
  23. data/lib/forest_admin_agent/routes/resources/store.rb +18 -15
  24. data/lib/forest_admin_agent/routes/resources/update.rb +9 -9
  25. data/lib/forest_admin_agent/routes/resources/update_field.rb +17 -17
  26. data/lib/forest_admin_agent/utils/schema/schema_emitter.rb +1 -1
  27. data/lib/forest_admin_agent/version.rb +1 -1
  28. metadata +2 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dcedc2aca2e7965829e92f88f166f74264dd820084052ae74d225beb3a166aa8
4
- data.tar.gz: 8931db8225893bcb912b3a445d88f6323d58bac33caf166051b5753df84c3676
3
+ metadata.gz: d06c27d8cb7afd9c9e531c215dd12f253af4e0dc92f67496d45a789a925c8024
4
+ data.tar.gz: 93fa83fec26dcce76b175cdb1cafdbc2bf55a4d94bb91835a9ed7cb830e67da8
5
5
  SHA512:
6
- metadata.gz: 6faf436c5fbf5a3facf3313644f02483b990f09eaf10bd99aba7a96a999c6d45b9c9b8b426eaa19152627a1cf42f33d87b00a495417a5382876d231f6ee8b15b
7
- data.tar.gz: e9a7c9e2e5763539a1dea190ed0c3cd46a2b5fff17b0aa447660695ba58faa5d37bedc23cbfac48a7be8383ceeeb8abaa9a95df54e14ce9aefc6399931946f95
6
+ metadata.gz: 48ef7caf5cec3cb36abc575bf1f49b1c12c45c21380e0a579a5637feda6ed9d894f1a088019666526cc9ec688fc0018544917327aab82060370ba97278911dcc
7
+ data.tar.gz: 8b962f14fdc92733b8d4ed312d167d99169ad472c93ec885117a3569c691b271c3c0e64d37b221f42ecf62fc487a0af557d3ee0470e43d18d9768cd57ce091f9
@@ -5,18 +5,20 @@ module ForestAdminAgent
5
5
  if args.dig(:headers, 'action_dispatch.remote_ip')
6
6
  Facades::Whitelist.check_ip(args[:headers]['action_dispatch.remote_ip'].to_s)
7
7
  end
8
- @caller = Utils::QueryStringParser.parse_caller(args)
9
- @permissions = ForestAdminAgent::Services::Permissions.new(@caller)
10
- super
8
+
9
+ context = super
10
+ context.caller = Utils::QueryStringParser.parse_caller(args)
11
+ context.permissions = ForestAdminAgent::Services::Permissions.new(context.caller)
12
+ context
11
13
  end
12
14
 
13
- def format_attributes(args)
15
+ def format_attributes(args, collection)
14
16
  record = args[:params][:data][:attributes] || {}
15
17
 
16
18
  args[:params][:data][:relationships]&.map do |field, value|
17
- schema = @collection.schema[:fields][field]
19
+ schema = collection.schema[:fields][field]
18
20
 
19
- record[schema.foreign_key] = value['data']['id'] if schema.type == 'ManyToOne'
21
+ record[schema.foreign_key] = value.dig('data', 'id') if schema.type == 'ManyToOne'
20
22
  end
21
23
 
22
24
  record || {}
@@ -2,14 +2,16 @@ module ForestAdminAgent
2
2
  module Routes
3
3
  class AbstractRelatedRoute < AbstractAuthenticatedRoute
4
4
  def build(args = {})
5
- super
5
+ context = super
6
6
 
7
- relation = @collection.schema[:fields][args[:params]['relation_name']]
8
- @child_collection = if relation.type == 'PolymorphicManyToOne'
9
- @datasource.get_collection(args[:params]['data']['type'])
10
- else
11
- @datasource.get_collection(relation.foreign_collection)
12
- end
7
+ relation = context.collection.schema[:fields][args[:params]['relation_name']]
8
+ context.child_collection = if relation.type == 'PolymorphicManyToOne'
9
+ context.datasource.get_collection(args[:params]['data']['type'])
10
+ else
11
+ context.datasource.get_collection(relation.foreign_collection)
12
+ end
13
+
14
+ context
13
15
  end
14
16
  end
15
17
  end
@@ -7,10 +7,14 @@ module ForestAdminAgent
7
7
  end
8
8
 
9
9
  def build(args)
10
- @datasource = ForestAdminAgent::Facades::Container.datasource
11
- return unless args[:params]['collection_name']
10
+ context = RequestContext.new
11
+ context.datasource = ForestAdminAgent::Facades::Container.datasource
12
12
 
13
- @collection = @datasource.get_collection(args[:params]['collection_name'])
13
+ if args[:params]['collection_name']
14
+ context.collection = context.datasource.get_collection(args[:params]['collection_name'])
15
+ end
16
+
17
+ context
14
18
  end
15
19
 
16
20
  def routes
@@ -14,14 +14,14 @@ module ForestAdminAgent
14
14
 
15
15
  def initialize(collection, action)
16
16
  @action_name = action
17
- @collection = collection
17
+ @route_collection = collection
18
18
  super()
19
19
  end
20
20
 
21
21
  def setup_routes
22
- action_index = @collection.schema[:actions].keys.index(@action_name)
22
+ action_index = @route_collection.schema[:actions].keys.index(@action_name)
23
23
  slug = ForestAdminAgent::Utils::Schema::GeneratorAction.get_action_slug(@action_name)
24
- route_name = "forest_action_#{@collection.name}_#{action_index}_#{slug}"
24
+ route_name = "forest_action_#{@route_collection.name}_#{action_index}_#{slug}"
25
25
  path = "/_actions/:collection_name/#{action_index}/#{slug}"
26
26
 
27
27
  add_route(route_name, 'post', path, proc { |args| handle_request(args) })
@@ -47,12 +47,12 @@ module ForestAdminAgent
47
47
  end
48
48
 
49
49
  def handle_request(args = {})
50
- build(args)
50
+ context = build(args)
51
51
  args = middleware_custom_action_approval_request_data(args)
52
- filter_for_caller = get_record_selection(args)
53
- get_record_selection(args, include_user_scope: false)
52
+ filter_for_caller = get_record_selection(args, context)
53
+ get_record_selection(args, context, include_user_scope: false)
54
54
 
55
- @permissions.can_smart_action?(args, @collection, filter_for_caller)
55
+ context.permissions.can_smart_action?(args, context.collection, filter_for_caller)
56
56
 
57
57
  raw_data = args.dig(:params, :data, :attributes, :values)
58
58
 
@@ -60,8 +60,8 @@ module ForestAdminAgent
60
60
  # better send invalid data to the getForm() customer handler than to the execute() one.
61
61
  unsafe_data = Schema::ForestValueConverter.make_form_data_unsafe(raw_data)
62
62
 
63
- fields = @collection.get_form(
64
- @caller,
63
+ fields = context.collection.get_form(
64
+ context.caller,
65
65
  @action_name,
66
66
  unsafe_data,
67
67
  filter_for_caller,
@@ -70,24 +70,27 @@ module ForestAdminAgent
70
70
 
71
71
  # Now that we have the field list, we can parse the data again.
72
72
  data = Schema::ForestValueConverter.make_form_data(
73
- @datasource,
73
+ context.datasource,
74
74
  raw_data,
75
75
  fields.reject { |field| field.type == 'Layout' }
76
76
  )
77
77
 
78
- { content: @collection.execute(@caller, @action_name, data, filter_for_caller) }
78
+ { content: context.collection.execute(context.caller, @action_name, data, filter_for_caller) }
79
79
  end
80
80
 
81
81
  def handle_hook_request(args = {})
82
- build(args)
82
+ context = build(args)
83
83
  forest_fields = args.dig(:params, :data, :attributes, :fields)
84
- data = (Schema::ForestValueConverter.make_form_data_from_fields(@datasource, forest_fields) if forest_fields)
85
- filter = get_record_selection(args)
84
+ data = (if forest_fields
85
+ Schema::ForestValueConverter.make_form_data_from_fields(context.datasource,
86
+ forest_fields)
87
+ end)
88
+ filter = get_record_selection(args, context)
86
89
  search_values = {}
87
90
  forest_fields&.each { |field| search_values[field['field']] = field['searchValue'] }
88
91
 
89
- form = @collection.get_form(
90
- @caller,
92
+ form = context.collection.get_form(
93
+ context.caller,
91
94
  @action_name,
92
95
  data,
93
96
  filter,
@@ -102,7 +105,9 @@ module ForestAdminAgent
102
105
 
103
106
  {
104
107
  content: {
105
- fields: form_elements[:fields].map { |f| Schema::GeneratorAction.build_field_schema(@datasource, f) },
108
+ fields: form_elements[:fields].map do |f|
109
+ Schema::GeneratorAction.build_field_schema(context.datasource, f)
110
+ end,
106
111
  layout: Schema::GeneratorAction.build_layout(form_elements[:layout])
107
112
  }
108
113
  }
@@ -129,26 +134,26 @@ module ForestAdminAgent
129
134
  )[0])
130
135
  end
131
136
 
132
- def get_record_selection(args, include_user_scope: true)
137
+ def get_record_selection(args, context, include_user_scope: true)
133
138
  attributes = args.dig(:params, :data, :attributes)
134
139
 
135
140
  # Match user filter + search + scope? + segment
136
- scope = include_user_scope ? @permissions.get_scope(@collection) : nil
141
+ scope = include_user_scope ? context.permissions.get_scope(context.collection) : nil
137
142
  filter = Filter.new(
138
143
  condition_tree: ConditionTreeFactory.intersect(
139
144
  [
140
145
  scope,
141
146
  ForestAdminAgent::Utils::QueryStringParser.parse_condition_tree(
142
- @collection, args
147
+ context.collection, args
143
148
  )
144
149
  ]
145
150
  )
146
151
  )
147
152
 
148
153
  # Restrict the filter to the selected records for single or bulk actions
149
- if @collection.schema[:actions][@action_name].scope != Types::ActionScope::GLOBAL
150
- selection_ids = Utils::Id.parse_selection_ids(@collection, args[:params])
151
- selected_ids = ConditionTreeFactory.match_ids(@collection, selection_ids[:ids])
154
+ if context.collection.schema[:actions][@action_name].scope != Types::ActionScope::GLOBAL
155
+ selection_ids = Utils::Id.parse_selection_ids(context.collection, args[:params])
156
+ selected_ids = ConditionTreeFactory.match_ids(context.collection, selection_ids[:ids])
152
157
  selected_ids = selected_ids.inverse if selection_ids[:are_excluded]
153
158
  filter = filter.override(
154
159
  condition_tree: ConditionTreeFactory.intersect([filter.condition_tree, selected_ids])
@@ -158,10 +163,11 @@ module ForestAdminAgent
158
163
  # Restrict the filter further for the "related data" page
159
164
  unless attributes[:parent_association_name].nil?
160
165
  relation = attributes[:parent_association_name]
161
- parent = @datasource.get_collection(attributes[:parent_collection_name])
166
+ parent = context.datasource.get_collection(attributes[:parent_collection_name])
162
167
  parent_primary_key_values = Utils::Id.unpack_id(parent, attributes[:parent_collection_id])
163
168
 
164
- filter = FilterFactory.make_foreign_filter(parent, parent_primary_key_values, relation, @caller, filter)
169
+ filter = FilterFactory.make_foreign_filter(parent, parent_primary_key_values, relation, context.caller,
170
+ filter)
165
171
  end
166
172
 
167
173
  filter
@@ -2,7 +2,7 @@ require 'json'
2
2
  module ForestAdminAgent
3
3
  module Routes
4
4
  module Capabilities
5
- class Collections < AbstractAuthenticatedRoute
5
+ class Collections < AbstractRoute
6
6
  include ForestAdminDatasourceToolkit::Schema
7
7
 
8
8
  def setup_routes
@@ -15,13 +15,13 @@ module ForestAdminAgent
15
15
  end
16
16
 
17
17
  def handle_request(args = {})
18
- @datasource = ForestAdminAgent::Facades::Container.datasource
18
+ datasource = ForestAdminAgent::Facades::Container.datasource
19
19
  collections = args[:params]['collectionNames'] || []
20
20
 
21
- connections = @datasource.live_query_connections.keys.map { |connection_name| { name: connection_name } }
21
+ connections = datasource.live_query_connections.keys.map { |connection_name| { name: connection_name } }
22
22
 
23
23
  result = collections.map do |collection_name|
24
- collection = @datasource.get_collection(collection_name)
24
+ collection = datasource.get_collection(collection_name)
25
25
  {
26
26
  name: collection.name,
27
27
  fields: collection.schema[:fields].select { |_, field| field.is_a?(ColumnSchema) }.map do |name, field|
@@ -9,14 +9,14 @@ module ForestAdminAgent
9
9
 
10
10
  def initialize(collection, chart_name)
11
11
  @chart_name = chart_name
12
- @collection = collection
12
+ @route_collection = collection
13
13
 
14
14
  super()
15
15
  end
16
16
 
17
17
  def setup_routes
18
18
  # Mount both GET and POST, respectively for smart and api charts.
19
- collection_name = @collection.name
19
+ collection_name = @route_collection.name
20
20
  slug = @chart_name.parameterize
21
21
 
22
22
  add_route(
@@ -44,25 +44,25 @@ module ForestAdminAgent
44
44
  end
45
45
 
46
46
  def handle_api_chart(args)
47
- build(args)
47
+ context = build(args)
48
48
  {
49
49
  content: Serializer::ForestChartSerializer.serialize(
50
- @collection.render_chart(
51
- @caller,
50
+ context.collection.render_chart(
51
+ context.caller,
52
52
  @chart_name,
53
- Id.unpack_id(@collection, args[:params]['record_id'])
53
+ Id.unpack_id(context.collection, args[:params]['record_id'])
54
54
  )
55
55
  )
56
56
  }
57
57
  end
58
58
 
59
59
  def handle_smart_chart(args)
60
- build(args)
60
+ context = build(args)
61
61
  {
62
- content: @collection.render_chart(
63
- @caller,
62
+ content: context.collection.render_chart(
63
+ context.caller,
64
64
  @chart_name,
65
- Id.unpack_id(@collection, args[:params]['record_id'])
65
+ Id.unpack_id(context.collection, args[:params]['record_id'])
66
66
  )
67
67
  }
68
68
  end
@@ -7,7 +7,6 @@ module ForestAdminAgent
7
7
  class ApiChartDatasource < AbstractAuthenticatedRoute
8
8
  def initialize(chart_name)
9
9
  @chart_name = chart_name
10
- @datasource = ForestAdminAgent::Facades::Container.datasource
11
10
 
12
11
  super()
13
12
  end
@@ -38,12 +37,13 @@ module ForestAdminAgent
38
37
  end
39
38
 
40
39
  def handle_api_chart(args = {})
41
- @caller = Utils::QueryStringParser.parse_caller(args)
40
+ caller = Utils::QueryStringParser.parse_caller(args)
41
+ datasource = ForestAdminAgent::Facades::Container.datasource
42
42
 
43
43
  {
44
44
  content: Serializer::ForestChartSerializer.serialize(
45
- @datasource.render_chart(
46
- @caller,
45
+ datasource.render_chart(
46
+ caller,
47
47
  @chart_name
48
48
  )
49
49
  )
@@ -51,11 +51,12 @@ module ForestAdminAgent
51
51
  end
52
52
 
53
53
  def handle_smart_chart(args = {})
54
- @caller = Utils::QueryStringParser.parse_caller(args)
54
+ caller = Utils::QueryStringParser.parse_caller(args)
55
+ datasource = ForestAdminAgent::Facades::Container.datasource
55
56
 
56
57
  {
57
- content: @datasource.render_chart(
58
- @caller,
58
+ content: datasource.render_chart(
59
+ caller,
59
60
  @chart_name
60
61
  )
61
62
  }
@@ -11,8 +11,6 @@ module ForestAdminAgent
11
11
  include ForestAdminDatasourceToolkit::Components::Query::ConditionTree
12
12
  include ForestAdminDatasourceToolkit::Components::Charts
13
13
 
14
- attr_reader :filter
15
-
16
14
  FORMAT = {
17
15
  Day: '%d/%m/%Y',
18
16
  Week: 'W%V-%G',
@@ -28,97 +26,97 @@ module ForestAdminAgent
28
26
  end
29
27
 
30
28
  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(
29
+ context = build(args)
30
+ context.permissions.can_chart?(args[:params])
31
+ type = validate_and_get_type(args[:params][:type])
32
+ filter = Filter.new(
36
33
  condition_tree: ConditionTreeFactory.intersect(
37
34
  [
38
- @permissions.get_scope(@collection),
35
+ context.permissions.get_scope(context.collection),
39
36
  ForestAdminAgent::Utils::QueryStringParser.parse_condition_tree(
40
- @collection, args
37
+ context.collection, args
41
38
  )
42
39
  ]
43
40
  )
44
41
  )
45
42
 
46
- inject_context_variables
43
+ filter = inject_context_variables(filter, context, args)
47
44
 
48
- { content: Serializer::ForestChartSerializer.serialize(send(:"make_#{@type}")) }
45
+ { content: Serializer::ForestChartSerializer.serialize(send(:"make_#{type}", context, filter, args)) }
49
46
  end
50
47
 
51
48
  private
52
49
 
53
- def type=(type)
50
+ def validate_and_get_type(type)
54
51
  chart_types = %w[Value Objective Pie Line Leaderboard]
55
52
  unless chart_types.include?(type)
56
53
  raise ForestAdminDatasourceToolkit::Exceptions::ForestException, "Invalid Chart type #{type}"
57
54
  end
58
55
 
59
- @type = type.downcase
56
+ type.downcase
60
57
  end
61
58
 
62
- def inject_context_variables
63
- user = @permissions.get_user_data(@caller.id)
64
- team = @permissions.get_team(@caller.rendering_id)
59
+ def inject_context_variables(filter, context, args)
60
+ user = context.permissions.get_user_data(context.caller.id)
61
+ team = context.permissions.get_team(context.caller.rendering_id)
65
62
 
66
63
  context_variables = ForestAdminAgent::Utils::ContextVariables.new(team, user,
67
- @args[:params][:contextVariables])
68
- return unless @args[:params][:filter]
64
+ args[:params][:contextVariables])
65
+ return filter unless args[:params][:filter]
69
66
 
70
- @filter = @filter.override(condition_tree: ContextVariablesInjector.inject_context_in_filter(
71
- @filter.condition_tree, context_variables
67
+ filter.override(condition_tree: ContextVariablesInjector.inject_context_in_filter(
68
+ filter.condition_tree, context_variables
72
69
  ))
73
70
  end
74
71
 
75
- def make_value
76
- value = compute_value(@filter)
72
+ def make_value(context, filter, args)
73
+ value = compute_value(context, filter, args)
77
74
  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)
75
+ is_and_aggregator = filter.condition_tree&.try(:aggregator) == 'And'
76
+ with_count_previous = filter.condition_tree&.some_leaf(&:use_interval_operator)
80
77
 
81
78
  if with_count_previous && !is_and_aggregator
82
- previous_value = compute_value(FilterFactory.get_previous_period_filter(@filter, @caller.timezone))
79
+ previous_filter = FilterFactory.get_previous_period_filter(filter, context.caller.timezone)
80
+ previous_value = compute_value(context, previous_filter, args)
83
81
  end
84
82
 
85
83
  ValueChart.new(value, previous_value).serialize
86
84
  end
87
85
 
88
- def make_objective
89
- ObjectiveChart.new(compute_value(@filter)).serialize
86
+ def make_objective(context, filter, args)
87
+ ObjectiveChart.new(compute_value(context, filter, args)).serialize
90
88
  end
91
89
 
92
- def make_pie
93
- group_field = @args[:params][:groupByFieldName]
90
+ def make_pie(context, filter, args)
91
+ group_field = args[:params][:groupByFieldName]
94
92
  aggregation = Aggregation.new(
95
- operation: @args[:params][:aggregator],
96
- field: @args[:params][:aggregateFieldName],
93
+ operation: args[:params][:aggregator],
94
+ field: args[:params][:aggregateFieldName],
97
95
  groups: group_field ? [{ field: group_field }] : []
98
96
  )
99
97
 
100
- result = @collection.aggregate(@caller, @filter, aggregation)
98
+ result = context.collection.aggregate(context.caller, filter, aggregation)
101
99
 
102
100
  PieChart.new(result.map { |row| { key: row['group'][group_field], value: row['value'] } }).serialize
103
101
  end
104
102
 
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(
103
+ def make_line(context, filter, args)
104
+ group_by_field_name = args[:params][:groupByFieldName]
105
+ time_range = args[:params][:timeRange]
106
+ filter_only_with_values = filter.override(
109
107
  condition_tree: ConditionTree::ConditionTreeFactory.intersect(
110
108
  [
111
- @filter.condition_tree,
109
+ filter.condition_tree,
112
110
  ConditionTree::Nodes::ConditionTreeLeaf.new(group_by_field_name, ConditionTree::Operators::PRESENT)
113
111
  ]
114
112
  )
115
113
  )
116
- rows = @collection.aggregate(
117
- @caller,
114
+ rows = context.collection.aggregate(
115
+ context.caller,
118
116
  filter_only_with_values,
119
117
  Aggregation.new(
120
- operation: @args[:params][:aggregator],
121
- field: @args[:params][:aggregateFieldName],
118
+ operation: args[:params][:aggregator],
119
+ field: args[:params][:aggregateFieldName],
122
120
  groups: [{ field: group_by_field_name, operation: time_range }]
123
121
  )
124
122
  )
@@ -140,51 +138,51 @@ module ForestAdminAgent
140
138
  LineChart.new(result).serialize
141
139
  end
142
140
 
143
- def make_leaderboard
144
- field = @collection.schema[:fields][@args[:params][:relationshipFieldName]]
141
+ def make_leaderboard(context, filter, args)
142
+ field = context.collection.schema[:fields][args[:params][:relationshipFieldName]]
145
143
 
146
144
  if field && field.type == 'OneToMany'
147
145
  inverse = ForestAdminDatasourceToolkit::Utils::Collection.get_inverse_relation(
148
- @collection,
149
- @args[:params][:relationshipFieldName]
146
+ context.collection,
147
+ args[:params][:relationshipFieldName]
150
148
  )
151
149
  if inverse
152
150
  collection = field.foreign_collection
153
- filter = @filter.nest(inverse)
151
+ leaderboard_filter = filter.nest(inverse)
154
152
  aggregation = Aggregation.new(
155
- operation: @args[:params][:aggregator],
156
- field: @args[:params][:aggregateFieldName],
157
- groups: [{ field: "#{inverse}:#{@args[:params][:labelFieldName]}" }]
153
+ operation: args[:params][:aggregator],
154
+ field: args[:params][:aggregateFieldName],
155
+ groups: [{ field: "#{inverse}:#{args[:params][:labelFieldName]}" }]
158
156
  )
159
157
  end
160
158
  end
161
159
 
162
160
  if field && field.type == 'ManyToMany'
163
161
  origin = ForestAdminDatasourceToolkit::Utils::Collection.get_through_origin(
164
- @collection,
165
- @args[:params][:relationshipFieldName]
162
+ context.collection,
163
+ args[:params][:relationshipFieldName]
166
164
  )
167
165
  target = ForestAdminDatasourceToolkit::Utils::Collection.get_through_target(
168
- @collection,
169
- @args[:params][:relationshipFieldName]
166
+ context.collection,
167
+ args[:params][:relationshipFieldName]
170
168
  )
171
169
  if origin && target
172
170
  collection = field.through_collection
173
- filter = @filter.nest(origin)
171
+ leaderboard_filter = filter.nest(origin)
174
172
  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]}" }]
173
+ operation: args[:params][:aggregator],
174
+ field: args[:params][:aggregateFieldName] ? "#{target}:#{args[:params][:aggregateFieldName]}" : nil,
175
+ groups: [{ field: "#{origin}:#{args[:params][:labelFieldName]}" }]
178
176
  )
179
177
  end
180
178
  end
181
179
 
182
- if collection && filter && aggregation
183
- rows = @datasource.get_collection(collection).aggregate(
184
- @caller,
185
- filter,
180
+ if collection && leaderboard_filter && aggregation
181
+ rows = context.datasource.get_collection(collection).aggregate(
182
+ context.caller,
183
+ leaderboard_filter,
186
184
  aggregation,
187
- @args[:params][:limit]
185
+ args[:params][:limit]
188
186
  )
189
187
 
190
188
  result = rows.map do |row|
@@ -201,10 +199,10 @@ module ForestAdminAgent
201
199
  'Failed to generate leaderboard chart: parameters do not match pre-requisites'
202
200
  end
203
201
 
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)
202
+ def compute_value(context, filter, args)
203
+ aggregation = Aggregation.new(operation: args[:params][:aggregator],
204
+ field: args[:params][:aggregateFieldName])
205
+ result = context.collection.aggregate(context.caller, filter, aggregation)
208
206
 
209
207
  result[0]['value'] || 0
210
208
  end
@@ -0,0 +1,17 @@
1
+ module ForestAdminAgent
2
+ module Routes
3
+ # RequestContext holds request-specific data that should not be shared between concurrent requests.
4
+ # This prevents race conditions when route instances are reused across multiple requests.
5
+ class RequestContext
6
+ attr_accessor :datasource, :collection, :child_collection, :caller, :permissions
7
+
8
+ def initialize(datasource: nil, collection: nil, child_collection: nil, caller: nil, permissions: nil)
9
+ @datasource = datasource
10
+ @collection = collection
11
+ @child_collection = child_collection
12
+ @caller = caller
13
+ @permissions = permissions
14
+ end
15
+ end
16
+ end
17
+ end
@@ -15,24 +15,24 @@ module ForestAdminAgent
15
15
  end
16
16
 
17
17
  def handle_request(args = {})
18
- build(args)
19
- @permissions.can?(:browse, @collection)
18
+ context = build(args)
19
+ context.permissions.can?(:browse, context.collection)
20
20
 
21
- if @collection.is_countable?
21
+ if context.collection.is_countable?
22
22
  filter = ForestAdminDatasourceToolkit::Components::Query::Filter.new(
23
23
  condition_tree: ConditionTreeFactory.intersect(
24
24
  [
25
- @permissions.get_scope(@collection),
26
- parse_query_segment(@collection, args, @permissions, @caller),
27
- ForestAdminAgent::Utils::QueryStringParser.parse_condition_tree(@collection, args)
25
+ context.permissions.get_scope(context.collection),
26
+ parse_query_segment(context.collection, args, context.permissions, context.caller),
27
+ ForestAdminAgent::Utils::QueryStringParser.parse_condition_tree(context.collection, args)
28
28
  ]
29
29
  ),
30
- search: QueryStringParser.parse_search(@collection, args),
30
+ search: QueryStringParser.parse_search(context.collection, args),
31
31
  search_extended: QueryStringParser.parse_search_extended(args),
32
- segment: QueryStringParser.parse_segment(@collection, args)
32
+ segment: QueryStringParser.parse_segment(context.collection, args)
33
33
  )
34
34
  aggregation = ForestAdminDatasourceToolkit::Components::Query::Aggregation.new(operation: 'Count')
35
- result = @collection.aggregate(@caller, filter, aggregation)
35
+ result = context.collection.aggregate(context.caller, filter, aggregation)
36
36
 
37
37
  return {
38
38
  name: args[:params]['collection_name'],