Skip to content

Commit 9f3e284

Browse files
committedMar 11, 2021
Add STI relationship option
1 parent ab344c4 commit 9f3e284

File tree

9 files changed

+101
-33
lines changed

9 files changed

+101
-33
lines changed
 

‎lib/jsonapi/active_relation_resource.rb

+41-9
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,9 @@ def find_to_populate_by_keys(keys, options = {})
9292
def find_fragments(filters, options = {})
9393
include_directives = options[:include_directives] ? options[:include_directives].include_directives : {}
9494
resource_klass = self
95+
96+
fragments = {}
97+
9598
linkage_relationships = to_one_relationships_for_linkage(include_directives[:include_related])
9699

97100
sort_criteria = options.fetch(:sort_criteria) { [] }
@@ -129,19 +132,33 @@ def find_fragments(filters, options = {})
129132
if linkage_relationship.polymorphic? && linkage_relationship.belongs_to?
130133
linkage_relationship.resource_types.each do |resource_type|
131134
klass = resource_klass_for(resource_type)
132-
linkage_fields << {relationship_name: name, resource_klass: klass}
133-
134135
linkage_table_alias = join_manager.join_details_by_polymorphic_relationship(linkage_relationship, resource_type)[:alias]
135136
primary_key = klass._primary_key
137+
138+
linkage_fields << {relationship_name: name,
139+
linkage_relationship: linkage_relationship,
140+
resource_klass: klass,
141+
field: "#{concat_table_field(linkage_table_alias, primary_key)} AS #{linkage_table_alias}_#{primary_key}",
142+
alias: "#{linkage_table_alias}_#{primary_key}"}
143+
136144
pluck_fields << Arel.sql("#{concat_table_field(linkage_table_alias, primary_key)} AS #{linkage_table_alias}_#{primary_key}")
137145
end
138146
else
139147
klass = linkage_relationship.resource_klass
140-
linkage_fields << {relationship_name: name, resource_klass: klass}
141-
142148
linkage_table_alias = join_manager.join_details_by_relationship(linkage_relationship)[:alias]
143149
primary_key = klass._primary_key
150+
151+
linkage_fields << {relationship_name: name,
152+
linkage_relationship: linkage_relationship,
153+
resource_klass: klass,
154+
field: "#{concat_table_field(linkage_table_alias, primary_key)} AS #{linkage_table_alias}_#{primary_key}",
155+
alias: "#{linkage_table_alias}_#{primary_key}"}
156+
144157
pluck_fields << Arel.sql("#{concat_table_field(linkage_table_alias, primary_key)} AS #{linkage_table_alias}_#{primary_key}")
158+
159+
if linkage_relationship.sti?
160+
pluck_fields << Arel.sql("#{concat_table_field(linkage_table_alias, 'type')} AS #{linkage_table_alias}_type")
Has a conversation. Original line has a conversation.
161+
end
145162
end
146163
end
147164

@@ -158,7 +175,6 @@ def find_fragments(filters, options = {})
158175
pluck_fields << Arel.sql(field)
159176
end
160177

161-
fragments = {}
162178
rows = records.pluck(*pluck_fields)
163179
rows.each do |row|
164180
rid = JSONAPI::ResourceIdentity.new(resource_klass, pluck_fields.length == 1 ? row : row[0])
@@ -175,7 +191,14 @@ def find_fragments(filters, options = {})
175191
fragments[rid].initialize_related(linkage_field_details[:relationship_name])
176192
related_id = row[attributes_offset]
177193
if related_id
178-
related_rid = JSONAPI::ResourceIdentity.new(linkage_field_details[:resource_klass], related_id)
194+
if linkage_field_details[:linkage_relationship].sti?
195+
type = row[2]
196+
related_rid = JSONAPI::ResourceIdentity.new(resource_klass_for(type), related_id)
197+
attributes_offset+= 1
198+
else
199+
related_rid = JSONAPI::ResourceIdentity.new(linkage_field_details[:resource_klass], related_id)
200+
end
201+
179202
fragments[rid].add_related_identity(linkage_field_details[:relationship_name], related_rid)
180203
end
181204
attributes_offset+= 1
@@ -413,6 +436,10 @@ def find_related_monomorphic_fragments(source_rids, relationship, options, conne
413436
Arel.sql("#{concat_table_field(resource_table_alias, resource_klass._primary_key)} AS #{resource_table_alias}_#{resource_klass._primary_key}")
414437
]
415438

