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