forest_admin_agent 1.0.0.pre.beta.29 → 1.0.0.pre.beta.30

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2c5ae106d439132bfc53a11f1fe055e58922dda5140812af1a83851d49ef8961
4
- data.tar.gz: 594d82ff7aa21eb7d1f8f4d0ed203d3f7530784fffd4361d49dd04e0e0710783
3
+ metadata.gz: 7c8589b7ac5d1648ad10bebc5e3e2d8f55b176a4804f44f0d1b7dce77a747646
4
+ data.tar.gz: bfbb688a7c3bd8acb6849379757bf9255ae65a4ce272611d724122332d7774dc
5
5
  SHA512:
6
- metadata.gz: b10a66bdd437914e823fbfaee379f206eb1646a50743a3854f7490357dc0e33d58315429e45ef1eb652e9c926f876535afebb779f908c370181e3d2729415a96
7
- data.tar.gz: b5bf01858bf6dd700dad161214b5875dcc430d8b7ddbee8c333c4c32baa36b5135442449f6a0c96f2848a36b49ba43262bd75c54cf4a08f21249aaa760f031fa
6
+ metadata.gz: 206fc75159915647d7a7f895439d7ceb4e58baceade060b08853f2fdfcc27ac6dcaab0b20ffd6902b3bc96be3bc8915a7aad9f16332a946a3109f0a5a53def21
7
+ data.tar.gz: 653dec755d3dc730b9f84404f3d22430e729732e2e31dbea46d6b06b3e2e069e8db9f1e7dffc4a77be6d04ff682bfa3f69963d281599fae4b45c402def701606
data/README.md CHANGED
@@ -32,4 +32,4 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERN
32
32
 
33
33
  ## License
34
34
 
35
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
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).
@@ -12,7 +12,7 @@ Gem::Specification.new do |spec|
12
12
  spec.summary = "Ruby agent for Forest Admin."
13
13
  spec.description = "Forest is a modern admin interface that works on all major web frameworks. This gem makes Forest
14
14
  admin work on any Ruby application."
15
- spec.license = "MIT"
15
+ spec.license = "GPL-3.0"
16
16
  spec.required_ruby_version = ">= 3.0.0"
17
17
 
18
18
  spec.metadata["homepage_uri"] = spec.homepage
@@ -5,7 +5,7 @@ module ForestAdminAgent
5
5
 
6
6
  def self.routes
