From 889080d865341244924674e9cde710b4079dc94c Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 6 Mar 2024 17:46:46 +0100 Subject: [PATCH 1/8] feat(sort): add sort decorator --- .../collection_customizer.rb | 34 +++ .../decorators/decorators_stack.rb | 3 +- .../sort/sort_collection_decorator.rb | 201 ++++++++++++++++++ 3 files changed, 237 insertions(+), 1 deletion(-) create mode 100644 packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/sort/sort_collection_decorator.rb diff --git a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/collection_customizer.rb b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/collection_customizer.rb index d24cae173..d13233791 100644 --- a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/collection_customizer.rb +++ b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/collection_customizer.rb @@ -155,6 +155,40 @@ def add_field_validation(name, operator, value = nil) end end + # emulateFieldSorting(name: TColumnName): this { + # return this.pushCustomization(async () => { + # this.stack.sortEmulate.getCollection(this.name).emulateFieldSorting(name); + # }); + # } + # Enable sorting on a specific field using emulation. + # As for all the emulation method, the field sorting will be done in-memory. + # @param name the name of the field to enable emulation on + # @example + # .emulate_field_sorting('fullName') + def emulate_field_sorting(name) + push_customization( + proc { @stack.sort.get_collection(@name).emulate_field_sorting(name) } + ) + end + + # Replace an implementation for the sorting. + # The field sorting will be done by the datasource. + # @param name the name of the field to enable sort + # @param equivalentSort the sort equivalent + # @example + # .replace_field_sorting( + # 'fullName', + # [ + # { field: 'firstName', ascending: true }, + # { field: 'lastName', ascending: true }, + # ] + # ) + def replace_field_sorting(name, equivalent_sort) + push_customization( + proc { @stack.sort.get_collection(@name).replace_field_sorting(name, equivalent_sort) } + ) + end + private def push_customization(&customization) diff --git a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/decorators_stack.rb b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/decorators_stack.rb index 41b6c989f..0536c7ff5 100644 --- a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/decorators_stack.rb +++ b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/decorators_stack.rb @@ -4,7 +4,7 @@ class DecoratorsStack include ForestAdminDatasourceToolkit::Decorators attr_reader :datasource, :schema, :search, :early_computed, :late_computed, :action, :relation, :late_op_emulate, - :early_op_emulate, :validation + :early_op_emulate, :validation,:sort def initialize(datasource) @customizations = [] @@ -22,6 +22,7 @@ def initialize(datasource) last = DatasourceDecorator.new(last, OperatorsEquivalence::OperatorsEquivalenceCollectionDecorator) last = @search = DatasourceDecorator.new(last, Search::SearchCollectionDecorator) + last = @sort = DatasourceDecorator.new(last, Sort::SortCollectionDecorator) last = @action = DatasourceDecorator.new(last, Action::ActionCollectionDecorator) last = @schema = DatasourceDecorator.new(last, Schema::SchemaCollectionDecorator) last = @validation = DatasourceDecorator.new(last, Validation::ValidationCollectionDecorator) diff --git a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/sort/sort_collection_decorator.rb b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/sort/sort_collection_decorator.rb new file mode 100644 index 000000000..343237cd0 --- /dev/null +++ b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/sort/sort_collection_decorator.rb @@ -0,0 +1,201 @@ +module ForestAdminDatasourceCustomizer + module Decorators + module Sort + class SortCollectionDecorator < ForestAdminDatasourceToolkit::Decorators::CollectionDecorator + include ForestAdminDatasourceToolkit::Exceptions + include ForestAdminDatasourceToolkit::Validations + include ForestAdminDatasourceToolkit::Components::Query + include ForestAdminDatasourceToolkit::Utils + + attr_reader :sorts + + def initialize(child_collection, datasource) + super + @sorts = {} + end + + def emulate_field_sorting(name) + replace_or_emulate_field_sorting(name, nil) + end + + def replace_field_sorting(name, equivalent_sort) + if equivalent_sort.nil? + raise ForestException, 'A new sorting method should be provided to replace field sorting' + end + + replace_or_emulate_field_sorting(name, equivalent_sort) + end + + def list(caller, filter = nil, projection = nil) + child_filter = filter.override(sort: filter.sort&.replace_clauses do |clause| + rewrite_plain_sort_clause(clause) + end) + + if child_filter.sort&.none? { |field| emulated?(field) } + return child_collection.list(caller, child_filter, projection) + end + + # Fetch the whole collection, but only with the fields we need to sort + reference_records = child_collection.list(caller, child_filter.override(sort: nil, page: nil), + child_filter.sort.projection.with_pks(self)) + reference_records = child_filter.sort.apply(reference_records) + reference_records = child_filter.page.apply(reference_records) if child_filter.page + + # We now have the information we need to sort by the field + new_filter = Filter.new(condition_tree: ConditionTree::ConditionTreeFactory.match_records(schema, + reference_records)) + + records = child_collection.list(caller, new_filter, projection.with_pks(self)) + records = sort_records(reference_records, records) + + projection.apply(records) + end + + def refine_schema(child_schema) + # const fields: Record = {}; + # + # for (const [name, schema] of Object.entries(childSchema.fields)) { + # if (schema.type === 'Column') { + # let sortable = schema.isSortable; + # + # if (this.disabledSorts.has(name)) { + # // disableFieldSorting + # sortable = false; + # } else if (this.sorts.has(name)) { + # // replaceFieldSorting + # sortable = true; + # } + # + # fields[name] = { ...schema, isSortable: sortable }; + # } else { + # fields[name] = schema; + # } + # } + # + # return { ...childSchema, fields }; + fields = {} + + child_schema[:fields].each do |name, schema| + if schema.type == 'Column' + sortable = schema.is_sortable + sortable = true unless @sorts[name].nil? + fields[name] = schema.merge(is_sortable: sortable) + else + fields[name] = schema + end + end + + child_schema.merge(fields: fields) + end + + private + + def replace_or_emulate_field_sorting(name, equivalent_sort) + FieldValidator.validate(self, name) + @sorts[name] = equivalent_sort.nil? ? nil : Sort.new(equivalent_sort) ## ARRAY + mark_schema_as_dirty + end + + # private sortRecords(referenceRecords: RecordData[], records: RecordData[]): RecordData[] { + # const positionById: Record = {}; + # const sorted = new Array(records.length); + # + # for (const [index, record] of referenceRecords.entries()) { + # positionById[RecordUtils.getPrimaryKey(this.schema, record).join('|')] = index; + # } + # + # for (const record of records) { + # const id = RecordUtils.getPrimaryKey(this.schema, record).join('|'); + # sorted[positionById[id]] = record; + # } + # + # return sorted; + # } + def sort_records(reference_records, records) + position_by_id = {} + sorted = Array.new(records.length) + + reference_records.each_with_index do |record, index| + position_by_id[Record.primary_keys(schema, record).join('|')] = index + end + + records.each do |record| + id = Record.primary_keys(schema, record).join('|') + sorted[position_by_id[id]] = record + end + + sorted + end + + # private rewritePlainSortClause(clause: PlainSortClause): Sort { + # // Order by is targeting a field on another collection => recurse. + # if (clause.field.includes(':')) { + # const [prefix] = clause.field.split(':'); + # const schema = this.schema.fields[prefix] as RelationSchema; + # const association = this.dataSource.getCollection(schema.foreignCollection); + # + # return new Sort(clause) + # .unnest() + # .replaceClauses(subClause => association.rewritePlainSortClause(subClause)) + # .nest(prefix); + # } + # + # // Field that we own: recursively replace using equivalent sort + # let equivalentSort = this.sorts.get(clause.field); + # + # if (equivalentSort) { + # if (!clause.ascending) equivalentSort = equivalentSort.inverse(); + # + # return equivalentSort.replaceClauses(subClause => this.rewritePlainSortClause(subClause)); + # } + # + # return new Sort(clause); + # } + + def rewrite_plain_sort_clause(clause) + # Order by is targeting a field on another collection => recurse. + if clause[:field].include?(':') + prefix = clause[:field].split(':')[0] + schema = self.schema[:fields][prefix] + association = datasource.get_collection(schema.foreign_collection) + + return Sort.new(clause) + .unnest + .replace_clauses { |sub_clause| association.rewrite_plain_sort_clause(sub_clause) } + .nest(prefix) + end + + # Field that we own: recursively replace using equivalent sort + equivalent_sort = @sorts[clause[:field]] + + if equivalent_sort + equivalent_sort = equivalent_sort.inverse unless clause[:ascending] + + return equivalent_sort.replace_clauses { |sub_clause| rewrite_plain_sort_clause(sub_clause) } + end + + Sort.new(clause) + end + + # private isEmulated(path: string): boolean { + # const index = path.indexOf(':'); + # if (index === -1) return this.sorts.has(path); + # + # const { foreignCollection } = this.schema.fields[path.substring(0, index)] as RelationSchema; + # const association = this.dataSource.getCollection(foreignCollection); + # + # return association.isEmulated(path.substring(index + 1)); + # } + def emulated?(path) + index = path.index(':') + return @sorts.key?(path) if index.nil? + + foreign_collection = schema[:fields][path[0, index]][:foreign_collection] + association = datasource.get_collection(foreign_collection) + + association.emulated?(path[index + 1, path.length - index - 1]) + end + end + end + end +end From ff460180a1062a4f4ad9bc0abfd0bce23aadf51a Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 11 Mar 2024 16:47:11 +0100 Subject: [PATCH 2/8] chore: add method apply on page class --- .../forest_admin_datasource_toolkit/components/query/page.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/query/page.rb b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/query/page.rb index 24051f3de..85ef60865 100644 --- a/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/query/page.rb +++ b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/query/page.rb @@ -8,6 +8,11 @@ def initialize(offset:, limit:) @offset = offset @limit = limit end + + def apply(records) + end_index = @limit ? @offset + @limit : nil + records[@offset...end_index] + end end end end From 40fd5c6e014929d1c213af26a13136969583c0a9 Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 11 Mar 2024 17:55:13 +0100 Subject: [PATCH 3/8] fix: sort decorator --- .rubocop.yml | 1 + .../sort/sort_collection_decorator.rb | 146 +++++------------- 2 files changed, 37 insertions(+), 110 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 4563d7cf8..e72cb2ff9 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -262,6 +262,7 @@ Layout/LineLength: - 'packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/list.rb' - 'packages/forest_admin_agent/lib/forest_admin_agent/services/permissions.rb' - 'packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/relation/relation_collection_decorator.rb' + - 'packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/sort/sort_collection_decorator.rb' - 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/query/condition_tree/condition_tree_factory.rb' - 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/utils/collection.rb' - 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/query/filter_factory.rb' diff --git a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/sort/sort_collection_decorator.rb b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/sort/sort_collection_decorator.rb index 343237cd0..4e1fc777b 100644 --- a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/sort/sort_collection_decorator.rb +++ b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/sort/sort_collection_decorator.rb @@ -31,7 +31,7 @@ def list(caller, filter = nil, projection = nil) rewrite_plain_sort_clause(clause) end) - if child_filter.sort&.none? { |field| emulated?(field) } + if child_filter.sort&.none? { |clause| emulated?(clause[:field]) } return child_collection.list(caller, child_filter, projection) end @@ -52,106 +52,16 @@ def list(caller, filter = nil, projection = nil) end def refine_schema(child_schema) - # const fields: Record = {}; - # - # for (const [name, schema] of Object.entries(childSchema.fields)) { - # if (schema.type === 'Column') { - # let sortable = schema.isSortable; - # - # if (this.disabledSorts.has(name)) { - # // disableFieldSorting - # sortable = false; - # } else if (this.sorts.has(name)) { - # // replaceFieldSorting - # sortable = true; - # } - # - # fields[name] = { ...schema, isSortable: sortable }; - # } else { - # fields[name] = schema; - # } - # } - # - # return { ...childSchema, fields }; - fields = {} - child_schema[:fields].each do |name, schema| if schema.type == 'Column' - sortable = schema.is_sortable - sortable = true unless @sorts[name].nil? - fields[name] = schema.merge(is_sortable: sortable) - else - fields[name] = schema + schema.is_sortable = true if @sorts[name].nil? + child_schema[:fields][name] = schema end end - child_schema.merge(fields: fields) - end - - private - - def replace_or_emulate_field_sorting(name, equivalent_sort) - FieldValidator.validate(self, name) - @sorts[name] = equivalent_sort.nil? ? nil : Sort.new(equivalent_sort) ## ARRAY - mark_schema_as_dirty - end - - # private sortRecords(referenceRecords: RecordData[], records: RecordData[]): RecordData[] { - # const positionById: Record = {}; - # const sorted = new Array(records.length); - # - # for (const [index, record] of referenceRecords.entries()) { - # positionById[RecordUtils.getPrimaryKey(this.schema, record).join('|')] = index; - # } - # - # for (const record of records) { - # const id = RecordUtils.getPrimaryKey(this.schema, record).join('|'); - # sorted[positionById[id]] = record; - # } - # - # return sorted; - # } - def sort_records(reference_records, records) - position_by_id = {} - sorted = Array.new(records.length) - - reference_records.each_with_index do |record, index| - position_by_id[Record.primary_keys(schema, record).join('|')] = index - end - - records.each do |record| - id = Record.primary_keys(schema, record).join('|') - sorted[position_by_id[id]] = record - end - - sorted + child_schema end - # private rewritePlainSortClause(clause: PlainSortClause): Sort { - # // Order by is targeting a field on another collection => recurse. - # if (clause.field.includes(':')) { - # const [prefix] = clause.field.split(':'); - # const schema = this.schema.fields[prefix] as RelationSchema; - # const association = this.dataSource.getCollection(schema.foreignCollection); - # - # return new Sort(clause) - # .unnest() - # .replaceClauses(subClause => association.rewritePlainSortClause(subClause)) - # .nest(prefix); - # } - # - # // Field that we own: recursively replace using equivalent sort - # let equivalentSort = this.sorts.get(clause.field); - # - # if (equivalentSort) { - # if (!clause.ascending) equivalentSort = equivalentSort.inverse(); - # - # return equivalentSort.replaceClauses(subClause => this.rewritePlainSortClause(subClause)); - # } - # - # return new Sort(clause); - # } - def rewrite_plain_sort_clause(clause) # Order by is targeting a field on another collection => recurse. if clause[:field].include?(':') @@ -159,10 +69,10 @@ def rewrite_plain_sort_clause(clause) schema = self.schema[:fields][prefix] association = datasource.get_collection(schema.foreign_collection) - return Sort.new(clause) - .unnest - .replace_clauses { |sub_clause| association.rewrite_plain_sort_clause(sub_clause) } - .nest(prefix) + return ForestAdminDatasourceToolkit::Components::Query::Sort.new([clause]) + .unnest + .replace_clauses { |sub_clause| association.rewrite_plain_sort_clause(sub_clause) } + .nest(prefix) end # Field that we own: recursively replace using equivalent sort @@ -174,27 +84,43 @@ def rewrite_plain_sort_clause(clause) return equivalent_sort.replace_clauses { |sub_clause| rewrite_plain_sort_clause(sub_clause) } end - Sort.new(clause) + ForestAdminDatasourceToolkit::Components::Query::Sort.new([clause]) end - # private isEmulated(path: string): boolean { - # const index = path.indexOf(':'); - # if (index === -1) return this.sorts.has(path); - # - # const { foreignCollection } = this.schema.fields[path.substring(0, index)] as RelationSchema; - # const association = this.dataSource.getCollection(foreignCollection); - # - # return association.isEmulated(path.substring(index + 1)); - # } def emulated?(path) index = path.index(':') - return @sorts.key?(path) if index.nil? + return @sorts[path] if index.nil? - foreign_collection = schema[:fields][path[0, index]][:foreign_collection] + foreign_collection = schema[:fields][path[0, index]].foreign_collection association = datasource.get_collection(foreign_collection) association.emulated?(path[index + 1, path.length - index - 1]) end + + private + + def replace_or_emulate_field_sorting(name, equivalent_sort) + FieldValidator.validate(self, name) + @sorts[name] = + equivalent_sort ? ForestAdminDatasourceToolkit::Components::Query::Sort.new(equivalent_sort) : nil + mark_schema_as_dirty + end + + def sort_records(reference_records, records) + position_by_id = {} + sorted = Array.new(records.length) + + reference_records.each_with_index do |record, index| + position_by_id[Record.primary_keys(schema, record).join('|')] = index + end + + records.each do |record| + id = Record.primary_keys(schema, record).join('|') + sorted[position_by_id[id]] = record + end + + sorted + end end end end From 554fcb324255f5df9ecd11b620dc7af943a1b37c Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 12 Mar 2024 16:11:45 +0100 Subject: [PATCH 4/8] refactor: collection customize --- .../collection_customizer.rb | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/collection_customizer.rb b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/collection_customizer.rb index d13233791..43cf41141 100644 --- a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/collection_customizer.rb +++ b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/collection_customizer.rb @@ -157,7 +157,7 @@ def add_field_validation(name, operator, value = nil) # emulateFieldSorting(name: TColumnName): this { # return this.pushCustomization(async () => { - # this.stack.sortEmulate.getCollection(this.name).emulateFieldSorting(name); + # this.stack.sort_emulate.get_collection(this.name).emulate_field_sorting(name); # }); # } # Enable sorting on a specific field using emulation. @@ -166,15 +166,13 @@ def add_field_validation(name, operator, value = nil) # @example # .emulate_field_sorting('fullName') def emulate_field_sorting(name) - push_customization( - proc { @stack.sort.get_collection(@name).emulate_field_sorting(name) } - ) + push_customization { @stack.sort.get_collection(@name).emulate_field_sorting(name) } end # Replace an implementation for the sorting. # The field sorting will be done by the datasource. # @param name the name of the field to enable sort - # @param equivalentSort the sort equivalent + # @param equivalent_sort the sort equivalent # @example # .replace_field_sorting( # 'fullName', @@ -184,9 +182,7 @@ def emulate_field_sorting(name) # ] # ) def replace_field_sorting(name, equivalent_sort) - push_customization( - proc { @stack.sort.get_collection(@name).replace_field_sorting(name, equivalent_sort) } - ) + push_customization { @stack.sort.get_collection(@name).replace_field_sorting(name, equivalent_sort) } end private From 018f78536ce7409e3678170911672c7f3691b0c2 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 12 Mar 2024 16:22:12 +0100 Subject: [PATCH 5/8] chore: add test on sort decorator & collection customizer --- .../decorators/decorators_stack.rb | 2 +- .../collection_customizer_spec.rb | 43 +++- .../sort/sort_collection_decorator_spec.rb | 232 ++++++++++++++++++ 3 files changed, 275 insertions(+), 2 deletions(-) create mode 100644 packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/decorators/sort/sort_collection_decorator_spec.rb diff --git a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/decorators_stack.rb b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/decorators_stack.rb index 0536c7ff5..b1ef52be3 100644 --- a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/decorators_stack.rb +++ b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/decorators_stack.rb @@ -4,7 +4,7 @@ class DecoratorsStack include ForestAdminDatasourceToolkit::Decorators attr_reader :datasource, :schema, :search, :early_computed, :late_computed, :action, :relation, :late_op_emulate, - :early_op_emulate, :validation,:sort + :early_op_emulate, :validation, :sort def initialize(datasource) @customizations = [] diff --git a/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/collection_customizer_spec.rb b/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/collection_customizer_spec.rb index 4ea0412ac..7c41a1e7d 100644 --- a/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/collection_customizer_spec.rb +++ b/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/collection_customizer_spec.rb @@ -67,7 +67,7 @@ module ForestAdminDatasourceCustomizer schema: { fields: { 'id' => ColumnSchema.new(column_type: 'Number', is_primary_key: true, filter_operators: [Operators::EQUAL, Operators::IN]), - 'name' => ColumnSchema.new(column_type: 'String'), + 'name' => ColumnSchema.new(column_type: 'String', is_sortable: true), 'name_in_read_only' => ColumnSchema.new(column_type: 'String', is_read_only: true), 'book' => Relations::OneToOneSchema.new( origin_key: 'author_id', @@ -266,6 +266,14 @@ module ForestAdminDatasourceCustomizer expect(relation_collection.relations['myBooks'].origin_key).to eq('person_id') expect(relation_collection.relations['myBooks'].origin_key_target).to eq('id') end + + it 'does not allow replaceFieldSorting' do + customizer = described_class.new(@datasource_customizer, @datasource_customizer.stack, 'person') + customizer.add_one_to_one_relation('myBookAuthor', 'book_person', { origin_key: 'person_id', origin_key_target: 'id' }) + customizer.replace_field_sorting('myBookAuthor', []) + + expect { @datasource_customizer.datasource({}) }.to raise_error(Exceptions::ValidationError, "🌳🌳🌳 Unexpected field type: 'person.myBookAuthor' (found 'OneToOne' expected 'Column')") + end end context 'when adding external relation' do @@ -353,5 +361,38 @@ module ForestAdminDatasourceCustomizer expect(op_emulate_collection.fields['title']).to eq({ Operators::PRESENT => replacer }) end end + + context 'when using emulate_field_sorting' do + it 'emulate sort on field' do + stack = @datasource_customizer.stack + allow(stack.sort).to receive(:get_collection).with('person').and_return(@datasource_customizer.stack.sort.get_collection('person')) + + customizer = described_class.new(@datasource_customizer, @datasource_customizer.stack, 'person') + customizer.emulate_field_sorting('name') + @datasource_customizer.datasource({}) + + sort_collection = @datasource_customizer.stack.sort.get_collection('person') + + expect(sort_collection.sorts).to have_key('name') + expect(sort_collection.emulated?('name')).to be_nil + end + end + + context 'when using replace_field_sorting' do + it 'replace sort on field' do + stack = @datasource_customizer.stack + allow(stack.sort).to receive(:get_collection).with('person').and_return(@datasource_customizer.stack.sort.get_collection('person')) + + customizer = described_class.new(@datasource_customizer, @datasource_customizer.stack, 'person') + sort_clauses = [{ field: 'name', ascending: true }] + customizer.replace_field_sorting('name', sort_clauses) + @datasource_customizer.datasource({}) + + sort_collection = @datasource_customizer.stack.sort.get_collection('person') + + expect(sort_collection.sorts).to have_key('name') + expect(sort_collection.sorts['name']).to eq(ForestAdminDatasourceToolkit::Components::Query::Sort.new(sort_clauses)) + end + end end end diff --git a/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/decorators/sort/sort_collection_decorator_spec.rb b/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/decorators/sort/sort_collection_decorator_spec.rb new file mode 100644 index 000000000..63ba6a3b7 --- /dev/null +++ b/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/decorators/sort/sort_collection_decorator_spec.rb @@ -0,0 +1,232 @@ +require 'spec_helper' +require 'shared/caller' + +module ForestAdminDatasourceCustomizer + module Decorators + module Sort + include ForestAdminDatasourceToolkit + include ForestAdminDatasourceToolkit::Components::Query + include ForestAdminDatasourceToolkit::Components::Query::ConditionTree + include ForestAdminDatasourceToolkit::Decorators + include ForestAdminDatasourceToolkit::Schema + include ForestAdminDatasourceToolkit::Exceptions + + describe SortCollectionDecorator do + include_context 'with caller' + subject(:sort_collection_decorator) { described_class } + + let(:datasource) { ForestAdminDatasourceToolkit::Datasource.new } + let(:records) do + [ + { + 'id' => 1, + 'author_id' => 1, + 'author' => { 'id' => 1, 'first_name' => 'Isaac', 'last_name' => 'Asimov' }, + 'title' => 'Foundation' + }, + { + 'id' => 2, + 'author_id' => 2, + 'author' => { 'id' => 2, 'first_name' => 'Edward O.', 'last_name' => 'Thorp' }, + 'title' => 'Beat the dealer' + }, + { + 'id' => 3, + 'author_id' => 3, + 'author' => { 'id' => 3, 'first_name' => 'Roberto', 'last_name' => 'Saviano' }, + 'title' => 'Gomorrah' + } + ] + end + + before do + @collection_book = instance_double( + ForestAdminDatasourceToolkit::Collection, + name: 'book', + schema: { + fields: { + 'id' => ColumnSchema.new(column_type: 'Number', is_primary_key: true), + 'author_id' => ColumnSchema.new(column_type: 'String'), + 'author' => Relations::ManyToOneSchema.new( + foreign_key: 'author_id', + foreign_collection: 'person', + foreign_key_target: 'id' + ), + 'title' => ColumnSchema.new(column_type: 'String', is_sortable: false) + } + }, + datasource: datasource + ) + + @collection_person = instance_double( + ForestAdminDatasourceToolkit::Collection, + name: 'person', + schema: { + fields: { + 'id' => ColumnSchema.new(column_type: 'Number', is_primary_key: true), + 'first_name' => ColumnSchema.new(column_type: 'String'), + 'last_name' => ColumnSchema.new(column_type: 'String', is_sortable: false), + 'book' => Relations::OneToOneSchema.new( + origin_key: 'author_id', + origin_key_target: 'id', + foreign_collection: 'book' + ) + } + }, + datasource: datasource + ) + + allow(@collection_book).to receive(:list) do |_caller, filter, projection| + rows = records + rows = filter.condition_tree.apply(rows, @collection_book, 'Europe/Paris') if filter.condition_tree + rows = filter.sort.apply(rows) if filter.sort + rows = filter.page.apply(rows) if filter.page + + projection.apply(rows) + end + + datasource.add_collection(@collection_book) + datasource.add_collection(@collection_person) + + datasource_decorator = ForestAdminDatasourceToolkit::Decorators::DatasourceDecorator.new(datasource, sort_collection_decorator) + + @decorated_book = datasource_decorator.get_collection('book') + @decorated_person = datasource_decorator.get_collection('person') + end + + it 'emulate_field_sorting() should throw if the field does not exists' do + expect { @decorated_book.emulate_field_sorting('__dontExist') }.to raise_error(ValidationError, "🌳🌳🌳 Column not found: 'book.__dontExist'") + end + + it 'emulate_field_sorting() should throw if the field is not sortable' do + expect { @decorated_book.emulate_field_sorting('author') }.to raise_error(ValidationError, "🌳🌳🌳 Unexpected field type: 'book.author' (found 'ManyToOne' expected 'Column')") + end + + it 'replace_field_sorting() should throw if no equivalent_sort is provided' do + expect { @decorated_book.replace_field_sorting('author_id', nil) }.to raise_error(ForestException, '🌳🌳🌳 A new sorting method should be provided to replace field sorting') + end + + context 'when emulating sort on book.title (no relations)' do + before do + @decorated_book.emulate_field_sorting('title') + end + + it 'schema should be updated' do + schema = @decorated_book.schema[:fields]['title'] + expect(schema.is_sortable).to be_truthy + end + + it 'works in ascending order' do + records = @decorated_book.list( + caller, + Filter.new(sort: ForestAdminDatasourceToolkit::Components::Query::Sort.new([{ field: 'title', ascending: true }])), + Projection.new(%w[id title]) + ) + expect(records).to eq([ + { 'id' => 2, 'title' => 'Beat the dealer' }, + { 'id' => 1, 'title' => 'Foundation' }, + { 'id' => 3, 'title' => 'Gomorrah' } + ]) + end + + it 'works in descending order' do + records = @decorated_book.list( + caller, + Filter.new(sort: ForestAdminDatasourceToolkit::Components::Query::Sort.new([{ field: 'title', ascending: false }])), + Projection.new(%w[id title]) + ) + expect(records).to eq([ + { 'id' => 3, 'title' => 'Gomorrah' }, + { 'id' => 1, 'title' => 'Foundation' }, + { 'id' => 2, 'title' => 'Beat the dealer' } + ]) + end + + it 'works with pagination' do + records = @decorated_book.list( + caller, + Filter.new(page: Page.new(offset: 2, limit: 1), sort: ForestAdminDatasourceToolkit::Components::Query::Sort.new([{ field: 'title', ascending: false }])), + Projection.new(%w[id title]) + ) + expect(records).to eq([{ 'id' => 2, 'title' => 'Beat the dealer' }]) + end + end + + context 'when emulating sort on book.author.last_name (relation)' do + before do + @decorated_person.emulate_field_sorting('last_name') + end + + it 'schema should be updated' do + schema = @decorated_person.schema[:fields]['last_name'] + expect(schema.is_sortable).to be_truthy + end + + it 'works in ascending order' do + records = @decorated_book.list( + caller, + Filter.new(sort: ForestAdminDatasourceToolkit::Components::Query::Sort.new([{ field: 'author:last_name', ascending: true }])), + Projection.new(%w[id title author:last_name]) + ) + expect(records).to eq([ + { 'id' => 1, 'title' => 'Foundation', 'author' => { 'last_name' => 'Asimov' } }, + { 'id' => 3, 'title' => 'Gomorrah', 'author' => { 'last_name' => 'Saviano' } }, + { 'id' => 2, 'title' => 'Beat the dealer', 'author' => { 'last_name' => 'Thorp' } } + ]) + end + + it 'works in descending order' do + records = @decorated_book.list( + caller, + Filter.new(sort: ForestAdminDatasourceToolkit::Components::Query::Sort.new([{ field: 'author:last_name', ascending: false }])), + Projection.new(%w[id title author:last_name]) + ) + + expect(records).to eq([ + { 'id' => 2, 'title' => 'Beat the dealer', 'author' => { 'last_name' => 'Thorp' } }, + { 'id' => 3, 'title' => 'Gomorrah', 'author' => { 'last_name' => 'Saviano' } }, + { 'id' => 1, 'title' => 'Foundation', 'author' => { 'last_name' => 'Asimov' } } + ]) + end + end + + context 'when telling that sort(book.title) = sort(book.author.last_name)' do + before do + @decorated_book.replace_field_sorting('title', ForestAdminDatasourceToolkit::Components::Query::Sort.new([{ field: 'author:last_name', ascending: true }])) + end + + it 'schema should be updated' do + schema = @decorated_book.schema[:fields]['title'] + expect(schema.is_sortable).to be_truthy + end + + it 'works in ascending order' do + records = @decorated_book.list( + caller, + Filter.new(sort: ForestAdminDatasourceToolkit::Components::Query::Sort.new([{ field: 'title', ascending: true }])), + Projection.new(%w[id title author:last_name]) + ) + expect(records).to eq([ + { 'id' => 1, 'title' => 'Foundation', 'author' => { 'last_name' => 'Asimov' } }, + { 'id' => 3, 'title' => 'Gomorrah', 'author' => { 'last_name' => 'Saviano' } }, + { 'id' => 2, 'title' => 'Beat the dealer', 'author' => { 'last_name' => 'Thorp' } } + ]) + end + + it 'works in descending order' do + records = @decorated_book.list( + caller, + Filter.new(sort: ForestAdminDatasourceToolkit::Components::Query::Sort.new([{ field: 'title', ascending: false }])), + Projection.new(%w[id title author:last_name]) + ) + expect(records).to eq([ + { 'id' => 2, 'title' => 'Beat the dealer', 'author' => { 'last_name' => 'Thorp' } }, + { 'id' => 3, 'title' => 'Gomorrah', 'author' => { 'last_name' => 'Saviano' } }, + { 'id' => 1, 'title' => 'Foundation', 'author' => { 'last_name' => 'Asimov' } } + ]) + end + end + end + end + end +end From 51ddbfa539f6a19fd5dacfb50b441a080ef7e445 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 12 Mar 2024 16:25:32 +0100 Subject: [PATCH 6/8] fix: tests --- .../decorators/relation/relation_collection_decorator_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/decorators/relation/relation_collection_decorator_spec.rb b/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/decorators/relation/relation_collection_decorator_spec.rb index 25fb73b35..652b0e7c9 100644 --- a/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/decorators/relation/relation_collection_decorator_spec.rb +++ b/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/decorators/relation/relation_collection_decorator_spec.rb @@ -561,13 +561,13 @@ module Relation it 'replaces sorts in emulated many to one into sort by fk' do ascending = @datasource_decorator.get_collection('passport').list( caller, - Filter.new(sort: Sort.new([{ field: 'owner:name', ascending: true }])), + Filter.new(sort: ForestAdminDatasourceToolkit::Components::Query::Sort.new([{ field: 'owner:name', ascending: true }])), Projection.new(%w[id owner_id owner:name]) ) descending = @datasource_decorator.get_collection('passport').list( caller, - Filter.new(sort: Sort.new([{ field: 'owner:name', ascending: false }])), + Filter.new(sort: ForestAdminDatasourceToolkit::Components::Query::Sort.new([{ field: 'owner:name', ascending: false }])), Projection.new(%w[id owner_id owner:name]) ) From 07c3918c420f3226085ecf133a78212f9cf3ba46 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 12 Mar 2024 16:49:21 +0100 Subject: [PATCH 7/8] fix: tests --- .../decorators/sort/sort_collection_decorator.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/sort/sort_collection_decorator.rb b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/sort/sort_collection_decorator.rb index 4e1fc777b..9f57afd53 100644 --- a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/sort/sort_collection_decorator.rb +++ b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/sort/sort_collection_decorator.rb @@ -31,7 +31,7 @@ def list(caller, filter = nil, projection = nil) rewrite_plain_sort_clause(clause) end) - if child_filter.sort&.none? { |clause| emulated?(clause[:field]) } + if child_filter.sort.nil? || child_filter.sort.none? { |clause| emulated?(clause[:field]) } return child_collection.list(caller, child_filter, projection) end From b3ea242e60e994554f0dcab69a1cc01328833cef Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 18 Mar 2024 14:04:03 +0100 Subject: [PATCH 8/8] chore: update comment --- .../collection_customizer.rb | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/collection_customizer.rb b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/collection_customizer.rb index 43cf41141..5f4072c60 100644 --- a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/collection_customizer.rb +++ b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/collection_customizer.rb @@ -155,11 +155,6 @@ def add_field_validation(name, operator, value = nil) end end - # emulateFieldSorting(name: TColumnName): this { - # return this.pushCustomization(async () => { - # this.stack.sort_emulate.get_collection(this.name).emulate_field_sorting(name); - # }); - # } # Enable sorting on a specific field using emulation. # As for all the emulation method, the field sorting will be done in-memory. # @param name the name of the field to enable emulation on