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
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@ def setup_routes
def handle_request(args = {})
context = build(args)
context.permissions.can?(:add, context.collection)
relations = authorized_linked_one_to_one_relations(args, context)
data = format_attributes(args, context.collection)
record = context.collection.create(context.caller, data)
link_one_to_one_relations(args, record, context)
link_one_to_one_relations(relations, record, context)
Comment thread
hercemer42 marked this conversation as resolved.
id = ForestAdminDatasourceToolkit::Utils::Record.primary_keys(context.collection, record)
filter = ForestAdminDatasourceToolkit::Components::Query::Filter.new(
condition_tree: ConditionTree::ConditionTreeFactory.match_ids(context.collection, [id])
Expand All @@ -37,24 +38,63 @@ def handle_request(args = {})
}
end

def link_one_to_one_relations(args, record, context)
args[:params][:data][:relationships]&.map do |field, value|
schema = context.collection.schema[:fields][field]
next unless %w[OneToOne PolymorphicOneToOne].include?(schema.type)
private

primary_key_values = Utils::Id.unpack_id(context.collection, value['data']['id'], with_key: true)
foreign_collection = context.datasource.get_collection(schema.foreign_collection)
# Load the value that will be used as origin_key
origin_value = record[schema.origin_key_target]
def authorized_linked_one_to_one_relations(args, context)
linked_one_to_one_relations(args, context).map do |relation|
foreign_collection = relation[:foreign_collection]

# update new relation (may update zero or one records).
patch = { schema.origin_key => origin_value }
context.permissions.can?(:edit, foreign_collection)

relation.merge(
scope: context.permissions.get_scope(foreign_collection),
primary_key_values: Utils::Id.unpack_id(foreign_collection, relation[:id], with_key: true)
)
end
end

def linked_one_to_one_relations(args, context)
relationships = args.dig(:params, :data, :relationships) || {}

relationships.filter_map { |field, value| linked_one_to_one_relation(field, value, context) }
end

def linked_one_to_one_relation(field, value, context)
schema = context.collection.schema[:fields][field]
return unless %w[OneToOne PolymorphicOneToOne].include?(schema.type)

id = value.dig('data', 'id')
return if id.nil?

{
schema: schema,
foreign_collection: context.datasource.get_collection(schema.foreign_collection),
id: id
}
end

def link_one_to_one_relations(relations, record, context)
relations.each do |relation|
schema = relation[:schema]
foreign_collection = relation[:foreign_collection]

patch = { schema.origin_key => record[schema.origin_key_target] }
if schema.type == 'PolymorphicOneToOne'
patch[schema.origin_type_field] =
context.collection.name.gsub('__', '::')
end
condition_tree = ConditionTree::ConditionTreeFactory.match_records(foreign_collection, [primary_key_values])
filter = Filter.new(condition_tree: condition_tree)

new_fk_owner = ConditionTree::ConditionTreeFactory.match_records(
foreign_collection, [relation[:primary_key_values]]
)
filter = Filter.new(
condition_tree: ConditionTree::ConditionTreeFactory.intersect(
Comment thread
hercemer42 marked this conversation as resolved.
[
relation[:scope],
new_fk_owner
]
)
)
foreign_collection.update(context.caller, filter, patch)
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ module Resources

before do
allow(ForestAdminAgent::Services::Permissions).to receive(:new).and_return(permissions)
allow(permissions).to receive(:can?).and_return(true)
allow(permissions).to receive_messages(can?: true, get_scope: nil)
end

