arango-driver 3.5.0.alpha0
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 +7 -0
- data/LICENSE +21 -0
- data/README.md +1073 -0
- data/arango_opal.js +15 -0
- data/lib/arango-driver.rb +61 -0
- data/lib/arango.rb +96 -0
- data/lib/arango/aql.rb +188 -0
- data/lib/arango/collection.rb +575 -0
- data/lib/arango/collection/documents.rb +122 -0
- data/lib/arango/collection/edges.rb +149 -0
- data/lib/arango/collection/importing.rb +57 -0
- data/lib/arango/collection/indexes.rb +53 -0
- data/lib/arango/collection/replication.rb +24 -0
- data/lib/arango/collection/user.rb +28 -0
- data/lib/arango/cursor.rb +67 -0
- data/lib/arango/database.rb +188 -0
- data/lib/arango/database/analyzer.rb +21 -0
- data/lib/arango/database/aql_functions.rb +54 -0
- data/lib/arango/database/aql_queries.rb +114 -0
- data/lib/arango/database/aql_query_cache.rb +27 -0
- data/lib/arango/database/collections.rb +100 -0
- data/lib/arango/database/foxx_services.rb +103 -0
- data/lib/arango/database/graph_access.rb +27 -0
- data/lib/arango/database/http_route.rb +9 -0
- data/lib/arango/database/replication.rb +96 -0
- data/lib/arango/database/stream_transactions.rb +25 -0
- data/lib/arango/database/tasks.rb +67 -0
- data/lib/arango/database/transactions.rb +15 -0
- data/lib/arango/database/user.rb +26 -0
- data/lib/arango/database/view_access.rb +37 -0
- data/lib/arango/document.rb +443 -0
- data/lib/arango/edge.rb +164 -0
- data/lib/arango/error.rb +97 -0
- data/lib/arango/error_db.rb +27 -0
- data/lib/arango/foxx.rb +255 -0
- data/lib/arango/graph.rb +202 -0
- data/lib/arango/graph/basics.rb +39 -0
- data/lib/arango/graph/edge_access.rb +56 -0
- data/lib/arango/graph/vertex_access.rb +33 -0
- data/lib/arango/helper/collection_assignment.rb +13 -0
- data/lib/arango/helper/database_assignment.rb +14 -0
- data/lib/arango/helper/request_method.rb +45 -0
- data/lib/arango/helper/return.rb +21 -0
- data/lib/arango/helper/satisfaction.rb +28 -0
- data/lib/arango/helper/server_assignment.rb +13 -0
- data/lib/arango/helper/traversal.rb +12 -0
- data/lib/arango/index.rb +103 -0
- data/lib/arango/replication.rb +231 -0
- data/lib/arango/request.rb +92 -0
- data/lib/arango/request_batch.rb +174 -0
- data/lib/arango/result.rb +130 -0
- data/lib/arango/search_view.rb +23 -0
- data/lib/arango/server.rb +68 -0
- data/lib/arango/server/administration.rb +296 -0
- data/lib/arango/server/agency.rb +23 -0
- data/lib/arango/server/async.rb +51 -0
- data/lib/arango/server/batch.rb +35 -0
- data/lib/arango/server/config.rb +76 -0
- data/lib/arango/server/databases.rb +71 -0
- data/lib/arango/server/monitoring.rb +17 -0
- data/lib/arango/server/opal_support.rb +95 -0
- data/lib/arango/server/tasks.rb +69 -0
- data/lib/arango/server/user.rb +22 -0
- data/lib/arango/task.rb +223 -0
- data/lib/arango/transaction.rb +113 -0
- data/lib/arango/traversal.rb +212 -0
- data/lib/arango/user.rb +174 -0
- data/lib/arango/version.rb +3 -0
- data/lib/arango/vertex.rb +112 -0
- data/lib/arango/view.rb +124 -0
- data/lib/arango/view/basics.rb +25 -0
- metadata +296 -0
data/lib/arango/graph.rb
ADDED
@@ -0,0 +1,202 @@
|
|
1
|
+
# === GRAPH ===
|
2
|
+
|
3
|
+
module Arango
|
4
|
+
class Graph
|
5
|
+
include Arango::Helper::Satisfaction
|
6
|
+
include Arango::Helper::Return
|
7
|
+
include Arango::Helper::DatabaseAssignment
|
8
|
+
|
9
|
+
def initialize(name:, database:, body: {}, cache_name: nil, edge_definitions: [], is_smart: nil, number_of_shards: nil, orphan_collections: [],
|
10
|
+
replication_factor: nil, smart_graph_attribute: nil)
|
11
|
+
assign_database(database)
|
12
|
+
unless cache_name.nil?
|
13
|
+
@cache_name = cache_name
|
14
|
+
@server.cache.save(:graph, cache_name, self)
|
15
|
+
end
|
16
|
+
body[:_key] ||= name
|
17
|
+
body[:_id] ||= "_graphs/#{name}"
|
18
|
+
body[:edgeDefinitions] ||= edge_definitions
|
19
|
+
body[:isSmart] ||= is_smart
|
20
|
+
body[:numberOfShards] ||= number_of_shards
|
21
|
+
body[:orphanCollections] ||= orphan_collections
|
22
|
+
body[:replicationFactor] ||= replication_factor
|
23
|
+
body[:smartGraphAttribute] ||= smart_graph_attribute
|
24
|
+
assign_attributes(body)
|
25
|
+
end
|
26
|
+
|
27
|
+
# === DEFINE ===
|
28
|
+
|
29
|
+
attr_reader :body, :cache_name, :database, :id, :is_smart, :name, :rev, :server
|
30
|
+
attr_accessor :number_of_shards, :replication_factor, :smart_graph_attribute
|
31
|
+
alias key name
|
32
|
+
|
33
|
+
def body=(result)
|
34
|
+
@body = result
|
35
|
+
assign_edge_definitions(result[:edgeDefinitions] || @edge_definitions)
|
36
|
+
assign_orphan_collections(result[:orphanCollections] || @orphan_collections)
|
37
|
+
@name = result[:_key] || @name
|
38
|
+
@id = result[:_id] || @id
|
39
|
+
@id = "_graphs/#{@name}" if @id.nil? && !@name.nil?
|
40
|
+
@rev = result[:_rev] || @rev
|
41
|
+
@is_smart = result[:isSmart] || @is_smart
|
42
|
+
@number_of_shards = result[:numberOfShards] || @number_of_shards
|
43
|
+
@replication_factor = result[:replicationFactor] || @replication_factor
|
44
|
+
@smart_graph_attribute = result[:smartGraphAttribute] || @smart_graph_attribute
|
45
|
+
if @server.active_cache && @cache_name.nil?
|
46
|
+
@cache_name = "#{@database.name}/#{@name}"
|
47
|
+
@server.cache.save(:graph, @cache_name, self)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
alias assign_attributes body=
|
51
|
+
|
52
|
+
def name=(name)
|
53
|
+
@name = name
|
54
|
+
@id = "_graphs/#{@name}"
|
55
|
+
end
|
56
|
+
|
57
|
+
def return_collection(collection, type=nil)
|
58
|
+
satisfy_class?(collection, [Arango::Collection, String])
|
59
|
+
case collection
|
60
|
+
when Arango::Collection
|
61
|
+
return collection
|
62
|
+
when String
|
63
|
+
return Arango::Collection.new(name: collection,
|
64
|
+
database: @database, type: type, graph: self)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def edge_definitions_raw
|
69
|
+
@edge_definitions ||= []
|
70
|
+
@edge_definitions.map do |edgedef|
|
71
|
+
{
|
72
|
+
collection: edgedef[:collection].name,
|
73
|
+
from: edgedef[:from].map{|t| t.name},
|
74
|
+
to: edgedef[:to].map{|t| t.name}
|
75
|
+
}
|
76
|
+
end
|
77
|
+
end
|
78
|
+
private :edge_definitions_raw
|
79
|
+
|
80
|
+
def edge_definitions(raw=false)
|
81
|
+
return edge_definitions_raw if raw
|
82
|
+
return @edge_definitions
|
83
|
+
end
|
84
|
+
|
85
|
+
def edge_definitions=(edge_definitions)
|
86
|
+
@edge_definitions = []
|
87
|
+
edge_definitions ||= []
|
88
|
+
edge_definitions = [edge_definitions] unless edge_definitions.is_a?(Array)
|
89
|
+
edge_definitions.each do |edge_definition|
|
90
|
+
hash = {}
|
91
|
+
hash[:collection] = return_collection(edge_definition[:collection], :edge)
|
92
|
+
edge_definition[:from] ||= []
|
93
|
+
edge_definition[:to] ||= []
|
94
|
+
hash[:from] = edge_definition[:from].map{|t| return_collection(t)}
|
95
|
+
hash[:to] = edge_definition[:to].map{|t| return_collection(t)}
|
96
|
+
setup_orphan_collection_after_adding_edge_definitions(hash)
|
97
|
+
@edge_definitions << hash
|
98
|
+
end
|
99
|
+
end
|
100
|
+
alias assign_edge_definitions edge_definitions=
|
101
|
+
|
102
|
+
def orphan_collections=(orphan_collections)
|
103
|
+
orphan_collections ||= []
|
104
|
+
orphan_collections = [orphan_collections] unless orphan_collections.is_a?(Array)
|
105
|
+
@orphan_collections = orphan_collections.map{|oc| add_orphan_collection(oc)}
|
106
|
+
end
|
107
|
+
alias assign_orphan_collections orphan_collections=
|
108
|
+
|
109
|
+
def orphan_collections_raw
|
110
|
+
@orphan_collections ||= []
|
111
|
+
@orphan_collections.map{|oc| oc.name}
|
112
|
+
end
|
113
|
+
private :orphan_collections_raw
|
114
|
+
|
115
|
+
def orphan_collections(raw=false)
|
116
|
+
return orphan_collections_raw if raw
|
117
|
+
return @orphan_collections
|
118
|
+
end
|
119
|
+
|
120
|
+
# === HANDLE ORPHAN COLLECTION ===
|
121
|
+
|
122
|
+
def add_orphan_collection(orphanCollection)
|
123
|
+
orphanCollection = return_collection(orphanCollection)
|
124
|
+
if @edge_definitions.any? do |ed|
|
125
|
+
names = []
|
126
|
+
names |= ed[:from].map{|f| f&.name}
|
127
|
+
names |= ed[:to].map{|t| t&.name}
|
128
|
+
names.include?(orphanCollection.name)
|
129
|
+
end
|
130
|
+
raise Arango::Error.new err: :orphan_collection_used_by_edge_definition, data: {collection: orphanCollection.name}
|
131
|
+
end
|
132
|
+
return orphanCollection
|
133
|
+
end
|
134
|
+
private :add_orphan_collection
|
135
|
+
|
136
|
+
def setup_orphan_collection_after_adding_edge_definitions(edge_definition)
|
137
|
+
collection = []
|
138
|
+
collection |= edge_definition[:from]
|
139
|
+
collection |= edge_definition[:to]
|
140
|
+
@orphan_collections.delete_if{|c| collection.include?(c.name)}
|
141
|
+
end
|
142
|
+
private :setup_orphan_collection_after_adding_edge_definitions
|
143
|
+
|
144
|
+
def setup_orphan_collection_after_removing_edge_definitions(edge_definition)
|
145
|
+
edgeCollection = edge_definition[:collection].name
|
146
|
+
collections |= []
|
147
|
+
collections |= edge_definition[:from]
|
148
|
+
collections |= edge_definition[:to]
|
149
|
+
collections.each do |collection|
|
150
|
+
unless @edge_definitions.any? do |ed|
|
151
|
+
if ed[:collection].name != edgeCollection
|
152
|
+
names = []
|
153
|
+
names |= ed[:from].map{|f| f&.name}
|
154
|
+
names |= ed[:to].map{|t| t&.name}
|
155
|
+
names.include?(collection.name)
|
156
|
+
else
|
157
|
+
false
|
158
|
+
end
|
159
|
+
end
|
160
|
+
unless @orphan_collections.map{|oc| oc.name}.include?(collection.name)
|
161
|
+
@orphan_collections << collection
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
private :setup_orphan_collection_after_removing_edge_definitions
|
167
|
+
|
168
|
+
# === REQUEST ===
|
169
|
+
|
170
|
+
def request(action, url, body: {}, headers: {}, query: {}, key: nil, return_direct_result: false)
|
171
|
+
url = "_api/gharial/#{@name}/#{url}"
|
172
|
+
@database.request(action, url, body: body, headers: headers,
|
173
|
+
query: query, key: key, return_direct_result: return_direct_result)
|
174
|
+
end
|
175
|
+
|
176
|
+
# === TO HASH ===
|
177
|
+
|
178
|
+
def to_h
|
179
|
+
{
|
180
|
+
name: @name,
|
181
|
+
id: @id,
|
182
|
+
rev: @rev,
|
183
|
+
isSmart: @is_smart,
|
184
|
+
numberOfShards: @number_of_shards,
|
185
|
+
replicationFactor: @replication_factor,
|
186
|
+
smartGraphAttribute: @smart_graph_attribute,
|
187
|
+
edgeDefinitions: edge_definitions_raw,
|
188
|
+
orphanCollections: orphan_collections_raw,
|
189
|
+
cache_name: @cache_name,
|
190
|
+
database: @database.name
|
191
|
+
}.delete_if{|k,v| v.nil?}
|
192
|
+
end
|
193
|
+
|
194
|
+
# === GET ===
|
195
|
+
|
196
|
+
def retrieve
|
197
|
+
result = @database.request("GET", "_api/gharial/#{@name}", key: :graph)
|
198
|
+
return_element(result)
|
199
|
+
end
|
200
|
+
|
201
|
+
end
|
202
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Arango
|
2
|
+
module Graph
|
3
|
+
module Basics
|
4
|
+
def create(is_smart: @is_smart, smart_graph_attribute: @smart_graph_attribute,
|
5
|
+
number_of_shards: @number_of_shards)
|
6
|
+
body = {
|
7
|
+
name: @name,
|
8
|
+
edgeDefinitions: edge_definitions_raw,
|
9
|
+
orphanCollections: orphan_collections_raw,
|
10
|
+
isSmart: is_smart,
|
11
|
+
options: {
|
12
|
+
smartGraphAttribute: smart_graph_attribute,
|
13
|
+
numberOfShards: number_of_shards
|
14
|
+
}
|
15
|
+
}
|
16
|
+
body[:options].delete_if{|k,v| v.nil?}
|
17
|
+
body.delete(:options) if body[:options].empty?
|
18
|
+
result = @database.request("POST", "_api/gharial", body: body, key: :graph)
|
19
|
+
return_element(result)
|
20
|
+
end
|
21
|
+
|
22
|
+
def exist?
|
23
|
+
|
24
|
+
end
|
25
|
+
alias exists? exist?
|
26
|
+
|
27
|
+
def info
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
def drop(dropCollections: nil)
|
32
|
+
query = { dropCollections: dropCollections }
|
33
|
+
result = @database.request("DELETE", "_api/gharial/#{@name}", query: query,
|
34
|
+
key: :removed)
|
35
|
+
return_delete(result)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Arango
|
2
|
+
module Graph
|
3
|
+
module EdgeAccess
|
4
|
+
def edge_collection
|
5
|
+
|
6
|
+
end
|
7
|
+
|
8
|
+
def edge_collections
|
9
|
+
result = request("GET", "edge", key: :collections)
|
10
|
+
return result if @database.server.async != false
|
11
|
+
return result if return_directly?(result)
|
12
|
+
result.map{|r| Arango::Collection.new(database: @database, name: r, type: :edge)}
|
13
|
+
end
|
14
|
+
|
15
|
+
def edge_definitions
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
def add_edge_definition(collection:, from:, to:)
|
20
|
+
satisfy_class?(collection, [String, Arango::Collection])
|
21
|
+
satisfy_class?(from, [String, Arango::Collection], true)
|
22
|
+
satisfy_class?(to, [String, Arango::Collection], true)
|
23
|
+
from = [from] unless from.is_a?(Array)
|
24
|
+
to = [to] unless to.is_a?(Array)
|
25
|
+
body = {}
|
26
|
+
body[:collection] = collection.is_a?(String) ? collection : collection.name
|
27
|
+
body[:from] = from.map{|f| f.is_a?(String) ? f : f.name }
|
28
|
+
body[:to] = to.map{|t| t.is_a?(String) ? t : t.name }
|
29
|
+
result = request("POST", "edge", body: body, key: :graph)
|
30
|
+
return_element(result)
|
31
|
+
end
|
32
|
+
|
33
|
+
def replace_edge_definition(collection:, from:, to:)
|
34
|
+
satisfy_class?(collection, [String, Arango::Collection])
|
35
|
+
satisfy_class?(from, [String, Arango::Collection], true)
|
36
|
+
satisfy_class?(to, [String, Arango::Collection], true)
|
37
|
+
from = [from] unless from.is_a?(Array)
|
38
|
+
to = [to] unless to.is_a?(Array)
|
39
|
+
body = {}
|
40
|
+
body[:collection] = collection.is_a?(String) ? collection : collection.name
|
41
|
+
body[:from] = from.map{|f| f.is_a?(String) ? f : f.name }
|
42
|
+
body[:to] = to.map{|t| t.is_a?(String) ? t : t.name }
|
43
|
+
result = request("PUT", "edge/#{body[:collection]}", body: body, key: :graph)
|
44
|
+
return_element(result)
|
45
|
+
end
|
46
|
+
|
47
|
+
def remove_edge_definition(collection:, dropCollection: nil)
|
48
|
+
satisfy_class?(collection, [String, Arango::Collection])
|
49
|
+
query = {dropCollection: dropCollection}
|
50
|
+
collection = collection.is_a?(String) ? collection : collection.name
|
51
|
+
result = request("DELETE", "edge/#{collection}", query: query, key: :graph)
|
52
|
+
return_element(result)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Arango
|
2
|
+
module Graph
|
3
|
+
module VertexAccess
|
4
|
+
def vertex_collection
|
5
|
+
|
6
|
+
end
|
7
|
+
|
8
|
+
def vertex_collections
|
9
|
+
result = request("GET", "vertex", key: :collections)
|
10
|
+
return result if return_directly?(result)
|
11
|
+
result.map do |x|
|
12
|
+
Arango::Collection.new(name: x, database: @database, graph: self)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def add_vertex_collection(collection:)
|
17
|
+
satisfy_class?(collection, [String, Arango::Collection])
|
18
|
+
collection = collection.is_a?(String) ? collection : collection.name
|
19
|
+
body = { collection: collection }
|
20
|
+
result = request("POST", "vertex", body: body, key: :graph)
|
21
|
+
return_element(result)
|
22
|
+
end
|
23
|
+
|
24
|
+
def remove_vertex_collection(collection:, dropCollection: nil)
|
25
|
+
query = {dropCollection: dropCollection}
|
26
|
+
satisfy_class?(collection, [String, Arango::Collection])
|
27
|
+
collection = collection.is_a?(String) ? collection : collection.name
|
28
|
+
result = request("DELETE", "vertex/#{collection}", query: query, key: :graph)
|
29
|
+
return_element(result)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Arango
|
2
|
+
module Helper
|
3
|
+
module CollectionAssignment
|
4
|
+
def assign_collection(collection)
|
5
|
+
satisfy_class?(collection, [Arango::Collection])
|
6
|
+
@collection = collection
|
7
|
+
@graph = @collection.graph
|
8
|
+
@database = @collection.database
|
9
|
+
@arango_server = @database.arango_server
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Arango
|
2
|
+
module Helper
|
3
|
+
module RequestMethod
|
4
|
+
def request_method(method_name, &block)
|
5
|
+
promise_method_name = "batch_#{method_name}".to_sym
|
6
|
+
define_method(method_name) do |*args|
|
7
|
+
request_hash = instance_exec(*args, &block)
|
8
|
+
@database.execute_request(request_hash)
|
9
|
+
end
|
10
|
+
define_method(promise_method_name) do |*args|
|
11
|
+
request_hash = instance_exec(*args, &block)
|
12
|
+
@database.batch_request(request_hash)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def multi_request_method(method_name, &block)
|
17
|
+
promise_method_name = "batch_#{method_name}".to_sym
|
18
|
+
define_method(method_name) do |*args|
|
19
|
+
requests = instance_exec(*args, &block)
|
20
|
+
@database.execute_requests(requests)
|
21
|
+
end
|
22
|
+
define_method(promise_method_name) do |*args|
|
23
|
+
requests= instance_exec(*args, &block)
|
24
|
+
promises = []
|
25
|
+
requests.each do |request_hash|
|
26
|
+
promises << @database.batch_request(request_hash)
|
27
|
+
end
|
28
|
+
Promise.when(*promises).then { |values| values.last }
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def aql_request_method(method_name, &block)
|
33
|
+
promise_method_name = "batch_#{method_name}".to_sym
|
34
|
+
define_method(method_name) do |*args|
|
35
|
+
request_hash = instance_exec(*args, &block)
|
36
|
+
@database.execute_aql(request_hash)
|
37
|
+
end
|
38
|
+
define_method(promise_method_name) do |*args|
|
39
|
+
request_hash = instance_exec(*args, &block)
|
40
|
+
@database.batch_execute_aql(request_hash)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Arango
|
2
|
+
module Helper
|
3
|
+
module Return
|
4
|
+
def return_directly?(result)
|
5
|
+
return result if @server.async || @server.return_output
|
6
|
+
result == true
|
7
|
+
end
|
8
|
+
|
9
|
+
def return_element(result)
|
10
|
+
return result unless @server.async
|
11
|
+
assign_attributes(result)
|
12
|
+
return_directly?(result) ? result : self
|
13
|
+
end
|
14
|
+
|
15
|
+
def return_delete(result)
|
16
|
+
return result unless @server.async
|
17
|
+
return_directly?(result) ? result : true
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Arango
|
2
|
+
module Helper
|
3
|
+
module Satisfaction
|
4
|
+
def satisfy_class?(object, classes=[String], array=false)
|
5
|
+
if array
|
6
|
+
object = [object] unless object.is_a?(Array)
|
7
|
+
object.each do |obj|
|
8
|
+
satisfy_class?(obj, classes, false)
|
9
|
+
end
|
10
|
+
else
|
11
|
+
return if classes.include?(object.class)
|
12
|
+
name ||= object.object_id.to_s
|
13
|
+
raise Arango::Error.new err: :wrong_class, data: { wrong_value: name, wrong_class: object.class.to_s, expected_class: classes.to_s }
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def satisfy_category?(object, list)
|
18
|
+
return if list.include?(object)
|
19
|
+
name = object.object_id.to_s
|
20
|
+
raise Arango::Error.new err: :wrong_element, data: { wrong_attribute: name, wrong_value: object, list: list }
|
21
|
+
end
|
22
|
+
|
23
|
+
def warning_deprecated(warning, name)
|
24
|
+
puts "ARANGORB WARNING: #{name} function is deprecated" if warning
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|