engine2 1.0.5 → 1.0.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (86) hide show
  1. checksums.yaml +5 -5
  2. data/app/actions.coffee +93 -58
  3. data/app/app.css +12 -0
  4. data/app/engine2.coffee +42 -24
  5. data/conf/message.yaml +1 -0
  6. data/conf/message_pl.yaml +1 -0
  7. data/config.coffee +2 -2
  8. data/engine2.gemspec +1 -1
  9. data/lib/engine2/action.rb +130 -126
  10. data/lib/engine2/action/array.rb +4 -4
  11. data/lib/engine2/action/decode.rb +13 -9
  12. data/lib/engine2/action/infra.rb +3 -3
  13. data/lib/engine2/action/list.rb +17 -10
  14. data/lib/engine2/action_node.rb +1 -2
  15. data/lib/engine2/core.rb +35 -7
  16. data/lib/engine2/model.rb +64 -15
  17. data/lib/engine2/post_bootstrap.rb +1 -1
  18. data/lib/engine2/pre_bootstrap.rb +10 -0
  19. data/lib/engine2/scheme.rb +2 -2
  20. data/lib/engine2/templates.rb +8 -0
  21. data/lib/engine2/type_info.rb +37 -15
  22. data/lib/engine2/version.rb +1 -1
  23. data/package.json +8 -5
  24. data/views/fields/blob.slim +1 -1
  25. data/views/fields/bs_select.slim +2 -2
  26. data/views/fields/bsselect_picker.slim +4 -4
  27. data/views/fields/bsselect_picker_opt.slim +5 -5
  28. data/views/fields/checkbox.slim +4 -4
  29. data/views/fields/checkbox_buttons.slim +3 -3
  30. data/views/fields/checkbox_buttons_opt.slim +3 -3
  31. data/views/fields/currency.slim +2 -2
  32. data/views/fields/date.slim +4 -4
  33. data/views/fields/date_range.slim +9 -9
  34. data/views/fields/date_time.slim +9 -9
  35. data/views/fields/datetime.slim +8 -8
  36. data/views/fields/decimal.slim +1 -1
  37. data/views/fields/decimal_date.slim +3 -3
  38. data/views/fields/decimal_time.slim +3 -3
  39. data/views/fields/email.slim +3 -3
  40. data/views/fields/file_store.slim +4 -4
  41. data/views/fields/input_text.slim +4 -4
  42. data/views/fields/integer.slim +1 -1
  43. data/views/fields/list_bsmselect.slim +20 -0
  44. data/views/fields/list_bsselect.slim +5 -5
  45. data/views/fields/list_bsselect_opt.slim +6 -6
  46. data/views/fields/list_buttons.slim +1 -1
  47. data/views/fields/list_buttons_opt.slim +2 -2
  48. data/views/fields/list_select.slim +4 -4
  49. data/views/fields/list_select_opt.slim +5 -5
  50. data/views/fields/password.slim +4 -4
  51. data/views/fields/radio_checkbox.slim +3 -3
  52. data/views/fields/scaffold.slim +1 -1
  53. data/views/fields/scaffold_picker.slim +5 -5
  54. data/views/fields/select_picker.slim +3 -3
  55. data/views/fields/select_picker_opt.slim +4 -4
  56. data/views/fields/text_area.slim +3 -3
  57. data/views/fields/time.slim +5 -4
  58. data/views/fields/typeahead_picker.slim +5 -5
  59. data/views/scaffold/fields.slim +4 -4
  60. data/views/scaffold/form.slim +1 -1
  61. data/views/scaffold/form_collapse.slim +4 -3
  62. data/views/scaffold/form_tabs.slim +3 -2
  63. data/views/scaffold/search.slim +2 -2
  64. data/views/scaffold/search_collapse.slim +6 -5
  65. data/views/scaffold/search_tabs.slim +4 -3
  66. data/views/scaffold/view.slim +2 -2
  67. data/views/scaffold/view_collapse.slim +5 -4
  68. data/views/scaffold/view_tabs.slim +4 -3
  69. data/views/search_fields/bsmselect_picker.slim +4 -4
  70. data/views/search_fields/bsselect_picker.slim +4 -4
  71. data/views/search_fields/checkbox.slim +3 -3
  72. data/views/search_fields/checkbox2.slim +5 -5
  73. data/views/search_fields/checkbox_buttons.slim +3 -3
  74. data/views/search_fields/date_range.slim +8 -8
  75. data/views/search_fields/decimal_date_range.slim +5 -5
  76. data/views/search_fields/input_text.slim +2 -2
  77. data/views/search_fields/integer.slim +1 -1
  78. data/views/search_fields/integer_range.slim +2 -2
  79. data/views/search_fields/list_bsmselect.slim +4 -4
  80. data/views/search_fields/list_bsselect.slim +4 -4
  81. data/views/search_fields/list_buttons.slim +2 -2
  82. data/views/search_fields/list_select.slim +3 -3
  83. data/views/search_fields/scaffold_picker.slim +2 -2
  84. data/views/search_fields/select_picker.slim +3 -3
  85. data/views/search_fields/typeahead_picker.slim +4 -4
  86. metadata +6 -5
