diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/routes/capabilities/collections.rb b/packages/forest_admin_agent/lib/forest_admin_agent/routes/capabilities/collections.rb index 8c88ab67e..db847e196 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/routes/capabilities/collections.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/routes/capabilities/collections.rb @@ -24,11 +24,13 @@ def handle_request(args = {}) collection = datasource.get_collection(collection_name) { name: collection.name, - fields: collection.schema[:fields].select { |_, field| field.is_a?(ColumnSchema) }.map do |name, field| + fields: collection.schema[:fields].filter_map do |name, field| + next unless field.is_a?(ColumnSchema) + { name: name, type: field.column_type, - operators: field.filter_operators.map { |operator| operator } + operators: field.filter_operators.to_a } end } diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/serializer/forest_serializer.rb b/packages/forest_admin_agent/lib/forest_admin_agent/serializer/forest_serializer.rb index 348e9714b..0f2ba7d0d 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/serializer/forest_serializer.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/serializer/forest_serializer.rb @@ -29,10 +29,7 @@ def id @options[:class_name].gsub('::', '__') ) primary_keys = ForestAdminDatasourceToolkit::Utils::Schema.primary_keys(forest_collection) - id = [] - primary_keys.each { |key| id << @object[key] } - - id.join('|') + primary_keys.map { |key| @object[key] }.join('|') end def format_name(attribute_name) diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/services/permissions.rb b/packages/forest_admin_agent/lib/forest_admin_agent/services/permissions.rb index c497425db..8db0dcc32 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/services/permissions.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/services/permissions.rb @@ -263,7 +263,7 @@ def permission_system? def find_action_from_endpoint(collection_name, path, http_method) endpoint = path.partition('/forest/')[1..].join schema_file = JSON.parse(File.read(Facades::Container.config_from_cache[:schema_path])) - actions = schema_file['collections']&.select { |collection| collection['name'] == collection_name }&.first&.dig('actions') + actions = schema_file['collections']&.find { |collection| collection['name'] == collection_name }&.dig('actions') return nil if actions.nil? || actions.empty? diff --git a/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/parser/column.rb b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/parser/column.rb index 5723c7c57..59e578d2a 100644 --- a/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/parser/column.rb +++ b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/parser/column.rb @@ -43,15 +43,13 @@ def get_column_type(model, column) end def get_enum_values(model, column) - enum_values = [] - if get_column_type(model, column) == 'Enum' - if sti_column?(model, column) - model.descendants.each { |sti_model| enum_values << sti_model.name } - else - model.defined_enums[column.name].each_key { |name| enum_values << name } - end + return [] unless get_column_type(model, column) == 'Enum' + + if sti_column?(model, column) + model.descendants.map(&:name) + else + model.defined_enums[column.name].keys end - enum_values end def sti_column?(model, column) diff --git a/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/parser/validation.rb b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/parser/validation.rb index e59e472bd..27b3b15b1 100644 --- a/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/parser/validation.rb +++ b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/parser/validation.rb @@ -6,7 +6,7 @@ def get_validations(column) validations = [] # NOTICE: Do not consider validations if a before_validation Active Records # Callback is detected. - return validations if @model._validation_callbacks.map(&:kind).include? :before + return validations if @model._validation_callbacks.any? { |callback| callback.kind == :before } if @model._validators? && @model._validators[column.name.to_sym].size.positive? @model._validators[column.name.to_sym].each do |validator| diff --git a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/composite_datasource.rb b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/composite_datasource.rb index 6a42802c8..053e73663 100644 --- a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/composite_datasource.rb +++ b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/composite_datasource.rb @@ -52,10 +52,9 @@ def execute_native_query(connection_name, query, context_variables = {}) end def add_data_source(datasource) - existing_names = collections.map { |c| c.respond_to?(:name) ? c.name : c.to_s } - datasource.collections.each do |c| - new_name = c.respond_to?(:name) ? c.name : c.to_s - raise ArgumentError, "Collection '#{new_name}' already exists" if existing_names.include?(new_name) + existing_names = collections.keys + datasource.collections.each_key do |name| + raise ArgumentError, "Collection '#{name}' already exists" if existing_names.include?(name) end existing_charts = schema[:charts] diff --git a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/computed/utils/flattener.rb b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/computed/utils/flattener.rb index add0941ee..0c182b2b0 100644 --- a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/computed/utils/flattener.rb +++ b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/computed/utils/flattener.rb @@ -17,16 +17,22 @@ def dig(*_keys) MARKER_NAME = '__null_marker'.freeze def self.with_null_marker(projection) + seen = Set.new(projection) new_projection = Projection.new(projection) + projection.each do |path| parts = path.split(':') parts.slice(1, parts.size).each_with_index do |_item, index| - new_projection << "#{parts.slice(0, index + 1).join(":")}:#{MARKER_NAME}" + marker = "#{parts.slice(0, index + 1).join(":")}:#{MARKER_NAME}" + next if seen.include?(marker) + + seen << marker + new_projection << marker end end - new_projection.uniq + new_projection end def self.flatten(records, projection) diff --git a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/relation/relation_collection_decorator.rb b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/relation/relation_collection_decorator.rb index 8fe979dd5..918583d0f 100644 --- a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/relation/relation_collection_decorator.rb +++ b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/relation/relation_collection_decorator.rb @@ -240,31 +240,37 @@ def re_project_in_place(caller, records, projection) def re_project_relation_in_place(caller, records, name, projection) field_schema = schema[:fields][name] - return if field_schema.type == 'PolymorphicManyToOne' - association = datasource.get_collection(field_schema.foreign_collection) - if !@relations[name] + association = datasource.get_collection(field_schema.foreign_collection) association.re_project_in_place(caller, records.filter_map { |r| r[name] }, projection) elsif field_schema.type == 'ManyToOne' - ids = records.filter_map { |record| record[field_schema.foreign_key] }.uniq - sub_filter = Filter.new(condition_tree: ConditionTreeLeaf.new(field_schema.foreign_key_target, 'In', ids)) - sub_records = association.list(caller, sub_filter, projection.union([field_schema.foreign_key_target])) - - records.each do |record| - record[name] = sub_records.find { |sr| sr[field_schema.foreign_key_target] == record[field_schema.foreign_key] } - end + assign_many_to_one_records(caller, records, name, field_schema, projection) elsif ['OneToOne', 'OneToMany'].include?(field_schema.type) - ids = records.filter_map { |record| record[field_schema.origin_key_target] }.uniq - sub_filter = Filter.new(condition_tree: ConditionTreeLeaf.new(field_schema.origin_key, 'In', ids)) - sub_records = association.list(caller, sub_filter, projection.union([field_schema.origin_key])) - - records.each do |record| - record[name] = sub_records.find { |sr| sr[field_schema.origin_key] == record[field_schema.origin_key_target] } - end + assign_to_one_or_many_records(caller, records, name, field_schema, projection) end end + + def assign_many_to_one_records(caller, records, name, field_schema, projection) + association = datasource.get_collection(field_schema.foreign_collection) + ids = records.each_with_object(Set.new) { |record, set| set << record[field_schema.foreign_key] }.delete(nil).to_a + sub_filter = Filter.new(condition_tree: ConditionTreeLeaf.new(field_schema.foreign_key_target, 'In', ids)) + sub_records = association.list(caller, sub_filter, projection.union([field_schema.foreign_key_target])) + sub_records_by_key = sub_records.to_h { |sr| [sr[field_schema.foreign_key_target], sr] } + + records.each { |record| record[name] = sub_records_by_key[record[field_schema.foreign_key]] } + end + + def assign_to_one_or_many_records(caller, records, name, field_schema, projection) + association = datasource.get_collection(field_schema.foreign_collection) + ids = records.each_with_object(Set.new) { |record, set| set << record[field_schema.origin_key_target] }.delete(nil).to_a + sub_filter = Filter.new(condition_tree: ConditionTreeLeaf.new(field_schema.origin_key, 'In', ids)) + sub_records = association.list(caller, sub_filter, projection.union([field_schema.origin_key])) + sub_records_by_key = sub_records.to_h { |sr| [sr[field_schema.origin_key], sr] } + + records.each { |record| record[name] = sub_records_by_key[record[field_schema.origin_key_target]] } + end end end end diff --git a/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/datasource_customizer_spec.rb b/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/datasource_customizer_spec.rb index f9623b2b8..d91299183 100644 --- a/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/datasource_customizer_spec.rb +++ b/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/datasource_customizer_spec.rb @@ -161,7 +161,7 @@ def schema datasource = instance_double(ForestAdminDatasourceToolkit::Datasource, schema: { charts: [] }, render_chart: nil, - collections: [], + collections: {}, live_query_connections: {}) customizer = described_class.new diff --git a/packages/forest_admin_datasource_mongoid/lib/forest_admin_datasource_mongoid/parser/relation.rb b/packages/forest_admin_datasource_mongoid/lib/forest_admin_datasource_mongoid/parser/relation.rb index 75ea689fa..3c33341f0 100644 --- a/packages/forest_admin_datasource_mongoid/lib/forest_admin_datasource_mongoid/parser/relation.rb +++ b/packages/forest_admin_datasource_mongoid/lib/forest_admin_datasource_mongoid/parser/relation.rb @@ -4,10 +4,12 @@ module Relation def get_polymorphic_types(relation_name) types = {} - ObjectSpace.each_object(Class).select { |klass| klass < Mongoid::Document }.each do |model| - if model.relations.any? { |_, relation| relation.options[:as] == relation_name.to_sym } - primary_key = model.fields.keys.find { |key| model.fields[key].options[:as] == :id } || :_id - types[model.name.gsub('::', '__')] = primary_key.to_s + ObjectSpace.each_object(Class).each do |klass| + next unless klass < Mongoid::Document + + if klass.relations.any? { |_, relation| relation.options[:as] == relation_name.to_sym } + primary_key = klass.fields.keys.find { |key| klass.fields[key].options[:as] == :id } || :_id + types[klass.name.gsub('::', '__')] = primary_key.to_s end end diff --git a/packages/forest_admin_datasource_mongoid/lib/forest_admin_datasource_mongoid/parser/validation.rb b/packages/forest_admin_datasource_mongoid/lib/forest_admin_datasource_mongoid/parser/validation.rb index 10e3aa302..81203fd45 100644 --- a/packages/forest_admin_datasource_mongoid/lib/forest_admin_datasource_mongoid/parser/validation.rb +++ b/packages/forest_admin_datasource_mongoid/lib/forest_admin_datasource_mongoid/parser/validation.rb @@ -4,33 +4,35 @@ module Validation include ForestAdminDatasourceToolkit::Components::Query::ConditionTree def get_validations(model, column) return [] if column.is_a?(Hash) + return [] if before_validation_callback?(model) + return [] unless model._validators? && model._validators[column.name.to_sym].size.positive? - validations = [] - # NOTICE: Do not consider validations if a before_validation Active Records - # Callback is detected. + parse_column_validators(model._validators[column.name.to_sym], column) + end + + def before_validation_callback?(model) default_callback_excluded = [:normalize_changed_in_place_attributes] - return validations if model._validation_callbacks - .reject { |callback| default_callback_excluded.include?(callback.filter) } - .map(&:kind).include?(:before) + model._validation_callbacks.any? do |callback| + !default_callback_excluded.include?(callback.filter) && callback.kind == :before + end + end - if model._validators? && model._validators[column.name.to_sym].size.positive? - model._validators[column.name.to_sym].each do |validator| - # NOTICE: Do not consider conditional validations - next if validator.options[:if] || validator.options[:unless] || validator.options[:on] + def parse_column_validators(validators, column) + validations = [] + validators.each do |validator| + next if validator.options[:if] || validator.options[:unless] || validator.options[:on] - case validator.class.to_s - when Mongoid::Validatable::PresenceValidator.to_s - validations << { operator: Operators::PRESENT } - when ActiveModel::Validations::NumericalityValidator.to_s - validations = parse_numericality_validator(validator, validations) - when Mongoid::Validatable::LengthValidator.to_s - validations = parse_length_validator(validator, validations, column) - when Mongoid::Validatable::FormatValidator.to_s - validations = parse_format_validator(validator, validations) - end + case validator.class.to_s + when Mongoid::Validatable::PresenceValidator.to_s + validations << { operator: Operators::PRESENT } + when ActiveModel::Validations::NumericalityValidator.to_s + validations = parse_numericality_validator(validator, validations) + when Mongoid::Validatable::LengthValidator.to_s + validations = parse_length_validator(validator, validations, column) + when Mongoid::Validatable::FormatValidator.to_s + validations = parse_format_validator(validator, validations) end end - validations end diff --git a/packages/forest_admin_datasource_mongoid/lib/forest_admin_datasource_mongoid/utils/add_null_values.rb b/packages/forest_admin_datasource_mongoid/lib/forest_admin_datasource_mongoid/utils/add_null_values.rb index 878e0dade..8bf5244d9 100644 --- a/packages/forest_admin_datasource_mongoid/lib/forest_admin_datasource_mongoid/utils/add_null_values.rb +++ b/packages/forest_admin_datasource_mongoid/lib/forest_admin_datasource_mongoid/utils/add_null_values.rb @@ -28,11 +28,13 @@ def add_null_values_on_record(record, projection) result[field_prefix] ||= nil end - nested_prefixes = projection.select { |field| field.include?(':') }.map { |field| field.split(':').first }.uniq + nested_prefixes = projection.filter_map { |field| field.split(':').first if field.include?(':') }.uniq nested_prefixes.each do |nested_prefix| - child_paths = projection.filter { |field| field.start_with?("#{nested_prefix}:") } - .map { |field| field[(nested_prefix.size + 1)..] } + prefix_with_colon = "#{nested_prefix}:" + child_paths = projection.filter_map do |field| + field[(nested_prefix.size + 1)..] if field.start_with?(prefix_with_colon) + end next unless result[nested_prefix] && !result[nested_prefix].nil? diff --git a/packages/forest_admin_datasource_mongoid/lib/forest_admin_datasource_mongoid/utils/helpers.rb b/packages/forest_admin_datasource_mongoid/lib/forest_admin_datasource_mongoid/utils/helpers.rb index 2b9043049..779c1e70e 100644 --- a/packages/forest_admin_datasource_mongoid/lib/forest_admin_datasource_mongoid/utils/helpers.rb +++ b/packages/forest_admin_datasource_mongoid/lib/forest_admin_datasource_mongoid/utils/helpers.rb @@ -5,7 +5,8 @@ module Helpers # @example # unnest(['firstname', 'book.title', 'book.author'], 'book') == ['title', 'author'] def unnest(strings, prefix) - strings.select { |field| field.start_with?("#{prefix}.") }.map { |field| field[(prefix.size + 1)..] } + prefix_with_dot = "#{prefix}." + strings.filter_map { |field| field[(prefix.size + 1)..] if field.start_with?(prefix_with_dot) } end def escape(str) diff --git a/packages/forest_admin_datasource_mongoid/lib/forest_admin_datasource_mongoid/utils/pipeline/filter_generator.rb b/packages/forest_admin_datasource_mongoid/lib/forest_admin_datasource_mongoid/utils/pipeline/filter_generator.rb index 171306ca1..649942b33 100644 --- a/packages/forest_admin_datasource_mongoid/lib/forest_admin_datasource_mongoid/utils/pipeline/filter_generator.rb +++ b/packages/forest_admin_datasource_mongoid/lib/forest_admin_datasource_mongoid/utils/pipeline/filter_generator.rb @@ -76,7 +76,7 @@ def self.list_fields_used_in_filter_tree(condition_tree, fields) if condition_tree.is_a? Nodes::ConditionTreeBranch condition_tree.conditions.each { |condition| list_fields_used_in_filter_tree(condition, fields) } elsif condition_tree&.field&.include?(':') - list_paths(condition_tree.field).each { |field| fields << field } + fields.merge(list_paths(condition_tree.field)) end end diff --git a/packages/forest_admin_datasource_mongoid/lib/forest_admin_datasource_mongoid/utils/pipeline/lookup_generator.rb b/packages/forest_admin_datasource_mongoid/lib/forest_admin_datasource_mongoid/utils/pipeline/lookup_generator.rb index be9620643..d5db7390b 100644 --- a/packages/forest_admin_datasource_mongoid/lib/forest_admin_datasource_mongoid/utils/pipeline/lookup_generator.rb +++ b/packages/forest_admin_datasource_mongoid/lib/forest_admin_datasource_mongoid/utils/pipeline/lookup_generator.rb @@ -35,23 +35,25 @@ def self.add_fields(name, projection, options) return {} if options[:include] && !options[:include].include?(name) return {} if options[:exclude]&.include?(name) - projection.filter { |field| field.include?('@@@') } - .map { |field_name| "#{name}.#{field_name.tr(":", ".")}" } - .each_with_object({}) do |curr, acc| - acc[curr] = "$#{curr.gsub("@@@", ".")}" - end + projection.each_with_object({}) do |field_name, acc| + next unless field_name.include?('@@@') + + curr = "#{name}.#{field_name.tr(":", ".")}" + acc[curr] = "$#{curr.gsub("@@@", ".")}" + end end def self.lookup_relation(current_path, schema_stack, name, projection, options) - models = ObjectSpace - .each_object(Class) - .select { |klass| klass < Mongoid::Document && klass.name && !klass.name.start_with?('Mongoid::') } - .to_h { |klass| [klass.name, klass] } + models = ObjectSpace.each_object(Class).with_object({}) do |klass, hash| + next unless klass < Mongoid::Document && klass.name && !klass.name.start_with?('Mongoid::') + + hash[klass.name] = klass + end as = current_path ? "#{current_path}.#{name}" : name - last_schema = schema_stack[schema_stack.length - 1] - previous_schema = schema_stack.slice(0..(schema_stack.length - 1)) + last_schema = schema_stack.last + previous_schema = schema_stack return {} if options[:include] && !options[:include].include?(as) return {} if options[:exclude]&.include?(as) diff --git a/packages/forest_admin_datasource_mongoid/spec/spec_helper.rb b/packages/forest_admin_datasource_mongoid/spec/spec_helper.rb index d22ba39b3..830b194fe 100644 --- a/packages/forest_admin_datasource_mongoid/spec/spec_helper.rb +++ b/packages/forest_admin_datasource_mongoid/spec/spec_helper.rb @@ -32,7 +32,7 @@ models_to_load = Dir.glob(File.join('spec', 'dummy', 'app', 'models', '**', '*.rb')) .collect { |file_path| File.basename(file_path, '.rb').camelize.constantize } - allow(ObjectSpace).to receive(:each_object).and_return(models_to_load) + allow(ObjectSpace).to receive(:each_object).and_return(models_to_load.each) end config.before(:suite) do