praxis 2.0.pre.4 → 2.0.pre.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (98) hide show
  1. checksums.yaml +5 -5
  2. data/.rspec +0 -1
  3. data/.ruby-version +1 -0
  4. data/CHANGELOG.md +31 -0
  5. data/Gemfile +1 -1
  6. data/Guardfile +2 -1
  7. data/Rakefile +1 -7
  8. data/TODO.md +28 -0
  9. data/lib/api_browser/package-lock.json +7110 -0
  10. data/lib/praxis.rb +7 -4
  11. data/lib/praxis/action_definition.rb +9 -16
  12. data/lib/praxis/api_general_info.rb +21 -0
  13. data/lib/praxis/application.rb +1 -2
  14. data/lib/praxis/bootloader_stages/routing.rb +2 -4
  15. data/lib/praxis/docs/generator.rb +11 -6
  16. data/lib/praxis/docs/open_api_generator.rb +255 -0
  17. data/lib/praxis/docs/openapi/info_object.rb +39 -0
  18. data/lib/praxis/docs/openapi/media_type_object.rb +59 -0
  19. data/lib/praxis/docs/openapi/operation_object.rb +40 -0
  20. data/lib/praxis/docs/openapi/parameter_object.rb +69 -0
  21. data/lib/praxis/docs/openapi/paths_object.rb +55 -0
  22. data/lib/praxis/docs/openapi/request_body_object.rb +51 -0
  23. data/lib/praxis/docs/openapi/response_object.rb +63 -0
  24. data/lib/praxis/docs/openapi/responses_object.rb +44 -0
  25. data/lib/praxis/docs/openapi/schema_object.rb +87 -0
  26. data/lib/praxis/docs/openapi/server_object.rb +24 -0
  27. data/lib/praxis/docs/openapi/tag_object.rb +21 -0
  28. data/lib/praxis/extensions/attribute_filtering.rb +2 -0
  29. data/lib/praxis/extensions/attribute_filtering/active_record_filter_query_builder.rb +148 -157
  30. data/lib/praxis/extensions/attribute_filtering/active_record_patches.rb +15 -0
  31. data/lib/praxis/extensions/attribute_filtering/active_record_patches/5x.rb +90 -0
  32. data/lib/praxis/extensions/attribute_filtering/active_record_patches/6_0.rb +68 -0
  33. data/lib/praxis/extensions/attribute_filtering/active_record_patches/6_1_plus.rb +58 -0
  34. data/lib/praxis/extensions/attribute_filtering/filter_tree_node.rb +35 -0
  35. data/lib/praxis/extensions/attribute_filtering/filtering_params.rb +12 -24
  36. data/lib/praxis/extensions/attribute_filtering/sequel_filter_query_builder.rb +3 -2
  37. data/lib/praxis/extensions/field_selection/active_record_query_selector.rb +7 -9
  38. data/lib/praxis/extensions/field_selection/field_selector.rb +4 -0
  39. data/lib/praxis/extensions/field_selection/sequel_query_selector.rb +6 -9
  40. data/lib/praxis/extensions/pagination.rb +130 -0
  41. data/lib/praxis/extensions/pagination/active_record_pagination_handler.rb +42 -0
  42. data/lib/praxis/extensions/pagination/header_generator.rb +70 -0
  43. data/lib/praxis/extensions/pagination/ordering_params.rb +238 -0
  44. data/lib/praxis/extensions/pagination/pagination_handler.rb +68 -0
  45. data/lib/praxis/extensions/pagination/pagination_params.rb +378 -0
  46. data/lib/praxis/extensions/pagination/sequel_pagination_handler.rb +45 -0
  47. data/lib/praxis/handlers/json.rb +2 -0
  48. data/lib/praxis/handlers/www_form.rb +5 -0
  49. data/lib/praxis/handlers/{xml.rb → xml-sample.rb} +6 -0
  50. data/lib/praxis/links.rb +4 -0
  51. data/lib/praxis/mapper/active_model_compat.rb +23 -5
  52. data/lib/praxis/mapper/resource.rb +16 -9
  53. data/lib/praxis/mapper/selector_generator.rb +0 -4
  54. data/lib/praxis/mapper/sequel_compat.rb +1 -0
  55. data/lib/praxis/media_type.rb +1 -56
  56. data/lib/praxis/multipart/part.rb +5 -2
  57. data/lib/praxis/plugins/mapper_plugin.rb +1 -1
  58. data/lib/praxis/plugins/pagination_plugin.rb +71 -0
  59. data/lib/praxis/resource_definition.rb +4 -12
  60. data/lib/praxis/response_definition.rb +1 -1
  61. data/lib/praxis/route.rb +2 -4
  62. data/lib/praxis/routing_config.rb +4 -8
  63. data/lib/praxis/tasks/api_docs.rb +23 -0
  64. data/lib/praxis/tasks/routes.rb +10 -15
  65. data/lib/praxis/types/media_type_common.rb +10 -0
  66. data/lib/praxis/types/multipart_array.rb +62 -0
  67. data/lib/praxis/validation_handler.rb +1 -2
  68. data/lib/praxis/version.rb +1 -1
  69. data/praxis.gemspec +4 -5
  70. data/spec/functional_spec.rb +9 -6
  71. data/spec/praxis/action_definition_spec.rb +4 -16
  72. data/spec/praxis/api_general_info_spec.rb +6 -6
  73. data/spec/praxis/extensions/attribute_filtering/active_record_filter_query_builder_spec.rb +304 -0
  74. data/spec/praxis/extensions/attribute_filtering/filter_tree_node_spec.rb +39 -0
  75. data/spec/praxis/extensions/attribute_filtering/filtering_params_spec.rb +140 -0
  76. data/spec/praxis/extensions/field_expansion_spec.rb +6 -24
  77. data/spec/praxis/extensions/field_selection/active_record_query_selector_spec.rb +15 -11
  78. data/spec/praxis/extensions/field_selection/sequel_query_selector_spec.rb +4 -3
  79. data/spec/praxis/extensions/pagination/active_record_pagination_handler_spec.rb +130 -0
  80. data/spec/praxis/extensions/{field_selection/support → support}/spec_resources_active_model.rb +45 -2
  81. data/spec/praxis/extensions/{field_selection/support → support}/spec_resources_sequel.rb +0 -0
  82. data/spec/praxis/media_type_spec.rb +5 -129
  83. data/spec/praxis/request_spec.rb +3 -22
  84. data/spec/praxis/resource_definition_spec.rb +1 -1
  85. data/spec/praxis/response_definition_spec.rb +8 -9
  86. data/spec/praxis/route_spec.rb +2 -9
  87. data/spec/praxis/routing_config_spec.rb +4 -13
  88. data/spec/praxis/types/multipart_array_spec.rb +4 -21
  89. data/spec/spec_app/config/environment.rb +0 -2
  90. data/spec/spec_app/design/api.rb +7 -1
  91. data/spec/spec_app/design/media_types/instance.rb +0 -8
  92. data/spec/spec_app/design/media_types/volume.rb +0 -12
  93. data/spec/spec_app/design/resources/instances.rb +1 -2
  94. data/spec/spec_helper.rb +6 -0
  95. data/spec/support/spec_media_types.rb +0 -73
  96. metadata +51 -49
  97. data/spec/praxis/handlers/xml_spec.rb +0 -177
  98. data/spec/praxis/links_spec.rb +0 -68