439+
if relationship.sti?
440+
pluck_fields << Arel.sql("#{concat_table_field(resource_table_alias, 'type')} AS #{resource_table_alias}_type")
441+
end
442+
416443
cache_field = resource_klass.attribute_to_model_field(:_cache_field) if options[:cache]
417444
if cache_field
418445
pluck_fields << Arel.sql("#{concat_table_field(resource_table_alias, cache_field[:name])} AS #{resource_table_alias}_#{cache_field[:name]}")
@@ -458,12 +485,17 @@ def find_related_monomorphic_fragments(source_rids, relationship, options, conne
458485
fragments = {}
459486
rows = records.distinct.pluck(*pluck_fields)
460487
rows.each do |row|
461-
rid = JSONAPI::ResourceIdentity.new(resource_klass, row[1])
488+
if relationship.sti?
489+
type = row[2]
490+
rid = JSONAPI::ResourceIdentity.new(resource_klass_for(type), row[1])
491+
attributes_offset = 3
492+
else
493+
rid = JSONAPI::ResourceIdentity.new(resource_klass, row[1])
494+
attributes_offset = 2
495+
end
462496

463497
fragments[rid] ||= JSONAPI::ResourceFragment.new(rid)
464498

465-
attributes_offset = 2
466-
467499
if cache_field
468500
fragments[rid].cache = cast_to_attribute_type(row[attributes_offset], cache_field[:type])
469501
attributes_offset+= 1

‎lib/jsonapi/basic_resource.rb

+2-2
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,7 @@ def _replace_to_many_links(relationship_type, relationship_key_values, options)
298298
_create_to_many_links(relationship_type, to_add, {})
299299

300300
@reload_needed = true
301-
elsif relationship.polymorphic?
301+
elsif relationship.polymorphic? || relationship.sti?
302302
relationship_key_values.each do |relationship_key_value|
303303
relationship_resource_klass = self.class.resource_klass_for(relationship_key_value[:type])
304304
ids = relationship_key_value[:ids]
@@ -1094,7 +1094,7 @@ def define_relationship_methods(relationship_name, relationship_klass, options)
10941094
end
10951095

10961096
def define_foreign_key_setter(relationship)
1097-
if relationship.polymorphic?
1097+
if relationship.polymorphic? || relationship.sti?
10981098
define_on_resource "#{relationship.foreign_key}=" do |v|
10991099
_model.method("#{relationship.foreign_key}=").call(v[:id])
11001100
_model.public_send("#{relationship.polymorphic_type}=", v[:type])

‎lib/jsonapi/relationship.rb

+6-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
module JSONAPI
22
class Relationship
33
attr_reader :acts_as_set, :foreign_key, :options, :name,
4-
:class_name, :polymorphic, :always_include_optional_linkage_data,
4+
:class_name, :polymorphic, :sti, :always_include_optional_linkage_data,
55
:parent_resource, :eager_load_on_include, :custom_methods,
66
:inverse_relationship, :allow_include
77

@@ -22,6 +22,7 @@ def initialize(name, options = {})
2222
ActiveSupport::Deprecation.warn('Use polymorphic_types instead of polymorphic_relations')
2323
@polymorphic_types ||= options[:polymorphic_relations]
2424
end
25+
@sti = options.fetch(:sti, false)
2526

2627
@always_include_optional_linkage_data = options.fetch(:always_include_optional_linkage_data, false) == true
2728
@eager_load_on_include = options.fetch(:eager_load_on_include, false) == true
@@ -39,6 +40,8 @@ def initialize(name, options = {})
3940
end
4041

4142
alias_method :polymorphic?, :polymorphic
43+
alias_method :sti?, :sti
44+
4245
alias_method :parent_resource_klass, :parent_resource
4346

4447
def primary_key
@@ -63,12 +66,12 @@ def self.polymorphic_types(name)
6366
next unless Module === klass
6467
if ActiveRecord::Base > klass
6568
klass.reflect_on_all_associations(:has_many).select{|r| r.options[:as] }.each do |reflection|
66-
(hash[reflection.options[:as]] ||= []) << klass.name.downcase
69+
(hash[reflection.options[:as]] ||= []) << klass.name.underscore
6770
end
6871
end
6972
end
7073
end
71-
@poly_hash[name.to_sym]
74+
@poly_hash[name.to_sym] || []
7275
end
7376

7477
def resource_types

‎lib/jsonapi/request_parser.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -567,7 +567,7 @@ def parse_to_many_relationship(resource_klass, link_value, relationship, &add_re
567567
if links_object.length == 0
568568
add_result.call([])
569569
else
570-
if relationship.polymorphic?
570+
if relationship.polymorphic? || relationship.sti?
571571
polymorphic_results = []
572572

573573
links_object.each_pair do |type, keys|

‎test/controllers/controller_test.rb

+18
Original file line numberDiff line numberDiff line change
@@ -2726,6 +2726,12 @@ def test_show_related_resource_no_namespace
27262726
"self" => "http://test.host/people/1001/relationships/expense_entries",
27272727
"related" => "http://test.host/people/1001/expense_entries"
27282728
}
2729+
},
2730+
"favorite-vehicle" => {
2731+
"links" => {
2732+
"self" => "http://test.host/people/1001/relationships/favorite_vehicle",
2733+
"related" => "http://test.host/people/1001/favorite_vehicle"
2734+
}
27292735
}
27302736
}
27312737
}
@@ -2747,6 +2753,18 @@ def test_show_related_resource_includes
27472753
JSONAPI.configuration = original_config
27482754
end
27492755

