forest_admin_agent 1.46.0 → 1.46.2

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: 1f60fdc1a8849851e0aeb16b6ac0f42eabe7a01ebc54b432ae75ef2ba3b9ba00
4
- data.tar.gz: 68772b2ffdd8247f2de2308d231d4225b61e033deeb8508cfe686325d4e52f41
3
+ metadata.gz: 66c2c677df8cd9ee2cdb91925ea87a84e7d7b949d5ebf2f57104de3591e1df24
4
+ data.tar.gz: 8a844fc3354a9e522f04038e6f966252370650980035e1ad59763a0a91d74331
5
5
  SHA512:
6
- metadata.gz: 6b17eb054da746a8c13de4b164c8f3128b4b89f1d52bab2d7a5ba848c904f441afc70412c3e01a74090e7783cb2c9cc8ffff11ce31eca67828c3b5517befbecc
7
- data.tar.gz: bbbbc65fcb9f9d0404de90f78c6f216561775ad7e4b36b85669b191c3c506d471feef4510c708f88b2a7b77c9a1de8dbb1656a439da665c8afb38838486beab1
6
+ metadata.gz: f1c7ffe69debc653eeec2013f3038e61a16119c78d18930851e8a2d45b09c32ebb4378843295c38fb05d2b3d6d779e1a9ab65f76decd08a587c41ee91ef1a46e
7
+ data.tar.gz: 42b9d25705b3695cdeb9858026362f5732df3f54433afec6e17fd351615411cd8fa98bec3129e101ab1df5595e3c87f6d5c78297a3130387f35b7deab356033d
@@ -11,6 +11,7 @@ module ForestAdminAgent
11
11
  include ForestAdminDatasourceToolkit::Components::Query
12
12
  include ForestAdminDatasourceToolkit::Components::Query::ConditionTree
13
13
  include ForestAdminDatasourceCustomizer::Decorators::Action
14
+ include ForestAdminAgent::Routes::QueryHandler
14
15
 
15
16
  def initialize(collection, action)
16
17
  @action_name = action
@@ -223,18 +224,7 @@ module ForestAdminAgent
223
224
  def get_record_selection(args, context)
224
225
  attributes = args.dig(:params, :data, :attributes)
225
226
 
226
- # Match user filter + search + scope? + segment
227
- scope = context.permissions.get_scope(context.collection)
228
- filter = Filter.new(
229
- condition_tree: ConditionTreeFactory.intersect(
230
- [
231
- scope,
232
- ForestAdminAgent::Utils::QueryStringParser.parse_condition_tree(
233
- context.collection, args
234
- )
235
- ]
236
- )
237
- )
227
+ filter = build_context_filter(context.collection, args, context.permissions, context.caller)
238
228
 
239
229
  # Restrict the filter to the selected records for single or bulk actions
240
230
  if context.collection.schema[:actions][@action_name].scope != Types::ActionScope::GLOBAL
@@ -23,23 +23,25 @@ module ForestAdminAgent
23
23
  end
24
24
 
25
25
  def parse_query_segment(collection, args, permissions, caller)
26
- return unless args[:params][:segmentQuery]
26
+ segment_query = QueryStringParser.subset_or_query(args, :segmentQuery)
27
+ return unless segment_query
27
28
 
28
- unless args[:params][:connectionName]
29
+ connection_name = QueryStringParser.subset_or_query(args, :connectionName)
30
+ unless connection_name
29
31
  raise ForestAdminAgent::Http::Exceptions::UnprocessableError, 'Missing native query connection attribute'
30
32
  end
31
33
 
32
- QueryValidator.valid?(args[:params][:segmentQuery])
34
+ QueryValidator.valid?(segment_query)
33
35
 
34
- permissions.can_execute_query_segment?(collection, args[:params][:segmentQuery], args[:params][:connectionName])
36
+ permissions.can_execute_query_segment?(collection, segment_query, connection_name)
35
37
 
36
38
  ids = execute_query(
37
39
  collection.datasource,
38
- args[:params][:segmentQuery],
39
- args[:params][:connectionName],
40
+ segment_query,
41
+ connection_name,
40
42
  permissions,
41
43
  caller,
42
- args[:params][:contextVariables]
44
+ QueryStringParser.subset_or_query(args, :contextVariables)
43
45
  ).map(&:values)
