From 6c62d084a9e13a15397b99558d6c759a0229643a Mon Sep 17 00:00:00 2001 From: Nicolas Alexandre Date: Mon, 16 Oct 2023 17:02:23 +0200 Subject: [PATCH 01/18] feat: add route post --- .../lib/forest_admin_agent/http/router.rb | 3 +- .../routes/resources/store.rb | 32 +++++++++++++++++++ .../collection.rb | 4 +++ .../contracts/collection_contract.rb | 2 +- 4 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/store.rb diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/http/router.rb b/packages/forest_admin_agent/lib/forest_admin_agent/http/router.rb index ed3d016c1..ce01b9b8d 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/http/router.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/http/router.rb @@ -10,7 +10,8 @@ def self.routes System::HealthCheck.new.routes, Security::Authentication.new.routes, Resources::List.new.routes, - Resources::Count.new.routes + Resources::Count.new.routes, + Resources::Store.new.routes ].inject(&:merge) end diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/store.rb b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/store.rb new file mode 100644 index 000000000..2e40294b6 --- /dev/null +++ b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/store.rb @@ -0,0 +1,32 @@ +require 'jsonapi-serializers' + +module ForestAdminAgent + module Routes + module Resources + class Store < AbstractAuthenticatedRoute + include ForestAdminAgent::Builder + def setup_routes + add_route('forest_create', 'post', '/:collection_name', ->(args) { handle_request(args) }) + + self + end + + def handle_request(args = {}) + build(args) + caller = ForestAdminAgent::Utils::QueryStringParser.parse_caller(args) + data = args[:params][:data][:attributes].permit(@collection.fields.keys).to_h + record = @collection.create(caller, data) + + { + name: args[:params]['collection_name'], + content: JSONAPI::Serializer.serialize( + record, + is_collection: false, + serializer: Serializer::ForestSerializer + ) + } + end + end + end + end +end diff --git a/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/collection.rb b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/collection.rb index a8135358e..e9728e0fe 100644 --- a/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/collection.rb +++ b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/collection.rb @@ -32,6 +32,10 @@ def aggregate(_caller, _filter, aggregation) ] end + def create(caller, data) + @model.create(data) + end + private def query_joins(projection) diff --git a/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/contracts/collection_contract.rb b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/contracts/collection_contract.rb index ba79cb9ec..06c8223c7 100644 --- a/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/contracts/collection_contract.rb +++ b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/contracts/collection_contract.rb @@ -22,7 +22,7 @@ def form raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'" end - def create + def create(caller, data) raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'" end From 78c7c50ccecaa294b21a517ec971d666fa34688a Mon Sep 17 00:00:00 2001 From: Nicolas Alexandre Date: Tue, 17 Oct 2023 17:44:47 +0200 Subject: [PATCH 02/18] feat: add show route --- .../http/Exceptions/not_found_error.rb | 11 ++++ .../lib/forest_admin_agent/http/router.rb | 1 + .../routes/resources/show.rb | 45 +++++++++++++++ .../lib/forest_admin_agent/utils/id.rb | 25 +++++++++ .../collection.rb | 36 ++++++------ .../utils/query.rb | 55 +++++++++++++++++++ 6 files changed, 154 insertions(+), 19 deletions(-) create mode 100644 packages/forest_admin_agent/lib/forest_admin_agent/http/Exceptions/not_found_error.rb create mode 100644 packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/show.rb create mode 100644 packages/forest_admin_agent/lib/forest_admin_agent/utils/id.rb create mode 100644 packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/query.rb diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/http/Exceptions/not_found_error.rb b/packages/forest_admin_agent/lib/forest_admin_agent/http/Exceptions/not_found_error.rb new file mode 100644 index 000000000..74c12db9b --- /dev/null +++ b/packages/forest_admin_agent/lib/forest_admin_agent/http/Exceptions/not_found_error.rb @@ -0,0 +1,11 @@ +module ForestAdminAgent + module Http + module Exceptions + class NotFoundError < StandardError + def initialize(msg, name = 'NotFoundError') + super msg + end + end + end + end +end diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/http/router.rb b/packages/forest_admin_agent/lib/forest_admin_agent/http/router.rb index ce01b9b8d..66c5e0d04 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/http/router.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/http/router.rb @@ -10,6 +10,7 @@ def self.routes System::HealthCheck.new.routes, Security::Authentication.new.routes, Resources::List.new.routes, + Resources::Show.new.routes, Resources::Count.new.routes, Resources::Store.new.routes ].inject(&:merge) diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/show.rb b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/show.rb new file mode 100644 index 000000000..637652d77 --- /dev/null +++ b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/show.rb @@ -0,0 +1,45 @@ +require 'jsonapi-serializers' +require 'ostruct' + +module ForestAdminAgent + module Routes + module Resources + class Show < AbstractAuthenticatedRoute + include ForestAdminAgent::Builder + include ForestAdminDatasourceToolkit::Components::Query + def setup_routes + add_route('forest_show', 'get', '/:collection_name/:id', ->(args) { handle_request(args) }) + + self + end + + def handle_request(args = {}) + build(args) + id = Utils::Id.unpack_id(@collection, args[:params]['id'], true) + caller = ForestAdminAgent::Utils::QueryStringParser.parse_caller(args) + condition_tree = OpenStruct.new(field: 'id' , operator: "EQUAL", value: id['id']) + #TODO: replace condition_tree by ConditionTreeFactory.matchIds(this.collection.schema, [id]), + filter = ForestAdminDatasourceToolkit::Components::Query::Filter.new( + condition_tree: condition_tree, + page: ForestAdminAgent::Utils::QueryStringParser.parse_pagination(args) + ) + projection = ProjectionFactory.all(@collection) + + records = @collection.list(caller, filter, projection) + + raise Http::Exceptions::NotFoundError.new 'Record does not exists' unless records.size > 0 + + { + name: args[:params]['collection_name'], + content: JSONAPI::Serializer.serialize( + records[0], + is_collection: false, + serializer: Serializer::ForestSerializer, + include: projection.relations.keys + ) + } + end + end + end + end +end diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/utils/id.rb b/packages/forest_admin_agent/lib/forest_admin_agent/utils/id.rb new file mode 100644 index 000000000..fa554bb4a --- /dev/null +++ b/packages/forest_admin_agent/lib/forest_admin_agent/utils/id.rb @@ -0,0 +1,25 @@ +module ForestAdminAgent + module Utils + class Id + include ForestAdminDatasourceToolkit::Utils + def self.unpack_id(collection, packed_id, with_key = false) + primary_keys = ForestAdminDatasourceToolkit::Utils::Schema.primary_keys(collection) + primary_key_values = packed_id.split('|') + if ((nb_pks = primary_keys.size) != (nb_values = primary_key_values.size)) + raise Exceptions::ForestException.new "Expected $primaryKeyNames a size of #{nb_pks} values, found #{nb_values}" + end + + result = primary_keys.map.with_index do |pk_name, index| + field = collection.fields[pk_name] + value = primary_key_values[index] + casted_value = field.column_type == 'Number' ? value.to_i : value + #TODO: call FieldValidator::validateValue($value, $field, $castedValue); + + [pk_name, casted_value] + end.to_h + + with_key ? result : result.values + end + end + end +end diff --git a/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/collection.rb b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/collection.rb index e9728e0fe..95bb57a5a 100644 --- a/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/collection.rb +++ b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/collection.rb @@ -15,10 +15,8 @@ def initialize(datasource, model) end def list(_caller, filter, projection) - query_joins(projection) - query_select(projection) - - @model.offset(filter.page.offset).limit(filter.page.limit).all + query = Utils::Query.new(model, projection, filter).build + query.offset(filter.page.offset).limit(filter.page.limit).all end def aggregate(_caller, _filter, aggregation) @@ -32,26 +30,26 @@ def aggregate(_caller, _filter, aggregation) ] end - def create(caller, data) + def create(_caller, data) @model.create(data) end private - def query_joins(projection) - @model.joins(projection.relations.keys.map(&:to_sym)) - end - - def query_select(projection) - query = projection.columns.join(', ') - - projection.relations.each do |relation, fields| - relation_table = datasource.collection(relation).model.table_name - fields.each { |field| query += ", #{relation_table}.#{field}" } - end - - @model.select(query) - end + # def query_joins(projection) + # @model.joins(projection.relations.keys.map(&:to_sym)) + # end + # + # def query_select(projection) + # query = projection.columns.join(', ') + # + # projection.relations.each do |relation, fields| + # relation_table = datasource.collection(relation).model.table_name + # fields.each { |field| query += ", #{relation_table}.#{field}" } + # end + # + # @model.select(query) + # end def fetch_fields @model.columns_hash.each do |column_name, column| diff --git a/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/query.rb b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/query.rb new file mode 100644 index 000000000..5fd60f946 --- /dev/null +++ b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/query.rb @@ -0,0 +1,55 @@ +module ForestAdminDatasourceActiveRecord + module Utils + class Query + def initialize(model, projection, filter) + @query = model + @projection = projection + @filter = filter + end + + def build + @query = select + @query = apply_filter + + return @query + end + + def apply_filter + @query = apply_condition_tree(@filter.condition_tree) unless @filter.condition_tree.nil? + + return @query + end + + def apply_condition_tree(condition_tree, aggregator = nil) + # if condition_tree.is_a ConditionTreeBranch + #TODO: add for ConditionTreeBranch + # else + compute_main_operator(condition_tree, aggregator || 'and') + # end + end + + def compute_main_operator(condition_tree, aggregator) + field = condition_tree.field + value = condition_tree.value + case condition_tree.operator + when 'EQUAL' + @query = @query.send(aggregator, @query.where({ field => value })) + end + + return @query + end + + def select + query_select = @projection.columns.join(', ') + + @projection.relations.each do |relation, fields| + relation_table = datasource.collection(relation).model.table_name + fields.each { |field| query += ", #{relation_table}.#{field}" } + end + + @query = @query.select(query_select) + @query = @query.joins(@projection.relations.keys.map(&:to_sym)) + end + end + end +end From 8cb7ee870ea8e727c63ee3310228c22a49034d66 Mon Sep 17 00:00:00 2001 From: Nicolas Alexandre Date: Tue, 17 Oct 2023 17:50:34 +0200 Subject: [PATCH 03/18] chore: lint --- .../lib/forest_admin_agent/routes/resources/show.rb | 8 ++++---- .../lib/forest_admin_agent/utils/id.rb | 8 ++++---- .../utils/query.rb | 12 ++++++------ 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/show.rb b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/show.rb index 637652d77..c41db08d1 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/show.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/show.rb @@ -15,10 +15,10 @@ def setup_routes def handle_request(args = {}) build(args) - id = Utils::Id.unpack_id(@collection, args[:params]['id'], true) + id = Utils::Id.unpack_id(@collection, args[:params]['id'], with_key: true) caller = ForestAdminAgent::Utils::QueryStringParser.parse_caller(args) - condition_tree = OpenStruct.new(field: 'id' , operator: "EQUAL", value: id['id']) - #TODO: replace condition_tree by ConditionTreeFactory.matchIds(this.collection.schema, [id]), + condition_tree = OpenStruct.new(field: 'id', operator: 'EQUAL', value: id['id']) + # TODO: replace condition_tree by ConditionTreeFactory.matchIds(this.collection.schema, [id]), filter = ForestAdminDatasourceToolkit::Components::Query::Filter.new( condition_tree: condition_tree, page: ForestAdminAgent::Utils::QueryStringParser.parse_pagination(args) @@ -27,7 +27,7 @@ def handle_request(args = {}) records = @collection.list(caller, filter, projection) - raise Http::Exceptions::NotFoundError.new 'Record does not exists' unless records.size > 0 + raise Http::Exceptions::NotFoundError, 'Record does not exists' unless records.size.positive? { name: args[:params]['collection_name'], diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/utils/id.rb b/packages/forest_admin_agent/lib/forest_admin_agent/utils/id.rb index fa554bb4a..df95d1c6e 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/utils/id.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/utils/id.rb @@ -2,18 +2,18 @@ module ForestAdminAgent module Utils class Id include ForestAdminDatasourceToolkit::Utils - def self.unpack_id(collection, packed_id, with_key = false) + def self.unpack_id(collection, packed_id, with_key: false) primary_keys = ForestAdminDatasourceToolkit::Utils::Schema.primary_keys(collection) primary_key_values = packed_id.split('|') - if ((nb_pks = primary_keys.size) != (nb_values = primary_key_values.size)) - raise Exceptions::ForestException.new "Expected $primaryKeyNames a size of #{nb_pks} values, found #{nb_values}" + if (nb_pks = primary_keys.size) != (nb_values = primary_key_values.size) + raise Exceptions::ForestException, "Expected $primaryKeyNames a size of #{nb_pks} values, found #{nb_values}" end result = primary_keys.map.with_index do |pk_name, index| field = collection.fields[pk_name] value = primary_key_values[index] casted_value = field.column_type == 'Number' ? value.to_i : value - #TODO: call FieldValidator::validateValue($value, $field, $castedValue); + # TODO: call FieldValidator::validateValue($value, $field, $castedValue); [pk_name, casted_value] end.to_h diff --git a/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/query.rb b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/query.rb index 5fd60f946..fbe90bf18 100644 --- a/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/query.rb +++ b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/query.rb @@ -11,20 +11,20 @@ def build @query = select @query = apply_filter - return @query + @query end def apply_filter @query = apply_condition_tree(@filter.condition_tree) unless @filter.condition_tree.nil? - return @query + @query end def apply_condition_tree(condition_tree, aggregator = nil) # if condition_tree.is_a ConditionTreeBranch - #TODO: add for ConditionTreeBranch + # TODO: add for ConditionTreeBranch # else - compute_main_operator(condition_tree, aggregator || 'and') + compute_main_operator(condition_tree, aggregator || 'and') # end end @@ -36,7 +36,7 @@ def compute_main_operator(condition_tree, aggregator) @query = @query.send(aggregator, @query.where({ field => value })) end - return @query + @query end def select @@ -44,7 +44,7 @@ def select @projection.relations.each do |relation, fields| relation_table = datasource.collection(relation).model.table_name - fields.each { |field| query += ", #{relation_table}.#{field}" } + fields.each { |field| query_select += ", #{relation_table}.#{field}" } end @query = @query.select(query_select) From f79d3810e974fa5a38237f7e98909ed680639bcd Mon Sep 17 00:00:00 2001 From: Nicolas Alexandre Date: Tue, 17 Oct 2023 17:56:02 +0200 Subject: [PATCH 04/18] feat(exception): update not found error --- .rubocop.yml | 3 ++- .../lib/forest_admin_agent/http/Exceptions/not_found_error.rb | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.rubocop.yml b/.rubocop.yml index 70e822775..85e49c349 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -42,12 +42,13 @@ Metrics/AbcSize: - 'packages/forest_admin_agent/lib/forest_admin_agent/utils/query_string_parser.rb' - 'packages/forest_admin_agent/lib/forest_admin_agent/utils/schema/generator_field.rb' - 'packages/forest_admin_agent/lib/forest_admin_agent/utils/schema/schema_emitter.rb' + - 'packages/forest_admin_agent/lib/forest_admin_agent/utils/id.rb' - 'packages/forest_admin_agent/lib/forest_admin_agent/serializer/json_api_serializer.rb' + - 'packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/show.rb' - 'packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/parser/validation.rb' - 'packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/collection.rb' - 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/query/projection.rb' - 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/query/projection_factory.rb' - - Metrics/CyclomaticComplexity: Exclude: diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/http/Exceptions/not_found_error.rb b/packages/forest_admin_agent/lib/forest_admin_agent/http/Exceptions/not_found_error.rb index 74c12db9b..6fa1d7da3 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/http/Exceptions/not_found_error.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/http/Exceptions/not_found_error.rb @@ -2,8 +2,12 @@ module ForestAdminAgent module Http module Exceptions class NotFoundError < StandardError + + attr_reader :name, :status def initialize(msg, name = 'NotFoundError') super msg + @name = name + @status = 404 end end end From dbf509a80bfc475ad119160a50f4dba3e3e15030 Mon Sep 17 00:00:00 2001 From: Nicolas Alexandre Date: Wed, 18 Oct 2023 16:35:58 +0200 Subject: [PATCH 05/18] feat: update query builder --- .../routes/abstract_authenticated_route.rb | 1 + .../routes/resources/store.rb | 22 ++++++++++-- .../collection.rb | 35 ++++++------------- .../utils/query.rb | 16 +++++---- 4 files changed, 40 insertions(+), 34 deletions(-) diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/routes/abstract_authenticated_route.rb b/packages/forest_admin_agent/lib/forest_admin_agent/routes/abstract_authenticated_route.rb index 65bce570d..8b1843d5b 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/routes/abstract_authenticated_route.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/routes/abstract_authenticated_route.rb @@ -5,6 +5,7 @@ def build(args = {}) if args.dig(:headers, 'action_dispatch.remote_ip') Facades::Whitelist.check_ip(args[:headers]['action_dispatch.remote_ip'].to_s) end + @caller = Utils::QueryStringParser.parse_caller(args) super end end diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/store.rb b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/store.rb index 2e40294b6..9e548bfa4 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/store.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/store.rb @@ -13,9 +13,8 @@ def setup_routes def handle_request(args = {}) build(args) - caller = ForestAdminAgent::Utils::QueryStringParser.parse_caller(args) - data = args[:params][:data][:attributes].permit(@collection.fields.keys).to_h - record = @collection.create(caller, data) + data, relationships = format_attributes(args) + record = @collection.create(@caller, data) { name: args[:params]['collection_name'], @@ -26,6 +25,23 @@ def handle_request(args = {}) ) } end + + private + + def format_attributes(args) + record = args[:params][:data][:attributes].permit(@collection.fields.keys).to_h + relations = {} + + args[:params][:data][:relationships].to_unsafe_h.map do |field, value| + schema = @collection.fields[field] + + if (schema.type == 'ManyToOne') + record[schema.foreign_key] = value[:data][schema.foreign_key_target] + end + end + + [record, relations] + end end end end diff --git a/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/collection.rb b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/collection.rb index 95bb57a5a..3c1a24843 100644 --- a/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/collection.rb +++ b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/collection.rb @@ -8,14 +8,14 @@ class Collection < ForestAdminDatasourceToolkit::Collection def initialize(datasource, model) @model = model - name = model.name.split('::').last.downcase + name = model.name.demodulize.underscore super(datasource, name) fetch_fields fetch_associations end def list(_caller, filter, projection) - query = Utils::Query.new(model, projection, filter).build + query = Utils::Query.new(self, projection, filter).build query.offset(filter.page.offset).limit(filter.page.limit).all end @@ -36,21 +36,6 @@ def create(_caller, data) private - # def query_joins(projection) - # @model.joins(projection.relations.keys.map(&:to_sym)) - # end - # - # def query_select(projection) - # query = projection.columns.join(', ') - # - # projection.relations.each do |relation, fields| - # relation_table = datasource.collection(relation).model.table_name - # fields.each { |field| query += ", #{relation_table}.#{field}" } - # end - # - # @model.select(query) - # end - def fetch_fields @model.columns_hash.each do |column_name, column| # TODO: check is not sti column @@ -77,18 +62,18 @@ def fetch_associations add_field( association.name.to_s, ForestAdminDatasourceToolkit::Schema::Relations::OneToOneSchema.new( - foreign_collection: association.class_name.downcase, + foreign_collection: association.class_name.demodulize.underscore, origin_key: association.foreign_key, - origin_key_target: association.join_foreign_key + origin_key_target: association.association_primary_key ) ) when :belongs_to add_field( association.name.to_s, ForestAdminDatasourceToolkit::Schema::Relations::ManyToOneSchema.new( - foreign_collection: association.class_name.downcase, + foreign_collection: association.class_name.demodulize.underscore, foreign_key: association.foreign_key, - foreign_key_target: association.join_foreign_key + foreign_key_target: association.association_primary_key ) ) when :has_many @@ -96,21 +81,21 @@ def fetch_associations add_field( association.name.to_s, ForestAdminDatasourceToolkit::Schema::Relations::ManyToManySchema.new( - foreign_collection: association.class_name.downcase, + foreign_collection: association.class_name.demodulize.underscore, origin_key: association.through_reflection.join_foreign_key, origin_key_target: association.through_reflection.foreign_key, foreign_key: association.join_foreign_key, foreign_key_target: association.association_primary_key, - through_collection: association.through_reflection.class_name.downcase + through_collection: association.through_reflection.class_name.demodulize.underscore ) ) else add_field( association.name.to_s, ForestAdminDatasourceToolkit::Schema::Relations::OneToManySchema.new( - foreign_collection: association.class_name.downcase, + foreign_collection: association.class_name.demodulize.underscore, origin_key: association.foreign_key, - origin_key_target: association.join_foreign_key + origin_key_target: association.association_primary_key ) ) end diff --git a/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/query.rb b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/query.rb index fbe90bf18..a8bb413dc 100644 --- a/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/query.rb +++ b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/query.rb @@ -1,8 +1,9 @@ module ForestAdminDatasourceActiveRecord module Utils class Query - def initialize(model, projection, filter) - @query = model + def initialize(collection, projection, filter) + @collection = collection + @query = @collection.model @projection = projection @filter = filter end @@ -40,15 +41,18 @@ def compute_main_operator(condition_tree, aggregator) end def select - query_select = @projection.columns.join(', ') + query_select = @projection.columns.map { |field| "#{@collection.model.table_name}.#{field}" }.join(', ') @projection.relations.each do |relation, fields| - relation_table = datasource.collection(relation).model.table_name - fields.each { |field| query_select += ", #{relation_table}.#{field}" } + relation_schema = @collection.fields[relation] + query_select += ", #{@collection.model.table_name}.#{relation_schema.foreign_key}" + # fields.each { |field| query_select += ", #{relation_table}.#{field}" } end @query = @query.select(query_select) - @query = @query.joins(@projection.relations.keys.map(&:to_sym)) + @query = @query.eager_load(@projection.relations.keys.map(&:to_sym)) + # TODO: replace eager_load by joins because eager_load select ALL columns of relation + # @query = @query.joins(@projection.relations.keys.map(&:to_sym)) end end end From 449e46ecffb9c8679217337d5e60bf557f8fbdb3 Mon Sep 17 00:00:00 2001 From: Nicolas Alexandre Date: Wed, 18 Oct 2023 17:11:32 +0200 Subject: [PATCH 06/18] chore: lint --- .rubocop.yml | 2 ++ .../forest_admin_agent/http/Exceptions/not_found_error.rb | 2 +- .../lib/forest_admin_agent/routes/resources/store.rb | 6 ++---- .../forest_admin_datasource_active_record/utils/query.rb | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 85e49c349..e4381f27a 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -45,7 +45,9 @@ Metrics/AbcSize: - 'packages/forest_admin_agent/lib/forest_admin_agent/utils/id.rb' - 'packages/forest_admin_agent/lib/forest_admin_agent/serializer/json_api_serializer.rb' - 'packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/show.rb' + - 'packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/store.rb' - 'packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/parser/validation.rb' + - 'packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/query.rb' - 'packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/collection.rb' - 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/query/projection.rb' - 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/query/projection_factory.rb' diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/http/Exceptions/not_found_error.rb b/packages/forest_admin_agent/lib/forest_admin_agent/http/Exceptions/not_found_error.rb index 6fa1d7da3..7e1da5633 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/http/Exceptions/not_found_error.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/http/Exceptions/not_found_error.rb @@ -2,8 +2,8 @@ module ForestAdminAgent module Http module Exceptions class NotFoundError < StandardError - attr_reader :name, :status + def initialize(msg, name = 'NotFoundError') super msg @name = name diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/store.rb b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/store.rb index 9e548bfa4..b433cce22 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/store.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/store.rb @@ -13,7 +13,7 @@ def setup_routes def handle_request(args = {}) build(args) - data, relationships = format_attributes(args) + data, = format_attributes(args) record = @collection.create(@caller, data) { @@ -35,9 +35,7 @@ def format_attributes(args) args[:params][:data][:relationships].to_unsafe_h.map do |field, value| schema = @collection.fields[field] - if (schema.type == 'ManyToOne') - record[schema.foreign_key] = value[:data][schema.foreign_key_target] - end + record[schema.foreign_key] = value[:data][schema.foreign_key_target] if schema.type == 'ManyToOne' end [record, relations] diff --git a/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/query.rb b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/query.rb index a8bb413dc..1bee848b1 100644 --- a/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/query.rb +++ b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/query.rb @@ -43,7 +43,7 @@ def compute_main_operator(condition_tree, aggregator) def select query_select = @projection.columns.map { |field| "#{@collection.model.table_name}.#{field}" }.join(', ') - @projection.relations.each do |relation, fields| + @projection.relations.each do |relation, _fields| relation_schema = @collection.fields[relation] query_select += ", #{@collection.model.table_name}.#{relation_schema.foreign_key}" # fields.each { |field| query_select += ", #{relation_table}.#{field}" } From 7fbc55b9f07488872581bdcb66f1b2985fc3585b Mon Sep 17 00:00:00 2001 From: Nicolas Alexandre Date: Thu, 19 Oct 2023 12:06:02 +0200 Subject: [PATCH 07/18] feat: add update route --- .../lib/forest_admin_agent/http/router.rb | 3 +- .../routes/abstract_authenticated_route.rb | 15 +++++++ .../routes/resources/store.rb | 15 ------- .../routes/resources/update.rb | 42 +++++++++++++++++++ .../collection.rb | 5 +++ .../utils/query.rb | 24 ++++++----- .../contracts/collection_contract.rb | 2 +- 7 files changed, 79 insertions(+), 27 deletions(-) create mode 100644 packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/update.rb diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/http/router.rb b/packages/forest_admin_agent/lib/forest_admin_agent/http/router.rb index 66c5e0d04..1e2034b65 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/http/router.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/http/router.rb @@ -12,7 +12,8 @@ def self.routes Resources::List.new.routes, Resources::Show.new.routes, Resources::Count.new.routes, - Resources::Store.new.routes + Resources::Store.new.routes, + Resources::Update.new.routes ].inject(&:merge) end diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/routes/abstract_authenticated_route.rb b/packages/forest_admin_agent/lib/forest_admin_agent/routes/abstract_authenticated_route.rb index 8b1843d5b..1b9232bab 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/routes/abstract_authenticated_route.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/routes/abstract_authenticated_route.rb @@ -8,6 +8,21 @@ def build(args = {}) @caller = Utils::QueryStringParser.parse_caller(args) super end + + def format_attributes(args) + record = args[:params][:data][:attributes].permit(@collection.fields.keys).to_h + relations = {} + + if ! args[:params][:data][:relationships].nil? + args[:params][:data][:relationships].to_unsafe_h.map do |field, value| + schema = @collection.fields[field] + + record[schema.foreign_key] = value[:data][schema.foreign_key_target] if schema.type == 'ManyToOne' + end + end + + [record, relations] + end end end end diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/store.rb b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/store.rb index b433cce22..ea0ff2442 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/store.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/store.rb @@ -25,21 +25,6 @@ def handle_request(args = {}) ) } end - - private - - def format_attributes(args) - record = args[:params][:data][:attributes].permit(@collection.fields.keys).to_h - relations = {} - - args[:params][:data][:relationships].to_unsafe_h.map do |field, value| - schema = @collection.fields[field] - - record[schema.foreign_key] = value[:data][schema.foreign_key_target] if schema.type == 'ManyToOne' - end - - [record, relations] - end end end end diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/update.rb b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/update.rb new file mode 100644 index 000000000..683a044f8 --- /dev/null +++ b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/update.rb @@ -0,0 +1,42 @@ +require 'jsonapi-serializers' + +module ForestAdminAgent + module Routes + module Resources + class Update < AbstractAuthenticatedRoute + include ForestAdminAgent::Builder + include ForestAdminDatasourceToolkit::Components::Query + + def setup_routes + add_route('forest_update', 'put', '/:collection_name/:id', ->(args) { handle_request(args) }) + + self + end + + def handle_request(args = {}) + build(args) + id = Utils::Id.unpack_id(@collection, args[:params]['id'], with_key: true) + caller = ForestAdminAgent::Utils::QueryStringParser.parse_caller(args) + condition_tree = OpenStruct.new(field: 'id', operator: 'EQUAL', value: id['id']) + # TODO: replace condition_tree by ConditionTreeFactory.matchIds(this.collection.schema, [id]), + filter = ForestAdminDatasourceToolkit::Components::Query::Filter.new( + condition_tree: condition_tree, + page: ForestAdminAgent::Utils::QueryStringParser.parse_pagination(args) + ) + data, = format_attributes(args) + @collection.update(@caller, filter, data) + records = @collection.list(caller, filter, ProjectionFactory.all(@collection)) + + { + name: args[:params]['collection_name'], + content: JSONAPI::Serializer.serialize( + records[0], + is_collection: false, + serializer: Serializer::ForestSerializer + ) + } + end + end + end + end +end diff --git a/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/collection.rb b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/collection.rb index 3c1a24843..09a70046a 100644 --- a/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/collection.rb +++ b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/collection.rb @@ -34,6 +34,11 @@ def create(_caller, data) @model.create(data) end + def update(_caller, filter, data) + entity = Utils::Query.new(self,nil, filter).build.first + entity.update(data) + end + private def fetch_fields diff --git a/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/query.rb b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/query.rb index 1bee848b1..8a42de604 100644 --- a/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/query.rb +++ b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/query.rb @@ -41,18 +41,22 @@ def compute_main_operator(condition_tree, aggregator) end def select - query_select = @projection.columns.map { |field| "#{@collection.model.table_name}.#{field}" }.join(', ') - - @projection.relations.each do |relation, _fields| - relation_schema = @collection.fields[relation] - query_select += ", #{@collection.model.table_name}.#{relation_schema.foreign_key}" - # fields.each { |field| query_select += ", #{relation_table}.#{field}" } + if !@projection.nil? + query_select = @projection.columns.map { |field| "#{@collection.model.table_name}.#{field}" }.join(', ') + + @projection.relations.each do |relation, _fields| + relation_schema = @collection.fields[relation] + query_select += ", #{@collection.model.table_name}.#{relation_schema.foreign_key}" + # fields.each { |field| query_select += ", #{relation_table}.#{field}" } + end + + @query = @query.select(query_select) + @query = @query.eager_load(@projection.relations.keys.map(&:to_sym)) + # TODO: replace eager_load by joins because eager_load select ALL columns of relation + # @query = @query.joins(@projection.relations.keys.map(&:to_sym)) end - @query = @query.select(query_select) - @query = @query.eager_load(@projection.relations.keys.map(&:to_sym)) - # TODO: replace eager_load by joins because eager_load select ALL columns of relation - # @query = @query.joins(@projection.relations.keys.map(&:to_sym)) + @query end end end diff --git a/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/contracts/collection_contract.rb b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/contracts/collection_contract.rb index 06c8223c7..623f4e29a 100644 --- a/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/contracts/collection_contract.rb +++ b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/contracts/collection_contract.rb @@ -30,7 +30,7 @@ def list(caller, filter, projection) raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'" end - def update + def update(caller, filter, data) raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'" end From e17c7575fe2c9dbb3f4a9fa29f0d3f72b5dc9bd8 Mon Sep 17 00:00:00 2001 From: Nicolas Alexandre Date: Thu, 19 Oct 2023 15:21:34 +0200 Subject: [PATCH 08/18] chore: lint --- .rubocop.yml | 2 ++ .../routes/abstract_authenticated_route.rb | 8 +++----- .../forest_admin_datasource_active_record/collection.rb | 2 +- .../forest_admin_datasource_active_record/utils/query.rb | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index e4381f27a..904a30232 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -44,8 +44,10 @@ Metrics/AbcSize: - 'packages/forest_admin_agent/lib/forest_admin_agent/utils/schema/schema_emitter.rb' - 'packages/forest_admin_agent/lib/forest_admin_agent/utils/id.rb' - 'packages/forest_admin_agent/lib/forest_admin_agent/serializer/json_api_serializer.rb' + - 'packages/forest_admin_agent/lib/forest_admin_agent/routes/abstract_authenticated_route.rb' - 'packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/show.rb' - 'packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/store.rb' + - 'packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/update.rb' - 'packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/parser/validation.rb' - 'packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/query.rb' - 'packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/collection.rb' diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/routes/abstract_authenticated_route.rb b/packages/forest_admin_agent/lib/forest_admin_agent/routes/abstract_authenticated_route.rb index 1b9232bab..9a1a670ae 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/routes/abstract_authenticated_route.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/routes/abstract_authenticated_route.rb @@ -13,12 +13,10 @@ def format_attributes(args) record = args[:params][:data][:attributes].permit(@collection.fields.keys).to_h relations = {} - if ! args[:params][:data][:relationships].nil? - args[:params][:data][:relationships].to_unsafe_h.map do |field, value| - schema = @collection.fields[field] + args[:params][:data][:relationships]&.to_unsafe_h&.map do |field, value| + schema = @collection.fields[field] - record[schema.foreign_key] = value[:data][schema.foreign_key_target] if schema.type == 'ManyToOne' - end + record[schema.foreign_key] = value[:data][schema.foreign_key_target] if schema.type == 'ManyToOne' end [record, relations] diff --git a/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/collection.rb b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/collection.rb index 09a70046a..be54c560e 100644 --- a/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/collection.rb +++ b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/collection.rb @@ -35,7 +35,7 @@ def create(_caller, data) end def update(_caller, filter, data) - entity = Utils::Query.new(self,nil, filter).build.first + entity = Utils::Query.new(self, nil, filter).build.first entity.update(data) end diff --git a/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/query.rb b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/query.rb index 8a42de604..016e2eed9 100644 --- a/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/query.rb +++ b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/query.rb @@ -41,7 +41,7 @@ def compute_main_operator(condition_tree, aggregator) end def select - if !@projection.nil? + unless @projection.nil? query_select = @projection.columns.map { |field| "#{@collection.model.table_name}.#{field}" }.join(', ') @projection.relations.each do |relation, _fields| From a2b37567ae604282764f9e35041aec72bb9d0299 Mon Sep 17 00:00:00 2001 From: Nicolas Alexandre Date: Fri, 20 Oct 2023 16:17:28 +0200 Subject: [PATCH 09/18] feat: add deletes routes --- .rubocop.yml | 1 + .../lib/forest_admin_agent/http/router.rb | 3 +- .../routes/resources/delete.rb | 44 +++++++++++++++++++ .../lib/forest_admin_agent/utils/id.rb | 13 ++++++ .../collection.rb | 5 +++ .../utils/query.rb | 2 +- .../contracts/collection_contract.rb | 2 +- 7 files changed, 67 insertions(+), 3 deletions(-) create mode 100644 packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/delete.rb diff --git a/.rubocop.yml b/.rubocop.yml index 904a30232..aa6d222b4 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -44,6 +44,7 @@ Metrics/AbcSize: - 'packages/forest_admin_agent/lib/forest_admin_agent/utils/schema/schema_emitter.rb' - 'packages/forest_admin_agent/lib/forest_admin_agent/utils/id.rb' - 'packages/forest_admin_agent/lib/forest_admin_agent/serializer/json_api_serializer.rb' + - 'packages/forest_admin_agent/lib/forest_admin_agent/http/router.rb' - 'packages/forest_admin_agent/lib/forest_admin_agent/routes/abstract_authenticated_route.rb' - 'packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/show.rb' - 'packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/store.rb' diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/http/router.rb b/packages/forest_admin_agent/lib/forest_admin_agent/http/router.rb index 1e2034b65..b9a6739f6 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/http/router.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/http/router.rb @@ -9,9 +9,10 @@ def self.routes # api_charts_routes, System::HealthCheck.new.routes, Security::Authentication.new.routes, + Resources::Count.new.routes, + Resources::Delete.new.routes, Resources::List.new.routes, Resources::Show.new.routes, - Resources::Count.new.routes, Resources::Store.new.routes, Resources::Update.new.routes ].inject(&:merge) diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/delete.rb b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/delete.rb new file mode 100644 index 000000000..d39dcee53 --- /dev/null +++ b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/delete.rb @@ -0,0 +1,44 @@ +require 'jsonapi-serializers' + +module ForestAdminAgent + module Routes + module Resources + class Delete < AbstractAuthenticatedRoute + include ForestAdminAgent::Builder + include ForestAdminDatasourceToolkit::Components::Query + + def setup_routes + add_route('forest_delete_bulk', 'delete', '/:collection_name', ->(args) { handle_request_bulk(args) }) + add_route('forest_delete', 'delete', '/:collection_name/:id', ->(args) { handle_request(args) }) + + self + end + + def handle_request(args = {}) + build(args) + id = Utils::Id.unpack_id(@collection, args[:params]['id']) + delete_records(args, { ids: [id], are_excluded: false }) + + { content: nil, status: 204 } + end + + def handle_request_bulk(args = {}) + build(args) + selection_ids = Utils::Id.parse_selection_ids(@collection, args[:params].to_unsafe_h) + delete_records(args, selection_ids) + + { content: nil, status: 204 } + end + + def delete_records(_args, selection_ids) + # TODO: replace by ConditionTreeFactory.matchIds(this.collection.schema, selectionIds.ids) + condition_tree = OpenStruct.new(field: 'id', operator: 'IN', value: selection_ids[:ids][0]) + condition_tree.inverse if selection_ids[:are_excluded] + filter = ForestAdminDatasourceToolkit::Components::Query::Filter.new(condition_tree: condition_tree) + + @collection.delete(@caller, filter) + end + end + end + end +end diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/utils/id.rb b/packages/forest_admin_agent/lib/forest_admin_agent/utils/id.rb index df95d1c6e..93d460b5e 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/utils/id.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/utils/id.rb @@ -20,6 +20,19 @@ def self.unpack_id(collection, packed_id, with_key: false) with_key ? result : result.values end + + def self.unpack_ids(collection, packed_ids) + packed_ids.map { |item| unpack_id(collection, item) } + end + + def self.parse_selection_ids(collection, params) + attributes = params.dig('data', 'attributes') + are_excluded = attributes&.key?('all_records') ? attributes['all_records'] : false + input_ids = attributes&.key?('ids') ? attributes['ids'] : params['data'].map { |item| item['id'] } + ids = unpack_ids(collection, are_excluded ? attributes['all_records_ids_excluded'] : input_ids) + + { are_excluded: are_excluded, ids: ids } + end end end end diff --git a/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/collection.rb b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/collection.rb index be54c560e..d408e46a6 100644 --- a/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/collection.rb +++ b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/collection.rb @@ -39,6 +39,11 @@ def update(_caller, filter, data) entity.update(data) end + def delete(_caller, filter) + entities = Utils::Query.new(self, nil, filter).build + entities&.each(&:destroy) + end + private def fetch_fields diff --git a/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/query.rb b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/query.rb index 016e2eed9..2060a06ec 100644 --- a/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/query.rb +++ b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/query.rb @@ -33,7 +33,7 @@ def compute_main_operator(condition_tree, aggregator) field = condition_tree.field value = condition_tree.value case condition_tree.operator - when 'EQUAL' + when 'EQUAL', 'IN' @query = @query.send(aggregator, @query.where({ field => value })) end diff --git a/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/contracts/collection_contract.rb b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/contracts/collection_contract.rb index 623f4e29a..6a0c61fd8 100644 --- a/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/contracts/collection_contract.rb +++ b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/contracts/collection_contract.rb @@ -34,7 +34,7 @@ def update(caller, filter, data) raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'" end - def delete + def delete(caller, filter) raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'" end From 8b2bae34677230c9af0d93304171bc8453059eca Mon Sep 17 00:00:00 2001 From: Nicolas Alexandre Date: Tue, 24 Oct 2023 17:35:28 +0200 Subject: [PATCH 10/18] fix(serializer): allow to serialise only selected colmuns --- .../lib/forest_admin_agent/routes/resources/list.rb | 3 ++- .../serializer/forest_serializer.rb | 11 +++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/list.rb b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/list.rb index 9da61abdc..a9d5b2c4f 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/list.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/list.rb @@ -26,7 +26,8 @@ def handle_request(args = {}) records, is_collection: true, serializer: Serializer::ForestSerializer, - include: projection.relations.keys + include: projection.relations.keys, + fields: { @collection.model.model_name.plural.to_sym => projection } ) } 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 54013e7d2..2dc7b626e 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 @@ -47,6 +47,7 @@ def attributes return {} if attributes_map.nil? attributes = {} + attributes_map.each do |attribute_name, attr_data| next if !should_include_attr?(attribute_name, attr_data) value = evaluate_attr_or_block(attribute_name, attr_data[:attr_or_block]) @@ -55,6 +56,16 @@ def attributes attributes end + def evaluate_attr_or_block(attribute_name, attr_or_block) + if attr_or_block.is_a?(Proc) + # A custom block was given, call it to get the value. + instance_eval(&attr_or_block) + else + # Default behavior, call a method by the name of the attribute. + object.try(attr_or_block) + end + end + def add_to_one_association(name, options = {}, &block) options[:include_links] = options.fetch(:include_links, true) options[:include_data] = options.fetch(:include_data, false) From c6e1d09b97327f012c902e94e45c702f60133ded Mon Sep 17 00:00:00 2001 From: Nicolas Alexandre Date: Tue, 24 Oct 2023 17:35:50 +0200 Subject: [PATCH 11/18] feat: update store route --- .../routes/abstract_authenticated_route.rb | 3 +-- .../routes/resources/delete.rb | 1 + .../routes/resources/store.rb | 25 ++++++++++++++++++- .../routes/resources/update.rb | 3 ++- .../utils/query.rb | 6 ++++- 5 files changed, 33 insertions(+), 5 deletions(-) diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/routes/abstract_authenticated_route.rb b/packages/forest_admin_agent/lib/forest_admin_agent/routes/abstract_authenticated_route.rb index 9a1a670ae..094b95fb2 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/routes/abstract_authenticated_route.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/routes/abstract_authenticated_route.rb @@ -11,7 +11,6 @@ def build(args = {}) def format_attributes(args) record = args[:params][:data][:attributes].permit(@collection.fields.keys).to_h - relations = {} args[:params][:data][:relationships]&.to_unsafe_h&.map do |field, value| schema = @collection.fields[field] @@ -19,7 +18,7 @@ def format_attributes(args) record[schema.foreign_key] = value[:data][schema.foreign_key_target] if schema.type == 'ManyToOne' end - [record, relations] + record end end end diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/delete.rb b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/delete.rb index d39dcee53..9a1084c49 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/delete.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/delete.rb @@ -1,4 +1,5 @@ require 'jsonapi-serializers' +require 'ostruct' module ForestAdminAgent module Routes diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/store.rb b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/store.rb index ea0ff2442..c82047f04 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/store.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/store.rb @@ -1,4 +1,5 @@ require 'jsonapi-serializers' +require 'ostruct' module ForestAdminAgent module Routes @@ -13,8 +14,9 @@ def setup_routes def handle_request(args = {}) build(args) - data, = format_attributes(args) + data = format_attributes(args) record = @collection.create(@caller, data) + link_one_to_one_relations(args, record) { name: args[:params]['collection_name'], @@ -25,6 +27,27 @@ def handle_request(args = {}) ) } end + + def link_one_to_one_relations(args, record) + relations = {} + + args[:params][:data][:relationships]&.to_unsafe_h&.map do |field, value| + schema = @collection.fields[field] + if schema.type == 'OneToOne' + id = Utils::Id.unpack_id(@collection, value['data']['id'], with_key: true) + relations[field] = id + foreign_collection = @datasource.collection(schema.foreign_collection) + # Load the value that will be used as origin_key + origin_value = record[schema.origin_key_target] + + # update new relation (may update zero or one records). + # TODO: replace by ConditionTreeFactory.matchRecords(foreignCollection.schema, [linked]); + condition_tree = OpenStruct.new(field: 'id', operator: 'EQUAL', value: id['id']) + filter = ForestAdminDatasourceToolkit::Components::Query::Filter.new(condition_tree: condition_tree) + foreign_collection.update(@caller, filter, {schema.origin_key => origin_value}) + end + end + end end end end diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/update.rb b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/update.rb index 683a044f8..e48396c38 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/update.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/update.rb @@ -1,4 +1,5 @@ require 'jsonapi-serializers' +require 'ostruct' module ForestAdminAgent module Routes @@ -23,7 +24,7 @@ def handle_request(args = {}) condition_tree: condition_tree, page: ForestAdminAgent::Utils::QueryStringParser.parse_pagination(args) ) - data, = format_attributes(args) + data = format_attributes(args) @collection.update(@caller, filter, data) records = @collection.list(caller, filter, ProjectionFactory.all(@collection)) diff --git a/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/query.rb b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/query.rb index 2060a06ec..6d4ed5f3a 100644 --- a/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/query.rb +++ b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/query.rb @@ -46,7 +46,11 @@ def select @projection.relations.each do |relation, _fields| relation_schema = @collection.fields[relation] - query_select += ", #{@collection.model.table_name}.#{relation_schema.foreign_key}" + if relation_schema.type == 'OneToOne' + query_select += ", #{@collection.model.table_name}.#{relation_schema.origin_key_target}" + else + query_select += ", #{@collection.model.table_name}.#{relation_schema.foreign_key}" + end # fields.each { |field| query_select += ", #{relation_table}.#{field}" } end From 91a8fab426d01d91c0739fdbdb081f0a4ed257e4 Mon Sep 17 00:00:00 2001 From: Nicolas Alexandre Date: Tue, 24 Oct 2023 17:45:52 +0200 Subject: [PATCH 12/18] chore: lint --- .../lib/forest_admin_agent/routes/resources/store.rb | 2 +- .../utils/query.rb | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/store.rb b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/store.rb index c82047f04..4d528b1c4 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/store.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/store.rb @@ -44,7 +44,7 @@ def link_one_to_one_relations(args, record) # TODO: replace by ConditionTreeFactory.matchRecords(foreignCollection.schema, [linked]); condition_tree = OpenStruct.new(field: 'id', operator: 'EQUAL', value: id['id']) filter = ForestAdminDatasourceToolkit::Components::Query::Filter.new(condition_tree: condition_tree) - foreign_collection.update(@caller, filter, {schema.origin_key => origin_value}) + foreign_collection.update(@caller, filter, { schema.origin_key => origin_value }) end end end diff --git a/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/query.rb b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/query.rb index 6d4ed5f3a..fa258f2c7 100644 --- a/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/query.rb +++ b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/query.rb @@ -46,12 +46,11 @@ def select @projection.relations.each do |relation, _fields| relation_schema = @collection.fields[relation] - if relation_schema.type == 'OneToOne' - query_select += ", #{@collection.model.table_name}.#{relation_schema.origin_key_target}" - else - query_select += ", #{@collection.model.table_name}.#{relation_schema.foreign_key}" - end - # fields.each { |field| query_select += ", #{relation_table}.#{field}" } + query_select += if relation_schema.type == 'OneToOne' + ", #{@collection.model.table_name}.#{relation_schema.origin_key_target}" + else + ", #{@collection.model.table_name}.#{relation_schema.foreign_key}" + end end @query = @query.select(query_select) From 20a292694c1297194d24bb0b4f099f1210fd7fc5 Mon Sep 17 00:00:00 2001 From: Nicolas Alexandre Date: Wed, 25 Oct 2023 18:10:23 +0200 Subject: [PATCH 13/18] test: add test on create route --- .rubocop.yml | 1 + .../routes/abstract_authenticated_route.rb | 6 +- .../routes/resources/list.rb | 3 +- .../routes/resources/store.rb | 4 +- .../lib/forest_admin_agent/utils/id.rb | 2 +- .../routes/resources/store_spec.rb | 211 ++++++++++++++++++ .../forest_admin_rails/forest_controller.rb | 2 +- 7 files changed, 220 insertions(+), 9 deletions(-) create mode 100644 packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/store_spec.rb diff --git a/.rubocop.yml b/.rubocop.yml index aa6d222b4..fef3fba85 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -54,6 +54,7 @@ Metrics/AbcSize: - 'packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/collection.rb' - 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/query/projection.rb' - 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/query/projection_factory.rb' + - 'packages/forest_admin_rails/app/controllers/forest_admin_rails/forest_controller.rb' Metrics/CyclomaticComplexity: Exclude: diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/routes/abstract_authenticated_route.rb b/packages/forest_admin_agent/lib/forest_admin_agent/routes/abstract_authenticated_route.rb index 094b95fb2..559ee96bc 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/routes/abstract_authenticated_route.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/routes/abstract_authenticated_route.rb @@ -10,12 +10,12 @@ def build(args = {}) end def format_attributes(args) - record = args[:params][:data][:attributes].permit(@collection.fields.keys).to_h + record = args[:params][:data][:attributes] - args[:params][:data][:relationships]&.to_unsafe_h&.map do |field, value| + args[:params][:data][:relationships]&.map do |field, value| schema = @collection.fields[field] - record[schema.foreign_key] = value[:data][schema.foreign_key_target] if schema.type == 'ManyToOne' + record[schema.foreign_key] = value['data'][schema.foreign_key_target] if schema.type == 'ManyToOne' end record diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/list.rb b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/list.rb index a9d5b2c4f..9da61abdc 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/list.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/list.rb @@ -26,8 +26,7 @@ def handle_request(args = {}) records, is_collection: true, serializer: Serializer::ForestSerializer, - include: projection.relations.keys, - fields: { @collection.model.model_name.plural.to_sym => projection } + include: projection.relations.keys ) } end diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/store.rb b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/store.rb index 4d528b1c4..2925bab3d 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/store.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/store.rb @@ -7,7 +7,7 @@ module Resources class Store < AbstractAuthenticatedRoute include ForestAdminAgent::Builder def setup_routes - add_route('forest_create', 'post', '/:collection_name', ->(args) { handle_request(args) }) + add_route('forest_store', 'post', '/:collection_name', ->(args) { handle_request(args) }) self end @@ -31,7 +31,7 @@ def handle_request(args = {}) def link_one_to_one_relations(args, record) relations = {} - args[:params][:data][:relationships]&.to_unsafe_h&.map do |field, value| + args[:params][:data][:relationships]&.map do |field, value| schema = @collection.fields[field] if schema.type == 'OneToOne' id = Utils::Id.unpack_id(@collection, value['data']['id'], with_key: true) diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/utils/id.rb b/packages/forest_admin_agent/lib/forest_admin_agent/utils/id.rb index 93d460b5e..1059e7dba 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/utils/id.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/utils/id.rb @@ -4,7 +4,7 @@ class Id include ForestAdminDatasourceToolkit::Utils def self.unpack_id(collection, packed_id, with_key: false) primary_keys = ForestAdminDatasourceToolkit::Utils::Schema.primary_keys(collection) - primary_key_values = packed_id.split('|') + primary_key_values = packed_id.to_s.split('|') if (nb_pks = primary_keys.size) != (nb_values = primary_key_values.size) raise Exceptions::ForestException, "Expected $primaryKeyNames a size of #{nb_pks} values, found #{nb_values}" end diff --git a/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/store_spec.rb b/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/store_spec.rb new file mode 100644 index 000000000..11b9f0a38 --- /dev/null +++ b/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/store_spec.rb @@ -0,0 +1,211 @@ +require 'spec_helper' +require 'singleton' +require 'ostruct' +require 'shared/caller' + +module ForestAdminAgent + module Routes + module Resources + include ForestAdminDatasourceToolkit + include ForestAdminDatasourceToolkit::Schema + describe Store do + include_context 'with caller' + subject(:store) { described_class.new } + let(:args) do + { + headers: { 'HTTP_AUTHORIZATION' => bearer }, + params: { + 'collection_name' => 'book', + 'timezone' => 'Europe/Paris' + } + } + end + + it 'adds the route forest_store' do + store.setup_routes + expect(store.routes.include?('forest_store')).to be true + expect(store.routes.length).to eq 1 + end + + describe 'simple case' do + before do + book_class = Struct.new(:id, :title, :published_at, :price) do + # def name + # 'book' + # end + + def respond_to?(arg) + return false if arg == :each + + super arg + end + end + stub_const('Book', book_class) + end + + it 'call create and return an serialized content' do + attributes = { + 'title' => 'Harry potter and the goblet of fire', + 'published_at' => '2000-07-07T21:00:00.000Z', + 'price' => 6.75 + } + + datasource = Datasource.new + collection = Collection.new(datasource, 'book') + collection.add_fields( + { + 'id' => ColumnSchema.new(column_type: 'Number', is_primary_key: true), + 'title' => ColumnSchema.new(column_type: 'String'), + 'published_at' => ColumnSchema.new(column_type: 'Date'), + 'price' => ColumnSchema.new(column_type: 'Number') + } + ) + allow(ForestAdminAgent::Builder::AgentFactory.instance).to receive(:send_schema).and_return(nil) + datasource.add_collection(collection) + ForestAdminAgent::Builder::AgentFactory.instance.add_datasource(datasource) + ForestAdminAgent::Builder::AgentFactory.instance.build + args[:params][:data] = { attributes: attributes, type: 'books' } + book = Book.new(1, attributes['title'], attributes['published_at'], attributes['price']) + + allow(collection).to receive(:create).and_return(book) + result = store.handle_request(args) + expect(result[:name]).to eq('book') + expect(result[:content]).to eq( + 'data' => + { + 'type' => 'book', + 'id' => '1', + 'attributes' => { + 'id' => 1, + 'title' => 'Harry potter and the goblet of fire', + 'published_at' => '2000-07-07T21:00:00.000Z', + 'price' => 6.75 + }, + 'links' => { 'self' => 'forest/book/1' } + } + ) + end + end + + describe 'with relation' do + before do + person_class = Struct.new(:id, :name) do + def respond_to?(arg) + return false if arg == :each + + super arg + end + end + + passport_class = Struct.new(:id, :person_id) do + def respond_to?(arg) + return false if arg == :each + + super arg + end + end + stub_const('Person', person_class) + stub_const('Passport', passport_class) + + @datasource = Datasource.new + collection_person = Collection.new(@datasource, 'person') + collection_person.add_fields( + { + 'id' => ColumnSchema.new(column_type: 'Number', is_primary_key: true), + 'name' => ColumnSchema.new(column_type: 'String'), + 'passport' => Relations::OneToOneSchema.new( + origin_key: 'person_id', + origin_key_target: 'id', + foreign_collection: 'passport' + ) + } + ) + + collection_passport = Collection.new(@datasource, 'passport') + collection_passport.add_fields( + { + 'id' => ColumnSchema.new(column_type: 'Number', is_primary_key: true), + 'person_id' => ColumnSchema.new(column_type: 'Number'), + 'person' => Relations::ManyToOneSchema.new( + foreign_key: 'person_id', + foreign_key_target: 'id', + foreign_collection: 'passport' + ) + } + ) + @datasource.add_collection(collection_person) + @datasource.add_collection(collection_passport) + allow(ForestAdminAgent::Builder::AgentFactory.instance).to receive(:send_schema).and_return(nil) + ForestAdminAgent::Builder::AgentFactory.instance.add_datasource(@datasource) + ForestAdminAgent::Builder::AgentFactory.instance.build + end + + describe 'with one to one relation' do + it 'call create and return an serialized content' do + args[:params][:data] = { + attributes: { 'name' => 'john' }, + relationships: { 'passport' => { 'data' => { 'type' => 'passports', 'id' => 1 } } }, + type: 'persons' + } + args[:params]['collection_name'] = 'person' + + allow(@datasource.collection('person')).to receive(:create).and_return(Person.new(1, 'john')) + allow(@datasource.collection('passport')).to receive(:update).and_return(Passport.new(1, 1)) + result = store.handle_request(args) + expect(result[:name]).to eq('person') + expect(result[:content]).to eq( + 'data' => + { + 'type' => 'person', + 'id' => '1', + 'attributes' => { 'id' => 1, 'name' => 'john' }, + 'links' => { 'self' => 'forest/person/1' }, + 'relationships' => { + 'passport' => { + 'data' => nil, + 'links' => { 'related' => { 'href' => 'forest/person/1/relationships/passport' } } + } + } + } + ) + end + end + + describe 'with many to one relation' do + it 'call create and return an serialized content' do + args[:params][:data] = { + attributes: {}, + relationships: { 'person' => { 'data' => { 'type' => 'person', 'id' => 1 } } }, + type: 'persons' + } + args[:params]['collection_name'] = 'passport' + + allow(@datasource.collection('passport')).to receive(:create).and_return(Passport.new(1, 1)) + result = store.handle_request(args) + expect(result[:name]).to eq('passport') + expect(result[:content]).to eq( + 'data' => + { + 'type' => 'passport', + 'id' => '1', + 'attributes' => { 'id' => 1, 'person_id' => 1 }, + 'links' => { 'self' => 'forest/passport/1' }, + 'relationships' => { + 'person' => { + 'data' => nil, + 'links' => { + 'related' => { + 'href' => 'forest/passport/1/relationships/person' + } + } + } + } + } + ) + end + end + end + end + end + end +end diff --git a/packages/forest_admin_rails/app/controllers/forest_admin_rails/forest_controller.rb b/packages/forest_admin_rails/app/controllers/forest_admin_rails/forest_controller.rb index 6aa97e5c7..853e05bf1 100644 --- a/packages/forest_admin_rails/app/controllers/forest_admin_rails/forest_controller.rb +++ b/packages/forest_admin_rails/app/controllers/forest_admin_rails/forest_controller.rb @@ -6,7 +6,7 @@ def index if ForestAdminAgent::Http::Router.routes.key? params['route_alias'] route = ForestAdminAgent::Http::Router.routes[params['route_alias']] - forest_response route[:closure].call({ params: params, headers: request.headers.to_h }) + forest_response route[:closure].call({ params: params.to_unsafe_h, headers: request.headers.to_h }) else render json: { error: 'Route not found' }, status: 404 end From 98a37461d1ad00b4b3115c062306ebaf1e9286a4 Mon Sep 17 00:00:00 2001 From: Nicolas Alexandre Date: Thu, 26 Oct 2023 11:51:06 +0200 Subject: [PATCH 14/18] test: add test on update route --- .../lib/forest_admin_agent/utils/id.rb | 1 + .../routes/resources/update_spec.rb | 79 +++++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/update_spec.rb diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/utils/id.rb b/packages/forest_admin_agent/lib/forest_admin_agent/utils/id.rb index 1059e7dba..9f21e220c 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/utils/id.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/utils/id.rb @@ -2,6 +2,7 @@ module ForestAdminAgent module Utils class Id include ForestAdminDatasourceToolkit::Utils + include ForestAdminDatasourceToolkit def self.unpack_id(collection, packed_id, with_key: false) primary_keys = ForestAdminDatasourceToolkit::Utils::Schema.primary_keys(collection) primary_key_values = packed_id.to_s.split('|') diff --git a/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/update_spec.rb b/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/update_spec.rb new file mode 100644 index 000000000..3fd8daee5 --- /dev/null +++ b/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/update_spec.rb @@ -0,0 +1,79 @@ +require 'spec_helper' +require 'singleton' +require 'ostruct' +require 'shared/caller' + +module ForestAdminAgent + module Routes + module Resources + include ForestAdminDatasourceToolkit + include ForestAdminDatasourceToolkit::Schema + describe Update do + include_context 'with caller' + subject(:update) { described_class.new } + let(:args) do + { + headers: { 'HTTP_AUTHORIZATION' => bearer }, + params: { + 'collection_name' => 'book', + 'timezone' => 'Europe/Paris' + } + } + end + + it 'adds the route forest_store' do + update.setup_routes + expect(update.routes.include?('forest_update')).to be true + expect(update.routes.length).to eq 1 + end + + describe 'handle_request' do + before do + book_class = Struct.new(:id, :title, :published_at, :price) do + def respond_to?(arg) + return false if arg == :each + + super arg + end + end + stub_const('Book', book_class) + end + + it 'call update and return an serialized content' do + datasource = Datasource.new + collection = Collection.new(datasource, 'book') + collection.add_fields( + { + 'id' => ColumnSchema.new(column_type: 'Number', is_primary_key: true), + 'title' => ColumnSchema.new(column_type: 'String') + } + ) + allow(ForestAdminAgent::Builder::AgentFactory.instance).to receive(:send_schema).and_return(nil) + datasource.add_collection(collection) + ForestAdminAgent::Builder::AgentFactory.instance.add_datasource(datasource) + ForestAdminAgent::Builder::AgentFactory.instance.build + + args[:params][:data] = { attributes: { 'title' => 'Harry potter and the goblet of fire' } } + args[:params]['id'] = '1' + book = Book.new(1, 'Harry potter and the goblet of fire') + allow(collection).to receive_messages(list: [book], update: true) + result = update.handle_request(args) + expect(result[:name]).to eq('book') + expect(result[:content]).to eq( + 'data' => + { + 'type' => 'book', + 'id' => '1', + 'attributes' => { + 'id' => 1, + 'title' => 'Harry potter and the goblet of fire' + }, + 'links' => { 'self' => 'forest/book/1' } + } + ) + end + end + end + end + end +end From c01280e96e2b7a137d981f3f4b61a68476c666dc Mon Sep 17 00:00:00 2001 From: Nicolas Alexandre Date: Thu, 26 Oct 2023 11:58:04 +0200 Subject: [PATCH 15/18] test: add test on delete route --- .../routes/resources/delete_spec.rb | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/delete_spec.rb diff --git a/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/delete_spec.rb b/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/delete_spec.rb new file mode 100644 index 000000000..4cfd266f8 --- /dev/null +++ b/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/delete_spec.rb @@ -0,0 +1,33 @@ +require 'spec_helper' +require 'singleton' +require 'ostruct' +require 'shared/caller' + +module ForestAdminAgent + module Routes + module Resources + include ForestAdminDatasourceToolkit + include ForestAdminDatasourceToolkit::Schema + describe Delete do + include_context 'with caller' + subject(:delete) { described_class.new } + let(:args) do + { + headers: { 'HTTP_AUTHORIZATION' => bearer }, + params: { + 'collection_name' => 'book', + 'timezone' => 'Europe/Paris' + } + } + end + + it 'adds the route forest_store' do + delete.setup_routes + expect(delete.routes.include?('forest_delete')).to be true + expect(delete.routes.include?('forest_delete_bulk')).to be true + expect(delete.routes.length).to eq 2 + end + end + end + end +end From 91533ce48d936871469978ca2853147d53122ba6 Mon Sep 17 00:00:00 2001 From: Nicolas Alexandre Date: Thu, 26 Oct 2023 16:52:32 +0200 Subject: [PATCH 16/18] test: add test on show route --- .../routes/resources/list_spec.rb | 6 +- .../routes/resources/show_spec.rb | 85 +++++++++++++++++++ .../contracts/collection_contract_spec.rb | 6 +- 3 files changed, 89 insertions(+), 8 deletions(-) create mode 100644 packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/show_spec.rb diff --git a/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/list_spec.rb b/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/list_spec.rb index ce1a4ef6d..77db68924 100644 --- a/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/list_spec.rb +++ b/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/list_spec.rb @@ -22,11 +22,7 @@ module Resources end before do - user_class = Struct.new(:id, :first_name, :last_name) do - def name - 'user' - end - end + user_class = Struct.new(:id, :first_name, :last_name) stub_const('User', user_class) datasource = Datasource.new diff --git a/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/show_spec.rb b/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/show_spec.rb new file mode 100644 index 000000000..c573482b5 --- /dev/null +++ b/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/show_spec.rb @@ -0,0 +1,85 @@ +require 'spec_helper' +require 'singleton' +require 'ostruct' +require 'shared/caller' + +module ForestAdminAgent + module Routes + module Resources + include ForestAdminDatasourceToolkit + include ForestAdminDatasourceToolkit::Schema + describe Show do + include_context 'with caller' + subject(:show) { described_class.new } + let(:args) do + { + headers: { 'HTTP_AUTHORIZATION' => bearer }, + params: { + 'collection_name' => 'user', + 'timezone' => 'Europe/Paris' + } + } + end + + let(:datasource) do + user_class = Struct.new(:id, :first_name, :last_name) do + def respond_to?(arg) + return false if arg == :each + + super arg + end + end + stub_const('User', user_class) + + datasource = Datasource.new + collection = Collection.new(datasource, 'user') + collection.add_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') + } + ) + allow(ForestAdminAgent::Builder::AgentFactory.instance).to receive(:send_schema).and_return(nil) + datasource.add_collection(collection) + ForestAdminAgent::Builder::AgentFactory.instance.add_datasource(datasource) + ForestAdminAgent::Builder::AgentFactory.instance.build + + datasource + end + + it 'adds the route forest_list' do + show.setup_routes + expect(show.routes.include?('forest_show')).to be true + expect(show.routes.length).to eq 1 + end + + it 'return an serialized content' do + allow(datasource.collection('user')).to receive(:list).and_return( + [ + User.new(1, 'foo', 'foo') + ] + ) + args[:params]['id'] = 1 + + result = show.handle_request(args) + + expect(result[:name]).to eq('user') + expect(result[:content]).to eq( + 'data' => { + 'type' => 'user', + 'id' => '1', + 'attributes' => { + 'id' => 1, + 'first_name' => 'foo', + 'last_name' => 'foo' + }, + 'links' => { 'self' => 'forest/user/1' } + }, + 'included' => [] + ) + end + end + end + end +end diff --git a/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/components/contracts/collection_contract_spec.rb b/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/components/contracts/collection_contract_spec.rb index 083cd6b44..d11775143 100644 --- a/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/components/contracts/collection_contract_spec.rb +++ b/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/components/contracts/collection_contract_spec.rb @@ -27,10 +27,10 @@ module Contracts it { expect { collection.name }.to raise_error(NotImplementedError) } it { expect { collection.execute }.to raise_error(NotImplementedError) } it { expect { collection.form }.to raise_error(NotImplementedError) } - it { expect { collection.create }.to raise_error(NotImplementedError) } + it { expect { collection.create(caller, {}) }.to raise_error(NotImplementedError) } it { expect { collection.list(caller, Filter.new, Projection.new) }.to raise_error(NotImplementedError) } - it { expect { collection.update }.to raise_error(NotImplementedError) } - it { expect { collection.delete }.to raise_error(NotImplementedError) } + it { expect { collection.update(caller, Filter.new, {}) }.to raise_error(NotImplementedError) } + it { expect { collection.delete(caller, Filter.new) }.to raise_error(NotImplementedError) } it { expect { collection.render_chart }.to raise_error(NotImplementedError) } it { From b0b41a7f65c7bea17a21f4820d9dab20d724d9e2 Mon Sep 17 00:00:00 2001 From: Nicolas Alexandre Date: Thu, 26 Oct 2023 18:15:36 +0200 Subject: [PATCH 17/18] test: add tests for utils id class --- .../lib/forest_admin_agent/utils/id.rb | 2 +- .../lib/forest_admin_agent/utils/id_spec.rb | 146 ++++++++++++++++++ 2 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/id_spec.rb diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/utils/id.rb b/packages/forest_admin_agent/lib/forest_admin_agent/utils/id.rb index 9f21e220c..258a2c084 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/utils/id.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/utils/id.rb @@ -7,7 +7,7 @@ def self.unpack_id(collection, packed_id, with_key: false) primary_keys = ForestAdminDatasourceToolkit::Utils::Schema.primary_keys(collection) primary_key_values = packed_id.to_s.split('|') if (nb_pks = primary_keys.size) != (nb_values = primary_key_values.size) - raise Exceptions::ForestException, "Expected $primaryKeyNames a size of #{nb_pks} values, found #{nb_values}" + raise Exceptions::ForestException, "Expected #{nb_pks} primary keys, found #{nb_values}" end result = primary_keys.map.with_index do |pk_name, index| diff --git a/packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/id_spec.rb b/packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/id_spec.rb new file mode 100644 index 000000000..796d6fc21 --- /dev/null +++ b/packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/id_spec.rb @@ -0,0 +1,146 @@ +require 'spec_helper' +require 'shared/caller' + +module ForestAdminAgent + module Utils + include ForestAdminDatasourceToolkit + include ForestAdminDatasourceToolkit::Schema + + describe Id do + let(:datasource) do + datasource = Datasource.new + collection_person = Collection.new(datasource, 'person') + collection_person.add_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') + } + ) + + collection_pks = Collection.new(datasource, 'pks') + collection_pks.add_fields( + { + 'key1' => ColumnSchema.new(column_type: 'Number', is_primary_key: true), + 'key2' => ColumnSchema.new(column_type: 'Number', is_primary_key: true) + } + ) + + collection_foo = Collection.new(datasource, 'foo') + collection_foo.add_fields( + { + 'name' => ColumnSchema.new(column_type: 'String') + } + ) + + datasource.add_collection(collection_person) + datasource.add_collection(collection_pks) + datasource.add_collection(collection_foo) + + datasource + end + + describe 'unpack_id' do + context 'when collection has one pk' do + it 'return the list of id value' do + collection = datasource.collection('person') + expect(described_class.unpack_id(collection, 1)).to eq([1]) + end + + it 'raise when not expected number of pks is unpack' do + collection = datasource.collection('person') + + expect do + expect(described_class.unpack_id(collection, '1|foo')) + end.to raise_error( + ForestAdminDatasourceToolkit::Exceptions::ForestException, + '🌳🌳🌳 Expected 1 primary keys, found 2' + ) + end + end + + context 'when collection has multiple pks' do + it 'return the list of id value' do + collection = datasource.collection('pks') + expect(described_class.unpack_id(collection, '1|1')).to eq([1, 1]) + end + + it 'raise when not expected number of pks is unpack' do + collection = datasource.collection('pks') + + expect do + expect(described_class.unpack_id(collection, '1')) + end.to raise_error( + ForestAdminDatasourceToolkit::Exceptions::ForestException, + '🌳🌳🌳 Expected 2 primary keys, found 1' + ) + end + end + end + + describe 'unpack_ids' do + it 'return an array of list id values' do + collection = datasource.collection('pks') + expect(described_class.unpack_ids(collection, ['1|1'])).to eq([[1, 1]]) + end + end + + describe 'parse_selection_ids' do + it 'return a hash with excluded_ids' do + collection = datasource.collection('person') + args = { + 'data' => { + 'attributes' => { + 'ids' => %w[1 2 3], + 'collection_name' => 'User', + 'parent_collection_name' => nil, + 'parent_collection_id' => nil, + 'parent_association_name' => nil, + 'all_records' => true, + 'all_records_subset_query' => [ + 'fields[Car]' => 'id,first_name,last_name', + 'page[number]' => 1, + 'page[size]' => 15 + ], + 'all_records_ids_excluded' => ['4'], + 'smart_action_id' => nil + } + } + } + + expect(described_class.parse_selection_ids(collection, args)).to eq({ are_excluded: true, ids: [[4]] }) + end + + it 'return a hash with ids' do + collection = datasource.collection('person') + args = { + 'data' => { + 'attributes' => { + 'ids' => %w[1 2 3], + 'collection_name' => 'User', + 'parent_collection_name' => nil, + 'parent_collection_id' => nil, + 'parent_association_name' => nil, + 'all_records' => false, + 'all_records_subset_query' => [ + 'fields[Car]' => 'id,first_name,last_name', + 'page[number]' => 1, + 'page[size]' => 15 + ], + 'all_records_ids_excluded' => ['4'], + 'smart_action_id' => nil + } + } + } + + expect(described_class.parse_selection_ids(collection, args)).to eq( + { + are_excluded: false, + ids: [[1], [2], [3]] + } + ) + end + end + end + end +end From 0b6e7547cce65a33ef74e1f4eb0ddd52ff08ae16 Mon Sep 17 00:00:00 2001 From: Nicolas Alexandre Date: Fri, 27 Oct 2023 11:10:27 +0200 Subject: [PATCH 18/18] chore: lint --- .../spec/lib/forest_admin_agent/utils/id_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/id_spec.rb b/packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/id_spec.rb index 796d6fc21..5dc3449af 100644 --- a/packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/id_spec.rb +++ b/packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/id_spec.rb @@ -51,7 +51,7 @@ module Utils collection = datasource.collection('person') expect do - expect(described_class.unpack_id(collection, '1|foo')) + described_class.unpack_id(collection, '1|foo') end.to raise_error( ForestAdminDatasourceToolkit::Exceptions::ForestException, '🌳🌳🌳 Expected 1 primary keys, found 2' @@ -69,7 +69,7 @@ module Utils collection = datasource.collection('pks') expect do - expect(described_class.unpack_id(collection, '1')) + described_class.unpack_id(collection, '1') end.to raise_error( ForestAdminDatasourceToolkit::Exceptions::ForestException, '🌳🌳🌳 Expected 2 primary keys, found 1'