XTDB (the database Biff uses) does not enforce schema on its own. Biff provides schema enforcement with Malli. Here's a Malli crash course.
Say we want to save a user document like this:
{:xt/id #uuid "..."
:user/email "bob@example.com"
:user/favorite-color :blue
:user/age 132}
We could model it in our Malli schema like so:
(def schema
{:user/id :uuid
:user [:map {:closed true}
[:xt/id :user/id]
[:user/email :string]
[:user/joined-at inst?]
[:user/foo {:optional true} :string]
[:user/bar {:optional true} :string]]
...})
For the attribute types, Malli has a handful of
type schemas like :uuid
and :string above, and it also supports
predicate schemas like
inst? above.
For compactness, most of the attributes (like :user/email and :user/joined-at) are
defined inside the document schema. But for the :xt/id key, it often makes sense to create
an alias (:user/id in this example) so that it can be used in other documents. For example, the
:msg document below includes a :msg/user key which has a value of type :uuid, and the :user/id
key makes it clear that this key is meant to refer to a :user document:
(def schema
{:user/id :uuid
...
:msg/id :uuid
:msg [:map {:closed true}
[:xt/id :msg/id]
[:msg/user :user/id]
...]})
Note however that Biff doesn't enforce that the :msg/user key actually points
to a user document. As long as the key is a UUID, it will pass the schema
check.
For mild convenience, your schema can be defined with the biff/doc-schema helper function:
(ns com.example.schema
(:require [com.biffweb :refer [doc-schema] :rename {doc-schema doc}]))
(def schema
{:user/id :uuid
:user (doc {:required [[:xt/id :user/id]
[:user/email :string]
[:user/joined-at inst?]]
:optional [[:user/foo :string]
[:user/bar :string]]})
...})
See also: