Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
V0.11 refactor resource classes to modules (#1406)
Browse files Browse the repository at this point in the history
* Restore previous include directives behavior

* Default sort use _primary_key

* Remove support for pluck attributes

* Pass relationship instead of relationship name

* Update copyright date

* Ignore docker-compose override files

* add _relation_name method

* Rework resource class to support using modules for retrieving resources by way of a `resource_retrieval_strategy`

Removes BasicResource class and replaces ActiveRelationResource with a module

* Use `_relationship` helper method

* Add ActiveRelationRetrieval

Allows retrieval of resources by querying the primary table and joining the source table - the opposite of the v10 version

* Skip extra pluck queries when not caching a resource

* Test Cleanup

* Adjust tested query counts based on default_resource_retrieval_strategy

* create_implicit_polymorphic_type_relationships

* Add ActiveRelationRetrievalV09

* Move resource down in the load order

* Use underscore instead of downcase

* Refactor Resource to load retrieval strategy as class loads

* Simplify loading resource retrieval strategy modules

Add SimpleResource that does not load a resource retrieval strategy module

* Remove no longer need deferred_relationship code

* Add warning about potentially unused `records_for_populate`

* Rework loading the resource_retrieval_strategy to fix issue in real projects

* Use SortedSets for resource_identities

* Add sorted_set gem

* Remove rails 5 support
lgebhardt authored Sep 19, 2023
1 parent 3cd93a2 commit a2fc02f
Showing 42 changed files with 5,690 additions and 3,056 deletions.
14 changes: 0 additions & 14 deletions .github/workflows/ruby.yml
Original file line number Diff line number Diff line change
@@ -47,35 +47,21 @@ jobs:
- '7.0'
- '6.1'
- '6.0'
- '5.2'
- '5.1'
database_url:
- sqlite3:test_db
- postgresql://postgres:password@localhost:5432/test
- mysql2://root:[email protected]:3306/test
exclude:
- ruby: '3.2'
rails: '6.0'
- ruby: '3.2'
rails: '5.2'
- ruby: '3.2'
rails: '5.1'
- ruby: '3.1'
rails: '6.0'
- ruby: '3.1'
rails: '5.2'
- ruby: '3.1'
rails: '5.1'
- ruby: '3.0'
rails: '6.0'
- ruby: '3.0'
rails: '5.2'
- ruby: '3.0'
rails: '5.1'
- ruby: '2.6'
rails: '7.0'
- database_url: postgresql://postgres:password@localhost:5432/test
rails: '5.1'
env:
RAILS_VERSION: ${{ matrix.rails }}
DATABASE_URL: ${{ matrix.database_url }}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -23,3 +23,4 @@ test_db
test_db-journal
.idea
*.iml
*.override.yml
2 changes: 1 addition & 1 deletion LICENSE.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Copyright (c) 2014-2021 Cerebris Corporation
Copyright (c) 2014-2023 Cerebris Corporation

MIT License

2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -110,4 +110,4 @@ and **paste the content into the issue description or attach as a file**:

## License

Copyright 2014-2021 Cerebris Corporation. MIT License (see LICENSE for details).
Copyright 2014-2023 Cerebris Corporation. MIT License (see LICENSE for details).
2 changes: 2 additions & 0 deletions jsonapi-resources.gemspec
Original file line number Diff line number Diff line change
@@ -27,7 +27,9 @@ Gem::Specification.new do |spec|
spec.add_development_dependency 'pry'
spec.add_development_dependency 'concurrent-ruby-ext'
spec.add_development_dependency 'database_cleaner'
spec.add_development_dependency 'hashie'
spec.add_dependency 'activerecord', '>= 5.1'
spec.add_dependency 'railties', '>= 5.1'
spec.add_dependency 'concurrent-ruby'
spec.add_dependency 'sorted_set'
end
8 changes: 6 additions & 2 deletions lib/jsonapi-resources.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
require 'jsonapi/resources/railtie'
require 'jsonapi/naive_cache'
require 'jsonapi/compiled_json'
require 'jsonapi/basic_resource'
require 'jsonapi/active_relation_resource'
require 'jsonapi/active_relation_retrieval'
require 'jsonapi/active_relation_retrieval_v09'
require 'jsonapi/active_relation_retrieval_v10'
require 'jsonapi/resource_common'
require 'jsonapi/resource'
require 'jsonapi/simple_resource'
require 'jsonapi/cached_response_fragment'
require 'jsonapi/response_document'
require 'jsonapi/acts_as_resource_controller'
@@ -35,6 +38,7 @@
require 'jsonapi/link_builder'
require 'jsonapi/active_relation/adapters/join_left_active_record_adapter'
require 'jsonapi/active_relation/join_manager'
require 'jsonapi/active_relation/join_manager_v10'
require 'jsonapi/resource_identity'
require 'jsonapi/resource_fragment'
require 'jsonapi/resource_tree'
38 changes: 27 additions & 11 deletions lib/jsonapi/active_relation/join_manager.rb
Original file line number Diff line number Diff line change
@@ -7,17 +7,22 @@ class JoinManager
attr_reader :resource_klass,
:source_relationship,
:resource_join_tree,
:join_details
:join_details,
:through_source

def initialize(resource_klass:,
source_relationship: nil,
source_resource_klass: nil,
through_source: false,
relationships: nil,
filters: nil,
sort_criteria: nil)

@resource_klass = resource_klass
@source_resource_klass = source_resource_klass
@join_details = nil
@collected_aliases = Set.new
@through_source = through_source

@resource_join_tree = {
root: {
@@ -45,7 +50,7 @@ def join(records, options)
# this method gets the join details whether they are on a relationship or are just pseudo details for the base
# resource. Specify the resource type for polymorphic relationships
#
def source_join_details(type=nil)
def source_join_details(type = nil)
if source_relationship
related_resource_klass = type ? resource_klass.resource_klass_for(type) : source_relationship.resource_klass
segment = PathSegment::Relationship.new(relationship: source_relationship, resource_klass: related_resource_klass)
@@ -90,14 +95,20 @@ def self.get_join_arel_node(records, options = {})
end

def self.alias_from_arel_node(node)
case node.left
# case node.left
case node&.left
when Arel::Table
node.left.name
when Arel::Nodes::TableAlias
node.left.right
when Arel::Nodes::StringJoin
# :nocov:
warn "alias_from_arel_node: Unsupported join type - use custom filtering and sorting"
warn "alias_from_arel_node: Unsupported join type `Arel::Nodes::StringJoin` - use custom filtering and sorting"
nil
# :nocov:
else
# :nocov:
warn "alias_from_arel_node: Unsupported join type `#{node&.left.to_s}`"
nil
# :nocov:
end
@@ -163,7 +174,8 @@ def perform_joins(records, options)
options: options)
}

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

if relationship == source_relationship
if relationship.polymorphic? && relationship.belongs_to?
@@ -175,15 +187,19 @@ def perform_joins(records, options)

# We're adding the source alias with two keys. We only want the check for duplicate aliases once.
# See the note in `add_join_details`.
check_for_duplicate_alias = !(relationship == source_relationship)
add_join_details(PathSegment::Relationship.new(relationship: relationship, resource_klass: related_resource_klass), details, check_for_duplicate_alias)
check_for_duplicate_alias = relationship != source_relationship
path_segment = PathSegment::Relationship.new(relationship: relationship,
resource_klass: related_resource_klass)

add_join_details(path_segment, details, check_for_duplicate_alias)
end
end
records
end

def add_join(path, default_type = :inner, default_polymorphic_join_type = :left)
if source_relationship
# puts "add_join #{path} default_type=#{default_type} default_polymorphic_join_type=#{default_polymorphic_join_type}"
if source_relationship && through_source
if source_relationship.polymorphic?
# Polymorphic paths will come it with the resource_type as the first segment (for example `#documents.comments`)
# We just need to prepend the relationship portion the
@@ -195,9 +211,9 @@ def add_join(path, default_type = :inner, default_polymorphic_join_type = :left)
sourced_path = path
end

join_manager, _field = parse_path_to_tree(sourced_path, resource_klass, default_type, default_polymorphic_join_type)
join_tree, _field = parse_path_to_tree(sourced_path, resource_klass, default_type, default_polymorphic_join_type)

@resource_join_tree[:root].deep_merge!(join_manager) { |key, val, other_val|
@resource_join_tree[:root].deep_merge!(join_tree) { |key, val, other_val|
if key == :join_type
if val == other_val
val
@@ -294,4 +310,4 @@ def add_relationships(relationships)
end
end
end
end
end
297 changes: 297 additions & 0 deletions lib/jsonapi/active_relation/join_manager_v10.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,297 @@
module JSONAPI
module ActiveRelation

# Stores relationship paths starting from the resource_klass, consolidating duplicate paths from
# relationships, filters and sorts. When joins are made the table aliases are tracked in join_details
class JoinManagerV10
attr_reader :resource_klass,
:source_relationship,
:resource_join_tree,
:join_details

def initialize(resource_klass:,
source_relationship: nil,
relationships: nil,
filters: nil,
sort_criteria: nil)

@resource_klass = resource_klass
@join_details = nil
@collected_aliases = Set.new

@resource_join_tree = {
root: {
join_type: :root,
resource_klasses: {
resource_klass => {
relationships: {}
}
}
}
}
add_source_relationship(source_relationship)
add_sort_criteria(sort_criteria)
add_filters(filters)
add_relationships(relationships)
end

def join(records, options)
fail "can't be joined again" if @join_details
@join_details = {}
perform_joins(records, options)
end

# source details will only be on a relationship if the source_relationship is set
# this method gets the join details whether they are on a relationship or are just pseudo details for the base
# resource. Specify the resource type for polymorphic relationships
#
def source_join_details(type=nil)
if source_relationship
related_resource_klass = type ? resource_klass.resource_klass_for(type) : source_relationship.resource_klass
segment = PathSegment::Relationship.new(relationship: source_relationship, resource_klass: related_resource_klass)
details = @join_details[segment]
else
if type
details = @join_details["##{type}"]
else
details = @join_details['']
end
end
details
end

def join_details_by_polymorphic_relationship(relationship, type)
segment = PathSegment::Relationship.new(relationship: relationship, resource_klass: resource_klass.resource_klass_for(type))
@join_details[segment]
end

def join_details_by_relationship(relationship)
segment = PathSegment::Relationship.new(relationship: relationship, resource_klass: relationship.resource_klass)
@join_details[segment]
end

def self.get_join_arel_node(records, options = {})
init_join_sources = records.arel.join_sources
init_join_sources_length = init_join_sources.length

records = yield(records, options)

join_sources = records.arel.join_sources
if join_sources.length > init_join_sources_length
last_join = (join_sources - init_join_sources).last
else
# :nocov:
warn "get_join_arel_node: No join added"
last_join = nil
# :nocov:
end

return records, last_join
end

def self.alias_from_arel_node(node)
case node.left
when Arel::Table
node.left.name
when Arel::Nodes::TableAlias
node.left.right
when Arel::Nodes::StringJoin
# :nocov:
warn "alias_from_arel_node: Unsupported join type - use custom filtering and sorting"
nil
# :nocov:
end
end

private

def flatten_join_tree_by_depth(join_array = [], node = @resource_join_tree, level = 0)
join_array[level] = [] unless join_array[level]

node.each do |relationship, relationship_details|
relationship_details[:resource_klasses].each do |related_resource_klass, resource_details|
join_array[level] << { relationship: relationship,
relationship_details: relationship_details,
related_resource_klass: related_resource_klass}
flatten_join_tree_by_depth(join_array, resource_details[:relationships], level+1)
end
end
join_array
end

def add_join_details(join_key, details, check_for_duplicate_alias = true)
fail "details already set" if @join_details.has_key?(join_key)
@join_details[join_key] = details

# Joins are being tracked as they are added to the built up relation. If the same table is added to a
# relation more than once subsequent versions will be assigned an alias. Depending on the order the joins
# are made the computed aliases may change. The order this library performs the joins was chosen
# to prevent this. However if the relation is reordered it should result in reusing on of the earlier
# aliases (in this case a plain table name). The following check will catch this an raise an exception.
# An exception is appropriate because not using the correct alias could leak data due to filters and
# applied permissions being performed on the wrong data.
if check_for_duplicate_alias && @collected_aliases.include?(details[:alias])
fail "alias '#{details[:alias]}' has already been added. Possible relation reordering"
end

@collected_aliases << details[:alias]
end

def perform_joins(records, options)
join_array = flatten_join_tree_by_depth

join_array.each do |level_joins|
level_joins.each do |join_details|
relationship = join_details[:relationship]
relationship_details = join_details[:relationship_details]
related_resource_klass = join_details[:related_resource_klass]
join_type = relationship_details[:join_type]

if relationship == :root
unless source_relationship
add_join_details('', {alias: resource_klass._table_name, join_type: :root})
end
next
end

records, join_node = self.class.get_join_arel_node(records, options) {|records, options|
related_resource_klass.join_relationship(
records: records,
resource_type: related_resource_klass._type,
join_type: join_type,
relationship: relationship,
options: options)
}

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

if relationship == source_relationship
if relationship.polymorphic? && relationship.belongs_to?
add_join_details("##{related_resource_klass._type}", details)
else
add_join_details('', details)
end
end

# We're adding the source alias with two keys. We only want the check for duplicate aliases once.
# See the note in `add_join_details`.
check_for_duplicate_alias = !(relationship == source_relationship)
add_join_details(PathSegment::Relationship.new(relationship: relationship, resource_klass: related_resource_klass), details, check_for_duplicate_alias)
end
end
records
end

def add_join(path, default_type = :inner, default_polymorphic_join_type = :left)
if source_relationship
if source_relationship.polymorphic?
# Polymorphic paths will come it with the resource_type as the first segment (for example `#documents.comments`)
# We just need to prepend the relationship portion the
sourced_path = "#{source_relationship.name}#{path}"
else
sourced_path = "#{source_relationship.name}.#{path}"
end
else
sourced_path = path
end

join_manager, _field = parse_path_to_tree(sourced_path, resource_klass, default_type, default_polymorphic_join_type)

@resource_join_tree[:root].deep_merge!(join_manager) { |key, val, other_val|
if key == :join_type
if val == other_val
val
else
:inner
end
end
}
end

def process_path_to_tree(path_segments, resource_klass, default_join_type, default_polymorphic_join_type)
node = {
resource_klasses: {
resource_klass => {
relationships: {}
}
}
}

segment = path_segments.shift

if segment.is_a?(PathSegment::Relationship)
node[:resource_klasses][resource_klass][:relationships][segment.relationship] ||= {}

# join polymorphic as left joins
node[:resource_klasses][resource_klass][:relationships][segment.relationship][:join_type] ||=
segment.relationship.polymorphic? ? default_polymorphic_join_type : default_join_type

segment.relationship.resource_types.each do |related_resource_type|
related_resource_klass = resource_klass.resource_klass_for(related_resource_type)

# If the resource type was specified in the path segment we want to only process the next segments for
# that resource type, otherwise process for all
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)
node[:resource_klasses][resource_klass][:relationships][segment.relationship].deep_merge!(related_resource_tree)
end
end
end
node
end

def parse_path_to_tree(path_string, resource_klass, default_join_type = :inner, default_polymorphic_join_type = :left)
path = JSONAPI::Path.new(resource_klass: resource_klass, path_string: path_string)

field = path.segments[-1]
return process_path_to_tree(path.segments, resource_klass, default_join_type, default_polymorphic_join_type), field
end

def add_source_relationship(source_relationship)
@source_relationship = source_relationship

if @source_relationship
resource_klasses = {}
source_relationship.resource_types.each do |related_resource_type|
related_resource_klass = resource_klass.resource_klass_for(related_resource_type)
resource_klasses[related_resource_klass] = {relationships: {}}
end

join_type = source_relationship.polymorphic? ? :left : :inner

@resource_join_tree[:root][:resource_klasses][resource_klass][:relationships][@source_relationship] = {
source: true, resource_klasses: resource_klasses, join_type: join_type
}
end
end

def add_filters(filters)
return if filters.blank?
filters.each_key do |filter|
# Do not add joins for filters with an apply callable. This can be overridden by setting perform_joins to true
next if resource_klass._allowed_filters[filter].try(:[], :apply) &&
!resource_klass._allowed_filters[filter].try(:[], :perform_joins)

add_join(filter, :left)
end
end

def add_sort_criteria(sort_criteria)
return if sort_criteria.blank?

sort_criteria.each do |sort|
add_join(sort[:field], :left)
end
end

def add_relationships(relationships)
return if relationships.blank?
relationships.each do |relationship|
add_join(relationship, :left)
end
end
end
end
end
883 changes: 883 additions & 0 deletions lib/jsonapi/active_relation_retrieval.rb

Large diffs are not rendered by default.

713 changes: 713 additions & 0 deletions lib/jsonapi/active_relation_retrieval_v09.rb

Large diffs are not rendered by default.

Large diffs are not rendered by default.

30 changes: 19 additions & 11 deletions lib/jsonapi/configuration.rb
Original file line number Diff line number Diff line change
@@ -39,7 +39,8 @@ class Configuration
:default_resource_cache_field,
:resource_cache_digest_function,
:resource_cache_usage_report_function,
:default_exclude_links
:default_exclude_links,
:default_resource_retrieval_strategy

def initialize
#:underscored_key, :camelized_key, :dasherized_key, or custom
@@ -158,6 +159,21 @@ def initialize
# and relationships. Accepts either `:default`, `:none`, or array containing the
# specific default links to exclude, which may be `:self` and `:related`.
self.default_exclude_links = :none

# Global configuration for resource retrieval strategy used by the Resource class.
# Selecting a default_resource_retrieval_strategy will affect all resources that derive from
# Resource. The default value is 'JSONAPI::ActiveRelationRetrieval'.
#
# To use multiple retrieval strategies in an app set this to :none and set a custom retrieval strategy
# per resource (or base resource) using the class method `load_resource_retrieval_strategy`.
#
# Available strategies:
# 'JSONAPI::ActiveRelationRetrieval'
# 'JSONAPI::ActiveRelationRetrievalV09'
# 'JSONAPI::ActiveRelationRetrievalV10'
# :none
# :self
self.default_resource_retrieval_strategy = 'JSONAPI::ActiveRelationRetrieval'
end

def cache_formatters=(bool)
@@ -244,16 +260,6 @@ def allow_include=(allow_include)
@default_allow_include_to_many = allow_include
end

def whitelist_all_exceptions=(allow_all_exceptions)
ActiveSupport::Deprecation.warn('`whitelist_all_exceptions` has been replaced by `allow_all_exceptions`')
@allow_all_exceptions = allow_all_exceptions
end

def exception_class_whitelist=(exception_class_allowlist)
ActiveSupport::Deprecation.warn('`exception_class_whitelist` has been replaced by `exception_class_allowlist`')
@exception_class_allowlist = exception_class_allowlist
end

attr_writer :allow_sort, :allow_filter, :default_allow_include_to_one, :default_allow_include_to_many

attr_writer :default_paginator
@@ -309,6 +315,8 @@ def exception_class_whitelist=(exception_class_allowlist)
attr_writer :resource_cache_usage_report_function

attr_writer :default_exclude_links

attr_writer :default_resource_retrieval_strategy
end

class << self
94 changes: 75 additions & 19 deletions lib/jsonapi/include_directives.rb
Original file line number Diff line number Diff line change
@@ -4,46 +4,102 @@ class IncludeDirectives
# For example ['posts.comments.tags']
# will transform into =>
# {
# posts: {
# include_related: {
# comments:{
# include_related: {
# tags: {
# include_related: {}
# }
# include_related: {
# posts: {
# include: true,
# include_related: {
# comments: {
# include: true,
# include_related: {
# tags: {
# include: true,
# include_related: {},
# include_in_join: true
# }
# },
# include_in_join: true
# }
# }
# },
# include_in_join: true
# }
# }
# }

def initialize(resource_klass, includes_array)
def initialize(resource_klass, includes_array, force_eager_load: false)
@resource_klass = resource_klass
@force_eager_load = force_eager_load
@include_directives_hash = { include_related: {} }
includes_array.each do |include|
parse_include(include)
end
end

def include_directives
@include_directives_hash
end

def [](name)
@include_directives_hash[name]
end

private
def model_includes
get_includes(@include_directives_hash)
end

def parse_include(include)
path = JSONAPI::Path.new(resource_klass: @resource_klass,
path_string: include,
ensure_default_field: false,
parse_fields: false)
private

def get_related(current_path)
current = @include_directives_hash
current_resource_klass = @resource_klass
current_path.split('.').each do |fragment|
fragment = fragment.to_sym

if current_resource_klass
current_relationship = current_resource_klass._relationship(fragment)
current_resource_klass = current_relationship.try(:resource_klass)
else
raise JSONAPI::Exceptions::InvalidInclude.new(current_resource_klass, current_path)
end

include_in_join = @force_eager_load || !current_relationship || current_relationship.eager_load_on_include

path.segments.each do |segment|
relationship_name = segment.relationship.name.to_sym
current[:include_related][fragment] ||= { include: false, include_related: {}, include_in_join: include_in_join }
current = current[:include_related][fragment]
end
current
end

def get_includes(directive, only_joined_includes = true)
ir = directive[:include_related]
ir = ir.select { |_k,v| v[:include_in_join] } if only_joined_includes

ir.map do |name, sub_directive|
sub = get_includes(sub_directive, only_joined_includes)
sub.any? ? { name => sub } : name
end
end

def parse_include(include)
parts = include.split('.')
local_path = ''

parts.each do |name|
local_path += local_path.length > 0 ? ".#{name}" : name
related = get_related(local_path)
related[:include] = true
end
end

current[:include_related][relationship_name] ||= { include_related: {} }
current = current[:include_related][relationship_name]
def delve_paths(obj)
case obj
when Array
obj.map{|elem| delve_paths(elem)}.flatten(1)
when Hash
obj.map{|k,v| [[k]] + delve_paths(v).map{|path| [k] + path } }.flatten(1)
when Symbol, String
[[obj]]
else
raise "delve_paths cannot descend into #{obj.class.name}"
end

rescue JSONAPI::Exceptions::InvalidRelationship => _e
17 changes: 11 additions & 6 deletions lib/jsonapi/processor.rb
Original file line number Diff line number Diff line change
@@ -106,6 +106,7 @@ def show
def show_relationship
parent_key = params[:parent_key]
relationship_type = params[:relationship_type].to_sym
relationship = resource_klass._relationship(relationship_type)
paginator = params[:paginator]
sort_criteria = params[:sort_criteria]
include_directives = params[:include_directives]
@@ -123,14 +124,14 @@ def show_relationship

resource_tree = find_related_resource_tree(
parent_resource,
relationship_type,
relationship,
options,
nil
)

JSONAPI::RelationshipOperationResult.new(:ok,
parent_resource,
resource_klass._relationship(relationship_type),
relationship,
resource_tree.fragments.keys,
result_options)
end
@@ -198,9 +199,11 @@ def show_related_resources
(paginator && paginator.class.requires_record_count) ||
(JSONAPI.configuration.top_level_meta_include_page_count))

relationship = source_resource.class._relationship(relationship_type)

opts[:record_count] = source_resource.class.count_related(
source_resource,
relationship_type,
relationship,
options)
end

@@ -382,11 +385,13 @@ def find_resource_tree(options, include_related)
PrimaryResourceTree.new(fragments: fragments, include_related: include_related, options: options)
end

def find_related_resource_tree(parent_resource, relationship_name, options, include_related)
def find_related_resource_tree(parent_resource, relationship, options, include_related)
options = options.except(:include_directives)
options[:cache] = resource_klass.caching?

fragments = resource_klass.find_included_fragments([parent_resource], relationship_name, options)
parent_resource_fragment = parent_resource.fragment(primary: true)

fragments = resource_klass.find_related_fragments(parent_resource_fragment, relationship, options)
PrimaryResourceTree.new(fragments: fragments, include_related: include_related, options: options)
end

@@ -396,7 +401,7 @@ def find_resource_tree_from_relationship(resource, relationship_name, options, i
options = options.except(:include_directives)
options[:cache] = relationship.resource_klass.caching?

fragments = resource.class.find_related_fragments([resource], relationship_name, options)
fragments = resource.class.find_related_fragments(resource.fragment, relationship, options)

PrimaryResourceTree.new(fragments: fragments, include_related: include_related, options: options)
end
120 changes: 86 additions & 34 deletions lib/jsonapi/relationship.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
module JSONAPI
class Relationship
attr_reader :acts_as_set, :foreign_key, :options, :name,
:class_name, :polymorphic, :always_include_optional_linkage_data,
:class_name, :polymorphic, :always_include_optional_linkage_data, :exclude_linkage_data,
:parent_resource, :eager_load_on_include, :custom_methods,
:inverse_relationship, :allow_include
:inverse_relationship, :allow_include, :hidden

attr_writer :allow_include

@@ -15,19 +15,23 @@ def initialize(name, options = {})
@acts_as_set = options.fetch(:acts_as_set, false) == true
@foreign_key = options[:foreign_key] ? options[:foreign_key].to_sym : nil
@parent_resource = options[:parent_resource]
@relation_name = options.fetch(:relation_name, @name)
@relation_name = options[:relation_name]
@polymorphic = options.fetch(:polymorphic, false) == true
@polymorphic_types = options[:polymorphic_types]
if options[:polymorphic_relations]
ActiveSupport::Deprecation.warn('Use polymorphic_types instead of polymorphic_relations')
@polymorphic_types ||= options[:polymorphic_relations]
end

@hidden = options.fetch(:hidden, false) == true

@exclude_linkage_data = options[:exclude_linkage_data]
@always_include_optional_linkage_data = options.fetch(:always_include_optional_linkage_data, false) == true
@eager_load_on_include = options.fetch(:eager_load_on_include, true) == true
@allow_include = options[:allow_include]
@class_name = nil
@inverse_relationship = nil

@inverse_relationship = options[:inverse_relationship]&.to_sym

@_routed = false
@_warned_missing_route = false
@@ -57,13 +61,27 @@ def table_name
# :nocov:
end

def inverse_relationship
unless @inverse_relationship
@inverse_relationship ||= if resource_klass._relationship(@parent_resource._type.to_s.singularize).present?
@parent_resource._type.to_s.singularize.to_sym
elsif resource_klass._relationship(@parent_resource._type).present?
@parent_resource._type.to_sym
else
nil
end
end

@inverse_relationship
end

def self.polymorphic_types(name)
@poly_hash ||= {}.tap do |hash|
ObjectSpace.each_object do |klass|
next unless Module === klass
if ActiveRecord::Base > klass
klass.reflect_on_all_associations(:has_many).select{|r| r.options[:as] }.each do |reflection|
(hash[reflection.options[:as]] ||= []) << klass.name.downcase
klass.reflect_on_all_associations(:has_many).select { |r| r.options[:as] }.each do |reflection|
(hash[reflection.options[:as]] ||= []) << klass.name.underscore
end
end
end
@@ -73,7 +91,7 @@ def self.polymorphic_types(name)

def resource_types
if polymorphic? && belongs_to?
@polymorphic_types ||= self.class.polymorphic_types(@relation_name).collect {|t| t.pluralize}
@polymorphic_types ||= self.class.polymorphic_types(_relation_name).collect { |t| t.pluralize }
else
[resource_klass._type.to_s.pluralize]
end
@@ -84,15 +102,15 @@ def type
end

def relation_name(options)
case @relation_name
when Symbol
# :nocov:
@relation_name
# :nocov:
when String
@relation_name.to_sym
when Proc
@relation_name.call(options)
case _relation_name
when Symbol
# :nocov:
_relation_name
# :nocov:
when String
_relation_name.to_sym
when Proc
_relation_name.call(options)
end
end

@@ -108,14 +126,14 @@ def readonly?

def exclude_links(exclude)
case exclude
when :default, "default"
@_exclude_links = [:self, :related]
when :none, "none"
@_exclude_links = []
when Array
@_exclude_links = exclude.collect {|link| link.to_sym}
else
fail "Invalid exclude_links"
when :default, "default"
@_exclude_links = [:self, :related]
when :none, "none"
@_exclude_links = []
when Array
@_exclude_links = exclude.collect { |link| link.to_sym }
else
fail "Invalid exclude_links"
end
end

@@ -127,6 +145,10 @@ def exclude_link?(link)
_exclude_links.include?(link.to_sym)
end

def _relation_name
@relation_name || @name
end

class ToOne < Relationship
attr_reader :foreign_key_on

@@ -135,9 +157,16 @@ def initialize(name, options = {})
@class_name = options.fetch(:class_name, name.to_s.camelize)
@foreign_key ||= "#{name}_id".to_sym
@foreign_key_on = options.fetch(:foreign_key_on, :self)
if parent_resource
@inverse_relationship = options.fetch(:inverse_relationship, parent_resource._type)
# if parent_resource
# @inverse_relationship = options.fetch(:inverse_relationship, parent_resource._type)
# end

if options.fetch(:create_implicit_polymorphic_type_relationships, true) == true && polymorphic?
# Setup the implicit relationships for the polymorphic types and exclude linkage data
setup_implicit_relationships_for_polymorphic_types
end

@polymorphic_type_relationship_for = options[:polymorphic_type_relationship_for]
end

def to_s
@@ -152,11 +181,30 @@ def belongs_to?
# :nocov:
end

def hidden?
@hidden || @polymorphic_type_relationship_for.present?
end

def polymorphic_type
"#{name}_type" if polymorphic?
end

def setup_implicit_relationships_for_polymorphic_types(exclude_linkage_data: true)
types = self.class.polymorphic_types(_relation_name)
unless types.present?
warn "No polymorphic types found for #{parent_resource.name} #{_relation_name}"
return
end

types.each do |type|
parent_resource.has_one(type.to_s.underscore.singularize,
exclude_linkage_data: exclude_linkage_data,
polymorphic_type_relationship_for: name)
end
end

def include_optional_linkage_data?
return false if @exclude_linkage_data
@always_include_optional_linkage_data || JSONAPI::configuration.always_include_to_one_linkage_data
end

@@ -167,10 +215,10 @@ def allow_include?(context = nil)
@allow_include
end

if !!strategy == strategy #check for boolean
if !!strategy == strategy # check for boolean
return strategy
elsif strategy.is_a?(Symbol) || strategy.is_a?(String)
parent_resource.send(strategy, context)
parent_resource_klass.send(strategy, context)
else
strategy.call(context)
end
@@ -185,17 +233,21 @@ def initialize(name, options = {})
@class_name = options.fetch(:class_name, name.to_s.camelize.singularize)
@foreign_key ||= "#{name.to_s.singularize}_ids".to_sym
@reflect = options.fetch(:reflect, true) == true
if parent_resource
@inverse_relationship = options.fetch(:inverse_relationship, parent_resource._type.to_s.singularize.to_sym)
end
# if parent_resource
# @inverse_relationship = options.fetch(:inverse_relationship, parent_resource._type.to_s.singularize.to_sym)
# end
end

def to_s
# :nocov: useful for debugging
"#{parent_resource}.#{name}(ToMany)"
"#{parent_resource_klass}.#{name}(ToMany)"
# :nocov:
end

def hidden?
@hidden
end

def include_optional_linkage_data?
# :nocov:
@always_include_optional_linkage_data || JSONAPI::configuration.always_include_to_many_linkage_data
@@ -209,10 +261,10 @@ def allow_include?(context = nil)
@allow_include
end

if !!strategy == strategy #check for boolean
if !!strategy == strategy # check for boolean
return strategy
elsif strategy.is_a?(Symbol) || strategy.is_a?(String)
parent_resource.send(strategy, context)
parent_resource_klass.send(strategy, context)
else
strategy.call(context)
end
10 changes: 8 additions & 2 deletions lib/jsonapi/resource.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
require 'jsonapi/callbacks'
require 'jsonapi/configuration'

module JSONAPI
class Resource < ActiveRelationResource
class Resource
include ResourceCommon
root_resource
abstract
immutable
end
end
end
247 changes: 172 additions & 75 deletions lib/jsonapi/basic_resource.rb → lib/jsonapi/resource_common.rb

Large diffs are not rendered by default.

13 changes: 3 additions & 10 deletions lib/jsonapi/resource_fragment.rb
Original file line number Diff line number Diff line change
@@ -8,11 +8,9 @@ module JSONAPI
# related_from - a set of related resource identities that loaded the fragment
# resource - a resource instance
#
# Todo: optionally use these for faster responses by bypassing model instantiation)
# attributes - resource attributes

class ResourceFragment
attr_reader :identity, :attributes, :related_from, :related, :resource
attr_reader :identity, :related_from, :related, :resource

attr_accessor :primary, :cache

@@ -24,9 +22,8 @@ def initialize(identity, resource: nil, cache: nil, primary: false)
@resource = resource
@primary = primary

@attributes = {}
@related = {}
@related_from = Set.new
@related_from = SortedSet.new
end

def initialize_related(relationship_name)
@@ -46,9 +43,5 @@ def merge_related_identities(relationship_name, identities)
def add_related_from(identity)
@related_from << identity
end

def add_attribute(name, value)
@attributes[name] = value
end
end
end
end
4 changes: 4 additions & 0 deletions lib/jsonapi/resource_identity.rb
Original file line number Diff line number Diff line change
@@ -32,6 +32,10 @@ def hash
[@resource_klass, @id].hash
end

def <=>(other_identity)
self.id <=> other_identity.id
end

# Creates a string representation of the identifier.
def to_s
# :nocov:
2 changes: 1 addition & 1 deletion lib/jsonapi/resource_serializer.rb
Original file line number Diff line number Diff line change
@@ -305,7 +305,7 @@ def cached_relationships_hash(source, fetchable_fields, relationship_data)
if field_set.include?(name)

relationship_name = unformat_key(name).to_sym
relationship_klass = source.resource_klass._relationships[relationship_name]
relationship_klass = source.resource_klass._relationship(relationship_name)

if relationship_klass.is_a?(JSONAPI::Relationship::ToOne)
# include_linkage = @always_include_to_one_linkage_data | relationship_klass.always_include_linkage_data
4 changes: 2 additions & 2 deletions lib/jsonapi/resource_set.rb
Original file line number Diff line number Diff line change
@@ -9,7 +9,7 @@ def initialize(source, include_related = nil, options = nil)
@populated = false
tree = if source.is_a?(JSONAPI::ResourceTree)
source
elsif source.class < JSONAPI::BasicResource
elsif source.class.include?(JSONAPI::ResourceCommon)
JSONAPI::PrimaryResourceTree.new(resource: source, include_related: include_related, options: options)
elsif source.is_a?(Array)
JSONAPI::PrimaryResourceTree.new(resources: source, include_related: include_related, options: options)
@@ -178,7 +178,7 @@ def flatten_resource_tree(resource_tree, flattened_tree = {})
flattened_tree[resource_klass][id][:resource] ||= fragment.resource if fragment.resource

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] ||= SortedSet.new
flattened_tree[resource_klass][id][:relationships][relationship_name].merge(related_rids)
end
end
51 changes: 3 additions & 48 deletions lib/jsonapi/resource_tree.rb
Original file line number Diff line number Diff line change
@@ -81,7 +81,7 @@ def load_included(resource_klass, source_resource_tree, include_related, options
find_related_resource_options[:cache] = resource_klass.caching?

related_fragments = resource_klass.find_included_fragments(source_resource_tree.fragments.values,
relationship_name,
relationship,
find_related_resource_options)

related_resource_tree = source_resource_tree.get_related_resource_tree(relationship)
@@ -94,51 +94,6 @@ def load_included(resource_klass, source_resource_tree, include_related, options
options)
end
end

def add_resources_to_tree(resource_klass,
tree,
resources,
include_related,
source_rid: nil,
source_relationship_name: nil,
connect_source_identity: true)
fragments = {}

resources.each do |resource|
next unless resource

# fragments[resource.identity] ||= ResourceFragment.new(resource.identity, resource: resource)
# resource_fragment = fragments[resource.identity]
# ToDo: revert when not needed for testing
resource_fragment = if fragments[resource.identity]
fragments[resource.identity]
else
fragments[resource.identity] = ResourceFragment.new(resource.identity, resource: resource)
fragments[resource.identity]
end

if resource.class.caching?
resource_fragment.cache = resource.cache_field_value
end

linkage_relationships = resource_klass.to_one_relationships_for_linkage(resource.class, include_related)
linkage_relationships.each do |relationship_name|
related_resource = resource.send(relationship_name)
resource_fragment.add_related_identity(relationship_name, related_resource&.identity)
end

if source_rid && connect_source_identity
resource_fragment.add_related_from(source_rid)
source_klass = source_rid.resource_klass
related_relationship_name = source_klass._relationships[source_relationship_name].inverse_relationship
if related_relationship_name
resource_fragment.add_related_identity(related_relationship_name, source_rid)
end
end
end

tree.add_resource_fragments(fragments, include_related)
end
end

class PrimaryResourceTree < ResourceTree
@@ -180,7 +135,7 @@ def complete_includes!(include_related, options)
resource_klasses = Set.new
@fragments.each_key { |identity| resource_klasses << identity.resource_klass }

resource_klasses.each { |resource_klass| load_included(resource_klass, self, include_related, options)}
resource_klasses.each { |resource_klass| load_included(resource_klass, self, include_related, options) }

self
end
@@ -231,4 +186,4 @@ def add_resource_fragment(fragment, include_related)
end
end
end
end
end
2 changes: 1 addition & 1 deletion lib/jsonapi/response_document.rb
Original file line number Diff line number Diff line change
@@ -117,7 +117,7 @@ def update_links(serializer, result)

result.pagination_params.each_pair do |link_name, params|
if result.is_a?(JSONAPI::RelatedResourcesSetOperationResult)
relationship = result.source_resource.class._relationships[result._type.to_sym]
relationship = result.source_resource.class._relationship(result._type)
unless relationship.exclude_link?(link_name)
link = serializer.link_builder.relationships_related_link(result.source_resource, relationship, query_params(params))
end
4 changes: 2 additions & 2 deletions lib/jsonapi/routing_ext.rb
Original file line number Diff line number Diff line change
@@ -222,7 +222,7 @@ def jsonapi_related_resource(*relationship)
options = relationship.extract_options!.dup

relationship_name = relationship.first
relationship = source._relationships[relationship_name]
relationship = source._relationship(relationship_name)

relationship._routed = true

@@ -246,7 +246,7 @@ def jsonapi_related_resources(*relationship)
options = relationship.extract_options!.dup

relationship_name = relationship.first
relationship = source._relationships[relationship_name]
relationship = source._relationship(relationship_name)

relationship._routed = true

11 changes: 11 additions & 0 deletions lib/jsonapi/simple_resource.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
require 'jsonapi/callbacks'
require 'jsonapi/configuration'

module JSONAPI
class SimpleResource
include ResourceCommon
root_resource
abstract
immutable
end
end
2 changes: 1 addition & 1 deletion lib/tasks/check_upgrade.rake
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@ namespace :jsonapi do
task :check_upgrade => :environment do
Rails.application.eager_load!

resource_klasses = ObjectSpace.each_object(Class).select { |klass| klass < JSONAPI::Resource}
resource_klasses = ObjectSpace.each_object(Class).select { |klass| klass.include?(JSONAPI::ResourceCommon)}

puts "Checking #{resource_klasses.count} resources"

3,577 changes: 1,810 additions & 1,767 deletions test/controllers/controller_test.rb

Large diffs are not rendered by default.

40 changes: 26 additions & 14 deletions test/fixtures/active_record.rb
Original file line number Diff line number Diff line change
@@ -467,6 +467,7 @@ class ResponseText < ActiveRecord::Base
end

class ResponseText::Paragraph < ResponseText
belongs_to :response
end

class Person < ActiveRecord::Base
@@ -590,7 +591,7 @@ class Planet < ActiveRecord::Base

def check_not_pluto
# Pluto can't be a planet, so cancel the save
if name.downcase == 'pluto'
if name.underscore == 'pluto'
throw(:abort)
end
end
@@ -728,7 +729,7 @@ class Picture < ActiveRecord::Base
belongs_to :document, -> { where( pictures: { imageable_type: 'Document' } ) }, foreign_key: 'imageable_id'
belongs_to :product, -> { where( pictures: { imageable_type: 'Product' } ) }, foreign_key: 'imageable_id'

has_one :file_properties, as: 'fileable'
has_one :file_properties, as: :fileable
end

class Vehicle < ActiveRecord::Base
@@ -744,13 +745,13 @@ class Boat < Vehicle
class Document < ActiveRecord::Base
has_many :pictures, as: :imageable
belongs_to :author, class_name: 'Person', foreign_key: 'author_id'
has_one :file_properties, as: 'fileable'
has_one :file_properties, as: :fileable
end

class Product < ActiveRecord::Base
has_many :pictures, as: :imageable
belongs_to :designer, class_name: 'Person', foreign_key: 'designer_id'
has_one :file_properties, as: 'fileable'
has_one :file_properties, as: :fileable
end

class FileProperties < ActiveRecord::Base
@@ -1250,7 +1251,9 @@ def responses=params
}
}
end
def responses

