json_api_model 0.4.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/README.md +52 -0
- data/lib/json_api_model/associatable.rb +6 -3
- data/lib/json_api_model/associations.rb +2 -0
- data/lib/json_api_model/associations/base.rb +25 -27
- data/lib/json_api_model/associations/belongs_to.rb +6 -2
- data/lib/json_api_model/associations/has.rb +25 -13
- data/lib/json_api_model/associations/preloader.rb +59 -0
- data/lib/json_api_model/associations/preloaders.rb +32 -0
- data/lib/json_api_model/associations/preloaders/base.rb +46 -0
- data/lib/json_api_model/associations/preloaders/belongs_to.rb +23 -0
- data/lib/json_api_model/associations/preloaders/has.rb +26 -0
- data/lib/json_api_model/model.rb +1 -1
- data/lib/json_api_model/result_set.rb +4 -0
- data/lib/json_api_model/version.rb +1 -1
- metadata +7 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 61778944ededa50685c1d990d6f6a66c78254d4f
|
4
|
+
data.tar.gz: bd95c987db432e21b75457dddd973e3bcb3394eb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f98a1388dee293f467b2af6b75134e8523ffc1c785a874e9a369b0b01a18139b75a20b9c6f804ab34bc07342f0bc0df2761e3234efa467d8a459e8c21202beff
|
7
|
+
data.tar.gz: 9d78a9616a2c80e2b67ae51f47951d641e690e7cc412bdad82377e624b30efdf2a3dd581a03e8c019e8e3b0bd296350bea11cc1d1479dc70c8cbd0ee3b57c7fb
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -184,6 +184,58 @@ class MyModelInRussianConroller < ApplicationController
|
|
184
184
|
end
|
185
185
|
```
|
186
186
|
|
187
|
+
### Preloading
|
188
|
+
|
189
|
+
If you have a model that you need to bulk load along with its associations, in `ActiveRecord`, you can do `MyModel.where( args ).preload( :association, other_association: :nested association )`. Well, you can do that here as well.
|
190
|
+
|
191
|
+
__NOTE__(1): consider the complexity, especially for remote models
|
192
|
+
|
193
|
+
__NOTE__(2): for remote models, the preloader does not currently autopage
|
194
|
+
|
195
|
+
#### Example
|
196
|
+
|
197
|
+
Let's say you have a mixed environemnt of local and remote models, like so:
|
198
|
+
|
199
|
+
```ruby
|
200
|
+
module Example
|
201
|
+
class User < JsonApiModel::Model
|
202
|
+
wraps Example::Client::User
|
203
|
+
|
204
|
+
# remote model
|
205
|
+
belongs_to :org, class_name: "Example::Org"
|
206
|
+
|
207
|
+
# local model
|
208
|
+
has_one :profile
|
209
|
+
end
|
210
|
+
|
211
|
+
class Org < JsonApiModel::Model
|
212
|
+
wraps Example::Client::Org
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
class Profile < ActiveRecord::Base
|
217
|
+
belongs_to :user
|
218
|
+
end
|
219
|
+
```
|
220
|
+
|
221
|
+
You can bulk prefetch associations for a block of `User`s
|
222
|
+
|
223
|
+
```ruby
|
224
|
+
Example::User.where( name: [ "Greg", "Mike" ] ).preload( :org, :profile )
|
225
|
+
```
|
226
|
+
|
227
|
+
Will make 3 calls:
|
228
|
+
|
229
|
+
* `User` to fetch `.where( name: [ "Greg", "Mike" ])`
|
230
|
+
* `Org` to fetch the orgs associated with those users, as returned in their relationship blocks
|
231
|
+
* `Profile` to fetch `where( user_id: users.map( &:id ) )`
|
232
|
+
|
233
|
+
Or if you already have your `users` loaded, you can call
|
234
|
+
|
235
|
+
```ruby
|
236
|
+
JsonApiModel::Associations::Preloader.preload( users, :org, :profile )
|
237
|
+
```
|
238
|
+
and that'll do the same thing, minus the `User` call
|
187
239
|
|
188
240
|
### Configuration
|
189
241
|
|
@@ -57,12 +57,15 @@ module JsonApiModel
|
|
57
57
|
self.__cached_associations ||= {}
|
58
58
|
|
59
59
|
unless self.__cached_associations.has_key? association.name
|
60
|
-
|
61
|
-
|
62
|
-
self.__cached_associations[association.name] = result
|
60
|
+
self.send( "#{association.name}=", association.fetch( self ) )
|
63
61
|
end
|
64
62
|
self.__cached_associations[association.name]
|
65
63
|
end
|
64
|
+
|
65
|
+
define_method "#{association.name}=" do | value |
|
66
|
+
self.__cached_associations ||= {}
|
67
|
+
self.__cached_associations[association.name] = value
|
68
|
+
end
|
66
69
|
end
|
67
70
|
end
|
68
71
|
end
|
@@ -6,5 +6,7 @@ module JsonApiModel
|
|
6
6
|
autoload :Has, 'json_api_model/associations/has'
|
7
7
|
autoload :HasMany, 'json_api_model/associations/has_many'
|
8
8
|
autoload :HasOne, 'json_api_model/associations/has_one'
|
9
|
+
autoload :Preloader, 'json_api_model/associations/preloader'
|
10
|
+
autoload :Preloaders, 'json_api_model/associations/preloaders'
|
9
11
|
end
|
10
12
|
end
|
@@ -2,41 +2,46 @@ module JsonApiModel
|
|
2
2
|
module Associations
|
3
3
|
class Base
|
4
4
|
|
5
|
-
attr_accessor :name, :opts, :key
|
5
|
+
attr_accessor :name, :opts, :key, :base_class
|
6
|
+
delegate :preload, to: :preloader
|
6
7
|
|
7
8
|
def initialize( base_class, name, opts = {} )
|
8
|
-
self.name
|
9
|
-
self.opts
|
10
|
-
self.key
|
9
|
+
self.name = name
|
10
|
+
self.opts = opts
|
11
|
+
self.key = idify( base_class )
|
12
|
+
self.base_class = base_class
|
11
13
|
|
12
|
-
|
14
|
+
validate_opts!
|
13
15
|
end
|
14
16
|
|
15
17
|
def fetch( instance )
|
16
|
-
process
|
18
|
+
process association_class.send( action, query( instance ) )
|
17
19
|
end
|
18
20
|
|
19
|
-
|
20
|
-
|
21
|
-
def klass
|
22
|
-
@klass ||= association_class( name, opts )
|
21
|
+
def json_relationship?
|
22
|
+
association_class < JsonApiModel::Model
|
23
23
|
end
|
24
24
|
|
25
|
-
def association_class
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
a_class || a_class_name || derived_class_for( name )
|
25
|
+
def association_class
|
26
|
+
@associated_class ||= opts[:class] ||
|
27
|
+
opts[:class_name]&.constantize ||
|
28
|
+
derived_class
|
30
29
|
end
|
31
30
|
|
32
|
-
def
|
33
|
-
|
31
|
+
def process( results )
|
32
|
+
results
|
34
33
|
end
|
35
34
|
|
35
|
+
protected
|
36
|
+
|
36
37
|
def idify( class_name )
|
37
38
|
"#{class_name.to_s.demodulize.underscore}_id"
|
38
39
|
end
|
39
40
|
|
41
|
+
def derived_class
|
42
|
+
name.to_s.singularize.classify.constantize
|
43
|
+
end
|
44
|
+
|
40
45
|
def supported_options
|
41
46
|
[ :class, :class_name ] + additional_options
|
42
47
|
end
|
@@ -45,21 +50,14 @@ module JsonApiModel
|
|
45
50
|
[]
|
46
51
|
end
|
47
52
|
|
48
|
-
def
|
53
|
+
def validate_opts!
|
49
54
|
if name.to_s == "object"
|
50
55
|
raise "#{base_class}: 'object_id' is a reserved keyword in ruby and cannot be overridden"
|
51
56
|
end
|
52
|
-
|
53
|
-
|
54
|
-
list = invalid_options.map{|o|"'#{o}'"}.to_sentence
|
55
|
-
plural = invalid_options.count > 1
|
56
|
-
raise "#{base_class}: #{list} #{plural ? "are" : "is"} not supported."
|
57
|
+
(opts.keys - supported_options).each do | opt |
|
58
|
+
raise "#{base_class}: #{opt} is not supported."
|
57
59
|
end
|
58
60
|
end
|
59
|
-
|
60
|
-
def process( results )
|
61
|
-
results
|
62
|
-
end
|
63
61
|
end
|
64
62
|
end
|
65
63
|
end
|
@@ -11,13 +11,17 @@ module JsonApiModel
|
|
11
11
|
"#{name}_id"
|
12
12
|
end
|
13
13
|
|
14
|
-
def
|
15
|
-
if
|
14
|
+
def ids( instance )
|
15
|
+
if json_relationship?
|
16
16
|
instance.relationship_ids( name ).first
|
17
17
|
else
|
18
18
|
instance.send key
|
19
19
|
end
|
20
20
|
end
|
21
|
+
|
22
|
+
def query( instance )
|
23
|
+
ids( instance )
|
24
|
+
end
|
21
25
|
end
|
22
26
|
end
|
23
27
|
end
|
@@ -6,20 +6,32 @@ module JsonApiModel
|
|
6
6
|
:where
|
7
7
|
end
|
8
8
|
|
9
|
-
def
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
9
|
+
def additional_options
|
10
|
+
[ :as, :through ]
|
11
|
+
end
|
12
|
+
|
13
|
+
def key
|
14
|
+
if json_relationship? || through?
|
15
|
+
:id
|
14
16
|
elsif as?
|
15
|
-
|
17
|
+
"#{as}_id"
|
16
18
|
else
|
17
|
-
|
19
|
+
idify( base_class )
|
18
20
|
end
|
19
21
|
end
|
20
22
|
|
21
|
-
def
|
22
|
-
|
23
|
+
def ids( instance )
|
24
|
+
if json_relationship?
|
25
|
+
instance.relationship_ids( name )
|
26
|
+
elsif through?
|
27
|
+
target_ids( instance )
|
28
|
+
else
|
29
|
+
instance.id
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def query( instance )
|
34
|
+
{ key => ids( instance ) }
|
23
35
|
end
|
24
36
|
|
25
37
|
protected
|
@@ -41,15 +53,15 @@ module JsonApiModel
|
|
41
53
|
end
|
42
54
|
|
43
55
|
def target_ids( instance )
|
44
|
-
|
45
|
-
|
46
|
-
|
56
|
+
intermediates = Array(instance.send( through ) )
|
57
|
+
|
58
|
+
intermediates.map do | intermediate |
|
47
59
|
intermediate.send( through_key )
|
48
60
|
end
|
49
61
|
end
|
50
62
|
|
51
63
|
def through_key
|
52
|
-
idify
|
64
|
+
idify association_class
|
53
65
|
end
|
54
66
|
end
|
55
67
|
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module JsonApiModel
|
2
|
+
module Associations
|
3
|
+
class Preloader
|
4
|
+
|
5
|
+
class << self
|
6
|
+
def preload( objects, *preloads )
|
7
|
+
new( objects, preloads ).preload
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
attr_accessor :objects, :preloads
|
12
|
+
|
13
|
+
def initialize( objects, preloads )
|
14
|
+
@objects = Array( objects ).compact
|
15
|
+
@preloads = preloads
|
16
|
+
|
17
|
+
validate_homogenity!
|
18
|
+
end
|
19
|
+
|
20
|
+
def preload
|
21
|
+
@preloads.each do | preload |
|
22
|
+
case preload
|
23
|
+
when Hash
|
24
|
+
preload.each do | preload, subpreloads |
|
25
|
+
preloader = Preloaders.preloader_for( @objects, preload )
|
26
|
+
|
27
|
+
subobjects = preloader.load
|
28
|
+
preloader.assign subobjects
|
29
|
+
|
30
|
+
subobjects.preload( subpreloads )
|
31
|
+
end
|
32
|
+
else
|
33
|
+
Preloaders.preloader_for( @objects, preload ).fetch
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
@objects
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def validate_homogenity!
|
43
|
+
unless homogeneous?
|
44
|
+
raise "JsonApiModel::Associations::Preloader.preload called with a heterogeneous array of objects."
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def object_class
|
49
|
+
@object_class ||= @objects.first.class
|
50
|
+
end
|
51
|
+
|
52
|
+
def homogeneous?
|
53
|
+
@objects.all? do |obj|
|
54
|
+
obj.is_a?(object_class)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module JsonApiModel
|
2
|
+
module Associations
|
3
|
+
module Preloaders
|
4
|
+
autoload :Base, 'json_api_model/associations/preloaders/base'
|
5
|
+
autoload :BelongsTo, 'json_api_model/associations/preloaders/belongs_to'
|
6
|
+
autoload :Has, 'json_api_model/associations/preloaders/has'
|
7
|
+
|
8
|
+
class << self
|
9
|
+
def preloader_for( objects, preload )
|
10
|
+
klass = object_class( objects )
|
11
|
+
association = klass.__associations.fetch preload
|
12
|
+
|
13
|
+
PREOLOADERS[ association.class ].new( objects, association )
|
14
|
+
rescue KeyError
|
15
|
+
raise "#{klass}##{preload.to_s} is not a valid association"
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def object_class( objects )
|
21
|
+
objects.first.class
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
PREOLOADERS = {
|
26
|
+
JsonApiModel::Associations::BelongsTo => JsonApiModel::Associations::Preloaders::BelongsTo,
|
27
|
+
JsonApiModel::Associations::HasOne => JsonApiModel::Associations::Preloaders::Has,
|
28
|
+
JsonApiModel::Associations::HasMany => JsonApiModel::Associations::Preloaders::Has
|
29
|
+
}
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module JsonApiModel
|
2
|
+
module Associations
|
3
|
+
module Preloaders
|
4
|
+
class Base
|
5
|
+
|
6
|
+
attr_accessor :association
|
7
|
+
delegate :key, :name, :association_class, :action, :process, :ids, :base_class, to: :association
|
8
|
+
|
9
|
+
def initialize( objects, association )
|
10
|
+
@objects = Array( objects )
|
11
|
+
@association = association
|
12
|
+
end
|
13
|
+
|
14
|
+
def fetch
|
15
|
+
assign load
|
16
|
+
end
|
17
|
+
|
18
|
+
def load
|
19
|
+
association_class.send( action, query( @objects ) )
|
20
|
+
end
|
21
|
+
|
22
|
+
def assign( results )
|
23
|
+
validate_assignability!( results )
|
24
|
+
@objects.each do | object |
|
25
|
+
|
26
|
+
associated_objects = results.to_a.select do |r|
|
27
|
+
associated_key( r ).in? Array( ids( object ) )
|
28
|
+
end
|
29
|
+
|
30
|
+
object.send( "#{name}=", process( associated_objects ) )
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
protected
|
35
|
+
|
36
|
+
def validate_assignability!( results )
|
37
|
+
results.each do | object |
|
38
|
+
unless associated_key( object )
|
39
|
+
raise "Preloading #{association_class}.#{lookup} failed: results don't identify an association."
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module JsonApiModel
|
2
|
+
module Associations
|
3
|
+
module Preloaders
|
4
|
+
class BelongsTo < Base
|
5
|
+
def associated_key( object )
|
6
|
+
object.id
|
7
|
+
rescue => e
|
8
|
+
nil
|
9
|
+
end
|
10
|
+
|
11
|
+
def lookup
|
12
|
+
:id
|
13
|
+
end
|
14
|
+
|
15
|
+
def query( instances )
|
16
|
+
instances.map do | instance |
|
17
|
+
ids( instance )
|
18
|
+
end.uniq
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module JsonApiModel
|
2
|
+
module Associations
|
3
|
+
module Preloaders
|
4
|
+
class Has < Base
|
5
|
+
def associated_key( object )
|
6
|
+
object.respond_to?( key ) ? object.send( key ) : object.id
|
7
|
+
rescue => e
|
8
|
+
nil
|
9
|
+
end
|
10
|
+
|
11
|
+
def query( instances )
|
12
|
+
instances.each_with_object( { key => [] } ) do | instance, query |
|
13
|
+
query[ key ] += Array( ids( instance ) )
|
14
|
+
query[ key ].uniq!
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
protected
|
19
|
+
|
20
|
+
def lookup
|
21
|
+
base_class.to_s.demodulize.underscore
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/lib/json_api_model/model.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
module JsonApiModel
|
2
2
|
class Model
|
3
3
|
class << self
|
4
|
-
delegate :where, :order, :includes, :select, :all, :paginate, :page, :with_params, :first, :find, :last, to: :__new_scope
|
4
|
+
delegate :where, :order, :includes, :select, :all, :paginate, :page, :with_params, :first, :find, :last, :preload, to: :__new_scope
|
5
5
|
|
6
6
|
attr_reader :client_class
|
7
7
|
def wraps( client_class )
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: json_api_model
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Greg Orlov
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-
|
11
|
+
date: 2018-09-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -135,6 +135,11 @@ files:
|
|
135
135
|
- lib/json_api_model/associations/has.rb
|
136
136
|
- lib/json_api_model/associations/has_many.rb
|
137
137
|
- lib/json_api_model/associations/has_one.rb
|
138
|
+
- lib/json_api_model/associations/preloader.rb
|
139
|
+
- lib/json_api_model/associations/preloaders.rb
|
140
|
+
- lib/json_api_model/associations/preloaders/base.rb
|
141
|
+
- lib/json_api_model/associations/preloaders/belongs_to.rb
|
142
|
+
- lib/json_api_model/associations/preloaders/has.rb
|
138
143
|
- lib/json_api_model/instrumenter.rb
|
139
144
|
- lib/json_api_model/instrumenter/null_instrumenter.rb
|
140
145
|
- lib/json_api_model/model.rb
|