shoden 0.4.0 → 1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.gitignore +1 -0
- data/.travis.yml +7 -3
- data/Gemfile +5 -0
- data/Gemfile.lock +33 -0
- data/README.md +42 -4
- data/Rakefile +1 -1
- data/lib/shoden.rb +9 -300
- data/lib/shoden/model.rb +259 -0
- data/lib/shoden/proxy.rb +46 -0
- data/shoden.gemspec +6 -4
- data/test/shoden_test.rb +46 -32
- metadata +35 -17
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: d83a4046d2ee009631a461d017eb91587d878950b5bd2ccdd71caff7d1cbff18
|
4
|
+
data.tar.gz: 16d8fd4fd98795d99727440786ef56e7afc5a25b772753831c21dade14cb28c2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1790591d549b5d230962ad6b72d718857e536a040f32c7c2f6a7ea04db6ae52dbe42575904d0ea2210e09f23d6c9076114440f2ba888ba487e0a1ba570256f63
|
7
|
+
data.tar.gz: 21bd29c0a876cbce995a9a833e03c453c179db0bae6e51a209995196dbac156633fd91120618759b25016dfa2b8c63dca68a5c9b25b93792b66edb22bcfa8cfb
|
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
coverage
|
data/.travis.yml
CHANGED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
shoden (1.0)
|
5
|
+
pg (~> 1.1)
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: https://rubygems.org/
|
9
|
+
specs:
|
10
|
+
clap (1.0.0)
|
11
|
+
cutest (1.2.3)
|
12
|
+
clap
|
13
|
+
docile (1.3.1)
|
14
|
+
json (2.2.0)
|
15
|
+
pg (1.1.4)
|
16
|
+
rake (12.3.2)
|
17
|
+
simplecov (0.16.1)
|
18
|
+
docile (~> 1.1)
|
19
|
+
json (>= 1.8, < 3)
|
20
|
+
simplecov-html (~> 0.10.0)
|
21
|
+
simplecov-html (0.10.2)
|
22
|
+
|
23
|
+
PLATFORMS
|
24
|
+
ruby
|
25
|
+
|
26
|
+
DEPENDENCIES
|
27
|
+
cutest (~> 1.2)
|
28
|
+
rake
|
29
|
+
shoden!
|
30
|
+
simplecov (~> 0.16)
|
31
|
+
|
32
|
+
BUNDLED WITH
|
33
|
+
1.17.2
|
data/README.md
CHANGED
@@ -1,10 +1,9 @@
|
|
1
|
-
# Shôden - [![
|
1
|
+
# Shôden - [![Build Status](https://travis-ci.org/elcuervo/shoden.svg)](https://travis-ci.org/elcuervo/shoden)
|
2
2
|
|
3
|
-
![Elephant god](
|
3
|
+
![Elephant god](https://images.unsplash.com/photo-1523190301657-195ef118bb36?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1500&q=80)
|
4
4
|
|
5
5
|
Shôden is a persistance library on top of Postgres.
|
6
|
-
|
7
|
-
Postgres as a main database.
|
6
|
+
Uses JSONB as an storage abstraction so no need for migrations.
|
8
7
|
|
9
8
|
## Installation
|
10
9
|
|
@@ -12,6 +11,20 @@ Postgres as a main database.
|
|
12
11
|
gem install shoden
|
13
12
|
```
|
14
13
|
|
14
|
+
## Connect
|
15
|
+
|
16
|
+
Shoden connects by default using a `DATABASE_URL` env variable.
|
17
|
+
But you can change the connection string by calling `Shoden.url=`
|
18
|
+
|
19
|
+
## Setup
|
20
|
+
|
21
|
+
Shoden needs a setup method to create the proper tables.
|
22
|
+
You should do that after connecting
|
23
|
+
|
24
|
+
```ruby
|
25
|
+
Shoden.setup
|
26
|
+
```
|
27
|
+
|
15
28
|
## Models
|
16
29
|
|
17
30
|
```ruby
|
@@ -47,6 +60,17 @@ class Post < Shoden::Model
|
|
47
60
|
end
|
48
61
|
```
|
49
62
|
|
63
|
+
## Attributes
|
64
|
+
|
65
|
+
Shoden attributes offer you a way to type cast the values, or to perform changes
|
66
|
+
in the data itself.
|
67
|
+
|
68
|
+
```ruby
|
69
|
+
class Shout < Shoden::Model
|
70
|
+
attribute :what, ->(x) { x.uppcase }
|
71
|
+
end
|
72
|
+
```
|
73
|
+
|
50
74
|
## Indexing
|
51
75
|
|
52
76
|
```ruby
|
@@ -58,3 +82,17 @@ class User < Shoden::Model
|
|
58
82
|
unique :email
|
59
83
|
end
|
60
84
|
```
|
85
|
+
|
86
|
+
## Querying
|
87
|
+
|
88
|
+
You can query models or relations using the `filter` method.
|
89
|
+
|
90
|
+
```ruby
|
91
|
+
User.filter(email: "elcuervo@elcuervo.net")
|
92
|
+
User.first
|
93
|
+
User.last
|
94
|
+
User.count
|
95
|
+
```
|
96
|
+
|
97
|
+
You can go through the entire set using: `User.all` which will give you a
|
98
|
+
`Enumerator::Lazy`
|
data/Rakefile
CHANGED
data/lib/shoden.rb
CHANGED
@@ -1,7 +1,6 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
|
4
|
-
Sequel.extension :pg_hstore, :pg_hstore_ops
|
1
|
+
require "pg"
|
2
|
+
require "set"
|
3
|
+
require "shoden/model"
|
5
4
|
|
6
5
|
module Shoden
|
7
6
|
Error = Class.new(StandardError)
|
@@ -9,56 +8,12 @@ module Shoden
|
|
9
8
|
NotFound = Class.new(Error)
|
10
9
|
UniqueIndexViolation = Class.new(Error)
|
11
10
|
|
12
|
-
Proxy = Struct.new(:klass, :parent) do
|
13
|
-
def create(args = {})
|
14
|
-
klass.create(args.merge(key => parent.id))
|
15
|
-
end
|
16
|
-
|
17
|
-
def all
|
18
|
-
klass.filter(parent_filter)
|
19
|
-
end
|
20
|
-
|
21
|
-
def count
|
22
|
-
klass.count
|
23
|
-
end
|
24
|
-
|
25
|
-
def any?
|
26
|
-
count > 0
|
27
|
-
end
|
28
|
-
|
29
|
-
def first
|
30
|
-
filter = { order: "id ASC LIMIT 1" }.merge!(parent_filter)
|
31
|
-
klass.filter(filter).first
|
32
|
-
end
|
33
|
-
|
34
|
-
def last
|
35
|
-
filter = { order: "id DESC LIMIT 1" }.merge!(parent_filter)
|
36
|
-
klass.filter(filter).first
|
37
|
-
end
|
38
|
-
|
39
|
-
def [](id)
|
40
|
-
filter = { id: id }.merge!(parent_filter)
|
41
|
-
|
42
|
-
klass.filter(filter).first
|
43
|
-
end
|
44
|
-
|
45
|
-
private
|
46
|
-
|
47
|
-
def parent_filter
|
48
|
-
{ key => parent.id }
|
49
|
-
end
|
50
|
-
|
51
|
-
def key
|
52
|
-
"#{parent.class.to_reference}_id".freeze
|
53
|
-
end
|
54
|
-
end
|
55
|
-
|
56
11
|
def self.url=(url)
|
57
12
|
@_url = url
|
58
13
|
end
|
59
14
|
|
60
15
|
def self.url
|
61
|
-
@_url ||= ENV[
|
16
|
+
@_url ||= ENV["DATABASE_URL"]
|
62
17
|
end
|
63
18
|
|
64
19
|
def self.models
|
@@ -66,263 +21,17 @@ module Shoden
|
|
66
21
|
end
|
67
22
|
|
68
23
|
def self.connection
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
require 'logger'
|
73
|
-
loggers << Logger.new($stdout)
|
24
|
+
@_connection ||= begin
|
25
|
+
uri = URI.parse(url)
|
26
|
+
PG.connect(uri.hostname, uri.port, nil, nil, uri.path[1..-1], uri.user, uri.password)
|
74
27
|
end
|
75
|
-
|
76
|
-
@_connection ||= Sequel.connect(url, loggers: loggers)
|
77
28
|
end
|
78
29
|
|
79
30
|
def self.setup
|
80
|
-
|
81
|
-
models.each { |m| m.setup }
|
31
|
+
models.each(&:setup)
|
82
32
|
end
|
83
33
|
|
84
34
|
def self.destroy_tables
|
85
|
-
models.each
|
86
|
-
end
|
87
|
-
|
88
|
-
class Model
|
89
|
-
def initialize(attrs = {})
|
90
|
-
@_id = attrs.delete(:id) if attrs[:id]
|
91
|
-
@attributes = {}
|
92
|
-
update(attrs)
|
93
|
-
end
|
94
|
-
|
95
|
-
def id
|
96
|
-
return nil if !defined?(@_id)
|
97
|
-
@_id.to_i
|
98
|
-
end
|
99
|
-
|
100
|
-
def destroy
|
101
|
-
self.class.lookup(id).delete
|
102
|
-
end
|
103
|
-
|
104
|
-
def update(attrs = {})
|
105
|
-
attrs.each { |name, value| send(:"#{name}=", value) }
|
106
|
-
end
|
107
|
-
|
108
|
-
def update_attributes(attrs = {})
|
109
|
-
update(attrs)
|
110
|
-
save
|
111
|
-
end
|
112
|
-
|
113
|
-
def save
|
114
|
-
self.class.save(self)
|
115
|
-
self
|
116
|
-
end
|
117
|
-
|
118
|
-
def load!
|
119
|
-
ret = self.class.lookup(@_id)
|
120
|
-
return nil if ret.nil?
|
121
|
-
update(ret.to_a.first[:data])
|
122
|
-
self
|
123
|
-
end
|
124
|
-
|
125
|
-
def self.inherited(model)
|
126
|
-
Shoden.models.add(model)
|
127
|
-
end
|
128
|
-
|
129
|
-
def self.save(record)
|
130
|
-
if record.id
|
131
|
-
table.where(id: record.id).update(data: record.attributes)
|
132
|
-
else
|
133
|
-
begin
|
134
|
-
id = table.insert(data: record.attributes)
|
135
|
-
record.instance_variable_set(:@_id, id)
|
136
|
-
rescue Sequel::UniqueConstraintViolation
|
137
|
-
raise UniqueIndexViolation
|
138
|
-
end
|
139
|
-
end
|
140
|
-
end
|
141
|
-
|
142
|
-
def self.all
|
143
|
-
collect
|
144
|
-
end
|
145
|
-
|
146
|
-
def self.count
|
147
|
-
size = 0
|
148
|
-
Shoden.connection.fetch("SELECT COUNT(*) FROM \"#{table_name}\"") do |r|
|
149
|
-
size = r[:count]
|
150
|
-
end
|
151
|
-
|
152
|
-
size
|
153
|
-
end
|
154
|
-
|
155
|
-
def self.first
|
156
|
-
collect("ORDER BY id ASC LIMIT 1").first
|
157
|
-
end
|
158
|
-
|
159
|
-
def self.last
|
160
|
-
collect("ORDER BY id DESC LIMIT 1").first
|
161
|
-
end
|
162
|
-
|
163
|
-
def self.create(attrs = {})
|
164
|
-
new(attrs).save
|
165
|
-
end
|
166
|
-
|
167
|
-
def self.attributes
|
168
|
-
@attributes ||= []
|
169
|
-
end
|
170
|
-
|
171
|
-
def self.indices
|
172
|
-
@indices ||= []
|
173
|
-
end
|
174
|
-
|
175
|
-
def self.uniques
|
176
|
-
@uniques ||= []
|
177
|
-
end
|
178
|
-
|
179
|
-
def self.[](id)
|
180
|
-
new(id: id).load!
|
181
|
-
end
|
182
|
-
|
183
|
-
def self.index(name)
|
184
|
-
indices << name if !indices.include?(name)
|
185
|
-
end
|
186
|
-
|
187
|
-
def self.unique(name)
|
188
|
-
uniques << name if !uniques.include?(name)
|
189
|
-
end
|
190
|
-
|
191
|
-
def self.attribute(name, caster = ->(x) { x })
|
192
|
-
attributes << name if !attributes.include?(name)
|
193
|
-
|
194
|
-
define_method(name) { caster[@attributes[name]] }
|
195
|
-
define_method(:"#{name}=") { |value| @attributes[name] = value }
|
196
|
-
end
|
197
|
-
|
198
|
-
def self.collection(name, model)
|
199
|
-
define_method(name) do
|
200
|
-
klass = Kernel.const_get(model)
|
201
|
-
Proxy.new(klass, self)
|
202
|
-
end
|
203
|
-
end
|
204
|
-
|
205
|
-
def self.reference(name, model)
|
206
|
-
reader = :"#{name}_id"
|
207
|
-
writer = :"#{name}_id="
|
208
|
-
|
209
|
-
attributes << name if !attributes.include?(name)
|
210
|
-
|
211
|
-
define_method(reader) { @attributes[reader] }
|
212
|
-
define_method(writer) { |value| @attributes[reader] = value }
|
213
|
-
|
214
|
-
define_method(name) do
|
215
|
-
klass = Kernel.const_get("Shoden::#{model}")
|
216
|
-
klass[send(reader)]
|
217
|
-
end
|
218
|
-
end
|
219
|
-
|
220
|
-
def self.filter(conditions = {})
|
221
|
-
query = []
|
222
|
-
id = conditions.delete(:id)
|
223
|
-
order = conditions.delete(:order)
|
224
|
-
|
225
|
-
if id && !conditions.any?
|
226
|
-
rows = table.where(id: id)
|
227
|
-
else
|
228
|
-
conditions.each { |k,v| query << "data->'#{k}' = '#{v}'" }
|
229
|
-
seek_conditions = query.join(" AND ")
|
230
|
-
|
231
|
-
where = "WHERE (#{seek_conditions})"
|
232
|
-
|
233
|
-
where += " AND id = '#{id}'" if id
|
234
|
-
order_condition = "ORDER BY #{order}" if order
|
235
|
-
|
236
|
-
sql = "#{base_query} #{where} #{order_condition}"
|
237
|
-
|
238
|
-
rows = Shoden.connection.fetch(sql) || []
|
239
|
-
end
|
240
|
-
|
241
|
-
rows.lazy.map do |row|
|
242
|
-
attrs = row[:data].merge({ id: row[:id] })
|
243
|
-
|
244
|
-
new(attrs)
|
245
|
-
end
|
246
|
-
end
|
247
|
-
|
248
|
-
def attributes
|
249
|
-
sanitized = @attributes.map do |k, _|
|
250
|
-
val = send(k)
|
251
|
-
return if val.nil?
|
252
|
-
[k, val.to_s]
|
253
|
-
end.compact
|
254
|
-
|
255
|
-
Sequel::Postgres::HStore.new(sanitized)
|
256
|
-
end
|
257
|
-
|
258
|
-
private
|
259
|
-
|
260
|
-
def self.base_query
|
261
|
-
"SELECT * FROM \"#{table_name}\""
|
262
|
-
end
|
263
|
-
|
264
|
-
def self.collect(condition = '')
|
265
|
-
records = []
|
266
|
-
Shoden.connection.fetch("SELECT * FROM \"#{table_name}\" #{condition}") do |r|
|
267
|
-
attrs = r[:data].merge(id: r[:id])
|
268
|
-
records << new(attrs)
|
269
|
-
end
|
270
|
-
records
|
271
|
-
end
|
272
|
-
|
273
|
-
def self.table_name
|
274
|
-
:"Shoden::#{self.name}"
|
275
|
-
end
|
276
|
-
|
277
|
-
def self.to_reference
|
278
|
-
name.to_s.
|
279
|
-
match(/^(?:.*::)*(.*)$/)[1].
|
280
|
-
gsub(/([a-z\d])([A-Z])/, '\1_\2').
|
281
|
-
downcase.to_sym
|
282
|
-
end
|
283
|
-
|
284
|
-
def self.create_index(name, type = '')
|
285
|
-
conn.execute <<EOS
|
286
|
-
CREATE #{type.upcase} INDEX index_#{self.name}_#{name}
|
287
|
-
ON "#{table_name}" (( data -> '#{name}'))
|
288
|
-
WHERE ( data ? '#{name}' );
|
289
|
-
EOS
|
290
|
-
rescue
|
291
|
-
end
|
292
|
-
|
293
|
-
def self.lookup(id)
|
294
|
-
row = table.where(id: id)
|
295
|
-
return nil if !row.any?
|
296
|
-
|
297
|
-
row
|
298
|
-
end
|
299
|
-
|
300
|
-
def self.setup
|
301
|
-
conn.create_table? table_name do
|
302
|
-
primary_key :id
|
303
|
-
hstore :data
|
304
|
-
end
|
305
|
-
|
306
|
-
indices.each { |i| create_index(i) }
|
307
|
-
uniques.each { |i| create_index(i, :unique) }
|
308
|
-
end
|
309
|
-
|
310
|
-
def self.destroy_all
|
311
|
-
conn.execute("DELETE FROM \"#{table_name}\"")
|
312
|
-
rescue Sequel::DatabaseError
|
313
|
-
end
|
314
|
-
|
315
|
-
def self.destroy_table
|
316
|
-
conn.drop_table(table_name)
|
317
|
-
rescue Sequel::DatabaseError
|
318
|
-
end
|
319
|
-
|
320
|
-
def self.table
|
321
|
-
conn[table_name]
|
322
|
-
end
|
323
|
-
|
324
|
-
def self.conn
|
325
|
-
Shoden.connection
|
326
|
-
end
|
35
|
+
models.each(&:destroy_table)
|
327
36
|
end
|
328
37
|
end
|
data/lib/shoden/model.rb
ADDED
@@ -0,0 +1,259 @@
|
|
1
|
+
require "json"
|
2
|
+
require "shoden/proxy"
|
3
|
+
|
4
|
+
module Shoden
|
5
|
+
class Model
|
6
|
+
attr_reader :attributes
|
7
|
+
|
8
|
+
def initialize(attrs = {})
|
9
|
+
@_id = attrs.delete(:id) if attrs[:id]
|
10
|
+
@attributes = {}
|
11
|
+
update(attrs)
|
12
|
+
end
|
13
|
+
|
14
|
+
def id
|
15
|
+
return nil unless defined?(@_id)
|
16
|
+
@_id.to_i
|
17
|
+
end
|
18
|
+
|
19
|
+
def destroy
|
20
|
+
query = "DELETE FROM \"#{self.class.table_name}\" WHERE id = $1 RETURNING id"
|
21
|
+
ret = Shoden.connection.exec_params(query, [id])
|
22
|
+
ret.first["id"]
|
23
|
+
end
|
24
|
+
|
25
|
+
def update(attrs = {})
|
26
|
+
attrs.each { |name, value| send(:"#{name}=", value) }
|
27
|
+
end
|
28
|
+
|
29
|
+
def update_attributes(attrs = {})
|
30
|
+
update(attrs)
|
31
|
+
save
|
32
|
+
end
|
33
|
+
|
34
|
+
def save
|
35
|
+
self.class.save(self)
|
36
|
+
self
|
37
|
+
end
|
38
|
+
|
39
|
+
def load!
|
40
|
+
ret = self.class.lookup(@_id)
|
41
|
+
return nil if ret.nil?
|
42
|
+
data = self.class.from_json(ret.first["data"])
|
43
|
+
update(data)
|
44
|
+
|
45
|
+
self
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.inherited(model)
|
49
|
+
Shoden.models.add(model)
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.save(record)
|
53
|
+
if record.id
|
54
|
+
query = "UPDATE \"#{table_name}\" SET data = $1 WHERE id = $2"
|
55
|
+
Shoden.connection.exec_params(query, [record.attributes, record.id])
|
56
|
+
else
|
57
|
+
begin
|
58
|
+
query = "INSERT INTO \"#{table_name}\" (data) VALUES ($1) RETURNING id"
|
59
|
+
res = Shoden.connection.exec_params(query, [record.attributes.to_json])
|
60
|
+
record.instance_variable_set(:@_id, res.first["id"])
|
61
|
+
rescue PG::UniqueViolation
|
62
|
+
raise Shoden::UniqueIndexViolation
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.all
|
68
|
+
collect
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.count
|
72
|
+
query = "SELECT COUNT(*) FROM \"#{table_name}\""
|
73
|
+
Shoden.connection.exec(query).first["count"].to_i
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.first
|
77
|
+
collect(order: "id ASC", limit: 1).first
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.last
|
81
|
+
collect(order: "id DESC", limit: 1).first
|
82
|
+
end
|
83
|
+
|
84
|
+
def self.create(attrs = {})
|
85
|
+
new(attrs).save
|
86
|
+
end
|
87
|
+
|
88
|
+
def self.attributes
|
89
|
+
@attributes ||= []
|
90
|
+
end
|
91
|
+
|
92
|
+
def self.indices
|
93
|
+
@indices ||= []
|
94
|
+
end
|
95
|
+
|
96
|
+
def self.uniques
|
97
|
+
@uniques ||= []
|
98
|
+
end
|
99
|
+
|
100
|
+
def self.[](id)
|
101
|
+
new(id: id).load!
|
102
|
+
end
|
103
|
+
|
104
|
+
def self.index(name)
|
105
|
+
indices << name unless indices.include?(name)
|
106
|
+
end
|
107
|
+
|
108
|
+
def self.unique(name)
|
109
|
+
uniques << name unless uniques.include?(name)
|
110
|
+
end
|
111
|
+
|
112
|
+
def self.attribute(name, caster = ->(x) { x })
|
113
|
+
attributes << name unless attributes.include?(name)
|
114
|
+
|
115
|
+
define_method(name) { caster[@attributes[name]] }
|
116
|
+
define_method(:"#{name}=") { |value| @attributes[name] = value }
|
117
|
+
end
|
118
|
+
|
119
|
+
def self.collection(name, model)
|
120
|
+
define_method(name) do
|
121
|
+
klass = Kernel.const_get(model)
|
122
|
+
Shoden::Proxy.new(klass, self)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def self.reference(name, model)
|
127
|
+
reader = :"#{name}_id"
|
128
|
+
writer = :"#{name}_id="
|
129
|
+
|
130
|
+
attributes << name unless attributes.include?(name)
|
131
|
+
|
132
|
+
define_method(reader) { @attributes[reader] }
|
133
|
+
define_method(writer) { |value| @attributes[reader] = value }
|
134
|
+
|
135
|
+
define_method(name) do
|
136
|
+
klass = Kernel.const_get("Shoden::#{model}")
|
137
|
+
klass[send(reader)]
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def self.filter(conditions = {})
|
142
|
+
rows = query(conditions: conditions)
|
143
|
+
|
144
|
+
rows.lazy.map do |row|
|
145
|
+
data = from_json(row["data"])
|
146
|
+
data[:id] = row["id"].to_i
|
147
|
+
|
148
|
+
new(data)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def self.query(fields: "*", conditions: {})
|
153
|
+
id = conditions.delete(:id)
|
154
|
+
order = conditions.delete(:order)
|
155
|
+
|
156
|
+
if id && conditions.none?
|
157
|
+
sql = "#{base_query(fields)} WHERE id = $1"
|
158
|
+
Shoden.connection.exec_params(sql, [id]) || []
|
159
|
+
else
|
160
|
+
count = conditions.count
|
161
|
+
where = count.times.map { |i| "data->>$#{2 * i + 1} = $#{2 * i + 2}" }
|
162
|
+
params = conditions.flatten
|
163
|
+
|
164
|
+
if id
|
165
|
+
where << "id = $#{count * 2 + 1}"
|
166
|
+
params << id
|
167
|
+
end
|
168
|
+
|
169
|
+
where = where.join(" AND ")
|
170
|
+
order_condition = "ORDER BY #{order}" if order
|
171
|
+
sql = "#{base_query(fields)} WHERE #{where} #{order_condition}"
|
172
|
+
|
173
|
+
Shoden.connection.exec_params(sql, params) || []
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
private
|
178
|
+
|
179
|
+
def self.from_json(string)
|
180
|
+
JSON.parse(string, symbolize_names: true)
|
181
|
+
end
|
182
|
+
|
183
|
+
def self.base_query(fields = "*")
|
184
|
+
"SELECT #{fields} FROM \"#{table_name}\""
|
185
|
+
end
|
186
|
+
|
187
|
+
def self.collect(order: :id, limit: 0)
|
188
|
+
query = base_query("*")
|
189
|
+
|
190
|
+
params = [].tap do |item|
|
191
|
+
item << order
|
192
|
+
query << " ORDER BY $1"
|
193
|
+
|
194
|
+
if limit > 0
|
195
|
+
item << limit
|
196
|
+
query << " LIMIT $2 "
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
[].tap do |records|
|
201
|
+
Shoden.connection.exec_params(query, params).each do |row|
|
202
|
+
data = from_json(row["data"])
|
203
|
+
data[:id] = row["id"].to_i
|
204
|
+
|
205
|
+
records << new(data)
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
def self.table_name
|
211
|
+
:"Shoden::#{name}"
|
212
|
+
end
|
213
|
+
|
214
|
+
def self.to_reference
|
215
|
+
name.to_s
|
216
|
+
.match(/^(?:.*::)*(.*)$/)[1]
|
217
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
218
|
+
.downcase.to_sym
|
219
|
+
end
|
220
|
+
|
221
|
+
def self.create_index(name, type = "")
|
222
|
+
query = <<~EOS
|
223
|
+
CREATE #{type.upcase} INDEX index_#{self.name}_#{name}
|
224
|
+
ON "#{table_name}" (( data ->> '#{name}'))
|
225
|
+
WHERE ( data ? '#{name}' );
|
226
|
+
EOS
|
227
|
+
|
228
|
+
Shoden.connection.exec(query)
|
229
|
+
end
|
230
|
+
|
231
|
+
def self.lookup(id)
|
232
|
+
query = "SELECT * FROM \"#{table_name}\" WHERE id = $1"
|
233
|
+
row = Shoden.connection.exec_params(query, [id])
|
234
|
+
return nil if row.none?
|
235
|
+
|
236
|
+
row
|
237
|
+
end
|
238
|
+
|
239
|
+
def self.setup
|
240
|
+
Shoden.connection.exec <<~EOS
|
241
|
+
CREATE TABLE IF NOT EXISTS \"#{table_name}\" (
|
242
|
+
id SERIAL NOT NULL PRIMARY KEY,
|
243
|
+
data JSONB
|
244
|
+
)
|
245
|
+
EOS
|
246
|
+
|
247
|
+
indices.each { |i| create_index(i) }
|
248
|
+
uniques.each { |i| create_index(i, :unique) }
|
249
|
+
end
|
250
|
+
|
251
|
+
def self.destroy_all
|
252
|
+
Shoden.connection.exec("DELETE FROM \"#{table_name}\"")
|
253
|
+
end
|
254
|
+
|
255
|
+
def self.destroy_table
|
256
|
+
Shoden.connection.exec("DROP TABLE IF EXISTS \"#{table_name}\"")
|
257
|
+
end
|
258
|
+
end
|
259
|
+
end
|
data/lib/shoden/proxy.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
module Shoden
|
2
|
+
Proxy = Struct.new(:klass, :parent) do
|
3
|
+
def create(args = {})
|
4
|
+
klass.create(args.merge(key => parent.id))
|
5
|
+
end
|
6
|
+
|
7
|
+
def all
|
8
|
+
klass.filter(parent_filter)
|
9
|
+
end
|
10
|
+
|
11
|
+
def count
|
12
|
+
row = klass.query(fields: "COUNT(id)", conditions: parent_filter)
|
13
|
+
row.first["count"].to_i
|
14
|
+
end
|
15
|
+
|
16
|
+
def any?
|
17
|
+
count > 0
|
18
|
+
end
|
19
|
+
|
20
|
+
def first
|
21
|
+
filter = { order: "id ASC LIMIT 1" }.merge!(parent_filter)
|
22
|
+
klass.filter(filter).first
|
23
|
+
end
|
24
|
+
|
25
|
+
def last
|
26
|
+
filter = { order: "id DESC LIMIT 1" }.merge!(parent_filter)
|
27
|
+
klass.filter(filter).first
|
28
|
+
end
|
29
|
+
|
30
|
+
def [](id)
|
31
|
+
filter = { id: id }.merge!(parent_filter)
|
32
|
+
|
33
|
+
klass.filter(filter).first
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def parent_filter
|
39
|
+
{ key => parent.id }
|
40
|
+
end
|
41
|
+
|
42
|
+
def key
|
43
|
+
"#{parent.class.to_reference}_id".freeze
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
data/shoden.gemspec
CHANGED
@@ -1,15 +1,17 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
s.name = "shoden"
|
3
|
-
s.version = "
|
3
|
+
s.version = "1.0"
|
4
4
|
s.summary = "Object hash mapper for postgres"
|
5
5
|
s.description = "Slim postgres models"
|
6
6
|
s.authors = ["elcuervo"]
|
7
|
-
s.licenses = [
|
7
|
+
s.licenses = %w[MIT HUGWARE]
|
8
8
|
s.email = ["yo@brunoaguirre.com"]
|
9
9
|
s.homepage = "http://github.com/elcuervo/shoden"
|
10
10
|
s.files = `git ls-files`.split("\n")
|
11
11
|
s.test_files = `git ls-files test`.split("\n")
|
12
12
|
|
13
|
-
s.add_dependency("
|
14
|
-
|
13
|
+
s.add_dependency("pg", "~> 1.1")
|
14
|
+
|
15
|
+
s.add_development_dependency("cutest", "~> 1.2")
|
16
|
+
s.add_development_dependency("simplecov", "~> 0.16")
|
15
17
|
end
|
data/test/shoden_test.rb
CHANGED
@@ -1,5 +1,9 @@
|
|
1
|
-
require
|
2
|
-
require
|
1
|
+
require "cutest"
|
2
|
+
require "simplecov"
|
3
|
+
|
4
|
+
SimpleCov.start
|
5
|
+
|
6
|
+
require "shoden"
|
3
7
|
|
4
8
|
Model = Class.new(Shoden::Model)
|
5
9
|
|
@@ -12,8 +16,8 @@ class Person < Shoden::Model
|
|
12
16
|
end
|
13
17
|
|
14
18
|
class Tree < Shoden::Model
|
15
|
-
attribute
|
16
|
-
collection
|
19
|
+
attribute :name
|
20
|
+
collection :sprouts, :Sprout
|
17
21
|
end
|
18
22
|
|
19
23
|
class Sprout < Shoden::Model
|
@@ -34,61 +38,70 @@ setup do
|
|
34
38
|
Shoden.setup
|
35
39
|
end
|
36
40
|
|
37
|
-
test
|
41
|
+
test "url setup" do
|
42
|
+
Shoden.url = ENV["DATABASE_URL"]
|
43
|
+
end
|
44
|
+
|
45
|
+
test "model" do
|
38
46
|
model = Model.create
|
39
|
-
|
47
|
+
|
48
|
+
assert Model.count
|
49
|
+
assert_equal model.id.class, Integer
|
40
50
|
end
|
41
51
|
|
42
|
-
test
|
43
|
-
user = User.create name:
|
44
|
-
assert_equal user.name,
|
52
|
+
test "attributes" do
|
53
|
+
user = User.create name: "Michel"
|
54
|
+
assert_equal user.name, "Michel"
|
45
55
|
end
|
46
56
|
|
47
|
-
test
|
48
|
-
user = User.create name:
|
57
|
+
test "update" do
|
58
|
+
user = User.create name: "Cyril"
|
49
59
|
id = user.id
|
50
60
|
|
51
|
-
assert_equal user.name,
|
61
|
+
assert_equal user.name, "Cyril"
|
52
62
|
|
53
|
-
user.name =
|
63
|
+
user.name = "cyx"
|
54
64
|
user.save
|
55
65
|
|
56
|
-
assert_equal user.name,
|
66
|
+
assert_equal user.name, "cyx"
|
57
67
|
assert_equal user.id, id
|
58
68
|
|
59
|
-
user.update_attributes(name:
|
60
|
-
assert_equal user.name,
|
69
|
+
user.update_attributes(name: "Cyril")
|
70
|
+
assert_equal user.name, "Cyril"
|
61
71
|
end
|
62
72
|
|
63
|
-
test
|
64
|
-
person = { email:
|
73
|
+
test "filtering" do
|
74
|
+
person = { email: "edgar@poe.com" }
|
65
75
|
Person.create(person)
|
66
76
|
p = Person.filter(person).first
|
67
77
|
|
68
|
-
assert p.email ==
|
78
|
+
assert p.email == "edgar@poe.com"
|
69
79
|
end
|
70
80
|
|
71
|
-
test
|
72
|
-
tree = Tree.create(name:
|
81
|
+
test "relations" do
|
82
|
+
tree = Tree.create(name: "asd")
|
83
|
+
tree2 = Tree.create(name: "qwe")
|
73
84
|
|
74
85
|
assert tree.id
|
75
|
-
assert_equal tree.name,
|
86
|
+
assert_equal tree.name, "asd"
|
76
87
|
|
77
88
|
sprout = tree.sprouts.create(leaves: 4)
|
89
|
+
tree2.sprouts.create(leaves: 5)
|
78
90
|
|
79
91
|
assert sprout.is_a?(Sprout)
|
80
92
|
assert tree.sprouts.all.each.is_a?(Enumerator)
|
81
93
|
|
82
94
|
assert tree.sprouts[sprout.id].id == sprout.id
|
83
95
|
|
96
|
+
assert tree.sprouts.any?
|
84
97
|
assert_equal tree.sprouts.count, 1
|
85
98
|
assert_equal sprout.tree.id, tree.id
|
86
|
-
assert tree.sprouts.first
|
87
|
-
assert tree.sprouts.last
|
99
|
+
assert !tree.sprouts.first.nil?
|
100
|
+
assert !tree.sprouts.last.nil?
|
88
101
|
end
|
89
102
|
|
90
|
-
test
|
91
|
-
user = User.create(name:
|
103
|
+
test "deletion" do
|
104
|
+
user = User.create(name: "Damian")
|
92
105
|
id = user.id
|
93
106
|
|
94
107
|
user.destroy
|
@@ -96,27 +109,28 @@ test 'deletion' do
|
|
96
109
|
assert_equal User[id], nil
|
97
110
|
end
|
98
111
|
|
99
|
-
test
|
112
|
+
test "casting" do
|
100
113
|
a = A.create(n: 1)
|
101
114
|
a_prime = A[a.id]
|
102
115
|
|
103
116
|
assert_equal a_prime.n, 1
|
104
117
|
end
|
105
118
|
|
106
|
-
test
|
107
|
-
person = Person.create(email:
|
119
|
+
test "indices" do
|
120
|
+
person = Person.create(email: "edgar@poe.com", origin: "The internerd")
|
108
121
|
|
109
122
|
assert person.id
|
110
123
|
|
111
124
|
assert_raise Shoden::UniqueIndexViolation do
|
112
|
-
Person.create(email:
|
125
|
+
Person.create(email: "edgar@poe.com", origin: "Montevideo City")
|
113
126
|
end
|
114
127
|
end
|
115
128
|
|
116
|
-
test
|
129
|
+
test "basic querying" do
|
117
130
|
User.destroy_all
|
118
131
|
5.times { User.create }
|
119
132
|
|
133
|
+
assert User.query(conditions: { id: 1 })
|
134
|
+
assert User.first != User.last
|
120
135
|
assert_equal User.all.size, 5
|
121
|
-
|
122
136
|
end
|
metadata
CHANGED
@@ -1,43 +1,57 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: shoden
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: '1.0'
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- elcuervo
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2019-09-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
name:
|
14
|
+
name: pg
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- - ~>
|
17
|
+
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version:
|
19
|
+
version: '1.1'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- - ~>
|
24
|
+
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version:
|
26
|
+
version: '1.1'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: cutest
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- - ~>
|
31
|
+
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: 1.2
|
33
|
+
version: '1.2'
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- - ~>
|
38
|
+
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: 1.2
|
40
|
+
version: '1.2'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: simplecov
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0.16'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0.16'
|
41
55
|
description: Slim postgres models
|
42
56
|
email:
|
43
57
|
- yo@brunoaguirre.com
|
@@ -45,13 +59,18 @@ executables: []
|
|
45
59
|
extensions: []
|
46
60
|
extra_rdoc_files: []
|
47
61
|
files:
|
48
|
-
- .gems
|
49
|
-
- .
|
62
|
+
- ".gems"
|
63
|
+
- ".gitignore"
|
64
|
+
- ".travis.yml"
|
65
|
+
- Gemfile
|
66
|
+
- Gemfile.lock
|
50
67
|
- HUGS
|
51
68
|
- LICENSE
|
52
69
|
- README.md
|
53
70
|
- Rakefile
|
54
71
|
- lib/shoden.rb
|
72
|
+
- lib/shoden/model.rb
|
73
|
+
- lib/shoden/proxy.rb
|
55
74
|
- shoden.gemspec
|
56
75
|
- test/shoden_test.rb
|
57
76
|
homepage: http://github.com/elcuervo/shoden
|
@@ -65,17 +84,16 @@ require_paths:
|
|
65
84
|
- lib
|
66
85
|
required_ruby_version: !ruby/object:Gem::Requirement
|
67
86
|
requirements:
|
68
|
-
- -
|
87
|
+
- - ">="
|
69
88
|
- !ruby/object:Gem::Version
|
70
89
|
version: '0'
|
71
90
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
72
91
|
requirements:
|
73
|
-
- -
|
92
|
+
- - ">="
|
74
93
|
- !ruby/object:Gem::Version
|
75
94
|
version: '0'
|
76
95
|
requirements: []
|
77
|
-
|
78
|
-
rubygems_version: 2.0.14
|
96
|
+
rubygems_version: 3.0.3
|
79
97
|
signing_key:
|
80
98
|
specification_version: 4
|
81
99
|
summary: Object hash mapper for postgres
|