The app we generated in Getting Started and expanded in Modelling the Domain is now ready to be used by real Authors via the Web. Sequent is not a web framework and can be used with any web framework of your choice. For this guide we use Sinatra.

Installation

In your Gemfile add:

gem 'sinatra'
gem 'sinatra-flash'
gem 'sinatra-contrib'
gem 'webrick'
gem 'rackup'

And then run bundle install. We will set up Sinatra to run as a modular application.

Create app/web.rb:

require 'sinatra/base'
require 'sinatra/flash' # for displaying flash messages
require 'sinatra/reloader' # for hot reloading changes
require_relative '../blog'

class Web < Sinatra::Base
  enable :sessions
  register Sinatra::Flash

  configure :development do
    register Sinatra::Reloader
  end

  get '/' do
    "Welcome to Sequent!"
  end

  helpers ERB::Util
end

Create config.ru:

require './app/web'

run Web

For now this is enough.

To run the application, execute on the command line:

bundle exec rackup -p 4567

Once WEBrick has started without errors, click:

Open application

If you see "Welcome to Sequent!", we are good to go!

Sign up as Author

For this guide we want to be able to sign up as an Author. In a later guide we will go full CRUD on the application and actually create Posts with Authors.

To keep focus on the usage of Sequent in a web application, this guide will not go into styling the web application.

Form

The get '/' method will serve a sign up/sign in form. This form ties to the AddAuthor command.

First we change the get '/' method to serve us an erb containing an html form, allowing us to post a form with the name and email values that the AddAuthor command requires.

In app/web.rb change the get '/' to:

get '/' do
  erb :index
end

Create app/views/index.erb with:

<html>
  <body>
    <pre><%= flash.inspect %></pre>
    <form method="post" action="/authors">
      <div>
        <label for="name">Name</label>
        <input id="name" name="name" type="text"/>
      </div>
      <div>
        <label for="email">Email</label>
        <input id="email" name="email" type="email"/>
      </div>
      <button>Create author</button>
    </form>
  </body>
</html>

When opening our web application, we see a simple form that allows us to submit values for creating a new Author.

signup author form

In order to achieve the functionality of actually creating an author, we need to respond to the post '/authors' method. We need to parse the post params and construct a Sequent::Command that we will pass into the CommandService.

In app/web.rb add:

post '/authors' do
  author_id = Sequent.new_uuid
  command = AddAuthor.from_params(params.merge(aggregate_id: author_id))
  Sequent.command_service.execute_commands *command

  flash[:notice] = 'Account created'
  redirect "/"
end

Calling a command in Sequent generally follows the code signature as seen above:

  1. Parse parameters to the relevant Command
  2. Execute Command
  3. Redirect (or do whatever you like)

Let’s fill in a name and an e-mail and see what happens when we click on Create author.

It blows up with the following error:

2024-10-10 15:31:03 - ActiveRecord::ConnectionNotEstablished - No connection pool for 'ActiveRecord::Base' found. (ActiveRecord::ConnectionNotEstablished)

Since we are using ActiveRecord outside Rails we need to set up connection handling ourselves.

Connecting to a Database

We can create a simple Database class that handles creating connections to the database.

Create app/database.rb:

require 'sequent'

class Database
  class << self
    def establish_connection(env = ENV['SEQUENT_ENV'])
      Sequent::Support::Database.connect!(env)
    end
  end
end

As you can see this is just a small wrapper for ActiveRecord.

To establish the database connections on boot time we add a file boot.rb. This will contain all the code needed to require and boot our app. In the case that the SEQUENT_ENV is unset, we set it equal to development, which ensures the correct database config is loaded before connecting.

Create boot.rb:

ENV['SEQUENT_ENV'] ||= 'development'

require './app/database'
Database.establish_connection

require './app/web'

Update config.ru to:

require './boot'

run Web

Since we are using Sinatra, we also need to give the transaction back to the pool after each request. So we need to add an after block in our app/web.rb.

Update app/web.rb with:

after do
  Sequent::ApplicationRecord.connection_handler.clear_active_connections!
end

If you are using the multiple db feature and have more than one role for your database, you need to clear the connection for each role:

after do
  Sequent::ApplicationRecord.connection_handler.all_connection_pools.map(&:role).each do |role|
    Sequent::ApplicationRecord.connection_handler.clear_active_connections!(role)
  end
end

Restart your web application if it’s running.

Final test

Now try filling in a name and e-mail address in the application, and submit the form.

Success! It works when you see signup author created

We successfully transformed an html form to a Command and executed it.

When the name and/or e-mail field is empty when submitting the form, you will see a CommandNotValid error. This is the error Sequent raises when Command validations fail. You can handle these exceptions any way you like.

Inspect the events

Let’s inspect the sequent_schema and see if the events are actually stored in the database.

  1. Run:
    psql blog_development
    
  2. Execute the query:
    select aggregate_id, sequence_number, event_type from sequent_schema.event_records order by id, sequence_number;
    
  3. This should display:
                 aggregate_id             | sequence_number |    event_type
    --------------------------------------+-----------------+------------------
     85507d60-8645-4a8a-bdb8-3a9c86a0c635 |               1 | UsernamesCreated
     85507d60-8645-4a8a-bdb8-3a9c86a0c635 |               2 | UsernameAdded
     a8b1a534-f50b-4173-a73b-5b4a8bbcdd12 |               1 | AuthorCreated
     a8b1a534-f50b-4173-a73b-5b4a8bbcdd12 |               2 | AuthorNameSet
     a8b1a534-f50b-4173-a73b-5b4a8bbcdd12 |               3 | AuthorEmailSet
    (5 rows)
    

