├── .dockerignore ├── .env ├── .gitignore ├── .vscode ├── launch.json └── tasks.json ├── Dockerfile ├── LICENSE ├── README.md ├── __mocks__ ├── fileMock.js └── styleMock.js ├── __tests__ ├── backendTesting │ └── backend.test.js ├── endToEndTesting │ └── puppeteer.test.js └── frontendTesting │ └── login.test.js ├── babel.config.js ├── dist └── index.html ├── docker-compose.yml ├── jest-setup.js ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── client │ ├── App.js │ ├── assets │ │ ├── Cavin.jpg │ │ ├── Jordan.jpg │ │ ├── Kelly.jpg │ │ ├── Rebecca.png │ │ └── VisiQLLogo.png │ ├── components │ │ ├── About.js │ │ ├── DBInput.js │ │ ├── DeleteProject.js │ │ ├── Documentation.js │ │ ├── EditorPopOut.tsx │ │ ├── EditorPopOutHandler.tsx │ │ ├── GraphiQLPlayground.js │ │ ├── Homepage.tsx │ │ ├── Login.js │ │ ├── Navbar.tsx │ │ ├── NotSignedIn.js │ │ ├── ProjectDeleted.js │ │ ├── ProjectSaved.js │ │ ├── ProjectToolbar.js │ │ ├── ProjectUpdated.js │ │ ├── ProjectsGrid.js │ │ ├── ProjectsPage.js │ │ ├── SaveProject.js │ │ ├── SchemaContainer.tsx │ │ ├── Team.js │ │ ├── TeamCards.tsx │ │ ├── Tree.js │ │ ├── TreePopOutHandler.js │ │ ├── UpdateProject.js │ │ └── VisualizerContainer.tsx │ ├── index.html │ ├── index.js │ └── scss │ │ ├── _index.scss │ │ ├── _variables.scss │ │ ├── about.scss │ │ ├── application.scss │ │ ├── graphiql.scss │ │ └── login.scss ├── react.app.env.d.ts └── server │ ├── controllers │ ├── authController.js │ ├── dbLinkController.js │ ├── dbSchemaController.js │ ├── fnKeyController.js │ ├── mutationController.js │ ├── projectController.js │ ├── resolverController.js │ ├── schemaGen.js │ ├── testController.js │ ├── treeController.js │ └── userController.js │ ├── models │ ├── models.js │ ├── starwarsModel.js │ └── userModel.js │ ├── routes │ ├── dbLink.ts │ ├── graphqlRouter.ts │ ├── projectRouter.ts │ ├── resolvers.js │ ├── schema.js │ └── userRouter.ts │ ├── server.ts │ └── testDB.js ├── tsconfig.json └── webpack.config.js /.dockerignore: -------------------------------------------------------------------------------- 1 | **/.classpath 2 | **/.dockerignore 3 | # **/.env 4 | **/.git 5 | **/.gitignore 6 | **/.project 7 | **/.settings 8 | **/.toolstarget 9 | **/.vs 10 | **/.vscode 11 | **/*.*proj.user 12 | **/*.dbmdl 13 | **/*.jfm 14 | **/charts 15 | **/docker-compose* 16 | **/compose* 17 | **/Dockerfile* 18 | **/node_modules 19 | **/npm-debug.log 20 | **/obj 21 | **/secrets.dev.yaml 22 | **/values.dev.yaml 23 | LICENSE 24 | README.md -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | 2 | USER_URI="postgres://wjfvpxpy:WeVwrwbydep-z4f0h0-LbBjq-CUxyyBx@peanut.db.elephantsql.com/wjfvpxpy" 3 | ACCESS_TOKEN_SERVER="563f7b0ca10ba7fb67363fa821787c1a411d8040cadf942fc4869564001c67473583e998752fc739b5c4d6f6f8e59bc2404513c1902dd5b408128a4145805bf6" 4 | REFRESH_TOKEN_SERVER="587f66f3c640112d1bbb1e985fad06e8247be4842a1c6e71f0d21b971feb4b769312e481925b3ec80634d16c996e456295d6a14f89aa3b9a7e29e0362a0b7370" 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .env 3 | dist/ 4 | dist 5 | .env -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "name": "Docker Node.js Launch", 5 | "type": "docker", 6 | "request": "launch", 7 | "preLaunchTask": "docker-run: debug", 8 | "platform": "node" 9 | } 10 | ] 11 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "docker-build", 6 | "label": "docker-build", 7 | "platform": "node", 8 | "dockerBuild": { 9 | "dockerfile": "${workspaceFolder}/Dockerfile", 10 | "context": "${workspaceFolder}", 11 | "pull": true 12 | } 13 | }, 14 | { 15 | "type": "docker-run", 16 | "label": "docker-run: release", 17 | "dependsOn": [ 18 | "docker-build" 19 | ], 20 | "platform": "node" 21 | }, 22 | { 23 | "type": "docker-run", 24 | "label": "docker-run: debug", 25 | "dependsOn": [ 26 | "docker-build" 27 | ], 28 | "dockerRun": { 29 | "env": { 30 | "DEBUG": "*", 31 | "NODE_ENV": "development" 32 | } 33 | }, 34 | "node": { 35 | "enableDebugging": true 36 | } 37 | } 38 | ] 39 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16.16 2 | WORKDIR /usr/src/app 3 | COPY . /usr/src/app 4 | RUN npm install 5 | RUN npm run build 6 | EXPOSE 3000 7 | ENTRYPOINT ["npm", "start"] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VisiQL 2 | 3 | GraphQL is a query language for APIs that allows for engineers to customize their requests to ensure the data delivered is exactly what they need for their projects and nothing more. VisiQL was created by a small team of engineers who are passionate about encouraging other developers to get started with using GraphQL’s powerful abilities in their own projects. VisiQL accomplishes this by abstracting away the heavy lifting of creating the necessary schemas and resolvers for querying PostgreSQL databases with GraphQL, and providing sophisticated visualization tools to allow developers to easily customize and add to VisiQL’s generated code to save time standing up the rest of their queries. 4 | 5 | Getting started with VisiQL is easy. Simply fork our repo, clone to your local machine, and install all dependencies. 6 | From there run: 7 | 8 | npm run build 9 | 10 | npm run dev 11 | 12 | And your local instance of VisiQL will be running on port 8080. 13 | 14 | Generating your GraphQL Code 15 | 16 | Upon logging in or continuing as a guest, enter your desired PostgresQL database link in the textbox and click submit. Note that at this time, VisiQL was created to only support generating code for PostgresQL databases, but we welcome the Open Source community to add support for additional database types. Upon submission, you will see your database visualization and GraphQL schema generated and displayed instantly. 17 | 18 | ![EnterDB Link Step1](https://user-images.githubusercontent.com/13178363/205097233-7d40234b-a249-4fdd-85aa-7283d377cd49.gif) 19 | 20 | Toggle on the left hand side between the Schema and Resolvers tabs to review the code, or click the open button on the code editor to see both the generated Schema and Resolver in one view. You can also edit this generated code, right from within the editors and easily copy your changes. 21 | 22 | ![SchemaEditor Step2](https://user-images.githubusercontent.com/13178363/205098399-44baabca-8828-4c57-8ca5-b388cee5879a.gif) 23 | 24 | 25 | Visualize your Database 26 | 27 | On the left hand side, you will see a tree graph generated from your PostgresQL database. Click on the nodes to collapse and expand the table data and focus on only one part of your database. Or, use your trackpad or mouse to zoom in on your tree graph, clicking and dragging to view different parts of your database in finer detail. Hover over foreign keys to highlight relationships with primary keys and associated table data. If you need a bigger space to work with your database visualization, click the open button on the bottom right side of the visualizer panel. The same zoom, pan, and highlighting functionality is available in this larger view. 28 | 29 | ![Visualizer Step 3](https://user-images.githubusercontent.com/13178363/205099466-7b426d96-dca6-4107-987e-60add8674f26.gif) 30 | 31 | Projects Page 32 | 33 | On the Projects page, you will find a table with information about your saved projects. There are columns for the project name, the date it was most recently updated, and buttons for viewing or deleting the projects. Projects can be ordered alphabetically by Project Name, or by the date they were Last Updated. 34 | 35 | ![ProjectsPageVisiql](https://user-images.githubusercontent.com/104098416/205107467-b4a2bb12-0e6a-4448-818d-ed3f73f51070.gif) 36 | 37 | Saving your Project 38 | 39 | For users that are signed in, you will have the ability to save your projects to your projects folder. 40 | 41 | ![Save Project Step 4](https://user-images.githubusercontent.com/13178363/205096108-d40ba8fa-7768-43be-ab2d-d1b3bc0a3d1b.gif) 42 | 43 | Updating your Project 44 | 45 | While a saved project is loaded to the Home page, clicking the ‘Update Project’ icon from the toolbar will open a popover with a field for optionally changing the name of your project. Click ‘UPDATE’ to save your changes to the database. A popover will alert you of a successful update. 46 | 47 | ![UpdateProjectVisiql](https://user-images.githubusercontent.com/104098416/205108184-0a76f0bf-1972-48f6-ac0d-af2dfb8539f0.gif) 48 | Opening Projects from Toolbar 49 | 50 | Deleting a Project 51 | 52 | On the projects page, clicking the ‘Delete Project’ button will prompt you to confirm the deletion of the associated project. A popover will notify you of a successful deletion. 53 | 54 | ![DeleteProjectVisiql](https://user-images.githubusercontent.com/104098416/205109109-287e5624-d721-44dc-a303-cd68e4cc3b35.gif) 55 | 56 | If logged in, clicking the ‘View Projects’ folder icon from the toolbar will navigate to the Projects page. 57 | 58 | ![ToolbarProjectsVisiql](https://user-images.githubusercontent.com/104098416/205107994-318ec565-fd76-46dc-8af0-669a322facee.gif) 59 | 60 | GraphiQL Playground 61 | 62 | Our team has built a direct integration with GraphiQL into our application. When you first access our application, an Apollo Server will automatically load demo data from the Star Wars API into the GraphiQL Playground. In order to have a server spun up using your own database data, edit the data in the following files in your forked repository of VisiQL (hint: you can generate the code you need using VisiQL!) - resolvers.js, schema.js, and starwarsModel.js. Upon relaunch of the app, your GraphiQL Playground will be ready for querying with your inputted data. 63 | 64 | ![GraphiQL Playground](https://user-images.githubusercontent.com/13178363/205096199-77fb5e27-c422-4e9f-95df-a8607d99a9e3.gif) 65 | -------------------------------------------------------------------------------- /__mocks__/fileMock.js: -------------------------------------------------------------------------------- 1 | module.exports = 'test-file-stub'; -------------------------------------------------------------------------------- /__mocks__/styleMock.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; -------------------------------------------------------------------------------- /__tests__/backendTesting/backend.test.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const userDb = require('../../src/server/models/userModel'); 3 | const request = require('supertest'); 4 | const app = require('../../src/server/server'); 5 | 6 | describe('server tests', () => { 7 | 8 | describe('#user-router testing', () => { 9 | it('logs in with correct credentials', async () => { 10 | const response = await request(app) 11 | .post('/user/login') 12 | .send({ 13 | username: 'aa', 14 | password: 'aa', 15 | }) 16 | .set('Accept', 'application/json'); 17 | expect(response.status).toEqual(200); 18 | expect(response.body).toHaveProperty('accessToken'); 19 | }); 20 | 21 | it('does not log in if incorrect credentials', async () => { 22 | const response = await request(app) 23 | .post('/user/login') 24 | .send({ 25 | username: 'aa', 26 | password: 'bb', 27 | }) 28 | .set('Accept', 'application/json'); 29 | expect(response.status).not.toEqual(200); 30 | expect(response.body).not.toHaveProperty('accessToken'); 31 | }); 32 | 33 | // check if username exists 34 | it.each` 35 | username | whatResponseEquals 36 | ${'aa'} | ${'exists'} 37 | ${'a'.repeat(200)} | ${'nonexistent'} 38 | ` ('returns $whatResponseEquals if username equals $username', async ({ username, whatResponseEquals }) => { 39 | const response = await request(app) 40 | .post('/user/check') 41 | .send({ username }) 42 | .set('Accept', 'application/json'); 43 | expect(response.status).toEqual(200); 44 | expect(response.body).toEqual(whatResponseEquals); 45 | }); 46 | 47 | it('checks if signup works', async () => { 48 | const randomUsername = 'a'.repeat(200); 49 | 50 | const response = await request(app) 51 | .post('/user/signup') 52 | .send({ 53 | username: randomUsername, 54 | firstName: 'first', 55 | lastName: 'last', 56 | password: 'password', 57 | email: 'test@test.com', 58 | }) 59 | .set('Accept', 'application/json'); 60 | expect(response.status).toEqual(201); 61 | expect(response.body).toHaveProperty('username'); 62 | 63 | const deleteQuery = `DELETE FROM users WHERE username = '${randomUsername}' returning *`; 64 | const { rows } = await userDb.query(deleteQuery); 65 | expect(rows[0].username).toEqual(randomUsername) 66 | }) 67 | 68 | it('checks if server blocks existing username sign up attempt', async () => { 69 | const response = await request(app) 70 | .post('/user/signup') 71 | .send({ 72 | username: 'aa', 73 | name: 'name', 74 | password: 'password', 75 | email: 'test@test.com', 76 | }) 77 | .set('Accept', 'application/json'); 78 | expect(response.status).not.toEqual(201); 79 | expect(response.body).not.toHaveProperty('username'); 80 | }) 81 | }); 82 | describe('#dbLink-router testing', () => { 83 | it('providing working dbLink works properly', async () => { 84 | const response = await request(app) 85 | .post('/db') 86 | .send({ 87 | dbLink: 'postgres://nfvukbtr:d7AGqY2wq7gu0Y58CNUcHjK1oXC9aHov@peanut.db.elephantsql.com/nfvukbtr' 88 | }) 89 | .set('Accept', 'application/json'); 90 | expect(response.body).toHaveProperty('fnKeys'); 91 | expect(response.body).toHaveProperty('parsedFnKeys'); 92 | expect(response.body).toHaveProperty('parsedPrimaryKeys'); 93 | expect(response.body).toHaveProperty('schemaString'); 94 | expect(response.body).toHaveProperty('tree'); 95 | expect(response.status).toEqual(202); 96 | }); 97 | 98 | it('providing invalid dbLink does not work', async () => { 99 | const response = await request(app) 100 | .post('/db') 101 | .send({ 102 | dbLink: 'fakeLink' 103 | }) 104 | .set('Accept', 'application/json'); 105 | expect(response.body).not.toHaveProperty('fnKeys'); 106 | expect(response.body).not.toHaveProperty('parsedFnKeys'); 107 | expect(response.body).not.toHaveProperty('parsedPrimaryKeys'); 108 | expect(response.body).not.toHaveProperty('schemaString'); 109 | expect(response.body).not.toHaveProperty('tree'); 110 | expect(response.status).not.toEqual(202); 111 | }); 112 | }); 113 | }); -------------------------------------------------------------------------------- /__tests__/endToEndTesting/puppeteer.test.js: -------------------------------------------------------------------------------- 1 | const puppeteer = require('puppeteer'); 2 | 3 | /* 4 | * This is the test file for frontend integration 5 | * and end-to-end browser testing using Puppeteer 6 | */ 7 | 8 | //defines the app address 9 | const APP = `http://localhost:${process.env.PORT || 3000}/`; 10 | 11 | describe('Front-end Integration/Features', () => { 12 | const timeout = 6000; 13 | let browser; 14 | let page; 15 | 16 | beforeAll(async () => { 17 | browser = await puppeteer.launch({ 18 | args: ['--no-sandbox', '--disable-setuid-sandbox'], 19 | }); 20 | page = await browser.newPage(); 21 | }, timeout); 22 | 23 | afterAll(() => { 24 | browser.close(); 25 | }); 26 | 27 | describe('Initial display', () => { 28 | it('loads successfully', async () => { 29 | // We navigate to the page at the beginning of each case so we have a 30 | // fresh start 31 | await page.goto(APP); 32 | await page.waitForSelector('#homepage-container'); 33 | 34 | }, timeout); 35 | 36 | it('displays a usable database input field', async () => { 37 | await page.goto(APP); 38 | await page.waitForSelector('.db-textfield'); 39 | // const textField = await page.waitForSelector('.db-textfield'); 40 | // textField.onChange = (e) => e.target.value; 41 | // await page.click('.db-textfield'); 42 | // await page.keyboard.type('db-textfield', 'Tallahassee'); 43 | // const inputValue = await page.$eval('.db-textfield', el => el.value); 44 | // expect(inputValue).toBe('Tallahassee'); 45 | 46 | }, timeout); 47 | //unable to test this as shown in the Unit because of the useInput hook 48 | }, timeout); 49 | }); 50 | -------------------------------------------------------------------------------- /__tests__/frontendTesting/login.test.js: -------------------------------------------------------------------------------- 1 | import React from 'React'; 2 | import userEvent from '@testing-library/user-event' 3 | import { render, screen, waitFor } from '@testing-library/react'; 4 | 5 | import Login from '../../src/client/components/Login'; 6 | 7 | /* 8 | * This is the test file for unit and integration tests of the Login component 9 | * using Jest and React Testing Library 10 | */ 11 | const mockedUsedNavigate = jest.fn(); 12 | 13 | jest.mock("react-router-dom", () => ({ 14 | ...jest.requireActual("react-router-dom"), 15 | useNavigate: () => mockedUsedNavigate 16 | })); 17 | 18 | //Unit testing for Login component 19 | describe('Login', () => { 20 | test('It renders the Login component', () => { 21 | render(); 22 | }) 23 | 24 | test('It should have buttons for login, sign up, and continue as guest', async () => { 25 | render(); 26 | const buttons = await screen.findAllByRole('button'); 27 | expect(buttons.length).toBe(4); 28 | expect(buttons[1]).toHaveTextContent('Login'); 29 | expect(buttons[2]).toHaveTextContent('Sign Up'); 30 | expect(buttons[3]).toHaveTextContent('Continue As Guest'); 31 | }); 32 | 33 | test('It should have input boxes for username and password', () => { 34 | render(); 35 | expect(screen.findByLabelText('Username')).toBeInTheDocument; 36 | expect(screen.findByLabelText('Password')).toBeInTheDocument; 37 | expect(screen.findByLabelText('Hello')).not.toBeInTheDocument; 38 | }); 39 | }) -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ['@babel/preset-env', {targets: {node: 'current'}}], 4 | '@babel/preset-typescript', ['@babel/preset-react', {runtime: 'automatic'}] 5 | ], 6 | }; -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | VisiQL
-------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | visiql: 5 | image: visiql 6 | build: 7 | context: . 8 | dockerfile: ./Dockerfile 9 | environment: 10 | NODE_ENV: production 11 | ports: 12 | - 3000:3000 13 | - 4000:4000 -------------------------------------------------------------------------------- /jest-setup.js: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom' -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @returns {Promise} */ 2 | module.exports = async () => { 3 | return { 4 | verbose: true, 5 | moduleNameMapper: { 6 | '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': 7 | '/__mocks__/fileMock.js', 8 | '\\.(css|less|scss)$': 'identity-obj-proxy', 9 | }, 10 | // testEnvironment: 'jsdom', 11 | testPathIgnorePatterns: [ 12 | "/node_modules/", 13 | "/src/client/scss/", 14 | "/dist" 15 | ], 16 | setupFilesAfterEnv: ['/jest-setup.js'], 17 | preset: 'jest-puppeteer' 18 | }; 19 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "visiql", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "proxy": "http//localhost:3000", 7 | "scripts": { 8 | "start": "NODE_ENV=production ts-node src/server/server.ts src/server/routes/graphqlRouter.ts", 9 | "build": "NODE_ENV=production webpack", 10 | "dev": "NODE_ENV=development webpack serve --open --hot & nodemon src/server/server.ts src/server/routes/graphqlRouter.ts", 11 | "test": "jest --detectOpenHandles", 12 | "tsc": "tsc" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/oslabs-beta/VisiQL.git" 17 | }, 18 | "keywords": [], 19 | "author": "", 20 | "license": "ISC", 21 | "bugs": { 22 | "url": "https://github.com/oslabs-beta/VisiQL/issues" 23 | }, 24 | "homepage": "https://github.com/oslabs-beta/VisiQL#readme", 25 | "dependencies": { 26 | "@apollo/server": "^4.2.2", 27 | "@emotion/react": "^11.10.4", 28 | "@emotion/styled": "^11.10.4", 29 | "@graphiql/react": "^0.15.0", 30 | "@graphiql/toolkit": "^0.8.0", 31 | "@mui/icons-material": "^5.10.14", 32 | "@mui/lab": "^5.0.0-alpha.109", 33 | "@mui/material": "^5.10.13", 34 | "@mui/styled-engine-sc": "^5.10.6", 35 | "@mui/system": "^5.10.15", 36 | "@mui/x-data-grid": "^5.17.13", 37 | "@types/express": "^4.17.14", 38 | "@types/node": "^18.11.9", 39 | "@types/react": "^18.0.25", 40 | "apollo-server": "^3.11.1", 41 | "bcrypt": "^5.1.0", 42 | "bootstrap": "^5.2.2", 43 | "cookie-parser": "^1.4.6", 44 | "d3": "^7.6.1", 45 | "dotenv": "^16.0.3", 46 | "express": "^4.18.2", 47 | "file-loader": "^6.2.0", 48 | "graphiql": "^2.2.0", 49 | "graphql": "^16.6.0", 50 | "graphql-ws": "^5.11.2", 51 | "jsonwebtoken": "^9.0.0", 52 | "pg": "^8.8.0", 53 | "prismjs": "^1.29.0", 54 | "react": "^18.2.0", 55 | "react-bootstrap": "^2.6.0", 56 | "react-dom": "^18.2.0", 57 | "react-router": "^5.2.0", 58 | "react-router-dom": "^6.4.2", 59 | "react-simple-code-editor": "^0.13.1", 60 | "resize-observer-polyfill": "^1.5.1", 61 | "styled-components": "^5.3.6", 62 | "ts-loader": "^9.4.1", 63 | "ts-node": "^10.9.1", 64 | "tsc": "^2.0.4", 65 | "tsc-loader": "^1.0.4", 66 | "typescript": "^4.8.4", 67 | "ws": "^8.11.0" 68 | }, 69 | "devDependencies": { 70 | "@babel/core": "^7.19.3", 71 | "@babel/preset-env": "^7.19.4", 72 | "@babel/preset-react": "^7.18.6", 73 | "@babel/preset-typescript": "^7.18.6", 74 | "@testing-library/jest-dom": "^5.15.0", 75 | "@testing-library/react": "^13.4.0", 76 | "@testing-library/user-event": "^13.5.0", 77 | "@types/expect-puppeteer": "^5.0.2", 78 | "@types/jest-environment-puppeteer": "^5.0.3", 79 | "@types/puppeteer": "^7.0.4", 80 | "babel-jest": "29.4.1", 81 | "babel-loader": "^8.2.5", 82 | "concurrently": "^6.0.2", 83 | "cross-env": "^7.0.3", 84 | "css-loader": "^6.7.1", 85 | "html-webpack-plugin": "^5.5.0", 86 | "identity-obj-proxy": "^3.0.0", 87 | "jest": "^29.3.1", 88 | "jest-environment-jsdom": "^29.3.1", 89 | "jest-puppeteer": "^6.2.0", 90 | "mini-css-extract-plugin": "^2.6.1", 91 | "nodemon": "^2.0.7", 92 | "puppeteer": "^19.6.2", 93 | "react-test-renderer": "^18.2.0", 94 | "sass": "^1.55.0", 95 | "sass-loader": "^13.1.0", 96 | "style-loader": "^3.3.1", 97 | "supertest": "^6.3.1", 98 | "ts-jest": "^29.0.3", 99 | "webpack": "^5.74.0", 100 | "webpack-cli": "^4.10.0", 101 | "webpack-dev-server": "^4.11.1" 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/client/App.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Route, Routes } from 'react-router-dom'; 3 | import Homepage from './components/Homepage'; 4 | import About from './components/About'; 5 | import Login from './components/Login'; 6 | import ProjectsPage from './components/ProjectsPage'; 7 | import GraphiQLPlayground from './components/GraphiQLPlayground'; 8 | import Navbar from './components/Navbar'; 9 | 10 | const App = () => { 11 | const initialData = { 12 | name: 'Database', 13 | children: [ 14 | { 15 | name: 'Table-1', 16 | children: [ 17 | { 18 | name: 'Column-1', 19 | }, 20 | { 21 | name: 'Column-2', 22 | }, 23 | { 24 | name: 'Column-3', 25 | }, 26 | ], 27 | }, 28 | { 29 | name: 'Table-2', 30 | }, 31 | ], 32 | }; 33 | const [loggedIn, setLoggedIn] = useState(false); 34 | const [currentUserId, setCurrentUserId] = useState(''); //should we set this to null to by typesafe? 35 | const [notSignedInPop, setNotSignedInPop] = useState(false); 36 | 37 | const [dbSchemaData, dbSchemaDataOnChange] = useState( 38 | 'Enter a Postgres DB link to generate your schema...' 39 | ); 40 | const [treeData, setTreeData] = useState(initialData); 41 | const [resolverData, setResolverData] = useState( 42 | 'Enter a Postgres DB link to generate your resolvers...' 43 | ); 44 | const [projectId, setProjectId] = useState(null); 45 | 46 | const [projectName, setProjectName] = useState(''); 47 | 48 | const [showTree, setShowTree] = useState(true); 49 | 50 | const tokenChecker = async () => { 51 | try { 52 | const token = await fetch('/user/checkToken', { 53 | headers: { 'Content-Type': 'application/json' }, 54 | }); 55 | 56 | const tokenCheck = await token.json(); 57 | 58 | if (tokenCheck.status === 'success') { 59 | setLoggedIn(true); 60 | setNotSignedInPop(false); 61 | setCurrentUserId(tokenCheck.id); 62 | } else { 63 | setLoggedIn(false); 64 | } 65 | } catch (err) { 66 | console.log('error'); 67 | } 68 | }; 69 | tokenChecker(); 70 | 71 | return ( 72 |
73 | 84 | 85 | 109 | } 110 | /> 111 | 112 | 121 | } 122 | /> 123 | 131 | } 132 | /> 133 | 134 | 146 | } 147 | /> 148 | 149 | 163 | } 164 | /> 165 | 174 | } 175 | /> 176 | 177 |
178 | ); 179 | }; 180 | 181 | export default App; 182 | -------------------------------------------------------------------------------- /src/client/assets/Cavin.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/VisiQL/66b92c9bee45f96d6c576688f8d2d2e0d4d944a7/src/client/assets/Cavin.jpg -------------------------------------------------------------------------------- /src/client/assets/Jordan.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/VisiQL/66b92c9bee45f96d6c576688f8d2d2e0d4d944a7/src/client/assets/Jordan.jpg -------------------------------------------------------------------------------- /src/client/assets/Kelly.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/VisiQL/66b92c9bee45f96d6c576688f8d2d2e0d4d944a7/src/client/assets/Kelly.jpg -------------------------------------------------------------------------------- /src/client/assets/Rebecca.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/VisiQL/66b92c9bee45f96d6c576688f8d2d2e0d4d944a7/src/client/assets/Rebecca.png -------------------------------------------------------------------------------- /src/client/assets/VisiQLLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/VisiQL/66b92c9bee45f96d6c576688f8d2d2e0d4d944a7/src/client/assets/VisiQLLogo.png -------------------------------------------------------------------------------- /src/client/components/About.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Team from './Team'; 3 | import Documentation from './Documentation'; 4 | import '../scss/about.scss'; 5 | 6 | const About = () => { 7 | return ( 8 |
9 | 10 | 11 |
12 | ); 13 | }; 14 | 15 | export default About; 16 | -------------------------------------------------------------------------------- /src/client/components/DBInput.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { TextField, Button } from '@mui/material'; 3 | import { useState } from 'react'; 4 | import SchemaContainer from './SchemaContainer'; 5 | import VisualizerContainer from './VisualizerContainer'; 6 | import ProjectToolbar from './ProjectToolbar'; 7 | 8 | const useInput = (init) => { 9 | const [value, setValue] = useState(init); 10 | const onChange = (e) => { 11 | setValue(e.target.value); 12 | }; 13 | const reset = () => { 14 | setValue(init); 15 | }; 16 | return [value, onChange, reset]; 17 | }; 18 | 19 | const DBInput = (props) => { 20 | const [dbLink, dbLinkOnChange, resetdbLink] = useInput(''); 21 | const [dataReceived, setDataReceived] = useState(false); 22 | 23 | const { 24 | projectId, 25 | setProjectId, 26 | projectName, 27 | setProjectName, 28 | showTree, 29 | setShowTree, 30 | dbSchemaData, 31 | dbSchemaDataOnChange, 32 | treeData, 33 | setTreeData, 34 | resolverData, 35 | setResolverData, 36 | loggedIn, 37 | setLoggedIn, 38 | setNotSignedInPop, 39 | notSignedInPop, 40 | } = props; 41 | 42 | const saveDBLink = (event) => { 43 | if (dbLink === '') { 44 | return alert('Please enter a database link'); 45 | } else if (!dbLink.includes('postgres')) { 46 | return alert('Please enter a postgres database link'); 47 | } else { 48 | const body = { dbLink }; 49 | resetdbLink(); 50 | fetch('/db', { 51 | method: 'POST', 52 | headers: { 53 | 'Content-Type': 'Application/JSON', 54 | }, 55 | body: JSON.stringify(body), 56 | }) 57 | .then((data) => data.json()) 58 | .then((data) => { 59 | console.log(data); 60 | dbSchemaDataOnChange(data.schemaString); 61 | setResolverData(data.resolverString); 62 | setDataReceived(true); 63 | setTreeData(data.tree); 64 | setProjectId(null); //reset projectid and projectname after new submission so data from update isn't overwritten 65 | setProjectName(null); 66 | }) 67 | .catch((err) => console.log('dbLink fetch /db: ERROR:', err)); 68 | } 69 | 70 | }; 71 | 72 | const displayDemo = () => { 73 | fetch('/db', { 74 | method: 'GET', 75 | }) 76 | .then((data) => data.json()) 77 | .then((data) => { 78 | console.log(data); 79 | dbSchemaDataOnChange(data.schemaString); 80 | setResolverData(data.resolverString); 81 | setDataReceived(true); 82 | setTreeData(data.tree); 83 | }) 84 | .catch((err) => console.log('dbLink fetch /db: ERROR:', err)); 85 | } 86 | 87 | return ( 88 |
89 |
90 |
91 | 100 | 112 | 124 | 125 | 126 |
127 |
128 | 137 | 142 | 156 |
157 |
158 | ); 159 | }; 160 | 161 | export default DBInput; 162 | -------------------------------------------------------------------------------- /src/client/components/DeleteProject.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button } from '@mui/material'; 3 | 4 | const DeleteProject = (props) => { 5 | 6 | return props.trigger ? ( 7 |
8 |
9 |

Are you sure you want to delete this project?

10 |
11 | 23 | 35 |
36 |
37 |
38 | ) : ( 39 | null 40 | ); 41 | }; 42 | 43 | export default DeleteProject; -------------------------------------------------------------------------------- /src/client/components/Documentation.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import '../scss/about.scss'; 3 | 4 | const Documentation = () => { 5 | return ( 6 |
7 |
8 |

About VisiQL

9 |
10 |
11 |

12 |
13 | VisiQL was created by a small team of engineers who are passionate 14 | about encouraging other developers to get started with using 15 | GraphQL’s powerful abilities in their own projects. 16 |
17 |

GraphQL is a query language for APIs that allows for 18 | engineers to customize their requests to ensure the data delivered is 19 | exactly what they need for their projects and nothing more. VisiQL 20 | accomplishes this by abstracting away the heavy lifting of creating 21 | the necessary schemas and resolvers for querying PostgreSQL databases 22 | with GraphQL and providing sophisticated visualization tools to allow 23 | developers to easily customize and add to VisiQL’s generated code to 24 | save time setting up the rest of their queries.

Getting 25 | started with VisiQL is easy. Simply start by signing into your 26 | account. If you don’t have an account yet, you can either create one, 27 | or continue using VisiQL as a guest. Note that if you do choose to 28 | create an account, you will also have the ability to save and edit 29 | your projects so that you can revisit your projects for any new 30 | requirements that may come up throughout your development process. 31 |
32 |
33 |

34 |
35 |
36 | Generating your GraphQL Code 37 |
38 |

39 | Upon logging in or continuing as a guest, enter your desired 40 | PostgreSQL database link in the textbox and click submit. Note that 41 | at this time, VisiQL was created to only support generating code for 42 | PostgreSQL databases, but we welcome the Open Source community to 43 | add support for additional database types. 44 |
45 |
46 | Upon submission, you will see your database visualization and 47 | GraphQL schema generated and displayed instantly. Toggle on the left 48 | hand side between the schema and resolvers tabs to review the code, 49 | or click the open button on the code editor to see both the 50 | generated schema and resolver in one view. 51 |
52 |
53 |

54 |
55 |
56 |
Visualize your Database
57 |

58 | On the right hand side, you will see a tree graph generated from your 59 | PostgreSQL database. Click on the nodes to collapse and expand the 60 | table data and focus on only one part of your database. 61 |
62 |
63 | Or, use your trackpad or mouse to zoom in on your tree graph, 64 | clicking and dragging to view different parts of your database in 65 | finer detail. 66 |
67 |
68 | Hover over foreign keys to highlight relationships with primary keys 69 | and associated table data. 70 |
71 |
72 | If you need a bigger space to work with your database visualization, 73 | click the open button on the bottom right side of the visualizer 74 | panel. The same zoom, pan, and highlighting functionality is 75 | available in this larger view. 76 |
77 |
78 |

79 |
80 |
81 |
82 | ); 83 | }; 84 | 85 | export default Documentation; 86 | -------------------------------------------------------------------------------- /src/client/components/EditorPopOut.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import Editor from 'react-simple-code-editor'; 3 | import { highlight, languages } from 'prismjs/components/prism-core'; 4 | import 'prismjs/components/prism-clike'; 5 | import 'prismjs/components/prism-javascript'; 6 | import 'prismjs/themes/prism-okaidia.css'; 7 | import { IconButton, Tooltip } from '@mui/material' 8 | import ContentCopyIcon from '@mui/icons-material/ContentCopy'; 9 | import DoneOutlineIcon from '@mui/icons-material/DoneOutline'; 10 | 11 | type EditorPopOutProps = { 12 | dbSchemaData: string; 13 | dbSchemaDataOnChange: Function; 14 | resolverData: string; 15 | setResolverData: Function; 16 | close: Function; 17 | }; 18 | 19 | const EditorPopOut = ({ 20 | dbSchemaData, 21 | dbSchemaDataOnChange, 22 | resolverData, 23 | setResolverData, 24 | // close, 25 | }: EditorPopOutProps) => { 26 | const [currSchIcon, setCurrSchIcon] = useState( 27 | 28 | ); 29 | const [currResIcon, setCurrResIcon] = useState( 30 | 31 | ); 32 | const [currTooltip, setCurrTooltip] = useState(

Copy

); 33 | const resetIcons = () => { 34 | setCurrTooltip(

Copy

); 35 | setCurrSchIcon(); 36 | setCurrResIcon(); 37 | }; 38 | 39 | function delay(callback: Function, waitTime: number) { 40 | return function delayedFunction() { 41 | return setTimeout(callback, waitTime); 42 | }; 43 | } 44 | const delayedFunc = delay(() => resetIcons(), 1000); 45 | 46 | const handleClickSch = () => { 47 | navigator.clipboard.writeText(dbSchemaData); 48 | setCurrTooltip(

Copied

); 49 | setCurrSchIcon(); 50 | delayedFunc(); 51 | }; 52 | 53 | const handleClickRes = () => { 54 | navigator.clipboard.writeText(resolverData); 55 | setCurrTooltip(

Copied

); 56 | setCurrResIcon(); 57 | delayedFunc(); 58 | }; 59 | 60 | return( 61 | <> 62 | 63 | dbSchemaDataOnChange(code)} 68 | highlight={(code) => highlight(code, languages.js)} 69 | style={{ 70 | fontFamily: '"Fira code", "Fira Mono", monospace', 71 | fontSize: 20, 72 | }} /> 73 | 77 | {currSchIcon} 78 | 79 | 80 | setResolverData(code)} 85 | highlight={(code) => highlight(code, languages.js)} 86 | style={{ 87 | fontFamily: '"Fira code", "Fira Mono", monospace', 88 | fontSize: 20, 89 | }} /> 90 | 91 | 95 | {currResIcon} 96 | 97 | 98 | 99 | 100 | ) 101 | }; 102 | 103 | export default EditorPopOut; -------------------------------------------------------------------------------- /src/client/components/EditorPopOutHandler.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button } from '@mui/material'; 3 | import EditorPopOut from './EditorPopOut'; 4 | 5 | type EditorPopOutHandlerProps = { 6 | close: Function; 7 | dbSchemaData: string; 8 | resolverData: string; 9 | trigger: boolean; 10 | dbSchemaDataOnChange: Function; 11 | setResolverData: Function; 12 | setShowTree: Function; 13 | } 14 | 15 | const EditorPopOutHandler = ({ 16 | close, 17 | dbSchemaData, 18 | resolverData, 19 | dbSchemaDataOnChange, 20 | setResolverData, 21 | trigger, 22 | setShowTree, }: EditorPopOutHandlerProps) => { 23 | 24 | const handleClick = () => { 25 | close(false); 26 | setShowTree(true); 27 | } 28 | return trigger ? ( 29 |
30 |
31 | 32 | 46 | 48 | 49 |
50 |
51 | ) : ( 52 | null 53 | ); 54 | }; 55 | 56 | export default EditorPopOutHandler; -------------------------------------------------------------------------------- /src/client/components/GraphiQLPlayground.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { GraphiQL } from 'graphiql'; 3 | import { createGraphiQLFetcher } from '@graphiql/toolkit'; 4 | 5 | import 'graphiql/graphiql.css'; 6 | 7 | const GraphiQLPlayground = ({ 8 | dbSchemaData, 9 | resolverData, 10 | loggedIn, 11 | setCurrentUserId, 12 | notSignedInPop, 13 | setNotSignedInPop, 14 | }) => { 15 | const fetcher = createGraphiQLFetcher({ 16 | url: 'http://localhost:4000/', 17 | }); 18 | 19 | //----> post request for sending schema and resolver data to backend for dynamic graphiql playground functionality 20 | 21 | // const generatedGQL = () => { 22 | // // const body = { dbSchemaData, resolverData }; 23 | // fetch('/graphql', { 24 | // headers: { 25 | // 'Content-Type': 'Application/JSON', 26 | // // Connection: 'Upgrade', 27 | // }, 28 | // // body: JSON.stringify(body), 29 | // }) 30 | // .then((data) => data.json()) 31 | // .then((data) => { 32 | // console.log(data); 33 | // }) 34 | // .catch((err) => console.log('dbLink fetch /graphql: ERROR:', err)); 35 | // }; 36 | // generatedGQL(); 37 | 38 | return ( 39 |
40 |
41 | 42 |
43 |
44 | ); 45 | }; 46 | 47 | export default GraphiQLPlayground; 48 | -------------------------------------------------------------------------------- /src/client/components/Homepage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | //@ts-ignore 3 | import DBInput from './DBInput'; 4 | //@ts-ignore 5 | import NotSignedIn from './NotSignedIn'; 6 | 7 | 8 | type HomepageProps = { 9 | loggedIn: Boolean; 10 | setLoggedIn: Function; 11 | setCurrentUserId: Function; 12 | currentUserId: Number; 13 | dbSchemaData: String; 14 | dbSchemaDataOnChange: Function; 15 | treeData: Object; 16 | setTreeData: Function; 17 | resolverData: String; 18 | setResolverData: Function; 19 | projectId: Number; 20 | setProjectId: Function; 21 | projectName: String; 22 | setProjectName: Function; 23 | notSignedInPop: Boolean; 24 | setNotSignedInPop: Function; 25 | showTree: boolean; 26 | setShowTree: Function; 27 | }; 28 | 29 | const Homepage = ({ 30 | loggedIn, 31 | setLoggedIn, 32 | currentUserId, 33 | dbSchemaData, 34 | dbSchemaDataOnChange, 35 | treeData, 36 | setTreeData, 37 | resolverData, 38 | setResolverData, 39 | projectId, 40 | setProjectId, 41 | projectName, 42 | setProjectName, 43 | notSignedInPop, 44 | setNotSignedInPop, 45 | showTree, 46 | setShowTree, 47 | }: HomepageProps) => { 48 | return ( 49 |
50 | 69 | 70 |
71 | ); 72 | }; 73 | 74 | export default Homepage; 75 | -------------------------------------------------------------------------------- /src/client/components/Login.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | import { Container, Grid, Paper, TextField, InputAdornment, Button, IconButton } from '@mui/material'; 4 | import VisibilityIcon from '@mui/icons-material/Visibility'; 5 | import VisibilityOffIcon from '@mui/icons-material/VisibilityOff'; 6 | import logo from '../assets/VisiQLLogo.png'; 7 | import '../scss/login.scss'; 8 | 9 | 10 | const useInput = (init) => { 11 | const [value, setValue] = useState(init); 12 | const reset = () => { 13 | setValue(init); 14 | } 15 | const set = (e) => { 16 | value, 17 | console.log(e.target.value); 18 | setValue(e.target.value); 19 | 20 | }; 21 | 22 | return [value, set, reset]; 23 | }; 24 | 25 | 26 | 27 | const Login = (props) => { 28 | const [state, setState] = useState({ loginPage: true }); 29 | const { loginPage } = state; 30 | const navigate = useNavigate(); 31 | const [username, setUsername, resetUsername] = useInput(''); 32 | const [password, setPassword, resetPassword] = useInput(''); 33 | const [firstName, setFirstName, resetFirstName] = useInput(''); 34 | const [lastName, setLastName, resetLastName] = useInput(''); 35 | const [email, setEmail, resetEmail] = useInput(''); 36 | const [password2, setPassword2, resetPassword2] = useInput(''); 37 | const [passwordVis, setPasswordVis] = useState(false); 38 | 39 | // const checkUserExist = [setUsername, checkExistence]; 40 | // changes the boolean value of the variable that the conditional rendering depends on 41 | const showOtherPage = () => { 42 | console.log(!loginPage) 43 | // if text is in input boxes, clear it 44 | resetUsername(); 45 | resetPassword(); 46 | if (!loginPage) { 47 | resetPassword2(); 48 | resetFirstName(); 49 | resetLastName(); 50 | resetEmail(); 51 | } 52 | setState((prevState) => { 53 | console.log(loginPage) 54 | return { 55 | // ...prevState, 56 | loginPage: !loginPage, 57 | }; 58 | 59 | }); 60 | }; 61 | console.log(loginPage) 62 | const handlePassVis = () => { 63 | setPasswordVis(!passwordVis) 64 | }; 65 | 66 | const logInUser = async (e) => { 67 | try { 68 | // prevents the automatic page refresh 69 | e.preventDefault(); 70 | 71 | console.log(username, password) 72 | // catch invalid inputs 73 | if (username === '' || password === '') return; 74 | 75 | const credentialCheck = await fetch('/user/login', { 76 | method: 'POST', 77 | headers: { 'Content-Type': 'application/json' }, 78 | body: JSON.stringify({ username, password }), 79 | }).then((response) => { 80 | if (response.status === 200) { 81 | props.setLoggedIn(true); 82 | props.tokenChecker(); 83 | navigate('/'); 84 | } else alert('Username or password is incorrect.'); 85 | resetUsername(); 86 | resetPassword(); 87 | return; 88 | }); 89 | 90 | } catch (err) { 91 | console.log('err: ', err); 92 | } 93 | }; 94 | 95 | 96 | const signUserUp = async (e) => { 97 | try { 98 | e.preventDefault(); 99 | 100 | // catch invalid inputs 101 | if ( 102 | firstName === '' || 103 | lastName === '' || 104 | email === '' || 105 | username === '' || 106 | password === '' || 107 | password !== password2 108 | ) 109 | return; 110 | // check if username is already taken prior to sign up attempt 111 | const checkExistence = await fetch('/user/check', { 112 | method: 'POST', 113 | headers: { 'Content-Type': 'application/json' }, 114 | body: JSON.stringify({ username }), 115 | }); 116 | const parsedCheckExistence = await checkExistence.json(); 117 | if (parsedCheckExistence === 'exists') { 118 | alert('Username is already taken.'); 119 | return; 120 | } 121 | 122 | // create new username account 123 | const createdUser = await fetch('/user/signup', { 124 | method: 'POST', 125 | headers: { 'Content-Type': 'application/json' }, 126 | body: JSON.stringify({ firstName, lastName, email, username, password }), 127 | }); 128 | const parsedUserInfo = await createdUser.json(); 129 | // if user sign up is successful: alert user and redirect to login screen 130 | showOtherPage(); 131 | alert('Sign Up Successful!'); 132 | resetFirstName(); 133 | resetLastName(); 134 | resetEmail(); 135 | resetUsername(); 136 | resetPassword(); 137 | resetPassword2(); 138 | 139 | } catch (err) { 140 | console.log('err: ', err); 141 | } 142 | }; 143 | 144 | const guestMode = () => { 145 | navigate('/'); 146 | return; 147 | }; 148 | 149 | return ( 150 | <> 151 | {loginPage ? ( 152 | <> 153 |
154 |
155 |

Welcome Back!

156 |
157 | 158 | 159 | 165 | 166 | 167 | 168 | 177 | 178 | 179 | 192 | 193 | {passwordVis? ( 194 | 195 | ) : ( 196 | 197 | ) 198 | } 199 | 200 | 201 | ), 202 | 203 | }} 204 | /> 205 | 206 | 207 | 216 | 217 |