@@ -45,7 +45,7 @@ module Engine2
45
45
 
46
46
  if order_str = params[:order]
47
47
  order = order_str.to_sym
48
- handler.permit lookup(:info, order, :sort)
48
+ handler.permit lookup(:fields, order, :sort)
49
49
  entries = entries.sort_by{|e|e[order].to_s}
50
50
  entries = entries.reverse if params[:asc] == "true"
51
51
  end
@@ -60,14 +60,14 @@ module Engine2
60
60
  def list_search entries, handler, search
61
61
  hash = JSON.parse(search, symbolize_names: true) rescue handler.halt_forbidden
62
62
  model = assets[:model]
63
- sfields = lookup(:search_fields)
63
+ sfields = lookup(:search_field_list)
64
64
  handler.permit sfields
65
65
  hash.each_pair do |name, value|
66
66
  handler.permit sfields.include?(name)
67
67
 
68
- type_info = get_type_info(name)
68
+ type_info = model.find_type_info(name)
69
69
  entries = if filter = (@filters && @filters[name]) || (dynamic? && (static.filters && static.filters[name]))
70
- filter.(entries, hash, handler)
70
+ filter.(handler, entries, hash)
71
71
  elsif filter = DefaultFilters[type_info[:otype]]
72
72
  filter.(entries, name, value, type_info, hash)
73
73
  else
@@ -19,8 +19,8 @@ module Engine2
19
19
  end
20
20
 
21
21
  def post_process
22
- if fields = @meta[:fields]
23
- fields = fields - static.meta[:fields] if dynamic?
22
+ if fields = @meta[:field_list]
23
+ fields = fields - static.meta[:field_list] if dynamic?
24
24
  # no decorate here
25
25
  fields.each do |name|
26
26
  type_info = assets[:model].type_info[name] # foreign keys ?
@@ -55,7 +55,7 @@ module Engine2
55
55
  action_type :decode_list
56
56
 
57
57
  def invoke handler
58
- {entries: get_query.limit(200).all}
58
+ {entries: get_query.limit(200).load_all}
59
59
  end
60
60
  end
61
61
 
@@ -64,20 +64,24 @@ module Engine2
64
64
 
65
65
  def pre_run
66
66
  super
67
- limit 10
67
+ @limit = 10
68
68
  end
69
69
 
70
70
  def limit lmt
71
- @meta[:limit] = lmt
71
+ @limit = lmt
72
+ end
73
+
74
+ def case_insensitive
75
+ @case_insensitive = true
72
76
  end
73
77
 
74
78
  def invoke handler
75
79
  if query = handler.params[:query]
76
- condition = @meta[:decode_fields].map{|f|f.like("%#{query}%")}.reduce{|q, f| q | f}
77
- {entries: get_query.where(condition).limit(@meta[:limit]).all}
80
+ condition = @meta[:decode_fields].map{|f|f.like("%#{query}%", case_insensitive: @case_insensitive)}.reduce{|q, f| q | f}
81
+ {entries: get_query.where(condition).limit(@limit).load_all}
78
82
  else
79
83
  handler.permit id = handler.params[:id]
80
- record = get_query[Hash[assets[:model].primary_keys.zip(split_keys(id))]]
84
+ record = get_query.load Hash[assets[:model].primary_keys.zip(split_keys(id))]
81
85
  # handler.halt_not_found(LOCS[:no_entry]) unless record
82
86
  {entry: record}
