Permalink
Browse files

Feature: Support for one-to-one associations (#425)

  • Loading branch information...
mereghost authored and jodosha committed Aug 10, 2017
1 parent 14e9841 commit d005ec9d3699cb576b895e45f832929fba3a3be5
@@ -1,6 +1,7 @@
require 'rom-sql'
require 'hanami/model/associations/has_many'
require 'hanami/model/associations/belongs_to'
require 'hanami/model/associations/has_many'
require 'hanami/model/associations/has_one'
require 'hanami/model/associations/many_to_many'

module Hanami
@@ -23,10 +24,13 @@ def self.build(repository, target, subject)
#
# @since 0.7.0
# @api private
# rubocop:disable Metrics/MethodLength
def self.lookup(association)
case association
when ROM::SQL::Association::ManyToMany
Associations::ManyToMany
when ROM::SQL::Association::OneToOne
Associations::HasOne
when ROM::SQL::Association::OneToMany
Associations::HasMany
when ROM::SQL::Association::ManyToOne
@@ -35,6 +39,7 @@ def self.lookup(association)
raise "Unsupported association: #{association}"
end
end
# rubocop:enable Metrics/MethodLength
end
end
end
@@ -20,6 +20,10 @@ def has_many(relation, **args)
@repository.__send__(:relations, args[:through]) if args[:through]
end

def has_one(relation, *)
@repository.__send__(:relations, Hanami::Utils::String.new(relation).pluralize.to_sym)
end

# @since x.x.x
# @api private
def belongs_to(relation, *)
@@ -0,0 +1,147 @@
module Hanami
module Model
module Associations
# Many-To-One association
#
# @since x.x.x
# @api private
class HasOne
# @since x.x.x
# @api private
def self.schema_type(entity)
Sql::Types::Schema::AssociationType.new(entity)
end
#
# @since x.x.x
# @api private
attr_reader :repository

# @since x.x.x
# @api private
attr_reader :source

# @since x.x.x
# @api private
attr_reader :target

# @since x.x.x
# @api private
attr_reader :subject

# @since x.x.x
# @api private
attr_reader :scope

# @since x.x.x
# @api private
def initialize(repository, source, target, subject, scope = nil)
@repository = repository
@source = source
@target = target
@subject = subject.to_hash unless subject.nil?
@scope = scope || _build_scope
freeze
end

def one
scope.one
end

def create(data)
entity.new(
command(:create, aggregate(target), use: [:timestamps]).call(data)
)
end

def add(data)
command(:create, relation(target), use: [:timestamps]).call(associate(data))
end

def update(data)
command(:update, relation(target), use: [:timestamps]).call(associate(data))
end

def remove
command(:delete, relation(target)).by_pk(scope.one.id).call
end

def replace(data)
repository.transaction do
remove
add(data)
end
end

private

# @since x.x.x
# @api private
def entity
repository.class.entity
end

# @since x.x.x
# @api private
def aggregate(name)
repository.aggregate(name)
end

# @since x.x.x
# @api private
def command(target, relation, options = {})
repository.command(target, relation, options)
end

# @since x.x.x
# @api private
def relation(name)
repository.relations[Hanami::Utils::String.new(name).pluralize]
end

# @since x.x.x
# @api private
def container
repository.container
end

# @since x.x.x
# @api private
def primary_key
association_keys.first
end

# @since x.x.x
# @api private
def foreign_key
association_keys.last
end

# @since x.x.x
# @api private
def associate(data)
relation(source)
.associations[target]
.associate(container.relations, data, subject)
end

# Returns primary key and foreign key
#
# @since x.x.x
# @api private
def association_keys
relation(source)
.associations[target]
.__send__(:join_key_map, container.relations)
end

# @since x.x.x
# @api private
def _build_scope
result = relation(target)
result = result.where(foreign_key => subject.fetch(primary_key)) unless subject.nil?
result.as(Model::MappedRelation.mapper_name)
end
end
end
end
end
@@ -85,7 +85,7 @@ def remove(id)
.one
if association_record
ar_id = association_record.public_send relation(through).primary_key
command(:delete, relation(through), use: [:timestamps]).by_pk(ar_id).call
command(:delete, relation(through)).by_pk(ar_id).call
end
end
# rubocop:enable Metrics/AbcSize
@@ -0,0 +1,73 @@
require 'spec_helper'

RSpec.describe 'Associations (has_one)' do
let(:repository) { UserRepository.new }

it "returns nil if the association wasn't preloaded" do
user = repository.create(name: 'John Doe')
found = repository.find(user.id)

expect(found.avatar).to be_nil
end

it 'preloads the associated record' do
user = repository.create(name: 'Baruch Spinoza')
avatar = AvatarRepository.new.create(user_id: user.id, url: 'http://www.notarealurl.com/avatar.png')
found = repository.find_with_avatar(user.id)
expect(found).to eq(user)
expect(found.avatar).to eq(avatar)
end

it 'returns an Avatar' do
user = repository.create(name: 'Simone de Beauvoir')
avatar = AvatarRepository.new.create(user_id: user.id, url: 'http://www.notarealurl.com/simone.png')
found = repository.avatar_for(user)

expect(found).to eq(avatar)
end

it 'adds an an Avatar to an existing User' do
user = repository.create(name: 'Jean Paul-Sartre')
avatar = repository.add_avatar(user, url: 'http://www.notarealurl.com/sartre.png')
found = repository.find_with_avatar(user.id)

expect(found).to eq(user)
expect(found.avatar.id).to eq(avatar.id)
expect(found.avatar.url).to eq('http://www.notarealurl.com/sartre.png')
end

it 'creates a User and an Avatar' do
user = repository.create_with_avatar(name: 'Lao Tse', avatar: { url: 'http://lao-tse.io/me.jpg' })
found = repository.find_with_avatar(user.id)

expect(found.name).to eq(user.name)
expect(found.avatar).to eq(user.avatar)
expect(found.avatar.url).to eq('http://lao-tse.io/me.jpg')
end

it 'returns nil if the association was preloaded but no associated object is set' do
user = repository.create(name: 'Henry Jenkins')
found = repository.find_with_avatar(user.id)

expect(found).to eq(user)
expect(found.avatar).to be_nil
end

it 'removes the Avatar' do
user = repository.create_with_avatar(name: 'Bob Ross', avatar: { url: 'http://bobross/happy_little_avatar.jpg' })
repository.remove_avatar(user)
found = repository.find_with_avatar(user.id)

expect(found.avatar).to be_nil
end

it 'replaces the associated object' do
user = repository.create_with_avatar(name: 'Frank Herbert', avatar: { url: 'http://not-real.com/avatar.jpg' })
repository.replace_avatar(user, url: 'http://totally-correct.com/avatar.jpg')
found = repository.find_with_avatar(user.id)

expect(found.avatar).to_not eq(user.avatar)

expect(AvatarRepository.new.by_user(user.id).size).to eq(1)
end
end
@@ -25,6 +25,9 @@ class AccessToken < Hanami::Entity
class SourceFile < Hanami::Entity
end

class Avatar < Hanami::Entity
end

class Warehouse < Hanami::Entity
attributes do
attribute :id, Types::Int
@@ -67,7 +70,45 @@ class Color < Hanami::Entity
class Label < Hanami::Entity
end

class AvatarRepository < Hanami::Repository
associations do
belongs_to :user
end

def by_user(id)
avatars.where(user_id: id).to_a
end
end

class UserRepository < Hanami::Repository
associations do
has_one :avatar
end

def find_with_avatar(id)
aggregate(:avatar).where(id: id).as(User).one
end

def create_with_avatar(data)
assoc(:avatar).create(data)
end

def remove_avatar(user)
assoc(:avatar, user).remove
end

def add_avatar(user, data)
assoc(:avatar, user).add(data)
end

def replace_avatar(user, data)
assoc(:avatar, user).replace(data)
end

def avatar_for(user)
assoc(:avatar, user).one
end

def by_name(name)
users.where(name: name)
end

0 comments on commit d005ec9

Please sign in to comment.