├── .github └── workflows │ └── cypress_testing.yml ├── .gitignore ├── .vscode ├── launch.json └── settings.json ├── Dockerfile ├── LICENSE ├── README.md ├── app ├── (root) │ ├── Login │ │ ├── AnimateLogin.tsx │ │ ├── Login.tsx │ │ └── page.tsx │ ├── Register │ │ ├── Register.tsx │ │ └── page.tsx │ ├── RootContainer.tsx │ ├── frontendTypes.tsx │ ├── globals.css │ ├── layout.tsx │ └── page.tsx ├── Main │ ├── .DS_Store │ ├── About │ │ └── page.tsx │ ├── Chart │ │ ├── (components) │ │ │ ├── AboutPage.tsx │ │ │ ├── Chart.tsx │ │ │ ├── DisplayContainer.tsx │ │ │ ├── LoadContainer.tsx │ │ │ ├── LoadItem.tsx │ │ │ ├── MainContainer.tsx │ │ │ ├── NavigationBar.tsx │ │ │ ├── Resolver.tsx │ │ │ ├── ResolverDisplay.tsx │ │ │ ├── SaveContainer.tsx │ │ │ ├── Schema.tsx │ │ │ ├── VisualizeDB.tsx │ │ │ └── VisualizeSchemaResolver.tsx │ │ ├── (flow) │ │ │ ├── Edges.tsx │ │ │ ├── Flow.tsx │ │ │ ├── Nodes.tsx │ │ │ ├── TableNode.tsx │ │ │ ├── TableRow.tsx │ │ │ └── dummyRes.tsx │ │ └── page.tsx │ ├── layout.tsx │ └── loading.tsx └── types.ts ├── codingStandards.txt ├── cypress.config.ts ├── cypress ├── e2e │ ├── root-to-main.cy.ts │ ├── root.cy.ts │ └── unit-testing.cy.ts ├── fixtures │ └── example.json ├── support │ ├── commands.ts │ └── e2e.ts └── unit-testing │ └── unit-testing.cy.ts ├── docker-compose.yml ├── next-env.d.ts ├── next.config.js ├── package-lock.json ├── package.json ├── pages └── api │ └── graphql.ts ├── postcss.config.js ├── public ├── andres.jpeg ├── brian.png ├── daniel.png ├── logo.png └── stephen.png ├── server ├── .DS_Store ├── Dockerfile ├── db │ ├── dbConnection.ts │ └── quilDBConnection.ts ├── graphql │ ├── models │ │ ├── resolvers │ │ │ └── query.resolver.ts │ │ └── schemas │ │ │ ├── Data.ts │ │ │ ├── Mutation.ts │ │ │ ├── Query.ts │ │ │ └── Signin.ts │ └── modelsSetup.ts ├── helperFunctions.ts ├── middleware │ ├── auth.ts │ └── userController.ts ├── package-lock.json ├── package.json ├── resolverGenerator.ts ├── schemaGenerator.ts ├── server.ts ├── tsconfig.json └── types.ts ├── tailwind.config.js └── tsconfig.json /.github/workflows/cypress_testing.yml: -------------------------------------------------------------------------------- 1 | name: End-to-end tests 2 | on: [push] 3 | jobs: 4 | cypress-run: 5 | runs-on: ubuntu-20.04 6 | steps: 7 | - name: Checkout 8 | uses: actions/checkout@v3 9 | - run: npm i 10 | # Install NPM dependencies, cache them correctly 11 | # and run all Cypress tests 12 | - name: Cypress run 13 | uses: cypress-io/github-action@v5 14 | with: 15 | build: npm run build 16 | start: npm start 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | 13 | # next 14 | .next/ 15 | # Runtime data 16 | pids 17 | *.pid 18 | *.seed 19 | *.pid.lock 20 | 21 | # Directory for instrumented libs generated by jscoverage/JSCover 22 | lib-cov 23 | 24 | # Coverage directory used by tools like istanbul 25 | coverage 26 | *.lcov 27 | 28 | # nyc test coverage 29 | .nyc_output 30 | 31 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 32 | .grunt 33 | 34 | # Bower dependency directory (https://bower.io/) 35 | bower_components 36 | 37 | # node-waf configuration 38 | .lock-wscript 39 | 40 | # Compiled binary addons (https://nodejs.org/api/addons.html) 41 | build/Release 42 | 43 | # Dependency directories 44 | node_modules/ 45 | jspm_packages/ 46 | 47 | # TypeScript v1 declaration files 48 | typings/ 49 | 50 | # TypeScript cache 51 | *.tsbuildinfo 52 | 53 | # Optional npm cache directory 54 | .npm 55 | 56 | # Optional eslint cache 57 | .eslintcache 58 | 59 | # Microbundle cache 60 | .rpt2_cache/ 61 | .rts2_cache_cjs/ 62 | .rts2_cache_es/ 63 | .rts2_cache_umd/ 64 | 65 | # Optional REPL history 66 | .node_repl_history 67 | 68 | # Output of 'npm pack' 69 | *.tgz 70 | 71 | # Yarn Integrity file 72 | .yarn-integrity 73 | 74 | # dotenv environment variables file 75 | .env 76 | .env.test 77 | 78 | # parcel-bundler cache (https://parceljs.org/) 79 | .cache 80 | 81 | # Next.js build output 82 | .next 83 | 84 | # Nuxt.js build / generate output 85 | .nuxt 86 | dist 87 | 88 | # Gatsby files 89 | .cache/ 90 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 91 | # https://nextjs.org/blog/next-9-1#public-directory-support 92 | # public 93 | 94 | # vuepress build output 95 | .vuepress/dist 96 | 97 | # Serverless directories 98 | .serverless/ 99 | 100 | # FuseBox cache 101 | .fusebox/ 102 | 103 | # DynamoDB Local files 104 | .dynamodb/ 105 | 106 | # TernJS port file 107 | .tern-port 108 | 109 | .idea -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | 8 | 9 | 10 | { 11 | "type": "node", 12 | "request": "launch", 13 | "name": "Launch Chrome against localhost", 14 | "url": "http://localhost:8080", 15 | "webRoot": "${workspaceFolder}" 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "typescript.enablePromptUseWorkspaceTsdk": true 4 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16.17.1 2 | 3 | WORKDIR /usr/src/app 4 | 5 | COPY package.json /usr/src/app/ 6 | 7 | RUN npm install && npm install typescript -g 8 | 9 | COPY . /usr/src/app/ 10 | 11 | RUN tsc 12 | 13 | RUN npm run build 14 | 15 | CMD npm start 16 | 17 | EXPOSE 3000 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 OSLabs Beta 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # QuiL - Writing GraphQL for you 2 | 3 | QuiL is an open source developer tool that simplfies the process of implementing GraphQL and helps engineers better understand their data base. 4 | 5 | ### Table of Contents 6 | 7 | -General Information 8 | -Technologies Used 9 | -Screenshots 10 | -How to use 11 | -To do 12 | -Contributors 13 | 14 | ### GraphQL Schema & Resolver Generator 15 | 16 | Powered by QuiL's database analyzation algorithm, QuiL is able to use a database connection string and produce nessecary GQL schemas and GQL resolvers a developer would need to start a GraphQL backend 17 | 18 | ### Technologies Used 19 | 20 | -Next.js 13 21 | -React.js (React Hooks) - v18.2.0 22 | -React-flow-renderer - v11.2.0 23 | -Express - v4.18.2 24 | -jest - v29.3.1 25 | -Cypress v11.2.0 26 | -supertest - v6.1.6 27 | -Docker 28 | -Tailwindcss - v3.2.2 29 | 30 | ### How to use 31 | 32 | 1. On the root page you will have the option to input your PostgreSQL database URI or a sample database. 33 | 2. The main page will have the database of your choice rendered. 34 | 3. Center of the dev tool you will be able to visualize and interact with the database. 35 | 4. Above the visualizer off to the right you will have three buttons. 36 | a. First field allows you to input a PostgreSQL URI followed by a [Launch] button. 37 | b. Second button [Save] will give you the opportunity to save this project for future use. 38 | c. Third button [Load] lets you access previously saved projects. 39 | 5. On the top-left hand side you will have a [View Schemas/Resolvers] button that will open a drawer that gives you access to the generated schema types and resolvers generated by QuiL. 40 | 41 | ### To Do 42 | 43 | -Test metrics 44 | 45 | -Be able to use more SQL databases then onto NoSQL. 46 | 47 | -Integrate Redis caching. 48 | 49 | 50 | 51 | 52 | ### Contributors 53 | 54 | -Brian Tran 55 | 56 | -Stephen Fitzsimmons 57 | 58 | -Daniel An 59 | 60 | -Andres Jaramillo 61 | 62 | 63 | -------------------------------------------------------------------------------- /app/(root)/Login/AnimateLogin.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { motion } from "framer-motion"; 3 | const AnimationLogin = () => { 4 | return ( 5 |
6 |
7 | 13 | Login to 14 | 15 | 20 | 26 | view 27 | 28 | 29 | 35 | QuiL 36 | 37 |
38 | 46 | 55 | Databases 56 | 57 | 58 | 66 | 75 | Schemas 76 | 77 | 78 | 79 | 87 | 96 | Resolvers 97 | 98 | 99 |
100 | ); 101 | }; 102 | 103 | export default AnimationLogin; 104 | -------------------------------------------------------------------------------- /app/(root)/Login/Login.tsx: -------------------------------------------------------------------------------- 1 | import { inputObj, userObj } from '../../(root)/frontendTypes'; 2 | import Link from 'next/link'; 3 | import { motion } from 'framer-motion'; 4 | import { useRouter } from 'next/navigation'; 5 | import AnimationLogin from './AnimateLogin'; 6 | const randomstring = require('randomstring'); 7 | 8 | const Login = () => { 9 | const router = useRouter(); 10 | 11 | const SIGNIN_STATE_CODE = 'c2lnbmlu'; 12 | 13 | const loginHandler = async (e: any) => { 14 | e.preventDefault(); 15 | const userObj: userObj = { 16 | username: e.target.username.value, 17 | password: e.target.password.value, 18 | }; 19 | 20 | let data = await fetch('/api/graphql', { 21 | method: 'POST', 22 | headers: { 23 | 'Content-Type': 'application/json', 24 | }, 25 | body: JSON.stringify({ 26 | query: `mutation { 27 | signin(username: "${userObj.username}", password: "${userObj.password}") { 28 | token 29 | } 30 | }`, 31 | }), 32 | }) 33 | .then(data => { 34 | return data.json(); 35 | }) 36 | .then(data => { 37 | console.log('data', data); 38 | 39 | localStorage.setItem('token', data.data.signin.token); 40 | router.push('/'); 41 | }); 42 | }; 43 | 44 | return ( 45 |
46 |
47 | 48 | 66 |
67 | 70 | 77 | 78 | 81 | 88 |
89 | 96 |
97 |
98 |

OR

99 | 108 | 115 | 116 |
117 |
118 |
119 | ); 120 | }; 121 | 122 | export default Login; 123 | -------------------------------------------------------------------------------- /app/(root)/Login/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import Login from './Login'; 4 | import { useEffect, useState } from 'react'; 5 | import { useRouter } from 'next/navigation'; 6 | 7 | export default function Page({ searchParams }: any) { 8 | const [code, setCode] = useState(searchParams.code); 9 | const router = useRouter(); 10 | 11 | useEffect(() => { 12 | if (code) { 13 | const handleOAuth = async (code: string) => { 14 | const oauthResponse = await fetch('/api/graphql', { 15 | method: 'POST', 16 | headers: { 17 | 'Content-Type': 'application/json', 18 | }, 19 | body: JSON.stringify({ 20 | query: `mutation { 21 | postOAuth(code: "${code}", oauthType: "signin") { 22 | token 23 | } 24 | }`, 25 | }), 26 | }).then(res => res.json()); 27 | localStorage.setItem('token', oauthResponse.data.postOAuth.token); 28 | router.push('/'); 29 | }; 30 | handleOAuth(code); 31 | } 32 | }, [code]); 33 | 34 | if (code) { 35 | return ( 36 |
37 |

Authorizing OAuth

38 |
39 | ); 40 | } 41 | return ( 42 |
43 | 44 |
45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /app/(root)/Register/Register.tsx: -------------------------------------------------------------------------------- 1 | import { inputObj, userObj } from '../../(root)/frontendTypes'; 2 | import { motion } from 'framer-motion'; 3 | import Link from 'next/link'; 4 | import { useRouter } from 'next/navigation'; 5 | const randomstring = require('randomstring'); 6 | 7 | const Register = () => { 8 | const router = useRouter(); 9 | const REGISTER_STATE_CODE = 'cmVnaXN0ZXI'; 10 | 11 | const createUserHandler = async (e: any) => { 12 | e.preventDefault(); 13 | const userObj: userObj = { 14 | username: e.target.username.value, 15 | password: e.target.password.value, 16 | }; 17 | let data = await fetch('/api/graphql', { 18 | method: 'POST', 19 | headers: { 20 | 'Content-Type': 'application/json', 21 | }, 22 | body: JSON.stringify({ 23 | query: `mutation { 24 | newUser(username: "${userObj.username}", password: "${userObj.password}") { 25 | token 26 | } 27 | }`, 28 | }), 29 | }) 30 | .then(data => { 31 | return data.json(); 32 | }) 33 | .then(data => { 34 | if (data.data.newUser.token) { 35 | localStorage.setItem('token', data.data.newUser.token); 36 | router.push('/'); 37 | } else throw new Error(); 38 | }); 39 | }; 40 | 41 | return ( 42 |
43 |
44 | 50 |

Register with QuiL

51 |

52 | Registering with QuiL will grant you access to save your URI's and 53 | themes. Simply create your account by entering your desired username 54 | and password or register with you github account. 55 |

56 |
57 | 75 |
80 | 83 | 90 | 91 | 94 | 100 |
101 | 108 |
109 |
110 |

OR

111 | 122 | 129 | 130 |
131 |
132 |
133 | ); 134 | }; 135 | 136 | { 137 | /* */ 138 | } 139 | { 140 | /* */ 141 | } 142 | 143 | export default Register; 144 | 145 | /* 146 | mutation ($password: String, $username: String, $newUserUsername2: String, $newUserPassword2: String) newUser { 147 | newUser ($password: String, $username: String, $newUserUsername2: String, $newUserPassword2: String) { 148 | newUser(password: "${userObj.password}" username: "${userObj.username}") { 149 | token 150 | } 151 | } 152 | */ 153 | -------------------------------------------------------------------------------- /app/(root)/Register/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import Register from "./Register"; 3 | import { useRouter } from "next/navigation"; 4 | 5 | // const handleEmail = () => { 6 | // console.log(e.target.value, ' inside handle email'); 7 | // }; 8 | 9 | // const handleUsername = () => { 10 | // console.log(e.target.value, ' inside handle username'); 11 | // }; 12 | 13 | // const handlePassword = () => { 14 | // console.log(e.target.value, ' inside handle password'); 15 | // }; 16 | 17 | // const handleSubmit = () => { 18 | // console.log(e.target.value, ' handle the submit'); 19 | // }; 20 | 21 | export default function Page() { 22 | // const router = useRouter(); 23 | // router.prefetch(`/Main/Chart?URI=${uriParam}`) 24 | 25 | return ( 26 |
27 | 28 |
29 | ); 30 | } -------------------------------------------------------------------------------- /app/(root)/RootContainer.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import React, { useEffect, useState } from 'react'; 3 | import { useRouter } from 'next/navigation'; 4 | import { motion } from 'framer-motion'; 5 | import jwt_decode from 'jwt-decode'; 6 | import { toast, ToastContainer } from 'react-toastify'; 7 | import 'react-toastify/dist/ReactToastify.css'; 8 | import { decoded } from './frontendTypes'; 9 | 10 | const RootContainer = ({ 11 | authCode, 12 | stateCode, 13 | }: { 14 | authCode: string; 15 | stateCode: string; 16 | }) => { 17 | const [initialURI, setInitialURI] = useState(null); 18 | const [sampleURI, setSampleURI] = useState(null); 19 | const [code, setCode] = useState(authCode); 20 | const [stateString, setStateCode] = useState(stateCode); 21 | 22 | const [userJWT, setUserJWT] = useState(null); 23 | 24 | const router = useRouter(); 25 | 26 | const handleUserURI = (e: React.ChangeEvent): void => { 27 | let sanitize = e.target.value.trim(); 28 | setInitialURI(sanitize); 29 | }; 30 | const handleSampleURI = (e: React.ChangeEvent): void => { 31 | setSampleURI(e.target.value); 32 | }; 33 | let rootLoading: any; 34 | const sanitizeLaunch = (e: any) => { 35 | if (sampleURI || initialURI.includes('postgres')) { 36 | handleLaunch(e); 37 | rootLoading = toast.loading('Loading content..'); 38 | } else { 39 | toast.error('Not a valid PostgreSQL URL'); 40 | } 41 | }; 42 | 43 | const handleLaunch = (e: React.MouseEvent): void => { 44 | const URI = initialURI ? initialURI : sampleURI; 45 | router.push(`/Main/Chart?URI=${URI}`); 46 | toast.dismiss(rootLoading); 47 | }; 48 | useEffect(() => { 49 | const handleLogin = async (code: string) => { 50 | let currJWT = window.localStorage.getItem('token'); 51 | 52 | let oauthType; 53 | 54 | if (stateString) { 55 | if (stateString.includes('c2lnbmlu')) oauthType = 'signin'; 56 | if (stateString.includes('cmVnaXN0ZXI')) oauthType = 'register'; 57 | } 58 | 59 | if (code) { 60 | const queryValue = `mutation { 61 | postOAuth(code: "${code}", oauthType: "${oauthType}") { 62 | token 63 | } 64 | }`; 65 | 66 | const oauthResponse = await fetch('/api/graphql', { 67 | method: 'POST', 68 | headers: { 69 | 'Content-Type': 'application/json', 70 | }, 71 | body: JSON.stringify({ 72 | query: queryValue, 73 | }), 74 | }).then(res => res.json()); 75 | 76 | if (oauthResponse.data.postOAuth.token !== null) { 77 | localStorage.setItem('token', oauthResponse.data.postOAuth.token); 78 | } 79 | } 80 | 81 | currJWT = window.localStorage.getItem('token'); 82 | let decoded: decoded; 83 | 84 | if (currJWT || currJWT !== null) { 85 | decoded = jwt_decode(currJWT); 86 | } 87 | // if JWT doesnt exist, set userJWT to null 88 | if (!decoded) setUserJWT(null); 89 | // otherwise decode it and set userJWT object 90 | else setUserJWT(decoded); 91 | }; 92 | handleLogin(code); 93 | }, []); 94 | 95 | return ( 96 |
97 |
98 | {userJWT ? ( 99 | 107 | Welcome back 108 | {userJWT.username} 109 | 110 | ) : ( 111 | 112 | 120 | Welcome to 121 | 122 | 131 | QuiL 132 | 133 | 141 | QuiL is a developer tool used to visualize an existing relational 142 | database and generate the GraphQL schemas & resolvers for that 143 | data base. This is intended to help developers see how to 144 | transition to GraphQL from a traditional REST API architecture. 145 | 146 | 147 | )} 148 | 154 | 155 |
156 | 159 | 167 |
168 |
169 | 172 | 189 |
190 |
191 |
192 | 200 | 212 | {userJWT ? ( 213 |
214 | 223 |
224 | ) : ( 225 | <> 226 |
227 |

OR

228 |
229 | 237 | 244 |
245 |
246 | 247 | )} 248 |
249 |
250 |
251 |
252 | ); 253 | }; 254 | 255 | export default RootContainer; 256 | -------------------------------------------------------------------------------- /app/(root)/frontendTypes.tsx: -------------------------------------------------------------------------------- 1 | import { type } from 'os'; 2 | import { Node, Edge, NodeChange, EdgeChange } from 'reactflow'; 3 | import { StringMappingType } from 'typescript'; 4 | import { 5 | nodeShape, 6 | ResolverStrings, 7 | SingleSchemaType, 8 | } from '../../server/types'; 9 | 10 | export type DisplayContainerProps = { 11 | displayMode: string; 12 | userInputURI: (e: string) => void; 13 | uriLaunch: () => Promise; 14 | resQL: resQL; 15 | schemaGen: () => void; 16 | resolverGen: () => void; 17 | edges: Edge[]; 18 | nodes: Node[]; 19 | handleSetEdges: (cb: (eds: Edge[]) => Edge[]) => void; 20 | handleSetNodes: (cb: (nds: Node[]) => Node[]) => void; 21 | userJWT: any; 22 | userProjects: projectType[] | []; 23 | URI: string; 24 | removeDeletedProject: Function; 25 | }; 26 | 27 | export type NavigationBarProps = { 28 | userJWT: object | null; 29 | theme?: string; 30 | handleSetTheme?: (e: string) => void; 31 | aboutPageMode: () => void; 32 | mainPageMode: () => void; 33 | }; 34 | 35 | export type VisualizeSchemaResolverProps = { 36 | displayMode: string; 37 | resQL: resQL; 38 | }; 39 | 40 | export type SchemaProps = { 41 | resQL: resQL; 42 | }; 43 | 44 | export type ResolverProps = { 45 | resQL: resQL; 46 | }; 47 | 48 | export type VisualizeDBProps = { 49 | userInputURI: (e: string) => void; 50 | nodes: Node[]; 51 | edges: Edge[]; 52 | handleSetEdges: (cb: (eds: Edge[]) => Edge[]) => void; 53 | handleSetNodes: (cb: (nds: Node[]) => Node[]) => void; 54 | uriLaunch: () => Promise; 55 | }; 56 | 57 | export type ChartProps = { 58 | nodes: Node[]; 59 | edges: Edge[]; 60 | handleSetEdges: (cb: (eds: Edge[]) => Edge[]) => void; 61 | handleSetNodes: (cb: (nds: Node[]) => Node[]) => void; 62 | }; 63 | 64 | export type AboutPageProps = { 65 | theme: string; 66 | }; 67 | // handleSetNodes/handleSetEdges may need to change 68 | export type FlowProps = { 69 | nodes: Node[]; 70 | edges: Edge[]; 71 | handleSetEdges: (cb: (eds: Edge[]) => Edge[]) => void; 72 | handleSetNodes: (cb: (nds: Node[]) => Node[]) => void; 73 | }; 74 | 75 | export type resQL = { 76 | data: { getAllData: getAllData }; 77 | }; 78 | 79 | export type getAllData = { 80 | nodes: nodeShape[]; 81 | resolvers: ResolverStrings[]; 82 | schemas: SingleSchemaType[]; 83 | }; 84 | 85 | export type nodes = node[]; 86 | 87 | export type node = { 88 | name: string; 89 | primaryKey: string; 90 | columns: columns; 91 | edges: edge[]; 92 | }; 93 | 94 | export type edge = { 95 | fKey: string; 96 | refTable: string; 97 | }; 98 | export type columns = column[]; 99 | 100 | export type column = { 101 | columnName: string; 102 | dataType: string; 103 | }; 104 | 105 | export interface data { 106 | name: string; 107 | key: number; 108 | columns: columns; 109 | edges: edge[]; 110 | refTables: string[]; 111 | arrFKeys: string[]; 112 | } 113 | 114 | export type position = { 115 | x: number; 116 | y: number; 117 | }; 118 | 119 | export type MainContainerProps = { 120 | URI: string; 121 | initialNodes: Node[]; 122 | initialEdges: Edge[]; 123 | data: resQL; 124 | }; 125 | 126 | export type userObj = { 127 | [k: string]: string; 128 | }; 129 | 130 | export type inputObj = { 131 | name: string; 132 | }; 133 | 134 | export type loggedUser = { 135 | [k: string]: any; 136 | }; 137 | 138 | export type decoded = { 139 | [k: string]: any; 140 | }; 141 | 142 | export type projectType = { 143 | name: string; 144 | owner_id: string; 145 | saved_db: string[]; 146 | _id: string; 147 | }; 148 | -------------------------------------------------------------------------------- /app/(root)/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/(root)/layout.tsx: -------------------------------------------------------------------------------- 1 | import "./globals.css"; 2 | import React from "react"; 3 | export default function RootLayout({ 4 | children, 5 | }: { 6 | children: React.ReactNode; 7 | }) { 8 | return ( 9 | 10 | {children} 11 | 12 | ); 13 | } 14 | 15 | // layout page is where you would fetch data (pretty much acts as app.js where app.js is the parent component) 16 | // global components like NavBar or Footer would live here 17 | // layout page can nested. You can have multiple layouts of sub-directories that would only apply to the children components 18 | // about/ example.com/about 19 | // [slug]/ example.com/{slug} --> represents a dynamic route. [slug] acts as a wild card and usually contains things such as ids or username 20 | // (group)/ example.com(???) --> ignores 21 | -------------------------------------------------------------------------------- /app/(root)/page.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import RootContainer from './RootContainer'; 3 | 4 | export default function Page({ searchParams }: any) { 5 | return ( 6 | 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /app/Main/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/QuiL/8328a039999732b798888c264ee9d47efbe46f90/app/Main/.DS_Store -------------------------------------------------------------------------------- /app/Main/About/page.tsx: -------------------------------------------------------------------------------- 1 | import NavigationBar from '../Chart/(components)/NavigationBar'; 2 | 3 | export default function Page() { 4 | 5 | return ( 6 |
7 |

inside /ABOUT

8 |
9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /app/Main/Chart/(components)/AboutPage.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { motion } from "framer-motion"; 3 | import Image from "next/image"; 4 | const AboutPage = (): JSX.Element => { 5 | return ( 6 |
7 |
8 | 14 | 20 | Nice To Meet You! 21 | 22 | 28 | Here's How You Can Learn More 29 | 30 | 31 | 37 | About Us 38 | 39 |
40 |
41 | 47 | 52 | Brian img 59 | 60 |
61 |

62 | Brian

Tran 63 |

64 |

Software Engineer

65 |
66 | 72 | 73 | 79 | 80 | 81 | 82 | 83 | 89 | 90 | 96 | 97 | 98 | 99 | 100 |
101 |
102 |
103 | 109 | 114 | stephen img 121 | 122 |
123 |

Stephen Fitzsimmons

124 |

Software Engineer

125 |
126 | 132 | 133 | 139 | 140 | 141 | 142 | 143 | 149 | 150 | 156 | 157 | 158 | 159 | 160 |
161 |
162 |
163 | 169 | 174 | daniel img 181 | 182 |
183 |

184 | Daniel

An 185 |

186 |

Software Engineer

187 |
188 | 194 | 195 | 201 | 202 | 203 | 204 | 205 | 211 | 212 | 218 | 219 | 220 | 221 | 222 |
223 |
224 |
225 | 231 | 236 | andres img 243 | 244 |
245 |

Andres Jaramillo

246 |

Software Engineer

247 |
248 | 254 | 255 | 261 | 262 | 263 | 264 | 265 | 271 | 272 | 278 | 279 | 280 | 281 | 282 |
283 |
284 |
285 |
286 |
287 | ); 288 | }; 289 | 290 | export default AboutPage; 291 | -------------------------------------------------------------------------------- /app/Main/Chart/(components)/Chart.tsx: -------------------------------------------------------------------------------- 1 | import Flow from '../(flow)/Flow'; 2 | import React from 'react'; 3 | import { ChartProps } from '../../../(root)/frontendTypes'; 4 | 5 | const Chart = ({ 6 | nodes = null, 7 | edges = null, 8 | handleSetNodes, 9 | handleSetEdges, 10 | }: ChartProps): JSX.Element => { 11 | return ( 12 |
13 | 19 |
20 | ); 21 | }; 22 | 23 | export default Chart; 24 | // -------------------------------------------------------------------------------- /app/Main/Chart/(components)/DisplayContainer.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import React, { useState } from 'react'; 3 | import VisualizeDB from './VisualizeDB'; 4 | import VisualizeSchemaResolver from './VisualizeSchemaResolver'; 5 | import { DisplayContainerProps } from '../../../(root)/frontendTypes'; 6 | import { motion } from 'framer-motion'; 7 | import SaveContainer from './SaveContainer'; 8 | import LoadContainer from './LoadContainer'; 9 | import LoadItem from './LoadItem'; 10 | import AboutPage from './AboutPage'; 11 | import Link from 'next/link'; 12 | 13 | const DisplayContainer = ({ 14 | displayMode, 15 | userInputURI, 16 | uriLaunch, 17 | resQL, 18 | schemaGen, 19 | resolverGen, 20 | edges, 21 | nodes, 22 | handleSetEdges, 23 | handleSetNodes, 24 | userJWT, 25 | userProjects, 26 | URI, 27 | removeDeletedProject, 28 | }: DisplayContainerProps): JSX.Element => { 29 | const [saveModalVisible, setSaveModalVisible] = useState(true); 30 | const [loadModalVisible, setLoadModalVisible] = useState(true); 31 | 32 | let schemaTabStyle = 'tab tab-bordered'; 33 | let resolverTabStyle = 'tab tab-bordered'; 34 | switch (displayMode) { 35 | case 'schemaMode': 36 | schemaTabStyle = 'tab tab-bordered tab-active'; 37 | break; 38 | case 'resolverMode': 39 | resolverTabStyle = 'tab tab-bordered tab-active'; 40 | break; 41 | } 42 | 43 | const saveURIHandler = async (e: any) => { 44 | e.preventDefault(); 45 | let data = await fetch('/api/graphql', { 46 | method: 'POST', 47 | headers: { 48 | 'Content-Type': 'application/json', 49 | }, 50 | body: JSON.stringify({ 51 | query: `mutation { 52 | saveData(projectName: "${e.target.URInickname.value}", projectData: "${e.target.URIstring.value}", userId: ${userJWT.userId}) { 53 | projectId 54 | projectName 55 | success 56 | } 57 | }`, 58 | }), 59 | }) 60 | .then((data) => { 61 | return data.json(); 62 | }) 63 | .then((data) => { 64 | setSaveModalVisible(false); 65 | }); 66 | }; 67 | 68 | const setLoadVisibility = () => setLoadModalVisible(false); 69 | 70 | const LoadComponents = []; 71 | for (let i = 0; i < userProjects.length; i++) { 72 | LoadComponents.push(); 73 | } 74 | 75 | if (displayMode === 'aboutPage') { 76 | return ; 77 | } else 78 | return ( 79 |
80 |
81 | 82 |
83 |
84 | 92 | View Schemas/Resolvers 93 | 94 | 100 | userInputURI(e.target.value)} 103 | className="input input-sm input-bordered w-full mx-1" 104 | placeholder="insert URI" 105 | data-cy="insert-uri-main" 106 | > 107 | 108 | 116 | {/* Save Button and Modal */} 117 | 124 | 125 | {saveModalVisible && ( 126 | <> 127 |
128 | 133 |
134 |
135 | 141 | 142 | {/* MAKE CONDITIONAL*/} 143 | {userJWT ? ( 144 | <> 145 |

146 | Save Your Database 147 |

148 |
saveURIHandler(e)}> 149 | 152 | 158 | 159 | 162 | 169 | 170 |
171 | 179 |
180 |
181 | 182 | ) : ( 183 | <> 184 |

191 | Please login to save your project!! 192 |

193 |
199 | 200 | 208 | 209 | 210 | 213 | 214 |
215 | 216 | )} 217 | {/* MAKE CONDITIONAL*/} 218 |
219 |
220 |
221 | 222 | )} 223 | {/* Load Button and Modal */} 224 | 225 | 232 | {loadModalVisible && ( 233 |
234 | 239 |
240 |
241 | 247 | {userJWT ? ( 248 |
249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | {userProjects.map((e: any, i: any) => ( 258 | 267 | ))} 268 | 269 |
Project Name
270 |
271 | ) : ( 272 | <> 273 |

280 | Please login to load your project!! 281 |

282 |
288 | 289 | 297 | 298 | 299 | 302 | 303 |
304 | 305 | )} 306 |
307 |
308 |
309 | )} 310 |
311 |
312 | 320 |
321 |
322 | 323 |
324 |
    325 |
  • schemaGen()}> 326 | Schemas 327 |
  • 328 |
  • resolverGen()}> 329 | Resolvers 330 |
  • 331 |
