Search code examples
rubyruby-object-mapperrom-rb

rom-rb form validation when using multiple relations


I'm trying out http://rom-rb.org/ and can't figure out how to get a presence validation to pass in the presence of multiple source models. I would expect the following script to save a new event and organiser, but instead it says that event_name is not present.

What am I missing?

require 'bundler/inline'

gemfile do
  source 'https://rubygems.org'
  gem 'rom'
  gem 'rom-sql'
  gem 'rom-rails'
  gem 'activemodel'
  gem 'sqlite3'
  gem 'activesupport'
end

require 'rom'
require 'rom-rails'

`rm -Rf /tmp/romtest.sqlite`
ROM.setup(:sql, 'sqlite:///tmp/romtest.sqlite')

class Events < ROM::Relation[:sql]
end

class Organisers < ROM::Relation[:sql]
end

class CreateEvent < ROM::Commands::Create[:sql]
  relation :events
  register_as :create
  result :one

  associates :organiser, key: [:organiser_id, :id]
end

class CreateOrganiser < ROM::Commands::Create[:sql]
  relation :organisers
  register_as :create
  result :one
end

class CreateEventWithOrganiser < ROM::Model::Form
  commands organisers: :create, events: :create

  input do
    attribute :email
    attribute :event_name
  end

  validations do
    validates :event_name, presence: true
  end

  def commit!
    command = organisers.create.with(
      email: email,
    ) >> events.create.with(
      name: event_name,
    )

    command.transaction do
      command.call
    end
  end
end

ROM.finalize
rom = ROM.env
gateway = rom.gateways.fetch(:default)
migration = gateway.migration do
  change do
    create_table :organisers do
      primary_key :id
      column :email, String, null: false
    end

    create_table :events do
      primary_key :id
      column :name, String, null: false
      column :organiser_id, Integer, null: false
    end
  end
end

migration.apply(gateway.connection, :up)

f = CreateEventWithOrganiser.build(
  email:      'test@example.com',
  event_name: 'Test Event'
)

# Unexpectedly fails
f.save
puts f.errors.full_messages
# => "Event name can't be blank"

Solution

  • Here's an updated version of your script which works:

    require 'rom'
    require 'rom-rails'
    
    `rm -Rf /tmp/romtest.sqlite`
    ROM.setup(:sql, 'sqlite:///tmp/romtest.sqlite')
    
    class Events < ROM::Relation[:sql]
    end
    
    class Organisers < ROM::Relation[:sql]
    end
    
    class CreateEvent < ROM::Commands::Create[:sql]
      relation :events
      register_as :create
      result :one
    
      associates :organiser, key: [:organiser_id, :id]
    end
    
    class CreateOrganiser < ROM::Commands::Create[:sql]
      relation :organisers
      register_as :create
      result :one
    end
    
    class CreateEventWithOrganiser < ROM::Model::Form
      inject_commands_for :organisers, :events
    
      input do
        attribute :email
        attribute :event_name
      end
    
      validations do
        validates :event_name, presence: true
      end
    
      def commit!
        validate!
    
        return if errors.any?
    
        command = organisers.create.with(
          email: email
        ) >> events.create.with(
          name: event_name
        )
    
        command.transaction do
          command.call
        end
      end
    end
    
    ROM.finalize
    rom = ROM.env
    gateway = rom.gateways.fetch(:default)
    migration = gateway.migration do
      change do
        create_table :organisers do
          primary_key :id
          column :email, String, null: false
        end
    
        create_table :events do
          primary_key :id
          column :name, String, null: false
          column :organiser_id, Integer, null: false
        end
      end
    end
    
    migration.apply(gateway.connection, :up)
    
    f = CreateEventWithOrganiser.build(
      email:      'test@example.com',
      event_name: 'Test Event'
    )
    
    puts f.save.result.inspect
    # #<ROM::Commands::Result::Success:0x007fa92b589ea0 @value={:id=>1, :name=>"Test Event", :organiser_id=>1}>
    

    The reason why it didn't work with commands is because this method will generate command objects for your form and set provided validations for each command, which will only work correctly if you used a single command. Otherwise same validator is used for each command which doesn't make sense. When you use inject_commands_for it will grab your own commands where validators are not set so you are free to handle validations yourself.

    I think we should stop setting validators on commands which would make your original sample work but notice that you need to call validate! yourself.

    I hope this helps.

    I also created a gist showing how to do the same without a form: https://gist.github.com/solnic/3b68342482cf1414f719