Skip to content
/ oksa Public

Generate GraphQL queries using Clojure data structures.

License

Notifications You must be signed in to change notification settings

metosin/oksa

Folders and files

NameName
Last commit message
Last commit date

Latest commit

author
Ilmo Raunio
Mar 17, 2025
6aa3076 · Mar 17, 2025
Aug 5, 2024
Jun 6, 2024
Aug 8, 2024
Jul 30, 2024
Jun 7, 2024
Sep 14, 2023
Mar 17, 2025
May 16, 2023
May 2, 2023
Mar 17, 2025
May 24, 2024
Aug 4, 2024
Aug 5, 2024
May 16, 2023
May 16, 2023
Aug 5, 2024

Repository files navigation

oksa

Generate GraphQL queries using Clojure data structures.

  • Support latest stable GraphQL spec
  • Malli-like syntax or programmatic API
  • Clojure, ClojureScript, and babashka

Project status

Clojars Project Slack cljdoc badge bb compatible

Oksa is currently experimental.

Usage

(require '[oksa.core :as o])
(require '[oksa.alpha.api :as oa])

(o/gql [:hello [:world]])
;; => "{hello{world}}"

;; programmatic
(o/gql (oa/select :hello (oa/select :world)))
;; => "{hello{world}}"

Operation definitions

Selection sets

Fields can be selected:

(o/gql [:foo])
;; => "{foo}"

(o/gql (oa/select :foo))
;; => "{foo}"
(o/gql [:foo :bar])
;; => "{foo bar}"

(o/gql (oa/select :foo :bar))
;; => "{foo bar}"
(o/gql [:bar [:qux [:baz]]])
;; => "{bar{qux{baz}}}"

(o/gql (oa/select :bar
         (oa/select :qux
           (oa/select :baz))))
;; => "{bar{qux{baz}}}"
(o/gql [:foo :bar [:qux [:baz]]])
;; => "{foo bar{qux{baz}}}"

(o/gql (oa/select :foo :bar
         (oa/select :qux
           (oa/select :baz))))
;; => "{foo bar{qux{baz}}}"
(o/gql [:foo :bar [:qux :baz]])
;; => "{foo bar{qux baz}}"

(o/gql (oa/select :foo :bar
         (oa/select :qux :baz)))
;; => "{foo bar{qux baz}}"
(o/gql [:foo [:bar [:baz :qux] :frob]])
;; => "{foo{bar{baz qux} frob}}"

(o/gql (oa/select :foo
         (oa/select :bar
           (oa/select :baz :qux)
           :frob)))
;; => "{foo{bar{baz qux} frob}}"

Strings are supported for field names:

(o/gql ["query" "foo"])
;; => "{query foo}"

(o/gql (oa/select "query" "foo"))
;; => "{query foo}"

Aliases:

(o/gql [[:foo {:alias :bar}]])
;; => "{bar:foo}"

(o/gql (oa/select (oa/field :foo (oa/opts (oa/alias :bar)))))
;; => "{bar:foo}"

Arguments:

(o/gql [[:foo {:arguments {:a 1
                           :b "hello world"
                           :c true
                           :d nil
                           :e :foo
                           :f [1 2 3]
                           :g {:frob {:foo 1
                                      :bar 2}}
                           :h :$fooVar}}]])
;; => "{foo(a:1, b:\"hello world\", c:true, d:null, e:foo, f:[1 2 3], g:{frob:{foo:1, bar:2}}, h:$fooVar)}"

;; alt
(o/gql
  (oa/select
    (oa/field :foo
      (oa/opts (oa/arguments :a 1
                             :b "hello world"
                             :c true
                             :d nil
                             :e :foo
                             :f [1 2 3]
                             :g {:frob {:foo 1
                                        :bar 2}}
                             :h :$fooVar)))))
;; => "{foo(a:1, b:\"hello world\", c:true, d:null, e:foo, f:[1 2 3], g:{frob:{foo:1, bar:2}}, h:$fooVar)}"

Directives:

(o/gql [[:foo {:directives [:bar]}]])
;; => "{foo@bar}"

(o/gql (oa/select (oa/field :foo (oa/opts (oa/directive :bar)))))
;; => "{foo@bar}"

Directive arguments:

(o/gql [[:foo {:directives [[:bar {:arguments {:qux 123}}]]}]])
;; => "{foo@bar(qux:123)}"

(o/gql (oa/select (oa/field :foo (oa/opts (oa/directive :bar (oa/arguments :qux 123))))))
;; => "{foo@bar(qux:123)}"

Queries

(o/gql [:oksa/query [:foo :bar [:qux [:baz]]]])
;; => "query {foo bar{qux{baz}}}"

(o/gql (oa/query
         (oa/select :foo :bar
           (oa/select :qux
             (oa/select :baz)))))
;; => "query {foo bar{qux{baz}}}"

Query names:

(o/gql [:oksa/query {:name :Foo} [:foo]])
;; => "query Foo {foo}"

(o/gql (oa/query (oa/opts (oa/name :Foo))
         (oa/select :foo)))
;; => "query Foo {foo}"

Query directives:

(o/gql [:oksa/query {:directives [:foo]} [:foo]])
;; => "query @foo{foo}"

(o/gql (oa/query (oa/opts (oa/directive :foo))
         (oa/select :foo)))
;; => "query @foo{foo}"
(o/gql [:oksa/query {:directives [:foo :bar]} [:foo]])
;; => "query @foo @bar{foo}"

(o/gql (oa/query (oa/opts (oa/directives :foo :bar))
         (oa/select :foo)))
;; => "query @foo @bar{foo}"

Query directive arguments:

(o/gql [:oksa/query {:directives [[:foo {:arguments {:bar 123}}]]} [:foo]])
;; => "query @foo(bar:123){foo}"

(o/gql (oa/query (oa/opts (oa/directive :foo (oa/arguments :bar 123)))
         (oa/select :foo)))
;; => "query @foo(bar:123){foo}"

Mutations

(o/gql [:oksa/mutation [:foo :bar [:qux [:baz]]]])
;; => "mutation {foo bar{qux{baz}}}"

(o/gql (oa/mutation (oa/select :foo :bar
                      (oa/select :qux
                        (oa/select :baz)))))
;; => "mutation {foo bar{qux{baz}}}"
(o/gql [:oksa/mutation {:name :Foo} [:foo]])
;; => "mutation Foo {foo}"

(o/gql (oa/mutation (oa/opts (oa/name :Foo))
         (oa/select :foo)))
;; => "mutation Foo {foo}"

Subscriptions

(o/gql [:oksa/subscription [:foo :bar [:qux [:baz]]]])
;; => "subscription {foo bar{qux{baz}}}"

(o/gql (oa/subscription (oa/select :foo :bar
                          (oa/select :qux
                            (oa/select :baz)))))
;; => "subscription {foo bar{qux{baz}}}"
(o/gql [:oksa/subscription {:name :Foo} [:foo]])
;; => "subscription Foo {foo}"

(o/gql (oa/subscription (oa/opts (oa/name :Foo))
         (oa/select :foo)))
;; => "subscription Foo {foo}"

Variable definitions

Named types are supported:

(o/gql [:oksa/query {:variables [:fooVar :FooType]} [:fooField]])
;; => "query ($fooVar:FooType){fooField}"

(o/gql (oa/query (oa/opts (oa/variables :fooVar :FooType))
         (oa/select :fooField)))
;; => "query ($fooVar:FooType){fooField}"
(o/gql [:oksa/query {:variables [:fooVar :FooType :barVar :BarType]} [:fooField]])
;; => "query ($fooVar:FooType,$barVar:BarType){fooField}"

(o/gql (oa/query (oa/opts (oa/variables :fooVar :FooType :barVar :BarType))
         (oa/select :fooField)))
;; => "query ($fooVar:FooType,$barVar:BarType){fooField}"

Lists can be created:

(o/gql [:oksa/query {:variables [:fooVar [:oksa/list :FooType]]} [:fooField]])
;; => "query ($fooVar:[FooType]){fooField}"

;; alt
(o/gql [:oksa/query {:variables [:fooVar [:FooType]]} [:fooField]])
;; => "query ($fooVar:[FooType]){fooField}"

;; programmatic
(o/gql (oa/query (oa/opts (oa/variable :fooVar (oa/list :FooType)))
         (oa/select :fooField)))
;; => "query ($fooVar:[FooType]){fooField}"
(o/gql [:oksa/query {:variables [:fooVar [:oksa/list [:oksa/list :BarType]]]} [:fooField]])
;; => "query ($fooVar:[[BarType]]){fooField}"

;; alt
(o/gql [:oksa/query {:variables [:fooVar [[:BarType]]]} [:fooField]])
;; => "query ($fooVar:[[BarType]]){fooField}"

;; programmatic
(o/gql (oa/query (oa/opts (oa/variable :fooVar (oa/list (oa/list :BarType))))
         (oa/select :fooField)))
;; => "query ($fooVar:[[BarType]]){fooField}"

Non-null types can be created:

(o/gql [:oksa/query {:variables [:fooVar [:FooType {:non-null true}]]} [:fooField]])
;; => "query ($fooVar:FooType!){fooField}"

;; alt
(o/gql [:oksa/query {:variables [:fooVar :FooType!]} [:fooField]])
;; => "query ($fooVar:FooType!){fooField}"

;; programmatic
(o/gql (oa/query (oa/opts (oa/variable :fooVar (oa/type! :FooType)))
         (oa/select :fooField)))
;; => "query ($fooVar:FooType!){fooField}"
(o/gql [:oksa/query {:variables [:fooVar [:oksa/list {:non-null true} :BarType]]} [:fooField]])
;; => "query ($fooVar:[BarType]!){fooField}"

;; alt
(o/gql [:oksa/query {:variables [:fooVar [:! :BarType]]} [:fooField]])
;; => "query ($fooVar:[BarType]!){fooField}"

;; programmatic
(o/gql (oa/query (oa/opts (oa/variable :fooVar (oa/list! :BarType)))
         (oa/select :fooField)))
;; => "query ($fooVar:[BarType]!){fooField}"

Getting crazy with it:

(o/gql [:oksa/query {:variables [:fooVar [:! [:! :BarType!]]]} [:fooField]])
;; => "query ($fooVar:[[BarType!]!]!){fooField}"

(o/gql (oa/query (oa/opts (oa/variable :fooVar (oa/list! (oa/list! (oa/type! :BarType)))))
         (oa/select :fooField)))
;; => "query ($fooVar:[[BarType!]!]!){fooField}"

Variable definitions can have directives:

(o/gql [:oksa/query {:variables [:foo {:directives [:fooDirective]} :Bar]} [:fooField]])
;; => "query ($foo:Bar @fooDirective){fooField}"

(o/gql (oa/query (oa/opts (oa/variable :foo (oa/opts (oa/directive :fooDirective)) :Bar))
                 (oa/select :fooField)))
;; => "query ($foo:Bar @fooDirective){fooField}"
(o/gql [:oksa/query {:variables [:foo {:directives [[:fooDirective {:arguments {:fooArg 123}}]]} :Bar]} [:fooField]])
;; => "query ($foo:Bar @fooDirective(fooArg:123)){fooField}"

(o/gql (oa/query (oa/opts (oa/variable :foo (oa/opts (oa/directive :fooDirective (oa/argument :fooArg 123))) :Bar))
                 (oa/select :fooField)))
;; => "query ($foo:Bar @fooDirective(fooArg:123)){fooField}"

Fragments

Fragment definitions can be created:

(o/gql [:oksa/fragment {:name :Foo :on :Bar} [:foo]])
;; => "fragment Foo on Bar{foo}"

;; alt
(o/gql [:# {:name :Foo :on :Bar} [:foo]])
;; => "fragment Foo on Bar{foo}"

;; programmatic
(o/gql (oa/fragment (oa/opts
                      (oa/name :Foo)
                      (oa/on :Bar))
         (oa/select :foo)))
;; => "fragment Foo on Bar{foo}"

Fragment directives:

(o/gql [:oksa/fragment {:name :foo
                        :on :Foo
                        :directives [:fooDirective]}
           [:bar]])
;; => "fragment foo on Foo@fooDirective{bar}"

;; alt
(o/gql (oa/fragment (oa/opts
                      (oa/name :foo)
                      (oa/on :Foo)
                      (oa/directive :fooDirective))
         (oa/select :bar)))
;; => "fragment foo on Foo@fooDirective{bar}"

Fragment directive arguments:

(o/gql [:oksa/fragment {:name :foo
                        :on :Foo
                        :directives [[:fooDirective {:arguments {:bar 123}}]]} [:bar]])
;; => "fragment foo on Foo@fooDirective(bar:123){bar}"

;; alt
(o/gql (oa/fragment (oa/opts
                      (oa/name :foo)
                      (oa/on :Foo)
                      (oa/directive :fooDirective (oa/argument :bar 123)))
         (oa/select :bar)))
;; => "fragment foo on Foo@fooDirective(bar:123){bar}"

Fragment spreads:

(o/gql [:foo [:oksa/fragment-spread {:name :bar}]])
;; => "{foo ...bar}"

(o/gql [:foo [:... {:name :bar}]])
;; => "{foo ...bar}"

(o/gql (oa/select :foo (oa/fragment-spread (oa/opts (oa/name :bar)))))
;; => "{foo ...bar}"

Fragment spread directives:

(o/gql [[:... {:name :foo :directives [:bar]}]])
;; => "{...foo@bar}"

(o/gql (oa/select (oa/fragment-spread (oa/opts (oa/name :foo) (oa/directive :bar)))))
;; => "{...foo@bar}"

Fragment spread directive arguments:

(o/gql [[:... {:name :foo :directives [[:bar {:arguments {:qux 123}}]]}]])
;; => "{...foo@bar(qux:123)}"

(o/gql (oa/select (oa/fragment-spread (oa/opts (oa/name :foo) (oa/directive :bar (oa/arguments :qux 123))))))
;; => "{...foo@bar(qux:123)}"

Inline fragments:

(o/gql [:foo [:oksa/inline-fragment [:bar]]])
;; => "{foo ...{bar}}"

;; alt
(o/gql [:foo [:... [:bar]]])
;; => "{foo ...{bar}}"

;; programmatic
(o/gql (oa/select :foo (oa/inline-fragment (oa/select :bar))))
;; => "{foo ...{bar}}"

Type condition is supported:

(o/gql [:foo [:... {:on :Bar} [:bar]]])
;; => "{foo ...on Bar{bar}}"

(o/gql (oa/select :foo (oa/inline-fragment (oa/opts (oa/on :Bar))
                         (oa/select :bar))))
;; => "{foo ...on Bar{bar}}"

Inline fragment directives:

(o/gql [[:... {:directives [:foo]} [:bar]]])
;; => "{...@foo{bar}}"

(o/gql (oa/select (oa/inline-fragment (oa/opts (oa/directive :foo))
                    (oa/select :bar))))
;; => "{...@foo{bar}}"

Inline fragment directive arguments:

(o/gql [[:... {:directives [[:foo {:arguments {:bar 123}}]]} [:foobar]]])
;; => "{...@foo(bar:123){foobar}}"

(o/gql (oa/select (oa/inline-fragment (oa/opts (oa/directive :foo (oa/argument :bar 123)))
                    (oa/select :foobar))))
;; => "{...@foo(bar:123){foobar}}"

Document

Putting it all together:

(o/gql [:oksa/document
           [:foo]
           [:oksa/query [:bar]]
           [:oksa/mutation [:qux]]
           [:oksa/subscription [:baz]]
           [:oksa/fragment {:name :foo :on :Foo} [:bar]]])
;; => "{foo}\nquery {bar}\nmutation {qux}\nsubscription {baz}\nfragment foo on Foo{bar}"

;; alt
(o/gql
  (oa/document
    (oa/select :foo)
    (oa/query (oa/select :bar))
    (oa/mutation (oa/select :qux))
    (oa/subscription (oa/select :baz))
    (oa/fragment (oa/opts
                (oa/name :foo)
                (oa/on :Foo))
      (oa/select :bar))))
;; => "{foo}\nquery {bar}\nmutation {qux}\nsubscription {baz}\nfragment foo on Foo{bar}"

(o/gql [:<> [:foo] [:bar]]) ; :<> also supported
;; => "{foo}\n{bar}"

More complete example using oksa.alpha.api:

(o/gql
  (oa/document
    (oa/fragment (oa/opts
                   (oa/name :Kikka)
                   (oa/on :Kukka))
      (oa/select :kikka :kukka))
    (oa/select :ylakikka
      (oa/select :kikka :kukka :kakku)
      ;; v similar to ^, but allow to specify option for single field (only)
      (oa/field :kikka (oa/opts
                         (oa/alias :KIKKA)
                         (oa/directives :kakku :kukka)
                         (oa/directive :kikkaized {:x 1 :y 2 :z 3})))
      (when false (oa/field :conditionalKikka)) ; nils are dropped
      (oa/fragment-spread (oa/opts (oa/name :FooFragment)))
      (oa/inline-fragment (oa/opts (oa/on :Kikka))
        (oa/select :kikka :kukka)))
    (oa/query (oa/opts (oa/name :KikkaQuery))
      (oa/select :specialKikka))
    (oa/mutation (oa/opts
                   (oa/name :saveKikka)
                   (oa/variable :myKikka (oa/opts (oa/default 123)) :KikkaType))
      (oa/select :getKikka))
    (oa/subscription (oa/opts (oa/name :subscribeToKikka))
      (oa/select :realtimeKikka))))
;; => "fragment Kikka on Kukka{kikka kukka}\n{ylakikka{kikka kukka kakku} KIKKA:kikka@kakku @kukka @kikkaized(x:1, y:2, z:3) ...FooFragment ...on Kikka{kikka kukka}}\nquery KikkaQuery {specialKikka}\nmutation saveKikka ($myKikka:KikkaType=123){getKikka}\nsubscription subscribeToKikka {realtimeKikka}"

Name transformation

Oksa supports name transformation:

(require '[camel-snake-kebab.core :as csk])

(o/gql*
  {:oksa/name-fn csk/->camelCase}
  [[:foo-bar {:alias :bar-foo
              :directives [:foo-bar]
              :arguments {:foo-arg :bar-value}}
    [:foo-bar]]
   :naked-foo-bar
   [:...
    [:foo-bar]]
   [:... {:on :foo-bar-fragment
          :directives [:foo-bar]}
    [:foo-bar]]])
;; => "{barFoo:fooBar(fooArg:barValue)@fooBar{fooBar} nakedFooBar ...{fooBar} ...on fooBarFragment@fooBar{fooBar}}"

Field transformation is supported:

(o/gql*
  {:oksa/field-fn csk/->camelCase}
  [:foo-bar
   [:foo-bar]
   :naked-foo-bar
   [:...
    [:foo-bar]]
   [:... {:on :SomeType}
    [:foo-bar]]])
;; => "{fooBar{fooBar} nakedFooBar ...{fooBar} ...on SomeType{fooBar}}"

Directives can also be transformed:

(o/gql*
  {:oksa/directive-fn csk/->snake_case}
  [[:foo {:directives [:some-thing]}]])
;; => "{foo@some_thing}"

You can also override using enums or types:

(o/gql*
  {:oksa/name-fn csk/->camelCase
   :oksa/enum-fn csk/->SCREAMING_SNAKE_CASE
   :oksa/type-fn csk/->PascalCase}
  [:oksa/query {:variables [:foo-var {:default :foo-value} :foo-type]}
   [:foobar]])
;; => "query ($fooVar:FooType=FOO_VALUE){foobar}"

Local overriding also supported on fields:

(o/gql*
  {:oksa/name-fn csk/->camelCase}
  [[:screaming-field {:oksa/field-fn csk/->SCREAMING_SNAKE_CASE}]
   :talking-field])
;; => "{SCREAMING_FIELD talkingField}"

An example using a custom transformer to preserve the namespace as part of a field:

(defn custom-name [key]
  (str (when (namespace key)
         (str (namespace key) "_"))
       (name key)))

(o/gql* {:oksa/field-fn custom-name} [:employee [:user/name :user/address]])
;; => "{employee{user_name user_address}}"

Rationale

There are some awesome GraphQL query generation libraries out there, notably:

With oksa we want to provide:

  • A platform-agnostic library meant for purely building GraphQL queries.
  • Support for the entire syntax under ExecutableDefinition plus some parts from Document for added composability of queries.
  • A data-driven library with a malli-like syntax.