action_blocks 0.1.0

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 (69) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +121 -0
  4. data/Rakefile +33 -0
  5. data/app/assets/config/action_blocks.js +2 -0
  6. data/app/assets/javascripts/action_blocks/application.js +15 -0
  7. data/app/assets/stylesheets/action_blocks/application.css +15 -0
  8. data/app/controllers/action_blocks/attachments_controller.rb +22 -0
  9. data/app/controllers/action_blocks/barchart_blocks_controller.rb +14 -0
  10. data/app/controllers/action_blocks/base_controller.rb +22 -0
  11. data/app/controllers/action_blocks/blocks_controller.rb +16 -0
  12. data/app/controllers/action_blocks/command_blocks_controller.rb +6 -0
  13. data/app/controllers/action_blocks/form_blocks_controller.rb +13 -0
  14. data/app/controllers/action_blocks/model_blocks_controller.rb +13 -0
  15. data/app/controllers/action_blocks/table_blocks_controller.rb +13 -0
  16. data/app/controllers/action_blocks/workspace_blocks_controller.rb +6 -0
  17. data/app/helpers/action_blocks/application_helper.rb +4 -0
  18. data/app/jobs/action_blocks/application_job.rb +4 -0
  19. data/app/mailers/action_blocks/application_mailer.rb +6 -0
  20. data/app/models/action_blocks/application_record.rb +5 -0
  21. data/app/views/layouts/action_blocks/application.html.erb +16 -0
  22. data/config/initializers/action_blocks.rb +9 -0
  23. data/config/routes.rb +10 -0
  24. data/lib/action_block_loader.rb +120 -0
  25. data/lib/action_blocks.rb +76 -0
  26. data/lib/action_blocks/builders/authorization_builder.rb +21 -0
  27. data/lib/action_blocks/builders/barchart_builder.rb +48 -0
  28. data/lib/action_blocks/builders/base_builder.rb +221 -0
  29. data/lib/action_blocks/builders/block_type.rb +11 -0
  30. data/lib/action_blocks/builders/command_builder.rb +6 -0
  31. data/lib/action_blocks/builders/form_builder.rb +117 -0
  32. data/lib/action_blocks/builders/layout_builder.rb +15 -0
  33. data/lib/action_blocks/builders/model_builder.rb +566 -0
  34. data/lib/action_blocks/builders/summary_field_aggregation_functions.rb +41 -0
  35. data/lib/action_blocks/builders/table_builder.rb +259 -0
  36. data/lib/action_blocks/builders/workspace_builder.rb +282 -0
  37. data/lib/action_blocks/data_engine/authorization_adapter.rb +127 -0
  38. data/lib/action_blocks/data_engine/data_engine.rb +116 -0
  39. data/lib/action_blocks/data_engine/database_functions.rb +39 -0
  40. data/lib/action_blocks/data_engine/fields_engine.rb +103 -0
  41. data/lib/action_blocks/data_engine/filter_adapter.rb +105 -0
  42. data/lib/action_blocks/data_engine/filter_engine.rb +88 -0
  43. data/lib/action_blocks/data_engine/selections_via_where_engine.rb +134 -0
  44. data/lib/action_blocks/data_engine/summary_engine.rb +72 -0
  45. data/lib/action_blocks/engine.rb +5 -0
  46. data/lib/action_blocks/error.rb +62 -0
  47. data/lib/action_blocks/generator_helper.rb +134 -0
  48. data/lib/action_blocks/generators/action_blocks/model_block/USAGE +8 -0
  49. data/lib/action_blocks/generators/action_blocks/model_block/model_block_generator.rb +17 -0
  50. data/lib/action_blocks/generators/action_blocks/model_block/templates/model_block.rb +13 -0
  51. data/lib/action_blocks/generators/action_blocks/type/USAGE +8 -0
  52. data/lib/action_blocks/generators/action_blocks/type/templates/controller.rb +3 -0
  53. data/lib/action_blocks/generators/action_blocks/type/templates/dsl.rb +38 -0
  54. data/lib/action_blocks/generators/action_blocks/type/templates/type.css +3 -0
  55. data/lib/action_blocks/generators/action_blocks/type/templates/type.js +22 -0
  56. data/lib/action_blocks/generators/action_blocks/type/type_generator.rb +33 -0
  57. data/lib/action_blocks/store.rb +151 -0
  58. data/lib/action_blocks/version.rb +3 -0
  59. data/lib/generators/active_blocks/model_block/USAGE +8 -0
  60. data/lib/generators/active_blocks/model_block/model_block_generator.rb +17 -0
  61. data/lib/generators/active_blocks/model_block/templates/model_block.rb +13 -0
  62. data/lib/generators/active_blocks/type/USAGE +8 -0
  63. data/lib/generators/active_blocks/type/templates/controller.rb +3 -0
  64. data/lib/generators/active_blocks/type/templates/dsl.rb +38 -0
  65. data/lib/generators/active_blocks/type/templates/type.css +3 -0
  66. data/lib/generators/active_blocks/type/templates/type.js +22 -0
  67. data/lib/generators/active_blocks/type/type_generator.rb +33 -0
  68. data/lib/tasks/active_blocks_tasks.rake +4 -0
  69. metadata +128 -0
