Building a web application with Sequent
Tying it all together
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.
Installing Sinatra
In your Gemfile
add:
gem 'sinatra'
gem 'sinatra-flash'
gem 'sinatra-contrib'
gem 'webrick'
And then run bundle install
. We will set up Sinatra to run as a modular application.
To make use of Sinatra, we need to create / modify the following files:
Create ./app/web.rb
:
require 'sinatra/base'
require 'sinatra/flash' # for displaying flash messages
require 'sinatra/reloader' # for hot reloading changes we make
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
Update ./config.ru
:
require './app/web'
run Web
For now this is enough. On the command line execute bundle exec rackup -p 4567
and open localhost:4567. If you see "Welcome to Sequent!"
then we are good to go!
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.
This guide will not go into styling the web application we are creating, to keep focus on the usage of Sequent in a web application.
Sign Up as Author
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
add:
class Web < Sinatra::Base
...
get '/' do
erb :index
end
...
end
Create app/views/index.erb
:
<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 visiting localhost:4567, we see a simple form that allows us to submit values for creating a new Author.
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:
- Parse parameters to the relevant
Command
- Execute Command
- 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:
ActiveRecord::ConnectionNotEstablished at /
No connection pool with 'primary' found.`
Since we are using ActiveRecord outside Rails we need to set up connection handling ourselves.
In order to do so, we can create a simple Database
class that handles creating connections to the database.
Connecting to a Database
Create app/database.rb
:
require 'yaml'
require 'erb'
require 'active_record'
require 'sequent'
class Database
class << self
def database_config(env = ENV['SEQUENT_ENV'])
@config ||= YAML.load(ERB.new(File.read('db/database.yml')).result, aliases: true)[env]
end
def establish_connection(env = ENV['SEQUENT_ENV'])
config = database_config(env)
yield(config) if block_given?
Sequent::ApplicationRecord.configurations = { env.to_s => config.stringify_keys }
Sequent::ApplicationRecord.establish_connection config
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
:
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
:
class Web < Sinatra::Base
...
after do
Sequent::ApplicationRecord.clear_active_connections!
end
...
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:
class Web < Sinatra::Base
...
after do
ActiveRecord::Base.connection_handler.all_connection_pools.map(&:role).each do |role|
ActiveRecord::Base.connection_handler.clear_active_connections!(role)
end
end
...
end
Let’s restart the app, fill in a name and email, and submit the form.
Success!
Yeah! 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.
Let’s inspect the sequent_schema
and see if the events are actually stored in the database.
$ psql blog_development
blog_development=# select aggregate_id, sequence_number, event_type from sequent_schema.event_records order by id, sequence_number;
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.
Creating a Projector and using Migrations
Next we will display the existing authors. In Sequent this is done in 5 steps:
1. 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
2. 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);
3. 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
Remember to ensure it’s being required in blog.rb
:
require_relative 'app/projectors/author_projector'
4. Update Sequent configuration
Add the new projector to our Sequent config.
Update config/initializers/sequent.rb
:
require './db/migrations'
Sequent.configure do |config|
config.migrations_class_name = 'Migrations'
config.command_handlers = [
PostCommandHandler,
AuthorCommandHandler,
].map(&:new)
config.event_handlers = [
PostProjector,
AuthorProjector
].map(&:new)
end
5. Update and run the migration
Update db/migrations.rb
:
VIEW_SCHEMA_VERSION = 2 # <= update this to version 2
class Migrations < Sequent::Migrations::Projectors
def self.version
VIEW_SCHEMA_VERSION
end
def self.versions
{
'1' => [
PostProjector
],
'2' => [ # <= add here which projectors you want to rebuild
AuthorProjector
]
}
end
end
Make sure you have updated your VIEW_SCHEMA_VERSION
constant.
Stop your app, run this migration and see what happens:
$ bundle exec rake sequent:migrate:online && bundle exec rake sequent:migrate:offline
INFO -- : group_exponent: 3
INFO -- : Start replaying events
INFO -- : Number of groups 4096
INFO -- : group_exponent: 1
INFO -- : Start replaying events
INFO -- : Number of groups 16
INFO -- : Migrated to version 2
Let’s inspect the database again:
$ psql blog_development
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
Let’s create a new view to display the details of an individual author.
In app/web.rb
add:
class Web < Sinatra::Base``
...
get '/authors/:aggregate_id' do
@author = AuthorRecord.find_by(aggregate_id: params[:aggregate_id])
erb :'authors/show'
end
...
end
Create app/views/authors/show.erb
:
<html>
<body>
<h1>Author <%= h @author.name %> </h1>
<p>Email: <%= h @author.email %></p>
<p>
<a href="/authors">Show all</a>
</p>
</body>
</html>
Navigation within the App
To allow navigation inside the web app we add the following methods and views:
In app/web.rb
add:
class Web < Sinatra::Base
...
get '/authors' do
@authors = AuthorRecord.all
erb :'authors/index'
end
...
end
In app/views/index.erb
add:
<body>
<nav style="border-bottom: 1px solid #333; padding-bottom: 1rem;">
<a href="/authors">All authors</a>
</nav>
...
Create app/views/authors/index.erb
:
<html>
<body>
<p>
Back to <a href="/">index</a>
</p>
<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>
Summary
In this guide we learned:
- How to use Sequent in a Sinatra web application
- Add a Projector and Migration
- Use the Projector to display data in the web application
The full sourcecode of this guide is available here: sequent-examples.
We will continue with this app in the Finishing the web application guide.