├── .DS_Store
├── .gitignore
├── LICENSE
├── README.md
├── __tests__
├── backend
│ └── route.test.js
└── frontend
│ └── jest.test.js
├── client
├── .DS_Store
├── components
│ ├── .DS_Store
│ ├── App.js
│ ├── Header.js
│ ├── Metric.js
│ ├── QueryHistory.js
│ ├── QueryInput.js
│ ├── Result.js
│ ├── Schema.js
│ ├── SchemaVisual.js
│ ├── SideBar.js
│ └── URILink.js
└── context
│ ├── global-context.js
│ └── global-reducer.js
├── jest.config.js
├── next.config.js
├── package-lock.json
├── package.json
├── pages
├── _app.js
└── index.js
├── postcss.config.js
├── public
├── .DS_Store
├── demo.gif
├── gif-4.gif
├── graph.gif
├── metricql-transparent.png
├── metricql.png
├── query-history.gif
├── schema.gif
└── visualizer.gif
├── server
├── controllers
│ ├── postgreSQLController.js
│ └── schemaFunc.js
├── generator
│ ├── generateTypes.js
│ ├── generatorResolver.js
│ ├── resolverFunc.js
│ ├── schema.js
│ ├── testPSQL.js
│ └── typesFunc.js
├── graphQLServer
│ └── schema.js
├── query
│ └── tables.sql
├── router.js
└── server.js
├── styles
├── SideBar.module.css
├── URILink.module.css
└── globals.css
└── tailwind.config.js
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/MetriQL/a94ace09255761003f81003d420b3640be4649c0/.DS_Store
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .env
3 | .next
4 | .DS_store
5 | .next
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 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 | # MetricQL
2 | ## A GraphQL Migration Tool
3 |
4 | ### Table of Contents
5 |
6 | About MetricQL
7 | Getting Started
8 | Contributors
9 | To Do
10 | Usage
11 | Built With
12 | Developers
13 | Acknowledgments
14 | License
15 |
16 |
17 | ### About MetricQL
18 | MetricQL is a migration assistance tool that facilitates the transition from REST to GraphQL. Generate, customize, and export GraphQL schemas from existing PostgreSQL databases and visualize performance metrics.
19 |
20 | Transform existing PostgresQL databases into GraphQL code, including query resolvers and types.
21 | Visualize PostgresQL entity-relational diagrams to view and analyze database relationships.
22 | Edit, test and compare queries with the code playground and query history panel.
23 | Analyze query response times to optimize GraphQL performance.
24 |
25 |
26 |
27 | Accelerated by OS Labs.
28 |
29 | ### Getting Started is Easy!
30 | You can access our tool's URL input in-browser, or download the application for access to all additional features. Initial instructions for both methods are detailed below:
31 |
32 | #### In-Browser ####
33 | Visit metricql.com for easy access. Navigate to the ‘Generate Schema’ page to access our URL input form. Copy a relational database link (i.e., PostgresQL) and paste into the input box. After clicking submit, you will see your GraphQL types on the left, and your resolvers on the right. For access to the full host of MetricQL’s features, fork & clone our repo for an easy spin-up.
34 |
35 | #### Download Instructions ####
36 | * Clone our repo onto your personal machine
37 | * Once the file has been cloned and opened, run 'npm install' in your CLI
38 | * After installation, run 'npm build'
39 | * If you run into any issues with the option to install with "--legacy-peer-deps", do so
40 | * Once the build is complete, use the 'npm run dev' command to spin up the application, accessing it through localhost:3000
41 |
42 |
43 | ### Database Connection
44 |
45 | Connect to MetricQL by inputting an existing PostgreSQL URI to auto-populate data, or use our sample database to test our utilities.
46 |
47 | 
48 |
49 | ### Accessing Customized GraphQL Code*
50 |
51 | Input your specific query under Query Input (code playground) and click Submit to generate customized GraphQL code based on your needs.
52 |
53 | 
54 |
55 | Access query history via the left panel.
56 |
57 | 
58 |
59 | Export or highlight and copy the auto-generated GraphQL code that displays beneath the performance graph.
60 | Visit the sidebar on the left to view GraphQL Types and Resolvers.
61 | Click on Export or highlight and copy/paste.
62 |
63 | 
64 |
65 | ### ER Visualizer*
66 |
67 | The PostgreSQL entity-relational diagram is also accessible via the sidebar on the left, simply click on “View Visualizer” to manipulate and analyze relationships.
68 |
69 | 
70 |
71 | ### Performance*
72 |
73 | Easily view performance metrics on the top right panel and start analyzing and comparing the efficiency of your GraphQL queries.
74 |
75 | 
76 |
77 | ### Contributions
78 | MetricQL greatly welcomes any contributions from the open source community! Please click here to view our contribution FAQ page. A big thank you for your interest and passion in contributing to MetricQL!
79 |
80 |
81 | ### Contributing to MetricQL
82 |
83 | The MetricQL team would like to thank you for your interest in helping to maintain and improve our app!
84 | Please follow these steps for a seamless contribution experience:
85 | There are 3 npm actions you need to run before working:
86 | npm install
87 | npm run build
88 | npm run dev (to launch website)
89 | Reporting Bugs and Adding New Features
90 | All code changes happen through Github Pull Requests and we actively welcome them! To submit your pull request, follow the steps below:
91 |
92 |
93 | ### Fork the Project
94 |
95 | Create your Feature Branch from dev (git checkout -b feature/NewFeature)
96 | Commit your Changes (git commit -m 'Add some NewFeature')
97 | Push to the Branch on your Fork (git push origin feature/NewFeature)
98 | Open a Pull Request from the Branch on your Fork to the dev branch on the MetricQL Dev Branch
99 | We will review Pull Requests on an ongoing basis.
100 |
101 | ### Pull Requests
102 |
103 | Fork the repo and create your branch from dev.
104 | If you've added code that should be tested, add tests.
105 | If you've changed APIs, please update the documentation.
106 | Ensure the tests pass by running npm run tests.
107 | Make sure your code lints.
108 | Submit a pull request.
109 | To-Dos
110 | Non-Relational Database Integration to extend users' options by allowing non-relational DB imports
111 | TypeScript code refactoring
112 | Add support for all SQL data types
113 |
114 | ### Built With
115 |
116 | MetricQL was built using the following frameworks and libraries:
117 |
118 | Next.js
119 | React
120 | React ContextAPI
121 | GraphQL
122 | Node.js
123 | Express
124 | PostgreSQL
125 | Jest
126 | Supertest
127 |
128 |
129 | ### Contributors
130 |
131 | Rehema Armorer
132 |
133 | Diana Li
134 |
135 | Raymond Huang
136 |
137 | Eric Rodgers
138 |
139 | Alfonso Zamarripa
140 |
141 | ### Acknowledgments
142 |
143 | A big thank you to the tech accelerator Open Source Labs for their continued support and sponsorship throughout this whole process.
144 |
145 | This project is licensed under the MIT License - see the License.MD file for details
146 |
--------------------------------------------------------------------------------
/__tests__/backend/route.test.js:
--------------------------------------------------------------------------------
1 | const request = require('supertest');
2 |
3 | const server = 'http://localhost:3001'
4 |
5 | describe('Route integration', () => {
6 | describe('/schema', () => {
7 | describe('POST', () => {
8 | it('responds with 200 status and application/json content type', () => {
9 | return request(server)
10 | .post('/schema')
11 | .expect('Content-Type', /application\/json/)
12 | .expect(200)
13 | })
14 | })
15 | describe('GET', () => {
16 | it('responds with 200 status and text/html content type', () => {
17 | return request(server)
18 | .get('/')
19 | .expect('Content-Type', /text\/html/)
20 | .expect(200)
21 | })
22 | })
23 | })
24 | })
25 |
--------------------------------------------------------------------------------
/__tests__/frontend/jest.test.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { render, fireEvent, screen } from '@testing-library/react';
3 | import { rest } from 'msw';
4 | import { setupServer } from 'msw/node';
5 |
6 | //overall app
7 | import App from '../../client/components/App';
8 | //Splash page
9 | import splashPage from '../../pages/index.js';
10 | import Nav from '../../client/components/Nav.js';
11 | import MainFeature from '../../client/components/styles/MainFeature.js'
12 | import SecondFeature from '../../client/components/styles/SecondFeature.js'
13 | import ThirdFeature from '../../client/components/styles/ThirdFeature.js'
14 | import Team from '../../client/components/styles/Team.js'
15 |
16 | //Tool page
17 | import toolPage from '../../pages/main.js'
18 |
19 |
20 | //establish mock server
21 | const server = setupServer(
22 | rest.get('/', (req, res, ctx) => {
23 | return res(ctx.status(200), ctx.json({ greeting: 'hello' }));
24 | })
25 | )
26 |
27 | //create variables for mock contexts
28 | let realContext;
29 | let fakeContext;
30 |
31 | //establish API mock before any and all tests
32 | beforeAll(() => server.listen());
33 | //set up mock context before each test
34 | beforeEach(() => {
35 | realContext = React.useContext;
36 | fakeContext = React.useContext = jest.fn()
37 | })
38 | //reset handlers that are declared during testing and reset real context after reach tet
39 | afterEach(() => {
40 | React.useContext = realContext;
41 | server.resetHandlers();
42 | })
43 |
44 | //close server at end of all tests
45 | afterAll(() => server.close())
46 |
47 | xdescribe('Renders Site', () => {
48 | describe('renders homepage', () => {
49 | test('renders nav bar', async () => {
50 | const { getByText } = render( )
51 | const element = await getByText(/Download/i);
52 | expect(element).toBeInTheDocument();
53 | });
54 |
55 | test ('renders MainFeature', async () => {
56 | const { findByText } = render( )
57 | const element = await findByText(/MetricQL/);
58 | expect(element).toBeInTheDocument();
59 | })
60 | test ('renders SecondFeature', async () => {
61 | const { findByText } = render( )
62 | const element = await findByText(/website/);
63 | expect(element).toBeInTheDocument();
64 | })
65 | test ('renders ThirdFeature', async () => {
66 | const { findByText } = render( )
67 | const element = await findByText(/Schema/);
68 | expect(element).toBeInTheDocument();
69 | })
70 | test ('renders Team', async () => {
71 | const { findByText } = render( )
72 | const element = await findByText(/Alfonso/);
73 | expect(element).toBeInTheDocument();
74 | })
75 | })
76 |
77 | describe('renders tool page', () => {
78 | test('modal shows a submit button', () => {
79 | const handleClose = jest.fn()
80 | const {getByText} = render(
81 |
82 | test
83 |
84 | )
85 | expect(getByText('test')).toBeTruthy();
86 | fireEvent.click(getByText(/close/i))
87 | expect(handleClose).toHaveBeenCalledTimes(1)
88 | })
89 | })
90 | })
--------------------------------------------------------------------------------
/client/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/MetriQL/a94ace09255761003f81003d420b3640be4649c0/client/.DS_Store
--------------------------------------------------------------------------------
/client/components/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/MetriQL/a94ace09255761003f81003d420b3640be4649c0/client/components/.DS_Store
--------------------------------------------------------------------------------
/client/components/App.js:
--------------------------------------------------------------------------------
1 | function App({ children }) {
2 |
3 | return (
4 | <>
5 |
6 |
7 | {children}
8 |
9 |
10 | >
11 | )
12 | }
13 |
14 | export default App
--------------------------------------------------------------------------------
/client/components/Header.js:
--------------------------------------------------------------------------------
1 |
2 | const Header = () => {
3 | return (
4 |
5 |
17 |
18 | )
19 | }
20 |
21 | export default Header;
--------------------------------------------------------------------------------
/client/components/Metric.js:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react';
2 | import { GraphContext } from '../context/global-context';
3 |
4 | import {
5 | Chart as ChartJS,
6 | LineElement,
7 | PointElement,
8 | CategoryScale,
9 | LinearScale,
10 | Title,
11 | Tooltip,
12 | Legend,
13 | } from 'chart.js';
14 |
15 | //import line (to be rendered) from react-chartjs-2
16 | import { Chart, Line } from 'react-chartjs-2';
17 |
18 | //register plugins to be applied globally (to all charts)
19 | ChartJS.register(
20 | CategoryScale,
21 | LinearScale,
22 | PointElement,
23 | LineElement,
24 | Title,
25 | Tooltip,
26 | Legend
27 | );
28 |
29 |
30 |
31 | //what is attached to main page rendering
32 | function Metric() {
33 | const { speedState } = useContext(GraphContext)
34 |
35 | const labels = speedState.speed.map((el, index) => { return index + 1 });
36 |
37 |
38 | const options = {
39 | responsive: true,
40 | plugins: {
41 | legend: {
42 | display: false,
43 | },
44 | title: {
45 | display: true,
46 | text: 'Speed per Fetch'
47 | },
48 | toolTip: {
49 | display: true,
50 | }
51 | },
52 | scales: {
53 | yAxes: [
54 | {
55 | gridLines: {
56 | zeroLineColor: "white"
57 | }
58 | }
59 | ],
60 | xAxes: [
61 | {
62 | gridLines: {
63 | zeroLinecolor: "white"
64 | }
65 | }
66 | ]
67 | }
68 | }
69 |
70 | //create data parameters object
71 | const data = {
72 | labels,
73 | datasets: [
74 | {
75 | label: 'ms',
76 | lineTension: 0.40,
77 | data: speedState.speed,
78 | borderColor: 'rgb(258, 34, 12)',
79 | backgroundColor: 'rgba(253, 99, 132, 0.5)',
80 | }
81 | ]
82 | }
83 | return (
84 |
85 |
86 |
87 | )
88 | }
89 |
90 | export default Metric;
--------------------------------------------------------------------------------
/client/components/QueryHistory.js:
--------------------------------------------------------------------------------
1 | import { useState, useContext } from "react";
2 | import CodeMirror from '@uiw/react-codemirror';
3 | import { javascript } from '@codemirror/lang-javascript';
4 | import { styled } from '@mui/material/styles';
5 | import ArrowForwardIosSharpIcon from '@mui/icons-material/ArrowForwardIosSharp';
6 | import MuiAccordion from '@mui/material/Accordion';
7 | import MuiAccordionSummary from '@mui/material/AccordionSummary';
8 | import MuiAccordionDetails from '@mui/material/AccordionDetails';
9 | import Typography from '@mui/material/Typography';
10 |
11 | import { HistoryContext } from '../context/global-context'
12 |
13 | const QueryHistory = () => {
14 |
15 | const { codeState, displayState, displayDispatch, speedState } = useContext(HistoryContext)
16 |
17 | const handleChange = (panel) => (event, newExpanded) => {
18 | displayDispatch({
19 | type: 'UPDATE_HISTORY_DISPLAY',
20 | payload: newExpanded ? panel : false
21 | });
22 | }
23 |
24 | const Accordion = styled((props) => (
25 |
26 | ))(({ theme }) => ({
27 | border: `1px solid ${theme.palette.divider}`,
28 | '&:not(:last-child)': {
29 | borderBottom: 0,
30 | },
31 | '&:before': {
32 | display: 'none',
33 | },
34 | }));
35 |
36 | const AccordionSummary = styled((props) => (
37 | }
39 | {...props}
40 | style={{
41 | backgroundColor: "#5304EE"
42 | }}
43 | />
44 | ))(({ theme }) => ({
45 | backgroundColor: '#5304EE',
46 | flexDirection: 'row-reverse',
47 | '& .MuiAccordionSummary-expandIconWrapper.Mui-expanded': {
48 | transform: 'rotate(90deg)',
49 | },
50 | '& .MuiAccordionSummary-content': {
51 | marginLeft: theme.spacing(1),
52 | },
53 | }));
54 |
55 | const AccordionDetails = styled(MuiAccordionDetails)(({ theme }) => ({
56 | padding: theme.spacing(2),
57 | borderTop: '1px solid rgba(0, 0, 0, .125)',
58 | }));
59 |
60 |
61 | const query = codeState.query.map((query, index) => {
62 | return (
63 |
64 |
65 |
66 | Query {index + 1} Speed : {speedState.speed[index].toFixed()}ms
67 |
68 |
69 |
70 |
76 |
77 |
78 |
79 |
80 | )
81 | })
82 | return (
83 |
84 | {query}
85 |
86 | )
87 | }
88 |
89 | export default QueryHistory;
--------------------------------------------------------------------------------
/client/components/QueryInput.js:
--------------------------------------------------------------------------------
1 | import { useState, useContext } from 'react'
2 | import CodeMirror, { placeholder } from '@uiw/react-codemirror';
3 | import { javascript } from '@codemirror/lang-javascript';
4 | import Button from '@mui/material/Button';
5 | import Stack from '@mui/material/Stack';
6 |
7 | import { QueryContext } from '../context/global-context';
8 |
9 |
10 | function QueryInput() {
11 |
12 | const { codeState, codeDispatch, speedUpdate, speedState } = useContext(QueryContext);
13 |
14 | const queryChangeHandler = (value) => {
15 | codeDispatch({
16 | type: 'UPDATE_QUERY_INPUT',
17 | payload: {
18 | queryInput: value
19 | }
20 | });
21 | };
22 |
23 | const resetHandler = () => {
24 | codeDispatch({
25 | type: 'UPDATE_QUERY_INPUT',
26 | payload: {
27 | queryInput: ''
28 | }
29 | })
30 | };
31 |
32 | const submitHandler = async () => {
33 |
34 | const requestOptions = {
35 | method: 'POST',
36 | headers: { 'Content-Type': 'application/json' },
37 | body: JSON.stringify({ query: `${codeState.queryInput}` })
38 | };
39 |
40 | if (speedState.firstQuery) {
41 | const firstRun = await fetch("https://mql-back.herokuapp.com/graphql", requestOptions)
42 |
43 | speedUpdate({
44 | type: 'UPDATE_FIRST_QUERY',
45 | payload: false
46 | });
47 |
48 | };
49 |
50 | const start = performance.now();
51 | const result = await fetch("https://mql-back.herokuapp.com/graphql", requestOptions) //create toggle between /schema and schema-user
52 | const jsonData = await result.json();
53 | const cleanResponse = JSON.stringify(jsonData, null, 2)
54 | const end = performance.now();
55 |
56 | const time = end - start;
57 |
58 | speedUpdate({
59 | type: 'UPDATE_SPEED',
60 | payload: {
61 | speed: [...speedState.speed, time]
62 | }
63 | });
64 |
65 | codeDispatch({
66 | type: 'UPDATE_RESULT',
67 | payload: {
68 | query: codeState.queryInput,
69 | result: cleanResponse
70 | }
71 | });
72 |
73 | }
74 |
75 | return (
76 |
77 |
78 |
Query Input
79 |
80 |
81 | Submit
91 |
92 | Reset
97 |
98 |
99 |
100 |
{
107 | queryChangeHandler(e);
108 | }}
109 | />
110 |
111 |
112 | )
113 | }
114 |
115 | export default QueryInput;
--------------------------------------------------------------------------------
/client/components/Result.js:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react';
2 | import CodeMirror from '@uiw/react-codemirror';
3 | import { javascript } from '@codemirror/lang-javascript';
4 | import { dark } from '@material-ui/core/styles/createPalette';
5 |
6 | import { SchemaContext } from '../context/global-context'
7 | function Result() {
8 |
9 | const { codeState } = useContext(SchemaContext)
10 |
11 | return (
12 | <>
13 |
22 | >
23 | )
24 | }
25 |
26 | export default Result;
--------------------------------------------------------------------------------
/client/components/Schema.js:
--------------------------------------------------------------------------------
1 | import CodeMirror from '@uiw/react-codemirror';
2 | import { javascript } from '@codemirror/lang-javascript';
3 |
4 | function Schema({ schema, view }) {
5 |
6 | const viewMaterial = (view === 'types')
7 |
8 | const types = schema.types;
9 | const resolvers = schema.resolvers;
10 |
11 | return (
12 |
13 |
21 |
22 | )
23 | }
24 |
25 | export default Schema
26 |
27 |
--------------------------------------------------------------------------------
/client/components/SchemaVisual.js:
--------------------------------------------------------------------------------
1 | import React, { useContext, useEffect } from 'react';
2 | import * as d3 from 'd3';
3 |
4 | import { URLContext } from '../context/global-context';
5 |
6 | export default function SchemaVisual({ visuals }) {
7 | // const { urlState } = useContext(URLContext);
8 |
9 | const treeData = visuals.visuals;
10 | console.log("should be json", visuals.visuals)
11 | function visualTree() {
12 |
13 | let margin = { top: 0, right: 90, bottom: 40, left: 70 },
14 | width = 1200 - margin.left - margin.right,
15 | height = 725 - margin.top - margin.bottom;
16 |
17 | let i = 0,
18 | duration = 750;
19 | // grabbing from DOM
20 | const svg = d3
21 | .select('#visualDisplay')
22 | .attr("width", width + margin.right + margin.left)
23 | .attr("height", height + margin.top + margin.bottom)
24 | .attr("transform", "translate(" + margin.left + "," + margin.top + ")")
25 |
26 |
27 | const g = svg.append('g')
28 |
29 | //defining where the actual area of the tree is
30 | const treemap = d3
31 | .tree()
32 | .size([height, width])
33 |
34 |
35 | // defining the parent root & it's coordinates
36 | const root = d3.hierarchy(treeData, (d) => d.children);
37 | root.x0 = svg.style.height / 2; // ! trying to find the middle of the svg graph to root the tree
38 | root.y0 = 0;
39 | root.children.forEach(collapse);
40 |
41 | // assigning each node properties and an id
42 | root.each(function (d) {
43 | d.name = d.data.name;
44 | d.id = i;
45 | i += 1;
46 | });
47 |
48 | // to actually open up the tree graph
49 | update(root);
50 |
51 | function collapse(d) {
52 | if (d.children) {
53 | d._children = d.children;
54 | d._children.forEach(collapse);
55 | d.children = null;
56 | }
57 | }
58 |
59 |
60 | function update(source) {
61 | const treeData = treemap(root);
62 | const nodes = treeData.descendants();
63 |
64 | nodes.forEach(function (d) {
65 | d.y = d.depth * 380;
66 | });
67 |
68 | const node = g
69 | .selectAll('.node')
70 | .data(nodes, (d) => d.id || (d.id = ++i));
71 |
72 | // the starting location of all nodes (aka the tree root's location)
73 | const nodeEnter = node
74 | .enter()
75 | .append('g')
76 | .attr('class', 'node')
77 | .attr('id', (d) => d.id)
78 | .style("cursor", "pointer")
79 | .attr("transform", function (d) {
80 | return "translate(" + source.y0 + "," + source.x0 + ")";
81 | })
82 | .on('click', click);
83 |
84 | nodeEnter
85 | .attr("class", "node")
86 | .attr("r", 1e-6)
87 | .style("fill", function (d) {
88 | return d.parent ? "#0C131F" : "#5304EE";
89 | })
90 |
91 | nodeEnter
92 | .append("rect")
93 | .attr("rx", function (d) {
94 | if (d.parent) return d.children || d._children ? 0 : 6;
95 | return 10;
96 | })
97 | .attr("ry", function (d) {
98 | if (d.parent) return d.children || d._children ? 0 : 6;
99 | return 10;
100 | })
101 | .attr("stroke-width", function (d) {
102 | return d.parent ? 1 : 0;
103 | })
104 | .attr("stroke", function (d) {
105 | return d.children || d._children
106 | ? "rgb(3, 192, 220)"
107 | : "rgb(38, 222, 176)";
108 | })
109 | .attr("stroke-dasharray", function (d) {
110 | return d.children || d._children ? "0" : "2.2";
111 | })
112 | .attr("stroke-opacity", function (d) {
113 | return d.children || d._children ? "1" : "0.6";
114 | })
115 | .attr("x", 0)
116 | .attr("y", -10)
117 | .attr("width", function (d) {
118 | return d.parent ? 210 : 100;
119 | })
120 | .attr("height", 20);
121 |
122 | // adding text label to each node
123 | nodeEnter
124 | .append("text")
125 | .style("fill", function (d) {
126 | if (d.parent) {
127 | return d.children || d._children ? "#ffffff" : "rgb(38, 222, 176)";
128 | }
129 | return "rgb(39, 43, 77)";
130 | })
131 | .style("font", "18px sans-serif")
132 | .attr("dy", ".35em")
133 | .attr("x", function (d) {
134 | return d.parent ? 105 : 50;
135 | })
136 | .attr("text-anchor", function (d) {
137 | return "middle";
138 | })
139 | .text(function (d) {
140 | return d.data.name;
141 | });
142 |
143 | // we are merging the original spot to the child point (overrwriting the objects)
144 | const nodeUpdate = nodeEnter.merge(node);
145 |
146 | // created the location that will move the children to their designated spots
147 | nodeUpdate
148 | .transition()
149 | .duration(duration)
150 | .attr("transform", function (d) {
151 | console.log('update');
152 | return "translate(" + d.y + "," + d.x + ")";
153 | });
154 |
155 | // style the child node at its correct location
156 | nodeUpdate
157 | .select('circle')
158 | // .attr('r', 6.5)
159 | .attr('fill', (d) => (d._children ? "#ffffff" : "rgb(38, 222, 176)")) //! diff circle fill for parent / child after child moves
160 | .attr('cursor', (d) => {
161 | if (d._children || d.children) return 'pointer';
162 | }); //! to remove the cursor pointer if a child
163 |
164 | nodeUpdate
165 | .select('text')
166 | .style('fill', (d) =>
167 | d._children || d.children ? "#ffffff" : "rgb(38, 222, 176)"
168 | ) // ! change color of text if parent
169 | .style('font-weight', (d) =>
170 | d._children || d.children ? 'bolder' : 'normal'
171 | ) // ! to make parent text bold
172 | .style('fill-opacity', 1)
173 |
174 | // defining the "disappearance" of the children nodes of the collapsed parent node
175 | const nodeExit = node
176 | .exit()
177 | .transition()
178 | .duration(duration)
179 | .attr("transform", function (d) {
180 | return "translate(" + source.y + "," + source.x + ")";
181 | })
182 | .remove();
183 |
184 | // styling the invisibility of the collapsed child
185 | nodeUpdate.select("circle").style("opacity", 1e-6);
186 | nodeUpdate.select("circle").attr("r", 1e-6);
187 | nodeExit.select('text').style('fill-opacity', 1e-6);
188 |
189 | nodes.forEach((d) => {
190 | d.x0 = d.x;
191 | d.y0 = d.y;
192 | });
193 |
194 | /******** LINKS (PATH) *******/
195 |
196 | // defining the number of links we need, excluding the root
197 | const links = nodes.slice(1);
198 |
199 | const link = svg.selectAll("path.link").data(links, function (d) {
200 | return d.id;
201 | });
202 |
203 | // starts the links at the parent's previous position
204 | const linkEnter = link
205 | .enter()
206 | .insert("path", "g")
207 | .attr("class", "link")
208 | .attr("d", function (d) {
209 | var o = { x: source.x0, y: source.y0 };
210 | return diagonal(o, o);
211 | });
212 |
213 | const linkUpdate = linkEnter.merge(link);
214 | linkUpdate
215 | .transition()
216 | .duration(duration)
217 | .attr("d", function (d) {
218 | return diagonal(d, d.parent);
219 | });
220 |
221 | const linkExit = link
222 | .exit()
223 | .transition()
224 | .duration(duration)
225 | .attr("d", function (d) {
226 | var o = { x: source.x, y: source.y };
227 | return diagonal(o, o);
228 | })
229 | .remove();
230 |
231 | nodes.forEach(function (d) {
232 | d.x0 = d.x;
233 | d.y0 = d.y;
234 | });
235 |
236 | function diagonal(s, d) {
237 | return `M ${s.y} ${s.x}
238 | C ${(s.y + (d.y)) / 2} ${s.x},
239 | ${(s.y + d.y) / 2} ${d.x},
240 | ${d.y} ${d.x}`;
241 | }
242 |
243 | function click(event, d) {
244 | if (d.children) {
245 | d._children = d.children;
246 | d.children = null;
247 | } else {
248 | d.children = d._children;
249 | d._children = null;
250 | }
251 | update(d);
252 | }
253 |
254 | d3.selectAll('path')
255 | .attr('fill', 'none')
256 | .attr('stroke', 'rgb(55,68,105)')
257 | .attr('stroke-width', 1)
258 |
259 | }
260 | }
261 |
262 | useEffect(visualTree, [])
263 |
264 | return (
265 |
266 |
267 |
268 | )
269 |
270 | }
271 |
--------------------------------------------------------------------------------
/client/components/SideBar.js:
--------------------------------------------------------------------------------
1 | import { useContext } from "react";
2 | import Dialog from "@mui/material/Dialog";
3 | import SchemaVisual from '../components/SchemaVisual'
4 | import Schema from "./Schema";
5 | import classes from "../../styles/SideBar.module.css";
6 | import { SidebarContext } from "../context/global-context";
7 | import StorageIcon from '@mui/icons-material/Storage';
8 | import FeaturedPlayListIcon from '@mui/icons-material/FeaturedPlayList';
9 | import InsertPhotoIcon from '@mui/icons-material/InsertPhoto';
10 | import SaveAltOutlinedIcon from '@mui/icons-material/SaveAltOutlined';
11 |
12 | function SideBar({ openDB }) {
13 | const { displayState, displayDispatch, urlState } = useContext(SidebarContext);
14 |
15 | // const closeSidebar = () => {
16 | // displayDispatch({
17 | // type: "UPDATE_SIDEBAR_DISPLAY",
18 | // payload: false,
19 | // });
20 | // };
21 |
22 | const openDBHandler = () => {
23 | return openDB();
24 | };
25 |
26 | const handleSchemaOpen = (value) => {
27 | console.log('openplease')
28 | displayDispatch({
29 | type: "UPDATE_SCHEMA_DISPLAY",
30 | payload: value,
31 | });
32 | };
33 |
34 | const handleSchemaClose = () => {
35 | console.log('please close')
36 | displayDispatch({
37 | type: "UPDATE_SCHEMA_DISPLAY",
38 | payload: false,
39 | });
40 | };
41 |
42 | const handleSchemaVisOpen = () => {
43 | displayDispatch({
44 | type: "UPDATE_D3_DISPLAY",
45 | payload: true,
46 | });
47 | };
48 |
49 | const handleSchemaVisClose = () => {
50 | displayDispatch({
51 | type: "UPDATE_D3_DISPLAY",
52 | payload: false,
53 | });
54 | };
55 |
56 | return (
57 |
137 |
138 | );
139 | }
140 |
141 | export default SideBar;
--------------------------------------------------------------------------------
/client/components/URILink.js:
--------------------------------------------------------------------------------
1 | import React, { useContext, useState } from "react";
2 | import { URLContext } from "../context/global-context";
3 |
4 | import { secret } from "../../server/generator/testPSQL";
5 | import cryptoJs from "crypto-js";
6 | import classes from "../../styles/URILink.module.css";
7 |
8 | import TextField from "@mui/material/TextField";
9 | import Box from "@mui/material/Box";
10 | import DialogContent from "@mui/material/DialogContent";
11 | import DialogTitle from "@mui/material/DialogTitle";
12 |
13 | const URILink = ({ closeHandler }) => {
14 | const { urlState, urlDispatch } = useContext(URLContext);
15 |
16 | const handleChange = (e) => {
17 | urlDispatch({
18 | type: "UPDATE_INPUT_URL",
19 | payload: e.target.value,
20 | });
21 | urlDispatch({
22 | type: "UPDATE_ENTRY_ERROR",
23 | payload: false,
24 | });
25 | urlDispatch({
26 | type: "UPDATE_INVALID_ERROR",
27 | payload: false,
28 | });
29 | };
30 |
31 | const submitHandler = async (e) => {
32 | e.preventDefault();
33 |
34 | const requestOptions = {
35 | method: "POST",
36 | headers: { "Content-Type": "application/json" },
37 | };
38 |
39 | if (e.target.value === "submitNew" || e.key === "Enter") {
40 | if (urlState.inputURL) {
41 | // i think we should sanitize newUrl against SQL injections
42 | let encryptedUrl = cryptoJs.AES.encrypt(
43 | urlState.inputURL,
44 | secret
45 | ).toString();
46 | requestOptions.body = JSON.stringify({ uri: encryptedUrl });
47 |
48 | urlDispatch({
49 | type: "UPDATE_URL",
50 | payload: {
51 | url: encryptedUrl,
52 | },
53 | });
54 | } else {
55 | return urlDispatch({
56 | type: "UPDATE_ENTRY_ERROR",
57 | payload: true,
58 | });
59 | }
60 | }
61 |
62 | const result = await fetch("http://localhost:8080/schema", requestOptions);
63 | const jsonData = await result.json();
64 |
65 | if (!jsonData.schema) {
66 | urlDispatch({
67 | type: "UPDATE_INPUT_URL",
68 | payload: "",
69 | });
70 | urlDispatch({
71 | type: "UPDATE_INVALID_ERROR",
72 | payload: true,
73 | });
74 | } else {
75 | urlDispatch({
76 | type: "UPDATE_SCHEMA",
77 | payload: {
78 | types: jsonData.schema.types,
79 | resolvers: jsonData.schema.resolvers,
80 | visuals: jsonData.visuals
81 | },
82 | });
83 | return closeHandler();
84 | }
85 | };
86 |
87 | return (
88 |
89 |
98 | {urlState.invalidError ? (
99 |
100 | Please submit a valid URL
101 |
102 | ) : (
103 | Enter your Database URL
104 | )}
105 | {
116 | if (e.key === "Enter") {
117 | submitHandler(e);
118 | }
119 | }}
120 | >
121 |
122 |
129 | Submit URL
130 |
131 |
132 |
133 |
134 | {urlState.entryError ? (
135 |
138 | Press "Use Default DB" to use our default database
139 |
140 | ) : (
141 |
142 | Press "Use Default DB" to use our default database
143 |
144 | )}
145 |
152 | Use Default DB
153 |
154 |
155 |
156 | );
157 | };
158 |
159 | export default URILink;
160 |
--------------------------------------------------------------------------------
/client/context/global-context.js:
--------------------------------------------------------------------------------
1 | // this file simply creates the contexts, 1 for state and 1 for dispatch
2 | // this is best practice to avoid unnecessary rerenders
3 | // both are imported into GlobalState.js where they are used as Provider elements which will form a wrapper around child components (in our case App)
4 | import { createContext, useContext } from 'react';
5 |
6 | const GlobalContext = createContext();
7 |
8 | const GlobalDispatch = createContext();
9 |
10 | export const SchemaContext = createContext();
11 |
12 | export const QueryContext = createContext();
13 |
14 | export const GraphContext = createContext();
15 |
16 | export const URLContext = createContext();
17 |
18 | export const StatusContext = createContext();
19 |
20 | export const HistoryContext = createContext();
21 |
22 | export const SidebarContext = createContext();
23 |
24 | export {GlobalContext, GlobalDispatch};
25 |
--------------------------------------------------------------------------------
/client/context/global-reducer.js:
--------------------------------------------------------------------------------
1 | // here we create the reducer function
2 | // we import our actions as variables to avoid bugs from mispelling
3 |
4 | export const initialCodeState = {
5 | query: [],
6 | result: '',
7 | queryInput: ''
8 | };
9 |
10 | export const initialSpeedState = {
11 | speed: [],
12 | firstQuery: true,
13 | };
14 |
15 | export const initialURLState = {
16 | url: "",
17 | inputURL: "",
18 | types: "",
19 | resolvers: "",
20 | entryError: false,
21 | invalidError: false,
22 | visuals: {},
23 | };
24 |
25 | export const initialDisplayState = {
26 | URIModal: true,
27 | sidebar: false,
28 | schema: false,
29 | history: false,
30 | visuals: false,
31 | };
32 |
33 | export const codeReducer = (state, action) => {
34 | switch (action.type) {
35 | case 'UPDATE_RESULT':
36 | return {
37 | ...state,
38 | query: [...state.query, action.payload.query],
39 | result: action.payload.result
40 | };
41 | case 'UPDATE_QUERY_INPUT':
42 | return {
43 | ...state,
44 | queryInput: action.payload.queryInput
45 | }
46 | }
47 | };
48 |
49 | export const speedReducer = (state, action) => {
50 | switch (action.type) {
51 | case 'UPDATE_SPEED':
52 | console.log(action.payload.speed);
53 | return {
54 | ...state,
55 | speed: action.payload.speed
56 | };
57 | case 'UPDATE_FIRST_QUERY':
58 | return {
59 | ...state,
60 | firstQuery: action.payload
61 | }
62 | }
63 | }
64 |
65 | export const displayReducer = (state, action) => {
66 | switch (action.type) {
67 | case 'UPDATE_MODAL_DISPLAY':
68 | return {
69 | ...state,
70 | URIModal: action.payload
71 | };
72 | case 'UPDATE_SIDEBAR_DISPLAY':
73 | return {
74 | ...state,
75 | sidebar: action.payload
76 | };
77 | case 'UPDATE_SCHEMA_DISPLAY':
78 | return {
79 | ...state,
80 | schema: action.payload
81 | };
82 | case 'UPDATE_HISTORY_DISPLAY':
83 | return {
84 | ...state,
85 | history: action.payload
86 | };
87 | case 'UPDATE_D3_DISPLAY':
88 | return {
89 | ...state,
90 | visuals: action.payload
91 | }
92 | }
93 | };
94 |
95 |
96 | export const urlReducer = (state, action) => {
97 | switch (action.type) {
98 | case "UPDATE_URL":
99 | return {
100 | ...state,
101 | url: action.payload.url,
102 | };
103 | case "UPDATE_SCHEMA":
104 | return {
105 | ...state,
106 | types: action.payload.types,
107 | resolvers: action.payload.resolvers,
108 | visuals: action.payload.visuals
109 | };
110 | case "UPDATE_ENTRY_ERROR":
111 | return {
112 | ...state,
113 | entryError: action.payload,
114 | };
115 | case "UPDATE_INVALID_ERROR":
116 | return {
117 | ...state,
118 | invalidError: action.payload,
119 | };
120 | case "UPDATE_INPUT_URL":
121 | return {
122 | ...state,
123 | inputURL: action.payload,
124 | };
125 |
126 | case 'UPDATE_SCHEMA':
127 | return {
128 | ...state,
129 | types: action.payload.types,
130 | resolvers: action.payload.resolvers
131 | }
132 |
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | // jest.config.js
2 | const nextJest = require('next/jest')
3 |
4 | const createJestConfig = nextJest({
5 | // Provide the path to your Next.js app to load next.config.js and .env files in your test environment
6 | dir: './',
7 | })
8 |
9 | // Add any custom config to be passed to Jest
10 | const customJestConfig = {
11 | moduleDirectories: ['node_modules', '/'],
12 | testEnvironment: 'jest-environment-jsdom',
13 |
14 | moduleNameMapper: {
15 | '^d3$': '/node_modules/d3/dist/d3.min.js',
16 | },
17 | }
18 |
19 | // createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
20 | module.exports = createJestConfig(customJestConfig)
21 |
22 |
23 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/MetriQL/a94ace09255761003f81003d420b3640be4649c0/next.config.js
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "metriql",
3 | "version": "1.0.0",
4 | "description": "A GraphQL Migration Tool",
5 | "main": "webpack.config.js",
6 | "scripts": {
7 | "dev": "next dev & nodemon server/server.js",
8 | "build": "next build",
9 | "start": "next start",
10 | "lint": "next lint",
11 | "test": "echo \"Error: no test specified\" && exit 1"
12 | },
13 | "repository": {
14 | "type": "git",
15 | "url": "git+https://github.com/oslabs-beta/MetriQL.git"
16 | },
17 | "keywords": [],
18 | "author": "Diana Li, Eric Rodgers, Raymond Huang, Rehema Armorer, Alfonso Zamarripa",
19 | "license": "ISC",
20 | "bugs": {
21 | "url": "https://github.com/oslabs-beta/MetriQL/issues"
22 | },
23 | "homepage": "https://github.com/oslabs-beta/MetriQL#readme",
24 | "devDependencies": {
25 | "@hot-loader/react-dom": "^17.0.2",
26 | "@types/react": "^17.0.39",
27 | "@types/react-dom": "^17.0.13",
28 | "autoprefixer": "^10.4.2",
29 | "cross-env": "^7.0.3",
30 | "css-loader": "^6.7.0",
31 | "dotenv": "^16.0.0",
32 | "eslint": "^8.10.0",
33 | "file-loader": "^6.2.0",
34 | "html-webpack-plugin": "^5.5.0",
35 | "jest": "^27.5.1",
36 | "node-sass": "^7.0.1",
37 | "nodemon": "^2.0.15",
38 | "postcss": "^8.4.8",
39 | "sass": "^1.49.9",
40 | "sass-loader": "^12.6.0",
41 | "style-loader": "^3.3.1",
42 | "tailwindcss": "^3.0.23",
43 | "ts-loader": "^9.2.7",
44 | "typescript": "^4.6.2",
45 | "url-loader": "^4.1.1",
46 | "webpack": "^5.70.0",
47 | "webpack-cli": "^4.9.2",
48 | "webpack-dev-server": "^4.7.4",
49 | "webpack-hot-middleware": "^2.25.1"
50 | },
51 | "dependencies": {
52 | "@apollo/client": "^3.5.10",
53 | "@codemirror/lang-javascript": "^0.19.7",
54 | "@emotion/react": "^11.8.2",
55 | "@emotion/styled": "^11.8.1",
56 | "@faker-js/faker": "^6.0.0-beta.0",
57 | "@material-ui/core": "^4.12.3",
58 | "@material-ui/icons": "^4.11.2",
59 | "@mui/icons-material": "^5.5.1",
60 | "@mui/material": "^5.5.1",
61 | "@mui/styles": "^5.5.1",
62 | "@mui/system": "^5.5.1",
63 | "@testing-library/jest-dom": "^5.16.3",
64 | "@testing-library/react": "^13.0.0",
65 | "@uiw/react-codemirror": "^4.5.1",
66 | "apollo-server-express": "^3.6.3",
67 | "axios": "^0.26.1",
68 | "bcryptjs": "^2.4.3",
69 | "body-parser": "^1.19.2",
70 | "camelcase": "^6.3.0",
71 | "chart.js": "^3.7.1",
72 | "codemirror": "^5.65.2",
73 | "codemirror-graphql": "^1.2.12",
74 | "cookie-parser": "^1.4.6",
75 | "cors": "^2.8.5",
76 | "crypto-js": "^4.1.1",
77 | "d3": "^7.3.0",
78 | "express": "^4.17.3",
79 | "express-graphql": "^0.12.0",
80 | "express-session": "^1.17.2",
81 | "fontsource-roboto": "^4.0.0",
82 | "framer-motion": "^6.2.8",
83 | "graphql": "^16.3.0",
84 | "graphql-tools": "^8.2.0",
85 | "msw": "^0.39.2",
86 | "next": "^12.1.0",
87 | "pascal-case": "^3.1.2",
88 | "passport": "^0.5.2",
89 | "passport-github2": "^0.1.12",
90 | "pg": "^8.7.3",
91 | "pluralize": "^8.0.0",
92 | "react": "^17.0.2",
93 | "react-chartjs-2": "^4.0.1",
94 | "react-dom": "^17.0.2",
95 | "react-hot-loader": "^4.13.0",
96 | "react-icons": "^4.3.1",
97 | "styled-components": "^5.3.5",
98 | "supertest": "^6.2.2"
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/pages/_app.js:
--------------------------------------------------------------------------------
1 | import {
2 | ApolloClient,
3 | InMemoryCache,
4 | ApolloProvider,
5 | HttpLink,
6 | from,
7 | } from '@apollo/client';
8 |
9 | import App from '../client/components/App';
10 |
11 | import '../styles/globals.css'
12 |
13 | const link = from([
14 | new HttpLink({ uri: "http://localhost:8080/graphql"})
15 | ])
16 | //create apollo client to get graphql data
17 | const client = new ApolloClient({
18 | cache: new InMemoryCache(),
19 | link: link
20 | })
21 |
22 |
23 | function MyApp({ Component, pageProps }) {
24 | return (
25 |
26 |
27 |
28 |
29 |
30 | )
31 | }
32 |
33 | export default MyApp;
--------------------------------------------------------------------------------
/pages/index.js:
--------------------------------------------------------------------------------
1 | import { useReducer } from 'react';
2 | import Dialog from '@mui/material/Dialog';
3 |
4 | import { SchemaContext, QueryContext, GraphContext, URLContext, SidebarContext, HistoryContext } from '../client/context/global-context';
5 | import {
6 | initialCodeState,
7 | codeReducer,
8 | initialSpeedState,
9 | speedReducer,
10 | initialURLState,
11 | urlReducer,
12 | initialDisplayState,
13 | displayReducer,
14 | } from "../client/context/global-reducer";
15 | import Header from '../client/components/Header';
16 | import SideBar from '../client/components/SideBar';
17 | import Metric from '../client/components/Metric';
18 | import QueryInput from '../client/components/QueryInput';
19 | import Result from '../client/components/Result';
20 | import URILink from '../client/components/URILink';
21 | import QueryHistory from '../client/components/QueryHistory';
22 |
23 | function MainPage() {
24 |
25 | const [codeState, codeDispatch] = useReducer(codeReducer, initialCodeState);
26 |
27 | const [speedState, speedUpdate] = useReducer(speedReducer, initialSpeedState);
28 |
29 | const [urlState, urlDispatch] = useReducer(urlReducer, initialURLState);
30 |
31 | const [displayState, displayDispatch] = useReducer(displayReducer, initialDisplayState);
32 |
33 | const handleClickOpen = () => {
34 | displayDispatch({
35 | type: 'UPDATE_MODAL_DISPLAY',
36 | payload: true
37 | })
38 | };
39 |
40 | const handleClose = () => {
41 | displayDispatch({
42 | type: 'UPDATE_MODAL_DISPLAY',
43 | payload: false
44 | })
45 | };
46 |
47 |
48 | return (
49 |
50 |
51 |
52 |
53 |
59 |
60 |
61 |
62 |
63 |
64 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
85 |
86 |
87 |
88 |
89 |
93 |
94 |
95 |
96 |
101 |
102 |
103 |
104 |
105 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 | )
121 | }
122 |
123 | export default MainPage
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/public/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/MetriQL/a94ace09255761003f81003d420b3640be4649c0/public/.DS_Store
--------------------------------------------------------------------------------
/public/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/MetriQL/a94ace09255761003f81003d420b3640be4649c0/public/demo.gif
--------------------------------------------------------------------------------
/public/gif-4.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/MetriQL/a94ace09255761003f81003d420b3640be4649c0/public/gif-4.gif
--------------------------------------------------------------------------------
/public/graph.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/MetriQL/a94ace09255761003f81003d420b3640be4649c0/public/graph.gif
--------------------------------------------------------------------------------
/public/metricql-transparent.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/MetriQL/a94ace09255761003f81003d420b3640be4649c0/public/metricql-transparent.png
--------------------------------------------------------------------------------
/public/metricql.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/MetriQL/a94ace09255761003f81003d420b3640be4649c0/public/metricql.png
--------------------------------------------------------------------------------
/public/query-history.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/MetriQL/a94ace09255761003f81003d420b3640be4649c0/public/query-history.gif
--------------------------------------------------------------------------------
/public/schema.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/MetriQL/a94ace09255761003f81003d420b3640be4649c0/public/schema.gif
--------------------------------------------------------------------------------
/public/visualizer.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/MetriQL/a94ace09255761003f81003d420b3640be4649c0/public/visualizer.gif
--------------------------------------------------------------------------------
/server/controllers/postgreSQLController.js:
--------------------------------------------------------------------------------
1 | const { Pool } = require("pg");
2 | const CryptoJS = require("crypto-js");
3 | const fs = require('fs')
4 | const pgQuery = fs.readFileSync('server/query/tables.sql', 'utf8')
5 | const schema = require('../generator/schema.js')
6 | const { schemaImport, schemaExport } = require('./schemaFunc')
7 | const { isReferenceTable } = require('../generator/resolverFunc')
8 | const { secret } = require('../generator/testPSQL.js');
9 | const path = require('path');
10 | require('dotenv').config();
11 |
12 |
13 | const PG_URI_STARWARS = process.env.PG_URI_STARWARS;
14 |
15 | const decryptURI = (encryptedUserURI) => {
16 | const data = CryptoJS.AES.decrypt(encryptedUserURI, secret);
17 | const decryptedURI = data.toString(CryptoJS.enc.Utf8);
18 | return decryptedURI;
19 | };
20 |
21 | const postgreSQLController = {};
22 |
23 |
24 | postgreSQLController.table = async (req, res, next) => {
25 | let postURI;
26 | req.body.uri ? (postURI = decryptURI(req.body.uri)) : (postURI = PG_URI_STARWARS)
27 |
28 |
29 | res.locals.URI = postURI;
30 | const db = new Pool({ connectionString: postURI });
31 | try {
32 | const result = await db.query(pgQuery);
33 | res.locals.SQLtables = result.rows[0].tables;
34 | next();
35 | } catch (err) {
36 | return next({
37 | log: `Error occurred in postgreSQLController.getSchema ERROR: ${err}`,
38 | message: { err: `Error occured in postgreSQLController.getSchema. Check server log for more detail ${err}` },
39 | })
40 | }
41 | }
42 |
43 | postgreSQLController.schemaGenerator = (req, res, next) => {
44 | const { SQLtables } = res.locals;
45 | try {
46 | const types = schema.typeGenerator(SQLtables);
47 | const resolvers = schema.resolverGenerator(SQLtables);
48 | res.locals.schema = { types, resolvers };
49 | next();
50 | } catch (err) {
51 | return next({
52 | log: `Error occurred in postgreSQLController.schemaGenerator ERROR: ${err}`,
53 | message: { err: `Error occured in postgreSQLControllers.schemaGenerator. Check server log for more detail. ${err}` }
54 | })
55 | }
56 | }
57 |
58 | postgreSQLController.writeSchemaToFile = (req, res, next) => {
59 | try {
60 | console.log('111')
61 | const { URI } = res.locals;
62 | console.log(URI)
63 | const schemaImportText = schemaImport(URI);
64 | console.log('result')
65 | const schemaExportText = schemaExport();
66 | console.log('first')
67 | const schemaFile =
68 | schemaImportText +
69 | '\n' +
70 | res.locals.schema.types +
71 | '\n' +
72 | res.locals.schema.resolvers +
73 | '\n' +
74 | schemaExportText;
75 |
76 | console.log('second')
77 | fs.writeFileSync(
78 | path.resolve(__dirname, '../graphQLServer/schema.js'),
79 | schemaFile
80 | );
81 | console.log('third');
82 | next();
83 | } catch (err) {
84 | const errObj = {
85 | log: `Error in writeSchemaToFile: ${err}`,
86 | status: 400,
87 | message: { err: { err } },
88 | };
89 | return next(errObj);
90 | }
91 | };
92 |
93 | postgreSQLController.d3JSONGenerator = (req, res, next) => {
94 | try {
95 | const { SQLtables } = res.locals;
96 | const children = [];
97 | const root = { name: 'Queries', children };
98 |
99 | Object.keys(SQLtables).forEach((table) => {
100 |
101 | const { foreignKeys, referencedBy, columns } = SQLtables[table];
102 | if (!foreignKeys || !isReferenceTable(foreignKeys, columns)) {
103 | const point = [];
104 |
105 | if (foreignKeys) {
106 | Object.keys(foreignKeys).forEach((item) => {
107 | const { referenceTable } = foreignKeys[item];
108 | point.push(referenceTable);
109 | });
110 | }
111 |
112 |
113 | const tableChildren = [];
114 |
115 | Object.keys(columns).forEach((column) => {
116 | const child = {};
117 | child['name'] = column;
118 | child['type'] = columns[column].dataType;
119 | child['columnDefault'] = columns[column].columnDefault;
120 | child['isNullable'] = columns[column].isNullable;
121 | child['charMaxLength'] = columns[column].charMaxLength;
122 |
123 | tableChildren.push(child);
124 | });
125 |
126 | const tableData = {};
127 | tableData['name'] = table;
128 | tableData['foreignKeys'] = point;
129 | tableData['referencedBy'] = referencedBy
130 | ? Object.keys(referencedBy)
131 | : [];
132 | tableData['children'] = tableChildren;
133 |
134 | children.push(tableData);
135 | }
136 | });
137 |
138 | res.locals.d3JSON = root;
139 | console.log(res.locals.d3JSON)
140 | return next();
141 | } catch (err) {
142 | const errObj = {
143 | log: `Error in postgreSQLController.d3JSONGenerator: ${err}`,
144 | status: 400,
145 | message: { err: {err} },
146 | };
147 | return next(errObj);
148 | }
149 | };
150 |
151 |
152 |
153 |
154 | module.exports = postgreSQLController
--------------------------------------------------------------------------------
/server/controllers/schemaFunc.js:
--------------------------------------------------------------------------------
1 | const schemaImport = (uri) => {
2 | return (
3 | `const { makeExecutableSchema } = require('graphql-tools');\n` +
4 | `const { Pool } = require('pg');\n` +
5 | `const PG_URI = '${uri}';\n\n` +
6 | `const pool = new Pool({\n` +
7 | ` connectionString: PG_URI\n` +
8 | `});\n\n` +
9 | `const db = {};\n` +
10 | `db.query = (text,params, callback) => {
11 | console.log('executed query:', text)
12 | return pool.query(text, params, callback) \n};\n\n`
13 | );
14 | };
15 |
16 | const schemaExport = () => {
17 | return ` const schema = makeExecutableSchema({
18 | typeDefs,
19 | resolvers,
20 | });
21 |
22 | module.exports = schema;`;
23 | };
24 |
25 | module.exports = {
26 | schemaImport,
27 | schemaExport
28 | }
--------------------------------------------------------------------------------
/server/generator/generateTypes.js:
--------------------------------------------------------------------------------
1 | const { pascalCase } = require('pascal-case');
2 | const toCamelCase = require('camelcase');
3 | const { singular } = require('pluralize');
4 | const { typesFunc } = require('./typesFunc')
5 |
6 | const generateTypes = {};
7 |
8 | generateTypes.queries = (tableName, tableData) => {
9 | const { primaryKey, foreignKeys, columns } = tableData
10 | const tableNameSingular = singular(tableName);
11 | const primaryKeyType = typesFunc.typeSet(columns[primaryKey].dataType);
12 | let byID = toCamelCase(tableNameSingular);
13 | if (tableNameSingular === tableName) byID += 'ByID'
14 |
15 | return (
16 | ` ${toCamelCase(tableName)}: [${pascalCase(tableNameSingular)}!]!\n` +
17 | ` ${byID}(${primaryKey}: ${primaryKeyType}!): ${pascalCase(tableNameSingular)}!\n`
18 | );
19 |
20 | };
21 |
22 | generateTypes.mutations = (tableName, tableData) => {
23 | const {primaryKey, foreignKeys, columns} = tableData;
24 |
25 | return (
26 | typesFunc.mutCreate(tableName, primaryKey, foreignKeys, columns) +
27 | typesFunc.mutUpdate(tableName, primaryKey, foreignKeys, columns) +
28 | typesFunc.mutDelete(tableName, primaryKey)
29 | );
30 | };
31 |
32 | generateTypes.custom = (tableName, tables) => {
33 | const { primaryKey, foreignKeys, columns } = tables[tableName];
34 | const primaryKeyType = typesFunc.typeSet(columns[primaryKey].dataType);
35 | return `${
36 | ` type ${pascalCase(singular(tableName))} {\n` +
37 | ` ${primaryKey}: ${primaryKeyType}`
38 | }${typesFunc.getColumns(
39 | primaryKey,
40 | foreignKeys,
41 | columns
42 | )}${typesFunc.getRelationships(tableName, tables)}\n }\n\n`;
43 | }
44 |
45 | module.exports = generateTypes;
--------------------------------------------------------------------------------
/server/generator/generatorResolver.js:
--------------------------------------------------------------------------------
1 | const { singular } = require('pluralize');
2 | const { pascalCase } = require('pascal-case');
3 | const { resolverFunc } = require('./resolverFunc')
4 |
5 | const generateResolver = {};
6 |
7 | generateResolver.queries = (tableName, tableData) => {
8 | const { primaryKey } = tableData;
9 | const queryPrimaryKey = resolverFunc.queryPrimaryKey(tableName, primaryKey);
10 | const queryAll = resolverFunc.queryAll(tableName);
11 | console.log('here')
12 | return `\n${queryPrimaryKey}\n${queryAll}`;
13 | }
14 |
15 |
16 | generateResolver.mutations = (tableName, tableData) => {
17 | const { primaryKey, columns } = tableData;
18 | const createMut = resolverFunc.mutCreate(
19 | tableName,
20 | primaryKey,
21 | columns
22 | );
23 | const updateMut = resolverFunc.mutUpdate(
24 | tableName,
25 | primaryKey,
26 | columns
27 | );
28 | const deleteMut = resolverFunc.mutDelete(
29 | tableName,
30 | primaryKey
31 | );
32 | return `${createMut}\n${updateMut}\n${deleteMut}\n`;
33 | };
34 |
35 | generateResolver.custom = (tableName, SQLtables) => {
36 | const { referencedBy } = SQLtables[tableName];
37 | if (!referencedBy) return '';
38 | const queryName = pascalCase(singular(tableName));
39 | let relationshipTypes = '';
40 |
41 | relationshipTypes += resolverFunc.determineRelationships(
42 | tableName,
43 | SQLtables
44 | );
45 | return `
46 | ${queryName}: {
47 | ${relationshipTypes}
48 | },\n`;
49 | };
50 |
51 | module.exports = generateResolver;
--------------------------------------------------------------------------------
/server/generator/resolverFunc.js:
--------------------------------------------------------------------------------
1 | const toCamelCase = require('camelcase');
2 | const { singular } = require('pluralize');
3 | const { ModuleFilenameHelpers } = require('webpack');
4 |
5 | const resolverFunc = {};
6 |
7 | resolverFunc.queryPrimaryKey = (tableName, primaryKey) => {
8 | let queryName = toCamelCase(singular(tableName));
9 | if (tableName === singular(tableName)) queryName += 'ByID';
10 | return `
11 | ${queryName}: (parent, args) => {
12 | const query = 'SELECT * FROM ${tableName} WHERE ${primaryKey} = $1';
13 | const values = [args.${primaryKey}];
14 | return db.query(query, values)
15 | .then(data => data.rows[0])
16 | .catch(err => new Error(err));
17 | },`;
18 | };
19 |
20 | resolverFunc.queryAll = (tableName) => {
21 | const queryName = toCamelCase(tableName);
22 | return `
23 | ${queryName}: () => {
24 | const query = 'SELECT * FROM ${tableName}';
25 | return db.query(query)
26 | .then(data => data.rows)
27 | .catch(err => new Error(err));
28 | },`;
29 | };
30 |
31 | resolverFunc.mutCreate = (tableName, primaryKey, columns) => {
32 | const queryName = toCamelCase('create_' + singular(tableName));
33 | const columnNames = Object.keys(columns);
34 | const queryValues = columnNames.filter((column) => column !== primaryKey);
35 |
36 | return `
37 | ${queryName}: (parent, args) => {
38 | const query = 'INSERT INTO ${tableName}(${queryValues.join(
39 | ', '
40 | )}) VALUES(${queryValues.map((el, i) => `$${++i}`).join(', ')}) RETURNING *';
41 | const values = [${queryValues
42 | .map((column) => `args.${column}`)
43 | .join(', ')}];
44 | return db.query(query, values)
45 | .then(data => data.rows[0])
46 | .catch(err => new Error(err));
47 | },`;
48 | };
49 |
50 | resolverFunc.mutUpdate = (tableName, primaryKey, columns) => {
51 | const queryName = toCamelCase('update_' + singular(tableName));
52 | const columnNames = Object.keys(columns);
53 | const queryValues = columnNames.filter((column) => column !== primaryKey);
54 | const conditional = queryValues.length + 1;
55 |
56 | return `
57 | ${queryName}: (parent, args) => {
58 | const query = 'UPDATE ${tableName} SET ${queryValues
59 | .map((el, i) => `${el}=$${++i}`)
60 | .join(', ')} WHERE ${primaryKey} = $${conditional} RETURNING *';
61 | const values = [${queryValues
62 | .map((column) => `args.${column}`)
63 | .join(', ')}, args.${primaryKey}];
64 | return db.query(query, values)
65 | .then(data => data.rows[0])
66 | .catch(err => new Error(err));
67 | },`;
68 | };
69 |
70 | resolverFunc.mutDelete = (tableName, primaryKey) => {
71 | const queryName = toCamelCase('delete_' + singular(tableName));
72 |
73 | return `
74 | ${queryName}: (parent, args) => {
75 | const query = 'DELETE FROM ${tableName} WHERE ${primaryKey} = $1 RETURNING *';
76 | const values = [args.${primaryKey}];
77 | return db.query(query, values)
78 | .then(data => data.rows[0])
79 | .catch(err => new Error(err));
80 | },`;
81 | };
82 | const isReferenceTable = (foreignKeys, columns) => {
83 | return Object.keys(columns).length === Object.keys(foreignKeys).length + 1;
84 | };
85 |
86 | resolverFunc.determineRelationships = (tableName, SQLtables) => {
87 | const { primaryKey, referencedBy, foreignKeys } = SQLtables[tableName];
88 | let relationships = '';
89 |
90 | Object.keys(referencedBy).forEach((refTable) => {
91 | const {
92 | referencedBy: foreignRefBy,
93 | foreignKeys: foreignFKeys,
94 | columns: foreignColumns,
95 | } = SQLtables[refTable];
96 |
97 | // One-to-One
98 | if (foreignRefBy && foreignRefBy[tableName]) {
99 | relationships += oneToOne(
100 | tableName,
101 | primaryKey,
102 | refTable,
103 | referencedBy[refTable]
104 | );
105 | const oneToOne = (
106 | tableName,
107 | primaryKey,
108 | refTable,
109 | refForeignKey
110 | ) => {
111 | return `
112 | ${toCamelCase(refTable)}: (${toCamelCase(tableName)}) => {
113 | const query = 'SELECT * FROM ${refTable} WHERE ${refForeignKey} = $1';
114 | const values = [${tableName}.${primaryKey}];
115 | return db.query(query, values)
116 | .then(data => data.rows[0])
117 | .catch(err => new Error(err));
118 | },`;
119 | };
120 | }
121 | // One-to-Many
122 | else if (!isReferenceTable(foreignFKeys, foreignColumns)) {
123 | relationships += oneToMany(
124 | tableName,
125 | primaryKey,
126 | refTable,
127 | referencedBy[refTable]
128 | );
129 | }
130 | // Many-to-Many
131 | else {
132 | Object.keys(foreignFKeys).forEach((FKey) => {
133 | if (tableName !== foreignFKeys[FKey].referenceTable) {
134 | const manyToManyTable = foreignFKeys[FKey].referenceTable;
135 | const manyToManyTableRefKey = FKey;
136 |
137 | const currentTableRefKey =
138 | SQLtables[tableName].referencedBy[refTable];
139 | const manyToManyTablePKey = SQLtables[manyToManyTable].primaryKey;
140 | relationships += manyToMany(
141 | tableName,
142 | primaryKey,
143 | refTable,
144 | manyToManyTableRefKey,
145 | currentTableRefKey,
146 | manyToManyTable,
147 | manyToManyTablePKey
148 | );
149 | }
150 | });
151 |
152 | if (foreignKeys) {
153 | Object.keys(foreignKeys).forEach((fk) => {
154 | const refTable = SQLtables[tableName].foreignKeys[fk].referenceTable;
155 | const refTablePk = SQLtables[refTable].primaryKey;
156 | const refKey = SQLtables[tableName].foreignKeys[fk].referenceKey;
157 | const checkQuery = foreignKeyCheck(
158 | tableName,
159 | primaryKey,
160 | refKey,
161 | fk,
162 | refTable,
163 | refTablePk
164 | );
165 | if (!relationships.includes(checkQuery)) relationships += checkQuery;
166 | });
167 | }
168 | }
169 | });
170 | return relationships;
171 | };
172 |
173 | const oneToMany = (
174 | tableName,
175 | primaryKey,
176 | refTable,
177 | refForeignKey
178 | ) => {
179 | return `
180 | ${toCamelCase(refTable)}: (${toCamelCase(tableName)}) => {
181 | const query = 'SELECT * FROM ${refTable} WHERE ${refForeignKey} $1';
182 | const values = [${tableName}.${primaryKey}];
183 | return db.query(query, values)
184 | .then(data => data.rows)
185 | .catch(err => new Error(err));
186 | },`;
187 | };
188 |
189 | const manyToMany = (
190 | tableName,
191 | primaryKey,
192 | refTable,
193 | manyToManyTableRefKey,
194 | currentTableRefKey,
195 | manyToManyTable,
196 | manyToManyTablePKey
197 | ) => {
198 | return `
199 | ${toCamelCase(manyToManyTable)}: (${toCamelCase(tableName)}) => {
200 | const query = 'SELECT * FROM ${manyToManyTable} LEFT OUTER JOIN ${refTable} ON ${manyToManyTable}.${manyToManyTablePKey} = ${refTable}.${manyToManyTableRefKey} WHERE ${refTable}.${currentTableRefKey} = $1';
201 | const values = [${tableName}.${primaryKey}];
202 | return db.query(query, values)
203 | .then(data => data.rows)
204 | .catch(err => new Error(err));
205 | }, `;
206 | };
207 |
208 | const foreignKeyCheck = (
209 | tableName,
210 | primaryKey,
211 | refKey,
212 | fk,
213 | refTable,
214 | refTablePK
215 | ) => {
216 | return `
217 | ${toCamelCase(refTable)}: (${toCamelCase(tableName)}) => {
218 | const query = 'SELECT ${refTable}.* FROM ${refTable} LEFT OUTER JOIN ${tableName} ON ${refTable}.${refTablePK} = ${tableName}.${refKey} WHERE ${tableName}.${primaryKey} = $1';
219 | const values = [${tableName}.${primaryKey}];
220 | return db.query(query, values)
221 | .then(data => data.rows)
222 | .catch(err => new Error(err));
223 | }, `;
224 | };
225 |
226 | module.exports = {
227 | resolverFunc,
228 | isReferenceTable
229 | };
--------------------------------------------------------------------------------
/server/generator/schema.js:
--------------------------------------------------------------------------------
1 | const generateTypes = require('./generateTypes');
2 | const generateResolver = require('./generatorResolver')
3 | const { isReferenceTable } = require('./resolverFunc');
4 |
5 | const schema = {};
6 |
7 |
8 | schema.typeGenerator = (SQLtables) => {
9 | let queryType = '';
10 | let mutationType = '';
11 | let customType = '';
12 | for (const tableName in SQLtables) {
13 | const tableData = SQLtables[tableName];
14 | const { foreignKeys, columns } = tableData;
15 | if (!foreignKeys || !isReferenceTable(foreignKeys, columns)) {
16 | queryType += generateTypes.queries(tableName, tableData);
17 | mutationType += generateTypes.mutations(tableName, tableData);
18 | customType += generateTypes.custom(tableName, SQLtables);
19 | }
20 | }
21 | const types =
22 | `${'const typeDefs = `\n' + ' type Query {\n'}${queryType} }\n\n` +
23 | ` type Mutation {${mutationType} }\n\n` +
24 | `${customType}\`;\n\n`;
25 |
26 | return types;
27 | }
28 |
29 | schema.resolverGenerator = (SQLtables) => {
30 | let queryResolver = '';
31 | let mutationResolver = '';
32 | let customResolver = '';
33 | for (const tableName in SQLtables) {
34 | const tableData = SQLtables[tableName]
35 | const { foreignKeys, columns } = tableData;
36 | if (!foreignKeys || !isReferenceTable(foreignKeys, columns)) {
37 | console.log('2.1')
38 | queryResolver += generateResolver.queries(tableName, tableData);
39 | console.log('2.2')
40 | mutationResolver += generateResolver.mutations(tableName, tableData);
41 | console.log('2.3')
42 | customResolver += generateResolver.custom(tableName, SQLtables);
43 | }
44 | }
45 | const resolvers =
46 | '\n const resolvers = {\n' +
47 | ' Query: {' +
48 | ` ${queryResolver}\n` +
49 | ' },\n\n' +
50 | ' Mutation: {\n' +
51 | ` ${mutationResolver}\n` +
52 | ' },\n' +
53 | ` ${customResolver}\n }\n`;
54 |
55 | return resolvers;
56 | }
57 |
58 | module.exports = schema;
59 |
--------------------------------------------------------------------------------
/server/generator/testPSQL.js:
--------------------------------------------------------------------------------
1 | const URI = 'postgres://sugmjzbp:wvcZ6SvHu61w8qdmDYBb1uvdnqtLZ_82@suleiman.db.elephantsql.com:5432/sugmjzbp';
2 | const secret = 'metricql';
3 | module.exports = { URI, secret };
--------------------------------------------------------------------------------
/server/generator/typesFunc.js:
--------------------------------------------------------------------------------
1 | const toCamelCase = require('camelcase');
2 | const { pascalCase } = require('pascal-case');
3 | const { singular } = require('pluralize');
4 |
5 | const typesFunc = {};
6 |
7 | typesFunc.typeSet = (str) => {
8 | switch (str) {
9 | case 'character varying':
10 | return 'String';
11 | case 'character':
12 | return 'String';
13 | case 'integer':
14 | return 'Int';
15 | case 'text':
16 | return 'String';
17 | case 'date':
18 | return 'String';
19 | case 'boolean':
20 | return 'Boolean';
21 | default:
22 | return 'Int';
23 | }
24 | };
25 |
26 | const typeConversion = {
27 | 'character varying': 'String',
28 | character: 'String',
29 | integer: 'Int',
30 | text: 'String',
31 | date: 'String',
32 | boolean: 'Boolean',
33 | numeric: 'Int',
34 | };
35 |
36 | const paramType = (primaryKey, foreignKeys, columns, isRequired) => {
37 | let typeDef = '';
38 | for (const columnName in columns) {
39 | const { dataType, isNullable } = columns[columnName];
40 | if (!isRequired && columnName === primaryKey) {
41 | continue;
42 | }
43 |
44 | if (isRequired && columnName === primaryKey) {
45 | typeDef += ` ${columnName}: ${typeConversion[dataType] ? typeConversion[dataType] : 'Int'
46 | }!,\n`;
47 | }
48 | else {
49 | typeDef += ` ${columnName}: ${typeConversion[dataType] ? typeConversion[dataType] : 'Int'
50 | }`;
51 | if (isNullable !== 'YES') typeDef += '!';
52 | typeDef += ',\n';
53 | }
54 | }
55 | if (typeDef !== '') typeDef += ' ';
56 | return typeDef;
57 | };
58 |
59 | typesFunc.mutCreate = (tableName, primaryKey, foreignKeys, columns) => {
60 | return `\n ${toCamelCase(`create_${singular(tableName)}`
61 | )} (\n${paramType(
62 | primaryKey,
63 | foreignKeys,
64 | columns,
65 | false
66 | )}): ${pascalCase(singular(tableName))}!\n`
67 | };
68 |
69 | typesFunc.mutUpdate = (tableName, primaryKey, foreignKeys, columns) => {
70 | return `\n ${toCamelCase(
71 | `update_${singular(tableName)}`
72 | )}(\n${paramType(
73 | primaryKey,
74 | foreignKeys,
75 | columns,
76 | true
77 | )}): ${pascalCase(singular(tableName))}!\n`;
78 | };
79 |
80 | typesFunc.mutDelete = (tableName, primaryKey) => {
81 | return `\n ${toCamelCase(
82 | `delete_${singular(tableName)}`
83 | )}(${primaryKey}: ID!): ${pascalCase(singular(tableName))}!\n`;
84 | }
85 |
86 | typesFunc.getColumns = (primaryKey, foreignKeys, columns) => {
87 | let columnsStr = '';
88 | for (const columnName in columns) {
89 | if (!(foreignKeys && foreignKeys[columnName]) && columnName !== primaryKey) {
90 | const { dataType, isNullable, columnDefault } = columns[columnName];
91 | columnsStr += `\n ${columnName}: ${typeConversion[dataType] ? typeConversion[dataType] : 'Int'
92 | }`;
93 | if (isNullable === 'NO' && columnDefault === null) columnsStr += '!';
94 | }
95 | }
96 | return columnsStr;
97 | };
98 |
99 | typesFunc.getRelationships = (tableName, tables) => {
100 | let relationships = '';
101 | const alreadyAddedType = [];
102 | for (const refTableName in tables[tableName].referencedBy) {
103 | const {
104 | referencedBy: foreignRefBy,
105 | foreignKeys: foreignFKeys,
106 | columns: foreignColumns,
107 | } = tables[refTableName];
108 |
109 | if (foreignRefBy && foreignRefBy[tableName]) {
110 | if (!alreadyAddedType.includes(refTableName)) {
111 | alreadyAddedType.push(refTableName);
112 | const refTableType = pascalCase(singular(refTableName));
113 | relationships += `\n ${toCamelCase(
114 | singular(refTableName)
115 | )}: ${refTableType}`;
116 | }
117 | } else if (
118 | Object.keys(foreignColumns).length !==
119 | Object.keys(foreignFKeys).length + 1
120 | ) {
121 | if (!alreadyAddedType.includes(refTableName)) {
122 | alreadyAddedType.push(refTableName);
123 | const refTableType = pascalCase(singular(refTableName));
124 |
125 | relationships += `\n ${toCamelCase(
126 | refTableName
127 | )}: [${refTableType}]`;
128 | }
129 | }
130 |
131 | for (const foreignFKey in foreignFKeys) {
132 | if (tableName !== foreignFKeys[foreignFKey].referenceTable) {
133 | if (!alreadyAddedType.includes(refTableName)) {
134 | alreadyAddedType.push(refTableName);
135 | const manyToManyTable = toCamelCase(
136 | foreignFKeys[foreignFKey].referenceTable
137 | );
138 | relationships += `\n ${manyToManyTable}: [${pascalCase(
139 | singular(manyToManyTable)
140 | )}]`;
141 | }
142 | }
143 | }
144 | }
145 | for (const FKTableName in tables[tableName].foreignKeys) {
146 | const object = tables[tableName].foreignKeys[FKTableName];
147 | const refTableName = object.referenceTable;
148 | if (refTableName) {
149 | const refTableType = pascalCase(singular(refTableName));
150 | relationships += `\n ${toCamelCase(refTableName)}: [${refTableType}]`;
151 | }
152 | }
153 |
154 | return relationships;
155 | };
156 |
157 | module.exports = { typesFunc };
--------------------------------------------------------------------------------
/server/graphQLServer/schema.js:
--------------------------------------------------------------------------------
1 | const { makeExecutableSchema } = require('graphql-tools');
2 | const { Pool } = require('pg');
3 | const PG_URI = 'postgres://hgokvgqx:8y0x9A3vgaIFSSCZMLDieF-LgoWlh_mi@castor.db.elephantsql.com/hgokvgqx';
4 |
5 | const pool = new Pool({
6 | connectionString: PG_URI
7 | });
8 |
9 | const db = {};
10 | db.query = (text,params, callback) => {
11 | console.log('executed query:', text)
12 | return pool.query(text, params, callback)
13 | };
14 |
15 |
16 | const typeDefs = `
17 | type Query {
18 | people: [Person!]!
19 | person(_id: Int!): Person!
20 | films: [Film!]!
21 | film(_id: Int!): Film!
22 | planets: [Planet!]!
23 | planet(_id: Int!): Planet!
24 | species: [Species!]!
25 | speciesByID(_id: Int!): Species!
26 | vessels: [Vessel!]!
27 | vessel(_id: Int!): Vessel!
28 | starshipSpecs: [StarshipSpec!]!
29 | starshipSpec(_id: Int!): StarshipSpec!
30 | }
31 |
32 | type Mutation {
33 | createPerson (
34 | gender: String,
35 | species_id: Int,
36 | homeworld_id: Int,
37 | height: Int,
38 | mass: String,
39 | hair_color: String,
40 | skin_color: String,
41 | eye_color: String,
42 | name: String!,
43 | birth_year: String,
44 | ): Person!
45 |
46 | updatePerson(
47 | gender: String,
48 | species_id: Int,
49 | homeworld_id: Int,
50 | height: Int,
51 | _id: Int!,
52 | mass: String,
53 | hair_color: String,
54 | skin_color: String,
55 | eye_color: String,
56 | name: String!,
57 | birth_year: String,
58 | ): Person!
59 |
60 | deletePerson(_id: ID!): Person!
61 |
62 | createFilm (
63 | director: String!,
64 | opening_crawl: String!,
65 | episode_id: Int!,
66 | title: String!,
67 | release_date: String!,
68 | producer: String!,
69 | ): Film!
70 |
71 | updateFilm(
72 | director: String!,
73 | opening_crawl: String!,
74 | episode_id: Int!,
75 | _id: Int!,
76 | title: String!,
77 | release_date: String!,
78 | producer: String!,
79 | ): Film!
80 |
81 | deleteFilm(_id: ID!): Film!
82 |
83 | createPlanet (
84 | orbital_period: Int,
85 | climate: String,
86 | gravity: String,
87 | terrain: String,
88 | surface_water: String,
89 | population: Int,
90 | name: String,
91 | rotation_period: Int,
92 | diameter: Int,
93 | ): Planet!
94 |
95 | updatePlanet(
96 | orbital_period: Int,
97 | climate: String,
98 | gravity: String,
99 | terrain: String,
100 | surface_water: String,
101 | population: Int,
102 | _id: Int!,
103 | name: String,
104 | rotation_period: Int,
105 | diameter: Int,
106 | ): Planet!
107 |
108 | deletePlanet(_id: ID!): Planet!
109 |
110 | createSpecies (
111 | hair_colors: String,
112 | name: String!,
113 | classification: String,
114 | average_height: String,
115 | average_lifespan: String,
116 | skin_colors: String,
117 | eye_colors: String,
118 | language: String,
119 | homeworld_id: Int,
120 | ): Species!
121 |
122 | updateSpecies(
123 | hair_colors: String,
124 | name: String!,
125 | classification: String,
126 | average_height: String,
127 | average_lifespan: String,
128 | skin_colors: String,
129 | eye_colors: String,
130 | language: String,
131 | homeworld_id: Int,
132 | _id: Int!,
133 | ): Species!
134 |
135 | deleteSpecies(_id: ID!): Species!
136 |
137 | createVessel (
138 | cost_in_credits: Int,
139 | length: String,
140 | vessel_type: String!,
141 | model: String,
142 | manufacturer: String,
143 | name: String!,
144 | vessel_class: String!,
145 | max_atmosphering_speed: String,
146 | crew: Int,
147 | passengers: Int,
148 | cargo_capacity: String,
149 | consumables: String,
150 | ): Vessel!
151 |
152 | updateVessel(
153 | cost_in_credits: Int,
154 | length: String,
155 | vessel_type: String!,
156 | model: String,
157 | manufacturer: String,
158 | name: String!,
159 | vessel_class: String!,
160 | max_atmosphering_speed: String,
161 | crew: Int,
162 | passengers: Int,
163 | cargo_capacity: String,
164 | consumables: String,
165 | _id: Int!,
166 | ): Vessel!
167 |
168 | deleteVessel(_id: ID!): Vessel!
169 |
170 | createStarshipSpec (
171 | vessel_id: Int!,
172 | MGLT: String,
173 | hyperdrive_rating: String,
174 | ): StarshipSpec!
175 |
176 | updateStarshipSpec(
177 | _id: Int!,
178 | vessel_id: Int!,
179 | MGLT: String,
180 | hyperdrive_rating: String,
181 | ): StarshipSpec!
182 |
183 | deleteStarshipSpec(_id: ID!): StarshipSpec!
184 | }
185 |
186 | type Person {
187 | _id: Int
188 | gender: String
189 | height: Int
190 | mass: String
191 | hair_color: String
192 | skin_color: String
193 | eye_color: String
194 | name: String!
195 | birth_year: String
196 | films: [Film]
197 | vessels: [Vessel]
198 | species: [Species]
199 | planets: [Planet]
200 | }
201 |
202 | type Film {
203 | _id: Int
204 | director: String!
205 | opening_crawl: String!
206 | episode_id: Int!
207 | title: String!
208 | release_date: String!
209 | producer: String!
210 | planets: [Planet]
211 | people: [Person]
212 | vessels: [Vessel]
213 | species: [Species]
214 | }
215 |
216 | type Planet {
217 | _id: Int
218 | orbital_period: Int
219 | climate: String
220 | gravity: String
221 | terrain: String
222 | surface_water: String
223 | population: Int
224 | name: String
225 | rotation_period: Int
226 | diameter: Int
227 | films: [Film]
228 | species: [Species]
229 | people: [Person]
230 | }
231 |
232 | type Species {
233 | _id: Int
234 | hair_colors: String
235 | name: String!
236 | classification: String
237 | average_height: String
238 | average_lifespan: String
239 | skin_colors: String
240 | eye_colors: String
241 | language: String
242 | people: [Person]
243 | films: [Film]
244 | planets: [Planet]
245 | }
246 |
247 | type Vessel {
248 | _id: Int
249 | cost_in_credits: Int
250 | length: String
251 | vessel_type: String!
252 | model: String
253 | manufacturer: String
254 | name: String!
255 | vessel_class: String!
256 | max_atmosphering_speed: String
257 | crew: Int
258 | passengers: Int
259 | cargo_capacity: String
260 | consumables: String
261 | films: [Film]
262 | people: [Person]
263 | starshipSpecs: [StarshipSpec]
264 | }
265 |
266 | type StarshipSpec {
267 | _id: Int
268 | MGLT: String
269 | hyperdrive_rating: String
270 | vessels: [Vessel]
271 | }
272 |
273 | `;
274 |
275 |
276 |
277 | const resolvers = {
278 | Query: {
279 |
280 | person: (parent, args) => {
281 | const query = 'SELECT * FROM people WHERE _id = $1';
282 | const values = [args._id];
283 | return db.query(query, values)
284 | .then(data => data.rows[0])
285 | .catch(err => new Error(err));
286 | },
287 |
288 | people: () => {
289 | const query = 'SELECT * FROM people';
290 | return db.query(query)
291 | .then(data => data.rows)
292 | .catch(err => new Error(err));
293 | },
294 |
295 | film: (parent, args) => {
296 | const query = 'SELECT * FROM films WHERE _id = $1';
297 | const values = [args._id];
298 | return db.query(query, values)
299 | .then(data => data.rows[0])
300 | .catch(err => new Error(err));
301 | },
302 |
303 | films: () => {
304 | const query = 'SELECT * FROM films';
305 | return db.query(query)
306 | .then(data => data.rows)
307 | .catch(err => new Error(err));
308 | },
309 |
310 | planet: (parent, args) => {
311 | const query = 'SELECT * FROM planets WHERE _id = $1';
312 | const values = [args._id];
313 | return db.query(query, values)
314 | .then(data => data.rows[0])
315 | .catch(err => new Error(err));
316 | },
317 |
318 | planets: () => {
319 | const query = 'SELECT * FROM planets';
320 | return db.query(query)
321 | .then(data => data.rows)
322 | .catch(err => new Error(err));
323 | },
324 |
325 | speciesByID: (parent, args) => {
326 | const query = 'SELECT * FROM species WHERE _id = $1';
327 | const values = [args._id];
328 | return db.query(query, values)
329 | .then(data => data.rows[0])
330 | .catch(err => new Error(err));
331 | },
332 |
333 | species: () => {
334 | const query = 'SELECT * FROM species';
335 | return db.query(query)
336 | .then(data => data.rows)
337 | .catch(err => new Error(err));
338 | },
339 |
340 | vessel: (parent, args) => {
341 | const query = 'SELECT * FROM vessels WHERE _id = $1';
342 | const values = [args._id];
343 | return db.query(query, values)
344 | .then(data => data.rows[0])
345 | .catch(err => new Error(err));
346 | },
347 |
348 | vessels: () => {
349 | const query = 'SELECT * FROM vessels';
350 | return db.query(query)
351 | .then(data => data.rows)
352 | .catch(err => new Error(err));
353 | },
354 |
355 | starshipSpec: (parent, args) => {
356 | const query = 'SELECT * FROM starship_specs WHERE _id = $1';
357 | const values = [args._id];
358 | return db.query(query, values)
359 | .then(data => data.rows[0])
360 | .catch(err => new Error(err));
361 | },
362 |
363 | starshipSpecs: () => {
364 | const query = 'SELECT * FROM starship_specs';
365 | return db.query(query)
366 | .then(data => data.rows)
367 | .catch(err => new Error(err));
368 | },
369 | },
370 |
371 | Mutation: {
372 |
373 | createPerson: (parent, args) => {
374 | const query = 'INSERT INTO people(gender, species_id, homeworld_id, height, mass, hair_color, skin_color, eye_color, name, birth_year) VALUES($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING *';
375 | const values = [args.gender, args.species_id, args.homeworld_id, args.height, args.mass, args.hair_color, args.skin_color, args.eye_color, args.name, args.birth_year];
376 | return db.query(query, values)
377 | .then(data => data.rows[0])
378 | .catch(err => new Error(err));
379 | },
380 |
381 | updatePerson: (parent, args) => {
382 | const query = 'UPDATE people SET gender=$1, species_id=$2, homeworld_id=$3, height=$4, mass=$5, hair_color=$6, skin_color=$7, eye_color=$8, name=$9, birth_year=$10 WHERE _id = $11 RETURNING *';
383 | const values = [args.gender, args.species_id, args.homeworld_id, args.height, args.mass, args.hair_color, args.skin_color, args.eye_color, args.name, args.birth_year, args._id];
384 | return db.query(query, values)
385 | .then(data => data.rows[0])
386 | .catch(err => new Error(err));
387 | },
388 |
389 | deletePerson: (parent, args) => {
390 | const query = 'DELETE FROM people WHERE _id = $1 RETURNING *';
391 | const values = [args._id];
392 | return db.query(query, values)
393 | .then(data => data.rows[0])
394 | .catch(err => new Error(err));
395 | },
396 |
397 | createFilm: (parent, args) => {
398 | const query = 'INSERT INTO films(director, opening_crawl, episode_id, title, release_date, producer) VALUES($1, $2, $3, $4, $5, $6) RETURNING *';
399 | const values = [args.director, args.opening_crawl, args.episode_id, args.title, args.release_date, args.producer];
400 | return db.query(query, values)
401 | .then(data => data.rows[0])
402 | .catch(err => new Error(err));
403 | },
404 |
405 | updateFilm: (parent, args) => {
406 | const query = 'UPDATE films SET director=$1, opening_crawl=$2, episode_id=$3, title=$4, release_date=$5, producer=$6 WHERE _id = $7 RETURNING *';
407 | const values = [args.director, args.opening_crawl, args.episode_id, args.title, args.release_date, args.producer, args._id];
408 | return db.query(query, values)
409 | .then(data => data.rows[0])
410 | .catch(err => new Error(err));
411 | },
412 |
413 | deleteFilm: (parent, args) => {
414 | const query = 'DELETE FROM films WHERE _id = $1 RETURNING *';
415 | const values = [args._id];
416 | return db.query(query, values)
417 | .then(data => data.rows[0])
418 | .catch(err => new Error(err));
419 | },
420 |
421 | createPlanet: (parent, args) => {
422 | const query = 'INSERT INTO planets(orbital_period, climate, gravity, terrain, surface_water, population, name, rotation_period, diameter) VALUES($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING *';
423 | const values = [args.orbital_period, args.climate, args.gravity, args.terrain, args.surface_water, args.population, args.name, args.rotation_period, args.diameter];
424 | return db.query(query, values)
425 | .then(data => data.rows[0])
426 | .catch(err => new Error(err));
427 | },
428 |
429 | updatePlanet: (parent, args) => {
430 | const query = 'UPDATE planets SET orbital_period=$1, climate=$2, gravity=$3, terrain=$4, surface_water=$5, population=$6, name=$7, rotation_period=$8, diameter=$9 WHERE _id = $10 RETURNING *';
431 | const values = [args.orbital_period, args.climate, args.gravity, args.terrain, args.surface_water, args.population, args.name, args.rotation_period, args.diameter, args._id];
432 | return db.query(query, values)
433 | .then(data => data.rows[0])
434 | .catch(err => new Error(err));
435 | },
436 |
437 | deletePlanet: (parent, args) => {
438 | const query = 'DELETE FROM planets WHERE _id = $1 RETURNING *';
439 | const values = [args._id];
440 | return db.query(query, values)
441 | .then(data => data.rows[0])
442 | .catch(err => new Error(err));
443 | },
444 |
445 | createSpecies: (parent, args) => {
446 | const query = 'INSERT INTO species(hair_colors, name, classification, average_height, average_lifespan, skin_colors, eye_colors, language, homeworld_id) VALUES($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING *';
447 | const values = [args.hair_colors, args.name, args.classification, args.average_height, args.average_lifespan, args.skin_colors, args.eye_colors, args.language, args.homeworld_id];
448 | return db.query(query, values)
449 | .then(data => data.rows[0])
450 | .catch(err => new Error(err));
451 | },
452 |
453 | updateSpecies: (parent, args) => {
454 | const query = 'UPDATE species SET hair_colors=$1, name=$2, classification=$3, average_height=$4, average_lifespan=$5, skin_colors=$6, eye_colors=$7, language=$8, homeworld_id=$9 WHERE _id = $10 RETURNING *';
455 | const values = [args.hair_colors, args.name, args.classification, args.average_height, args.average_lifespan, args.skin_colors, args.eye_colors, args.language, args.homeworld_id, args._id];
456 | return db.query(query, values)
457 | .then(data => data.rows[0])
458 | .catch(err => new Error(err));
459 | },
460 |
461 | deleteSpecies: (parent, args) => {
462 | const query = 'DELETE FROM species WHERE _id = $1 RETURNING *';
463 | const values = [args._id];
464 | return db.query(query, values)
465 | .then(data => data.rows[0])
466 | .catch(err => new Error(err));
467 | },
468 |
469 | createVessel: (parent, args) => {
470 | const query = 'INSERT INTO vessels(cost_in_credits, length, vessel_type, model, manufacturer, name, vessel_class, max_atmosphering_speed, crew, passengers, cargo_capacity, consumables) VALUES($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING *';
471 | const values = [args.cost_in_credits, args.length, args.vessel_type, args.model, args.manufacturer, args.name, args.vessel_class, args.max_atmosphering_speed, args.crew, args.passengers, args.cargo_capacity, args.consumables];
472 | return db.query(query, values)
473 | .then(data => data.rows[0])
474 | .catch(err => new Error(err));
475 | },
476 |
477 | updateVessel: (parent, args) => {
478 | const query = 'UPDATE vessels SET cost_in_credits=$1, length=$2, vessel_type=$3, model=$4, manufacturer=$5, name=$6, vessel_class=$7, max_atmosphering_speed=$8, crew=$9, passengers=$10, cargo_capacity=$11, consumables=$12 WHERE _id = $13 RETURNING *';
479 | const values = [args.cost_in_credits, args.length, args.vessel_type, args.model, args.manufacturer, args.name, args.vessel_class, args.max_atmosphering_speed, args.crew, args.passengers, args.cargo_capacity, args.consumables, args._id];
480 | return db.query(query, values)
481 | .then(data => data.rows[0])
482 | .catch(err => new Error(err));
483 | },
484 |
485 | deleteVessel: (parent, args) => {
486 | const query = 'DELETE FROM vessels WHERE _id = $1 RETURNING *';
487 | const values = [args._id];
488 | return db.query(query, values)
489 | .then(data => data.rows[0])
490 | .catch(err => new Error(err));
491 | },
492 |
493 | createStarshipSpec: (parent, args) => {
494 | const query = 'INSERT INTO starship_specs(vessel_id, MGLT, hyperdrive_rating) VALUES($1, $2, $3) RETURNING *';
495 | const values = [args.vessel_id, args.MGLT, args.hyperdrive_rating];
496 | return db.query(query, values)
497 | .then(data => data.rows[0])
498 | .catch(err => new Error(err));
499 | },
500 |
501 | updateStarshipSpec: (parent, args) => {
502 | const query = 'UPDATE starship_specs SET vessel_id=$1, MGLT=$2, hyperdrive_rating=$3 WHERE _id = $4 RETURNING *';
503 | const values = [args.vessel_id, args.MGLT, args.hyperdrive_rating, args._id];
504 | return db.query(query, values)
505 | .then(data => data.rows[0])
506 | .catch(err => new Error(err));
507 | },
508 |
509 | deleteStarshipSpec: (parent, args) => {
510 | const query = 'DELETE FROM starship_specs WHERE _id = $1 RETURNING *';
511 | const values = [args._id];
512 | return db.query(query, values)
513 | .then(data => data.rows[0])
514 | .catch(err => new Error(err));
515 | },
516 |
517 | },
518 |
519 | Person: {
520 |
521 | films: (people) => {
522 | const query = 'SELECT * FROM films LEFT OUTER JOIN people_in_films ON films._id = people_in_films.film_id WHERE people_in_films.person_id = $1';
523 | const values = [people._id];
524 | return db.query(query, values)
525 | .then(data => data.rows)
526 | .catch(err => new Error(err));
527 | },
528 | species: (people) => {
529 | const query = 'SELECT species.* FROM species LEFT OUTER JOIN people ON species._id = people._id WHERE people._id = $1';
530 | const values = [people._id];
531 | return db.query(query, values)
532 | .then(data => data.rows)
533 | .catch(err => new Error(err));
534 | },
535 | planets: (people) => {
536 | const query = 'SELECT planets.* FROM planets LEFT OUTER JOIN people ON planets._id = people._id WHERE people._id = $1';
537 | const values = [people._id];
538 | return db.query(query, values)
539 | .then(data => data.rows)
540 | .catch(err => new Error(err));
541 | },
542 | vessels: (people) => {
543 | const query = 'SELECT * FROM vessels LEFT OUTER JOIN pilots ON vessels._id = pilots.vessel_id WHERE pilots.person_id = $1';
544 | const values = [people._id];
545 | return db.query(query, values)
546 | .then(data => data.rows)
547 | .catch(err => new Error(err));
548 | },
549 | },
550 |
551 | Film: {
552 |
553 | planets: (films) => {
554 | const query = 'SELECT * FROM planets LEFT OUTER JOIN planets_in_films ON planets._id = planets_in_films.planet_id WHERE planets_in_films.film_id = $1';
555 | const values = [films._id];
556 | return db.query(query, values)
557 | .then(data => data.rows)
558 | .catch(err => new Error(err));
559 | },
560 | people: (films) => {
561 | const query = 'SELECT * FROM people LEFT OUTER JOIN people_in_films ON people._id = people_in_films.person_id WHERE people_in_films.film_id = $1';
562 | const values = [films._id];
563 | return db.query(query, values)
564 | .then(data => data.rows)
565 | .catch(err => new Error(err));
566 | },
567 | vessels: (films) => {
568 | const query = 'SELECT * FROM vessels LEFT OUTER JOIN vessels_in_films ON vessels._id = vessels_in_films.vessel_id WHERE vessels_in_films.film_id = $1';
569 | const values = [films._id];
570 | return db.query(query, values)
571 | .then(data => data.rows)
572 | .catch(err => new Error(err));
573 | },
574 | species: (films) => {
575 | const query = 'SELECT * FROM species LEFT OUTER JOIN species_in_films ON species._id = species_in_films.species_id WHERE species_in_films.film_id = $1';
576 | const values = [films._id];
577 | return db.query(query, values)
578 | .then(data => data.rows)
579 | .catch(err => new Error(err));
580 | },
581 | },
582 |
583 | Planet: {
584 |
585 | films: (planets) => {
586 | const query = 'SELECT * FROM films LEFT OUTER JOIN planets_in_films ON films._id = planets_in_films.film_id WHERE planets_in_films.planet_id = $1';
587 | const values = [planets._id];
588 | return db.query(query, values)
589 | .then(data => data.rows)
590 | .catch(err => new Error(err));
591 | },
592 | species: (planets) => {
593 | const query = 'SELECT * FROM species WHERE homeworld_id $1';
594 | const values = [planets._id];
595 | return db.query(query, values)
596 | .then(data => data.rows)
597 | .catch(err => new Error(err));
598 | },
599 | people: (planets) => {
600 | const query = 'SELECT * FROM people WHERE homeworld_id $1';
601 | const values = [planets._id];
602 | return db.query(query, values)
603 | .then(data => data.rows)
604 | .catch(err => new Error(err));
605 | },
606 | },
607 |
608 | Species: {
609 |
610 | people: (species) => {
611 | const query = 'SELECT * FROM people WHERE species_id $1';
612 | const values = [species._id];
613 | return db.query(query, values)
614 | .then(data => data.rows)
615 | .catch(err => new Error(err));
616 | },
617 | films: (species) => {
618 | const query = 'SELECT * FROM films LEFT OUTER JOIN species_in_films ON films._id = species_in_films.film_id WHERE species_in_films.species_id = $1';
619 | const values = [species._id];
620 | return db.query(query, values)
621 | .then(data => data.rows)
622 | .catch(err => new Error(err));
623 | },
624 | planets: (species) => {
625 | const query = 'SELECT planets.* FROM planets LEFT OUTER JOIN species ON planets._id = species._id WHERE species._id = $1';
626 | const values = [species._id];
627 | return db.query(query, values)
628 | .then(data => data.rows)
629 | .catch(err => new Error(err));
630 | },
631 | },
632 |
633 | Vessel: {
634 |
635 | films: (vessels) => {
636 | const query = 'SELECT * FROM films LEFT OUTER JOIN vessels_in_films ON films._id = vessels_in_films.film_id WHERE vessels_in_films.vessel_id = $1';
637 | const values = [vessels._id];
638 | return db.query(query, values)
639 | .then(data => data.rows)
640 | .catch(err => new Error(err));
641 | },
642 | people: (vessels) => {
643 | const query = 'SELECT * FROM people LEFT OUTER JOIN pilots ON people._id = pilots.person_id WHERE pilots.vessel_id = $1';
644 | const values = [vessels._id];
645 | return db.query(query, values)
646 | .then(data => data.rows)
647 | .catch(err => new Error(err));
648 | },
649 | starshipSpecs: (vessels) => {
650 | const query = 'SELECT * FROM starship_specs WHERE vessel_id $1';
651 | const values = [vessels._id];
652 | return db.query(query, values)
653 | .then(data => data.rows)
654 | .catch(err => new Error(err));
655 | },
656 | },
657 |
658 | }
659 |
660 | const schema = makeExecutableSchema({
661 | typeDefs,
662 | resolvers,
663 | });
664 |
665 | module.exports = schema;
--------------------------------------------------------------------------------
/server/query/tables.sql:
--------------------------------------------------------------------------------
1 | SELECT json_object_agg( ----------> creates a json object; accepts 2 args; 1st is key, 2nd is value
2 | pk.table_name, json_build_object( --------> creates a json object; accepts variable # args; matches up key, then value as pair
3 | 'primaryKey', pk.primary_key,
4 | 'foreignKeys', fk.foreign_keys,
5 | 'referencedBy', rd.referenced_by,
6 | 'columns', td.columns
7 | )
8 | ) AS tables
9 |
10 | FROM ( -- Primary key data (pk)
11 |
12 | ---------------------------------------------------------------------------
13 | SELECT conrelid::regclass AS table_name, -- regclass will turn conrelid to actual table name
14 | substring(pg_get_constraintdef(oid), '\((.*?)\)') AS primary_key --(.*?) matches any character (except for line terminators))
15 | FROM pg_constraint ---- The catalog pg_constraint stores check, primary key, unique, and foreign key constraints on tables -- https://www.postgresql.org/docs/8.2/catalog-pg-constraint.html
16 | WHERE contype = 'p' AND connamespace = 'public'::regnamespace -- regnamespace will turn connamespace(number) to actual name space
17 | ---------------------------------------------------------------------------
18 | ) AS pk
19 |
20 | LEFT OUTER JOIN ( -- Foreign key data (fk)
21 | ---------------------------------------------------------------------------------------
22 | SELECT conrelid::regclass AS table_name,
23 | json_object_agg(
24 | substring(pg_get_constraintdef(oid), '\((.*?)\)'), json_build_object(
25 | 'referenceTable', substring(pg_get_constraintdef(oid), 'REFERENCES (.*?)\('),
26 | 'referenceKey', substring(pg_get_constraintdef(oid), 'REFERENCES.*?\((.*?)\)')
27 | )
28 | ) AS foreign_keys
29 | FROM pg_constraint
30 | WHERE contype = 'f' AND connamespace = 'public'::regnamespace
31 | GROUP BY table_name
32 | ---------------------------------------------------------------------------------------
33 | ) AS fk
34 | ON pk.table_name = fk.table_name
35 |
36 | LEFT OUTER JOIN ( -- Reference data (rd)
37 | ---------------------------------------------------------------------------------------------------
38 | SELECT substring(pg_get_constraintdef(oid), 'REFERENCES (.*?)\(') AS table_name, json_object_agg(
39 | conrelid::regclass, substring(pg_get_constraintdef(oid), '\((.*?)\)')
40 | ) AS referenced_by
41 | FROM pg_constraint
42 | WHERE contype = 'f' AND connamespace = 'public'::regnamespace
43 | GROUP BY table_name
44 | ---------------------------------------------------------------------------------------------------
45 | ) AS rd
46 | ON pk.table_name::regclass = rd.table_name::regclass
47 |
48 | LEFT OUTER JOIN ( -- Table data (td)
49 | -----------------------------------------------------------------
50 | SELECT tab.table_name, json_object_agg(
51 | col.column_name, json_build_object(
52 | 'dataType', col.data_type,
53 | 'columnDefault', col.column_default,
54 | 'charMaxLength', col.character_maximum_length,
55 | 'isNullable', col.is_nullable
56 | )
57 | ) AS columns
58 |
59 | -- Table names
60 | FROM (
61 | SELECT table_name FROM information_schema.tables --------- built-in; lists of all the tables in a selected database --- https://www.sqlshack.com/learn-sql-the-information_schema-database/
62 | WHERE table_type='BASE TABLE' AND table_schema='public'
63 | ) AS tab
64 |
65 | -- Table columns
66 | INNER JOIN information_schema.columns AS col
67 | ON tab.table_name = col.table_name
68 | GROUP BY tab.table_name
69 | -----------------------------------------------------------------
70 | ) AS td
71 | ON td.table_name::regclass = pk.table_name
72 |
--------------------------------------------------------------------------------
/server/router.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const router = express.Router();
3 | const postgreSQLController = require('./controllers/postgreSQLController');
4 |
5 | router.post('/schema',
6 | postgreSQLController.table,
7 | postgreSQLController.schemaGenerator,
8 | postgreSQLController.writeSchemaToFile,
9 | postgreSQLController.d3JSONGenerator,
10 | (req, res) => {
11 | res.status(200).json({schema: res.locals.schema, visuals: res.locals.d3JSON})
12 | }
13 | )
14 |
15 | module.exports = router;
--------------------------------------------------------------------------------
/server/server.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const cors = require('cors');
3 | const path = require('path');
4 | const router = require('./router')
5 | const { graphqlHTTP } = require('express-graphql')
6 | const schema = require('./graphQLServer/schema');
7 | const session = require('express-session');
8 |
9 | const app = express();
10 | const PORT = 8080 || process.env.PORT;
11 |
12 | app.use(cors());
13 |
14 | //initializing session for user
15 | app.use(session({
16 | secret: 'secret',
17 | resave: true,
18 | //prevents a new session when a new proprety is added
19 | saveUninitialized: false,
20 | }))
21 |
22 | app.use(express.json());
23 | app.use(express.urlencoded({ extended: true }));
24 |
25 | app.use('/graphql',
26 | graphqlHTTP({
27 | schema ,
28 | graphiql: true,
29 | })
30 | );
31 |
32 | // app.use('/auth', authRouter);
33 |
34 | app.use('/', router)
35 |
36 | app.use('*', (req, res) => res.status(404).send('Wrong Page, something is not right'));
37 |
38 | app.use((err, req, res, next) => {
39 | console.log(err)
40 | const defaultErr = {
41 | log: 'Express error handler in unknown middleware error',
42 | status: 500,
43 | message: { err: 'An error occurred' },
44 | };
45 | const errorObj = Object.assign({}, defaultErr, err);
46 | return res.status(errorObj.status).json(errorObj.message);
47 | });
48 |
49 |
50 | app.listen(PORT, () => {
51 | console.log(`Servers listening on ${PORT}`);
52 | });
53 |
54 | module.exports = app;
--------------------------------------------------------------------------------
/styles/SideBar.module.css:
--------------------------------------------------------------------------------
1 |
2 | @import url('https://fonts.googleapis.com/css?family=Manjari:100,400,700&display=swap');
3 |
4 |
5 | .sidebar {
6 | background-color: rgba(37, 187, 34, 0.01);
7 | width: 60px;
8 | height: 100vh;
9 | display: flex;
10 | justify-content: center;
11 | position: fixed;
12 | z-index: 999;
13 | left: 0;
14 | top: 0;
15 | bottom: 0;
16 | border-radius: 5px;
17 | }
18 |
19 | .mainMenu {
20 | margin-block-start: 0;
21 | margin-block-end: 0;
22 | margin-inline-start: 0px;
23 | margin-inline-end: 0px;
24 | padding-inline-start: 0;
25 | display: flex;
26 | flex-direction: column;
27 | align-items: center;
28 | }
29 |
30 | .menuItem {
31 | list-style: none;
32 | position: relative;
33 | width: 85px;
34 | height: 65px;
35 | margin-bottom: 80px;
36 | }
37 |
38 | .menuItemOne {
39 | list-style: none;
40 | position: relative;
41 | width: 85px;
42 | height: 65px;
43 | margin-top: 50px;
44 | margin-bottom: 80px;
45 | }
46 |
47 |
48 | .menuTxt {
49 | opacity: 0;
50 | width: 0px;
51 | min-width: 0px;
52 | overflow: hidden;
53 | transition: 300ms linear;
54 | transition-delay: 250ms;
55 | display: flex;
56 | align-items: center;
57 | position: relative;
58 | top: 2px;
59 | white-space: nowrap;
60 | overflow: hidden;
61 | }
62 |
63 | .menuA {
64 | padding: 35px 33.5px;
65 | display: flex;
66 | align-items: center;
67 | justify-content: center;
68 | text-decoration: none;
69 | }
70 |
71 | .menuIcon {
72 | display: block;
73 | font-size: 1.5rem;
74 | color: #bdbdbd;
75 | position: relative;
76 | z-index: 100;
77 | transition: 400ms;
78 | }
79 |
80 | .menuItem:hover .menuTxt {
81 | opacity: 1;
82 | width: 100%;
83 | min-width: 40px;
84 | padding: 0px 20px;
85 | transition-delay: 0s;
86 | color: #979595;
87 | }
88 |
89 | .menuTxtHld {
90 | position: absolute;
91 | z-index: 99;
92 | background: rgba(255, 255, 255, 0);
93 | border: 1px solid rgba(0, 0, 0, 0);
94 | border-radius: 38px;
95 | font-size: .94rem;
96 | box-shadow: 0px 0px 8px rgba(0, 0, 0, 0);
97 | padding: 9.5px 8px;
98 | transition: 250ms linear;
99 | display: flex;
100 | align-items: center;
101 | left: 20px;
102 | transition-delay: 300ms;
103 | }
104 |
105 | .menuItem:hover .menuTxtHld {
106 | box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.07);
107 | background: #5304EE;
108 | border: 1px solid rgba(0, 0, 0, 0.05);
109 | transition-delay: 0ms;
110 | }
111 | .active .menuIcon {
112 | color: #03A9F4;
113 | }
--------------------------------------------------------------------------------
/styles/URILink.module.css:
--------------------------------------------------------------------------------
1 | .modal {
2 | display: flex;
3 | flex-direction: row;
4 | justify-content: center;
5 | }
6 |
7 | .InputURI {
8 | width: 90%;
9 | };
--------------------------------------------------------------------------------
/styles/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | content: [
3 | "./pages/**/*.{js,ts,jsx,tsx}",
4 | "./client/components/**/*.{js,ts,jsx,tsx}",
5 | ],
6 | theme: {
7 | screens: {
8 | sm: '640px',
9 | md: '768px',
10 | lg: '1024px',
11 | xl: '1280px',
12 | "2xl": "1550px",
13 | },
14 | extend: {
15 | colors: {
16 | 'primary': 'rgb(231, 234, 246)',
17 | 'header': '#a2a8d3',
18 | 'purple': '#6415FF',
19 | 'purple1': '#5304EE',
20 | 'purple2': '#4102bd',
21 | 'gray': '#1a202c',
22 | 'secondary': '#7c8ba1',
23 | 'blue': '#2a4365',
24 | 'dark2': '#1D1127',
25 | 'dark3': '#130918',
26 | 'dark1': '#2B1D38',
27 | 'white1': 'rgb(207, 204, 204)',
28 | 'backgroundGrey': '#030812',
29 | 'sidebarGrey': '#2a2e38',
30 | 'navBarGrey': '#0d131f',
31 | 'historyMedGrey': '#282C34',
32 | 'darkGrey': '#141f33',
33 | }
34 | },
35 | },
36 | plugins: [],
37 | }
--------------------------------------------------------------------------------