7
7
  [
8
- # actions_routes,
8
+ actions_routes,
9
9
  # api_charts_routes,
10
10
  System::HealthCheck.new.routes,
11
11
  Security::Authentication.new.routes,
@@ -25,14 +25,14 @@ module ForestAdminAgent
25
25
  end
26
26
 
27
27
  def self.actions_routes
28
- routes = []
29
- # TODO
30
- # AgentFactory.get('datasource').collections.each do |collection|
31
- # collection.get_actions.each do |action_name, action|
32
- # routes << Actions.new(collection, action_name).routes
33
- # end
34
- # end
35
- routes.flatten
28
+ routes = {}
29
+ Facades::Container.datasource.collections.each_value do |collection|
30
+ collection.schema[:actions].each_key do |action_name|
31
+ routes.merge!(Action::Action.new(collection, action_name).routes)
32
+ end
33
+ end
34
+
35
+ routes
36
36
  end
37
37
 
38
38
  def self.api_charts_routes
@@ -0,0 +1,137 @@
1
+ require 'jsonapi-serializers'
2
+ require 'active_support/inflector'
3
+
4
+ module ForestAdminAgent
5
+ module Routes
6
+ module Action
7
+ class Action < AbstractAuthenticatedRoute
8
+ include ForestAdminAgent::Builder
9
+ include ForestAdminAgent::Utils
10
+ include ForestAdminDatasourceToolkit::Components::Query
11
+ include ForestAdminDatasourceToolkit::Components::Query::ConditionTree
12
+ include ForestAdminDatasourceCustomizer::Decorators::Action
13
+
14
+ def initialize(collection, action)
15
+ @action_name = action
16
+ @collection = collection
17
+ super()
18
+ end
19
+
20
+ def setup_routes
21
+ action_index = @collection.schema[:actions].keys.index(@action_name)
22
+ slug = ForestAdminAgent::Utils::Schema::GeneratorAction.get_action_slug(@action_name)
23
+ route_name = "forest_action_#{@collection.name}_#{action_index}_#{slug}"
24
+ path = "/_actions/:collection_name/#{action_index}/#{slug}"
25
+
26
+ add_route(route_name, 'post', path, proc { |args| handle_request(args) })
27
+ add_route(
28
+ "#{route_name}_load",
29
+ 'post',
30
+ "#{path}/hooks/load",
31
+ proc { |args| handle_hook_request(args) }
32
+ )
33
+ add_route(
34
+ "#{route_name}_change",
35
+ 'post',
36
+ "#{path}/hooks/change",
37
+ proc { |args| handle_hook_request(args) }
38
+ )
39
+ self
40
+ end
41
+
42
+ def handle_request(args = {})
43
+ build(args)
44
+ filter_for_caller = get_record_selection(args)
45
+ get_record_selection(args, include_user_scope: false)
46
+
47
+ # TODO: permission
48
+
49
+ raw_data = args.dig(:params, :data, :attributes, :values)
50
+
51
+ # As forms are dynamic, we don't have any way to ensure that we're parsing the data correctly
52
+ # better send invalid data to the getForm() customer handler than to the execute() one.
53
+ unsafe_data = Schema::ForestValueConverter.make_form_data_unsafe(raw_data)
54
+
55
+ fields = @collection.get_form(
56
+ @caller,
57
+ @action_name,
58
+ unsafe_data,
59
+ filter_for_caller,
60
+ { include_hidden_fields: true } # during execute, we need all possible fields
61
+ )
62
+
63
+ # Now that we have the field list, we can parse the data again.
64
+ data = Schema::ForestValueConverter.make_form_data(@datasource, raw_data, fields)
65
+
66
+ { content: @collection.execute(@caller, @action_name, data, filter_for_caller) }
67
+ end
68
+
69
+ def handle_hook_request(args = {})
70
+ build(args)
71
+ forest_fields = args.dig(:params, :data, :attributes, :fields)
72
+ data = (Schema::ForestValueConverter.make_form_data_from_fields(@datasource, forest_fields) if forest_fields)
73
+ filter = get_record_selection(args)
74
+
75
+ fields = @collection.get_form(
76
+ @caller,
77
+ @action_name,
78
+ data,
79
+ filter,
80
+ {
81
+ change_field: nil,
82
+ search_field: nil,
83
+ search_values: {},
84
+ includeHiddenFields: false
85
+ }
86
+ )
87
+
88
+ {
89
+ content: {
90
+ fields: fields&.map { |field| Schema::GeneratorAction.build_field_schema(@datasource, field) } || {}
91
+ }
92
+ }
93
+ end
94
+
95
+ private
96
+
97
+ def get_record_selection(args, include_user_scope: true)
98
+ attributes = args.dig(:params, :data, :attributes)
99
+
100
+ # Match user filter + search + scope? + segment
101
+ scope = include_user_scope ? @permissions.get_scope(@collection) : nil
102
+ filter = Filter.new(
103
+ condition_tree: ConditionTreeFactory.intersect(
104
+ [
105
+ scope,
106
+ ForestAdminAgent::Utils::QueryStringParser.parse_condition_tree(
107
+ @collection, args
108
+ )
109
+ ]
110
+ )
111
+ )
112
+
113
+ # Restrict the filter to the selected records for single or bulk actions
114
+ if @collection.schema[:actions][@action_name].scope != Types::ActionScope::GLOBAL
115
+ selection_ids = Utils::Id.parse_selection_ids(@collection, args[:params])
116
+ selected_ids = ConditionTreeFactory.match_ids(@collection, selection_ids[:ids])
117
+ selected_ids = selected_ids.inverse if selection_ids[:are_excluded]
118
+ filter = filter.override(
119
+ condition_tree: ConditionTreeFactory.intersect([filter.condition_tree, selected_ids])
120
+ )
121
+ end
122
+
123
+ # Restrict the filter further for the "related data" page
124
+ unless attributes[:parent_association_name].nil?
125
+ relation = attributes[:parent_association_name]
126
+ parent = @datasource.get_collection(attributes[:parent_collection_name])
127
+ parent_id = Utils::Id.unpack_id(parent, attributes[:parent_collection_id])
128
+
129
+ filter = FilterFactory.make_foreign_filter(parent, parent_id, relation, @caller, filter)
130
+ end
131
+
132
+ filter
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end
@@ -41,16 +41,16 @@ module ForestAdminAgent
41
41
 
42
42
  def self.parse_selection_ids(collection, params, with_key: false)
43
43
  attributes = begin
44
- params.dig('data', 'attributes')
44
+ params.dig(:data, :attributes)
45
45
  rescue StandardError
46
46
  nil
47
47
  end
48
48
 
49
- are_excluded = attributes&.key?('all_records') ? attributes['all_records'] : false
50
- input_ids = attributes&.key?('ids') ? attributes['ids'] : params['data'].map { |item| item['id'] }
49
+ are_excluded = attributes&.key?(:all_records) ? attributes[:all_records] : false
50
+ input_ids = attributes&.key?(:ids) ? attributes[:ids] : params[:data].map { |item| item['id'] }
51
51
  ids = unpack_ids(
52
52
  collection,
53
- are_excluded ? attributes['all_records_ids_excluded'] : input_ids,
53
+ are_excluded ? attributes[:all_records_ids_excluded] : input_ids,
54
54
  with_key: with_key
55
55
  )
56
56
 
@@ -0,0 +1,27 @@
1
+ module ForestAdminAgent
2
+ module Utils
3
+ module Schema
4
+ class ActionFields
5
+ def self.collection_field?(field)
6
+ field&.type == 'Collection'
7
+ end
8
+
9
+ def self.enum_field?(field)
10
+ field&.type == 'Enum'
11
+ end
12
+
13
+ def self.enum_list_field?(field)
14
+ field&.type == 'EnumList'
15
+ end
16
+
17
+ def self.file_field?(field)
18
+ field&.type == 'File'
19
+ end
20
+
21
+ def self.file_list_field?(field)
22
+ field&.type == 'FileList'
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,105 @@
1
+ module ForestAdminAgent
2
+ module Utils
3
+ module Schema
4
+ class ForestValueConverter
5
+ # This last form data parser tries to guess the types from the data itself.
6
+ #
7
+ # - Fields with type "Collection" which target collections where the pk is not a string or
8
+ # derivative (mongoid, uuid, ...) won't be parser correctly, as we don't have enough information
9
+ # to properly guess the type
10
+ # - Fields of type "String" but where the final user entered a data-uri manually in the frontend
11
+ # will be wrongfully parsed.
12
+ def self.make_form_data_unsafe(raw_data)
13
+ data = {}
14
+ raw_data.each do |key, value|
15
+ # Skip fields from the default form
16
+ next if Schema::GeneratorAction::DEFAULT_FIELDS.map { |f| f[:field] }.include?(key)
17
+
18
+ data[key] = if value.is_a?(Array) && value.all? { |v| data_uri?(v) }
19
+ value.map { |uri| parse_data_uri(uri) }
20
+ elsif data_uri?(value)
21
+ parse_data_uri(value)
22
+ else
23
+ value
24
+ end
25
+ end
26
+
27
+ data
28
+ end
29
+
30
+ def self.make_form_data_from_fields(datasource, fields)
31
+ data = {}
32
+ fields.each_value do |field|
33
+ next if Schema::GeneratorAction::DEFAULT_FIELDS.map { |f| f[:field] }.include?(field.field)
34
+
35
+ if field.reference && field.value
36
+ collection_name = field.reference.split('.').first
37
+ collection = datasource.get_collection(collection_name)
38
+ data[field.field] = Utils::Id.unpack_id(collection, field.value)
39
+ elsif field.type == 'File'
40
+ data[field.field] = parse_data_uri(field.value)
41
+ elsif field.type.is_a?(Array) && field.type[0] == 'File'
42
+ data[field.field] = field.value.map { |v| parse_data_uri(v) }
43
+ else
44
+ data[field.field] = field.value
45
+ end
46
+ end
47
+
48
+ data
49
+ end
50
+
51
+ # Proper form data parser which converts data from an action form result to the format
52
+ # that is internally used in datasources.
53
+ def self.make_form_data(datasource, raw_data, fields)
54
+ data = {}
55
+ raw_data.each do |key, value|
56
+ field = fields.find { |f| f.label == key }
57
+ # Skip fields from the default form
58
+ next if Schema::GeneratorAction::DEFAULT_FIELDS.map { |f| f[:field] }.include?(key)
59
+
60
+ if ActionFields.collection_field?(field) && !value.nil?
61
+ collection = datasource.get_collection(field.collection_name)
62
+ data[key] = Utils::Id.unpack_id(collection, value)
63
+ elsif ActionFields.file_field?(field)
64
+ data[key] = parse_data_uri(value)
65
+ elsif ActionFields.file_list_field?(field)
66
+ data[key] = value.map { |v| parse_data_uri(v) }
67
+ else
68
+ data[key] = value
69
+ end
70
+ end
71
+
72
+ data
73
+ end
74
+
75
+ def self.data_uri?(value)
76
+ value.is_a?(String) && value.start_with?('data:')
77
+ end
78
+
79
+ def self.value_to_forest(field)
80
+ if ActionFields.enum_field?(field)
81
+ return field.enum_values.include?(field.value) ? field.value : nil
82
+ end
83
+
84
+ return field.value.select { |v| field.enum_values.include?(v) } if ActionFields.enum_list_field?(field)
85
+
86
+ return field.value.join('|') if ActionFields.collection_field?(field)
87
+
88
+ # return make_data_uri(field.value) if ActionFields.file_field?(field)
89
+ #
90
+ # return value.map { |f| make_data_uri(f) } if ActionFields.file_list_field?(field)
91
+
92
+ field.value
93
+ end
94
+
95
+ def self.make_data_uri(file)
96
+ # TODO: to implement
97
+ end
98
+
99
+ def self.parse_data_uri(file)
100
+ # TODO: to implement
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,101 @@
1
+ module ForestAdminAgent
2
+ module Utils
3
+ module Schema
4
+ class GeneratorAction
5
+ DEFAULT_FIELDS = [
6
+ {
7
+ field: 'Loading...',
8
+ type: 'String',
9
+ isReadOnly: true,
10
+ defaultValue: 'Form is loading',
11
+ value: nil,
12
+ description: '',
13
+ enums: nil,
14
+ hook: nil,
15
+ isRequired: false,
16
+ reference: nil,
17
+ widgetEdit: nil
18
+ }
19
+ ].freeze
20
+
21
+ def self.get_action_slug(name)
22
+ name.downcase.strip.tr(' ', '-').gsub(/[^\w-]/, '')
23
+ end
24
+
25
+ def self.build_schema(collection, name)
26
+ schema = collection.schema[:actions][name]
27
+ action_index = collection.schema[:actions].keys.index(name)
28
+ slug = get_action_slug(name)
29
+ fields = build_fields(collection, name, schema)
30
+
31
+ {
32
+ id: "#{collection.name}-#{action_index}-#{slug}",
33
+ name: name,
34
+ type: schema.scope.downcase,
35
+ baseUrl: nil,
36
+ endpoint: "/forest/_actions/#{collection.name}/#{action_index}/#{slug}",
37
+ httpMethod: 'POST',
38
+ redirect: nil, # frontend ignores this attribute
39
+ download: schema.is_generate_file,
40
+ fields: fields,
41
+ hooks: {
42
+ load: !schema.static_form?,
43
+ # Always registering the change hook has no consequences, even if we don't use it.
44
+ change: ['changeHook']
45
+ }
46
+ }
47
+ end
48
+
49
+ def self.build_field_schema(datasource, field)
50
+ output = {
51
+ description: field.description,
52
+ isRequired: field.is_required,
53
+ isReadOnly: field.is_read_only,
54
+ field: field.label,
55
+ value: ForestValueConverter.value_to_forest(field)
56
+ }
57
+
58
+ output[:hook] = 'changeHook' if field.respond_to?(:watch_changes) && field.watch_changes
59
+
60
+ if ActionFields.collection_field?(field)
61
+ collection = datasource.get_collection(field.collection_name)
62
+ pk = ForestAdminDatasourceToolkit::Utils::Schema.primary_keys(collection)
63
+ pk_schema = collection.schema[:fields][pk]
64
+
65
+ output[:type] = pk_schema.column_type
66
+ output[:reference] = "#{collection.name}.#{pk}"
67
+ elsif field.type.end_with?('List')
68
+ output[:type] = [field.type.delete_suffix('List')]
69
+ else
70
+ output[:type] = field.type
71
+ end
72
+
73
+ output[:enums] = field.enum_values if ActionFields.enum_field?(field) || ActionFields.enum_list_field?(field)
74
+
75
+ output
76
+ end
77
+
78
+ class << self
79
+ private
80
+
81
+ def build_fields(collection, name, action)
82
+ return DEFAULT_FIELDS unless action.static_form?
83
+
84
+ fields = collection.get_form(nil, name)
85
+ if fields
86
+ return fields.map do |field|
87
+ new_field = build_field_schema(collection.datasource, field)
88
+ new_field[:default_value] = new_field[:value]
89
+ new_field.delete(:value)
90
+
91
+ new_field
92
+ end
93
+ end
94
+
95
+ []
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
@@ -6,7 +6,7 @@ module ForestAdminAgent
6
6
 
7
7
  def self.build_schema(collection)
8
8
  {
9
- actions: {},
9
+ actions: build_actions(collection),
10
10
  fields: build_fields(collection),
11
11
  icon: nil,
12
12
  integration: nil,
@@ -29,6 +29,14 @@ module ForestAdminAgent
29
29
  fields.map { |name, _field| GeneratorField.build_schema(collection, name) }
30
30
  .sort_by { |v| v[:field] }
31
31
  end
32
+
33
+ def self.build_actions(collection)
34
+ if collection.schema[:actions]
35
+ collection.schema[:actions].keys.sort.map { |name| GeneratorAction.build_schema(collection, name) }
36
+ else
37
+ {}
38
+ end
39
+ end
32
40
  end
33
41
  end
34
42
  end
@@ -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.29"
10
+ LIANA_VERSION = "1.0.0-beta.30"
11
11
 
12
12
  def self.get_serialized_schema(datasource)
13
13
  schema_path = Facades::Container.cache(:schema_path)
@@ -75,7 +75,7 @@ module ForestAdminAgent
75
75
  attributes: collection,
76
76
  relationships: {
77
77
  actions: { data: get_smart_features_by_collection('actions', collection_actions) },
78
- segments: { data: get_smart_features_by_collection('segments', collection_actions) }
78
+ segments: { data: get_smart_features_by_collection('segments', collection_segments) }
79
79
  }
80
80
  }
81
81
  )
