Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions lib/jsonapi_compliable/adapters/active_record_sideloading.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def belongs_to(association_name, scope: nil, resource:, foreign_key:, primary_ke
def has_one(association_name, scope: nil, resource:, foreign_key:, primary_key: :id, &blk)
_scope = scope

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

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

def polymorphic_belongs_to(association_name, group_by:, groups:, &blk)
allow_sideload association_name, polymorphic: true do
group_by(&group_by)
allow_sideload association_name, type: :polymorphic_belongs_to, polymorphic: true do
group_by(group_by)

groups.each_pair do |type, config|
primary_key = config[:primary_key] || :id
foreign_key = config[:foreign_key]

allow_sideload type, resource: config[:resource] do
allow_sideload type, parent: self, primary_key: primary_key, foreign_key: foreign_key, type: :belongs_to, resource: config[:resource] do
scope do |parents|
parent_ids = parents.map { |p| p.send(foreign_key) }
parent_ids.compact!
Expand Down
51 changes: 40 additions & 11 deletions lib/jsonapi_compliable/sideload.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ module JsonapiCompliable
# @attr_reader [Hash] sideloads The associated sibling sideloads
# @attr_reader [Proc] scope_proc The configured 'scope' block
# @attr_reader [Proc] assign_proc The configured 'assign' block
# @attr_reader [Proc] grouper The configured 'group_by' proc
# @attr_reader [Symbol] grouping_field The configured 'group_by' symbol
# @attr_reader [Symbol] foreign_key The attribute used to match objects - need not be a true database foreign key.
# @attr_reader [Symbol] primary_key The attribute used to match objects - need not be a true database primary key.
# @attr_reader [Symbol] type One of :has_many, :belongs_to, etc
Expand All @@ -15,10 +15,11 @@ class Sideload
:resource_class,
:polymorphic,
:polymorphic_groups,
:parent,
:sideloads,
:scope_proc,
:assign_proc,
:grouper,
:grouping_field,
:foreign_key,
:primary_key,
:type
Expand All @@ -28,12 +29,13 @@ class Sideload
# An anonymous Resource will be assigned when none provided.
#
# @see Adapters::Abstract#sideloading_module
def initialize(name, type: nil, resource: nil, polymorphic: false, primary_key: :id, foreign_key: nil)
def initialize(name, type: nil, resource: nil, polymorphic: false, primary_key: :id, foreign_key: nil, parent: nil)
@name = name
@resource_class = (resource || Class.new(Resource))
@sideloads = {}
@polymorphic = !!polymorphic
@polymorphic_groups = {} if polymorphic?
@parent = parent
@primary_key = primary_key
@foreign_key = foreign_key
@type = type
Expand All @@ -55,7 +57,7 @@ def resource
# +Business+ or +Government+:
#
# allow_sideload :organization, :polymorphic: true do
# group_by { |record| record.organization_type }
# group_by :organization_type
#
# allow_sideload 'Business', resource: BusinessResource do
# # ... code ...
Expand All @@ -70,7 +72,7 @@ def resource
# with ActiveRecord:
#
# polymorphic_belongs_to :organization,
# group_by: ->(office) { office.organization_type },
# group_by: :organization_type,
# groups: {
# 'Business' => {
# scope: -> { Business.all },
Expand Down Expand Up @@ -181,21 +183,25 @@ def assign(&blk)
# @see #name
# @see #type
def associate(parent, child)
resource_class.config[:adapter].associate(parent, child, name, type)
association_name = @parent ? @parent.name : name
resource_class.config[:adapter].associate parent,
child,
association_name,
type
end

# Define a proc that groups the parent records. For instance, with
# Define an attribute that groups the parent records. For instance, with
# an ActiveRecord polymorphic belongs_to there will be a +parent_id+
# and +parent_type+. We would want to group on +parent_type+:
#
# allow_sideload :organization, polymorphic: true do
# # group parent_type, parent here is 'organization'
# group_by ->(office) { office.organization_type }
# group_by :organization_type
# end
#
# @see #polymorphic?
def group_by(&grouper)
@grouper = grouper
def group_by(grouping_field)
@grouping_field = grouping_field
end

# Resolve the sideload.
Expand Down Expand Up @@ -323,6 +329,13 @@ def to_hash(processed = [])
result
end

# @api private
def polymorphic_child_for_type(type)
polymorphic_groups.values.find do |v|
v.resource_class.config[:type] == type
end
end

private

def nested_sideload_hash(sideload, processed)
Expand All @@ -333,8 +346,24 @@ def nested_sideload_hash(sideload, processed)
end
end

def polymorphic_grouper(grouping_field)
lambda do |record|
if record.is_a?(Hash)
if record.keys[0].is_a?(Symbol)
record[grouping_field]
else
record[grouping_field.to_s]
end
else
record.send(grouping_field)
end
end
end

def resolve_polymorphic(parents, query)
parents.group_by(&@grouper).each_pair do |group_type, group_members|
grouper = polymorphic_grouper(@grouping_field)

parents.group_by(&grouper).each_pair do |group_type, group_members|
sideload_for_group = @polymorphic_groups[group_type]
if sideload_for_group
sideload_for_group.resolve(group_members, query, name)
Expand Down
11 changes: 9 additions & 2 deletions lib/jsonapi_compliable/util/persistence.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,18 @@ def run
def update_foreign_key(parent_object, attrs, x)
if [:destroy, :disassociate].include?(x[:meta][:method])
attrs[x[:foreign_key]] = nil
update_foreign_type(attrs, x, null: true) if x[:is_polymorphic]
else
attrs[x[:foreign_key]] = parent_object.send(x[:primary_key])
update_foreign_type(attrs, x) if x[:is_polymorphic]
end
end

def update_foreign_type(attrs, x, null: false)
grouping_field = x[:sideload].parent.grouping_field
attrs[grouping_field] = null ? nil : x[:sideload].name
end

def update_foreign_key_for_parents(parents)
parents.each do |x|
update_foreign_key(x[:object], @attributes, x)
Expand Down Expand Up @@ -88,7 +95,7 @@ def persist_object(method, attributes)

def process_has_many(relationships)
[].tap do |processed|
iterate(except: [:belongs_to]) do |x|
iterate(except: [:polymorphic_belongs_to, :belongs_to]) do |x|
yield x
x[:object] = x[:sideload].resource
.persist_with_relationships(x[:meta], x[:attributes], x[:relationships])
Expand All @@ -99,7 +106,7 @@ def process_has_many(relationships)

def process_belongs_to(relationships)
[].tap do |processed|
iterate(only: [:belongs_to]) do |x|
iterate(only: [:polymorphic_belongs_to, :belongs_to]) do |x|
x[:object] = x[:sideload].resource
.persist_with_relationships(x[:meta], x[:attributes], x[:relationships])
processed << x
Expand Down
6 changes: 6 additions & 0 deletions lib/jsonapi_compliable/util/relationship_payload.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,14 @@ def should_yield?(type)
end

def payload_for(sideload, relationship_payload)
if sideload.polymorphic?
type = relationship_payload[:meta][:jsonapi_type]
sideload = sideload.polymorphic_child_for_type(type)
end

{
sideload: sideload,
is_polymorphic: !sideload.parent.nil?,
primary_key: sideload.primary_key,
foreign_key: sideload.foreign_key,
attributes: relationship_payload[:attributes],
Expand Down
3 changes: 2 additions & 1 deletion lib/jsonapi_compliable/util/validation_response.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ def to_a
private

def valid_object?(object)
object.respond_to?(:errors) && object.errors.blank?
!object.respond_to?(:errors) ||
(object.respond_to?(:errors) && object.errors.blank?)
end

def all_valid?(model, deserialized_params)
Expand Down
43 changes: 43 additions & 0 deletions spec/fixtures/employee_directory.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,17 @@
t.string :description
end

create_table :offices do |t|
t.string :address
end

create_table :home_offices do |t|
t.string :address
end

create_table :employees do |t|
t.string :workspace_type
t.integer :workspace_id
t.integer :classification_id
t.string :first_name
t.string :last_name
Expand All @@ -30,7 +40,16 @@ class Classification < ApplicationRecord
validates :description, presence: true
end

class Office < ApplicationRecord
has_many :employees, as: :workspace
end

class HomeOffice < ApplicationRecord
has_many :employees, as: :workspace
end

class Employee < ApplicationRecord
belongs_to :workspace, polymorphic: true
belongs_to :classification
has_many :positions
validates :first_name, presence: true
Expand Down Expand Up @@ -69,6 +88,16 @@ class PositionResource < ApplicationResource
resource: DepartmentResource
end

class OfficeResource < ApplicationResource
type 'offices'
model Office
end

class HomeOfficeResource < ApplicationResource
type 'home_offices'
model HomeOffice
end

class EmployeeResource < ApplicationResource
type 'employees'
model Employee
Expand All @@ -81,6 +110,20 @@ class EmployeeResource < ApplicationResource
scope: -> { Position.all },
foreign_key: :employee_id,
resource: PositionResource
polymorphic_belongs_to :workspace,
group_by: :workspace_type,
groups: {
'Office' => {
scope: -> { Office.all },
resource: OfficeResource,
foreign_key: :workspace_id
},
'HomeOffice' => {
scope: -> { HomeOffice.all },
resource: HomeOfficeResource,
foreign_key: :workspace_id
}
}
end

class SerializableAbstract < JSONAPI::Serializable::Resource
Expand Down
2 changes: 1 addition & 1 deletion spec/integration/rails/finders_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ class AuthorResource < JsonapiCompliable::Resource
foreign_key: { author_hobbies: :author_id }

polymorphic_belongs_to :dwelling,
group_by: proc { |author| author.dwelling_type },
group_by: :dwelling_type,
groups: {
'House' => {
foreign_key: :dwelling_id,
Expand Down
56 changes: 56 additions & 0 deletions spec/integration/rails/persistence_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,62 @@ def do_put(id)
end
end

describe 'nested polymorphic relationship' do
let(:workspace_type) { 'offices' }

let(:payload) do
{
data: {
type: 'employees',
attributes: { first_name: 'Joe' },
relationships: {
workspace: {
data: {
:'temp-id' => 'work1', type: workspace_type, method: 'create'
}
}
}
},
included: [
{
type: workspace_type,
:'temp-id' => 'work1',
attributes: {
address: 'Fake Workspace Address'
}
}
]
}
end

context 'with jsonapi type "offices"' do
it 'associates workspace as office' do
do_post
employee = Employee.first
expect(employee.workspace).to be_a(Office)
end
end

context 'with jsonapi type "home_offices"' do
let(:workspace_type) { 'home_offices' }

it 'associates workspace as home office' do
do_post
employee = Employee.first
expect(employee.workspace).to be_a(HomeOffice)
end
end

it 'saves the relationship correctly' do
expect {
do_post
}.to change { Employee.count }.by(1)
employee = Employee.first
workspace = employee.workspace
expect(workspace.address).to eq('Fake Workspace Address')
end
end

describe 'nested create' do
let(:payload) do
{
Expand Down
Loading