├── .eslintrc.json ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── main.yml │ └── test.yml ├── .gitignore ├── .vscode └── settings.json ├── CODE_OF_CONDUCT.md ├── Dockerfile ├── LICENSE ├── README.md ├── __tests__ ├── enzyme.js ├── gitignore.test.js └── server.test.js ├── client ├── App.tsx ├── Components │ ├── BioComponent.tsx │ ├── FeatureComponent.tsx │ ├── FooterComponent.tsx │ ├── HeaderComponent.tsx │ ├── LeftDemoComponent.tsx │ ├── NavComponent.tsx │ ├── RightDemoComponent.tsx │ └── SchemaComponent.tsx ├── Containers │ ├── BioContainer.tsx │ ├── DemoContainer.tsx │ └── FlowContainer.tsx ├── Dashboard.tsx ├── Router.tsx ├── assets │ ├── demo1.gif │ ├── demo2.gif │ ├── demo3.gif │ ├── demo4.gif │ ├── favicon.ico │ ├── jennifer_chau_headshot.jpg │ ├── john_lin_headshot.jpg │ ├── johnnybryan.jpg │ ├── logo.png │ └── taras.jpg ├── index.tsx └── style.css ├── docs ├── object-directory.txt └── sql-notes.txt ├── index.html ├── jest.config.ts ├── package.json ├── server ├── controllers │ ├── GQLController.ts │ ├── SQLController.ts │ └── controllerTypes.ts ├── converters │ ├── converterTypes.ts │ ├── mutationConverter.ts │ ├── queryConverter.ts │ ├── resolvers.ts │ └── typeConverter.ts ├── router.ts ├── schema.ts ├── server.ts └── utils │ └── helperFunc.ts ├── tsconfig.json └── webpack.config.ts /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true, 5 | "es2021": true, 6 | "node": true 7 | }, 8 | "extends": [ 9 | "eslint:recommended", 10 | "plugin:react/recommended", 11 | "plugin:@typescript-eslint/recommended", 12 | "airbnb" 13 | ], 14 | "parser": "@typescript-eslint/parser", 15 | "parserOptions": { 16 | "ecmaFeatures": { 17 | "jsx": true 18 | }, 19 | "ecmaVersion": "latest" 20 | }, 21 | "plugins": [ 22 | "react", 23 | "@typescript-eslint" 24 | ], 25 | "rules": { 26 | "react/function-component-definition": "off", 27 | "react/jsx-filename-extension": "off", 28 | "no-console": "off", 29 | "no-unused-vars": "off", 30 | "@typescript-eslint/no-unused-vars": "off", 31 | "@typescript-eslint/no-var-requires": "off", 32 | "import/extensions": [ 33 | "error", 34 | "ignorePackages", 35 | { "js": "never", "jsx": "never", "ts": "never", "tsx": "never" } 36 | ], 37 | "eol-last": "off", 38 | "arrow-body-style": "off", 39 | "no-trailing-spaces": "off", 40 | "guard-for-in" : "off", 41 | "no-restricted-syntax": "off", 42 | "no-undef": "off", 43 | "import/no-import-module-exports": "off", 44 | "no-underscore-dangle": "off" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Deployment From Github To AWS 2 | on: 3 | push: 4 | branches: 5 | - main 6 | jobs: 7 | deploy: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout Latest Repo 11 | uses: actions/checkout@v2 12 | 13 | - name: Generate Deployment Package 14 | run: zip -r deploy.zip * -x "**node_modules**" 15 | 16 | - name: Deploy to EB 17 | uses: einaregilsson/beanstalk-deploy@v19 18 | with: 19 | aws_access_key: ${{ secrets.AWS_ACCESS_KEY_ID }} 20 | aws_secret_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 21 | application_name: ArtemisQL 22 | environment_name: Artemisql-env 23 | version_label: "artemisql-v1.2" 24 | region: us-east-1 25 | deployment_package: deploy.zip 26 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Testing Suite 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | - dev 8 | 9 | jobs: 10 | test: 11 | name: Testing our application against our testing suite. 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Use Node.js 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: '16.x' 20 | - name: npm install and test 21 | run: | 22 | npm install 23 | npm test 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | package-lock.json 2 | .env 3 | build 4 | .DS_STORE 5 | node_modules -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "workbench.colorCustomizations": { 3 | "activityBar.background": "#0301A3", 4 | "titleBar.activeBackground": "#0401E4", 5 | "titleBar.activeForeground": "#FAFAFF" 6 | } 7 | } -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | . 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16.13 2 | WORKDIR /usr/src/app 3 | COPY . /usr/src/app 4 | RUN npm install 5 | RUN npm run build 6 | EXPOSE 3000 7 | ENTRYPOINT ["node", "--loader", "ts-node/esm", "server/server.ts"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 OSLabs Beta 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

ArtemisQL

4 |

A GraphQL migration tool and relational database visualizer

5 | 6 | [![Contributors][contributors-shield]][contributors-url] 7 | [![Stargazers][stars-shield]][stars-url] 8 | [![Issues][issues-shield]][issues-url] 9 | [![LinkedIn][linkedin-shield]][linkedin-url] 10 |
11 | 12 | ## 🔎 Overview 13 | ArtemisQL is an open-source web application providing a SQL (Postgres) database GUI and custom-generated GraphQL schema (type defs, queries, mutations) and resolvers created by developers for developers, to ease the transition from REST to GraphQL. 14 | 15 | Read more on Medium.
16 | Accelerated by OS Labs. 17 | 18 | ## ⚙️ Getting Started 19 | ### Visit ArtemisQL.io to utilize the tool. 20 | 21 | #### Connect to a database 22 | * Input your PostgreSQL URI 23 | * OR use the sample database to view data rendered in an interactive diagram. 24 | 25 |
26 | 27 | #### Visualize your data 28 | * Easily view the relationships between the tables via the links that highlight the foreign key constraints. 29 | * Move any table and arrange them to optimally view the structure of the database and the relationships between the tables. 30 | 31 |
32 | 33 | #### Generate GraphQL Schema 34 | * View the generated GraphQL schema, including the types and associated resolvers. 35 | * Use the copy button to effortlessly integrate the code into your project. 36 | 37 |
38 | 39 | #### GraphQL Sandbox 40 | * Interactively construct full queries using the sample database. 41 | * Use the "Docs" to explore the possible queries, fields, types, mutations, and more. 42 | 43 |
44 | 45 | ## 🏗️ For Developers - How to Contribute 46 | We would love for you to test our application and submit any issues you encouter. Please feel free to fork your own repository to and submit your own pull requests. 47 | 48 | How you can contribute: 49 | - Submitting or resolving GitHub issues 50 | - Implementing features 51 | - Helping market our application 52 | 53 | Please make sure you have the following: 54 | - [NodeJS](https://nodejs.org/en/) 55 | - [NPM ](https://www.npmjs.com/) 56 | 57 | 1. Clone the repo. 58 | ```sh 59 | git clone https://github.com/oslabs-beta/ArtemisQL.git 60 | ``` 61 | 2. Install the package dependencies. 62 | ```sh 63 | npm install 64 | ``` 65 | 3. Create an `.env` file in the project root directory and initialize PG_URI constant. If you want to use your own PostgresQL database, feel free to put your URI here. If you would like to use our sample Starwars database, please contact us at helloartemisql@gmail.com. 66 | ```sh 67 | PG_URI= 68 | ``` 69 | 4. To run the application in development mode, please run following command and navigate to http://localhost:8080/. 70 | 71 | ```sh 72 | npm run dev 73 | ``` 74 | 75 | 5. To run the application in production mode, please run the following commands and navigate to http://localhost:3000/. 76 | ```sh 77 | npm start 78 | 79 | npm run build 80 | ``` 81 | 82 | 6. To run the application against our testing suite, please run the following command. 83 | ```sh 84 | npm run test 85 | ``` 86 | ## 🧬 Built With 87 | 88 | - [React](https://reactjs.org/) 89 | - [React Flow](https://reactflow.dev/) 90 | - [React Router](https://reactrouter.com/) 91 | - [Material UI](https://mui.com/) 92 | - [GraphQL](https://graphql.org/) 93 | - [TypeScript](https://www.typescriptlang.org/) 94 | - [Node](https://nodejs.org/) 95 | - [Express](https://expressjs.com/) 96 | - [PostgreSQL](https://www.postgresql.org/) 97 | 98 | ## 🤖 Developers 99 | 100 | [![JohnnyBryan][johnny-bryan-shield]][johnny-bryan-linkedin-url]
101 | [![JenniferChau][jennifer-chau-shield]][jennifer-chau-linkedin-url]
102 | [![JohnLin][john-lin-shield]][john-lin-linkedin-url]
103 | [![TarasSukhoverskyi][taras-sukhoverskyi-shield]][taras-sukhoverskyi-linkedin-url] 104 | 105 | 106 | ## License 107 | This product is licensed under the MIT License. 108 | 109 | [contributors-shield]: https://img.shields.io/github/contributors/oslabs-beta/artemisql.svg?style=for-the-badge 110 | [contributors-url]: https://github.com/oslabs-beta/artemisql/graphs/contributors 111 | [stars-shield]: https://img.shields.io/github/stars/oslabs-beta/artemisql.svg?style=for-the-badge 112 | [stars-url]: https://github.com/oslabs-beta/artemisql/stargazers 113 | [issues-shield]: https://img.shields.io/github/issues/oslabs-beta/artemisql.svg?style=for-the-badge 114 | [issues-url]: https://github.com/oslabs-beta/artemisql/issues 115 | [linkedin-shield]: https://img.shields.io/badge/-LinkedIn-black.svg?style=for-the-badge&logo=linkedin&colorB=555 116 | [linkedin-url]: https://www.linkedin.com/company/artemisql 117 | 118 | [johnny-bryan-shield]: https://img.shields.io/badge/-Johnny%20Bryan-black.svg?style=for-the-badge&logo=linkedin&colorB=555 119 | [johnny-bryan-linkedin-url]: https://www.linkedin.com/in/john-bryan-10a3bbb9/ 120 | [jennifer-chau-shield]: https://img.shields.io/badge/-Jennifer%20Chau-black.svg?style=for-the-badge&logo=linkedin&colorB=555 121 | [jennifer-chau-linkedin-url]: https://www.linkedin.com/in/jenniferchau512/ 122 | [john-lin-shield]: https://img.shields.io/badge/-John%20Lin-black.svg?style=for-the-badge&logo=linkedin&colorB=555 123 | [john-lin-linkedin-url]: https://www.linkedin.com/in/john-lin-/ 124 | [taras-sukhoverskyi-shield]: https://img.shields.io/badge/-Taras%20Sukhoverskyi-black.svg?style=for-the-badge&logo=linkedin&colorB=555 125 | [taras-sukhoverskyi-linkedin-url]: https://www.linkedin.com/in/taras-sukhoverskyi-628642145/ 126 | -------------------------------------------------------------------------------- /__tests__/enzyme.js: -------------------------------------------------------------------------------- 1 | // import React from 'react'; 2 | // import { expect } from 'chai'; 3 | // import { shallow } from 'enzyme'; 4 | // import sinon from 'sinon'; 5 | 6 | // import BioComponent from ''; 7 | // import TeamBiosComponent from ''; 8 | 9 | // describe('', () => { 10 | // it('renders four components', () => { 11 | // const wrapper = shallow(); 12 | // expect(wrapper.find(BioComponent)).to.have.lengthOf(4); 13 | // }); -------------------------------------------------------------------------------- /__tests__/gitignore.test.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | const gitignore = path.resolve(__dirname, '../.gitignore'); 5 | 6 | describe('Testing Git Ignore File', () => { 7 | const data = fs.readFileSync(gitignore, 'utf8'); 8 | 9 | it('Check if .env is included.', () => { 10 | expect(data).toContain('.env'); 11 | }); 12 | 13 | it('Check if package-lock.json is included.', () => { 14 | expect(data).toContain('package-lock.json'); 15 | }); 16 | 17 | it('Check if build folder is included.', () => { 18 | expect(data).toContain('build'); 19 | }); 20 | 21 | it('Check if .DS_STORE is not included.', () => { 22 | expect(data).toContain('.DS_STORE'); 23 | }); 24 | }); -------------------------------------------------------------------------------- /__tests__/server.test.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest'); 2 | 3 | const server = 'http://localhost:3000'; 4 | //const app = require('../server/server'); 5 | 6 | describe('Route Integration Tests', () => { 7 | describe('/', () => { 8 | describe('GET', () => { 9 | // const server = 'http://localhost:3000'; 10 | xit('responds with 200 status and text/html content type', () => { 11 | return request(server) 12 | .get('/') 13 | .expect('Content-type', /text\/html/) 14 | .expect(200); 15 | }); 16 | }); 17 | }); 18 | 19 | describe('/submit', () => { 20 | describe('GET', () => { 21 | xit('responds with 200 status and application/json content type', () => { 22 | return request(server) 23 | .get('/submit') 24 | .send('postgres://nhiumazy:oys61uE526v3wjfIhANYXURgyoo-ty28@fanny.db.elephantsql.com/nhiumazy') 25 | .expect('Content-type', 'application/json; charset=utf-8') 26 | .expect(200); 27 | }); 28 | }); 29 | }); 30 | 31 | describe('/nonexistentendpoint', () => { 32 | describe('GET', () => { 33 | xit('responds with 400 status', () => { 34 | return request(server) 35 | .get('/doesnotexist') 36 | .expect(400); 37 | }); 38 | }); 39 | }); 40 | 41 | describe('/schema', () => { 42 | describe('GET', () => { 43 | xit('responds with 200 status and text/html content type', () => { 44 | return request(server) 45 | .get('/schema') 46 | .expect('Content-type', /text\/html/) 47 | .expect(200); 48 | }); 49 | }); 50 | }); 51 | }); -------------------------------------------------------------------------------- /client/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import HeaderComponent from './Components/HeaderComponent' 3 | import FeatureComponent from './Components/FeatureComponent' 4 | import BioContainer from './Containers/BioContainer' 5 | import FooterComponent from './Components/FooterComponent' 6 | import DemoContainer from './Containers/DemoContainer' 7 | 8 | // RENDERS landing page 9 | function App() { 10 | return ( 11 |
12 | 13 | 14 | 15 | 16 | 17 |
18 | ); 19 | } 20 | 21 | export default App; -------------------------------------------------------------------------------- /client/Components/BioComponent.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { Link, Box, Typography, Grid, Avatar } from '@mui/material'; 3 | import LinkedInIcon from '@mui/icons-material/LinkedIn'; 4 | import GitHubIcon from '@mui/icons-material/GitHub'; 5 | 6 | const BioComponent = ({ profilePic, fullName, gitHubLink, linkedInLink }) => { 7 | return ( 8 | 9 | 10 | 11 | {fullName} 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | ); 21 | }; 22 | export default BioComponent; -------------------------------------------------------------------------------- /client/Components/FeatureComponent.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { Container, Box, Typography, Grid } from '@mui/material'; 3 | import StorageIcon from '@mui/icons-material/Storage'; 4 | import AutoGraphIcon from '@mui/icons-material/AutoGraph'; 5 | import TravelExploreIcon from '@mui/icons-material/TravelExplore'; 6 | 7 | // Renders feature components: "visualize", "generate", and "explore", icons, and body text 8 | const FeatureComponent = () => { 9 | return ( 10 |
11 | 12 | 13 | 14 | 15 |
16 | 17 | VISUALIZE 18 |
19 | 20 | Connect to your SQL database to our interactive dashboard to visualize your table and relationship data displayed in real-time. 21 | 22 |
23 | 24 |
25 | 26 | GENERATE 27 |
28 | 29 | Generate accurate and exportable GraphQL schema and resolvers tailored to your specific database needs. 30 | 31 |
32 | 33 |
34 | 35 | EXPLORE 36 |
37 | 38 | Connect to our Graphiql sandbox to demo your GraphQL queries and mutations, using your newly generated schemas as reference. 39 | 40 |
41 |
42 |
43 |
44 |
45 | ); 46 | }; 47 | export default FeatureComponent; -------------------------------------------------------------------------------- /client/Components/FooterComponent.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button, AppBar, Typography, Toolbar, IconButton } from '@mui/material'; 3 | import LinkedInIcon from '@mui/icons-material/LinkedIn'; 4 | import MonetizationOnIcon from '@mui/icons-material/MonetizationOn'; 5 | import GitHubIcon from '@mui/icons-material/GitHub'; 6 | import ArticleOutlinedIcon from '@mui/icons-material/ArticleOutlined'; 7 | 8 | // Renders: Footer 9 | const FooterComponent = () => { 10 | return ( 11 | 12 | 13 | 14 | © 2021 ArtemisQL 15 | 16 | 17 | Accelerated by OS Labs 18 | 19 | {/* */} 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | ); 32 | }; 33 | 34 | export default FooterComponent; -------------------------------------------------------------------------------- /client/Components/HeaderComponent.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { Container, Button, Box, Typography } from '@mui/material'; 3 | import { Link } from 'react-router-dom'; 4 | 5 | const HeaderComponent = () => { 6 | return ( 7 |
8 | 9 | 10 | 11 | A 12 | r 13 | t 14 | e 15 | m 16 | i 17 | s 18 | Q 19 | L 20 | 21 | Visualize your data, generate new prototypes, and explore the possibilities of GraphQL 22 | 23 | 26 | 27 | 28 | 29 |
30 | ); 31 | }; 32 | 33 | export default HeaderComponent; -------------------------------------------------------------------------------- /client/Components/LeftDemoComponent.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { Box, Typography, Grid, CardMedia, Card } from '@mui/material'; 3 | 4 | type Props = { 5 | image: any; 6 | text: string; 7 | number: string; 8 | stepHeader: string; 9 | stepDetails: string; 10 | } 11 | 12 | // left or right either containers an image or some text 13 | const LeftDemoComponent = ({image, text, number, stepHeader, stepDetails}: Props) => { 14 | return ( 15 | 16 | <> 17 | 18 | 19 | 20 | 21 | 22 | {/* text */} 23 | 24 | 25 | 26 | {number} 27 | {stepHeader} 28 | {stepDetails} 29 | 30 | 31 | 32 | 33 | ); 34 | }; 35 | // borderStyle: "solid", 36 | // borderWidth: 2, 37 | export default LeftDemoComponent; -------------------------------------------------------------------------------- /client/Components/NavComponent.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import AppBar from '@mui/material/AppBar'; 3 | import Box from '@mui/material/Box'; 4 | import Toolbar from '@mui/material/Toolbar'; 5 | import Button from '@mui/material/Button'; 6 | import IconButton from '@mui/material/IconButton'; 7 | import GitHubIcon from '@mui/icons-material/GitHub'; 8 | import BugReportIcon from '@mui/icons-material/BugReport'; 9 | import PlayCircleFilledIcon from '@mui/icons-material/PlayCircleFilled'; 10 | import LinkedInIcon from '@mui/icons-material/LinkedIn'; 11 | import ArticleOutlinedIcon from '@mui/icons-material/ArticleOutlined'; 12 | import { Link } from 'react-router-dom'; 13 | import logo from '../assets/logo.png'; 14 | 15 | 16 | // Renders: NAV BAR: app logo, "get started" button, "sandbox" button, github, linked-in, and medium icons 17 | const NavComponent = () => { 18 | return ( 19 | 20 | 21 | 22 | 23 | logo 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | ); 42 | }; 43 | 44 | export default NavComponent; -------------------------------------------------------------------------------- /client/Components/RightDemoComponent.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { Box, Typography, Grid, CardMedia, Card, Link } from '@mui/material'; 3 | 4 | type Props = { 5 | image: any; 6 | text: string; 7 | number: string; 8 | stepHeader: string; 9 | stepDetails: string; 10 | } 11 | 12 | const RightDemoComponent = ({image, text, number, stepHeader, stepDetails}: Props) => { 13 | return ( 14 | <> 15 | {/* text */} 16 | 17 | 18 | 19 | {number} 20 | {stepHeader} 21 | 22 | {stepDetails} 23 | 24 | {text}. 25 | 26 | 27 | 28 | 29 | 30 | 31 | {/* image */} 32 | 33 | 34 | 35 | 36 | 37 | 38 | ); 39 | }; 40 | 41 | export default RightDemoComponent; -------------------------------------------------------------------------------- /client/Components/SchemaComponent.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Container, Button, Grid, Typography } from '@mui/material'; 3 | import Highlight from 'react-highlight'; 4 | import Switch from '@mui/material/Switch'; 5 | 6 | type Props = { 7 | schema: string; 8 | resolvers: string; 9 | } 10 | 11 | function SchemaComponent({schema, resolvers}: Props) { 12 | const [text, setText] = useState(schema); 13 | const [position, setPosition] = useState('0px'); 14 | const [showContainer, setShowContainer] = useState(true); 15 | 16 | // modifies schema tab position state on switch change 17 | const onSwitch = () => { 18 | const tab:any = document.getElementById('tab'); 19 | if (showContainer) { 20 | setPosition('-1000px') 21 | tab.className = 'slideOut'; 22 | setShowContainer(false); 23 | } else { 24 | setPosition('25px') 25 | tab.className = 'slideIn'; 26 | setShowContainer(true); 27 | } 28 | }; 29 | 30 | // copies current text to clipboard to be exported 31 | const copyCode = () => { 32 | navigator.clipboard.writeText(text); 33 | }; 34 | 35 | // renders switch, code container, and buttons 36 | return ( 37 | 38 |
39 | SHOW SCHEMA 40 | 41 |
42 | 43 | 44 |
45 | 46 | {text} 47 | 48 |
49 | {/*
*/} 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 |
62 |
63 |
64 | ); 65 | } 66 | 67 | // COMPONENT STYLING 68 | const containerStyle = { 69 | position: 'absolute', 70 | top: '8vh', 71 | right: '0px', 72 | zIndex: '99' 73 | } 74 | 75 | const switchStyle = { 76 | display: 'flex', 77 | alignItems: 'center', 78 | marginTop: '2vh', 79 | marginBottom: '1vh', 80 | marginRight: '25px', 81 | paddingLeft: '10px', 82 | borderStyle: 'solid', 83 | borderWidth: '1px', 84 | borderColor: '#403D39', 85 | borderRadius: '5px', 86 | backgroundColor:'#eeeeee' 87 | } 88 | 89 | const buttonStyle = { 90 | fontWeight: 'normal', 91 | borderWidth: 2, 92 | } 93 | 94 | export default SchemaComponent; -------------------------------------------------------------------------------- /client/Containers/BioContainer.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { Link, Container, Box, Typography, Grid, Avatar } from '@mui/material'; 3 | import BioComponent from '../Components/BioComponent'; 4 | import John_Lin from '../assets/john_lin_headshot.jpg'; 5 | import Johnny_Bryan from '../assets/johnnybryan.jpg'; 6 | import Taras from '../assets/taras.jpg'; 7 | import Jennifer_Chau from '../assets/jennifer_chau_headshot.jpg'; 8 | 9 | const profile = { 10 | JB: { 11 | fullName: 'Johnny Bryan', 12 | profilePic: Johnny_Bryan, 13 | gitHubLink:'https://github.com/johnnybryan', 14 | linkedInLink:'https://www.linkedin.com/in/john-bryan-10a3bbb9/' 15 | }, 16 | JC: { 17 | fullName: 'Jennifer Chau', 18 | profilePic: Jennifer_Chau, 19 | gitHubLink:'https://github.com/jenniferchau', 20 | linkedInLink:'https://www.linkedin.com/in/jenniferchau512/' 21 | }, 22 | JL: { 23 | fullName: 'John Lin', 24 | profilePic: John_Lin, 25 | gitHubLink:'https://github.com/itzJohn', 26 | linkedInLink:'https://www.linkedin.com/in/john-lin-/' 27 | }, 28 | TS: { 29 | fullName: 'Taras Sukhoverskyi', 30 | profilePic: Taras, 31 | gitHubLink:'https://github.com/Tarantino23', 32 | linkedInLink:'https://www.linkedin.com/in/taras-sukhoverskyi-628642145/' 33 | }, 34 | }; 35 | 36 | const TeamBiosComponent = () => { 37 | return ( 38 |
39 | 40 | 41 | 42 | ArtemisQL Team 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 |
53 | ); 54 | }; 55 | export default TeamBiosComponent; -------------------------------------------------------------------------------- /client/Containers/DemoContainer.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { Box, Typography, Grid, Container, Link } from '@mui/material'; 3 | import LeftDemoComponent from '../Components/LeftDemoComponent'; 4 | import RightDemoComponent from '../Components/RightDemoComponent'; 5 | import demo1 from '../assets/demo1.gif' 6 | import demo2 from '../assets/demo2.gif' 7 | import demo3 from '../assets/demo3.gif' 8 | import demo4 from '../assets/demo4.gif' 9 | 10 | const DemoContainer = () => { 11 | return ( 12 | // Demo Header 13 |
14 | 15 | 16 | How to use ArtemisQL 17 | 18 | We've designed a simple and intuitive process to help you migrate from a REST API to 19 | {/* GraphQL */} 20 | 21 | GraphQL 22 | 23 | . Here's how it works. 24 | 25 | 26 | 27 | 28 | {/* Demo Body */} 29 | 30 | 31 | 32 | {/* // Demo Body - Two parts - Image and Text */} 33 | 40 | 47 | 54 | 61 | 62 | 63 | 64 |
65 | ); 66 | }; 67 | 68 | export default DemoContainer; -------------------------------------------------------------------------------- /client/Containers/FlowContainer.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import ReactFlow, { Background, Controls, Handle } from 'react-flow-renderer'; 3 | import SchemaComponent from '../Components/SchemaComponent'; 4 | 5 | // TYPE DEFINITIONS 6 | type Elements = any[]; 7 | 8 | type Edge = { 9 | id: number; 10 | source: string; 11 | target: string; 12 | sourceHandle: string; 13 | style: object; 14 | animated: boolean; 15 | }; 16 | 17 | type Node = { 18 | id: string | number; 19 | type: string; 20 | data: object; 21 | position: object; 22 | }; 23 | 24 | type NodeType = { 25 | special: object; 26 | } 27 | 28 | type Props = { 29 | data: object; 30 | schema: string; 31 | resolvers: string; 32 | } 33 | 34 | // RENDERS TABLE NODES & SCHEMA COMPONENT 35 | const FlowContainer = ({ data, schema, resolvers }: Props) => { 36 | 37 | // declare elements array to store table nodes and edges 38 | const elements: Elements = new Array(); 39 | 40 | // declare coordinate variables for table node positions/mapping 41 | let positionX = 100; 42 | let positionY = 250; 43 | let row = 0; 44 | 45 | // Iterate through each table... 46 | for (const key in data) { 47 | // assign key to tableName and its value(array of columns) as table 48 | const tableName = key; 49 | const table = data[key]; 50 | // capitalize table name and assign it to node title 51 | const title = tableName.toUpperCase(); 52 | // declare primary / foreign key variables 53 | let primaryKey = false; 54 | const foreignKeys: any = []; 55 | 56 | // iterate through each column of each table creating creating columnName and dataType rows 57 | const columns = table.map((column: any) => { 58 | let count = 0; 59 | let hasForeignKey = false; 60 | 61 | // check for primary key 62 | if (column.constraint_type === 'PRIMARY KEY') primaryKey = true; 63 | // check for foreign keys 64 | else if (column.constraint_type === 'FOREIGN KEY') { 65 | hasForeignKey = true; 66 | // declare an object to store edges 67 | const edge: Edge = { 68 | id: count++, 69 | source: tableName, 70 | target: column.foreign_table, 71 | sourceHandle: column.column_name, 72 | style: { stroke: "#00009f" }, 73 | animated: true, 74 | }; 75 | // append edge object to elements array 76 | elements.push(edge); 77 | } 78 | 79 | // append columnName and hasForeignKey to foreignKeys array 80 | foreignKeys.push([column.column_name, hasForeignKey]); 81 | 82 | // return new column and data type to render in newNode 83 | return ( 84 |