def responses(options)
[]
end

def self.creatable_fields(context)
@@ -1522,7 +1525,7 @@ class EmployeeResource < JSONAPI::Resource
has_many :expense_entries
end

class PoroResource < JSONAPI::BasicResource
class PoroResource < JSONAPI::SimpleResource
root_resource

class << self
@@ -1635,7 +1638,6 @@ def find_by_keys(keys, options = {})
end

class BreedResource < PoroResource

attribute :name, format: :title

# This is unneeded, just here for testing
@@ -1708,7 +1710,9 @@ class CraterResource < JSONAPI::Resource

filter :description, apply: -> (records, value, options) {
fail "context not set" unless options[:context][:current_user] != nil && options[:context][:current_user] == $test_user
records.where(concat_table_field(options.dig(:_relation_helper_options, :join_manager).source_join_details[:alias], :description) => value)
join_manager = options.dig(:_relation_helper_options, :join_manager)
field = join_manager ? get_aliased_field('description', join_manager) : 'description'
records.where(Arel.sql(field) => value)
}

def self.verify_key(key, context = nil)
@@ -1749,7 +1753,11 @@ class PictureResource < JSONAPI::Resource
has_one :author

has_one :imageable, polymorphic: true
has_one :file_properties, inverse_relationship: :fileable, :foreign_key_on => :related, polymorphic: true
# the imageable polymorphic relationship will implicitly create the following relationships
# has_one :document, exclude_linkage_data: true, polymorphic_type_relationship_for: :imageable
# has_one :product, exclude_linkage_data: true, polymorphic_type_relationship_for: :imageable