it 'adds the route forest_store' do
Expand Down Expand Up @@ -187,7 +187,10 @@ def respond_to?(arg)
is_primary_key: true,
filter_operators: [Operators::IN, Operators::EQUAL]
),
'person_id' => ColumnSchema.new(column_type: 'Number'),
'person_id' => ColumnSchema.new(
column_type: 'Number',
filter_operators: [Operators::IN, Operators::EQUAL, Operators::NOT_EQUAL]
),
'person' => Relations::ManyToOneSchema.new(
foreign_key: 'person_id',
foreign_key_target: 'id',
Expand All @@ -201,8 +204,20 @@ def respond_to?(arg)
allow(ForestAdminAgent::Builder::AgentFactory.instance).to receive(:send_schema).and_return(nil)
ForestAdminAgent::Builder::AgentFactory.instance.add_datasource(@datasource)
ForestAdminAgent::Builder::AgentFactory.instance.build

allow(permissions).to receive(:can?) do |action, collection|
checked_permissions << [action, collection.name]
raise ForestAdminAgent::Http::Exceptions::ForbiddenError if denied_permissions.include?(
[action, collection.name]
)

true
end
end

let(:checked_permissions) { [] }
let(:denied_permissions) { [] }

describe 'with one to one relation' do
it 'call create and return an serialized content' do
args[:params][:data] = {
Expand All @@ -220,6 +235,7 @@ def respond_to?(arg)
)

result = store.handle_request(args)
expect(checked_permissions).to eq([[:add, 'person'], [:edit, 'passport']])
expect(@datasource.get_collection('person')).to have_received(:create) do |caller, data|
expect(caller).to be_instance_of(Components::Caller)
expect(data).to eq({ 'name' => 'john' })
Expand All @@ -245,6 +261,211 @@ def respond_to?(arg)
}
)
end

it 'authorizes the edit on the foreign collection and intersects its scope' do
args[:params][:data] = {
attributes: { 'name' => 'john' },
relationships: { 'passport' => { 'data' => { 'type' => 'passports', 'id' => 1 } } },
type: 'persons'
}
args[:params]['collection_name'] = 'person'
allow(permissions).to receive(:get_scope) do |collection|
Nodes::ConditionTreeLeaf.new('person_id', Operators::NOT_EQUAL, 99) if collection.name == 'passport'
end
allow(@datasource.get_collection('person')).to receive_messages(
create: { 'id' => 1, 'name' => 'john' },
list: [{ 'id' => 1, 'name' => 'john' }]
)

store.handle_request(args)

expect(checked_permissions).to eq([[:add, 'person'], [:edit, 'passport']])
expect(@datasource.get_collection('passport')).to have_received(:update) do |_caller, filter, _data|
expect(filter.condition_tree.to_h).to eq(
{
aggregator: 'And',
conditions: [
{ field: 'person_id', operator: Operators::NOT_EQUAL, value: 99 },
{ field: 'id', operator: Operators::EQUAL, value: 1 }
]
}
)
end
end

it 'creates nothing when the edit is denied on the foreign collection' do
denied_permissions << [:edit, 'passport']
args[:params][:data] = {
attributes: { 'name' => 'john' },
relationships: { 'passport' => { 'data' => { 'type' => 'passports', 'id' => 1 } } },
type: 'persons'
}
args[:params]['collection_name'] = 'person'

expect { store.handle_request(args) }
.to raise_error(ForestAdminAgent::Http::Exceptions::ForbiddenError)
expect(@datasource.get_collection('person')).not_to have_received(:create)
expect(@datasource.get_collection('passport')).not_to have_received(:update)
end

it 'checks the edit permission before parsing the linked id' do
denied_permissions << [:edit, 'passport']
args[:params][:data] = {
attributes: { 'name' => 'john' },
relationships: { 'passport' => { 'data' => { 'type' => 'passports', 'id' => 'malformed|id' } } },
type: 'persons'
}
args[:params]['collection_name'] = 'person'

expect { store.handle_request(args) }
.to raise_error(ForestAdminAgent::Http::Exceptions::ForbiddenError)
end

it 'creates nothing when the linked id is malformed' do
args[:params][:data] = {
attributes: { 'name' => 'john' },
relationships: { 'passport' => { 'data' => { 'type' => 'passports', 'id' => 'malformed|id' } } },
type: 'persons'
}
args[:params]['collection_name'] = 'person'

expect { store.handle_request(args) }
.to raise_error(ForestAdminDatasourceToolkit::Exceptions::ForestException)
expect(@datasource.get_collection('person')).not_to have_received(:create)
expect(@datasource.get_collection('passport')).not_to have_received(:update)
end

it 'creates nothing when the scope cannot be resolved' do
args[:params][:data] = {
attributes: { 'name' => 'john' },
relationships: { 'passport' => { 'data' => { 'type' => 'passports', 'id' => 1 } } },
type: 'persons'
}
args[:params]['collection_name'] = 'person'
allow(permissions).to receive(:get_scope) do |collection|
raise ForestAdminDatasourceToolkit::Exceptions::ForestException, 'boom' if collection.name == 'passport'
end