83
87
  end
@@ -92,7 +96,7 @@ module Engine2
92
96
  end
93
97
 
94
98
  def invoke_decode handler, ids
95
- records = get_query.where(ids.map{|keys| Hash[assets[:model].primary_keys.zip(keys)]}.reduce{|q, c| q | c}).all
99
+ records = get_query.where(ids.map{|keys| Hash[assets[:model].primary_keys.zip(keys)]}.reduce{|q, c| q | c}).load_all
96
100
  # handler.halt_not_found(LOCS[:no_entry]) if records.empty?
97
101
  records
98
102
  end
@@ -263,9 +263,9 @@ module Engine2
263
263
  super
264
264
  panel_class 'modal-default'
265
265
  panel_title LOCS[:login_title]
266
- info! :name, loc: LOCS[:user_name]
266
+ fields! :name, loc: LOCS[:user_name]
267
267
  menu(:panel_menu).modify_option :approve, name: :login, icon: :"log-in"
268
- @meta[:fields] = [:name, :password]
268
+ @meta[:field_list] = [:name, :password]
269
269
  parent_action = node.parent.*
270
270
  if parent_action.is_a? ActionMenuSupport
271
271
  parent_action.menu(:menu).option :login_form, icon: :"log-in", disabled: "action.action_pending()"
@@ -281,7 +281,7 @@ module Engine2
281
281
  include ActionApproveSupport
282
282
  action_type :login
283
283
 
284
- def validate_record handler, record
284
+ def validate_record handler, record, parent_id
285
285
  super
286
286
  record.values[:password] = nil
287
287
  end
