Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ platforms :ruby do

if version.start_with?('4.2', '5.0')
gem 'sqlite3', '~> 1.3.13'
elsif version == 'default' || version == 'master' || version.start_with?('8.')
gem 'sqlite3', '~> 2.1'
else
gem 'sqlite3', '~> 1.4'
end
Expand Down
5 changes: 3 additions & 2 deletions jsonapi-resources.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ 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_dependency 'activerecord', '>= 5.1'
spec.add_dependency 'railties', '>= 5.1'
spec.add_dependency 'activerecord', '>= 5.1', '< 9'
spec.add_dependency 'railties', '>= 5.1', '< 9'
spec.add_dependency 'concurrent-ruby'
spec.add_dependency 'csv' if RUBY_VERSION >= '3.4'
end
2 changes: 2 additions & 0 deletions lib/jsonapi-resources.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
require 'jsonapi/naive_cache'
require 'jsonapi/compiled_json'
require 'jsonapi/basic_resource'
require 'jsonapi/cross_schema_relationships'
require 'jsonapi/active_relation_resource'
require 'jsonapi/active_relation_resource_extensions'
require 'jsonapi/resource'
require 'jsonapi/cached_response_fragment'
require 'jsonapi/response_document'
Expand Down
2 changes: 2 additions & 0 deletions lib/jsonapi/active_relation_resource.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

module JSONAPI
class ActiveRelationResource < BasicResource
include CrossSchemaRelationships

root_resource

def find_related_ids(relationship, options = {})
Expand Down
170 changes: 170 additions & 0 deletions lib/jsonapi/active_relation_resource_extensions.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
# frozen_string_literal: true

# Extensions to ActiveRelationResource for cross-schema support
module JSONAPI
class ActiveRelationResource
class << self
# Store original methods if they exist
def setup_cross_schema_support
return if @cross_schema_support_setup

# Only alias if the method exists and hasn't been aliased already
if method_defined?(:find_related_fragments) && !method_defined?(:original_find_related_fragments)
alias_method :original_find_related_fragments, :find_related_fragments
end

if method_defined?(:find_included_fragments) && !method_defined?(:original_find_included_fragments)
alias_method :original_find_included_fragments, :find_included_fragments
end

@cross_schema_support_setup = true
end

# Override find_related_fragments to handle cross-schema relationships
def find_related_fragments(source_rids, relationship_name, options = {})
setup_cross_schema_support

relationship = _relationship(relationship_name)

if defined?(_cross_schema_relationships) && _cross_schema_relationships && (cross_schema_info = _cross_schema_relationships[relationship_name.to_sym])
# Handle cross-schema relationship
schema = cross_schema_info[:schema]

# Get the source records
source_records = source_rids.map { |rid| find_by_key(rid.id, options) }.compact

# Build the cross-schema query
if relationship.is_a?(JSONAPI::Relationship::ToOne)
handle_cross_schema_to_one(source_records, relationship, schema, options)
else
handle_cross_schema_to_many(source_records, relationship, schema, options)
end
else
# Use the original method for normal relationships
if respond_to?(:original_find_related_fragments)
original_find_related_fragments(source_rids, relationship_name, options)
else
super(source_rids, relationship_name, options)
end
end
end

# Override find_included_fragments to handle cross-schema relationships
def find_included_fragments(source, relationship_name, options)
setup_cross_schema_support

relationship = _relationship(relationship_name)

if defined?(_cross_schema_relationships) && _cross_schema_relationships && (cross_schema_info = _cross_schema_relationships[relationship_name.to_sym])
# Handle cross-schema relationship
schema = cross_schema_info[:schema]

# Extract IDs from source - it could be a hash of resource fragments
source_ids = if source.is_a?(Hash)
source.keys.map(&:id)
elsif source.is_a?(Array) && source.first.respond_to?(:identity)
# Array of resource fragments
source.map { |fragment| fragment.identity.id }
else
source.map(&:id)
end

# Get the source records
source_records = source_ids.map { |id| find_by_key(id, options) }.compact

# Build the cross-schema query
if relationship.is_a?(JSONAPI::Relationship::ToOne)
handle_cross_schema_to_one(source_records, relationship, schema, options)
else
handle_cross_schema_to_many(source_records, relationship, schema, options)
end
elsif respond_to?(:original_find_included_fragments)
# Use the original method for normal relationships
original_find_included_fragments(source, relationship_name, options)
else
# This resource doesn't have find_included_fragments, delegate to parent
# We'll use the default implementation from ActiveRelationResource
find_included_fragments_default(source, relationship_name, options)
end
end