has_one :file_properties, :foreign_key_on => :related

filter 'imageable.name', perform_joins: true, apply: -> (records, value, options) {
join_manager = options.dig(:_relation_helper_options, :join_manager)
@@ -1766,6 +1774,7 @@ class PictureResource < JSONAPI::Resource

class ImageableResource < JSONAPI::Resource
polymorphic
has_one :picture
end

class FileableResource < JSONAPI::Resource
@@ -1774,15 +1783,20 @@ class FileableResource < JSONAPI::Resource

class DocumentResource < JSONAPI::Resource
attribute :name
has_many :pictures, inverse_relationship: :imageable

# Will use implicitly defined inverse relationship on PictureResource
has_many :pictures

has_one :author, class_name: 'Person'

has_one :file_properties, inverse_relationship: :fileable, :foreign_key_on => :related
end

class ProductResource < JSONAPI::Resource
attribute :name
has_many :pictures, inverse_relationship: :imageable

# Will use implicitly defined inverse relationship on PictureResource
has_many :pictures
has_one :designer, class_name: 'Person'

has_one :file_properties, inverse_relationship: :fileable, :foreign_key_on => :related
@@ -2193,8 +2207,6 @@ class CommentResource < CommentResource; end
class PostResource < PostResource
attribute :base

has_one :author

def base
_model.title
end
@@ -2344,7 +2356,7 @@ class PreferencesResource < JSONAPI::Resource
key
}

