6 |
7 | ## HYPNOS
8 |
9 | Welcome to __Hypnos(beta)__, a streamlined desktop application to sandbox GraphQL calls to RESTful APIs using the Apollo-link-rest package from Apollo Client, as well as giving detailed, helpful error messages.
10 |
11 |
12 |
13 |
14 |
15 | *\* We recently added tabs to handle multiple entries!*
16 |
17 | ## Getting Started
18 |
19 | __Requirements__
20 |
21 | All you need to do is download your respective OS package, install, and go!
22 |
23 |
24 | __How to Install__
25 |
26 | Download for Mac or Windows (Linux coming soon!)
27 |
28 | __Mac__: [hypnos-0.0.2.dmg](https://github.com/oslabs-beta/hypnos/releases/download/0.0.2/Hypnos-0.0.2-mac.dmg)
29 |
30 | __Windows Zipped Config__: [hypnos-0.0.2.zip](https://github.com/oslabs-beta/hypnos/releases/download/0.0.2/Hypnos-0.0.2-win.zip)
31 |
32 | Install the app to your applications folder.
33 |
34 | ## How to Use
35 |
36 | __Enter your API endpoint__
37 |
38 | Hypnos supports calls to both open APIs and APIs requiring a key. Future versions will include support for APIs requiring Basic Auth and Oauth2
39 |
40 |
41 |
42 |
43 |
44 |
45 | __Enter your Apollo-link-rest query__
46 |
47 | A sample query is provided. Further documentation on Apollo-link-rest calls can be found in the Apollo GraphQL documents here:
48 |
49 |
50 |
51 |
52 |
53 |
54 | __Hypnos will return the GraphQL response or meaningful errors__
55 |
56 | If there are any errors in query formatting, they will be displayed on the right
57 |
58 |
59 |
60 |
61 |
62 |
63 | ## Features
64 |
65 | __Hypnos History__
66 |
67 | Hypnos will store your local query history, persisting queries from your sessions. You can delete your entire history, or you can delete individual queries. You also have the option of repopulating the current tab with a previous query.
68 |
69 |
70 |
71 |
72 |
73 | __Tabs__
74 |
75 | Want to submit another query, but don't want to submit your current query just yet? Just open up a new tab! Hypnos will save the endpoint and query, and you can come back to it later.
76 |
77 |
78 |
79 |
80 |
81 | __Authenticated APIs__
82 |
83 | A majority of APIs require a key in order to gain access to their endpoints, but have no fear: Hypnos supports API keys. Type in your endpoint and query as you would normally, but before you submit the query, go to __Submit API Key__. Clicking on it will open a new window in which you can enter the key for your headers and the API key given to you by the API itself. Once you submit the keys, you can go ahead and submit your query.
84 |
85 |
86 |
87 | ## Resources
88 |
89 | Built with Electron and React
90 |
91 |
92 | __Authors__: [Dillon Garrett](https://github.com/dillon-garrett), [Sophie Nye](https://github.com/SophieNye), [Will Robinson](https://github.com/wrobinson91)
93 |
--------------------------------------------------------------------------------
/__tests__/contextReducer.js:
--------------------------------------------------------------------------------
1 | import { reducer, initialState } from '../client/Context';
2 | import * as types from '../client/Constants/actionTypes';
3 | import * as errorResponse from '../client/Constants/errors/errorResponseStrings';
4 |
5 | describe('React Context reducer tests', () => {
6 | // console.log('reducer is: ', reducer);
7 | let state;
8 | let newState;
9 |
10 | beforeEach(() => {
11 | state = { ...initialState };
12 | });
13 |
14 | describe('default state', () => {
15 | it("reducer should return same state when there's no type property in action parameter", () => {
16 | // strict equality in this instance
17 | expect(reducer(state, {})).toBe(state);
18 | });
19 | it('reducer should return same state when given unknown action type', () => {
20 | // strict equality in this instance
21 | expect(reducer(state, { type: 'test', payload: 'test' })).toBe(state);
22 | });
23 | });
24 |
25 | describe('action type tests', () => {
26 | // mock/lite version of payload from running a succesful query
27 | const mockPayload = {
28 | newAPIKey: '',
29 | newEndpoint: 'https://api.tvmaze.com/',
30 | newHeadersKey: '',
31 | query: {
32 | definitions: [{}],
33 | kind: 'Document',
34 | loc: {
35 | end: 237,
36 | source: {},
37 | start: 0
38 | }
39 | },
40 | queryResultObject: 'show',
41 | ranQueryTab: '1',
42 | type: types.RUN_QUERY
43 | };
44 |
45 | describe('RUN_QUERY case', () => {
46 | beforeAll(() => {
47 | // receive new state after reducer has been run
48 | newState = reducer(state, mockPayload);
49 | });
50 |
51 | it('queryGQLError, endpointFromDB, historyTextValue should always be empty strings', () => {
52 | expect(newState.queryGQLError).toBe('');
53 | expect(newState.endpointFromDB).toBe('');
54 | expect(newState.historyTextValue).toBe('');
55 | });
56 |
57 | it("expect queryResultObject to be 'show'", () => {
58 | expect(newState.queryResultObject).toBe('show');
59 | });
60 |
61 | it('expect headersKey to be empty string', () => {
62 | expect(newState.headersKey).toBe('');
63 | });
64 |
65 | it('expect apiKey to be empty string', () => {
66 | expect(newState.apiKey).toBe('');
67 | });
68 |
69 | it('expect query property to contain shallow copy of action query & tab from which query was run', () => {
70 | // newState = reducer(state, mockPayload);
71 | expect(newState.query.query).toEqual(mockPayload.query);
72 | expect(newState.query.ranQueryTab).toBe(mockPayload.ranQueryTab);
73 | });
74 |
75 | it('expect endpointHistory at index 1 to be changed to what is in payload', () => {
76 | expect(newState.endpointHistory[mockPayload.ranQueryTab]).toBe(mockPayload.newEndpoint);
77 | });
78 | });
79 |
80 | describe('RESET_STATE case', () => {
81 | beforeAll(() => {
82 | // run a query on tab '1'
83 | newState = reducer(state, mockPayload);
84 | // run a new query from a new tab
85 | newState = reducer(newState, { ...mockPayload, ranQueryTab: '2' });
86 | // reset on the secondary tab
87 | newState = reducer(newState, { type: types.RESET_STATE, currentTab: '2' });
88 | });
89 |
90 | it('state outside of endpointHistory for everything but current tab should match initial state version', () => {
91 | expect(newState.query).toEqual(initialState.query);
92 | expect(newState.queryResultObject).toBe(initialState.queryResultObject);
93 | expect(newState.queryGQLError).toBe(initialState.queryGQLError);
94 | expect(newState.endpoint).toBe(initialState.endpoint);
95 | expect(newState.historyTextValue).toBe(initialState.historyTextValue);
96 | expect(newState.historyIdx).toBe(initialState.historyIdx);
97 | expect(newState.headersKey).toBe(initialState.headersKey);
98 | expect(newState.apiKey).toBe(initialState.apiKey);
99 | expect(newState.endpointFromDB).toBe(initialState.endpointFromDB);
100 | // this is the only data point that should NOT match the initial state
101 | expect(newState.endpointHistory[mockPayload.ranQueryTab]).toBe(mockPayload.newEndpoint);
102 | expect(newState.endpointHistory['2']).toBe(initialState.endpoint);
103 | });
104 | });
105 |
106 | describe('GQL_ERROR case', () => {
107 | // when a query is not constrcucted correctly
108 | beforeAll(() => {
109 | // run one successful query
110 | newState = reducer(state, mockPayload);
111 | // query construction errored out
112 | newState = reducer(newState, {
113 | type: types.GQL_ERROR,
114 | // picked one specific error -- dispatch format is the same for any of these cases
115 | gqlError: errorResponse.queryMethodError
116 | });
117 | });
118 | it('query object should have errored version', () => {
119 | expect(newState.query).toEqual({
120 | query: '',
121 | ranQueryTab: -1
122 | });
123 | });
124 | it('expect queryResultObject and historyTextValue to be empty strings', () => {
125 | expect(newState.queryResultObject).toBe('');
126 | expect(newState.historyTextValue).toBe('');
127 | });
128 | it('expect queryGQLError to match error message from payload', () => {
129 | expect(newState.queryGQLError).toBe(errorResponse.queryMethodError);
130 | });
131 | });
132 | });
133 | });
134 |
--------------------------------------------------------------------------------
/__tests__/enzyme.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { configure, shallow, mount } from 'enzyme';
3 | import Adapter from 'enzyme-adapter-react-16';
4 | import toJson from 'enzyme-to-json';
5 | import renderer from 'react-test-renderer';
6 | import Dexie from 'dexie';
7 | import Modal from 'react-modal';
8 | import indexedDB from 'fake-indexeddb';
9 |
10 | import HistoryDisplay from '../client/Components/HistoryDisplay';
11 | import App from '../client/App';
12 |
13 | configure({ adapter: new Adapter() });
14 |
15 | const db = new Dexie('testHistoryDB', { indexedDB });
16 | db.version(1).stores({
17 | history: '++id, query, endpoint'
18 | });
19 |
20 | describe('First React component test with Enzyme', () => {
21 | it('renders without crashing', () => {
22 | shallow();
23 | });
24 | });
25 |
26 | describe('React unit tests', () => {
27 | beforeAll(() => {
28 | db.history.put({
29 | query: 'query1',
30 | endpoint: 'endpoint1'
31 | });
32 | db.history.put({
33 | query: 'query2',
34 | endpoint: 'endpoint2'
35 | });
36 | db.history.put({
37 | query: 'query3',
38 | endpoint: 'endpoint3'
39 | });
40 | });
41 |
42 | describe('HistoryDisplay renders', () => {
43 | let wrapper;
44 |
45 | // beforeAll(() => {
46 | // wrapper = shallow();
47 | // });
48 |
49 | it('database returns an array of queries', () => {
50 | console.log('in db test');
51 | let array = [1, 2, 3];
52 | db.history.toArray().then(queries => {
53 | array = queries;
54 | });
55 | expect(array).toHaveLength(3);
56 | });
57 | });
58 |
59 | afterAll(() => {
60 | db.history.clear();
61 | });
62 | });
63 |
--------------------------------------------------------------------------------
/__tests__/fetchErrorCheck.js:
--------------------------------------------------------------------------------
1 | import fetchErrorCheck from '../client/utils/queryInput/fetchErrorCheck';
2 | import * as errorMessages from '../client/Constants/errors/errorStrings';
3 |
4 | const mockDispatch = jest.fn();
5 | const mockErrorObject = {
6 | locations: [{}],
7 | message: '',
8 | extensions: undefined,
9 | nodes: undefined,
10 | positions: [32],
11 | source: ``,
12 | stack: ''
13 | };
14 |
15 | describe('fetch error check for syntactically incorrect queries', () => {
16 | beforeEach(() => {
17 | mockDispatch.mockClear();
18 | mockErrorObject.message = '';
19 | });
20 |
21 | it('invoking fetchErrorCheck should invoke dispatch once', () => {
22 | mockErrorObject.message = errorMessages.queryMethodError;
23 | fetchErrorCheck(mockErrorObject, mockDispatch);
24 | expect(mockDispatch.mock.calls.length).toBe(1);
25 | });
26 |
27 | it('generic error not covered should throw error, and dispatch should be fired once', () => {
28 | mockErrorObject.message = errorMessages.queryMethodError;
29 | try {
30 | fetchErrorCheck(mockErrorObject, mockDispatch);
31 | } catch (e) {
32 | expect(e).toBe(typeof Error);
33 | } finally {
34 | expect(mockDispatch.mock.calls.length).toBe(1);
35 | }
36 | });
37 | });
38 |
--------------------------------------------------------------------------------
/__tests__/puppeteer.js:
--------------------------------------------------------------------------------
1 | const puppeteer = require('puppeteer');
2 |
3 | const APP = `http://localhost:${process.env.PORT || 3000}/`;
4 |
5 | describe('front end renders', () => {
6 | let browser;
7 | let page;
8 |
9 | beforeAll(async () => {
10 | browser = await puppeteer.launch({
11 | args: ['--no-sandbox', '--disable-setuid-sandbox']
12 | });
13 | page = await browser.newPage();
14 | });
15 |
16 | afterAll(() => {
17 | browser.close();
18 | });
19 |
20 | describe('Initial display', () => {
21 | it('queries container loads successfully', async () => {
22 | // We navigate to the page at the beginning of each case so we have a
23 | // fresh start
24 | await page.goto(APP);
25 | await page.waitForSelector('#queries-container');
26 | const queriesContainer = await page.$eval('#queries-container', el => !!el);
27 | expect(queriesContainer).toBe(true);
28 | });
29 |
30 | it('query input loads successfully', async () => {
31 | await page.goto(APP);
32 | await page.waitForSelector('#query-input');
33 | const queriesInput = await page.$eval('#query-input', el => !!el);
34 | expect(queriesInput).toBe(true);
35 | });
36 |
37 | it('query output loads successfully', async () => {
38 | await page.goto(APP);
39 | await page.waitForSelector('#query-output');
40 | const queriesOutput = await page.$eval('#query-output', el => !!el);
41 | expect(queriesOutput).toBe(true);
42 | });
43 | });
44 | });
45 |
--------------------------------------------------------------------------------
/assets/Logo-full-size.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/hypnos/0c131c66582f3f444b02bdbe2df234560b589127/assets/Logo-full-size.png
--------------------------------------------------------------------------------
/assets/Logo-full.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/hypnos/0c131c66582f3f444b02bdbe2df234560b589127/assets/Logo-full.png
--------------------------------------------------------------------------------
/assets/enter-endpoint.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/hypnos/0c131c66582f3f444b02bdbe2df234560b589127/assets/enter-endpoint.gif
--------------------------------------------------------------------------------
/assets/enter-query.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/hypnos/0c131c66582f3f444b02bdbe2df234560b589127/assets/enter-query.gif
--------------------------------------------------------------------------------
/assets/errors.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/hypnos/0c131c66582f3f444b02bdbe2df234560b589127/assets/errors.gif
--------------------------------------------------------------------------------
/assets/history.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/hypnos/0c131c66582f3f444b02bdbe2df234560b589127/assets/history.gif
--------------------------------------------------------------------------------
/assets/hypnos-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/hypnos/0c131c66582f3f444b02bdbe2df234560b589127/assets/hypnos-icon.png
--------------------------------------------------------------------------------
/assets/hypnos-logo-large.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/hypnos/0c131c66582f3f444b02bdbe2df234560b589127/assets/hypnos-logo-large.png
--------------------------------------------------------------------------------
/assets/tabs.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/hypnos/0c131c66582f3f444b02bdbe2df234560b589127/assets/tabs.gif
--------------------------------------------------------------------------------
/client/App.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | // not sure we need the below
3 | // import './StyleSheets/App.scss';
4 |
5 | // import {
6 | // Tab, Tabs, TabList, TabPanel,
7 | // } from 'react-tabs';
8 | // not sure we need the below
9 | // import 'react-tabs/style/react-tabs.css';
10 |
11 | import { RestLink } from 'apollo-link-rest';
12 | import { ApolloClient } from 'apollo-client';
13 | import { InMemoryCache } from 'apollo-cache-inmemory';
14 | import { ApolloProvider } from 'react-apollo';
15 | import { onError } from 'apollo-link-error';
16 | import { ApolloLink } from 'apollo-link';
17 | // import { createHttpLink } from 'apollo-link-http';
18 |
19 | import Header from './Components/Header';
20 | // history display now rendered inside Tabs Manager
21 | // import HistoryDisplay from './Components/HistoryDisplay';
22 | import TabsManager from './Containers/TabsManager';
23 | // import DeleteButton from './Components/MiniComponents/TabsDeleteButton';
24 | // import QueriesContainer from './Containers/QueriesContainer';
25 | import { StateProvider, useStateValue } from './Context';
26 |
27 | import * as errorResponse from './Constants/errors/errorResponseStrings';
28 |
29 | // import * as errorDispatchObj from './Constants/errors/errorDispatchObjects';
30 | // using a proxy to get around CORS. We do not need a server.
31 |
32 | const proxy = Number(process.env.IS_DEV) === 1 ? 'https://cors-anywhere.herokuapp.com/' : '';
33 |
34 | const App = () => {
35 | const [{ endpoint, apiKey, headersKey }] = useStateValue();
36 |
37 | // instantiated errorLink
38 | // const httpLink = createHttpLink({ uri: proxy + endpoint });
39 |
40 | const headersOptions = {
41 | 'Content-Type': 'application/json'
42 | // 'Access-Control-Allow-Origin': '*',
43 | };
44 |
45 | if (apiKey !== '' && headersKey !== '') {
46 | // console.log('apiKey: ', apiKey);
47 | // console.log('headersKey: ', headersKey);
48 | headersOptions[headersKey] = apiKey;
49 | // console.log('headersOptions ', headersOptions);
50 | }
51 |
52 | const restLink = new RestLink({
53 | // might be able to use custom fetch here for error checking?
54 | uri: proxy + endpoint,
55 | fetchOptions: {
56 | mode: 'no-cors'
57 | },
58 | headers: headersOptions,
59 | // onError: ({ networkError, graphQLErrors }) => {
60 | // console.log('graphQLErrors', graphQLErrors);
61 | // console.log('networkError', networkError);
62 | // },
63 | customFetch: (uri, fetchOptions) =>
64 | // console.log('in custom fetch. fetchOptions: ', fetchOptions);
65 | new Promise((resolve, reject) => {
66 | fetch(uri, fetchOptions)
67 | .then(res => {
68 | // const clone = res.clone();
69 | // console.log('in first then lock, custom fetch: ', res);
70 | if (res.status === 404) {
71 | // dispatch inside of here seems to break it
72 | // dispatch(errorDispatchObj.endpointPath404Error);
73 | reject(new Error(errorResponse.endpointPath404Error));
74 | }
75 | // console.log('clone.json: ', clone.json());
76 | else return resolve(res);
77 | })
78 | // .then((data) => {
79 | // console.log('data in 2nd then block: ', data);
80 | // return resolve(data);
81 | // })
82 | .catch(e => {
83 | // console.log('error in custom fetch');
84 | reject('error in custom fetch: ', e);
85 | });
86 | })
87 | // credentials: 'include',
88 | });
89 |
90 | // error link, which isn't actually being triggered at all
91 | const errorLink = onError(({ graphQLErrors, networkError, operation, response, forward }) => {
92 | // operation and response are other props in the onError obj
93 | // console.log('operation in errorLink: ', operation);
94 | // console.log('response in errorLink: ', response);
95 | if (graphQLErrors) {
96 | graphQLErrors.map(({ message, locations, path }) =>
97 | console.log(`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`)
98 | );
99 | }
100 | if (networkError) console.log('Network Error: ', networkError);
101 |
102 | // forward(operation);
103 | });
104 |
105 | // const httpLink = createHttpLink(proxy + endpoint);
106 |
107 | const client = new ApolloClient({
108 | // added errorLink here
109 | link: ApolloLink.from([errorLink, restLink]),
110 | cache: new InMemoryCache()
111 | // fetchPolicy: 'cache-first',
112 |
113 | // handling errors on default
114 | // defaultOptions: {
115 | // watchQuery: {
116 | // fetchPolicy: 'cache-and-network',
117 | // errorPolicy: 'all',
118 | // },
119 | // query: {
120 | // fetchPolicy: 'cache-and-network',
121 | // errorPolicy: 'all',
122 | // },
123 | // },
124 | // onError: ({ networkError, graphQLErrors }) => {
125 | // console.log('graphQLErrors', graphQLErrors);
126 | // console.log('networkError', networkError);
127 | // },
128 | });
129 |
130 | // history display moved to render inside of TabsManager
131 | // QC instances render inside tabs manager
132 | return (
133 |
134 |
135 |
136 |
137 |
138 |
139 | );
140 | };
141 |
142 | const statefulApp = () => (
143 |
144 |
145 |
146 | );
147 |
148 | export default statefulApp;
149 |
--------------------------------------------------------------------------------
/client/Components/APIKeyModal.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import Modal from 'react-modal';
3 | import { apiKeyModalStyle as styleObj } from '../Constants/inlineComponentStyle';
4 |
5 | const APIModal = props => {
6 | const { modalOptions, setModalOptions } = props;
7 |
8 | const [apiTextValue, setApiTextValue] = useState('');
9 | const [headerValue, setHeaderValue] = useState('');
10 |
11 | const openModal = () => {
12 | setModalOptions({
13 | ...modalOptions,
14 | isModalOpen: true
15 | });
16 | };
17 |
18 | const closeModal = () => {
19 | // console.log('close modal happened');
20 | setModalOptions({
21 | ...modalOptions,
22 | newHeadersKey: headerValue.trim(),
23 | newAPIKey: apiTextValue.trim(),
24 | isModalOpen: false
25 | });
26 | };
27 |
28 | return (
29 |
30 |
35 | closeModal()}
39 | style={styleObj}
40 | contentLabel="API Key"
41 | >
42 |
If your endpoint requires an API key, please enter it here.
43 |
44 |
71 |
72 |
73 | );
74 | };
75 |
76 | // this is supposed to be down here, but it looks weird. commented out for now
77 | // Modal.setAppElement('#root');
78 |
79 | export default APIModal;
80 |
--------------------------------------------------------------------------------
/client/Components/EndpointField.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useStateValue } from '../Context';
3 | import APIModal from './APIKeyModal';
4 |
5 | import defaultEndpoint from '../Constants/defaultEndpoint';
6 |
7 | const EndpointField = props => {
8 | // streamlined to not use local state from queryInput component
9 | const { setNewAPIEndpoint, stateTabReference, modalOptions, setModalOptions } = props;
10 | // 8/12: deleted endpoint from useStateValue below
11 | const [{ endpointHistory }] = useStateValue();
12 |
13 | return (
14 |
15 | {
19 | // have to assign value from text area instead of local state, since state setter
20 | // and dispatch are async
21 |
22 | const newUrl = e.target.value;
23 | setNewAPIEndpoint(newUrl);
24 | }}
25 | />
26 |
27 |
28 | );
29 | };
30 |
31 | export default EndpointField;
32 |
--------------------------------------------------------------------------------
/client/Components/Header.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const header = () => (
4 |
5 |
20 |
21 | );
22 | };
23 |
24 | export default URLResultCheck;
25 |
--------------------------------------------------------------------------------
/client/Components/QueryInput.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Controlled as CodeMirror } from 'react-codemirror2';
3 | import gql from 'graphql-tag';
4 | import { useStateValue } from '../Context';
5 | import EndpointField from './EndpointField';
6 | // import db from '../db';
7 | import * as types from '../Constants/actionTypes';
8 |
9 | // // import Code Mirror styling all at once
10 | // NOTE: THIS IS NOW BEING IMPORTED IN INDEX.HTML FILE
11 | // import '../StyleSheets/external/CodeMirror.css';
12 |
13 | import fetchErrorCheck from '../utils/queryInput/fetchErrorCheck';
14 | import addQueryToDB from '../utils/queryInput/addQueryToDB';
15 | // import handleQueryFetch from '../utils/queryInput/handleQueryFetch';
16 |
17 | import defaultEndpoint from '../Constants/defaultEndpoint';
18 |
19 | // from addons folder of codemirror
20 | require('codemirror/addon/display/autorefresh');
21 |
22 | // SHOULD MAKE NOTE: API key should be supplied in endpoint field
23 | // using a proxy to get around CORS. We do not need a server.
24 |
25 | // wrote example query so it can be used as a placeholder in textarea
26 | const exampleQuery = `# Example query:
27 | query ditto {
28 | pokemon @rest(type: "Pokemon", path: "ditto/") {
29 | name
30 | abilities
31 | }
32 | }`;
33 |
34 | const QueryInput = props => {
35 | const { stateTabReference } = props;
36 |
37 | // deleted endpoint from useStateValue below
38 | const [
39 | { historyTextValue, isModalOpen, endpointHistory, historyIdx },
40 | dispatch
41 | ] = useStateValue();
42 | const [textValue, setTextValue] = useState(exampleQuery);
43 |
44 | const [modalOptions, setModalOptions] = useState({
45 | isModalOpen: false,
46 | newHeadersKey: '',
47 | newAPIKey: ''
48 | });
49 | // if edit button has been clicked, then historyTextValue exists in state. reassigned to fill out
50 | // code mirror text area
51 |
52 | const [newAPIEndpoint, setNewAPIEndpoint] = useState('');
53 |
54 | if (
55 | historyTextValue !== '' &&
56 | textValue !== historyTextValue &&
57 | historyIdx === stateTabReference
58 | ) {
59 | // if a user has asked for an old query, repopulate
60 |
61 | setTextValue(historyTextValue);
62 | setNewAPIEndpoint(endpointHistory[stateTabReference]);
63 |
64 | dispatch({
65 | type: types.RESET_GET_QUERY
66 | });
67 | // once history is assigned down here, reset it in context
68 | // dispatch({
69 | // type: types.RESET_GET_QUERY,
70 | // });
71 | }
72 |
73 | const handleSubmit = () => {
74 | event.preventDefault();
75 | // old way
76 | // const urlToSend = newAPIEndpoint || endpoint;
77 | // new way
78 | // console.log('new api endpoint: ', newAPIEndpoint);
79 | // console.log('history endpoint: ', endpointHistory[stateTabReference]);
80 | // console.log('new api endpoint: ', defaultEndpoint);
81 | const urlToSend = newAPIEndpoint || endpointHistory[stateTabReference] || defaultEndpoint;
82 |
83 | // console.log('url being sent: ', urlToSend);
84 | try {
85 | gql([`${textValue}`]);
86 | } catch (err) {
87 | // console.log('could not make tag: ', err);
88 | // NEED CATCH FOR NO PATH STRING AT ALL
89 | // 'Syntax Error: Unexpected )'
90 |
91 | // NEED 404 CHECK -- PULL FROM HANDLE QUERY FETCH?
92 | fetchErrorCheck(err, dispatch);
93 | return;
94 | }
95 |
96 | // console.log('regex test: ', textValue.match(/(?<=\{\W)(.*?)(?=\@)/g));
97 | const regexResult = textValue.match(/(?<=\{\W)(.*?)(?=\@)/g);
98 | Promise.all([
99 | addQueryToDB(textValue, urlToSend),
100 | dispatch({
101 | type: types.RUN_QUERY,
102 | // decontructed using of gql tag to make query object. need to pass in a stringliteral.
103 | query: gql([`${textValue}`]),
104 | // pulls of key for where data will be in result obj
105 | queryResultObject: regexResult
106 | ? textValue.match(/(?<=\{\W)(.*?)(?=\@)/g)[0].trim()
107 | : 'null',
108 | newEndpoint: urlToSend,
109 | ranQueryTab: stateTabReference,
110 | newHeadersKey: modalOptions.newHeadersKey,
111 | newAPIKey: modalOptions.newAPIKey
112 | }),
113 | setModalOptions({
114 | ...modalOptions,
115 | newHeadersKey: '',
116 | newAPIKey: ''
117 | })
118 | ])
119 |
120 | // .then(() => console.log('DB entry added and dispatch successful.'))
121 | .catch(e => console.log('Error in DB add/dispatch chain: ', e));
122 | };
123 |
124 | return (
125 | <>
126 |
132 |
133 |
190 |
191 | >
192 | );
193 | };
194 |
195 | export default QueryInput;
196 |
--------------------------------------------------------------------------------
/client/Components/QueryOutputDisplay.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { useStateValue } from '../Context';
3 | import { jsonFormatter } from '../utils/queryOutputDisplay/jsonFormatter';
4 | import nullChecker from '../utils/queryOutputDisplay/nullChecker';
5 | // import nullResultChecker from '../utils/queryOutputDisplay/nullResultChecker';
6 |
7 | import ErrorDisplay from './MiniComponents/ErrorDisplay';
8 | import URLResultCheck from './MiniComponents/URLResultCheck';
9 | import NullResultCheck from './MiniComponents/NullResultCheck';
10 |
11 | const QueryOutputDisplay = props => {
12 | // ! TODO: MOVE ERROR CHECKING INTO A DIFFERENT FILE BECAUSE THIS IS A LOT
13 | const [{ queryResultObject, queryGQLError }] = useStateValue();
14 | const [isHovering, toggleHover] = useState(false);
15 | // pull props off from GQL query running
16 | const { loading, error } = props;
17 |
18 | // if the current tab matches the tab from which the query was run, show contents. if not, make invisible
19 | // const styleObj = { visibility: ranQueryTab === stateTabReference ? 'visible' : 'hidden' };
20 |
21 | // result is assigned either the successful query data or an error string
22 | const result = props[queryResultObject] ? props[queryResultObject] : queryGQLError;
23 |
24 | // checking if __typeName on the result object exists. If it doesn't, we send an error message
25 | if (loading === false) {
26 | // console.log(result);
27 | // if result comes back as an array - checks 0th index, will not work for nested result arrays
28 | if (Array.isArray(result)) {
29 | if (!result[0].__typename) {
30 | return (
31 |
32 | );
33 | }
34 | // if result comes back as a flat object
35 | } else if (!Object.keys(result).includes('__typename')) {
36 | return (
37 |
38 | );
39 | }
40 | }
41 |
42 | // checking to see if there are any null values on the results object
43 | // if so, means that the query field was improperly named or doesn't exist
44 |
45 | // ! NOTE: NEEDS TO ACCOUNT FOR ARRAYS, AS WELL AS NULL RESULT CHECKER
46 | // ! BUT DOES IT?
47 | const testNull = nullChecker(result);
48 |
49 | // checking if there are any values from our result that look like a url (surface level only)
50 | let urlAsPropCheck = false;
51 | // result will either be an object or string (error message)
52 | if (typeof result === 'object') {
53 | // ensures curVal is not null and that it is a string. a URL-like response
54 | // will only be a string
55 | // NOT NESTED
56 | urlAsPropCheck = Object.values(result).reduce((acc, curVal) => {
57 | if (curVal !== null && typeof curVal === 'string') return curVal.includes('http') || acc;
58 | return acc;
59 | }, false);
60 | }
61 |
62 | // if there are any values from our result that look like a url, make an array of LIs
63 | // now rendered inside return
64 |
65 | // loading and error cases do not have query-output IDs
66 | // loading and error come from GraphQL query result
67 |
68 | // ! TEST: for local state of result. if it's an empty string, query hasn't been run
69 | if (loading) {
70 | return (
71 |
72 |
73 |
74 | );
75 | }
76 |
77 | // need to figure out how to deal with this one -- 7/30 at 11:00 am
78 | // if (error.message === 'Network error: forward is not a function')
79 |
80 | // any error from a graphql query that's not already accounted for is rendered here
81 | if (error) {
82 | if (error.message === 'Network error: forward is not a function') {
83 | return (
84 |
89 | );
90 | }
91 | return ;
92 | }
93 |
94 | return (
95 | <>
96 | <>
97 | {testNull && (
98 |
99 | )}
100 | >
101 | <>{urlAsPropCheck && }>
102 |
103 |
104 | {jsonFormatter(result)}
105 |
106 |
107 |
108 |
109 | >
110 | );
111 | };
112 |
113 | export default QueryOutputDisplay;
114 |
--------------------------------------------------------------------------------
/client/Constants/actionTypes.js:
--------------------------------------------------------------------------------
1 | export const RUN_QUERY = 'RUN_QUERY';
2 | // export const SUBMIT_ENDPOINT = 'SUBMIT_ENDPOINT';
3 | export const RESET_STATE = 'RESET_STATE';
4 | export const GQL_ERROR = 'GQL_ERROR';
5 | export const EDIT_QUERY_FROM_DB = 'EDIT_QUERY_FROM_DB';
6 | export const RESET_GET_QUERY = 'RESET_GET_QUERY';
7 |
--------------------------------------------------------------------------------
/client/Constants/defaultEndpoint.js:
--------------------------------------------------------------------------------
1 | const defaultEndpoint = 'https://pokeapi.co/api/v2/pokemon/';
2 | export default defaultEndpoint;
3 |
--------------------------------------------------------------------------------
/client/Constants/errors/errorDispatchObjects.js:
--------------------------------------------------------------------------------
1 | import { GQL_ERROR } from '../actionTypes';
2 | import * as errorResponse from './errorResponseStrings';
3 |
4 | class ErrorDispatch {
5 | constructor(errorMessage) {
6 | this.type = GQL_ERROR;
7 | this.gqlError = errorMessage;
8 | }
9 | }
10 |
11 | export const queryMethodError = new ErrorDispatch(errorResponse.queryMethodError);
12 | export const multipleQueriesError = new ErrorDispatch(errorResponse.multipleQueriesError);
13 | export const varBeforeRestError = new ErrorDispatch(errorResponse.varBeforeRestError);
14 | export const curlyBracketError = new ErrorDispatch(errorResponse.curlyBracketError);
15 | export const queryFieldBlankError = new ErrorDispatch(errorResponse.queryFieldBlankError);
16 | export const typeSyntaxError = new ErrorDispatch(errorResponse.typeSyntaxError);
17 | export const noRestCallError = new ErrorDispatch(errorResponse.noRestCallError);
18 | export const noPathOrTypeError = new ErrorDispatch(errorResponse.noPathOrTypeError);
19 | export const endpointPath404Error = new ErrorDispatch(errorResponse.endpointPath404Error);
20 | export const singleQuotesError = new ErrorDispatch(errorResponse.singleQuotesError);
21 | export const badFieldError = new ErrorDispatch(errorResponse.badFieldError);
22 | export const unterminatedStringError = new ErrorDispatch(errorResponse.unterminatedStringError);
23 | export const genericError = new ErrorDispatch(errorResponse.genericError);
24 |
25 | // BELOW: Old way to make error dispatches, before constructor
26 |
27 | // export const queryMethodError = {
28 | // type: GQL_ERROR,
29 | // gqlError: errorResponse.queryMethodError,
30 | // };
31 |
32 | // export const multipleQueriesError = {
33 | // type: GQL_ERROR,
34 | // gqlError: errorResponse.multipleQueriesError,
35 | // };
36 |
37 | // export const varBeforeRestError = {
38 | // type: GQL_ERROR,
39 | // gqlError: errorResponse.varBeforeRestError,
40 | // };
41 |
42 | // export const curlyBracketError = {
43 | // type: GQL_ERROR,
44 | // gqlError: errorResponse.curlyBracketError,
45 | // };
46 |
47 | // export const queryFieldBlankError = {
48 | // type: GQL_ERROR,
49 | // gqlError: errorResponse.queryFieldBlankError,
50 | // };
51 |
52 | // export const typeSyntaxError = {
53 | // type: GQL_ERROR,
54 | // gqlError: errorResponse.typeSyntaxError,
55 | // };
56 |
57 | // export const noRestCallError = {
58 | // type: GQL_ERROR,
59 | // gqlError: errorResponse.noRestCallError,
60 | // };
61 |
62 | // export const noPathOrTypeError = {
63 | // type: GQL_ERROR,
64 | // gqlError: errorResponse.noPathOrTypeError,
65 | // };
66 |
67 | // export const endpointPath404Error = {
68 | // type: GQL_ERROR,
69 | // gqlError: errorResponse.endpointPath404Error,
70 | // };
71 |
72 | // export const singleQuotesError = {
73 | // type: GQL_ERROR,
74 | // gqlError: errorResponse.singleQuotesError,
75 | // };
76 |
77 | // export const badFieldError = {
78 | // type: GQL_ERROR,
79 | // gqlError: errorResponse.badFieldError,
80 | // };
81 |
82 | // export const unterminatedStringError = {
83 | // type: GQL_ERROR,
84 | // gqlError: errorResponse.unterminatedStringError,
85 | // };
86 |
--------------------------------------------------------------------------------
/client/Constants/errors/errorResponseStrings.js:
--------------------------------------------------------------------------------
1 | export const queryMethodError =
2 | 'Query method is invalid. Please double check your query on line 1.';
3 | export const multipleQueriesError =
4 | 'Currently attempting to run multiple queries, but only one query, subscription, or mutation may be run at one time.';
5 | export const varBeforeRestError =
6 | 'Variable before "@rest" cannot be blank. Please click reset and check line 3 of the example for reference.';
7 | export const curlyBracketError = 'Query must be wrapped in curly brackets.';
8 | export const queryFieldBlankError =
9 | 'Query fields cannot be blank. Please click "Reset" and check line 4 of the example for reference.';
10 | export const typeSyntaxError = 'Inside @rest, "type" must be followed by a colon (e.g. type:).';
11 | export const noRestCallError = 'Query must have an @rest call.';
12 | export const noPathOrTypeError =
13 | "@rest must have a 'path' and 'type' property. Please click reset to check the example for reference.";
14 | export const endpointPath404Error = 'Endpoint is invalid. Please double check your endpoint.';
15 | export const singleQuotesError = 'Please use double quotes (" ") instead of single quotes (\' \').';
16 | export const badFieldError =
17 | 'One or more of your query fields might be written incorrectly. Please double check them.';
18 | export const unterminatedStringError =
19 | 'An open string has not been closed with double quotes(" "). Please double check your query.';
20 | export const genericError =
21 | 'There was an uncaught error in your GraphQL syntax. Please double check your query.';
22 |
--------------------------------------------------------------------------------
/client/Constants/errors/errorStrings.js:
--------------------------------------------------------------------------------
1 | export const queryMethodError = 'Syntax Error: Unexpected Name';
2 | export const multipleQueriesError = 'react-apollo';
3 | export const varBeforeRestError = 'Syntax Error: Expected Name, found @';
4 | export const curlyBracketError1 = 'Syntax Error: Expected Name, found ';
5 | export const curlyBracketError2 = 'Syntax Error: Expected {';
6 | export const curlyBracketError3 = 'Syntax Error: Unexpected }';
7 | export const queryFieldBlankError = 'Syntax Error: Expected Name, found }';
8 | export const typeSyntaxError = 'Syntax Error: Expected :';
9 | export const noRestCallError = "Cannot read property '0' of null";
10 | export const badArgumentOrFieldError = 'Syntax Error: Expected Name';
11 | export const singleQuotesError =
12 | 'Syntax Error: Unexpected single quote character (\'), did you mean to use a double quote (")?';
13 | export const unterminatedStringError = 'Syntax Error: Unterminated string.';
14 |
--------------------------------------------------------------------------------
/client/Constants/inlineComponentStyle.js:
--------------------------------------------------------------------------------
1 | export const tabsDeleteButtonStyle = {
2 | borderStyle: 'none',
3 | paddingLeft: '5px',
4 | fontSize: '10px',
5 | backgroundColor: '#f7f9fb',
6 | // isHidden must be handled at component level
7 | visibility: 'visible'
8 | };
9 |
10 | export const tabsDeleteButtonMainStyle = {
11 | ...tabsDeleteButtonStyle,
12 | visibility: 'hidden'
13 | };
14 |
15 | export const apiKeyModalStyle = {
16 | content: {
17 | top: '50%',
18 | left: '50%',
19 | right: 'auto',
20 | bottom: 'auto',
21 | marginRight: '-50%',
22 | transform: 'translate(-50%, -50%)',
23 | backgroundColor: '#31708b',
24 | // width: '100px',
25 | borderRadius: '5px'
26 | }
27 | };
28 |
29 | export const tabStyle = {
30 | fontFamily: 'Helvetica, sans-serif',
31 | fontSize: '12px',
32 | backgroundColor: '#f7f9fb'
33 | };
34 |
35 | export const tabStyleMain = {
36 | ...tabStyle
37 | // height: '13px',
38 | // marginBottom used to be needed. now it's not
39 | // marginBottom: '1px',
40 | };
41 |
42 | export const addButtonStyle = {
43 | fontSize: '25px',
44 | borderStyle: 'none',
45 | paddingLeft: '5px',
46 | paddingBottom: '-6px',
47 | backgroundColor: '#f7f9fb',
48 | outline: 'none',
49 | alignSelf: 'center'
50 | };
51 |
--------------------------------------------------------------------------------
/client/Containers/QueriesContainer.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { graphql } from 'react-apollo';
3 | import { useStateValue } from '../Context';
4 | // NOTE: moved endpoint field to inside query
5 | import QueryOutputDisplay from '../Components/QueryOutputDisplay';
6 | import QueryInput from '../Components/QueryInput';
7 | import { multipleQueriesError as multipleQueriesDispatchObj } from '../Constants/errors/errorDispatchObjects';
8 |
9 | // MOVED modal to inside endpoint field
10 | // import APIModal from '../Components/APIKeyModal';
11 | // Modal.setAppElement('#root')
12 |
13 | const QueriesContainer = props => {
14 | const { stateTabReference } = props;
15 |
16 | const [
17 | {
18 | query: { query, ranQueryTab },
19 | queryResultObject,
20 | queryGQLError
21 | },
22 | dispatch
23 | ] = useStateValue();
24 |
25 | // error thrown because it evals before anything is in query
26 | let OutputOfQuery;
27 | if (query !== '') {
28 | // console.log('inside of QC, after if block');
29 | // if something is in query, assign QoQ to output of query
30 | // had to pass on props with the props object. it "parses" bigass object
31 | // before it's passed on. one thing needed for dynamism: the name of the prop
32 | // on the data object. e.g. query ditto { !!!POKEMON }
33 |
34 | // if query.definitions is an array with the number of queries. It should not be greater than 1
35 | if (query.definitions.length > 1) {
36 | // GraphQL can only run one query at a time, so even though this if statement block is to check for error, we need to send only one query to GQL so that the app doesn't break
37 |
38 | query.definitions = [query.definitions[0]];
39 |
40 | // seems to work now with new tabs, with dispatch moved out of props
41 | dispatch(multipleQueriesDispatchObj);
42 |
43 | OutputOfQuery = graphql(query, {
44 | onError: e => {
45 | // not working
46 | console.log('Too many queries being run.', e);
47 | },
48 | props: ({ data }) => {
49 | // dispatch moved to before query being run
50 |
51 | if (data.loading) {
52 | return {
53 | stateTabReference,
54 | loading: data.loading
55 | };
56 | }
57 | return {
58 | stateTabReference,
59 | error:
60 | 'Currently attempting to run multiple queries, but only one query, subscription, or mutation may be run at one time'
61 | };
62 | }
63 | })(QueryOutputDisplay);
64 | } else {
65 | OutputOfQuery = graphql(query, {
66 | // options: {
67 | // errorPolicy: 'true',
68 | // },
69 | props: ({ data }) => {
70 | if (data.loading) {
71 | return {
72 | stateTabReference,
73 | loading: data.loading
74 | };
75 | }
76 | if (data.error) {
77 | // console.log('error inside QC: ', data.error);
78 | return {
79 | stateTabReference,
80 | error: data.error
81 | };
82 | }
83 | // if query successful, instantiate result obj.
84 | const resultObj = {
85 | stateTabReference,
86 | loading: false
87 | };
88 | // separately assign queryResultVar to output obj
89 | resultObj[queryResultObject] = data[queryResultObject];
90 | return resultObj;
91 | }
92 | // render QOD with props from GraphQL query
93 | })(QueryOutputDisplay);
94 | // console.log(query, 'this is query after QOD')
95 | }
96 | }
97 |
98 | // NOTE: moved endpoint field to inside query
99 | // NOTE: ERRORS ARE MOSTLY BEING RENDERED HERE, NOT INSIDE QUERY OUTPUT DISPLAY.
100 | // ERRORS RENDERED INSIDE OF QOD ARE UNCAUGHT GQL ERRORS
101 |
102 | // too out this conditional from first item under article
103 |
104 | return (
105 |
106 |
107 |
108 |
109 | {query !== '' && stateTabReference === ranQueryTab && }
110 | {queryGQLError !== '' &&
);
14 | else if (Array.isArray(object[key])) {
15 | for (let i = 0; i < object[key].length; i += 1) {
16 | if (object[key][i] === null)
17 | nullVals.push(
18 |
{`${errorPath + key}at index ${i}`}
19 | );
20 | else if (!Array.isArray(object[key][i]) && typeof object[key][i] === 'object') {
21 | nullPath = nullResultCheck(object[key][i], `${errorPath + key} at index ${i} => `);
22 | // ensure nullPath is not an empty string, in that there's a new nullpath to add
23 | if (nullPath !== '') {
24 | // nullVals is always an array. so if path is an array of strings, flatten vals out
25 | // before pushing
26 | nullPath.forEach((curNullPath, idx) => {
27 | nullVals.push(
{curNullPath}
);
28 | });
29 | // nullVals.push(...nullPath);
30 | }
31 | }
32 | }
33 | }
34 | // if value at current key is an object, recurse and assign to a variable
35 | else if (!Array.isArray(object[key]) && typeof object[key] === 'object') {
36 | nullPath = nullResultCheck(object[key], `${errorPath + key} => `);
37 | // ensure nullPath is not an empty string, in that there's a new nullpath to add
38 | if (nullPath !== '') {
39 | // nullVals is always an array. so if path is an array of strings, flatten vals out
40 | // before pushing
41 | nullPath.forEach((curNullPath, idx) => {
42 | nullVals.push(
{curNullPath}
);
43 | });
44 | // nullVals.push(...nullPath);
45 | }
46 | }
47 | }
48 |
49 | // return flat array of string(s)
50 | return nullVals;
51 | };
52 |
53 | const objectTest = {
54 | name: 'Will',
55 | testNull: {
56 | value: 12,
57 | moreNull: null,
58 | superNull: {
59 | pong: null,
60 | bed: 'yes'
61 | }
62 | },
63 | flatNull: null,
64 | age: 90
65 | };
66 |
67 | // should be array of strings
68 | // console.log(nullResultCheck(objectTest));
69 |
70 | export default nullResultCheck;
71 |
--------------------------------------------------------------------------------
/dev/Code Snippets/DEV_QueryInput.jsx:
--------------------------------------------------------------------------------
1 | { /* NOTE: IN CASE THERE ARE RESET/IMMEDIATELY QUERY ISSUES, USE BUTTON OUTSIDE FORM */ }
2 | { /* */ }
23 |
24 |
25 | // /////
26 |
27 | // ! NOTE: USE RUN QUERY DISPATCH HERE TO TEST FOR NESTED QUERIES
28 | // dispatch({
29 | // type: types.RUN_QUERY,
30 | // // decontructed using of gql tag to make query object. need to pass in a stringliteral.
31 | // query: gql([`${textValue}`]),
32 | // // pulls of key for where data will be in result obj
33 | // queryResultObject: textValue.match(/(?<=\{\W)(.*?)(?=\@)/g)[0].trim(),
34 | // newEndpoint: urlToSend,
35 | // });
36 | // // reset local api endpoint
37 | // setNewAPIEndpoint('');
38 | // return;
39 | // ! END OF NESTED TEST
40 |
41 | // ! TO DELETE: TEST METHOD TO SEE IF FRONTEND CONNECTS TO SERVER
42 | // const serverCheck = () => {
43 | // event.preventDefault();
44 |
45 | // // this goes directly to dev server
46 | // // works with localhost:3030. need to have server SERVE up app
47 | // fetch('/api')
48 | // .then(response => response.json())
49 | // .then((data) => {
50 | // console.log('response: ', data.msg);
51 | // })
52 | // .catch(e => console.log('error in server test: ', e));
53 | // };
54 |
--------------------------------------------------------------------------------
/dev/Code Snippets/DEV_handleQueryFetch.js:
--------------------------------------------------------------------------------
1 | import gql from 'graphql-tag';
2 | import fetchErrorCheck from './fetchErrorCheck';
3 | import * as types from '../../Constants/actionTypes';
4 |
5 | const proxy = Number(process.env.IS_DEV) === 1 ? 'https://cors-anywhere.herokuapp.com/' : '';
6 |
7 | // MIGHT rework to invoke inside of apollo custom fetch.
8 |
9 | const handleQueryFetch = (textValue, urlToSend, dispatch, setNewAPIEndpoint) => {
10 | // prevent refresh
11 | event.preventDefault();
12 |
13 | // console.log('Running handleQueryFetch');
14 | // const urlToSend = newAPIEndpoint || endpoint;
15 |
16 | // // send textValue to Dexie db
17 | // db.history.put({
18 | // query: textValue,
19 | // endpoint: urlToSend,
20 | // })
21 | // .then(() => console.log('Sent to database.'))
22 | // .catch(e => console.log('Error adding query to database.'));
23 |
24 |
25 | // ! NOTE: Nested test dispatch added to codeSnippets
26 |
27 | // make initial fetch to api, to ensure endpoint is valid. proxy to get around CORS
28 |
29 | // added w/ new promise format
30 | return new Promise((resolve, reject) => {
31 | // * NOTE, 8/6: THROWING ERRORS BEFORE CATCH SEEMS TO HAVE BETTER ERROR MESSAGING IN CONSOLE LOGS
32 | // * REJECTING IN CATCH SEEMS TO HAVE BETTER HANDLING
33 |
34 | fetch(proxy + urlToSend, {
35 | // mode: 'no-cors',
36 | headers: {
37 | 'Access-Control-Allow-Origin': '*',
38 | 'Content-Type': 'application/json',
39 | },
40 | })
41 | .then((response) => {
42 | // catch all for when textValue is null
43 | // console.log('in first then block for fetch');
44 |
45 | // execute regex filtering on the path param
46 | const pathRegex = textValue.match(/(?<=path:\W*\")\S*(?=\")/gi);
47 | // SHOULD ADD CHECKS FOR 400, 401, 403, maybe more
48 | // 422 => happened in one instance with an API key but no attached query
49 | if (response.status === 404) {
50 | dispatch({
51 | // send off error message for endpoint 404
52 | type: types.GQL_ERROR,
53 | gqlError: 'Endpoint is invalid. Please double check your endpoint.',
54 | });
55 | // throwing error stops promise chain
56 | throw new Error('Endpoint is invalid. Please double check your endpoint.');
57 | } else if (pathRegex === null) {
58 | // if regex is null, then there's no path
59 | dispatch({
60 | // dispatch path error
61 | type: types.GQL_ERROR,
62 | // changed this dispatch message to match error thrown below
63 | gqlError: 'Path is invalid. Please double check your path.',
64 | // gqlError: '@rest must have a \'path\' and \'type\' property. Please click reset to check the example for reference.',
65 | });
66 | // throwing error/reject stops promise chain
67 | throw new Error('Path is invalid. Please double check your path.');
68 | } else {
69 | // if regex is NOT null, there was a path. fetch is now made to endpoint + path
70 | const path = textValue.match(/(?<=path:\W*\")\S*(?=\")/gi)[0].trim();
71 | // ! NEED: check if there's a param in the path
72 | // return fetch, which creates a promise
73 | return fetch(proxy + urlToSend + path, {
74 | headers: {
75 | 'Access-Control-Allow-Origin': '*',
76 | 'Content-Type': 'application/json',
77 | },
78 | });
79 | }
80 | })
81 | // for checking if the path is correct
82 | .then((response) => {
83 | // console.log('in second then block of fetch');
84 | if (response.status === 404) {
85 | dispatch({
86 | type: types.GQL_ERROR,
87 | gqlError: 'Path is invalid. Please double check your path.',
88 | });
89 | throw new Error('Path is invalid. Please double check your path.');
90 | } else return response.json();
91 | })
92 | .then((data) => {
93 | // console.log('in third then of fetch, before run query dispatch');
94 | // if get request is successful, parse it here. fire dispatch to run query
95 | dispatch({
96 | type: types.RUN_QUERY,
97 | // decontructed using of gql tag to make query object. need to pass in a stringliteral.
98 | query: gql([`${textValue}`]),
99 | // pulls of key for where data will be in result obj
100 | queryResultObject: textValue.match(/(?<=\{\W)(.*?)(?=\@)/g)[0].trim(),
101 | newEndpoint: urlToSend,
102 | });
103 | // reset local api endpoint
104 | setNewAPIEndpoint('');
105 | resolve('Fetch chain successful.');
106 | })
107 | .catch((error) => {
108 | // moved error checking to other file for code clarity
109 | fetchErrorCheck(error, dispatch, reject);
110 | // added w/new promise format
111 | });
112 | });
113 | };
114 |
115 | export default handleQueryFetch;
116 |
--------------------------------------------------------------------------------
/electron.js:
--------------------------------------------------------------------------------
1 | const electron = require('electron');
2 | // not presently using clipboard
3 | const { clipboard } = require('electron');
4 | const url = require('url');
5 | const path = require('path');
6 |
7 | // to determine whether a dev environ is being un
8 | const isDev = require('electron-is-dev');
9 | // const isDev= false;
10 | const { app, BrowserWindow, Menu } = electron;
11 |
12 | let mainWindow;
13 |
14 | // listen for app to be ready
15 | // NOTE: LOCAL HOST CHANGED TO 8080 SO EXPRESS CAN SERVE UP ELECTORN
16 | app.on('ready', () => {
17 | // create new window
18 | mainWindow = new BrowserWindow({ width: 1170, height: 800 });
19 | // load html into the window
20 | console.log('Dev environment on: ', isDev);
21 | mainWindow.loadURL(
22 | url.format({
23 | // ssiwtched back to 3000 from 8080 because no longer using server
24 | pathname: isDev ? '//localhost:3000' : path.join(__dirname, './build/index.html'),
25 | protocol: isDev ? 'http:' : 'file:',
26 | slashes: true
27 | })
28 | );
29 |
30 | // build menu from template
31 | if (isDev) mainMenuTemplate.push(devToolsMenu);
32 | const mainMenu = Menu.buildFromTemplate(mainMenuTemplate);
33 | // insert Menu
34 | Menu.setApplicationMenu(mainMenu);
35 | });
36 |
37 | // create menu template
38 | const mainMenuTemplate = [
39 | {
40 | label: 'File',
41 | submenu: [
42 | {
43 | label: 'Quit',
44 | click() {
45 | app.quit();
46 | },
47 | accelerator: 'CommandOrControl+Q'
48 | }
49 | ]
50 | },
51 | {
52 | label: 'Edit',
53 | submenu: [
54 | { label: 'Undo', accelerator: 'CmdOrCtrl+Z', selector: 'undo:' },
55 | { label: 'Redo', accelerator: 'Shift+CmdOrCtrl+Z', selector: 'redo:' },
56 | { type: 'separator' },
57 | { label: 'Cut', accelerator: 'CmdOrCtrl+X', selector: 'cut:' },
58 | { label: 'Copy', accelerator: 'CmdOrCtrl+C', selector: 'copy:' },
59 | { label: 'Paste', accelerator: 'CmdOrCtrl+V', selector: 'paste:' },
60 | { label: 'Select All', accelerator: 'CmdOrCtrl+A', selector: 'selectAll:' }
61 | ]
62 | }
63 | ];
64 |
65 | const devToolsMenu = {
66 | label: 'View',
67 | submenu: [
68 | {
69 | // should be available only in dev environment
70 | open: false,
71 | label: 'DevTools',
72 | click() {
73 | if (!this.open) {
74 | mainWindow.webContents.openDevTools();
75 | this.open = true;
76 | } else {
77 | mainWindow.webContents.closeDevTools();
78 | this.open = false;
79 | }
80 | },
81 | accelerator: 'f12'
82 | }
83 | ]
84 | };
85 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Hypnos
7 |
8 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "hypnos",
3 | "version": "0.0.2",
4 | "description": "An app to test GraphQL calls on RESTful APIs, via Apollo.",
5 | "main": "electron.js",
6 | "homepage": "./",
7 | "scripts": {
8 | "start-electron": "electron .",
9 | "start-electron-prod": "ELECTRON_IS_DEV=0 npm run start-electron",
10 | "electron-build": "electron-builder -mwl",
11 | "clean-build": "rm -rf build/ .cache dist/",
12 | "react-start": "NODE_ENV=development parcel -p 3000 index.html --out-dir build",
13 | "react-build": "NODE_ENV=production parcel build index.html --out-dir build --public-url ./",
14 | "start": "npm run react-start & wait-on http://localhost:3000 && ELECTRON_IS_DEV=1 npm run start-electron",
15 | "build": "npm run clean-build && npm run react-build && ELECTRON_IS_DEV=0 npm run electron-build",
16 | "test": "jest --verbose"
17 | },
18 | "keywords": [
19 | "Apollo",
20 | "Apollo-Link-Rest",
21 | "Electron",
22 | "Jest",
23 | "Enzyme",
24 | "Puppeteer",
25 | "Testing",
26 | "Dexie",
27 | "IndexDB",
28 | "React",
29 | "Parcel",
30 | "GraphQL",
31 | "REST API",
32 | "API",
33 | "Query",
34 | "Mutation"
35 | ],
36 | "author": {
37 | "name": "Sophie Nye",
38 | "email": "sophie.nye@gmail.com"
39 | },
40 | "contributors": [
41 | "Dillon Garrett