actn-db 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,14 @@
1
+ require 'oj'
2
+
3
+ class ::String
4
+ def to_json
5
+ self
6
+ end
7
+ def as_json
8
+ Oj.load(self)
9
+ end
10
+ def to_domain
11
+ return self unless self.start_with?("http")
12
+ self.match(/[http|http]:\/\/(\w*:\d*|\w*)\/?/)[1] rescue nil
13
+ end
14
+ end
data/lib/actn/db.rb ADDED
@@ -0,0 +1,24 @@
1
+ require "actn/paths"
2
+ require "actn/db/version"
3
+ require "actn/db/pg"
4
+ require "actn/db/set"
5
+ require "actn/db/mod"
6
+ require "actn/db/model"
7
+
8
+ module Actn
9
+
10
+ module DB
11
+ extend PG
12
+ include Paths
13
+
14
+ def self.gem_root
15
+ @@gem_root ||= File.expand_path('../../../', __FILE__)
16
+ end
17
+
18
+ def self.paths
19
+ @@paths ||= [self.gem_root]
20
+ end
21
+
22
+ end
23
+
24
+ end
@@ -0,0 +1,175 @@
1
+ require 'actn/db/pg'
2
+ require 'actn/db/set'
3
+ require "active_model"
4
+
5
+ module Actn
6
+ module DB
7
+ class Mod
8
+
9
+ include PG
10
+
11
+ include ActiveModel::Model
12
+ extend ActiveModel::Callbacks
13
+ include ActiveModel::Validations
14
+ include ActiveModel::Validations::Callbacks
15
+ include ActiveModel::Serializers::JSON
16
+
17
+ define_model_callbacks :create
18
+ define_model_callbacks :update
19
+ define_model_callbacks :destroy
20
+
21
+ class << self
22
+
23
+ attr_accessor :schema, :table, :set
24
+
25
+ def set
26
+ @set ||= Set.new(schema, table)
27
+ end
28
+
29
+ def data_attr_accessor *fields
30
+ fields.each do |field|
31
+ class_eval %Q{
32
+ def #{field}; self.attributes['#{field}'] end
33
+ def #{field}=val; self.attributes['#{field}']=val end
34
+ }
35
+ end
36
+ end
37
+
38
+ def create data = {}
39
+ record = new(data)
40
+ record.save
41
+ record
42
+ end
43
+
44
+ [:validate_and_upsert, :upsert, :update, :delete, :delete_all].each do |meth|
45
+ class_eval <<-RUBY
46
+ def #{meth} *args
47
+ set.#{meth}(*args)
48
+ end
49
+ RUBY
50
+ end
51
+
52
+ [:find_by, :find].each do |meth|
53
+ class_eval <<-RUBY
54
+ def #{meth} conds
55
+ return nil if conds.empty?
56
+ new.from_json(set.#{meth}(conds)) rescue nil
57
+ end
58
+ RUBY
59
+ end
60
+
61
+ [:all, :where].each do |meth|
62
+ class_eval <<-RUBY
63
+ def #{meth} *args
64
+ (Oj.load(set.#{meth}(*args)) rescue {}).map{ |attrs| new(attrs) }
65
+ end
66
+ RUBY
67
+ end
68
+
69
+ def count conds = {}
70
+ set.count(conds).match(/(\d+)/)[1].to_i
71
+ end
72
+
73
+ end
74
+
75
+ self.schema = "public"
76
+
77
+ attr_accessor :attributes
78
+ data_attr_accessor :uuid, :locale, :created_at, :updated_at
79
+
80
+
81
+ def initialize data = {}
82
+ data ||= {}
83
+ register_attr_accessors data
84
+ super(data)
85
+ end
86
+
87
+ def attributes
88
+ @attributes ||= {}
89
+ end
90
+
91
+ def persisted?
92
+ self.uuid.present?
93
+ end
94
+
95
+ def uuid
96
+ self.attributes['uuid']
97
+ end
98
+
99
+ ##
100
+ # persistency
101
+
102
+ def save
103
+ create_or_update if valid?
104
+ end
105
+
106
+ def update attrs
107
+ register_attr_accessors attrs
108
+ self.attributes.update(attrs)
109
+ save
110
+ end
111
+
112
+ def destroy
113
+ return unless self.persisted?
114
+ run_callbacks :destroy do
115
+ if result = self.class.delete(identity)
116
+ process_upsert_response result
117
+ end
118
+ end
119
+ end
120
+
121
+
122
+ private
123
+
124
+ def create_or_update
125
+ self.persisted? ? _update : _insert
126
+ end
127
+
128
+ def _update
129
+ run_callbacks :update do
130
+ # if result = self.class.update(self.attributes,identity)
131
+ if result = self.class.validate_and_upsert(self.attributes.merge(identity))
132
+ process_upsert_response result
133
+ end
134
+ end
135
+ end
136
+
137
+ def _insert
138
+ run_callbacks :create do
139
+ if result = self.class.validate_and_upsert(self.attributes)
140
+ process_upsert_response result
141
+ end
142
+ end
143
+ end
144
+
145
+ def identity
146
+ {'uuid' => self.uuid}
147
+ end
148
+
149
+ ##
150
+ # allows dynamic attributes!
151
+
152
+ def register_attr_accessors data
153
+ data.deep_stringify_keys!
154
+ # ignore attributes declared with attr_accessor method
155
+ self.class.data_attr_accessor *data.keys.keep_if{ |key|
156
+ public_methods.keep_if{ |n| n.to_s.start_with? key }.empty?
157
+ }
158
+ end
159
+
160
+ private
161
+
162
+ def process_upsert_response result
163
+ if result =~ /uuid/
164
+ self.from_json(result)
165
+ elsif result =~ /error/
166
+ JsonSchemaError.new(result).errors.each do |field,err|
167
+ errors.add(field,err.keys.flatten.join(","))
168
+ end
169
+ end
170
+ self
171
+ end
172
+
173
+ end
174
+ end
175
+ end
@@ -0,0 +1,25 @@
1
+ require 'actn/db/mod'
2
+ require 'active_support/inflector'
3
+
4
+ module Actn
5
+ module DB
6
+ class Model < Mod
7
+
8
+ self.table = "models"
9
+ self.schema = "core"
10
+
11
+ data_attr_accessor :table_schema, :name, :indexes, :schema, :hooks
12
+
13
+ before_create :classify_name
14
+ before_update :classify_name
15
+
16
+ private
17
+
18
+ def classify_name
19
+ self.name = self.name.classify if self.name
20
+ end
21
+
22
+
23
+ end
24
+ end
25
+ end
data/lib/actn/db/pg.rb ADDED
@@ -0,0 +1,88 @@
1
+ require 'oj'
2
+ require 'uri'
3
+ require 'pg/em'
4
+ require 'pg/em/connection_pool'
5
+ require 'active_support/inflector'
6
+ require 'actn/core_ext/hash'
7
+ require 'actn/core_ext/string'
8
+
9
+ module Actn
10
+ module DB
11
+ module PG
12
+
13
+ class JsonSchemaError < StandardError
14
+ def initialize payload
15
+ # puts " PAYLOAD #{payload}"
16
+ @errors = Oj.load(payload)['errors']['validation']
17
+ end
18
+ def errors
19
+ @errors
20
+ end
21
+ end
22
+
23
+ Oj.default_options = { time_format: :ruby, mode: :compat }
24
+
25
+ def exec_func func_name, *params
26
+ sql = "SELECT __#{func_name}(#{ (params.length-1).times.inject("$1"){ |m,i| "#{m},$#{ i + 2 }"} })"
27
+ exec_prepared sql.parameterize.underscore, sql, params
28
+ end
29
+
30
+ def exec_prepared statement, sql, params = []
31
+
32
+ pg.prepare statement, sql rescue ::PG::DuplicatePstatement
33
+
34
+ begin
35
+ result = pg.exec_prepared(statement, params)
36
+ json = result.values.flatten.first
37
+ result.clear
38
+ json
39
+ rescue ::PG::InvalidSqlStatementName
40
+ exec_params sql, params
41
+ end
42
+
43
+ end
44
+
45
+ def exec_params sql, params = []
46
+ result = pg.exec_params(sql, params)
47
+ json = result.values.flatten.first
48
+ result.clear
49
+ json
50
+ end
51
+
52
+ ##
53
+ # :singleton method
54
+ # holds db connection
55
+
56
+ def pg
57
+ @@connection_pool ||= ::PG::EM::ConnectionPool.new(db_config)
58
+ end
59
+
60
+
61
+ ##
62
+ # :singleton method
63
+ # parses database url
64
+
65
+ def db_config
66
+ @@config ||= begin
67
+ db = URI.parse(ENV['DATABASE_URL'] || 'postgres://localhost')
68
+ config = {
69
+ dbname: db.path[1..-1],
70
+ host: db.host,
71
+ port: db.port,
72
+ size: ENV['DB_POOL_SIZE'] || 5,
73
+ async_autoreconnect: ENV['DB_ASYNC_AUTO_RECONNECT'] || true,
74
+ connect_timeout: ENV['DB_CONN_TIMEOUT'] || 60,
75
+ query_timeout: ENV['DB_QUERY_TIMEOUT'] || 30,
76
+ on_autoreconnect: proc { |pg| pg.exec "SELECT plv8_startup();" rescue nil },
77
+ on_connect: proc { |pg| pg.exec "SELECT plv8_startup();" rescue nil }
78
+ }
79
+ config[:user] = db.user if db.user
80
+ config[:password] = db.password if db.password
81
+ config
82
+ end
83
+ end
84
+
85
+ end
86
+
87
+ end
88
+ end
@@ -0,0 +1,67 @@
1
+ require 'actn/db/pg'
2
+
3
+ module Actn
4
+ module DB
5
+ class Set
6
+
7
+ include PG
8
+
9
+ def self.tables
10
+ @@tables ||= {}
11
+ end
12
+
13
+ def self.[]table
14
+ self.tables[table] ||= new(table)
15
+ end
16
+
17
+ attr_accessor :table, :schema
18
+
19
+ def initialize schema = :public, table
20
+ self.table = table
21
+ self.schema = schema
22
+ end
23
+
24
+
25
+ [:query,:upsert, :update, :delete].each do |meth|
26
+ class_eval <<-CODE
27
+ def #{meth} *args
28
+ exec_func :#{meth}, schema, table, *args.map(&:to_json)
29
+ end
30
+ CODE
31
+ end
32
+
33
+ def validate_and_upsert data
34
+ sql = "SELECT __upsert($1,$2,__validate($3,$4))"
35
+ exec_prepared sql.parameterize.underscore, sql, [schema, table, table.classify, data.to_json]
36
+ end
37
+
38
+
39
+ def count conds = {}
40
+ exec_func :query, schema, table, {select: 'COUNT(id)'}.merge(conds).to_json
41
+ end
42
+
43
+
44
+ def all
45
+ where({})
46
+ end
47
+
48
+ def where cond
49
+ query({where: cond})
50
+ end
51
+
52
+ def find_by cond
53
+ query({where: cond,limit: 1})[1..-2]
54
+ end
55
+
56
+ def find uuid
57
+ find_by(uuid: uuid)
58
+ end
59
+
60
+ def delete_all
61
+ delete({})
62
+ end
63
+
64
+
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,96 @@
1
+ require 'actn/db'
2
+
3
+ namespace :db do
4
+
5
+ desc "erases and rewinds all dbs"
6
+ task :reset do
7
+ Rake::Task["db:drop"].execute
8
+ Rake::Task["db:create"].execute
9
+ Rake::Task["db:migrate"].execute
10
+ end
11
+
12
+ desc "migrate your database"
13
+ task :migrate do
14
+
15
+ puts "Db Migrating... #{db_config[:dbname]}"
16
+ pg = PG::EM::Client.new(db_config)
17
+
18
+
19
+
20
+ Actn::DB.paths.uniq.each do |path|
21
+
22
+ puts path
23
+
24
+ pg.exec(File.read("#{path}/db/__setup.sql")) if File.exists?("#{path}/db/__setup.sql")
25
+
26
+ if File.exists?("#{path}/db/lib")
27
+ `coffee --compile --output #{path}/db/lib #{path}/db/lib` rescue nil
28
+
29
+ Dir.glob("#{path}/db/lib/*.js").each do |js|
30
+ name = File.basename(js,".js").split("_").last
31
+ sql = "INSERT INTO plv8_modules values ($1,true,$2)"
32
+ pg.exec_params(sql,[name,File.read(js)])
33
+ end
34
+ end
35
+
36
+ pg.exec(File.read("#{path}/db/__functions.sql")) if File.exists?("#{path}/db/__functions.sql")
37
+
38
+ if File.exists?("#{path}/db/")
39
+ Dir.glob("#{path}/db/*.sql").each do |sql|
40
+ unless File.basename(sql,".sql").start_with? "__"
41
+ puts sql
42
+ pg.exec(File.read(sql))
43
+ end
44
+ end
45
+ end
46
+
47
+ if File.exists?("#{path}/db/schemas")
48
+ Dir.glob("#{path}/db/schemas/*.json").each do |json|
49
+ name = File.basename(json,".json").capitalize
50
+ schema = {schema: Oj.load(File.read(json))}
51
+
52
+ sql = "UPDATE core.models SET data = __patch(data,$1,true) WHERE __string(data,'name'::text) = $2 RETURNING id;"
53
+ updated = pg.exec_params(sql,[ Oj.dump(schema), name ]).values.flatten(1).first.to_i
54
+
55
+ if updated == 0
56
+ sql = "INSERT INTO core.models (data) values (__patch(__defaults(),$1,true)) RETURNING id;"
57
+ inserted = pg.exec_params(sql, [Oj.dump(schema.merge(name: name, table_schema: "core"))]).values.flatten(1).first.to_i
58
+ end
59
+
60
+ puts "#{name} inserted:#{inserted} updated:#{updated}"
61
+ end
62
+
63
+ pg.exec "SELECT plv8_startup();"
64
+
65
+ end
66
+
67
+
68
+ end
69
+
70
+ end
71
+
72
+ desc 'Drops the database'
73
+ task :drop do
74
+ puts "Db Dropping... #{db_config[:dbname]}"
75
+ pg = PG::EM::Client.new(pg_config)
76
+ sql = "DROP DATABASE IF EXISTS \"%s\";" % [db_config[:dbname]]
77
+ pg.exec(sql).error_message
78
+ end
79
+
80
+ desc 'Creates the database'
81
+ task :create do
82
+ puts "Db Creating... #{db_config[:dbname]}"
83
+ pg = PG::EM::Client.new(pg_config)
84
+ sql = "CREATE DATABASE \"%s\" ENCODING = 'utf8';" % [db_config[:dbname]]
85
+ pg.exec(sql)
86
+ end
87
+
88
+ def db_config
89
+ @db_config ||= Actn::DB.db_config.dup.tap{|s| s.delete(:size) }
90
+ end
91
+
92
+ def pg_config
93
+ @pg_config ||= db_config.dup.merge({'dbname' => 'postgres'})
94
+ end
95
+
96
+ end