arcadedb 0.3.3 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,5 +1,6 @@
1
1
  module Arcade
2
2
  module Api
3
+ extend Primitives
3
4
  =begin
4
5
  This is a simple admin interface
5
6
 
@@ -8,7 +9,7 @@ module Arcade
8
9
  $ Arcade::Api.drop_database <a string> # returns true if successfull
9
10
 
10
11
  $ Arcade::Api.create_document <database>, <type>, attributes
11
- $ Arcade::Api.execute( <database> ) { <query> }
12
+ $ Arcade::Api.execute( <database> [, session_id: some_session_id ]) { <query> }
12
13
  $ Arcade::Api.query( <database> ) { <query> }
13
14
  $ Arcade::Api.get_record <database>, rid # returns a hash
14
15
 
@@ -22,46 +23,53 @@ module Arcade
22
23
 
23
24
  =end
24
25
 
26
+ # ------------------------------ Service methods ------------------------------------------------- #
27
+ # ------------------------------ ------------------------------------------------- #
28
+ # ------------------------------ databases ------------------------------------------------- #
29
+ # returns an array of databases present on the database-server #
25
30
 
26
31
  def self.databases
27
32
  get_data 'databases'
28
33
  end
29
34
 
35
+ # ------------------------------ create database ------------------------------------------------- #
36
+ # creates a database if not present #
30
37
  def self.create_database name
31
- unless databases.include?( name.to_s )
32
- payload = { "command" => "create database #{name}" }.to_json
33
- post_data "server", { body: payload }.merge( auth ).merge( json )
34
- end
35
- rescue QueryError => e
38
+ return if databases.include?( name.to_s )
39
+ payload = { "command" => "create database #{name}" }
40
+ post_data "server", payload
41
+ rescue Arcade::QueryError => e
36
42
  logger.fatal "Create database #{name} through \"POST create/#{name}\" failed"
37
43
  logger.fatal e
38
44
  raise
39
45
  end
40
46
 
47
+ # ------------------------------ drop database ------------------------------------------------- #
48
+ # deletes the given database #
41
49
  def self.drop_database name
42
- if databases.include?( name.to_s )
43
- payload = { "command" => "drop database #{name}" }.to_json
44
- post_data "server", { body: payload }.merge( auth ).merge( json )
45
- end
50
+ return unless databases.include?( name.to_s )
51
+ payload = {"command" => "drop database #{name}" }
52
+ post_data "server", payload
53
+ rescue Arcade::QueryError => e
54
+ logger.fatal "Drop database #{name} through \"POST drop database/#{name}\" failed"
55
+ raise
46
56
  end
47
57
  # ------------------------------ create document ------------------------------------------------- #
48
- # adds a document to the database
58
+ # adds a document to the specified database table
49
59
  #
50
60
  # specify database-fields as hash-type parameters
51
61
  #
52
62
  # i.e Arcade::Api.create_document 'devel', 'documents', name: 'herta meyer', age: 56, sex: 'f'
53
63
  #
54
64
  # returns the rid of the inserted dataset
55
- #
56
- def self.create_document database, type, **attributes
57
- payload = { "@type" => type }.merge( attributes ).to_json
58
- logger.debug "C: #{payload}"
59
- options = if session.nil?
60
- { body: payload }.merge( auth ).merge( json )
61
- else
62
- { body: payload }.merge( auth ).merge( json ).merge( headers: { "arcadedb-session-id" => session })
63
- end
64
- post_data "document/#{database}", options
65
+ #
66
+ def self.create_document database, type, session_id: nil, **attributes
67
+ payload = { "@type" => type }.merge( attributes )
68
+ if session_id.nil?
69
+ post_data "document/#{database}", payload
70
+ else
71
+ post_transaction "document/#{database}", payload, session_id: session_id
72
+ end
65
73
  end
66
74
 
67
75
  # ------------------------------ execute ------------------------------------------------- #
@@ -71,32 +79,26 @@ module Arcade
71
79
  #
72
80
  # returns an Array of results (if propriate)
73
81
  # i.e
74
- # Arcade::Api.execcute( "devel" ) { 'select from test ' }
82
+ # Arcade::Api.execute( "devel" ) { 'select from test ' }
75
83
  # =y [{"@rid"=>"#57:0", "@type"=>"test", "name"=>"Hugo"}, {"@rid"=>"#60:0", "@type"=>"test", "name"=>"Hubert"}]
76
84
  #
