├── .dockerignore ├── .gitignore ├── CHANGELOG.md ├── Dockerfile ├── Procfile ├── README.md ├── client ├── .babelrc ├── __mocks__ │ └── fileMock.js ├── jest.setup.js ├── package.json ├── public │ ├── index.html │ └── smallLogo.png ├── src │ ├── App.jsx │ ├── assets │ │ ├── gif │ │ │ ├── editNodes.gif │ │ │ └── generateTypedefsAndResolver.gif │ │ ├── logos │ │ │ ├── addGraph.png │ │ │ ├── githubLogo.png │ │ │ ├── hero-img.png │ │ │ ├── linkedin.png │ │ │ └── smallLogo.png │ │ ├── styles │ │ │ ├── defaultstyles.scss │ │ │ ├── globalstyles.scss │ │ │ └── variables.scss │ │ └── team-pics │ │ │ ├── Brian.png │ │ │ ├── Dan.png │ │ │ ├── Erick.png │ │ │ ├── Jonathan.png │ │ │ └── Mingzhu.png │ ├── components │ │ ├── About │ │ │ ├── About.jsx │ │ │ ├── About.test.js │ │ │ └── about.scss │ │ ├── AuthorizedNavbar │ │ │ ├── AuthorizedNavbar.jsx │ │ │ └── authorizednavbar.scss │ │ ├── Dashboard │ │ │ ├── Dashboard.jsx │ │ │ ├── Dashboard.test.js │ │ │ └── dashboard.scss │ │ ├── DashboardGrid │ │ │ ├── DashboardGrid.jsx │ │ │ └── dashboardgrid.scss │ │ ├── GenerateTabs │ │ │ ├── genTab.jsx │ │ │ ├── genTab.test.js │ │ │ └── gentab.scss │ │ ├── Graph │ │ │ ├── Graph.jsx │ │ │ ├── Graph.test.js │ │ │ └── graph.scss │ │ ├── GraphCard │ │ │ ├── GraphCard.jsx │ │ │ ├── GraphCard.test.js │ │ │ └── graphcard.scss │ │ ├── Login │ │ │ ├── Login.jsx │ │ │ ├── Login.test.js │ │ │ └── login.scss │ │ ├── Main │ │ │ ├── Main.jsx │ │ │ ├── Main.test.js │ │ │ └── main.scss │ │ ├── ModalGraphName │ │ │ └── ModalGraphName.jsx │ │ ├── Navbar │ │ │ ├── Navbar.jsx │ │ │ └── navbar.scss │ │ ├── NodeSchema │ │ │ ├── AddNodeDialog.jsx │ │ │ ├── NodeList.jsx │ │ │ ├── SchemaVisualizer.jsx │ │ │ ├── nodelist.scss │ │ │ └── schemavisualizer.scss │ │ ├── Signup │ │ │ ├── Signup.jsx │ │ │ ├── Signup.test.js │ │ │ └── signup.scss │ │ ├── Team │ │ │ ├── Team.jsx │ │ │ └── team.scss │ │ ├── UploadSqlSchema │ │ │ ├── UploadSqlSchema.jsx │ │ │ ├── UploadSqlSchema.test.js │ │ │ └── uploadsqlschema.scss │ │ └── algorithms │ │ │ ├── resolver_generator.js │ │ │ ├── schema_generator.js │ │ │ └── schema_parser.js │ ├── contexts │ │ ├── AuthContext.js │ │ ├── GraphContext.jsx │ │ └── ThemeContext.js │ ├── index.js │ └── index.scss └── webpack.config.js ├── package.json └── server ├── __tests__ └── authRouter.test.js ├── controllers ├── graphController.js └── userController.js ├── models ├── User.js └── userModels.js ├── package.json ├── routes ├── authRouter.js └── graphRouter.js └── server.js /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | build 4 | .git 5 | .DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Node.js dependencies 2 | /node_modules 3 | /client/node_modules 4 | /server/node_modules 5 | 6 | # Dependency lock files 7 | /package-lock.json 8 | /client/package-lock.json 9 | /server/package-lock.json 10 | 11 | # Environment files 12 | /server/.env 13 | 14 | # macOS Finder metadata 15 | .DS_Store 16 | 17 | # Build directories (if you use build tools that output to specific directories) 18 | client/build 19 | server/build 20 | 21 | # Logs 22 | *.log 23 | 24 | # Miscellaneous 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to the MoleQLar project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | - Increase test coverage 11 | - OAuth integration with Github / Google 12 | - Media Queries for authorized pages 13 | - More descriptive error handling in file generation 14 | - Create refresh tokens 15 | - Store images of saved graphs on dashboard 16 | - Additional SQL database types (RDS, MySQL) 17 | - MongoDB/DynamoDB integration 18 | 19 | ## [1.0.0] - 2024-07-24 20 | 21 | ### Added 22 | - Initial release of MoleQLar 23 | - User authentication (signup, login, logout) 24 | - Database schema upload and visualization 25 | - Interactive node graph editing 26 | - Automatic generation of GraphQL typedefs and resolvers 27 | - Project saving and retrieval 28 | - Light/Dark mode settings 29 | - PostgreSQL to GraphQL conversion 30 | - SQL file parser for generating GraphQL Object Types and fields 31 | - Node-based graph for visualizing GraphQL types and relationships 32 | 33 | ### Changed 34 | - N/A for initial release 35 | 36 | ### Deprecated 37 | - N/A for initial release 38 | 39 | ### Removed 40 | - N/A for initial release 41 | 42 | ### Fixed 43 | - N/A for initial release 44 | 45 | ### Security 46 | - Implemented secure user authentication using JWT 47 | 48 | [Unreleased]: https://github.com/oslabs-beta/moleQLar/compare/v1.0.0...HEAD 49 | [1.0.0]: https://github.com/oslabs-beta/moleQLar/releases/tag/v1.0.0 50 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build stage for the client 2 | FROM node:14-alpine AS build-stage 3 | WORKDIR /app/client 4 | COPY client/package*.json ./ 5 | RUN npm install 6 | COPY client ./ 7 | RUN npm run build 8 | 9 | # Production stage 10 | FROM node:14-alpine 11 | WORKDIR /app 12 | 13 | # Set up server 14 | COPY server/package*.json ./server/ 15 | RUN npm install --prefix ./server 16 | COPY server ./server 17 | 18 | # Copy client build to server's static files directory 19 | COPY --from=build-stage /app/client/build ./server/build 20 | 21 | # Expose the ports 22 | EXPOSE 3000 23 | 24 | # Start the server 25 | CMD ["node", "server/server.js"] -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: npm run start 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MoleQLar 2 | 3 | ## Description 4 | 5 | MoleQLar is a full-stack application that automatically generates GraphQL typedefs and resolver definitions for PostgreSQL databases. It provides a visual interface for uploading database schemas and modifying GraphQL types. 6 | 7 | ## Problem 8 | 9 | Database schema analysis and GraphQL implementation currently face several challenges: 10 | 11 | 1. Database Complexity: Analyzing and understanding database structures can be complex and time-consuming. 12 | 2. Repetitive Tasks: Implementing GraphQL boilerplate code is often tedious and repetitive, whether working with an existing database or starting from scratch. 13 | 3. Lack of Visualization: It's difficult to visualize schema structures and the relationships between data, which can hinder understanding and development. 14 | 15 | ## Solution 16 | 17 | MoleQLar addresses these challenges through the following features: 18 | 19 | 1. Node-Based GUI: Provides an intuitive interface for interacting with database schemas. 20 | 2. PostgreSQL to GraphQL Conversion: Automatically generates an interactive node-based graph from PostgreSQL database schema files. 21 | 3. SQL File Parser: Implements an algorithm to generate GraphQL Object Types and fields from SQL files. 22 | 4. Node-Based Graph: Visualizes GraphQL types and relationships, making it easier to understand complex schemas. 23 | 5. Authentication: Securely authenticates users and authorizes their access to database schemas and GraphQL query schemas. 24 | 6. Automatic Generation: Creates GraphQL resolvers and typeDefs based on the visualized schema. 25 | 26 | These solutions aim to simplify the process of working with database schemas and implementing GraphQL, making development more efficient and less error-prone. 27 | 28 | ## Features 29 | 30 | - User authentication (signup, login, logout) 31 | - Database schema upload and visualization 32 | - Interactive node graph editing 33 | - Automatic generation of GraphQL typedefs and resolvers 34 | - Project saving and retrieval 35 | - Light/Dark mode settings 36 | 37 | ## Technologies Used 38 | 39 | - Frontend: React, React Flow Renderer, MUI, SCSS 40 | - Backend: Node.js, Express 41 | - Database: PostgreSQL (with plans to migrate to MongoDB) 42 | - Authentication: JWT 43 | - Testing: Jest, React Testing Library 44 | - Other: GraphQL, Docker, AWS Services, Webpack, React Router 45 | 46 | ## Usage 47 | 48 | 1. **Login/Sign Up** 49 | 50 | - Visit the MoleQLar website 51 | - If you're a new user, click "Sign Up" to create a new account 52 | - If you already have an account, log in 53 | 54 | 2. **Upload Database Schema** 55 | 56 | - After logging in, click the "Upload Schema" button 57 | - Select your PostgreSQL database schema SQL file 58 | - The system will automatically parse your schema and generate a visual node graph 59 | 60 | 3. **View and Edit Node Graph** 61 | 62 | - In the generated node graph, you can see your database tables and their relationships 63 | - Click on any node to view detailed information 64 | - Use the editing tools to modify node names or fields 65 | 66 | ![Edit Nodes Demo](./client/src/assets/gif/editNodes.gif) 67 | 68 | 4. **Generate GraphQL TypeDefs and Resolvers** 69 | 70 | - After editing the node graph, click the "Generate GraphQL" button 71 | - The system will automatically generate corresponding GraphQL TypeDefs and Resolvers 72 | - You can copy the generated code or download it as a file 73 | 74 | ![Generate TypeDefs and Resolvers Demo](./client/src/assets/gif/generateTypedefsAndResolver.gif) 75 | 76 | 5. **Save Project** 77 | 78 | - Name your project 79 | - Click the "Save Project" button to save your node graph and generated GraphQL code 80 | - You can return and continue editing your saved project at any time 81 | 82 | 6. **View Saved Projects** 83 | - On the main page, you can see all your saved projects 84 | - Click on any project name to reopen and continue working on it 85 | 86 | Note: Ensure your database schema is correctly formatted for the best visualization and code generation results. If you encounter any issues during use, please refer to our documentation or contact our support team. 87 | 88 | # Generating Database Schema SQL File 89 | 90 | Before uploading your database schema to MoleQLar, you may need to generate a SQL file containing your schema. Here's how to do it using PostgreSQL: 91 | 92 | ## Prerequisites 93 | 94 | Ensure you have PostgreSQL client tools installed on your system. 95 | 96 | ## Steps 97 | 98 | 1. Open a terminal and run the following command: 99 | 100 | ```sh 101 | pg_dump --username=your_username --host=your_host --port=your_port --dbname=your_database --schema-only --file=your_schema.sql 102 | ``` 103 | 104 | 2. Replace the placeholders with your actual database connection details: 105 | 106 | - `your_username`: Your database username 107 | - `your_host`: The host address of your database 108 | - `your_port`: The port number (default is usually 5432) 109 | - `your_database`: The name of your database 110 | - `your_schema.sql`: The desired name for your output file 111 | 112 | 3. When prompted, enter your database password. 113 | 114 | After the command completes, you should have a file named your_schema.sql in your current directory containing the database schema. 115 | 116 | This SQL file can then be used with MoleQLar as described in the Usage section above. 117 | 118 | > **Note:** Be careful not to share or commit your actual database credentials. It's recommended to use environment variables or a secure method to manage database connections in your application. 119 | 120 | ## Contributing 121 | 122 | We welcome contributions to MoleQLar! If you'd like to contribute, please follow these steps: 123 | 124 | 1. Fork the repository on GitHub. 125 | 2. Clone your forked repository to your local machine. 126 | 3. Create a new branch for your feature or bug fix. 127 | 4. Make your changes and commit them with a clear, descriptive commit message. 128 | 5. Push your changes to your fork on GitHub. 129 | 6. Submit a pull request to the main MoleQLar repository. 130 | 131 | ### Guidelines 132 | 133 | - Before starting work on a major feature or change, please open an issue to discuss it with the maintainers. 134 | - Write clear, commented code and follow the existing code style. 135 | - Include tests for new features or bug fixes. 136 | - Update the documentation as necessary. 137 | - Ensure your code lints without errors. 138 | 139 | ### Setting Up the Development Environment 140 | 141 | 1. Ensure you have Node.js and npm installed. 142 | 2. Navigate to the client folder and install dependencies: 143 | `cd client` 144 | `npm install` 145 | 3. Start the server in the client folder: `npm start` 146 | 4. Navigate to the server folder and install dependencies: 147 | `cd ../server` 148 | `npm install` 149 | 5. Start the server in the server folder: `npm start` 150 | 151 | ### Running Tests 152 | 153 | Run the test suite with: `npm test` 154 | 155 | ### Reporting Bugs 156 | 157 | If you find a bug, please open an issue on GitHub with a clear description of the problem and steps to reproduce it. 158 | 159 | We appreciate your contributions to making MoleQLar better! 160 | 161 | ## Contact 162 | 163 | For any questions or feedback about MoleQLar, please feel free to reach out to our team: 164 | 165 | - Brian Yang: jibriyang91@gmail.com 166 | - Dan Hudgens: danw.hudgens@gmail.com 167 | - Erick Alvarez: erick505alvarez@gmail.com 168 | - Jonathan Ghebrial: jony@ghebrial.com 169 | - Mingzhu Wan: mingzhuwan@gmail.com 170 | 171 | You can also find more information about our project on our [GitHub repository](https://github.com/oslabs-beta/moleQLar). 172 | -------------------------------------------------------------------------------- /client/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"] 3 | } 4 | -------------------------------------------------------------------------------- /client/__mocks__/fileMock.js: -------------------------------------------------------------------------------- 1 | module.exports = 'test-file-stub'; 2 | -------------------------------------------------------------------------------- /client/jest.setup.js: -------------------------------------------------------------------------------- 1 | // jest.setup.js 2 | require('jest-fetch-mock').enableMocks(); 3 | 4 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "test": "cross-env NODE_ENV=test jest --runInBand --detectOpenHandles --forceExit --verbose", 7 | "start": "webpack-dev-server --mode production --open", 8 | "dev": "npx webpack-dev-server --mode development --open --hot", 9 | "build": "npx webpack --mode production" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "description": "", 15 | "dependencies": { 16 | "@emotion/react": "^11.11.4", 17 | "@emotion/styled": "^11.11.5", 18 | "@mui/icons-material": "^5.16.0", 19 | "@mui/material": "^5.15.21", 20 | "@testing-library/jest-dom": "^6.4.6", 21 | "@testing-library/react": "^16.0.0", 22 | "@testing-library/user-event": "^14.5.2", 23 | "axios": "^1.7.2", 24 | "cross-env": "^7.0.3", 25 | "dotenv-webpack": "^8.1.0", 26 | "dropzone": "^6.0.0-beta.2", 27 | "jest-environment-jsdom": "^29.7.0", 28 | "pluralize": "^8.0.0", 29 | "react": "^18.3.1", 30 | "react-dom": "^18.3.1", 31 | "react-dropzone": "^14.2.3", 32 | "react-icons": "^5.2.1", 33 | "react-router-dom": "^6.24.0", 34 | "reactflow": "^11.11.4" 35 | }, 36 | "devDependencies": { 37 | "@babel/core": "^7.24.7", 38 | "@babel/preset-env": "^7.24.7", 39 | "@babel/preset-react": "^7.24.7", 40 | "babel-jest": "^29.7.0", 41 | "babel-loader": "^9.1.3", 42 | "css-loader": "^7.1.2", 43 | "file-loader": "^6.2.0", 44 | "html-webpack-plugin": "^5.6.0", 45 | "jest": "^29.7.0", 46 | "jest-css-modules-transform": "^4.4.2", 47 | "jest-fetch-mock": "^3.0.3", 48 | "raw-loader": "^4.0.2", 49 | "sass": "^1.77.6", 50 | "sass-loader": "^14.2.1", 51 | "style-loader": "^4.0.0", 52 | "webpack": "^5.92.1", 53 | "webpack-bundle-analyzer": "^4.10.2", 54 | "webpack-cli": "^5.1.4", 55 | "webpack-dev-server": "^5.0.4" 56 | }, 57 | "jest": { 58 | "testEnvironment": "jsdom", 59 | "moduleNameMapper": { 60 | "\\.(jpg|jpeg|png|gif|svg|scss)$": "/__mocks__/fileMock.js" 61 | }, 62 | "transformIgnorePatterns": [ 63 | "/node_modules/", 64 | "\\.(jpg|jpeg|png|gif|svg|scss)$" 65 | ], 66 | "transform": { 67 | "^.+\\.jsx?$": "babel-jest", 68 | "\\.(css|scss)$": "/node_modules/jest-css-modules-transform" 69 | }, 70 | "setupFiles": [ 71 | "./jest.setup.js" 72 | ] 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | moleQLar 9 | 10 | 11 |
12 | 13 | -------------------------------------------------------------------------------- /client/public/smallLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/moleQLar/56326ec9a182ea2e847605f5019f40d5ea4257e1/client/public/smallLogo.png -------------------------------------------------------------------------------- /client/src/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { Suspense, lazy } from "react"; 2 | import { BrowserRouter, Route, Routes, Outlet, Navigate } from 'react-router-dom'; 3 | import { AuthProvider, useAuth } from "./contexts/AuthContext.js"; 4 | import { ThemeProvider } from "./contexts/ThemeContext.js"; 5 | import "./index.scss" 6 | 7 | const Main = lazy(() => import("./components/Main/Main")); 8 | const Signup = lazy(() => import("./components/Signup/Signup")); 9 | const Login = lazy(() => import("./components/Login/Login")); 10 | const Team = lazy(() => import('./components/Team/Team')); 11 | const About = lazy(() => import('./components/About/About')); 12 | const Dashboard = lazy(() => import('./components/Dashboard/Dashboard')); 13 | const Graph = lazy(() => import('./components/Graph/Graph')); 14 | import { GraphProvider } from './contexts/GraphContext'; 15 | 16 | const Loading = () =>
Loading...
; 17 | 18 | // A wrapper for that redirects to the login page if the user is not authenticated. 19 | const PrivateRoutes = () => { 20 | const { authState, setAuthState } = useAuth(); 21 | 22 | if (authState.loading) { 23 | return
Loading...
; 24 | } 25 | if (!authState.isAuth) { 26 | return ; 27 | } 28 | // if logged im 29 | return ( 30 | 31 | 32 | 33 | ) 34 | }; 35 | 36 | const AppRoutes = () => ( 37 | }> 38 | 39 | } /> 40 | } /> 41 | } /> 42 | } /> 43 | } /> 44 | }> 45 | } /> 46 | } /> 47 | 48 | 49 | 50 | ); 51 | 52 | const App = () => { 53 | return ( 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | ); 62 | }; 63 | 64 | export default App; -------------------------------------------------------------------------------- /client/src/assets/gif/editNodes.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/moleQLar/56326ec9a182ea2e847605f5019f40d5ea4257e1/client/src/assets/gif/editNodes.gif -------------------------------------------------------------------------------- /client/src/assets/gif/generateTypedefsAndResolver.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/moleQLar/56326ec9a182ea2e847605f5019f40d5ea4257e1/client/src/assets/gif/generateTypedefsAndResolver.gif -------------------------------------------------------------------------------- /client/src/assets/logos/addGraph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/moleQLar/56326ec9a182ea2e847605f5019f40d5ea4257e1/client/src/assets/logos/addGraph.png -------------------------------------------------------------------------------- /client/src/assets/logos/githubLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/moleQLar/56326ec9a182ea2e847605f5019f40d5ea4257e1/client/src/assets/logos/githubLogo.png -------------------------------------------------------------------------------- /client/src/assets/logos/hero-img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/moleQLar/56326ec9a182ea2e847605f5019f40d5ea4257e1/client/src/assets/logos/hero-img.png -------------------------------------------------------------------------------- /client/src/assets/logos/linkedin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/moleQLar/56326ec9a182ea2e847605f5019f40d5ea4257e1/client/src/assets/logos/linkedin.png -------------------------------------------------------------------------------- /client/src/assets/logos/smallLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/moleQLar/56326ec9a182ea2e847605f5019f40d5ea4257e1/client/src/assets/logos/smallLogo.png -------------------------------------------------------------------------------- /client/src/assets/styles/defaultstyles.scss: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | html { 6 | font-size: 16px; 7 | width: 100%; 8 | min-height: 100%; 9 | display: flex; 10 | flex-direction: column; 11 | } 12 | 13 | body { 14 | width: 100%; 15 | height: 100%; 16 | display: flex; 17 | flex-direction: column; 18 | flex-grow: 1; 19 | margin: 0; 20 | padding: 0; 21 | font-family: "Outfit", sans-serif; 22 | background-color: $color-white; 23 | color: $color-black; 24 | } 25 | 26 | h1 { 27 | margin: 0; 28 | } 29 | 30 | h1, 31 | h2, 32 | h3, 33 | h4, 34 | h5, 35 | h6 { 36 | font-family: "Outfit"; 37 | } 38 | 39 | p { 40 | font-family: "Nunito", serif; 41 | } 42 | 43 | ul { 44 | padding: 0; 45 | margin: 0; 46 | } 47 | 48 | a { 49 | text-decoration: none; 50 | } 51 | 52 | #root { 53 | min-width: 700px; 54 | width: 100%; 55 | height: 100%; 56 | padding: 0; 57 | margin: 0; 58 | display: flex; 59 | flex-direction: column; 60 | justify-content: flex-start; 61 | flex-grow: 1; 62 | // border: 10px solid magenta; 63 | // overflow-y: hidden; 64 | } 65 | -------------------------------------------------------------------------------- /client/src/assets/styles/globalstyles.scss: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Nunito:ital,wght@0,200..1000;1,200..1000&family=Outfit:wght@100..900&display=swap'); 2 | 3 | @import "variables"; 4 | @import 'defaultstyles'; 5 | 6 | -------------------------------------------------------------------------------- /client/src/assets/styles/variables.scss: -------------------------------------------------------------------------------- 1 | $color-primary: #c978fb; 2 | $color-secondary: #64268a; 3 | $color-tertiary: #31afd4; 4 | $color-quaternary: #093758; 5 | $color-black: #190624; 6 | $color-white: #fcfcfc; 7 | $color-black-text: #756a7c; 8 | -------------------------------------------------------------------------------- /client/src/assets/team-pics/Brian.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/moleQLar/56326ec9a182ea2e847605f5019f40d5ea4257e1/client/src/assets/team-pics/Brian.png -------------------------------------------------------------------------------- /client/src/assets/team-pics/Dan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/moleQLar/56326ec9a182ea2e847605f5019f40d5ea4257e1/client/src/assets/team-pics/Dan.png -------------------------------------------------------------------------------- /client/src/assets/team-pics/Erick.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/moleQLar/56326ec9a182ea2e847605f5019f40d5ea4257e1/client/src/assets/team-pics/Erick.png -------------------------------------------------------------------------------- /client/src/assets/team-pics/Jonathan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/moleQLar/56326ec9a182ea2e847605f5019f40d5ea4257e1/client/src/assets/team-pics/Jonathan.png -------------------------------------------------------------------------------- /client/src/assets/team-pics/Mingzhu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/moleQLar/56326ec9a182ea2e847605f5019f40d5ea4257e1/client/src/assets/team-pics/Mingzhu.png -------------------------------------------------------------------------------- /client/src/components/About/About.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Navbar from '../Navbar/Navbar'; 3 | import './about.scss'; 4 | // JSX to define About page 5 | const About = () => { 6 | return ( 7 | <> 8 | 9 |
10 |

We’re changing the way people think about GraphQL

11 |

12 | moleQLar is a technology solution founded on the premise that interacting with databases should be intuitive, efficient, and hassle-free. We aim to empower developers by providing tools that simplify the process of creating GraphQL APIs, allowing them to focus on building amazing applications. 13 |

14 |

