forest_admin_datasource_customizer 1.0.0.pre.beta.21 → 1.0.0.pre.beta.57

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/forest_admin_datasource_customizer.gemspec +3 -2
  3. data/lib/forest_admin_datasource_customizer/collection_customizer.rb +292 -5
  4. data/lib/forest_admin_datasource_customizer/context/agent_customization_context.rb +18 -0
  5. data/lib/forest_admin_datasource_customizer/context/collection_customization_context.rb +15 -0
  6. data/lib/forest_admin_datasource_customizer/context/relaxed_wrappers/relaxed_collection.rb +50 -0
  7. data/lib/forest_admin_datasource_customizer/context/relaxed_wrappers/relaxed_data_source.rb +18 -0
  8. data/lib/forest_admin_datasource_customizer/datasource_customizer.rb +43 -13
  9. data/lib/forest_admin_datasource_customizer/decorators/action/action_collection_decorator.rb +137 -0
  10. data/lib/forest_admin_datasource_customizer/decorators/action/base_action.rb +67 -0
  11. data/lib/forest_admin_datasource_customizer/decorators/action/context/action_context.rb +56 -0
  12. data/lib/forest_admin_datasource_customizer/decorators/action/context/action_context_single.rb +26 -0
  13. data/lib/forest_admin_datasource_customizer/decorators/action/dynamic_field.rb +50 -0
  14. data/lib/forest_admin_datasource_customizer/decorators/action/result_builder.rb +68 -0
  15. data/lib/forest_admin_datasource_customizer/decorators/action/types/action_scope.rb +15 -0
  16. data/lib/forest_admin_datasource_customizer/decorators/action/types/field_type.rb +35 -0
  17. data/lib/forest_admin_datasource_customizer/decorators/action/widget_field.rb +357 -0
  18. data/lib/forest_admin_datasource_customizer/decorators/binary/binary_collection_decorator.rb +215 -0
  19. data/lib/forest_admin_datasource_customizer/decorators/binary/binary_helper.rb +17 -0
  20. data/lib/forest_admin_datasource_customizer/decorators/chart/chart_collection_decorator.rb +41 -0
  21. data/lib/forest_admin_datasource_customizer/decorators/chart/chart_context.rb +33 -0
  22. data/lib/forest_admin_datasource_customizer/decorators/chart/chart_datasource_decorator.rb +46 -0
  23. data/lib/forest_admin_datasource_customizer/decorators/chart/result_builder.rb +148 -0
  24. data/lib/forest_admin_datasource_customizer/decorators/computed/compute_collection_decorator.rb +115 -0
  25. data/lib/forest_admin_datasource_customizer/decorators/computed/computed_definition.rb +21 -0
  26. data/lib/forest_admin_datasource_customizer/decorators/computed/utils/computed_field.rb +74 -0
  27. data/lib/forest_admin_datasource_customizer/decorators/computed/utils/flattener.rb +49 -0
  28. data/lib/forest_admin_datasource_customizer/decorators/decorators_stack.rb +33 -4
  29. data/lib/forest_admin_datasource_customizer/decorators/hook/context/after/hook_after_aggregate_context.rb +18 -0
  30. data/lib/forest_admin_datasource_customizer/decorators/hook/context/after/hook_after_create_context.rb +18 -0
  31. data/lib/forest_admin_datasource_customizer/decorators/hook/context/after/hook_after_delete_context.rb +12 -0
  32. data/lib/forest_admin_datasource_customizer/decorators/hook/context/after/hook_after_list_context.rb +18 -0
  33. data/lib/forest_admin_datasource_customizer/decorators/hook/context/after/hook_after_update_context.rb +12 -0
  34. data/lib/forest_admin_datasource_customizer/decorators/hook/context/before/hook_before_aggregate_context.rb +20 -0
  35. data/lib/forest_admin_datasource_customizer/decorators/hook/context/before/hook_before_create_context.rb +18 -0
  36. data/lib/forest_admin_datasource_customizer/decorators/hook/context/before/hook_before_delete_context.rb +18 -0
  37. data/lib/forest_admin_datasource_customizer/decorators/hook/context/before/hook_before_list_context.rb +19 -0
  38. data/lib/forest_admin_datasource_customizer/decorators/hook/context/before/hook_before_update_context.rb +19 -0
  39. data/lib/forest_admin_datasource_customizer/decorators/hook/context/hook_context.rb +22 -0
  40. data/lib/forest_admin_datasource_customizer/decorators/hook/hook_collection_decorator.rb +95 -0
  41. data/lib/forest_admin_datasource_customizer/decorators/hook/hooks.rb +26 -0
  42. data/lib/forest_admin_datasource_customizer/decorators/operators_emulate/operators_emulate_collection_decorator.rb +118 -0
  43. data/lib/forest_admin_datasource_customizer/decorators/operators_equivalence/operators_equivalence_collection_decorator.rb +50 -0
  44. data/lib/forest_admin_datasource_customizer/decorators/override/context/create_override_customization_context.rb +16 -0
  45. data/lib/forest_admin_datasource_customizer/decorators/override/context/delete_override_customization_context.rb +16 -0
  46. data/lib/forest_admin_datasource_customizer/decorators/override/context/update_override_customization_context.rb +17 -0
  47. data/lib/forest_admin_datasource_customizer/decorators/override/override_collection_decorator.rb +49 -0
  48. data/lib/forest_admin_datasource_customizer/decorators/publication/publication_collection_decorator.rb +95 -0
  49. data/lib/forest_admin_datasource_customizer/decorators/publication/publication_datasource_decorator.rb +57 -0
  50. data/lib/forest_admin_datasource_customizer/decorators/relation/relation_collection_decorator.rb +268 -0
  51. data/lib/forest_admin_datasource_customizer/decorators/rename_collection/rename_collection_datasource_decorator.rb +70 -0
  52. data/lib/forest_admin_datasource_customizer/decorators/rename_collection/rename_collection_decorator.rb +37 -0
  53. data/lib/forest_admin_datasource_customizer/decorators/rename_field/rename_field_collection_decorator.rb +190 -0
  54. data/lib/forest_admin_datasource_customizer/decorators/schema/schema_collection_decorator.rb +21 -0
  55. data/lib/forest_admin_datasource_customizer/decorators/search/search_collection_decorator.rb +135 -0
  56. data/lib/forest_admin_datasource_customizer/decorators/segment/segment_collection_decorator.rb +60 -0
  57. data/lib/forest_admin_datasource_customizer/decorators/sort/sort_collection_decorator.rb +127 -0
  58. data/lib/forest_admin_datasource_customizer/decorators/validation/validation_collection_decorator.rb +82 -0
  59. data/lib/forest_admin_datasource_customizer/decorators/write/create_relations/create_relations_collection_decorator.rb +75 -0
  60. data/lib/forest_admin_datasource_customizer/decorators/write/update_relations/update_relations_collection_decorator.rb +96 -0
  61. data/lib/forest_admin_datasource_customizer/decorators/write/write_datasource_decorator.rb +14 -0
  62. data/lib/forest_admin_datasource_customizer/decorators/write/write_replace/write_customization_context.rb +18 -0
  63. data/lib/forest_admin_datasource_customizer/decorators/write/write_replace/write_replace_collection_decorator.rb +125 -0
  64. data/lib/forest_admin_datasource_customizer/plugins/add_external_relation.rb +27 -0
  65. data/lib/forest_admin_datasource_customizer/plugins/import_field.rb +74 -0
  66. data/lib/forest_admin_datasource_customizer/version.rb +1 -1
  67. metadata +84 -5
  68. 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
@@ -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
@@ -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