2756+
def test_show_include_sti
2757+
original_config = JSONAPI.configuration.dup
2758+
JSONAPI.configuration.json_key_format = :dasherized_key
2759+
JSONAPI.configuration.route_format = :underscored_key
2760+
get :show, params: {id: '1005', include: 'vehicles,favorite-vehicle'}
2761+
assert_response :success
2762+
assert_equal 'cars', json_response['included'][0]['type']
2763+
assert_equal 'cars', json_response['data']['relationships']['favorite-vehicle']['data']['type']
2764+
ensure
2765+
JSONAPI.configuration = original_config
2766+
end
2767+
27502768
def test_show_related_resource_nil
27512769
assert_cacheable_get :show_related_resource, params: {post_id: '17', relationship: 'author', source:'posts'}
27522770
assert_response :success

‎test/fixtures/active_record.rb

+16-14
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
t.boolean :book_admin, default: false
4444
t.boolean :special, default: false
4545
t.timestamps null: false
46+
t.integer :favorite_vehicle_id, index: true
4647
end
4748

4849
create_table :author_details, force: true do |t|
@@ -439,11 +440,11 @@ class Session < ActiveRecord::Base
439440

440441
class Response < ActiveRecord::Base
441442
belongs_to :session
442-
has_one :paragraph, :class_name => "ResponseText::Paragraph"
443+
has_one :paragraph, :class_name => "Paragraph"
443444

444445
def response_type
445446
case self.type
446-
when "Response::SingleTextbox"
447+
when "SingleTextbox"
447448
"single_textbox"
448449
else
449450
"question"
@@ -452,21 +453,21 @@ def response_type
452453
def response_type=type
453454
self.type = case type
454455
when "single_textbox"
455-
"Response::SingleTextbox"
456+
"SingleTextbox"
456457
else
457458
"Response"
458459
end
459460
end
460461
end
461462

462-
class Response::SingleTextbox < Response
463-
has_one :paragraph, :class_name => "ResponseText::Paragraph", :foreign_key => :response_id
463+
class SingleTextbox < Response
464+
has_one :paragraph, :class_name => "Paragraph", :foreign_key => :response_id
464465
end
465466

466467
class ResponseText < ActiveRecord::Base
467468
end
468469

469-
class ResponseText::Paragraph < ResponseText
470+
class Paragraph < ResponseText
470471
end
471472

472473
class Person < ActiveRecord::Base
@@ -478,6 +479,7 @@ class Person < ActiveRecord::Base
478479
belongs_to :preferences
479480
belongs_to :hair_cut
480481
has_one :author_detail
482+
belongs_to :favorite_vehicle, class_name: 'Vehicle'
481483

482484
has_and_belongs_to_many :books, join_table: :book_authors
483485
has_and_belongs_to_many :not_banned_books, -> { merge(Book.not_banned) },
@@ -1265,7 +1267,7 @@ def responses=params
12651267
(datum[:relationships] || {}).each_pair { |k,v|
12661268
case k
12671269
when "paragraph"
1268-
response.paragraph = ResponseText::Paragraph.create(((v[:data][:attributes].respond_to?(:permit))? v[:data][:attributes].permit(:text) : v[:data][:attributes]))
1270+
response.paragraph = Paragraph.create(((v[:data][:attributes].respond_to?(:permit))? v[:data][:attributes].permit(:text) : v[:data][:attributes]))
12691271
end
12701272
}
12711273
}
@@ -1285,17 +1287,18 @@ def fetchable_fields
12851287
end
12861288

12871289
class ResponseResource < JSONAPI::Resource
1288-
model_hint model: Response::SingleTextbox, resource: :response
1289-
12901290
has_one :session
12911291

12921292
attributes :question_id, :response_type
12931293

12941294
has_one :paragraph
12951295
end
12961296

