├── .babelrc ├── .eslintrc.json ├── .gitignore ├── .nebulis.json ├── .prettierignore ├── .prettierrc ├── .travis.yml ├── Dockerfile ├── Dockerfile-dev ├── Dockerrun.aws.json ├── LICENSE ├── README.md ├── __mocks__ ├── fileMock.js └── styleMock.js ├── __tests__ ├── enzyme.js ├── puppeteer.js └── server-test.js ├── client ├── App.tsx ├── UI.tsx ├── components │ ├── Sidebar.tsx │ ├── VisGraph.tsx │ ├── codebox.tsx │ ├── link-popup │ │ ├── Form.tsx │ │ ├── FormMySQL.tsx │ │ ├── LinkContainer.tsx │ │ └── Modal.tsx │ └── nav-bars │ │ ├── Footer.tsx │ │ └── TopNav.tsx ├── downloadHelperFunctions │ ├── connectToDB.js │ ├── packagejsonFile.js │ ├── schemaFile.js │ └── serverFile.js ├── forceGraph │ ├── ForceGraph.jsx │ ├── deleteIcon.svg │ └── generators │ │ ├── ForceGraphGenerator.js │ │ ├── d3DataBuilder.js │ │ ├── deleteFunctions.js │ │ ├── helperFunctions.js │ │ ├── resolverGenerator.js │ │ ├── schemaGenerator.js │ │ └── typeGenerator.js ├── index.html ├── index.tsx ├── styles.css └── test.html ├── docker-compose-test.yml ├── jest-setup.js ├── jest-teardown.js ├── package-lock.json ├── package.json ├── public ├── icon │ ├── android-chrome-192x192.png │ ├── android-chrome-384x384.png │ ├── apple-touch-icon.png │ ├── browserconfig.xml │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── mstile-150x150.png │ ├── robots.txt │ ├── safari-pinned-tab.svg │ └── site.webmanifest ├── logo-for-github.jpg ├── lucidQL-logo.png └── lucidQL.png ├── scripts └── deploy.sh ├── server ├── SDL-definedSchemas │ ├── controllers │ │ ├── mySQLController.js │ │ └── pgController.js │ ├── generators │ │ ├── resolverGenerator.js │ │ ├── schemaGenerator.js │ │ └── typeGenerator.js │ └── helpers │ │ └── helperFunctions.js ├── dummy_server │ ├── connectToDB.js │ └── schema.js ├── queries │ └── tableData.sql ├── routes │ ├── mySQLRoute.js │ └── pgRoute.js └── server.js ├── tsconfig.json ├── tslint.json └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-typescript", "@babel/preset-react"], 3 | "plugins": [ 4 | "@babel/plugin-proposal-class-properties", 5 | "@babel/plugin-proposal-object-rest-spread", 6 | "@babel/transform-async-to-generator", 7 | "@babel/plugin-transform-runtime" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["airbnb", "prettier"], 3 | "plugins": ["prettier"], 4 | "rules": { 5 | "prettier/prettier": ["error"] 6 | }, 7 | "settings": { 8 | "import/resolver": { 9 | "node": { 10 | "extensions": [".js", ".jsx", ".ts", ".tsx"] 11 | } 12 | } 13 | }, 14 | "env": { 15 | "jest": true 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | .DS_Store 4 | coverage/ 5 | build -------------------------------------------------------------------------------- /.nebulis.json: -------------------------------------------------------------------------------- 1 | { 2 | "server": "192.168.0.8", 3 | "port": 1337 4 | } 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | build 2 | resolverGenerator.js -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 125, 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | SERVICES: 2 | - docker 3 | DIST: xenial 4 | 5 | SCRIPT: 6 | - docker-compose -f docker-compose-test.yml up --abort-on-container-exit 7 | - docker run -p 8081:8081 dockerizedgraphql/cql-prod --abort-on-container-exit 8 | - python3 -VV 9 | - pip -V 10 | BEFORE_DEPLOY: 11 | # install the aws cli 12 | - python3 -m pip install --user awscli 13 | # install the elastic beanstalk cli 14 | - python3 -m pip install --user awsebcli 15 | # Append exe location to our PATH 16 | - export PATH=$PATH:$HOME/.local/bin 17 | ENV: 18 | GLOBAL: 19 | - PATH=/opt/python/3.7.1/bin:$PATH 20 | DEPLOY: 21 | PROVIDER: script 22 | SKIP_CLEANUP: true 23 | ON: master 24 | SCRIPT: sh $TRAVIS_BUILD_DIR/scripts/deploy.sh 25 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12.18.3 2 | 3 | WORKDIR /usr/src/app 4 | 5 | COPY . /usr/src/app 6 | 7 | RUN npm install 8 | 9 | RUN npm run build 10 | 11 | EXPOSE 8081 12 | 13 | ENTRYPOINT [ "npm", "start" ] -------------------------------------------------------------------------------- /Dockerfile-dev: -------------------------------------------------------------------------------- 1 | FROM node:12.18.3 2 | 3 | RUN npm install --global jest 4 | 5 | WORKDIR /usr/src/app 6 | 7 | COPY . /usr/src/app 8 | 9 | RUN npm install 10 | 11 | RUN npm run build 12 | 13 | EXPOSE 8081 14 | 15 | ENTRYPOINT [ "npm", "test" ] 16 | -------------------------------------------------------------------------------- /Dockerrun.aws.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSEBDockerrunVersion": "1", 3 | "Image": { 4 | "Name": "944451176696.dkr.ecr.us-east-1.amazonaws.com/cql:", 5 | "Update": "true" 6 | }, 7 | "Ports": [{ 8 | "ContainerPort": "3000" 9 | }] 10 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 OSLabs Beta/lucidQL 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 | ![Image of Logo](/public/logo-for-github.jpg) 2 | 3 | # 4 | 5 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/oslabs-beta/LucidQL/blob/master/LICENSE) ![GitHub package.json version](https://img.shields.io/github/package-json/v/oslabs-beta/LucidQL?color=blue) [![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](https://github.com/oslabs-beta/LucidQL/issues) 6 | 7 | ### What is lucidQL? 8 | 9 | lucidQL is an open-source tool to assist developers in the migration from a legacy RESTful API to GraphQL. It also serves as a visualizer tool for your GraphQL API. lucidQL allows the user to view the GraphQL schema layout and relationships between data. 10 | 11 | Check out the tool - www.lucidQL.com 12 | 13 | ### How does lucidQL work? 14 | 15 | First choose your desired Postgres database that you want to use to create a graphQL API, and enter the link where prompted, and click submit. lucidQL will immediately start by reading your database, extracting all the tables, and their relationships, and generate a GraphQL schema. 16 | 17 | ![Image of Entering Link](https://media.giphy.com/media/1N6nX99joOh2zUGkAw/giphy.gif) 18 | 19 | A visual representation will display on the left side of the tool, users can analyze their current database and their table relationships. 20 | 21 | The lucidQL tool allows the user to simplify their schema by being able to delete any relationships that they no longer require. 22 | 23 | - If any tables are undesired in the final product, simply drag a table to the garbage icon, lucidQL will handle the rest. 24 | - The GraphQL schemas will be regenerated accordingly to your changes. 25 | - if you make a mistake, simply click the "undo" button. 26 | 27 | ![Image of Dragging to Garbage](https://media.giphy.com/media/9NEeXDUayldkGkok4k/giphy.gif) 28 | 29 | ### How to Test Your Schema, Resolvers and Mutations 30 | 31 | #### Option A: Enter your Postgres URI and start testing your GraphQL API right away! 32 | 33 | The lucidQL tool comes pre-packaged with a backend, which enables the user to access GraphQL playground and start querying your new API, immediately. After entering a Postgres URI. The user will just have to click on "GraphQL PLayground" which can be accessed through the side menu bar. 34 | 35 | 36 | 37 | #### Option B: Download your package by clicking on the "Download" button 38 | 39 | 1. Download and Save 40 | 2. Unzip package 41 | 3. Open directory 42 | 4. Install dependencies: `npm install` 43 | 5. Run the application: `npm start` 44 | 6. Once the application is running, enter localhost:3000/playground in your browser to start querying your database 45 | 7. Test and Query! 46 | 47 | The downloaded package includes the following - 48 | 49 | - A connection file connects your Postgres API to your server. 50 | - The server file sets up your server and includes playground so that you can query the information from your API as needed. 51 | - Lastly, your schema file will provide you with queries, mutations, and resolvers based on your pre-existing relational database. 52 | 53 | ### Contributing 54 | 55 | We would love for you to test this application out and submit any issues you encounter. Also, feel free to fork to your own repository and submit PRs. 56 | 57 | Here are some of the ways you can start contributing! 58 | 59 | - Bug Fixes 60 | - Adding Features (Check Future Features above) 61 | - Submitting or resolving any GitHub Issues 62 | - Help market our platform 63 | - Any way that can spread word or improve lucidQL 64 | 65 | ### Authors 66 | 67 | - Martin Chiang 68 | - Stanley Huang 69 | - Darwin Sinchi 70 | 71 | ### License 72 | 73 | MIT 74 | -------------------------------------------------------------------------------- /__mocks__/fileMock.js: -------------------------------------------------------------------------------- 1 | module.exports = ''; 2 | -------------------------------------------------------------------------------- /__mocks__/styleMock.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | -------------------------------------------------------------------------------- /__tests__/enzyme.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { configure, shallow, mount } from 'enzyme'; 3 | import Adapter from 'enzyme-adapter-react-16'; 4 | import { RecoilRoot } from 'recoil'; 5 | 6 | // import toJson from 'enzyme-to-json'; 7 | import TopNav from '../client/components/nav-bars/TopNav'; 8 | import App from '../client/App'; 9 | import Modal from '../client/components/link-popup/Modal.tsx'; 10 | import Footer from '../client/components/nav-bars/Footer.tsx'; 11 | import Form from '../client/components/link-popup/Form.tsx'; 12 | 13 | configure({ adapter: new Adapter() }); 14 | 15 | describe('React Unit Test', () => { 16 | describe('Modal Sample Button Test', () => { 17 | let wrapper; 18 | const props = { 19 | onSubmitSample: jest.fn(), 20 | }; 21 | beforeAll(() => { 22 | wrapper = shallow(
); 23 | }); 24 | it('Button will invoke a function once Sample Button is clicked', () => { 25 | wrapper.find('#testenzyme').simulate('click'); 26 | expect(props.onSubmitSample).toHaveBeenCalled(); 27 | }); 28 | }); 29 | describe('mySQL Test', () => { 30 | let wrapper; 31 | const props = { 32 | mySQLButton: jest.fn(), 33 | }; 34 | beforeAll(() => { 35 | wrapper = shallow(); 36 | }); 37 | it('Button will bring you to mySQL modal once it is clicked', () => { 38 | wrapper.find('#testenzyme2').simulate('click'); 39 | expect(props.mySQLButton).toHaveBeenCalled(); 40 | }); 41 | }); 42 | 43 | describe('Close Button Test', () => { 44 | let wrapper; 45 | //wrapper 46 | const props = { 47 | closeModal: jest.fn(), 48 | }; 49 | beforeAll(() => { 50 | wrapper = shallow(); 51 | }); 52 | it('Modal will close once this button is clicked', () => { 53 | wrapper.find('#closeModal1').simulate('click'); 54 | expect(props.closeModal).toHaveBeenCalled(); 55 | }); 56 | }); 57 | 58 | describe('Enter Postgres URI', () => { 59 | let wrapper; 60 | const props = { 61 | showModal: jest.fn(), 62 | }; 63 | beforeAll(() => { 64 | wrapper = shallow(); 65 | }); 66 | it('Modal to insert postgres URI opens', () => { 67 | wrapper.find('#postgresURIbutton').simulate('click'); 68 | expect(props.showModal).toHaveBeenCalled(); 69 | }); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /__tests__/puppeteer.js: -------------------------------------------------------------------------------- 1 | const puppeteer = require('puppeteer'); 2 | 3 | const APP = `http://localhost:${process.env.PORT || 8081}/`; 4 | 5 | describe('Front-end Integration/Features', () => { 6 | let browser; 7 | let page; 8 | 9 | beforeAll(async () => { 10 | browser = await puppeteer.launch({ 11 | args: ['--no-sandbox', '--disable-setuid-sandbox'], 12 | headless: true, 13 | }); 14 | page = await browser.newPage(); 15 | }); 16 | 17 | afterAll(() => { 18 | APP.destroy(); 19 | }); 20 | 21 | describe('Initial display', () => { 22 | it('loads successfully', async () => { 23 | // We navigate to the page at the beginning of each case so we have a fresh start 24 | await page.goto(APP); 25 | await page.waitForSelector('.modal-body'); 26 | const title = await page.$eval('h3', (el) => el.innerHTML); 27 | expect(title).toBe('LucidQL'); 28 | }); 29 | 30 | it('displays a usable input field for a Postgres URI', async () => { 31 | await page.goto(APP); 32 | await page.waitForSelector('.form-group'); 33 | await page.focus('#link'); 34 | await page.keyboard.type('www.sometestlink.com'); 35 | const inputValue = await page.$eval('#link', (el) => el.value); 36 | expect(inputValue).toBe('www.sometestlink.com'); 37 | }); 38 | 39 | it('clicking on submit while input is empty does not close modal', async () => { 40 | await page.goto(APP); 41 | await page.waitForSelector('.modal-body'); 42 | const title = await page.$eval('h3', (el) => el.innerHTML); 43 | await page.click('.submit'); 44 | expect(title).toBe('LucidQL'); 45 | }); 46 | 47 | it('MySQL button exists on modal', async () => { 48 | await page.goto(APP); 49 | await page.waitForSelector('.modal-body'); 50 | const mySQLButton = await page.$eval('.mySQL', (el) => el.innerHTML); 51 | expect(mySQLButton).toBe('Use MySQL Database'); 52 | }); 53 | 54 | it('Clicking MySQL button brings you to the MySQL page', async () => { 55 | await page.goto(APP); 56 | await page.waitForSelector('.modal-body'); 57 | await page.click('.mySQL'); 58 | const mySQLModal = await page.$eval('p', (el) => el.innerHTML); 59 | 60 | expect(mySQLModal).toBe('Please enter MySQL information below.'); 61 | }); 62 | 63 | it('clicking on close button closes the modal', async () => { 64 | await page.goto(APP); 65 | await page.waitForSelector('.form-group'); 66 | await page.click('._modal-close-icon'); 67 | const title = (await page.$('h3', (el) => el.innerHTML)) || null; 68 | expect(title).toBe(null); 69 | }); 70 | }); 71 | describe('Main Page', () => { 72 | it('successfully loads top navbar', async () => { 73 | // We navigate to the page at the beginning of each case so we have a fresh start 74 | await page.goto(APP); 75 | const topNav = (await page.$('.sticky-nav')) !== null; 76 | expect(topNav).toBe(true); 77 | }); 78 | it('successfully loads footer navbar', async () => { 79 | const footerNav = (await page.$('.footer')) !== null; 80 | expect(footerNav).toBe(true); 81 | }); 82 | it('successfully loads d3 graph container', async () => { 83 | const graphArea = (await page.$('.footer')) !== null; 84 | expect(graphArea).toBe(true); 85 | }); 86 | it('successfully loads code mirror', async () => { 87 | const codeMirror = (await page.$('.CodeMirror')) !== null; 88 | expect(codeMirror).toBe(true); 89 | }); 90 | it('successfully loads split bar in middle of page', async () => { 91 | const splitBar = (await page.$('.sc-htpNat.fXAXjb.sc-bdVaJa.cjjWdp')) !== null; 92 | expect(splitBar).toBe(true); 93 | }); 94 | }); 95 | }); 96 | -------------------------------------------------------------------------------- /__tests__/server-test.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-unresolved, import/no-extraneous-dependencies 2 | const request = require('supertest'); 3 | 4 | const server = 'http://localhost:8081'; 5 | 6 | describe('Route integration', () => { 7 | describe('Get request to "/" endpoint', () => { 8 | describe('GET', () => { 9 | it('responds with 200 status and text/html content type', () => 10 | request(server) 11 | .get('/') 12 | .expect('Content-Type', /text\/html/) 13 | .expect(200)); 14 | }); 15 | }); 16 | 17 | describe('Get request to "/graphql" endpoint', () => { 18 | describe('GET', () => { 19 | it('responds with 400 status and application/json content type', () => 20 | request(server) 21 | .get('/graphql') 22 | .expect('Content-Type', /application\/json/) 23 | .expect(400)); 24 | 25 | it('responds with 400 status and application/json content type', () => 26 | request(server) 27 | .get( 28 | '/graphql?query=%7B%0A%20%20people%20%7B%0A%20%20%20%20name%0A%20%20%20%20hair_color%0A%20%20%20%20eye_color%0A%20%20%7D%0A%7D%0A' 29 | ) 30 | .expect('Content-Type', /application\/json/) 31 | .expect(200)); 32 | }); 33 | }); 34 | 35 | describe('Get request to "/playground" endpoint', () => { 36 | describe('GET', () => { 37 | it('responds with 200 status and text/html content type', () => 38 | request(server) 39 | .get('/playground') 40 | .expect('Content-Type', /text\/html/) 41 | .expect(200)); 42 | }); 43 | }); 44 | 45 | describe('Get request to "/db/pg/sdl" endpoint', () => { 46 | describe('POST', () => { 47 | it('responds with 500 status and application/json content type when not sending a request body', () => 48 | request(server) 49 | .post('/db/pg/sdl') 50 | .expect('Content-Type', /application\/json/) 51 | .expect(500)); 52 | 53 | it('responds with 200 status and application/json content type when sending URI', () => 54 | request(server) 55 | .post('/db/pg/sdl') 56 | .send({ uri: 'postgres://mxnahgtg:V5_1wi1TPrDLRvmsl0pKczgf9SMQy1j6@lallah.db.elephantsql.com:5432/mxnahgtg' }) 57 | .set('Accept', 'application/json') 58 | .expect('Content-Type', /application\/json/) 59 | .expect(200)); 60 | }); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /client/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { RecoilRoot, atom, useRecoilState } from 'recoil'; 3 | import LinkContainer from './components/link-popup/LinkContainer'; 4 | import TopNav from './components/nav-bars/TopNav'; 5 | import CodeBox from './components/codebox'; 6 | import SplitPane from 'react-split-pane'; 7 | import ForceGraph from './forceGraph/ForceGraph'; 8 | import Footer from './components/nav-bars/Footer'; 9 | import Sidebar from './components/Sidebar'; 10 | import './styles.css'; 11 | 12 | export const state = atom({ 13 | key: 'state', 14 | default: { 15 | link: '', 16 | modal: true, 17 | schema: '', 18 | tables: {}, 19 | tableModified: false, 20 | history: [], 21 | }, 22 | }); 23 | 24 | const App: React.FC = () => { 25 | const [data, setData] = useRecoilState(state); 26 | 27 | const showModal = () => { 28 | setData({ ...data, modal: true }); 29 | }; 30 | 31 | const handleUndo = () => { 32 | if (data.history.length > 0) { 33 | const newHistory = [...data.history] 34 | const prevData = newHistory.pop(); // get latest tableObj 35 | const prevTable = prevData.table; 36 | const prevSchema = prevData.schema; 37 | setData({ ...data, schema: prevSchema, tables: prevTable, history: newHistory }); 38 | } 39 | } 40 | 41 | const Annotation = () => { 42 | return ( 43 |
44 |
⟶ Foreign Key To (Postgres)
45 |
⎯⎯⎯⎯⎯ Able To Query Each Other (GraphQL)
46 | 47 |
48 | ) 49 | } 50 | 51 | return ( 52 |
53 |
54 | 55 | 56 | 57 | 58 |
59 | {!data.modal ? : null} 60 | {/* {!data.modal ? : null} */} 61 | {!data.modal ? : null} 62 |
63 |
64 | 65 |
66 |
67 |
68 |
69 |
70 | ); 71 | }; 72 | 73 | function Root() { 74 | return ( 75 | 76 | 77 | 78 | ); 79 | } 80 | 81 | export default Root; 82 | -------------------------------------------------------------------------------- /client/UI.tsx: -------------------------------------------------------------------------------- 1 | function createGraph(data) { 2 | const colors = ['red', 'blue', 'green', 'orange', 'purple', 'gray']; 3 | let nodes = []; 4 | let edges = []; 5 | // access all table names on object 6 | let keys = Object.keys(data); 7 | 8 | for (let i = 0; i < colors.length; i++) { 9 | // access properties on each table (columns, pointsTo, referencedBy) 10 | if (!data[keys[i]]) { 11 | break; 12 | } else { 13 | let columns = data[keys[i]].columns; 14 | let pointsTo = data[keys[i]].pointsTo; 15 | let referencedBy = data[keys[i]].referecedBy; 16 | 17 | nodes.push({ id: keys[i], label: keys[i], color: colors[i], size: 20, shape: 'circle' }); 18 | for (let j = 0; j < columns.length; j++) { 19 | nodes.push({ id: columns[j] + j, label: columns[j], color: colors[i], shape: 'circle' }); 20 | edges.push({ from: keys[i], to: columns[j] + j }); 21 | } 22 | 23 | if (!pointsTo[0]) { 24 | continue; 25 | } else { 26 | pointsTo.forEach((point) => { 27 | edges.push({ 28 | from: keys[i], 29 | to: point, 30 | hightlight: 'red', 31 | physics: { springLength: 200 }, 32 | }); 33 | }); 34 | } 35 | 36 | if (!referencedBy[0]) { 37 | continue; 38 | } else { 39 | referencedBy.forEach((ref) => { 40 | edges.push({ from: ref, to: keys[i], highlight: 'cyan', physics: { springLength: 200 } }); 41 | }); 42 | } 43 | } 44 | } 45 | return { nodes, edges }; 46 | } 47 | 48 | export default createGraph; 49 | -------------------------------------------------------------------------------- /client/components/Sidebar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useRecoilValue } from 'recoil'; 3 | import { state } from '../App'; 4 | import { writeToDummyServer } from './nav-bars/Footer'; 5 | 6 | const SideBar: React.FC = () => { 7 | const data = useRecoilValue(state); 8 | const closeNav = () => { 9 | document.querySelector('#mySidebar').style.width = '0'; 10 | document.querySelector('#main').style.marginLeft = '0'; 11 | }; 12 | 13 | return ( 14 | 26 | ); 27 | }; 28 | 29 | export default SideBar; 30 | -------------------------------------------------------------------------------- /client/components/VisGraph.tsx: -------------------------------------------------------------------------------- 1 | import Graph from 'react-graph-vis'; 2 | import React, { Component, useEffect, useState } from 'react'; 3 | import { atom, selector, useRecoilValue, useRecoilState } from 'recoil'; 4 | import createGraph from '../UI'; 5 | import { state } from '../App'; 6 | 7 | const options = { 8 | edges: { 9 | arrows: { 10 | to: { enabled: true, scaleFactor: 1 }, 11 | middle: { enabled: false, scaleFactor: 1 }, 12 | from: { enabled: false, scaleFactor: 1 }, 13 | }, 14 | color: { 15 | // color: 'black', 16 | }, 17 | smooth: false, 18 | }, 19 | nodes: { 20 | borderWidth: 1, 21 | borderWidthSelected: 2, 22 | shape: 'circle', 23 | color: { 24 | border: 'black', 25 | }, 26 | font: { 27 | color: 'white', 28 | size: 10, 29 | face: 'Tahoma', 30 | background: 'none', 31 | strokeWidth: 0, 32 | strokeColor: '#ffffff', 33 | }, 34 | shadow: true, 35 | }, 36 | physics: { 37 | maxVelocity: 146, 38 | solver: 'forceAtlas2Based', 39 | timestep: 0.35, 40 | stabilization: { 41 | enabled: true, 42 | iterations: 100, 43 | updateInterval: 10, 44 | }, 45 | }, 46 | layout: { 47 | randomSeed: undefined, 48 | improvedLayout: true, 49 | }, 50 | }; 51 | 52 | const style = { 53 | display: 'flex', 54 | width: '100rem', 55 | height: '70rem', 56 | }; 57 | 58 | const VisGraph: React.FC = () => { 59 | const [fetched, setFetched] = useState(false); 60 | const [graph, setGraph] = useState({}); 61 | const [data, setData] = useRecoilState(state); 62 | 63 | const getData = () => { 64 | fetch('/db/pg/sdl', { 65 | method: 'POST', 66 | headers: { 67 | 'Content-Type': 'application/json', 68 | }, 69 | 70 | body: JSON.stringify({ uri: data.link }), 71 | }) 72 | .then((response) => response.json()) 73 | .then((response) => { 74 | setGraph(createGraph(response.d3Data)); 75 | setFetched(true); 76 | }); 77 | }; 78 | 79 | useEffect(() => { 80 | getData(); 81 | }, [data]); 82 | 83 | if (fetched) { 84 | return ( 85 |
86 | 87 |
88 | ); 89 | } else if (!fetched) { 90 | return ( 91 |
92 |

Please enter URI to display GraphQL endpoints...

93 |
94 | ); 95 | } 96 | }; 97 | 98 | export default VisGraph; 99 | -------------------------------------------------------------------------------- /client/components/codebox.tsx: -------------------------------------------------------------------------------- 1 | import React, { MouseEvent } from 'react'; 2 | import { UnControlled as CodeMirror } from 'react-codemirror2'; 3 | import 'codemirror/mode/javascript/javascript'; 4 | import '../../node_modules/codemirror/lib/codemirror.css'; 5 | import '../../node_modules/codemirror/theme/dracula.css'; 6 | import { state } from '../App'; 7 | import { useRecoilValue } from 'recoil'; 8 | 9 | const CodeBox: React.FC = () => { 10 | const data = useRecoilValue(state); 11 | 12 | return ( 13 |
14 | 23 |
24 | ); 25 | }; 26 | 27 | export default CodeBox; 28 | -------------------------------------------------------------------------------- /client/components/link-popup/Form.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const Form = ({ onSubmit, onSubmitSample, mySQLButton }) => { 4 | return ( 5 |
6 | 7 |

LucidQL

8 |
9 |
Quickly build your GraphQL API from an existing Postgres database.
10 |
Dragging to trash can will remove Table or Columns you do not need from your GraphQL schema.
11 |
12 |

Please enter a PostgresQL link below.

13 |
14 | 15 | 16 |
17 |
18 | 21 |
22 | 30 |

31 | 39 |
40 | 41 |
42 | ); 43 | }; 44 | export default Form; 45 | -------------------------------------------------------------------------------- /client/components/link-popup/FormMySQL.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const FormMySQL = ({ onSubmitMySQL, mySQLButton }) => { 4 | return ( 5 |
6 |
7 |

LucidQL

8 |

Please enter MySQL information below.

9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 | 23 |

24 | 27 |
28 |
29 |
30 | ); 31 | }; 32 | 33 | export default FormMySQL; 34 | -------------------------------------------------------------------------------- /client/components/link-popup/LinkContainer.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { state } from '../../App'; 3 | import { useRecoilState } from 'recoil'; 4 | import { Modal } from './Modal'; 5 | import 'regenerator-runtime/runtime'; 6 | 7 | const LinkContainer: React.FC = () => { 8 | const [data, setData] = useRecoilState(state); 9 | 10 | useEffect(() => { 11 | toggleScrollLock(); 12 | }, [data]); 13 | 14 | const fetchSchemaPG = (link) => { 15 | fetch('/db/pg/sdl', { 16 | method: 'POST', 17 | headers: { 18 | 'Content-Type': 'application/json', 19 | }, 20 | body: JSON.stringify({ uri: link }), 21 | }) 22 | .then((response) => response.json()) 23 | .then((response) => { 24 | setData({ ...data, link: link, modal: false, schema: response.schema, tables: response.tables }); 25 | }); 26 | }; 27 | 28 | const onSubmit = async (event) => { 29 | event.preventDefault(event); 30 | if (event.target.link.value.trim() !== '') { 31 | fetchSchemaPG(event.target.link.value); 32 | } 33 | }; 34 | 35 | const fetchSchemaMySQL = (host, user, password, database) => { 36 | fetch('/db/mySQL/sdl', { 37 | method: 'POST', 38 | headers: { 39 | 'Content-Type': 'application/json', 40 | }, 41 | body: JSON.stringify({ host, user, password, database }), 42 | }) 43 | .then((response) => response.json()) 44 | .then((response) => { 45 | setData({ link: '', modal: false, schema: response.schema, d3Data: response.d3Data }); 46 | }); 47 | }; 48 | 49 | const onSubmitMySQL = async (event) => { 50 | event.preventDefault(event); 51 | const { host, user, password, database } = event.target; 52 | // if (event.target.link.value.trim() !== '') { 53 | // fetchSchemaPG(event.target.link.value); 54 | // } 55 | fetchSchemaMySQL(host.value, user.value, password.value, database.value); 56 | }; 57 | 58 | const onSubmitSample = async (event) => { 59 | event.preventDefault(event); 60 | fetch('/db/pg/sdl', { 61 | method: 'POST', 62 | headers: { 63 | 'Content-Type': 'application/json', 64 | }, 65 | body: JSON.stringify({ 66 | uri: 'postgres://ordddiou:g5OjOyAIFxf-tsLk1uwu4ZOfbJfiCFbh@ruby.db.elephantsql.com:5432/ordddiou', 67 | }), 68 | }) 69 | .then((response) => response.json()) 70 | .then((response) => { 71 | setData({ 72 | ...data, 73 | link: 'postgres://ordddiou:g5OjOyAIFxf-tsLk1uwu4ZOfbJfiCFbh@ruby.db.elephantsql.com:5432/ordddiou', 74 | modal: false, 75 | schema: response.schema, 76 | tables: response.tables, 77 | }); 78 | }); 79 | }; 80 | 81 | const closeModal = () => { 82 | setData({ ...data, modal: false }); 83 | }; 84 | 85 | const toggleScrollLock = () => { 86 | document.querySelector('html').classList.toggle('scroll-lock'); 87 | }; 88 | 89 | return ( 90 | 91 | {data.modal ? ( 92 | 93 | ) : null} 94 | 95 | ); 96 | }; 97 | 98 | export default LinkContainer; 99 | -------------------------------------------------------------------------------- /client/components/link-popup/Modal.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { Form } from './Form'; 4 | import { FormMySQL } from './FormMySQL'; 5 | import FocusTrap from 'focus-trap-react'; 6 | 7 | export const Modal: React.FC = ({ onSubmit, onSubmitMySQL, closeModal, onSubmitSample }) => { 8 | const [modalState, setModalState] = useState('postgres'); 9 | 10 | const mySQLButton = () => { 11 | return modalState === 'mysql' ? setModalState('postgres') : setModalState('mysql'); 12 | }; 13 | 14 | if (modalState === 'postgres') { 15 | return ReactDOM.createPortal( 16 | 17 | 35 | , 36 | document.body 37 | ); 38 | } else { 39 | return ReactDOM.createPortal( 40 | 41 | 53 | , 54 | document.body 55 | ); 56 | } 57 | }; 58 | 59 | export default Modal; 60 | -------------------------------------------------------------------------------- /client/components/nav-bars/Footer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { packagejsonFile } from '../../downloadHelperFunctions/packagejsonFile'; 3 | import { connectToDB } from '../../downloadHelperFunctions/connectToDB'; 4 | import { schemaFile } from '../../downloadHelperFunctions/schemaFile'; 5 | import { serverFile } from '../../downloadHelperFunctions/serverFile'; 6 | import JSZip from 'jszip'; 7 | //below lets you download files within your browser 8 | import FileSaver from 'file-saver'; 9 | import { useRecoilValue } from 'recoil'; 10 | import { state } from '../../App'; 11 | import { Navbar, Nav, Form, Button } from 'react-bootstrap'; 12 | 13 | export const writeToDummyServer = (schema: string, link: string) => { 14 | const connectToDBFile = connectToDB(link); 15 | const toSchemaFile = schemaFile(schema); 16 | 17 | const postOptions = { 18 | method: 'POST', 19 | headers: { 'Content-Type': 'application/json' }, 20 | body: JSON.stringify({ db: connectToDBFile, schema: toSchemaFile }), 21 | }; 22 | 23 | fetch('/db/pg/writefile', postOptions) 24 | .then((response) => response.json()) 25 | .then((data) => console.log(data)); 26 | }; 27 | 28 | const Footer: React.FC = () => { 29 | const data = useRecoilValue(state); 30 | 31 | const handleDownloadFiles = (event: React.MouseEvent) => { 32 | event.preventDefault(); 33 | const zip = new JSZip(); 34 | 35 | zip.folder('lucidQL').file('package.json', packagejsonFile()); 36 | zip.folder('lucidQL').file('server.js', serverFile()); 37 | zip.folder('lucidQL').file('schema.js', schemaFile(data.schema)); 38 | zip.folder('lucidQL').file('connectToDB.js', connectToDB(data.link)); 39 | zip.generateAsync({ type: 'blob' }).then(function (content: any) { 40 | FileSaver.saveAs(content, 'lucidQL.zip'); 41 | }); 42 | }; 43 | 44 | return ( 45 | 46 | 47 | 48 | 49 | 64 | 65 | 66 | ); 67 | }; 68 | 69 | export default Footer; 70 | -------------------------------------------------------------------------------- /client/components/nav-bars/TopNav.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Navbar, 4 | Nav, 5 | NavDropdown, 6 | Form, 7 | FormControl, 8 | Button 9 | } from "react-bootstrap"; 10 | import logo from "../../../public/lucidQL-logo.png"; 11 | import logoBrand from "../../../public/lucidQL.png"; 12 | 13 | const TopNav: React.FC = ({ showModal }) => { 14 | function openNav() { 15 | document.querySelector("#mySidebar").style.width = "250px"; 16 | document.querySelector("#main").style.marginLeft = "250px"; 17 | } 18 | 19 | return ( 20 | 27 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 49 | 50 | 51 | ); 52 | }; 53 | 54 | export default TopNav; 55 | 56 | /* */ 57 | -------------------------------------------------------------------------------- /client/downloadHelperFunctions/connectToDB.js: -------------------------------------------------------------------------------- 1 | function connectToDB(uri) { 2 | const dbConnection = `const {Pool} = require('pg'); 3 | const PG_URI = '${uri.trim()}'; 4 | 5 | const pool = new Pool({ 6 | connectionString: PG_URI 7 | }) 8 | 9 | module.exports = { 10 | query: (text,params, callback) => { 11 | console.log('executed query:', text) 12 | return pool.query(text, params, callback) 13 | } 14 | } 15 | `; 16 | return dbConnection; 17 | } 18 | 19 | module.exports = { 20 | connectToDB, 21 | }; 22 | -------------------------------------------------------------------------------- /client/downloadHelperFunctions/packagejsonFile.js: -------------------------------------------------------------------------------- 1 | function packagejsonFile() { 2 | const packagejsonContent = ` 3 | { 4 | "name": "lucidQL", 5 | "version": "1.0.0", 6 | "description": "Visualizer for GraphQL APIs + a schema and resolver creator", 7 | "main": "server.js", 8 | "scripts": { 9 | "start": "nodemon server/server.js" 10 | }, 11 | "author": "lucidQL Team - Stanley Huang, Martin Chiang, Darwin Sinchi", 12 | "license": "ISC", 13 | "dependencies": { 14 | "dotenv": "^8.2.0", 15 | "express": "^4.17.1", 16 | "express-graphql": "^0.9.0", 17 | "graphql": "^15.3.0", 18 | "graphql-playground-middleware-express": "^1.7.15", 19 | "graphql-tools": "^4.0.7", 20 | "pg": "^8.3.3" 21 | }, 22 | "devDependencies": { 23 | "nodemon": "^2.0.4" 24 | } 25 | } 26 | `; 27 | return packagejsonContent; 28 | } 29 | 30 | module.exports = { 31 | packagejsonFile, 32 | }; 33 | -------------------------------------------------------------------------------- /client/downloadHelperFunctions/schemaFile.js: -------------------------------------------------------------------------------- 1 | function schemaFile(schema) { 2 | const fileContent = `const { makeExecutableSchema } = require('graphql-tools'); 3 | const db = require('./connectToDB');\n\n${schema}`; 4 | 5 | return fileContent; 6 | } 7 | 8 | module.exports = { 9 | schemaFile, 10 | }; 11 | -------------------------------------------------------------------------------- /client/downloadHelperFunctions/serverFile.js: -------------------------------------------------------------------------------- 1 | function serverFile() { 2 | const serverInfo = `const express = require('express'); 3 | const expressGraphQL = require('express-graphql'); 4 | require('dotenv').config(); 5 | const expressPlayground = require('graphql-playground-middleware-express').default; 6 | 7 | const schema = require('./schema'); 8 | const app = express(); 9 | const PORT = 3000; 10 | 11 | app.use( 12 | '/graphql', 13 | expressGraphQL({ 14 | schema 15 | }) 16 | ); 17 | 18 | app.get('/playground', expressPlayground({ endpoint: '/graphql' })); 19 | 20 | app.listen(PORT, () => { 21 | console.log('Welcome to lucidQL! To query your database enter your URI into the frontend app, then you can begin using your new schemas, please go to http://localhost:3000/playground'); 22 | });`; 23 | 24 | return serverInfo; 25 | } 26 | 27 | module.exports = { 28 | serverFile, 29 | }; 30 | -------------------------------------------------------------------------------- /client/forceGraph/ForceGraph.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from 'react'; 2 | import * as d3 from 'd3'; 3 | import { useRecoilState } from 'recoil'; 4 | import { state } from '../App'; 5 | import { runForceGraph } from './generators/ForceGraphGenerator'; 6 | import { deleteTables, deleteColumns } from './generators/deleteFunctions'; 7 | 8 | // The ForceGraph component will be the container for the generated force graph and ForceGraphGenerator will generate the graph using D3 9 | function ForceGraph() { 10 | // useRef hook to reference the container element (common practice for React + d3) 11 | const containerRef = useRef(null); 12 | const [data, setData] = useRecoilState(state); 13 | 14 | const nodeHoverTooltip = React.useCallback((node) => { 15 | if (node.primary) 16 | return `

Table: ${node.name}

(SQL info)
Primary Key : ${node.primaryKey}
Columns Count : ${ 17 | node.columnCount 18 | }
Foreign Keys :
${node.foreignKeys.length > 0 ? node.foreignKeys : 'N/A'}
Referenced by :
${ 19 | node.referencedBy.length > 0 ? node.referencedBy : 'N/A' 20 | }`; 21 | return `

Column: ${node.name}

(SQL info)
dataType : ${node.dataType}
isNullable : ${node.isNullable}
charMaxLength : ${node.charMaxLength}
columnDefault : ${node.columnDefault}

`; 22 | }, []); 23 | 24 | const handleDeleteTables = (currentState, tableToExclude) => { 25 | const { newTables, newSchema, history } = deleteTables(currentState, tableToExclude); 26 | setData({ ...data, tables: newTables, schema: newSchema, tableModified: true, history }); 27 | }; 28 | 29 | const handleDeleteColumns = (currentState, columnToExclude, parentName, foreignKeyToDelete) => { 30 | const { newTables, newSchema, history } = deleteColumns(currentState, columnToExclude, parentName, foreignKeyToDelete); 31 | setData({ ...data, tables: newTables, schema: newSchema, tableModified: true, history }); 32 | }; 33 | 34 | // useEffect hook to detect successful ref mount, as well as data change 35 | useEffect(() => { 36 | if (containerRef.current) { 37 | if (data.tableModified) { 38 | // if this is an update, clear up the svg before re-run graph 39 | d3.select(containerRef.current).html(''); 40 | } 41 | runForceGraph(containerRef.current, data, nodeHoverTooltip, handleDeleteTables, handleDeleteColumns); 42 | } 43 | }, [data.tables]); 44 | 45 | // create a reference to the div which will wrap the generated graph and nothing more. 46 | return ; 47 | } 48 | 49 | export default ForceGraph; 50 | -------------------------------------------------------------------------------- /client/forceGraph/deleteIcon.svg: -------------------------------------------------------------------------------- 1 | 50 all -------------------------------------------------------------------------------- /client/forceGraph/generators/ForceGraphGenerator.js: -------------------------------------------------------------------------------- 1 | import * as d3 from 'd3'; 2 | import { generateNodeAndLink, simplifyTable } from './d3DataBuilder'; 3 | import deleteIconSrc from '../deleteIcon.svg'; 4 | 5 | export function runForceGraph(container, data, nodeHoverTooltip, handleDeleteTables, handleDeleteColumns) { 6 | const simplifiedData = simplifyTable(data.tables); 7 | const d3Arrays = generateNodeAndLink(simplifiedData) 8 | const { tableNodes, columnNodes, referencedBy, pointsTo, linksToColumns } = d3Arrays; 9 | 10 | // get the container’s width and height from bounding rectangle: 11 | const containerRect = container.getBoundingClientRect(); 12 | const height = containerRect.height; 13 | const width = containerRect.width; 14 | 15 | const topPadding = 250; 16 | const deleteIconY = 160; 17 | const deleteIconRadius = 24; 18 | 19 | // add the option to drag the force graph nodes as part of it’s simulation. 20 | const drag = (simulation) => { 21 | const dragStart = (event, d) => { 22 | // if condition: dragstarted would be called once for each of the two fingers. 23 | // The first time it get's called d3.event.active would be 0, and the simulation would be restarted. 24 | // The second time d3.event.active would be 1, so the simulation wouldn't restart again, because it's already going. 25 | if (!event.active) simulation.alphaTarget(0.3).restart(); 26 | d.fx = d.x; 27 | d.fy = d.y; 28 | }; 29 | 30 | const dragging = (event, d) => { 31 | d.fx = event.x; 32 | d.fy = event.y; 33 | }; 34 | 35 | const dragEnd = (event, d) => { 36 | if (!event.active) simulation.alphaTarget(0); // if no event.active, stop the simulation 37 | d.fx = event.x; // will stay in place after drag ends 38 | d.fy = event.y; // if set to null node will will go back to original position 39 | 40 | // check if nodes are being dragged to the trash can 41 | if (2 * width / 5 - deleteIconRadius < event.x && event.x < 2 * width / 5 + deleteIconRadius && 42 | 2 * deleteIconY - deleteIconRadius < event.y && event.y < 2 * deleteIconY + deleteIconRadius) { 43 | 44 | if (event.subject.primary) { 45 | handleDeleteTables(data, event.subject.name); 46 | } else { 47 | handleDeleteColumns(data, event.subject.name, event.subject.parent, event.subject.foreignKey) 48 | } 49 | } 50 | }; 51 | 52 | return d3.drag().on('start', dragStart).on('drag', dragging).on('end', dragEnd); 53 | }; 54 | 55 | // Handle the node tooltip generation: add the tooltip element to the graph 56 | const tooltip = document.querySelector('#graph-tooltip'); 57 | if (!tooltip) { 58 | const tooltipDiv = document.createElement('div'); 59 | tooltipDiv.classList.add('tooltip'); 60 | tooltipDiv.style.opacity = '0'; 61 | tooltipDiv.id = 'graph-tooltip'; 62 | document.body.appendChild(tooltipDiv); 63 | } 64 | const div = d3.select('#graph-tooltip'); 65 | 66 | const addTooltip = (hoverTooltip, d, x, y) => { 67 | div.transition().duration(200).style('opacity', 0.9); 68 | div 69 | .html(hoverTooltip(d)) 70 | .style('left', `${x}px`) 71 | .style('top', `${y + 10}px`); 72 | }; 73 | 74 | const removeTooltip = () => { 75 | div.transition().duration(200).style('opacity', 0); 76 | }; 77 | 78 | const nodesArr = tableNodes.concat(columnNodes); 79 | 80 | const linksArr = pointsTo; 81 | const linesArr = linksToColumns.concat(referencedBy) 82 | 83 | const simulation = d3 84 | .forceSimulation(nodesArr) 85 | .force('link',d3.forceLink(linksArr).id((d) => d.id).distance(400).strength(1)) 86 | .force('line',d3 87 | .forceLink(linesArr) 88 | .id((d) => d.id).distance((d) => d.type? 400: 60)) 89 | .force('charge', d3.forceManyBody().strength(-500)) // Negative numbers indicate a repulsive force and positive numbers an attractive force. Strength defaults to -30 upon installation. 90 | .force('collide',d3.forceCollide(25)) 91 | .force('x', d3.forceX()) // These two siblings push nodes towards the desired x and y position. 92 | .force('y', d3.forceY()); // default to the middle 93 | 94 | 95 | const svg = d3 96 | .select(container) 97 | .append('svg') 98 | .attr('viewBox', [-width / 2, -height / 2, width, height]) 99 | const deleteIcon = svg.append('image') 100 | 101 | .attr('x', 2 * width / 5 - deleteIconRadius) 102 | .attr('y', 2 * deleteIconY - deleteIconRadius) 103 | .attr('width', 2.5 * deleteIconRadius) 104 | .attr('height', 2.5 * deleteIconRadius) 105 | .attr('xlink:href', deleteIconSrc) 106 | 107 | // Initialize the links between tables and its columns 108 | const line = svg 109 | .append('g') 110 | .attr('stroke', '#000') 111 | .attr('stroke-opacity', 0.2) 112 | .selectAll('line') 113 | // .data(linesArr) 114 | .data(linksToColumns) 115 | .join('line') 116 | 117 | const refLine = svg 118 | .append('g') 119 | .attr('stroke', '#767c77') 120 | .attr('stroke-opacity', 1) 121 | .style('stroke-width', '1.5px') 122 | .selectAll('line') 123 | .data(referencedBy) 124 | // .data(linksToColumns) 125 | .join('line') 126 | 127 | // appending little triangles, path object, as arrowhead 128 | // The element is used to store graphical objects that will be used at a later time 129 | // The element defines the graphic that is to be used for drawing arrowheads or polymarkers 130 | // on a given , , or element. 131 | svg 132 | .append('svg:defs') 133 | .selectAll('marker') 134 | .data(['end']) 135 | .enter() 136 | .append('svg:marker') 137 | .attr('id', String) 138 | .attr('viewBox', '0 -5 10 10') //the bound of the SVG viewport for the current SVG fragment. defines a coordinate system 10 wide and 10 high starting on (0,-5) 139 | .attr('refX', 28) // x coordinate for the reference point of the marker. If circle is bigger, this need to be bigger. 140 | .attr('refY', 0) 141 | .attr('orient', 'auto') 142 | .attr('markerWidth', 15) 143 | .attr('markerHeight', 15) 144 | .attr('xoverflow', 'visible') 145 | .append('svg:path') 146 | .attr('d', 'M 0,-5 L 10,0 L 0,5') 147 | .attr('fill', 'orange') 148 | .style('stroke', 'none'); 149 | 150 | // add the curved line 151 | const path = svg 152 | .append('svg:g') 153 | .selectAll('path') 154 | .data(linksArr) 155 | .join('svg:path') 156 | .attr('fill', 'none') 157 | .style('stroke', (d) => (d.type === 'pointsTo' ? 'orange' : '#767c77')) 158 | .style('stroke-width', '1.5px') 159 | .attr('marker-end', 'url(#end)'); 160 | //The marker-end attribute defines the arrowhead or polymarker that will be drawn at the final vertex of the given shape. 161 | 162 | const node = svg 163 | .append('g') 164 | .attr('stroke', '#fff') 165 | .attr('stroke-width', 1) 166 | .selectAll('circle') 167 | .data(nodesArr) 168 | .join('circle') 169 | .style('cursor', 'move') 170 | .attr('r', (d) => (d.primary ? 40 : 25)) 171 | .attr('fill', (d) => (d.primary ? '#967bb6' : ( d.foreignKey? 'orange' :'#aacfcf'))) 172 | .call(drag(simulation)); 173 | 174 | const label = svg 175 | .append('g') 176 | .attr('class', 'labels') 177 | .selectAll('text') 178 | .data(nodesArr) 179 | .join('text') 180 | .attr('text-anchor', 'middle') 181 | .attr('dominant-baseline', 'central') 182 | .attr('class', (d) => (d.primary ? 'primary_node' : 'column_node')) 183 | .text((d) => d.primary ? (d.name.length > 7 ? d.name.slice(0, 6)+'..' : d.name) : (d.name.length > 5 ? d.name.slice(0, 5)+'..' : d.name)) 184 | .call(drag(simulation)); 185 | 186 | label 187 | .on('mouseover', (event, d) => addTooltip(nodeHoverTooltip, d, event.pageX, event.pageY)) 188 | .on('mouseout', () => removeTooltip()); 189 | 190 | simulation.on('tick', () => { 191 | // update link positions 192 | line 193 | .attr('x1', (d) => d.source.x) 194 | .attr('y1', (d) => d.source.y) 195 | .attr('x2', (d) => d.target.x) 196 | .attr('y2', (d) => d.target.y); 197 | 198 | refLine 199 | .attr('x1', (d) => d.source.x) 200 | .attr('y1', (d) => d.source.y) 201 | .attr('x2', (d) => d.target.x) 202 | .attr('y2', (d) => d.target.y); 203 | 204 | // update node positions 205 | node.attr('cx', (d) => d.x).attr('cy', (d) => d.y); 206 | 207 | label.attr('x', (d) => d.x).attr('y', (d) => d.y); 208 | 209 | // update the curvy lines 210 | path.attr('d', (d) => { 211 | const dx = d.target.x - d.source.x; 212 | const dy = d.target.y - d.source.y; 213 | const dr = Math.sqrt(dx * dx + dy * dy); 214 | return `M${d.source.x},${d.source.y}A${dr},${dr} 0 0,1 ${d.target.x},${d.target.y}`; 215 | }); 216 | }); 217 | 218 | return { 219 | destroy: () => { 220 | simulation.stop(); 221 | }, 222 | nodes: () => { 223 | return svg.node(); 224 | }, 225 | }; 226 | } 227 | -------------------------------------------------------------------------------- /client/forceGraph/generators/d3DataBuilder.js: -------------------------------------------------------------------------------- 1 | export const generateNodeAndLink = (data) => { 2 | const d3Arrays = { tableNodes:[], columnNodes:[], referencedBy:[], pointsTo: [], linksToColumns:[] }; 3 | for (let tableName in data) { // handle table node 4 | const parentNode = { 5 | id: `Parent-${tableName}`, 6 | name: tableName, 7 | primary: true, 8 | primaryKey: data[tableName].primaryKey, 9 | foreignKeys: data[tableName].foreignKeys, 10 | columnCount: data[tableName].columns.length, 11 | referencedBy: data[tableName].referencedBy 12 | } 13 | d3Arrays.tableNodes.push(parentNode); 14 | 15 | // handle links between tables 16 | data[tableName].pointsTo.forEach((targetTable) => { 17 | const parentLink = { 18 | source: `Parent-${tableName}`, 19 | target: `Parent-${targetTable}`, 20 | type: 'pointsTo', 21 | } 22 | d3Arrays.pointsTo.push(parentLink); 23 | }) 24 | data[tableName].referencedBy.forEach(refTable => { 25 | const parentLink = { 26 | source: `Parent-${refTable}`, 27 | target: `Parent-${tableName}`, 28 | type: 'referencedBy', 29 | } 30 | d3Arrays.referencedBy.push(parentLink); 31 | }) 32 | 33 | // handle links between column and its parent 34 | data[tableName].columns.forEach((columnObj) => { 35 | const childNode = { 36 | id: `${tableName}-${columnObj.columnName}`, 37 | name: columnObj.columnName, 38 | parent: tableName, 39 | ...columnObj, 40 | }; 41 | const childLink = { 42 | source: `Parent-${tableName}`, 43 | target: `${tableName}-${columnObj.columnName}` 44 | } 45 | if (data[tableName].foreignKeys.includes(columnObj.columnName)) { // if this column is a foreign key 46 | childNode.foreignKey = true; 47 | } 48 | d3Arrays.columnNodes.push(childNode); 49 | d3Arrays.linksToColumns.push(childLink) 50 | }) 51 | } 52 | return d3Arrays; 53 | }; 54 | 55 | export const simplifyTable = (OriginalTables) => { 56 | const newTables = {}; 57 | 58 | for (const table in OriginalTables) { 59 | const currentTable = OriginalTables[table]; 60 | if ( // if this is not a join table 61 | !currentTable.foreignKeys || 62 | Object.keys(currentTable.columns).length !== Object.keys(currentTable.foreignKeys).length + 1 63 | ) { 64 | const pointsTo = []; 65 | const foreignKeys = []; 66 | for (const objName in currentTable.foreignKeys) { 67 | pointsTo.push(currentTable.foreignKeys[objName].referenceTable); 68 | foreignKeys.push(objName); 69 | } 70 | const referencedBy = []; 71 | for (const refTableName in currentTable.referencedBy) { 72 | if ( 73 | !OriginalTables[refTableName].foreignKeys || 74 | Object.keys(OriginalTables[refTableName].columns).length !== 75 | Object.keys(OriginalTables[refTableName].foreignKeys).length + 1 76 | ) { 77 | referencedBy.push(refTableName); 78 | } else { // else it's a join table 79 | for (const foreignKey in OriginalTables[refTableName].foreignKeys) { 80 | const joinedTable = OriginalTables[refTableName].foreignKeys[foreignKey].referenceTable; 81 | if (joinedTable !== table) { 82 | referencedBy.push(joinedTable); 83 | } 84 | } 85 | } 86 | } 87 | const columns = []; 88 | for (const columnName in currentTable.columns) { 89 | if (columnName !== currentTable.primaryKey) { 90 | const columnInfo = { 91 | columnName, 92 | ...currentTable.columns[columnName] 93 | } 94 | columns.push(columnInfo); 95 | } 96 | } 97 | 98 | newTables[table] = { 99 | pointsTo, 100 | referencedBy, 101 | columns, 102 | foreignKeys, 103 | primaryKey: currentTable.primaryKey 104 | }; 105 | } 106 | } 107 | 108 | return newTables; 109 | }; 110 | -------------------------------------------------------------------------------- /client/forceGraph/generators/deleteFunctions.js: -------------------------------------------------------------------------------- 1 | import { assembleSchema } from './schemaGenerator'; 2 | 3 | export function deleteTables(currentState, tableToExclude) { 4 | const { tables: tablesObj, schema: currentSchema, history: currentHistory } = currentState; 5 | const newTables = {} 6 | const helperFuncOnJoinTable = (table, tableToExclude) => { // if this is a join table and it has included tableToExclude 7 | if (!table.foreignKeys) return false; // no foreign keys, not a join table so just return false 8 | if (Object.keys(table.columns).length === Object.keys(table.foreignKeys).length + 1) { // if this is a join table 9 | for(let key in table.foreignKeys) { 10 | if (table.foreignKeys[key].referenceTable === tableToExclude) return true; 11 | } 12 | } 13 | return false; 14 | } 15 | for (let tableName in tablesObj) { 16 | if (tableName !== tableToExclude && !helperFuncOnJoinTable(tablesObj[tableName], tableToExclude)) { 17 | newTables[tableName] = {}; 18 | newTables[tableName].primaryKey = tablesObj[tableName].primaryKey; 19 | newTables[tableName].columns = tablesObj[tableName].columns; 20 | let newFK = {} 21 | for (let key in tablesObj[tableName].foreignKeys) { 22 | if (tablesObj[tableName].foreignKeys[key].referenceTable !== tableToExclude) { 23 | newFK[key] = tablesObj[tableName].foreignKeys[key] 24 | } 25 | } 26 | newTables[tableName].foreignKeys = newFK 27 | let newRefby = {} 28 | for (let refByTableName in tablesObj[tableName].referencedBy) { 29 | if (refByTableName !== tableToExclude && !helperFuncOnJoinTable(tablesObj[refByTableName], tableToExclude)) { 30 | newRefby[refByTableName] = tablesObj[tableName].referencedBy[refByTableName] 31 | } 32 | } 33 | newTables[tableName].referencedBy = newRefby; 34 | } 35 | } 36 | const newSchema = assembleSchema(newTables); 37 | const history = [...currentHistory, {table: tablesObj, schema: currentSchema}]; 38 | return { newTables, newSchema, history } 39 | } 40 | 41 | export function deleteColumns(currentState, columnToExclude, parentName, foreignKeyToDelete) { 42 | const { tables: tablesObj, schema: currentSchema, history: currentHistory } = currentState; 43 | const newTables = {}; 44 | 45 | let FKTargetTable = null; 46 | if (foreignKeyToDelete) { 47 | FKTargetTable = tablesObj[parentName].foreignKeys[columnToExclude].referenceTable 48 | } 49 | 50 | for (let tableName in tablesObj) { 51 | if (tableName === parentName) { 52 | const newColumns = {}; 53 | for (let objName in tablesObj[parentName].columns) { 54 | if (objName !== columnToExclude) { 55 | newColumns[objName] = tablesObj[parentName].columns[objName] 56 | } 57 | } 58 | 59 | if (foreignKeyToDelete) { // if foreignKeyToDelete is true, modify newForeignKeys 60 | const newForeignKeys = {}; 61 | for (let objName in tablesObj[parentName].foreignKeys) { 62 | if (objName !== columnToExclude) { 63 | newForeignKeys[objName] = tablesObj[parentName].foreignKeys[objName]; 64 | } 65 | } 66 | newTables[tableName] = {...tablesObj[tableName], columns: newColumns, foreignKeys: newForeignKeys} 67 | } else { // if foreignKeyToDelete is false... 68 | newTables[tableName] = {...tablesObj[tableName], columns: newColumns} 69 | } 70 | } else if (foreignKeyToDelete && tableName === FKTargetTable) { 71 | // if we need to deal with a table that is referenced by this FK 72 | const newRefby = {} 73 | for (let refObjName in tablesObj[FKTargetTable].referencedBy) { 74 | if (refObjName !== parentName) { 75 | newRefby[refObjName] = tablesObj[FKTargetTable].referencedBy[refObjName]; 76 | } 77 | } 78 | newTables[FKTargetTable] = {...tablesObj[FKTargetTable], referencedBy: newRefby} 79 | } else { // else for other tables we can just make a copy 80 | newTables[tableName] = tablesObj[tableName] 81 | } 82 | } 83 | 84 | const newSchema = assembleSchema(newTables); 85 | const history = [...currentHistory, {table: tablesObj, schema: currentSchema}]; 86 | return { newTables, newSchema, history } 87 | } -------------------------------------------------------------------------------- /client/forceGraph/generators/helperFunctions.js: -------------------------------------------------------------------------------- 1 | function toPascalCase(str) { 2 | return capitalize(toCamelCase(str)); 3 | } 4 | 5 | function capitalize(str) { 6 | return `${str[0].toUpperCase()}${str.slice(1)}`; 7 | } 8 | 9 | function toCamelCase(str) { 10 | let varName = str.toLowerCase(); 11 | for (let i = 0, len = varName.length; i < len; i++) { 12 | const isSeparator = varName[i] === '-' || varName[i] === '_'; 13 | if (varName[i + 1] && isSeparator) { 14 | const char = i === 0 ? varName[i + 1] : varName[i + 1].toUpperCase(); 15 | varName = varName.slice(0, i) + char + varName.slice(i + 2); 16 | } else if (isSeparator) { 17 | varName = varName.slice(0, i); 18 | } 19 | } 20 | return varName; 21 | } 22 | 23 | function typeSet(str) { 24 | switch (str) { 25 | case 'character varying': 26 | return 'String'; 27 | break; 28 | case 'character': 29 | return 'String'; 30 | break; 31 | case 'integer': 32 | return 'Int'; 33 | break; 34 | case 'text': 35 | return 'String'; 36 | break; 37 | case 'date': 38 | return 'String'; 39 | break; 40 | case 'boolean': 41 | return 'Boolean'; 42 | break; 43 | default: 44 | return 'Int'; 45 | } 46 | } 47 | 48 | function getPrimaryKeyType(primaryKey, columns) { 49 | return typeSet(columns[primaryKey].dataType); 50 | } 51 | 52 | export { 53 | capitalize, 54 | toPascalCase, 55 | toCamelCase, 56 | typeSet, 57 | getPrimaryKeyType, 58 | }; 59 | -------------------------------------------------------------------------------- /client/forceGraph/generators/resolverGenerator.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle */ 2 | /* eslint-disable guard-for-in */ 3 | /* eslint-disable no-restricted-syntax */ 4 | // eslint-disable-next-line prettier/prettier 5 | import { singular } from 'pluralize'; 6 | import { 7 | toCamelCase, 8 | toPascalCase, 9 | } from './helperFunctions'; 10 | 11 | const ResolverGenerator = { 12 | _values: {}, 13 | }; 14 | 15 | ResolverGenerator.reset = function () { 16 | this._values = {}; 17 | }; 18 | 19 | ResolverGenerator.queries = function queries(tableName, { primaryKey }) { 20 | return `\n${this._columnQuery(tableName, primaryKey)}` + `\n${this._allColumnQuery(tableName)}`; 21 | }; 22 | 23 | ResolverGenerator.mutations = function mutations(tableName, tableData) { 24 | const { primaryKey, foreignKeys, columns } = tableData; 25 | this._createValues(primaryKey, foreignKeys, columns); 26 | return ( 27 | `${this._createMutation(tableName, primaryKey, foreignKeys, columns)}\n` + 28 | `${this._updateMutation(tableName, primaryKey, foreignKeys, columns)}\n` + 29 | `${this._deleteMutations(tableName, primaryKey)}\n\n` 30 | ); 31 | }; 32 | 33 | ResolverGenerator.getRelationships = function getRelationships(tableName, tables) { 34 | const { primaryKey, referencedBy } = tables[tableName]; 35 | if (!referencedBy) return ''; 36 | let relationships = `\n ${toPascalCase(singular(tableName))}: {\n`; 37 | for (const refTableName in referencedBy) { 38 | const { 39 | referencedBy: foreignRefBy, 40 | foreignKeys: foreignFKeys, 41 | columns: foreignColumns, 42 | } = tables[refTableName]; 43 | const refTableType = toPascalCase(singular(refTableName)); 44 | // One-to-one 45 | if (foreignRefBy && foreignRefBy[tableName]) 46 | relationships += this._oneToOne( 47 | tableName, 48 | primaryKey, 49 | refTableName, 50 | referencedBy[refTableName] 51 | ); 52 | // One-to-many 53 | else if (Object.keys(foreignColumns).length !== Object.keys(foreignFKeys).length + 1) 54 | relationships += this._oneToMany( 55 | tableName, 56 | primaryKey, 57 | refTableName, 58 | referencedBy[refTableName] 59 | ); 60 | // Many-to-many 61 | for (const foreignFKey in foreignFKeys) { 62 | if (tableName !== foreignFKeys[foreignFKey].referenceTable) { 63 | // Do not include original table in output 64 | const manyToManyTable = foreignFKeys[foreignFKey].referenceTable; 65 | const refKey = tables[tableName].referencedBy[refTableName]; 66 | const manyRefKey = tables[manyToManyTable].referencedBy[refTableName]; 67 | const { primaryKey: manyPrimaryKey } = tables[manyToManyTable]; 68 | 69 | relationships += this._manyToMany( 70 | tableName, 71 | primaryKey, 72 | refTableName, 73 | refKey, 74 | manyRefKey, 75 | manyToManyTable, 76 | manyPrimaryKey 77 | ); 78 | } 79 | } 80 | 81 | for (const FKTableName in tables[tableName].foreignKeys) { 82 | const object = tables[tableName].foreignKeys[FKTableName]; 83 | const refTableName = object.referenceTable; 84 | const refKey = object.referenceKey; 85 | 86 | const newQuery = this._FKTable(tableName, primaryKey, tableName, refKey, FKTableName, refTableName, primaryKey) 87 | if (!relationships.includes(newQuery)) relationships += newQuery 88 | } 89 | } 90 | relationships += ' },\n'; 91 | return relationships; 92 | }; 93 | 94 | ResolverGenerator._oneToOne = function oneToOne(tableName, primaryKey, refTableName, refKey) { 95 | return ( 96 | ` ${toCamelCase(refTableName)}: async (${toCamelCase(tableName)}) => {\n` + 97 | ' try {\n' + 98 | ` const query = \'SELECT * FROM ${refTableName} WHERE ${refKey} = $1\';\n` + 99 | ` const values = [${primaryKey}]\n` + 100 | ' return await db.query(query, values).then((res) => res.rows[0]);\n' + 101 | ' } catch (err) {\n' + 102 | ' //throw new Error(err)\n' + 103 | ' }\n' + 104 | ' },\n' 105 | ); 106 | }; 107 | 108 | ResolverGenerator._oneToMany = function oneToMany(tableName, primaryKey, refTableName, refKey) { 109 | return ( 110 | ` ${toCamelCase(refTableName)}: async (${toCamelCase(tableName)}) => {\n` + 111 | ' try {\n' + 112 | ` const query = \'SELECT * FROM ${refTableName} WHERE ${refKey} = $1\';\n` + 113 | ` const values = [${primaryKey}]\n` + 114 | ' return await db.query(query, values).then((res) => res.rows);\n' + 115 | ' } catch (err) {\n' + 116 | ' //throw new Error(err)\n' + 117 | ' }\n' + 118 | ' },\n' 119 | ); 120 | }; 121 | 122 | ResolverGenerator._manyToMany = function manyToMany( 123 | tableName, 124 | primaryKey, 125 | joinTableName, 126 | refKey, 127 | manyRefKey, 128 | manyTableName, 129 | manyPrimaryKey 130 | ) { 131 | const camTableName = toCamelCase(tableName); 132 | return ( 133 | ` ${toCamelCase(manyTableName)}: async (${camTableName}) => {\n` + 134 | ' try {\n' + 135 | ` const query = \'SELECT * FROM ${manyTableName} LEFT OUTER JOIN ${joinTableName} ON ${manyTableName}.${manyPrimaryKey} = ${joinTableName}.${manyRefKey} WHERE ${joinTableName}.${refKey} = $1\';\n` + 136 | ` const values = [${camTableName}.${primaryKey}]\n` + 137 | ' return await db.query(query, values).then((res) => res.rows);\n' + 138 | ' } catch (err) {\n' + 139 | ' //throw new Error(err)\n' + 140 | ' }\n' + 141 | ' },\n' 142 | ); 143 | }; 144 | 145 | ResolverGenerator._FKTable = function FKTable(tableName, primaryKey, joinTableName, refKey, manyRefKey, manyTableName, manyPrimaryKey) { 146 | const camTableName = toCamelCase(tableName); 147 | return ( 148 | ` ${toCamelCase(manyTableName)}: async (${camTableName}) => {\n` + 149 | ' try {\n' + 150 | ` const query = \'SELECT ${manyTableName}.* FROM ${manyTableName} LEFT OUTER JOIN ${joinTableName} ON ${manyTableName}.${manyPrimaryKey} = ${joinTableName}.${manyRefKey} WHERE ${joinTableName}.${refKey} = $1\';\n` + 151 | ` const values = [${camTableName}.${primaryKey}]\n` + 152 | ' return await db.query(query, values).then((res) => res.rows);\n' + 153 | ' } catch (err) {\n' + 154 | ' //throw new Error(err)\n' + 155 | ' }\n' + 156 | ' },\n' 157 | ); 158 | }; 159 | 160 | ResolverGenerator._createValues = function values(primaryKey, foreignKeys, columns) { 161 | let index = 1; 162 | for (let columnName in columns) { 163 | // if (!(foreignKeys && foreignKeys[columnName]) && columnName !== primaryKey) { // why? 164 | if (columnName !== primaryKey) { 165 | this._values[index++] = columnName; 166 | } 167 | } 168 | return this._values; 169 | }; 170 | 171 | ResolverGenerator._columnQuery = function column(tableName, primaryKey) { 172 | let byID = toCamelCase(singular(tableName)); 173 | if (byID === toCamelCase(tableName)) byID += 'ByID'; 174 | return ( 175 | ` ${byID}: (parent, args) => {\n` + 176 | ' try{\n' + 177 | ` const query = 'SELECT * FROM ${tableName} WHERE ${primaryKey} = $1';\n` + 178 | ` const values = [args.${primaryKey}];\n` + 179 | ' return db.query(query, values).then((res) => res.rows[0]);\n' + 180 | ' } catch (err) {\n' + 181 | ' throw new Error(err);\n' + 182 | ' }\n' + 183 | ' },' 184 | ); 185 | }; 186 | 187 | ResolverGenerator._allColumnQuery = function allColumn(tableName) { 188 | return ( 189 | ` ${toCamelCase(tableName)}: () => {\n` + 190 | ' try {\n' + 191 | ` const query = 'SELECT * FROM ${tableName}';\n` + 192 | ' return db.query(query).then((res) => res.rows);\n' + 193 | ' } catch (err) {\n' + 194 | ' throw new Error(err);\n' + 195 | ' }\n' + 196 | ' },' 197 | ); 198 | }; 199 | 200 | ResolverGenerator._createMutation = function createColumn( 201 | tableName, 202 | primaryKey, 203 | foreignKeys, 204 | columns 205 | ) { 206 | return ( 207 | ` ${toCamelCase(`create_${singular(tableName)}`)}: (parent, args) => {\n` + 208 | ` const query = 'INSERT INTO ${tableName}(${Object.values(this._values).join( 209 | ', ' 210 | )}) VALUES(${Object.keys(this._values) 211 | .map((x) => `$${x}`) 212 | .join(', ')})';\n` + 213 | ` const values = [${Object.values(this._values) 214 | .map((x) => `args.${x}`) 215 | .join(', ')}];\n` + 216 | ' try {\n' + 217 | ' return db.query(query, values);\n' + 218 | ' } catch (err) {\n' + 219 | ' throw new Error(err);\n' + 220 | ' }\n' + 221 | ' },' 222 | ); 223 | }; 224 | 225 | ResolverGenerator._updateMutation = function updateColumn( 226 | tableName, 227 | primaryKey, 228 | foreignKeys, 229 | columns 230 | ) { 231 | let displaySet = ''; 232 | for (const key in this._values) displaySet += `${this._values[key]}=$${key}, `; 233 | return ( 234 | ` ${toCamelCase(`update_${singular(tableName)}`)}: (parent, args) => {\n` + 235 | ' try {\n' + 236 | ` const query = 'UPDATE ${tableName} SET ${displaySet.slice(0, displaySet.length - 2)} WHERE ${primaryKey} = $${ 237 | Object.entries(this._values).length + 1 238 | }';\n` + 239 | ` const values = [${Object.values(this._values) 240 | .map((x) => `args.${x}`) 241 | .join(', ')}, args.${primaryKey}];\n` + 242 | ' return db.query(query, values).then((res) => res.rows);\n' + 243 | ' } catch (err) {\n' + 244 | ' throw new Error(err);\n' + 245 | ' }\n' + 246 | ' },' 247 | ); 248 | }; 249 | 250 | ResolverGenerator._deleteMutations = function deleteColumn(tableName, primaryKey) { 251 | return ( 252 | ` ${toCamelCase(`delete_${singular(tableName)}`)}: (parent, args) => {\n` + 253 | ' try {\n' + 254 | ` const query = 'DELETE FROM ${tableName} WHERE ${primaryKey} = $1';\n` + 255 | ` const values = [args.${primaryKey}];\n` + 256 | ' return db.query(query, values).then((res) => res.rows);\n' + 257 | ' } catch (err) {\n' + 258 | ' throw new Error(err);\n' + 259 | ' }\n' + 260 | ' },' 261 | ); 262 | }; 263 | 264 | export default ResolverGenerator; 265 | -------------------------------------------------------------------------------- /client/forceGraph/generators/schemaGenerator.js: -------------------------------------------------------------------------------- 1 | import TypeGenerator from './typeGenerator'; 2 | import ResolverGenerator from './resolverGenerator'; 3 | 4 | // assembles all programmatic schema and resolvers together in one string 5 | export const assembleSchema = (tables) => { 6 | let queryType = ''; 7 | let mutationType = ''; 8 | let customTypes = ''; 9 | let queryResolvers = ''; 10 | let mutationResolvers = ''; 11 | let relationshipResolvers = ''; 12 | for (const tableName in tables) { 13 | const tableData = tables[tableName]; 14 | const { foreignKeys, columns } = tableData; 15 | queryType += TypeGenerator.queries(tableName, tableData); 16 | mutationType += TypeGenerator.mutations(tableName, tableData); 17 | customTypes += TypeGenerator.customTypes(tableName, tables); 18 | if (!foreignKeys || Object.keys(columns).length !== Object.keys(foreignKeys).length + 1) { 19 | queryResolvers += ResolverGenerator.queries(tableName, tableData); 20 | mutationResolvers += ResolverGenerator.mutations(tableName, tableData); 21 | relationshipResolvers += ResolverGenerator.getRelationships(tableName, tables); 22 | } 23 | } 24 | return ( 25 | `${'const typeDefs = `\n' + ' type Query {\n'}${queryType} }\n\n` + 26 | ` type Mutation {${mutationType} }\n\n` + 27 | `${customTypes}\`;\n\n` + 28 | 'const resolvers = {\n' + 29 | ' Query: {' + 30 | ` ${queryResolvers}\n` + 31 | ' },\n\n' + 32 | ` Mutation: {\n${mutationResolvers} },\n${relationshipResolvers}}\n\n` + 33 | 'const schema = makeExecutableSchema({\n' + 34 | ' typeDefs,\n' + 35 | ' resolvers,\n' + 36 | '});\n\n' + 37 | 'module.exports = schema;' 38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /client/forceGraph/generators/typeGenerator.js: -------------------------------------------------------------------------------- 1 | import { singular } from 'pluralize'; 2 | import { 3 | toCamelCase, 4 | toPascalCase, 5 | typeSet, 6 | getPrimaryKeyType, 7 | } from './helperFunctions'; 8 | 9 | const TypeGenerator = {}; 10 | 11 | TypeGenerator.queries = function queries(tableName, tableData) { 12 | const { primaryKey, foreignKeys, columns } = tableData; 13 | const nameSingular = singular(tableName); 14 | const primaryKeyType = getPrimaryKeyType(primaryKey, columns); 15 | if (!foreignKeys || Object.keys(columns).length !== Object.keys(foreignKeys).length + 1) { 16 | // Do not output pure join tables 17 | let byID = toCamelCase(nameSingular); 18 | if (nameSingular === tableName) byID += 'ByID'; 19 | return ( 20 | ` ${toCamelCase(tableName)}: [${toPascalCase(nameSingular)}!]!\n` + 21 | ` ${byID}(${primaryKey}: ${primaryKeyType}!): ${toPascalCase(nameSingular)}!\n` 22 | ); 23 | } 24 | return ''; 25 | }; 26 | 27 | TypeGenerator.mutations = function mutations(tableName, tableData) { 28 | const { primaryKey, foreignKeys, columns } = tableData; 29 | if (!foreignKeys || Object.keys(columns).length !== Object.keys(foreignKeys).length + 1) { 30 | // Do not output pure join tables 31 | return ( 32 | this._create(tableName, primaryKey, foreignKeys, columns) + 33 | this._update(tableName, primaryKey, foreignKeys, columns) + 34 | this._destroy(tableName, primaryKey) 35 | ); 36 | } 37 | return ''; 38 | }; 39 | 40 | TypeGenerator.customTypes = function customTypes(tableName, tables) { 41 | const { primaryKey, foreignKeys, columns } = tables[tableName]; 42 | const primaryKeyType = getPrimaryKeyType(primaryKey, columns); 43 | if (foreignKeys === null || Object.keys(columns).length !== Object.keys(foreignKeys).length + 1) { 44 | return `${ 45 | ` type ${toPascalCase(singular(tableName))} {\n` + ` ${primaryKey}: ${primaryKeyType}!` 46 | }${this._columns(primaryKey, foreignKeys, columns)}${this._getRelationships( 47 | tableName, 48 | tables 49 | )}\n }\n\n`; 50 | } 51 | return ''; 52 | }; 53 | 54 | TypeGenerator._columns = function columns(primaryKey, foreignKeys, columns) { 55 | let colStr = ''; 56 | for (const columnName in columns) { 57 | if (!(foreignKeys && foreignKeys[columnName]) && columnName !== primaryKey) { 58 | const { dataType, isNullable, columnDefault } = columns[columnName]; 59 | colStr += `\n ${columnName}: ${typeSet(dataType)}`; 60 | if (isNullable === 'NO' && columnDefault === null) colStr += '!'; 61 | } 62 | } 63 | return colStr; 64 | }; 65 | 66 | // Get table relationships 67 | TypeGenerator._getRelationships = function getRelationships(tableName, tables) { 68 | let relationships = ''; 69 | const alreadyAddedType = []; // cache to track which relation has been added or not 70 | for (const refTableName in tables[tableName].referencedBy) { 71 | const { 72 | referencedBy: foreignRefBy, 73 | foreignKeys: foreignFKeys, 74 | columns: foreignColumns, 75 | } = tables[refTableName]; 76 | 77 | // One-to-one: when we can find tableName in foreignRefBy, that means this is a direct one to one relation 78 | if (foreignRefBy && foreignRefBy[tableName]) { 79 | if (!alreadyAddedType.includes(refTableName)) { 80 | // check if this refTableType has already been added by other tableName 81 | alreadyAddedType.push(refTableName); 82 | const refTableType = toPascalCase(singular(refTableName)); 83 | relationships += `\n ${toCamelCase(singular(reftableName))}: ${refTableType}`; 84 | } 85 | } 86 | 87 | // One-to-many: check if this is a join table, and if it's not, we can add relations) 88 | // example2: people table will meet this criteria 89 | // example3: species and people table will meet this criteria 90 | else if (Object.keys(foreignColumns).length !== Object.keys(foreignFKeys).length + 1) { 91 | if (!alreadyAddedType.includes(refTableName)) { 92 | // check if this refTableType has already been added by other tableName 93 | alreadyAddedType.push(refTableName); 94 | const refTableType = toPascalCase(singular(refTableName)); 95 | relationships += `\n ${toCamelCase(refTableName)}: [${refTableType}]`; 96 | } 97 | } 98 | 99 | // Many-to-many relations (so now handling join tables!) 100 | for (const foreignFKey in foreignFKeys) { 101 | 102 | if (tableName !== foreignFKeys[foreignFKey].referenceTable) { 103 | // Do not include original table in output 104 | if (!alreadyAddedType.includes(refTableName)) { 105 | // check if this refTableType has already been added by other tableName 106 | alreadyAddedType.push(refTableName); 107 | const manyToManyTable = toCamelCase(foreignFKeys[foreignFKey].referenceTable); 108 | 109 | relationships += `\n ${manyToManyTable}: [${toPascalCase(singular(manyToManyTable))}]`; 110 | } 111 | } 112 | } 113 | } 114 | for (const FKTableName in tables[tableName].foreignKeys) { 115 | const object = tables[tableName].foreignKeys[FKTableName]; 116 | const refTableName = object.referenceTable; 117 | const refTableType = toPascalCase(singular(refTableName)); 118 | relationships += `\n ${toCamelCase(refTableName)}: [${refTableType}]`; 119 | } 120 | 121 | return relationships; 122 | }; 123 | 124 | TypeGenerator._create = function create(tableName, primaryKey, foreignKeys, columns) { 125 | return `\n ${toCamelCase(`create_${singular(tableName)}`)}(\n${this._typeParams( 126 | primaryKey, 127 | foreignKeys, 128 | columns, 129 | false 130 | )}): ${toPascalCase(singular(tableName))}!\n`; 131 | }; 132 | 133 | TypeGenerator._update = function update(tableName, primaryKey, foreignKeys, columns) { 134 | return `\n ${toCamelCase(`update_${singular(tableName)}`)}(\n${this._typeParams( 135 | primaryKey, 136 | foreignKeys, 137 | columns, 138 | true 139 | )}): ${toPascalCase(singular(tableName))}!\n`; 140 | }; 141 | 142 | TypeGenerator._destroy = function destroy(tableName, primaryKey) { 143 | return `\n ${toCamelCase(`delete_${singular(tableName)}`)}(${primaryKey}: ID!): ${toPascalCase( 144 | singular(tableName) 145 | )}!\n`; 146 | }; 147 | 148 | TypeGenerator._typeParams = function addParams(primaryKey, foreignKeys, columns, needId) { 149 | let typeDef = ''; 150 | for (const columnName in columns) { 151 | const { dataType, isNullable } = columns[columnName]; 152 | if (!needId && columnName === primaryKey) { 153 | // handle mutation on creating 154 | continue; // we don't need Id during creating, so skip this loop when columnName === primaryKey 155 | } 156 | 157 | if (needId && columnName === primaryKey) { 158 | // handle mutation on updating (will need Id) 159 | typeDef += ` ${columnName}: ${typeSet(dataType)}!,\n`; // automatically add '!,\n' (not null) 160 | } else { 161 | typeDef += ` ${columnName}: ${typeSet(dataType)}`; 162 | if (isNullable !== 'YES') typeDef += '!'; 163 | typeDef += ',\n'; 164 | } 165 | // } 166 | } 167 | if (typeDef !== '') typeDef += ' '; 168 | return typeDef; 169 | }; 170 | 171 | export default TypeGenerator; 172 | 173 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 13 | 14 | 15 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | Welcome to lucidQL 28 | 29 | 30 | 31 |
32 | 33 | 39 | 40 | 41 | 42 |
43 | 44 | 45 | -------------------------------------------------------------------------------- /client/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App.tsx'; 4 | 5 | ReactDOM.render( 6 | 7 | 8 | , 9 | document.getElementById('root') 10 | ); 11 | -------------------------------------------------------------------------------- /client/styles.css: -------------------------------------------------------------------------------- 1 | .App { 2 | font-family: sans-serif; 3 | text-align: center; 4 | } 5 | 6 | body { 7 | margin: 0; 8 | padding: 0; 9 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 10 | 'Droid Sans', 'Helvetica Neue', sans-serif; 11 | -webkit-font-smoothing: antialiased; 12 | -moz-osx-font-smoothing: grayscale; 13 | box-sizing: border-box; 14 | overflow: hidden; 15 | } 16 | 17 | *, 18 | *:before, 19 | *:after { 20 | box-sizing: inherit; 21 | } 22 | 23 | /* ---Nav Bar CSS--- */ 24 | .sticky-nav { 25 | position: sticky; 26 | top: 0; 27 | width: 100%; 28 | z-index: 10; 29 | border-bottom: 1px solid rgb(130, 130, 130); 30 | color: black; 31 | } 32 | 33 | .btn { 34 | background-color: #967bb6; 35 | color: white; 36 | } 37 | 38 | .secondary-btn { 39 | background: #f6f6f7; 40 | color: black; 41 | } 42 | 43 | .logo { 44 | width: 1.5%; 45 | } 46 | 47 | .logo-brand { 48 | width: 15%; 49 | margin: 0px auto; 50 | } 51 | 52 | .footer { 53 | position: sticky; 54 | background-color: #f5f5f5; 55 | bottom: 0; 56 | width: 100%; 57 | z-index: 10; 58 | border-top: 1px solid rgb(203, 201, 201); 59 | } 60 | 61 | /* .footer-container { 62 | display: flex; 63 | width: 100%; 64 | align-items: center; 65 | justify-content: right; 66 | } */ 67 | 68 | /* ---Nav Bar CSS--- END*/ 69 | 70 | /* D3/Visgraph Div */ 71 | .graph-div { 72 | display: flex; 73 | align-items: center; 74 | justify-content: space-evenly; 75 | height: 100%; 76 | padding: 1rem; 77 | background-color: #f5f5f5; 78 | } 79 | 80 | .annotation { 81 | position: fixed; 82 | bottom: 6vh; 83 | left: 1vw; 84 | z-index: 2; 85 | } 86 | 87 | .SVG_container { 88 | width: 100%; 89 | height: 100%; 90 | position: relative; 91 | } 92 | 93 | .primary_node { 94 | font-size: 24px; 95 | fill: white; 96 | } 97 | 98 | .column_node { 99 | font-size: 12px; 100 | fill: white 101 | } 102 | 103 | div.tooltip { 104 | position: absolute; 105 | text-align: center; 106 | width: 200px; 107 | padding: 10px; 108 | font: 14px sans-serif; 109 | background: #1a1a2e; 110 | color: white; 111 | border: 0; 112 | border-radius: 8px; 113 | pointer-events: none; 114 | z-index: 5; 115 | line-height: 1.5rem; 116 | } 117 | 118 | /* ---Codebox CSS--- */ 119 | .code-box { 120 | display: inline-flexbox; 121 | width: 100%; 122 | background-color: black; 123 | } 124 | 125 | .CodeMirror-hscrollbar { 126 | overflow: hidden; 127 | } 128 | 129 | .CodeMirror { 130 | width: 100%; 131 | height: 90vh; 132 | overflow: hidden; 133 | } 134 | 135 | /* ----POPUP LINK BOX---- */ 136 | .modal-cover { 137 | position: fixed; 138 | top: 0; 139 | left: 0; 140 | width: 100%; 141 | height: 100%; 142 | z-index: 10; 143 | transform: translateZ(0); 144 | background-color: rgba(0, 0, 0, 0.5); 145 | } 146 | 147 | .modal-area { 148 | position: fixed; 149 | top: 0; 150 | left: 0; 151 | width: 100%; 152 | height: 100%; 153 | padding: 5em 3em 3em 3em; 154 | background-color: #ffffff; 155 | box-shadow: 0 0 10px 3px rgba(0, 0, 0, 0.1); 156 | overflow-y: auto; 157 | -webkit-overflow-scrolling: touch; 158 | border-radius: 3%; 159 | } 160 | 161 | @media screen and (min-width: 500px) { 162 | .modal-area { 163 | left: 50%; 164 | top: 50%; 165 | height: auto; 166 | transform: translate(-50%, -50%); 167 | max-width: 55em; 168 | max-height: calc(100% - 1em); 169 | } 170 | } 171 | 172 | ._modal-close { 173 | position: absolute; 174 | top: 0; 175 | right: 0; 176 | padding: 0.5em; 177 | line-height: 1; 178 | background: #f6f6f7; 179 | border: 0; 180 | box-shadow: 0; 181 | cursor: pointer; 182 | } 183 | 184 | ._modal-close-icon { 185 | width: 25px; 186 | height: 25px; 187 | fill: transparent; 188 | stroke: black; 189 | stroke-linecap: round; 190 | stroke-width: 2; 191 | } 192 | 193 | .modal-body { 194 | padding-top: 0.25em; 195 | } 196 | 197 | ._hide-visual { 198 | border: 0 !important; 199 | clip: rect(0 0 0 0) !important; 200 | height: 1px !important; 201 | margin: -1px !important; 202 | overflow: hidden !important; 203 | padding: 0 !important; 204 | position: absolute !important; 205 | width: 1px !important; 206 | white-space: nowrap !important; 207 | } 208 | 209 | .scroll-lock { 210 | overflow: hidden; 211 | /* margin-right: 17px; */ 212 | } 213 | 214 | /* ----POPUP LINK BOX---- END*/ 215 | 216 | /* ---SIDEBAR---- */ 217 | 218 | .sidebar { 219 | height: 100%; 220 | width: 0; 221 | position: fixed; 222 | z-index: 1; 223 | top: 0; 224 | left: 0; 225 | background-color: #111; 226 | overflow-x: hidden; 227 | transition: 0.5s; 228 | padding-top: 60px; 229 | } 230 | 231 | .sidebar a { 232 | padding: 8px 8px 8px 32px; 233 | text-decoration: none; 234 | font-size: 25px; 235 | color: #818181; 236 | display: block; 237 | transition: 0.3s; 238 | } 239 | 240 | .sidebar a:hover { 241 | color: #f1f1f1; 242 | } 243 | 244 | .sidebar .closebtn { 245 | position: absolute; 246 | top: 0; 247 | right: 25px; 248 | font-size: 36px; 249 | margin-left: 50px; 250 | } 251 | 252 | .openbtn { 253 | font-size: 20px; 254 | cursor: pointer; 255 | color: black; 256 | padding: 10px 15px; 257 | border: none; 258 | margin: auto; 259 | /* margin-top: 20px; */ 260 | } 261 | 262 | .openbtn:hover { 263 | background-color: #444; 264 | } 265 | 266 | #main { 267 | transition: margin-left 0.5s; 268 | margin: 0; 269 | } 270 | 271 | /* On smaller screens, where height is less than 450px, change the style of the sidenav (less padding and a smaller font size) */ 272 | @media screen and (max-height: 450px) { 273 | .sidebar { 274 | padding-top: 15px; 275 | } 276 | .sidebar a { 277 | font-size: 18px; 278 | } 279 | } 280 | 281 | /* Resizer bar */ 282 | 283 | .sc-htpNat.fXAXjb.sc-bdVaJa.cjjWdp { 284 | width: 19px; 285 | margin: 0 -5px; 286 | border-left: 5px solid rgba(255, 255, 255, 1); 287 | border-right: 5px solid rgba(255, 255, 255, 1); 288 | } 289 | 290 | .sc-htpNat.fXAXjb.sc-bdVaJa.cjjWdp:hover { 291 | width: 22px; 292 | margin: 0 -6px; 293 | cursor: col-resize; 294 | -webkit-transition: all 0.1s ease; 295 | transition: all 0.1s ease; 296 | } 297 | 298 | hr { 299 | background-color: rgb(211, 211, 211); 300 | } 301 | -------------------------------------------------------------------------------- /client/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 | 15 | 16 | 125 | -------------------------------------------------------------------------------- /docker-compose-test.yml: -------------------------------------------------------------------------------- 1 | version: '3.0' 2 | services: 3 | test: 4 | image: dockerizedgraphql/cql-dev 5 | container_name: 'cql-test' 6 | ports: 7 | - '8081:8081' 8 | volumes: 9 | - .:/usr/src/app 10 | - node_modules:/usr/src/app/node_modules 11 | command: npm test 12 | volumes: 13 | node_modules: 14 | -------------------------------------------------------------------------------- /jest-setup.js: -------------------------------------------------------------------------------- 1 | module.exports = async () => { 2 | global.testServer = await require('./server/server.js'); 3 | }; 4 | -------------------------------------------------------------------------------- /jest-teardown.js: -------------------------------------------------------------------------------- 1 | module.exports = async (globalConfig) => { 2 | await testServer.close(); 3 | }; 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "LucidQL", 3 | "version": "0.1.0", 4 | "description": "A developer tool that helps developers migrate to a GraphQL API from an established relational database and allows the user to view and simplify these relations.", 5 | "authors": "Darwin Sinchi, Martin Chiang, Stanley Huang", 6 | "license": "MIT", 7 | "bugs": { 8 | "url": "https://github.com/oslabs-beta/lucidQL/issues" 9 | }, 10 | "homepage": "", 11 | "main": "index.js", 12 | "scripts": { 13 | "test": "NODE_ENV=test jest --verbose", 14 | "start": "nodemon server/server.js", 15 | "build": "webpack --mode production", 16 | "dev": "nodemon server/server.js & webpack-dev-server --mode development --open --hot", 17 | "dev:hot": "NODE_ENV=development nodemon ./server/server.js & NODE_ENV=development webpack-dev-server --open --hot --inline --progress --colors --watch --content-base ./" 18 | }, 19 | "jest": { 20 | "globalSetup": "./jest-setup.js", 21 | "globalTeardown": "./jest-teardown.js", 22 | "moduleNameMapper": { 23 | "\\.(jpg|ico|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/__mocks__/fileMock.js", 24 | "\\.(css|less)$": "/__mocks__/fileMock.js" 25 | } 26 | }, 27 | "dependencies": { 28 | "@babel/plugin-transform-async-to-generator": "^7.10.4", 29 | "@babel/plugin-transform-runtime": "^7.11.5", 30 | "@babel/runtime": "^7.11.2", 31 | "@types/d3": "^5.7.2", 32 | "@types/d3-selection-multi": "^1.0.8", 33 | "@types/file-saver": "^2.0.1", 34 | "@types/react": "^16.9.49", 35 | "@types/react-dom": "^16.9.8", 36 | "@types/recoil": "0.0.1", 37 | "codemirror": "^5.57.0", 38 | "d3": "^6.1.1", 39 | "d3-selection": "^2.0.0", 40 | "d3-selection-multi": "^1.0.1", 41 | "enzyme": "^3.11.0", 42 | "express": "^4.17.1", 43 | "express-graphql": "^0.9.0", 44 | "file-saver": "^2.0.2", 45 | "focus-trap-react": "^8.0.0", 46 | "fs": "0.0.1-security", 47 | "graphql": "^15.3.0", 48 | "graphql-playground-middleware-express": "^1.7.20", 49 | "graphql-playground-react": "^1.7.26", 50 | "graphql-tools": "^6.2.3", 51 | "jszip": "^3.5.0", 52 | "mysql2": "^2.2.3", 53 | "pg": "^8.3.3", 54 | "pg-hstore": "^2.3.3", 55 | "pluralize": "^8.0.0", 56 | "react": "^16.13.1", 57 | "react-bootstrap": "^1.3.0", 58 | "react-codemirror2": "^7.2.1", 59 | "react-dom": "^16.13.1", 60 | "react-graph-vis": "^1.0.5", 61 | "react-router": "^5.2.0", 62 | "react-split-pane": "^2.0.3", 63 | "recoil": "0.0.10", 64 | "regenerator-runtime": "^0.13.7", 65 | "vis-react": "^0.5.1" 66 | }, 67 | "devDependencies": { 68 | "@babel/core": "^7.11.5", 69 | "@babel/plugin-proposal-class-properties": "^7.10.4", 70 | "@babel/plugin-proposal-object-rest-spread": "^7.11.0", 71 | "@babel/preset-env": "^7.11.5", 72 | "@babel/preset-react": "^7.10.4", 73 | "@babel/preset-typescript": "^7.10.4", 74 | "babel-core": "^6.26.3", 75 | "babel-jest": "^26.3.0", 76 | "babel-loader": "^8.1.0", 77 | "css-loader": "^4.2.2", 78 | "enzyme-adapter-react-16": "^1.15.4", 79 | "eslint": "^7.2.0", 80 | "eslint-config-airbnb": "^18.2.0", 81 | "eslint-config-prettier": "^6.11.0", 82 | "eslint-plugin-import": "^2.22.0", 83 | "eslint-plugin-jsx-a11y": "^6.3.1", 84 | "eslint-plugin-prettier": "^3.1.4", 85 | "eslint-plugin-react": "^7.20.6", 86 | "eslint-plugin-react-hooks": "^4.0.0", 87 | "html-webpack-plugin": "^4.4.1", 88 | "jest": "^26.4.2", 89 | "jest-puppeteer": "^3.4.0", 90 | "nodemon": "^2.0.4", 91 | "prettier": "^2.1.1", 92 | "puppeteer": "^5.3.1", 93 | "style-loader": "^1.2.1", 94 | "supertest": "^4.0.2", 95 | "ts-loader": "^8.0.3", 96 | "tslint": "^6.1.3", 97 | "tslint-immutable": "^6.0.1", 98 | "typescript": "^4.0.2", 99 | "url-loader": "^4.1.0", 100 | "webpack": "^4.44.1", 101 | "webpack-cli": "^3.3.12", 102 | "webpack-dev-server": "^3.11.0" 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /public/icon/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/LucidQL/79367dbf76736faa4e42ef04db7c3e5668f1d7f5/public/icon/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/icon/android-chrome-384x384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/LucidQL/79367dbf76736faa4e42ef04db7c3e5668f1d7f5/public/icon/android-chrome-384x384.png -------------------------------------------------------------------------------- /public/icon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/LucidQL/79367dbf76736faa4e42ef04db7c3e5668f1d7f5/public/icon/apple-touch-icon.png -------------------------------------------------------------------------------- /public/icon/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #da532c 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /public/icon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/LucidQL/79367dbf76736faa4e42ef04db7c3e5668f1d7f5/public/icon/favicon-16x16.png -------------------------------------------------------------------------------- /public/icon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/LucidQL/79367dbf76736faa4e42ef04db7c3e5668f1d7f5/public/icon/favicon-32x32.png -------------------------------------------------------------------------------- /public/icon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/LucidQL/79367dbf76736faa4e42ef04db7c3e5668f1d7f5/public/icon/favicon.ico -------------------------------------------------------------------------------- /public/icon/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 14 | 15 | 19 | 20 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | lucidQL 37 | 38 | 39 | 40 |
41 | 51 | 56 | 61 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /public/icon/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/LucidQL/79367dbf76736faa4e42ef04db7c3e5668f1d7f5/public/icon/logo192.png -------------------------------------------------------------------------------- /public/icon/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/LucidQL/79367dbf76736faa4e42ef04db7c3e5668f1d7f5/public/icon/mstile-150x150.png -------------------------------------------------------------------------------- /public/icon/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/icon/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 17 | 19 | 21 | 26 | 31 | 33 | 35 | 37 | 39 | 41 | 43 | 45 | 50 | 55 | 57 | 59 | 61 | 63 | 65 | 71 | 76 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /public/icon/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-384x384.png", 12 | "sizes": "384x384", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /public/logo-for-github.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/LucidQL/79367dbf76736faa4e42ef04db7c3e5668f1d7f5/public/logo-for-github.jpg -------------------------------------------------------------------------------- /public/lucidQL-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/LucidQL/79367dbf76736faa4e42ef04db7c3e5668f1d7f5/public/lucidQL-logo.png -------------------------------------------------------------------------------- /public/lucidQL.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/LucidQL/79367dbf76736faa4e42ef04db7c3e5668f1d7f5/public/lucidQL.png -------------------------------------------------------------------------------- /scripts/deploy.sh: -------------------------------------------------------------------------------- 1 | echo "Processing deploy.sh" 2 | # Set EB BUCKET as env variable 3 | EB_BUCKET=elasticbeanstalk-us-east-1-944451176696 4 | # Set the default region for aws cli 5 | aws configure set default.region us-east-1 6 | # Log in to ECR 7 | eval $(aws ecr get-login --no-include-email --region us-east-1) 8 | # Build docker image based on our production Dockerfile 9 | docker build -t [orgname]/mm . 10 | # tag the image with the Travis-CI SHA 11 | docker tag [orgname]/mm:latest http://944451176696.dkr.ecr.us-east-1.amazonaws.com/cql:$TRAVIS_COMMIT 12 | # Push built image to ECS 13 | docker push http://944451176696.dkr.ecr.us-east-1.amazonaws.com/cql:$TRAVIS_COMMIT 14 | # Use the linux sed command to replace the text '' in our Dockerrun file with the Travis-CI SHA key 15 | sed -i='' "s//$TRAVIS_COMMIT/" Dockerrun.aws.json 16 | # Zip up our codebase, along with modified Dockerrun and our .ebextensions directory 17 | zip -r mm-prod-deploy.zip Dockerrun.aws.json .ebextensions 18 | # Upload zip file to s3 bucket 19 | aws s3 cp mm-prod-deploy.zip s3://$EB_BUCKET/mm-prod-deploy.zip 20 | # Create a new application version with new Dockerrun 21 | aws elasticbeanstalk create-application-version --application-name lucidQL --version-label $TRAVIS_COMMIT --source-bundle S3Bucket=$EB_BUCKET,S3Key=mm-prod-deploy.zip 22 | # Update environment to use new version number 23 | aws elasticbeanstalk update-environment --environment-name lucidql-env --version-label $TRAVIS_COMMIT -------------------------------------------------------------------------------- /server/SDL-definedSchemas/controllers/mySQLController.js: -------------------------------------------------------------------------------- 1 | const mysql = require('mysql2'); 2 | 3 | const mySQLController = {}; 4 | 5 | // This method is suitable for older SQL databases 6 | mySQLController.getTables = (req, res, next) => { 7 | const config = { 8 | host: req.body.host, 9 | user: req.body.user, 10 | password: req.body.password, 11 | database: req.body.database, 12 | }; 13 | const connection = mysql.createConnection(config); 14 | 15 | connection.query('SHOW tables', async (err, results, fields) => { 16 | if (err) { 17 | return res.json('error'); 18 | } 19 | const allTables = {}; 20 | const tables = []; 21 | for (let i = 0; i < results.length; i++) { 22 | const table = Object.values(results[i])[0]; 23 | allTables[table] = {}; 24 | tables.push(table); 25 | } 26 | const columns = []; 27 | const keys = []; 28 | for (let i = 0; i < tables.length; i++) { 29 | const data = await connection.promise().query(`SHOW KEYS FROM ${tables[i]};`); 30 | const key = data[0]; 31 | allTables[tables[i]].primaryKey = {}; 32 | allTables[tables[i]].foreignKeys = {}; 33 | for (let j = 0; j < key.length; j++) { 34 | const { Key_name, Column_name } = key[j]; 35 | if (Key_name === 'PRIMARY') { 36 | allTables[tables[i]].primaryKey = Column_name; 37 | } else { 38 | allTables[tables[i]].foreignKeys[Key_name] = {}; 39 | allTables[tables[i]].foreignKeys[Key_name].referenceKey = allTables[tables[i]].primaryKey; 40 | } 41 | } 42 | keys.push(key); 43 | } 44 | for (let i = 0; i < tables.length; i++) { 45 | const data = await connection.promise().query(`SHOW FIELDS FROM ${tables[i]};`); 46 | const column = data[0]; 47 | allTables[tables[i]].columns = {}; 48 | for (let j = 0; j < column.length; j++) { 49 | allTables[tables[i]].columns[column[j].Field] = {}; 50 | const { Field, Null } = column[j]; 51 | allTables[tables[i]].columns[column[j].Field].dataType = Field; 52 | allTables[tables[i]].columns[column[j].Field].isNullable = Null; 53 | } 54 | columns.push(column); 55 | } 56 | 57 | res.locals.tables = allTables; 58 | 59 | return next(); 60 | }); 61 | }; 62 | 63 | module.exports = mySQLController; 64 | -------------------------------------------------------------------------------- /server/SDL-definedSchemas/controllers/pgController.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-restricted-syntax */ 2 | /* eslint-disable guard-for-in */ 3 | const fs = require('fs'); 4 | const { Pool } = require('pg'); 5 | const SchemaGenerator = require('../generators/schemaGenerator'); 6 | 7 | const pgQuery = fs.readFileSync('server/queries/tableData.sql', 'utf8'); 8 | 9 | const pgController = {}; 10 | 11 | // Middleware function for recovering info from pg tables 12 | pgController.getPGTables = (req, res, next) => { 13 | const db = new Pool({ connectionString: req.body.uri.trim() }); 14 | 15 | db.query(pgQuery) 16 | .then((data) => { 17 | res.locals.tables = data.rows[0].tables; 18 | return next(); 19 | }) 20 | .catch((err) => res.json('error')); 21 | 22 | console.log(req.body.uri); 23 | }; 24 | 25 | // Middleware function for assembling SDL schema 26 | pgController.assembleSDLSchema = (req, res, next) => { 27 | try { 28 | res.locals.SDLSchema = SchemaGenerator.assembleSchema(res.locals.tables); 29 | return next(); 30 | } catch (err) { 31 | return next({ 32 | log: err, 33 | status: 500, 34 | message: { err: 'There was a problem assembling SDL schema' }, 35 | }); 36 | } 37 | }; 38 | 39 | pgController.compileData = (req, res, next) => { 40 | try { 41 | const OriginalTables = res.locals.tables; 42 | const newTables = {}; 43 | 44 | for (const table in OriginalTables) { 45 | const currentTable = OriginalTables[table]; 46 | if ( 47 | !currentTable.foreignKeys || 48 | Object.keys(currentTable.columns).length !== Object.keys(currentTable.foreignKeys).length + 1 49 | ) { 50 | const pointsTo = []; 51 | for (const objName in currentTable.foreignKeys) { 52 | pointsTo.push(currentTable.foreignKeys[objName].referenceTable); 53 | } 54 | const referecedBy = []; 55 | for (const refTableName in currentTable.referencedBy) { 56 | if ( 57 | !OriginalTables[refTableName].foreignKeys || 58 | Object.keys(OriginalTables[refTableName].columns).length !== 59 | Object.keys(OriginalTables[refTableName].foreignKeys).length + 1 60 | ) { 61 | referecedBy.push(refTableName); 62 | } else { 63 | // else it's a join table 64 | for (const foreignKey in OriginalTables[refTableName].foreignKeys) { 65 | const joinedTable = OriginalTables[refTableName].foreignKeys[foreignKey].referenceTable; 66 | if (joinedTable !== table) { 67 | referecedBy.push(joinedTable); 68 | } 69 | } 70 | } 71 | } 72 | const columns = []; 73 | for (const columnName in currentTable.columns) { 74 | if (columnName !== currentTable.primaryKey) { 75 | columns.push(columnName); 76 | } 77 | } 78 | 79 | newTables[table] = { 80 | pointsTo, 81 | referecedBy, 82 | columns, 83 | }; 84 | } 85 | } 86 | res.locals.d3Data = newTables; 87 | return next(); 88 | } catch (err) { 89 | return next({ 90 | log: err, 91 | status: 500, 92 | message: { 93 | err: 'There was a problem compiling tables in pgController.compileData', 94 | }, 95 | }); 96 | } 97 | }; 98 | 99 | module.exports = pgController; 100 | -------------------------------------------------------------------------------- /server/SDL-definedSchemas/generators/resolverGenerator.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle */ 2 | /* eslint-disable guard-for-in */ 3 | /* eslint-disable no-restricted-syntax */ 4 | // eslint-disable-next-line prettier/prettier 5 | // prettier-ignore 6 | const { singular } = require('pluralize'); 7 | const { toCamelCase, toPascalCase } = require('../helpers/helperFunctions'); 8 | 9 | const ResolverGenerator = { 10 | _values: {}, 11 | }; 12 | 13 | ResolverGenerator.reset = function () { 14 | this._values = {}; 15 | }; 16 | 17 | ResolverGenerator.queries = function queries(tableName, { primaryKey }) { 18 | this.reset(); 19 | return `\n${this._columnQuery(tableName, primaryKey)}` + `\n${this._allColumnQuery(tableName)}`; 20 | }; 21 | 22 | ResolverGenerator.mutations = function mutations(tableName, tableData) { 23 | const { primaryKey, foreignKeys, columns } = tableData; 24 | this._createValues(primaryKey, foreignKeys, columns); 25 | return ( 26 | `${this._createMutation(tableName, primaryKey, foreignKeys, columns)}\n` + 27 | `${this._updateMutation(tableName, primaryKey, foreignKeys, columns)}\n` + 28 | `${this._deleteMutations(tableName, primaryKey)}\n\n` 29 | ); 30 | }; 31 | 32 | ResolverGenerator.getRelationships = function getRelationships(tableName, tables) { 33 | 34 | const { primaryKey, referencedBy } = tables[tableName]; 35 | if (!referencedBy) return ''; 36 | let relationships = `\n ${toPascalCase(singular(tableName))}: {\n`; 37 | for (const refTableName in referencedBy) { 38 | const { referencedBy: foreignRefBy, foreignKeys: foreignFKeys, columns: foreignColumns } = tables[refTableName]; 39 | const refTableType = toPascalCase(singular(refTableName)); 40 | // One-to-one 41 | if (foreignRefBy && foreignRefBy[tableName]) relationships += this._oneToOne(tableName, primaryKey, refTableName, referencedBy[refTableName]); 42 | // One-to-many 43 | else if (Object.keys(foreignColumns).length !== Object.keys(foreignFKeys).length + 1) relationships += this._oneToMany(tableName, primaryKey, refTableName, referencedBy[refTableName]); 44 | // Many-to-many 45 | for (const foreignFKey in foreignFKeys) { 46 | if (tableName !== foreignFKeys[foreignFKey].referenceTable) { 47 | // Do not include original table in output 48 | const manyToManyTable = foreignFKeys[foreignFKey].referenceTable; 49 | const refKey = tables[tableName].referencedBy[refTableName]; 50 | const manyRefKey = tables[manyToManyTable].referencedBy[refTableName]; 51 | const { primaryKey: manyPrimaryKey } = tables[manyToManyTable]; 52 | 53 | relationships += this._manyToMany(tableName, primaryKey, refTableName, refKey, manyRefKey, manyToManyTable, manyPrimaryKey); 54 | } 55 | } 56 | 57 | for (const FKTableName in tables[tableName].foreignKeys) { 58 | const object = tables[tableName].foreignKeys[FKTableName]; 59 | const refTableName = object.referenceTable; 60 | const refKey = object.referenceKey; 61 | const newQuery = this._FKTable(tableName, primaryKey, tableName, refKey, FKTableName, refTableName, primaryKey) 62 | if (!relationships.includes(newQuery)) relationships += newQuery 63 | } 64 | } 65 | relationships += ' },\n'; 66 | return relationships; 67 | }; 68 | 69 | ResolverGenerator._oneToOne = function oneToOne(tableName, primaryKey, refTableName, refKey) { 70 | return ( 71 | ` ${toCamelCase(refTableName)}: async (${toCamelCase(tableName)}) => {\n` + 72 | ' try {\n' + 73 | ` const query = \'SELECT * FROM ${refTableName} WHERE ${refKey} = $1\';\n` + 74 | ` const values = [${primaryKey}]\n` + 75 | ' return await db.query(query, values).then((res) => res.rows[0]);\n' + 76 | ' } catch (err) {\n' + 77 | ' //throw new Error(err)\n' + 78 | ' }\n' + 79 | ' },\n' 80 | ); 81 | }; 82 | 83 | ResolverGenerator._oneToMany = function oneToMany(tableName, primaryKey, refTableName, refKey) { 84 | return ( 85 | ` ${toCamelCase(refTableName)}: async (${toCamelCase(tableName)}) => {\n` + 86 | ' try {\n' + 87 | ` const query = \'SELECT * FROM ${refTableName} WHERE ${refKey} = $1\';\n` + 88 | ` const values = [${tableName}.${primaryKey}]\n` + 89 | ' return await db.query(query, values).then((res) => res.rows);\n' + 90 | ' } catch (err) {\n' + 91 | ' //throw new Error(err)\n' + 92 | ' }\n' + 93 | ' },\n' 94 | ); 95 | }; 96 | 97 | ResolverGenerator._manyToMany = function manyToMany(tableName, primaryKey, joinTableName, refKey, manyRefKey, manyTableName, manyPrimaryKey) { 98 | const camTableName = toCamelCase(tableName); 99 | return ( 100 | ` ${toCamelCase(manyTableName)}: async (${camTableName}) => {\n` + 101 | ' try {\n' + 102 | ` const query = \'SELECT * FROM ${manyTableName} LEFT OUTER JOIN ${joinTableName} ON ${manyTableName}.${manyPrimaryKey} = ${joinTableName}.${manyRefKey} WHERE ${joinTableName}.${refKey} = $1\';\n` + 103 | ` const values = [${camTableName}.${primaryKey}]\n` + 104 | ' return await db.query(query, values).then((res) => res.rows);\n' + 105 | ' } catch (err) {\n' + 106 | ' //throw new Error(err)\n' + 107 | ' }\n' + 108 | ' },\n' 109 | ); 110 | }; 111 | 112 | ResolverGenerator._FKTable = function FKTable(tableName, primaryKey, joinTableName, refKey, manyRefKey, manyTableName, manyPrimaryKey) { 113 | const camTableName = toCamelCase(tableName); 114 | return ( 115 | ` ${toCamelCase(manyTableName)}: async (${camTableName}) => {\n` + 116 | ' try {\n' + 117 | ` const query = \'SELECT ${manyTableName}.* FROM ${manyTableName} LEFT OUTER JOIN ${joinTableName} ON ${manyTableName}.${manyPrimaryKey} = ${joinTableName}.${manyRefKey} WHERE ${joinTableName}.${refKey} = $1\';\n` + 118 | ` const values = [${camTableName}.${primaryKey}]\n` + 119 | ' return await db.query(query, values).then((res) => res.rows);\n' + 120 | ' } catch (err) {\n' + 121 | ' //throw new Error(err)\n' + 122 | ' }\n' + 123 | ' },\n' 124 | ); 125 | }; 126 | 127 | ResolverGenerator._createValues = function values(primaryKey, foreignKeys, columns) { 128 | let index = 1; 129 | for (let columnName in columns) { 130 | // if (!(foreignKeys && foreignKeys[columnName]) && columnName !== primaryKey) { // why 131 | if (columnName !== primaryKey) { 132 | this._values[index++] = columnName; 133 | } 134 | } 135 | return this._values; 136 | }; 137 | 138 | ResolverGenerator._columnQuery = function column(tableName, primaryKey) { 139 | let byID = toCamelCase(singular(tableName)); 140 | if (byID === toCamelCase(tableName)) byID += 'ByID'; 141 | return ( 142 | ` ${byID}: (parent, args) => {\n` + 143 | ' try{\n' + 144 | ` const query = 'SELECT * FROM ${tableName} WHERE ${primaryKey} = $1';\n` + 145 | ` const values = [args.${primaryKey}];\n` + 146 | ' return db.query(query, values).then((res) => res.rows[0]);\n' + 147 | ' } catch (err) {\n' + 148 | ' throw new Error(err);\n' + 149 | ' }\n' + 150 | ' },' 151 | ); 152 | }; 153 | 154 | ResolverGenerator._allColumnQuery = function allColumn(tableName) { 155 | return ( 156 | ` ${toCamelCase(tableName)}: () => {\n` + 157 | ' try {\n' + 158 | ` const query = 'SELECT * FROM ${tableName}';\n` + 159 | ' return db.query(query).then((res) => res.rows);\n' + 160 | ' } catch (err) {\n' + 161 | ' throw new Error(err);\n' + 162 | ' }\n' + 163 | ' },' 164 | ); 165 | }; 166 | 167 | ResolverGenerator._createMutation = function createColumn(tableName, primaryKey, foreignKeys, columns) { 168 | return ( 169 | ` ${toCamelCase(`create_${singular(tableName)}`)}: (parent, args) => {\n` + 170 | ` const query = 'INSERT INTO ${tableName}(${Object.values(this._values).join(', ')}) VALUES(${Object.keys(this._values) 171 | .map((x) => `$${x}`) 172 | .join(', ')}) RETURNING *';\n` + 173 | ` const values = [${Object.values(this._values) 174 | .map((x) => `args.${x}`) 175 | .join(', ')}];\n` + 176 | ' try {\n' + 177 | ' return db.query(query, values).then(res => res.rows[0]);\n' + 178 | ' } catch (err) {\n' + 179 | ' throw new Error(err);\n' + 180 | ' }\n' + 181 | ' },' 182 | ); 183 | }; 184 | 185 | ResolverGenerator._updateMutation = function updateColumn(tableName, primaryKey, foreignKeys, columns) { 186 | let displaySet = ''; 187 | for (const key in this._values) displaySet += `${this._values[key]}=$${key}, `; 188 | return ( 189 | ` ${toCamelCase(`update_${singular(tableName)}`)}: (parent, args) => {\n` + 190 | ' try {\n' + 191 | ` const query = 'UPDATE ${tableName} SET ${displaySet.slice(0, displaySet.length - 2)} WHERE ${primaryKey} = $${Object.entries(this._values).length + 1} RETURNING *';\n` + 192 | ` const values = [${Object.values(this._values) 193 | .map((x) => `args.${x}`) 194 | .join(', ')}, args.${primaryKey}];\n` + 195 | ' return db.query(query, values).then((res) => res.rows[0]);\n' + 196 | ' } catch (err) {\n' + 197 | ' throw new Error(err);\n' + 198 | ' }\n' + 199 | ' },' 200 | ); 201 | }; 202 | 203 | ResolverGenerator._deleteMutations = function deleteColumn(tableName, primaryKey) { 204 | return ( 205 | ` ${toCamelCase(`delete_${singular(tableName)}`)}: (parent, args) => {\n` + 206 | ' try {\n' + 207 | ` const query = 'DELETE FROM ${tableName} WHERE ${primaryKey} = $1 RETURNING *';\n` + 208 | ` const values = [args.${primaryKey}];\n` + 209 | ' return db.query(query, values).then((res) => res.rows[0]);\n' + 210 | ' } catch (err) {\n' + 211 | ' throw new Error(err);\n' + 212 | ' }\n' + 213 | ' },' 214 | ); 215 | }; 216 | 217 | module.exports = ResolverGenerator; 218 | -------------------------------------------------------------------------------- /server/SDL-definedSchemas/generators/schemaGenerator.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-restricted-syntax */ 2 | /* eslint-disable guard-for-in */ 3 | const TypeGenerator = require('./typeGenerator'); 4 | const ResolverGenerator = require('./resolverGenerator'); 5 | 6 | const SchemaGenerator = {}; 7 | // assembles all programmatic schema and resolvers together in one string 8 | SchemaGenerator.assembleSchema = function assembleSchema(tables) { 9 | let queryType = ''; 10 | let mutationType = ''; 11 | let customTypes = ''; 12 | let queryResolvers = ''; 13 | let mutationResolvers = ''; 14 | let relationshipResolvers = ''; 15 | for (const tableName in tables) { 16 | const tableData = tables[tableName]; 17 | const { foreignKeys, columns } = tableData; 18 | queryType += TypeGenerator.queries(tableName, tableData); 19 | mutationType += TypeGenerator.mutations(tableName, tableData); 20 | customTypes += TypeGenerator.customTypes(tableName, tables); 21 | if (!foreignKeys || Object.keys(columns).length !== Object.keys(foreignKeys).length + 1) { 22 | queryResolvers += ResolverGenerator.queries(tableName, tableData); 23 | mutationResolvers += ResolverGenerator.mutations(tableName, tableData); 24 | relationshipResolvers += ResolverGenerator.getRelationships(tableName, tables); 25 | } 26 | } 27 | return ( 28 | `${'const typeDefs = `\n' + ' type Query {\n'}${queryType} }\n\n` + 29 | ` type Mutation {${mutationType} }\n\n` + 30 | `${customTypes}\`;\n\n` + 31 | 'const resolvers = {\n' + 32 | ' Query: {' + 33 | ` ${queryResolvers}\n` + 34 | ' },\n\n' + 35 | ` Mutation: {\n${mutationResolvers} },\n${relationshipResolvers}}\n\n` + 36 | 'const schema = makeExecutableSchema({\n' + 37 | ' typeDefs,\n' + 38 | ' resolvers,\n' + 39 | '});\n\n' + 40 | 'module.exports = schema;' 41 | ); 42 | }; 43 | 44 | module.exports = SchemaGenerator; 45 | -------------------------------------------------------------------------------- /server/SDL-definedSchemas/generators/typeGenerator.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-restricted-syntax */ 2 | /* eslint-disable guard-for-in */ 3 | const { singular } = require('pluralize'); 4 | const { toCamelCase, toPascalCase, typeSet, getPrimaryKeyType } = require('../helpers/helperFunctions'); 5 | 6 | const TypeGenerator = {}; 7 | 8 | TypeGenerator.queries = function queries(tableName, tableData) { 9 | const { primaryKey, foreignKeys, columns } = tableData; 10 | const nameSingular = singular(tableName); 11 | const primaryKeyType = getPrimaryKeyType(primaryKey, columns); 12 | if (!foreignKeys || Object.keys(columns).length !== Object.keys(foreignKeys).length + 1) { 13 | // Do not output pure join tables 14 | let byID = toCamelCase(nameSingular); 15 | if (nameSingular === tableName) byID += 'ByID'; 16 | return ( 17 | ` ${toCamelCase(tableName)}: [${toPascalCase(nameSingular)}!]!\n` + 18 | ` ${byID}(${primaryKey}: ${primaryKeyType}!): ${toPascalCase(nameSingular)}!\n` 19 | ); 20 | } 21 | return ''; 22 | }; 23 | 24 | TypeGenerator.mutations = function mutations(tableName, tableData) { 25 | const { primaryKey, foreignKeys, columns } = tableData; 26 | if (!foreignKeys || Object.keys(columns).length !== Object.keys(foreignKeys).length + 1) { 27 | // Do not output pure join tables 28 | return ( 29 | this._create(tableName, primaryKey, foreignKeys, columns) + 30 | this._update(tableName, primaryKey, foreignKeys, columns) + 31 | this._destroy(tableName, primaryKey) 32 | ); 33 | } 34 | return ''; 35 | }; 36 | 37 | TypeGenerator.customTypes = function customTypes(tableName, tables) { 38 | const { primaryKey, foreignKeys, columns } = tables[tableName]; 39 | const primaryKeyType = getPrimaryKeyType(primaryKey, columns); 40 | if (foreignKeys === null || Object.keys(columns).length !== Object.keys(foreignKeys).length + 1) { 41 | return `${` type ${toPascalCase(singular(tableName))} {\n` + ` ${primaryKey}: ${primaryKeyType}!`}${this._columns( 42 | primaryKey, 43 | foreignKeys, 44 | columns 45 | )}${this._getRelationships(tableName, tables)}\n }\n\n`; 46 | } 47 | return ''; 48 | }; 49 | 50 | TypeGenerator._columns = function columns(primaryKey, foreignKeys, columns) { 51 | let colStr = ''; 52 | for (const columnName in columns) { 53 | if (!(foreignKeys && foreignKeys[columnName]) && columnName !== primaryKey) { 54 | const { dataType, isNullable, columnDefault } = columns[columnName]; 55 | colStr += `\n ${columnName}: ${typeSet(dataType)}`; 56 | if (isNullable === 'NO' && columnDefault === null) colStr += '!'; 57 | } 58 | } 59 | return colStr; 60 | }; 61 | 62 | // Get table relationships 63 | TypeGenerator._getRelationships = function getRelationships(tableName, tables) { 64 | let relationships = ''; 65 | const alreadyAddedType = []; // cache to track which relation has been added or not 66 | for (const refTableName in tables[tableName].referencedBy) { 67 | // example1 (table name: film): refTableName: planets_in_films, vessels_in_films, people_in_films, species_in_films 68 | // example2 (when table name is : species): refTableName: people, species_in_films 69 | // example3 (when table name is : planets:): refTableName: planets_in_films, species, people 70 | const { referencedBy: foreignRefBy, foreignKeys: foreignFKeys, columns: foreignColumns } = tables[refTableName]; 71 | 72 | // One-to-one: when we can find tableName in foreignRefBy, that means this is a direct one to one relation 73 | if (foreignRefBy && foreignRefBy[tableName]) { 74 | if (!alreadyAddedType.includes(refTableName)) { 75 | // check if this refTableType has already been added by other tableName 76 | alreadyAddedType.push(refTableName); 77 | const refTableType = toPascalCase(singular(refTableName)); 78 | relationships += `\n ${toCamelCase(singular(reftableName))}: ${refTableType}`; 79 | } 80 | } 81 | 82 | // One-to-many: check if this is a join table, and if it's not, we can add relations) 83 | // example2: people table will meet this criteria 84 | // example3: species and people table will meet this criteria 85 | else if (Object.keys(foreignColumns).length !== Object.keys(foreignFKeys).length + 1) { 86 | if (!alreadyAddedType.includes(refTableName)) { 87 | // check if this refTableType has already been added by other tableName 88 | alreadyAddedType.push(refTableName); 89 | const refTableType = toPascalCase(singular(refTableName)); 90 | 91 | relationships += `\n ${toCamelCase(refTableName)}: [${refTableType}]`; 92 | // add 'people: [Person]' and to relation 93 | } 94 | } 95 | 96 | // Many-to-many relations (so now handling join tables!) 97 | for (const foreignFKey in foreignFKeys) { 98 | 99 | if (tableName !== foreignFKeys[foreignFKey].referenceTable) { 100 | // Do not include original table in output 101 | if (!alreadyAddedType.includes(refTableName)) { 102 | // check if this refTableType has already been added by other tableName 103 | alreadyAddedType.push(refTableName); 104 | const manyToManyTable = toCamelCase(foreignFKeys[foreignFKey].referenceTable); 105 | relationships += `\n ${manyToManyTable}: [${toPascalCase(singular(manyToManyTable))}]`; 106 | } 107 | } 108 | } 109 | } 110 | for (const FKTableName in tables[tableName].foreignKeys) { 111 | const object = tables[tableName].foreignKeys[FKTableName]; 112 | const refTableName = object.referenceTable; 113 | if (refTableName) { 114 | const refTableType = toPascalCase(singular(refTableName)); 115 | relationships += `\n ${toCamelCase(refTableName)}: [${refTableType}]`; 116 | } 117 | } 118 | 119 | return relationships; 120 | }; 121 | 122 | TypeGenerator._create = function create(tableName, primaryKey, foreignKeys, columns) { 123 | return `\n ${toCamelCase(`create_${singular(tableName)}`)}(\n${this._typeParams( 124 | primaryKey, 125 | foreignKeys, 126 | columns, 127 | false 128 | )}): ${toPascalCase(singular(tableName))}!\n`; 129 | }; 130 | 131 | TypeGenerator._update = function update(tableName, primaryKey, foreignKeys, columns) { 132 | return `\n ${toCamelCase(`update_${singular(tableName)}`)}(\n${this._typeParams( 133 | primaryKey, 134 | foreignKeys, 135 | columns, 136 | true 137 | )}): ${toPascalCase(singular(tableName))}!\n`; 138 | }; 139 | 140 | TypeGenerator._destroy = function destroy(tableName, primaryKey) { 141 | return `\n ${toCamelCase(`delete_${singular(tableName)}`)}(${primaryKey}: ID!): ${toPascalCase( 142 | singular(tableName) 143 | )}!\n`; 144 | }; 145 | 146 | TypeGenerator._typeParams = function addParams(primaryKey, foreignKeys, columns, needId) { 147 | let typeDef = ''; 148 | for (const columnName in columns) { 149 | const { dataType, isNullable } = columns[columnName]; 150 | if (!needId && columnName === primaryKey) { 151 | // handle mutation on creating 152 | continue; // we don't need Id during creating, so skip this loop when columnName === primaryKey 153 | } 154 | 155 | if (needId && columnName === primaryKey) { 156 | // handle mutation on updating (will need Id) 157 | typeDef += ` ${columnName}: ${typeSet(dataType)}!,\n`; // automatically add '!,\n' (not null) 158 | } else { 159 | typeDef += ` ${columnName}: ${typeSet(dataType)}`; 160 | if (isNullable !== 'YES') typeDef += '!'; 161 | typeDef += ',\n'; 162 | } 163 | // } 164 | } 165 | if (typeDef !== '') typeDef += ' '; 166 | return typeDef; 167 | }; 168 | 169 | module.exports = TypeGenerator; 170 | 171 | -------------------------------------------------------------------------------- /server/SDL-definedSchemas/helpers/helperFunctions.js: -------------------------------------------------------------------------------- 1 | function toPascalCase(str) { 2 | return capitalize(toCamelCase(str)); 3 | } 4 | 5 | function capitalize(str) { 6 | return `${str[0].toUpperCase()}${str.slice(1)}`; 7 | } 8 | 9 | function toCamelCase(str) { 10 | let varName = str.toLowerCase(); 11 | for (let i = 0, len = varName.length; i < len; i++) { 12 | const isSeparator = varName[i] === '-' || varName[i] === '_'; 13 | if (varName[i + 1] && isSeparator) { 14 | const char = i === 0 ? varName[i + 1] : varName[i + 1].toUpperCase(); 15 | varName = varName.slice(0, i) + char + varName.slice(i + 2); 16 | } else if (isSeparator) { 17 | varName = varName.slice(0, i); 18 | } 19 | } 20 | return varName; 21 | } 22 | 23 | function typeSet(str) { 24 | switch (str) { 25 | case 'character varying': 26 | return 'String'; 27 | break; 28 | case 'character': 29 | return 'String'; 30 | break; 31 | case 'integer': 32 | return 'Int'; 33 | break; 34 | case 'text': 35 | return 'String'; 36 | break; 37 | case 'date': 38 | return 'String'; 39 | break; 40 | case 'boolean': 41 | return 'Boolean'; 42 | break; 43 | default: 44 | return 'Int'; 45 | } 46 | } 47 | 48 | function getPrimaryKeyType(primaryKey, columns) { 49 | return typeSet(columns[primaryKey].dataType); 50 | } 51 | 52 | module.exports = { 53 | capitalize, 54 | toPascalCase, 55 | toCamelCase, 56 | typeSet, 57 | getPrimaryKeyType, 58 | }; 59 | -------------------------------------------------------------------------------- /server/dummy_server/connectToDB.js: -------------------------------------------------------------------------------- 1 | const {Pool} = require('pg'); 2 | const PG_URI = 'postgres://ordddiou:g5OjOyAIFxf-tsLk1uwu4ZOfbJfiCFbh@ruby.db.elephantsql.com:5432/ordddiou'; 3 | 4 | const pool = new Pool({ 5 | connectionString: PG_URI 6 | }) 7 | 8 | module.exports = { 9 | query: (text,params, callback) => { 10 | console.log('executed query:', text) 11 | return pool.query(text, params, callback) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /server/dummy_server/schema.js: -------------------------------------------------------------------------------- 1 | const { makeExecutableSchema } = require('graphql-tools'); 2 | const db = require('./connectToDB'); 3 | 4 | const typeDefs = ` 5 | type Query { 6 | people: [Person!]! 7 | person(_id: Int!): Person! 8 | films: [Film!]! 9 | film(_id: Int!): Film! 10 | planets: [Planet!]! 11 | planet(_id: Int!): Planet! 12 | species: [Species!]! 13 | speciesByID(_id: Int!): Species! 14 | vessels: [Vessel!]! 15 | vessel(_id: Int!): Vessel! 16 | starshipSpecs: [StarshipSpec!]! 17 | starshipSpec(_id: Int!): StarshipSpec! 18 | } 19 | 20 | type Mutation { 21 | createPerson( 22 | gender: String, 23 | species_id: Int, 24 | homeworld_id: Int, 25 | height: Int, 26 | mass: String, 27 | hair_color: String, 28 | skin_color: String, 29 | eye_color: String, 30 | name: String!, 31 | birth_year: String, 32 | ): Person! 33 | 34 | updatePerson( 35 | gender: String, 36 | species_id: Int, 37 | homeworld_id: Int, 38 | height: Int, 39 | _id: Int!, 40 | mass: String, 41 | hair_color: String, 42 | skin_color: String, 43 | eye_color: String, 44 | name: String!, 45 | birth_year: String, 46 | ): Person! 47 | 48 | deletePerson(_id: ID!): Person! 49 | 50 | createFilm( 51 | director: String!, 52 | opening_crawl: String!, 53 | episode_id: Int!, 54 | title: String!, 55 | release_date: String!, 56 | producer: String!, 57 | ): Film! 58 | 59 | updateFilm( 60 | director: String!, 61 | opening_crawl: String!, 62 | episode_id: Int!, 63 | _id: Int!, 64 | title: String!, 65 | release_date: String!, 66 | producer: String!, 67 | ): Film! 68 | 69 | deleteFilm(_id: ID!): Film! 70 | 71 | createPlanet( 72 | orbital_period: Int, 73 | climate: String, 74 | gravity: String, 75 | terrain: String, 76 | surface_water: String, 77 | population: Int, 78 | name: String, 79 | rotation_period: Int, 80 | diameter: Int, 81 | ): Planet! 82 | 83 | updatePlanet( 84 | orbital_period: Int, 85 | climate: String, 86 | gravity: String, 87 | terrain: String, 88 | surface_water: String, 89 | population: Int, 90 | _id: Int!, 91 | name: String, 92 | rotation_period: Int, 93 | diameter: Int, 94 | ): Planet! 95 | 96 | deletePlanet(_id: ID!): Planet! 97 | 98 | createSpecies( 99 | hair_colors: String, 100 | name: String!, 101 | classification: String, 102 | average_height: String, 103 | average_lifespan: String, 104 | skin_colors: String, 105 | eye_colors: String, 106 | language: String, 107 | homeworld_id: Int, 108 | ): Species! 109 | 110 | updateSpecies( 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 | _id: Int!, 121 | ): Species! 122 | 123 | deleteSpecies(_id: ID!): Species! 124 | 125 | createVessel( 126 | cost_in_credits: Int, 127 | length: String, 128 | vessel_type: String!, 129 | model: String, 130 | manufacturer: String, 131 | name: String!, 132 | vessel_class: String!, 133 | max_atmosphering_speed: String, 134 | crew: Int, 135 | passengers: Int, 136 | cargo_capacity: String, 137 | consumables: String, 138 | ): Vessel! 139 | 140 | updateVessel( 141 | cost_in_credits: Int, 142 | length: String, 143 | vessel_type: String!, 144 | model: String, 145 | manufacturer: String, 146 | name: String!, 147 | vessel_class: String!, 148 | max_atmosphering_speed: String, 149 | crew: Int, 150 | passengers: Int, 151 | cargo_capacity: String, 152 | consumables: String, 153 | _id: Int!, 154 | ): Vessel! 155 | 156 | deleteVessel(_id: ID!): Vessel! 157 | 158 | createStarshipSpec( 159 | vessel_id: Int!, 160 | MGLT: String, 161 | hyperdrive_rating: String, 162 | ): StarshipSpec! 163 | 164 | updateStarshipSpec( 165 | _id: Int!, 166 | vessel_id: Int!, 167 | MGLT: String, 168 | hyperdrive_rating: String, 169 | ): StarshipSpec! 170 | 171 | deleteStarshipSpec(_id: ID!): StarshipSpec! 172 | } 173 | 174 | type Person { 175 | _id: Int! 176 | gender: String 177 | height: Int 178 | mass: String 179 | hair_color: String 180 | skin_color: String 181 | eye_color: String 182 | name: String! 183 | birth_year: String 184 | films: [Film] 185 | vessels: [Vessel] 186 | species: [Species] 187 | planets: [Planet] 188 | } 189 | 190 | type Film { 191 | _id: Int! 192 | director: String! 193 | opening_crawl: String! 194 | episode_id: Int! 195 | title: String! 196 | release_date: String! 197 | producer: String! 198 | planets: [Planet] 199 | people: [Person] 200 | vessels: [Vessel] 201 | species: [Species] 202 | } 203 | 204 | type Planet { 205 | _id: Int! 206 | orbital_period: Int 207 | climate: String 208 | gravity: String 209 | terrain: String 210 | surface_water: String 211 | population: Int 212 | name: String 213 | rotation_period: Int 214 | diameter: Int 215 | films: [Film] 216 | species: [Species] 217 | people: [Person] 218 | } 219 | 220 | type Species { 221 | _id: Int! 222 | hair_colors: String 223 | name: String! 224 | classification: String 225 | average_height: String 226 | average_lifespan: String 227 | skin_colors: String 228 | eye_colors: String 229 | language: String 230 | people: [Person] 231 | films: [Film] 232 | planets: [Planet] 233 | } 234 | 235 | type Vessel { 236 | _id: Int! 237 | cost_in_credits: Int 238 | length: String 239 | vessel_type: String! 240 | model: String 241 | manufacturer: String 242 | name: String! 243 | vessel_class: String! 244 | max_atmosphering_speed: String 245 | crew: Int 246 | passengers: Int 247 | cargo_capacity: String 248 | consumables: String 249 | films: [Film] 250 | people: [Person] 251 | starshipSpecs: [StarshipSpec] 252 | } 253 | 254 | type StarshipSpec { 255 | _id: Int! 256 | MGLT: String 257 | hyperdrive_rating: String 258 | vessels: [Vessel] 259 | } 260 | 261 | `; 262 | 263 | const resolvers = { 264 | Query: { 265 | person: (parent, args) => { 266 | try{ 267 | const query = 'SELECT * FROM people WHERE _id = $1'; 268 | const values = [args._id]; 269 | return db.query(query, values).then((res) => res.rows[0]); 270 | } catch (err) { 271 | throw new Error(err); 272 | } 273 | }, 274 | people: () => { 275 | try { 276 | const query = 'SELECT * FROM people'; 277 | return db.query(query).then((res) => res.rows); 278 | } catch (err) { 279 | throw new Error(err); 280 | } 281 | }, 282 | film: (parent, args) => { 283 | try{ 284 | const query = 'SELECT * FROM films WHERE _id = $1'; 285 | const values = [args._id]; 286 | return db.query(query, values).then((res) => res.rows[0]); 287 | } catch (err) { 288 | throw new Error(err); 289 | } 290 | }, 291 | films: () => { 292 | try { 293 | const query = 'SELECT * FROM films'; 294 | return db.query(query).then((res) => res.rows); 295 | } catch (err) { 296 | throw new Error(err); 297 | } 298 | }, 299 | planet: (parent, args) => { 300 | try{ 301 | const query = 'SELECT * FROM planets WHERE _id = $1'; 302 | const values = [args._id]; 303 | return db.query(query, values).then((res) => res.rows[0]); 304 | } catch (err) { 305 | throw new Error(err); 306 | } 307 | }, 308 | planets: () => { 309 | try { 310 | const query = 'SELECT * FROM planets'; 311 | return db.query(query).then((res) => res.rows); 312 | } catch (err) { 313 | throw new Error(err); 314 | } 315 | }, 316 | speciesByID: (parent, args) => { 317 | try{ 318 | const query = 'SELECT * FROM species WHERE _id = $1'; 319 | const values = [args._id]; 320 | return db.query(query, values).then((res) => res.rows[0]); 321 | } catch (err) { 322 | throw new Error(err); 323 | } 324 | }, 325 | species: () => { 326 | try { 327 | const query = 'SELECT * FROM species'; 328 | return db.query(query).then((res) => res.rows); 329 | } catch (err) { 330 | throw new Error(err); 331 | } 332 | }, 333 | vessel: (parent, args) => { 334 | try{ 335 | const query = 'SELECT * FROM vessels WHERE _id = $1'; 336 | const values = [args._id]; 337 | return db.query(query, values).then((res) => res.rows[0]); 338 | } catch (err) { 339 | throw new Error(err); 340 | } 341 | }, 342 | vessels: () => { 343 | try { 344 | const query = 'SELECT * FROM vessels'; 345 | return db.query(query).then((res) => res.rows); 346 | } catch (err) { 347 | throw new Error(err); 348 | } 349 | }, 350 | starshipSpec: (parent, args) => { 351 | try{ 352 | const query = 'SELECT * FROM starship_specs WHERE _id = $1'; 353 | const values = [args._id]; 354 | return db.query(query, values).then((res) => res.rows[0]); 355 | } catch (err) { 356 | throw new Error(err); 357 | } 358 | }, 359 | starshipSpecs: () => { 360 | try { 361 | const query = 'SELECT * FROM starship_specs'; 362 | return db.query(query).then((res) => res.rows); 363 | } catch (err) { 364 | throw new Error(err); 365 | } 366 | }, 367 | }, 368 | 369 | Mutation: { 370 | createPerson: (parent, args) => { 371 | 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 *'; 372 | 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]; 373 | try { 374 | return db.query(query, values).then(res => res.rows[0]); 375 | } catch (err) { 376 | throw new Error(err); 377 | } 378 | }, 379 | updatePerson: (parent, args) => { 380 | try { 381 | 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 *'; 382 | 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]; 383 | return db.query(query, values).then((res) => res.rows[0]); 384 | } catch (err) { 385 | throw new Error(err); 386 | } 387 | }, 388 | deletePerson: (parent, args) => { 389 | try { 390 | const query = 'DELETE FROM people WHERE _id = $1 RETURNING *'; 391 | const values = [args._id]; 392 | return db.query(query, values).then((res) => res.rows[0]); 393 | } catch (err) { 394 | throw new Error(err); 395 | } 396 | }, 397 | 398 | createFilm: (parent, args) => { 399 | const query = 'INSERT INTO films(director, opening_crawl, episode_id, title, release_date, producer) VALUES($1, $2, $3, $4, $5, $6) RETURNING *'; 400 | const values = [args.director, args.opening_crawl, args.episode_id, args.title, args.release_date, args.producer]; 401 | try { 402 | return db.query(query, values).then(res => res.rows[0]); 403 | } catch (err) { 404 | throw new Error(err); 405 | } 406 | }, 407 | updateFilm: (parent, args) => { 408 | try { 409 | const query = 'UPDATE films SET director=$1, opening_crawl=$2, episode_id=$3, title=$4, release_date=$5, producer=$6 WHERE _id = $7 RETURNING *'; 410 | const values = [args.director, args.opening_crawl, args.episode_id, args.title, args.release_date, args.producer, args._id]; 411 | return db.query(query, values).then((res) => res.rows[0]); 412 | } catch (err) { 413 | throw new Error(err); 414 | } 415 | }, 416 | deleteFilm: (parent, args) => { 417 | try { 418 | const query = 'DELETE FROM films WHERE _id = $1 RETURNING *'; 419 | const values = [args._id]; 420 | return db.query(query, values).then((res) => res.rows[0]); 421 | } catch (err) { 422 | throw new Error(err); 423 | } 424 | }, 425 | 426 | createPlanet: (parent, args) => { 427 | 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 *'; 428 | const values = [args.orbital_period, args.climate, args.gravity, args.terrain, args.surface_water, args.population, args.name, args.rotation_period, args.diameter]; 429 | try { 430 | return db.query(query, values).then(res => res.rows[0]); 431 | } catch (err) { 432 | throw new Error(err); 433 | } 434 | }, 435 | updatePlanet: (parent, args) => { 436 | try { 437 | 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 *'; 438 | 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]; 439 | return db.query(query, values).then((res) => res.rows[0]); 440 | } catch (err) { 441 | throw new Error(err); 442 | } 443 | }, 444 | deletePlanet: (parent, args) => { 445 | try { 446 | const query = 'DELETE FROM planets WHERE _id = $1 RETURNING *'; 447 | const values = [args._id]; 448 | return db.query(query, values).then((res) => res.rows[0]); 449 | } catch (err) { 450 | throw new Error(err); 451 | } 452 | }, 453 | 454 | createSpecies: (parent, args) => { 455 | 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 *'; 456 | 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]; 457 | try { 458 | return db.query(query, values).then(res => res.rows[0]); 459 | } catch (err) { 460 | throw new Error(err); 461 | } 462 | }, 463 | updateSpecies: (parent, args) => { 464 | try { 465 | 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 *'; 466 | 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]; 467 | return db.query(query, values).then((res) => res.rows[0]); 468 | } catch (err) { 469 | throw new Error(err); 470 | } 471 | }, 472 | deleteSpecies: (parent, args) => { 473 | try { 474 | const query = 'DELETE FROM species WHERE _id = $1 RETURNING *'; 475 | const values = [args._id]; 476 | return db.query(query, values).then((res) => res.rows[0]); 477 | } catch (err) { 478 | throw new Error(err); 479 | } 480 | }, 481 | 482 | createVessel: (parent, args) => { 483 | 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 *'; 484 | 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]; 485 | try { 486 | return db.query(query, values).then(res => res.rows[0]); 487 | } catch (err) { 488 | throw new Error(err); 489 | } 490 | }, 491 | updateVessel: (parent, args) => { 492 | try { 493 | 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 *'; 494 | 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]; 495 | return db.query(query, values).then((res) => res.rows[0]); 496 | } catch (err) { 497 | throw new Error(err); 498 | } 499 | }, 500 | deleteVessel: (parent, args) => { 501 | try { 502 | const query = 'DELETE FROM vessels WHERE _id = $1 RETURNING *'; 503 | const values = [args._id]; 504 | return db.query(query, values).then((res) => res.rows[0]); 505 | } catch (err) { 506 | throw new Error(err); 507 | } 508 | }, 509 | 510 | createStarshipSpec: (parent, args) => { 511 | const query = 'INSERT INTO starship_specs(vessel_id, MGLT, hyperdrive_rating) VALUES($1, $2, $3) RETURNING *'; 512 | const values = [args.vessel_id, args.MGLT, args.hyperdrive_rating]; 513 | try { 514 | return db.query(query, values).then(res => res.rows[0]); 515 | } catch (err) { 516 | throw new Error(err); 517 | } 518 | }, 519 | updateStarshipSpec: (parent, args) => { 520 | try { 521 | const query = 'UPDATE starship_specs SET vessel_id=$1, MGLT=$2, hyperdrive_rating=$3 WHERE _id = $4 RETURNING *'; 522 | const values = [args.vessel_id, args.MGLT, args.hyperdrive_rating, args._id]; 523 | return db.query(query, values).then((res) => res.rows[0]); 524 | } catch (err) { 525 | throw new Error(err); 526 | } 527 | }, 528 | deleteStarshipSpec: (parent, args) => { 529 | try { 530 | const query = 'DELETE FROM starship_specs WHERE _id = $1 RETURNING *'; 531 | const values = [args._id]; 532 | return db.query(query, values).then((res) => res.rows[0]); 533 | } catch (err) { 534 | throw new Error(err); 535 | } 536 | }, 537 | 538 | }, 539 | 540 | Person: { 541 | films: async (people) => { 542 | try { 543 | 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'; 544 | const values = [people._id] 545 | return await db.query(query, values).then((res) => res.rows); 546 | } catch (err) { 547 | //throw new Error(err) 548 | } 549 | }, 550 | species: async (people) => { 551 | try { 552 | const query = 'SELECT species.* FROM species LEFT OUTER JOIN people ON species._id = people.species_id WHERE people._id = $1'; 553 | const values = [people._id] 554 | return await db.query(query, values).then((res) => res.rows); 555 | } catch (err) { 556 | //throw new Error(err) 557 | } 558 | }, 559 | planets: async (people) => { 560 | try { 561 | const query = 'SELECT planets.* FROM planets LEFT OUTER JOIN people ON planets._id = people.homeworld_id WHERE people._id = $1'; 562 | const values = [people._id] 563 | return await db.query(query, values).then((res) => res.rows); 564 | } catch (err) { 565 | //throw new Error(err) 566 | } 567 | }, 568 | vessels: async (people) => { 569 | try { 570 | const query = 'SELECT * FROM vessels LEFT OUTER JOIN pilots ON vessels._id = pilots.vessel_id WHERE pilots.person_id = $1'; 571 | const values = [people._id] 572 | return await db.query(query, values).then((res) => res.rows); 573 | } catch (err) { 574 | //throw new Error(err) 575 | } 576 | }, 577 | }, 578 | 579 | Film: { 580 | planets: async (films) => { 581 | try { 582 | 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'; 583 | const values = [films._id] 584 | return await db.query(query, values).then((res) => res.rows); 585 | } catch (err) { 586 | //throw new Error(err) 587 | } 588 | }, 589 | people: async (films) => { 590 | try { 591 | 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'; 592 | const values = [films._id] 593 | return await db.query(query, values).then((res) => res.rows); 594 | } catch (err) { 595 | //throw new Error(err) 596 | } 597 | }, 598 | vessels: async (films) => { 599 | try { 600 | 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'; 601 | const values = [films._id] 602 | return await db.query(query, values).then((res) => res.rows); 603 | } catch (err) { 604 | //throw new Error(err) 605 | } 606 | }, 607 | species: async (films) => { 608 | try { 609 | 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'; 610 | const values = [films._id] 611 | return await db.query(query, values).then((res) => res.rows); 612 | } catch (err) { 613 | //throw new Error(err) 614 | } 615 | }, 616 | }, 617 | 618 | Planet: { 619 | films: async (planets) => { 620 | try { 621 | 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'; 622 | const values = [planets._id] 623 | return await db.query(query, values).then((res) => res.rows); 624 | } catch (err) { 625 | //throw new Error(err) 626 | } 627 | }, 628 | species: async (planets) => { 629 | try { 630 | const query = 'SELECT * FROM species WHERE homeworld_id = $1'; 631 | const values = [planets._id] 632 | return await db.query(query, values).then((res) => res.rows); 633 | } catch (err) { 634 | //throw new Error(err) 635 | } 636 | }, 637 | people: async (planets) => { 638 | try { 639 | const query = 'SELECT * FROM people WHERE homeworld_id = $1'; 640 | const values = [planets._id] 641 | return await db.query(query, values).then((res) => res.rows); 642 | } catch (err) { 643 | //throw new Error(err) 644 | } 645 | }, 646 | species: async (planets) => { 647 | try { 648 | const query = 'SELECT * FROM species LEFT OUTER JOIN people ON species._id = people.species_id WHERE people.homeworld_id = $1'; 649 | const values = [planets._id] 650 | return await db.query(query, values).then((res) => res.rows); 651 | } catch (err) { 652 | //throw new Error(err) 653 | } 654 | }, 655 | }, 656 | 657 | Species: { 658 | people: async (species) => { 659 | try { 660 | const query = 'SELECT * FROM people WHERE species_id = $1'; 661 | const values = [species._id] 662 | return await db.query(query, values).then((res) => res.rows); 663 | } catch (err) { 664 | //throw new Error(err) 665 | } 666 | }, 667 | planets: async (species) => { 668 | try { 669 | const query = 'SELECT * FROM planets LEFT OUTER JOIN people ON planets._id = people.homeworld_id WHERE people.species_id = $1'; 670 | const values = [species._id] 671 | return await db.query(query, values).then((res) => res.rows); 672 | } catch (err) { 673 | //throw new Error(err) 674 | } 675 | }, 676 | planets: async (species) => { 677 | try { 678 | const query = 'SELECT planets.* FROM planets LEFT OUTER JOIN species ON planets._id = species.homeworld_id WHERE species._id = $1'; 679 | const values = [species._id] 680 | return await db.query(query, values).then((res) => res.rows); 681 | } catch (err) { 682 | //throw new Error(err) 683 | } 684 | }, 685 | films: async (species) => { 686 | try { 687 | 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'; 688 | const values = [species._id] 689 | return await db.query(query, values).then((res) => res.rows); 690 | } catch (err) { 691 | //throw new Error(err) 692 | } 693 | }, 694 | }, 695 | 696 | Vessel: { 697 | films: async (vessels) => { 698 | try { 699 | 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'; 700 | const values = [vessels._id] 701 | return await db.query(query, values).then((res) => res.rows); 702 | } catch (err) { 703 | //throw new Error(err) 704 | } 705 | }, 706 | people: async (vessels) => { 707 | try { 708 | const query = 'SELECT * FROM people LEFT OUTER JOIN pilots ON people._id = pilots.person_id WHERE pilots.vessel_id = $1'; 709 | const values = [vessels._id] 710 | return await db.query(query, values).then((res) => res.rows); 711 | } catch (err) { 712 | //throw new Error(err) 713 | } 714 | }, 715 | starshipSpecs: async (vessels) => { 716 | try { 717 | const query = 'SELECT * FROM starship_specs WHERE vessel_id = $1'; 718 | const values = [vessels._id] 719 | return await db.query(query, values).then((res) => res.rows); 720 | } catch (err) { 721 | //throw new Error(err) 722 | } 723 | }, 724 | }, 725 | } 726 | 727 | const schema = makeExecutableSchema({ 728 | typeDefs, 729 | resolvers, 730 | }); 731 | 732 | module.exports = schema; -------------------------------------------------------------------------------- /server/queries/tableData.sql: -------------------------------------------------------------------------------- 1 | SELECT json_object_agg( 2 | pk.table_name, json_build_object( 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 | SELECT conrelid::regclass AS table_name, -- regclass will turn conrelid to actual tale name 13 | substring(pg_get_constraintdef(oid), '\((.*?)\)') AS primary_key --(.*?) matches any character (except for line terminators)) 14 | FROM pg_constraint 15 | WHERE contype = 'p' AND connamespace = 'public'::regnamespace -- regnamespace will turn connamespace(number) to actual name space 16 | --------------------------------------------------------------------------- 17 | ) AS pk 18 | 19 | LEFT OUTER JOIN ( -- Foreign key data (fk) 20 | --------------------------------------------------------------------------------------- 21 | SELECT conrelid::regclass AS table_name, 22 | json_object_agg( 23 | substring(pg_get_constraintdef(oid), '\((.*?)\)'), json_build_object( 24 | 'referenceTable', substring(pg_get_constraintdef(oid), 'REFERENCES (.*?)\('), 25 | 'referenceKey', substring(pg_get_constraintdef(oid), 'REFERENCES.*?\((.*?)\)') 26 | ) 27 | ) AS foreign_keys 28 | FROM pg_constraint 29 | WHERE contype = 'f' AND connamespace = 'public'::regnamespace 30 | GROUP BY table_name 31 | --------------------------------------------------------------------------------------- 32 | ) AS fk 33 | ON pk.table_name = fk.table_name 34 | 35 | LEFT OUTER JOIN ( -- Reference data (rd) 36 | --------------------------------------------------------------------------------------------------- 37 | SELECT substring(pg_get_constraintdef(oid), 'REFERENCES (.*?)\(') AS table_name, json_object_agg( 38 | conrelid::regclass, substring(pg_get_constraintdef(oid), '\((.*?)\)') 39 | ) AS referenced_by 40 | FROM pg_constraint 41 | WHERE contype = 'f' AND connamespace = 'public'::regnamespace 42 | GROUP BY table_name 43 | --------------------------------------------------------------------------------------------------- 44 | ) AS rd 45 | ON pk.table_name::regclass = rd.table_name::regclass 46 | 47 | LEFT OUTER JOIN ( -- Table data (td) 48 | ----------------------------------------------------------------- 49 | SELECT tab.table_name, json_object_agg( 50 | col.column_name, json_build_object( 51 | 'dataType', col.data_type, 52 | 'columnDefault', col.column_default, 53 | 'charMaxLength', col.character_maximum_length, 54 | 'isNullable', col.is_nullable 55 | ) 56 | ) AS columns 57 | 58 | -- Table names 59 | FROM ( 60 | SELECT table_name FROM information_schema.tables 61 | WHERE table_type='BASE TABLE' AND table_schema='public' 62 | ) AS tab 63 | 64 | -- Table columns 65 | INNER JOIN information_schema.columns AS col 66 | ON tab.table_name = col.table_name 67 | GROUP BY tab.table_name 68 | ----------------------------------------------------------------- 69 | ) AS td 70 | ON td.table_name::regclass = pk.table_name 71 | 72 | -- postgres://ordddiou:g5OjOyAIFxf-tsLk1uwu4ZOfbJfiCFbh@ruby.db.elephantsql.com:5432/ordddiou 73 | -------------------------------------------------------------------------------- /server/routes/mySQLRoute.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | const mySQLController = require('../SDL-definedSchemas/controllers/mySQLController'); 3 | const pgController = require('../SDL-definedSchemas/controllers/pgController'); 4 | 5 | router.post('/sdl', mySQLController.getTables, pgController.assembleSDLSchema, pgController.compileData, (req, res) => { 6 | res.status(200).json({ schema: res.locals.SDLSchema, d3Data: res.locals.d3Data }); 7 | }); 8 | 9 | module.exports = router; 10 | -------------------------------------------------------------------------------- /server/routes/pgRoute.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | const pgController = require('../SDL-definedSchemas/controllers/pgController'); 5 | 6 | router.post('/sdl', pgController.getPGTables, pgController.assembleSDLSchema, pgController.compileData, (req, res) => { 7 | res.status(200).json({ schema: res.locals.SDLSchema, tables: res.locals.tables }); 8 | }); 9 | 10 | const dummyServerController = {}; 11 | 12 | dummyServerController.writeFiles = (req, res, next) => { 13 | const { db, schema } = req.body; 14 | fs.writeFileSync(path.resolve(__dirname, '../dummy_server/schema.js'), schema); 15 | fs.writeFileSync(path.resolve(__dirname, '../dummy_server/connectToDB.js'), db); 16 | next(); 17 | }; 18 | 19 | router.post('/writefile', dummyServerController.writeFiles, (req, res) => { 20 | res.json('got it!'); 21 | }); 22 | 23 | 24 | module.exports = router; 25 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const path = require('path'); 3 | const https = require('https'); 4 | const fs = require('fs'); 5 | const expressGraphQL = require('express-graphql'); 6 | const expressPlayground = require('graphql-playground-middleware-express').default; 7 | const pgRouter = require('./routes/pgRoute'); 8 | const mySQLRouter = require('./routes/mySQLRoute'); 9 | const schema = require('./dummy_server/schema'); 10 | 11 | const app = express(); 12 | const PORT = process.env.PORT || 8081; 13 | 14 | /* Express logic/handler */ 15 | app.use(express.json()); 16 | app.use(express.urlencoded({ extended: true })); 17 | 18 | app.use(express.static(path.join(__dirname, '../build'))); 19 | 20 | app.use('/db/pg', pgRouter); 21 | app.use('/db/mySQL', mySQLRouter); 22 | 23 | app.get('/', (req, res) => { 24 | res.sendFile(path.join(__dirname, '../client/index.html')); 25 | }); 26 | 27 | app.use( 28 | '/graphql', 29 | expressGraphQL({ 30 | schema, 31 | }) 32 | ); 33 | 34 | app.get('/playground', expressPlayground({ endpoint: '/graphql' })); 35 | 36 | app.use((err, req, res, next) => { 37 | const defaultErr = { 38 | log: 'An error occured in unknown middleware', 39 | status: 500, 40 | message: { err: 'An error occurred' }, 41 | }; 42 | const errObj = { ...defaultErr, ...err }; 43 | console.log(errObj.log); 44 | res.status(errObj.status).json(errObj.message); 45 | }); 46 | 47 | app.listen(PORT, () => console.log(`lucidQL is ready at: http://localhost:${PORT}`)); 48 | 49 | module.exports = app; 50 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // Target latest version of ECMAScript. 4 | "target": "esnext", 5 | // path to output directory 6 | "outDir": "./build/", 7 | // enable strict null checks as a best practice 8 | "strictNullChecks": true, 9 | // Search under node_modules for non-relative imports. 10 | "moduleResolution": "node", 11 | // Process & infer types from .js files. 12 | "allowJs": true, 13 | // Don't emit; allow Babel to transform files. 14 | "noEmit": true, 15 | // Enable strictest settings like strictNullChecks & noImplicitAny. 16 | "strict": true, 17 | // Import non-ES modules as default imports. 18 | "esModuleInterop": true, 19 | // use typescript to transpile jsx to js 20 | "jsx": "react", 21 | "baseUrl": "./client", 22 | "lib": ["es2015", "dom.iterable", "es2016.array.include", "es2017.object", "dom"], 23 | "module": "es6", 24 | "removeComments": true, 25 | "alwaysStrict": true, 26 | "allowUnreachableCode": false, 27 | "noImplicitAny": true, 28 | "noImplicitThis": true, 29 | "noUnusedLocals": true, 30 | "noUnusedParameters": true, 31 | "noImplicitReturns": true, 32 | "noFallthroughCasesInSwitch": true, 33 | "forceConsistentCasingInFileNames": true, 34 | "importHelpers": true 35 | } 36 | } 37 | // { 38 | // "compilerOptions": { 39 | // "target": "es5", 40 | // "jsx": "react", 41 | // "module": "es2015" 42 | // }, 43 | // "exclude": [ 44 | // "build" 45 | // ] 46 | // } 47 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint-immutable" 4 | ], 5 | "rules": { 6 | "no-var-keyword": true, 7 | "no-parameter-reassignment": true, 8 | "typedef": false, 9 | "readonly-keyword": false, 10 | "readonly-array": false, 11 | "no-let": false, 12 | "no-array-mutation": true, 13 | "no-object-mutation": false, 14 | "no-this": false, 15 | "no-class": false, 16 | "no-mixed-interface": false, 17 | "no-expression-statement": false, 18 | "member-ordering": [ 19 | true, 20 | "variables-before-functions" 21 | ], 22 | "no-any": false, 23 | "no-inferrable-types": [ 24 | false 25 | ], 26 | "no-internal-module": true, 27 | "no-var-requires": false, 28 | "typedef-whitespace": [ 29 | true, 30 | { 31 | "call-signature": "nospace", 32 | "index-signature": "nospace", 33 | "parameter": "nospace", 34 | "property-declaration": "nospace", 35 | "variable-declaration": "nospace" 36 | }, 37 | { 38 | "call-signature": "space", 39 | "index-signature": "space", 40 | "parameter": "space", 41 | "property-declaration": "space", 42 | "variable-declaration": "space" 43 | } 44 | ], 45 | "ban": false, 46 | "curly": false, 47 | "forin": true, 48 | "label-position": true, 49 | "no-arg": true, 50 | "no-bitwise": false, 51 | "no-conditional-assignment": true, 52 | "no-console": [ 53 | true, 54 | "info", 55 | "time", 56 | "timeEnd", 57 | "trace" 58 | ], 59 | "no-construct": true, 60 | "no-debugger": true, 61 | "no-duplicate-variable": true, 62 | "no-empty": false, 63 | "no-eval": true, 64 | "strictPropertyInitialization": false, 65 | "no-null-keyword": false, 66 | "no-shadowed-variable": false, 67 | "no-string-literal": false, 68 | "no-switch-case-fall-through": true, 69 | "no-unused-expression": [ 70 | true, 71 | "allow-fast-null-checks" 72 | ], 73 | "no-use-before-declare": true, 74 | "radix": true, 75 | "switch-default": true, 76 | "triple-equals": [ 77 | true, 78 | "allow-undefined-check", 79 | "allow-null-check" 80 | ], 81 | "eofline": false, 82 | "indent": [ 83 | true, 84 | "tabs" 85 | ], 86 | "max-line-length": [ 87 | true, 88 | 250 89 | ], 90 | "no-require-imports": false, 91 | "no-trailing-whitespace": true, 92 | "object-literal-sort-keys": false, 93 | "trailing-comma": [ 94 | true, 95 | { 96 | "multiline": "never", 97 | "singleline": "never" 98 | } 99 | ], 100 | "align": [ 101 | true 102 | ], 103 | "class-name": true, 104 | "comment-format": [ 105 | true, 106 | "check-space" 107 | ], 108 | "interface-name": [ 109 | false 110 | ], 111 | "jsdoc-format": true, 112 | "no-consecutive-blank-lines": [ 113 | true 114 | ], 115 | "no-parameter-properties": false, 116 | "one-line": [ 117 | true, 118 | "check-open-brace", 119 | "check-catch", 120 | "check-else", 121 | "check-finally", 122 | "check-whitespace" 123 | ], 124 | "quotemark": [ 125 | true, 126 | "single", 127 | "avoid-escape" 128 | ], 129 | "semicolon": [ 130 | true, 131 | "always" 132 | ], 133 | "variable-name": [ 134 | false, 135 | "check-format", 136 | "allow-leading-underscore", 137 | "ban-keywords" 138 | ], 139 | "whitespace": [ 140 | true, 141 | "check-branch", 142 | "check-decl", 143 | "check-operator", 144 | "check-separator", 145 | "check-type" 146 | ] 147 | } 148 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | 4 | module.exports = { 5 | // webpack will take the files from ./client/index 6 | entry: './client/index.tsx', 7 | // and output it into /dist as bundle.js 8 | output: { 9 | path: path.join(__dirname, '/build'), 10 | filename: 'bundle.js', 11 | }, 12 | // adding .ts and .tsx to resolve.extensions will help babel look for .ts and .tsx files to transpile 13 | resolve: { 14 | extensions: ['.ts', '.tsx', '.js', '.jsx'], 15 | }, 16 | module: { 17 | rules: [ 18 | // we use babel-loader to load our jsx and tsx files 19 | { 20 | test: /\.(ts|js)x?$/, 21 | exclude: /node_modules/, 22 | use: { 23 | loader: 'babel-loader', 24 | }, 25 | }, 26 | // css-loader to bundle all the css files into one file and style-loader to add all the styles inside the style tag of the document 27 | { 28 | test: /\.css$/, 29 | use: ['style-loader', 'css-loader'], 30 | }, 31 | { 32 | test: /.(png|jpg|woff|woff2|eot|ttf|svg|gif)$/, 33 | loader: 'url-loader?limit=1024000', 34 | }, 35 | ], 36 | }, 37 | devServer: { 38 | publicPath: '/build', 39 | proxy: { 40 | '/': 'http://localhost:8081', 41 | }, 42 | hot: true, 43 | }, 44 | plugins: [ 45 | new HtmlWebpackPlugin({ 46 | template: './client/index.html', 47 | filename: 'index.html', 48 | inject: 'body', 49 | }), 50 | ], 51 | }; 52 | --------------------------------------------------------------------------------