has_one :person, :foreign_key_on => :related
has_one :person, foreign_key_on: :related, relation_name: :author

attribute :nickname
end
2 changes: 1 addition & 1 deletion test/helpers/assertions.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
module Helpers
module Assertions
def assert_hash_equals(exp, act, msg = nil)
msg = message(msg, '') { diff exp, act }
msg = message(msg, '') { diff exp.deep_stringify_keys, act.deep_stringify_keys }
assert(matches_hash?(exp, act, {exact: true}), msg)
end

11 changes: 10 additions & 1 deletion test/helpers/configuration_helpers.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
module Helpers
module ConfigurationHelpers
def with_jsonapi_config_changes(&block)
orig_config = JSONAPI.configuration.dup
yield
ensure
$PostProcessorRaisesErrors = false
$PostSerializerRaisesErrors = false
JSONAPI.configuration = orig_config
end

def with_jsonapi_config(new_config_options)
original_config = JSONAPI.configuration.dup # TODO should be a deep dup
begin
@@ -29,7 +38,7 @@ def with_resource_caching(cache, classes = :all)
with_jsonapi_config(new_config_options) do
if classes == :all or (classes.is_a?(Hash) && classes.keys == [:except])
resource_classes = ObjectSpace.each_object(Class).select do |klass|
if klass < JSONAPI::BasicResource
if klass < JSONAPI::Resource
# Not using Resource#_model_class to avoid tripping the warning early, which could
# cause ResourceTest#test_nil_model_class to fail.
model_class = klass._model_name.to_s.safe_constantize
27 changes: 26 additions & 1 deletion test/integration/requests/request_test.rb
Original file line number Diff line number Diff line change
@@ -31,6 +31,8 @@ def test_get_not_found
end