@@ -83,7 +83,7 @@ module ForestAdminAgent
83
83
 
84
84
  {
85
85
  data: data,
86
- included: included.reject!(&:empty?),
86
+ included: included.reject!(&:empty?)&.flatten,
87
87
  meta: schema[:meta]
88
88
  }
89
89
  end
@@ -95,6 +95,8 @@ module ForestAdminAgent
95
95
  smart_feature[:attributes] = value if with_attributes
96
96
  smart_features << smart_feature
97
97
  end
98
+
99
+ smart_features
98
100
  end
99
101
  end
100
102
  end
@@ -1,3 +1,3 @@
1
1
  module ForestAdminAgent
2
- VERSION = "1.0.0-beta.29"
2
+ VERSION = "1.0.0-beta.30"
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.29
4
+ version: 1.0.0.pre.beta.30
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-01-24 00:00:00.000000000 Z
12
+ date: 2024-02-13 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activesupport
@@ -242,6 +242,7 @@ files:
242
242
  - lib/forest_admin_agent/routes/abstract_authenticated_route.rb
243
243
  - lib/forest_admin_agent/routes/abstract_related_route.rb
244
244
  - lib/forest_admin_agent/routes/abstract_route.rb
245
+ - lib/forest_admin_agent/routes/action/action.rb
245
246
  - lib/forest_admin_agent/routes/charts/charts.rb
