├── project ├── build.properties ├── plugins.sbt └── Webpack.scala ├── app ├── javascripts │ ├── index.js │ ├── css │ │ ├── format.css │ │ ├── SchemaEditor.css │ │ ├── index.css │ │ ├── proxy.css │ │ └── graphiql.css │ ├── proxy.jsx │ ├── graphiql-workspace.jsx │ ├── format.jsx │ ├── utility │ │ ├── find.js │ │ ├── debounce.js │ │ ├── elementPosition.js │ │ ├── CodeMirrorSizer.js │ │ ├── getSelectedOperationName.js │ │ ├── getQueryFacts.js │ │ ├── introspectionQueries.js │ │ ├── onHasCompletion.js │ │ └── fillLeafs.js │ ├── SchemaEditor.jsx │ ├── GraphQLFormatter.jsx │ └── GraphQLProxy.jsx ├── views │ ├── font.scala.html │ ├── ga.scala.html │ ├── main.scala.html │ ├── workspace.scala.html │ └── index.scala.html ├── Filters.scala └── controllers │ ├── JsonPath.scala │ ├── Application.scala │ └── Materializer.scala ├── public └── images │ ├── favicon.png │ ├── sangria-relay-logo.png │ ├── graphql-logo.svg │ ├── graphql-logo-1.svg │ ├── sangria-logo-1.svg │ ├── sangria-logo.svg │ ├── sangria-relay-logo.svg │ ├── sip-of-sangria.svg │ └── sip-of-sangria-1.svg ├── .gitignore ├── LICENSE ├── webpack.sbt ├── README.md ├── conf ├── logback.xml ├── routes └── application.conf ├── package.json └── webpack.config.js /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.13.16 2 | -------------------------------------------------------------------------------- /app/javascripts/index.js: -------------------------------------------------------------------------------- 1 | import "hover.css/css/hover-min.css" 2 | import "./css/index.css" 3 | -------------------------------------------------------------------------------- /public/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OlegIlyenko/graphql-toolbox/HEAD/public/images/favicon.png -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.3") 2 | addSbtPlugin("com.heroku" % "sbt-heroku" % "0.4.3") -------------------------------------------------------------------------------- /public/images/sangria-relay-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OlegIlyenko/graphql-toolbox/HEAD/public/images/sangria-relay-logo.png -------------------------------------------------------------------------------- /app/views/font.scala.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/javascripts/css/format.css: -------------------------------------------------------------------------------- 1 | body { 2 | height: 100%; 3 | margin: 0; 4 | width: 100%; 5 | overflow: hidden; 6 | } 7 | 8 | #formatter { 9 | height: 100vh; 10 | } 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/Filters.scala: -------------------------------------------------------------------------------- 1 | import javax.inject.Inject 2 | 3 | import play.api.http.HttpFilters 4 | import play.filters.cors.CORSFilter 5 | 6 | class Filters @Inject()(corsFilter: CORSFilter) extends HttpFilters { 7 | def filters = Seq(corsFilter) 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /graphql.schema.json 2 | /graphql.config.json 3 | logs 4 | target 5 | /.idea 6 | /.idea_modules 7 | /.classpath 8 | /.project 9 | /.settings 10 | /RUNNING_PID 11 | /node_modules 12 | 13 | *.iml 14 | 15 | # webpack output 16 | /public/*.* -------------------------------------------------------------------------------- /app/javascripts/proxy.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import {GraphQLProxy} from './GraphQLProxy.jsx'; 4 | 5 | import './css/proxy.css' 6 | 7 | ReactDOM.render(, document.getElementById('proxy')); -------------------------------------------------------------------------------- /app/views/ga.scala.html: -------------------------------------------------------------------------------- 1 | @(gaCode: Option[String]) 2 | 3 | @if(gaCode.isDefined) { 4 | 13 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This software is licensed under the Apache 2 license, quoted below. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this project except in compliance with 4 | the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. 5 | 6 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an 7 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific 8 | language governing permissions and limitations under the License. -------------------------------------------------------------------------------- /app/javascripts/graphiql-workspace.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import {GraphiQLWorkspace, AppConfig} from 'graphiql-workspace'; 4 | 5 | import 'graphiql-workspace/graphiql-workspace.css' 6 | import 'graphiql/graphiql.css' 7 | 8 | module.exports.setupGraphiQLWorkspace = function (bootstrapOptions = {}) { 9 | this.bootstrapOptions = bootstrapOptions; 10 | const config = new AppConfig("graphiql", bootstrapOptions); 11 | ReactDOM.render(, document.getElementById('graphiql-workspace')); 12 | } 13 | -------------------------------------------------------------------------------- /app/javascripts/format.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import {GraphQLFormatter} from './GraphQLFormatter.jsx'; 4 | 5 | import './css/format.css' 6 | 7 | function formatter(value) { 8 | return fetch('/format-query', { 9 | method: 'post', 10 | headers: { 11 | 'Accept': 'text/plain', 12 | 'Content-Type': 'text/plain', 13 | }, 14 | body: value, 15 | credentials: 'include', 16 | }).then(function (response) { 17 | return response.text(); 18 | }); 19 | } 20 | 21 | ReactDOM.render(, document.getElementById('formatter')); -------------------------------------------------------------------------------- /webpack.sbt: -------------------------------------------------------------------------------- 1 | import play.sbt.PlayImport.PlayKeys._ 2 | import sbt.Keys._ 3 | 4 | playRunHooks <+= baseDirectory.map(Webpack.apply) 5 | 6 | lazy val webpack = taskKey[Unit]("Run webpack when packaging the application") 7 | 8 | def runWebpack(file: File) = { 9 | Process("node_modules/.bin/webpack" + sys.props.get("os.name").filter(_.toLowerCase.contains("windows")).map(_ => ".cmd").getOrElse(""), file) ! 10 | } 11 | 12 | webpack := { 13 | if(runWebpack(baseDirectory.value) != 0) throw new Exception("Something goes wrong when running webpack.") 14 | } 15 | 16 | dist <<= dist dependsOn webpack 17 | 18 | stage <<= stage dependsOn webpack -------------------------------------------------------------------------------- /app/javascripts/utility/find.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | /* eslint-disable no-undef */ 3 | /** 4 | * Copyright (c) 2015, Facebook, Inc. 5 | * All rights reserved. 6 | * 7 | * This source code is licensed under the BSD-style license found in the 8 | * LICENSE file in the root directory of this source tree. An additional grant 9 | * of patent rights can be found in the PATENTS file in the same directory. 10 | */ 11 | 12 | export default function find( 13 | list: Array, 14 | predicate: (item: T) => boolean 15 | ): ?T { 16 | for (let i = 0; i < list.length; i++) { 17 | if (predicate(list[i])) { 18 | return list[i]; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## GraphQL Toolbox 2 | 3 | A set of [GraphQL](https://facebook.github.io/graphql) tools to help with GraphQL server and client development. 4 | 5 | ### Getting Started 6 | 7 | The prerequisites are [SBT](http://www.scala-sbt.org/download.html), [Java 8](http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html), [npm](https://www.npmjs.com/). 8 | 9 | In order to run the server locally you need to execute following commands: 10 | 11 | ```bash 12 | $ npm install 13 | $ sbt run 14 | ``` 15 | 16 | This will run the Play app in a dev mode which means that it will pick up all of the changes to a source code (this also includes resources managed by webpack). 17 | -------------------------------------------------------------------------------- /app/javascripts/utility/debounce.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the 6 | * LICENSE-examples file in the root directory of this source tree. 7 | */ 8 | 9 | /** 10 | * Provided a duration and a function, returns a new function which is called 11 | * `duration` milliseconds after the last call. 12 | */ 13 | export default function debounce(duration, fn) { 14 | let timeout; 15 | return function () { 16 | clearTimeout(timeout); 17 | timeout = setTimeout(() => { 18 | timeout = null; 19 | fn.apply(this, arguments); 20 | }, duration); 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /conf/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %coloredLevel - %logger - %message%n%xException 8 | 9 | 10 | 11 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/javascripts/utility/elementPosition.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the 6 | * LICENSE-examples file in the root directory of this source tree. 7 | */ 8 | 9 | /** 10 | * Utility functions to get a pixel distance from left/top of the window. 11 | */ 12 | 13 | export function getLeft(initialElem) { 14 | let pt = 0; 15 | let elem = initialElem; 16 | while (elem.offsetParent) { 17 | pt += elem.offsetLeft; 18 | elem = elem.offsetParent; 19 | } 20 | return pt; 21 | } 22 | 23 | export function getTop(initialElem) { 24 | let pt = 0; 25 | let elem = initialElem; 26 | while (elem.offsetParent) { 27 | pt += elem.offsetTop; 28 | elem = elem.offsetParent; 29 | } 30 | return pt; 31 | } 32 | -------------------------------------------------------------------------------- /conf/routes: -------------------------------------------------------------------------------- 1 | # Routes 2 | # This file defines all application routes (Higher priority routes first) 3 | # ~~~~ 4 | 5 | # Home page 6 | GET / controllers.Application.index 7 | 8 | GET /proxy controllers.Application.proxy 9 | POST /graphql-proxy controllers.Application.graphqlProxy 10 | 11 | GET /format controllers.Application.format 12 | GET /format-query controllers.Application.formatGet(query: String) 13 | POST /format-query controllers.Application.formatPost 14 | 15 | GET /graphiql controllers.Application.graphiql 16 | 17 | POST /proxy-graphql-request controllers.Application.proxyRequest 18 | 19 | # Map static resources from the /public folder to the /assets URL path 20 | GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset) 21 | -------------------------------------------------------------------------------- /app/javascripts/css/SchemaEditor.css: -------------------------------------------------------------------------------- 1 | .codemirrorWrap { 2 | -webkit-flex: 1; 3 | flex: 1; 4 | position: relative; 5 | } 6 | 7 | .CodeMirror-lines { 8 | padding: 20px 0; 9 | } 10 | .result-window .CodeMirror { 11 | background: #f6f7f8; 12 | } 13 | 14 | .result-window .CodeMirror-gutters { 15 | background-color: #eeeeee; 16 | border-color: #e0e0e0; 17 | cursor: col-resize; 18 | } 19 | 20 | .result-window .CodeMirror-foldgutter, 21 | .result-window .CodeMirror-foldgutter-open:after, 22 | .result-window .CodeMirror-foldgutter-folded:after { 23 | padding-left: 3px; 24 | } 25 | 26 | div.CodeMirror span.CodeMirror-matchingbracket { 27 | color: #555; 28 | text-decoration: underline; 29 | } 30 | 31 | div.CodeMirror span.CodeMirror-nonmatchingbracket { 32 | color: #f00; 33 | } 34 | 35 | .resultWrap .CodeMirror-gutters { 36 | cursor: col-resize; 37 | } 38 | 39 | .resultWrap .CodeMirror { 40 | background: #f6f7f8; 41 | } -------------------------------------------------------------------------------- /app/javascripts/utility/CodeMirrorSizer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the 6 | * LICENSE-examples file in the root directory of this source tree. 7 | */ 8 | 9 | import ReactDOM from 'react-dom'; 10 | 11 | 12 | /** 13 | * When a containing DOM node's height has been altered, trigger a resize of 14 | * the related CodeMirror instance so that it is always correctly sized. 15 | */ 16 | export default class CodeMirrorSizer { 17 | constructor() { 18 | this.sizes = []; 19 | } 20 | 21 | updateSizes(components) { 22 | components.forEach((component, i) => { 23 | const size = ReactDOM.findDOMNode(component).clientHeight; 24 | if (i <= this.sizes.length && size !== this.sizes[i]) { 25 | component.getCodeMirror().setSize(); 26 | } 27 | this.sizes[i] = size; 28 | }); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /project/Webpack.scala: -------------------------------------------------------------------------------- 1 | import java.net.InetSocketAddress 2 | 3 | import play.sbt.PlayRunHook 4 | import sbt._ 5 | 6 | object Webpack { 7 | def apply(base: File): PlayRunHook = { 8 | object WebpackHook extends PlayRunHook { 9 | var process: Option[Process] = None 10 | 11 | override def beforeStarted() = { 12 | process = Option( 13 | Process("node_modules/.bin/webpack" + sys.props.get("os.name").filter(_.toLowerCase.contains("windows")).map(_ => ".cmd").getOrElse(""), base).run() 14 | ) 15 | } 16 | 17 | override def afterStarted(addr: InetSocketAddress) = { 18 | afterStopped() 19 | process = Option( 20 | Process("node_modules/.bin/webpack" + sys.props.get("os.name").filter(_.toLowerCase.contains("windows")).map(_ => ".cmd").getOrElse("") + " --watch", base).run() 21 | ) 22 | } 23 | 24 | override def afterStopped() = { 25 | process.foreach(_.destroy()) 26 | process = None 27 | } 28 | } 29 | 30 | WebpackHook 31 | } 32 | } -------------------------------------------------------------------------------- /app/views/main.scala.html: -------------------------------------------------------------------------------- 1 | @(title: String, gaCode: Option[String])(content: Html) 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | @title 13 | 14 | @font() 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | @content 23 | 24 | 25 | 26 | @ga(gaCode) 27 | 28 | -------------------------------------------------------------------------------- /app/views/workspace.scala.html: -------------------------------------------------------------------------------- 1 | @(title: String, name: String, id: String, gaCode: Option[String], includeFont: Boolean = true) 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | @title 12 | 13 | @if(includeFont) { 14 | @font() 15 | } 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 29 | @ga(gaCode) 30 | 31 | -------------------------------------------------------------------------------- /app/javascripts/utility/getSelectedOperationName.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the 6 | * LICENSE-examples file in the root directory of this source tree. 7 | */ 8 | 9 | /** 10 | * Provided optional previous operations and selected name, and a next list of 11 | * operations, determine what the next selected operation should be. 12 | */ 13 | export default function getSelectedOperationName( 14 | prevOperations, 15 | prevSelectedOperationName, 16 | operations 17 | ) { 18 | // If there are not enough operations to bother with, return nothing. 19 | if (!operations || operations.length < 1) { 20 | return; 21 | } 22 | 23 | // If a previous selection still exists, continue to use it. 24 | const names = operations.map(op => op.name && op.name.value); 25 | if (prevSelectedOperationName && 26 | names.indexOf(prevSelectedOperationName) !== -1) { 27 | return prevSelectedOperationName; 28 | } 29 | 30 | // If a previous selection was the Nth operation, use the same Nth. 31 | if (prevSelectedOperationName && prevOperations) { 32 | const prevNames = prevOperations.map(op => op.name && op.name.value); 33 | const prevIndex = prevNames.indexOf(prevSelectedOperationName); 34 | if (prevIndex && prevIndex < names.length) { 35 | return names[prevIndex]; 36 | } 37 | } 38 | 39 | // Use the first operation. 40 | return names[0]; 41 | } 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-toolbox", 3 | "description": "GraphQL Toolbox", 4 | "version": "0.1.0", 5 | "dependencies": { 6 | "bootstrap-sass": "^3.3.6", 7 | "brace": "^0.7.0", 8 | "classnames": "2.2.3", 9 | "codemirror": "^5.16.0", 10 | "codemirror-graphql": "^0.5.2", 11 | "graphiql": "^0.11.5", 12 | "graphql": "^0.10.0", 13 | "graphql-relay": "0.5.1", 14 | "historyjs": "^1.8.0-b2", 15 | "hover.css": "^2.0.2", 16 | "jquery": "^2.2.0", 17 | "jquery.hotkeys": "^0.1.0", 18 | "lodash": "^4.13.1", 19 | "moment": "^2.14.1", 20 | "react": "15.4.2", 21 | "react-bootstrap": "^0.30.7", 22 | "react-codemirror": "^0.3.0", 23 | "react-dom": "15.4.2", 24 | "react-dropzone": "^3.10.0", 25 | "react-relay": "0.10.0", 26 | "urijs": "^1.17.0", 27 | "whatwg-fetch": "^1.0.0", 28 | "graphiql-workspace": "1.1.1" 29 | }, 30 | "devDependencies": { 31 | "babel-core": "6.23.1", 32 | "babel-loader": "6.3.2", 33 | "babel-polyfill": "6.23.0", 34 | "babel-preset-es2015": "6.22.0", 35 | "babel-preset-react": "6.23.0", 36 | "babel-preset-stage-0": "6.22.0", 37 | "babel-relay-plugin": "0.11.0", 38 | "bootstrap-loader": "^2.0.0-beta.21", 39 | "css-loader": "^0.26.1", 40 | "extract-text-webpack-plugin": "^2.0.0-rc.3", 41 | "file-loader": "^0.10.0", 42 | "imports-loader": "^0.7.0", 43 | "node-sass": "^4.5.0", 44 | "resolve-url-loader": "^2.0.0", 45 | "sass-loader": "^6.0.1", 46 | "style-loader": "^0.13.1", 47 | "url-loader": "^0.5.7", 48 | "webpack": "2.2.1" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /conf/application.conf: -------------------------------------------------------------------------------- 1 | # This is the main configuration file for the application. 2 | # ~~~~~ 3 | 4 | # Secret key 5 | # ~~~~~ 6 | # The secret key is used to secure cryptographics functions. 7 | # 8 | # This must be changed for production, but we recommend not changing it in this file. 9 | # 10 | # See http://www.playframework.com/documentation/latest/ApplicationSecret for more details. 11 | play.crypto.secret="changeme" 12 | play.crypto.secret=${?APPLICATION_SECRET} 13 | 14 | # The application languages 15 | # ~~~~~ 16 | play.i18n.langs = ["en"] 17 | 18 | # Router 19 | # ~~~~~ 20 | # Define the Router object to use for this application. 21 | # This router will be looked up first when the application is starting up, 22 | # so make sure this is the entry point. 23 | # Furthermore, it's assumed your route file is named properly. 24 | # So for an application router like `my.application.Router`, 25 | # you may need to define a router file `conf/my.application.routes`. 26 | # Default to Routes in the root package (and conf/routes) 27 | # play.http.router = my.application.Routes 28 | 29 | # Database configuration 30 | # ~~~~~ 31 | # You can declare as many datasources as you want. 32 | # By convention, the default datasource is named `default` 33 | # 34 | # db.default.driver=org.h2.Driver 35 | # db.default.url="jdbc:h2:mem:play" 36 | # db.default.username=sa 37 | # db.default.password="" 38 | 39 | # Evolutions 40 | # ~~~~~ 41 | # You can disable evolutions if needed 42 | # play.evolutions.enabled=false 43 | 44 | # You can disable evolutions for a specific datasource if necessary 45 | # play.evolutions.db.default.enabled=false 46 | -------------------------------------------------------------------------------- /public/images/graphql-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | -------------------------------------------------------------------------------- /public/images/graphql-logo-1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | -------------------------------------------------------------------------------- /app/javascripts/utility/getQueryFacts.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the 6 | * LICENSE-examples file in the root directory of this source tree. 7 | */ 8 | 9 | import { parse, typeFromAST } from 'graphql'; 10 | 11 | 12 | /** 13 | * Provided previous "queryFacts", a GraphQL schema, and a query document 14 | * string, return a set of facts about that query useful for GraphiQL features. 15 | * 16 | * If the query cannot be parsed, returns undefined. 17 | */ 18 | export default function getQueryFacts(schema, documentStr) { 19 | if (!documentStr) { 20 | return; 21 | } 22 | 23 | let documentAST; 24 | try { 25 | documentAST = parse(documentStr); 26 | } catch (e) { 27 | return; 28 | } 29 | 30 | const variableToType = schema ? collectVariables(schema, documentAST) : null; 31 | 32 | // Collect operations by their names. 33 | const operations = []; 34 | documentAST.definitions.forEach(def => { 35 | if (def.kind === 'OperationDefinition') { 36 | operations.push(def); 37 | } 38 | }); 39 | 40 | return { variableToType, operations }; 41 | } 42 | 43 | /** 44 | * Provided a schema and a document, produces a `variableToType` Object. 45 | */ 46 | export function collectVariables(schema, documentAST) { 47 | const variableToType = Object.create(null); 48 | documentAST.definitions.forEach(definition => { 49 | if (definition.kind === 'OperationDefinition') { 50 | const variableDefinitions = definition.variableDefinitions; 51 | if (variableDefinitions) { 52 | variableDefinitions.forEach(({ variable, type }) => { 53 | const inputType = typeFromAST(schema, type); 54 | if (inputType) { 55 | variableToType[variable.name.value] = inputType; 56 | } 57 | }); 58 | } 59 | } 60 | }); 61 | return variableToType; 62 | } 63 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 2 | const webpack = require("webpack"); 3 | 4 | var PROD = JSON.parse(process.env.PROD_ENV || 'false'); 5 | 6 | module.exports = { 7 | entry: { 8 | format: ['whatwg-fetch', "./app/javascripts/format.jsx"], 9 | proxy: ['whatwg-fetch', "./app/javascripts/proxy.jsx"], 10 | graphiql: ['whatwg-fetch', "./app/javascripts/graphiql-workspace.jsx"], 11 | index: ["./app/javascripts/index.js"] 12 | }, 13 | 14 | devtool: 'source-map', 15 | 16 | output: { 17 | path: __dirname + "/public/", 18 | publicPath: "/assets/", 19 | filename: "[name].js", 20 | library: '[name]' 21 | }, 22 | 23 | resolve: { 24 | extensions: ['.webpack.js', '.web.js', '.ts', '.js'], 25 | alias: { 26 | history: 'historyjs/scripts/bundled-uncompressed/html4+html5/jquery.history' 27 | } 28 | }, 29 | 30 | plugins: [ 31 | new ExtractTextPlugin({filename: '[name].css', allChunks: true}), 32 | new webpack.ProvidePlugin({"window.jQuery": "jquery"}) 33 | ].concat( 34 | PROD ? [new webpack.optimize.UglifyJsPlugin({compress: {warnings: false }})] : [] 35 | ), 36 | 37 | watchOptions: { 38 | aggregateTimeout: 10 39 | }, 40 | 41 | module: { 42 | rules: [ 43 | { 44 | test: /\.jsx?$/, exclude: /node_modules/, loader: 'babel-loader', 45 | query: {presets: ['react', 'es2015', 'stage-0']} 46 | }, 47 | {test: /\.scss$/, loader: ExtractTextPlugin.extract({fallback: 'style-loader', loader: 'css-loader?sourceMap!sass-loader?sourceMap'})}, 48 | {test: /\.css$/, loader: ExtractTextPlugin.extract({fallback: 'style-loader', loader: 'css-loader'})}, 49 | {test: /\.(woff2?|ttf|eot|svg)$/, loader: 'url-loader?limit=10000'}, 50 | {test: /bootstrap-sass[\\\/].*\.js/, loader: 'imports-loader?jQuery=jquery'}, 51 | {test: /jquery.hotkeys[\\\/].*\.js/, loader: 'imports-loader?jQuery=jquery'} 52 | ] 53 | } 54 | }; -------------------------------------------------------------------------------- /app/javascripts/utility/introspectionQueries.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | export { introspectionQuery } from 'graphql'; 11 | 12 | // Some GraphQL services do not support subscriptions and fail an introspection 13 | // query which includes the `subscriptionType` field as the stock introspection 14 | // query does. This backup query removes that field. 15 | export const introspectionQuerySansSubscriptions = ` 16 | query IntrospectionQuery { 17 | __schema { 18 | queryType { name } 19 | mutationType { name } 20 | types { 21 | ...FullType 22 | } 23 | directives { 24 | name 25 | description 26 | locations 27 | args { 28 | ...InputValue 29 | } 30 | } 31 | } 32 | } 33 | 34 | fragment FullType on __Type { 35 | kind 36 | name 37 | description 38 | fields(includeDeprecated: true) { 39 | name 40 | description 41 | args { 42 | ...InputValue 43 | } 44 | type { 45 | ...TypeRef 46 | } 47 | isDeprecated 48 | deprecationReason 49 | } 50 | inputFields { 51 | ...InputValue 52 | } 53 | interfaces { 54 | ...TypeRef 55 | } 56 | enumValues(includeDeprecated: true) { 57 | name 58 | description 59 | isDeprecated 60 | deprecationReason 61 | } 62 | possibleTypes { 63 | ...TypeRef 64 | } 65 | } 66 | 67 | fragment InputValue on __InputValue { 68 | name 69 | description 70 | type { ...TypeRef } 71 | defaultValue 72 | } 73 | 74 | fragment TypeRef on __Type { 75 | kind 76 | name 77 | ofType { 78 | kind 79 | name 80 | ofType { 81 | kind 82 | name 83 | ofType { 84 | kind 85 | name 86 | ofType { 87 | kind 88 | name 89 | ofType { 90 | kind 91 | name 92 | ofType { 93 | kind 94 | name 95 | ofType { 96 | kind 97 | name 98 | } 99 | } 100 | } 101 | } 102 | } 103 | } 104 | } 105 | } 106 | `; 107 | -------------------------------------------------------------------------------- /app/javascripts/css/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #FFFFFF; 3 | font-family: proxima-nova, "Helvetica Neue", Helvetica, Arial, sans-serif; 4 | } 5 | 6 | .header { 7 | background-color: #D45CA2; 8 | color: #FFFFFF; 9 | } 10 | 11 | .header .logo-text, header p { 12 | opacity: 0.8; 13 | } 14 | 15 | .header .logo-text { 16 | font-size: 64px; 17 | font-weight: 400; 18 | display: inline-block; 19 | vertical-align: middle; 20 | } 21 | 22 | .header p { 23 | opacity: 0.8; 24 | } 25 | 26 | .logo-wrap { 27 | padding-top: 30px; 28 | padding-bottom: 30px; 29 | opacity: 0.8; 30 | text-align: center; 31 | } 32 | 33 | .logo-wrap img { 34 | width: 300px; 35 | opacity: 0.7; 36 | display: inline-block; 37 | vertical-align: middle; 38 | padding-left: 40px; 39 | padding-right: 40px; 40 | } 41 | 42 | p { 43 | font-size: 20px; 44 | } 45 | 46 | .header { 47 | padding-bottom: 30px; 48 | margin-bottom: 30px; 49 | } 50 | 51 | .header a, .header a:visited { 52 | color: #FFFFFF; 53 | text-decoration: underline; 54 | } 55 | 56 | .header a:hover { 57 | color: #FFFFFF; 58 | text-decoration: none; 59 | } 60 | 61 | .item .glyphicon { 62 | font-size: 80px; 63 | padding-right: 10px; 64 | } 65 | 66 | .item h1 { 67 | padding-left: 20px; 68 | /*background-color: #D45CA2;*/ 69 | margin-top: 0px; 70 | border-top-left-radius: 9px; 71 | border-top-right-radius: 9px; 72 | padding-bottom: 15px; 73 | padding-top: 15px; 74 | text-align: center; 75 | } 76 | 77 | .item h1 span { 78 | vertical-align: middle; 79 | opacity: 0.8; 80 | } 81 | 82 | .item { 83 | text-decoration: none; 84 | /*border: 1px solid #D45CA2;*/ 85 | padding: 0; 86 | margin: 0; 87 | border-radius: 10px; 88 | } 89 | 90 | .item p { 91 | padding: 20px; 92 | } 93 | 94 | .item:hover, .item:visited, .item:active, .item:focus { 95 | text-decoration: none; 96 | } 97 | 98 | .item-wrap { 99 | padding-left: 25px; 100 | padding-right: 25px; 101 | } 102 | 103 | .hvr-shutter-out-vertical, .hvr-radial-out { 104 | background-color: #fff1fa; 105 | color: #484848; 106 | } 107 | 108 | .hvr-shutter-out-vertical:before, .hvr-radial-out:before { 109 | 110 | border-radius: 10px; 111 | background-color: #D45CA2; 112 | color: white; 113 | } 114 | 115 | .footer { 116 | position:fixed; 117 | left:0px; 118 | bottom:0px; 119 | width:100%; 120 | font-size: 20px; 121 | padding-bottom: 20px; 122 | padding-top: 20px; 123 | 124 | background-color: white; 125 | 126 | opacity: 0.7; 127 | } 128 | 129 | .footer img { 130 | width: 120px; 131 | vertical-align: middle; 132 | } 133 | 134 | .footer span { 135 | vertical-align: middle; 136 | } 137 | -------------------------------------------------------------------------------- /app/views/index.scala.html: -------------------------------------------------------------------------------- 1 | @(gaCode:Option[String]) 2 | 3 | @main("GraphQL Toolbox", gaCode) { 4 | 5 |
6 |
7 |
8 |
GraphQLToolbox
9 | 10 |

11 | GraphQL Toolbox is a set of GraphQL tools that aims to help with GraphQL server and client development. 12 | If you would like to experiment with this project, add new tools or improve existing one, then you can find it on the GitHub. 13 |

14 |
15 |
16 |
17 | 18 | 56 | 57 | 66 | } 67 | -------------------------------------------------------------------------------- /app/javascripts/css/proxy.css: -------------------------------------------------------------------------------- 1 | body { 2 | height: 100%; 3 | margin: 0; 4 | width: 100%; 5 | overflow: hidden; 6 | } 7 | 8 | #proxy { 9 | height: 100vh; 10 | } 11 | 12 | #proxy * { 13 | box-sizing: content-box; 14 | } 15 | 16 | .proxyWrap { 17 | display: flex; 18 | height: 100%; 19 | width: 100%; 20 | } 21 | 22 | .proxyWrapLeft, .proxyWrapRight { 23 | flex-direction: column; 24 | flex: 1; 25 | height: 100%; 26 | } 27 | 28 | .proxyWrapLeft .query-editor { 29 | position: relative; 30 | width: 100%; 31 | height: 100%; 32 | border-right: solid 1px #e0e0e0; 33 | } 34 | 35 | .proxyWrapLeft .topBarWrap { 36 | display: -webkit-flex; 37 | display: flex; 38 | -webkit-flex-direction: row; 39 | flex-direction: row; 40 | 41 | flex-grow: 0; 42 | flex-shrink: 0; 43 | } 44 | 45 | .proxyWrapLeft .topBar { 46 | -webkit-align-items: center; 47 | align-items: center; 48 | background: -webkit-linear-gradient(#f7f7f7, #e2e2e2); 49 | background: linear-gradient(#f7f7f7, #e2e2e2); 50 | border-bottom: 1px solid #d0d0d0; 51 | cursor: default; 52 | display: -webkit-flex; 53 | display: flex; 54 | height: 34px; 55 | padding: 7px 14px 6px; 56 | -webkit-flex: 1; 57 | flex: 1; 58 | -webkit-flex-direction: row; 59 | flex-direction: row; 60 | -webkit-user-select: none; 61 | user-select: none; 62 | } 63 | 64 | .proxyWrap { 65 | color: #141823; 66 | display: flex; 67 | display: -webkit-flex; 68 | flex-direction: row; 69 | -webkit-flex-direction: row; 70 | font-family: system, 71 | -apple-system, 72 | 'San Francisco', 73 | '.SFNSDisplay-Regular', 74 | 'Segoe UI', 75 | Segoe, 76 | 'Segoe WP', 77 | 'Helvetica Neue', 78 | helvetica, 79 | 'Lucida Grande', 80 | arial, 81 | sans-serif; 82 | font-size: 14px; 83 | height: 100%; 84 | margin: 0; 85 | overflow: hidden; 86 | width: 100%; 87 | } 88 | 89 | .proxyWrap .title { 90 | font-size: 18px; 91 | } 92 | 93 | .proxyWrap .title em { 94 | font-family: georgia; 95 | font-size: 19px; 96 | } 97 | 98 | .proxyWrapLeft .CodeMirror { 99 | color: #141823; 100 | font-family: 'Consolas', 'Inconsolata', 'Droid Sans Mono', 'Monaco', monospace; 101 | font-size: 13px; 102 | height: 100%; 103 | left: 0; 104 | position: absolute; 105 | top: 0; 106 | width: 100%; 107 | } 108 | 109 | .graphiql-container .editorBar { 110 | margin-left: 1px; 111 | } 112 | 113 | .graphiql-container .queryWrap .CodeMirror-gutters { 114 | cursor: col-resize; 115 | } 116 | 117 | .proxyWrapLeft { 118 | display: flex; 119 | } 120 | 121 | .error { 122 | padding: 15px; 123 | opacity: 0.8; 124 | background-color: #ff5661; 125 | color: white; 126 | } 127 | 128 | .error pre { 129 | white-space: pre-wrap; 130 | color: white; 131 | 132 | font-size: 14px; 133 | word-break: break-all; 134 | word-wrap: break-word; 135 | background-color: transparent; 136 | border: none; 137 | } 138 | 139 | .modal-body { 140 | height: 600px; 141 | overflow-y: auto; 142 | overflow-x: hidden; 143 | margin: 0; 144 | padding: 20px; 145 | } 146 | 147 | .editorWrap { 148 | overflow: hidden; 149 | } -------------------------------------------------------------------------------- /app/javascripts/SchemaEditor.jsx: -------------------------------------------------------------------------------- 1 | import React, {PropTypes} from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import CodeMirror from 'codemirror'; 4 | 5 | import 'codemirror/addon/hint/show-hint'; 6 | import 'codemirror/addon/comment/comment'; 7 | import 'codemirror/addon/edit/matchbrackets'; 8 | import 'codemirror/addon/edit/closebrackets'; 9 | import 'codemirror/addon/fold/foldgutter'; 10 | import 'codemirror/addon/fold/brace-fold'; 11 | import 'codemirror/addon/lint/lint'; 12 | import 'codemirror/keymap/sublime'; 13 | import 'codemirror-graphql/hint'; 14 | import 'codemirror-graphql/lint'; 15 | import 'codemirror-graphql/mode'; 16 | 17 | import 'codemirror/lib/codemirror.css' 18 | import 'graphiql/graphiql.css' 19 | import './css/SchemaEditor.css' 20 | 21 | export class SchemaEditor extends React.Component { 22 | static propTypes = { 23 | value: PropTypes.string, 24 | onEdit: PropTypes.func, 25 | readonly: PropTypes.bool 26 | } 27 | 28 | constructor(props) { 29 | super(); 30 | 31 | // Keep a cached version of the value, this cache will be updated when the 32 | // editor is updated, which can later be used to protect the editor from 33 | // unnecessary updates during the update lifecycle. 34 | this.cachedValue = props.value || ''; 35 | } 36 | 37 | shouldComponentUpdate(nextProps, nextState) { 38 | return !!this.props.readonly; // do not disturb the editor 39 | } 40 | 41 | componentDidMount() { 42 | this.editor = CodeMirror(ReactDOM.findDOMNode(this), { 43 | value: this.props.value || '', 44 | readOnly: this.props.readonly || false, 45 | lineWrapping: this.props.readonly || false, 46 | lineNumbers: true, 47 | tabSize: 2, 48 | mode: 'graphql', 49 | theme: 'graphiql', 50 | keyMap: 'sublime', 51 | autoCloseBrackets: true, 52 | matchBrackets: true, 53 | showCursorWhenSelecting: true, 54 | foldGutter: { 55 | minFoldSize: 4 56 | }, 57 | gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'], 58 | extraKeys: { 59 | 'Cmd-Space': () => this.editor.showHint({completeSingle: true}), 60 | 'Ctrl-Space': () => this.editor.showHint({completeSingle: true}), 61 | 'Alt-Space': () => this.editor.showHint({completeSingle: true}), 62 | 'Shift-Space': () => this.editor.showHint({completeSingle: true}), 63 | 64 | // Editor improvements 65 | 'Ctrl-Left': 'goSubwordLeft', 66 | 'Ctrl-Right': 'goSubwordRight', 67 | 'Alt-Left': 'goGroupLeft', 68 | 'Alt-Right': 'goGroupRight', 69 | } 70 | }); 71 | 72 | this.editor.on('change', this._onEdit); 73 | this.editor.on('keyup', this._onKeyUp); 74 | } 75 | 76 | componentDidUpdate(prevProps) { 77 | // Ensure the changes caused by this update are not interpretted as 78 | // user-input changes which could otherwise result in an infinite 79 | // event loop. 80 | this.ignoreChangeEvent = true; 81 | 82 | if (this.props.value !== prevProps.value && this.props.value !== this.cachedValue) { 83 | this.cachedValue = this.props.value; 84 | this.editor.setValue(this.props.value); 85 | } 86 | 87 | this.ignoreChangeEvent = false; 88 | } 89 | 90 | componentWillUnmount() { 91 | this.editor.off('change', this._onEdit); 92 | this.editor.off('keyup', this._onKeyUp); 93 | this.editor = null; 94 | } 95 | 96 | render() { 97 | return
; 98 | } 99 | 100 | /** 101 | * Public API for retrieving the CodeMirror instance from this 102 | * React component. 103 | */ 104 | getCodeMirror() { 105 | return this.editor; 106 | } 107 | 108 | _onKeyUp = (cm, event) => { 109 | const code = event.keyCode; 110 | if ( 111 | (code >= 65 && code <= 90) || // letters 112 | (!event.shiftKey && code >= 48 && code <= 57) || // numbers 113 | (event.shiftKey && code === 189) || // underscore 114 | (event.shiftKey && code === 50) || // @ 115 | (event.shiftKey && code === 57) // ( 116 | ) { 117 | this.editor.execCommand('autocomplete'); 118 | } 119 | } 120 | 121 | _onEdit = () => { 122 | if (!this.ignoreChangeEvent) { 123 | this.cachedValue = this.editor.getValue(); 124 | if (this.props.onEdit) { 125 | this.props.onEdit(this.cachedValue); 126 | } 127 | } 128 | } 129 | } -------------------------------------------------------------------------------- /app/javascripts/utility/onHasCompletion.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | import CodeMirror from 'codemirror'; 11 | import { GraphQLNonNull, GraphQLList } from 'graphql'; 12 | import marked from 'marked'; 13 | 14 | 15 | /** 16 | * Render a custom UI for CodeMirror's hint which includes additional info 17 | * about the type and description for the selected context. 18 | */ 19 | export default function onHasCompletion(cm, data, onHintInformationRender) { 20 | let wrapper; 21 | let information; 22 | 23 | // When a hint result is selected, we touch the UI. 24 | CodeMirror.on(data, 'select', (ctx, el) => { 25 | // Only the first time (usually when the hint UI is first displayed) 26 | // do we create the wrapping node. 27 | if (!wrapper) { 28 | // Wrap the existing hint UI, so we have a place to put information. 29 | const hintsUl = el.parentNode; 30 | const container = hintsUl.parentNode; 31 | wrapper = document.createElement('div'); 32 | container.appendChild(wrapper); 33 | 34 | // CodeMirror vertically inverts the hint UI if there is not enough 35 | // space below the cursor. Since this modified UI appends to the bottom 36 | // of CodeMirror's existing UI, it could cover the cursor. This adjusts 37 | // the positioning of the hint UI to accomodate. 38 | let top = hintsUl.style.top; 39 | let bottom = ''; 40 | const cursorTop = cm.cursorCoords().top; 41 | if (parseInt(top, 10) < cursorTop) { 42 | top = ''; 43 | bottom = (window.innerHeight - cursorTop + 3) + 'px'; 44 | } 45 | 46 | // Style the wrapper, remove positioning from hints. Note that usage 47 | // of this option will need to specify CSS to remove some styles from 48 | // the existing hint UI. 49 | wrapper.className = 'CodeMirror-hints-wrapper'; 50 | wrapper.style.left = hintsUl.style.left; 51 | wrapper.style.top = top; 52 | wrapper.style.bottom = bottom; 53 | hintsUl.style.left = ''; 54 | hintsUl.style.top = ''; 55 | 56 | // This "information" node will contain the additional info about the 57 | // highlighted typeahead option. 58 | information = document.createElement('div'); 59 | information.className = 'CodeMirror-hint-information'; 60 | if (bottom) { 61 | wrapper.appendChild(information); 62 | wrapper.appendChild(hintsUl); 63 | } else { 64 | wrapper.appendChild(hintsUl); 65 | wrapper.appendChild(information); 66 | } 67 | 68 | // When CodeMirror attempts to remove the hint UI, we detect that it was 69 | // removed from our wrapper and in turn remove the wrapper from the 70 | // original container. 71 | let onRemoveFn; 72 | wrapper.addEventListener('DOMNodeRemoved', onRemoveFn = event => { 73 | if (event.target === hintsUl) { 74 | wrapper.removeEventListener('DOMNodeRemoved', onRemoveFn); 75 | wrapper.parentNode.removeChild(wrapper); 76 | wrapper = null; 77 | information = null; 78 | onRemoveFn = null; 79 | } 80 | }); 81 | } 82 | 83 | // Now that the UI has been set up, add info to information. 84 | const description = ctx.description ? 85 | marked(ctx.description, { smartypants: true }) : 86 | 'Self descriptive.'; 87 | const type = ctx.type ? 88 | '' + renderType(ctx.type) + '' : 89 | ''; 90 | 91 | information.innerHTML = '
' + 92 | (description.slice(0, 3) === '

' ? 93 | '

' + type + description.slice(3) : 94 | type + description) + '

'; 95 | 96 | // Additional rendering? 97 | if (onHintInformationRender) { 98 | onHintInformationRender(information); 99 | } 100 | }); 101 | } 102 | 103 | function renderType(type) { 104 | if (type instanceof GraphQLNonNull) { 105 | return `${renderType(type.ofType)}!`; 106 | } 107 | if (type instanceof GraphQLList) { 108 | return `[${renderType(type.ofType)}]`; 109 | } 110 | return `${type.name}`; 111 | } 112 | -------------------------------------------------------------------------------- /app/controllers/JsonPath.scala: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import play.api.libs.json._ 4 | 5 | import io.gatling.jsonpath.{JsonPath ⇒ GatlingJsonPath, _} 6 | import io.gatling.jsonpath.AST._ 7 | import scala.util.Try 8 | 9 | // copy/pasted from https://github.com/josephpconley/play-jsonpath/blob/master/src/main/scala/com/josephpconley/jsonpath/JSONPath.scala 10 | // it was not updated anymore so it needs to be recompiled 11 | object JsonPath { 12 | private val parser = new Parser 13 | 14 | private def error(msg: String = "") = throw new Exception("Bad JSONPath query " + msg) 15 | 16 | def compile(q: String) = Try(parser.compile(q)).isSuccess 17 | 18 | def query(q: String, js: JsValue): JsValue = { 19 | val tokens = parser.compile(q).getOrElse(error()) 20 | parse(tokens, js) 21 | } 22 | 23 | def parse(tokens: List[PathToken], js: JsValue): JsValue = tokens.foldLeft[JsValue](js)( (js, token) => token match { 24 | case Field(name) => js match { 25 | case JsObject(fields) => fields.find(_._1 == name).map(_._2).getOrElse(error("Couldn't find field")) 26 | case JsArray(arr) => JsArray(arr.map(obj => (obj \ name).asOpt[JsValue].getOrElse(error()))) 27 | case _ => error() 28 | } 29 | case RecursiveField(name) => js match { 30 | case JsObject(fields) => { 31 | var value = js \\ name 32 | if(value.head.isInstanceOf[JsArray]){ 33 | value = value.flatMap(_.as[JsArray].value) 34 | } 35 | JsArray(value) 36 | } 37 | case JsArray(arr) => { 38 | var value = arr.flatMap(_ \\ name) 39 | if(value.head.isInstanceOf[JsArray]){ 40 | value = value.flatMap(_.as[JsArray].value) 41 | } 42 | JsArray(value) 43 | } 44 | case _ => error() 45 | } 46 | case MultiField(names) => js match { 47 | case JsObject(fields) => JsArray(fields.filter(f => names.contains(f._1)).map(_._2).toSeq) 48 | case _ => error() 49 | } 50 | case AnyField => js match { 51 | case JsObject(fields) => JsArray(fields.map(_._2).toSeq) 52 | case JsArray(arr) => js 53 | case _ => error() 54 | } 55 | case RecursiveAnyField => js 56 | case ArraySlice(start, stop, step) => 57 | js.asOpt[JsArray].map { arr => 58 | var sliced = if(start.getOrElse(0) >= 0) arr.value.drop(start.getOrElse(0)) else arr.value.takeRight(Math.abs(start.get)) 59 | sliced = if(stop.getOrElse(arr.value.size) >= 0) sliced.slice(0, stop.getOrElse(arr.value.size)) else sliced.dropRight(Math.abs(stop.get)) 60 | 61 | if(step < 0){ 62 | sliced = sliced.reverse 63 | } 64 | JsArray(sliced.zipWithIndex.filter(_._2 % Math.abs(step) == 0).map(_._1)) 65 | }.getOrElse(error()) 66 | case ArrayRandomAccess(indices) => { 67 | val arr = js.as[JsArray].value 68 | val selectedIndices = indices.map(i => if(i >= 0) i else arr.size + i).toSet.toSeq 69 | // println(selectedIndices + " " + js) 70 | 71 | if(selectedIndices.size == 1) arr(selectedIndices.head) else JsArray(selectedIndices.map(arr(_))) 72 | } 73 | case ft: FilterToken => JsArray(parseFilterToken(ft, js)) 74 | case _ => js 75 | }) 76 | 77 | private def parseFilterToken(ft: FilterToken, js: JsValue): Seq[JsValue] = ft match { 78 | case HasFilter(SubQuery(tokens)) => 79 | (for{ 80 | arr <- js.asOpt[JsArray] 81 | objs <- Try(arr.value.map(_.as[JsObject])).toOption 82 | } yield { 83 | tokens.last match { 84 | case Field(name) => objs.filter(_.keys.contains(name)) 85 | case MultiField(names) => objs.filter(_.keys.intersect(names.toSet) == names.toSet) 86 | case _ => error() 87 | } 88 | }).getOrElse(error()) 89 | case ComparisonFilter(op, lhv, rhv) => { 90 | js.asOpt[JsArray].map { arr => 91 | arr.value.filter{obj => 92 | val left = parseFilterValue(lhv, obj) 93 | val right = parseFilterValue(rhv, obj) 94 | op.apply(left, right) 95 | } 96 | }.getOrElse(error()) 97 | } 98 | case BooleanFilter(binOp, lht, rht) => { 99 | val leftJs = parseFilterToken(lht, js) 100 | val rightJs = parseFilterToken(rht, js) 101 | 102 | binOp match { 103 | case OrOperator => leftJs.union(rightJs).toSet.toSeq 104 | case AndOperator => leftJs.intersect(rightJs) 105 | } 106 | } 107 | } 108 | 109 | private def parseFilterValue(fv: FilterValue, js: JsValue): Any = fv match { 110 | case SubQuery(tokens) => Try{ 111 | primitive(parse(tokens, js)) match { 112 | case n:Number => n.doubleValue() 113 | case a @ _ => a 114 | } 115 | }.getOrElse(error()) 116 | case dv: FilterDirectValue => dv.value 117 | } 118 | 119 | def primitive(js: JsValue): Any = js match { 120 | case JsNumber(n) => n 121 | case JsString(s) => s 122 | case JsBoolean(b) => b 123 | case _ => throw new Exception("Not a JsPrimitive: " + js) 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /app/javascripts/css/graphiql.css: -------------------------------------------------------------------------------- 1 | body, html { 2 | height: 100%; 3 | margin: 0; 4 | width: 100%; 5 | overflow: hidden; 6 | 7 | font-family: proxima-nova, "Helvetica Neue", Helvetica, Arial, sans-serif; 8 | } 9 | 10 | #graphiql-tool { 11 | height: 100%; 12 | } 13 | 14 | .graphiql-tool-cont1 * { 15 | box-sizing: content-box; 16 | } 17 | 18 | .proxyWrap { 19 | display: flex; 20 | height: 100%; 21 | width: 100%; 22 | } 23 | 24 | .proxyWrapLeft, .proxyWrapRight { 25 | flex-direction: column; 26 | flex: 1; 27 | height: 100%; 28 | } 29 | 30 | .proxyWrapLeft .query-editor { 31 | position: relative; 32 | width: 100%; 33 | height: 100%; 34 | border-right: solid 1px #e0e0e0; 35 | } 36 | 37 | .proxyWrapLeft .topBarWrap { 38 | display: -webkit-flex; 39 | display: flex; 40 | -webkit-flex-direction: row; 41 | flex-direction: row; 42 | 43 | flex-grow: 0; 44 | flex-shrink: 0; 45 | } 46 | 47 | .proxyWrapLeft .topBar { 48 | -webkit-align-items: center; 49 | align-items: center; 50 | background: -webkit-linear-gradient(#f7f7f7, #e2e2e2); 51 | background: linear-gradient(#f7f7f7, #e2e2e2); 52 | border-bottom: 1px solid #d0d0d0; 53 | cursor: default; 54 | display: -webkit-flex; 55 | display: flex; 56 | height: 34px; 57 | padding: 7px 14px 6px; 58 | -webkit-flex: 1; 59 | flex: 1; 60 | -webkit-flex-direction: row; 61 | flex-direction: row; 62 | -webkit-user-select: none; 63 | user-select: none; 64 | } 65 | 66 | .proxyWrap { 67 | color: #141823; 68 | display: flex; 69 | display: -webkit-flex; 70 | flex-direction: row; 71 | -webkit-flex-direction: row; 72 | font-family: system, 73 | -apple-system, 74 | 'San Francisco', 75 | '.SFNSDisplay-Regular', 76 | 'Segoe UI', 77 | Segoe, 78 | 'Segoe WP', 79 | 'Helvetica Neue', 80 | helvetica, 81 | 'Lucida Grande', 82 | arial, 83 | sans-serif; 84 | font-size: 14px; 85 | height: 100%; 86 | margin: 0; 87 | overflow: hidden; 88 | width: 100%; 89 | } 90 | 91 | .proxyWrap .title { 92 | font-size: 18px; 93 | } 94 | 95 | .proxyWrap .title em { 96 | font-family: georgia; 97 | font-size: 19px; 98 | } 99 | 100 | .proxyWrapLeft .CodeMirror { 101 | color: #141823; 102 | font-family: 'Consolas', 'Inconsolata', 'Droid Sans Mono', 'Monaco', monospace; 103 | font-size: 13px; 104 | height: 100%; 105 | left: 0; 106 | position: absolute; 107 | top: 0; 108 | width: 100%; 109 | } 110 | 111 | .graphiql-container .editorBar { 112 | margin-left: 1px; 113 | } 114 | 115 | .graphiql-container .queryWrap .CodeMirror-gutters { 116 | cursor: col-resize; 117 | } 118 | 119 | .proxyWrapLeft { 120 | display: flex; 121 | } 122 | 123 | .error { 124 | padding: 15px; 125 | opacity: 0.8; 126 | background-color: #ff5661; 127 | color: white; 128 | } 129 | 130 | .error pre { 131 | white-space: pre-wrap; 132 | color: white; 133 | 134 | font-size: 14px; 135 | word-break: break-all; 136 | word-wrap: break-word; 137 | background-color: transparent; 138 | border: none; 139 | } 140 | 141 | .modal-body { 142 | overflow-y: auto; 143 | overflow-x: hidden; 144 | margin: 0; 145 | padding: 20px; 146 | } 147 | 148 | .graphiql-tool-cont { 149 | width: 100%; 150 | height: 100%; 151 | display: flex; 152 | flex-direction: column; 153 | } 154 | 155 | .graphiql-tool-cont1 { 156 | flex: 2 0px; 157 | border-top: 1px solid #e0e0e0; 158 | } 159 | 160 | .tabs { 161 | height: 100%; 162 | display: flex; 163 | flex-direction: column; 164 | } 165 | 166 | .tabs .nav-tabs { 167 | background: rgba(212, 212, 212, 0.2); 168 | } 169 | 170 | .tabs .nav-tabs li a { 171 | border-top-right-radius: 0; 172 | border-top-left-radius: 0; 173 | border-top-color: white; 174 | } 175 | 176 | .tabs .tab-content { 177 | height: 100%; 178 | display: flex; 179 | } 180 | 181 | .tabs .tab-content .tab-pane { 182 | flex: 1; 183 | } 184 | 185 | .close-button { 186 | margin-left: 10px; 187 | padding-right: 0; 188 | margin-right: 0; 189 | } 190 | 191 | .dropzone { 192 | display: inline-block; 193 | } 194 | 195 | .dropzone-active { 196 | background-color: #d9ffcf; 197 | } 198 | 199 | .tab-top { 200 | display: flex; 201 | justify-content: space-between; 202 | } 203 | 204 | .headers { 205 | flex: 2; 206 | padding: 20px; 207 | 208 | } 209 | 210 | .headers table { 211 | max-width: 800px; 212 | } 213 | 214 | .headers td { 215 | vertical-align: middle !important; 216 | } 217 | 218 | .tab-form { 219 | padding: 20px; 220 | width: 700px; 221 | } 222 | 223 | .graphiql-collapsed-tab { 224 | padding: 20px; 225 | width: 100%; 226 | flex: 2; 227 | cursor: pointer; 228 | } 229 | 230 | /*.graphiql-collapsed-tab:hover {*/ 231 | /*background-color: #F4F4F4;*/ 232 | /*}*/ 233 | 234 | .tab-form .btn{ 235 | font-size: 14.5px; 236 | } 237 | 238 | .header-add { 239 | font-size: 15px !important; 240 | } 241 | 242 | .nav-tabs li a { 243 | top: -1px; 244 | } 245 | 246 | .graphiql-toolbar { 247 | padding: 6px; 248 | } 249 | 250 | .toolbar { 251 | overflow-x: visible !important; 252 | } 253 | 254 | .bs-toolbar-button { 255 | font-size: 13px !important; 256 | } 257 | 258 | .code-pop { 259 | display: inline-table !important; 260 | } 261 | 262 | .code-pop pre { 263 | border: none; 264 | background: none; 265 | } -------------------------------------------------------------------------------- /app/javascripts/utility/fillLeafs.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the 6 | * LICENSE-examples file in the root directory of this source tree. 7 | */ 8 | 9 | import { 10 | getNamedType, 11 | isLeafType, 12 | parse, 13 | print, 14 | TypeInfo, 15 | visit, 16 | } from 'graphql'; 17 | 18 | 19 | /** 20 | * Given a document string which may not be valid due to terminal fields not 21 | * representing leaf values (Spec Section: "Leaf Field Selections"), and a 22 | * function which provides reasonable default field names for a given type, 23 | * this function will attempt to produce a schema which is valid after filling 24 | * in selection sets for the invalid fields. 25 | * 26 | * Note that there is no guarantee that the result will be a valid query, this 27 | * utility represents a "best effort" which may be useful within IDE tools. 28 | */ 29 | export function fillLeafs(schema, docString, getDefaultFieldNames) { 30 | const insertions = []; 31 | 32 | if (!schema) { 33 | return { insertions, result: docString }; 34 | } 35 | 36 | let ast; 37 | try { 38 | ast = parse(docString); 39 | } catch (error) { 40 | return { insertions, result: docString }; 41 | } 42 | 43 | const fieldNameFn = getDefaultFieldNames || defaultGetDefaultFieldNames; 44 | const typeInfo = new TypeInfo(schema); 45 | visit(ast, { 46 | leave(node) { 47 | typeInfo.leave(node); 48 | }, 49 | enter(node) { 50 | typeInfo.enter(node); 51 | if (node.kind === 'Field' && !node.selectionSet) { 52 | const fieldType = typeInfo.getType(); 53 | const selectionSet = buildSelectionSet(fieldType, fieldNameFn); 54 | if (selectionSet) { 55 | const indent = getIndentation(docString, node.loc.start); 56 | insertions.push({ 57 | index: node.loc.end, 58 | string: ' ' + print(selectionSet).replace(/\n/g, '\n' + indent) 59 | }); 60 | } 61 | } 62 | } 63 | }); 64 | 65 | // Apply the insertions, but also return the insertions metadata. 66 | return { 67 | insertions, 68 | result: withInsertions(docString, insertions), 69 | }; 70 | } 71 | 72 | // The default function to use for producing the default fields from a type. 73 | // This function first looks for some common patterns, and falls back to 74 | // including all leaf-type fields. 75 | function defaultGetDefaultFieldNames(type) { 76 | // If this type cannot access fields, then return an empty set. 77 | if (!type.getFields) { 78 | return []; 79 | } 80 | 81 | const fields = type.getFields(); 82 | 83 | // Is there an `id` field? 84 | if (fields['id']) { 85 | return [ 'id' ]; 86 | } 87 | 88 | // Is there an `edges` field? 89 | if (fields['edges']) { 90 | return [ 'edges' ]; 91 | } 92 | 93 | // Is there an `node` field? 94 | if (fields['node']) { 95 | return [ 'node' ]; 96 | } 97 | 98 | // Include all leaf-type fields. 99 | const leafFieldNames = []; 100 | Object.keys(fields).forEach(fieldName => { 101 | if (isLeafType(fields[fieldName].type)) { 102 | leafFieldNames.push(fieldName); 103 | } 104 | }); 105 | return leafFieldNames; 106 | } 107 | 108 | // Given a GraphQL type, and a function which produces field names, recursively 109 | // generate a SelectionSet which includes default fields. 110 | function buildSelectionSet(type, getDefaultFieldNames) { 111 | // Unwrap any non-null or list types. 112 | const namedType = getNamedType(type); 113 | 114 | // Unknown types and leaf types do not have selection sets. 115 | if (!type || isLeafType(type)) { 116 | return; 117 | } 118 | 119 | // Get an array of field names to use. 120 | const fieldNames = getDefaultFieldNames(namedType); 121 | 122 | // If there are no field names to use, return no selection set. 123 | if (!Array.isArray(fieldNames) || fieldNames.length === 0) { 124 | return; 125 | } 126 | 127 | // Build a selection set of each field, calling buildSelectionSet recursively. 128 | return { 129 | kind: 'SelectionSet', 130 | selections: fieldNames.map(fieldName => { 131 | const fieldDef = namedType.getFields()[fieldName]; 132 | const fieldType = fieldDef ? fieldDef.type : null; 133 | return { 134 | kind: 'Field', 135 | name: { 136 | kind: 'Name', 137 | value: fieldName 138 | }, 139 | selectionSet: buildSelectionSet(fieldType, getDefaultFieldNames) 140 | }; 141 | }) 142 | }; 143 | } 144 | 145 | // Given an initial string, and a list of "insertion" { index, string } objects, 146 | // return a new string with these insertions applied. 147 | function withInsertions(initial, insertions) { 148 | if (insertions.length === 0) { 149 | return initial; 150 | } 151 | let edited = ''; 152 | let prevIndex = 0; 153 | insertions.forEach(({ index, string }) => { 154 | edited += initial.slice(prevIndex, index) + string; 155 | prevIndex = index; 156 | }); 157 | edited += initial.slice(prevIndex); 158 | return edited; 159 | } 160 | 161 | // Given a string and an index, look backwards to find the string of whitespace 162 | // following the next previous line break. 163 | function getIndentation(str, index) { 164 | let indentStart = index; 165 | let indentEnd = index; 166 | while (indentStart) { 167 | const c = str.charCodeAt(indentStart - 1); 168 | // line break 169 | if (c === 10 || c === 13 || c === 0x2028 || c === 0x2029) { 170 | break; 171 | } 172 | indentStart--; 173 | // not white space 174 | if (c !== 9 && c !== 11 && c !== 12 && c !== 32 && c !== 160) { 175 | indentEnd = indentStart; 176 | } 177 | } 178 | return str.substring(indentStart, indentEnd); 179 | } 180 | -------------------------------------------------------------------------------- /app/controllers/Application.scala: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import javax.inject.Inject 4 | 5 | import akka.actor.ActorSystem 6 | import play.api.libs.json.{JsObject, JsString, Json} 7 | import play.api.libs.ws.WSClient 8 | import play.api.mvc._ 9 | import play.api.Configuration 10 | import sangria.execution._ 11 | import sangria.parser.{QueryParser, SyntaxError} 12 | import sangria.marshalling.playJson._ 13 | import sangria.renderer.QueryRenderer 14 | import sangria.schema.{Schema, SchemaMaterializationException} 15 | 16 | import scala.concurrent.Future 17 | import scala.util.control.NonFatal 18 | import scala.util.{Failure, Success} 19 | 20 | class Application @Inject()(system: ActorSystem, config: Configuration, client: WSClient) extends InjectedController { 21 | import system.dispatcher 22 | 23 | val materializer = new Materializer(client) 24 | 25 | val gaCode = config.getOptional[String]("gaCode") 26 | 27 | case class TooComplexQueryError(message: String) extends Exception(message) 28 | 29 | val complexityRejector = QueryReducer.rejectComplexQueries(20000, (complexity: Double, _: Any) ⇒ 30 | TooComplexQueryError(s"Query complexity is $complexity but max allowed complexity is 20000. Please reduce the number of the fields in the query.")) 31 | 32 | val exceptionHandler = ExceptionHandler { 33 | case (m, error: TooComplexQueryError) ⇒ HandledException(error.getMessage) 34 | case (m, NonFatal(error)) ⇒ 35 | HandledException(error.getMessage) 36 | } 37 | 38 | def index = Action { 39 | Ok(views.html.index(gaCode)) 40 | } 41 | 42 | def format = Action { 43 | Ok(views.html.workspace("GraphQL Formatter", "format", "formatter", gaCode)) 44 | } 45 | 46 | def graphiql = Action { 47 | Ok(views.html.workspace("GraphiQL", "graphiql", "graphiql-workspace", gaCode)) 48 | } 49 | 50 | def proxy = Action { 51 | Ok(views.html.workspace("GraphQL HTTP Proxy", "proxy", "proxy", gaCode)) 52 | } 53 | 54 | def graphqlProxy = Action.async(parse.json) { request ⇒ 55 | val query = (request.body \ "query").as[String] 56 | val schema = (request.body \ "schema").as[String] 57 | val operation = (request.body \ "operationName").asOpt[String] 58 | 59 | val variables = (request.body \ "variables").toOption.flatMap { 60 | case JsString(vars) ⇒ Some(parseVariables(vars)) 61 | case obj: JsObject ⇒ Some(obj) 62 | case _ ⇒ None 63 | } 64 | 65 | materializeSchema(schema) flatMap { 66 | case Right(matSchema) ⇒ 67 | executeQuery(matSchema, query, variables, operation) 68 | case Left(result) ⇒ 69 | Future.successful(result) 70 | } 71 | } 72 | 73 | def proxyRequest = Action.async(parse.json) { request ⇒ 74 | val url = (request.body \ "url").as[String] 75 | val rawHeaders = (request.body \ "headers").as[Seq[JsObject]] 76 | val removeFields = Set("url", "headers") 77 | val body = JsObject(request.body.asInstanceOf[JsObject].fields.filterNot(f ⇒ removeFields contains f._1)) 78 | val headers = rawHeaders.map(o ⇒ (o \ "name").as[String] → (o \ "value").as[String]) 79 | 80 | client.url(url).addHttpHeaders(headers: _*).post(body) 81 | .map { resp ⇒ 82 | new Status(resp.status)(resp.json) 83 | } 84 | } 85 | 86 | private def materializeSchema(schemaDef: String): Future[Either[Result, (Schema[MatCtx, Any], MatCtx)]] = { 87 | QueryParser.parse(schemaDef) match { 88 | case Success(schemaAst) ⇒ 89 | materializer.loadContext(schemaAst).map { ctx ⇒ 90 | try { 91 | Right(Schema.buildFromAst(schemaAst, materializer.schemaBuilder(ctx).validateSchemaWithException(schemaAst)) → ctx) 92 | } catch { 93 | case e: SchemaMaterializationException ⇒ 94 | Left(BadRequest(Json.obj("materializationError" → e.getMessage))) 95 | 96 | case NonFatal(error) ⇒ 97 | Left(BadRequest(Json.obj("unexpectedError" → error.getMessage))) 98 | } 99 | } 100 | 101 | case Failure(error: SyntaxError) ⇒ 102 | Future.successful(Left(BadRequest(Json.obj( 103 | "syntaxError" → error.getMessage, 104 | "locations" → Json.arr(Json.obj( 105 | "line" → error.originalError.position.line, 106 | "column" → error.originalError.position.column)))))) 107 | case Failure(error) ⇒ 108 | throw error 109 | } 110 | } 111 | 112 | private def parseVariables(variables: String) = 113 | if (variables.trim == "" || variables.trim == "null") Json.obj() else Json.parse(variables).as[JsObject] 114 | 115 | private def executeQuery(schemaAndRoot: (Schema[MatCtx, Any], MatCtx), query: String, variables: Option[JsObject], operation: Option[String]) = 116 | QueryParser.parse(query) match { 117 | 118 | // query parsed successfully, time to execute it! 119 | case Success(queryAst) ⇒ 120 | val (schema, ctx) = schemaAndRoot 121 | 122 | Executor.execute(schema, queryAst, 123 | root = ctx.vars, 124 | userContext = ctx, 125 | operationName = operation, 126 | variables = variables getOrElse Json.obj(), 127 | exceptionHandler = exceptionHandler, 128 | queryReducers = complexityRejector :: Nil, 129 | maxQueryDepth = Some(15)) 130 | .map(Ok(_)) 131 | .recover { 132 | case error: QueryAnalysisError ⇒ BadRequest(error.resolveError) 133 | case error: ErrorWithResolver ⇒ InternalServerError(error.resolveError) 134 | } 135 | 136 | case Failure(error: SyntaxError) ⇒ 137 | Future.successful(BadRequest(Json.obj( 138 | "syntaxError" → error.getMessage, 139 | "locations" → Json.arr(Json.obj( 140 | "line" → error.originalError.position.line, 141 | "column" → error.originalError.position.column))))) 142 | 143 | case Failure(error) ⇒ 144 | throw error 145 | } 146 | 147 | def formatGet(query: String) = Action.async { request ⇒ 148 | formatQuery(query) 149 | } 150 | 151 | def formatPost = Action.async(parse.text) { request ⇒ 152 | formatQuery(request.body) 153 | } 154 | 155 | private def formatQuery(query: String) = { 156 | QueryParser.parse(query) match { 157 | case Success(ast) ⇒ 158 | Future.successful(Ok(QueryRenderer.render(ast))) 159 | case Failure(error) ⇒ 160 | Future.successful(BadRequest(error.getMessage)) 161 | 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /app/javascripts/GraphQLFormatter.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import { SchemaEditor } from './SchemaEditor.jsx'; 5 | import CodeMirrorSizer from 'graphiql-workspace/dist/utility/CodeMirrorSizer'; 6 | import getQueryFacts from 'graphiql-workspace/dist/utility/getQueryFacts'; 7 | import getSelectedOperationName from 'graphiql-workspace/dist/utility/getSelectedOperationName'; 8 | import debounce from 'graphiql-workspace/dist/utility/debounce'; 9 | import find from 'graphiql-workspace/dist/utility/find'; 10 | import { fillLeafs } from 'graphiql-workspace/dist/utility/fillLeafs'; 11 | import { getLeft, getTop } from 'graphiql-workspace/dist/utility/elementPosition'; 12 | import { KeepLastTaskQueue } from 'graphiql-workspace'; 13 | 14 | import 'graphiql/graphiql.css' 15 | 16 | export class GraphQLFormatter extends React.Component { 17 | static propTypes = { 18 | formatter: PropTypes.func.isRequired, 19 | value: PropTypes.string, 20 | } 21 | 22 | constructor(props) { 23 | super(props); 24 | 25 | // Determine the initial query to display. 26 | const query = 27 | props.query !== undefined ? props.value : defaultQuery; 28 | 29 | // Initialize state 30 | this.state = { 31 | query, 32 | editorFlex: 1, 33 | }; 34 | 35 | this.taskQueue = new KeepLastTaskQueue() 36 | } 37 | 38 | componentDidMount() { 39 | // Utility for keeping CodeMirror correctly sized. 40 | this.codeMirrorSizer = new CodeMirrorSizer(); 41 | 42 | this.handleFormat(this.state.query) 43 | } 44 | 45 | componentDidUpdate() { 46 | // If this update caused DOM nodes to have changed sizes, update the 47 | // corresponding CodeMirror instance sizes to match. 48 | this.codeMirrorSizer.updateSizes([ 49 | this.queryEditorComponent, 50 | this.resultComponent, 51 | ]); 52 | } 53 | 54 | render() { 55 | const children = React.Children.toArray(this.props.children); 56 | 57 | const logo = find(children, child => child.type === GraphQLFormatter.Logo) || 58 | ; 59 | 60 | const footer = find(children, child => child.type === GraphQLFormatter.Footer); 61 | 62 | const queryWrapStyle = { 63 | WebkitFlex: this.state.editorFlex, 64 | flex: this.state.editorFlex, 65 | }; 66 | 67 | return ( 68 |
69 |
70 |
71 |
72 | {logo} 73 |
74 |
75 |
{ this.editorBarComponent = n; }} 77 | className="editorBar" 78 | onMouseDown={this.handleResizeStart}> 79 |
80 | { this.queryEditorComponent = n; }} 82 | value={this.state.query} 83 | onEdit={this.handleEditQuery} 84 | /> 85 |
86 | 87 |
88 | { this.resultComponent = c; }} 90 | value={this.state.response} 91 | readonly={true} 92 | /> 93 | {footer} 94 |
95 |
96 |
97 |
98 | ); 99 | } 100 | 101 | handleFormat = value => { 102 | const editedQuery = value || this.state.query; 103 | 104 | this.taskQueue.add(() => { 105 | return this._format(editedQuery).then(result => { 106 | this.setState({ 107 | isWaitingForResponse: false, 108 | response: result, 109 | }); 110 | }) 111 | }) 112 | 113 | // If an operation was explicitly provided, different from the current 114 | this.setState({ 115 | isWaitingForResponse: true 116 | }); 117 | } 118 | 119 | _format(query) { 120 | const formatter = this.props.formatter; 121 | const fetch = formatter(query); 122 | 123 | return fetch.catch(error => { 124 | this.setState({ 125 | isWaitingForResponse: false, 126 | response: error && (error.stack || String(error)) 127 | }); 128 | }); 129 | } 130 | 131 | handleEditQuery = value => { 132 | this.handleFormat(value) 133 | } 134 | 135 | handleResizeStart = downEvent => { 136 | if (!this._didClickDragBar(downEvent)) { 137 | return; 138 | } 139 | 140 | downEvent.preventDefault(); 141 | 142 | const offset = downEvent.clientX - getLeft(downEvent.target); 143 | 144 | let onMouseMove = moveEvent => { 145 | if (moveEvent.buttons === 0) { 146 | return onMouseUp(); 147 | } 148 | 149 | const editorBar = ReactDOM.findDOMNode(this.editorBarComponent); 150 | const leftSize = moveEvent.clientX - getLeft(editorBar) - offset; 151 | const rightSize = editorBar.clientWidth - leftSize; 152 | 153 | this.setState({ editorFlex: leftSize / rightSize }); 154 | }; 155 | 156 | let onMouseUp = () => { 157 | document.removeEventListener('mousemove', onMouseMove); 158 | document.removeEventListener('mouseup', onMouseUp); 159 | onMouseMove = null; 160 | onMouseUp = null; 161 | }; 162 | 163 | document.addEventListener('mousemove', onMouseMove); 164 | document.addEventListener('mouseup', onMouseUp); 165 | } 166 | 167 | _didClickDragBar(event) { 168 | // Only for primary unmodified clicks 169 | if (event.button !== 0 || event.ctrlKey) { 170 | return false; 171 | } 172 | let target = event.target; 173 | // We use codemirror's gutter as the drag bar. 174 | if (target.className.indexOf('CodeMirror-gutter') !== 0) { 175 | return false; 176 | } 177 | // Specifically the result window's drag bar. 178 | const resultWindow = ReactDOM.findDOMNode(this.resultComponent); 179 | while (target) { 180 | if (target === resultWindow) { 181 | return true; 182 | } 183 | target = target.parentNode; 184 | } 185 | return false; 186 | } 187 | } 188 | 189 | GraphQLFormatter.Logo = function GraphiQLLogo(props) { 190 | return ( 191 |
192 | {props.children || GraphQL Formatter} 193 |
194 | ); 195 | }; 196 | 197 | // Configure the UI by providing this Component as a child of GraphiQL. 198 | GraphQLFormatter.Footer = function GraphiQLFooter(props) { 199 | return ( 200 |
201 | {props.children} 202 |
203 | ); 204 | }; 205 | 206 | const defaultQuery = 207 | `type 208 | Query { 209 | id( 210 | # foo bar 211 | arg: Int 212 | ): Int! 213 | # test 214 | 215 | #comment 216 | value: 217 | String 218 | } 219 | 220 | #My schema 221 | 222 | 223 | 224 | schema { 225 | # foo 226 | 227 | #bar 228 | query: Query 229 | } 230 | `; -------------------------------------------------------------------------------- /app/controllers/Materializer.scala: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import play.api.libs.json._ 4 | import play.api.libs.ws.WSClient 5 | import sangria.ast 6 | import sangria.schema._ 7 | import sangria.marshalling.MarshallingUtil._ 8 | import sangria.marshalling.playJson._ 9 | import sangria.marshalling.queryAst._ 10 | import sangria.schema.ResolverBasedAstSchemaBuilder.{resolveDirectives, extractValue} 11 | 12 | import scala.concurrent.{ExecutionContext, Future} 13 | 14 | class Materializer(client: WSClient)(implicit ec: ExecutionContext) { 15 | object Args { 16 | val HeaderType = InputObjectType("Header", fields = List( 17 | InputField("name", StringType), 18 | InputField("value", StringType))) 19 | 20 | val QueryParamType = InputObjectType("QueryParam", fields = List( 21 | InputField("name", StringType), 22 | InputField("value", StringType))) 23 | 24 | val IncludeType = InputObjectType("GraphQLSchemaInclude", fields = List( 25 | InputField("name", StringType), 26 | InputField("url", StringType))) 27 | 28 | val IncludeFieldsType = InputObjectType("GraphQLIncludeFields", fields = List( 29 | InputField("schema", StringType), 30 | InputField("type", StringType), 31 | InputField("fields", OptionInputType(ListInputType(StringType))))) 32 | 33 | val NameOpt = Argument("name", OptionInputType(StringType)) 34 | val NameReq = Argument("name", StringType) 35 | val Path = Argument("path", OptionInputType(StringType)) 36 | val JsonValue = Argument("value", StringType) 37 | val Url = Argument("url", StringType) 38 | val Headers = Argument("headers", OptionInputType(ListInputType(HeaderType))) 39 | val QueryParams = Argument("query", OptionInputType(ListInputType(QueryParamType))) 40 | val ForAll = Argument("forAll", OptionInputType(StringType)) 41 | val Schemas = Argument("schemas", ListInputType(IncludeType)) 42 | val Fields = Argument("fields", ListInputType(IncludeFieldsType)) 43 | } 44 | 45 | object Dirs { 46 | val Context = Directive("context", 47 | arguments = Args.NameOpt :: Args.Path :: Nil, 48 | locations = Set(DirectiveLocation.FieldDefinition)) 49 | 50 | val Value = Directive("value", 51 | arguments = Args.NameOpt :: Args.Path :: Nil, 52 | locations = Set(DirectiveLocation.FieldDefinition)) 53 | 54 | val Arg = Directive("arg", 55 | arguments = Args.NameReq :: Nil, 56 | locations = Set(DirectiveLocation.FieldDefinition)) 57 | 58 | val JsonConst = Directive("jsonValue", 59 | arguments = Args.JsonValue :: Nil, 60 | locations = Set(DirectiveLocation.FieldDefinition, DirectiveLocation.Schema)) 61 | 62 | val HttpGet = Directive("httpGet", 63 | arguments = Args.Url :: Args.Headers :: Args.QueryParams :: Args.ForAll :: Nil, 64 | locations = Set(DirectiveLocation.FieldDefinition)) 65 | 66 | val IncludeGraphQL = Directive("includeGraphQL", 67 | arguments = Args.Schemas :: Nil, 68 | locations = Set(DirectiveLocation.Schema)) 69 | 70 | val IncludeField = Directive("include", 71 | arguments = Args.Fields :: Nil, 72 | locations = Set(DirectiveLocation.Object)) 73 | } 74 | 75 | def schemaBuilder(ctx: MatCtx) = AstSchemaBuilder.resolverBased[MatCtx]( 76 | AdditionalDirectives(Seq(Dirs.IncludeGraphQL)), 77 | AdditionalTypes(ctx.allTypes.toList), 78 | DirectiveResolver(Dirs.Context, c ⇒ c.withArgs(Args.NameOpt, Args.Path) { (name, path) ⇒ 79 | name 80 | .map(n ⇒ extractValue(c.ctx.field.fieldType, c.ctx.ctx.vars.get(n))) 81 | .orElse(path.map(p ⇒ extractValue(c.ctx.field.fieldType, Some(JsonPath.query(p, c.ctx.ctx.vars))))) 82 | .getOrElse(throw SchemaMaterializationException(s"Can't find a directive argument 'path' or 'name'.")) 83 | }), 84 | 85 | DirectiveResolver(Dirs.Value, c ⇒ c.withArgs(Args.NameOpt, Args.Path) { (name, path) ⇒ 86 | def extract(value: Any) = 87 | name 88 | .map(n ⇒ extractValue(c.ctx.field.fieldType, value.asInstanceOf[JsValue].get(n))) 89 | .orElse(path.map(p ⇒ extractValue(c.ctx.field.fieldType, Some(JsonPath.query(p, value.asInstanceOf[JsValue]))))) 90 | .getOrElse(throw SchemaMaterializationException(s"Can't find a directive argument 'path' or 'name'.")) 91 | 92 | c.lastValue map (_.map(extract)) getOrElse extract(c.ctx.value) 93 | }), 94 | 95 | DirectiveResolver(Dirs.Arg, c ⇒ 96 | extractValue(c.ctx.field.fieldType, 97 | convertArgs(c.ctx.args, c.ctx.astFields.head).get(c arg Args.NameReq))), 98 | 99 | DynamicDirectiveResolver[MatCtx, JsValue]("const", c ⇒ 100 | extractValue(c.ctx.field.fieldType, Some(c.args.get("value") match { 101 | case Some(JsString(str)) ⇒ JsString(fillPlaceholders(c.ctx, str)) 102 | case Some(jv) ⇒ jv 103 | case _ ⇒ JsNull 104 | }))), 105 | 106 | DirectiveResolver(Dirs.JsonConst, c ⇒ 107 | extractValue(c.ctx.field.fieldType, 108 | Some(Json.parse(fillPlaceholders(c.ctx, c arg Args.JsonValue))))), 109 | 110 | DirectiveResolver(Dirs.HttpGet, 111 | complexity = Some(_ ⇒ (_, _, _) ⇒ 1000.0), 112 | resolve = c ⇒ c.withArgs(Args.Url, Args.Headers, Args.QueryParams, Args.ForAll) { (rawUrl, rawHeaders, rawQueryParams, forAll) ⇒ 113 | val args = Some(convertArgs(c.ctx.args, c.ctx.astFields.head)) 114 | 115 | def extractMap(in: Option[scala.Seq[InputObjectType.DefaultInput]], elem: JsValue) = 116 | rawHeaders.map(_.map(h ⇒ h("name").asInstanceOf[String] → fillPlaceholders(c.ctx, h("value").asInstanceOf[String], args, elem))).getOrElse(Nil) 117 | 118 | def makeRequest(tpe: OutputType[_], c: Context[MatCtx, _], args: Option[JsObject], elem: JsValue = JsNull) = { 119 | val url = fillPlaceholders(c, rawUrl, args, elem) 120 | val headers = extractMap(rawHeaders, elem) 121 | val query = extractMap(rawQueryParams, elem) 122 | val request = client.url(url).addHttpHeaders(headers: _*).addQueryStringParameters(query: _*) 123 | 124 | request.execute().map(resp ⇒ resp.json) 125 | } 126 | 127 | forAll match { 128 | case Some(elem) ⇒ 129 | JsonPath.query(elem, c.ctx.value.asInstanceOf[JsValue]) match { 130 | case JsArray(elems) ⇒ 131 | Future.sequence(elems.map(e ⇒ makeRequest(namedType(c.ctx.field.fieldType), c.ctx, args, e))) map { v ⇒ 132 | extractValue(c.ctx.field.fieldType, Some(JsArray(v.asInstanceOf[Seq[JsValue]]): JsValue)) 133 | } 134 | case e ⇒ 135 | makeRequest(c.ctx.field.fieldType, c.ctx, args, e) 136 | } 137 | case None ⇒ 138 | makeRequest(c.ctx.field.fieldType, c.ctx, args) 139 | } 140 | }), 141 | 142 | ExistingFieldResolver { 143 | case (o: GraphQLIncludedSchema, _, f) if ctx.graphqlIncludes.exists(_.include.name == o.include.name) && f.astDirectives.exists(_.name == "delegate") ⇒ 144 | val schema = ctx.graphqlIncludes.find(_.include.name == o.include.name).get 145 | 146 | c ⇒ { 147 | val query = ast.Document(Vector(ast.OperationDefinition(ast.OperationType.Query, selections = c.astFields))) 148 | 149 | ctx.request(schema, query, c.astFields.head.outputName) 150 | } 151 | }, 152 | 153 | DirectiveFieldProvider(Dirs.IncludeField, _.withArgs(Args.Fields) { fields ⇒ 154 | fields.toList.flatMap { f ⇒ 155 | val name = f("schema").asInstanceOf[String] 156 | val typeName = f("type").asInstanceOf[String] 157 | val includes = f.get("fields").asInstanceOf[Option[Option[Seq[String]]]].flatten 158 | 159 | ctx.findFields(name, typeName, includes) 160 | } 161 | }), 162 | 163 | ExistingScalarResolver { 164 | case ctx ⇒ ctx.existing.copy( 165 | coerceUserInput = Right(_), 166 | coerceOutput = (v, _) ⇒ v, 167 | coerceInput = v ⇒ Right(queryAstInputUnmarshaller.getScalaScalarValue(v))) 168 | }, 169 | 170 | AnyFieldResolver.defaultInput[MatCtx, JsValue]) 171 | 172 | val rootValueLoc = Set(DirectiveLocation.Schema) 173 | 174 | def rootValue(schemaAst: ast.Document) = { 175 | val values = resolveDirectives(schemaAst, 176 | GenericDirectiveResolver(Dirs.JsonConst, rootValueLoc, 177 | c ⇒ Some(Json.parse(c arg Args.JsonValue))), 178 | GenericDynamicDirectiveResolver[JsValue, JsValue]("const", rootValueLoc, 179 | c ⇒ c.args.get("value"))) 180 | 181 | JsObject(values.foldLeft(Map.empty[String, JsValue]) { 182 | case (acc, JsObject(vv)) ⇒ acc ++ vv 183 | case (acc, _) ⇒ acc 184 | }) 185 | } 186 | 187 | def graphqlIncludes(schemaAst: ast.Document) = 188 | resolveDirectives(schemaAst, 189 | GenericDirectiveResolver(Dirs.IncludeGraphQL, resolve = 190 | c ⇒ Some(c.arg(Args.Schemas).map(s ⇒ GraphQLInclude(s("url").asInstanceOf[String], s("name").asInstanceOf[String]))))).flatten 191 | 192 | def loadIncludedSchemas(includes: Vector[GraphQLInclude]): Future[Vector[GraphQLIncludedSchema]] = { 193 | val loaded = 194 | includes.map { include ⇒ 195 | val introspectionBody = Json.obj("query" → sangria.introspection.introspectionQuery.renderPretty) 196 | 197 | client.url(include.url).post(introspectionBody).map(resp ⇒ 198 | GraphQLIncludedSchema(include, Schema.buildFromIntrospection(resp.json))) 199 | } 200 | 201 | Future.sequence(loaded) 202 | } 203 | 204 | def loadContext(schemaAst: ast.Document): Future[MatCtx] = { 205 | val includes = graphqlIncludes(schemaAst) 206 | val vars = rootValue(schemaAst) 207 | 208 | loadIncludedSchemas(includes).map(MatCtx(client, vars, _)) 209 | } 210 | 211 | def namedType(tpe: OutputType[_]): OutputType[_] = tpe match { 212 | case ListType(of) ⇒ namedType(of) 213 | case OptionType(of) ⇒ namedType(of) 214 | case t ⇒ t 215 | } 216 | 217 | private def convertArgs(args: Args, field: ast.Field): JsObject = 218 | JsObject(args.raw.keys.flatMap(name ⇒ field.arguments.find(_.name == name).map(a ⇒ a.name → a.value.convertMarshaled[JsValue])).toMap) 219 | 220 | private val PlaceholderRegExp = """\$\{([^}]+)\}""".r 221 | 222 | private def render(value: JsValue) = value match { 223 | case JsString(s) ⇒ s 224 | case JsNumber(n) ⇒ "" + n 225 | case JsBoolean(b) ⇒ "" + b 226 | case v ⇒ "" + v 227 | } 228 | 229 | private def fillPlaceholders(ctx: Context[MatCtx, _], value: String, cachedArgs: Option[JsObject] = None, elem: JsValue = JsNull): String = { 230 | lazy val args = cachedArgs getOrElse convertArgs(ctx.args, ctx.astFields.head) 231 | 232 | PlaceholderRegExp.findAllMatchIn(value).toVector.foldLeft(value) { case (acc, p) ⇒ 233 | val placeholder = p.group(0) 234 | 235 | val idx = p.group(1).indexOf(".") 236 | 237 | if (idx < 0) throw new IllegalStateException(s"Invalid placeholder '$placeholder'. It should contain two parts: scope (like value or ctx) and extractor (name of the field or JSON path) separated byt dot (.).") 238 | 239 | val (scope, selectorWithDot) = p.group(1).splitAt(idx) 240 | val selector = selectorWithDot.substring(1) 241 | 242 | val source = scope match { 243 | case "value" ⇒ ctx.value.asInstanceOf[JsValue] 244 | case "ctx" ⇒ ctx.ctx.vars 245 | case "arg" ⇒ args 246 | case "elem" ⇒ elem 247 | case s ⇒ throw new IllegalStateException(s"Unsupported placeholder scope '$s'. Supported scopes are: value, ctx, arg, elem.") 248 | } 249 | 250 | val value = 251 | if (selector.startsWith("$")) 252 | render(JsonPath.query(selector, source)) 253 | else 254 | source.get(selector).map(render).getOrElse("") 255 | 256 | acc.replace(placeholder, value) 257 | } 258 | } 259 | 260 | implicit class JsonOps(value: JsValue) { 261 | def get(key: String) = value match { 262 | case JsObject(fields) ⇒ fields.get(key) 263 | case _ ⇒ None 264 | } 265 | } 266 | } 267 | 268 | case class MatCtx(client: WSClient, vars: JsValue, graphqlIncludes: Vector[GraphQLIncludedSchema]) { 269 | val allTypes = graphqlIncludes.flatMap(_.types) 270 | 271 | def request(schema: GraphQLIncludedSchema, query: ast.Document, extractName: String)(implicit ec: ExecutionContext): Future[JsValue] = { 272 | val body = Json.obj("query" → query.renderPretty) 273 | 274 | client.url(schema.include.url).post(body).map(resp ⇒ ((resp.json \ "data").get \ extractName).get) 275 | } 276 | 277 | def findFields(name: String, typeName: String, includeFields: Option[Seq[String]]): List[MaterializedField[MatCtx, _]] = 278 | graphqlIncludes.find(_.include.name == name).toList.flatMap { s ⇒ 279 | val tpe = s.schema.getOutputType(ast.NamedType(typeName), topLevel = true) 280 | val fields = tpe.toList 281 | .collect {case obj: ObjectLikeType[_, _] ⇒ obj} 282 | .flatMap { t ⇒ 283 | val fields = includeFields match { 284 | case Some(inc) ⇒ t.uniqueFields.filter(f ⇒ includeFields contains f.name) 285 | case _ ⇒ t.uniqueFields 286 | } 287 | 288 | fields.asInstanceOf[Vector[Field[MatCtx, Any]]] 289 | } 290 | 291 | 292 | fields.map(f ⇒ MaterializedField(s, f.copy(astDirectives = Vector(ast.Directive("delegate", Vector.empty))))) 293 | } 294 | } 295 | 296 | case class GraphQLInclude(url: String, name: String) 297 | case class GraphQLIncludedSchema(include: GraphQLInclude, schema: Schema[_, _]) extends MatOrigin { 298 | private val rootTypeNames = Set(schema.query.name) ++ schema.mutation.map(_.name).toSet ++ schema.subscription.map(_.name).toSet 299 | 300 | val types = schema.allTypes.values 301 | .filterNot(t ⇒ Schema.isBuiltInType(t.name) || rootTypeNames.contains(t.name)).toVector 302 | .map(MaterializedType(this, _)) 303 | 304 | def description = s"included schema '${include.name}'" 305 | } -------------------------------------------------------------------------------- /public/images/sangria-logo-1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 11 | 14 | 18 | 19 | 25 | 30 | 32 | 34 | 36 | 38 | 40 | 42 | 44 | 46 | 47 | 48 | 54 | 59 | 61 | 63 | 65 | 67 | 69 | 71 | 73 | 75 | 76 | 77 | 80 | 83 | 84 | 85 | 88 | 91 | 92 | 93 | 94 | 104 | 111 | 118 | 124 | 129 | 132 | 135 | 144 | 145 | 146 | -------------------------------------------------------------------------------- /public/images/sangria-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 11 | 14 | 18 | 19 | 25 | 30 | 32 | 34 | 36 | 38 | 40 | 42 | 44 | 46 | 47 | 48 | 54 | 59 | 61 | 63 | 65 | 67 | 69 | 71 | 73 | 75 | 76 | 77 | 80 | 83 | 84 | 85 | 88 | 91 | 92 | 93 | 94 | 104 | 111 | 118 | 124 | 129 | 132 | 135 | 144 | 145 | 146 | -------------------------------------------------------------------------------- /public/images/sangria-relay-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 11 | 14 | 18 | 19 | 25 | 30 | 32 | 34 | 36 | 38 | 40 | 42 | 44 | 46 | 47 | 48 | 54 | 59 | 61 | 63 | 65 | 67 | 69 | 71 | 73 | 75 | 76 | 77 | 80 | 83 | 84 | 85 | 88 | 91 | 92 | 93 | 94 | 104 | 111 | 118 | 124 | 129 | 132 | 135 | 144 | 145 | 146 | 148 | 152 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | -------------------------------------------------------------------------------- /app/javascripts/GraphQLProxy.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import { SchemaEditor } from './SchemaEditor.jsx'; 5 | import GraphiQL from 'graphiql'; 6 | import CodeMirrorSizer from 'graphiql-workspace/dist/utility/CodeMirrorSizer'; 7 | import getQueryFacts from 'graphiql-workspace/dist/utility/getQueryFacts'; 8 | import getSelectedOperationName from 'graphiql-workspace/dist/utility/getSelectedOperationName'; 9 | import debounce from 'graphiql-workspace/dist/utility/debounce'; 10 | import find from 'graphiql-workspace/dist/utility/find'; 11 | import { fillLeafs } from 'graphiql-workspace/dist/utility/fillLeafs'; 12 | import { getLeft, getTop } from 'graphiql-workspace/dist/utility/elementPosition'; 13 | import { KeepLastTaskQueue } from 'graphiql-workspace'; 14 | 15 | import Modal from 'react-bootstrap/lib/Modal'; 16 | import Button from 'react-bootstrap/lib/Button'; 17 | 18 | import { 19 | introspectionQuery 20 | } from 'graphiql-workspace/dist/utility/introspectionQueries'; 21 | 22 | import { 23 | buildClientSchema, 24 | GraphQLSchema, 25 | parse, 26 | print, 27 | } from 'graphql'; 28 | 29 | import 'graphiql/graphiql.css' 30 | 31 | export class GraphQLProxy extends React.Component { 32 | static propTypes = { 33 | value: PropTypes.string, 34 | } 35 | 36 | constructor(props) { 37 | super(props); 38 | 39 | // Determine the initial query to display. 40 | const query = 41 | props.query !== undefined ? props.value : defaultQuery; 42 | 43 | // Initialize state 44 | this.state = { 45 | query, 46 | editorFlex: 0.4, 47 | }; 48 | 49 | this.taskQueue = new KeepLastTaskQueue() 50 | } 51 | 52 | componentDidMount() { 53 | // Utility for keeping CodeMirror correctly sized. 54 | this.codeMirrorSizer = new CodeMirrorSizer(); 55 | 56 | //this.handleFormat(this.state.query) 57 | } 58 | 59 | componentDidUpdate() { 60 | // If this update caused DOM nodes to have changed sizes, update the 61 | // corresponding CodeMirror instance sizes to match. 62 | this.codeMirrorSizer.updateSizes([ 63 | this.queryEditorComponent, 64 | //this.resultComponent, 65 | ]); 66 | } 67 | 68 | render() { 69 | const children = React.Children.toArray(this.props.children); 70 | 71 | const queryWrapStyle = { 72 | WebkitFlex: this.state.editorFlex, 73 | flex: this.state.editorFlex, 74 | }; 75 | 76 | return ( 77 |
{ this.editorBarComponent = n; }}> 78 |
79 |
80 |
81 |
82 | Schema Editor (help) 83 |
84 |
85 |
86 | { this.queryEditorComponent = n; }} 88 | value={this.state.query} 89 | onEdit={this.handleEditQuery} 90 | /> 91 | {this.state.error && 92 |
{this.state.error}
} 93 |
94 |
95 | { this.resultComponent = c; }} 97 | fetcher = {this.fetcher.bind(this)} 98 | schema = {this.state.schema} 99 | defaultQuery = {defaultGraphiqlQuery} 100 | /> 101 |
102 | 103 | 104 | 105 | Schema Definition Help 106 | 107 | 108 |

