arcadedb 0.3.1 → 0.4

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.
@@ -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