15 | Our mission is to streamline the process of creating GraphQL layers over PostgreSQL databases, offering a seamless experience that saves time and resources. With moleQLar, developers can generate robust and flexible GraphQL APIs in seconds, facilitating smoother data management and integration. 16 |

17 |

18 | We believe in fostering innovation and accessibility in the tech industry, and moleQLar is designed to make advanced data interaction accessible to developers of all skill levels. Join us in transforming the way we interact with data, and experience the future of GraphQL with moleQLar. 19 |

20 |
21 |

moleQLar is a software solution focused on improving developer experience with GraphQL and PostgreSQL. Join us in revolutionizing data interaction.

22 |
23 |
24 | 25 | ); 26 | }; 27 | 28 | export default About; -------------------------------------------------------------------------------- /client/src/components/About/About.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen, waitFor, fireEvent } from '@testing-library/react'; 3 | import '@testing-library/jest-dom'; 4 | import About from './About.jsx'; 5 | import { BrowserRouter } from 'react-router-dom'; 6 | import { MemoryRouter, Routes, Route } from 'react-router-dom'; 7 | import Team from '../Team/Team.jsx'; 8 | import Main from '../Main/Main.jsx'; 9 | 10 | describe('About page', () => { 11 | //reset mocks before each test 12 | beforeEach(() => { 13 | fetch.resetMocks(); 14 | }); 15 | 16 | test('About component is properly rendered to page', () => { 17 | render( 18 | 19 | 20 | 21 | ); 22 | 23 | //select components on about page to test for correct rendering 24 | const mainHeader = screen.getByText(/think about GraphQL/i); 25 | const homeLinkNav = screen.getByRole('link', { name: /Home/i }); 26 | const homeLinkIcon = screen.getByAltText(/Small Logo/i); 27 | const teamLink = screen.getByRole('link', { name: /Team/i }); 28 | const githubLink = screen.getByAltText(/GitHub/i); 29 | 30 | // Verify selected fields are rendering properly 31 | expect(mainHeader).toBeInTheDocument(); 32 | expect(homeLinkNav).toBeInTheDocument(); 33 | expect(homeLinkIcon).toBeInTheDocument(); 34 | expect(teamLink).toBeInTheDocument(); 35 | expect(githubLink).toBeInTheDocument(); 36 | }); 37 | 38 | describe('Navigation tests for About page', () => { 39 | //reset memory router before each navigation test 40 | beforeEach(() => { 41 | render( 42 | 43 | 44 | } /> 45 | } /> 46 | } /> 47 | 48 | 49 | ); 50 | }); 51 | test('Successfully navigates to Team route on click', () => { 52 | const teamLink = screen.getByRole('link', { name: /Team/i }); 53 | fireEvent.click(teamLink); 54 | expect(screen.getByText(/Meet the Team/i)).toBeInTheDocument(); 55 | }); 56 | test('Successfully navigates to Home/Main route using navbar link', () => { 57 | const homeLinkNav = screen.getByRole('link', { name: /Home/i }); 58 | fireEvent.click(homeLinkNav); 59 | expect( 60 | screen.getByText(/Implementation in seconds/i) 61 | ).toBeInTheDocument(); 62 | }); 63 | test('Successfully navigates to Home/Main route using icon link', () => { 64 | const homeLinkIcon = screen.getByAltText(/Small Logo/i); 65 | fireEvent.click(homeLinkIcon); 66 | expect( 67 | screen.getByText(/Implementation in seconds/i) 68 | ).toBeInTheDocument(); 69 | }); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /client/src/components/About/about.scss: -------------------------------------------------------------------------------- 1 | @import '../../assets/styles/variables.scss'; 2 | 3 | .about-section { 4 | text-align: center; 5 | padding: 50px 20px; 6 | color: black; 7 | } 8 | 9 | .about-section h2 { 10 | font-size: 36px; 11 | margin-bottom: 20px; 12 | font-weight: bold; 13 | } 14 | 15 | .about-section p { 16 | font-size: 18px; 17 | margin: 20px 0; 18 | line-height: 1.6; 19 | max-width: 800px; 20 | margin-left: auto; 21 | margin-right: auto; 22 | } 23 | 24 | .about-footer { 25 | background-color: rgb(172, 48, 172); 26 | padding: 20px 0; 27 | margin-top: 40px; 28 | } 29 | 30 | .about-footer p { 31 | font-size: 17px; 32 | color: white; 33 | } 34 | 35 | // 响应式设计 36 | @media (max-width: 1400px) { 37 | .about-section h2 { 38 | font-size: 32px; 39 | } 40 | 41 | .about-section p { 42 | font-size: 17px; 43 | max-width: 750px; 44 | } 45 | } 46 | 47 | @media (max-width: 1300px) { 48 | .about-section { 49 | padding: 40px 15px; 50 | } 51 | 52 | .about-section h2 { 53 | font-size: 30px; 54 | } 55 | 56 | .about-section p { 57 | font-size: 16px; 58 | max-width: 700px; 59 | } 60 | } 61 | 62 | @media (max-width: 1000px) { 63 | .about-section h2 { 64 | font-size: 28px; 65 | } 66 | 67 | .about-section p { 68 | font-size: 15px; 69 | max-width: 600px; 70 | } 71 | 72 | .about-footer p { 73 | font-size: 15px; 74 | } 75 | } 76 | 77 | @media (max-width: 800px) { 78 | .about-section { 79 | padding: 30px 10px; 80 | } 81 | 82 | .about-section h2 { 83 | font-size: 24px; 84 | margin-bottom: 15px; 85 | } 86 | 87 | .about-section p { 88 | font-size: 14px; 89 | margin: 15px 0; 90 | max-width: 100%; 91 | } 92 | 93 | .about-footer { 94 | padding: 15px 0; 95 | margin-top: 30px; 96 | } 97 | 98 | .about-footer p { 99 | font-size: 13px; 100 | } 101 | } -------------------------------------------------------------------------------- /client/src/components/AuthorizedNavbar/AuthorizedNavbar.jsx: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import React, { useContext } from 'react'; 3 | import { NavLink, useNavigate } from 'react-router-dom'; 4 | import smallLogo from '../../assets/logos/smallLogo.png'; 5 | import { useAuth } from '../../contexts/AuthContext.js'; 6 | import { useTheme } from '../../contexts/ThemeContext'; 7 | import { FaSun, FaMoon } from 'react-icons/fa'; 8 | import './authorizednavbar.scss'; 9 | 10 | // Authorized Navbar Defined 11 | const AuthorizedNavbar = () => { 12 | const navigate = useNavigate(); 13 | const { authState, setAuthState } = useAuth(); 14 | // Handle Authorized State and to Handle Logout 15 | const handleLogOut = () => { 16 | setAuthState({ 17 | isAuth: false, 18 | username: "", 19 | user_id: null, 20 | }); 21 | localStorage.removeItem("username"); 22 | localStorage.removeItem("user_id"); 23 | localStorage.removeItem("token"); 24 | return navigate('/'); 25 | } 26 | 27 | const {darkMode, toggleDarkMode} = useTheme() 28 | // JSX to define our Authorized Navbar 29 | return ( 30 | 53 | ); 54 | }; 55 | 56 | export default AuthorizedNavbar; -------------------------------------------------------------------------------- /client/src/components/AuthorizedNavbar/authorizednavbar.scss: -------------------------------------------------------------------------------- 1 | @import "../../assets/styles/variables.scss"; 2 | @import "../Navbar/navbar.scss"; 3 | 4 | .navbar { 5 | // background-color: #f8f9fa; 6 | background-color: $color-white; 7 | // color: #000; 8 | color: $color-black; 9 | transition: background-color 0.3s, color 0.3s; 10 | 11 | &.dark { 12 | // background-color: #333; 13 | // color: #fff; 14 | background-color: $color-black; 15 | color: $color-white; 16 | 17 | .auth-nav-link { 18 | // color: #fff; 19 | color: $color-white; 20 | } 21 | 22 | .logo { 23 | // color: #fff; 24 | color: $color-white; 25 | } 26 | } 27 | } 28 | 29 | .auth-nav-links { 30 | width: auto; 31 | display: flex; 32 | align-items: center; 33 | list-style: none; 34 | 35 | .auth-nav-link { 36 | color: inherit; 37 | width: min-content; 38 | } 39 | } 40 | 41 | .auth-nav-link { 42 | text-decoration: none; 43 | color: $color-black; 44 | font-size: 1.4rem; 45 | font-weight: bold; 46 | opacity: 50%; 47 | } 48 | 49 | .active { 50 | opacity: 100%; 51 | } 52 | 53 | .auth-nav-link:last-child { 54 | margin-left: 4rem; 55 | } 56 | 57 | .logo-container .smallLogo { 58 | height: 40px; 59 | margin-right: 10px; 60 | } 61 | 62 | .logo-container .logo { 63 | font-size: 1.5rem; 64 | font-weight: bold; 65 | } 66 | 67 | .theme-toggle { 68 | background: none; 69 | border: none; 70 | cursor: pointer; 71 | padding: 0; 72 | display: flex; 73 | align-items: center; 74 | justify-content: center; 75 | color: inherit; 76 | font-size: 1.2rem; 77 | margin-left: 1rem; 78 | transition: color 0.3s ease, transform 0.3s ease; 79 | 80 | &:hover { 81 | color: $color-primary; 82 | transform: scale(1.1); 83 | } 84 | 85 | &:focus { 86 | outline: none; 87 | } 88 | } 89 | 90 | .navbar.dark .theme-toggle { 91 | color: #f1c40f; 92 | 93 | &:hover { 94 | color: $color-primary; 95 | } 96 | } 97 | 98 | @media (max-width: 1400px) { 99 | .navbar { 100 | padding: 8px 16px; 101 | } 102 | 103 | .logo-container .smallLogo { 104 | height: 36px; 105 | } 106 | 107 | .logo-container .logo { 108 | font-size: 1.4rem; 109 | } 110 | 111 | .auth-nav-link { 112 | font-size: 1rem; 113 | } 114 | 115 | .theme-toggle { 116 | font-size: 1.1rem; 117 | } 118 | } 119 | 120 | @media (max-width: 1300px) { 121 | .navbar { 122 | padding: 6px 14px; 123 | } 124 | 125 | .logo-container .smallLogo { 126 | height: 32px; 127 | } 128 | 129 | .logo-container .logo { 130 | font-size: 1.3rem; 131 | } 132 | 133 | .auth-nav-link { 134 | font-size: 0.9rem; 135 | } 136 | 137 | .auth-nav-link:last-child { 138 | margin-left: 3rem; 139 | } 140 | 141 | .theme-toggle { 142 | font-size: 1rem; 143 | } 144 | } 145 | 146 | @media (max-width: 1000px) { 147 | .navbar { 148 | padding: 5px 12px; 149 | } 150 | 151 | .logo-container .smallLogo { 152 | height: 28px; 153 | } 154 | 155 | .logo-container .logo { 156 | font-size: 1.2rem; 157 | } 158 | 159 | .auth-nav-link { 160 | font-size: 0.85rem; 161 | } 162 | 163 | .auth-nav-link:last-child { 164 | margin-left: 2rem; 165 | } 166 | 167 | .theme-toggle { 168 | font-size: 0.9rem; 169 | margin-left: 0.8rem; 170 | } 171 | } 172 | 173 | @media (max-width: 800px) { 174 | .navbar { 175 | padding: 4px 10px; 176 | flex-direction: column; 177 | align-items: center; 178 | } 179 | 180 | .logo-container { 181 | margin-bottom: 10px; 182 | } 183 | 184 | .logo-container .smallLogo { 185 | height: 24px; 186 | } 187 | 188 | .logo-container .logo { 189 | font-size: 1.1rem; 190 | } 191 | 192 | .auth-nav-links { 193 | width: 100%; 194 | justify-content: center; 195 | } 196 | 197 | .auth-nav-link { 198 | font-size: 0.8rem; 199 | padding: 5px 10px; 200 | } 201 | 202 | .auth-nav-link:last-child { 203 | margin-left: 1rem; 204 | } 205 | 206 | .theme-toggle { 207 | font-size: 0.8rem; 208 | margin-left: 0.6rem; 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /client/src/components/Dashboard/Dashboard.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useState } from 'react'; 2 | import AuthorizedNavbar from '../AuthorizedNavbar/AuthorizedNavbar'; 3 | import DashboardGrid from '../DashboardGrid/DashboardGrid'; 4 | import ModalGraphName from '../ModalGraphName/ModalGraphName'; 5 | import addGraph from '../../assets/logos/addGraph.png'; 6 | import { useNavigate } from 'react-router-dom'; 7 | import { useTheme } from '../../contexts/ThemeContext'; 8 | import './dashboard.scss'; 9 | 10 | // Defining Dashboard Component 11 | function Dashboard() { 12 | const { darkMode } = useTheme(); 13 | const [ modalVisibility, setModalVisibility ] = useState(false); 14 | 15 | const handleModalOpen = (e) => { 16 | // e.preventDefault(); 17 | setModalVisibility(true); 18 | } 19 | const handleModalClose = (e) => { 20 | // e.preventDefault(); 21 | setModalVisibility(false); 22 | } 23 | // JSX to define Dashboard Page 24 | return ( 25 | <> 26 | 27 | 28 |
29 |

Your Saved Graphs

30 | 31 | 32 | 33 | {/*
34 | 37 |

Click here to create a new project

38 |
39 |
*/} 40 | 41 | 42 | ); 43 | } 44 | 45 | export default Dashboard; 46 | -------------------------------------------------------------------------------- /client/src/components/Dashboard/Dashboard.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen, fireEvent } from '@testing-library/react'; 3 | import '@testing-library/jest-dom'; 4 | import Dashboard from './Dashboard'; 5 | import { ThemeProvider } from '../../contexts/ThemeContext'; 6 | import { BrowserRouter } from 'react-router-dom'; 7 | 8 | // Mock the child components 9 | jest.mock('../AuthorizedNavbar/AuthorizedNavbar', () => () =>
Mocked Navbar
); 10 | jest.mock('../DashboardGrid/DashboardGrid', () => ({ handleModalOpen }) => ( 11 |
12 | Mocked DashboardGrid 13 | 14 |
15 | )); 16 | jest.mock('../ModalGraphName/ModalGraphName', () => ({ modalVisibility, handleModalClose }) => ( 17 | modalVisibility ?
Mocked Modal
: null 18 | )); 19 | 20 | const renderDashboard = (darkMode = false) => { 21 | return render( 22 | 23 | 24 | 25 | 26 | 27 | ); 28 | }; 29 | 30 | describe('Dashboard Component', () => { 31 | test('renders Dashboard with correct title', () => { 32 | renderDashboard(); 33 | expect(screen.getByText('Your Saved Graphs')).toBeInTheDocument(); 34 | }); 35 | 36 | test('renders AuthorizedNavbar', () => { 37 | renderDashboard(); 38 | expect(screen.getByTestId('authorized-navbar')).toBeInTheDocument(); 39 | }); 40 | 41 | test('renders DashboardGrid', () => { 42 | renderDashboard(); 43 | expect(screen.getByTestId('dashboard-grid')).toBeInTheDocument(); 44 | }); 45 | 46 | test('opens modal when "Open Modal" button is clicked', () => { 47 | renderDashboard(); 48 | fireEvent.click(screen.getByText('Open Modal')); 49 | expect(screen.getByTestId('modal-graph-name')).toBeInTheDocument(); 50 | }); 51 | 52 | test('closes modal when "Close" button in modal is clicked', () => { 53 | renderDashboard(); 54 | fireEvent.click(screen.getByText('Open Modal')); 55 | fireEvent.click(screen.getByText('Close')); 56 | expect(screen.queryByTestId('modal-graph-name')).not.toBeInTheDocument(); 57 | }); 58 | 59 | test('does not apply dark mode class when darkMode is false', () => { 60 | renderDashboard(false); 61 | expect(screen.getByText('Your Saved Graphs').className).not.toContain('dark'); 62 | expect(screen.getByTestId('dashboard-grid').parentElement.className).not.toContain('dark'); 63 | }); 64 | }); -------------------------------------------------------------------------------- /client/src/components/Dashboard/dashboard.scss: -------------------------------------------------------------------------------- 1 | @import '../../assets/styles/variables.scss'; 2 | 3 | .dashboard-container { 4 | padding: 1rem; 5 | width: 100%; 6 | // height: 100%; 7 | flex-grow: 1; 8 | display: flex; 9 | flex-direction: column; 10 | align-items: center; 11 | justify-content: flex-start; 12 | &.dark { 13 | background-color: #121212; 14 | color: #ffffff; 15 | } 16 | // border: 5px solid magenta; 17 | } 18 | 19 | .dashboard-title { 20 | // border: 2px solid red; 21 | margin-bottom: 2rem; 22 | // background-color: #f8f9fa; 23 | // color: #000; 24 | // background-color: $color-white; 25 | color: $color-black; 26 | 27 | &.dark { 28 | color: $color-white; 29 | } 30 | } 31 | 32 | .add-graph-section { 33 | display: flex; 34 | flex-direction: row; 35 | align-items: center; 36 | margin-top: 20px; 37 | width: 100%; 38 | text-align: left; 39 | } 40 | 41 | .add-graph-button { 42 | background: none; 43 | border: none; 44 | cursor: pointer; 45 | padding: 0; 46 | } 47 | 48 | .add-graph-button img { 49 | width: 100px; 50 | filter: none; 51 | 52 | .dark & { 53 | filter: invert(1); 54 | } 55 | } 56 | 57 | 58 | .add-graph-section p { 59 | margin-left: 20px; 60 | font-size: 16px; 61 | // color: black; 62 | color: $color-black; 63 | 64 | .dark & { 65 | // color: white; 66 | color: $color-white; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /client/src/components/DashboardGrid/DashboardGrid.jsx: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import React, { useEffect } from 'react'; 3 | import { useAuth } from '../../contexts/AuthContext'; 4 | import { useGraphContext } from '../../contexts/GraphContext'; 5 | import GraphCard from '../GraphCard/GraphCard'; 6 | import addGraph from '../../assets/logos/addGraph.png'; 7 | import './dashboardgrid.scss'; 8 | 9 | // Defining Dashboard Grid upon specific user's graph list 10 | const DashboardGrid = ({ handleModalOpen, handleModalClose }) => { 11 | const { graphList, setGraphList } = useGraphContext(); 12 | const { username, userId } = useAuth(); 13 | 14 | // fetch user's graphList 15 | useEffect(() => { 16 | const fetchGraphList = async () => { 17 | // define request header and payload 18 | const config = { 19 | headers: { authorization: localStorage.getItem('token') } 20 | }; 21 | try { 22 | const response = await axios.get(`/api/graph/${userId}`, config); 23 | // success 24 | setGraphList(response.data.graphList); 25 | } catch(err) { 26 | if (err.reponse) { 27 | console.log('Failed to fetch graphList. Error response:', err.response); 28 | console.log('Failed to fetch graphList. Error status:', err.status); 29 | } else if (err.request) { 30 | console.log('Error request:', err.request); 31 | } else { 32 | console.log('Error message:', err.message); 33 | } 34 | } 35 | } 36 | fetchGraphList(); 37 | return; 38 | }, []) 39 | 40 | const graphCards = graphList.map((graph) => { 41 | return 42 | }) 43 | // JSX to define our Dashboard Grid div 44 | return ( 45 |
46 | {/*
47 | 50 |

Click here to create a new project

51 |
*/} 52 |
+
53 | {graphCards} 54 |
55 | ) 56 | } 57 | 58 | export default DashboardGrid -------------------------------------------------------------------------------- /client/src/components/DashboardGrid/dashboardgrid.scss: -------------------------------------------------------------------------------- 1 | @import '../../assets/styles/variables.scss'; 2 | 3 | .dashboard-grid { 4 | display: grid; 5 | width: 100%; 6 | flex-grow: 1; 7 | grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr)); 8 | grid-template-rows: autofill; 9 | gap: 2rem; 10 | // border: 10px solid orange; 11 | } 12 | 13 | // graph cards start 14 | 15 | @mixin color-opacity($property, $color, $opacity) { 16 | $r: red($color); 17 | $g: green($color); 18 | $b: blue($color); 19 | // border-bottom: 2px solid rgba($r, $g, $b, $opacity); 20 | #{$property}: rgba($r, $g, $b, $opacity); 21 | } 22 | 23 | .graph-card { 24 | font-size: 1.5rem; 25 | cursor: pointer; 26 | width: 10rem; 27 | height: 10rem; 28 | background-color: $color-white; 29 | border-width: .5rem; 30 | border-style: solid; 31 | @include color-opacity(border-color, $color-black, 20%); 32 | border-radius: 1rem; 33 | display: flex; 34 | justify-content: center; 35 | align-items: center; 36 | } 37 | 38 | .new-graph-card { 39 | font-size: 4rem; 40 | @include color-opacity(color, $color-black, 30%) 41 | } -------------------------------------------------------------------------------- /client/src/components/GenerateTabs/genTab.jsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { useState, useEffect } from 'react'; 3 | import { 4 | Box, 5 | Button, 6 | Dialog, 7 | DialogActions, 8 | DialogContent, 9 | DialogTitle, 10 | Tabs, 11 | Tab, 12 | ButtonGroup, 13 | IconButton, 14 | Snackbar, 15 | Alert, 16 | } from '@mui/material'; 17 | import ContentCopyIcon from '@mui/icons-material/ContentCopy'; 18 | import '../NodeSchema/schemavisualizer.scss'; 19 | import schemaGenerator from '../algorithms/schema_generator'; 20 | import resolverGenerator from '../algorithms/resolver_generator'; 21 | import './gentab.scss'; 22 | // Defined Custom Tab Panel to pass down probs and manage children component 23 | function CustomTabPanel(props) { 24 | const { children, value, index, ...other } = props; 25 | 26 | return ( 27 | 36 | ); 37 | } 38 | // a11yProps defined to enhance the accessiblity of tabs 39 | function a11yProps(index) { 40 | return { 41 | id: `simple-tab-${index}`, 42 | 'aria-controls': `simple-tabpanel-${index}`, 43 | }; 44 | } 45 | 46 | // BasicTabs defined to hold inner tabs - child component 47 | function BasicTabs({ generatedSchema, generatedResolver }) { 48 | const [value, setValue] = useState(0); 49 | const [snackbarOpen, setSnackbarOpen] = useState(false); 50 | const [snackbarMessage, setSnackbarMessage] = useState(''); 51 | 52 | const handleChange = (event, newValue) => { 53 | setValue(newValue); 54 | }; 55 | 56 | function handleCopy(text) { 57 | navigator.clipboard 58 | .writeText(text) 59 | .then(() => { 60 | setSnackbarMessage('Copied to clipboard'); 61 | setSnackbarOpen(true); 62 | // alert('copied'); 63 | }) 64 | .catch((err) => { 65 | setSnackbarMessage('Failed to copy'); 66 | setSnackbarOpen(true); 67 | // alert('Failed to copy: ', err); 68 | }); 69 | } 70 | 71 | function handleSnackbarClose() { 72 | setSnackbarOpen(false); 73 | } 74 | // JSX to construct Inner Tab - Child 75 | return ( 76 | <> 77 | 78 | 83 | 84 | 85 | 86 | 87 | 88 | 89 |

TypeDefs

90 | 91 | handleCopy(generatedSchema.join('\n'))} 93 | /> 94 | 95 |
96 | 97 | {generatedSchema.map((item, index) => ( 98 | 110 |
111 |               {item}
112 |             
113 |
114 | ))} 115 |
116 | 117 | 118 |

Resolver

