Skip to content

Commit 90891be

Browse files
authored
Ensure has_one artificially limits (#91)
We don't want to limit a has_one. If a Post has_one Detail, and we were listing all posts and sideloading their detail, only one *total* detail would be in the response. So we can't paginate, and instead we assign only the relevant children. The work done here is to remove the excessive results from the array. This is important because subsequent sideloads will scope to that array. To make this work, we ensure the `assign` block is fired before processing further sideloads, which makes better mental sense in any case. See also: rails/rails#10621
1 parent 86d1b75 commit 90891be

File tree

6 files changed

+71
-3
lines changed

6 files changed

+71
-3
lines changed

lib/jsonapi_compliable/adapters/active_record_sideloading.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,15 @@ def has_one(association_name, scope: nil, resource:, foreign_key:, primary_key:
5353
_scope.call.where(foreign_key => parent_ids.uniq.compact)
5454
end
5555

56+
# The 'assigned' code here is to remove all children that do not
57+
# get assigned. This is because there is no 'limit(1)' in the query.
58+
# If we did 'limit(1)' for the query, it wouldn't work for index
59+
# actions (only 1 would come back, when we want one *per result*).
60+
#
61+
# Instead, avoid pagination in the query, assign only one result, and
62+
# remove anything else. This is more or less what AR does.
5663
assign do |parents, children|
64+
assigned = []
5765
parents.each do |parent|
5866
parent.association(association_name).loaded!
5967
relevant_child = children.find { |c| c.send(foreign_key) == parent.send(primary_key) }
@@ -65,6 +73,10 @@ def has_one(association_name, scope: nil, resource:, foreign_key:, primary_key:
6573
association.send(:set_owner_attributes, relevant_child)
6674
association.send(:set_inverse_instance, relevant_child)
6775
association.send(:target=, relevant_child)
76+
assigned << relevant_child
77+
end
78+
(children - assigned).each do |unassigned|
79+
children.delete(unassigned)
6880
end
6981
end
7082

lib/jsonapi_compliable/scope.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ def resolve
6464
[]
6565
else
6666
resolved = @resource.resolve(@object)
67+
yield resolved if block_given?
6768
sideload(resolved, query_hash[:include]) if query_hash[:include]
6869
resolved
6970
end

lib/jsonapi_compliable/sideload.rb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -399,8 +399,9 @@ def resolve_polymorphic(parents, query)
399399
def resolve_basic(parents, query, namespace)
400400
sideload_scope = scope_proc.call(parents)
401401
sideload_scope = Scope.new(sideload_scope, resource_class.new, query, default_paginate: false, namespace: namespace)
402-
sideload_results = sideload_scope.resolve
403-
assign_proc.call(parents, sideload_results)
402+
sideload_scope.resolve do |sideload_results|
403+
assign_proc.call(parents, sideload_results)
404+
end
404405
end
405406
end
406407
end

spec/fixtures/legacy.rb

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@
4040
t.string :picture
4141
end
4242

43+
create_table :bio_labels do |t|
44+
t.integer :bio_id
45+
end
46+
4347
create_table :genres do |t|
4448
t.string :name
4549
t.timestamps
@@ -111,6 +115,11 @@ class Hobby < LegacyApplicationRecord
111115

112116
class Bio < LegacyApplicationRecord
113117
belongs_to :author
118+
has_many :bio_labels
119+
end
120+
121+
class BioLabel < LegacyApplicationRecord
122+
belongs_to :bio
114123
end
115124

116125
class Genre < LegacyApplicationRecord
@@ -213,6 +222,12 @@ class SerializableBio < LegacySerializableAbstract
213222
extra_attribute :created_at do
214223
Time.now
215224
end
225+
226+
has_many :bio_labels
227+
end
228+
229+
class SerializableBioLabel < LegacySerializableAbstract
230+
type 'bio_labels'
216231
end
217232

218233
class SerializableState < LegacySerializableAbstract

spec/integration/rails/finders_spec.rb

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,27 @@ class DwellingResource < JsonapiCompliable::Resource
3434
resource: StateResource
3535
end
3636

37+
class BioLabelResource < JsonapiCompliable::Resource
38+
type :bio_labels
39+
use_adapter JsonapiCompliable::Adapters::ActiveRecord
40+
end
41+
3742
class BioResource < JsonapiCompliable::Resource
3843
type :bios
3944
use_adapter JsonapiCompliable::Adapters::ActiveRecord
45+
46+
has_many :bio_labels,
47+
foreign_key: :bio_id,
48+
scope: -> { BioLabel.all },
49+
resource: BioLabelResource do
50+
# Ensure if we get too many bios/labels, they
51+
# will still come back in the response.
52+
assign do |bios, labels|
53+
bios.each do |b|
54+
b.bio_labels = labels
55+
end
56+
end
57+
end
4058
end
4159

4260
class HobbyResource < JsonapiCompliable::Resource
@@ -267,6 +285,23 @@ def json
267285
expect(bio).to have_key('created_at')
268286
expect(bio).to_not have_key('picture')
269287
end
288+
289+
# Model/Resource has has_one, but it's just a subset of a has_many
290+
context 'when multiple records (faux-has_one)' do
291+
let!(:bio2) { Bio.create!(author: author1, picture: 'imgur', description: 'author bio') }
292+
293+
context 'and there is another level of association' do
294+
before do
295+
bio.bio_labels << BioLabel.create!
296+
bio2.bio_labels << BioLabel.create!
297+
end
298+
299+
it 'still works' do
300+
get :index, params: { include: 'bio.bio_labels' }
301+
expect(json_includes('bio_labels').length).to eq(1)
302+
end
303+
end
304+
end
270305
end
271306

272307
context 'sideloading has_and_belongs_to_many' do

spec/sideload_spec.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,11 @@ def foo
7777
let(:results) { [{ parent_id: 1 }] }
7878
let(:base_scope) { double }
7979
let(:scope_proc) { ->(parents) { base_scope } }
80-
let(:scope) { double(resolve: results) }
80+
let(:scope) do
81+
scope = double
82+
allow(scope).to receive(:resolve).and_yield(results)
83+
scope
84+
end
8185

8286
before do
8387
instance.scope { |parents| base_scope }

0 commit comments

Comments
 (0)