├── .DS_Store
├── .gitignore
├── LICENSE
├── README.md
├── _tests
├── App.test.js
├── enzyme.test.js
└── server.test.js
├── assets
└── icons
│ ├── mac
│ └── icon.icns
│ ├── png
│ └── icon.png
│ └── win
│ └── icon.ico
├── build
└── bundle.js
├── client
├── components
│ ├── App.jsx
│ ├── Container.jsx
│ ├── DatabInputModal.jsx
│ ├── Errors.jsx
│ ├── Metrics.jsx
│ ├── Response.jsx
│ ├── Schema.jsx
│ ├── SchemaCards.jsx
│ ├── SubmitQuery.jsx
│ ├── SubmitUrlComponent.jsx
│ └── test.js
├── index.js
└── styles
│ └── style.css
├── index.html
├── main.js
├── package-lock.json
├── package.json
├── server
├── controllers
│ └── queryController.js
├── createTable.sql
├── models
│ └── queryModels.js
└── server.js
├── webpack.config.js
├── yarn 2.lock
└── yarn.lock
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/miraql/ec2024cf292aa04baff46e716c672c1fb0b05ca2/.DS_Store
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | build
3 | release-builds
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 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 | # MiraQL
2 |
3 | 
4 |
5 | ## MiraQL
6 |
7 | MiraQL is an Electron based desktop app designed to assist programmers by rendering graphical representations of GraphQL queries and schemas, and providing a space for performance and speed metrics monitoring.
8 |
9 | MiraQL is currently in beta. Please feel free to post any issues to our GitHub. We welcome any and all feedback!
10 |
11 | Read about our project on [Medium](https://medium.com/@jane.minhyung.kim/introducing-miraql-the-next-generation-graphql-gui-cd03adb80ed2)!
12 |
13 | ## Table of Contents
14 |
15 | ### Download Instructions
16 |
17 | - [Getting Started](#download-instructions)
18 | - [Usage](#getting-started)
19 | - [Contact Us](#contact-us)
20 | - [Production Team](#the-production-team)
21 | - [License](#license)
22 |
23 | ## Download Instructions
24 |
25 | 1. Simply click to download: [Mac](https://drive.google.com/file/d/1lHg_gUCfCbpjMr11J5snq23-wAogvkWz/view?usp=sharing), Windows, Linux
26 | 2. Extract the file from the dmg and double click to open.
27 | 3. You may need to go in to Security & Privacy settings to allow MiraQL to be opened.
28 |
29 | _Please note: our app is currently unsupported by Big Sur._
30 |
31 | ## Getting Started
32 |
33 | **Feature 1: GraphQL Playground**
34 | Once your app opens, users can submit API endpoints in the form of URLs and test their queries and responses. Write and submit queries in the input field to receive responses or errors, and view them in the response and errors tabs.
35 |
36 | 
37 |
38 | **Feature 2: Easy Error Visualization**
39 | If your response returns any errors, head to the errors tab to view them in an intuitive manner. View the status, status code, error type, and error message in an easy to read table.
40 |
41 | 
42 |
43 | **Feature 3: Schema Visualization**
44 | Users can add in tables for a graphical representation of all the schema’s fields and relationships that they can then refer to while writing or editing queries.
45 |
46 | 
47 |
48 | **Feature 4: Response Metrics Analysis**
49 | Go to the metrics tab to view data visualizations of query response speeds. Compare the speed of your most recent query to those of all previous query requests made with MiraQL in a line graph that updates every time you submit a query.
50 |
51 | 
52 |
53 | ## Contact Us
54 |
55 | - [Website](https://miraql.org)
56 | - [Github Link](https://github.com/oslabs-beta/miraql)
57 |
58 | ## The Production Team
59 |
60 | - [Kristiina Eelnurme](https://www.linkedin.com/in/kristiina-eelnurme/) - [@keelnu](https://github.com/keelnu)
61 | - [Sabrina Goldfarb](https://www.linkedin.com/in/sabrinagoldfarb/) - [@sgoldfarb2](https://github.com/sgoldfarb2)
62 | - [Jane Kim](https://www.linkedin.com/in/janeminhyungkim/) - [@janemkim](https://github.com/miguelmanalo)
63 | - [Miguel Manalo](https://www.linkedin.com/in/mmanalo/) - [@miguelmanalo](https://github.com/miguelmanalo)
64 | - [Sanaya Mirpuri](https://www.linkedin.com/in/sanayamirpuri/) - [@sanayamirpuri](https://github.com/sanayamirpuri)
65 |
66 | ## License
67 |
68 | This project is licensed under the [MIT License](https://opensource.org/licenses/mit-license.php).
69 |
--------------------------------------------------------------------------------
/_tests/App.test.js:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { render } from "@testing-library/react";
3 |
4 | import { App } from "./App";
5 |
6 |
--------------------------------------------------------------------------------
/_tests/enzyme.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { configure, shallow } from 'enzyme';
3 | import Adapter from 'enzyme-adapter-react-16';
4 | import toJson from 'enzyme-to-json';
5 |
6 | // Using Enzyme to wrap around React test utilities to shallow render the tree and test it.
7 | import App from '../client/coponents/App';
8 | import Container from '../client/components/Container';
9 | import SubmitUrlComponent from '../client/components/SubmitUrlComponent';
10 |
11 | configure({ adapter: new Adapter() });
12 |
13 | // Unit tests for MiraQL
14 | describe('MiraQL unit tests', () => {
15 | describe('App', () => {
16 | let wrapper;
17 | const props = {
18 |
19 | }
20 | })
21 | })
22 |
23 |
--------------------------------------------------------------------------------
/_tests/server.test.js:
--------------------------------------------------------------------------------
1 | const request = require("supertest");
2 |
3 | // Make sure to run npm run dev before running test
4 | // npm run dev is hosted on 8080
5 | const server = "http://localhost:8080";
6 |
7 | describe("Route integration", () => {
8 | // make sure the GET is working at http://localhost:8080/
9 | describe("/", () => {
10 | describe("GET", () => {
11 | it("responds with http 200 status and text/html content type", () => {
12 | return request(server)
13 | .get("/")
14 | .expect("Content-Type", /text\/html/)
15 | .expect(200);
16 | });
17 | });
18 | });
19 | });
20 |
--------------------------------------------------------------------------------
/assets/icons/mac/icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/miraql/ec2024cf292aa04baff46e716c672c1fb0b05ca2/assets/icons/mac/icon.icns
--------------------------------------------------------------------------------
/assets/icons/png/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/miraql/ec2024cf292aa04baff46e716c672c1fb0b05ca2/assets/icons/png/icon.png
--------------------------------------------------------------------------------
/assets/icons/win/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/miraql/ec2024cf292aa04baff46e716c672c1fb0b05ca2/assets/icons/win/icon.ico
--------------------------------------------------------------------------------
/client/components/App.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Container from './Container.jsx'
3 |
4 |
5 |
6 | function App(){
7 | return (
8 |
9 |
10 |
11 | )
12 | }
13 | export default App;
--------------------------------------------------------------------------------
/client/components/Container.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Grid, GridItem, Image, Stack, Heading } from '@chakra-ui/react';
3 | import SubmitUrlComponent from './SubmitUrlComponent.jsx';
4 | import '../styles/style.css';
5 |
6 | // container holds the MiraQL header and submitURL component
7 | function Container() {
8 | return (
9 |
10 |
11 |
12 |
13 |
21 | MiraQL
22 |
23 |
24 |
25 |
26 |
27 | );
28 | }
29 |
30 | export default Container;
31 |
--------------------------------------------------------------------------------
/client/components/DatabInputModal.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import Schema from "./Schema.jsx";
3 | import SchemaCards from "./SchemaCards.jsx";
4 | import {
5 | Modal,
6 | ModalOverlay,
7 | ModalContent,
8 | ModalHeader,
9 | ModalFooter,
10 | ModalBody,
11 | ModalCloseButton,
12 | useDisclosure,
13 | Button,
14 | Editable,
15 | EditableInput,
16 | EditablePreview,
17 | Select,
18 | Switch,
19 | FormControl,
20 | FormLabel,
21 | HStack,
22 | VStack,
23 | Flex,
24 | Spacer,
25 | } from "@chakra-ui/react";
26 |
27 | const initialValues = {
28 | fieldName: "",
29 | fieldType: "",
30 | defaultValue: "",
31 | primaryKey: false,
32 | unique: false,
33 | required: false,
34 | queryable: false,
35 | tableRelationship: "",
36 | fieldRelationship: "",
37 | typeRelationship: "",
38 | };
39 |
40 | function DatabInputModal() {
41 | // first make a deep copy of our initial values
42 | let copyOfInitValues = Object.assign({}, initialValues);
43 | // use hooks to create a new state based on the copy of the initial values
44 | let [inputs, setInputs] = useState([copyOfInitValues]);
45 |
46 | // create a state for tableName
47 | let [ourTableName, setTableName] = useState("");
48 |
49 | const changeTableName = (fieldName) => ({ target }) =>
50 | setTableName(target.value);
51 |
52 | // this function handles changes to the initial input field state (1st row being added)
53 | // const onChangeForField = fieldName => ({target}) => setInputs(state => ({...state,[fieldName]:target.value}));
54 |
55 | const onChangeForNow = (ev, index) => {
56 | // grab our event.target and use destructuring to make variables name and value with ev.target.name and ev.target.value respectively
57 | const { name, value } = ev.target;
58 | // create a variable list that is an array of a copy of our object
59 | const list = [...inputs];
60 | // edit the value of list at the index with a key of the name
61 | list[index][name] = value;
62 | // use hooks to change our state using set inputs with our updated list
63 | setInputs(list);
64 | };
65 |
66 | // handle click event of the Remove button
67 | const handleRemoveClick = (index) => {
68 | const list = [...inputs];
69 | list.splice(index, 1);
70 | setInputs(list);
71 | };
72 |
73 | // handle click event of the Add button
74 | const handleAddClick = () => {
75 | // when we make a new row, we use our untouched version of initial values, so we have a clean copy of initialValues in our state
76 | setInputs([...inputs, copyOfInitValues]);
77 | };
78 |
79 | const stateBoolean = true;
80 | // this function runs when the save button is clicked, it runs a post request to the database and then it empties out the modal and resets state
81 | const saveButtonClick = () => {
82 | fetch("/schema", {
83 | headers: { "Content-type": "application/json" },
84 | body: JSON.stringify({ ourTableName, inputs }),
85 | method: "POST",
86 | }).then((res) => {
87 | setInputs([copyOfInitValues]);
88 | setTableName("");
89 | });
90 | console.log({ ourTableName, inputs });
91 | console.log("saved!");
92 | changeBoolFunc(stateBoolean);
93 | onClose();
94 | };
95 |
96 | const changeBoolFunc = (bool) => {
97 | return !bool;
98 | };
99 |
100 | const { isOpen, onOpen, onClose } = useDisclosure();
101 |
102 | return (
103 | <>
104 |
105 | Add Table
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 | {/* this first editable block is an input field for the table name */}
114 |
120 |
121 |
126 |
127 |
128 |
129 |
130 | {inputs.map((element, i) => {
131 | // console.log(element)
132 | // console.log(i)
133 | return (
134 | <>
135 |
136 | {/* this second editable block is an input field for the field name */}
137 |
138 | Field name
139 |
145 |
146 | onChangeForNow(ev, i)}
149 | value={element.fieldName}
150 | key={`fieldName${i}`}
151 | />
152 |
153 |
154 |
155 | {/* Dropdown menu to select your field type */}
156 |
157 | Field Type
158 | onChangeForNow(ev, i)}
162 | key={`fieldType${i}`}
163 | >
164 | ID
165 | String
166 | Boolean
167 | Int
168 | Float
169 |
170 |
171 |
172 | {/* this editable block is an input field for the default value */}
173 |
174 | Default Value
175 |
181 |
182 | onChangeForNow(ev, i)}
185 | value={element.defaultValue}
186 | key={`defaultValue${i}`}
187 | />
188 |
189 |
190 |
191 | {/* these blocks are for our four switches */}
192 |
197 |
198 |
199 | Primary Key
200 |
201 | onChangeForNow(ev, i)}
205 | value={true}
206 | key={`primaryKey${i}`}
207 | />
208 |
209 |
210 |
211 |
216 |
217 |
218 | Unique
219 |
220 | onChangeForNow(ev, i)}
224 | value={true}
225 | key={`uniqueSwitch${i}`}
226 | />
227 |
228 |
229 |
230 |
235 |
236 |
237 | Required?
238 |
239 | onChangeForNow(ev, i)}
243 | value={true}
244 | key={`requiredSwitch${i}`}
245 | />
246 |
247 |
248 |
249 |
254 |
255 |
256 | Queryable?
257 |
258 | onChangeForNow(ev, i)}
262 | value={true}
263 | key={`queryableSwitch${i}`}
264 | />
265 |
266 |
267 |
268 | {/* these three selects are for table relationship, field relationship, and type of relationship */}
269 |
270 | Table Relationship
271 | onChangeForNow(ev, i)}
275 | key={`tableRelationship${i}`}
276 | >
277 | ID
278 |
279 |
280 |
281 |
282 | Field Relationship
283 | onChangeForNow(ev, i)}
287 | key={`fieldRelationship${i}`}
288 | >
289 | ID
290 |
291 |
292 |
293 |
294 | Type of Relationship
295 | onChangeForNow(ev, i)}
299 | key={`typeRelationship${i}`}
300 | >
301 | ID
302 |
303 | {inputs.length !== 1 && (
304 | handleRemoveClick(i)}>
305 | Remove
306 |
307 | )}
308 |
309 |
310 |
311 | {inputs.length - 1 === i && (
312 |
313 | + Add Field
314 |
315 | )}
316 | >
317 | );
318 | })}
319 |
320 |
321 |
322 |
323 |
324 | Save
325 |
326 |
327 |
328 |
329 | {/* */}
330 | {/* pass down state using NEW COMPONENT WITH CARDS */}
331 |
332 | >
333 | );
334 | }
335 |
336 | export default DatabInputModal;
337 |
--------------------------------------------------------------------------------
/client/components/Errors.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import {
3 | Table,
4 | Thead,
5 | Tbody,
6 | Tr,
7 | Th,
8 | Td,
9 | TableCaption,
10 | Text,
11 | } from '@chakra-ui/react';
12 |
13 | const Errors = ({ fetchResponse, errors }) => {
14 | // setting variables
15 | let status = '';
16 | let statusCode = '';
17 | let errorType = '';
18 | let errorMessage = '';
19 |
20 | // if there are errors show these errors
21 | if (errors === true) {
22 | status = 'Unsuccessful Query';
23 | errorType = 'Client Error';
24 | statusCode = '400';
25 | errorMessage = 'Incorrect syntax or connection URL';
26 | }
27 |
28 | return (
29 |
30 |
31 |
32 | Query Information
33 | Result
34 |
35 |
36 |
37 |
38 | Status
39 |
40 | {status}
41 |
42 |
43 |
44 | Status Code
45 |
46 | {statusCode}
47 |
48 |
49 |
50 | Error Type
51 |
52 | {errorType}
53 |
54 |
55 |
56 | Message
57 |
58 | {errorMessage}
59 |
60 |
61 |
62 |
63 | );
64 | };
65 |
66 | export default Errors;
67 |
--------------------------------------------------------------------------------
/client/components/Metrics.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Line } from 'react-chartjs-2';
3 |
4 | const Metrics = ({ fetchResponse, queryResponseTime, queryTitle }) => {
5 | // calculate query response time average
6 | const queryResponseAverage = (
7 | queryResponseTime.reduce((a, b) => a + b, 0) / queryResponseTime.length
8 | ).toFixed(2);
9 |
10 | // store all chart data in state and add in passed down data as labels and data
11 | const state = {
12 | labels: queryTitle,
13 | datasets: [
14 | {
15 | label: 'Time',
16 | fill: false,
17 | lineTension: 0.2,
18 | backgroundColor: 'rgba(244, 93, 183, 1)',
19 | borderColor: 'rgba(0,0,0,1)',
20 | borderWidth: 2,
21 | data: queryResponseTime,
22 | },
23 | ],
24 | };
25 |
26 | return (
27 |
28 |
42 |
43 | );
44 | };
45 |
46 | export default Metrics;
47 |
--------------------------------------------------------------------------------
/client/components/Response.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { UnControlled as CodeMirror } from 'react-codemirror2';
3 | import '../../node_modules/codemirror/lib/codemirror.css';
4 | import '../../node_modules/codemirror/theme/neo.css';
5 | import 'codemirror/mode/javascript/javascript';
6 | import 'codemirror/addon/edit/closebrackets';
7 | import {
8 | Tabs,
9 | TabList,
10 | TabPanels,
11 | Tab,
12 | TabPanel,
13 | Grid,
14 | GridItem,
15 | } from '@chakra-ui/react';
16 | import Errors from './Errors.jsx';
17 |
18 | function Response({ urlValue, query, fetchResponse, errors }) {
19 | // clean response to show in code mirror
20 | const cleanResponse = JSON.stringify(fetchResponse, null, 2);
21 |
22 | return (
23 |
24 |
25 | Response
26 | Errors
27 |
28 |
29 |
30 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | );
49 | }
50 |
51 | export default Response;
52 |
--------------------------------------------------------------------------------
/client/components/Schema.jsx:
--------------------------------------------------------------------------------
1 | import { tree } from 'd3';
2 | import React, { useContext, useEffect, useState } from 'react';
3 | import { Tree, treeUtil } from 'react-d3-tree';
4 | import { Button } from "@chakra-ui/react";
5 |
6 | // const myTreeData = [
7 | // {
8 | // name: 'Queries',
9 | // // attributes: {
10 | // // keyA: "val A",
11 | // // keyB: "val B",
12 | // // keyC: "val C",
13 | // // },
14 | // nodeSvgShape: {
15 | // shape: 'rect',
16 | // shapeProps: {
17 | // width: 20,
18 | // height: 20,
19 | // x: -10,
20 | // y: -10,
21 | // fill: 'yellow',
22 | // },
23 | // },
24 | // children: [
25 | // {
26 | // name: 'Author',
27 | // nodeSvgShape: {
28 | // shape: 'rect',
29 | // shapeProps: {
30 | // width: 20,
31 | // height: 20,
32 | // x: -10,
33 | // y: -10,
34 | // fill: 'yellow',
35 | // },
36 | // },
37 | // // styles: {
38 | // // links:{
39 | // // stroke: 'yellow',
40 | // // strokeWidth: "2px",
41 | // // }
42 | // // },
43 |
44 | // children: [
45 | // {
46 | // name: 'authorid',
47 | // },
48 | // {
49 | // name: 'age',
50 | // },
51 | // {
52 | // name: 'name',
53 | // },
54 | // ],
55 | // },
56 |
57 | // {
58 | // name: 'Book',
59 | // children: [
60 | // {
61 | // name: 'bookid',
62 | // },
63 | // {
64 | // name: 'genre',
65 | // },
66 | // {
67 | // name: 'name',
68 | // },
69 | // ],
70 | // },
71 | // ],
72 | // },
73 | // ];
74 |
75 | function Schema(query) {
76 | // console.log(props)
77 | // const [schemaResponse, setResponse] = useState([]);
78 | // const [fieldResponse, setFields] = useState([]);
79 | const [updateState, stateUpdates] = useState();
80 | const [allData, setAllData] = useState();
81 | const [treeData, setTreeData] = useState();
82 |
83 | let stateBoolean = true;
84 |
85 | function handleUpdate() {
86 | // console.log('that update was handled')
87 | stateUpdates({});
88 | }
89 |
90 | // fetch request to get all the table names and field information for our tables
91 | // const schemaTreeData = [];
92 |
93 | useEffect(() => {
94 | fetch('/everything')
95 | .then((res) => {
96 | return res.json();
97 | })
98 | .then((res) => {
99 | setAllData(res);
100 | })
101 | .catch((err) => {
102 | console.log(err);
103 | });
104 | }, [updateState]);
105 |
106 | useEffect(() => {
107 | treeUtil.parseJSON('/everything').then((data) => setTreeData(data));
108 | }, []);
109 |
110 | const fillTreeData = () => {
111 | treeUtil.parseJSON('/everything').then((data) => setTreeData(data));
112 | };
113 |
114 | // console.log('this is ALLLLLLL DATA', allData)
115 |
116 | const cache = {};
117 | // testArray is created to start our tree and be the root node
118 | const testArray = [{ name: 'Queries', children: [] }];
119 | console.log('this is test array', testArray);
120 |
121 | // this creates our entire cache
122 | if (allData) {
123 | for (let i = 0; i < allData.length; i++) {
124 | // console.log('what im pushing', allData[i]["field_name"])
125 | if (!cache[allData[i]['schema_name']]) {
126 | cache[allData[i]['schema_name']] = [allData[i]['field_name']];
127 | // console.log('cache', cache);
128 | } else cache[allData[i]['schema_name']].push(allData[i]['field_name']);
129 | }
130 | console.log('this is the cache', cache);
131 |
132 | const conversion = (cache) => {
133 | const output = [];
134 |
135 | for (const key in cache) {
136 | const newObj = {};
137 | const fields = cache[key];
138 | newObj.name = key;
139 | newObj.children = fields.map((el) => {
140 | return { name: el };
141 | });
142 | testArray[0]['children'].push(newObj);
143 | }
144 | console.log(
145 | 'this is the test array after we push in our tree',
146 | testArray
147 | );
148 | };
149 |
150 | conversion(cache);
151 | }
152 |
153 | const svgSquare = {
154 | shape: 'rect',
155 | shapeProps: {
156 | width: 20,
157 | height: 20,
158 | x: -10,
159 | y: -10,
160 | fill: 'pink',
161 | },
162 | };
163 | if (allData === undefined) {
164 | return <>Still loading...>;
165 | }
166 | return (
167 | <>
168 | Render Schema
169 | {/* Render Schema */}
170 |
184 | >
185 | );
186 | }
187 |
188 | export default Schema;
189 |
--------------------------------------------------------------------------------
/client/components/SchemaCards.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import {
3 | Table,
4 | Thead,
5 | Tbody,
6 | Tr,
7 | Th,
8 | Td,
9 | TableCaption,
10 | Box,
11 | } from "@chakra-ui/react";
12 |
13 | const SchemaCards = (boolSwap) => {
14 | const [updateState, stateUpdates] = useState();
15 | const [allData, setAllData] = useState([]);
16 |
17 |
18 |
19 | // Fetch join table from /everything and populate setAllData state
20 | // useEffect(() => {
21 | // fetch("/everything")
22 | // .then((res) => {
23 | // return res.json();
24 | // })
25 | // .then((res) =>{
26 | // setAllData(res)
27 | // })
28 | // .catch((err) => {
29 | // console.log(err);
30 | // });
31 | // }, [updateState]);
32 |
33 | useEffect(() => {
34 | fetch("/everything")
35 | .then((res) => {
36 | return res.json();
37 | })
38 | .then((res) =>{
39 | setAllData(res)
40 | })
41 | .catch((err) => {
42 | console.log(err);
43 | });
44 | }, [boolSwap]);
45 |
46 | // Parse through setAllData state and organize data into cache by table name, field name, and field type
47 | const cache = {};
48 | for(let i = 0; i < allData.length; i++) {
49 | if (!cache[allData[i]["schema_name"]]) {
50 | cache[allData[i]["schema_name"]] = [allData[i]["field_name"], allData[i]["field_type"]];
51 | }
52 | else cache[allData[i]["schema_name"]].push(allData[i]["field_name"], allData[i]["field_type"]);
53 | };
54 |
55 | // Create array of arrays of cache with Object.entries
56 | const cacheEntries = Object.entries(cache);
57 | // console.log('cacheEntries', cacheEntries);
58 |
59 | // Create table array to populate with tables from setAllData
60 | const tableArray = [];
61 |
62 | // Create a table with the data inside each element in cacheEntries.
63 | cacheEntries.forEach(element => {
64 | // console.log('fieldTypes', element[1]);
65 |
66 | // Create a table to populate with table data
67 | const table = [];
68 | // Push field names and field types into each row in table
69 | for (let i = 0; i < element[1].length; i = i + 2) {
70 | table.push(
71 |
72 | {element[1][i]}
73 | {element[1][i+1]}
74 |
75 | );
76 | };
77 |
78 | // Push all tables with schema name(tableHeader) and buttons into tableArray
79 | tableArray.push(
80 | <>
81 |
82 |
83 |
84 | {/* {// grab table_name from response object} */}
85 |
86 | {element[0]}
87 |
88 |
89 | {/* {// grab field_name and field_type from response object} */}
90 | Field Name
91 | Field Type
92 |
93 | {table}
94 |
95 | {/* */}
96 |
97 | Delete
98 | Update
99 | >
100 | );
101 | });
102 |
103 | // Return out all the tables created.
104 | return (
105 | <>
106 |
107 | {tableArray}
108 |
109 | >
110 | );
111 | };
112 |
113 | export default SchemaCards;
114 |
--------------------------------------------------------------------------------
/client/components/SubmitQuery.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import {
3 | Grid,
4 | GridItem,
5 | Text,
6 | Textarea,
7 | Button,
8 | ButtonGroup,
9 | Stack,
10 | Tabs,
11 | TabList,
12 | TabPanels,
13 | Tab,
14 | TabPanel,
15 | } from '@chakra-ui/react';
16 | import Response from './Response.jsx';
17 | import { Controlled as CodeMirror } from 'react-codemirror2';
18 | import '../../node_modules/codemirror/lib/codemirror.css';
19 | import '../../node_modules/codemirror/theme/neo.css';
20 | import 'codemirror/mode/javascript/javascript';
21 | import 'codemirror/addon/edit/closebrackets';
22 |
23 | import '../styles/style.css';
24 | import DatabInputModal from './DatabInputModal.jsx';
25 | import Schema from './Schema.jsx';
26 | import Metrics from './Metrics.jsx';
27 |
28 | function SubmitQuery({ urlValue }) {
29 | // hook to hold query in state
30 | let [query, setQuery] = useState('');
31 | // hook to hold fetch response in state
32 | let [fetchResponse, setFetchResponse] = useState('');
33 | // hook to hold responseTime in state
34 | let [queryResponseTime, setQueryResponseTime] = useState([]);
35 | // hook to hold query number in state
36 | let [queryTitle, setQueryNumber] = useState([]);
37 | // hook to catch errors in fetch request
38 | let [errors, setErrors] = useState(false);
39 |
40 | // handle query text input change
41 | const handleQueryChange = (e) => {
42 | let inputValue = e.target.value;
43 | setQuery(inputValue);
44 | };
45 |
46 | // function to handle submit button (query request and timer)
47 | const handleSubmit = () => {
48 | // start a timer
49 | const startTime = Date.now();
50 |
51 | // push query input into array in state to render on metrics graph
52 | setQueryNumber((queryTitle) => [...queryTitle, 'Query']);
53 |
54 | // execute a fetch request to get query response and stop time
55 | fetch(`${urlValue}`, {
56 | method: 'POST',
57 | headers: {
58 | 'Content-type': 'application/json',
59 | },
60 | body: JSON.stringify({ query: query }),
61 | })
62 | .then((res) => res.json())
63 | .then((res) => {
64 | // stop timer and push query resolve time to state
65 | const responseTime = (Date.now() - startTime) / 1000;
66 | setQueryResponseTime((queryResponseTime) => [
67 | ...queryResponseTime,
68 | responseTime,
69 | ]);
70 | // store fetch response in state
71 | setFetchResponse(res);
72 | // check if fetch response has errors update state
73 | if (res.errors) {
74 | setErrors(true);
75 | }
76 | })
77 | .catch((error) => console.log(error));
78 | };
79 |
80 | return (
81 |
82 |
83 |
84 |
85 | Submit Query
86 |
87 |
88 |
89 | {
100 | setQuery(value);
101 | }}
102 | onChange={(editor, data, value) => {}}
103 | />
104 |
110 | Submit
111 |
112 |
113 |
114 |
115 |
116 |
117 |
123 |
124 |
125 |
126 |
127 | Add Tables
128 | Schema
129 | Metrics
130 |
131 |
132 |
133 |
134 | {/* new tables card component? */}
135 |
136 |
137 |
138 |
139 |
140 |
147 |
148 |
149 |
150 |
151 |
152 | );
153 | }
154 |
155 | export default SubmitQuery;
156 |
--------------------------------------------------------------------------------
/client/components/SubmitUrlComponent.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import {
3 | FormControl,
4 | FormLabel,
5 | Input,
6 | Button,
7 | Stack,
8 | Grid,
9 | GridItem,
10 | Popover,
11 | PopoverTrigger,
12 | PopoverContent,
13 | PopoverHeader,
14 | PopoverBody,
15 | PopoverFooter,
16 | PopoverArrow,
17 | PopoverCloseButton,
18 | } from '@chakra-ui/react';
19 | import SubmitQuery from './SubmitQuery.jsx';
20 |
21 | // submitURL component is the parent to submitQuery component (so we can access urlValue in state)
22 |
23 | function SubmitUrlComponent() {
24 | // react hooks to set state for url value
25 | let [urlValue, setUrlValue] = useState('');
26 |
27 | // function to handle change for the url input and store whatever is typed in state
28 | const handleUrlInput = (e) => {
29 | let inputValue = e.target.value;
30 | setUrlValue(inputValue);
31 | };
32 |
33 | // return chakra form, added function created above onChange in the input field and returning submitQuery component
34 | return (
35 |
36 |
37 |
38 |
39 | URL:
40 |
41 |
46 |
47 |
48 |
49 | Connect
50 |
51 |
52 |
53 | URL Submitted!
54 |
55 |
56 |
57 |
58 | Now that you’ve submitted a URL, please submit a query and/or
59 | schema!
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 | );
69 | }
70 |
71 | export default SubmitUrlComponent;
72 |
--------------------------------------------------------------------------------
/client/components/test.js:
--------------------------------------------------------------------------------
1 | const testData = {
2 | books: ['author', 'name', 'title'],
3 | authors: ['id', 'name'],
4 | };
5 |
6 | const conversion = (cache) => {
7 | const output = [];
8 |
9 | for (const key in cache) {
10 | const newObj = {};
11 | const fields = cache[key];
12 | newObj.name = key;
13 | newObj.children = fields.map((el) => {
14 | return { name: el };
15 | });
16 | output.push(newObj);
17 | }
18 | return output;
19 | };
20 |
21 | console.log(conversion(testData));
22 |
--------------------------------------------------------------------------------
/client/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from 'react-dom';
3 | //import ChakraProvider component to use ChakraUI
4 | import { ChakraProvider, extendTheme } from '@chakra-ui/react';
5 | import App from './components/App.jsx';
6 |
7 | // adding Chaka brand colors
8 | const colors = {
9 | brand: {},
10 | };
11 |
12 | // extending theme with colors dictated above
13 | const theme = extendTheme({ colors });
14 |
15 | // rendering full app - added root script
16 | render(
17 |
18 |
19 | ,
20 | document.getElementById('root')
21 | );
22 |
--------------------------------------------------------------------------------
/client/styles/style.css:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap');
2 |
3 | .CodeMirror {
4 | border: 1px solid #eee;
5 | min-height: 80%;
6 | }
7 |
8 | #submitbutton {
9 | margin-top: 20px;
10 | float: right;
11 | }
12 |
13 | Image {
14 | object-position: center;
15 | background-color: green;
16 | }
17 |
18 | h2 {
19 | /* border: 2px solid green; */
20 | padding: 0 0 0 5px;
21 | margin: 0;
22 | font-family: 'Roboto', sans-serif;
23 | font-weight: 700;
24 | color: #d53f8c;
25 | }
26 |
27 | .updateDelete {
28 | border: 2px solid #d53f8c;
29 | border-radius: 15%;
30 | float: right;
31 | background-color: #d53f8c;
32 | color: white;
33 | margin: 10px;
34 | padding: 5px;
35 | }
36 |
37 | table,
38 | th,
39 | td {
40 | border: 1px solid black;
41 | padding: 15px;
42 | border-collapse: collapse;
43 | }
44 |
45 | .tableHeader {
46 | padding: 15px;
47 | }
48 |
49 | th {
50 | text-align: left;
51 | }
52 |
53 | table {
54 | border-spacing: 5px;
55 | background-color: white;
56 | }
57 |
58 |
59 | .renderSchemaBtn {
60 | border: 2px solid #d53f8c;
61 | border-radius: 15%;
62 | background-color: #d53f8c;
63 | color: white;
64 | margin: 10px;
65 | padding: 5px;
66 | font-style: bold;
67 | }
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | MiraQL
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/main.js:
--------------------------------------------------------------------------------
1 | const { app, BrowserWindow } = require('electron');
2 | const path = require('path');
3 | // const log = require('electron-log');
4 | const request = require('request');
5 | // const contextMenu = require('electron-context-menu');
6 | const express = require('express'); //your express app
7 | // require('../client/components/App.jsx')
8 |
9 | // contextMenu({
10 | // prepend: (defaultActions, params, browserWindow) => [
11 | // {
12 | // label: 'Rainbow',
13 | // // Only show it when right-clicking images
14 | // visible: params.mediaType === 'image',
15 | // },
16 | // {
17 | // label: 'Search Google for “{selection}”',
18 | // // Only show it when right-clicking text
19 | // visible: params.selectionText.trim().length > 0,
20 | // click: () => {
21 | // shell.openExternal(
22 | // `https://google.com/search?q=${encodeURIComponent(
23 | // params.selectionText
24 | // )}`
25 | // );
26 | // },
27 | // },
28 | // ],
29 | // });
30 |
31 | let mainWindow;
32 | (async () => {
33 | await app.whenReady();
34 |
35 | mainWindow = new BrowserWindow(webPreferences, {
36 | spellcheck: true,
37 | });
38 | })();
39 |
40 | function createWindow() {
41 | const win = new BrowserWindow({
42 | width: 800,
43 | height: 600,
44 | webPreferences: {
45 | nodeIntegration: true,
46 | },
47 | });
48 |
49 | win.loadFile('./index.html');
50 | }
51 |
52 | // app.whenReady().then(createWindow);
53 |
54 | app.on('ready', function () {
55 | express();
56 | mainWindow = new BrowserWindow({
57 | width: 1280,
58 | height: 720,
59 | autoHideMenuBar: true,
60 | useContentSize: true,
61 | resizable: true,
62 | });
63 | mainWindow.loadURL('http://localhost:3000/');
64 | mainWindow.focus();
65 | });
66 |
67 | app.on('window-all-closed', () => {
68 | if (process.platform !== 'darwin') {
69 | app.quit();
70 | }
71 | });
72 |
73 | app.on('activate', () => {
74 | if (BrowserWindow.getAllWindows().length === 0) {
75 | createWindow();
76 | }
77 | });
78 |
79 | app.on(
80 | 'window-all-closed',
81 | () => process.platform !== 'darwin' && app.quit() // "darwin" targets macOS only.
82 | );
83 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "MiraQL",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "main.js",
6 | "scripts": {
7 | "comment-2": "NODE_ENV=production nodemon server/server.js",
8 | "start": "nodemon server/server.js & electron .",
9 | "package-mac": "electron-packager . --overwrite --platform=darwin --arch=x64 --icon=assets/icons/mac/icon.icns --prune=true --out=release-builds",
10 | "package-win": "electron-packager . electron-tutorial-app --overwrite --asar=true --platform=win32 --arch=ia32 --icon=assets/icons/win/icon.ico --prune=true --out=release-builds --version-string.CompanyName=CE --version-string.FileDescription=CE --version-string.ProductName=\"MiraQLThanks icons/png/1024x1024.png --prune=true --out=release-builds",
11 | "build": "NODE_ENV=production webpack",
12 | "dev": "NODE_ENV=development nodemon server/server.js & webpack-dev-server --open",
13 | "test": "jest --watchAll"
14 | },
15 | "keywords": [],
16 | "author": "",
17 | "license": "ISC",
18 | "devDependencies": {
19 | "@babel/core": "^7.12.3",
20 | "@babel/plugin-transform-runtime": "^7.12.1",
21 | "@babel/preset-env": "^7.12.1",
22 | "@babel/preset-react": "^7.12.5",
23 | "@babel/runtime": "^7.12.5",
24 | "@webpack-cli/serve": "^1.1.0",
25 | "babel-eslint": "^10.1.0",
26 | "babel-loader": "^8.2.1",
27 | "css-loader": "^5.0.1",
28 | "electron": "^11.0.4",
29 | "electron-context-menu": "^2.4.0",
30 | "electron-log": "^4.3.1",
31 | "electron-unhandled": "^3.0.2",
32 | "enzyme": "^3.11.0",
33 | "eslint": "^7.13.0",
34 | "eslint-config-airbnb": "^18.2.1",
35 | "eslint-config-prettier": "^6.15.0",
36 | "eslint-plugin-import": "^2.22.1",
37 | "eslint-plugin-jsx-a11y": "^6.4.1",
38 | "eslint-plugin-prettier": "^3.1.4",
39 | "eslint-plugin-react": "^7.21.5",
40 | "eslint-plugin-react-hooks": "^4.2.0",
41 | "http-proxy-middleware": "^1.0.6",
42 | "jest": "^26.6.3",
43 | "nodemon": "^1.18.9",
44 | "prettier": "^2.1.2",
45 | "sass-loader": "^10.1.0",
46 | "style-loader": "^2.0.0",
47 | "supertest": "^6.0.1",
48 | "webpack": "^4.44.2",
49 | "webpack-cli": "^3.2.3",
50 | "webpack-dev-server": "^3.11.0"
51 | },
52 | "dependencies": {
53 | "@chakra-ui/react": "^1.0.3",
54 | "@emotion/react": "^11.1.2",
55 | "@emotion/styled": "^11.0.0",
56 | "bcryptjs": "^2.4.3",
57 | "body-parser": "^1.19.0",
58 | "chart.js": "^2.9.4",
59 | "codemirror": "^5.58.3",
60 | "codemirror-graphql": "^0.13.0",
61 | "cookie": "^0.4.1",
62 | "cookie-parser": "^1.4.5",
63 | "cookie-session": "^1.4.0",
64 | "d3": "^6.3.1",
65 | "electron-packager": "^15.2.0",
66 | "express": "^4.12.3",
67 | "framer-motion": "^2.9.5",
68 | "graphql": "^15.4.0",
69 | "node-fetch": "^2.6.1",
70 | "passport": "^0.4.1",
71 | "passport-facebook": "^3.0.0",
72 | "passport-google-oauth20": "^2.0.0",
73 | "pg": "^8.5.1",
74 | "react": "^16.2.0",
75 | "react-chartjs-2": "^2.11.1",
76 | "react-codemirror2": "^7.2.1",
77 | "react-cookie": "^4.0.3",
78 | "react-d3-tree": "^1.16.1",
79 | "react-dom": "^16.3.1",
80 | "react-hook-form": "^6.11.5",
81 | "react-icons": "^4.1.0",
82 | "request": "^2.88.2",
83 | "sass": "^1.29.0",
84 | "sass-loader": "^10.1.0",
85 | "style-loader": "^2.0.0",
86 | "webpack": "^4.44.2"
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/server/controllers/queryController.js:
--------------------------------------------------------------------------------
1 | const db = require("../models/queryModels");
2 |
3 | const queryController = {};
4 |
5 | // GET request to get all db info for schema_list and fields tables
6 | queryController.getAllSchemaList = (req, res, next) => {
7 | // schema_name, field_name, field_type
8 | const { schema_name, field_name, field_type } = req.body;
9 | // const item = `SELECT * FROM schema_list, fields WHERE schema_list`
10 | const text = `SELECT * FROM "public"."schema_list" LIMIT 100`;
11 | db.query(text)
12 | .then((data) => {
13 | // console.log('getAllSchemaList is: ', data);
14 | res.status(200).json(data.rows);
15 | next();
16 | })
17 | .catch((err) => {
18 | console.log(err);
19 | });
20 | };
21 |
22 | queryController.getAllFields = (req, res, next) => {
23 | // schema_name, field_name, field_type
24 | const { id, field_name, field_type } = req.body;
25 | // const item = `SELECT * FROM schema_list, fields WHERE schema_list`
26 | const text = `SELECT * FROM "public"."fields" LIMIT 100`;
27 | db.query(text)
28 | .then((data) => {
29 | // console.log('getAllFields is: ', data);
30 | res.status(200).json(data.rows);
31 | next();
32 | })
33 | .catch((err) => {
34 | console.log(err);
35 | });
36 | };
37 |
38 |
39 | queryController.getEverything = (req, res, next) => {
40 | const queryStr = 'SELECT schema_name, field_name, field_type FROM schema_list RIGHT JOIN fields ON schema_list.id=fields.schema_list_id'
41 |
42 | db.query(queryStr)
43 | .then((data) => {
44 | res.json(data.rows)
45 | })
46 | }
47 | // POST request: creates a new row in the schema_list table
48 | // req.body would send the 'schema_name'
49 | queryController.addRowSchemaList = (req, res, next) => {
50 | const { ourTableName } = req.body;
51 | // console.log(schema_name, 'schema_name')
52 | const text = `INSERT INTO schema_list (schema_name) VALUES ($1)`;
53 |
54 | db.query(text, [ourTableName])
55 | .then((data) => {
56 | // console.log(data, 'data')
57 | res.status(200).send("schema_name added to schema_list table");
58 | next();
59 | })
60 | .catch((err) => {
61 | console.log(err);
62 | });
63 | };
64 |
65 | // user adds many rows to fields table - 'post' request
66 | queryController.addManyFieldsRows = (req, res, next) => {
67 | const arr = req.body;
68 | // we will get an array of objects with multiple rows that will need to be added
69 | let addToSchema = "";
70 | // `SELECT id FROM schema_list WHERE schema_name = ($1)demo-schema`
71 | const getIdString = `SELECT id FROM schema_list WHERE schema_name = $1`;
72 | console.log("this is the reqest body", req.body);
73 | // $1 is going to point to [req.body.tableName]
74 | const getIdParam = [req.body.ourTableName];
75 | console.log("getIdParam", getIdParam);
76 | // once we query this, save it to a variable, and pass that variable to our INSERT statement as the foreign key
77 | let idNumber = 0;
78 | db.query(getIdString, getIdParam).then((data) => {
79 | console.log("we made it in here!!!!!");
80 | console.log("this is our data", data);
81 | idNumber = data.rows[0].id;
82 | console.log("inside query", idNumber);
83 |
84 | const inputsArray = req.body.inputs;
85 | console.log("inputsarray", inputsArray);
86 |
87 | for (let i = 0; i < inputsArray.length; i++) {
88 | const userFieldName = inputsArray[i].fieldName;
89 | const userFieldType = inputsArray[i].fieldType;
90 | const userDefaultValue = inputsArray[i].defaultValue;
91 | const userPrimaryKey = inputsArray[i].primaryKey;
92 | const userUnique = inputsArray[i].unique;
93 | const userRequired = inputsArray[i].required;
94 | const userQueryable = inputsArray[i].queryable;
95 | const userTableRelate = inputsArray[i].tableRelationship;
96 | const userFieldRelate = inputsArray[i].fieldRelationship;
97 | const userTypeRelate = inputsArray[i].typeRelationship;
98 |
99 | console.log(
100 | "userFieldName: ",
101 | userFieldName,
102 | "userFieldType: ",
103 | userFieldType,
104 | "userDefaultValue:",
105 | userDefaultValue,
106 | "userPrimaryKey: ",
107 | userPrimaryKey,
108 | "userUnique: ",
109 | userUnique,
110 | "userRequired",
111 | userRequired,
112 | "userQueryable",
113 | userQueryable
114 | );
115 |
116 | let concatStr =
117 | "(" +
118 | `'${userFieldName}'` +
119 | "," +
120 | `'${userFieldType}'` +
121 | "," +
122 | `'${userDefaultValue}'` +
123 | "," +
124 | `'${userPrimaryKey}'` +
125 | "," +
126 | `'${userUnique}'` +
127 | "," +
128 | `'${userRequired}'` +
129 | "," +
130 | `'${userQueryable}'` +
131 | "," +
132 | `'${userTableRelate}'` +
133 | "," +
134 | `'${userFieldRelate}'` +
135 | "," +
136 | `'${userTypeRelate}'` +
137 | "," +
138 | `${idNumber}` +
139 | "),";
140 | // console.log('this is the concatenated string', concatStr);
141 | addToSchema = addToSchema + concatStr;
142 | // console.log('this is our addToSchema query after concat:', addToSchema);
143 | }
144 | let oldDbQuery = `INSERT INTO fields (field_name, field_type, default_value, primary_key, unique_bool, required_bool, queryable, table_relationship, field_relationship, type_relationship, schema_list_id) VALUES `;
145 |
146 | let newDB = oldDbQuery + addToSchema;
147 | // console.log(newDB, 'newDB');
148 | let lengthSlice = newDB.length - 1;
149 | // console.log(lengthSlice, 'lengthSlice');
150 | let newDbQuery = newDB.slice(0, lengthSlice);
151 | // console.log(newDbQuery, 'newDbQuery');
152 | console.log("this is the new DB query, pls work! ", newDbQuery);
153 |
154 | db.query(newDbQuery)
155 | .then((data) => {
156 | // console.log('data ln 120: ', data);
157 | // console.log(`this is the data from the user's rows table: `, data.rows);
158 | res.status(200).send(`Rows added to fields table`);
159 | next();
160 | // res.locals.userCart = data.rows;
161 | })
162 | .catch((err) => {
163 | console.log("ERROR: No rows added.", err);
164 | });
165 | });
166 |
167 | // data.rows[0].id
168 | };
169 |
170 | // DELETE request: to delete a row from the fields table
171 | queryController.deleteFieldsRow = (req, res, next) => {
172 | // query fields table based on id and delete if item exists
173 | const id = parseInt(req.params.id);
174 | const text = `DELETE FROM fields WHERE id = $1`;
175 | // send our query over to the db passing in the query and id
176 | db.query(text, [id])
177 | .then(() => {
178 | res.status(200).send(`fields row deleted with ID: ${id}`);
179 | next();
180 | })
181 | .catch((err) => {
182 | console.log(err);
183 | });
184 | };
185 |
186 | // DELETE request: to delete a row in the schema_list
187 | queryController.deleteSchemaRow = (req, res, next) => {
188 | const id = parseInt(req.params.id);
189 | const text = `DELETE FROM schema_list WHERE id = $1`;
190 | db.query(text, [id])
191 | .then(() => {
192 | res.status(200).send(`schema_list row deleted with ID: ${id}`);
193 | next();
194 | })
195 | .catch((err) => {
196 | console.log(err);
197 | });
198 | };
199 |
200 | /* STRETCH FEATURES*/
201 | // POST request that adds a new row to fields table w/ existing schema_name
202 | queryController.addFieldsRow = (req, res, next) => {};
203 |
204 | // PATCH request to edit/update existing fields in an existing fields/schema table
205 | queryController.editFieldsRow = (req, res, next) => {};
206 |
207 | module.exports = queryController;
208 |
--------------------------------------------------------------------------------
/server/createTable.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE schema_list (
2 | id SERIAL PRIMARY KEY,
3 | table_name VARCHAR(50)
4 | );
5 |
6 | CREATE TABLE fields (
7 | id SERIAL PRIMARY KEY,
8 | field_name VARCHAR(140) NOT NULL,
9 | field_type VARCHAR(140) NOT NULL,
10 | default_value VARCHAR(140),
11 | primary_key BOOLEAN NOT NULL,
12 | unique_bool BOOLEAN NOT NULL,
13 | required_bool BOOLEAN NOT NULL,
14 | queryable BOOLEAN NOT NULL,
15 | table_relationship VARCHAR(140) NOT NULL,
16 | field_relationship VARCHAR(140) NOT NULL,
17 | type_relationship VARCHAR(140),
18 | created_on TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
19 | schema_list_id INT FOREIGN KEY
20 | );
--------------------------------------------------------------------------------
/server/models/queryModels.js:
--------------------------------------------------------------------------------
1 | const { Pool } = require('pg');
2 |
3 | const PG_URI =
4 | // this will need to change to something local
5 | 'postgres://fijaxqtj:WO5xTwVyLm45kWLk0SAhi2XBYEf_4az-@suleiman.db.elephantsql.com:5432/fijaxqtj';
6 |
7 | // create a new pool here using the connection string above
8 | const pool = new Pool({
9 | connectionString: PG_URI,
10 | });
11 |
12 | module.exports = {
13 | query: (text, params, callback) => {
14 | // console.log('executed query', text);
15 | return pool.query(text, params, callback);
16 | },
17 | };
18 |
--------------------------------------------------------------------------------
/server/server.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const app = express();
3 | const path = require("path");
4 | const bodyParser = require("body-parser");
5 | const queryController = require("./controllers/queryController.js");
6 |
7 | // handle parsing request body and cookies
8 | app.use(bodyParser.json());
9 | app.use(bodyParser.urlencoded({ extended: true }));
10 |
11 | // if (process.env.NODE_ENV === "production") {
12 | app.use("/build", express.static(path.join(__dirname, "../build")));
13 | // serve index.html on the route '/'
14 | app.get("/", (req, res) => {
15 | res.sendFile(path.join(__dirname, "../index.html"));
16 | });
17 | // }
18 |
19 | // ROUTE HANDLERS FOR MIDDLEWARE CRUD FUNCTIONALITY:
20 | // GET request to /schema and /fields to get all db info
21 | app.get("/schema", queryController.getAllSchemaList);
22 | app.get("/field", queryController.getAllFields);
23 | app.get("/everything", queryController.getEverything)
24 | // Initial POST request to add a new row to our schema_list and add all the field rows to the fields table
25 | app.post(
26 | "/schema",
27 | queryController.addRowSchemaList,
28 | queryController.addManyFieldsRows
29 | );
30 | // DELETE request to delete row(s) by ID(s)
31 | app.delete("/field/:id", queryController.deleteFieldsRow);
32 | // // DELETE request to delete a row in the schema_list table
33 | app.delete("/schema/:id", queryController.deleteSchemaRow);
34 |
35 | /* STRETCH FEATURES:
36 | additional endpoints to add/edit rows in the fields table */
37 | // POST request to add row(s) to field table where table_name already exists
38 | app.post("/schema/:id", queryController.addFieldsRow);
39 | // PATCH request to edit the fields in an existing row in the fields table
40 | app.patch("/field/:id", queryController.editFieldsRow);
41 |
42 | // catch-all route handler for any requests to an unknown route
43 | app.use("*", (req, res) => res.sendStatus(404));
44 |
45 | // // global error handler
46 | app.use((err, req, res, next) => {
47 | console.log(err);
48 | res.status(500).send("Internal Server Error");
49 | });
50 |
51 | app.listen(3000, () => {
52 | console.log("Listening on port 3000!");
53 | });
54 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const webpack = require("webpack");
2 | const path = require("path");
3 |
4 | const config = {
5 | entry: "./client/index.js",
6 | output: {
7 | path: path.resolve(`${__dirname}/build`),
8 | filename: "bundle.js",
9 | },
10 | mode: process.env.NODE_ENV,
11 | devServer: {
12 | hot: true,
13 | proxy: {
14 | "/schema": "http://localhost:3000",
15 | "/field": "http://localhost:3000",
16 | "/everything": "http://localhost:3000"
17 | },
18 | publicPath: "/build/",
19 | },
20 | devtool: "eval-cheap-source-map",
21 | module: {
22 | rules: [
23 | {
24 | test: /\.jsx?/,
25 | exclude: /(node_modules)/,
26 | use: {
27 | loader: "babel-loader",
28 | options: {
29 | presets: ["@babel/preset-env", "@babel/preset-react"],
30 | },
31 | },
32 | },
33 | {
34 | test: /\.css$/,
35 | use: ["style-loader", "css-loader"],
36 | },
37 | ],
38 | },
39 | };
40 |
41 | module.exports = config;
42 |
--------------------------------------------------------------------------------