@@ -0,0 +1,24 @@
1
+ module Praxis
2
+ module Docs
3
+ module OpenApi
4
+ class ServerObject
5
+ # https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#server-object
6
+ attr_reader :url, :description, :variables
7
+ def initialize(url: , description: nil, variables: [])
8
+ @url = url
9
+ @description = description
10
+ @variables = variables
11
+ raise "OpenApi docs require a 'url' for your server object." unless url
12
+ end
13
+
14
+ def dump
15
+ result = {url: url}
16
+ result[:description] = description if description
17
+ result[:variables] = variables unless variables.empty?
18
+
19
+ result
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,21 @@
1
+ module Praxis
2
+ module Docs
3
+ module OpenApi
4
+ class TagObject
5
+ attr_reader :name, :description
6
+ def initialize(name:,description: )
7
+ @name = name
8
+ @description = description
9
+ end
10
+
11
+ def dump
12
+ {
13
+ name: name,
14
+ description: description,
15
+ #externalDocs: ???,
16
+ }
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,2 @@
1
+ require 'praxis/extensions/attribute_filtering/filtering_params'
2
+ require 'praxis/extensions/attribute_filtering/filter_tree_node'
@@ -1,178 +1,169 @@
1
+
2
+
1
3
  module Praxis
2
4
  module Extensions
