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 maps should be open and it's safe because data is namespaced. The benefit to this approach is that this is the first syntax that is simple enough that it doesn't require tool assistance to write (the current hyperfiddle metamodel is not palatable without the UI helping)
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 but we 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 onto 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 positioned at a point in the graph by their ident, so their EAV parameters can be inferred. You can resolve any other query whose dependencies are satisfied through a ctx
protocol. In a prod configuration, these evaluate securely on the server.
(defmethod hyperfiddle.api/tx :dustingetz/register [ctx [e a v] props]
[[:db/add v :dustingetz/registered-date (js/Date.)]])
Transactions are attached to the stage
button of popovers. This method runs and it's return value is concatenated with the popover form, for final form submission in a single transaction. If you click cancel
on a popover it discards the form without submitting any transaction. The form value is available for inspection via ctx
.
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"}]