77
- def self.execute database, query=nil
78
- pl = query.nil? ? provide_payload(yield) : provide_payload(query)
79
- options = { body: pl }.merge( auth ).merge( json )
80
- unless session.nil?
81
- options = options.merge( headers: { "arcadedb-session-id" => session })
85
+ def self.execute database, session_id: nil
86
+ pl = provide_payload(yield)
87
+ if session_id.nil?
88
+ post_data "command/#{database}" , pl
89
+ else
90
+ post_transaction "command/#{database}" , pl, session_id: session_id
82
91
  end
83
- post_data "command/#{database}" , options
84
- rescue Arcade::QueryError => e
85
- # puts e.methods
86
- #puts e.exception
87
- # puts e.full_message
88
- if e.message =~ /retry/
89
- retry
90
- else
91
- raise e.message
92
- end
93
92
  end
94
93
 
95
94
  # ------------------------------ query ------------------------------------------------- #
96
95
  # same for idempotent queries
97
- def self.query database, query
98
- options = { body: provide_payload(query) }.merge( auth ).merge( json )
99
- post_data "query/#{database}" , options
96
+ def self.query database, query, session_id: nil
97
+ if session_id.nil?
98
+ post_data "query/#{database}" , provide_payload(query)
99
+ else
100
+ post_transaction "query/#{database}" , provide_payload(query), session_id: session_id
101
+ end
100
102
  end
101
103
 
102
104
  # ------------------------------ get_record ------------------------------------------------- #
@@ -114,7 +116,7 @@ module Arcade
114
116
  if rid.rid?
115
117
  get_data "document/#{database}/#{rid}"
116
118
  else
117
- raise Arcade::Error "Get requires a rid input"
119
+ raise Error "Get requires a rid input"
118
120
  end
119
121
  end
120
122
 
@@ -133,20 +135,17 @@ module Arcade
133
135
  #
134
136
  def self.property database, type, **args
135
137
 
136
- begin_transaction database
138
+ s= begin_transaction database
137
139
  success = args.map do | name, format |
138
- r= execute(database) {" create property #{type.to_s}.#{name.to_s} #{format.to_s} " } &.first
139
- if r.nil?
140
- false
141
- else
142
- r.keys == [ :propertyName, :typeName, :operation ] && r[:operation] == 'create property'
143
- end
140
+ r= execute(database, session_id: s) {" create property #{type.to_s}.#{name.to_s} #{format.to_s} " } &.first
141
+ r.nil? ? false : r[:operation] == 'create property'
144
142
  end.uniq
145
143
  if success == [true]
146
- commit database
144
+ commit database, session_id: s
147
145
  true
148
146
  else
149
- rollback database
147
+ rollback database log: false, session_id: s
148
+ false
150
149
  end
151
150
 
152
151
 
@@ -155,54 +154,23 @@ module Arcade
155
154
  # ------------------------------ index ------------------------------------------------- #
156
155
  def self.index database, type, name , *properties
157
156
  properties = properties.map( &:to_s )
158
- unique_requested = "unique" if properties.delete("unique")
157
+ unique_requested = "unique" if properties.delete("unique")
159
158
  unique_requested = "notunique" if properties.delete("notunique" )
160
159
  automatic = true if
161
160
  properties << name if properties.empty?
162
- # puts " create index #{type.to_s}[#{name.to_s}] on #{type} ( #{properties.join(',')} ) #{unique_requested}"
163
- # VV 22.10: providing an index-name raises an Error ( Encountered " "(" "( "" at line 1, column 44. Was expecting one of: <EOF> <SCHEMA> ... <NULL_STRATEGY> ... ";" ... "," ... )) )
164
- # named indices droped for now
165
- success = execute(database) {" create index on #{type} ( #{properties.join(',')} ) #{unique_requested}" } &.first
161
+ success = execute(database) {" create index IF NOT EXISTS on #{type} (#{properties.join(', ')}) #{unique_requested}" } &.first
166
162
  # puts "success: #{success}"
167
- success && success.keys == [ :totalIndexed, :name, :operation ] && success[:operation] == 'create index'
168
-
169
- end
170
-
163
+ success[:operation] == 'create index'
171
164
 
172
- # ------------------------------ transaction ------------------------------------------------- #
173
- #
174
- def self.begin_transaction database
175
- result = Typhoeus.post Arcade::Config.base_uri + "begin/#{database}", auth
176
- @session_id = result.headers["arcadedb-session-id"]
177
-
178
- # returns the session-id
179
165
  end
180
166
 
181
167
 
