├── .DS_Store ├── src ├── .DS_Store ├── assets │ ├── .DS_Store │ ├── Github.png │ ├── LinkedIn.png │ ├── MariposaLogo.png │ ├── Profile_Adam.png │ ├── Profile_Alex.png │ ├── Profile_James.png │ ├── Profile_Mark.png │ └── MariposaLogo(800px).png ├── styles │ ├── colorsAndFonts.scss │ ├── styles.scss.d.ts │ ├── _StaticPages.scss │ ├── styles.scss │ └── _LandingPage.scss ├── components │ ├── Sandbox.tsx │ ├── StaticPages │ │ ├── NavBarLandingPage.tsx │ │ ├── LandingPageContainer.tsx │ │ └── AboutUs.tsx │ ├── graph.tsx │ ├── Tree.tsx │ ├── Resolvers.tsx │ ├── MainPage.tsx │ ├── formComponents │ │ ├── WebLoginForm.js │ │ └── WebRegisterForm.js │ └── MainPageNavBar.tsx ├── services │ ├── auth-header.js │ ├── user.service.js │ └── auth_service.js ├── store.js ├── index.tsx ├── slices │ ├── messages.js │ └── authentication.js └── App.js ├── client ├── .DS_Store └── components │ └── Login.css ├── .gitignore ├── babel.config.js ├── server ├── models │ ├── mariposaDB.ts │ └── projectDB.ts ├── schema │ └── schema.ts ├── routes │ ├── mariposa.ts │ └── project.ts ├── types │ ├── PoolWrapper.ts │ ├── DBResponseTypes.ts │ └── dummyTables.ts ├── SQLConversion │ ├── GQLObjectTypeCreator.ts │ ├── typeDefMaker.ts │ ├── GQLQueryTypeCreator.ts │ ├── SQLQueryHelpers.ts │ ├── GQLMutationTypeCreator.ts │ ├── SQLSchemaHelpers.ts │ ├── SQLConversionHelpers.ts │ ├── resolverMaker.ts │ └── resolverStringMaker.ts ├── controllers │ ├── mariposaDBController.ts │ └── projectDBController.ts └── server.ts ├── dist ├── index.html └── js │ └── main.js.LICENSE.txt ├── index.html ├── Dockerfile ├── user.ts ├── README.md ├── webpack.config.js ├── __tests__ └── supertest.js ├── package.json └── tsconfig.json /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/mariposa/HEAD/.DS_Store -------------------------------------------------------------------------------- /src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/mariposa/HEAD/src/.DS_Store -------------------------------------------------------------------------------- /client/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/mariposa/HEAD/client/.DS_Store -------------------------------------------------------------------------------- /src/assets/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/mariposa/HEAD/src/assets/.DS_Store -------------------------------------------------------------------------------- /src/assets/Github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/mariposa/HEAD/src/assets/Github.png -------------------------------------------------------------------------------- /src/assets/LinkedIn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/mariposa/HEAD/src/assets/LinkedIn.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | node_modules 4 | g6tree.tsx 5 | dist 6 | DS_Store 7 | .DS_Store -------------------------------------------------------------------------------- /src/assets/MariposaLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/mariposa/HEAD/src/assets/MariposaLogo.png -------------------------------------------------------------------------------- /src/assets/Profile_Adam.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/mariposa/HEAD/src/assets/Profile_Adam.png -------------------------------------------------------------------------------- /src/assets/Profile_Alex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/mariposa/HEAD/src/assets/Profile_Alex.png -------------------------------------------------------------------------------- /src/assets/Profile_James.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/mariposa/HEAD/src/assets/Profile_James.png -------------------------------------------------------------------------------- /src/assets/Profile_Mark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/mariposa/HEAD/src/assets/Profile_Mark.png -------------------------------------------------------------------------------- /src/assets/MariposaLogo(800px).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/mariposa/HEAD/src/assets/MariposaLogo(800px).png -------------------------------------------------------------------------------- /src/styles/colorsAndFonts.scss: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@500&display=swap'); 2 | $fontOfBody: Montserrat; 3 | 4 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@babel/preset-env', 4 | '@babel/preset-react', 5 | '@babel/preset-typescript' 6 | ] 7 | } -------------------------------------------------------------------------------- /server/models/mariposaDB.ts: -------------------------------------------------------------------------------- 1 | import { PoolWrapper } from '../types/PoolWrapper'; 2 | 3 | const PG_URI: string = `YOUR_USER_DB`; 4 | 5 | const db = new PoolWrapper(PG_URI); 6 | 7 | export default db; 8 | -------------------------------------------------------------------------------- /src/components/Sandbox.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function Sandbox() { 4 | 5 | return ( 6 |
7 | {window.open('/graphql', '_blank')} 8 |
9 | ) 10 | } -------------------------------------------------------------------------------- /src/styles/styles.scss.d.ts: -------------------------------------------------------------------------------- 1 | // This file is automatically generated. 2 | // Please do not change this file! 3 | interface CssExports { 4 | 5 | } 6 | export const cssExports: CssExports; 7 | export default cssExports; 8 | -------------------------------------------------------------------------------- /server/models/projectDB.ts: -------------------------------------------------------------------------------- 1 | import { PoolWrapper } from '../types/PoolWrapper'; 2 | 3 | // To be updated by user input 4 | export const PG_URI: string = `postgres://vozivmzl:6vudzc_5vjGcmGFKOClHXhcJfXLW-QXB@fanny.db.elephantsql.com/vozivmzl`; 5 | 6 | const db = new PoolWrapper(PG_URI); 7 | 8 | export default db; -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | Mariposa
-------------------------------------------------------------------------------- /src/services/auth-header.js: -------------------------------------------------------------------------------- 1 | /* 2 | Checks Local Storage for user item. If there is a logged in user with accessToken (JWT), 3 | return HTTP Authorization header. Otherwise, return an empty object. 4 | */ 5 | export function authHeader() { 6 | const user = JSON.parse(localStorage.getItem('user')); 7 | return user && user.accessToken ? { 'x-access-token': user.accessToken } : {}; 8 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Mariposa 6 | 7 | 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit'; 2 | //import reducers 3 | import authReducer from './slices/authentication'; 4 | import messageReducer from './slices/messages'; 5 | //bundle reducers 6 | const reducer = { 7 | auth: authReducer, 8 | message: messageReducer 9 | } 10 | 11 | const store = configureStore({ 12 | reducer: reducer, 13 | devTools: true, 14 | }) 15 | 16 | export default store; -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import 'bootstrap/dist/css/bootstrap.min.css'; 4 | import './styles/styles.scss'; 5 | import { App } from './App' 6 | import store from './store' 7 | import { Provider } from 'react-redux' 8 | 9 | const rootDiv = document.createElement("div"); 10 | rootDiv.setAttribute("id", "root"); 11 | document.body.appendChild(rootDiv); 12 | 13 | ReactDOM.render( 14 | 15 | 16 | , 17 | rootDiv 18 | ); 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/slices/messages.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | 3 | const initialState = {}; 4 | 5 | const messageSlice = createSlice({ 6 | name: "message", 7 | initialState, 8 | reducers: { 9 | setMessage: (state, action) => { 10 | return { message: action.payload }; 11 | }, 12 | clearMessage: () => { 13 | return { message: "" }; 14 | }, 15 | }, 16 | }); 17 | 18 | const { reducer, actions } = messageSlice; 19 | 20 | export const { setMessage, clearMessage } = actions 21 | export default reducer; -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Start FROM a baseline image of node v16.13 2 | # Set up a WORKDIR for application in the container and set it to /usr/src/app. 3 | # COPY all of your application files to the WORKDIR in the container 4 | # RUN a command to npm install your node_modules in the container 5 | # RUN a command to build your application in the container 6 | # EXPOSE your server port (3000) 7 | # Create an ENTRYPOINT where you'll run node ./server/server.js 8 | 9 | FROM node:16.13 10 | WORKDIR /usr/src/app 11 | COPY . /usr/src/app/ 12 | RUN npm install 13 | RUN npm run build:react 14 | EXPOSE 3000 15 | ENTRYPOINT npm run prod:server -------------------------------------------------------------------------------- /src/components/StaticPages/NavBarLandingPage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | function NavBarLandingPage() { 5 | return ( 6 |
7 | 13 | 16 |
17 | ); 18 | } 19 | 20 | export default NavBarLandingPage; 21 | -------------------------------------------------------------------------------- /server/schema/schema.ts: -------------------------------------------------------------------------------- 1 | import { makeExecutableSchema } from '@graphql-tools/schema'; 2 | import { IResolvers } from '@graphql-tools/utils'; 3 | import db from '../models/projectDB'; 4 | import resolverMaker from "../SQLConversion/resolverMaker"; 5 | import { GraphQLSchema } from "graphql/type/schema"; 6 | import { typeDefMaker } from "../SQLConversion/typeDefMaker"; 7 | import { tables } from '../types/dummyTables'; 8 | 9 | const resolvers: IResolvers = resolverMaker.generateResolvers(tables, db); 10 | const typeDefs: string = typeDefMaker.generateTypes(tables); 11 | 12 | export const schema: GraphQLSchema = makeExecutableSchema({ 13 | typeDefs, 14 | resolvers 15 | }); -------------------------------------------------------------------------------- /server/routes/mariposa.ts: -------------------------------------------------------------------------------- 1 | import express, {Request, Response, NextFunction} from 'express'; 2 | import mariposaDBController from '../controllers/mariposaDBController'; 3 | const mariposaRouter = express.Router(); 4 | 5 | // posts a user of given name and password 6 | mariposaRouter.post('/signup', mariposaDBController.signUp, (req: Request, res: Response, next: NextFunction) => { 7 | return res.json({message: res.locals.signup}); 8 | }); 9 | 10 | mariposaRouter.post('/signin', mariposaDBController.signIn, (req: Request, res: Response, next: NextFunction) => { 11 | return res.json({accessToken: res.locals.accessToken}); 12 | }); 13 | 14 | 15 | export default mariposaRouter; -------------------------------------------------------------------------------- /src/services/user.service.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { authHeader } from "./auth-header"; 3 | 4 | export const userService = { 5 | getPublicContent, 6 | getUserBoard, 7 | getModeratorBoard, 8 | getAdminBoard, 9 | }; 10 | 11 | const API_URL = "http://localhost:3000/api/test/"; 12 | 13 | const getPublicContent = () => { 14 | return axios.get(API_URL + "all"); 15 | }; 16 | 17 | const getUserBoard = () => { 18 | return axios.get(API_URL + "user", { headers: authHeader() }); 19 | }; 20 | 21 | const getModeratorBoard = () => { 22 | return axios.get(API_URL + "mod", { headers: authHeader() }); 23 | }; 24 | 25 | const getAdminBoard = () => { 26 | return axios.get(API_URL + "admin", { headers: authHeader() }); 27 | }; -------------------------------------------------------------------------------- /user.ts: -------------------------------------------------------------------------------- 1 | 2 | const regexp = new RegExp(/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/); 3 | 4 | console.log(regexp.test('james@hello.com')); 5 | console.log(regexp.test('james@hello')); 6 | 7 | class User { 8 | private id: number; 9 | private username: string; 10 | private email: string; 11 | constructor(id: number, username: string, email: string) { 12 | this.id = id, 13 | this.username = username, 14 | this.email = email 15 | } 16 | getId(): number { 17 | return this.id; 18 | } 19 | getUsername(): string { 20 | return this.username; 21 | } 22 | getEmail(): string { 23 | return this.email; 24 | } 25 | } 26 | 27 | export default User; -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BrowserRouter as Router, Route, Routes } from 'react-router-dom' 3 | 4 | import LandingPageContainer from './components/StaticPages/LandingPageContainer'; 5 | import AboutUs from './components/StaticPages/AboutUs'; 6 | import MainPage from './components/MainPage' 7 | 8 | 9 | export const App = () => { 10 | return ( 11 | 12 | 13 | } 16 | /> 17 | } 20 | /> 21 | } 24 | /> 25 | 26 | 27 | 28 | ); 29 | }; 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /server/types/PoolWrapper.ts: -------------------------------------------------------------------------------- 1 | import { Pool, QueryResult } from "pg"; 2 | 3 | export class PoolWrapper { 4 | pg_uri: string; 5 | pool: Pool; 6 | 7 | constructor(pg_uri: string) { 8 | this.pg_uri = pg_uri; 9 | this.pool = new Pool({ 10 | connectionString: this.pg_uri, 11 | }) 12 | } 13 | query = (text: string, params: string[] = []): Promise => { 14 | console.log('executed query', text); 15 | return new Promise((resolve, reject) => { 16 | this.pool.query(text, params, (err, res) => { 17 | if (err) return reject(err); 18 | else { 19 | resolve((res)); 20 | } 21 | }); 22 | }); 23 | } 24 | updateUri = (uri: string) => { 25 | if (this.pg_uri !== uri) { 26 | this.pg_uri = uri; 27 | this.pool = new Pool({ 28 | connectionString: uri, 29 | }); 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /src/components/graph.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Paper from '@mui/material/Paper'; 3 | import { createTheme, ThemeProvider } from '@mui/material/styles'; 4 | import Tree from './Tree' 5 | 6 | 7 | const theme = createTheme({ palette: { mode: 'light' } }); 8 | 9 | export default function graph(props: any) { 10 | return ( 11 |
12 | 13 | 14 | 23 | 24 |
25 | 26 |
27 |
28 |
29 |
30 | 31 | ) 32 | } -------------------------------------------------------------------------------- /server/SQLConversion/GQLObjectTypeCreator.ts: -------------------------------------------------------------------------------- 1 | import { SQLConversionHelpers } from './SQLConversionHelpers' 2 | import { Table, Column } from '../types/DBResponseTypes'; 3 | const { checkIsTableJoin, fieldValueCreator, inObjectTypeCase } = SQLConversionHelpers; 4 | 5 | // IF IN JOIN TABLE IT's an [TYPE], othersie TYPE 6 | export const GQLObjectTypeCreator = (tableObject: Table): string => { 7 | const { tablename, columns } = tableObject; 8 | let type = ""; 9 | if (checkIsTableJoin(columns)) { 10 | return ''; 11 | } else { 12 | type += `type ${inObjectTypeCase(tablename)} {\n`; 13 | columns.forEach((columnObj: Column) => { 14 | const { column_name, constraint_type, primary_table } = columnObj; 15 | if (constraint_type === 'FOREIGN KEY' && primary_table) { 16 | type += ` ${primary_table}: ${inObjectTypeCase(primary_table)}\n` 17 | } else { 18 | type += ` ${column_name}: ${fieldValueCreator(columnObj)}\n` 19 | } 20 | }); 21 | } 22 | type += '}\n'; 23 | 24 | return type; 25 | } 26 | 27 | -------------------------------------------------------------------------------- /server/SQLConversion/typeDefMaker.ts: -------------------------------------------------------------------------------- 1 | import { Table } from "../types/DBResponseTypes"; 2 | import { GQLMutationTypeCreator } from "./GQLMutationTypeCreator"; 3 | import { GQLObjectTypeCreator } from "./GQLObjectTypeCreator"; 4 | import { GQLQueryTypeCreator } from "./GQLQueryTypeCreator"; 5 | 6 | export const typeDefMaker = { 7 | generateTypes(tables: Table[]): string { 8 | let typeDefs = ''; 9 | typeDefs += tables.reduce((acc: string, table: Table) => { 10 | acc += GQLObjectTypeCreator(table); 11 | return acc; 12 | }, ''); 13 | 14 | let queryTypes = 'type Query {'; 15 | tables.forEach((table: Table) => { 16 | queryTypes += GQLQueryTypeCreator(table); 17 | }); 18 | queryTypes += '\n}'; 19 | typeDefs += queryTypes; 20 | 21 | let mutationTypes = `\ntype Mutation {`; 22 | tables.forEach((table: Table) => { 23 | mutationTypes += GQLMutationTypeCreator(table); 24 | }); 25 | mutationTypes = mutationTypes.substring(0, mutationTypes.length - 1) + '\n}'; 26 | typeDefs += mutationTypes; 27 | 28 | return typeDefs; 29 | } 30 | } -------------------------------------------------------------------------------- /src/services/auth_service.js: -------------------------------------------------------------------------------- 1 | /* 2 | Make asynchronous HTTP requests 3 | */ 4 | import axios from 'axios'; 5 | 6 | const API_URL = '/mariposa/auth/'; 7 | 8 | 9 | 10 | export const register = (firstname, lastname, username, email, password) => { 11 | //make a post request to the server 12 | return axios.post(API_URL + 'signup', {firstname, lastname, username, email, password}); 13 | }; 14 | 15 | export const login = (username, password) => { 16 | return axios 17 | .post(API_URL + 'signin', {username, password}) 18 | .then((response) => { 19 | console.log(response.data) 20 | if (response.data.accessToken) { 21 | //set local storage key/value pair 22 | 23 | 24 | localStorage.setItem('user', JSON.stringify(response.data)); 25 | 26 | } 27 | 28 | return response.data; 29 | }) 30 | .catch(error => { 31 | return error.message; 32 | }); 33 | }; 34 | 35 | const logout = () => { 36 | localStorage.removeItem('user'); 37 | }; 38 | 39 | //available authService functions - definitions below 40 | export const authService = { 41 | register, 42 | login, 43 | logout, 44 | }; 45 | -------------------------------------------------------------------------------- /server/routes/project.ts: -------------------------------------------------------------------------------- 1 | import express, { Request, Response, NextFunction } from 'express'; 2 | import { projectDBController } from '../controllers/projectDBController'; 3 | import { rowsToD3, rowsToTable } from '../SQLConversion/SQLSchemaHelpers'; 4 | const projectRouter = express.Router(); 5 | const { updateDatabase, getAllTables, buildTypeDefs, buildResolvers } = projectDBController; 6 | 7 | projectRouter.post('/tables', updateDatabase, getAllTables, buildTypeDefs, buildResolvers, (req: Request, res: Response, next: NextFunction) => { 8 | return res.status(201).json({ 9 | typeDefs: res.locals.typeDefs, 10 | resolverString: res.locals.resolverString, 11 | }); 12 | }); 13 | 14 | projectRouter.get('/tables', getAllTables, (req: Request, res: Response, next: NextFunction) => { 15 | return res.status(200).json(rowsToTable(res.locals.userDbResponse)); 16 | }); 17 | 18 | //returns all tables and relevant SQL schema from user's provided db in desired D3 form 19 | projectRouter.post('/D3tables', updateDatabase, getAllTables, (req: Request, res: Response, next: NextFunction) => { 20 | return res.status(201).json(rowsToD3(res.locals.userDbResponse)); 21 | }); 22 | 23 | export default projectRouter; -------------------------------------------------------------------------------- /server/SQLConversion/GQLQueryTypeCreator.ts: -------------------------------------------------------------------------------- 1 | import { SQLConversionHelpers } from './SQLConversionHelpers' 2 | import { Table, Column } from '../types/DBResponseTypes'; 3 | const {checkIsTableJoin, fieldValueCreator, inObjectTypeCase, queryPluralCase, querySingularCase} = SQLConversionHelpers; 4 | 5 | export const GQLQueryTypeCreator = (tableObject: Table): string => { 6 | const {tablename, columns} = tableObject; 7 | let queryType = ""; 8 | if(checkIsTableJoin(columns)){ 9 | return ''; 10 | } else { 11 | const typeObject = inObjectTypeCase(tablename); 12 | let queryPluralType = `\n ${queryPluralCase(tablename)}: [${typeObject}]!`; 13 | let querySingularType = `\n ${querySingularCase(tablename)}(`; 14 | columns.forEach((columnObj: Column) => { 15 | const{column_name, constraint_type} = columnObj; 16 | if(constraint_type === 'PRIMARY KEY') { 17 | //args creator, account for more than one arg 18 | querySingularType += `${column_name}: ${fieldValueCreator(columnObj)},`; 19 | } 20 | }); 21 | //remove ending comma and replace with '): [typeObject]' 22 | querySingularType = querySingularType.substring(0, querySingularType.length - 1) + `): ${typeObject}!`; 23 | queryType += queryPluralType + querySingularType; 24 | } 25 | return queryType; 26 | } -------------------------------------------------------------------------------- /server/SQLConversion/SQLQueryHelpers.ts: -------------------------------------------------------------------------------- 1 | //helper function to shape response array of Table objects 2 | /***modified queryResult to type any[]***/ 3 | 4 | import {DBQueryResponse, Table } from '../types/DBResponseTypes'; 5 | 6 | export default function rowsToTable(queryResult: any[]): Table[] { 7 | const tablesObject = queryResult.reduce((tablesObject: { [key: string]: Table }, curr: DBQueryResponse) => { 8 | const { tablename, column_id, column_name, data_type, is_nullable, is_updatable, column_default, constraint_name, constraint_type, primary_table, primary_column } = curr; 9 | //check to see if tablesObject has a tablename property 10 | if (!tablesObject.hasOwnProperty(tablename)) { 11 | //if falsy, create a tablename property and assign it to an object with tablename and columns properties 12 | tablesObject[tablename] = { 13 | tablename: tablename, 14 | columns: [], 15 | } 16 | } 17 | /*if tablename is a property of tablesObject, then bundle the associated columns in an object 18 | and push them into a columns array*/ 19 | tablesObject[tablename].columns.push({ 20 | column_id, 21 | column_name, 22 | data_type, 23 | is_nullable, 24 | is_updatable, 25 | column_default, 26 | constraint_name, 27 | constraint_type, 28 | primary_table, 29 | primary_column 30 | }); 31 | return tablesObject; 32 | }, {}); 33 | return Object.values(tablesObject); 34 | } -------------------------------------------------------------------------------- /server/SQLConversion/GQLMutationTypeCreator.ts: -------------------------------------------------------------------------------- 1 | import { SQLConversionHelpers } from './SQLConversionHelpers' 2 | import { Table, Column } from '../types/DBResponseTypes'; 3 | const {checkIsTableJoin, fieldValueCreator, inObjectTypeCase, queryPluralCase, querySingularCase} = SQLConversionHelpers; 4 | 5 | 6 | export const GQLMutationTypeCreator = (tableObject: Table): string => { 7 | const {tablename, columns} = tableObject; 8 | let mutationType = ''; 9 | if(checkIsTableJoin(columns)){ 10 | return ''; 11 | } else { 12 | const typeObject = inObjectTypeCase(tablename); 13 | let mutationFields = '' 14 | let primaryKey; 15 | columns.forEach((columnObj: Column) => { 16 | const{column_name, constraint_type} = columnObj; 17 | if(constraint_type === "PRIMARY KEY") primaryKey = column_name; 18 | mutationFields += `\n ${column_name}: ${fieldValueCreator(columnObj)},`; 19 | }); 20 | mutationFields = mutationFields.substring(0, mutationFields.length - 1) 21 | const addMutation = `\n add${typeObject}(${mutationFields}): ${typeObject}!\n`; 22 | const updateMutation = `\n update${typeObject}(${mutationFields}): ${typeObject}!\n`; 23 | const deleteMutation =`\n delete${typeObject}(${primaryKey}: ID!): ${typeObject}!\n`; 24 | //remove ending comma and replace with '): [typeObject]' 25 | mutationType = addMutation + updateMutation + deleteMutation; 26 | } 27 | return mutationType; 28 | } 29 | 30 | -------------------------------------------------------------------------------- /src/components/Tree.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useEffect, useState } from "react"; 2 | import Tree from 'react-d3-tree'; 3 | 4 | //get request to the endpoint and set the state equal to this 5 | function TreeChart(props: any) { 6 | const [treeDataNew, setTreeDataNew] = useState({ 7 | name: '', 8 | children: [], 9 | }); 10 | 11 | //do the get request, obtaint the res.locals. setTreeData(res.locals.) 12 | useEffect(() => { 13 | setTreeDataNew(props.treeData); 14 | }) 15 | 16 | const straightPathFunc = (linkDatum: any, orientation: any) => { 17 | const { source, target } = linkDatum; 18 | return orientation === 'horizontal' 19 | ? `M${source.x},${source.y}L${target.x},${target.y}` 20 | : `M${source.x},${source.y}L${target.x},${target.y}`; 21 | } 22 | return ( 23 | // `` will fill width/height of its `#treeWrapper` container 24 |
25 | 39 |
40 | ); 41 | } 42 | 43 | export default TreeChart; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ![mariopsa](https://user-images.githubusercontent.com/81339845/146243011-e6842ffa-0abf-48e5-bb2d-9037e43a4509.jpg) 3 | 4 | 5 | # Mariposa 6 | 7 | Mariposa is an open-source GraphQL visualization and migration tool designed to help developers incorporate GraphQL into existing RESTful PostgresQL APIs. 8 | 9 | ## Features 10 | 11 | - Visualize your Postgres data (or sample data) 12 | - Generate TypeDefs and Resolvers for your data 13 | - Copy your schema 14 | - Test queries in the interactive playground 15 | 16 | 17 | ## Demo 18 | 19 | Log in or Sign Up! 20 | 21 | ![login](https://media.giphy.com/media/lyyPmCjymefTtSOPW5/giphy.gif) 22 | 23 | Input URI, visualize your data, and copy your GQL Schema! 24 | 25 | ![useApp](https://media.giphy.com/media/cF2KVuoOvTRu8zGxA0/giphy.gif) 26 | ## Deployment 27 | 28 | To run this project locally, clone the repo and then... 29 | 30 | ```bash 31 | npm run build 32 | npm run prod:server 33 | ``` 34 | 35 | Navigate to localhost:3000 in a browser. 36 | 37 | 38 | ## License 39 | 40 | [MIT](https://choosealicense.com/licenses/mit/) 41 | 42 | 43 | ## Tech Stack 44 | 45 | **Client:** React, Redux, MaterialUI 46 | 47 | **Server:** Node, Express, GraphQL 48 | 49 | 50 | ## Contributing 51 | 52 | Contributions are always welcome! 53 | 54 | Please make a PR from a new branch, or open up an issue! 55 | 56 | 57 | ## Authors 58 | 59 | - [Adam Berri](https://www.github.com/adamberri) 60 | - [Alex Barbazan](https://www.github.com/agbarbazan) 61 | - [James Maguire](https://www.github.com/jwmaguire15) 62 | - [Mark Dolan](https://www.github.com/markdolan1) 63 | 64 | -------------------------------------------------------------------------------- /src/styles/_StaticPages.scss: -------------------------------------------------------------------------------- 1 | @import './colorsAndFonts.scss'; 2 | 3 | 4 | .aboutUsWrapper { 5 | display: flex; 6 | flex-direction: column; 7 | justify-content: center; 8 | align-items: center; 9 | color: rgb(255, 255, 255); 10 | 11 | h1{ 12 | position: relative; 13 | margin-top: 1vh; 14 | } 15 | 16 | 17 | h2{ 18 | margin: 30vh 0 5vh ; 19 | position: relative; 20 | } 21 | 22 | p { 23 | font-family: $fontOfBody; 24 | width: 70%; 25 | text-align: center; 26 | position: relative; 27 | } 28 | 29 | 30 | .team { 31 | height: 10vh; 32 | display: flex; 33 | justify-content: center; 34 | align-items: center; 35 | position: relative; 36 | top: 10vh; 37 | justify-content: space-evenly; 38 | 39 | .imageDiv { 40 | margin-top: 1vh; 41 | display: flex; 42 | flex-direction: column; 43 | align-items: center; 44 | width: 20vw; 45 | 46 | $pictureWidth: 25vw; 47 | $pictureHeight: 25vh; 48 | $backgroundSize: 25vw; 49 | 50 | #profilePic { 51 | // background-image: url('../assets/image0.png'); 52 | height: 20vh; 53 | width: 20vh; 54 | border-radius: 50%; 55 | background-repeat: no-repeat; 56 | } 57 | } 58 | .contactIcons { 59 | display: flex; 60 | gap: 1vh; 61 | #linkedin { 62 | background-size: 3vh; 63 | width: 3vh; 64 | height: 3vh; 65 | border: none; 66 | background-color: transparent; 67 | cursor: pointer; 68 | } 69 | #github { 70 | background-size: 3vh; 71 | width: 3vh; 72 | height: 3vh; 73 | border: none; 74 | background-color: transparent; 75 | cursor: pointer; 76 | } 77 | } 78 | } 79 | } 80 | 81 | 82 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | // const nodeExternals = require('webpack-node-externals'); 3 | // const StartServerPlugin = require('start-server-webpack-plugin'); 4 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 5 | 6 | module.exports = { 7 | mode: process.env.NODE_ENV,//'development', 8 | entry: './src/index.tsx', 9 | module: { 10 | rules: [{ 11 | test: /\.(js|ts|tsx)$/, 12 | // include: /src/, 13 | use: [{ loader: 'babel-loader' }] 14 | }, 15 | { 16 | test: /\.(sass|css|scss)$/, 17 | use: ['style-loader', 'css-modules-typescript-loader', 'css-loader', 'sass-loader'] 18 | }, 19 | { 20 | test: /\.(png|jpe?g|gif)$/i, 21 | use: [{ loader: 'file-loader' }], 22 | }, 23 | { 24 | test: /\.svg$/, 25 | use: [ 26 | { 27 | loader: 'svg-url-loader', 28 | options: { 29 | limit: 10000, 30 | }, 31 | } 32 | ] 33 | }] 34 | }, 35 | output: { 36 | path: path.resolve(__dirname, 'dist'), 37 | filename: 'js/[name].js', 38 | }, 39 | devServer: { 40 | static: path.join(__dirname, '../dist/renderer'), 41 | historyApiFallback: true, 42 | compress: true, 43 | hot: true, 44 | port: 8080, 45 | // publicPath: '/', 46 | proxy: { 47 | '/': 'http://localhost:3000' 48 | }, 49 | }, 50 | resolve: { 51 | // Add `.ts` and `.tsx` as a resolvable extension. 52 | extensions: [".ts", ".tsx", ".js"] 53 | }, 54 | output: { 55 | path: path.resolve(__dirname, 'dist'), 56 | filename: 'js/[name].js', 57 | }, 58 | plugins: [ 59 | new HtmlWebpackPlugin({ 60 | title: 'dev', 61 | template: './index.html' 62 | }), 63 | ], 64 | }; -------------------------------------------------------------------------------- /server/controllers/mariposaDBController.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | import db from '../models/mariposaDB' 3 | 4 | const dbController = { 5 | async signUp(req: Request, res: Response, next: NextFunction) { 6 | try { 7 | const { firstname, lastname, username, email, password }: { firstname: string, lastname: string, username: string, email: string, password: string } = req.body; 8 | const query = `INSERT INTO user_accounts(firstname, lastname, username, email, password) 9 | VALUES($1, $2, $3, $4, $5) RETURNING firstname, lastname, username, email, password`; 10 | const values = [firstname, lastname, username, email, password]; 11 | const result = await db.query(query, values); 12 | 13 | res.locals.signup = 'Successful registration'; 14 | return next(); 15 | } 16 | catch (err) { 17 | return next(err); 18 | } 19 | }, 20 | async signIn(req: Request, res: Response, next: NextFunction) { 21 | try { 22 | const {username, password }: { username: string, password: string } = req.body; 23 | //check to see if username provided is an email instead 24 | const regex = new RegExp('@'); 25 | const userColumn = !regex.test(username) ? 'username' : 'email'; 26 | 27 | const query = `SELECT * FROM user_accounts WHERE ${userColumn} = $1 AND password = $2` 28 | const values = [username, password]; 29 | 30 | const result = await db.query(query, values); 31 | const user = result.rows[0]; 32 | 33 | res.locals.accessToken = user ? 'dfj23424fwefw' : ''; 34 | return next(); 35 | } 36 | catch (err) { 37 | return next(err); 38 | } 39 | }, 40 | // end of dbController class 41 | } 42 | 43 | 44 | export default dbController; -------------------------------------------------------------------------------- /server/server.ts: -------------------------------------------------------------------------------- 1 | import express, { Request, Response, NextFunction } from 'express'; 2 | import { graphqlHTTP } from 'express-graphql'; 3 | import path from 'path'; 4 | import mariposaRouter from './routes/mariposa'; 5 | import projectRouter from './routes/project'; 6 | import { schema } from './schema/schema'; 7 | /*require in routers: mariposaRouter for app requests / projectRouter 8 | for user db/graphql migration*/ 9 | const jwt = require('jsonwebtoken'); 10 | const app = express(); 11 | const PORT = 3000; 12 | 13 | const graphiql = graphqlHTTP({ 14 | schema, 15 | graphiql: true 16 | }); 17 | 18 | app.use(express.json()); 19 | app.use(express.urlencoded({ extended: true })); 20 | 21 | // routes 22 | app.use('/mariposa/auth', mariposaRouter); 23 | app.use('/project', projectRouter); 24 | app.use('/graphql', graphiql); 25 | 26 | if (process.env.NODE_ENV === 'production') { 27 | app.get('/', (req, res) => { 28 | return res.status(200).sendFile(path.resolve(__dirname, '../dist/index.html')); 29 | }); 30 | app.use('/', express.static(path.resolve(__dirname, '../dist'))); 31 | app.use('/js', express.static(path.resolve(__dirname, '../dist/js'))); 32 | }; 33 | 34 | app.use("*", (req: Request, res: Response) => { 35 | return res.status(404).send("Error, path not found"); 36 | }); 37 | 38 | //global error handler 39 | app.use((err: Error, req: Request, res: Response, next: NextFunction) => { 40 | const errorObj = { 41 | log: "global error handler in express app", 42 | message: { err: "global error handler in express app" }, 43 | }; 44 | const errorObject = Object.assign({}, errorObj, err); 45 | console.log(errorObject); 46 | return res.status(500).json(errorObject); 47 | }); 48 | 49 | if (process.env.NODE_ENV !== 'test') { 50 | app.listen(PORT, () => { 51 | console.log(`listening on port: ${PORT}`); 52 | }); 53 | } 54 | 55 | export default app; -------------------------------------------------------------------------------- /src/components/Resolvers.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import Paper from '@mui/material/Paper'; 3 | import { createTheme, ThemeProvider } from '@mui/material/styles'; 4 | import Button from '@mui/material/Button'; 5 | import ButtonGroup from '@mui/material/ButtonGroup'; 6 | import Box from '@mui/material/Box'; 7 | import ContentPasteIcon from '@mui/icons-material/ContentPaste'; 8 | const theme = createTheme({ palette: { mode: 'light' } }); 9 | import Highlight from 'react-highlight'; 10 | 11 | export default function Resolvers(props:any) { 12 | 13 | //state for the user defaulted to resolvers 14 | const [resolver, setResolver] = useState(false); 15 | 16 | return ( 17 | 18 | 19 | 26 |
27 | *': { 33 | m: 1, 34 | }, 35 | }} 36 | > 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | {resolver ? props.schema : props.text} 48 | 49 | 50 | 51 |
52 |
53 |
54 | ) 55 | } 56 | -------------------------------------------------------------------------------- /server/types/DBResponseTypes.ts: -------------------------------------------------------------------------------- 1 | // interface that follows the structure of D3 data 2 | export interface D3Data { 3 | name: string, 4 | children: D3Data[] 5 | attributes?: {[key: string]: string}, 6 | } 7 | 8 | export class D3Schema implements D3Data { 9 | name: string; 10 | children: D3Table[]; 11 | attributes?: {[key: string]: string}; 12 | 13 | constructor(name: string, children: D3Table[], attributes?: {[key: string]: string}) { 14 | this.name = name, 15 | this.attributes = attributes, 16 | this.children = children 17 | } 18 | } 19 | 20 | export class D3Table implements D3Data { 21 | name: string; 22 | children: D3Column[]; 23 | attributes?: {[key: string]: string}; 24 | 25 | constructor(name: string, children: D3Column[], attributes?: {[key: string]: string}) { 26 | this.name = name, 27 | this.attributes = attributes, 28 | this.children = children 29 | } 30 | } 31 | 32 | export class D3Column implements D3Data { 33 | name: string; 34 | children: D3Data[]; 35 | attributes: { 36 | data_type: string, 37 | constraint?: string 38 | }; 39 | 40 | constructor(name: string, attributes: {data_type: string, constraint?: string}, children: D3Column[] = []) { 41 | this.name = name, 42 | this.attributes = attributes, 43 | this.children = children 44 | } 45 | } 46 | 47 | /* 48 | Used in rowsToTable helper function in getAllTables method found in projectDBController. 49 | Used to return an array of Table objects. 50 | */ 51 | export interface Table { 52 | tablename: string 53 | columns: Column[] // contains columns associated with a given SQL table 54 | } 55 | 56 | //includes relevant columns from the INFORMATION_SCHEMA_COLUMNS in SQL server 57 | export interface Column { 58 | column_id: number, 59 | column_name: string, 60 | data_type: string, 61 | is_nullable: string, 62 | is_updatable: string, 63 | column_default: string | null, 64 | constraint_name: string | null, 65 | constraint_type: string | null, 66 | primary_table: string | null, 67 | primary_column: string | null, 68 | } 69 | 70 | export interface DBQueryResponse extends Column { 71 | schema: string, 72 | tablename: string, 73 | } -------------------------------------------------------------------------------- /__tests__/supertest.js: -------------------------------------------------------------------------------- 1 | const app = require("../../server/server"); // Link to your server file 2 | //let server = 'http://localhost:3000'; 3 | const supertest = require("supertest"); 4 | //const request = supertest(app); 5 | 6 | // sample test 7 | app.get("/test", async (req, res) => { 8 | res.json({ message: "pass!" }); 9 | }); 10 | 11 | 12 | it("gets the test endpoint", async done => { 13 | const response = await request.get("/test"); 14 | 15 | expect(response.status).toBe(200); 16 | expect(response.body.message).toBe("pass!"); 17 | done(); 18 | }); 19 | 20 | describe('Server Tests', () => { 21 | describe('/', () => { 22 | describe('GET - root endpoint', () => { 23 | server = 'http://localhost:3000' 24 | it('responds with 200 status and text/html content type', () => { 25 | return request(server) 26 | .get('/') 27 | .expect('Content-type', /text\/html/) 28 | .expect(200); 29 | }); 30 | }); 31 | }); 32 | 33 | describe("POST /users", () => { 34 | 35 | describe("given a username and password", () => { 36 | // should save a username and password to the DB 37 | 38 | // should respond with a json object containing the user id 39 | 40 | test("should respond with a 200 status code", () => { 41 | const response = request(app).post("/users").send({ 42 | username: "username", 43 | password: "password" 44 | }) 45 | expect(response.statusCode).toBe(200) 46 | }) 47 | // should specify json in the content type header 48 | test("should specify json in the content type header", async () => { 49 | const response = await request(app).post("/users").send({ 50 | username: "username", 51 | password: "password" 52 | }) 53 | expect(response.headers['content-type']).toEqual(expect.stringContaining("json")) 54 | 55 | }) 56 | }) 57 | 58 | describe("when the username and password is missing", () => { 59 | 60 | }) 61 | 62 | }) 63 | }) -------------------------------------------------------------------------------- /dist/js/main.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /* 2 | object-assign 3 | (c) Sindre Sorhus 4 | @license MIT 5 | */ 6 | 7 | /** 8 | * React Router DOM v6.1.1 9 | * 10 | * Copyright (c) Remix Software Inc. 11 | * 12 | * This source code is licensed under the MIT license found in the 13 | * LICENSE.md file in the root directory of this source tree. 14 | * 15 | * @license MIT 16 | */ 17 | 18 | /** 19 | * React Router v6.1.1 20 | * 21 | * Copyright (c) Remix Software Inc. 22 | * 23 | * This source code is licensed under the MIT license found in the 24 | * LICENSE.md file in the root directory of this source tree. 25 | * 26 | * @license MIT 27 | */ 28 | 29 | /** @license MUI v5.2.4 30 | * 31 | * This source code is licensed under the MIT license found in the 32 | * LICENSE file in the root directory of this source tree. 33 | */ 34 | 35 | /** @license React v0.20.2 36 | * scheduler.production.min.js 37 | * 38 | * Copyright (c) Facebook, Inc. and its affiliates. 39 | * 40 | * This source code is licensed under the MIT license found in the 41 | * LICENSE file in the root directory of this source tree. 42 | */ 43 | 44 | /** @license React v16.13.1 45 | * react-is.production.min.js 46 | * 47 | * Copyright (c) Facebook, Inc. and its affiliates. 48 | * 49 | * This source code is licensed under the MIT license found in the 50 | * LICENSE file in the root directory of this source tree. 51 | */ 52 | 53 | /** @license React v17.0.2 54 | * react-dom.production.min.js 55 | * 56 | * Copyright (c) Facebook, Inc. and its affiliates. 57 | * 58 | * This source code is licensed under the MIT license found in the 59 | * LICENSE file in the root directory of this source tree. 60 | */ 61 | 62 | /** @license React v17.0.2 63 | * react-jsx-runtime.production.min.js 64 | * 65 | * Copyright (c) Facebook, Inc. and its affiliates. 66 | * 67 | * This source code is licensed under the MIT license found in the 68 | * LICENSE file in the root directory of this source tree. 69 | */ 70 | 71 | /** @license React v17.0.2 72 | * react.production.min.js 73 | * 74 | * Copyright (c) Facebook, Inc. and its affiliates. 75 | * 76 | * This source code is licensed under the MIT license found in the 77 | * LICENSE file in the root directory of this source tree. 78 | */ 79 | -------------------------------------------------------------------------------- /client/components/Login.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: border-box; 5 | font-family: montserrat, sans-serif; 6 | } 7 | 8 | input, 9 | button { 10 | appearance: none; 11 | background: none; 12 | border: none; 13 | outline: none; 14 | } 15 | 16 | .App { 17 | height: 100vh; 18 | display: flex; 19 | align-items: center; 20 | justify-content: center; 21 | background-color: #53565a; 22 | } 23 | 24 | form { 25 | display: block; 26 | position: relative; 27 | } 28 | 29 | form:after { 30 | content: ""; 31 | display: block; 32 | position: absolute; 33 | top: -5px; 34 | left: -5px; 35 | right: -5px; 36 | bottom: -5px; 37 | z-index: 1; 38 | background-image: linear-gradient(to bottom right, #ffce00, #fe4880); 39 | } 40 | 41 | form .form-inner { 42 | position: relative; 43 | display: block; 44 | background-color: #fff; 45 | padding: 30px; 46 | z-index: 2; 47 | } 48 | 49 | form .form-inner h2 { 50 | color: #888; 51 | font-size: 28px; 52 | font-weight: 500; 53 | margin-bottom: 30px; 54 | } 55 | 56 | form .form-inner .form-group { 57 | display: block; 58 | width: 300px; 59 | margin-bottom: 15px; 60 | } 61 | 62 | .form-inner .form-group label { 63 | display: block; 64 | color: #666; 65 | font-size: 12px; 66 | margin-bottom: 5px; 67 | transition: 0.4s; 68 | } 69 | 70 | .form-inner.form-group focus-within label { 71 | color: #fe4880; 72 | } 73 | 74 | form .form-inner .form-group input { 75 | display: block; 76 | width: 100%; 77 | padding: 10px 15px; 78 | background-color: #f8f8f8; 79 | border-radius: 8px; 80 | transition: 0.4s; 81 | } 82 | 83 | form.form-inner .form-group input focus { 84 | box-shadow: 0px 0px 3px rgba(0, 0, 0, 0.2); 85 | } 86 | 87 | form .form-inner input[type="submit"] { 88 | display: inline-block; 89 | padding: 10px 15px; 90 | border-radius: 8px; 91 | background-image: linear-gradient(to right, #ffce00 50%, #ffce00 50%); 92 | background-size: 200%; 93 | background-position: 0%; 94 | transition: 0.4s; 95 | color: #fff; 96 | font-weight: 700; 97 | cursor: pointer; 98 | } 99 | 100 | form .form-inner input[type="submit"]:hover { 101 | background-position: 100% 0%; 102 | } 103 | -------------------------------------------------------------------------------- /server/SQLConversion/SQLSchemaHelpers.ts: -------------------------------------------------------------------------------- 1 | //helper function to shape response array of Table objects 2 | 3 | import { D3Column, D3Schema, D3Table, DBQueryResponse, Table } from "../types/DBResponseTypes"; 4 | 5 | /***modified queryResult to type any[]***/ 6 | export function rowsToTable(queryResult: DBQueryResponse[]): Table[] { 7 | const tablesObject = queryResult.reduce((tablesObject: { [key: string]: Table }, curr: DBQueryResponse) => { 8 | const { tablename, column_id, column_name, data_type, is_nullable, is_updatable, column_default, constraint_name, constraint_type, primary_table, primary_column } = curr; 9 | //check to see if tablesObject has a tablename property 10 | if (!tablesObject.hasOwnProperty(tablename)) { 11 | //if falsy, create a tablename property and assign it to an object with tablename and columns properties 12 | tablesObject[tablename] = { 13 | tablename: tablename, 14 | columns: [], 15 | } 16 | } 17 | /*if tablename is a property of tablesObject, then bundle the associated columns in an object 18 | and push them into a columns array*/ 19 | tablesObject[tablename].columns.push({ 20 | column_id, 21 | column_name, 22 | data_type, 23 | is_nullable, 24 | is_updatable, 25 | column_default, 26 | constraint_name, 27 | constraint_type, 28 | primary_table, 29 | primary_column 30 | }); 31 | return tablesObject; 32 | }, {}); 33 | return Object.values(tablesObject); 34 | } 35 | 36 | export function rowsToD3(queryResult: DBQueryResponse[]): D3Schema { 37 | let schemaname = ''; 38 | const tablesObject = queryResult.reduce((tablesObject: { [key: string]: D3Table }, curr: DBQueryResponse) => { 39 | const { schema, tablename, column_name, data_type, constraint_type } = curr; 40 | schemaname = schema; 41 | //check to see if tablesObject has a tablename property 42 | if (!tablesObject.hasOwnProperty(tablename)) { 43 | tablesObject[tablename] = new D3Table(tablename, []); 44 | } 45 | const attribute: { data_type: string, constraint?: string } = constraint_type ? { data_type, constraint: constraint_type } : { data_type } 46 | tablesObject[tablename].children.push(new D3Column(column_name, attribute)); 47 | return tablesObject; 48 | }, {}); 49 | 50 | return new D3Schema(schemaname, Object.values(tablesObject)); 51 | } -------------------------------------------------------------------------------- /server/SQLConversion/SQLConversionHelpers.ts: -------------------------------------------------------------------------------- 1 | const pluralize = require('pluralize'); 2 | const { singular } = pluralize; 3 | import { Table, Column } from '../types/DBResponseTypes'; 4 | 5 | 6 | function checkIsNullable(isNullable: string): string { 7 | return isNullable === "NO" ? '!' : ''; 8 | } 9 | 10 | function inPascalCase(tablename: string): string { 11 | const regex = /(^|_)./g; 12 | return tablename.replace(regex, (str: string) => str.slice(-1).toUpperCase()); 13 | } 14 | 15 | export const SQLConversionHelpers = { 16 | //given a column object, returns a supported GrapqhQL datatype with field nullability 17 | checkIsTableJoin(columnsArr: Column[]): boolean { 18 | let foreignKeyCount = 0; 19 | columnsArr.forEach((columnsObj: Column) => { 20 | const { constraint_type } = columnsObj; 21 | if (constraint_type === 'FOREIGN KEY') foreignKeyCount++; 22 | }) 23 | return foreignKeyCount === columnsArr.length - 1 ? true : false; 24 | }, 25 | 26 | fieldValueCreator(columnObject: Column): string { 27 | const { data_type, is_nullable, constraint_type } = columnObject; 28 | if (constraint_type === 'PRIMARY KEY' || constraint_type === 'FOREIGN KEY') return 'ID' + checkIsNullable(is_nullable); // CHECK IF IT's KEY OR NOT 29 | const dataTypeConversion: { [key: string]: string } = { 30 | 'bigint': 'Int', 31 | 'boolean': 'Boolean', 32 | 'character': 'String', 33 | 'character varying': 'String', 34 | 'date': 'String', 35 | 'integer': 'Int', 36 | 'numeric': 'Int', 37 | 'json': 'JSON', 38 | 'smallint': 'Int', 39 | 'timestamp with time zone': 'String', 40 | 'timestamp without time zone': 'String' 41 | } 42 | //if SQL datatype not included in dataTypeConversion object, return undefined 43 | const gqlDataType = dataTypeConversion[data_type]; 44 | //check for nullability only if gqlDataType is defined 45 | return gqlDataType + checkIsNullable(is_nullable); 46 | }, 47 | //given a table name, converts to Pascal case as per Type names naming convention 48 | inObjectTypeCase(tablename: string): string { 49 | const pascalizedName = inPascalCase(tablename); 50 | return singular(pascalizedName); 51 | }, 52 | queryPluralCase(tablename: string): string { 53 | let pascalizedName = inPascalCase(tablename); 54 | return pascalizedName[0].toLowerCase() + pascalizedName.substring(1); 55 | }, 56 | querySingularCase(tablename: string): string { 57 | let pascalizedName = inPascalCase(tablename); 58 | let sing: string = singular(pascalizedName[0].toLowerCase() + pascalizedName.substring(1)); 59 | return sing.endsWith('s') ? sing.substring(0, sing.length - 1) : sing; 60 | } 61 | } -------------------------------------------------------------------------------- /src/components/StaticPages/LandingPageContainer.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import NavBarLandingPage from './NavBarLandingPage'; 3 | import {Link, Navigate} from 'react-router-dom'; 4 | import { WebLoginForm } from '../formComponents/WebLoginForm' 5 | import { WebRegisterForm } from '../formComponents/WebRegisterForm' 6 | import MariposaLogo from '../../assets/MariposaLogo.svg'; 7 | 8 | 9 | 10 | function LandingPage() { 11 | const[hideNavBar, setHideNavBar] = useState(false); 12 | const[changeToFormDisplay, setChangeToFormDisplay] = useState(false); 13 | const[formToDisplay, setFormToDisplay] = useState('login'); 14 | 15 | const handleStartNow = () => { 16 | setChangeToFormDisplay(true); 17 | setHideNavBar(true); 18 | } 19 | 20 | const handleBackButton = () => { 21 | setChangeToFormDisplay(false); 22 | setFormToDisplay('login'); 23 | setHideNavBar(false); 24 | } 25 | function guestLogin() { 26 | fetch('/mariposa/auth/signin', { 27 | method: 'POST', 28 | body: JSON.stringify({ username: 'guest', password: 'password' }), 29 | }).then((response) => { 30 | if (response.status === 200) { 31 | console.log('good') 32 | setGuest(true); 33 | } 34 | }); 35 | } 36 | console.log(formToDisplay) 37 | const[guest, setGuest] = useState(false); 38 | 39 | return ( 40 |
41 | { 42 | !hideNavBar ?
: 43 |
44 | 45 | 49 | 50 |
51 | } 52 | 53 |
54 | 55 |
56 | { 57 | !changeToFormDisplay && ( 58 |
59 |

A Restful API to GraphQL Migration Tool

60 |
61 | 64 | 67 |
68 |
69 | )} 70 | 71 | {((changeToFormDisplay && 72 | formToDisplay === 'login') && ()) || 73 | ((changeToFormDisplay && 74 | formToDisplay === 'register') && ())} 75 | {guest && } 76 |
77 | ); 78 | } 79 | 80 | export default LandingPage; 81 | -------------------------------------------------------------------------------- /src/components/MainPage.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import Grid from '@mui/material/Grid'; 3 | import Graph from './Graph'; 4 | import ResponsiveAppBar from './MainPageNavBar'; 5 | import Resolvers from './Resolvers'; 6 | import Button from '@mui/material/Button'; 7 | import TextField from '@mui/material/TextField'; 8 | 9 | export default function LandingPage(props: any) { 10 | const [uriBtnClose, setHandleUriBtnClose] = useState(false); 11 | const [uriString, setUriString] = useState(''); 12 | const [treeData, setTreeData] = useState({ 13 | name: '', 14 | children: [], 15 | }) 16 | 17 | const [text, setText] = useState(''); 18 | const [schema, setSchema] = useState(''); 19 | 20 | function obtainTreeData(uriString: string) { 21 | fetch('/project/D3tables', { 22 | method: 'POST', 23 | headers: { 24 | 'Content-Type': 'application/json' 25 | }, 26 | body: JSON.stringify({ uri: uriString }) 27 | }) 28 | .then(res => res.json()) 29 | .then(data => { 30 | setTreeData(data) 31 | }) 32 | } 33 | 34 | function obtainResolvers(uriString: string) { 35 | fetch('/project/tables', { 36 | method: 'POST', 37 | headers: { 38 | 'Content-Type': 'application/json' 39 | }, 40 | body: JSON.stringify({ uri: uriString }) 41 | }) 42 | .then(res => res.json()) 43 | .then(data => { 44 | setText(data.typeDefs); 45 | setSchema(data.resolverString); 46 | }) 47 | } 48 | 49 | const handleClick = (uriString: string, e: any) => { 50 | e.preventDefault() 51 | setHandleUriBtnClose(true); //closes uri box 52 | setUriString(''); //cleans uri bar 53 | obtainResolvers(uriString); 54 | obtainTreeData(uriString); 55 | 56 | } 57 | 58 | return ( 59 | 60 |
61 | 62 | 63 | {!uriBtnClose &&
64 | 65 | setUriString(e.target.value)}/> 66 |
67 | 68 | 69 |
70 |
71 | } 72 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 |
87 | ) 88 | } -------------------------------------------------------------------------------- /src/slices/authentication.js: -------------------------------------------------------------------------------- 1 | //standalone runtime for Regenerator-compiled generator and async functions (DO NOT DELETE) 2 | import regeneratorRuntime from "regenerator-runtime"; 3 | //createAsyncThunk returns a standard Redux thunk action creator 4 | import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; 5 | //use authService to make async requests - authService.register, login, logout 6 | import { authService } from '../services/auth_service'; 7 | 8 | // setMessage dispatched if authentication is successful or fails 9 | import { setMessage } from './messages'; 10 | //create async thunks 11 | 12 | const user = JSON.parse(localStorage.getItem("user")); 13 | 14 | export const register = createAsyncThunk( 15 | 'auth/register', //action type 16 | async ({ //payloadCreator callback that returns a promise 17 | firstname, 18 | lastname, 19 | username, 20 | email, 21 | password 22 | }, 23 | thunkAPI //object that contains all parameters normally passed to a Redux thunk function 24 | ) => { 25 | try { 26 | const response = await authService.register(firstname, lastname, username, email, password); 27 | 28 | thunkAPI.dispatch(setMessage(response.data.message)); 29 | return response.data; //contains our response from server 30 | } catch (error) { 31 | const message = 32 | (error.response && 33 | error.response.data && 34 | error.response.data.message) || 35 | error.message || 36 | error.toString(); 37 | thunkAPI.dispatch(setMessage(message)); 38 | return thunkAPI.rejectWithValue(); 39 | } 40 | } 41 | ); 42 | 43 | export const login = createAsyncThunk( 44 | "auth/login", 45 | async ({ username, password}, thunkAPI) => { 46 | try { 47 | const data = await authService.login(username, password); 48 | return { user: data }; 49 | } catch (error) { 50 | const message = 51 | (error.response && 52 | error.response.data && 53 | error.response.data.message) || 54 | error.message || 55 | error.toString(); 56 | thunkAPI.dispatch(setMessage(message)); 57 | return thunkAPI.rejectWithValue(); 58 | } 59 | } 60 | ); 61 | 62 | export const logout = createAsyncThunk( 63 | "auth/logout", 64 | async () => { 65 | await authService.logout(); 66 | }); 67 | 68 | //set initial state here 69 | const initialState = user 70 | ? { isLoggedIn: true, user } 71 | : { isLoggedIn: false, user: null }; 72 | 73 | const authSlice = createSlice({ 74 | name: "auth", 75 | initialState, 76 | extraReducers: { //extraReducers allows createSlice to respond to other action types besides the types it has generated 77 | [register.fulfilled]: (state, action) => { 78 | state.isLoggedIn = false; 79 | }, 80 | [register.rejected]: (state, action) => { 81 | state.isLoggedIn = false; 82 | }, 83 | [login.fulfilled]: (state, action) => { 84 | state.isLoggedIn = true; 85 | state.user = action.payload.user; 86 | }, 87 | [login.rejected]: (state, action) => { 88 | state.isLoggedIn = false; 89 | state.user = null; 90 | }, 91 | [logout.fulfilled]: (state, action) => { 92 | state.isLoggedIn = false; 93 | state.user = null; 94 | }, 95 | }, 96 | }); 97 | 98 | const { reducer } = authSlice; 99 | export default reducer; -------------------------------------------------------------------------------- /server/controllers/projectDBController.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | import { Table } from '../types/DBResponseTypes'; 3 | import rowsToTable from '../SQLConversion/SQLQueryHelpers'; 4 | import { IResolvers } from '@graphql-tools/utils'; 5 | import resolverMaker from '../SQLConversion/resolverMaker'; 6 | import db, { PG_URI } from '../models/projectDB'; 7 | import { resolverStringMaker } from '../SQLConversion/resolverStringMaker'; 8 | import { typeDefMaker } from '../SQLConversion/typeDefMaker'; 9 | 10 | export const projectDBController = { 11 | async updateDatabase(req: Request, res: Response, next: NextFunction) { 12 | const { uri } = req.body; 13 | if (uri) db.updateUri(uri); 14 | else db.updateUri(PG_URI); 15 | next(); 16 | }, 17 | 18 | async getAllTables(req: Request, res: Response, next: NextFunction) { 19 | try { 20 | const query = ` 21 | SELECT col.table_schema AS schema, 22 | col.table_name AS tablename, 23 | col.ordinal_position AS column_id, 24 | col.column_name, 25 | col.data_type, 26 | col.is_nullable, 27 | col.is_updatable, 28 | col.column_default, 29 | kcu.constraint_name, 30 | tc.constraint_type, 31 | kcu2.table_name as primary_table, 32 | kcu2.column_name as primary_column 33 | FROM information_schema.columns col 34 | JOIN pg_catalog.pg_tables pg_ 35 | ON col.table_name = pg_.tablename 36 | LEFT JOIN information_schema.key_column_usage kcu 37 | ON col.table_name = kcu.table_name 38 | AND col.column_name = kcu.column_name 39 | LEFT JOIN information_schema.table_constraints tc 40 | ON kcu.constraint_name = tc.constraint_name 41 | LEFT JOIN information_schema.referential_constraints rc 42 | ON kcu.constraint_name = rc.constraint_name 43 | LEFT JOIN information_schema.key_column_usage kcu2 44 | ON kcu2.constraint_name = rc.unique_constraint_name 45 | 46 | WHERE pg_.schemaname != 'pg_catalog' 47 | AND pg_.schemaname != 'information_schema' 48 | ORDER BY col.table_schema, 49 | col.table_name, 50 | column_id; 51 | `; 52 | const queryResult = await db.query(query); 53 | res.locals.userDbResponse = queryResult.rows; 54 | return next(); 55 | } 56 | catch (err) { 57 | return next({ 58 | log: `Express error handler caught error in the getAllTables controller, ${err}`, 59 | message: { err: 'An error occurred in the getAllTables controller' } 60 | }); 61 | } 62 | }, 63 | buildTypeDefs(req: Request, res: Response, next: NextFunction) { 64 | const arrayOfTableObjects = rowsToTable(res.locals.userDbResponse); 65 | res.locals.tablesArray = arrayOfTableObjects; 66 | res.locals.typeDefs = typeDefMaker.generateTypes(arrayOfTableObjects); 67 | return next(); 68 | }, 69 | buildResolvers(req: Request, res: Response, next: NextFunction) { 70 | const arrayOfTableObjects: Table[] = res.locals.tablesArray; 71 | const db = res.locals.db; 72 | const resolvers: IResolvers = resolverMaker.generateResolvers(arrayOfTableObjects, db); 73 | const resolverString: string = resolverStringMaker.generateResolverString(arrayOfTableObjects); 74 | res.locals.resolvers = resolvers; 75 | res.locals.resolverString = resolverString; 76 | return next(); 77 | }, 78 | } 79 | -------------------------------------------------------------------------------- /src/styles/styles.scss: -------------------------------------------------------------------------------- 1 | @use "LandingPage"; 2 | @use "StaticPages"; 3 | @import './colorsAndFonts.scss'; 4 | 5 | 6 | body { 7 | background: linear-gradient(to right, rgba(0, 0, 0, 0.824), rgba(0, 0, 0, 0.598)); 8 | margin: 10%; 9 | font-family: $fontOfBody; 10 | 11 | } 12 | 13 | .graphD3 { 14 | height: 100%; 15 | width: 100%; 16 | } 17 | 18 | #container { 19 | width: 100%; 20 | height: 100%; 21 | } 22 | .resolverText { 23 | margin-top: 4%; 24 | } 25 | 26 | .resolvers { 27 | height: 100%; 28 | width: 100%; 29 | border-radius: 5px; 30 | box-shadow: pink; 31 | } 32 | 33 | .hljs{ 34 | background: transparent; 35 | } 36 | 37 | * { 38 | margin: 0; 39 | padding: 0; 40 | //box-sizing: border-box; 41 | // font-family: montserrat, sans-serif; 42 | } 43 | 44 | input, 45 | button { 46 | appearance: none; 47 | background: none; 48 | border: none; 49 | outline: none; 50 | } 51 | 52 | .loginApp { 53 | height: 100vh; 54 | display: flex; 55 | align-items: center; 56 | justify-content: center; 57 | background-color: #53565a; 58 | } 59 | 60 | #login-logo{ 61 | width: 70%; 62 | padding-top: 20px; 63 | margin: auto; 64 | display: block; 65 | } 66 | 67 | .button-box{ 68 | width: 220px; 69 | margin: 35px auto; 70 | position: relative; 71 | box-shadow: 0 0 20px 9px rgb(248, 218, 223); 72 | border-radius: 30px; 73 | } 74 | 75 | .toggle-btn{ 76 | padding: 10px 30px; 77 | cursor: pointer; 78 | background: transparent; 79 | border: 0; 80 | outline: none; 81 | position: relative; 82 | } 83 | 84 | #btn{ 85 | top: 0; 86 | left: 0; 87 | position: absolute; 88 | width: 110px; 89 | height: 100%; 90 | background: #df29df; 91 | border-radius: 30px; 92 | transition: .5s; 93 | } 94 | 95 | .input-group{ 96 | top: 180; 97 | left: 50px; 98 | position: absolute; 99 | width: 280px; 100 | transition: .5s; 101 | } 102 | 103 | .input-field{ 104 | width: 100%; 105 | padding: 10px 0; 106 | margin: 5px 0; 107 | border-left: 0; 108 | border-top: 0; 109 | border-right: 0; 110 | border-bottom: 1px solid #999; 111 | outline: none; 112 | background: transparent; 113 | } 114 | 115 | .submit-btn{ 116 | width: 85%; 117 | padding: 10px 30px; 118 | cursor: pointer; 119 | display: block; 120 | margin: auto; 121 | background: #df29df; 122 | border: 0; 123 | outline: none; 124 | border-radius: 30px; 125 | color: white 126 | } 127 | 128 | .check-box{ 129 | margin: 30px 10px 30px 0; 130 | } 131 | 132 | 133 | button { 134 | background-color: white; 135 | } 136 | 137 | .node__root > circle { 138 | fill: #1976d2; 139 | } 140 | 141 | .node__branch > circle { 142 | fill: #1976d2; 143 | } 144 | 145 | .node__leaf > circle { 146 | fill: #1976d2; 147 | } 148 | 149 | 150 | .navAppBar{ 151 | background: linear-gradient(to right, rgba(0, 0, 0, 0.824), rgba(160, 6, 160, 0.598)); 152 | border-radius: 5px; 153 | } 154 | 155 | .enterUri{ 156 | h2 { 157 | font-size: 29px; 158 | font-weight: lighter; 159 | display: inline-block; 160 | font-family:'Open Sans', sans-serif; 161 | margin:0; 162 | margin-right: 42px; 163 | } 164 | button { 165 | margin:0; 166 | font-size: 2vh; 167 | margin-top: 1%; 168 | margin-left: 45%; 169 | width: 10%; 170 | display: inline-block; 171 | vertical-align:left; 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mariposa", 3 | "version": "1.0.0", 4 | "description": "a graphql migration and visualization tool", 5 | "main": "./dist/main.js", 6 | "scripts": { 7 | "test": "jest", 8 | "dev": "NODE_ENV=production webpack-dev-server --config webpack.config.js --mode development", 9 | "build": "NODE_ENV=production webpack --config ./webpack.config.js", 10 | "dev:server": "NODE_ENV=development nodemon server/server.ts", 11 | "prod:server": "NODE_ENV=production ts-node server/server.ts", 12 | "start": "NODE_ENV=production npm run build && npm run prod:server" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/oslabs-beta/mariposa.git" 17 | }, 18 | "author": "Adam Berri, Alex Barbazan, James Maguire, Mark Dolan", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/oslabs-beta/mariposa/issues" 22 | }, 23 | "homepage": "https://github.com/oslabs-beta/mariposa#readme", 24 | "devDependencies": { 25 | "@babel/plugin-transform-runtime": "^7.16.4", 26 | "@types/express": "^4.17.13", 27 | "@types/pg": "^8.6.1", 28 | "@types/react": "^17.0.36", 29 | "@types/react-dom": "^17.0.11", 30 | "@types/react-highlight": "^0.12.5", 31 | "@types/react-resizable": "^1.7.4", 32 | "electron": "^16.0.1", 33 | "graphql-tools": "^8.2.0", 34 | "html-webpack-plugin": "^5.5.0", 35 | "pg": "^8.7.1", 36 | "react": "^17.0.2", 37 | "react-dom": "^17.0.2", 38 | "resize-observer-polyfill": "^1.5.1", 39 | "style-loader": "^3.3.1", 40 | "ts-loader": "^9.2.6", 41 | "typescript": "^4.5.2", 42 | "typings-for-css-modules-loader": "^1.7.0", 43 | "webpack": "^5.64.2", 44 | "webpack-cli": "^4.9.1", 45 | "webpack-dev-server": "^4.5.0", 46 | "webpack-electron-reload": "^1.0.1" 47 | }, 48 | "dependencies": { 49 | "@babel/core": "^7.16.0", 50 | "@babel/preset-env": "^7.16.4", 51 | "@babel/preset-react": "^7.16.0", 52 | "@babel/preset-typescript": "^7.16.0", 53 | "@devbookhq/splitter": "^1.2.4", 54 | "@emotion/react": "^11.7.0", 55 | "@emotion/styled": "^11.6.0", 56 | "@graphql-tools/schema": "^8.3.1", 57 | "@graphql-tools/utils": "^8.5.3", 58 | "@mui/icons-material": "^5.2.0", 59 | "@mui/material": "^5.2.1", 60 | "@mui/styled-engine-sc": "^5.1.0", 61 | "@reduxjs/toolkit": "^1.6.2", 62 | "axios": "^0.24.0", 63 | "babel-loader": "^8.2.3", 64 | "bootstrap": "^5.1.3", 65 | "css-loader": "^6.5.1", 66 | "css-modules-typescript-loader": "^4.0.1", 67 | "d3": "^7.1.1", 68 | "electron-reload": "^2.0.0-alpha.1", 69 | "express": "^4.17.1", 70 | "express-graphql": "^0.12.0", 71 | "file-loader": "^6.2.0", 72 | "formik": "^2.2.9", 73 | "graphql": "^15.7.2", 74 | "highlight.js": "^11.3.1", 75 | "jsonwebtoken": "^8.5.1", 76 | "nodemon": "^2.0.15", 77 | "pluralize": "^8.0.0", 78 | "react-d3": "^0.4.0", 79 | "react-d3-tree": "^3.1.1", 80 | "react-highlight": "^0.14.0", 81 | "react-json-pretty": "^2.2.0", 82 | "react-redux": "^7.2.6", 83 | "react-resizable": "^3.0.4", 84 | "react-router": "^6.1.0", 85 | "react-router-dom": "^6.0.2", 86 | "react-spring": "^9.3.2", 87 | "redux-starter-kit": "^2.0.0", 88 | "sass": "^1.43.4", 89 | "sass-loader": "^12.4.0", 90 | "start-server-webpack-plugin": "^2.2.5", 91 | "styled-components": "^5.3.3", 92 | "svg-url-loader": "^7.1.1", 93 | "ts-node": "^10.4.0", 94 | "webpack-node-externals": "^3.0.0", 95 | "yup": "^0.32.11" 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/components/StaticPages/AboutUs.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Link} from 'react-router-dom'; 3 | 4 | import Linkedin from '../../assets/LinkedIn.png'; 5 | import Github from '../../assets/Github.png' 6 | import Adam from '../../assets/Profile_Adam.png' 7 | import Alex from '../../assets/Profile_Alex.png' 8 | import James from '../../assets/Profile_James.png' 9 | import Mark from '../../assets/Profile_Mark.png' 10 | 11 | function AboutUs() { 12 | 13 | return ( 14 |
15 | 16 | 20 |

