The two layers#
An eezo application talks to a database, serves HTTP, or does both, and it says which by the artifact it depends on. This page is about what that choice buys you, what it forbids, and how the two halves fit together when you have both.
What a layer is#
An layer is one side of the application that eezo brings up before your code runs and takes down after:
- The database layer is the connection pool and the schema commands. eezo builds the pool
from your settings, installs it where
transactandreadcan find it, runs your program, and closes it afterwards. - The http layer is the server and the routes. eezo starts Jetty on your port, serves your route table, and on shutdown refuses new requests, finishes the ones in flight, and only then returns, so the database layer closes after the last request.
A derivation belongs to one layer. Table is the database layer's. Form and Resource are the
http layer's. If your application doesn't have a layer, that layer's derivations aren't on the
classpath, and asking for one is a compile error.
Three artifacts, three entry traits#
| artifact | entry trait | layers |
|---|---|---|
eezo-http | HttpApp | http |
eezo-db | DbApp | database |
eezo | EezoApp | both |
eezo, the umbrella, is the default. It's what eezo new puts in your build, and EezoApp is
one trait that has everything: routes and the server's overrides from HttpApp, schema and
the database lifecycle from DbApp, and the live socket from LiveApp in between. An
application that only needs one layer depends on that layer's artifact and extends its trait. The
examples in the repository cover all three: hello is eezo-http alone, reminders is
eezo-db alone, blog and todo are the umbrella.
What the missing layer looks like#
The hello example has the http layer only. Its README tries a model deriving Table and shows
the compiler's answer:
[error] -- [E008] Not Found Error: hello/src/main/scala/models/Note.scala:4:15
[error] 4 |import io.eezo.db.Table
[error] | ^^^^^^^^^^
[error] | value db is not a member of io.eezoThe reminders example is the mirror image: eezo-db alone, and derives Form fails on the
import of io.eezo.http.Form the same way. There's no flag to flip. The package isn't there
because the artifact isn't there, and the cure is one line in build.sbt: depend on eezo and
extend EezoApp.
The commands follow the same rule. main is inherited from a small dispatch trait both layers
share, and each layer adds its own commands in front of it. On the umbrella, help lists all of
them:
eezo
status code vs live database
sync [--apply] [--force] apply the code/db diff directly (dev only)
freeze <name> write a migration from schema.json -> code
migrate [--apply] apply pending migrations, then verify
reset drop everything and recreate from the model
drop drop everything
dump print the derived snapshot
ddl print full DDL
dev serve with the route listing and the reload client on
routes the mounted table, with boot's warnings
help this list
freeze takes --accept-all / --skip-destructive in place of the prompt
(bare --json implies --skip-destructive)
every command takes --json for machine-readable output (same exit codes);
no arguments runs the applicationThe first eight are the database layer's, the next two are the http layer's. On eezo-http
alone, status is an unknown command, and on eezo-db alone so is dev. Every command takes
--json, with the same exit codes, so a script gates on the code and parses the body.
Stacking the two#
Each layer has a program method, which is what running with no arguments does. On the http
layer it's boot(), and boot defaults to serving the routes. On the database layer it's
withDatabase(boot()), and boot is abstract because a database-only application is its job.
Both layers implement program concretely, and neither marks it override. That's what makes
stacking them by hand a compile error:
object Main extends DbApp with HttpApp // does not compileThe compiler sees two concrete programs and refuses until something says which one it means.
If the layers had written override, the later mixin would win silently, and extends DbApp with HttpApp would serve with no database installed, in whichever order happened to put
HttpApp last. EezoApp is the one place that resolves it, and it resolves it in words:
override protected def program(): Unit = withDatabase(boot())The http layer's boot, under the database layer's lifecycle. dev composes the same way: the
umbrella runs the drift check first, under the database, and serves the drift page instead of
the application while the drift is dangerous.
The modules around the layers#
eezo-authsits on the http layer and never depends oneezo-db. A guard reads the session and calls two functions you supply, so it never opens a connection or knows a table exists. The one place aPasswordmeets storage is theColumn[Password]you write beside your own user model, where both modules are already visible. An application with a login page and no database doesn't get a JDBC driver on its classpath.eezo-livesits on the http layer too.LiveAppadds the live socket and client script to whatever the application serves.EezoAppincludes it, and an application oneezo-httpalone writesextends LiveAppinstead ofextends HttpAppto get it.eezo-testkitsees all of them. It boots a real server against a real database and drives it over HTTP and WebSocket, for tests.
Where to go next#
The http layer's routes come from your file layout, which is routing. The database layer's pool, scopes and query layer are the database layer.