182
- # ------------------------------ commit ------------------------------------------------- #
183
- def self.commit database
184
- options = auth.merge( headers: { "arcadedb-session-id" => session })
185
- post_data "commit/#{database}", options
186
- @session_id = nil
187
- end
188
-
189
- # ------------------------------ rollback ------------------------------------------------- #
190
- def self.rollback database, publish_error=true
191
- options = auth.merge( headers: { "arcadedb-session-id" => session })
192
- post_data "rollback/#{database}", options
193
- @session_id = nil
194
- raise Arcade::RollbackError "A Transaction has been rolled back" if publish_error
195
- end
196
-
197
168
  private
198
169
 
199
170
  def self.logger
200
171
  Database.logger
201
172
  end
202
173
 
203
- def self.session
204
- @session_id
205
- end
206
174
 
207
175
  def self. provide_payload( the_yield, action: :post )
208
176
  unless the_yield.is_a? Hash
@@ -231,76 +199,20 @@ module Arcade
231
199
  [ :serializer, value.to_sym ]
232
200
  end
233
201
  when :language
234
- if [:sql, :cypher, :gremlin, :neo4j ].include? value.to_sym
202
+ if [:sql, :cypher, :gremlin, :neo4j, :sqlscript, :graphql, :mongo ].include? value.to_sym
235
203
  [ :language, value.to_sym ]
236
204
  end
237
205
  end # case
238
- end .to_h ).to_json # map
239
- end
240
-
241
- def self.get_data command, options = auth
242
- result = Typhoeus.get Arcade::Config.base_uri + command, options
243
- analyse_result(result, command)
206
+ end .to_h ) # map
244
207
  end
245
208
 
246
209
 
247
- def self.post_data command, options = auth
248
- # puts "Post DATA #{command} #{options}" # debug
249
- i = 0; a=""
250
- loop do
251
- result = Typhoeus.post Arcade::Config.base_uri + command, options
252
- # Code: 503 – Service Unavailable
253
- if result.response_code.to_i == 503 # retry two times
254
- i += 1
255
- raise Arcade::QueryError, JSON.parse( result.response_body, symbolize_names: true )[:result] if i > 3
256
- sleep 0.1
257
- else
258
- a= analyse_result(result, command )
259
- break
260
- end
261
- end
262
- a
263
- end
264
210
 
265
- # returns the json-response
266
- def self.analyse_result r, command
267
- if r.success?
268
- return nil if r.response_code == 204 # no content
269
- result = JSON.parse( r.response_body, symbolize_names: true )[:result]
270
- if result == [{}]
271
- []
272
- else
273
- result
274
- end
275
- elsif r.timed_out?
276
- raise Arcade::Error "Timeout Error", caller
277
- []
278
- elsif r.response_code > 0
279
- logger.error "Execution Failure – Code: #{ r.response_code } – #{r.status_message} "
280
- error_message = JSON.parse( r.response_body, symbolize_names: true )
281
- logger.error "ErrorMessage: #{ error_message[:detail]} "
282
- if error_message[:detail] =~ /Duplicated key/
283
- raise Arcade::IndexError, error_message[:detail]
284
- else
285
- # available fields: :detail, :exception, error
286
- puts error_message[:detail]
287
- #raise error_message[:detail], caller
288
- end
289
- end
290
- end
291
211
  def self.auth
292
212
  @a ||= { httpauth: :basic,
293
- username: Arcade::Config.admin[:user],
294
- password: Arcade::Config.admin[:pass] }
213
+ username: Config.admin[:user],
214
+ password: Config.admin[:pass] }
295
215
  end
296
216
 
297
- def self.json
298
- { headers: { "Content-Type" => "application/json"} }
299
- end
300
- # not tested
301
- def self.delete_data command
302
- result = Typhoeus.delete Arcade::Config.base_uri + command, auth
303
- analyse_result(result, command)
304
- end
305
217
  end
306
218
  end
