Hodur is a descriptive domain modeling approach and related collection of libraries for Clojure.
By using Hodur you can define your domain model as data, parse and validate it, and then either consume your model via an API making your apps respond to the defined model or use one of the many plugins to help you achieve mechanical, repetitive results faster and in a purely functional manner.
For a deeper insight into the motivations behind Hodur, check the motivation doc.
Hodur has a highly modular architecture. Hodur Engine is always required as it provides the meta-database functions and APIs consumed by plugins.
Therefore, refer the Hodur Engine’s Getting Started first and then return here for Lacinia-specific setup.
After having set up hodur-engine
as described above, we also need
to add hodur/lacinia-schema
, hodur/datomic-schema
, and
hodur/lacinia-datomic-adapter
individually to the deps.edn
file:
{:deps {:hodur/engine {:mvn/version "0.1.2"}
:hodur/lacinia-schema {:mvn/version "0.1.0"}
:hodur/datomic-schema {:mvn/version "0.1.0"}
:hodur/lacinia-datomic-adapter {:mvn/version "0.1.0"}}}
You should require
it any way you see fit:
(require '[hodur/lacinia-schema.core :as hodur-lacinia])
Let’s expand our Person
model from the original getting started by
“tagging” the Person
entity for Lacinia. You can read more about
the concept of tagging for plugins in the sessions below but, in
short, this is the way we, model designers, use to specify which
entities we want to be exposed to which plugins.
(def meta-db (hodur/init-schema
'[^{:lacinia/tag-recursive true}
Person
[^String first-name
^String last-name]]))
The hodur-lacinia-schema
plugin exposes a function called schema
that generates your model as a Lacinia schema payload:
(def lacinia-schema (hodur-lacinia/schema meta-db))
When you inspect datomic-schema
, this is what you have:
{:objects
{:Person
{:fields
{:firstName {:type (non-null String)},
:lastName {:type (non-null String)}}}}}
Assuming Lacinia’s com.walmartlabs.lacinia.schema
is bound to
schema
, you can initialize your instance by compiling the schema like this:
(def compiled-schema (-> lacinia-schema
schema/compile))
Most certainly you will have some resolvers defined in your schema
(say :person-query/resolver
that you want to bind to function
person-query-resolver
). In this case, attach the resolvers using
Lacinia’s com.walmartlabs.lacinia.util/attach-resolvers
function
(shown in this next example as bound to util/attach-resolvers
:
(def compiled-schema (-> lacinia-schema
(util/attach-resolvers
{:person-query/resolver person-query-resolver})
schema/compile))
All Hodur plugins follow the Model Definition as described on Hodur Engine’s documentation.
In many cases you want simply to have a GraphQL query that returns a specific instance of an entity.
For these cases, make sure your Lacinia query field is marked with
:lacinia/query
plus :lacinia->datomic.query/type :one
. Both the
query field and the entity returned must be tagged for lacinia and
datomic.
The adapter will look for params in the field marked with
:lacinia->datomic.param/lookup-ref
. Each of these params must
point to a Datomic lookup ref that will be used to located this
entity.
In the following example, we are specifying that param email
will
cause the adapter to search for an employee that has the attribute
:employee/email
set to the value of the parameter, therefore
fetching it.
[QueryRoot
[^{:type Employee
:lacinia->datomic.query/type :one}
employee
[^{:type String
:optional true
:lacinia->datomic.param/lookup-ref :employee/email}
email]]
It’s important to mention that, in order for this to work on
Datomic, the attribute :employee/email
must be marked as
identity. Example:
[Employee
[^{:type String
:datomic/unique :db.unique/identity}
email
^{:type String}
first-name]]
If you have any sort of treatment you want to do on the parameters
before sending them to Datomic, you can specify
:lacinia->datomic.param/transform
on the param. This marker
points to a fully qualified symbol that is expected to be resolved
to your desired function.
TBD
:lacinia->datomic.query/type :many
- must be in a field in a
lacinia/query
- both entity and field must be lacinia tagged
- “list object”:
[^Integer total-count
^PageInfo page-info
^{:type Employee :cardinality [0 n]} nodes]
- then page info:
[PageInfo [:type Integer total-pages
:type Integer current-page
:type Integer page-size
:type Integer current-offset
:type Boolean has-next
:type Integer next-offset
:type Boolean has-prev
:type Integer prev-offset]
:lacinia->datomic.param/offset true :lacinia->datomic.param/limit true
:lacinia->datomic.param/filter-builder user/new-build-employee-name-search-where
- one to one are simple lookups
- everything else is paginated… two options:
:lacinia->datomic.field/lookup-many
formerly called lookup
:lacinia->datomic.field/reverse-lookup-many
formerly called reverse-lookup
TBD
- TBD: should we have a final “resolver” like function with the prepped payload before returning the response???
TBD
- :lacinia->datomic.field/depends-on [:employee/first-name :employee/last-name]
- then can receive both on a resolver
TBD
Notes:
For creating payload:
map-to
might be ok - it’s a way to limit the input and still indicate what to parse to- a limitation here is that fields MUST be exactly the same
- alternative here would be to have a a keyword mapper… for
instance,
EmployeeInput
would have a map-to:employee
- it feels similar :point-up:
Entity Identification:
- we need a kind of
lookup-ref
to help find the entity on an upsert scenario - it would have to be something like
email
->:employee/email
- consider whether we could have a automatic
uuid
generator too - or maybe a kind of “transformer” that gets called with the args so that users can extend it (as in, creating a random uuid if one is not offered)
Deletion:
- :db/retractEntity needs to be considered for sure (it seems to smartly trickle to attributes, refs, and components)
[:db/retractEntity [:person/email "[email protected]"]]
- :db/retract needs to specify Op E A V
- if you specify a V that does not exist, the transaction gets through but does not change anything
Field marked as /retract-entity and email marked as lookup-ref
retractPerson (email: "bla")
Field marked as /retract, email as lookup-ref, and projectuuid as /target-ref?
retractProjectFromPerson (email: "bla", projectUuid: "xxx")
Field marked as /retract, email as lookup-ref, and factInput as /retract-map? ~retractFactsFromPerson (email: “bla”, factInput: {likes: “pizza”})
:lacinia->datomic.input/map-to Employee
:lacinia->datomic.input/attach-from Project TBD> try to remove
:lacinia->datomic.input/dbid true TBD> try to remove
:lacinia->datomic.input/delete-from Project TBD> try to remove
:lacinia->datomic.mutation/type :upsert TBD> keep
:lacinia->datomic.mutation/type :add-to TBD> try to remove
:lacinia->datomic.mutation/type :attach-to TBD> try to remove
:lacinia->datomic.mutation/type :delete TBD> keep
TBD: what does it mean in practice
If you find a bug, submit a GitHub issue.
This project is looking for team members who can help this project succeed! If you are interested in becoming a team member please open an issue.
Copyright © 2018 Tiago Luchini
Distributed under the MIT License (see LICENSE).