diff --git a/lib/jsonapi/active_relation/join_manager.rb b/lib/jsonapi/active_relation/join_manager.rb index 3d1ec34b5..4258deb6b 100644 --- a/lib/jsonapi/active_relation/join_manager.rb +++ b/lib/jsonapi/active_relation/join_manager.rb @@ -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} @@ -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 diff --git a/lib/jsonapi/active_relation_resource.rb b/lib/jsonapi/active_relation_resource.rb index e2611613f..aa4643018 100644 --- a/lib/jsonapi/active_relation_resource.rb +++ b/lib/jsonapi/active_relation_resource.rb @@ -71,7 +71,12 @@ def find_by_keys(keys, options = {}) # @param keys [Array] 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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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]) @@ -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| @@ -495,7 +536,6 @@ def find_related_monomorphic_fragments(source_rids, relationship, options, conne end end end - fragments end @@ -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 : {} @@ -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, @@ -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 @@ -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 @@ -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, @@ -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 diff --git a/lib/jsonapi/basic_resource.rb b/lib/jsonapi/basic_resource.rb index ea8b19ea7..29b36110b 100644 --- a/lib/jsonapi/basic_resource.rb +++ b/lib/jsonapi/basic_resource.rb @@ -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| diff --git a/lib/jsonapi/formatter.rb b/lib/jsonapi/formatter.rb index 6f2922b57..67385596d 100644 --- a/lib/jsonapi/formatter.rb +++ b/lib/jsonapi/formatter.rb @@ -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 diff --git a/lib/jsonapi/link_builder.rb b/lib/jsonapi/link_builder.rb index 6ede8a022..f0e957962 100644 --- a/lib/jsonapi/link_builder.rb +++ b/lib/jsonapi/link_builder.rb @@ -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" @@ -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) diff --git a/lib/jsonapi/resource_controller_metal.rb b/lib/jsonapi/resource_controller_metal.rb index c950e4659..8166317d4 100644 --- a/lib/jsonapi/resource_controller_metal.rb +++ b/lib/jsonapi/resource_controller_metal.rb @@ -5,7 +5,6 @@ class ResourceControllerMetal < ActionController::Metal ActionController::Rendering, ActionController::Renderers::All, ActionController::StrongParameters, - ActionController::ForceSSL, ActionController::Instrumentation, JSONAPI::ActsAsResourceController ].freeze diff --git a/lib/jsonapi/resource_fragment.rb b/lib/jsonapi/resource_fragment.rb index 933ad6b8e..af7bcd7a9 100644 --- a/lib/jsonapi/resource_fragment.rb +++ b/lib/jsonapi/resource_fragment.rb @@ -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 diff --git a/lib/jsonapi/resource_identity.rb b/lib/jsonapi/resource_identity.rb index 72635ecb4..4dda6f047 100644 --- a/lib/jsonapi/resource_identity.rb +++ b/lib/jsonapi/resource_identity.rb @@ -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) @@ -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 diff --git a/lib/jsonapi/resource_serializer.rb b/lib/jsonapi/resource_serializer.rb index c7adb6c18..793071ccc 100644 --- a/lib/jsonapi/resource_serializer.rb +++ b/lib/jsonapi/resource_serializer.rb @@ -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)}) @@ -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 diff --git a/lib/jsonapi/resource_set.rb b/lib/jsonapi/resource_set.rb index b1fb136bf..0585e98fc 100644 --- a/lib/jsonapi/resource_set.rb +++ b/lib/jsonapi/resource_set.rb @@ -153,15 +153,23 @@ def flatten_resource_id_tree(resource_id_tree, flattened_tree = {}) resource_klass = resource_rid.resource_klass id = resource_rid.id + custom_id = resource_rid.custom_id + flattened_tree[resource_klass] ||= {} - flattened_tree[resource_klass][id] ||= {primary: fragment.primary, relationships: {}} - flattened_tree[resource_klass][id][:cache_id] ||= fragment.cache + if custom_id + flattened_tree[resource_klass][custom_id] ||= {primary: fragment.primary, relationships: {}} + flattened_tree[resource_klass][custom_id][:cache_id] ||= fragment.custom_cache + else + flattened_tree[resource_klass][id] ||= {primary: fragment.primary, relationships: {}} + flattened_tree[resource_klass][id][:cache_id] ||= fragment.cache + end fragment.related.try(:each_pair) do |relationship_name, related_rids| - flattened_tree[resource_klass][id][:relationships][relationship_name] ||= Set.new - flattened_tree[resource_klass][id][:relationships][relationship_name].merge(related_rids) + final_key = custom_id ? custom_id : id + flattened_tree[resource_klass][final_key][:relationships][relationship_name] ||= Set.new + flattened_tree[resource_klass][final_key][:relationships][relationship_name].merge(related_rids) end end