119 | 120 | handleCopy(generatedResolver.join('\n'))} 122 | /> 123 | 124 |
125 | {generatedResolver.map((item, index) => ( 126 | 138 |
139 |               {item}
140 |             
141 |
142 | ))} 143 |
144 | 149 | {snackbarMessage} 150 | 151 | 152 | ); 153 | } 154 | // Defining Main Tab Functionality - contains BasicTabs 155 | const GenerateTab = ({ open, onClose, nodes, edges }) => { 156 | // Storing ER of Schema Generator Function 157 | 158 | let generatedSchemaData = []; 159 | if (open) generatedSchemaData = schemaGenerator(nodes, edges); 160 | // Storing ER of Resolver Generator Function 161 | let generatedResolverData = []; 162 | if (open) generatedResolverData = resolverGenerator(nodes, edges); 163 | 164 | if (!open) return null; 165 | // JSX to construct GenerateTab popup tab 166 | return ( 167 |
168 | 169 | Tabs 170 | 171 | 176 | 177 | 178 | 181 | 182 | 183 |
184 | ); 185 | }; 186 | 187 | export default GenerateTab; 188 | -------------------------------------------------------------------------------- /client/src/components/GenerateTabs/genTab.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen, fireEvent, waitFor } from '@testing-library/react'; 3 | import '@testing-library/jest-dom'; 4 | import GenerateTab from './genTab'; 5 | import schemaGenerator from '../algorithms/schema_generator'; 6 | import resolverGenerator from '../algorithms/resolver_generator'; 7 | 8 | // Mock the schema and resolver generator functions 9 | jest.mock('../algorithms/schema_generator'); 10 | jest.mock('../algorithms/resolver_generator'); 11 | 12 | // Mock the clipboard API 13 | Object.assign(navigator, { 14 | clipboard: { 15 | writeText: jest.fn(), 16 | }, 17 | }); 18 | 19 | describe('GenerateTab Component', () => { 20 | const mockNodes = [{ id: 'node1' }]; 21 | const mockEdges = [{ id: 'edge1' }]; 22 | const mockOnClose = jest.fn(); 23 | 24 | beforeEach(() => { 25 | schemaGenerator.mockReturnValue(['Schema data']); 26 | resolverGenerator.mockReturnValue(['Resolver data']); 27 | }); 28 | 29 | it('renders nothing when closed', () => { 30 | const { container } = render(); 31 | expect(container).toBeEmptyDOMElement(); 32 | }); 33 | 34 | it('renders dialog when open', () => { 35 | render(); 36 | expect(screen.getByText('Tabs')).toBeInTheDocument(); 37 | }); 38 | 39 | it('calls onClose when close button is clicked', () => { 40 | render(); 41 | fireEvent.click(screen.getByText('Close')); 42 | expect(mockOnClose).toHaveBeenCalled(); 43 | }); 44 | 45 | it('displays generated schema and resolver data', () => { 46 | render(); 47 | expect(screen.getByText('Schema data')).toBeInTheDocument(); 48 | fireEvent.click(screen.getByText('Resolver')); 49 | expect(screen.getByText('Resolver data')).toBeInTheDocument(); 50 | }); 51 | 52 | it('shows error snackbar when copy fails', async () => { 53 | navigator.clipboard.writeText.mockRejectedValueOnce(new Error('Copy failed')); 54 | render(); 55 | fireEvent.click(screen.getAllByTestId('ContentCopyIcon')[0]); 56 | await waitFor(() => { 57 | expect(screen.getByText('Failed to copy')).toBeInTheDocument(); 58 | }); 59 | }); 60 | }); -------------------------------------------------------------------------------- /client/src/components/GenerateTabs/gentab.scss: -------------------------------------------------------------------------------- 1 | h3 { 2 | white-space: pre; 3 | font-weight: 300; 4 | } -------------------------------------------------------------------------------- /client/src/components/Graph/Graph.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useState } from 'react'; 2 | import UploadSqlSchema from '../UploadSqlSchema/UploadSqlSchema'; 3 | import AuthorizedNavbar from '../AuthorizedNavbar/AuthorizedNavbar'; 4 | import SchemaVisualizer from '../NodeSchema/SchemaVisualizer' 5 | import { useGraphContext } from '../../contexts/GraphContext.jsx'; 6 | import '../NodeSchema/schemavisualizer.scss'; 7 | // Graph compnent to hold sqlContent state management 8 | function Graph() { 9 | const [sqlContents, setSqlContents] = useState([]); 10 | const { graphName, setGraphName } = useGraphContext(); 11 | // const { graphId, setGraphId } = useGraphContext(); 12 | 13 | const handleUploadBtn = (content) => { 14 | setSqlContents([...sqlContents, content]); 15 | }; 16 | // JSX to structure pass AuthorizedNavbar & UploadSqlSchema 17 | return ( 18 | <> 19 | 20 | 21 | 22 | ); 23 | } 24 | 25 | export default Graph; 26 | -------------------------------------------------------------------------------- /client/src/components/Graph/Graph.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import '@testing-library/jest-dom'; 4 | import { BrowserRouter } from 'react-router-dom'; 5 | import { GraphContext } from '../../contexts/GraphContext'; 6 | import Graph from './Graph'; 7 | 8 | // Mock the AuthorizedNavbar and UploadSqlSchema components 9 | jest.mock('../AuthorizedNavbar/AuthorizedNavbar', () => { 10 | return function MockAuthorizedNavbar() { 11 | return
AuthorizedNavbar
; 12 | }; 13 | }); 14 | 15 | jest.mock('../UploadSqlSchema/UploadSqlSchema', () => { 16 | return function MockUploadSqlSchema() { 17 | return
UploadSqlSchema
; 18 | }; 19 | }); 20 | 21 | // Mock the useGraphContext hook 22 | jest.mock('../../contexts/GraphContext', () => ({ 23 | useGraphContext: jest.fn(), 24 | })); 25 | 26 | describe('Graph Component', () => { 27 | const mockSetGraphName = jest.fn(); 28 | 29 | beforeEach(() => { 30 | // Setup mock for useGraphContext 31 | require('../../contexts/GraphContext').useGraphContext.mockReturnValue({ 32 | graphName: 'Test Graph', 33 | setGraphName: mockSetGraphName, 34 | }); 35 | }); 36 | 37 | it('renders without crashing', () => { 38 | render( 39 | 40 | 41 | 42 | ); 43 | expect(screen.getByTestId('authorized-navbar')).toBeInTheDocument(); 44 | expect(screen.getByTestId('upload-sql-schema')).toBeInTheDocument(); 45 | }); 46 | 47 | it('uses the GraphContext', () => { 48 | render( 49 | 50 | 51 | 52 | ); 53 | expect(require('../../contexts/GraphContext').useGraphContext).toHaveBeenCalled(); 54 | }); 55 | }); -------------------------------------------------------------------------------- /client/src/components/Graph/graph.scss: -------------------------------------------------------------------------------- 1 | @import '../../assets/styles/variables.scss'; 2 | -------------------------------------------------------------------------------- /client/src/components/GraphCard/GraphCard.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | import { useAuth } from '../../contexts/AuthContext'; 4 | import './graphcard.scss' 5 | // GraphCard component, passed in with graphID & graphName 6 | const GraphCard = ({graphId, graphName}) => { 7 | const navigate = useNavigate(); 8 | const { authState } = useAuth(); 9 | // Handling click functionality to navigate to graph 10 | const handleClick = () => { 11 | // navigate to appropriate graph 12 | return navigate(`/graph/${authState.userId}/${graphId}`); 13 | } 14 | 15 | return ( 16 |
handleClick()}>{graphName}
17 | ) 18 | } 19 | 20 | export default GraphCard -------------------------------------------------------------------------------- /client/src/components/GraphCard/GraphCard.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen, fireEvent } from '@testing-library/react'; 3 | import '@testing-library/jest-dom'; 4 | import { useNavigate } from 'react-router-dom'; 5 | import { useAuth } from '../../contexts/AuthContext'; 6 | import GraphCard from './GraphCard'; 7 | 8 | // Mock the useNavigate hook 9 | jest.mock('react-router-dom', () => ({ 10 | useNavigate: jest.fn(), 11 | })); 12 | 13 | // Mock the useAuth hook 14 | jest.mock('../../contexts/AuthContext', () => ({ 15 | useAuth: jest.fn(), 16 | })); 17 | 18 | describe('GraphCard Component', () => { 19 | const mockNavigate = jest.fn(); 20 | const mockAuthState = { 21 | userId: 'testUserId', 22 | }; 23 | 24 | beforeEach(() => { 25 | useNavigate.mockReturnValue(mockNavigate); 26 | useAuth.mockReturnValue({ authState: mockAuthState }); 27 | }); 28 | 29 | it('renders with correct graph name', () => { 30 | render(); 31 | expect(screen.getByText('Test Graph')).toBeInTheDocument(); 32 | }); 33 | 34 | it('navigates to correct URL when clicked', () => { 35 | render(); 36 | const card = screen.getByText('Test Graph'); 37 | fireEvent.click(card); 38 | expect(mockNavigate).toHaveBeenCalledWith('/graph/testUserId/123'); 39 | }); 40 | 41 | it('uses authState from useAuth hook', () => { 42 | render(); 43 | const card = screen.getByText('Test Graph'); 44 | fireEvent.click(card); 45 | expect(useAuth).toHaveBeenCalled(); 46 | }); 47 | 48 | it('applies correct CSS class', () => { 49 | render(); 50 | const card = screen.getByText('Test Graph'); 51 | expect(card).toHaveClass('graph-card'); 52 | }); 53 | 54 | it('renders correctly with different props', () => { 55 | render(); 56 | expect(screen.getByText('Another Graph')).toBeInTheDocument(); 57 | }); 58 | 59 | it('handles click event when auth state changes', () => { 60 | useAuth.mockReturnValue({ authState: { userId: 'newUserId' } }); 61 | render(); 62 | const card = screen.getByText('Test Graph'); 63 | fireEvent.click(card); 64 | expect(mockNavigate).toHaveBeenCalledWith('/graph/newUserId/123'); 65 | }); 66 | }); -------------------------------------------------------------------------------- /client/src/components/GraphCard/graphcard.scss: -------------------------------------------------------------------------------- 1 | @import '../../assets/styles/variables.scss'; 2 | 3 | -------------------------------------------------------------------------------- /client/src/components/Login/Login.jsx: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import React, { useContext, useState, useEffect } from 'react'; 3 | import { createTheme, ThemeProvider } from '@mui/material/styles'; 4 | import Button from '@mui/material/Button'; 5 | import TextField from '@mui/material/TextField'; 6 | import Link from '@mui/material/Link'; 7 | import Grid from '@mui/material/Grid'; 8 | import Box from '@mui/material/Box'; 9 | import Typography from '@mui/material/Typography'; 10 | import heroImg from '../../assets/logos/hero-img.png'; 11 | import { useAuth } from '../../contexts/AuthContext'; 12 | import Navbar from '../Navbar/Navbar'; 13 | import { useNavigate } from 'react-router-dom'; 14 | 15 | // Defining Default Theme 16 | const theme = createTheme({ 17 | palette: { 18 | primary: { 19 | main: '#C978FB', 20 | }, 21 | }, 22 | }); 23 | 24 | // Login Functionality 25 | function Login() { 26 | const [username, setUsername] = useState(''); 27 | const [password, setPassword] = useState(''); 28 | const { authState, setAuthState } = useAuth(); 29 | const navigate = useNavigate(); 30 | 31 | // useEffect to navigate to dashboard 32 | useEffect(() => { 33 | if (authState.isAuth) { 34 | navigate('/dashboard'); 35 | } 36 | }, []); 37 | 38 | // Handle Form Submit 39 | const handleSubmit = async (e) => { 40 | e.preventDefault(); 41 | // await login(username, password); 42 | 43 | // send request to server to login user 44 | try { 45 | const response = await axios.post('/api/auth/login', { 46 | username, 47 | password, 48 | }); 49 | // success 50 | const data = response.data; 51 | setAuthState({ 52 | isAuth: true, 53 | username: data.username, 54 | userId: data.userId, 55 | }); 56 | // console.log('logged in - saving to local storage'); 57 | localStorage.setItem('username', data.username); 58 | localStorage.setItem('userId', data.userId); 59 | localStorage.setItem('token', response.headers['authorization']); 60 | return navigate('/dashboard'); 61 | } catch (err) { 62 | if (err.response) { 63 | // fail - unable to log in 64 | // The request was made and the server responded with a status code 65 | // that falls out of the range of 2xx 66 | console.log('Failed to login. Error response data:', err.response.data); 67 | console.log( 68 | 'Failed to login. Error response status:', 69 | err.response.status 70 | ); 71 | } else if (err.request) { 72 | // The request was made but no response was received 73 | console.log('Error request:', err.request); 74 | } else { 75 | // Something happened in setting up the request that triggered an Error 76 | console.log('Error message:', err.message); 77 | } 78 | } 79 | }; 80 | 81 | // JSX to define Login Component 82 | return ( 83 | <> 84 | 85 | 86 |
87 |
96 | molecule image 101 |
102 | 111 | 112 | Login 113 | 114 | 115 | setUsername(e.target.value)} 126 | /> 127 | setPassword(e.target.value)} 138 | /> 139 | 147 | 148 | 149 | 150 | {"Don't have an account? Sign Up"} 151 | 152 | 153 | 154 | 155 | 156 |
157 |
158 | 159 | ); 160 | } 161 | 162 | export default Login; 163 | -------------------------------------------------------------------------------- /client/src/components/Login/Login.test.js: -------------------------------------------------------------------------------- 1 | // Import necessary modules and functions 2 | import React from 'react'; // Import React library 3 | import { render, screen, fireEvent, act } from '@testing-library/react'; // Import testing functions from React Testing Library 4 | import '@testing-library/jest-dom'; // Extend Jest matchers with custom matchers for DOM nodes 5 | import { useAuth } from '../../contexts/AuthContext.js'; // Import AuthContext 6 | import { ThemeProvider } from '../../contexts/ThemeContext.js'; 7 | import { useGraphContext } from '../../contexts/GraphContext.jsx'; 8 | import { createTheme } from '@mui/material'; 9 | import { MemoryRouter, Routes, Route } from 'react-router-dom'; 10 | import Login from './Login.jsx'; // Import the Login component 11 | import Dashboard from '../Dashboard/Dashboard.jsx'; 12 | import Signup from '../Signup/Signup.jsx'; 13 | import Main from '../Main/Main.jsx'; 14 | import About from '../About/About.jsx'; 15 | import Team from '../Team/Team.jsx'; 16 | import axios from 'axios'; 17 | 18 | //mock contexts and axios 19 | jest.mock('../../contexts/AuthContext'); 20 | jest.mock('../../contexts/GraphContext.jsx'); 21 | jest.mock('axios'); 22 | 23 | //mock return values for auth context 24 | const mockUseAuth = useAuth; 25 | mockUseAuth.mockReturnValue({ 26 | authState: { isAuth: false }, 27 | setAuthState: jest.fn(), 28 | }); 29 | 30 | //mock return values for graph context 31 | const mockUseGraph = useGraphContext; 32 | mockUseGraph.mockReturnValue({ 33 | graphName: 'testGraph', 34 | setGraphName: jest.fn(), 35 | graphList: [], 36 | setGraphList: jest.fn, 37 | }); 38 | 39 | // create custom MUI theme to pass to theme provider 40 | const theme = createTheme({ 41 | palette: { 42 | primary: { 43 | main: '#9c27b0', 44 | }, 45 | custom: { 46 | darkMode: false, 47 | }, 48 | }, 49 | }); 50 | 51 | // Describe the test suite for the Login component 52 | describe('Login Component', () => { 53 | // Reset any mocked fetch calls before each test 54 | beforeEach(() => { 55 | fetch.resetMocks(); 56 | }); 57 | 58 | test('renders Login component correctly', () => { 59 | render( 60 | 61 | 62 | 63 | ); 64 | 65 | //select components on login page to test for correct rendering 66 | const homeLinkNav = screen.getByRole('link', { name: /Home/i }); 67 | const homeLinkIcon = screen.getByAltText(/Small Logo/i); 68 | const teamLink = screen.getByRole('link', { name: /Team/i }); 69 | const aboutLink = screen.getByRole('link', { name: /About/i }); 70 | const githubLink = screen.getByAltText(/GitHub/i); 71 | const username = screen.getByLabelText(/Username/i); 72 | const password = screen.getByLabelText(/Password/i); 73 | const mainImage = screen.getByAltText(/molecule image/i); 74 | const loginButton = screen.getByRole('button', { name: /login/i }); 75 | const signupLink = screen.getByRole('link', { name: /have an account/i }); 76 | 77 | // Verify selected fields are rendering properly 78 | expect(homeLinkNav).toBeInTheDocument(); 79 | expect(homeLinkIcon).toBeInTheDocument(); 80 | expect(teamLink).toBeInTheDocument(); 81 | expect(aboutLink).toBeInTheDocument(); 82 | expect(githubLink).toBeInTheDocument(); 83 | expect(username).toBeInTheDocument(); 84 | expect(password).toBeInTheDocument(); 85 | expect(mainImage).toBeInTheDocument(); 86 | expect(loginButton).toBeInTheDocument(); 87 | expect(signupLink).toBeInTheDocument(); 88 | }); 89 | }); 90 | 91 | describe('Navigation tests for Login page', () => { 92 | //reset memory router before each navigation test 93 | beforeEach(() => { 94 | render( 95 | 96 | 97 | } /> 98 | } /> 99 | } /> 100 | } /> 101 | } /> 102 | } /> 103 | 104 | 105 | ); 106 | }); 107 | 108 | // Test Navigating to Home/Main Route upon Icon Click 109 | test('Successfully navigates to Home/Main route using icon link', () => { 110 | const homeLinkIcon = screen.getByAltText(/Small Logo/i); 111 | fireEvent.click(homeLinkIcon); 112 | expect(screen.getByText(/Implementation in seconds/i)).toBeInTheDocument(); 113 | }); 114 | // Test Navigation to Home/Main upon NavBar Click 115 | test('Successfully navigates to Home/Main route using navbar link', () => { 116 | const homeLinkNav = screen.getByRole('link', { name: /Home/i }); 117 | fireEvent.click(homeLinkNav); 118 | expect(screen.getByText(/Implementation in seconds/i)).toBeInTheDocument(); 119 | }); 120 | // Test Navigate to Team page upon click 121 | test('Successfully navigates to Team route on click', () => { 122 | const teamLink = screen.getByRole('link', { name: /Team/i }); 123 | fireEvent.click(teamLink); 124 | expect(screen.getByText(/Meet the Team/i)).toBeInTheDocument(); 125 | }); 126 | // Test Navigate to About page upon click 127 | test('Successfully navigates to About route on click', () => { 128 | const aboutLink = screen.getByRole('link', { name: /About/i }); 129 | fireEvent.click(aboutLink); 130 | expect(screen.getByText(/think about GraphQL/i)).toBeInTheDocument(); 131 | }); 132 | // Test Navigate to Signup upon click 133 | test('Successfully navigates to Signup route on click', () => { 134 | const signupLink = screen.getByRole('link', { 135 | name: /Don't have an account/i, 136 | }); 137 | fireEvent.click(signupLink); 138 | expect( 139 | screen.getByRole('link', { name: /have an account/i }) 140 | ).toBeInTheDocument(); 141 | }); 142 | }); 143 | // Test for Successful Login 144 | describe('Handles form submission and successfully logs in', () => { 145 | test('Successfully navigates to Dashboard route on login', async () => { 146 | //mock resolved value from axios call 147 | axios.post.mockResolvedValue({ 148 | data: { 149 | username: 'test1', 150 | userId: '111111', 151 | }, 152 | headers: { 153 | authorization: 'Bearer token', 154 | }, 155 | }); 156 | //render component using themeProvider to handle dashboard theme 157 | render( 158 | 159 | 160 | 161 | } /> 162 | } /> 163 | 164 | 165 | 166 | ); 167 | 168 | //select login button and simulate form entry 169 | const loginButton = screen.getByRole('button', { name: /login/i }); 170 | 171 | fireEvent.change(screen.getByLabelText(/Username/i), { 172 | target: { value: 'test1' }, 173 | }); 174 | fireEvent.change(screen.getByLabelText(/Password/i), { 175 | target: { value: '111111' }, 176 | }); 177 | 178 | //submit form and check for dashboard rendering 179 | await act(async () => { 180 | fireEvent.click(loginButton); 181 | }); 182 | 183 | expect( 184 | screen.getByRole('link', { name: /Dashboard/i }) 185 | ).toBeInTheDocument(); 186 | }); 187 | }); 188 | 189 | -------------------------------------------------------------------------------- /client/src/components/Login/login.scss: -------------------------------------------------------------------------------- 1 | @import '../../assets/styles/variables.scss'; 2 | 3 | .login-div { 4 | display: flex; 5 | justify-content: space-between; 6 | align-items: center; 7 | padding: 50px 20px; 8 | background-color: white; 9 | height: calc(100vh - 60px); 10 | } -------------------------------------------------------------------------------- /client/src/components/Main/Main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import heroImg from '../../assets/logos/hero-img.png' 3 | import Navbar from '../Navbar/Navbar'; 4 | import { useNavigate } from 'react-router-dom'; 5 | import { FaRocket, FaMagic, FaCogs, FaSync } from 'react-icons/fa'; 6 | import './main.scss'; 7 | // Main Component to construct 8 | const Main = () => { 9 | const navigate = useNavigate(); 10 | return ( 11 | <> 12 | 13 |
14 |
15 |
16 |

Implementation
in seconds

17 |

Automatically generate a GraphQL layer over your
PostgreSQL database

18 |
19 | 20 | 21 |
22 |
23 |
24 | molecule image 25 |
26 |
27 | 28 |
29 |
30 |

What is moleQLar?

31 |

moleQLar is an open-source tool that simplifies the process of overlaying a GraphQL implementation over your PostgreSQL database. Whether you're working with a monolithic or microservice architecture, moleQLar streamlines your development process.

32 |
33 | 34 |
35 |

Key Features

36 |
37 |
38 |
39 | 40 |

Automatic Schema Generation

41 |

Instantly generate GraphQL schemas from your PostgreSQL database structure.

42 |
43 |
44 | 45 |

Intuitive Visual Interface

46 |

Easily modify and customize your GraphQL schema using our user-friendly interface.

47 |
48 |
49 | 50 |

Customizable Resolvers

51 |

Fine-tune your resolvers to match your specific business logic and requirements.

52 |
53 |
54 | 55 |

Real-time Schema Updates

56 |

See your changes reflected in real-time as you modify your GraphQL schema.

57 |
58 |
59 |
60 |
61 | 62 |
63 |

How to Use moleQLar

64 |
    65 |
  1. Sign up for an account on moleQLar.
  2. 66 |
  3. Connect your PostgreSQL database.
  4. 67 |
  5. Use our visual interface to customize your GraphQL schema.
  6. 68 |
  7. Generate and download your GraphQL schema and resolvers.
  8. 69 |
  9. Integrate the generated code into your project.
  10. 70 |
71 |
72 | 73 |
74 |

Ready to simplify your GraphQL implementation?