1297+
class SingleTextboxResource < ResponseResource
1298+
end
1299+
12971300
class ParagraphResource < JSONAPI::Resource
1298-
model_name 'ResponseText::Paragraph'
1301+
model_name 'Paragraph'
12991302

13001303
attributes :text
13011304

@@ -1308,8 +1311,9 @@ class PersonResource < BaseResource
13081311

13091312
has_many :comments, inverse_relationship: :author
13101313
has_many :posts, inverse_relationship: :author
1311-
has_many :vehicles, polymorphic: true
1314+
has_many :vehicles, sti: true
13121315

1316+
has_one :favorite_vehicle, class_name: 'Vehicle', sti: true
13131317
has_one :preferences
13141318
has_one :hair_cut
13151319

@@ -1357,12 +1361,10 @@ class VehicleResource < JSONAPI::Resource
13571361
end
13581362

13591363
class CarResource < VehicleResource
1360-
model_name "Car"
13611364
attributes :drive_layout
13621365
end
13631366

13641367
class BoatResource < VehicleResource
1365-
model_name "Boat"
13661368
attributes :length_at_water_line
13671369
end
13681370

@@ -2167,7 +2169,7 @@ class CommentResource < CommentResource; end
21672169
class ExpenseEntryResource < ExpenseEntryResource; end
21682170
class IsoCurrencyResource < IsoCurrencyResource; end
21692171
class EmployeeResource < EmployeeResource; end
2170-
class VehicleResource < PersonResource; end
2172+
class VehicleResource < VehicleResource; end
21712173
class HairCutResource < HairCutResource; end
21722174
end
21732175
end

‎test/fixtures/people.yml

+1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ e:
3131
date_joined: <%= DateTime.parse('2013-11-30 4:20:00 UTC +00:00') %>
3232
book_admin: true
3333
preferences_id: 55
34+
favorite_vehicle_id: 1
3435

3536
x:
3637
id: 1000

‎test/integration/requests/request_test.rb

+4-4
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,7 @@ def test_post_single
317317
assert_jsonapi_response 201
318318
end
319319

320-
def test_post_polymorphic_with_has_many_relationship
320+
def test_post_sti_with_has_many_relationship
321321
post '/people', params:
322322
{
323323
'data' => {
@@ -380,7 +380,7 @@ def test_post_polymorphic_invalid_with_wrong_type
380380
assert_jsonapi_response 400, msg: "Submitting a thing as a vehicle should raise a type mismatch error"
381381
end
382382

383-
def test_post_polymorphic_invalid_with_not_matched_type_and_id
383+
def test_post_sti_invalid_with_not_matched_type_and_id
384384
post '/people', params:
385385
{
386386
'data' => {
@@ -405,7 +405,7 @@ def test_post_polymorphic_invalid_with_not_matched_type_and_id
405405
'Accept' => JSONAPI::MEDIA_TYPE
406406
}
407407

408-
assert_jsonapi_response 404, msg: "Submitting a thing as a vehicle should raise a record not found"
408+
assert_jsonapi_response 404, msg: "Submitting a vehicle should raise a record not found if the type does not match"
409409
end
410410

411411
def test_post_single_missing_data_contents
@@ -680,7 +680,7 @@ def test_patch_polymorphic_invalid_with_wrong_type
680680
assert_jsonapi_response 400, msg: "Submitting a thing as a vehicle should raise a type mismatch error"
681681
end
682682

683-
def test_patch_polymorphic_invalid_with_not_matched_type_and_id
683+
def test_patch_sti_invalid_with_not_matched_type_and_id
684684
patch '/people/1000', params:
685685
{
686686
'data' => {

‎test/unit/serializer/serializer_test.rb

+12
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,12 @@ def test_serializer_include
323323
self: '/people/1001/relationships/expenseEntries',
324324
related: '/people/1001/expenseEntries'
325325
}
326+
},
327+
favoriteVehicle: {
328+
links: {
329+
self: '/people/1001/relationships/favoriteVehicle',
330+
related: '/people/1001/favoriteVehicle'
331+
}
326332
}
327333
}
328334
}
@@ -455,6 +461,12 @@ def test_serializer_key_format
455461
self: '/people/1001/relationships/expenseEntries',
456462
related: '/people/1001/expenseEntries'
457463
}
464+
},
465+
favorite_vehicle: {
466+
links: {
467+
self: '/people/1001/relationships/favoriteVehicle',
468+
related: '/people/1001/favoriteVehicle'
469+
}
458470
}
459471
}
460472
}

0 commit comments

Comments
 (0)
Please sign in to comment.