Skip to content

Commit

Permalink
feat: add all writing operations (#9)
Browse files Browse the repository at this point in the history
  • Loading branch information
nicolasalexandre9 authored Oct 27, 2023
1 parent c78cfe4 commit 3fd6a7a
Show file tree
Hide file tree
Showing 21 changed files with 928 additions and 39 deletions.
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

0 comments on commit 3fd6a7a

Please sign in to comment.