Skip to content
Mar 19, 2019
Simplify (routes), (wrap-routes)
Ensure parent middleware runs before child middleware.

Parent middleware should run before child middleware which means that it 
needs to be added last in the vector sequence, not first

@swlkr swlkr released this Mar 19, 2019 · 3 commits to master since this release

Fix a bug from legacy resource route names

Assets 2

@swlkr swlkr released this Mar 19, 2019 · 4 commits to master since this release

  • Where clause generation simplification
  • Pluralize person to people instead of "persons"
  • Add [:resource :only [:index :view]] and [:resource :except [:index :view]] support to routes
  • Add (coast/find-by :table {:where "clause"}) function
Assets 2

@swlkr swlkr released this Mar 19, 2019 · 12 commits to master since this release

There was a bug where 500 errors would return {:status 500 body: {:status 500 :body ""}} instead of just what you put in the function 😬

Assets 2

@swlkr swlkr released this Mar 19, 2019 · 18 commits to master since this release

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 insert/update/delete. They no longer return the value that was changed, they just return the number of records changed.

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/routes
    (coast/site-routes :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 have to pass layout in app.

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/routes
    (coast/site-routes :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.

Assets 2

@swlkr swlkr released this Dec 22, 2018 · 161 commits to master since this release

Apparently postgres doesn't let you create a table named user but it will let you make a table named "user" which you then have to quote everywhere, so the two options were:

  1. Quote all table names everywhere (won't work with defq)
  2. Warn when trying to create a table named user

Went with option 2, so when you make a migration like this:

{:db/col :user/name :db/type "text"}

When you run db/migrate you'll get an Exception that looks like this:

Exception in thread "main" java.lang.Exception: user is a reserved word in postgres try a different name for this table
Assets 2

@swlkr swlkr released this Oct 30, 2018 · 166 commits to master since this release

  • b40b884 - Coerce timestamps from pulls to #inst's
  • d015bb8 - Add # support to url-for
  • 0b06ac3 - Don't require components ns to be in the project
  • c289ce9 - Look for "/404" and "/500" routes
  • c2b4b69 - Stop throwing reader is nil when assets don't exist
  • 63c2b13 - Order deps alphabetically
  • 39c752b - Add wrap-layout to coast ns
  • 64d0e57 - Replace make server with make repl + a call to (server/-main)
  • 770a7aa - Fix routes header
  • dcca5a5 - Add quickstart to README
Assets 2

@swlkr swlkr released this Oct 30, 2018 · 176 commits to master since this release

  • f96e1f8 - Eager load components and routes on startup

Routes wouldn't load unless you loaded everything in the REPL, not just the server namespace

Assets 2

@swlkr swlkr released this Oct 3, 2018 · 180 commits to master since this release

  • 0f0299e - Change url-for to handle qualified keys
  • fab6c6b - Add a sweet docstring
  • 215a259 - Update README.md
  • 69b85f5 - Wrap read template in :div
  • c8773ac - Don't escape dev exception page
  • 9642d48 - Update list template

The biggest change here is url-for now handles qualified keywords and automatically converts them to keywords with dashes! Inspiration was this issue here: #29

Before:

[:get "/todos/:todo-id" :todo.index/view]

(let [todo {:todo/id 1}]
  (url-for :todo.index/view {:todo-id (:todo/id todo)}))

Now:

[:get "/todos/:todo-id" :todo.index/view]

(let [todo {:todo/id 1}]
  (url-for :todo.index/view todo))

Thank you to @mgerlach-klick for kicking the tires 🚘 so hard and @NielsRenard for that README change!

Assets 2

@swlkr swlkr released this Oct 3, 2018 · 186 commits to master since this release

url-for actually has a second arg, I swear

Assets 2
You can’t perform that action at this time.