├── .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 |
8 | window.open('https://github.com/oslabs-beta/mariposa')
9 | }
10 | >
11 | Documentation
12 |
13 |
14 | The Team
15 |
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 | 
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 | 
22 |
23 | Input URI, visualize your data, and copy your GQL Schema!
24 |
25 | 
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 | setResolver(false)}>TypeDefs
39 | setResolver(true)}>Resolvers
40 | { navigator.clipboard.writeText((resolver) ? props.schema : props.text) }}>
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 | Back
48 |
49 |
50 |
51 | }
52 |
53 |
54 |
55 |
56 | {
57 | !changeToFormDisplay && (
58 |
59 |
A Restful API to GraphQL Migration Tool
60 |
61 |
62 | Login
63 |
64 |
65 | Free demo
66 |
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 |
setHandleUriBtnClose(true)}>Close
65 |
setUriString(e.target.value)}/>
66 |
67 | handleClick(uriString, e)}>Submit
68 | handleClick('', e)}>Sample Data
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 |
18 | Back
19 |
20 |
Meet the Mariposa Team
21 |
22 |
23 |
24 |
Adam Berri
25 |
26 |
{
27 | window.open('https://www.linkedin.com/in/adam-berri-606782144/');
28 | }
29 | } id={"linkedin"}>
30 |
31 |
32 |
33 |
{
34 | window.open('https://github.com/AdamBerri');
35 | }
36 | } id={"github"}>
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
Alex Barbazan
45 |
46 |
{
47 | window.open('https://www.linkedin.com/in/alexander-barbazan-b1041662/');
48 | }
49 | } id={"linkedin"}>
50 |
51 |
52 |
53 |
{
54 | window.open('https://github.com/agbarbazan');
55 | }
56 | } id={"github"}>
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
James Maguire
65 |
66 |
{
67 | window.open('https://www.linkedin.com/in/james-maguire-0b267812a/');
68 | }
69 | } id={"linkedin"}>
70 |
71 |
72 |
73 |
{
74 | window.open('https://github.com/jwmaguire15');
75 | }
76 | } id={"github"}>
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
Mark Dolan
85 |
86 |
{
87 | window.open('https://www.linkedin.com/in/mark-dolan-86459819/');
88 | }
89 | } id={"linkedin"}>
90 |
91 |
92 |
93 |
{
94 | window.open('https://github.com/markdolan1');
95 | }
96 | } id={"github"}>
97 |
98 |
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 |
116 |
117 |
118 | Don't have account? Register
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 |
107 |
108 |
109 | {pages.map((page) => (
110 | page === "URI" ?
111 |
112 |
113 | URI
114 |
115 |
:
116 | page === "Sandbox" ?
117 |
118 |
119 | SANDBOX
120 |
121 |
:
122 |
123 | {page}
124 |
125 | ))}
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
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 |
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 |
--------------------------------------------------------------------------------