├── .babelrc ├── .gitignore ├── LICENSE ├── README.md ├── __tests__ ├── login.test.tsx └── server │ └── routes.ts ├── assets ├── favicon.ico ├── login-uri-sequence.gif ├── login.gif ├── logo-bulb.png ├── logo-bulb.svg ├── logo-extra-padded.png ├── logo-extra-padded.svg ├── logo-main.png ├── logo-main.svg ├── logo-padded.png ├── logo-padded.svg ├── query-diagram-render.gif ├── results-scroll.gif ├── save-history.gif ├── settings-tab.gif └── smarter-db.png ├── client ├── App.tsx ├── Context.tsx ├── components │ ├── HomeComponents │ │ ├── Diagram.tsx │ │ ├── DiagramLogic │ │ │ ├── ConditionalSchemaParser.tsx │ │ │ ├── CustomColumnNode.tsx │ │ │ ├── CustomTitleNode.tsx │ │ │ ├── LayoutCalc.tsx │ │ │ └── ParseNodes.tsx │ │ ├── Header.tsx │ │ ├── InputContainer.tsx │ │ ├── InputContainerComponents │ │ │ ├── History.tsx │ │ │ ├── QueryInput.tsx │ │ │ └── Settings.tsx │ │ └── QueryResults.tsx │ ├── Homepage.tsx │ ├── Login.tsx │ └── Signup.tsx ├── index.tsx └── styles │ ├── InputContainer.scss │ ├── diagram.scss │ ├── header.scss │ ├── login.scss │ ├── resultbar.scss │ └── split.scss ├── index.html ├── jest.config.js ├── package.json ├── server ├── app.ts ├── controllers │ ├── cookieController.ts │ ├── dbController.ts │ ├── schemaController.ts │ └── userController.ts ├── models │ └── userModel.ts ├── routes │ └── router.ts └── server.ts ├── src └── static │ ├── diagram-rearranged.png │ ├── express.png │ ├── history-2.png │ ├── history-3.png │ ├── history.png │ ├── icons8-react-native-48.png │ ├── login.png │ ├── postgresql.png │ ├── query-people-species.png │ ├── react-flow.svg │ ├── redis.png │ ├── results.png │ ├── sass.png │ ├── settings-credentials.png │ ├── settings-uri.png │ ├── settings.png │ ├── smarter-logo-padded.png │ └── typescript.png ├── tsconfig.json ├── types ├── custom.ts └── express │ └── index.d.ts └── webpack.config.ts /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-typescript", 5 | "@babel/preset-react" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /dist 3 | package-lock.json 4 | .DS_Store 5 | .env 6 | dump.rdb 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 OSLabs Beta 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 |
5 |
6 |    7 |    8 |    9 |    10 |    11 |    12 | 13 |
14 |
15 |
16 | ⚡ Getting Started   17 | 📝 User guide   18 | 📬 Features   19 | 🧠 How to contribute   20 | 👥 Contributors   21 | ☕️ Supporters   22 |
23 | 24 |
25 | 26 |

27 | SQL databases often contain a complex network of interconnected tables. This can pose a challenge for developers when attempting to build or optimize queries that involve multiple tables. 28 | 29 | smartER is a query visualizing tool that works with your postgreSQL database to provide dynamically rendered ER diagrams. It is a web application written in TypeScript that reads a user's database schema and renders ER diagrams based on a given query string. To parse through SQL schemas, it uses the pgsql-ast-parser to produce a typed Abstract Syntax Tree, which is further parsed to build custom nodes with React Flow and finally position them using elkjs. 30 | 31 |

32 | 33 |

🛠 Built With

34 | 35 | - React 36 | - TypeScript 37 | - PostgreSQL 38 | - Express 39 | - Sass 40 | - Redis 41 | - React Flow 42 | 43 |

⚡ Getting started

44 | Our application is pretty simple to get up and running! 45 | 46 | Install redis: 47 | 48 | ```js 49 | brew install redis 50 | ``` 51 | 52 | Install other dependencies: 53 | 54 | ```js 55 | npm install 56 | ``` 57 | 58 | Set up your database. Ours looks like this:
59 | 60 | 61 | Set your .env variables: 62 | 63 | 69 |
70 | Start the application: 71 | 72 | ```js 73 | npm start 74 | ``` 75 | 76 |

📝 User guide

77 | 78 | On application load, the user will be prompted to log in before being directed to the homepage. First time users should first create an account 79 | 80 | 81 | 82 | Navigate to the settings tab and input either the URI or credentials for your database 83 | 84 | 85 | Once the database is connected, navigate to the query tab and begin typing your query - notice your ER diagram renders and updates as you type! 86 | 87 | 88 | Scroll through your query results at any time, they are rendered as you type as well 89 | 90 | 91 | Save your query and re-render it later by clicking on it in the History tab 92 | 93 | 94 |

📬 Features:

95 | 96 | smartER aims to provide a seamless user experience, offering: 97 | - Automatic rendering based on a valid query string, with helpful error messages for invalid query strings 98 | - Linking of relationships in the ER diagram via a dotted line 99 | - Linking of JOIN columns from your query with a bolded line 100 | - Highlighting of all columns in your SELECT statement for visual clarity and accessibility 101 | - Support for all postgreSQL SELECT queries, including unions, subqueries, and aggregations 102 | - An interactive and easily rearranged ER diagram for optimal clarity on your database relationships 103 | - A responsive UI that allows the user to decide which features get the most real estate 104 | 105 |

🧠 How to contribute

106 | 107 |

smartER is currently in alpha and we would love to hear your feedback, encouragement, advice, suggestions, or problems. If you would like to contribute, please fork, clone, and make pull requests. If you would like to report an issue or submit a feature request, please do so. We would love to hear how we can make smartER more useful for you! If you would like to reach the smartER team directly for any other reason, please email us

108 | 109 |

👥 Contributors

110 | 111 |

Joyce Kwak @github @linkedin

112 | 113 |

114 | Melissa McLaughlin @github @linkedin

115 | 116 |

Nathan Ngo @github @linkedin

117 | 118 |

Brian Vu @github @linkedin

119 | 120 |

☕️ Supporters

