forest_admin_agent 1.37.3 → 1.38.0

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: 8fdf5c141f3b667c5d637882a071c6775bbf8dd6c814415b53c7cfa1478725d3
4
- data.tar.gz: 8e51a39b2ac4461d530d64b9bd24e93dd37463057c68d39a505e6ee63ba7b338
3
+ metadata.gz: 5455a6164932af84cf535d72cd896814fcb24a83106cae4c6efbf94d3f15f650
4
+ data.tar.gz: f31a0a6cde3793cd7bc2f5bfc1c2462a0001d4e80e414e7433064f2907dff45d
5
5
  SHA512:
6
- metadata.gz: cd9a857e7b0c8cd264f998522bca85aef1048253ab7ba30106383fbf91d2744581a4356edad2431d2ea6c392bb67c031390debf5dd0fa496648d2ed454f504a7
7
- data.tar.gz: 906a6c9db2e4911fc8b2dea2993c5a274fd2e2fb1b43ce985dfb97e8dbd3563bdbf2592a21d8ac3bac2b580413e871bbacf0f86bf7fc1ec0766e564e16c575fd
6
+ metadata.gz: 929f4669e1b73c51b615f0259a6b34e6cfa59cb89c7e9dde693c7c47238b4c5b2fe608d5a3eceb73d6ed025fe5d5a83a33759920187c61bb1eddd3b0ab239bf1
7
+ data.tar.gz: 638dd7596eae9fde1b272e5389d3da042e0aaf3e12080fbdf449866a61a8535a75ae8d46cbf39c5c3eed56792f249c7146d770179a976fe0961b5e6c6b445b02
@@ -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]
@@ -50,7 +50,7 @@ module ForestAdminAgent
50
50
  condition_tree: ConditionTree::ConditionTreeFactory.intersect(
51
51
  [
52
52
  ConditionTree::Nodes::ConditionTreeLeaf.new(id, 'Equal', value),
53
- context.permissions.get_scope(context.collection)
53
+ context.permissions.get_scope(context.child_collection)
54
54
  ]
55
55
  )
56
56
  )
@@ -67,7 +67,7 @@ module ForestAdminAgent
67
67
  condition_tree: ConditionTree::ConditionTreeFactory.intersect(
68
68
  [
69
69
  ConditionTree::Nodes::ConditionTreeLeaf.new(id, 'Equal', value),
70
- context.permissions.get_scope(context.collection)
70
+ context.permissions.get_scope(context.child_collection)
71
71
  ]
72
72
  )
73
73
  )
@@ -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)
@@ -29,7 +29,7 @@ module ForestAdminAgent
29
29
  if is_delete_mode
30
30
  context.permissions.can?(:delete, context.child_collection)
31
31
  else
32
- context.permissions.can?(:edit, context.collection)
32
+ context.permissions.can?(:edit, context.child_collection)
33
33
  end
34
34
 
35
35
  filter = get_base_foreign_filter(args, context)
@@ -22,9 +22,12 @@ module ForestAdminAgent
22
22
 
23
23
  def handle_request(args = {})
24
24
  context = build(args)
25
- context.permissions.can?(:edit, context.collection)
26
25
 
27
26
  relation = context.collection.schema[:fields][args[:params]['relation_name']]
27
+
28
+ # Must run before unpack_id: unauthorized callers get a 403, not a validation error
29
+ context.permissions.can?(:edit, mutated_collection(relation, context))
30
+
28
31
  parent_primary_key_values = Utils::Id.unpack_id(context.collection, args[:params]['id'])
29
32
 
30
33
  linked_primary_key_values = if (id = args.dig(:params, 'data', 'id'))
@@ -47,13 +50,32 @@ module ForestAdminAgent
47
50
 
48
51
  private
49
52
 
