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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a57e523c5b7c53d6a40452a6f9ea21a7a1b858ab1a39a3f5cac3fe83ec93ec27
4
- data.tar.gz: a6cf096f51d0ed6c12fa9787e69b74c75663f1eba63981822369ba41a73bd621
3
+ metadata.gz: 26154cad59202641c5e5b05438306ca77b63a79a764a8f65d4a31a0c7808c33f
4
+ data.tar.gz: 9e9bfab31439f364e7a0e3624001a952a6f51b670fb6677cbf00cfcb476006a8
5
5
  SHA512:
6
- metadata.gz: ba6b15e85b6c7bd38cf9a32b26824b52c8078a33b0001add15942a042c8d5afd8fd4d7a3e21d742cdce700338a62f1a0b928d3d40ee8af171ae92acad415d02f
7
- data.tar.gz: 9deebebbc665d471aa7450e23df9bd12df12d0333cca89aefa1cd3cabf710a755c0821ee944d41daa8ee1437b31ce5ddb90256f53b8be8f55cd1b819618a6b2d
6
+ metadata.gz: 601b7736694dbe374753c7b8c9d02563c7a7ea4e17fc1d0eb50964942953ebcdc6bed0c373fafe058fbaaa84e80171e685db8ed3d3a4612c7c82bbdfb81a3a84
7
+ data.tar.gz: d592ca27fb7c138fa7e347d8e99fcd09a55a914e959faa7824144441163ae306201733c6c35e284a953d803a1a3344c07ae003ad04e44c226b31f52d89544b99
@@ -12,12 +12,12 @@ Gem::Specification.new do |spec|
12
12
  spec.summary = "Ruby agent for Forest Admin."
13
13
  spec.description = "Forest is a modern admin interface that works on all major web frameworks. This gem makes Forest
14
14
  admin work on any Ruby application."
15
- spec.license = "MIT"
15
+ spec.license = "GPL-3.0"
16
16
  spec.required_ruby_version = ">= 3.0.0"
17
17
 
18
18
  spec.metadata["homepage_uri"] = spec.homepage
19
19
  spec.metadata["source_code_uri"] = "https://github.com/ForestAdmin/agent-ruby"
20
- spec.metadata["changelog_uri"] = "https://github.com/ForestAdmin/agent-ruby/CHANGELOG.md"
20
+ spec.metadata["changelog_uri"] = "https://github.com/ForestAdmin/agent-ruby/blob/main/CHANGELOG.md"
21
21
  spec.metadata["rubygems_mfa_required"] = "false"
22
22
 
23
23
  # Specify which files should be added to the gem when it is released.
@@ -34,5 +34,6 @@ admin work on any Ruby application."
34
34
  spec.require_paths = ["lib"]
35
35
 
36
36
  spec.add_dependency "activesupport", ">= 6.1"
37
+ spec.add_dependency 'marcel', '~> 1.0', '>= 1.0.4'
37
38
  spec.add_dependency "zeitwerk", "~> 2.3"
38
39
  end
@@ -1,5 +1,6 @@
1
1
  module ForestAdminDatasourceCustomizer
2
2
  class CollectionCustomizer
3
+ include ForestAdminDatasourceToolkit::Validations
3
4
  attr_reader :datasource_customizer, :stack, :name
4
5
 
5
6
  def initialize(datasource_customizer, stack, name)
@@ -8,18 +9,304 @@ module ForestAdminDatasourceCustomizer
8
9
  @name = name
9
10
  end
10
11
 
12
+ def add_action(name, definition)
13
+ push_customization { @stack.action.get_collection(@name).add_action(name, definition) }
14
+ end
15
+
11
16
  def schema
12
- @stack.datasource.get_collection(@name).schema
17
+ @stack.validation.get_collection(@name).schema
13
18
  end
14
19
 
15
20
  def collection
16
- @stack.datasource.get_collection(@name)
21
+ @stack.validation.get_collection(@name)
17
22
  end
18
23
 
19
24
  def use(plugin, options = [])
