Skip to content

Instantly share code, notes, and snippets.

@nrfm
Forked from dustingetz/hyperfiddle-metamodel.md
Created October 10, 2019 13:53
Show Gist options
  • Save nrfm/bb3d01377e99e07b5f55fa5ed7205dab to your computer and use it in GitHub Desktop.
Save nrfm/bb3d01377e99e07b5f55fa5ed7205dab to your computer and use it in GitHub Desktop.

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."

CRUD UI definition

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)

image

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

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))}

Validation

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]))

View progressive enhancement

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

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.

Datomic Schema

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"}]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment