├── .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 |
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 | {/* }>Buy Us A Coffee */}
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 |
24 | Get Started
25 |
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 |
24 |
25 |
26 | }>Get Started
27 |
28 | }> Sandbox
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 | setText(schema)}>Schema
53 |
54 |
55 | setText(resolvers)}>Resolvers
56 |
57 |
58 | copyCode()}>Copy
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 | getDataFromDB(databaseURL)} endIcon={ }>Submit
94 |
95 |
96 |
97 | getSampleDB()} endIcon={ }>Demo Database
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;
--------------------------------------------------------------------------------