safrano 0.2.0 → 0.3.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.
- checksums.yaml +4 -4
- data/lib/multipart.rb +92 -91
- data/lib/odata/attribute.rb +15 -6
- data/lib/odata/collection.rb +109 -106
- data/lib/odata/collection_filter.rb +14 -485
- data/lib/odata/collection_order.rb +15 -22
- data/lib/odata/entity.rb +31 -41
- data/lib/odata/filter/error.rb +53 -0
- data/lib/odata/filter/parse.rb +171 -0
- data/lib/odata/filter/sequel.rb +208 -0
- data/lib/odata/filter/token.rb +59 -0
- data/lib/odata/filter/tree.rb +368 -0
- data/lib/odata/relations.rb +36 -0
- data/lib/odata/url_parameters.rb +58 -0
- data/lib/odata/walker.rb +55 -42
- data/lib/request.rb +1 -3
- data/lib/safrano.rb +17 -0
- data/lib/safrano_core.rb +30 -7
- data/lib/sequel/plugins/join_by_paths.rb +239 -0
- data/lib/sequel_join_by_paths.rb +5 -0
- data/lib/service.rb +84 -112
- metadata +11 -3
data/lib/odata/relations.rb
CHANGED
@@ -47,6 +47,16 @@ module OData
|
|
47
47
|
raise ArgumentError
|
48
48
|
end
|
49
49
|
end
|
50
|
+
|
51
|
+
def with_metadata_info(xnamespace)
|
52
|
+
bdprops = []
|
53
|
+
each_endobj do |eo|
|
54
|
+
bdprops << { type: "#{xnamespace}.#{eo}",
|
55
|
+
role: eo,
|
56
|
+
multiplicity: multiplicity[eo] }
|
57
|
+
end
|
58
|
+
yield name, bdprops
|
59
|
+
end
|
50
60
|
end
|
51
61
|
|
52
62
|
# some usefull stuff
|
@@ -74,5 +84,31 @@ module OData
|
|
74
84
|
@list[rid] = rel
|
75
85
|
end
|
76
86
|
end
|
87
|
+
|
88
|
+
def get_metadata_xml_attribs(from, to, assoc_type, xnamespace)
|
89
|
+
rel = get([from, to])
|
90
|
+
# use Sequel reflection to get multiplicity (will be used later
|
91
|
+
# in 2. Associations below)
|
92
|
+
case assoc_type
|
93
|
+
# TODO: use multiplicity 1 when needed instead of '0..1'
|
94
|
+
when :one_to_one
|
95
|
+
rel.set_multiplicity(from, '0..1')
|
96
|
+
rel.set_multiplicity(to, '0..1')
|
97
|
+
when :one_to_many
|
98
|
+
rel.set_multiplicity(from, '0..1')
|
99
|
+
rel.set_multiplicity(to, '*')
|
100
|
+
when :many_to_one
|
101
|
+
rel.set_multiplicity(from, '*')
|
102
|
+
rel.set_multiplicity(to, '0..1')
|
103
|
+
when :many_to_many
|
104
|
+
rel.set_multiplicity(from, '*')
|
105
|
+
rel.set_multiplicity(to, '*')
|
106
|
+
end
|
107
|
+
# <NavigationProperty Name="Supplier"
|
108
|
+
# Relationship="ODataDemo.Product_Supplier_Supplier_Products"
|
109
|
+
# FromRole="Product_Supplier" ToRole="Supplier_Products"/>
|
110
|
+
{ 'Name' => to, 'Relationship' => "#{xnamespace}.#{rel.name}",
|
111
|
+
'FromRole' => from, 'ToRole' => to }
|
112
|
+
end
|
77
113
|
end
|
78
114
|
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'odata/error.rb'
|
2
|
+
|
3
|
+
# url parameters processing . Mostly delegates to specialised classes
|
4
|
+
# (filter, order...) to convert into Sequel exprs.
|
5
|
+
module OData
|
6
|
+
class UrlParameters
|
7
|
+
attr_reader :filt
|
8
|
+
attr_reader :ordby
|
9
|
+
def initialize(jh, params)
|
10
|
+
@jh = jh
|
11
|
+
@params = params
|
12
|
+
end
|
13
|
+
|
14
|
+
def check_filter
|
15
|
+
return unless @params['$filter']
|
16
|
+
|
17
|
+
@filt = FilterByParse.new(@params['$filter'], @jh)
|
18
|
+
return BadRequestFilterParseError if @filt.parse_error?
|
19
|
+
|
20
|
+
# nil is the expected return for no errors
|
21
|
+
nil
|
22
|
+
end
|
23
|
+
|
24
|
+
def check_order
|
25
|
+
return unless @params['$orderby']
|
26
|
+
|
27
|
+
pordlist = @params['$orderby'].dup
|
28
|
+
pordlist.split(',').each do |pord|
|
29
|
+
pord.strip!
|
30
|
+
qualfn, dir = pord.split(/\s/)
|
31
|
+
qualfn.strip!
|
32
|
+
dir.strip! if dir
|
33
|
+
return BadRequestError unless @jh.start_model.attrib_path_valid? qualfn
|
34
|
+
return BadRequestError unless [nil, 'asc', 'desc'].include? dir
|
35
|
+
end
|
36
|
+
|
37
|
+
@ordby = Order.new_by_parse(@params['$orderby'], @jh)
|
38
|
+
|
39
|
+
# nil is the expected return for no errors
|
40
|
+
nil
|
41
|
+
end
|
42
|
+
|
43
|
+
def apply_to_dataset(dtcx)
|
44
|
+
return dtcx if (@filt.nil? && @ordby.nil?)
|
45
|
+
|
46
|
+
if @filt.nil?
|
47
|
+
dtcx = @jh.dataset.select_all(@jh.start_model.table_name)
|
48
|
+
@ordby.apply_to_dataset(dtcx)
|
49
|
+
elsif @ordby.nil?
|
50
|
+
@filt.apply_to_dataset(dtcx)
|
51
|
+
else
|
52
|
+
filtexpr = @filt.sequel_expr
|
53
|
+
dtcx = @jh.dataset(dtcx).where(filtexpr).select_all(@jh.start_model.table_name)
|
54
|
+
@ordby.apply_to_dataset(dtcx)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
data/lib/odata/walker.rb
CHANGED
@@ -79,57 +79,70 @@ module OData
|
|
79
79
|
end
|
80
80
|
end
|
81
81
|
|
82
|
+
# perform a content-id ($batch changeset ref) transition
|
83
|
+
def do_run_with_content_id
|
84
|
+
if @content_id_refs.is_a? Hash
|
85
|
+
if (@context = @content_id_refs[@context.to_s])
|
86
|
+
@status = :run
|
87
|
+
else
|
88
|
+
@context = nil
|
89
|
+
@status = :error
|
90
|
+
# TODO: more appropriate error handling
|
91
|
+
@error = OData::ErrorNotFound
|
92
|
+
end
|
93
|
+
else
|
94
|
+
@context = nil
|
95
|
+
@status = :error
|
96
|
+
# TODO: more appropriate error handling
|
97
|
+
@error = OData::ErrorNotFound
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# little hacks... depending on returned state, set some attributes
|
102
|
+
def state_mappings
|
103
|
+
case @status
|
104
|
+
when :end_with_count
|
105
|
+
@do_count = true
|
106
|
+
@status = :end
|
107
|
+
when :end_with_value
|
108
|
+
@raw_value = true
|
109
|
+
@status = :end
|
110
|
+
when :run_with_links
|
111
|
+
@do_links = true
|
112
|
+
@status = :run
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def do_next_transition
|
117
|
+
@context, @status, @error = @tr_next.do_transition(@context)
|
118
|
+
# little hack's
|
119
|
+
case @status
|
120
|
+
# we dont have the content-id references data on service level
|
121
|
+
# but we have it here, so in case of a $content-id transition
|
122
|
+
# the returned context is just the id, and we get the final result
|
123
|
+
# entity reference here and place it in @context
|
124
|
+
when :run_with_content_id
|
125
|
+
do_run_with_content_id
|
126
|
+
end
|
127
|
+
|
128
|
+
@contexts << @context
|
129
|
+
@path_remain = @tr_next.path_remain
|
130
|
+
@path_done << @tr_next.path_done
|
131
|
+
|
132
|
+
# little hack's
|
133
|
+
state_mappings
|
134
|
+
end
|
135
|
+
|
82
136
|
def eo
|
83
137
|
while @context
|
84
138
|
get_next_transition
|
85
139
|
if @tr_next
|
86
|
-
|
87
|
-
# little hack's
|
88
|
-
case @status
|
89
|
-
# we dont have the content-id references data on service level
|
90
|
-
# but we have it here, so in case of a $content-id transition
|
91
|
-
# the returned context is just the id, and we get the final result
|
92
|
-
# entity reference here and place it in @context
|
93
|
-
when :run_with_content_id
|
94
|
-
if @content_id_refs.is_a? Hash
|
95
|
-
if (@context = @content_id_refs[@context.to_s])
|
96
|
-
@status = :run
|
97
|
-
else
|
98
|
-
@context = nil
|
99
|
-
@status = :error
|
100
|
-
# TODO: more appropriate error handling
|
101
|
-
@error = OData::ErrorNotFound
|
102
|
-
end
|
103
|
-
else
|
104
|
-
@context = nil
|
105
|
-
@status = :error
|
106
|
-
# TODO: more appropriate error handling
|
107
|
-
@error = OData::ErrorNotFound
|
108
|
-
end
|
109
|
-
end
|
110
|
-
|
111
|
-
@contexts << @context
|
112
|
-
@path_remain = @tr_next.path_remain
|
113
|
-
@path_done << @tr_next.path_done
|
114
|
-
|
115
|
-
# little hack's
|
116
|
-
case @status
|
117
|
-
when :end_with_count
|
118
|
-
@do_count = true
|
119
|
-
@status = :end
|
120
|
-
when :end_with_value
|
121
|
-
@raw_value = true
|
122
|
-
@status = :end
|
123
|
-
when :run_with_links
|
124
|
-
@do_links = true
|
125
|
-
@status = :run
|
126
|
-
end
|
140
|
+
do_next_transition
|
127
141
|
else
|
128
142
|
@context = nil
|
129
143
|
@status = :error
|
130
144
|
@error = OData::ErrorNotFound
|
131
145
|
end
|
132
|
-
|
133
146
|
end
|
134
147
|
# TODO: shouldnt we raise an error here if @status != :end ?
|
135
148
|
return false unless @status == :end
|
data/lib/request.rb
CHANGED
@@ -8,8 +8,6 @@ module OData
|
|
8
8
|
# is not passed
|
9
9
|
class Request < Rack::Request
|
10
10
|
HEADER_PARAM = /\s*[\w.]+=(?:[\w.]+|"(?:[^"\\]|\\.)*")?\s*/.freeze
|
11
|
-
# HEADER_VALUE_WITH_PARAMS = /(?:(?:\w+|\*)\/(?:\w+(?:\.|\-|\+)?|\*)*)
|
12
|
-
# \s*(?:;#{HEADER_PARAM})*/
|
13
11
|
HEADER_VAL_RAW = '(?:\w+|\*)\/(?:\w+(?:\.|\-|\+)?|\*)*'.freeze
|
14
12
|
HEADER_VAL_WITH_PAR = /(?:#{HEADER_VAL_RAW})\s*(?:;#{HEADER_PARAM})*/.freeze
|
15
13
|
|
@@ -135,7 +133,7 @@ module OData
|
|
135
133
|
end
|
136
134
|
|
137
135
|
def with_parsed_data(on_error: nil)
|
138
|
-
if content_type ==
|
136
|
+
if content_type == APPJSON
|
139
137
|
# Parse json payload
|
140
138
|
begin
|
141
139
|
data = JSON.parse(body.read)
|
data/lib/safrano.rb
CHANGED
@@ -10,5 +10,22 @@ require 'odata/collection.rb'
|
|
10
10
|
require 'service.rb'
|
11
11
|
require 'odata/walker.rb'
|
12
12
|
require 'sequel'
|
13
|
+
require_relative './sequel_join_by_paths.rb'
|
13
14
|
require 'rack_app'
|
14
15
|
require 'odata_rack_builder'
|
16
|
+
|
17
|
+
# picked from activsupport; needed for ruby < 2.5
|
18
|
+
# Destructively converts all keys using the +block+ operations.
|
19
|
+
# Same as +transform_keys+ but modifies +self+.
|
20
|
+
class Hash
|
21
|
+
def transform_keys!
|
22
|
+
keys.each do |key|
|
23
|
+
self[yield(key)] = delete(key)
|
24
|
+
end
|
25
|
+
self
|
26
|
+
end unless method_defined? :transform_keys!
|
27
|
+
|
28
|
+
def symbolize_keys!
|
29
|
+
transform_keys! { |key| key.to_sym rescue key }
|
30
|
+
end
|
31
|
+
end
|
data/lib/safrano_core.rb
CHANGED
@@ -1,7 +1,24 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
-
#
|
3
|
+
# our main namespace
|
4
4
|
module OData
|
5
|
+
# some prominent constants... probably already defined elsewhere eg in Rack
|
6
|
+
# but lets KISS
|
7
|
+
CONTENT_TYPE = 'Content-Type'.freeze
|
8
|
+
TEXTPLAIN_UTF8 = 'text/plain;charset=utf-8'.freeze
|
9
|
+
APPJSON = 'application/json'.freeze
|
10
|
+
APPXML = 'application/xml'.freeze
|
11
|
+
|
12
|
+
APPXML_UTF8 = 'application/xml;charset=utf-8'.freeze
|
13
|
+
APPATOMXML_UTF8 = 'application/atomsvc+xml;charset=utf-8'.freeze
|
14
|
+
APPJSON_UTF8 = 'application/json;charset=utf-8'.freeze
|
15
|
+
|
16
|
+
CT_JSON = { CONTENT_TYPE => APPJSON_UTF8 }.freeze
|
17
|
+
CT_TEXT = { CONTENT_TYPE => TEXTPLAIN_UTF8 }.freeze
|
18
|
+
CT_ATOMXML = { CONTENT_TYPE => APPATOMXML_UTF8 }.freeze
|
19
|
+
CT_APPXML = { CONTENT_TYPE => APPXML_UTF8 }.freeze
|
20
|
+
|
21
|
+
# Type mapping DB --> Edm
|
5
22
|
# TypeMap = {"INTEGER" => "Edm.Int32" , "TEXT" => "Edm.String",
|
6
23
|
# "STRING" => "Edm.String"}
|
7
24
|
# Todo: complete mapping... this is just for the most common ones
|
@@ -12,6 +29,8 @@ module OData
|
|
12
29
|
# Time File TrueClass FalseClass'.freeze
|
13
30
|
# Classes specifying generic types that Sequel will convert to
|
14
31
|
# database-specific types.
|
32
|
+
DB_TYPE_STRING_RGX = /\ACHAR\s*\(\d+\)\z/.freeze
|
33
|
+
|
15
34
|
def self.get_edm_type(db_type:)
|
16
35
|
case db_type
|
17
36
|
when 'INTEGER'
|
@@ -19,7 +38,7 @@ module OData
|
|
19
38
|
when 'TEXT', 'STRING'
|
20
39
|
'Edm.String'
|
21
40
|
else
|
22
|
-
'Edm.String' if
|
41
|
+
'Edm.String' if DB_TYPE_STRING_RGX =~ db_type
|
23
42
|
end
|
24
43
|
end
|
25
44
|
end
|
@@ -45,22 +64,25 @@ module Safrano
|
|
45
64
|
attr_accessor :trans
|
46
65
|
attr_accessor :match_result
|
47
66
|
attr_accessor :rgx
|
48
|
-
|
67
|
+
attr_reader :remain_idx
|
68
|
+
def initialize(arg, trans: nil, remain_idx: 2)
|
49
69
|
@rgx = if arg.respond_to? :each_char
|
50
70
|
Regexp.new(arg)
|
51
71
|
else
|
52
72
|
arg
|
53
73
|
end
|
54
74
|
@trans = trans
|
75
|
+
@remain_idx = remain_idx
|
55
76
|
end
|
56
77
|
|
57
78
|
def do_match(str)
|
58
79
|
@match_result = @rgx.match(str)
|
59
80
|
end
|
60
81
|
|
61
|
-
#
|
82
|
+
# remain_idx is the index of the last match-data. ususally its 2
|
83
|
+
# but can be overidden
|
62
84
|
def path_remain
|
63
|
-
@match_result[
|
85
|
+
@match_result[@remain_idx] if @match_result && @match_result[@remain_idx]
|
64
86
|
end
|
65
87
|
|
66
88
|
def path_done
|
@@ -81,8 +103,9 @@ module Safrano
|
|
81
103
|
trans: 'transition_metadata')
|
82
104
|
TransitionBatch = Transition.new('\A(\/\$batch)(.*)',
|
83
105
|
trans: 'transition_batch')
|
84
|
-
TransitionContentId = Transition.new('\A(
|
85
|
-
trans: 'transition_content_id'
|
106
|
+
TransitionContentId = Transition.new('\A(\/\$(\d+))(.*)',
|
107
|
+
trans: 'transition_content_id',
|
108
|
+
remain_idx: 3)
|
86
109
|
TransitionCount = Transition.new('(\A\/\$count)(.*)\z',
|
87
110
|
trans: 'transition_count')
|
88
111
|
TransitionValue = Transition.new('(\A\/\$value)(.*)\z',
|
@@ -0,0 +1,239 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'set'
|
4
|
+
require 'sequel'
|
5
|
+
|
6
|
+
# some helpers
|
7
|
+
class String
|
8
|
+
# thanks https://stackoverflow.com/questions/1448670/ruby-stringto-class
|
9
|
+
def constantize
|
10
|
+
names = split('::')
|
11
|
+
names.shift if names.empty? || names.first.empty?
|
12
|
+
|
13
|
+
const = Object
|
14
|
+
names.each do |name|
|
15
|
+
const = if const.const_defined?(name)
|
16
|
+
const.const_get(name)
|
17
|
+
else
|
18
|
+
const.const_missing(name)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
const
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class PathNode < String
|
26
|
+
attr_reader :table_name
|
27
|
+
attr_accessor :model_class
|
28
|
+
attr_accessor :path_str
|
29
|
+
def initialize(str, start_model)
|
30
|
+
super(str)
|
31
|
+
@start_model = start_model
|
32
|
+
end
|
33
|
+
|
34
|
+
def table_name
|
35
|
+
@model_class.table_name
|
36
|
+
end
|
37
|
+
|
38
|
+
def set_model_class_by_parent_model(pmclass)
|
39
|
+
@model_class = pmclass.association_reflection(to_sym)[:class_name].constantize
|
40
|
+
end
|
41
|
+
|
42
|
+
def alias_sym
|
43
|
+
@start_model.get_alias_sym(path_str)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class QPath
|
48
|
+
attr_reader :segments
|
49
|
+
attr_reader :start_model
|
50
|
+
attr_reader :qpath
|
51
|
+
|
52
|
+
def initialize(smodel, path_str)
|
53
|
+
@qpath = path_str
|
54
|
+
@start_model = smodel
|
55
|
+
segment
|
56
|
+
end
|
57
|
+
|
58
|
+
def size
|
59
|
+
@segments.size
|
60
|
+
end
|
61
|
+
|
62
|
+
# source: ..https://ruby-doc.org/core-2.6.5/Hash.html
|
63
|
+
def ==(other)
|
64
|
+
(self.class === other) &&
|
65
|
+
(@start_model == other.start_model) &&
|
66
|
+
(@qpath == other.qpath)
|
67
|
+
end
|
68
|
+
|
69
|
+
alias eql? ==
|
70
|
+
|
71
|
+
def hash
|
72
|
+
@start_model.hash ^ @qpath.hash # XOR
|
73
|
+
end
|
74
|
+
|
75
|
+
def segment
|
76
|
+
@segments = []
|
77
|
+
start_node = @start_model.create_path_node('')
|
78
|
+
start_node.model_class = @start_model
|
79
|
+
path_str = ''
|
80
|
+
path_nodes = []
|
81
|
+
start_node.path_str = path_str
|
82
|
+
nodes = [start_node]
|
83
|
+
nodes.concat @qpath.split('/').map { |nstr| @start_model.create_path_node(nstr) }
|
84
|
+
if (ns = nodes.size) > 1
|
85
|
+
nodes[0...-1].each_with_index do |node, i|
|
86
|
+
nodes[i + 1].set_model_class_by_parent_model(node.model_class)
|
87
|
+
path_nodes << nodes[i + 1]
|
88
|
+
nodes[i + 1].path_str = path_nodes.join('/')
|
89
|
+
@segments << [node, nodes[i + 1]]
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
class JoinByPathsHelper < Set
|
96
|
+
attr_reader :result
|
97
|
+
attr_reader :start_model
|
98
|
+
|
99
|
+
def initialize(smodel)
|
100
|
+
super()
|
101
|
+
@start_model = smodel
|
102
|
+
end
|
103
|
+
|
104
|
+
def build_unique_join_segments
|
105
|
+
return (@result = []) if empty?
|
106
|
+
|
107
|
+
maxlen = map(&:size).max
|
108
|
+
iset = nil
|
109
|
+
@result = []
|
110
|
+
|
111
|
+
(0...maxlen).each do |i|
|
112
|
+
iset = Set.new
|
113
|
+
each { |qp| (iset << qp.segments[i]) if qp.segments[i] }
|
114
|
+
@result << iset
|
115
|
+
end
|
116
|
+
|
117
|
+
@result.map! do |jseg|
|
118
|
+
jseg.map do |seg|
|
119
|
+
leftm = seg.first.model_class
|
120
|
+
assoc = leftm.association_reflection(seg.last.to_sym)
|
121
|
+
rightm = seg.last.model_class
|
122
|
+
# cf. documentation in sequel/model/associations.rb
|
123
|
+
case assoc[:type]
|
124
|
+
# :many_to_one :: Foreign key in current model's table points to
|
125
|
+
# associated model's primary key.
|
126
|
+
when :many_to_one
|
127
|
+
lks = [assoc[:key]].flatten
|
128
|
+
rks = [rightm.primary_key].flatten
|
129
|
+
|
130
|
+
when :one_to_many, :one_to_one
|
131
|
+
# :one_to_many :: Foreign key in associated model's table points to this
|
132
|
+
# model's primary key.
|
133
|
+
# :one_to_one :: Similar to one_to_many in terms of foreign keys, but
|
134
|
+
# only one object is associated to the current object through the
|
135
|
+
# association.
|
136
|
+
lks = [leftm.primary_key].flatten
|
137
|
+
rks = [assoc[:key]].flatten
|
138
|
+
|
139
|
+
# TODO
|
140
|
+
# when # :many_to_many :: A join table is used that has a foreign key that points
|
141
|
+
# to this model's primary key and a foreign key that points to the
|
142
|
+
# associated model's primary key. Each current model object can be
|
143
|
+
# associated with many associated model objects, and each associated
|
144
|
+
# model object can be associated with many current model objects.
|
145
|
+
# when # :one_through_one :: Similar to many_to_many in terms of foreign keys, but only one object
|
146
|
+
# is associated to the current object through the association.
|
147
|
+
# Provides only getter methods, no setter or modification methods.
|
148
|
+
|
149
|
+
end
|
150
|
+
|
151
|
+
lks.map! { |k| Sequel[seg.first.alias_sym][k] } unless seg.first.empty?
|
152
|
+
|
153
|
+
rks.map! { |k| Sequel[seg.last.alias_sym][k] }
|
154
|
+
|
155
|
+
{ type: assoc[:type],
|
156
|
+
segment: seg,
|
157
|
+
left: leftm.table_name,
|
158
|
+
right: rightm.table_name,
|
159
|
+
alias: seg.last.alias_sym,
|
160
|
+
left_keys: lks,
|
161
|
+
right_keys: rks,
|
162
|
+
cond: rks.zip(lks).to_h }
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def add(path_str)
|
168
|
+
super(QPath.new(@start_model, path_str))
|
169
|
+
end
|
170
|
+
|
171
|
+
def dataset(start_dtset = nil)
|
172
|
+
start_dataset = (start_dtset || @start_model.dataset)
|
173
|
+
return start_dataset if empty?
|
174
|
+
|
175
|
+
build_unique_join_segments
|
176
|
+
@result.flatten.inject(start_dataset) do |dt, jo|
|
177
|
+
dt.left_join(Sequel[jo[:right]].as(jo[:alias]),
|
178
|
+
jo[:cond],
|
179
|
+
implicit_qualifier: jo[:left])
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
def join_by_paths_helper(*pathlist)
|
184
|
+
pathlist.each { |path_str| add path_str }
|
185
|
+
self
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
module Sequel
|
190
|
+
module Plugins
|
191
|
+
module JoinByPaths
|
192
|
+
def self.apply(model)
|
193
|
+
model.instance_exec do
|
194
|
+
@aliases_sym = {}
|
195
|
+
@alias_cnt = 0
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
def self.configure(model)
|
200
|
+
model.instance_eval do
|
201
|
+
@aliases_sym = {}
|
202
|
+
@alias_cnt = 0
|
203
|
+
end
|
204
|
+
end
|
205
|
+
module ClassMethods
|
206
|
+
attr_reader :aliases_sym
|
207
|
+
attr_reader :alias_cnt
|
208
|
+
Plugins.inherited_instance_variables(self,
|
209
|
+
:@aliases_sym => :dup,
|
210
|
+
:@alias_cnt => :dup)
|
211
|
+
|
212
|
+
def get_alias_sym(pstr)
|
213
|
+
if @aliases_sym.key?(pstr)
|
214
|
+
@aliases_sym[pstr]
|
215
|
+
else
|
216
|
+
@alias_cnt += 1
|
217
|
+
@aliases_sym[pstr] = "a#{@alias_cnt}".to_sym
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
def create_path_node(pstr)
|
222
|
+
PathNode.new(pstr, self)
|
223
|
+
end
|
224
|
+
|
225
|
+
def join_by_paths_helper(*pathlist)
|
226
|
+
jh = JoinByPathsHelper.new(self)
|
227
|
+
pathlist.each { |path_str| jh.add path_str }
|
228
|
+
jh
|
229
|
+
end
|
230
|
+
end
|
231
|
+
module DatasetMethods
|
232
|
+
attr_reader :join_helper
|
233
|
+
def join_by_paths(*pathlist)
|
234
|
+
model.join_by_paths_helper(*pathlist).dataset
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|