├── .gitignore ├── LICENSE ├── README.md ├── __tests__ ├── __config__ │ └── jest.setup.js ├── components │ └── Input.test.js └── unit │ ├── autoQuery.test.js │ ├── checkObject.test.js │ └── createTree.test.js ├── babel.config.js ├── client ├── App.jsx ├── components │ ├── Input.jsx │ ├── Output.jsx │ ├── TopBar.jsx │ └── Tree.jsx ├── index.js └── stylesheets │ ├── CodeMirror-default-overwrites.scss │ ├── body.scss │ ├── bottom-bar.scss │ ├── custom-colors.scss │ ├── custom-themes │ └── custom-nord.scss │ ├── endpoint-input.scss │ ├── error-message.scss │ ├── get-schema-btn.scss │ ├── input-container.scss │ ├── input-instance.scss │ ├── io-container.scss │ ├── logo.scss │ ├── main-container.scss │ ├── output-container.scss │ ├── send-btn.scss │ ├── styles.scss │ ├── top-bar.scss │ ├── tree.scss │ └── widget-btn.scss ├── helpers ├── autoQuery.js ├── createTree.js ├── customHeader.js ├── customToggle.js └── validateObject.js ├── images ├── Logo-Dark.png ├── PractiQL-local-storage.gif ├── PractiQL-logodark.png ├── PractiQL-logolite.png ├── PractiQL-mq1.gif ├── PractiQL-mq2.gif ├── PractiQL-schemaTree.gif └── logo-lite.png ├── index.html ├── package.json ├── server └── server.js └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | package-lock.json 107 | 108 | build -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 OSLabs Beta 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 | 2 | 3 | # 4 |

5 | license 6 | issues 7 | Repo stars 8 |

