Skip to content

Latest commit

 

History

History
568 lines (435 loc) · 11.1 KB

index.org

File metadata and controls

568 lines (435 loc) · 11.1 KB

Sinatra with activerecord

#+tags[]: ruby, sinatra, activerecord

I want to build a very simple sinatra app, but I also want it to connect to the database. Here is the walk through for how to have a very base install, which includes migrations.

The final code is available at wschenk/sinatra-ar-template.

Setup the base

Add up your Gemfile:

bundle init
bundle add bundler sinatra sqlite3 sinatra-activerecord puma rackup
bundle add rerun --group development

Simple config.ru

# config.ru
require_relative 'app'

run Sinatra::Application

Dockerize

A pretty standard Dockerfile:

ARG RUBY_VERSION=3.2.2
FROM ruby:$RUBY_VERSION-slim as base

RUN apt-get update -qq && \
    apt-get install --no-install-recommends -y build-essential curl

RUN gem update --system --no-document && \
    bundle config set --local without development

    # Rack app lives here
WORKDIR /app

 # Install application gems
COPY Gemfile* .
RUN bundle install --without development

RUN useradd ruby --home /app --shell /bin/bash
USER ruby:ruby

# Copy application code
COPY --chown=ruby:ruby . .

# Start the server
EXPOSE 3000
  CMD ["bundle", "exec", "rackup", "--host", "0.0.0.0", "--port", "3000"]

Rake tasks

And now a Rakefile, where we add runner tasks, docker tasks, as well as the activerecord migration tasks.

# Rakefile
require "sinatra/activerecord/rake"

desc "Starts up a development server that autostarts when a file changes"
task :dev do
  system "PORT=3000 rerun --ignore 'views/*,index.css' \"bundler exec rackup\""
end

desc "Builds a Docker image and runs"
task :build do
  system "docker build . -t app && docker run -it --rm -p 3000:3000 app"
end

namespace :db do
  task :load_config do
    require "./app"
  end
end

Setup the app

Now an app.rb

# app.rb
require 'sinatra'
require "sinatra/activerecord"
require_relative 'routes/posts.rb'
require_relative 'routes/account.rb'

# For cookies
use Rack::Session::Cookie, :key => 'rack.session',
    :path => '/',
    :secret => 'sosecret'

set :default_content_type, :json

get '/' do
  {message:"Hello world."}
end

get '/up' do
  {success:true}
end

Database Config

First create a directory for the database.yml file:

mkdir config

Then setup sqlite3:

development:
  adapter: sqlite3
  database: db/development.sqlite3
  pool: 5
  timeout: 5000
rake db:create

Create a model

We’ll begin by creating 2 directories, one that stores the model logic and the other which defines the routes

mkdir routes models

Database

Let’s add a model, for example post

rake db:create_migration post

Then we can add our fields to it to it

class Post < ActiveRecord::Migration[7.0]
  def change
    create_table :posts do |t|
      t.string :name
      t.text :body
      t.timestamps
    end
  end
end

Then create the table

rake db:migrate

And we can verify that it’s there

echo .schema posts | \
    sqlite3 db/development.sqlite3 | \
    fold -w 80 -s

Code

First the model, where we tell it we need to have some required fields

# models/post.rb
class Post < ActiveRecord::Base
  validates_presence_of :name, :body
end

Then the routes itself, where we either return a list of all the posts or we create a post and return it.

# routes/posts.rb
require_relative '../models/post.rb'

get '/posts' do
  Post.all.to_json
end

post '/posts' do
  p = Post.new( name: params[:name], body: params[:body] )
  if !p.save
    p.errors.to_json
  else
    p.to_json
  end
end

Testing it out

Start the server

rake dev

Then we can test this out:

curl http://localhost:9292/posts

Add a post

curl http://localhost:9292/posts -d "name=First Post&body=This is the body" | jq .

Then we can see the results:

curl http://localhost:9292/posts | jq .

We can also try to add a post that’s missing a required field:

curl http://localhost:9292/posts -d "name=No body" | jq .

Adding a password

Lets see how to add authentication.

bundle add bcrypt --version '~> 3.1.7'

Create the migration and run it:

rake db:create_migration account
class Account < ActiveRecord::Migration[7.0]
  def change
    create_table :accounts do |t|
      t.string :name
      t.string :password_digest
    end
  end
end
rake db:migrate

Add the model and the route

# models/account.rb
class Account < ActiveRecord::Base
  validates :name, uniqueness: true, presence: true

  has_secure_password
end

Then lets add some routes for it:

# routes/account.rb

require_relative '../models/account.rb'

post '/signup' do
  account = Account.new(
    name: params[:name],
    password: params[:password],
    password_confirmation: params[:password_confirmation] || '')

  if account.save
    account.to_json
  else
    account.errors.to_json
  end
end

post '/login' do
  account = Account.find_by( name: params[:name])&.authenticate(params[:password])

  if account
    session[:account_id] = account.id
    puts "setting session #{session[:account_id]}"
  end

  { success: account }.to_json
end

get '/private' do
  auth_check do
    { message: "This is a secret" }.to_json
  end
end

def auth_check
  unless session[:account_id]
    return { access: :denied }.to_json
  else
    return yield
  end
end

Test account creation

Empty password confirmation

curl http://localhost:9292/signup -d "name=will&password=password" | jq .

Not matched password confirmation

curl http://localhost:9292/signup -d \
     "name=will&password=password&password_confirmation=pass" | jq .

Happy path

curl http://localhost:9292/signup -d \
     "name=will&password=password&password_confirmation=password" | jq .

Trying a double signup

curl http://localhost:9292/signup -d \
     "name=will&password=password&password_confirmation=password" | jq .

Testing login

Unauthenticated access

curl http://localhost:9292/private

Login and store the cookie in the jar!

curl http://localhost:9292/login \
     -d 'name=will&password=password' \
     -c cookies.txt | jq .

Pass in the session cookie

curl -b cookies.txt http://localhost:9292/private | jq .

Create a model

Add a migration, for a table called for example poi

rake db:create_migration poi

Here is an example migration

:tangle db/migrate/20230930161023_poi.rb
class Poi < ActiveRecord::Migration[7.0]
  def change
    create_table :pois do |t|
      t.string :name
      t.decimal :latitude, precision: 10, scale: 6
      t.decimal :longitude, precision: 10, scale: 6
    end
  end
end

Then we can run it:

rake db:migrate

Check out the table

echo .schema pois | \
    sqlite3 db.sqlite3 | \
    fold -w 80 -s