20
- push_customization(
21
- -> { plugin.run(@datasource_customizer, self, options) }
22
- )
25
+ push_customization { plugin.new.run(@datasource_customizer, self, options) }
26
+ end
27
+
28
+ def disable_count
29
+ push_customization { @stack.schema.get_collection(@name).override_schema(countable: false) }
30
+ end
31
+
32
+ def replace_search(definition)
33
+ push_customization { @stack.search.get_collection(@name).replace_search(definition) }
34
+ end
35
+
36
+ def add_field(name, definition)
37
+ push_customization do
38
+ collection_before_relations = @stack.early_computed.get_collection(@name)
39
+ collection_after_relations = @stack.late_computed.get_collection(@name)
40
+ can_be_computed_before_relations = definition.dependencies.all? do |field|
41
+ !ForestAdminDatasourceToolkit::Utils::Collection.get_field_schema(collection_before_relations, field).nil?
42
+ rescue StandardError
43
+ false
44
+ end
45
+
46
+ collection = can_be_computed_before_relations ? collection_before_relations : collection_after_relations
47
+
48
+ collection.register_computed(name, definition)
49
+ end
50
+ end
51
+
52
+ def emulate_field_operator(name, operator)
53
+ push_customization do
54
+ collection = if @stack.early_op_emulate.get_collection(@name).schema[:fields].key?(name)
55
+ @stack.early_op_emulate.get_collection(@name)
56
+ else
57
+ @stack.late_op_emulate.get_collection(@name)
58
+ end
59
+
60
+ collection.emulate_field_operator(name, operator)
61
+ end
62
+ end
63
+
64
+ def emulate_field_filtering(name)
65
+ push_customization do
66
+ collection = @stack.late_op_emulate.get_collection(@name)
67
+ field = collection.schema[:fields][name]
68
+
69
+ if field.column_type.is_a?(String)
70
+ operators = Rules.get_allowed_operators_for_column_type[field.column_type]
71
+
72
+ operators.each do |operator|
73
+ emulate_field_operator(name, operator) unless field.filter_operators&.include?(operator)
74
+ end
75
+ end
76
+ end
77
+ end
78
+
79
+ def replace_field_operator(name, operator, &replacer)
80
+ push_customization do
81
+ collection = if @stack.early_op_emulate.get_collection(@name).schema[:fields].key?(name)
82
+ @stack.early_op_emulate.get_collection(@name)
83
+ else
84
+ @stack.late_op_emulate.get_collection(@name)
85
+ end
86
+
87
+ collection.replace_field_operator(name, operator, &replacer)
88
+ end
89
+ end
90
+
91
+ # Add a many to one relation to the collection
92
+ # @param name name of the new relation
93
+ # @param foreign_collection name of the targeted collection
94
+ # @param options extra information about the relation
95
+ # @example
96
+ # books.add_many_to_one_relation('my_author', 'persons', { foreign_key: 'author_id' })
97
+ def add_many_to_one_relation(name, foreign_collection, options = {})
98
+ push_relation(name, {
99
+ type: 'ManyToOne',
100
+ foreign_collection: foreign_collection,
101
+ foreign_key: options[:foreign_key],
102
+ foreign_key_target: options[:foreign_key_target]
103
+ })
104
+ end
105
+
106
+ # Add a one to many relation to the collection
107
+ # @param name name of the new relation
108
+ # @param foreign_collection name of the targeted collection
109
+ # @param options extra information about the relation
110
+ # @example
111
+ # persons.add_one_to_many_relation('written_books', 'books', { origin_key: 'author_id' })
112
+ def add_one_to_many_relation(name, foreign_collection, options = {})
113
+ push_relation(name, {
114
+ type: 'OneToMany',
115
+ foreign_collection: foreign_collection,
116
+ origin_key: options[:origin_key],
117
+ origin_key_target: options[:origin_key_target]
118
+ })
119
+ end
120
+
121
+ # Add a one to one relation to the collection
122
+ # @param name name of the new relation
123
+ # @param foreign_collection name of the targeted collection
124
+ # @param options extra information about the relation
125
+ # @example
126
+ # persons.add_one_to_one_relation('best_friend', 'persons', { origin_key: 'best_friend_id' })
127
+ def add_one_to_one_relation(name, foreign_collection, options = {})
128
+ push_relation(name, {
129
+ type: 'OneToOne',
130
+ foreign_collection: foreign_collection,
131
+ origin_key: options[:origin_key],
132
+ origin_key_target: options[:origin_key_target]
133
+ })
134
+ end
135
+
136
+ # Add a many to many relation to the collection
137
+ # @param name name of the new relation
138
+ # @param foreign_collection name of the targeted collection
139
+ # @param through_collection name of the intermediary collection
140
+ # @param options extra information about the relation
141
+ # @example
142
+ # dvds.add_many_to_many_relation('rentals_of_this_dvd', 'rentals', 'dvd_rentals', {
143
+ # origin_key: 'dvd_id',
144
+ # foreign_key: 'rental_id'
145
+ # })
146
+ def add_many_to_many_relation(name, foreign_collection, through_collection, options = {})
147
+ push_relation(name, {
148
+ type: 'ManyToMany',
149
+ foreign_collection: foreign_collection,
150
+ through_collection: through_collection,
151
+ origin_key: options[:origin_key],
152
+ origin_key_target: options[:origin_key_target],
153
+ foreign_key: options[:foreign_key],
154
+ foreign_key_target: options[:foreign_key_target]
155
+ })
156
+ end
157
+
158
+ def add_external_relation(name, definition)
159
+ use(ForestAdminDatasourceCustomizer::Plugins::AddExternalRelation, { name: name }.merge(definition))
160
+ end
161
+
162
+ def import_field(name, definition)
163
+ use(ForestAdminDatasourceCustomizer::Plugins::ImportField, { name: name }.merge(definition))
164
+ end
165
+
166
+ # Add a new validator to the edition form of a given field
167
+ # @param name The name of the field
168
+ # @param operator The validator that you wish to add
169
+ # @param value A configuration value that the validator may need
170
+ # @example
171
+ # .add_field_validation('first_name', Operators::LONGER_THAN, 2)
172
+ def add_field_validation(name, operator, value = nil)
173
+ push_customization do
174
+ @stack.validation.get_collection(@name).add_validation(name, { operator: operator, value: value })
175
+ end
176
+ end
177
+
178
+ # Enable sorting on a specific field using emulation.
179
+ # As for all the emulation method, the field sorting will be done in-memory.
180
+ # @param name the name of the field to enable emulation on
181
+ # @example
182
+ # .emulate_field_sorting('fullName')
183
+ def emulate_field_sorting(name)
184
+ push_customization { @stack.sort.get_collection(@name).emulate_field_sorting(name) }
185
+ end
186
+
187
+ # Replace an implementation for the sorting.
188
+ # The field sorting will be done by the datasource.
189
+ # @param name the name of the field to enable sort
190
+ # @param equivalent_sort the sort equivalent
191
+ # @example
192
+ # .replace_field_sorting(
193
+ # 'fullName',
194
+ # [
195
+ # { field: 'firstName', ascending: true },
196
+ # { field: 'lastName', ascending: true },
197
+ # ]
198
+ # )
199
+ def replace_field_sorting(name, equivalent_sort)
200
+ push_customization { @stack.sort.get_collection(@name).replace_field_sorting(name, equivalent_sort) }
201
+ end
202
+
203
+ # Remove fields from the exported schema (they will still be usable within the agent).
204
+ # @param names the names of the field or the relation
205
+ # @example
206
+ # .remove_field('fieldNameToRemove', 'relationNameToRemove')
207
+ def remove_field(*names)
208
+ push_customization do
209
+ collection = @stack.publication.get_collection(@name)
210
+ names.each { |name| collection.change_field_visibility(name, false) }
211
+ end
212
+ end
213
+
214
+ # Rename fields from the exported schema.
215
+ # @param current_name the current name of the field or the relation in a given collection
216
+ # @param new_name the new name of the field or the relation
217
+ # @example
218
+ # rename_field('currentFieldOrRelationName', 'newFieldOrRelationName')
219
+ def rename_field(current_name, new_name)
220
+ push_customization { @stack.rename_field.get_collection(@name).rename_field(current_name, new_name) }
221
+ end
222
+
223
+ # Replace the write behavior of a field.
224
+ # @param name the name of the field
225
+ # @param definition the function or a value to represent the write behavior
226
+ # @example
227
+ # .replace_field_writing('author_last_name') do
228
+ # { 'author' => { 'last_name' => value } }
229
+ # end
230
+ def replace_field_writing(name, &definition)
231
+ push_customization { @stack.write.get_collection(@name).replace_field_writing(name, &definition) }
232
+ end
233
+
234
+ # Create a new API chart
235
+ # @param name name of the chart
236
+ # @param definition definition of the chart
237
+ # @example
238
+ # .add_chart('num_customers') do |context, result_builder|
239
+ # return result_builder.distribution({
240
+ # tomatoes: 10,
241
+ # potatoes: 20,
242
+ # carrots: 30,
243
+ # });
244
+ # end
245
+ def add_chart(name, &definition)
246
+ push_customization { @stack.chart.get_collection(@name).add_chart(name, &definition) }
247
+ end
248
+
249
+ # Add a new hook handler to an action
250
+ # @param position Either if the hook is executed before or after the action
251
+ # @param type Type of action which should be hooked
252
+ # @param handler Callback that should be executed when the hook is triggered
253
+ # @example
254
+ # .add_hook('before', 'list') do |context|
255
+ # # Do something before the list action
256
+ # end
257
+ def add_hook(position, type, &handler)
258
+ push_customization { @stack.hook.get_collection(@name).add_hook(position, type, handler) }
259
+ end
260
+
261
+ # Add a new segment on the collection.
262
+ # @param name the name of the segment
263
+ # @param definition a function used to generate a condition tree or a condition tree
264
+ # @example
265
+ # .add_segment(
266
+ # 'Wrote more than 2 books',
267
+ # { field: 'booksCount', operator: 'GreaterThan', value: 2 }
268
+ # );
269
+ def add_segment(name, &definition)
270
+ push_customization { @stack.segment.get_collection(@name).add_segment(name, definition) }
271
+ end
272
+
273
+ # Choose how binary data should be transported to the GUI.
274
+ # By default, all fields are transported as 'datauri', with the exception of primary and foreign
275
+ # keys.
276
+ #
277
+ # Using 'datauri' allows to use the FilePicker widget, while 'hex' is more suitable for
278
+ # short binary data (for instance binary uuids).
279
+ #
280
+ # @param name the name of the field
281
+ # @param binary_mode either 'datauri' or 'hex'
282
+ # @example
283
+ # .replace_field_binary_mode('avatar', 'datauri');
284
+ def replace_field_binary_mode(name, binary_mode)
285
+ push_customization { @stack.binary.get_collection(@name).set_binary_mode(name, binary_mode) }
286
+ end
287
+
288
+ def override_create(&handler)
289
+ push_customization { @stack.override.get_collection(@name).add_create_handler(handler) }
290
+ end
291
+
292
+ def override_update(&handler)
293
+ push_customization { @stack.override.get_collection(@name).add_update_handler(handler) }
294
+ end
295
+
296
+ def override_delete(&handler)
297
+ push_customization { @stack.override.get_collection(@name).add_delete_handler(handler) }
298
+ end
299
+
300
+ private
301
+
302
+ def push_customization(&customization)
303
+ @stack.queue_customization(customization)
304
+
305
+ self
306
+ end
307
+
308
+ def push_relation(name, definition)
309
+ push_customization { @stack.relation.get_collection(@name).add_relation(name, definition) }
23
310
  end
