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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 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