forest_admin_agent 1.12.14 → 1.12.16
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 +4 -4
- data/lib/forest_admin_agent/builder/agent_factory.rb +1 -14
- data/lib/forest_admin_agent/facades/container.rb +4 -3
- data/lib/forest_admin_agent/routes/abstract_authenticated_route.rb +7 -5
- data/lib/forest_admin_agent/routes/abstract_related_route.rb +9 -7
- data/lib/forest_admin_agent/routes/abstract_route.rb +7 -3
- data/lib/forest_admin_agent/routes/action/actions.rb +31 -25
- data/lib/forest_admin_agent/routes/capabilities/collections.rb +4 -4
- data/lib/forest_admin_agent/routes/charts/api_chart_collection.rb +10 -10
- data/lib/forest_admin_agent/routes/charts/api_chart_datasource.rb +8 -7
- data/lib/forest_admin_agent/routes/charts/charts.rb +64 -66
- data/lib/forest_admin_agent/routes/request_context.rb +17 -0
- data/lib/forest_admin_agent/routes/resources/count.rb +9 -9
- data/lib/forest_admin_agent/routes/resources/csv.rb +11 -11
- data/lib/forest_admin_agent/routes/resources/delete.rb +20 -19
- data/lib/forest_admin_agent/routes/resources/list.rb +14 -14
- data/lib/forest_admin_agent/routes/resources/native_query.rb +9 -9
- data/lib/forest_admin_agent/routes/resources/related/associate_related.rb +32 -29
- data/lib/forest_admin_agent/routes/resources/related/count_related.rb +9 -9
- data/lib/forest_admin_agent/routes/resources/related/csv_related.rb +10 -9
- data/lib/forest_admin_agent/routes/resources/related/dissociate_related.rb +63 -40
- data/lib/forest_admin_agent/routes/resources/related/list_related.rb +12 -11
- data/lib/forest_admin_agent/routes/resources/related/update_related.rb +60 -52
- data/lib/forest_admin_agent/routes/resources/show.rb +8 -8
- data/lib/forest_admin_agent/routes/resources/store.rb +18 -15
- data/lib/forest_admin_agent/routes/resources/update.rb +9 -9
- data/lib/forest_admin_agent/routes/resources/update_field.rb +17 -17
- data/lib/forest_admin_agent/utils/schema/schema_emitter.rb +1 -1
- data/lib/forest_admin_agent/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ef78c2e321cc6bf9f5c08237f9f3cee7b47844047b81fd23d0ecf6e19bc96dd5
|
|
4
|
+
data.tar.gz: f96dcb145c4622164c8ec9d49a79d0fcd97aa1b898c8e48b1034fd3f65aa498b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b8a6a19c4068913b44af6dd8c9c94d2474f874314b604b2960431f7200510fa82ef8d114cf17eb29ed964cae0b75e899e800cf4f7c04d0cad88897de1d6d7fcc
|
|
7
|
+
data.tar.gz: 6998491c62ee7a225f73c9fb96e52c00245e60d4cd21e2f9c08b7b2a10db5f3b6adef68c755c82eb848c6ccbd54483b7582c8bbe1ffe43c6a06c93b68005af70
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
require 'dry-container'
|
|
2
|
-
require 'filecache'
|
|
3
2
|
require 'json'
|
|
4
3
|
|
|
5
4
|
module ForestAdminAgent
|
|
@@ -10,8 +9,6 @@ module ForestAdminAgent
|
|
|
10
9
|
include ForestAdminAgent::Http::Exceptions
|
|
11
10
|
include ForestAdminDatasourceToolkit::Exceptions
|
|
12
11
|
|
|
13
|
-
TTL_SCHEMA = 7200
|
|
14
|
-
|
|
15
12
|
attr_reader :customizer, :container, :has_env_secret
|
|
16
13
|
|
|
17
14
|
def setup(options)
|
|
@@ -137,11 +134,6 @@ module ForestAdminAgent
|
|
|
137
134
|
if should_send || force
|
|
138
135
|
client = ForestAdminAgent::Http::ForestAdminApiRequester.new
|
|
139
136
|
client.post('/forest/apimaps', api_map.to_json)
|
|
140
|
-
schema_file_hash_cache = FileCache.new('app', @options[:cache_dir].to_s, TTL_SCHEMA)
|
|
141
|
-
schema_file_hash_cache.get_or_set 'value' do
|
|
142
|
-
api_map[:meta][:schemaFileHash]
|
|
143
|
-
end
|
|
144
|
-
@container.register(:schema_file_hash, schema_file_hash_cache)
|
|
145
137
|
ForestAdminAgent::Facades::Container.logger.log('Info', 'schema was updated, sending new version')
|
|
146
138
|
else
|
|
147
139
|
@container.resolve(:logger)
|
|
@@ -172,16 +164,11 @@ module ForestAdminAgent
|
|
|
172
164
|
end
|
|
173
165
|
|
|
174
166
|
def build_cache
|
|
175
|
-
@container.register(:cache, FileCache.new('app', @options[:cache_dir].to_s, TTL_SCHEMA))
|
|
176
|
-
return unless @has_env_secret
|
|
177
|
-
|
|
178
|
-
cache = @container.resolve(:cache)
|
|
179
|
-
|
|
180
167
|
@options[:customize_error_message] =
|
|
181
168
|
clean_option_value(@options[:customize_error_message], 'config.customize_error_message =')
|
|
182
169
|
@options[:logger] = clean_option_value(@options[:logger], 'config.logger =')
|
|
183
170
|
|
|
184
|
-
|
|
171
|
+
@container.register(:config, @options.to_h)
|
|
185
172
|
end
|
|
186
173
|
|
|
187
174
|
def build_logger
|
|
@@ -16,13 +16,14 @@ module ForestAdminAgent
|
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
def self.config_from_cache
|
|
19
|
-
instance.resolve(:
|
|
19
|
+
instance.resolve(:config)
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
def self.cache(key)
|
|
23
|
-
|
|
23
|
+
config = config_from_cache
|
|
24
|
+
raise "Key #{key} not found in config" unless config.key?(key)
|
|
24
25
|
|
|
25
|
-
|
|
26
|
+
config[key]
|
|
26
27
|
end
|
|
27
28
|
end
|
|
28
29
|
end
|
|
@@ -5,16 +5,18 @@ 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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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 =
|
|
19
|
+
schema = collection.schema[:fields][field]
|
|
18
20
|
|
|
19
21
|
record[schema.foreign_key] = value.dig('data', 'id') if schema.type == 'ManyToOne'
|
|
20
22
|
end
|
|
@@ -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 =
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
11
|
-
|
|
10
|
+
context = RequestContext.new
|
|
11
|
+
context.datasource = ForestAdminAgent::Facades::Container.datasource
|
|
12
12
|
|
|
13
|
-
|
|
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
|
-
@
|
|
17
|
+
@route_collection = collection
|
|
18
18
|
super()
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
def setup_routes
|
|
22
|
-
action_index = @
|
|
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_#{@
|
|
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
|
-
|
|
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 =
|
|
64
|
-
|
|
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
|
-
|
|
73
|
+
context.datasource,
|
|
74
74
|
raw_data,
|
|
75
75
|
fields.reject { |field| field.type == 'Layout' }
|
|
76
76
|
)
|
|
77
77
|
|
|
78
|
-
{ content:
|
|
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 = (
|
|
85
|
-
|
|
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 =
|
|
90
|
-
|
|
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
|
|
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 ?
|
|
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
|
-
|
|
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
|
|
150
|
-
selection_ids = Utils::Id.parse_selection_ids(
|
|
151
|
-
selected_ids = ConditionTreeFactory.match_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 =
|
|
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,
|
|
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 <
|
|
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
|
-
|
|
18
|
+
datasource = ForestAdminAgent::Facades::Container.datasource
|
|
19
19
|
collections = args[:params]['collectionNames'] || []
|
|
20
20
|
|
|
21
|
-
connections =
|
|
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 =
|
|
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
|
-
@
|
|
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 = @
|
|
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
|
-
|
|
51
|
-
|
|
50
|
+
context.collection.render_chart(
|
|
51
|
+
context.caller,
|
|
52
52
|
@chart_name,
|
|
53
|
-
Id.unpack_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:
|
|
63
|
-
|
|
62
|
+
content: context.collection.render_chart(
|
|
63
|
+
context.caller,
|
|
64
64
|
@chart_name,
|
|
65
|
-
Id.unpack_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
|
-
|
|
40
|
+
caller = Utils::QueryStringParser.parse_caller(args)
|
|
41
|
+
datasource = ForestAdminAgent::Facades::Container.datasource
|
|
42
42
|
|
|
43
43
|
{
|
|
44
44
|
content: Serializer::ForestChartSerializer.serialize(
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
54
|
+
caller = Utils::QueryStringParser.parse_caller(args)
|
|
55
|
+
datasource = ForestAdminAgent::Facades::Container.datasource
|
|
55
56
|
|
|
56
57
|
{
|
|
57
|
-
content:
|
|
58
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
35
|
+
context.permissions.get_scope(context.collection),
|
|
39
36
|
ForestAdminAgent::Utils::QueryStringParser.parse_condition_tree(
|
|
40
|
-
|
|
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_#{
|
|
45
|
+
{ content: Serializer::ForestChartSerializer.serialize(send(:"make_#{type}", context, filter, args)) }
|
|
49
46
|
end
|
|
50
47
|
|
|
51
48
|
private
|
|
52
49
|
|
|
53
|
-
def
|
|
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
|
-
|
|
56
|
+
type.downcase
|
|
60
57
|
end
|
|
61
58
|
|
|
62
|
-
def inject_context_variables
|
|
63
|
-
user =
|
|
64
|
-
team =
|
|
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
|
-
|
|
68
|
-
return unless
|
|
64
|
+
args[:params][:contextVariables])
|
|
65
|
+
return filter unless args[:params][:filter]
|
|
69
66
|
|
|
70
|
-
|
|
71
|
-
|
|
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(
|
|
72
|
+
def make_value(context, filter, args)
|
|
73
|
+
value = compute_value(context, filter, args)
|
|
77
74
|
previous_value = nil
|
|
78
|
-
is_and_aggregator =
|
|
79
|
-
with_count_previous =
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
90
|
+
def make_pie(context, filter, args)
|
|
91
|
+
group_field = args[:params][:groupByFieldName]
|
|
94
92
|
aggregation = Aggregation.new(
|
|
95
|
-
operation:
|
|
96
|
-
field:
|
|
93
|
+
operation: args[:params][:aggregator],
|
|
94
|
+
field: args[:params][:aggregateFieldName],
|
|
97
95
|
groups: group_field ? [{ field: group_field }] : []
|
|
98
96
|
)
|
|
99
97
|
|
|
100
|
-
result =
|
|
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 =
|
|
107
|
-
time_range =
|
|
108
|
-
filter_only_with_values =
|
|
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
|
-
|
|
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 =
|
|
117
|
-
|
|
114
|
+
rows = context.collection.aggregate(
|
|
115
|
+
context.caller,
|
|
118
116
|
filter_only_with_values,
|
|
119
117
|
Aggregation.new(
|
|
120
|
-
operation:
|
|
121
|
-
field:
|
|
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 =
|
|
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
|
-
|
|
149
|
-
|
|
146
|
+
context.collection,
|
|
147
|
+
args[:params][:relationshipFieldName]
|
|
150
148
|
)
|
|
151
149
|
if inverse
|
|
152
150
|
collection = field.foreign_collection
|
|
153
|
-
|
|
151
|
+
leaderboard_filter = filter.nest(inverse)
|
|
154
152
|
aggregation = Aggregation.new(
|
|
155
|
-
operation:
|
|
156
|
-
field:
|
|
157
|
-
groups: [{ field: "#{inverse}:#{
|
|
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
|
-
|
|
165
|
-
|
|
162
|
+
context.collection,
|
|
163
|
+
args[:params][:relationshipFieldName]
|
|
166
164
|
)
|
|
167
165
|
target = ForestAdminDatasourceToolkit::Utils::Collection.get_through_target(
|
|
168
|
-
|
|
169
|
-
|
|
166
|
+
context.collection,
|
|
167
|
+
args[:params][:relationshipFieldName]
|
|
170
168
|
)
|
|
171
169
|
if origin && target
|
|
172
170
|
collection = field.through_collection
|
|
173
|
-
|
|
171
|
+
leaderboard_filter = filter.nest(origin)
|
|
174
172
|
aggregation = Aggregation.new(
|
|
175
|
-
operation:
|
|
176
|
-
field:
|
|
177
|
-
groups: [{ field: "#{origin}:#{
|
|
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 &&
|
|
183
|
-
rows =
|
|
184
|
-
|
|
185
|
-
|
|
180
|
+
if collection && leaderboard_filter && aggregation
|
|
181
|
+
rows = context.datasource.get_collection(collection).aggregate(
|
|
182
|
+
context.caller,
|
|
183
|
+
leaderboard_filter,
|
|
186
184
|
aggregation,
|
|
187
|
-
|
|
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:
|
|
206
|
-
field:
|
|
207
|
-
result =
|
|
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
|