109 | Schema definition is based on GraphQL IDL additions. 110 | IDL syntax allows you to define full GraphQL schema with interfaces, types, enums etc. 111 | In order to provide resolution logic for the fields, you can use directives described below. 112 | Directives will define how fields will behave. By default (if no directive is provided), 113 | field resolve function will treat a contextual value as a JSON object and will return it's 114 | property with the same name. 115 |

116 | 117 |

Directives

118 | 119 |
120 |               directive @httpGet(url: String!, headers: ObjectOrList, query: ObjectOrList, forAll: String) on FIELD_DEFINITION
121 |             
122 | 123 |

124 | Provides a way to resolve the field with a result of a GET HTTP request.

125 | Supports following arguments: 126 |

127 | 128 |
    129 |
  • url - the URL of an HTTP request
  • 130 |
  • 131 | headers - headers that should be sent with the request. 132 | The value can be either an input object (e.g {`{Authorization: "Bearer FOOBARBAZ"}`}) 133 | or a list with name-value pairs (e.g. [{`{name: "Authorization", value: "Bearer FOOBARBAZ"}`}]) 134 |
  • 135 |
  • 136 | query - query string parameters that should be sent with the request. 137 | The value can be either an input object (e.g {`{limit: 10, offset: 0}`}) 138 | or a list with name-value pairs (e.g. [{`{name: "page-number", value: "1"}`}]) 139 |
  • 140 |
  • 141 | forAll - A JSON Path expression. For every element, 142 | returned by this expression executed against current context value, 143 | a separate HTTP request would be sent. An elem placeholder 144 | scope may be used in combination with this argument. 145 |
  • 146 |