44
46
 
45
47
  condition_tree_segment = ConditionTree::ConditionTreeFactory.match_ids(collection, ids)
@@ -47,6 +49,29 @@ module ForestAdminAgent
47
49
 
48
50
  condition_tree_segment
49
51
  end
52
+
53
+ def build_context_filter(collection, args, permissions, caller, sort: nil)
54
+ condition_tree = QueryStringParser.parse_condition_tree(collection, args)
55
+ search = QueryStringParser.parse_search(collection, args)
56
+ search_extended = QueryStringParser.parse_search_extended(args)
57
+ applied = { condition_tree: condition_tree, search: search, search_extended: search_extended }
58
+ applied[:sort] = sort if sort
59
+ permissions.assert_can_read_query_fields(collection, **applied)
60
+
61
+ Filter.new(
62
+ condition_tree: ConditionTree::ConditionTreeFactory.intersect(
63
+ [
64
+ permissions.get_scope(collection),
65
+ condition_tree,
66
+ parse_query_segment(collection, args, permissions, caller)
67
+ ]
68
+ ),
69
+ search: search,
70
+ search_extended: search_extended,
71
+ sort: sort,
72
+ segment: QueryStringParser.parse_segment(collection, args)
73
+ )
74
+ end
50
75
  end
51
76
  end
52
77
  end
@@ -19,28 +19,7 @@ module ForestAdminAgent
19
19
  context.permissions.can?(:browse, context.collection)
20
20
 
21
21
  if context.collection.is_countable?
22
- condition_tree = ForestAdminAgent::Utils::QueryStringParser.parse_condition_tree(
23
- context.collection, args
24
- )
25
- search = QueryStringParser.parse_search(context.collection, args)
26
- search_extended = QueryStringParser.parse_search_extended(args)
27
- context.permissions.assert_can_read_query_fields(
28
- context.collection,
29
- condition_tree: condition_tree, search: search, search_extended: search_extended
30
- )
31
-
32
- filter = ForestAdminDatasourceToolkit::Components::Query::Filter.new(
33
- condition_tree: ConditionTreeFactory.intersect(
34
- [
35
- context.permissions.get_scope(context.collection),
36
- parse_query_segment(context.collection, args, context.permissions, context.caller),
37
- condition_tree
38
- ]
39
- ),
40
- search: search,
41
- search_extended: search_extended,
42
- segment: QueryStringParser.parse_segment(context.collection, args)
43
- )
22
+ filter = build_context_filter(context.collection, args, context.permissions, context.caller)
44
23
  aggregation = ForestAdminDatasourceToolkit::Components::Query::Aggregation.new(operation: 'Count')
45
24
  result = context.collection.aggregate(context.caller, filter, aggregation)
46
25
 
@@ -23,28 +23,8 @@ module ForestAdminAgent
23
23
  context.permissions.can?(:browse, context.collection)
24
24
  context.permissions.can?(:export, context.collection)
25
25
 
26
- condition_tree = QueryStringParser.parse_condition_tree(context.collection, args)
27
- search = QueryStringParser.parse_search(context.collection, args)
28
- search_extended = QueryStringParser.parse_search_extended(args)
29
26
  sort = QueryStringParser.parse_sort(context.collection, args)
30
- context.permissions.assert_can_read_query_fields(
31
- context.collection,
32
- condition_tree: condition_tree, sort: sort, search: search, search_extended: search_extended
33
- )
34
-
35
- filter = ForestAdminDatasourceToolkit::Components::Query::Filter.new(
36
- condition_tree: ConditionTreeFactory.intersect(
37
- [
38
- context.permissions.get_scope(context.collection),
39
- parse_query_segment(context.collection, args, context.permissions, context.caller),
40
- condition_tree
41
- ]
42
- ),
43
- search: search,
44
- search_extended: search_extended,
45
- sort: sort,
46
- segment: QueryStringParser.parse_segment(context.collection, args)
47
- )
27
+ filter = build_context_filter(context.collection, args, context.permissions, context.caller, sort: sort)
48
28
  requested = QueryStringParser.parse_requested_projection(context.collection, args)
