Upgrading from eta

The theta release contains a number of bug fixes and API improvements to keep the code base simple.

Getting Started

The first step to upgrade from eta to theta is to update coast itself and add your database driver to deps.edn

; deps.edn
{:deps {coast-framework/coast.theta {:mvn/version "1.0.0"}
        org.postgresql/postgresql {:mvn/version "42.2.5"}
       ; or for sqlite
       org.xerial/sqlite-jdbc {:mvn/version "3.25.2"}}}

This is the first release where multiple databses (postgres and sqlite) are supported, but it also means that the database driver is up to you, not coast, similar to all of the other web frameworks out there.

The next step is to add another path to deps.edn's :paths key:

; deps.edn
{:paths ["db" "src" "resources"]}

The db folder is now where all database related files are stored instead of resources

Finally, re-download the coast shell script just like if you were installing coast again for the first time. There is a reason it's coast.theta and not coast.eta

curl -o /usr/local/bin/coast https://raw.githubusercontent.com/coast-framework/coast/master/coast && chmod a+x /usr/local/bin/coast

Migrations

There were a just a few changes to the way database migrations and database schema definitions are handled, so instead of confusing edn migrations which should still be supported, you can now define migrations with clojure and define the schema yourself as well. Plain SQL migrations still work and will always work.

Here's how the new migrations work

coast gen migration create-table-member email:text nick-name:text password:text photo:text

This generates a file in the db folder that looks like this:

(ns migrations.20190926190239-create-table-member
  (:require [coast.db.migrations :refer :all]))

(defn change []
  (create-table :member
    (text :email)
    (text :nick-name)
    (text :password)
    (text :photo)
    (timestamps)))

There are more helpers for columns and references detailed in Migrations

Previously, this was a confusing mess of edn without any clear rhyme or reason. Hopefully this is an improvement over that. Running migrations is the same as before:

make db/migrate

This does not generate a resources/schema.edn like before because the schema for relationships has been separated and is now defined by you, which means pull queries not only work with * as in

(pull '* [:author/id 1])
; or
(q '[:pull *
     :from author]) ; this will recursively pull the whole database starting from the author table

but this also means that pull queries and the rest of coast works with existing database schemas. Here's how

Schema

Before, the schema was tied to the database migrations, which seems like a great idea in theory, but in practice it made the migrations complex and brittle. Coast has moved away from that and has copied rails style schema definitions like so:

; db/associations.clj
(ns associations
  (:require [coast.db.associations :refer [table belongs-to has-many tables]]))

(defn associations []
  (tables
    (table :member
      (has-many :todos))

    (table :todo
      (belongs-to :member))))

This new associations file is essentially rails' model definitions all rolled into the same file because in coast you don't need models, just data in -> data out. These functions also build what was schema.edn but you have a lot more control over the column names, the table names and foreign key names, so something like this would also work

; db/associations.clj
(ns associations
  (:require [coast.db.associations :refer [table belongs-to has-many tables]]))

(defn associations []
  (tables
    (table :users
      (primary-key "uid")
      (has-many :todos :table-name "items"
                       :foreign-key "item_id"))

    (table :todos
      (primary-key "uid")
      (belongs-to :users :foreign-key "uid"))))

There's also support for "shortcutting" through intermediate join tables which gives the same experience as a "many to many" relationship:

; db/associations.clj
(ns associations
  (:require [coast.db.associations :refer [table belongs-to has-many tables]]))

(defn associations []
  (tables
    (table :member
      (has-many :todos))

    (table :todo
      (belongs-to :member)
      (has-many :tagged)
      (has-many :tags :through :tagged))

    (table :tagged
      (belongs-to :todo)
      (belongs-to :tag)

    (table :tag
      (has-many :tagged)
      (has-many :todos :through :tagged)))))

Querying

Querying is largely the same, there are new helpers like

(coast/fetch :author 1)

This retrieves the whole row by primary key (assuming your primary key is id). Other notable differences are the requirement of a from statement in all queries:

