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 @@ -15,11 +15,11 @@ def add_action(name, definition)
end

def schema
@stack.datasource.get_collection(@name).schema
@stack.validation.get_collection(@name).schema
end

def collection
@stack.datasource.get_collection(@name)
@stack.validation.get_collection(@name)
end

def disable_count
Expand Down Expand Up @@ -129,6 +129,18 @@ def add_external_relation(name, definition)
use(ForestAdminDatasourceCustomizer::Plugins::AddExternalRelation, { name: name }.merge(definition))
end

# Add a new validator to the edition form of a given field
# @param name The name of the field
# @param operator The validator that you wish to add
# @param value A configuration value that the validator may need
# @example
# .add_field_validation('first_name', Operators::LONGER_THAN, 2)
def add_field_validation(name, operator, value = nil)
push_customization(
proc { @stack.validation.get_collection(@name).add_validation(name, { operator: operator, value: value }) }
)
end

private

def push_customization(customization)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module Decorators
class DecoratorsStack
include ForestAdminDatasourceToolkit::Decorators

attr_reader :datasource, :schema, :search, :early_computed, :late_computed, :action, :relation
attr_reader :datasource, :schema, :search, :early_computed, :late_computed, :action, :relation, :validation

def initialize(datasource)
@customizations = []
Expand All @@ -19,6 +19,7 @@ def initialize(datasource)
last = @search = DatasourceDecorator.new(last, Search::SearchCollectionDecorator)
last = @action = DatasourceDecorator.new(last, Action::ActionCollectionDecorator)
last = @schema = DatasourceDecorator.new(last, Schema::SchemaCollectionDecorator)
last = @validation = DatasourceDecorator.new(last, Validation::ValidationCollectionDecorator)
@datasource = last
end

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
module ForestAdminDatasourceCustomizer
module Decorators
module Validation
class ValidationCollectionDecorator < ForestAdminDatasourceToolkit::Decorators::CollectionDecorator
include ForestAdminDatasourceToolkit::Validations
include ForestAdminDatasourceToolkit::Components::Query::ConditionTree
include ForestAdminDatasourceToolkit::Exceptions
attr_reader :validation

def initialize(child_collection, datasource)
super
@validation = {}
end

def add_validation(name, validation)
FieldValidator.validate(self, name)

field = @child_collection.schema[:fields][name]
if field.nil? || field.type != 'Column'
raise ForestException,
'Cannot add validators on a relation, use the foreign key instead'
end
raise ForestException, 'Cannot add validators on a readonly field' if field.is_read_only

@validation[name] ||= []
@validation[name].push(validation)
mark_schema_as_dirty
end

def create(caller, data)
data.each { |record| validate(record, caller.timezone, true) }
child_collection.create(caller, data)
end

def update(caller, filter, patch)
validate(patch, caller.timezone, false)
child_collection.update(caller, filter, patch)
end

def refine_schema(child_schema)
@validation.each do |name, rules|
field = child_schema[:fields][name]
field.validations = (field.validations || []).concat(rules)
child_schema[:fields][name] = field
end

child_schema
end

private

def validate(record, timezone, all_fields)
@validation.each do |name, rules|
next unless all_fields || record.key?(name)

# When setting a field to nil, only the "Present" validator is relevant
applicable_rules = record[name].nil? ? rules.select { |r| r[:operator] == Operators::PRESENT } : rules

applicable_rules.each do |validator|
raw_leaf = { field: name }.merge(validator)
tree = ConditionTreeFactory.from_plain_object(raw_leaf)
next if tree.match(record, self, timezone)

message = "#{name} failed validation rule :"
rule = if validator.key?(:value)
"#{validator[:operator]}(#{if validator[:value].is_a?(Array)
validator[:value].join(",")
else
validator[:value]
end})"
else
validator[:operator]
end

raise ForestAdminDatasourceToolkit::Exceptions::ValidationError, "#{message} #{rule}"
end
end
end
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -306,5 +306,21 @@ module ForestAdminDatasourceCustomizer
expect { @datasource_customizer.datasource({}) }.to raise_error(ForestAdminDatasourceToolkit::Exceptions::ForestException, '🌳🌳🌳 The options parameter must contains the following keys: `name, schema, listRecords`')
end
end

context 'when adding a field validation' do
it 'adds a validation rule' do
stack = @datasource_customizer.stack
allow(stack.validation).to receive(:get_collection).with('book').and_return(@datasource_customizer.stack.validation.get_collection('book'))

customizer = described_class.new(@datasource_customizer, @datasource_customizer.stack, 'book')
customizer.add_field_validation('title', Operators::LONGER_THAN, 5)
@datasource_customizer.datasource({})

validation_collection = @datasource_customizer.stack.validation.get_collection('book')

expect(validation_collection.validation).to have_key('title')
expect(validation_collection.validation['title']).to eq([{ operator: Operators::LONGER_THAN, value: 5 }])
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
require 'spec_helper'
require 'shared/caller'

module ForestAdminDatasourceCustomizer
module Decorators
module Validation
include ForestAdminDatasourceToolkit::Schema
include ForestAdminDatasourceToolkit::Components::Query
include ForestAdminDatasourceToolkit::Components::Query::ConditionTree
include ForestAdminDatasourceToolkit::Exceptions

