json_api_model 0.4.0 → 0.5.0
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/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
|