3
- class ActiveRecordFilterQueryBuilder
4
- attr_reader :query, :table, :model
5
-
6
- # Abstract class, which needs to be used by subclassing it through the .for method, to set the mapping of attributes
7
- class << self
8
- def for(definition)
9
- Class.new(self) do
10
- @attr_to_column = case definition
11
- when Hash
12
- definition
13
- when Array
14
- definition.each_with_object({}) { |item, hash| hash[item.to_sym] = item }
15
- else
16
- raise "Cannot use FilterQueryBuilder.of without passing an array or a hash (Got: #{definition.class.name})"
17
- end
18
- class << self
19
- attr_reader :attr_to_column
20
- end
21
- end
5
+ module AttributeFiltering
6
+ ALIAS_TABLE_PREFIX = ''
7
+ require_relative 'active_record_patches'
8
+
9
+ class ActiveRecordFilterQueryBuilder
10
+ attr_reader :query, :model, :attr_to_column
11
+
12
+ # Base query to build upon
13
+ def initialize(query: , model:, filters_map:, debug: false)
14
+ @query = query
15
+ @model = model
16
+ @attr_to_column = filters_map
17
+ @logger = debug ? Logger.new(STDOUT) : nil
18
+ end
19
+
20
+ def debug(msg)
21
+ @logger && @logger.puts(msg)
22
22
  end
23
- end
24
23
 
25
- # Base query to build upon
26
- def initialize(query: , model:)
27
- @query = query
28
- @table = model.table_name
29
- @last_join_alias = model.table_name
30
- @alias_counter = 0;
31
- end
24
+ def generate(filters)
25
+ # Resolve the names and values first, based on filters_map
26
+ root_node = _convert_to_treenode(filters)
27
+ craft_filter_query(root_node, for_model: @model)
28
+ debug("SQL due to filters: #{@query.all.to_sql}")
29
+ @query
30
+ end
32
31
 
33
- def pick_alias( name )
34
- @alias_counter += 1
35
- "#{name}#{@alias_counter}"
36
- end
32
+ def craft_filter_query(nodetree, for_model:)
33
+ result = _compute_joins_and_conditions_data(nodetree, model: for_model)
34
+ @query = query.joins(result[:associations_hash]) unless result[:associations_hash].empty?
37
35
 
38
- def build_clause(filters)
39
- filters.each do |item|
40
- attr = item[:name]
41
- spec = item[:specs]
42
- column_name = attr_to_column[attr]
43
- raise "Filtering by #{attr} not allowed (no mapping found)" unless column_name
44
- if column_name.is_a?(Proc)
45
- bindings = column_name.call(spec)
46
- # A hash of bindings, consisting of a key with column name and a value to the query value
47
- bindings.each do|col,val|
48
- assoc_or_field, *rest = col.to_s.split('.')
49
- expand_binding(column_name: assoc_or_field, rest: rest, op: spec[:op], value: val, use_this_name_for_clause: @last_join_alias)
50
- end
51
- else
52
- assoc_or_field, *rest = column_name.to_s.split('.')
53
- expand_binding(column_name: assoc_or_field, rest: rest, **spec, use_this_name_for_clause: @last_join_alias)
36
+ result[:conditions].each do |condition|
37
+ filter_name = condition[:name]
38
+ filter_value = condition[:value]
39
+ column_prefix = condition[:column_prefix]
40
+
41
+ colo = condition[:model].columns_hash[filter_name.to_s]
42
+ add_clause(column_prefix: column_prefix, column_object: colo, op: condition[:op], value: filter_value)
54
43
  end