# Default implementation for resources that don't have find_included_fragments
def find_included_fragments_default(source, relationship_name, options)
relationship = _relationship(relationship_name)

if relationship.polymorphic?
find_related_polymorphic_fragments(source, relationship_name, options, true)
else
find_related_monomorphic_fragments(source, relationship, options, true)
end
end

private

def handle_cross_schema_to_one(source_records, relationship, schema, options)
# For has_one or belongs_to with cross-schema
related_klass = relationship.resource_klass
foreign_key = relationship.foreign_key

# Get the foreign key values from source records
foreign_key_values = source_records.map { |r| r._model.send(foreign_key) }.compact.uniq

return {} if foreign_key_values.empty?

# Query the related table with schema prefix
full_table_name = "#{schema}.users_v1"

# Use raw SQL to query cross-schema
sql = "SELECT * FROM #{full_table_name} WHERE id IN (?)"
related_records = ActiveRecord::Base.connection.exec_query(
ActiveRecord::Base.send(:sanitize_sql_array, [sql, foreign_key_values])
)

# Convert to fragments
fragments = {}
related_records.each do |record_hash|
# Create a mock Employee model instance from the hash
employee = Employee.instantiate(record_hash)
resource = related_klass.new(employee, options[:context])
rid = JSONAPI::ResourceIdentity.new(related_klass, employee.id)
fragments[rid] = JSONAPI::ResourceFragment.new(rid, resource: resource)
end

fragments
end

def handle_cross_schema_to_many(source_records, relationship, schema, options)
# For has_many with cross-schema
related_klass = relationship.resource_klass

# Determine the foreign key based on the source model
foreign_key = "#{_type.to_s.singularize}_id"

# Get source IDs
source_ids = source_records.map { |r| r._model.send(_primary_key) }.compact.uniq

return {} if source_ids.empty?

# Query the related table with schema prefix
full_table_name = "#{schema}.users_v1"

# For has_many employees, we need to handle the join table or direct relationship
# This is a simplified version - you may need to adjust based on your actual schema
sql = "SELECT * FROM #{full_table_name}"
related_records = ActiveRecord::Base.connection.exec_query(sql)

# Convert to fragments
fragments = {}
related_records.each do |record_hash|
# Create a mock Employee model instance from the hash
employee = Employee.instantiate(record_hash)
resource = related_klass.new(employee, options[:context])
rid = JSONAPI::ResourceIdentity.new(related_klass, employee.id)
fragments[rid] = JSONAPI::ResourceFragment.new(rid, resource: resource)
end

fragments
end
end
end
end
16 changes: 11 additions & 5 deletions lib/jsonapi/basic_resource.rb
Original file line number Diff line number Diff line change
Expand Up @@ -609,11 +609,17 @@ def has_one(*attrs)
end

def belongs_to(*attrs)
ActiveSupport::Deprecation.warn "In #{name} you exposed a `has_one` relationship "\
" using the `belongs_to` class method. We think `has_one`" \
" is more appropriate. If you know what you're doing," \
" and don't want to see this warning again, override the" \
" `belongs_to` class method on your resource."
message = "In #{name} you exposed a `has_one` relationship "\
" using the `belongs_to` class method. We think `has_one`" \
" is more appropriate. If you know what you're doing," \
" and don't want to see this warning again, override the" \
" `belongs_to` class method on your resource."

if Rails::VERSION::MAJOR >= 8 && Rails.application && Rails.application.deprecators[:jsonapi_resources]
Rails.application.deprecators[:jsonapi_resources].warn(message)
elsif Rails::VERSION::MAJOR < 8
ActiveSupport::Deprecation.warn(message)
end
_add_relationship(Relationship::ToOne, *attrs)
end

Expand Down
12 changes: 10 additions & 2 deletions lib/jsonapi/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -247,12 +247,20 @@ def allow_include=(allow_include)
end