def test_post_sessions
skip "This test isn't compatible with v09" if testing_v09?

session_id = SecureRandom.uuid

post '/sessions', params: {
@@ -1485,7 +1487,18 @@ def test_include_parameter_openquoted
end

def test_getting_different_resources_when_sti
assert_cacheable_jsonapi_get '/vehicles'
get '/vehicles'
assert_jsonapi_response 200
types = json_response['data'].map{|r| r['type']}.to_set
assert types == Set['cars', 'boats']

# Testing the cached get separately since find_to_populate_by_keys does not use sorting resulting in
# unsorted results with STI
cache = ActiveSupport::Cache::MemoryStore.new
with_resource_caching(cache) do
get '/vehicles'
end
assert_jsonapi_response 200
types = json_response['data'].map{|r| r['type']}.to_set
assert types == Set['cars', 'boats']
end
@@ -1557,6 +1570,10 @@ def test_get_resource_include_singleton_relationship
"links" => {
"self" => "http://www.example.com/api/v9/preferences/relationships/person",
"related" => "http://www.example.com/api/v9/preferences/person"
},
'data' => {
'type' => 'people',
'id' => '1005'
}
}
},
@@ -1616,6 +1633,10 @@ def test_caching_included_singleton
"links" => {
"self" => "http://www.example.com/api/v9/preferences/relationships/person",
"related" => "http://www.example.com/api/v9/preferences/person"
},
"data" => {
"type" => "people",
"id" => "1005"
}
}
},
@@ -1664,6 +1685,10 @@ def test_caching_included_singleton
"links" => {
"self" => "http://www.example.com/api/v9/preferences/relationships/person",
"related" => "http://www.example.com/api/v9/preferences/person"
},
"data" => {
"type" => "people",
"id" => "1001"
}
}
},
49 changes: 34 additions & 15 deletions test/test_helper.rb
Original file line number Diff line number Diff line change
@@ -164,7 +164,7 @@ def assign_parameters(routes, controller_path, action, parameters, generated_pat
def assert_query_count(expected, msg = nil, &block)
@queries = []
callback = lambda {|_, _, _, _, payload|
@queries.push payload[:sql]
@queries.push payload[:sql] unless payload[:sql].starts_with?("SELECT name FROM sqlite_master WHERE name <> 'sqlite_sequence'")
}
ActiveSupport::Notifications.subscribed(callback, 'sql.active_record', &block)

@@ -502,6 +502,10 @@ def db_true
def sql_for_compare(sql)
sql.tr(db_quote_identifier, %{"})
end

def response_json_for_compare(response)
response.pretty_inspect
end
end

class ActiveSupport::TestCase
@@ -544,8 +548,8 @@ def assert_cacheable_jsonapi_get(url, cached_classes = :all)
end

assert_equal(
sql_for_compare(non_caching_response.pretty_inspect),
sql_for_compare(json_response.pretty_inspect),
response_json_for_compare(non_caching_response),
response_json_for_compare(json_response),
"Cache warmup response must match normal response"
)

@@ -554,13 +558,18 @@ def assert_cacheable_jsonapi_get(url, cached_classes = :all)
end

assert_equal(
sql_for_compare(non_caching_response.pretty_inspect),
sql_for_compare(json_response.pretty_inspect),
response_json_for_compare(non_caching_response),
response_json_for_compare(json_response),
"Cached response must match normal response"
)
assert_equal 0, cached[:total][:misses], "Cached response must not cause any cache misses"
assert_equal warmup[:total][:misses], cached[:total][:hits], "Cached response must use cache"
end


def testing_v09?
JSONAPI.configuration.default_resource_retrieval_strategy == 'JSONAPI::ActiveRelationRetrievalV09'
end
end

class ActionController::TestCase
@@ -623,16 +632,18 @@ def assert_cacheable_get(action, **args)
"Cache (mode: #{mode}) #{phase} response status must match normal response"
)
assert_equal(
sql_for_compare(non_caching_response.pretty_inspect),
sql_for_compare(json_response_sans_all_backtraces.pretty_inspect),
"Cache (mode: #{mode}) #{phase} response body must match normal response"
)
assert_operator(
cache_queries.size,
:<=,
normal_queries.size,
"Cache (mode: #{mode}) #{phase} action made too many queries:\n#{cache_queries.pretty_inspect}"
response_json_for_compare(non_caching_response),
response_json_for_compare(json_response_sans_all_backtraces),
"Cache (mode: #{mode}) #{phase} response body must match normal response\n#{non_caching_response.pretty_inspect},\n#{json_response_sans_all_backtraces.pretty_inspect}"
)

# The query count will now differ between the cached and non cached versions so we will not test that
# assert_operator(
# cache_queries.size,
# :<=,
# normal_queries.size,
# "Cache (mode: #{mode}) #{phase} action made too many queries:\n#{cache_queries.pretty_inspect}"
# )
end

if mode == :all
@@ -661,6 +672,14 @@ def assert_cacheable_get(action, **args)
@queries = orig_queries
end

def testing_v10?
JSONAPI.configuration.default_resource_retrieval_strategy == 'JSONAPI::ActiveRelationRetrievalV10'
end

def testing_v09?
JSONAPI.configuration.default_resource_retrieval_strategy == 'JSONAPI::ActiveRelationRetrievalV09'
end

private

def json_response_sans_all_backtraces
@@ -725,7 +744,7 @@ def format(raw_value)
end

def unformat(value)
value.to_s.downcase
value.to_s.underscore
end
end
end
45 changes: 27 additions & 18 deletions test/unit/active_relation_resource_finder/join_manager_test.rb
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
require File.expand_path('../../../test_helper', __FILE__)
require 'jsonapi-resources'

class JoinTreeTest < ActiveSupport::TestCase
class JoinManagerTest < ActiveSupport::TestCase
# def setup
# JSONAPI.configuration.default_alias_on_join = false
# end
#
# def teardown
# JSONAPI.configuration.default_alias_on_join = false
# end

def test_no_added_joins
join_manager = JSONAPI::ActiveRelation::JoinManager.new(resource_klass: PostResource)
join_manager = JSONAPI::ActiveRelation::JoinManagerV10.new(resource_klass: PostResource)

records = PostResource.records({})
records = join_manager.join(records, {})
@@ -15,7 +22,7 @@ def test_no_added_joins

def test_add_single_join
filters = {'tags' => ['1']}
join_manager = JSONAPI::ActiveRelation::JoinManager.new(resource_klass: PostResource, filters: filters)
join_manager = JSONAPI::ActiveRelation::JoinManagerV10.new(resource_klass: PostResource, filters: filters)
records = PostResource.records({})
records = join_manager.join(records, {})
assert_equal 'SELECT "posts".* FROM "posts" LEFT OUTER JOIN "posts_tags" ON "posts_tags"."post_id" = "posts"."id" LEFT OUTER JOIN "tags" ON "tags"."id" = "posts_tags"."tag_id"', sql_for_compare(records.to_sql)
@@ -25,7 +32,7 @@ def test_add_single_join

def test_add_single_sort_join
sort_criteria = [{field: 'tags.name', direction: :desc}]
join_manager = JSONAPI::ActiveRelation::JoinManager.new(resource_klass: PostResource, sort_criteria: sort_criteria)
join_manager = JSONAPI::ActiveRelation::JoinManagerV10.new(resource_klass: PostResource, sort_criteria: sort_criteria)
records = PostResource.records({})
records = join_manager.join(records, {})

@@ -37,7 +44,7 @@ def test_add_single_sort_join
def test_add_single_sort_and_filter_join
filters = {'tags' => ['1']}
sort_criteria = [{field: 'tags.name', direction: :desc}]
join_manager = JSONAPI::ActiveRelation::JoinManager.new(resource_klass: PostResource, sort_criteria: sort_criteria, filters: filters)
join_manager = JSONAPI::ActiveRelation::JoinManagerV10.new(resource_klass: PostResource, sort_criteria: sort_criteria, filters: filters)
records = PostResource.records({})
records = join_manager.join(records, {})
assert_equal 'SELECT "posts".* FROM "posts" LEFT OUTER JOIN "posts_tags" ON "posts_tags"."post_id" = "posts"."id" LEFT OUTER JOIN "tags" ON "tags"."id" = "posts_tags"."tag_id"', sql_for_compare(records.to_sql)
@@ -51,7 +58,7 @@ def test_add_sibling_joins
'author' => ['1']
}

join_manager = JSONAPI::ActiveRelation::JoinManager.new(resource_klass: PostResource, filters: filters)
join_manager = JSONAPI::ActiveRelation::JoinManagerV10.new(resource_klass: PostResource, filters: filters)
records = PostResource.records({})
records = join_manager.join(records, {})

@@ -63,7 +70,7 @@ def test_add_sibling_joins


def test_add_joins_source_relationship
join_manager = JSONAPI::ActiveRelation::JoinManager.new(resource_klass: PostResource,
join_manager = JSONAPI::ActiveRelation::JoinManagerV10.new(resource_klass: PostResource,
source_relationship: PostResource._relationship(:comments))
records = PostResource.records({})
records = join_manager.join(records, {})
@@ -74,7 +81,7 @@ def test_add_joins_source_relationship


def test_add_joins_source_relationship_with_custom_apply
join_manager = JSONAPI::ActiveRelation::JoinManager.new(resource_klass: Api::V10::PostResource,
join_manager = JSONAPI::ActiveRelation::JoinManagerV10.new(resource_klass: Api::V10::PostResource,
source_relationship: Api::V10::PostResource._relationship(:comments))
records = Api::V10::PostResource.records({})
records = join_manager.join(records, {})
@@ -93,7 +100,7 @@ def test_add_nested_scoped_joins
'author' => ['1']
}

join_manager = JSONAPI::ActiveRelation::JoinManager.new(resource_klass: Api::V10::PostResource, filters: filters)
join_manager = JSONAPI::ActiveRelation::JoinManagerV10.new(resource_klass: Api::V10::PostResource, filters: filters)
records = Api::V10::PostResource.records({})
records = join_manager.join(records, {})

@@ -110,7 +117,7 @@ def test_add_nested_scoped_joins
'comments.tags' => ['1']
}

join_manager = JSONAPI::ActiveRelation::JoinManager.new(resource_klass: Api::V10::PostResource, filters: filters)
join_manager = JSONAPI::ActiveRelation::JoinManagerV10.new(resource_klass: Api::V10::PostResource, filters: filters)
records = Api::V10::PostResource.records({})
records = join_manager.join(records, {})

@@ -128,7 +135,7 @@ def test_add_nested_joins_with_fields
'author.foo' => ['1']
}