85 | {column.column_name} 86 | 87 | {column.data_type} 88 | 89 |

90 | ); 91 | }); 92 | 93 | // create a new node object 94 | const newNode: Node = { 95 | id: tableName, 96 | type: 'special', 97 | data: { 98 | label: 99 |
100 |
101 |

{title}

102 |
103 |
104 | {columns} 105 |
106 |
, 107 | pk: primaryKey, 108 | fk: foreignKeys, 109 | }, 110 | position: { x: positionX, y: positionY }, 111 | }; 112 | 113 | // assign table node position 114 | row += 1; 115 | positionY += 600; 116 | if (row % 2 === 0) { 117 | positionY = 250; 118 | positionX += 400; 119 | } 120 | 121 | // append new node to elements array 122 | elements.push(newNode); 123 | } 124 | 125 | return ( 126 |
127 | 128 | 134 | 135 | 136 | 137 |
138 | ); 139 | }; 140 | 141 | // Custom Node Component 142 | const CustomNode = ({ data }) => { 143 | // initial handle position 144 | let index = 111; 145 | return ( 146 |
147 | 148 | {/* table data */} 149 | {data.label} 150 | 151 | {/* primary key handle */} 152 | {data.pk ? ( 153 | 159 | ) : ( 160 | '' 161 | )} 162 | 163 | {/* foreign key handles */} 164 | {data.fk.map((el: [string, boolean]) => { 165 | if (el[0] === '_id') index = 111; 166 | else if (el[1] === true) { 167 | return ( 168 | 174 | )} else { 175 | index += 37; 176 | } 177 | })} 178 |
179 | ); 180 | }; 181 | 182 | // assign custom node to special type 183 | const nodeTypes: NodeType = { 184 | special: CustomNode, 185 | }; 186 | 187 | // COMPONENT STYLING 188 | const customNodeStyle = { 189 | color: "#403D39", 190 | backgroundColor: "#eeeeee", 191 | fontFamily: "JetBrains Mono", 192 | borderRadius: 5, 193 | borderStyle: "solid", 194 | borderWidth: 2, 195 | borderColor: "#8cbbad", 196 | transition: "all 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms", 197 | boxShadow: "0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23)", 198 | }; 199 | 200 | const titleStyle = { 201 | background: "#282b2e", 202 | fontSize: "16px", 203 | color: "#36acaa", 204 | TextAlign: "center", 205 | padding: "5px 20px", 206 | borderTopLeftRadius: "5px", 207 | borderTopRightRadius: "5px" 208 | }; 209 | 210 | const containerStyle = { 211 | padding: 10, 212 | }; 213 | 214 | export default FlowContainer; -------------------------------------------------------------------------------- /client/Dashboard.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { Container, TextField } from '@mui/material'; 3 | import Grid from '@mui/material/Grid'; 4 | import Button from '@mui/material/Button'; 5 | import StorageIcon from '@mui/icons-material/Storage'; 6 | import SendIcon from '@mui/icons-material/Send'; 7 | import FlowContainer from './Containers/FlowContainer'; 8 | import axios from 'axios'; 9 | 10 | type Request = { 11 | method: string; 12 | url: string; 13 | params?: object | null; 14 | } 15 | 16 | // CONDITIONALLY RENDERS: input page / dashboard container 17 | const Dashboard = () => { 18 | const [data, setData] = useState({}); 19 | const [schemaType, setSchemaType] = useState(''); 20 | const [resolvers, setResolvers] = useState(''); 21 | const [databaseURL, setDatabaseURL] = useState(''); 22 | const [message, setMessage] = useState(''); 23 | const [showSchema, setSchema] = useState(false); 24 | 25 | function onChangeHandler(input: any) { 26 | const { name, value } = input.currentTarget; 27 | if (name === 'urlInput') { 28 | setDatabaseURL(value); 29 | } 30 | } 31 | 32 | function getSampleDB() { 33 | const request: Request = { 34 | method: 'GET', 35 | url: '/submit', 36 | }; 37 | 38 | axios.request(request) 39 | .then((res) => { 40 | if (res.status = 200) { 41 | setData(res.data.allTables); 42 | setSchemaType(res.data.finalString); 43 | setResolvers(res.data.resolverString); 44 | setSchema(true); 45 | } 46 | }) 47 | .catch(console.error); 48 | } 49 | 50 | function getDataFromDB(dbLink: string) { 51 | if (!dbLink) { 52 | setMessage('Invalid URI, please try again'); 53 | return; 54 | } 55 | 56 | const request: Request = { 57 | method: 'GET', 58 | url: '/submit', 59 | params: { dbLink }, 60 | }; 61 | 62 | axios.request(request) 63 | .then((res) => { 64 | if (res.status = 200) { 65 | setData(res.data.allTables); 66 | setSchemaType(res.data.finalString); 67 | setResolvers(res.data.resolverString); 68 | setSchema(true); 69 | } 70 | }) 71 | .catch((err) => { 72 | setMessage('Invalid URI, please try again'); 73 | }); 74 | } 75 | 76 | if (showSchema) { 77 | return ( 78 | 79 | ); 80 | } 81 | 82 | return ( 83 |
84 | 85 | 86 | 87 | 88 | onChangeHandler(input)} fullWidth variant="standard" label="Database URL" /> 89 | {message} 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 |
103 | 104 | ); 105 | } 106 | 107 | // COMPONENT STYLING 108 | 109 | const divStyle = { 110 | display: 'flex', 111 | alignItems: 'center', 112 | height: '85%' 113 | } 114 | 115 | const containerStyle = { 116 | border: '1px solid #403D39', 117 | borderRadius: '5px', 118 | boxShadow: '3px 3px #888888', 119 | height: '160px', 120 | paddingTop: '20px' 121 | } 122 | 123 | const buttonStyle = { 124 | fontWeight: 'normal' 125 | } 126 | 127 | export default Dashboard; -------------------------------------------------------------------------------- /client/Router.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BrowserRouter, Route, Routes as Switch } from 'react-router-dom'; 3 | import App from './App'; 4 | import Dashboard from './Dashboard'; 5 | import NavComponent from './Components/NavComponent'; 6 | 7 | const Router = () => ( 8 | 9 | 10 | 11 | } /> 12 | } /> 13 | 14 | 15 | ); 16 | 17 | export default Router; -------------------------------------------------------------------------------- /client/assets/demo1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ArtemisQL/87a7fc438312d6c31a49c39e00fb24c9050c5d75/client/assets/demo1.gif -------------------------------------------------------------------------------- /client/assets/demo2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ArtemisQL/87a7fc438312d6c31a49c39e00fb24c9050c5d75/client/assets/demo2.gif -------------------------------------------------------------------------------- /client/assets/demo3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ArtemisQL/87a7fc438312d6c31a49c39e00fb24c9050c5d75/client/assets/demo3.gif -------------------------------------------------------------------------------- /client/assets/demo4.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ArtemisQL/87a7fc438312d6c31a49c39e00fb24c9050c5d75/client/assets/demo4.gif -------------------------------------------------------------------------------- /client/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ArtemisQL/87a7fc438312d6c31a49c39e00fb24c9050c5d75/client/assets/favicon.ico -------------------------------------------------------------------------------- /client/assets/jennifer_chau_headshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ArtemisQL/87a7fc438312d6c31a49c39e00fb24c9050c5d75/client/assets/jennifer_chau_headshot.jpg -------------------------------------------------------------------------------- /client/assets/john_lin_headshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ArtemisQL/87a7fc438312d6c31a49c39e00fb24c9050c5d75/client/assets/john_lin_headshot.jpg -------------------------------------------------------------------------------- /client/assets/johnnybryan.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ArtemisQL/87a7fc438312d6c31a49c39e00fb24c9050c5d75/client/assets/johnnybryan.jpg -------------------------------------------------------------------------------- /client/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ArtemisQL/87a7fc438312d6c31a49c39e00fb24c9050c5d75/client/assets/logo.png -------------------------------------------------------------------------------- /client/assets/taras.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ArtemisQL/87a7fc438312d6c31a49c39e00fb24c9050c5d75/client/assets/taras.jpg -------------------------------------------------------------------------------- /client/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | // import App from './App'; 4 | import Router from './Router'; 5 | import { ThemeProvider, createTheme, ThemeOptions } from '@mui/material/styles'; 6 | import './style.css'; 7 | import '@fontsource/roboto/300.css'; 8 | import '@fontsource/roboto/400.css'; 9 | import '@fontsource/roboto/500.css'; 10 | import '@fontsource/roboto/700.css'; 11 | 12 | /* 13 | APP COLOR PALETTE: 14 | dark gray: '#282b2e' 15 | aquablue: 'rgb(54, 172, 170)' || '#36acaa' 16 | lime green: '#93C763' 17 | light gray: '#eeeeee' 18 | graphql pink: '#E10098' 19 | cobalt blue: '#00009f' 20 | */ 21 | 22 | const theme = createTheme({ 23 | 24 | palette: { 25 | primary: { 26 | main: '#282b2e', 27 | }, 28 | secondary: { 29 | main: '#36acaa', 30 | }, 31 | warning: { 32 | main: '#E10098' 33 | }, 34 | info: { 35 | main: '#00009f' 36 | }, 37 | success: { 38 | main: '#93C763' 39 | }, 40 | }, 41 | 42 | components: { 43 | MuiButton: { 44 | styleOverrides: { 45 | root: { 46 | '&:hover': { 47 | color: '#36acaa', 48 | }, 49 | }, 50 | }, 51 | }, 52 | MuiIconButton: { 53 | styleOverrides: { 54 | root: { 55 | '&:hover': { 56 | color: '#36acaa', 57 | }, 58 | }, 59 | }, 60 | } 61 | }, 62 | }); 63 | 64 | ReactDOM.render( 65 | 66 | 67 | 68 | , 69 | document.getElementById('root'), 70 | ); -------------------------------------------------------------------------------- /client/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | } 4 | 5 | /* button:hover { 6 | color: "#EB5E28"; 7 | /* font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif 8 | } */ 9 | 10 | .button:hover { 11 | background-color: '#00009f' 12 | } 13 | 14 | pre { 15 | margin: 0; 16 | } 17 | 18 | .avatar { 19 | width: 150px !important; 20 | height: 150px !important; 21 | margin-left: auto !important; 22 | margin-right: auto !important; 23 | margin-bottom: 10px !important; 24 | } 25 | 26 | section { 27 | min-height: 50vh !important; 28 | } 29 | 30 | .media { 31 | height: 415px; 32 | width: 600px; 33 | } 34 | 35 | .slideIn { 36 | animation-name: in; 37 | animation-duration: 1s; 38 | } 39 | 40 | .slideOut { 41 | animation-name: out; 42 | animation-duration: 2s; 43 | } 44 | 45 | .a { 46 | animation-name: artemisAppear; 47 | animation-duration: 2s; 48 | } 49 | 50 | .r { 51 | animation-name: artemisAppear; 52 | animation-duration: 2.5s; 53 | } 54 | 55 | .t { 56 | animation-name: artemisAppear; 57 | animation-duration: 3s; 58 | } 59 | 60 | .e { 61 | animation-name: artemisAppear; 62 | animation-duration: 3.5s; 63 | } 64 | 65 | .m { 66 | animation-name: artemisAppear; 67 | animation-duration: 4s; 68 | } 69 | 70 | .i { 71 | animation-name: artemisAppear; 72 | animation-duration: 4.5s; 73 | } 74 | 75 | .s { 76 | animation-name: artemisAppear; 77 | animation-duration: 5s; 78 | } 79 | 80 | .q { 81 | animation-name: qlAppear; 82 | animation-duration: 6s; 83 | } 84 | 85 | .l { 86 | animation-name: qlAppear; 87 | animation-duration: 7s; 88 | } 89 | 90 | .subheader { 91 | animation-name: subheaderAppear; 92 | animation-duration: 6s; 93 | } 94 | 95 | .button { 96 | animation-name: buttonAppear; 97 | animation-duration: 7s; 98 | } 99 | 100 | .icon1 { 101 | animation-name: iconAppear; 102 | animation-duration: 2s; 103 | } 104 | 105 | .icon2 { 106 | animation-name: iconAppear; 107 | animation-duration: 4s; 108 | } 109 | 110 | .icon3 { 111 | animation-name: iconAppear; 112 | animation-duration: 6s; 113 | } 114 | 115 | .vis { 116 | animation-name: textAppear; 117 | animation-duration: 3s; 118 | } 119 | 120 | .gen { 121 | animation-name: textAppear; 122 | animation-duration: 5s; 123 | } 124 | 125 | .exp { 126 | animation-name: textAppear; 127 | animation-duration: 7s; 128 | } 129 | 130 | .body1 { 131 | animation-name: bodyAppear; 132 | animation-duration: 3s; 133 | } 134 | 135 | .body2 { 136 | animation-name: bodyAppear; 137 | animation-duration: 5s; 138 | } 139 | 140 | .body3 { 141 | animation-name: bodyAppear; 142 | animation-duration: 7s; 143 | } 144 | 145 | @keyframes out { 146 | 0% { 147 | transform: translateX(-1000px); 148 | } 149 | 150 | 100% { 151 | transform: translateX(0); 152 | } 153 | } 154 | 155 | @keyframes in { 156 | 0% { 157 | transform: translateX(1000px); 158 | } 159 | 160 | 100% { 161 | transform: translateX(0); 162 | } 163 | } 164 | 165 | @keyframes artemisAppear { 166 | from {color: white;} 167 | to {color: #282b2e;} 168 | } 169 | 170 | @keyframes qlAppear { 171 | from {color: white;} 172 | to {color: #E10098;} 173 | } 174 | 175 | @keyframes subheaderAppear { 176 | from {color: white;} 177 | to {color: "text-secondary";} 178 | } 179 | 180 | @keyframes buttonAppear { 181 | from {background-color: white; border-color: white} 182 | to {background-color: #282b2e; border-color: #282b2e} 183 | } 184 | 185 | @keyframes iconAppear { 186 | from {color: white;} 187 | to {color: #E10098;} 188 | } 189 | 190 | @keyframes textAppear { 191 | from {color: white;} 192 | to {color: #00009f;} 193 | } 194 | 195 | @keyframes bodyAppear { 196 | from {color: white;} 197 | to {color: #282b2e;} 198 | } -------------------------------------------------------------------------------- /docs/object-directory.txt: -------------------------------------------------------------------------------- 1 | queryTables: an array of objects, where each object is a column/field in the database. Each object holds information 2 | about the column, such as the data_type, column_name, table_name, etc. Includes join table columns. 3 | 4 | baseTableQuery: an array of objects, where each object is a column/field in the database. Each object holds information 5 | about the column, such as the data_type, column_name, table_name, etc. Excludes join table columns. 6 | 7 | allTables: an object, where each key is a table name, and value is an array of objects, where each object in the array 8 | holds all information about each column/field on the table. Includes all join tables. 9 | 10 | baseTables: an object, where each key is a table name, and value is an array of objects, where each object in the array 11 | holds all information about each column/field on the table. Excludes all join tables. 12 | 13 | joinTables: an object, where each key is a table name, and value is an array of objects, where each object in the array 14 | holds all information about each column/field on the table. Only includes join tables. 15 | 16 | baseTableNames: an array of strings, where each element is a table name. Excludes join tables. 17 | 18 | joinTableNames: an array of strings, where each element is a table name. Only includes join tables. 19 | 20 | schema: an object where each key is a table name, and the value is another object. The nested object holds all the 21 | column names (keys) and GraphQL data type (value). 22 | 23 | mutationObj: an object where each key is a mutation type with each table name (ex. addFilm, updateFilm, deleteFilm, 24 | addPlanet, updatePlanet, ...). Each value is another object that holds all column names and corresponding GraphQL 25 | data types. 26 | 27 | 28 | Samples are shown below for the structure (does not include all properties and elements): 29 | 30 | /** queryTables **/ 31 | [ 32 | { 33 | column_name: '_id', 34 | table_name: 'films', 35 | data_type: 'integer', 36 | character_maximum_length: null, 37 | is_nullable: 'NO', 38 | constraint_name: 'films_pk', 39 | constraint_type: 'PRIMARY KEY', 40 | foreign_table: null, 41 | foreign_column: null 42 | }, 43 | { 44 | column_name: 'director', 45 | table_name: 'films', 46 | data_type: 'character varying', 47 | character_maximum_length: null, 48 | is_nullable: 'NO', 49 | constraint_name: null, 50 | constraint_type: null, 51 | foreign_table: null, 52 | foreign_column: null 53 | }, 54 | { 55 | column_name: 'episode_id', 56 | table_name: 'films', 57 | data_type: 'integer', 58 | character_maximum_length: null, 59 | is_nullable: 'NO', 60 | constraint_name: null, 61 | constraint_type: null, 62 | foreign_table: null, 63 | foreign_column: null 64 | }, 65 | { 66 | column_name: 'opening_crawl', 67 | table_name: 'films', 68 | data_type: 'character varying', 69 | character_maximum_length: null, 70 | is_nullable: 'NO', 71 | constraint_name: null, 72 | constraint_type: null, 73 | foreign_table: null, 74 | foreign_column: null 75 | }, 76 | { 77 | column_name: 'name', 78 | table_name: 'planets', 79 | data_type: 'character varying', 80 | character_maximum_length: null, 81 | is_nullable: 'YES', 82 | constraint_name: null, 83 | constraint_type: null, 84 | foreign_table: null, 85 | foreign_column: null 86 | }, 87 | { 88 | column_name: 'orbital_period', 89 | table_name: 'planets', 90 | data_type: 'integer', 91 | character_maximum_length: null, 92 | is_nullable: 'YES', 93 | constraint_name: null, 94 | constraint_type: null, 95 | foreign_table: null, 96 | foreign_column: null 97 | }, 98 | { 99 | column_name: 'population', 100 | table_name: 'planets', 101 | data_type: 'bigint', 102 | character_maximum_length: null, 103 | is_nullable: 'YES', 104 | constraint_name: null, 105 | constraint_type: null, 106 | foreign_table: null, 107 | foreign_column: null 108 | }, 109 | { 110 | column_name: 'rotation_period', 111 | table_name: 'planets', 112 | data_type: 'integer', 113 | character_maximum_length: null, 114 | is_nullable: 'YES', 115 | constraint_name: null, 116 | constraint_type: null, 117 | foreign_table: null, 118 | foreign_column: null 119 | }, 120 | { 121 | column_name: 'name', 122 | table_name: 'vessels', 123 | data_type: 'character varying', 124 | character_maximum_length: null, 125 | is_nullable: 'NO', 126 | constraint_name: null, 127 | constraint_type: null, 128 | foreign_table: null, 129 | foreign_column: null 130 | }, 131 | { 132 | column_name: 'passengers', 133 | table_name: 'vessels', 134 | data_type: 'integer', 135 | character_maximum_length: null, 136 | is_nullable: 'YES', 137 | constraint_name: null, 138 | constraint_type: null, 139 | foreign_table: null, 140 | foreign_column: null 141 | }, 142 | { 143 | column_name: 'vessel_class', 144 | table_name: 'vessels', 145 | data_type: 'character varying', 146 | character_maximum_length: null, 147 | is_nullable: 'NO', 148 | constraint_name: null, 149 | constraint_type: null, 150 | foreign_table: null, 151 | foreign_column: null 152 | }, 153 | { 154 | column_name: '_id', 155 | table_name: 'vessels_in_films', 156 | data_type: 'integer', 157 | character_maximum_length: null, 158 | is_nullable: 'NO', 159 | constraint_name: 'vessels_in_films_pk', 160 | constraint_type: 'PRIMARY KEY', 161 | foreign_table: null, 162 | foreign_column: null 163 | }, 164 | { 165 | column_name: 'film_id', 166 | table_name: 'vessels_in_films', 167 | data_type: 'bigint', 168 | character_maximum_length: null, 169 | is_nullable: 'NO', 170 | constraint_name: 'vessels_in_films_fk1', 171 | constraint_type: 'FOREIGN KEY', 172 | foreign_table: 'films', 173 | foreign_column: '_id' 174 | }, 175 | { 176 | column_name: 'vessel_id', 177 | table_name: 'vessels_in_films', 178 | data_type: 'bigint', 179 | character_maximum_length: null, 180 | is_nullable: 'NO', 181 | constraint_name: 'vessels_in_films_fk0', 182 | constraint_type: 'FOREIGN KEY', 183 | foreign_table: 'vessels', 184 | foreign_column: '_id' 185 | } 186 | ] 187 | 188 | /** baseTablesQuery **/ 189 | [ 190 | { 191 | column_name: '_id', 192 | table_name: 'films', 193 | data_type: 'integer', 194 | character_maximum_length: null, 195 | is_nullable: 'NO', 196 | constraint_name: 'films_pk', 197 | constraint_type: 'PRIMARY KEY', 198 | foreign_table: null, 199 | foreign_column: null 200 | }, 201 | { 202 | column_name: 'director', 203 | table_name: 'films', 204 | data_type: 'character varying', 205 | character_maximum_length: null, 206 | is_nullable: 'NO', 207 | constraint_name: null, 208 | constraint_type: null, 209 | foreign_table: null, 210 | foreign_column: null 211 | }, 212 | { 213 | column_name: 'episode_id', 214 | table_name: 'films', 215 | data_type: 'integer', 216 | character_maximum_length: null, 217 | is_nullable: 'NO', 218 | constraint_name: null, 219 | constraint_type: null, 220 | foreign_table: null, 221 | foreign_column: null 222 | }, 223 | { 224 | column_name: 'opening_crawl', 225 | table_name: 'films', 226 | data_type: 'character varying', 227 | character_maximum_length: null, 228 | is_nullable: 'NO', 229 | constraint_name: null, 230 | constraint_type: null, 231 | foreign_table: null, 232 | foreign_column: null 233 | }, 234 | { 235 | column_name: 'producer', 236 | table_name: 'films', 237 | data_type: 'character varying', 238 | character_maximum_length: null, 239 | is_nullable: 'NO', 240 | constraint_name: null, 241 | constraint_type: null, 242 | foreign_table: null, 243 | foreign_column: null 244 | }, 245 | { 246 | column_name: 'release_date', 247 | table_name: 'films', 248 | data_type: 'date', 249 | character_maximum_length: null, 250 | is_nullable: 'NO', 251 | constraint_name: null, 252 | constraint_type: null, 253 | foreign_table: null, 254 | foreign_column: null 255 | }, 256 | { 257 | column_name: 'title', 258 | table_name: 'films', 259 | data_type: 'character varying', 260 | character_maximum_length: null, 261 | is_nullable: 'NO', 262 | constraint_name: null, 263 | constraint_type: null, 264 | foreign_table: null, 265 | foreign_column: null 266 | }, 267 | { 268 | column_name: '_id', 269 | table_name: 'people', 270 | data_type: 'integer', 271 | character_maximum_length: null, 272 | is_nullable: 'NO', 273 | constraint_name: 'people_pk', 274 | constraint_type: 'PRIMARY KEY', 275 | foreign_table: null, 276 | foreign_column: null 277 | }, 278 | { 279 | column_name: 'birth_year', 280 | table_name: 'people', 281 | data_type: 'character varying', 282 | character_maximum_length: null, 283 | is_nullable: 'YES', 284 | constraint_name: null, 285 | constraint_type: null, 286 | foreign_table: null, 287 | foreign_column: null 288 | }, 289 | { 290 | column_name: 'eye_color', 291 | table_name: 'people', 292 | data_type: 'character varying', 293 | character_maximum_length: null, 294 | is_nullable: 'YES', 295 | constraint_name: null, 296 | constraint_type: null, 297 | foreign_table: null, 298 | foreign_column: null 299 | }, 300 | { 301 | column_name: 'gender', 302 | table_name: 'people', 303 | data_type: 'character varying', 304 | character_maximum_length: null, 305 | is_nullable: 'YES', 306 | constraint_name: null, 307 | constraint_type: null, 308 | foreign_table: null, 309 | foreign_column: null 310 | }, 311 | { 312 | column_name: 'hair_color', 313 | table_name: 'people', 314 | data_type: 'character varying', 315 | character_maximum_length: null, 316 | is_nullable: 'YES', 317 | constraint_name: null, 318 | constraint_type: null, 319 | foreign_table: null, 320 | foreign_column: null 321 | }, 322 | { 323 | column_name: 'height', 324 | table_name: 'people', 325 | data_type: 'integer', 326 | character_maximum_length: null, 327 | is_nullable: 'YES', 328 | constraint_name: null, 329 | constraint_type: null, 330 | foreign_table: null, 331 | foreign_column: null 332 | }, 333 | { 334 | column_name: 'homeworld_id', 335 | table_name: 'people', 336 | data_type: 'bigint', 337 | character_maximum_length: null, 338 | is_nullable: 'YES', 339 | constraint_name: 'people_fk1', 340 | constraint_type: 'FOREIGN KEY', 341 | foreign_table: 'planets', 342 | foreign_column: '_id' 343 | }, 344 | { 345 | column_name: 'mass', 346 | table_name: 'people', 347 | data_type: 'character varying', 348 | character_maximum_length: null, 349 | is_nullable: 'YES', 350 | constraint_name: null, 351 | constraint_type: null, 352 | foreign_table: null, 353 | foreign_column: null 354 | }, 355 | { 356 | column_name: 'name', 357 | table_name: 'people', 358 | data_type: 'character varying', 359 | character_maximum_length: null, 360 | is_nullable: 'NO', 361 | constraint_name: null, 362 | constraint_type: null, 363 | foreign_table: null, 364 | foreign_column: null 365 | }, 366 | { 367 | column_name: 'skin_color', 368 | table_name: 'people', 369 | data_type: 'character varying', 370 | character_maximum_length: null, 371 | is_nullable: 'YES', 372 | constraint_name: null, 373 | constraint_type: null, 374 | foreign_table: null, 375 | foreign_column: null 376 | }, 377 | { 378 | column_name: 'species_id', 379 | table_name: 'people', 380 | data_type: 'bigint', 381 | character_maximum_length: null, 382 | is_nullable: 'YES', 383 | constraint_name: 'people_fk0', 384 | constraint_type: 'FOREIGN KEY', 385 | foreign_table: 'species', 386 | foreign_column: '_id' 387 | }, 388 | { 389 | column_name: '_id', 390 | table_name: 'planets', 391 | data_type: 'integer', 392 | character_maximum_length: null, 393 | is_nullable: 'NO', 394 | constraint_name: 'planets_pk', 395 | constraint_type: 'PRIMARY KEY', 396 | foreign_table: null, 397 | foreign_column: null 398 | }, 399 | { 400 | column_name: 'climate', 401 | table_name: 'planets', 402 | data_type: 'character varying', 403 | character_maximum_length: null, 404 | is_nullable: 'YES', 405 | constraint_name: null, 406 | constraint_type: null, 407 | foreign_table: null, 408 | foreign_column: null 409 | }, 410 | { 411 | column_name: 'diameter', 412 | table_name: 'planets', 413 | data_type: 'integer', 414 | character_maximum_length: null, 415 | is_nullable: 'YES', 416 | constraint_name: null, 417 | constraint_type: null, 418 | foreign_table: null, 419 | foreign_column: null 420 | }, 421 | ] 422 | 423 | /** allTables **/ 424 | { 425 | films: [ 426 | { 427 | column_name: '_id', 428 | table_name: 'films', 429 | data_type: 'integer', 430 | character_maximum_length: null, 431 | is_nullable: 'NO', 432 | constraint_name: 'films_pk', 433 | constraint_type: 'PRIMARY KEY', 434 | foreign_table: null, 435 | foreign_column: null 436 | }, 437 | { 438 | column_name: 'director', 439 | table_name: 'films', 440 | data_type: 'character varying', 441 | character_maximum_length: null, 442 | is_nullable: 'NO', 443 | constraint_name: null, 444 | constraint_type: null, 445 | foreign_table: null, 446 | foreign_column: null 447 | }, 448 | { 449 | column_name: 'episode_id', 450 | table_name: 'films', 451 | data_type: 'integer', 452 | character_maximum_length: null, 453 | is_nullable: 'NO', 454 | constraint_name: null, 455 | constraint_type: null, 456 | foreign_table: null, 457 | foreign_column: null 458 | }, 459 | { 460 | column_name: 'opening_crawl', 461 | table_name: 'films', 462 | data_type: 'character varying', 463 | character_maximum_length: null, 464 | is_nullable: 'NO', 465 | constraint_name: null, 466 | constraint_type: null, 467 | foreign_table: null, 468 | foreign_column: null 469 | }, 470 | { 471 | column_name: 'producer', 472 | table_name: 'films', 473 | data_type: 'character varying', 474 | character_maximum_length: null, 475 | is_nullable: 'NO', 476 | constraint_name: null, 477 | constraint_type: null, 478 | foreign_table: null, 479 | foreign_column: null 480 | }, 481 | { 482 | column_name: 'release_date', 483 | table_name: 'films', 484 | data_type: 'date', 485 | character_maximum_length: null, 486 | is_nullable: 'NO', 487 | constraint_name: null, 488 | constraint_type: null, 489 | foreign_table: null, 490 | foreign_column: null 491 | }, 492 | { 493 | column_name: 'title', 494 | table_name: 'films', 495 | data_type: 'character varying', 496 | character_maximum_length: null, 497 | is_nullable: 'NO', 498 | constraint_name: null, 499 | constraint_type: null, 500 | foreign_table: null, 501 | foreign_column: null 502 | } 503 | ], 504 | people: [ 505 | { 506 | column_name: '_id', 507 | table_name: 'people', 508 | data_type: 'integer', 509 | character_maximum_length: null, 510 | is_nullable: 'NO', 511 | constraint_name: 'people_pk', 512 | constraint_type: 'PRIMARY KEY', 513 | foreign_table: null, 514 | foreign_column: null 515 | }, 516 | { 517 | column_name: 'birth_year', 518 | table_name: 'people', 519 | data_type: 'character varying', 520 | character_maximum_length: null, 521 | is_nullable: 'YES', 522 | constraint_name: null, 523 | constraint_type: null, 524 | foreign_table: null, 525 | foreign_column: null 526 | }, 527 | { 528 | column_name: 'eye_color', 529 | table_name: 'people', 530 | data_type: 'character varying', 531 | character_maximum_length: null, 532 | is_nullable: 'YES', 533 | constraint_name: null, 534 | constraint_type: null, 535 | foreign_table: null, 536 | foreign_column: null 537 | }, 538 | { 539 | column_name: 'gender', 540 | table_name: 'people', 541 | data_type: 'character varying', 542 | character_maximum_length: null, 543 | is_nullable: 'YES', 544 | constraint_name: null, 545 | constraint_type: null, 546 | foreign_table: null, 547 | foreign_column: null 548 | }, 549 | { 550 | column_name: 'hair_color', 551 | table_name: 'people', 552 | data_type: 'character varying', 553 | character_maximum_length: null, 554 | is_nullable: 'YES', 555 | constraint_name: null, 556 | constraint_type: null, 557 | foreign_table: null, 558 | foreign_column: null 559 | }, 560 | { 561 | column_name: 'height', 562 | table_name: 'people', 563 | data_type: 'integer', 564 | character_maximum_length: null, 565 | is_nullable: 'YES', 566 | constraint_name: null, 567 | constraint_type: null, 568 | foreign_table: null, 569 | foreign_column: null 570 | }, 571 | { 572 | column_name: 'homeworld_id', 573 | table_name: 'people', 574 | data_type: 'bigint', 575 | character_maximum_length: null, 576 | is_nullable: 'YES', 577 | constraint_name: 'people_fk1', 578 | constraint_type: 'FOREIGN KEY', 579 | foreign_table: 'planets', 580 | foreign_column: '_id' 581 | }, 582 | { 583 | column_name: 'mass', 584 | table_name: 'people', 585 | data_type: 'character varying', 586 | character_maximum_length: null, 587 | is_nullable: 'YES', 588 | constraint_name: null, 589 | constraint_type: null, 590 | foreign_table: null, 591 | foreign_column: null 592 | }, 593 | { 594 | column_name: 'name', 595 | table_name: 'people', 596 | data_type: 'character varying', 597 | character_maximum_length: null, 598 | is_nullable: 'NO', 599 | constraint_name: null, 600 | constraint_type: null, 601 | foreign_table: null, 602 | foreign_column: null 603 | }, 604 | { 605 | column_name: 'skin_color', 606 | table_name: 'people', 607 | data_type: 'character varying', 608 | character_maximum_length: null, 609 | is_nullable: 'YES', 610 | constraint_name: null, 611 | constraint_type: null, 612 | foreign_table: null, 613 | foreign_column: null 614 | }, 615 | { 616 | column_name: 'species_id', 617 | table_name: 'people', 618 | data_type: 'bigint', 619 | character_maximum_length: null, 620 | is_nullable: 'YES', 621 | constraint_name: 'people_fk0', 622 | constraint_type: 'FOREIGN KEY', 623 | foreign_table: 'species', 624 | foreign_column: '_id' 625 | } 626 | ], 627 | people_in_films: [ 628 | { 629 | column_name: '_id', 630 | table_name: 'people_in_films', 631 | data_type: 'integer', 632 | character_maximum_length: null, 633 | is_nullable: 'NO', 634 | constraint_name: 'people_in_films_pk', 635 | constraint_type: 'PRIMARY KEY', 636 | foreign_table: null, 637 | foreign_column: null 638 | }, 639 | { 640 | column_name: 'film_id', 641 | table_name: 'people_in_films', 642 | data_type: 'bigint', 643 | character_maximum_length: null, 644 | is_nullable: 'NO', 645 | constraint_name: 'people_in_films_fk1', 646 | constraint_type: 'FOREIGN KEY', 647 | foreign_table: 'films', 648 | foreign_column: '_id' 649 | }, 650 | { 651 | column_name: 'person_id', 652 | table_name: 'people_in_films', 653 | data_type: 'bigint', 654 | character_maximum_length: null, 655 | is_nullable: 'NO', 656 | constraint_name: 'people_in_films_fk0', 657 | constraint_type: 'FOREIGN KEY', 658 | foreign_table: 'people', 659 | foreign_column: '_id' 660 | } 661 | ], 662 | pilots: [ 663 | { 664 | column_name: '_id', 665 | table_name: 'pilots', 666 | data_type: 'integer', 667 | character_maximum_length: null, 668 | is_nullable: 'NO', 669 | constraint_name: 'pilots_pk', 670 | constraint_type: 'PRIMARY KEY', 671 | foreign_table: null, 672 | foreign_column: null 673 | }, 674 | { 675 | column_name: 'person_id', 676 | table_name: 'pilots', 677 | data_type: 'bigint', 678 | character_maximum_length: null, 679 | is_nullable: 'NO', 680 | constraint_name: 'pilots_fk0', 681 | constraint_type: 'FOREIGN KEY', 682 | foreign_table: 'people', 683 | foreign_column: '_id' 684 | }, 685 | { 686 | column_name: 'vessel_id', 687 | table_name: 'pilots', 688 | data_type: 'bigint', 689 | character_maximum_length: null, 690 | is_nullable: 'NO', 691 | constraint_name: 'pilots_fk1', 692 | constraint_type: 'FOREIGN KEY', 693 | foreign_table: 'vessels', 694 | foreign_column: '_id' 695 | } 696 | ], 697 | planets: [ 698 | { 699 | column_name: '_id', 700 | table_name: 'planets', 701 | data_type: 'integer', 702 | character_maximum_length: null, 703 | is_nullable: 'NO', 704 | constraint_name: 'planets_pk', 705 | constraint_type: 'PRIMARY KEY', 706 | foreign_table: null, 707 | foreign_column: null 708 | }, 709 | { 710 | column_name: 'climate', 711 | table_name: 'planets', 712 | data_type: 'character varying', 713 | character_maximum_length: null, 714 | is_nullable: 'YES', 715 | constraint_name: null, 716 | constraint_type: null, 717 | foreign_table: null, 718 | foreign_column: null 719 | }, 720 | { 721 | column_name: 'diameter', 722 | table_name: 'planets', 723 | data_type: 'integer', 724 | character_maximum_length: null, 725 | is_nullable: 'YES', 726 | constraint_name: null, 727 | constraint_type: null, 728 | foreign_table: null, 729 | foreign_column: null 730 | }, 731 | { 732 | column_name: 'gravity', 733 | table_name: 'planets', 734 | data_type: 'character varying', 735 | character_maximum_length: null, 736 | is_nullable: 'YES', 737 | constraint_name: null, 738 | constraint_type: null, 739 | foreign_table: null, 740 | foreign_column: null 741 | }, 742 | { 743 | column_name: 'name', 744 | table_name: 'planets', 745 | data_type: 'character varying', 746 | character_maximum_length: null, 747 | is_nullable: 'YES', 748 | constraint_name: null, 749 | constraint_type: null, 750 | foreign_table: null, 751 | foreign_column: null 752 | }, 753 | { 754 | column_name: 'orbital_period', 755 | table_name: 'planets', 756 | data_type: 'integer', 757 | character_maximum_length: null, 758 | is_nullable: 'YES', 759 | constraint_name: null, 760 | constraint_type: null, 761 | foreign_table: null, 762 | foreign_column: null 763 | }, 764 | { 765 | column_name: 'population', 766 | table_name: 'planets', 767 | data_type: 'bigint', 768 | character_maximum_length: null, 769 | is_nullable: 'YES', 770 | constraint_name: null, 771 | constraint_type: null, 772 | foreign_table: null, 773 | foreign_column: null 774 | }, 775 | { 776 | column_name: 'rotation_period', 777 | table_name: 'planets', 778 | data_type: 'integer', 779 | character_maximum_length: null, 780 | is_nullable: 'YES', 781 | constraint_name: null, 782 | constraint_type: null, 783 | foreign_table: null, 784 | foreign_column: null 785 | }, 786 | { 787 | column_name: 'surface_water', 788 | table_name: 'planets', 789 | data_type: 'character varying', 790 | character_maximum_length: null, 791 | is_nullable: 'YES', 792 | constraint_name: null, 793 | constraint_type: null, 794 | foreign_table: null, 795 | foreign_column: null 796 | }, 797 | { 798 | column_name: 'terrain', 799 | table_name: 'planets', 800 | data_type: 'character varying', 801 | character_maximum_length: null, 802 | is_nullable: 'YES', 803 | constraint_name: null, 804 | constraint_type: null, 805 | foreign_table: null, 806 | foreign_column: null 807 | } 808 | ], 809 | planets_in_films: [ 810 | { 811 | column_name: '_id', 812 | table_name: 'planets_in_films', 813 | data_type: 'integer', 814 | character_maximum_length: null, 815 | is_nullable: 'NO', 816 | constraint_name: 'planets_in_films_pk', 817 | constraint_type: 'PRIMARY KEY', 818 | foreign_table: null, 819 | foreign_column: null 820 | }, 821 | { 822 | column_name: 'film_id', 823 | table_name: 'planets_in_films', 824 | data_type: 'bigint', 825 | character_maximum_length: null, 826 | is_nullable: 'NO', 827 | constraint_name: 'planets_in_films_fk0', 828 | constraint_type: 'FOREIGN KEY', 829 | foreign_table: 'films', 830 | foreign_column: '_id' 831 | }, 832 | { 833 | column_name: 'planet_id', 834 | table_name: 'planets_in_films', 835 | data_type: 'bigint', 836 | character_maximum_length: null, 837 | is_nullable: 'NO', 838 | constraint_name: 'planets_in_films_fk1', 839 | constraint_type: 'FOREIGN KEY', 840 | foreign_table: 'planets', 841 | foreign_column: '_id' 842 | } 843 | ], 844 | species: [ 845 | { 846 | column_name: '_id', 847 | table_name: 'species', 848 | data_type: 'integer', 849 | character_maximum_length: null, 850 | is_nullable: 'NO', 851 | constraint_name: 'species_pk', 852 | constraint_type: 'PRIMARY KEY', 853 | foreign_table: null, 854 | foreign_column: null 855 | }, 856 | { 857 | column_name: 'average_height', 858 | table_name: 'species', 859 | data_type: 'character varying', 860 | character_maximum_length: null, 861 | is_nullable: 'YES', 862 | constraint_name: null, 863 | constraint_type: null, 864 | foreign_table: null, 865 | foreign_column: null 866 | }, 867 | { 868 | column_name: 'average_lifespan', 869 | table_name: 'species', 870 | data_type: 'character varying', 871 | character_maximum_length: null, 872 | is_nullable: 'YES', 873 | constraint_name: null, 874 | constraint_type: null, 875 | foreign_table: null, 876 | foreign_column: null 877 | }, 878 | { 879 | column_name: 'classification', 880 | table_name: 'species', 881 | data_type: 'character varying', 882 | character_maximum_length: null, 883 | is_nullable: 'YES', 884 | constraint_name: null, 885 | constraint_type: null, 886 | foreign_table: null, 887 | foreign_column: null 888 | }, 889 | { 890 | column_name: 'eye_colors', 891 | table_name: 'species', 892 | data_type: 'character varying', 893 | character_maximum_length: null, 894 | is_nullable: 'YES', 895 | constraint_name: null, 896 | constraint_type: null, 897 | foreign_table: null, 898 | foreign_column: null 899 | }, 900 | { 901 | column_name: 'hair_colors', 902 | table_name: 'species', 903 | data_type: 'character varying', 904 | character_maximum_length: null, 905 | is_nullable: 'YES', 906 | constraint_name: null, 907 | constraint_type: null, 908 | foreign_table: null, 909 | foreign_column: null 910 | }, 911 | { 912 | column_name: 'homeworld_id', 913 | table_name: 'species', 914 | data_type: 'bigint', 915 | character_maximum_length: null, 916 | is_nullable: 'YES', 917 | constraint_name: 'species_fk0', 918 | constraint_type: 'FOREIGN KEY', 919 | foreign_table: 'planets', 920 | foreign_column: '_id' 921 | }, 922 | { 923 | column_name: 'language', 924 | table_name: 'species', 925 | data_type: 'character varying', 926 | character_maximum_length: null, 927 | is_nullable: 'YES', 928 | constraint_name: null, 929 | constraint_type: null, 930 | foreign_table: null, 931 | foreign_column: null 932 | }, 933 | { 934 | column_name: 'name', 935 | table_name: 'species', 936 | data_type: 'character varying', 937 | character_maximum_length: null, 938 | is_nullable: 'NO', 939 | constraint_name: null, 940 | constraint_type: null, 941 | foreign_table: null, 942 | foreign_column: null 943 | }, 944 | { 945 | column_name: 'skin_colors', 946 | table_name: 'species', 947 | data_type: 'character varying', 948 | character_maximum_length: null, 949 | is_nullable: 'YES', 950 | constraint_name: null, 951 | constraint_type: null, 952 | foreign_table: null, 953 | foreign_column: null 954 | } 955 | ], 956 | species_in_films: [ 957 | { 958 | column_name: '_id', 959 | table_name: 'species_in_films', 960 | data_type: 'integer', 961 | character_maximum_length: null, 962 | is_nullable: 'NO', 963 | constraint_name: 'species_in_films_pk', 964 | constraint_type: 'PRIMARY KEY', 965 | foreign_table: null, 966 | foreign_column: null 967 | }, 968 | { 969 | column_name: 'film_id', 970 | table_name: 'species_in_films', 971 | data_type: 'bigint', 972 | character_maximum_length: null, 973 | is_nullable: 'NO', 974 | constraint_name: 'species_in_films_fk0', 975 | constraint_type: 'FOREIGN KEY', 976 | foreign_table: 'films', 977 | foreign_column: '_id' 978 | }, 979 | { 980 | column_name: 'species_id', 981 | table_name: 'species_in_films', 982 | data_type: 'bigint', 983 | character_maximum_length: null, 984 | is_nullable: 'NO', 985 | constraint_name: 'species_in_films_fk1', 986 | constraint_type: 'FOREIGN KEY', 987 | foreign_table: 'species', 988 | foreign_column: '_id' 989 | } 990 | ], 991 | starship_specs: [ 992 | { 993 | column_name: 'MGLT', 994 | table_name: 'starship_specs', 995 | data_type: 'character varying', 996 | character_maximum_length: null, 997 | is_nullable: 'YES', 998 | constraint_name: null, 999 | constraint_type: null, 1000 | foreign_table: null, 1001 | foreign_column: null 1002 | }, 1003 | { 1004 | column_name: '_id', 1005 | table_name: 'starship_specs', 1006 | data_type: 'integer', 1007 | character_maximum_length: null, 1008 | is_nullable: 'NO', 1009 | constraint_name: 'starship_specs_pk', 1010 | constraint_type: 'PRIMARY KEY', 1011 | foreign_table: null, 1012 | foreign_column: null 1013 | }, 1014 | { 1015 | column_name: 'hyperdrive_rating', 1016 | table_name: 'starship_specs', 1017 | data_type: 'character varying', 1018 | character_maximum_length: null, 1019 | is_nullable: 'YES', 1020 | constraint_name: null, 1021 | constraint_type: null, 1022 | foreign_table: null, 1023 | foreign_column: null 1024 | }, 1025 | { 1026 | column_name: 'vessel_id', 1027 | table_name: 'starship_specs', 1028 | data_type: 'bigint', 1029 | character_maximum_length: null, 1030 | is_nullable: 'NO', 1031 | constraint_name: 'starship_specs_fk0', 1032 | constraint_type: 'FOREIGN KEY', 1033 | foreign_table: 'vessels', 1034 | foreign_column: '_id' 1035 | } 1036 | ], 1037 | vessels: [ 1038 | { 1039 | column_name: '_id', 1040 | table_name: 'vessels', 1041 | data_type: 'integer', 1042 | character_maximum_length: null, 1043 | is_nullable: 'NO', 1044 | constraint_name: 'vessels_pk', 1045 | constraint_type: 'PRIMARY KEY', 1046 | foreign_table: null, 1047 | foreign_column: null 1048 | }, 1049 | { 1050 | column_name: 'cargo_capacity', 1051 | table_name: 'vessels', 1052 | data_type: 'character varying', 1053 | character_maximum_length: null, 1054 | is_nullable: 'YES', 1055 | constraint_name: null, 1056 | constraint_type: null, 1057 | foreign_table: null, 1058 | foreign_column: null 1059 | }, 1060 | { 1061 | column_name: 'consumables', 1062 | table_name: 'vessels', 1063 | data_type: 'character varying', 1064 | character_maximum_length: null, 1065 | is_nullable: 'YES', 1066 | constraint_name: null, 1067 | constraint_type: null, 1068 | foreign_table: null, 1069 | foreign_column: null 1070 | }, 1071 | { 1072 | column_name: 'cost_in_credits', 1073 | table_name: 'vessels', 1074 | data_type: 'bigint', 1075 | character_maximum_length: null, 1076 | is_nullable: 'YES', 1077 | constraint_name: null, 1078 | constraint_type: null, 1079 | foreign_table: null, 1080 | foreign_column: null 1081 | }, 1082 | { 1083 | column_name: 'crew', 1084 | table_name: 'vessels', 1085 | data_type: 'integer', 1086 | character_maximum_length: null, 1087 | is_nullable: 'YES', 1088 | constraint_name: null, 1089 | constraint_type: null, 1090 | foreign_table: null, 1091 | foreign_column: null 1092 | }, 1093 | { 1094 | column_name: 'length', 1095 | table_name: 'vessels', 1096 | data_type: 'character varying', 1097 | character_maximum_length: null, 1098 | is_nullable: 'YES', 1099 | constraint_name: null, 1100 | constraint_type: null, 1101 | foreign_table: null, 1102 | foreign_column: null 1103 | }, 1104 | { 1105 | column_name: 'manufacturer', 1106 | table_name: 'vessels', 1107 | data_type: 'character varying', 1108 | character_maximum_length: null, 1109 | is_nullable: 'YES', 1110 | constraint_name: null, 1111 | constraint_type: null, 1112 | foreign_table: null, 1113 | foreign_column: null 1114 | }, 1115 | { 1116 | column_name: 'max_atmosphering_speed', 1117 | table_name: 'vessels', 1118 | data_type: 'character varying', 1119 | character_maximum_length: null, 1120 | is_nullable: 'YES', 1121 | constraint_name: null, 1122 | constraint_type: null, 1123 | foreign_table: null, 1124 | foreign_column: null 1125 | }, 1126 | { 1127 | column_name: 'model', 1128 | table_name: 'vessels', 1129 | data_type: 'character varying', 1130 | character_maximum_length: null, 1131 | is_nullable: 'YES', 1132 | constraint_name: null, 1133 | constraint_type: null, 1134 | foreign_table: null, 1135 | foreign_column: null 1136 | }, 1137 | { 1138 | column_name: 'name', 1139 | table_name: 'vessels', 1140 | data_type: 'character varying', 1141 | character_maximum_length: null, 1142 | is_nullable: 'NO', 1143 | constraint_name: null, 1144 | constraint_type: null, 1145 | foreign_table: null, 1146 | foreign_column: null 1147 | }, 1148 | { 1149 | column_name: 'passengers', 1150 | table_name: 'vessels', 1151 | data_type: 'integer', 1152 | character_maximum_length: null, 1153 | is_nullable: 'YES', 1154 | constraint_name: null, 1155 | constraint_type: null, 1156 | foreign_table: null, 1157 | foreign_column: null 1158 | }, 1159 | { 1160 | column_name: 'vessel_class', 1161 | table_name: 'vessels', 1162 | data_type: 'character varying', 1163 | character_maximum_length: null, 1164 | is_nullable: 'NO', 1165 | constraint_name: null, 1166 | constraint_type: null, 1167 | foreign_table: null, 1168 | foreign_column: null 1169 | }, 1170 | { 1171 | column_name: 'vessel_type', 1172 | table_name: 'vessels', 1173 | data_type: 'character varying', 1174 | character_maximum_length: null, 1175 | is_nullable: 'NO', 1176 | constraint_name: null, 1177 | constraint_type: null, 1178 | foreign_table: null, 1179 | foreign_column: null 1180 | } 1181 | ], 1182 | vessels_in_films: [ 1183 | { 1184 | column_name: '_id', 1185 | table_name: 'vessels_in_films', 1186 | data_type: 'integer', 1187 | character_maximum_length: null, 1188 | is_nullable: 'NO', 1189 | constraint_name: 'vessels_in_films_pk', 1190 | constraint_type: 'PRIMARY KEY', 1191 | foreign_table: null, 1192 | foreign_column: null 1193 | }, 1194 | { 1195 | column_name: 'film_id', 1196 | table_name: 'vessels_in_films', 1197 | data_type: 'bigint', 1198 | character_maximum_length: null, 1199 | is_nullable: 'NO', 1200 | constraint_name: 'vessels_in_films_fk1', 1201 | constraint_type: 'FOREIGN KEY', 1202 | foreign_table: 'films', 1203 | foreign_column: '_id' 1204 | }, 1205 | { 1206 | column_name: 'vessel_id', 1207 | table_name: 'vessels_in_films', 1208 | data_type: 'bigint', 1209 | character_maximum_length: null, 1210 | is_nullable: 'NO', 1211 | constraint_name: 'vessels_in_films_fk0', 1212 | constraint_type: 'FOREIGN KEY', 1213 | foreign_table: 'vessels', 1214 | foreign_column: '_id' 1215 | } 1216 | ] 1217 | } 1218 | /** baseTables **/ 1219 | { 1220 | planets: [ 1221 | { 1222 | column_name: '_id', 1223 | table_name: 'planets', 1224 | data_type: 'integer', 1225 | character_maximum_length: null, 1226 | is_nullable: 'NO', 1227 | constraint_name: 'planets_pk', 1228 | constraint_type: 'PRIMARY KEY', 1229 | foreign_table: null, 1230 | foreign_column: null 1231 | }, 1232 | { 1233 | column_name: 'climate', 1234 | table_name: 'planets', 1235 | data_type: 'character varying', 1236 | character_maximum_length: null, 1237 | is_nullable: 'YES', 1238 | constraint_name: null, 1239 | constraint_type: null, 1240 | foreign_table: null, 1241 | foreign_column: null 1242 | }, 1243 | { 1244 | column_name: 'diameter', 1245 | table_name: 'planets', 1246 | data_type: 'integer', 1247 | character_maximum_length: null, 1248 | is_nullable: 'YES', 1249 | constraint_name: null, 1250 | constraint_type: null, 1251 | foreign_table: null, 1252 | foreign_column: null 1253 | }, 1254 | { 1255 | column_name: 'gravity', 1256 | table_name: 'planets', 1257 | data_type: 'character varying', 1258 | character_maximum_length: null, 1259 | is_nullable: 'YES', 1260 | constraint_name: null, 1261 | constraint_type: null, 1262 | foreign_table: null, 1263 | foreign_column: null 1264 | }, 1265 | { 1266 | column_name: 'name', 1267 | table_name: 'planets', 1268 | data_type: 'character varying', 1269 | character_maximum_length: null, 1270 | is_nullable: 'YES', 1271 | constraint_name: null, 1272 | constraint_type: null, 1273 | foreign_table: null, 1274 | foreign_column: null 1275 | }, 1276 | { 1277 | column_name: 'orbital_period', 1278 | table_name: 'planets', 1279 | data_type: 'integer', 1280 | character_maximum_length: null, 1281 | is_nullable: 'YES', 1282 | constraint_name: null, 1283 | constraint_type: null, 1284 | foreign_table: null, 1285 | foreign_column: null 1286 | }, 1287 | { 1288 | column_name: 'population', 1289 | table_name: 'planets', 1290 | data_type: 'bigint', 1291 | character_maximum_length: null, 1292 | is_nullable: 'YES', 1293 | constraint_name: null, 1294 | constraint_type: null, 1295 | foreign_table: null, 1296 | foreign_column: null 1297 | }, 1298 | { 1299 | column_name: 'rotation_period', 1300 | table_name: 'planets', 1301 | data_type: 'integer', 1302 | character_maximum_length: null, 1303 | is_nullable: 'YES', 1304 | constraint_name: null, 1305 | constraint_type: null, 1306 | foreign_table: null, 1307 | foreign_column: null 1308 | }, 1309 | ], 1310 | } 1311 | 1312 | /** joinTables **/ 1313 | joinTables { 1314 | people_in_films: [ 1315 | { 1316 | column_name: '_id', 1317 | table_name: 'people_in_films', 1318 | data_type: 'integer', 1319 | character_maximum_length: null, 1320 | is_nullable: 'NO', 1321 | constraint_name: 'people_in_films_pk', 1322 | constraint_type: 'PRIMARY KEY', 1323 | foreign_table: null, 1324 | foreign_column: null 1325 | }, 1326 | { 1327 | column_name: 'film_id', 1328 | table_name: 'people_in_films', 1329 | data_type: 'bigint', 1330 | character_maximum_length: null, 1331 | is_nullable: 'NO', 1332 | constraint_name: 'people_in_films_fk1', 1333 | constraint_type: 'FOREIGN KEY', 1334 | foreign_table: 'films', 1335 | foreign_column: '_id' 1336 | }, 1337 | { 1338 | column_name: 'person_id', 1339 | table_name: 'people_in_films', 1340 | data_type: 'bigint', 1341 | character_maximum_length: null, 1342 | is_nullable: 'NO', 1343 | constraint_name: 'people_in_films_fk0', 1344 | constraint_type: 'FOREIGN KEY', 1345 | foreign_table: 'people', 1346 | foreign_column: '_id' 1347 | } 1348 | ], 1349 | pilots: [ 1350 | { 1351 | column_name: '_id', 1352 | table_name: 'pilots', 1353 | data_type: 'integer', 1354 | character_maximum_length: null, 1355 | is_nullable: 'NO', 1356 | constraint_name: 'pilots_pk', 1357 | constraint_type: 'PRIMARY KEY', 1358 | foreign_table: null, 1359 | foreign_column: null 1360 | }, 1361 | { 1362 | column_name: 'person_id', 1363 | table_name: 'pilots', 1364 | data_type: 'bigint', 1365 | character_maximum_length: null, 1366 | is_nullable: 'NO', 1367 | constraint_name: 'pilots_fk0', 1368 | constraint_type: 'FOREIGN KEY', 1369 | foreign_table: 'people', 1370 | foreign_column: '_id' 1371 | }, 1372 | { 1373 | column_name: 'vessel_id', 1374 | table_name: 'pilots', 1375 | data_type: 'bigint', 1376 | character_maximum_length: null, 1377 | is_nullable: 'NO', 1378 | constraint_name: 'pilots_fk1', 1379 | constraint_type: 'FOREIGN KEY', 1380 | foreign_table: 'vessels', 1381 | foreign_column: '_id' 1382 | } 1383 | ], 1384 | planets_in_films: [ 1385 | { 1386 | column_name: '_id', 1387 | table_name: 'planets_in_films', 1388 | data_type: 'integer', 1389 | character_maximum_length: null, 1390 | is_nullable: 'NO', 1391 | constraint_name: 'planets_in_films_pk', 1392 | constraint_type: 'PRIMARY KEY', 1393 | foreign_table: null, 1394 | foreign_column: null 1395 | }, 1396 | { 1397 | column_name: 'film_id', 1398 | table_name: 'planets_in_films', 1399 | data_type: 'bigint', 1400 | character_maximum_length: null, 1401 | is_nullable: 'NO', 1402 | constraint_name: 'planets_in_films_fk0', 1403 | constraint_type: 'FOREIGN KEY', 1404 | foreign_table: 'films', 1405 | foreign_column: '_id' 1406 | }, 1407 | { 1408 | column_name: 'planet_id', 1409 | table_name: 'planets_in_films', 1410 | data_type: 'bigint', 1411 | character_maximum_length: null, 1412 | is_nullable: 'NO', 1413 | constraint_name: 'planets_in_films_fk1', 1414 | constraint_type: 'FOREIGN KEY', 1415 | foreign_table: 'planets', 1416 | foreign_column: '_id' 1417 | } 1418 | ], 1419 | species_in_films: [ 1420 | { 1421 | column_name: '_id', 1422 | table_name: 'species_in_films', 1423 | data_type: 'integer', 1424 | character_maximum_length: null, 1425 | is_nullable: 'NO', 1426 | constraint_name: 'species_in_films_pk', 1427 | constraint_type: 'PRIMARY KEY', 1428 | foreign_table: null, 1429 | foreign_column: null 1430 | }, 1431 | { 1432 | column_name: 'film_id', 1433 | table_name: 'species_in_films', 1434 | data_type: 'bigint', 1435 | character_maximum_length: null, 1436 | is_nullable: 'NO', 1437 | constraint_name: 'species_in_films_fk0', 1438 | constraint_type: 'FOREIGN KEY', 1439 | foreign_table: 'films', 1440 | foreign_column: '_id' 1441 | }, 1442 | { 1443 | column_name: 'species_id', 1444 | table_name: 'species_in_films', 1445 | data_type: 'bigint', 1446 | character_maximum_length: null, 1447 | is_nullable: 'NO', 1448 | constraint_name: 'species_in_films_fk1', 1449 | constraint_type: 'FOREIGN KEY', 1450 | foreign_table: 'species', 1451 | foreign_column: '_id' 1452 | } 1453 | ], 1454 | vessels_in_films: [ 1455 | { 1456 | column_name: '_id', 1457 | table_name: 'vessels_in_films', 1458 | data_type: 'integer', 1459 | character_maximum_length: null, 1460 | is_nullable: 'NO', 1461 | constraint_name: 'vessels_in_films_pk', 1462 | constraint_type: 'PRIMARY KEY', 1463 | foreign_table: null, 1464 | foreign_column: null 1465 | }, 1466 | { 1467 | column_name: 'film_id', 1468 | table_name: 'vessels_in_films', 1469 | data_type: 'bigint', 1470 | character_maximum_length: null, 1471 | is_nullable: 'NO', 1472 | constraint_name: 'vessels_in_films_fk1', 1473 | constraint_type: 'FOREIGN KEY', 1474 | foreign_table: 'films', 1475 | foreign_column: '_id' 1476 | }, 1477 | { 1478 | column_name: 'vessel_id', 1479 | table_name: 'vessels_in_films', 1480 | data_type: 'bigint', 1481 | character_maximum_length: null, 1482 | is_nullable: 'NO', 1483 | constraint_name: 'vessels_in_films_fk0', 1484 | constraint_type: 'FOREIGN KEY', 1485 | foreign_table: 'vessels', 1486 | foreign_column: '_id' 1487 | } 1488 | ] 1489 | } 1490 | 1491 | /** baseTableNames **/ 1492 | [ 1493 | 'films', 1494 | 'people', 1495 | 'planets', 1496 | 'species', 1497 | 'starship_specs', 1498 | 'vessels' 1499 | ] 1500 | 1501 | /** joinTableNames **/ 1502 | [ 1503 | 'people_in_films', 1504 | 'pilots', 1505 | 'planets_in_films', 1506 | 'species_in_films', 1507 | 'vessels_in_films' 1508 | ] 1509 | 1510 | /** schema **/ 1511 | { 1512 | films: { 1513 | _id: 'ID!', 1514 | director: 'String!', 1515 | episode_id: 'Int!', 1516 | opening_crawl: 'String!', 1517 | producer: 'String!', 1518 | release_date: 'String!', 1519 | title: 'String!', 1520 | people: '[Person]', 1521 | planets: '[Planet]', 1522 | species: '[Species]', 1523 | vessels: '[Vessel]' 1524 | }, 1525 | people: { 1526 | _id: 'ID!', 1527 | birth_year: 'String', 1528 | eye_color: 'String', 1529 | gender: 'String', 1530 | hair_color: 'String', 1531 | height: 'Int', 1532 | planets: '[Planet]', 1533 | mass: 'String', 1534 | name: 'String!', 1535 | skin_color: 'String', 1536 | species: '[Species]', 1537 | films: '[Film]', 1538 | vessels: '[Vessel]' 1539 | }, 1540 | planets: { 1541 | _id: 'ID!', 1542 | climate: 'String', 1543 | diameter: 'Int', 1544 | gravity: 'String', 1545 | name: 'String', 1546 | orbital_period: 'Int', 1547 | population: 'Float', 1548 | rotation_period: 'Int', 1549 | surface_water: 'String', 1550 | terrain: 'String', 1551 | people: '[Person]', 1552 | species: '[Species]', 1553 | films: '[Film]' 1554 | }, 1555 | species: { 1556 | _id: 'ID!', 1557 | average_height: 'String', 1558 | average_lifespan: 'String', 1559 | classification: 'String', 1560 | eye_colors: 'String', 1561 | hair_colors: 'String', 1562 | planets: '[Planet]', 1563 | language: 'String', 1564 | name: 'String!', 1565 | skin_colors: 'String', 1566 | people: '[Person]', 1567 | films: '[Film]' 1568 | }, 1569 | starship_specs: { 1570 | MGLT: 'String', 1571 | _id: 'ID!', 1572 | hyperdrive_rating: 'String', 1573 | vessels: '[Vessel]' 1574 | }, 1575 | vessels: { 1576 | _id: 'ID!', 1577 | cargo_capacity: 'String', 1578 | consumables: 'String', 1579 | cost_in_credits: 'Float', 1580 | crew: 'Int', 1581 | length: 'String', 1582 | manufacturer: 'String', 1583 | max_atmosphering_speed: 'String', 1584 | model: 'String', 1585 | name: 'String!', 1586 | passengers: 'Int', 1587 | vessel_class: 'String!', 1588 | vessel_type: 'String!', 1589 | starshipSpecs: '[StarshipSpec]', 1590 | people: '[Person]', 1591 | films: '[Film]' 1592 | } 1593 | } 1594 | 1595 | /** mutationObj **/ 1596 | { 1597 | addFilm: { 1598 | director: 'String!', 1599 | episode_id: 'Int!', 1600 | opening_crawl: 'String!', 1601 | producer: 'String!', 1602 | release_date: 'String!', 1603 | title: 'String!', 1604 | formatted_table_name_for_dev_use: 'Film', 1605 | table_name_for_dev_use: 'films' 1606 | }, 1607 | updateFilm: { 1608 | _id: 'ID', 1609 | director: 'String', 1610 | episode_id: 'Int', 1611 | opening_crawl: 'String', 1612 | producer: 'String', 1613 | release_date: 'String', 1614 | title: 'String', 1615 | formatted_table_name_for_dev_use: 'Film', 1616 | table_name_for_dev_use: 'films' 1617 | }, 1618 | deleteFilm: { 1619 | _id: 'ID!', 1620 | formatted_table_name_for_dev_use: 'Film', 1621 | table_name_for_dev_use: 'films' 1622 | }, 1623 | addPerson: { 1624 | birth_year: 'String', 1625 | eye_color: 'String', 1626 | gender: 'String', 1627 | hair_color: 'String', 1628 | height: 'Int', 1629 | homeworld_id: 'Int', 1630 | mass: 'String', 1631 | name: 'String!', 1632 | skin_color: 'String', 1633 | species_id: 'Int', 1634 | formatted_table_name_for_dev_use: 'Person', 1635 | table_name_for_dev_use: 'people' 1636 | }, 1637 | updatePerson: { 1638 | _id: 'ID', 1639 | birth_year: 'String', 1640 | eye_color: 'String', 1641 | gender: 'String', 1642 | hair_color: 'String', 1643 | height: 'Int', 1644 | homeworld_id: 'Int', 1645 | mass: 'String', 1646 | name: 'String', 1647 | skin_color: 'String', 1648 | species_id: 'Int', 1649 | formatted_table_name_for_dev_use: 'Person', 1650 | table_name_for_dev_use: 'people' 1651 | }, 1652 | deletePerson: { 1653 | _id: 'ID!', 1654 | formatted_table_name_for_dev_use: 'Person', 1655 | table_name_for_dev_use: 'people' 1656 | }, 1657 | addPlanet: { 1658 | climate: 'String', 1659 | diameter: 'Int', 1660 | gravity: 'String', 1661 | name: 'String', 1662 | orbital_period: 'Int', 1663 | population: 'Float', 1664 | rotation_period: 'Int', 1665 | surface_water: 'String', 1666 | terrain: 'String', 1667 | formatted_table_name_for_dev_use: 'Planet', 1668 | table_name_for_dev_use: 'planets' 1669 | }, 1670 | updatePlanet: { 1671 | _id: 'ID', 1672 | climate: 'String', 1673 | diameter: 'Int', 1674 | gravity: 'String', 1675 | name: 'String', 1676 | orbital_period: 'Int', 1677 | population: 'Float', 1678 | rotation_period: 'Int', 1679 | surface_water: 'String', 1680 | terrain: 'String', 1681 | formatted_table_name_for_dev_use: 'Planet', 1682 | table_name_for_dev_use: 'planets' 1683 | }, 1684 | deletePlanet: { 1685 | _id: 'ID!', 1686 | formatted_table_name_for_dev_use: 'Planet', 1687 | table_name_for_dev_use: 'planets' 1688 | }, 1689 | addSpecies: { 1690 | average_height: 'String', 1691 | average_lifespan: 'String', 1692 | classification: 'String', 1693 | eye_colors: 'String', 1694 | hair_colors: 'String', 1695 | homeworld_id: 'Int', 1696 | language: 'String', 1697 | name: 'String!', 1698 | skin_colors: 'String', 1699 | formatted_table_name_for_dev_use: 'Species', 1700 | table_name_for_dev_use: 'species' 1701 | }, 1702 | updateSpecies: { 1703 | _id: 'ID', 1704 | average_height: 'String', 1705 | average_lifespan: 'String', 1706 | classification: 'String', 1707 | eye_colors: 'String', 1708 | hair_colors: 'String', 1709 | homeworld_id: 'Int', 1710 | language: 'String', 1711 | name: 'String', 1712 | skin_colors: 'String', 1713 | formatted_table_name_for_dev_use: 'Species', 1714 | table_name_for_dev_use: 'species' 1715 | }, 1716 | deleteSpecies: { 1717 | _id: 'ID!', 1718 | formatted_table_name_for_dev_use: 'Species', 1719 | table_name_for_dev_use: 'species' 1720 | }, 1721 | addStarshipSpec: { 1722 | MGLT: 'String', 1723 | hyperdrive_rating: 'String', 1724 | vessel_id: 'Int!', 1725 | formatted_table_name_for_dev_use: 'StarshipSpec', 1726 | table_name_for_dev_use: 'starship_specs' 1727 | }, 1728 | updateStarshipSpec: { 1729 | MGLT: 'String', 1730 | _id: 'ID', 1731 | hyperdrive_rating: 'String', 1732 | vessel_id: 'Int', 1733 | formatted_table_name_for_dev_use: 'StarshipSpec', 1734 | table_name_for_dev_use: 'starship_specs' 1735 | }, 1736 | deleteStarshipSpec: { 1737 | _id: 'ID!', 1738 | formatted_table_name_for_dev_use: 'StarshipSpec', 1739 | table_name_for_dev_use: 'starship_specs' 1740 | }, 1741 | addVessel: { 1742 | cargo_capacity: 'String', 1743 | consumables: 'String', 1744 | cost_in_credits: 'Float', 1745 | crew: 'Int', 1746 | length: 'String', 1747 | manufacturer: 'String', 1748 | max_atmosphering_speed: 'String', 1749 | model: 'String', 1750 | name: 'String!', 1751 | passengers: 'Int', 1752 | vessel_class: 'String!', 1753 | vessel_type: 'String!', 1754 | formatted_table_name_for_dev_use: 'Vessel', 1755 | table_name_for_dev_use: 'vessels' 1756 | }, 1757 | updateVessel: { 1758 | _id: 'ID', 1759 | cargo_capacity: 'String', 1760 | consumables: 'String', 1761 | cost_in_credits: 'Float', 1762 | crew: 'Int', 1763 | length: 'String', 1764 | manufacturer: 'String', 1765 | max_atmosphering_speed: 'String', 1766 | model: 'String', 1767 | name: 'String', 1768 | passengers: 'Int', 1769 | vessel_class: 'String', 1770 | vessel_type: 'String', 1771 | formatted_table_name_for_dev_use: 'Vessel', 1772 | table_name_for_dev_use: 'vessels' 1773 | }, 1774 | deleteVessel: { 1775 | _id: 'ID!', 1776 | formatted_table_name_for_dev_use: 'Vessel', 1777 | table_name_for_dev_use: 'vessels' 1778 | } 1779 | } -------------------------------------------------------------------------------- /docs/sql-notes.txt: -------------------------------------------------------------------------------- 1 | 2 | /* SELECT * FROM pg_catalog.pg_tables 3 | WHERE schemaname ='public' */ 4 | ***** pg_catalog.pg_tables ***** 5 | - tablename (planets / people_in_films) 6 | 7 | /* SELECT * FROM information_schema.tables WHERE table_schema = 'public' AND table_type = 'BASE TABLE'*/ 8 | ***** information_schema.tables ***** 9 | - table_name (planets / people_in_films) 10 | 11 | 12 | /* select * FROM information_schema.columns where table_name = 'planets' */ 13 | for every table, get all the columns (fields) 14 | for each column, we want info like: 15 | ***** information_schema.columns ***** 16 | - table_name (people) (kcu) 17 | - column_name (_id) (kcu) 18 | - ordinal_position (1) 19 | - column_default (nextval('people__id_seq'::regclass) ) 20 | - is_nullable (NO) 21 | - data_type (integer) 22 | - udt_name (int4) 23 | 24 | 25 | ***** information_schema.key_column_usage (returns one row for each column that is constrained as a key) ***** 26 | foreign keys, primary keys 27 | - constraint_name (people_fk0 / people_fk1 /people_pk) 28 | - table_name (people / people / people) 29 | - column_name (species_id / homeworld_id / _id) 30 | 31 | ***** information_schema.table_constraints (returns one row for each table constraint) ***** 32 | - constraint_type (PRIMARY KEY) / (FOREIGN KEY) 33 | - table_name (people) / (people) 34 | - constraint_name (people_pk) / (people_fk0) 35 | 36 | ***** information_schema.referential_constraints (returns one row for each foreign key constraint) ***** 37 | 38 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ArtemisQL 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * For a detailed explanation regarding each configuration property and type check, visit: 3 | * https://jestjs.io/docs/configuration 4 | */ 5 | 6 | export default { 7 | // All imported modules in your tests should be mocked automatically 8 | // automock: false, 9 | 10 | // Stop running tests after `n` failures 11 | // bail: 0, 12 | 13 | // The directory where Jest should store its cached dependency information 14 | // cacheDirectory: "/private/var/folders/hn/_v27lq0n7d14clqcxnxgtxj80000gn/T/jest_dx", 15 | 16 | // Automatically clear mock calls and instances between every test 17 | clearMocks: true, 18 | testEnvironment: 'node', 19 | preset: 'ts-jest/presets/js-with-ts', 20 | 21 | // Indicates whether the coverage information should be collected while executing the test 22 | // collectCoverage: false, 23 | 24 | // An array of glob patterns indicating a set of files for which coverage information should be collected 25 | // collectCoverageFrom: undefined, 26 | 27 | // The directory where Jest should output its coverage files 28 | // coverageDirectory: undefined, 29 | 30 | // An array of regexp pattern strings used to skip coverage collection 31 | // coveragePathIgnorePatterns: [ 32 | // "/node_modules/" 33 | // ], 34 | 35 | // Indicates which provider should be used to instrument code for coverage 36 | // coverageProvider: "babel", 37 | 38 | // A list of reporter names that Jest uses when writing coverage reports 39 | // coverageReporters: [ 40 | // "json", 41 | // "text", 42 | // "lcov", 43 | // "clover" 44 | // ], 45 | 46 | // An object that configures minimum threshold enforcement for coverage results 47 | // coverageThreshold: undefined, 48 | 49 | // A path to a custom dependency extractor 50 | // dependencyExtractor: undefined, 51 | 52 | // Make calling deprecated APIs throw helpful error messages 53 | // errorOnDeprecated: false, 54 | 55 | // Force coverage collection from ignored files using an array of glob patterns 56 | // forceCoverageMatch: [], 57 | 58 | // A path to a module which exports an async function that is triggered once before all test suites 59 | // globalSetup: undefined, 60 | 61 | // A path to a module which exports an async function that is triggered once after all test suites 62 | // globalTeardown: undefined, 63 | 64 | // A set of global variables that need to be available in all test environments 65 | // globals: {}, 66 | 67 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. 68 | // maxWorkers: "50%", 69 | 70 | // An array of directory names to be searched recursively up from the requiring module's location 71 | // moduleDirectories: [ 72 | // "node_modules" 73 | // ], 74 | 75 | // An array of file extensions your modules use 76 | // moduleFileExtensions: [ 77 | // "js", 78 | // "jsx", 79 | // "ts", 80 | // "tsx", 81 | // "json", 82 | // "node" 83 | // ], 84 | 85 | // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module 86 | // moduleNameMapper: {}, 87 | 88 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 89 | // modulePathIgnorePatterns: [], 90 | 91 | // Activates notifications for test results 92 | // notify: false, 93 | 94 | // An enum that specifies notification mode. Requires { notify: true } 95 | // notifyMode: "failure-change", 96 | 97 | // A preset that is used as a base for Jest's configuration 98 | // preset: undefined, 99 | 100 | // Run tests from one or more projects 101 | // projects: undefined, 102 | 103 | // Use this configuration option to add custom reporters to Jest 104 | // reporters: undefined, 105 | 106 | // Automatically reset mock state between every test 107 | // resetMocks: false, 108 | 109 | // Reset the module registry before running each individual test 110 | // resetModules: false, 111 | 112 | // A path to a custom resolver 113 | // resolver: undefined, 114 | 115 | // Automatically restore mock state between every test 116 | // restoreMocks: false, 117 | 118 | // The root directory that Jest should scan for tests and modules within 119 | // rootDir: undefined, 120 | 121 | // A list of paths to directories that Jest should use to search for files in 122 | // roots: [ 123 | // "" 124 | // ], 125 | 126 | // Allows you to use a custom runner instead of Jest's default test runner 127 | // runner: "jest-runner", 128 | 129 | // The paths to modules that run some code to configure or set up the testing environment before each test 130 | // setupFiles: [], 131 | 132 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 133 | // setupFilesAfterEnv: [], 134 | 135 | // The number of seconds after which a test is considered as slow and reported as such in the results. 136 | // slowTestThreshold: 5, 137 | 138 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 139 | // snapshotSerializers: [], 140 | 141 | // The test environment that will be used for testing 142 | // testEnvironment: "jest-environment-node", 143 | 144 | // Options that will be passed to the testEnvironment 145 | // testEnvironmentOptions: {}, 146 | 147 | // Adds a location field to test results 148 | // testLocationInResults: false, 149 | 150 | // The glob patterns Jest uses to detect test files 151 | // testMatch: [ 152 | // "**/__tests__/**/*.[jt]s?(x)", 153 | // "**/?(*.)+(spec|test).[tj]s?(x)" 154 | // ], 155 | 156 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 157 | // testPathIgnorePatterns: [ 158 | // "/node_modules/" 159 | // ], 160 | 161 | // The regexp pattern or array of patterns that Jest uses to detect test files 162 | // testRegex: [], 163 | 164 | // This option allows the use of a custom results processor 165 | // testResultsProcessor: undefined, 166 | 167 | // This option allows use of a custom test runner 168 | // testRunner: "jest-circus/runner", 169 | 170 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 171 | // testURL: "http://localhost", 172 | 173 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 174 | // timers: "real", 175 | 176 | // A map from regular expressions to paths to transformers 177 | // transform: undefined, 178 | 179 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 180 | // transformIgnorePatterns: [ 181 | // "/node_modules/", 182 | // "\\.pnp\\.[^\\/]+$" 183 | // ], 184 | 185 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 186 | // unmockedModulePathPatterns: undefined, 187 | 188 | // Indicates whether each individual test should be reported during the run 189 | // verbose: undefined, 190 | 191 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 192 | // watchPathIgnorePatterns: [], 193 | 194 | // Whether to use watchman for file crawling 195 | // watchman: true, 196 | }; 197 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "artemisql", 3 | "version": "1.0.0", 4 | "description": "A GraphQL migration tool and relational database visualizer", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node --loader ts-node/esm server/server.ts", 8 | "build": "webpack", 9 | "dev": "nodemon server/server.ts & webpack serve --open --hot", 10 | "test": "jest" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/oslabs-beta/ArtemisQL.git" 15 | }, 16 | "author": "Johnny Bryan, Jennifer Chau, John Lin, Taras Sukhoverskyi", 17 | "license": "MIT", 18 | "bugs": { 19 | "url": "https://github.com/oslabs-beta/ArtemisQL/issues" 20 | }, 21 | "homepage": "https://github.com/oslabs-beta/ArtemisQL#readme", 22 | "dependencies": { 23 | "@emotion/react": "^11.6.0", 24 | "@emotion/styled": "^11.6.0", 25 | "@fontsource/roboto": "^4.5.1", 26 | "@mui/icons-material": "^5.1.1", 27 | "@mui/material": "5.1.1", 28 | "@mui/utils": "5.1.1", 29 | "@types/webpack-dev-server": "^4.5.0", 30 | "axios": "^0.24.0", 31 | "css-loader": "^6.5.1", 32 | "dotenv": "^10.0.0", 33 | "express": "^4.17.1", 34 | "express-graphql": "^0.12.0", 35 | "graphiql": "^1.5.8", 36 | "graphql": "^15.7.2", 37 | "graphql-tools": "^8.2.0", 38 | "highlight.js": "^11.3.1", 39 | "html-webpack-plugin": "^5.5.0", 40 | "pg": "^8.7.1", 41 | "pluralize": "^8.0.0", 42 | "react": "^17.0.2", 43 | "react-dom": "^17.0.2", 44 | "react-flow-renderer": "^9.6.11", 45 | "react-highlight": "^0.14.0", 46 | "react-router": "^6.0.2", 47 | "react-router-dom": "^6.0.2", 48 | "style-loader": "^3.3.1", 49 | "ts-node": "^10.4.0", 50 | "webpack": "^5.64.2" 51 | }, 52 | "devDependencies": { 53 | "@babel/core": "^7.16.0", 54 | "@babel/preset-env": "^7.16.4", 55 | "@babel/preset-react": "^7.16.0", 56 | "@babel/preset-typescript": "^7.16.0", 57 | "@types/express": "^4.17.13", 58 | "@types/jest": "^27.0.3", 59 | "@types/node": "^16.11.9", 60 | "@types/react-dom": "^17.0.11", 61 | "@typescript-eslint/eslint-plugin": "^5.4.0", 62 | "@typescript-eslint/parser": "^5.4.0", 63 | "babel-loader": "^8.2.3", 64 | "concurrently": "^6.4.0", 65 | "eslint": "^8.3.0", 66 | "eslint-config-airbnb": "^19.0.1", 67 | "eslint-plugin-import": "^2.25.3", 68 | "eslint-plugin-jsx-a11y": "^6.5.1", 69 | "eslint-plugin-react": "^7.27.1", 70 | "eslint-plugin-react-hooks": "^4.3.0", 71 | "file-loader": "^6.2.0", 72 | "html-webpack-plugin": "^5.5.0", 73 | "jest": "^27.4.5", 74 | "nodemon": "^2.0.15", 75 | "supertest": "^6.1.6", 76 | "ts-jest": "^27.1.1", 77 | "typescript": "^4.5.4", 78 | "webpack": "^5.64.2", 79 | "webpack-cli": "^4.9.1", 80 | "webpack-dev-server": "^4.5.0" 81 | }, 82 | "resolutions": { 83 | "@mui/utils": "5.1.1", 84 | "@mui/material": "5.1.1" 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /server/controllers/GQLController.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | import { GQLControllerType, MutationObjectType } from './controllerTypes'; 3 | import typeConverter from '../converters/typeConverter'; 4 | import queryConverter from '../converters/queryConverter'; 5 | import mutationConverter from '../converters/mutationConverter'; 6 | import resolvers from '../converters/resolvers'; 7 | const { capitalizeAndSingularize } = require('../utils/helperFunc'); 8 | 9 | // Create GraphQL Schema (Type Defs) 10 | const createSchemaTypeDefs = (req: Request, res: Response, next: NextFunction) => { 11 | const schema = {}; 12 | const bTables = {}; 13 | const jTables = {}; 14 | const { allTables } = res.locals; 15 | // sort the tables 16 | const { baseTables, joinTables } = typeConverter.sortTables(allTables, bTables, jTables); 17 | // exclude join table column information 18 | const baseTableQuery = typeConverter.createBaseTableQuery(baseTables); 19 | // store table names in separate arrays 20 | const baseTableNames = Object.keys(baseTables); 21 | const joinTableNames = Object.keys(joinTables); 22 | 23 | // store pertinent information in res.locals for later usage 24 | res.locals.baseTables = baseTables; 25 | res.locals.joinTables = joinTables; 26 | res.locals.baseTableNames = baseTableNames; 27 | res.locals.joinTableNames = joinTableNames; 28 | res.locals.baseTableQuery = baseTableQuery; 29 | 30 | // create the initial type defs for the schemas for base tables 31 | for (const key of baseTableNames) { 32 | schema[key] = typeConverter.createInitialTypeDef(key, baseTables, baseTableQuery); 33 | } 34 | 35 | // add foreign keys to the initial type def 36 | for (const key of joinTableNames) { 37 | // schema gets mutated here, so no need to return anything 38 | typeConverter.addForeignKeysToTypeDef(key, schema, joinTables); 39 | } 40 | 41 | // convert schema object to string for client 42 | const typeString = typeConverter.finalizeTypeDef(schema); 43 | 44 | res.locals.schema = schema; 45 | res.locals.typeString = typeString; 46 | return next(); 47 | }; 48 | 49 | // create GraphQL Schema (queries) 50 | const createSchemaQuery = (req, res, next) => { 51 | const { schema, typeString } = res.locals; 52 | 53 | // create opening curly brace 54 | let queryString = `\ntype Query { \n`; 55 | 56 | for (const key in schema) { 57 | queryString += queryConverter.createQuerySchema(key); 58 | } 59 | 60 | // create the closing curly brace 61 | queryString += `}`; 62 | 63 | // append the query string with the type string 64 | res.locals.finalString = typeString + queryString; 65 | 66 | return next(); 67 | }; 68 | 69 | // create GraphQL Schema (mutations) 70 | const createSchemaMutation = (req: Request, res: Response, next: NextFunction) => { 71 | const { baseTables } = res.locals; 72 | 73 | const mutationObj: MutationObjectType = {}; 74 | 75 | // loop through base tables object 76 | for (const key in baseTables) { 77 | /// invoke add 78 | const [add, addMutations] = mutationConverter.add(key, baseTables[key]) as [string, object]; 79 | mutationObj[add] = addMutations; 80 | 81 | // invoke update 82 | // 'as', guarantees that mutationConverter will return an array with first el as string and second el as object 83 | // ideally, need to create a type of function update that returns an array 84 | const [update, updateMutations] = mutationConverter 85 | .update(key, baseTables[key]) as [string, object]; 86 | mutationObj[update] = updateMutations; 87 | 88 | // invoke delete 89 | const [del, deleteMutations] = mutationConverter 90 | .del(key, baseTables[key]) as [string, object]; 91 | mutationObj[del] = deleteMutations; 92 | } 93 | 94 | // format/stringify mutationObj 95 | const mutationString = mutationConverter.stringify(mutationObj); 96 | res.locals.mutationObj = mutationObj; 97 | res.locals.finalString += mutationString; 98 | return next(); 99 | }; 100 | 101 | // create GraphQL Resolvers 102 | const createResolver = (req: Request, res: Response, next: NextFunction) => { 103 | const { 104 | baseTableNames, 105 | mutationObj, 106 | baseTableQuery, 107 | baseTables, 108 | joinTables, 109 | } = res.locals; 110 | 111 | // opening curly brace 112 | let resolverString = `const resolvers = { \n`; 113 | 114 | // QUERY portion of the resolver 115 | resolverString += ` Query: {`; 116 | for (const key of baseTableNames) { 117 | resolverString += resolvers.createQuery(key); 118 | } 119 | resolverString += `\n },`; 120 | 121 | // MUTATION portion of the resolver 122 | resolverString += ` 123 | 124 | Mutation: {`; 125 | 126 | const mutationTypes = Object.keys(mutationObj); 127 | 128 | for (const key of mutationTypes) { 129 | resolverString += resolvers.createMutation(key, mutationObj); 130 | } 131 | resolverString += `\n },\n\n`; 132 | 133 | // RELATIONAL portion of the resolver 134 | // loop through all base table names 135 | for (const key of baseTableNames) { 136 | // at each base table name 137 | resolverString += ` ${capitalizeAndSingularize(key)}: {`; 138 | // 1. check own table columns - append to string 139 | resolverString += resolvers.checkOwnTable(key, baseTables); 140 | // 2. check all base table cols - append to string 141 | resolverString += resolvers.checkBaseTableCols(key, baseTableQuery); 142 | // 3. check join tables and cols - append to string 143 | resolverString += resolvers.checkJoinTableCols(key, joinTables); 144 | // close current table bracket 145 | resolverString += `\n },\n`; 146 | } 147 | 148 | // close resolver bracket 149 | resolverString += `}\n`; 150 | 151 | res.locals.resolverString = resolverString; 152 | return next(); 153 | }; 154 | 155 | const GQLController: GQLControllerType = { 156 | createSchemaTypeDefs, 157 | createSchemaQuery, 158 | createSchemaMutation, 159 | createResolver, 160 | }; 161 | 162 | export default GQLController; -------------------------------------------------------------------------------- /server/controllers/SQLController.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | import { SQLControllerType } from './controllerTypes'; 3 | import dotenv from 'dotenv'; 4 | dotenv.config(); 5 | const { Pool } = require('pg'); 6 | 7 | // get all database metadata (tables and columns) from user's selected DB 8 | const getAllMetadata = async (req: Request, res: Response, next: NextFunction) => { 9 | const PG_URI = (!req.query.dbLink) ? process.env.PG_URI : req.query.dbLink; 10 | const db = new Pool({ 11 | connectionString: PG_URI, 12 | }); 13 | 14 | const queryString = ` 15 | SELECT 16 | cols.column_name, 17 | cols.table_name, 18 | cols.data_type, 19 | cols.character_maximum_length, 20 | cols.is_nullable, 21 | kcu.constraint_name, 22 | cons.constraint_type, 23 | rel_kcu.table_name AS foreign_table, 24 | rel_kcu.column_name AS foreign_column 25 | FROM information_schema.columns cols 26 | LEFT JOIN information_schema.key_column_usage kcu 27 | ON cols.column_name = kcu.column_name 28 | AND cols.table_name = kcu.table_name 29 | LEFT JOIN information_schema.table_constraints cons 30 | ON kcu.constraint_name = cons.constraint_name 31 | LEFT JOIN information_schema.referential_constraints rco 32 | ON rco.constraint_name = cons.constraint_name 33 | LEFT JOIN information_schema.key_column_usage rel_kcu 34 | ON rco.unique_constraint_name = rel_kcu.constraint_name 35 | 36 | LEFT JOIN information_schema.tables tbls 37 | ON cols.table_name = tbls.table_name 38 | 39 | WHERE cols.table_schema = 'public' AND tbls.table_type = 'BASE TABLE' 40 | ORDER BY cols.table_name`; 41 | 42 | try { 43 | const data = await db.query(queryString); 44 | if (!data) { 45 | throw (new Error('Error querying from sql database')); 46 | } 47 | // data.rows is an array of objects 48 | res.locals.queryTables = data.rows; 49 | return next(); 50 | } catch (err) { 51 | console.log(err); 52 | return next({ 53 | log: `Express error handler caught error in the getAllMetadata controller, ${err}`, 54 | message: { err: 'An error occurred in the getAllMetadata controller' }, 55 | }); 56 | } 57 | }; 58 | 59 | // format sql results to client 60 | const formatQueryResult = (req: Request, res: Response, next: NextFunction) => { 61 | // iterate through the array of object columns info 62 | const allTables = {}; 63 | for (let i = 0; i < res.locals.queryTables.length; i += 1) { 64 | // if object doesn't have table name add it to the object 65 | const key = res.locals.queryTables[i].table_name; 66 | if (!allTables[key]) { 67 | allTables[key] = [res.locals.queryTables[i]]; 68 | } else { 69 | allTables[key].push(res.locals.queryTables[i]); 70 | } 71 | } 72 | res.locals.allTables = allTables; 73 | return next(); 74 | }; 75 | 76 | const SQLController: SQLControllerType = { getAllMetadata, formatQueryResult }; 77 | export default SQLController; -------------------------------------------------------------------------------- /server/controllers/controllerTypes.ts: -------------------------------------------------------------------------------- 1 | // import { Request, Response } from 'express'; 2 | 3 | export interface SQLControllerType { 4 | getAllMetadata: (request, response, next) => void; 5 | formatQueryResult: (request, response, next) => void; 6 | } 7 | 8 | export interface GQLControllerType { 9 | createSchemaTypeDefs: (request, response, next) => void; 10 | createSchemaQuery: (request, response, next) => void; 11 | createSchemaMutation: (request, response, next) => void; 12 | createResolver: (request, response, next) => void; 13 | } 14 | 15 | export interface MutationObjectType { 16 | [key: string]: any; 17 | } 18 | -------------------------------------------------------------------------------- /server/converters/converterTypes.ts: -------------------------------------------------------------------------------- 1 | // Converter Function Interfaces 2 | export interface MutationConverterType { 3 | add: (string, ArrayOfColumns) => [string, object]; 4 | update: (string, ArrayOfColumns) => [string, object]; 5 | del: (string, ArrayOfColumns) => [string, object]; 6 | stringify: (MutationObject) => string; 7 | } 8 | 9 | export interface ResolversType { 10 | createQuery: (string) => string; 11 | createMutation: (string, MutationObject) => string; 12 | checkOwnTable: (string, Tables) => string; 13 | checkBaseTableCols: (string, ArrayOfColumns) => string; 14 | checkJoinTableCols: (string, Tables) => string; 15 | } 16 | 17 | export interface QueryConverterType { 18 | createQuerySchema: (string) => void; 19 | } 20 | 21 | export interface typeConverterType { 22 | sortTables: (allTables, baseTables, joinTables) => any; 23 | createBaseTableQuery: (baseTables) => ArrayOfColumns; 24 | createInitialTypeDef: (baseTableName, baseTables, baseTableQuery) => StringObject; 25 | addForeignKeysToTypeDef: (joinTableName, schema, joinTables) => void; 26 | finalizeTypeDef: (schema) => string; 27 | } 28 | 29 | // interfaces for data structures used throughout controllers 30 | export interface Column { 31 | column_name: string, 32 | table_name: string, 33 | data_type: string, 34 | character_maximum_length: number | null, 35 | is_nullable: string, 36 | constraint_name: string | null, 37 | constraint_type: string | null, 38 | foreign_table: string | null, 39 | foreign_column: string | null 40 | } 41 | 42 | export interface StringObject { 43 | [key: string]: string; 44 | } 45 | 46 | export type ArrayOfColumns = Column[]; 47 | 48 | export interface Tables { 49 | [key: string]: ArrayOfColumns 50 | } 51 | 52 | export interface SchemaTable { 53 | [key: string]: StringObject 54 | } 55 | 56 | export interface MutationTable { 57 | [key: string]: string 58 | } 59 | 60 | export interface MutationObject { 61 | [key: string]: MutationTable 62 | } -------------------------------------------------------------------------------- /server/converters/mutationConverter.ts: -------------------------------------------------------------------------------- 1 | import { MutationConverterType, MutationTable, ArrayOfColumns, MutationObject } from './converterTypes'; 2 | // import MutationTable from './converterTypes' 3 | // import ArrayOfColumns from './converterTypes' 4 | // import MutationObject from './converterTypes' 5 | const { convertDataType, checkNullable, capitalizeAndSingularize } = require('../utils/helperFunc.ts'); 6 | 7 | // const mutationConverter = {}; 8 | 9 | /** 10 | * Creates add mutation for current table 11 | * @param {string} tableName 12 | * @param {array} arrOfColumns array of columns/fields based on current tableName 13 | * @returns {array} returns a tuple with 2 elements: add string and 14 | * single mutation add object (to add as properties in mutationObj) 15 | */ 16 | const add = (tableName: string, arrOfColumns: ArrayOfColumns): [string, object] => { 17 | const addObj: MutationTable = {}; 18 | // iterate through array of columns 19 | for (const column of arrOfColumns) { 20 | if (column.column_name !== '_id') { 21 | // for each column, invoke convertDataTypeMutationAdd, and then invoke checkNullableMutationAdd 22 | let type = convertDataType(column.data_type, column.column_name); 23 | type += checkNullable(column.is_nullable); 24 | // for each column, then push into addObj (key: ) 25 | addObj[column.column_name] = type; 26 | } 27 | } 28 | 29 | const formattedTableName = capitalizeAndSingularize(tableName); 30 | 31 | addObj.formatted_table_name_for_dev_use = formattedTableName; 32 | addObj.table_name_for_dev_use = tableName; 33 | 34 | const addKey = `add${formattedTableName}`; 35 | const outputArr: [string, object] = [addKey, addObj]; 36 | return outputArr; 37 | }; 38 | 39 | /** 40 | * Creates update mutation for current table 41 | * @param {string} tableName 42 | * @param {array} arrOfColumns array of columns/fields based on current tableName 43 | * @returns {array} returns a tuple with 2 elements: update string and 44 | * single mutation update object (to add as properties in mutationObj) 45 | */ 46 | const update = (tableName: string, arrOfColumns: ArrayOfColumns): [string, object] => { 47 | const updateObj: MutationTable = {}; 48 | 49 | // iterate through array of columns 50 | for (const column of arrOfColumns) { 51 | // for each column, invoke convertDataTypeMutationAdd 52 | const type = convertDataType(column.data_type, column.column_name); 53 | updateObj[column.column_name] = type; 54 | } 55 | 56 | const formattedTableName = capitalizeAndSingularize(tableName); 57 | 58 | updateObj.formatted_table_name_for_dev_use = formattedTableName; 59 | updateObj.table_name_for_dev_use = tableName; 60 | 61 | const updateKey = `update${formattedTableName}`; 62 | const outputArr: [string, object] = [updateKey, updateObj]; 63 | return outputArr; 64 | }; 65 | 66 | /** 67 | * Creates delete mutation for current table 68 | * @param {string} tableName 69 | * @param {array} arrOfColumns array of columns/fields based on current tableName 70 | * @returns {array} returns a tuple with 2 elements: add string and 71 | * single mutation add object (to add as properties in mutationObj) 72 | */ 73 | const del = (tableName: string, arrOfColumns: ArrayOfColumns): [string, object] => { 74 | const deleteObj: MutationTable = {}; 75 | 76 | // iterate through array of columns 77 | for (const column of arrOfColumns) { 78 | if (column.constraint_type === 'PRIMARY KEY') { 79 | deleteObj[column.column_name] = 'ID!'; 80 | } 81 | } 82 | 83 | const formattedTableName = capitalizeAndSingularize(tableName); 84 | deleteObj.formatted_table_name_for_dev_use = formattedTableName; 85 | deleteObj.table_name_for_dev_use = tableName; 86 | const deleteKey = `delete${formattedTableName}`; 87 | const outputArr: [string, object] = [deleteKey, deleteObj]; 88 | return outputArr; 89 | }; 90 | 91 | /** 92 | * Converts mutationObj to a string 93 | * @param {object} mutationObj 94 | * @returns {string} returns final mutation string formatted for client 95 | */ 96 | const stringify = (mutationObj: MutationObject): string => { 97 | let mutationString = '\n\ntype Mutation { \n'; 98 | // iterate through mutation object 99 | for (const mutationType in mutationObj) { 100 | mutationString += ` ${mutationType}( \n`; 101 | 102 | // iterate through all the fields within each mutationType 103 | for (const column in mutationObj[mutationType]) { 104 | if (column !== 'table_name_for_dev_use' && column !== 'formatted_table_name_for_dev_use') { 105 | mutationString += ` ${column}: ${mutationObj[mutationType][column]}, \n`; 106 | } 107 | } 108 | mutationString += ` ): ${mutationObj[mutationType].formatted_table_name_for_dev_use}! \n\n`; 109 | } 110 | mutationString += `} \n`; 111 | return mutationString; 112 | }; 113 | 114 | const mutationConverter: MutationConverterType = { 115 | add, 116 | update, 117 | del, 118 | stringify, 119 | }; 120 | 121 | export default mutationConverter; -------------------------------------------------------------------------------- /server/converters/queryConverter.ts: -------------------------------------------------------------------------------- 1 | import { QueryConverterType } from './converterTypes'; 2 | 3 | const { capitalizeAndSingularize, makeCamelCase, makeCamelCaseAndSingularize } = require('../utils/helperFunc.ts'); 4 | 5 | /** 6 | * Creates query schema 7 | * @param {string} tableName 8 | * @returns {string} returns a string for the query schema 9 | */ 10 | const createQuerySchema = (tableName: string): string => { 11 | // tableName = 'starship_specs' 12 | // starshipSpecs: [StarshipSpec] 13 | // starshipSpec(_id: ID!): StarshipSpec 14 | 15 | // check if singuliarzed table is the same as tableName 16 | // if so, then append 'ById' to differentiate 17 | let singularAndCamelTableName: string = makeCamelCaseAndSingularize(tableName); 18 | if (singularAndCamelTableName === tableName) { 19 | singularAndCamelTableName += 'ById'; 20 | } 21 | const temp: string = capitalizeAndSingularize(tableName); 22 | const tableQuery = ` ${makeCamelCase(tableName)}: [${temp}] \n ${singularAndCamelTableName}(_id: ID!): ${temp} \n`; 23 | 24 | return tableQuery; 25 | }; 26 | const queryConverter: QueryConverterType = { createQuerySchema }; 27 | 28 | export default queryConverter; -------------------------------------------------------------------------------- /server/converters/resolvers.ts: -------------------------------------------------------------------------------- 1 | import { Tables, MutationObject, ArrayOfColumns, ResolversType } from './converterTypes'; 2 | 3 | const { makeCamelCase, makeCamelCaseAndSingularize } = require('../utils/helperFunc.ts'); 4 | 5 | /** 6 | * Creates resolvers query to query all and query by id 7 | * @param {string} baseTableName 8 | * @returns {string} string of resolvers sql query for query all and to query by id for each table 9 | */ 10 | const createQuery = (baseTableName: string): string => { 11 | // baseTableName is pluralized version, ex. 'planets' 12 | let currString = ''; 13 | // first, append query strings for baseTableName 14 | currString += ` 15 | ${makeCamelCase(baseTableName)}: async () => { 16 | try { 17 | const query = 'SELECT * FROM ${baseTableName}'; 18 | const data = await db.query(query); 19 | console.log('sql query results data.rows', data.rows); 20 | return data.rows; 21 | } catch (err) { 22 | throw new Error(err); 23 | } 24 | },`; 25 | 26 | // second, append strings for singularized baseTableName 27 | // check if singuliarzed table is the same as baseTableName 28 | // if so, then append 'ById' to differentiate 29 | let singularAndCamelTableName = makeCamelCaseAndSingularize(baseTableName); 30 | if (singularAndCamelTableName === baseTableName) { 31 | singularAndCamelTableName += 'ById'; 32 | } 33 | currString += ` 34 | ${singularAndCamelTableName}: async (parent, args, context, info) => { 35 | try { 36 | const query = 'SELECT * FROM ${baseTableName} WHERE _id = $1'; 37 | const values = [args._id]; 38 | const data = await db.query(query, values); 39 | console.log('sql query result data.rows[0]', data.rows[0]); 40 | return data.rows[0]; 41 | } catch (err) { 42 | throw new Error(err); 43 | } 44 | },`; 45 | 46 | return currString; 47 | }; 48 | 49 | /** 50 | * Creates mutation resolvers (add, update, delete) 51 | * @param {string} mutationType 52 | * @param {object} mutationObj 53 | * @returns {string} string of sql resolvers query for mutations of each table 54 | */ 55 | const createMutation = (mutationType: string, mutationObj: MutationObject): string => { 56 | let currString = ''; 57 | // ADD 58 | if (mutationType.includes('add')) { 59 | let queryString = ''; 60 | let valuesString = ''; 61 | let argsString = ''; 62 | let counter = 1; 63 | for (const key in mutationObj[mutationType]) { 64 | if (key !== 'formatted_table_name_for_dev_use' && key !== 'table_name_for_dev_use') { 65 | // add line breaks and spaces for client formatting 66 | if (counter % 3 === 0) { 67 | queryString += `\n `; 68 | argsString += `\n `; 69 | } 70 | // create query string 71 | queryString += `${key}, `; 72 | // create values string 73 | valuesString += `$${counter}, `; 74 | counter += 1; 75 | // create args string 76 | argsString += `args.${key}, `; 77 | } 78 | } 79 | // remove last comma and trailing space 80 | const finalQueryString = queryString.slice(0, -2); 81 | const finalValuesString = valuesString.slice(0, -2); 82 | const finalArgsString = argsString.slice(0, -2); 83 | 84 | currString += ` 85 | ${mutationType}: async (parent, args, context, info) => { 86 | try { 87 | const query = \`INSERT INTO ${mutationObj[mutationType].table_name_for_dev_use} (${finalQueryString}) 88 | VALUES (${finalValuesString}) 89 | RETURNING *\`; 90 | const values = [${finalArgsString}]; 91 | const data = await db.query(query, values); 92 | console.log('insert sql result data.rows[0]', data.rows[0]); 93 | return data.rows[0]; 94 | } catch (err) { 95 | throw new Error(err); 96 | } 97 | },`; 98 | } else if (mutationType.includes('delete')) { 99 | // DELETE 100 | currString += ` 101 | ${mutationType}: async (parent, args, context, info) => { 102 | try { 103 | const query = \`DELETE FROM ${mutationObj[mutationType].table_name_for_dev_use} 104 | WHERE _id = $1 RETURNING *\`; 105 | const values = [args._id]; 106 | const data = await db.query(query, values); 107 | console.log('delete sql result data.rows[0]', data.rows[0]); 108 | return data.rows[0]; 109 | } catch (err) { 110 | throw new Error(err); 111 | } 112 | },`; 113 | } else if (mutationType.includes('update')) { 114 | // UPDATE 115 | currString += ` 116 | ${mutationType}: async (parent, args, context, info) => { 117 | try { 118 | // sanitizing data for sql insert 119 | const argsArr = Object.keys(args).filter((el) => (el !== '_id')); 120 | const setStr = argsArr 121 | .map((el, i) => el + ' = $' + (i + 1)) 122 | .join(', '); 123 | argsArr.push('_id'); 124 | const pKey = '$' + argsArr.length; 125 | const valuesArr = argsArr.map((el) => args[el]); 126 | 127 | // insert query 128 | const query = 'UPDATE ${mutationObj[mutationType].table_name_for_dev_use} SET ' + setStr + ' WHERE _id = ' + pKey + ' RETURNING *'; 129 | const values = valuesArr; 130 | const data = await db.query(query, values); 131 | console.log('insert sql result data.rows[0]', data.rows[0]); 132 | return data.rows[0]; 133 | } catch (err) { 134 | throw new Error(err); 135 | } 136 | },`; 137 | } else { 138 | // failsafe option 139 | currString += ` 140 | ${mutationType}: (parent, args, context, info) => { 141 | try { 142 | // insert sql query here 143 | } catch (err) { 144 | throw new Error(err); 145 | } 146 | },`; 147 | } 148 | 149 | return currString; 150 | }; 151 | 152 | /** 153 | * Check own table for foreign keys to create resolver queries 154 | * @param {string} baseTableName 155 | * @param {object} baseTables 156 | * @returns {string} string of sql resolvers query 157 | */ 158 | const checkOwnTable = (baseTableName: string, baseTables: Tables): string => { 159 | let currString = ''; 160 | for (const column of baseTables[baseTableName]) { 161 | if (column.constraint_type === 'FOREIGN KEY') { 162 | currString += ` 163 | ${makeCamelCase(column.foreign_table)}: async (${baseTableName}) => { 164 | try { 165 | const query = \`SELECT ${column.foreign_table}.* FROM ${column.foreign_table} 166 | LEFT OUTER JOIN ${baseTableName} 167 | ON ${column.foreign_table}._id = ${baseTableName}.${column.column_name} 168 | WHERE ${baseTableName}._id = $1\`; 169 | const values = [${baseTableName}._id]; 170 | const data = await db.query(query, values); 171 | return data.rows; 172 | } catch (err) { 173 | throw new Error(err); 174 | } 175 | },`; 176 | } 177 | } 178 | 179 | return currString; 180 | }; 181 | 182 | /** 183 | * Check other base table columns/fields for relationships to baseTableName 184 | * @param {string} baseTableName 185 | * @param {object} baseTables 186 | * @returns {string} string of sql resolvers query 187 | */ 188 | const checkBaseTableCols = (baseTableName: string, baseTableQuery: ArrayOfColumns): string => { 189 | let currString = ''; 190 | for (const column of baseTableQuery) { 191 | if (column.foreign_table === baseTableName) { 192 | currString += ` 193 | ${makeCamelCase(column.table_name)}: async (${baseTableName}) => { 194 | try { 195 | const query = \`SELECT * FROM ${column.table_name} 196 | WHERE ${column.column_name} = $1\`; 197 | const values = [${baseTableName}._id]; 198 | const data = await db.query(query, values); 199 | return data.rows; 200 | } catch (err) { 201 | throw new Error(err); 202 | } 203 | },`; 204 | } 205 | } 206 | 207 | return currString; 208 | }; 209 | 210 | /** 211 | * Check join table columns/fields for relationships to baseTableName 212 | * @param {string} baseTableName 213 | * @param {object} joinTables 214 | * @returns {string} string of sql resolvers query 215 | */ 216 | const checkJoinTableCols = (baseTableName: string, joinTables: Tables): string => { 217 | let currString = ''; 218 | const relationships: string[] = []; 219 | 220 | for (const currJoinTable in joinTables) { 221 | for (const column of joinTables[currJoinTable]) { 222 | if (column.foreign_table === baseTableName) { 223 | relationships.push(currJoinTable); 224 | } 225 | } 226 | } 227 | // relationships = [people_in_films, pilot] 228 | for (const table of relationships) { 229 | // const foreignKeys = []; 230 | const foreignKeysObj = {}; 231 | for (const column of joinTables[table]) { 232 | if (column.constraint_type === 'FOREIGN KEY' && column.foreign_table !== null) { 233 | // foreignKeys.push(column.foreign_table); 234 | foreignKeysObj[column.foreign_table] = column.column_name; 235 | } 236 | } 237 | // const foreignKeys = {films: 'film_id', species: 'species_id'} 238 | const foreignKeys = Object.keys(foreignKeysObj); 239 | for (let i = 0; i < foreignKeys.length; i += 1) { 240 | if (foreignKeys[i] === baseTableName) { 241 | const index = i === 1 ? 0 : 1; 242 | currString += ` 243 | ${makeCamelCase(foreignKeys[index])}: async (${baseTableName}) => { 244 | try { 245 | const query = \`SELECT * FROM ${foreignKeys[index]} 246 | LEFT OUTER JOIN ${table} 247 | ON ${foreignKeys[index]}._id = ${table}.${foreignKeysObj[foreignKeys[index]]} 248 | WHERE ${table}.${foreignKeysObj[baseTableName]} = $1\`; 249 | const values = [${baseTableName}._id]; 250 | const data = await db.query(query, values); 251 | return data.rows; 252 | } catch (err) { 253 | throw new Error(err); 254 | } 255 | },`; 256 | } 257 | } 258 | } 259 | return currString; 260 | }; 261 | 262 | const resolvers: ResolversType = { 263 | createQuery, 264 | createMutation, 265 | checkOwnTable, 266 | checkBaseTableCols, 267 | checkJoinTableCols, 268 | }; 269 | // module.exports = resolvers; 270 | export default resolvers; -------------------------------------------------------------------------------- /server/converters/typeConverter.ts: -------------------------------------------------------------------------------- 1 | import { Tables, ArrayOfColumns, SchemaTable, StringObject, typeConverterType } from './converterTypes'; 2 | const { convertDataType, checkNullable, capitalizeAndSingularize, makeCamelCase } = require('../utils/helperFunc.ts'); 3 | 4 | /** 5 | * Checks if the current table is a join table or not 6 | * @param {object} allTables 7 | * @param {string} tableName 8 | * @returns {boolean} returns true if the table is a join table 9 | * 10 | * Based on fact that junction tables in SQL contain the primary key columns of 11 | * TWO tables you want to relate 12 | * https://docs.microsoft.com/en-us/sql/ssms/visual-db-tools/map-many-to-many-relationships-visual-database-tools?view=sql-server-ver15 13 | */ 14 | const checkIfJoinTable = (allTables: Tables, tableName: string): boolean => { 15 | // initialize counter of foreign keys 16 | let fKCounter = 0; 17 | // loop through array 18 | for (let i = 0; i < allTables[tableName].length; i += 1) { 19 | // at each element, check if current element's constraint_type is FOREIGN KEY 20 | if (allTables[tableName][i].constraint_type === 'FOREIGN KEY') { 21 | // if so, increment counter by 1 22 | fKCounter += 1; 23 | } 24 | } 25 | // compare number of foreign keys (counter) with length of array 26 | // if number of foreign keys is one less than the length of the array 27 | if (allTables[tableName].length - 1 === fKCounter) { 28 | // then return true 29 | return true; 30 | } 31 | return false; 32 | }; 33 | 34 | /** 35 | * Sorts allTables into baseTables (non-join tables) and joinTables (join tables) 36 | * @param {object} allTables 37 | * @param {object} baseTables 38 | * @param {object} joinTables 39 | * @returns {object} returns an object with two properties: baseTables and joinTables that 40 | * were mutated during this function invocation 41 | */ 42 | const sortTables = (allTables: Tables, baseTables: Tables, joinTables: Tables): any => { 43 | for (const key in allTables) { 44 | if (checkIfJoinTable(allTables, key)) joinTables[key] = allTables[key]; 45 | else baseTables[key] = allTables[key]; 46 | } 47 | 48 | return { baseTables, joinTables }; 49 | }; 50 | 51 | /** 52 | * Converts baseTables (object of array, where each element in the array is a column/field object) 53 | * to an array 54 | * @param {object} baseTables 55 | * @returns {array} returns an array of objects, where each object is a non-join table column/field 56 | */ 57 | const createBaseTableQuery = (baseTables: Tables): ArrayOfColumns => { 58 | const array: ArrayOfColumns = []; 59 | for (const baseTableName in baseTables) { 60 | for (const column of baseTables[baseTableName]) { 61 | array.push(column); 62 | } 63 | } 64 | return array; 65 | }; 66 | 67 | /** 68 | * Sorts allTables into baseTables (non-join tables) and joinTables (join tables) 69 | * @param {string} baseTableName 70 | * @param {object} baseTables 71 | * @param {array} baseTableQuery 72 | * @returns {object} returns an object where each property is a column from one table and 73 | * its GraphQL data type. This object represents ONE table's columns. 74 | */ 75 | const createInitialTypeDef = (baseTableName: string, baseTables: Tables, baseTableQuery: ArrayOfColumns): StringObject => { 76 | const tableType: StringObject = {}; 77 | // iterate through array of objects/columns 78 | for (let i = 0; i < baseTables[baseTableName].length; i += 1) { 79 | const column = baseTables[baseTableName][i]; 80 | // if its a foreign key, singularize the foreign table as the value 81 | // foreign table : [singularize the foreign table and make PascalCase] 82 | if (column.constraint_type === 'FOREIGN KEY' && column.foreign_table !== null) { 83 | const key = column.foreign_table; 84 | 85 | const formattedTableName = capitalizeAndSingularize(key); 86 | // add to schema object 87 | tableType[key] = `[${formattedTableName}]`; 88 | } else { 89 | // at each column, convert data type, then check for nullables 90 | let type = convertDataType(column.data_type, column.column_name); 91 | type += checkNullable(column.is_nullable); 92 | // add to schema object 93 | tableType[column.column_name] = type; 94 | } 95 | } 96 | // check all base table cols for relationships to baseTable 97 | for (const column of baseTableQuery) { 98 | if (column.foreign_table === baseTableName) { 99 | tableType[makeCamelCase(column.table_name)] = `[${capitalizeAndSingularize(column.table_name)}]`; 100 | } 101 | } 102 | 103 | return tableType; 104 | }; 105 | 106 | /** 107 | * Adds foreign keys to the type definitions 108 | * @param {string} joinTableName 109 | * @param {object} schema 110 | * @param {object} joinTables 111 | * @returns {void} returns nothing, mutates schema object in argument 112 | */ 113 | const addForeignKeysToTypeDef = (joinTableName: string, schema: SchemaTable, joinTables: Tables): void => { 114 | // iterate through array (ex. vessels_in_films) 115 | const foreignKeys: string[] = []; 116 | for (let i = 0; i < joinTables[joinTableName].length; i += 1) { 117 | const column = joinTables[joinTableName][i]; 118 | // get both foreign keys (using constraint_type) and push foreign_table to an array 119 | // (ex. ['films', 'vessels']) 120 | if (column.constraint_type === 'FOREIGN KEY' && column.foreign_table !== null) { 121 | foreignKeys.push(column.foreign_table); 122 | } 123 | } 124 | 125 | // get first foreign key (array[0]) (ex. films) 126 | // go to base table object referenced in second foreign key (array[1]) (ex. vessels) 127 | // add property to vessels base table with information from first fK 128 | 129 | if (schema[foreignKeys[0]]) { // films 130 | const formattedTableName1 = capitalizeAndSingularize(foreignKeys[1]); 131 | // films -> { vessels: [Vessel] } 132 | schema[foreignKeys[0]][foreignKeys[1]] = `[${formattedTableName1}]`; 133 | } 134 | 135 | if (schema[foreignKeys[1]]) { // vessels 136 | // vessels -> { films: [Film] } 137 | const formattedTableName2 = capitalizeAndSingularize(foreignKeys[0]); 138 | schema[foreignKeys[1]][foreignKeys[0]] = `[${formattedTableName2}]`; 139 | } 140 | }; 141 | 142 | /** 143 | * Convert type defs in schema to a string 144 | * @param {object} schema 145 | * @returns {string} formats the type def as a string for client 146 | */ 147 | const finalizeTypeDef = (schema: SchemaTable): string => { 148 | let typeString = ''; 149 | 150 | for (const key in schema) { 151 | const formattedTableName = capitalizeAndSingularize(key); 152 | const typeKey = `type ${formattedTableName}`; 153 | typeString += `\n${typeKey} {\n`; 154 | const table = schema[key]; 155 | 156 | for (const column in table) { 157 | // 2 spaces + '_id: ID!' + line break 158 | const newColumn = ` ${column}: ${table[column]}\n`; 159 | // append to typeString 160 | typeString += newColumn; 161 | } 162 | typeString += '} \n'; 163 | } 164 | return typeString; 165 | }; 166 | 167 | const typeConverter: typeConverterType = { 168 | sortTables, 169 | createBaseTableQuery, 170 | createInitialTypeDef, 171 | addForeignKeysToTypeDef, 172 | finalizeTypeDef, 173 | }; 174 | 175 | export default typeConverter; 176 | // module.exports = typeConverter; -------------------------------------------------------------------------------- /server/router.ts: -------------------------------------------------------------------------------- 1 | import express, { Request, Response } from 'express'; 2 | import SQLController from './controllers/SQLController'; 3 | import GQLController from './controllers/GQLController'; 4 | import schema from './schema'; 5 | import { graphqlHTTP } from 'express-graphql'; 6 | 7 | const router = express.Router(); 8 | 9 | 10 | // const schema = require('./schema.ts'); 11 | 12 | router.get( 13 | '/submit', 14 | SQLController.getAllMetadata, 15 | SQLController.formatQueryResult, 16 | GQLController.createSchemaTypeDefs, 17 | GQLController.createSchemaQuery, 18 | GQLController.createSchemaMutation, 19 | GQLController.createResolver, 20 | (req: Request, res: Response) => { 21 | // allTables (for SQL visualizer) 22 | // finalString (GraphQL Schema) 23 | // resolverString (GraphQL Resolver) 24 | console.log('sent all info to client'); 25 | return res.status(200).json(res.locals); 26 | }, 27 | ); 28 | // format finalString to code 29 | 30 | // const schema = makeExecutableSchema({ 31 | // res.locals.finalString 32 | // // allowUndefinedInResolve: false, 33 | // // resolverValidationOptions: { 34 | // // // requireResolversForArgs: 'error', 35 | // // // requireResolversForAllFields: 'warn', 36 | // // }, 37 | // }); 38 | 39 | const queryString = ` 40 | # Welcome to GraphiQL 41 | # 42 | # GraphiQL is an in-browser tool for writing, validating, and 43 | # testing GraphQL queries. 44 | # 45 | # Type queries into this side of the screen, and you will see intelligent 46 | # typeaheads aware of the current GraphQL type schema and live syntax and 47 | # validation errors highlighted within the text. 48 | # 49 | # GraphQL queries typically start with a "{" character. Lines that start 50 | # with a # are ignored. 51 | # 52 | # Here's an example query to get started: 53 | # 54 | # To get a list of all PEOPLE by name and gender, use the following QUERY: 55 | # 56 | # { 57 | # people { 58 | # name 59 | # gender 60 | # } 61 | # } 62 | # 63 | # To add a person to the database, use the following MUTATION: 64 | # 65 | # mutation { 66 | # addPerson(gender: "Male", name: "John", species_id: 11){ 67 | # name 68 | # gender 69 | # species { 70 | # name 71 | # } 72 | # } 73 | # } 74 | `; 75 | 76 | router.use( 77 | '/sandbox', 78 | graphqlHTTP({ 79 | // schema (types of queries, mutations, types) + resolvers 80 | schema, 81 | graphiql: { 82 | editorTheme: 'solarized light', 83 | defaultQuery: queryString, 84 | }, 85 | }), 86 | ); 87 | 88 | export default router; -------------------------------------------------------------------------------- /server/schema.ts: -------------------------------------------------------------------------------- 1 | import { makeExecutableSchema } from 'graphql-tools'; 2 | import { Pool } from 'pg'; 3 | import dotenv from 'dotenv'; 4 | dotenv.config(); 5 | 6 | const PG_URI = process.env.PG_URI 7 | 8 | const pool = new Pool({ 9 | connectionString: PG_URI, 10 | }); 11 | 12 | interface dbType { 13 | query: (text: any, params?: any, callback?: any) => any | void; 14 | } 15 | 16 | const db: dbType = { 17 | query: (text, params, callback) => { 18 | console.log('executed query:', text); 19 | return pool.query(text, params, callback); 20 | } 21 | }; 22 | 23 | // SCHEMA FOR GRAPHIQL SANDBOX 24 | const typeDefs = ` 25 | type Film { 26 | _id: ID! 27 | director: String! 28 | episode_id: Int! 29 | opening_crawl: String! 30 | producer: String! 31 | release_date: String! 32 | title: String! 33 | people: [Person] 34 | planets: [Planet] 35 | species: [Species] 36 | vessels: [Vessel] 37 | } 38 | 39 | type Person { 40 | _id: ID! 41 | birth_year: String 42 | eye_color: String 43 | gender: String 44 | hair_color: String 45 | height: Int 46 | planets: [Planet] 47 | mass: String 48 | name: String! 49 | skin_color: String 50 | species: [Species] 51 | films: [Film] 52 | vessels: [Vessel] 53 | } 54 | 55 | type Planet { 56 | _id: ID! 57 | climate: String 58 | diameter: Int 59 | gravity: String 60 | name: String 61 | orbital_period: Int 62 | population: Float 63 | rotation_period: Int 64 | surface_water: String 65 | terrain: String 66 | people: [Person] 67 | species: [Species] 68 | films: [Film] 69 | } 70 | 71 | type Species { 72 | _id: ID! 73 | average_height: String 74 | average_lifespan: String 75 | classification: String 76 | eye_colors: String 77 | hair_colors: String 78 | planets: [Planet] 79 | language: String 80 | name: String! 81 | skin_colors: String 82 | people: [Person] 83 | films: [Film] 84 | } 85 | 86 | type StarshipSpec { 87 | MGLT: String 88 | _id: ID! 89 | hyperdrive_rating: String 90 | vessels: [Vessel] 91 | } 92 | 93 | type Vessel { 94 | _id: ID! 95 | cargo_capacity: String 96 | consumables: String 97 | cost_in_credits: Float 98 | crew: Int 99 | length: String 100 | manufacturer: String 101 | max_atmosphering_speed: String 102 | model: String 103 | name: String! 104 | passengers: Int 105 | vessel_class: String! 106 | vessel_type: String! 107 | starshipSpecs: [StarshipSpec] 108 | people: [Person] 109 | films: [Film] 110 | } 111 | 112 | type Query { 113 | films: [Film] 114 | film(_id: ID!): Film 115 | people: [Person] 116 | person(_id: ID!): Person 117 | planets: [Planet] 118 | planet(_id: ID!): Planet 119 | species: [Species] 120 | speciesById(_id: ID!): Species 121 | starshipSpecs: [StarshipSpec] 122 | starshipSpec(_id: ID!): StarshipSpec 123 | vessels: [Vessel] 124 | vessel(_id: ID!): Vessel 125 | } 126 | 127 | type Mutation { 128 | addFilm( 129 | director: String!, 130 | episode_id: Int!, 131 | opening_crawl: String!, 132 | producer: String!, 133 | release_date: String!, 134 | title: String!, 135 | ): Film! 136 | 137 | updateFilm( 138 | _id: ID, 139 | director: String, 140 | episode_id: Int, 141 | opening_crawl: String, 142 | producer: String, 143 | release_date: String, 144 | title: String, 145 | ): Film! 146 | 147 | deleteFilm( 148 | _id: ID!, 149 | ): Film! 150 | 151 | addPerson( 152 | birth_year: String, 153 | eye_color: String, 154 | gender: String, 155 | hair_color: String, 156 | height: Int, 157 | homeworld_id: Int, 158 | mass: String, 159 | name: String!, 160 | skin_color: String, 161 | species_id: Int, 162 | ): Person! 163 | 164 | updatePerson( 165 | _id: ID, 166 | birth_year: String, 167 | eye_color: String, 168 | gender: String, 169 | hair_color: String, 170 | height: Int, 171 | homeworld_id: Int, 172 | mass: String, 173 | name: String, 174 | skin_color: String, 175 | species_id: Int, 176 | ): Person! 177 | 178 | deletePerson( 179 | _id: ID!, 180 | ): Person! 181 | 182 | addPlanet( 183 | climate: String, 184 | diameter: Int, 185 | gravity: String, 186 | name: String, 187 | orbital_period: Int, 188 | population: Float, 189 | rotation_period: Int, 190 | surface_water: String, 191 | terrain: String, 192 | ): Planet! 193 | 194 | updatePlanet( 195 | _id: ID, 196 | climate: String, 197 | diameter: Int, 198 | gravity: String, 199 | name: String, 200 | orbital_period: Int, 201 | population: Float, 202 | rotation_period: Int, 203 | surface_water: String, 204 | terrain: String, 205 | ): Planet! 206 | 207 | deletePlanet( 208 | _id: ID!, 209 | ): Planet! 210 | 211 | addSpecies( 212 | average_height: String, 213 | average_lifespan: String, 214 | classification: String, 215 | eye_colors: String, 216 | hair_colors: String, 217 | homeworld_id: Int, 218 | language: String, 219 | name: String!, 220 | skin_colors: String, 221 | ): Species! 222 | 223 | updateSpecies( 224 | _id: ID, 225 | average_height: String, 226 | average_lifespan: String, 227 | classification: String, 228 | eye_colors: String, 229 | hair_colors: String, 230 | homeworld_id: Int, 231 | language: String, 232 | name: String, 233 | skin_colors: String, 234 | ): Species! 235 | 236 | deleteSpecies( 237 | _id: ID!, 238 | ): Species! 239 | 240 | addStarshipSpec( 241 | MGLT: String, 242 | hyperdrive_rating: String, 243 | vessel_id: Int!, 244 | ): StarshipSpec! 245 | 246 | updateStarshipSpec( 247 | MGLT: String, 248 | _id: ID, 249 | hyperdrive_rating: String, 250 | vessel_id: Int, 251 | ): StarshipSpec! 252 | 253 | deleteStarshipSpec( 254 | _id: ID!, 255 | ): StarshipSpec! 256 | 257 | addVessel( 258 | cargo_capacity: String, 259 | consumables: String, 260 | cost_in_credits: Float, 261 | crew: Int, 262 | length: String, 263 | manufacturer: String, 264 | max_atmosphering_speed: String, 265 | model: String, 266 | name: String!, 267 | passengers: Int, 268 | vessel_class: String!, 269 | vessel_type: String!, 270 | ): Vessel! 271 | 272 | updateVessel( 273 | _id: ID, 274 | cargo_capacity: String, 275 | consumables: String, 276 | cost_in_credits: Float, 277 | crew: Int, 278 | length: String, 279 | manufacturer: String, 280 | max_atmosphering_speed: String, 281 | model: String, 282 | name: String, 283 | passengers: Int, 284 | vessel_class: String, 285 | vessel_type: String, 286 | ): Vessel! 287 | 288 | deleteVessel( 289 | _id: ID!, 290 | ): Vessel! 291 | 292 | } 293 | `; 294 | 295 | // RESOLVER FOR GRAPHIQL SANDBOX 296 | const resolvers = { 297 | Query: { 298 | films: async () => { 299 | try { 300 | const query = 'SELECT * FROM films'; 301 | const data = await db.query(query); 302 | console.log('sql query results data.rows', data.rows); 303 | return data.rows; 304 | } catch (err: any) { 305 | console.log('error in query', err); 306 | } 307 | }, 308 | film: async (parent, args, context, info) => { 309 | try { 310 | const query = 'SELECT * FROM films WHERE _id = $1'; 311 | const values = [args._id]; 312 | const data = await db.query(query, values); 313 | console.log('sql query result data.rows[0]', data.rows[0]); 314 | return data.rows[0]; 315 | } catch (err: any) { 316 | throw new Error(err); 317 | } 318 | }, 319 | people: async () => { 320 | try { 321 | const query = 'SELECT * FROM people'; 322 | const data = await db.query(query); 323 | console.log('sql query results data.rows', data.rows); 324 | return data.rows; 325 | } catch (err: any) { 326 | throw new Error(err); 327 | } 328 | }, 329 | person: async (parent, args, context, info) => { 330 | try { 331 | const query = 'SELECT * FROM people WHERE _id = $1'; 332 | const values = [args._id]; 333 | const data = await db.query(query, values); 334 | console.log('sql query result data.rows[0]', data.rows[0]); 335 | return data.rows[0]; 336 | } catch (err: any) { 337 | throw new Error(err); 338 | } 339 | }, 340 | planets: async () => { 341 | try { 342 | const query = 'SELECT * FROM planets'; 343 | const data = await db.query(query); 344 | console.log('sql query results data.rows', data.rows); 345 | return data.rows; 346 | } catch (err: any) { 347 | throw new Error(err); 348 | } 349 | }, 350 | planet: async (parent, args, context, info) => { 351 | try { 352 | const query = 'SELECT * FROM planets WHERE _id = $1'; 353 | const values = [args._id]; 354 | const data = await db.query(query, values); 355 | console.log('sql query result data.rows[0]', data.rows[0]); 356 | return data.rows[0]; 357 | } catch (err: any) { 358 | throw new Error(err); 359 | } 360 | }, 361 | species: async () => { 362 | try { 363 | const query = 'SELECT * FROM species'; 364 | const data = await db.query(query); 365 | console.log('sql query results data.rows', data.rows); 366 | return data.rows; 367 | } catch (err: any) { 368 | throw new Error(err); 369 | } 370 | }, 371 | speciesById: async (parent, args, context, info) => { 372 | try { 373 | const query = 'SELECT * FROM species WHERE _id = $1'; 374 | const values = [args._id]; 375 | const data = await db.query(query, values); 376 | console.log('sql query result data.rows[0]', data.rows[0]); 377 | return data.rows[0]; 378 | } catch (err: any) { 379 | throw new Error(err); 380 | } 381 | }, 382 | starshipSpecs: async () => { 383 | try { 384 | const query = 'SELECT * FROM starship_specs'; 385 | const data = await db.query(query); 386 | console.log('sql query results data.rows', data.rows); 387 | return data.rows; 388 | } catch (err: any) { 389 | throw new Error(err); 390 | } 391 | }, 392 | starshipSpec: async (parent, args, context, info) => { 393 | try { 394 | const query = 'SELECT * FROM starship_specs WHERE _id = $1'; 395 | const values = [args._id]; 396 | const data = await db.query(query, values); 397 | console.log('sql query result data.rows[0]', data.rows[0]); 398 | return data.rows[0]; 399 | } catch (err: any) { 400 | throw new Error(err); 401 | } 402 | }, 403 | vessels: async () => { 404 | try { 405 | const query = 'SELECT * FROM vessels'; 406 | const data = await db.query(query); 407 | console.log('sql query results data.rows', data.rows); 408 | return data.rows; 409 | } catch (err: any) { 410 | throw new Error(err); 411 | } 412 | }, 413 | vessel: async (parent, args, context, info) => { 414 | try { 415 | const query = 'SELECT * FROM vessels WHERE _id = $1'; 416 | const values = [args._id]; 417 | const data = await db.query(query, values); 418 | console.log('sql query result data.rows[0]', data.rows[0]); 419 | return data.rows[0]; 420 | } catch (err: any) { 421 | throw new Error(err); 422 | } 423 | }, 424 | }, 425 | 426 | Mutation: { 427 | addFilm: async (parent, args, context, info) => { 428 | try { 429 | const query = `INSERT INTO films (director, episode_id, 430 | opening_crawl, producer, release_date, 431 | title) 432 | VALUES ($1, $2, $3, $4, $5, $6) 433 | RETURNING *`; 434 | const values = [args.director, args.episode_id, 435 | args.opening_crawl, args.producer, args.release_date, 436 | args.title]; 437 | const data = await db.query(query, values); 438 | console.log('insert sql result data.rows[0]', data.rows[0]); 439 | return data.rows[0]; 440 | } catch (err: any) { 441 | throw new Error(err); 442 | } 443 | }, 444 | updateFilm: async (parent, args, context, info) => { 445 | try { 446 | // sanitizing data for sql insert 447 | const argsArr = Object.keys(args).filter((el) => (el !== '_id')); 448 | const setStr = argsArr 449 | .map((el, i) => el + ' = $' + (i + 1)) 450 | .join(', '); 451 | argsArr.push('_id'); 452 | const pKey = '$' + argsArr.length; 453 | const valuesArr = argsArr.map((el) => args[el]); 454 | 455 | // insert query 456 | const query = 'UPDATE films SET ' + setStr + ' WHERE _id = ' + pKey + ' RETURNING *'; 457 | const values = valuesArr; 458 | const data = await db.query(query, values); 459 | console.log('insert sql result data.rows[0]', data.rows[0]); 460 | return data.rows[0]; 461 | } catch (err: any) { 462 | throw new Error(err); 463 | } 464 | }, 465 | deleteFilm: async (parent, args, context, info) => { 466 | try { 467 | const query = `DELETE FROM films 468 | WHERE _id = $1 RETURNING *`; 469 | const values = [args._id]; 470 | const data = await db.query(query, values); 471 | console.log('delete sql result data.rows[0]', data.rows[0]); 472 | return data.rows[0]; 473 | } catch (err: any) { 474 | throw new Error(err); 475 | } 476 | }, 477 | addPerson: async (parent, args, context, info) => { 478 | try { 479 | const query = `INSERT INTO people (birth_year, eye_color, 480 | gender, hair_color, height, 481 | homeworld_id, mass, name, 482 | skin_color, species_id) 483 | VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) 484 | RETURNING *`; 485 | const values = [args.birth_year, args.eye_color, 486 | args.gender, args.hair_color, args.height, 487 | args.homeworld_id, args.mass, args.name, 488 | args.skin_color, args.species_id]; 489 | const data = await db.query(query, values); 490 | console.log('insert sql result data.rows[0]', data.rows[0]); 491 | return data.rows[0]; 492 | } catch (err: any) { 493 | throw new Error(err); 494 | } 495 | }, 496 | updatePerson: async (parent, args, context, info) => { 497 | try { 498 | // sanitizing data for sql insert 499 | const argsArr = Object.keys(args).filter((el) => (el !== '_id')); 500 | const setStr = argsArr 501 | .map((el, i) => el + ' = $' + (i + 1)) 502 | .join(', '); 503 | argsArr.push('_id'); 504 | const pKey = '$' + argsArr.length; 505 | const valuesArr = argsArr.map((el) => args[el]); 506 | 507 | // insert query 508 | const query = 'UPDATE people SET ' + setStr + ' WHERE _id = ' + pKey + ' RETURNING *'; 509 | const values = valuesArr; 510 | const data = await db.query(query, values); 511 | console.log('insert sql result data.rows[0]', data.rows[0]); 512 | return data.rows[0]; 513 | } catch (err: any) { 514 | throw new Error(err); 515 | } 516 | }, 517 | deletePerson: async (parent, args, context, info) => { 518 | try { 519 | const query = `DELETE FROM people 520 | WHERE _id = $1 RETURNING *`; 521 | const values = [args._id]; 522 | const data = await db.query(query, values); 523 | console.log('delete sql result data.rows[0]', data.rows[0]); 524 | return data.rows[0]; 525 | } catch (err: any) { 526 | throw new Error(err); 527 | } 528 | }, 529 | addPlanet: async (parent, args, context, info) => { 530 | try { 531 | const query = `INSERT INTO planets (climate, diameter, 532 | gravity, name, orbital_period, 533 | population, rotation_period, surface_water, 534 | terrain) 535 | VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) 536 | RETURNING *`; 537 | const values = [args.climate, args.diameter, 538 | args.gravity, args.name, args.orbital_period, 539 | args.population, args.rotation_period, args.surface_water, 540 | args.terrain]; 541 | const data = await db.query(query, values); 542 | console.log('insert sql result data.rows[0]', data.rows[0]); 543 | return data.rows[0]; 544 | } catch (err: any) { 545 | throw new Error(err); 546 | } 547 | }, 548 | updatePlanet: async (parent, args, context, info) => { 549 | try { 550 | // sanitizing data for sql insert 551 | const argsArr = Object.keys(args).filter((el) => (el !== '_id')); 552 | const setStr = argsArr 553 | .map((el, i) => el + ' = $' + (i + 1)) 554 | .join(', '); 555 | argsArr.push('_id'); 556 | const pKey = '$' + argsArr.length; 557 | const valuesArr = argsArr.map((el) => args[el]); 558 | 559 | // insert query 560 | const query = 'UPDATE planets SET ' + setStr + ' WHERE _id = ' + pKey + ' RETURNING *'; 561 | const values = valuesArr; 562 | const data = await db.query(query, values); 563 | console.log('insert sql result data.rows[0]', data.rows[0]); 564 | return data.rows[0]; 565 | } catch (err: any) { 566 | throw new Error(err); 567 | } 568 | }, 569 | deletePlanet: async (parent, args, context, info) => { 570 | try { 571 | const query = `DELETE FROM planets 572 | WHERE _id = $1 RETURNING *`; 573 | const values = [args._id]; 574 | const data = await db.query(query, values); 575 | console.log('delete sql result data.rows[0]', data.rows[0]); 576 | return data.rows[0]; 577 | } catch (err: any) { 578 | throw new Error(err); 579 | } 580 | }, 581 | addSpecies: async (parent, args, context, info) => { 582 | try { 583 | const query = `INSERT INTO species (average_height, average_lifespan, 584 | classification, eye_colors, hair_colors, 585 | homeworld_id, language, name, 586 | skin_colors) 587 | VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) 588 | RETURNING *`; 589 | const values = [args.average_height, args.average_lifespan, 590 | args.classification, args.eye_colors, args.hair_colors, 591 | args.homeworld_id, args.language, args.name, 592 | args.skin_colors]; 593 | const data = await db.query(query, values); 594 | console.log('insert sql result data.rows[0]', data.rows[0]); 595 | return data.rows[0]; 596 | } catch (err: any) { 597 | throw new Error(err); 598 | } 599 | }, 600 | updateSpecies: async (parent, args, context, info) => { 601 | try { 602 | // sanitizing data for sql insert 603 | const argsArr = Object.keys(args).filter((el) => (el !== '_id')); 604 | const setStr = argsArr 605 | .map((el, i) => el + ' = $' + (i + 1)) 606 | .join(', '); 607 | argsArr.push('_id'); 608 | const pKey = '$' + argsArr.length; 609 | const valuesArr = argsArr.map((el) => args[el]); 610 | 611 | // insert query 612 | const query = 'UPDATE species SET ' + setStr + ' WHERE _id = ' + pKey + ' RETURNING *'; 613 | const values = valuesArr; 614 | const data = await db.query(query, values); 615 | console.log('insert sql result data.rows[0]', data.rows[0]); 616 | return data.rows[0]; 617 | } catch (err: any) { 618 | throw new Error(err); 619 | } 620 | }, 621 | deleteSpecies: async (parent, args, context, info) => { 622 | try { 623 | const query = `DELETE FROM species 624 | WHERE _id = $1 RETURNING *`; 625 | const values = [args._id]; 626 | const data = await db.query(query, values); 627 | console.log('delete sql result data.rows[0]', data.rows[0]); 628 | return data.rows[0]; 629 | } catch (err: any) { 630 | throw new Error(err); 631 | } 632 | }, 633 | addStarshipSpec: async (parent, args, context, info) => { 634 | try { 635 | const query = `INSERT INTO starship_specs (MGLT, hyperdrive_rating, 636 | vessel_id) 637 | VALUES ($1, $2, $3) 638 | RETURNING *`; 639 | const values = [args.MGLT, args.hyperdrive_rating, 640 | args.vessel_id]; 641 | const data = await db.query(query, values); 642 | console.log('insert sql result data.rows[0]', data.rows[0]); 643 | return data.rows[0]; 644 | } catch (err: any) { 645 | throw new Error(err); 646 | } 647 | }, 648 | updateStarshipSpec: async (parent, args, context, info) => { 649 | try { 650 | // sanitizing data for sql insert 651 | const argsArr = Object.keys(args).filter((el) => (el !== '_id')); 652 | const setStr = argsArr 653 | .map((el, i) => el + ' = $' + (i + 1)) 654 | .join(', '); 655 | argsArr.push('_id'); 656 | const pKey = '$' + argsArr.length; 657 | const valuesArr = argsArr.map((el) => args[el]); 658 | 659 | // insert query 660 | const query = 'UPDATE starship_specs SET ' + setStr + ' WHERE _id = ' + pKey + ' RETURNING *'; 661 | const values = valuesArr; 662 | const data = await db.query(query, values); 663 | console.log('insert sql result data.rows[0]', data.rows[0]); 664 | return data.rows[0]; 665 | } catch (err: any) { 666 | throw new Error(err); 667 | } 668 | }, 669 | deleteStarshipSpec: async (parent, args, context, info) => { 670 | try { 671 | const query = `DELETE FROM starship_specs 672 | WHERE _id = $1 RETURNING *`; 673 | const values = [args._id]; 674 | const data = await db.query(query, values); 675 | console.log('delete sql result data.rows[0]', data.rows[0]); 676 | return data.rows[0]; 677 | } catch (err: any) { 678 | throw new Error(err); 679 | } 680 | }, 681 | addVessel: async (parent, args, context, info) => { 682 | try { 683 | const query = `INSERT INTO vessels (cargo_capacity, consumables, 684 | cost_in_credits, crew, length, 685 | manufacturer, max_atmosphering_speed, model, 686 | name, passengers, vessel_class, 687 | vessel_type) 688 | VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) 689 | RETURNING *`; 690 | const values = [args.cargo_capacity, args.consumables, 691 | args.cost_in_credits, args.crew, args.length, 692 | args.manufacturer, args.max_atmosphering_speed, args.model, 693 | args.name, args.passengers, args.vessel_class, 694 | args.vessel_type]; 695 | const data = await db.query(query, values); 696 | console.log('insert sql result data.rows[0]', data.rows[0]); 697 | return data.rows[0]; 698 | } catch (err: any) { 699 | throw new Error(err); 700 | } 701 | }, 702 | updateVessel: async (parent, args, context, info) => { 703 | try { 704 | // sanitizing data for sql insert 705 | const argsArr = Object.keys(args).filter((el) => (el !== '_id')); 706 | const setStr = argsArr 707 | .map((el, i) => el + ' = $' + (i + 1)) 708 | .join(', '); 709 | argsArr.push('_id'); 710 | const pKey = '$' + argsArr.length; 711 | const valuesArr = argsArr.map((el) => args[el]); 712 | 713 | // insert query 714 | const query = 'UPDATE vessels SET ' + setStr + ' WHERE _id = ' + pKey + ' RETURNING *'; 715 | const values = valuesArr; 716 | const data = await db.query(query, values); 717 | console.log('insert sql result data.rows[0]', data.rows[0]); 718 | return data.rows[0]; 719 | } catch (err: any) { 720 | throw new Error(err); 721 | } 722 | }, 723 | deleteVessel: async (parent, args, context, info) => { 724 | try { 725 | const query = `DELETE FROM vessels 726 | WHERE _id = $1 RETURNING *`; 727 | const values = [args._id]; 728 | const data = await db.query(query, values); 729 | console.log('delete sql result data.rows[0]', data.rows[0]); 730 | return data.rows[0]; 731 | } catch (err: any) { 732 | throw new Error(err); 733 | } 734 | }, 735 | }, 736 | 737 | Film: { 738 | people: async (films) => { 739 | try { 740 | const query = `SELECT * FROM people 741 | LEFT OUTER JOIN people_in_films 742 | ON people._id = people_in_films.person_id 743 | WHERE people_in_films.film_id = $1`; 744 | const values = [films._id]; 745 | const data = await db.query(query, values); 746 | return data.rows; 747 | } catch (err: any) { 748 | throw new Error(err); 749 | } 750 | }, 751 | planets: async (films) => { 752 | try { 753 | const query = `SELECT * FROM planets 754 | LEFT OUTER JOIN planets_in_films 755 | ON planets._id = planets_in_films.planet_id 756 | WHERE planets_in_films.film_id = $1`; 757 | const values = [films._id]; 758 | const data = await db.query(query, values); 759 | return data.rows; 760 | } catch (err: any) { 761 | throw new Error(err); 762 | } 763 | }, 764 | species: async (films) => { 765 | try { 766 | const query = `SELECT * FROM species 767 | LEFT OUTER JOIN species_in_films 768 | ON species._id = species_in_films.species_id 769 | WHERE species_in_films.film_id = $1`; 770 | const values = [films._id]; 771 | const data = await db.query(query, values); 772 | return data.rows; 773 | } catch (err: any) { 774 | throw new Error(err); 775 | } 776 | }, 777 | vessels: async (films) => { 778 | try { 779 | const query = `SELECT * FROM vessels 780 | LEFT OUTER JOIN vessels_in_films 781 | ON vessels._id = vessels_in_films.vessel_id 782 | WHERE vessels_in_films.film_id = $1`; 783 | const values = [films._id]; 784 | const data = await db.query(query, values); 785 | return data.rows; 786 | } catch (err: any) { 787 | throw new Error(err); 788 | } 789 | }, 790 | }, 791 | Person: { 792 | planets: async (people) => { 793 | try { 794 | const query = `SELECT planets.* FROM planets 795 | LEFT OUTER JOIN people 796 | ON planets._id = people.homeworld_id 797 | WHERE people._id = $1`; 798 | const values = [people._id]; 799 | const data = await db.query(query, values); 800 | return data.rows; 801 | } catch (err: any) { 802 | throw new Error(err); 803 | } 804 | }, 805 | species: async (people) => { 806 | try { 807 | const query = `SELECT species.* FROM species 808 | LEFT OUTER JOIN people 809 | ON species._id = people.species_id 810 | WHERE people._id = $1`; 811 | const values = [people._id]; 812 | const data = await db.query(query, values); 813 | return data.rows; 814 | } catch (err: any) { 815 | throw new Error(err); 816 | } 817 | }, 818 | films: async (people) => { 819 | try { 820 | const query = `SELECT * FROM films 821 | LEFT OUTER JOIN people_in_films 822 | ON films._id = people_in_films.film_id 823 | WHERE people_in_films.person_id = $1`; 824 | const values = [people._id]; 825 | const data = await db.query(query, values); 826 | return data.rows; 827 | } catch (err: any) { 828 | throw new Error(err); 829 | } 830 | }, 831 | vessels: async (people) => { 832 | try { 833 | const query = `SELECT * FROM vessels 834 | LEFT OUTER JOIN pilots 835 | ON vessels._id = pilots.vessel_id 836 | WHERE pilots.person_id = $1`; 837 | const values = [people._id]; 838 | const data = await db.query(query, values); 839 | return data.rows; 840 | } catch (err: any) { 841 | throw new Error(err); 842 | } 843 | }, 844 | }, 845 | Planet: { 846 | people: async (planets) => { 847 | try { 848 | const query = `SELECT * FROM people 849 | WHERE homeworld_id = $1`; 850 | const values = [planets._id]; 851 | const data = await db.query(query, values); 852 | return data.rows; 853 | } catch (err: any) { 854 | throw new Error(err); 855 | } 856 | }, 857 | species: async (planets) => { 858 | try { 859 | const query = `SELECT * FROM species 860 | WHERE homeworld_id = $1`; 861 | const values = [planets._id]; 862 | const data = await db.query(query, values); 863 | return data.rows; 864 | } catch (err: any) { 865 | throw new Error(err); 866 | } 867 | }, 868 | films: async (planets) => { 869 | try { 870 | const query = `SELECT * FROM films 871 | LEFT OUTER JOIN planets_in_films 872 | ON films._id = planets_in_films.film_id 873 | WHERE planets_in_films.planet_id = $1`; 874 | const values = [planets._id]; 875 | const data = await db.query(query, values); 876 | return data.rows; 877 | } catch (err: any) { 878 | throw new Error(err); 879 | } 880 | }, 881 | }, 882 | Species: { 883 | planets: async (species) => { 884 | try { 885 | const query = `SELECT planets.* FROM planets 886 | LEFT OUTER JOIN species 887 | ON planets._id = species.homeworld_id 888 | WHERE species._id = $1`; 889 | const values = [species._id]; 890 | const data = await db.query(query, values); 891 | return data.rows; 892 | } catch (err: any) { 893 | throw new Error(err); 894 | } 895 | }, 896 | people: async (species) => { 897 | try { 898 | const query = `SELECT * FROM people 899 | WHERE species_id = $1`; 900 | const values = [species._id]; 901 | const data = await db.query(query, values); 902 | return data.rows; 903 | } catch (err: any) { 904 | throw new Error(err); 905 | } 906 | }, 907 | films: async (species) => { 908 | try { 909 | const query = `SELECT * FROM films 910 | LEFT OUTER JOIN species_in_films 911 | ON films._id = species_in_films.film_id 912 | WHERE species_in_films.species_id = $1`; 913 | const values = [species._id]; 914 | const data = await db.query(query, values); 915 | return data.rows; 916 | } catch (err: any) { 917 | throw new Error(err); 918 | } 919 | }, 920 | }, 921 | StarshipSpec: { 922 | vessels: async (starship_specs) => { 923 | try { 924 | const query = `SELECT vessels.* FROM vessels 925 | LEFT OUTER JOIN starship_specs 926 | ON vessels._id = starship_specs.vessel_id 927 | WHERE starship_specs._id = $1`; 928 | const values = [starship_specs._id]; 929 | const data = await db.query(query, values); 930 | return data.rows; 931 | } catch (err: any) { 932 | throw new Error(err); 933 | } 934 | }, 935 | }, 936 | Vessel: { 937 | starshipSpecs: async (vessels) => { 938 | try { 939 | const query = `SELECT * FROM starship_specs 940 | WHERE vessel_id = $1`; 941 | const values = [vessels._id]; 942 | const data = await db.query(query, values); 943 | return data.rows; 944 | } catch (err: any) { 945 | throw new Error(err); 946 | } 947 | }, 948 | people: async (vessels) => { 949 | try { 950 | const query = `SELECT * FROM people 951 | LEFT OUTER JOIN pilots 952 | ON people._id = pilots.person_id 953 | WHERE pilots.vessel_id = $1`; 954 | const values = [vessels._id]; 955 | const data = await db.query(query, values); 956 | return data.rows; 957 | } catch (err: any) { 958 | throw new Error(err); 959 | } 960 | }, 961 | films: async (vessels) => { 962 | try { 963 | const query = `SELECT * FROM films 964 | LEFT OUTER JOIN vessels_in_films 965 | ON films._id = vessels_in_films.film_id 966 | WHERE vessels_in_films.vessel_id = $1`; 967 | const values = [vessels._id]; 968 | const data = await db.query(query, values); 969 | return data.rows; 970 | } catch (err: any) { 971 | throw new Error(err); 972 | } 973 | }, 974 | }, 975 | }; 976 | 977 | const schema = makeExecutableSchema({ 978 | typeDefs, 979 | resolvers, 980 | }); 981 | 982 | export default schema; -------------------------------------------------------------------------------- /server/server.ts: -------------------------------------------------------------------------------- 1 | import express, { 2 | Application, Request, Response, NextFunction, 3 | } from 'express'; 4 | import Router from './router'; 5 | 6 | const path = require('path'); 7 | 8 | const PORT = 3000; 9 | const app: Application = express(); 10 | 11 | // handle parsing request body 12 | app.use(express.json()); 13 | app.use(express.urlencoded({ extended: true })); 14 | 15 | // handle requests for static files 16 | app.use('/client', express.static(path.join(__dirname, '../client/assets'))); 17 | 18 | app.use('/', Router); 19 | 20 | app.use('/build', express.static(path.join(__dirname, '../build'))); 21 | 22 | app.get('/', (req: Request, res: Response) => { 23 | return res.status(200).sendFile(path.join(__dirname, '../index.html')); 24 | }); 25 | 26 | app.get('/schema', (req: Request, res: Response) => { 27 | return res.status(200).sendFile(path.join(__dirname, '../index.html')); 28 | }); 29 | 30 | // catch-all route handler 31 | app.use('*', (req: Request, res: Response) => res.status(400).send('This is not the page you\'re looking for...')); 32 | 33 | // global error handler 34 | app.use((err, req: Request, res: Response, next: NextFunction) => { 35 | const defaultErr = { 36 | log: 'Express error handler caught unknown middleware error', 37 | status: 400, 38 | message: { err: 'An error occurred' }, 39 | }; 40 | const errorObj = { ...defaultErr, ...err }; 41 | console.log(errorObj.log); 42 | return res.status(errorObj.status).json(errorObj.message); 43 | }); 44 | 45 | app.listen(3000, () => { 46 | console.log(`⚡Server is listening on port ${PORT}... 🚀`); 47 | }); 48 | 49 | export default app; -------------------------------------------------------------------------------- /server/utils/helperFunc.ts: -------------------------------------------------------------------------------- 1 | import { singular } from 'pluralize'; 2 | 3 | /** 4 | * Converts the SQL column's data_type from to GraphQL data types 5 | * @param {string} type (column/field data type) 6 | * @param {string} columnName (column/field name) 7 | * @returns {string} GraphQL data type of column/field 8 | */ 9 | module.exports.convertDataType = (type: string, columnName: string): string => { 10 | if (columnName === '_id') return 'ID'; 11 | if (columnName.includes('_id')) return 'Int'; 12 | switch (type) { 13 | case 'character varying': return 'String'; 14 | case 'character': return 'String'; 15 | case 'date': return 'String'; 16 | case 'boolean': return 'Boolean'; 17 | case 'integer': return 'Int'; 18 | case 'numeric': return 'Int'; 19 | case 'ARRAY': return '[String]'; 20 | case 'smallint': return 'Int'; 21 | case 'bigint': return 'Float'; 22 | case 'timestamp with time zone': return 'timestamptz'; 23 | default: return type; 24 | } 25 | }; 26 | 27 | /** 28 | * Converts the SQL column's is_nullable from to GraphQL is_nullable 29 | * @param {string} isNullable (column's is_nullable) 30 | * @returns {string} ! or '' 31 | */ 32 | module.exports.checkNullable = (isNullable: string): string => { 33 | if (isNullable === 'NO') { 34 | return '!'; 35 | } 36 | return ''; 37 | }; 38 | 39 | /** 40 | * Pascalizes and singularlizes a table name 41 | * @param {string} tableName 42 | * @returns {string} pascalized and singularlized table name 43 | */ 44 | module.exports.capitalizeAndSingularize = (tableName: string): string => { 45 | const split = tableName.split('_'); 46 | const pascalize = split.map((ele) => ele[0].toUpperCase() + ele.slice(1)).join(''); 47 | const singularize: string = singular(pascalize); 48 | return singularize; 49 | }; 50 | 51 | /** 52 | * Camelcase and singularlizes a table name 53 | * @param {string} tableName 54 | * @returns {string} Camelcase and singularlized table name 55 | */ 56 | module.exports.makeCamelCaseAndSingularize = (tableName: string): string => { 57 | const split = tableName.split('_'); 58 | const camelCaseArray: string[] = []; 59 | for (let i = 0; i < split.length; i += 1) { 60 | if (i === 0) { 61 | camelCaseArray.push(split[i]); 62 | } else { 63 | camelCaseArray.push(split[i][0].toUpperCase() + split[i].slice(1)); 64 | } 65 | } 66 | const camelCase = camelCaseArray.join(''); 67 | const singularize = singular(camelCase); 68 | return singularize; 69 | }; 70 | 71 | /** 72 | * Camelcase a table name 73 | * @param {string} tableName 74 | * @returns {string} Camelcase table name 75 | */ 76 | module.exports.makeCamelCase = (tableName: string): string => { 77 | const split = tableName.split('_'); 78 | const camelCaseArray: string[] = []; 79 | for (let i = 0; i < split.length; i += 1) { 80 | if (i === 0) { 81 | camelCaseArray.push(split[i]); 82 | } else { 83 | camelCaseArray.push(split[i][0].toUpperCase() + split[i].slice(1)); 84 | } 85 | } 86 | const camelCase = camelCaseArray.join(''); 87 | return camelCase; 88 | }; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "lib": ["dom", "dom.iterable", "esnext"], 6 | "allowJs": true, 7 | "allowSyntheticDefaultImports": true, 8 | "skipLibCheck": true, 9 | "esModuleInterop": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "noEmit": true, 16 | "jsx": "react", 17 | "noImplicitAny": false 18 | }, 19 | "include": ["client"] 20 | } -------------------------------------------------------------------------------- /webpack.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import webpack from 'webpack'; 3 | import HtmlWebpackPlugin from 'html-webpack-plugin'; 4 | 5 | const config: any = { 6 | mode: 'development', // process.env.NODE_ENV 7 | entry: './client/index.tsx', 8 | devtool: 'inline-source-map', 9 | // target: 'electron-renderer', 10 | module: { 11 | rules: [ 12 | { 13 | test: /\.(ts|js)x?$/, 14 | exclude: /node_modules/, 15 | use: { 16 | loader: 'babel-loader', 17 | options: { 18 | presets: [ 19 | '@babel/preset-env', 20 | '@babel/preset-react', 21 | '@babel/preset-typescript', 22 | ], 23 | }, 24 | }, 25 | }, 26 | { 27 | test: [/\.s[ac]ss$/i, /\.css$/i], 28 | use: [ 29 | // Creates `style` nodes from JS strings 30 | 'style-loader', 31 | // Translates CSS into CommonJS 32 | 'css-loader', 33 | // Compiles Sass to CSS 34 | // 'sass-loader', 35 | ], 36 | }, 37 | { 38 | test: /\.(png|jpe?g|gif)$/i, 39 | use: [{ loader: 'file-loader' }], 40 | }, 41 | ], 42 | }, 43 | resolve: { 44 | extensions: ['.tsx', '.ts', '.js'], 45 | }, 46 | output: { 47 | path: path.resolve(__dirname, 'build'), 48 | filename: 'bundle.js', 49 | }, 50 | devServer: { 51 | static: { 52 | directory: path.resolve(__dirname, 'build'), 53 | publicPath: '/', 54 | }, 55 | // contentBase: path.join(__dirname, "build"), 56 | compress: true, 57 | port: 8080, 58 | proxy: { 59 | '/': 'http://localhost:3000', 60 | '/schema': 'http://localhost:3000', 61 | '/client': 'http://localhost:3000', 62 | }, 63 | historyApiFallback: true, 64 | // hot: true 65 | }, 66 | 67 | plugins: [ 68 | new HtmlWebpackPlugin({ 69 | title: 'development', 70 | template: './index.html', 71 | })], 72 | 73 | }; 74 | 75 | export default config; --------------------------------------------------------------------------------