├── .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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
55 |
56 | If logged in, clicking the ‘View Projects’ folder icon from the toolbar will navigate to the Projects page.
57 |
58 | 
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 | 
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 |
125 |
126 |
127 |
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 | {
18 | props.deleteProjectFunc();
19 | }}
20 | >
21 | Delete
22 |
23 | {
30 | props.close(false);
31 | }}
32 | >
33 | Cancel
34 |
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 | {
41 | // close(false);
42 | // }}
43 | >
44 | Close
45 |
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 |
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 | Login
216 |
217 | Don't Have an Account Yet?
218 |
219 | Sign Up
228 |
229 |
230 | Continue As Guest
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 | Sign Up
372 |
373 | Already Have An Account?
374 |
375 | Return to Login
384 |
385 |
386 | Continue As Guest
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 |
72 |
73 |
92 |
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 | navigate('/login')}
18 | >
19 | Sign In or Sign Up
20 |
21 | props.close(false)}
28 | >
29 | Continue as Guest Without Saving{' '}
30 |
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 | {
18 | props.close(false);
19 | props.setGetData(true);
20 | }}
21 | >
22 | Back to Projects
23 |
24 | {
31 | props.close(false);
32 | navigate('/');
33 | }}
34 | >
35 | Home
36 |
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 | navigate('/myprojects')}
18 | >
19 | View Your Saved Projects
20 |
21 | {
28 | props.close(false);
29 | }}
30 | >
31 | Back to Current Project
32 |
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 | navigate('/myprojects')}
18 | >
19 | View Your Saved Projects
20 |
21 | {
28 | props.close(false);
29 | }}
30 | >
31 | Back to Current Project
32 |
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 Open Project ;
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 Delete Project ;
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 |
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 |
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 | props.saveProjectFunc()}
23 | >
24 | Save
25 |
26 | {
33 | props.close(false);
34 | }}
35 | >
36 | Cancel
37 |
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 |
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 | {
19 | props.close(false);
20 | }}
21 | >
22 | Close
23 |
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 | props.updateProjectFunc(newName)}
27 | >
28 | Update
29 |
30 | {
37 | props.close(false);
38 | }}
39 | >
40 | Cancel
41 |
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 |
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 |
--------------------------------------------------------------------------------