332 |
    333 |
  • 334 |
    335 | 339 |
    340 |
  • 341 |
342 |
343 |
344 |
345 |
346 | ); 347 | }; 348 | 349 | export default DisplayContainer; 350 | -------------------------------------------------------------------------------- /app/Main/Chart/(components)/LoadContainer.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | function LoadContainer() { 4 | return ( 5 |
6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
URI NicknameURI
URI NicknameURI
URI NicknameURI
26 |
27 | ); 28 | } 29 | 30 | export default LoadContainer; 31 | -------------------------------------------------------------------------------- /app/Main/Chart/(components)/LoadItem.tsx: -------------------------------------------------------------------------------- 1 | import { any } from 'cypress/types/bluebird'; 2 | import React, { useState } from 'react'; 3 | import { projectType } from '../../../(root)/frontendTypes'; 4 | 5 | type LoadItemProps = { 6 | userProject: projectType; 7 | key: string; 8 | id: string; 9 | uriLaunch: Function; 10 | setLoadVisibility: Function; 11 | setLoadModalVisible: Function; 12 | removeDeletedProject: Function; 13 | }; 14 | 15 | function LoadItem({ 16 | userProject, 17 | key, 18 | id, 19 | uriLaunch, 20 | setLoadVisibility, 21 | setLoadModalVisible, 22 | removeDeletedProject, 23 | }: LoadItemProps) { 24 | const [projectVisibility, setProjectVisibility] = useState(true); 25 | const remove = (el: any) => { 26 | let element = el; 27 | element.remove(); 28 | }; 29 | 30 | const deleteURIHandler = async (id: any): Promise => { 31 | let data = await fetch('/api/graphql', { 32 | method: 'POST', 33 | headers: { 34 | 'Content-Type': 'application/json', 35 | }, 36 | body: JSON.stringify({ 37 | query: `mutation { 38 | deleteProject(projectId: ${id}) { 39 | deleted 40 | } 41 | } 42 | `, 43 | }), 44 | }).then(data => { 45 | return data.json(); 46 | }); 47 | setProjectVisibility(false); 48 | }; 49 | 50 | const handleLoadClick = (e: any, uri: any) => { 51 | setLoadModalVisible(false); 52 | uriLaunch(e, uri); 53 | }; 54 | 55 | return ( 56 | <> 57 | {projectVisibility && ( 58 | 59 | {key} 60 | {userProject.name} 61 | 62 | 69 | 70 | 71 | 80 | 81 | 82 | )} 83 | 84 | ); 85 | } 86 | 87 | export default LoadItem; 88 | -------------------------------------------------------------------------------- /app/Main/Chart/(components)/MainContainer.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | // postgres://lkdxllvk:GTIkPygxpPOx0ZVNJ3luQHEfApEIJekP@heffalump.db.elephantsql.com/lkdxllvk 3 | import React, { useEffect, useState } from 'react'; 4 | import DisplayContainer from './DisplayContainer'; 5 | import { Node, Edge } from 'reactflow'; 6 | import createNodes from '../(flow)/Nodes'; 7 | import createEdges from '../(flow)/Edges'; 8 | import NavigationBar from './NavigationBar'; 9 | import jwt_decode from 'jwt-decode'; 10 | import { motion } from 'framer-motion'; 11 | import { toast, ToastContainer } from 'react-toastify'; 12 | import { 13 | MainContainerProps, 14 | projectType, 15 | resQL, 16 | } from '../../../(root)/frontendTypes'; 17 | 18 | import 'react-toastify/dist/ReactToastify.css'; 19 | const MainContainer = ({ 20 | URI, 21 | initialNodes, 22 | initialEdges, 23 | data, 24 | }: MainContainerProps): JSX.Element => { 25 | const [displayMode, setDisplayMode] = useState('schemaMode'); 26 | const [uri, setURI] = useState(''); 27 | const [resQL, setResQL] = useState(data); 28 | const [nodes, setNodes] = useState(initialNodes); 29 | const [edges, setEdges] = useState(initialEdges); 30 | const [theme, setTheme] = useState('night'); 31 | const [userJWT, setUserJWT] = useState(); 32 | const [userProjects, setUserProjects] = useState([]); 33 | const [toastTheme, setToastTheme] = useState<'light' | 'dark' | 'colored'>( 34 | 'dark' 35 | ); 36 | useEffect(() => { 37 | try { 38 | const getUserProjects = async (): Promise => { 39 | let currJWT = window.localStorage.getItem('token'); 40 | let decoded: any; 41 | if (currJWT) { 42 | decoded = await jwt_decode(currJWT); 43 | setUserJWT(decoded); 44 | } 45 | // if JWT doesnt exist, set userJWT to null 46 | if (!decoded) setUserJWT(null); 47 | // otherwise decode it and set userJWT object 48 | if (currJWT) { 49 | let data = await fetch('/api/graphql', { 50 | method: 'POST', 51 | headers: { 52 | 'Content-Type': 'application/json', 53 | }, 54 | body: JSON.stringify({ 55 | query: `query { 56 | getUserProjects(userId: ${decoded.userId}) { 57 | db { 58 | name 59 | owner_id 60 | saved_db 61 | _id 62 | } 63 | success 64 | } 65 | }`, 66 | }), 67 | }) 68 | .then((data) => { 69 | return data.json(); 70 | }) 71 | .then((data) => { 72 | setUserProjects(data.data.getUserProjects.db); 73 | }); 74 | } 75 | }; 76 | getUserProjects(); 77 | } catch (error) {} 78 | }, []); 79 | 80 | const removeDeletedProject = (id: any) => { 81 | setUserProjects((oldState) => { 82 | return oldState.filter((e: any) => e._id === id); 83 | }); 84 | }; 85 | 86 | //invoked in VisualizeSchemaResolver 87 | // Schema Mode is to display the Schemas (drawer) generated 88 | const schemaGen = (): void => { 89 | setDisplayMode('schemaMode'); 90 | }; 91 | //invoked in VisualizeSchemaResolver 92 | // Resolver Mode is to display the Resolvers (drawer) generated 93 | const resolverGen = (): void => { 94 | setDisplayMode('resolverMode'); 95 | }; 96 | 97 | const aboutPageMode = (): void => { 98 | setDisplayMode('aboutPage'); 99 | }; 100 | 101 | const mainPageMode = (): void => { 102 | setDisplayMode('mainPage'); 103 | }; 104 | 105 | //invoked in visualizeDB. 106 | // Checks for error in the users before invoking the fetch 107 | const uriLaunch = async (): Promise => { 108 | // e.preventDefault(); 109 | if (uri.includes('postgres')) { 110 | launchUri(uri); 111 | } else { 112 | toast.error('Not a valid PostgreSQL URL'); 113 | } 114 | }; 115 | 116 | const launchUri = async (loadedUri: string): Promise => { 117 | const toastLoading = toast.loading('loading content'); 118 | let launchURI = loadedUri || uri; 119 | let data = await fetch('/api/graphql', { 120 | method: 'POST', 121 | headers: { 122 | 'Content-Type': 'application/json', 123 | }, 124 | 125 | body: JSON.stringify({ 126 | query: `query GetData { 127 | getAllData(uri: "${launchURI}") { 128 | nodes { 129 | name, 130 | primaryKey, 131 | columns { 132 | columnName, 133 | dataType 134 | }, 135 | edges { 136 | fKey, 137 | refTable 138 | } 139 | }, 140 | resolvers { 141 | tableName, 142 | resolver 143 | }, 144 | schemas { 145 | tableName, 146 | schemas 147 | } 148 | } 149 | }`, 150 | }), 151 | }); 152 | let res = await data.json(); 153 | toast.dismiss(toastLoading); 154 | if ( 155 | res.data.getAllData.nodes.length === 0 && 156 | res.data.getAllData.resolvers.length === 0 && 157 | res.data.getAllData.schemas.length === 0 158 | ) { 159 | toast.error('Empty database or bad URL'); 160 | } 161 | setResQL(res); 162 | setNodes(createNodes(res)); 163 | setEdges(createEdges(res)); 164 | }; 165 | 166 | // handleSetNodes takes in a callback (cb). That callback takes in 167 | const handleSetNodes = (cb: (nds: Node[]) => Node[]): void => { 168 | setNodes(cb); 169 | }; 170 | const handleSetEdges = (): void => { 171 | setEdges(edges); 172 | }; 173 | 174 | // invoked inside visualizeDB. users input (uri) 175 | const userInputURI = (e: string): void => { 176 | let sanitize = e.trim(); 177 | setURI(sanitize); 178 | }; 179 | 180 | // changing the themes for Toast(notifications) and Tailwind/app 181 | const handleSetTheme = (value: any): void => { 182 | setTheme(value); 183 | if (theme !== 'light' && theme !== 'night') { 184 | setToastTheme('colored'); 185 | } else setToastTheme('light'); 186 | }; 187 | 188 | return ( 189 |
190 | 195 | 201 | 202 | 214 | 230 |
231 | ); 232 | }; 233 | 234 | export default MainContainer; 235 | -------------------------------------------------------------------------------- /app/Main/Chart/(components)/NavigationBar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import Link from "next/link"; 3 | import { useRouter } from "next/navigation"; 4 | import React, { useState } from "react"; 5 | import { useSearchParams } from "next/navigation"; 6 | import { NavigationBarProps } from "../../../(root)/frontendTypes"; 7 | import test from "node:test"; 8 | import { setSyntheticLeadingComments } from "typescript"; 9 | import quil from "./quil.png"; 10 | import { motion } from "framer-motion"; 11 | import Image from "next/image"; 12 | const NavigationBar = ({ 13 | userJWT, 14 | handleSetTheme, 15 | aboutPageMode, 16 | mainPageMode, 17 | }: NavigationBarProps): JSX.Element => { 18 | const router = useRouter(); 19 | const searchParams = useSearchParams(); 20 | const URIfromRoot = searchParams.get("URI"); 21 | const [uriParam, setUriParam] = useState(URIfromRoot); 22 | 23 | return ( 24 | 29 |
30 | 31 | mainPageMode()} 34 | > 35 | 36 | 37 | 38 |
39 |
40 | mainPageMode()} 44 | > 45 | Main 46 | 47 | {userJWT ? ( 48 | { 52 | window.localStorage.removeItem("token"); 53 | window.location.reload(); 54 | }} 55 | > 56 | Log Out 57 | 58 | ) : ( 59 | <> 60 | { 64 | router.push("/Login"); 65 | }} 66 | > 67 | Login 68 | 69 | router.push("/Register")} 73 | > 74 | Register 75 | 76 | 77 | )} 78 | aboutPageMode()} 82 | > 83 | About 84 | 85 | handleSetTheme(e.target.value)} 88 | className='select bg-neutral-content w-1/3 max-w-xs text-base-300 mr-9' 89 | > 90 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 |
101 |
102 |
103 |
104 | ); 105 | }; 106 | 107 | export default NavigationBar; 108 | -------------------------------------------------------------------------------- /app/Main/Chart/(components)/Resolver.tsx: -------------------------------------------------------------------------------- 1 | import { ResolverProps } from '../../../(root)/frontendTypes'; 2 | import React, { useState } from 'react'; 3 | import { Card } from './ResolverDisplay'; 4 | import { ResolverStrings } from '../../../../server/types'; 5 | 6 | const Resolver = ({ resQL }: ResolverProps): JSX.Element => { 7 | const [copyStatus, setCopyStatus] = useState('Copy'); 8 | if (Object.keys(resQL).length === 0) return; 9 | const { resolvers } = resQL.data.getAllData; 10 | 11 | const onClick = () => { 12 | const allResolvers = resolvers.reduce((all, curr) => { 13 | return all + curr.resolver; 14 | }, ''); 15 | 16 | const formatted = `Query {` + allResolvers + `\n }`; 17 | navigator.clipboard.writeText(formatted); 18 | setCopyStatus('Copied!'); 19 | setTimeout(() => { 20 | setCopyStatus('Copy'); 21 | }, 5000); 22 | }; 23 | 24 | return ( 25 |
26 |
27 |
28 | 31 |
32 |
33 | {resolvers.map((e: ResolverStrings) => ( 34 | 38 | ))} 39 |
40 | ); 41 | }; 42 | 43 | export default Resolver; 44 | -------------------------------------------------------------------------------- /app/Main/Chart/(components)/ResolverDisplay.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { CopyTwoTone } from "@ant-design/icons"; 3 | import { javascript } from "@codemirror/lang-javascript"; 4 | import { dracula } from "@uiw/codemirror-theme-dracula"; 5 | import ReactCodeMirror from "@uiw/react-codemirror"; 6 | import React, { useState } from "react"; 7 | import { motion } from "framer-motion"; 8 | 9 | type CardProps = { 10 | tableName: string; 11 | value: any; 12 | }; 13 | 14 | type ResolverMirrorProps = { 15 | value: any; 16 | }; 17 | 18 | export const ResolverMirror = ({ value }: ResolverMirrorProps) => { 19 | return ( 20 |
21 | 28 |
29 | ); 30 | }; 31 | 32 | export const Card = ({ value, tableName }: CardProps) => { 33 | const [copyStatus, setCopyStatus] = useState("Copy"); 34 | 35 | const onClick = () => { 36 | navigator.clipboard.writeText(value); 37 | setCopyStatus("Copied!"); 38 | setTimeout(() => { 39 | setCopyStatus("Copy"); 40 | }, 5000); 41 | }; 42 | return ( 43 |
44 |
45 |
46 | 51 |
52 |

53 | {tableName[0].toUpperCase() + 54 | tableName.substring(1, tableName.length)} 55 |

56 |
57 | 60 |
61 |
62 | 63 |
64 |
65 |
66 |
67 | ); 68 | }; 69 | -------------------------------------------------------------------------------- /app/Main/Chart/(components)/SaveContainer.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | function SaveContainer() { 4 | return ( 5 |
6 | {/* The button to open modal */} 7 | 10 | {/* Put this part before tag */} 11 | 12 |
13 |
14 | 20 |

21 | Congratulations random Internet user! 22 |

23 |
24 |
25 |
26 | ); 27 | } 28 | 29 | export default SaveContainer; 30 | 31 |
32 |
33 |
34 | 37 | 43 | 46 | 52 |
53 |
54 | 55 |
56 |
57 |
; 58 | -------------------------------------------------------------------------------- /app/Main/Chart/(components)/Schema.tsx: -------------------------------------------------------------------------------- 1 | import { SchemaProps } from '../../../(root)/frontendTypes'; 2 | import React, { useState } from 'react'; 3 | import { Card } from './ResolverDisplay'; 4 | import { SingleSchemaType } from '../../../../server/types'; 5 | 6 | const Schema = ({ resQL }: SchemaProps): JSX.Element => { 7 | const [copyStatus, setCopyStatus] = useState('Copy'); 8 | 9 | 10 | if (Object.keys(resQL).length === 0) return; 11 | 12 | const { schemas } = resQL.data.getAllData; 13 | const onClick = () => { 14 | // const allResolvers = resolvers.reduce((all, curr) => { 15 | // return all + curr.resolver; 16 | // }, ''); 17 | 18 | // const formatted = `Query {` + allResolvers + `\n }`; 19 | navigator.clipboard.writeText( 20 | schemas.reduce((a, b: SingleSchemaType) => a + b.schemas, '') 21 | ); 22 | setCopyStatus('Copied!'); 23 | setTimeout(() => { 24 | setCopyStatus('Copy'); 25 | }, 5000); 26 | }; 27 | 28 | return ( 29 |
30 |
31 |
32 | 35 |
36 |
37 | {schemas.map((e: SingleSchemaType) => ( 38 | 39 | ))} 40 |
41 | ); 42 | }; 43 | 44 | export default Schema; 45 | -------------------------------------------------------------------------------- /app/Main/Chart/(components)/VisualizeDB.tsx: -------------------------------------------------------------------------------- 1 | import { VisualizeDBProps } from '../../../(root)/frontendTypes'; 2 | import Chart from './Chart'; 3 | import React from 'react'; 4 | import { motion } from 'framer-motion'; 5 | const VisualizeDB = ({ 6 | nodes, 7 | edges, 8 | handleSetEdges, 9 | handleSetNodes, 10 | }: VisualizeDBProps): JSX.Element => { 11 | return ( 12 | <> 13 | 19 | 25 | 26 | 27 | ); 28 | }; 29 | 30 | export default VisualizeDB; 31 | -------------------------------------------------------------------------------- /app/Main/Chart/(components)/VisualizeSchemaResolver.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import React, { useState } from 'react'; 3 | import Schema from './Schema'; 4 | import Resolver from './Resolver'; 5 | import { VisualizeSchemaResolverProps } from '../../../(root)/frontendTypes'; 6 | const VisualizeSchemaResolver = ({ 7 | displayMode, 8 | resQL, 9 | }: VisualizeSchemaResolverProps): JSX.Element => { 10 | let modeComponent; 11 | switch (displayMode) { 12 | case 'schemaMode': 13 | modeComponent = ; 14 | 15 | break; 16 | case 'resolverMode': 17 | modeComponent = ; 18 | break; 19 | default: 20 | modeComponent = null; 21 | } 22 | return
{modeComponent}
; 23 | }; 24 | export default VisualizeSchemaResolver; 25 | -------------------------------------------------------------------------------- /app/Main/Chart/(flow)/Edges.tsx: -------------------------------------------------------------------------------- 1 | import { Edge, StepEdge, StraightEdge } from 'reactflow'; 2 | import { edge, resQL } from '../../../(root)/frontendTypes'; 3 | 4 | const createEdges = (res: resQL): Edge[] => { 5 | if (!res.data.getAllData.nodes) return []; 6 | // edges array to be populated and sent to Flow to render 7 | const edges: Edge[] = []; 8 | // array of nodes from response 9 | const nodes = res.data.getAllData.nodes; 10 | // loop through each node from response 11 | nodes.forEach((node, i) => { 12 | // check to see if each node has any edges 13 | if (node.edges.length !== 0) { 14 | // if so, loop through edges of current node 15 | node.edges.forEach(edge => { 16 | const newEdge = { 17 | id: `${node.name}-${edge.refTable}`, 18 | animated: true, 19 | style: { stroke: 'hsl(var(--sc))', strokeWidth: 3 }, 20 | source: node.name, 21 | type: 'default', 22 | sourceHandle: edge.fKey, 23 | targetHandle: edge.refTable, 24 | target: edge.refTable, 25 | }; 26 | edges.push(newEdge); 27 | }); 28 | } 29 | }); 30 | return edges; 31 | }; 32 | 33 | export default createEdges; 34 | -------------------------------------------------------------------------------- /app/Main/Chart/(flow)/Flow.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import React, { useCallback } from 'react'; 3 | import ReactFlow, { 4 | Controls, 5 | Background, 6 | applyEdgeChanges, 7 | applyNodeChanges, 8 | NodeChange, // TS Generic 9 | EdgeChange, // TS Generic 10 | Connection, // TS Generic 11 | addEdge, 12 | } from 'reactflow'; 13 | import TableNode from './TableNode'; 14 | import 'reactflow/dist/style.css'; 15 | import { FlowProps } from '../../../(root)/frontendTypes'; 16 | import { motion } from 'framer-motion'; 17 | 18 | const nodeTypes = { tableNode: TableNode }; 19 | 20 | const Flow = ({ nodes, edges, handleSetNodes, handleSetEdges }: FlowProps) => { 21 | const onNodesChange = useCallback( 22 | (changes: NodeChange[]) => 23 | handleSetNodes((nds) => applyNodeChanges(changes, nds)), 24 | [handleSetNodes] 25 | ); 26 | 27 | const onEdgesChange = useCallback( 28 | (changes: EdgeChange[]) => 29 | handleSetEdges((eds) => applyEdgeChanges(changes, eds)), 30 | [handleSetEdges] 31 | ); 32 | 33 | const onConnect = useCallback( 34 | (connection: Connection) => 35 | handleSetEdges((eds) => addEdge(connection, eds)), 36 | [handleSetEdges] 37 | ); 38 | 39 | return ( 40 |
41 | 50 | 51 | 52 | 53 |
54 | ); 55 | }; 56 | 57 | export default Flow; 58 | -------------------------------------------------------------------------------- /app/Main/Chart/(flow)/Nodes.tsx: -------------------------------------------------------------------------------- 1 | import { Node } from 'reactflow'; 2 | import { position, resQL } from '../../../(root)/frontendTypes'; 3 | 4 | const createNodes = (res: resQL): Node[] => { 5 | if (!res.data.getAllData.nodes) return []; 6 | const positions: position[] = [ 7 | { x: 0, y: 0 }, 8 | { x: 500, y: 0 }, 9 | { x: 0, y: 350 }, 10 | { x: 500, y: 350 }, 11 | { x: 0, y: 700 }, 12 | { x: 500, y: 700 }, 13 | { x: 0, y: 1050 }, 14 | { x: 500, y: 1050 }, 15 | { x: 0, y: 1400 }, 16 | { x: 500, y: 1400 }, 17 | { x: 0, y: 1750 }, 18 | { x: 500, y: 1750 }, 19 | { x: 0, y: 2100 }, 20 | { x: 500, y: 2100 }, 21 | { x: 0, y: 2450 }, 22 | { x: 500, y: 2450 }, 23 | { x: 0, y: 2450 }, 24 | ]; 25 | // pass down an array of FKeys so that each row can check to see their dataType is a foreign key, 26 | // in 27 | const arrFKeys: string[] = []; 28 | // array of tableNames that needs a handle. 'refTables will be passed down to each TableNode to 29 | // determine whether the TableNode needs a target 30 | const refTables: string[] = []; 31 | // array of nodes from response 32 | const resNodes = res.data.getAllData.nodes; 33 | // map through the nodes and create template for each node 34 | const nodes: Node[] = resNodes.map((node, i) => { 35 | // check to see if current node has any edges 36 | if (node.edges.length !== 0) { 37 | // if so, loop through edges and push the name refTable to 'refTables' 38 | node.edges.forEach(edge => { 39 | refTables.push(edge.refTable); 40 | arrFKeys.push(edge.fKey); 41 | }); 42 | } 43 | return { 44 | id: node.name, 45 | type: 'tableNode', 46 | position: positions[i], 47 | data: { 48 | name: node.name, 49 | key: i, 50 | columns: node.columns, 51 | edges: node.edges, 52 | refTables: refTables, 53 | arrFKeys: arrFKeys, 54 | }, 55 | }; 56 | }); 57 | return nodes; 58 | }; 59 | 60 | export default createNodes; 61 | -------------------------------------------------------------------------------- /app/Main/Chart/(flow)/TableNode.tsx: -------------------------------------------------------------------------------- 1 | import { Handle, Position } from 'reactflow'; 2 | import TableRow from './TableRow'; 3 | import React from 'react'; 4 | import { motion } from 'framer-motion'; 5 | 6 | const TableNode = ({ data }: any) => { 7 | const handles: JSX.Element[] = []; 8 | const arrOfFKeys = []; 9 | let pixels = 130; 10 | const tableFields = data.columns.map((column: any) => { 11 | // if the columnName is a foreign key, give it a source handle at the correct key 12 | if (data.arrFKeys.includes(column.columnName)) { 13 | const handleStyle = { 14 | top: `${pixels.toString()}px`, 15 | bottom: 'auto', 16 | height: '8px', 17 | width: '8px', 18 | }; 19 | pixels += 42; 20 | handles.push( 21 | 27 | ); 28 | } 29 | // if current Node is a RefTable, give it a handle on the top right of the Table. (can possibly change to Left of table at primary key) 30 | if (data.refTables.includes(data.name)) { 31 | handles.push( 32 | 38 | ); 39 | } 40 | return ( 41 | 42 | ); 43 | }); 44 | 45 | return ( 46 | 47 | {handles} 48 |
49 | {data.name} 50 |
51 | 52 | 53 | 54 | 55 | 56 | {tableFields} 57 |
ColumnType
58 |
59 | ); 60 | }; 61 | 62 | export default TableNode; 63 | -------------------------------------------------------------------------------- /app/Main/Chart/(flow)/TableRow.tsx: -------------------------------------------------------------------------------- 1 | import { Handle, Position } from 'reactflow'; 2 | import React from 'react'; 3 | const TableRow = ({ columnName, dataType }: any) => { 4 | return ( 5 | <> 6 | 7 | 8 | {columnName} 9 | 10 | {dataType} 11 | 12 | 13 | ); 14 | }; 15 | 16 | export default TableRow; 17 | -------------------------------------------------------------------------------- /app/Main/Chart/(flow)/dummyRes.tsx: -------------------------------------------------------------------------------- 1 | // starwars dummy data 2 | 3 | import { resQL } from '../../../(root)/frontendTypes'; 4 | 5 | const res: resQL = { 6 | data: { 7 | getAllData: { 8 | nodes: [ 9 | { 10 | name: 'species', 11 | primaryKey: '_id', 12 | columns: [ 13 | { 14 | columnName: '_id', 15 | dataType: 'integer', 16 | }, 17 | { 18 | columnName: 'homeworld_id', 19 | dataType: 'bigint', 20 | }, 21 | { 22 | columnName: 'classification', 23 | dataType: 'character varying', 24 | }, 25 | { 26 | columnName: 'average_height', 27 | dataType: 'character varying', 28 | }, 29 | { 30 | columnName: 'average_lifespan', 31 | dataType: 'character varying', 32 | }, 33 | { 34 | columnName: 'hair_colors', 35 | dataType: 'character varying', 36 | }, 37 | { 38 | columnName: 'skin_colors', 39 | dataType: 'character varying', 40 | }, 41 | { 42 | columnName: 'eye_colors', 43 | dataType: 'character varying', 44 | }, 45 | { 46 | columnName: 'language', 47 | dataType: 'character varying', 48 | }, 49 | { 50 | columnName: 'name', 51 | dataType: 'character varying', 52 | }, 53 | ], 54 | edges: [ 55 | { 56 | fKey: 'homeworld_id', 57 | refTable: 'planets', 58 | }, 59 | ], 60 | isIntersectionTable: false, 61 | }, 62 | { 63 | name: 'people', 64 | primaryKey: '_id', 65 | columns: [ 66 | { 67 | columnName: '_id', 68 | dataType: 'integer', 69 | }, 70 | { 71 | columnName: 'species_id', 72 | dataType: 'bigint', 73 | }, 74 | { 75 | columnName: 'homeworld_id', 76 | dataType: 'bigint', 77 | }, 78 | { 79 | columnName: 'height', 80 | dataType: 'integer', 81 | }, 82 | { 83 | columnName: 'skin_color', 84 | dataType: 'character varying', 85 | }, 86 | { 87 | columnName: 'eye_color', 88 | dataType: 'character varying', 89 | }, 90 | { 91 | columnName: 'birth_year', 92 | dataType: 'character varying', 93 | }, 94 | { 95 | columnName: 'gender', 96 | dataType: 'character varying', 97 | }, 98 | { 99 | columnName: 'name', 100 | dataType: 'character varying', 101 | }, 102 | { 103 | columnName: 'mass', 104 | dataType: 'character varying', 105 | }, 106 | { 107 | columnName: 'hair_color', 108 | dataType: 'character varying', 109 | }, 110 | ], 111 | edges: [ 112 | { 113 | fKey: 'species_id', 114 | refTable: 'species', 115 | }, 116 | { 117 | fKey: 'homeworld_id', 118 | refTable: 'planets', 119 | }, 120 | ], 121 | isIntersectionTable: false, 122 | }, 123 | { 124 | name: 'planets', 125 | primaryKey: '_id', 126 | columns: [ 127 | { 128 | columnName: '_id', 129 | dataType: 'integer', 130 | }, 131 | { 132 | columnName: 'rotation_period', 133 | dataType: 'integer', 134 | }, 135 | { 136 | columnName: 'orbital_period', 137 | dataType: 'integer', 138 | }, 139 | { 140 | columnName: 'diameter', 141 | dataType: 'integer', 142 | }, 143 | { 144 | columnName: 'population', 145 | dataType: 'bigint', 146 | }, 147 | { 148 | columnName: 'climate', 149 | dataType: 'character varying', 150 | }, 151 | { 152 | columnName: 'name', 153 | dataType: 'character varying', 154 | }, 155 | { 156 | columnName: 'gravity', 157 | dataType: 'character varying', 158 | }, 159 | { 160 | columnName: 'terrain', 161 | dataType: 'character varying', 162 | }, 163 | { 164 | columnName: 'surface_water', 165 | dataType: 'character varying', 166 | }, 167 | ], 168 | edges: [], 169 | isIntersectionTable: false, 170 | }, 171 | { 172 | name: 'people_in_films', 173 | primaryKey: '_id', 174 | columns: [ 175 | { 176 | columnName: '_id', 177 | dataType: 'integer', 178 | }, 179 | { 180 | columnName: 'person_id', 181 | dataType: 'bigint', 182 | }, 183 | { 184 | columnName: 'film_id', 185 | dataType: 'bigint', 186 | }, 187 | ], 188 | edges: [ 189 | { 190 | fKey: 'person_id', 191 | refTable: 'people', 192 | }, 193 | { 194 | fKey: 'film_id', 195 | refTable: 'films', 196 | }, 197 | ], 198 | isIntersectionTable: true, 199 | }, 200 | { 201 | name: 'films', 202 | primaryKey: '_id', 203 | columns: [ 204 | { 205 | columnName: 'episode_id', 206 | dataType: 'integer', 207 | }, 208 | { 209 | columnName: '_id', 210 | dataType: 'integer', 211 | }, 212 | { 213 | columnName: 'release_date', 214 | dataType: 'date', 215 | }, 216 | { 217 | columnName: 'producer', 218 | dataType: 'character varying', 219 | }, 220 | { 221 | columnName: 'opening_crawl', 222 | dataType: 'character varying', 223 | }, 224 | { 225 | columnName: 'title', 226 | dataType: 'character varying', 227 | }, 228 | { 229 | columnName: 'director', 230 | dataType: 'character varying', 231 | }, 232 | ], 233 | edges: [], 234 | isIntersectionTable: false, 235 | }, 236 | { 237 | name: 'species_in_films', 238 | primaryKey: '_id', 239 | columns: [ 240 | { 241 | columnName: '_id', 242 | dataType: 'integer', 243 | }, 244 | { 245 | columnName: 'film_id', 246 | dataType: 'bigint', 247 | }, 248 | { 249 | columnName: 'species_id', 250 | dataType: 'bigint', 251 | }, 252 | ], 253 | edges: [ 254 | { 255 | fKey: 'film_id', 256 | refTable: 'films', 257 | }, 258 | { 259 | fKey: 'species_id', 260 | refTable: 'species', 261 | }, 262 | ], 263 | isIntersectionTable: true, 264 | }, 265 | { 266 | name: 'planets_in_films', 267 | primaryKey: '_id', 268 | columns: [ 269 | { 270 | columnName: '_id', 271 | dataType: 'integer', 272 | }, 273 | { 274 | columnName: 'film_id', 275 | dataType: 'bigint', 276 | }, 277 | { 278 | columnName: 'planet_id', 279 | dataType: 'bigint', 280 | }, 281 | ], 282 | edges: [ 283 | { 284 | fKey: 'film_id', 285 | refTable: 'films', 286 | }, 287 | { 288 | fKey: 'planet_id', 289 | refTable: 'planets', 290 | }, 291 | ], 292 | isIntersectionTable: true, 293 | }, 294 | { 295 | name: 'pilots', 296 | primaryKey: '_id', 297 | columns: [ 298 | { 299 | columnName: '_id', 300 | dataType: 'integer', 301 | }, 302 | { 303 | columnName: 'person_id', 304 | dataType: 'bigint', 305 | }, 306 | { 307 | columnName: 'vessel_id', 308 | dataType: 'bigint', 309 | }, 310 | ], 311 | edges: [ 312 | { 313 | fKey: 'vessel_id', 314 | refTable: 'vessels', 315 | }, 316 | { 317 | fKey: 'person_id', 318 | refTable: 'people', 319 | }, 320 | ], 321 | isIntersectionTable: true, 322 | }, 323 | { 324 | name: 'vessels', 325 | primaryKey: '_id', 326 | columns: [ 327 | { 328 | columnName: '_id', 329 | dataType: 'integer', 330 | }, 331 | { 332 | columnName: 'cost_in_credits', 333 | dataType: 'bigint', 334 | }, 335 | { 336 | columnName: 'crew', 337 | dataType: 'integer', 338 | }, 339 | { 340 | columnName: 'passengers', 341 | dataType: 'integer', 342 | }, 343 | { 344 | columnName: 'vessel_type', 345 | dataType: 'character varying', 346 | }, 347 | { 348 | columnName: 'vessel_class', 349 | dataType: 'character varying', 350 | }, 351 | { 352 | columnName: 'consumables', 353 | dataType: 'character varying', 354 | }, 355 | { 356 | columnName: 'length', 357 | dataType: 'character varying', 358 | }, 359 | { 360 | columnName: 'max_atmosphering_speed', 361 | dataType: 'character varying', 362 | }, 363 | { 364 | columnName: 'cargo_capacity', 365 | dataType: 'character varying', 366 | }, 367 | { 368 | columnName: 'name', 369 | dataType: 'character varying', 370 | }, 371 | { 372 | columnName: 'manufacturer', 373 | dataType: 'character varying', 374 | }, 375 | { 376 | columnName: 'model', 377 | dataType: 'character varying', 378 | }, 379 | ], 380 | edges: [], 381 | isIntersectionTable: false, 382 | }, 383 | { 384 | name: 'vessels_in_films', 385 | primaryKey: '_id', 386 | columns: [ 387 | { 388 | columnName: '_id', 389 | dataType: 'integer', 390 | }, 391 | { 392 | columnName: 'vessel_id', 393 | dataType: 'bigint', 394 | }, 395 | { 396 | columnName: 'film_id', 397 | dataType: 'bigint', 398 | }, 399 | ], 400 | edges: [ 401 | { 402 | fKey: 'vessel_id', 403 | refTable: 'vessels', 404 | }, 405 | { 406 | fKey: 'film_id', 407 | refTable: 'films', 408 | }, 409 | ], 410 | isIntersectionTable: true, 411 | }, 412 | { 413 | name: 'starship_specs', 414 | primaryKey: '_id', 415 | columns: [ 416 | { 417 | columnName: '_id', 418 | dataType: 'integer', 419 | }, 420 | { 421 | columnName: 'vessel_id', 422 | dataType: 'bigint', 423 | }, 424 | { 425 | columnName: 'hyperdrive_rating', 426 | dataType: 'character varying', 427 | }, 428 | { 429 | columnName: 'MGLT', 430 | dataType: 'character varying', 431 | }, 432 | ], 433 | edges: [ 434 | { 435 | fKey: 'vessel_id', 436 | refTable: 'vessels', 437 | }, 438 | ], 439 | isIntersectionTable: false, 440 | }, 441 | ], 442 | resolvers: [ 443 | { 444 | tableName: 'species', 445 | resolver: 446 | '\n species: async () => {\n const query = `SELECT * FROM species`;\n const values = [node.name];\n const { rows } = await quilDBConnection_1.quilDbConnection.query(query, values);\n return rows;\n }\n\n speciesById: async (_, args) => {\n const query = `SELECT * FROM species WHERE _id = $1`;\n const values = [args._id];\n const { rows } = await quilDBConnection_1.quilDbConnection.query(query, values);\n return rows;\n }', 447 | }, 448 | { 449 | tableName: 'people', 450 | resolver: 451 | '\n people: async () => {\n const query = `SELECT * FROM people`;\n const values = [node.name];\n const { rows } = await quilDBConnection_1.quilDbConnection.query(query, values);\n return rows;\n }\n\n person: async (_, args) => {\n const query = `SELECT * FROM people WHERE _id = $1`;\n const values = [args._id];\n const { rows } = await quilDBConnection_1.quilDbConnection.query(query, values);\n return rows;\n }', 452 | }, 453 | { 454 | tableName: 'planets', 455 | resolver: 456 | '\n planets: async () => {\n const query = `SELECT * FROM planets`;\n const values = [node.name];\n const { rows } = await quilDBConnection_1.quilDbConnection.query(query, values);\n return rows;\n }\n\n planet: async (_, args) => {\n const query = `SELECT * FROM planets WHERE _id = $1`;\n const values = [args._id];\n const { rows } = await quilDBConnection_1.quilDbConnection.query(query, values);\n return rows;\n }', 457 | }, 458 | { 459 | tableName: 'films', 460 | resolver: 461 | '\n films: async () => {\n const query = `SELECT * FROM films`;\n const values = [node.name];\n const { rows } = await quilDBConnection_1.quilDbConnection.query(query, values);\n return rows;\n }\n\n film: async (_, args) => {\n const query = `SELECT * FROM films WHERE _id = $1`;\n const values = [args._id];\n const { rows } = await quilDBConnection_1.quilDbConnection.query(query, values);\n return rows;\n }', 462 | }, 463 | { 464 | tableName: 'vessels', 465 | resolver: 466 | '\n vessels: async () => {\n const query = `SELECT * FROM vessels`;\n const values = [node.name];\n const { rows } = await quilDBConnection_1.quilDbConnection.query(query, values);\n return rows;\n }\n\n vessel: async (_, args) => {\n const query = `SELECT * FROM vessels WHERE _id = $1`;\n const values = [args._id];\n const { rows } = await quilDBConnection_1.quilDbConnection.query(query, values);\n return rows;\n }', 467 | }, 468 | { 469 | tableName: 'starship_specs', 470 | resolver: 471 | '\n starship_specs: async () => {\n const query = `SELECT * FROM starship_specs`;\n const values = [node.name];\n const { rows } = await quilDBConnection_1.quilDbConnection.query(query, values);\n return rows;\n }\n\n starship_spec: async (_, args) => {\n const query = `SELECT * FROM starship_specs WHERE _id = $1`;\n const values = [args._id];\n const { rows } = await quilDBConnection_1.quilDbConnection.query(query, values);\n return rows;\n }', 472 | }, 473 | ], 474 | schemas: [ 475 | { 476 | tableName: 'species', 477 | schemas: 478 | 'type Species {\n _id: ID!\n name: String!\n classification: String\n average_height: String\n average_lifespan: String\n hair_colors: String\n skin_colors: String\n eye_colors: String\n language: String\n people: [Person]\n films: [Film]\n planets: [Planet]\n}\n \n', 479 | }, 480 | { 481 | tableName: 'people', 482 | schemas: 483 | 'type Person {\n _id: ID!\n name: String!\n mass: String\n hair_color: String\n skin_color: String\n eye_color: String\n birth_year: String\n gender: String\n height: Int\n films: [Film]\n vessels: [Vessel]\n species: [Species]\n planets: [Planet]\n}\n \n', 484 | }, 485 | { 486 | tableName: 'planets', 487 | schemas: 488 | 'type Planet {\n _id: ID!\n name: String\n rotation_period: Int\n orbital_period: Int\n diameter: Int\n climate: String\n gravity: String\n terrain: String\n surface_water: String\n population: Int\n species: [Species]\n people: [Person]\n films: [Film]\n}\n \n', 489 | }, 490 | { 491 | tableName: 'films', 492 | schemas: 493 | 'type Film {\n _id: ID!\n title: String!\n episode_id: Int!\n opening_crawl: String!\n director: String!\n producer: String!\n release_date: String!\n people: [Person]\n species: [Species]\n planets: [Planet]\n vessels: [Vessel]\n}\n \n', 494 | }, 495 | { 496 | tableName: 'vessels', 497 | schemas: 498 | 'type Vessel {\n _id: ID!\n name: String!\n manufacturer: String\n model: String\n vessel_type: String!\n vessel_class: String!\n cost_in_credits: Int\n length: String\n max_atmosphering_speed: String\n crew: Int\n passengers: Int\n cargo_capacity: String\n consumables: String\n people: [Person]\n films: [Film]\n starship_specs: [Starship_spec]\n}\n \n', 499 | }, 500 | { 501 | tableName: 'starship_specs', 502 | schemas: 503 | 'type Starship_spec {\n _id: ID!\n hyperdrive_rating: String\n MGLT: String\n vessels: [Vessel]\n}\n \n', 504 | }, 505 | ], 506 | }, 507 | }, 508 | }; 509 | 510 | export default res; 511 | -------------------------------------------------------------------------------- /app/Main/Chart/page.tsx: -------------------------------------------------------------------------------- 1 | import MainContainer from './(components)/MainContainer'; 2 | import createNodes from './(flow)/Nodes'; 3 | import createEdges from './(flow)/Edges'; 4 | 5 | import 'react-toastify/dist/ReactToastify.css'; 6 | 7 | async function getData(URI: string) { 8 | let data = await fetch( 9 | 'http://quilbackend1-env.eba-52zmdsmp.us-east-1.elasticbeanstalk.com/graphql', 10 | { 11 | method: 'POST', 12 | 13 | headers: { 14 | 'Content-Type': 'application/json', 15 | }, 16 | 17 | body: JSON.stringify({ 18 | query: `query GetData { 19 | getAllData(uri: "${URI}") { 20 | nodes { 21 | name, 22 | primaryKey, 23 | columns { 24 | columnName, 25 | dataType 26 | }, 27 | edges { 28 | fKey, 29 | refTable 30 | } 31 | }, 32 | resolvers { 33 | tableName, 34 | resolver 35 | }, 36 | schemas { 37 | tableName, 38 | schemas 39 | } 40 | } 41 | }`, 42 | }), 43 | } 44 | ); 45 | 46 | const res = await data?.json(); 47 | return res; 48 | } 49 | 50 | export default async function Page({ 51 | searchParams, 52 | }: { 53 | searchParams: { URI: string }; 54 | }) { 55 | let initialNodes: any; 56 | let initialEdges: any; 57 | let data: any; 58 | 59 | if (!searchParams.URI) { 60 | initialNodes = []; 61 | initialEdges = []; 62 | data = {}; 63 | } else { 64 | data = await getData(searchParams.URI); 65 | initialNodes = createNodes(data); 66 | initialEdges = createEdges(data); 67 | } 68 | 69 | return ( 70 | // Parent component of reactflow needs a height and width in order to display 71 |
72 | 78 |
79 |
80 | ); 81 | 82 | // const data = await getData(searchParams.URI); 83 | // if (!data.getAllData) { 84 | // return

