- Date:
For a couple of years I worked on a front-end project where the back-end service is a Dynamics (D365) dataverse thing. Making queries to the thing involved using OData queries.
Essentially OData calls are very long HTTP queries. They get a little bit goofy and unweildy to create. One of the most frustrating things about Dynamics is that what you see in the dashboard isn’t really representative of the actual database table names and columns. Because of this, it’s a nice to have a helpful tool to bounce queries off of Dynamics to see if you get the intended data from the back-end.
For example, a query might look like: GET https://veterinarians.ca/api/data/v9.2/cats(f1e2d3c4-b5a6-7890-1234-567890abcdef)?$select=pet_name,pet_species,pet_id&$expand=vaccinations($select=vaccination_type,vaccination_id)
This might get us a JSON response body containing:
{
"@odata.context": "https://veterinarians.ca/api/data/v9.2/$metadata#cats(pet_name,pet_species,pet_id,vaccinations(vaccination_type,vaccination_id))/$entity",
"pet_id": "f1e2d3c4-b5a6-7890-1234-567890abcdef",
"pet_name": "Mochi",
"pet_species": "Felis catus",
"vaccinations": [
{
"vaccination_id": "9a8b7c6d-1111-2222-3333-444455556666",
"vaccination_type": "Rabies"
},
{
"vaccination_id": "9a8b7c6d-7777-8888-9999-aaaabbbbcccc",
"vaccination_type": "FVRCP"
},
{
"vaccination_id": "9a8b7c6d-dddd-eeee-ffff-000011112222",
"vaccination_type": "Feline Leukemia (FeLV)"
}
]
}
I suppose I could use any number of tools to make my queries. Perhaps a tool more modern, graphical, or more “convenient” than Babashka but no! I’m going to do it with Babashka in Neovim, btw.
What is Babashka?
Straight from the horses mouth, Babashka is a “fast native Clojure scripting runtime.” Without going into too much detail, it is a Clojure runtime made with GraalVM. Due to its limitations and optomizations, Babashka becomes a scripting interpreter that starts very quickly.
This makes it a great candidate for fun scripting in a way that bash scripting isn’t as fun for. It also has a built-in nREPL for evaluating Clojure expressions!
Setting Things Up
Things you’ll need if you wanna do this yourself:
- Babashka
- An Editor that supports evaluating Lisp expressions. The two I’ve tried that work really well are Emacs and Neovim. For this tutorial I’m going with Neovim accompanied by Conjure.
- Optionally, a way to dynamically set your environment variables to keep secrets out of your commits. Something like Direnv would do.
Conjure is an amazing Neovim plugin. It will create a REPL for a wide variety of languages, including Clojure.
Open a shell at the root of your project where your environment is properly set
up, and start an nREPL session: bb nrepl-server
Inside of Neovim, from the project root, create a Clojure file. I called mine
dynamics.clj. Use the :ConjureConnect 1667 command, which may connect you to
the default Babashka nREPL port.
From there you may now evaluate Clojure expressions in many ways to get immediate feedback of the results!
For example, Let’s say my cursor is here:
(def env-vars {:dynamics-url (System/getenv "DYNAMICS_URL")
:dynamics-header (System/getenv "DYNAMICS_HEADER")
:dynamics-key (System/getenv "DYNAMICS_KEY")})<-- Cursor
I can use my local leader keybind of ,ee to evaluate the balanced expression.
With this, env-vars will be defined in the runtime and the REPL will report
-> nil as the return value. From there, I could then evaluate (:dynamics-url env-vars) and see the dynamics URL directly in the editor buffer :)
You may use :help conjure to learn about all the different ways you can
evaluate the buffer and split the output view if a pop-up window doesn’t suit
your needs.
A Practical Example
Here is some clever setup and boilerplate that I wrote to make my life a little easier:
(require '[babashka.http-client :as http])
(require '[cheshire.core :as json])
(import [java.net URLEncoder])
(def env-vars {:dynamics-url (System/getenv "DYNAMICS_URL")
:dynamics-header (System/getenv "DYNAMICS_HEADER")
:dynamics-key (System/getenv "DYNAMICS_KEY")})
(when (some empty? (vals env-vars))
(throw (Throwable. "Environment variables may be missing")))
(defn do-dynamics-get-operation [operation]
(let [url (str (:dynamics-url env-vars)
"/api/Operations?statement="
(URLEncoder/encode operation))]
(prn (str "get: " url))
(http/get url
{:headers {"Accept" "text/plain"
"Content-Type" "application/json"
(:dynamics-header env-vars) (:dynamics-key env-vars)}})))
(defn do-dynamics-patch-operation [operation body]
(let [url (str (:dynamics-url env-vars)
"/api/Operations?statement="
(URLEncoder/encode operation))]
(prn (str "patch: " url))
(http/patch url
{:headers {"Accept" "text/plain"
"Content-Type" "application/json"
(:dynamics-header env-vars) (:dynamics-key env-vars)}
:body (json/encode body)})))
(defn do-dynamics-post-operation [operation body]
(let [url (str (:dynamics-url env-vars)
"/api/Operations?statement="
(URLEncoder/encode operation))]
(prn (str "post: " url))
(http/post url
{:headers {"Accept" "text/plain"
"Content-Type" "application/json"
(:dynamics-header env-vars) (:dynamics-key env-vars)}
:body (json/encode body)})))
Note: The very useful Java interoperability of Clojure with URLEncoder!
Using these functions, I was able to do nearly any query of interest to me. I could evaluate the buffer, load all of that Clojure into memory, then start playing around:
(defn get-cat-by-id [cat-id]
(let [operation (str "cats(" cat-id ")"
"?$select=cat_id,cat_name,cat_species")]
(:body (do-dynamics-get-operation operation))))
(defn get-tabby-cats []
(let [operation (str "cats"
"?$select=pet_id,pet_name,pet_species,pet_type"
"&$filter=(pet_type eq tabby)")]
(:body (do-dynamics-get-operation operation))))
(get-cat-by-id "f1e2d3c4-b5a6-7890-1234-567890abcdef")
Evaluating the above code would get me all of the ?$select data for the
example cat at the start of this article.
So there it is! How I used Babashka at work. Let me know on Mastodon if you found this article helpful :)