post_json 1.0.11 → 1.0.12
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 +4 -4
- data/README.md +87 -71
- data/lib/post_json/base.rb +67 -8
- data/lib/post_json/version.rb +1 -1
- data/spec/models/base_spec.rb +49 -1
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 52a70dfcec7f3478c9b2fab7e465c5b33c97229b
|
4
|
+
data.tar.gz: 7d302f9d087bc00360e203320e0afd5b64e3eb1d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b17682fa4780f53680fc7c7c55bb423974208d79356ba6ec70a9ab2ebc16ce9c83a7cc60b6101b71b507797434d84ef33cf9c818b40bbeed268c4b7045d69782
|
7
|
+
data.tar.gz: 99bbe826b5aab7659aaa74fa2e8413023b398940b4bf50aa98ed540989d339a2488c9e7300fb0af0c35cebcbcb4c37dc7d54d7180f41c5f4509e486ce476d4b2
|
data/README.md
CHANGED
@@ -1,41 +1,38 @@
|
|
1
1
|
# Welcome to PostJson
|
2
2
|
|
3
|
-
PostJson is everything you expect of ActiveRecord and PostgreSQL,
|
4
|
-
(free as a bird - no schemas).
|
3
|
+
PostJson is everything you expect of ActiveRecord and PostgreSQL, with the added power and dynamic nature of a document database (Free as a bird! No schemas!).
|
5
4
|
|
6
|
-
PostJson
|
7
|
-
PostJson, because we love document databases and PostgreSQL. PostJson combine features of Ruby, ActiveRecord and
|
8
|
-
PostgreSQL to provide a great document database.
|
5
|
+
PostJson combines features of Ruby, ActiveRecord and PostgreSQL to provide a great document database by taking advantage of PostgreSQL 9.2+ support for JavaScript (Google's V8 engine). We started the work on PostJson, because we love document databases **and** PostgreSQL.
|
9
6
|
|
10
|
-
See example of how we use PostJson as part of
|
7
|
+
See the example of how we use PostJson as part of [Jumpstarter](https://github.com/webnuts/jumpstarter).
|
11
8
|
|
12
9
|
|
13
|
-
##
|
10
|
+
## Installation
|
14
11
|
|
15
|
-
|
12
|
+
Add the gem to your `Gemfile`:
|
16
13
|
|
17
14
|
gem 'post_json'
|
18
|
-
|
19
|
-
### At the command prompt, install the gem, run the generator, and migrate the db:
|
20
15
|
|
21
|
-
|
22
|
-
|
23
|
-
|
16
|
+
Then:
|
17
|
+
|
18
|
+
$ bundle install
|
19
|
+
|
20
|
+
Run the generator and migrate the db:
|
21
|
+
|
22
|
+
$ rails g post_json:install
|
23
|
+
$ rake db:migrate
|
24
24
|
|
25
25
|
That's it!
|
26
26
|
|
27
27
|
(See POSTGRESQL_INSTALL.md if you need the install instructions for PostgreSQL with PLV8)
|
28
28
|
|
29
|
-
##
|
30
|
-
|
31
|
-
You should feel home right away, if you already know ActiveRecord. PostJson try hard to respect the ActiveRecord
|
32
|
-
API, so methods work and do as you would expect from ActiveRecord.
|
29
|
+
## Usage
|
33
30
|
|
34
|
-
PostJson
|
31
|
+
PostJson also tries hard to respect the ActiveRecord API, so, if you have experience with ActiveRecord, the model methods work as you would expect.
|
35
32
|
|
36
|
-
|
33
|
+
### Model
|
37
34
|
|
38
|
-
|
35
|
+
All PostJson models represent a collection.
|
39
36
|
|
40
37
|
```ruby
|
41
38
|
class Person < PostJson::Collection["people"]
|
@@ -43,12 +40,11 @@ end
|
|
43
40
|
|
44
41
|
me = Person.create(name: "Jacob")
|
45
42
|
```
|
43
|
+
|
44
|
+
__Notice you don't have to define model attributes anywhere!__
|
46
45
|
|
47
|
-
As you can see
|
48
|
-
|
49
|
-
|
50
|
-
`Person` can do the same as any model class inheriting `ActiveRecord::Base`.
|
51
|
-
|
46
|
+
As you can see, this is very similar to a standard ActiveRecord model. `PostJson::Collection["people"]` inherits from `PostJson::Base`, which, in turn, inherits from `ActiveRecord::Base`. This is part of the reason the `Person` model will seem so familiar.
|
47
|
+
|
52
48
|
You can also skip the creation of a class:
|
53
49
|
|
54
50
|
```ruby
|
@@ -56,7 +52,9 @@ people = PostJson::Collection["people"]
|
|
56
52
|
me = people.create(name: "Jacob")
|
57
53
|
```
|
58
54
|
|
59
|
-
###
|
55
|
+
### Validations
|
56
|
+
|
57
|
+
Use standard `ActiveRecord` validations in your models:
|
60
58
|
|
61
59
|
```ruby
|
62
60
|
class Person < PostJson::Collection["people"]
|
@@ -64,20 +62,13 @@ class Person < PostJson::Collection["people"]
|
|
64
62
|
end
|
65
63
|
```
|
66
64
|
|
67
|
-
PostJson::Collection["people"] returns a class, which is based on `PostJson::Base`, which is based on
|
68
|
-
`ActiveRecord::Base`. So its the exact same validation as you may know.
|
69
|
-
|
70
65
|
Read the <a href="http://guides.rubyonrails.org/active_record_validations.html" target="_blank">Rails guide about validation</a> if you need more information.
|
71
66
|
|
72
|
-
###
|
67
|
+
### Querying
|
73
68
|
|
74
69
|
```ruby
|
75
70
|
me = Person.create(name: "Jacob", details: {age: 33})
|
76
|
-
```
|
77
71
|
|
78
|
-
Now we can make a query and get the document:
|
79
|
-
|
80
|
-
```ruby
|
81
72
|
# PostJson supports filtering on nested attributes
|
82
73
|
also_me_1 = Person.where(details: {age: 33}).first
|
83
74
|
also_me_2 = Person.where("details.age" => 33).first
|
@@ -89,9 +80,7 @@ also_me_3 = Person.where("function(doc) { return doc.details.age == 33; }").firs
|
|
89
80
|
also_me_4 = Person.where("json_details.age = ?", 33).first
|
90
81
|
```
|
91
82
|
|
92
|
-
### Accessing attributes
|
93
|
-
|
94
|
-
Like you would expect with ActiveRecord:
|
83
|
+
### Accessing attributes
|
95
84
|
|
96
85
|
```ruby
|
97
86
|
person = Person.create(name: "Jacob")
|
@@ -115,22 +104,55 @@ puts person.name_changed? # => false
|
|
115
104
|
puts person.name_change # => nil
|
116
105
|
```
|
117
106
|
|
118
|
-
###
|
107
|
+
### Transformation with `select`
|
119
108
|
|
120
|
-
|
109
|
+
The `select` method allows you to transform a collection of documents into an array of hashes that contain only the attributes you want. The hash passed to `select` maps keys to selectors of arbitrary depth.
|
110
|
+
|
111
|
+
> In this example we only want the 'name' and 'age' attributes from the `Person` but 'age' is nested under 'details'.
|
121
112
|
|
122
113
|
```ruby
|
114
|
+
# create a person with age nested under details
|
123
115
|
me = Person.create(name: "Jacob", details: {age: 33})
|
124
116
|
|
117
|
+
# the dot (.) signifies that the selector is looking for a nested attribute
|
125
118
|
other_me = Person.limit(1).select({name: "name", age: "details.age"}).first
|
126
119
|
|
127
120
|
puts other_me
|
128
121
|
# => {name: "Jacob", age: 33}
|
122
|
+
```
|
123
|
+
|
124
|
+
### Dates
|
125
|
+
|
126
|
+
Dates are not natively supported by JSON. This is why dates are persisted as strings.
|
127
|
+
|
128
|
+
```ruby
|
129
|
+
me = Person.create(name: "Jacob", nested: {now: Time.now})
|
130
|
+
puts me.attributes
|
131
|
+
# => {"name"=>"Jacob", "nested"=>{"now"=>2013-10-24 16:15:05 +0200}, "id"=>"fb9ef4bb-1441-4392-a95d-6402f72829db", "version"=>1, "created_at"=>Thu, 24 Oct 2013 14:15:05 UTC +00:00, "updated_at"=>Thu, 24 Oct 2013 14:15:05 UTC +00:00}
|
132
|
+
```
|
133
|
+
|
134
|
+
Lets reload it and see how it is stored:
|
129
135
|
|
136
|
+
```ruby
|
137
|
+
me.reload
|
138
|
+
puts me.attributes
|
139
|
+
# => {"name"=>"Jacob", "nested"=>{"now"=>"2013-10-24T14:15:05.783Z"}, "id"=>"fb9ef4bb-1441-4392-a95d-6402f72829db", "version"=>1, "created_at"=>"2013-10-24T14:15:05.831Z", "updated_at"=>"2013-10-24T14:15:05.831Z"}
|
130
140
|
```
|
131
|
-
`select` takes a hash as argument and return an array of hashes. The value of each key/value pair in the hash argument is a selector. Selectors can point at attributes at root level, but also nested attributes. Each level of attributes is seperated with a dot (.).
|
132
141
|
|
133
|
-
|
142
|
+
PostJson will serialize Time and DateTime to format `strftime('%Y-%m-%dT%H:%M:%S.%LZ')` when persisting documents.
|
143
|
+
|
144
|
+
PostJson will also parse an attribute's value to a `Time` object, if the value is a string and matches the format.
|
145
|
+
|
146
|
+
### Supported methods
|
147
|
+
|
148
|
+
all, any?, blank?, count, delete, delete_all, destroy, destroy_all, each, empty?, except, exists?, find, find_by,
|
149
|
+
find_by!, find_each, find_in_batches, first, first!, first_or_create, first_or_initialize, ids, last, limit, load,
|
150
|
+
many?, offset, only, order, pluck, reorder, reverse_order, select, size, take, take!, to_a, to_sql, and where.
|
151
|
+
|
152
|
+
We also added `page(page, per_page)`, which translate into `offset((page-1)*per_page).limit(per_page)`.
|
153
|
+
|
154
|
+
|
155
|
+
## Configuration Options
|
134
156
|
|
135
157
|
```ruby
|
136
158
|
PostJson.setup "people" do |collection|
|
@@ -144,14 +166,7 @@ PostJson.setup "people" do |collection|
|
|
144
166
|
end
|
145
167
|
```
|
146
168
|
|
147
|
-
|
148
|
-
|
149
|
-
all, any?, blank?, count, delete, delete_all, destroy, destroy_all, each, empty?, except, exists?, find, find_by,
|
150
|
-
find_by!, find_each, find_in_batches, first, first!, first_or_create, first_or_initialize, ids, last, limit, load,
|
151
|
-
many?, offset, only, order, pluck, reorder, reverse_order, select, size, take, take!, to_a, to_sql, and where.
|
152
|
-
|
153
|
-
We also added `page(page, per_page)`, which translate into `offset((page-1)*per_page).limit(per_page)`.
|
154
|
-
|
169
|
+
For a Rails project this configuration could go in an initializer (`config/initializers/post_json.rb`).
|
155
170
|
|
156
171
|
## Performance
|
157
172
|
|
@@ -163,7 +178,7 @@ test_model = PostJson::Collection["test"]
|
|
163
178
|
content = test_model.last.content
|
164
179
|
|
165
180
|
result = test_model.where(content: content).count
|
166
|
-
# Rails debug
|
181
|
+
# Rails debug duration was 975.5ms
|
167
182
|
```
|
168
183
|
|
169
184
|
The duration was above 50ms as you can see.
|
@@ -174,7 +189,7 @@ Now lets see how the performance will be on the second and future queries using
|
|
174
189
|
|
175
190
|
```ruby
|
176
191
|
result = test_model.where(content: content).count
|
177
|
-
# Rails debug
|
192
|
+
# Rails debug duration was 1.5ms
|
178
193
|
```
|
179
194
|
|
180
195
|
It shows PostgreSQL as a document database combined with indexing has great performance out of the box.
|
@@ -183,45 +198,46 @@ See the next section about "Dynamic Indexes" for details.
|
|
183
198
|
|
184
199
|
## Dynamic Indexes
|
185
200
|
|
186
|
-
|
201
|
+
PostJson will measure the duration of each `SELECT` query and instruct PostgreSQL to create an index,
|
202
|
+
if the query duration is above a specified threshold. This feature is called `Dynamic Index`. Since most
|
203
|
+
applications perform the same queries over and over again we think you'll find this useful.
|
187
204
|
|
188
|
-
|
189
|
-
so queries speed up considerably.
|
205
|
+
Each collection (like `PostJson::Collection["people"]` above) has two index attributes:
|
190
206
|
|
191
|
-
|
192
|
-
|
207
|
+
* **use_dynamic_index** (default: true)
|
208
|
+
* **create_dynamic_index_milliseconds_threshold** (default: 50)
|
193
209
|
|
194
|
-
|
195
|
-
attribute `create_dynamic_index_milliseconds_threshold` (which is 50 by default).
|
210
|
+
### Example
|
196
211
|
|
197
|
-
|
212
|
+
```ruby
|
213
|
+
PostJson::Collection["people"].where(name: "Jacob").count
|
198
214
|
|
199
|
-
|
215
|
+
# => query duration > 50ms
|
216
|
+
```
|
200
217
|
|
201
|
-
PostJson will
|
202
|
-
you execute a query with `name` the performance will be much improved.
|
218
|
+
PostJson will check for an index on `name` and create it if it doesn't exist.
|
203
219
|
|
204
|
-
|
220
|
+
### Index configuration
|
205
221
|
|
206
222
|
```ruby
|
207
223
|
class Person < PostJson::Collection["people"]
|
208
224
|
self.create_dynamic_index_milliseconds_threshold = 75
|
209
225
|
end
|
226
|
+
```
|
210
227
|
|
211
|
-
|
228
|
+
or:
|
212
229
|
|
230
|
+
```ruby
|
213
231
|
PostJson::Collection["people"].create_dynamic_index_milliseconds_threshold = 75
|
214
|
-
|
215
|
-
# Now indexes are only created if queries are slower than 75 milliseconds.
|
216
232
|
```
|
217
233
|
|
218
|
-
|
234
|
+
### WARNING
|
219
235
|
|
220
|
-
Do not set
|
236
|
+
Do not set the dynamic index threshold too low as PostJson will try to create an index for every query. A threshold of 1 millisecond would be less than the duration of almost all queries.
|
221
237
|
|
222
238
|
## Primary Keys
|
223
239
|
|
224
|
-
PostJson
|
240
|
+
PostJson assigns UUID as primary key (id):
|
225
241
|
|
226
242
|
```ruby
|
227
243
|
me = Person.create(name: "Jacob")
|
@@ -230,13 +246,13 @@ puts me.id
|
|
230
246
|
# => "297a2500-a456-459b-b3e9-e876f59602c2"
|
231
247
|
```
|
232
248
|
|
233
|
-
|
249
|
+
or you can set it directly:
|
234
250
|
|
235
251
|
```ruby
|
236
252
|
john_doe = Person.create(id: "John Doe")
|
237
253
|
```
|
238
254
|
|
239
|
-
|
255
|
+
The primary key is downcased when doing a query or finding records:
|
240
256
|
|
241
257
|
```ruby
|
242
258
|
found = Person.where(id: "JOhN DoE").first
|
data/lib/post_json/base.rb
CHANGED
@@ -110,15 +110,15 @@ module PostJson
|
|
110
110
|
def __doc__body_convert_attribute_type(attribute_name, value)
|
111
111
|
case value
|
112
112
|
when /^[0-9]{4}-[0-1][0-9]-[0-3][0-9]T[0-2][0-9]:[0-5][0-9]:[0-5][0-9]\.[0-9]{3}Z$/
|
113
|
-
Time.parse(value)
|
113
|
+
Time.zone.parse(value)
|
114
114
|
when Hash
|
115
115
|
value.inject(HashWithIndifferentAccess.new) do |result, (key, value)|
|
116
|
-
result[key] =
|
116
|
+
result[key] = __doc__body_convert_attribute_type("#{attribute_name}.#{key}", value)
|
117
117
|
result
|
118
118
|
end
|
119
119
|
when Array
|
120
120
|
value.map.with_index do |array_value, index|
|
121
|
-
|
121
|
+
__doc__body_convert_attribute_type("#{attribute_name}[#{index}]", array_value)
|
122
122
|
end
|
123
123
|
else
|
124
124
|
value
|
@@ -173,11 +173,11 @@ module PostJson
|
|
173
173
|
method_name
|
174
174
|
end
|
175
175
|
|
176
|
-
if attribute_name.in?(attribute_names)
|
176
|
+
if attribute_name.in?(attribute_names) || self.class.column_names.include?(attribute_name) || super_respond_to?(attribute_name.to_sym)
|
177
|
+
super
|
178
|
+
else
|
177
179
|
self.class.define_attribute_accessor(attribute_name)
|
178
180
|
send(method_symbol, *args)
|
179
|
-
else
|
180
|
-
super
|
181
181
|
end
|
182
182
|
end
|
183
183
|
|
@@ -253,6 +253,54 @@ module PostJson
|
|
253
253
|
end
|
254
254
|
RUBY
|
255
255
|
end
|
256
|
+
|
257
|
+
def convert_attribute_value_before_save(primary_key, selector, value)
|
258
|
+
case value
|
259
|
+
when Time
|
260
|
+
value.in_time_zone
|
261
|
+
when DateTime
|
262
|
+
value.to_time.in_time_zone
|
263
|
+
else
|
264
|
+
value
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
def convert_document_hash_before_save(primary_key, document_hash, prefix = nil)
|
269
|
+
if document_hash
|
270
|
+
document_hash.inject(HashWithIndifferentAccess.new) do |result_hash, (key, value)|
|
271
|
+
selector = if prefix
|
272
|
+
"#{prefix}.#{key}"
|
273
|
+
else
|
274
|
+
key
|
275
|
+
end
|
276
|
+
case value
|
277
|
+
when Hash
|
278
|
+
result_hash[key] = convert_document_hash_before_save(primary_key, value, selector)
|
279
|
+
when Array
|
280
|
+
result_hash[key] = convert_document_array_before_save(primary_key, value, selector)
|
281
|
+
else
|
282
|
+
result_hash[key] = convert_attribute_value_before_save(primary_key, selector, value)
|
283
|
+
end
|
284
|
+
result_hash
|
285
|
+
end
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
def convert_document_array_before_save(primary_key, document_array, prefix = nil)
|
290
|
+
if document_array
|
291
|
+
document_array.map.with_index do |value, index|
|
292
|
+
selector = "#{prefix}[#{index}]"
|
293
|
+
case value
|
294
|
+
when Hash
|
295
|
+
convert_document_hash_before_save(primary_key, value, selector)
|
296
|
+
when Array
|
297
|
+
convert_document_array_before_save(primary_key, value, selector)
|
298
|
+
else
|
299
|
+
convert_attribute_value_before_save(primary_key, selector, value)
|
300
|
+
end
|
301
|
+
end
|
302
|
+
end
|
303
|
+
end
|
256
304
|
end
|
257
305
|
|
258
306
|
protected
|
@@ -280,10 +328,11 @@ module PostJson
|
|
280
328
|
end
|
281
329
|
|
282
330
|
if self.class.persisted_settings.use_timestamps
|
283
|
-
__local__current_time = Time.zone.now
|
331
|
+
__local__current_time = Time.zone.now
|
284
332
|
__doc__body_write_attribute(self.class.persisted_settings.created_at_attribute_name, __local__current_time)
|
285
333
|
__doc__body_write_attribute(self.class.persisted_settings.updated_at_attribute_name, __local__current_time)
|
286
334
|
end
|
335
|
+
|
287
336
|
super
|
288
337
|
end
|
289
338
|
|
@@ -302,10 +351,20 @@ module PostJson
|
|
302
351
|
end
|
303
352
|
|
304
353
|
if self.class.persisted_settings.use_timestamps && __doc__body_attribute_changed?(self.class.persisted_settings.updated_at_attribute_name)
|
305
|
-
__local__current_time = Time.zone.now
|
354
|
+
__local__current_time = Time.zone.now
|
306
355
|
__doc__body_write_attribute(self.class.persisted_settings.updated_at_attribute_name, __local__current_time)
|
307
356
|
end
|
308
357
|
super
|
309
358
|
end
|
359
|
+
|
360
|
+
def typecasted_attribute_value(name)
|
361
|
+
result = super
|
362
|
+
name = name.to_s
|
363
|
+
if name == '__doc__body'
|
364
|
+
self.class.convert_document_hash_before_save(self[self.primary_key], result)
|
365
|
+
else
|
366
|
+
result
|
367
|
+
end
|
368
|
+
end
|
310
369
|
end
|
311
370
|
end
|
data/lib/post_json/version.rb
CHANGED
data/spec/models/base_spec.rb
CHANGED
@@ -433,4 +433,52 @@ describe "Base model" do
|
|
433
433
|
it { subject.find("John").age.should == 25 }
|
434
434
|
end
|
435
435
|
end
|
436
|
-
|
436
|
+
|
437
|
+
context "dates" do
|
438
|
+
let(:time) { Time.now }
|
439
|
+
let(:time_in_zone) { time.in_time_zone }
|
440
|
+
let(:date_time) { time.to_datetime }
|
441
|
+
let(:time_result) { Time.parse(time_in_zone.strftime('%Y-%m-%dT%H:%M:%S.%LZ')) }
|
442
|
+
let(:time_hash) { { 'time' => time, 'time_in_zone' => time_in_zone, 'date_time' => date_time } }
|
443
|
+
let(:time_array) { [time, time_in_zone, date_time] }
|
444
|
+
let(:record) { PostJson::Collection["dates"].new(time: time, time_in_zone: time_in_zone, date_time: date_time, time_hash: time_hash, time_array: time_array) }
|
445
|
+
|
446
|
+
context "before save" do
|
447
|
+
subject { record }
|
448
|
+
|
449
|
+
its(:time) { should == time }
|
450
|
+
its(:time_in_zone) { should == time_in_zone }
|
451
|
+
its(:date_time) { should == date_time }
|
452
|
+
its(:time_hash) { should == time_hash }
|
453
|
+
its(:time_array) { should == time_array }
|
454
|
+
end
|
455
|
+
|
456
|
+
context "after save" do
|
457
|
+
subject { record }
|
458
|
+
|
459
|
+
before do
|
460
|
+
subject.save!
|
461
|
+
end
|
462
|
+
|
463
|
+
its(:time) { should == time }
|
464
|
+
its(:time_in_zone) { should == time_in_zone }
|
465
|
+
its(:date_time) { should == date_time }
|
466
|
+
its(:time_hash) { should == time_hash }
|
467
|
+
its(:time_array) { should == time_array }
|
468
|
+
|
469
|
+
context "and reload" do
|
470
|
+
|
471
|
+
|
472
|
+
before do
|
473
|
+
subject.reload
|
474
|
+
end
|
475
|
+
|
476
|
+
its(:time) { should == time_result }
|
477
|
+
its(:time_in_zone) { should == time_result }
|
478
|
+
its(:date_time) { should == time_result }
|
479
|
+
its(:time_hash) { should == { 'time' => time_result, 'time_in_zone' => time_result, 'date_time' => time_result } }
|
480
|
+
its(:time_array) { should == [time_result]*3 }
|
481
|
+
end
|
482
|
+
end
|
483
|
+
end
|
484
|
+
end
|