24
311
  end
25
312
  end
@@ -0,0 +1,18 @@
1
+ module ForestAdminDatasourceCustomizer
2
+ module Context
3
+ class AgentCustomizationContext
4
+ include ForestAdminDatasourceCustomizer::Context::RelaxedWrappers
5
+
6
+ attr_reader :caller
7
+
8
+ def initialize(datasource, caller)
9
+ @real_datasource = datasource
10
+ @caller = caller
11
+ end
12
+
13
+ def datasource
14
+ RelaxedDataSource.new(@real_datasource, @caller)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,15 @@
1
+ module ForestAdminDatasourceCustomizer
2
+ module Context
3
+ class CollectionCustomizationContext < AgentCustomizationContext
4
+ include ForestAdminDatasourceCustomizer::Context::RelaxedWrappers
5
+ def initialize(collection, caller)
6
+ super(collection.datasource, caller)
7
+ @real_collection = collection
8
+ end
9
+
10
+ def collection
11
+ RelaxedCollection.new(@real_collection, @caller)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,50 @@
1
+ module ForestAdminDatasourceCustomizer
2
+ module Context
3
+ module RelaxedWrappers
4
+ class RelaxedCollection
5
+ include ForestAdminDatasourceToolkit::Components::Query
6
+
7
+ def initialize(collection, caller)
8
+ @collection = collection
9
+ @caller = caller
10
+ end
11
+
12
+ def native_driver
13
+ @collection.native_driver
14
+ end
15
+
16
+ def schema
17
+ @collection.schema
18
+ end
19
+
20
+ def execute(name, form_values = {}, filter = nil)
21
+ @collection.execute(@caller, name, form_values, filter)
22
+ end
23
+
24
+ def get_form(name, data = nil, filter = nil, metas = {})
25
+ @collection.get_form(@caller, name, data, filter, metas)
26
+ end
27
+
28
+ def create(data)
29
+ @collection.create(@caller, data)
30
+ end
31
+
32
+ def list(filter, projection)
33
+ @collection.list(@caller, filter, projection)
34
+ end
35
+
36
+ def update(filter, data)
37
+ @collection.update(@caller, filter, data)
38
+ end
39
+
40
+ def delete(filter)
41
+ @collection.delete(@caller, filter)
42
+ end
43
+
44
+ def aggregate(filter, aggregation, limit = nil)
45
+ @collection.aggregate(@caller, filter, aggregation, limit)
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,18 @@
1
+ module ForestAdminDatasourceCustomizer
2
+ module Context
3
+ module RelaxedWrappers
4
+ class RelaxedDataSource
5
+ def initialize(datasource, caller)
6
+ @real_datasource = datasource
7
+ @caller = caller
8
+ end
9
+
10
+ # Get a collection from a datasource
11
+ # @param name the name of the collection
12
+ def get_collection(name)
13
+ RelaxedCollection.new(@real_datasource.get_collection(name), @caller)
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -8,7 +8,7 @@ module ForestAdminDatasourceCustomizer
8
8
  end