55
44
  end
56
- query
57
- end
58
45
 
59
- # TODO: Support more relationship types (including things like polymorphic..etc)
60
- def do_join(query, assoc , source_alias, table_alias)
61
- reflection = query.reflections[assoc.to_s]
62
- do_join_reflection( query, reflection, source_alias, table_alias )
63
- end
46
+ private
64
47
 
65
- def do_join_reflection( query, reflection, source_alias, table_alias )
66
- c = query.connection
67
- case reflection
68
- when ActiveRecord::Reflection::BelongsToReflection
69
- join_clause = "INNER JOIN %s as %s ON %s.%s = %s.%s " % \
70
- [c.quote_table_name(reflection.klass.table_name),
71
- c.quote_table_name(table_alias),
72
- c.quote_table_name(table_alias),
73
- c.quote_column_name(reflection.association_primary_key),
74
- c.quote_table_name(source_alias),
75
- c.quote_column_name(reflection.association_foreign_key)
76
- ]
77
- query.joins(join_clause)
78
- when ActiveRecord::Reflection::HasManyReflection
79
- # join_clause = "INNER JOIN #{reflection.klass.table_name} as #{table_alias} ON" + \
80
- # " \"#{source_alias}\".\"id\" = \"#{table_alias}\".\"#{reflection.foreign_key}\" "
81
- join_clause = "INNER JOIN %s as %s ON %s.%s = %s.%s " % \
82
- [c.quote_table_name(reflection.klass.table_name),
83
- c.quote_table_name(table_alias),
84
- c.quote_table_name(source_alias),
85
- c.quote_column_name(reflection.active_record.primary_key),
86
- c.quote_table_name(table_alias),
87
- c.quote_column_name(reflection.foreign_key)
88
- ]
89
-
90
- if reflection.type # && reflection.options[:as]....
91
- # addition = " AND \"#{table_alias}\".\"#{reflection.type}\" = \'#{reflection.active_record.class_name}\'"
92
- addition = " AND %s.%s = %s" % \
93
- [ c.quote_table_name(table_alias),
94
- c.quote_table_name(reflection.type),
95
- c.quote(reflection.active_record.class_name)]
96
-
97
- join_clause += addition
48
+ # Resolve and convert from filters, to a more manageable and param-type-independent structure
49
+ def _convert_to_treenode(filters)
50
+ # Resolve the names and values first, based on filters_map
51
+ resolved_array = []
52
+ filters.parsed_array.each do |filter|
53
+ mapped_value = attr_to_column[filter[:name]]
54
+ raise "Filtering by #{filter[:name]} not allowed (no mapping found)" unless mapped_value
55
+ bindings_array = \
56
+ if mapped_value.is_a?(Proc)
57
+ result = mapped_value.call(filter)
58
+ # Result could be an array of hashes (each hash has name/op/value to identify a condition)
59
+ result.is_a?(Array) ? result : [result]
60
+ else
61
+ # For non-procs there's only 1 filter and 1 value (we're just overriding the mapped value)
62
+ [filter.merge( name: mapped_value)]
63
+ end
64
+ resolved_array = resolved_array + bindings_array
98
65
  end
