This is probably going to be the next iteration of the declarative CRUD metamodel that powers Hyperfiddle. It's just a design sketch, the current metamodel in prod is different. Hyperfiddle is an easy way to make a CRUD app. Hyperfiddle is based on Datomic, a simple graph database with the goal of "enabling declarative data programming in applications."
This extends Datomic pull syntax into something that composes in richer ways. The key idea is that Pull notation expresses implicit joins and thus can be used to declare data dependencies implicitly, without needing to name them. We also handle tempids, named transactions, and hyperlinks to other pages. We satisfy the hypermedia constraint, like HTML and the web.
{identity ; Pass through URL params to query
[{:dustingetz/event-registration ; virtual attribute identifying a query
[:db/id
(:dustingetz/email {:hf/a :dustingetz/registrant-edit}) ; hyperlink to detail form
:dustingetz/name
{:dustingetz/gender
[:db/ident]}
{:dustingetz/shirt-size
[:db/ident]}]}]
nil ; no query params
[{:dustingetz/genders ; genders query (for picklist)
[:db/ident]}]
((hf/new) {:hf/tx :dustingetz/register}) ; generate a Datomic tempid and wire up a transaction
[:db/id
:dustingetz/email
:dustingetz/name
{:dustingetz/gender
[:db/ident
{:dustingetz/shirt-sizes ; shirt-sizes query depends on gender
[:db/ident]}]}
{:dustingetz/shirt-size
[:db/ident]}]}
We hesitated to go in the direction of extending Datomic syntax because it could conflict in the future, however Rich has been quite clear that Datomic queries are data, maps should be open (explicitly should not be closed), etc, and this is all safe because data is namespaced. The benefit to this syntax is it's simple enough that it doesn't require tool assistance to write (the current hyperfiddle metamodel is not palatable without UI assistance).
The popover is triggered by (hf/new)
which allocates a tempid for a CREATE operation. CREATE operations are composed of a parent query and a child form (implicit in the pull). The popover indicates two things: 1) where in the graph the parent/child reference is, and 2) that the insertion is transactional and can be discared as a unit.
Queries are declarative which lets us use metaprogramming techniques to manipulate them as data, for example a client UI typeahead picker may specify additional database filters as datalog which will be spliced into the query.
{:dustingetz/genders
[:find (pull ?e [:db/ident])
:where
[?e :db/ident ?i]
[(namespace ?i) ?ns]
[(ground "dustingetz.gender") ?ns]]
:dustingetz/shirt-sizes
[:in $ ?gender ; Query dependency, automatically inferred from the pull
:find (pull ?e [:db/ident])
:where
[?e :db/ident ?i]
[?e :dustingetz.reg/gender ?gender]
[(namespace ?i) ?ns]
[(ground "dustingetz.shirt-size") ?ns]]
:dustingetz/entity-history ; just an example of how server code eval might work
(->> (d/q '[:in $ ?e
:find ?a ?v ?tx ?x
:where
[?e ?a ?v ?tx ?x]]
(d/history *$*)
%)
(map #(assoc % 0 (:db/ident (d/entity db (get % 0)))))
(group-by #(nth % 2))
(map-values #(sort-by first %))
(sort-by first))}
This is spec1, we're on a collision course with spec2 and looking forward to discovering how they unify.
(s/def :dustingetz/register (s/keys :req [:dustingetz/email
:dustingetz/name]
:opt [:dustingetz/gender
:dustingetz/shirt-size]))
Hyperfiddle handles forms & tables automatically and lets you customize rendering on an attribute basis. You can of course control the entire renderer. Picklists are simply view progressive enhancement of a form field plus a query. Note that there is no data-sync I/O, async, error handling or other side effects in views!
(defmethod hyperfiddle.api/render #{:dustingetz/gender} ; custom renderer for ::gender
[ctx props]
[hfui/select ctx ; present as picklist
{:options :dustingetz/genders ; Wire up picklist options to named query
:option-label :db/ident}]) ; picklist label
(defmethod hyperfiddle.api/render #{:dustingetz/shirt-size} ; custom renderer for ::shirt-size
[ctx props]
[hfui/select ctx ; second picklist
{:options :dustingetz/shirt-sizes
:option-label :db/ident
:hf/where '[[(name ?i) ?name] ; client specified database filters
[(clojure.string/includes? ?name %)]]}])
Transactions are attached to the stage
button of popovers. They are positioned at a point in the graph through the pull, so their EAV parameters can be inferred.
(defmethod hyperfiddle.api/tx :dustingetz/register [ctx [e a v] props]
[[:db/add v :dustingetz/registered-date (js/Date.)]])
This method runs and it's return value is concatenated with the popover form datoms, for final form submission in a single transaction. If you click cancel
on a popover it discards the form datoms without submitting any transaction. The form value is available for inspection via ctx
as well as any other query or value in scope whose dependencies are satisfied (e.g. ::shirt-sizes is available because there is a ::gender in scope).
In a prod configuration, transaction functions evaluate securely on the server (behind an HTTP POST or something).
Hyperfiddle understands: ident, valueType, cardinality, unique, isComponent and generates idiomatic Datomic transactions, including lookup refs.
[{:db/ident :dustingetz/name,
:db/valueType :db.type/string,
:db/cardinality :db.cardinality/one,
:db/doc "Registrant's name"}
{:db/ident :dustingetz/email,
:db/valueType :db.type/string,
:db/cardinality :db.cardinality/one,
:db/unique :db.unique/identity,
:db/doc "Registrant's email"}
{:db/ident :dustingetz/gender,
:db/valueType :db.type/ref,
:db/cardinality :db.cardinality/one,
:db/doc "Registrant's gender (for shirt size)"}
{:db/ident :dustingetz/shirt-size,
:db/valueType :db.type/ref,
:db/cardinality :db.cardinality/one,
:db/doc "Selected tee-shirt size"}]