The theta release contains a number of bug fixes and API improvements to keep the code base simple.
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.5.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
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
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 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.
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 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 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
.
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.
Can you improve this documentation? These fine people already did:
Sean Walker & JohnEdit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close