99
- query.joins(join_clause)
100
- when ActiveRecord::Reflection::ThroughReflection
101
- #puts "TODO: choose different alias (based on matching table type...)"
102
- talias = pick_alias(reflection.through_reflection.table_name)
103
- salias = source_alias
104
-
105
- query = do_join_reflection(query, reflection.through_reflection, salias, talias)
106
- #puts "TODO: choose different alias ?????????"
107
- salias = talias
108
-
109
- through_model = reflection.through_reflection.klass
110
- through_assoc = reflection.name
111
- final_reflection = reflection.source_reflection
112
-
113
- do_join_reflection(query, final_reflection, salias, table_alias)
114
- else
115
- raise "Joins for this association type are currently UNSUPPORTED: #{reflection.inspect}"
66
+ FilterTreeNode.new(resolved_array, path: [ALIAS_TABLE_PREFIX])
116
67
  end
117
- end
118
68
 
119
- def expand_binding(column_name:,rest: , op:,value:, use_this_name_for_clause: column_name)
120
- unless rest.empty?
121
- joined_alias = pick_alias(column_name)
122
- @query = do_join(query, column_name, @last_join_alias, joined_alias)
123
- saved_join_alias = @last_join_alias
124
- @last_join_alias = joined_alias
125
- new_column_name, *new_rest = rest
126
- expand_binding(column_name: new_column_name, rest: new_rest, op: op, value: value, use_this_name_for_clause: joined_alias)
127
- @last_join_alias = saved_join_alias
128
- else
129
- column_name = "#{use_this_name_for_clause}.#{column_name}"
130
- add_clause(column_name: column_name, op: op, value: value)
69
+ # Calculate join tree and conditions array for the nodetree object and its children
70
+ def _compute_joins_and_conditions_data(nodetree, model:)
71
+ h = {}
72
+ conditions = []
73
+ nodetree.children.each do |name, child|
74
+ child_model = model.reflections[name.to_s].klass
75
+ result = _compute_joins_and_conditions_data(child, model: child_model)
76
+ h[name] = result[:associations_hash]
77
+ conditions += result[:conditions]
78
+ end
79
+ column_prefix = nodetree.path == [ALIAS_TABLE_PREFIX] ? model.table_name : nodetree.path.join('/')
80
+ #column_prefix = nodetree.path == [ALIAS_TABLE_PREFIX] ? nil : nodetree.path.join('/')
81
+ nodetree.conditions.each do |condition|
82
+ conditions += [condition.merge(column_prefix: column_prefix, model: model)]
83
+ end
84
+ {associations_hash: h, conditions: conditions}
131
85
  end
132
- end
133
-
134
- def attr_to_column
135
- # Class method defined by the subclassing Class (using .for)
136
- self.class.attr_to_column
137
- end
138
86
 
139
- # Private to try to funnel all column names through `build_clause` that restricts
140
- # the attribute names better (to allow more difficult SQL injections )
141
- private def add_clause(column_name:, op:, value:)
142
- likeval = get_like_value(value)
143
- @query = case op
144
- when '='
145
- if likeval
146
- query.where("#{column_name} LIKE ?", likeval)
147
- else
148
- query.where(column_name => value)
149
- end
150
- when '!='
151
- if likeval
152
- query.where("#{column_name} NOT LIKE ?", likeval)
87
+ def add_clause(column_prefix:, column_object:, op:, value:)
88
+ @query = @query.references(column_prefix) #Mark where clause referencing the appropriate alias
89
+ likeval = get_like_value(value)
90
+ case op
91
+ when '!' # name! means => name IS NOT NULL (and the incoming value is nil)
92
+ op = '!='
93
+ value = nil # Enforce it is indeed nil (should be)
94
+ when '!!'
95
+ op = '='
96
+ value = nil # Enforce it is indeed nil (should be)
97
+ end
98
+ @query = case op
99
+ when '='
100
+ if likeval
101
+ add_safe_where(tab: column_prefix, col: column_object, op: 'LIKE', value: likeval)
102
+ else
103
+ quoted_right = quote_right_part(value: value, column_object: column_object, negative: false)
104
+ query.where("#{quote_column_path(column_prefix, column_object)} #{quoted_right}")
105
+ end
106
+ when '!='
107
+ if likeval
108
+ add_safe_where(tab: column_prefix, col: column_object, op: 'NOT LIKE', value: likeval)
109
+ else
110
+ quoted_right = quote_right_part(value: value, column_object: column_object, negative: true)
111
+ query.where("#{quote_column_path(column_prefix, column_object)} #{quoted_right}")
112
+ end
113
+ when '>'
114
+ add_safe_where(tab: column_prefix, col: column_object, op: '>', value: value)
115
+ when '<'
116
+ add_safe_where(tab: column_prefix, col: column_object, op: '<', value: value)
117
+ when '>='
118
+ add_safe_where(tab: column_prefix, col: column_object, op: '>=', value: value)
119
+ when '<='
120
+ add_safe_where(tab: column_prefix, col: column_object, op: '<=', value: value)
153
121
  else