9
9
 
10
10
  def schema
11
- @stack.datasource.schema
11
+ @stack.validation.schema
12
12
  end
13
13
 
14
14
  def get_collection(name)
@@ -19,33 +19,63 @@ module ForestAdminDatasourceCustomizer
19
19
  @stack.datasource.collections.transform_values { |collection| get_collection(collection.name) }
20
20
  end
21
21
 
22
- def datasource
23
- # TODO: call @stack.apply_queued_customizations(logger);
22
+ def datasource(logger)
23
+ @stack.apply_queued_customizations(logger)
24
24
 
25
25
  @stack.datasource
26
26
  end
27
27
 
28
- def add_datasource(datasource, _options)
29
- # TODO: add include/exclude behavior
30
- # TODO: add rename behavior
28
+ def add_datasource(datasource, options)
29
+ @stack.queue_customization(lambda {
30
+ if options[:include] || options[:exclude]
31
+ publication_decorator = Decorators::Publication::PublicationDatasourceDecorator.new(datasource)
32
+ publication_decorator.keep_collections_matching(options[:include], options[:exclude])
33
+ datasource = publication_decorator
34
+ end
31
35
 
32
- datasource.collections.each_value { |collection| @composite_datasource.add_collection(collection) }
36
+ if options[:rename]
37
+ rename_collection_decorator = Decorators::RenameCollection::RenameCollectionDatasourceDecorator.new(
38
+ datasource
39
+ )
40
+ rename_collection_decorator.rename_collections(options[:rename])
41
+ datasource = rename_collection_decorator
42
+ end
43
+
44
+ datasource.collections.each_value do |collection|
45
+ @composite_datasource.add_collection(collection)
46
+ end
47
+ })
48
+
49
+ self
33
50
  end
