When using a document database I am schemaless, meaning I no longer have an init.sql that I frequently consult to ensure I’m adding the right fields. One good answer, as mentioned by didibus, is to leverage Spec for this. So instead of definitions for each table in init.sql, I’ve built myself a cljc file which contains entries like this:
I’m not a pro with spec. The height of my appreciation is the way re-frame allows specs to be automatically connected with front-end db transactions, failing if the spec check fails. Other than that, though, I’m not sure what best practices are from here: are my specs missing anything? What’s the most efficient way to make use of them?
Here is my current attempt at this: I use (assert) to check if the map matches a spec before I add it to the database. Which spec it should match is composed by two keys expected to be in the map. What do you think? Is there a simpler way to accomplish this?
(s/def :centrifuge.user/v1
(s/keys :req [:doctype/version
:user/name
:user/role
:user/password]
:req-un [::doctype]))
(defn construct-specname
"Construct the name of the spec from the `:doctype` and `:doctype/version` of `m`,
producing something like `:centrifuge.user/v1` which should match an existing spec"
[m]
(let [{t :doctype v :doctype/version} m]
(keyword (str "centrifuge." t) (str "v" v))))
(defn crux-conforms?
"Determine whether a map conforms as one of our maps"
[m]
(let [specname (construct-specname m)]
(s/conform specname m)))
(defn explain-str
"Explain conformity of the given map `m`"
[m]
(let [specname (construct-specname m)]
(s/explain-str specname m)))
(defn PUT
"Create something in crux"
([crux m]
(assert (crux-conforms? m)
(str "Invalid document: "
(cspecs/explain-str m)))
(crux/submit-tx crux [:crux.tx/put m])))
Sometimes I also use :post constraint too if I need to make sure that the output has some specific shape/properties. This post from Fogus has more examples of this type of contraints clojures-pre-and-post
However you have to take some precaution when doing this to save directly to database because if you have extra additional fields in the user map the validity check will let it pass and you might be saving more data than you were willing to or even breaking something.
Don’t know how much I can add on the topic besides:
performance: malli is designed with performance in mind, allowing you to get the fastest possible validation. Its specs are data structures and less a DSL.
closed vs. open: your spec is [:map [:x int?] [:y int?]]. with open schema, {:x 1 :y 2 :z 3} satisfies it. With closed schema, it fails since :z is not in schema.