├── .gitignore ├── Makefile ├── README.md ├── examples ├── albums.js ├── core.js ├── schema.gql └── starWarsData.js ├── package.json ├── project.clj ├── resources └── schema.gql ├── src └── graphql-tlc │ ├── common.cljs │ ├── consumer.cljs │ └── schema.cljs └── test └── graphql-tlc ├── core.cljs └── runner.cljs /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | **/*.iml 3 | out/ 4 | target/ 5 | node_modules/ 6 | .lein-failures 7 | .DS_Store 8 | 9 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all deps clean build test watch debug run 2 | 3 | all: build 4 | 5 | deps: 6 | lein deps 7 | 8 | clean: 9 | lein clean 10 | 11 | build: 12 | mkdir -p out/prod/resources 13 | cp resources/* out/prod/resources 14 | lein cljsbuild once main 15 | echo 'module.exports.getSchema = graphql_tlc.consumer.get_schema;' >> out/prod/graphql-tlc.js 16 | 17 | ONCE_FLAG=once 18 | test: 19 | mkdir -p out/test/resources 20 | cp resources/* out/test/resources 21 | lein doo node test $(ONCE_FLAG) 22 | 23 | watch: ONCE_FLAG= 24 | watch: test 25 | 26 | DEBUG_FLAG= 27 | debug: DEBUG_FLAG=--debug 28 | debug: run 29 | 30 | run: build 31 | cd out/prod/ && node graphql-tlc.js $(DEBUG_FLAG) 32 | 33 | publish: clean build 34 | npm publish 35 | 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # graphql-tlc 2 | 3 | ### A simpler interface to GraphQL 4 | 5 | DEPRECATED!!! 6 | 7 | This project is now hosted at: 8 | https://github.com/johanatan/speako 9 | 10 | And the NPM module is available at: 11 | https://www.npmjs.com/package/speako 12 | 13 | -------------------------------------------------------------------------------- /examples/albums.js: -------------------------------------------------------------------------------- 1 | 2 | var tlc = require('../'); 3 | var gql = require('graphql'); 4 | var labels = [ 5 | {'id': 1, 'name': 'Apple Records', 'founded': '1968'}, 6 | {'id': 2, 'name': 'Harvest Records', 'founded': '1969'}]; 7 | var albums = [ 8 | {'id': 1, 'name': 'Dark Side Of The Moon', 'releaseDate': 'March 1, 1973', 9 | 'artist': 'Pink Floyd', 'label': labels[1]}, 10 | {'id': 2, 'name': 'The Beatles', 'releaseDate': 'November 22, 1968', 11 | 'artist': 'The Beatles', 'label': labels[0]}, 12 | {'id': 3, 'name': 'The Wall', 'releaseDate': 'August 1, 1982', 13 | 'artist': 'Pink Floyd', 'label': labels[1]}]; 14 | var dataResolver = {"query": function (typename, predicate) { 15 | console.assert(typename == "Album"); 16 | if (predicate == "all()") return albums; 17 | else { 18 | var predicates = predicate.split("&"); 19 | var filters = predicates.map(function(p) { 20 | var [field, value] = p.split("="); 21 | var fields = field.split("."); 22 | if (fields.length == 2) { 23 | console.assert(fields[0] == "label"); 24 | return function(elem) { return elem[fields[0]][fields[1]] == value; }; 25 | } 26 | return function(elem) { return elem[field] == value; }; 27 | }); 28 | return albums.filter(function(elem) { return filters.every(function(f) { return f(elem); }); }); 29 | } 30 | }, "create": function (typename, inputs) { 31 | inputs.id = albums.length + 1; 32 | albums.push(inputs); 33 | return inputs; 34 | }}; 35 | var schema = 36 | tlc.getSchema(dataResolver, 37 | ["type Label { id: ID! name: String founded: String album: Album } ", 38 | "type Album { id: ID! name: String releaseDate: String artist: String label: Label }"].join(" ")); 39 | var printer = function(res) { console.log(JSON.stringify(res, null, 2)); }; 40 | gql.graphql(schema, 41 | "{ Album(artist: \"Pink Floyd\", label: { name: \"Harvest Records\" }) { name artist releaseDate } }") .then(printer); 42 | gql.graphql(schema, "{ Album(artist: \"Pink Floyd\", name: \"The Wall\") { name artist releaseDate } }").then(printer); 43 | gql.graphql(schema, "{ Album(id: 2) { name artist releaseDate } }").then(printer); 44 | gql.graphql(schema, "{ Albums { name artist releaseDate } }").then(printer); 45 | gql.graphql(schema, 46 | "mutation m { createAlbum(name:\"The Division Bell\", releaseDate: \"March 28, 1994\", artist:\"Pink Floyd\") { id name } }") 47 | .then(printer); 48 | -------------------------------------------------------------------------------- /examples/core.js: -------------------------------------------------------------------------------- 1 | 2 | var starWarsData = require('./starWarsData.js'); 3 | var tlc = require('../'); 4 | var gql = require('graphql'); 5 | 6 | var getters = {"Human": starWarsData.getHuman, "Droid": starWarsData.getDroid}; 7 | var dataResolver = {"query": function (typename, predicate) { 8 | var res = getters[typename].call(null, parseInt(predicate.split("=")[1])); 9 | return res; 10 | }}; 11 | var starWarsSchema = tlc.getSchema(dataResolver, "./schema.gql"); 12 | 13 | gql.graphql(starWarsSchema, "{ Human(id: 1000) { name }}").then(function (res) { 14 | console.log(res); 15 | }); 16 | -------------------------------------------------------------------------------- /examples/schema.gql: -------------------------------------------------------------------------------- 1 | enum Episode { NEWHOPE, EMPIRE, JEDI } 2 | 3 | type Human { 4 | id: ID! 5 | name: String 6 | friends: [Character] 7 | appearsIn: [Episode] 8 | homePlanet: String 9 | } 10 | 11 | type Droid { 12 | id: ID! 13 | name: String 14 | friends: [Character] 15 | appearsIn: [Episode] 16 | primaryFunction: String 17 | } 18 | 19 | union Character = Human | Droid -------------------------------------------------------------------------------- /examples/starWarsData.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015, Facebook, Inc. 3 | * All rights reserved. 4 | * LICENSE file in the root directory of this source tree. An additional grant 5 | * of patent rights can be found in the PATENTS file in the same directory. 6 | */ 7 | 8 | /** 9 | * This defines a basic set of data for our Star Wars Schema. 10 | * 11 | * This data is hard coded for the sake of the demo, but you could imagine 12 | * fetching this data from a backend service rather than from hardcoded 13 | * JSON objects in a more complex demo. 14 | */ 15 | 16 | var luke = { 17 | id: '1000', 18 | name: 'Luke Skywalker', 19 | friends: [ '1002', '1003', '2000', '2001' ], 20 | appearsIn: [ 4, 5, 6 ], 21 | homePlanet: 'Tatooine', 22 | }; 23 | 24 | var vader = { 25 | id: '1001', 26 | name: 'Darth Vader', 27 | friends: [ '1004' ], 28 | appearsIn: [ 4, 5, 6 ], 29 | homePlanet: 'Tatooine', 30 | }; 31 | 32 | var han = { 33 | id: '1002', 34 | name: 'Han Solo', 35 | friends: [ '1000', '1003', '2001' ], 36 | appearsIn: [ 4, 5, 6 ], 37 | }; 38 | 39 | var leia = { 40 | id: '1003', 41 | name: 'Leia Organa', 42 | friends: [ '1000', '1002', '2000', '2001' ], 43 | appearsIn: [ 4, 5, 6 ], 44 | homePlanet: 'Alderaan', 45 | }; 46 | 47 | var tarkin = { 48 | id: '1004', 49 | name: 'Wilhuff Tarkin', 50 | friends: [ '1001' ], 51 | appearsIn: [ 4 ], 52 | }; 53 | 54 | var humanData = { 55 | 1000: luke, 56 | 1001: vader, 57 | 1002: han, 58 | 1003: leia, 59 | 1004: tarkin, 60 | }; 61 | 62 | var threepio = { 63 | id: '2000', 64 | name: 'C-3PO', 65 | friends: [ '1000', '1002', '1003', '2001' ], 66 | appearsIn: [ 4, 5, 6 ], 67 | primaryFunction: 'Protocol', 68 | }; 69 | 70 | var artoo = { 71 | id: '2001', 72 | name: 'R2-D2', 73 | friends: [ '1000', '1002', '1003' ], 74 | appearsIn: [ 4, 5, 6 ], 75 | primaryFunction: 'Astromech', 76 | }; 77 | 78 | var droidData = { 79 | 2000: threepio, 80 | 2001: artoo, 81 | }; 82 | 83 | module.exports.getHero = function (episode) { 84 | if (episode === 5) { 85 | return luke; 86 | } 87 | return artoo; 88 | } 89 | 90 | module.exports.getHuman = function(id) { 91 | return humanData[id]; 92 | } 93 | 94 | module.exports.getDroid = function(id) { 95 | return droidData[id]; 96 | } 97 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-tlc", 3 | "preferGlobal": false, 4 | "version": "0.9.27", 5 | "author": "Jonathan Leonard ", 6 | "description": "A compiler (written in ClojureScript) for GraphQL type language.", 7 | "contributors": [ 8 | "Jonathan Leonard " 9 | ], 10 | "repository": { 11 | "type": "git", 12 | "url": "git://github.com/johanatan/graphql-type-lang-compiler.git" 13 | }, 14 | "license": "SEE LICENSE IN ", 15 | "dependencies": { 16 | "npm": ">= 1.1.2", 17 | "graphql": "0.8.2", 18 | "flat": "2.0.1", 19 | "source-map-support": "0.2.8" 20 | }, 21 | "devDependencies": {}, 22 | "optionalDependencies": {}, 23 | "engines": { 24 | "node": ">= 4.1.1" 25 | }, 26 | "homepage": "https://github.com/johanatan/graphql-type-lang-compiler", 27 | "main": "out/prod/graphql-tlc.js", 28 | "files": ["out/prod/graphql-tlc.js", "README.md", "examples/albums.js"], 29 | "tonicExampleFilename": "examples/albums.js" 30 | } 31 | 32 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject graphql-tlc "0.1.0-SNAPSHOT" 2 | :description "GraphQL Type Language Compiler" 3 | :dependencies [[org.clojure/clojure "1.7.0"] 4 | [org.clojure/core.match "0.3.0-alpha4"] 5 | [com.lucasbradstreet/cljs-uuid-utils "1.0.2"] 6 | [com.lucasbradstreet/instaparse-cljs "1.4.1.0"] 7 | [org.clojure/clojurescript "0.0-3308"]] 8 | :node-dependencies [[source-map-support "0.2.8"] 9 | [flat "2.0.1"] 10 | [graphql "0.8.2"]] 11 | :plugins [[lein-npm "0.4.0"] 12 | [lein-doo "0.1.5-SNAPSHOT"] 13 | [lein-cljsbuild "1.0.5"]] 14 | :hooks [leiningen.cljsbuild] 15 | :source-paths ["src" "target/classes"] 16 | :clean-targets ["out"] 17 | :cljsbuild { 18 | :builds { 19 | :test { 20 | :source-paths ["src" "test"] 21 | :compiler { 22 | :output-to "out/test/graphql-tlc.js" 23 | :output-dir "out/test" 24 | :main 'graphql-tlc.runner 25 | :optimizations :simple 26 | :pretty-print true 27 | :target :nodejs 28 | :hashbang false 29 | :cache-analysis true 30 | :source-map "out/test/graphql-tlc.js.map"}} 31 | :main { 32 | :source-paths ["src"] 33 | :compiler { 34 | :output-to "out/prod/graphql-tlc.js" 35 | ;:source-map "out/prod/graphql-tlc.js.map" 36 | :output-dir "out/prod" 37 | :optimizations :simple 38 | :pretty-print true 39 | :target :nodejs 40 | :hashbang false 41 | :cache-analysis true}}} 42 | :test-commands {:unit ["node" :node-runner "out/test/graphql-tlc.js"]}}) 43 | 44 | -------------------------------------------------------------------------------- /resources/schema.gql: -------------------------------------------------------------------------------- 1 | 2 | type Color { 3 | id: ID! 4 | name: String 5 | } 6 | -------------------------------------------------------------------------------- /src/graphql-tlc/common.cljs: -------------------------------------------------------------------------------- 1 | ;; Copyright (c) 2015 Jonathan L. Leonard 2 | 3 | (ns graphql-tlc.common 4 | (:require [goog.string.format] 5 | [cljs.pprint :as pprint] 6 | [clojure.string :as string] 7 | [cljs.nodejs :as node])) 8 | 9 | (enable-console-print!) 10 | 11 | (def fs (node/require "fs")) 12 | 13 | (def DEBUG (atom false)) 14 | (defn pprint-str [obj] (pprint/write obj :stream nil)) 15 | (defn dbg-print [fmt & args] (if @DEBUG 16 | (let [ppargs (map #(pprint-str %) args)] 17 | (console/log (apply (partial format fmt) ppargs))))) 18 | (defn dbg-banner-print [fmt & args] 19 | (let [banner (string/join (repeat 85 "="))] 20 | (dbg-print (string/join [banner "\n" fmt "\n" banner]) args))) 21 | (defn dbg-obj-print [obj] (console/log obj) obj) 22 | (defn dbg-obj-print-in [props obj] (console/log (apply (partial aget obj) props)) obj) 23 | (defn dbg-file [msg] ;; GraphQL eats console output occuring in our callbacks. 24 | (.appendFileSync fs "./debug.log" (format "%s\n" (pprint-str msg))) msg) 25 | 26 | (defn jskeys [jsobj] 27 | (.keys js/Object jsobj)) 28 | 29 | (defn format 30 | "Formats a string using goog.string.format." 31 | [fmt & args] 32 | (apply goog.string/format fmt args)) 33 | 34 | (defn single [col] (assert (= 1 (count col))) (first col)) 35 | 36 | (defn pluralize [noun] (format "%s%s" noun (if (goog.string/endsWith noun "s") "es" "s"))) 37 | -------------------------------------------------------------------------------- /src/graphql-tlc/consumer.cljs: -------------------------------------------------------------------------------- 1 | ;; Copyright (c) 2015 Jonathan L. Leonard 2 | 3 | (ns graphql-tlc.consumer 4 | (:require [graphql-tlc.common :as common] 5 | [graphql-tlc.schema :as schema] 6 | [clojure.set :as set] 7 | [clojure.walk :as walk] 8 | [clojure.string] 9 | [cljs.nodejs :as node])) 10 | 11 | (def ^:private gql (node/require "graphql")) 12 | (def ^:private flat (node/require "flat")) 13 | 14 | (defprotocol DataResolver 15 | (query [this typename predicate]) 16 | (create [this typename inputs]) 17 | (modify [this typename inputs]) 18 | (delete [this typename id])) 19 | 20 | (def ^:private primitive-types { 21 | "ID" gql.GraphQLID 22 | "Boolean" gql.GraphQLBoolean 23 | "String" gql.GraphQLString 24 | "Float" gql.GraphQLFloat 25 | "Int" gql.GraphQLInt }) 26 | 27 | (defn GraphQLConsumer [data-resolver] 28 | (let [type-map (atom primitive-types) 29 | inputs-map (atom {}) 30 | fields-map (atom {}) 31 | unions (atom []) 32 | enums (atom {})] 33 | (letfn [ 34 | (get-by-id [typename id] 35 | (query data-resolver typename (common/format "id=%s" (js->clj id)))) 36 | (get-by-fields [typename fields] 37 | (let [msg (clojure.string/join "&" (map #(common/format "%s=%s" %1 (js->clj (aget fields %1))) 38 | (common/jskeys fields)))] 39 | (query data-resolver typename msg))) 40 | (is-enum? [typ] 41 | (contains? (set (keys @enums)) typ)) 42 | (is-primitive? [typ] 43 | (or (is-enum? typ) (contains? (set (keys primitive-types)) typ))) 44 | (modify-type [typ is-list? is-not-null?] 45 | (let [list-comb (if is-list? #(new gql.GraphQLList %) identity) 46 | not-null-comb (if is-not-null? #(new gql.GraphQLNonNull %) identity) 47 | composed (comp not-null-comb list-comb) 48 | res (composed typ)] res)) 49 | (get-type [typ is-list? is-not-null?] (modify-type (get @type-map typ) is-list? is-not-null?)) 50 | (get-input-type [typ is-list? is-not-null?] (if (contains? (set (keys @inputs-map)) typ) 51 | (@inputs-map typ) 52 | (get-type typ is-list? is-not-null?))) 53 | (get-resolver [datatype is-list? fieldname] 54 | (if (not (is-primitive? datatype)) 55 | {:resolve (fn [parent] 56 | (if is-list? 57 | (clj->js (map (fn [id] (get-by-id datatype id)) (aget parent fieldname))) 58 | (get-by-id datatype (aget parent fieldname))))})) 59 | (get-field-spec [[fieldname datatype is-list? is-not-null?]] 60 | (let [typ (get-type datatype is-list? is-not-null?) 61 | resolver (get-resolver datatype is-list? fieldname) 62 | res {fieldname (merge {:type typ} resolver)}] res)) 63 | (input-object-typename [typename] (common/format "%sInput" typename)) 64 | (convert-to-input-object-field [created field] 65 | (let [wrappers (atom '()) 66 | field-type (atom nil)] 67 | (loop [ft (.-type field)] 68 | (if (.-ofType ft) 69 | (do 70 | (if (not= (type ft) gql.GraphQLNonNull) ; all fields in our input type are optional 71 | (swap! wrappers conj (.-constructor ft))) 72 | (recur (.-ofType ft))) 73 | (reset! field-type ft))) 74 | (cond 75 | (contains? (set (keys created)) @field-type) (reset! field-type (created @field-type)) 76 | (not (gql.isInputType @field-type)) (reset! field-type (create-input-object created @field-type))) 77 | (clj->js {:type (reduce (fn [typ clas] (clas. typ)) @field-type @wrappers)}))) 78 | (create-input-object [created object-type] 79 | (let [cell (atom nil) 80 | fields #(clj->js 81 | (into {} (for [[k v] (js->clj (.getFields object-type))] 82 | [k (convert-to-input-object-field (assoc created object-type @cell) (clj->js v))]))) 83 | res (gql.GraphQLInputObjectType. 84 | (clj->js {:name (common/format "%s%s" (input-object-typename (.-name object-type)) (gensym)) 85 | :fields fields}))] 86 | (reset! cell res)))] 87 | (reify schema/TypeConsumer 88 | (consume-object [_ typename field-descriptors] 89 | (let [field-specs (map get-field-spec field-descriptors) 90 | fieldnames (map first field-descriptors) 91 | merged (delay (apply merge field-specs)) 92 | descriptors {:name typename :fields #(clj->js @merged)} 93 | res (gql.GraphQLObjectType. (clj->js descriptors))] 94 | (assert (not (contains? @type-map typename)) 95 | (common/format "Duplicate type name: %s" typename)) 96 | (assert (not (contains? @fields-map fieldnames)) 97 | (common/format "Duplicate field set: %s" (common/pprint-str fieldnames))) 98 | (assert (contains? (set fieldnames) "id") 99 | (common/format "Type must contain an 'id' field. Fields: %s" 100 | (common/pprint-str fieldnames))) 101 | (swap! type-map assoc typename res) 102 | (swap! fields-map assoc fieldnames [typename (map rest field-descriptors)]) 103 | (console/log (common/format "Created object type thunk: %s" typename)) 104 | (swap! inputs-map assoc typename #(create-input-object {} res)) 105 | (console/log (common/format "Created input object type thunk: %s for type: %s" 106 | (input-object-typename typename) typename)))) 107 | (consume-union [_ typename constituents] 108 | (let [types (map #(get @type-map %) constituents) 109 | descriptor {:name typename :types types 110 | :resolveType (fn [value] (let [fields (keys (js->clj value))] 111 | (get @type-map (first (get @fields-map fields)))))} 112 | res (gql.GraphQLUnionType. (clj->js descriptor))] 113 | (swap! type-map assoc typename res) 114 | (swap! unions concat [typename]) 115 | (console/log (common/format "Created union type: %s: descriptor: %s" typename descriptor)) [res descriptor])) 116 | (consume-enum [_ typename constituents] 117 | (let [descriptor {:name typename :values (apply merge constituents)} 118 | res (gql.GraphQLEnumType. (clj->js descriptor))] 119 | (swap! type-map assoc typename res) 120 | (swap! enums assoc typename constituents) 121 | (console/log (common/format "Created enum type: %s: descriptor: %s" typename descriptor)) [res descriptor])) 122 | (finished [_] 123 | (let [union-input-type-desc { :name "Union" 124 | :fields { :type { :type (gql.GraphQLNonNull. gql.GraphQLString)} 125 | :id { :type (gql.GraphQLNonNull. gql.GraphQLID)}}} 126 | union-input-type (gql.GraphQLInputObjectType. (clj->js union-input-type-desc))] 127 | (letfn [ 128 | (get-query-descriptors [typ] 129 | (let [[field-names field-meta] (first (filter (fn [[k v]] (= (v 0) typ)) @fields-map)) 130 | zipped (map vector field-names (second field-meta)) 131 | with-types (map (fn [[k v]] [k (get-input-type (nth v 0) (nth v 1) false)]) zipped) 132 | args (apply merge (map #(do {(keyword (%1 0)) {:type (%1 1)}}) with-types)) 133 | res 134 | [{typ 135 | {:type (gql.GraphQLList. (get @type-map typ)) 136 | :args args 137 | :resolve (fn [root obj] (clj->js (get-by-fields typ (flat obj))))}} 138 | {(common/pluralize typ) 139 | {:type (gql.GraphQLList. (get @type-map typ)) 140 | :resolve (fn [root] (query data-resolver typ "all()"))}}]] 141 | (common/dbg-print "Query descriptors for typename: %s: %s" typ res) res)) 142 | (get-args [typ req-mod?] 143 | (letfn [ 144 | (get-ref-type [typ] (cond (is-enum? typ) (get @type-map typ) 145 | (contains? (set @unions) typ) union-input-type 146 | :else gql.GraphQLID)) 147 | (get-mutation-arg-type [[typ is-list? is-non-null?]] 148 | (modify-type (or (get primitive-types typ) (get-ref-type typ)) 149 | is-list? (if req-mod? is-non-null? false)))] 150 | (let [kvs (seq @fields-map) 151 | kv (common/single (filter #(= (first (second %)) typ) kvs)) 152 | pairs (partition 2 (interleave (first kv) (second (second kv)))) 153 | sans-id (remove #(= (first %) "id") pairs) 154 | res (map (fn [pair] {(first pair) {:type (get-mutation-arg-type (second pair))}}) sans-id)] res))) 155 | (get-mutations [typ] 156 | (letfn [ 157 | (get-id [o] (get o "id")) 158 | (get-mutation [prefix resolver req-mod? get-args? transform args] {(common/format "%s%s" prefix typ) { 159 | :type (get @type-map typ) 160 | :args (into args (if get-args? (get-args typ req-mod?) {})) 161 | :resolve (fn [root obj] (resolver data-resolver typ (transform (js->clj obj))))}})] 162 | (let [res [(get-mutation "create" create true true identity {}) 163 | (get-mutation "update" modify false true identity 164 | {:id {:type (gql.GraphQLNonNull. gql.GraphQLID)}}) 165 | (get-mutation "delete" delete false false get-id 166 | {:id {:type (gql.GraphQLNonNull. gql.GraphQLID)}})]] 167 | (common/dbg-print "Mutation descriptors for typename: %s: %s" typ res) res))) 168 | (create-obj-type [name fields] 169 | (let [descriptor { :name name :fields (apply merge (flatten fields))} 170 | res (gql.GraphQLObjectType. (clj->js descriptor))] 171 | (common/dbg-print "Created GraphQLObjectType: %s" descriptor) res))] 172 | (let [types (set/difference (set (keys @type-map)) (set (keys primitive-types)) (set (keys @enums)))] 173 | (common/dbg-print "Created union input type: %s" union-input-type-desc) 174 | (reset! inputs-map (into {} (for [[k v] @inputs-map] [k (v)]))) 175 | (gql.GraphQLSchema. 176 | (clj->js 177 | {:query (create-obj-type "RootQuery" (map get-query-descriptors types)) 178 | :mutation 179 | (create-obj-type "RootMutation" (map get-mutations (set/difference types (set @unions))))})))))))))) 180 | 181 | (defn- bail [msg] (fn [& _] (throw (js/Error. (common/format "Not implemented: '%s'." msg))))) 182 | 183 | (defn get-data-resolver [is-js? {:keys [query create modify delete] 184 | :or {query (bail "query") 185 | create (bail "create") 186 | modify (bail "modify") 187 | delete (bail "delete")}}] 188 | (reify DataResolver 189 | (query [_ typename predicate] (query typename predicate)) 190 | (create [_ typename inputs] (create typename (if is-js? (clj->js inputs) inputs))) 191 | (modify [_ typename inputs] (modify typename (if is-js? (clj->js inputs) inputs))) 192 | (delete [_ typename id] (delete typename id)))) 193 | 194 | (defn get-schema [resolver-methods schema-filename-or-contents] 195 | (let [is-js? (object? resolver-methods)] 196 | (first (second (schema/load-schema schema-filename-or-contents 197 | (GraphQLConsumer (get-data-resolver is-js? 198 | (if is-js? 199 | (walk/keywordize-keys (js->clj resolver-methods)) 200 | resolver-methods)))))))) 201 | 202 | (defn noop [] nil) 203 | 204 | (set! *main-cli-fn* noop) 205 | -------------------------------------------------------------------------------- /src/graphql-tlc/schema.cljs: -------------------------------------------------------------------------------- 1 | ;; Copyright (c) 2015 Jonathan L. Leonard 2 | 3 | (ns graphql-tlc.schema 4 | (:require [cljs.nodejs :as node] 5 | [cljs.core.match :refer-macros [match]] 6 | [graphql-tlc.common :as common] 7 | [instaparse.core :as insta])) 8 | 9 | (def fs (node/require "fs")) 10 | (def gql (node/require "graphql")) 11 | 12 | (def ^:private grammar 13 | " = TYPE+ 14 | TYPE = (OBJECT | UNION | ENUM) 15 | = TYPE_KEYWORD IDENTIFIER <'{'> FIELD+ <'}'> 16 | TYPE_KEYWORD = 'type' 17 | RWS = #'\\s+' 18 | WS = #'\\s*' 19 | IDENTIFIER = #'[a-zA-Z0-9_]+' 20 | FIELD = IDENTIFIER <':'> (LIST | DATATYPE) [NOTNULL] 21 | LIST = <'['> DATATYPE <']'> 22 | NOTNULL = <'!'> 23 | DATATYPE = 'ID' | 'Boolean' | 'String' | 'Float' | 'Int' | IDENTIFIER 24 | = UNION_KEYWORD IDENTIFIER <'='> IDENTIFIER OR_CLAUSE+ 25 | UNION_KEYWORD = 'union' 26 | = <'|'> IDENTIFIER 27 | = ENUM_KEYWORD IDENTIFIER <'{'> ENUM_VAL COMMA_ENUM_VAL+ <'}'> 28 | ENUM_KEYWORD = 'enum' 29 | ENUM_VAL = IDENTIFIER 30 | = <','> ENUM_VAL") 31 | 32 | (node/enable-util-print!) 33 | (def ^:private type-language-parser (insta/parser grammar :output-format :enlive)) 34 | 35 | (defn- extract-content [m] (get m :content)) 36 | (defn- extract-single-content [m] (common/single (extract-content m))) 37 | 38 | (defn- extract-field-descriptors [parsed] 39 | (assert (= :FIELD (get parsed :tag))) 40 | (let [content (extract-content parsed) 41 | [field-comp type-comp & not-null-comp] content 42 | fieldname (extract-single-content field-comp) 43 | is-list? (= :LIST (get type-comp :tag)) 44 | dt-content (extract-single-content ((if is-list? extract-single-content identity) type-comp)) 45 | datatype (extract-single-content dt-content) 46 | is-not-null? (= 1 (count not-null-comp))] 47 | [fieldname datatype is-list? is-not-null?])) 48 | 49 | (defn- get-object-descriptors [parsed] 50 | (assert (= {:tag :TYPE_KEYWORD :content (list "type")} (first parsed))) 51 | (let [[_ typename-comp & field-comps] parsed 52 | typename (extract-single-content typename-comp) 53 | field-descriptors (map extract-field-descriptors field-comps)] [typename field-descriptors])) 54 | 55 | (defn- get-union-descriptors [parsed] 56 | (assert (= {:tag :UNION_KEYWORD :content (list "union")} (first parsed))) 57 | (let [typename (extract-single-content (second parsed)) 58 | constituents (map #(extract-single-content %) (drop 2 parsed))] 59 | [typename constituents])) 60 | 61 | (defn- get-enum-descriptors [parsed] 62 | (assert (= {:tag :ENUM_KEYWORD, :content (list "enum")}) (first parsed)) 63 | (let [typename (extract-single-content (second parsed)) 64 | values (map-indexed (fn [i p] {(extract-single-content (extract-single-content p)) {:value (+ 1 i)}}) (drop 2 parsed))] 65 | [typename values])) 66 | 67 | (defprotocol TypeConsumer 68 | (consume-object [this typename field-descriptors]) 69 | (consume-union [this typename constituents]) 70 | (consume-enum [this typename constituents]) 71 | (finished [this])) 72 | 73 | (defmulti load-schema #(.existsSync fs %)) 74 | 75 | (defmethod load-schema true [filename & consumers] 76 | (apply load-schema (.toString (.readFileSync fs filename)) consumers)) 77 | 78 | (defmethod load-schema false [schema-str & consumers] 79 | (let [parsed (type-language-parser schema-str)] 80 | (common/dbg-banner-print "Parsed: %s" parsed) 81 | (doseq [p parsed] 82 | (assert (= :TYPE (get p :tag)) (common/format "Expected :TYPE. Actual: %s. Parsed: %s" (get p :tag) p)) 83 | (let [content (extract-content p) 84 | [impl descriptors] (match [(get (first content) :tag)] 85 | [:UNION_KEYWORD] [consume-union (get-union-descriptors content)] 86 | [:TYPE_KEYWORD] [consume-object (get-object-descriptors content)] 87 | [:ENUM_KEYWORD] [consume-enum (get-enum-descriptors content)])] 88 | (doseq [consumer consumers] 89 | (apply (partial impl consumer) descriptors)))) 90 | [schema-str (doall (map finished consumers))])) 91 | -------------------------------------------------------------------------------- /test/graphql-tlc/core.cljs: -------------------------------------------------------------------------------- 1 | ;; Copyright (c) 2015 Jonathan L. Leonard 2 | 3 | (ns graphql-tlc.test.core 4 | (:require [cljs.test :refer-macros [deftest is async]] 5 | [cljs.nodejs :as node] 6 | [graphql-tlc.consumer :as consumer])) 7 | 8 | (def gql (node/require "graphql")) 9 | 10 | (defn- call-graphql 11 | ([cb schema query] (call-graphql cb schema query nil)) 12 | ([cb schema query params] 13 | (.then ((.-graphql gql) schema query params) (fn [res] 14 | (cb (.stringify js/JSON res nil 2)))))) 15 | 16 | (defn file-schema [resolver] (consumer/get-schema resolver "./resources/schema.gql")) 17 | 18 | (deftest loads-schema-from-file (is (file-schema {}))) 19 | 20 | (deftest simple-select 21 | (async done 22 | (let [expected {"data" {"Colors" nil}} 23 | comparator (fn [s] (is (= expected (js->clj (.parse js/JSON s)))) (done)) 24 | resolver {:query (fn [typename predicate] 25 | (is (and (= typename "Color") (= predicate "all()"))) nil)}] 26 | (call-graphql comparator (file-schema resolver) "{ Colors { id name } }")))) 27 | -------------------------------------------------------------------------------- /test/graphql-tlc/runner.cljs: -------------------------------------------------------------------------------- 1 | ;; Copyright (c) 2015 Jonathan L. Leonard 2 | 3 | (ns graphql-tlc.runner 4 | (:require [cljs.test :as test] 5 | [doo.runner :refer-macros [doo-all-tests doo-tests]] 6 | [graphql-tlc.test.core])) 7 | 8 | (doo-tests 'graphql-tlc.test.core) 9 | 10 | --------------------------------------------------------------------------------