Skip to content

Commit

Permalink
Implements polymorphic associations (Closes #51)
Browse files Browse the repository at this point in the history
  • Loading branch information
zedtux committed Feb 28, 2021
1 parent 84acc47 commit 23de418
Show file tree
Hide file tree
Showing 6 changed files with 122 additions and 7 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Dockerfile, docker-compose and Earthfile
- Test Ruby 3 + Rails 6 on Travis CI
- Implements the ReQL `during` command
- Implements polymorphic associations

## [0.34.1] - 2021-02-18
### Fixed
Expand Down
34 changes: 32 additions & 2 deletions lib/no_brainer/document/association/belongs_to.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ class NoBrainer::Document::Association::BelongsTo

class Metadata
VALID_OPTIONS = [:primary_key, :foreign_key, :class_name, :foreign_key_store_as,
:index, :validates, :required, :uniq, :unique]
:index, :validates, :required, :uniq, :unique, :polymorphic]
include NoBrainer::Document::Association::Core::Metadata
include NoBrainer::Document::Association::EagerLoader::Generic

Expand Down Expand Up @@ -31,7 +31,11 @@ def primary_key
end

def target_model
get_model_by_name(options[:class_name] || target_name.to_s.camelize)
if options[:polymorphic]
get_model_by_name(owner_model.send([target_name, :type].join('_')))
else
get_model_by_name(options[:class_name] || target_name.to_s.camelize)
end
end

def base_criteria
Expand All @@ -49,6 +53,15 @@ def hook

owner_model.field(foreign_key, :store_as => options[:foreign_key_store_as], :index => options[:index])

if options[:polymorphic]
type_column_name = [target_name, :type].join('_')
id_column_name = [target_name, primary_key].join('_')

owner_model.field(type_column_name.to_sym, type: String)
owner_model.field(id_column_name.to_sym, type: String)
owner_model.index([id_column_name.to_sym, type_column_name.to_sym])
end

unless options[:validates] == false
owner_model.validates(target_name, options[:validates]) if options[:validates]

Expand Down Expand Up @@ -97,6 +110,17 @@ def assign_foreign_key(value)
@target_container = nil
end

def polymorphic_read
return target if loaded?

target_id = owner.read_attribute(foreign_key)
target_class = owner.read_attribute([target_name, :type].join('_').to_sym)

if target_id && target_class
preload(target_class.where(primary_key => target_id).first)
end
end

def read
return target if loaded?

Expand All @@ -105,6 +129,12 @@ def read
end
end

def polymorphic_write(target)
owner.write_attribute(foreign_key, target.try(primary_key))
owner.write_attribute([target_name, :type].join('_').to_sym, target.class.name)
preload(target)
end

def write(target)
assert_target_type(target)
owner.write_attribute(foreign_key, target.try(primary_key))
Expand Down
4 changes: 2 additions & 2 deletions lib/no_brainer/document/association/core.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ def delegate(method_src, method_dst, options={})

def hook
options.assert_valid_keys(*self.class.const_get(:VALID_OPTIONS))
delegate("#{target_name}=", :write)
delegate("#{target_name}", :read)
delegate("#{target_name}=", "#{'polymorphic_' if options[:polymorphic]}write".to_sym)
delegate("#{target_name}", "#{'polymorphic_' if options[:polymorphic]}read".to_sym)
end

def add_callback_for(what)
Expand Down
20 changes: 17 additions & 3 deletions lib/no_brainer/document/association/has_many.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ class NoBrainer::Document::Association::HasMany
include NoBrainer::Document::Association::Core

class Metadata
VALID_OPTIONS = [:primary_key, :foreign_key, :class_name, :dependent, :scope]
VALID_OPTIONS = [:primary_key, :foreign_key, :class_name, :dependent, :scope,
:as]
include NoBrainer::Document::Association::Core::Metadata
include NoBrainer::Document::Association::EagerLoader::Generic

Expand Down Expand Up @@ -58,8 +59,21 @@ def eager_load_target_key; foreign_key; end
end

def target_criteria
@target_criteria ||= base_criteria.where(foreign_key => owner.__send__(primary_key))
.after_find(set_inverse_proc)
@target_criteria ||= begin
query_criteria = { foreign_key => owner.__send__(primary_key) }

if metadata.options[:as]
polymorphic_id_foreign_key = [metadata.options[:as], primary_key].join('_')
polymorphic_type_foreign_key = [metadata.options[:as], :type].join('_')

query_criteria = {
polymorphic_id_foreign_key => owner.__send__(primary_key),
polymorphic_type_foreign_key => owner.class.name
}
end

base_criteria.where(query_criteria).after_find(set_inverse_proc)
end
end

def read
Expand Down
43 changes: 43 additions & 0 deletions spec/integration/associations/belongs_to_polymorphic_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
require 'spec_helper'

describe 'belongs_to polymorphic' do
before do
load_belongs_to_polymorphic_models
NoBrainer.sync_indexes
end

let(:event) { Event.create }
let(:restaurant) { Restaurant.create }

context 'creating a polymorphic model document' do
it 'saves the model class as type and model id' do
picture = Picture.create(imageable: restaurant)

expect(picture.imageable_type).to eql(restaurant.class.name)
# `imageable__id_` instead of imageable_id since the primary key
# has been changed for Rspec, see spec/spec_helper.rb line 32-33.
expect(picture.imageable__id_).to eql(restaurant._id_)
end
end

context 'accessing a has_one polymorphic model document' do
it 'returns the associated document' do
logo = Logo.create(imageable: restaurant)

expect(restaurant.logo).to eql(logo)
end
end

context 'accessing a has_many polymorphic model document' do
it 'returns the associated documents' do
picture1 = Picture.create(imageable: restaurant)
picture2 = Picture.create(imageable: restaurant)

expect(restaurant.pictures.to_a).to eql([picture1, picture2])

picture3 = Picture.create(imageable: event)

expect(event.pictures.to_a).to eql([picture3])
end
end
end
27 changes: 27 additions & 0 deletions spec/support/models.rb
Original file line number Diff line number Diff line change
Expand Up @@ -99,5 +99,32 @@ def load_polymorphic_models
end
end

def load_belongs_to_polymorphic_models
define_class :Logo do
include NoBrainer::Document

belongs_to :imageable, polymorphic: true
end

define_class :Picture do
include NoBrainer::Document

belongs_to :imageable, polymorphic: true
end

define_class :Event do
include NoBrainer::Document

has_many :pictures, as: :imageable
end

define_class :Restaurant do
include NoBrainer::Document

has_one :logo, as: :imageable
has_many :pictures, as: :imageable
end
end

RSpec.configure { |config| config.include self }
end

0 comments on commit 23de418

Please sign in to comment.