154
- query.where.not(column_name => value)
122
+ raise "Unsupported Operator!!! #{op}"
155
123
  end
156
- when '>'
157
- query.where("#{column_name} > ?", value)
158
- when '<'
159
- query.where("#{column_name} < ?", value)
160
- when '>='
161
- query.where("#{column_name} >= ?", value)
162
- when '<='
163
- query.where("#{column_name} <= ?", value)
164
- else
165
- raise "Unsupported Operator!!! #{op}"
166
- end
167
- end
124
+ end
125
+
126
+ def add_safe_where(tab:, col:, op:, value:)
127
+ quoted_value = query.connection.quote_default_expression(value,col)
128
+ query.where("#{quote_column_path(tab, col)} #{op} #{quoted_value}")
129
+ end
168
130
 
169
- # Returns nil if the value was not a fuzzzy pattern
170
- def get_like_value(value)
171
- if value.is_a?(String) && (value[-1] == '*' || value[0] == '*')
172
- likeval = value.dup
173
- likeval[-1] = '%' if value[-1] == '*'
174
- likeval[0] = '%' if value[0] == '*'
175
- likeval
131
+ def quote_column_path(prefix, column_object)
132
+ c = query.connection
133
+ quoted_column = c.quote_column_name(column_object.name)
134
+ if prefix
135
+ quoted_table = c.quote_table_name(prefix)
136
+ "#{quoted_table}.#{quoted_column}"
137
+ else
138
+ quoted_column
139
+ end
140
+ end
141
+
142
+ def quote_right_part(value:, column_object:, negative:)
143
+ conn = query.connection
144
+ if value.nil?
145
+ no = negative ? ' NOT' : ''
146
+ "IS#{no} #{conn.quote_default_expression(value,column_object)}"
147
+ elsif value.is_a?(Array)
148
+ no = negative ? 'NOT ' : ''
149
+ list = value.map{|v| conn.quote_default_expression(v,column_object)}
150
+ "#{no}IN (#{list.join(',')})"
151
+ elsif value && value.is_a?(Range)
152
+ raise "TODO!"
153
+ else
154
+ op = negative ? '<>' : '='
155
+ "#{op} #{conn.quote_default_expression(value,column_object)}"
156
+ end
157
+ end
158
+
159
+ # Returns nil if the value was not a fuzzzy pattern
160
+ def get_like_value(value)
161
+ if value.is_a?(String) && (value[-1] == '*' || value[0] == '*')
162
+ likeval = value.dup
163
+ likeval[-1] = '%' if value[-1] == '*'
164
+ likeval[0] = '%' if value[0] == '*'
165
+ likeval
166
+ end
176
167
  end
177
168
  end
178
169
  end