@@ -0,0 +1,126 @@
1
+ module Arcade
2
+ module Api
3
+ module Primitives
4
+
5
+ # This module handles the interaction with the database through HTTPX
6
+ #
7
+ # ------------------------------ http ------------------------------------------------------------ #
8
+ # persistent http handle to the database
9
+
10
+ def http
11
+ # break_on = -> (response) { response.status == 500 }
12
+ @http ||= HTTPX.plugin(:basic_auth).basic_auth(auth[:username], auth[:password])
13
+ .plugin(:persistent)
14
+ .plugin(:circuit_breaker)
15
+ # .plugin(:circuit_breaker, circuit_breaker_break_on: break_on)
16
+ end
17
+
18
+ # ------------------------------ get data -------------------------------------------------------- #
19
+ def get_data command
20
+ case response = http.get( Config.base_uri + command )
21
+ in {status: 200..299}
22
+ # success
23
+ JSON.parse( response.body, symbolize_names: true )[:result]
24
+ in {status: 400..}
25
+ raise Arcade::QueryError.new **response.json( symbolize_names: true )
26
+ else
27
+ # # http error
28
+ raise response
29
+ end
30
+
31
+ JSON.parse( response.body, symbolize_names: true )[:result]
32
+ # alternative to `raise for status `
33
+ # case response = http.basic_auth(auth[:username], auth[:password]).get( Config.base_uri + command )
34
+ # in { status: 200..203, body: }
35
+ # puts "success: #{JSON.parse(body, symbolize_names: true)[:result]}"
36
+ # in { status: 400..499, body: }
37
+ # puts "client error: #{body.json}"
38
+ # in { status: 500.., body: }
39
+ # puts "server error: #{body.to_s}"
40
+ # in { error: error }
41
+ # puts "error: #{error.message}"
42
+ # else
43
+ # raise "unexpected: #{response}"
44
+ # end
45
+ # puts "result : #{response}"
46
+ # puts "code: #{response.status}"
47
+ # analyse_result(response, command)
48
+ end
49
+
50
+ # ------------------------------ post data -------------------------------------------------------- #
51
+ def post_data command, payload
52
+ case response = http.post( Config.base_uri + command, json: payload )
53
+ in {status: 200..299}
54
+ # success
55
+ JSON.parse( response.body, symbolize_names: true )[:result]
56
+ in {status: 400..}
57
+ detail = response.json( symbolize_names: true )[:detail]
58
+ if detail =~ /Please retry the operation/
59
+ logger.error "--------------------------------"
60
+ logger.error " ----> Operation repeated <---- "
61
+ logger.error detail
62
+ logger.error "The query --> #{payload.inspect}"
63
+ logger.error "--------------------------------"
64
+ sleep 1
65
+ post_data command, payload
66
+ else
67
+ raise Arcade::QueryError.new **response.json( symbolize_names: true )
68
+ end
69
+ else
70
+ # # http error
71
+ raise response
72
+ end
73
+ # response.raise_for_status
74
+ end
75
+
76
+ # ------------------------------ transaction ------------------------------------------------- #
77
+ #
78
+ # The payload, optional as a JSON, accepts the following parameters:
79
+ # isolationLevel: READ_COMMITTED (default) or REPEATABLE_READ. (not implemented)
80
+ #
81
+ def begin_transaction database
82
+ result = http.post Config.base_uri + "begin/#{database}"
83
+ # returns the session-id
84
+ result.headers["arcadedb-session-id"]
85
+ end
86
+
87
+ # ------------------------------ post transaction ------------------------------------------------- #
88
+ def post_transaction command, params, session_id:
89
+ http_a = http.with( headers: { "arcadedb-session-id" => session_id } , debug_level: 1)
90
+ case response = http_a.post( Config.base_uri + command, json: params )
91
+ in {status: 200..299}
92
+ # success
93
+ JSON.parse( response.body, symbolize_names: true )[:result]
94
+ in {status: 400..}
95
+ ## debug
96
+ # puts "Command: #{command}"
97
+ # puts "params: #{params}"
98
+ # puts response.json( symbolize_names: true )
99
+ raise Arcade::QueryError.new **response.json( symbolize_names: true )
100
+ else
101
+ # # http error
102
+ raise response
103
+ end
104
+ end
105
+
106
+ # ------------------------------ commit ------------------------------------------------- #
107
+ def commit database, session_id:
108
+ http_a = http.with( headers: { "arcadedb-session-id" => session_id } , debug_level: 1)
109
+ response = http_a.post( Config.base_uri + "commit/#{database}" )
110
+ response.status # returns 204 --> success
111
+ # 403 --> invalid credentials
112
+ # 500 --> Transaction not begun
113
+
114
+ end
115
+
116
+ # ------------------------------ rollback ------------------------------------------------- #
117
+ def rollback database, session_id: , log: true
118
+ http_a = http.with( headers: { "arcadedb-session-id" => session_id } , debug_level: 1)
119
+ response = http_a.post( Config.base_uri + "rollback/#{database}" )
120
+ logger.info "A Transaction has been rolled back" if log
121
+ response.status # returns 500 !
122
+ end
123
+ end
124
+ end
125
+ end
126
+