actn-db 0.0.1

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,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