forest_admin_agent 1.37.4 → 1.38.1

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: e658e8812d89132182a96392c58221793cf5991bd95b4c0d3170dff0676a693c
4
- data.tar.gz: b7cf6705ebdda5d3785e78290f1b1f4b762b1eac7b157c999bbbde4b6206b8f3
3
+ metadata.gz: bbec2d5605e6d383151f61a3b58c602b0c65099685c03cd7e2f910146a563058
4
+ data.tar.gz: 53f010f4b995181820015c7bda91ef07a7d84bd8a7ee66fa9887f4e933bcbbbc
5
5
  SHA512:
6
- metadata.gz: 2feb766efd70870058a92d79ebde8cc3a41cc83452767a7478f6d5a4e66aa72346c0b1208364696fe67c5b4d4378439797240cf387d4462ea198a4f5df521152
7
- data.tar.gz: d49ef0aca2acdb55a4c6b8d5ac58861165017f424958787064635322ff2fe961ecaa6e206ecc80c4fc495a5986434615f1aa5b9b599d62ce390733b0a4bb89e8
6
+ metadata.gz: a34d395d4eab6ad9a034e9ef0b93b6737740a7a832da40e10ceab61787e5e3c2949776eb1c7bbefdd91cfdc175312e0e954e090c63804403e83e151080c6e16c
7
+ data.tar.gz: 18058de101b452b870c8ef90a2be2990ba07c197b2d64347e4e229d4f37156b26eaaf9cc96ab885911f5144e06ee939e9108044b85333eb8e97a6f442e22d2f7
@@ -65,6 +65,8 @@ module ForestAdminAgent
65
65
  nativeQueryConnections: connections,
66
66
  agentCapabilities: {
67
67
  canUseProjectionOnGetOne: true,
68
+ canUseProjectionViaHeader: true,
69
+ canUseProjectionViaHeaderOnList: true,
68
70
  canUseMultipleFieldsProjectionOnRelation: true
69
71
  }
70
72
  },
@@ -37,7 +37,7 @@ module ForestAdminAgent
37
37
  sort: QueryStringParser.parse_sort(context.collection, args),
38
38
  segment: QueryStringParser.parse_segment(context.collection, args)
39
39
  )
40
- projection = QueryStringParser.parse_projection(context.collection, args)
40
+ projection = QueryStringParser.parse_projection_from_request(context.collection, args)
41
41
  filename = args[:params][:filename] || args[:params]['collection_name']
42
42
  filename += '.csv' unless /\.csv$/i.match?(filename)
43
43
  header = args[:params][:header]
@@ -32,8 +32,9 @@ module ForestAdminAgent
32
32
  ]
33
33
  )
34
34
  )
35
- projection = ForestAdminAgent::Utils::QueryStringParser.parse_projection_with_pks(context.child_collection,
36
- args)
35
+ projection = ForestAdminAgent::Utils::QueryStringParser.parse_projection_from_request(
36
+ context.child_collection, args
37
+ )
37
38
 
38
39
  # Get the parent record primary keys
39
40
  primary_key_values = Utils::Id.unpack_id(context.collection, args[:params]['id'], with_key: true)
@@ -17,9 +17,10 @@ module ForestAdminAgent
17
17
  def handle_request(args = {})
18
18
  context = build(args)
19
19
  context.permissions.can?(:add, context.collection)
20
+ relations = authorized_linked_one_to_one_relations(args, context)
20
21
  data = format_attributes(args, context.collection)
21
22
  record = context.collection.create(context.caller, data)
22
- link_one_to_one_relations(args, record, context)
23
+ link_one_to_one_relations(relations, record, context)
23
24
  id = ForestAdminDatasourceToolkit::Utils::Record.primary_keys(context.collection, record)
24
25
  filter = ForestAdminDatasourceToolkit::Components::Query::Filter.new(
25
26
  condition_tree: ConditionTree::ConditionTreeFactory.match_ids(context.collection, [id])
@@ -37,24 +38,63 @@ module ForestAdminAgent
37
38
  }
38
39
  end
39
40
 
40
- def link_one_to_one_relations(args, record, context)
41
- args[:params][:data][:relationships]&.map do |field, value|
42
- schema = context.collection.schema[:fields][field]
43
- next unless %w[OneToOne PolymorphicOneToOne].include?(schema.type)
41
+ private
44
42
 
45
- primary_key_values = Utils::Id.unpack_id(context.collection, value['data']['id'], with_key: true)
46
- foreign_collection = context.datasource.get_collection(schema.foreign_collection)
47
- # Load the value that will be used as origin_key
48
- origin_value = record[schema.origin_key_target]
43
+ def authorized_linked_one_to_one_relations(args, context)
44
+ linked_one_to_one_relations(args, context).map do |relation|
45
+ foreign_collection = relation[:foreign_collection]
49
46
 