75 | 76 |
77 |
78 |
79 | 80 | ); 81 | }; 82 | 83 | export default Main; -------------------------------------------------------------------------------- /client/src/components/Main/Main.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen, fireEvent } from '@testing-library/react'; 3 | import '@testing-library/jest-dom'; 4 | import { BrowserRouter } from 'react-router-dom'; 5 | import { MemoryRouter, Routes, Route } from 'react-router-dom'; 6 | import About from '../About/About.jsx'; 7 | import Team from '../Team/Team.jsx'; 8 | import Signup from '../Signup/Signup.jsx'; 9 | import Login from '../Login/Login.jsx'; 10 | import Main from './Main.jsx'; 11 | import { useAuth } from '../../contexts/AuthContext'; 12 | 13 | //mock auth context 14 | jest.mock('../../contexts/AuthContext'); 15 | 16 | //mock return values for auth context 17 | const mockUseAuth = useAuth; 18 | mockUseAuth.mockReturnValue({ 19 | authState: { isAuth: false }, 20 | setAuthState: jest.fn(), 21 | }) 22 | 23 | 24 | describe('Main page', () => { 25 | beforeEach(() => { 26 | fetch.resetMocks(); 27 | }); 28 | 29 | test('Main component is properly rendered to page', () => { 30 | render( 31 | 32 |
33 | 34 | ); 35 | 36 | //select components on main page to test for correct rendering 37 | const mainHeader = screen.getByText(/Implementation in seconds/i); 38 | const homeLinkNav = screen.getByRole('link', { name: /Home/i }); 39 | const homeLinkIcon = screen.getByAltText(/Small Logo/i); 40 | const teamLink = screen.getByRole('link', { name: /Team/i }); 41 | const aboutLink = screen.getByRole('link', { name: /About/i }); 42 | const githubLink = screen.getByAltText(/GitHub/i); 43 | const loginButton = screen.getByRole('button', {name: /Log In/i }); 44 | const signupButtons = screen.getAllByRole('button', {name: /Sign Up/i }); 45 | const mainImage = screen.getByAltText(/molecule image/i); 46 | 47 | //verify selected fields are rendering properly 48 | expect(mainHeader).toBeInTheDocument(); 49 | expect(homeLinkNav).toBeInTheDocument(); 50 | expect(homeLinkIcon).toBeInTheDocument(); 51 | expect(teamLink).toBeInTheDocument(); 52 | expect(aboutLink).toBeInTheDocument(); 53 | expect(githubLink).toBeInTheDocument(); 54 | expect(loginButton).toBeInTheDocument(); 55 | expect(signupButtons.length).toBeGreaterThan(0); 56 | expect(mainImage).toBeVisible(); 57 | }); 58 | 59 | describe('Navigation tests for Main page', () => { 60 | //reset memory router before each navigation test 61 | beforeEach(() => { 62 | render( 63 | 64 | 65 | } /> 66 | } /> 67 | } /> 68 | } /> 69 | } /> 70 | 71 | 72 | ); 73 | }); 74 | 75 | test('Successfully navigates to Team route on click', () => { 76 | const teamLink = screen.getByRole('link', { name: /Team/i }); 77 | fireEvent.click(teamLink); 78 | expect(screen.getByText(/Meet the Team/i)).toBeInTheDocument(); 79 | }); 80 | 81 | test('Successfully navigates to About route on click', () => { 82 | const aboutLink = screen.getByRole('link', { name: /About/i }); 83 | fireEvent.click(aboutLink); 84 | expect( 85 | screen.getByText(/think about GraphQL/i) 86 | ).toBeInTheDocument(); 87 | }); 88 | 89 | test('Successfully navigates to Login route on click', () => { 90 | const loginButton = screen.getByRole('button', { name: /Log In/i }); 91 | fireEvent.click(loginButton); 92 | expect( 93 | screen.getByText(/Don't have an account?/i) 94 | ).toBeInTheDocument(); 95 | }); 96 | 97 | test('Successfully navigates to Signup route on click', () => { 98 | const signupButtons = screen.getAllByRole('button', { name: /Sign Up/i }); 99 | fireEvent.click(signupButtons[0]); // Click the first "Sign Up" button 100 | expect(screen.getByText(/Already have an account?/i)).toBeInTheDocument(); 101 | }); 102 | 103 | }); 104 | }); 105 | -------------------------------------------------------------------------------- /client/src/components/Main/main.scss: -------------------------------------------------------------------------------- 1 | @import "../../assets/styles/variables.scss"; 2 | 3 | $section-max-width: 1200px; 4 | 5 | .container { 6 | height: 100%; 7 | min-height: 600px; 8 | flex-grow: 1; 9 | width: 100%; 10 | max-width: 1600px; 11 | margin: 0 auto; 12 | display: flex; 13 | flex-direction: column; 14 | align-items: center; 15 | justify-content: center; 16 | padding: 1rem; 17 | } 18 | 19 | .hero-section { 20 | width: 100%; 21 | display: flex; 22 | // flex-direction: row; 23 | justify-content: center; 24 | gap: 2vw; 25 | align-items: center; 26 | margin: 0; 27 | padding: 2% 2% 4% 2%; 28 | // border: 5px solid red; 29 | } 30 | 31 | .hero-content { 32 | max-width: 700px; 33 | width: 50%; 34 | // border: 2px solid orange; 35 | } 36 | 37 | .hero-content h1 { 38 | // max-width: 600px; 39 | // font-size: 6rem; 40 | font-size: clamp(3rem, 6.5vw, 6.5rem); 41 | color: $color-black; 42 | line-height: 90%; 43 | letter-spacing: -2px; 44 | } 45 | 46 | .hero-content p { 47 | max-width: 36rem; 48 | // font-size: 1.4rem; 49 | font-size: clamp(1.5rem, 1.5vw, 1.6rem); 50 | color: $color-black; 51 | opacity: 60%; 52 | font-weight: 300; 53 | // border: 2px solid blue; 54 | } 55 | 56 | .main-btns-container { 57 | margin-top: 2em; 58 | } 59 | 60 | .btn-home { 61 | cursor: pointer; 62 | border-radius: 0.5em; 63 | transition: transform 100ms ease-in; 64 | font-size: 1.4em; 65 | font-weight: 600; 66 | padding: 0.5em 2em; 67 | width: 14rem; 68 | height: 3.5rem; 69 | 70 | &:hover { 71 | transform: translateY(-1px); 72 | transition: transform 100ms ease-in; 73 | } 74 | } 75 | 76 | .btn-signup { 77 | background-color: $color-white; 78 | color: $color-primary; 79 | margin-left: 2rem; 80 | border: none; 81 | box-shadow: inset 0 0 0 0.1em $color-primary; 82 | } 83 | 84 | .btn-login { 85 | background-color: $color-primary; 86 | color: $color-white; 87 | border: none; 88 | } 89 | 90 | .hero-img-container { 91 | // outline: 2px solid green; 92 | // max-width: 50%; 93 | flex-grow: 1; 94 | } 95 | 96 | .hero-img { 97 | width: 100%; 98 | } 99 | 100 | .introduction { 101 | // width: 100%; 102 | // padding: 4rem 2rem; 103 | // margin: 0 auto; 104 | // padding: 4rem 2rem; 105 | // background-color: "blue"; 106 | 107 | .introduction-header { 108 | max-width: $section-max-width; 109 | margin: 0 auto 4rem; 110 | text-align: center; 111 | background-color: $color-white; 112 | 113 | h2 { 114 | font-size: 3rem; 115 | color: $color-black; 116 | margin-bottom: 1rem; 117 | } 118 | 119 | p { 120 | font-size: 1.4rem; 121 | color: $color-black; 122 | opacity: 60%; 123 | } 124 | } 125 | 126 | .features { 127 | margin-bottom: 6rem; 128 | 129 | h3 { 130 | font-weight: 600; 131 | font-size: 2.2rem; 132 | text-align: center; 133 | margin-bottom: 1rem; 134 | color: $color-black; 135 | } 136 | 137 | .feature-grid-container { 138 | display: flex; 139 | width: 100%; 140 | justify-content: center; 141 | // border: 2px solid red; 142 | } 143 | 144 | .feature-grid { 145 | display: grid; 146 | grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); 147 | gap: 2rem; 148 | width: 100%; 149 | 150 | .feature-item { 151 | background-color: #fff; 152 | padding: 2rem; 153 | border-radius: 8px; 154 | box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); 155 | transition: transform 0.3s ease; 156 | 157 | &:hover { 158 | transform: translateY(-5px); 159 | } 160 | 161 | .feature-icon { 162 | font-size: 2.5rem; 163 | color: $color-primary; 164 | margin-bottom: 1rem; 165 | } 166 | 167 | h4 { 168 | font-size: 1.5rem; 169 | margin-bottom: 0.5rem; 170 | color: $color-secondary; 171 | } 172 | 173 | p { 174 | font-size: 1.3rem; 175 | color: $color-black; 176 | opacity: 0.7; 177 | } 178 | } 179 | } 180 | } 181 | 182 | .how-to-use { 183 | max-width: $section-max-width; 184 | // margin: 0 auto 4rem; 185 | margin-bottom: 4rem; 186 | 187 | h3 { 188 | font-weight: 600; 189 | font-size: 2.5rem; 190 | text-align: center; 191 | margin-bottom: 2rem; 192 | color: $color-black; 193 | } 194 | 195 | .steps { 196 | background-color: #fff; 197 | padding: 2rem; 198 | border-radius: 8px; 199 | box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); 200 | counter-reset: step-counter; 201 | list-style-type: none; // Remove default numbering 202 | 203 | li { 204 | font-size: 1.2rem; 205 | margin-bottom: 1.5rem; 206 | padding-left: 3rem; 207 | position: relative; 208 | color: $color-black-text; 209 | transition: all 0.3s ease; 210 | 211 | &:hover { 212 | opacity: 1; 213 | transform: translateX(5px); 214 | } 215 | 216 | &::before { 217 | content: counter(step-counter); 218 | counter-increment: step-counter; 219 | position: absolute; 220 | left: 0; 221 | top: 50%; 222 | transform: translateY(-50%); 223 | width: 2rem; 224 | height: 2rem; 225 | background-color: $color-primary; 226 | color: white; 227 | border-radius: 50%; 228 | display: flex; 229 | align-items: center; 230 | justify-content: center; 231 | font-size: 1rem; 232 | font-weight: bold; 233 | } 234 | 235 | &:last-child { 236 | margin-bottom: 0; 237 | } 238 | } 239 | } 240 | } 241 | 242 | .cta { 243 | text-align: center; 244 | margin-bottom: 2rem; 245 | 246 | p { 247 | font-weight: 250; 248 | font-size: 1.2rem; 249 | margin-bottom: 1rem; 250 | color: $color-black; 251 | opacity: 1; 252 | } 253 | } 254 | } 255 | 256 | // Responsive media queries 257 | 258 | @media (max-width: 1400px) { 259 | html { 260 | font-size: 14px; 261 | } 262 | } 263 | 264 | @media (max-width: 1300px) { 265 | .container { 266 | min-height: auto; 267 | } 268 | .main-btns-container { 269 | margin-top: 1.5em; 270 | } 271 | .btn-home { 272 | font-size: 1rem; 273 | width: 10rem; 274 | height: 2.75rem; 275 | } 276 | .btn-signup { 277 | margin-left: 1.5rem; 278 | } 279 | } 280 | 281 | // only for feature grid 282 | @media (max-width: 1300px) { 283 | .feature-grid { 284 | max-width: 800px; 285 | } 286 | } 287 | 288 | @media (max-width: 1000px) { 289 | .btn-home { 290 | width: 9rem; 291 | height: 2.75rem; 292 | font-size: 1rem; 293 | } 294 | .btn-signup { 295 | margin-left: 1.2rem; 296 | } 297 | } 298 | 299 | @media (max-width: 800px) { 300 | // switch to mobile layout 301 | .hero-section { 302 | flex-direction: column; 303 | gap: 4rem; 304 | } 305 | .hero-content { 306 | width: auto; 307 | max-width: 500px; 308 | order: 2; 309 | } 310 | .hero-content h1 { 311 | font-size: 4.5rem; 312 | text-align: center; 313 | } 314 | .hero-content p { 315 | font-size: 1.5rem; 316 | max-width: auto; 317 | text-align: center; 318 | } 319 | .hero-img-container { 320 | max-width: 500px; 321 | order: 1; 322 | } 323 | .main-btns-container { 324 | display: flex; 325 | justify-content: center; 326 | } 327 | } 328 | -------------------------------------------------------------------------------- /client/src/components/ModalGraphName/ModalGraphName.jsx: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import React, { useState } from 'react'; 3 | import { useNavigate } from 'react-router-dom'; 4 | import { Modal, Box, Typography, TextField, Button } from '@mui/material'; 5 | import { createTheme, ThemeProvider } from '@mui/material/styles'; 6 | import { useAuth } from '../../contexts/AuthContext'; 7 | import { useGraphContext } from '../../contexts/GraphContext'; 8 | 9 | // Modal will pop up only when user creates new graph 10 | const ModalGraphName = (props) => { 11 | const navigate = useNavigate(); 12 | const { authState, setAuthState } = useAuth(); 13 | const { graphName, setGraphName } = useGraphContext(); 14 | // const { graphId, setGraphId } = useGraphContext(); 15 | const { modalVisibility, handleModalClose } = props; 16 | 17 | 18 | // Handle Graph Name Submittion 19 | const handleGraphNameSubmit = async () => { 20 | // send POST request to server 21 | const userId = authState.userId; 22 | const config = { 23 | headers: { authorization: localStorage.getItem("token") }, 24 | } 25 | const payload = { 26 | username: authState.username, 27 | userId: authState.userId, 28 | graphName: graphName, 29 | } 30 | try { 31 | const response = await axios.post(`/api/graph/${userId}`, payload, config); 32 | 33 | // Update graph state - save graph_name, graph_id 34 | setGraphName(response.data.graphName) 35 | setGraphName(response.data.graphId) 36 | 37 | // redirect to /graph/:userId/:graphId 38 | if (response.data) { 39 | return navigate(`/graph/${response.data.userId}/${response.data.graphId}`) 40 | } else { 41 | throw Error('Response missing data'); 42 | } 43 | } catch (err) { 44 | if (err.response) { 45 | // fail - unable to create graph 46 | console.log('Failed to create graph. Error response data:', err.response); 47 | } else if (err.request) { 48 | console.log('Error request:', err.request); 49 | } else { 50 | console.log('Error message:', err.message); 51 | } 52 | } 53 | } 54 | // Default Color 55 | const colors = { 56 | 'color-primary': '#C978FB', 57 | 'color-secondary': '#64268A', 58 | 'color-tertiary': '#31AFD4', 59 | 'color-quaternary': '#093758', 60 | 'color-black': '#190624', 61 | 'color-white': '#FCFCFC', 62 | } 63 | // Default Color Theme 64 | const theme = createTheme({ 65 | typography: { 66 | fontFamily: 'Nunito, Arial, sans-serif', 67 | }, 68 | components: { 69 | MuiButton: { 70 | styleOverrides: { 71 | root: { 72 | backgroundColor: '#C978FB', 73 | fontFamily: 'Outfit', 74 | boxShadow: 'none', 75 | color: colors['color-white'], 76 | marginTop: '1rem', 77 | borderRadius: '.4em', 78 | textTransform: 'none', 79 | fontWeight: 700, 80 | letterSpacing: '0.02rem', 81 | fontSize: '1rem', 82 | transition: 'all 200ms', 83 | '&:hover': { 84 | transform: 'translateY(-1px)', 85 | backgroundColor: '#C978FB', 86 | boxShadow: 'none', 87 | transition: 'all 200ms' 88 | } 89 | } 90 | } 91 | } 92 | } 93 | }) 94 | 95 | const boxStyle = { 96 | backgroundColor: colors['color-white'], 97 | display: 'flex', 98 | flexDirection: 'column', 99 | width: '600px', 100 | borderRadius: '.4em', 101 | position: 'absolute', 102 | top: '50%', 103 | left: '50%', 104 | transform: 'translate(-50%, -50%)', 105 | p: '2rem', 106 | boxShadow: 8, 107 | '&:focus': { 108 | outline: 'none', 109 | } 110 | } 111 | 112 | const titleStyle = { 113 | fontWeight: 500, 114 | fontSize: '1rem', 115 | } 116 | // JSX to define Theme Component-Model Graph 117 | return ( 118 | 119 | 124 | 125 | 126 | Please enter your new graph name: 127 | 128 | setGraphName(e.target.value)} 136 | required 137 | /> 138 | 139 | 140 | 141 | 142 | ) 143 | } 144 | 145 | export default ModalGraphName; -------------------------------------------------------------------------------- /client/src/components/Navbar/Navbar.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import { Link, NavLink } from 'react-router-dom'; 3 | import githubLogo from '../../assets/logos/githubLogo.png'; 4 | import linkedInLogo from '../../assets/logos/linkedin.png'; 5 | import smallLogo from '../../assets/logos/smallLogo.png'; 6 | import './navbar.scss'; 7 | 8 | // Navbar before login component 9 | const Navbar = () => { 10 | return ( 11 | 44 | ); 45 | }; 46 | 47 | export default Navbar; 48 | -------------------------------------------------------------------------------- /client/src/components/Navbar/navbar.scss: -------------------------------------------------------------------------------- 1 | @import "../../assets/styles/variables.scss"; 2 | 3 | .navbar { 4 | width: 100%; 5 | height: 4.5rem; 6 | display: flex; 7 | justify-content: space-between; 8 | align-items: center; 9 | padding: 10px 20px; 10 | background-color: $color-white; 11 | } 12 | 13 | .logo-container { 14 | display: flex; 15 | justify-content: flex-start; 16 | align-items: center; 17 | width: 10rem; 18 | // border: 2px solid red; 19 | } 20 | 21 | .smallLogo { 22 | height: 2.4rem; 23 | margin-right: 10px; 24 | } 25 | 26 | .navbar .logo { 27 | font-size: 35px; 28 | font-weight: bold; 29 | color: $color-black; 30 | text-decoration: none; 31 | } 32 | 33 | .logo { 34 | display: flex; 35 | justify-content: center; 36 | align-items: center; 37 | // border: solid 1px green; 38 | } 39 | 40 | .logo-text { 41 | transform: translateY(-0.04em); 42 | } 43 | 44 | .nav-links { 45 | list-style: none; 46 | display: flex; 47 | justify-content: space-between; 48 | width: 28rem; 49 | // background-color: red; 50 | } 51 | 52 | .nav-link { 53 | text-decoration: none; 54 | color: $color-black; 55 | font-size: 1.8rem; 56 | font-weight: bold; 57 | opacity: 50%; 58 | } 59 | 60 | .active { 61 | opacity: 100%; 62 | } 63 | 64 | .social-icons { 65 | width: 10rem; 66 | display: flex; 67 | justify-content: flex-end; 68 | } 69 | 70 | .navbar .social-icons a { 71 | // margin-left: 15px; 72 | color: $color-black; 73 | } 74 | 75 | .githubLogo, 76 | .linkedIn { 77 | width: 2.4rem; 78 | height: 2.4rem; 79 | margin-left: 2rem; 80 | } 81 | 82 | // @media (max-width: 1300px) { 83 | // .nav-links { 84 | // width: 22rem; 85 | // } 86 | // .nav-link { 87 | // font-size: 1.6rem; 88 | // } 89 | // } 90 | 91 | // @media (max-width: 800px) { 92 | // .nav-links { 93 | // width: 17rem; 94 | // } 95 | // .nav-link { 96 | // font-size: 1.4rem; 97 | // } 98 | // } 99 | @media (max-width: 1400px) { 100 | .navbar { 101 | padding: 8px 16px; 102 | } 103 | 104 | .logo-container { 105 | width: 9rem; 106 | } 107 | 108 | .navbar .logo { 109 | font-size: 32px; 110 | } 111 | 112 | .nav-links { 113 | width: 26rem; 114 | } 115 | 116 | .nav-link { 117 | font-size: 1.7rem; 118 | } 119 | 120 | .githubLogo, 121 | .linkedIn { 122 | width: 2.2rem; 123 | height: 2.2rem; 124 | margin-left: 1.8rem; 125 | } 126 | } 127 | 128 | @media (max-width: 1300px) { 129 | .navbar { 130 | height: 4rem; 131 | } 132 | 133 | .smallLogo { 134 | height: 2.2rem; 135 | } 136 | 137 | .navbar .logo { 138 | font-size: 30px; 139 | } 140 | 141 | .nav-links { 142 | width: 22rem; 143 | } 144 | 145 | .nav-link { 146 | font-size: 1.6rem; 147 | } 148 | 149 | .social-icons { 150 | width: 9rem; 151 | } 152 | } 153 | 154 | @media (max-width: 1000px) { 155 | .navbar { 156 | height: 3.5rem; 157 | padding: 6px 12px; 158 | } 159 | 160 | .logo-container { 161 | width: 8rem; 162 | } 163 | 164 | .smallLogo { 165 | height: 2rem; 166 | } 167 | 168 | .navbar .logo { 169 | font-size: 26px; 170 | } 171 | 172 | .nav-links { 173 | width: 20rem; 174 | } 175 | 176 | .nav-link { 177 | font-size: 1.4rem; 178 | } 179 | 180 | .social-icons { 181 | width: 8rem; 182 | } 183 | 184 | .githubLogo, 185 | .linkedIn { 186 | width: 2rem; 187 | height: 2rem; 188 | margin-left: 1.5rem; 189 | } 190 | } 191 | 192 | @media (max-width: 800px) { 193 | .navbar { 194 | height: 3rem; 195 | padding: 4px 8px; 196 | } 197 | 198 | .logo-container { 199 | width: 7rem; 200 | } 201 | 202 | .smallLogo { 203 | height: 1.8rem; 204 | } 205 | 206 | .navbar .logo { 207 | font-size: 22px; 208 | } 209 | 210 | .nav-link { 211 | font-size: 1.2rem; 212 | } 213 | 214 | .social-icons { 215 | width: 7rem; 216 | } 217 | 218 | .githubLogo, 219 | .linkedIn { 220 | width: 1.8rem; 221 | height: 1.8rem; 222 | margin-left: 1.2rem; 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /client/src/components/NodeSchema/AddNodeDialog.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { 3 | Dialog, 4 | DialogTitle, 5 | DialogContent, 6 | DialogActions, 7 | TextField, 8 | Button, 9 | Select, 10 | MenuItem, 11 | FormControl, 12 | InputLabel, 13 | Checkbox, 14 | FormControlLabel, 15 | IconButton, 16 | List, 17 | ListItem, 18 | ListItemText, 19 | ListItemSecondaryAction, 20 | Grid, 21 | useTheme, 22 | } from '@mui/material'; 23 | import pluralize from 'pluralize'; 24 | import DeleteIcon from '@mui/icons-material/Delete'; 25 | import AddIcon from '@mui/icons-material/Add'; 26 | import EditIcon from '@mui/icons-material/Edit'; 27 | import { useTheme as useCustomTheme } from '../../contexts/ThemeContext'; 28 | 29 | //NodeDialog component handles edits to node graph 30 | const NodeDialog = ({ 31 | open, 32 | onClose, 33 | onAddNode, 34 | onEditNode, 35 | editingNode = null, 36 | primaryKeys, 37 | relationships, 38 | handleSetEdges, 39 | tables, 40 | colorScheme, 41 | }) => { 42 | //define state variables for component 43 | const [nodeName, setNodeName] = useState(''); 44 | const [fields, setFields] = useState([]); 45 | const [newField, setNewField] = useState({ 46 | name: '', 47 | type: 'String', 48 | required: false, 49 | isForeignKey: '', 50 | }); 51 | const [isForeignKeyVisible, setIsForeignKeyVisible] = useState(false); 52 | 53 | //define themes for component 54 | const { darkMode } = useCustomTheme(); 55 | const theme = useTheme(); 56 | 57 | useEffect(() => { 58 | if (editingNode) { 59 | setNodeName(editingNode.data.label); 60 | setFields( 61 | editingNode.data.columns.fields.map((col) => ({ 62 | name: col.name, 63 | type: col.type, 64 | required: col.required, 65 | isForeignKey: col.isForeignKey, 66 | })) 67 | ); 68 | } else { 69 | setNodeName(''); 70 | setFields([]); 71 | } 72 | }, [editingNode]); 73 | 74 | const handleAddField = () => { 75 | if (newField.name) { 76 | setFields([...fields, newField]); 77 | setNewField({ 78 | name: '', 79 | type: 'String', 80 | required: false, 81 | isForeignKey: '', 82 | }); 83 | } 84 | }; 85 | 86 | const handleRemoveField = (index) => { 87 | const updatedFields = fields.filter((_, i) => i !== index); 88 | setFields(updatedFields); 89 | }; 90 | 91 | const handleEditField = (index) => { 92 | setIsForeignKeyVisible(fields[index].isForeignKey); 93 | setNewField(fields[index]); 94 | handleRemoveField(index); 95 | }; 96 | 97 | const handleSubmit = () => { 98 | if (nodeName && fields.length > 0) { 99 | const nodeData = { name: nodeName, fields }; 100 | if (editingNode) { 101 | onEditNode(editingNode.id, nodeData); 102 | } else { 103 | onAddNode(nodeData); 104 | } 105 | setNodeName(''); 106 | setFields([]); 107 | onClose(); 108 | } 109 | }; 110 | 111 | const dialogStyle = { 112 | paper: { 113 | backgroundColor: darkMode ? theme.palette.background.paper : '#fff', 114 | color: darkMode ? theme.palette.text.primary : theme.palette.text.primary, 115 | }, 116 | }; 117 | 118 | const textFieldStyle = { 119 | input: { 120 | color: darkMode ? theme.palette.text.primary : theme.palette.text.primary, 121 | }, 122 | label: { 123 | color: darkMode 124 | ? theme.palette.text.secondary 125 | : theme.palette.text.secondary, 126 | }, 127 | }; 128 | //create primary key menu for edge creation 129 | const primaryKeyMenu = []; 130 | primaryKeys.forEach((pk) => { 131 | primaryKeyMenu.push({pk}); 132 | }); 133 | 134 | return ( 135 | 142 | {editingNode ? 'Edit Node' : 'Add New Node'} 143 | 144 | setNodeName(e.target.value)} 151 | sx={{ mb: 3, ...textFieldStyle }} 152 | /> 153 | 154 | {fields.map((field, index) => ( 155 | 156 | 162 | 163 | handleEditField(index)}> 164 | 165 | 166 | handleRemoveField(index)}> 167 | 168 | 169 | 170 | 171 | ))} 172 | 173 | 180 | 181 | 186 | setNewField({ ...newField, name: e.target.value }) 187 | } 188 | sx={textFieldStyle} 189 | /> 190 | 191 | 192 | 193 | 194 | Type 195 | 196 | 211 | 212 | 213 | 214 | 219 | setNewField({ ...newField, required: e.target.checked }) 220 | } 221 | /> 222 | } 223 | label='NOT NULL' 224 | /> 225 | 226 | 227 | { 232 | if (isForeignKeyVisible) { 233 | //if user removes foreign key, remove associated edge from edges object and update field 234 | setNewField({ ...newField, isForeignKey: '' }); 235 | handleSetEdges( 236 | relationships.filter( 237 | (edge) => 238 | !( 239 | edge.source === nodeName && 240 | edge.sourceHandle === newField.name 241 | ) 242 | ) 243 | ); 244 | } 245 | //update conditional rendering 246 | setIsForeignKeyVisible(!isForeignKeyVisible); 247 | }} 248 | /> 249 | } 250 | label='FOREIGN KEY' 251 | /> 252 | 253 | 254 | 263 | 264 | 265 | 272 | 278 | 279 | 280 | References 281 | 282 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 343 | 344 | 345 | ); 346 | }; 347 | 348 | export default NodeDialog; 349 | -------------------------------------------------------------------------------- /client/src/components/NodeSchema/NodeList.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { 3 | Box, 4 | List, 5 | ListItem, 6 | ListItemText, 7 | IconButton, 8 | Collapse, 9 | Table, 10 | TableBody, 11 | TableCell, 12 | TableContainer, 13 | TableHead, 14 | TableRow, 15 | Paper, 16 | Button, 17 | } from '@mui/material'; 18 | import DeleteIcon from '@mui/icons-material/Delete'; 19 | import EditIcon from '@mui/icons-material/Edit'; 20 | import ExpandLess from '@mui/icons-material/ExpandLess'; 21 | import ExpandMore from '@mui/icons-material/ExpandMore'; 22 | import NodeDialog from './AddNodeDialog'; 23 | import { useTheme } from '../../contexts/ThemeContext'; 24 | import { useGraphContext } from '../../contexts/GraphContext.jsx'; 25 | 26 | import './nodelist.scss'; // styles 27 | 28 | //NodeList component handles sidebar/node manipulation for node graph and acts as parent to dialogs 29 | const NodeList = ({ 30 | tables, 31 | relationships, 32 | handleSetEdges, 33 | onSelectTable, 34 | onDeleteTable, 35 | onAddNode, 36 | onEditNode, 37 | selectedTableId, 38 | primaryKeys, 39 | colorScheme 40 | }) => { 41 | //declare state variables and contexts for component 42 | const [openTable, setOpenTable] = useState(null); 43 | const [isNodeDialogOpen, setIsNodeDialogOpen] = useState(false); 44 | const [editingNode, setEditingNode] = useState(null); 45 | const { darkMode } = useTheme(); 46 | const { graphName, setGraphName } = useGraphContext(); 47 | const { graphId, setGraphId } = useGraphContext(); 48 | 49 | const handleClick = (tableId) => { 50 | setOpenTable(openTable === tableId ? null : tableId); 51 | onSelectTable(tableId); 52 | }; 53 | 54 | const handleAddNode = (newNode) => { 55 | onAddNode(newNode); 56 | }; 57 | 58 | const handleEditNode = (nodeId, updatedNode) => { 59 | onEditNode(nodeId, updatedNode); 60 | setIsNodeDialogOpen(false); 61 | setEditingNode(null); 62 | }; 63 | 64 | const openEditDialog = (table) => { 65 | setEditingNode(table); 66 | setIsNodeDialogOpen(true); 67 | }; 68 | 69 | return ( 70 |
71 |
72 |

