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,215 @@
1
+ require 'base64'
2
+ require 'marcel'
3
+
4
+ module ForestAdminDatasourceCustomizer
5
+ module Decorators
6
+ module Binary
7
+ class BinaryCollectionDecorator < ForestAdminDatasourceToolkit::Decorators::CollectionDecorator
8
+ include ForestAdminDatasourceToolkit
9
+ include ForestAdminDatasourceToolkit::Decorators
10
+ include ForestAdminDatasourceToolkit::Components::Query::ConditionTree
11
+
12
+ OPERATORS_WITH_REPLACEMENT = [Operators::AFTER, Operators::BEFORE, Operators::CONTAINS,
13
+ Operators::ENDS_WITH, Operators::EQUAL, Operators::GREATER_THAN,
14
+ Operators::I_CONTAINS, Operators::NOT_IN, Operators::I_ENDS_WITH,
15
+ Operators::I_STARTS_WITH, Operators::LESS_THAN, Operators::NOT_CONTAINS,
16
+ Operators::NOT_EQUAL, Operators::STARTS_WITH, Operators::IN].freeze
17
+
18
+ def initialize(child_collection, datasource)
19
+ super
20
+ @use_hex_conversion = {}
21
+ end
22
+
23
+ def set_binary_mode(name, type)
24
+ field = @child_collection.schema[:fields][name]
25
+
26
+ raise Exceptions::ForestException, 'Invalid binary mode' unless %w[datauri hex].include?(type)
27
+
28
+ unless field&.type == 'Column' && field&.column_type == 'Binary'
29
+ raise Exceptions::ForestException, 'Expected a binary field'
30
+ end
31
+
32
+ @use_hex_conversion[name] = (type == 'hex')
33
+ mark_schema_as_dirty
34
+ end
35
+
36
+ def refine_schema(sub_schema)
37
+ fields = {}
38
+
39
+ sub_schema[:fields].each do |name, schema|
40
+ if schema.type == 'Column'
41
+ new_schema = schema.dup
42
+ new_schema.column_type = replace_column_type(schema.column_type)
43
+ new_schema.validations = replace_validation(name, schema)
44
+ fields[name] = new_schema
45
+ else
46
+ fields[name] = schema
47
+ end
48
+ end
49
+
50
+ sub_schema[:fields] = fields
51
+ sub_schema
52
+ end
53
+
54
+ def refine_filter(_caller, filter = nil)
55
+ filter&.override(
56
+ condition_tree: filter&.condition_tree&.replace_leafs do |leaf|
57
+ convert_condition_tree_leaf(leaf)
58
+ end
59
+ )
60
+ end
61
+
62
+ def create(caller, data)
63
+ data_with_binary = convert_record(true, data)
64
+ record = super(caller, data_with_binary)
65
+
66
+ convert_record(false, record)
67
+ end
68
+
69
+ def list(caller, filter, projection)
70
+ records = super
71
+ records.map! { |record| convert_record(false, record) }
72
+
73
+ records
74
+ end
75
+
76
+ def update(caller, filter, patch)
77
+ super(caller, filter, convert_record(true, patch))
78
+ end
79
+
80
+ def aggregate(caller, filter, aggregation, limit = nil)
81
+ rows = super
82
+ rows.map! do |row|
83
+ {
84
+ 'value' => row['value'],
85
+ 'group' => row['group'].to_h { |path, value| [path, convert_value(false, path, value)] }
86
+ }
87
+ end
88
+ end
89
+
90
+ def convert_condition_tree_leaf(leaf)
91
+ prefix, suffix = leaf.field.split(':')
92
+ schema = @child_collection.schema[:fields][prefix]
93
+
94
+ if schema.type != 'Column'
95
+ condition_tree = @datasource.get_collection(schema.foreign_collection).convert_condition_tree_leaf(
96
+ leaf.override(field: suffix)
97
+ )
98
+
99
+ return condition_tree.nest(prefix)
100
+ end
101
+
102
+ if OPERATORS_WITH_REPLACEMENT.include?(leaf.operator)
103
+ column_type = if [Operators::IN, Operators::NOT_IN].include?(leaf.operator)
104
+ [schema.column_type]
105
+ else
106
+ schema.column_type
107
+ end
108
+
109
+ return leaf.override(
110
+ value: convert_value_helper(true, column_type, should_use_hex(prefix), leaf.value)
111
+ )
112
+ end
113
+
114
+ leaf
115
+ end
116
+
117
+ def should_use_hex(name)
118
+ return @use_hex_conversion[name] if @use_hex_conversion.key?(name)
119
+
120
+ Utils::Schema.primary_key?(@child_collection, name) || Utils::Schema.foreign_key?(@child_collection, name)
121
+ end
122
+
123
+ def convert_record(to_backend, record)
124
+ if record
125
+ record = record.to_h do |path, value|
126
+ [path, convert_value(to_backend, path, value)]
127
+ end
128
+ end
129
+
130
+ record
131
+ end
132
+
133
+ def convert_value(to_backend, path, value)
134
+ prefix, suffix = path.split(':')
135
+ field = @child_collection.schema[:fields][prefix]
136
+
137
+ if field.type != 'Column'
138
+ foreign_collection = @datasource.get_collection(field.foreign_collection)
139
+
140
+ return suffix ? foreign_collection.convert_value(to_backend, suffix,
141
+ value) : foreign_collection.convert_record(to_backend,
142
+ value)
143
+ end
144
+
145
+ binary_mode = should_use_hex(path)
146
+
147
+ convert_value_helper(to_backend, field.column_type, binary_mode, value)
148
+ end
149
+
150
+ def convert_value_helper(to_backend, column_type, use_hex, value)
151
+ if value
152
+ return convert_scalar(to_backend, use_hex, value) if column_type == 'Binary'
153
+
154
+ if column_type.is_a? Array
155
+ return value.map { |v| convert_value_helper(to_backend, column_type[0], use_hex, v) }
156
+ end
157
+
158
+ unless column_type.is_a? String
159
+ return column_type.to_h { |key, type| [key, convert_value_helper(to_backend, type, use_hex, value[key])] }
160
+ end
161
+ end
162
+
163
+ value
164
+ end
165
+
166
+ def convert_scalar(to_backend, use_hex, value)
167
+ if to_backend
168
+ return use_hex ? BinaryHelper.hex_to_bin(value) : Base64.strict_decode64(value.partition(',')[2])
169
+ end
170
+
171
+ return BinaryHelper.bin_to_hex(value) if use_hex
172
+
173
+ data = Base64.strict_encode64(value)
174
+ mime = Marcel::MimeType.for StringIO.new(value)
175
+
176
+ "data:#{mime};base64,#{data}"
177
+ end
178
+
179
+ def replace_column_type(column_type)
180
+ if column_type.is_a? String
181
+ return column_type == 'Binary' ? 'String' : column_type
182
+ end
183
+
184
+ return [replace_column_type(column_type[0])] if column_type.is_a? Array
185
+
186
+ column_type.transform_values { |type| replace_column_type(type) }
187
+ end
188
+
189
+ def replace_validation(name, column_schema)
190
+ if column_schema.column_type == 'Binary'
191
+ validations = []
192
+ min_length = (column_schema.validations.find { |v| v[:operator] == Operators::LONGER_THAN } || {})[:value]
193
+ max_length = (column_schema.validations.find { |v| v[:operator] == Operators::SHORTER_THAN } || {})[:value]
194
+
195
+ if should_use_hex(name)
196
+ validations << { operator: Operators::MATCH, value: '/^[0-9a-f]+$/' }
197
+ validations << { operator: Operators::LONGER_THAN, value: (min_length * 2) + 1 } if min_length
198
+ validations << { operator: Operators::SHORTER_THAN, value: (max_length * 2) - 1 } if max_length
199
+ else
200
+ validations << { operator: Operators::MATCH, value: '/^data:.*;base64,.*/' }
201
+ end
202
+
203
+ if column_schema.validations.find { |v| v[:operator] == Operators::PRESENT }
204
+ validations << { operator: Operators::PRESENT }
205
+ end
206
+
207
+ return validations
208
+ end
209
+
210
+ column_schema.validations
211
+ end
212
+ end
213
+ end
214
+ end
215
+ end
@@ -0,0 +1,17 @@
1
+ require 'base64'
2
+
3
+ module ForestAdminDatasourceCustomizer
4
+ module Decorators
5
+ module Binary
6
+ class BinaryHelper
7
+ def self.bin_to_hex(data)
8
+ data.unpack1('H*')
9
+ end
10
+
11
+ def self.hex_to_bin(data)
12
+ data.scan(/../).map(&:hex).pack('c*')
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,41 @@
1
+ module ForestAdminDatasourceCustomizer
2
+ module Decorators
3
+ module Chart
4
+ class ChartCollectionDecorator < ForestAdminDatasourceToolkit::Decorators::CollectionDecorator
5
+ include ForestAdminDatasourceToolkit
6
+ include ForestAdminDatasourceToolkit::Decorators
7
+
8
+ attr_reader :charts
9
+
10
+ def initialize(child_collection, datasource)
11
+ @charts = {}
12
+ super
13
+ end
14
+
15
+ def add_chart(name, &definition)
16
+ raise(Exceptions::ForestException, "Chart #{name} already exists.") if schema[:charts].include?(name)
17
+
18
+ @charts[name] = definition
19
+ mark_schema_as_dirty
20
+ end
21
+
22
+ def render_chart(caller, name, record_id)
23
+ if @charts.key?(name)
24
+ context = ChartContext.new(self, caller, record_id)
25
+ result_builder = ResultBuilder.new
26
+
27
+ return @charts[name].call(context, result_builder)
28
+ end
29
+
30
+ @child_collection.render_chart(caller, name, record_id)
31
+ end
32
+
33
+ def refine_schema(sub_schema)
34
+ sub_schema[:charts] = sub_schema[:charts].union(@charts.keys)
35
+
36
+ sub_schema
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,33 @@
1
+ module ForestAdminDatasourceCustomizer
2
+ module Decorators
3
+ module Chart
4
+ class ChartContext < ForestAdminDatasourceCustomizer::Context::CollectionCustomizationContext
5
+ include ForestAdminDatasourceToolkit
6
+ include ForestAdminDatasourceToolkit::Components::Query
7
+ include ForestAdminDatasourceToolkit::Components::Query::ConditionTree
8
+
9
+ attr_reader :composite_record_id
10
+
11
+ def initialize(collection, caller, record_id)
12
+ super(collection, caller)
13
+ @composite_record_id = record_id
14
+ end
15
+
16
+ def record_id
17
+ if @composite_record_id.size > 1
18
+ raise Exceptions::ForestException,
19
+ "Collection is using a composite pk: use 'context.composite_record_id'."
20
+ end
21
+
22
+ @composite_record_id[0]
23
+ end
24
+
25
+ def get_record(fields)
26
+ condition_tree = ConditionTreeFactory.match_ids(@real_collection, [@composite_record_id])
27
+
28
+ collection.list(Filter.new(condition_tree: condition_tree), Projection.new(fields))[0]
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,46 @@
1
+ module ForestAdminDatasourceCustomizer
2
+ module Decorators
3
+ module Chart
4
+ class ChartDatasourceDecorator < ForestAdminDatasourceToolkit::Decorators::DatasourceDecorator
5
+ include ForestAdminDatasourceToolkit
6
+ include ForestAdminDatasourceToolkit::Decorators
7
+
8
+ def initialize(child_datasource)
9
+ @charts = {}
10
+ super(child_datasource, ChartCollectionDecorator)
11
+ end
12
+
13
+ def schema
14
+ child_schema = @child_datasource.schema.dup
15
+
16
+ duplicate = @charts.keys.find { |name| child_schema[:charts].include?(name) }
17
+
18
+ raise(Exceptions::ForestException, "Chart #{duplicate} is defined twice.") if duplicate
19
+
20
+ child_schema[:charts] = child_schema[:charts].union(@charts.keys)
21
+
22
+ child_schema
23
+ end
24
+
25
+ def add_chart(name, &definition)
26
+ raise(Exceptions::ForestException, "Chart #{name} already exists.") if schema[:charts].include?(name)
27
+
28
+ @charts[name] = definition
29
+ end
30
+
31
+ def render_chart(caller, name)
32
+ chart_definition = @charts[name]
33
+
34
+ if chart_definition
35
+ return chart_definition.call(
36
+ Context::AgentCustomizationContext.new(self, caller),
37
+ ResultBuilder.new
38
+ )
39
+ end
40
+
41
+ super
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,148 @@
1
+ require 'active_support/all'
2
+
3
+ module ForestAdminDatasourceCustomizer
4
+ module Decorators
5
+ module Chart
6
+ class ResultBuilder
7
+ include ForestAdminDatasourceToolkit::Components::Charts
8
+ include ForestAdminDatasourceToolkit::Utils
9
+
10
+ TIME_FORMAT = {
11
+ Day: '%d/%m/%Y',
12
+ Week: 'W%V-%G',
13
+ Month: '%b %y',
14
+ Year: '%Y'
15
+ }.freeze
16
+
17
+ def value(value, previous_value = nil)
18
+ ValueChart.new(value, previous_value).serialize
19
+ end
20
+
21
+ def distribution(data)
22
+ data = HashHelper.convert_keys(data, :to_s).map do |key, value|
23
+ { key: key, value: value }
24
+ end
25
+
26
+ PieChart.new(data).serialize
27
+ end
28
+
29
+ def percentage(value)
30
+ PercentageChart.new(value).serialize
31
+ end
32
+
33
+ def objective(value, objective)
34
+ ObjectiveChart.new(value, objective).serialize
35
+ end
36
+
37
+ def leaderboard(value)
38
+ data = distribution(value).sort { |a, b| b[:value] - a[:value] }
39
+
40
+ LeaderboardChart.new(data).serialize
41
+ end
42
+
43
+ # Add a TimeBasedChart based on a time range and a set of values.
44
+ # @param time_range - The time range for the chart, specified as "Year", "Month", "Week" or "Day".
45
+ # @param values - This is an array of objects with 'date' and 'value' properties
46
+ # @returns {TimeBasedChart} Returns a TimeBasedChart representing the data within the specified
47
+ # time range.
48
+ #
49
+ # @example
50
+ # time_based(
51
+ # 'Day',
52
+ # [
53
+ # { date: '2023-01-01', value: 42 },
54
+ # { date: '2023-01-02', value: 55 },
55
+ # { date: '2023-01-03', value: null },
56
+ # ]
57
+ # );
58
+ def time_based(time_range, values)
59
+ return [] if values.nil?
60
+
61
+ values = HashHelper.convert_keys(values, :to_sym)
62
+ values = values.map { |date, value| { date: date, value: value } } unless values.is_a? Array
63
+ data = build_time_based_chart_result(time_range, values)
64
+
65
+ LineChart.new(data).serialize
66
+ end
67
+
68
+ # Add a MultipleTimeBasedChart based on a time range,
69
+ # an array of dates, and multiple lines of data.
70
+ #
71
+ # @param time_range - The time range for the chart, specified as "Year", "Month", "Week" or "Day".
72
+ # @param dates - An array of dates that define the x-axis values for the chart.
73
+ # @param lines - An array of lines, each containing a label and an array of numeric data values (or null)
74
+ # corresponding to the dates.
75
+ # @returns {MultipleTimeBasedChart} Returns a MultipleTimeBasedChart representing multiple
76
+ # lines of data within the specified time range.
77
+ #
78
+ # @example
79
+ # multiple_time_based(
80
+ # 'Day',
81
+ # [
82
+ # Date.new('1985-10-26'),
83
+ # Date.new('2011-10-05T14:48:00.000Z'),
84
+ # Date.new()
85
+ # ],
86
+ # [
87
+ # { label: 'line1', values: [1, 2, 3] },
88
+ # { label: 'line2', values: [3, 4, null] }
89
+ # ],
90
+ # );
91
+ def multiple_time_based(time_range, dates, lines)
92
+ return { labels: nil, values: nil } if dates.nil? || lines.nil?
93
+
94
+ formatted_times = nil
95
+ formatted_lines = lines.map do |line|
96
+ values = dates.each_with_index.with_object([]) do |(date, index), memo|
97
+ memo.push({ date: date, value: line[:values][index] })
98
+ end
99
+
100
+ build_time_based = build_time_based_chart_result(time_range, values)
101
+ formatted_times = build_time_based.map { |time_based| time_based[:label] } if formatted_times.nil?
102
+
103
+ { key: line[:label], values: build_time_based.map { |time_based| time_based[:values][:value] } }
104
+ end
105
+
106
+ {
107
+ labels: formatted_times,
108
+ values: formatted_times&.length&.positive? ? formatted_lines : nil
109
+ }
110
+ end
111
+
112
+ def smart(data)
113
+ data
114
+ end
115
+
116
+ private
117
+
118
+ def build_time_based_chart_result(time_range, points)
119
+ return [] if points.empty?
120
+
121
+ format = TIME_FORMAT[time_range.to_sym]
122
+ formatted = {}
123
+ points.each do |point|
124
+ point[:date] = DateTime.parse(point[:date].to_s) if point[:date].is_a?(String) || point[:date].is_a?(Symbol)
125
+ label = point[:date].strftime(format)
126
+ formatted[label] = (formatted[label] || 0) + point[:value] if point[:value].is_a? Numeric
127
+ end
128
+
129
+ data_points = []
130
+ dates = points.map { |point| point[:date] }
131
+ .sort { |date_a, date_b| date_a - date_b }
132
+
133
+ # first date
134
+ current = dates.first.send(:"beginning_of_#{time_range.to_s.downcase}")
135
+ last = dates.last
136
+
137
+ while current <= last
138
+ label = current.strftime(format)
139
+ data_points << { label: label, values: { value: formatted[label] || 0 } }
140
+ current += 1.send(time_range.to_s.downcase.to_sym)
141
+ end
142
+
143
+ data_points
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,115 @@
1
+ module ForestAdminDatasourceCustomizer
2
+ module Decorators
3
+ module Computed
4
+ class ComputeCollectionDecorator < ForestAdminDatasourceToolkit::Decorators::CollectionDecorator
5
+ include ForestAdminDatasourceToolkit::Components::Query
6
+ include ForestAdminDatasourceToolkit::Validations
7
+ include ForestAdminDatasourceCustomizer::Decorators::Computed::Utils
8
+ include ForestAdminDatasourceToolkit::Exceptions
9
+
10
+ def initialize(child_collection, datasource)
11
+ super
12
+ @computeds = {}
13
+ end
14
+
15
+ def get_computed(path)
16
+ index = path.index(':')
17
+ return @computeds[path] if index.nil?
18
+
19
+ foreign_collection = schema[:fields][path[0, index]].foreign_collection
20
+ association = @datasource.get_collection(foreign_collection)
21
+
22
+ association.get_computed(path[index + 1, path.length - index - 1])
23
+ end
24
+
25
+ def register_computed(name, computed)
26
+ FieldValidator.validate_name(@name, name)
27
+
28
+ # Check that all dependencies exist and are columns
29
+ computed.dependencies.each do |field|
30
+ FieldValidator.validate(self, field)
31
+ end
32
+
33
+ if computed.dependencies.length <= 0
34
+ raise ForestException,
35
+ "Computed field '#{name}' must have at least one dependency."
36
+ end
37
+
38
+ @computeds[name] = computed
39
+ mark_schema_as_dirty
40
+ end
41
+
42
+ def list(caller, filter, projection)
43
+ child_projection = projection.replace { |path| rewrite_field(self, path) }
44
+ records = @child_collection.list(caller, filter, child_projection)
45
+ return records if child_projection.equals(projection)
46
+
47
+ context = ForestAdminDatasourceCustomizer::Context::CollectionCustomizationContext.new(self, caller)
48
+
49
+ ComputedField.compute_from_records(context, self, child_projection, projection, records)
50
+ end
51
+
52
+ def aggregate(caller, filter, aggregation, limit = nil)
53
+ # No computed are used in the aggregation => just delegate to the underlying collection.
54
+ unless aggregation.projection.any? do |field|
55
+ get_computed(field)
56
+ end
57
+ return @child_collection.aggregate(caller, filter, aggregation,
58
+ limit)
59
+ end
60
+
61
+ # Fallback to full emulation.
62
+ aggregation.apply(
63
+ list(caller, filter, aggregation.projection),
64
+ caller.timezone,
65
+ limit
66
+ )
67
+ end
68
+
69
+ def refine_schema(child_schema)
70
+ schema = child_schema.clone
71
+ schema[:fields] = child_schema[:fields].clone
72
+
73
+ @computeds.each do |name, computed|
74
+ schema[:fields][name] = ForestAdminDatasourceToolkit::Schema::ColumnSchema.new(
75
+ column_type: computed.column_type,
76
+ default_value: computed.default_value,
77
+ enum_values: computed.enum_values || [],
78
+ filter_operators: [],
79
+ is_primary_key: false,
80
+ is_read_only: true,
81
+ is_sortable: false
82
+ )
83
+ end
84
+
85
+ schema
86
+ end
87
+
88
+ def rewrite_field(collection, path)
89
+ # Projection is targeting a field on another collection => recurse.
90
+ if path.include?(':')
91
+ prefix = path.split(':')[0]
92
+ schema = collection.schema[:fields][prefix]
93
+ association = collection.datasource.get_collection(schema.foreign_collection)
94
+
95
+ return Projection.new([path])
96
+ .unnest
97
+ .replace { |sub_path| rewrite_field(association, sub_path) }
98
+ .nest(prefix: prefix)
99
+ end
100
+
101
+ # Computed field that we own: recursively replace by dependencies
102
+ computed = collection.get_computed(path)
103
+
104
+ if computed
105
+ Projection.new(computed.dependencies.flatten).replace do |dep_path|
106
+ rewrite_field(collection, dep_path)
107
+ end
108
+ else
109
+ Projection.new([path])
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,21 @@
1
+ module ForestAdminDatasourceCustomizer
2
+ module Decorators
3
+ module Computed
4
+ class ComputedDefinition
5
+ attr_reader :column_type, :dependencies, :default_value, :enum_values
6
+
7
+ def initialize(column_type:, dependencies:, values:, default_value: nil, enum_values: nil)
8
+ @column_type = column_type
9
+ @dependencies = dependencies
10
+ @values = values
11
+ @default_value = default_value
12
+ @enum_values = enum_values
13
+ end
14
+
15
+ def get_values(*args)
16
+ @values.call(*args)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end