49
29
  projection = context.permissions.redact_projection(
50
30
  context.collection,
@@ -8,6 +8,9 @@ module ForestAdminAgent
8
8
  include ForestAdminAgent::Builder
9
9
  include ForestAdminDatasourceToolkit::Components::Query
10
10
  include ForestAdminDatasourceToolkit::Components::Query::ConditionTree
11
+ include ForestAdminAgent::Routes::QueryHandler
12
+
13
+ NULLIFIED_POLYMORPHIC_TYPES = %w[PolymorphicOneToOne PolymorphicOneToMany].freeze
11
14
 
12
15
  def setup_routes
13
16
  add_route('forest_delete_bulk', 'delete', '/:collection_name', ->(args) { handle_request_bulk(args) })
@@ -39,45 +42,44 @@ module ForestAdminAgent
39
42
  selection_ids[:ids])
40
43
  condition_tree_ids = condition_tree_ids.inverse if selection_ids[:are_excluded]
41
44
 
42
- context.collection.schema[:fields].each_value do |field_schema|
43
- next unless ['PolymorphicOneToOne', 'PolymorphicOneToMany'].include?(field_schema.type)
44
- next if field_schema.respond_to?(:cascade_on_delete) && field_schema.cascade_on_delete
45
+ filter = build_context_filter(context.collection, args, context.permissions, context.caller)
46
+ filter = filter.override(
47
+ condition_tree: ConditionTree::ConditionTreeFactory.intersect([filter.condition_tree, condition_tree_ids])
48
+ )
49
+
50
+ relations = nullified_polymorphic_relations(context.collection)
51
+ # ponytail: loads every deleted record's keys at once, page it if select-all deletes outgrow memory
52
+ records = if relations.any?
53
+ context.collection.list(context.caller, filter,
54
+ Projection.new(relations.map(&:origin_key_target).uniq))
55
+ end
45
56
 
46
- origin_values = selection_ids[:ids].map do |pk_hash|
47
- if pk_hash.is_a?(Hash)
48
- pk_hash[field_schema.origin_key_target]
49
- else
50
- pk_names = ForestAdminDatasourceToolkit::Utils::Schema.primary_keys(context.collection)
51
- index = pk_names.index(field_schema.origin_key_target)
52
- pk_hash[index] if index
53
- end
54
- end
57
+ context.collection.delete(context.caller, filter)
58
+ relations.each { |relation| nullify_polymorphic_references(context, relation, records) }
59
+ end
55
60
 
56
- condition_tree = Nodes::ConditionTreeBranch.new(
57
- 'And',
58
- [
59
- Nodes::ConditionTreeLeaf.new(field_schema.origin_key, Operators::IN, origin_values),
60
- Nodes::ConditionTreeLeaf.new(field_schema.origin_type_field, Operators::EQUAL,
61
- context.collection.name.gsub('__', '::'))
62
- ]
63
- )
64
- filter = Filter.new(condition_tree: condition_tree)
65
- context.datasource.get_collection(field_schema.foreign_collection)
66
- .update(context.caller, filter, { field_schema.origin_key => nil,
67
- field_schema.origin_type_field => nil })
61
+ def nullified_polymorphic_relations(collection)
62
+ collection.schema[:fields].values.select do |field_schema|
63
+ NULLIFIED_POLYMORPHIC_TYPES.include?(field_schema.type) &&
64
+ !(field_schema.respond_to?(:cascade_on_delete) && field_schema.cascade_on_delete)
68
65
  end
66
+ end
69
67
 
70
- filter = ForestAdminDatasourceToolkit::Components::Query::Filter.new(
71
- condition_tree: ConditionTree::ConditionTreeFactory.intersect(
72
- [
73
- Utils::QueryStringParser.parse_condition_tree(context.collection, args),
74
- condition_tree_ids,
75
- context.permissions.get_scope(context.collection)
76
- ]
77
- )
78
- )
68
+ def nullify_polymorphic_references(context, relation, records)
69
+ origin_values = records.map { |record| record[relation.origin_key_target] }
70
+ return if origin_values.empty?
79
71
 