53
+ def mutated_collection(relation, context)
54
+ return context.child_collection if ['OneToOne', 'PolymorphicOneToOne'].include?(relation.type)
55
+
56
+ context.collection
57
+ end
58
+
59
+ def scoped_fk_owner_filter(parent_primary_key_values, context)
60
+ fk_owner = ConditionTree::ConditionTreeFactory.match_ids(context.collection, [parent_primary_key_values])
61
+
62
+ Filter.new(
63
+ condition_tree: ConditionTree::ConditionTreeFactory.intersect(
64
+ [
65
+ context.permissions.get_scope(context.collection),
66
+ fk_owner
67
+ ]
68
+ )
69
+ )
70
+ end
71
+
50
72
  def update_many_to_one(relation, parent_primary_key_values, linked_primary_key_values, context)
51
73
  foreign_value = if linked_primary_key_values
52
74
  Collection.get_value(context.child_collection, context.caller, linked_primary_key_values,
53
75
  relation.foreign_key_target)
54
76
  end
55
- fk_owner = ConditionTree::ConditionTreeFactory.match_ids(context.collection, [parent_primary_key_values])
56
- context.collection.update(context.caller, Filter.new(condition_tree: fk_owner),
77
+
78
+ context.collection.update(context.caller, scoped_fk_owner_filter(parent_primary_key_values, context),
57
79
  { relation.foreign_key => foreign_value })
58
80
  end
59
81
 
@@ -68,10 +90,9 @@ module ForestAdminAgent
68
90
  end
69
91
 
70
92
  polymorphic_type = context.child_collection.name.gsub('__', '::')
71
- fk_owner = ConditionTree::ConditionTreeFactory.match_ids(context.collection, [parent_primary_key_values])
72
93
  context.collection.update(
73
94
  context.caller,
74
- Filter.new(condition_tree: fk_owner),
95
+ scoped_fk_owner_filter(parent_primary_key_values, context),
75
96
  {
76
97
  relation.foreign_key => foreign_value,
77
98
  relation.foreign_key_type_field => polymorphic_type
@@ -101,7 +122,7 @@ module ForestAdminAgent
101
122
  old_fk_owner_filter = Filter.new(
102
123
  condition_tree: ConditionTree::ConditionTreeFactory.intersect(
103
124
  [
104
- context.permissions.get_scope(context.collection),
125
+ context.permissions.get_scope(context.child_collection),
105
126
  ConditionTree::Nodes::ConditionTreeBranch.new(
106
127
  'And',
107
128
  [
@@ -150,7 +171,7 @@ module ForestAdminAgent
150
171
  Filter.new(
151
172
  condition_tree: ConditionTree::ConditionTreeFactory.intersect(
152
173
  [
153
- context.permissions.get_scope(context.collection), new_fk_owner
174
+ context.permissions.get_scope(context.child_collection), new_fk_owner
154
175
  ]
155
176
  )
156
177
  ),
@@ -164,7 +185,7 @@ module ForestAdminAgent
164
185
  old_fk_owner_filter = Filter.new(
165
186
  condition_tree: ConditionTree::ConditionTreeFactory.intersect(
166
187
  [
167
- context.permissions.get_scope(context.collection),
188
+ context.permissions.get_scope(context.child_collection),
168
189
  ConditionTree::Nodes::ConditionTreeLeaf.new(
169
190
  relation.origin_key,
170
191
  ConditionTree::Operators::EQUAL,
@@ -198,7 +219,7 @@ module ForestAdminAgent
198
219
  context.caller,
199
220
  Filter.new(condition_tree: ConditionTree::ConditionTreeFactory.intersect(
200
221
  [
201
- context.permissions.get_scope(context.collection),
222
+ context.permissions.get_scope(context.child_collection),
202
223
  new_fk_owner
203
224
  ]
204
225
  )),
@@ -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.3"
9
+ LIANA_VERSION = "1.38.0"
10
10
 
11
11
  def self.generate(datasource)
12
12
  datasource.collections
@@ -1,3 +1,3 @@
1
1
  module ForestAdminAgent
2
- VERSION = "1.37.3"
2
+ VERSION = "1.38.0"
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.3
4
+ version: 1.38.0
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-12 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activesupport