Skip to content

Commit 84de936

Browse files
authored
Merge pull request #31 from richmolj/master
Add support for polymorphic writes
2 parents 446bf22 + 89211de commit 84de936

File tree

10 files changed

+189
-23
lines changed

10 files changed

+189
-23
lines changed

lib/jsonapi_compliable/adapters/active_record_sideloading.rb

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ def belongs_to(association_name, scope: nil, resource:, foreign_key:, primary_ke
4747
def has_one(association_name, scope: nil, resource:, foreign_key:, primary_key: :id, &blk)
4848
_scope = scope
4949

50-
allow_sideload association_name, resource: resource do
50+
allow_sideload association_name, type: :has_one, foreign_key: foreign_key, primary_key: primary_key, resource: resource do
5151
scope do |parents|
5252
parent_ids = parents.map { |p| p.send(primary_key) }
5353
_scope.call.where(foreign_key => parent_ids.uniq.compact)
@@ -71,7 +71,7 @@ def has_and_belongs_to_many(association_name, scope: nil, resource:, foreign_key
7171
fk = foreign_key.values.first
7272
_scope = scope
7373

74-
allow_sideload association_name, resource: resource do
74+
allow_sideload association_name, type: :habtm, foreign_key: foreign_key, primary_key: primary_key, resource: resource do
7575
scope do |parents|
7676
parent_ids = parents.map { |p| p.send(primary_key) }
7777
parent_ids.uniq!
@@ -94,14 +94,14 @@ def has_and_belongs_to_many(association_name, scope: nil, resource:, foreign_key
9494
end
9595

9696
def polymorphic_belongs_to(association_name, group_by:, groups:, &blk)
97-
allow_sideload association_name, polymorphic: true do
98-
group_by(&group_by)
97+
allow_sideload association_name, type: :polymorphic_belongs_to, polymorphic: true do
98+
group_by(group_by)
9999

100100
groups.each_pair do |type, config|
101101
primary_key = config[:primary_key] || :id
102102
foreign_key = config[:foreign_key]
103103

104-
allow_sideload type, resource: config[:resource] do
104+
allow_sideload type, parent: self, primary_key: primary_key, foreign_key: foreign_key, type: :belongs_to, resource: config[:resource] do
105105
scope do |parents|
106106
parent_ids = parents.map { |p| p.send(foreign_key) }
107107
parent_ids.compact!

lib/jsonapi_compliable/sideload.rb

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ module JsonapiCompliable
66
# @attr_reader [Hash] sideloads The associated sibling sideloads
77
# @attr_reader [Proc] scope_proc The configured 'scope' block
88
# @attr_reader [Proc] assign_proc The configured 'assign' block
9-
# @attr_reader [Proc] grouper The configured 'group_by' proc
9+
# @attr_reader [Symbol] grouping_field The configured 'group_by' symbol
1010
# @attr_reader [Symbol] foreign_key The attribute used to match objects - need not be a true database foreign key.
1111
# @attr_reader [Symbol] primary_key The attribute used to match objects - need not be a true database primary key.
1212
# @attr_reader [Symbol] type One of :has_many, :belongs_to, etc
@@ -15,10 +15,11 @@ class Sideload
1515
:resource_class,
1616
:polymorphic,
1717
:polymorphic_groups,
18+
:parent,
1819
:sideloads,
1920
:scope_proc,
2021
:assign_proc,
21-
:grouper,
22+
:grouping_field,
2223
:foreign_key,
2324
:primary_key,
2425
:type
@@ -28,12 +29,13 @@ class Sideload
2829
# An anonymous Resource will be assigned when none provided.
2930
#
3031
# @see Adapters::Abstract#sideloading_module
31-
def initialize(name, type: nil, resource: nil, polymorphic: false, primary_key: :id, foreign_key: nil)
32+
def initialize(name, type: nil, resource: nil, polymorphic: false, primary_key: :id, foreign_key: nil, parent: nil)
3233
@name = name
3334
@resource_class = (resource || Class.new(Resource))
3435
@sideloads = {}
3536
@polymorphic = !!polymorphic
3637
@polymorphic_groups = {} if polymorphic?
38+
@parent = parent
3739
@primary_key = primary_key
3840
@foreign_key = foreign_key
3941
@type = type
@@ -55,7 +57,7 @@ def resource
5557
# +Business+ or +Government+:
5658
#
5759
# allow_sideload :organization, :polymorphic: true do
58-
# group_by { |record| record.organization_type }
60+
# group_by :organization_type
5961
#
6062
# allow_sideload 'Business', resource: BusinessResource do
6163
# # ... code ...
@@ -70,7 +72,7 @@ def resource
7072
# with ActiveRecord:
7173
#
7274
# polymorphic_belongs_to :organization,
73-
# group_by: ->(office) { office.organization_type },
75+
# group_by: :organization_type,
7476
# groups: {
7577
# 'Business' => {
7678
# scope: -> { Business.all },
@@ -181,21 +183,25 @@ def assign(&blk)
181183
# @see #name
182184
# @see #type
183185
def associate(parent, child)
184-
resource_class.config[:adapter].associate(parent, child, name, type)
186+
association_name = @parent ? @parent.name : name
187+
resource_class.config[:adapter].associate parent,
188+
child,
189+
association_name,
190+
type
185191
end
186192

187-
# Define a proc that groups the parent records. For instance, with
193+
# Define an attribute that groups the parent records. For instance, with
188194
# an ActiveRecord polymorphic belongs_to there will be a +parent_id+
189195
# and +parent_type+. We would want to group on +parent_type+:
190196
#
191197
# allow_sideload :organization, polymorphic: true do
192198
# # group parent_type, parent here is 'organization'
193-
# group_by ->(office) { office.organization_type }
199+
# group_by :organization_type
194200
# end
195201
#
196202
# @see #polymorphic?
197-
def group_by(&grouper)
198-
@grouper = grouper
203+
def group_by(grouping_field)
204+
@grouping_field = grouping_field
199205
end
200206

201207
# Resolve the sideload.
@@ -323,6 +329,13 @@ def to_hash(processed = [])
323329
result
324330
end
325331

332+
# @api private
333+
def polymorphic_child_for_type(type)
334+
polymorphic_groups.values.find do |v|
335+
v.resource_class.config[:type] == type
336+
end
337+
end
338+
326339
private
327340

328341
def nested_sideload_hash(sideload, processed)
@@ -333,8 +346,24 @@ def nested_sideload_hash(sideload, processed)
333346
end
334347
end
335348

349+
def polymorphic_grouper(grouping_field)
350+
lambda do |record|
351+
if record.is_a?(Hash)
352+
if record.keys[0].is_a?(Symbol)
353+
record[grouping_field]
354+
else
355+
record[grouping_field.to_s]
356+
end
357+
else
358+
record.send(grouping_field)
359+
end
360+
end
361+
end
362+
336363
def resolve_polymorphic(parents, query)
337-
parents.group_by(&@grouper).each_pair do |group_type, group_members|
364+
grouper = polymorphic_grouper(@grouping_field)
365+
366+
parents.group_by(&grouper).each_pair do |group_type, group_members|
338367
sideload_for_group = @polymorphic_groups[group_type]
339368
if sideload_for_group
340369
sideload_for_group.resolve(group_members, query, name)

lib/jsonapi_compliable/util/persistence.rb

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,18 @@ def run
5252
def update_foreign_key(parent_object, attrs, x)
5353
if [:destroy, :disassociate].include?(x[:meta][:method])
5454
attrs[x[:foreign_key]] = nil
55+
update_foreign_type(attrs, x, null: true) if x[:is_polymorphic]
5556
else
5657
attrs[x[:foreign_key]] = parent_object.send(x[:primary_key])
58+
update_foreign_type(attrs, x) if x[:is_polymorphic]
5759
end
5860
end
5961

62+
def update_foreign_type(attrs, x, null: false)
63+
grouping_field = x[:sideload].parent.grouping_field
64+
attrs[grouping_field] = null ? nil : x[:sideload].name
65+
end
66+
6067
def update_foreign_key_for_parents(parents)
6168
parents.each do |x|
6269
update_foreign_key(x[:object], @attributes, x)
@@ -88,7 +95,7 @@ def persist_object(method, attributes)
8895

8996
def process_has_many(relationships)
9097
[].tap do |processed|
91-
iterate(except: [:belongs_to]) do |x|
98+
iterate(except: [:polymorphic_belongs_to, :belongs_to]) do |x|
9299
yield x
93100
x[:object] = x[:sideload].resource
94101
.persist_with_relationships(x[:meta], x[:attributes], x[:relationships])
@@ -99,7 +106,7 @@ def process_has_many(relationships)
99106

100107
def process_belongs_to(relationships)
101108
[].tap do |processed|
102-
iterate(only: [:belongs_to]) do |x|
109+
iterate(only: [:polymorphic_belongs_to, :belongs_to]) do |x|
103110
x[:object] = x[:sideload].resource
104111
.persist_with_relationships(x[:meta], x[:attributes], x[:relationships])
105112
processed << x

lib/jsonapi_compliable/util/relationship_payload.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,14 @@ def should_yield?(type)
4343
end
4444

4545
def payload_for(sideload, relationship_payload)
46+
if sideload.polymorphic?
47+
type = relationship_payload[:meta][:jsonapi_type]
48+
sideload = sideload.polymorphic_child_for_type(type)
49+
end
50+
4651
{
4752
sideload: sideload,
53+
is_polymorphic: !sideload.parent.nil?,
4854
primary_key: sideload.primary_key,
4955
foreign_key: sideload.foreign_key,
5056
attributes: relationship_payload[:attributes],

lib/jsonapi_compliable/util/validation_response.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ def to_a
3333
private
3434

3535
def valid_object?(object)
36-
object.respond_to?(:errors) && object.errors.blank?
36+
!object.respond_to?(:errors) ||
37+
(object.respond_to?(:errors) && object.errors.blank?)
3738
end
3839

3940
def all_valid?(model, deserialized_params)

spec/fixtures/employee_directory.rb

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,17 @@
33
t.string :description
44
end
55

6+
create_table :offices do |t|
7+
t.string :address
8+
end
9+
10+
create_table :home_offices do |t|
11+
t.string :address
12+
end
13+
614
create_table :employees do |t|
15+
t.string :workspace_type
16+
t.integer :workspace_id
717
t.integer :classification_id
818
t.string :first_name
919
t.string :last_name
@@ -30,7 +40,16 @@ class Classification < ApplicationRecord
3040
validates :description, presence: true
3141
end
3242

43+
class Office < ApplicationRecord
44+
has_many :employees, as: :workspace
45+
end
46+
47+
class HomeOffice < ApplicationRecord
48+
has_many :employees, as: :workspace
49+
end
50+
3351
class Employee < ApplicationRecord
52+
belongs_to :workspace, polymorphic: true
3453
belongs_to :classification
3554
has_many :positions
3655
validates :first_name, presence: true
@@ -69,6 +88,16 @@ class PositionResource < ApplicationResource
6988
resource: DepartmentResource
7089
end
7190

91+
class OfficeResource < ApplicationResource
92+
type 'offices'
93+
model Office
94+
end
95+
96+
class HomeOfficeResource < ApplicationResource
97+
type 'home_offices'
98+
model HomeOffice
99+
end
100+
72101
class EmployeeResource < ApplicationResource
73102
type 'employees'
74103
model Employee
@@ -81,6 +110,20 @@ class EmployeeResource < ApplicationResource
81110
scope: -> { Position.all },
82111
foreign_key: :employee_id,
83112
resource: PositionResource
113+
polymorphic_belongs_to :workspace,
114+
group_by: :workspace_type,
115+
groups: {
116+
'Office' => {
117+
scope: -> { Office.all },
118+
resource: OfficeResource,
119+
foreign_key: :workspace_id
120+
},
121+
'HomeOffice' => {
122+
scope: -> { HomeOffice.all },
123+
resource: HomeOfficeResource,
124+
foreign_key: :workspace_id
125+
}
126+
}
84127
end
85128

86129
class SerializableAbstract < JSONAPI::Serializable::Resource

spec/integration/rails/finders_spec.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ class AuthorResource < JsonapiCompliable::Resource
8686
foreign_key: { author_hobbies: :author_id }
8787

8888
polymorphic_belongs_to :dwelling,
89-
group_by: proc { |author| author.dwelling_type },
89+
group_by: :dwelling_type,
9090
groups: {
9191
'House' => {
9292
foreign_key: :dwelling_id,

spec/integration/rails/persistence_spec.rb

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,62 @@ def do_put(id)
149149
end
150150
end
151151

152+
describe 'nested polymorphic relationship' do
153+
let(:workspace_type) { 'offices' }
154+
155+
let(:payload) do
156+
{
157+
data: {
158+
type: 'employees',
159+
attributes: { first_name: 'Joe' },
160+
relationships: {
161+
workspace: {
162+
data: {
163+
:'temp-id' => 'work1', type: workspace_type, method: 'create'
164+
}
165+
}
166+
}
167+
},
168+
included: [
169+
{
170+
type: workspace_type,
171+
:'temp-id' => 'work1',
172+
attributes: {
173+
address: 'Fake Workspace Address'
174+
}
175+
}
176+
]
177+
}
178+
end
179+
180+
context 'with jsonapi type "offices"' do
181+
it 'associates workspace as office' do
182+
do_post
183+
employee = Employee.first
184+
expect(employee.workspace).to be_a(Office)
185+
end
186+
end
187+
188+
context 'with jsonapi type "home_offices"' do
189+
let(:workspace_type) { 'home_offices' }
190+
191+
it 'associates workspace as home office' do
192+
do_post
193+
employee = Employee.first
194+
expect(employee.workspace).to be_a(HomeOffice)
195+
end
196+
end
197+
198+
it 'saves the relationship correctly' do
199+
expect {
200+
do_post
201+
}.to change { Employee.count }.by(1)
202+
employee = Employee.first
203+
workspace = employee.workspace
204+
expect(workspace.address).to eq('Fake Workspace Address')
205+
end
206+
end
207+
152208
describe 'nested create' do
153209
let(:payload) do
154210
{

0 commit comments

Comments
 (0)