Meet the Mariposa Team

21 |
22 |
23 | 24 |

Adam Berri

25 |
26 | 33 | 39 |
40 |
41 | 42 |
43 | 44 |

Alex Barbazan

45 |
46 | 53 | 59 |
60 |
61 | 62 |
63 | 64 |

James Maguire

65 |
66 | 73 | 79 |
80 |
81 | 82 |
83 | 84 |

Mark Dolan

85 |
86 | 93 | 99 |
100 |
101 | {/*theTeam*/} 102 |
103 |

Proud to be part of the Open Source Community

104 |

105 | As software developers who have been deeply inspired by other Open Source projects, 106 | we are extremely excited to contribute back to the Community with Mariposa. We hope 107 | this product will serve developers well and look forward to expanding its potential. 108 | Special thanks to OS Labs for giving us this opportunity. 109 |

110 | 111 | {/*aboutUsWrapper*/} 112 |
113 | ); 114 | } 115 | export default AboutUs; 116 | -------------------------------------------------------------------------------- /src/components/formComponents/WebLoginForm.js: -------------------------------------------------------------------------------- 1 | import React, {useState, useEffect} from "react"; 2 | import {useDispatch, useSelector} from "react-redux"; 3 | import {Link, Navigate} from 'react-router-dom'; 4 | import MariposaLogo from '../../assets/MariposaLogo.png'; 5 | // import styles from '../../styles/LandingPage.module.scss' 6 | 7 | // import formik and yup libraries for form validation 8 | import {Formik, Field, Form, ErrorMessage} from "formik"; 9 | import * as Yup from "yup"; 10 | 11 | // import async thunk - login 12 | import {login} from "../../slices/authentication"; 13 | // import reducer - clearMessage 14 | import {clearMessage} from "../../slices/messages"; 15 | 16 | export const WebLoginForm = (props) => { // disable login submit button if loading 17 | const {setFormToDisplay} = props; 18 | const [loading, setLoading] = useState(false); 19 | const [redirect, setRedirect] = useState(false); 20 | 21 | 22 | // access pieces of state from store 23 | const {isLoggedIn} = useSelector((state) => state.auth); 24 | const {message} = useSelector((state) => state.auth); 25 | 26 | // dispatch method to dispatch an action and trigger a state change 27 | const dispatch = useDispatch(); 28 | 29 | // if dispatch has been invoked, we want to run the clearMessage reducer 30 | useEffect(() => { 31 | dispatch(clearMessage()); 32 | }, [dispatch]); 33 | 34 | const initialValues = { 35 | username: "", 36 | password: "" 37 | }; 38 | 39 | const validationSchema = Yup.object().shape({ 40 | username: Yup.string().required("This field is required!"), 41 | password: Yup.string().required("This field is required!"), 42 | }); 43 | 44 | const handleLogin = (formValue) => { // take in user's provided username and password 45 | const {username, password} = formValue; 46 | // disable login button by set loading to true 47 | setLoading(true); 48 | dispatch(login({username, password})).unwrap().then(() => { 49 | setRedirect(true); 50 | }).catch(() => { // if login fails, enable login button again 51 | setLoading(false); 52 | }); 53 | }; 54 | 55 | if (isLoggedIn) { 56 | 57 | } 58 | 59 | const handleFormDisplay = () => setFormToDisplay('register'); 60 | 61 | 62 | 63 | return ( 64 |
65 | 66 | 71 |
72 |