80
- context.collection.delete(context.caller, filter)
72
+ condition_tree = Nodes::ConditionTreeBranch.new(
73
+ 'And',
74
+ [
75
+ Nodes::ConditionTreeLeaf.new(relation.origin_key, Operators::IN, origin_values),
76
+ Nodes::ConditionTreeLeaf.new(relation.origin_type_field, Operators::EQUAL,
77
+ context.collection.name.gsub('__', '::'))
78
+ ]
79
+ )
80
+ context.datasource.get_collection(relation.foreign_collection)
81
+ .update(context.caller, Filter.new(condition_tree: condition_tree),
82
+ { relation.origin_key => nil, relation.origin_type_field => nil })
81
83
  end
82
84
  end
83
85
  end
@@ -18,29 +18,9 @@ module ForestAdminAgent
18
18
  context = build(args)
19
19
  context.permissions.can?(:browse, context.collection)
20
20
 
21
- condition_tree = QueryStringParser.parse_condition_tree(context.collection, args)
22
- search = QueryStringParser.parse_search(context.collection, args)
23
- search_extended = QueryStringParser.parse_search_extended(args)
24
21
  sort = QueryStringParser.parse_sort(context.collection, args)
25
- context.permissions.assert_can_read_query_fields(
26
- context.collection,
27
- condition_tree: condition_tree, sort: sort, search: search, search_extended: search_extended
28
- )
29
-
30
- filter = ForestAdminDatasourceToolkit::Components::Query::Filter.new(
31
- condition_tree: ConditionTreeFactory.intersect(
32
- [
33
- context.permissions.get_scope(context.collection),
34
- condition_tree,
35
- parse_query_segment(context.collection, args, context.permissions, context.caller)
36
- ]
37
- ),
38
- page: QueryStringParser.parse_pagination(args),
39
- search: search,
40
- search_extended: search_extended,
41
- sort: sort,
42
- segment: QueryStringParser.parse_segment(context.collection, args)
43
- )
22
+ filter = build_context_filter(context.collection, args, context.permissions, context.caller, sort: sort)
23
+ .override(page: QueryStringParser.parse_pagination(args))
44
24
 
45
25
  projection = redacted_projection_with_pks(context, context.collection, args)
46
26
  records = context.collection.list(context.caller, filter, projection)
@@ -8,6 +8,7 @@ module ForestAdminAgent
8
8
  include ForestAdminAgent::Builder
9
9
  include ForestAdminDatasourceToolkit::Utils
10
10
  include ForestAdminDatasourceToolkit::Components::Query
11
+ include ForestAdminAgent::Routes::QueryHandler
11
12
 
12
13
  def setup_routes
