├── .babelrc ├── .flowconfig ├── .gitignore ├── .prettierrc ├── CodeExporter.css ├── LICENSE ├── README.md ├── demo.gif ├── package.json ├── src ├── CodeExporter.css ├── CodeExporter.js ├── index.js ├── snippets │ ├── __helpers__ │ │ └── getOptionCombinations.js │ ├── index.js │ └── javascript │ │ ├── __tests__ │ │ ├── __snapshots__ │ │ │ ├── fetch-test.js.snap │ │ │ ├── reactApollo-test.js.snap │ │ │ └── reactHooks-test.js.snap │ │ ├── fetch-test.js │ │ ├── reactApollo-test.js │ │ └── reactHooks-test.js │ │ ├── fetch.js │ │ └── reactApollo.js ├── toposort.js └── utils │ ├── capitalizeFirstLetter.js │ ├── index.js │ └── jsCommentsFactory.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "es2015", 5 | { 6 | "modules": false 7 | } 8 | ], 9 | "stage-0", 10 | "react" 11 | ], 12 | "env": { 13 | "commonjs": { 14 | "plugins": [ 15 | "transform-es2015-modules-commonjs" 16 | ] 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/es/.* 3 | .*/lib/.* 4 | 5 | [include] 6 | 7 | [libs] 8 | 9 | [lints] 10 | 11 | [options] 12 | 13 | [strict] 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | es/ 3 | lib/ 4 | *.log -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "bracketSpacing": false, 5 | "jsxBracketSameLine": true, 6 | "printWidth": 80 7 | } 8 | -------------------------------------------------------------------------------- /CodeExporter.css: -------------------------------------------------------------------------------- 1 | .graphiql-code-exporter .CodeMirror { 2 | position: relative; 3 | font-size: 11px; 4 | background: transparent; 5 | } 6 | .graphiql-code-exporter .CodeMirror-lines { 7 | padding-top: 0; 8 | } 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 OneGraph 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Code Exporter for GraphiQL 2 | 3 | A GraphiQL addon that generates ready-to-run code for your queries and mutations. 4 | It provides a wide range of default snippets, but is also extendable with custom snippets. 5 | 6 | [![Bundlesize](https://badgen.net/bundlephobia/minzip/graphiql-code-exporter?color=green&icon=npm)](https://bundlephobia.com/result?p=graphiql-code-exporter) 7 | 8 | ![Demo](demo.gif) 9 | 10 | > Read the [introduction blog post](https://www.onegraph.com/blog/2019/05/03_Filling_in_the_GraphQL_Pipeline_Ready_to_use_code_generation.html) to learn why and how we built it! 11 | 12 | ## Installation 13 | 14 | ```sh 15 | # yarn 16 | yarn add graphiql-code-exporter 17 | 18 | # npm 19 | npm i --save graphiql-code-exporter 20 | ``` 21 | 22 | ## Built-in Snippets 23 | 24 | - **JavaScript** 25 | - fetch 26 | - react-apollo 27 | 28 | [ < your favorite language/framework/library > ](https://github.com/OneGraph/graphiql-code-exporter/pulls) 29 | 30 | ## Usage 31 | 32 | ```javascript 33 | import React, { Component, Fragment } from 'react' 34 | import GraphiQL from 'graphiql' 35 | import CodeExporter from 'graphiql-code-exporter' 36 | import 'graphiql-code-exporter/CodeExporter.css'; 37 | 38 | // Optional if you want to use a custom theme 39 | import 'codemirror/theme/neo.css'; 40 | 41 | const serverUrl = /* your server url here */ 42 | 43 | export default class GraphiQLWithCodeExporter extends Component { 44 | state = { 45 | codeExporterIsVisible: false, 46 | query: '' 47 | } 48 | 49 | toggleCodeExporter = () => this.setState({ 50 | codeExporterIsVisible: !this.state.codeExporterIsVisible 51 | }) 52 | 53 | updateQuery = query => this.setState({ 54 | query 55 | }) 56 | 57 | render() { 58 | const { query, codeExporterIsVisible } = this.state 59 | 60 | const codeExporter = codeExporterIsVisible ? ( 61 | 75 | ) : null 76 | 77 | return ( 78 | 79 | 82 | 87 | 88 | {codeExporter} 89 | 90 | ) 91 | } 92 | } 93 | ``` 94 | 95 | ## Props 96 | 97 | | Property | Type | Description | 98 | | ---------------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 99 | | hideCodeExporter | _Function_ | A callback function that is called when clicking the close (x) button in the upper right corner of the panel. | 100 | | serverUrl | _URI_ | The server url for your GraphQL endpoint. | 101 | | query | _string_ | A string containing the GraphQL query that is synced with the GraphiQL query editor. | 102 | | snippets | _Snippet[]_ | A list of snippet objects that one can choose from to generate code snippets. | 103 | | headers | _Object?_ |  An optional object containing app specific HTTP headers | 104 | | context | _Object?_ |  An optional object containing any additional keys required to render app specific snippets | 105 | | codeMirrorTheme | _string?_ | The name of the [CodeMirror theme](https://codemirror.net/demo/theme.html) you'd like to use e.g. `neo`. Make sure to also import the theme's css in your code (e.g. `import 'codemirror/theme/neo.css')` | 106 | 107 | ## Snippets 108 | 109 | What we call **snippet** here, is actually an object with 4 required keys. 110 | 111 | | Key | Type | Description | 112 | |--------------------------|------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------| 113 | | name | _string_ | A name that is used to identify the snippet. | 114 | | language | _string_ | A language string that is used to group the snippets by language. | 115 | | codeMirrorMode | _string_ | A valid [CodeMirror mode](https://codemirror.net/mode/) used for syntax highlighting. Make sure to also require the mode (e.g. `import 'codemirror/mode/jsx/jsx'` | 116 | | options | _Option[]_ | Options are rendered as checkboxes and can be used to customize snippets. They must have an unique id, a label and an initial value of either true or false. | 117 | | generate | _Function_ | A function that returns the generated code as a single string. It receives below listed data as an object. | 118 | | generateCodesandboxFiles | _Function_ | A function that returns a set of codesandbox files, e.g. `{'index.js': {content: 'console.log("hello world")'}}`. It receives below listed data as an object. | 119 | 120 | #### Snippet Data 121 | 122 | | Key | Type |  Description | 123 | | ---------- | ------------- | --------------------------------------------------------------------------------------- | 124 | | serverUrl | _string_ | The passed GraphQL server url | 125 | | operations | _Operation[]_ | A list of GraphQL operations where each operation has the [operation](#operation) keys. | 126 | | options | *Object*  | A map of option-boolean pairs providing whether an option is selected or not | 127 | | headers | _Object?_ | The `headers` object that is passed to the CodeExporter component | 128 | | context | _Object?_ | The `context` object that is passed to the CodeExporter component | 129 | 130 | ##### Operation 131 | 132 | | Key |  Type | Description | 133 | | ------------ | ----------------------- | --------------------------------------------------------------------------------------------------------- | 134 | | name | _string_ | The selected GraphQL operation name | 135 | | type | _"query" \| "mutation"_ | The selected operation's type | 136 | | query |  *string* |  A formatted string containing the GraphQL operation |   | 137 | | variableName | _string_ | The operation name but in UPPER_CASE as that's the common way to declare GraphQL operations in JavaScript | 138 | | operation | _Object_ | The plain GraphQL parser result for this operation | 139 | | variables | _Object_ |  A map of all used variables for this specific operation | 140 | 141 | #### Example 142 | 143 | The following example implements a subset of the built-in _Fetch API_ snippet. 144 | The output will look similar to the demo above. 145 | 146 | ```javascript 147 | const fetchSnippet = { 148 | language: 'JavaScript', 149 | prismLanguage: 'javascript', 150 | name: 'Fetch API', 151 | options: [ 152 | { 153 | id: 'server', 154 | label: 'server-side usage', 155 | initial: false, 156 | }, 157 | ], 158 | generate: ({serverUrl, operations, options}) => { 159 | // we only render the first operation here 160 | const {query} = operations[0]; 161 | 162 | const serverImport = options.server 163 | ? 'import { fetch } from "node-fetch"' 164 | : ''; 165 | 166 | return ` 167 | ${serverImport} 168 | 169 | const res = await fetch("${serverUrl}", { 170 | method: 'POST', 171 | body: JSON.stringify({ query: \`${query}\` }), 172 | }) 173 | const { errors, data } = await res.json() 174 | 175 | // Do something with the response 176 | console.log(data, errors) 177 | `; 178 | }, 179 | }; 180 | ``` 181 | 182 | #### Extending the built-in snippets 183 | 184 | If we want to use both custom and all the built-in snippets, we can import them from npm. 185 | 186 | ```javascript 187 | import snippets from 'graphiql-code-exporter/lib/snippets' 188 | 189 | const customSnippet = /* custom snippet */ 190 | 191 | const extendedSnippets = [ 192 | ...snippets, 193 | customSnippet 194 | ] 195 | ``` 196 | 197 | This is also useful if you want to filter or modify single snippets. 198 | 199 | ## License 200 | 201 | graphiql-code-exporter is licensed under the [MIT License](http://opensource.org/licenses/MIT).
202 | Documentation is licensed under [Creative Common License](http://creativecommons.org/licenses/by/4.0/).
203 | Created with ♥ by [@rofrischmann](http://rofrischmann.de) and all the great contributors. 204 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OneGraph/graphiql-code-exporter/ab861a9ab0dbcf8b566d120f10be589055e3e7c2/demo.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphiql-code-exporter", 3 | "version": "3.0.2", 4 | "description": "Export working code snippets from GraphiQL queries", 5 | "main": "lib/index.js", 6 | "module": "es/index.js", 7 | "jsnext:main": "es/index.js", 8 | "files": [ 9 | "lib", 10 | "src", 11 | "es", 12 | "CodeExporter.css" 13 | ], 14 | "scripts": { 15 | "build:es": "babel -d es src --ignore __tests__", 16 | "build:lib": "cross-env BABEL_ENV=commonjs babel -d lib src --ignore __tests__", 17 | "build": "yarn build:es && yarn build:lib && yarn build:flow:es && yarn build:flow:lib", 18 | "build:clean": "rm -r ./lib && mkdir ./lib && rm -r ./es && mkdir ./es", 19 | "build:flow:es": "flow-copy-source -v -i '**/__tests__/**' src es", 20 | "build:flow:lib": "flow-copy-source -v -i '**/__tests__/**' src lib", 21 | "prettier": "prettier --write \"src/**/*.js\" \"docs/**/*.md\" README.md", 22 | "release": "yarn build && npm publish", 23 | "test": "yarn flow && cross-env BABEL_ENV=commonjs jest", 24 | "flow": "flow" 25 | }, 26 | "prettier": { 27 | "singleQuote": true, 28 | "trailingComma": "all", 29 | "bracketSpacing": false, 30 | "jsxBracketSameLine": true, 31 | "printWidth": 80 32 | }, 33 | "repository": { 34 | "type": "git", 35 | "url": "git+https://github.com/OneGraph/graphiql-code-exporter.git" 36 | }, 37 | "keywords": [ 38 | "onegraph", 39 | "graphql", 40 | "graphiql", 41 | "tools", 42 | "devTools", 43 | "DX" 44 | ], 45 | "author": "", 46 | "license": "MIT", 47 | "bugs": { 48 | "url": "https://github.com/OneGraph/graphiql-code-exporter/issues" 49 | }, 50 | "homepage": "https://github.com/OneGraph/graphiql-code-exporter#readme", 51 | "peerDependencies": { 52 | "codemirror": "^5.26.0", 53 | "graphql": "^14.1.1", 54 | "react": "^15.6.0 || ^16.0.0", 55 | "react-dom": "^16.2.0" 56 | }, 57 | "dependencies": { 58 | "copy-to-clipboard": "^3.0.8" 59 | }, 60 | "devDependencies": { 61 | "babel": "^6.5.2", 62 | "babel-cli": "^6.6.0", 63 | "babel-core": "^6.6.0", 64 | "babel-jest": "^19.0.0", 65 | "babel-plugin-transform-es2015-modules-commonjs": "^6.26.2", 66 | "babel-preset-es2015": "^6.6.0", 67 | "babel-preset-react": "^6.5.0", 68 | "babel-preset-stage-0": "^6.5.0", 69 | "codemirror": "^5.44.0", 70 | "cross-env": "^5.2.0", 71 | "flow-bin": "^0.94.0", 72 | "flow-copy-source": "^2.0.3", 73 | "graphql": "^14.1.1", 74 | "jest": "^19.0.0", 75 | "prettier": "^1.16.4", 76 | "react": "^16.7.0", 77 | "react-dom": "^16.2.0" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/CodeExporter.css: -------------------------------------------------------------------------------- 1 | .graphiql-code-exporter .CodeMirror { 2 | position: relative; 3 | font-size: 11px; 4 | background: transparent; 5 | } 6 | .graphiql-code-exporter .CodeMirror-lines { 7 | padding-top: 0; 8 | } 9 | 10 | .graphiql-code-exporter .CodeMirror-cursors { 11 | display: none; 12 | } 13 | -------------------------------------------------------------------------------- /src/CodeExporter.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, {Component} from 'react'; 3 | import copy from 'copy-to-clipboard'; 4 | import {parse, print} from 'graphql'; 5 | // $FlowFixMe: can't find module 6 | import CodeMirror from 'codemirror'; 7 | import toposort from './toposort.js'; 8 | 9 | import type { 10 | GraphQLSchema, 11 | FragmentDefinitionNode, 12 | OperationDefinitionNode, 13 | VariableDefinitionNode, 14 | OperationTypeNode, 15 | SelectionSetNode, 16 | } from 'graphql'; 17 | 18 | function formatVariableName(name: string) { 19 | var uppercasePattern = /[A-Z]/g; 20 | 21 | return ( 22 | name.charAt(0).toUpperCase() + 23 | name 24 | .slice(1) 25 | .replace(uppercasePattern, '_$&') 26 | .toUpperCase() 27 | ); 28 | } 29 | 30 | const copyIcon = ( 31 | 36 | 37 | 38 | 39 | ); 40 | 41 | const codesandboxIcon = ( 42 | 49 | 50 | 54 | 55 | 56 | ); 57 | 58 | export type Variables = {[key: string]: ?mixed}; 59 | 60 | // TODO: Need clearer separation between option defs and option values 61 | export type Options = Array<{id: string, label: string, initial: boolean}>; 62 | 63 | export type OptionValues = {[id: string]: boolean}; 64 | 65 | export type OperationData = { 66 | query: string, 67 | name: string, 68 | displayName: string, 69 | type: OperationTypeNode | 'fragment', 70 | variableName: string, 71 | variables: Variables, 72 | operationDefinition: OperationDefinitionNode | FragmentDefinitionNode, 73 | fragmentDependencies: Array, 74 | }; 75 | 76 | export type GenerateOptions = { 77 | serverUrl: string, 78 | headers: {[name: string]: string}, 79 | context: Object, 80 | operationDataList: Array, 81 | options: OptionValues, 82 | }; 83 | 84 | export type CodesandboxFile = { 85 | content: string | mixed, 86 | }; 87 | 88 | export type CodesandboxFiles = { 89 | [filename: string]: CodesandboxFile, 90 | }; 91 | 92 | export type Snippet = { 93 | options: Options, 94 | language: string, 95 | codeMirrorMode: string, 96 | name: string, 97 | generate: (options: GenerateOptions) => string, 98 | generateCodesandboxFiles?: ?(options: GenerateOptions) => CodesandboxFiles, 99 | }; 100 | 101 | export const computeOperationDataList = ({ 102 | query, 103 | variables, 104 | }: { 105 | query: string, 106 | variables: Variables, 107 | }) => { 108 | const operationDefinitions = getOperationNodes(query); 109 | 110 | const fragmentDefinitions: Array = []; 111 | 112 | for (const operationDefinition of operationDefinitions) { 113 | if (operationDefinition.kind === 'FragmentDefinition') { 114 | fragmentDefinitions.push(operationDefinition); 115 | } 116 | } 117 | 118 | const rawOperationDataList: Array = operationDefinitions.map( 119 | ( 120 | operationDefinition: OperationDefinitionNode | FragmentDefinitionNode, 121 | ) => ({ 122 | query: print(operationDefinition), 123 | name: getOperationName(operationDefinition), 124 | displayName: getOperationDisplayName(operationDefinition), 125 | // $FlowFixMe: Come back for this 126 | type: operationDefinition.operation || 'fragment', 127 | variableName: formatVariableName(getOperationName(operationDefinition)), 128 | variables: getUsedVariables(variables, operationDefinition), 129 | operationDefinition, 130 | fragmentDependencies: findFragmentDependencies( 131 | fragmentDefinitions, 132 | operationDefinition, 133 | ), 134 | }), 135 | ); 136 | 137 | const operationDataList = toposort(rawOperationDataList); 138 | 139 | return { 140 | operationDefinitions: operationDefinitions, 141 | fragmentDefinitions: fragmentDefinitions, 142 | rawOperationDataList: rawOperationDataList, 143 | operationDataList: operationDataList, 144 | }; 145 | }; 146 | 147 | async function createCodesandbox( 148 | files: CodesandboxFiles, 149 | ): Promise<{sandboxId: string}> { 150 | const res = await fetch( 151 | 'https://codesandbox.io/api/v1/sandboxes/define?json=1', 152 | { 153 | method: 'POST', 154 | headers: { 155 | 'Content-Type': 'application/json', 156 | Accept: 'application/json', 157 | }, 158 | body: JSON.stringify({files}), 159 | }, 160 | ); 161 | const json = await res.json(); 162 | if (!json.sandbox_id) { 163 | throw new Error('Invalid response from Codesandbox API'); 164 | } else { 165 | return {sandboxId: json.sandbox_id}; 166 | } 167 | } 168 | 169 | let findFragmentDependencies = ( 170 | operationDefinitions: Array, 171 | def: OperationDefinitionNode | FragmentDefinitionNode, 172 | ): Array => { 173 | const fragmentByName = (name: string) => { 174 | return operationDefinitions.find(def => def.name.value === name); 175 | }; 176 | 177 | const findReferencedFragments = ( 178 | selectionSet: SelectionSetNode, 179 | ): Array => { 180 | const selections = selectionSet.selections; 181 | 182 | const namedFragments = selections 183 | .map(selection => { 184 | if (selection.kind === 'FragmentSpread') { 185 | return fragmentByName(selection.name.value); 186 | } else { 187 | return null; 188 | } 189 | }) 190 | .filter(Boolean); 191 | 192 | const nestedNamedFragments: Array = selections.reduce( 193 | (acc, selection) => { 194 | if ( 195 | (selection.kind === 'Field' || 196 | selection.kind === 'SelectionNode' || 197 | selection.kind === 'InlineFragment') && 198 | selection.selectionSet !== undefined 199 | ) { 200 | return acc.concat(findReferencedFragments(selection.selectionSet)); 201 | } else { 202 | return acc; 203 | } 204 | }, 205 | [], 206 | ); 207 | 208 | return namedFragments.concat(nestedNamedFragments); 209 | }; 210 | 211 | const selectionSet = def.selectionSet; 212 | 213 | return findReferencedFragments(selectionSet); 214 | }; 215 | 216 | let operationNodesMemo: [ 217 | ?string, 218 | ?Array, 219 | ] = [null, null]; 220 | function getOperationNodes( 221 | query: string, 222 | ): Array { 223 | if (operationNodesMemo[0] === query && operationNodesMemo[1]) { 224 | return operationNodesMemo[1]; 225 | } 226 | const operationDefinitions = []; 227 | try { 228 | for (const def of parse(query).definitions) { 229 | if ( 230 | def.kind === 'FragmentDefinition' || 231 | def.kind === 'OperationDefinition' 232 | ) { 233 | operationDefinitions.push(def); 234 | } 235 | } 236 | } catch (e) {} 237 | operationNodesMemo = [query, operationDefinitions]; 238 | return operationDefinitions; 239 | } 240 | 241 | const getUsedVariables = ( 242 | variables: Variables, 243 | operationDefinition: OperationDefinitionNode | FragmentDefinitionNode, 244 | ): Variables => { 245 | return (operationDefinition.variableDefinitions || []).reduce( 246 | (usedVariables, variable: VariableDefinitionNode) => { 247 | const variableName = variable.variable.name.value; 248 | if (variables[variableName]) { 249 | usedVariables[variableName] = variables[variableName]; 250 | } 251 | 252 | return usedVariables; 253 | }, 254 | {}, 255 | ); 256 | }; 257 | 258 | const getOperationName = ( 259 | operationDefinition: OperationDefinitionNode | FragmentDefinitionNode, 260 | ) => 261 | operationDefinition.name 262 | ? operationDefinition.name.value 263 | : operationDefinition.operation; 264 | 265 | const getOperationDisplayName = (operationDefinition): string => 266 | operationDefinition.name 267 | ? operationDefinition.name.value 268 | : ''; 269 | 270 | /** 271 | * ToolbarMenu 272 | * 273 | * A menu style button to use within the Toolbar. 274 | * Copied from GraphiQL: https://github.com/graphql/graphiql/blob/272e2371fc7715217739efd7817ce6343cb4fbec/src/components/ToolbarMenu.js#L16-L80 275 | */ 276 | export class ToolbarMenu extends Component< 277 | {title: string, label: string, children: React$Node}, 278 | {visible: boolean}, 279 | > { 280 | state = {visible: false}; 281 | _node: ?HTMLAnchorElement; 282 | _listener: ?(e: Event) => void; 283 | 284 | componentWillUnmount() { 285 | this._release(); 286 | } 287 | 288 | render() { 289 | const visible = this.state.visible; 290 | return ( 291 | // eslint-disable-next-line 292 | e.preventDefault()} 296 | ref={node => { 297 | this._node = node; 298 | }} 299 | title={this.props.title}> 300 | {this.props.label} 301 | 302 | 303 | 304 |
    305 | {this.props.children} 306 |
307 |
308 | ); 309 | } 310 | 311 | _subscribe() { 312 | if (!this._listener) { 313 | this._listener = this.handleClick.bind(this); 314 | document.addEventListener('click', this._listener); 315 | } 316 | } 317 | 318 | _release() { 319 | if (this._listener) { 320 | document.removeEventListener('click', this._listener); 321 | this._listener = null; 322 | } 323 | } 324 | 325 | handleClick(e: Event) { 326 | if (this._node !== e.target) { 327 | e.preventDefault(); 328 | this.setState({visible: false}); 329 | this._release(); 330 | } 331 | } 332 | 333 | handleOpen = (e: Event) => { 334 | e.preventDefault(); 335 | this.setState({visible: true}); 336 | this._subscribe(); 337 | }; 338 | } 339 | 340 | type CodeDisplayProps = {code: string, mode: string, theme: ?string}; 341 | 342 | class CodeDisplay extends React.PureComponent { 343 | _node: ?HTMLDivElement; 344 | editor: CodeMirror; 345 | componentDidMount() { 346 | this.editor = CodeMirror(this._node, { 347 | value: this.props.code.trim(), 348 | lineNumbers: false, 349 | mode: this.props.mode, 350 | readOnly: true, 351 | theme: this.props.theme, 352 | }); 353 | } 354 | 355 | componentDidUpdate(prevProps: CodeDisplayProps) { 356 | if (this.props.code !== prevProps.code) { 357 | this.editor.setValue(this.props.code); 358 | } 359 | if (this.props.mode !== prevProps.mode) { 360 | this.editor.setOption('mode', this.props.mode); 361 | } 362 | if (this.props.theme !== prevProps.theme) { 363 | this.editor.setOption('theme', this.props.theme); 364 | } 365 | } 366 | 367 | render() { 368 | return
(this._node = ref)} />; 369 | } 370 | } 371 | 372 | type Props = {| 373 | snippet: ?Snippet, 374 | snippets: Array, 375 | query: string, 376 | serverUrl: string, 377 | context: Object, 378 | variables: Variables, 379 | headers: {[name: string]: string}, 380 | setOptionValue?: (id: string, value: boolean) => void, 381 | optionValues: OptionValues, 382 | codeMirrorTheme: ?string, 383 | onSelectSnippet: ?(snippet: Snippet) => void, 384 | onSetOptionValue: ?(snippet: Snippet, option: string, value: boolean) => void, 385 | onGenerateCodesandbox?: ?({sandboxId: string}) => void, 386 | schema: ?GraphQLSchema, 387 | |}; 388 | type State = {| 389 | showCopiedTooltip: boolean, 390 | optionValuesBySnippet: Map, 391 | snippet: ?Snippet, 392 | codesandboxResult: 393 | | null 394 | | {type: 'loading'} 395 | | {type: 'success', sandboxId: string} 396 | | {type: 'error', error: string}, 397 | |}; 398 | 399 | class CodeExporter extends Component { 400 | style: ?HTMLLinkElement; 401 | state = { 402 | showCopiedTooltip: false, 403 | optionValuesBySnippet: new Map(), 404 | snippet: null, 405 | codesandboxResult: null, 406 | }; 407 | 408 | _activeSnippet = (): Snippet => 409 | this.props.snippet || this.state.snippet || this.props.snippets[0]; 410 | 411 | setSnippet = (snippet: Snippet) => { 412 | this.props.onSelectSnippet && this.props.onSelectSnippet(snippet); 413 | this.setState({snippet, codesandboxResult: null}); 414 | }; 415 | 416 | setLanguage = (language: string) => { 417 | const snippet = this.props.snippets.find( 418 | snippet => snippet.language === language, 419 | ); 420 | 421 | if (snippet) { 422 | this.setSnippet(snippet); 423 | } 424 | }; 425 | 426 | handleSetOptionValue = (snippet: Snippet, id: string, value: boolean) => { 427 | this.props.onSetOptionValue && 428 | this.props.onSetOptionValue(snippet, id, value); 429 | const {optionValuesBySnippet} = this.state; 430 | const snippetOptions = optionValuesBySnippet.get(snippet) || {}; 431 | optionValuesBySnippet.set(snippet, {...snippetOptions, [id]: value}); 432 | 433 | return this.setState({optionValuesBySnippet}); 434 | }; 435 | 436 | getOptionValues = (snippet: Snippet) => { 437 | const snippetDefaults = snippet.options.reduce( 438 | (acc, option) => ({...acc, [option.id]: option.initial}), 439 | {}, 440 | ); 441 | return { 442 | ...snippetDefaults, 443 | ...(this.state.optionValuesBySnippet.get(snippet) || {}), 444 | ...this.props.optionValues, 445 | }; 446 | }; 447 | 448 | _generateCodesandbox = async (operationDataList: Array) => { 449 | this.setState({codesandboxResult: {type: 'loading'}}); 450 | const snippet = this._activeSnippet(); 451 | if (!snippet) { 452 | // Shouldn't be able to get in this state, but just in case... 453 | this.setState({ 454 | codesandboxResult: {type: 'error', error: 'No active snippet'}, 455 | }); 456 | return; 457 | } 458 | const generateFiles = snippet.generateCodesandboxFiles; 459 | if (!generateFiles) { 460 | // Shouldn't be able to get in this state, but just in case... 461 | this.setState({ 462 | codesandboxResult: { 463 | type: 'error', 464 | error: 'Snippet does not support CodeSandbox', 465 | }, 466 | }); 467 | return; 468 | } 469 | try { 470 | const sandboxResult = await createCodesandbox( 471 | generateFiles( 472 | this._collectOptions(snippet, operationDataList, this.props.schema), 473 | ), 474 | ); 475 | this.setState({ 476 | codesandboxResult: {type: 'success', ...sandboxResult}, 477 | }); 478 | this.props.onGenerateCodesandbox && 479 | this.props.onGenerateCodesandbox(sandboxResult); 480 | } catch (e) { 481 | console.error('Error generating codesandbox', e); 482 | this.setState({ 483 | codesandboxResult: { 484 | type: 'error', 485 | error: 'Failed to generate CodeSandbox', 486 | }, 487 | }); 488 | } 489 | }; 490 | 491 | _collectOptions = ( 492 | snippet: Snippet, 493 | operationDataList: Array, 494 | schema: ?GraphQLSchema, 495 | ): GenerateOptions => { 496 | const {serverUrl, context = {}, headers = {}} = this.props; 497 | const optionValues = this.getOptionValues(snippet); 498 | return { 499 | serverUrl, 500 | headers, 501 | context, 502 | operationDataList, 503 | options: optionValues, 504 | schema, 505 | }; 506 | }; 507 | 508 | render() { 509 | const {query, snippets, variables = {}} = this.props; 510 | const {showCopiedTooltip, codesandboxResult} = this.state; 511 | 512 | const snippet = this._activeSnippet(); 513 | const {name, language, generate} = snippet; 514 | 515 | const { 516 | operationDefinitions: operationDefinitions, 517 | fragmentDefinitions: fragmentDefinitions, 518 | rawOperationDataList: rawOperationDataList, 519 | operationDataList: operationDataList, 520 | } = computeOperationDataList({query, variables}); 521 | 522 | const optionValues: Array = this.getOptionValues(snippet); 523 | 524 | const codeSnippet = operationDefinitions.length 525 | ? generate( 526 | this._collectOptions(snippet, operationDataList, this.props.schema), 527 | ) 528 | : null; 529 | 530 | const supportsCodesandbox = snippet.generateCodesandboxFiles; 531 | 532 | const languages = [ 533 | ...new Set(snippets.map(snippet => snippet.language)), 534 | ].sort((a, b) => a.localeCompare(b)); 535 | 536 | return ( 537 |
538 |
543 |
544 | 545 | {languages.map((lang: string) => ( 546 |
  • this.setLanguage(lang)}> 547 | {lang} 548 |
  • 549 | ))} 550 |
    551 | 552 | {snippets 553 | .filter(snippet => snippet.language === language) 554 | .map(snippet => ( 555 |
  • this.setSnippet(snippet)}> 558 | {snippet.name} 559 |
  • 560 | ))} 561 |
    562 |
    563 | {snippet.options.length > 0 ? ( 564 |
    565 |
    572 | Options 573 |
    574 | {snippet.options.map(option => ( 575 |
    576 | 583 | this.handleSetOptionValue( 584 | snippet, 585 | option.id, 586 | // $FlowFixMe: Come back for this 587 | !optionValues[option.id], 588 | ) 589 | } 590 | /> 591 | 594 |
    595 | ))} 596 |
    597 | ) : ( 598 |
    599 | )} 600 | {supportsCodesandbox ? ( 601 |
    602 | 624 | {codesandboxResult ? ( 625 |
    626 | {codesandboxResult.type === 'loading' ? ( 627 | 'Loading...' 628 | ) : codesandboxResult.type === 'error' ? ( 629 | `Error: ${codesandboxResult.error}` 630 | ) : ( 631 | 635 | Visit CodeSandbox 636 | 637 | )} 638 |
    639 | ) : null} 640 |
    641 | ) : null} 642 |
    643 | 686 |
    693 | {codeSnippet ? ( 694 | 699 | ) : ( 700 |
    701 | The query is invalid. 702 |
    703 | The generated code will appear here once the errors in the query 704 | editor are resolved. 705 |
    706 | )} 707 |
    708 |
    709 | ); 710 | } 711 | } 712 | 713 | class ErrorBoundary extends React.Component<*, {hasError: boolean}> { 714 | state = {hasError: false}; 715 | 716 | componentDidCatch(error, info) { 717 | this.setState({hasError: true}); 718 | console.error('Error in component', error, info); 719 | } 720 | 721 | render() { 722 | if (this.state.hasError) { 723 | return ( 724 |
    725 | Error generating code. Please{' '} 726 | 730 | report your query on Spectrum 731 | 732 | . 733 |
    734 | ); 735 | } 736 | return this.props.children; 737 | } 738 | } 739 | 740 | type WrapperProps = { 741 | query: string, 742 | serverUrl: string, 743 | variables: string, 744 | context: Object, 745 | headers?: {[name: string]: string}, 746 | hideCodeExporter: () => void, 747 | snippets: Array, 748 | snippet?: Snippet, 749 | codeMirrorTheme?: string, 750 | onSelectSnippet?: (snippet: Snippet) => void, 751 | onSetOptionValue?: (snippet: Snippet, option: string, value: boolean) => void, 752 | optionValues?: OptionValues, 753 | onGenerateCodesandbox?: ?({sandboxId: string}) => void, 754 | schema: ?GraphQLSchema, 755 | }; 756 | 757 | // we borrow class names from graphiql's CSS as the visual appearance is the same 758 | // yet we might want to change that at some point in order to have a self-contained standalone 759 | export default function CodeExporterWrapper({ 760 | query, 761 | serverUrl, 762 | variables, 763 | context = {}, 764 | headers = {}, 765 | hideCodeExporter = () => {}, 766 | snippets, 767 | snippet, 768 | codeMirrorTheme, 769 | onSelectSnippet, 770 | onSetOptionValue, 771 | optionValues, 772 | onGenerateCodesandbox, 773 | schema, 774 | }: WrapperProps) { 775 | let jsonVariables: Variables = {}; 776 | 777 | try { 778 | const parsedVariables = JSON.parse(variables); 779 | if (typeof parsedVariables === 'object') { 780 | jsonVariables = parsedVariables; 781 | } 782 | } catch (e) {} 783 | 784 | return ( 785 |
    792 |
    793 |
    Code Exporter
    794 |
    795 |
    796 | {'\u2715'} 797 |
    798 |
    799 |
    800 |
    803 | {snippets.length ? ( 804 | 805 | 820 | 821 | ) : ( 822 |
    823 | Please provide a list of snippets 824 |
    825 | )} 826 |
    827 |
    828 | ); 829 | } 830 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import CodeExporter, {computeOperationDataList} from './CodeExporter'; 4 | import capitalizeFirstLetter from './utils/capitalizeFirstLetter'; 5 | import jsCommentsFactory from './utils/jsCommentsFactory'; 6 | import snippets from './snippets/index'; 7 | 8 | export type { 9 | CodesandboxFile, 10 | CodesandboxFiles, 11 | Snippet, 12 | GenerateOptions, 13 | OperationData, 14 | Options, 15 | OptionValues, 16 | Variables, 17 | } from './CodeExporter'; 18 | 19 | export { 20 | computeOperationDataList, 21 | capitalizeFirstLetter, 22 | jsCommentsFactory, 23 | snippets, 24 | }; 25 | 26 | export default CodeExporter; 27 | -------------------------------------------------------------------------------- /src/snippets/__helpers__/getOptionCombinations.js: -------------------------------------------------------------------------------- 1 | export default function getOptionCombinations(options) { 2 | const optionIds = options.map(option => option.id); 3 | const combinationCount = Math.pow(2, optionIds.length); 4 | 5 | return Array.from(Array(combinationCount).keys()).reduce( 6 | (combinations, index) => { 7 | const booleanValues = index 8 | .toString(2) 9 | .padStart(3, '0') 10 | .split(''); 11 | 12 | const optionMap = optionIds.reduce((map, name, i) => { 13 | map[name] = Boolean(parseInt(booleanValues[i])); 14 | return map; 15 | }, {}); 16 | 17 | combinations.push(optionMap); 18 | return combinations; 19 | }, 20 | [], 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/snippets/index.js: -------------------------------------------------------------------------------- 1 | // javascript 2 | import jsFetch from './javascript/fetch'; 3 | import jsReactApollo from './javascript/reactApollo'; 4 | 5 | export default [jsFetch, jsReactApollo]; 6 | -------------------------------------------------------------------------------- /src/snippets/javascript/__tests__/__snapshots__/fetch-test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Generating a JavaScript:fetch snippet should generate the correct mutation snippet 1`] = ` 4 | Object { 5 | "options": Object { 6 | "asyncAwait": false, 7 | "comments": false, 8 | "server": false, 9 | }, 10 | "snippet": " 11 | const TEST_MUTATION = \` 12 | 13 | mutation testMutation { 14 | addData(id: \\"id\\") { 15 | id 16 | } 17 | }\` 18 | 19 | const serverUrl = \\"https://api.myservice.com/\\" 20 | 21 | fetch(serverUrl, { 22 | method: \\"POST\\", 23 | headers: {}, 24 | body: JSON.stringify({ 25 | query: TEST_MUTATION, 26 | variables: {} 27 | }) 28 | }) 29 | .then(res => res.json()) 30 | .then(({ data, errors }) => { 31 | if (errors) { 32 | console.error(errors) 33 | } 34 | 35 | console.log(data) 36 | }) 37 | .catch(err => { 38 | console.error(err) 39 | }) 40 | ", 41 | } 42 | `; 43 | 44 | exports[`Generating a JavaScript:fetch snippet should generate the correct mutation snippet 2`] = ` 45 | Object { 46 | "options": Object { 47 | "asyncAwait": true, 48 | "comments": false, 49 | "server": false, 50 | }, 51 | "snippet": " 52 | const TEST_MUTATION = \` 53 | 54 | mutation testMutation { 55 | addData(id: \\"id\\") { 56 | id 57 | } 58 | }\` 59 | 60 | const serverUrl = \\"https://api.myservice.com/\\" 61 | 62 | const res = await fetch(serverUrl, { 63 | method: \\"POST\\", 64 | headers: {}, 65 | body: JSON.stringify({ 66 | query: TEST_MUTATION, 67 | variables: {} 68 | }) 69 | }) 70 | const { errors, data } = await res.json() 71 | 72 | if (errors) { 73 | console.error(errors) 74 | } 75 | 76 | console.log(data) 77 | ", 78 | } 79 | `; 80 | 81 | exports[`Generating a JavaScript:fetch snippet should generate the correct mutation snippet 3`] = ` 82 | Object { 83 | "options": Object { 84 | "asyncAwait": false, 85 | "comments": false, 86 | "server": true, 87 | }, 88 | "snippet": " 89 | import fetch from \\"node-fetch\\" 90 | 91 | const TEST_MUTATION = \` 92 | 93 | mutation testMutation { 94 | addData(id: \\"id\\") { 95 | id 96 | } 97 | }\` 98 | 99 | const serverUrl = \\"https://api.myservice.com/\\" 100 | 101 | fetch(serverUrl, { 102 | method: \\"POST\\", 103 | headers: {}, 104 | body: JSON.stringify({ 105 | query: TEST_MUTATION, 106 | variables: {} 107 | }) 108 | }) 109 | .then(res => res.json()) 110 | .then(({ data, errors }) => { 111 | if (errors) { 112 | console.error(errors) 113 | } 114 | 115 | console.log(data) 116 | }) 117 | .catch(err => { 118 | console.error(err) 119 | }) 120 | ", 121 | } 122 | `; 123 | 124 | exports[`Generating a JavaScript:fetch snippet should generate the correct mutation snippet 4`] = ` 125 | Object { 126 | "options": Object { 127 | "asyncAwait": true, 128 | "comments": false, 129 | "server": true, 130 | }, 131 | "snippet": " 132 | import fetch from \\"node-fetch\\" 133 | 134 | const TEST_MUTATION = \` 135 | 136 | mutation testMutation { 137 | addData(id: \\"id\\") { 138 | id 139 | } 140 | }\` 141 | 142 | const serverUrl = \\"https://api.myservice.com/\\" 143 | 144 | const res = await fetch(serverUrl, { 145 | method: \\"POST\\", 146 | headers: {}, 147 | body: JSON.stringify({ 148 | query: TEST_MUTATION, 149 | variables: {} 150 | }) 151 | }) 152 | const { errors, data } = await res.json() 153 | 154 | if (errors) { 155 | console.error(errors) 156 | } 157 | 158 | console.log(data) 159 | ", 160 | } 161 | `; 162 | 163 | exports[`Generating a JavaScript:fetch snippet should generate the correct mutation snippet 5`] = ` 164 | Object { 165 | "options": Object { 166 | "asyncAwait": false, 167 | "comments": true, 168 | "server": false, 169 | }, 170 | "snippet": " 171 | const TEST_MUTATION = \` 172 | 173 | mutation testMutation { 174 | addData(id: \\"id\\") { 175 | id 176 | } 177 | }\` 178 | 179 | const serverUrl = \\"https://api.myservice.com/\\" 180 | 181 | fetch(serverUrl, { 182 | method: \\"POST\\", 183 | headers: {}, 184 | body: JSON.stringify({ 185 | query: TEST_MUTATION, 186 | variables: {} 187 | }) 188 | }) 189 | .then(res => res.json()) 190 | .then(({ data, errors }) => { 191 | if (errors) { 192 | // handle OneGraph errors 193 | console.error(errors) 194 | } 195 | 196 | // do something with data 197 | console.log(data) 198 | }) 199 | .catch(err => { 200 | // handle fetch error 201 | console.error(err) 202 | }) 203 | ", 204 | } 205 | `; 206 | 207 | exports[`Generating a JavaScript:fetch snippet should generate the correct mutation snippet 6`] = ` 208 | Object { 209 | "options": Object { 210 | "asyncAwait": true, 211 | "comments": true, 212 | "server": false, 213 | }, 214 | "snippet": " 215 | const TEST_MUTATION = \` 216 | 217 | mutation testMutation { 218 | addData(id: \\"id\\") { 219 | id 220 | } 221 | }\` 222 | 223 | const serverUrl = \\"https://api.myservice.com/\\" 224 | 225 | const res = await fetch(serverUrl, { 226 | method: \\"POST\\", 227 | headers: {}, 228 | body: JSON.stringify({ 229 | query: TEST_MUTATION, 230 | variables: {} 231 | }) 232 | }) 233 | const { errors, data } = await res.json() 234 | 235 | if (errors) { 236 | // handle OneGraph errors 237 | console.error(errors) 238 | } 239 | 240 | // do something with data 241 | console.log(data) 242 | ", 243 | } 244 | `; 245 | 246 | exports[`Generating a JavaScript:fetch snippet should generate the correct mutation snippet 7`] = ` 247 | Object { 248 | "options": Object { 249 | "asyncAwait": false, 250 | "comments": true, 251 | "server": true, 252 | }, 253 | "snippet": " 254 | // Node doesn't implement fetch so we have to import it 255 | import fetch from \\"node-fetch\\" 256 | 257 | const TEST_MUTATION = \` 258 | 259 | mutation testMutation { 260 | addData(id: \\"id\\") { 261 | id 262 | } 263 | }\` 264 | 265 | const serverUrl = \\"https://api.myservice.com/\\" 266 | 267 | fetch(serverUrl, { 268 | method: \\"POST\\", 269 | headers: {}, 270 | body: JSON.stringify({ 271 | query: TEST_MUTATION, 272 | variables: {} 273 | }) 274 | }) 275 | .then(res => res.json()) 276 | .then(({ data, errors }) => { 277 | if (errors) { 278 | // handle OneGraph errors 279 | console.error(errors) 280 | } 281 | 282 | // do something with data 283 | console.log(data) 284 | }) 285 | .catch(err => { 286 | // handle fetch error 287 | console.error(err) 288 | }) 289 | ", 290 | } 291 | `; 292 | 293 | exports[`Generating a JavaScript:fetch snippet should generate the correct mutation snippet 8`] = ` 294 | Object { 295 | "options": Object { 296 | "asyncAwait": true, 297 | "comments": true, 298 | "server": true, 299 | }, 300 | "snippet": " 301 | // Node doesn't implement fetch so we have to import it 302 | import fetch from \\"node-fetch\\" 303 | 304 | const TEST_MUTATION = \` 305 | 306 | mutation testMutation { 307 | addData(id: \\"id\\") { 308 | id 309 | } 310 | }\` 311 | 312 | const serverUrl = \\"https://api.myservice.com/\\" 313 | 314 | const res = await fetch(serverUrl, { 315 | method: \\"POST\\", 316 | headers: {}, 317 | body: JSON.stringify({ 318 | query: TEST_MUTATION, 319 | variables: {} 320 | }) 321 | }) 322 | const { errors, data } = await res.json() 323 | 324 | if (errors) { 325 | // handle OneGraph errors 326 | console.error(errors) 327 | } 328 | 329 | // do something with data 330 | console.log(data) 331 | ", 332 | } 333 | `; 334 | 335 | exports[`Generating a JavaScript:fetch snippet should generate the correct query snippet 1`] = ` 336 | Object { 337 | "options": Object { 338 | "asyncAwait": false, 339 | "comments": false, 340 | "server": false, 341 | }, 342 | "snippet": " 343 | const TEST_QUERY = \` 344 | 345 | query testQuery { 346 | someData { 347 | id 348 | } 349 | } 350 | \` 351 | 352 | const serverUrl = \\"https://api.myservice.com/\\" 353 | 354 | fetch(serverUrl, { 355 | method: \\"POST\\", 356 | headers: {}, 357 | body: JSON.stringify({ 358 | query: TEST_QUERY, 359 | variables: {} 360 | }) 361 | }) 362 | .then(res => res.json()) 363 | .then(({ data, errors }) => { 364 | if (errors) { 365 | console.error(errors) 366 | } 367 | 368 | console.log(data) 369 | }) 370 | .catch(err => { 371 | console.error(err) 372 | }) 373 | ", 374 | } 375 | `; 376 | 377 | exports[`Generating a JavaScript:fetch snippet should generate the correct query snippet 2`] = ` 378 | Object { 379 | "options": Object { 380 | "asyncAwait": true, 381 | "comments": false, 382 | "server": false, 383 | }, 384 | "snippet": " 385 | const TEST_QUERY = \` 386 | 387 | query testQuery { 388 | someData { 389 | id 390 | } 391 | } 392 | \` 393 | 394 | const serverUrl = \\"https://api.myservice.com/\\" 395 | 396 | const res = await fetch(serverUrl, { 397 | method: \\"POST\\", 398 | headers: {}, 399 | body: JSON.stringify({ 400 | query: TEST_QUERY, 401 | variables: {} 402 | }) 403 | }) 404 | const { errors, data } = await res.json() 405 | 406 | if (errors) { 407 | console.error(errors) 408 | } 409 | 410 | console.log(data) 411 | ", 412 | } 413 | `; 414 | 415 | exports[`Generating a JavaScript:fetch snippet should generate the correct query snippet 3`] = ` 416 | Object { 417 | "options": Object { 418 | "asyncAwait": false, 419 | "comments": false, 420 | "server": true, 421 | }, 422 | "snippet": " 423 | import fetch from \\"node-fetch\\" 424 | 425 | const TEST_QUERY = \` 426 | 427 | query testQuery { 428 | someData { 429 | id 430 | } 431 | } 432 | \` 433 | 434 | const serverUrl = \\"https://api.myservice.com/\\" 435 | 436 | fetch(serverUrl, { 437 | method: \\"POST\\", 438 | headers: {}, 439 | body: JSON.stringify({ 440 | query: TEST_QUERY, 441 | variables: {} 442 | }) 443 | }) 444 | .then(res => res.json()) 445 | .then(({ data, errors }) => { 446 | if (errors) { 447 | console.error(errors) 448 | } 449 | 450 | console.log(data) 451 | }) 452 | .catch(err => { 453 | console.error(err) 454 | }) 455 | ", 456 | } 457 | `; 458 | 459 | exports[`Generating a JavaScript:fetch snippet should generate the correct query snippet 4`] = ` 460 | Object { 461 | "options": Object { 462 | "asyncAwait": true, 463 | "comments": false, 464 | "server": true, 465 | }, 466 | "snippet": " 467 | import fetch from \\"node-fetch\\" 468 | 469 | const TEST_QUERY = \` 470 | 471 | query testQuery { 472 | someData { 473 | id 474 | } 475 | } 476 | \` 477 | 478 | const serverUrl = \\"https://api.myservice.com/\\" 479 | 480 | const res = await fetch(serverUrl, { 481 | method: \\"POST\\", 482 | headers: {}, 483 | body: JSON.stringify({ 484 | query: TEST_QUERY, 485 | variables: {} 486 | }) 487 | }) 488 | const { errors, data } = await res.json() 489 | 490 | if (errors) { 491 | console.error(errors) 492 | } 493 | 494 | console.log(data) 495 | ", 496 | } 497 | `; 498 | 499 | exports[`Generating a JavaScript:fetch snippet should generate the correct query snippet 5`] = ` 500 | Object { 501 | "options": Object { 502 | "asyncAwait": false, 503 | "comments": true, 504 | "server": false, 505 | }, 506 | "snippet": " 507 | const TEST_QUERY = \` 508 | 509 | query testQuery { 510 | someData { 511 | id 512 | } 513 | } 514 | \` 515 | 516 | const serverUrl = \\"https://api.myservice.com/\\" 517 | 518 | fetch(serverUrl, { 519 | method: \\"POST\\", 520 | headers: {}, 521 | body: JSON.stringify({ 522 | query: TEST_QUERY, 523 | variables: {} 524 | }) 525 | }) 526 | .then(res => res.json()) 527 | .then(({ data, errors }) => { 528 | if (errors) { 529 | // handle OneGraph errors 530 | console.error(errors) 531 | } 532 | 533 | // do something with data 534 | console.log(data) 535 | }) 536 | .catch(err => { 537 | // handle fetch error 538 | console.error(err) 539 | }) 540 | ", 541 | } 542 | `; 543 | 544 | exports[`Generating a JavaScript:fetch snippet should generate the correct query snippet 6`] = ` 545 | Object { 546 | "options": Object { 547 | "asyncAwait": true, 548 | "comments": true, 549 | "server": false, 550 | }, 551 | "snippet": " 552 | const TEST_QUERY = \` 553 | 554 | query testQuery { 555 | someData { 556 | id 557 | } 558 | } 559 | \` 560 | 561 | const serverUrl = \\"https://api.myservice.com/\\" 562 | 563 | const res = await fetch(serverUrl, { 564 | method: \\"POST\\", 565 | headers: {}, 566 | body: JSON.stringify({ 567 | query: TEST_QUERY, 568 | variables: {} 569 | }) 570 | }) 571 | const { errors, data } = await res.json() 572 | 573 | if (errors) { 574 | // handle OneGraph errors 575 | console.error(errors) 576 | } 577 | 578 | // do something with data 579 | console.log(data) 580 | ", 581 | } 582 | `; 583 | 584 | exports[`Generating a JavaScript:fetch snippet should generate the correct query snippet 7`] = ` 585 | Object { 586 | "options": Object { 587 | "asyncAwait": false, 588 | "comments": true, 589 | "server": true, 590 | }, 591 | "snippet": " 592 | // Node doesn't implement fetch so we have to import it 593 | import fetch from \\"node-fetch\\" 594 | 595 | const TEST_QUERY = \` 596 | 597 | query testQuery { 598 | someData { 599 | id 600 | } 601 | } 602 | \` 603 | 604 | const serverUrl = \\"https://api.myservice.com/\\" 605 | 606 | fetch(serverUrl, { 607 | method: \\"POST\\", 608 | headers: {}, 609 | body: JSON.stringify({ 610 | query: TEST_QUERY, 611 | variables: {} 612 | }) 613 | }) 614 | .then(res => res.json()) 615 | .then(({ data, errors }) => { 616 | if (errors) { 617 | // handle OneGraph errors 618 | console.error(errors) 619 | } 620 | 621 | // do something with data 622 | console.log(data) 623 | }) 624 | .catch(err => { 625 | // handle fetch error 626 | console.error(err) 627 | }) 628 | ", 629 | } 630 | `; 631 | 632 | exports[`Generating a JavaScript:fetch snippet should generate the correct query snippet 8`] = ` 633 | Object { 634 | "options": Object { 635 | "asyncAwait": true, 636 | "comments": true, 637 | "server": true, 638 | }, 639 | "snippet": " 640 | // Node doesn't implement fetch so we have to import it 641 | import fetch from \\"node-fetch\\" 642 | 643 | const TEST_QUERY = \` 644 | 645 | query testQuery { 646 | someData { 647 | id 648 | } 649 | } 650 | \` 651 | 652 | const serverUrl = \\"https://api.myservice.com/\\" 653 | 654 | const res = await fetch(serverUrl, { 655 | method: \\"POST\\", 656 | headers: {}, 657 | body: JSON.stringify({ 658 | query: TEST_QUERY, 659 | variables: {} 660 | }) 661 | }) 662 | const { errors, data } = await res.json() 663 | 664 | if (errors) { 665 | // handle OneGraph errors 666 | console.error(errors) 667 | } 668 | 669 | // do something with data 670 | console.log(data) 671 | ", 672 | } 673 | `; 674 | -------------------------------------------------------------------------------- /src/snippets/javascript/__tests__/__snapshots__/reactApollo-test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Generating a JavaScript:react-apollo snippet should generate the correct mutation snippet 1`] = ` 4 | Object { 5 | "options": Object { 6 | "client": false, 7 | "reactNative": false, 8 | }, 9 | "snippet": " 10 | import gql from \\"graphql-tag\\" 11 | import { Mutation } from \\"react-apollo\\" 12 | 13 | const TEST_MUTATION = gql\` 14 | 15 | mutation testMutation { 16 | addData(id: \\"id\\") { 17 | id 18 | } 19 | }\` 20 | 21 | function TestMutation() { 22 | return ( 23 | 27 | {(testMutation, { loading, error, data }) => { 28 | if (loading) return
    Loading
    29 | if (error) return
    Error
    30 | 31 | // call testMutation() to run the mutation 32 | return ( 33 | 40 | ) 41 | }} 42 |
    43 | ) 44 | } 45 | ", 46 | } 47 | `; 48 | 49 | exports[`Generating a JavaScript:react-apollo snippet should generate the correct mutation snippet 2`] = ` 50 | Object { 51 | "options": Object { 52 | "client": false, 53 | "reactNative": false, 54 | }, 55 | "snippet": " 56 | import gql from \\"graphql-tag\\" 57 | import { Mutation } from \\"react-apollo\\" 58 | 59 | const TEST_MUTATION = gql\` 60 | 61 | mutation testMutation { 62 | addData(id: \\"id\\") { 63 | id 64 | } 65 | }\` 66 | 67 | function TestMutation() { 68 | return ( 69 | 73 | {(testMutation, { loading, error, data }) => { 74 | if (loading) return
    Loading
    75 | if (error) return
    Error
    76 | 77 | // call testMutation() to run the mutation 78 | return ( 79 | 86 | ) 87 | }} 88 |
    89 | ) 90 | } 91 | ", 92 | } 93 | `; 94 | 95 | exports[`Generating a JavaScript:react-apollo snippet should generate the correct mutation snippet 3`] = ` 96 | Object { 97 | "options": Object { 98 | "client": false, 99 | "reactNative": true, 100 | }, 101 | "snippet": " 102 | import gql from \\"graphql-tag\\" 103 | import { Mutation } from \\"react-apollo\\" 104 | import { View } from \\"react-native\\" 105 | 106 | const TEST_MUTATION = gql\` 107 | 108 | mutation testMutation { 109 | addData(id: \\"id\\") { 110 | id 111 | } 112 | }\` 113 | 114 | function TestMutation() { 115 | return ( 116 | 120 | {(testMutation, { loading, error, data }) => { 121 | if (loading) return Loading 122 | if (error) return Error 123 | 124 | // call testMutation() to run the mutation 125 | return ( 126 | 133 | ) 134 | }} 135 | 136 | ) 137 | } 138 | ", 139 | } 140 | `; 141 | 142 | exports[`Generating a JavaScript:react-apollo snippet should generate the correct mutation snippet 4`] = ` 143 | Object { 144 | "options": Object { 145 | "client": false, 146 | "reactNative": true, 147 | }, 148 | "snippet": " 149 | import gql from \\"graphql-tag\\" 150 | import { Mutation } from \\"react-apollo\\" 151 | import { View } from \\"react-native\\" 152 | 153 | const TEST_MUTATION = gql\` 154 | 155 | mutation testMutation { 156 | addData(id: \\"id\\") { 157 | id 158 | } 159 | }\` 160 | 161 | function TestMutation() { 162 | return ( 163 | 167 | {(testMutation, { loading, error, data }) => { 168 | if (loading) return Loading 169 | if (error) return Error 170 | 171 | // call testMutation() to run the mutation 172 | return ( 173 | 180 | ) 181 | }} 182 | 183 | ) 184 | } 185 | ", 186 | } 187 | `; 188 | 189 | exports[`Generating a JavaScript:react-apollo snippet should generate the correct query snippet 1`] = ` 190 | Object { 191 | "options": Object { 192 | "client": false, 193 | "reactNative": false, 194 | }, 195 | "snippet": " 196 | import gql from \\"graphql-tag\\" 197 | import { Query } from \\"react-apollo\\" 198 | 199 | const TEST_QUERY = gql\` 200 | 201 | query testQuery { 202 | someData { 203 | id 204 | } 205 | } 206 | \` 207 | 208 | function TestQuery() { 209 | return ( 210 | 215 | {({ loading, error, data }) => { 216 | if (loading) return
    Loading
    217 | if (error) return
    Error
    218 | 219 | if (data) { 220 | return ( 221 |
    {JSON.stringify(data, null, 2)}
    222 | ) 223 | } 224 | }} 225 |
    226 | ) 227 | } 228 | ", 229 | } 230 | `; 231 | 232 | exports[`Generating a JavaScript:react-apollo snippet should generate the correct query snippet 2`] = ` 233 | Object { 234 | "options": Object { 235 | "client": false, 236 | "reactNative": false, 237 | }, 238 | "snippet": " 239 | import gql from \\"graphql-tag\\" 240 | import { Query } from \\"react-apollo\\" 241 | 242 | const TEST_QUERY = gql\` 243 | 244 | query testQuery { 245 | someData { 246 | id 247 | } 248 | } 249 | \` 250 | 251 | function TestQuery() { 252 | return ( 253 | 258 | {({ loading, error, data }) => { 259 | if (loading) return
    Loading
    260 | if (error) return
    Error
    261 | 262 | if (data) { 263 | return ( 264 |
    {JSON.stringify(data, null, 2)}
    265 | ) 266 | } 267 | }} 268 |
    269 | ) 270 | } 271 | ", 272 | } 273 | `; 274 | 275 | exports[`Generating a JavaScript:react-apollo snippet should generate the correct query snippet 3`] = ` 276 | Object { 277 | "options": Object { 278 | "client": false, 279 | "reactNative": true, 280 | }, 281 | "snippet": " 282 | import gql from \\"graphql-tag\\" 283 | import { Query } from \\"react-apollo\\" 284 | import { View } from \\"react-native\\" 285 | 286 | const TEST_QUERY = gql\` 287 | 288 | query testQuery { 289 | someData { 290 | id 291 | } 292 | } 293 | \` 294 | 295 | function TestQuery() { 296 | return ( 297 | 302 | {({ loading, error, data }) => { 303 | if (loading) return Loading 304 | if (error) return Error 305 | 306 | if (data) { 307 | return ( 308 | 309 | {JSON.stringify(data, null, 2)} 310 | 311 | ) 312 | } 313 | }} 314 | 315 | ) 316 | } 317 | ", 318 | } 319 | `; 320 | 321 | exports[`Generating a JavaScript:react-apollo snippet should generate the correct query snippet 4`] = ` 322 | Object { 323 | "options": Object { 324 | "client": false, 325 | "reactNative": true, 326 | }, 327 | "snippet": " 328 | import gql from \\"graphql-tag\\" 329 | import { Query } from \\"react-apollo\\" 330 | import { View } from \\"react-native\\" 331 | 332 | const TEST_QUERY = gql\` 333 | 334 | query testQuery { 335 | someData { 336 | id 337 | } 338 | } 339 | \` 340 | 341 | function TestQuery() { 342 | return ( 343 | 348 | {({ loading, error, data }) => { 349 | if (loading) return Loading 350 | if (error) return Error 351 | 352 | if (data) { 353 | return ( 354 | 355 | {JSON.stringify(data, null, 2)} 356 | 357 | ) 358 | } 359 | }} 360 | 361 | ) 362 | } 363 | ", 364 | } 365 | `; 366 | -------------------------------------------------------------------------------- /src/snippets/javascript/__tests__/__snapshots__/reactHooks-test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Generating a JavaScript:react-hooks snippet should generate the correct mutation snippet 1`] = ` 4 | Object { 5 | "options": Object { 6 | "comments": false, 7 | "reactNative": false, 8 | }, 9 | "snippet": " 10 | import React, { useState, useEffect } from \\"react\\" 11 | 12 | const TEST_MUTATION = \` 13 | 14 | mutation testMutation { 15 | addData(id: \\"id\\") { 16 | id 17 | } 18 | }\` 19 | 20 | const serverUrl = \\"https://api.myservice.com/\\" 21 | 22 | function TestMutation() { 23 | const [loading, setLoading] = useState(false) 24 | const [errors, setErrors] = useState([]) 25 | const [data, setData] = useState(null) 26 | 27 | useEffect(() => { 28 | setLoading(true) 29 | 30 | fetch(serverUrl, { 31 | method: \\"POST\\", 32 | headers: {}, 33 | body: JSON.stringify({ 34 | query: TEST_MUTATION, 35 | variables: {} 36 | }) 37 | }) 38 | .then(res => res.json()) 39 | .then(({ data, errors }) => { 40 | if (errors) { 41 | setErrors(errors) 42 | } 43 | 44 | setData(data) 45 | }) 46 | .catch(err => setErrors([err])) 47 | .finally(() => setLoading(false)) 48 | }, []) 49 | 50 | if (loading) return
    Loading
    51 | if (errors.length > 0) 52 | return
    {JSON.stringify(errors)}
    53 | 54 | return
    {JSON.stringify(data, null, 2)}
    55 | } 56 | ", 57 | } 58 | `; 59 | 60 | exports[`Generating a JavaScript:react-hooks snippet should generate the correct mutation snippet 2`] = ` 61 | Object { 62 | "options": Object { 63 | "comments": false, 64 | "reactNative": false, 65 | }, 66 | "snippet": " 67 | import React, { useState, useEffect } from \\"react\\" 68 | 69 | const TEST_MUTATION = \` 70 | 71 | mutation testMutation { 72 | addData(id: \\"id\\") { 73 | id 74 | } 75 | }\` 76 | 77 | const serverUrl = \\"https://api.myservice.com/\\" 78 | 79 | function TestMutation() { 80 | const [loading, setLoading] = useState(false) 81 | const [errors, setErrors] = useState([]) 82 | const [data, setData] = useState(null) 83 | 84 | useEffect(() => { 85 | setLoading(true) 86 | 87 | fetch(serverUrl, { 88 | method: \\"POST\\", 89 | headers: {}, 90 | body: JSON.stringify({ 91 | query: TEST_MUTATION, 92 | variables: {} 93 | }) 94 | }) 95 | .then(res => res.json()) 96 | .then(({ data, errors }) => { 97 | if (errors) { 98 | setErrors(errors) 99 | } 100 | 101 | setData(data) 102 | }) 103 | .catch(err => setErrors([err])) 104 | .finally(() => setLoading(false)) 105 | }, []) 106 | 107 | if (loading) return
    Loading
    108 | if (errors.length > 0) 109 | return
    {JSON.stringify(errors)}
    110 | 111 | return
    {JSON.stringify(data, null, 2)}
    112 | } 113 | ", 114 | } 115 | `; 116 | 117 | exports[`Generating a JavaScript:react-hooks snippet should generate the correct mutation snippet 3`] = ` 118 | Object { 119 | "options": Object { 120 | "comments": false, 121 | "reactNative": true, 122 | }, 123 | "snippet": " 124 | import React, { useState, useEffect } from \\"react\\" 125 | import { View } from \\"react-native\\" 126 | 127 | const TEST_MUTATION = \` 128 | 129 | mutation testMutation { 130 | addData(id: \\"id\\") { 131 | id 132 | } 133 | }\` 134 | 135 | const serverUrl = \\"https://api.myservice.com/\\" 136 | 137 | function TestMutation() { 138 | const [loading, setLoading] = useState(false) 139 | const [errors, setErrors] = useState([]) 140 | const [data, setData] = useState(null) 141 | 142 | useEffect(() => { 143 | setLoading(true) 144 | 145 | fetch(serverUrl, { 146 | method: \\"POST\\", 147 | headers: {}, 148 | body: JSON.stringify({ 149 | query: TEST_MUTATION, 150 | variables: {} 151 | }) 152 | }) 153 | .then(res => res.json()) 154 | .then(({ data, errors }) => { 155 | if (errors) { 156 | setErrors(errors) 157 | } 158 | 159 | setData(data) 160 | }) 161 | .catch(err => setErrors([err])) 162 | .finally(() => setLoading(false)) 163 | }, []) 164 | 165 | if (loading) return Loading 166 | if (errors.length > 0) 167 | return {JSON.stringify(errors)} 168 | 169 | return {JSON.stringify(data, null, 2)} 170 | } 171 | ", 172 | } 173 | `; 174 | 175 | exports[`Generating a JavaScript:react-hooks snippet should generate the correct mutation snippet 4`] = ` 176 | Object { 177 | "options": Object { 178 | "comments": false, 179 | "reactNative": true, 180 | }, 181 | "snippet": " 182 | import React, { useState, useEffect } from \\"react\\" 183 | import { View } from \\"react-native\\" 184 | 185 | const TEST_MUTATION = \` 186 | 187 | mutation testMutation { 188 | addData(id: \\"id\\") { 189 | id 190 | } 191 | }\` 192 | 193 | const serverUrl = \\"https://api.myservice.com/\\" 194 | 195 | function TestMutation() { 196 | const [loading, setLoading] = useState(false) 197 | const [errors, setErrors] = useState([]) 198 | const [data, setData] = useState(null) 199 | 200 | useEffect(() => { 201 | setLoading(true) 202 | 203 | fetch(serverUrl, { 204 | method: \\"POST\\", 205 | headers: {}, 206 | body: JSON.stringify({ 207 | query: TEST_MUTATION, 208 | variables: {} 209 | }) 210 | }) 211 | .then(res => res.json()) 212 | .then(({ data, errors }) => { 213 | if (errors) { 214 | setErrors(errors) 215 | } 216 | 217 | setData(data) 218 | }) 219 | .catch(err => setErrors([err])) 220 | .finally(() => setLoading(false)) 221 | }, []) 222 | 223 | if (loading) return Loading 224 | if (errors.length > 0) 225 | return {JSON.stringify(errors)} 226 | 227 | return {JSON.stringify(data, null, 2)} 228 | } 229 | ", 230 | } 231 | `; 232 | 233 | exports[`Generating a JavaScript:react-hooks snippet should generate the correct query snippet 1`] = ` 234 | Object { 235 | "options": Object { 236 | "comments": false, 237 | "reactNative": false, 238 | }, 239 | "snippet": " 240 | import React, { useState, useEffect } from \\"react\\" 241 | 242 | const TEST_QUERY = \` 243 | 244 | query testQuery { 245 | someData { 246 | id 247 | } 248 | } 249 | \` 250 | 251 | const serverUrl = \\"https://api.myservice.com/\\" 252 | 253 | function TestQuery() { 254 | const [loading, setLoading] = useState(false) 255 | const [errors, setErrors] = useState([]) 256 | const [data, setData] = useState(null) 257 | 258 | useEffect(() => { 259 | setLoading(true) 260 | 261 | fetch(serverUrl, { 262 | method: \\"POST\\", 263 | headers: {}, 264 | body: JSON.stringify({ 265 | query: TEST_QUERY, 266 | variables: {} 267 | }) 268 | }) 269 | .then(res => res.json()) 270 | .then(({ data, errors }) => { 271 | if (errors) { 272 | setErrors(errors) 273 | } 274 | 275 | setData(data) 276 | }) 277 | .catch(err => setErrors([err])) 278 | .finally(() => setLoading(false)) 279 | }, []) 280 | 281 | if (loading) return
    Loading
    282 | if (errors.length > 0) 283 | return
    {JSON.stringify(errors)}
    284 | 285 | return
    {JSON.stringify(data, null, 2)}
    286 | } 287 | ", 288 | } 289 | `; 290 | 291 | exports[`Generating a JavaScript:react-hooks snippet should generate the correct query snippet 2`] = ` 292 | Object { 293 | "options": Object { 294 | "comments": false, 295 | "reactNative": false, 296 | }, 297 | "snippet": " 298 | import React, { useState, useEffect } from \\"react\\" 299 | 300 | const TEST_QUERY = \` 301 | 302 | query testQuery { 303 | someData { 304 | id 305 | } 306 | } 307 | \` 308 | 309 | const serverUrl = \\"https://api.myservice.com/\\" 310 | 311 | function TestQuery() { 312 | const [loading, setLoading] = useState(false) 313 | const [errors, setErrors] = useState([]) 314 | const [data, setData] = useState(null) 315 | 316 | useEffect(() => { 317 | setLoading(true) 318 | 319 | fetch(serverUrl, { 320 | method: \\"POST\\", 321 | headers: {}, 322 | body: JSON.stringify({ 323 | query: TEST_QUERY, 324 | variables: {} 325 | }) 326 | }) 327 | .then(res => res.json()) 328 | .then(({ data, errors }) => { 329 | if (errors) { 330 | setErrors(errors) 331 | } 332 | 333 | setData(data) 334 | }) 335 | .catch(err => setErrors([err])) 336 | .finally(() => setLoading(false)) 337 | }, []) 338 | 339 | if (loading) return
    Loading
    340 | if (errors.length > 0) 341 | return
    {JSON.stringify(errors)}
    342 | 343 | return
    {JSON.stringify(data, null, 2)}
    344 | } 345 | ", 346 | } 347 | `; 348 | 349 | exports[`Generating a JavaScript:react-hooks snippet should generate the correct query snippet 3`] = ` 350 | Object { 351 | "options": Object { 352 | "comments": false, 353 | "reactNative": true, 354 | }, 355 | "snippet": " 356 | import React, { useState, useEffect } from \\"react\\" 357 | import { View } from \\"react-native\\" 358 | 359 | const TEST_QUERY = \` 360 | 361 | query testQuery { 362 | someData { 363 | id 364 | } 365 | } 366 | \` 367 | 368 | const serverUrl = \\"https://api.myservice.com/\\" 369 | 370 | function TestQuery() { 371 | const [loading, setLoading] = useState(false) 372 | const [errors, setErrors] = useState([]) 373 | const [data, setData] = useState(null) 374 | 375 | useEffect(() => { 376 | setLoading(true) 377 | 378 | fetch(serverUrl, { 379 | method: \\"POST\\", 380 | headers: {}, 381 | body: JSON.stringify({ 382 | query: TEST_QUERY, 383 | variables: {} 384 | }) 385 | }) 386 | .then(res => res.json()) 387 | .then(({ data, errors }) => { 388 | if (errors) { 389 | setErrors(errors) 390 | } 391 | 392 | setData(data) 393 | }) 394 | .catch(err => setErrors([err])) 395 | .finally(() => setLoading(false)) 396 | }, []) 397 | 398 | if (loading) return Loading 399 | if (errors.length > 0) 400 | return {JSON.stringify(errors)} 401 | 402 | return {JSON.stringify(data, null, 2)} 403 | } 404 | ", 405 | } 406 | `; 407 | 408 | exports[`Generating a JavaScript:react-hooks snippet should generate the correct query snippet 4`] = ` 409 | Object { 410 | "options": Object { 411 | "comments": false, 412 | "reactNative": true, 413 | }, 414 | "snippet": " 415 | import React, { useState, useEffect } from \\"react\\" 416 | import { View } from \\"react-native\\" 417 | 418 | const TEST_QUERY = \` 419 | 420 | query testQuery { 421 | someData { 422 | id 423 | } 424 | } 425 | \` 426 | 427 | const serverUrl = \\"https://api.myservice.com/\\" 428 | 429 | function TestQuery() { 430 | const [loading, setLoading] = useState(false) 431 | const [errors, setErrors] = useState([]) 432 | const [data, setData] = useState(null) 433 | 434 | useEffect(() => { 435 | setLoading(true) 436 | 437 | fetch(serverUrl, { 438 | method: \\"POST\\", 439 | headers: {}, 440 | body: JSON.stringify({ 441 | query: TEST_QUERY, 442 | variables: {} 443 | }) 444 | }) 445 | .then(res => res.json()) 446 | .then(({ data, errors }) => { 447 | if (errors) { 448 | setErrors(errors) 449 | } 450 | 451 | setData(data) 452 | }) 453 | .catch(err => setErrors([err])) 454 | .finally(() => setLoading(false)) 455 | }, []) 456 | 457 | if (loading) return Loading 458 | if (errors.length > 0) 459 | return {JSON.stringify(errors)} 460 | 461 | return {JSON.stringify(data, null, 2)} 462 | } 463 | ", 464 | } 465 | `; 466 | -------------------------------------------------------------------------------- /src/snippets/javascript/__tests__/fetch-test.js: -------------------------------------------------------------------------------- 1 | import snippetObject from '../fetch'; 2 | 3 | import getOptionCombinations from '../../__helpers__/getOptionCombinations'; 4 | 5 | const {options, generate} = snippetObject; 6 | const optionCombinations = getOptionCombinations(options); 7 | 8 | const testQuery = ` 9 | query testQuery { 10 | someData { 11 | id 12 | } 13 | } 14 | `; 15 | 16 | const testMutation = ` 17 | mutation testMutation { 18 | addData(id: "id") { 19 | id 20 | } 21 | }`; 22 | 23 | describe('Generating a JavaScript:fetch snippet', () => { 24 | it('should generate the correct query snippet', () => { 25 | optionCombinations.forEach(options => { 26 | const snippet = generate({ 27 | headers: {}, 28 | variables: {}, 29 | serverUrl: 'https://api.myservice.com/', 30 | operation: testQuery, 31 | operationType: 'query', 32 | variableName: 'TEST_QUERY', 33 | operationName: 'testQuery', 34 | options, 35 | }); 36 | 37 | expect({ 38 | options, 39 | snippet: '\n' + snippet, 40 | }).toMatchSnapshot(); 41 | }); 42 | }); 43 | 44 | it('should generate the correct mutation snippet', () => { 45 | optionCombinations.forEach(options => { 46 | const snippet = generate({ 47 | headers: {}, 48 | variables: {}, 49 | serverUrl: 'https://api.myservice.com/', 50 | operation: testMutation, 51 | operationType: 'mutation', 52 | variableName: 'TEST_MUTATION', 53 | operationName: 'testMutation', 54 | options, 55 | }); 56 | 57 | expect({ 58 | options, 59 | snippet: '\n' + snippet, 60 | }).toMatchSnapshot(); 61 | }); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /src/snippets/javascript/__tests__/reactApollo-test.js: -------------------------------------------------------------------------------- 1 | import snippetObject from '../reactApollo'; 2 | 3 | import getOptionCombinations from '../../__helpers__/getOptionCombinations'; 4 | 5 | const {options, generate} = snippetObject; 6 | const optionCombinations = getOptionCombinations(options); 7 | 8 | const testQuery = ` 9 | query testQuery { 10 | someData { 11 | id 12 | } 13 | } 14 | `; 15 | 16 | const testMutation = ` 17 | mutation testMutation { 18 | addData(id: "id") { 19 | id 20 | } 21 | }`; 22 | 23 | describe('Generating a JavaScript:react-apollo snippet', () => { 24 | it('should generate the correct query snippet', () => { 25 | optionCombinations.forEach(options => { 26 | const snippet = generate({ 27 | headers: {}, 28 | variables: {}, 29 | serverUrl: 'https://api.myservice.com/', 30 | operation: testQuery, 31 | operationType: 'query', 32 | variableName: 'TEST_QUERY', 33 | operationName: 'testQuery', 34 | options, 35 | }); 36 | 37 | expect({ 38 | options, 39 | snippet: '\n' + snippet, 40 | }).toMatchSnapshot(); 41 | }); 42 | }); 43 | 44 | it('should generate the correct mutation snippet', () => { 45 | optionCombinations.forEach(options => { 46 | const snippet = generate({ 47 | headers: {}, 48 | variables: {}, 49 | serverUrl: 'https://api.myservice.com/', 50 | operation: testMutation, 51 | operationType: 'mutation', 52 | variableName: 'TEST_MUTATION', 53 | operationName: 'testMutation', 54 | options, 55 | }); 56 | 57 | expect({ 58 | options, 59 | snippet: '\n' + snippet, 60 | }).toMatchSnapshot(); 61 | }); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /src/snippets/javascript/__tests__/reactHooks-test.js: -------------------------------------------------------------------------------- 1 | import snippetObject from '../reactHooks'; 2 | 3 | import getOptionCombinations from '../../__helpers__/getOptionCombinations'; 4 | 5 | const {options, generate} = snippetObject; 6 | const optionCombinations = getOptionCombinations(options); 7 | 8 | const testQuery = ` 9 | query testQuery { 10 | someData { 11 | id 12 | } 13 | } 14 | `; 15 | 16 | const testMutation = ` 17 | mutation testMutation { 18 | addData(id: "id") { 19 | id 20 | } 21 | }`; 22 | 23 | describe('Generating a JavaScript:react-hooks snippet', () => { 24 | it('should generate the correct query snippet', () => { 25 | optionCombinations.forEach(options => { 26 | const snippet = generate({ 27 | headers: {}, 28 | variables: {}, 29 | serverUrl: 'https://api.myservice.com/', 30 | operation: testQuery, 31 | operationType: 'query', 32 | variableName: 'TEST_QUERY', 33 | operationName: 'testQuery', 34 | options, 35 | }); 36 | 37 | expect({ 38 | options, 39 | snippet: '\n' + snippet, 40 | }).toMatchSnapshot(); 41 | }); 42 | }); 43 | 44 | it('should generate the correct mutation snippet', () => { 45 | optionCombinations.forEach(options => { 46 | const snippet = generate({ 47 | headers: {}, 48 | variables: {}, 49 | serverUrl: 'https://api.myservice.com/', 50 | operation: testMutation, 51 | operationType: 'mutation', 52 | variableName: 'TEST_MUTATION', 53 | operationName: 'testMutation', 54 | options, 55 | }); 56 | 57 | expect({ 58 | options, 59 | snippet: '\n' + snippet, 60 | }).toMatchSnapshot(); 61 | }); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /src/snippets/javascript/fetch.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import capitalizeFirstLetter from '../../utils/capitalizeFirstLetter'; 4 | import commentsFactory from '../../utils/jsCommentsFactory.js'; 5 | import { 6 | isOperationNamed, 7 | collapseExtraNewlines, 8 | addLeftWhitespace, 9 | } from '../../utils'; 10 | 11 | import 'codemirror/mode/javascript/javascript'; 12 | 13 | import type {Snippet, OperationData} from '../../index.js'; 14 | 15 | const snippetOptions = [ 16 | { 17 | id: 'server', 18 | label: 'server-side usage', 19 | initial: false, 20 | }, 21 | { 22 | id: 'asyncAwait', 23 | label: 'async/await', 24 | initial: true, 25 | }, 26 | ]; 27 | 28 | const comments = { 29 | setup: `This setup is only needed once per application`, 30 | nodeFetch: `Node doesn't implement fetch so we have to import it`, 31 | graphqlError: `handle those errors like a pro`, 32 | graphqlData: `do something great with this precious data`, 33 | fetchError: `handle errors from fetch itself`, 34 | }; 35 | 36 | function generateDocumentQuery( 37 | operationDataList: Array, 38 | ): string { 39 | const body = operationDataList 40 | .map(operationData => operationData.query) 41 | .join('\n\n') 42 | .trim(); 43 | 44 | return `const operationsDoc = \` 45 | ${addLeftWhitespace(body, 2)} 46 | \`;`; 47 | } 48 | 49 | const fetcherName = 'fetchGraphQL'; 50 | 51 | function operationFunctionName(operationData: OperationData) { 52 | const {type} = operationData; 53 | 54 | const prefix = 55 | type === 'query' 56 | ? 'fetch' 57 | : type === 'mutation' 58 | ? 'execute' 59 | : type === 'subscription' 60 | ? 'subscribeTo' 61 | : ''; 62 | 63 | const fnName = 64 | prefix + 65 | (prefix.length > 0 66 | ? capitalizeFirstLetter(operationData.name) 67 | : operationData.name); 68 | 69 | return fnName; 70 | } 71 | 72 | // Promise-based functions 73 | function promiseFetcher(serverUrl: string, headers: string): string { 74 | return `function ${fetcherName}(operationsDoc, operationName, variables) { 75 | return fetch( 76 | "${serverUrl}", 77 | { 78 | method: "POST",${ 79 | headers 80 | ? ` 81 | headers: { 82 | ${addLeftWhitespace(headers, 8)} 83 | },` 84 | : '' 85 | } 86 | body: JSON.stringify({ 87 | query: operationsDoc, 88 | variables: variables, 89 | operationName: operationName 90 | }) 91 | } 92 | ).then((result) => result.json()); 93 | }`; 94 | } 95 | 96 | function fetcherFunctions(operationDataList: Array): string { 97 | return operationDataList 98 | .map(operationData => { 99 | const fnName = operationFunctionName(operationData); 100 | const params = ( 101 | operationData.operationDefinition.variableDefinitions || [] 102 | ).map(def => def.variable.name.value); 103 | const variablesBody = params 104 | .map(param => `"${param}": ${param}`) 105 | .join(', '); 106 | const variables = `{${variablesBody}}`; 107 | return `function ${fnName}(${params.join(', ')}) { 108 | return ${fetcherName}( 109 | operationsDoc, 110 | "${operationData.name}", 111 | ${variables} 112 | ); 113 | }`; 114 | }) 115 | .join('\n\n'); 116 | } 117 | 118 | function promiseFetcherInvocation( 119 | getComment, 120 | operationDataList: Array, 121 | vars, 122 | ): string { 123 | return operationDataList 124 | .map(namedOperationData => { 125 | const params = ( 126 | namedOperationData.operationDefinition.variableDefinitions || [] 127 | ).map(def => def.variable.name.value); 128 | const variables = Object.entries(namedOperationData.variables || {}).map( 129 | ([key, value]) => `const ${key} = ${JSON.stringify(value, null, 2)};`, 130 | ); 131 | return `${variables.join('\n')} 132 | 133 | ${operationFunctionName(namedOperationData)}(${params.join(', ')}) 134 | .then(({ data, errors }) => { 135 | if (errors) { 136 | ${getComment('graphqlError')} 137 | console.error(errors); 138 | } 139 | ${getComment('graphqlData')} 140 | console.log(data); 141 | }) 142 | .catch((error) => { 143 | ${getComment('fetchError')} 144 | console.error(error); 145 | });`; 146 | }) 147 | .join('\n\n'); 148 | } 149 | 150 | // Async-await-based functions 151 | function asyncFetcher(serverUrl: string, headers: string): string { 152 | return `async function ${fetcherName}(operationsDoc, operationName, variables) { 153 | const result = await fetch( 154 | "${serverUrl}", 155 | { 156 | method: "POST",${ 157 | headers 158 | ? ` 159 | headers: { 160 | ${addLeftWhitespace(headers, 8)} 161 | },` 162 | : '' 163 | } 164 | body: JSON.stringify({ 165 | query: operationsDoc, 166 | variables: variables, 167 | operationName: operationName 168 | }) 169 | } 170 | ); 171 | 172 | return await result.json(); 173 | }`; 174 | } 175 | 176 | function asyncFetcherInvocation( 177 | getComment, 178 | operationDataList: Array, 179 | vars, 180 | ): string { 181 | return operationDataList 182 | .map(namedOperationData => { 183 | const params = ( 184 | namedOperationData.operationDefinition.variableDefinitions || [] 185 | ).map(def => def.variable.name.value); 186 | const variables = Object.entries(namedOperationData.variables || {}).map( 187 | ([key, value]) => `const ${key} = ${JSON.stringify(value, null, 2)};`, 188 | ); 189 | return `async function start${capitalizeFirstLetter( 190 | operationFunctionName(namedOperationData), 191 | )}(${params.join(', ')}) { 192 | const { errors, data } = await ${operationFunctionName( 193 | namedOperationData, 194 | )}(${params.join(', ')}); 195 | 196 | if (errors) { 197 | ${getComment('graphqlError')} 198 | console.error(errors); 199 | } 200 | 201 | ${getComment('graphqlData')} 202 | console.log(data); 203 | } 204 | 205 | ${variables.join('\n')} 206 | 207 | start${capitalizeFirstLetter( 208 | operationFunctionName(namedOperationData), 209 | )}(${params.join(', ')});`; 210 | }) 211 | .join('\n\n'); 212 | } 213 | 214 | // Snippet generation! 215 | const snippet: Snippet = { 216 | language: 'JavaScript', 217 | codeMirrorMode: 'javascript', 218 | name: 'fetch', 219 | options: snippetOptions, 220 | generate: opts => { 221 | const {serverUrl, headers, options} = opts; 222 | 223 | const operationDataList = opts.operationDataList.map( 224 | (operationData, idx) => { 225 | if (!isOperationNamed(operationData)) { 226 | return { 227 | ...operationData, 228 | name: `unnamed${capitalizeFirstLetter(operationData.type)}${idx + 229 | 1}`.trim(), 230 | query: 231 | `# Consider giving this ${ 232 | operationData.type 233 | } a unique, descriptive 234 | # name in your application as a best practice 235 | ${operationData.type} unnamed${capitalizeFirstLetter(operationData.type)}${idx + 236 | 1} ` + 237 | operationData.query 238 | .trim() 239 | .replace(/^(query|mutation|subscription) /i, ''), 240 | }; 241 | } else { 242 | return operationData; 243 | } 244 | }, 245 | ); 246 | 247 | const getComment = commentsFactory(true, comments); 248 | 249 | const serverComment = options.server ? getComment('nodeFetch') : ''; 250 | const serverImport = options.server 251 | ? `import fetch from "node-fetch";\n` 252 | : ''; 253 | 254 | const graphqlQuery = generateDocumentQuery(operationDataList); 255 | const vars = JSON.stringify({}, null, 2); 256 | const headersValues = []; 257 | for (const header of Object.keys(headers)) { 258 | if (header && headers[header]) { 259 | headersValues.push(`"${header}": "${headers[header]}"`); 260 | } 261 | } 262 | const heads = headersValues.length ? `${headersValues.join(',\n')}` : ''; 263 | 264 | const requiredDeps = [ 265 | options.server ? '"node-fetch": "^2.5.0"' : null, 266 | ].filter(Boolean); 267 | 268 | const packageDeps = 269 | requiredDeps.length > 0 270 | ? `/* 271 | Add these to your \`package.json\`: 272 | ${addLeftWhitespace(requiredDeps.join(',\n'), 2)} 273 | */ 274 | ` 275 | : ''; 276 | 277 | const fetcher = options.asyncAwait 278 | ? asyncFetcher(serverUrl, heads) 279 | : promiseFetcher(serverUrl, heads); 280 | 281 | const fetcherFunctionsDefs = fetcherFunctions(operationDataList); 282 | 283 | const fetcherInvocation = options.asyncAwait 284 | ? asyncFetcherInvocation(getComment, operationDataList, vars) 285 | : promiseFetcherInvocation(getComment, operationDataList, vars); 286 | 287 | const snippet = ` 288 | /* 289 | This is an example snippet - you should consider tailoring it 290 | to your service. 291 | */ 292 | ${packageDeps} 293 | ${serverComment} 294 | ${serverImport} 295 | 296 | ${fetcher} 297 | 298 | ${graphqlQuery} 299 | 300 | ${fetcherFunctionsDefs} 301 | 302 | ${fetcherInvocation}`; 303 | 304 | return collapseExtraNewlines(snippet.trim()); 305 | }, 306 | }; 307 | 308 | export default snippet; 309 | -------------------------------------------------------------------------------- /src/snippets/javascript/reactApollo.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import capitalizeFirstLetter from '../../utils/capitalizeFirstLetter'; 4 | import commentsFactory from '../../utils/jsCommentsFactory.js'; 5 | import { 6 | distinct, 7 | isOperationNamed, 8 | collapseExtraNewlines, 9 | addLeftWhitespace, 10 | } from '../../utils/index.js'; 11 | 12 | import 'codemirror/mode/jsx/jsx'; 13 | 14 | import type {Snippet, OperationData} from '../../index.js'; 15 | 16 | const comments = { 17 | setup: `This setup is only needed once per application`, 18 | }; 19 | 20 | function formatVariableName(operationData: OperationData): string { 21 | const {name} = operationData; 22 | return ( 23 | name.charAt(0).toUpperCase() + 24 | name 25 | .slice(1) 26 | .replace(/[A-Z]/g, '_$&') 27 | .toUpperCase() 28 | ); 29 | } 30 | 31 | function operationVariableName(operation: OperationData): string { 32 | const {type} = operation; 33 | return formatVariableName(operation) + '_' + type.toUpperCase(); 34 | } 35 | 36 | function operationVariables(operationData: OperationData) { 37 | const params = ( 38 | operationData.operationDefinition.variableDefinitions || [] 39 | ).map(def => def.variable.name.value); 40 | const variablesBody = params.map(param => `"${param}": ${param}`).join(', '); 41 | const variables = `{${variablesBody}}`; 42 | 43 | const propsBody = params 44 | .map(param => `"${param}": props.${param}`) 45 | .join(', '); 46 | const props = `{${propsBody}}`; 47 | 48 | return {params, variables, props}; 49 | } 50 | 51 | function operationComponentName(operationData: OperationData): string { 52 | const {type} = operationData; 53 | 54 | const suffix = 55 | type === 'query' 56 | ? 'Query' 57 | : type === 'mutation' 58 | ? 'Mutation' 59 | : type === 'subscription' 60 | ? 'Subscription' 61 | : ''; 62 | 63 | return suffix.length > 0 64 | ? '' + capitalizeFirstLetter(operationData.name) + suffix 65 | : capitalizeFirstLetter(operationData.name); 66 | } 67 | 68 | function mutationComponent( 69 | getComment, 70 | options, 71 | element, 72 | operationData, 73 | heads, 74 | vars, 75 | ) { 76 | const {params, variables} = operationVariables(operationData); 77 | 78 | const call = `${operationData.name}(${ 79 | params.length === 0 ? '' : `${variables}` 80 | })`; 81 | 82 | const onClick = `() => ${call}`; 83 | 84 | return ` 92 | {(${operationData.name}, { loading, error, data }) => { 93 | if (loading) return <${element}>Loading 94 | 95 | if (error) 96 | return ( 97 | <${element}> 98 | Error in ${operationVariableName(operationData)} 99 | {JSON.stringify(error, null, 2)} 100 | 101 | ); 102 | 103 | const dataEl = data ? ( 104 | <${element}>{JSON.stringify(data, null, 2)} 105 | ) : null; 106 | 107 | return ( 108 |
    109 | {dataEl} 110 | 111 | 114 |
    115 | ); 116 | }} 117 |
    `; 118 | } 119 | 120 | const queryComponent = ( 121 | getComment, 122 | options, 123 | element, 124 | operationData, 125 | heads, 126 | vars, 127 | ) => { 128 | const {params, props} = operationVariables(operationData); 129 | return ` 141 | {({ loading, error, data }) => { 142 | if (loading) return <${element}>Loading 143 | if (error) 144 | return ( 145 | <${element}> 146 | Error in ${operationVariableName(operationData)} 147 | {JSON.stringify(error, null, 2)} 148 | 149 | ); 150 | 151 | if (data) { 152 | return ( 153 | <${element}>{JSON.stringify(data, null, 2)} 154 | ) 155 | } 156 | }} 157 | `; 158 | }; 159 | 160 | const snippet: Snippet = { 161 | language: 'JavaScript', 162 | codeMirrorMode: 'jsx', 163 | name: 'react-apollo', 164 | options: [ 165 | { 166 | id: 'client', 167 | label: 'with client setup', 168 | initial: true, 169 | }, 170 | { 171 | id: 'imports', 172 | label: 'with required imports', 173 | initial: true, 174 | }, 175 | ], 176 | generate: opts => { 177 | const {headers, options, serverUrl} = opts; 178 | 179 | const getComment = commentsFactory(true, comments); 180 | 181 | const operationDataList = opts.operationDataList.map( 182 | (operationData, idx) => { 183 | if (!isOperationNamed(operationData)) { 184 | return { 185 | ...operationData, 186 | name: `unnamed${capitalizeFirstLetter(operationData.type)}${idx + 187 | 1}`.trim(), 188 | query: 189 | `# Consider giving this ${ 190 | operationData.type 191 | } a unique, descriptive 192 | # name in your application as a best practice 193 | ${operationData.type} unnamed${capitalizeFirstLetter(operationData.type)}${idx + 194 | 1} ` + 195 | operationData.query 196 | .trim() 197 | .replace(/^(query|mutation|subscription) /i, ''), 198 | }; 199 | } else { 200 | return operationData; 201 | } 202 | }, 203 | ); 204 | 205 | const element = options.reactNative ? 'View' : 'pre'; 206 | const vars = JSON.stringify({}, null, 2); 207 | const headersValues = [...Object.keys(headers || [])] 208 | .filter(k => headers[k]) 209 | .map(k => `"${k}": "${headers[k]}"`) 210 | .join(',\n'); 211 | 212 | const heads = `{${headersValues}}`; 213 | 214 | const packageDeps = `/* 215 | Add these to your \`package.json\`: 216 | "apollo-boost": "^0.3.1", 217 | "graphql": "^14.2.1", 218 | "graphql-tag": "^2.10.0", 219 | "react-apollo": "^2.5.5" 220 | */ 221 | 222 | `; 223 | 224 | const clientSetup = options.client 225 | ? `${getComment('setup')}; 226 | const apolloClient = new ApolloClient({ 227 | cache: new InMemoryCache(), 228 | link: new HttpLink({ 229 | uri: "${serverUrl}", 230 | }), 231 | });\n` 232 | : ''; 233 | 234 | const operationTypes = distinct( 235 | operationDataList.map(operationData => operationData.type), 236 | ); 237 | 238 | const imports = [ 239 | operationTypes.indexOf('query') > -1 ? 'Query' : null, 240 | operationTypes.indexOf('mutation') > -1 ? 'Mutation' : null, 241 | 'ApolloProvider', 242 | ].filter(Boolean); 243 | 244 | const reactApolloImports = `import { ${imports.join( 245 | ', ', 246 | )} } from "react-apollo";`; 247 | const reactImports = `import React from "react"; 248 | import ReactDOM from "react-dom"; 249 | import { ${ 250 | options.client ? 'ApolloClient, ' : '' 251 | }InMemoryCache, HttpLink } from "apollo-boost";`; 252 | 253 | const gqlImport = 'import gql from "graphql-tag";'; 254 | 255 | const generalImports = options.imports 256 | ? `${gqlImport} 257 | ${reactImports} 258 | ${reactApolloImports}` 259 | : ''; 260 | 261 | const components = operationDataList 262 | .map(operationData => { 263 | const componentFn = 264 | operationData.type === 'query' 265 | ? queryComponent 266 | : operationData.type === 'mutation' 267 | ? mutationComponent 268 | : () => 269 | `"We don't support ${ 270 | operationData.type 271 | } GraphQL operations yet"`; 272 | 273 | const graphqlOperation = `const ${operationVariableName( 274 | operationData, 275 | )} = gql\` 276 | ${addLeftWhitespace(operationData.query, 2)} 277 | \`;`; 278 | 279 | const component = `${graphqlOperation} 280 | 281 | const ${operationComponentName(operationData)} = (props) => { 282 | return ( 283 | ${addLeftWhitespace( 284 | componentFn( 285 | // $FlowFixMe: Add flow type to utils fn 286 | getComment, 287 | options, 288 | element, 289 | operationData, 290 | heads, 291 | vars, 292 | ), 293 | 4, 294 | )} 295 | ) 296 | };`; 297 | 298 | return component; 299 | }) 300 | .join('\n\n'); 301 | 302 | const componentInstantiations = operationDataList 303 | .map(operationData => { 304 | const {params} = operationVariables(operationData); 305 | 306 | const props = params.map(param => `${param}={${param}}`).join(' '); 307 | 308 | return `<${operationComponentName(operationData)} ${props} />`; 309 | }) 310 | .join('\n'); 311 | 312 | const variableInstantiations = operationDataList 313 | .map(operationData => { 314 | const variables = Object.entries(operationData.variables || {}).map( 315 | ([key, value]) => `const ${key} = ${JSON.stringify(value, null, 2)};`, 316 | ); 317 | 318 | return `${variables.join('\n')}`; 319 | }) 320 | .join('\n\n'); 321 | 322 | const containerComponent = `${variableInstantiations} 323 | 324 | const container = ( 325 | 326 | ${addLeftWhitespace(componentInstantiations, 4)} 327 | 328 | );`; 329 | 330 | const snippet = ` 331 | /* This is an example snippet - you should consider tailoring it 332 | to your service. 333 | */ 334 | ${packageDeps}${generalImports} 335 | 336 | ${clientSetup} 337 | 338 | ${components} 339 | 340 | ${containerComponent} 341 | 342 | ReactDOM.render(container, document.getElementById("root"));`; 343 | return collapseExtraNewlines(snippet.trim()); 344 | }, 345 | }; 346 | 347 | export default snippet; 348 | -------------------------------------------------------------------------------- /src/toposort.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type {FragmentDefinitionNode, OperationDefinitionNode} from 'graphql'; 3 | import type {OperationData} from './CodeExporter.js'; 4 | 5 | type stringBoolMap = {[string]: boolean}; 6 | 7 | const operationDataByName = ( 8 | graph: Array, 9 | name: string, 10 | ): ?OperationData => { 11 | return graph.find(operationData => operationData.name === name); 12 | }; 13 | 14 | function topologicalSortHelper( 15 | node: OperationData, 16 | visited: stringBoolMap, 17 | temp: stringBoolMap, 18 | graph: Array, 19 | result, 20 | ) { 21 | temp[node.name] = true; 22 | var neighbors = node.fragmentDependencies; 23 | for (var i = 0; i < neighbors.length; i += 1) { 24 | var fragmentDependency = neighbors[i]; 25 | var fragmentOperationData = operationDataByName( 26 | graph, 27 | fragmentDependency.name.value, 28 | ); 29 | 30 | if (!fragmentOperationData) { 31 | continue; 32 | } 33 | 34 | if (temp[fragmentOperationData.name]) { 35 | console.error('The operation graph has a cycle'); 36 | continue; 37 | } 38 | if (!visited[fragmentOperationData.name]) { 39 | topologicalSortHelper( 40 | fragmentOperationData, 41 | visited, 42 | temp, 43 | graph, 44 | result, 45 | ); 46 | } 47 | } 48 | temp[node.name] = false; 49 | visited[node.name] = true; 50 | result.push(node); 51 | } 52 | 53 | export default function toposort(graph: Array) { 54 | var result: Array = []; 55 | var visited = {}; 56 | var temp = {}; 57 | for (var node of graph) { 58 | if (!visited[node.name] && !temp[node.name]) { 59 | topologicalSortHelper(node, visited, temp, graph, result); 60 | } 61 | } 62 | return result; 63 | } 64 | -------------------------------------------------------------------------------- /src/utils/capitalizeFirstLetter.js: -------------------------------------------------------------------------------- 1 | export default function capitalizeFirstLetter(string) { 2 | return string.charAt(0).toUpperCase() + string.slice(1); 3 | } 4 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | function distinct(array: Array): Array { 2 | return [...new Set(array)]; 3 | } 4 | 5 | const unnamedSymbols = ['query', 'mutation', 'subscription']; 6 | 7 | function isOperationNamed(operationData: OperationData): boolean { 8 | return unnamedSymbols.indexOf(operationData.name.trim()) === -1; 9 | } 10 | 11 | const findFirstNamedOperation = ( 12 | operations: Array, 13 | ): ?OperationData => operations.find(isOperationNamed); 14 | 15 | function addLeftWhitespace(s: string, padding: number): string { 16 | const pad = [...new Array(padding + 1)].join(' '); 17 | return s 18 | .split('\n') 19 | .map(x => `${pad}${x}`) 20 | .join('\n'); 21 | } 22 | 23 | function collapseExtraNewlines(s: string): string { 24 | return s.replace(/\n{2,}/g, '\n\n'); 25 | } 26 | 27 | export { 28 | distinct, 29 | findFirstNamedOperation, 30 | isOperationNamed, 31 | addLeftWhitespace, 32 | collapseExtraNewlines, 33 | }; 34 | -------------------------------------------------------------------------------- /src/utils/jsCommentsFactory.js: -------------------------------------------------------------------------------- 1 | export default function commentFactory(commentsEnabled, comments) { 2 | return id => (commentsEnabled ? '// ' + comments[id] : ''); 3 | } 4 | --------------------------------------------------------------------------------