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