34
51
 
35
- def add_chart(name, definition)
36
- # TODO: to implement
52
+ # Create a new API chart
53
+ # @param name name of the chart
54
+ # @param definition definition of the chart
55
+ # @example
56
+ # .addChart('num_customers') { |context, result_builder| result_builder.value(123) }
57
+ def add_chart(name, &definition)
58
+ push_customization { @stack.chart.add_chart(name, &definition) }
37
59
  end
38
60
 
39
61
  def use(plugin, options)
40
- # TODO: to implement
62
+ push_customization { plugin.new.run(self, nil, options) }
41
63
  end
42
64
 
43
65
  def customize_collection(name, handle)
44
- # TODO: to implement
66
+ handle.call(get_collection(name))
67
+ end
68
+
69
+ def remove_collection(*names)
70
+ @stack.queue_customization(-> { @stack.publication.keep_collections_matching(nil, names) })
71
+
72
+ self
45
73
  end
46
74
 
47
- def remove_collection(names)
48
- # TODO: to implement
75
+ private
76
+
77
+ def push_customization(&customization)
78
+ @stack.queue_customization(customization)
49
79
  end
50
80
  end
51
81
  end
@@ -0,0 +1,137 @@
1
+ module ForestAdminDatasourceCustomizer
2
+ module Decorators
3
+ module Action
4
+ class ActionCollectionDecorator < ForestAdminDatasourceToolkit::Decorators::CollectionDecorator
5
+ include ForestAdminDatasourceToolkit::Components
6
+
7
+ def initialize(child_collection, datasource)
8
+ super
9
+ @actions = {}
10
+ end
11
+
12
+ def add_action(name, action)
13
+ action.build_fields
14
+ @actions[name] = action
15
+
16
+ mark_schema_as_dirty
17
+ end
18
+
19
+ def execute(caller, name, data, filter = nil)
20
+ action = @actions[name]
21
+ return @child_collection.execute(caller, name, data, filter) if action.nil?
22
+
23
+ context = get_context(caller, action, data, filter)
24
+
25
+ result_builder = ResultBuilder.new
26
+ result = action.execute.call(context, result_builder)
27
+
28
+ return result if result.is_a? Hash
29
+
30
+ result_builder.success
31
+ end
32
+
33
+ def get_form(caller, name, data = nil, filter = nil, metas = {})
34
+ action = @actions[name]
35
+ return @child_collection.get_form(caller, name, data, filter, metas) if action.nil?
36
+ return [] if action.form.nil?
37
+
38
+ form_values = data || {}
39
+ used = []
40
+ context = get_context(caller, action, form_values, filter, used, metas[:change_field])
41
+
42
+ dynamic_fields = action.form
43
+ if metas[:search_field]
44
+ # in the case of a search hook,
45
+ # we don't want to rebuild all the fields. only the one searched
46
+ dynamic_fields = dynamic_fields.select { |field| field.label == metas[:search_field] }
47
+ end
48
+ dynamic_fields = drop_defaults(context, dynamic_fields, form_values)
49
+ dynamic_fields = drop_ifs(context, dynamic_fields) unless metas[:include_hidden_fields]
50
+
51
+ fields = drop_deferred(context, metas[:search_values], dynamic_fields)
52
+
53
+ fields.each do |field|
54
+ if field.value.nil?
55
+ # customer did not define a handler to rewrite the previous value => reuse current one.
56
+ field.value = form_values[field.label]
57
+ end
58
+
59
+ # fields that were accessed through the context.get_form_value(x) getter should be watched.
60
+ field.watch_changes = used.include?(field.label)
61
+ end
62
+
63
+ fields
64
+ end
65
+
66
+ def refine_schema(sub_schema)
67
+ sub_schema[:actions] = @actions
68
+
69
+ sub_schema
70
+ end
71
+
72
+ private
73
+
74
+ def drop_defaults(context, fields, data)
75
+ unvalued_fields = fields.reject { |field| data.key?(field.label) }
76
+ defaults = unvalued_fields.map { |field| evaluate(context, field.default_value) }
77
+ unvalued_fields.each_with_index { |field, index| data[field.label] = defaults[index] }
78
+
79
+ fields.each { |field| field.default_value = nil }
80
+
81
+ fields
82
+ end
83
+
84
+ def drop_ifs(context, fields)
85
+ if_values = fields.map { |field| !field.if_condition || evaluate(context, field.if_condition) }
86
+ new_fields = fields.select.with_index { |_field, index| if_values[index] }
87
+ new_fields.each do |field|
88
+ field = field.dup
89
+ field.if_condition = nil
90
+ end
91
+
92
+ new_fields
93
+ end
94
+
95
+ def drop_deferred(context, search_values, fields)
96
+ new_fields = []
97
+ fields.each do |field|
98
+ field = field.dup
99
+ field.instance_variables.each do |key|
100
+ key = key.to_s.delete('@').to_sym
101
+
102
+ next unless field.respond_to?(key)
103
+
104
+ # call getter corresponding to the key and then set the evaluated value
105
+ value = field.send(key)
106
+ key = key.to_s.concat('=').to_sym
107
+
108
+ field.send(key, evaluate(context, value, search_values&.dig(field.label)))
109
+ end
110
+
111
+ new_fields << Actions::ActionFieldFactory.build(field.to_h)
112
+ end
113
+
114
+ new_fields
115
+ end
116
+
117
+ def evaluate(context, value, search_value = nil)
118
+ if value.respond_to?(:call)
119
+ return value.call(context, search_value) if search_value
120
+
121
+ return value.call(context)
122
+ end
123
+
124
+ value
125
+ end
126
+
127
+ def get_context(caller, action, form_values = [], filter = nil, used = [], change_field = nil)
128
+ if action.scope == Types::ActionScope::SINGLE
129
+ return Context::ActionContextSingle.new(self, caller, form_values, filter, used, change_field)
130
+ end
131
+
132
+ Context::ActionContext.new(self, caller, form_values, filter, used, change_field)
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end