Skip to content

Commit 970f3d8

Browse files
authored
feat(hooks): developers can dynamically add or remove smart actions fields (#465)
BREAKING CHANGE: fields parameters on hook function is no longer a map of field, it is now an array. change hook is no longer choosen by the field name, field need to have hook defined inside it definition by addin a props hook.
1 parent cb7826d commit 970f3d8

File tree

11 files changed

+306
-181
lines changed

11 files changed

+306
-181
lines changed

app/controllers/forest_liana/actions_controller.rb

Lines changed: 44 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -27,34 +27,45 @@ def get_record
2727
end
2828

2929
def get_smart_action_load_ctx(fields)
30-
fields = fields.reduce({}) do |p, c|
31-
ForestLiana::WidgetsHelper.set_field_widget(c)
32-
p.update(c[:field] => c.merge!(value: nil))
30+
fields = fields.map do |field|
31+
ForestLiana::WidgetsHelper.set_field_widget(field)
32+
field[:value] = nil unless field[:value]
33+
field
3334
end
3435
{:record => get_record, :fields => fields}
3536
end
3637

37-
def get_smart_action_change_ctx(fields)
38-
fields = fields.reduce({}) do |p, c|
39-
field = c.permit!.to_h.symbolize_keys
38+
def get_smart_action_change_ctx(fields, field_changed)
39+
found_field_changed = fields.find{|field| field[:field] == field_changed}
40+
fields = fields.map do |field|
41+
field = field.permit!.to_h.symbolize_keys
4042
ForestLiana::WidgetsHelper.set_field_widget(field)
41-
p.update(c[:field] => field)
43+
field
4244
end
43-
{:record => get_record, :fields => fields}
45+
{:record => get_record, :field_changed => found_field_changed, :fields => fields}
4446
end
4547

46-
def handle_result(result, formatted_fields, action)
47-
if result.nil? || !result.is_a?(Hash)
48-
return render status: 500, json: { error: 'Error in smart action load hook: hook must return an object' }
48+
def handle_result(result, action)
49+
if result.nil? || !result.is_a?(Array)
50+
return render status: 500, json: { error: 'Error in smart action load hook: hook must return an array of fields' }
4951
end
50-
is_same_data_structure = ForestLiana::IsSameDataStructureHelper::Analyser.new(formatted_fields, result, 1)
51-
unless is_same_data_structure.perform
52-
return render status: 500, json: { error: 'Error in smart action hook: fields must be unchanged (no addition nor deletion allowed)' }
52+
53+
# Validate that the fields are well formed.
54+
begin
55+
# action.hooks[:change] is a hashmap here
56+
# to do the validation, only the hook names are require
57+
change_hooks_name = action.hooks[:change].nil? ? nil : action.hooks[:change].keys
58+
ForestLiana::SmartActionFieldValidator.validate_smart_action_fields(result, action.name, change_hooks_name)
59+
rescue ForestLiana::Errors::SmartActionInvalidFieldError => invalid_field_error
60+
FOREST_LOGGER.warn invalid_field_error.message
61+
rescue ForestLiana::Errors::SmartActionInvalidFieldHookError => invalid_hook_error
62+
FOREST_LOGGER.error invalid_hook_error.message
63+
return render status: 500, json: { error: invalid_hook_error.message }
5364
end
5465

5566
# Apply result on fields (transform the object back to an array), preserve order.
56-
fields = action.fields.map do |field|
57-
updated_field = result[field[:field]]
67+
fields = result.map do |field|
68+
updated_field = result.find{|f| f[:field] == field[:field]}
5869

5970
# Reset `value` when not present in `enums` (which means `enums` has changed).
6071
if updated_field[:enums].is_a?(Array)
@@ -72,7 +83,7 @@ def handle_result(result, formatted_fields, action)
7283
updated_field
7384
end
7485

75-
render serializer: nil, json: { fields: fields}, status: :ok
86+
render serializer: nil, json: { fields: fields }, status: :ok
7687
end
7788

7889
def load
@@ -81,32 +92,36 @@ def load
8192
if !action
8293
render status: 500, json: {error: 'Error in smart action load hook: cannot retrieve action from collection'}
8394
else
84-
# Transform fields from array to an object to ease usage in hook, adds null value.
95+
# Get the smart action hook load context
8596
context = get_smart_action_load_ctx(action.fields)
86-
formatted_fields = context[:fields].clone # clone for following test on is_same_data_structure
8797

8898
# Call the user-defined load hook.
8999
result = action.hooks[:load].(context)
90100

91-
handle_result(result, formatted_fields, action)
101+
handle_result(result, action)
92102
end
93103
end
94104

95105
def change
96106
action = get_action(params[:collectionName])
97107

98108
if !action
99-
render status: 500, json: {error: 'Error in smart action change hook: cannot retrieve action from collection'}
100-
else
101-
# Transform fields from array to an object to ease usage in hook.
102-
context = get_smart_action_change_ctx(params[:fields])
103-
formatted_fields = context[:fields].clone # clone for following test on is_same_data_structure
109+
return render status: 500, json: {error: 'Error in smart action change hook: cannot retrieve action from collection'}
110+
elsif params[:fields].nil?
111+
return render status: 500, json: {error: 'Error in smart action change hook: fields params is mandatory'}
112+
elsif !params[:fields].is_a?(Array)
113+
return render status: 500, json: {error: 'Error in smart action change hook: fields params must be an array'}
114+
end
104115

105-
# Call the user-defined change hook.
106-
result = action.hooks[:change][params[:changedField]].(context)
116+
# Get the smart action hook change context
117+
context = get_smart_action_change_ctx(params[:fields], params[:changedField])
107118

108-
handle_result(result, formatted_fields, action)
109-
end
119+
field_changed_hook = context[:field_changed][:hook]
120+
121+
# Call the user-defined change hook.
122+
result = action.hooks[:change][field_changed_hook].(context)
123+
124+
handle_result(result, action)
110125
end
111126
end
112127
end

app/helpers/forest_liana/is_same_data_structure_helper.rb

Lines changed: 0 additions & 44 deletions
This file was deleted.

app/services/forest_liana/apimap_sorter.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ class ApimapSorter
5252
'description',
5353
'position',
5454
'widget',
55+
'hook',
5556
]
5657
KEYS_SEGMENT = ['name']
5758

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
module ForestLiana
2+
class SmartActionFieldValidator
3+
4+
@@accepted_primitive_field_type = [
5+
'String',
6+
'Number',
7+
'Date',
8+
'Boolean',
9+
'File',
10+
'Enum',
11+
'Json',
12+
'Dateonly',
13+
]
14+
15+
@@accepted_array_field_type = [
16+
'String',
17+
'Number',
18+
'Date',
19+
'boolean',
20+
'File',
21+
'Enum',
22+
]
23+
24+
def self.validate_field(field, action_name)
25+
raise ForestLiana::Errors::SmartActionInvalidFieldError.new(action_name, nil, "The field attribute must be defined") if !field || field[:field].nil?
26+
raise ForestLiana::Errors::SmartActionInvalidFieldError.new(action_name, nil, "The field attribute must be a string.") if !field[:field].is_a?(String)
27+
raise ForestLiana::Errors::SmartActionInvalidFieldError.new(action_name, field[:field], "The description attribute must be a string.") if field[:description] && !field[:description].is_a?(String)
28+
raise ForestLiana::Errors::SmartActionInvalidFieldError.new(action_name, field[:field], "The enums attribute must be an array.") if field[:enums] && !field[:enums].is_a?(Array)
29+
raise ForestLiana::Errors::SmartActionInvalidFieldError.new(action_name, field[:field], "The reference attribute must be a string.") if field[:reference] && !field[:reference].is_a?(String)
30+
31+
is_type_valid = field[:type].is_a?(Array) ?
32+
@@accepted_array_field_type.include?(field[:type][0]) :
33+
@@accepted_primitive_field_type.include?(field[:type])
34+
35+
raise ForestLiana::Errors::SmartActionInvalidFieldError.new(action_name, field[:field], "The type attribute must be a valid type. See the documentation for more information. https://docs.forestadmin.com/documentation/reference-guide/fields/create-and-manage-smart-fields#available-field-options.") if !is_type_valid
36+
end
37+
38+
def self.validate_field_change_hook(field, action_name, hooks)
39+
raise ForestLiana::Errors::SmartActionInvalidFieldHookError.new(action_name, field[:field], field[:hook]) if field[:hook] && !hooks.find{|hook| hook == field[:hook]}
40+
end
41+
42+
def self.validate_smart_action_fields(fields, action_name, change_hooks)
43+
fields.map{|field|
44+
self.validate_field(field.symbolize_keys, action_name)
45+
self.validate_field_change_hook(field.symbolize_keys, action_name, change_hooks) if change_hooks
46+
}
47+
end
48+
end
49+
end