{graphName}

73 | 74 | {tables.map((table) => ( 75 | 76 | 93 | handleClick(table.id)} 96 | sx={{ 97 | color: darkMode ? '#fff' : '#000', 98 | flexGrow: 1, 99 | }} 100 | /> 101 | 102 | openEditDialog(table)} 105 | sx={{ p: 0.5, color: darkMode ? '#fff' : '#000' }} 106 | > 107 | 108 | 109 | onDeleteTable(table.id)} 112 | sx={{ p: 0.5, color: darkMode ? '#fff' : '#000' }} 113 | > 114 | 115 | 116 | 117 | 118 | 123 | 124 | 128 | 129 | 130 | 131 | 132 | Name 133 | 134 | 135 | Type 136 | 137 | 138 | Constraints 139 | 140 | 141 | 142 | 143 | {table.data.columns && 144 | table.data.columns.fields && 145 | table.data.columns.fields.map((column, index) => ( 146 | 147 | 152 | {column.name} 153 | 154 | 157 | {column.type} 158 | 159 | 162 | {column.required ? 'NOT NULL' : ''} 163 | 164 | 165 | ))} 166 | 167 |
168 |
169 |
170 |
171 |
172 | ))} 173 |
174 |
175 | 176 |
177 | 183 | { 186 | setIsNodeDialogOpen(false); 187 | setEditingNode(null); 188 | }} 189 | tables={tables} 190 | onAddNode={handleAddNode} 191 | onEditNode={handleEditNode} 192 | editingNode={editingNode} 193 | darkMode={darkMode} 194 | primaryKeys={primaryKeys} 195 | relationships={relationships} 196 | handleSetEdges={handleSetEdges} 197 | colorScheme={colorScheme} 198 | /> 199 |
200 |
201 | ); 202 | }; 203 | 204 | export default NodeList; 205 | -------------------------------------------------------------------------------- /client/src/components/NodeSchema/SchemaVisualizer.jsx: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import React, { useCallback, useEffect, useState, useRef } from 'react'; 3 | import { useNavigate, useParams } from 'react-router-dom'; 4 | import ReactFlow, { 5 | Background, 6 | Controls, 7 | MiniMap, 8 | useNodesState, 9 | useEdgesState, 10 | ReactFlowProvider, 11 | getBezierPath, 12 | Handle, 13 | Position, 14 | } from 'reactflow'; 15 | import { parseSqlSchema } from '../algorithms/schema_parser'; 16 | import NodeList from './NodeList'; 17 | import './schemavisualizer.scss'; // styles 18 | import GenerateTab from '../GenerateTabs/genTab'; 19 | import { useTheme } from '../../contexts/ThemeContext'; 20 | 21 | import { useAuth } from '../../contexts/AuthContext'; 22 | import { useGraphContext } from '../../contexts/GraphContext'; 23 | import pluralize from 'pluralize'; 24 | 25 | // Custom node component for representing database tables 26 | // Memoized for performance optimization in large graphs 27 | const TableNode = React.memo(({ data, id, selected }) => ( 28 |
41 |
49 | {data.label} 50 |
51 | {data.columns && 52 | data.columns.fields && 53 | data.columns.fields.map((col, index) => ( 54 |
55 | 61 | 67 | {col.name} 68 | ({col.type}) 69 | {col.required && NOT NULL} 70 |
71 | ))} 72 |
73 | )); 74 | 75 | const colorScheme = [ 76 | '#ff6b6b', 77 | '#4ecdc4', 78 | '#45aaf2', 79 | '#feca57', 80 | '#a55eea', 81 | '#ff9ff3', 82 | '#54a0ff', 83 | '#5f27cd', 84 | '#48dbfb', 85 | '#ff9ff3', 86 | ]; 87 | 88 | // Custom edge component to visualize relationships between tables 89 | // Allows for custom styling and labels on edges 90 | const CustomEdge = ({ 91 | id, 92 | sourceX, 93 | sourceY, 94 | targetX, 95 | targetY, 96 | sourcePosition, 97 | targetPosition, 98 | data, 99 | }) => { 100 | const [edgePath] = getBezierPath({ 101 | sourceX, 102 | sourceY, 103 | sourcePosition, 104 | targetX, 105 | targetY, 106 | targetPosition, 107 | }); 108 | 109 | return ( 110 | <> 111 | 121 | 122 | 132 | {data.label} 133 | 134 | 135 | 136 | ); 137 | }; 138 | 139 | const nodeTypes = { 140 | table: TableNode, 141 | }; 142 | 143 | const edgeTypes = { 144 | custom: CustomEdge, 145 | }; 146 | 147 | // Main component for visualizing and editing database schemas 148 | const SchemaVisualizer = ({ sqlContents, handleUploadBtn }) => { 149 | //declare state variables for component 150 | const [nodes, setNodes, onNodesChange] = useNodesState([]); 151 | const [edges, setEdges, onEdgesChange] = useEdgesState([]); 152 | const [selectedNode, setSelectedNode] = useState(null); 153 | const [focusMode, setFocusMode] = useState(false); 154 | const reactFlowWrapper = useRef(null); 155 | const [reactFlowInstance, setReactFlowInstance] = useState(null); 156 | const { darkMode } = useTheme(); 157 | const [primaryKeys, setPrimaryKeys] = useState([]); 158 | 159 | //define hooks 160 | const navigate = useNavigate(); 161 | const { username } = useAuth(); 162 | const { graphName, setGraphName } = useGraphContext(); 163 | 164 | // get URL params 165 | const { userId, graphId } = useParams(); 166 | 167 | //handleSetEdges updates edges state variable 168 | const handleSetEdges = (newEdges) => { 169 | setEdges(newEdges); 170 | }; 171 | 172 | // Fetch graph data from the server on component mount 173 | // This allows for persistent storage and retrieval of user's graph data 174 | useEffect(() => { 175 | const fetchGraphData = async () => { 176 | // fetch from server 177 | const config = { 178 | headers: { authorization: localStorage.getItem('token') }, 179 | }; 180 | try { 181 | // GET from server 182 | const response = await axios.get( 183 | `/api/graph/${userId}/${graphId}`, 184 | config 185 | ); 186 | let serverNodes, serverEdges; 187 | response.data.nodes === '' 188 | ? (serverNodes = []) 189 | : (serverNodes = JSON.parse(response.data.nodes)); 190 | response.data.edges === '' 191 | ? (serverEdges = []) 192 | : (serverEdges = JSON.parse(response.data.edges)); 193 | 194 | //set initial node and edge states from database stored graph 195 | await setGraphName(response.data.graphName); 196 | await setNodes(serverNodes); 197 | await setEdges(serverEdges); 198 | setPrimaryKeys( 199 | serverNodes.map((node) => node.dbTableName + '.' + node.primaryKey) 200 | ); 201 | } catch (err) { 202 | if (err.response) { 203 | // fail - unable to log in 204 | // request made, server responded with status code outside of 2xx range 205 | console.log( 206 | 'Failed to pull graph. Error response data:', 207 | err.response.data 208 | ); 209 | console.log( 210 | 'Failed to pull graph. Error response status:', 211 | err.response.status 212 | ); 213 | } else if (err.request) { 214 | console.log('Error request:', err.request); 215 | } else { 216 | console.log('Error message:', err.message); 217 | } 218 | navigate('/dashboard'); 219 | } 220 | }; 221 | fetchGraphData(); 222 | }, []); 223 | 224 | // Save the current graph state to the server 225 | // This function is called when the user clicks the save button 226 | // Allows for persistence of user's work across sessions 227 | const handleSaveBtn = async () => { 228 | // save functionality 229 | // convert nodes and edges to string 230 | const nodeString = JSON.stringify(nodes); 231 | const edgeString = JSON.stringify(edges); 232 | 233 | // send POST request to /api/graph/:userId/:graphId 234 | const config = { 235 | headers: { authorization: localStorage.getItem('token') }, 236 | }; 237 | const payload = { 238 | username: username, 239 | userId: userId, 240 | graphName: graphName, 241 | graphId: graphId, 242 | nodes: nodeString, 243 | edges: edgeString, 244 | }; 245 | try { 246 | const response = await axios.put( 247 | `/api/graph/${userId}/${graphId}`, 248 | payload, 249 | config 250 | ); 251 | // success 252 | // console.log('Successfully saved node graph to database'); 253 | // console.log('response:', response); 254 | } catch (err) { 255 | if (err.response) { 256 | // request made, server responded with status code outside of 2xx range 257 | console.log('Failed ot save graph data:', err.response.data); 258 | console.log('Failed ot save graph status:', err.response.status); 259 | } else if (err.request) { 260 | console.log('Error request:', err.request); 261 | } else { 262 | console.log('Error message:', err.message); 263 | } 264 | } 265 | }; 266 | 267 | //function handles setter for updating primary keys state variable 268 | const handleSetPrimaryKeys = () => { 269 | setPrimaryKeys(tables.map((table) => table.id + '.' + table.primaryKey)); 270 | }; 271 | 272 | // Toggle the generation tab visibility 273 | // This function is called when the user clicks the generate button 274 | // Separates the graph visualization from code generation for a cleaner UI 275 | const [genTabOpen, setGenTabOpen] = useState(false); 276 | const handleGenTabOpen = () => { 277 | setGenTabOpen((prev) => !prev); 278 | }; 279 | const handleGenTabClose = () => { 280 | setGenTabOpen(false); 281 | }; 282 | 283 | // Remove a node from the graph and clean up related edges 284 | // This function ensures graph consistency when deleting nodes 285 | const deleteNode = useCallback( 286 | (id) => { 287 | setNodes((prevNodes) => prevNodes.filter((node) => node.id !== id)); 288 | setEdges((prevEdges) => 289 | prevEdges.filter((edge) => edge.source !== id && edge.target !== id) 290 | ); 291 | if (selectedNode === id) { 292 | setSelectedNode(null); 293 | setFocusMode(false); 294 | } 295 | }, 296 | [setNodes, setEdges, selectedNode] 297 | ); 298 | 299 | // Select a node and focus the view on it 300 | // This function enhances user experience by highlighting the selected node 301 | // and its immediate relationships 302 | const selectNode = useCallback( 303 | (id) => { 304 | setSelectedNode(id); 305 | setFocusMode(true); 306 | setNodes((nds) => 307 | nds.map((node) => ({ 308 | ...node, 309 | selected: node.id === id, 310 | })) 311 | ); 312 | setEdges((eds) => 313 | eds.map((edge) => ({ 314 | ...edge, 315 | data: { 316 | ...edge.data, 317 | hidden: !(edge.source === id || edge.target === id), 318 | }, 319 | })) 320 | ); 321 | if (reactFlowInstance) { 322 | const node = nodes.find((n) => n.id === id); 323 | if (node) { 324 | reactFlowInstance.setCenter(node.position.x, node.position.y, { 325 | duration: 800, 326 | zoom: 1.2, 327 | }); 328 | } 329 | } 330 | }, 331 | [setNodes, setEdges, nodes, reactFlowInstance] 332 | ); 333 | 334 | // Add a new node to the graph 335 | // This function is used when the user wants to manually add a new table 336 | const addNode = useCallback( 337 | (newNode) => { 338 | const nodeId = `table-${nodes.length + 1}`; 339 | const position = { x: 0, y: 0 }; 340 | if (reactFlowInstance) { 341 | const { x, y } = reactFlowInstance.project({ x: 100, y: 100 }); 342 | position.x = x; 343 | position.y = y; 344 | } 345 | 346 | const newTableNode = { 347 | id: newNode.name, 348 | type: 'table', 349 | position, 350 | primaryKey: newNode.fields[0].name, 351 | data: { 352 | label: newNode.name, 353 | columns: { 354 | fields: newNode.fields.map((field) => ({ 355 | name: field.name, 356 | type: field.type, 357 | required: field.required, 358 | isForeignKey: field.isForeignKey, 359 | })), 360 | primaryKey: newNode.fields[0].name, 361 | }, 362 | }, 363 | }; 364 | //link to database table 365 | const dbTable = pluralize(newNode.name).replace( 366 | /^./, 367 | newNode.name[0].toLowerCase() 368 | ); 369 | //add primary key to primaryKeys array 370 | const newPrimaryKeys = [ 371 | ...primaryKeys, 372 | dbTable + '.' + newTableNode.data.columns.primaryKey, 373 | ]; 374 | setPrimaryKeys(newPrimaryKeys); 375 | setNodes((nds) => [...nds, newTableNode]); 376 | }, 377 | [nodes, reactFlowInstance, setNodes, setPrimaryKeys] 378 | ); 379 | 380 | // Edit an existing node in the graph 381 | // This function allows users to modify table structures after initial creation 382 | const editNode = useCallback( 383 | (nodeId, updatedNode) => { 384 | setNodes((nds) => 385 | nds.map((node) => 386 | node.id === nodeId 387 | ? { 388 | ...node, 389 | data: { 390 | ...node.data, 391 | label: updatedNode.name, 392 | columns: { 393 | fields: updatedNode.fields.map((field) => ({ 394 | name: field.name, 395 | type: field.type, 396 | required: field.required, 397 | isForeignKey: field.isForeignKey, 398 | })), 399 | }, 400 | }, 401 | } 402 | : node 403 | ) 404 | ); 405 | }, 406 | [setNodes] 407 | ); 408 | 409 | // Parse SQL contents and update the graph 410 | // This effect runs when new SQL content is uploaded, automating the graph creation process 411 | useEffect(() => { 412 | if (sqlContents.length > 0) { 413 | const { nodes: newNodes, edges: newEdges } = parseSqlSchema( 414 | sqlContents[sqlContents.length - 1] 415 | ); 416 | //color connections in the node graph 417 | const coloredEdges = newEdges.map((edge, index) => ({ 418 | ...edge, 419 | type: 'custom', 420 | data: { 421 | color: colorScheme[index % colorScheme.length], 422 | label: `${edge.sourceHandle} → ${edge.targetHandle}`, 423 | hidden: false, 424 | }, 425 | style: { stroke: colorScheme[index % colorScheme.length] }, 426 | })); 427 | //set initial states from schema parser algorithm output 428 | setPrimaryKeys( 429 | newNodes.map((node) => node.dbTableName + '.' + node.primaryKey) 430 | ); 431 | setNodes(newNodes); 432 | setEdges(coloredEdges); 433 | if (reactFlowInstance) { 434 | setTimeout(() => { 435 | reactFlowInstance.fitView({ padding: 0.1, includeHiddenNodes: true }); 436 | }, 100); 437 | } 438 | } 439 | }, [sqlContents, setNodes, setEdges, setPrimaryKeys, reactFlowInstance]); 440 | 441 | // Render the schema visualizer component 442 | // This includes the node list, graph area, and generation tab 443 | return ( 444 |
445 | 451 | 463 | 464 |
468 |
469 | 476 | 479 |
480 | 481 | selectNode(node.id)} 492 | onInit={setReactFlowInstance} 493 | > 494 | 495 | 502 | 509 | 510 |
511 |
512 |
513 | ); 514 | }; 515 | 516 | export default SchemaVisualizer; 517 | -------------------------------------------------------------------------------- /client/src/components/NodeSchema/nodelist.scss: -------------------------------------------------------------------------------- 1 | @import "../../assets/styles/variables.scss"; 2 | 3 | @mixin color-opacity($property, $color, $opacity) { 4 | $r: red($color); 5 | $g: green($color); 6 | $b: blue($color); 7 | // border-bottom: 2px solid rgba($r, $g, $b, $opacity); 8 | #{$property}: rgba($r, $g, $b, $opacity); 9 | } 10 | 11 | .sidebar { 12 | position: relative; 13 | // max-height: calc(100vh - 4.5rem); 14 | width: 300px; 15 | height: auto; 16 | display: flex; 17 | flex-direction: column; 18 | justify-content: space-between; 19 | border-width: 2px; 20 | border-style: solid; 21 | @include color-opacity(border-color, $color-black, 10%); 22 | // border: solid 3px green; 23 | 24 | background-color: $color-white; 25 | color: $color-black; 26 | 27 | &.dark { 28 | background-color: $color-black; 29 | color: $color-white; 30 | @include color-opacity(border-color, $color-white, 10%); 31 | } 32 | } 33 | 34 | .sidebar-top { 35 | padding: 1rem 0.5rem 0; 36 | .sidebar-heading { 37 | font-size: 1.5rem; 38 | font-weight: 600; 39 | text-align: center; 40 | } 41 | } 42 | 43 | .sidebar-bottom { 44 | // background-color: $color-white; 45 | width: 100%; 46 | position: absolute; 47 | bottom: 0; 48 | z-index: 2; 49 | padding-top: 1rem; 50 | display: flex; 51 | flex-direction: column; 52 | align-items: center; 53 | background-color: inherit; 54 | 55 | button { 56 | margin-bottom: 1rem; 57 | } 58 | 59 | .btn-graph { 60 | cursor: pointer; 61 | border-radius: 0.5em; 62 | width: 12rem; 63 | height: 3.5rem; 64 | font-weight: 600; 65 | font-size: 1.2rem; 66 | color: $color-white; 67 | transition: transform 100ms ease-in; 68 | 69 | &:hover { 70 | transform: translateY(-1px); 71 | transition: transform 100ms ease-in; 72 | } 73 | } 74 | 75 | .btn-add-node { 76 | background-color: $color-primary; 77 | border: none; 78 | } 79 | 80 | .btn-clear { 81 | background-color: $color-white; 82 | color: $color-primary; 83 | border: 2px solid $color-primary; 84 | 85 | .dark & { 86 | background-color: $color-black; 87 | color: $color-primary; 88 | border-color: $color-primary; 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /client/src/components/NodeSchema/schemavisualizer.scss: -------------------------------------------------------------------------------- 1 | @import "../../assets/styles/variables.scss"; 2 | 3 | .schema-visualizer { 4 | width: 100%; 5 | height: 100%; 6 | display: flex; 7 | flex-direction: row; 8 | justify-content: space-between; 9 | overflow: hidden; 10 | flex-grow: 1; 11 | // border: 5px solid magenta; 12 | } 13 | 14 | .node-graph-container { 15 | position: relative; 16 | flex-grow: 1; 17 | display: flex; 18 | flex-direction: column; 19 | background: $color-white; 20 | 21 | &.dark { 22 | background: $color-black; 23 | } 24 | } 25 | 26 | .node-graph { 27 | width: 100%; 28 | height: 100%; 29 | flex-grow: 1; 30 | background-color: $color-white; 31 | 32 | &.dark { 33 | background-color: $color-black; 34 | } 35 | } 36 | 37 | // buttons start 38 | .graph-btn-container { 39 | position: absolute; 40 | z-index: 1; 41 | top: 1rem; 42 | right: 1rem; 43 | } 44 | 45 | .btn-graph { 46 | cursor: pointer; 47 | border-radius: 0.5em; 48 | width: 12rem; 49 | height: 3.5rem; 50 | font-weight: 600; 51 | font-size: 1.2rem; 52 | color: $color-white; 53 | transition: transform 100ms ease-in; 54 | 55 | &:hover { 56 | transform: translateY(-1px); 57 | transition: transform 100ms ease-in; 58 | } 59 | } 60 | 61 | .btn-save { 62 | background-color: $color-primary; 63 | margin-left: 1rem; 64 | } 65 | 66 | .generate { 67 | width: min-content; 68 | height: min-content; 69 | top: auto; 70 | bottom: 2rem; 71 | } 72 | 73 | .btn-generate { 74 | background-color: $color-primary; 75 | // background-color: $color-primary; 76 | // border: $color-white solid .2em; 77 | // font-size: 1.4rem; 78 | // width: 14rem; 79 | // height: 4rem; 80 | } 81 | // buttons end 82 | -------------------------------------------------------------------------------- /client/src/components/Signup/Signup.jsx: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import React, { useState, useEffect } from 'react'; 3 | import { useNavigate } from 'react-router-dom'; 4 | import Button from '@mui/material/Button'; 5 | import { createTheme, ThemeProvider } from '@mui/material/styles'; 6 | import TextField from '@mui/material/TextField'; 7 | import Link from '@mui/material/Link'; 8 | import Grid from '@mui/material/Grid'; 9 | import Box from '@mui/material/Box'; 10 | import Typography from '@mui/material/Typography'; 11 | import Alert from '@mui/material/Alert'; 12 | import CircularProgress from '@mui/material/CircularProgress'; 13 | import heroImg from '../../assets/logos/hero-img.png'; 14 | import Navbar from '../Navbar/Navbar'; 15 | import { useAuth } from '../../contexts/AuthContext'; 16 | import './signup.scss'; 17 | 18 | // Defining Default Theme 19 | const theme = createTheme({ 20 | palette: { 21 | primary: { 22 | main: '#9c27b0', 23 | }, 24 | }, 25 | }); 26 | 27 | // Signup Functionality 28 | function Signup() { 29 | let navigate = useNavigate(); 30 | const [formData, setFormData] = useState({ 31 | username: '', 32 | email: '', 33 | password: '', 34 | confirmPassword: '', 35 | }); 36 | const [errors, setErrors] = useState({}); 37 | const [isLoading, setIsLoading] = useState(false); 38 | const [submitError, setSubmitError] = useState(''); 39 | const [submitSuccess, setSubmitSuccess] = useState(false); 40 | const { authState, setAuthState } = useAuth(); 41 | 42 | // useEffect to Navgiate to Dashboard 43 | useEffect(() => { 44 | if (authState.isAuth) { 45 | navigate('/dashboard'); 46 | } 47 | }, []); 48 | 49 | const handleChange = (e) => { 50 | const { name, value } = e.target; 51 | setFormData({ ...formData, [name]: value }); 52 | if (errors[name]) { 53 | setErrors({ ...errors, [name]: '' }); 54 | } 55 | }; 56 | 57 | // Form Validation 58 | const validateForm = () => { 59 | const newErrors = {}; 60 | if (!formData.username.trim()) newErrors.username = 'Username is required'; 61 | if (!formData.email.trim()) newErrors.email = 'Email is required'; 62 | else if (!/\S+@\S+\.\S+/.test(formData.email)) 63 | newErrors.email = 'Email is invalid'; 64 | if (!formData.password) newErrors.password = 'Password is required'; 65 | // else if (formData.password.length < 6) newErrors.password = 'Password must be at least 6 characters'; 66 | if (formData.password !== formData.confirmPassword) 67 | newErrors.confirmPassword = 'Passwords do not match'; 68 | 69 | setErrors(newErrors); 70 | return Object.keys(newErrors).length === 0; 71 | }; 72 | 73 | // Handling Form Submit 74 | const handleSubmit = async (e) => { 75 | e.preventDefault(); 76 | if (validateForm()) { 77 | setIsLoading(true); 78 | setSubmitError(''); 79 | try { 80 | // await signup(formData); // send POST request to server to create new user 81 | 82 | try { 83 | const response = await axios.post('/api/auth/signup', formData); 84 | const data = response.data; 85 | setAuthState({ 86 | isAuth: true, 87 | username: data.username, 88 | userId: data.userId, 89 | }); 90 | // console.log('signup succesful - updating local storage'); 91 | localStorage.setItem('username', data.username); 92 | localStorage.setItem('userId', data.userId); 93 | localStorage.setItem('token', response.headers['authorization']); 94 | // navigate outside of try-catch block 95 | } catch (err) { 96 | if (err.response) { 97 | // response status code outside 2XX 98 | console.log( 99 | 'Failed to sign up. Error response data:', 100 | err.response.data 101 | ); 102 | console.log( 103 | 'Failed to sign up. Error response status:', 104 | err.response.status 105 | ); 106 | } else if (err.request) { 107 | // request was made, but no response received 108 | console.log('Error request:', err.request); 109 | } else { 110 | // error in setting up request 111 | console.log('Error message:', err.message); 112 | } 113 | } 114 | setSubmitSuccess(true); 115 | } catch (error) { 116 | console.error(error); 117 | setSubmitError( 118 | error.response?.data?.message || 'An error occurred during signup' 119 | ); 120 | } finally { 121 | setIsLoading(false); 122 | } 123 | return navigate('/dashboard'); 124 | } 125 | }; 126 | 127 | // JSX to define Signup Component 128 | return ( 129 | <> 130 | 131 | 132 | 133 |
134 |
135 | Main Graphic 136 |
137 | 138 | 148 | 149 | Sign up 150 | 151 | 152 | {submitSuccess && ( 153 | 154 | Signup successful! You can now login. 155 | 156 | )} 157 | 158 | {submitError && ( 159 | 160 | {submitError} 161 | 162 | )} 163 | 164 | 170 | 171 | 172 | 184 | 185 | 186 | 198 | 199 | 200 | 213 | 214 | 215 | 227 | 228 | 229 | 238 | 239 | 240 | 241 | Already have an account? Sign in 242 | 243 | 244 | 245 | 246 | 247 |
248 |
249 | 250 | ); 251 | } 252 | 253 | export default Signup; 254 | -------------------------------------------------------------------------------- /client/src/components/Signup/Signup.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen, act, fireEvent } from '@testing-library/react'; 3 | import '@testing-library/jest-dom'; 4 | import { BrowserRouter } from 'react-router-dom'; 5 | import { MemoryRouter, Routes, Route } from 'react-router-dom'; 6 | import { useAuth } from '../../contexts/AuthContext'; 7 | import { ThemeProvider } from '../../contexts/ThemeContext.js'; 8 | import { useGraphContext } from '../../contexts/GraphContext.jsx'; 9 | import { createTheme } from '@mui/material'; 10 | import About from '../About/About.jsx'; 11 | import Team from '../Team/Team.jsx'; 12 | import Signup from './Signup.jsx'; 13 | import Login from '../Login/Login.jsx'; 14 | import Main from '../Main/Main.jsx'; 15 | import Dashboard from '../Dashboard/Dashboard.jsx'; 16 | import axios from 'axios'; 17 | 18 | //mock contexts and axios 19 | jest.mock('../../contexts/AuthContext'); 20 | jest.mock('../../contexts/GraphContext.jsx'); 21 | jest.mock('axios'); 22 | 23 | //mock return values for auth context 24 | const mockUseAuth = useAuth; 25 | mockUseAuth.mockReturnValue({ 26 | authState: { isAuth: false }, 27 | setAuthState: jest.fn(), 28 | }); 29 | 30 | //mock return values for graph context 31 | const mockUseGraph = useGraphContext; 32 | mockUseGraph.mockReturnValue({ 33 | graphName: 'testGraph', 34 | setGraphName: jest.fn(), 35 | graphList: [], 36 | setGraphList: jest.fn, 37 | }); 38 | 39 | // create custom MUI theme to pass to theme provider 40 | const theme = createTheme({ 41 | palette: { 42 | primary: { 43 | main: '#9c27b0', 44 | }, 45 | custom: { 46 | darkMode: false, 47 | }, 48 | }, 49 | }); 50 | 51 | describe('Signup page', () => { 52 | beforeEach(() => { 53 | fetch.resetMocks(); 54 | }); 55 | 56 | test('Signup component is properly rendered to page', () => { 57 | render( 58 | 59 | 60 | 61 | ); 62 | 63 | //select components on signup page to test for correct rendering 64 | const homeLinkNav = screen.getByRole('link', { name: /Home/i }); 65 | const homeLinkIcon = screen.getByAltText(/Small Logo/i); 66 | const teamLink = screen.getByRole('link', { name: /Team/i }); 67 | const aboutLink = screen.getByRole('link', { name: /About/i }); 68 | const githubLink = screen.getByAltText(/GitHub/i); 69 | const username = screen.getByLabelText(/Username/i); 70 | const email = screen.getByLabelText(/Email Address/i); 71 | const password = screen.getAllByText(/Password/i)[0]; 72 | const confirmPw = screen.getByLabelText(/Confirm Password/i); 73 | const signupButton = screen.getByRole('button', { name: /Sign up/i }); 74 | const mainImage = screen.getByAltText(/Main Graphic/i); 75 | const loginLink = screen.getByRole('link', { 76 | name: /Already have an account/i, 77 | }); 78 | 79 | //verify selected fields are rendering properly 80 | expect(homeLinkNav).toBeInTheDocument(); 81 | expect(homeLinkIcon).toBeInTheDocument(); 82 | expect(teamLink).toBeInTheDocument(); 83 | expect(aboutLink).toBeInTheDocument(); 84 | expect(githubLink).toBeInTheDocument(); 85 | expect(username).toBeInTheDocument(); 86 | expect(email).toBeInTheDocument(); 87 | expect(password).toBeInTheDocument(); 88 | expect(confirmPw).toBeInTheDocument(); 89 | expect(signupButton).toBeInTheDocument(); 90 | expect(mainImage).toBeInTheDocument(); 91 | expect(loginLink).toBeInTheDocument(); 92 | }); 93 | 94 | // test suite for Signup Page 95 | describe('Navigation tests for Signup page', () => { 96 | //reset memory router before each navigation test 97 | beforeEach(() => { 98 | render( 99 | 100 | 101 | } /> 102 | } /> 103 | } /> 104 | } /> 105 | } /> 106 | } /> 107 | 108 | 109 | ); 110 | }); 111 | // Test Navigation to Home/Main upon Icon click 112 | test('Successfully navigates to Home/Main route using icon link', () => { 113 | const homeLinkIcon = screen.getByAltText(/Small Logo/i); 114 | fireEvent.click(homeLinkIcon); 115 | expect( 116 | screen.getByText(/Implementation in seconds/i) 117 | ).toBeInTheDocument(); 118 | }); 119 | // Test Navigation to Home/Main upon NavBar click 120 | test('Successfully navigates to Home/Main route using navbar link', () => { 121 | const homeLinkNav = screen.getByRole('link', { name: /Home/i }); 122 | fireEvent.click(homeLinkNav); 123 | expect( 124 | screen.getByText(/Implementation in seconds/i) 125 | ).toBeInTheDocument(); 126 | }); 127 | // Test Navigation to Team page upon click 128 | test('Successfully navigates to Team route on click', () => { 129 | const teamLink = screen.getByRole('link', { name: /Team/i }); 130 | fireEvent.click(teamLink); 131 | expect(screen.getByText(/Meet the Team/i)).toBeInTheDocument(); 132 | }); 133 | // Test Navigation to About page upon click 134 | test('Successfully navigates to About route on click', () => { 135 | const aboutLink = screen.getByRole('link', { name: /About/i }); 136 | fireEvent.click(aboutLink); 137 | expect(screen.getByText(/think about GraphQL/i)).toBeInTheDocument(); 138 | }); 139 | // Test Navigation to Login page upon click 140 | test('Successfully navigates to Login route on click', () => { 141 | const loginLink = screen.getByRole('link', { 142 | name: /Already have an account/i, 143 | }); 144 | fireEvent.click(loginLink); 145 | expect( 146 | screen.getByRole('link', { name: /have an account/i }) 147 | ).toBeInTheDocument(); 148 | }); 149 | }); 150 | // Test for Successful Signup Submission 151 | describe('Handles form submission and successfully signs up', () => { 152 | test('Successfully navigates to Dashboard route on signup', async () => { 153 | //mock resolved value from axios call 154 | axios.post.mockResolvedValue({ 155 | data: { 156 | username: 'test1', 157 | userId: '111111', 158 | }, 159 | headers: { 160 | authorization: 'Bearer token', 161 | }, 162 | }); 163 | //render component using themeProvider to handle dashboard theme 164 | render( 165 | 166 | 167 | 168 | } /> 169 | } /> 170 | 171 | 172 | 173 | ); 174 | 175 | //select signup button and simulate form entry 176 | const signupButton = screen.getByRole('button', { name: /Sign up/i }); 177 | 178 | fireEvent.change(screen.getByLabelText(/Username/i), { 179 | target: { value: 'test1' }, 180 | }); 181 | 182 | fireEvent.change(screen.getByLabelText(/Email Address/i), { 183 | target: { value: 'test1@gmail.com' }, 184 | }); 185 | 186 | const passwordFields = screen.getAllByLabelText(/Password/i); 187 | passwordFields.forEach((field) => { 188 | fireEvent.change(field, { 189 | target: { value: '111111' }, 190 | }); 191 | }); 192 | 193 | //submit form and check for dashboard rendering 194 | await act(async () => { 195 | fireEvent.click(signupButton); 196 | }); 197 | 198 | expect( 199 | screen.getByRole('link', { name: /Dashboard/i }) 200 | ).toBeInTheDocument(); 201 | }); 202 | }); 203 | }); 204 | -------------------------------------------------------------------------------- /client/src/components/Signup/signup.scss: -------------------------------------------------------------------------------- 1 | @import '../../assets/styles/variables.scss'; 2 | 3 | .signup-hero-img { 4 | display: flex; 5 | justify-content: center; 6 | align-items: center; 7 | margin-top: 3rem; 8 | } 9 | 10 | @media (max-width: 800px) { 11 | .signup-hero-img { 12 | margin-top: 2rem; 13 | } 14 | } -------------------------------------------------------------------------------- /client/src/components/Team/Team.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import jonathanImg from '../../assets/team-pics/Jonathan.png'; 3 | import mingzhuImg from '../../assets/team-pics/Mingzhu.png'; 4 | import ericImg from '../../assets/team-pics/Erick.png'; 5 | import brianImg from '../../assets/team-pics/Brian.png'; 6 | import danImg from '../../assets/team-pics/Dan.png'; 7 | import githubLogo from '../../assets/logos/githubLogo.png'; 8 | import linkedInLogo from '../../assets/logos/linkedin.png'; 9 | 10 | import Navbar from '../Navbar/Navbar'; 11 | import './team.scss'; 12 | // Team Page defined with JSX 13 | const Team = () => { 14 | return ( 15 | <> 16 | 17 |
18 |

