actn-db 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.travis.yml +3 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +29 -0
- data/Rakefile +12 -0
- data/actn-db.gemspec +30 -0
- data/db/1_db.sql +54 -0
- data/db/__functions.sql +472 -0
- data/db/__setup.sql +37 -0
- data/db/lib/_0_actn.js +23 -0
- data/db/lib/_1_underscore.js +1 -0
- data/db/lib/_2_jjv.js +739 -0
- data/db/lib/_3_inflections.js +634 -0
- data/db/lib/_4_builder.coffee +136 -0
- data/db/lib/_4_builder.js +218 -0
- data/db/schemas/model.json +76 -0
- data/lib/actn/core_ext/hash.rb +10 -0
- data/lib/actn/core_ext/kernel.rb +8 -0
- data/lib/actn/core_ext/string.rb +14 -0
- data/lib/actn/db.rb +24 -0
- data/lib/actn/db/mod.rb +175 -0
- data/lib/actn/db/model.rb +25 -0
- data/lib/actn/db/pg.rb +88 -0
- data/lib/actn/db/set.rb +67 -0
- data/lib/actn/db/tasks/db.rake +96 -0
- data/lib/actn/db/version.rb +5 -0
- data/lib/actn/paths.rb +38 -0
- data/test/actn/test_mod.rb +54 -0
- data/test/actn/test_model.rb +49 -0
- data/test/actn/test_pg_funcs.rb +71 -0
- data/test/actn/test_set.rb +57 -0
- data/test/minitest_helper.rb +18 -0
- metadata +208 -0
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
|
data/lib/actn/db/mod.rb
ADDED
@@ -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
|
data/lib/actn/db/set.rb
ADDED
@@ -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
|