Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class Actions < AbstractAuthenticatedRoute
include ForestAdminDatasourceToolkit::Components::Query
include ForestAdminDatasourceToolkit::Components::Query::ConditionTree
include ForestAdminDatasourceCustomizer::Decorators::Action
include ForestAdminAgent::Routes::QueryHandler
Comment thread
bexchauveto marked this conversation as resolved.

def initialize(collection, action)
@action_name = action
Expand Down Expand Up @@ -223,18 +224,7 @@ def decode_signed_approval_request(signed_request)
def get_record_selection(args, context)
attributes = args.dig(:params, :data, :attributes)

# Match user filter + search + scope? + segment
scope = context.permissions.get_scope(context.collection)
filter = Filter.new(
condition_tree: ConditionTreeFactory.intersect(
[
scope,
ForestAdminAgent::Utils::QueryStringParser.parse_condition_tree(
context.collection, args
)
]
)
)
filter = build_context_filter(context.collection, args, context.permissions, context.caller)

# Restrict the filter to the selected records for single or bulk actions
if context.collection.schema[:actions][@action_name].scope != Types::ActionScope::GLOBAL
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,30 +23,55 @@ def execute_query(datasource, query, connection_name, permissions, caller, conte
end

def parse_query_segment(collection, args, permissions, caller)
return unless args[:params][:segmentQuery]
segment_query = QueryStringParser.subset_or_query(args, :segmentQuery)
return unless segment_query

unless args[:params][:connectionName]
connection_name = QueryStringParser.subset_or_query(args, :connectionName)
unless connection_name
raise ForestAdminAgent::Http::Exceptions::UnprocessableError, 'Missing native query connection attribute'
end

QueryValidator.valid?(args[:params][:segmentQuery])
QueryValidator.valid?(segment_query)

permissions.can_execute_query_segment?(collection, args[:params][:segmentQuery], args[:params][:connectionName])
permissions.can_execute_query_segment?(collection, segment_query, connection_name)

ids = execute_query(
collection.datasource,
args[:params][:segmentQuery],
args[:params][:connectionName],
segment_query,
connection_name,
permissions,
caller,
args[:params][:contextVariables]
QueryStringParser.subset_or_query(args, :contextVariables)
).map(&:values)

condition_tree_segment = ConditionTree::ConditionTreeFactory.match_ids(collection, ids)
ConditionTreeValidator.validate(condition_tree_segment, collection)

condition_tree_segment
end

def build_context_filter(collection, args, permissions, caller, sort: nil)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Function with many parameters (count = 5): build_context_filter [qlty:function-parameters]

condition_tree = QueryStringParser.parse_condition_tree(collection, args)
search = QueryStringParser.parse_search(collection, args)
search_extended = QueryStringParser.parse_search_extended(args)
applied = { condition_tree: condition_tree, search: search, search_extended: search_extended }
applied[:sort] = sort if sort
permissions.assert_can_read_query_fields(collection, **applied)

Filter.new(
condition_tree: ConditionTree::ConditionTreeFactory.intersect(
[
permissions.get_scope(collection),
condition_tree,
parse_query_segment(collection, args, permissions, caller)
]
),
search: search,
search_extended: search_extended,
sort: sort,
segment: QueryStringParser.parse_segment(collection, args)
)
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -19,28 +19,7 @@ def handle_request(args = {})
context.permissions.can?(:browse, context.collection)

if context.collection.is_countable?
condition_tree = ForestAdminAgent::Utils::QueryStringParser.parse_condition_tree(
context.collection, args
)
search = QueryStringParser.parse_search(context.collection, args)
search_extended = QueryStringParser.parse_search_extended(args)
context.permissions.assert_can_read_query_fields(
context.collection,
condition_tree: condition_tree, search: search, search_extended: search_extended
)

filter = ForestAdminDatasourceToolkit::Components::Query::Filter.new(
condition_tree: ConditionTreeFactory.intersect(
[
context.permissions.get_scope(context.collection),
parse_query_segment(context.collection, args, context.permissions, context.caller),
condition_tree
]
),
search: search,
search_extended: search_extended,
segment: QueryStringParser.parse_segment(context.collection, args)
)
filter = build_context_filter(context.collection, args, context.permissions, context.caller)
aggregation = ForestAdminDatasourceToolkit::Components::Query::Aggregation.new(operation: 'Count')
result = context.collection.aggregate(context.caller, filter, aggregation)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,28 +23,8 @@ def handle_request(args = {})
context.permissions.can?(:browse, context.collection)
context.permissions.can?(:export, context.collection)

condition_tree = QueryStringParser.parse_condition_tree(context.collection, args)
search = QueryStringParser.parse_search(context.collection, args)
search_extended = QueryStringParser.parse_search_extended(args)
sort = QueryStringParser.parse_sort(context.collection, args)
context.permissions.assert_can_read_query_fields(
context.collection,
condition_tree: condition_tree, sort: sort, search: search, search_extended: search_extended
)

filter = ForestAdminDatasourceToolkit::Components::Query::Filter.new(
condition_tree: ConditionTreeFactory.intersect(
[
context.permissions.get_scope(context.collection),
parse_query_segment(context.collection, args, context.permissions, context.caller),
condition_tree
]
),
search: search,
search_extended: search_extended,
sort: sort,
segment: QueryStringParser.parse_segment(context.collection, args)
)
filter = build_context_filter(context.collection, args, context.permissions, context.caller, sort: sort)
requested = QueryStringParser.parse_requested_projection(context.collection, args)
projection = context.permissions.redact_projection(
context.collection,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ class Delete < AbstractAuthenticatedRoute
include ForestAdminAgent::Builder
include ForestAdminDatasourceToolkit::Components::Query
include ForestAdminDatasourceToolkit::Components::Query::ConditionTree
include ForestAdminAgent::Routes::QueryHandler

NULLIFIED_POLYMORPHIC_TYPES = %w[PolymorphicOneToOne PolymorphicOneToMany].freeze

def setup_routes
add_route('forest_delete_bulk', 'delete', '/:collection_name', ->(args) { handle_request_bulk(args) })
Expand Down Expand Up @@ -39,45 +42,44 @@ def delete_records(args, selection_ids, context)
selection_ids[:ids])
condition_tree_ids = condition_tree_ids.inverse if selection_ids[:are_excluded]

context.collection.schema[:fields].each_value do |field_schema|
next unless ['PolymorphicOneToOne', 'PolymorphicOneToMany'].include?(field_schema.type)
next if field_schema.respond_to?(:cascade_on_delete) && field_schema.cascade_on_delete
filter = build_context_filter(context.collection, args, context.permissions, context.caller)
filter = filter.override(
condition_tree: ConditionTree::ConditionTreeFactory.intersect([filter.condition_tree, condition_tree_ids])
)

relations = nullified_polymorphic_relations(context.collection)
# ponytail: loads every deleted record's keys at once, page it if select-all deletes outgrow memory
records = if relations.any?
context.collection.list(context.caller, filter,
Projection.new(relations.map(&:origin_key_target).uniq))
end

origin_values = selection_ids[:ids].map do |pk_hash|
if pk_hash.is_a?(Hash)
pk_hash[field_schema.origin_key_target]
else
pk_names = ForestAdminDatasourceToolkit::Utils::Schema.primary_keys(context.collection)
index = pk_names.index(field_schema.origin_key_target)
pk_hash[index] if index
end
end
context.collection.delete(context.caller, filter)
relations.each { |relation| nullify_polymorphic_references(context, relation, records) }
end

condition_tree = Nodes::ConditionTreeBranch.new(
'And',
[
Nodes::ConditionTreeLeaf.new(field_schema.origin_key, Operators::IN, origin_values),
Nodes::ConditionTreeLeaf.new(field_schema.origin_type_field, Operators::EQUAL,
context.collection.name.gsub('__', '::'))
]
)
filter = Filter.new(condition_tree: condition_tree)
context.datasource.get_collection(field_schema.foreign_collection)
.update(context.caller, filter, { field_schema.origin_key => nil,
field_schema.origin_type_field => nil })
def nullified_polymorphic_relations(collection)
collection.schema[:fields].values.select do |field_schema|
NULLIFIED_POLYMORPHIC_TYPES.include?(field_schema.type) &&
!(field_schema.respond_to?(:cascade_on_delete) && field_schema.cascade_on_delete)
end
end

filter = ForestAdminDatasourceToolkit::Components::Query::Filter.new(
condition_tree: ConditionTree::ConditionTreeFactory.intersect(
[
Utils::QueryStringParser.parse_condition_tree(context.collection, args),
condition_tree_ids,
context.permissions.get_scope(context.collection)
]
)
)
def nullify_polymorphic_references(context, relation, records)
origin_values = records.map { |record| record[relation.origin_key_target] }
return if origin_values.empty?

context.collection.delete(context.caller, filter)
condition_tree = Nodes::ConditionTreeBranch.new(
'And',
[
Nodes::ConditionTreeLeaf.new(relation.origin_key, Operators::IN, origin_values),
Nodes::ConditionTreeLeaf.new(relation.origin_type_field, Operators::EQUAL,
context.collection.name.gsub('__', '::'))
]
)
context.datasource.get_collection(relation.foreign_collection)
.update(context.caller, Filter.new(condition_tree: condition_tree),
{ relation.origin_key => nil, relation.origin_type_field => nil })
end
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,29 +18,9 @@ def handle_request(args = {})
context = build(args)
context.permissions.can?(:browse, context.collection)

condition_tree = QueryStringParser.parse_condition_tree(context.collection, args)
search = QueryStringParser.parse_search(context.collection, args)
search_extended = QueryStringParser.parse_search_extended(args)
sort = QueryStringParser.parse_sort(context.collection, args)
context.permissions.assert_can_read_query_fields(
context.collection,
condition_tree: condition_tree, sort: sort, search: search, search_extended: search_extended
)

filter = ForestAdminDatasourceToolkit::Components::Query::Filter.new(
condition_tree: ConditionTreeFactory.intersect(
[
context.permissions.get_scope(context.collection),
condition_tree,
parse_query_segment(context.collection, args, context.permissions, context.caller)
]
),
page: QueryStringParser.parse_pagination(args),
search: search,
search_extended: search_extended,
sort: sort,
segment: QueryStringParser.parse_segment(context.collection, args)
)
filter = build_context_filter(context.collection, args, context.permissions, context.caller, sort: sort)
.override(page: QueryStringParser.parse_pagination(args))

projection = redacted_projection_with_pks(context, context.collection, args)
records = context.collection.list(context.caller, filter, projection)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ class CountRelated < AbstractRelatedRoute
include ForestAdminAgent::Builder
include ForestAdminDatasourceToolkit::Utils
include ForestAdminDatasourceToolkit::Components::Query
include ForestAdminAgent::Routes::QueryHandler

def setup_routes
add_route(
Expand All @@ -25,7 +26,7 @@ def handle_request(args = {})
context.permissions.can?(:browse, context.child_collection)

if context.child_collection.is_countable?
filter = Filter.new(condition_tree: context.permissions.get_scope(context.child_collection))
filter = build_context_filter(context.child_collection, args, context.permissions, context.caller)
primary_key_values = Utils::Id.unpack_id(context.collection, args[:params]['id'], with_key: true)
result = Collection.aggregate_relation(
context.collection,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ class CsvRelated < AbstractRelatedRoute
include ForestAdminAgent::Utils
include ForestAdminDatasourceToolkit::Utils
include ForestAdminDatasourceToolkit::Components::Query::ConditionTree
include ForestAdminAgent::Routes::QueryHandler

def setup_routes
add_route(
Expand All @@ -23,18 +24,7 @@ def handle_request(args = {})
context = build(args)
context.permissions.can?(:browse, context.child_collection)
context.permissions.can?(:export, context.child_collection)
condition_tree = ForestAdminAgent::Utils::QueryStringParser.parse_condition_tree(
context.child_collection, args
)
context.permissions.assert_can_read_query_fields(
context.child_collection, condition_tree: condition_tree
)

filter = ForestAdminDatasourceToolkit::Components::Query::Filter.new(
condition_tree: ConditionTreeFactory.intersect(
[context.permissions.get_scope(context.child_collection), condition_tree]
)
)
filter = build_context_filter(context.child_collection, args, context.permissions, context.caller)
requested = ForestAdminAgent::Utils::QueryStringParser.parse_requested_projection(
context.child_collection, args
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ class DissociateRelated < AbstractRelatedRoute
include ForestAdminAgent::Builder
include ForestAdminDatasourceToolkit::Utils
include ForestAdminDatasourceToolkit::Components::Query
include ForestAdminAgent::Routes::QueryHandler

def setup_routes
add_route(
Expand Down Expand Up @@ -126,14 +127,9 @@ def get_base_foreign_filter(args, context)
raise ForestAdminDatasourceToolkit::Exceptions::ForestException, 'Expected no empty id list'
end

Filter.new(
condition_tree: ConditionTree::ConditionTreeFactory.intersect(
[
context.permissions.get_scope(context.child_collection),
Utils::QueryStringParser.parse_condition_tree(context.child_collection, args),
selected_ids
]
)
filter = build_context_filter(context.child_collection, args, context.permissions, context.caller)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Opus 5.5 (claude-opus-5-5), from manual testing at b9b6b40: a select-all delete on a searched related list now deletes fewer records than the user selected, because the related list itself ignores the search.

  1. Open a project's Tasks related list and search brief. The list still shows every task, Fuel the rocket included.
  2. Select all records, untick one, then Delete the tasks. The confirm says 3 tasks.
  3. Only the 2 brief tasks are deleted. Fuel the rocket survives.

Before this PR the same action deleted all 3, since the related list and the delete both ignored the search. Now the delete honours it through build_context_filter here, while list_related.rb:34, count_related.rb:28 and csv_related.rb:33 still build their filter from the scope and filters only, never search or segment. Node's related list, count and csv go through ContextFilterFactory.build.

A way out: build those three filters with build_context_filter, as list, count and csv now do.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Opus 5.5 (claude-opus-5-5): Related list, count and csv now build their filter through build_context_filter, so they show and count what a select-all delete reaches.
76957e2

Why

Each route has a spec where a search and a segment reach the related filter.
Related count also applies the list's filters now, which it used to drop.

filter.override(
condition_tree: ConditionTree::ConditionTreeFactory.intersect([filter.condition_tree, selected_ids])
)
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ class ListRelated < AbstractRelatedRoute
include ForestAdminAgent::Builder
include ForestAdminDatasourceToolkit::Utils
include ForestAdminDatasourceToolkit::Components::Query::ConditionTree
include ForestAdminAgent::Routes::QueryHandler

def setup_routes
add_route(
Expand All @@ -23,21 +24,10 @@ def setup_routes
def handle_request(args = {})
context = build(args)
context.permissions.can?(:browse, context.child_collection)
condition_tree = ForestAdminAgent::Utils::QueryStringParser.parse_condition_tree(
context.child_collection, args
)
sort = ForestAdminAgent::Utils::QueryStringParser.parse_sort(context.child_collection, args)
context.permissions.assert_can_read_query_fields(
context.child_collection, condition_tree: condition_tree, sort: sort
)

filter = ForestAdminDatasourceToolkit::Components::Query::Filter.new(
condition_tree: ConditionTreeFactory.intersect(
[context.permissions.get_scope(context.child_collection), condition_tree]
),
page: ForestAdminAgent::Utils::QueryStringParser.parse_pagination(args),
sort: sort
)
filter = build_context_filter(context.child_collection, args, context.permissions, context.caller,
sort: sort)
.override(page: ForestAdminAgent::Utils::QueryStringParser.parse_pagination(args))
projection = redacted_projection_with_pks(context, context.child_collection, args)
primary_key_values = Utils::Id.unpack_id(context.collection, args[:params]['id'], with_key: true)
records = Collection.list_relation(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -318,8 +318,7 @@ def self.parse_chart_parameters(args)
end

def self.parse_segment(collection, args)
segment = args.dig(:params, :data, :attributes, :all_records_subset_query,
:segment) || args.dig(:params, :segment)
segment = subset_or_query(args, :segment) || args.dig(:params, :segment)

return unless segment

Expand Down
Loading
Loading