9 | 10 | 11 | **PractiQL** is an open-source, browser-based, GraphQL IDE. Users can send a single query to a GraphQL API or send out multiple queries simultaneously. Users also have the ability to click nodes on a treelist representing the target schema and have queries automatically generated for an intuitive experience designed to improve developer workflow. 12 | 13 | 14 | # **Features** 15 | 16 | ## Multiple Queries Simultaneously 17 | PractiQL provides users with the ability to create and send out multiple queries at once without the need for GraphQL aliases. Results are returned in separate code blocks, including results for queries to the same field, for an intuitive and expedited experience with little mental overhead. 18 | Send a bunch of queries. Get a bunch of responses. It just works. (NOTE: For internal API's, please enable CORS.) 19 |
20 |
21 | 22 |
23 | 24 | ## Write a Bunch of Queries. Send the Ones You Want 25 | Users also have the option to send out different combinations of multiple queries. It’s as easy as highlighting the queries you want to send and sending them. 26 |
27 |
28 | 29 |
30 |
31 | 32 | ## Schema Trees 33 | PractiQL can render a tree list model of a target GraphQL API for an easy-to-navigate representation of the schema. We call the models schema trees, and they’re a quick and organized way to view and interact with entire GraphQL schemas at once. 34 |
35 |
36 | ## Automatically Generated Queries 37 | Users also have the ability to generate queries by clicking on the nodes of the schema tree instead of typing those queries out manually. The result is an extremely intuitive experience and improved developer workflow that’s especially useful when testing new GraphQL APIs. 38 | No more guessing. No more relying on auto-completed fields. Generate queries as you’re browsing the schema. 39 |
40 | 41 |
42 |
43 | 44 | ## Saved Queries 45 | Navigated away from PractiQL? No problem. Queries are saved in local storage and populate automatically when you return. 46 |
47 | 48 |
49 | 50 | # **Contributors** 51 | 52 | 53 | [Anthony Cruz](linkedin.com/in/anthonycruz2) [@anthonycruz1](https://github.com/anthonycruz1) 54 | 55 | [Les C.](linkedin.com/in/leschae) [@lesc999](https://github.com/lesc999) 56 | 57 | [Rob Caporino](https://www.linkedin.com/in/rob-a-caporino/) [@rcaporino](https://github.com/rcaporino) 58 | 59 | [David Nadler](https://www.linkedin.com/in/davenads/) [@davenads](https://github.com/Davenads) 60 | 61 | 62 | -------------------------------------------------------------------------------- /__tests__/__config__/jest.setup.js: -------------------------------------------------------------------------------- 1 | const Enzyme = require('enzyme'); 2 | const Adapter = require('enzyme-adapter-react-16'); 3 | 4 | Enzyme.configure({ adapter: new Adapter() }); 5 | -------------------------------------------------------------------------------- /__tests__/components/Input.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Input from '../../client/components/Input.jsx'; 3 | import { shallow } from 'enzyme'; 4 | 5 | describe('...', () => { 6 | const inputComponent = shallow(); 7 | it('...renders div element as the container', () => { 8 | expect(inputComponent.type()).toBe('div'); 9 | }); 10 | it('...div has class "input-container-outer"', () => { 11 | expect(inputComponent.hasClass('input-container-outer')).toBeTruthy(); 12 | }); 13 | it('...renders React component ', () => { 14 | expect(inputComponent.text()).toBe(''); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /__tests__/unit/autoQuery.test.js: -------------------------------------------------------------------------------- 1 | import Input from '../../client/components/Input'; 2 | 3 | const autoQuery = require('../../helpers/autoQuery'); 4 | 5 | test('autoQuery() returns error if array is not passed.', () => { 6 | expect(autoQuery(7)).toEqual(''); 7 | }); 8 | test('autoQuery() returns string representing a query when an array is passed', () => { 9 | expect(autoQuery(['parent', 'child'])).toEqual( 10 | '{\n parent{\n child\n }\n}' 11 | ); 12 | }); 13 | -------------------------------------------------------------------------------- /__tests__/unit/checkObject.test.js: -------------------------------------------------------------------------------- 1 | const validateObject = require('../../helpers/validateObject'); 2 | 3 | test('validateObject() returns error if array is passed.', () => { 4 | expect(validateObject([])).toBeFalsy(); 5 | }); 6 | test('validateObject() returns error if number is passed.', () => { 7 | expect(validateObject(7)).toBeFalsy(); 8 | }); 9 | test('validateObject() returns error if string is passed.', () => { 10 | expect(validateObject('test')).toBeFalsy(); 11 | }); 12 | test('validateObject() returns error if map is passed.', () => { 13 | expect(validateObject(new Map())).toBeFalsy(); 14 | }); 15 | test('validateObject() returns error if set is passed.', () => { 16 | expect(validateObject(new Set())).toBeFalsy(); 17 | }); 18 | test('validateObject() returns true if empty object is passed.', () => { 19 | expect(validateObject({})).toBeTruthy(); 20 | }); 21 | test('validateObject() returns true if object is passed.', () => { 22 | expect(validateObject({ a: 1, b: 2, c: 3 })).toBeTruthy(); 23 | }); 24 | -------------------------------------------------------------------------------- /__tests__/unit/createTree.test.js: -------------------------------------------------------------------------------- 1 | const createTree = require('../../helpers/createTree'); 2 | 3 | test('createTree() returns custom error object when number is passed.', () => { 4 | expect(createTree(7)).toStrictEqual({ 5 | error: 'Sorry, something went wrong', 6 | }); 7 | }); 8 | test('createTree() returns custom error object when string is passed.', () => { 9 | expect(createTree('string')).toStrictEqual({ 10 | error: 'Sorry, something went wrong', 11 | }); 12 | }); 13 | test('createTree() returns custom error object when array is passed.', () => { 14 | expect(createTree([])).toStrictEqual({ 15 | error: 'Sorry, something went wrong', 16 | }); 17 | }); 18 | test('createTree() returns custom error object when non-schema object is passed.', () => { 19 | expect(createTree({})).toStrictEqual({ 20 | error: 'Sorry, something went wrong', 21 | }); 22 | }); 23 | test('createTree() returns custom error object when schema._queryType.name does not exist.', () => { 24 | expect( 25 | createTree({ _queryType: { testProperty: 'this is not a name' } }) 26 | ).toStrictEqual({ 27 | error: 'Sorry, something went wrong', 28 | }); 29 | }); 30 | test('createTree() returns custom error object when schema._queryType.name is not a string.', () => { 31 | expect(createTree({ _queryType: { name: 7 } })).toStrictEqual({ 32 | error: 'Sorry, something went wrong', 33 | }); 34 | }); 35 | test('createTree() returns custom error object when schema._queryType.fields does not exist.', () => { 36 | expect(createTree({ _queryType: 'test' })).toStrictEqual({ 37 | error: 'Sorry, something went wrong', 38 | }); 39 | }); 40 | 41 | test('createTree() returns tree object when schema object is passed.', () => { 42 | expect( 43 | createTree({ 44 | _queryType: { name: 'Test Schema', _fields: {} }, 45 | }) 46 | ).toStrictEqual({ 47 | name: 'Test Schema', 48 | children: [], 49 | toggled: true, 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | // needed for tests to run 2 | module.exports = { 3 | presets: [ 4 | ['@babel/preset-env', { targets: { node: 'current' } }], 5 | '@babel/preset-react', 6 | ], 7 | }; 8 | -------------------------------------------------------------------------------- /client/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { getIntrospectionQuery, buildClientSchema } from 'graphql'; 3 | import Output from './components/Output'; 4 | import TopBar from './components/TopBar'; 5 | import Input from './components/Input'; 6 | import 'codemirror/lib/codemirror.css'; 7 | import 'codemirror/theme/nord.css'; 8 | import 'codemirror/addon/hint/show-hint.css'; 9 | import 'codemirror/addon/hint/show-hint'; 10 | import 'codemirror/addon/lint/lint'; 11 | import 'codemirror/addon/edit/matchbrackets.js'; 12 | import 'codemirror/addon/edit/closebrackets.js'; 13 | import 'codemirror/addon/fold/foldgutter'; 14 | import 'codemirror/addon/fold/brace-fold'; 15 | import 'codemirror/addon/fold/foldgutter.css'; 16 | import 'codemirror-graphql/hint'; 17 | import 'codemirror-graphql/lint'; 18 | import 'codemirror-graphql/mode'; 19 | import 'codemirror/mode/javascript/javascript'; 20 | import 'codemirror/addon/scroll/simplescrollbars.css'; 21 | import 'codemirror/addon/scroll/simplescrollbars'; 22 | import 'codemirror-graphql/results/mode'; 23 | import Tree from './components/Tree.jsx'; 24 | import createTree from '../helpers/createTree.js'; 25 | export default function App(props) { 26 | const { theme, endpoint } = props; 27 | const [editor, setEditor] = useState(''); 28 | const [input, setInput] = useState( 29 | localStorage.getItem('PractiQL') || '' 30 | ); 31 | const [myTheme, setMyTheme] = useState(theme); 32 | const [querySubjects, setQuerySubjects] = useState([]); 33 | const [results, setResults] = useState(false); 34 | const [schema, setSchema] = useState(''); 35 | const [selection, setSelection] = useState(''); 36 | const [sideBarWidth, setSideBarWidth] = useState({ 37 | width: '0rem', 38 | padding: '0.5rem 0', 39 | }); 40 | const [stateEndpoint, setStateEndpoint] = useState(endpoint); 41 | const [treeObj, setTreeObj] = useState({}); 42 | 43 | // Sends introspection query to endpoint and sets results as schema 44 | useEffect(() => { 45 | fetch(stateEndpoint, { 46 | method: 'POST', 47 | headers: { 48 | Accept: 'application/json', 49 | 'Content-Type': 'application/json', 50 | }, 51 | body: JSON.stringify({ 52 | query: getIntrospectionQuery(), 53 | }), 54 | }) 55 | .then((res) => res.json()) 56 | .then((schemaJSON) => { 57 | setSchema(buildClientSchema(schemaJSON.data)); 58 | }); 59 | }, [stateEndpoint]); 60 | 61 | // Uses schema to build tree 62 | useEffect(() => { 63 | if (schema) { 64 | setTreeObj(createTree(schema)); 65 | } 66 | }, [schema]); 67 | 68 | // Sets new endpoints 69 | const handleBtnClick = (newEndpoint) => { 70 | // Sets new endpoint 71 | setStateEndpoint(newEndpoint); 72 | setQuerySubjects([]); 73 | // if sidebar is open, closes sidebar and removes tree from state 74 | if (sideBarWidth.width !== '0rem') { 75 | handleCloseSideBar(); 76 | } 77 | }; 78 | 79 | // Generates autoQueries 80 | const handleAutoQuery = (query) => { 81 | // If current input is empty, return query, else return query one line under current input 82 | const newInput = input === '' ? query : input + '\n' + query; 83 | setInput(newInput); 84 | }; 85 | 86 | // Constructs new tree diagram and opens side bar 87 | const handleSchemaRequest = () => { 88 | const widthToSet = sideBarWidth.width === '0rem' ? '20rem' : '0rem'; 89 | const paddingToSet = 90 | sideBarWidth.padding === '0.5rem 0' ? '0.5rem' : '0.5rem 0'; 91 | setSideBarWidth({ width: widthToSet, padding: paddingToSet }); 92 | setTreeObj(createTree(schema)); 93 | }; 94 | 95 | // Close sidebar 96 | const handleCloseSideBar = () => { 97 | setSideBarWidth({ width: '0rem', padding: '0.5rem 0' }); 98 | setTreeObj({}); 99 | // closes bottom bar 100 | const bottomBar = document.getElementById('bottom-bar'); 101 | bottomBar.style.height = '0'; 102 | bottomBar.style.padding = '1rem 0.75rem 0 0.75rem'; 103 | }; 104 | 105 | // Sets new editor for keyboard shortcuts 106 | const setNewEditor = (newEditor) => { 107 | setEditor(newEditor); 108 | }; 109 | 110 | // Expands bottom bar when mouse enters 111 | const handleBottomBarExpand = () => { 112 | const bottomBar = document.getElementById('bottom-bar'); 113 | bottomBar.style.removeProperty = 'height'; 114 | bottomBar.style.padding = '1rem 0.75rem 2rem 0.75rem'; 115 | }; 116 | 117 | // Collapses bottom bar when mouse leaves and if side bar is not open 118 | const handleBottomBarCollapse = () => { 119 | // if sidebar is open, bottom bar stays expanded 120 | if (sideBarWidth.width !== '0rem') return; 121 | const bottomBar = document.getElementById('bottom-bar'); 122 | bottomBar.style.height = '0'; 123 | bottomBar.style.padding = '1rem 0.75rem 0 0.75rem'; 124 | }; 125 | 126 | // Save queries in the LocalStorage 127 | const handleSnapshot = () => { 128 | //LocalStorage 129 | localStorage.setItem('PractiQL', input.trim()); 130 | }; 131 | 132 | return ( 133 |
134 |
135 |
136 | 145 |
146 |
147 |
148 | 157 |
163 |
164 | 🜉 165 |
166 | 170 | Schema 171 | 172 | 173 | Docs 174 | 175 | 179 | Save Snapshot 180 | 181 |
182 |
183 | 184 |
185 | 191 |
192 | 193 |
194 |
195 | 196 | X 197 | 198 |
199 |
Schema
200 |
201 | 202 |
203 |
204 |
205 |
206 |
207 | ); 208 | } 209 | -------------------------------------------------------------------------------- /client/components/Input.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Controlled as ControlledEditor } from 'react-codemirror2'; 3 | import { ValidationContext, SDLValidationContext } from 'graphql'; 4 | 5 | export default function Input(props) { 6 | const { 7 | autoQuery, 8 | value, 9 | onChange, 10 | selection, 11 | onSelectionChange, 12 | schema, 13 | theme, 14 | } = props; 15 | 16 | function handleChange(editor, data, value) { 17 | onChange(value); 18 | } 19 | 20 | function handleSelection(sel) { 21 | if (onSelectionChange) { 22 | onSelectionChange(sel); 23 | } 24 | } 25 | 26 | function handlePress(editor, keyEvent) { 27 | if (!editor.state.completionActive && keyEvent.keyCode != 13) { 28 | editor.showHint({ completeSingle: false }); 29 | } 30 | } 31 | 32 | return ( 33 |
34 | { 40 | handleSelection(editor.getSelection()); 41 | }} 42 | editorDidMount={(editor) => { 43 | editor.display.wrapper.className = 44 | editor.display.wrapper.className + ' input-instance'; 45 | props.setNewEditor(editor); 46 | }} 47 | options={{ 48 | foldGutter: true, 49 | gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'], 50 | matchBrackets: true, 51 | autoCloseBrackets: true, 52 | lineWrapping: true, 53 | indentUnit: 2, 54 | tabSize: 2, 55 | //currently is not linting need to look into it, might need options 56 | mode: 'graphql', 57 | // lint: { 58 | // schema: schema, 59 | // }, 60 | showHint: true, 61 | hintOptions: { 62 | schema: schema, 63 | }, 64 | lineNumbers: true, 65 | theme: theme, 66 | extraKeys: { 'Ctrl-Space': 'autocomplete' }, 67 | }} 68 | /> 69 |
70 | ); 71 | } 72 | -------------------------------------------------------------------------------- /client/components/Output.jsx: -------------------------------------------------------------------------------- 1 | import CodeMirror, { overlayMode } from 'codemirror'; 2 | import React, { useState, useEffect, useCallback } from 'react'; 3 | import { Controlled as ControlledEditor } from 'react-codemirror2'; 4 | 5 | export default function Output(props) { 6 | const [editorToGrab, setEditor] = useState(null); 7 | const [value, setValue] = useState(''); 8 | const { 9 | displayName, 10 | language, 11 | results, 12 | onChange, 13 | theme, 14 | numOfQueries, 15 | } = props; 16 | 17 | useEffect(() => { 18 | // Returns results folded 19 | // How: copies the results into a headless CodeMirror instance. This headless instance is used to count lines and identify where to fold code 20 | // If only one query was sent, won't collapse code. Returns code expanded. 21 | if (results && Object.keys(results).length === 1) return; 22 | 23 | if (editorToGrab) { 24 | let count = 1; 25 | let lastLine = 0; 26 | for (let key in results) { 27 | let instance = new CodeMirror(document.createElement('div'), { 28 | value: JSON.stringify(results[key], null, 2), 29 | }); 30 | 31 | if (count === 1) { 32 | editorToGrab.foldCode(1); 33 | } else { 34 | editorToGrab.foldCode(lastLine + 1); 35 | } 36 | count++; 37 | lastLine += instance.lineCount(); 38 | } 39 | } 40 | }, [results]); 41 | 42 | return ( 43 | <> 44 | { 49 | setEditor(editor); 50 | }} 51 | on 52 | options={{ 53 | mode: 'javascript', 54 | foldGutter: true, 55 | gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'], 56 | readOnly: true, 57 | lineNumbers: true, 58 | theme: theme, 59 | scrollbarStyle: null, 60 | }} 61 | > 62 | 63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /client/components/TopBar.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | // https://countries.trevorblades.com/ 4 | export default function TopBar(props) { 5 | const { endpoint, input, selection, setResults, setQuerySubjects } = props; 6 | 7 | const handleClick = () => { 8 | //LocalStorage 9 | localStorage.setItem('PractiQL', input.trim()); 10 | 11 | const sel = selection ? selection.trim() : input.trim(); 12 | 13 | const isMutation = sel.includes('mutation'); 14 | 15 | let myQuery; 16 | if (isMutation) { 17 | myQuery = 'mutation {\r\n'; 18 | } else { 19 | myQuery = 'query myquery {\r\n'; 20 | } 21 | const arrItems = matchRecursiveRegExp(sel, '{', '}'); 22 | 23 | let querySubjects = []; 24 | 25 | 26 | for (let i = 0; i < arrItems.length; i++) { 27 | const x = arrItems[i]; 28 | // IS THIS A MERGED QUERY? 29 | if (x.includes(',')) { 30 | const items = x.split(','); 31 | 32 | for (let i = 0; i < items.length; i++) { 33 | // DOES THIS item HAVE AN ALIAS? 34 | if (items[i].includes(':')) { 35 | querySubjects.push( 36 | items[i].substring(0, items[i].indexOf(':')).trim() 37 | ); 38 | 39 | myQuery += items[i] + (i < items.length - 1 ? ',\r\n' : '\r\n'); 40 | } else { 41 | // ADD ALIAS TO RETURN MULTIPLE RESULTSETS 42 | const alias = 43 | items[i].substring(0, items[i].indexOf('{')).trim() + 44 | '_' + 45 | i.toString(); 46 | querySubjects.push(alias); 47 | 48 | myQuery += 49 | alias + 50 | ' : ' + 51 | items[i] + 52 | (i < items.length - 1 ? ',\r\n' : '\r\n'); 53 | } 54 | } 55 | } else { 56 | // DOES THIS item HAVE AN ALIAS? 57 | let test = x.substring(0, x.indexOf(':')).trim(); 58 | if (test.indexOf('(') > 0) { 59 | if (x.trimStart().startsWith('__')) { 60 | querySubjects.push(x.substring(0, x.indexOf('{')).trim()); 61 | } else { 62 | querySubjects.push(x.substring(0, x.indexOf('(')).trim()); 63 | } 64 | myQuery += arrItems[i].trim(); 65 | } else { 66 | let alias; 67 | const query = x.substring(0, x.indexOf('{')).trim(); 68 | const repeat = querySubjects.includes(query); 69 | if (repeat) { 70 | // CREATE AN ALIAS 71 | alias = query + '_' + i.toString(); 72 | } 73 | 74 | querySubjects.push(repeat ? alias : query); 75 | myQuery += 76 | (repeat ? alias + ' : ' : '') + 77 | arrItems[i].trim() + 78 | (i < arrItems.length - 1 ? ',\r\n' : '\r\n'); 79 | } 80 | } 81 | } 82 | 83 | myQuery += '}'; 84 | 85 | fetch(endpoint, { 86 | method: 'POST', 87 | headers: { 'Content-Type': 'application/json' }, 88 | body: JSON.stringify({ 89 | query: myQuery, 90 | }), 91 | }) 92 | .then((res) => res.json()) 93 | .then((data) => { 94 | if (data.errors) { 95 | setResults(data.errors); 96 | return; 97 | } 98 | 99 | // SET STATE - results 100 | setResults(data.data); 101 | setQuerySubjects(querySubjects); 102 | }) 103 | .catch((err) => { 104 | console.log(err); 105 | }); 106 | }; 107 | 108 | function matchRecursiveRegExp(str, left, right) { 109 | const x = new RegExp(left + '|' + right, 'g'); 110 | const l = new RegExp(left); 111 | let a = []; 112 | let t, s, m; 113 | 114 | t = 0; 115 | 116 | while ((m = x.exec(str))) { 117 | if (l.test(m[0])) { 118 | if (!t++) { 119 | s = x.lastIndex; 120 | } 121 | } else if (t) { 122 | if (!--t) { 123 | a.push(str.slice(s, m.index)); 124 | } 125 | } 126 | } 127 | return a; 128 | } 129 | 130 | function handleBtnClick() { 131 | // passes value of input to props.handleBtnClick 132 | const inputValue = document.getElementById('endpoint-input').value; 133 | props.handleBtnClick(inputValue); 134 | 135 | let count = 6; 136 | let toggle = false; 137 | const color = inputValue ? '#a9d0c6' : 'red'; 138 | 139 | const intervalID = setInterval(() => { 140 | const endpointIcon = document.getElementById('endpoint-input-icon'); 141 | if (!toggle) endpointIcon.style.color = color; 142 | else endpointIcon.style.color = 'gray'; 143 | toggle = !toggle; 144 | count--; 145 | if (count === 0) clearInterval(intervalID); 146 | }, 175); 147 | } 148 | 149 | // Sets keyboard shortcut for sending queries 150 | if (props.editor !== '') { 151 | const editor = props.editor; 152 | const keyMap = { 153 | 'Ctrl-Enter': handleClick, 154 | }; 155 | editor.addKeyMap(keyMap); 156 | } 157 | 158 | // Sets enter shortcut for endpoint input 159 | function handleChange(e) { 160 | if (e.charCode === 13) handleBtnClick(); 161 | } 162 | return ( 163 |
164 | {/* PractiQL */} 165 | 166 | 167 | 171 | 172 | 175 |
176 |
181 | ↻ 182 |
183 | 190 |
191 |
192 | ); 193 | } 194 | -------------------------------------------------------------------------------- /client/components/Tree.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Treebeard, decorators } from 'react-treebeard'; 3 | import autoQuery from '../../helpers/autoQuery'; 4 | import customHeader from '../../helpers/customHeader'; 5 | import customToggle from '../../helpers/customToggle'; 6 | 7 | 8 | export default function TreeExample(props) { 9 | const { tree } = props; 10 | const [cursor, setCursor] = useState(false); 11 | const [data, setData] = useState(tree); 12 | 13 | 14 | decorators.Header = customHeader; 15 | decorators.Toggle = customToggle; 16 | 17 | const onToggle = (node, toggled) => { 18 | if (cursor) { 19 | cursor.active = false; 20 | } 21 | 22 | // Checks if clicked node in tree diagram is a scalar value. 23 | if (node.scalar) { 24 | // If true, generates query for input instance. 25 | const queryToAdd = autoQuery(node.autoQueryChain); 26 | props.handleAutoQuery(queryToAdd); 27 | } 28 | 29 | node.active = true; 30 | if (node.children) { 31 | node.toggled = toggled; 32 | } 33 | setCursor(node); 34 | setData(Object.assign({}, data)); 35 | }; 36 | 37 | if (tree.error) 38 | return ( 39 | <> 40 | {tree.error} 41 | 42 | ); 43 | else return ; 44 | } 45 | -------------------------------------------------------------------------------- /client/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import App from './App'; 4 | import styles from './stylesheets/styles.scss'; 5 | 6 | render( 7 | , 8 | document.getElementById('root') 9 | ); 10 | -------------------------------------------------------------------------------- /client/stylesheets/CodeMirror-default-overwrites.scss: -------------------------------------------------------------------------------- 1 | .CodeMirror { 2 | // max-height: 150px; 3 | transition: height 185ms, max-height 185ms; 4 | color: #141823; 5 | font-family: 'Consolas', 'Inconsolata', 'Droid Sans Mono', 'Monaco', monospace; 6 | font-size: 13px; 7 | height: 100%; 8 | left: 0; 9 | position: absolute; 10 | top: 0; 11 | width: 100%; 12 | } 13 | 14 | .CodeMirror-overlayscroll-vertical div { 15 | background-color: rgba(65, 65, 65, 0.15); 16 | } 17 | -------------------------------------------------------------------------------- /client/stylesheets/body.scss: -------------------------------------------------------------------------------- 1 | body { 2 | // font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, 3 | // Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serifs; 4 | font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', 5 | 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 6 | 'Helvetica Neue', sans-serif; 7 | margin: 0; 8 | // overflow: hidden; 9 | // background-color: $nord-primary; 10 | } 11 | -------------------------------------------------------------------------------- /client/stylesheets/bottom-bar.scss: -------------------------------------------------------------------------------- 1 | .bottom-bar { 2 | color: gray; 3 | font-size: 0.95rem; 4 | background-color: $nord-black; 5 | height: 0; 6 | padding: 1rem 0.75rem 0rem 0.75rem; 7 | transition: padding 200ms; 8 | } 9 | 10 | .bottom-bar-options { 11 | margin-left: 3rem; 12 | } 13 | 14 | .bottom-bar-options:first-of-type { 15 | margin-left: -1.5rem; 16 | } 17 | 18 | .bottom-bar-schema { 19 | cursor: pointer; 20 | } 21 | 22 | .bottom-bar-schema:hover { 23 | color: $nord-green; 24 | } 25 | 26 | .bottom-bar-snapshot { 27 | cursor: pointer; 28 | } 29 | 30 | .bottom-bar-snapshot:hover { 31 | color: $nord-green; 32 | } 33 | 34 | .bottom-bar-toggle-icon-wrapper { 35 | display: inline-flex; 36 | height: 1.4rem; 37 | width: 1.3rem; 38 | padding: 0rem 0.3rem 0rem 0.3rem; 39 | color: gray; 40 | position: relative; 41 | // top should be negative bottom-bar padding and icon height, e.g. padding = .75rem, icon height = 1rem, then icon top should be -1.75rem 42 | top: -2.15rem; 43 | background-color: $nord-black; 44 | z-index: 4; 45 | justify-content: center; 46 | border-top-left-radius: 3px; 47 | border-top-right-radius: 3px; 48 | cursor: pointer; 49 | } 50 | 51 | .bottom-bar-toggle-icon { 52 | // icon is flipped upside down, so style accordingly 53 | transform: rotate(180deg); 54 | display: flex; 55 | align-items: center; 56 | font-size: 1.5rem; 57 | padding-bottom: 0.5rem; 58 | color: $nord-green; 59 | } 60 | 61 | .bottom-bar-unavailable { 62 | color: rgb(48, 48, 48); 63 | } 64 | -------------------------------------------------------------------------------- /client/stylesheets/custom-colors.scss: -------------------------------------------------------------------------------- 1 | $secondary-color: rgba(135, 250, 231, 0.849); 2 | $nord-primary: #2e3440; 3 | $text-black: rgb(70, 70, 70); 4 | $nord-black: rgb(32, 32, 32); 5 | // $nord-green: hsl(164, 29%, 64%); 6 | $nord-green: #a9d0c6; 7 | $treebeard-blue: #21252b; 8 | -------------------------------------------------------------------------------- /client/stylesheets/custom-themes/custom-nord.scss: -------------------------------------------------------------------------------- 1 | /* To use the complete custom nord theme: 2 | 1. import 'codemirror/theme/nord.css' into input and output components 3 | 2. Set theme option to 'nord' for each component 4 | 3. Import './custom-themes/custom-nord' at the end of styles.scss. 5 | */ 6 | .top-bar--nord { 7 | background: none; 8 | background: linear-gradient($nord-primary 45%, $nord-black); 9 | border-bottom: 1px solid black; 10 | } 11 | 12 | .logo--nord { 13 | font-size: 1.5rem; 14 | color: $nord-green; 15 | } 16 | 17 | .output-container-outer--nord { 18 | border-left-color: $nord-black; 19 | background-color: $nord-primary; 20 | } 21 | 22 | .output-container-inner--nord { 23 | border-color: $nord-black; 24 | } 25 | 26 | .widget-btn--nord { 27 | color: rgba(15, 15, 15); 28 | background-color: rgba(185, 185, 185, 0.15); 29 | } 30 | -------------------------------------------------------------------------------- /client/stylesheets/endpoint-input.scss: -------------------------------------------------------------------------------- 1 | .endpoint-input-wrapper { 2 | display: flex; 3 | align-items: center; 4 | width: 100%; 5 | border: 1px solid #202020; 6 | border-radius: 3px; 7 | } 8 | 9 | .endpoint-input { 10 | color: gray; 11 | background-color: #2e3440; 12 | font-size: 1.025rem; 13 | width: 100%; 14 | outline: none; 15 | padding: 0.3rem; 16 | border: 0; 17 | border-left: 1px solid #202020; 18 | border-radius: 3px; 19 | border-top-left-radius: 0; 20 | border-bottom-left-radius: 0; 21 | height: 100%; 22 | } 23 | 24 | .endpoint-input-icon { 25 | color: gray; 26 | font-size: 1.1rem; 27 | padding: 0 0.5rem; 28 | cursor: pointer; 29 | transition: color 200ms; 30 | } 31 | 32 | .endpoint-input-icon:hover { 33 | color: $nord-green; 34 | } 35 | -------------------------------------------------------------------------------- /client/stylesheets/error-message.scss: -------------------------------------------------------------------------------- 1 | .error-message { 2 | color: rgb(206, 75, 75); 3 | } 4 | -------------------------------------------------------------------------------- /client/stylesheets/get-schema-btn.scss: -------------------------------------------------------------------------------- 1 | .get-schema-btn { 2 | background-color: purple; 3 | outline: none; 4 | } 5 | -------------------------------------------------------------------------------- /client/stylesheets/input-container.scss: -------------------------------------------------------------------------------- 1 | .input-container-wrapper { 2 | // border: 5px solid purple; 3 | display: -webkit-box; 4 | display: -ms-flexbox; 5 | display: flex; 6 | -webkit-box-orient: vertical; 7 | -webkit-box-direction: normal; 8 | -ms-flex-direction: column; 9 | flex-direction: column; 10 | -webkit-box-flex: 1; 11 | -ms-flex: 1; 12 | flex: 1 1 0%; 13 | } 14 | 15 | .input-container-outer { 16 | display: -webkit-box; 17 | display: -ms-flexbox; 18 | display: flex; 19 | -webkit-box-orient: vertical; 20 | -webkit-box-direction: normal; 21 | -ms-flex-direction: column; 22 | flex-direction: column; 23 | -webkit-box-flex: 1; 24 | -ms-flex: 1; 25 | flex: 1 1 0%; 26 | // border: 5px solid brown; 27 | } 28 | 29 | .input-container-inner { 30 | -webkit-box-flex: 1; 31 | -ms-flex: 1; 32 | flex: 1; 33 | position: relative; 34 | // background-color: $nord-primary; 35 | // border: 5px solid pink; 36 | // overflow-y: hidden; 37 | } 38 | -------------------------------------------------------------------------------- /client/stylesheets/input-instance.scss: -------------------------------------------------------------------------------- 1 | .input-instance { 2 | // box-sizing: border-box; 3 | // border: 5px solid purple; 4 | // height: 85vh; 5 | // max-height: 85vh; 6 | // align-self: stretch; 7 | // overflow-y: auto; 8 | } 9 | -------------------------------------------------------------------------------- /client/stylesheets/io-container.scss: -------------------------------------------------------------------------------- 1 | .io-container { 2 | // flex: 1; 3 | display: -webkit-box; 4 | display: -ms-flexbox; 5 | display: flex; 6 | -webkit-box-orient: horizontal; 7 | -webkit-box-direction: normal; 8 | -ms-flex-direction: row; 9 | flex-direction: row; 10 | -webkit-box-flex: 1; 11 | -ms-flex: 1; 12 | flex: 1; 13 | // border: 5px solid orange; 14 | } 15 | -------------------------------------------------------------------------------- /client/stylesheets/logo.scss: -------------------------------------------------------------------------------- 1 | .logo { 2 | color: $text-black; 3 | margin-left: 0.5rem; 4 | } 5 | -------------------------------------------------------------------------------- /client/stylesheets/main-container.scss: -------------------------------------------------------------------------------- 1 | .main-container { 2 | display: -webkit-box; 3 | display: -ms-flexbox; 4 | display: flex; 5 | -webkit-box-orient: horizontal; 6 | -webkit-box-direction: normal; 7 | -ms-flex-direction: row; 8 | flex-direction: row; 9 | height: 100%; 10 | margin: 0; 11 | overflow: hidden; 12 | width: 100%; 13 | // border: 5px solid green; 14 | } 15 | -------------------------------------------------------------------------------- /client/stylesheets/output-container.scss: -------------------------------------------------------------------------------- 1 | .output-container-outer { 2 | border-left: 0.1rem solid rgb(238, 238, 238); 3 | // flex-grow: 1; 4 | // max-width: 50%; 5 | display: -webkit-box; 6 | display: -ms-flexbox; 7 | display: flex; 8 | -webkit-box-orient: vertical; 9 | -webkit-box-direction: normal; 10 | -ms-flex-direction: column; 11 | flex-direction: column; 12 | -webkit-box-flex: 1; 13 | -ms-flex: 1; 14 | flex: 1; 15 | position: relative; 16 | } 17 | 18 | .output-container-inner { 19 | border: 1px solid rgb(235, 235, 235); 20 | // overflow-y: hidden; 21 | -webkit-box-flex: 1; 22 | -ms-flex: 1; 23 | flex: 1; 24 | height: 100%; 25 | position: relative; 26 | } 27 | 28 | .output-container-inner:first-of-type { 29 | border-top: 0; 30 | } 31 | -------------------------------------------------------------------------------- /client/stylesheets/send-btn.scss: -------------------------------------------------------------------------------- 1 | .send-btn { 2 | cursor: pointer; 3 | color: $nord-green; 4 | padding: 0.3rem; 5 | width: 5rem; 6 | margin: 0.5rem; 7 | border: 1px solid #202020; 8 | border-radius: 3px; 9 | font-size: 1.025rem; 10 | background-color: rgba(0, 0, 0, 0.03); 11 | transition: font-size 200ms, background-color 200ms; 12 | } 13 | 14 | .send-btn:hover { 15 | color: lighten($nord-green, 10%); 16 | background-color: lighten(rgba(212, 212, 212, 0.08), 45%); 17 | } 18 | 19 | .send-btn:active { 20 | color: black; 21 | background-color: darken(rgba(212, 212, 212, 0.06), 50%); 22 | font-size: 1rem; 23 | box-shadow: 0; 24 | } 25 | 26 | .send-btn:focus { 27 | outline: none; 28 | } 29 | -------------------------------------------------------------------------------- /client/stylesheets/styles.scss: -------------------------------------------------------------------------------- 1 | @import 'custom-colors'; 2 | @import 'bottom-bar'; 3 | @import 'body'; 4 | @import 'CodeMirror-default-overwrites'; 5 | @import 'endpoint-input'; 6 | @import 'top-bar'; 7 | @import 'main-container'; 8 | @import 'io-container'; 9 | @import 'input-container'; 10 | @import 'output-container'; 11 | @import 'input-instance'; 12 | @import 'send-btn'; 13 | @import 'widget-btn'; 14 | @import 'logo'; 15 | @import 'get-schema-btn'; 16 | @import 'error-message'; 17 | @import './custom-themes/custom-nord'; 18 | @import 'tree'; 19 | 20 | body, 21 | html { 22 | margin: 0; 23 | } 24 | 25 | #root { 26 | height: 100vh; 27 | } 28 | 29 | .content-wrap { 30 | display: -webkit-box; 31 | display: -ms-flexbox; 32 | display: flex; 33 | -webkit-box-orient: vertical; 34 | -webkit-box-direction: normal; 35 | -ms-flex-direction: column; 36 | flex-direction: column; 37 | -webkit-box-flex: 1; 38 | -ms-flex: 1; 39 | flex: 1; 40 | } 41 | 42 | .rd3t-label { 43 | } 44 | 45 | tspan { 46 | color: red !important; 47 | } 48 | 49 | text { 50 | max-width: 10px; 51 | } 52 | 53 | #hidden { 54 | display: none; 55 | } 56 | -------------------------------------------------------------------------------- /client/stylesheets/top-bar.scss: -------------------------------------------------------------------------------- 1 | .top-bar-wrap { 2 | display: -webkit-box; 3 | display: -ms-flexbox; 4 | display: flex; 5 | -webkit-box-orient: horizontal; 6 | -webkit-box-direction: normal; 7 | -ms-flex-direction: row; 8 | flex-direction: row; 9 | } 10 | 11 | .top-bar { 12 | // flex-grow: 1; 13 | // box-sizing: border-box; 14 | // min-width: 100%; 15 | // background: linear-gradient(rgb(204, 204, 204), rgb(238, 238, 238)); 16 | // border-bottom: 1px solid rgb(217, 217, 217); 17 | // border: 5px solid aqua; 18 | -webkit-box-align: center; 19 | -ms-flex-align: center; 20 | align-items: center; 21 | background: -webkit-linear-gradient(#f7f7f7, #e2e2e2); 22 | background: linear-gradient(#f7f7f7, #e2e2e2); 23 | border-bottom: 1px solid #d0d0d0; 24 | cursor: default; 25 | display: -webkit-box; 26 | display: -ms-flexbox; 27 | display: flex; 28 | -webkit-box-orient: horizontal; 29 | -webkit-box-direction: normal; 30 | -ms-flex-direction: row; 31 | flex-direction: row; 32 | -webkit-box-flex: 1; 33 | -ms-flex: 1; 34 | flex: 1; 35 | height: 34px; 36 | padding: 7px 14px 6px; 37 | -webkit-user-select: none; 38 | -moz-user-select: none; 39 | -ms-user-select: none; 40 | user-select: none; 41 | } 42 | -------------------------------------------------------------------------------- /client/stylesheets/tree.scss: -------------------------------------------------------------------------------- 1 | .outer-tree-wrap { 2 | height: 100%; 3 | transition: width 350ms, padding 350ms; 4 | background-color: $treebeard-blue; 5 | // background-color: $nord-black; 6 | padding: 0.5rem; 7 | width: 20rem; 8 | } 9 | 10 | .inner-tree-wrap { 11 | width: 100%; 12 | height: 100%; 13 | position: relative; 14 | } 15 | 16 | .close-tree-wrapper { 17 | background-color: #21252b; 18 | // background-color: $nord-black; 19 | text-align: end; 20 | } 21 | 22 | .sidebar-schema, 23 | .sidebar-schema-description { 24 | font-size: 2rem; 25 | color: lightgray; 26 | text-align: start; 27 | border-bottom: 1px solid gray; 28 | margin-bottom: 0.5rem; 29 | } 30 | 31 | .sidebar-schema-description { 32 | font-size: 0.9rem; 33 | font-weight: lighter; 34 | margin-bottom: 1rem; 35 | } 36 | 37 | .close-tree-btn { 38 | font-size: 0.95rem; 39 | line-height: 1; 40 | color: #9da5ab; 41 | cursor: pointer; 42 | padding-right: 0.5rem; 43 | } 44 | 45 | .css-f91fgu { 46 | height: 100%; 47 | left: 0; 48 | position: absolute; 49 | top: 0; 50 | width: 100%; 51 | overflow: hidden; 52 | overflow-y: auto; 53 | } 54 | 55 | .css-f91fgu ul, 56 | .css-f91fgu li { 57 | background-color: $treebeard-blue; 58 | } 59 | -------------------------------------------------------------------------------- /client/stylesheets/widget-btn.scss: -------------------------------------------------------------------------------- 1 | .widget-btn { 2 | position: absolute; 3 | color: rgba(65, 65, 65, 0.4); 4 | right: 1.1rem; 5 | margin-top: 0.3rem; 6 | z-index: 100; 7 | background-color: rgba(75, 75, 75, 0.065); 8 | border-radius: 0.3rem; 9 | border: 0; 10 | outline: none; 11 | cursor: pointer; 12 | transition: all 235ms; 13 | } 14 | 15 | .widget-btn:hover { 16 | color: darken(rgb(100, 100, 100), 45%); 17 | background-color: rgba(135, 250, 231, 0.849); 18 | } 19 | -------------------------------------------------------------------------------- /helpers/autoQuery.js: -------------------------------------------------------------------------------- 1 | function autoQuery(autoQueryChain) { 2 | // Uses autoQueryChain array to generate a query for input instance. Returns a string. 3 | if (!Array.isArray(autoQueryChain)) { 4 | console.log(new Error('Must pass an array to autoQuery')); 5 | return ''; 6 | } 7 | let spaceCount = 2; 8 | const query = autoQueryChain.reduce((acc, el, index) => { 9 | let spaces = ' '.repeat(spaceCount); 10 | acc += `{ 11 | ${spaces}${el}`; 12 | 13 | if (index === autoQueryChain.length - 1) { 14 | for (let i = 0; i <= index; i++) { 15 | spaces = ' '.repeat(spaceCount - 2); 16 | acc += ` 17 | ${spaces}}`; 18 | spaceCount -= 2; 19 | } 20 | } 21 | 22 | spaceCount += 2; 23 | return acc; 24 | }, ''); 25 | return query; 26 | } 27 | 28 | module.exports = autoQuery; 29 | -------------------------------------------------------------------------------- /helpers/createTree.js: -------------------------------------------------------------------------------- 1 | function createTree(schema) { 2 | // createTree() takes a GraphQL schema object and creates a tree for a Treebeard component 3 | 4 | // creates custom error object and error message 5 | const errorObject = { error: 'Sorry, something went wrong' }; 6 | const createTreeError = new Error('Schema not valid object'); 7 | 8 | // Validates schema argument 9 | if ( 10 | !(schema !== null && typeof schema === 'object') || 11 | !schema._queryType || 12 | !schema._queryType.name || 13 | !(typeof schema._queryType.name !== 'String') || 14 | !schema._queryType._fields 15 | ) { 16 | console.log(createTreeError); 17 | return errorObject; 18 | } 19 | 20 | if(!schema) return {name: 'no schema found'}; 21 | 22 | const myTree = [ 23 | { 24 | name: schema._queryType.name, 25 | children: [], 26 | type: schema._queryType 27 | } 28 | ]; 29 | 30 | 31 | if(schema._mutationType) { 32 | myTree.push({name: schema._mutationType.name, children: [], type: schema._mutationType}); 33 | } 34 | 35 | myTree.forEach(child => { 36 | createTopLevel(child); 37 | }) 38 | 39 | 40 | function createTopLevel(topChild) { 41 | mainFields = Object.values(topChild.type._fields); 42 | mainFields.forEach((field) => { 43 | const typeDef = {}; 44 | const attributes = {}; 45 | const children = []; 46 | 47 | for (let i = 0; i < field.args.length; i++) { 48 | attributes[field.args[i].name] = findType(field.args[i].type).name; 49 | } 50 | 51 | const innerChildren = Object.values(findSubFields(field.type)); 52 | 53 | for (let i = 0; i < innerChildren.length; i++) { 54 | children.push(getChildren(innerChildren[i], field.name)); 55 | } 56 | 57 | typeDef.name = field.name; 58 | typeDef.autoQueryChain = [field.name]; 59 | typeDef.attributes = attributes; 60 | typeDef.children = children; 61 | 62 | topChild.children.push(typeDef); 63 | }); 64 | } 65 | 66 | function findType(type) { 67 | if (type.name) return { name: type.name, description: type.description }; 68 | return findType(type.ofType); 69 | } 70 | 71 | function findSubFields(type) { 72 | if (type.name) return type._fields; 73 | return findSubFields(type.ofType); 74 | } 75 | 76 | function getChildren(child, parentName) { 77 | const typeDef = {}; 78 | const attributes = {}; 79 | const children = []; 80 | 81 | for (let i = 0; i < child.args.length; i++) { 82 | attributes[child.args[i].name] = findType(child.args[i].type).name; 83 | } 84 | 85 | const innerChild = findType(child.type); 86 | children.push({ 87 | name: innerChild.name, 88 | autoQueryChain: [parentName, child.name], 89 | scalar: true, 90 | attributes: { description: innerChild.description }, 91 | }); 92 | 93 | typeDef.name = child.name; 94 | typeDef.autoQueryChain = [parentName, child.name]; 95 | typeDef.attributes = attributes; 96 | typeDef.children = children; 97 | 98 | return typeDef; 99 | } 100 | return myTree; 101 | } 102 | 103 | module.exports = createTree; 104 | -------------------------------------------------------------------------------- /helpers/customHeader.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | const customHeader = (props) => { 4 | const [nodeStyle, setNodeStyle] = useState({ base: props.style.base }); 5 | const [hover, setHover] = useState(false); 6 | const [position, setPosition] = useState('absolute') 7 | 8 | let argString = '' 9 | const args = props.node.attributes ? Object.entries(props.node.attributes) : ''; 10 | if(typeof args === 'object') { 11 | args.forEach(arg => { 12 | if(arg[0] === 'description') argString = arg[1]; 13 | else argString += `(${arg[0]} : ${arg[1]})`; 14 | }) 15 | } 16 | 17 | const onMouseOver = node => { 18 | if (node) { 19 | setNodeStyle(() => ({ 20 | base: { ...props.style.base, ...{ color: "#a8cfc5" } } 21 | })); 22 | // setHover(true); 23 | Object.keys(node.attributes).length === 0 ? setHover(false) : setHover(true); 24 | if(node.attributes.description) setPosition('relative'); 25 | } 26 | }; 27 | 28 | const onMouseLeave = node => { 29 | if (node) { 30 | setNodeStyle(() => ({ base: props.style.base })); 31 | setHover(false); 32 | } 33 | }; 34 | 35 | return ( 36 |
37 |
onMouseOver(props.node)} onMouseLeave={() => onMouseLeave(props.node)}> 38 | {`${props.node.name} `} 39 |
40 |
41 | ) 42 | } 43 | 44 | export default customHeader; -------------------------------------------------------------------------------- /helpers/customToggle.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const customToggle = (props) => { 4 | return ( 5 |
6 | 7 |
8 | ) 9 | } 10 | 11 | export default customToggle; -------------------------------------------------------------------------------- /helpers/validateObject.js: -------------------------------------------------------------------------------- 1 | function validateObject(value) { 2 | // validates value is object. To be used with createTree.js and any other function needing an object 3 | return ( 4 | !Array.isArray(value) && 5 | !(value instanceof Map) && 6 | !(value instanceof Set) && 7 | value !== null && 8 | typeof value === 'object' 9 | ); 10 | } 11 | 12 | module.exports = validateObject; 13 | -------------------------------------------------------------------------------- /images/Logo-Dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/PractiQL/28821749619376cabc79be2d530df6039ce6300b/images/Logo-Dark.png -------------------------------------------------------------------------------- /images/PractiQL-local-storage.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/PractiQL/28821749619376cabc79be2d530df6039ce6300b/images/PractiQL-local-storage.gif -------------------------------------------------------------------------------- /images/PractiQL-logodark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/PractiQL/28821749619376cabc79be2d530df6039ce6300b/images/PractiQL-logodark.png -------------------------------------------------------------------------------- /images/PractiQL-logolite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/PractiQL/28821749619376cabc79be2d530df6039ce6300b/images/PractiQL-logolite.png -------------------------------------------------------------------------------- /images/PractiQL-mq1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/PractiQL/28821749619376cabc79be2d530df6039ce6300b/images/PractiQL-mq1.gif -------------------------------------------------------------------------------- /images/PractiQL-mq2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/PractiQL/28821749619376cabc79be2d530df6039ce6300b/images/PractiQL-mq2.gif -------------------------------------------------------------------------------- /images/PractiQL-schemaTree.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/PractiQL/28821749619376cabc79be2d530df6039ce6300b/images/PractiQL-schemaTree.gif -------------------------------------------------------------------------------- /images/logo-lite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/PractiQL/28821749619376cabc79be2d530df6039ce6300b/images/logo-lite.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | PractiQL 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "practiql", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "webpack.config.js", 6 | "scripts": { 7 | "build": "NODE_ENV=production webpack", 8 | "dev": "NODE_ENV=development webpack serve --open & nodemon server/server.js", 9 | "start": "node server/server.js", 10 | "test": "jest", 11 | "test-vs": "jest --verbose --silent", 12 | "windows": "concurrently \"cross-env NODE_ENV=development nodemon server/server.js\" \"NODE_ENV=development webpack serve --open\"" 13 | }, 14 | "jest": { 15 | "setupFiles": [ 16 | "/__tests__/__config__/jest.setup.js" 17 | ], 18 | "testPathIgnorePatterns": [ 19 | "node_modules", 20 | "/__tests__/__config__/" 21 | ] 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/oslabs-beta/PractiQL.git" 26 | }, 27 | "keywords": [], 28 | "author": "", 29 | "license": "ISC", 30 | "bugs": { 31 | "url": "https://github.com/oslabs-beta/PractiQL/issues" 32 | }, 33 | "homepage": "https://github.com/oslabs-beta/PractiQL#readme", 34 | "dependencies": { 35 | "codemirror": "^5.60.0", 36 | "codemirror-graphql": "^1.0.0", 37 | "express": "^4.17.1", 38 | "graphql": "^15.5.0", 39 | "react": "^17.0.1", 40 | "react-codemirror2": "^7.2.1", 41 | "react-d3-tree": "^2.0.1", 42 | "react-dom": "^17.0.1", 43 | "react-treebeard": "^3.2.4" 44 | }, 45 | "devDependencies": { 46 | "@babel/core": "^7.13.8", 47 | "@babel/preset-env": "^7.13.9", 48 | "@babel/preset-react": "^7.12.13", 49 | "babel-jest": "^26.6.3", 50 | "babel-loader": "^8.2.2", 51 | "css-loader": "^5.2.0", 52 | "enzyme": "^3.11.0", 53 | "enzyme-adapter-react-16": "^1.15.6", 54 | "jest": "^26.6.3", 55 | "nodemon": "^2.0.7", 56 | "sass": "^1.32.8", 57 | "sass-loader": "^11.0.1", 58 | "style-loader": "^2.0.0", 59 | "webpack": "^5.24.3", 60 | "webpack-cli": "^4.5.0", 61 | "webpack-dev-server": "^3.11.2" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const path = require('path'); 3 | const app = express(); 4 | 5 | app.use(express.json()); 6 | 7 | app.use('/build', express.static(path.resolve(__dirname, '../build'))); 8 | 9 | app.get('/', (req, res) => { 10 | return res.status(200).sendFile(path.resolve(__dirname, '../index.html')); 11 | }); 12 | 13 | app.listen(3000, () => { 14 | console.log('Server listening on port 3000'); 15 | }); 16 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | mode: process.env.NODE_ENV, 5 | entry: './client/index.js', 6 | output: { 7 | path: path.resolve(__dirname, 'build'), 8 | filename: 'bundle.js', 9 | }, 10 | module: { 11 | rules: [ 12 | { 13 | test: /.(jsx|js)$/, 14 | exclude: /(node_modules)/, 15 | use: { 16 | loader: 'babel-loader', 17 | options: { 18 | presets: ['@babel/preset-env', '@babel/preset-react'], 19 | }, 20 | }, 21 | }, 22 | { 23 | test: /\.(sa|sc|c)ss$/, 24 | use: ['style-loader', 'css-loader', 'sass-loader'], 25 | }, 26 | ], 27 | }, 28 | devtool: 'eval-source-map', 29 | devServer: { 30 | publicPath: '/build/', 31 | proxy: { 32 | '/': 'http://localhost:3000', 33 | }, 34 | }, 35 | resolve: { 36 | extensions: ['.js', '.jsx'], 37 | }, 38 | }; 39 | --------------------------------------------------------------------------------