@@ -9,6 +9,9 @@ module Engine2
9
9
  (DefaultFilters ||= {}).merge!(
10
10
  string: lambda{|query, name, value, type_info, hash|
11
11
  case type_info[:type]
12
+ when :list_select
13
+ raise E2Error.new("Filter unimplemented for string multi list_select, field: '#{name.to_sym}'") if type_info[:multiselect] # todo
14
+ query.where(name => value)
12
15
  when :many_to_one
13
16
  query.where(name => value)
14
17
  else
@@ -39,8 +42,8 @@ module Engine2
39
42
  else
40
43
  query.where(from ? name >= from.to_i : name <= to.to_i)
41
44
  end
42
- elsif value.is_a? Integer
43
- query.where(name => value)
45
+ elsif value.is_a?(Integer) || value.is_a?(String)
46
+ query.where(name => value.to_i)
44
47
  elsif value.is_a? Array
45
48
  if !value.empty?
46
49
  case type_info[:type]
@@ -52,7 +55,11 @@ module Engine2
52
55
  query.where(keys.map{|k| hash[k]}.transpose.map{|vals| Hash[keys.zip(vals)]}.reduce{|q, c| q | c})
53
56
  end
54
57
  when :list_select
55
- query.where(name => value) # decode in sql query ?
58
+ if type_info[:multiselect]
59
+ query.where(~{(name.sql_number & value.reduce(0, :|)) => 0})
60
+ else
61
+ query.where(name => value) # decode in sql query ?
62
+ end
56
63
  when :integer
57
64
  query
58
65
  else
@@ -88,10 +95,10 @@ module Engine2
88
95
 
89
96
  if order_str = params[:order]
90
97
  order = order_str.to_sym
91
- handler.permit lookup(:info, order, :sort)
98
+ handler.permit lookup(:fields, order, :sort)
92
99
 
93
100
  if order_blk = (@orders && @orders[order]) || (dynamic? && (static.orders && static.orders[order]))
94
- query = order_blk.(query, handler)
101
+ query = order_blk.(handler, query)
95
102
  else
96
103
  order = model.table_name.q(order) if model.type_info[order]
97
104
  query = query.order(order)
@@ -106,7 +113,7 @@ module Engine2
106
113
 
107
114
  query = query.limit(per_page, page)
108
115
 
109
- res = {entries: query.all}
116
+ res = {entries: query.load_all}
110
117
  res[:count] = count if count
111
118
  res
112
119
  end
@@ -114,14 +121,14 @@ module Engine2
114
121
  def list_search query, handler, search
115
122
  hash = JSON.parse(search, symbolize_names: true) rescue handler.halt_forbidden
116
123
  model = assets[:model]
117
- sfields = lookup(:search_fields)
124
+ sfields = lookup(:search_field_list)
118
125
  handler.permit sfields
119
126
  hash.each_pair do |name, value|
120
127
  handler.permit name = sfields.find{|sf|sf.to_sym == name}
121
128
 
122
- type_info = get_type_info(name)
129
+ type_info = model.find_type_info(name)
123
130
  query = if filter = (@filters && @filters[name]) || (dynamic? && (static.filters && static.filters[name]))
124
- filter.(query, hash, handler)
131
+ filter.(handler, query, hash)
125
132
  elsif filter = DefaultFilters[type_info[:otype]]
126
133
  name = model.type_info[name] ? model.table_name.q(name) : Sequel.expr(name)
127
134
  filter.(query, name, value, type_info, hash)
@@ -205,7 +212,7 @@ module Engine2
205
212
  if h.initial? && nd = node.parent.nodes[:decode_entry]
206
213
  action = nd.*
207
214
  rec = action.invoke_decode(h, [[h.params[:parent_id]]]).first
208
- panel_title "#{static.panel[:title]} - #{rec}"
215
+ panel_title "#{static.panel[:title]} - #{action.meta[:decode_fields].map{|f|rec[f]}.join(action.meta[:separator])}"
209
216
  end
210
217
  end
211
218
  end
@@ -38,7 +38,7 @@ module Engine2
38
38
  end
39
39
 
40
40
  def check_access! handler
41
- !@access_block || @access_block.(handler)
41
+ !@access_block || @access_block.(handler)
42
42
  end
43
43
 
44
44
  def run_scheme name, *args, &blk
@@ -162,7 +162,6 @@ module Engine2
162
162
  each_node do |node|
163
163
  if model = node.*.assets[:model]
164
164
  model_name = model.name.to_sym
165
- model.synchronize_type_info
166
165
  model_nodes[model_name] = node.to_a_rec{|a| !a.*.assets[:assoc]}
167
166
  node.run_scheme(model_name) if SCHEMES[model_name, false]
168
167
  false
@@ -328,6 +328,33 @@ module E2Model
328
328
  end
329
329
 
330
330
  module DatasetMethods
331
+ def load *args
332
+ if entry = self[*args]
333
+ model.after_load_processors.each do |name, proc|
334
+ type_info = model.find_type_info(name)
335
+ name_sym = name.to_sym
336
+ proc.(entry, name_sym, type_info) if entry.key?(name_sym)
337
+ end if model.after_load_processors
338
+ entry
339
+ end
340
+ end
341
+
342
+ def load_all
343
+ entries = self.all
344
+ apply_after_load_processors(model, entries) if model.after_load_processors
345
+ entries
346
+ end
347
+
348
+ def apply_after_load_processors model, entries
349
+ model.after_load_processors.each do |name, proc|
350
+ type_info = model.find_type_info(name)
351
+ name_sym = name.to_sym
352
+ entries.each do |entry|
353
+ proc.(entry, name_sym, type_info) if entry.key?(name_sym)
354
+ end
355
+ end
356
+ end
357
+
331
358
  def ensure_primary_key
332
359
  pk = model.primary_keys
333
360
  raise Engine2::E2Error.new("No primary key defined for model #{model}") unless pk && pk.all?
@@ -376,12 +403,12 @@ module E2Model
376
403
  end
377
404
  end
378
405
 
379
- def setup! fields
406
+ def setup_query fields
380
407
  joins = {}
381
408
  type_info = model.type_info
382
409
  model_table_name = model.table_name
383
410
 
384
- @opts[:select] = @opts[:select].map do |sel|
411
+ select = @opts[:select].map do |sel|
385
412
  extract_select sel do |table, name, aliaz|
386
413
  info = if table
387
414
  if table == model_table_name
@@ -416,11 +443,9 @@ module E2Model
416
443
  end
417
444
  end
418
445
  end
419
- end
446
+ end.compact
420
447
 
421
- @opts[:select].compact!.freeze
422
-
423
- joins.reduce(self) do |joined, (table, assoc)|
448
+ joins.reduce(clone(select: select)) do |joined, (table, assoc)|
424
449
  m = assoc.associated_class
425
450
  case assoc[:type]
426
451
  when :many_to_one
@@ -521,7 +546,10 @@ module Engine2
521
546
  load 'engine2/models/UserInfo.rb'
522
547
  Dir["#{Engine2::SETTINGS.path_for(:model_path)}/*"].each{|m| load m}
523
548
  puts "MODELS: #{Sequel::DATABASES.reduce(0){|s, d|s + d.models.size}}, Time: #{Time.now - t}"
524
- Sequel::DATABASES.each &:dump_schema_cache_to_file
549
+ Sequel::DATABASES.each do |db|
550
+ db.dump_schema_cache_to_file
551
+ db.models.each{|n, m|m.synchronize_type_info}
552
+ end
525
553
 
526
554
  send(:remove_const, :ROOT) if defined? ROOT
527
555
  const_set(:ROOT, ActionNode.new(nil, :api, RootAction, {}))
@@ -5,7 +5,7 @@ module Engine2
5
5
  module Model
6
6
  attr_reader :dummies
7
7
  attr_reader :many_to_one_associations, :one_to_many_associations, :many_to_many_associations #, :one_to_one_associations
8
- attr_reader :before_save_processors, :after_save_processors, :before_destroy_processors, :after_destroy_processors
8
+ attr_reader :after_load_processors, :before_save_processors, :after_save_processors, :before_destroy_processors, :after_destroy_processors
9
9
  attr_reader :validation_in_transaction
10
10
 
11
11
  def self.extended cls
@@ -19,6 +19,7 @@ module Engine2
19
19
  @many_to_many_associations = association_reflections.select{|n, a| a[:type] == :many_to_many}
20
20
  # @one_to_one_associations = association_reflections.select{|n, a| a[:type] == :one_to_one}
21
21
  @validation_in_transaction = nil
22
+ @after_load_processors = nil
22
23
  @before_save_processors = nil
23
24
  @after_save_processors = nil
24
25
  @around_save_processors = nil
@@ -109,9 +110,27 @@ module Engine2
109
110
  end
110
111
  end
111
112
 
113
+ def find_type_info name
114
+ model = self
115
+ info = case name
116
+ when Symbol
117
+ model.type_info[name]
118
+ when Sequel::SQL::QualifiedIdentifier
119
+ assoc = model.many_to_one_associations[name.table] || model.many_to_many_associations[name.table]
120
+ raise E2Error.new("Association #{name.table} not found for model #{model}") unless assoc
121
+ assoc.associated_class.type_info[name.column]
122
+ else
123
+ raise E2Error.new("Unknown type info key: #{name} in model #{model}")
124
+ end
125
+
126
+ raise E2Error.new("Type info not found for '#{name}' in model '#{model}'") unless info
127
+ info
128
+ end
129
+
112
130
  def synchronize_type_info
113
131
  resolve_dependencies
114
132
  verify_associations
133
+ @after_load_processors = install_processors(AfterLoadProcessors)
115
134
  @before_save_processors = install_processors(BeforeSaveProcessors)
116
135
  @after_save_processors = install_processors(AfterSaveProcessors)
117
136
  @around_save_processors = {}
@@ -120,18 +139,6 @@ module Engine2
120
139
  @type_info_synchronized = true
121
140
  end
122
141
 
123
- def verify_associations
124
- one_to_many_associations.each do |name, assoc|
125
- other = assoc.associated_class
126
- other_type_info = other.type_info
127
- if other_keys = assoc[:keys]
128
- other_keys.each do |key|
129
- raise E2Error.new("No key '#{key}' found in model '#{other}' being related from #{self}") unless other_type_info[key]
130
- end
131
- end
132
- end
133
- end
134
-
135
142
  def resolve_dependencies
136
143
  resolved = {}
137
144
  @type_info.each_pair do |name, info|
@@ -153,6 +160,18 @@ module Engine2
153
160
  resolved[name] = @type_info[name]
154
161
  end
155
162
 
163
+ def verify_associations
164
+ one_to_many_associations.each do |name, assoc|
165
+ other = assoc.associated_class
166
+ other_type_info = other.type_info
167
+ if other_keys = assoc[:keys]
168
+ other_keys.each do |key|
169
+ raise E2Error.new("No key '#{key}' found in model '#{other}' being related from #{self}") unless other_type_info[key]
170
+ end
171
+ end
172
+ end
173
+ end
174
+
156
175
  attr_reader :scheme_name, :scheme_args
157
176
 
158
177
  def scheme s_name = :default, opts = nil, &blk
@@ -272,7 +291,14 @@ module Engine2
272
291
  },
273
292
  list_select: lambda{|record, field, info|
274
293
  value = record.values[field]
275
- LOCS[:invalid_list_value] unless info[:list].any?{|a|a.first == value}
294
+ values = info[:values].map(&:first)
295
+
296
+ result = if info[:multiselect]
297
+ value.is_a?(Array) && (values - value).length == values.length - value.length
298
+ else
299
+ values.include?(value)
300
+ end
301
+ LOCS[:invalid_list_value] unless result
276
302
  },
277
303
  decimal: lambda{|record, field, info|
278
304
  value = record.values[field]
@@ -300,6 +326,20 @@ module Engine2
300
326
  }
301
327
  )