(coast/q '[:select * :from author])

Previously you could omit the from and do this:

(coast/q '[:select author/*])

This may come back but I don't believe it works for this version. Another small change to pull queries inside of q

(coast/q '[:pull author/id
                 {:author/posts [post/title post/body]}
           :from author]

Previously you had to surround the pull symbols with a vector, now you don't have to!

Another thing that's changed is transact has been deprecated in favor of the much simpler insert/update/delete functions:

(coast/insert {:member/handle "sean"
               :member/email "sean@swlkr.com"
               :member/password "whatever"
               :member/photo "/some/path/to/photo.jpg"})

(coast/update {:member/id 1
               :member/email "me@seanwalker.xyz"})

(coast/delete {:member/id 1})

You can also pass vectors of maps as well and everything should work assuming all maps have the same columns and all maps in update have a primary key column specified

Lesser known but will now work

(coast/execute! '[:update author
                  :set email = ?email
                  :where id = ?id]
                {:email "new-email@email.com"
                 :id [1 2 3]})

Oh one last thing about delete. It no longer returns the value that was deleted, it just returns the number of rows deleted.

Exception Handling

There was quite a bit of postgres specific code related to raise/rescue, that is gone now since the postgres library isn't included anymore, which means any postgres exceptions like foreign key constraint violations or unique constraint violations will show up as exceptions in application code.

Routing

Routing has changed in a few ways, before you had to nest route vectors in another vector, which was confusing, now you call routes on the individual route vectors and coast does some formatting magic to get it into the right format.

(ns routes
  (:require [coast]))

(def routes
  (coast/site
    (coast/with-layout :components/layout
      [:get "/" :home/index]
      [:get "/posts" :post/index]
      [:get "/posts/:id" :post/view]
      [:get "/posts/build" :post/build]
      [:post "/posts" :post/create]
      [:get "/posts/:id/edit" :post/edit]
      [:post "/posts/:id/edit" :post/change]
      [:post "/posts/:id/delete" :post/delete])))

Before you had to wrap all vectors in another vector, now you don't it makes things a little cleaner.

Also multiple layout support per batch of routes is easier as well since you no longer pass :layout into app. Simply wrap any routes with with-layout and give that a function with two arguments and you're in business.

Since the vector of vectors confusion is gone now, routes more naturally lend themselves to function helpers and resource-style url formats:

(ns routes
  (:require [coast]))

(def routes
  (coast/site
    (coast/with-layout :components/layout
      [:resource :posts]

      ; is equal to all of the below routes

      [:get "/posts" :post/index]
      [:get "/posts/build" :post/build]
      [:get "/posts/:id" :post/view]
      [:post "/posts" :post/create]
      [:get "/posts/:id/edit" :post/edit]
      [:post "/posts/:id/edit" :post/change]
      [:post "/posts/:id/delete" :post/delete])))

Views

Views have changed quite a bit, previous versions of coast treated code files like controllers that return html and that's back again, so before each file was separated in view/action function pairs in folders for each "action" that's not the case any more, the default layout for code is now this:

; src/<table>.clj
(defn index [request])
(defn view [request])
(defn build [request])
(defn create [request])
(defn edit [request])
(defn change [request])
(defn delete [request])

index and view correspond to a list/table page and a single row page.

build and create correspond to a new database row form page and a place to submit that form and insert the new row into the database

edit and change represent a form to edit an existing row and a place to submit that form and update the row in the db

delete represents you guessed it a place to submit a delete form.

There are a few new helpers too, even though the old view helpers will still work:

(ns home
  (:require [coast]))

(coast/redirect-to ::index)

This is a combination of redirect and url-for and it makes the handlers so much cleaner.

There's also form-for

(ns home
  (:require [coast]))

(defn edit [request]
  (coast/form-for ::change {:author/id 1}))

This is a combination of coast/form and action-for.

Environment

While .env continues to work, there's now another option when it comes to configuring the app's envrionment: env.edn.

This is similar to .env except instead of key=value it's just edn and this can be checked in to the repo since the database configuration is now separate in db.edn and uses the env variables in production by default.

Just remember to change the session key and set the database values in the environment in production!

That's it for the major changes in Coast.