diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml new file mode 100644 index 000000000..469d92873 --- /dev/null +++ b/.github/workflows/ruby.yml @@ -0,0 +1,75 @@ +name: CI + +on: + push: + branches: [ 'master', 'release-0-8', 'release-0-9', 'release-0-10' ] + pull_request: + branches: ['**'] + +jobs: + tests: + runs-on: ubuntu-latest + services: + mysql: + image: mysql + env: + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: test + ports: + - 3306:3306 + options: >- + --health-cmd "mysqladmin ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + postgres: + image: postgres + env: + POSTGRES_PASSWORD: password + POSTGRES_DB: test + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + strategy: + fail-fast: false + matrix: + ruby: + - 2.6.6 + - 2.7.2 + - 3.0.0 + rails: + - 6.1.1 + - 6.0.3.4 + - 5.2.4.4 + - 5.1.7 + database_url: + - mysql2://root:root@127.0.0.1:3306/test + - postgresql://postgres:password@localhost:5432/test + - sqlite3:test_db + exclude: + - ruby: 3.0.0 + rails: 6.0.3.4 + - ruby: 3.0.0 + rails: 5.2.4.4 + - ruby: 3.0.0 + rails: 5.1.7 + - database_url: postgresql://postgres:password@localhost:5432/test + rails: 5.1.7 + env: + RAILS_VERSION: ${{ matrix.rails }} + DATABASE_URL: ${{ matrix.database_url }} + name: Ruby ${{ matrix.ruby }} Rails ${{ matrix.rails }} DB ${{ matrix.database_url }} + steps: + - uses: actions/checkout@v2 + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + - name: Install dependencies + run: bundle install --jobs 4 --retry 3 + - name: Run tests + run: bundle exec rake test diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 679787e7d..000000000 --- a/.travis.yml +++ /dev/null @@ -1,31 +0,0 @@ -language: ruby -sudo: false -services: - - postgresql -env: - - RAILS_VERSION=6.0.0 DATABASE_URL=postgres://postgres@localhost/jr_test - - RAILS_VERSION=6.0.0 - - RAILS_VERSION=5.2.3 DATABASE_URL=postgres://postgres@localhost/jr_test - - RAILS_VERSION=5.2.3 - - RAILS_VERSION=5.1.7 - - RAILS_VERSION=5.0.7.2 - - RAILS_VERSION=4.2.11 -rvm: - - 2.4.9 - - 2.5.7 - - 2.6.5 -matrix: - exclude: - - rvm: 2.6.5 - env: "RAILS_VERSION=4.2.11" - - rvm: 2.4.9 - env: "RAILS_VERSION=6.0.0" - - rvm: 2.4.9 - env: "RAILS_VERSION=6.0.0 DATABASE_URL=postgres://postgres@localhost/jr_test" - - rvm: 2.4.9 - env: "RAILS_VERSION=5.2.3 DATABASE_URL=postgres://postgres@localhost/jr_test" -before_install: - - gem install bundler --version 1.17.3 -before_script: - - sh -c "if [ '$DATABASE_URL' = 'postgres://postgres@localhost/jr_test' ]; then psql -c 'DROP DATABASE IF EXISTS jr_test;' -U postgres; fi" - - sh -c "if [ '$DATABASE_URL' = 'postgres://postgres@localhost/jr_test' ]; then psql -c 'CREATE DATABASE jr_test;' -U postgres; fi" \ No newline at end of file diff --git a/Gemfile b/Gemfile index 2535d0200..5c866218f 100644 --- a/Gemfile +++ b/Gemfile @@ -10,6 +10,7 @@ version = ENV['RAILS_VERSION'] || 'default' platforms :ruby do gem 'pg' + gem 'mysql2' if version.start_with?('4.2', '5.0') gem 'sqlite3', '~> 1.3.13' @@ -26,4 +27,4 @@ when 'default' gem 'railties', '>= 6.0' else gem 'railties', "~> #{version}" -end \ No newline at end of file +end diff --git a/lib/jsonapi/active_relation/join_manager.rb b/lib/jsonapi/active_relation/join_manager.rb index 3d1ec34b5..ffa1cf0f1 100644 --- a/lib/jsonapi/active_relation/join_manager.rb +++ b/lib/jsonapi/active_relation/join_manager.rb @@ -147,9 +147,15 @@ def perform_joins(records, options) related_resource_klass = join_details[:related_resource_klass] join_type = relationship_details[:join_type] + join_options = { + relationship: relationship, + relationship_details: relationship_details, + related_resource_klass: related_resource_klass, + } + if relationship == :root unless source_relationship - add_join_details('', {alias: resource_klass._table_name, join_type: :root}) + add_join_details('', {alias: resource_klass._table_name, join_type: :root, join_options: join_options}) end next end @@ -163,7 +169,7 @@ def perform_joins(records, options) options: options) } - details = {alias: self.class.alias_from_arel_node(join_node), join_type: join_type} + details = {alias: self.class.alias_from_arel_node(join_node), join_type: join_type, join_options: join_options} if relationship == source_relationship if relationship.polymorphic? && relationship.belongs_to? diff --git a/lib/jsonapi/active_relation_resource.rb b/lib/jsonapi/active_relation_resource.rb index e2611613f..c2056776c 100644 --- a/lib/jsonapi/active_relation_resource.rb +++ b/lib/jsonapi/active_relation_resource.rb @@ -114,11 +114,11 @@ def find_fragments(filters, options = {}) # This alias is going to be resolve down to the model's table name and will not actually be an alias 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}")] + pluck_fields = [sql_field_with_alias(resource_table_alias, resource_klass._primary_key)] cache_field = attribute_to_model_field(:_cache_field) if options[:cache] if cache_field - pluck_fields << Arel.sql("#{concat_table_field(resource_table_alias, cache_field[:name])} AS #{resource_table_alias}_#{cache_field[:name]}") + pluck_fields << sql_field_with_alias(resource_table_alias, cache_field[:name]) end linkage_fields = [] @@ -133,7 +133,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 - pluck_fields << Arel.sql("#{concat_table_field(linkage_table_alias, primary_key)} AS #{linkage_table_alias}_#{primary_key}") + pluck_fields << sql_field_with_alias(linkage_table_alias, primary_key) end else klass = linkage_relationship.resource_klass @@ -141,7 +141,7 @@ def find_fragments(filters, options = {}) linkage_table_alias = join_manager.join_details_by_relationship(linkage_relationship)[:alias] primary_key = klass._primary_key - pluck_fields << Arel.sql("#{concat_table_field(linkage_table_alias, primary_key)} AS #{linkage_table_alias}_#{primary_key}") + pluck_fields << sql_field_with_alias(linkage_table_alias, primary_key) end end @@ -150,7 +150,7 @@ def find_fragments(filters, options = {}) attributes.try(:each) do |attribute| model_field = resource_klass.attribute_to_model_field(attribute) model_fields[attribute] = model_field - pluck_fields << Arel.sql("#{concat_table_field(resource_table_alias, model_field[:name])} AS #{resource_table_alias}_#{model_field[:name]}") + pluck_fields << sql_field_with_alias(resource_table_alias, model_field[:name]) end sort_fields = options.dig(:_relation_helper_options, :sort_fields) @@ -324,6 +324,11 @@ def apply_join(records:, relationship:, resource_type:, join_type:, options:) records = records.joins_left(relation_name) end end + + if relationship.use_related_resource_records_for_joins + records = records.merge(self.records(options)) + end + records end @@ -409,13 +414,13 @@ def find_related_monomorphic_fragments(source_rids, relationship, options, conne 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}") + Arel.sql("#{_table_name}.#{_primary_key} AS \"source_id\""), + sql_field_with_alias(resource_table_alias, resource_klass._primary_key) ] cache_field = resource_klass.attribute_to_model_field(:_cache_field) if options[:cache] if cache_field - pluck_fields << Arel.sql("#{concat_table_field(resource_table_alias, cache_field[:name])} AS #{resource_table_alias}_#{cache_field[:name]}") + pluck_fields << sql_field_with_alias(resource_table_alias, cache_field[:name]) end linkage_fields = [] @@ -430,7 +435,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 - pluck_fields << Arel.sql("#{concat_table_field(linkage_table_alias, primary_key)} AS #{linkage_table_alias}_#{primary_key}") + pluck_fields << sql_field_with_alias(linkage_table_alias, primary_key) end else klass = linkage_relationship.resource_klass @@ -438,7 +443,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 - pluck_fields << Arel.sql("#{concat_table_field(linkage_table_alias, primary_key)} AS #{linkage_table_alias}_#{primary_key}") + pluck_fields << sql_field_with_alias(linkage_table_alias, primary_key) end end @@ -447,7 +452,7 @@ def find_related_monomorphic_fragments(source_rids, relationship, options, conne attributes.try(:each) do |attribute| model_field = resource_klass.attribute_to_model_field(attribute) model_fields[attribute] = model_field - pluck_fields << Arel.sql("#{concat_table_field(resource_table_alias, model_field[:name])} AS #{resource_table_alias}_#{model_field[:name]}") + pluck_fields << sql_field_with_alias(resource_table_alias, model_field[:name]) end sort_fields = options.dig(:_relation_helper_options, :sort_fields) @@ -543,9 +548,9 @@ def find_related_polymorphic_fragments(source_rids, relationship, options, conne 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}") + Arel.sql("#{primary_key} AS #{alias_table_field(_table_name, _primary_key)}"), + Arel.sql("#{related_key} AS #{alias_table_field(_table_name, relationship.foreign_key)}"), + Arel.sql("#{related_type} AS #{alias_table_field(_table_name, relationship.polymorphic_type)}") ] # Get the additional fields from each relation. There's a limitation that the fields must exist in each relation @@ -570,7 +575,7 @@ def find_related_polymorphic_fragments(source_rids, relationship, options, conne cache_offset = relation_index if cache_field - pluck_fields << Arel.sql("#{concat_table_field(table_alias, cache_field[:name])} AS cache_#{type}_#{cache_field[:name]}") + pluck_fields << sql_field_with_alias(table_alias, cache_field[:name]) relation_index+= 1 end @@ -579,7 +584,7 @@ def find_related_polymorphic_fragments(source_rids, relationship, options, conne attributes.try(:each) do |attribute| model_field = related_klass.attribute_to_model_field(attribute) model_fields[attribute] = model_field - pluck_fields << Arel.sql("#{concat_table_field(table_alias, model_field[:name])} AS #{table_alias}_#{model_field[:name]}") + pluck_fields << sql_field_with_alias(table_alias, model_field[:name]) relation_index+= 1 end @@ -616,7 +621,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 - pluck_fields << Arel.sql("#{concat_table_field(linkage_table_alias, primary_key)} AS #{linkage_table_alias}_#{primary_key}") + pluck_fields << sql_field_with_alias(linkage_table_alias, primary_key) end else klass = linkage_relationship.resource_klass @@ -624,7 +629,7 @@ def find_related_polymorphic_fragments(source_rids, relationship, options, conne linkage_table_alias = join_manager.join_details_by_relationship(linkage_relationship)[:alias] primary_key = klass._primary_key - pluck_fields << Arel.sql("#{concat_table_field(linkage_table_alias, primary_key)} AS #{linkage_table_alias}_#{primary_key}") + pluck_fields << sql_field_with_alias(linkage_table_alias, primary_key) end end @@ -649,7 +654,7 @@ def find_related_polymorphic_fragments(source_rids, relationship, options, conne end end - relation_position = relation_positions[row[2].downcase.pluralize] + relation_position = relation_positions[row[2].underscore.pluralize] model_fields = relation_position[:model_fields] cache_field = relation_position[:cache_field] cache_offset = relation_position[:cache_offset] @@ -756,7 +761,7 @@ def apply_single_sort(records, field, direction, options) # Assumes ActiveRecord's counting. Override if you need a different counting method def count_records(records) - if Rails::VERSION::MAJOR >= 5 && ActiveRecord::VERSION::MINOR >= 1 + if (Rails::VERSION::MAJOR == 5 && ActiveRecord::VERSION::MINOR >= 1) || Rails::VERSION::MAJOR >= 6 records.count(:all) else records.count @@ -790,7 +795,31 @@ def concat_table_field(table, field, quoted = false) if table.blank? || field.to_s.include?('.') # :nocov: if quoted - "\"#{field.to_s}\"" + quote(field) + else + field.to_s + end + # :nocov: + else + if quoted + "#{quote(table)}.#{quote(field)}" + else + # :nocov: + "#{table.to_s}.#{field.to_s}" + # :nocov: + end + end + end + + def sql_field_with_alias(table, field, quoted = true) + Arel.sql("#{concat_table_field(table, field, quoted)} AS #{alias_table_field(table, field, quoted)}") + end + + def alias_table_field(table, field, quoted = false) + if table.blank? || field.to_s.include?('.') + # :nocov: + if quoted + quote(field) else field.to_s end @@ -798,14 +827,19 @@ def concat_table_field(table, field, quoted = false) else if quoted # :nocov: - "\"#{table.to_s}\".\"#{field.to_s}\"" + quote("#{table.to_s}_#{field.to_s}") # :nocov: else - "#{table.to_s}.#{field.to_s}" + "#{table.to_s}_#{field.to_s}" end end end + def quote(field) + "\"#{field.to_s}\"" + end + + def apply_filters(records, filters, options = {}) if filters filters.each do |filter, value| diff --git a/lib/jsonapi/acts_as_resource_controller.rb b/lib/jsonapi/acts_as_resource_controller.rb index a882a564a..57c232016 100644 --- a/lib/jsonapi/acts_as_resource_controller.rb +++ b/lib/jsonapi/acts_as_resource_controller.rb @@ -161,8 +161,8 @@ def resource_klass_name def verify_content_type_header if ['create', 'create_relationship', 'update_relationship', 'update'].include?(params[:action]) - unless request.content_type == JSONAPI::MEDIA_TYPE - fail JSONAPI::Exceptions::UnsupportedMediaTypeError.new(request.content_type) + unless request.media_type == JSONAPI::MEDIA_TYPE + fail JSONAPI::Exceptions::UnsupportedMediaTypeError.new(request.media_type) end end true diff --git a/lib/jsonapi/basic_resource.rb b/lib/jsonapi/basic_resource.rb index ea8b19ea7..ecd6e4778 100644 --- a/lib/jsonapi/basic_resource.rb +++ b/lib/jsonapi/basic_resource.rb @@ -873,7 +873,7 @@ def _polymorphic_name if !_polymorphic '' else - @_polymorphic_name ||= _model_name.to_s.downcase + @_polymorphic_name ||= _model_name.to_s.underscore end end @@ -927,7 +927,7 @@ def _polymorphic_types next unless Module === klass if klass < ActiveRecord::Base klass.reflect_on_all_associations(:has_many).select{|r| r.options[:as] }.each do |reflection| - (hash[reflection.options[:as]] ||= []) << klass.name.downcase + (hash[reflection.options[:as]] ||= []) << klass.name.underscore end end end @@ -1058,7 +1058,7 @@ def default_sort end def construct_order_options(sort_params) - sort_params ||= default_sort + sort_params = default_sort if sort_params.blank? return {} unless sort_params diff --git a/lib/jsonapi/cached_response_fragment.rb b/lib/jsonapi/cached_response_fragment.rb index 4f2abccdb..b1038921f 100644 --- a/lib/jsonapi/cached_response_fragment.rb +++ b/lib/jsonapi/cached_response_fragment.rb @@ -51,8 +51,8 @@ def initialize(resource_klass, id, type, context, fetchable_fields, relationship @fetchable_fields = Set.new(fetchable_fields) # Relationships left uncompiled because we'll often want to insert included ids on retrieval - @relationships = relationships - + # Remove the data since that should not be cached + @relationships = relationships&.transform_values {|v| v.delete_if {|k, _v| k == 'data'} } @links_json = CompiledJson.of(links_json) @attributes_json = CompiledJson.of(attributes_json) @meta_json = CompiledJson.of(meta_json) diff --git a/lib/jsonapi/configuration.rb b/lib/jsonapi/configuration.rb index f7e899cfa..834ca1b1c 100644 --- a/lib/jsonapi/configuration.rb +++ b/lib/jsonapi/configuration.rb @@ -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, + :use_related_resource_records_for_joins def initialize #:underscored_key, :camelized_key, :dasherized_key, or custom @@ -158,6 +159,11 @@ 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 + + # Use a related resource's `records` when performing joins. This setting allows included resources to account for + # permission scopes. It can be overridden explicitly per relationship. Furthermore, specifying a `relation_name` + # on a relationship will cause this setting to be ignored. + self.use_related_resource_records_for_joins = true end def cache_formatters=(bool) @@ -299,6 +305,8 @@ def allow_include=(allow_include) attr_writer :resource_cache_usage_report_function attr_writer :default_exclude_links + + attr_writer :use_related_resource_records_for_joins end class << self diff --git a/lib/jsonapi/error.rb b/lib/jsonapi/error.rb index a5d878af8..334ae1ccd 100644 --- a/lib/jsonapi/error.rb +++ b/lib/jsonapi/error.rb @@ -15,7 +15,7 @@ def initialize(options = {}) @source = options[:source] @links = options[:links] - @status = Rack::Utils::SYMBOL_TO_STATUS_CODE[options[:status]].to_s + @status = Rack::Utils.status_code(options[:status]).to_s @meta = options[:meta] end @@ -46,7 +46,7 @@ def update_with_overrides(error_object_overrides) if error_object_overrides[:status] # :nocov: - @status = Rack::Utils::SYMBOL_TO_STATUS_CODE[error_object_overrides[:status]].to_s + @status = Rack::Utils.status_code(error_object_overrides[:status]).to_s # :nocov: end @meta = error_object_overrides[:meta] || @meta diff --git a/lib/jsonapi/paginator.rb b/lib/jsonapi/paginator.rb index 53f8fbbe4..3e1ac6989 100644 --- a/lib/jsonapi/paginator.rb +++ b/lib/jsonapi/paginator.rb @@ -13,9 +13,16 @@ def links_page_params(_options = {}) # :nocov: end + def requires_record_count + # :nocov: + self.class.requires_record_count + # :nocov: + end + class << self def requires_record_count # :nocov: + # @deprecated false # :nocov: end @@ -36,10 +43,15 @@ def initialize(params) verify_pagination_params end + # @deprecated def self.requires_record_count true end + def requires_record_count + true + end + def apply(relation, _order_options) relation.offset(@offset).limit(@limit) end @@ -127,10 +139,15 @@ def initialize(params) verify_pagination_params end + # @deprecated def self.requires_record_count true end + def requires_record_count + true + end + def calculate_page_count(record_count) (record_count / @size.to_f).ceil end diff --git a/lib/jsonapi/path_segment.rb b/lib/jsonapi/path_segment.rb index aa2d78050..c0bac6536 100644 --- a/lib/jsonapi/path_segment.rb +++ b/lib/jsonapi/path_segment.rb @@ -30,7 +30,7 @@ def initialize(relationship:, resource_klass: nil) end def eql?(other) - relationship == other.relationship && resource_klass == other.resource_klass + other.is_a?(JSONAPI::PathSegment::Relationship) && relationship == other.relationship && resource_klass == other.resource_klass end def hash @@ -59,7 +59,7 @@ def initialize(resource_klass:, field_name:) end def eql?(other) - field_name == other.field_name && resource_klass == other.resource_klass + other.is_a?(JSONAPI::PathSegment::Field) && field_name == other.field_name && resource_klass == other.resource_klass end def delegated_field_name diff --git a/lib/jsonapi/processor.rb b/lib/jsonapi/processor.rb index de3459c82..e90acf41b 100644 --- a/lib/jsonapi/processor.rb +++ b/lib/jsonapi/processor.rb @@ -65,13 +65,13 @@ def find resource_set.populate!(serializer, context, find_options) page_options = result_options - if (JSONAPI.configuration.top_level_meta_include_record_count || (paginator && paginator.class.requires_record_count)) + if (top_level_meta_include_record_count || (paginator && paginator.requires_record_count)) page_options[:record_count] = resource_klass.count(verified_filters, context: context, include_directives: include_directives) end - if (JSONAPI.configuration.top_level_meta_include_page_count && paginator && page_options[:record_count]) + if (top_level_meta_include_page_count && paginator && page_options[:record_count]) page_options[:page_count] = paginator ? paginator.calculate_page_count(page_options[:record_count]) : 1 end @@ -197,9 +197,9 @@ def show_related_resources resource_set.populate!(serializer, context, find_options) opts = result_options - if ((JSONAPI.configuration.top_level_meta_include_record_count) || - (paginator && paginator.class.requires_record_count) || - (JSONAPI.configuration.top_level_meta_include_page_count)) + if ((top_level_meta_include_record_count) || + (paginator && paginator.requires_record_count) || + (top_level_meta_include_page_count)) opts[:record_count] = source_resource.class.count_related( source_resource.identity, @@ -207,13 +207,13 @@ def show_related_resources find_options) end - if (JSONAPI.configuration.top_level_meta_include_page_count && opts[:record_count]) + if (top_level_meta_include_page_count && opts[:record_count]) opts[:page_count] = paginator.calculate_page_count(opts[:record_count]) end opts[:pagination_params] = if paginator && JSONAPI.configuration.top_level_links_include_pagination page_options = {} - page_options[:record_count] = opts[:record_count] if paginator.class.requires_record_count + page_options[:record_count] = opts[:record_count] if paginator.requires_record_count paginator.links_page_params(page_options.merge(fetched_resources: resource_set)) else {} @@ -382,6 +382,14 @@ def find_related_resource_set(resource, relationship_name, include_directives, o JSONAPI::ResourceSet.new(resource_id_tree) end + def top_level_meta_include_record_count + JSONAPI.configuration.top_level_meta_include_record_count + end + + def top_level_meta_include_page_count + JSONAPI.configuration.top_level_meta_include_page_count + end + private def find_related_resource_id_tree(resource_klass, source_id, relationship_name, find_options, include_related) options = find_options.except(:include_directives) diff --git a/lib/jsonapi/relationship.rb b/lib/jsonapi/relationship.rb index 77e700b78..5261b6049 100644 --- a/lib/jsonapi/relationship.rb +++ b/lib/jsonapi/relationship.rb @@ -3,7 +3,7 @@ class Relationship attr_reader :acts_as_set, :foreign_key, :options, :name, :class_name, :polymorphic, :always_include_optional_linkage_data, :parent_resource, :eager_load_on_include, :custom_methods, - :inverse_relationship, :allow_include + :inverse_relationship, :allow_include, :use_related_resource_records_for_joins attr_writer :allow_include @@ -23,6 +23,15 @@ def initialize(name, options = {}) @polymorphic_types ||= options[:polymorphic_relations] end + use_related_resource_records_for_joins_default = if options[:relation_name] + false + else + JSONAPI.configuration.use_related_resource_records_for_joins + end + + @use_related_resource_records_for_joins = options.fetch(:use_related_resource_records_for_joins, + use_related_resource_records_for_joins_default) == true + @always_include_optional_linkage_data = options.fetch(:always_include_optional_linkage_data, false) == true @eager_load_on_include = options.fetch(:eager_load_on_include, false) == true @allow_include = options[:allow_include] @@ -63,7 +72,7 @@ def self.polymorphic_types(name) 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 + (hash[reflection.options[:as]] ||= []) << klass.name.underscore end end end diff --git a/lib/jsonapi/request_parser.rb b/lib/jsonapi/request_parser.rb index b8a8a8de9..4d72e911e 100644 --- a/lib/jsonapi/request_parser.rb +++ b/lib/jsonapi/request_parser.rb @@ -418,7 +418,7 @@ def parse_sort_criteria(resource_klass, sort_criteria) sorts = sort_criteria elsif sort_criteria.is_a?(String) begin - raw = URI.unescape(sort_criteria) + raw = URI.decode_www_form_component(sort_criteria) sorts = CSV.parse_line(raw) rescue CSV::MalformedCSVError fail JSONAPI::Exceptions::InvalidSortCriteria.new(format_key(resource_klass._type), raw) diff --git a/lib/jsonapi/resource_controller_metal.rb b/lib/jsonapi/resource_controller_metal.rb index c950e4659..f6f82e246 100644 --- a/lib/jsonapi/resource_controller_metal.rb +++ b/lib/jsonapi/resource_controller_metal.rb @@ -5,10 +5,10 @@ class ResourceControllerMetal < ActionController::Metal ActionController::Rendering, ActionController::Renderers::All, ActionController::StrongParameters, - ActionController::ForceSSL, + Gem::Requirement.new('< 6.1').satisfied_by?(ActionPack.gem_version) ? ActionController::ForceSSL : nil, ActionController::Instrumentation, JSONAPI::ActsAsResourceController - ].freeze + ].compact.freeze # Note, the url_helpers are not loaded. This will prevent links from being generated for resources, and warnings # will be emitted. Link support can be added by including `Rails.application.routes.url_helpers`, and links diff --git a/lib/jsonapi/resources/version.rb b/lib/jsonapi/resources/version.rb index 49297ca88..db9cbe9bc 100644 --- a/lib/jsonapi/resources/version.rb +++ b/lib/jsonapi/resources/version.rb @@ -1,5 +1,5 @@ module JSONAPI module Resources - VERSION = '0.10.2' + VERSION = '0.10.7' end end diff --git a/test/controllers/controller_test.rb b/test/controllers/controller_test.rb index e815a0e24..3b27f96a5 100644 --- a/test/controllers/controller_test.rb +++ b/test/controllers/controller_test.rb @@ -304,6 +304,22 @@ def test_index_filter_not_allowed JSONAPI.configuration.allow_filter = true end + def test_cached_result_does_not_include_relationship_data + JSONAPI.configuration.resource_cache = ActiveSupport::Cache::MemoryStore.new + JSONAPI.configuration.default_caching = true + + get :show, params: {id: '1', include: 'author'} + assert_response :success + assert json_response['data']['relationships']['author']['data'] + + get :show, params: {id: '1'} + assert_response :success + refute json_response['data']['relationships']['author']['data'] + ensure + JSONAPI.configuration.resource_cache = nil + JSONAPI.configuration.default_caching = false + end + def test_index_include_one_level_query_count assert_query_count(4) do assert_cacheable_get :index, params: {include: 'author'} diff --git a/test/fixtures/active_record.rb b/test/fixtures/active_record.rb index bdb718bbf..6170eca81 100644 --- a/test/fixtures/active_record.rb +++ b/test/fixtures/active_record.rb @@ -503,7 +503,7 @@ class Post < ActiveRecord::Base belongs_to :writer, class_name: 'Person', foreign_key: 'author_id' has_many :comments has_and_belongs_to_many :tags, join_table: :posts_tags - has_many :special_post_tags, source: :tag + has_many :special_post_tags has_many :special_tags, through: :special_post_tags, source: :tag belongs_to :section belongs_to :parent_post, class_name: 'Post', foreign_key: 'parent_post_id' @@ -745,8 +745,8 @@ class Picture < ActiveRecord::Base belongs_to :author, class_name: 'Person', foreign_key: 'author_id' belongs_to :imageable, polymorphic: true - belongs_to :document, -> { where( pictures: { imageable_type: 'Document' } ).eager_load( :pictures ) }, foreign_key: 'imageable_id' - belongs_to :product, -> { where( pictures: { imageable_type: 'Product' } ).eager_load( :pictures ) }, foreign_key: 'imageable_id' + 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' end @@ -1413,7 +1413,7 @@ class PostResource < JSONAPI::Resource has_one :author, class_name: 'Person' has_one :section - has_many :tags, acts_as_set: true, inverse_relationship: :posts, eager_load_on_include: false + has_many :tags, acts_as_set: true, inverse_relationship: :posts has_many :comments, acts_as_set: false, inverse_relationship: :post # Not needed - just for testing @@ -1932,16 +1932,7 @@ class AuthorResource < JSONAPI::Resource model_name 'Person' attributes :name - has_many :books, inverse_relationship: :authors, relation_name: -> (options) { - book_admin = options[:context][:book_admin] || options[:context][:current_user].try(:book_admin) - - if book_admin - :books - else - :not_banned_books - end - } - + has_many :books has_many :book_comments end @@ -1981,6 +1972,9 @@ class BookResource < JSONAPI::Resource } filter :banned, apply: :apply_filter_banned + filter :title, apply: ->(records, value, options) { + records.where('books.title LIKE ?', "#{value[0]}%") + } class << self def books @@ -1992,10 +1986,9 @@ def not_banned_books end def records(options = {}) - context = options[:context] - current_user = context ? context[:current_user] : nil + current_user = options.dig(:context, :current_user) - records = _model_class.all + records = super # Hide the banned books from people who are not book admins unless current_user && current_user.book_admin records = records.where(not_banned_books) @@ -2004,8 +1997,7 @@ def records(options = {}) end def apply_filter_banned(records, value, options) - context = options[:context] - current_user = context ? context[:current_user] : nil + current_user = options.dig(:context, :current_user) # Only book admins might filter for banned books if current_user && current_user.book_admin @@ -2045,7 +2037,7 @@ def approved_comments(approved = true) end def records(options = {}) - current_user = options[:context][:current_user] + current_user = options.dig(:context, :current_user) _model_class.for_user(current_user) end end @@ -2100,7 +2092,7 @@ class PostResource < JSONAPI::Resource has_one :author, class_name: 'Person', exclude_links: [:self, "related"] has_one :section, exclude_links: [:self, :related] - has_many :tags, acts_as_set: true, inverse_relationship: :posts, eager_load_on_include: false, exclude_links: :default + has_many :tags, acts_as_set: true, inverse_relationship: :posts, exclude_links: :default has_many :comments, acts_as_set: false, inverse_relationship: :post, exclude_links: ["self", :related] end diff --git a/test/helpers/functional_helpers.rb b/test/helpers/functional_helpers.rb index e0f504df2..3d6dc9d34 100644 --- a/test/helpers/functional_helpers.rb +++ b/test/helpers/functional_helpers.rb @@ -32,8 +32,8 @@ module FunctionalHelpers # end # end # - # if @response.content_type - # ct = @response.content_type + # if @response.media_type + # ct = @response.media_type # elsif methods.include?('assert_response_response') # ct = assert_response_response # else diff --git a/test/integration/book_authorization_test.rb b/test/integration/book_authorization_test.rb new file mode 100644 index 000000000..6327f2ba3 --- /dev/null +++ b/test/integration/book_authorization_test.rb @@ -0,0 +1,38 @@ +require File.expand_path('../../test_helper', __FILE__) + +class BookAuthorizationTest < ActionDispatch::IntegrationTest + def setup + DatabaseCleaner.start + JSONAPI.configuration.json_key_format = :underscored_key + JSONAPI.configuration.route_format = :underscored_route + Api::V2::BookResource.paginator :offset + end + + def test_restricted_records_primary + Api::V2::BookResource.paginator :none + + # Not a book admin + $test_user = Person.find(1001) + assert_cacheable_jsonapi_get '/api/v2/books?filter[title]=Book%206' + assert_equal 12, json_response['data'].size + + # book_admin + $test_user = Person.find(1005) + assert_cacheable_jsonapi_get '/api/v2/books?filter[title]=Book%206' + assert_equal 111, json_response['data'].size + end + + def test_restricted_records_related + Api::V2::BookResource.paginator :none + + # Not a book admin + $test_user = Person.find(1001) + assert_cacheable_jsonapi_get '/api/v2/authors/1002/books' + assert_equal 1, json_response['data'].size + + # book_admin + $test_user = Person.find(1005) + assert_cacheable_jsonapi_get '/api/v2/authors/1002/books' + assert_equal 2, json_response['data'].size + end +end diff --git a/test/integration/requests/request_test.rb b/test/integration/requests/request_test.rb index e59cdd580..1863b5c7d 100644 --- a/test/integration/requests/request_test.rb +++ b/test/integration/requests/request_test.rb @@ -1608,7 +1608,7 @@ def test_caching_included_singleton } $test_user = Person.find(1001) - assert_equal 2, JSONAPI.configuration.resource_cache.instance_variable_get(:@key_access).length + assert_equal 2, JSONAPI.configuration.resource_cache.instance_variable_get(:@data).length get "/api/v9/people/#{$test_user.id}?include=preferences" assert_jsonapi_response 200 @@ -1655,7 +1655,7 @@ def test_caching_included_singleton ] } - assert_equal 4, JSONAPI.configuration.resource_cache.instance_variable_get(:@key_access).length + assert_equal 4, JSONAPI.configuration.resource_cache.instance_variable_get(:@data).length ensure JSONAPI.configuration = original_config @@ -1700,7 +1700,7 @@ def test_caching_singleton_primary } } - assert_equal 1, JSONAPI.configuration.resource_cache.instance_variable_get(:@key_access).length + assert_equal 1, JSONAPI.configuration.resource_cache.instance_variable_get(:@data).length $test_user = Person.find(1001) @@ -1728,7 +1728,7 @@ def test_caching_singleton_primary } } - assert_equal 2, JSONAPI.configuration.resource_cache.instance_variable_get(:@key_access).length + assert_equal 2, JSONAPI.configuration.resource_cache.instance_variable_get(:@data).length ensure JSONAPI.configuration = original_config diff --git a/test/test_helper.rb b/test/test_helper.rb index 97e51fe7d..0cb0489b9 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -88,9 +88,9 @@ class Engine < ::Rails::Engine # Monkeypatch ActionController::TestCase to delete the RAW_POST_DATA on subsequent calls in the same test. if Rails::VERSION::MAJOR >= 5 module ClearRawPostHeader - def process(action, *args) + def process(action, **args) @request.delete_header 'RAW_POST_DATA' - super + super action, **args end end @@ -513,7 +513,7 @@ class ActionDispatch::IntegrationTest fixtures :all def assert_jsonapi_response(expected_status, msg = nil) - assert_equal JSONAPI::MEDIA_TYPE, response.content_type + assert_equal JSONAPI::MEDIA_TYPE, response.media_type if status != expected_status && status >= 400 pp json_response rescue nil end @@ -560,13 +560,13 @@ def assert_cacheable_jsonapi_get(url, cached_classes = :all) end class ActionController::TestCase - def assert_cacheable_get(action, *args) + def assert_cacheable_get(action, **args) assert_nil JSONAPI.configuration.resource_cache normal_queries = [] normal_query_callback = lambda {|_, _, _, _, payload| normal_queries.push payload[:sql] } ActiveSupport::Notifications.subscribed(normal_query_callback, 'sql.active_record') do - get action, *args + get action, **args end non_caching_response = json_response_sans_all_backtraces non_caching_status = response.status @@ -602,7 +602,7 @@ def assert_cacheable_get(action, *args) @controller = nil setup_controller_request_and_response @request.headers.merge!(orig_request_headers.dup) - get action, *args + get action, **args end end rescue Exception diff --git a/test/unit/active_relation_resource_finder/join_manager_test.rb b/test/unit/active_relation_resource_finder/join_manager_test.rb index 9fb13a82b..2b38d0642 100644 --- a/test/unit/active_relation_resource_finder/join_manager_test.rb +++ b/test/unit/active_relation_resource_finder/join_manager_test.rb @@ -23,7 +23,7 @@ def test_no_added_joins records = join_manager.join(records, {}) assert_equal 'SELECT "posts".* FROM "posts"', records.to_sql - assert_hash_equals({alias: 'posts', join_type: :root}, join_manager.source_join_details) + assert_hash_equals({alias: 'posts', join_type: :root}, join_manager.source_join_details.except!(:join_options)) end def test_add_single_join @@ -32,8 +32,22 @@ def test_add_single_join 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"', 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: 'posts', join_type: :root}, join_manager.source_join_details.except!(:join_options)) + assert_hash_equals({alias: 'tags', join_type: :left}, join_manager.join_details_by_relationship(PostResource._relationship(:tags)).except!(:join_options)) + end + + def test_joins_have_join_options + filters = {'tags' => ['1']} + join_manager = JSONAPI::ActiveRelation::JoinManager.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"', records.to_sql + + source_join_options = join_manager.source_join_details[:join_options] + assert_array_equals [:relationship, :relationship_details, :related_resource_klass], source_join_options.keys + + relationship_join_options = join_manager.join_details_by_relationship(PostResource._relationship(:tags))[:join_options] + assert_array_equals [:relationship, :relationship_details, :related_resource_klass], relationship_join_options.keys end def test_add_single_sort_join @@ -43,8 +57,8 @@ def test_add_single_sort_join 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"', 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: 'posts', join_type: :root}, join_manager.source_join_details.except!(:join_options)) + assert_hash_equals({alias: 'tags', join_type: :left}, join_manager.join_details_by_relationship(PostResource._relationship(:tags)).except!(:join_options)) end def test_add_single_sort_and_filter_join @@ -54,8 +68,8 @@ def test_add_single_sort_and_filter_join 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"', 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: 'posts', join_type: :root}, join_manager.source_join_details.except!(:join_options)) + assert_hash_equals({alias: 'tags', join_type: :left}, join_manager.join_details_by_relationship(PostResource._relationship(:tags)).except!(:join_options)) end def test_add_sibling_joins @@ -69,9 +83,9 @@ def test_add_sibling_joins 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"', 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))) + assert_hash_equals({alias: 'posts', join_type: :root}, join_manager.source_join_details.except!(:join_options)) + assert_hash_equals({alias: 'tags', join_type: :left}, join_manager.join_details_by_relationship(PostResource._relationship(:tags)).except!(:join_options)) + assert_hash_equals({alias: 'people', join_type: :left}, join_manager.join_details_by_relationship(PostResource._relationship(:author)).except!(:join_options)) end @@ -82,7 +96,7 @@ def test_add_joins_source_relationship records = join_manager.join(records, {}) assert_equal 'SELECT "posts".* FROM "posts" INNER JOIN "comments" ON "comments"."post_id" = "posts"."id"', records.to_sql - assert_hash_equals({alias: 'comments', join_type: :inner}, join_manager.source_join_details) + assert_hash_equals({alias: 'comments', join_type: :inner}, join_manager.source_join_details.except!(:join_options)) end @@ -96,7 +110,7 @@ def test_add_joins_source_relationship_with_custom_apply assert_equal sql, records.to_sql - assert_hash_equals({alias: 'comments', join_type: :inner}, join_manager.source_join_details) + assert_hash_equals({alias: 'comments', join_type: :inner}, join_manager.source_join_details.except!(:join_options)) end def test_add_nested_scoped_joins @@ -110,15 +124,11 @@ def test_add_nested_scoped_joins records = Api::V10::PostResource.records({}) records = join_manager.join(records, {}) - sql = 'SELECT "posts".* FROM "posts" LEFT OUTER JOIN "comments" ON "comments"."post_id" = "posts"."id" LEFT OUTER JOIN "people" ON "people"."id" = "posts"."author_id" LEFT OUTER JOIN "people" "authors_comments" ON "authors_comments"."id" = "comments"."author_id" LEFT OUTER JOIN "comments_tags" ON "comments_tags"."comment_id" = "comments"."id" LEFT OUTER JOIN "tags" ON "tags"."id" = "comments_tags"."tag_id" WHERE "comments"."approved" = ' + db_true + ' AND "author"."special" = ' + db_true - - assert_equal sql, records.to_sql - - 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))) + assert_hash_equals({alias: 'posts', join_type: :root}, join_manager.source_join_details.except!(:join_options)) + assert_hash_equals({alias: 'comments', join_type: :left}, join_manager.join_details_by_relationship(Api::V10::PostResource._relationship(:comments)).except!(:join_options)) + assert_hash_equals({alias: 'authors_comments', join_type: :left}, join_manager.join_details_by_relationship(Api::V10::CommentResource._relationship(:author)).except!(:join_options)) + assert_hash_equals({alias: 'tags', join_type: :left}, join_manager.join_details_by_relationship(Api::V10::CommentResource._relationship(:tags)).except!(:join_options)) + assert_hash_equals({alias: 'people', join_type: :left}, join_manager.join_details_by_relationship(Api::V10::PostResource._relationship(:author)).except!(:join_options)) # Now test with different order for the filters filters = { @@ -131,33 +141,11 @@ def test_add_nested_scoped_joins records = Api::V10::PostResource.records({}) records = join_manager.join(records, {}) - # Note sql is in different order, but aliases should still be right - sql = 'SELECT "posts".* FROM "posts" LEFT OUTER JOIN "people" ON "people"."id" = "posts"."author_id" LEFT OUTER JOIN "comments" ON "comments"."post_id" = "posts"."id" LEFT OUTER JOIN "people" "authors_comments" ON "authors_comments"."id" = "comments"."author_id" LEFT OUTER JOIN "comments_tags" ON "comments_tags"."comment_id" = "comments"."id" LEFT OUTER JOIN "tags" ON "tags"."id" = "comments_tags"."tag_id" WHERE "comments"."approved" = ' + db_true + ' AND "author"."special" = ' + db_true - - assert_equal sql, records.to_sql - - 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))) - - # Easier to read SQL to show joins are the same, but in different order - # Pass 1 - # SELECT "posts".* FROM "posts" - # LEFT OUTER JOIN "comments" ON "comments"."post_id" = "posts"."id" - # LEFT OUTER JOIN "people" ON "people"."id" = "posts"."author_id" - # LEFT OUTER JOIN "people" "authors_comments" ON "authors_comments"."id" = "comments"."author_id" - # LEFT OUTER JOIN "comments_tags" ON "comments_tags"."comment_id" = "comments"."id" - # LEFT OUTER JOIN "tags" ON "tags"."id" = "comments_tags"."tag_id" WHERE "comments"."approved" = 1 AND "author"."special" = 1 - # - # Pass 2 - # SELECT "posts".* FROM "posts" - # LEFT OUTER JOIN "people" ON "people"."id" = "posts"."author_id" - # LEFT OUTER JOIN "comments" ON "comments"."post_id" = "posts"."id" - # LEFT OUTER JOIN "people" "authors_comments" ON "authors_comments"."id" = "comments"."author_id" - # LEFT OUTER JOIN "comments_tags" ON "comments_tags"."comment_id" = "comments"."id" - # LEFT OUTER JOIN "tags" ON "tags"."id" = "comments_tags"."tag_id" WHERE "comments"."approved" = 1 AND "author"."special" = 1 + assert_hash_equals({alias: 'posts', join_type: :root}, join_manager.source_join_details.except!(:join_options)) + assert_hash_equals({alias: 'comments', join_type: :left}, join_manager.join_details_by_relationship(Api::V10::PostResource._relationship(:comments)).except!(:join_options)) + assert_hash_equals({alias: 'authors_comments', join_type: :left}, join_manager.join_details_by_relationship(Api::V10::CommentResource._relationship(:author)).except!(:join_options)) + assert_hash_equals({alias: 'tags', join_type: :left}, join_manager.join_details_by_relationship(Api::V10::CommentResource._relationship(:tags)).except!(:join_options)) + assert_hash_equals({alias: 'people', join_type: :left}, join_manager.join_details_by_relationship(Api::V10::PostResource._relationship(:author)).except!(:join_options)) end def test_add_nested_joins_with_fields @@ -171,15 +159,11 @@ def test_add_nested_joins_with_fields records = Api::V10::PostResource.records({}) records = join_manager.join(records, {}) - sql = 'SELECT "posts".* FROM "posts" LEFT OUTER JOIN "comments" ON "comments"."post_id" = "posts"."id" LEFT OUTER JOIN "people" ON "people"."id" = "posts"."author_id" LEFT OUTER JOIN "people" "authors_comments" ON "authors_comments"."id" = "comments"."author_id" LEFT OUTER JOIN "comments_tags" ON "comments_tags"."comment_id" = "comments"."id" LEFT OUTER JOIN "tags" ON "tags"."id" = "comments_tags"."tag_id" WHERE "comments"."approved" = ' + db_true + ' AND "author"."special" = ' + db_true - - assert_equal sql, records.to_sql - - 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))) + assert_hash_equals({alias: 'posts', join_type: :root}, join_manager.source_join_details.except!(:join_options)) + assert_hash_equals({alias: 'comments', join_type: :left}, join_manager.join_details_by_relationship(Api::V10::PostResource._relationship(:comments)).except!(:join_options)) + assert_hash_equals({alias: 'authors_comments', join_type: :left}, join_manager.join_details_by_relationship(Api::V10::CommentResource._relationship(:author)).except!(:join_options)) + assert_hash_equals({alias: 'tags', join_type: :left}, join_manager.join_details_by_relationship(Api::V10::CommentResource._relationship(:tags)).except!(:join_options)) + assert_hash_equals({alias: 'people', join_type: :left}, join_manager.join_details_by_relationship(Api::V10::PostResource._relationship(:author)).except!(:join_options)) end def test_add_joins_with_sub_relationship @@ -190,15 +174,10 @@ def test_add_joins_with_sub_relationship records = Api::V10::PostResource.records({}) records = join_manager.join(records, {}) - sql = 'SELECT "posts".* FROM "posts" INNER JOIN "comments" ON "comments"."post_id" = "posts"."id" LEFT OUTER JOIN "people" ON "people"."id" = "comments"."author_id" LEFT OUTER JOIN "comments_tags" ON "comments_tags"."comment_id" = "comments"."id" LEFT OUTER JOIN "tags" ON "tags"."id" = "comments_tags"."tag_id" LEFT OUTER JOIN "comments" "comments_people" ON "comments_people"."author_id" = "people"."id" WHERE "comments"."approved" = ' + db_true + ' AND "author"."special" = ' + db_true - - assert_equal sql, records.to_sql - - 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: 'people', 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: 'comments_people', join_type: :left}, join_manager.join_details_by_relationship(Api::V10::PersonResource._relationship(:comments))) + assert_hash_equals({alias: 'comments', join_type: :inner}, join_manager.source_join_details.except!(:join_options)) + assert_hash_equals({alias: 'comments', join_type: :inner}, join_manager.join_details_by_relationship(Api::V10::PostResource._relationship(:comments)).except!(:join_options)) + assert_hash_equals({alias: 'tags', join_type: :left}, join_manager.join_details_by_relationship(Api::V10::CommentResource._relationship(:tags)).except!(:join_options)) + assert_hash_equals({alias: 'comments_people', join_type: :left}, join_manager.join_details_by_relationship(Api::V10::PersonResource._relationship(:comments)).except!(:join_options)) end def test_add_joins_with_sub_relationship_and_filters @@ -216,11 +195,11 @@ def test_add_joins_with_sub_relationship_and_filters 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))) + assert_hash_equals({alias: 'comments', join_type: :inner}, join_manager.source_join_details.except!(:join_options)) + assert_hash_equals({alias: 'comments', join_type: :inner}, join_manager.join_details_by_relationship(PostResource._relationship(:comments)).except!(:join_options)) + assert_hash_equals({alias: 'people', join_type: :left}, join_manager.join_details_by_relationship(CommentResource._relationship(:author)).except!(:join_options)) + assert_hash_equals({alias: 'tags', join_type: :left}, join_manager.join_details_by_relationship(CommentResource._relationship(:tags)).except!(:join_options)) + assert_hash_equals({alias: 'comments_people', join_type: :left}, join_manager.join_details_by_relationship(PersonResource._relationship(:comments)).except!(:join_options)) end def test_polymorphic_join_belongs_to_just_source @@ -231,10 +210,10 @@ def test_polymorphic_join_belongs_to_just_source 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\'', 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')) + assert_hash_equals({alias: 'products', join_type: :left}, join_manager.source_join_details('products').except!(:join_options)) + assert_hash_equals({alias: 'documents', join_type: :left}, join_manager.source_join_details('documents').except!(:join_options)) + assert_hash_equals({alias: 'products', join_type: :left}, join_manager.join_details_by_polymorphic_relationship(PictureResource._relationship(:imageable), 'products').except!(:join_options)) + assert_hash_equals({alias: 'documents', join_type: :left}, join_manager.join_details_by_polymorphic_relationship(PictureResource._relationship(:imageable), 'documents').except!(:join_options)) end def test_polymorphic_join_belongs_to_filter @@ -245,9 +224,9 @@ def test_polymorphic_join_belongs_to_filter 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\'', 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')) + assert_hash_equals({alias: 'pictures', join_type: :root}, join_manager.source_join_details.except!(:join_options)) + assert_hash_equals({alias: 'products', join_type: :left}, join_manager.join_details_by_polymorphic_relationship(PictureResource._relationship(:imageable), 'products').except!(:join_options)) + assert_hash_equals({alias: 'documents', join_type: :left}, join_manager.join_details_by_polymorphic_relationship(PictureResource._relationship(:imageable), 'documents').except!(:join_options)) end def test_polymorphic_join_belongs_to_filter_on_resource @@ -263,10 +242,9 @@ def test_polymorphic_join_belongs_to_filter_on_resource records = PictureResource.records({}) records = join_manager.join(records, {}) - assert_equal 'SELECT "pictures".* FROM "pictures" LEFT OUTER JOIN "documents" ON "documents"."id" = "pictures"."imageable_id" AND "pictures"."imageable_type" = \'Document\' LEFT OUTER JOIN "products" ON "products"."id" = "pictures"."imageable_id" AND "pictures"."imageable_type" = \'Product\' LEFT OUTER JOIN "file_properties" ON "file_properties"."fileable_id" = "pictures"."id" AND "file_properties"."fileable_type" = \'Picture\'', 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')) - assert_hash_equals({alias: 'file_properties', join_type: :left}, join_manager.join_details_by_relationship(PictureResource._relationship(:file_properties))) + assert_hash_equals({alias: 'pictures', join_type: :root}, join_manager.source_join_details.except!(:join_options)) + assert_hash_equals({alias: 'products', join_type: :left}, join_manager.join_details_by_polymorphic_relationship(PictureResource._relationship(:imageable), 'products').except!(:join_options)) + assert_hash_equals({alias: 'documents', join_type: :left}, join_manager.join_details_by_polymorphic_relationship(PictureResource._relationship(:imageable), 'documents').except!(:join_options)) + assert_hash_equals({alias: 'file_properties', join_type: :left}, join_manager.join_details_by_relationship(PictureResource._relationship(:file_properties)).except!(:join_options)) end end diff --git a/test/unit/resource/relationship_test.rb b/test/unit/resource/relationship_test.rb index a98d26601..e030ff309 100644 --- a/test/unit/resource/relationship_test.rb +++ b/test/unit/resource/relationship_test.rb @@ -18,6 +18,41 @@ def self.is_admin(context) end end +class TestRelationshipOptionsPostsResource < JSONAPI::Resource + model_name 'Post' + has_one :author, allow_include: :is_admin, use_related_resource_records_for_joins: false +end + +class RelationshipTest < ActiveSupport::TestCase + def test_use_related_resource_records_for_joins_enabled_by_default + assert JSONAPI.configuration.use_related_resource_records_for_joins == true + relationship = JSONAPI::Relationship::ToOne.new(:author) + assert relationship.use_related_resource_records_for_joins + end + + def test_use_related_resource_records_for_joins_can_be_disabled_globally + original_config = JSONAPI.configuration.dup + + JSONAPI.configuration.use_related_resource_records_for_joins = false + relationship = JSONAPI::Relationship::ToOne.new(:author) + assert relationship.use_related_resource_records_for_joins == false + ensure + JSONAPI.configuration = original_config + end + + def test_use_related_resource_records_for_joins_is_disabled_by_deafult_with_relation_name + relationship = JSONAPI::Relationship::ToOne.new(:author, + relation_name: "foo" ) + refute relationship.use_related_resource_records_for_joins + end + + def test_use_related_resource_records_for_joins_can_be_disabled + relationship = JSONAPI::Relationship::ToOne.new(:author, + use_related_resource_records_for_joins: false ) + refute relationship.use_related_resource_records_for_joins + end +end + class HasOneRelationshipTest < ActiveSupport::TestCase def test_polymorphic_type diff --git a/test/unit/resource/resource_test.rb b/test/unit/resource/resource_test.rb index 0bb8d1aa7..84a5ed565 100644 --- a/test/unit/resource/resource_test.rb +++ b/test/unit/resource/resource_test.rb @@ -13,7 +13,7 @@ class PostWithBadAfterSave < ActiveRecord::Base after_save :do_some_after_save_stuff def do_some_after_save_stuff - errors[:base] << 'Boom! Error added in after_save callback.' + errors.add(:base, 'Boom! Error added in after_save callback.') raise ActiveRecord::RecordInvalid.new(self) end end @@ -23,7 +23,7 @@ class PostWithCustomValidationContext < ActiveRecord::Base validate :api_specific_check, on: :json_api_create def api_specific_check - errors[:base] << 'Record is invalid' + errors.add(:base, 'Record is invalid') end end