A duct library for building simple database-driven handlers for the datomic database.
Ported from duct.handler.sql).
To install, add the following to your project dependencies:
[hden/duct.handler.datomic "0.1.0-SNAPSHOT"]
This library is designed to be used with a routing library with Duct bindings, such as Ataraxy.
Querying the database generally follows a HTTP GET request. There are two keys for creating handlers that query the database:
:duct.handler.datomic/query
- for multiple results:duct.handler.datomic/query-one
- for when you have only one resultThe simplest usage is a handler that queries the database the same way each time:
{[:duct.handler.datomic/query :example.handler.product/list]
{:db #ig/ref :duct.database/datomic
:query [:find (pull ?e [*])
:where [?e :product/id _]]}}
In the above example, a composite key is used to provide a unique identifier for the handler.
The :db
option should be a duct.database.datomic.Boundary
record, and
the :query
option should be a datalog query form.
If you omit the :db
option, the ig/prep
stage will default it to
#ig/ref :duct.database/datomic
. This means you can write:
{[:duct.handler.datomic/query :example.handler.product/list]
{:query [:find (pull ?e [*])
:where [?e :product/id _]]}}
If you want to change the query based on the request, you can
destructure the parameters you want in the :request
option and binds them
to the args option:
The db argument if automatically bind to the first argument.
{[:duct.handler.datomic/query-one :example.handler.product/find]
{:db #ig/ref :duct.database/datomic
:request {[_ id] :ataraxy/result}
:args [id]
:query [:find (pull ?e [*])
:in $ ?id
:where [?e :product/id ?id]]}}
The response can also be altered. The :rename
option is available
for renaming keys returned in the result set:
{[:duct.handler.datomic/query :example.handler.product/list]
{:db #ig/ref :duct.database/datomic
:query [:find (pull ?e [:product/id :product/name])
:where [?e :product/id _]]
:rename {:product/id :id, :product/name :name}}}
The :hrefs
option adds hypertext references based on URI
Templates:
{[:duct.handler.datomic/query :example.handler.product/list]
{:db #ig/ref :duct.database/datomic
:query [:find (pull ?e [:product/id :product/name])
:where [?e :product/id _]]
:hrefs {:self "/products{/id}"}}}
The :hrefs
key takes template parameters from the result fields, and
from the request destructuring.
Finally, the :remove
key allows keys to be removed from the
response. This is useful if you want a key to be used in a href, but
not to show up in the final response:
{[:duct.handler.datomic/query :example.handler.product/list]
{:db #ig/ref :duct.database/datomic
:query [:find (pull ?e [:product/id :product/name])
:where [?e :product/id _]]
:hrefs {:self "/products{/id}"}
:remove [:product/id]}}
Sometimes a HTTP request will alter the database. There are two keys for creating handlers that update the database:
:duct.handler.datomic/insert
- for inserting datoms:duct.handler.datomic/execute
- for updating or deleting datomsThe :duct.handler.datomic/insert
key is designed to respond to a HTTP
POST
event and send a "Created" 201 response with a "Location"
header created from the generated ID of a transaction. For example:
{[:duct.handler.datomic/insert :example.handler.product/create]
{:db #ig/ref :duct.database/datomic
:request {{:strs [id name]} :form-params}
:args {:tx-data [{:product/id id :product/name name}]
:location "/products{/id}"}}
The resolved tempids are also available as template parameters.
The :duct.handler.datomic/execute
doesn't have to worry about generated
keys; it's designed to report to HTTP DELETE
and PUT
requests. If
the transaction appends one or more datoms, a "No Content" 204 response is
returned, otherwise, if zero datoms are appended (rare for datomic), a
404 response is returned.
For example:
{[:duct.handler.datomic/execute :example.handler.product/update]
{:db #ig/ref :duct.database/datomic
:request {[_ id name] :ataraxy/result}
:args {:tx-data [{:product/id id :product/name name}]}
[:duct.handler.datomic/execute :example.handler.product/destroy]
{:db #ig/ref :duct.database/datomic
:request {[_ id] :ataraxy/result}
:args {:tx-data [[:db/retractEntity [:product/id id]]]}}
Together with a router like Ataraxy, the configuration might look something like:
{:duct.core/project-ns example
:duct.core/environment :production
:duct.module.web/api {}
:duct.module/sql {}
:duct.module/ataraxy
{"/products"
{[:get] [:product/list]
[:get "/" id] [:product/find ^uuid id]
[:post {{:strs [name]} :form-params}]
[:product/create name]
[:put "/" id {{:strs [name]} :form-params}]
[:product/update ^uuid id name]
[:delete "/" id]
[:product/destroy ^uuid id]}}
[:duct.handler.datomic/query :example.handler.product/list]
{:query [:find (pull ?e [*])
:where [?e :product/id _]]}
[:duct.handler.datomic/query-one :example.handler.product/find]
{:request {[_ id] :ataraxy/result}
:args [id]
:query [:find (pull ?e [*])
:in $ ?id
:where [?e :product/id ?id]]}
[:duct.handler.datomic/insert :example.handler.product/create]
{:request {[_ id name] :ataraxy/result}
:args {:tx-data [{:product/id id :product/name name}]
:location "/products{/id}"}
[:duct.handler.datomic/execute :example.handler.product/update]
{:request {[_ id name] :ataraxy/result}
:args {:tx-data [{:product/id id :product/name name}]}
[:duct.handler.datomic/execute :example.handler.product/destroy]
{:request {[_ id] :ataraxy/result}
:args {:tx-data [[:db/retractEntity [:product/id id]]]}}
Note that the :db
key can be omitted in this case, because the
:duct.module/datomic
module will automatically populate the handlers
with a database connection.
This library can produce simple handlers that require only the information present in the request map. When paired with a good routing library, this can be surprisingly powerful.
However, don't overuse this library. If your requirements for a
handler are more complex, then create your own init-key
method for
the handler. It's entirely possible, even likely, that your app will
contain handlers created via this library, and handlers that are
created through your own init-key
methods.
Although it's not a requirement to use this library, you should almost certainly
want to create your own unique identity instead of exposing Datomic's :db/id
to
the API. It is recommend that you always have some kind of external ID for entities in you system.
This example might look harmless, but I assure you that it's not.
{:duct.core/project-ns example
:duct.module/ataraxy
{"/products"
{[:delete "/" id] [:product/destroy ^int id]}}
[:duct.handler.datomic/execute :example.handler.product/destroy]
{:request {[_ id] :ataraxy/result}
:args {:tx-data [[:db/retractEntity id]]}}
When people calls your API endpoint with DELETE /products/17592186045422
, how
do you know that the entity is a product?
Here is a better way:
{:duct.core/project-ns example
:duct.module/ataraxy
{"/products"
{[:delete "/" id] [:product/destroy id]}}
[:duct.handler.datomic/execute :example.handler.product/destroy]
{:request {[_ id] :ataraxy/result}
:args {:tx-data [[:db/retractEntity [:product/id id]]]}}
Copyright © 2020 Haokang Den
This program and the accompanying materials are made available under the terms of the Eclipse Public License 2.0 which is available at http://www.eclipse.org/legal/epl-2.0.
Can you improve this documentation?Edit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close