@@ -0,0 +1,15 @@
1
+ require 'active_record'
2
+
3
+ maj, min, _ = ActiveRecord.gem_version.segments
4
+
5
+ if maj == 5
6
+ require_relative 'active_record_patches/5x.rb'
7
+ elsif maj == 6
8
+ if min == 0
9
+ require_relative 'active_record_patches/6_0.rb'
10
+ else
11
+ require_relative 'active_record_patches/6_1_plus.rb'
12
+ end
13
+ else
14
+ raise "Filtering only supported for ActiveRecord >= 5 && <= 6"
15
+ end
@@ -0,0 +1,90 @@
1
+ require 'active_record'
2
+
3
+ module ActiveRecord
4
+ PRAXIS_JOIN_ALIAS_PREFIX = Praxis::Extensions::AttributeFiltering::ALIAS_TABLE_PREFIX
5
+ class Relation
6
+ def construct_join_dependency
7
+ including = eager_load_values + includes_values
8
+ # Praxis: inject references into the join dependency
9
+ ActiveRecord::Associations::JoinDependency.new(
10
+ klass, table, including, references: references_values
11
+ )
12
+ end
13
+
14
+ def build_join_query(manager, buckets, join_type, aliases)
15
+ buckets.default = []
16
+
17
+ association_joins = buckets[:association_join]
18
+ stashed_joins = buckets[:stashed_join]
19
+ join_nodes = buckets[:join_node].uniq
20
+ string_joins = buckets[:string_join].map(&:strip).uniq
21
+
22
+ join_list = join_nodes + convert_join_strings_to_ast(string_joins)
23
+ alias_tracker = alias_tracker(join_list, aliases)
24
+
25
+ # Praxis: inject references into the join dependency
26
+ join_dependency = ActiveRecord::Associations::JoinDependency.new(
27
+ klass, table, association_joins, references: references_values
28
+ )
29
+
30
+ joins = join_dependency.join_constraints(stashed_joins, join_type, alias_tracker)
31
+ joins.each { |join| manager.from(join) }
32
+
33
+ manager.join_sources.concat(join_list)
34
+
35
+ alias_tracker.aliases
36
+ end
37
+
38
+ end
39
+ module Associations
40
+ class JoinDependency
41
+ attr_accessor :references
42
+ private
43
+ def initialize(base, table, associations, references: )
44
+ tree = self.class.make_tree associations
45
+ @references = references # Save the references values into the instance (to use during build)
46
+ @join_root = JoinBase.new(base, table, build(tree, base))
47
+ end
48
+
49
+ # Praxis: table aliases for is shared for 5x and 6.0
50
+ def table_aliases_for(parent, node)
51
+ node.reflection.chain.map do |reflection|
52
+ is_root_reflection = reflection == node.reflection
53
+ table = alias_tracker.aliased_table_for(
54
+ reflection.table_name,
55
+ table_alias_for(reflection, parent, !is_root_reflection),
56
+ reflection.klass.type_caster
57
+ )
58
+ # through tables do not need a special alias_path alias (as they shouldn't really referenced by the client)
59
+ if is_root_reflection && node.alias_path
60
+ table = table.left if table.is_a?(Arel::Nodes::TableAlias) #un-alias it if necessary
61
+ table = table.alias(node.alias_path.join('/'))
62
+ end
63
+ table
64
+ end
65
+ end
66
+
67
+ # Praxis: build for is shared for 5x and 6.0
68
+ def build(associations, base_klass, path: [PRAXIS_JOIN_ALIAS_PREFIX])
69
+ associations.map do |name, right|
70
+ reflection = find_reflection base_klass, name
71
+ reflection.check_validity!
72
+ reflection.check_eager_loadable!
73
+
74
+ if reflection.polymorphic?
75
+ raise EagerLoadPolymorphicError.new(reflection)
76
+ end
77
+ # Praxis: set an alias_path in the JoinAssociation if its path matches a requested reference
78
+ child_path = (path && !path.empty?) ? path + [name] : nil
79
+ association = JoinAssociation.new(reflection, build(right, reflection.klass, path: child_path))
80
+ association.alias_path = child_path if references.include?(child_path.join('/'))
81
+ association
82
+ end
83
+ end
84
+
85
+ end
86
+ class ActiveRecord::Associations::JoinDependency::JoinAssociation
87
+ attr_accessor :alias_path
88
+ end
89
+ end
90
+ end