We can see all our events are stored in the event store. The column event_json is left out of the query for readability.

Store Author records

Next we will project the Author events to records in the database. In Sequent this is done in the following steps.

Create the AuthorRecord

Since we are using ActiveRecord, we need to create a record class corresponding to Author that we will call the AuthorRecord.

Create app/records/author_record.rb:

class AuthorRecord < Sequent::ApplicationRecord
end

Create the corresponding SQL file

Create db/tables/author_records.sql:

CREATE TABLE author_records%SUFFIX% (
  id serial NOT NULL,
  aggregate_id uuid NOT NULL,
  name character varying,
  email character varying,
  CONSTRAINT author_records_pkey%SUFFIX% PRIMARY KEY (id)
);

CREATE UNIQUE INDEX author_records_keys%SUFFIX% ON author_records%SUFFIX% USING btree (aggregate_id);

Create the Projector

In order to create an AuthorRecord based on the events we need to create the AuthorProjector.

Create app/projectors/author_projector.rb:

require_relative '../records/author_record'
require_relative '../../lib/author/events'

class AuthorProjector < Sequent::Projector
  manages_tables AuthorRecord

  on AuthorCreated do |event|
    create_record(
      AuthorRecord,
      aggregate_id: event.aggregate_id
    )
  end

  on AuthorNameSet do |event|
    update_all_records(
      AuthorRecord,
      { aggregate_id: event.aggregate_id },
      event.attributes.slice(:name)
    )
  end

  on AuthorEmailSet do |event|
    update_all_records(
      AuthorRecord,
      { aggregate_id: event.aggregate_id },
      event.attributes.slice(:email)
    )
  end
end

Ensure it’s being required in blog.rb:

require_relative 'app/projectors/author_projector'

Update and run the migration

To migrate the database, update the view_schema version and add the projectors that need to be rebuild.

Update db/migrations.rb to:

require 'sequent/migrations/projectors'

VIEW_SCHEMA_VERSION = 2 # <= updated to version 2

class Migrations < Sequent::Migrations::Projectors
  def self.version
    VIEW_SCHEMA_VERSION
  end

  def self.versions
    {
      '1' => [
        PostProjector
      ],
      '2' => [ 
        AuthorProjector # <= Projectors that need to be rebuild
      ]
    }
  end
end

Make sure you have updated your VIEW_SCHEMA_VERSION constant.

Stop your app, run the migration and see what happens:

bundle exec rake sequent:migrate:online && 
bundle exec rake sequent:migrate:offline
I, [..]  INFO -- : Start migrate_online for version 2
I, [..]  INFO -- : Number of groups 4096
I, [..]  INFO -- : groups: 4096
I, [..]  INFO -- : Start replaying events
I, [..]  INFO -- : Done migrate_online for version 2
I, [..]  INFO -- : Start migrate_offline for version 2
I, [..]  INFO -- : Number of groups 16
I, [..]  INFO -- : groups: 16
I, [..]  INFO -- : Start replaying events
I, [..]  INFO -- : Migrated to version 2

Inspect the events

Let’s inspect the database again:

psql blog_development 
select * from view_schema.author_records;
 id |             aggregate_id             | name |     email
----+--------------------------------------+------+----------------
  1 | a8b1a534-f50b-4173-a73b-5b4a8bbcdd12 | ben  | ben@sequent.io

We have authors in the database! This means we can also display them in our app.

Displaying the Authors

Author list

To show a list of authors and allow navigation inside the web app we add the following methods and views.

In app/web.rb add:

get '/authors' do
  @authors = AuthorRecord.all
  erb :'authors/index'
end

In app/views/index.erb add this right after the <body>:

<nav style="border-bottom: 1px solid #333; padding-bottom: 1rem;">
  <a href="/authors">All authors</a>
</nav>

Create app/views/authors/index.erb with:

<html>
  <body>
    <p>
      Back to <a href="/">index</a>
    </p>
    <table class="table">
      <thead>
        <tr>
          <th>ID</th>
          <th>Name</th>
          <th>E-mail</th>
        </tr>
      </thead>
      <tbody>
        <% @authors.each do |author| %>
          <tr>
            <td>
              <a href="/authors/<%= author.aggregate_id %>"><%= h author.aggregate_id %></a>
            </td>
            <td><%= h author.name %></td>
            <td><%= h author.email %></td>
          </tr>
        <% end %>
      </tbody>
    </table>
  </body>
</html>

Restart your web application if it’s still running to make sure any changes to blog.rb or the Sequent config are propagated.

Open your application, click on All authors and you should see all author records:

author list

Author details

Let’s create a new view to display the details of an individual author.

In app/web.rb add:

get '/authors/:author_id' do
  @author = AuthorRecord.find_by(aggregate_id: params[:author_id])
  erb :'authors/show'
end

Create app/views/authors/show.erb with:

<html>
  <body>
    <h1>Author <%= h @author.name %> </h1>
    <p>Email: <%= h @author.email %></p>
    <p>
      <a href="/authors">Show all</a>
    </p>
  </body>
</html>

In your application, click on All authors. You should now be able to view the details of an author by clicking on the ID:

author details

Summary

In this guide we learned:

  1. How to execute a Sequent command in a Sinatra web application
  2. Establish a connection to a database
  3. Store database records with a Projector and Migration
  4. Display the database records

The full source code of the web application is available in the sequent-examples repository.

We will continue with this web application in the Finishing the web application guide.