147 | 148 |

149 | url, headers and query may contain the placeholders which are described below. 150 | value directive may be used in combination with httpGet - it will extract part of the relevant JSON out of the HTTP response. 151 |

152 | 153 |
154 |               directive @const(value: Any!) on FIELD_DEFINITION | SCHEMA
155 |             
156 | 157 |

158 | Provides a way to resolve a field with a constant value. 159 | value can be any valid GraphQL input value. It would be treated as a JSON value. 160 |

161 | 162 |
163 |               directive @jsonConst(value: String!) on FIELD_DEFINITION | SCHEMA
164 |             
165 | 166 |

167 | Provides a way to resolve a field with a constant value. 168 | value should be a valid JSON value. 169 |

170 | 171 |
172 |               directive @arg(name: String!) on FIELD_DEFINITION
173 |             
174 | 175 |

176 | Provides a way to resolve a field with value of one of its arguments. 177 |

178 | 179 |
180 |               directive @value(name: String, path: String) on FIELD_DEFINITION
181 |             
182 | 183 |

184 | Extracts a value(s) from the context object. It supports following extractors via arguments (only one can be used): 185 |

186 | 187 |
    188 |
  • name - Extracts a named property value from a context JSON object
  • 189 |
  • path - A JSON Path expression. It would be executed against current context JSON value.
  • 190 |