Don't Have an Account Yet?

218 | 219 | 228 | 229 | 230 | 239 | 240 |
241 |
242 |
243 |
244 | 245 |
246 | 247 | ) : ( 248 | <> 249 |
250 |
251 |

Welcome - Let's Get VisiQL!

252 |
253 | 254 | 255 | 261 | 262 | 263 | 264 | 273 | 274 | 275 | 284 | 285 | 286 | 295 | 296 | 297 | 298 | 307 | 308 | 309 | 321 | 322 | {passwordVis? ( 323 | 324 | ) : ( 325 | 326 | ) 327 | } 328 | 329 | 330 | ), 331 | 332 | }} 333 | /> 334 | 335 | 336 | 348 | 349 | {passwordVis? ( 350 | 351 | ) : ( 352 | 353 | ) 354 | } 355 | 356 | 357 | ), 358 | 359 | }} 360 | /> 361 | 362 | 363 | 372 | 373 |

Already Have An Account?

374 | 375 | 384 | 385 | 386 | 395 | 396 |
397 |
398 |
399 |
400 |
401 | 402 | )} 403 | 404 | ); 405 | }; 406 | 407 | export default Login; -------------------------------------------------------------------------------- /src/client/components/Navbar.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from '@mui/material'; 2 | import React from 'react'; 3 | import { Link, useNavigate } from 'react-router-dom'; 4 | import logo from '../assets/VisiQLLogo.png'; 5 | import styles from './scss/_index.scss'; 6 | 7 | const ProjectsPage = require('./ProjectsPage'); 8 | 9 | type NavbarProps = { 10 | loggedIn: Boolean; 11 | setCurrentUserId: Function; 12 | notSignedInPop: Boolean; 13 | setNotSignedInPop: Function; 14 | dbSchemaDataOnChange: Function; 15 | setTreeData: Function; 16 | blankTree: Object; 17 | setResolverData: Function; 18 | }; 19 | 20 | const Navbar = ({ 21 | loggedIn, 22 | setCurrentUserId, 23 | setNotSignedInPop, 24 | dbSchemaDataOnChange, 25 | setTreeData, 26 | blankTree, 27 | setResolverData, 28 | }: NavbarProps) => { 29 | const navigate = useNavigate(); 30 | const thisOrThat = () => { 31 | if (loggedIn) { 32 | return Projects; 33 | } else { 34 | return ( 35 | setNotSignedInPop(true)} to='/'> 36 | Projects 37 | 38 | ); 39 | } 40 | }; 41 | 42 | const signOut = async () => { 43 | try { 44 | setCurrentUserId(''); 45 | document.cookie = 46 | 'token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; 47 | setResolverData('Enter a Postgres DB link to generate your resolvers...'); 48 | dbSchemaDataOnChange( 49 | 'Enter a Postgres DB lin to generate your schema...' 50 | ); 51 | setTreeData(blankTree); 52 | navigate('/login'); 53 | return; 54 | } catch (err) { 55 | console.log('error'); 56 | } 57 | }; 58 | const signInOut = () => { 59 | if (!loggedIn) { 60 | return Sign In; 61 | } else { 62 | return ( 63 | 64 | Sign Out 65 | 66 | ); 67 | } 68 | }; 69 | 70 | return ( 71 | 93 | ); 94 | }; 95 | 96 | export default Navbar; 97 | -------------------------------------------------------------------------------- /src/client/components/NotSignedIn.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button } from '@mui/material'; 3 | import { useNavigate } from 'react-router-dom'; 4 | 5 | const NotSignedIn = (props) => { 6 | const navigate = useNavigate(); 7 | return props.trigger ? ( 8 |
9 |
10 |

Please Sign In Or Sign Up To View Your Saved Projects

11 | 21 | 31 |
32 |
33 | ) : ( 34 | null 35 | ); 36 | }; 37 | 38 | export default NotSignedIn; 39 | -------------------------------------------------------------------------------- /src/client/components/ProjectDeleted.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button } from '@mui/material'; 3 | import { useNavigate } from 'react-router-dom'; 4 | 5 | const ProjectDeleted = (props) => { 6 | const navigate = useNavigate(); 7 | return props.trigger ? ( 8 |
9 |
10 |

Project Deleted

11 | 24 | 37 |
38 |
39 | ) : ( 40 | null 41 | ); 42 | }; 43 | 44 | export default ProjectDeleted; -------------------------------------------------------------------------------- /src/client/components/ProjectSaved.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button } from '@mui/material'; 3 | import { useNavigate } from 'react-router-dom'; 4 | 5 | const ProjectSaved = (props) => { 6 | const navigate = useNavigate(); 7 | return props.trigger ? ( 8 |
9 |
10 |

Project Saved!

11 | 21 | 33 |
34 |
35 | ) : ( 36 | null 37 | ); 38 | }; 39 | 40 | export default ProjectSaved; 41 | -------------------------------------------------------------------------------- /src/client/components/ProjectToolbar.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | import { 4 | SpeedDial, 5 | SpeedDialAction, 6 | SpeedDialIcon, 7 | } from '@mui/material'; 8 | import EditIcon from '@mui/icons-material/Edit'; 9 | import SaveIcon from '@mui/icons-material/Save'; 10 | import FolderOpenIcon from '@mui/icons-material/FolderOpen'; 11 | import UpgradeIcon from '@mui/icons-material/Upgrade'; 12 | import SaveProject from './SaveProject'; 13 | import ProjectSaved from './ProjectSaved'; 14 | import UpdateProject from './UpdateProject'; 15 | import ProjectUpdated from './ProjectUpdated'; 16 | import NotSignedIn from './NotSignedIn'; 17 | 18 | const ProjectToolbar = (props) => { 19 | const { 20 | currentUserId, 21 | schemaData, 22 | treeData, 23 | resolverData, 24 | projectId, 25 | projectName, 26 | setProjectName, 27 | } = props; 28 | 29 | const [saveProjExpand, setSaveProjExpand] = useState(false); 30 | const [projectSaved, setProjectSaved] = useState(false); 31 | const navigate = useNavigate(); 32 | 33 | const [updateProjExpand, setUpdateProjExpand] = useState(false); 34 | const [projectUpdated, setProjectUpdated] = useState(false); 35 | 36 | const useInput = (e) => { 37 | //click event from saveproject 38 | setProjectName(e.target.value); 39 | }; 40 | 41 | const saveProjectFunc = () => { 42 | if (projectName === '') return alert('Please enter a project name'); 43 | if ( 44 | props.schemaData === 'Enter a Postgres DB link to generate your schema...' 45 | ) 46 | return alert('Please enter a database link to start your project'); 47 | if (!props.currentUserId) 48 | return alert('You must be signed in to save a project'); 49 | 50 | const date = new Date().toString(); 51 | const body = { 52 | user: currentUserId, 53 | projectName: projectName, 54 | schemaData: schemaData, 55 | treeData: treeData, 56 | date: date, 57 | resolverData: resolverData, 58 | }; 59 | console.log('post body', body); 60 | fetch('/projects/save', { 61 | method: 'POST', 62 | headers: { 63 | 'Content-Type': 'Application/JSON', 64 | }, 65 | body: JSON.stringify(body), 66 | }) 67 | .then((data) => data.json()) 68 | .then((data) => { 69 | console.log(data); 70 | }) 71 | .catch((err) => console.log('dbLink fetch /project/save: ERROR:', err)); 72 | setProjectSaved(true); 73 | setSaveProjExpand(false); 74 | setProjectName(''); //clear projectname after save. not necessary? 75 | }; 76 | 77 | const updateProjectFunc = async (newName) => { 78 | if (newName === '' || newName === ' ') { 79 | newName = projectName; 80 | } 81 | const date = new Date().toString(); 82 | const body = { 83 | id: projectId, 84 | name: newName, 85 | schema: schemaData, 86 | date: date, 87 | resolver: resolverData, 88 | }; 89 | const request = await fetch('/projects/update', { 90 | method: 'PATCH', 91 | headers: { 'Content-Type': 'application/json' }, 92 | body: JSON.stringify(body), 93 | }); 94 | const response = await request.json(); 95 | console.log(response.success); 96 | if (response.success) setProjectUpdated(true); 97 | else alert("couldn't update project"); 98 | setUpdateProjExpand(false); 99 | }; 100 | 101 | const actions = [ 102 | { 103 | icon: , 104 | name: 'Save Project', 105 | function: function () { 106 | if (props.loggedIn) { 107 | return setSaveProjExpand(true); 108 | } else { 109 | props.setNotSignedInPop(true); 110 | return; 111 | } 112 | }, 113 | }, 114 | { 115 | icon: , 116 | name: 'View Projects', 117 | function: function () { 118 | console.log(props.loggedIn); 119 | if (props.loggedIn) { 120 | console.log('in true'); 121 | navigate('/myprojects'); 122 | } else { 123 | console.log('in false'); 124 | props.setNotSignedInPop(true); 125 | return; 126 | } 127 | }, 128 | }, 129 | { 130 | icon: , 131 | name: 'Upate Project', 132 | function: function () { 133 | if (!projectId) return alert('Plese load a saved project.'); 134 | return setUpdateProjExpand(true); 135 | }, 136 | }, 137 | ]; 138 | const actionSize = { 139 | width: 90, 140 | height: 90, 141 | tooltip: { 142 | fontSize: 90, 143 | }, 144 | }; 145 | const classes = { 146 | tooltip: { 147 | fontSize: 90, 148 | }, 149 | }; 150 | 151 | return ( 152 |
153 | } />} 167 | > 168 | {actions.map((action) => ( 169 | { 176 | return action.function(); 177 | }} 178 | /> 179 | ))} 180 | 181 | 188 | 189 | 195 | 196 | 200 |
201 | ); 202 | }; 203 | 204 | export default ProjectToolbar; 205 | -------------------------------------------------------------------------------- /src/client/components/ProjectUpdated.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button } from '@mui/material'; 3 | import { useNavigate } from 'react-router-dom'; 4 | 5 | const ProjectUpdated = (props) => { 6 | const navigate = useNavigate(); 7 | return props.trigger ? ( 8 |
9 |
10 |

Project Updated!

11 | 21 | 33 |
34 |
35 | ) : ( 36 | null 37 | ); 38 | }; 39 | 40 | export default ProjectUpdated; -------------------------------------------------------------------------------- /src/client/components/ProjectsGrid.js: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import Button from '@mui/material/Button'; 3 | import { DataGrid } from '@mui/x-data-grid'; 4 | import { useNavigate } from 'react-router-dom'; 5 | import DeleteProject from './DeleteProject'; 6 | import ProjectDeleted from './ProjectDeleted'; 7 | const ProjectsGrid = props => { 8 | 9 | const { projects, setTreeData, dbSchemaDataOnChange, setResolverData, projectId, setProjectId, setProjectName, deletePopup, setDeletePopup, setGetData } = props; 10 | const [projectDeleted, setProjectDeleted ] = useState(false); 11 | const navigate = useNavigate(); 12 | 13 | 14 | const deleteProjectFunc = async () => { 15 | console.log('projectid in deletefunc', projectId); 16 | const request = await fetch(`/projects/delete/${projectId}`, { 17 | method: 'DELETE', 18 | headers: {'Content-Type': 'application/json'} 19 | }) 20 | const response = await request.json(); 21 | if (response.success) setProjectDeleted(true) 22 | else alert('unable to delete project'); 23 | props.setDeletePopup(false); 24 | } 25 | 26 | const columns = [ 27 | { field: 'name', headerName: 'Project Name', width: 300 }, 28 | { field: 'date', headerName: 'Last Updated', width: 500 }, 29 | { 30 | field: 'open', 31 | headerName: '', 32 | sortable: false, 33 | width: 200, 34 | renderCell: (params) => { 35 | const openProject = (e) => { 36 | e.stopPropagation(); // don't select this row after clicking 37 | setTreeData(JSON.parse(params.row.tree)); 38 | dbSchemaDataOnChange(params.row.schema); 39 | setResolverData(params.row.resolver); 40 | setProjectId(params.row.id); 41 | setProjectName(params.row.name); 42 | navigate('/'); 43 | 44 | }; 45 | 46 | return ; 47 | }, 48 | }, 49 | { 50 | field: 'delete', 51 | headerName: '', 52 | sortable: false, 53 | width: 250, 54 | renderCell: (params) => { 55 | const deletePop = async (e) => { 56 | console.log('params',params); 57 | e.stopPropagation(); // don't select this row after clicking 58 | setProjectId(params.id); 59 | setDeletePopup(true); 60 | }; 61 | 62 | return ; 63 | }, 64 | }, 65 | ]; 66 | 67 | const rows = []; 68 | 69 | function makeData(project) { 70 | const schema = project.schema_data; 71 | const tree = project.tree_data; 72 | const id = project.id; 73 | const name = project.project_name; 74 | const date = project.last_updated; 75 | const resolver = project.resolver_data; 76 | return { id, name, date, schema, tree, resolver } 77 | } 78 | projects.forEach(proj => { 79 | rows.push(makeData(proj)) 80 | }); 81 | 82 | return ( 83 | 84 |
85 |

My Projects

86 |
87 | 88 |
89 |
90 | 91 | 92 |
93 |
94 | 95 | ); 96 | 97 | }; 98 | export default ProjectsGrid; -------------------------------------------------------------------------------- /src/client/components/ProjectsPage.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import ProjectsGrid from './ProjectsGrid'; 3 | 4 | const ProjectsPage = (props) => { 5 | const [projects, updateProjects] = useState([]); 6 | const [deletePopup, setDeletePopup] = useState(false); 7 | const { 8 | currentUserId, 9 | setTreeData, 10 | dbSchemaDataOnChange, 11 | setResolverData, 12 | projectId, 13 | setProjectId, 14 | setProjectName, 15 | } = props; 16 | const [getData, setGetData] = useState(true); 17 | 18 | const fetchData = async () => { 19 | if (!getData) return; 20 | console.log('id:', currentUserId); 21 | const data = await fetch(`/projects/${currentUserId}`, { 22 | headers: { 'Content-Type': 'application/json' }, 23 | }); 24 | const projectList = await data.json(); 25 | updateProjects(projectList); 26 | setGetData(false); 27 | }; 28 | 29 | fetchData(); 30 | 31 | return ( 32 |
33 |
34 | 46 |
47 |
48 | ); 49 | }; 50 | 51 | export default ProjectsPage; 52 | -------------------------------------------------------------------------------- /src/client/components/SaveProject.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button, TextField } from '@mui/material'; 3 | 4 | const SaveProject = (props) => { 5 | return props.trigger ? ( 6 |
7 |
8 |

Save Your Project

9 | 15 |
16 | 26 | 38 |
39 |
40 |
41 | ) : ( 42 | null 43 | ); 44 | }; 45 | 46 | export default SaveProject; 47 | -------------------------------------------------------------------------------- /src/client/components/SchemaContainer.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import Editor from 'react-simple-code-editor'; 3 | import { highlight, languages } from 'prismjs/components/prism-core'; 4 | import 'prismjs/components/prism-clike'; 5 | import 'prismjs/components/prism-javascript'; 6 | import 'prismjs/themes/prism-okaidia.css'; 7 | import { IconButton, Tooltip, Tab } from '@mui/material'; 8 | import { TabContext, TabList, TabPanel } from '@mui/lab'; 9 | import ContentCopyIcon from '@mui/icons-material/ContentCopy'; 10 | import DoneOutlineIcon from '@mui/icons-material/DoneOutline'; 11 | import OpenInNewIcon from '@mui/icons-material/OpenInNew'; 12 | import EditorPopOutHandler from './EditorPopOutHandler'; 13 | 14 | 15 | 16 | type SchemaContainerProps = { 17 | dbSchemaData: string; 18 | dbSchemaDataOnChange: Function; 19 | resolverData: string; 20 | setResolverData: Function; 21 | showTree: boolean; 22 | setShowTree: Function; 23 | }; 24 | 25 | 26 | 27 | const SchemaContainer = ({ 28 | dbSchemaData, 29 | dbSchemaDataOnChange, 30 | resolverData, 31 | setResolverData, 32 | setShowTree 33 | }: SchemaContainerProps) => { 34 | const [currIcon, setCurrIcon] = useState( 35 | 36 | ); 37 | const [currTooltip, setCurrTooltip] = useState(

Copy

); 38 | 39 | // const [currClick, setCurrClick] = useState(false); 40 | //handle state of current tab 41 | const [tab, setTab] = useState('1'); 42 | const [editorExpand, setEditorExpand] = useState(false); 43 | 44 | 45 | const changeDisplay = () => { 46 | setEditorExpand(true); 47 | setShowTree(false); 48 | } 49 | 50 | //handle changing of tabs 51 | const handleChange = (event: React.SyntheticEvent, newValue: string) => { 52 | setTab(newValue); 53 | }; 54 | 55 | const resetIcons = () => { 56 | setCurrTooltip(

Copy

); 57 | setCurrIcon(); 58 | }; 59 | 60 | function delay(callback: Function, waitTime: number) { 61 | return function delayedFunction() { 62 | return setTimeout(callback, waitTime); 63 | }; 64 | } 65 | const delayedFunc = delay(() => resetIcons(), 3000); 66 | 67 | const handleClick = () => { 68 | 69 | if(tab === '1') navigator.clipboard.writeText(dbSchemaData); 70 | else navigator.clipboard.writeText(resolverData); 71 | setCurrTooltip(

Copied

); 72 | setCurrIcon(); 73 | delayedFunc(); 74 | }; 75 | 76 | return ( 77 |
78 | 79 | 80 | 83 | 84 | 85 | 86 |
87 | dbSchemaDataOnChange(code)} 91 | highlight={(code) => highlight(code, languages.js)} 92 | style={{ 93 | fontFamily: '"Fira code", "Fira Mono", monospace', 94 | fontSize: 20, 95 | }} 96 | /> 97 |
98 | 99 | 104 | {currIcon} 105 | 106 | 107 | Open in New Window} placement='top' arrow> 108 | 112 | {} 113 | 114 | 115 | 124 | 125 |
126 | 127 |
128 | setResolverData(code)} 132 | highlight={(code) => highlight(code, languages.js)} 133 | style={{ 134 | fontFamily: '"Fira code", "Fira Mono", monospace', 135 | fontSize: 20, 136 | }} 137 | /> 138 |
139 | 140 | 145 | {currIcon} 146 | 147 | 148 | Open in New Window} placement='top' arrow> 149 | 153 | {} 154 | 155 | 156 | 165 |
166 | 167 |
168 | 169 |
170 | ); 171 | }; 172 | 173 | export default SchemaContainer; -------------------------------------------------------------------------------- /src/client/components/Team.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Kelly from '../assets/Kelly.jpg'; 3 | import Jordan from '../assets/Jordan.jpg'; 4 | import Cavin from '../assets/Cavin.jpg'; 5 | import Rebecca from '../assets/Rebecca.png'; 6 | import TeamCards from './TeamCards'; 7 | 8 | const Team = () => { 9 | const bios = [ 10 | { 11 | teammembername: 'Kelly Cuevas', 12 | title: 'Software Engineer', 13 | headshot: Kelly, 14 | github: 'https://github.com/KellyCuevas', 15 | linkedin: 'https://www.linkedin.com/in/kelly-cuevas/', 16 | }, 17 | { 18 | teammembername: 'Jordan Jeter', 19 | title: 'Software Engineer', 20 | headshot: Jordan, 21 | github: 'https://github.com/gpys', 22 | linkedin: 'https://www.linkedin.com/', 23 | }, 24 | { 25 | teammembername: 'Cavin Park', 26 | title: 'Software Engineer', 27 | headshot: Cavin, 28 | github: 'https://github.com/sanghpark1', 29 | linkedin: 'https://www.linkedin.com/in/sanghpark1/', 30 | }, 31 | { 32 | teammembername: 'Rebecca Shesser', 33 | title: 'Software Engineer', 34 | headshot: Rebecca, 35 | github: 'https://github.com/rebshess', 36 | linkedin: 'https://www.linkedin.com/in/rebeccashesser/', 37 | }, 38 | ]; 39 | const bioArr = bios.map((bio, i) => { 40 | return ; 41 | }); 42 | 43 | return ( 44 |
45 |

Meet the Team

46 |
{bioArr}
47 |
48 | ); 49 | }; 50 | 51 | export default Team; 52 | -------------------------------------------------------------------------------- /src/client/components/TeamCards.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { GitHub } from '@mui/icons-material'; 3 | import { LinkedIn } from '@mui/icons-material'; 4 | import Kelly from '../assets/Kelly.jpg'; 5 | import Jordan from '../assets/Jordan.jpg'; 6 | import Cavin from '../assets/Cavin.jpg'; 7 | import Rebecca from '../assets/Rebecca.png'; 8 | import { info } from 'console'; 9 | 10 | type TeamCardsProps = { 11 | info: { 12 | teammembername: string; 13 | title: string; 14 | headshot: string; 15 | linkedin: string; 16 | github: string; 17 | } 18 | } 19 | const TeamCards = ({ 20 | info: { 21 | teammembername, 22 | title, 23 | headshot, 24 | linkedin, 25 | github 26 | } 27 | }: TeamCardsProps) => { 28 | 29 | return ( 30 |
31 |

{teammembername}

32 | 33 |
34 |
  • {title}
  • 35 |
    36 |
  • 37 | 38 | 39 | LinkedIn 40 | 41 |
  • 42 |
    43 |
  • 44 | 45 | 46 | GitHub 47 | 48 |
  • 49 |
    50 |
    51 | ); 52 | }; 53 | 54 | export default TeamCards; 55 | -------------------------------------------------------------------------------- /src/client/components/Tree.js: -------------------------------------------------------------------------------- 1 | import React, { useRef, useEffect, useState } from 'react'; 2 | import { 3 | select, 4 | hierarchy, 5 | tree, 6 | linkHorizontal, 7 | zoom, 8 | } from 'd3'; 9 | 10 | 11 | const Tree = ({ data }) => { 12 | const svgRef = useRef(); 13 | 14 | useEffect(() => { 15 | const svg = select(svgRef.current); 16 | const clearAllNodes = svg.selectAll('g').remove(); 17 | const root = hierarchy(data); 18 | const cacheReferenceTableNames = {}; 19 | root.descendants().forEach((d, i) => { 20 | d.id = i; 21 | d._children = d.children; 22 | if (d.depth === 3) { 23 | cacheReferenceTableNames[d.data.name] = []; 24 | d.children = d.children ? null : d._children; 25 | } 26 | }); 27 | 28 | const zoomG = svg.append('g'); 29 | //append gLink and gNode to zoomG in order to capture everything rendered for the zoom 30 | const gLink = zoomG.append('g').attr('fill', 'none'); 31 | 32 | const gNode = zoomG.append('g') 33 | .attr('cursor', 'pointer') 34 | .attr('pointer-events', 'all') 35 | .attr('id', 'node-parent'); 36 | 37 | //handleZoom takes zoom event object 38 | const handleZoom = ({transform}) => { 39 | zoomG.attr('transform', transform); 40 | } 41 | 42 | const Zoom = zoom() 43 | .scaleExtent([0.7, 8]) 44 | .on('zoom', handleZoom); 45 | svg.call(Zoom); 46 | 47 | const update = (source) => { 48 | const duration = 500; 49 | const nodes = root.descendants().reverse(); 50 | const links = root.links(); 51 | 52 | const treeLayout = tree().size([ 53 | document.getElementById('diagram').clientHeight, 54 | document.getElementById('diagram').clientWidth, 55 | ]); 56 | treeLayout(root); 57 | 58 | //make size of diagram div responsive 59 | const diagramDiv = document.getElementById('diagram'); 60 | svg.attr('viewBox', [ 61 | document.getElementById('diagram').clientWidth * -0.11, 62 | document.getElementById('diagram').clientHeight * 0.3, 63 | document.getElementById('diagram').clientWidth * 1.3, 64 | document.getElementById('diagram').clientHeight / 2, 65 | ]); 66 | 67 | 68 | const linkGenerator = linkHorizontal() 69 | .x((node) => node.y) 70 | .y((node) => node.x); 71 | 72 | const transition = svg.transition().duration(duration); 73 | 74 | const node = gNode.selectAll('g').data(nodes, (d) => d.id); 75 | 76 | const nodeEnter = node 77 | .enter() 78 | .append('g') 79 | .attr('class', 'node') 80 | .attr('transform', (d) => `translate(${source.y0},${source.x0})`) 81 | .attr('fill-opacity', 0) 82 | .attr('stroke-opacity', 0); 83 | 84 | nodeEnter 85 | .append('circle') 86 | .attr('class', (d) => { 87 | if (d.depth === 2 || d.depth === 4) return 'circle column-circle'; 88 | return 'circle'; 89 | }) 90 | .attr('r', (d) => { 91 | const nodeCountCircleSizeRatio = (document.getElementById('diagram').clientHeight) / (document.getElementById('node-parent').childElementCount) / 2; 92 | if (nodeCountCircleSizeRatio < 3 && (d.depth === 2 || d.depth === 4)) { 93 | return nodeCountCircleSizeRatio; 94 | } 95 | return 3; 96 | }) 97 | .attr('fill', (d) => (d._children ? '#ed6a5a' : '#5ca4a9')) 98 | .attr('stroke-width', 10) 99 | .on('click', (event, d) => { 100 | d.children = d.children ? null : d._children; 101 | update(d); 102 | const nodeCountfontSizeRatio = (document.getElementById('diagram').clientHeight) / (document.getElementById('node-parent').childElementCount) * 1.35; 103 | if (nodeCountfontSizeRatio < 16) { 104 | svg.selectAll('.column-node').attr('font-size', nodeCountfontSizeRatio); 105 | } else { 106 | svg.selectAll('.column-node').attr('font-size', 16); 107 | } 108 | const nodeCountCircleSizeRatio = (document.getElementById('diagram').clientHeight) / (document.getElementById('node-parent').childElementCount) / 2; 109 | if (nodeCountCircleSizeRatio < 3) { 110 | svg.selectAll('.column-circle').attr('r', nodeCountCircleSizeRatio); 111 | } else { 112 | svg.selectAll('.column-circle').attr('r', 3); 113 | } 114 | }); 115 | 116 | nodeEnter 117 | .append('text') 118 | .attr('class', (d) => { 119 | if (d.depth === 2 || d.depth === 4) return 'label column-node' 120 | return 'label'; 121 | }) 122 | .attr('id', (d) => `aa${d.id}`) 123 | .attr('dy', '0.31em') 124 | .attr('x', (d) => (d._children ? -6 : 6)) 125 | .attr('text-anchor', (d) => (d._children ? 'end' : 'beginning')) 126 | // .attr('fill', (d) => { 127 | // if (d.data.name.slice(0, 7) === 'primKey') return 'red'; 128 | .attr('fill', (d) => { 129 | if (d.depth === 4 && d.data.name.slice(0, 7) === 'primKey') 130 | return 'red'; 131 | return 'black'; 132 | }) 133 | .text((d) => { 134 | if (d.data.name.slice(0, 7) === 'primKey') { 135 | d.data.name = d.data.name.slice(7); 136 | } 137 | if ( 138 | (d.depth === 1 || d.depth === 3) && 139 | cacheReferenceTableNames[d.data.name] 140 | ) { 141 | cacheReferenceTableNames[d.data.name].push(d.id); 142 | } 143 | return d.data.name; 144 | }) 145 | .on('click', (event, node) => { 146 | if (node.depth === 0 || node.depth === 1 || node.depth === 3) return; 147 | if (node.data.name[node.data.name.length - 1] === '!') { 148 | node.data.name = node.data.name.slice(0, -1); 149 | } else node.data.name += '!'; 150 | console.log('node.data.name: ', node.data.name); 151 | svg.selectAll('.label').text((d) => d.data.name); 152 | }) 153 | .attr('font-size', d => { 154 | const nodeCountfontSizeRatio = (document.getElementById('diagram').clientHeight) / (document.getElementById('node-parent').childElementCount) * 1.35; 155 | if (nodeCountfontSizeRatio <= 16 && (d.depth === 2 || d.depth === 4)) { 156 | return nodeCountfontSizeRatio; 157 | }; 158 | return 16; 159 | }) 160 | .on('mouseover', (event, d) => { 161 | if (cacheReferenceTableNames[d.data.name]) { 162 | cacheReferenceTableNames[d.data.name].forEach((ele) => { 163 | svg.selectAll(`#aa${ele}`).attr('fill', 'orange'); 164 | }); 165 | } 166 | }) 167 | .on('mouseout', (event, d) => { 168 | cacheReferenceTableNames[d.data.name].forEach((ele) => { 169 | svg.selectAll(`#aa${ele}`).attr('fill', 'black'); 170 | }); 171 | }) 172 | .clone(true) 173 | .lower() 174 | .attr('stroke-linejoin', 'round') 175 | .attr('stroke-width', 3) 176 | .attr('stroke-opacity', 0); 177 | 178 | const nodeUpdate = node 179 | .merge(nodeEnter) 180 | .transition(transition) 181 | .attr('transform', (d) => `translate(${d.y},${d.x})`) 182 | .attr('fill-opacity', 1) 183 | .attr('stroke-opacity', 1); 184 | 185 | const nodeExit = node 186 | .exit() 187 | .transition(transition) 188 | .remove() 189 | .attr('transform', (d) => `translate(${source.y},${source.x})`) 190 | .attr('fill-opacity', 0) 191 | .attr('stroke-opacity', 0); 192 | 193 | const link = gLink.selectAll('path').data(links, (d) => d.target.id); 194 | 195 | const linkEnter = link 196 | .enter() 197 | .append('path') 198 | .attr('class', 'link') 199 | .attr('stroke', '#5ca4a9') 200 | .attr('d', (d) => { 201 | const o = { x: source.x0, y: source.y0 }; 202 | return linkGenerator({ source: o, target: o }); 203 | }); 204 | 205 | link 206 | .merge(linkEnter) 207 | .transition(transition) 208 | .attr('d', linkGenerator) 209 | .transition() 210 | .attr('stroke-dashoffset', 0); 211 | 212 | link 213 | .exit() 214 | .transition(transition) 215 | .remove() 216 | .attr('d', (d) => { 217 | const o = { x: source.x, y: source.y }; 218 | return linkGenerator({ source: o, target: o }); 219 | }); 220 | 221 | root.eachBefore((d) => { 222 | d.x0 = d.x; 223 | d.y0 = d.y; 224 | }); 225 | 226 | 227 | 228 | }; 229 | 230 | update(root); 231 | }, [data]); 232 | 233 | return ( 234 |
    235 | 236 |
    237 | ); 238 | }; 239 | 240 | export default Tree; 241 | -------------------------------------------------------------------------------- /src/client/components/TreePopOutHandler.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button } from '@mui/material'; 3 | 4 | import Tree from './Tree'; 5 | 6 | const TreePopOutHandler = (props) => { 7 | return props.trigger ? ( 8 |
    9 |
    10 |
    11 | 24 | 25 |
    26 |
    27 |
    28 | ) : ( 29 | ' ' 30 | ); 31 | }; 32 | 33 | export default TreePopOutHandler; 34 | -------------------------------------------------------------------------------- /src/client/components/UpdateProject.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Button, TextField } from '@mui/material'; 3 | 4 | const UpdateProject = (props) => { 5 | const [newName, setNewName] = useState(props.projectName) 6 | const updateProjName = e => { 7 | setNewName(e.target.value) 8 | }; 9 | //const placeholderString = `current name: ${props.projectName}` 10 | return props.trigger ? ( 11 |
    12 |
    13 |

    Update Project

    14 | 19 |
    20 | 30 | 42 |
    43 |
    44 |
    45 | ) : ( 46 | null 47 | ); 48 | }; 49 | 50 | export default UpdateProject; -------------------------------------------------------------------------------- /src/client/components/VisualizerContainer.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | //@ts-ignore 3 | import Tree from './Tree'; 4 | import OpenInNewIcon from '@mui/icons-material/OpenInNew'; 5 | import { IconButton, Tooltip } from '@mui/material'; 6 | //@ts-ignore 7 | import TreePopOutHandler from './TreePopOutHandler'; 8 | 9 | const VisualizerContainer = (props: { data: object, showTree: boolean}) => { 10 | const [treeExpand, setTreeExpand] = useState(false); 11 | 12 | return props.showTree? ( 13 |
    14 |
    15 |
    16 | 17 |
    18 |
    19 | Open in New Window} placement='top' arrow> 20 | { 23 | setTreeExpand(true); 24 | }} 25 | > 26 | {} 27 | 28 | 29 | 34 |
    35 | ) : ( 36 | null 37 | ); 38 | }; 39 | 40 | export default VisualizerContainer; 41 | //might need the fetch request and onSubmit here to trigger the update to data 42 | // 43 | -------------------------------------------------------------------------------- /src/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | VisiQL 7 | 11 | 12 | 13 |
    14 | 15 | 16 | -------------------------------------------------------------------------------- /src/client/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BrowserRouter } from 'react-router-dom'; 3 | import { createRoot } from 'react-dom/client'; 4 | import App from './App'; 5 | import styles from './scss/_index.scss'; 6 | 7 | 8 | const container = document.getElementById('root'); 9 | const root = createRoot(container); 10 | root.render( 11 | 12 | 13 | 14 | ); 15 | -------------------------------------------------------------------------------- /src/client/scss/_index.scss: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Kumbh+Sans:wght@100;200;300;400;500;600;700;800;900&display=swap'); 2 | @import '_variables'; 3 | 4 | * { 5 | margin: 0; 6 | padding: 0; 7 | // box-sizing: border-box; 8 | } 9 | html { 10 | margin: 0; 11 | padding: 0; 12 | } 13 | body { 14 | margin-top: 9em; 15 | padding: 0; 16 | } 17 | head { 18 | margin: 0; 19 | padding: 0; 20 | } 21 | 22 | a { 23 | text-decoration: none; 24 | color: $darkteal; 25 | &:hover { 26 | color: $lightteal; 27 | } 28 | } 29 | 30 | #navbar { 31 | font-family: 'Kumbh Sans', sans-serif; 32 | position: fixed; 33 | display: flex; 34 | flex-direction: row; 35 | top: 0; 36 | padding-top: 1rem; 37 | padding-bottom: 1rem; 38 | padding-left: 1rem; 39 | background-color: $paleteal; 40 | justify-content: space-between; 41 | align-items: center; 42 | width: 100%; 43 | z-index: 5; 44 | } 45 | 46 | ul { 47 | display: flex; 48 | flex-direction: row; 49 | gap: 20px; 50 | } 51 | 52 | li { 53 | list-style: none; 54 | font-size: 30px; 55 | margin-right: 2rem; 56 | } 57 | 58 | #vis-container { 59 | display: flex; 60 | flex-direction: column; 61 | gap: 40px; 62 | align-content: center; 63 | justify-content: center; 64 | overflow: scroll; 65 | } 66 | 67 | #tree-svg { 68 | font-family: 'Kumbh Sans', sans-serif; 69 | height: 70vh; 70 | width: 55vw; 71 | overflow-x: scroll; 72 | overflow-y: scroll; 73 | align-self: center; 74 | justify-self: center; 75 | } 76 | 77 | .db-input { 78 | display: flex; 79 | flex-direction: row; 80 | gap: 20px; 81 | margin-right: auto; 82 | margin-left: auto; 83 | margin-top: 3rem; 84 | } 85 | 86 | 87 | #homepage { 88 | display: flex; 89 | flex-direction: row; 90 | } 91 | 92 | .schema-editor-container { 93 | background-color: rgb(127, 127, 127); 94 | border-radius: 10px; 95 | margin-top: 2rem; 96 | height: 70vh; 97 | width: 20vw; 98 | // overflow-x: scroll; 99 | overflow-y: auto; 100 | box-shadow: rgba(0, 0, 0, 0.25) 0px 54px 55px, 101 | rgba(0, 0, 0, 0.12) 0px -12px 30px, rgba(0, 0, 0, 0.12) 0px 4px 6px, 102 | rgba(0, 0, 0, 0.17) 0px 12px 13px, rgba(0, 0, 0, 0.09) 0px -3px 5px; 103 | } 104 | 105 | 106 | 107 | 108 | .combined-editor-container { 109 | background-color: rgb(127, 127, 127); 110 | border-radius: 10px; 111 | margin-top: 3rem; 112 | overflow-y: auto; 113 | overflow-x: auto; 114 | display: flex; 115 | flex-direction: row; 116 | align-items: flex-start; 117 | align-content: space-between; 118 | justify-content: space-evenly; 119 | gap: 10px; 120 | height: 80vh; 121 | width: 80vw; 122 | padding: 50px; 123 | margin-right: auto; 124 | margin-left: auto; 125 | border-radius: 10px; 126 | 127 | } 128 | // below is for resolver component 129 | .schema-editor-container2 { 130 | background-color: rgb(127, 127, 127); 131 | border-radius: 10px; 132 | margin-top: 2rem; 133 | height: 100%; 134 | width: 100%; 135 | overflow-x: auto; 136 | overflow-y: auto; 137 | box-shadow: rgba(0, 0, 0, 0.25) 0px 54px 55px, 138 | rgba(0, 0, 0, 0.12) 0px -12px 30px, rgba(0, 0, 0, 0.12) 0px 4px 6px, 139 | rgba(0, 0, 0, 0.17) 0px 12px 13px, rgba(0, 0, 0, 0.09) 0px -3px 5px; 140 | } 141 | // above is for resolver component 142 | 143 | #diagram { 144 | font-family: 'Kumbh Sans', sans-serif; 145 | border-radius: 10px; 146 | height: 70vh; 147 | width: 55vw; 148 | margin-top: 2rem; 149 | box-shadow: rgba(0, 0, 0, 0.25) 0px 54px 55px, 150 | rgba(0, 0, 0, 0.12) 0px -12px 30px, rgba(0, 0, 0, 0.12) 0px 4px 6px, 151 | rgba(0, 0, 0, 0.17) 0px 12px 13px, rgba(0, 0, 0, 0.09) 0px -3px 5px; 152 | overflow: auto; 153 | 154 | } 155 | 156 | .schema-vis-container { 157 | display: flex; 158 | flex-direction: row; 159 | justify-content: center; 160 | gap: 100px; 161 | position: relative; 162 | top: 0; 163 | left: 0; 164 | } 165 | 166 | .tree-vis-container { 167 | display: flex; 168 | flex-direction: row; 169 | justify-content: center; 170 | gap: 100px; 171 | position: relative; 172 | top: 48px; 173 | left: 0; 174 | } 175 | .editor-container { 176 | position: relative; 177 | top: 0; 178 | left: 0; 179 | } 180 | .copy-button { 181 | position: absolute; 182 | float: right; 183 | top: -130px; 184 | left: -30px; 185 | } 186 | 187 | .expand-button { 188 | position: absolute; 189 | float: right; 190 | top: -80px; 191 | left: 30px; 192 | } 193 | 194 | .tree-expand-button { 195 | position: absolute; 196 | float: right; 197 | top: -100px; 198 | left: -30px; 199 | 200 | } 201 | 202 | .tree-pop-out, 203 | .editor-pop-out { 204 | display: flex; 205 | flex-direction: column; 206 | top: 0; 207 | left: 0; 208 | height: 100vh; 209 | position: fixed; 210 | width: 100%; 211 | background-color: rgba(0, 0, 0, 0.2); 212 | z-index: 999; 213 | } 214 | 215 | .tree-pop-out-container, 216 | .editor-pop-out-container { 217 | display: flex; 218 | flex-direction: column; 219 | align-items: center; 220 | align-content: space-between; 221 | gap: 10px; 222 | margin-top: 50px; 223 | background-color: rgb(251, 247, 242); 224 | height: 80vh; 225 | width: 80vw; 226 | padding: 50px; 227 | margin-right: auto; 228 | margin-left: auto; 229 | border-radius: 10px; 230 | } 231 | 232 | .tree-pop-out-close { 233 | position: absolute; 234 | top: .1rem; 235 | right: .1rem; 236 | color: #fff; 237 | } 238 | 239 | .editor-pop-out-close { 240 | position: fixed; 241 | top: .1rem; 242 | right: .1rem; 243 | color: #fff; 244 | height: 5rem; 245 | width: 5rem; 246 | } 247 | .save-project-popover-parent { 248 | display: flex; 249 | flex-direction: column; 250 | top: 0; 251 | left: 0; 252 | height: 100vh; 253 | position: fixed; 254 | width: 100%; 255 | background-color: rgba(0, 0, 0, 0.2); 256 | } 257 | 258 | .save-project-popover { 259 | display: flex; 260 | flex-direction: column; 261 | overflow: hidden; 262 | align-items: center; 263 | align-content: space-between; 264 | gap: 20px; 265 | margin-top: auto; 266 | margin-bottom: auto; 267 | background-color: rgb(251, 247, 242); 268 | height: 20em; 269 | width: 35em; 270 | padding: 50px; 271 | margin-right: auto; 272 | margin-left: auto; 273 | border-radius: 10px; 274 | font-family: 'Kumbh Sans', sans-serif; 275 | color: $darkteal; 276 | z-index: 1; 277 | } 278 | 279 | .project-save-cancel-buttons { 280 | display: flex; 281 | gap: 20px; 282 | } 283 | 284 | #projectTable { 285 | display: flex; 286 | flex-direction: row; 287 | justify-content: center; 288 | gap: 100px; 289 | position: relative; 290 | top: 0; 291 | left: 0; 292 | } 293 | 294 | #projectTitle { 295 | color: $orange; 296 | font-size: 3rem; 297 | margin-top: 25px; 298 | margin-bottom: 0; 299 | font-family: 'Kumbh Sans', sans-serif; 300 | text-align: center; 301 | } 302 | 303 | 304 | @media (max-width: 900px) { 305 | #navbar, ul { 306 | flex-direction: column; 307 | } 308 | } 309 | 310 | -------------------------------------------------------------------------------- /src/client/scss/_variables.scss: -------------------------------------------------------------------------------- 1 | $orange: #ed6a5a; 2 | $yellow: #f4f1bb; 3 | $darkteal: #5ca4a9; 4 | $lightteal: #9bc1bc; 5 | $paleteal: #e6ebe0; 6 | $offwhite: #fffdd3; 7 | -------------------------------------------------------------------------------- /src/client/scss/about.scss: -------------------------------------------------------------------------------- 1 | .team-container { 2 | display: inline; 3 | font-family: 'Kumbh Sans', sans-serif; 4 | text-align: center; 5 | padding-bottom: 10rem; 6 | } 7 | 8 | .headshots-links { 9 | display: flex; 10 | align-items: center; 11 | justify-content: center; 12 | gap: 1.5rem; 13 | padding-bottom: 10rem; 14 | } 15 | 16 | .team-title { 17 | font-size: 4rem; 18 | } 19 | .team-name { 20 | font-weight: normal; 21 | font-size: 2.5rem; 22 | margin-bottom: 10px; 23 | } 24 | 25 | .team-member-card a { 26 | font-size: 1.5rem; 27 | } 28 | 29 | li { 30 | display: flex; 31 | align-items: center; 32 | justify-content: center; 33 | gap: 5px; 34 | } 35 | .title { 36 | font-weight: bold; 37 | font-size: 1.75rem; 38 | } 39 | 40 | .intro-paragraph { 41 | color: rgb(88, 88, 88); 42 | font-size: 1.5rem; 43 | font-weight: normal; 44 | } 45 | 46 | .docs-title { 47 | text-align: center; 48 | font-family: 'Kumbh Sans', sans-serif; 49 | font-size: 4rem; 50 | } 51 | 52 | .visiql-statement { 53 | font-size: 2rem; 54 | text-align: center; 55 | font-style: italic; 56 | font-weight: normal; 57 | } 58 | 59 | .generating-docs-title { 60 | font-family: 'Kumbh Sans', sans-serif; 61 | font-size: 2rem; 62 | color: black; 63 | } 64 | 65 | .generating-docs { 66 | color: rgb(88, 88, 88); 67 | font-size: 1.5rem; 68 | font-weight: normal; 69 | } 70 | 71 | .doc-container { 72 | margin: 4rem; 73 | } 74 | 75 | 76 | @media (max-width: 900px) { 77 | .headshots-links { 78 | flex-direction: column; 79 | } 80 | html { 81 | font-size: 12px; 82 | } 83 | .doc-container { 84 | margin-top: 425px; 85 | } 86 | } -------------------------------------------------------------------------------- /src/client/scss/application.scss: -------------------------------------------------------------------------------- 1 | @import '_variables'; 2 | @import '_index'; 3 | -------------------------------------------------------------------------------- /src/client/scss/graphiql.scss: -------------------------------------------------------------------------------- 1 | .graphiql-container * { 2 | box-sizing: border-box; 3 | } 4 | 5 | .graphiql-container, 6 | .CodeMirror-info, 7 | .CodeMirror-lint-tooltip, 8 | reach-portal { 9 | /* Colors */ 10 | --color-primary: 320, 95%, 43%; 11 | --color-secondary: 242, 51%, 61%; 12 | --color-tertiary: 188, 100%, 36%; 13 | --color-info: 208, 100%, 46%; 14 | --color-success: 158, 60%, 42%; 15 | --color-warning: 36, 100%, 41%; 16 | --color-error: 13, 93%, 58%; 17 | --color-neutral: 219, 28%, 32%; 18 | --color-base: 219, 28%, 100%; 19 | 20 | /* Color alpha values */ 21 | --alpha-secondary: 0.76; 22 | --alpha-tertiary: 0.5; 23 | --alpha-background-heavy: 0.15; 24 | --alpha-background-medium: 0.1; 25 | --alpha-background-light: 0.07; 26 | 27 | /* Font */ 28 | --font-family: 'Roboto', sans-serif; 29 | --font-family-mono: 'Fira Code', monospace; 30 | --font-size-hint: calc(12rem / 16); 31 | --font-size-inline-code: calc(13rem / 16); 32 | --font-size-body: calc(15rem / 16); 33 | --font-size-h4: calc(18rem / 16); 34 | --font-size-h3: calc(22rem / 16); 35 | --font-size-h2: calc(29rem / 16); 36 | --font-weight-regular: 400; 37 | --font-weight-medium: 500; 38 | --line-height: 1.5; 39 | 40 | /* Spacing */ 41 | --px-2: 2px; 42 | --px-4: 4px; 43 | --px-6: 6px; 44 | --px-8: 8px; 45 | --px-10: 10px; 46 | --px-12: 12px; 47 | --px-16: 16px; 48 | --px-20: 20px; 49 | --px-24: 24px; 50 | 51 | /* Border radius */ 52 | --border-radius-2: 2px; 53 | --border-radius-4: 4px; 54 | --border-radius-8: 8px; 55 | --border-radius-12: 12px; 56 | 57 | /* Popover styles (tooltip, dialog, etc) */ 58 | --popover-box-shadow: 0px 6px 20px rgba(59, 76, 106, 0.13), 59 | 0px 1.34018px 4.46726px rgba(59, 76, 106, 0.0774939), 60 | 0px 0.399006px 1.33002px rgba(59, 76, 106, 0.0525061); 61 | --popover-border: none; 62 | 63 | /* Layout */ 64 | --sidebar-width: 60px; 65 | --toolbar-width: 40px; 66 | --session-header-height: 51px; 67 | } 68 | 69 | @media (prefers-color-scheme: dark) { 70 | body:not(.graphiql-light) .graphiql-container, 71 | body:not(.graphiql-light) .CodeMirror-info, 72 | body:not(.graphiql-light) .CodeMirror-lint-tooltip, 73 | body:not(.graphiql-light) reach-portal { 74 | --color-primary: 338, 100%, 67%; 75 | --color-secondary: 243, 100%, 77%; 76 | --color-tertiary: 188, 100%, 44%; 77 | --color-info: 208, 100%, 72%; 78 | --color-success: 158, 100%, 42%; 79 | --color-warning: 30, 100%, 80%; 80 | --color-error: 13, 100%, 58%; 81 | --color-neutral: 219, 29%, 78%; 82 | --color-base: 219, 29%, 18%; 83 | 84 | --popover-box-shadow: none; 85 | --popover-border: 1px solid hsl(var(--color-neutral)); 86 | } 87 | } 88 | 89 | body.graphiql-dark .graphiql-container, 90 | body.graphiql-dark .CodeMirror-info, 91 | body.graphiql-dark .CodeMirror-lint-tooltip, 92 | body.graphiql-dark reach-portal { 93 | --color-primary: 338, 100%, 67%; 94 | --color-secondary: 243, 100%, 77%; 95 | --color-tertiary: 188, 100%, 44%; 96 | --color-info: 208, 100%, 72%; 97 | --color-success: 158, 100%, 42%; 98 | --color-warning: 30, 100%, 80%; 99 | --color-error: 13, 100%, 58%; 100 | --color-neutral: 219, 29%, 78%; 101 | --color-base: 219, 29%, 18%; 102 | 103 | --popover-box-shadow: none; 104 | --popover-border: 1px solid hsl(var(--color-neutral)); 105 | } 106 | 107 | .graphiql-container, 108 | .CodeMirror-info, 109 | .CodeMirror-lint-tooltip, 110 | reach-portal { 111 | &, 112 | &:is(button) { 113 | color: hsla(var(--color-neutral), 1); 114 | font-family: var(--font-family); 115 | font-size: var(--font-size-body); 116 | font-weight: var(----font-weight-regular); 117 | line-height: var(--line-height); 118 | } 119 | 120 | & input { 121 | color: hsla(var(--color-neutral), 1); 122 | font-family: var(--font-family); 123 | font-size: var(--font-size-caption); 124 | 125 | &::placeholder { 126 | color: hsla(var(--color-neutral), var(--alpha-secondary)); 127 | } 128 | } 129 | 130 | & a { 131 | color: hsl(var(--color-primary)); 132 | 133 | &:focus { 134 | outline: hsl(var(--color-primary)) auto 1px; 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/client/scss/login.scss: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Kumbh+Sans:wght@100;200;300;400;500;600;700;800;900&display=swap'); 2 | @import '_variables'; 3 | 4 | * { 5 | margin: 0; 6 | padding: 0; 7 | box-sizing: border-box; 8 | } 9 | 10 | .login-body, .signin-body { 11 | background-color: $paleteal; 12 | min-height: 100vh; 13 | margin-top: -20px; 14 | } 15 | 16 | 17 | .login-logo { 18 | display: flex; 19 | flex-direction: column; 20 | justify-content: center; 21 | align-items: center; 22 | 23 | } 24 | // #login-img { 25 | // margin-top: 10vh; 26 | // display: hidden; 27 | 28 | // } 29 | 30 | 31 | 32 | // #signin-img { 33 | // margin-top: 5vh; 34 | 35 | // } 36 | 37 | h2 { 38 | color: $orange; 39 | font-size: 3rem; 40 | margin-top: 25px; 41 | margin-bottom: 30px; 42 | font-family: 'Kumbh Sans', sans-serif; 43 | } 44 | 45 | .login-body h2 { 46 | font-size: 4rem; 47 | margin-top: 50px; 48 | } 49 | 50 | .signin-body h2 { 51 | margin-bottom: 20px; 52 | margin-top: 5px; 53 | } 54 | 55 | .sign-up-message { 56 | font-family: 'Kumbh Sans', sans-serif; 57 | font-weight: 500; 58 | text-align: center; 59 | margin-top: 2rem; 60 | color: rgb(78, 74, 74); 61 | } 62 | 63 | @media (max-width: 900px) { 64 | .signin-body h2, .login-body h2 { 65 | margin-top: 375px; 66 | } 67 | 68 | .signin-body h2 { 69 | text-align: center; 70 | } 71 | } -------------------------------------------------------------------------------- /src/react.app.env.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.png'; 2 | declare module '*.svg'; 3 | declare module '*.jpeg'; 4 | declare module '*.scss'; 5 | declare module 'prismjs/components/prism-core'; 6 | declare module '*.ts'; 7 | declare module '*.jpg' { 8 | const value: any; 9 | export = value; 10 | }; -------------------------------------------------------------------------------- /src/server/controllers/authController.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const jwt = require('jsonwebtoken'); 3 | const cookieParser = require('cookie-parser'); 4 | 5 | const authController = {}; 6 | 7 | authController.checkToken = (req, res, next) => { 8 | const authCookie = req.cookies.token; 9 | if (!authCookie) { 10 | return next(); 11 | } else { 12 | jwt.verify(authCookie, process.env.ACCESS_TOKEN_SERVER, (err, user) => { 13 | if (err) { 14 | res.locals.authenticate = 'fail'; 15 | return next(); 16 | } else { 17 | res.locals.authenticate = { status: 'success', id: user.id }; 18 | return next(); 19 | } 20 | }); 21 | } 22 | }; 23 | 24 | module.exports = authController; 25 | -------------------------------------------------------------------------------- /src/server/controllers/dbLinkController.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const db = require('../models/models'); 3 | 4 | const dbLinkController = {}; 5 | 6 | dbLinkController.connectDemo = async (req, res, next) => { 7 | try { 8 | // insert database link into .env file for use to connect database and make queries 9 | process.env.PG_URI = 'postgres://xstskucd:HUMKg1LzALryqlQM26N5uKLWF5ol1fbT@peanut.db.elephantsql.com/xstskucd'; 10 | db.newPool(); 11 | return next(); 12 | } catch (err) { 13 | return next({ err }); 14 | } 15 | }; 16 | 17 | dbLinkController.connectDb = async (req, res, next) => { 18 | try { 19 | // insert database link into .env file for use to connect database and make queries 20 | process.env.PG_URI = req.body.dbLink; 21 | db.newPool(); 22 | return next(); 23 | } catch (err) { 24 | return next({ err }); 25 | } 26 | }; 27 | 28 | dbLinkController.extractFnKeys = async (req, res, next) => { 29 | try { 30 | const fKQuery = 31 | "SELECT conrelid::regclass AS table_name, pg_get_constraintdef(oid) FROM pg_constraint WHERE contype = 'f' AND connamespace = 'public'::regnamespace ORDER BY conrelid::regclass::text, contype DESC;"; 32 | const { rows: data } = await db.query(fKQuery); 33 | res.locals.fnKeys = data; 34 | return next(); 35 | } catch (err) { 36 | return next({ 37 | log: `error occured in dbLinkController.test: ${err}`, 38 | status: 400, 39 | message: {err: 'error extracting foreign keys'}, 40 | }); 41 | } 42 | }; 43 | 44 | module.exports = dbLinkController; 45 | -------------------------------------------------------------------------------- /src/server/controllers/dbSchemaController.js: -------------------------------------------------------------------------------- 1 | const db = require('../models/models'); 2 | // require('dotenv').config(); 3 | 4 | const dbSchemaController = {}; 5 | 6 | dbSchemaController.getSchema = async (req, res, next) => { 7 | try { 8 | const queryStr = 9 | "SELECT current_database(), table_schema, table_name, ordinal_position as position, column_name, data_type, column_default as default_value FROM information_schema.columns WHERE table_schema NOT IN ('information_schema', 'pg_catalog') AND table_name != 'pg_stat_statements' ORDER BY table_schema, table_name, ordinal_position;"; 10 | const data = await db.query(queryStr); 11 | 12 | const database = data.rows; 13 | 14 | const dbSchema = { 15 | db_name: database[0].current_database, 16 | tables: {}, 17 | }; 18 | const tableSet = new Set(); 19 | 20 | for (let i = 0; i < database.length; i++) { 21 | let table = database[i].table_name; 22 | tableSet.add(table); 23 | 24 | if (!dbSchema.tables[table]) dbSchema.tables[table] = {}; 25 | if (!dbSchema.tables[table].columns) dbSchema.tables[table].columns = {}; 26 | let type = database[i].data_type; 27 | switch (type) { 28 | case 'bigint': // signed eight-byte integer 29 | case 'integer': // signed four-byte integer 30 | case 'bigserial': // autoincrementing eight-byte integer 31 | case 'bytea': // binary data (“byte array”) 32 | case 'smallint': // signed two-byte integer 33 | case 'smallserial': // autoincrementing two-byte integer 34 | case 'serial': // autoincrementing four-byte integer 35 | type = 'Int'; 36 | break; 37 | case 'date': // calendar date (year, month, day) 38 | case 'character': // fixed-length character string 39 | // should have a case for varchar[(n)]/character varying [(n)]? 40 | case 'character varying': // variable-length character string 41 | case 'bit': // fixed-length bit string 42 | case 'bit varying': // variable-length bit string 43 | case 'cidr': // IPv4 or IPv6 network address 44 | case 'inet': // IPv4 or IPv6 host address 45 | case 'json': // textual JSON data 46 | case 'jsonb': // binary JSON data, decomposed 47 | case 'text': // variable-length character string 48 | case 'time': // time of day 49 | case 'timestamp': // date and time 50 | case 'tsquery': // text search query 51 | case 'tsvector': // text search document 52 | type = 'String'; 53 | break; 54 | case 'boolean': 55 | type = 'Boolean'; 56 | break; 57 | case 'double precision': // double precision floating-point number (8 bytes) 58 | case 'numeric': // exact numeric of selectable precision 59 | case 'real': // single precision floating-point number (4 bytes) 60 | type = 'Float'; 61 | break; 62 | default: 63 | type = 'String'; 64 | break; 65 | } 66 | 67 | dbSchema.tables[table].columns[database[i].column_name] = type; 68 | } 69 | 70 | const tableNames = Array.from(tableSet); 71 | 72 | res.locals.dbSchema = dbSchema; 73 | 74 | res.locals.tables = tableNames; 75 | return next(); 76 | } catch (err) { 77 | return next({ 78 | log: `error occured in dbSchemaController.getSchema: ${err}`, 79 | status: 400, 80 | message: {err: 'error generating schema'}, 81 | }); 82 | } 83 | }; 84 | 85 | module.exports = dbSchemaController; 86 | -------------------------------------------------------------------------------- /src/server/controllers/fnKeyController.js: -------------------------------------------------------------------------------- 1 | const fnKeyController = {}; 2 | 3 | fnKeyController.parseFnKeyData = async (req, res, next) => { 4 | try { 5 | const foreignKeys = {}; 6 | await res.locals.fnKeys.forEach((ele) => { 7 | if (!foreignKeys[ele.table_name]) foreignKeys[ele.table_name] = {}; 8 | const arr = ele.pg_get_constraintdef.split(' '); 9 | const fnKey = arr[2].slice(1, -1); 10 | foreignKeys[ele.table_name][fnKey] = {}; 11 | const rawRef = arr[4].split('('); 12 | const refTable = rawRef[0]; 13 | const refKey = rawRef[1].slice(0, -1); 14 | foreignKeys[ele.table_name][fnKey][refTable] = refKey; 15 | }); 16 | res.locals.parsedFnKeys = foreignKeys; 17 | next(); 18 | 19 | } catch (err) { 20 | return next({ 21 | log: `error occured in fnKeyController.parseFnKeyData: ${err}`, 22 | status: 400, 23 | message: {err: 'error parsing foreign keys'}, 24 | }); 25 | } 26 | }; 27 | 28 | fnKeyController.parsePrimaryKeyData = async (req, res, next) => { 29 | try { 30 | const primaryKeys = {}; 31 | await res.locals.fnKeys.forEach((ele) => { 32 | const arr = ele.pg_get_constraintdef.split(' '); 33 | const fnKey = arr[2].slice(1, -1); 34 | const rawRef = arr[4].split('('); 35 | const refTable = rawRef[0]; 36 | const refKey = rawRef[1].slice(0, -1); 37 | if (!primaryKeys[refTable]) primaryKeys[refTable] = []; 38 | primaryKeys[refTable].push(ele.table_name); 39 | }); 40 | res.locals.parsedPrimaryKeys = primaryKeys; 41 | next(); 42 | 43 | } catch (err) { 44 | return next({ 45 | log: `error occured in fnKeyController.parsePrimaryKeyData: ${err}`, 46 | status: 400, 47 | message: {err: 'error getting primary keys'}, 48 | }); 49 | } 50 | }; 51 | 52 | module.exports = fnKeyController; 53 | -------------------------------------------------------------------------------- /src/server/controllers/mutationController.js: -------------------------------------------------------------------------------- 1 | const mutationController = {}; 2 | 3 | mutationController.mutationSchema = (req, res, next) => { 4 | const fnKeys = res.locals.parsedFnKeys; 5 | const primKeys = { ...res.locals.parsedPrimaryKeys }; 6 | const databaseInfo = { ...res.locals.dbSchema.tables }; 7 | let databaseName = res.locals.databaseName || 'dbModelName'; 8 | let mutationSchema = 'type Mutation {\n'; 9 | 10 | const upperCamelCase = (table) => { 11 | const changeCase = Array.from(table); 12 | for (let i = 0; i < changeCase.length; i++) { 13 | // table names: make snake case into camel case 14 | if (changeCase[i] === '_' && changeCase[i + 1]) { 15 | changeCase[i + 1] = changeCase[i + 1].toUpperCase(); 16 | changeCase[i] = ''; 17 | } else if (changeCase[i] === '_' && !changeCase[i + 1]) { 18 | changeCase[i] = ''; 19 | } 20 | // table names: changing plural to singular 21 | if ( 22 | /[^aeiou]/i.test(changeCase[changeCase.length - 2]) && 23 | /s/i.test(changeCase[changeCase.length - 1]) 24 | ) { 25 | changeCase[changeCase.length - 1] = ''; 26 | } 27 | } 28 | changeCase[0] = changeCase[0].toUpperCase(); 29 | //final version of formatted table type 30 | return changeCase.join(''); 31 | }; 32 | 33 | for (let table in databaseInfo) { 34 | const typeName = upperCamelCase(table); 35 | mutationSchema += ` add${typeName}(input: Add${typeName}Input): ${typeName}\n update${typeName}(_id: ID, input: Update${typeName}Input): ${typeName}\n delete${typeName}(_id: ID): ${typeName}\n\n`; 36 | } 37 | mutationSchema += '}\n\n'; 38 | 39 | for (let table in databaseInfo) { 40 | const typeName = upperCamelCase(table); 41 | const columns = databaseInfo[table].columns; 42 | if (columns.hasOwnProperty('_id')) delete databaseInfo[table].columns._id; 43 | else if (columns.hasOwnProperty('id')) 44 | delete databaseInfo[table].columns.id; 45 | 46 | mutationSchema += `input Add${typeName}Input {\n`; 47 | for (let columnName in columns) { 48 | if (columnName !== 'id' && columnName !== '_id') 49 | mutationSchema += ` ${columnName}: ${columns[columnName]}\n`; 50 | } 51 | mutationSchema += '}\n\n'; 52 | 53 | mutationSchema += `input Update${typeName}Input {\n`; 54 | for (let columnName in columns) { 55 | if (columnName !== 'id' && columnName !== '_id') 56 | mutationSchema += ` ${columnName}: ${columns[columnName]}\n`; 57 | } 58 | mutationSchema += '}\n\n'; 59 | } 60 | 61 | res.locals.schemaString += mutationSchema; 62 | return next(); 63 | }; 64 | 65 | mutationController.mutationResolver = (req, res, next) => { 66 | const fnKeys = res.locals.parsedFnKeys; 67 | const primKeys = { ...res.locals.parsedPrimaryKeys }; 68 | const databaseInfo = { ...res.locals.dbSchema.tables }; 69 | let databaseName = res.locals.databaseName || 'dbModelName'; 70 | let mutationResolver = 'Mutation: {\n'; 71 | 72 | const upperCamelCase = (table) => { 73 | const changeCase = Array.from(table); 74 | for (let i = 0; i < changeCase.length; i++) { 75 | // table names: make snake case into camel case 76 | if (changeCase[i] === '_' && changeCase[i + 1]) { 77 | changeCase[i + 1] = changeCase[i + 1].toUpperCase(); 78 | changeCase[i] = ''; 79 | } else if (changeCase[i] === '_' && !changeCase[i + 1]) { 80 | changeCase[i] = ''; 81 | } 82 | // table names: changing plural to singular 83 | if ( 84 | /[^aeiou]/i.test(changeCase[changeCase.length - 2]) && 85 | /s/i.test(changeCase[changeCase.length - 1]) 86 | ) { 87 | changeCase[changeCase.length - 1] = ''; 88 | } 89 | } 90 | changeCase[0] = changeCase[0].toUpperCase(); 91 | //final version of formatted table type 92 | return changeCase.join(''); 93 | }; 94 | 95 | for (let table in databaseInfo) { 96 | // for input: Add 97 | const typeName = upperCamelCase(table); 98 | const columns = databaseInfo[table].columns; 99 | if (columns.hasOwnProperty('_id')) delete databaseInfo[table].columns._id; 100 | else if (columns.hasOwnProperty('id')) 101 | delete databaseInfo[table].columns.id; 102 | const properties = Object.keys(columns); 103 | mutationResolver += ` add${typeName}: async (parent, { input }, context) => {\n try {\n const { `; 104 | for (let i = 0; i < properties.length; i++) { 105 | if (properties[i] !== 'id' && properties[i] !== '_id') { 106 | if (i === properties.length - 1) { 107 | mutationResolver += `${properties[i]} } = input;\n const queryStr = 'INSERT INTO ${table} (`; 108 | } else { 109 | mutationResolver += `${properties[i]}, `; 110 | } 111 | } 112 | } 113 | for (let i = 0; i < properties.length; i++) { 114 | if (properties[i] !== 'id' && properties[i] !== '_id') { 115 | if (i === properties.length - 1) { 116 | mutationResolver += `${properties[i]}) VALUES (`; 117 | } else { 118 | mutationResolver += `${properties[i]}, `; 119 | } 120 | } 121 | } 122 | for (let i = 0; i < properties.length; i++) { 123 | if (properties[i] !== 'id' && properties[i] !== '_id') { 124 | if (i === properties.length - 1) { 125 | mutationResolver += `$${ 126 | i + 1 127 | }) RETURNING *'; \n const values = [ `; 128 | } else { 129 | mutationResolver += `$${i + 1}, `; 130 | } 131 | } 132 | } 133 | for (let i = 0; i < properties.length; i++) { 134 | if (properties[i] !== 'id' && properties[i] !== '_id') { 135 | if (i === properties.length - 1) { 136 | mutationResolver += `${properties[i]} ];\n const { rows } = await ${databaseName}.query(queryStr, values);\n return rows[1] ? rows : rows[0];\n } catch (err) {\n console.log(err);\n }\n },\n`; 137 | } else { 138 | mutationResolver += `${properties[i]}, `; 139 | } 140 | } 141 | } 142 | 143 | // for input: Update 144 | 145 | mutationResolver += ` update${typeName}: async (parent, { input, _id }, context) => {\n try {\n const { `; 146 | for (let i = 0; i < properties.length; i++) { 147 | if (properties[i] !== 'id' && properties[i] !== '_id') { 148 | if (i === properties.length - 1) { 149 | mutationResolver += `${properties[i]} } = input;\n const queryStr = 'UPDATE ${table} SET `; 150 | } else { 151 | mutationResolver += `${properties[i]}, `; 152 | } 153 | } 154 | } 155 | for (let i = 0; i < properties.length; i++) { 156 | if (properties[i] !== 'id' && properties[i] !== '_id') { 157 | if (i === properties.length - 1) { 158 | mutationResolver += `${properties[i]} = $${i + 1} WHERE _id = $${ 159 | properties.length + 1 160 | } RETURNING *';\n const values = [ `; 161 | } else { 162 | mutationResolver += `${properties[i]} = $${i + 1}, `; 163 | } 164 | } 165 | } 166 | for (let i = 0; i < properties.length; i++) { 167 | if (properties[i] !== 'id' && properties[i] !== '_id') { 168 | if (i === properties.length - 1) { 169 | mutationResolver += `${properties[i]}, _id ];\n const { rows } = await ${databaseName}.query(queryStr, values);\n return rows[1] ? rows : rows[0];\n } catch (err) {\n console.log(err);\n }\n },\n`; 170 | } else { 171 | mutationResolver += `${properties[i]}, `; 172 | } 173 | } 174 | } 175 | 176 | // for input: Delete 177 | mutationResolver += ` delete${typeName}: async (parent, args, context) => {\n try {\n const queryStr = 'DELETE FROM ${table} WHERE _id = $1 RETURNING *';\n const values = [ args._id ];\n const { rows } = await ${databaseName}.query(queryStr, values);\n return rows[1] ? rows : rows[0];\n } catch (err) {\n console.log(err);\n }\n },\n\n`; 178 | } 179 | mutationResolver += '}\n\n'; 180 | 181 | // res.locals.resolverString += mutationResolver + res.locals.exportString; 182 | res.locals.resolverString += mutationResolver; 183 | 184 | return next(); 185 | }; 186 | 187 | module.exports = mutationController; 188 | -------------------------------------------------------------------------------- /src/server/controllers/projectController.js: -------------------------------------------------------------------------------- 1 | const userDb = require('../models/userModel'); 2 | 3 | const projectController = {}; 4 | 5 | projectController.saveProject = async (req, res, next) => { 6 | try { 7 | const { user, projectName, schemaData, treeData, date, resolverData } = req.body; 8 | console.log('date:', date) 9 | const saveQuery = 10 | 'INSERT INTO projects(project_name, schema_data, tree_data, user_id, last_updated, resolver_data) VALUES ($1, $2, $3, $4, $5, $6) RETURNING *'; 11 | 12 | const values = [projectName, schemaData, treeData, user, date, resolverData]; 13 | console.log(values); 14 | const { rows } = await userDb.query(saveQuery, values); 15 | console.log('rows0',rows[0]); 16 | // send back data of newly created account to client-side 17 | res.locals.savedProject = rows[0]; 18 | return next(); 19 | } catch (err) { 20 | return next({ 21 | log: `error occurred in projectController.saveProject: ${err}`, 22 | status: 400, 23 | message: {err: 'Couldn\'t save project.'}, 24 | }); 25 | } 26 | }; 27 | 28 | projectController.getProjects = async (req, res, next) => { 29 | const { id } = req.params; 30 | try{ 31 | const projectQuery = `SELECT * FROM projects WHERE user_id = ${id}` 32 | const { rows } = await userDb.query(projectQuery); 33 | // console.log('the data', rows); 34 | res.locals.projects = rows; 35 | return next(); 36 | } 37 | catch(err){ 38 | return next({ 39 | log: `error occurred in projectController.getProjects: ${err}`, 40 | status: 400, 41 | message: {err: 'couldn\'t get projects'}, 42 | }); 43 | } 44 | }; 45 | 46 | projectController.updateProject = async (req, res, next) => { 47 | const { id, name, schema, date, resolver } = req.body; 48 | const updateQuery = `UPDATE projects SET project_name=$1, schema_data=$2, last_updated=$3, resolver_data=$4 WHERE id=${id} RETURNING *`; 49 | const values = [name, schema, date, resolver]; 50 | try{ 51 | const { rowCount, rows } = await userDb.query(updateQuery, values); 52 | res.locals.updated = rows[0]; 53 | res.locals.success = rowCount === 1 ? true : false; 54 | return next(); 55 | } 56 | catch(err){ 57 | console.log('error in updateproj:', err); 58 | return next({ 59 | log: `error occurred in projectController.updateProject: ${err}`, 60 | status: 400, 61 | message: {err: 'couldn\'t update project'}, 62 | }); 63 | } 64 | }; 65 | 66 | projectController.deleteProject = async (req, res, next) => { 67 | const { id } = req.params; 68 | const deleteQuery = `DELETE FROM projects WHERE id=${id}`; 69 | try{ 70 | const { rowCount } = await userDb.query(deleteQuery); 71 | res.locals.deleted = `${rowCount} project(s) deleted.` 72 | res.locals.success = rowCount === 1 ? true : false; 73 | console.log(res.locals.deleted) 74 | return next(); 75 | } 76 | catch(err){ 77 | return next({ 78 | log: `error occurred in projectController.deleteProject: ${err}`, 79 | status: 400, 80 | message: {err: 'couldn\'t delete project'}, 81 | }); 82 | } 83 | }; 84 | 85 | module.exports = projectController; 86 | -------------------------------------------------------------------------------- /src/server/controllers/resolverController.js: -------------------------------------------------------------------------------- 1 | const resolverController = {}; 2 | 3 | resolverController.genResolver = (req, res, next) => { 4 | const fnKeys = res.locals.parsedFnKeys; 5 | const primKeys = { ...res.locals.parsedPrimaryKeys }; 6 | const databaseInfo = res.locals.dbSchema.tables; 7 | let exportString = 'module.exports = { Query,'; 8 | let resolverString = 'Query: {\n'; 9 | let databaseName = res.locals.databaseName || 'dbModelName'; 10 | 11 | for (let table in databaseInfo) { 12 | // run for each table in database 13 | let logic = ' try {\n'; 14 | logic += ` const queryStr = 'SELECT * FROM ${table}';\n const { rows } = await ${databaseName}.query(queryStr);\n return rows;\n`; 15 | logic += ` } catch (err) {\n console.log(err)\n }`; 16 | resolverString += ` ${table}: async (parent, args, context) => {\n ${logic}\n },\n`; 17 | 18 | // below: add resolver for singular of table name (if plural) 19 | 20 | // if table name ends in consonant + "s" 21 | if ( 22 | /[^aeiou]/i.test(table[table.length - 2]) && 23 | /s/i.test(table[table.length - 1]) 24 | ) { 25 | const tableSingular = table.slice(0, -1); 26 | let singLogic = ' try {\n'; 27 | singLogic += ` const queryStr = \`SELECT * FROM ${table} WHERE _id = $1\`;\n const values = [ args._id ]\n const { rows } = await ${databaseName}.query(queryStr, values);\n return rows[1] ? rows : rows[0];\n`; 28 | singLogic += ` } catch (err) {\n console.log(err);\n }`; 29 | resolverString += ` ${tableSingular}: async (parent, args, context) => {\n ${singLogic}\n },\n`; 30 | } 31 | // accounting for if table name is people 32 | else if (table === 'people') { 33 | const tableSingular = 'person'; 34 | let singLogic = ' try {\n'; 35 | singLogic += ` const queryStr = \`SELECT * FROM ${table} WHERE _id = $1\`;\n const values = [ args._id ]\n const { rows } = await ${databaseName}.query(queryStr, values);\n return rows[1] ? rows : rows[0];\n`; 36 | singLogic += ` } catch (err) {\n console.log(err);\n }`; 37 | resolverString += ` ${tableSingular}: async (parent, args, context) => {\n ${singLogic}\n },\n`; 38 | } 39 | // accounting for if table name ends in "ies" like "species" 40 | else if (table.slice(table.length - 3) === 'ies') { 41 | const tableSingular = table.slice(0, -1); 42 | let singLogic = ' try {\n'; 43 | singLogic += ` const queryStr = \`SELECT * FROM ${table} WHERE _id = $1\`;\n const values = [ args._id ]\n const { rows } = await ${databaseName}.query(queryStr, values);\n return rows[1] ? rows : rows[0];\n`; 44 | singLogic += ` } catch (err) {\n console.log(err);\n }`; 45 | resolverString += ` ${tableSingular}: async (parent, args, context) => {\n ${singLogic}\n },\n`; 46 | } 47 | // catch all (make table singular) 48 | else { 49 | const tableSingular = table + '_single'; 50 | let singLogic = ' try {\n'; 51 | singLogic += ` const queryStr = \`SELECT * FROM ${table} WHERE _id = $1\`;\n const values = [ args._id ]\n const { rows } = await ${databaseName}.query(queryStr, values);\n return rows[1] ? rows : rows[0];\n`; 52 | singLogic += ` } catch (err) {\n console.log(err);\n }`; 53 | resolverString += ` ${tableSingular}: async (parent, args, context) => {\n ${singLogic}\n },\n`; 54 | } 55 | } 56 | resolverString += '},\n\n'; 57 | 58 | // logic for foreign keys to primary tables -------------------------------------------- 59 | for (const foreignTable in fnKeys) { 60 | // console.log('foreignTable: ', foreignTable); 61 | //logic to format table name type 62 | const upperCamelCase = (table) => { 63 | const changeCase = Array.from(table); 64 | for (let i = 0; i < changeCase.length; i++) { 65 | // table names: make snake case into camel case 66 | if (changeCase[i] === '_' && changeCase[i + 1]) { 67 | changeCase[i + 1] = changeCase[i + 1].toUpperCase(); 68 | changeCase[i] = ''; 69 | } else if (changeCase[i] === '_' && !changeCase[i + 1]) { 70 | changeCase[i] = ''; 71 | } 72 | // table names: changing plural to singular 73 | if ( 74 | /[^aeiou]/i.test(changeCase[changeCase.length - 2]) && 75 | /s/i.test(changeCase[changeCase.length - 1]) 76 | ) { 77 | changeCase[changeCase.length - 1] = ''; 78 | } 79 | } 80 | changeCase[0] = changeCase[0].toUpperCase(); 81 | //final version of formatted table type 82 | return changeCase.join(''); 83 | }; 84 | exportString += ` ${upperCamelCase(foreignTable)},`; 85 | resolverString += `${upperCamelCase(foreignTable)}: {\n`; 86 | for (const foreignKey in fnKeys[foreignTable]) { 87 | const primaryTable = Object.keys(fnKeys[foreignTable][foreignKey]); 88 | const primaryColumn = Object.values(foreignKey); 89 | resolverString += ` ${foreignKey}_info: async (parent, args, context) => {\n try {\n const queryStr = 'SELECT * FROM ${primaryTable[0]} WHERE _id = $1';\n const values = [ parent._id ]\n const { rows } = await ${databaseName}.query(queryStr, values);\n return rows[1] ? rows : rows[0]\n } catch (err) {\n console.log(err);\n }\n },\n`; 90 | 91 | // matches from foreign keys 92 | // ????/ not WHERE _id = $1 but need WHERE species_id = $1 93 | if (primKeys.hasOwnProperty(foreignTable)) { 94 | primKeys[foreignTable].forEach((ele) => { 95 | const fnColumns = Object.keys(fnKeys[ele]); 96 | // console.log('fnColumns: ', fnColumns); 97 | // console.log('foreignTable: ', foreignTable); 98 | let fnColumnName = ''; 99 | for (let i = 0; i < fnColumns.length; i++) { 100 | // console.log('foreignRefTable: ', Object.keys(fnKeys[ele][fnColumns[i]])[0]) 101 | if (foreignTable === Object.keys(fnKeys[ele][fnColumns[i]])[0]) { 102 | fnColumnName += fnColumns[i]; 103 | break; 104 | } 105 | } 106 | 107 | resolverString += ` ${ele}: async (parent, args, context) => {\n try {\n const queryStr = 'SELECT * FROM ${ele} WHERE ${ 108 | fnColumnName || '_id' 109 | } = $1';\n const values = [ parent._id ]\n const { rows } = await ${databaseName}.query(queryStr, values);\n return rows[1] ? rows : rows[0]\n } catch (err) {\n console.log(err);\n }\n },\n`; 110 | }); 111 | delete primKeys[foreignTable]; 112 | } 113 | } 114 | resolverString += '},\n'; 115 | } 116 | // rest of logic for primary keys to foreign tables -------------------------------------------- 117 | for (let prim in primKeys) { 118 | const upperCamelCase = (table) => { 119 | const changeCase = Array.from(table); 120 | for (let i = 0; i < changeCase.length; i++) { 121 | // table names: make snake case into camel case 122 | if (changeCase[i] === '_' && changeCase[i + 1]) { 123 | changeCase[i + 1] = changeCase[i + 1].toUpperCase(); 124 | changeCase[i] = ''; 125 | } else if (changeCase[i] === '_' && !changeCase[i + 1]) { 126 | changeCase[i] = ''; 127 | } 128 | // table names: changing plural to singular 129 | if ( 130 | /[^aeiou]/i.test(changeCase[changeCase.length - 2]) && 131 | /s/i.test(changeCase[changeCase.length - 1]) 132 | ) { 133 | changeCase[changeCase.length - 1] = ''; 134 | } 135 | } 136 | changeCase[0] = changeCase[0].toUpperCase(); 137 | //final version of formatted table type 138 | return changeCase.join(''); 139 | }; 140 | 141 | exportString += ` ${upperCamelCase(prim)},`; 142 | resolverString += `${upperCamelCase(prim)}: {\n`; 143 | 144 | primKeys[prim].forEach((ele) => { 145 | const fnColumns = Object.keys(fnKeys[ele]); 146 | let fnColumnName = ''; 147 | for (let i = 0; i < fnColumns.length; i++) { 148 | // console.log('foreignRefTable: ', Object.keys(fnKeys[ele][fnColumns[i]])[0]) 149 | if (prim === Object.keys(fnKeys[ele][fnColumns[i]])[0]) { 150 | fnColumnName += fnColumns[i]; 151 | break; 152 | } 153 | } 154 | 155 | resolverString += ` ${ele}: async (parent, args, context) => {\n try {\n const queryStr = 'SELECT * FROM ${ele} WHERE ${ 156 | fnColumnName || '_id' 157 | } = $1';\n const values = [ parent._id ]\n const { rows } = await ${databaseName}.query(queryStr, values);\n return rows[1] ? rows : rows[0]\n } catch (err) {\n console.log(err);\n }\n },\n`; 158 | }); 159 | resolverString += '},\n\n'; 160 | } 161 | exportString += ' Mutation }'; 162 | res.locals.exportString = exportString; 163 | res.locals.resolverString = resolverString || 'Resolver Creation Error'; 164 | // for resolver logic for graphiQL server @4000 165 | const resolverLogic = resolverString.replace('\n', ''); 166 | // res.locals.resolverLogic = { resolverLogic }; 167 | // console.log(res.locals.resolverLogic); 168 | // for extracting just names of each resolver 169 | // res.locals.resolverNames = exportString.slice(19, -2); 170 | return next(); 171 | }; 172 | 173 | module.exports = resolverController; 174 | -------------------------------------------------------------------------------- /src/server/controllers/schemaGen.js: -------------------------------------------------------------------------------- 1 | const schemaGen = {}; 2 | 3 | schemaGen.genSchema = (req, res, next) => { 4 | const fnKeys = res.locals.parsedFnKeys; 5 | const primKeys = res.locals.parsedPrimaryKeys; 6 | const dbOb = res.locals.dbSchema.tables; 7 | 8 | let schemaString = 'type Query {\n'; 9 | 10 | for (let table in dbOb) { 11 | //logic to format table name type 12 | const changeCase = Array.from(table); 13 | for (let i = 0; i < changeCase.length; i++){ 14 | // table names: make snake case into camel case 15 | if (changeCase[i] === '_' && changeCase[i+1]) { 16 | changeCase[i+1] = changeCase[i+1].toUpperCase(); 17 | changeCase[i] = ''; 18 | } else if (changeCase[i] === '_' && !changeCase[i+1]) { 19 | changeCase[i] = '' 20 | }; 21 | // table names: changing plural to singular 22 | if( /[^aeiou]/i.test(changeCase[changeCase.length-2]) && /s/i.test(changeCase[changeCase.length-1])){ 23 | changeCase[changeCase.length-1] = ''; 24 | }; 25 | } 26 | changeCase[0] = changeCase[0].toUpperCase(); 27 | //final version of formatted table type 28 | const camelCaseTable = changeCase.join(''); 29 | //logic to get singleular form of table for keys 30 | let tableSingular; 31 | if (/[^aeiou]/i.test(table[table.length - 2]) && /s/i.test(table[table.length - 1])) { 32 | tableSingular = table.slice(0, -1); 33 | 34 | } 35 | // accounting for if table name is people 36 | else if (table === 'people') { 37 | tableSingular = 'person'; 38 | 39 | } 40 | // accounting for if table name ends in "ies" like "species" 41 | else if (table.slice(table.length - 3) === 'ies') { 42 | tableSingular = table.slice(0, -1); 43 | 44 | } else { 45 | tableSingular = table + "_single"; 46 | } 47 | schemaString += ` ${table}: [${camelCaseTable}]\n`; 48 | schemaString += ` ${tableSingular}(_id: ID): ${camelCaseTable}\n`; 49 | } 50 | schemaString += '}\n\n'; 51 | // added "type Query" above 52 | 53 | for (const table in dbOb){ 54 | let string = 'type '; 55 | let tableName = String(table); 56 | const type = Array.from(tableName); 57 | 58 | for (let i = 0; i < type.length; i++){ 59 | // table names: make snake case into camel case 60 | if (type[i] === '_' && type[i+1]) { 61 | type[i+1] = type[i+1].toUpperCase(); 62 | type[i] = ''; 63 | } else if (type[i] === '_' && !type[i+1]) { 64 | type[i] = '' 65 | }; 66 | // table names: changing plural to singular 67 | if( /[^aeiou]/i.test(type[type.length-2]) && /s/i.test(type[type.length-1])){ 68 | type[type.length-1] = ''; 69 | }; 70 | } 71 | type[0] = type[0].toUpperCase(); 72 | const pascalType = type.join(''); 73 | string += pascalType + ' {\n'; 74 | 75 | for (const col in dbOb[table].columns){ 76 | // console.log('col is id', col === '_id'); 77 | // if (col === '_id') col = col.slice(1); 78 | // console.log(col); 79 | 80 | // to change "_id" to "id" and its data type to "ID" 81 | // hard coded for _id <- is there a better way? 82 | // if (col.toLowerCase() === '_id' || col.toLowerCase() === 'id') { 83 | // string += ' id: ID\n'; 84 | // } else { 85 | string += ' ' + col + ': ' + dbOb[table].columns[col] + '\n'; 86 | // } 87 | }; 88 | 89 | 90 | 91 | 92 | // below adds "foreignkey: referenceTableType" 93 | const fnKeysTables = Object.keys(fnKeys); 94 | if (fnKeysTables.includes(tableName)) { 95 | // console.log('tableName: ', tableName); 96 | const foreignKeys = Object.keys(fnKeys[tableName]); 97 | const referenceTableProp = Object.values(fnKeys[tableName]); 98 | for (let i = 0; i < foreignKeys.length; i++) { 99 | // if (fnKeysTables.includes(tableName)) console.log('tableName1: ', tableName); 100 | let referenceTableName = Object.keys(referenceTableProp[i])[0]; 101 | // console.log('referenceTableName: ', referenceTableName); 102 | // use above to connect to schema 103 | // homeworld_id_info ?? 104 | const changeCase = Array.from(referenceTableName); 105 | 106 | for (let i = 0; i < changeCase.length; i++){ 107 | // table names: make snake case into camel case 108 | if (changeCase[i] === '_' && changeCase[i+1]) { 109 | changeCase[i+1] = changeCase[i+1].toUpperCase(); 110 | changeCase[i] = ''; 111 | } else if (changeCase[i] === '_' && !changeCase[i+1]) { 112 | changeCase[i] = '' 113 | }; 114 | // table names: changing plural to singular 115 | if( /[^aeiou]/i.test(changeCase[changeCase.length-2]) && /s/i.test(changeCase[changeCase.length-1])){ 116 | changeCase[changeCase.length-1] = ''; 117 | }; 118 | } 119 | changeCase[0] = changeCase[0].toUpperCase(); 120 | const camelCaseTable = changeCase.join(''); 121 | string += ' ' + foreignKeys[i] + '_info' + ': ' + camelCaseTable + '\n'; 122 | } 123 | }; 124 | // below adds "primarykey: foreignKeyTableType" 125 | const primaryKeysTables = Object.keys(primKeys); 126 | if (primaryKeysTables.includes(tableName)) { 127 | // console.log('tableName: ', tableName); 128 | const foreignTables = primKeys[tableName]; 129 | for (let i = 0; i < foreignTables.length; i++) { 130 | const foreignName = foreignTables[i]; 131 | // console.log('foreignTables: ', foreignTables); 132 | // console.log('foreignName: ', foreignName); 133 | // change snake case to camel case 134 | const changeCase = Array.from(foreignName); 135 | 136 | for (let i = 0; i < changeCase.length; i++){ 137 | // table names: make snake case into camel case 138 | if (changeCase[i] === '_' && changeCase[i+1]) { 139 | changeCase[i+1] = changeCase[i+1].toUpperCase(); 140 | changeCase[i] = ''; 141 | } else if (changeCase[i] === '_' && !changeCase[i+1]) { 142 | changeCase[i] = '' 143 | }; 144 | // table names: changing plural to singular 145 | if( /[^aeiou]/i.test(changeCase[changeCase.length-2]) && /s/i.test(changeCase[changeCase.length-1])){ 146 | changeCase[changeCase.length-1] = ''; 147 | }; 148 | } 149 | changeCase[0] = changeCase[0].toUpperCase(); 150 | const camelCaseTable = changeCase.join(''); 151 | string += ' ' + foreignName + ': [' + camelCaseTable + ']\n'; 152 | } 153 | }; 154 | 155 | string += '} \n\n'; 156 | 157 | schemaString += string; 158 | }; 159 | // console.log(schemaString); 160 | 161 | res.locals.schemaString = schemaString; 162 | return next(); 163 | }; 164 | 165 | module.exports = schemaGen; -------------------------------------------------------------------------------- /src/server/controllers/testController.js: -------------------------------------------------------------------------------- 1 | const testController = { 2 | testing: 'hi', 3 | }; 4 | 5 | module.exports = testController; 6 | -------------------------------------------------------------------------------- /src/server/controllers/treeController.js: -------------------------------------------------------------------------------- 1 | const treeController = {}; 2 | 3 | treeController.treeSchema = (req, res, next) => { 4 | const db = res.locals.dbSchema; 5 | const foreignTables = Object.keys(res.locals.parsedFnKeys); 6 | if (!db) { 7 | return next({ 8 | error: err, 9 | message: 'error occured in treeController.treeSchema', 10 | status: 400, 11 | }); 12 | }; 13 | const tree = { 14 | name: db.db_name, 15 | children: [], 16 | }; 17 | const tables = db.tables; 18 | for (const table in db.tables) { 19 | const obj = { 20 | name: table, 21 | children: [], 22 | }; 23 | for (const col in db.tables[table].columns){ 24 | const ob = { 25 | name: col, 26 | }; 27 | // if table has foreign key(s) 28 | if (foreignTables.includes(table) && res.locals.parsedFnKeys[table][col]) { 29 | const refTable = Object.keys(res.locals.parsedFnKeys[table][col])[0]; 30 | const refCol = Object.values(res.locals.parsedFnKeys[table][col])[0]; 31 | const foreignChildren = []; 32 | for (const fnCol in db.tables[refTable].columns) { 33 | const fnOb = { 34 | name: fnCol, 35 | }; 36 | if (fnCol === refCol) fnOb.name = `primKey${fnCol}` 37 | foreignChildren.push(fnOb); 38 | } 39 | ob.children = [{ name: refTable, children: foreignChildren }]; 40 | } 41 | obj.children.push(ob); 42 | }; 43 | tree.children.push(obj); 44 | }; 45 | res.locals.tree = tree; 46 | // console.log(res.locals.tree); 47 | // console.log(tree.children[2]); 48 | 49 | return next(); 50 | }; 51 | 52 | module.exports = treeController; -------------------------------------------------------------------------------- /src/server/controllers/userController.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const userDb = require('../models/userModel'); 3 | const bcrypt = require('bcrypt'); 4 | const jwt = require('jsonwebtoken'); 5 | const cookieParser = require('cookie-parser'); 6 | 7 | const userController = {}; 8 | 9 | userController.checkUsernameExistence = async (req, res, next) => { 10 | try { 11 | const { username } = req.body; 12 | 13 | const existenceQuery = `SELECT * FROM users WHERE username = '${username}';`; 14 | // const value = [username]; 15 | const { rows } = await userDb.query(existenceQuery); 16 | if (rows[0]) res.locals.existence = 'exists'; 17 | else res.locals.existence = 'nonexistent'; 18 | // console.log('res.locals.existence: ', res.locals.existence); 19 | return next(); 20 | } catch (err) { 21 | return next({ 22 | log: `error occured in userController.checkUsernameExistence: ${err}`, 23 | status: 400, 24 | message: {err: 'error checking credentials'}, 25 | }); 26 | } 27 | }; 28 | 29 | userController.signUp = async (req, res, next) => { 30 | try { 31 | const { firstName, lastName, email, username, password } = req.body; 32 | // create hash password via bcrypt 33 | const salt = await bcrypt.genSalt(10); 34 | const hashed = await bcrypt.hash(password, salt); 35 | // execute query to create new user row in user table 36 | const textQuery = `INSERT INTO users(firstname, lastname, email, username, password) VALUES ($1, $2, $3, $4, $5) RETURNING *`; 37 | const values = [firstName, lastName, email, username, hashed]; 38 | const { rows } = await userDb.query(textQuery, values); 39 | // send back data of newly created account to client-side 40 | res.locals.signedUp = rows[0]; 41 | return next(); 42 | } catch (err) { 43 | return next({ 44 | log: `error occured in userController.signUp: ${err}`, 45 | status: 400, 46 | message: 'error in signup', 47 | }); 48 | } 49 | }; 50 | 51 | userController.login = async (req, res, next) => { 52 | try { 53 | const { username, password } = req.body; 54 | // execute query to check if username exists in users table 55 | const loginQuery = `SELECT * FROM users WHERE username = '${username}'`; 56 | const { rows } = await userDb.query(loginQuery); 57 | 58 | // compare inputted password to stored hashed password 59 | if (await bcrypt.compare(password, rows[0].password)) { 60 | // console.log('id', rows[0]._id); 61 | const user = { name: username, id: rows[0]._id }; 62 | const accessToken = jwt.sign(user, process.env.ACCESS_TOKEN_SERVER); 63 | console.log(accessToken); 64 | res.cookie('token', accessToken); 65 | res.locals.loggedIn = { accessToken: accessToken }; 66 | } else { 67 | return next({ status: 403 }); 68 | } 69 | next(); 70 | } catch (err) { 71 | return next({ 72 | log: `error occured in userController.login: ${err}`, 73 | status: 400, 74 | message: {err: 'error in login'}, 75 | }); 76 | } 77 | }; 78 | 79 | module.exports = userController; 80 | -------------------------------------------------------------------------------- /src/server/models/models.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const { Pool } = require('pg'); 3 | 4 | // initialized globally to allow query method to obtain its value 5 | let pool; 6 | 7 | module.exports = { 8 | // newPool method establishes new connection to database URL after user provides it 9 | newPool: () => { 10 | pool = new Pool({ 11 | connectionString: process.env.PG_URI, 12 | }); 13 | }, 14 | query: (text, params, callback) => { 15 | console.log('executed query: ', text); 16 | return pool.query(text, params, callback); 17 | }, 18 | }; -------------------------------------------------------------------------------- /src/server/models/starwarsModel.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const { Pool } = require('pg'); 3 | 4 | let userPool = new Pool({ 5 | connectionString: 6 | 'postgres://xstskucd:HUMKg1LzALryqlQM26N5uKLWF5ol1fbT@peanut.db.elephantsql.com/xstskucd', 7 | }); 8 | module.exports = { 9 | query: (text, params, callback) => { 10 | console.log('executed query: ', text); 11 | return userPool.query(text, params, callback); 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /src/server/models/userModel.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const { Pool } = require('pg'); 3 | 4 | let userPool = new Pool({ 5 | connectionString: process.env.USER_URI, 6 | }); 7 | module.exports = { 8 | query: (text, params, callback) => { 9 | console.log('executed query: ', text); 10 | return userPool.query(text, params, callback); 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /src/server/routes/dbLink.ts: -------------------------------------------------------------------------------- 1 | import express, { Request, Response } from 'express'; 2 | const router = express.Router(); 3 | const dbLinkController = require('../controllers/dbLinkController'); 4 | const dbSchemaController = require('../controllers/dbSchemaController'); 5 | const fnKeyController = require('../controllers/fnKeyController'); 6 | const treeController = require('../controllers/treeController'); 7 | const schemaGen = require('../controllers/schemaGen'); 8 | const resolverController = require('../controllers/resolverController'); 9 | const mutationController = require('../controllers/mutationController'); 10 | 11 | router.post( 12 | '/resolver', 13 | dbLinkController.connectDb, 14 | dbLinkController.extractFnKeys, 15 | fnKeyController.parseFnKeyData, 16 | fnKeyController.parsePrimaryKeyData, 17 | dbSchemaController.getSchema, 18 | resolverController.genResolver, 19 | mutationController.mutationResolver, 20 | (req, res) => res.status(200).json(res.locals.resolverString) 21 | ); 22 | 23 | router.post( 24 | '/', 25 | dbLinkController.connectDb, 26 | dbLinkController.extractFnKeys, 27 | fnKeyController.parseFnKeyData, 28 | fnKeyController.parsePrimaryKeyData, 29 | dbSchemaController.getSchema, 30 | treeController.treeSchema, 31 | schemaGen.genSchema, 32 | resolverController.genResolver, 33 | mutationController.mutationSchema, 34 | mutationController.mutationResolver, 35 | (req, res) => { 36 | return res.status(202).json(res.locals); 37 | } 38 | ); 39 | 40 | router.get( 41 | '/', 42 | dbLinkController.connectDemo, 43 | dbLinkController.extractFnKeys, 44 | fnKeyController.parseFnKeyData, 45 | fnKeyController.parsePrimaryKeyData, 46 | dbSchemaController.getSchema, 47 | treeController.treeSchema, 48 | schemaGen.genSchema, 49 | resolverController.genResolver, 50 | mutationController.mutationSchema, 51 | mutationController.mutationResolver, 52 | (req, res) => { 53 | return res.status(202).json(res.locals); 54 | } 55 | ); 56 | 57 | module.exports = router; 58 | -------------------------------------------------------------------------------- /src/server/routes/graphqlRouter.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction } from 'express'; 2 | 3 | const { ApolloServer, gql } = require('apollo-server'); 4 | 5 | const { typeDefs } = require('./schema'); 6 | const { 7 | Query, 8 | People, 9 | PeopleInFilm, 10 | Pilot, 11 | PlanetsInFilm, 12 | Species, 13 | SpeciesInFilm, 14 | StarshipSpec, 15 | VesselsInFilm, 16 | Planet, 17 | Film, 18 | Vessel, 19 | Mutation, 20 | } = require('./resolvers'); 21 | 22 | const server = new ApolloServer({ 23 | typeDefs, 24 | resolvers: { 25 | Query, 26 | People, 27 | PeopleInFilm, 28 | Pilot, 29 | PlanetsInFilm, 30 | Species, 31 | SpeciesInFilm, 32 | StarshipSpec, 33 | VesselsInFilm, 34 | Planet, 35 | Film, 36 | Vessel, 37 | Mutation, 38 | }, 39 | }); 40 | 41 | // server will be listening at 4000 42 | //@ts-ignore 43 | server.listen().then(({ url }) => { 44 | console.log('Server listening at ' + url); 45 | }); 46 | -------------------------------------------------------------------------------- /src/server/routes/projectRouter.ts: -------------------------------------------------------------------------------- 1 | import express, { Request, Response } from 'express'; 2 | const router = express.Router(); 3 | const projectController = require('../controllers/projectController'); 4 | 5 | router.post('/save', projectController.saveProject, (req, res) => { 6 | return res.status(200).json(res.locals.savedProject); 7 | }); 8 | 9 | router.get('/:id', projectController.getProjects, (req, res) => { 10 | return res.status(200).json(res.locals.projects); 11 | }); 12 | 13 | router.patch('/update', projectController.updateProject, (req, res) => { 14 | return res.status(200).json(res.locals); 15 | }); 16 | 17 | router.delete('/delete/:id', projectController.deleteProject, (req, res) => { 18 | return res.status(200).json(res.locals); 19 | }); 20 | 21 | module.exports = router; -------------------------------------------------------------------------------- /src/server/routes/schema.js: -------------------------------------------------------------------------------- 1 | const { gql } = require('apollo-server'); 2 | 3 | exports.typeDefs = gql` 4 | type Query { 5 | films: [Film] 6 | film(_id: ID): Film 7 | fulltable: [Fulltable] 8 | fulltable_single(_id: ID): Fulltable 9 | people: [People] 10 | person(_id: ID): People 11 | people_in_films: [PeopleInFilm] 12 | people_in_film(_id: ID): PeopleInFilm 13 | pilots: [Pilot] 14 | pilot(_id: ID): Pilot 15 | planets: [Planet] 16 | planet(_id: ID): Planet 17 | planets_in_films: [PlanetsInFilm] 18 | planets_in_film(_id: ID): PlanetsInFilm 19 | species: [Species] 20 | specie(_id: ID): Species 21 | species_in_films: [SpeciesInFilm] 22 | species_in_film(_id: ID): SpeciesInFilm 23 | starship_specs: [StarshipSpec] 24 | starship_spec(_id: ID): StarshipSpec 25 | vessels: [Vessel] 26 | vessel(_id: ID): Vessel 27 | vessels_in_films: [VesselsInFilm] 28 | vessels_in_film(_id: ID): VesselsInFilm 29 | } 30 | 31 | type Film { 32 | _id: Int 33 | title: String 34 | episode_id: Int 35 | opening_crawl: String 36 | director: String 37 | producer: String 38 | release_date: String 39 | people_in_films: [PeopleInFilm] 40 | planets_in_films: [PlanetsInFilm] 41 | species_in_films: [SpeciesInFilm] 42 | vessels_in_films: [VesselsInFilm] 43 | } 44 | 45 | type Fulltable { 46 | name: String 47 | } 48 | 49 | type People { 50 | _id: Int 51 | name: String 52 | mass: String 53 | hair_color: String 54 | skin_color: String 55 | eye_color: String 56 | birth_year: String 57 | gender: String 58 | species_id: Int 59 | homeworld_id: Int 60 | height: Int 61 | species_id_info: Species 62 | homeworld_id_info: Planet 63 | people_in_films: [PeopleInFilm] 64 | pilots: [Pilot] 65 | } 66 | 67 | type PeopleInFilm { 68 | _id: Int 69 | person_id: Int 70 | film_id: Int 71 | person_id_info: People 72 | film_id_info: Film 73 | } 74 | 75 | type Pilot { 76 | _id: Int 77 | person_id: Int 78 | vessel_id: Int 79 | vessel_id_info: Vessel 80 | person_id_info: People 81 | } 82 | 83 | type Planet { 84 | _id: Int 85 | name: String 86 | rotation_period: Int 87 | orbital_period: Int 88 | diameter: Int 89 | climate: String 90 | gravity: String 91 | terrain: String 92 | surface_water: String 93 | population: Int 94 | people: [People] 95 | planets_in_films: [PlanetsInFilm] 96 | species: [Species] 97 | } 98 | 99 | type PlanetsInFilm { 100 | _id: Int 101 | film_id: Int 102 | planet_id: Int 103 | film_id_info: Film 104 | planet_id_info: Planet 105 | } 106 | 107 | type Species { 108 | _id: Int 109 | name: String 110 | classification: String 111 | average_height: String 112 | average_lifespan: String 113 | hair_colors: String 114 | skin_colors: String 115 | eye_colors: String 116 | language: String 117 | homeworld_id: Int 118 | homeworld_id_info: Planet 119 | people: [People] 120 | species_in_films: [SpeciesInFilm] 121 | } 122 | 123 | type SpeciesInFilm { 124 | _id: Int 125 | film_id: Int 126 | species_id: Int 127 | film_id_info: Film 128 | species_id_info: Species 129 | } 130 | 131 | type StarshipSpec { 132 | _id: Int 133 | hyperdrive_rating: String 134 | MGLT: String 135 | vessel_id: Int 136 | vessel_id_info: Vessel 137 | } 138 | 139 | type Vessel { 140 | _id: Int 141 | name: String 142 | manufacturer: String 143 | model: String 144 | vessel_type: String 145 | vessel_class: String 146 | cost_in_credits: Int 147 | length: String 148 | max_atmosphering_speed: String 149 | crew: Int 150 | passengers: Int 151 | cargo_capacity: String 152 | consumables: String 153 | pilots: [Pilot] 154 | starship_specs: [StarshipSpec] 155 | vessels_in_films: [VesselsInFilm] 156 | } 157 | 158 | type VesselsInFilm { 159 | _id: Int 160 | vessel_id: Int 161 | film_id: Int 162 | vessel_id_info: Vessel 163 | film_id_info: Film 164 | } 165 | 166 | type Mutation { 167 | addFilm(input: AddFilmInput): Film 168 | updateFilm(_id: ID, input: UpdateFilmInput): Film 169 | deleteFilm(_id: ID): Film 170 | 171 | addFulltable(input: AddFulltableInput): Fulltable 172 | updateFulltable(_id: ID, input: UpdateFulltableInput): Fulltable 173 | deleteFulltable(_id: ID): Fulltable 174 | 175 | addPeople(input: AddPeopleInput): People 176 | updatePeople(_id: ID, input: UpdatePeopleInput): People 177 | deletePeople(_id: ID): People 178 | 179 | addPeopleInFilm(input: AddPeopleInFilmInput): PeopleInFilm 180 | updatePeopleInFilm(_id: ID, input: UpdatePeopleInFilmInput): PeopleInFilm 181 | deletePeopleInFilm(_id: ID): PeopleInFilm 182 | 183 | addPilot(input: AddPilotInput): Pilot 184 | updatePilot(_id: ID, input: UpdatePilotInput): Pilot 185 | deletePilot(_id: ID): Pilot 186 | 187 | addPlanet(input: AddPlanetInput): Planet 188 | updatePlanet(_id: ID, input: UpdatePlanetInput): Planet 189 | deletePlanet(_id: ID): Planet 190 | 191 | addPlanetsInFilm(input: AddPlanetsInFilmInput): PlanetsInFilm 192 | updatePlanetsInFilm(_id: ID, input: UpdatePlanetsInFilmInput): PlanetsInFilm 193 | deletePlanetsInFilm(_id: ID): PlanetsInFilm 194 | 195 | addSpecies(input: AddSpeciesInput): Species 196 | updateSpecies(_id: ID, input: UpdateSpeciesInput): Species 197 | deleteSpecies(_id: ID): Species 198 | 199 | addSpeciesInFilm(input: AddSpeciesInFilmInput): SpeciesInFilm 200 | updateSpeciesInFilm(_id: ID, input: UpdateSpeciesInFilmInput): SpeciesInFilm 201 | deleteSpeciesInFilm(_id: ID): SpeciesInFilm 202 | 203 | addStarshipSpec(input: AddStarshipSpecInput): StarshipSpec 204 | updateStarshipSpec(_id: ID, input: UpdateStarshipSpecInput): StarshipSpec 205 | deleteStarshipSpec(_id: ID): StarshipSpec 206 | 207 | addVessel(input: AddVesselInput): Vessel 208 | updateVessel(_id: ID, input: UpdateVesselInput): Vessel 209 | deleteVessel(_id: ID): Vessel 210 | 211 | addVesselsInFilm(input: AddVesselsInFilmInput): VesselsInFilm 212 | updateVesselsInFilm(_id: ID, input: UpdateVesselsInFilmInput): VesselsInFilm 213 | deleteVesselsInFilm(_id: ID): VesselsInFilm 214 | } 215 | 216 | input AddFilmInput { 217 | title: String 218 | episode_id: Int 219 | opening_crawl: String 220 | director: String 221 | producer: String 222 | release_date: String 223 | } 224 | 225 | input UpdateFilmInput { 226 | title: String 227 | episode_id: Int 228 | opening_crawl: String 229 | director: String 230 | producer: String 231 | release_date: String 232 | } 233 | 234 | input AddFulltableInput { 235 | name: String 236 | } 237 | 238 | input UpdateFulltableInput { 239 | name: String 240 | } 241 | 242 | input AddPeopleInput { 243 | name: String 244 | mass: String 245 | hair_color: String 246 | skin_color: String 247 | eye_color: String 248 | birth_year: String 249 | gender: String 250 | species_id: Int 251 | homeworld_id: Int 252 | height: Int 253 | } 254 | 255 | input UpdatePeopleInput { 256 | name: String 257 | mass: String 258 | hair_color: String 259 | skin_color: String 260 | eye_color: String 261 | birth_year: String 262 | gender: String 263 | species_id: Int 264 | homeworld_id: Int 265 | height: Int 266 | } 267 | 268 | input AddPeopleInFilmInput { 269 | person_id: Int 270 | film_id: Int 271 | } 272 | 273 | input UpdatePeopleInFilmInput { 274 | person_id: Int 275 | film_id: Int 276 | } 277 | 278 | input AddPilotInput { 279 | person_id: Int 280 | vessel_id: Int 281 | } 282 | 283 | input UpdatePilotInput { 284 | person_id: Int 285 | vessel_id: Int 286 | } 287 | 288 | input AddPlanetInput { 289 | name: String 290 | rotation_period: Int 291 | orbital_period: Int 292 | diameter: Int 293 | climate: String 294 | gravity: String 295 | terrain: String 296 | surface_water: String 297 | population: Int 298 | } 299 | 300 | input UpdatePlanetInput { 301 | name: String 302 | rotation_period: Int 303 | orbital_period: Int 304 | diameter: Int 305 | climate: String 306 | gravity: String 307 | terrain: String 308 | surface_water: String 309 | population: Int 310 | } 311 | 312 | input AddPlanetsInFilmInput { 313 | film_id: Int 314 | planet_id: Int 315 | } 316 | 317 | input UpdatePlanetsInFilmInput { 318 | film_id: Int 319 | planet_id: Int 320 | } 321 | 322 | input AddSpeciesInput { 323 | name: String 324 | classification: String 325 | average_height: String 326 | average_lifespan: String 327 | hair_colors: String 328 | skin_colors: String 329 | eye_colors: String 330 | language: String 331 | homeworld_id: Int 332 | } 333 | 334 | input UpdateSpeciesInput { 335 | name: String 336 | classification: String 337 | average_height: String 338 | average_lifespan: String 339 | hair_colors: String 340 | skin_colors: String 341 | eye_colors: String 342 | language: String 343 | homeworld_id: Int 344 | } 345 | 346 | input AddSpeciesInFilmInput { 347 | film_id: Int 348 | species_id: Int 349 | } 350 | 351 | input UpdateSpeciesInFilmInput { 352 | film_id: Int 353 | species_id: Int 354 | } 355 | 356 | input AddStarshipSpecInput { 357 | hyperdrive_rating: String 358 | MGLT: String 359 | vessel_id: Int 360 | } 361 | 362 | input UpdateStarshipSpecInput { 363 | hyperdrive_rating: String 364 | MGLT: String 365 | vessel_id: Int 366 | } 367 | 368 | input AddVesselInput { 369 | name: String 370 | manufacturer: String 371 | model: String 372 | vessel_type: String 373 | vessel_class: String 374 | cost_in_credits: Int 375 | length: String 376 | max_atmosphering_speed: String 377 | crew: Int 378 | passengers: Int 379 | cargo_capacity: String 380 | consumables: String 381 | } 382 | 383 | input UpdateVesselInput { 384 | name: String 385 | manufacturer: String 386 | model: String 387 | vessel_type: String 388 | vessel_class: String 389 | cost_in_credits: Int 390 | length: String 391 | max_atmosphering_speed: String 392 | crew: Int 393 | passengers: Int 394 | cargo_capacity: String 395 | consumables: String 396 | } 397 | 398 | input AddVesselsInFilmInput { 399 | vessel_id: Int 400 | film_id: Int 401 | } 402 | 403 | input UpdateVesselsInFilmInput { 404 | vessel_id: Int 405 | film_id: Int 406 | } 407 | `; 408 | -------------------------------------------------------------------------------- /src/server/routes/userRouter.ts: -------------------------------------------------------------------------------- 1 | import express, { Request, Response, Router } from 'express'; 2 | const jwt = require('jsonwebtoken'); 3 | const path = require('path'); 4 | const router: Router = express.Router(); 5 | const authController = require('../controllers/authController'); 6 | const testController = require('../controllers/testController'); 7 | const userController = require('../controllers/userController'); 8 | 9 | router.get('/checkToken', authController.checkToken, (req, res) => { 10 | return res.status(200).json(res.locals.authenticate); 11 | }); 12 | router.get('/signOut', (req, res) => { 13 | return res.clearCookie('token').status(200); 14 | }); 15 | router.post('/check', userController.checkUsernameExistence, (req, res) => { 16 | return res.status(200).json(res.locals.existence); 17 | }); 18 | 19 | router.post('/signup', userController.signUp, (req, res) => { 20 | return res.status(201).json(res.locals.signedUp); 21 | }); 22 | 23 | router.post('/login', userController.login, (req: Request, res: Response) => { 24 | return res.status(200).json(res.locals.loggedIn); 25 | }); 26 | 27 | module.exports = router; 28 | -------------------------------------------------------------------------------- /src/server/server.ts: -------------------------------------------------------------------------------- 1 | import express, { 2 | Request, 3 | Response, 4 | NextFunction, 5 | RequestHandler, 6 | } from 'express'; 7 | const path = require('path'); 8 | const app = express(); 9 | const PORT = 3000; 10 | const dbLinkRouter = require('./routes/dbLink'); 11 | const userRouter = require('./routes/userRouter'); 12 | const projectRouter = require('./routes/projectRouter'); 13 | const graphqlController = require('./routes/graphqlRouter'); 14 | const authController = require('./controllers/authController'); 15 | const cookieParser = require('cookie-parser'); 16 | 17 | 18 | type ServerError = { 19 | log: string; 20 | status: number; 21 | message: { 22 | err: string; 23 | }; 24 | }; 25 | 26 | // parse incoming request body 27 | app.use(express.json()); 28 | app.use(express.urlencoded({ extended: true })); 29 | app.use(cookieParser()); 30 | 31 | // app.use(express.static(path.resolve(__dirname, './src/client'))); 32 | 33 | // app.use((req, res, next) => { 34 | // res.setHeader('Access-Control-Allow-Origin', '*'); 35 | // res.setHeader('Access-Control-Allow-Credentials', 'true'); 36 | // res.setHeader('Access-Control-Allow-Methods', 'GET,HEAD,OPTIONS,POST,PUT'); 37 | // res.setHeader( 38 | // 'Access-Control-Allow-Headers', 39 | // 'Access-Control-Allow-Headers, Origin,Accept, X-Requested-With, Content-Type, Access-Control-Request-Method, Access-Control-Request-Headers' 40 | // ); 41 | // res.status(200); 42 | // next(); 43 | // }); 44 | 45 | // for login/signup 46 | app.use('/user', userRouter); 47 | app.use('/projects', projectRouter); 48 | 49 | // send database link to appropriate router 50 | app.use('/db', dbLinkRouter, (req, res) => { 51 | res.status(200).json('success'); 52 | }); 53 | 54 | 55 | // statically serve everything in the build folder on the route '/build' 56 | if(process.env.NODE_ENV === 'production') { 57 | console.log('made it to server') 58 | app.use(express.static(path.join(__dirname, '../../dist'))); 59 | // serve index.html on the route '/' 60 | console.log('made it past build') 61 | app.get('/*', (req, res) => { 62 | console.log('made it to app.get') 63 | res.status(200).sendFile(path.join(__dirname, '../../dist/index.html')); 64 | }); 65 | } 66 | 67 | // catch all error handler 68 | app.use((req, res) => res.status(404).send('This page does not exist.')); 69 | 70 | // global error handler 71 | app.use((err: ServerError, req: Request, res: Response, next: NextFunction) => { 72 | console.log(err) 73 | const defaultErr = { 74 | log: 'Global Error handler triggered', 75 | status: 500, 76 | message: { err: 'Error occurred' }, 77 | }; 78 | const errorObj = Object.assign(defaultErr, err) 79 | console.log(errorObj.log); 80 | return res.status(errorObj.status).json(errorObj.message); 81 | }); 82 | 83 | app.listen(PORT, () => console.log('server listening on port ' + PORT)); 84 | module.exports = app; 85 | -------------------------------------------------------------------------------- /src/server/testDB.js: -------------------------------------------------------------------------------- 1 | 2 | exports.testDB = [ 3 | { 4 | table_schema: 'public', 5 | table_name: 'films', 6 | position: 1, 7 | column_name: '_id', 8 | data_type: 'integer', 9 | default_value: "nextval('films__id_seq'::regclass)" 10 | }, 11 | { 12 | table_schema: 'public', 13 | table_name: 'films', 14 | position: 2, 15 | column_name: 'title', 16 | data_type: 'character varying', 17 | default_value: null 18 | }, 19 | { 20 | table_schema: 'public', 21 | table_name: 'films', 22 | position: 3, 23 | column_name: 'episode_id', 24 | data_type: 'integer', 25 | default_value: null 26 | }, 27 | { 28 | table_schema: 'public', 29 | table_name: 'films', 30 | position: 4, 31 | column_name: 'opening_crawl', 32 | data_type: 'character varying', 33 | default_value: null 34 | }, 35 | { 36 | table_schema: 'public', 37 | table_name: 'films', 38 | position: 5, 39 | column_name: 'director', 40 | data_type: 'character varying', 41 | default_value: null 42 | }, 43 | { 44 | table_schema: 'public', 45 | table_name: 'films', 46 | position: 6, 47 | column_name: 'producer', 48 | data_type: 'character varying', 49 | default_value: null 50 | }, 51 | { 52 | table_schema: 'public', 53 | table_name: 'films', 54 | position: 7, 55 | column_name: 'release_date', 56 | data_type: 'date', 57 | default_value: null 58 | }, 59 | { 60 | table_schema: 'public', 61 | table_name: 'people', 62 | position: 1, 63 | column_name: '_id', 64 | data_type: 'integer', 65 | default_value: "nextval('people__id_seq'::regclass)" 66 | }, 67 | { 68 | table_schema: 'public', 69 | table_name: 'people', 70 | position: 2, 71 | column_name: 'name', 72 | data_type: 'character varying', 73 | default_value: null 74 | }, 75 | { 76 | table_schema: 'public', 77 | table_name: 'people', 78 | position: 3, 79 | column_name: 'mass', 80 | data_type: 'character varying', 81 | default_value: null 82 | }, 83 | { 84 | table_schema: 'public', 85 | table_name: 'people', 86 | position: 4, 87 | column_name: 'hair_color', 88 | data_type: 'character varying', 89 | default_value: null 90 | }, 91 | { 92 | table_schema: 'public', 93 | table_name: 'people', 94 | position: 5, 95 | column_name: 'skin_color', 96 | data_type: 'character varying', 97 | default_value: null 98 | }, 99 | { 100 | table_schema: 'public', 101 | table_name: 'people', 102 | position: 6, 103 | column_name: 'eye_color', 104 | data_type: 'character varying', 105 | default_value: null 106 | }, 107 | { 108 | table_schema: 'public', 109 | table_name: 'people', 110 | position: 7, 111 | column_name: 'birth_year', 112 | data_type: 'character varying', 113 | default_value: null 114 | }, 115 | { 116 | table_schema: 'public', 117 | table_name: 'people', 118 | position: 8, 119 | column_name: 'gender', 120 | data_type: 'character varying', 121 | default_value: null 122 | }, 123 | { 124 | table_schema: 'public', 125 | table_name: 'people', 126 | position: 9, 127 | column_name: 'species_id', 128 | data_type: 'bigint', 129 | default_value: null 130 | }, 131 | { 132 | table_schema: 'public', 133 | table_name: 'people', 134 | position: 10, 135 | column_name: 'homeworld_id', 136 | data_type: 'bigint', 137 | default_value: null 138 | }, 139 | { 140 | table_schema: 'public', 141 | table_name: 'people', 142 | position: 11, 143 | column_name: 'height', 144 | data_type: 'integer', 145 | default_value: null 146 | }, 147 | { 148 | table_schema: 'public', 149 | table_name: 'people_in_films', 150 | position: 1, 151 | column_name: '_id', 152 | data_type: 'integer', 153 | default_value: "nextval('people_in_films__id_seq'::regclass)" 154 | }, 155 | { 156 | table_schema: 'public', 157 | table_name: 'people_in_films', 158 | position: 2, 159 | column_name: 'person_id', 160 | data_type: 'bigint', 161 | default_value: null 162 | }, 163 | { 164 | table_schema: 'public', 165 | table_name: 'people_in_films', 166 | position: 3, 167 | column_name: 'film_id', 168 | data_type: 'bigint', 169 | default_value: null 170 | }, 171 | { 172 | table_schema: 'public', 173 | table_name: 'pilots', 174 | position: 1, 175 | column_name: '_id', 176 | data_type: 'integer', 177 | default_value: "nextval('pilots__id_seq'::regclass)" 178 | }, 179 | { 180 | table_schema: 'public', 181 | table_name: 'pilots', 182 | position: 2, 183 | column_name: 'person_id', 184 | data_type: 'bigint', 185 | default_value: null 186 | }, 187 | { 188 | table_schema: 'public', 189 | table_name: 'pilots', 190 | position: 3, 191 | column_name: 'vessel_id', 192 | data_type: 'bigint', 193 | default_value: null 194 | }, 195 | { 196 | table_schema: 'public', 197 | table_name: 'planets', 198 | position: 1, 199 | column_name: '_id', 200 | data_type: 'integer', 201 | default_value: "nextval('planets__id_seq'::regclass)" 202 | }, 203 | { 204 | table_schema: 'public', 205 | table_name: 'planets', 206 | position: 2, 207 | column_name: 'name', 208 | data_type: 'character varying', 209 | default_value: null 210 | }, 211 | { 212 | table_schema: 'public', 213 | table_name: 'planets', 214 | position: 3, 215 | column_name: 'rotation_period', 216 | data_type: 'integer', 217 | default_value: null 218 | }, 219 | { 220 | table_schema: 'public', 221 | table_name: 'planets', 222 | position: 4, 223 | column_name: 'orbital_period', 224 | data_type: 'integer', 225 | default_value: null 226 | }, 227 | { 228 | table_schema: 'public', 229 | table_name: 'planets', 230 | position: 5, 231 | column_name: 'diameter', 232 | data_type: 'integer', 233 | default_value: null 234 | }, 235 | { 236 | table_schema: 'public', 237 | table_name: 'planets', 238 | position: 6, 239 | column_name: 'climate', 240 | data_type: 'character varying', 241 | default_value: null 242 | }, 243 | { 244 | table_schema: 'public', 245 | table_name: 'planets', 246 | position: 7, 247 | column_name: 'gravity', 248 | data_type: 'character varying', 249 | default_value: null 250 | }, 251 | { 252 | table_schema: 'public', 253 | table_name: 'planets', 254 | position: 8, 255 | column_name: 'terrain', 256 | data_type: 'character varying', 257 | default_value: null 258 | }, 259 | { 260 | table_schema: 'public', 261 | table_name: 'planets', 262 | position: 9, 263 | column_name: 'surface_water', 264 | data_type: 'character varying', 265 | default_value: null 266 | }, 267 | { 268 | table_schema: 'public', 269 | table_name: 'planets', 270 | position: 10, 271 | column_name: 'population', 272 | data_type: 'bigint', 273 | default_value: null 274 | }, 275 | { 276 | table_schema: 'public', 277 | table_name: 'planets_in_films', 278 | position: 1, 279 | column_name: '_id', 280 | data_type: 'integer', 281 | default_value: "nextval('planets_in_films__id_seq'::regclass)" 282 | }, 283 | { 284 | table_schema: 'public', 285 | table_name: 'planets_in_films', 286 | position: 2, 287 | column_name: 'film_id', 288 | data_type: 'bigint', 289 | default_value: null 290 | }, 291 | { 292 | table_schema: 'public', 293 | table_name: 'planets_in_films', 294 | position: 3, 295 | column_name: 'planet_id', 296 | data_type: 'bigint', 297 | default_value: null 298 | }, 299 | { 300 | table_schema: 'public', 301 | table_name: 'species', 302 | position: 1, 303 | column_name: '_id', 304 | data_type: 'integer', 305 | default_value: "nextval('species__id_seq'::regclass)" 306 | }, 307 | { 308 | table_schema: 'public', 309 | table_name: 'species', 310 | position: 2, 311 | column_name: 'name', 312 | data_type: 'character varying', 313 | default_value: null 314 | }, 315 | { 316 | table_schema: 'public', 317 | table_name: 'species', 318 | position: 3, 319 | column_name: 'classification', 320 | data_type: 'character varying', 321 | default_value: null 322 | }, 323 | { 324 | table_schema: 'public', 325 | table_name: 'species', 326 | position: 4, 327 | column_name: 'average_height', 328 | data_type: 'character varying', 329 | default_value: null 330 | }, 331 | { 332 | table_schema: 'public', 333 | table_name: 'species', 334 | position: 5, 335 | column_name: 'average_lifespan', 336 | data_type: 'character varying', 337 | default_value: null 338 | }, 339 | { 340 | table_schema: 'public', 341 | table_name: 'species', 342 | position: 6, 343 | column_name: 'hair_colors', 344 | data_type: 'character varying', 345 | default_value: null 346 | }, 347 | { 348 | table_schema: 'public', 349 | table_name: 'species', 350 | position: 7, 351 | column_name: 'skin_colors', 352 | data_type: 'character varying', 353 | default_value: null 354 | }, 355 | { 356 | table_schema: 'public', 357 | table_name: 'species', 358 | position: 8, 359 | column_name: 'eye_colors', 360 | data_type: 'character varying', 361 | default_value: null 362 | }, 363 | { 364 | table_schema: 'public', 365 | table_name: 'species', 366 | position: 9, 367 | column_name: 'language', 368 | data_type: 'character varying', 369 | default_value: null 370 | }, 371 | { 372 | table_schema: 'public', 373 | table_name: 'species', 374 | position: 10, 375 | column_name: 'homeworld_id', 376 | data_type: 'bigint', 377 | default_value: null 378 | }, 379 | { 380 | table_schema: 'public', 381 | table_name: 'species_in_films', 382 | position: 1, 383 | column_name: '_id', 384 | data_type: 'integer', 385 | default_value: "nextval('species_in_films__id_seq'::regclass)" 386 | }, 387 | { 388 | table_schema: 'public', 389 | table_name: 'species_in_films', 390 | position: 2, 391 | column_name: 'film_id', 392 | data_type: 'bigint', 393 | default_value: null 394 | }, 395 | { 396 | table_schema: 'public', 397 | table_name: 'species_in_films', 398 | position: 3, 399 | column_name: 'species_id', 400 | data_type: 'bigint', 401 | default_value: null 402 | }, 403 | { 404 | table_schema: 'public', 405 | table_name: 'starship_specs', 406 | position: 1, 407 | column_name: '_id', 408 | data_type: 'integer', 409 | default_value: "nextval('starship_specs__id_seq'::regclass)" 410 | }, 411 | { 412 | table_schema: 'public', 413 | table_name: 'starship_specs', 414 | position: 2, 415 | column_name: 'hyperdrive_rating', 416 | data_type: 'character varying', 417 | default_value: null 418 | }, 419 | { 420 | table_schema: 'public', 421 | table_name: 'starship_specs', 422 | position: 3, 423 | column_name: 'MGLT', 424 | data_type: 'character varying', 425 | default_value: null 426 | }, 427 | { 428 | table_schema: 'public', 429 | table_name: 'starship_specs', 430 | position: 4, 431 | column_name: 'vessel_id', 432 | data_type: 'bigint', 433 | default_value: null 434 | }, 435 | { 436 | table_schema: 'public', 437 | table_name: 'vessels', 438 | position: 1, 439 | column_name: '_id', 440 | data_type: 'integer', 441 | default_value: "nextval('vessels__id_seq'::regclass)" 442 | }, 443 | { 444 | table_schema: 'public', 445 | table_name: 'vessels', 446 | position: 2, 447 | column_name: 'name', 448 | data_type: 'character varying', 449 | default_value: null 450 | }, 451 | { 452 | table_schema: 'public', 453 | table_name: 'vessels', 454 | position: 3, 455 | column_name: 'manufacturer', 456 | data_type: 'character varying', 457 | default_value: null 458 | }, 459 | { 460 | table_schema: 'public', 461 | table_name: 'vessels', 462 | position: 4, 463 | column_name: 'model', 464 | data_type: 'character varying', 465 | default_value: null 466 | }, 467 | { 468 | table_schema: 'public', 469 | table_name: 'vessels', 470 | position: 5, 471 | column_name: 'vessel_type', 472 | data_type: 'character varying', 473 | default_value: null 474 | }, 475 | { 476 | table_schema: 'public', 477 | table_name: 'vessels', 478 | position: 6, 479 | column_name: 'vessel_class', 480 | data_type: 'character varying', 481 | default_value: null 482 | }, 483 | { 484 | table_schema: 'public', 485 | table_name: 'vessels', 486 | position: 7, 487 | column_name: 'cost_in_credits', 488 | data_type: 'bigint', 489 | default_value: null 490 | }, 491 | { 492 | table_schema: 'public', 493 | table_name: 'vessels', 494 | position: 8, 495 | column_name: 'length', 496 | data_type: 'character varying', 497 | default_value: null 498 | }, 499 | { 500 | table_schema: 'public', 501 | table_name: 'vessels', 502 | position: 9, 503 | column_name: 'max_atmosphering_speed', 504 | data_type: 'character varying', 505 | default_value: null 506 | }, 507 | { 508 | table_schema: 'public', 509 | table_name: 'vessels', 510 | position: 10, 511 | column_name: 'crew', 512 | data_type: 'integer', 513 | default_value: null 514 | }, 515 | { 516 | table_schema: 'public', 517 | table_name: 'vessels', 518 | position: 11, 519 | column_name: 'passengers', 520 | data_type: 'integer', 521 | default_value: null 522 | }, 523 | { 524 | table_schema: 'public', 525 | table_name: 'vessels', 526 | position: 12, 527 | column_name: 'cargo_capacity', 528 | data_type: 'character varying', 529 | default_value: null 530 | }, 531 | { 532 | table_schema: 'public', 533 | table_name: 'vessels', 534 | position: 13, 535 | column_name: 'consumables', 536 | data_type: 'character varying', 537 | default_value: null 538 | }, 539 | { 540 | table_schema: 'public', 541 | table_name: 'vessels_in_films', 542 | position: 1, 543 | column_name: '_id', 544 | data_type: 'integer', 545 | default_value: "nextval('vessels_in_films__id_seq'::regclass)" 546 | }, 547 | { 548 | table_schema: 'public', 549 | table_name: 'vessels_in_films', 550 | position: 2, 551 | column_name: 'vessel_id', 552 | data_type: 'bigint', 553 | default_value: null 554 | }, 555 | { 556 | table_schema: 'public', 557 | table_name: 'vessels_in_films', 558 | position: 3, 559 | column_name: 'film_id', 560 | data_type: 'bigint', 561 | default_value: null 562 | } 563 | ] 564 | 565 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "ES2022", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | "jsx": "react", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 26 | 27 | /* Modules */ 28 | "module": "commonjs", /* Specify what module code is generated. */ 29 | "rootDir": "./", /* Specify the root folder within your source files. */ 30 | // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 35 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 38 | // "resolveJsonModule": true, /* Enable importing .json files. */ 39 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 40 | 41 | /* JavaScript Support */ 42 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 43 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 44 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 45 | 46 | /* Emit */ 47 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 48 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 49 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 50 | "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 51 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 52 | // "outDir": "./", /* Specify an output folder for all emitted files. */ 53 | "removeComments": true, /* Disable emitting comments. */ 54 | // "noEmit": true, /* Disable emitting files from a compilation. */ 55 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 56 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 57 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 58 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 59 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 60 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 61 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 62 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 63 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 64 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 65 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 66 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 67 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 68 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 69 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 70 | 71 | /* Interop Constraints */ 72 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 73 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 74 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 75 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 76 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 77 | 78 | /* Type Checking */ 79 | "strict": true, /* Enable all strict type-checking options. */ 80 | "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 81 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 82 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 83 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 84 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 85 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 86 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 87 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 88 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 89 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 90 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 91 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 92 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 93 | "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 94 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 95 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 96 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 97 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 98 | 99 | /* Completeness */ 100 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 101 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 102 | }, 103 | "include": ["src/**/*"] 104 | } 105 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | 2 | const webpack = require('webpack'); 3 | const path = require('path'); 4 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 5 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 6 | 7 | const config = { 8 | entry: './src/client/index.js', 9 | output: { 10 | path: path.resolve(__dirname, 'dist'), 11 | filename: 'bundle.js', 12 | clean: true, 13 | }, 14 | mode: process.env.NODE_ENV, 15 | devtool: 'source-map', 16 | devServer: { 17 | historyApiFallback: true, 18 | hot: true, 19 | static: { 20 | directory: path.join(__dirname, './build'), 21 | publicPath: '/', 22 | }, 23 | proxy: { 24 | '/user': 'http://localhost:3000', 25 | '/db': 'http://localhost:3000', 26 | '/projects': 'http://localhost:3000', 27 | '/graphiql': 'http://localhost:3000', 28 | '/request': 'http://localhost:4000', 29 | }, 30 | compress: true, 31 | port: 8080, 32 | }, 33 | module: { 34 | rules: [ 35 | { 36 | test: /\.(js|jsx)$/, 37 | exclude: /node_modules/, 38 | use: { 39 | loader: 'babel-loader', 40 | options: { 41 | presets: ['@babel/preset-env', '@babel/preset-react'], 42 | }, 43 | }, 44 | }, 45 | { 46 | test: /\.css$/, 47 | use: ['style-loader', 'css-loader'], 48 | }, 49 | { 50 | test: /\.scss$/, 51 | use: ['style-loader', 'css-loader', 'sass-loader'], 52 | }, 53 | { 54 | test: /\.png|svg|jpg|gif$/, 55 | exclude: /node_modules/, 56 | use: ['file-loader'], 57 | }, 58 | { 59 | test: /\.(ts|tsx)$/, 60 | exclude: /node_modules/, 61 | use: ['ts-loader'], 62 | }, 63 | ], 64 | }, 65 | resolve: { extensions: ['*', '.js', '.jsx', '.ts', '.tsx'] }, 66 | plugins: [ 67 | new HtmlWebpackPlugin({ template: './src/client/index.html' }), 68 | new MiniCssExtractPlugin(), 69 | ], 70 | }; 71 | 72 | module.exports = config; 73 | --------------------------------------------------------------------------------