config/initializers/errors.rb

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,23 @@ def initialize(message="Invalid SQL query for this Live Query")
1313
end
1414
end
1515

16+
class SmartActionInvalidFieldError < StandardError
17+
def initialize(action_name=nil, field_name=nil, message=nil)
18+
error_message = ""
19+
error_message << "Error while parsing action \"#{action_name}\"" if !action_name.nil?
20+
error_message << " on field \"#{field_name}\"" if !field_name.nil?
21+
error_message << ": " if !field_name.nil? || !action_name.nil?
22+
error_message << message if !message.nil?
23+
super(error_message)
24+
end
25+
end
26+
27+
class SmartActionInvalidFieldHookError < StandardError
28+
def initialize(action_name=nil, field_name=nil, hook_name=nil)
29+
super("The hook \"#{hook_name}\" of \"#{field_name}\" field on the smart action \"#{action_name}\" is not defined.")
30+
end
31+
end
32+
1633
class ExpectedError < StandardError
1734
attr_reader :error_code, :status, :message, :name
1835

lib/forest_liana/bootstrapper.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ def generate_action_hooks()
6060
a = get_action(c, action['name'])
6161
load = !a.hooks.nil? && a.hooks.key?(:load) && a.hooks[:load].is_a?(Proc)
6262
change = !a.hooks.nil? && a.hooks.key?(:change) && a.hooks[:change].is_a?(Hash) ? a.hooks[:change].keys : []
63-
action['hooks'] = {:load => load, :change => change}
63+
action['hooks'] = {'load' => load, 'change' => change}
6464
end
6565
end
6666
end

lib/forest_liana/schema_file_updater.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ class SchemaFileUpdater
6363
'description',
6464
'position',
6565
'widget',
66+
'hook',
6667
]
6768
KEYS_SEGMENT = ['name']
6869

@@ -96,6 +97,13 @@ def initialize filename, collections, meta
9697
end
9798

9899
collection['actions'] = collection['actions'].map do |action|
100+
begin
101+
SmartActionFieldValidator.validate_smart_action_fields(action['fields'], action['name'], action['hooks']['change'])
102+
rescue ForestLiana::Errors::SmartActionInvalidFieldError => invalid_field_error
103+
FOREST_LOGGER.warn invalid_field_error.message
104+
rescue ForestLiana::Errors::SmartActionInvalidFieldHookError => invalid_hook_error
105+
FOREST_LOGGER.error invalid_hook_error.message
106+
end
99107
action['fields'] = action['fields'].map { |field| field.slice(*KEYS_ACTION_FIELD) }
100108
action.slice(*KEYS_ACTION)
101109
end

spec/helpers/forest_liana/is_same_data_structure_helper_spec.rb

Lines changed: 0 additions & 87 deletions
This file was deleted.

0 commit comments

Comments
 (0)