Commit 78f7bca9 authored by Ivar Refsdal's avatar Ivar Refsdal
Browse files

First cut custom tx function

parent ce78cee3
# vase-transact-fn
## Getting Started
1. Start the application: `lein run`
2. Go to [localhost:8080](http://localhost:8080/) to see: `Hello World!`
3. Read your app's source code at src/vase_transact_fn/service.clj. Explore the docs of functions
that define routes and responses.
4. See your Vase API Specification at `resources/vase-transact-fn_service.edn`.
5. Run your app's tests with `lein test`. Read the tests at test/vase_transact_fn/service_test.clj.
6. Learn more! See the [Links section below](#links).
## Configuration
To configure logging see config/logback.xml. By default, the app logs to stdout and logs/.
To learn more about configuring Logback, read its [documentation](
## Developing your service
1. Start a new REPL: `lein repl`
2. Start your service in dev-mode: `(def dev-serv (run-dev))`
3. Connect your editor to the running REPL session.
Re-evaluated code will be seen immediately in the service.
4. All changes to your Vase Service Descriptor will be loaded - no re-evaluation
### [Docker]( container support
1. Build an uberjar of your service: `lein uberjar`
2. Build a Docker image: `sudo docker build -t vase-transact-fn .`
3. Run your Docker image: `docker run -p 8080:8080 vase-transact-fn`
### [OSv]( unikernel support with [Capstan](
1. Build and run your image: `capstan run -f "8080:8080"`
Once the image it built, it's cached. To delete the image and build a new one:
1. `capstan rmi vase-transact-fn; capstan build`
## Links
* [Pedestal examples](
* [Vase examples](
Executing a DB function on json payload.
## Example usage
Start the server
lein repl
(def srv (run-dev))
In another terminal
curl -H "Content-Type: application/json" -X POST -d '{"payload": [{"item/name": "milk_bottle"}]}' http://localhost:8080/api/accounts/v1/hello
In the console you should then see `hello from DB land:: ==>> {:item/name milk_bottle}`.
This function that emits this line is both used and specified in [vase-transact-fn_service.edn](resources/vase-transact-fn_service.edn):
{:vase.norm/txes [[{:db/id #db/id [:db.part/user]
:db/ident :my-great-fn
:db/fn #db/fn
{:lang :clojure
:params [db m]
:code (do
(println "hello from DB land:: ==>>" m)
{:db/id #db/id[:db.part/db]
:db/ident :item/name
:db/valueType :db.type/string
:db/unique :db.unique/identity
:db/cardinality :db.cardinality/one
:db/doc "The name of an item"}]]}}
{:vase.api/routes {"/hello" {:post [#nsd/transact-fn {:name :accounts.v1/item-create-with-function
:db-fn :my-great-fn
:properties [:item/name]}]}}}}}}
\ No newline at end of file
{:activated-apis [:vase-transact-fn/v1]
:datomic-uri "datomic:mem://example"
;; Datomic Schema Norms
;; --------------------
;; Supports full/long Datomic schemas
{:vase.norm/txes [[{:db/id #db/id[:db.part/db]
:db/ident :company/name
:db/unique :db.unique/value
:db/valueType :db.type/string
{:vase.norm/txes [[{:db/id #db/id [:db.part/user]
:db/ident :my-great-fn
:db/fn #db/fn
{:lang :clojure
:params [db m]
:code (do
(println "hello from DB land:: ==>>" m)
{:db/id #db/id[:db.part/db]
:db/ident :item/name
:db/valueType :db.type/string
:db/unique :db.unique/identity
:db/cardinality :db.cardinality/one
:db.install/_attribute :db.part/db}]]}
{:vase.norm/requires [:vase-transact-fn/base-schema] ;; Also supports schema dependencies
;; and supports short/basic schema definitions
:vase.norm/txes [#vase/schema-tx [[:user/userId :one :long :identity "A User's unique identifier"]
[:user/userEmail :one :string :unique "The user's email"]
;; :fulltext also implies :index
[:user/userBio :one :string :fulltext "A short blurb about the user"]
[:user/company :one :ref "The user's employer"]]]}}
;; Global Specs for the API
;; ------------------------
:db/doc "The name of an item"}]]}}
{:vase-transact-fn.test/age (fn [age] (> age 21))
:vase-transact-fn.test/name (clojure.spec/and string? not-empty)
:vase-transact-fn.test/person (clojure.spec/keys :req-un [:vase-transact-fn.test/name
;; API Tagged Chunks/Versions
;; --------------------------
{"/hello" {:get #vase/respond {:name :vase-transact-fn.v1/simple-response
:body "Hello World"}}
"/hello2" {:get #vase/respond {:name :vase-transact-fn.v1/param-response
;; POST bodies and query string args are bound in :params
:params [user]
;; `edn-coerce` will edn/read-string on params, with all active data readers
:edn-coerce [user]
:body (if user
(str "Hello " user ". You are a: " (type user))
"Hello World!")}}
"/redirect-to-google" {:get #vase/redirect {:name :vase-transact-fn.v1/r-page
:url ""}}
"/redirect-to-param" {:get #vase/redirect {:name :vase-transact-fn.v1/ar-page
:params [someurl]
:url someurl}}
;; Validate (with clojure.spec) happens on the entire `param` map
"/validate" {:post #vase/validate {:name :vase-transact-fn.v1/validate-page
:spec :vase-transact-fn.test/person}}
;; Just use datomic queries
"/db" {:get #vase/query {:name :vase-transact-fn.v1/db-page
:params []
:query [:find ?e ?v
:where [?e :db/ident ?v]]}}
"/users" {:get #vase/query {:name :vase-transact-fn.v1/users-page
:params []
:query [:find ?id ?email
[?e :user/userId ?id]
[?e :user/userEmail ?email]]}}
"/users/:id" {:get #vase/query {:name :vase-transact-fn.v1/user-id-page
:params [id]
:edn-coerce [id]
:query [:find ?e
:in $ ?id
[?e :user/userId ?id]]}}
"/user" {:get #vase/query {:name :vase-transact-fn.v1/user-page
;; All params are required to perform the query
:params [email]
:query [:find ?e
:in $ ?email
[?e :user/userEmail ?email]]}
:post #vase/transact {:name :vase-transact-fn.v1/user-create
;; `:properties` are pulled from the parameters
:properties [:db/id
:delete #vase/transact {:name :vase-transact-fn.v1/user-delete
:db-op :vase/retract-entity
;; :vase/retract-entity requires :db/id to be supplied
:properties [:db/id]}}
"/jane-and-someone" {:get #vase/query {:name :vase-transact-fn.v1/fogussomeone-page
;; Params can have default values, using the "default pair" notation
:params [[someone ""]]
:constants [""]
:query [:find ?e
:in $ ?someone ?jane
[(list ?someone ?jane) [?emails ...]]
[?e :user/userEmail ?emails]]}}}
;:vase.api/interceptors [] ;; Any extra interceptors to apply to this API chunk/version
:vase.api/schemas [:vase-transact-fn/user-schema]
:vase.api/forward-headers ["vaserequest-id"]}}}}
{:vase.api/routes {"/hello" {:post [#nsd/transact-fn {:name :accounts.v1/item-create-with-function
:db-fn :my-great-fn
:properties [:item/name]}]}}}}}}
\ No newline at end of file
{nsd/transact-fn vase-transact-fn.literals/transact-fn}
\ No newline at end of file
(ns vase-transact-fn.actions
(:require [com.cognitect.vase.util :as util]
[io.pedestal.interceptor :as interceptor]
[datomic.api :as d]))
(defn dynamic-interceptor
"Build an interceptor/interceptor from a map of keys to
expressions. The expressions will be evaluated and must evaluate to
a function of 1 argument. At runtime the function will be called
with a Pedestal context map."
[name literal exprs]
{:name name}
(util/map-vals eval exprs)))
{:action-literal literal}))
(def eav (juxt :e :a :v))
(defn apply-tx
[conn tx-data args]
(->> tx-data
(d/transact conn)
(map eav))})
(defn process-fn [db-fn entity-data]
[db-fn entity-data])
(defn transact-fn-action-exprs
"Return code for a Pedestal context function that executes a
`properties` is a collection of keywords that name Datomic
attributes. When an HTTP request arrives, these keywords are matched
with their parameter values in the request to form an entity map.
`db-op` may be either :vase/assert-entity, :vase/retract-entity, or
nil. When `nil`, Vase will assume the transaction body is a
collection of Datomic entity maps.
`headers` is an expression that evaluates to a map of header
name (string) to header value (string). May be nil."
[properties db-fn headers to]
(assert (or (nil? headers) (map? headers)) (str "Headers should be a map. I got " headers))
(let [to (or to ::transact-data)]
`(fn [{~'request :request :as ~'context}]
(let [args# (mapv
#(into {} (filter second (select-keys % ~(vec properties))))
(get-in ~'request [:json-params :payload]))
tx-data# (mapv (partial process-fn ~db-fn) args#)
conn# (:conn ~'request)
response-body# (apply-tx
resp# (util/response
(util/status-code response-body# (:errors ~'context)))]
(if (empty? (:io.pedestal.interceptor.chain/queue ~'context))
(assoc ~'context :response resp#)
(-> ~'context
(assoc ~to response-body#)
(assoc-in [:request :db] (d/db conn#))))))))
(defn transact-fn-action
"Returns a Pedestal interceptor that executes a Datomic function
on an entry."
[name properties db-fn headers to]
(transact-fn-action-exprs properties db-fn headers to)
(ns vase-transact-fn.literals
(:require [io.pedestal.interceptor :as i]
[vase-transact-fn.actions :as actions]))
(defrecord TransactFnAction [name properties db-fn headers to doc]
(-interceptor [_]
(actions/transact-fn-action name properties db-fn headers to)))
(defn transact-fn [form]
{:pre [(map? form)
(:name form)
(-> form :name keyword?)]}
(map->TransactFnAction form))
......@@ -3,6 +3,7 @@
(:require [io.pedestal.http :as server]
[io.pedestal.http.route :as route]
[com.cognitect.vase :as vase]
[vase-transact-fn.literals :as literals]
[vase-transact-fn.service :as service]))
(defn activate-vase
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment