Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add resource_key_type feature #1345

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
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
7 changes: 6 additions & 1 deletion lib/jsonapi/active_relation/join_manager.rb
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,10 @@ def perform_joins(records, options)
relationship: relationship,
options: options)
}
unless join_node
warn "join node for #{related_resource_klass._type} not found"
next
end

details = {alias: self.class.alias_from_arel_node(join_node), join_type: join_type}

Expand Down Expand Up @@ -234,7 +238,8 @@ def process_path_to_tree(path_segments, resource_klass, default_join_type, defau
process_all_types = !segment.path_specified_resource_klass?

if process_all_types || related_resource_klass == segment.resource_klass
related_resource_tree = process_path_to_tree(path_segments.dup, related_resource_klass, default_join_type, default_polymorphic_join_type)
# Prefer using `segment.resource_klass` insead of `related_resource_klass` to avoid inheritance issue
related_resource_tree = process_path_to_tree(path_segments.dup, segment.resource_klass, default_join_type, default_polymorphic_join_type)
node[:resource_klasses][resource_klass][:relationships][segment.relationship].deep_merge!(related_resource_tree)
end
end
Expand Down
87 changes: 68 additions & 19 deletions lib/jsonapi/active_relation_resource.rb
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,12 @@ def find_by_keys(keys, options = {})
# @param keys [Array<key>] Array of primary keys to find resources for
# @option options [Hash] :context The context of the request, set in the controller
def find_to_populate_by_keys(keys, options = {})
records = records_for_populate(options).where(_primary_key => keys)
records =
if resource_key_type == :id
records_for_populate(options).where(_primary_key => keys)
else
records_for_populate(options).where(resource_key_type => keys)
end
resources_for(records, options[:context])
end

Expand Down Expand Up @@ -115,6 +120,10 @@ def find_fragments(filters, options = {})
resource_table_alias = resource_klass._table_name

pluck_fields = [Arel.sql("#{concat_table_field(resource_table_alias, resource_klass._primary_key)} AS #{resource_table_alias}_#{resource_klass._primary_key}")]

if resource_klass.resource_key_type != :id
pluck_fields << Arel.sql("#{concat_table_field(resource_table_alias, resource_klass.resource_key_type)} AS #{resource_table_alias}_#{resource_klass.resource_key_type}")
end

cache_field = attribute_to_model_field(:_cache_field) if options[:cache]
if cache_field
Expand All @@ -133,6 +142,7 @@ def find_fragments(filters, options = {})

linkage_table_alias = join_manager.join_details_by_polymorphic_relationship(linkage_relationship, resource_type)[:alias]
primary_key = klass._primary_key
# TODO: maybe an update here too for `custom` key?
pluck_fields << Arel.sql("#{concat_table_field(linkage_table_alias, primary_key)} AS #{linkage_table_alias}_#{primary_key}")
end
else
Expand All @@ -141,6 +151,7 @@ def find_fragments(filters, options = {})

linkage_table_alias = join_manager.join_details_by_relationship(linkage_relationship)[:alias]
primary_key = klass._primary_key
# TODO: maybe an update here too for `custom` key?
pluck_fields << Arel.sql("#{concat_table_field(linkage_table_alias, primary_key)} AS #{linkage_table_alias}_#{primary_key}")
end
end
Expand All @@ -161,13 +172,18 @@ def find_fragments(filters, options = {})
fragments = {}
rows = records.pluck(*pluck_fields)
rows.each do |row|
rid = JSONAPI::ResourceIdentity.new(resource_klass, pluck_fields.length == 1 ? row : row[0])
nb_ids = resource_klass.resource_key_type == 'id' ? 1 : 2
rid = JSONAPI::ResourceIdentity.new(resource_klass, *row.first(nb_ids))

fragments[rid] ||= JSONAPI::ResourceFragment.new(rid)
attributes_offset = 1

if cache_field
fragments[rid].cache = cast_to_attribute_type(row[1], cache_field[:type])
unless resource_klass.resource_key_type == 'id'
fragments[rid].custom_cache = cast_to_attribute_type(row[2], cache_field[:type])
attributes_offset+= 1
end
attributes_offset+= 1
end

Expand Down Expand Up @@ -245,13 +261,20 @@ def count_related(source_rid, relationship_name, options = {})
records = apply_request_settings_to_records(records: records(options),
resource_klass: related_klass,
primary_keys: source_rid.id,
custom_primary_keys: source_rids.custom_id,
join_manager: join_manager,
filters: filters,
options: options)

related_alias = join_manager.join_details_by_relationship(relationship)[:alias]

records = records.select(Arel.sql("#{concat_table_field(related_alias, related_klass._primary_key)}"))
records =
if resource_klass.resource_key_type != 'id'
records.select(Arel.sql("#{concat_table_field(related_alias, related_klass.resource_key_type)}"))
else
records.select(Arel.sql("#{concat_table_field(related_alias, related_klass._primary_key)}"))
end


count_records(records)
end
Expand Down Expand Up @@ -366,7 +389,12 @@ def to_one_relationships_for_linkage(include_related)
end

def find_record_by_key(key, options = {})
record = apply_request_settings_to_records(records: records(options), primary_keys: key, options: options).first
custom_key = nil
if resource_key_type != 'id'
custom_key = key
key = nil
end
record = apply_request_settings_to_records(records: records(options), primary_keys: key, custom_primary_keys: custom_key, options: options).first
fail JSONAPI::Exceptions::RecordNotFound.new(key) if record.nil?
record
end
Expand All @@ -378,6 +406,7 @@ def find_records_by_keys(keys, options = {})
def find_related_monomorphic_fragments(source_rids, relationship, options, connect_source_identity)
filters = options.fetch(:filters, {})
source_ids = source_rids.collect {|rid| rid.id}
custom_source_ids = source_rids.collect {|rid| rid.custom_id}

include_directives = options[:include_directives] ? options[:include_directives].include_directives : {}
resource_klass = relationship.resource_klass
Expand All @@ -401,17 +430,24 @@ def find_related_monomorphic_fragments(source_rids, relationship, options, conne
resource_klass: resource_klass,
sort_criteria: sort_criteria,
primary_keys: source_ids,
custom_primary_keys: custom_source_ids,
paginator: paginator,
filters: filters,
join_manager: join_manager,
options: options)

resource_table_alias = join_manager.join_details_by_relationship(relationship)[:alias]

pluck_fields = [
Arel.sql("#{_table_name}.#{_primary_key} AS source_id"),
Arel.sql("#{concat_table_field(resource_table_alias, resource_klass._primary_key)} AS #{resource_table_alias}_#{resource_klass._primary_key}")
]
pluck_fields = [Arel.sql("#{_table_name}.#{_primary_key} AS source_id")]
if resource_key_type != 'id'
custom_primary_key = concat_table_field(_table_name, resource_key_type)
pluck_fields << Arel.sql("#{custom_primary_key} AS #{_table_name}_#{resource_key_type}")
end
pluck_fields << Arel.sql("#{concat_table_field(resource_table_alias, resource_klass._primary_key)} AS #{resource_table_alias}_#{resource_klass._primary_key}")
if resource_klass.resource_key_type != 'id'
custom_resource_primary_key = concat_table_field(resource_table_alias, resource_klass.resource_key_type)
pluck_fields << Arel.sql("#{custom_resource_primary_key} AS #{resource_table_alias}_#{resource_klass.resource_key_type}")
end
# TODO: maybe an update here too for `custom` key?

cache_field = resource_klass.attribute_to_model_field(:_cache_field) if options[:cache]
if cache_field
Expand All @@ -430,6 +466,7 @@ def find_related_monomorphic_fragments(source_rids, relationship, options, conne

linkage_table_alias = join_manager.join_details_by_polymorphic_relationship(linkage_relationship, resource_type)[:alias]
primary_key = klass._primary_key
# TODO: maybe an update here too for `custom` key?
pluck_fields << Arel.sql("#{concat_table_field(linkage_table_alias, primary_key)} AS #{linkage_table_alias}_#{primary_key}")
end
else
Expand All @@ -438,6 +475,7 @@ def find_related_monomorphic_fragments(source_rids, relationship, options, conne

linkage_table_alias = join_manager.join_details_by_relationship(linkage_relationship)[:alias]
primary_key = klass._primary_key
# TODO: maybe an update here too for `custom` key?
pluck_fields << Arel.sql("#{concat_table_field(linkage_table_alias, primary_key)} AS #{linkage_table_alias}_#{primary_key}")
end
end
Expand All @@ -458,11 +496,14 @@ def find_related_monomorphic_fragments(source_rids, relationship, options, conne
fragments = {}
rows = records.distinct.pluck(*pluck_fields)
rows.each do |row|
rid = JSONAPI::ResourceIdentity.new(resource_klass, row[1])
field_offset = 1
field_offset += 1 if resource_key_type != 'id'
nb_after_offset = resource_klass.resource_key_type == 'id' ? 1 : 2
rid = JSONAPI::ResourceIdentity.new(resource_klass, *row[field_offset, nb_after_offset])

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

attributes_offset = 2
attributes_offset = field_offset + nb_after_offset

if cache_field
fragments[rid].cache = cast_to_attribute_type(row[attributes_offset], cache_field[:type])
Expand All @@ -474,8 +515,8 @@ def find_related_monomorphic_fragments(source_rids, relationship, options, conne
attributes_offset+= 1
end

source_rid = JSONAPI::ResourceIdentity.new(self, row[0])

source_rid = JSONAPI::ResourceIdentity.new(self, *row.first(nb_after_offset))
fragments[rid].add_related_from(source_rid)

linkage_fields.each do |linkage_field|
Expand All @@ -495,7 +536,6 @@ def find_related_monomorphic_fragments(source_rids, relationship, options, conne
end
end
end

fragments
end

Expand All @@ -504,6 +544,7 @@ def find_related_monomorphic_fragments(source_rids, relationship, options, conne
def find_related_polymorphic_fragments(source_rids, relationship, options, connect_source_identity)
filters = options.fetch(:filters, {})
source_ids = source_rids.collect {|rid| rid.id}
custom_source_ids = source_rids.collect {|rid| rid.custom_id}

resource_klass = relationship.resource_klass
include_directives = options[:include_directives] ? options[:include_directives].include_directives : {}
Expand Down Expand Up @@ -533,6 +574,7 @@ def find_related_polymorphic_fragments(source_rids, relationship, options, conne
resource_klass: resource_klass,
sort_primary: true,
primary_keys: source_ids,
custom_primary_keys: custom_source_ids,
paginator: paginator,
filters: filters,
join_manager: join_manager,
Expand All @@ -542,11 +584,13 @@ def find_related_polymorphic_fragments(source_rids, relationship, options, conne
related_key = concat_table_field(_table_name, relationship.foreign_key)
related_type = concat_table_field(_table_name, relationship.polymorphic_type)

pluck_fields = [
Arel.sql("#{primary_key} AS #{_table_name}_#{_primary_key}"),
Arel.sql("#{related_key} AS #{_table_name}_#{relationship.foreign_key}"),
Arel.sql("#{related_type} AS #{_table_name}_#{relationship.polymorphic_type}")
]
pluck_fields = [Arel.sql("#{primary_key} AS #{_table_name}_#{_primary_key}")]
if resource_klass.resource_key_type != 'id'
custom_primary_key = concat_table_field(_table_name, resource_key_type)
pluck_fields << Arel.sql("#{custom_primary_key} AS #{_table_name}_#{resource_key_type}")
end
pluck_fields << Arel.sql("#{related_key} AS #{_table_name}_#{relationship.foreign_key}")
pluck_fields << Arel.sql("#{related_type} AS #{_table_name}_#{relationship.polymorphic_type}")

# Get the additional fields from each relation. There's a limitation that the fields must exist in each relation

Expand Down Expand Up @@ -616,6 +660,7 @@ def find_related_polymorphic_fragments(source_rids, relationship, options, conne

linkage_table_alias = join_manager.join_details_by_polymorphic_relationship(linkage_relationship, resource_type)[:alias]
primary_key = klass._primary_key
# TODO: maybe an update here too for `custom` key?
pluck_fields << Arel.sql("#{concat_table_field(linkage_table_alias, primary_key)} AS #{linkage_table_alias}_#{primary_key}")
end
else
Expand Down Expand Up @@ -685,6 +730,7 @@ def apply_request_settings_to_records(records:,
resource_klass: self,
filters: {},
primary_keys: nil,
custom_primary_keys: nil,
sort_criteria: nil,
sort_primary: nil,
paginator: nil,
Expand All @@ -696,8 +742,11 @@ def apply_request_settings_to_records(records:,

if primary_keys
records = records.where(_primary_key => primary_keys)
elsif custom_primary_keys
records = records.where(resource_key_type => custom_primary_keys)
end


unless filters.empty?
records = resource_klass.filter_records(records, filters, options)
end
Expand Down
2 changes: 1 addition & 1 deletion lib/jsonapi/basic_resource.rb
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,7 @@ def _replace_to_many_links(relationship_type, relationship_key_values, options)
if reflect
existing_rids = self.class.find_related_fragments([identity], relationship_type, options)

existing = existing_rids.keys.collect { |rid| rid.id }
existing = existing_rids.keys.collect { |rid| rid.custom_id || rid.id }

to_delete = existing - (relationship_key_values & existing)
to_delete.each do |key|
Expand Down
11 changes: 11 additions & 0 deletions lib/jsonapi/formatter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,17 @@ def uncached
end

class UnderscoredKeyFormatter < JSONAPI::KeyFormatter
class << self
def format(arg)
# take care of modules
arg.to_s.split('/').last
end

def unformat(arg, modules: [])
# take care of modules
modules.push(arg.to_s).join('/')
end
end
end

class CamelizedKeyFormatter < JSONAPI::KeyFormatter
Expand Down
8 changes: 6 additions & 2 deletions lib/jsonapi/link_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def engine?
def primary_resources_url
if @primary_resource_klass._routed
primary_resources_path = resources_path(primary_resource_klass)
@primary_resources_url_cached ||= "#{ base_url }#{ engine_mount_point }#{ primary_resources_path }"
@primary_resources_url_cached ||= "#{ base_url }#{ serialized_engine_mount_point }#{ primary_resources_path }"
else
if JSONAPI.configuration.warn_on_missing_routes && !@primary_resource_klass._warned_missing_route
warn "primary_resources_url for #{@primary_resource_klass} could not be generated"
Expand Down Expand Up @@ -136,7 +136,11 @@ def resource_path(source)
end

def resource_url(source)
"#{ base_url }#{ engine_mount_point }#{ resource_path(source) }"
"#{ base_url }#{ serialized_engine_mount_point }#{ resource_path(source) }"
end

def serialized_engine_mount_point
engine_mount_point == "/" ? "" : engine_mount_point
end

def route_for_relationship(relationship)
Expand Down
1 change: 0 additions & 1 deletion lib/jsonapi/resource_controller_metal.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ class ResourceControllerMetal < ActionController::Metal
ActionController::Rendering,
ActionController::Renderers::All,
ActionController::StrongParameters,
ActionController::ForceSSL,
ActionController::Instrumentation,
JSONAPI::ActsAsResourceController
].freeze
Expand Down
3 changes: 2 additions & 1 deletion lib/jsonapi/resource_fragment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,14 @@ module JSONAPI
class ResourceFragment
attr_reader :identity, :attributes, :related_from, :related

attr_accessor :primary, :cache
attr_accessor :primary, :cache, :custom_cache

alias :cache_field :cache #ToDo: Rename one or the other

def initialize(identity)
@identity = identity
@cache = nil
@custom_cache = nil
@attributes = {}
@related = {}
@primary = false
Expand Down
11 changes: 6 additions & 5 deletions lib/jsonapi/resource_identity.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@ module JSONAPI
# rid = ResourceIdentity.new(PostResource, 12)
#
class ResourceIdentity
attr_reader :resource_klass, :id
attr_reader :resource_klass, :id, :custom_id

def initialize(resource_klass, id)
def initialize(resource_klass, id, custom_id = nil)
@resource_klass = resource_klass
@id = id
@custom_id = custom_id
end

def ==(other)
Expand All @@ -25,17 +26,17 @@ def ==(other)
end

def eql?(other)
other.is_a?(ResourceIdentity) && other.resource_klass == @resource_klass && other.id == @id
other.is_a?(ResourceIdentity) && other.resource_klass == @resource_klass && other.id == @id && other.custom_id == @custom_id
end

def hash
[@resource_klass, @id].hash
[@resource_klass, @id, @custom_id].hash
end

# Creates a string representation of the identifier.
def to_s
# :nocov:
"#{resource_klass}:#{id}"
[resource_klass, id, custom_id].compact.join(':')
# :nocov:
end
end
Expand Down
4 changes: 2 additions & 2 deletions lib/jsonapi/resource_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,7 @@ def to_many_linkage(rids)
linkage = []

rids && rids.each do |details|
id = details.id
id = details.custom_id || details.id
type = details.resource_klass.try(:_type)
if type && id
linkage.append({'type' => format_key(type), 'id' => @id_formatter.format(id)})
Expand All @@ -346,7 +346,7 @@ def to_one_linkage(rid)

{
'type' => format_key(rid.resource_klass._type),
'id' => @id_formatter.format(rid.id),
'id' => @id_formatter.format(rid.custom_id || rid.id),
}
end

Expand Down
Loading