expect { store.handle_request(args) }
.to raise_error(ForestAdminDatasourceToolkit::Exceptions::ForestException)
expect(@datasource.get_collection('person')).not_to have_received(:create)
expect(@datasource.get_collection('passport')).not_to have_received(:update)
end

it 'drops the link silently when the scope excludes the linked record' do
args[:params][:data] = {
attributes: { 'name' => 'john' },
relationships: { 'passport' => { 'data' => { 'type' => 'passports', 'id' => 1 } } },
type: 'persons'
}
args[:params]['collection_name'] = 'person'
allow(permissions).to receive(:get_scope) do |collection|
Nodes::ConditionTreeLeaf.new('person_id', Operators::EQUAL, 99) if collection.name == 'passport'
end
allow(@datasource.get_collection('person')).to receive_messages(
create: { 'id' => 1, 'name' => 'john' },
list: [{ 'id' => 1, 'name' => 'john' }]
)
allow(@datasource.get_collection('passport')).to receive(:update).and_return(nil)

result = store.handle_request(args)

expect(result[:content]['data']['id']).to eq('1')
expect(@datasource.get_collection('person')).to have_received(:create)
expect(@datasource.get_collection('passport')).to have_received(:update) do |_caller, filter, _data|
expect(filter.condition_tree.to_h).to eq(
{
aggregator: 'And',
conditions: [
{ field: 'person_id', operator: Operators::EQUAL, value: 99 },
{ field: 'id', operator: Operators::EQUAL, value: 1 }
]
}
)
end
end

it 'ignores a one to one relationship carrying no data' do
args[:params][:data] = {
attributes: { 'name' => 'john' },
relationships: { 'passport' => { 'data' => nil } },
type: 'persons'
}
args[:params]['collection_name'] = 'person'
allow(@datasource.get_collection('person')).to receive_messages(
create: { 'id' => 1, 'name' => 'john' },
list: [{ 'id' => 1, 'name' => 'john' }]
)

store.handle_request(args)

expect(checked_permissions).to eq([[:add, 'person']])
expect(@datasource.get_collection('passport')).not_to have_received(:update)
end
end

describe 'with polymorphic one to one relation' do
before do
collection_address = build_collection(
name: 'address',
schema: {
fields: {
'reference' => ColumnSchema.new(
column_type: 'String',
is_primary_key: true,
filter_operators: [Operators::IN, Operators::EQUAL]
),
'location' => ColumnSchema.new(
column_type: 'String',
filter_operators: [Operators::IN, Operators::EQUAL]
),
'addressable_id' => ColumnSchema.new(column_type: 'Number'),
'addressable_type' => ColumnSchema.new(column_type: 'String')
}
}
)
@datasource.add_collection(collection_address)
@datasource.get_collection('person').schema[:fields]['address'] =
Relations::PolymorphicOneToOneSchema.new(
origin_key: 'addressable_id',
origin_key_target: 'id',
foreign_collection: 'address',
origin_type_field: 'addressable_type',
origin_type_value: 'Person::Legacy'
)
end

it 'authorizes the edit, intersects the scope and matches the foreign primary key' do
args[:params][:data] = {
attributes: { 'name' => 'john' },
relationships: { 'address' => { 'data' => { 'type' => 'addresses', 'id' => 'ref-1' } } },
type: 'persons'
}
args[:params]['collection_name'] = 'person'
allow(permissions).to receive(:get_scope) do |collection|
Nodes::ConditionTreeLeaf.new('location', Operators::EQUAL, 'paris') if collection.name == 'address'
end
allow(@datasource.get_collection('person')).to receive_messages(
create: { 'id' => 1, 'name' => 'john' },
list: [{ 'id' => 1, 'name' => 'john' }]
)

store.handle_request(args)

expect(checked_permissions).to eq([[:add, 'person'], [:edit, 'address']])
expect(@datasource.get_collection('address')).to have_received(:update) do |_caller, filter, data|
expect(data).to eq({ 'addressable_id' => 1, 'addressable_type' => 'person' })
expect(filter.condition_tree.to_h).to eq(
{
aggregator: 'And',
conditions: [
{ field: 'location', operator: Operators::EQUAL, value: 'paris' },
{ field: 'reference', operator: Operators::EQUAL, value: 'ref-1' }
]
}
)
end
end
end

describe 'with many to one relation' do
Expand Down
Loading