121 | -------------------------------------------------------------------------------- /__tests__/login.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen, fireEvent } from '@testing-library/react'; 3 | import { MemoryRouter, Route, Routes } from 'react-router-dom'; 4 | import Login from '../client/components/Login'; 5 | import userEvent from '@testing-library/user-event'; 6 | import '@testing-library/jest-dom/extend-expect'; 7 | 8 | describe('Unit testing for Login', () => { 9 | beforeEach(() => { 10 | render( 11 | 12 | 13 | } /> 14 | 15 | 16 | ); 17 | }); 18 | 19 | it('Renders the Login component', () => { 20 | const inputs = document.querySelectorAll('input'); 21 | 22 | expect(inputs).toHaveLength(2); 23 | expect(inputs[0].placeholder).toBe('email'); 24 | expect(inputs[1].placeholder).toBe('password'); 25 | }); 26 | 27 | it('Does not render incorrect password/email message', () => { 28 | const incorrectMessage = document.getElementsByClassName('small-text'); 29 | expect(incorrectMessage).toHaveLength(0); 30 | }); 31 | 32 | it('Renders error message on wrong login', () => { 33 | const emailInput = screen.getByPlaceholderText('email'); 34 | const passwordInput = screen.getByPlaceholderText('password'); 35 | 36 | userEvent.type(emailInput, ''); 37 | userEvent.type(passwordInput, ''); 38 | 39 | const loginButton = document.querySelector( 40 | 'button[type="submit"]' 41 | ); 42 | console.log('loginButton', loginButton); 43 | // userEvent.click(loginButton); 44 | fireEvent.click(loginButton!); 45 | 46 | //expect to see error message "Incorrect Password or Email" 47 | expect(screen.getByText('Incorrect Password or Email')).toBeDefined(); 48 | // const errorMessage = document.getElementsByClassName('small-text'); 49 | // console.log('errorMessage', errorMessage); 50 | // expect(errorMessage).toHaveLength(1); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /__tests__/server/routes.ts: -------------------------------------------------------------------------------- 1 | import request from 'supertest'; 2 | import app from '../../server/app'; 3 | 4 | // API Route testing 5 | describe('/api', () => { 6 | describe('/getQueryResults', () => { 7 | describe('POST', () => { 8 | it('responds with 200 status and application/json content type', async () => { 9 | const res = await request(app) 10 | .post('/api/getQueryResults') 11 | .send({ queryString: 'SELECT name from people' }); 12 | expect(res.statusCode).toEqual(200); 13 | }); 14 | }); 15 | }); 16 | describe('/getSchema', () => { 17 | describe('GET', () => {}); 18 | }); 19 | describe('/getHistory', () => { 20 | describe('GET', () => {}); 21 | }); 22 | describe('/addURI', () => { 23 | describe('POST', () => {}); 24 | }); 25 | }); 26 | 27 | // User route testing 28 | describe('/user', () => { 29 | describe('/emailCheck', () => { 30 | describe('POST', () => {}); 31 | }); 32 | 33 | describe('/signup', () => { 34 | describe('POST', () => {}); 35 | }); 36 | 37 | describe('/login', () => { 38 | describe('POST', () => {}); 39 | }); 40 | 41 | describe('/changePassword', () => { 42 | describe('POST', () => {}); 43 | }); 44 | 45 | describe('/logout', () => { 46 | describe('POST', () => {}); 47 | }); 48 | 49 | describe('/authenticate', () => { 50 | describe('GET', () => {}); 51 | }); 52 | }); 53 | 54 | // Catch all 404 testing 55 | describe('/', () => { 56 | it('Should return 404 and status message when sending invalid URL', async () => { 57 | const res = await request(app).get('/DKLSJA'); 58 | expect(res.statusCode).toEqual(404); 59 | expect(res.body).toEqual( 60 | `This is not the page you are looking for ¯\\_(ツ)_/¯` 61 | ); 62 | return; 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/smartER/f25708190ffbf068e24ab1df60936ce417f5d03b/assets/favicon.ico -------------------------------------------------------------------------------- /assets/login-uri-sequence.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/smartER/f25708190ffbf068e24ab1df60936ce417f5d03b/assets/login-uri-sequence.gif -------------------------------------------------------------------------------- /assets/login.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/smartER/f25708190ffbf068e24ab1df60936ce417f5d03b/assets/login.gif -------------------------------------------------------------------------------- /assets/logo-bulb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/smartER/f25708190ffbf068e24ab1df60936ce417f5d03b/assets/logo-bulb.png -------------------------------------------------------------------------------- /assets/logo-bulb.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 15 | 16 | -------------------------------------------------------------------------------- /assets/logo-extra-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/smartER/f25708190ffbf068e24ab1df60936ce417f5d03b/assets/logo-extra-padded.png -------------------------------------------------------------------------------- /assets/logo-extra-padded.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 15 | 16 | -------------------------------------------------------------------------------- /assets/logo-main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/smartER/f25708190ffbf068e24ab1df60936ce417f5d03b/assets/logo-main.png -------------------------------------------------------------------------------- /assets/logo-main.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 15 | 16 | -------------------------------------------------------------------------------- /assets/logo-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/smartER/f25708190ffbf068e24ab1df60936ce417f5d03b/assets/logo-padded.png -------------------------------------------------------------------------------- /assets/logo-padded.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 15 | 16 | -------------------------------------------------------------------------------- /assets/query-diagram-render.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/smartER/f25708190ffbf068e24ab1df60936ce417f5d03b/assets/query-diagram-render.gif -------------------------------------------------------------------------------- /assets/results-scroll.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/smartER/f25708190ffbf068e24ab1df60936ce417f5d03b/assets/results-scroll.gif -------------------------------------------------------------------------------- /assets/save-history.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/smartER/f25708190ffbf068e24ab1df60936ce417f5d03b/assets/save-history.gif -------------------------------------------------------------------------------- /assets/settings-tab.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/smartER/f25708190ffbf068e24ab1df60936ce417f5d03b/assets/settings-tab.gif -------------------------------------------------------------------------------- /assets/smarter-db.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/smartER/f25708190ffbf068e24ab1df60936ce417f5d03b/assets/smarter-db.png -------------------------------------------------------------------------------- /client/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Router, Routes, Route } from 'react-router-dom'; 3 | import { LoginContext, HomepageContext } from './Context'; 4 | import Login from './components/Login'; 5 | import Homepage from './components/Homepage'; 6 | import Signup from './components/Signup'; 7 | import { Navigate } from 'react-router-dom'; 8 | 9 | const App = () => { 10 | const [email, setEmail] = useState(''); 11 | const [password, setPassword] = useState(''); 12 | const [submit, setSubmit] = useState(false); 13 | const [queryString, setQueryString] = useState(''); 14 | const [history, setHistory] = useState([]); 15 | const [uri, setUri] = useState(''); 16 | const [dbCredentials, setDBCredentials] = useState({ 17 | host: '', 18 | port: 0, 19 | dbUsername: '', 20 | dbPassword: '', 21 | database: '', 22 | }); 23 | const [queryResponse, setQueryResponse] = useState([]); 24 | const [masterData, setMasterData] = useState({}); 25 | const [renderedData, setRenderedData] = useState({}); 26 | const [renderedDataPositions, setRenderedDataPositions] = useState({}); 27 | const [errorMessages, setErrorMessages] = useState(['']); 28 | const [reset, setReset] = useState(false); 29 | 30 | return ( 31 | 39 | 65 | 66 | } /> 67 | } /> 68 | 73 | ) : ( 74 | 75 | ) 76 | } 77 | /> 78 | 79 | 80 | 81 | ); 82 | }; 83 | 84 | export default App; 85 | -------------------------------------------------------------------------------- /client/Context.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext, Dispatch, SetStateAction } from 'react'; 2 | 3 | // Login context 4 | export type LoginContextType = { 5 | email: string; 6 | setEmail: Dispatch>; 7 | password: string; 8 | setPassword: Dispatch>; 9 | }; 10 | 11 | const defaultState = { 12 | email: '', 13 | setEmail: () => {}, 14 | password: '', 15 | setPassword: () => {}, 16 | }; 17 | 18 | export const LoginContext = createContext( 19 | defaultState 20 | ); 21 | 22 | //Homepage Context 23 | export type HomepageContextType = { 24 | submit: boolean; 25 | setSubmit: Dispatch>; 26 | queryString: string; 27 | setQueryString: Dispatch>; 28 | history: historyType[]; 29 | setHistory: Dispatch>; 30 | uri: string; 31 | setUri: Dispatch>; 32 | dbCredentials: dbCredentialsType; 33 | setDBCredentials: Dispatch>; 34 | queryResponse: any; 35 | setQueryResponse: Dispatch>; 36 | masterData: any; 37 | setMasterData: Dispatch>; 38 | renderedData: any; 39 | setRenderedData: Dispatch>; 40 | renderedDataPositions: any; 41 | setRenderedDataPositions: Dispatch>; 42 | errorMessages: string[]; 43 | setErrorMessages: Dispatch>; 44 | reset: boolean; 45 | setReset: Dispatch>; 46 | }; 47 | 48 | const defaultHomeState = { 49 | submit: false, 50 | setSubmit: () => {}, 51 | queryString: '', 52 | setQueryString: () => {}, 53 | history: [], 54 | setHistory: () => {}, 55 | uri: '', 56 | setUri: () => {}, 57 | dbCredentials: { 58 | host: '', 59 | port: 0, 60 | dbUsername: '', 61 | dbPassword: '', 62 | database: '', 63 | }, 64 | setDBCredentials: () => {}, 65 | queryResponse: [], 66 | setQueryResponse: () => {}, 67 | masterData: {}, 68 | setMasterData: () => {}, 69 | renderedData: {}, 70 | setRenderedData: () => {}, 71 | renderedDataPositions: [], 72 | setRenderedDataPositions: () => [], 73 | errorMessages: [], 74 | setErrorMessages: () => [], 75 | reset: false, 76 | setReset: () => {}, 77 | }; 78 | 79 | export const HomepageContext = createContext( 80 | defaultHomeState 81 | ); 82 | 83 | //TYPES 84 | export interface historyType { 85 | created_at: string; 86 | query: string; 87 | } 88 | 89 | export interface dbCredentialsType { 90 | host: string; 91 | port: number; 92 | dbUsername: string; 93 | dbPassword: string; 94 | database: string; 95 | } 96 | -------------------------------------------------------------------------------- /client/components/HomeComponents/Diagram.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useContext, useCallback, useEffect } from 'react'; 2 | import ReactFlow, { 3 | Controls, 4 | Background, 5 | useNodesState, 6 | useEdgesState, 7 | addEdge, 8 | ReactFlowProvider, 9 | } from 'reactflow'; 10 | import 'reactflow/dist/style.css'; 11 | import { HomepageContext } from '../../Context'; 12 | import { parseEdges, parseNodes } from './DiagramLogic/ParseNodes'; 13 | import CustomColumnNode from './DiagramLogic/CustomColumnNode'; 14 | import CustomTitleNode from './DiagramLogic/CustomTitleNode'; 15 | import conditionalSchemaParser from './DiagramLogic/ConditionalSchemaParser'; 16 | import { getElkData } from './DiagramLogic/LayoutCalc'; 17 | 18 | const proOptions = { hideAttribution: true }; 19 | const nodeTypes = { 20 | CustomColumnNode: CustomColumnNode, 21 | CustomTitleNode: CustomTitleNode, 22 | }; 23 | 24 | const fitViewOptions = { 25 | padding: 10, 26 | }; 27 | 28 | const Diagram: FC<{}> = () => { 29 | const [nodes, setNodes, onNodesChange] = useNodesState([]); 30 | const [edges, setEdges, onEdgesChange] = useEdgesState([]); 31 | 32 | const { 33 | queryString, 34 | submit, 35 | masterData, 36 | setMasterData, 37 | renderedData, 38 | setRenderedData, 39 | renderedDataPositions, 40 | setRenderedDataPositions, 41 | errorMessages, 42 | setErrorMessages, 43 | queryResponse, 44 | setQueryResponse, 45 | reset, 46 | setReset, 47 | } = useContext(HomepageContext)!; 48 | 49 | async function getQueryResults() { 50 | //setSubmit to trigger useEffect for re-rendering Diagram.tsx 51 | try { 52 | const created_at = String(Date.now()); 53 | const data = await fetch('/api/getQueryResults', { 54 | method: 'POST', 55 | headers: { 'Content-Type': 'application/json' }, 56 | body: JSON.stringify({ created_at, queryString }), 57 | }); 58 | if (data.status === 200) { 59 | const parsedData = await data.json(); 60 | setQueryResponse(parsedData); 61 | } 62 | } catch (error) { 63 | console.log(`Error in QueryInput.tsx ${error}`); 64 | return `Error in QueryInput.tsx ${error}`; 65 | } 66 | } 67 | 68 | const onConnect = useCallback( 69 | (params: any) => setEdges((eds) => addEdge(params, eds)), 70 | [setEdges] 71 | ); 72 | 73 | const onNodeDragStop = (e: any, node: any) => { 74 | setRenderedDataPositions([...renderedDataPositions, node]); 75 | }; 76 | 77 | const getERDiagram = async () => { 78 | try { 79 | const data = await fetch('/api/getSchema', { 80 | method: 'GET', 81 | headers: { 'Content-Type': 'application/json' }, 82 | }); 83 | const parsedData = await data.json(); 84 | setMasterData(parsedData); 85 | return; 86 | } catch (error) { 87 | console.log(`Error in getERDiagram: ${error}`); 88 | } 89 | }; 90 | 91 | useEffect(() => { 92 | getERDiagram(); 93 | }, []); 94 | 95 | useEffect(() => { 96 | setNodes([]); 97 | setEdges([]); 98 | }, [reset]); 99 | 100 | // when submit value changes, parse query to conditionally render ER diagram and if no errors are found in the logic, 101 | // invoke getQueryResults function to render query results 102 | useEffect(() => { 103 | if (queryString) { 104 | async function updateNodes() { 105 | setErrorMessages(['']); 106 | const queryParse = conditionalSchemaParser(queryString, masterData); 107 | const errorArr = queryParse.errorArr; 108 | setErrorMessages(errorArr); 109 | if (!errorArr[0]) getQueryResults(); 110 | 111 | const defaultNodes = parseNodes(queryParse.mainObj); 112 | const defaultEdges = parseEdges(queryParse.mainObj); 113 | 114 | // if no new tables are being added, retain positions; else recalculate 115 | let positions = []; 116 | const currentTables = Object.keys(renderedData); 117 | const newTables = Object.keys(queryParse.mainObj); 118 | const combinedLength = new Set(currentTables.concat(newTables)).size; 119 | if ( 120 | currentTables.length === newTables.length && 121 | currentTables.length === combinedLength 122 | ) { 123 | positions = renderedDataPositions; 124 | } 125 | const dataElk = await getElkData(defaultNodes, defaultEdges, positions); 126 | 127 | setNodes(dataElk); 128 | setRenderedDataPositions(dataElk); 129 | setEdges(defaultEdges); 130 | setRenderedData(queryParse.mainObj); 131 | } 132 | updateNodes(); 133 | } 134 | }, [submit]); 135 | 136 | return ( 137 | 138 |
139 | 150 | 151 | 152 | 153 |
154 |
155 | ); 156 | }; 157 | 158 | export default Diagram; 159 | -------------------------------------------------------------------------------- /client/components/HomeComponents/DiagramLogic/ConditionalSchemaParser.tsx: -------------------------------------------------------------------------------- 1 | import { Statement, parseFirst } from 'pgsql-ast-parser'; 2 | 3 | interface columnObj { 4 | table_name: string; 5 | column_name: string; 6 | data_type: string; 7 | primary_key?: boolean; 8 | foreign_key?: boolean; 9 | linkedTable?: string; 10 | linkedTableColumn?: string; 11 | activeColumn?: boolean; 12 | activeLink?: boolean; 13 | foreign_tables?: string[]; 14 | } 15 | type returnObj = { 16 | errorArr: string[]; 17 | mainObj: Record>; 18 | }; 19 | type tableAlias = { 20 | [key: string]: string; 21 | }; 22 | type mainObj = { 23 | [key: string]: Record; 24 | }; 25 | type key = { 26 | name: string; 27 | table: { name: string }; 28 | type: string; 29 | }; 30 | 31 | function conditionalSchemaParser(query: string, schema: any): returnObj { 32 | // errorArr contains errors that we find in the query when running mainFunc 33 | const errorArr: string[] = []; 34 | // mainObj contains a partial copy of the ER diagram with flagged columns and tables 35 | const mainObj: mainObj = {}; 36 | const tableAliasLookup: tableAlias = {}; 37 | let activeTables: string[]; 38 | const columnsWithUndefinedAlias: Record> = {}; 39 | let currentSubqueryAlias: string; 40 | 41 | const ast: Statement = parseFirst(query); 42 | const queue: any[] = []; 43 | queue.push(ast); 44 | 45 | const selectHandler = (obj: any) => { 46 | activeTables = []; 47 | tableHandler(obj.from); 48 | columnHandler(obj.columns); 49 | 50 | // cleanup aliases 51 | if ( 52 | currentSubqueryAlias && 53 | columnsWithUndefinedAlias[currentSubqueryAlias] 54 | ) { 55 | delete columnsWithUndefinedAlias[currentSubqueryAlias]; 56 | } 57 | currentSubqueryAlias = ''; 58 | 59 | for (let key in obj) { 60 | switch (key) { 61 | case 'from': 62 | break; 63 | case 'columns': 64 | break; 65 | case 'type': 66 | break; 67 | case 'where': 68 | break; 69 | default: 70 | queue.push(obj[key]); 71 | } 72 | } 73 | }; 74 | 75 | const tableHandler = (arr: any[]) => { 76 | for (let i = 0; i < arr.length; i++) { 77 | const currentTable = arr[i]; 78 | const type = currentTable.type; 79 | switch (type) { 80 | case 'table': { 81 | const tableName = currentTable.name.name; 82 | const alias = currentTable.name.alias; 83 | if (schema[tableName as keyof typeof schema]) { 84 | // Deep copy the table from data 85 | if (!mainObj[tableName]) { 86 | mainObj[tableName] = JSON.parse( 87 | JSON.stringify(schema[tableName as keyof typeof schema]) 88 | ); 89 | } 90 | activeTables.push(tableName); 91 | // Update alias 92 | if (currentTable.join) queue.push(currentTable); // find example of this 93 | 94 | if (alias) tableAliasLookup[alias] = tableName; 95 | else tableAliasLookup[tableName] = tableName; 96 | } else { 97 | // Error to push because the table doesn't exist in our data 98 | errorArr.push(`Table name ${tableName} is not found in database`); 99 | } 100 | 101 | break; 102 | } 103 | default: { 104 | queue.push(currentTable); 105 | break; 106 | } 107 | } 108 | } 109 | }; 110 | 111 | const columnHandler = (arr: any[]) => { 112 | for (let i = 0; i < arr.length; i++) { 113 | const currentColumn = arr[i]; 114 | const currentColDetails = currentColumn.expr || currentColumn; 115 | const type = currentColDetails.type; 116 | 117 | switch (type) { 118 | // Update master obj with active columns add activeColumn = true 119 | case 'ref': 120 | const columnName = currentColDetails.name; 121 | 122 | // subquery logic: if current col array is part of a subquery and the col is not referenced in the main query, skip it 123 | if ( 124 | (currentSubqueryAlias && 125 | !columnsWithUndefinedAlias[currentSubqueryAlias]) || 126 | (columnsWithUndefinedAlias[currentSubqueryAlias] && 127 | !columnsWithUndefinedAlias[currentSubqueryAlias].has(columnName)) 128 | ) { 129 | break; 130 | } 131 | 132 | let specifiedTable: string | undefined; 133 | if (currentColDetails.table && currentColDetails.table.name) { 134 | const lookupAlias = currentColDetails.table.name; 135 | // console.log('table alias lookup', tableAliasLookup); 136 | // console.log('lookup value', lookupAlias); 137 | specifiedTable = tableAliasLookup[lookupAlias]; 138 | 139 | // if no specified table is found (alias is defined in subquery) 140 | if (!specifiedTable) { 141 | // if column with alias already exists, push to key value pair 142 | if (columnsWithUndefinedAlias[lookupAlias]) { 143 | columnsWithUndefinedAlias[lookupAlias].add(columnName); 144 | } else 145 | columnsWithUndefinedAlias[lookupAlias] = new Set([columnName]); 146 | break; 147 | } 148 | } 149 | 150 | // if tableName is specified, tables is array of tableName, else tables is array of all tables in query 151 | let tables: string[] = []; 152 | if (specifiedTable) { 153 | tables.push(specifiedTable); 154 | } else { 155 | tables = [...activeTables]; 156 | } 157 | 158 | let colMatchCount: number = 0; 159 | for (let table of tables) { 160 | if (columnName !== '*') { 161 | if (mainObj[table][columnName]) { 162 | colMatchCount++; 163 | mainObj[table][columnName].activeColumn = true; 164 | } 165 | } else { 166 | for (let column in mainObj[table]) { 167 | mainObj[table][column].activeColumn = true; 168 | } 169 | } 170 | } 171 | 172 | if (colMatchCount === 0 && columnName !== '*') 173 | errorArr.push(`Column ${columnName} does not exist`); 174 | if (colMatchCount > 1) 175 | errorArr.push(`Column ${columnName} exists in more than one table`); 176 | break; 177 | 178 | case 'call': 179 | columnHandler(currentColDetails.args); 180 | break; 181 | default: 182 | queue.push(currentColDetails); 183 | } 184 | } 185 | // if type is select, invoke selectHandler <- have not seen any columns with type select. 186 | // if (columnObj.type && columnObj.type === 'select') { 187 | // selectHandler(columnObj) 188 | // } 189 | }; 190 | 191 | const joinHandler = (obj: any) => { 192 | const left = obj.on.left; 193 | const right = obj.on.right; 194 | if (left) flagActiveLinks(left, tableAliasLookup, mainObj, errorArr); 195 | else errorArr.push(`No column found for left position of ${obj.type}`); 196 | if (right) flagActiveLinks(right, tableAliasLookup, mainObj, errorArr); 197 | else errorArr.push(`No column found for right position of ${obj.type}`); 198 | }; 199 | 200 | const connectedTablesHandler = (table: string) => { 201 | const isJoinTable = (tableObj: any): boolean => { 202 | for (let column in tableObj) { 203 | const key = 204 | tableObj[column].foreign_key || tableObj[column].primary_key; 205 | if (!key) return false; 206 | } 207 | return true; 208 | }; 209 | 210 | // for (const table in mainObj) { 211 | for (const column in mainObj[table]) { 212 | const linkedTable = mainObj[table][column].linkedTable; 213 | const foreignTables = mainObj[table][column].foreign_tables; 214 | 215 | if (foreignTables) { 216 | // iterate through foreign_tables array and copy any missing tables to mainObj 217 | for (const foreignTable of foreignTables) { 218 | if (!mainObj[foreignTable]) 219 | mainObj[foreignTable] = { 220 | ...schema[foreignTable as keyof typeof schema], 221 | }; 222 | 223 | // if foreign table is a join table, go one layer out 224 | // stretch: user option to toggle this feature on/off? 225 | if (isJoinTable(mainObj[foreignTable])) 226 | connectedTablesHandler(foreignTable); 227 | } 228 | } 229 | 230 | if (linkedTable && !mainObj[linkedTable]) { 231 | mainObj[linkedTable] = JSON.parse( 232 | JSON.stringify(schema[linkedTable as keyof typeof schema]) 233 | ); 234 | 235 | // if linked table is a join table, go one layer out 236 | if (isJoinTable(mainObj[linkedTable])) 237 | connectedTablesHandler(linkedTable); 238 | } 239 | } 240 | // } 241 | }; 242 | 243 | while (queue.length) { 244 | const obj = queue[0]; 245 | 246 | let type = obj.type; 247 | if (type) { 248 | type = type.toLowerCase(); 249 | if (type === 'select') { 250 | selectHandler(obj); 251 | } else if (type.includes('join')) { 252 | joinHandler(obj); 253 | } else if (type === 'statement') { 254 | currentSubqueryAlias = obj.alias; 255 | for (let key in obj) { 256 | if (key.toLowerCase() !== 'where' && typeof obj[key] === 'object') 257 | queue.push(obj[key]); 258 | } 259 | } else { 260 | for (let key in obj) { 261 | if (key.toLowerCase() !== 'where' && typeof obj[key] === 'object') 262 | queue.push(obj[key]); 263 | } 264 | } 265 | } 266 | queue.shift(); 267 | } 268 | 269 | for (const table in mainObj) connectedTablesHandler(table); 270 | return { errorArr: errorArr, mainObj: mainObj }; 271 | } 272 | 273 | function flagActiveLinks( 274 | onKey: key, 275 | tableAlias: tableAlias, 276 | mainObj: mainObj, 277 | errorArr: string[] 278 | ) { 279 | if (onKey.table.name) { 280 | // attempt to lookup table name by alias 281 | const tableName = tableAlias[onKey.table.name]; 282 | if (tableName) { 283 | // identify col name and flag it in mainObj 284 | const columnName = onKey.name; 285 | mainObj[tableName][columnName].activeLink = true; 286 | } // Potential error push here if tableName was not found? 287 | } else { 288 | // else iterate through mainObj and check for a table that has a column name that matches 289 | const tables = Object.keys(mainObj); 290 | const columnName = onKey.name; 291 | let counter = 0; 292 | for (let i = 0; i < tables.length; i++) { 293 | const tableName = tables[i]; 294 | //keep track of matches, if no matches, add to errorArr, if >1 flag both and add to errArr that column exists in more than one table 295 | if (mainObj[tableName][columnName]) { 296 | mainObj[tableName][columnName].activeLink = true; 297 | counter++; 298 | } 299 | // Potential error push here if tableName/columnName was not found? 300 | } 301 | if (counter === 0) errorArr.push('no cols found'); 302 | else if (counter > 1) errorArr.push('too many cols'); 303 | } 304 | } 305 | 306 | export default conditionalSchemaParser; 307 | -------------------------------------------------------------------------------- /client/components/HomeComponents/DiagramLogic/CustomColumnNode.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | import { Handle, Position } from 'reactflow'; 3 | 4 | const handleStyleLeft = { 5 | opacity: '0', 6 | marginLeft: '5px', 7 | }; 8 | const handleStyleRight = { 9 | opacity: '0', 10 | marginRight: '5px', 11 | }; 12 | 13 | function CustomColumnNode({ data, isConnectable }: any) { 14 | const onChange = useCallback((evt: any) => { 15 | // console.log(evt.target.value); 16 | }, []); 17 | 18 | return ( 19 |
20 | 26 |
27 |
{data.icon}
28 |
29 |
{data.columnName}
30 |
{data.dataType}
31 |
32 |
33 | 40 |
41 | ); 42 | } 43 | 44 | export default CustomColumnNode; 45 | -------------------------------------------------------------------------------- /client/components/HomeComponents/DiagramLogic/CustomTitleNode.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | import { Handle, Position } from 'reactflow'; 3 | 4 | const handleStyleLeft = { 5 | opacity: '0', 6 | marginLeft: '5px', 7 | }; 8 | const handleStyleRight = { 9 | opacity: '0', 10 | marginRight: '5px', 11 | }; 12 | 13 | function CustomTitleNode({ data, isConnectable }: any) { 14 | const onChange = useCallback((evt: any) => { 15 | // console.log(evt.target.value); 16 | }, []); 17 | 18 | return ( 19 |
20 | 26 |
27 | 30 |
31 | 38 |
39 | ); 40 | } 41 | 42 | export default CustomTitleNode; 43 | -------------------------------------------------------------------------------- /client/components/HomeComponents/DiagramLogic/LayoutCalc.tsx: -------------------------------------------------------------------------------- 1 | import Elk from 'elkjs'; 2 | 3 | // elk settings 4 | const elk: any = new Elk({ 5 | defaultLayoutOptions: { 6 | 'elk.algorithm': 'layered', 7 | 'elk.direction': 'RIGHT', 8 | 'elk.spacing.nodeNode': '100', 9 | 'elk.layered.spacing.nodeNodeBetweenLayers': '110', 10 | 'elk.layered.noOverlap': 'true', 11 | 'elk.layered.spacing.edgeNodeBetweenLayers': '100', 12 | 'elk.padding': '[top=50, bottom=50, left=50, right=50]', 13 | 'elk.edgeRouting': 'SPLINES', 14 | 'elk.layered.nodePlacement.strategy': 'SIMPLE', 15 | // 'elk.edgeRouting.splines.mode': 'CONSERVATIVE', 16 | // 'elk.considerModelOrder.strategy': 'PREFER_NODES', 17 | 'elk.crossingMinimization.strategy': 'LAYER_SWEEP', 18 | // 'elk.layered.layering.strategy': 'NETWORK_SIMPLEX' 19 | }, 20 | }); 21 | 22 | // grab information for nodes and edges needed for elk 23 | export async function getElkData(nodes: any, edges: any, startPositions: any) { 24 | const elkNodes: any[] = []; 25 | const elkEdges: any[] = []; 26 | 27 | // add node data to elkNodes 28 | nodes.forEach((node: any) => { 29 | elkNodes.push({ 30 | id: node.id, 31 | width: node.style.width, 32 | height: node.style.height, 33 | }); 34 | }); 35 | 36 | // add edges as only groups, not individual column nodes 37 | edges.forEach((edge: any) => { 38 | elkEdges.push({ 39 | id: edge.id, 40 | source: `${edge.source.split('.')[1]}.group`, 41 | target: `${edge.target.split('.')[1]}.group`, 42 | }); 43 | }); 44 | 45 | // run elk 46 | const elkCalculate = await elk.layout({ 47 | id: 'root', 48 | children: elkNodes, 49 | edges: elkEdges, 50 | }); 51 | 52 | // update nodes with elk positions, switching back from group edges to individual column edges 53 | const positions = nodes.map((node: any) => { 54 | const elkNode = elkCalculate.children.find( 55 | (elkNode: any) => elkNode.id === node.id 56 | ); 57 | if (node.id.includes('group')) { 58 | let x = elkNode.x; 59 | let y = elkNode.y; 60 | 61 | // find starting position of current node 62 | let matchingNode; 63 | if (Array.isArray(startPositions)) { 64 | for (let startNode of startPositions) { 65 | // console.log('startNode', startNode); 66 | if (JSON.stringify(startNode.id) === JSON.stringify(node.id)) { 67 | matchingNode = startNode; 68 | } 69 | } 70 | } 71 | 72 | if (matchingNode) { 73 | x = matchingNode.position.x; 74 | y = matchingNode.position.y; 75 | } 76 | 77 | return { 78 | ...node, 79 | position: { 80 | x: x, 81 | y: y, 82 | }, 83 | }; 84 | } else return node; 85 | }); 86 | return positions; 87 | } 88 | -------------------------------------------------------------------------------- /client/components/HomeComponents/DiagramLogic/ParseNodes.tsx: -------------------------------------------------------------------------------- 1 | import CustomColumnNode from './CustomColumnNode'; 2 | import React from 'react'; 3 | import IonIcon from '@reacticons/ionicons'; 4 | 5 | export function parseNodes(rawData: any): any { 6 | const standardHeight = 25; 7 | // const pkey = ; 8 | const pkey = ; 9 | const fkey = ; 10 | 11 | const nodes: any = []; 12 | let j = 0; 13 | for (const table in rawData) { 14 | // create GROUPS 15 | const newContainer = { 16 | id: `${table}.group`, 17 | type: 'group', 18 | // position: { x: 200 * j, y: Math.random() * 100 }, //Control spacing of tables here, Probably needs an algo 19 | data: { label: table }, 20 | style: { 21 | height: 22 | Object.keys(rawData[table]).length * standardHeight + 23 | standardHeight + 24 | 0, 25 | width: 180, //was 152 26 | display: 'flex', 27 | opacity: 0.25, 28 | zIndex: 10, 29 | }, 30 | draggable: true, 31 | }; 32 | j++; 33 | nodes.push(newContainer); 34 | 35 | // create table name node 36 | const newColumnTitle = { 37 | id: `${table}.columnName`, 38 | type: 'CustomTitleNode', // swap for default of CustomTitleNode 39 | parentNode: `${table}.group`, 40 | extent: 'default', 41 | position: { x: 0, y: 0 + 0 }, 42 | data: { label: table }, 43 | sourcePosition: 'bottom', 44 | targetPosition: 'bottom', 45 | style: { 46 | background: '#1E3D59', 47 | color: 'F5F0E1', 48 | borderRadius: '5px', 49 | opacity: 1, 50 | transition: 'opacity 250ms ease-in', 51 | width: 180, 52 | height: standardHeight, 53 | }, 54 | draggable: false, 55 | }; 56 | nodes.push(newColumnTitle); 57 | 58 | //create Column nodes 59 | let i = 0; 60 | for (const columnObj in rawData[table]) { 61 | const column = rawData[table][columnObj]; 62 | const newColumnNode = { 63 | id: `${columnObj}.${table}.node`, 64 | type: 'CustomColumnNode', //swap for default or CustomColumnNode 65 | parentNode: `${table}.group`, 66 | extent: 'parent', 67 | position: { 68 | x: 0, 69 | y: i === 0 ? standardHeight : i * standardHeight + standardHeight, 70 | }, //Control spacing of tables here, Probably needs an algo 71 | data: { 72 | label: `${columnObj} | ${column.data_type}`, 73 | icon:
, 74 | columnName: columnObj, 75 | dataType: column.data_type, 76 | }, 77 | sourcePosition: 'right', 78 | targetPosition: 'left', 79 | draggable: false, 80 | style: { 81 | background: 'transparent', 82 | borderRadius: '', 83 | opacity: 1, 84 | transition: 'opacity 250ms ease-in', 85 | width: 180, 86 | height: standardHeight, 87 | }, 88 | }; 89 | 90 | if (column.primary_key) { 91 | newColumnNode.data.icon = pkey; 92 | newColumnNode.data.columnName = `${columnObj}`; 93 | newColumnNode.data.dataType = `${column.data_type}`; 94 | } 95 | if (column.foreign_key) { 96 | newColumnNode.data.icon = fkey; 97 | newColumnNode.data.columnName = `${columnObj}`; 98 | newColumnNode.data.dataType = `${column.data_type}`; 99 | } 100 | //check if active column 101 | if (column.activeColumn) { 102 | newColumnNode.style = { 103 | background: '#FFC13B', 104 | borderRadius: '5px', 105 | width: 180, 106 | opacity: 1, 107 | transition: 'opacity 250ms ease-in', 108 | height: standardHeight, 109 | }; 110 | } else { 111 | newColumnNode.style = { 112 | background: '#6B7B8C', 113 | borderRadius: '5px', 114 | width: 180, 115 | opacity: 1, 116 | transition: 'opacity 250ms ease-in', 117 | height: standardHeight, 118 | }; 119 | } 120 | nodes.push(newColumnNode); 121 | i++; 122 | } 123 | } 124 | return nodes; 125 | } 126 | 127 | export function parseEdges(data: any): any { 128 | const edges: any = []; 129 | for (const table in data) { 130 | for (const columnObj in data[table]) { 131 | const columnName = data[table][columnObj].column_name; 132 | const column = data[table][columnObj]; 133 | if (column.foreign_key) { 134 | const newEdge = { 135 | id: `${table}.${columnName}->${column.linkedTableColumn}.${column.column_name}`, 136 | source: `${columnName}.${table}.node`, 137 | target: `${column.linkedTableColumn}.${column.linkedTable}.node`, 138 | type: 'default', 139 | style: {}, 140 | animated: false, 141 | }; 142 | if (column.activeLink && data[column.linkedTable]) { 143 | newEdge.animated = false; 144 | newEdge.style = { 145 | stroke: 'white', 146 | strokeWidth: '5', 147 | }; 148 | } else { 149 | newEdge.animated = false; 150 | newEdge.style = { 151 | strokeWidth: '3', 152 | stroke: 'grey', 153 | strokeDasharray: '5,5', 154 | }; 155 | } 156 | // check if active connection 157 | 158 | edges.push(newEdge); 159 | } 160 | } 161 | } 162 | return edges; 163 | } 164 | -------------------------------------------------------------------------------- /client/components/HomeComponents/Header.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | import { LoginContext } from '../../Context'; 4 | 5 | // clicking profile modal will openthe option to logout 6 | const Header: React.FC<{}> = () => { 7 | const { email, setEmail, password, setPassword } = useContext(LoginContext)!; 8 | const navigate = useNavigate(); 9 | 10 | const handleClick = async (e: any) => { 11 | try { 12 | e.preventDefault(); 13 | const data = await fetch('/user/logout', { 14 | method: 'POST', 15 | headers: { 'Content-Type': 'application/json' }, 16 | }); 17 | setEmail(''); 18 | setPassword(''); 19 | localStorage.setItem('userIn', 'false'); 20 | navigate('/'); 21 | } catch (error) { 22 | console.log(`Error in useEffect logoutHandleClick ${error}`); 23 | return `Error in useEffect logoutHandleClick ${error}`; 24 | } 25 | // reset state of email and password to blank string 26 | 27 | //TODO: Add a fetch request delete cookies 28 | }; 29 | return ( 30 |
31 |
32 | 35 |
36 | ); 37 | }; 38 | export default Header; 39 | -------------------------------------------------------------------------------- /client/components/HomeComponents/InputContainer.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import QueryInput from './InputContainerComponents/QueryInput'; 3 | import History from './InputContainerComponents/History'; 4 | import Settings from './InputContainerComponents/Settings'; 5 | 6 | const InputContainer: React.FC<{}> = () => { 7 | const [tab, setTab] = useState('Query'); 8 | 9 | return ( 10 |
11 |
12 | 18 | 24 | 30 |
31 | 32 |
33 |
34 | 35 |
36 |
39 | 40 |
41 |
44 | 45 |
46 |
47 |
48 | ); 49 | }; 50 | 51 | export default InputContainer; 52 | -------------------------------------------------------------------------------- /client/components/HomeComponents/InputContainerComponents/History.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useContext, useEffect } from 'react'; 2 | import { HomepageContext } from '../../../Context'; 3 | 4 | interface setTab { 5 | setTab: React.Dispatch>; 6 | } 7 | 8 | const History: React.FC = ({ setTab }) => { 9 | const { 10 | history, 11 | setHistory, 12 | queryString, 13 | setQueryString, 14 | submit, 15 | setSubmit, 16 | } = useContext(HomepageContext)!; 17 | const [historyElements, setHistoryElements] = useState([]); 18 | 19 | const getHistory = async () => { 20 | try { 21 | const data = await fetch('api/getHistory', { 22 | method: 'GET', 23 | headers: { 'Content-Type': 'application/json' }, 24 | }); 25 | const parsedHistory = await data.json(); 26 | setHistory((prev: any) => { 27 | prev.push(...parsedHistory); 28 | return prev; 29 | }); 30 | 31 | makeHistoryElements(); 32 | } catch (error) { 33 | console.log('history error', error); 34 | } 35 | }; 36 | 37 | const convertTime = (dateString: string) => { 38 | return new Date(dateString).toLocaleString(); 39 | }; 40 | 41 | const makeHistoryElements = () => { 42 | const elements: any = history.map((object, index) => { 43 | const localTime = convertTime(object.created_at); 44 | return ( 45 |
  • 46 |
      47 |
    • 52 | {object.query} 53 |
    • 54 |
    • 58 | {localTime} 59 |
    • 60 |
    61 |
  • 62 | ); 63 | }); 64 | setHistoryElements(elements); 65 | }; 66 | 67 | useEffect(() => { 68 | getHistory(); 69 | }, []); 70 | 71 | useEffect(() => { 72 | makeHistoryElements(); 73 | }, [history]); 74 | 75 | const setHistoricalQuery = (e: any) => { 76 | setTab('Query'); 77 | setQueryString(e.target.innerText); 78 | // TODO: this set submit is not triggering the Diagram useEffect to update 79 | setSubmit(!submit); 80 | }; 81 | 82 | return ( 83 |
    84 |
    85 |
      86 | {historyElements} 87 |
    • 88 |
        89 |
      • 90 | Query 91 |
      • 92 |
      • 93 | Created At 94 |
      • 95 |
      96 |
    • 97 |
    98 |
    99 |
    100 | ); 101 | }; 102 | 103 | export default History; 104 | -------------------------------------------------------------------------------- /client/components/HomeComponents/InputContainerComponents/QueryInput.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import { HomepageContext } from '../../../Context'; 3 | import { debounce } from 'lodash'; 4 | import { check } from 'express-validator'; 5 | 6 | const QueryInput: React.FC<{}> = () => { 7 | const { 8 | queryString, 9 | setQueryString, 10 | history, 11 | setHistory, 12 | errorMessages, 13 | setErrorMessages, 14 | submit, 15 | setSubmit, 16 | queryResponse, 17 | setQueryResponse, 18 | reset, 19 | setReset, 20 | } = useContext(HomepageContext)!; 21 | 22 | const errorList = () => { 23 | // if the query is not valid, errorMessage will be returned 24 | if (errorMessages.length > 0) { 25 | const ErrorMessage: any = errorMessages.map((err, i) => { 26 | return ( 27 |
    31 | {`⚠ ${err}`} 32 |
    33 | ); 34 | }); 35 | return ErrorMessage; 36 | } 37 | }; 38 | 39 | const err = errorList(); 40 | // Handle submit of queryString 41 | const handleSubmit = async (e: any) => { 42 | e.preventDefault(); 43 | //setSubmit to trigger useEffect for re-rendering Diagram.tsx and getting query results 44 | setSubmit(!submit); 45 | // POST request to database with queryString 46 | try { 47 | const created_at = String(Date.now()); 48 | const data = await fetch('/api/postHistory', { 49 | method: 'POST', 50 | headers: { 'Content-Type': 'application/json' }, 51 | body: JSON.stringify({ queryString }), 52 | }); 53 | if (data.status === 200) { 54 | const parsedData = await data.json(); 55 | setHistory([ 56 | ...history, 57 | { created_at: parsedData, query: queryString }, 58 | ]); 59 | } else { 60 | // errorList(); 61 | } 62 | } catch (error) { 63 | console.log(`Error in QueryInput.tsx ${error}`); 64 | return `Error in QueryInput.tsx ${error}`; 65 | } 66 | }; 67 | 68 | let checkPause: boolean; 69 | const handleTyping = (e: any) => { 70 | setQueryString(e.target.value); 71 | const lastChar = e.target.value[e.target.value.length - 1]; 72 | const keys = new Set([' ', ',', ';', 'Tab', 'Return']); 73 | const lowerCaseQuery = e.target.value.toLowerCase(); 74 | if ( 75 | keys.has(lastChar) && 76 | lowerCaseQuery.includes('select') && 77 | lowerCaseQuery.includes('from') 78 | ) { 79 | setSubmit(!submit); 80 | errorList(); 81 | // do not check for pause if the last character entered was in the list of keys 82 | checkPause = false; 83 | } else { 84 | checkPause = true; 85 | } 86 | }; 87 | 88 | const handlePause = debounce(() => { 89 | // only run if handleTyping functionality did not just run 90 | if (checkPause) { 91 | const lowerCaseQuery = queryString.toLowerCase(); 92 | if ( 93 | lowerCaseQuery.includes('select') && 94 | lowerCaseQuery.includes('from') 95 | ) { 96 | setSubmit(!submit); 97 | } 98 | } 99 | }, 500); 100 | 101 | // handling tab key 102 | const handleKeys = (e: any) => { 103 | if (e.key === 'Tab') { 104 | e.preventDefault(); 105 | const start = e.target.selectionStart; 106 | const end = e.target.selectionEnd; 107 | 108 | e.target.value = `${e.target.value.substring( 109 | 0, 110 | start 111 | )}\t${e.target.value.substring(end)}`; 112 | e.target.selectionStart = e.target.selectionEnd = start + 1; 113 | } 114 | }; 115 | 116 | return ( 117 |
    118 |
    119 | 128 | 138 | 145 |
    146 | {errorMessages[0] &&
    {err}
    } 147 |
    148 | ); 149 | }; 150 | 151 | export default QueryInput; 152 | -------------------------------------------------------------------------------- /client/components/HomeComponents/InputContainerComponents/Settings.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useState, useEffect } from 'react'; 2 | import { HomepageContext, dbCredentialsType } from '../../../Context'; 3 | 4 | const CONNECTION: any = { 5 | success: 'Successfully connected!', 6 | fail: 'Failed to connect. Please try again.', 7 | }; 8 | 9 | interface setTab { 10 | setTab: React.Dispatch>; 11 | } 12 | 13 | const Settings: React.FC = ({ setTab }) => { 14 | const { 15 | uri, 16 | setUri, 17 | dbCredentials, 18 | setDBCredentials, 19 | masterData, 20 | setMasterData, 21 | } = useContext(HomepageContext)!; 22 | 23 | const [connectionStatus, setConnectionStatus] = useState(null); 24 | 25 | useEffect(() => { 26 | let timer = setTimeout(() => { 27 | setConnectionStatus(null); 28 | }, 3000); 29 | return () => { 30 | clearTimeout(timer); 31 | }; 32 | }, [connectionStatus]); 33 | 34 | //Handle submission of new URI 35 | const handleSubmit = async (e: any) => { 36 | e.preventDefault(); 37 | let encodedURI: string; 38 | if (uri) { 39 | encodedURI = encodeURIComponent(uri); 40 | } else { 41 | const { host, port, dbUsername, dbPassword, database } = dbCredentials; 42 | const hostspec = port ? `${host}:${port}` : host; 43 | encodedURI = encodeURIComponent( 44 | `postgres://${dbUsername}:${dbPassword}@${hostspec}/${database}` 45 | ); 46 | } 47 | try { 48 | const data = await fetch('/api/addURI', { 49 | method: 'POST', 50 | headers: { 'Content-Type': 'application/json' }, 51 | body: JSON.stringify({ encodedURI }), 52 | }); 53 | if (data.status === 200) { 54 | setConnectionStatus('success'); 55 | setTimeout(() => { 56 | setTab('Query'); 57 | }, 1500); 58 | setUri(''); 59 | setDBCredentials({ 60 | host: '', 61 | port: 0, 62 | dbUsername: '', 63 | dbPassword: '', 64 | database: '', 65 | }); 66 | const parsedData = await data.json(); 67 | setMasterData(parsedData); 68 | return; 69 | } else { 70 | setConnectionStatus('fail'); 71 | } 72 | } catch (error) { 73 | setConnectionStatus('fail'); 74 | return `Error in Settings.tsx ${error}`; 75 | } 76 | }; 77 | 78 | return ( 79 |
    80 |
    81 |

    URI

    82 |
    83 | 93 | 94 |

    Credentials

    95 | 96 | 102 | setDBCredentials((prevState: dbCredentialsType) => { 103 | return { ...prevState, host: e.target.value }; 104 | }) 105 | } 106 | value={dbCredentials.host} 107 | /> 108 | 109 | 115 | setDBCredentials((prevState: any) => { 116 | return { ...prevState, port: e.target.value }; 117 | }) 118 | } 119 | value={dbCredentials.port} 120 | /> 121 | 122 | 128 | setDBCredentials((prevState: dbCredentialsType) => { 129 | return { ...prevState, dbUsername: e.target.value }; 130 | }) 131 | } 132 | value={dbCredentials.dbUsername} 133 | /> 134 | 135 | 141 | setDBCredentials((prevState: dbCredentialsType) => { 142 | return { ...prevState, dbPassword: e.target.value }; 143 | }) 144 | } 145 | value={dbCredentials.dbPassword} 146 | /> 147 | 148 | 154 | setDBCredentials((prevState: dbCredentialsType) => { 155 | return { ...prevState, database: e.target.value }; 156 | }) 157 | } 158 | value={dbCredentials.database} 159 | /> 160 | 167 | {connectionStatus ? ( 168 |
    175 | {CONNECTION[connectionStatus]} 176 |
    177 | ) : null} 178 |
    179 |
    180 |
    181 | ); 182 | }; 183 | 184 | export default Settings; 185 | -------------------------------------------------------------------------------- /client/components/HomeComponents/QueryResults.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import { HomepageContext } from '../../Context'; 3 | 4 | const QueryResults: React.FC<{}> = () => { 5 | const { queryResponse, setQueryResponse } = useContext(HomepageContext)!; 6 | const { errorMessages, setErrorMessages } = useContext(HomepageContext)!; 7 | 8 | // if data response from backend is 200 then set queryResponse to the data 9 | /* columnNames: [['id','name',...] */ 10 | let columnNames: string[] = []; 11 | 12 | // table headers 13 | for (const row of queryResponse) { 14 | columnNames = Object.keys(row); 15 | } 16 | 17 | // table rows 18 | const columnsValue: string | number[][] = []; 19 | let columnsValuesInner: any = []; 20 | // get an array of just the values from each object in the queryResponse array 21 | for (let i = 0; i < queryResponse.length; i++) { 22 | const resultsArr = Object.entries(queryResponse[i]); 23 | for (const result of resultsArr) { 24 | columnsValuesInner.push(result[1]); 25 | } 26 | columnsValue.push(columnsValuesInner); 27 | columnsValuesInner = []; 28 | } 29 | 30 | if (!errorMessages[0]) 31 | return ( 32 | <> 33 |
    34 |

    Query Results

    35 |
    36 | 37 | 38 | 39 | {columnNames.map((column) => { 40 | return ( 41 | 44 | ); 45 | })} 46 | 47 | 48 | 49 | {columnsValue.map((column, index) => { 50 | const columnsArray: JSX.Element[] = []; 51 | column.map((data, i) => { 52 | return columnsArray.push( 53 | 56 | ); 57 | }); 58 | return ( 59 | 60 | {columnsArray} 61 | 62 | ); 63 | })} 64 | 65 |
    42 | {column} 43 |
    54 | {data} 55 |
    66 |
    67 |
    68 | 69 | ); 70 | else 71 | return ( 72 | <> 73 |
    74 |

    Query Results

    75 |
    76 | 77 | 78 | 79 | 80 | See query error above 81 |
    82 |
    83 |
    84 | 85 | ); 86 | }; 87 | 88 | export default QueryResults; 89 | 90 | // { 91 | // columnsValue.map((values: any) => { 92 | // 93 | // {columnNames.map((column: any) => { 94 | // return {values[column]}; 95 | // })} 96 | // ; 97 | // }); 98 | // } 99 | 100 | // {columnsValue.map((value) => { 101 | // 102 | // {columnNames.map((column) => ( 103 | // ( 104 | // {value[column]} 105 | // 106 | // ))} 107 | // 108 | // })} 109 | -------------------------------------------------------------------------------- /client/components/Homepage.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useState, useContext, useEffect } from 'react'; 2 | import Diagram from './HomeComponents/Diagram'; 3 | import Header from './HomeComponents/Header'; 4 | import InputContainer from './HomeComponents/InputContainer'; 5 | import QueryResults from './HomeComponents/QueryResults'; 6 | import Split from 'react-split'; 7 | import { useNavigate } from 'react-router-dom'; 8 | 9 | const TopComp = () => { 10 | const navigate = useNavigate(); 11 | 12 | return ( 13 | 14 | 15 | 16 | 17 | ); 18 | }; 19 | 20 | const Homepage: React.FC<{}> = () => { 21 | return ( 22 | <> 23 |
    24 |
    25 | 32 | 33 | 34 | 35 |
    36 | 37 | ); 38 | }; 39 | 40 | export default Homepage; 41 | -------------------------------------------------------------------------------- /client/components/Login.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useState } from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | import { LoginContext } from '../Context'; 4 | 5 | const Login: React.FC<{}> = () => { 6 | const { email, setEmail, password, setPassword } = useContext(LoginContext)!; 7 | const [validCredentials, setValidCredentials] = useState(true); 8 | const navigate = useNavigate(); 9 | 10 | const handleSubmit = async (e: any) => { 11 | try { 12 | e.preventDefault(); 13 | const data = await fetch('/user/login', { 14 | method: 'POST', 15 | headers: { 'Content-Type': 'application/json' }, 16 | body: JSON.stringify({ email, password }), 17 | }); 18 | 19 | // if the user is authenticated 200 20 | if (data.status === 200) { 21 | setEmail(''); 22 | setPassword(''); 23 | // JOYCE: unsure if i need to keep this both in state and in localStorage 24 | localStorage.setItem('userIn', 'true'); 25 | 26 | navigate('/homepage'); 27 | } 28 | // 401 incorrect pw , 400 middleware error 29 | else if (data.status === 401) { 30 | setPassword(''); 31 | setValidCredentials(false); 32 | } else if (data.status === 400) { 33 | setEmail(''); 34 | setPassword(''); 35 | setValidCredentials(false); 36 | } 37 | } catch (error) { 38 | console.log(`Error in useEffect login ${error}`); 39 | return `Error in useEffect login ${error}`; 40 | } 41 | }; 42 | 43 | //Sign-up Route 44 | const routeToSignUp = (e: any) => { 45 | e.preventDefault(); 46 | navigate('/signup'); 47 | }; 48 | 49 | return ( 50 |
    51 |
    52 |
    53 |
    54 |
    55 |
    56 | 68 |
    69 |
    70 | 81 |
    82 | 83 | 86 | 87 | {!validCredentials && ( 88 |
    Incorrect Password or Email
    89 | )} 90 |
    91 |
    92 | Don't have an Account?{' '} 93 | 96 |
    97 |
    98 | ); 99 | }; 100 | 101 | export default Login; 102 | -------------------------------------------------------------------------------- /client/components/Signup.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useState, useContext, useEffect } from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | import { LoginContext } from '../Context'; 4 | 5 | const Signup: React.FC<{}> = () => { 6 | const { email, setEmail, password, setPassword } = useContext(LoginContext)!; 7 | const [secondPw, setSecondPW] = useState(''); 8 | const [doPwMatch, setPwMatch] = useState(true); 9 | 10 | // TODO: Set up an email already exists function 11 | const [emailExistsError, setEmailExistsError] = useState(false); 12 | 13 | const navigate = useNavigate(); 14 | 15 | //EMAIL CHECK 16 | useEffect(() => { 17 | (async () => { 18 | try { 19 | //if email has a '@' and '.' 20 | const validEmail = new RegExp(/^\S+@\S+\.\S\S+$/); 21 | 22 | if (email.match(validEmail)) { 23 | //Make API req to backend since match has been found to be valid email 24 | setEmailExistsError(false); 25 | const data = await fetch('/user/emailCheck', { 26 | method: 'POST', 27 | headers: { 'Content-Type': 'application/json' }, 28 | body: JSON.stringify({ email }), 29 | }); 30 | const parsedData = await data.json(); 31 | if (parsedData === 'user exists') { 32 | setEmailExistsError(true); 33 | } 34 | } 35 | } catch (error) { 36 | console.log(`Error in useEffect signup.tsx ${error}`); 37 | return `Error in useEffect signup.tsx ${error}`; 38 | } 39 | })(); 40 | }, [email]); 41 | 42 | const handleSubmit = async (e: any) => { 43 | try { 44 | e.preventDefault(); 45 | 46 | if (secondPw === password) { 47 | const data = await fetch('/user/signup', { 48 | method: 'POST', 49 | headers: { 'Content-Type': 'application/json' }, 50 | body: JSON.stringify({ email, password }), 51 | }); 52 | 53 | if (data.status === 200) { 54 | localStorage.setItem('userIn', 'true'); 55 | navigate('/homepage'); 56 | } 57 | // 400 General Error | 409 User already exists | 200 Success 58 | else if (data.status === 404 || data.status === 400) { 59 | setEmail(''); 60 | setPassword(''); 61 | setSecondPW(''); 62 | setPwMatch(true); 63 | } else if (data.status === 409) { 64 | setEmail(''); 65 | setPassword(''); 66 | navigate('/'); 67 | } 68 | } else { 69 | setPassword(''); 70 | setSecondPW(''); 71 | setPwMatch(false); 72 | } 73 | } catch (error) { 74 | console.log(`Error in signup.tsx handleSubmit ${error}`); 75 | return `Error in signup.tsx handleSubmit ${error}`; 76 | } 77 | }; 78 | 79 | // login route 80 | const routeToLogin = (e: React.MouseEvent) => { 81 | e.preventDefault(); 82 | navigate('/'); 83 | }; 84 | 85 | return ( 86 |
    87 |
    88 |
    89 |
    90 |
    91 |
    92 | 104 |
    105 | {emailExistsError && ( 106 |
    Email already in use.
    107 | )} 108 |
    109 | 120 |
    121 |
    122 | 133 |
    134 | 137 | {!doPwMatch && ( 138 |
    Passwords do not match.
    139 | )} 140 |
    141 |
    142 | Already have an account?{' '} 143 | 146 |
    147 |
    148 | ); 149 | }; 150 | 151 | export default Signup; 152 | -------------------------------------------------------------------------------- /client/index.tsx: -------------------------------------------------------------------------------- 1 | import {createRoot} from 'react-dom/client'; 2 | import React from 'react'; 3 | import App from './App'; 4 | import {BrowserRouter} from 'react-router-dom'; 5 | import './styles/login.scss'; 6 | import './styles/InputContainer.scss'; 7 | import './styles/header.scss'; 8 | import './styles/resultbar.scss'; 9 | import './styles/split.scss'; 10 | import './styles/diagram.scss'; 11 | 12 | const rootElement = document.getElementById('root'); 13 | if (!rootElement) throw new Error('Failed to find the root element'); 14 | const root = createRoot(rootElement); 15 | root.render( 16 | 17 | 18 | 19 | ); 20 | -------------------------------------------------------------------------------- /client/styles/InputContainer.scss: -------------------------------------------------------------------------------- 1 | //Tab Styling 2 | //https://www.youtube.com/watch?v=WkREeDy2WQ4 3 | @import url('https://fonts.googleapis.com/css2?family=Lato&display=swap'); 4 | $color1: #020024; 5 | $color2: #2e2c57; 6 | $color3: #004f77; 7 | $color4: #00a3b5; 8 | $color5: #6bfbce; 9 | // input box color 10 | $color6: #d6d9e6; 11 | 12 | body { 13 | font-family: 'Lato', sans-serif; 14 | } 15 | 16 | .input-container { 17 | border: none; 18 | display: flex; 19 | flex-direction: column; 20 | margin-left: 0.8%; 21 | margin-bottom: 0px; 22 | width: 40%; 23 | height: 100%; 24 | background-color: transparent; 25 | overflow-y: scroll; 26 | border-radius: 0 0 5px 5px; 27 | filter: drop-shadow(-2px 1px 20px rgba(0, 163, 181, 0.25)); 28 | } 29 | 30 | // holds the three buttons 31 | .tab-container { 32 | display: flex; 33 | flex-direction: row; 34 | border: none; 35 | justify-content: center; 36 | background-color: transparent; 37 | } 38 | 39 | .tab-content { 40 | background: rgb(0, 79, 119); 41 | background: linear-gradient( 42 | 0deg, 43 | rgba(0, 79, 119, 1) 0%, 44 | rgba(46, 44, 87, 1) 100% 45 | ); 46 | height: 100%; 47 | overflow: auto; 48 | } 49 | 50 | .tabs { 51 | margin: 0; 52 | padding: 0; 53 | text-align: center; 54 | width: 100%; 55 | height: 5vh; 56 | max-height: 40px; 57 | min-height: 15px; 58 | background: rgb(171, 165, 165); 59 | color: white; 60 | border: 1px solid lighten($color2, 10%); 61 | cursor: pointer; 62 | border-radius: 8px 8px 0 0; 63 | box-sizing: content-box; 64 | position: relative; 65 | outline: none; 66 | } 67 | 68 | .active-tabs { 69 | background: $color2; 70 | border-bottom: 1px solid transparent; 71 | } 72 | 73 | .active-tabs::before { 74 | content: ''; 75 | display: block; 76 | position: absolute; 77 | top: -5px; 78 | left: 50%; 79 | transform: translateX(-50%); 80 | } 81 | 82 | button { 83 | border: none; 84 | } 85 | .content-tabs { 86 | flex-grow: 1; 87 | } 88 | 89 | .content { 90 | display: none; 91 | } 92 | 93 | .active-content { 94 | display: block; 95 | height: 100%; 96 | overflow: hidden; 97 | } 98 | 99 | ::-webkit-scrollbar { 100 | width: 10px; 101 | } 102 | 103 | ::-webkit-scrollbar-thumb { 104 | border-radius: 5px; 105 | background-color: #004f77; 106 | } 107 | 108 | ::-webkit-scrollbar-track { 109 | background-color: transparent; 110 | border-radius: 5px; 111 | } 112 | 113 | //QUERY STYLING 114 | .query-main { 115 | display: flex; 116 | flex-direction: column; 117 | align-items: center; 118 | padding-bottom: 40px; 119 | height: calc(100% - 40px); 120 | 121 | .query-main-inner { 122 | flex-grow: 3; 123 | position: relative; 124 | margin: 40px 0 5px; 125 | width: 80%; 126 | // height: 90%; 127 | 128 | .query-input { 129 | resize: none; 130 | background-color: $color6; 131 | outline: none; 132 | box-sizing: border-box; 133 | border: none; 134 | border-radius: 5px; 135 | padding: 12px 20px; 136 | font-family: 'Lato', sans-serif; 137 | // resize: vertical; 138 | overflow: auto; 139 | } 140 | 141 | .query-input:focus { 142 | outline: solid 1px lighten($color1, 15%); 143 | } 144 | } 145 | .error-message-container { 146 | width: 80%; 147 | 148 | .error-message { 149 | background-color: lighten(red, 20%); 150 | border-radius: 5px; 151 | color: white; 152 | font-size: 10px; 153 | font-family: 'Lato', sans-serif; 154 | padding: 5px 5px 5px 5px; 155 | margin-bottom: 3px; 156 | @keyframes fadeInDown { 157 | 0% { 158 | opacity: 0; 159 | transform: translateY(-20px); 160 | } 161 | 100% { 162 | opacity: 1; 163 | transform: translateY(0); 164 | } 165 | } 166 | } 167 | } 168 | } 169 | 170 | .submit-query-button { 171 | position: absolute; 172 | bottom: 0; 173 | right: 5px; 174 | background-color: $color6; 175 | border-radius: 0 0 5px 0; 176 | width: 20%; 177 | height: 30px; 178 | font-size: 12px; 179 | text-align: right; 180 | } 181 | 182 | .clear-button { 183 | position: absolute; 184 | bottom: 0; 185 | left: 5px; 186 | background-color: $color6; 187 | border-radius: 0 0 0 5px; 188 | width: 20%; 189 | height: 30px; 190 | font-size: 12px; 191 | text-align: left; 192 | } 193 | .submit-query-button:hover, 194 | .clear-button:hover { 195 | cursor: pointer; 196 | color: lighten(black, 52%); 197 | } 198 | 199 | textarea { 200 | height: 100%; 201 | width: 100%; 202 | } 203 | 204 | //SETTINGS STYLING 205 | .settings-main { 206 | height: 100%; 207 | overflow: auto; 208 | font-family: 'Lato', sans-serif; 209 | font-size: 11px; 210 | 211 | .uri-main { 212 | display: flex; 213 | flex-direction: column; 214 | align-items: center; 215 | margin: 0 20%; 216 | 217 | form { 218 | width: 100%; 219 | max-width: 250px; 220 | 221 | label { 222 | display: flex; 223 | flex-wrap: nowrap; 224 | width: 100%; 225 | 226 | // .db-submit-settings { 227 | // background-color: $color6; 228 | // // width: 30%; 229 | // height: 36px; 230 | // border-radius: 0 5px 5px 0; 231 | // width: 20%; 232 | // min-width: 60px; 233 | // } 234 | 235 | // .db-submit-settings:hover { 236 | // cursor: pointer; 237 | // color: lighten(black, 52%); 238 | // } 239 | } 240 | } 241 | 242 | .settings-title { 243 | text-align: center; 244 | color: white; 245 | margin: 40px 0 0 0; 246 | } 247 | 248 | form { 249 | display: flex; 250 | flex-direction: column; 251 | align-items: center; 252 | width: 100%; 253 | // max-width: 250px; 254 | 255 | label { 256 | margin-top: 5px; 257 | } 258 | 259 | .settings-input { 260 | width: 100%; 261 | max-width: 234px; 262 | height: 20px; 263 | border: 1px $color1; 264 | background-color: $color6; 265 | border-radius: 5px; 266 | color: black; 267 | padding: 8px; 268 | } 269 | 270 | .settings-input:focus { 271 | outline: solid 1px lighten(rgba(128, 128, 128, 0.443), 15%); 272 | } 273 | 274 | .submit-settings { 275 | margin-top: 10px; 276 | background-color: $color5; 277 | border-radius: 5px; 278 | border-width: 0; 279 | height: 30px; 280 | width: 100px; 281 | font-size: 11px; 282 | color: lighten(black, 5%); 283 | cursor: pointer; 284 | } 285 | 286 | .submit-settings:hover { 287 | background-color: darken($color5, 15%); 288 | cursor: pointer; 289 | } 290 | } 291 | } 292 | 293 | .connection-content { 294 | position: absolute; 295 | z-index: 100; 296 | bottom: 10px; 297 | } 298 | .failed { 299 | background-color: $color2; 300 | border-radius: 5px; 301 | padding: 5px; 302 | } 303 | .success { 304 | background-color: $color5; 305 | color: $color2; 306 | border-radius: 5px; 307 | padding: 5px; 308 | } 309 | } 310 | 311 | // HISTORY 312 | .history-table-container { 313 | height: 100%; 314 | 315 | .history-table { 316 | margin: 40px 10%; 317 | width: 80%; 318 | background-color: rgba(255, 255, 255, 0.45); 319 | border-radius: 5px; 320 | height: calc(100% - 80px); 321 | overflow-x: hidden; 322 | overflow-y: auto; 323 | display: flex; 324 | justify-content: center; 325 | position: relative; 326 | 327 | .query-table { 328 | list-style: none; 329 | display: flex; 330 | flex-direction: column-reverse; 331 | justify-content: flex-end; 332 | position: absolute; 333 | font-size: 10pt; 334 | padding: 3px 10px; 335 | color: black; 336 | width: 100%; 337 | height: calc(100% - 20px); 338 | margin: 0 20px; 339 | 340 | li { 341 | .header-row, 342 | .history-rows { 343 | list-style: none; 344 | display: grid; 345 | grid-template-columns: 80% 20%; 346 | 347 | margin: 0; 348 | padding: 10px; 349 | } 350 | } 351 | 352 | .header-row { 353 | padding: 5px 5px 2px 5px; 354 | border-bottom: 2px solid $color3; 355 | vertical-align: bottom; 356 | } 357 | 358 | .history-rows { 359 | border-bottom: 1px solid $color3; 360 | vertical-align: top; 361 | padding: 0 5px; 362 | } 363 | 364 | .history-query { 365 | text-align: left; 366 | flex-grow: 2; 367 | } 368 | 369 | .history-time { 370 | text-align: right; 371 | } 372 | } 373 | } 374 | 375 | .history-query:hover { 376 | cursor: pointer; 377 | color: #6bfbce; 378 | } 379 | } 380 | -------------------------------------------------------------------------------- /client/styles/diagram.scss: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Lato&display=swap'); 2 | $color1: #020024; 3 | $color2: #004f77; 4 | $color3: #00a3b5; 5 | $color4: #6bfbce; 6 | 7 | .diagram-box { 8 | border: 1px solid $color1; 9 | border: 1px solid lighten($color2, 10%); 10 | display: flex; 11 | width: 100%; 12 | } 13 | 14 | .column-node { 15 | border: 1px solid #1e3d59; 16 | border-radius: 5px; 17 | text-align: center; 18 | display: flex; 19 | align-items: center; 20 | justify-content: center; 21 | height: 25px; 22 | width: 180px; 23 | animation: fade 0.9s ease-in; 24 | } 25 | 26 | .column-container { 27 | width: 180px; 28 | display: grid; 29 | grid-template-columns: 12px auto; 30 | 31 | .column-icon { 32 | padding-left: 3px; 33 | } 34 | 35 | #fkey { 36 | font-size: 12px; 37 | } 38 | 39 | .column-data { 40 | max-width: 155px; 41 | display: flex; 42 | width: 100%; 43 | height: 100%; 44 | align-items: center; 45 | justify-content: space-between; 46 | margin-left: 5px; 47 | margin-right: 5px; 48 | // text-overflow: wrap; 49 | 50 | .column-name { 51 | color: white; 52 | text-align: left; 53 | // width: 100%; 54 | overflow: hidden; 55 | text-overflow: ellipsis; 56 | padding: 0 0 3px 2px; 57 | // white-space: wrap; 58 | } 59 | 60 | .column-name:hover { 61 | overflow: visible; 62 | } 63 | 64 | .column-type { 65 | text-align: right; 66 | font-size: 60%; 67 | white-space: nowrap; 68 | overflow: hidden; 69 | text-overflow: ellipsis; 70 | max-width: 30%; 71 | } 72 | 73 | .column-type:hover { 74 | overflow: visible; 75 | } 76 | } 77 | } 78 | 79 | @keyframes fade { 80 | 0% { 81 | opacity: 0; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /client/styles/header.scss: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Lato&display=swap'); 2 | 3 | $color1: #020024; 4 | $color2: #004f77; 5 | $color3: #00a3b5; 6 | $color4: #6bfbce; 7 | 8 | body { 9 | margin: 0; 10 | padding: 0; 11 | overflow: auto; 12 | } 13 | 14 | .header { 15 | position: fixed; 16 | width: 100%; 17 | height: 50px; 18 | top: 10px; 19 | left: 3%; 20 | background-color: transparent; 21 | 22 | .logo { 23 | width: 250px; 24 | height: 75px; 25 | position: absolute; 26 | margin: -5px 0 0 -1%; 27 | background-image: url(../../assets/logo-main.png); 28 | background-size: contain; 29 | background-repeat: no-repeat; 30 | } 31 | } 32 | 33 | .logout-button { 34 | position: fixed; 35 | top: 2%; 36 | right: 2%; 37 | outline: none; 38 | background-color: transparent; 39 | color: white; 40 | border: none; 41 | cursor: pointer; 42 | } 43 | 44 | .logout-button:hover { 45 | color: darken(white, 25%); 46 | } 47 | 48 | .main-container { 49 | // border: 1px solid lighten($color2, 10%); 50 | border-radius: 5px; 51 | background-color: transparent; 52 | margin: 75px auto 0; 53 | width: 95%; 54 | height: 95%; 55 | } 56 | 57 | .main-container-split { 58 | height: 85vh; 59 | } 60 | -------------------------------------------------------------------------------- /client/styles/login.scss: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+Avestan&family=Oswald&family=Roboto&display=swap'); 2 | $color1: #020024; 3 | $color2: #004f77; 4 | $color3: #00a3b5; 5 | $color4: #6bfbce; 6 | body { 7 | background: rgb(2, 0, 36); 8 | background: linear-gradient( 9 | 90deg, 10 | rgba(2, 0, 36, 1) 0%, 11 | rgba(46, 46, 58, 1) 65%, 12 | rgba(69, 69, 121, 1) 96% 13 | ); 14 | color: white; 15 | } 16 | 17 | .login-container { 18 | margin-top: 200px; 19 | margin-bottom: 20px; 20 | 21 | .logo-container { 22 | display: flex; 23 | flex-direction: column; 24 | justify-content: center; 25 | align-items: center; 26 | margin-bottom: 10px; 27 | 28 | .logo { 29 | width: 250px; 30 | height: 75px; 31 | // position: absolute; 32 | background-image: url(../../assets/logo-main.png); 33 | background-size: contain; 34 | background-repeat: no-repeat; 35 | } 36 | } 37 | 38 | .loginForm { 39 | display: flex; 40 | flex-direction: column; 41 | gap: 20px; 42 | justify-content: center; 43 | align-items: center; 44 | } 45 | 46 | .login-text { 47 | display: flex; 48 | flex-direction: column; 49 | font-size: 12px; 50 | margin-bottom: 5px; 51 | } 52 | 53 | .formLine { 54 | display: flex; 55 | flex-direction: column; 56 | align-items: center; 57 | } 58 | 59 | .user-input { 60 | width: 150px; 61 | height: 30px; 62 | border: 1px $color1; 63 | background-color: rgba(128, 128, 128, 0.443); 64 | border-radius: 5px; 65 | color: white; 66 | padding: 8px; 67 | } 68 | 69 | .user-input:focus { 70 | outline: solid 1px lighten(rgba(128, 128, 128, 0.443), 15%); 71 | } 72 | .login-footer { 73 | display: flex; 74 | flex-direction: column; 75 | margin-top: 10px; 76 | margin-bottom: 0; 77 | font-size: 10px; 78 | gap: 10px; 79 | align-items: center; 80 | font-family: 'Noto Sans Avestan', sans-serif; 81 | } 82 | 83 | .submit { 84 | background-color: lighten(white, 10%); 85 | border-radius: 5px; 86 | border-width: 0; 87 | height: 30px; 88 | width: 100px; 89 | font-size: 11px; 90 | color: lighten(black, 5%); 91 | cursor: pointer; 92 | } 93 | 94 | .submit:hover { 95 | background-color: darken(white, 5%); 96 | } 97 | 98 | .small-submit { 99 | background-color: transparent; 100 | color: white; 101 | font-size: 11px; 102 | border: none; 103 | cursor: pointer; 104 | text-decoration: underline; 105 | margin-top: 0; 106 | } 107 | 108 | .small-submit:hover { 109 | color: darken(white, 25%); 110 | } 111 | 112 | .small-text { 113 | font-size: 11px; 114 | color: red; 115 | font-family: 'Noto Sans Avestan', sans-serif; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /client/styles/resultbar.scss: -------------------------------------------------------------------------------- 1 | $color1: #020024; 2 | $color2: #004f77; 3 | $color3: #00a3b5; 4 | $color4: #6bfbce; 5 | 6 | .query-table-outer-container { 7 | margin: 0.8%; 8 | padding: 10px; 9 | // background: rgb(0, 79, 119); 10 | background: linear-gradient( 11 | 0deg, 12 | rgba(0, 79, 119, 1) 0%, 13 | rgba(46, 44, 87, 1) 100% 14 | ); 15 | border-radius: 5px; 16 | width: 98%; 17 | // table-layout: fixed; 18 | display: flex; 19 | flex-direction: column; 20 | filter: drop-shadow(-2px 1px 20px rgba(0, 163, 181, 0.25)); 21 | 22 | h2 { 23 | margin: 0; 24 | font-size: 18px; 25 | } 26 | 27 | .query-table-container { 28 | margin: 0.8%; 29 | background: rgba(255, 255, 255, 0.375); 30 | border-radius: 5px; 31 | // width: 98%; 32 | overflow: auto; 33 | height: 98%; 34 | } 35 | 36 | // .query-table-container:hover { 37 | // overflow: auto; 38 | // } 39 | 40 | .query-table { 41 | font-size: 10pt; 42 | padding: 3px 10px; 43 | color: black; 44 | width: auto; 45 | // max-width: 100%; 46 | height: 100%; 47 | table-layout: auto; 48 | 49 | th { 50 | // background-color: $color2; 51 | padding: 5px 5px 2px 5px; 52 | border-bottom: 2px solid $color2; 53 | vertical-align: bottom; 54 | } 55 | 56 | td { 57 | border-bottom: 1px solid $color2; 58 | vertical-align: top; 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /client/styles/split.scss: -------------------------------------------------------------------------------- 1 | .flex { 2 | display: flex; 3 | } 4 | 5 | .gutter { 6 | // background-color: #eee; 7 | background-repeat: no-repeat; 8 | background-position: 50%; 9 | } 10 | 11 | .gutter.gutter-horizontal { 12 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAeCAYAAADkftS9AAAAIklEQVQoU2M4c+bMfxAGAgYYmwGrIIiDjrELjpo5aiZeMwF+yNnOs5KSvgAAAABJRU5ErkJggg=='); 13 | cursor: col-resize; 14 | } 15 | 16 | .gutter.gutter-vertical { 17 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAFAQMAAABo7865AAAABlBMVEVHcEzMzMzyAv2sAAAAAXRSTlMAQObYZgAAABBJREFUeF5jOAMEEAIEEFwAn3kMwcB6I2AAAAAASUVORK5CYII='); 18 | cursor: row-resize; 19 | } 20 | 21 | .flex2 { 22 | display: flex; 23 | flex-direction: column; 24 | } 25 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | smartER 8 | 9 | 10 | 11 | 12 |
    13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'jsdom', 3 | }; 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "query-visualizer", 3 | "version": "1.0.0", 4 | "description": "visualize your sql queries as you type", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "concurrently \"NODE_ENV=development webpack serve --hot --open\" \"npx nodemon ./server/server.ts & redis-server\"", 8 | "front-end": "NODE_ENV=development webpack serve --hot --open", 9 | "server": "npx nodemon ./server/server.ts & redis-server", 10 | "build": "NODE_ENV=production webpack", 11 | "test": "NODE_ENV=test jest" 12 | }, 13 | "author": "", 14 | "license": "ISC", 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/oslabs-beta/smartER.git" 18 | }, 19 | "contributors": [ 20 | "Joyce Kwak", 21 | "Melissa McLaughlin", 22 | "Nathan Ngo", 23 | "Brian Vu" 24 | ], 25 | "devDependencies": { 26 | "@babel/core": "^7.21.3", 27 | "@babel/preset-env": "^7.21.4", 28 | "@babel/preset-react": "^7.18.6", 29 | "@babel/preset-typescript": "^7.21.0", 30 | "@types/bcrypt": "^5.0.0", 31 | "@types/express": "^4.17.17", 32 | "@types/jest": "^29.5.0", 33 | "@types/jsonwebtoken": "^9.0.1", 34 | "@types/lodash": "^4.14.192", 35 | "@types/morgan": "^1.9.4", 36 | "@types/node": "^18.15.5", 37 | "@types/pg": "^8.6.6", 38 | "@types/react": "^18.0.28", 39 | "@types/react-dom": "^18.0.11", 40 | "babel-jest": "^29.5.0", 41 | "babel-loader": "^9.1.2", 42 | "concurrently": "^7.6.0", 43 | "cookie-parser": "^1.4.6", 44 | "cross-env": "^7.0.3", 45 | "css-loader": "^6.7.3", 46 | "express": "^4.18.2", 47 | "html-webpack-plugin": "^5.5.0", 48 | "jest": "^29.5.0", 49 | "jsonwebtoken": "^9.0.0", 50 | "nodemon": "^2.0.21", 51 | "path": "^0.12.7", 52 | "react-test-renderer": "^18.2.0", 53 | "sass-loader": "^13.2.1", 54 | "style-loader": "^3.3.2", 55 | "ts-loader": "^9.4.2", 56 | "ts-node": "^10.9.1", 57 | "typescript": "^5.0.2", 58 | "webpack": "^5.76.2", 59 | "webpack-cli": "^5.0.1", 60 | "webpack-dev-server": "^4.13.1" 61 | }, 62 | "dependencies": { 63 | "@fortawesome/free-regular-svg-icons": "^6.4.0", 64 | "@fortawesome/free-solid-svg-icons": "^6.4.0", 65 | "@fortawesome/react-fontawesome": "^0.2.0", 66 | "@reactflow/controls": "^11.1.10", 67 | "@reacticons/ionicons": "^7.1.0", 68 | "@testing-library/jest-dom": "^5.16.5", 69 | "@testing-library/react": "^14.0.0", 70 | "@testing-library/user-event": "^14.4.3", 71 | "@types/cookie-parser": "^1.4.3", 72 | "@types/cryptr": "^4.0.1", 73 | "@types/jest": "^29.5.0", 74 | "@types/lodash": "^4.14.192", 75 | "@types/react-router": "^5.1.20", 76 | "@types/supertest": "^2.0.12", 77 | "@typescript-eslint/eslint-plugin": "^5.56.0", 78 | "bcrypt": "^5.1.0", 79 | "cryptr": "^6.2.0", 80 | "dotenv": "^16.0.3", 81 | "elkjs": "^0.8.2", 82 | "express-validator": "^6.15.0", 83 | "jest": "^29.5.0", 84 | "jest-environment-jsdom": "^29.5.0", 85 | "lodash": "^4.17.21", 86 | "lodash.debounce": "^4.0.8", 87 | "morgan": "^1.10.0", 88 | "pg": "^8.10.0", 89 | "pgsql-ast-parser": "^11.0.1", 90 | "react": "^18.2.0", 91 | "react-dom": "^18.2.0", 92 | "react-router": "^6.9.0", 93 | "react-router-dom": "^6.9.0", 94 | "react-split": "^2.0.14", 95 | "react-syntax-highlighter": "^15.5.0", 96 | "reactflow": "^11.6.1", 97 | "redis": "^4.6.5", 98 | "redis-server": "^1.2.2", 99 | "sass": "^1.59.3", 100 | "supertest": "^6.3.3", 101 | "ts-jest": "^29.0.5", 102 | "web-worker": "^1.2.0" 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /server/app.ts: -------------------------------------------------------------------------------- 1 | import express, { Request, Response, NextFunction } from 'express'; 2 | import morgan from 'morgan'; 3 | import cookieParser from 'cookie-parser'; 4 | import apiRouter from './routes/router'; 5 | import userController from './controllers/userController'; 6 | import { body, validationResult } from 'express-validator'; 7 | import cookieController from './controllers/cookieController'; 8 | 9 | const app = express(); 10 | 11 | //General middleware 12 | app.use(morgan('dev')); 13 | app.use(cookieParser()); 14 | app.use(express.json()); 15 | app.use(express.urlencoded({ extended: true })); 16 | 17 | // post request to check if user input email is unique 18 | app.post( 19 | '/user/emailCheck', 20 | body('email').isEmail().normalizeEmail(), 21 | (req, res, next) => { 22 | const errors = validationResult(req); 23 | if (!errors.isEmpty()) 24 | return next({ 25 | log: 'error: invalid email address', 26 | status: 400, 27 | message: { err: 'invalid email address' }, 28 | }); 29 | else return next(); 30 | }, 31 | userController.checkForEmail, 32 | (req, res, next) => { 33 | if (res.locals.userExists) { 34 | res.status(200).json('user exists'); 35 | } else res.status(200).json('unique email'); 36 | } 37 | ); 38 | 39 | // post request to add new user to db 40 | app.post( 41 | '/user/signup', 42 | body('email').isEmail().normalizeEmail(), 43 | body('password').not().isEmpty(), 44 | (req, res, next) => { 45 | const errors = validationResult(req); 46 | if (errors && !errors.isEmpty()) 47 | return res.status(400).json({ error: errors.array() }); 48 | else return next(); 49 | }, 50 | userController.checkForEmail, 51 | userController.createUser, 52 | cookieController.setJwtCookie, 53 | (req, res) => { 54 | return res.status(200).send(); 55 | } 56 | ); 57 | 58 | app.post( 59 | '/user/login', 60 | body('email').isEmail().normalizeEmail(), 61 | body('password').not().isEmpty(), 62 | userController.verifyUser, 63 | cookieController.setJwtCookie, 64 | cookieController.setDbCookie, 65 | (req, res) => { 66 | return res.status(200).send(); 67 | } 68 | ); 69 | 70 | app.post( 71 | '/user/changePassword', 72 | body('email').isEmail().normalizeEmail(), 73 | body('password').not().isEmpty(), 74 | body('newPassword').not().isEmpty(), 75 | userController.verifyUser, 76 | userController.changePassword, 77 | (req, res) => { 78 | return res.status(200).send(); 79 | } 80 | ); 81 | 82 | app.post( 83 | '/user/logout', 84 | userController.authenticateToken, 85 | userController.blacklistToken, 86 | (req, res) => { 87 | return res.status(200).send(); 88 | } 89 | ); 90 | 91 | app.get('/user/authenticate', userController.authenticateToken, (req, res) => { 92 | return res.status(200).send(); 93 | }); 94 | 95 | // API Route 96 | app.use('/api', userController.authenticateToken, apiRouter); 97 | 98 | // Catch all 4048 99 | app.use('/', (req: Request, res: Response) => { 100 | res.status(404).json(`This is not the page you are looking for ¯\\_(ツ)_/¯`); 101 | }); 102 | // Encrypt 2way, read only access, credentials stored on VS Code local Storage 103 | 104 | // Global error handler 105 | app.use((err: any, req: Request, res: Response, next: NextFunction) => { 106 | const defaultErr = { 107 | log: 'Express error handler caught unknown middleware error', 108 | status: 500, 109 | message: { err: 'An error occurred' }, 110 | }; 111 | console.log(err.log); 112 | console.log(err.message); 113 | const errorObj = Object.assign({}, defaultErr, err); 114 | return res.status(errorObj.status).json(errorObj.message); 115 | }); 116 | 117 | export default app; 118 | -------------------------------------------------------------------------------- /server/controllers/cookieController.ts: -------------------------------------------------------------------------------- 1 | import jwt from 'jsonwebtoken'; 2 | import { RequestHandler } from 'express'; 3 | import db from '../models/userModel'; 4 | import dotenv from 'dotenv'; 5 | dotenv.config(); 6 | 7 | interface cookieControllers { 8 | setJwtCookie: RequestHandler; 9 | setDbCookie: RequestHandler; 10 | } 11 | 12 | const cookieController: cookieControllers = { 13 | setJwtCookie: async (req, res, next) => { 14 | try { 15 | const createJwt = (email: string) => { 16 | const secret = process.env.JWT_SECRET_KEY; 17 | if (secret) { 18 | const token = jwt.sign( 19 | { 20 | email: email, 21 | }, 22 | secret, 23 | { 24 | expiresIn: '7d', 25 | } 26 | ); 27 | return token; 28 | } 29 | }; 30 | 31 | const { email } = req.body; 32 | 33 | const jwtToken = createJwt(email); 34 | 35 | res.locals.JWT = jwtToken; 36 | res.cookie('JWT', jwtToken, { 37 | httpOnly: true, 38 | secure: true, 39 | }); 40 | // res.header('Authorization', jwtToken); 41 | return next(); 42 | } catch (error) { 43 | return next({ 44 | log: 'error running cookieController.setJwtCookie middleware', 45 | status: 400, 46 | message: { err: error }, 47 | }); 48 | } 49 | }, 50 | 51 | setDbCookie: async (req, res, next) => { 52 | try { 53 | if (res.locals.dbId) { 54 | // if uri was just saved, pull dbId from res.locals 55 | res.cookie('dbId', res.locals.dbId, { 56 | httpOnly: true, 57 | secure: true, 58 | }); 59 | return next(); 60 | } else if (res.locals.user_id) { 61 | // if user just signed up or logged in, get user id and use query to find most recent URI for that user 62 | // STRETCH: allow user to select from list of saved URIs instead of always pulling the last one 63 | const sql = await db.query(` 64 | SELECT _id FROM databases 65 | WHERE user_id = ${res.locals.user_id} 66 | ORDER BY _id desc 67 | ;`); 68 | 69 | let dbId = 0; 70 | if (sql.rowCount) dbId = sql.rows[0]._id; 71 | res.cookie('dbId', dbId, { 72 | httpOnly: true, 73 | secure: true, 74 | }); 75 | 76 | res.locals.dbId = dbId; 77 | return next(); 78 | } else throw new Error('user not set'); 79 | } catch (error) { 80 | return next({ 81 | log: 'error running cookieController.setDbCookie middleware', 82 | status: 400, 83 | message: { err: error }, 84 | }); 85 | } 86 | }, 87 | }; 88 | 89 | export default cookieController; 90 | -------------------------------------------------------------------------------- /server/controllers/dbController.ts: -------------------------------------------------------------------------------- 1 | import { RequestHandler } from 'express'; 2 | import db from '../models/userModel'; 3 | import dotenv from 'dotenv'; 4 | import {} from 'pg'; 5 | import Cryptr from 'cryptr'; 6 | dotenv.config(); 7 | 8 | interface dbControllers { 9 | saveURI: RequestHandler; 10 | getHistory: RequestHandler; 11 | postHistory: RequestHandler; 12 | } 13 | 14 | const dbController: dbControllers = { 15 | saveURI: async (req, res, next) => { 16 | try { 17 | if (req.user) { 18 | const { id } = req.user; 19 | const { encodedURI } = req.body; 20 | 21 | const cryptr = new Cryptr(process.env.URI_SECRET_KEY || 'test', { 22 | pbkdf2Iterations: 10000, 23 | saltLength: 10, 24 | }); 25 | 26 | const encryptedUri = cryptr.encrypt(encodedURI); 27 | const postUri = await db.query(` 28 | INSERT INTO databases (user_id, uri) 29 | VALUES (${id}, '${encryptedUri}') 30 | RETURNING _id 31 | ;`); 32 | 33 | res.locals.dbId = postUri.rows[0]._id; 34 | return next(); 35 | } else throw new Error('user not set'); 36 | } catch (error) { 37 | return next({ 38 | log: `Error in dbController.saveURI ${error}`, 39 | status: 400, 40 | message: { error }, 41 | }); 42 | } 43 | }, 44 | 45 | getHistory: async (req, res, next) => { 46 | try { 47 | const { dbId } = req.cookies; 48 | if (dbId) { 49 | const history = await db.query(` 50 | SELECT 51 | h.created_at, h.query 52 | FROM history h 53 | JOIN databases d on d._id = h.database_id 54 | WHERE d._id = ${dbId} 55 | ;`); 56 | console.log('history', history); 57 | res.locals.queryHistory = history.rows; 58 | return next(); 59 | } else throw new Error('db not found'); 60 | } catch (error) { 61 | return next({ 62 | log: `Error in dbController.getHistory ${error}`, 63 | status: 400, 64 | message: { error }, 65 | }); 66 | } 67 | }, 68 | 69 | postHistory: async (req, res, next) => { 70 | try { 71 | const { dbId } = req.cookies; 72 | if (req.cookies.dbId) { 73 | const { created_at, queryString } = req.body; 74 | const dateInt = Math.floor(parseInt(created_at) / 1000); 75 | const saveHistory = await db.query( 76 | `INSERT INTO history (database_id, created_at, query) 77 | VALUES (${dbId}, current_timestamp, $1) 78 | RETURNING created_at 79 | ;`, 80 | [queryString] 81 | ); 82 | res.locals.timestamp = saveHistory.rows[0].created_at; 83 | return next(); 84 | } else throw new Error('db not found'); 85 | } catch (error) { 86 | return next({ 87 | log: `Error in dbController.postHistory ${error}`, 88 | status: 400, 89 | message: { error }, 90 | }); 91 | } 92 | }, 93 | }; 94 | 95 | export default dbController; 96 | -------------------------------------------------------------------------------- /server/controllers/schemaController.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction, RequestHandler } from 'express'; 2 | import { Pool } from 'pg'; 3 | import dotenv from 'dotenv'; 4 | import db from '../models/userModel'; 5 | import { table } from 'console'; 6 | import Cryptr from 'cryptr'; 7 | dotenv.config(); 8 | 9 | interface schemaControllers { 10 | connectDb: RequestHandler; 11 | getSchemaPostgreSQL: RequestHandler; 12 | getQueryResults: RequestHandler; 13 | } 14 | // 15 | const schemaController: schemaControllers = { 16 | connectDb: async (req, res, next) => { 17 | try { 18 | console.log('running connectDb'); 19 | let dbId; 20 | if (res.locals.dbId) dbId = res.locals.dbId; 21 | if (!dbId) dbId = req.cookies.dbId; 22 | 23 | if (!dbId) throw new Error('no db cookie'); 24 | 25 | const dbResult = await db.query(` 26 | SELECT uri FROM databases 27 | WHERE _id = ${dbId} 28 | ;`); 29 | 30 | const cryptr = new Cryptr(process.env.URI_SECRET_KEY || 'test', { 31 | pbkdf2Iterations: 10000, 32 | saltLength: 10, 33 | }); 34 | 35 | const decryptedUri = cryptr.decrypt(dbResult.rows[0].uri); 36 | const pg_uri = decodeURIComponent(decryptedUri); 37 | 38 | var envCredentials: any = { connectionString: pg_uri }; 39 | res.locals.pg = new Pool(envCredentials); 40 | return next(); 41 | } catch (error) { 42 | return next({ 43 | log: `Error in schemaController.connectDb ${error}`, 44 | status: 400, 45 | message: { error }, 46 | }); 47 | } 48 | }, 49 | getSchemaPostgreSQL: async (req, res, next) => { 50 | try { 51 | const pg = res.locals.pg; 52 | // Get all relationships between all tables 53 | // Identify the current schema name for use in full schema query 54 | const currentSchemaSQL = await pg.query( 55 | `SELECT current_schema FROM current_schema` 56 | ); 57 | 58 | const currentSchema = currentSchemaSQL.rows[0].current_schema; 59 | 60 | const constraintArr: Record[] = []; 61 | // Get Relationships, Tables names, Column names, Data types 62 | const query = `SELECT * FROM ( 63 | SELECT DISTINCT ON (c.table_name, c.column_name) 64 | c.table_name, 65 | c.column_name, 66 | c.data_type, 67 | c. ordinal_position, 68 | max(case when tc.constraint_type = 'PRIMARY KEY' then 1 else 0 end) OVER(PARTITION BY c.table_name, c.column_name) AS is_primary_key, 69 | cc.table_name as table_origin, 70 | cc.column_name as table_column 71 | 72 | FROM information_schema.key_column_usage kc 73 | 74 | INNER JOIN information_schema.table_constraints tc 75 | ON kc.table_name = tc.table_name AND kc.table_schema = tc.table_schema AND kc.constraint_name = tc.constraint_name 76 | 77 | LEFT JOIN information_schema.constraint_column_usage cc 78 | ON cc.constraint_name = kc.constraint_name AND tc.constraint_type = 'FOREIGN KEY' 79 | 80 | RIGHT JOIN information_schema.columns c 81 | ON c.table_name = kc.table_name AND c.column_name = kc.column_name 82 | 83 | WHERE c.table_schema = '${currentSchema}' AND is_updatable = 'YES' 84 | 85 | ORDER BY c.table_name, c.column_name, is_primary_key desc, table_origin) subquery 86 | 87 | ORDER BY table_name, ordinal_position;`; 88 | const schema = await pg.query(query); 89 | 90 | // Initialize object to hold returned data 91 | let erDiagram: Record = {}; 92 | let tableObj: Record = {}; 93 | // Make custom type for any on tableObj 94 | 95 | // Iterate through array of all table names, columns, and data types 96 | for (let i = 0; i < schema.rows.length; i++) { 97 | let nextTableName; 98 | if (schema.rows[i + 1]) nextTableName = schema.rows[i + 1].table_name; 99 | // current represents each object in the array 100 | const current = schema.rows[i]; 101 | //column object type and declaration 102 | 103 | // Assign table name and column name 104 | tableObj[current.column_name] = {}; 105 | tableObj[current.column_name].table_name = current.table_name; 106 | tableObj[current.column_name].column_name = current.column_name; 107 | 108 | // Assign data type 109 | if (current.data_type === 'integer') 110 | tableObj[current.column_name].data_type = 'int'; 111 | else if (current.data_type === 'character varying') 112 | tableObj[current.column_name].data_type = 'varchar'; 113 | else tableObj[current.column_name].data_type = current.data_type; 114 | 115 | // Add relationships and constraints if there are any 116 | if (current.is_primary_key) { 117 | tableObj[current.column_name].primary_key = true; 118 | tableObj[current.column_name].foreign_tables = []; 119 | } 120 | 121 | // table_origin is only given when column is a foreign key 122 | if (current.table_origin) { 123 | const constraintObj: Record = {}; 124 | constraintObj[`${[current.table_origin]}.${current.table_column}`] = 125 | current.table_name; 126 | tableObj[current.column_name].foreign_key = true; 127 | tableObj[current.column_name].linkedTable = current.table_origin; 128 | tableObj[current.column_name].linkedTableColumn = 129 | current.table_column; 130 | 131 | constraintArr.push({ ...constraintObj }); 132 | } 133 | 134 | // if table name at next row is a different table, 135 | // push a deep copy of the tableObj to final ER diagram and reset tableObj 136 | if (!nextTableName || nextTableName !== current.table_name) { 137 | erDiagram[current.table_name] = { ...tableObj }; 138 | tableObj = {}; 139 | } 140 | } 141 | 142 | for (const constraint of constraintArr) { 143 | for (const relationship in constraint) { 144 | const string = relationship.split('.'); // [species, _id] 145 | const tableName = string[0]; // species 146 | const columnName = string[1]; // _id 147 | const tableOrigin = constraint[relationship]; // people 148 | erDiagram[tableName][columnName].foreign_tables.push(tableOrigin); 149 | } 150 | } 151 | 152 | res.locals.erDiagram = erDiagram; 153 | return next(); 154 | } catch (error) { 155 | return next({ 156 | log: `Error in schemaController.getSchemaPostgreSQL ${error}`, 157 | status: 400, 158 | message: { error }, 159 | }); 160 | } 161 | }, 162 | getQueryResults: async (req, res, next) => { 163 | try { 164 | const { queryString } = req.body; 165 | const pg = res.locals.pg; 166 | // Make a query based on the passed in queryString 167 | const getQuery = await pg.query(queryString); 168 | 169 | // Return query to FE 170 | const results = getQuery.rows; 171 | res.locals.queryResults = results; 172 | return next(); 173 | } catch (error) { 174 | return next({ 175 | log: `Error in schemaController.getQueryResults ${error}`, 176 | status: 400, 177 | message: { error }, 178 | }); 179 | } 180 | }, 181 | }; 182 | 183 | export default schemaController; 184 | -------------------------------------------------------------------------------- /server/controllers/userController.ts: -------------------------------------------------------------------------------- 1 | import db from '../models/userModel'; 2 | import { RequestHandler } from 'express'; 3 | import jwt from 'jsonwebtoken'; 4 | import { redisClient } from '../server'; 5 | import bcrypt from 'bcrypt'; 6 | const SALTROUNDS = 5; 7 | import dotenv from 'dotenv'; 8 | dotenv.config(); 9 | 10 | interface userControllers { 11 | checkForEmail: RequestHandler; 12 | createUser: RequestHandler; 13 | verifyUser: RequestHandler; 14 | changePassword: RequestHandler; 15 | authenticateToken: RequestHandler; 16 | blacklistToken: RequestHandler; 17 | } 18 | 19 | const comparePassword = async (password: string, hashedPassword: string) => { 20 | return await bcrypt.compare(password, hashedPassword); 21 | }; 22 | 23 | const userController: userControllers = { 24 | // confirm whether user exists based on email passed in 25 | checkForEmail: async (req, res, next) => { 26 | try { 27 | const emailLookup = await db.query( 28 | `SELECT _id FROM users WHERE email = '${req.body.email}'` 29 | ); 30 | res.locals.userExists = Boolean(emailLookup.rowCount); 31 | return next(); 32 | } catch (error) { 33 | return next({ 34 | log: 'error running userController.checkForEmail middleware', 35 | status: 400, 36 | message: { err: error }, 37 | }); 38 | } 39 | }, 40 | 41 | // create user based on email and password passed in 42 | createUser: async (req, res, next) => { 43 | try { 44 | if (res.locals.userExists) { 45 | return next({ 46 | log: 'error: email already exists', 47 | status: 409, 48 | message: { err: 'account with this email already exists' }, 49 | }); 50 | } 51 | 52 | const { email, password } = req.body; 53 | const hashedPassword = await bcrypt.hash(password, SALTROUNDS); 54 | const newUser = await db.query(` 55 | INSERT into users (email, password) 56 | VALUES ('${email}', '${hashedPassword}') 57 | RETURNING _id 58 | ;`); 59 | 60 | res.locals.user_id = newUser.rows[0]._id; 61 | return next(); 62 | } catch (error) { 63 | return next({ 64 | log: 'error running userController.createUser middleware', 65 | status: 400, 66 | message: { err: error }, 67 | }); 68 | } 69 | }, 70 | 71 | // verify user by email/password combination 72 | verifyUser: async (req, res, next) => { 73 | try { 74 | const { email, password } = req.body; 75 | const pwLookup = await db.query( 76 | `SELECT _id, password FROM users WHERE email = '${email}'` 77 | ); 78 | 79 | res.locals.user_id = pwLookup.rows[0]._id; 80 | 81 | const hashedPassword = pwLookup.rows[0].password; 82 | const isValidPw = await comparePassword(password, hashedPassword); 83 | if (!isValidPw) { 84 | return next({ 85 | log: 'error: incorrect password', 86 | status: 401, 87 | message: { err: 'email or password is incorrect' }, 88 | }); 89 | } else return next(); 90 | } catch (error) { 91 | return next({ 92 | log: 'error running userController.verifyUser middleware', 93 | status: 400, 94 | message: { err: error }, 95 | }); 96 | } 97 | }, 98 | 99 | // verify user by email/password combination 100 | changePassword: async (req, res, next) => { 101 | try { 102 | const { email, newPassword } = req.body; 103 | const hashedPassword = await bcrypt.hash(newPassword, SALTROUNDS); 104 | await db.query( 105 | `UPDATE users SET password = '${hashedPassword}' WHERE email = '${email}'` 106 | ); 107 | return next(); 108 | } catch (error) { 109 | return next({ 110 | log: 'error running userController.verifyUser middleware', 111 | status: 400, 112 | message: { err: error }, 113 | }); 114 | } 115 | }, 116 | 117 | // protect API routes by validating JWT 118 | authenticateToken: async (req, res, next) => { 119 | try { 120 | // console.log('running authenticateToken'); 121 | const token: string | undefined = req.cookies.JWT; 122 | if (token) console.log('validating token'); 123 | 124 | // reject request if no token provided 125 | if (!token) { 126 | return next({ 127 | log: 'no token provided', 128 | status: 401, 129 | message: { err: 'no token provided' }, 130 | }); 131 | } 132 | 133 | // reject request if token is in deny list (user logged out) 134 | const inDenyList = await redisClient.get(`bl_${token}`); 135 | if (inDenyList) { 136 | return next({ 137 | log: 'JWT rejected', 138 | status: 401, 139 | message: { err: 'JWT rejected' }, 140 | }); 141 | } 142 | 143 | // reject request if token is invalid 144 | // console.log('confirming token is valid'); 145 | const secret = process.env.JWT_SECRET_KEY; 146 | if (token && secret) { 147 | jwt.verify(token, secret, (error, payload) => { 148 | if (error) { 149 | return next({ 150 | log: 'JWT invalid', 151 | status: 401, 152 | message: { err: error }, 153 | }); 154 | } 155 | 156 | // declare types of properties on payload 157 | const decodedToken = payload as { email: string; exp: number }; 158 | const { email, exp } = decodedToken; 159 | 160 | // confirm payload contains correct properties and they are expected type before adding to request 161 | if ( 162 | email && 163 | typeof email === 'string' && 164 | exp && 165 | typeof exp === 'number' 166 | ) { 167 | db.query(`SELECT _id FROM users WHERE email = '${email}'`).then( 168 | (result: any) => { 169 | const userId = result.rows[0]._id; 170 | 171 | req.user = { 172 | email: email, 173 | id: userId, 174 | token: token, 175 | exp: exp, 176 | }; 177 | 178 | return next(); 179 | } 180 | ); 181 | } else 182 | return next({ 183 | log: 'JWT invalid', 184 | status: 401, 185 | message: { err: error }, 186 | }); 187 | }); 188 | } 189 | } catch (error) { 190 | return next({ 191 | log: 'error running userController.authenticateToken middleware', 192 | status: 400, 193 | message: { err: error }, 194 | }); 195 | } 196 | }, 197 | 198 | blacklistToken: async (req, res, next) => { 199 | try { 200 | const { user } = req; 201 | 202 | if (user && user.token && user.exp) { 203 | const { token, exp } = user; 204 | console.log('blacklisting token'); 205 | const token_key = `bl_${token}`; 206 | await redisClient.set(token_key, token); 207 | redisClient.expireAt(token_key, exp); 208 | } 209 | 210 | return next(); 211 | } catch (error) { 212 | return next({ 213 | log: 'error running userController.blacklistToken middleware', 214 | status: 400, 215 | message: { err: error }, 216 | }); 217 | } 218 | }, 219 | }; 220 | 221 | export default userController; 222 | -------------------------------------------------------------------------------- /server/models/userModel.ts: -------------------------------------------------------------------------------- 1 | const { Pool } = require('pg'); 2 | import dotenv from 'dotenv'; 3 | dotenv.config(); 4 | const PG_URI = process.env.DATABASE_API; 5 | 6 | // create a new pool here using the connection string above 7 | const pool = new Pool({ 8 | connectionString: PG_URI, 9 | }); 10 | 11 | // export an object that contains a property called query, 12 | // which is a function that returns the invocation of pool.query() after logging the query 13 | // This will be required in the controllers to be the access point to the database 14 | 15 | export default { 16 | query: (text: string, arr?: string[]) => { 17 | console.log('executed query', text); 18 | return pool.query(text, arr); 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /server/routes/router.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import schemaController from '../controllers/schemaController'; 3 | import userController from '../controllers/userController'; 4 | import dbController from '../controllers/dbController'; 5 | import cookieController from '../controllers/cookieController'; 6 | const router = express.Router(); 7 | 8 | // Possibly add route for storing users previous login credentials or URIs? 9 | router.post( 10 | '/getQueryResults', 11 | schemaController.connectDb, 12 | schemaController.getQueryResults, 13 | (req, res) => { 14 | res.status(200).json(res.locals.queryResults); 15 | } 16 | ); 17 | 18 | router.post('/postHistory', dbController.postHistory, (req, res) => { 19 | res.status(200).json(res.locals.timestamp); 20 | }); 21 | 22 | router.get( 23 | '/getSchema', 24 | schemaController.connectDb, 25 | schemaController.getSchemaPostgreSQL, 26 | (req, res) => { 27 | res.status(200).json(res.locals.erDiagram); 28 | } 29 | ); 30 | 31 | router.get('/getHistory', dbController.getHistory, (req, res) => { 32 | res.status(200).json(res.locals.queryHistory); 33 | }); 34 | 35 | router.post( 36 | '/addURI', 37 | dbController.saveURI, 38 | cookieController.setDbCookie, 39 | schemaController.connectDb, 40 | schemaController.getSchemaPostgreSQL, 41 | (req, res) => { 42 | res.status(200).json(res.locals.erDiagram); 43 | } 44 | ); 45 | 46 | router.delete('/', (req, res, next) => {}); 47 | 48 | export default router; 49 | -------------------------------------------------------------------------------- /server/server.ts: -------------------------------------------------------------------------------- 1 | import app from './app'; 2 | import dotenv from 'dotenv'; 3 | import { createClient, RedisClientType } from 'redis'; 4 | dotenv.config(); 5 | 6 | const PORT = process.env.PORT || 9001; 7 | 8 | app.listen(PORT, () => { 9 | console.log(`⚡️Express:${PORT} ⚡️`); 10 | }); 11 | 12 | // connect redis for use in logout functionality 13 | let redisClient: RedisClientType; 14 | (async () => { 15 | redisClient = createClient(); 16 | 17 | redisClient.on('error', (error) => { 18 | // ECONNREFUSED error will be thrown if redis is not installed. brew install redis to resolve 19 | console.log(`Error connecting redis: ${error}`); 20 | }); 21 | 22 | await redisClient.connect(); 23 | })(); 24 | 25 | export { redisClient }; 26 | -------------------------------------------------------------------------------- /src/static/diagram-rearranged.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/smartER/f25708190ffbf068e24ab1df60936ce417f5d03b/src/static/diagram-rearranged.png -------------------------------------------------------------------------------- /src/static/express.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/smartER/f25708190ffbf068e24ab1df60936ce417f5d03b/src/static/express.png -------------------------------------------------------------------------------- /src/static/history-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/smartER/f25708190ffbf068e24ab1df60936ce417f5d03b/src/static/history-2.png -------------------------------------------------------------------------------- /src/static/history-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/smartER/f25708190ffbf068e24ab1df60936ce417f5d03b/src/static/history-3.png -------------------------------------------------------------------------------- /src/static/history.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/smartER/f25708190ffbf068e24ab1df60936ce417f5d03b/src/static/history.png -------------------------------------------------------------------------------- /src/static/icons8-react-native-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/smartER/f25708190ffbf068e24ab1df60936ce417f5d03b/src/static/icons8-react-native-48.png -------------------------------------------------------------------------------- /src/static/login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/smartER/f25708190ffbf068e24ab1df60936ce417f5d03b/src/static/login.png -------------------------------------------------------------------------------- /src/static/postgresql.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/smartER/f25708190ffbf068e24ab1df60936ce417f5d03b/src/static/postgresql.png -------------------------------------------------------------------------------- /src/static/query-people-species.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/smartER/f25708190ffbf068e24ab1df60936ce417f5d03b/src/static/query-people-species.png -------------------------------------------------------------------------------- /src/static/react-flow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/static/redis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/smartER/f25708190ffbf068e24ab1df60936ce417f5d03b/src/static/redis.png -------------------------------------------------------------------------------- /src/static/results.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/smartER/f25708190ffbf068e24ab1df60936ce417f5d03b/src/static/results.png -------------------------------------------------------------------------------- /src/static/sass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/smartER/f25708190ffbf068e24ab1df60936ce417f5d03b/src/static/sass.png -------------------------------------------------------------------------------- /src/static/settings-credentials.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/smartER/f25708190ffbf068e24ab1df60936ce417f5d03b/src/static/settings-credentials.png -------------------------------------------------------------------------------- /src/static/settings-uri.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/smartER/f25708190ffbf068e24ab1df60936ce417f5d03b/src/static/settings-uri.png -------------------------------------------------------------------------------- /src/static/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/smartER/f25708190ffbf068e24ab1df60936ce417f5d03b/src/static/settings.png -------------------------------------------------------------------------------- /src/static/smarter-logo-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/smartER/f25708190ffbf068e24ab1df60936ce417f5d03b/src/static/smarter-logo-padded.png -------------------------------------------------------------------------------- /src/static/typescript.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/smartER/f25708190ffbf068e24ab1df60936ce417f5d03b/src/static/typescript.png -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "noImplicitAny": true, 5 | "lib": ["DOM", "ESNEXT"], 6 | "target": "ES6", 7 | "jsx": "react-jsx", 8 | "allowJs": true, 9 | "moduleResolution": "node", 10 | "strict": true, 11 | "allowSyntheticDefaultImports": true, 12 | "esModuleInterop": true, 13 | "typeRoots": ["./types", "node_modules/@types"] 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /types/custom.ts: -------------------------------------------------------------------------------- 1 | export type User = { 2 | email: string; 3 | id: number; 4 | token: string | null; 5 | exp: number; 6 | }; 7 | -------------------------------------------------------------------------------- /types/express/index.d.ts: -------------------------------------------------------------------------------- 1 | /* 2 | TypeScript uses the .d.ts declaration files to load type information about a library written in JavaScript. 3 | Here, the index.d.ts global module will be used by TypeScript to extend the Express Request type globally through 4 | declaration merging. According to the Express source code, this is the officially endorsed way to extend the Request type. 5 | */ 6 | 7 | import { User } from '../custom'; 8 | 9 | export {}; 10 | 11 | declare global { 12 | namespace Express { 13 | export interface Request { 14 | user?: User; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /webpack.config.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import HtmlWebpackPlugin from 'html-webpack-plugin'; 3 | 4 | export default { 5 | entry: ['./client/index.tsx'], 6 | output: { 7 | path: path.resolve(__dirname, 'dist'), 8 | publicPath: '/', 9 | filename: 'bundle.js', 10 | }, 11 | mode: process.env.NODE_ENV ? process.env.NODE_ENV : 'development', 12 | devServer: { 13 | static: { 14 | directory: path.resolve(__dirname, 'dist'), 15 | publicPath: '/', 16 | }, 17 | port: 8080, 18 | historyApiFallback: true, 19 | headers: { 'Access-Control-Allow-Origin': '*' }, 20 | proxy: { 21 | // '/api/**': 'http://localhost:9001/', 22 | '/api': 'http://localhost:9001/', 23 | '/user': 'http://localhost:9001/', 24 | }, 25 | }, 26 | module: { 27 | rules: [ 28 | { 29 | test: /\.(ts|js)x?$/, 30 | exclude: /node_modules/, 31 | include: [path.resolve(__dirname, 'client')], 32 | use: 'babel-loader', 33 | }, 34 | { 35 | test: /.(scss|sass|css)$/, 36 | exclude: /node_modules\/(?!@?reactflow).*/, 37 | use: ['style-loader', 'css-loader', 'sass-loader'], 38 | }, 39 | ], 40 | }, 41 | plugins: [new HtmlWebpackPlugin({ template: './index.html' })], 42 | resolve: { 43 | extensions: ['.ts', '.tsx', '.js', '.jsx'], 44 | }, 45 | }; 46 | --------------------------------------------------------------------------------