forest_admin_datasource_customizer 1.0.0.pre.beta.21 → 1.0.0.pre.beta.58
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/forest_admin_datasource_customizer.gemspec +3 -2
- data/lib/forest_admin_datasource_customizer/collection_customizer.rb +292 -5
- data/lib/forest_admin_datasource_customizer/context/agent_customization_context.rb +18 -0
- data/lib/forest_admin_datasource_customizer/context/collection_customization_context.rb +15 -0
- data/lib/forest_admin_datasource_customizer/context/relaxed_wrappers/relaxed_collection.rb +50 -0
- data/lib/forest_admin_datasource_customizer/context/relaxed_wrappers/relaxed_data_source.rb +18 -0
- data/lib/forest_admin_datasource_customizer/datasource_customizer.rb +43 -13
- data/lib/forest_admin_datasource_customizer/decorators/action/action_collection_decorator.rb +137 -0
- data/lib/forest_admin_datasource_customizer/decorators/action/base_action.rb +67 -0
- data/lib/forest_admin_datasource_customizer/decorators/action/context/action_context.rb +56 -0
- data/lib/forest_admin_datasource_customizer/decorators/action/context/action_context_single.rb +26 -0
- data/lib/forest_admin_datasource_customizer/decorators/action/dynamic_field.rb +50 -0
- data/lib/forest_admin_datasource_customizer/decorators/action/result_builder.rb +68 -0
- data/lib/forest_admin_datasource_customizer/decorators/action/types/action_scope.rb +15 -0
- data/lib/forest_admin_datasource_customizer/decorators/action/types/field_type.rb +35 -0
- data/lib/forest_admin_datasource_customizer/decorators/action/widget_field.rb +357 -0
- data/lib/forest_admin_datasource_customizer/decorators/binary/binary_collection_decorator.rb +215 -0
- data/lib/forest_admin_datasource_customizer/decorators/binary/binary_helper.rb +17 -0
- data/lib/forest_admin_datasource_customizer/decorators/chart/chart_collection_decorator.rb +41 -0
- data/lib/forest_admin_datasource_customizer/decorators/chart/chart_context.rb +33 -0
- data/lib/forest_admin_datasource_customizer/decorators/chart/chart_datasource_decorator.rb +46 -0
- data/lib/forest_admin_datasource_customizer/decorators/chart/result_builder.rb +148 -0
- data/lib/forest_admin_datasource_customizer/decorators/computed/compute_collection_decorator.rb +115 -0
- data/lib/forest_admin_datasource_customizer/decorators/computed/computed_definition.rb +21 -0
- data/lib/forest_admin_datasource_customizer/decorators/computed/utils/computed_field.rb +74 -0
- data/lib/forest_admin_datasource_customizer/decorators/computed/utils/flattener.rb +49 -0
- data/lib/forest_admin_datasource_customizer/decorators/decorators_stack.rb +33 -4
- data/lib/forest_admin_datasource_customizer/decorators/hook/context/after/hook_after_aggregate_context.rb +18 -0
- data/lib/forest_admin_datasource_customizer/decorators/hook/context/after/hook_after_create_context.rb +18 -0
- data/lib/forest_admin_datasource_customizer/decorators/hook/context/after/hook_after_delete_context.rb +12 -0
- data/lib/forest_admin_datasource_customizer/decorators/hook/context/after/hook_after_list_context.rb +18 -0
- data/lib/forest_admin_datasource_customizer/decorators/hook/context/after/hook_after_update_context.rb +12 -0
- data/lib/forest_admin_datasource_customizer/decorators/hook/context/before/hook_before_aggregate_context.rb +20 -0
- data/lib/forest_admin_datasource_customizer/decorators/hook/context/before/hook_before_create_context.rb +18 -0
- data/lib/forest_admin_datasource_customizer/decorators/hook/context/before/hook_before_delete_context.rb +18 -0
- data/lib/forest_admin_datasource_customizer/decorators/hook/context/before/hook_before_list_context.rb +19 -0
- data/lib/forest_admin_datasource_customizer/decorators/hook/context/before/hook_before_update_context.rb +19 -0
- data/lib/forest_admin_datasource_customizer/decorators/hook/context/hook_context.rb +22 -0
- data/lib/forest_admin_datasource_customizer/decorators/hook/hook_collection_decorator.rb +95 -0
- data/lib/forest_admin_datasource_customizer/decorators/hook/hooks.rb +26 -0
- data/lib/forest_admin_datasource_customizer/decorators/operators_emulate/operators_emulate_collection_decorator.rb +118 -0
- data/lib/forest_admin_datasource_customizer/decorators/operators_equivalence/operators_equivalence_collection_decorator.rb +50 -0
- data/lib/forest_admin_datasource_customizer/decorators/override/context/create_override_customization_context.rb +16 -0
- data/lib/forest_admin_datasource_customizer/decorators/override/context/delete_override_customization_context.rb +16 -0
- data/lib/forest_admin_datasource_customizer/decorators/override/context/update_override_customization_context.rb +17 -0
- data/lib/forest_admin_datasource_customizer/decorators/override/override_collection_decorator.rb +49 -0
- data/lib/forest_admin_datasource_customizer/decorators/publication/publication_collection_decorator.rb +95 -0
- data/lib/forest_admin_datasource_customizer/decorators/publication/publication_datasource_decorator.rb +57 -0
- data/lib/forest_admin_datasource_customizer/decorators/relation/relation_collection_decorator.rb +268 -0
- data/lib/forest_admin_datasource_customizer/decorators/rename_collection/rename_collection_datasource_decorator.rb +70 -0
- data/lib/forest_admin_datasource_customizer/decorators/rename_collection/rename_collection_decorator.rb +37 -0
- data/lib/forest_admin_datasource_customizer/decorators/rename_field/rename_field_collection_decorator.rb +190 -0
- data/lib/forest_admin_datasource_customizer/decorators/schema/schema_collection_decorator.rb +21 -0
- data/lib/forest_admin_datasource_customizer/decorators/search/search_collection_decorator.rb +135 -0
- data/lib/forest_admin_datasource_customizer/decorators/segment/segment_collection_decorator.rb +60 -0
- data/lib/forest_admin_datasource_customizer/decorators/sort/sort_collection_decorator.rb +127 -0
- data/lib/forest_admin_datasource_customizer/decorators/validation/validation_collection_decorator.rb +82 -0
- data/lib/forest_admin_datasource_customizer/decorators/write/create_relations/create_relations_collection_decorator.rb +75 -0
- data/lib/forest_admin_datasource_customizer/decorators/write/update_relations/update_relations_collection_decorator.rb +96 -0
- data/lib/forest_admin_datasource_customizer/decorators/write/write_datasource_decorator.rb +14 -0
- data/lib/forest_admin_datasource_customizer/decorators/write/write_replace/write_customization_context.rb +18 -0
- data/lib/forest_admin_datasource_customizer/decorators/write/write_replace/write_replace_collection_decorator.rb +125 -0
- data/lib/forest_admin_datasource_customizer/plugins/add_external_relation.rb +27 -0
- data/lib/forest_admin_datasource_customizer/plugins/import_field.rb +74 -0
- data/lib/forest_admin_datasource_customizer/version.rb +1 -1
- metadata +84 -5
- data/README.md +0 -31
@@ -0,0 +1,190 @@
|
|
1
|
+
module ForestAdminDatasourceCustomizer
|
2
|
+
module Decorators
|
3
|
+
module RenameField
|
4
|
+
class RenameFieldCollectionDecorator < ForestAdminDatasourceToolkit::Decorators::CollectionDecorator
|
5
|
+
include ForestAdminDatasourceToolkit::Decorators
|
6
|
+
include ForestAdminDatasourceToolkit::Components::Query::ConditionTree
|
7
|
+
|
8
|
+
attr_accessor :from_child_collection, :to_child_collection
|
9
|
+
|
10
|
+
def initialize(child_collection, datasource)
|
11
|
+
super
|
12
|
+
@from_child_collection = {}
|
13
|
+
@to_child_collection = {}
|
14
|
+
end
|
15
|
+
|
16
|
+
def rename_field(current_name, new_name)
|
17
|
+
unless schema[:fields][current_name]
|
18
|
+
raise ForestAdminDatasourceToolkit::Exceptions::ForestException, "No such field '#{current_name}'"
|
19
|
+
end
|
20
|
+
|
21
|
+
initial_name = current_name
|
22
|
+
|
23
|
+
ForestAdminDatasourceToolkit::Validations::FieldValidator.validate_name(name, new_name)
|
24
|
+
|
25
|
+
# Revert previous renaming (avoids conflicts and need to recurse on @to_child_collection).
|
26
|
+
if to_child_collection[current_name]
|
27
|
+
child_name = to_child_collection[current_name]
|
28
|
+
to_child_collection.delete(current_name)
|
29
|
+
from_child_collection.delete(child_name)
|
30
|
+
initial_name = child_name
|
31
|
+
mark_all_schema_as_dirty
|
32
|
+
end
|
33
|
+
|
34
|
+
# Do not update arrays if renaming is a no-op (ie: customer is cancelling a previous rename)
|
35
|
+
return unless initial_name != new_name
|
36
|
+
|
37
|
+
from_child_collection[initial_name] = new_name
|
38
|
+
to_child_collection[new_name] = initial_name
|
39
|
+
mark_all_schema_as_dirty
|
40
|
+
end
|
41
|
+
|
42
|
+
def refine_schema(sub_schema)
|
43
|
+
fields = {}
|
44
|
+
|
45
|
+
sub_schema[:fields].each do |old_name, old_schema|
|
46
|
+
case old_schema.type
|
47
|
+
when 'ManyToOne'
|
48
|
+
old_schema.foreign_key = from_child_collection[old_schema.foreign_key] || old_schema.foreign_key
|
49
|
+
when 'OneToMany', 'OneToOne'
|
50
|
+
relation = datasource.get_collection(old_schema.foreign_collection)
|
51
|
+
old_schema.origin_key = relation.from_child_collection[old_schema.origin_key] || old_schema.origin_key
|
52
|
+
when 'ManyToMany'
|
53
|
+
through = datasource.get_collection(old_schema.through_collection)
|
54
|
+
old_schema.foreign_key = through.from_child_collection[old_schema.foreign_key] || old_schema.foreign_key
|
55
|
+
old_schema.origin_key = through.from_child_collection[old_schema.origin_key] || old_schema.origin_key
|
56
|
+
end
|
57
|
+
|
58
|
+
fields[from_child_collection[old_name] || old_name] = old_schema
|
59
|
+
end
|
60
|
+
|
61
|
+
sub_schema[:fields] = fields
|
62
|
+
|
63
|
+
sub_schema
|
64
|
+
end
|
65
|
+
|
66
|
+
def refine_filter(_caller, filter = nil)
|
67
|
+
filter&.override(
|
68
|
+
condition_tree: filter.condition_tree&.replace_fields do |field|
|
69
|
+
path_to_child_collection(field)
|
70
|
+
end,
|
71
|
+
sort: filter.sort&.replace_clauses do |clause|
|
72
|
+
{
|
73
|
+
field: path_to_child_collection(clause[:field]),
|
74
|
+
ascending: clause[:ascending]
|
75
|
+
}
|
76
|
+
end
|
77
|
+
)
|
78
|
+
end
|
79
|
+
|
80
|
+
def create(caller, data)
|
81
|
+
record = @child_collection.create(
|
82
|
+
caller,
|
83
|
+
record_to_child_collection(data)
|
84
|
+
)
|
85
|
+
|
86
|
+
record_from_child_collection(record)
|
87
|
+
end
|
88
|
+
|
89
|
+
def list(caller, filter, projection)
|
90
|
+
child_projection = projection.replace { |field| path_to_child_collection(field) }
|
91
|
+
records = @child_collection.list(caller, filter, child_projection)
|
92
|
+
return records if child_projection == projection
|
93
|
+
|
94
|
+
records.map { |record| record_from_child_collection(record) }
|
95
|
+
end
|
96
|
+
|
97
|
+
def update(caller, filter, patch)
|
98
|
+
@child_collection.update(caller, filter, record_to_child_collection(patch))
|
99
|
+
end
|
100
|
+
|
101
|
+
def aggregate(caller, filter, aggregation, limit = nil)
|
102
|
+
rows = @child_collection.aggregate(
|
103
|
+
caller,
|
104
|
+
filter,
|
105
|
+
aggregation.replace_fields { |field| path_to_child_collection(field) },
|
106
|
+
limit
|
107
|
+
)
|
108
|
+
|
109
|
+
rows.map do |row|
|
110
|
+
{
|
111
|
+
'value' => row['value'],
|
112
|
+
'group' => row['group']&.reduce({}) do |memo, group|
|
113
|
+
path, value = group
|
114
|
+
memo.merge({ path_from_child_collection(path) => value })
|
115
|
+
end
|
116
|
+
}
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# rubocop:disable Lint/UselessMethodDefinition
|
121
|
+
def mark_schema_as_dirty
|
122
|
+
super
|
123
|
+
end
|
124
|
+
# rubocop:enable Lint/UselessMethodDefinition
|
125
|
+
|
126
|
+
def mark_all_schema_as_dirty
|
127
|
+
datasource.collections.each_value(&:mark_schema_as_dirty)
|
128
|
+
end
|
129
|
+
|
130
|
+
# Convert field path from child collection to this collection
|
131
|
+
def path_from_child_collection(path)
|
132
|
+
if path.include?(':')
|
133
|
+
paths = path.split(':')
|
134
|
+
child_field = paths[0]
|
135
|
+
relation_name = from_child_collection[child_field] || child_field
|
136
|
+
relation_schema = schema[:fields][relation_name]
|
137
|
+
relation = datasource.get_collection(relation_schema.foreign_collection)
|
138
|
+
|
139
|
+
return "#{relation_name}:#{relation.path_from_child_collection(paths[1])}"
|
140
|
+
end
|
141
|
+
|
142
|
+
from_child_collection[path] ||= path
|
143
|
+
end
|
144
|
+
|
145
|
+
# Convert field path from this collection to child collection
|
146
|
+
def path_to_child_collection(path)
|
147
|
+
if path.include?(':')
|
148
|
+
paths = path.split(':')
|
149
|
+
relation_name = paths[0]
|
150
|
+
relation_schema = schema[:fields][relation_name]
|
151
|
+
relation = datasource.get_collection(relation_schema.foreign_collection)
|
152
|
+
child_field = to_child_collection[relation_name] || relation_name
|
153
|
+
|
154
|
+
return "#{child_field}:#{relation.path_to_child_collection(paths[1])}"
|
155
|
+
end
|
156
|
+
|
157
|
+
to_child_collection[path] ||= path
|
158
|
+
end
|
159
|
+
|
160
|
+
# Convert record from this collection to the child collection
|
161
|
+
def record_to_child_collection(record)
|
162
|
+
child_record = {}
|
163
|
+
record.each do |field, value|
|
164
|
+
child_record[to_child_collection[field] || field] = value
|
165
|
+
end
|
166
|
+
|
167
|
+
child_record
|
168
|
+
end
|
169
|
+
|
170
|
+
def record_from_child_collection(child_record)
|
171
|
+
record = {}
|
172
|
+
child_record.each do |child_field, value|
|
173
|
+
field = from_child_collection[child_field] || child_field
|
174
|
+
field_schema = schema[:fields][field]
|
175
|
+
|
176
|
+
# Perform the mapping, recurse for relations
|
177
|
+
if field_schema.type == 'Column' || value.nil?
|
178
|
+
record[field] = value
|
179
|
+
else
|
180
|
+
relation = datasource.get_collection(field_schema.foreign_collection)
|
181
|
+
record[field] = relation.record_from_child_collection(value)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
record
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module ForestAdminDatasourceCustomizer
|
2
|
+
module Decorators
|
3
|
+
module Schema
|
4
|
+
class SchemaCollectionDecorator < ForestAdminDatasourceToolkit::Decorators::CollectionDecorator
|
5
|
+
def initialize(child_collection, datasource)
|
6
|
+
super
|
7
|
+
@schema_override = {}
|
8
|
+
end
|
9
|
+
|
10
|
+
def override_schema(value)
|
11
|
+
@schema_override.merge!(value)
|
12
|
+
mark_schema_as_dirty
|
13
|
+
end
|
14
|
+
|
15
|
+
def refine_schema(sub_schema)
|
16
|
+
sub_schema.merge(@schema_override)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,135 @@
|
|
1
|
+
module ForestAdminDatasourceCustomizer
|
2
|
+
module Decorators
|
3
|
+
module Search
|
4
|
+
class SearchCollectionDecorator < ForestAdminDatasourceToolkit::Decorators::CollectionDecorator
|
5
|
+
include ForestAdminDatasourceToolkit::Schema
|
6
|
+
include ForestAdminDatasourceToolkit::Components::Query::ConditionTree
|
7
|
+
|
8
|
+
def initialize(child_collection, datasource)
|
9
|
+
super
|
10
|
+
@replacer = nil
|
11
|
+
end
|
12
|
+
|
13
|
+
def replace_search(replacer)
|
14
|
+
@replacer = replacer
|
15
|
+
end
|
16
|
+
|
17
|
+
def refine_schema(sub_schema)
|
18
|
+
sub_schema.merge({ searchable: true })
|
19
|
+
end
|
20
|
+
|
21
|
+
def refine_filter(caller, filter)
|
22
|
+
# Search string is not significant
|
23
|
+
return filter.override({ search: nil }) if !filter || !filter.search || filter.search.strip&.length&.zero?
|
24
|
+
|
25
|
+
# Implement search ourselves
|
26
|
+
if @replacer || !@child_collection.schema[:searchable]
|
27
|
+
ctx = ForestAdminDatasourceCustomizer::Context::CollectionCustomizationContext.new(self, caller)
|
28
|
+
tree = default_replacer(filter.search, filter.search_extended)
|
29
|
+
|
30
|
+
if @replacer
|
31
|
+
plain_tree = @replacer.call(filter.search, filter.search_extended, ctx)
|
32
|
+
tree = ConditionTreeFactory.from_plain_object(plain_tree)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Note that if no fields are searchable with the provided searchString, the conditions
|
36
|
+
# array might be empty, which will create a condition returning zero records
|
37
|
+
# (this is the desired behavior).
|
38
|
+
return filter.override({
|
39
|
+
condition_tree: ConditionTreeFactory.intersect([filter.condition_tree, tree]),
|
40
|
+
search: nil
|
41
|
+
})
|
42
|
+
end
|
43
|
+
|
44
|
+
# Let sub-collection deal with the search
|
45
|
+
filter
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def default_replacer(search, extended)
|
51
|
+
searchable_fields = get_fields(@child_collection, extended)
|
52
|
+
|
53
|
+
conditions = searchable_fields.map do |field, schema|
|
54
|
+
build_condition(field, schema, search)
|
55
|
+
end
|
56
|
+
|
57
|
+
ConditionTreeFactory.union(conditions)
|
58
|
+
end
|
59
|
+
|
60
|
+
def build_condition(field, schema, search_string)
|
61
|
+
column_type = schema.column_type
|
62
|
+
enum_values = schema.enum_values
|
63
|
+
filter_operators = schema.filter_operators
|
64
|
+
is_number = number?(search_string)
|
65
|
+
is_uuid = uuid?(search_string)
|
66
|
+
|
67
|
+
if column_type == PrimitiveType::NUMBER && is_number && filter_operators&.include?(Operators::EQUAL)
|
68
|
+
return Nodes::ConditionTreeLeaf.new(field, Operators::EQUAL, search_string.to_f)
|
69
|
+
end
|
70
|
+
|
71
|
+
if column_type == PrimitiveType::ENUM && filter_operators&.include?(Operators::EQUAL)
|
72
|
+
search_value = lenient_find(enum_values, search_string)
|
73
|
+
|
74
|
+
return Nodes::ConditionTreeLeaf.new(field, Operators::EQUAL, search_value) if search_value
|
75
|
+
end
|
76
|
+
|
77
|
+
if column_type == PrimitiveType::STRING
|
78
|
+
is_case_sensitive = !search_string.casecmp(search_string).zero?
|
79
|
+
supports_i_contains = filter_operators&.include?(Operators::I_CONTAINS)
|
80
|
+
supports_contains = filter_operators&.include?(Operators::CONTAINS)
|
81
|
+
supports_equal = filter_operators&.include?(Operators::EQUAL)
|
82
|
+
|
83
|
+
operator = nil
|
84
|
+
if supports_i_contains && (is_case_sensitive || !supports_contains)
|
85
|
+
operator = Operators::I_CONTAINS
|
86
|
+
elsif supports_contains
|
87
|
+
operator = Operators::CONTAINS
|
88
|
+
elsif supports_equal
|
89
|
+
operator = Operators::EQUAL
|
90
|
+
end
|
91
|
+
|
92
|
+
return Nodes::ConditionTreeLeaf.new(field, operator, search_string) if operator
|
93
|
+
end
|
94
|
+
|
95
|
+
if column_type == PrimitiveType::UUID && is_uuid && filter_operators&.include?(Operators::EQUAL)
|
96
|
+
return Nodes::ConditionTreeLeaf.new(field, Operators::EQUAL, search_string)
|
97
|
+
end
|
98
|
+
|
99
|
+
nil
|
100
|
+
end
|
101
|
+
|
102
|
+
def get_fields(collection, extended)
|
103
|
+
fields = []
|
104
|
+
collection.schema[:fields].each do |name, field|
|
105
|
+
fields.push([name, field]) if field.type == 'Column'
|
106
|
+
|
107
|
+
next unless extended && (field.type == 'ManyToOne' || field.type == 'OneToOne')
|
108
|
+
|
109
|
+
related = collection.datasource.get_collection(field.foreign_collection)
|
110
|
+
|
111
|
+
related.schema[:fields].each do |sub_name, sub_field|
|
112
|
+
fields.push(["#{name}:#{sub_name}", sub_field]) if sub_field.type == 'Column'
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
fields
|
117
|
+
end
|
118
|
+
|
119
|
+
def lenient_find(haystack, needle)
|
120
|
+
haystack&.find { |v| v == needle.strip } || haystack&.find { |v| v.downcase == needle.downcase.strip }
|
121
|
+
end
|
122
|
+
|
123
|
+
def uuid?(value)
|
124
|
+
value.to_s.downcase.match?(/^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/i)
|
125
|
+
end
|
126
|
+
|
127
|
+
def number?(value)
|
128
|
+
true if Float(value)
|
129
|
+
rescue StandardError
|
130
|
+
false
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
data/lib/forest_admin_datasource_customizer/decorators/segment/segment_collection_decorator.rb
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
module ForestAdminDatasourceCustomizer
|
2
|
+
module Decorators
|
3
|
+
module Segment
|
4
|
+
class SegmentCollectionDecorator < ForestAdminDatasourceToolkit::Decorators::CollectionDecorator
|
5
|
+
include ForestAdminDatasourceToolkit::Decorators
|
6
|
+
include ForestAdminDatasourceToolkit::Validations
|
7
|
+
include ForestAdminDatasourceToolkit::Components::Query::ConditionTree
|
8
|
+
|
9
|
+
attr_reader :segments
|
10
|
+
|
11
|
+
def initialize(child_collection, datasource)
|
12
|
+
super
|
13
|
+
@segments = {}
|
14
|
+
end
|
15
|
+
|
16
|
+
def add_segment(name, definition)
|
17
|
+
@segments[name] = definition
|
18
|
+
|
19
|
+
mark_schema_as_dirty
|
20
|
+
end
|
21
|
+
|
22
|
+
def refine_schema(sub_schema)
|
23
|
+
sub_schema[:segments] = sub_schema[:segments].merge(@segments)
|
24
|
+
|
25
|
+
sub_schema
|
26
|
+
end
|
27
|
+
|
28
|
+
def refine_filter(caller, filter = nil)
|
29
|
+
return nil unless filter
|
30
|
+
|
31
|
+
condition_tree = filter.condition_tree
|
32
|
+
segment = filter.segment
|
33
|
+
|
34
|
+
if segment && @segments.key?(segment)
|
35
|
+
definition = @segments[segment]
|
36
|
+
|
37
|
+
result = if definition.respond_to? :call
|
38
|
+
definition.call(Context::CollectionCustomizationContext.new(self, caller))
|
39
|
+
else
|
40
|
+
definition
|
41
|
+
end
|
42
|
+
|
43
|
+
condition_tree_segment = if result.is_a? Nodes::ConditionTree
|
44
|
+
result
|
45
|
+
else
|
46
|
+
ConditionTreeFactory.from_plain_object(result)
|
47
|
+
end
|
48
|
+
|
49
|
+
ConditionTreeValidator.validate(condition_tree_segment, self)
|
50
|
+
|
51
|
+
condition_tree = ConditionTreeFactory.intersect([condition_tree_segment, filter.condition_tree])
|
52
|
+
segment = nil
|
53
|
+
end
|
54
|
+
|
55
|
+
filter.override(condition_tree: condition_tree, segment: segment)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
module ForestAdminDatasourceCustomizer
|
2
|
+
module Decorators
|
3
|
+
module Sort
|
4
|
+
class SortCollectionDecorator < ForestAdminDatasourceToolkit::Decorators::CollectionDecorator
|
5
|
+
include ForestAdminDatasourceToolkit::Exceptions
|
6
|
+
include ForestAdminDatasourceToolkit::Validations
|
7
|
+
include ForestAdminDatasourceToolkit::Components::Query
|
8
|
+
include ForestAdminDatasourceToolkit::Utils
|
9
|
+
|
10
|
+
attr_reader :sorts
|
11
|
+
|
12
|
+
def initialize(child_collection, datasource)
|
13
|
+
super
|
14
|
+
@sorts = {}
|
15
|
+
end
|
16
|
+
|
17
|
+
def emulate_field_sorting(name)
|
18
|
+
replace_or_emulate_field_sorting(name, nil)
|
19
|
+
end
|
20
|
+
|
21
|
+
def replace_field_sorting(name, equivalent_sort)
|
22
|
+
if equivalent_sort.nil?
|
23
|
+
raise ForestException, 'A new sorting method should be provided to replace field sorting'
|
24
|
+
end
|
25
|
+
|
26
|
+
replace_or_emulate_field_sorting(name, equivalent_sort)
|
27
|
+
end
|
28
|
+
|
29
|
+
def list(caller, filter = nil, projection = nil)
|
30
|
+
child_filter = filter.override(sort: filter.sort&.replace_clauses do |clause|
|
31
|
+
rewrite_plain_sort_clause(clause)
|
32
|
+
end)
|
33
|
+
|
34
|
+
if child_filter.sort.nil? || child_filter.sort.none? { |clause| emulated?(clause[:field]) }
|
35
|
+
return child_collection.list(caller, child_filter, projection)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Fetch the whole collection, but only with the fields we need to sort
|
39
|
+
reference_records = child_collection.list(caller, child_filter.override(sort: nil, page: nil),
|
40
|
+
child_filter.sort.projection.with_pks(self))
|
41
|
+
reference_records = child_filter.sort.apply(reference_records)
|
42
|
+
reference_records = child_filter.page.apply(reference_records) if child_filter.page
|
43
|
+
|
44
|
+
# We now have the information we need to sort by the field
|
45
|
+
new_filter = Filter.new(condition_tree: ConditionTree::ConditionTreeFactory.match_records(schema,
|
46
|
+
reference_records))
|
47
|
+
|
48
|
+
records = child_collection.list(caller, new_filter, projection.with_pks(self))
|
49
|
+
records = sort_records(reference_records, records)
|
50
|
+
|
51
|
+
projection.apply(records)
|
52
|
+
end
|
53
|
+
|
54
|
+
def refine_schema(child_schema)
|
55
|
+
child_schema[:fields].each do |name, schema|
|
56
|
+
if schema.type == 'Column'
|
57
|
+
schema.is_sortable = true if @sorts[name].nil?
|
58
|
+
child_schema[:fields][name] = schema
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
child_schema
|
63
|
+
end
|
64
|
+
|
65
|
+
def rewrite_plain_sort_clause(clause)
|
66
|
+
# Order by is targeting a field on another collection => recurse.
|
67
|
+
if clause[:field].include?(':')
|
68
|
+
prefix = clause[:field].split(':')[0]
|
69
|
+
schema = self.schema[:fields][prefix]
|
70
|
+
association = datasource.get_collection(schema.foreign_collection)
|
71
|
+
|
72
|
+
return ForestAdminDatasourceToolkit::Components::Query::Sort.new([clause])
|
73
|
+
.unnest
|
74
|
+
.replace_clauses { |sub_clause| association.rewrite_plain_sort_clause(sub_clause) }
|
75
|
+
.nest(prefix)
|
76
|
+
end
|
77
|
+
|
78
|
+
# Field that we own: recursively replace using equivalent sort
|
79
|
+
equivalent_sort = @sorts[clause[:field]]
|
80
|
+
|
81
|
+
if equivalent_sort
|
82
|
+
equivalent_sort = equivalent_sort.inverse unless clause[:ascending]
|
83
|
+
|
84
|
+
return equivalent_sort.replace_clauses { |sub_clause| rewrite_plain_sort_clause(sub_clause) }
|
85
|
+
end
|
86
|
+
|
87
|
+
ForestAdminDatasourceToolkit::Components::Query::Sort.new([clause])
|
88
|
+
end
|
89
|
+
|
90
|
+
def emulated?(path)
|
91
|
+
index = path.index(':')
|
92
|
+
return @sorts[path] if index.nil?
|
93
|
+
|
94
|
+
foreign_collection = schema[:fields][path[0, index]].foreign_collection
|
95
|
+
association = datasource.get_collection(foreign_collection)
|
96
|
+
|
97
|
+
association.emulated?(path[index + 1, path.length - index - 1])
|
98
|
+
end
|
99
|
+
|
100
|
+
private
|
101
|
+
|
102
|
+
def replace_or_emulate_field_sorting(name, equivalent_sort)
|
103
|
+
FieldValidator.validate(self, name)
|
104
|
+
@sorts[name] =
|
105
|
+
equivalent_sort ? ForestAdminDatasourceToolkit::Components::Query::Sort.new(equivalent_sort) : nil
|
106
|
+
mark_schema_as_dirty
|
107
|
+
end
|
108
|
+
|
109
|
+
def sort_records(reference_records, records)
|
110
|
+
position_by_id = {}
|
111
|
+
sorted = Array.new(records.length)
|
112
|
+
|
113
|
+
reference_records.each_with_index do |record, index|
|
114
|
+
position_by_id[Record.primary_keys(schema, record).join('|')] = index
|
115
|
+
end
|
116
|
+
|
117
|
+
records.each do |record|
|
118
|
+
id = Record.primary_keys(schema, record).join('|')
|
119
|
+
sorted[position_by_id[id]] = record
|
120
|
+
end
|
121
|
+
|
122
|
+
sorted
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
data/lib/forest_admin_datasource_customizer/decorators/validation/validation_collection_decorator.rb
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
module ForestAdminDatasourceCustomizer
|
2
|
+
module Decorators
|
3
|
+
module Validation
|
4
|
+
class ValidationCollectionDecorator < ForestAdminDatasourceToolkit::Decorators::CollectionDecorator
|
5
|
+
include ForestAdminDatasourceToolkit::Validations
|
6
|
+
include ForestAdminDatasourceToolkit::Components::Query::ConditionTree
|
7
|
+
include ForestAdminDatasourceToolkit::Exceptions
|
8
|
+
attr_reader :validation
|
9
|
+
|
10
|
+
def initialize(child_collection, datasource)
|
11
|
+
super
|
12
|
+
@validation = {}
|
13
|
+
end
|
14
|
+
|
15
|
+
def add_validation(name, validation)
|
16
|
+
FieldValidator.validate(self, name)
|
17
|
+
|
18
|
+
field = @child_collection.schema[:fields][name]
|
19
|
+
if field.nil? || field.type != 'Column'
|
20
|
+
raise ForestException,
|
21
|
+
'Cannot add validators on a relation, use the foreign key instead'
|
22
|
+
end
|
23
|
+
raise ForestException, 'Cannot add validators on a readonly field' if field.is_read_only
|
24
|
+
|
25
|
+
@validation[name] ||= []
|
26
|
+
@validation[name].push(validation)
|
27
|
+
mark_schema_as_dirty
|
28
|
+
end
|
29
|
+
|
30
|
+
def create(caller, data)
|
31
|
+
data.each { |record| validate(record, caller.timezone, true) }
|
32
|
+
child_collection.create(caller, data)
|
33
|
+
end
|
34
|
+
|
35
|
+
def update(caller, filter, patch)
|
36
|
+
validate(patch, caller.timezone, false)
|
37
|
+
child_collection.update(caller, filter, patch)
|
38
|
+
end
|
39
|
+
|
40
|
+
def refine_schema(child_schema)
|
41
|
+
@validation.each do |name, rules|
|
42
|
+
field = child_schema[:fields][name]
|
43
|
+
field.validations = (field.validations || []).concat(rules)
|
44
|
+
child_schema[:fields][name] = field
|
45
|
+
end
|
46
|
+
|
47
|
+
child_schema
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def validate(record, timezone, all_fields)
|
53
|
+
@validation.each do |name, rules|
|
54
|
+
next unless all_fields || record.key?(name)
|
55
|
+
|
56
|
+
# When setting a field to nil, only the "Present" validator is relevant
|
57
|
+
applicable_rules = record[name].nil? ? rules.select { |r| r[:operator] == Operators::PRESENT } : rules
|
58
|
+
|
59
|
+
applicable_rules.each do |validator|
|
60
|
+
raw_leaf = { field: name }.merge(validator)
|
61
|
+
tree = ConditionTreeFactory.from_plain_object(raw_leaf)
|
62
|
+
next if tree.match(record, self, timezone)
|
63
|
+
|
64
|
+
message = "#{name} failed validation rule :"
|
65
|
+
rule = if validator.key?(:value)
|
66
|
+
"#{validator[:operator]}(#{if validator[:value].is_a?(Array)
|
67
|
+
validator[:value].join(",")
|
68
|
+
else
|
69
|
+
validator[:value]
|
70
|
+
end})"
|
71
|
+
else
|
72
|
+
validator[:operator]
|
73
|
+
end
|
74
|
+
|
75
|
+
raise ValidationError, "#{message} #{rule}"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
module ForestAdminDatasourceCustomizer
|
2
|
+
module Decorators
|
3
|
+
module Write
|
4
|
+
module CreateRelations
|
5
|
+
class CreateRelationsCollectionDecorator < ForestAdminDatasourceToolkit::Decorators::CollectionDecorator
|
6
|
+
include ForestAdminDatasourceToolkit::Components::Query
|
7
|
+
include ForestAdminDatasourceToolkit::Components::Query::ConditionTree
|
8
|
+
include ForestAdminDatasourceToolkit::Components::Query::ConditionTree::Nodes
|
9
|
+
def create(caller, data)
|
10
|
+
# Step 1: Remove all relations from records, and store them in a map
|
11
|
+
# Note: the extractRelations method modifies the records array in place!
|
12
|
+
records_by_relation = extract_relations(data)
|
13
|
+
|
14
|
+
# Step 2: Create the many-to-one relations, and put the foreign keys in the records
|
15
|
+
records_by_relation.each do |key, entries|
|
16
|
+
create_many_to_one_relation(caller, data, key, entries) if schema[:fields][key].type == 'ManyToOne'
|
17
|
+
end
|
18
|
+
|
19
|
+
# Step 3: Create the records
|
20
|
+
records_with_pk = child_collection.create(caller, data)
|
21
|
+
|
22
|
+
# Step 4: Create the one-to-one relations
|
23
|
+
# Note: the create_one_to_one_relation method modifies the records_with_pk array in place!
|
24
|
+
records_by_relation.each do |key, entries|
|
25
|
+
if schema[:fields][key].type == 'OneToOne'
|
26
|
+
create_one_to_one_relation(caller, records_with_pk, key, entries)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
records_with_pk
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def extract_relations(record)
|
36
|
+
records_by_relation = {}
|
37
|
+
|
38
|
+
record.each do |key, value|
|
39
|
+
next unless schema[:fields][key].type != 'Column'
|
40
|
+
|
41
|
+
value.each do |sub_key, sub_record|
|
42
|
+
records_by_relation[key] ||= {}
|
43
|
+
records_by_relation[key][sub_key] = sub_record
|
44
|
+
end
|
45
|
+
record.delete(key)
|
46
|
+
end
|
47
|
+
|
48
|
+
records_by_relation
|
49
|
+
end
|
50
|
+
|
51
|
+
def create_many_to_one_relation(caller, records, key, entries)
|
52
|
+
field_schema = schema[:fields][key]
|
53
|
+
relation = datasource.get_collection(field_schema.foreign_collection)
|
54
|
+
|
55
|
+
if records.key?(field_schema.foreign_key)
|
56
|
+
value = records[field_schema.foreign_key]
|
57
|
+
condition_tree = ConditionTreeLeaf.new(field_schema.foreign_key_target, Operators::EQUAL, value)
|
58
|
+
relation.update(caller, Filter.new(condition_tree: condition_tree), entries)
|
59
|
+
else
|
60
|
+
related_record = relation.create(caller, entries)
|
61
|
+
records[field_schema.foreign_key] = related_record[field_schema.foreign_key_target]
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def create_one_to_one_relation(caller, records, key, entries)
|
66
|
+
field_schema = schema[:fields][key]
|
67
|
+
relation = datasource.get_collection(field_schema.foreign_collection)
|
68
|
+
|
69
|
+
relation.create(caller, entries.merge(field_schema.origin_key => records[field_schema.origin_key_target]))
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|