246
247
  - lib/forest_admin_agent/routes/resources/count.rb
247
248
  - lib/forest_admin_agent/routes/resources/delete.rb
@@ -270,7 +271,10 @@ files:
270
271
  - lib/forest_admin_agent/utils/error_messages.rb
271
272
  - lib/forest_admin_agent/utils/id.rb
272
273
  - lib/forest_admin_agent/utils/query_string_parser.rb
274
+ - lib/forest_admin_agent/utils/schema/action_fields.rb
275
+ - lib/forest_admin_agent/utils/schema/forest_value_converter.rb
273
276
  - lib/forest_admin_agent/utils/schema/frontend_filterable.rb
277
+ - lib/forest_admin_agent/utils/schema/generator_action.rb
274
278
  - lib/forest_admin_agent/utils/schema/generator_collection.rb
275
279
  - lib/forest_admin_agent/utils/schema/generator_field.rb
276
280
  - lib/forest_admin_agent/utils/schema/schema_emitter.rb
@@ -288,7 +292,7 @@ files:
288
292
  - sig/forest_admin_agent/routes/system/health_check.rbs
289
293
  homepage: https://www.forestadmin.com
290
294
  licenses:
291
- - MIT
295
+ - GPL-3.0
292
296
  metadata:
293
297
  homepage_uri: https://www.forestadmin.com
294
298
  source_code_uri: https://github.com/ForestAdmin/agent-ruby