forest_admin_agent 1.0.0.pre.beta.47 → 1.0.0.pre.beta.49
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/forest_admin_agent.gemspec +1 -1
- data/lib/forest_admin_agent/builder/agent_factory.rb +10 -0
- data/lib/forest_admin_agent/facades/container.rb +3 -1
- data/lib/forest_admin_agent/http/Exceptions/authentication_open_id_client.rb +2 -2
- data/lib/forest_admin_agent/http/error_handling.rb +18 -0
- data/lib/forest_admin_agent/http/router.rb +2 -0
- data/lib/forest_admin_agent/routes/abstract_route.rb +2 -2
- data/lib/forest_admin_agent/routes/action/action.rb +1 -1
- data/lib/forest_admin_agent/routes/charts/api_chart_datasource.rb +9 -5
- data/lib/forest_admin_agent/routes/resources/csv.rb +51 -0
- data/lib/forest_admin_agent/routes/resources/list.rb +3 -2
- data/lib/forest_admin_agent/routes/resources/related/count_related.rb +1 -1
- data/lib/forest_admin_agent/routes/resources/related/csv_related.rb +60 -0
- data/lib/forest_admin_agent/routes/resources/related/list_related.rb +0 -1
- data/lib/forest_admin_agent/serializer/forest_serializer.rb +3 -3
- data/lib/forest_admin_agent/services/logger_service.rb +1 -2
- data/lib/forest_admin_agent/services/permissions.rb +3 -2
- data/lib/forest_admin_agent/services/sse_cache_invalidation.rb +19 -7
- data/lib/forest_admin_agent/utils/csv_generator.rb +44 -0
- data/lib/forest_admin_agent/utils/id.rb +1 -1
- data/lib/forest_admin_agent/utils/query_string_parser.rb +15 -4
- data/lib/forest_admin_agent/utils/schema/forest_value_converter.rb +12 -11
- data/lib/forest_admin_agent/utils/schema/frontend_validation_utils.rb +127 -0
- data/lib/forest_admin_agent/utils/schema/generator_action.rb +2 -1
- data/lib/forest_admin_agent/utils/schema/generator_collection.rb +9 -1
- data/lib/forest_admin_agent/utils/schema/generator_field.rb +11 -11
- data/lib/forest_admin_agent/utils/schema/schema_emitter.rb +1 -1
- data/lib/forest_admin_agent/version.rb +1 -1
- metadata +8 -4
- data/README.md +0 -35
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 23ea6e5c071b956705a5f6fb880be2a837c07714c33117e20304ad07921ab7bf
|
4
|
+
data.tar.gz: 7637f501d7dcf319eedace4183e2b40675c71bd8faa5fc06e62a5a72f91c3580
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0656cfcadb4df52020d45f816f49332337686c77ec5d1996448aa67f246be319aa8d1a43860ce63c558be15106751c249aff61fb2f9d27303c934b99bbe4d4a1
|
7
|
+
data.tar.gz: b9ac0d65d0dc13e3a5c011b22ccef0e5208da775087d3b95fbcc17ec7cc4427d78b5ad4e50c4d7b43b10e24750dc610928806dcde69abacf43e7a41f230411f7
|
data/forest_admin_agent.gemspec
CHANGED
@@ -17,7 +17,7 @@ admin work on any Ruby application."
|
|
17
17
|
|
18
18
|
spec.metadata["homepage_uri"] = spec.homepage
|
19
19
|
spec.metadata["source_code_uri"] = "https://github.com/ForestAdmin/agent-ruby"
|
20
|
-
spec.metadata["changelog_uri"] = "https://github.com/ForestAdmin/agent-ruby/CHANGELOG.md"
|
20
|
+
spec.metadata["changelog_uri"] = "https://github.com/ForestAdmin/agent-ruby/blob/main/CHANGELOG.md"
|
21
21
|
spec.metadata["rubygems_mfa_required"] = "false"
|
22
22
|
|
23
23
|
# Specify which files should be added to the gem when it is released.
|
@@ -34,6 +34,8 @@ module ForestAdminAgent
|
|
34
34
|
|
35
35
|
def customize_collection(name, &handle)
|
36
36
|
@customizer.customize_collection(name, handle)
|
37
|
+
|
38
|
+
self
|
37
39
|
end
|
38
40
|
|
39
41
|
def build
|
@@ -74,6 +76,14 @@ module ForestAdminAgent
|
|
74
76
|
return unless @has_env_secret
|
75
77
|
|
76
78
|
cache = @container.resolve(:cache)
|
79
|
+
@options[:customize_error_message] = @options[:customize_error_message]
|
80
|
+
&.source
|
81
|
+
&.strip
|
82
|
+
&.delete_prefix('config.customize_error_message =')
|
83
|
+
&.strip
|
84
|
+
|
85
|
+
@options[:logger] = @options[:logger]&.source&.strip&.delete_prefix('config.logger =')&.strip
|
86
|
+
|
77
87
|
cache.set('config', @options.to_h)
|
78
88
|
end
|
79
89
|
|
@@ -2,12 +2,12 @@ module ForestAdminAgent
|
|
2
2
|
module Http
|
3
3
|
module Exceptions
|
4
4
|
class AuthenticationOpenIdClient < HttpException
|
5
|
-
attr_reader :error, :
|
5
|
+
attr_reader :error, :message, :state
|
6
6
|
|
7
7
|
def initialize(error, error_description, state)
|
8
8
|
super(error, 401, error_description)
|
9
9
|
@error = error
|
10
|
-
@
|
10
|
+
@message = error_description
|
11
11
|
@state = state
|
12
12
|
end
|
13
13
|
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module ForestAdminAgent
|
2
|
+
module Http
|
3
|
+
module ErrorHandling
|
4
|
+
def get_error_message(error)
|
5
|
+
if error.respond_to?(:ancestors) && error.ancestors.include?(ForestAdminAgent::Http::Exceptions::HttpException)
|
6
|
+
return error.message
|
7
|
+
end
|
8
|
+
|
9
|
+
if (customizer = ForestAdminAgent::Facades::Container.cache(:customize_error_message))
|
10
|
+
message = eval(customizer).call(error)
|
11
|
+
return message if message
|
12
|
+
end
|
13
|
+
|
14
|
+
'Unexpected error'
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -12,10 +12,12 @@ module ForestAdminAgent
|
|
12
12
|
Charts::Charts.new.routes,
|
13
13
|
Resources::Count.new.routes,
|
14
14
|
Resources::Delete.new.routes,
|
15
|
+
Resources::Csv.new.routes,
|
15
16
|
Resources::List.new.routes,
|
16
17
|
Resources::Show.new.routes,
|
17
18
|
Resources::Store.new.routes,
|
18
19
|
Resources::Update.new.routes,
|
20
|
+
Resources::Related::CsvRelated.new.routes,
|
19
21
|
Resources::Related::ListRelated.new.routes,
|
20
22
|
Resources::Related::CountRelated.new.routes,
|
21
23
|
Resources::Related::AssociateRelated.new.routes,
|
@@ -15,8 +15,8 @@ module ForestAdminAgent
|
|
15
15
|
@routes ||= {}
|
16
16
|
end
|
17
17
|
|
18
|
-
def add_route(name, method, uri, closure)
|
19
|
-
@routes[name] = { method: method, uri: uri, closure: closure }
|
18
|
+
def add_route(name, method, uri, closure, format = 'json')
|
19
|
+
@routes[name] = { method: method, uri: uri, closure: closure, format: format }
|
20
20
|
end
|
21
21
|
|
22
22
|
def setup_routes
|
@@ -20,24 +20,26 @@ module ForestAdminAgent
|
|
20
20
|
"forest_chart_get_#{slug}",
|
21
21
|
'get',
|
22
22
|
"/_charts/#{slug}",
|
23
|
-
proc { handle_smart_chart }
|
23
|
+
proc { |args| handle_smart_chart(args) }
|
24
24
|
)
|
25
25
|
|
26
26
|
add_route(
|
27
27
|
"forest_chart_post_#{slug}",
|
28
28
|
'post',
|
29
29
|
"/_charts/#{slug}",
|
30
|
-
proc { handle_api_chart }
|
30
|
+
proc { |args| handle_api_chart(args) }
|
31
31
|
)
|
32
32
|
|
33
33
|
unless Facades::Container.cache(:is_production)
|
34
|
-
Facades::Container.logger.log('Info', "/forest/_charts/#{slug}")
|
34
|
+
Facades::Container.logger.log('Info', "Chart #{@chart_name} was mounted at /forest/_charts/#{slug}")
|
35
35
|
end
|
36
36
|
|
37
37
|
self
|
38
38
|
end
|
39
39
|
|
40
|
-
def handle_api_chart
|
40
|
+
def handle_api_chart(args = {})
|
41
|
+
@caller = Utils::QueryStringParser.parse_caller(args)
|
42
|
+
|
41
43
|
{
|
42
44
|
content: Serializer::ForestChartSerializer.serialize(
|
43
45
|
@datasource.render_chart(
|
@@ -48,7 +50,9 @@ module ForestAdminAgent
|
|
48
50
|
}
|
49
51
|
end
|
50
52
|
|
51
|
-
def handle_smart_chart
|
53
|
+
def handle_smart_chart(args = {})
|
54
|
+
@caller = Utils::QueryStringParser.parse_caller(args)
|
55
|
+
|
52
56
|
{
|
53
57
|
content: @datasource.render_chart(
|
54
58
|
@caller,
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module ForestAdminAgent
|
2
|
+
module Routes
|
3
|
+
module Resources
|
4
|
+
class Csv < AbstractAuthenticatedRoute
|
5
|
+
include ForestAdminDatasourceToolkit::Components::Query::ConditionTree
|
6
|
+
include ForestAdminAgent::Utils
|
7
|
+
|
8
|
+
def setup_routes
|
9
|
+
add_route(
|
10
|
+
'forest_list_csv',
|
11
|
+
'get',
|
12
|
+
'/:collection_name.:format',
|
13
|
+
->(args) { handle_request(args) },
|
14
|
+
'csv'
|
15
|
+
)
|
16
|
+
|
17
|
+
self
|
18
|
+
end
|
19
|
+
|
20
|
+
def handle_request(args = {})
|
21
|
+
build(args)
|
22
|
+
@permissions.can?(:browse, @collection)
|
23
|
+
@permissions.can?(:export, @collection)
|
24
|
+
filter = ForestAdminDatasourceToolkit::Components::Query::Filter.new(
|
25
|
+
condition_tree: ConditionTreeFactory.intersect(
|
26
|
+
[
|
27
|
+
@permissions.get_scope(@collection),
|
28
|
+
ForestAdminAgent::Utils::QueryStringParser.parse_condition_tree(
|
29
|
+
@collection, args
|
30
|
+
)
|
31
|
+
]
|
32
|
+
),
|
33
|
+
search: ForestAdminAgent::Utils::QueryStringParser.parse_search(@collection, args),
|
34
|
+
search_extended: ForestAdminAgent::Utils::QueryStringParser.parse_search_extended(args)
|
35
|
+
)
|
36
|
+
projection = ForestAdminAgent::Utils::QueryStringParser.parse_projection(@collection, args)
|
37
|
+
records = @collection.list(@caller, filter, projection)
|
38
|
+
filename = args[:params][:filename] || "#{args[:params]["collection_name"]}.csv"
|
39
|
+
filename += '.csv' unless /\.csv$/i.match?(filename)
|
40
|
+
|
41
|
+
{
|
42
|
+
content: {
|
43
|
+
export: CsvGenerator.generate(records, projection)
|
44
|
+
},
|
45
|
+
filename: filename
|
46
|
+
}
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -4,7 +4,6 @@ module ForestAdminAgent
|
|
4
4
|
module Routes
|
5
5
|
module Resources
|
6
6
|
class List < AbstractAuthenticatedRoute
|
7
|
-
include ForestAdminAgent::Builder
|
8
7
|
include ForestAdminDatasourceToolkit::Components::Query::ConditionTree
|
9
8
|
def setup_routes
|
10
9
|
add_route('forest_list', 'get', '/:collection_name', ->(args) { handle_request(args) })
|
@@ -24,7 +23,9 @@ module ForestAdminAgent
|
|
24
23
|
]),
|
25
24
|
page: ForestAdminAgent::Utils::QueryStringParser.parse_pagination(args),
|
26
25
|
search: ForestAdminAgent::Utils::QueryStringParser.parse_search(@collection, args),
|
27
|
-
search_extended: ForestAdminAgent::Utils::QueryStringParser.parse_search_extended(args)
|
26
|
+
search_extended: ForestAdminAgent::Utils::QueryStringParser.parse_search_extended(args),
|
27
|
+
sort: ForestAdminAgent::Utils::QueryStringParser.parse_sort(@collection, args),
|
28
|
+
segment: ForestAdminAgent::Utils::QueryStringParser.parse_segment(@collection, args)
|
28
29
|
)
|
29
30
|
|
30
31
|
projection = ForestAdminAgent::Utils::QueryStringParser.parse_projection_with_pks(@collection, args)
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module ForestAdminAgent
|
2
|
+
module Routes
|
3
|
+
module Resources
|
4
|
+
module Related
|
5
|
+
class CsvRelated < AbstractRelatedRoute
|
6
|
+
include ForestAdminAgent::Utils
|
7
|
+
include ForestAdminDatasourceToolkit::Utils
|
8
|
+
include ForestAdminDatasourceToolkit::Components::Query::ConditionTree
|
9
|
+
|
10
|
+
def setup_routes
|
11
|
+
add_route(
|
12
|
+
'forest_related_list_csv',
|
13
|
+
'get',
|
14
|
+
'/:collection_name/:id/relationships/:relation_name.:format',
|
15
|
+
->(args) { handle_request(args) },
|
16
|
+
'csv'
|
17
|
+
)
|
18
|
+
|
19
|
+
self
|
20
|
+
end
|
21
|
+
|
22
|
+
def handle_request(args = {})
|
23
|
+
build(args)
|
24
|
+
@permissions.can?(:browse, @collection)
|
25
|
+
@permissions.can?(:export, @collection)
|
26
|
+
|
27
|
+
filter = ForestAdminDatasourceToolkit::Components::Query::Filter.new(
|
28
|
+
condition_tree: ConditionTreeFactory.intersect(
|
29
|
+
[
|
30
|
+
@permissions.get_scope(@collection),
|
31
|
+
ForestAdminAgent::Utils::QueryStringParser.parse_condition_tree(@child_collection, args)
|
32
|
+
]
|
33
|
+
)
|
34
|
+
)
|
35
|
+
projection = ForestAdminAgent::Utils::QueryStringParser.parse_projection_with_pks(@child_collection, args)
|
36
|
+
id = Utils::Id.unpack_id(@collection, args[:params]['id'], with_key: true)
|
37
|
+
records = Collection.list_relation(
|
38
|
+
@collection,
|
39
|
+
id,
|
40
|
+
args[:params]['relation_name'],
|
41
|
+
@caller,
|
42
|
+
filter,
|
43
|
+
projection
|
44
|
+
)
|
45
|
+
|
46
|
+
filename = args[:params][:filename] || "#{args[:params]["relation_name"]}.csv"
|
47
|
+
filename += '.csv' unless /\.csv$/i.match?(filename)
|
48
|
+
|
49
|
+
{
|
50
|
+
content: {
|
51
|
+
export: CsvGenerator.generate(records, projection)
|
52
|
+
},
|
53
|
+
filename: filename
|
54
|
+
}
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -23,7 +23,6 @@ module ForestAdminAgent
|
|
23
23
|
def handle_request(args = {})
|
24
24
|
build(args)
|
25
25
|
@permissions.can?(:browse, @collection)
|
26
|
-
# TODO: add csv behaviour
|
27
26
|
|
28
27
|
filter = ForestAdminDatasourceToolkit::Components::Query::Filter.new(
|
29
28
|
condition_tree: ConditionTreeFactory.intersect(
|
@@ -16,7 +16,7 @@ module ForestAdminAgent
|
|
16
16
|
end
|
17
17
|
|
18
18
|
def base_url
|
19
|
-
|
19
|
+
'/forest'
|
20
20
|
end
|
21
21
|
|
22
22
|
def type
|
@@ -177,11 +177,11 @@ module ForestAdminAgent
|
|
177
177
|
end
|
178
178
|
|
179
179
|
def relationship_self_link(attribute_name)
|
180
|
-
"
|
180
|
+
"#{self_link}/relationships/#{format_name(attribute_name)}"
|
181
181
|
end
|
182
182
|
|
183
183
|
def relationship_related_link(attribute_name)
|
184
|
-
"
|
184
|
+
"#{self_link}/#{format_name(attribute_name)}"
|
185
185
|
end
|
186
186
|
end
|
187
187
|
end
|
@@ -16,12 +16,11 @@ module ForestAdminAgent
|
|
16
16
|
@logger_level = logger_level
|
17
17
|
@logger = logger
|
18
18
|
@default_logger = MonoLogger.new($stdout)
|
19
|
-
# TODO: HANDLE FORMATTER
|
20
19
|
end
|
21
20
|
|
22
21
|
def log(level, message)
|
23
22
|
if @logger
|
24
|
-
@logger.call(get_level(level), message)
|
23
|
+
eval(@logger).call(get_level(level), message)
|
25
24
|
else
|
26
25
|
@default_logger.add(get_level(level), message)
|
27
26
|
end
|
@@ -95,7 +95,7 @@ module ForestAdminAgent
|
|
95
95
|
filter
|
96
96
|
)
|
97
97
|
|
98
|
-
smart_action_approval.can_execute?
|
98
|
+
is_allowed = smart_action_approval.can_execute?
|
99
99
|
ForestAdminAgent::Facades::Container.logger.log(
|
100
100
|
'Debug',
|
101
101
|
"User #{user_data[:roleId]} is #{is_allowed ? "" : "not"} allowed to perform #{action["name"]}"
|
@@ -213,7 +213,8 @@ module ForestAdminAgent
|
|
213
213
|
end[:enable]
|
214
214
|
end
|
215
215
|
|
216
|
-
def find_action_from_endpoint(collection_name,
|
216
|
+
def find_action_from_endpoint(collection_name, path, http_method)
|
217
|
+
endpoint = path.partition('/forest/')[1..].join
|
217
218
|
schema_file = JSON.parse(File.read(Facades::Container.config_from_cache[:schema_path]))
|
218
219
|
actions = schema_file['collections']&.select { |collection| collection['name'] == collection_name }&.first&.dig('actions')
|
219
220
|
|
@@ -26,18 +26,30 @@ module ForestAdminAgent
|
|
26
26
|
|
27
27
|
MESSAGE_CACHE_KEYS[event.type]&.each do |cache_key|
|
28
28
|
Permissions.invalidate_cache(cache_key)
|
29
|
-
|
30
|
-
|
29
|
+
ForestAdminAgent::Facades::Container.logger.log(
|
30
|
+
'Info',
|
31
|
+
"invalidate cache #{MESSAGE_CACHE_KEYS[event.type]} for event #{event.type}"
|
32
|
+
)
|
31
33
|
end
|
32
|
-
|
33
|
-
|
34
|
+
|
35
|
+
ForestAdminAgent::Facades::Container.logger.log(
|
36
|
+
'Info',
|
37
|
+
"SSECacheInvalidation: unhandled message from server: #{event.type}"
|
38
|
+
)
|
34
39
|
end
|
35
40
|
end
|
36
41
|
rescue StandardError
|
42
|
+
ForestAdminAgent::Facades::Container.logger.log(
|
43
|
+
'Debug',
|
44
|
+
'SSE connection to forestadmin server'
|
45
|
+
)
|
46
|
+
|
47
|
+
ForestAdminAgent::Facades::Container.logger.log(
|
48
|
+
'Warning',
|
49
|
+
'SSE connection to forestadmin server closed unexpectedly, retrying.'
|
50
|
+
)
|
51
|
+
|
37
52
|
raise ForestException, 'Failed to reach SSE data from ForestAdmin server.'
|
38
|
-
# TODO: HANDLE LOGGER
|
39
|
-
# "debug", "SSE connection to forestadmin server due to ..."
|
40
|
-
# "warning", "SSE connection to forestadmin server closed unexpectedly, retrying."
|
41
53
|
end
|
42
54
|
end
|
43
55
|
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'csv'
|
2
|
+
|
3
|
+
module ForestAdminAgent
|
4
|
+
module Utils
|
5
|
+
class CsvGenerator
|
6
|
+
def self.generate(records, projection)
|
7
|
+
data = {}
|
8
|
+
projection.each do |schema_field|
|
9
|
+
is_relation = schema_field.include?(':') && projection.relations.key?(schema_field.split(':').first)
|
10
|
+
col_name = (is_relation ? schema_field.split(':').first : schema_field)
|
11
|
+
|
12
|
+
data[col_name] = []
|
13
|
+
records.each do |row|
|
14
|
+
data[col_name] << if is_relation
|
15
|
+
row[col_name][schema_field.split(':').last]
|
16
|
+
else
|
17
|
+
row[col_name]
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
generate_csv_string(data)
|
23
|
+
end
|
24
|
+
|
25
|
+
# data = {
|
26
|
+
# "id" => [1, 2],
|
27
|
+
# "email" => ["mv@test.com", "na@test.com"],
|
28
|
+
# "name" => ["Matthieu", "Nicolas"],
|
29
|
+
# }
|
30
|
+
def self.generate_csv_string(data)
|
31
|
+
CSV.generate do |csv|
|
32
|
+
# headers
|
33
|
+
csv << data.keys
|
34
|
+
|
35
|
+
num_rows = data.values.first.size
|
36
|
+
num_rows.times do |i|
|
37
|
+
row = data.keys.map { |key| data[key][i] }
|
38
|
+
csv << row
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -27,7 +27,7 @@ module ForestAdminAgent
|
|
27
27
|
field = collection.schema[:fields][pk_name]
|
28
28
|
value = primary_key_values[index]
|
29
29
|
casted_value = field.column_type == 'Number' ? value.to_i : value
|
30
|
-
|
30
|
+
ForestAdminDatasourceToolkit::Validations::FieldValidator.validate_value(value, field, casted_value)
|
31
31
|
|
32
32
|
[pk_name, casted_value]
|
33
33
|
end.to_h
|
@@ -23,16 +23,16 @@ module ForestAdminAgent
|
|
23
23
|
return if filters.nil?
|
24
24
|
|
25
25
|
filters = JSON.parse(filters, symbolize_names: true) if filters.is_a? String
|
26
|
-
# TODO: add else for convert all keys to sym
|
27
26
|
|
28
27
|
ConditionTreeParser.from_plain_object(collection, filters)
|
29
|
-
# TODO: ConditionTreeValidator::validate($conditionTree, $collection);
|
30
28
|
end
|
31
29
|
|
32
30
|
def self.parse_caller(args)
|
33
31
|
unless args.dig(:headers, 'HTTP_AUTHORIZATION')
|
34
|
-
|
35
|
-
|
32
|
+
raise Http::Exceptions::HttpException.new(
|
33
|
+
401,
|
34
|
+
'You must be logged in to access at this resource.'
|
35
|
+
)
|
36
36
|
end
|
37
37
|
|
38
38
|
timezone = args[:params]['timezone']
|
@@ -125,6 +125,17 @@ module ForestAdminAgent
|
|
125
125
|
|
126
126
|
sort
|
127
127
|
end
|
128
|
+
|
129
|
+
def self.parse_segment(collection, args)
|
130
|
+
segment = args.dig(:params, :data, :attributes, :all_records_subset_query,
|
131
|
+
:segment) || args.dig(:params, :segment)
|
132
|
+
|
133
|
+
return unless segment
|
134
|
+
|
135
|
+
raise ForestException, "Invalid segment: #{segment}" unless collection.schema[:segments].include?(segment)
|
136
|
+
|
137
|
+
segment
|
138
|
+
end
|
128
139
|
end
|
129
140
|
end
|
130
141
|
end
|
@@ -29,19 +29,20 @@ module ForestAdminAgent
|
|
29
29
|
|
30
30
|
def self.make_form_data_from_fields(datasource, fields)
|
31
31
|
data = {}
|
32
|
-
fields.each_value do |field|
|
33
|
-
next if Schema::GeneratorAction::DEFAULT_FIELDS.map { |f| f[:field] }.include?(field.field)
|
34
32
|
|
35
|
-
|
36
|
-
|
33
|
+
fields.each do |field|
|
34
|
+
next if Schema::GeneratorAction::DEFAULT_FIELDS.map { |f| f[:field] }.include?(field['field'])
|
35
|
+
|
36
|
+
if field['reference'] && field['value']
|
37
|
+
collection_name = field['reference'].split('.').first
|
37
38
|
collection = datasource.get_collection(collection_name)
|
38
|
-
data[field
|
39
|
-
elsif field
|
40
|
-
data[field
|
41
|
-
elsif field
|
42
|
-
data[field
|
39
|
+
data[field['field']] = Utils::Id.unpack_id(collection, field['value'])
|
40
|
+
elsif field['type'] == 'File'
|
41
|
+
data[field['field']] = parse_data_uri(field['value'])
|
42
|
+
elsif field['type'].is_a?(Array) && field['type'][0] == 'File'
|
43
|
+
data[field['field']] = field['value'].map { |v| parse_data_uri(v) }
|
43
44
|
else
|
44
|
-
data[field
|
45
|
+
data[field['field']] = field['value']
|
45
46
|
end
|
46
47
|
end
|
47
48
|
|
@@ -83,7 +84,7 @@ module ForestAdminAgent
|
|
83
84
|
|
84
85
|
return field.value.select { |v| field.enum_values.include?(v) } if ActionFields.enum_list_field?(field)
|
85
86
|
|
86
|
-
return field.value
|
87
|
+
return field.value&.join('|') if ActionFields.collection_field?(field)
|
87
88
|
|
88
89
|
# return make_data_uri(field.value) if ActionFields.file_field?(field)
|
89
90
|
#
|
@@ -0,0 +1,127 @@
|
|
1
|
+
module ForestAdminAgent
|
2
|
+
module Utils
|
3
|
+
module Schema
|
4
|
+
class FrontendValidationUtils
|
5
|
+
include ForestAdminDatasourceToolkit::Components::Query::ConditionTree
|
6
|
+
|
7
|
+
# Those operators depend on the current time so they won't work.
|
8
|
+
# The reason is that we need now() to be evaluated at query time, not at schema generation time.
|
9
|
+
EXCLUDED = [Operators::FUTURE, Operators::PAST, Operators::TODAY, Operators::YESTERDAY,
|
10
|
+
Operators::PREVIOUS_MONTH, Operators::PREVIOUS_QUARTER, Operators::PREVIOUS_WEEK,
|
11
|
+
Operators::PREVIOUS_X_DAYS, Operators::PREVIOUS_YEAR, Operators::AFTER_X_HOURS_AGO,
|
12
|
+
Operators::BEFORE_X_HOURS_AGO, Operators::PREVIOUS_X_DAYS_TO_DATE,
|
13
|
+
Operators::PREVIOUS_MONTH_TO_DATE, Operators::PREVIOUS_QUARTER_TO_DATE,
|
14
|
+
Operators::PREVIOUS_WEEK_TO_DATE, Operators::PREVIOUS_YEAR_TO_DATE].freeze
|
15
|
+
|
16
|
+
SUPPORTED = {
|
17
|
+
Operators::PRESENT => proc { { type: 'is present', message: 'Field is required' } },
|
18
|
+
Operators::AFTER => proc do |rule|
|
19
|
+
{ type: 'is after', value: rule[:value], message: "Value must be after #{rule[:value]}" }
|
20
|
+
end,
|
21
|
+
Operators::BEFORE => proc do |rule|
|
22
|
+
{ type: 'is before', value: rule[:value], message: "Value must be before #{rule[:value]}" }
|
23
|
+
end,
|
24
|
+
Operators::CONTAINS => proc do |rule|
|
25
|
+
{ type: 'is contains', value: rule[:value], message: "Value must contain #{rule[:value]}" }
|
26
|
+
end,
|
27
|
+
Operators::GREATER_THAN => proc do |rule|
|
28
|
+
{ type: 'is greater than', value: rule[:value], message: "Value must be greater than #{rule[:value]}" }
|
29
|
+
end,
|
30
|
+
Operators::LESS_THAN => proc do |rule|
|
31
|
+
{ type: 'is less than', value: rule[:value], message: "Value must be lower than #{rule[:value]}" }
|
32
|
+
end,
|
33
|
+
Operators::LONGER_THAN => proc do |rule|
|
34
|
+
{ type: 'is longer than', value: rule[:value],
|
35
|
+
message: "Value must be longer than #{rule[:value]} characters" }
|
36
|
+
end,
|
37
|
+
Operators::SHORTER_THAN => proc do |rule|
|
38
|
+
{
|
39
|
+
type: 'is shorter than',
|
40
|
+
value: rule[:value],
|
41
|
+
message: "Value must be shorter than #{rule[:value]} characters"
|
42
|
+
}
|
43
|
+
end,
|
44
|
+
Operators::MATCH => proc do |rule|
|
45
|
+
{
|
46
|
+
type: 'is like', # `is like` actually expects a regular expression, not a 'like pattern'
|
47
|
+
value: rule[:value].to_s,
|
48
|
+
message: "Value must match #{rule[:value]}"
|
49
|
+
}
|
50
|
+
end
|
51
|
+
}.freeze
|
52
|
+
|
53
|
+
def self.convert_validation_list(column)
|
54
|
+
return [] if column.validations.empty?
|
55
|
+
|
56
|
+
rules = column.validations.dup.map { |rule| simplify_rule(column.column_type, rule) }
|
57
|
+
remove_duplicates_in_place(rules).map { |rule| SUPPORTED[rule[:operator]].call(rule) }
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.simplify_rule(column_type, rule)
|
61
|
+
return [] if EXCLUDED.include?(rule[:operator])
|
62
|
+
|
63
|
+
return rule if SUPPORTED.key?(rule[:operator])
|
64
|
+
|
65
|
+
begin
|
66
|
+
# Add the 'Equal|NotEqual' operators to unlock the `In|NotIn -> Match` replacement rules.
|
67
|
+
# This is a bit hacky, but it allows to reuse the existing logic.
|
68
|
+
operators = SUPPORTED.keys
|
69
|
+
operators << Operators::EQUAL
|
70
|
+
operators << Operators::NOT_EQUAL
|
71
|
+
|
72
|
+
# Rewrite the rule to use only operators that the frontend supports.
|
73
|
+
leaf = Nodes::ConditionTreeLeaf.new('field', rule[:operator], rule[:value])
|
74
|
+
timezone = 'Europe/Paris' # we're sending the schema => use random tz
|
75
|
+
tree = ConditionTreeEquivalent.get_equivalent_tree(leaf, operators, column_type, timezone)
|
76
|
+
|
77
|
+
if tree.is_a? Nodes::ConditionTreeLeaf
|
78
|
+
[tree]
|
79
|
+
else
|
80
|
+
tree.conditions
|
81
|
+
end
|
82
|
+
rescue StandardError
|
83
|
+
# Just ignore errors, they mean that the operator is not supported by the frontend
|
84
|
+
# and that we don't have an automatic conversion for it.
|
85
|
+
#
|
86
|
+
# In that case we fallback to just validating the data entry in the agent (which is better
|
87
|
+
# than nothing but will not be as user friendly as the frontend validation).
|
88
|
+
end
|
89
|
+
|
90
|
+
# Drop the rule if we don't know how to convert it (we could log a warning here).
|
91
|
+
[]
|
92
|
+
end
|
93
|
+
|
94
|
+
# The frontend crashes when it receives multiple rules of the same type.
|
95
|
+
# This method merges the rules which can be merged and drops the others.
|
96
|
+
def self.remove_duplicates_in_place(rules)
|
97
|
+
used = {}
|
98
|
+
rules.each_with_index do |rule, key|
|
99
|
+
if used.key?(rule[:operator])
|
100
|
+
rule = rules[rule[:operator]]
|
101
|
+
new_rule = rule
|
102
|
+
rules.delete(key)
|
103
|
+
rules[used[rule[:operator]]] = merge_into(rule, new_rule)
|
104
|
+
else
|
105
|
+
used[rule[:operator]] = key
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
rules
|
110
|
+
end
|
111
|
+
|
112
|
+
def merge_into(rule, new_rule)
|
113
|
+
if [Operators::GREATER_THAN, Operators::AFTER, Operators::LONGER_THAN].include? rule[:operator]
|
114
|
+
rule[:value] = [rule[:value], new_rule[:value]].max
|
115
|
+
elsif [Operators::LESS_THAN, Operators::BEFORE, Operators::SHORTER_THAN].include? rule[:operator]
|
116
|
+
rule[:value] = [rule[:value], new_rule[:value]].min
|
117
|
+
elsif rule[:operator] == Operators::MATCH
|
118
|
+
# TODO
|
119
|
+
end
|
120
|
+
# else Ignore the rules that we can't deduplicate (we could log a warning here).
|
121
|
+
|
122
|
+
rule
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
@@ -59,7 +59,7 @@ module ForestAdminAgent
|
|
59
59
|
|
60
60
|
if ActionFields.collection_field?(field)
|
61
61
|
collection = datasource.get_collection(field.collection_name)
|
62
|
-
pk = ForestAdminDatasourceToolkit::Utils::Schema.primary_keys(collection)
|
62
|
+
pk = ForestAdminDatasourceToolkit::Utils::Schema.primary_keys(collection)[0]
|
63
63
|
pk_schema = collection.schema[:fields][pk]
|
64
64
|
|
65
65
|
output[:type] = pk_schema.column_type
|
@@ -82,6 +82,7 @@ module ForestAdminAgent
|
|
82
82
|
return DEFAULT_FIELDS unless action.static_form?
|
83
83
|
|
84
84
|
fields = collection.get_form(nil, name)
|
85
|
+
|
85
86
|
if fields
|
86
87
|
return fields.map do |field|
|
87
88
|
new_field = build_field_schema(collection.datasource, field)
|
@@ -16,7 +16,7 @@ module ForestAdminAgent
|
|
16
16
|
name: collection.name,
|
17
17
|
onlyForRelationships: false,
|
18
18
|
paginationType: 'page',
|
19
|
-
segments:
|
19
|
+
segments: build_segments(collection)
|
20
20
|
}
|
21
21
|
end
|
22
22
|
|
@@ -37,6 +37,14 @@ module ForestAdminAgent
|
|
37
37
|
{}
|
38
38
|
end
|
39
39
|
end
|
40
|
+
|
41
|
+
def self.build_segments(collection)
|
42
|
+
if collection.schema[:segments]
|
43
|
+
collection.schema[:segments].keys.sort.map { |name| { id: "#{collection.name}.#{name}", name: name } }
|
44
|
+
else
|
45
|
+
[]
|
46
|
+
end
|
47
|
+
end
|
40
48
|
end
|
41
49
|
end
|
42
50
|
end
|
@@ -35,8 +35,7 @@ module ForestAdminAgent
|
|
35
35
|
field: name,
|
36
36
|
integration: nil,
|
37
37
|
inverseOf: nil,
|
38
|
-
|
39
|
-
isFilterable: true, # TODO: remove when implementing operators decorators
|
38
|
+
isFilterable: FrontendFilterable.filterable?(column.column_type, column.filter_operators),
|
40
39
|
isPrimaryKey: column.is_primary_key,
|
41
40
|
|
42
41
|
# When a column is a foreign key, it is readonly.
|
@@ -48,7 +47,7 @@ module ForestAdminAgent
|
|
48
47
|
isVirtual: false,
|
49
48
|
reference: nil,
|
50
49
|
type: convert_column_type(column.column_type),
|
51
|
-
validations:
|
50
|
+
validations: FrontendValidationUtils.convert_validation_list(column)
|
52
51
|
}
|
53
52
|
end
|
54
53
|
|
@@ -67,9 +66,10 @@ module ForestAdminAgent
|
|
67
66
|
}
|
68
67
|
end
|
69
68
|
|
70
|
-
def foreign_collection_filterable?
|
71
|
-
|
72
|
-
|
69
|
+
def foreign_collection_filterable?(foreign_collection)
|
70
|
+
foreign_collection.schema[:fields].values.any? do |field|
|
71
|
+
field.type == 'Column' && FrontendFilterable.filterable?(field.column_type, field.filter_operators)
|
72
|
+
end
|
73
73
|
end
|
74
74
|
|
75
75
|
def build_many_to_many_schema(relation, collection, foreign_collection, base_schema)
|
@@ -122,7 +122,7 @@ module ForestAdminAgent
|
|
122
122
|
{
|
123
123
|
type: key_field.column_type,
|
124
124
|
defaultValue: nil,
|
125
|
-
isFilterable: foreign_collection_filterable
|
125
|
+
isFilterable: foreign_collection_filterable?(foreign_collection),
|
126
126
|
isPrimaryKey: false,
|
127
127
|
isRequired: false,
|
128
128
|
isReadOnly: key_field.is_read_only,
|
@@ -140,12 +140,12 @@ module ForestAdminAgent
|
|
140
140
|
{
|
141
141
|
type: key_field.column_type,
|
142
142
|
defaultValue: key_field.default_value,
|
143
|
-
isFilterable: foreign_collection_filterable
|
143
|
+
isFilterable: foreign_collection_filterable?(foreign_collection),
|
144
144
|
isPrimaryKey: false,
|
145
|
-
isRequired:
|
145
|
+
isRequired: key_field.validations.any? { |v| v[:operator] == 'Present' },
|
146
146
|
isReadOnly: key_field.is_read_only,
|
147
147
|
isSortable: key_field.is_sortable,
|
148
|
-
validations:
|
148
|
+
validations: FrontendValidationUtils.convert_validation_list(key_field),
|
149
149
|
reference: "#{foreign_collection.name}.#{relation.foreign_key_target}"
|
150
150
|
}
|
151
151
|
)
|
@@ -161,7 +161,7 @@ module ForestAdminAgent
|
|
161
161
|
integration: nil,
|
162
162
|
isReadOnly: nil,
|
163
163
|
isVirtual: false,
|
164
|
-
inverseOf:
|
164
|
+
inverseOf: ForestAdminDatasourceToolkit::Utils::Collection.get_inverse_relation(collection, name),
|
165
165
|
relationship: RELATION_MAP[relation.type]
|
166
166
|
}
|
167
167
|
|
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.
|
4
|
+
version: 1.0.0.pre.beta.49
|
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: 2024-05-
|
12
|
+
date: 2024-05-23 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: activesupport
|
@@ -218,7 +218,6 @@ extensions: []
|
|
218
218
|
extra_rdoc_files: []
|
219
219
|
files:
|
220
220
|
- ".rspec"
|
221
|
-
- README.md
|
222
221
|
- Rakefile
|
223
222
|
- forest_admin
|
224
223
|
- forest_admin_agent.gemspec
|
@@ -239,6 +238,7 @@ files:
|
|
239
238
|
- lib/forest_admin_agent/http/Exceptions/require_approval.rb
|
240
239
|
- lib/forest_admin_agent/http/Exceptions/unprocessable_error.rb
|
241
240
|
- lib/forest_admin_agent/http/Exceptions/validation_error.rb
|
241
|
+
- lib/forest_admin_agent/http/error_handling.rb
|
242
242
|
- lib/forest_admin_agent/http/forest_admin_api_requester.rb
|
243
243
|
- lib/forest_admin_agent/http/router.rb
|
244
244
|
- lib/forest_admin_agent/routes/abstract_authenticated_route.rb
|
@@ -249,10 +249,12 @@ files:
|
|
249
249
|
- lib/forest_admin_agent/routes/charts/api_chart_datasource.rb
|
250
250
|
- lib/forest_admin_agent/routes/charts/charts.rb
|
251
251
|
- lib/forest_admin_agent/routes/resources/count.rb
|
252
|
+
- lib/forest_admin_agent/routes/resources/csv.rb
|
252
253
|
- lib/forest_admin_agent/routes/resources/delete.rb
|
253
254
|
- lib/forest_admin_agent/routes/resources/list.rb
|
254
255
|
- lib/forest_admin_agent/routes/resources/related/associate_related.rb
|
255
256
|
- lib/forest_admin_agent/routes/resources/related/count_related.rb
|
257
|
+
- lib/forest_admin_agent/routes/resources/related/csv_related.rb
|
256
258
|
- lib/forest_admin_agent/routes/resources/related/dissociate_related.rb
|
257
259
|
- lib/forest_admin_agent/routes/resources/related/list_related.rb
|
258
260
|
- lib/forest_admin_agent/routes/resources/related/update_related.rb
|
@@ -272,12 +274,14 @@ files:
|
|
272
274
|
- lib/forest_admin_agent/utils/condition_tree_parser.rb
|
273
275
|
- lib/forest_admin_agent/utils/context_variables.rb
|
274
276
|
- lib/forest_admin_agent/utils/context_variables_injector.rb
|
277
|
+
- lib/forest_admin_agent/utils/csv_generator.rb
|
275
278
|
- lib/forest_admin_agent/utils/error_messages.rb
|
276
279
|
- lib/forest_admin_agent/utils/id.rb
|
277
280
|
- lib/forest_admin_agent/utils/query_string_parser.rb
|
278
281
|
- lib/forest_admin_agent/utils/schema/action_fields.rb
|
279
282
|
- lib/forest_admin_agent/utils/schema/forest_value_converter.rb
|
280
283
|
- lib/forest_admin_agent/utils/schema/frontend_filterable.rb
|
284
|
+
- lib/forest_admin_agent/utils/schema/frontend_validation_utils.rb
|
281
285
|
- lib/forest_admin_agent/utils/schema/generator_action.rb
|
282
286
|
- lib/forest_admin_agent/utils/schema/generator_collection.rb
|
283
287
|
- lib/forest_admin_agent/utils/schema/generator_field.rb
|
@@ -300,7 +304,7 @@ licenses:
|
|
300
304
|
metadata:
|
301
305
|
homepage_uri: https://www.forestadmin.com
|
302
306
|
source_code_uri: https://github.com/ForestAdmin/agent-ruby
|
303
|
-
changelog_uri: https://github.com/ForestAdmin/agent-ruby/CHANGELOG.md
|
307
|
+
changelog_uri: https://github.com/ForestAdmin/agent-ruby/blob/main/CHANGELOG.md
|
304
308
|
rubygems_mfa_required: 'false'
|
305
309
|
post_install_message:
|
306
310
|
rdoc_options: []
|
data/README.md
DELETED
@@ -1,35 +0,0 @@
|
|
1
|
-
# Agent
|
2
|
-
|
3
|
-
TODO: Delete this and the text below, and describe your gem
|
4
|
-
|
5
|
-
Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/agent_ruby`. To experiment with that code, run `bin/console` for an interactive prompt.
|
6
|
-
|
7
|
-
## Installation
|
8
|
-
|
9
|
-
TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
|
10
|
-
|
11
|
-
Install the gem and add to the application's Gemfile by executing:
|
12
|
-
|
13
|
-
$ bundle add UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG
|
14
|
-
|
15
|
-
If bundler is not being used to manage dependencies, install the gem by executing:
|
16
|
-
|
17
|
-
$ gem install UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG
|
18
|
-
|
19
|
-
## Usage
|
20
|
-
|
21
|
-
TODO: Write usage instructions here
|
22
|
-
|
23
|
-
## Development
|
24
|
-
|
25
|
-
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
26
|
-
|
27
|
-
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
28
|
-
|
29
|
-
## Contributing
|
30
|
-
|
31
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/agent_ruby.
|
32
|
-
|
33
|
-
## License
|
34
|
-
|
35
|
-
The gem is available as open source under the terms of the [GPL-3.0 License](https://www.gnu.org/licenses/gpl-3.0.en.html).
|