191 | 192 |
193 |               directive @context(name: String, path: String) on FIELD_DEFINITION
194 |             
195 | 196 |

197 | Extracts a value(s) from the context object defined on the schema level. It supports following extractors via arguments (only one can be used): 198 |

199 | 200 |
    201 |
  • name - Extracts a named property value from a JSON object
  • 202 |
  • path - A JSON Path expression. It would be executed against current context JSON value, which is defined at the schema level.
  • 203 |
204 | 205 |

Placeholders

206 | 207 |

Placeholders may be used in some the directive arguments (inside of the strings) and the syntax looks like this:

208 | 209 |
210 |               {`\${value.$.results[0].film}`}
211 |             
212 | 213 |

214 | The placeholder consists of two parts separated by dot (.): the scope (value in this case) and 215 | the extractor ($.results[0].film - a JSON Path extractor in this example). 216 | The scope defines a place/value from which you would like extract a value. Following scopes are supported: 217 |

218 | 219 |
    220 |
  • arg - field argument
  • 221 |
  • value - a context value
  • 222 |
  • ctx - a context value which is defined on a schema level
  • 223 |
  • elem - an extracted element that comes from the forAll argument
  • 224 |
225 | 226 |

227 | The extractor can be either a string (the name of the property) or a JSON Path expression. 228 |