Loading

; 85 | // } else { 86 | // const { nodes } = data.getAllData; 87 | // const initialNodes = createNodes(nodes); 88 | // const initialEdges = createEdges(nodes); 89 | 90 | // return ( 91 | // // Parent component of reactflow needs a height and width in order to display 92 | //
93 | // 98 | //
99 | //
100 | // ); 101 | // } 102 | } 103 | -------------------------------------------------------------------------------- /app/Main/layout.tsx: -------------------------------------------------------------------------------- 1 | import '../(root)/globals.css'; 2 | export default function MainLayout({ 3 | children, 4 | }: { 5 | children: React.ReactNode; 6 | }) { 7 | return ( 8 | 9 | {children} 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /app/Main/loading.tsx: -------------------------------------------------------------------------------- 1 | import React, { Suspense } from "react"; 2 | import { toast, ToastContainer } from "react-toastify"; 3 | function Loading() { 4 | return ( 5 |
6 |
loading
7 |
8 | ); 9 | } 10 | 11 | export default Loading; 12 | -------------------------------------------------------------------------------- /app/types.ts: -------------------------------------------------------------------------------- 1 | export type GlobalServerError = { 2 | log: string; 3 | status: number; 4 | message: { err: string }; 5 | }; 6 | 7 | export type dbConstructor = { 8 | new (s: string): object; 9 | dbType: string; 10 | query: Function; 11 | pool: { query: Function }; 12 | queryTables: Function; 13 | queryColumns: Function; 14 | queryPKey: Function; 15 | queryFKeys: Function; 16 | queryTableLayout: Function; 17 | }; 18 | 19 | export type unparsedColumnShape = { 20 | column_name: string; 21 | data_type: string; 22 | }; 23 | 24 | export type parsedColumnShape = { 25 | columnName: string; 26 | dataType: string; 27 | }; 28 | 29 | export type unparsedKeys = { 30 | table_name: string; 31 | pg_get_constraintdef: string; 32 | }; 33 | 34 | export type parsedFKeys = { 35 | fKey: string; 36 | refTable: string; 37 | }; 38 | 39 | export type nodeShape = { 40 | name: string; 41 | primaryKey: string; 42 | columns: parsedColumnShape[]; 43 | edges: parsedFKeys[]; 44 | isIntersectionTable: boolean; 45 | }; 46 | 47 | export type objectOfArrOfNodes = { 48 | nodes: nodeShape[]; 49 | }; 50 | 51 | // TODO: change schema types 52 | export type schema = { 53 | [key: string]: any; 54 | }; 55 | 56 | export type pSQLToGQL = { 57 | [key: string]: string; 58 | }; 59 | 60 | // Server Types 61 | export type QuiLData = { 62 | nodes: nodeShape[]; 63 | resolvers: ResolverStrings[]; 64 | schemas: SingleSchemaType[]; 65 | }; 66 | 67 | export type TableResolver = { 68 | getOne: Function; 69 | getAll: Function; 70 | }; 71 | 72 | export type ResolverStrings = { 73 | tableName: string; 74 | resolver: string; 75 | }; 76 | 77 | export type TableResolver1 = Function; 78 | 79 | export interface ArgType { 80 | uri?: string; 81 | _id?: string; 82 | node?: nodeShape; 83 | } 84 | 85 | export type SingleSchemaType = { 86 | tableName: string; 87 | schemas: string; 88 | }; 89 | 90 | export type OAuthArgs = { 91 | code: string; 92 | oauthType: string; 93 | }; 94 | 95 | export type JWTResponse = { 96 | token: string; 97 | }; 98 | // andres added newuser 99 | export type CreateNewUserObject = { 100 | oauthUser: boolean; 101 | username: string; 102 | name?: string; 103 | avatarUrl?: string; 104 | password?: string; 105 | }; 106 | 107 | export type SaveProject = { 108 | projectName: string; 109 | projectData: string; 110 | userId: string; 111 | }; 112 | 113 | export type CreateNewAccountResponse = { 114 | success: boolean; 115 | username: string; 116 | userId: number; 117 | name?: string; 118 | avatarUrl?: string; 119 | }; 120 | 121 | export type SavedProjectRes = { 122 | projectName?: string; 123 | success: boolean; 124 | projectId?: number; 125 | }; 126 | 127 | export type GetUserProjectRes = { 128 | saved_db?: []; 129 | success: boolean; 130 | }; 131 | 132 | export type GetUser = { 133 | username: string; 134 | password: string; 135 | }; 136 | 137 | export type GetUserRes = { 138 | id_?: number; 139 | success: boolean; 140 | }; 141 | 142 | export interface MyContext { 143 | token?: String; 144 | } 145 | 146 | export type TokenJwt = { 147 | token: string; 148 | }; -------------------------------------------------------------------------------- /codingStandards.txt: -------------------------------------------------------------------------------- 1 | /*---> File Naming Conventions 2 | - All .tsx components should be PascalCase 3 | - All non tsx files should be camelCase 4 | */ 5 | 6 | // No more than 2 empty new lines between code blocks 7 | 8 | /* ====================================== 9 | --> Variable Conventions 10 | - All variables are camelCased 11 | - Always use const rather than var 12 | - Variable names should be semantic in what they do, favor descriptive names over short ones 13 | */ 14 | const randomNumber = Math.random() * 100; 15 | 16 | /* ====================================== 17 | Function Definitions 18 | */ 19 | // Arrow functions 20 | const createGetResolver = (value: number) => { 21 | console.log(value); 22 | }; 23 | 24 | // All strings should be single-quote 25 | 26 | /* All multi-line comments should use multi-line comments */ 27 | // All single line comments should be single-line comments 28 | 29 | /* 30 | Object/Array Conventions: 31 | - commas at end of key-value pairs in objs and array elements (even if there is only one key-value pair (object) or element (array) 32 | */ 33 | 34 | const clients = { 35 | id: '1', 36 | }; 37 | 38 | const people = ['Pablo']; 39 | 40 | /* 41 | For both if and else statements, if result is one-liner, then keep it single-line 42 | Otherwise, use curly brackets and put the else in the same line as closing curly bracked for if statement 43 | */ 44 | // Ex: 45 | if (clients) { 46 | console.log(true); 47 | } else { 48 | console.log(false); 49 | } 50 | -------------------------------------------------------------------------------- /cypress.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "cypress"; 2 | 3 | export default defineConfig({ 4 | e2e: { 5 | // experimentalSessionAndOrigin: true, 6 | setupNodeEvents(on, config) { 7 | // implement node event listeners here 8 | }, 9 | }, 10 | chromeWebSecurity: false // set this option to redirect to different URL's such as github for O 11 | }); 12 | -------------------------------------------------------------------------------- /cypress/e2e/root-to-main.cy.ts: -------------------------------------------------------------------------------- 1 | describe('checks Main page contents are properly displayed after being redirected from root', () => { 2 | 3 | const checkMainContents = () => { 4 | it('checks to see if main page contents are properly loaded', () => { 5 | cy.getByData('nav-bar').should('be.visible') 6 | cy.getByData('view-schemas-resolvers-btn').should('be.visible') 7 | cy.getByData('insert-uri-main').should('be.visible') 8 | cy.getByData('main-launch-btn').should('be.visible') 9 | cy.getByData('react-flow').should('be.visible') 10 | cy.getByData('nav-bar').should('be.visible') 11 | }) 12 | } 13 | 14 | context('mimics non logged in user choosing starwars sample data base from root', () => { 15 | it('successfully loads root', () => { 16 | cy.visit('http://localhost:3000/') 17 | }) 18 | it('clicks on sample database select and clicks starwars then clicks launch', () => { 19 | cy.getByData('select-sample-db').select('Star Wars') 20 | cy.getByData('root-launch').click() 21 | }) 22 | checkMainContents(); 23 | }) 24 | 25 | context('mimics non logged in user using personal URI input (starwars)', () => { 26 | it('successfully loads root', () => { 27 | cy.visit('http://localhost:3000/') 28 | }) 29 | it('clicks on sample database select and clicks starwars then clicks launch', () => { 30 | cy.getByData('root-uri-input').type('postgres://lkdxllvk:GTIkPygxpPOx0ZVNJ3luQHEfApEIJekP@heffalump.db.elephantsql.com/lkdxllvk') 31 | cy.getByData('root-launch').click() 32 | }) 33 | checkMainContents(); 34 | }) 35 | 36 | 37 | 38 | 39 | }) 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | export {}; -------------------------------------------------------------------------------- /cypress/e2e/root.cy.ts: -------------------------------------------------------------------------------- 1 | const checkRootContents = () => { 2 | it('checks to see if root page displays elements and contains the correct values', () => { 3 | cy.getByData('root-register').should('be.visible') 4 | cy.getByData('root-login').should('be.visible') 5 | cy.getByData('root-uri-input').should('be.visible') 6 | cy.getByData('root-h1').contains('QuiL').should('be.visible') 7 | cy.getByData('root-p').should('be.visible') 8 | cy.getByData('select-sample-db').select('Star Wars').should('have.value', 9 | 'postgres://lkdxllvk:GTIkPygxpPOx0ZVNJ3luQHEfApEIJekP@heffalump.db.elephantsql.com/lkdxllvk') 10 | cy.getByData('select-sample-db').select('Quitr').should('have.value', 11 | 'postgres://nsjouiot:4nVVHLiARTADoIiwArtQLG-HfkhQR03k@peanut.db.elephantsql.com/nsjouiot') 12 | }) 13 | } 14 | 15 | describe('checks to see root page contents are properly displayed', () => { 16 | it('successfully loads root', () => { 17 | cy.visit('http://localhost:3000/') //this will change to actual domain name 18 | checkRootContents(); 19 | }) 20 | }) 21 | 22 | 23 | // not done yet, need to figure out cookies within cypress 24 | describe('new user visiting root page and registering', () => { 25 | it('clicks register button and redirects to /Register', () => { 26 | cy.visit('http://localhost:3000/') 27 | cy.getByData('root-register-btn').click() 28 | cy.url().should('include', '/Register') 29 | cy.getByData('register-form').should('be.visible') 30 | }) 31 | it('clicks github register button and redirects to github', () => { 32 | cy.getByData('register-github-btn').click() 33 | cy.url().should('include', 'github.com/login') 34 | cy.get('.js-login-field').type('fakeGitHubQuil') 35 | cy.get('.js-password-field').type('codesmithquil1') 36 | cy.get('.js-sign-in-button').click() 37 | }) 38 | }) 39 | 40 | // errors out due to JWT issue. cannot read properties of null 41 | describe.only('returning user attempting to login', () => { 42 | it('clicks login button and redirects to /Login', () => { 43 | cy.visit('http://localhost:3000/') 44 | cy.getByData('root-login-btn').click() 45 | cy.url().should('include', '/Login') 46 | cy.getByData('login-form').should('be.visible') 47 | cy.getByData('login-username').type('KingFritz') 48 | cy.getByData('login-password').type('1') 49 | cy.getByData('login-button').click() 50 | }) 51 | }) 52 | 53 | export {}; -------------------------------------------------------------------------------- /cypress/e2e/unit-testing.cy.ts: -------------------------------------------------------------------------------- 1 | import createNodes from "../../app/Main/Chart/(flow)/Nodes"; 2 | import createEdges from "../../app/Main/Chart/(flow)/Edges"; 3 | import res from "../../app/Main/Chart/(flow)/dummyRes"; 4 | import { data } from 'cypress/types/jquery'; 5 | 6 | describe('unit test for application code', () => { 7 | context('Nodes.tsx', () => { 8 | const nodes = createNodes(res); 9 | const nodeKeys = ['id', 'type', 'position', 'data']; 10 | const dataKeys = ['name', 'key', 'columns', 'edges', 'refTables', 'arrFKeys'] 11 | it('should test if each nodes array exists and has length', () => { 12 | expect(Array.isArray(nodes)).to.eq(true); 13 | }); 14 | it('should test if each node has correct properties', () => { 15 | nodes.forEach(node => { 16 | expect(typeof node === 'object').to.eq(true) 17 | nodeKeys.forEach(key => { 18 | expect(node.hasOwnProperty(key)).to.eq(true) 19 | }) 20 | }) 21 | }) 22 | it('should test each property in each node has the correct value', () => { 23 | nodes.forEach(node => { 24 | expect(typeof node['id'] === 'string').to.eq(true) 25 | expect(node['type']).to.eq("tableNode") 26 | expect(node['position'].hasOwnProperty('x')).to.eq(true); 27 | expect(node['position'].hasOwnProperty('y')).to.eq(true); 28 | expect(node['data']).to.not.eq(null); 29 | expect(typeof node['data'] === 'object').to.eq(true) 30 | }) 31 | }) 32 | it('to see if data properties have the correct value', () => { 33 | nodes.forEach(node => { 34 | dataKeys.forEach(key => { 35 | expect(node.data.hasOwnProperty(key)).to.eq(true); 36 | }) 37 | }) 38 | }) 39 | }) 40 | 41 | context('Edges.tsx', () => { 42 | const edges = createEdges(res) 43 | const edgeKeys = ['id', 'animated', 'style', 'source', 'type', 'sourceHandle', 'targetHandle', 'target']; 44 | 45 | it('if each nodes array exists and has length', () => { 46 | expect(Array.isArray(edges)).to.eq(true); 47 | }); 48 | 49 | it('if each node is an object and has proper keys', () => { 50 | edges.forEach(edge => { 51 | expect(typeof edge === 'object').to.eq(true) 52 | edgeKeys.forEach(key => { 53 | expect(edge.hasOwnProperty(key)).to.eq(true) 54 | }) 55 | }) 56 | }) 57 | }) 58 | }) -------------------------------------------------------------------------------- /cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } 6 | -------------------------------------------------------------------------------- /cypress/support/commands.ts: -------------------------------------------------------------------------------- 1 | /// 2 | // *********************************************** 3 | // This example commands.ts shows you how to 4 | // create various custom commands and overwrite 5 | // existing commands. 6 | // 7 | // For more comprehensive examples of custom 8 | // commands please read more here: 9 | // https://on.cypress.io/custom-commands 10 | // *********************************************** 11 | // 12 | // 13 | // -- This is a parent command -- 14 | // Cypress.Commands.add('login', (email, password) => { ... }) 15 | // 16 | // 17 | // -- This is a child command -- 18 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) 19 | // 20 | // 21 | // -- This is a dual command -- 22 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) 23 | // 24 | // 25 | // -- This will overwrite an existing command -- 26 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) 27 | // 28 | // declare global { 29 | // namespace Cypress { 30 | // interface Chainable { 31 | // login(email: string, password: string): Chainable 32 | // drag(subject: string, options?: Partial): Chainable 33 | // dismiss(subject: string, options?: Partial): Chainable 34 | // visit(originalFn: CommandOriginalFn, url: string, options: Partial): Chainable 35 | // } 36 | // } 37 | // } 38 | 39 | declare global { 40 | namespace Cypress { 41 | interface Chainable { 42 | /** 43 | * Custom command to select DOM element by data-cy attribute. 44 | * @example cy.dataCy('greeting') 45 | */ 46 | getByData(value: string): Chainable> 47 | } 48 | } 49 | } 50 | 51 | // instead of: 52 | 53 | // cy.get("[data-cy='hero-heading']") 54 | // cy.getByData("hero-heading") 55 | 56 | Cypress.Commands.add('getByData', (selector) => { 57 | return cy.get(`[data-cy=${selector}]`) 58 | }) 59 | 60 | 61 | export {}; -------------------------------------------------------------------------------- /cypress/support/e2e.ts: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/e2e.ts is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') -------------------------------------------------------------------------------- /cypress/unit-testing/unit-testing.cy.ts: -------------------------------------------------------------------------------- 1 | import createNodes from "../../app/Main/Chart/(flow)/Nodes"; 2 | import createEdges from "../../app/Main/Chart/(flow)/Edges"; 3 | import res from "../../app/Main/Chart/(flow)/dummyRes"; 4 | import { data } from 'cypress/types/jquery'; 5 | 6 | describe('unit test for application code', () => { 7 | context('Nodes.tsx', () => { 8 | const nodes = createNodes(res); 9 | const nodeKeys = ['id', 'type', 'position', 'data']; 10 | const dataKeys = ['name', 'key', 'columns', 'edges', 'refTables', 'arrFKeys'] 11 | it('should test if each nodes array exists and has length', () => { 12 | expect(Array.isArray(nodes)).to.eq(true); 13 | }); 14 | it('should test if each node has correct properties', () => { 15 | nodes.forEach(node => { 16 | expect(typeof node === 'object').to.eq(true) 17 | nodeKeys.forEach(key => { 18 | expect(node.hasOwnProperty(key)).to.eq(true) 19 | }) 20 | }) 21 | }) 22 | it('should test each property in each node has the correct value', () => { 23 | nodes.forEach(node => { 24 | expect(typeof node['id'] === 'string').to.eq(true) 25 | expect(node['type']).to.eq("tableNode") 26 | expect(node['position'].hasOwnProperty('x')).to.eq(true); 27 | expect(node['position'].hasOwnProperty('y')).to.eq(true); 28 | expect(node['data']).to.not.eq(null); 29 | expect(typeof node['data'] === 'object').to.eq(true) 30 | }) 31 | }) 32 | it('to see if data properties have the correct value', () => { 33 | nodes.forEach(node => { 34 | dataKeys.forEach(key => { 35 | expect(node.data.hasOwnProperty(key)).to.eq(true); 36 | }) 37 | }) 38 | }) 39 | }) 40 | 41 | context('Edges.tsx', () => { 42 | const edges = createEdges(res) 43 | const edgeKeys = ['id', 'animated', 'style', 'source', 'type', 'sourceHandle', 'targetHandle', 'target']; 44 | 45 | it('if each nodes array exists and has length', () => { 46 | expect(Array.isArray(edges)).to.eq(true); 47 | }); 48 | 49 | it('if each node is an object and has proper keys', () => { 50 | edges.forEach(edge => { 51 | expect(typeof edge === 'object').to.eq(true) 52 | edgeKeys.forEach(key => { 53 | expect(edge.hasOwnProperty(key)).to.eq(true) 54 | }) 55 | }) 56 | }) 57 | }) 58 | }) -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | quilfrontend: 4 | image: 'quil-frontend' 5 | container_name: quil-frontend 6 | ports: 7 | - 3000:3000 8 | depends_on: 9 | - quilserver 10 | command: npm start 11 | quilserver: 12 | image: 'quil-backend' 13 | container_name: quil-server 14 | ports: 15 | - 4000:4000 16 | command: npm start 17 | environment: 18 | - GITHUB_OAUTH_CLIENT_ID=99436692da0716eb1c22 19 | - GITHUB_OAUTH_CLIENT_SECRET=5fa9b64049117efb7af84af8db2b2b16934f015b 20 | - JWT_SECRET=19da&0bVb1J4XTdg@fLsx^0117Is00 21 | - QUIL_DB_CONNECTION_STRING=postgres://enajtcbr:Y8qxFs9nSsuK55SN-Rqgd1SUfvIMTOSE@heffalump.db.elephantsql.com/enajtcbr 22 | - GITHUB_AUTHORIZE_URL=https://github.com/login/oauth/authorize 23 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | async rewrites() { 4 | return [ 5 | { 6 | source: '/graphql', 7 | destination: 'http://localhost:4000/graphql', 8 | }, 9 | ]; 10 | }, 11 | reactStrictMode: false, 12 | experimental: { 13 | appDir: true, 14 | }, 15 | }; 16 | 17 | module.exports = nextConfig; 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "quil", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "./dist/server.js", 6 | "scripts": { 7 | "build": "next build", 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "dev": "concurrently \"next dev\" \"ts-node-dev server/server.ts\"", 10 | "start": "next start" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/oslabs-beta/QuiL.git" 15 | }, 16 | "dependencies": { 17 | "@ant-design/icons": "^4.8.0", 18 | "@apollo/server": "^4.1.1", 19 | "@codemirror/lang-javascript": "^6.1.1", 20 | "@graphql-tools/load-files": "^6.6.1", 21 | "@graphql-tools/merge": "^8.3.11", 22 | "@lezer/highlight": "^1.1.2", 23 | "@uiw/codemirror-theme-dracula": "^4.13.2", 24 | "@uiw/codemirror-themes": "^4.13.2", 25 | "@uiw/react-codemirror": "^4.13.2", 26 | "axios": "^1.1.3", 27 | "bcrypt": "^5.1.0", 28 | "body-parser": "^1.20.1", 29 | "cors": "^2.8.5", 30 | "daisyui": "^2.40.1", 31 | "dotenv": "^16.0.3", 32 | "eslint-config-next": "^13.0.3", 33 | "express": "^4.18.2", 34 | "framer-motion": "^7.6.7", 35 | "graphql": "^16.6.0", 36 | "graphql-type-json": "^0.3.2", 37 | "jsonwebtoken": "^8.5.1", 38 | "jwt-decode": "^3.1.2", 39 | "next": "^13.0.3", 40 | "node-typescript": "^0.1.3", 41 | "pg": "^8.8.0", 42 | "pluralize": "^8.0.0", 43 | "randomstring": "^1.2.3", 44 | "react": "^18.2.0", 45 | "react-dom": "^18.2.0", 46 | "react-toastify": "^9.1.1", 47 | "reactflow": "^11.2.0", 48 | "ts-node": "^10.9.1", 49 | "tsc": "^2.0.4" 50 | }, 51 | "devDependencies": { 52 | "@babel/preset-typescript": "^7.18.6", 53 | "@jest/globals": "^29.3.1", 54 | "@tailwindcss/typography": "^0.5.8", 55 | "@types/cors": "^2.8.12", 56 | "@types/express": "^4.17.14", 57 | "@types/jsonwebtoken": "^8.5.9", 58 | "@types/node": "18.11.9", 59 | "@types/pg": "^8.6.5", 60 | "@types/react": "18.0.25", 61 | "autoprefixer": "^10.4.13", 62 | "concurrently": "^7.5.0", 63 | "cypress": "^11.2.0", 64 | "jest": "^29.3.1", 65 | "nodemon": "^2.0.20", 66 | "postcss": "^8.4.18", 67 | "tailwindcss": "^3.2.2", 68 | "ts-jest": "^29.0.3", 69 | "ts-node": "^10.9.1", 70 | "ts-node-dev": "^2.0.0", 71 | "typescript": "^4.8.4" 72 | }, 73 | "keywords": [], 74 | "author": "", 75 | "license": "ISC", 76 | "bugs": { 77 | "url": "https://github.com/oslabs-beta/QuiL/issues" 78 | }, 79 | "homepage": "https://github.com/oslabs-beta/QuiL#readme" 80 | } 81 | -------------------------------------------------------------------------------- /pages/api/graphql.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next'; 2 | import axios from 'axios'; 3 | 4 | const uri = 5 | 'http://quilbackend1-env.eba-52zmdsmp.us-east-1.elasticbeanstalk.com/graphql'; 6 | 7 | export default async function handler( 8 | request: NextApiRequest, 9 | response: NextApiResponse 10 | ) { 11 | console.log('here'); 12 | 13 | const { data } = await axios.post(uri, request.body); 14 | 15 | response.status(200).json(data); 16 | } 17 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | // module.exports = { 2 | // content: [ 3 | // './app/**/*.{js,ts,jsx,tsx}', 4 | // ], 5 | // theme: { 6 | // extend: {}, 7 | // }, 8 | // plugins: [], 9 | // }; 10 | 11 | module.exports = { 12 | plugins: { 13 | tailwindcss: {}, 14 | autoprefixer: {}, 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /public/andres.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/QuiL/8328a039999732b798888c264ee9d47efbe46f90/public/andres.jpeg -------------------------------------------------------------------------------- /public/brian.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/QuiL/8328a039999732b798888c264ee9d47efbe46f90/public/brian.png -------------------------------------------------------------------------------- /public/daniel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/QuiL/8328a039999732b798888c264ee9d47efbe46f90/public/daniel.png -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/QuiL/8328a039999732b798888c264ee9d47efbe46f90/public/logo.png -------------------------------------------------------------------------------- /public/stephen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/QuiL/8328a039999732b798888c264ee9d47efbe46f90/public/stephen.png -------------------------------------------------------------------------------- /server/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/QuiL/8328a039999732b798888c264ee9d47efbe46f90/server/.DS_Store -------------------------------------------------------------------------------- /server/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16.17.1 2 | 3 | WORKDIR /usr/src/app 4 | 5 | COPY package.json /usr/src/app/ 6 | 7 | RUN npm install && npm install typescript -g 8 | 9 | COPY . /usr/src/app/ 10 | 11 | RUN tsc 12 | 13 | CMD npm start 14 | 15 | EXPOSE 4000 -------------------------------------------------------------------------------- /server/db/dbConnection.ts: -------------------------------------------------------------------------------- 1 | import { GlobalServerError, dbConstructor } from '../types'; 2 | import { Pool } from 'pg'; 3 | 4 | /* 5 | create a func that accepts URI input and exports/returns query obj that's being exported (line 21), then export said func 6 | since return value is query obj, we can import it into helperFuncs 7 | */ 8 | 9 | function dbInstance(this: dbConstructor, inputURI: string): void { 10 | this.dbType = 'PostgreSQL'; 11 | this.pool = new Pool({ connectionString: inputURI }); 12 | this.query = (text: string, params?: object, callback?: Function): Object => { 13 | // console.log('executed query', text); 14 | return this.pool.query(text, params, callback); 15 | }; 16 | } 17 | 18 | // Given a database, return all table names in an array 19 | dbInstance.prototype.queryTables = async function (): Promise< 20 | [] | GlobalServerError 21 | > { 22 | try { 23 | if (this.dbType === 'PostgreSQL') { 24 | const tablesQuery = ` 25 | SELECT table_name 26 | FROM information_schema.tables 27 | WHERE table_schema='public' 28 | AND table_type='BASE TABLE';`; 29 | const { rows } = await this.query(tablesQuery); 30 | return rows; 31 | } 32 | } catch (err: unknown) { 33 | return { 34 | log: `dbConnection.js.queryTables: ERROR: ${err}`, 35 | status: 400, 36 | message: { err: 'An error occured' }, 37 | }; 38 | } 39 | }; 40 | 41 | // Given a table name, return all columns of that table in an array of objects 42 | dbInstance.prototype.queryColumns = async function ( 43 | tableName: string 44 | ): Promise<[] | GlobalServerError> { 45 | try { 46 | const query = ` 47 | SELECT column_name, data_type 48 | FROM information_schema.columns 49 | WHERE table_name = '${tableName}'; 50 | `; 51 | const { rows } = await this.query(query); 52 | return rows; 53 | } catch (err: unknown) { 54 | return { 55 | log: `dbConnection.js.queryColumns: ERROR: ${err}`, 56 | status: 400, 57 | message: { err: 'An error occured' }, 58 | }; 59 | } 60 | }; 61 | 62 | // Returns query for all primary keys of a database 63 | dbInstance.prototype.queryPKey = async function (): Promise< 64 | [] | GlobalServerError 65 | > { 66 | try { 67 | const query = ` 68 | SELECT conrelid::regclass AS table_name, 69 | conname AS primary_key, 70 | pg_get_constraintdef(oid) 71 | FROM pg_constraint 72 | WHERE contype = 'p' 73 | AND connamespace = 'public'::regnamespace 74 | ORDER BY conrelid::regclass::text, contype DESC; 75 | `; 76 | const { rows } = await this.query(query); 77 | return rows; 78 | } catch (err: unknown) { 79 | return { 80 | log: `dbConnection.js.queryPKey: ERROR: ${err}`, 81 | status: 400, 82 | message: { err: 'An error occured' }, 83 | }; 84 | } 85 | }; 86 | 87 | // Returns query for all 88 | dbInstance.prototype.queryFKeys = async function (): Promise< 89 | [] | GlobalServerError 90 | > { 91 | try { 92 | const query = ` 93 | SELECT conrelid::regclass AS table_name, 94 | conname AS foreign_key, 95 | pg_get_constraintdef(oid) 96 | FROM pg_constraint 97 | WHERE contype = 'f' 98 | AND connamespace = 'public'::regnamespace 99 | ORDER BY conrelid::regclass::text, contype DESC; 100 | `; 101 | const { rows } = await this.query(query); 102 | return rows; 103 | } catch (err: unknown) { 104 | return { 105 | log: `dbConnection.js.queryFKeys: ERROR: ${err}`, 106 | status: 400, 107 | message: { err: 'An error occured' }, 108 | }; 109 | } 110 | }; 111 | 112 | dbInstance.prototype.queryTableLayout = async function ( 113 | tableName: string 114 | ): Promise<[] | GlobalServerError> { 115 | try { 116 | const query = ` 117 | SELECT table_name, column_name, is_nullable, data_type 118 | FROM information_schema.columns 119 | WHERE table_schema = 'public' AND table_name = '${tableName}' 120 | `; 121 | const { rows } = await this.query(query); 122 | return rows; 123 | } catch (err: unknown) { 124 | return { 125 | log: `dbConnection.js.queryTableLayout: ERROR: ${err}`, 126 | status: 400, 127 | message: { err: 'An error occured' }, 128 | }; 129 | } 130 | }; 131 | 132 | export { dbInstance }; 133 | -------------------------------------------------------------------------------- /server/db/quilDBConnection.ts: -------------------------------------------------------------------------------- 1 | import { dbInstance } from './dbConnection'; 2 | 3 | const quilDbConnection = new (dbInstance as any)( 4 | process.env.QUIL_DB_CONNECTION_STRING 5 | ); 6 | export { quilDbConnection }; 7 | -------------------------------------------------------------------------------- /server/graphql/models/resolvers/query.resolver.ts: -------------------------------------------------------------------------------- 1 | // TypeScript Definitions 2 | import { 3 | ArgType, 4 | OAuthArgs, 5 | QuiLData, 6 | CreateNewUserObject, 7 | SaveProject, 8 | SavedProjectRes, 9 | GetUserProjectRes, 10 | GetUser, 11 | JWTResponse, 12 | } from '../../../types'; 13 | 14 | import { dbInstance } from '../../../db/dbConnection'; 15 | import { makeNodes } from '../../../helperFunctions'; 16 | 17 | // Import the userController 18 | import { userController } from '../../../middleware/userController'; 19 | 20 | // TODO: Refactor this to one object 21 | import { 22 | makeResolverFunctions, 23 | makeResolverStrings, 24 | } from '../../../resolverGenerator'; 25 | 26 | // TODO: refactor this to one object 27 | import { generateSchemas } from '../../../schemaGenerator'; 28 | 29 | import { generateJWT, handleOAuth } from '../../../middleware/auth'; 30 | 31 | //Here we define our main GQL resolver definitions. 32 | export const Query = { 33 | /* 34 | Returns all of the data required by the frontend to display in the display container, this includes 35 | the nodes, resolvers, and schemas 36 | */ 37 | getAllData: async (_: any, args: ArgType): Promise => { 38 | if (args.uri === 'undefined') 39 | return { 40 | nodes: null, 41 | resolvers: null, 42 | schemas: null, 43 | }; 44 | const dataBase = new (dbInstance as any)(args.uri); 45 | const { nodes } = await makeNodes(dataBase); 46 | const queryString = await generateSchemas(dataBase); 47 | return { 48 | nodes, 49 | resolvers: nodes.reduce((resolvers, node) => { 50 | if (!node.isIntersectionTable) 51 | resolvers.push( 52 | makeResolverStrings(node, makeResolverFunctions(node)) 53 | ); 54 | return resolvers; 55 | }, []), 56 | schemas: queryString, 57 | }; 58 | }, 59 | // Returns all of the user's projects with a user id 60 | // TODO: Fix the arg type. The arg will be arg.id 61 | getUserProjects: async (_: any, arg: {userId: number}): Promise => { 62 | return await userController.getUserProject(arg.userId); 63 | }, 64 | }; 65 | 66 | // Define all of our mutations for the GQL backend 67 | export const Mutation = { 68 | // Signin handles normal non-oauth signin if the signin is successfull a JWT is returned else token is null 69 | signin: async ( 70 | _: any, 71 | arg: GetUser 72 | ): Promise => { 73 | const user = await userController.validateUser({ 74 | username: arg.username, 75 | password: arg.password, 76 | }); 77 | 78 | if (user.success) { 79 | return generateJWT(user); 80 | } else { 81 | return { 82 | token: null, 83 | }; 84 | } 85 | }, 86 | 87 | // Handles regular non-oauth user sighnup 88 | newUser: async (_: any, obj: CreateNewUserObject): Promise => { 89 | return await generateJWT(await userController.createAccount(obj)); 90 | }, 91 | // Saves a project to a database 92 | saveData: async (_: any, obj: SaveProject): Promise => { 93 | return await userController.saveProject(obj); 94 | }, 95 | // Deletes a project from database 96 | deleteProject: async (_: any, args: {projectId: number} ): Promise => { 97 | return await userController.deleteProject(args.projectId) 98 | }, 99 | 100 | // Handles both login and register GitHub OAuth Requests 101 | postOAuth: async (_: any, args: OAuthArgs): Promise => { 102 | try { 103 | const { token } = await handleOAuth(args.code, args.oauthType); 104 | return { token }; 105 | } catch (error) { 106 | console.log(error.message); 107 | } 108 | }, 109 | }; 110 | -------------------------------------------------------------------------------- /server/graphql/models/schemas/Data.ts: -------------------------------------------------------------------------------- 1 | export const DataType: string = ` 2 | type Data { 3 | nodes: [Node], 4 | resolvers: [ResolverStrings], 5 | schemas: [SchemasObject] 6 | } 7 | 8 | type Node { 9 | name: String, 10 | primaryKey: String, 11 | columns: [ColumnData], 12 | edges: [Edge], 13 | isIntersectionTable: Boolean 14 | } 15 | 16 | type ColumnData { 17 | columnName: String, 18 | dataType: String 19 | } 20 | 21 | type Edge { 22 | fKey: String, 23 | refTable: String 24 | } 25 | 26 | type ResolverStrings { 27 | tableName: String, 28 | resolver: String 29 | } 30 | 31 | type ProjectData { 32 | name: String, 33 | owner_id: Int, 34 | saved_db: String, 35 | _id: Int 36 | } 37 | 38 | type SchemasObject { 39 | tableName: String, 40 | schemas: String 41 | }`; 42 | -------------------------------------------------------------------------------- /server/graphql/models/schemas/Mutation.ts: -------------------------------------------------------------------------------- 1 | export const MutationType: string = ` 2 | type Mutation { 3 | signin(username: String, password: String): JWTResponse, 4 | postOAuth(code: String, oauthType: String): JWTResponse, 5 | newUser(username: String, password: String): JWTResponse, 6 | saveData(projectName: String, projectData: String, userId: Int): SaveData, 7 | deleteProject(projectId: Int): DeleteResponse 8 | } 9 | 10 | 11 | type DeleteResponse { 12 | deleted: Boolean 13 | }`; 14 | -------------------------------------------------------------------------------- /server/graphql/models/schemas/Query.ts: -------------------------------------------------------------------------------- 1 | export const QueryType: string = ` 2 | type Query { 3 | getAllData(uri: String): Data, 4 | getUserProjects(userId: Int): GetUserProjectRes, 5 | testResolver: testType 6 | } 7 | 8 | type testType { 9 | test: String 10 | }`; 11 | -------------------------------------------------------------------------------- /server/graphql/models/schemas/Signin.ts: -------------------------------------------------------------------------------- 1 | export const SiginType: string = ` 2 | type SigninResponse { 3 | token: String, 4 | error: String 5 | } 6 | 7 | type JWTResponse { 8 | token: String 9 | } 10 | 11 | type CreatedUserResponse { 12 | success: Boolean, 13 | userId: Int, 14 | token: String 15 | } 16 | type GetUserProjectRes { 17 | db: [ProjectData], 18 | success: Boolean, 19 | projectId: Int 20 | } 21 | 22 | 23 | type SaveData { 24 | projectName: String, 25 | success: Boolean, 26 | projectId: Int 27 | } 28 | 29 | type GetUser { 30 | username: String, 31 | password: String, 32 | success: Boolean 33 | }`; 34 | -------------------------------------------------------------------------------- /server/graphql/modelsSetup.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import { loadFilesSync } from '@graphql-tools/load-files'; 3 | import { mergeTypeDefs, mergeResolvers } from '@graphql-tools/merge'; 4 | 5 | const resolverFiles = loadFilesSync(path.join(__dirname, './models/resolvers')); 6 | const schemaFiles = loadFilesSync(path.join(__dirname, './models/schemas')); 7 | 8 | /* 9 | This file combines ALL type definitions inside ./models/schemas AND all resolver definitions 10 | inside ./models/resolvers and exports a single instance of all of them. 11 | This enables server.js to have streamlined import statements as opposed to individually importing all definitions. 12 | */ 13 | 14 | export const typeDefs = mergeTypeDefs( 15 | /* 16 | After typescript migration loadFileSync created an array of objects instead an array of one string. 17 | mergeTypeDef seems to only work with an array consisting of one string. 18 | The following reduces all the strings in the array to one string to satisfy that requirement. 19 | TODO: Fix this so that mergeTypeDefs will work without manually reducing the strings 20 | */ 21 | schemaFiles.reduce((a, c) => { 22 | for (const key in c) { 23 | a = a + c[key]; 24 | } 25 | return a; 26 | }, '') 27 | ); 28 | 29 | export const resolvers = mergeResolvers(resolverFiles); 30 | -------------------------------------------------------------------------------- /server/helperFunctions.ts: -------------------------------------------------------------------------------- 1 | import { 2 | dbConstructor, 3 | unparsedColumnShape, 4 | parsedColumnShape, 5 | unparsedKeys, 6 | parsedFKeys, 7 | nodeShape, 8 | objectOfArrOfNodes, 9 | } from './types'; 10 | // function to write statement for PostgresQL to grab table names/data types of tables, foreign keys, primary keys 11 | const { query } = require('express'); 12 | const { debuglog } = require('util'); 13 | // const dbInstance = require('./db/dbConnection'); 14 | import { dbInstance } from './db/dbConnection'; 15 | import { isIntersectionTable } from './schemaGenerator'; 16 | 17 | // parses out columns to key-value name we want 18 | const parseColumns = (columns: unparsedColumnShape[]): parsedColumnShape[] => { 19 | return columns.map((e: unparsedColumnShape): parsedColumnShape => { 20 | return { columnName: e.column_name, dataType: e.data_type }; 21 | }); 22 | }; 23 | 24 | // returns pKey of a given table within a databse (so that it grabs pKeys even if not named '_id') 25 | const parsePKey = (queryResults: unparsedKeys[], tableName: string): string => { 26 | let unparsedPKey; 27 | let pKey = ''; 28 | for (let i = 0; i < queryResults.length; i++) { 29 | let el = queryResults[i]; 30 | if (el.table_name === tableName) { 31 | unparsedPKey = el.pg_get_constraintdef; 32 | } 33 | } 34 | let inParens = false; 35 | for (let j = 0; j < unparsedPKey.length; j++) { 36 | let char = unparsedPKey[j]; 37 | if (char === ')') inParens = false; 38 | if (inParens === true) pKey += char; 39 | if (char === '(') inParens = true; 40 | } 41 | return pKey; 42 | }; 43 | 44 | // given a table, returns foreign keys for a specific table 45 | const parseFKeys = ( 46 | queryResults: unparsedKeys[], 47 | tableName: string 48 | ): parsedFKeys[] => { 49 | const parsedFKeys = []; 50 | const unparsedFKeys = []; 51 | for (let i = 0; i < queryResults.length; i++) { 52 | let el = queryResults[i]; 53 | if (el.table_name === tableName) unparsedFKeys.push(el); 54 | } 55 | for (let j = 0; j < unparsedFKeys.length; j++) { 56 | const fKeyObj: parsedFKeys = { fKey: '', refTable: '' }; 57 | let unparsedFKey = unparsedFKeys[j].pg_get_constraintdef; 58 | // parse out foreign key 59 | let fKey = ''; 60 | let inFirstParens = false; 61 | for (let k = 0; k < unparsedFKey.length; k++) { 62 | let char = unparsedFKey[k]; 63 | if (char === ')') { 64 | inFirstParens = false; 65 | break; 66 | } 67 | if (inFirstParens === true) fKey += char; 68 | if (char === '(') inFirstParens = true; 69 | } 70 | // parse out reference table 71 | let keyword = 'REFERENCES '; 72 | let escapeChar = '('; 73 | let indexStartKeyword = unparsedFKey.indexOf(keyword); 74 | let fKeyRefUnsliced = unparsedFKey.slice( 75 | indexStartKeyword + keyword.length 76 | ); 77 | let indexOfEscapeChar = fKeyRefUnsliced.indexOf(escapeChar); 78 | let fKeyRef = fKeyRefUnsliced.slice(0, indexOfEscapeChar); 79 | fKeyObj.fKey = fKey; 80 | fKeyObj.refTable = fKeyRef; 81 | parsedFKeys.push(fKeyObj); 82 | } 83 | return parsedFKeys; 84 | }; 85 | 86 | const makeNodes = async (db: dbConstructor): Promise => { 87 | const arrOfNodes = []; 88 | if (db.dbType === 'PostgreSQL') { 89 | let tables = await db.queryTables(); 90 | for (let i = 0; i < tables.length; i++) { 91 | const node: nodeShape = { 92 | name: '', 93 | primaryKey: '', 94 | columns: [], 95 | edges: [], 96 | isIntersectionTable: false, 97 | }; 98 | node.name = `${tables[i].table_name}`; 99 | let unparsedPKey = await db.queryPKey(); 100 | node.primaryKey = parsePKey(unparsedPKey, node.name); 101 | let unparsedColumns = await db.queryColumns(node.name); 102 | node.columns = parseColumns(unparsedColumns); 103 | let unparsedFKeys = await db.queryFKeys(); 104 | node.edges = parseFKeys(unparsedFKeys, node.name); 105 | if (await isIntersectionTable(db, node.name)) node.isIntersectionTable = true; 106 | arrOfNodes.push(node); 107 | 108 | // test console logs to run these logs when invoked instead of having to write async-await tester func 109 | // console.log(unparsedFKeys) 110 | // console.log(node.name, node.edges) 111 | // console.log(node.name, unparsedColumns.length, node.edges.length) 112 | // console.log(parseFKeys(unparsedFKeys, node.name).length) 113 | } 114 | } 115 | // console.log(arrOfNodes); 116 | // console.log(arrOfNodes[0].edges) 117 | return { nodes: arrOfNodes }; 118 | }; 119 | 120 | // test instance with SWAPI DB 121 | // const sWAPI = new (dbInstance as any)( 122 | // 'postgres://eitysjmj:At82GArc1PcAD4nYgBoAODn0-XvBYo-A@peanut.db.elephantsql.com/eitysjmj' 123 | // ); 124 | // makeNodes(sWAPI); 125 | 126 | // const testQueries = async () => { 127 | // // test queryTables 128 | // // let queryResults = await sWAPI.queryTables(); 129 | // // return queryResults 130 | // // test queryPKey 131 | // // let queryResults = await sWAPI.queryPKey(); 132 | // // return parsePKey(queryResults, 'people')) 133 | // // test fKeys 134 | // // let queryResults = await sWAPI.queryFKeys(); 135 | // // return parseFKeys(queryResults, 'people'); 136 | // // test queryColumns 137 | // // let queryResults = await sWAPI.queryColumns('planets'); 138 | // // return parseColumns(queryResults) 139 | // } 140 | // console.log(testQueries()); 141 | 142 | export { parseColumns, parsePKey, parseFKeys, makeNodes }; 143 | -------------------------------------------------------------------------------- /server/middleware/auth.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import jwt from 'jsonwebtoken'; 3 | import { 4 | CreateNewAccountResponse, 5 | CreateNewUserObject, 6 | TokenJwt, 7 | } from '../types'; 8 | 9 | import { userController } from './userController'; 10 | 11 | /* 12 | This code is responsible for handling both login & register requests through OAuth 13 | To understand the OAuth flow start at the function handleOAuth() at the bottom of this file 14 | */ 15 | 16 | // Import github OAuth credentials from the environment variables 17 | const { GITHUB_OAUTH_CLIENT_ID, GITHUB_OAUTH_CLIENT_SECRET, JWT_SECRET } = 18 | process.env; 19 | 20 | /* 21 | Uses a github oauth code to exchange for an OAuth access token. 22 | This allows subsequent funcs to use this OAuth token to query the 23 | GitHub api on the authenticated user's behalf... 24 | */ 25 | export async function getOAuthToken( 26 | code: string 27 | ): Promise<{ gitHubToken: string }> { 28 | try { 29 | const gitHubOAuthAccessTokenUrl = 30 | 'https://github.com/login/oauth/access_token'; 31 | 32 | const params = { 33 | client_id: GITHUB_OAUTH_CLIENT_ID, 34 | client_secret: GITHUB_OAUTH_CLIENT_SECRET, 35 | code: code, 36 | }; 37 | 38 | const headers = { 39 | Accept: 'application/json', 40 | }; 41 | 42 | const accessTokenResponse = await axios.post( 43 | gitHubOAuthAccessTokenUrl, 44 | {}, 45 | { params, headers } 46 | ); 47 | 48 | return { gitHubToken: accessTokenResponse.data.access_token }; 49 | } catch (error) { 50 | console.log(error.message); 51 | return { gitHubToken: 'Invalid code' }; 52 | } 53 | } 54 | 55 | /* 56 | Uses the OAuth access token provided by getOAuthToken() to query the GitHub user 57 | endpoint and retrieve the authenticated user's data. Returns all the data github returns 58 | */ 59 | export async function getGitHubUserData(oauthAccessToken: string) { 60 | try { 61 | const { data } = await axios.get('https://api.github.com/user', { 62 | headers: { 63 | Authorization: `Bearer ${oauthAccessToken}`, 64 | Accept: 'application/json', 65 | }, 66 | }); 67 | return { gitHubUserData: data }; 68 | } catch (error) { 69 | console.log(error.message); 70 | } 71 | } 72 | 73 | /* 74 | Accepts the entire user object returned by getGitHubUserData() and constructs an object that is required to 75 | create a new QuiL user 76 | */ 77 | export function buildNewGitHubUserData(gitHubUserData: { 78 | login: string; 79 | avatar_url: string; 80 | name: string; 81 | }): CreateNewUserObject { 82 | return { 83 | oauthUser: true, 84 | name: gitHubUserData.name, 85 | username: gitHubUserData.login, 86 | avatarUrl: gitHubUserData.avatar_url, 87 | }; 88 | } 89 | 90 | /* 91 | Accepts a QuiL user object and encodes the user data into a JSON webtoken 92 | */ 93 | export function generateJWT(userObject: CreateNewAccountResponse): TokenJwt { 94 | const token = jwt.sign( 95 | { 96 | username: userObject.username, 97 | userId: userObject.userId, 98 | name: userObject.name, 99 | avatarUrl: userObject.avatarUrl, 100 | }, 101 | JWT_SECRET, 102 | { 103 | expiresIn: 3_600_000, 104 | } 105 | ); 106 | 107 | return { 108 | token, 109 | }; 110 | } 111 | 112 | /* 113 | Main fucntion for handling OAuth login & register. Based on the 'type' parameter, this func will either 114 | create a new user in QuiL's database using the Oauth information or will validate an exisiting user 115 | that is attempting to sign in via OAuth 116 | */ 117 | export async function handleOAuth( 118 | code: string, 119 | type: string 120 | ): Promise { 121 | try { 122 | const { gitHubToken } = await getOAuthToken(code); 123 | 124 | if (!gitHubToken) throw new Error('Bad credentials'); 125 | 126 | const { gitHubUserData } = await getGitHubUserData(gitHubToken); 127 | 128 | if (type === 'register') { 129 | const exisitingUser = await userController.getQuilUser( 130 | gitHubUserData.login 131 | ); 132 | 133 | if (exisitingUser) { 134 | return generateJWT(exisitingUser); 135 | } else { 136 | const newUserObj = buildNewGitHubUserData(gitHubUserData); 137 | const createdUser = await userController.createAccount(newUserObj); 138 | if (createdUser.success) { 139 | return generateJWT(createdUser); 140 | } else throw new Error('Error creating account'); 141 | } 142 | } 143 | 144 | if (type === 'signin') { 145 | const { login } = gitHubUserData; 146 | const user = await userController.getQuilUser(login); 147 | if (user.success) { 148 | return generateJWT(user); 149 | } else throw new Error('Error fetching account'); 150 | } 151 | } catch (error) { 152 | return { token: null }; 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /server/middleware/userController.ts: -------------------------------------------------------------------------------- 1 | const bcrypt = require('bcrypt'); 2 | import { quilDbConnection } from '../db/quilDBConnection'; 3 | import { 4 | SaveProject, 5 | SavedProjectRes, 6 | CreateNewUserObject, 7 | CreateNewAccountResponse, 8 | GetUser, 9 | } from '../types'; 10 | import * as dotenv from 'dotenv'; 11 | 12 | export const userController = { 13 | createAccount: async ( 14 | newUserObject: CreateNewUserObject 15 | ): Promise => { 16 | try { 17 | let values = []; 18 | if (newUserObject.oauthUser) { 19 | const { name, username, avatarUrl } = newUserObject; 20 | values = [username, name, avatarUrl, null]; 21 | } else { 22 | const { username, password } = newUserObject; 23 | const salt = await bcrypt.genSalt(10); 24 | const hashedPassword = await bcrypt.hash(password, salt); 25 | values = [username, null, null, hashedPassword]; 26 | } 27 | 28 | const query = `INSERT INTO users (username, name, avatar_url, password)\ 29 | VALUES ($1, $2, $3, $4) RETURNING *;`; 30 | 31 | const { rows } = await quilDbConnection.query(query, values); 32 | 33 | return { 34 | success: true, 35 | userId: rows[0]._id, 36 | username: rows[0].username, 37 | name: rows[0].name, 38 | avatarUrl: rows[0].avatar_url, 39 | }; 40 | } catch (err) { 41 | console.log(err.message); 42 | 43 | return { success: false, username: null, userId: null }; 44 | } 45 | }, 46 | saveProject: async (obj: SaveProject): Promise => { 47 | try { 48 | const { projectName, projectData, userId } = obj; 49 | // const salt = await bcrypt.genSalt(10); 50 | // const hashedProjectData = await bcrypt.hash(projectData, salt); 51 | const query = `INSERT INTO projects (name, saved_db, owner_id)\ 52 | VALUES ( $1, $2, (SELECT _id FROM users WHERE _id = $3)) RETURNING *;`; 53 | const values = [projectName, projectData, userId]; 54 | const { rows } = await quilDbConnection.query(query, values); 55 | return { projectName: projectName, success: true }; 56 | } catch (err) { 57 | return { success: false }; 58 | } 59 | }, 60 | getUserProject: async (userId: Number) => { 61 | try { 62 | const query = `SELECT * FROM projects WHERE owner_id = $1;`; 63 | const values = [userId]; 64 | const { rows } = await quilDbConnection.query(query, values); 65 | const resObj: any = { 66 | db: [], 67 | success: true, 68 | }; 69 | rows.forEach((el: any) => { 70 | resObj.db.push(el); 71 | }); 72 | return resObj; 73 | } catch (err) { 74 | return { success: false }; 75 | } 76 | }, 77 | validateUser: async (isUser: GetUser): Promise => { 78 | try { 79 | const { username, password } = isUser; 80 | const query = `SELECT * FROM users WHERE username = $1;`; 81 | const values = [username]; 82 | const { rows } = await quilDbConnection.query(query, values); 83 | if (rows.length === 0) 84 | return { success: false, username: null, userId: null }; 85 | else { 86 | const hashPass = rows[0].password; 87 | const result = await bcrypt.compare(password, hashPass); 88 | if (!result) return { success: false, username: null, userId: null }; 89 | return { 90 | success: true, 91 | userId: rows[0]._id, 92 | username: rows[0].username, 93 | name: rows[0].name, 94 | avatarUrl: rows[0].avatar_url, 95 | }; 96 | } 97 | } catch (err) { 98 | console.log(err, ' inside validate catch block'); 99 | } 100 | }, 101 | getQuilUser: async (username: string): Promise => { 102 | try { 103 | const queryString = 'SELECT * FROM users WHERE username = $1'; 104 | const values = [username]; 105 | const { rows } = await quilDbConnection.query(queryString, values); 106 | return { 107 | success: true, 108 | username: rows[0].username, 109 | userId: rows[0]._id, 110 | name: rows[0].name, 111 | avatarUrl: rows[0].avatar_url, 112 | }; 113 | } catch (error) { 114 | console.log(error.message); 115 | 116 | return { success: false, username: null, userId: null }; 117 | } 118 | }, 119 | deleteProject: async (projectId: number): Promise => { 120 | try { 121 | const queryString = 'DELETE FROM projects WHERE _id = $1;'; 122 | const values = [projectId]; 123 | const { rows } = await quilDbConnection.query(queryString, values); 124 | return { 125 | deleted: true, 126 | }; 127 | } catch (error) {} 128 | }, 129 | }; 130 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "quil", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "./dist/server.js", 6 | "scripts": { 7 | "build": "next build", 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "dev": "concurrently \"next dev\" \"ts-node-dev server/server.ts\"", 10 | "start": "node dist/server.js" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/oslabs-beta/QuiL.git" 15 | }, 16 | "dependencies": { 17 | "@ant-design/icons": "^4.8.0", 18 | "@apollo/server": "^4.1.1", 19 | "@codemirror/lang-javascript": "^6.1.1", 20 | "@graphql-tools/load-files": "^6.6.1", 21 | "@graphql-tools/merge": "^8.3.11", 22 | "@lezer/highlight": "^1.1.2", 23 | "@uiw/codemirror-theme-dracula": "^4.13.2", 24 | "@uiw/codemirror-themes": "^4.13.2", 25 | "@uiw/react-codemirror": "^4.13.2", 26 | "axios": "^1.1.3", 27 | "bcrypt": "^5.1.0", 28 | "body-parser": "^1.20.1", 29 | "cors": "^2.8.5", 30 | "daisyui": "^2.40.1", 31 | "dotenv": "^16.0.3", 32 | "eslint-config-next": "^13.0.3", 33 | "express": "^4.18.2", 34 | "framer-motion": "^7.6.7", 35 | "graphql": "^16.6.0", 36 | "graphql-type-json": "^0.3.2", 37 | "jsonwebtoken": "^8.5.1", 38 | "jwt-decode": "^3.1.2", 39 | "next": "^13.0.3", 40 | "node-typescript": "^0.1.3", 41 | "pg": "^8.8.0", 42 | "pluralize": "^8.0.0", 43 | "randomstring": "^1.2.3", 44 | "react": "^18.2.0", 45 | "react-dom": "^18.2.0", 46 | "react-toastify": "^9.1.1", 47 | "reactflow": "^11.2.0", 48 | "ts-node": "^10.9.1", 49 | "tsc": "^2.0.4" 50 | }, 51 | "devDependencies": { 52 | "@babel/preset-typescript": "^7.18.6", 53 | "@jest/globals": "^29.3.1", 54 | "@tailwindcss/typography": "^0.5.8", 55 | "@types/cors": "^2.8.12", 56 | "@types/express": "^4.17.14", 57 | "@types/jsonwebtoken": "^8.5.9", 58 | "@types/node": "18.11.9", 59 | "@types/pg": "^8.6.5", 60 | "@types/react": "18.0.25", 61 | "autoprefixer": "^10.4.13", 62 | "concurrently": "^7.5.0", 63 | "cypress": "^11.2.0", 64 | "jest": "^29.3.1", 65 | "nodemon": "^2.0.20", 66 | "postcss": "^8.4.18", 67 | "tailwindcss": "^3.2.2", 68 | "ts-jest": "^29.0.3", 69 | "ts-node": "^10.9.1", 70 | "ts-node-dev": "^2.0.0", 71 | "typescript": "^4.8.4" 72 | }, 73 | "keywords": [], 74 | "author": "", 75 | "license": "ISC", 76 | "bugs": { 77 | "url": "https://github.com/oslabs-beta/QuiL/issues" 78 | }, 79 | "homepage": "https://github.com/oslabs-beta/QuiL#readme" 80 | } 81 | -------------------------------------------------------------------------------- /server/resolverGenerator.ts: -------------------------------------------------------------------------------- 1 | const pluralize = require('pluralize'); 2 | // TODO: Combine and refactor Type definitions 3 | import { ArgType, ResolverStrings, TableResolver } from './types'; 4 | import { quilDbConnection as db } from './db/quilDBConnection'; 5 | 6 | /* 7 | This function creates the actual function defintions for resolvers related to the user's data base. 8 | This allows the functions definitions to be passed to makeResolverStrings which stringifies the 9 | defintions so they can be sent to the frontend 10 | */ 11 | 12 | export type node = { 13 | name: string; 14 | primaryKey: string; 15 | columns: columns; 16 | edges: edge[]; 17 | }; 18 | 19 | export type edge = { 20 | fKey: string; 21 | refTable: string; 22 | }; 23 | export type columns = column[]; 24 | 25 | export type column = { 26 | columnName: string; 27 | dataType: string; 28 | }; 29 | 30 | const makeResolverFunctions = (node: node): TableResolver => { 31 | const getOne = async (_: any, args: ArgType) => { 32 | const query = `SELECT * FROM ${node.name} WHERE ${node.primaryKey} = $1`; 33 | const values = [args._id]; 34 | const { rows } = await db.query(query, values); 35 | return rows; 36 | }; 37 | 38 | const getAll = async () => { 39 | const query = `SELECT * FROM ${node.name}`; 40 | const values = [node.name]; 41 | const { rows } = await db.query(query, values); 42 | return rows; 43 | }; 44 | 45 | return { 46 | getOne, 47 | getAll, 48 | }; 49 | }; 50 | /* 51 | This function resolves the template literals and formats the fucntions in a string form so they 52 | can be easily displayed in the code mirrors on the frontend 53 | */ 54 | const makeResolverStrings = ( 55 | node: node, 56 | resolvers: TableResolver 57 | ): ResolverStrings => { 58 | let singular = pluralize.singular(node.name); 59 | if (singular === node.name) singular = singular + 'ById'; 60 | const getOneString = 61 | `${singular}: ` + 62 | resolvers.getOne 63 | .toString() 64 | .replace(/\${node.name}/, node.name) 65 | .replace(/\${node.primaryKey}/, node.primaryKey); 66 | 67 | const resolver = 68 | `\n ${node.name}: ` + 69 | resolvers.getAll.toString().replace(/\${node.name}/, node.name) + 70 | `\n\n ${getOneString}`; 71 | 72 | return { 73 | tableName: node.name, 74 | resolver, 75 | }; 76 | }; 77 | 78 | export { makeResolverFunctions, makeResolverStrings }; 79 | -------------------------------------------------------------------------------- /server/schemaGenerator.ts: -------------------------------------------------------------------------------- 1 | import { dbConstructor, schema, pSQLToGQL, SingleSchemaType } from './types'; 2 | // const pluralize = require('pluralize'); 3 | // const { query } = require('express'); 4 | // const dbInstance = require('./db/dbConnection'); 5 | // const parsePKey = require ('./helperFunctions'); 6 | 7 | // import * as pluralize from "pluralize"; 8 | const pluralize = require('pluralize'); 9 | import { query } from 'express'; 10 | import { dbInstance } from './db/dbConnection'; 11 | import { 12 | parseColumns, 13 | parsePKey, 14 | parseFKeys, 15 | makeNodes, 16 | } from './helperFunctions'; 17 | import { Schema } from 'inspector'; 18 | 19 | // generates schemas given a database 20 | export const generateSchemas = async ( 21 | db: dbConstructor 22 | ): Promise => { 23 | let tables = await db.queryTables(); 24 | const schemas: schema[] = []; 25 | // to create initial schemas per table 26 | for (let i = 0; i < tables.length; i++) { 27 | let tableName = tables[i].table_name; 28 | const schema: schema = { table_name: tableName }; 29 | let tableQuery = await db.queryTableLayout(tableName); 30 | for (let j = 0; j < tableQuery.length; j++) { 31 | let column = tableQuery[j].column_name; 32 | let dataType = convertToGQL( 33 | tableQuery[j].data_type, 34 | tableQuery[j].is_nullable 35 | ); 36 | schema[column] = dataType; 37 | } 38 | // primary keys have to become "ID!" in GQL 39 | let unparsedPKey = await db.queryPKey(); 40 | let pKey = parsePKey(unparsedPKey, tableName); 41 | schema[pKey] = 'ID!'; 42 | schemas.push(schema); 43 | } 44 | // iterate again to append onto schemas: key-value pairs of table-instance (ex: People: [Person] in Planet schema) 45 | for (let j = 0; j < schemas.length; j++) { 46 | let schema = schemas[j]; 47 | let isIntTable = await isIntersectionTable(db, schema.table_name); 48 | let edges; 49 | let unparsedFKeys = await db.queryFKeys(); 50 | edges = parseFKeys(unparsedFKeys, schema.table_name); 51 | for (let k = 0; k < edges.length; k++) { 52 | for (let l = 0; l < schemas.length; l++) { 53 | if (edges[k].refTable === schemas[l].table_name) { 54 | // if a table is not an intersection table, then find its refTables and add key-value pair: tableName: singularizedTableName 55 | if (isIntTable === false) 56 | schemas[l][schema.table_name] = `[${formatStr(schema.table_name)}]`; 57 | else { 58 | for (let m = 0; m < edges.length; m++) { 59 | schemas[l][edges[m].refTable] = `[${formatStr( 60 | edges[m].refTable 61 | )}]`; 62 | } 63 | } 64 | } 65 | } 66 | } 67 | } 68 | // delete self-references in schemas, replace foreign keys with refTable instances, and delete all intersection tables from schemas array 69 | for (let n = 0; n < schemas.length; n++) { 70 | // delete self-references in schemas 71 | let tableName = tables[n].table_name; 72 | delete schemas[n][tableName]; 73 | // per table, loop through FKeys and replace with ref table instance 74 | let unparsedFKeys = await db.queryFKeys(); 75 | let edges = parseFKeys(unparsedFKeys, schemas[n].table_name); 76 | for (let o = 0; o < edges.length; o++) { 77 | delete schemas[n][edges[o].fKey]; 78 | schemas[n][edges[o].refTable] = `[${formatStr(edges[o].refTable)}]`; 79 | } 80 | // delete all intersection tables from schemas array 81 | let isIntTable = await isIntersectionTable(db, schemas[n].table_name); 82 | if (isIntTable === true) delete schemas[n]; 83 | } 84 | // need to get rid of undefined obj 85 | const noUndefinedArr = []; 86 | for (let p = 0; p < schemas.length; p++) { 87 | if (schemas[p] === undefined) continue; 88 | noUndefinedArr.push(schemas[p]); 89 | } 90 | // console.log(schemas); 91 | return formatSchemas(noUndefinedArr); 92 | }; 93 | 94 | // function to identify whether or not a table is an intersection table (table with all foreign keys) 95 | export const isIntersectionTable = async ( 96 | db: dbConstructor, 97 | tableName: string 98 | ): Promise => { 99 | const unparsedColumns = await db.queryColumns(tableName); 100 | let numColumns = unparsedColumns.length; 101 | let unparsedFKeys = await db.queryFKeys(); 102 | let numFKeys = parseFKeys(unparsedFKeys, tableName).length; 103 | // need to subtract 1 for primary key 104 | if (numColumns - 1 === numFKeys) return true; 105 | return false; 106 | }; 107 | 108 | // function to singularize a word and capitalize its first letter (ex: People --> 'Person') 109 | const formatStr = (str: string) => { 110 | // singularize word 111 | let singular = pluralize.singular(str); 112 | // capitalize first letter and return 113 | return `${singular[0].toUpperCase() + singular.slice(1)}`; 114 | }; 115 | 116 | // function to convert PostgreSQL data types into GraphQL data types 117 | const convertToGQL = (dataType: string, nullable: string): string => { 118 | dataType = pSQLToGQL[dataType]; 119 | if (nullable === 'NO') dataType += '!'; 120 | return dataType; 121 | }; 122 | 123 | // object to convert postgreSQL datatypes into GraphQL datatypes 124 | const pSQLToGQL: pSQLToGQL = { 125 | 'character varying': 'String', 126 | integer: 'Int', 127 | bigint: 'Int', 128 | date: 'String', 129 | }; 130 | 131 | // function to format one schema object 132 | const formatSchema = (schema: schema): string => { 133 | let returnStr = ''; 134 | returnStr += `type ${formatStr(schema.table_name)} {`; 135 | // add rest of contents of object except for table_name property 136 | for (let key in schema) { 137 | // ignore table_name property 138 | if (key === 'table_name') continue; 139 | // stringify rest and add to returnStr 140 | returnStr += `\n ${key}: ${schema[key]}`; 141 | } 142 | returnStr += '\n}'; 143 | return returnStr; 144 | }; 145 | 146 | // function to stringfy the entire array of schemas 147 | const formatSchemas = (schemasArray: schema[]): SingleSchemaType[] => { 148 | let returnArr = []; 149 | let returnStr = ''; 150 | for (let i = 0; i < schemasArray.length; i++) { 151 | returnStr = ''; 152 | returnStr += formatSchema(schemasArray[i]); 153 | returnStr += '\n \n'; 154 | returnArr.push({ 155 | tableName: schemasArray[i].table_name, 156 | schemas: returnStr, 157 | }); 158 | } 159 | return returnArr; 160 | }; 161 | 162 | const db = new (dbInstance as any)( 163 | 'postgres://eitysjmj:At82GArc1PcAD4nYgBoAODn0-XvBYo-A@peanut.db.elephantsql.com/eitysjmj' 164 | ); 165 | 166 | /* 167 | Loop through foreign keys query, specifically the pg_get_constraintdef strings 168 | save all pg_get_constraintdef strings in an array 169 | loop through tableNames and check if any of the array of strings includes that tableName 170 | if so, remember the table name associated with pg_get_constraintdef string 171 | grab any OTHER foreign keys in that table, and create a key-value pair: tableName: singularized version of tableName 172 | */ 173 | 174 | // Tests: 175 | 176 | // const testIntTable = async (db: any, tableName: any): Promise => { 177 | // let x = await isIntersectionTable(db, tableName); 178 | // console.log(x); 179 | // return x; 180 | // } 181 | 182 | // let tableName1 = 'planets_in_films'; 183 | // let tableName2 = 'planets'; 184 | // testIntTable(db, tableName1); 185 | // testIntTable(db, tableName2); 186 | 187 | // const test = async (db: any) => { 188 | // // let check = await makeNodes(db); 189 | // isIntersectionTable("planets_in_films"); 190 | // isIntersectionTable("planets"); 191 | // } 192 | -------------------------------------------------------------------------------- /server/server.ts: -------------------------------------------------------------------------------- 1 | import { ApolloServer } from '@apollo/server'; 2 | import { expressMiddleware } from '@apollo/server/express4'; 3 | import { ApolloServerPluginDrainHttpServer } from '@apollo/server/plugin/drainHttpServer'; 4 | import express from 'express'; 5 | import http from 'http'; 6 | import cors from 'cors'; 7 | import bodyParser from 'body-parser'; 8 | import * as dotenv from 'dotenv'; 9 | dotenv.config(); 10 | 11 | // Importing the combined type and resolver definitions to import to Apollo Server 12 | import { typeDefs, resolvers } from './graphql/modelsSetup'; 13 | // Type definitions 14 | import { MyContext } from './types'; 15 | 16 | /* 17 | This function defines the logic needed to initiate an Apollo GraphQL Server 18 | Apollo here builds on top of an express server 19 | */ 20 | async function startApolloServer() { 21 | const app = express(); 22 | app.use(express.urlencoded({ extended: false })); 23 | app.use((req, res, next) => { 24 | res.setHeader('Access-Control-Allow-Origin', '*'); 25 | res.setHeader('Access-Control-Allow-Credentials', 'true'); 26 | res.setHeader('Access-Control-Allow-Methods', 'GET,HEAD,OPTIONS,POST,PUT'); 27 | res.setHeader( 28 | 'Access-Control-Allow-Headers', 29 | 'Access-Control-Allow-Headers, Origin,Accept, X-Requested-With, Content-Type, Access-Control-Request-Method, Access-Control-Request-Headers' 30 | ); 31 | res.status(200); 32 | next(); 33 | }); 34 | // Our httpServer handles incoming requests to our Express app. 35 | // Below, we tell Apollo Server to "drain" this httpServer, 36 | // enabling our servers to shut down gracefully. 37 | const httpServer = http.createServer(app); 38 | 39 | // Same ApolloServer initialization as before, plus the drain plugin 40 | // for our httpServer.ww 41 | // We pass our merged type and resolvers definitions here 42 | const server = new ApolloServer({ 43 | typeDefs, 44 | resolvers, 45 | plugins: [ApolloServerPluginDrainHttpServer({ httpServer })], 46 | }); 47 | // Ensure we wait for our server to start 48 | await server.start(); 49 | 50 | // Set up our Express middleware to handle CORS, body parsing, 51 | // and our expressMiddleware function. 52 | app.use( 53 | '/graphql', 54 | cors(), 55 | bodyParser.json(), 56 | expressMiddleware(server, { 57 | context: async ({ req, res }) => { 58 | return { user: res.locals }; 59 | }, 60 | }) 61 | ); 62 | 63 | app.get('/', (req, res) => { 64 | res.send('hi'); 65 | }); 66 | // Modified server startup 67 | await new Promise(resolve => 68 | httpServer.listen({ port: 4000 }, resolve) 69 | ); 70 | // Display a log to notify that the GQL server is up and running 71 | console.log(`🪶 GraphQL server ready at http://localhost:4000/ 🪶`); 72 | } 73 | 74 | startApolloServer(); 75 | 76 | export { startApolloServer }; 77 | -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "lib": ["dom", "dom.iterable", "esnext", "es2020"], 5 | "noImplicitAny": true, 6 | "allowJs": true, 7 | "sourceMap": true, 8 | "skipLibCheck": true, 9 | "strict": false, 10 | "forceConsistentCasingInFileNames": true, 11 | "incremental": true, 12 | "esModuleInterop": true, 13 | "module": "CommonJS", 14 | "moduleResolution": "node", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "types": ["node"], 18 | "jsx": "preserve", 19 | "outDir": "./dist", 20 | "plugins": [ 21 | { 22 | "name": "next" 23 | } 24 | ], 25 | }, 26 | "include": ["**/*.ts"], 27 | "exclude": ["node_modules"] 28 | } 29 | -------------------------------------------------------------------------------- /server/types.ts: -------------------------------------------------------------------------------- 1 | export type GlobalServerError = { 2 | log: string; 3 | status: number; 4 | message: { err: string }; 5 | }; 6 | 7 | export type dbConstructor = { 8 | new (s: string): object; 9 | dbType: string; 10 | query: Function; 11 | pool: { query: Function }; 12 | queryTables: Function; 13 | queryColumns: Function; 14 | queryPKey: Function; 15 | queryFKeys: Function; 16 | queryTableLayout: Function; 17 | }; 18 | 19 | export type unparsedColumnShape = { 20 | column_name: string; 21 | data_type: string; 22 | }; 23 | 24 | export type parsedColumnShape = { 25 | columnName: string; 26 | dataType: string; 27 | }; 28 | 29 | export type unparsedKeys = { 30 | table_name: string; 31 | pg_get_constraintdef: string; 32 | }; 33 | 34 | export type parsedFKeys = { 35 | fKey: string; 36 | refTable: string; 37 | }; 38 | 39 | export type nodeShape = { 40 | name: string; 41 | primaryKey: string; 42 | columns: parsedColumnShape[]; 43 | edges: parsedFKeys[]; 44 | isIntersectionTable: boolean; 45 | }; 46 | 47 | export type objectOfArrOfNodes = { 48 | nodes: nodeShape[]; 49 | }; 50 | 51 | // TODO: change schema types 52 | export type schema = { 53 | [key: string]: any; 54 | }; 55 | 56 | export type pSQLToGQL = { 57 | [key: string]: string; 58 | }; 59 | 60 | // Server Types 61 | export type QuiLData = { 62 | nodes: nodeShape[]; 63 | resolvers: ResolverStrings[]; 64 | schemas: SingleSchemaType[]; 65 | }; 66 | 67 | export type TableResolver = { 68 | getOne: Function; 69 | getAll: Function; 70 | }; 71 | 72 | export type ResolverStrings = { 73 | tableName: string; 74 | resolver: string; 75 | }; 76 | 77 | export type TableResolver1 = Function; 78 | 79 | export interface ArgType { 80 | uri?: string; 81 | _id?: string; 82 | node?: nodeShape; 83 | } 84 | 85 | export type SingleSchemaType = { 86 | tableName: string; 87 | schemas: string; 88 | }; 89 | 90 | export type OAuthArgs = { 91 | code: string; 92 | oauthType: string; 93 | }; 94 | 95 | export type JWTResponse = { 96 | token: string; 97 | }; 98 | // andres added newuser 99 | export type CreateNewUserObject = { 100 | oauthUser: boolean; 101 | username: string; 102 | name?: string; 103 | avatarUrl?: string; 104 | password?: string; 105 | }; 106 | 107 | export type SaveProject = { 108 | projectName: string; 109 | projectData: string; 110 | userId: string; 111 | }; 112 | 113 | export type CreateNewAccountResponse = { 114 | success: boolean; 115 | username: string; 116 | userId: number; 117 | name?: string; 118 | avatarUrl?: string; 119 | }; 120 | 121 | export type SavedProjectRes = { 122 | projectName?: string; 123 | success: boolean; 124 | projectId?: number; 125 | }; 126 | 127 | export type GetUserProjectRes = { 128 | saved_db?: []; 129 | success: boolean; 130 | }; 131 | 132 | export type GetUser = { 133 | username: string; 134 | password: string; 135 | }; 136 | 137 | export type GetUserRes = { 138 | id_?: number; 139 | success: boolean; 140 | }; 141 | 142 | export interface MyContext { 143 | token?: String; 144 | } 145 | 146 | export type TokenJwt = { 147 | token: string; 148 | }; -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./app/**/*.{js,ts,jsx,tsx}", // Note the addition of the `app` directory. 5 | ], 6 | theme: { 7 | extend: {}, 8 | }, 9 | daisyui: { 10 | styled: true, 11 | themes: ["night", "light", "retro", "cyberpunk", "synthwave", "pastel"], 12 | }, 13 | plugins: [require("@tailwindcss/typography"), require("daisyui")], 14 | }; 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext", 8 | "es2020" 9 | ], 10 | "noImplicitAny": true, 11 | "allowJs": true, 12 | "sourceMap": true, 13 | "skipLibCheck": true, 14 | "strict": false, 15 | "forceConsistentCasingInFileNames": true, 16 | "incremental": true, 17 | "esModuleInterop": true, 18 | "module": "CommonJS", 19 | "moduleResolution": "node", 20 | "resolveJsonModule": true, 21 | "isolatedModules": true, 22 | "types": [ 23 | "node" 24 | ], 25 | "jsx": "preserve", 26 | "outDir": "./dist", 27 | "plugins": [ 28 | { 29 | "name": "next" 30 | } 31 | ], 32 | "noEmit": true 33 | }, 34 | "include": [ 35 | "next-env.d.ts", 36 | ".next/types/**/*.ts", 37 | "**/*.ts", 38 | "**/*.tsx", 39 | "app/Main/Login", 40 | "server/server.js", 41 | "server/middleware/userController.ts", 42 | "app/Main/Chart/page.jsx", 43 | "app/Main/Chart/page.jsx" 44 | ], 45 | "exclude": [ 46 | "node_modules" 47 | ] 48 | } 49 | --------------------------------------------------------------------------------