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.
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