├── .gitignore ├── LICENSE ├── README.md ├── deps.edn ├── doc └── intro.md ├── project.clj ├── src └── graphql_builder │ ├── core.cljc │ ├── generators │ ├── composed_mutation.cljc │ ├── composed_query.cljc │ ├── field.cljc │ ├── fragment.cljc │ ├── fragment_spread.cljc │ ├── inline_fragment.cljc │ ├── operation.cljc │ └── shared.cljc │ ├── parser.clj │ ├── parser.cljs │ └── util.cljc └── test └── graphql_builder ├── core_test.clj └── resources ├── 1.graphql ├── 2.graphql ├── parsed ├── .gitkeep └── statements.edn └── statements.edn /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | pom.xml 5 | pom.xml.asc 6 | *.jar 7 | *.class 8 | /.lein-* 9 | /.nrepl-port 10 | .hgignore 11 | .hg/ 12 | *.swp 13 | /.clj-kondo 14 | /.cpcache -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017 Mihael Konjevic (konjevic@gmail.com) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # graphql-builder 2 | 3 | [![Clojars Project](https://img.shields.io/clojars/v/floatingpointio/graphql-builder.svg)](https://clojars.org/floatingpointio/graphql-builder) 4 | 5 | GraphQL client library for Clojure and ClojureScript. 6 | 7 | ## Why 8 | 9 | Writing GraphQL queries in the frontend applications is not straight forward. In JavaScript world it is common to see GraphQL queries written as inline strings inside the application code: 10 | 11 | ```javascript 12 | client.query(` 13 | { 14 | allFilms { 15 | films { 16 | title 17 | } 18 | } 19 | } 20 | `).then(result => { 21 | console.log(result.allFilms); 22 | }); 23 | ``` 24 | 25 | Although it gets the work done, it is easy to make mistakes without syntax coloring, and any validation of the query syntax is impossible. In ClojureScript this approach looks even worse: 26 | 27 | ```clojure 28 | (def inline-fragment-source " 29 | query LoadStarships($starshipCount: Int!) { 30 | allStarships(first: $starshipCount) { 31 | edges { 32 | node { 33 | id 34 | name 35 | model 36 | costInCredits 37 | pilotConnection { 38 | edges { 39 | node { 40 | ...pilotFragment 41 | } 42 | } 43 | } 44 | } 45 | } 46 | } 47 | } 48 | fragment pilotFragment on Person { 49 | name 50 | homeworld { name } 51 | } 52 | ") 53 | ``` 54 | 55 | I wanted something similar to the [HugSQL](https://github.com/layerware/hugsql) library which would allow me to keep the queries inside the `.graphql` files while being able to easily use them from my frontend code. 56 | 57 | ## Approach 58 | 59 | This library uses the parser from the [alumbra](https://github.com/alumbra/alumbra.parser) library to parse the `.graphql` files and then implements the GraphQL code generation on top of the output format. 60 | 61 | Parsing and regenerating allows for some (automatic) advanced features: 62 | 63 | - Resolving dependencies between queries and fragments 64 | - Fragment inlining 65 | - Query namespacing (with prefixes) 66 | - Query composition - combine multiple queries into one query 67 | - Mutation composition – combine multiple mutations into one query 68 | - Subscriptions 69 | 70 | ## API 71 | 72 | Loading GraphQL files: 73 | 74 | ```clojure 75 | (ns graphql-test 76 | (:require 77 | [graphql-builder.parser :refer-macros [defgraphql]] 78 | [graphql-builder.core :as core])) 79 | 80 | (defgraphql graphql-queries "file1.graphql" "file2.graphql") 81 | (def query-map (core/query-map graphql-queries)) 82 | ``` 83 | 84 | If the GraphQL file contained the following: 85 | 86 | ``` 87 | query LoadStarships($starshipCount: Int!) { 88 | allStarships(first: $starshipCount) { 89 | edges { 90 | node { 91 | id 92 | name 93 | model 94 | costInCredits 95 | pilotConnection { 96 | edges { 97 | node { 98 | ...pilotFragment 99 | } 100 | } 101 | } 102 | } 103 | } 104 | } 105 | } 106 | fragment pilotFragment on Person { 107 | name 108 | homeworld { 109 | name 110 | } 111 | } 112 | ``` 113 | 114 | you could access the `LoadStarships` function like this: 115 | 116 | ```clojure 117 | (def load-starships-query (get-in query-map [:query :load-starships])) 118 | ``` 119 | 120 | The returned function accepts one argument: query variables (if needed). Calling the function will return the following: 121 | 122 | ```clojure 123 | (load-starships-query {}) 124 | 125 | ;; return value from the load-starships-query function 126 | {:graphql {:query "GraphQL Query string" 127 | :variables {...} ;; variables passed to the load-starships-query function 128 | :operationName "..." ;; Name of the query 129 | } 130 | :unpack (fn [data])} ;; function used to unpack the data returned from the GraphQL query 131 | ``` 132 | 133 | The returned GraphQL Query will contain all of the referenced fragments. 134 | 135 | Calling the GraphQL API is out of the scope of this library, but it can be easily implemented with any of the ClojureScript AJAX Libraries. 136 | 137 | ### Fragment Inlining 138 | 139 | graphql-builder can inline the referenced fragments inside the query. To inline the fragments, pass the `{:inline-fragments true}` config to the `query-map` function: 140 | 141 | ```clojure 142 | (ns graphql-test 143 | (:require 144 | [graphql-builder.parser :refer-macros [defgraphql]] 145 | [graphql-builder.core :as core])) 146 | 147 | (defgraphql graphql-queries "file1.graphql" "file2.graphql") 148 | (def query-map (core/query-map graphql-queries {:inline-fragments true})) 149 | ``` 150 | 151 | If you called the `load-starships-query` function again, the returned GraphQL string would look like this: 152 | 153 | ``` 154 | query LoadStarships($starshipCount: Int!) { 155 | allStarships(first: $starshipCount) { 156 | edges { 157 | node { 158 | id 159 | name 160 | model 161 | costInCredits 162 | pilotConnection { 163 | edges { 164 | node { 165 | name 166 | homeworld { 167 | name 168 | } 169 | } 170 | } 171 | } 172 | } 173 | } 174 | } 175 | } 176 | ``` 177 | 178 | ### Query prefixing (namespacing) 179 | 180 | grapqhl-builder can "namespace" the GraphQL query. To namespace the query, pass the `{:prefix "NameSpace"}` config to the `query-map` function: 181 | 182 | ```clojure 183 | (ns graphql-test 184 | (:require 185 | [graphql-builder.parser :refer-macros [defgraphql]] 186 | [graphql-builder.core :as core])) 187 | 188 | (defgraphql graphq-queries "file1.graphql" "file2.graphql") 189 | (def query-map (core/query-map graphql-queries {:prefix "NameSpace"})) 190 | ``` 191 | 192 | If you called the `load-starships-query` function again, the returned GraphQL string would look like this: 193 | 194 | ``` 195 | query LoadStarships($NameSpace__starshipCount: Int!) { 196 | NameSpace__allStarships: allStarships(first: $NameSpace__starshipCount) { 197 | edges { 198 | node { 199 | id 200 | name 201 | model 202 | costInCredits 203 | pilotConnection { 204 | edges { 205 | node { 206 | ...pilotFragment 207 | } 208 | } 209 | } 210 | } 211 | } 212 | } 213 | } 214 | ``` 215 | 216 | If the referenced fragments use variables, you **must** inline them to get the correct behavior. 217 | 218 | ### Query Composition 219 | 220 | Fragment inlining and namespacing are cool features on their own, but together they unlock the possibility to compose the queries. 221 | 222 | Let's say that you have GraphQL file that contains the following query: 223 | 224 | ``` 225 | query Hero($episode: String!) { 226 | hero(episode: $episode) { 227 | name 228 | } 229 | } 230 | ``` 231 | 232 | and you want to call the query for multiple episodes. Usually you would create another query for this: 233 | 234 | ``` 235 | query { 236 | empireHero: hero(episode: EMPIRE) { 237 | name 238 | } 239 | jediHero: hero(episode: JEDI) { 240 | name 241 | } 242 | } 243 | ``` 244 | 245 | but, with graphql-builder you can compose this query from the application code: 246 | 247 | 248 | ```clojure 249 | (def composed-query 250 | (core/composed-query graphql-queries {:jedi-hero "Hero" :empire-hero "Hero"})) 251 | ``` 252 | 253 | Now you can call this function and it will handle namespacing both of the query and the variables automatically: 254 | 255 | ```clojure 256 | (composed-query {:empire-hero {:episode "EMPIRE"}} {:jedi-hero {:episode "JEDI"}}) 257 | ``` 258 | 259 | This function will return the same object like the functions created by the `query-map`: 260 | 261 | ```clojure 262 | ;; return value from the load-starships-query function 263 | {:graphql {:query "GraphQL Query string" 264 | :variables {...} ;; variables passed to the load-starships-query function 265 | :operationName "..." ;; Name of the query 266 | } 267 | :unpack (fn [data])} ;; function used to unpack the data returned from the GraphQL query 268 | ``` 269 | 270 | In this case the GraphQL query string will look like this: 271 | 272 | ``` 273 | query ComposedQuery($JediHero__episode: String!, $EmpireHero__episode: String!) { 274 | JediHero__hero: hero(episode: $JediHero__episode) { 275 | name 276 | } 277 | EmpireHero__hero: hero(episode: $EmpireHero__episode) { 278 | name 279 | } 280 | } 281 | ``` 282 | 283 | When you receive the result, you can use the returned `unpack` function to unpack them. 284 | 285 | ```clojure 286 | (unpack {"EmpireHero__hero" {:name "Foo"} "JediHero__hero" {:name "Bar"}}) 287 | 288 | ;; This will return the unpacked results: 289 | 290 | {:empire-hero {"hero" "Foo"} 291 | :jedi-hero {"hero" "Bar"}} 292 | ``` 293 | 294 | 295 | ### Mutation Composition 296 | 297 | You can also compose mutations in the same manner you can compose queries. The only 298 | difference is that the mutations might depend on each other, so the ordering of those 299 | mutations might be relevant. 300 | 301 | This can be achieved by providing mutation keys that are sorted by the `sort` method in 302 | Clojure. 303 | 304 | Assuming you have a mutation 305 | 306 | ```javascript 307 | mutation AddStarship($name: String!){ 308 | addStarship(name: $name){ 309 | id 310 | } 311 | } 312 | ``` 313 | 314 | You can compose multiple mutations together using the `composed-mutation` function: 315 | 316 | ```clojure 317 | (def composed-mutation 318 | (core/composed-mutation graphql-queries {:add-starship-1 "AddStarship" 319 | :add-starship-2 "AddStarship"})) 320 | ``` 321 | 322 | When you execute the result, you get back the same structure as with composed queries, 323 | providing `unpack` function to parse the result from the server. 324 | 325 | ```clojure 326 | (let [{unpack :unpack} (composed-mutation)] 327 | (unpack {"AddStarship1__name" "starship-1" 328 | "AddStarship2__name" "starship-2"}}) 329 | ``` 330 | 331 | returns 332 | 333 | ```clojure 334 | {:add-starship-1 {"name" "starship-1"} 335 | :add-starship-2 {"name" "starship-2"}} 336 | ``` 337 | 338 | [Tests](https://github.com/retro/graphql-builder/blob/master/test/graphql_builder/core_test.clj) 339 | 340 | ## License 341 | 342 | Copyright Mihael Konjevic, Tibor Kranjcec (konjevic@gmail.com) © 2020 343 | 344 | Distributed under the MIT license. 345 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src"] 2 | :deps {org.clojure/clojure {:mvn/version "1.9.0"} 3 | alumbra/parser {:mvn/version "0.1.7"} 4 | camel-snake-kebab/camel-snake-kebab {:mvn/version "0.4.1"}}} 5 | -------------------------------------------------------------------------------- /doc/intro.md: -------------------------------------------------------------------------------- 1 | # Introduction to graphql-builder 2 | 3 | TODO: write [great documentation](http://jacobian.org/writing/what-to-write/) 4 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject floatingpointio/graphql-builder "0.1.15" 2 | :description "A Clojure(Script) library designed to help with the consuming of GraphQL APIs." 3 | :url "https://github.com/retro/graphql-builder" 4 | :license {:name "MIT" 5 | :url "https://opensource.org/licenses/MIT"} 6 | :dependencies [[org.clojure/clojure "1.9.0"] 7 | [alumbra/parser "0.1.7"] 8 | [camel-snake-kebab "0.4.0"]]) 9 | -------------------------------------------------------------------------------- /src/graphql_builder/core.cljc: -------------------------------------------------------------------------------- 1 | (ns graphql-builder.core 2 | (:require [graphql-builder.util :as util] 3 | [graphql-builder.generators.operation :as operation] 4 | [graphql-builder.generators.field :as field] 5 | [graphql-builder.generators.fragment-spread :as fragment-spread] 6 | [graphql-builder.generators.fragment :as fragment] 7 | [graphql-builder.generators.inline-fragment :as inline-fragment] 8 | [graphql-builder.generators.composed-query :as composed-query] 9 | [graphql-builder.generators.composed-mutation :as composed-mutation] 10 | [camel-snake-kebab.core :refer [->kebab-case]] 11 | [clojure.string :as str])) 12 | 13 | (def node-type->key 14 | {:operation-definition :operation 15 | :fragment-definition :fragment}) 16 | 17 | (defn generated->graphql [generated] 18 | (util/nl-join (map :query (flatten (map vals (vals generated)))))) 19 | 20 | (defn node-name [node] 21 | (case (:node-type node) 22 | :operation-definition (str (or (get-in node [:operation-type :name]) (gensym "operation"))) 23 | :fragment-definition (str (or (:name node) (gensym "fragment"))))) 24 | 25 | (defn collect-deps-dispatch [node] 26 | (case (:node-type node) 27 | :fragment-spread fragment-spread/collect-deps 28 | (fn [visitor parent-deps node] 29 | (apply concat (visitor parent-deps (:selection-set node)))))) 30 | 31 | (defn collect-deps-visit-node [visitor deps node] 32 | (let [collect-deps (collect-deps-dispatch node)] 33 | (collect-deps visitor deps node))) 34 | 35 | (defn collect-deps-visit-nodes [deps coll] 36 | (when (seq coll) 37 | (let [collected-deps (map #(collect-deps-visit-node collect-deps-visit-nodes deps %) coll)] 38 | collected-deps))) 39 | 40 | (defn generate-dispatch [node] 41 | (case (:node-type node) 42 | :operation-definition operation/generate 43 | :fragment-definition fragment/generate 44 | :field field/generate 45 | :fragment-spread fragment-spread/generate 46 | :inline-fragment inline-fragment/generate)) 47 | 48 | (defn generate-visit-node [visitor deps config indent-level node] 49 | (let [generate (generate-dispatch node)] 50 | (generate visitor deps config indent-level node))) 51 | 52 | (defn generate-visit-nodes [deps config indent-level coll] 53 | (if (seq coll) 54 | (map (fn [node] 55 | (generate-visit-node generate-visit-nodes deps config indent-level node)) coll))) 56 | 57 | (defn get-with-nested-deps [fragments deps] 58 | (reduce (fn [acc dep] 59 | (set (concat acc (get-with-nested-deps fragments (:deps (get fragments dep)))))) (set deps) deps)) 60 | 61 | (defn realize-deps [fragments deps] 62 | (let [with-nested-deps (get-with-nested-deps fragments deps)] 63 | (reduce (fn [acc f] (assoc acc f (get fragments f))) {} with-nested-deps))) 64 | 65 | (defn generate-node [config fragments node] 66 | (let [deps (set (collect-deps-visit-node collect-deps-visit-nodes [] node)) 67 | realized-deps (realize-deps fragments deps) 68 | query (if (false? (:generate? config)) 69 | [] 70 | (generate-visit-node generate-visit-nodes realized-deps config 0 node))] 71 | (assoc {} 72 | :node node 73 | :query (util/nl-join (flatten query)) 74 | :deps realized-deps))) 75 | 76 | (defn generate 77 | ([parsed-statement] (generate parsed-statement {})) 78 | ([parsed-statement config] 79 | (let [nodes (apply concat (vals parsed-statement)) 80 | fragment-definitions (:fragment-definitions parsed-statement) 81 | fragments (reduce 82 | (fn [acc f] 83 | (assoc acc (:name f) (assoc f :deps (collect-deps-visit-node collect-deps-visit-nodes [] f)))) 84 | {} fragment-definitions)] 85 | (reduce (fn [acc node] 86 | (assoc-in acc [(node-type->key (:node-type node)) (node-name node)] 87 | (generate-node config fragments node))) {} nodes)))) 88 | 89 | (defn build-operation-query [config op fragments] 90 | (let [fragment-queries (map (fn [[dep _]] (:query (get fragments dep))) (:deps op))] 91 | (if (:inline-fragments config) 92 | (:query op) 93 | (do 94 | (util/nl-join (into [(:query op)] fragment-queries)))))) 95 | 96 | (defn make-operation-fn [config name op fragments] 97 | (fn op-fn 98 | ([] (op-fn {})) 99 | ([vars] 100 | {:graphql {:operationName name 101 | :query (build-operation-query config op fragments) 102 | :variables (util/variables->graphql vars)} 103 | :unpack identity}))) 104 | 105 | (defn query-map 106 | ([parsed-statement] (query-map parsed-statement {})) 107 | ([parsed-statement config] 108 | (let [nodes (generate parsed-statement config) 109 | fragments (:fragment nodes)] 110 | (reduce (fn [acc [name op]] 111 | (assoc-in acc [(keyword (get-in op [:node :operation-type :type])) (keyword (->kebab-case name))] 112 | (make-operation-fn config name op fragments))) {} (:operation nodes))))) 113 | 114 | (defn composed-query [parsed-statement queries] 115 | (let [nodes (generate parsed-statement {:generate? false})] 116 | (composed-query/generate generate-visit-nodes queries nodes))) 117 | 118 | (defn composed-mutation [parsed-statement mutations] 119 | (let [nodes (generate parsed-statement {:generate? false})] 120 | (composed-mutation/generate generate-visit-nodes mutations nodes))) 121 | -------------------------------------------------------------------------------- /src/graphql_builder/generators/composed_mutation.cljc: -------------------------------------------------------------------------------- 1 | (ns graphql-builder.generators.composed-mutation 2 | (:require [graphql-builder.generators.composed-query :as composed-query] 3 | [graphql-builder.util :as util])) 4 | 5 | (defn make-mutation [mutation-name composition-parts] 6 | (-> [(str "mutation " mutation-name (composed-query/query-variables composition-parts) " {") 7 | ;; We need a way to have stable ordering for the mutations since they might 8 | ;; have dependencies to one another. If this is required – the keys are assumed to 9 | ;; be sortable and is left to the user to generate such keys. 10 | (map :children (->> composition-parts 11 | (into []) 12 | (sort-by first) 13 | (map second))) 14 | "}"] 15 | flatten 16 | util/nl-join)) 17 | 18 | (defn generate [visitor mutations nodes] 19 | (let [mutation-name "ComposedMutation" 20 | prefixes (composed-query/make-prefixes mutations) 21 | mutation-nodes (composed-query/make-query-nodes nodes mutations) 22 | composition-parts (composed-query/make-composition-parts visitor mutation-nodes prefixes) 23 | mutation (make-mutation mutation-name composition-parts)] 24 | (fn op-fn 25 | ([] (op-fn {})) 26 | ([vars] 27 | (let [namespaced-vars (composed-query/namespace-vars prefixes vars)] 28 | {:graphql {:operationName mutation-name 29 | :query mutation 30 | :variables namespaced-vars} 31 | :unpack (composed-query/make-unpack (util/reverse-map prefixes))}))))) 32 | -------------------------------------------------------------------------------- /src/graphql_builder/generators/composed_query.cljc: -------------------------------------------------------------------------------- 1 | (ns graphql-builder.generators.composed-query 2 | (:require [camel-snake-kebab.core :refer [->PascalCase]] 3 | [graphql-builder.generators.operation :refer [generate-for-composition]] 4 | [graphql-builder.util :as util] 5 | [clojure.string :as str])) 6 | 7 | (defn query-by-name [nodes name] 8 | (let [query (get-in nodes [:operation name])] 9 | (when (nil? query) 10 | (throw (ex-info "The query doesn't exist" {:query name}))) 11 | (when (= "mutation" (get-in query [:operation-type :type])) 12 | (throw (ex-info "The query is a mutation" {:query name}))) 13 | query)) 14 | 15 | (defn make-prefixes [queries] 16 | (reduce (fn [acc query-key] 17 | (assoc acc query-key (->PascalCase (name query-key)))) {} (keys queries))) 18 | 19 | (defn make-query-nodes [nodes queries] 20 | (reduce (fn [acc [query-key name]] 21 | (assoc acc query-key (query-by-name nodes name))) {} queries)) 22 | 23 | (defn make-composition-parts [visitor query-nodes prefixes] 24 | (reduce 25 | (fn [acc [query-key query]] 26 | (let [deps (:deps query) 27 | config {:inline-fragments true :prefix (get prefixes query-key)} 28 | node (:node query)] 29 | (assoc acc query-key (generate-for-composition visitor deps config 0 node)))) 30 | {} query-nodes)) 31 | 32 | (defn query-variables [composition-parts] 33 | (let [variables (remove nil? (map :variables (vals composition-parts)))] 34 | (when (seq variables) 35 | (str "(" (str/join ", " variables) ")")))) 36 | 37 | (defn make-query [query-name composition-parts] 38 | (-> [(str "query " query-name (query-variables composition-parts) " {") 39 | (map :children (vals composition-parts)) 40 | "}"] 41 | flatten 42 | util/nl-join)) 43 | 44 | (defn namespace-var [prefixes query-key [key var]] 45 | [(str (get prefixes query-key) "__" key) var]) 46 | 47 | (defn namespace-vars [prefixes vars] 48 | (reduce (fn [acc [query-key vars]] 49 | (let [prepared (util/variables->graphql vars)] 50 | (merge acc (into {} (map #(namespace-var prefixes query-key %) prepared))))) 51 | {} vars)) 52 | 53 | (defn make-unpack [prefixes] 54 | (fn [data] 55 | (reduce (fn [acc [prefix-key val]] 56 | (let [key-parts (str/split (name prefix-key) #"__") 57 | prefix (first key-parts) 58 | key (str/join "__" (rest key-parts))] 59 | (assoc-in acc [(get prefixes prefix) key] val))) 60 | {} data))) 61 | 62 | (defn generate [visitor queries nodes] 63 | (let [query-name "ComposedQuery" 64 | prefixes (make-prefixes queries) 65 | query-nodes (make-query-nodes nodes queries) 66 | composition-parts (make-composition-parts visitor query-nodes prefixes) 67 | add-variables (query-variables composition-parts) 68 | query (make-query query-name composition-parts)] 69 | (fn op-fn 70 | ([] (op-fn {})) 71 | ([vars] 72 | (let [namespaced-vars (namespace-vars prefixes vars)] 73 | {:graphql {:operationName query-name 74 | :query query 75 | :variables namespaced-vars} 76 | :unpack (make-unpack (util/reverse-map prefixes))}))))) 77 | -------------------------------------------------------------------------------- /src/graphql_builder/generators/field.cljc: -------------------------------------------------------------------------------- 1 | (ns graphql-builder.generators.field 2 | (:require [graphql-builder.util :as util :refer [combine-children]] 3 | [clojure.string :as str] 4 | [graphql-builder.generators.shared :refer [node-arguments directives open-block close-block]])) 5 | 6 | (defn field-name [node] 7 | (let [name (:name node) 8 | field-name (:field-name node)] 9 | (if name 10 | (str name ": " field-name) 11 | field-name))) 12 | 13 | (defn generate [visitor deps config indent-level node] 14 | [(str (util/indent indent-level (field-name node)) 15 | (directives node config) 16 | (node-arguments node config) 17 | (open-block node)) 18 | (visitor deps config (inc indent-level) (:selection-set node)) 19 | (close-block node indent-level)]) 20 | -------------------------------------------------------------------------------- /src/graphql_builder/generators/fragment.cljc: -------------------------------------------------------------------------------- 1 | (ns graphql-builder.generators.fragment 2 | (:require [graphql-builder.util :as util :refer [combine-children]] 3 | [clojure.string :as str] 4 | [graphql-builder.generators.shared :refer [node-arguments directives fragment-type-name open-block close-block]])) 5 | 6 | (defn fragment-name [node config] 7 | (str "fragment " (:name node) (fragment-type-name node) (directives node config))) 8 | 9 | (defn generate [visitor deps config indent-level node] 10 | [(str (util/indent indent-level (fragment-name node config)) 11 | (open-block node)) 12 | (visitor deps config (inc indent-level) (:selection-set node)) 13 | (close-block node indent-level)]) 14 | -------------------------------------------------------------------------------- /src/graphql_builder/generators/fragment_spread.cljc: -------------------------------------------------------------------------------- 1 | (ns graphql-builder.generators.fragment-spread 2 | (:require [graphql-builder.util :as util] 3 | [graphql-builder.generators.shared :refer [directives]])) 4 | 5 | (defn collect-deps [visitor parent-deps node] 6 | (conj parent-deps (:name node))) 7 | 8 | (defn generate [visitor deps config indent-level node] 9 | (if (:inline-fragments config) 10 | (let [fragment (get deps (:name node))] 11 | (visitor deps config indent-level (:selection-set fragment))) 12 | (util/indent indent-level (str "..." (:name node) (directives node config))))) 13 | -------------------------------------------------------------------------------- /src/graphql_builder/generators/inline_fragment.cljc: -------------------------------------------------------------------------------- 1 | (ns graphql-builder.generators.inline-fragment 2 | (:require [graphql-builder.util :as util :refer [combine-children]] 3 | [clojure.string :as str] 4 | [graphql-builder.generators.shared :refer [node-arguments directives fragment-type-name]])) 5 | 6 | (defn has-children? [node] 7 | (boolean (seq (:selection-set node)))) 8 | 9 | (defn fragment-name [node config] 10 | (str "..." (:name node) (fragment-type-name node) (directives node config))) 11 | 12 | (defn open-block [node] 13 | (when (has-children? node) " {")) 14 | 15 | (defn close-block [node indent-level] 16 | (when (has-children? node) (util/indent indent-level "}"))) 17 | 18 | (defn generate [visitor deps config indent-level node] 19 | [(str (util/indent indent-level (fragment-name node config)) 20 | (open-block node)) 21 | (visitor deps config (inc indent-level) (:selection-set node)) 22 | (close-block node indent-level)]) 23 | -------------------------------------------------------------------------------- /src/graphql_builder/generators/operation.cljc: -------------------------------------------------------------------------------- 1 | (ns graphql-builder.generators.operation 2 | (:require [graphql-builder.util :as util :refer [combine-children]] 3 | [clojure.string :as str] 4 | [graphql-builder.generators.shared :refer [quote-arg add-var-prefix object-default-value]])) 5 | 6 | (defn default-value [variable] 7 | (let [val (:default-value variable)] 8 | (when-not (nil? val) 9 | (if (and (vector? val) (= :object-value (first val))) 10 | (str " = " (object-default-value (last val) {})) 11 | (str " = " (quote-arg val)))))) 12 | 13 | (defn variable-value [variable] 14 | (let [inner-required? (get-in variable [:inner-type :required]) 15 | type-name (if (= :list (:node-type variable)) 16 | (str "[" 17 | (get-in variable [:inner-type :type-name]) 18 | (when inner-required? "!") 19 | "]") 20 | (:type-name variable)) 21 | required? (:required variable)] 22 | (str type-name (when required? "!") (default-value variable)))) 23 | 24 | (defn variable-name [variable config] 25 | (let [prefix (:prefix config) 26 | name (:variable-name variable)] 27 | (add-var-prefix prefix name))) 28 | 29 | (defn node-variables-body [node config] 30 | (when-let [variables (:variable-definitions node)] 31 | (str/join ", " (map #(str "$" (variable-name % config) ": " (variable-value %)) variables)))) 32 | 33 | (defn node-variables [node config] 34 | (when (:variable-definitions node) 35 | (str "(" 36 | (node-variables-body node config) 37 | ")"))) 38 | 39 | (defn operation-name [operation] 40 | (let [{:keys [type name]} (:operation-type operation)] 41 | (if name 42 | (str type " " name) 43 | type))) 44 | 45 | (defn add-prefix-to-selection-node [prefix node] 46 | (let [name (or (:name node) (:field-name node))] 47 | (assoc node :name (add-var-prefix prefix name)))) 48 | 49 | (defn children [node config] 50 | (let [node-type (get-in node [:operation-type :type]) 51 | children (:selection-set node) 52 | prefix (:prefix config)] 53 | (if prefix 54 | (map #(add-prefix-to-selection-node prefix %) children) 55 | children))) 56 | 57 | (defn generate [visitor deps config indent-level node] 58 | [(util/indent indent-level 59 | (str (operation-name node) (node-variables node config) " {")) 60 | (visitor deps config (inc indent-level) (children node config)) 61 | (util/indent indent-level "}")]) 62 | 63 | (defn generate-for-composition [visitor deps config indent-level node] 64 | {:variables (node-variables-body node config) 65 | :children (visitor deps config (inc indent-level) (children node config))}) 66 | -------------------------------------------------------------------------------- /src/graphql_builder/generators/shared.cljc: -------------------------------------------------------------------------------- 1 | (ns graphql-builder.generators.shared 2 | (:require [clojure.string :as str] 3 | [graphql-builder.util :as util])) 4 | 5 | ;; ToDo - check enum type vs string quoting, write tests for mixed enums in list node type 6 | (defn quote-arg [v] 7 | (cond 8 | (string? v) (str "\"" v "\"") 9 | (nil? v) "null" 10 | :else v)) 11 | 12 | (defn add-var-prefix [prefix name] 13 | (if prefix 14 | (str prefix "__" name) 15 | name)) 16 | 17 | (declare generate-arg-list) 18 | (declare generate-arg-vector) 19 | (defn generate-arg [{:keys [value-type value]} prefix] 20 | (case value-type 21 | :variable (str "$" (add-var-prefix prefix (:variable-name value))) 22 | :string (str "\"" value "\"") 23 | :object (generate-arg-list value prefix) 24 | :list (generate-arg-vector (:values value) prefix) 25 | :null "null" 26 | value)) 27 | 28 | (defn generate-arg-vector [args prefix] 29 | (str "[" 30 | (->> args 31 | (mapv (fn [v] 32 | (if (vector? v) 33 | (generate-arg (first v) prefix) 34 | (generate-arg v prefix)))) 35 | (str/join ", ")) 36 | "]")) 37 | 38 | (defn generate-arg-list [args prefix] 39 | (str "{" 40 | (->> args 41 | (mapv (fn [v] (str (:field-name v) ": " (generate-arg (:value v) prefix)))) 42 | (str/join ", ")) 43 | "}")) 44 | 45 | (defn parse-arg [v prefix] 46 | (cond 47 | (and (map? v) (get v :values)) 48 | (generate-arg-vector (get v :values) prefix) 49 | 50 | (and (map? v) (get v :variable-name)) 51 | (str "$" (add-var-prefix prefix (get v :variable-name))) 52 | 53 | (vector? v) 54 | (generate-arg-list v prefix) 55 | 56 | (and (map? v) (= :enum (:value-type v))) 57 | (:value v) 58 | 59 | :else 60 | (quote-arg v))) 61 | 62 | (defn object-default-value [value config] 63 | (str "{ " 64 | (str/join ", " (map (fn [v] (str (:name v) ": " (parse-arg (:value v) (:prefix config)))) value)) 65 | " }")) 66 | 67 | (defn get-enum-or-string-value [argument] 68 | (let [value (:value argument) 69 | value-type (:value-type argument)] 70 | (if (= :enum value-type) 71 | value 72 | (quote-arg value)))) 73 | 74 | (defn argument-value-value [argument config] 75 | (let [value (:value argument) 76 | value-type (:value-type argument)] 77 | (cond 78 | (:values value) (str "[" (str/join ", " (map get-enum-or-string-value (:values value))) "]") 79 | (and (vector? value) (= :object-value (first value))) (object-default-value (last value) config) 80 | :else (get-enum-or-string-value argument)))) 81 | 82 | (defn argument-value [argument config] 83 | (let [value (:value argument) 84 | variable-name (:variable-name argument)] 85 | (cond 86 | (not (nil? value)) (argument-value-value argument config) 87 | (not (nil? variable-name)) (str "$" (add-var-prefix (:prefix config) variable-name))))) 88 | 89 | (defn argument-name [argument config] 90 | (let [prefix (:prefix config) 91 | name (:argument-name argument)] 92 | (add-var-prefix prefix name))) 93 | 94 | (defn node-arguments [node config] 95 | (when-let [arguments (:arguments node)] 96 | (str "(" 97 | (str/join ", " (map #(str (:argument-name %) ": " (argument-value % config)) arguments)) 98 | ")"))) 99 | 100 | (defn directive [d config] 101 | (str "@" (:name d) (node-arguments d config))) 102 | 103 | (defn directives [node config] 104 | (when-let [ds (:directives node)] 105 | (str " " (str/join " " (map (fn [d] (directive d config)) ds))))) 106 | 107 | (defn fragment-type-name [node] 108 | (when-let [name (get-in node [:type-condition :type-name])] 109 | (str " on " name))) 110 | 111 | (defn has-children? [node] 112 | (boolean (seq (:selection-set node)))) 113 | 114 | (defn open-block [node] 115 | (when (has-children? node) " {")) 116 | 117 | (defn close-block [node indent-level] 118 | (when (has-children? node) (util/indent indent-level "}"))) 119 | -------------------------------------------------------------------------------- /src/graphql_builder/parser.clj: -------------------------------------------------------------------------------- 1 | (ns graphql-builder.parser 2 | (:require [alumbra.parser :as alumbra-parser] 3 | [clojure.walk :as walk] 4 | [clojure.string :as str] 5 | [clojure.java.io :as io])) 6 | 7 | (defn get-selection-set [node] 8 | (let [selection-set (:alumbra/selection-set node)] 9 | selection-set)) 10 | 11 | (defn get-parsed-object-value [value] 12 | (if (= (:value-type value) :enum) 13 | value 14 | (:value value))) 15 | 16 | (defn parse-object-values [values] 17 | (conj [] 18 | :object-value 19 | (mapv (fn [v] 20 | {:name (:field-name v) 21 | :value (get-parsed-object-value (:value v))}) values))) 22 | 23 | (defn get-default-value [node] 24 | (let [value (:value (:alumbra/default-value node))] 25 | (if (vector? value) 26 | (parse-object-values value) 27 | value))) 28 | 29 | (defn get-named-type-data [node] 30 | {:node-type :variable-definition 31 | :variable-name (:alumbra/variable-name node) 32 | :type-name (get-in node [:alumbra/type :alumbra/type-name]) 33 | :required (get-in node [:alumbra/type :alumbra/non-null?]) 34 | :default-value (get-default-value node)}) 35 | 36 | (defn get-list-type-data [node] 37 | {:node-type :list 38 | :variable-name (:alumbra/variable-name node) 39 | :inner-type {:type-name (get-in node [:alumbra/type :alumbra/element-type :alumbra/type-name]) 40 | :required (get-in node [:alumbra/type :alumbra/element-type :alumbra/non-null?])} 41 | :kind :LIST 42 | :required (get-in node [:alumbra/type :alumbra/non-null?]) 43 | :element-type (:alumbra/element-type node)}) 44 | 45 | (defn get-argument-value [node] 46 | (let [value (get-in node [:alumbra/argument-value :value])] 47 | (cond 48 | (vector? value) (parse-object-values value) 49 | (and (map? value) 50 | (contains? value :variable-name)) nil 51 | :else value))) 52 | 53 | (defn get-argument-variable-name [node] 54 | (let [value (get-in node [:alumbra/argument-value :value])] 55 | (if (and (map? value) 56 | (contains? value :variable-name)) 57 | (get value :variable-name) 58 | nil))) 59 | 60 | (defn get-argument-value-type [node] 61 | (get-in node [:alumbra/argument-value :value-type])) 62 | 63 | (defmulti alumbra-node->graphql-node 64 | (fn [node] 65 | (let [] 66 | (cond 67 | (or 68 | (= "mutation" (:alumbra/operation-type node)) 69 | (= "query" (:alumbra/operation-type node)) 70 | (= "subscription" (:alumbra/operation-type node)) 71 | (contains? node :alumbra/operation-name)) :operation 72 | (contains? node :alumbra/field-name) :field 73 | (contains? node :alumbra/argument-name) :argument 74 | (contains? node :alumbra/value-type) :value 75 | (and (contains? node :alumbra/fragment-name) 76 | (contains? node :alumbra/selection-set)) :fragment 77 | (contains? node :alumbra/fragment-name) :fragment-spread 78 | (contains? node :alumbra/type-condition) :inline-fragment 79 | (contains? node :alumbra/directive-name) :directive 80 | (contains? node :alumbra/variable-name) :variable 81 | (contains? node :alumbra/directives) :directives 82 | :else nil)))) 83 | 84 | (defmethod alumbra-node->graphql-node :default [node] 85 | node) 86 | 87 | (defmethod alumbra-node->graphql-node :operation [node] 88 | {:section :operation-definitions 89 | :node-type :operation-definition 90 | :operation-type {:type (:alumbra/operation-type node) 91 | :name (:alumbra/operation-name node)} 92 | :variable-definitions (:alumbra/variables node) 93 | :selection-set (:alumbra/selection-set node)}) 94 | 95 | (defmethod alumbra-node->graphql-node :field [node] 96 | {:node-type :field 97 | :field-name (:alumbra/field-name node) 98 | :name (:alumbra/field-alias node) 99 | :arguments (:alumbra/arguments node) 100 | :selection-set (get-selection-set node) 101 | :directives (:alumbra/directives node) 102 | :value (:alumbra/value node)}) 103 | 104 | (defmethod alumbra-node->graphql-node :argument [node] 105 | {:node-type :argument 106 | :argument-name (:alumbra/argument-name node) 107 | :value (get-argument-value node) 108 | :variable-name (get-argument-variable-name node) 109 | :value-type (get-argument-value-type node)}) 110 | 111 | (defmethod alumbra-node->graphql-node :value [node] 112 | (let [value-type (:alumbra/value-type node) 113 | value (get node (keyword "alumbra" (name value-type)))] 114 | {:value-type value-type 115 | :value (case value-type 116 | ;; map changed here to mapv. This fixes generating lists back to GraphQL (f.ex. OR-filter) 117 | :list {:values (mapv #(select-keys % [:value :value-type]) value)} 118 | :variable {:variable-name (:alumbra/variable-name node)} 119 | value)})) 120 | 121 | (defmethod alumbra-node->graphql-node :fragment [node] 122 | {:node-type :fragment-definition 123 | :section :fragment-definitions 124 | :name (:alumbra/fragment-name node) 125 | :type-condition {:type-name (get-in node [:alumbra/type-condition :alumbra/type-name])} 126 | :selection-set (get-selection-set node) 127 | :directives (:alumbra/directives node)}) 128 | 129 | (defmethod alumbra-node->graphql-node :fragment-spread [node] 130 | {:node-type :fragment-spread 131 | :name (:alumbra/fragment-name node) 132 | :directives (:alumbra/directives node)}) 133 | 134 | (defmethod alumbra-node->graphql-node :inline-fragment [node] 135 | {:node-type :inline-fragment 136 | :type-condition {:type-name (get-in node [:alumbra/type-condition :alumbra/type-name])} 137 | :selection-set (get-selection-set node)}) 138 | 139 | (defmethod alumbra-node->graphql-node :directive [node] 140 | {:node-type :directive 141 | :name (:alumbra/directive-name node) 142 | :arguments (:alumbra/arguments node)}) 143 | 144 | (defmethod alumbra-node->graphql-node :variable [node] 145 | (let [variable-type (get-in node [:alumbra/type :alumbra/type-class])] 146 | (case variable-type 147 | :named-type (get-named-type-data node) 148 | :list-type (get-list-type-data node) 149 | {:node-type :variable-definition 150 | :variable-name (:alumbra/variable-name node) 151 | :type-name (get-in node [:alumbra/type :alumbra/type-name]) 152 | :required (get-in node [:alumbra/type :alumbra/non-null?]) 153 | :default-value (get-default-value node)}))) 154 | 155 | (defmethod alumbra-node->graphql-node :directives [node] 156 | {:node-type :inline-fragment 157 | :directives (:alumbra/directives node) 158 | :selection-set (get-selection-set node)}) 159 | 160 | (defn alumbra->graphql [parsed-statement] 161 | (walk/postwalk 162 | (fn [node] 163 | (if (map? node) 164 | (alumbra-node->graphql-node node) 165 | node)) 166 | parsed-statement)) 167 | 168 | (defn parse [statement] 169 | (let [alumbra-parsed (alumbra-parser/parse-document statement) 170 | parsed-statement (alumbra->graphql alumbra-parsed)] 171 | (if-let [errors (seq (:alumbra/parser-errors alumbra-parsed))] 172 | (throw (ex-info "Cannot parse statement" {:statement statement 173 | :errors errors})) 174 | {:operations-definitions (:alumbra/operations parsed-statement) 175 | :fragment-definitions (:alumbra/fragments parsed-statement)}))) 176 | 177 | (defn read-file [file] 178 | (slurp 179 | (condp instance? file 180 | java.io.File file 181 | java.net.URL file 182 | (or (io/resource file) file)))) 183 | 184 | (defmacro defgraphql [name & files] 185 | (let [parsed (parse (str/join "\n" (map read-file files)))] 186 | `(def ~name ~parsed))) 187 | -------------------------------------------------------------------------------- /src/graphql_builder/parser.cljs: -------------------------------------------------------------------------------- 1 | (ns graphql-builder.parser) 2 | -------------------------------------------------------------------------------- /src/graphql_builder/util.cljc: -------------------------------------------------------------------------------- 1 | (ns graphql-builder.util 2 | (:require [clojure.string :as str] 3 | [clojure.walk :as walk] 4 | [camel-snake-kebab.core :refer [->camelCase]])) 5 | 6 | (defn nl-join [coll] 7 | (when (seq coll) 8 | (str/join "\n" (vec (remove nil? coll))))) 9 | 10 | (defn indent [level line] 11 | (str (str/join "" (repeat (* 2 level) " ")) line)) 12 | 13 | (defn combine-children [children] 14 | (reduce (fn [acc c] 15 | (let [children (or (:children acc) []) 16 | deps (or (:deps acc) []) 17 | c-deps (:deps c)] 18 | (assoc acc 19 | :children (conj children (:children c)) 20 | :deps (if c-deps (into deps c-deps) deps)))) 21 | {} children)) 22 | 23 | (defn transform-keys 24 | "Recursively transforms all map keys in coll with t." 25 | [t coll] 26 | (let [f (fn [[k v]] [(t k) v])] 27 | (walk/postwalk (fn [x] (if (map? x) (into {} (map f x)) x)) coll))) 28 | 29 | ;; added 30 | (defn- key->graphql [k] 31 | (if (string? k) 32 | k 33 | (->camelCase (name k)))) 34 | 35 | (defn variables->graphql [vars] 36 | (transform-keys key->graphql vars)) 37 | 38 | (defn reverse-map 39 | "Reverse the keys/values of a map" 40 | [m] 41 | (into {} (map (fn [[k v]] [v k]) m))) 42 | -------------------------------------------------------------------------------- /test/graphql_builder/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns graphql-builder.core-test 2 | (:require [clojure.test :refer :all] 3 | [graphql-builder.core :refer :all] 4 | [clojure.edn :as edn] 5 | [graphql-builder.core :as core] 6 | [clojure.pprint :as pp] 7 | [clojure.string :as str] 8 | [graphql-builder.util :refer [nl-join variables->graphql]] 9 | [graphql-builder.parser :refer [parse defgraphql]])) 10 | 11 | (def test-statements (map str/trim (edn/read-string (slurp "test/graphql_builder/resources/statements.edn")))) 12 | 13 | (deftest generate-test 14 | ;;test if we can recreate the same GraphQL source 15 | (doseq [test-statement test-statements] 16 | (is (= test-statement 17 | (core/generated->graphql (core/generate (parse test-statement))))))) 18 | 19 | (def inline-fragment-source " 20 | query LoadStarships($starshipCount: Int!) { 21 | allStarships(first: $starshipCount) { 22 | edges { 23 | node { 24 | id 25 | name 26 | model 27 | costInCredits 28 | pilotConnection { 29 | edges { 30 | node { 31 | ...pilotFragment 32 | } 33 | } 34 | } 35 | } 36 | } 37 | } 38 | } 39 | fragment pilotFragment on Person { 40 | name 41 | homeworld { name } 42 | } 43 | ") 44 | 45 | (def inline-fragment-result " 46 | query LoadStarships($starshipCount: Int!) { 47 | allStarships(first: $starshipCount) { 48 | edges { 49 | node { 50 | id 51 | name 52 | model 53 | costInCredits 54 | pilotConnection { 55 | edges { 56 | node { 57 | name 58 | homeworld { 59 | name 60 | } 61 | } 62 | } 63 | } 64 | } 65 | } 66 | } 67 | } 68 | ") 69 | 70 | (deftest inline-fragment-test 71 | (let [query-map (core/query-map (parse inline-fragment-source) {:inline-fragments true}) 72 | query-fn (get-in query-map [:query :load-starships])] 73 | (is (= (str/trim inline-fragment-result) 74 | (get-in (query-fn) [:graphql :query]))))) 75 | 76 | (def query-source " 77 | query LoadStarships($starshipCount: Int!) { 78 | allStarships(first: $starshipCount) { 79 | edges { 80 | node { 81 | id 82 | name 83 | model 84 | costInCredits 85 | pilotConnection { 86 | edges { 87 | node { 88 | ...pilotFragment 89 | } 90 | } 91 | } 92 | } 93 | } 94 | } 95 | } 96 | query LoadStarshipNames { 97 | allStarships(first: 7) { 98 | edges { 99 | node { 100 | name 101 | } 102 | } 103 | } 104 | } 105 | fragment pilotFragment on Person { 106 | name 107 | homeworld { name } 108 | } 109 | ") 110 | 111 | (def append-fragment-result " 112 | query LoadStarships($starshipCount: Int!) { 113 | allStarships(first: $starshipCount) { 114 | edges { 115 | node { 116 | id 117 | name 118 | model 119 | costInCredits 120 | pilotConnection { 121 | edges { 122 | node { 123 | ...pilotFragment 124 | } 125 | } 126 | } 127 | } 128 | } 129 | } 130 | } 131 | fragment pilotFragment on Person { 132 | name 133 | homeworld { 134 | name 135 | } 136 | } 137 | ") 138 | 139 | (def namespace-query-result " 140 | query LoadStarships($QueryNS__starshipCount: Int!) { 141 | QueryNS__allStarships: allStarships(first: $QueryNS__starshipCount) { 142 | edges { 143 | node { 144 | id 145 | name 146 | model 147 | costInCredits 148 | pilotConnection { 149 | edges { 150 | node { 151 | ...pilotFragment 152 | } 153 | } 154 | } 155 | } 156 | } 157 | } 158 | } 159 | fragment pilotFragment on Person { 160 | name 161 | homeworld { 162 | name 163 | } 164 | } 165 | ") 166 | 167 | (def namespace-inline-query-source " 168 | query Foo($bar: Int!) { 169 | me { 170 | ...bazFragment 171 | } 172 | } 173 | fragment bazFragment on Qux { 174 | name(foo: $bar) 175 | } 176 | ") 177 | 178 | (def namespace-inline-query-result " 179 | query Foo($QueryNS__bar: Int!) { 180 | QueryNS__me: me { 181 | name(foo: $QueryNS__bar) 182 | } 183 | } 184 | ") 185 | 186 | (def invalid-query "qaaa") 187 | 188 | (deftest invalid-query-test 189 | (is (thrown? clojure.lang.ExceptionInfo (parse invalid-query)))) 190 | 191 | (deftest append-fragment-test 192 | (let [query-map (core/query-map (parse inline-fragment-source)) 193 | query-fn (get-in query-map [:query :load-starships])] 194 | (is (= (str/trim append-fragment-result) 195 | (get-in (query-fn) [:graphql :query]))))) 196 | 197 | (deftest namespace-query-test 198 | (let [query-map (core/query-map (parse inline-fragment-source) {:prefix "QueryNS"}) 199 | query-fn (get-in query-map [:query :load-starships])] 200 | (is (= (str/trim namespace-query-result) 201 | (get-in (query-fn) [:graphql :query]))))) 202 | 203 | (deftest namespace-inline-query-test 204 | (let [query-map (core/query-map (parse namespace-inline-query-source) 205 | {:prefix "QueryNS" :inline-fragments true}) 206 | query-fn (get-in query-map [:query :foo])] 207 | (is (= (str/trim namespace-inline-query-result) 208 | (get-in (query-fn) [:graphql :query]))))) 209 | 210 | (def composed-query-source " 211 | query LoadStarships($starshipCount: Int!) { 212 | allStarships(first: $starshipCount) { 213 | edges { 214 | node { 215 | id 216 | name 217 | model 218 | costInCredits 219 | pilotConnection { 220 | edges { 221 | node { 222 | ...pilotFragment 223 | } 224 | } 225 | } 226 | } 227 | } 228 | } 229 | } 230 | query LoadStarshipNames($starshipCount: Int!) { 231 | allStarships(first: $starshipCount) { 232 | edges { 233 | node { 234 | name 235 | } 236 | } 237 | } 238 | } 239 | fragment pilotFragment on Person { 240 | name 241 | homeworld { name } 242 | } 243 | ") 244 | 245 | (def composed-query-result " 246 | query ComposedQuery($LoadStarships1__starshipCount: Int!, $LoadStarships2__starshipCount: Int!, $LoadStarshipNames__starshipCount: Int!) { 247 | LoadStarships1__allStarships: allStarships(first: $LoadStarships1__starshipCount) { 248 | edges { 249 | node { 250 | id 251 | name 252 | model 253 | costInCredits 254 | pilotConnection { 255 | edges { 256 | node { 257 | name 258 | homeworld { 259 | name 260 | } 261 | } 262 | } 263 | } 264 | } 265 | } 266 | } 267 | LoadStarships2__allStarships: allStarships(first: $LoadStarships2__starshipCount) { 268 | edges { 269 | node { 270 | id 271 | name 272 | model 273 | costInCredits 274 | pilotConnection { 275 | edges { 276 | node { 277 | name 278 | homeworld { 279 | name 280 | } 281 | } 282 | } 283 | } 284 | } 285 | } 286 | } 287 | LoadStarshipNames__allStarships: allStarships(first: $LoadStarshipNames__starshipCount) { 288 | edges { 289 | node { 290 | name 291 | } 292 | } 293 | } 294 | } 295 | ") 296 | 297 | (deftest composed-query-test 298 | (let [composed-fn (core/composed-query (parse composed-query-source) 299 | {:load-starships-1 "LoadStarships" 300 | :load-starships-2 "LoadStarships" 301 | :load-starship-names "LoadStarshipNames"}) 302 | composed-query (composed-fn) 303 | unpack (:unpack composed-query)] 304 | (is (= (str/trim composed-query-result) 305 | (get-in composed-query [:graphql :query]))) 306 | (is (= {:load-starships-1 {"foo" :bar}} 307 | (unpack {"LoadStarships1__foo" :bar}))))) 308 | 309 | (def composed-query-source-2 " 310 | query Hero($episode: String!) { 311 | hero(episode: $episode) { 312 | name 313 | } 314 | } 315 | ") 316 | 317 | (def composed-query-result-2 " 318 | query ComposedQuery($JediHero__episode: String!, $EmpireHero__episode: String!) { 319 | JediHero__hero: hero(episode: $JediHero__episode) { 320 | name 321 | } 322 | EmpireHero__hero: hero(episode: $EmpireHero__episode) { 323 | name 324 | } 325 | } 326 | ") 327 | 328 | (deftest composed-query-test-2 329 | (let [composed-fn (core/composed-query (parse composed-query-source-2) 330 | {:jedi-hero "Hero" 331 | :empire-hero "Hero"}) 332 | composed-query (composed-fn) 333 | unpack (:unpack composed-query)] 334 | (is (= (str/trim composed-query-result-2) 335 | (get-in composed-query [:graphql :query]))))) 336 | 337 | (def composed-query-result-3 " 338 | query ComposedQuery($LoadStarships1__starshipCount: Int!) { 339 | LoadStarships1__allStarships: allStarships(first: $LoadStarships1__starshipCount) { 340 | edges { 341 | node { 342 | id 343 | name 344 | model 345 | costInCredits 346 | pilotConnection { 347 | edges { 348 | node { 349 | name 350 | homeworld { 351 | name 352 | } 353 | } 354 | } 355 | } 356 | } 357 | } 358 | } 359 | } 360 | ") 361 | 362 | (deftest composed-query-test-3 363 | (let [composed-fn (core/composed-query (parse composed-query-source) 364 | {:load-starships-1 "LoadStarships"}) 365 | composed-query (composed-fn) 366 | unpack (:unpack composed-query)] 367 | (is (= (str/trim composed-query-result-3) 368 | (get-in composed-query [:graphql :query]))))) 369 | 370 | (defgraphql parsed-graphql 371 | "test/graphql_builder/resources/1.graphql" 372 | "test/graphql_builder/resources/2.graphql") 373 | 374 | (deftest defgrapqhl-test 375 | (is (= {:operations-definitions 376 | [{:section :operation-definitions 377 | :node-type :operation-definition 378 | :operation-type {:type "query" :name "LoadStarships"} 379 | :variable-definitions nil 380 | :selection-set 381 | [{:node-type :field 382 | :field-name "allStarships" 383 | :name nil 384 | :arguments nil 385 | :selection-set 386 | [{:node-type :field 387 | :field-name "name" 388 | :name nil 389 | :arguments nil 390 | :selection-set nil 391 | :directives nil 392 | :value nil}] 393 | :directives nil 394 | :value nil}]} 395 | {:section :operation-definitions 396 | :node-type :operation-definition 397 | :operation-type {:type "subscription" :name "LoadStarships"} 398 | :variable-definitions nil 399 | :selection-set 400 | [{:node-type :field 401 | :field-name "allStarships" 402 | :name nil 403 | :arguments nil 404 | :selection-set 405 | [{:node-type :field 406 | :field-name "name" 407 | :name nil 408 | :arguments nil 409 | :selection-set nil 410 | :directives nil 411 | :value nil} 412 | {:node-type :field 413 | :field-name "pilot" 414 | :name nil 415 | :arguments nil 416 | :selection-set 417 | [{:node-type :fragment-spread 418 | :name "pilotFragment" 419 | :directives nil}] 420 | :directives nil 421 | :value nil}] 422 | :directives nil 423 | :value nil}]}] 424 | :fragment-definitions 425 | [{:node-type :fragment-definition 426 | :section :fragment-definitions 427 | :name "pilotFragment" 428 | :type-condition {:type-name "Person"} 429 | :selection-set 430 | [{:node-type :field 431 | :field-name "name" 432 | :name nil 433 | :arguments nil 434 | :selection-set nil 435 | :directives nil 436 | :value nil} 437 | {:node-type :field 438 | :field-name "homeworld" 439 | :name nil 440 | :arguments nil 441 | :selection-set 442 | [{:node-type :field 443 | :field-name "name" 444 | :name nil 445 | :arguments nil 446 | :selection-set nil 447 | :directives nil 448 | :value nil}] 449 | :directives nil 450 | :value nil}] 451 | :directives nil}]} 452 | parsed-graphql))) 453 | 454 | (def query-custom-type-test-source " 455 | mutation createSiteWithSchema($name: String!, $label: String!, $contentSchema: [ContentFieldInput]!) { 456 | createSiteWithSchema(name: $name, label: $label, contentSchema: $contentSchema) { 457 | id 458 | name 459 | label 460 | contentSchema { 461 | ...contentSchemaSelection 462 | } 463 | } 464 | } 465 | fragment contentSchemaSelection on ContentField { 466 | type 467 | fieldType 468 | constraints 469 | extendsType 470 | allowedType 471 | allowedTypes 472 | fields { 473 | fieldType 474 | fieldName 475 | } 476 | } 477 | ") 478 | 479 | (deftest query-custom-type-test 480 | (is (= (str/trim query-custom-type-test-source) 481 | (core/generated->graphql (core/generate (parse query-custom-type-test-source)))))) 482 | 483 | 484 | (deftest variables->graphql-test 485 | (is (= {"fooBar" "baz"} 486 | (variables->graphql {:foo-bar "baz"})))) 487 | 488 | 489 | (def nested-fragment-source " 490 | query User { 491 | user { 492 | ...userFields 493 | } 494 | } 495 | 496 | fragment userFields on User { 497 | name 498 | messages { 499 | ...messageFields 500 | } 501 | } 502 | 503 | fragment messageFields on Message { 504 | title 505 | } 506 | ") 507 | 508 | (def nested-fragment-result 509 | " 510 | query User { 511 | user { 512 | ...userFields 513 | } 514 | } 515 | fragment userFields on User { 516 | name 517 | messages { 518 | ...messageFields 519 | } 520 | } 521 | fragment messageFields on Message { 522 | title 523 | } 524 | ") 525 | 526 | (deftest nested-fragment-test 527 | (let [query-map (core/query-map (parse nested-fragment-source) {}) 528 | query-fn (get-in query-map [:query :user])] 529 | (is (= (str/trim nested-fragment-result) 530 | (get-in (query-fn) [:graphql :query]))))) 531 | 532 | (def nested-fragment-source-on-subscription 533 | " 534 | subscription User { 535 | user { 536 | ...userFields 537 | } 538 | } 539 | 540 | fragment userFields on User { 541 | name 542 | messages { 543 | ...messageFields 544 | } 545 | } 546 | 547 | fragment messageFields on Message { 548 | title 549 | } 550 | ") 551 | 552 | (def nested-fragment-result-on-subscription 553 | " 554 | subscription User { 555 | user { 556 | ...userFields 557 | } 558 | } 559 | fragment userFields on User { 560 | name 561 | messages { 562 | ...messageFields 563 | } 564 | } 565 | fragment messageFields on Message { 566 | title 567 | } 568 | ") 569 | 570 | (deftest nested-fragment-test 571 | (let [query-map (core/query-map (parse nested-fragment-source-on-subscription) {}) 572 | query-fn (get-in query-map [:subscription :user])] 573 | (is (= (str/trim nested-fragment-result-on-subscription) 574 | (get-in (query-fn) [:graphql :query]))))) 575 | 576 | (def inline-nested-fragment-source " 577 | query User { 578 | user { 579 | ...userFields 580 | } 581 | } 582 | 583 | fragment userFields on User { 584 | name 585 | messages { 586 | ...messageFields 587 | } 588 | } 589 | 590 | fragment messageFields on Message { 591 | title 592 | } 593 | ") 594 | 595 | (def inline-nested-fragment-result 596 | " 597 | query User { 598 | user { 599 | name 600 | messages { 601 | title 602 | } 603 | } 604 | } 605 | ") 606 | 607 | (deftest inline-nested-fragment-test 608 | (let [query-map (core/query-map (parse inline-nested-fragment-source) {:inline-fragments true}) 609 | query-fn (get-in query-map [:query :user])] 610 | (is (= (str/trim inline-nested-fragment-result) 611 | (get-in (query-fn) [:graphql :query]))))) 612 | 613 | 614 | (def fragment-nesting-on-same-type " 615 | 616 | mutation validateOrderPersonalInformation($input: ValidateOrderPersonalInformationInput!) { 617 | validateOrderPersonalInformation(input: $input) { 618 | ...validateOrderPersonalInformationPayloadFields 619 | } 620 | } 621 | 622 | fragment validateOrderPersonalInformationPayloadFields on ValidateOrderPersonalInformationPayload { 623 | clientMutationId 624 | errors { 625 | ...errorFields 626 | } 627 | valid 628 | } 629 | 630 | fragment errorFields on Error { 631 | ...innerErrorFields 632 | suberrors { 633 | ...innerErrorFields 634 | suberrors { 635 | ...innerErrorFields 636 | suberrors { 637 | ...innerErrorFields 638 | } 639 | } 640 | } 641 | } 642 | 643 | fragment innerErrorFields on Error { 644 | index 645 | key 646 | messages 647 | } 648 | ") 649 | 650 | (def fragment-nesting-on-same-type-result 651 | " 652 | mutation validateOrderPersonalInformation($input: ValidateOrderPersonalInformationInput!) { 653 | validateOrderPersonalInformation(input: $input) { 654 | clientMutationId 655 | errors { 656 | index 657 | key 658 | messages 659 | suberrors { 660 | index 661 | key 662 | messages 663 | suberrors { 664 | index 665 | key 666 | messages 667 | suberrors { 668 | index 669 | key 670 | messages 671 | } 672 | } 673 | } 674 | } 675 | valid 676 | } 677 | } 678 | ") 679 | 680 | 681 | (deftest fragment-nesting-on-same-type-test 682 | (let [query-map (core/query-map (parse fragment-nesting-on-same-type) {:inline-fragments true}) 683 | query-fn (get-in query-map [:mutation :validate-order-personal-information])] 684 | (is (= (str/trim fragment-nesting-on-same-type-result) 685 | (get-in (query-fn) [:graphql :query]))))) 686 | 687 | 688 | (def required-inside-array 689 | " 690 | mutation update($id: ID, $description: String, $otherIds: [ID!]) { 691 | updateService(id: $id, description: $description, otherIds: $otherIds) { 692 | ...item 693 | } 694 | } 695 | ") 696 | 697 | (deftest required-inside-array-test 698 | (let [query-map (core/query-map (parse required-inside-array) {}) 699 | query-fn (get-in query-map [:mutation :update])] 700 | (is (= (str/trim required-inside-array) 701 | (get-in (query-fn) [:graphql :query]))))) 702 | 703 | 704 | (def required-inside-array-and-required-array 705 | " 706 | mutation update($id: ID, $description: String, $otherIds: [ID!]!) { 707 | updateService(id: $id, description: $description, otherIds: $otherIds) { 708 | ...item 709 | } 710 | } 711 | ") 712 | 713 | (deftest required-inside-array-and-required-array-test 714 | (let [query-map (core/query-map (parse required-inside-array) {}) 715 | query-fn (get-in query-map [:mutation :update])] 716 | (is (= (str/trim required-inside-array) 717 | (get-in (query-fn) [:graphql :query]))))) 718 | 719 | (def hardcoded-enum-value 720 | " 721 | query foo { 722 | image(size: \"LARGE\") { 723 | url 724 | } 725 | } 726 | ") 727 | 728 | (def processed-hardcoded-enum-value 729 | " 730 | query foo { 731 | image(size: LARGE) { 732 | url 733 | } 734 | } 735 | ") 736 | 737 | (deftest hardcoded-enum-value-test 738 | (let [query-map (core/query-map (parse hardcoded-enum-value) {}) 739 | query-fn (get-in query-map [:query :foo])] 740 | (is (= (str/trim hardcoded-enum-value) 741 | (get-in (query-fn) [:graphql :query]))))) 742 | 743 | (def alias-source 744 | " 745 | query User { 746 | foo: currentUser { 747 | name 748 | } 749 | } 750 | mutation Login { 751 | bar: login { 752 | token 753 | } 754 | } 755 | subscription Message { 756 | baz: message { 757 | text: content 758 | } 759 | } 760 | ") 761 | 762 | (deftest aliases-test 763 | (let [query-map (core/query-map (parse alias-source) {}) 764 | query-fn (get-in query-map [:query :user]) 765 | mutation-fn (get-in query-map [:mutation :login]) 766 | subscription-fn (get-in query-map [:subscription :message])] 767 | (is (= (str/trim alias-source) 768 | (str/join "\n" 769 | [(get-in (query-fn) [:graphql :query]) 770 | (get-in (mutation-fn) [:graphql :query]) 771 | (get-in (subscription-fn) [:graphql :query])]))))) 772 | 773 | (def enums-arg-source 774 | " 775 | query Foo { 776 | productList(someArg: moo, otherArg: \"DESC\") { 777 | name 778 | } 779 | } 780 | " 781 | ) 782 | 783 | (deftest enums-arg-test 784 | (let [query-map (core/query-map (parse enums-arg-source) {}) 785 | query-fn (get-in query-map [:query :foo])] 786 | (is (= (str/trim enums-arg-source) 787 | (get-in (query-fn) [:graphql :query]))))) 788 | 789 | (def argument-false-source 790 | "query Foo { 791 | productList(someArg: moo, otherArg: false) { 792 | name 793 | } 794 | } 795 | ") 796 | 797 | (deftest enums-arg-2-test 798 | (let [query-map (core/query-map (parse argument-false-source) {}) 799 | query-fn (get-in query-map [:query :foo])] 800 | (is (= (str/trim argument-false-source) 801 | (get-in (query-fn) [:graphql :query]))))) 802 | 803 | 804 | (def object-argument-parsing-source 805 | "query Foo($item: String) { 806 | productList(filter: { value: \"foo\", contains: [\"A\", \"B\"], using: {item: $item} }) { 807 | nodes { 808 | productNumber 809 | } 810 | } 811 | } 812 | ") 813 | 814 | (deftest object-argument-parsing-test 815 | (let [query-map (core/query-map (parse object-argument-parsing-source)) 816 | query-fn (get-in query-map [:query :foo])] 817 | (is (= (str/trim object-argument-parsing-source) 818 | (get-in (query-fn) [:graphql :query]))))) 819 | 820 | (def object-argument-parsing-source-2 821 | "query Foo($id: String, $patch: String) { 822 | productList(filter: { id: $id, patch: $patch }) { 823 | nodes { 824 | productNumber 825 | } 826 | } 827 | } 828 | ") 829 | 830 | (deftest object-argument-parsing-2-test 831 | (let [query-map (core/query-map (parse object-argument-parsing-source-2)) 832 | query-fn (get-in query-map [:query :foo])] 833 | (is (= (str/trim object-argument-parsing-source-2) 834 | (get-in (query-fn) [:graphql :query]))))) 835 | 836 | (def composed-mutation-source " 837 | mutation AddStarship($name: String!){ 838 | addStarship(name: $name){ 839 | id 840 | } 841 | }") 842 | 843 | (def composed-mutation-result " 844 | mutation ComposedMutation($AddStarship1__name: String!, $AddStarship2__name: String!) { 845 | AddStarship1__addStarship: addStarship(name: $AddStarship1__name) { 846 | id 847 | } 848 | AddStarship2__addStarship: addStarship(name: $AddStarship2__name) { 849 | id 850 | } 851 | }") 852 | 853 | (deftest composed-mutation-test 854 | (let [composed-fn (core/composed-mutation (parse composed-mutation-source) 855 | {:add-starship-1 "AddStarship" 856 | :add-starship-2 "AddStarship"}) 857 | composed-mutation (composed-fn) 858 | unpack (:unpack composed-mutation)] 859 | (is (= (str/trim composed-mutation-result) 860 | (get-in composed-mutation [:graphql :query]))) 861 | (is (= {:add-starship-1 {"name" :bar}} 862 | (unpack {"AddStarship1__name" :bar}))))) 863 | 864 | 865 | (def inline-enum-query-1 866 | "query AssignableTerminals { 867 | terminals(first: 100, filter: { assignability: ASSIGNABLE }) { 868 | edges { 869 | node { 870 | id 871 | terminalVersion 872 | deviceId 873 | availabilityState 874 | } 875 | } 876 | } 877 | }") 878 | 879 | (deftest inline-enum-query-test-1 880 | (let [query-map (core/query-map (parse inline-enum-query-1)) 881 | query-fn (get-in query-map [:query :assignable-terminals])] 882 | (is (= (str/trim inline-enum-query-1) 883 | (get-in (query-fn) [:graphql :query]))))) 884 | 885 | (def inline-enum-query-1a 886 | "query AssignableTerminals { 887 | terminals(first: 100, filter: { assignability: \"ASSIGNABLE\" }) { 888 | edges { 889 | node { 890 | id 891 | terminalVersion 892 | deviceId 893 | availabilityState 894 | } 895 | } 896 | } 897 | }") 898 | 899 | (deftest inline-enum-query-test-1a 900 | (let [query-map (core/query-map (parse inline-enum-query-1a)) 901 | query-fn (get-in query-map [:query :assignable-terminals])] 902 | (is (= (str/trim inline-enum-query-1a) 903 | (get-in (query-fn) [:graphql :query]))))) 904 | 905 | (def inline-enum-query-2 906 | "query AssignableTerminals { 907 | terminals(first: 100, filter: ASSIGNABLE) { 908 | edges { 909 | node { 910 | id 911 | terminalVersion 912 | deviceId 913 | availabilityState 914 | } 915 | } 916 | } 917 | }") 918 | 919 | (deftest inline-enum-query-test-2 920 | (let [query-map (core/query-map (parse inline-enum-query-2)) 921 | query-fn (get-in query-map [:query :assignable-terminals])] 922 | (is (= (str/trim inline-enum-query-2) 923 | (get-in (query-fn) [:graphql :query]))))) 924 | 925 | 926 | (def inline-enum-query-3 927 | "query AssignableTerminals { 928 | terminals(first: 100, filter: { assignability: {foo: ASSIGNABLE} }) { 929 | edges { 930 | node { 931 | id 932 | terminalVersion 933 | deviceId 934 | availabilityState 935 | } 936 | } 937 | } 938 | }") 939 | 940 | (deftest inline-enum-query-test-3 941 | (let [query-map (core/query-map (parse inline-enum-query-3)) 942 | query-fn (get-in query-map [:query :assignable-terminals])] 943 | (is (= (str/trim inline-enum-query-3) 944 | (get-in (query-fn) [:graphql :query]))))) 945 | 946 | (def list-query-argument-1 947 | "query Search($term: String) { 948 | objects(filter: { or: [{name: {startsWith: $term}}, {objectId: {startsWith: $term}}] }) { 949 | id 950 | } 951 | }") 952 | 953 | (deftest list-query-argument-test-1 954 | (let [query-map (core/query-map (parse list-query-argument-1)) 955 | query-fn (get-in query-map [:query :search])] 956 | (is (= (str/trim list-query-argument-1) 957 | (get-in (query-fn) [:graphql :query]))))) 958 | 959 | (def argument-defaults 960 | "query postCollectionQuery($list: Boolean = false) { 961 | postCollection { 962 | items { 963 | content @skip(if: $list) { 964 | json 965 | } 966 | shortDescription @include(if: $list) { 967 | json 968 | } 969 | } 970 | } 971 | }") 972 | 973 | (deftest arguments-defaults-test 974 | (let [query-map (core/query-map (parse argument-defaults)) 975 | query-fn (get-in query-map [:query :post-collection-query])] 976 | (is (= (str/trim argument-defaults) (get-in (query-fn) [:graphql :query]))))) 977 | 978 | (def argument-object-value-literal-null-query 979 | "query literalNull { 980 | events(where: { deletedAt: null }) { 981 | id 982 | } 983 | }") 984 | 985 | (deftest argument-object-value-literal-null-test 986 | (let [query-map (core/query-map (parse argument-object-value-literal-null-query)) 987 | query-fn (get-in query-map [:query :literal-null])] 988 | (is (= (str/trim argument-object-value-literal-null-query) (get-in (query-fn) [:graphql :query]))))) 989 | 990 | (def field-value-literal-null-query 991 | "query literalNull { 992 | events(where: { or: [{deletedAt: null}, {deletedAt_gt: \"2023-11-28T00:00:00Z\"}] }) { 993 | id 994 | } 995 | }") 996 | 997 | (deftest field-value-literal-null-test 998 | (let [query-map (core/query-map (parse field-value-literal-null-query)) 999 | query-fn (get-in query-map [:query :literal-null])] 1000 | (is (= (str/trim field-value-literal-null-query) (get-in (query-fn) [:graphql :query]))))) 1001 | -------------------------------------------------------------------------------- /test/graphql_builder/resources/1.graphql: -------------------------------------------------------------------------------- 1 | query LoadStarships { 2 | allStarships { 3 | name 4 | } 5 | } 6 | 7 | subscription LoadStarships { 8 | allStarships { 9 | name 10 | pilot { 11 | ...pilotFragment 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/graphql_builder/resources/2.graphql: -------------------------------------------------------------------------------- 1 | fragment pilotFragment on Person { 2 | name 3 | homeworld { name } 4 | } 5 | -------------------------------------------------------------------------------- /test/graphql_builder/resources/parsed/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retro/graphql-builder/22cc26b1133ed6dfd0f98b74505d40f429b9b725/test/graphql_builder/resources/parsed/.gitkeep -------------------------------------------------------------------------------- /test/graphql_builder/resources/parsed/statements.edn: -------------------------------------------------------------------------------- 1 | ( 2 | {:operation-definitions 3 | [{:section :operation-definitions, 4 | :node-type :operation-definition, 5 | :operation-type {:type "query"}, 6 | :selection-set 7 | [{:node-type :field, 8 | :field-name "user", 9 | :arguments 10 | [{:node-type :argument, :argument-name "id", :value 4}], 11 | :selection-set 12 | [{:node-type :field, :field-name "id"} 13 | {:node-type :field, :field-name "name"} 14 | {:node-type :field, 15 | :field-name "profilePic", 16 | :arguments 17 | [{:node-type :argument, :argument-name "width", :value 100} 18 | {:node-type :argument, 19 | :argument-name "height", 20 | :value 50.0}]}]}]}]} 21 | {:operation-definitions 22 | [{:section :operation-definitions, 23 | :node-type :operation-definition, 24 | :operation-type {:type "query"}, 25 | :selection-set 26 | [{:node-type :field, 27 | :field-name "user", 28 | :arguments 29 | [{:node-type :argument, :argument-name "id", :value 4}], 30 | :selection-set 31 | [{:node-type :field, :field-name "id"} 32 | {:node-type :field, :field-name "name"} 33 | {:node-type :field, 34 | :field-name "profilePic", 35 | :arguments 36 | [{:node-type :argument, :argument-name "width", :value 100} 37 | {:node-type :argument, 38 | :argument-name "height", 39 | :value 50}]}]}]}]} 40 | {:operation-definitions 41 | [{:section :operation-definitions, 42 | :node-type :operation-definition, 43 | :operation-type {:type "query"}, 44 | :selection-set 45 | [{:node-type :field, 46 | :field-name "user", 47 | :arguments 48 | [{:node-type :argument, :argument-name "id", :value 4}], 49 | :selection-set [{:node-type :field, :field-name "name"}]}]}]} 50 | {:operation-definitions 51 | [{:section :operation-definitions, 52 | :node-type :operation-definition, 53 | :operation-type {:type "mutation"}, 54 | :selection-set 55 | [{:node-type :field, 56 | :field-name "likeStory", 57 | :arguments 58 | [{:node-type :argument, :argument-name "storyID", :value 12345}], 59 | :selection-set 60 | [{:node-type :field, 61 | :field-name "story", 62 | :selection-set 63 | [{:node-type :field, :field-name "likeCount"}]}]}]}]} 64 | {:operation-definitions 65 | [{:section :operation-definitions, 66 | :node-type :operation-definition, 67 | :operation-type {:type "query"}, 68 | :selection-set 69 | [{:node-type :field, 70 | :field-name "me", 71 | :selection-set 72 | [{:node-type :field, :field-name "id"} 73 | {:node-type :field, :field-name "firstName"} 74 | {:node-type :field, :field-name "lastName"} 75 | {:node-type :field, 76 | :field-name "birthday", 77 | :selection-set 78 | [{:node-type :field, :field-name "month"} 79 | {:node-type :field, :field-name "day"}]} 80 | {:node-type :field, 81 | :field-name "friends", 82 | :selection-set [{:node-type :field, :field-name "name"}]}]}]}]} 83 | {:operation-definitions 84 | [{:section :operation-definitions, 85 | :node-type :operation-definition, 86 | :operation-type {:type "query"}, 87 | :selection-set 88 | [{:node-type :field, 89 | :field-name "me", 90 | :selection-set [{:node-type :field, :field-name "name"}]}]}]} 91 | {:operation-definitions 92 | [{:section :operation-definitions, 93 | :node-type :operation-definition, 94 | :operation-type {:type "query"}, 95 | :selection-set 96 | [{:node-type :field, 97 | :field-name "user", 98 | :arguments 99 | [{:node-type :argument, :argument-name "id", :value 4}], 100 | :selection-set [{:node-type :field, :field-name "name"}]}]}]} 101 | {:operation-definitions 102 | [{:section :operation-definitions, 103 | :node-type :operation-definition, 104 | :operation-type {:type "query"}, 105 | :selection-set 106 | [{:node-type :field, 107 | :field-name "user", 108 | :arguments 109 | [{:node-type :argument, :argument-name "id", :value 4}], 110 | :selection-set 111 | [{:node-type :field, :field-name "id"} 112 | {:node-type :field, :field-name "name"} 113 | {:node-type :field, 114 | :field-name "profilePic", 115 | :arguments 116 | [{:node-type :argument, 117 | :argument-name "size", 118 | :value 100}]}]}]}]} 119 | {:operation-definitions 120 | [{:section :operation-definitions, 121 | :node-type :operation-definition, 122 | :operation-type {:type "query"}, 123 | :selection-set 124 | [{:node-type :field, 125 | :field-name "user", 126 | :arguments 127 | [{:node-type :argument, :argument-name "id", :value 4}], 128 | :selection-set 129 | [{:node-type :field, :field-name "id"} 130 | {:node-type :field, :field-name "name"} 131 | {:node-type :field, 132 | :field-name "profilePic", 133 | :arguments 134 | [{:node-type :argument, :argument-name "width", :value 100} 135 | {:node-type :argument, 136 | :argument-name "height", 137 | :value 50}]}]}]}]} 138 | {:operation-definitions 139 | [{:section :operation-definitions, 140 | :node-type :operation-definition, 141 | :operation-type {:type "query"}, 142 | :selection-set 143 | [{:node-type :field, 144 | :field-name "user", 145 | :arguments 146 | [{:node-type :argument, :argument-name "id", :value 4}], 147 | :selection-set 148 | [{:node-type :field, :field-name "id"} 149 | {:node-type :field, :field-name "name"} 150 | {:node-type :field, 151 | :name "smallPic", 152 | :field-name "profilePic", 153 | :arguments 154 | [{:node-type :argument, :argument-name "size", :value 64}]} 155 | {:node-type :field, 156 | :name "bigPic", 157 | :field-name "profilePic", 158 | :arguments 159 | [{:node-type :argument, 160 | :argument-name "size", 161 | :value 1024}]}]}]}]} 162 | {:operation-definitions 163 | [{:section :operation-definitions, 164 | :node-type :operation-definition, 165 | :operation-type {:type "query"}, 166 | :selection-set 167 | [{:node-type :field, 168 | :name "zuck", 169 | :field-name "user", 170 | :arguments 171 | [{:node-type :argument, :argument-name "id", :value 4}], 172 | :selection-set 173 | [{:node-type :field, :field-name "id"} 174 | {:node-type :field, :field-name "name"}]}]}]} 175 | {:operation-definitions 176 | [{:section :operation-definitions, 177 | :node-type :operation-definition, 178 | :operation-type {:type "query", :name "noFragments"}, 179 | :selection-set 180 | [{:node-type :field, 181 | :field-name "user", 182 | :arguments 183 | [{:node-type :argument, :argument-name "id", :value 4}], 184 | :selection-set 185 | [{:node-type :field, 186 | :field-name "friends", 187 | :arguments 188 | [{:node-type :argument, :argument-name "first", :value 10}], 189 | :selection-set 190 | [{:node-type :field, :field-name "id"} 191 | {:node-type :field, :field-name "name"} 192 | {:node-type :field, 193 | :field-name "profilePic", 194 | :arguments 195 | [{:node-type :argument, :argument-name "size", :value 50}]}]} 196 | {:node-type :field, 197 | :field-name "mutualFriends", 198 | :arguments 199 | [{:node-type :argument, :argument-name "first", :value 10}], 200 | :selection-set 201 | [{:node-type :field, :field-name "id"} 202 | {:node-type :field, :field-name "name"} 203 | {:node-type :field, 204 | :field-name "profilePic", 205 | :arguments 206 | [{:node-type :argument, 207 | :argument-name "size", 208 | :value 50}]}]}]}]}]} 209 | {:operation-definitions 210 | [{:section :operation-definitions, 211 | :node-type :operation-definition, 212 | :operation-type {:type "query", :name "withFragments"}, 213 | :selection-set 214 | [{:node-type :field, 215 | :field-name "user", 216 | :arguments 217 | [{:node-type :argument, :argument-name "id", :value 4}], 218 | :selection-set 219 | [{:node-type :field, 220 | :field-name "friends", 221 | :arguments 222 | [{:node-type :argument, :argument-name "first", :value 10}], 223 | :selection-set 224 | [{:node-type :fragment-spread, :name "friendFields"}]} 225 | {:node-type :field, 226 | :field-name "mutualFriends", 227 | :arguments 228 | [{:node-type :argument, :argument-name "first", :value 10}], 229 | :selection-set 230 | [{:node-type :fragment-spread, :name "friendFields"}]}]}]}], 231 | :fragment-definitions 232 | [{:name "friendFields", 233 | :type-condition {:type-name "User"}, 234 | :selection-set 235 | [{:node-type :field, :field-name "id"} 236 | {:node-type :field, :field-name "name"} 237 | {:node-type :field, 238 | :field-name "profilePic", 239 | :arguments 240 | [{:node-type :argument, :argument-name "size", :value 50}]}], 241 | :node-type :fragment-definition, 242 | :section :fragment-definitions}]} 243 | {:operation-definitions 244 | [{:section :operation-definitions, 245 | :node-type :operation-definition, 246 | :operation-type {:type "query", :name "withNestedFragments"}, 247 | :selection-set 248 | [{:node-type :field, 249 | :field-name "user", 250 | :arguments 251 | [{:node-type :argument, :argument-name "id", :value 4}], 252 | :selection-set 253 | [{:node-type :field, 254 | :field-name "friends", 255 | :arguments 256 | [{:node-type :argument, :argument-name "first", :value 10}], 257 | :selection-set 258 | [{:node-type :fragment-spread, :name "friendFields"}]} 259 | {:node-type :field, 260 | :field-name "mutualFriends", 261 | :arguments 262 | [{:node-type :argument, :argument-name "first", :value 10}], 263 | :selection-set 264 | [{:node-type :fragment-spread, :name "friendFields"}]}]}]}], 265 | :fragment-definitions 266 | [{:name "friendFields", 267 | :type-condition {:type-name "User"}, 268 | :selection-set 269 | [{:node-type :field, :field-name "id"} 270 | {:node-type :field, :field-name "name"} 271 | {:node-type :fragment-spread, :name "standardProfilePic"}], 272 | :node-type :fragment-definition, 273 | :section :fragment-definitions} 274 | {:name "standardProfilePic", 275 | :type-condition {:type-name "User"}, 276 | :selection-set 277 | [{:node-type :field, 278 | :field-name "profilePic", 279 | :arguments 280 | [{:node-type :argument, :argument-name "size", :value 50}]}], 281 | :node-type :fragment-definition, 282 | :section :fragment-definitions}]} 283 | {:operation-definitions 284 | [{:section :operation-definitions, 285 | :node-type :operation-definition, 286 | :operation-type {:type "query", :name "FragmentTyping"}, 287 | :selection-set 288 | [{:node-type :field, 289 | :field-name "profiles", 290 | :arguments 291 | [{:node-type :argument, 292 | :argument-name "handles", 293 | :value {:values ["zuck" "cocacola"]}}], 294 | :selection-set 295 | [{:node-type :field, :field-name "handle"} 296 | {:node-type :fragment-spread, :name "userFragment"} 297 | {:node-type :fragment-spread, :name "pageFragment"}]}]}], 298 | :fragment-definitions 299 | [{:name "userFragment", 300 | :type-condition {:type-name "User"}, 301 | :selection-set 302 | [{:node-type :field, 303 | :field-name "friends", 304 | :selection-set [{:node-type :field, :field-name "count"}]}], 305 | :node-type :fragment-definition, 306 | :section :fragment-definitions} 307 | {:name "pageFragment", 308 | :type-condition {:type-name "Page"}, 309 | :selection-set 310 | [{:node-type :field, 311 | :field-name "likers", 312 | :selection-set [{:node-type :field, :field-name "count"}]}], 313 | :node-type :fragment-definition, 314 | :section :fragment-definitions}]} 315 | {:operation-definitions 316 | [{:section :operation-definitions, 317 | :node-type :operation-definition, 318 | :operation-type {:type "query", :name "inlineFragmentTyping"}, 319 | :selection-set 320 | [{:node-type :field, 321 | :field-name "profiles", 322 | :arguments 323 | [{:node-type :argument, 324 | :argument-name "handles", 325 | :value {:values ["zuck" "cocacola"]}}], 326 | :selection-set 327 | [{:node-type :field, :field-name "handle"} 328 | {:node-type :inline-fragment, 329 | :type-condition {:type-name "User"}, 330 | :selection-set 331 | [{:node-type :field, 332 | :field-name "friends", 333 | :selection-set [{:node-type :field, :field-name "count"}]}]} 334 | {:node-type :inline-fragment, 335 | :type-condition {:type-name "Page"}, 336 | :selection-set 337 | [{:node-type :field, 338 | :field-name "likers", 339 | :selection-set 340 | [{:node-type :field, :field-name "count"}]}]}]}]}]} 341 | {:operation-definitions 342 | [{:section :operation-definitions, 343 | :node-type :operation-definition, 344 | :operation-type {:type "query", :name "HeroForEpisode"}, 345 | :variable-definitions 346 | [{:node-type :variable-definition, 347 | :variable-name "ep", 348 | :type-name "Episode", 349 | :required true}], 350 | :selection-set 351 | [{:node-type :field, 352 | :field-name "hero", 353 | :arguments 354 | [{:node-type :argument, 355 | :argument-name "episode", 356 | :variable-name "ep"}], 357 | :selection-set 358 | [{:node-type :field, :field-name "name"} 359 | {:node-type :inline-fragment, 360 | :type-condition {:type-name "Droid"}, 361 | :selection-set 362 | [{:node-type :field, :field-name "primaryFunction"}]}]}]}]} 363 | {:operation-definitions 364 | [{:section :operation-definitions, 365 | :node-type :operation-definition, 366 | :operation-type {:type "query", :name "inlineFragmentNoType"}, 367 | :variable-definitions 368 | [{:node-type :variable-definition, 369 | :variable-name "expandedInfo", 370 | :type-name "Boolean"}], 371 | :selection-set 372 | [{:node-type :field, 373 | :field-name "user", 374 | :arguments 375 | [{:node-type :argument, :argument-name "handle", :value "zuck"}], 376 | :selection-set 377 | [{:node-type :field, :field-name "id"} 378 | {:node-type :field, :field-name "name"} 379 | {:node-type :inline-fragment, 380 | :directives 381 | [{:node-type :directive, 382 | :name "include", 383 | :arguments 384 | [{:node-type :argument, 385 | :argument-name "if", 386 | :variable-name "expandedInfo"}]}], 387 | :selection-set 388 | [{:node-type :field, :field-name "firstName"} 389 | {:node-type :field, :field-name "lastName"} 390 | {:node-type :field, :field-name "birthday"}]}]}]}]} 391 | {:operation-definitions 392 | [{:section :operation-definitions, 393 | :node-type :operation-definition, 394 | :operation-type {:type "query", :name "getZuckProfile"}, 395 | :variable-definitions 396 | [{:node-type :variable-definition, 397 | :variable-name "devicePicSize", 398 | :type-name "Int"}], 399 | :selection-set 400 | [{:node-type :field, 401 | :field-name "user", 402 | :arguments 403 | [{:node-type :argument, :argument-name "id", :value 4}], 404 | :selection-set 405 | [{:node-type :field, :field-name "id"} 406 | {:node-type :field, :field-name "name"} 407 | {:node-type :field, 408 | :field-name "profilePic", 409 | :arguments 410 | [{:node-type :argument, 411 | :argument-name "size", 412 | :variable-name "devicePicSize"}]}]}]}]} 413 | {:operation-definitions 414 | [{:section :operation-definitions, 415 | :node-type :operation-definition, 416 | :operation-type {:type "query", :name "hasConditionalFragment"}, 417 | :variable-definitions 418 | [{:node-type :variable-definition, 419 | :variable-name "condition", 420 | :type-name "Boolean"}], 421 | :selection-set 422 | [{:node-type :fragment-spread, 423 | :name "maybeFragment", 424 | :directives 425 | [{:node-type :directive, 426 | :name "include", 427 | :arguments 428 | [{:node-type :argument, 429 | :argument-name "if", 430 | :variable-name "condition"}]}]}]}], 431 | :fragment-definitions 432 | [{:name "maybeFragment", 433 | :type-condition {:type-name "Query"}, 434 | :selection-set 435 | [{:node-type :field, 436 | :field-name "me", 437 | :selection-set [{:node-type :field, :field-name "name"}]}], 438 | :node-type :fragment-definition, 439 | :section :fragment-definitions}]} 440 | {:operation-definitions 441 | [{:section :operation-definitions, 442 | :node-type :operation-definition, 443 | :operation-type {:type "query", :name "hasConditionalFragment"}, 444 | :variable-definitions 445 | [{:node-type :variable-definition, 446 | :variable-name "condition", 447 | :type-name "Boolean"}], 448 | :selection-set 449 | [{:node-type :fragment-spread, :name "maybeFragment"}]}], 450 | :fragment-definitions 451 | [{:name "maybeFragment", 452 | :type-condition {:type-name "Query"}, 453 | :directives 454 | [{:node-type :directive, 455 | :name "include", 456 | :arguments 457 | [{:node-type :argument, 458 | :argument-name "if", 459 | :variable-name "condition"}]}], 460 | :selection-set 461 | [{:node-type :field, 462 | :field-name "me", 463 | :selection-set [{:node-type :field, :field-name "name"}]}], 464 | :node-type :fragment-definition, 465 | :section :fragment-definitions}]} 466 | {:operation-definitions 467 | [{:section :operation-definitions, 468 | :node-type :operation-definition, 469 | :operation-type {:type "query", :name "myQuery"}, 470 | :variable-definitions 471 | [{:node-type :variable-definition, 472 | :variable-name "someTest", 473 | :type-name "Boolean"}], 474 | :selection-set 475 | [{:node-type :field, 476 | :field-name "experimentalField", 477 | :directives 478 | [{:node-type :directive, 479 | :name "skip", 480 | :arguments 481 | [{:node-type :argument, 482 | :argument-name "if", 483 | :variable-name "someTest"}]}]}]}]} 484 | {:operation-definitions 485 | [{:section :operation-definitions, 486 | :node-type :operation-definition, 487 | :operation-type {:type "mutation", :name "setName"}, 488 | :selection-set 489 | [{:node-type :field, 490 | :field-name "setName", 491 | :arguments 492 | [{:node-type :argument, :argument-name "name", :value "Zuck"}], 493 | :selection-set [{:node-type :field, :field-name "newName"}]}]}]} 494 | {:operation-definitions 495 | [{:section :operation-definitions, 496 | :node-type :operation-definition, 497 | :operation-type {:type "query"}, 498 | :selection-set 499 | [{:node-type :field, 500 | :field-name "human", 501 | :arguments 502 | [{:node-type :argument, :argument-name "id", :value 1000}], 503 | :selection-set 504 | [{:node-type :field, :field-name "name"} 505 | {:node-type :field, 506 | :field-name "height", 507 | :arguments 508 | [{:node-type :argument, :argument-name "unit", :value "FOOT"}]} 509 | {:node-type :field, 510 | :field-name "weight", 511 | :arguments 512 | [{:node-type :argument, 513 | :argument-name "above", 514 | :value 100}]}]}]}]} 515 | {:operation-definitions 516 | [{:section :operation-definitions, 517 | :node-type :operation-definition, 518 | :operation-type {:type "query", :name "WithDefaultValues"}, 519 | :variable-definitions 520 | [{:node-type :variable-definition, 521 | :variable-name "a", 522 | :type-name "Int", 523 | :default-value 1} 524 | {:node-type :variable-definition, 525 | :variable-name "b", 526 | :type-name "String", 527 | :required true, 528 | :default-value "ok"} 529 | {:node-type :variable-definition, 530 | :variable-name "c", 531 | :type-name "ComplexInput", 532 | :default-value 533 | [:object-value 534 | [{:name "requiredField", :value true} 535 | {:name "intField", :value 3}]]}], 536 | :selection-set 537 | [{:node-type :field, 538 | :field-name "dog", 539 | :selection-set [{:node-type :field, :field-name "name"}]}]}]} 540 | {:operation-definitions 541 | [{:section :operation-definitions, 542 | :node-type :operation-definition, 543 | :operation-type {:type "query"}, 544 | :selection-set 545 | [{:node-type :field, 546 | :name "empireHero", 547 | :field-name "hero", 548 | :arguments 549 | [{:node-type :argument, 550 | :argument-name "episode", 551 | :value "EMPIRE"}], 552 | :selection-set [{:node-type :field, :field-name "name"}]} 553 | {:node-type :field, 554 | :name "jediHero", 555 | :field-name "hero", 556 | :arguments 557 | [{:node-type :argument, 558 | :argument-name "episode", 559 | :value "JEDI"}], 560 | :selection-set [{:node-type :field, :field-name "name"}]}]}]} 561 | {:operation-definitions 562 | [{:section :operation-definitions, 563 | :node-type :operation-definition, 564 | :operation-type {:type "query"}, 565 | :selection-set 566 | [{:node-type :field, 567 | :name "leftComparison", 568 | :field-name "hero", 569 | :arguments 570 | [{:node-type :argument, 571 | :argument-name "episode", 572 | :value "EMPIRE"}], 573 | :selection-set 574 | [{:node-type :fragment-spread, :name "comparisonFields"}]} 575 | {:node-type :field, 576 | :name "rightComparison", 577 | :field-name "hero", 578 | :arguments 579 | [{:node-type :argument, 580 | :argument-name "episode", 581 | :value "JEDI"}], 582 | :selection-set 583 | [{:node-type :fragment-spread, :name "comparisonFields"}]}]}], 584 | :fragment-definitions 585 | [{:name "comparisonFields", 586 | :type-condition {:type-name "Character"}, 587 | :selection-set 588 | [{:node-type :field, :field-name "name"} 589 | {:node-type :field, :field-name "appearsIn"} 590 | {:node-type :field, 591 | :field-name "friends", 592 | :selection-set [{:node-type :field, :field-name "name"}]}], 593 | :node-type :fragment-definition, 594 | :section :fragment-definitions}]} 595 | {:operation-definitions 596 | [{:section :operation-definitions, 597 | :node-type :operation-definition, 598 | :operation-type {:type "mutation"}, 599 | :selection-set 600 | [{:node-type :field, 601 | :field-name "createHuman", 602 | :arguments 603 | [{:node-type :argument, 604 | :argument-name "name", 605 | :variable-name "testname"} 606 | {:node-type :argument, 607 | :argument-name "friends", 608 | :value {:values []}}], 609 | :selection-set [{:node-type :field, :field-name "id"}]}]}]} 610 | {:operation-definitions 611 | [{:section :operation-definitions, 612 | :node-type :operation-definition, 613 | :operation-type {:type "query", :name "Complex"}, 614 | :selection-set 615 | [{:node-type :field, 616 | :field-name "callComplex", 617 | :arguments 618 | [{:node-type :argument, 619 | :argument-name "complexArgument", 620 | :value 621 | [:object-value 622 | [{:name "complexAttr1", :value 42} 623 | {:name "complexAttr2", :value 1}]]}], 624 | :selection-set [{:node-type :field, :field-name "xyzzy"}]}]}]} 625 | ) 626 | 627 | 628 | -------------------------------------------------------------------------------- /test/graphql_builder/resources/statements.edn: -------------------------------------------------------------------------------- 1 | [ 2 | ;; 1 3 | "query { 4 | user(id: 4) { 5 | id 6 | name 7 | profilePic(width: 100, height: 50.0) 8 | } 9 | }" 10 | ;; 2 11 | "query { 12 | user(id: 4) { 13 | id 14 | name 15 | profilePic(width: 100, height: 50) 16 | } 17 | }" 18 | ;; 3 19 | "query { 20 | user(id: 4) { 21 | name 22 | } 23 | }" 24 | ;; 4 25 | "mutation { 26 | likeStory(storyID: 12345) { 27 | story { 28 | likeCount 29 | } 30 | } 31 | }" 32 | ;; 5 33 | "query { 34 | me { 35 | id 36 | firstName 37 | lastName 38 | birthday { 39 | month 40 | day 41 | } 42 | friends { 43 | name 44 | } 45 | } 46 | }" 47 | 48 | ;; 6 49 | "query { 50 | me { 51 | name 52 | } 53 | } 54 | " 55 | ;; 7 56 | "query { 57 | user(id: 4) { 58 | name 59 | } 60 | }" 61 | ;; 8 62 | "query { 63 | user(id: 4) { 64 | id 65 | name 66 | profilePic(size: 100) 67 | } 68 | }" 69 | ;; 9 70 | "query { 71 | user(id: 4) { 72 | id 73 | name 74 | profilePic(width: 100, height: 50) 75 | } 76 | } 77 | " 78 | ;; 10 79 | ;;In this example, we can fetch two profile pictures of different sizes and ensure the resulting object will not have duplicate keys: 80 | "query { 81 | user(id: 4) { 82 | id 83 | name 84 | smallPic: profilePic(size: 64) 85 | bigPic: profilePic(size: 1024) 86 | } 87 | } 88 | " 89 | ;; 11 90 | ;; Since the top level of a query is a field, it also can be given an alias: 91 | "query { 92 | zuck: user(id: 4) { 93 | id 94 | name 95 | } 96 | } 97 | " 98 | ;; 12 99 | ;; Fragments allow for the reuse of common repeated selections of fields, reducing duplicated text in the document. Inline Fragments can be used directly within a selection to condition upon a type condition when querying against an interface or union. 100 | 101 | ;; For example, if we wanted to fetch some common information about mutual friends as well as friends of some user: 102 | "query noFragments { 103 | user(id: 4) { 104 | friends(first: 10) { 105 | id 106 | name 107 | profilePic(size: 50) 108 | } 109 | mutualFriends(first: 10) { 110 | id 111 | name 112 | profilePic(size: 50) 113 | } 114 | } 115 | }" 116 | ;; 13 117 | ;; The repeated fields could be extracted into a fragment and composed by a parent fragment or query. 118 | "query withFragments { 119 | user(id: 4) { 120 | friends(first: 10) { 121 | ...friendFields 122 | } 123 | mutualFriends(first: 10) { 124 | ...friendFields 125 | } 126 | } 127 | } 128 | fragment friendFields on User { 129 | id 130 | name 131 | profilePic(size: 50) 132 | } 133 | " 134 | ;; 14 135 | ;; Fragments are consumed by using the spread operator (...). All fields selected by the fragment will be added to the query field selection at the same level as the fragment invocation. This happens through multiple levels of fragment spreads. 136 | ;; For example: 137 | "query withNestedFragments { 138 | user(id: 4) { 139 | friends(first: 10) { 140 | ...friendFields 141 | } 142 | mutualFriends(first: 10) { 143 | ...friendFields 144 | } 145 | } 146 | } 147 | fragment friendFields on User { 148 | id 149 | name 150 | ...standardProfilePic 151 | } 152 | fragment standardProfilePic on User { 153 | profilePic(size: 50) 154 | } 155 | " 156 | ;; 15 157 | "query FragmentTyping { 158 | profiles(handles: [\"zuck\", \"cocacola\"]) { 159 | handle 160 | ...userFragment 161 | ...pageFragment 162 | } 163 | } 164 | fragment userFragment on User { 165 | friends { 166 | count 167 | } 168 | } 169 | fragment pageFragment on Page { 170 | likers { 171 | count 172 | } 173 | }" 174 | ;; 16 175 | "query FragmentTypingEnumArgs { 176 | profiles(handles: [zuck, cocacola]) { 177 | handle 178 | ...userFragment 179 | ...pageFragment 180 | } 181 | } 182 | fragment userFragment on User { 183 | friends { 184 | count 185 | } 186 | } 187 | fragment pageFragment on Page { 188 | likers { 189 | count 190 | } 191 | }" 192 | 193 | ;; 17 194 | "query inlineFragmentTyping { 195 | profiles(handles: [\"zuck\", \"cocacola\"]) { 196 | handle 197 | ... on User { 198 | friends { 199 | count 200 | } 201 | } 202 | ... on Page { 203 | likers { 204 | count 205 | } 206 | } 207 | } 208 | }" 209 | ;; 18 210 | "query HeroForEpisode($ep: Episode!) { 211 | hero(episode: $ep) { 212 | name 213 | ... on Droid { 214 | primaryFunction 215 | } 216 | } 217 | }" 218 | ;; 19 219 | "query inlineFragmentNoType($expandedInfo: Boolean) { 220 | user(handle: \"zuck\") { 221 | id 222 | name 223 | ... @include(if: $expandedInfo) { 224 | firstName 225 | lastName 226 | birthday 227 | } 228 | } 229 | }" 230 | ;; 20 231 | "query getZuckProfile($devicePicSize: Int) { 232 | user(id: 4) { 233 | id 234 | name 235 | profilePic(size: $devicePicSize) 236 | } 237 | }" 238 | ;; 21 239 | "query hasConditionalFragment($condition: Boolean) { 240 | ...maybeFragment @include(if: $condition) 241 | } 242 | fragment maybeFragment on Query { 243 | me { 244 | name 245 | } 246 | }" 247 | ;; 22 248 | "query hasConditionalFragment($condition: Boolean) { 249 | ...maybeFragment 250 | } 251 | fragment maybeFragment on Query @include(if: $condition) { 252 | me { 253 | name 254 | } 255 | }" 256 | ;; 23 257 | "query myQuery($someTest: Boolean) { 258 | experimentalField @skip(if: $someTest) 259 | }" 260 | ;; 24 261 | "mutation setName { 262 | setName(name: \"Zuck\") { 263 | newName 264 | } 265 | }" 266 | ;; 25 267 | "query { 268 | human(id: 1000) { 269 | name 270 | height(unit: FOOT) 271 | weight(above: 100) 272 | } 273 | }" 274 | ;; 26 275 | "query WithDefaultValues($a: Int = 1, $b: String! = \"ok\", $c: ComplexInput = { requiredField: true, intField: 3 }) { 276 | dog { 277 | name 278 | } 279 | }" 280 | ;; 27 281 | "query { 282 | empireHero: hero(episode: EMPIRE) { 283 | name 284 | } 285 | jediHero: hero(episode: JEDI) { 286 | name 287 | } 288 | }" 289 | ;; 28 290 | "query { 291 | leftComparison: hero(episode: EMPIRE) { 292 | ...comparisonFields 293 | } 294 | rightComparison: hero(episode: JEDI) { 295 | ...comparisonFields 296 | } 297 | } 298 | fragment comparisonFields on Character { 299 | name 300 | appearsIn 301 | friends { 302 | name 303 | } 304 | }" 305 | ;; 29 306 | "mutation { 307 | createHuman(name: $testname, friends: []) { 308 | id 309 | } 310 | }" 311 | ;; 30 312 | "query Complex { 313 | callComplex(complexArgument: { complexAttr1: 42, complexAttr2: 1 }) { 314 | xyzzy 315 | } 316 | }" 317 | 318 | ;; 31 319 | "query FragmentTypingMixedArgs { 320 | profiles(handles: [\"zuck\", cocacola]) { 321 | handle 322 | ...userFragment 323 | ...pageFragment 324 | } 325 | } 326 | fragment userFragment on User { 327 | friends { 328 | count 329 | } 330 | } 331 | fragment pageFragment on Page { 332 | likers { 333 | count 334 | } 335 | }" 336 | 337 | ] 338 | --------------------------------------------------------------------------------