302
328
 
329
+ (AfterLoadProcessors ||= {}).merge!(
330
+ list_select: lambda{|record, field, info|
331
+ value = record[field]
332
+ record[field] = case info[:otype]
333
+ when :string
334
+ value.split(info[:separator])
335
+ when :integer
336
+ arr = []
337
+ value.bit_length.times{|i| arr << (1 << i) unless value[i].zero?}
338
+ arr
339
+ end if value && info[:multiselect]
340
+ }
341
+ )
342
+
303
343
  (BeforeSaveProcessors ||= {}).merge!(
304
344
  blob_store: lambda{|record, field, info|
305
345
  if value = record.values[field] # attachment info
@@ -326,6 +366,15 @@ module Engine2
326
366
  end
327
367
  File.delete("#{upload}/#{value[:rackname]}")
328
368
  end
369
+ },
370
+ list_select: lambda{|record, field, info|
371
+ value = record.values[field]
372
+ record[field] = case info[:otype]
373
+ when :string
374
+ value.join(info[:separator])
375
+ when :integer
376
+ value.reduce(0, :|)
377
+ end if value && info[:multiselect]
329
378
  }
330
379
  )
331
380
 
@@ -359,7 +408,7 @@ module Engine2
359
408
  # record.model.where(id).update(info[:field] => Sequel.blob(open("#{upload}/#{value[:rackname]}", "rb"){|f|f.read}))