50
- # update new relation (may update zero or one records).
51
- patch = { schema.origin_key => origin_value }
47
+ context.permissions.can?(:edit, foreign_collection)
48
+
49
+ relation.merge(
50
+ scope: context.permissions.get_scope(foreign_collection),
51
+ primary_key_values: Utils::Id.unpack_id(foreign_collection, relation[:id], with_key: true)
52
+ )
53
+ end
54
+ end
55
+
56
+ def linked_one_to_one_relations(args, context)
57
+ relationships = args.dig(:params, :data, :relationships) || {}
58
+
59
+ relationships.filter_map { |field, value| linked_one_to_one_relation(field, value, context) }
60
+ end
61
+
62
+ def linked_one_to_one_relation(field, value, context)
63
+ schema = context.collection.schema[:fields][field]
64
+ return unless %w[OneToOne PolymorphicOneToOne].include?(schema.type)
65
+
66
+ id = value.dig('data', 'id')
67
+ return if id.nil?
68
+
69
+ {
70
+ schema: schema,
71
+ foreign_collection: context.datasource.get_collection(schema.foreign_collection),
72
+ id: id
73
+ }
74
+ end
75
+
76
+ def link_one_to_one_relations(relations, record, context)
77
+ relations.each do |relation|
78
+ schema = relation[:schema]
79
+ foreign_collection = relation[:foreign_collection]
80
+
81
+ patch = { schema.origin_key => record[schema.origin_key_target] }
52
82
  if schema.type == 'PolymorphicOneToOne'
53
83
  patch[schema.origin_type_field] =
54
84
  context.collection.name.gsub('__', '::')
55
85
  end
56
- condition_tree = ConditionTree::ConditionTreeFactory.match_records(foreign_collection, [primary_key_values])
57
- filter = Filter.new(condition_tree: condition_tree)
86
+
87
+ new_fk_owner = ConditionTree::ConditionTreeFactory.match_records(
88
+ foreign_collection, [relation[:primary_key_values]]
89
+ )
90
+ filter = Filter.new(
91
+ condition_tree: ConditionTree::ConditionTreeFactory.intersect(
92
+ [
93
+ relation[:scope],
94
+ new_fk_owner
95
+ ]
96
+ )
97
+ )
58
98
  foreign_collection.update(context.caller, filter, patch)
59
99
  end
60
100
  end
@@ -47,6 +47,24 @@ module ForestAdminAgent
47
47
  raise BadRequestError, "Invalid projection: #{e.message}"
48
48
  end
49
49
 
50
+ def self.parse_projection_from_header(collection, args)
51
+ header = args.dig(:headers, 'HTTP_FOREST_PROJECTION')&.to_s&.strip
52
+
53
+ return if header.nil? || header.empty?
54
+
55
+ projection_fields = build_header_projection_fields(collection, header.split(',', -1).map(&:strip))
56
+
57
+ ForestAdminDatasourceToolkit::Validations::ProjectionValidator.validate?(collection, projection_fields)
58
+
59
+ Projection.new(projection_fields)
60
+ rescue ForestAdminDatasourceToolkit::Exceptions::ForestException => e
61
+ raise BadRequestError, "Invalid Forest-Projection header: #{e.message}"
62
+ end
63
+
64
+ def self.parse_projection_from_request(collection, args)
65
+ parse_projection_from_header(collection, args) || parse_projection(collection, args)
66
+ end
67
+
50
68
  def self.add_polymorphic_type_fields(collection, requested_field_names)
51
69
  polymorphic_relations = collection.schema[:fields].select { |_, field| field.type == 'PolymorphicManyToOne' }
52
70
 
@@ -83,6 +101,29 @@ module ForestAdminAgent
83
101
  end
84
102
  end
85
103
 
104
+ def self.build_header_projection_fields(collection, requested_paths)
105
+ if requested_paths.any? { |path| path.empty? || path.split(':', -1).any?(&:empty?) }
106
+ raise ForestAdminDatasourceToolkit::Exceptions::ValidationError, 'The projection contains an empty field.'
107
+ end
108
+
109
+ root_field_names = requested_paths.map { |path| path.split(':').first }
110
+ root_field_names_with_types = root_field_names.dup
111
+ add_polymorphic_type_fields(collection, root_field_names_with_types)
112
+
113
+ projection_fields = requested_paths.map do |path|
114
+ field_name, nested_path = path.split(':', 2)
115
+ field = get_field(collection, field_name)
116
+
117
+ if field.type == 'PolymorphicManyToOne' && (nested_path.nil? || nested_path == '*')
118
+ "#{field_name}:*"
119
+ else
120
+ path
121
+ end
122
+ end
123
+
124
+ projection_fields | (root_field_names_with_types - root_field_names)
125
+ end
126
+
86
127
  def self.get_field(collection, field_name)
87
128
  field = collection.schema[:fields][field_name]
88
129
  return field unless field.nil?
@@ -93,12 +134,11 @@ module ForestAdminAgent
93
134
  "Available fields are: [#{available_fields}]. " \
94
135
  'Please check if the field name is correct.'
95
136
  end
96
- private_class_method :add_polymorphic_type_fields, :build_projection_fields, :get_field
137
+ private_class_method :add_polymorphic_type_fields, :build_projection_fields,
138
+ :build_header_projection_fields, :get_field
97
139
 
98
140
  def self.parse_projection_with_pks(collection, args)
99
- projection = parse_projection(collection, args)
100
-
101
- projection.with_pks(collection)
141
+ parse_projection_from_request(collection, args).with_pks(collection)
102
142
  end
103
143
 
104
144
  def self.parse_pagination(args)
@@ -6,7 +6,7 @@ module ForestAdminAgent
6
6
  module Schema
7
7
  class SchemaEmitter
8
8
  LIANA_NAME = "agent-ruby"
9
- LIANA_VERSION = "1.37.4"
9
+ LIANA_VERSION = "1.38.1"
10
10
 
11
11
  def self.generate(datasource)
12
12
  datasource.collections
@@ -1,3 +1,3 @@
1
1
  module ForestAdminAgent
2
- VERSION = "1.37.4"
2
+ VERSION = "1.38.1"
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.37.4
4
+ version: 1.38.1
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: 2026-08-11 00:00:00.000000000 Z
12
+ date: 2026-08-13 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activesupport