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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,19 @@ 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/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'
- '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'
- '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:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
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
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,12 @@ 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::Count.new.routes
Resources::Show.new.routes,
Resources::Store.new.routes,
Resources::Update.new.routes
].inject(&:merge)
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,21 @@ 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

def format_attributes(args)
record = args[:params][:data][:attributes]

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'
end

record
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
require 'jsonapi-serializers'
require 'ostruct'

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
Original file line number Diff line number Diff line change
@@ -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'], 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)
)
projection = ProjectionFactory.all(@collection)

records = @collection.list(caller, filter, projection)

raise Http::Exceptions::NotFoundError, 'Record does not exists' unless records.size.positive?

{
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
require 'jsonapi-serializers'
require 'ostruct'

module ForestAdminAgent
module Routes
module Resources
class Store < AbstractAuthenticatedRoute
include ForestAdminAgent::Builder
def setup_routes
add_route('forest_store', 'post', '/:collection_name', ->(args) { handle_request(args) })

self
end

def handle_request(args = {})
build(args)
data = format_attributes(args)
record = @collection.create(@caller, data)
link_one_to_one_relations(args, record)

{
name: args[:params]['collection_name'],
content: JSONAPI::Serializer.serialize(
record,
is_collection: false,
serializer: Serializer::ForestSerializer
)
}
end

def link_one_to_one_relations(args, record)
relations = {}

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)
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
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
require 'jsonapi-serializers'
require 'ostruct'

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
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand All @@ -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)
Expand Down
39 changes: 39 additions & 0 deletions packages/forest_admin_agent/lib/forest_admin_agent/utils/id.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
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('|')
if (nb_pks = primary_keys.size) != (nb_values = primary_key_values.size)
raise Exceptions::ForestException, "Expected #{nb_pks} primary keys, 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

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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading