praxis 2.0.pre.3 → 2.0.pre.8
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.rspec +0 -1
- data/.ruby-version +1 -0
- data/CHANGELOG.md +26 -0
- data/Gemfile +1 -1
- data/Guardfile +2 -1
- data/Rakefile +1 -7
- data/TODO.md +28 -0
- data/lib/api_browser/package-lock.json +7110 -0
- data/lib/praxis.rb +7 -4
- data/lib/praxis/action_definition.rb +9 -16
- data/lib/praxis/api_general_info.rb +21 -0
- data/lib/praxis/application.rb +1 -2
- data/lib/praxis/bootloader_stages/routing.rb +2 -4
- data/lib/praxis/docs/generator.rb +11 -6
- data/lib/praxis/docs/open_api_generator.rb +255 -0
- data/lib/praxis/docs/openapi/info_object.rb +31 -0
- data/lib/praxis/docs/openapi/media_type_object.rb +59 -0
- data/lib/praxis/docs/openapi/operation_object.rb +40 -0
- data/lib/praxis/docs/openapi/parameter_object.rb +69 -0
- data/lib/praxis/docs/openapi/paths_object.rb +55 -0
- data/lib/praxis/docs/openapi/request_body_object.rb +51 -0
- data/lib/praxis/docs/openapi/response_object.rb +63 -0
- data/lib/praxis/docs/openapi/responses_object.rb +44 -0
- data/lib/praxis/docs/openapi/schema_object.rb +87 -0
- data/lib/praxis/docs/openapi/server_object.rb +24 -0
- data/lib/praxis/docs/openapi/tag_object.rb +21 -0
- data/lib/praxis/extensions/attribute_filtering.rb +2 -0
- data/lib/praxis/extensions/attribute_filtering/active_record_filter_query_builder.rb +148 -157
- data/lib/praxis/extensions/attribute_filtering/active_record_patches.rb +15 -0
- data/lib/praxis/extensions/attribute_filtering/active_record_patches/5x.rb +90 -0
- data/lib/praxis/extensions/attribute_filtering/active_record_patches/6_0.rb +68 -0
- data/lib/praxis/extensions/attribute_filtering/active_record_patches/6_1_plus.rb +58 -0
- data/lib/praxis/extensions/attribute_filtering/filter_tree_node.rb +35 -0
- data/lib/praxis/extensions/attribute_filtering/filtering_params.rb +13 -12
- data/lib/praxis/extensions/attribute_filtering/sequel_filter_query_builder.rb +3 -2
- data/lib/praxis/extensions/field_selection/active_record_query_selector.rb +7 -9
- data/lib/praxis/extensions/field_selection/field_selector.rb +4 -0
- data/lib/praxis/extensions/field_selection/sequel_query_selector.rb +6 -9
- data/lib/praxis/extensions/pagination.rb +130 -0
- data/lib/praxis/extensions/pagination/active_record_pagination_handler.rb +42 -0
- data/lib/praxis/extensions/pagination/header_generator.rb +70 -0
- data/lib/praxis/extensions/pagination/ordering_params.rb +238 -0
- data/lib/praxis/extensions/pagination/pagination_handler.rb +68 -0
- data/lib/praxis/extensions/pagination/pagination_params.rb +378 -0
- data/lib/praxis/extensions/pagination/sequel_pagination_handler.rb +45 -0
- data/lib/praxis/handlers/json.rb +2 -0
- data/lib/praxis/handlers/www_form.rb +5 -0
- data/lib/praxis/handlers/{xml.rb → xml-sample.rb} +6 -0
- data/lib/praxis/links.rb +4 -0
- data/lib/praxis/mapper/active_model_compat.rb +23 -5
- data/lib/praxis/mapper/resource.rb +16 -9
- data/lib/praxis/mapper/selector_generator.rb +11 -10
- data/lib/praxis/mapper/sequel_compat.rb +1 -0
- data/lib/praxis/media_type.rb +1 -56
- data/lib/praxis/multipart/part.rb +5 -2
- data/lib/praxis/plugins/mapper_plugin.rb +1 -1
- data/lib/praxis/plugins/pagination_plugin.rb +71 -0
- data/lib/praxis/resource_definition.rb +4 -12
- data/lib/praxis/response_definition.rb +1 -1
- data/lib/praxis/route.rb +2 -4
- data/lib/praxis/routing_config.rb +4 -8
- data/lib/praxis/tasks/api_docs.rb +23 -0
- data/lib/praxis/tasks/routes.rb +10 -15
- data/lib/praxis/types/media_type_common.rb +10 -0
- data/lib/praxis/types/multipart_array.rb +62 -0
- data/lib/praxis/validation_handler.rb +1 -2
- data/lib/praxis/version.rb +1 -1
- data/praxis.gemspec +4 -5
- data/spec/functional_spec.rb +9 -6
- data/spec/praxis/action_definition_spec.rb +4 -16
- data/spec/praxis/api_general_info_spec.rb +6 -6
- data/spec/praxis/extensions/attribute_filtering/active_record_filter_query_builder_spec.rb +304 -0
- data/spec/praxis/extensions/attribute_filtering/filter_tree_node_spec.rb +39 -0
- data/spec/praxis/extensions/attribute_filtering/filtering_params_spec.rb +34 -0
- data/spec/praxis/extensions/field_expansion_spec.rb +6 -24
- data/spec/praxis/extensions/field_selection/active_record_query_selector_spec.rb +15 -11
- data/spec/praxis/extensions/field_selection/sequel_query_selector_spec.rb +4 -3
- data/spec/praxis/extensions/pagination/active_record_pagination_handler_spec.rb +130 -0
- data/spec/praxis/extensions/{field_selection/support → support}/spec_resources_active_model.rb +45 -2
- data/spec/praxis/extensions/{field_selection/support → support}/spec_resources_sequel.rb +0 -0
- data/spec/praxis/mapper/selector_generator_spec.rb +32 -0
- data/spec/praxis/media_type_spec.rb +5 -129
- data/spec/praxis/request_spec.rb +3 -22
- data/spec/praxis/resource_definition_spec.rb +1 -1
- data/spec/praxis/response_definition_spec.rb +8 -9
- data/spec/praxis/route_spec.rb +2 -9
- data/spec/praxis/routing_config_spec.rb +4 -13
- data/spec/praxis/types/multipart_array_spec.rb +4 -21
- data/spec/spec_app/config/environment.rb +0 -2
- data/spec/spec_app/design/api.rb +7 -1
- data/spec/spec_app/design/media_types/instance.rb +0 -8
- data/spec/spec_app/design/media_types/volume.rb +0 -12
- data/spec/spec_app/design/resources/instances.rb +1 -2
- data/spec/spec_helper.rb +6 -0
- data/spec/support/spec_media_types.rb +0 -73
- metadata +51 -49
- data/spec/praxis/handlers/xml_spec.rb +0 -177
- 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
|
@@ -1,178 +1,169 @@
|
|
1
|
+
|
2
|
+
|
1
3
|
module Praxis
|
2
4
|
module Extensions
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
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
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
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
|
-
|
34
|
-
|
35
|
-
|
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
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
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
|
-
|
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
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
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
|
-
|
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
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
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
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
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
|
-
|
122
|
+
raise "Unsupported Operator!!! #{op}"
|
155
123
|
end
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
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
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
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
|