forest_admin_datasource_customizer 1.0.0.pre.beta.21 → 1.0.0.pre.beta.57
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|