describe ValidationCollectionDecorator do
include_context 'with caller'
let(:datasource) { ForestAdminDatasourceToolkit::Datasource.new }

before do
@collection_book = instance_double(
ForestAdminDatasourceToolkit::Collection,
name: 'book',
schema: {
fields: {
'id' => ColumnSchema.new(column_type: 'Number', is_primary_key: true, is_read_only: true),
'author_id' => ColumnSchema.new(column_type: 'String'),
'author' => Relations::ManyToOneSchema.new(
foreign_key: 'author_id',
foreign_collection: 'person',
foreign_key_target: 'id'
),
'title' => ColumnSchema.new(column_type: 'String', filter_operators: [Operators::LONGER_THAN, Operators::PRESENT]),
'sub_title' => ColumnSchema.new(column_type: 'String', filter_operators: [Operators::LONGER_THAN])
}
},
datasource: datasource
)

@collection_person = instance_double(
ForestAdminDatasourceToolkit::Collection,
name: 'person',
schema: {
fields: {
'id' => ColumnSchema.new(column_type: 'Number', is_primary_key: true),
'first_name' => ColumnSchema.new(column_type: 'String'),
'last_name' => ColumnSchema.new(column_type: 'String'),
'book' => Relations::OneToOneSchema.new(
origin_key: 'author_id',
origin_key_target: 'id',
foreign_collection: 'book'
)
}
},
datasource: datasource
)

datasource.add_collection(@collection_book)
datasource.add_collection(@collection_person)

datasource_decorator = ForestAdminDatasourceToolkit::Decorators::DatasourceDecorator.new(datasource, described_class)

@decorated_book = datasource_decorator.get_collection('book')
@decorated_person = datasource_decorator.get_collection('person')
end

it 'addValidation() should throw if the field does not exists' do
expect { @decorated_book.add_validation('__dontExist', { operator: Operators::PRESENT }) }.to raise_error(ValidationError, "🌳🌳🌳 Column not found: 'book.__dontExist'")
end

it 'addValidation() should throw if the field is readonly' do
expect { @decorated_book.add_validation('id', { operator: Operators::PRESENT }) }.to raise_error(ForestException, '🌳🌳🌳 Cannot add validators on a readonly field')
end

it 'addValidation() should throw if the field is a relation' do
expect { @decorated_book.add_validation('author', { operator: Operators::PRESENT }) }.to raise_error(ValidationError, "🌳🌳🌳 Unexpected field type: 'book.author' (found 'ManyToOne' expected 'Column')")
end

it 'addValidation() should throw if the field is in a relation' do
expect { @decorated_book.add_validation('author:first_name', { operator: Operators::PRESENT }) }.to raise_error(ForestException, '🌳🌳🌳 Cannot add validators on a relation, use the foreign key instead')
end

context 'with field selection when validating' do
before do
@decorated_book.add_validation('title', { operator: Operators::LONGER_THAN, value: 5 })
@decorated_book.add_validation('sub_title', { operator: Operators::LONGER_THAN, value: 5 })
end

it 'validates all fields when creating a record' do
allow(@collection_book).to receive(:create).and_return(nil)

expect { @decorated_book.create(caller, [{ 'title' => 'longtitle', 'sub_title' => '' }]) }.to raise_error(ValidationError, '🌳🌳🌳 sub_title failed validation rule : longer_than(5)')
end

it 'validates only changed fields when updating' do
allow(@decorated_book).to receive(:update).and_return(nil)
@decorated_book.update(caller, Filter.new, { 'title' => 'longtitle' })

expect(@decorated_book).to have_received(:update)
end
end

context 'with validation when setting to null (null allowed)' do
before do
@decorated_book.add_validation('title', { operator: Operators::LONGER_THAN, value: 5 })
end

it 'forwards create that respect the rule' do
allow(@decorated_book).to receive(:create).and_return(nil)

expect(@decorated_book.create(caller, [{ title: nil }])).to be_nil
end
end

context 'with validation on a defined value' do
before do
@decorated_book.add_validation('title', { operator: Operators::LONGER_THAN, value: 5 })
end

it 'forwards create that respect the rule' do
allow(@collection_book).to receive(:create).and_return(nil)
@decorated_book.create(caller, [{ title: '123456' }])

expect(@collection_book).to have_received(:create)
end

it 'forwards updates that respect the rule' do
allow(@collection_book).to receive(:update).and_return(nil)
@decorated_book.update(caller, Filter.new, { title: '123456' })

expect(@collection_book).to have_received(:update)
end

it 'rejects create that do not respect the rule' do
allow(@collection_book).to receive(:create).and_return(nil)

expect { @decorated_book.create(caller, [{ 'title' => '1234' }]) }.to raise_error(ValidationError, '🌳🌳🌳 title failed validation rule : longer_than(5)')
end

it 'rejects updates that do not respect the rule' do
allow(@collection_book).to receive(:update).and_return(true)

expect { @decorated_book.update(caller, Filter.new, { 'title' => '1234' }) }.to raise_error(ValidationError, '🌳🌳🌳 title failed validation rule : longer_than(5)')
end
end
end
end
end
end