Skip to content

Commit ba56e85

Browse files
author
Lee Richmond
committed
Fix many-to-many writes
This was leaving orphaned records in the 'through' table, even though everything worked as far as the API was concerned. The trick is to ensure we hit the through table when destroying.
1 parent 35ad835 commit ba56e85

File tree

3 files changed

+34
-10
lines changed

3 files changed

+34
-10
lines changed

lib/jsonapi_compliable/adapters/active_record.rb

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,11 +73,14 @@ def sideloading_module
7373
# @see Adapters::Abstract#associate
7474
def associate(parent, child, association_name, association_type)
7575
if association_type == :has_many
76-
parent.association(association_name).loaded!
77-
parent.association(association_name).add_to_target(child, :skip_callbacks)
76+
associate_many(parent, child, association_name)
7877
elsif association_type == :habtm
79-
parent.send(association_name) << child
80-
else
78+
if parent.send(association_name).exists?(child.id)
79+
associate_many(parent, child, association_name)
80+
else
81+
parent.send(association_name) << child
82+
end
83+
elsif
8184
child.send("#{association_name}=", parent)
8285
end
8386
end
@@ -113,6 +116,13 @@ def destroy(model_class, id)
113116
instance.destroy
114117
instance
115118
end
119+
120+
private
121+
122+
def associate_many(parent, child, association_name)
123+
parent.association(association_name).loaded!
124+
parent.association(association_name).add_to_target(child, :skip_callbacks)
125+
end
116126
end
117127
end
118128
end

lib/jsonapi_compliable/util/persistence.rb

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,13 +95,14 @@ def associate_parents(object, parents)
9595
end
9696

9797
def associate_children(object, children)
98-
# No need to associate destroyed objects
99-
return if @meta[:method] == :destroy
100-
10198
children.each do |x|
10299
if x[:object] && object
103100
if x[:meta][:method] == :disassociate
104101
x[:sideload].disassociate(object, x[:object])
102+
elsif x[:meta][:method] == :destroy
103+
if x[:sideload].type == :habtm
104+
x[:sideload].disassociate(object, x[:object])
105+
end # otherwise, no need to disassociate destroyed objects
105106
else
106107
x[:sideload].associate(object, x[:object])
107108
end

spec/integration/rails/persistence_spec.rb

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ def do_put(id)
154154
let(:prior_team) { Team.new(name: 'prior') }
155155
let(:disassociate_team) { Team.new(name: 'disassociate') }
156156
let(:destroy_team) { Team.new(name: 'destroy') }
157+
let(:associate_team) { Team.create!(name: 'preexisting') }
157158

158159
before do
159160
employee.teams << prior_team
@@ -172,7 +173,8 @@ def do_put(id)
172173
{ :'temp-id' => 'abc123', type: 'teams', method: 'create' },
173174
{ id: prior_team.id.to_s, type: 'teams', method: 'update' },
174175
{ id: disassociate_team.id.to_s, type: 'teams', method: 'disassociate' },
175-
{ id: destroy_team.id.to_s, type: 'teams', method: 'destroy' }
176+
{ id: destroy_team.id.to_s, type: 'teams', method: 'destroy' },
177+
{ id: associate_team.id.to_s, type: 'teams', method: 'update' }
176178
]
177179
}
178180
}
@@ -187,22 +189,33 @@ def do_put(id)
187189
id: prior_team.id.to_s,
188190
type: 'teams',
189191
attributes: { name: 'Updated!' }
192+
},
193+
{
194+
id: associate_team.id.to_s,
195+
type: 'teams'
190196
}
191197
]
192198
}
193199
end
194200

195-
it 'can create/update/disassociate/destroy' do
201+
it 'can create/update/disassociate/associate/destroy' do
196202
expect(employee.teams).to include(destroy_team)
197203
expect(employee.teams).to include(disassociate_team)
198204
do_put(employee.id)
205+
206+
# Should properly delete/create from the through table
207+
combos = EmployeeTeam.all.map { |et| [et.employee_id, et.team_id] }
208+
expect(combos.uniq.length).to eq(combos.length)
209+
199210
employee.reload
200211
expect(employee.teams).to_not include(disassociate_team)
201212
expect(employee.teams).to_not include(destroy_team)
202213
expect { disassociate_team.reload }.to_not raise_error
203214
expect { destroy_team.reload }.to raise_error(ActiveRecord::RecordNotFound)
204215
expect(prior_team.reload.name).to include('Updated!')
205-
expect((employee.teams - [prior_team]).first.name).to eq('Team #1')
216+
expect(employee.teams).to include(associate_team)
217+
expect((employee.teams - [prior_team, associate_team]).first.name)
218+
.to eq('Team #1')
206219
end
207220
end
208221

0 commit comments

Comments
 (0)