join_manager = JSONAPI::ActiveRelation::JoinManager.new(resource_klass: Api::V10::PostResource, filters: filters)
join_manager = JSONAPI::ActiveRelation::JoinManagerV10.new(resource_klass: Api::V10::PostResource, filters: filters)
records = Api::V10::PostResource.records({})
records = join_manager.join(records, {})

@@ -142,7 +149,7 @@ def test_add_nested_joins_with_fields
def test_add_joins_with_sub_relationship
relationships = %w(author author.comments tags)

join_manager = JSONAPI::ActiveRelation::JoinManager.new(resource_klass: Api::V10::PostResource, relationships: relationships,
join_manager = JSONAPI::ActiveRelation::JoinManagerV10.new(resource_klass: Api::V10::PostResource, relationships: relationships,
source_relationship: Api::V10::PostResource._relationship(:comments))
records = Api::V10::PostResource.records({})
records = join_manager.join(records, {})
@@ -161,7 +168,7 @@ def test_add_joins_with_sub_relationship_and_filters

relationships = %w(author author.comments tags)

join_manager = JSONAPI::ActiveRelation::JoinManager.new(resource_klass: PostResource,
join_manager = JSONAPI::ActiveRelation::JoinManagerV10.new(resource_klass: PostResource,
filters: filters,
relationships: relationships,
source_relationship: PostResource._relationship(:comments))
@@ -176,8 +183,10 @@ def test_add_joins_with_sub_relationship_and_filters
end

def test_polymorphic_join_belongs_to_just_source
join_manager = JSONAPI::ActiveRelation::JoinManager.new(resource_klass: PictureResource,
source_relationship: PictureResource._relationship(:imageable))
join_manager = JSONAPI::ActiveRelation::JoinManagerV10.new(
resource_klass: PictureResource,
source_relationship: PictureResource._relationship(:imageable)
)

records = PictureResource.records({})
records = join_manager.join(records, {})
@@ -191,7 +200,7 @@ def test_polymorphic_join_belongs_to_just_source

def test_polymorphic_join_belongs_to_filter
filters = {'imageable' => ['Foo']}
join_manager = JSONAPI::ActiveRelation::JoinManager.new(resource_klass: PictureResource, filters: filters)
join_manager = JSONAPI::ActiveRelation::JoinManagerV10.new(resource_klass: PictureResource, filters: filters)

records = PictureResource.records({})
records = join_manager.join(records, {})
@@ -208,12 +217,12 @@ def test_polymorphic_join_belongs_to_filter_on_resource
}

relationships = %w(imageable file_properties)
join_manager = JSONAPI::ActiveRelation::JoinManager.new(resource_klass: PictureResource,
join_manager = JSONAPI::ActiveRelation::JoinManagerV10.new(resource_klass: PictureResource,
filters: filters,
relationships: relationships)

records = PictureResource.records({})
records = join_manager.join(records, {})
join_manager.join(records, {})

assert_hash_equals({alias: 'pictures', join_type: :root}, join_manager.source_join_details)
assert_hash_equals({alias: 'products', join_type: :left}, join_manager.join_details_by_polymorphic_relationship(PictureResource._relationship(:imageable), 'products'))
222 changes: 222 additions & 0 deletions test/unit/active_relation_resource_finder/join_manager_v10_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
require File.expand_path('../../../test_helper', __FILE__)
require 'jsonapi-resources'

class JoinManagerV10Test < ActiveSupport::TestCase
def test_no_added_joins
join_manager = JSONAPI::ActiveRelation::JoinManagerV10.new(resource_klass: PostResource)

records = PostResource.records({})
records = join_manager.join(records, {})
assert_equal 'SELECT "posts".* FROM "posts"', sql_for_compare(records.to_sql)

assert_hash_equals({alias: 'posts', join_type: :root}, join_manager.source_join_details)
end

def test_add_single_join
filters = {'tags' => ['1']}
join_manager = JSONAPI::ActiveRelation::JoinManagerV10.new(resource_klass: PostResource, filters: filters)
records = PostResource.records({})
records = join_manager.join(records, {})
assert_equal 'SELECT "posts".* FROM "posts" LEFT OUTER JOIN "posts_tags" ON "posts_tags"."post_id" = "posts"."id" LEFT OUTER JOIN "tags" ON "tags"."id" = "posts_tags"."tag_id"', sql_for_compare(records.to_sql)
assert_hash_equals({alias: 'posts', join_type: :root}, join_manager.source_join_details)
assert_hash_equals({alias: 'tags', join_type: :left}, join_manager.join_details_by_relationship(PostResource._relationship(:tags)))
end

def test_add_single_sort_join
sort_criteria = [{field: 'tags.name', direction: :desc}]
join_manager = JSONAPI::ActiveRelation::JoinManagerV10.new(resource_klass: PostResource, sort_criteria: sort_criteria)
records = PostResource.records({})
records = join_manager.join(records, {})

assert_equal 'SELECT "posts".* FROM "posts" LEFT OUTER JOIN "posts_tags" ON "posts_tags"."post_id" = "posts"."id" LEFT OUTER JOIN "tags" ON "tags"."id" = "posts_tags"."tag_id"', sql_for_compare(records.to_sql)
assert_hash_equals({alias: 'posts', join_type: :root}, join_manager.source_join_details)
assert_hash_equals({alias: 'tags', join_type: :left}, join_manager.join_details_by_relationship(PostResource._relationship(:tags)))
end

def test_add_single_sort_and_filter_join
filters = {'tags' => ['1']}
sort_criteria = [{field: 'tags.name', direction: :desc}]
join_manager = JSONAPI::ActiveRelation::JoinManagerV10.new(resource_klass: PostResource, sort_criteria: sort_criteria, filters: filters)
records = PostResource.records({})
records = join_manager.join(records, {})
assert_equal 'SELECT "posts".* FROM "posts" LEFT OUTER JOIN "posts_tags" ON "posts_tags"."post_id" = "posts"."id" LEFT OUTER JOIN "tags" ON "tags"."id" = "posts_tags"."tag_id"', sql_for_compare(records.to_sql)
assert_hash_equals({alias: 'posts', join_type: :root}, join_manager.source_join_details)
assert_hash_equals({alias: 'tags', join_type: :left}, join_manager.join_details_by_relationship(PostResource._relationship(:tags)))
end

def test_add_sibling_joins
filters = {
'tags' => ['1'],
'author' => ['1']
}

join_manager = JSONAPI::ActiveRelation::JoinManagerV10.new(resource_klass: PostResource, filters: filters)
records = PostResource.records({})
records = join_manager.join(records, {})

assert_equal 'SELECT "posts".* FROM "posts" LEFT OUTER JOIN "posts_tags" ON "posts_tags"."post_id" = "posts"."id" LEFT OUTER JOIN "tags" ON "tags"."id" = "posts_tags"."tag_id" LEFT OUTER JOIN "people" ON "people"."id" = "posts"."author_id"', sql_for_compare(records.to_sql)
assert_hash_equals({alias: 'posts', join_type: :root}, join_manager.source_join_details)
assert_hash_equals({alias: 'tags', join_type: :left}, join_manager.join_details_by_relationship(PostResource._relationship(:tags)))
assert_hash_equals({alias: 'people', join_type: :left}, join_manager.join_details_by_relationship(PostResource._relationship(:author)))
end


def test_add_joins_source_relationship
join_manager = JSONAPI::ActiveRelation::JoinManagerV10.new(resource_klass: PostResource,
source_relationship: PostResource._relationship(:comments))
records = PostResource.records({})
records = join_manager.join(records, {})

assert_equal 'SELECT "posts".* FROM "posts" INNER JOIN "comments" ON "comments"."post_id" = "posts"."id"', sql_for_compare(records.to_sql)
assert_hash_equals({alias: 'comments', join_type: :inner}, join_manager.source_join_details)
end


def test_add_joins_source_relationship_with_custom_apply
join_manager = JSONAPI::ActiveRelation::JoinManagerV10.new(resource_klass: Api::V10::PostResource,
source_relationship: Api::V10::PostResource._relationship(:comments))
records = Api::V10::PostResource.records({})
records = join_manager.join(records, {})

sql = 'SELECT "posts".* FROM "posts" INNER JOIN "comments" ON "comments"."post_id" = "posts"."id" WHERE "comments"."approved" = ' + db_true

assert_equal sql, sql_for_compare(records.to_sql)

assert_hash_equals({alias: 'comments', join_type: :inner}, join_manager.source_join_details)
end

def test_add_nested_scoped_joins
filters = {
'comments.author' => ['1'],
'comments.tags' => ['1'],
'author' => ['1']
}

join_manager = JSONAPI::ActiveRelation::JoinManagerV10.new(resource_klass: Api::V10::PostResource, filters: filters)
records = Api::V10::PostResource.records({})
records = join_manager.join(records, {})

assert_hash_equals({alias: 'posts', join_type: :root}, join_manager.source_join_details)
assert_hash_equals({alias: 'comments', join_type: :left}, join_manager.join_details_by_relationship(Api::V10::PostResource._relationship(:comments)))
assert_hash_equals({alias: 'authors_comments', join_type: :left}, join_manager.join_details_by_relationship(Api::V10::CommentResource._relationship(:author)))
assert_hash_equals({alias: 'tags', join_type: :left}, join_manager.join_details_by_relationship(Api::V10::CommentResource._relationship(:tags)))
assert_hash_equals({alias: 'people', join_type: :left}, join_manager.join_details_by_relationship(Api::V10::PostResource._relationship(:author)))

# Now test with different order for the filters
filters = {
'author' => ['1'],
'comments.author' => ['1'],
'comments.tags' => ['1']
}

join_manager = JSONAPI::ActiveRelation::JoinManagerV10.new(resource_klass: Api::V10::PostResource, filters: filters)
records = Api::V10::PostResource.records({})
records = join_manager.join(records, {})

assert_hash_equals({alias: 'posts', join_type: :root}, join_manager.source_join_details)
assert_hash_equals({alias: 'comments', join_type: :left}, join_manager.join_details_by_relationship(Api::V10::PostResource._relationship(:comments)))
assert_hash_equals({alias: 'authors_comments', join_type: :left}, join_manager.join_details_by_relationship(Api::V10::CommentResource._relationship(:author)))
assert_hash_equals({alias: 'tags', join_type: :left}, join_manager.join_details_by_relationship(Api::V10::CommentResource._relationship(:tags)))
assert_hash_equals({alias: 'people', join_type: :left}, join_manager.join_details_by_relationship(Api::V10::PostResource._relationship(:author)))
end

def test_add_nested_joins_with_fields
filters = {
'comments.author.name' => ['1'],
'comments.tags.id' => ['1'],
'author.foo' => ['1']
}

join_manager = JSONAPI::ActiveRelation::JoinManagerV10.new(resource_klass: Api::V10::PostResource, filters: filters)
records = Api::V10::PostResource.records({})
records = join_manager.join(records, {})

assert_hash_equals({alias: 'posts', join_type: :root}, join_manager.source_join_details)
assert_hash_equals({alias: 'comments', join_type: :left}, join_manager.join_details_by_relationship(Api::V10::PostResource._relationship(:comments)))
assert_hash_equals({alias: 'authors_comments', join_type: :left}, join_manager.join_details_by_relationship(Api::V10::CommentResource._relationship(:author)))
assert_hash_equals({alias: 'tags', join_type: :left}, join_manager.join_details_by_relationship(Api::V10::CommentResource._relationship(:tags)))
assert_hash_equals({alias: 'people', join_type: :left}, join_manager.join_details_by_relationship(Api::V10::PostResource._relationship(:author)))
end

def test_add_joins_with_sub_relationship
relationships = %w(author author.comments tags)

join_manager = JSONAPI::ActiveRelation::JoinManagerV10.new(resource_klass: Api::V10::PostResource, relationships: relationships,
source_relationship: Api::V10::PostResource._relationship(:comments))
records = Api::V10::PostResource.records({})
records = join_manager.join(records, {})

assert_hash_equals({alias: 'comments', join_type: :inner}, join_manager.source_join_details)
assert_hash_equals({alias: 'comments', join_type: :inner}, join_manager.join_details_by_relationship(Api::V10::PostResource._relationship(:comments)))
assert_hash_equals({alias: 'tags', join_type: :left}, join_manager.join_details_by_relationship(Api::V10::CommentResource._relationship(:tags)))
assert_hash_equals({alias: 'comments_people', join_type: :left}, join_manager.join_details_by_relationship(Api::V10::PersonResource._relationship(:comments)))
end

def test_add_joins_with_sub_relationship_and_filters
filters = {
'author.name' => ['1'],
'author.comments.name' => ['Foo']
}

relationships = %w(author author.comments tags)

join_manager = JSONAPI::ActiveRelation::JoinManagerV10.new(resource_klass: PostResource,
filters: filters,
relationships: relationships,
source_relationship: PostResource._relationship(:comments))
records = PostResource.records({})
records = join_manager.join(records, {})

assert_hash_equals({alias: 'comments', join_type: :inner}, join_manager.source_join_details)
assert_hash_equals({alias: 'comments', join_type: :inner}, join_manager.join_details_by_relationship(PostResource._relationship(:comments)))
assert_hash_equals({alias: 'people', join_type: :left}, join_manager.join_details_by_relationship(CommentResource._relationship(:author)))
assert_hash_equals({alias: 'tags', join_type: :left}, join_manager.join_details_by_relationship(CommentResource._relationship(:tags)))
assert_hash_equals({alias: 'comments_people', join_type: :left}, join_manager.join_details_by_relationship(PersonResource._relationship(:comments)))
end

def test_polymorphic_join_belongs_to_just_source
join_manager = JSONAPI::ActiveRelation::JoinManagerV10.new(resource_klass: PictureResource,
source_relationship: PictureResource._relationship(:imageable))

records = PictureResource.records({})
records = join_manager.join(records, {})

# assert_equal 'SELECT "pictures".* FROM "pictures" LEFT OUTER JOIN "products" ON "products"."id" = "pictures"."imageable_id" AND "pictures"."imageable_type" = \'Product\' LEFT OUTER JOIN "documents" ON "documents"."id" = "pictures"."imageable_id" AND "pictures"."imageable_type" = \'Document\'', sql_for_compare(records.to_sql)
assert_hash_equals({alias: 'products', join_type: :left}, join_manager.source_join_details('products'))
assert_hash_equals({alias: 'documents', join_type: :left}, join_manager.source_join_details('documents'))
assert_hash_equals({alias: 'products', join_type: :left}, join_manager.join_details_by_polymorphic_relationship(PictureResource._relationship(:imageable), 'products'))
assert_hash_equals({alias: 'documents', join_type: :left}, join_manager.join_details_by_polymorphic_relationship(PictureResource._relationship(:imageable), 'documents'))
end

def test_polymorphic_join_belongs_to_filter
filters = {'imageable' => ['Foo']}
join_manager = JSONAPI::ActiveRelation::JoinManagerV10.new(resource_klass: PictureResource, filters: filters)

records = PictureResource.records({})
records = join_manager.join(records, {})

# assert_equal 'SELECT "pictures".* FROM "pictures" LEFT OUTER JOIN "products" ON "products"."id" = "pictures"."imageable_id" AND "pictures"."imageable_type" = \'Product\' LEFT OUTER JOIN "documents" ON "documents"."id" = "pictures"."imageable_id" AND "pictures"."imageable_type" = \'Document\'', sql_for_compare(records.to_sql)
assert_hash_equals({alias: 'pictures', join_type: :root}, join_manager.source_join_details)
assert_hash_equals({alias: 'products', join_type: :left}, join_manager.join_details_by_polymorphic_relationship(PictureResource._relationship(:imageable), 'products'))
assert_hash_equals({alias: 'documents', join_type: :left}, join_manager.join_details_by_polymorphic_relationship(PictureResource._relationship(:imageable), 'documents'))
end

def test_polymorphic_join_belongs_to_filter_on_resource
filters = {
'imageable#documents.name' => ['foo']
}

relationships = %w(imageable file_properties)
join_manager = JSONAPI::ActiveRelation::JoinManagerV10.new(resource_klass: PictureResource,
filters: filters,
relationships: relationships)

records = PictureResource.records({})
records = join_manager.join(records, {})

assert_hash_equals({alias: 'pictures', join_type: :root}, join_manager.source_join_details)
assert_hash_equals({alias: 'products', join_type: :left}, join_manager.join_details_by_polymorphic_relationship(PictureResource._relationship(:imageable), 'products'))
assert_hash_equals({alias: 'documents', join_type: :left}, join_manager.join_details_by_polymorphic_relationship(PictureResource._relationship(:imageable), 'documents'))
assert_hash_equals({alias: 'file_properties', join_type: :left}, join_manager.join_details_by_relationship(PictureResource._relationship(:file_properties)))
end
end
237 changes: 0 additions & 237 deletions test/unit/resource/active_relation_resource_test.rb

This file was deleted.

236 changes: 236 additions & 0 deletions test/unit/resource/active_relation_resource_v_10_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
require File.expand_path('../../../test_helper', __FILE__)

module V10
class BaseResource
include JSONAPI::ResourceCommon
resource_retrieval_strategy 'JSONAPI::ActiveRelationRetrievalV10'
abstract
end

class PostResource < V10::BaseResource
attribute :headline, delegate: :title
has_one :author
has_many :tags
end

class AuthorResource < V10::BaseResource
model_name 'Person'
attributes :name

has_many :posts, inverse_relationship: :author
has_many :pictures
end

class TagResource < V10::BaseResource
attributes :name

has_many :posts
end

class PictureResource < V10::BaseResource
attribute :name
has_one :author

has_one :imageable, polymorphic: true
end

class ImageableResource < V10::BaseResource
polymorphic
has_one :picture
end

class DocumentResource < V10::BaseResource
attribute :name

has_many :pictures

has_one :author, class_name: 'Person'
end

class ProductResource < V10::BaseResource
attribute :name
has_many :pictures
has_one :designer, class_name: 'Person'

has_one :file_properties, :foreign_key_on => :related

def picture_id
_model.picture.id
end
end
end

class ActiveRelationResourceTest < ActiveSupport::TestCase
def setup
# skip("Skipping: Currently test is only valid for ActiveRelationRetrievalV10")
end

def test_find_fragments_no_attributes
filters = {}
posts_identities = V10::PostResource.find_fragments(filters)

assert_equal 20, posts_identities.length
assert_equal JSONAPI::ResourceIdentity.new(V10::PostResource, 1), posts_identities.keys[0]
assert_equal JSONAPI::ResourceIdentity.new(V10::PostResource, 1), posts_identities.values[0].identity
assert posts_identities.values[0].is_a?(JSONAPI::ResourceFragment)
end

def test_find_fragments_cache_field
filters = {}
options = { cache: true }
posts_identities = V10::PostResource.find_fragments(filters, options)

assert_equal 20, posts_identities.length
assert_equal JSONAPI::ResourceIdentity.new(V10::PostResource, 1), posts_identities.keys[0]
assert_equal JSONAPI::ResourceIdentity.new(V10::PostResource, 1), posts_identities.values[0].identity
assert posts_identities.values[0].is_a?(JSONAPI::ResourceFragment)
assert posts_identities.values[0].cache.is_a?(ActiveSupport::TimeWithZone)
end

def test_find_related_has_one_fragments
options = {}
source_rids = [JSONAPI::ResourceIdentity.new(V10::PostResource, 1),
JSONAPI::ResourceIdentity.new(V10::PostResource, 2),
JSONAPI::ResourceIdentity.new(V10::PostResource, 20)]
source_fragments = source_rids.collect {|rid| JSONAPI::ResourceFragment.new(rid) }

relationship = V10::PostResource._relationship('author')
related_fragments = V10::PostResource.find_included_fragments(source_fragments, relationship, options)

assert_equal 2, related_fragments.length
assert_equal JSONAPI::ResourceIdentity.new(V10::AuthorResource, 1001), related_fragments.keys[0]
assert_equal JSONAPI::ResourceIdentity.new(V10::AuthorResource, 1001), related_fragments.values[0].identity
assert related_fragments.values[0].is_a?(JSONAPI::ResourceFragment)
assert_equal 2, related_fragments.values[0].related_from.length
end

def test_find_related_has_one_fragments_cache_field
options = { cache: true }
source_rids = [JSONAPI::ResourceIdentity.new(V10::PostResource, 1),
JSONAPI::ResourceIdentity.new(V10::PostResource, 2),
JSONAPI::ResourceIdentity.new(V10::PostResource, 20)]
source_fragments = source_rids.collect {|rid| JSONAPI::ResourceFragment.new(rid) }

relationship = V10::PostResource._relationship('author')
related_fragments = V10::PostResource.find_included_fragments(source_fragments, relationship, options)

assert_equal 2, related_fragments.length
assert_equal JSONAPI::ResourceIdentity.new(V10::AuthorResource, 1001), related_fragments.keys[0]
assert_equal JSONAPI::ResourceIdentity.new(V10::AuthorResource, 1001), related_fragments.values[0].identity
assert related_fragments.values[0].is_a?(JSONAPI::ResourceFragment)
assert_equal 2, related_fragments.values[0].related_from.length
assert related_fragments.values[0].cache.is_a?(ActiveSupport::TimeWithZone)
end

def test_find_related_has_many_fragments
options = {}
source_rids = [JSONAPI::ResourceIdentity.new(V10::PostResource, 1),
JSONAPI::ResourceIdentity.new(V10::PostResource, 2),
JSONAPI::ResourceIdentity.new(V10::PostResource, 12),
JSONAPI::ResourceIdentity.new(V10::PostResource, 14)]
source_fragments = source_rids.collect {|rid| JSONAPI::ResourceFragment.new(rid) }

relationship = V10::PostResource._relationship('tags')
related_fragments = V10::PostResource.send(:find_included_fragments, source_fragments, relationship, options)

assert_equal 8, related_fragments.length
assert_equal JSONAPI::ResourceIdentity.new(V10::TagResource, 501), related_fragments.keys[0]
assert_equal JSONAPI::ResourceIdentity.new(V10::TagResource, 501), related_fragments.values[0].identity
assert related_fragments.values[0].is_a?(JSONAPI::ResourceFragment)
assert_equal 1, related_fragments.values[0].related_from.length
assert_equal 2, related_fragments[JSONAPI::ResourceIdentity.new(V10::TagResource, 502)].related_from.length
end

def test_find_related_has_many_fragments_pagination
params = ActionController::Parameters.new(number: 2, size: 4)
options = { paginator: PagedPaginator.new(params) }
source_rids = [JSONAPI::ResourceIdentity.new(V10::PostResource, 15)]
source_fragments = source_rids.collect {|rid| JSONAPI::ResourceFragment.new(rid) }

relationship = V10::PostResource._relationship('tags')
related_fragments = V10::PostResource.find_included_fragments(source_fragments, relationship, options)

assert_equal 1, related_fragments.length
assert_equal JSONAPI::ResourceIdentity.new(V10::TagResource, 516), related_fragments.keys[0]
assert_equal JSONAPI::ResourceIdentity.new(V10::TagResource, 516), related_fragments.values[0].identity
assert related_fragments.values[0].is_a?(JSONAPI::ResourceFragment)
assert_equal 1, related_fragments.values[0].related_from.length
end

def test_find_related_has_many_fragments_cache_field
options = { cache: true }
source_rids = [JSONAPI::ResourceIdentity.new(V10::PostResource, 1),
JSONAPI::ResourceIdentity.new(V10::PostResource, 2),
JSONAPI::ResourceIdentity.new(V10::PostResource, 12),
JSONAPI::ResourceIdentity.new(V10::PostResource, 14)]
source_fragments = source_rids.collect {|rid| JSONAPI::ResourceFragment.new(rid) }

relationship = V10::PostResource._relationship('tags')
related_fragments = V10::PostResource.find_included_fragments(source_fragments, relationship, options)

assert_equal 8, related_fragments.length
assert_equal JSONAPI::ResourceIdentity.new(V10::TagResource, 501), related_fragments.keys[0]
assert_equal JSONAPI::ResourceIdentity.new(V10::TagResource, 501), related_fragments.values[0].identity
assert related_fragments.values[0].is_a?(JSONAPI::ResourceFragment)
assert_equal 1, related_fragments.values[0].related_from.length
assert_equal 2, related_fragments[JSONAPI::ResourceIdentity.new(V10::TagResource, 502)].related_from.length
assert related_fragments.values[0].cache.is_a?(ActiveSupport::TimeWithZone)
end

def test_find_related_polymorphic_fragments
options = {}
source_rids = [JSONAPI::ResourceIdentity.new(V10::PictureResource, 1),
JSONAPI::ResourceIdentity.new(V10::PictureResource, 2),
JSONAPI::ResourceIdentity.new(V10::PictureResource, 3)]
source_fragments = source_rids.collect {|rid| JSONAPI::ResourceFragment.new(rid) }

relationship = V10::PictureResource._relationship('imageable')
related_fragments = V10::PictureResource.find_included_fragments(source_fragments, relationship, options)

assert_equal 2, related_fragments.length
assert related_fragments.keys.include?(JSONAPI::ResourceIdentity.new(V10::ProductResource, 1))
assert related_fragments.keys.include?(JSONAPI::ResourceIdentity.new(V10::DocumentResource, 1))

assert related_fragments.values[0].is_a?(JSONAPI::ResourceFragment)
assert_equal 1, related_fragments.values[0].related_from.length
assert_equal JSONAPI::ResourceIdentity.new(V10::ProductResource, 1), related_fragments.values[0].identity
end

def test_find_related_polymorphic_fragments_cache_field
options = { cache: true }
source_rids = [JSONAPI::ResourceIdentity.new(V10::PictureResource, 1),
JSONAPI::ResourceIdentity.new(V10::PictureResource, 2),
JSONAPI::ResourceIdentity.new(V10::PictureResource, 3)]
source_fragments = source_rids.collect {|rid| JSONAPI::ResourceFragment.new(rid) }

relationship = V10::PictureResource._relationship('imageable')
related_fragments = V10::PictureResource.find_included_fragments(source_fragments, relationship, options)

assert_equal 2, related_fragments.length
assert related_fragments.keys.include?(JSONAPI::ResourceIdentity.new(V10::ProductResource, 1))
assert related_fragments.keys.include?(JSONAPI::ResourceIdentity.new(V10::DocumentResource, 1))

assert related_fragments.values[0].is_a?(JSONAPI::ResourceFragment)
assert_equal 1, related_fragments.values[0].related_from.length
assert related_fragments.values[0].cache.is_a?(ActiveSupport::TimeWithZone)
assert related_fragments.values[1].cache.is_a?(ActiveSupport::TimeWithZone)
end

def test_find_related_polymorphic_fragments_not_cached
options = { cache: false }
source_rids = [JSONAPI::ResourceIdentity.new(V10::PictureResource, 1),
JSONAPI::ResourceIdentity.new(V10::PictureResource, 2),
JSONAPI::ResourceIdentity.new(V10::PictureResource, 3)]
source_fragments = source_rids.collect {|rid| JSONAPI::ResourceFragment.new(rid) }

relationship = V10::PictureResource._relationship('imageable')
related_fragments = V10::PictureResource.find_included_fragments(source_fragments, relationship, options)

assert_equal 2, related_fragments.length
assert related_fragments.keys.include?(JSONAPI::ResourceIdentity.new(V10::ProductResource, 1))
assert related_fragments.keys.include?(JSONAPI::ResourceIdentity.new(V10::DocumentResource, 1))

assert related_fragments.values[0].is_a?(JSONAPI::ResourceFragment)
assert_equal 1, related_fragments.values[0].related_from.length
end
end
Loading

0 comments on commit a2fc02f

Please sign in to comment.