arcadedb 0.3.1 → 0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,367 @@
1
+ module Arcade
2
+ ##
3
+ # Implements the Database-Adapter
4
+ #
5
+ # currently, only attributes of type String are supported
6
+ #
7
+ # {Database-instance}.database points to the connected Aradedb-database
8
+ # DB.hi
9
+ #
10
+ ##
11
+ class Database
12
+ include Logging
13
+ extend Dry::Core::ClassAttributes
14
+ include Support::Model # provides allocate_model
15
+
16
+ defines :namespace
17
+ defines :environment
18
+
19
+ def initialize environment=:development
20
+ self.class.configure_logger( Config.logger )
21
+ @connection = connect environment
22
+ if self.class.environment.nil? # class attribute is set on the first call
23
+ # further instances of Database share the same environment
24
+ self.class.environment environment
25
+ end
26
+ self.class.namespace Object.const_get( Config.namespace )
27
+ end
28
+
29
+ def database
30
+ @database ||= Config.database[self.class.environment]
31
+ end
32
+
33
+ # ------------ types ------------...
34
+ # returns an Array of type-attributes
35
+ # [{:name=>"Account", :type=>"document"},
36
+ # {:name=>"test", :type=>"vertex"},
37
+ # {:name=>"test1", :type=>"vertex"},
38
+ # {:parentTypes=>["test1"], :name=>"test2", :type=>"vertex"}]
39
+ #
40
+ def types refresh=false
41
+ # uses API
42
+ if $types.nil? || refresh
43
+ $types = Api.query(database, "select from schema:types" )
44
+ .map{ |x| x.transform_keys &:to_sym } # symbolize keys
45
+ .map{ |y| y.delete_if{|_,b,| b.empty? } } # eliminate empty entries
46
+ end
47
+ $types
48
+ ## upom startup, this is the first access to the database-server
49
+ rescue NoMethodError => e
50
+ logger.fatal "Could not read Database Types. \n Is the database running?"
51
+ Kernel.exit
52
+ end
53
+
54
+ def indexes
55
+ DB.types.find{|x| x.key? :indexes }[:indexes]
56
+ end
57
+
58
+ # ------------ hierarchy -------------
59
+ # returns an Array of types
60
+ #
61
+ # each entry is an Array
62
+ # => [["test"], ["test1", "test2"]] (using the example above)
63
+ #
64
+ # Parameter: type -- one of 'vertex', 'document', 'edge'
65
+ def hierarchy type: 'vertex'
66
+ # uses API
67
+ # gets all types depending on the parent-type
68
+ pt = ->( s ) { types.find_all{ |x| x[:parentTypes] &.include?(s) }.map{ |v| v[:name] } }
69
+ # takes an array of base-types. gets recursivly all childs
70
+ child_types = -> (base_types) do
71
+ base_types.map do | bt |
72
+ if pt[ bt ].empty?
73
+ [ bt ]
74
+ else
75
+ [bt, child_types[ pt[ bt ] ] ].flatten
76
+ end
77
+ end
78
+ end
79
+
80
+ # gets child-types for all base-types
81
+ child_types[ types.find_all{ |x| !x[:parentTypes] && x[:type] == type.to_s }.map{ |v| v[:name] } ]
82
+
83
+ end
84
+
85
+
86
+ # ------------ create type -----------
87
+ # returns an Array
88
+ # Example: > create_type :vertex, :my_vertex
89
+ # => [{"typeName"=>"my_vertex", "operation"=>"create vertex type"}]
90
+ #
91
+ # takes additional arguments: extends: '<a supertype>' (for inheritance)
92
+ # bucket: <a list of bucket-id's >
93
+ # buckets: <how many bukets to assign>
94
+ #
95
+ # additional arguments are just added to the command
96
+ #
97
+ # its aliased as `create_class`
98
+ #
99
+ def create_type kind, type, **args
100
+
101
+ exe = -> do
102
+ case kind.to_s.downcase
103
+ when /^v/
104
+ "create vertex type #{type} "
105
+ when /^d/
106
+ "create document type #{type} "
107
+ when /^e/
108
+ "create edge type #{type} "
109
+ end.concat( args.map{|x,y| "#{x} #{y} "}.join)
110
+ end
111
+ db= Api.execute database, &exe
112
+ types( true ) # update cached schema
113
+ db
114
+
115
+ rescue HTTPX::HTTPError => e
116
+ # puts "ERROR: #{e.message.to_s}"
117
+ if e.status == 500 && e.message.to_s =~ /already exists/
118
+ Arcade::Database.logger.warn "Database type #{type} already present"
119
+ else
120
+ raise
121
+ end
122
+ end
123
+
124
+ alias create_class create_type
125
+
126
+ # ------------ drop type -----------
127
+ # delete any record prior to the attempt to drop a type.
128
+ # The `unsafe` option is nit implemented.
129
+ def drop_type type
130
+ Api.execute database, "drop type #{type} if exists"
131
+ end
132
+
133
+ # ------------ create -----------
134
+ # returns an rid of the successfully created vertex or document
135
+ #
136
+ # Parameter: name of the vertex or document type
137
+ # Hash of attributes
138
+ #
139
+ # Example: > DB.create :my_vertex, a: 14, name: "Hugo"
140
+ # => "#177:0"
141
+ #
142
+ def create type, **params
143
+ # uses API
144
+ Api.create_document database, type, **params
145
+ end
146
+
147
+ def insert **params
148
+
149
+ content_params = params.except( :type, :bucket, :index, :from, :return )
150
+ target_params = params.slice( :type, :bucket, :index )
151
+ if target_params.empty?
152
+ logger.error "Could not insert: target mising (type:, bucket:, index:)"
153
+ elsif content_params.empty?
154
+ logger.error "Nothing to Insert"
155
+ else
156
+ content = "CONTENT #{ content_params.to_json }"
157
+ target = target_params.map{|y,z| y==:type ? z : "#{y.to_s} #{ z } "}.join
158
+ Api.execute( database, "INSERT INTO #{target} #{content} ") &.first.allocate_model(false)
159
+ end
160
+ end
161
+
162
+ # ------------------------------ get ------------------------------------------------------ #
163
+ # Get fetches the record associated with the rid given as parameter.
164
+ #
165
+ # The rid is accepted as
166
+ # DB.get "#123:123", DB.get "123:123" or DB.get 123, 123
167
+ #
168
+ # Links are autoloaded (can be suppressed by the optional Block (false))
169
+ #
170
+ # puts DB.get( 19,0 )
171
+ # <my_document[#19:0]: emb : ["<my_alist[#33:0]: name : record 1, number : 1>", "<my_alist[#34:0]: name : record 2, number : 2>"]>
172
+ # puts DB.get( 19,0 ){ false }
173
+ # <my_document[#19:0]: emb : ["#33:0", "#34:0"]>
174
+ #
175
+ #
176
+ def get *rid
177
+ autocomplete = block_given? ? yield : true
178
+ rid = rid.join(':')
179
+ rid = rid[1..-1] if rid[0]=="#"
180
+ if rid.rid?
181
+ Api.query( database, "select from #{rid}" ).first &.allocate_model(autocomplete)
182
+ else
183
+ raise Arcade::QueryError "Get requires a rid input", caller
184
+ end
185
+ end
186
+
187
+ # ------------------------------ property ------------------------------------------------- #
188
+ # Adds properties to the type
189
+ #
190
+ # call via
191
+ # Api.property <database>, <type>, name1: a_format , name2: a_format
192
+ #
193
+ # Format is one of
194
+ # Boolean, Integer, Short, Long, Float, Double, String
195
+ # Datetime, Binary, Byte, Decimal, Link
196
+ # Embedded, EmbeddedList, EmbeddedMap
197
+ #
198
+ # In case of an Error, anything is rolled back and nil is returned
199
+ #
200
+ def self.property database, type, **args
201
+
202
+ begin_transaction database
203
+ success = args.map do | name, format |
204
+ r= execute(database) {" create property #{type.to_s}.#{name.to_s} #{format.to_s} " } &.first
205
+ if r.nil?
206
+ false
207
+ else
208
+ r.keys == [ :propertyName, :typeName, :operation ] && r[:operation] == 'create property'
209
+ end
210
+ end.uniq
211
+ if success == [true]
212
+ commit database
213
+ true
214
+ else
215
+ rollback database
216
+ end
217
+
218
+
219
+ end
220
+
221
+ # ------------------------------ index ------------------------------------------------- #
222
+ def self.index database, type, name , *properties
223
+ properties = properties.map( &:to_s )
224
+ unique_requested = "unique" if properties.delete("unique")
225
+ unique_requested = "notunique" if properties.delete("notunique" )
226
+ automatic = true if
227
+ properties << name if properties.empty?
228
+ end
229
+
230
+ def delete rid
231
+ r = Api.execute( database ){ "delete from #{rid}" }
232
+ success = r == [{ :count => 1 }]
233
+ end
234
+
235
+ # execute a command which modifies the database
236
+ #
237
+ # The operation is performed via Transaction/Commit
238
+ # If an Error occurs, its rolled back
239
+ #
240
+ def execute &block
241
+ s = Api.begin_transaction database
242
+ # begin
243
+ response = Api.execute database, nil, s, &block
244
+ # rescue HTTPX::HTTPError => e
245
+ # raise e.message
246
+ # puts e.methods
247
+ # puts e.status
248
+ # puts e.response
249
+ # puts e.message
250
+ # puts e.exception
251
+ # puts e.cause
252
+ # end
253
+ # puts response.inspect # debugging
254
+ r= if response.is_a? Hash
255
+ _allocate_model res
256
+ # elsif response.is_a? Array
257
+ # remove empty results
258
+ # response.delete_if{|y| y.empty?}
259
+ # response.map do | res |
260
+ # if res.key? :"@rid"
261
+ # allocate_model res
262
+ # else
263
+ # res
264
+ # end
265
+ # end
266
+ else
267
+ response
268
+ end
269
+ if Api.commit( database, s) == 204
270
+ r # return associated array of Arcade::Base-objects
271
+ else
272
+ []
273
+ end
274
+ rescue Dry::Struct::Error, HTTPX::HTTPError, Arcade::QueryError => e
275
+ Api.rollback database, s
276
+ logger.error "Execution FAILED --> Status #{e.status}"
277
+ # logger.error "Execution FAILED --> #{e.exception.message}"
278
+ [] # return empty result
279
+ end
280
+
281
+ # returns an array of results
282
+ #
283
+ # detects database-records and allocates them as model-objects
284
+ #
285
+ def query query_object
286
+ Api.query database, query_object.to_s
287
+ end
288
+
289
+ # returns an array of rid's (same logic as create)
290
+ def create_edge edge_class, from:, to:, **attributes
291
+
292
+ content = attributes.empty? ? "" : "CONTENT #{attributes.to_json}"
293
+ cr = ->( f, t ) do
294
+ begin
295
+ edges = Api.execute( database, "create edge #{edge_class} from #{f.rid} to #{t.rid} #{content}").allocate_model(false)
296
+ rescue HTTPX::HTTPError => e
297
+ # if e.status == 503
298
+ # puts e.status
299
+ # puts e.message
300
+ # puts e.message.class
301
+ # end
302
+ raise unless e.message =~ /Found duplicate key/
303
+ puts "#"+e.message.split("#").last[0..-3]
304
+ end
305
+ #else
306
+ # logger.error "Could not create Edge #{edge_class} from #{f} to #{t}"
307
+ ## logger.error edges.to_s
308
+ #end
309
+ end
310
+ from = [from] unless from.is_a? Array
311
+ to = [to] unless to.is_a? Array
312
+
313
+ from.map do | from_record |
314
+ to.map { | to_record | cr[ from_record, to_record ] if to_record.rid? } if from_record.rid?
315
+ end.flatten
316
+
317
+ end
318
+
319
+
320
+ # query all: select @rid, * from {database}
321
+
322
+ # not used
323
+ # def get_schema
324
+ # query( "select from schema:types" ).map do |a|
325
+ # puts "a: #{a}"
326
+ # class_name = a["name"]
327
+ # inherent_class = a["parentTypes"].empty? ? [Object,nil] : a["parentTypes"].map(&:camelcase_and_namespace)
328
+ # namespace, type_name = a["type"].camelcase_and_namespace
329
+ # namespace= Arcade if namespace.nil?
330
+ # klass= Dry::Core::ClassBuilder.new( name: type_name,
331
+ # parent: nil,
332
+ # namespace: namespace).call
333
+ # end
334
+ # rescue NameError
335
+ # logger.error "Dataset type #{e} not defined."
336
+ # raise
337
+ # end
338
+ # Postgres is not implemented
339
+ # connects to the database and initialises @connection
340
+ def connection
341
+ @connection
342
+ end
343
+
344
+ def connect environment=:development # environments: production devel test
345
+ if [:production, :development, :test].include? environment
346
+
347
+ # connect through the ruby postgres driver
348
+ # c= PG::Connection.new dbname: Config.database[environment],
349
+ # user: Config.username[environment],
350
+ # password: Config.password[environment],
351
+ # host: Config.pg[:host],
352
+ # port: Config.pg[:port]
353
+ #
354
+ end
355
+ rescue PG::ConnectionBad => e
356
+ if e.to_s =~ /Credentials/
357
+ logger.error "NOT CONNECTED ! Either Database is not present or credentials (#{ Config.username[environment]} / #{Config.password[environment]}) are wrong"
358
+ nil
359
+ else
360
+ raise
361
+ end
362
+ end # def
363
+
364
+
365
+
366
+ end # class
367
+ end # module
@@ -0,0 +1,71 @@
1
+ module Arcade
2
+
3
+ # Error handling
4
+ class Error < RuntimeError
5
+ end
6
+
7
+ class ArgumentError < ArgumentError
8
+ end
9
+
10
+ class SymbolError < ArgumentError
11
+ end
12
+
13
+ class LoadError < LoadError
14
+ end
15
+
16
+ class ImmutableError < RuntimeError
17
+ end
18
+ class IndexError < RuntimeError
19
+ end
20
+
21
+ class RollbackError < RuntimeError
22
+ end
23
+
24
+ class QueryError < RuntimeError
25
+ end
26
+
27
+ # used by Dry::Validation, not covered by "error"
28
+ class InvalidParamsError < StandardError
29
+ attr_reader :object
30
+ # @param [Hash] object that contains details about params errors.
31
+ # # @param [String] message of the error.
32
+ def initialize(object, message)
33
+ @object = object
34
+ super(message)
35
+ end
36
+ end
37
+
38
+ end # module Arcade
39
+
40
+ # Patching Object with universally accessible top level error method.
41
+ # The method is used throughout the lib instead of plainly raising exceptions.
42
+ # This allows lib user to easily inject user-specific error handling into the lib
43
+ # by just replacing Object#error method.
44
+ #def error message, type=:standard, backtrace=nil
45
+ # e = case type
46
+ # when :standard
47
+ # Arcade::Error.new message
48
+ # when :args
49
+ # Arcade::ArgumentError.new message
50
+ # when :symbol
51
+ # Arcade::SymbolError.new message
52
+ # when :load
53
+ # Arcade::LoadError.new message
54
+ # when :immutable
55
+ # Arcade::ImmutableError.new message
56
+ # when :commit
57
+ # Arcade::RollbackError.new message
58
+ # when :query
59
+ # Arcade::QueryError.new message
60
+ # when :args
61
+ # IB::ArgumentError.new message
62
+ # when :flex
63
+ # IB::FlexError.new message
64
+ # when :reader
65
+ # IB::TransmissionError.new message
66
+ # when :verify
67
+ # IB::VerifyError.new message
68
+ # end
69
+ # e.set_backtrace(backtrace) if backtrace
70
+ # raise e
71
+ #end
@@ -0,0 +1,38 @@
1
+ #require_relative 'default_formatter'
2
+ module Arcade
3
+ module Logging
4
+ def self.included(base)
5
+ base.extend ClassMethods
6
+ base.send :define_method, :logger do
7
+ base.logger
8
+ end
9
+ end
10
+
11
+ module ClassMethods
12
+ def logger
13
+ @logger
14
+ end
15
+
16
+ def logger=(logger)
17
+ @logger = logger
18
+ end
19
+
20
+ def configure_logger(log= STDOUT)
21
+ if log.is_a? Logger
22
+ @logger = log
23
+ else
24
+ @logger = Logger.new log
25
+ end
26
+ @logger.level = Logger::INFO
27
+ @logger.formatter = DefaultFormatter
28
+ end
29
+ end
30
+
31
+ class DefaultFormatter < Logger::Formatter
32
+ def self.call(severity, time, program_name, msg)
33
+ "#{time.strftime("%d.%m.(%X)")}#{"%5s" % severity}->#{msg}\n"
34
+ end
35
+ end
36
+ end
37
+ end
38
+ # source: https://github.com/jondot/sneakers/blob/master/lib/sneakers/concerns/logging.rb
@@ -0,0 +1,3 @@
1
+ module Arcade
2
+ VERSION = "0.4"
3
+ end
data/lib/arcade.rb ADDED
@@ -0,0 +1,36 @@
1
+ module Arcade
2
+
3
+ end
4
+
5
+ require "arcade/version"
6
+ require "dry/configurable"
7
+ require "dry/struct"
8
+ require "dry/core/class_builder"
9
+ require "dry/core/class_attributes"
10
+ require 'json'
11
+
12
+ module Types
13
+ include Dry.Types()
14
+ end
15
+ require 'yaml'
16
+ require 'securerandom'
17
+ require 'httpx'
18
+ require 'arcade/errors'
19
+ require_relative '../lib/support/object'
20
+ require_relative '../lib/support/string'
21
+ require_relative '../lib/support/class'
22
+ require_relative '../lib/support/sql'
23
+ require_relative '../lib/support/model'
24
+ require_relative '../lib/arcade/logging'
25
+ require_relative '../lib/config'
26
+ require_relative '../lib/support/conversions'
27
+ require_relative '../lib/arcade/api/primitives'
28
+ require_relative '../lib/arcade/api/operations'
29
+ require_relative '../lib/arcade/base'
30
+ require_relative '../lib/arcade/database'
31
+ require_relative '../lib/init'
32
+ require_relative "../lib/models"
33
+ require_relative '../lib/query'
34
+ require_relative '../lib/match'
35
+ require_relative '../lib/railtie' if defined? Rails::Railtie
36
+
data/lib/config.rb ADDED
@@ -0,0 +1,72 @@
1
+ module Arcade
2
+ class Config
3
+ extend Dry::Configurable
4
+ # central place to initialize constants
5
+ #
6
+ # ProjectRoot has to be a Pathname-Object
7
+ #
8
+ #puts "expand: #{File.expand_path(__dir__)}"
9
+ unless Arcade.const_defined?( :ProjectRoot )
10
+ Arcade::ProjectRoot = if defined?( Rails.env )
11
+ Rails.root
12
+ else
13
+ STDERR.puts "Using default (arcadedb gem) database credentials and settings"
14
+ # logger is not present at this stage
15
+ Pathname.new( File.expand_path( "../../", __FILE__ ))
16
+ end
17
+ else
18
+ STDERR.puts "Using provided database credentials and settings fron #{Arcade::ProjectRoot}"
19
+ end
20
+
21
+
22
+ # initialised a hash { environment => property }
23
+ setting :username, default: :user, reader: true,
24
+ constructor: ->(v) { yml(:environment).map{|x,y| [x , y[v.to_s]] }.to_h }
25
+ setting :password, default: :password, reader: true,
26
+ constructor: ->(v) { yml(:environment).map{|x,y| [x , y["pass"]] }.to_h }
27
+ setting :database, default: :database, reader: true,
28
+ constructor: ->(v) { yml(:environment).map{|x,y| [x , y["dbname"]] }.to_h }
29
+ setting(:base_uri, default: :host , reader: true,
30
+ constructor: ->(v) { "http://"+yml(:admin)[v]+':'+yml(:admin)[:port].to_s+"/api/v1/" })
31
+ setting :autoload, default: :autoload, reader: true , constructor: ->(v) { yml(v) }
32
+ setting :pg, default: :pg, reader: true , constructor: ->(v) { yml(v) }
33
+ setting :admin, default: :admin, reader: true , constructor: ->(v) { yml(v) }
34
+ setting :logger, default: :logger, reader: true ,
35
+ constructor: ->(v) do
36
+ if defined?( Rails.env )
37
+ Rails.logger
38
+ elsif Object.const_defined?(:Bridgetown)
39
+ else
40
+ output = yml(v)
41
+ if output.upcase == 'STDOUT'
42
+ Logger.new STDOUT
43
+ else
44
+ Logger.new File.open( output, 'a' )
45
+ end
46
+ end
47
+ end
48
+ setting :namespace, default: :namespace, reader: true , constructor: ->(v) { yml(v) }
49
+ setting :secret, reader: true, default: 12, constructor: ->(v) { seed(v) }
50
+ private
51
+ # if a config dir exists, use it.
52
+ # Standard: ProjectRoot/config.yml
53
+ def self.config_file
54
+
55
+ configdir = -> do
56
+ pr = ProjectRoot.is_a?(Pathname)? ProjectRoot : Pathname.new( ProjectRoot )
57
+ ( cd = pr + 'arcade.yml' ).exist? || ( cd = pr + 'config' + 'arcade.yml' ).exist? || ( cd = pr + 'config.yml' )
58
+ cd
59
+ end
60
+
61
+ @cd ||= configdir[]
62
+ end
63
+
64
+ def self.yml key=nil
65
+ y= YAML::load_file( config_file )
66
+ key.nil? ? y : y[key]
67
+ end
68
+ def self.seed( key= nil )
69
+ SecureRandom.hex( 40 )
70
+ end
71
+ end
72
+ end
data/lib/init.rb ADDED
@@ -0,0 +1,50 @@
1
+ module Arcade
2
+
3
+ # Arcade::Init.connect environment
4
+ # --------------------------------
5
+ # initializes the database connection
6
+ # and returns the active database handle
7
+ #
8
+ # The database cannot switched later
9
+ #
10
+ #
11
+ # Arcade::Init.db
12
+ # --------------
13
+ # returns an instance of the database handle
14
+ #
15
+ class Init
16
+ extend Dry::Core::ClassAttributes
17
+ defines :db # database handle
18
+
19
+ def self.connect e= :development
20
+
21
+ env = if e.to_s =~ /^p/
22
+ :production
23
+ elsif e.to_s =~ /^t/
24
+ :test
25
+ else
26
+ :development
27
+ end
28
+ # set the class attribute
29
+
30
+ db Database.new(env)
31
+ end
32
+ end
33
+
34
+ # Provides method `db` to every Model class
35
+ class Base
36
+ def self.db
37
+ Init.db
38
+ end
39
+ # expose db to instance methods as well
40
+ private define_method :db, &method(:db)
41
+ private_class_method :db
42
+ end
43
+ # Provides method `db` to every Query-Object
44
+ class Query
45
+ def db
46
+ Init.db
47
+ end
48
+ end
49
+
50
+ end