@@ -0,0 +1,127 @@
1
+ module ActionBlocks
2
+ # Data Engine
3
+ class AuthorizationAdapter
4
+ attr_accessor :engine, :user
5
+
6
+ def initialize(engine:, user:)
7
+ @engine = engine
8
+ @user = user
9
+ @model_id = @engine.root_klass.to_s.underscore
10
+ end
11
+
12
+ # get the lisp/scheme like data structure specifying row level security
13
+ def rls_scheme
14
+ rls = ActionBlocks.find("rls-#{@model_id}-#{@user.role}")
15
+ if !rls
16
+ return Arel::Nodes::False.new # [:eq, Arel::Nodes::True.new, Arel::Nodes::False.new]
17
+ end
18
+ if rls.scheme == nil
19
+ return Arel::Nodes::True.new # [:eq, Arel::Nodes::True.new, Arel::Nodes::True.new]
20
+ end
21
+ return ActionBlocks.find("rls-#{@model_id}-#{@user.role}").scheme
22
+ end
23
+
24
+ # Extract fields from lisp/scheme
25
+ def get_fields(expression)
26
+ results = []
27
+ if expression.class == Array
28
+ fn, *args = expression
29
+ if fn == :user
30
+ return []
31
+ else
32
+ return args.map { |a| get_fields(a) }.flatten.uniq
33
+ end
34
+ end
35
+ if expression.class == Symbol
36
+ return expression
37
+ end
38
+ return []
39
+ end
40
+
41
+ # Convert all fields to arel nodes while building up needed @engine.joins
42
+ def get_arel_attributes()
43
+ @fields = get_fields(rls_scheme)
44
+ @arel_attributes = {}
45
+ [@fields].flatten.each do |f|
46
+ f = ActionBlocks.find("field-#{@model_id}-#{f}")
47
+ select_req = f.select_requirements
48
+ if select_req[:type] == :summary
49
+ raise "Summary fields not supported in authorizations"
50
+ end
51
+ field_name = select_req[:field_name]
52
+ node, *rest = select_req[:path]
53
+ @arel_attributes[field_name] = walk_path(@engine.root_klass, node, @engine.root_key, rest)
54
+ end
55
+ return @arel_attributes
56
+ end
57
+
58
+ def evaluate(expression)
59
+ # Convert Symbol to Arel Attribute
60
+ if expression.class == Symbol
61
+ return @arel_attributes[expression]
62
+ end
63
+
64
+ # Convert Proc to it's result
65
+ if expression.class == Proc
66
+ proc_args = {}
67
+ # debug expression.parameters
68
+ if expression.parameters.include?([:keyreq, :user])
69
+ proc_args[:user] = @user
70
+ end
71
+ return expression.call(**proc_args)
72
+ end
73
+
74
+ # Convert expression to Arel Predicate
75
+ if expression.class == Array
76
+ fn, *args = expression
77
+ case fn
78
+ when :user
79
+ return @user.send(args[0])
80
+ when :eq
81
+ left, right = args
82
+ return evaluate(left).eq(evaluate(right))
83
+ when :not_eq
84
+ left, right = args
85
+ return evaluate(left).not_eq(evaluate(right))
86
+ when :and
87
+ return args.map {|x| evaluate(x)}.reduce(&:and)
88
+ when :or
89
+ return args.map {|x| evaluate(x)}.reduce(&:or)
90
+ else
91
+ raise "RLS function #{fn.inspect} not recognized"
92
+ end
93
+ end
94
+ return expression
95
+ end
96
+
97
+ def process
98
+ @arel_attributes = get_arel_attributes()
99
+ @engine.wheres << evaluate(rls_scheme)
100
+ end
101
+
102
+ def walk_path(klass, node, parent_key, col_path)
103
+ key = [parent_key, node].compact.join('_').to_sym
104
+ if node.class != Symbol
105
+ return node
106
+ end
107
+ if !col_path.empty?
108
+ # Create Arel Table Alias
109
+ relation = klass.reflections[node.to_s]
110
+ klass = relation.klass
111
+ @engine.tables[key] = klass.arel_table.alias(key) unless @engine.tables[key]
112
+ # Create Join
113
+ fk = relation.join_foreign_key
114
+ pk = relation.join_primary_key
115
+ join_on = @engine.tables[key].create_on(@engine.tables[parent_key][fk].eq(@engine.tables[key][pk]))
116
+ @engine.joins[key] = @engine.tables[parent_key].create_join(@engine.tables[key], join_on, Arel::Nodes::OuterJoin)
117
+ # Recurse
118
+ next_node, *rest = col_path
119
+ return walk_path(klass, next_node, key, rest)
120
+ else
121
+ return @engine.tables[parent_key][node.to_sym]
122
+ end
123
+ end
124
+
125
+ end
126
+ end
127
+
@@ -0,0 +1,116 @@
1
+ module ActionBlocks
2
+ # Data Engine
3
+ class DataEngine
4
+ def initialize(root_klass,
5
+ user: nil,
6
+ table_alias_prefix: nil,
7
+ select_reqs: [],
8
+ select_fields: [],
9
+ filter_reqs: [],
10
+ selection_match_reqs: [],
11
+ selection_filter_reqs: []
12
+ )
13
+ @root_klass = root_klass
14
+
15
+ @filter_reqs = filter_reqs
16
+
17
+ select_reqs_via_fields = select_fields.map(&:select_requirements)
18
+
19
+ if [select_reqs].length > 0
20
+ Rails.logger.warn "Passing select_reqs to Data Engine is deprecated."
21
+ end
22
+ all_select_reqs = [select_reqs, select_reqs_via_fields].flatten.compact
23
+
24
+ select_reqs_for_field_engine = all_select_reqs.select { |r| r[:type].nil? }
25
+ select_reqs_for_summary_engine = all_select_reqs.select { |r| r[:type] == :summary }
26
+
27
+ if ActionBlocks.config[:should_authorize]
28
+ if user.nil?
29
+ raise "@user must be provided to data engine when should_authorize is configured"
30
+ end
31
+ end
32
+ @user = user
33
+
34
+ @fields_engine = ActionBlocks.config[:fields_engine].new(
35
+ @root_klass,
36
+ user: user,
37
+ table_alias_prefix: table_alias_prefix,
38
+ select_reqs: select_reqs_for_field_engine
39
+ )
40
+
41
+ @selections_engine = ActionBlocks.config[:selections_engine].new(
42
+ @root_klass,
43
+ user: user,
44
+ table_alias_prefix: table_alias_prefix,
45
+ selection_match_reqs: selection_match_reqs,
46
+ selection_filter_reqs: selection_filter_reqs
47
+ )
48
+
49
+ # @filter_engine = ActionBlocks.config[:filter_engine].new(
50
+ # @root_klass,
51
+ # user: user,
52
+ # filter_reqs: filter_reqs,
53
+ # )
54
+
55
+ @summary_engine = ActionBlocks.config[:summary_engine].new(
56
+ @root_klass,
57
+ user: user,
58
+ summary_reqs: select_reqs_for_summary_engine
59
+ )
60
+
61
+ # if ActionBlocks.config[:should_authorize]
62
+ # @authorization_engine = ActionBlocks.config[:authorization_engine].new(
63
+ # @root_klass,
64
+ # user: user
65
+ # # table_alias_prefix: table_alias_prefix,
66
+ # # select_reqs: select_reqs_for_field_engine
67
+ # )
68
+ # end
69
+
70
+ process
71
+ end
72
+
73
+ def process
74
+ @fields_engine.process
75
+ @selections_engine.process
76
+ @summary_engine.process
77
+ # @filter_engine.process
78
+
79
+ @filter_adapter = FilterAdapter.new(engine: @fields_engine, user: @user, filter_reqs: @filter_reqs)
80
+ @filter_adapter.process
81
+
82
+ if ActionBlocks.config[:should_authorize]
83
+ @authorization_adapter = AuthorizationAdapter.new(engine: @fields_engine, user: @user)
84
+ @authorization_adapter.process
85
+ end
86
+
87
+
88
+ end
89
+
90
+ def to_json
91
+ sql = query.to_sql
92
+ jsql = "select array_to_json(array_agg(row_to_json(t)))
93
+ from (#{sql}) t"
94
+ ActiveRecord::Base.connection.select_value(jsql)
95
+ end
96
+
97
+ # Experimental
98
+ def first_to_json
99
+ # SELECT row_to_json(r)
100
+ sql = query.to_sql
101
+ jsql = "select row_to_json(t)
102
+ from (#{sql}) t"
103
+ ActiveRecord::Base.connection.select_value(jsql)
104
+ end
105
+
106
+ def query
107
+ engine_queries = [
108
+ @summary_engine.query,
109
+ @selections_engine.query,
110
+ @fields_engine.query,
111
+ # @filter_engine.query,
112
+ ]
113
+ engine_queries.reduce(&:merge)
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,39 @@
1
+ module ActionBlocks
2
+ class DatabaseFunctions
3
+ # methods define their own params, always followed by current node and current user
4
+ def timezone(tz, node, user, *args)
5
+ utc = Arel::Nodes::NamedFunction.new(
6
+ 'timezone',
7
+ [Arel::Nodes.build_quoted('UTC'), node]
8
+ )
9
+
10
+ Arel::Nodes::NamedFunction.new(
11
+ 'timezone',
12
+ [Arel::Nodes.build_quoted(tz), utc]
13
+ )
14
+ end
15
+
16
+ def count(node, *args)
17
+ Arel::Nodes::NamedFunction.new(
18
+ 'count',
19
+ [node]
20
+ )
21
+ end
22
+
23
+ def string_agg(delimiter, node, *args)
24
+ Arel::Nodes::NamedFunction.new(
25
+ 'string_agg',
26
+ [node, Arel::Nodes.build_quoted(delimiter)]
27
+ )
28
+ end
29
+
30
+ def every(predicate, value, node, *args)
31
+ every_part = Arel::Nodes::NamedFunction.new(
32
+ 'every',
33
+ [node.send(predicate, Arel::Nodes.build_quoted(value))]
34
+ )
35
+
36
+ Arel::Nodes::NamedFunction.new('CAST', [every_part.as('TEXT')])
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,103 @@
1
+ module ActionBlocks
2
+ # Data Engine
3
+ class FieldsEngine
4
+ attr_accessor :tables, :root_klass, :select_reqs, :selects, :joins,
5
+ :root_key, :joins, :wheres
6
+
7
+ def initialize(root_klass, user: nil, table_alias_prefix:, select_reqs: [])
8
+ @root_klass = root_klass
9
+ @user = user
10
+ @table_alias_prefix = table_alias_prefix
11
+ @select_reqs = select_reqs
12
+ @tables = {}
13
+ @selects = []
14
+ @joins = {}
15
+ @wheres = []
16
+ end
17
+
18
+ def process
19
+ root_table = @root_klass.arel_table.alias([@table_alias_prefix, @root_klass.to_s.underscore.pluralize].compact.join('_'))
20
+ @root_key = [@table_alias_prefix, @root_klass.to_s.underscore.pluralize].compact.join('_').to_sym
21
+
22
+ # Add base table to tables
23
+ @tables[@root_key.to_sym] = root_table
24
+
25
+ # Add needed relations to tables
26
+ @select_reqs.each do |selectreq|
27
+ # binding.pry
28
+ colname = selectreq[:field_name]
29
+ colpath = selectreq[:path]
30
+ function = selectreq[:function]
31
+ node, *rest = colpath
32
+ walk_colpath(@root_klass, node, @root_key, rest, colname, function)
33
+ end
34
+ end
35
+
36
+ def walk_colpath(klass, node, parent_key, col_path, colname, function)
37
+ key = [@table_alias_prefix, parent_key, node].compact.join('_').to_sym
38
+ if !col_path.empty?
39
+
40
+ next_klass = create_table_and_joins(klass, node, key, parent_key)
41
+
42
+ # Recurse
43
+ next_node, *rest = col_path
44
+ walk_colpath(next_klass, next_node, key, rest, colname, function)
45
+ else
46
+ # Create Arel Select
47
+ select = if function.nil?
48
+ @tables[parent_key][node.to_sym].as(colname.to_s)
49
+ else
50
+ DatabaseFunctions.new.instance_exec(@tables[parent_key][node.to_sym], @user, &function).as(colname.to_s)
51
+ end
52
+
53
+ @selects << select
54
+ end
55
+ end
56
+
57
+ def params_to_arel(aggregate_params)
58
+ aggregate_params.map { |param| param.is_a?(String) ? Arel::Nodes.build_quoted(param) : param } if aggregate_params
59
+ end
60
+
61
+ def create_table_and_joins(klass, node, key, parent_key, join_prefix = nil, associations = nil)
62
+ # Create Arel Table Alias
63
+ relation = klass.reflections[node.to_s] if node.is_a? Symbol
64
+ unless @tables[key]
65
+ @tables[key] = (relation ? relation.klass : node).arel_table.alias(key) unless @tables[key]
66
+
67
+ # Create Join
68
+ fk = associations ? associations[parent_key.to_s][:foreign_key] : relation.join_foreign_key
69
+ pk = associations ? associations[parent_key.to_s][:primary_key] : relation.join_primary_key
70
+ join_on = @tables[key].create_on(@tables[parent_key][fk].eq(@tables[key][pk]))
71
+ @joins[join_prefix ? [join_prefix, node.to_s.underscore].compact.join('_').to_sym : key] = @tables[parent_key].create_join(@tables[key], join_on, Arel::Nodes::OuterJoin)
72
+ end
73
+
74
+ relation ? relation.klass : node
75
+ end
76
+
77
+ def selects
78
+ @selects
79
+ end
80
+
81
+ def ordered_joins
82
+ @joins.values
83
+ end
84
+
85
+ def froms
86
+ @root_klass.arel_table.alias([@table_alias_prefix, @root_klass.to_s.underscore.pluralize].compact.join('_'))
87
+ end
88
+
89
+ def wheres
90
+ @wheres
91
+ end
92
+
93
+ def query
94
+ @root_klass
95
+ .from(froms)
96
+ .select(selects)
97
+ .joins(ordered_joins)
98
+ .where(wheres.compact.reduce(&:and))
99
+ end
100
+
101
+
102
+ end
103
+ end
@@ -0,0 +1,105 @@
1
+ module ActionBlocks
2
+ # Data Engine
3
+ class FilterAdapter
4
+ attr_accessor :engine, :user, :filter_reqs
5
+
6
+ def initialize(engine:, filter_reqs:, user:)
7
+ @engine = engine
8
+ @user = user
9
+ @rls_scheme = filter_reqs
10
+ @model_id = @engine.root_klass.to_s.underscore
11
+ end
12
+
13
+ # Extract fields from lisp/scheme
14
+ def get_fields(expression)
15
+ if expression.class == Array
16
+ fn, *args = expression
17
+ return [] if fn == :user
18
+ return args.map { |a| get_fields(a) }.flatten.uniq
19
+ end
20
+ return expression if expression.class == Symbol
21
+ return []
22
+ end
23
+
24
+ # Convert all fields to arel nodes while building up needed @engine.joins
25
+ def get_arel_attributes()
26
+ @fields = get_fields(@rls_scheme)
27
+ @arel_attributes = {}
28
+ [@fields].flatten.each do |f|
29
+ f = ActionBlocks.find("field-#{@model_id}-#{f}")
30
+ select_req = f.select_requirements
31
+ if select_req[:type] == :summary
32
+ raise "Summary fields not supported in authorizations"
33
+ end
34
+ field_name = select_req[:field_name]
35
+ node, *rest = select_req[:path]
36
+ @arel_attributes[field_name] = walk_path(@engine.root_klass, node, @engine.root_key, rest)
37
+ end
38
+ return @arel_attributes
39
+ end
40
+
41
+ def evaluate(expression)
42
+ # Convert Symbol to Arel Attribute
43
+ return @arel_attributes[expression] if expression.class == Symbol
44
+
45
+ # Convert Proc to it's result
46
+ if expression.class == Proc
47
+ proc_args = {}
48
+ # debug expression.parameters
49
+ if expression.parameters.include?([:keyreq, :user])
50
+ proc_args[:user] = @user
51
+ end
52
+ return expression.call(**proc_args)
53
+ end
54
+
55
+ # Convert expression to Arel Predicate
56
+ if expression.class == Array
57
+ fn, *args = expression
58
+ case fn
59
+ when :user
60
+ return @user.send(args[0])
61
+ when :eq
62
+ left, right = args
63
+ return evaluate(left).eq(evaluate(right))
64
+ when :not_eq
65
+ left, right = args
66
+ return evaluate(left).not_eq(evaluate(right))
67
+ when :and
68
+ return args.map {|x| evaluate(x)}.reduce(&:and)
69
+ when :or
70
+ return args.map {|x| evaluate(x)}.reduce(&:or)
71
+ else
72
+ raise "RLS function #{fn.inspect} not recognized"
73
+ end
74
+ end
75
+
76
+ return expression
77
+ end
78
+
79
+ def process
80
+ if (!@rls_scheme.empty?)
81
+ @arel_attributes = get_arel_attributes()
82
+ @engine.wheres << evaluate(@rls_scheme)
83
+ end
84
+ end
85
+
86
+ def walk_path(klass, node, parent_key, col_path)
87
+ key = [parent_key, node].compact.join('_').to_sym
88
+ return node if node.class != Symbol
89
+ return @engine.tables[parent_key][node.to_sym] if col_path.empty?
90
+
91
+ # Create Arel Table Alias
92
+ relation = klass.reflections[node.to_s]
93
+ klass = relation.klass
94
+ @engine.tables[key] = klass.arel_table.alias(key) unless @engine.tables[key]
95
+ # Create Join
96
+ fk = relation.join_foreign_key
97
+ pk = relation.join_primary_key
98
+ join_on = @engine.tables[key].create_on(@engine.tables[parent_key][fk].eq(@engine.tables[key][pk]))
99
+ @engine.joins[key] = @engine.tables[parent_key].create_join(@engine.tables[key], join_on, Arel::Nodes::OuterJoin)
100
+ # Recurse
101
+ next_node, *rest = col_path
102
+ return walk_path(klass, next_node, key, rest)
103
+ end
104
+ end
105
+ end