13
14
  add_route(
@@ -25,7 +26,7 @@ module ForestAdminAgent
25
26
  context.permissions.can?(:browse, context.child_collection)
26
27
 
27
28
  if context.child_collection.is_countable?
28
- filter = Filter.new(condition_tree: context.permissions.get_scope(context.child_collection))
29
+ filter = build_context_filter(context.child_collection, args, context.permissions, context.caller)
29
30
  primary_key_values = Utils::Id.unpack_id(context.collection, args[:params]['id'], with_key: true)
30
31
  result = Collection.aggregate_relation(
31
32
  context.collection,
@@ -6,6 +6,7 @@ module ForestAdminAgent
6
6
  include ForestAdminAgent::Utils
7
7
  include ForestAdminDatasourceToolkit::Utils
8
8
  include ForestAdminDatasourceToolkit::Components::Query::ConditionTree
9
+ include ForestAdminAgent::Routes::QueryHandler
9
10
 
10
11
  def setup_routes
11
12
  add_route(
@@ -23,18 +24,7 @@ module ForestAdminAgent
23
24
  context = build(args)
24
25
  context.permissions.can?(:browse, context.child_collection)
25
26
  context.permissions.can?(:export, context.child_collection)
26
- condition_tree = ForestAdminAgent::Utils::QueryStringParser.parse_condition_tree(
27
- context.child_collection, args
28
- )
29
- context.permissions.assert_can_read_query_fields(
30
- context.child_collection, condition_tree: condition_tree
31
- )
32
-
33
- filter = ForestAdminDatasourceToolkit::Components::Query::Filter.new(
34
- condition_tree: ConditionTreeFactory.intersect(
35
- [context.permissions.get_scope(context.child_collection), condition_tree]
36
- )
37
- )
27
+ filter = build_context_filter(context.child_collection, args, context.permissions, context.caller)
38
28
  requested = ForestAdminAgent::Utils::QueryStringParser.parse_requested_projection(
39
29
  context.child_collection, args
40
30
  )
@@ -8,6 +8,7 @@ module ForestAdminAgent
8
8
  include ForestAdminAgent::Builder
9
9
  include ForestAdminDatasourceToolkit::Utils
10
10
  include ForestAdminDatasourceToolkit::Components::Query
11
+ include ForestAdminAgent::Routes::QueryHandler
11
12
 
12
13
  def setup_routes
13
14
  add_route(
@@ -126,14 +127,9 @@ module ForestAdminAgent
126
127
  raise ForestAdminDatasourceToolkit::Exceptions::ForestException, 'Expected no empty id list'
127
128
  end
128
129
 
129
- Filter.new(
130
- condition_tree: ConditionTree::ConditionTreeFactory.intersect(
131
- [
132
- context.permissions.get_scope(context.child_collection),
133
- Utils::QueryStringParser.parse_condition_tree(context.child_collection, args),
134
- selected_ids
135
- ]
136
- )
130
+ filter = build_context_filter(context.child_collection, args, context.permissions, context.caller)
131
+ filter.override(
132
+ condition_tree: ConditionTree::ConditionTreeFactory.intersect([filter.condition_tree, selected_ids])
137
133
  )
138
134
  end
139
135
  end
@@ -8,6 +8,7 @@ module ForestAdminAgent
8
8
  include ForestAdminAgent::Builder
9
9
  include ForestAdminDatasourceToolkit::Utils
10
10
  include ForestAdminDatasourceToolkit::Components::Query::ConditionTree
11
+ include ForestAdminAgent::Routes::QueryHandler
11
12
 
12
13
  def setup_routes
13
14
  add_route(
@@ -23,21 +24,10 @@ module ForestAdminAgent
23
24
  def handle_request(args = {})
24
25
  context = build(args)
25
26
  context.permissions.can?(:browse, context.child_collection)
26
- condition_tree = ForestAdminAgent::Utils::QueryStringParser.parse_condition_tree(
27
- context.child_collection, args
28
- )
29
27
  sort = ForestAdminAgent::Utils::QueryStringParser.parse_sort(context.child_collection, args)
30
- context.permissions.assert_can_read_query_fields(
31
- context.child_collection, condition_tree: condition_tree, sort: sort
32
- )
33
-
34
- filter = ForestAdminDatasourceToolkit::Components::Query::Filter.new(
35
- condition_tree: ConditionTreeFactory.intersect(
36
- [context.permissions.get_scope(context.child_collection), condition_tree]
37
- ),
38
- page: ForestAdminAgent::Utils::QueryStringParser.parse_pagination(args),
39
- sort: sort
40
- )
28
+ filter = build_context_filter(context.child_collection, args, context.permissions, context.caller,
29
+ sort: sort)
30
+ .override(page: ForestAdminAgent::Utils::QueryStringParser.parse_pagination(args))
41
31
  projection = redacted_projection_with_pks(context, context.child_collection, args)
42
32
  primary_key_values = Utils::Id.unpack_id(context.collection, args[:params]['id'], with_key: true)
43
33
  records = Collection.list_relation(
@@ -318,8 +318,7 @@ module ForestAdminAgent
318
318
  end
319
319
 
320
320
  def self.parse_segment(collection, args)
321
- segment = args.dig(:params, :data, :attributes, :all_records_subset_query,
322
- :segment) || args.dig(:params, :segment)
321
+ segment = subset_or_query(args, :segment) || args.dig(:params, :segment)
323
322
 
324
323
  return unless segment
325
324
 
@@ -6,7 +6,7 @@ module ForestAdminAgent
6
6
  module Schema
7
7
  class SchemaEmitter
8
8
  LIANA_NAME = "agent-ruby"
9
- LIANA_VERSION = "1.46.0"
9
+ LIANA_VERSION = "1.46.2"
10
10
 
11
11
  def self.generate(datasource)
12
12
  datasource.collections
@@ -1,3 +1,3 @@
1
1
  module ForestAdminAgent
2
- VERSION = "1.46.0"
2
+ VERSION = "1.46.2"
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.46.0
4
+ version: 1.46.2
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-10-16 00:00:00.000000000 Z
12
+ date: 2026-10-20 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activerecord