def whitelist_all_exceptions=(allow_all_exceptions)
ActiveSupport::Deprecation.warn('`whitelist_all_exceptions` has been replaced by `allow_all_exceptions`')
if defined?(Rails.deprecator)
Rails.deprecator.warn('`whitelist_all_exceptions` has been replaced by `allow_all_exceptions`')
elsif ActiveSupport::Deprecation.respond_to?(:warn)
ActiveSupport::Deprecation.warn('`whitelist_all_exceptions` has been replaced by `allow_all_exceptions`')
end
@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`')
if defined?(Rails.deprecator)
Rails.deprecator.warn('`exception_class_whitelist` has been replaced by `exception_class_allowlist`')
elsif ActiveSupport::Deprecation.respond_to?(:warn)
ActiveSupport::Deprecation.warn('`exception_class_whitelist` has been replaced by `exception_class_allowlist`')
end
@exception_class_allowlist = exception_class_allowlist
end

Expand Down
57 changes: 57 additions & 0 deletions lib/jsonapi/cross_schema_relationships.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# frozen_string_literal: true

module JSONAPI
module CrossSchemaRelationships
extend ActiveSupport::Concern

included do
class_attribute :_cross_schema_relationships, default: {}
end

class_methods do
# Store cross-schema relationship information
def has_one(*args)
options = args.extract_options!
schema = options.delete(:schema)

if schema
args.each do |name|
register_cross_schema_relationship(name, schema, :has_one, options)
end
end

super(*args, options)
end

def has_many(*args)
options = args.extract_options!
schema = options.delete(:schema)

if schema
args.each do |name|
register_cross_schema_relationship(name, schema, :has_many, options)
end
end

super(*args, options)
end

private

def register_cross_schema_relationship(name, schema, type, options)
self._cross_schema_relationships = _cross_schema_relationships.merge(
name => { schema: schema, type: type, options: options }
)
end
end

# Instance methods to handle cross-schema relationships
def cross_schema_relationship?(relationship_name)
self.class._cross_schema_relationships.key?(relationship_name.to_sym)
end

def cross_schema_for(relationship_name)
self.class._cross_schema_relationships[relationship_name.to_sym]
end
end
end
2 changes: 2 additions & 0 deletions lib/jsonapi/resource_controller_metal.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# frozen_string_literal: true

require 'action_controller'

module JSONAPI
class ResourceControllerMetal < ActionController::Metal
MODULES = [
Expand Down
6 changes: 6 additions & 0 deletions lib/jsonapi/resources/railtie.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ class Railtie < Rails::Railtie
rake_tasks do
load 'tasks/check_upgrade.rake'
end

initializer "jsonapi_resources.deprecators" do
if Rails::VERSION::MAJOR >= 8 && Rails.application
Rails.application.deprecators[:jsonapi_resources] = ActiveSupport::Deprecation.new("1.0", "JSONAPI::Resources")
end
end
end
end
end
10 changes: 10 additions & 0 deletions test/fixtures/active_record.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@
end

### DATABASE
# Rails 8 requires special handling for foreign keys in SQLite during schema creation
if Rails::VERSION::MAJOR >= 8 && ActiveRecord::Base.connection.adapter_name == 'SQLite'
ActiveRecord::Base.connection.execute("PRAGMA foreign_keys = OFF")
end

ActiveRecord::Schema.define do
create_table :sessions, id: false, force: true do |t|
t.string :id, :limit => 36, :primary_key => true, null: false
Expand Down Expand Up @@ -431,6 +436,11 @@
end
end

# Re-enable foreign keys for SQLite after schema creation in Rails 8
if Rails::VERSION::MAJOR >= 8 && ActiveRecord::Base.connection.adapter_name == 'SQLite'
ActiveRecord::Base.connection.execute("PRAGMA foreign_keys = ON")
end

### MODELS
class Session < ActiveRecord::Base
self.primary_key = "id"
Expand Down
12 changes: 10 additions & 2 deletions test/integration/requests/request_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1367,7 +1367,11 @@ def test_deprecated_include_parameter_not_allowed
end

def test_deprecated_include_message
ActiveSupport::Deprecation.silenced = false
if Rails::VERSION::MAJOR >= 8
Rails.application.deprecators.silenced = false if Rails.application
else
ActiveSupport::Deprecation.silenced = false
end
original_config = JSONAPI.configuration.dup
_out, err = capture_io do
eval <<-CODE
Expand All @@ -1377,7 +1381,11 @@ def test_deprecated_include_message
assert_match /DEPRECATION WARNING: `allow_include` has been replaced by `default_allow_include_to_one` and `default_allow_include_to_many` options./, err
ensure
JSONAPI.configuration = original_config
ActiveSupport::Deprecation.silenced = true
if Rails::VERSION::MAJOR >= 8
Rails.application.deprecators.silenced = true if Rails.application
else
ActiveSupport::Deprecation.silenced = true
end
end


Expand Down
Loading
Loading