Meet the Team

19 |
20 |
21 | Jonathan Ghebrial 26 |

Jonathan Ghebrial

27 |

Software Engineer

28 | 43 |
44 |
45 | Mingzhu Wan 46 |

Mingzhu Wan

47 |

Software Engineer

48 | 60 |
61 |
62 | Eric Alvarez 63 |

Erick Alvarez

64 |

Software Engineer

65 | 80 |
81 |
82 | Brian Yang 83 |

Brian Yang

84 |

Software Engineer

85 | 100 |
101 |
102 | Dan Hudgens 103 |

Dan Hudgens

104 |

Software Engineer

105 | 120 |
121 |
122 |
123 | 124 | ); 125 | }; 126 | 127 | export default Team; 128 | -------------------------------------------------------------------------------- /client/src/components/Team/team.scss: -------------------------------------------------------------------------------- 1 | @import '../../assets/styles/variables.scss'; 2 | 3 | .team-section { 4 | width: 100%; 5 | text-align: center; 6 | padding: 3.125rem 1.25rem; 7 | display: flex; 8 | flex-direction: column; 9 | align-items: center; 10 | } 11 | 12 | .team-section h2 { 13 | font-size: 2rem; 14 | margin-bottom: 2rem; 15 | } 16 | 17 | .team-grid { 18 | display: flex; 19 | flex-wrap: wrap; 20 | justify-content: center; 21 | width: 100%; 22 | max-width: 75rem; 23 | } 24 | 25 | .team-member { 26 | text-align: center; 27 | width: 33.33%; 28 | box-sizing: border-box; 29 | padding: 0.625rem; 30 | } 31 | 32 | .profile-img { 33 | width: 9rem; 34 | height: 9rem; 35 | border-radius: 50%; 36 | object-fit: cover; 37 | } 38 | 39 | .team-member h3 { 40 | font-size: 1rem; 41 | margin: 0.625rem 0 0.3125rem; 42 | } 43 | 44 | .team-member p { 45 | font-size: 0.8rem; 46 | color: gray; 47 | } 48 | 49 | .social-links { 50 | display: flex; 51 | justify-content: center; 52 | margin-top: 0.625rem; 53 | } 54 | 55 | .social-links a { 56 | margin: 0 0.3125rem; 57 | } 58 | 59 | .social-logo { 60 | width: 1.5rem; 61 | height: 1.5rem; 62 | transition: transform 100ms ease; 63 | 64 | &:hover { 65 | transform: translateY(-1px); 66 | transition: transform 100ms ease; 67 | } 68 | } 69 | 70 | @media (max-width: 1400px) { 71 | .team-section { 72 | padding: 2.5rem 1rem; 73 | } 74 | 75 | .team-grid { 76 | max-width: 70rem; 77 | } 78 | 79 | .profile-img { 80 | width: 8rem; 81 | height: 8rem; 82 | } 83 | } 84 | 85 | @media (max-width: 1300px) { 86 | .team-grid { 87 | max-width: 65rem; 88 | } 89 | 90 | .team-member { 91 | width: 50%; 92 | padding: 0.5rem; 93 | } 94 | } 95 | 96 | @media (max-width: 1000px) { 97 | .team-section h2 { 98 | font-size: 1.75rem; 99 | } 100 | 101 | .team-grid { 102 | max-width: 50rem; 103 | } 104 | 105 | .profile-img { 106 | width: 7rem; 107 | height: 7rem; 108 | } 109 | 110 | .team-member h3 { 111 | font-size: 0.9rem; 112 | } 113 | 114 | .team-member p { 115 | font-size: 0.75rem; 116 | } 117 | } 118 | 119 | @media (max-width: 800px) { 120 | .team-section { 121 | padding: 2rem 0.75rem; 122 | } 123 | 124 | .team-section h2 { 125 | font-size: 1.5rem; 126 | margin-bottom: 1.5rem; 127 | } 128 | 129 | .team-grid { 130 | max-width: 100%; 131 | } 132 | 133 | .team-member { 134 | width: 100%; 135 | padding: 0.5rem 0; 136 | margin-bottom: 1.5rem; 137 | } 138 | 139 | .profile-img { 140 | width: 6rem; 141 | height: 6rem; 142 | } 143 | 144 | .social-logo { 145 | width: 1.25rem; 146 | height: 1.25rem; 147 | } 148 | } -------------------------------------------------------------------------------- /client/src/components/UploadSqlSchema/UploadSqlSchema.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useCallback } from 'react'; 2 | import Typography from '@mui/material/Typography'; 3 | import { useDropzone } from 'react-dropzone'; 4 | import SchemaVisualizer from '../NodeSchema/SchemaVisualizer'; 5 | 6 | import './uploadsqlschema.scss' // styles 7 | // import { useEffect } from 'react'; // for testing onDrop 8 | // import samplePgDump from '../algorithms/sample_pg_dump.sql'; // for testing onDrop 9 | import { useTheme } from '../../contexts/ThemeContext'; 10 | 11 | // Component for uploading SQL schema files and visualizing them 12 | // This component serves as the entry point for users to input their database schemas 13 | const UploadSqlSchema = () => { 14 | // State to store the contents of uploaded SQL files 15 | // Using an array allows for multiple file uploads 16 | const [sqlContents, setSqlContents] = useState([]); 17 | const { darkMode } = useTheme(); 18 | 19 | // Callback function to handle file drops 20 | // This function is memoized to prevent unnecessary re-renders 21 | // and to maintain consistent behavior across renders 22 | const onDrop = useCallback((acceptedFiles) => { 23 | acceptedFiles.forEach((file) => { 24 | const reader = new FileReader(); 25 | 26 | // Using FileReader allows us to read the contents of the file asynchronously 27 | // This prevents blocking the main thread with large file reads 28 | reader.onload = (e) => { 29 | setSqlContents((prevContents) => [...prevContents, e.target.result]); 30 | }; 31 | reader.readAsText(file); 32 | }); 33 | }, []); 34 | 35 | // Configuration for the dropzone 36 | // This setup allows for a better user experience by providing visual feedback 37 | // and restricting file types to SQL files 38 | const { getRootProps, getInputProps, isDragActive } = useDropzone({ 39 | onDrop, 40 | accept: { 41 | 'application/sql': ['.sql'], 42 | 'text/plain': ['.sql'], 43 | }, 44 | multiple: true, // Allowing multiple file uploads for batch processing 45 | }); 46 | 47 | return ( 48 |
49 | {/* Dropzone area for file uploads */} 50 | {/* The styling changes based on drag state and dark mode for better UX */} 51 |
52 | 53 | 54 | {isDragActive 55 | ? 'Drop SQL files here' 56 | : '+ Click or drag to add SQL files'} 57 | 58 |
59 | 60 | {/* SchemaVisualizer component to render the uploaded schema */} 61 | {/* This separation of concerns allows for modularity and easier maintenance */} 62 | 63 |
64 | ); 65 | }; 66 | 67 | export default UploadSqlSchema; -------------------------------------------------------------------------------- /client/src/components/UploadSqlSchema/UploadSqlSchema.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen, fireEvent, act } from '@testing-library/react'; 3 | import '@testing-library/jest-dom'; 4 | import UploadSqlSchema from './UploadSqlSchema'; 5 | import { ThemeProvider } from '../../contexts/ThemeContext'; 6 | 7 | // Mock the useDropzone hook 8 | jest.mock('react-dropzone', () => ({ 9 | useDropzone: () => ({ 10 | getRootProps: () => ({}), 11 | getInputProps: () => ({}), 12 | isDragActive: false, 13 | }), 14 | })); 15 | 16 | // Mock the SchemaVisualizer component 17 | jest.mock('../NodeSchema/SchemaVisualizer', () => ({ sqlContents }) => ( 18 |
19 | {sqlContents.map((content, index) => ( 20 |
{content}
21 | ))} 22 |
23 | )); 24 | 25 | const renderUploadSqlSchema = (darkMode = false) => { 26 | return render( 27 | 28 | 29 | 30 | ); 31 | }; 32 | 33 | describe('UploadSqlSchema Component', () => { 34 | test('renders UploadSqlSchema component', () => { 35 | renderUploadSqlSchema(); 36 | expect(screen.getByText('+ Click or drag to add SQL files')).toBeInTheDocument(); 37 | }); 38 | 39 | test('does not apply dark mode class when darkMode is false', () => { 40 | renderUploadSqlSchema(false); 41 | expect(screen.getByText('+ Click or drag to add SQL files').className).not.toContain('dark'); 42 | }); 43 | }); -------------------------------------------------------------------------------- /client/src/components/UploadSqlSchema/uploadsqlschema.scss: -------------------------------------------------------------------------------- 1 | @import '../../assets/styles/variables.scss'; 2 | 3 | @mixin color-opacity($property, $color, $opacity) { 4 | $r: red($color); 5 | $g: green($color); 6 | $b: blue($color); 7 | // border-bottom: 2px solid rgba($r, $g, $b, $opacity); 8 | #{$property}: rgba($r, $g, $b, $opacity); 9 | } 10 | 11 | .graph-container { 12 | position: relative; 13 | width: 100%; 14 | height: 100%; 15 | display: flex; 16 | flex-direction: column; 17 | flex-grow: 1; 18 | // @include border-opacity($color-black, 10%); 19 | border-width: 2px; 20 | border-style: solid; 21 | @include color-opacity(border-color, $color-black, 10%); 22 | // border: 5px solid red; 23 | background-color: $color-white; 24 | 25 | &.dark { 26 | background-color: $color-black; 27 | @include color-opacity(border-color, $color-white, 10%); 28 | } 29 | } 30 | 31 | // .overlay { 32 | // position: absolute; 33 | // z-index: 10; 34 | // inset: 0; 35 | // // background-color: $color-secondary; 36 | // @include color-opacity(background-color, $color-secondary, .8); 37 | // display: flex; 38 | // justify-content: center; 39 | // align-items: center; 40 | // } 41 | 42 | // .hidden { 43 | // inset: initial; 44 | // display: none; 45 | // } 46 | 47 | // .drop-box-container { 48 | // width: 600px; 49 | // height: 500px; 50 | // border-radius: .4em; 51 | // padding: 1em; 52 | // display: flex; 53 | // justify-content: center; 54 | // align-items: center; 55 | // background-color: $color-white; 56 | // // border: 10px solid green; 57 | // } 58 | 59 | // .drop-box { 60 | // cursor: 'pointer'; 61 | // width: 90%; 62 | // height: 90%; 63 | // display: flex; 64 | // align-items: center; 65 | // opacity: 1; 66 | // background-color: $color-white; 67 | // border: 2px dashed #1976d2 68 | // } 69 | 70 | // .drop-box-text { 71 | // display: inline-block; 72 | // width: 100%; 73 | // height: min-content; 74 | // text-align: center; 75 | // color: $color-black; 76 | // opacity: 1; 77 | // } 78 | 79 | .drop-box { 80 | cursor: pointer; 81 | height: 4rem; 82 | display: flex; 83 | justify-content: center; 84 | align-items: center; 85 | text-align: center; 86 | background-color: $color-white; 87 | border: 3px dashed #1976d2; 88 | 89 | .isDragActive { 90 | background-color: #e3f2fd; 91 | } 92 | 93 | &:hover { 94 | background-color: #e3f2fd; 95 | } 96 | } 97 | 98 | .drop-box { 99 | cursor: pointer; 100 | height: 4rem; 101 | display: flex; 102 | justify-content: center; 103 | align-items: center; 104 | text-align: center; 105 | background-color: $color-white; 106 | border: 3px dashed #1976d2; 107 | 108 | &.isDragActive { 109 | background-color: #e3f2fd; 110 | } 111 | 112 | &:hover { 113 | background-color: #e3f2fd; 114 | } 115 | 116 | &.dark { 117 | background-color: $color-black; 118 | border-color: #4dabf5; 119 | 120 | &.isDragActive, &:hover { 121 | background-color: #1e3a5f; 122 | } 123 | } 124 | } 125 | 126 | .drop-box-input { 127 | display: none; 128 | } 129 | 130 | .drop-box-text { 131 | color: $color-black; 132 | 133 | &.dark { 134 | color: $color-white; 135 | } 136 | } -------------------------------------------------------------------------------- /client/src/components/algorithms/resolver_generator.js: -------------------------------------------------------------------------------- 1 | import pluralize from 'pluralize'; 2 | 3 | 4 | //resolverGenerator function generates GraphQL resolver functions from current nodes and edges objects 5 | export default function resolverGenerator(nodes, edges) { 6 | //create aux objects to hold node relationships 7 | const oneToManyRelationships = {}; 8 | const manyToOneRelationships = {}; 9 | 10 | //loop through edges and add to relationship aux objects 11 | edges.forEach((edge) => { 12 | //if source node has no connections, create empty array to hold them 13 | if (manyToOneRelationships[edge.source] === undefined) { 14 | manyToOneRelationships[edge.source] = []; 15 | } 16 | //if target node has no connections, create empty array to hold them 17 | if (oneToManyRelationships[edge.target] === undefined) { 18 | oneToManyRelationships[edge.target] = []; 19 | } 20 | //push each connection onto aux objects 21 | manyToOneRelationships[edge.source].push({ 22 | targetNode: edge.target, 23 | targetField: edge.targetHandle, 24 | sourceField: edge.sourceHandle, 25 | dbTargetTable: edge.dbTargetTable, 26 | }); 27 | oneToManyRelationships[edge.target].push({ 28 | targetNode: edge.source, 29 | targetField: edge.sourceHandle, 30 | sourceField: edge.targetHandle, 31 | dbTargetTable: edge.dbSourceTable, 32 | }); 33 | }); 34 | //begin template resolver string 35 | let resolverString = `const resolvers = {\n Query: {\n`; 36 | 37 | //add resolvers for each GraphQL Object Type 38 | nodes.forEach((node) => { 39 | //add '_all' if plural form of word is same as singular form (species -> species_all) 40 | const pluralIsSingular = 41 | pluralize(node.id) === pluralize.singular(node.id) ? '_all' : ''; 42 | //add entry point for whole table 43 | resolverString += ` ${pluralize(node.id).replace( 44 | /^./, 45 | node.id[0].toLowerCase() 46 | )}${pluralIsSingular}() {\n return db.query('SELECT * FROM ${ 47 | node.dbTableName 48 | }').then((data) => data.rows);\n },\n`; 49 | 50 | //select primary key field for resolver queries 51 | const primaryKeyField = node.data.columns.fields.filter( 52 | (field) => field.name === node.primaryKey 53 | ); 54 | //add entry point for single element (by primary key) 55 | resolverString += ` ${pluralize 56 | .singular(node.id) 57 | .replace( 58 | /^./, 59 | node.id[0].toLowerCase() 60 | )}(_, args) {\n return db.query(\`SELECT * FROM ${ 61 | node.dbTableName 62 | } WHERE ${primaryKeyField[0].name} = '\${args.${ 63 | primaryKeyField[0].name 64 | }}'\`).then((data) => data.rows[0]);\n },\n`; 65 | }); 66 | //close off GraphQL Object Type string 67 | resolverString += ' },\n'; 68 | //add nested relationships to resolver string 69 | nodes.forEach((node) => { 70 | if ( 71 | manyToOneRelationships[node.id] !== undefined || 72 | oneToManyRelationships[node.id] !== undefined 73 | ) { 74 | //grab primary key for table 75 | const primaryKeyField = node.data.columns.fields.filter( 76 | (field) => field.name === node.primaryKey 77 | ); 78 | //add connection for each type if they exists 79 | resolverString += ` ${node.id}: {\n`; 80 | 81 | //add many to one relationships for current node 82 | if (manyToOneRelationships[node.id] !== undefined) { 83 | manyToOneRelationships[node.id].forEach((connection) => { 84 | resolverString += ` ${connection.targetNode.replace( 85 | /^./, 86 | connection.targetNode[0].toLowerCase() 87 | )}(parent) {\n`; 88 | resolverString += ` return db.query(\`SELECT ${connection.dbTargetTable}.* FROM ${connection.dbTargetTable} JOIN ${node.dbTableName} ON ${node.dbTableName}.${connection.sourceField} = ${connection.dbTargetTable}.${connection.targetField} WHERE ${node.dbTableName}.${primaryKeyField[0].name} = '\${parent.${primaryKeyField[0].name}}'\`).then((data) => data.rows[0]);\n },\n`; 89 | }); 90 | } 91 | //add one to many relationships for current node 92 | if (oneToManyRelationships[node.id] !== undefined) { 93 | oneToManyRelationships[node.id].forEach((connection) => { 94 | resolverString += ` ${pluralize(connection.targetNode).replace( 95 | /^./, 96 | connection.targetNode[0].toLowerCase() 97 | )}(parent) {\n`; 98 | resolverString += ` return db.query(\`SELECT ${connection.dbTargetTable}.* FROM ${connection.dbTargetTable} JOIN ${node.dbTableName} ON ${node.dbTableName}.${connection.sourceField} = ${connection.dbTargetTable}.${connection.targetField} WHERE ${node.dbTableName}.${primaryKeyField[0].name} = '\${parent.${primaryKeyField[0].name}}'\`).then((data) => data.rows);\n },\n`; 99 | }); 100 | } 101 | resolverString += ` },\n`; 102 | } 103 | }); 104 | resolverString += '}\n'; 105 | 106 | //output final resolver string as array for display and copying 107 | const resolverArray = resolverString.split('\n'); 108 | return resolverArray; 109 | } 110 | -------------------------------------------------------------------------------- /client/src/components/algorithms/schema_generator.js: -------------------------------------------------------------------------------- 1 | import pluralize from 'pluralize'; 2 | 3 | //schemaGenerator function generates GraphQL typeDefs from current nodes and edges objects 4 | export default function schemaGenerator(nodes, edges) { 5 | if (!nodes) { 6 | return 'No data available to generate schema.'; 7 | } 8 | //create aux objects to hold node relationships 9 | const oneToManyRelationships = {}; 10 | const manyToOneRelationships = {}; 11 | 12 | //for each edge between nodes 13 | edges.forEach((edge) => { 14 | //if source node has no connections, create empty array to hold them 15 | if (manyToOneRelationships[edge.source] === undefined) { 16 | manyToOneRelationships[edge.source] = []; 17 | } 18 | //if target node has no connections, create empty array to hold them 19 | if (oneToManyRelationships[edge.target] === undefined) { 20 | oneToManyRelationships[edge.target] = []; 21 | } 22 | //push each connection onto aux objects (node to node) 23 | manyToOneRelationships[edge.source].push(edge.target); 24 | oneToManyRelationships[edge.target].push(edge.source); 25 | }); 26 | 27 | let gql_schema = `#graphql\n`; 28 | 29 | let query_string = ` type Query {\n`; 30 | 31 | //for each node in current nodes object 32 | nodes.forEach((node) => { 33 | //select primary key field for query by primary key 34 | const primaryKeyField = node.data.columns.fields.filter( 35 | (field) => field.name === node.primaryKey 36 | ); 37 | //if plural/singular versions of id are the same, add 'All' to plural version 38 | const pluralIsSingular = 39 | pluralize(node.id) === pluralize.singular(node.id) ? '_all' : ''; 40 | //query for all elements in type (plural form) 41 | query_string += ` ${pluralize(node.id).replace( 42 | /^./, 43 | node.id[0].toLowerCase() 44 | )}${pluralIsSingular}: [${pluralize 45 | .singular(node.id) 46 | .replace(/^./, node.id[0].toUpperCase())}]\n`; 47 | 48 | //query for one element in type (by primary key - singular form) 49 | const required = primaryKeyField[0].required ? '!' : ''; 50 | query_string += ` ${pluralize 51 | .singular(node.id) 52 | .replace(/^./, node.id[0].toLowerCase())}(${primaryKeyField[0].name}: ${ 53 | primaryKeyField[0].type 54 | }${required}): ${pluralize 55 | .singular(node.id) 56 | .replace(/^./, node.id[0].toUpperCase())}\n`; 57 | 58 | //add type to schema string 59 | gql_schema += ` type ${pluralize 60 | .singular(node.id) 61 | .replace(/^./, node.id[0].toUpperCase())} {\n`; 62 | 63 | //add associated columns to GraphQL type 64 | for (let i = 0; i < node.data.columns.fields.length; i++) { 65 | //check if field is required 66 | const required = node.data.columns.fields[i].required ? '!' : ''; 67 | gql_schema += ` ${node.data.columns.fields[i].name}: ${node.data.columns.fields[i].type}${required}\n`; 68 | } 69 | 70 | //add current type's many to one relationships to schema 71 | if (manyToOneRelationships[node.id]) { 72 | manyToOneRelationships[node.id].forEach((connection) => { 73 | gql_schema += ` ${connection.replace( 74 | /^./, 75 | connection[0].toLowerCase() 76 | )}: ${connection}!\n`; 77 | }); 78 | } 79 | //add current type's one to many relationships to schema 80 | if (oneToManyRelationships[node.id]) { 81 | oneToManyRelationships[node.id].forEach((connection) => { 82 | gql_schema += ` ${pluralize(connection).replace( 83 | /^./, 84 | connection[0].toLowerCase() 85 | )}: [${connection}!]\n`; 86 | }); 87 | } 88 | //close open brackets 89 | gql_schema += ` }\n`; 90 | }); 91 | query_string += ` }\n`; 92 | 93 | //add query string to base schema and output as array for display and copying 94 | gql_schema += query_string; 95 | const testSchema = gql_schema.split('\n'); 96 | return testSchema; 97 | } 98 | -------------------------------------------------------------------------------- /client/src/components/algorithms/schema_parser.js: -------------------------------------------------------------------------------- 1 | const pluralize = require('pluralize'); 2 | 3 | //parseSqlSchema function parses pg_dump file and returns nodes and edges objects for file generation and display 4 | export function parseSqlSchema(sql) { 5 | //declare objects to hold tables and relationships 6 | const tables = {}; 7 | let currentTable; 8 | const relationships = []; 9 | let index = 0; 10 | 11 | //moreFields handles adding fields to GQL types 12 | let moreFields = false; 13 | //create mapping object to change SQL types to GQL types 14 | const typeMapper = { 15 | varchar: 'String', 16 | serial: 'ID', 17 | bigint: 'Int', 18 | DATE: 'String', 19 | integer: 'Int', 20 | character: 'String', 21 | }; 22 | try { 23 | // Split dump file and parse line by line 24 | const lineArray = sql.split(/\r?\n/); 25 | 26 | lineArray.forEach((line) => { 27 | // 'CREATE TABLE public' lines denote beginning of SQL table information 28 | if (line.startsWith('CREATE TABLE public')) { 29 | // Grab table name 30 | let tableName = line.match(/CREATE TABLE (\w+\.)?(\w+)/)[2]; 31 | currentTable = tableName; 32 | // Add tableName to tables object and assign template object to hold associated fields 33 | tables[currentTable] = { primaryKey: '', fields: [] }; 34 | moreFields = true; 35 | } else if (moreFields) { 36 | //if all fields have been added to table, set moreFields to false 37 | if (line.startsWith(')')) { 38 | moreFields = false; 39 | //while table still has more fields to add, continue adding 40 | } else { 41 | // Add each field to current table 42 | const lineArray = line.trim().split(' '); 43 | // Make sure field is valid 44 | if (lineArray.length >= 2) { 45 | const fieldName = lineArray[0].replace(/"/g, ''); 46 | //make sure field is not a primary key definition 47 | if (fieldName !== 'CONSTRAINT') { 48 | //remove trailing commas from non-null (required) fields to check if required 49 | const fieldType = typeMapper[lineArray[1].replace(/,/g, '')]; 50 | const required = line.toLowerCase().includes('not null'); 51 | // Add new field object to associated fields array on table object 52 | tables[currentTable].fields.push({ 53 | name: fieldName, 54 | type: fieldType, 55 | required: required, 56 | isForeignKey: '', 57 | }); 58 | } 59 | //if line is primary key definition, assign primary key to current node 60 | else { 61 | const match = line.match(/PRIMARY KEY \("?([^")]+)"?\)/); 62 | tables[currentTable].primaryKey = match[1]; 63 | } 64 | } 65 | } 66 | } 67 | // Grab relationships from alter tables (primary/foreign keys) 68 | else if (line.startsWith('ALTER TABLE')) { 69 | //standardize input 70 | line = line 71 | .replace('ALTER TABLE ONLY', 'ALTER TABLE') 72 | .replace(/"/g, ''); 73 | //only use public tables to link relationships 74 | if (line.startsWith('ALTER TABLE public')) { 75 | //if line has been split into two lines on import, concatenate them 76 | if (line.at(line.length - 1) !== ';') { 77 | //add next line to current 78 | line += lineArray[index + 1]; 79 | } 80 | //attempt to match line to relationship template 81 | const matchRelationship = line.match( 82 | /ALTER TABLE (\w+\.)?(\w+).*FOREIGN KEY \((\w+)\) REFERENCES (\w+\.)?(\w+)\((\w+)\)/ 83 | ); 84 | //attempt to match line to primary key template 85 | const matchPrimaryKey = line.match( 86 | /ALTER TABLE (\w+\.)?(\w+).*PRIMARY KEY \((\w+)\)/ 87 | ); 88 | //if line matches, store data 89 | if (matchRelationship) { 90 | const [ 91 | , 92 | sourceCheck, 93 | sourceTable, 94 | sourceField, 95 | targetCheck, 96 | targetTable, 97 | targetField, 98 | ] = matchRelationship; 99 | 100 | //change foreign key property on source field 101 | tables[sourceTable].fields.forEach((field) => { 102 | if (field.name === sourceField) 103 | field.isForeignKey = targetTable + '.' + targetField; 104 | }); 105 | 106 | //if both tables are public, push data onto relationships object 107 | if (sourceCheck === 'public.' && targetCheck === 'public.') { 108 | relationships.push({ 109 | source: sourceTable, 110 | sourceHandle: sourceField, 111 | target: targetTable, 112 | targetHandle: targetField, 113 | }); 114 | } 115 | //if line matches primary key template, assign primary key to current table 116 | } else if (matchPrimaryKey) { 117 | const [, tableCheck, tableName, pkField] = matchPrimaryKey; 118 | 119 | if (tableCheck === 'public.') { 120 | tables[tableName].primaryKey = pkField; 121 | } 122 | } 123 | } 124 | } 125 | index++; 126 | }); 127 | 128 | // Calculate grid layout from created nodes 129 | const gridLayout = (nodes, columns = 3, width = 250, height = 300) => { 130 | return nodes.map((node, index) => { 131 | const column = index % columns; 132 | const row = Math.floor(index / columns); 133 | return { 134 | ...node, 135 | position: { 136 | x: column * width, 137 | y: row * height, 138 | }, 139 | }; 140 | }); 141 | }; 142 | 143 | // Create nodes for React Flow display from tables object parsed from pg_dump file 144 | const nodes = gridLayout( 145 | Object.entries(tables).map(([tableName, columns]) => ({ 146 | id: pluralize 147 | .singular(tableName) 148 | .replace(/^./, tableName[0].toUpperCase()), 149 | type: 'table', 150 | dbTableName: tableName, 151 | primaryKey: columns.primaryKey, 152 | data: { 153 | label: pluralize 154 | .singular(tableName) 155 | .replace(/^./, tableName[0].toUpperCase()), 156 | columns: columns, 157 | }, 158 | })) 159 | ); 160 | 161 | // Create edges for React Flow display from relationships object parsed from pg_dump file 162 | const edges = relationships.map((rel) => ({ 163 | id: crypto.randomUUID(), 164 | source: pluralize 165 | .singular(rel.source) 166 | .replace(/^./, rel.source[0].toUpperCase()), 167 | sourceHandle: rel.sourceHandle, 168 | dbSourceTable: rel.source, 169 | target: pluralize 170 | .singular(rel.target) 171 | .replace(/^./, rel.target[0].toUpperCase()), 172 | targetHandle: rel.targetHandle, 173 | dbTargetTable: rel.target, 174 | type: 'smoothstep', 175 | animated: true, 176 | style: { stroke: '#fff' }, 177 | })); 178 | 179 | return { nodes, edges }; 180 | } catch (err) { 181 | const nodes = {}; 182 | const edges = {}; 183 | console.log('Unable to parse file'); 184 | alert('Unable to parse file'); 185 | return { nodes, edges }; 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /client/src/contexts/AuthContext.js: -------------------------------------------------------------------------------- 1 | import React, { createContext, useState, useEffect, useContext } from 'react'; 2 | // import { useNavigate } from 'react-router-dom'; 3 | import axios from 'axios'; 4 | 5 | export const AuthContext = createContext(); 6 | 7 | export const AuthProvider = ({ children }) => { 8 | // const navigate = useNavigate(); 9 | const [authState, setAuthState] = useState({ 10 | isAuth: false, 11 | username: '', 12 | userId: null, 13 | loading: true, // Add loading state 14 | }); 15 | 16 | useEffect(() => { 17 | async function verifyUser() { 18 | // check local storage 19 | const token = localStorage.getItem('token'); 20 | 21 | if (!token) { 22 | setAuthState((prev_state) => ({ ...prev_state, loading: false })); 23 | return; 24 | } 25 | 26 | if (!authState.loading && !authState.isAuth) { 27 | // navigate to login page -- handled in PrivateRoute Component 28 | } 29 | 30 | // verify user token 31 | let config = { 32 | headers: { authorization: `${token}` }, 33 | }; 34 | try { 35 | const response = await axios.post('/api/auth/protected', {}, config); 36 | if (response.status !== 200) { 37 | console.log('response.message:', response.message); 38 | console.log( 39 | 'Unable to verify user. Reponse status: ', 40 | response.status 41 | ); 42 | setAuthState({ 43 | isAuth: false, 44 | username: '', 45 | userId: null, 46 | loading: false, // Set loading to false when done 47 | }); 48 | // TODO - create refresh token 49 | } else { 50 | // successful login 51 | const data = response.data; 52 | localStorage.setItem('username', data.username); 53 | localStorage.setItem('userId', data.userId); 54 | localStorage.setItem('token', response.headers['authorization']); 55 | setAuthState({ 56 | isAuth: true, 57 | username: data.username, 58 | userId: data.userId, 59 | loading: false, // Set loading to false when done 60 | }); 61 | } 62 | } catch (err) { 63 | if (err.response) { 64 | // The request was made and the server responded with a status code 65 | // that falls out of the range of 2xx 66 | if (err.response.status === 401) { 67 | console.log('Error verifying user token'); 68 | setAuthState({ 69 | isAuth: false, 70 | username: '', 71 | userId: null, 72 | loading: false, 73 | }); 74 | localStorage.removeItem('username'); 75 | localStorage.removeItem('userId'); 76 | localStorage.removeItem('token'); 77 | } 78 | console.log('Error response data:', err.response.data); 79 | console.log('Error response status:', err.response.status); 80 | } else if (error.request) { 81 | // The request was made but no response was received 82 | console.log('Error request:', err.request); 83 | } else { 84 | // Something happened in setting up the request that triggered an Error 85 | console.log('Error message:', err.message); 86 | } 87 | // return navigate('/login'); 88 | } 89 | } 90 | verifyUser(); 91 | }, [authState.loading]); 92 | 93 | return ( 94 | 95 | {children} 96 | 97 | ); 98 | }; 99 | 100 | export const useAuth = () => { 101 | return useContext(AuthContext); 102 | }; 103 | -------------------------------------------------------------------------------- /client/src/contexts/GraphContext.jsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useContext, useState } from 'react'; 2 | 3 | export const GraphContext = createContext(); 4 | 5 | export const GraphProvider = ({ children }) => { 6 | const [ graphName, setGraphName ] = useState(''); 7 | const [ graphId, setGraphId] = useState(null); 8 | const [ graphList, setGraphList ] = useState([]); 9 | 10 | return ( 11 | 12 | { children } 13 | 14 | ) 15 | } 16 | 17 | export const useGraphContext = () => { 18 | return useContext(GraphContext); 19 | } -------------------------------------------------------------------------------- /client/src/contexts/ThemeContext.js: -------------------------------------------------------------------------------- 1 | import React, { createContext, useState, useContext } from 'react'; 2 | 3 | const ThemeContext = createContext(); 4 | // Dark Mode Features 5 | export const ThemeProvider = ({ children }) => { 6 | const [darkMode, setDarkMode] = useState(false); 7 | 8 | const toggleDarkMode = () => { 9 | setDarkMode(prevMode => !prevMode); 10 | }; 11 | 12 | return ( 13 | 14 | {children} 15 | 16 | ); 17 | }; 18 | 19 | export const useTheme = () => useContext(ThemeContext); -------------------------------------------------------------------------------- /client/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import './assets/styles/globalstyles.scss'; 4 | import App from './App.jsx'; 5 | 6 | ReactDOM.createRoot(document.getElementById('root')).render( 7 | 8 | 9 | 10 | ); 11 | -------------------------------------------------------------------------------- /client/src/index.scss: -------------------------------------------------------------------------------- 1 | @import './assets/styles/variables.scss'; 2 | 3 | body { 4 | transition: background-color 0.3s, color 0.3s; 5 | } 6 | 7 | body.dark { 8 | // background-color: #121212; 9 | // color: #ffffff; 10 | background-color: $color-black; 11 | color: $color-white; 12 | } 13 | 14 | .navbar { 15 | display: flex; 16 | justify-content: space-between; 17 | padding: 1rem; 18 | // background-color: #f8f9fa; 19 | background-color: $color-white; 20 | transition: background-color 0.3s; 21 | } 22 | 23 | .navbar.dark { 24 | background-color: $color-black; 25 | } -------------------------------------------------------------------------------- /client/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const HtmlWebPackPlugin = require("html-webpack-plugin"); 3 | const Dotenv = require("dotenv-webpack"); 4 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 5 | const BundleAnalyzerPlugin = 6 | require("webpack-bundle-analyzer").BundleAnalyzerPlugin; 7 | 8 | module.exports = { 9 | entry: "./src/index.js", 10 | mode: process.env.NODE_ENV === "production" ? "production" : "development", 11 | output: { 12 | filename: "[name].[contenthash].js", 13 | path: path.resolve(__dirname, "build"), 14 | publicPath: "/", 15 | }, 16 | module: { 17 | rules: [ 18 | { 19 | test: /\.sql$/, 20 | use: "raw-loader", 21 | }, 22 | { 23 | test: /\.jsx?/, 24 | exclude: /node_modules/, 25 | use: { 26 | loader: "babel-loader", 27 | options: { 28 | presets: ["@babel/preset-env", "@babel/preset-react"], 29 | }, 30 | }, 31 | }, 32 | { 33 | test: /\.s?css$/, 34 | use: [MiniCssExtractPlugin.loader, "css-loader", "sass-loader"], 35 | }, 36 | { 37 | test: /\.(png|jpg|gif|svg)$/, 38 | use: [ 39 | "file-loader", 40 | { 41 | loader: "image-webpack-loader", 42 | options: { 43 | mozjpeg: { 44 | progressive: true, 45 | quality: 65, 46 | }, 47 | }, 48 | }, 49 | ], 50 | }, 51 | ], 52 | }, 53 | resolve: { 54 | extensions: [".js", ".jsx"], 55 | }, 56 | plugins: [ 57 | new HtmlWebPackPlugin({ 58 | template: "./public/index.html", 59 | favicon: "./public/smallLogo.png", 60 | }), 61 | new Dotenv({ 62 | path: path.resolve(__dirname, "../server/.env"), 63 | }), 64 | new MiniCssExtractPlugin({ 65 | filename: "[name].[contenthash].css", 66 | }), 67 | process.env.ANALYZE && new BundleAnalyzerPlugin(), 68 | ].filter(Boolean), 69 | devServer: { 70 | historyApiFallback: true, 71 | static: { 72 | directory: path.resolve(__dirname, "build"), 73 | publicPath: "/", 74 | }, 75 | port: 8000, 76 | proxy: [ 77 | { 78 | context: ["/api"], 79 | target: "http://localhost:3000", 80 | }, 81 | ], 82 | }, 83 | optimization: { 84 | splitChunks: { 85 | chunks: "all", 86 | maxInitialRequests: Infinity, 87 | minSize: 0, 88 | cacheGroups: { 89 | vendor: { 90 | test: /[\\/]node_modules[\\/]/, 91 | name(module) { 92 | const packageName = module.context.match( 93 | /[\\/]node_modules[\\/](.*?)([\\/]|$)/ 94 | )[1]; 95 | return `npm.${packageName.replace("@", "")}`; 96 | }, 97 | }, 98 | }, 99 | }, 100 | }, 101 | performance: { 102 | hints: "warning", 103 | maxEntrypointSize: 512000, 104 | maxAssetSize: 512000, 105 | }, 106 | }; 107 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "moleqlar", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "heroku-postbuild": "NPM_CONFIG_PRODUCTION=false npm install --prefix client && npm run build --prefix client && npm install --prefix server", 6 | "build": "cd client && npm install && npm run build", 7 | "start": "cd server && npm install && npm start", 8 | "clean": "rm -rf /client/build" 9 | }, 10 | "engines": { 11 | "node": "16.x" 12 | }, 13 | "dependencies": { 14 | "webpack-cli": "^5.1.4" 15 | }, 16 | "devDependencies": { 17 | "image-webpack-loader": "^8.1.0", 18 | "mini-css-extract-plugin": "^2.9.0", 19 | "webpack-bundle-analyzer": "^4.10.2" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /server/__tests__/authRouter.test.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest'); 2 | const express = require('express'); 3 | const authRouter = require('../routes/authRouter'); // Adjust the path as necessary 4 | const userController = require('../controllers/userController'); 5 | 6 | const app = express(); 7 | app.use(express.json()); // Middleware to parse JSON 8 | app.use('/api/auth', authRouter); 9 | 10 | // Mock userController functions 11 | jest.mock('../controllers/userController', () => ({ 12 | hashing: (req, res, next) => next(), 13 | createUser: (req, res, next) => { 14 | res.locals.user = { username: 'testuser', id: 1 }; 15 | next(); 16 | }, 17 | signJWT: (req, res, next) => { 18 | res.locals.user.token = 'fake-jwt-token'; 19 | next(); 20 | }, 21 | loginUser: (req, res, next) => { 22 | res.locals.user = { username: 'testuser', id: 1, token: 'fake-jwt-token' }; 23 | next(); 24 | }, 25 | validateJWT: (req, res, next) => { 26 | res.locals.user = { username: 'testuser', id: 1 }; 27 | next(); 28 | } 29 | })); 30 | 31 | describe('Auth Router', () => { 32 | // Test Creating new user and if user object with token 33 | describe('POST /signup', () => { 34 | it('should create a new user and return user object with token', async () => { 35 | const response = await request(app) 36 | .post('/api/auth/signup') 37 | .send({ username: 'testuser', password: 'password123' }); 38 | 39 | expect(response.status).toBe(200); 40 | expect(response.body).toHaveProperty('username', 'testuser'); 41 | expect(response.body).toHaveProperty('token', 'fake-jwt-token'); 42 | }); 43 | }); 44 | // Test user login and return user object with token 45 | describe('POST /login', () => { 46 | it('should login user and return user object with token', async () => { 47 | const response = await request(app) 48 | .post('/api/auth/login') 49 | .send({ username: 'testuser', password: 'password123' }); 50 | 51 | expect(response.status).toBe(200); 52 | expect(response.body).toHaveProperty('username', 'testuser'); 53 | expect(response.body).toHaveProperty('token', 'fake-jwt-token'); 54 | }); 55 | }); 56 | // Test if user has been authenticated and accessible to proceed on protected routes 57 | describe('POST /protected', () => { 58 | it('should access protected route with valid token', async () => { 59 | const response = await request(app) 60 | .post('/api/auth/protected') 61 | .set('Authorization', 'Bearer fake-jwt-token') 62 | .send(); 63 | 64 | expect(response.status).toBe(200); 65 | expect(response.body).toHaveProperty('username', 'testuser'); 66 | }); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /server/controllers/graphController.js: -------------------------------------------------------------------------------- 1 | const db = require('../models/userModels'); 2 | 3 | const graphController = {}; 4 | 5 | graphController.createGraph = async (req, res, next) => { 6 | // create new graph in database 7 | const { userId, graphName } = req.body; 8 | 9 | if (!userId || !graphName) { 10 | return res.status(422).json({ error: 'Missing required parameters' }); 11 | } 12 | const nodeString = '', edgeString = ''; // initialize as empty 13 | 14 | const params = [userId, graphName, nodeString, edgeString]; 15 | const query = ` 16 | INSERT INTO graphs(user_id, graph_name, nodes, edges) 17 | VALUES ($1, $2, $3, $4) 18 | RETURNING *`; 19 | try { 20 | const newGraph = await db.query(query, params); 21 | // success 22 | res.locals.user.graphId = newGraph.rows[0].graph_id; 23 | res.locals.user.graphName = newGraph.rows[0].graph_name; 24 | return next(); 25 | } catch (err) { 26 | // fail 27 | console.log(err); 28 | return next({ 29 | log: 'Error in graphController.createGraph', 30 | message: `Unable to create new graph for user ${res.locals.user.username} and graph name ${graphName}`, 31 | status: 409, 32 | }); 33 | } 34 | } 35 | 36 | graphController.getGraph = async (req, res, next) => { 37 | // query database for requested graph 38 | const { userId, graphId } = req.params; 39 | // Check if parameters are undefined 40 | if (!userId || !graphId) { 41 | return res.status(422).json({ error: 'Missing required parameters' }); 42 | } 43 | 44 | const params = [graphId, userId]; 45 | const query = `SELECT * FROM graphs WHERE graph_id = $1 AND user_id = $2` 46 | try { 47 | const graph = await db.query(query, params); 48 | // success 49 | res.locals.user.graphId = graph.rows[0].graph_id; 50 | res.locals.user.graphName = graph.rows[0].graph_name; 51 | res.locals.user.nodes = graph.rows[0].nodes; 52 | res.locals.user.edges = graph.rows[0].edges; 53 | return next(); 54 | } catch (err) { 55 | // fail 56 | console.log(err); 57 | return next({ 58 | log: 'Error in graphController.getGraph', 59 | message: 'Unable to retrieve graph from database', 60 | status: 500, 61 | }) 62 | } 63 | } 64 | 65 | graphController.getGraphList = async (req, res, next) => { 66 | // get userId from res.locals 67 | const { userId } = res.locals.user; 68 | 69 | const params = [ userId ]; 70 | const query = `SELECT * FROM graphs WHERE user_id = $1`; 71 | 72 | try { 73 | const dbResponse = await db.query(query, params); 74 | const graphList = []; 75 | let row; 76 | for (let i = 0; i < dbResponse.rows.length; i++) { 77 | row = dbResponse.rows[i]; 78 | graphList.push({ 79 | graph_id: row['graph_id'], 80 | graph_name: row['graph_name'], 81 | // TODO - insert graph picture here 82 | }) 83 | } 84 | res.locals.user.graphList = graphList; 85 | } catch (err) { 86 | console.log(err); 87 | return next({ 88 | log: 'Error in graphController.getGraphList', 89 | message: `Unable to pull graphlist for userId: ${userId}`, 90 | status: 500, 91 | }); 92 | } 93 | 94 | return next(); 95 | } 96 | 97 | graphController.saveGraph = async (req, res, next) => { 98 | // update graph in database 99 | // destructure request payload 100 | const { userId, graphName, graphId, nodes, edges } = req.body; 101 | // check if thee parameters are undefined 102 | if (!userId || !graphName || !graphId || !nodes || !edges) { 103 | return res.status(422).json({ error: 'Missing required parameters' }); 104 | } 105 | // Update the database 106 | const params = [graphName, nodes, edges, graphId, userId]; 107 | const query = ` 108 | UPDATE graphs 109 | SET graph_name = $1, nodes = $2, edges = $3 110 | WHERE graph_id = $4 AND user_id = $5 111 | RETURNING *` 112 | try { 113 | const updatedGraph = await db.query(query, params); 114 | // success 115 | res.locals.user.userId = updatedGraph.rows[0].user_id; 116 | res.locals.user.graphName = updatedGraph.rows[0].graph_name; 117 | res.locals.user.graphId = updatedGraph.rows[0].graph_id; 118 | res.locals.user.nodes = updatedGraph.rows[0].nodes; 119 | res.locals.user.edges = updatedGraph.rows[0].edges; 120 | return next(); 121 | } catch (err) { 122 | console.log(err); 123 | return next({ 124 | log: 'Error in graphController.saveGraph', 125 | message: 'Unable to update graph in database', 126 | status: 500, 127 | }) 128 | } 129 | } 130 | 131 | module.exports = graphController 132 | -------------------------------------------------------------------------------- /server/controllers/userController.js: -------------------------------------------------------------------------------- 1 | const bcrypt = require('bcryptjs'); // Import bcryptjs library for hashing passwords 2 | const saltRounds = 10; // Define number of salt rounds for hashing 3 | const db = require('../models/userModels'); // Import database models for querying 4 | 5 | const jwt = require('jsonwebtoken'); // Import jsonwebtoken library for JWT creation/validation 6 | const JWT_SECRET = process.env.JWT_SECRET; // Define a secret key for signing JWTs 7 | const userController = {}; // Initialize an empty userController object 8 | 9 | // Middleware for hashing the password 10 | userController.hashing = async (req, res, next) => { 11 | const { password } = req.body; // Extract password from request body 12 | try { 13 | const salt = await bcrypt.genSalt(saltRounds); // Generate salt with defined rounds 14 | const hashWord = await bcrypt.hash(password, salt); // Hash the password using the generated salt 15 | res.locals.hashWord = hashWord; // Store the hashed password in res.locals for later use 16 | return next(); // Proceed to the next middleware 17 | } catch (err) { 18 | return next({ 19 | // Error handling 20 | log: 'Error in userController.hashing', 21 | message: { err: 'Error hashing password' }, 22 | }); 23 | } 24 | }; 25 | 26 | // Middleware for creating a new user 27 | userController.createUser = async (req, res, next) => { 28 | const { username, email } = req.body; // Extract username and email from request body 29 | const hashWord = res.locals.hashWord; // Retrieve hashed password from res.locals 30 | 31 | // Insert credentials into database 32 | const params = [username, hashWord, email]; // Define parameters for SQL query 33 | const query = `INSERT INTO users(username, password, email) VALUES ($1, $2, $3) RETURNING *`; // Define SQL query string 34 | db.query(query, params) // Execute SQL query with parameters 35 | .then((createdUser) => { 36 | // Handle successful user creation 37 | res.locals.user = { 38 | ...createdUser.rows[0], // Store created user information in res.locals 39 | userId: createdUser.rows[0].user_id, 40 | }; 41 | return next(); // Proceed to the next middleware 42 | }) 43 | .catch((err) => { 44 | // Error handling 45 | console.log(err); 46 | return next({ 47 | log: 'Error in userController.createUser', 48 | status: 500, 49 | message: { err: 'Error creating user' }, 50 | }); 51 | }); 52 | }; 53 | 54 | // Middleware for logging in a user 55 | userController.loginUser = async (req, res, next) => { 56 | const { username, password } = req.body; // Extract username and password from request body 57 | const loginQuery = `SELECT * FROM users WHERE username = '${username}'`; // Define SQL query string 58 | 59 | // Error checking for missing username or password 60 | if (!username || !password) { 61 | return next({ 62 | log: 'Error in userController.loginUser, username and password are required', 63 | message: 'username and password are required', 64 | status: 400, 65 | }); 66 | } 67 | 68 | // Validate credentials 69 | try { 70 | const result = await db.query(loginQuery); // Query database for user 71 | const dbPassword = result.rows[0].password; // Save password from database 72 | 73 | // Validate password 74 | const isMatch = await bcrypt.compare(password, dbPassword); // Compare provided password with stored hash 75 | if (!isMatch) { 76 | // Invalid credentials 77 | return res.status(401).json({ message: 'Invalid username or password' }); 78 | } 79 | 80 | // Send back user info to the client 81 | res.locals.user = { 82 | username: username, 83 | userId: result.rows[0].user_id, 84 | }; 85 | return next(); 86 | } catch (err) { 87 | console.log(err); 88 | return next({ 89 | log: `Error in userController.loginUser, ${err}`, 90 | message: { err: 'Error occurred in post request to /login' }, 91 | status: 500, 92 | }); 93 | } 94 | }; 95 | 96 | // Middleware for signing a JWT 97 | userController.signJWT = (req, res, next) => { 98 | // console.log("userController.signJWT - signing JWT"); 99 | 100 | // Generate JWT 101 | // State 102 | const state = { 103 | ...res.locals.user, // Include user information in the state 104 | }; 105 | 106 | // Sign token 107 | const token = jwt.sign(state, JWT_SECRET, { expiresIn: '1h' }); // Create a JWT with the state and secret key 108 | 109 | // Add token to response header 110 | res.setHeader('authorization', `Bearer ${token}`); // Add the token to the response header 111 | return next(); // Proceed to the next middleware 112 | }; 113 | 114 | // Middleware for validating a JWT 115 | userController.validateJWT = async (req, res, next) => { 116 | // console.log('userController.validateJWT'); 117 | 118 | // Check that JWT exists in client's local storage 119 | const token = req.headers['authorization'].replace('Bearer ', ''); // Extract token from authorization header 120 | if (token === 'null' && !JSON.parse(token)) { 121 | console.log('No token found'); 122 | // Denied - no token 123 | res.locals.user = null; 124 | return res.status(401).json({ message: 'No token, authorization denied' }); 125 | } 126 | 127 | // Check that token is valid 128 | try { 129 | const decoded = jwt.verify(token, JWT_SECRET); // Verify the token using the secret key 130 | 131 | // Success - send username back to the client 132 | res.locals.user = { 133 | username: decoded.username, 134 | userId: decoded.userId, 135 | }; 136 | return next(); 137 | } catch (err) { 138 | console.log('userController - err.name:', err.name); 139 | 140 | // Handle specific JWT errors 141 | if (err.name === 'TokenExpiredError') { 142 | return res 143 | .status(401) 144 | .json({ message: 'Token expired, authorization denied' }); 145 | } else if (err.name === 'JsonWebTokenError') { 146 | return res 147 | .status(401) 148 | .json({ message: 'Invalid token, authorization denied' }); 149 | } else { 150 | return res.status(500).json({ message: 'Internal server error' }); 151 | } 152 | } 153 | }; 154 | 155 | module.exports = userController; // Export userController object 156 | -------------------------------------------------------------------------------- /server/models/User.js: -------------------------------------------------------------------------------- 1 | const { Sequelize, DataTypes } = require('sequelize'); 2 | const sequelize = new Sequelize('database', 'username', 'password', { 3 | host: 'localhost', 4 | dialect: 'postgres', 5 | }); 6 | 7 | 8 | const User = sequelize.define('User', { 9 | firstName: { 10 | type: DataTypes.STRING, 11 | allowNull: false, 12 | }, 13 | lastName: { 14 | type: DataTypes.STRING, 15 | allowNull: false, 16 | }, 17 | username: { 18 | type: DataTypes.STRING, 19 | allowNull: false, 20 | unique: true, 21 | }, 22 | password: { 23 | type: DataTypes.STRING, 24 | allowNull: false, 25 | }, 26 | }); 27 | 28 | 29 | module.exports = User; 30 | -------------------------------------------------------------------------------- /server/models/userModels.js: -------------------------------------------------------------------------------- 1 | const { Pool } = require("pg"); 2 | 3 | const PG_URI = process.env.SQL_DATABASE_URI; 4 | 5 | const pool = new Pool({ 6 | connectionString: PG_URI, 7 | ssl: { 8 | rejectUnauthorized: false, 9 | }, 10 | }); 11 | 12 | // export an object with query property that returns an invocation of pool.query 13 | // require it in controller to be the access point to our database 14 | module.exports = { 15 | query: (text, params, callback) => { 16 | // console.log('executed query', text); 17 | return pool.query(text, params, callback); 18 | }, 19 | testConnection: async () => { 20 | try { 21 | await pool.query("SELECT 1"); 22 | console.log("Successfully connected to the database"); 23 | } catch (err) { 24 | console.error("Error connecting to the database", err); 25 | } 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "1.0.0", 4 | "main": "server.js", 5 | "scripts": { 6 | "start": "node server.js", 7 | "dev": "nodemon server.js", 8 | "test": " jest __tests__/**/*.test.js", 9 | "serve-client": "serve -s ../client/build", 10 | "concurrently-dev": "concurrently \"npm run dev\" \"npm run serve-client\"" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "license": "ISC", 15 | "dependencies": { 16 | "bcryptjs": "^2.4.3", 17 | "body-parser": "^1.20.2", 18 | "chai": "^5.1.1", 19 | "cors": "^2.8.5", 20 | "cross-env": "^7.0.3", 21 | "dotenv": "^16.4.5", 22 | "dotenv-webpack": "^8.1.0", 23 | "esm": "^3.2.25", 24 | "express": "^4.19.2", 25 | "jsonwebtoken": "^9.0.2", 26 | "pg": "^8.12.0", 27 | "pg-hstore": "^2.3.4", 28 | "pluralize": "^8.0.0", 29 | "postgres": "^3.4.4", 30 | "sequelize": "^6.37.3" 31 | }, 32 | "devDependencies": { 33 | "@babel/cli": "^7.24.7", 34 | "@babel/core": "^7.24.7", 35 | "babel-loader": "^9.1.3", 36 | "concurrently": "^8.2.2", 37 | "cross-env": "^7.0.3", 38 | "css-loader": "^7.1.2", 39 | "file-loader": "^6.2.0", 40 | "identity-obj-proxy": "^3.0.0", 41 | "jest": "^29.7.0", 42 | "mocha": "^10.6.0", 43 | "nodemon": "^3.1.3", 44 | "sass": "^1.77.5", 45 | "sass-loader": "^14.2.1", 46 | "serve": "^14.2.3", 47 | "style-loader": "^4.0.0", 48 | "supertest": "^7.0.0", 49 | "webpack": "^5.92.0", 50 | "webpack-cli": "^5.1.4", 51 | "webpack-dev-server": "^5.0.4" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /server/routes/authRouter.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const authRouter = express.Router(); 3 | 4 | const userController = require('../controllers/userController.js'); 5 | 6 | // signup route 7 | authRouter.post( 8 | '/signup', 9 | userController.hashing, 10 | userController.createUser, 11 | userController.signJWT, 12 | (req, res) => { 13 | // console.log('Reached /api/auth/signup'); 14 | return res.status(200).json(res.locals.user); 15 | } 16 | ); 17 | 18 | // login route 19 | authRouter.post( 20 | '/login', 21 | userController.loginUser, 22 | userController.signJWT, 23 | (req, res) => { 24 | // console.log('Reached /api/auth/login'); 25 | return res.status(200).json(res.locals.user); 26 | } 27 | ); 28 | 29 | authRouter.post( 30 | '/protected', 31 | userController.validateJWT, 32 | userController.signJWT, 33 | (req, res) => { 34 | // console.log('Reached /api/auth/protected'); 35 | // success - valid token 36 | return res.status(200).json(res.locals.user); 37 | } 38 | ); 39 | 40 | module.exports = authRouter; 41 | -------------------------------------------------------------------------------- /server/routes/graphRouter.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const graphRouter = express.Router(); 3 | 4 | const userController = require('../controllers/userController'); 5 | const graphController = require('../controllers/graphController'); 6 | 7 | 8 | graphRouter.get( 9 | '/:userId', 10 | userController.validateJWT, 11 | graphController.getGraphList, 12 | (req, res, next) => { 13 | // return all graphs in database with :userid -- expect from dashboard page 14 | // console.log('Reached GET /api/graph/:userId'); 15 | return res.status(200).json(res.locals.user); 16 | } 17 | ); 18 | 19 | 20 | graphRouter.post( 21 | '/:userId', 22 | userController.validateJWT, 23 | graphController.createGraph, 24 | (req, res, next) => { 25 | // console.log('Reached POST /api/graph/:userId'); 26 | // payload inclues username, user_id, graph_name, graph_id 27 | return res.status(201).json(res.locals.user); // return new graphId 28 | } 29 | ); 30 | 31 | 32 | graphRouter.put( 33 | '/:userId/:graphId', 34 | userController.validateJWT, 35 | graphController.saveGraph, 36 | (req, res, next) => { 37 | // save updated graph 38 | // req.body inclues username, userId, graphName, graphId, nodes, edges 39 | // console.log('Reached PUT /api/graph/:userId/:graphId'); 40 | return res.status(200).json(res.locals.user); 41 | } 42 | ); 43 | 44 | 45 | graphRouter.get( 46 | '/:userId/:graphId', 47 | userController.validateJWT, 48 | graphController.getGraph, 49 | (req, res, next) => { 50 | // retrieve specific graph 51 | // console.log('Reached GET /api/graph/:userId/:graphId'); 52 | // payload inclues username, userId, graphName, graphId, nodes, edges 53 | return res.status(200).json(res.locals.user); 54 | } 55 | ); 56 | 57 | module.exports = graphRouter; 58 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const express = require('express'); 3 | const app = express(); 4 | const bodyParser = require('body-parser'); 5 | const cors = require('cors'); 6 | 7 | app.use(cors()); 8 | require('dotenv').config(); 9 | 10 | app.use(bodyParser.urlencoded({ extended: true })); 11 | app.use(bodyParser.json()); 12 | 13 | const PORT = process.env.PORT || 3000; 14 | 15 | // static files 16 | app.use(express.static(path.join(__dirname, '../client/build'))); 17 | 18 | // Authorization Route 19 | const authRouter = require('./routes/authRouter'); 20 | app.use('/api/auth', authRouter); 21 | 22 | // Graph Routes 23 | const graphRouter = require('./routes/graphRouter'); 24 | app.use('/api/graph', graphRouter); 25 | 26 | // catch-all route 27 | app.get('*', (req, res) => { 28 | res.sendFile(path.join(__dirname, '../client/build')); 29 | }); 30 | 31 | // Global error handler: 32 | app.use((err, req, res, next) => { 33 | // console.log("GLOBAL ERROR HANDLER:", err); 34 | const defaultErr = { 35 | log: 'Express error handler caught middleware error', 36 | status: 500, 37 | message: { err: 'An error occurred' }, 38 | }; 39 | const errorObj = Object.assign({}, defaultErr, err); 40 | console.log(errorObj.log); 41 | return res.status(errorObj.status).json(errorObj.message); 42 | }); 43 | 44 | app.listen(PORT, () => { 45 | // console.log(`listening on port ${PORT}`); 46 | }); 47 | 48 | module.exports = app; 49 | --------------------------------------------------------------------------------