forest_admin_datasource_customizer 1.0.0.pre.beta.21 → 1.0.0.pre.beta.57
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/forest_admin_datasource_customizer.gemspec +3 -2
- data/lib/forest_admin_datasource_customizer/collection_customizer.rb +292 -5
- data/lib/forest_admin_datasource_customizer/context/agent_customization_context.rb +18 -0
- data/lib/forest_admin_datasource_customizer/context/collection_customization_context.rb +15 -0
- data/lib/forest_admin_datasource_customizer/context/relaxed_wrappers/relaxed_collection.rb +50 -0
- data/lib/forest_admin_datasource_customizer/context/relaxed_wrappers/relaxed_data_source.rb +18 -0
- data/lib/forest_admin_datasource_customizer/datasource_customizer.rb +43 -13
- data/lib/forest_admin_datasource_customizer/decorators/action/action_collection_decorator.rb +137 -0
- data/lib/forest_admin_datasource_customizer/decorators/action/base_action.rb +67 -0
- data/lib/forest_admin_datasource_customizer/decorators/action/context/action_context.rb +56 -0
- data/lib/forest_admin_datasource_customizer/decorators/action/context/action_context_single.rb +26 -0
- data/lib/forest_admin_datasource_customizer/decorators/action/dynamic_field.rb +50 -0
- data/lib/forest_admin_datasource_customizer/decorators/action/result_builder.rb +68 -0
- data/lib/forest_admin_datasource_customizer/decorators/action/types/action_scope.rb +15 -0
- data/lib/forest_admin_datasource_customizer/decorators/action/types/field_type.rb +35 -0
- data/lib/forest_admin_datasource_customizer/decorators/action/widget_field.rb +357 -0
- data/lib/forest_admin_datasource_customizer/decorators/binary/binary_collection_decorator.rb +215 -0
- data/lib/forest_admin_datasource_customizer/decorators/binary/binary_helper.rb +17 -0
- data/lib/forest_admin_datasource_customizer/decorators/chart/chart_collection_decorator.rb +41 -0
- data/lib/forest_admin_datasource_customizer/decorators/chart/chart_context.rb +33 -0
- data/lib/forest_admin_datasource_customizer/decorators/chart/chart_datasource_decorator.rb +46 -0
- data/lib/forest_admin_datasource_customizer/decorators/chart/result_builder.rb +148 -0
- data/lib/forest_admin_datasource_customizer/decorators/computed/compute_collection_decorator.rb +115 -0
- data/lib/forest_admin_datasource_customizer/decorators/computed/computed_definition.rb +21 -0
- data/lib/forest_admin_datasource_customizer/decorators/computed/utils/computed_field.rb +74 -0
- data/lib/forest_admin_datasource_customizer/decorators/computed/utils/flattener.rb +49 -0
- data/lib/forest_admin_datasource_customizer/decorators/decorators_stack.rb +33 -4
- data/lib/forest_admin_datasource_customizer/decorators/hook/context/after/hook_after_aggregate_context.rb +18 -0
- data/lib/forest_admin_datasource_customizer/decorators/hook/context/after/hook_after_create_context.rb +18 -0
- data/lib/forest_admin_datasource_customizer/decorators/hook/context/after/hook_after_delete_context.rb +12 -0
- data/lib/forest_admin_datasource_customizer/decorators/hook/context/after/hook_after_list_context.rb +18 -0
- data/lib/forest_admin_datasource_customizer/decorators/hook/context/after/hook_after_update_context.rb +12 -0
- data/lib/forest_admin_datasource_customizer/decorators/hook/context/before/hook_before_aggregate_context.rb +20 -0
- data/lib/forest_admin_datasource_customizer/decorators/hook/context/before/hook_before_create_context.rb +18 -0
- data/lib/forest_admin_datasource_customizer/decorators/hook/context/before/hook_before_delete_context.rb +18 -0
- data/lib/forest_admin_datasource_customizer/decorators/hook/context/before/hook_before_list_context.rb +19 -0
- data/lib/forest_admin_datasource_customizer/decorators/hook/context/before/hook_before_update_context.rb +19 -0
- data/lib/forest_admin_datasource_customizer/decorators/hook/context/hook_context.rb +22 -0
- data/lib/forest_admin_datasource_customizer/decorators/hook/hook_collection_decorator.rb +95 -0
- data/lib/forest_admin_datasource_customizer/decorators/hook/hooks.rb +26 -0
- data/lib/forest_admin_datasource_customizer/decorators/operators_emulate/operators_emulate_collection_decorator.rb +118 -0
- data/lib/forest_admin_datasource_customizer/decorators/operators_equivalence/operators_equivalence_collection_decorator.rb +50 -0
- data/lib/forest_admin_datasource_customizer/decorators/override/context/create_override_customization_context.rb +16 -0
- data/lib/forest_admin_datasource_customizer/decorators/override/context/delete_override_customization_context.rb +16 -0
- data/lib/forest_admin_datasource_customizer/decorators/override/context/update_override_customization_context.rb +17 -0
- data/lib/forest_admin_datasource_customizer/decorators/override/override_collection_decorator.rb +49 -0
- data/lib/forest_admin_datasource_customizer/decorators/publication/publication_collection_decorator.rb +95 -0
- data/lib/forest_admin_datasource_customizer/decorators/publication/publication_datasource_decorator.rb +57 -0
- data/lib/forest_admin_datasource_customizer/decorators/relation/relation_collection_decorator.rb +268 -0
- data/lib/forest_admin_datasource_customizer/decorators/rename_collection/rename_collection_datasource_decorator.rb +70 -0
- data/lib/forest_admin_datasource_customizer/decorators/rename_collection/rename_collection_decorator.rb +37 -0
- data/lib/forest_admin_datasource_customizer/decorators/rename_field/rename_field_collection_decorator.rb +190 -0
- data/lib/forest_admin_datasource_customizer/decorators/schema/schema_collection_decorator.rb +21 -0
- data/lib/forest_admin_datasource_customizer/decorators/search/search_collection_decorator.rb +135 -0
- data/lib/forest_admin_datasource_customizer/decorators/segment/segment_collection_decorator.rb +60 -0
- data/lib/forest_admin_datasource_customizer/decorators/sort/sort_collection_decorator.rb +127 -0
- data/lib/forest_admin_datasource_customizer/decorators/validation/validation_collection_decorator.rb +82 -0
- data/lib/forest_admin_datasource_customizer/decorators/write/create_relations/create_relations_collection_decorator.rb +75 -0
- data/lib/forest_admin_datasource_customizer/decorators/write/update_relations/update_relations_collection_decorator.rb +96 -0
- data/lib/forest_admin_datasource_customizer/decorators/write/write_datasource_decorator.rb +14 -0
- data/lib/forest_admin_datasource_customizer/decorators/write/write_replace/write_customization_context.rb +18 -0
- data/lib/forest_admin_datasource_customizer/decorators/write/write_replace/write_replace_collection_decorator.rb +125 -0
- data/lib/forest_admin_datasource_customizer/plugins/add_external_relation.rb +27 -0
- data/lib/forest_admin_datasource_customizer/plugins/import_field.rb +74 -0
- data/lib/forest_admin_datasource_customizer/version.rb +1 -1
- metadata +84 -5
- data/README.md +0 -31
|
@@ -0,0 +1,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
|
data/lib/forest_admin_datasource_customizer/decorators/computed/compute_collection_decorator.rb
ADDED
|
@@ -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
|