73 | Sign In 74 |

75 |
76 | 77 | 83 | 88 |
89 | 90 |
91 | 92 | 98 | 103 |
104 | 105 | 114 | 115 |
116 |
117 |

118 | Don't have account? 119 |

120 | 121 | { 122 | message && ( 123 |
124 |
125 | {message}
126 |
127 | ) 128 | } 129 | 130 | {redirect && } 131 |
132 | ); 133 | }; 134 | -------------------------------------------------------------------------------- /server/SQLConversion/resolverMaker.ts: -------------------------------------------------------------------------------- 1 | import { IResolvers } from "@graphql-tools/utils"; 2 | import { PoolWrapper } from "../types/PoolWrapper"; 3 | import { Table } from "../types/DBResponseTypes"; 4 | import { SQLConversionHelpers } from './SQLConversionHelpers' 5 | const {checkIsTableJoin, inObjectTypeCase, queryPluralCase, querySingularCase} = SQLConversionHelpers; 6 | 7 | const resolverMaker = { 8 | generateResolvers(tables: Table[], db: PoolWrapper): IResolvers { 9 | const resolver = tables.reduce((acc: IResolvers, curr: Table) => { 10 | const { tablename, columns } = curr; 11 | if(!checkIsTableJoin(columns)) { 12 | acc["Query"] = makeQueryResolver(acc["Query"], curr, db); 13 | acc["Mutation"] = makeMutationResolver(acc["Mutation"], curr, db); 14 | 15 | const tab = inObjectTypeCase(tablename); 16 | if (!acc.hasOwnProperty(tab)) { 17 | acc[tab] = {}; 18 | } 19 | // should have something for all foreign keys 20 | for (let i = 0; i < columns.length; i++) { 21 | const { constraint_type, column_name, primary_table, primary_column } = columns[i]; 22 | if(constraint_type === 'FOREIGN KEY' && primary_table && primary_table && primary_column) { 23 | acc[tab] = makeTypeResolver(acc[tab], column_name, primary_table, primary_column, db); 24 | } 25 | } 26 | } 27 | 28 | return acc; 29 | }, { 30 | Query: {}, 31 | Mutation: {}, 32 | }); 33 | 34 | 35 | return resolver; 36 | } 37 | } 38 | 39 | function makeQueryResolver(queryObj: { [key: string]: any }, table: Table, db: PoolWrapper): object { 40 | const { tablename, columns } = table; 41 | 42 | queryObj[queryPluralCase(tablename)] = async () => { // people, films, planets, etc. 43 | try { 44 | const query = `SELECT * FROM ${tablename}`; 45 | const result = await db.query(query); 46 | return result.rows; 47 | } catch (err) { 48 | console.log(err) 49 | /* INSERT YOUR ERROR HANDLING HERE */ 50 | } 51 | } 52 | 53 | for (let i = 0; i < columns.length; i++) { 54 | const { constraint_type, column_name } = columns[i]; 55 | if (constraint_type === "PRIMARY KEY") { 56 | // person, film, planet, etc. 57 | queryObj[querySingularCase(tablename)] = async (parent: any, args: { [key: string]: any }) => { 58 | console.log('Arguments', args); 59 | try { 60 | const query = `SELECT * FROM ${tablename} WHERE ${column_name} = $1`; 61 | const result = await db.query(query, [args[column_name].toString()]); 62 | return result.rows[0]; 63 | } catch (err) { 64 | console.log(err) 65 | /* INSERT YOUR ERROR HANDLING HERE */ 66 | } 67 | } 68 | break; 69 | } 70 | } 71 | 72 | return queryObj; 73 | } 74 | 75 | function makeMutationResolver(mutationObj: { [key: string]: any }, table: Table, db: PoolWrapper): object { 76 | 77 | const { tablename, columns } = table; 78 | 79 | mutationObj[`add${inObjectTypeCase(tablename)}`] = async (parent: any, args: { [key: string]: any }) => { 80 | console.log('Arguments', args); 81 | 82 | try { 83 | const col_array: string[] = []; 84 | const val_array: string[] = []; 85 | const param_array: string[] = []; 86 | for (const [key, value] of Object.entries(args)) { 87 | col_array.push(key); 88 | param_array.push(`$${col_array.length}`) 89 | val_array.push(value.toString()) 90 | }; 91 | const query = `INSERT INTO ${tablename}(${col_array.join()}) VALUES (${param_array.join()}) RETURNING *`; 92 | const result = await db.query(query, val_array); 93 | return result.rows[0]; 94 | } catch (err) { 95 | console.log(err) 96 | /* INSERT YOUR ERROR HANDLING HERE */ 97 | } 98 | } 99 | 100 | for (let i = 0; i < columns.length; i++) { 101 | const { constraint_type, column_name } = columns[i]; 102 | if (constraint_type === "PRIMARY KEY") { 103 | 104 | mutationObj[`update${inObjectTypeCase(tablename)}`] = async (parent: any, args: { [key: string]: any }) => { 105 | console.log('Arguments', args); 106 | 107 | try { 108 | const col_array: string[] = []; 109 | const val_array: string[] = []; 110 | for (const [key, value] of Object.entries(args)) { 111 | if (key !== column_name) { 112 | col_array.push(`${key} = $${col_array.length + 1}`); 113 | val_array.push(value.toString()) 114 | } 115 | }; 116 | // ['name = $1', 'mass = $2'] 117 | 118 | val_array.push(args[column_name]); 119 | const query = `UPDATE ${tablename} SET ${col_array.join()} WHERE ${column_name} = $${col_array.length + 1} RETURNING *`; 120 | const result = await db.query(query, val_array); 121 | return result.rows[0]; 122 | } catch (err) { 123 | console.log(err) 124 | /* INSERT YOUR ERROR HANDLING HERE */ 125 | } 126 | } 127 | } 128 | } 129 | return mutationObj; 130 | } 131 | 132 | function makeTypeResolver(typeObj: {[key: string]: any}, column_name: string, primary_table: string, primary_column: string, db: PoolWrapper): object { 133 | typeObj[primary_table] = async (parent: any) => { 134 | try { 135 | const query = `SELECT * FROM ${primary_table} WHERE ${primary_column} = $1`; 136 | const result = await db.query(query, [parent[column_name]]); 137 | return result.rows[0]; 138 | } catch(err) { 139 | console.log(err) 140 | /* INSERT ERROR HANDLING HERE */ 141 | } 142 | } 143 | return typeObj; 144 | } 145 | 146 | export default resolverMaker; -------------------------------------------------------------------------------- /src/styles/_LandingPage.scss: -------------------------------------------------------------------------------- 1 | @import './colorsAndFonts.scss';:root { 2 | --clr-neon: hsl(317 100% 54%); 3 | --clr-bg: hsl(323 21% 16%); 4 | } 5 | 6 | *, 7 | *::before, 8 | *::after { 9 | box-sizing: border-box; 10 | } 11 | 12 | .navWrapper { 13 | grid-area: nav; 14 | align-items: flex-end; 15 | 16 | .navBar { 17 | display: flex; 18 | justify-content: flex-end; 19 | gap: 2vh; 20 | } 21 | } 22 | 23 | .leftWrapper { 24 | grid-area: left; 25 | #logo { 26 | filter: drop-shadow(40px 40px 5px rgb(0 0 0 / 0.4)); 27 | } 28 | } 29 | .rightWrapper { 30 | grid-area: right; 31 | display: flex; 32 | flex-direction: column; 33 | justify-content: center; 34 | align-items: center; 35 | h2 { 36 | text-align: center; 37 | font-size: 3.5vh; 38 | color: white; 39 | padding: 6vh; 40 | } 41 | .buttonDiv { 42 | display: flex; 43 | gap: 2vh; 44 | } 45 | } 46 | 47 | .grid-container{ 48 | display: grid; 49 | grid-template-areas: 50 | 'nav nav nav nav' 51 | 'left left right right' 52 | 'left left right right'; 53 | grid-gap: 5vh; 54 | } 55 | 56 | .form-box { 57 | display: flex; 58 | flex-direction: column; 59 | align-items: center; 60 | width: 25em; 61 | min-height: 1em; 62 | position: relative; 63 | margin: auto; 64 | background: rgba(255, 255, 255, 0.05); 65 | padding: 5px; 66 | border-radius: 10px; 67 | border: solid 2px #df29df; 68 | box-shadow: 1px 3px 10px #2583b6; 69 | color: rgb(230, 225, 225); 70 | font-size: 2.5vh; 71 | // -webkit-text-stroke: 0.1vh #2583b6; 72 | 73 | Form { 74 | display: flex; 75 | flex-direction: column; 76 | justify-content: center; 77 | align-items: center; 78 | width: 100%; 79 | } 80 | 81 | #sign-in-text{ 82 | text-align: center; 83 | // -webkit-text-stroke: 0.2vh #2583b6; 84 | } 85 | 86 | .form-group { 87 | margin-top: 3vh; 88 | margin-bottom: 3vh; 89 | width: 80%; 90 | } 91 | 92 | .form-control{ 93 | background: transparent; 94 | color:hsl(317 100% 54%) 95 | } 96 | 97 | #loginBtn{ 98 | width: 40%; 99 | 100 | font-size: 1rem; 101 | 102 | display: inline-block; 103 | cursor: pointer; 104 | text-decoration: none; 105 | text-align: center; 106 | color: var(--clr-neon); 107 | border: var(--clr-neon) 0.125em solid; 108 | padding: 0.25em 1em; 109 | border-radius: 0.25em; 110 | background: transparent; 111 | 112 | text-shadow: 0 0 0.125em hsl(0 0% 100% / 0.3), 0 0 0.45em currentColor; 113 | 114 | box-shadow: inset 0 0 0.5em 0 var(--clr-neon), 0 0 0.5em 0 var(--clr-neon); 115 | 116 | position: relative; 117 | 118 | } 119 | 120 | #loginBtn::after { 121 | content: ""; 122 | position: absolute; 123 | box-shadow: 0 0 2em 0.5em var(--clr-neon); 124 | opacity: 0; 125 | background-color: var(--clr-neon); 126 | z-index: -1; 127 | transition: opacity 100ms linear; 128 | } 129 | 130 | #loginBtn:hover, 131 | #loginBtn:focus { 132 | color: white; 133 | text-shadow: none; 134 | background: rgb(55, 7, 101) 135 | } 136 | 137 | #loginBtn:hover::before, 138 | #loginBtn:focus::before { 139 | opacity: 1; 140 | } 141 | #loginBtn:hover::after, 142 | #loginBtn:focus::after { 143 | opacity: 1; 144 | } 145 | } 146 | 147 | .neon-button { 148 | font-size: 1rem; 149 | 150 | display: inline-block; 151 | cursor: pointer; 152 | text-decoration: none; 153 | text-align: center; 154 | color: var(--clr-neon); 155 | border: var(--clr-neon) 0.125em solid; 156 | padding: 0.25em 1em; 157 | border-radius: 0.25em; 158 | background: transparent; 159 | 160 | text-shadow: 0 0 0.125em hsl(0 0% 100% / 0.3), 0 0 0.45em currentColor; 161 | 162 | box-shadow: inset 0 0 0.5em 0 var(--clr-neon), 0 0 0.5em 0 var(--clr-neon); 163 | 164 | position: relative; 165 | 166 | } 167 | 168 | .neon-button::after { 169 | content: ""; 170 | position: absolute; 171 | box-shadow: 0 0 2em 0.5em var(--clr-neon); 172 | opacity: 0; 173 | background-color: var(--clr-neon); 174 | z-index: -1; 175 | transition: opacity 100ms linear; 176 | } 177 | 178 | .neon-button:hover, 179 | .neon-button:focus { 180 | color: white; 181 | text-shadow: none; 182 | background: rgb(55, 7, 101) 183 | } 184 | 185 | .neon-button:hover::before, 186 | .neon-button:focus::before { 187 | opacity: 1; 188 | } 189 | .neon-button:hover::after, 190 | .neon-button:focus::after { 191 | opacity: 1; 192 | } 193 | 194 | .uri-box{ 195 | padding: 10px; 196 | width: 25em; 197 | min-height: 1em; 198 | position: absolute; 199 | background: rgb(219, 188, 228); 200 | border-radius: 10px; 201 | border: solid 2px #df29df; 202 | box-shadow: 1px 3px 10px #2583b6; 203 | color: rgb(230, 225, 225); 204 | font-size: 2.5vh; 205 | margin: 25%; 206 | z-index: 1; 207 | display: flex; 208 | flex-direction: column; 209 | 210 | #submitBtn{ 211 | margin:0; 212 | font-size: 2vh; 213 | margin: 1%; 214 | width: 20%; 215 | display: inline-block; 216 | } 217 | } 218 | 219 | .uriButtons { 220 | display: flex; 221 | justify-content: space-around; 222 | } 223 | 224 | .mainDisplayContainer{ 225 | display:flex; 226 | flex-direction: column; 227 | width: 100%; 228 | z-index: -1; 229 | } 230 | 231 | .mainDisplayWrapper{ 232 | display: flex; 233 | margin-top: 2vh; 234 | gap: 2vh; 235 | min-height: 100vh; 236 | } 237 | 238 | .treeDiv{ 239 | display: flex; 240 | width: 100%; 241 | height: 100%; 242 | background: rgba(255, 255, 255, 0.05); 243 | 244 | } 245 | 246 | .resolverDiv{ 247 | display: flex; 248 | padding-left: 2vh; 249 | width: 100%; 250 | height: 100%; 251 | max-height: 100vh; 252 | background: rgba(255, 255, 255, 0.05); 253 | } 254 | 255 | .javascript{ 256 | font-size: 1.0vh; 257 | } 258 | 259 | .linkStyler{ 260 | text-decoration: none; 261 | color: hsl(317 100% 54%); 262 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | // "incremental": true, /* Enable incremental compilation */ 5 | "target": "es2017", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 6 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 7 | "lib": [ 8 | "dom", 9 | "es2015", 10 | "es2016", 11 | "es2017" 12 | ], /* Specify library files to be included in the compilation. */ 13 | "allowJs": true, /* Allow javascript files to be compiled. */ 14 | // "checkJs": true, /* Report errors in .js files. */ 15 | "jsx": "react", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 16 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 17 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 18 | "sourceMap": true, /* Generates corresponding '.map' file. */ 19 | // "outFile": "./", /* Concatenate and emit output to single file. */ 20 | "outDir": "./dist", /* Redirect output structure to the directory. */ 21 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 22 | // "composite": true, /* Enable project compilation */ 23 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 24 | // "removeComments": true, /* Do not emit comments to output. */ 25 | // "noEmit": true, /* Do not emit outputs. */ 26 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 27 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 28 | "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 29 | // "skipLibCheck": true, 30 | /* Strict Type-Checking Options */ 31 | "strict": true, /* Enable all strict type-checking options. */ 32 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 33 | // "strictNullChecks": true, /* Enable strict null checks. */ 34 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 35 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 36 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 37 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 38 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 39 | 40 | /* Additional Checks */ 41 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 42 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 43 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 44 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 45 | 46 | /* Module Resolution Options */ 47 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 48 | // "resolveJsonModule": true, 49 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 50 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 51 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 52 | // "typeRoots": [], /* List of folders to include type definitions from. */ 53 | // "types": [], /* Type declaration files to be included in compilation. */ 54 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 55 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 56 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 57 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 58 | 59 | /* Source Map Options */ 60 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 61 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 62 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 63 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 64 | 65 | /* Experimental Options */ 66 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 67 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 68 | 69 | /* Advanced Options */ 70 | // "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 71 | }, 72 | } -------------------------------------------------------------------------------- /server/SQLConversion/resolverStringMaker.ts: -------------------------------------------------------------------------------- 1 | import { Column, Table } from "../types/DBResponseTypes"; 2 | import { SQLConversionHelpers } from './SQLConversionHelpers'; 3 | const { checkIsTableJoin, inObjectTypeCase, queryPluralCase, querySingularCase } = SQLConversionHelpers; 4 | 5 | 6 | export const resolverStringMaker = { 7 | generateResolverString(tables: Table[]): string { 8 | const resolver = tables.reduce((acc: { [key: string]: { [key: string]: string } }, curr: Table) => { 9 | const { tablename, columns } = curr; 10 | if (!checkIsTableJoin(columns)) { 11 | acc["Query"] = makeQueryString(acc["Query"], curr); 12 | acc["Mutation"] = makeMutationString(acc["Mutation"], curr); 13 | const tab = inObjectTypeCase(tablename); 14 | if (!acc.hasOwnProperty(tab)) { 15 | acc[tab] = {}; 16 | } 17 | for (let i = 0; i < columns.length; i++) { 18 | const { constraint_type, column_name, primary_table, primary_column } = columns[i]; 19 | if (constraint_type === 'FOREIGN KEY' && primary_table && primary_column) { 20 | acc[tab] = makeTypeString(acc[tab], column_name, primary_table, primary_column); 21 | } 22 | } 23 | } 24 | else { 25 | const foreignKeys: Column[] = [] 26 | for (let i = 0; i < columns.length; i++) { 27 | const { constraint_type } = columns[i]; 28 | if (constraint_type === 'FOREIGN KEY') { 29 | foreignKeys.push(columns[i]); 30 | } 31 | } 32 | foreignKeys.forEach((col: Column, idx: number, array: Column[]) => { 33 | const other = array[1 - idx]; 34 | const tablename = other.primary_table; 35 | const column_name = other.primary_column; 36 | const { primary_table, primary_column } = col; 37 | if (primary_table && primary_column && tablename && column_name) { 38 | const tab = inObjectTypeCase(tablename); 39 | if (!acc.hasOwnProperty(tab)) { 40 | acc[tab] = {}; 41 | } 42 | acc[tab] = makeTypeString(acc[tab], primary_column, primary_table, column_name); 43 | } 44 | }); 45 | } 46 | 47 | 48 | return acc; 49 | }, { 50 | Query: {}, 51 | Mutation: {}, 52 | }); 53 | 54 | let resolverString = ''; 55 | for (const [key, value] of Object.entries(resolver)) { 56 | resolverString += `${key}: {\n`; 57 | for (const [item, string] of Object.entries(value)) { 58 | resolverString += ` ${item}: ${string},\n` 59 | } 60 | resolverString += `},\n` 61 | } 62 | return resolverString; 63 | } 64 | } 65 | 66 | function makeQueryString(queryObj: { [key: string]: string }, table: Table): { [key: string]: string } { 67 | const { tablename, columns } = table; 68 | 69 | queryObj[queryPluralCase(tablename)] = `async () => { 70 | try { 71 | const query = \`SELECT * FROM ${tablename}\`; 72 | const result = await db.query(query); 73 | return result.rows; 74 | } catch (err) { 75 | /* INSERT YOUR ERROR HANDLING HERE */ 76 | } 77 | }` 78 | 79 | for (let i = 0; i < columns.length; i++) { 80 | const { constraint_type, column_name } = columns[i]; 81 | if (constraint_type === "PRIMARY KEY") { 82 | // person, film, planet, etc. 83 | queryObj[querySingularCase(tablename)] = `async (parent: any, args: { [key: string]: any }) => { 84 | try { 85 | const query = \`SELECT * FROM ${tablename} WHERE ${column_name} = $1\`; 86 | const result = await db.query(query, [args[\"${column_name}\"].toString()]); 87 | return result.rows[0]; 88 | } catch (err) { 89 | /* INSERT YOUR ERROR HANDLING HERE */ 90 | } 91 | }` 92 | break; 93 | } 94 | } 95 | 96 | return queryObj; 97 | } 98 | 99 | function makeMutationString(mutationObj: { [key: string]: string }, table: Table): { [key: string]: string } { 100 | 101 | const { tablename, columns } = table; 102 | 103 | const col_array: string[] = []; 104 | const val_array: string[] = []; 105 | const param_array: string[] = []; 106 | let primary_key: string = ''; 107 | // loop through all columnes and get the right columns for add and for update 108 | for (let i = 0; i < columns.length; i++) { 109 | const { column_name, constraint_type } = columns[i]; 110 | if (constraint_type !== 'PRIMARY KEY') { 111 | col_array.push(column_name) 112 | param_array.push(`$${col_array.length}`); 113 | val_array.push(`args[\"${column_name}\"]`) 114 | } 115 | else { 116 | primary_key = column_name; 117 | } 118 | } 119 | 120 | mutationObj[`add${inObjectTypeCase(tablename)}`] = `async (parent: any, args: { [key: string]: any }) => { 121 | try { 122 | const query = \`INSERT INTO ${tablename}(${col_array.join()}) VALUES (${param_array.join()}) RETURNING *\`; 123 | const result = await db.query(query, [${val_array}]); 124 | return result.rows[0]; 125 | } catch (err) { 126 | /* INSERT YOUR ERROR HANDLING HERE */ 127 | } 128 | }` 129 | 130 | const update_array = col_array.reduce((acc: string[], curr: string, i: number) => { 131 | acc.push(`${curr} = $${i + 1}`) 132 | return acc; 133 | }, []) 134 | val_array.push(`args[\"${primary_key}\"]`); 135 | 136 | mutationObj[`update${inObjectTypeCase(tablename)}`] = `async (parent: any, args: { [key: string]: any }) => { 137 | try { 138 | const query = \`UPDATE ${tablename} SET ${update_array.join()} WHERE ${primary_key} = $${val_array.length} RETURNING *\`; 139 | const result = await db.query(query, [${val_array}]); 140 | return result.rows[0]; 141 | } catch (err) { 142 | /* INSERT YOUR ERROR HANDLING HERE */ 143 | } 144 | }` 145 | 146 | return mutationObj; 147 | } 148 | 149 | function makeTypeString(typeObj: { [key: string]: any }, column_name: string, primary_table: string, primary_column: string): { [key: string]: string } { 150 | typeObj[primary_table] = `async (parent: any) => { 151 | try { 152 | const query = \`SELECT * FROM ${primary_table} WHERE ${primary_column} = $1\`; 153 | const result = await db.query(query, [parent[\"${column_name}\"]]); 154 | return result.rows[0]; 155 | } catch(err) { 156 | /* INSERT ERROR HANDLING HERE */ 157 | } 158 | }` 159 | return typeObj; 160 | } -------------------------------------------------------------------------------- /src/components/MainPageNavBar.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Navigate } from 'react-router-dom'; 3 | import Box from '@mui/material/Box'; 4 | import Toolbar from '@mui/material/Toolbar'; 5 | import IconButton from '@mui/material/IconButton'; 6 | import Typography from '@mui/material/Typography'; 7 | import Menu from '@mui/material/Menu'; 8 | import MenuIcon from '@mui/icons-material/Menu'; 9 | import Container from '@mui/material/Container'; 10 | import Avatar from '@mui/material/Avatar'; 11 | import Button from '@mui/material/Button'; 12 | import Tooltip from '@mui/material/Tooltip'; 13 | import MenuItem from '@mui/material/MenuItem'; 14 | import LogoutIcon from '@mui/icons-material/Logout'; 15 | import MariposaLogo from '../assets/MariposaLogo.svg' 16 | 17 | const pages = ['URI', 'Sandbox']; 18 | const settings = ['Logout']; 19 | 20 | const ResponsiveAppBar = (props: any) => { 21 | const [anchorEl, setAnchorEl] = useState(null); 22 | const [redirect, setRedirect] = useState(false); 23 | 24 | const open = Boolean(anchorEl); 25 | const id = open ? 'simple-popper' : undefined; 26 | 27 | const [anchorElNav, setAnchorElNav] = useState(null); 28 | const [anchorElUser, setAnchorElUser] = useState(null); 29 | 30 | const handleOpenNavMenu = (event: React.MouseEvent) => { 31 | setAnchorElNav(event.currentTarget); 32 | }; 33 | const handleOpenUserMenu = (event: React.MouseEvent) => { 34 | setAnchorElUser(event.currentTarget); 35 | }; 36 | 37 | const handleCloseNavMenu = () => { 38 | setRedirect(true); 39 | }; 40 | 41 | const handleSandbox = () => { 42 | window.open('/graphql', '_blank') 43 | } 44 | 45 | const handleCloseUserMenu = () => { 46 | setAnchorElUser(null); 47 | }; 48 | 49 | const handleUriDisplay = () => { 50 | if (props.uriBtnClose === true) { 51 | props.setHandleUriBtnClose(false) 52 | } 53 | } 54 | 55 | return ( 56 |
57 | 58 | 59 | 60 | 61 | 69 | 70 | 71 | 89 | {pages.map((page) => ( 90 | page === "URI" ? 91 |
92 | 95 |
: 96 | page === "Sandbox" ? 97 |
98 | 101 |
: 102 | 105 | ))} 106 |
107 |
108 | 109 | {pages.map((page) => ( 110 | page === "URI" ? 111 |
112 | 115 |
: 116 | page === "Sandbox" ? 117 |
118 | 121 |
: 122 | 125 | ))} 126 |
127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 150 | {settings.map((setting) => ( 151 | 152 | {setting} 153 | {redirect && ()} 154 | 155 | ))} 156 | 157 | 158 |
159 |
160 |
161 | ); 162 | }; 163 | export default ResponsiveAppBar; -------------------------------------------------------------------------------- /src/components/formComponents/WebRegisterForm.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import {Link, Navigate} from 'react-router-dom'; 3 | import { useDispatch, useSelector } from "react-redux"; 4 | import { Formik, Field, Form, ErrorMessage } from "formik"; 5 | import * as Yup from "yup"; 6 | 7 | import { register } from "../../slices/authentication"; 8 | import { clearMessage } from "../../slices/messages"; 9 | 10 | export const WebRegisterForm = (props) => { 11 | const {setFormToDisplay} = props; 12 | 13 | const [successful, setSuccessful] = useState(false); 14 | const [redirect, setRedirect] = useState(false); 15 | 16 | const { message } = useSelector((state) => state.message); 17 | const dispatch = useDispatch(); 18 | 19 | useEffect(() => { 20 | dispatch(clearMessage()); 21 | }, [dispatch]); 22 | 23 | const initialValues = { 24 | firstname: "", 25 | lastname: "", 26 | username: "", 27 | email: "", 28 | password: "", 29 | }; 30 | 31 | const validationSchema = Yup.object().shape({ 32 | firstname: Yup.string().required("This field is required!"), 33 | lastname: Yup.string().required("This field is required!"), 34 | username: Yup.string() 35 | .test( 36 | "len", 37 | "The username must be between 8 and 20 characters.", 38 | (val) => 39 | val && 40 | val.toString().length >= 1 && 41 | val.toString().length <= 20 42 | ) 43 | .required("This field is required!"), 44 | email: Yup.string() 45 | .email("This is not a valid email.") 46 | .required("This field is required!"), 47 | password: Yup.string() 48 | .test( 49 | "len", 50 | "The password must be between 8 and 40 characters.", 51 | (val) => 52 | val && 53 | val.toString().length >= 1 && 54 | val.toString().length <= 40 55 | ) 56 | .required("This field is required!"), 57 | }); 58 | 59 | const handleRegister = (formValue) => { 60 | const {firstname, lastname, username, email, password } = formValue; 61 | setSuccessful(false); 62 | 63 | dispatch( 64 | //register thunk 65 | register({firstname, lastname, username, email, password})) 66 | .unwrap() 67 | .then(() => { 68 | setSuccessful(true); 69 | }) 70 | .catch(() => { 71 | setSuccessful(false); 72 | }); 73 | }; 74 | 75 | const handleFormDisplay = () => setFormToDisplay('login'); 76 | 77 | return ( 78 |
79 | 80 | {!successful && ( 81 |
82 | 83 | 88 |
89 |

90 | Register 91 |

92 | 93 |
94 | 95 | 100 | 104 |
105 | 106 |
107 | 108 | 114 | 118 |
119 | 120 |
121 | 122 | 128 | 133 |
134 | 135 |
136 | 137 | 142 | 147 |
148 | 149 |
150 | 151 | 157 | 162 |
163 | 164 |
165 | 166 |
167 | 168 |

169 | Already have an account? 170 |

171 |
172 |
173 |
174 | )} 175 | 176 | 177 | { 178 | message && ( 179 |
180 | 181 |
182 | {message} 183 |
184 | 185 |
186 | 187 |
188 | ) 189 | } 190 | {successful && setTimeout(() => setFormToDisplay('login'), 1000)} 191 |
192 | ); 193 | }; 194 | -------------------------------------------------------------------------------- /server/types/dummyTables.ts: -------------------------------------------------------------------------------- 1 | export const tables = [ 2 | { 3 | "tablename": "films", 4 | "columns": [ 5 | { 6 | "column_id": 1, 7 | "column_name": "_id", 8 | "data_type": "integer", 9 | "is_nullable": "NO", 10 | "is_updatable": "YES", 11 | "column_default": "nextval('films__id_seq'::regclass)", 12 | "constraint_name": "films_pk", 13 | "constraint_type": "PRIMARY KEY", 14 | "primary_table": null, 15 | "primary_column": null 16 | }, 17 | { 18 | "column_id": 2, 19 | "column_name": "title", 20 | "data_type": "character varying", 21 | "is_nullable": "NO", 22 | "is_updatable": "YES", 23 | "column_default": null, 24 | "constraint_name": null, 25 | "constraint_type": null, 26 | "primary_table": null, 27 | "primary_column": null 28 | }, 29 | { 30 | "column_id": 3, 31 | "column_name": "episode_id", 32 | "data_type": "integer", 33 | "is_nullable": "NO", 34 | "is_updatable": "YES", 35 | "column_default": null, 36 | "constraint_name": null, 37 | "constraint_type": null, 38 | "primary_table": null, 39 | "primary_column": null 40 | }, 41 | { 42 | "column_id": 4, 43 | "column_name": "opening_crawl", 44 | "data_type": "character varying", 45 | "is_nullable": "NO", 46 | "is_updatable": "YES", 47 | "column_default": null, 48 | "constraint_name": null, 49 | "constraint_type": null, 50 | "primary_table": null, 51 | "primary_column": null 52 | }, 53 | { 54 | "column_id": 5, 55 | "column_name": "director", 56 | "data_type": "character varying", 57 | "is_nullable": "NO", 58 | "is_updatable": "YES", 59 | "column_default": null, 60 | "constraint_name": null, 61 | "constraint_type": null, 62 | "primary_table": null, 63 | "primary_column": null 64 | }, 65 | { 66 | "column_id": 6, 67 | "column_name": "producer", 68 | "data_type": "character varying", 69 | "is_nullable": "NO", 70 | "is_updatable": "YES", 71 | "column_default": null, 72 | "constraint_name": null, 73 | "constraint_type": null, 74 | "primary_table": null, 75 | "primary_column": null 76 | }, 77 | { 78 | "column_id": 7, 79 | "column_name": "release_date", 80 | "data_type": "date", 81 | "is_nullable": "NO", 82 | "is_updatable": "YES", 83 | "column_default": null, 84 | "constraint_name": null, 85 | "constraint_type": null, 86 | "primary_table": null, 87 | "primary_column": null 88 | } 89 | ] 90 | }, 91 | { 92 | "tablename": "people", 93 | "columns": [ 94 | { 95 | "column_id": 1, 96 | "column_name": "_id", 97 | "data_type": "integer", 98 | "is_nullable": "NO", 99 | "is_updatable": "YES", 100 | "column_default": "nextval('people__id_seq'::regclass)", 101 | "constraint_name": "people_pk", 102 | "constraint_type": "PRIMARY KEY", 103 | "primary_table": null, 104 | "primary_column": null 105 | }, 106 | { 107 | "column_id": 2, 108 | "column_name": "name", 109 | "data_type": "character varying", 110 | "is_nullable": "NO", 111 | "is_updatable": "YES", 112 | "column_default": null, 113 | "constraint_name": null, 114 | "constraint_type": null, 115 | "primary_table": null, 116 | "primary_column": null 117 | }, 118 | { 119 | "column_id": 3, 120 | "column_name": "mass", 121 | "data_type": "character varying", 122 | "is_nullable": "YES", 123 | "is_updatable": "YES", 124 | "column_default": null, 125 | "constraint_name": null, 126 | "constraint_type": null, 127 | "primary_table": null, 128 | "primary_column": null 129 | }, 130 | { 131 | "column_id": 4, 132 | "column_name": "hair_color", 133 | "data_type": "character varying", 134 | "is_nullable": "YES", 135 | "is_updatable": "YES", 136 | "column_default": null, 137 | "constraint_name": null, 138 | "constraint_type": null, 139 | "primary_table": null, 140 | "primary_column": null 141 | }, 142 | { 143 | "column_id": 5, 144 | "column_name": "skin_color", 145 | "data_type": "character varying", 146 | "is_nullable": "YES", 147 | "is_updatable": "YES", 148 | "column_default": null, 149 | "constraint_name": null, 150 | "constraint_type": null, 151 | "primary_table": null, 152 | "primary_column": null 153 | }, 154 | { 155 | "column_id": 6, 156 | "column_name": "eye_color", 157 | "data_type": "character varying", 158 | "is_nullable": "YES", 159 | "is_updatable": "YES", 160 | "column_default": null, 161 | "constraint_name": null, 162 | "constraint_type": null, 163 | "primary_table": null, 164 | "primary_column": null 165 | }, 166 | { 167 | "column_id": 7, 168 | "column_name": "birth_year", 169 | "data_type": "character varying", 170 | "is_nullable": "YES", 171 | "is_updatable": "YES", 172 | "column_default": null, 173 | "constraint_name": null, 174 | "constraint_type": null, 175 | "primary_table": null, 176 | "primary_column": null 177 | }, 178 | { 179 | "column_id": 8, 180 | "column_name": "gender", 181 | "data_type": "character varying", 182 | "is_nullable": "YES", 183 | "is_updatable": "YES", 184 | "column_default": null, 185 | "constraint_name": null, 186 | "constraint_type": null, 187 | "primary_table": null, 188 | "primary_column": null 189 | }, 190 | { 191 | "column_id": 9, 192 | "column_name": "species_id", 193 | "data_type": "bigint", 194 | "is_nullable": "YES", 195 | "is_updatable": "YES", 196 | "column_default": null, 197 | "constraint_name": "people_fk0", 198 | "constraint_type": "FOREIGN KEY", 199 | "primary_table": "species", 200 | "primary_column": "_id" 201 | }, 202 | { 203 | "column_id": 10, 204 | "column_name": "homeworld_id", 205 | "data_type": "bigint", 206 | "is_nullable": "YES", 207 | "is_updatable": "YES", 208 | "column_default": null, 209 | "constraint_name": "people_fk1", 210 | "constraint_type": "FOREIGN KEY", 211 | "primary_table": "planets", 212 | "primary_column": "_id" 213 | }, 214 | { 215 | "column_id": 11, 216 | "column_name": "height", 217 | "data_type": "integer", 218 | "is_nullable": "YES", 219 | "is_updatable": "YES", 220 | "column_default": null, 221 | "constraint_name": null, 222 | "constraint_type": null, 223 | "primary_table": null, 224 | "primary_column": null 225 | } 226 | ] 227 | }, 228 | { 229 | "tablename": "people_in_films", 230 | "columns": [ 231 | { 232 | "column_id": 1, 233 | "column_name": "_id", 234 | "data_type": "integer", 235 | "is_nullable": "NO", 236 | "is_updatable": "YES", 237 | "column_default": "nextval('people_in_films__id_seq'::regclass)", 238 | "constraint_name": "people_in_films_pk", 239 | "constraint_type": "PRIMARY KEY", 240 | "primary_table": null, 241 | "primary_column": null 242 | }, 243 | { 244 | "column_id": 2, 245 | "column_name": "person_id", 246 | "data_type": "bigint", 247 | "is_nullable": "NO", 248 | "is_updatable": "YES", 249 | "column_default": null, 250 | "constraint_name": "people_in_films_fk0", 251 | "constraint_type": "FOREIGN KEY", 252 | "primary_table": "people", 253 | "primary_column": "_id" 254 | }, 255 | { 256 | "column_id": 3, 257 | "column_name": "film_id", 258 | "data_type": "bigint", 259 | "is_nullable": "NO", 260 | "is_updatable": "YES", 261 | "column_default": null, 262 | "constraint_name": "people_in_films_fk1", 263 | "constraint_type": "FOREIGN KEY", 264 | "primary_table": "films", 265 | "primary_column": "_id" 266 | } 267 | ] 268 | }, 269 | { 270 | "tablename": "pilots", 271 | "columns": [ 272 | { 273 | "column_id": 1, 274 | "column_name": "_id", 275 | "data_type": "integer", 276 | "is_nullable": "NO", 277 | "is_updatable": "YES", 278 | "column_default": "nextval('pilots__id_seq'::regclass)", 279 | "constraint_name": "pilots_pk", 280 | "constraint_type": "PRIMARY KEY", 281 | "primary_table": null, 282 | "primary_column": null 283 | }, 284 | { 285 | "column_id": 2, 286 | "column_name": "person_id", 287 | "data_type": "bigint", 288 | "is_nullable": "NO", 289 | "is_updatable": "YES", 290 | "column_default": null, 291 | "constraint_name": "pilots_fk0", 292 | "constraint_type": "FOREIGN KEY", 293 | "primary_table": "people", 294 | "primary_column": "_id" 295 | }, 296 | { 297 | "column_id": 3, 298 | "column_name": "vessel_id", 299 | "data_type": "bigint", 300 | "is_nullable": "NO", 301 | "is_updatable": "YES", 302 | "column_default": null, 303 | "constraint_name": "pilots_fk1", 304 | "constraint_type": "FOREIGN KEY", 305 | "primary_table": "vessels", 306 | "primary_column": "_id" 307 | } 308 | ] 309 | }, 310 | { 311 | "tablename": "planets", 312 | "columns": [ 313 | { 314 | "column_id": 1, 315 | "column_name": "_id", 316 | "data_type": "integer", 317 | "is_nullable": "NO", 318 | "is_updatable": "YES", 319 | "column_default": "nextval('planets__id_seq'::regclass)", 320 | "constraint_name": "planets_pk", 321 | "constraint_type": "PRIMARY KEY", 322 | "primary_table": null, 323 | "primary_column": null 324 | }, 325 | { 326 | "column_id": 2, 327 | "column_name": "name", 328 | "data_type": "character varying", 329 | "is_nullable": "YES", 330 | "is_updatable": "YES", 331 | "column_default": null, 332 | "constraint_name": null, 333 | "constraint_type": null, 334 | "primary_table": null, 335 | "primary_column": null 336 | }, 337 | { 338 | "column_id": 3, 339 | "column_name": "rotation_period", 340 | "data_type": "integer", 341 | "is_nullable": "YES", 342 | "is_updatable": "YES", 343 | "column_default": null, 344 | "constraint_name": null, 345 | "constraint_type": null, 346 | "primary_table": null, 347 | "primary_column": null 348 | }, 349 | { 350 | "column_id": 4, 351 | "column_name": "orbital_period", 352 | "data_type": "integer", 353 | "is_nullable": "YES", 354 | "is_updatable": "YES", 355 | "column_default": null, 356 | "constraint_name": null, 357 | "constraint_type": null, 358 | "primary_table": null, 359 | "primary_column": null 360 | }, 361 | { 362 | "column_id": 5, 363 | "column_name": "diameter", 364 | "data_type": "integer", 365 | "is_nullable": "YES", 366 | "is_updatable": "YES", 367 | "column_default": null, 368 | "constraint_name": null, 369 | "constraint_type": null, 370 | "primary_table": null, 371 | "primary_column": null 372 | }, 373 | { 374 | "column_id": 6, 375 | "column_name": "climate", 376 | "data_type": "character varying", 377 | "is_nullable": "YES", 378 | "is_updatable": "YES", 379 | "column_default": null, 380 | "constraint_name": null, 381 | "constraint_type": null, 382 | "primary_table": null, 383 | "primary_column": null 384 | }, 385 | { 386 | "column_id": 7, 387 | "column_name": "gravity", 388 | "data_type": "character varying", 389 | "is_nullable": "YES", 390 | "is_updatable": "YES", 391 | "column_default": null, 392 | "constraint_name": null, 393 | "constraint_type": null, 394 | "primary_table": null, 395 | "primary_column": null 396 | }, 397 | { 398 | "column_id": 8, 399 | "column_name": "terrain", 400 | "data_type": "character varying", 401 | "is_nullable": "YES", 402 | "is_updatable": "YES", 403 | "column_default": null, 404 | "constraint_name": null, 405 | "constraint_type": null, 406 | "primary_table": null, 407 | "primary_column": null 408 | }, 409 | { 410 | "column_id": 9, 411 | "column_name": "surface_water", 412 | "data_type": "character varying", 413 | "is_nullable": "YES", 414 | "is_updatable": "YES", 415 | "column_default": null, 416 | "constraint_name": null, 417 | "constraint_type": null, 418 | "primary_table": null, 419 | "primary_column": null 420 | }, 421 | { 422 | "column_id": 10, 423 | "column_name": "population", 424 | "data_type": "bigint", 425 | "is_nullable": "YES", 426 | "is_updatable": "YES", 427 | "column_default": null, 428 | "constraint_name": null, 429 | "constraint_type": null, 430 | "primary_table": null, 431 | "primary_column": null 432 | } 433 | ] 434 | }, 435 | { 436 | "tablename": "planets_in_films", 437 | "columns": [ 438 | { 439 | "column_id": 1, 440 | "column_name": "_id", 441 | "data_type": "integer", 442 | "is_nullable": "NO", 443 | "is_updatable": "YES", 444 | "column_default": "nextval('planets_in_films__id_seq'::regclass)", 445 | "constraint_name": "planets_in_films_pk", 446 | "constraint_type": "PRIMARY KEY", 447 | "primary_table": null, 448 | "primary_column": null 449 | }, 450 | { 451 | "column_id": 2, 452 | "column_name": "film_id", 453 | "data_type": "bigint", 454 | "is_nullable": "NO", 455 | "is_updatable": "YES", 456 | "column_default": null, 457 | "constraint_name": "planets_in_films_fk0", 458 | "constraint_type": "FOREIGN KEY", 459 | "primary_table": "films", 460 | "primary_column": "_id" 461 | }, 462 | { 463 | "column_id": 3, 464 | "column_name": "planet_id", 465 | "data_type": "bigint", 466 | "is_nullable": "NO", 467 | "is_updatable": "YES", 468 | "column_default": null, 469 | "constraint_name": "planets_in_films_fk1", 470 | "constraint_type": "FOREIGN KEY", 471 | "primary_table": "planets", 472 | "primary_column": "_id" 473 | } 474 | ] 475 | }, 476 | { 477 | "tablename": "species", 478 | "columns": [ 479 | { 480 | "column_id": 1, 481 | "column_name": "_id", 482 | "data_type": "integer", 483 | "is_nullable": "NO", 484 | "is_updatable": "YES", 485 | "column_default": "nextval('species__id_seq'::regclass)", 486 | "constraint_name": "species_pk", 487 | "constraint_type": "PRIMARY KEY", 488 | "primary_table": null, 489 | "primary_column": null 490 | }, 491 | { 492 | "column_id": 2, 493 | "column_name": "name", 494 | "data_type": "character varying", 495 | "is_nullable": "NO", 496 | "is_updatable": "YES", 497 | "column_default": null, 498 | "constraint_name": null, 499 | "constraint_type": null, 500 | "primary_table": null, 501 | "primary_column": null 502 | }, 503 | { 504 | "column_id": 3, 505 | "column_name": "classification", 506 | "data_type": "character varying", 507 | "is_nullable": "YES", 508 | "is_updatable": "YES", 509 | "column_default": null, 510 | "constraint_name": null, 511 | "constraint_type": null, 512 | "primary_table": null, 513 | "primary_column": null 514 | }, 515 | { 516 | "column_id": 4, 517 | "column_name": "average_height", 518 | "data_type": "character varying", 519 | "is_nullable": "YES", 520 | "is_updatable": "YES", 521 | "column_default": null, 522 | "constraint_name": null, 523 | "constraint_type": null, 524 | "primary_table": null, 525 | "primary_column": null 526 | }, 527 | { 528 | "column_id": 5, 529 | "column_name": "average_lifespan", 530 | "data_type": "character varying", 531 | "is_nullable": "YES", 532 | "is_updatable": "YES", 533 | "column_default": null, 534 | "constraint_name": null, 535 | "constraint_type": null, 536 | "primary_table": null, 537 | "primary_column": null 538 | }, 539 | { 540 | "column_id": 6, 541 | "column_name": "hair_colors", 542 | "data_type": "character varying", 543 | "is_nullable": "YES", 544 | "is_updatable": "YES", 545 | "column_default": null, 546 | "constraint_name": null, 547 | "constraint_type": null, 548 | "primary_table": null, 549 | "primary_column": null 550 | }, 551 | { 552 | "column_id": 7, 553 | "column_name": "skin_colors", 554 | "data_type": "character varying", 555 | "is_nullable": "YES", 556 | "is_updatable": "YES", 557 | "column_default": null, 558 | "constraint_name": null, 559 | "constraint_type": null, 560 | "primary_table": null, 561 | "primary_column": null 562 | }, 563 | { 564 | "column_id": 8, 565 | "column_name": "eye_colors", 566 | "data_type": "character varying", 567 | "is_nullable": "YES", 568 | "is_updatable": "YES", 569 | "column_default": null, 570 | "constraint_name": null, 571 | "constraint_type": null, 572 | "primary_table": null, 573 | "primary_column": null 574 | }, 575 | { 576 | "column_id": 9, 577 | "column_name": "language", 578 | "data_type": "character varying", 579 | "is_nullable": "YES", 580 | "is_updatable": "YES", 581 | "column_default": null, 582 | "constraint_name": null, 583 | "constraint_type": null, 584 | "primary_table": null, 585 | "primary_column": null 586 | }, 587 | { 588 | "column_id": 10, 589 | "column_name": "homeworld_id", 590 | "data_type": "bigint", 591 | "is_nullable": "YES", 592 | "is_updatable": "YES", 593 | "column_default": null, 594 | "constraint_name": "species_fk0", 595 | "constraint_type": "FOREIGN KEY", 596 | "primary_table": "planets", 597 | "primary_column": "_id" 598 | } 599 | ] 600 | }, 601 | { 602 | "tablename": "species_in_films", 603 | "columns": [ 604 | { 605 | "column_id": 1, 606 | "column_name": "_id", 607 | "data_type": "integer", 608 | "is_nullable": "NO", 609 | "is_updatable": "YES", 610 | "column_default": "nextval('species_in_films__id_seq'::regclass)", 611 | "constraint_name": "species_in_films_pk", 612 | "constraint_type": "PRIMARY KEY", 613 | "primary_table": null, 614 | "primary_column": null 615 | }, 616 | { 617 | "column_id": 2, 618 | "column_name": "film_id", 619 | "data_type": "bigint", 620 | "is_nullable": "NO", 621 | "is_updatable": "YES", 622 | "column_default": null, 623 | "constraint_name": "species_in_films_fk0", 624 | "constraint_type": "FOREIGN KEY", 625 | "primary_table": "films", 626 | "primary_column": "_id" 627 | }, 628 | { 629 | "column_id": 3, 630 | "column_name": "species_id", 631 | "data_type": "bigint", 632 | "is_nullable": "NO", 633 | "is_updatable": "YES", 634 | "column_default": null, 635 | "constraint_name": "species_in_films_fk1", 636 | "constraint_type": "FOREIGN KEY", 637 | "primary_table": "species", 638 | "primary_column": "_id" 639 | } 640 | ] 641 | }, 642 | { 643 | "tablename": "starship_specs", 644 | "columns": [ 645 | { 646 | "column_id": 1, 647 | "column_name": "_id", 648 | "data_type": "integer", 649 | "is_nullable": "NO", 650 | "is_updatable": "YES", 651 | "column_default": "nextval('starship_specs__id_seq'::regclass)", 652 | "constraint_name": "starship_specs_pk", 653 | "constraint_type": "PRIMARY KEY", 654 | "primary_table": null, 655 | "primary_column": null 656 | }, 657 | { 658 | "column_id": 2, 659 | "column_name": "hyperdrive_rating", 660 | "data_type": "character varying", 661 | "is_nullable": "YES", 662 | "is_updatable": "YES", 663 | "column_default": null, 664 | "constraint_name": null, 665 | "constraint_type": null, 666 | "primary_table": null, 667 | "primary_column": null 668 | }, 669 | { 670 | "column_id": 3, 671 | "column_name": "MGLT", 672 | "data_type": "character varying", 673 | "is_nullable": "YES", 674 | "is_updatable": "YES", 675 | "column_default": null, 676 | "constraint_name": null, 677 | "constraint_type": null, 678 | "primary_table": null, 679 | "primary_column": null 680 | }, 681 | { 682 | "column_id": 4, 683 | "column_name": "vessel_id", 684 | "data_type": "bigint", 685 | "is_nullable": "NO", 686 | "is_updatable": "YES", 687 | "column_default": null, 688 | "constraint_name": "starship_specs_fk0", 689 | "constraint_type": "FOREIGN KEY", 690 | "primary_table": "vessels", 691 | "primary_column": "_id" 692 | } 693 | ] 694 | }, 695 | { 696 | "tablename": "vessels", 697 | "columns": [ 698 | { 699 | "column_id": 1, 700 | "column_name": "_id", 701 | "data_type": "integer", 702 | "is_nullable": "NO", 703 | "is_updatable": "YES", 704 | "column_default": "nextval('vessels__id_seq'::regclass)", 705 | "constraint_name": "vessels_pk", 706 | "constraint_type": "PRIMARY KEY", 707 | "primary_table": null, 708 | "primary_column": null 709 | }, 710 | { 711 | "column_id": 2, 712 | "column_name": "name", 713 | "data_type": "character varying", 714 | "is_nullable": "NO", 715 | "is_updatable": "YES", 716 | "column_default": null, 717 | "constraint_name": null, 718 | "constraint_type": null, 719 | "primary_table": null, 720 | "primary_column": null 721 | }, 722 | { 723 | "column_id": 3, 724 | "column_name": "manufacturer", 725 | "data_type": "character varying", 726 | "is_nullable": "YES", 727 | "is_updatable": "YES", 728 | "column_default": null, 729 | "constraint_name": null, 730 | "constraint_type": null, 731 | "primary_table": null, 732 | "primary_column": null 733 | }, 734 | { 735 | "column_id": 4, 736 | "column_name": "model", 737 | "data_type": "character varying", 738 | "is_nullable": "YES", 739 | "is_updatable": "YES", 740 | "column_default": null, 741 | "constraint_name": null, 742 | "constraint_type": null, 743 | "primary_table": null, 744 | "primary_column": null 745 | }, 746 | { 747 | "column_id": 5, 748 | "column_name": "vessel_type", 749 | "data_type": "character varying", 750 | "is_nullable": "NO", 751 | "is_updatable": "YES", 752 | "column_default": null, 753 | "constraint_name": null, 754 | "constraint_type": null, 755 | "primary_table": null, 756 | "primary_column": null 757 | }, 758 | { 759 | "column_id": 6, 760 | "column_name": "vessel_class", 761 | "data_type": "character varying", 762 | "is_nullable": "NO", 763 | "is_updatable": "YES", 764 | "column_default": null, 765 | "constraint_name": null, 766 | "constraint_type": null, 767 | "primary_table": null, 768 | "primary_column": null 769 | }, 770 | { 771 | "column_id": 7, 772 | "column_name": "cost_in_credits", 773 | "data_type": "bigint", 774 | "is_nullable": "YES", 775 | "is_updatable": "YES", 776 | "column_default": null, 777 | "constraint_name": null, 778 | "constraint_type": null, 779 | "primary_table": null, 780 | "primary_column": null 781 | }, 782 | { 783 | "column_id": 8, 784 | "column_name": "length", 785 | "data_type": "character varying", 786 | "is_nullable": "YES", 787 | "is_updatable": "YES", 788 | "column_default": null, 789 | "constraint_name": null, 790 | "constraint_type": null, 791 | "primary_table": null, 792 | "primary_column": null 793 | }, 794 | { 795 | "column_id": 9, 796 | "column_name": "max_atmosphering_speed", 797 | "data_type": "character varying", 798 | "is_nullable": "YES", 799 | "is_updatable": "YES", 800 | "column_default": null, 801 | "constraint_name": null, 802 | "constraint_type": null, 803 | "primary_table": null, 804 | "primary_column": null 805 | }, 806 | { 807 | "column_id": 10, 808 | "column_name": "crew", 809 | "data_type": "integer", 810 | "is_nullable": "YES", 811 | "is_updatable": "YES", 812 | "column_default": null, 813 | "constraint_name": null, 814 | "constraint_type": null, 815 | "primary_table": null, 816 | "primary_column": null 817 | }, 818 | { 819 | "column_id": 11, 820 | "column_name": "passengers", 821 | "data_type": "integer", 822 | "is_nullable": "YES", 823 | "is_updatable": "YES", 824 | "column_default": null, 825 | "constraint_name": null, 826 | "constraint_type": null, 827 | "primary_table": null, 828 | "primary_column": null 829 | }, 830 | { 831 | "column_id": 12, 832 | "column_name": "cargo_capacity", 833 | "data_type": "character varying", 834 | "is_nullable": "YES", 835 | "is_updatable": "YES", 836 | "column_default": null, 837 | "constraint_name": null, 838 | "constraint_type": null, 839 | "primary_table": null, 840 | "primary_column": null 841 | }, 842 | { 843 | "column_id": 13, 844 | "column_name": "consumables", 845 | "data_type": "character varying", 846 | "is_nullable": "YES", 847 | "is_updatable": "YES", 848 | "column_default": null, 849 | "constraint_name": null, 850 | "constraint_type": null, 851 | "primary_table": null, 852 | "primary_column": null 853 | } 854 | ] 855 | }, 856 | { 857 | "tablename": "vessels_in_films", 858 | "columns": [ 859 | { 860 | "column_id": 1, 861 | "column_name": "_id", 862 | "data_type": "integer", 863 | "is_nullable": "NO", 864 | "is_updatable": "YES", 865 | "column_default": "nextval('vessels_in_films__id_seq'::regclass)", 866 | "constraint_name": "vessels_in_films_pk", 867 | "constraint_type": "PRIMARY KEY", 868 | "primary_table": null, 869 | "primary_column": null 870 | }, 871 | { 872 | "column_id": 2, 873 | "column_name": "vessel_id", 874 | "data_type": "bigint", 875 | "is_nullable": "NO", 876 | "is_updatable": "YES", 877 | "column_default": null, 878 | "constraint_name": "vessels_in_films_fk0", 879 | "constraint_type": "FOREIGN KEY", 880 | "primary_table": "vessels", 881 | "primary_column": "_id" 882 | }, 883 | { 884 | "column_id": 3, 885 | "column_name": "film_id", 886 | "data_type": "bigint", 887 | "is_nullable": "NO", 888 | "is_updatable": "YES", 889 | "column_default": null, 890 | "constraint_name": "vessels_in_films_fk1", 891 | "constraint_type": "FOREIGN KEY", 892 | "primary_table": "films", 893 | "primary_column": "_id" 894 | } 895 | ] 896 | } 897 | ] 898 | --------------------------------------------------------------------------------