Pen or sword - the shield is mightiest - Leona
A toolbox designed to make working with GraphQL and clojure.spec a more pleasant experience.
Leona can build Lacinia schema just by telling it the queries and mutations you want to make. You can add resolvers for specific fields and add middleware inside the executor.
PRs are welcome, and we often advertise issues (including rewards) over at WorksHub. If you're interested and want some direction, come and chat with us at #workshub on Clojurians Slack.
Major changes will be documented in the changelog
BREAKING CHANGES IN 0.2.x
(require '[leona.core :as leona])
(let [schema (-> (leona/create)
(leona/attach-query ::query-spec ::object query-resolver-fn)
(leona/attach-mutation ::mutation-spec ::object mutator-fn)
(leona/attach-field-resolver ::field-in-object field-resolver-fn)
(leona/attach-middleware middeware-fn)
(leona/compile))]
(leona/execute schema "query { myObject(id: 1001) { id, name, fieldInObject }}")
To add a query to the schema use attach-query
:
(-> (leona/create)
(leona/attach-query ::query-spec ::object query-resolver-fn))
::query-spec
is a spec for the GraphQL query, ::object
is the spec for the returned data, and query-resolver-fn
is the resolver function that will fetch and return the data.
Alternatively, your query-resolver-fn
has :leona/query-spec
and :leona/results-spec
metadata:
(defn
^{:leona/query-spec ::query-spec
:leona/results-spec ::object}
query-resolver-fn [])
(-> (leona/create)
(leona/attach-query query-resolver-fn))
Mutations are very similar to queries. To add a mutation to the schema use attach-mutation
:
(-> (leona/create)
(leona/attach-mutation ::mutation-spec ::object mutator-fn))
::mutation-spec
is a spec for the GraphQL mutation, ::object
is the spec for the returned data, and mutator-fn
is the function that will mutate the existing data and return the new, mutated data.
Alternatively, your mutator-fn
has :leona/mutation-spec
and :leona/results-spec
metadata:
(defn
^{:leona/mutation-spec ::mutation-spec
:leona/results-spec ::object}
mutator-fn [])
(-> (leona/create)
(leona/attach-query mutator-fn))
To provide a resolver for a specific field, use attach-field-resolver
:
(-> (leona/create)
(leona/attach-query ::query-spec ::object query-resolver-fn)
(leona/attach-field-resolver ::field-in-object field-resolver-fn)
::field-in-object
is a spec for the field in an existing object. It must match a field already being inserted, in either a query or mutation. If the field isn't found amongst the objects in the schema then it won't be inserted. field-resolver-fn
is a resolver fn for that specific field. As is true of all field resolvers, it will be called after the root query/mutation resolver, so the value
arg will already have data in it. The field resolver should add to this value.
It might be useful to add middleware inside the Lacinia executor e.g. you want to inspect a query/mutation prior to resolving or you want to inspect a value before it's passed back to Lacinia.
(-> (leona/create)
(leona/attach-middleware middeware-fn-1)
(leona/attach-middleware middeware-fn-2))
Middleware functions are applied in the order that they are attached and take 4 args:
(defn middleware-fn-1
[handler ctx query value]
;; do something
(handler))
(defn middleware-fn-2
[handler ctx query value]
(let [result (handler)]
;; do something
result)
Middleware should call (handler)
if they intend to allow the process to continue. Currently the ctx
, query
and value
args should not be passed into handler
as they cannot be overridden by the middleware.
In order to use the middleware you'll need to use leona's execute
fn:
(leona/execute compiled execute-string)
compiled
is the output of (leona/compile)
and execute-string
is a GraphQL query/mutation/etc.
Custom scalars are also supported.
(-> (leona/create)
...
(leona/attach-custom-scalar ::date {:parse #(tf/parse (tf/formatters :date-time) %)
:serialize #(tf/unparse (tf/formatters :date-time) %)}))
Anywhere the ::date
spec is referenced by an object, query or mutation, the parse
and serialize
fns will be used to transform the data. Be aware, however; whilst Leona will still perform its own internal spec validation, Lacinia will not perform validation for over-the-wire values. It's therefore important that your parse
fn can handle incorrect data (unlike the one in the example!) and will still return something valid to Leona.
If your spec cannot be inferred (automatically converted into an accurate schema) you can always override the inferred type by using the spec
function from spec-tools
:
(require '[spec-tools.core :as st])
(s/def ::object (st/spec object? {:type '(non-null String})) ;; always use `non-null`; Leona will remove it if the field is optional
If you'd like to add a description to the schema you can also use the spec
function:
(s/def ::object (st/spec string? {:description "This is my object}))
Sometimes, you may want to add a custom object that’s not referred to in any of your queries, mutations or field resolvers (e.g., if you want to refer to it from an external schema attached via attach-schema
). For this use case, Leona provides attach-object
:
(-> (leona/create)
...
(leona/attach-object :some/object :input? true))
If you pass :input?
, as in the example above, Leona will generate an input object (named objectInput
) in addition to an ordinary object.
If you're working with a large amount of legacy specs, sometimes you can have name clashes that aren't easy to resolve. To help with this you can use 'type aliases' which will automatically replace instances of type names wherever they are used.
(-> (leona/create)
...
(leona/attach-type-alias :my.ns/type :myType)
In this example, if my.ns/type
is an object, the corresponding object would be created as :myType
instead, and any references to my.ns/type
would automatically be updated to use the alias instead. Note, this doesn't refer the field names, just the types.
- The ordering of
attach-*
fns does not matter, other than for middleware. - By default, the casing used in the generated schema adheres to generally accepted GraphQL convention:
PascalCase
for object names,camelCase
for fields, queries and mutations, andSCREAMING_SNAKE_CASE
for enum values.
Copyright © 2020 WorksHub
Distributed under the Eclipse Public License either version 1.0 or (at your option) any later version.