360
409
  File.delete("#{upload}/#{value[:rackname]}")
361
410
  end
362
- }
411
+ },
363
412
  )
364
413
 
365
414
  (BeforeDestroyProcessors ||= {}).merge!(
@@ -9,7 +9,7 @@ module Sequel
9
9
  when Sequel::SQL::QualifiedIdentifier
10
10
  sel.column
11
11
  when Sequel::SQL::AliasedExpression
12
- Sequel::SQL::Identifier.new sel.aliaz
12
+ Sequel::SQL::Identifier.new sel.alias
13
13
  else
14
14
  sel # symbol ?
15
15
  end
@@ -24,6 +24,16 @@ module Sequel
24
24
  rs.getInt(1)
25
25
  end
26
26
  end
27
+
28
+ def valid_connection_sql
29
+ 'select 1 from sysibm.sysdummy1'
30
+ end
31
+ end if defined?(JDBC::AS400)
32
+
33
+ class JDBC::AS400::Dataset
34
+ def supports_where_true?
35
+ false
36
+ end
27
37
  end if defined?(JDBC::AS400)
28
38
 
29
39
  module SchemaCaching
@@ -163,7 +163,7 @@ module Engine2
163
163
  define_scheme :blob_store do |model, field|
164
164
  define_node :"#{field}_blob_store!", BlobStoreAction do
165
165
  self.*.model = model
166
- self.*.field = field # model.type_info[field][:field]
166
+ self.*.field = field
167
167
  define_node :download, DownloadBlobStoreAction
168
168
  define_node :upload, UploadBlobStoreAction
169
169
  end
@@ -175,7 +175,7 @@ module Engine2
175
175
  define_scheme :foreign_blob_store do |model, field|
176
176
  define_node :"#{field}_blob_store!", ForeignBlobStoreAction do
177
177
  self.*.model = model
178
- self.*.field = field # model.type_info[field][:field]
178
+ self.*.field = field
179
179
  define_node :download, DownloadForeignBlobStoreAction
180
180
  define_node :upload, UploadBlobStoreAction
181
181
  end