229 | 230 |

Descriptions

231 | 232 |

233 | All elements of a schema (like types, fields, arguments, etc.) 234 | support descriptions. Here is an example: 235 |

236 | 237 |
{
238 | `"""
239 | The root query type.
240 | """
241 | type Query {
242 |   "A character from the StarWars"
243 |   person(
244 |     "ID of a character"
245 |     id: Int!): Person
246 | }`
247 |             }
248 |
249 | 250 | 251 | 252 |
253 |
254 | ); 255 | } 256 | 257 | helpHide() { 258 | this.setState({showHelp: false}) 259 | } 260 | 261 | help(e) { 262 | e.preventDefault() 263 | this.setState({showHelp: true}) 264 | } 265 | 266 | fetcher(params) { 267 | if (!params.schema) 268 | params.schema = this.state.query 269 | 270 | return fetch('/graphql-proxy', { 271 | method: 'post', 272 | headers: { 273 | 'Accept': 'application/json', 274 | 'Content-Type': 'application/json', 275 | }, 276 | body: JSON.stringify(params), 277 | credentials: 'include', 278 | }).then(function (response) { 279 | return response.text(); 280 | }).then(function (responseBody) { 281 | try { 282 | return JSON.parse(responseBody); 283 | } catch (error) { 284 | return responseBody; 285 | } 286 | }); 287 | } 288 | 289 | handleEditQuery = value => { 290 | this.taskQueue.add(() => { 291 | return this.updateSchema(value) 292 | }) 293 | } 294 | 295 | updateSchema(schema) { 296 | const fetch = this.fetcher({query: introspectionQuery, schema}); 297 | 298 | return fetch.then(result => { 299 | if (result && result.data) { 300 | this.setState({query: schema, schema: buildClientSchema(result.data), error: null}); 301 | } else { 302 | var responseString = typeof result === 'string' ? 303 | result : 304 | JSON.stringify(result, null, 2); 305 | 306 | if (result.materiamlizationError) { 307 | responseString = result.materiamlizationError 308 | } else if (result.syntaxError) { 309 | responseString = result.syntaxError 310 | } else if (result.unexpectedError) { 311 | responseString = result.unexpectedError 312 | } 313 | 314 | this.setState({ error: responseString }); 315 | } 316 | }).catch(error => { 317 | // TODO: handle error! 318 | this.setState({ error: error && (error.stack || String(error)) }); 319 | }); 320 | } 321 | 322 | handleResizeStart = downEvent => { 323 | if (!this._didClickDragBar(downEvent)) { 324 | return; 325 | } 326 | 327 | downEvent.preventDefault(); 328 | 329 | const offset = downEvent.clientX - getLeft(downEvent.target); 330 | 331 | let onMouseMove = moveEvent => { 332 | if (moveEvent.buttons === 0) { 333 | return onMouseUp(); 334 | } 335 | 336 | const editorBar = ReactDOM.findDOMNode(this.editorBarComponent); 337 | const leftSize = moveEvent.clientX - getLeft(editorBar) - offset; 338 | const rightSize = editorBar.clientWidth - leftSize; 339 | 340 | this.setState({ editorFlex: (leftSize / rightSize) }); 341 | }; 342 | 343 | let onMouseUp = () => { 344 | document.removeEventListener('mousemove', onMouseMove); 345 | document.removeEventListener('mouseup', onMouseUp); 346 | onMouseMove = null; 347 | onMouseUp = null; 348 | }; 349 | 350 | document.addEventListener('mousemove', onMouseMove); 351 | document.addEventListener('mouseup', onMouseUp); 352 | } 353 | 354 | _didClickDragBar(event) { 355 | // Only for primary unmodified clicks 356 | if (event.button !== 0 || event.ctrlKey) { 357 | return false; 358 | } 359 | let target = event.target; 360 | 361 | // We use codemirror's gutter as the drag bar. 362 | // `CodeMirror-linenumbers` tells us that it's a left pane (only left pane has line numbers) 363 | if (target.className.indexOf('CodeMirror-gutter') < 0 || target.className.indexOf('CodeMirror-linenumbers') < 0) { 364 | return false; 365 | } 366 | // Specifically the result window's drag bar. 367 | const resultWindow = ReactDOM.findDOMNode(this.resultComponent); 368 | while (target) { 369 | if (target === resultWindow) { 370 | return true; 371 | } 372 | target = target.parentNode; 373 | } 374 | return false; 375 | } 376 | } 377 | 378 | const defaultQuery = 379 | `# It's an example schema 380 | # that proxies some poarets of the http://swapi.co 381 | schema 382 | @includeGraphQL(schemas: [{ 383 | name: "starWars" 384 | url: "http://try.sangria-graphql.org/graphql" 385 | }, { 386 | name: "universe" 387 | url: "https://www.universe.com/graphql/beta" 388 | }]) { 389 | 390 | query: Query 391 | } 392 | 393 | """ 394 | The root query type. 395 | """ 396 | type Query 397 | @include(fields: [ 398 | {schema: "starWars", type: "Query"} 399 | {schema: "universe", type: "Query"} 400 | ]) { 401 | 402 | "A character from the StarWars (REST API)" 403 | person(id: Int!): Person 404 | @httpGet(url: "http://swapi.co/api/people/\${arg.id}") 405 | 406 | "A list of characters from the StarWars (REST API)" 407 | people(page: Int): [Person] 408 | @httpGet( 409 | url: "http://swapi.co/api/people" 410 | query: {name: "page", value: "\${arg.page}"}) 411 | @value(name: "results") 412 | } 413 | 414 | type Film { 415 | title: String 416 | } 417 | 418 | type Person { 419 | name: String 420 | size: Int @value(name: "height") 421 | homeworld: Planet @httpGet(url: "\${value.homeworld}") 422 | films: [Film] @httpGet(forAll: "$.films", url: "\${elem.$}") 423 | } 424 | 425 | "A planet from the StarWars universe" 426 | type Planet { 427 | name: String 428 | }`; 429 | 430 | const defaultGraphiqlQuery = 431 | `query { 432 | person(id: 1) { 433 | name 434 | size 435 | 436 | homeworld { 437 | name 438 | } 439 | 440 | films { 441 | title 442 | } 443 | } 444 | }` -------------------------------------------------------------------------------- /public/images/sip-of-sangria.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 12 | 15 | 19 | 20 | 26 | 31 | 33 | 35 | 37 | 39 | 41 | 43 | 45 | 47 | 48 | 49 | 55 | 60 | 62 | 64 | 66 | 68 | 70 | 72 | 74 | 76 | 77 | 78 | 81 | 84 | 85 | 86 | 89 | 92 | 93 | 94 | 95 | 106 | 113 | 120 | 126 | 131 | 134 | 137 | 146 | 147 | 148 | 160 | 171 | 180 | 189 | 200 | 208 | 218 | 224 | 231 | 236 | 241 | 242 | 243 | 244 | 245 | 246 | -------------------------------------------------------------------------------- /public/images/sip-of-sangria-1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 15 | 16 | 17 | 21 | 24 | 28 | 29 | 35 | 39 | 41 | 43 | 45 | 47 | 49 | 51 | 53 | 55 | 56 | 57 | 63 | 68 | 70 | 72 | 74 | 76 | 78 | 80 | 82 | 84 | 85 | 86 | 89 | 92 | 93 | 94 | 97 | 100 | 101 | 102 | 103 | 114 | 121 | 128 | 134 | 139 | 142 | 145 | 154 | 155 | 156 | 168 | 179 | 188 | 197 | 208 | 216 | 226 | 232 | 239 | 244 | 249 | 250 | 251 | 252 | 253 | 254 | 256 | 260 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | --------------------------------------------------------------------------------